-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( " \n" " \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 => <>, 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.