Current section
Files
Jump to
Current section
Files
src/ywt@verify_key.erl
-module(ywt@verify_key).
-compile([no_auto_import, nowarn_unused_vars, nowarn_unused_function, nowarn_nomatch, inline]).
-define(FILEPATH, "src/ywt/verify_key.gleam").
-export([derived/1, algorithm/1, decoder/0, parse_jwk/1, set_decoder/0, parse_jwks/1, to_jwk/1, to_jwks/1, id/1, match/4, for_algorithm/2]).
-export_type([verify_key/0]).
-if(?OTP_RELEASE >= 27).
-define(MODULEDOC(Str), -moduledoc(Str)).
-define(DOC(Str), -doc(Str)).
-else.
-define(MODULEDOC(Str), -compile([])).
-define(DOC(Str), -compile([])).
-endif.
?MODULEDOC(
" <style>\n"
" .goto-top {\n"
" display: block;\n"
" font-size: 0.85em;\n"
" text-align: right;\n"
" }\n"
" </style>\n"
" <script>\n"
" (callback => document.readyState !== 'loading' ? callback() : document.addEventListener('DOMContentLoaded', callback, { once: true }))(() => {\n"
" const goToTop = document.createElement('a')\n"
" goToTop.classList.add('goto-top')\n"
" goToTop.setAttribute('href', '#')\n"
" goToTop.textContent = 'Back to top ↑'\n"
" for (const member of document.querySelectorAll('.member')) {\n"
" member.insertBefore(goToTop.cloneNode(true), null)\n"
" }\n"
" })\n"
" </script>\n"
).
-opaque verify_key() :: {verify_ecdsa,
gleam@option:option(binary()),
ywt@internal@core:named_curve(),
ywt@internal@core:digest_type(),
bitstring()} |
{verify_rsa,
gleam@option:option(binary()),
gleam@option:option(ywt@internal@core:digest_type()),
bigi:big_int(),
bigi:big_int(),
gleam@option:option(ywt@internal@core:padding())} |
{verify_hmac,
gleam@option:option(binary()),
ywt@internal@core:digest_type(),
bitstring()}.
-file("src/ywt/verify_key.gleam", 60).
?DOC(
" Derives a verification key from a signing key.\n"
"\n"
" For RSA and ECDSA this extracts public key material. For HMAC this returns\n"
" the same shared secret, so treat the result as sensitive.\n"
"\n"
" ```gleam\n"
" let verify_key = verify_key.derived(signing_key)\n"
" ```\n"
).
-spec derived(ywt@sign_key:sign_key()) -> verify_key().
derived(Sign_key) ->
ywt@sign_key:match(
Sign_key,
fun(Id, Curve, Digest_type, Public_key, _) ->
{verify_ecdsa, Id, Curve, Digest_type, Public_key}
end,
fun(Id@1, Digest, E, N, _, Padding) ->
{verify_rsa, Id@1, {some, Digest}, E, N, {some, Padding}}
end,
fun(Id@2, Digest@1, E@1, N@1, _, _, _, _, _, _, _, Padding@1) ->
{verify_rsa, Id@2, {some, Digest@1}, E@1, N@1, {some, Padding@1}}
end,
fun(Id@3, Digest_type@1, Secret) ->
{verify_hmac, Id@3, Digest_type@1, Secret}
end
).
-file("src/ywt/verify_key.gleam", 313).
?DOC(false).
-spec algorithm(verify_key()) -> {ok, ywt@algorithm:algorithm()} | {error, nil}.
algorithm(Key) ->
case Key of
{verify_ecdsa, _, _, sha256, _} ->
{ok, es256};
{verify_ecdsa, _, _, sha384, _} ->
{ok, es384};
{verify_ecdsa, _, _, sha512, _} ->
{ok, es512};
{verify_hmac, _, sha256, _} ->
{ok, hs256};
{verify_hmac, _, sha384, _} ->
{ok, hs384};
{verify_hmac, _, sha512, _} ->
{ok, hs512};
{verify_rsa, _, {some, sha256}, _, _, {some, rsa_pkcs1_padding}} ->
{ok, rs256};
{verify_rsa, _, {some, sha384}, _, _, {some, rsa_pkcs1_padding}} ->
{ok, rs384};
{verify_rsa, _, {some, sha512}, _, _, {some, rsa_pkcs1_padding}} ->
{ok, rs512};
{verify_rsa, _, {some, sha256}, _, _, {some, rsa_pkcs1_pss_padding}} ->
{ok, ps256};
{verify_rsa, _, {some, sha384}, _, _, {some, rsa_pkcs1_pss_padding}} ->
{ok, ps384};
{verify_rsa, _, {some, sha512}, _, _, {some, rsa_pkcs1_pss_padding}} ->
{ok, ps512};
{verify_rsa, _, _, _, _, _} ->
{error, nil}
end.
-file("src/ywt/verify_key.gleam", 184).
-spec rsa_decoder(gleam@option:option(binary())) -> gleam@dynamic@decode:decoder(verify_key()).
rsa_decoder(Id) ->
gleam@dynamic@decode:field(
<<"e"/utf8>>,
ywt@internal@core:int_decoder(),
fun(Exponent) ->
gleam@dynamic@decode:field(
<<"n"/utf8>>,
ywt@internal@core:int_decoder(),
fun(Modulus) ->
gleam@dynamic@decode:then(
case ywt@internal@core:rsa_modulus_is_large_enough(
Modulus
) of
true ->
gleam@dynamic@decode:success(nil);
false ->
gleam@dynamic@decode:failure(nil, <<"n"/utf8>>)
end,
fun(_) ->
gleam@dynamic@decode:optional_field(
<<"alg"/utf8>>,
none,
gleam@dynamic@decode:map(
ywt@algorithm:decoder(),
fun(Field@0) -> {some, Field@0} end
),
fun(Alg) -> case Alg of
none ->
Key = {verify_rsa,
Id,
none,
Exponent,
Modulus,
none},
gleam@dynamic@decode:success(Key);
{some, Alg@1} ->
case ywt@algorithm:padding(Alg@1) of
{ok, Padding} ->
Digest_type = {some,
ywt@algorithm:digest_type(
Alg@1
)},
Padding@1 = {some, Padding},
Key@1 = {verify_rsa,
Id,
Digest_type,
Exponent,
Modulus,
Padding@1},
gleam@dynamic@decode:success(
Key@1
);
{error, _} ->
gleam@dynamic@decode:failure(
{verify_rsa,
Id,
none,
Exponent,
Modulus,
none},
<<"alg"/utf8>>
)
end
end end
)
end
)
end
)
end
).
-file("src/ywt/verify_key.gleam", 141).
-spec ec_decoder(gleam@option:option(binary())) -> gleam@dynamic@decode:decoder(verify_key()).
ec_decoder(Id) ->
gleam@dynamic@decode:field(
<<"crv"/utf8>>,
ywt@internal@core:curve_decoder(),
fun(Crv) ->
Coord_size = ywt@internal@core:named_curve_size(Crv),
gleam@dynamic@decode:field(
<<"x"/utf8>>,
ywt@internal@core:bits_of_length_decoder(Coord_size),
fun(X) ->
gleam@dynamic@decode:field(
<<"y"/utf8>>,
ywt@internal@core:bits_of_length_decoder(Coord_size),
fun(Y) ->
Public_key = <<16#4, X/bitstring, Y/bitstring>>,
Digest_type = case Crv of
secp256r1 ->
sha256;
secp384r1 ->
sha384;
secp521r1 ->
sha512
end,
Verify_key = {verify_ecdsa,
Id,
Crv,
Digest_type,
Public_key},
Expected_alg = ywt@algorithm:ec_algorithm(Crv),
gleam@dynamic@decode:optional_field(
<<"alg"/utf8>>,
Expected_alg,
ywt@algorithm:decoder(),
fun(Alg) -> case Alg =:= Expected_alg of
true ->
gleam@dynamic@decode:success(
Verify_key
);
false ->
gleam@dynamic@decode:failure(
Verify_key,
<<"alg"/utf8>>
)
end end
)
end
)
end
)
end
).
-file("src/ywt/verify_key.gleam", 91).
?DOC(
" Decodes a public JWK into a verification key.\n"
"\n"
" Only decode keys from trusted sources. Unknown JWK fields are ignored;\n"
" `key_ops`, `use`, certificates, and key pinning are not enforced.\n"
" This decoder accepts EC and RSA public keys, not HMAC shared-secret JWKs.\n"
"\n"
" RSA JWKs may omit `alg`. In that case, JWT decoding uses the signed `alg`\n"
" header to specialize the key. Raw signature verification with that key fails\n"
" closed because there is no JWT header.\n"
" For raw JSON strings, prefer `parse_jwk` so duplicate member handling is\n"
" consistent on all targets.\n"
"\n"
" ```gleam\n"
" json.parse(jwk_json, verify_key.decoder())\n"
" ```\n"
).
-spec decoder() -> gleam@dynamic@decode:decoder(verify_key()).
decoder() ->
gleam@dynamic@decode:optional_field(
<<"kid"/utf8>>,
none,
gleam@dynamic@decode:map(
{decoder, fun gleam@dynamic@decode:decode_string/1},
fun(Field@0) -> {some, Field@0} end
),
fun(Id) ->
gleam@dynamic@decode:field(
<<"kty"/utf8>>,
{decoder, fun gleam@dynamic@decode:decode_string/1},
fun(Kty) -> case Kty of
<<"EC"/utf8>> ->
ec_decoder(Id);
<<"RSA"/utf8>> ->
rsa_decoder(Id);
_ ->
gleam@dynamic@decode:failure(
{verify_hmac, Id, sha256, <<>>},
<<"kty"/utf8>>
)
end end
)
end
).
-file("src/ywt/verify_key.gleam", 111).
?DOC(
" Parses a public JWK into a verification key.\n"
"\n"
" Duplicate JSON object members are handled consistently on all targets: the\n"
" lexically last member value is used, matching JavaScript's `JSON.parse`.\n"
"\n"
" ```gleam\n"
" verify_key.parse_jwk(jwk_json)\n"
" ```\n"
).
-spec parse_jwk(binary()) -> {ok, verify_key()} |
{error, gleam@json:decode_error()}.
parse_jwk(Jwk) ->
ywt@internal@jose_json:parse(Jwk, decoder()).
-file("src/ywt/verify_key.gleam", 125).
?DOC(
" Decodes a JWKS into a list of verification keys.\n"
"\n"
" This has the same trust and validation limits as `decoder`. It expects a JSON\n"
" object with a `keys` list.\n"
" For raw JSON strings, prefer `parse_jwks` so duplicate member handling is\n"
" consistent on all targets.\n"
"\n"
" ```gleam\n"
" json.parse(jwks_json, verify_key.set_decoder())\n"
" ```\n"
).
-spec set_decoder() -> gleam@dynamic@decode:decoder(list(verify_key())).
set_decoder() ->
gleam@dynamic@decode:at(
[<<"keys"/utf8>>],
gleam@dynamic@decode:list(decoder())
).
-file("src/ywt/verify_key.gleam", 137).
?DOC(
" Parses a JWKS into a list of verification keys.\n"
"\n"
" Duplicate JSON object members are handled consistently on all targets: the\n"
" lexically last member value is used, matching JavaScript's `JSON.parse`.\n"
"\n"
" ```gleam\n"
" verify_key.parse_jwks(jwks_json)\n"
" ```\n"
).
-spec parse_jwks(binary()) -> {ok, list(verify_key())} |
{error, gleam@json:decode_error()}.
parse_jwks(Jwks) ->
ywt@internal@jose_json:parse(Jwks, set_decoder()).
-file("src/ywt/verify_key.gleam", 238).
?DOC(
" Encodes a verification key as a JWK.\n"
"\n"
" Asymmetric keys encode only public key material. HMAC keys encode the shared\n"
" secret, so do not publish HMAC JWKs.\n"
"\n"
" ```gleam\n"
" verify_key.derived(signing_key) |> verify_key.to_jwk\n"
" ```\n"
).
-spec to_jwk(verify_key()) -> gleam@json:json().
to_jwk(Key) ->
Fields = case Key of
{verify_hmac, _, _, _} ->
[{<<"kty"/utf8>>, gleam@json:string(<<"oct"/utf8>>)},
{<<"k"/utf8>>,
ywt@internal@core:bits_to_json(erlang:element(4, Key))}];
{verify_ecdsa, _, _, _, _} ->
Coord_size = ywt@internal@core:named_curve_size(
erlang:element(3, Key)
),
{X@1, Y@1} = case erlang:element(5, Key) of
<<16#4, X:Coord_size/binary, Y:Coord_size/binary>> -> {X, Y};
_assert_fail ->
erlang:error(#{gleam_error => let_assert,
message => <<"Pattern match failed, no pattern matched the value."/utf8>>,
file => <<?FILEPATH/utf8>>,
module => <<"ywt/verify_key"/utf8>>,
function => <<"to_jwk"/utf8>>,
line => 247,
value => _assert_fail,
start => 8561,
'end' => 8656,
pattern_start => 8572,
pattern_end => 8631})
end,
[{<<"kty"/utf8>>, gleam@json:string(<<"EC"/utf8>>)},
{<<"crv"/utf8>>,
gleam@json:string(
ywt@internal@core:named_curve(erlang:element(3, Key))
)},
{<<"x"/utf8>>, ywt@internal@core:bits_to_json(X@1)},
{<<"y"/utf8>>, ywt@internal@core:bits_to_json(Y@1)}];
{verify_rsa, _, _, _, _, _} ->
[{<<"kty"/utf8>>, gleam@json:string(<<"RSA"/utf8>>)},
{<<"e"/utf8>>,
ywt@internal@core:int_to_json(erlang:element(4, Key))},
{<<"n"/utf8>>,
ywt@internal@core:int_to_json(erlang:element(5, Key))}]
end,
Fields@1 = case algorithm(Key) of
{ok, Alg} ->
[{<<"alg"/utf8>>, ywt@algorithm:to_json(Alg)} | Fields];
{error, nil} ->
Fields
end,
Fields@2 = case erlang:element(2, Key) of
{some, Id} ->
[{<<"kid"/utf8>>, gleam@json:string(Id)} | Fields@1];
none ->
Fields@1
end,
gleam@json:object(Fields@2).
-file("src/ywt/verify_key.gleam", 286).
?DOC(
" Encodes verification keys as a JWKS.\n"
"\n"
" This is suitable for publishing asymmetric keys during rotation. Do not\n"
" publish HMAC keys, because their JWK form contains the shared secret.\n"
"\n"
" ```gleam\n"
" verify_key.to_jwks([current_key, previous_key])\n"
" ```\n"
).
-spec to_jwks(list(verify_key())) -> gleam@json:json().
to_jwks(Keys) ->
gleam@json:object([{<<"keys"/utf8>>, gleam@json:array(Keys, fun to_jwk/1)}]).
-file("src/ywt/verify_key.gleam", 295).
?DOC(
" Returns the key id, if one is set.\n"
"\n"
" ```gleam\n"
" verify_key.id(key)\n"
" ```\n"
).
-spec id(verify_key()) -> {ok, binary()} | {error, nil}.
id(Key) ->
gleam@option:to_result(erlang:element(2, Key), nil).
-file("src/ywt/verify_key.gleam", 302).
?DOC(false).
-spec match(
verify_key(),
fun((gleam@option:option(binary()), ywt@internal@core:named_curve(), ywt@internal@core:digest_type(), bitstring()) -> FKP),
fun((gleam@option:option(binary()), gleam@option:option(ywt@internal@core:digest_type()), bigi:big_int(), bigi:big_int(), gleam@option:option(ywt@internal@core:padding())) -> FKP),
fun((gleam@option:option(binary()), ywt@internal@core:digest_type(), bitstring()) -> FKP)
) -> FKP.
match(Verify_key, On_ecdsa, On_rsa, On_hmac) ->
case Verify_key of
{verify_ecdsa, Id, Curve, Digest_type, Public_key} ->
On_ecdsa(Id, Curve, Digest_type, Public_key);
{verify_hmac, Id@1, Digest_type@1, Secret} ->
On_hmac(Id@1, Digest_type@1, Secret);
{verify_rsa, Id@2, Digest_type@2, Exponent, Modulus, Padding} ->
On_rsa(Id@2, Digest_type@2, Exponent, Modulus, Padding)
end.
-file("src/ywt/verify_key.gleam", 349).
?DOC(false).
-spec for_algorithm(verify_key(), ywt@algorithm:algorithm()) -> {ok,
verify_key()} |
{error, nil}.
for_algorithm(Key, Alg) ->
case Key of
{verify_rsa, Id, none, Exponent, Modulus, _} ->
case ywt@algorithm:padding(Alg) of
{ok, Padding} ->
Digest_type = {some, ywt@algorithm:digest_type(Alg)},
Padding@1 = {some, Padding},
{ok,
{verify_rsa,
Id,
Digest_type,
Exponent,
Modulus,
Padding@1}};
{error, nil} ->
{error, nil}
end;
_ ->
case algorithm(Key) =:= {ok, Alg} of
true ->
{ok, Key};
false ->
{error, nil}
end
end.