Auto-indexing service and GraphQL API for AT Protocol Records quickslice.slices.network/
atproto gleam graphql
at main 143 lines 5.3 kB view raw
1-module(jwt_ffi). 2-export([sign_jwt/3, derive_public_did_key/1, extract_public_key_coords/1]). 3 4%% Sign a JWT with ES256 using a multibase-encoded private key 5%% Args: ClaimsJson (binary), Kid (binary), PrivateKeyMultibase (binary) 6%% Returns: {ok, JWT} | {error, Reason} 7sign_jwt(ClaimsJson, Kid, PrivateKeyMultibase) -> 8 try 9 %% Parse multibase key (z-prefixed base58btc) 10 case parse_multibase_key(PrivateKeyMultibase) of 11 {ok, PrivateKeyBytes} -> 12 %% Generate EC key from private key bytes 13 {PubX, PubY} = derive_public_coords(PrivateKeyBytes), 14 15 %% Build JWK for signing 16 JWK = jose_jwk:from_map(#{ 17 <<"kty">> => <<"EC">>, 18 <<"crv">> => <<"P-256">>, 19 <<"x">> => base64url_encode(PubX), 20 <<"y">> => base64url_encode(PubY), 21 <<"d">> => base64url_encode(PrivateKeyBytes) 22 }), 23 24 %% Parse claims 25 Claims = json:decode(ClaimsJson), 26 27 %% Create JWT with header 28 JWS = jose_jws:from_map(#{ 29 <<"alg">> => <<"ES256">>, 30 <<"kid">> => Kid 31 }), 32 JWT = jose_jwt:from_map(Claims), 33 34 %% Sign 35 Signed = jose_jwt:sign(JWK, JWS, JWT), 36 {_JWS2, CompactToken} = jose_jws:compact(Signed), 37 38 {ok, CompactToken}; 39 {error, Reason} -> 40 {error, Reason} 41 end 42 catch 43 error:Error:Stack -> 44 {error, iolist_to_binary([<<"JWT signing error: ">>, 45 io_lib:format("~p at ~p", [Error, Stack])])} 46 end. 47 48%% Derive public did:key from private key multibase 49derive_public_did_key(PrivateKeyMultibase) -> 50 try 51 case parse_multibase_key(PrivateKeyMultibase) of 52 {ok, PrivateKeyBytes} -> 53 {PubX, PubY} = derive_public_coords(PrivateKeyBytes), 54 %% Compressed public key format 55 CompressedPub = compress_public_key(PubX, PubY), 56 %% Add multicodec prefix for P-256 public key (0x1200) 57 Prefixed = <<16#80, 16#24, CompressedPub/binary>>, 58 %% Encode as base58btc with 'z' prefix 59 Encoded = base58_encode(Prefixed), 60 {ok, <<"did:key:z", Encoded/binary>>}; 61 {error, Reason} -> 62 {error, Reason} 63 end 64 catch 65 _:_ -> {error, <<"Failed to derive public key">>} 66 end. 67 68%% Parse multibase key (z-prefixed base58btc) 69parse_multibase_key(<<"z", Rest/binary>>) -> 70 try 71 Decoded = base58_decode(Rest), 72 %% Skip multicodec prefix (2 bytes for P-256 private key) 73 <<_Prefix:2/binary, PrivateKey/binary>> = Decoded, 74 {ok, PrivateKey} 75 catch 76 _:_ -> {error, <<"Invalid multibase key format">>} 77 end; 78parse_multibase_key(_) -> 79 {error, <<"Unsupported multibase prefix">>}. 80 81%% Derive public key coordinates from private key 82derive_public_coords(PrivateKeyBytes) -> 83 %% Use crypto to compute public key from private key 84 %% crypto:generate_key returns {PublicKey, PrivateKey} 85 {<<4, XY/binary>>, _Priv} = crypto:generate_key(ecdh, secp256r1, PrivateKeyBytes), 86 <<X:32/binary, Y:32/binary>> = XY, 87 {X, Y}. 88 89%% Compress public key (02/03 prefix based on Y parity) 90compress_public_key(X, Y) -> 91 <<YLast>> = binary:part(Y, 31, 1), 92 Prefix = case YLast band 1 of 93 0 -> <<2>>; 94 1 -> <<3>> 95 end, 96 <<Prefix/binary, X/binary>>. 97 98%% Base58 Bitcoin alphabet 99-define(BASE58_ALPHABET, <<"123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz">>). 100 101base58_encode(Bin) -> 102 base58_encode(binary:decode_unsigned(Bin), <<>>). 103 104base58_encode(0, Acc) -> Acc; 105base58_encode(N, Acc) -> 106 Rem = N rem 58, 107 Char = binary:at(?BASE58_ALPHABET, Rem), 108 base58_encode(N div 58, <<Char, Acc/binary>>). 109 110base58_decode(Bin) -> 111 base58_decode(Bin, 0). 112 113base58_decode(<<>>, Acc) -> binary:encode_unsigned(Acc); 114base58_decode(<<C, Rest/binary>>, Acc) -> 115 case binary:match(?BASE58_ALPHABET, <<C>>) of 116 {Pos, 1} -> base58_decode(Rest, Acc * 58 + Pos); 117 nomatch -> error(invalid_base58) 118 end. 119 120%% Base64 URL-safe encoding (no padding) 121base64url_encode(Bin) -> 122 Base64 = base64:encode(Bin), 123 NoPlus = binary:replace(Base64, <<"+">>, <<"-">>, [global]), 124 NoSlash = binary:replace(NoPlus, <<"/">>, <<"_">>, [global]), 125 binary:replace(NoSlash, <<"=">>, <<"">>, [global]). 126 127%% Extract public key coordinates from private key multibase 128%% Takes a private key in multibase format (z42t...) 129%% Returns {ok, {XCoord, YCoord}} where coordinates are base64url-encoded binaries 130extract_public_key_coords(PrivateKeyMultibase) -> 131 try 132 case parse_multibase_key(PrivateKeyMultibase) of 133 {ok, PrivateKeyBytes} -> 134 {PubX, PubY} = derive_public_coords(PrivateKeyBytes), 135 X = base64url_encode(PubX), 136 Y = base64url_encode(PubY), 137 {ok, {X, Y}}; 138 {error, Reason} -> 139 {error, Reason} 140 end 141 catch 142 _:_ -> {error, <<"Failed to extract public key coordinates">>} 143 end.