Auto-indexing service and GraphQL API for AT Protocol Records
quickslice.slices.network/
atproto
gleam
graphql
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.