Current section

Files

Jump to
ywt_core src ywt@verify_key.erl
Raw

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, id/1, match/4, algorithm/1, decoder/0, set_decoder/0, to_jwk/1, to_jwks/1]).
-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.
-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()),
ywt@internal@core:digest_type(),
bigi:big_int(),
bigi:big_int(),
ywt@internal@core:padding()} |
{verify_hmac,
gleam@option:option(binary()),
ywt@internal@core:digest_type(),
bitstring()}.
-file("src/ywt/verify_key.gleam", 58).
?DOC(
" Construct a verification key for token validation from a signing key.\n"
"\n"
" ## Security Considerations\n"
" - For asymmetric keys (RSA/ECDSA), this extracts only the public key\n"
" - For symmetric keys (HMAC), this returns the same secret (use with caution)\n"
" - Distribute verify keys to services that only need to check token validity\n"
" - Never give signing keys to services that only need verification\n"
"\n"
" ## Example\n"
" ```gleam\n"
" import gleam/crypto\n"
"\n"
" let secret = crypto.strong_random_bytes(32)\n"
" // Create a signing key\n"
" let sign_key = sign_key.hs256(secret)\n"
"\n"
" // Extract verification key for a service\n"
" let verify_key = verify_key.derived(sign_key)\n"
"\n"
" // This token can be used to verify but not create tokens\n"
" ywt.decode(jwt_token, using: payload_decoder, claims: [], keys: [verify_key])\n"
" ```\n"
"\n"
" 💡 **Best Practice:** Only your auth service should have signing keys.\n"
" All other services should only have verification keys.\n"
"\n"
" <div style=\"text-align: right;\">\n"
" <a href=\"#\">\n"
" <small>Back to top ↑</small>\n"
" </a>\n"
" </div>\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, Digest, E, N, Padding}
end,
fun(Id@2, Digest@1, E@1, N@1, _, _, _, _, _, _, _, Padding@1) ->
{verify_rsa, Id@2, Digest@1, E@1, N@1, 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", 163).
-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) ->
gleam@dynamic@decode:field(
<<"x"/utf8>>,
ywt@internal@core:bits_decoder(),
fun(X) ->
gleam@dynamic@decode:field(
<<"y"/utf8>>,
ywt@internal@core:bits_decoder(),
fun(Y) ->
Public_key = <<16#4, X/bitstring, Y/bitstring>>,
Digest_type = case Crv of
secp256r1 ->
sha256;
secp384r1 ->
sha384;
secp521r1 ->
sha512
end,
gleam@dynamic@decode:success(
{verify_ecdsa, Id, Crv, Digest_type, Public_key}
)
end
)
end
)
end
).
-file("src/ywt/verify_key.gleam", 343).
?DOC(
" Extracts the key identifier from a verification key, if present.\n"
"\n"
" This function retrieves the optional key identifier that was assigned to a key.\n"
" Key identifiers are used for key selection during JWT verification.\n"
"\n"
" <div style=\"text-align: right;\">\n"
" <a href=\"#\">\n"
" <small>Back to top ↑</small>\n"
" </a>\n"
" </div>\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", 350).
?DOC(false).
-spec match(
verify_key(),
fun((gleam@option:option(binary()), ywt@internal@core:named_curve(), ywt@internal@core:digest_type(), bitstring()) -> FAB),
fun((gleam@option:option(binary()), ywt@internal@core:digest_type(), bigi:big_int(), bigi:big_int(), ywt@internal@core:padding()) -> FAB),
fun((gleam@option:option(binary()), ywt@internal@core:digest_type(), bitstring()) -> FAB)
) -> FAB.
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", 361).
?DOC(false).
-spec algorithm(verify_key()) -> ywt@algorithm:algorithm().
algorithm(Key) ->
case Key of
{verify_ecdsa, _, _, sha256, _} ->
es256;
{verify_ecdsa, _, _, sha384, _} ->
es384;
{verify_ecdsa, _, _, sha512, _} ->
es512;
{verify_hmac, _, sha256, _} ->
hs256;
{verify_hmac, _, sha384, _} ->
hs384;
{verify_hmac, _, sha512, _} ->
hs512;
{verify_rsa, _, sha256, _, _, rsa_pkcs1_padding} ->
rs256;
{verify_rsa, _, sha384, _, _, rsa_pkcs1_padding} ->
rs384;
{verify_rsa, _, sha512, _, _, rsa_pkcs1_padding} ->
rs512;
{verify_rsa, _, sha256, _, _, rsa_pkcs1_pss_padding} ->
ps256;
{verify_rsa, _, sha384, _, _, rsa_pkcs1_pss_padding} ->
ps384;
{verify_rsa, _, sha512, _, _, rsa_pkcs1_pss_padding} ->
ps512
end.
-file("src/ywt/verify_key.gleam", 196).
-spec rsa_decoder(gleam@option:option(binary())) -> gleam@dynamic@decode:decoder(verify_key()).
rsa_decoder(Id) ->
gleam@dynamic@decode:field(
<<"alg"/utf8>>,
ywt@algorithm:decoder(),
fun(Alg) ->
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) ->
Digest_type = ywt@algorithm:digest_type(Alg),
gleam@dynamic@decode:then(
case ywt@algorithm:padding(Alg) of
{ok, Padding} ->
gleam@dynamic@decode:success(Padding);
{error, _} ->
gleam@dynamic@decode:failure(
rsa_pkcs1_padding,
<<"alg"/utf8>>
)
end,
fun(Padding@1) ->
gleam@dynamic@decode:success(
{verify_rsa,
Id,
Digest_type,
Exponent,
Modulus,
Padding@1}
)
end
)
end
)
end
)
end
).
-file("src/ywt/verify_key.gleam", 114).
?DOC(
" Decodes a JSON Web Key (JWK) containing a public key into a verification key.\n"
"\n"
" Use this when receiving public keys from external sources, key management services,\n"
" or when implementing key rotation with public key distribution.\n"
"\n"
" This function only supports RSA or Elliptic Curve keys.\n"
"\n"
" ## Security Considerations\n"
" - Only accept keys from trusted sources\n"
" - Only accepts asymmetric public keys (EC, RSA) - never processes private key material\n"
" - Does not implement key pinning or certificate validation\n"
"\n"
" ## Example\n"
" ```gleam\n"
" import gleam/json\n"
"\n"
" // Receive a public key from your identity provider\n"
" let jwk_json = \"{\n"
" \\\"kty\\\": \\\"EC\\\",\n"
" \\\"crv\\\": \\\"P-256\\\",\n"
" \\\"alg\\\": \\\"ES256\\\",\n"
" \\\"x\\\": \\\"...\\\",\n"
" \\\"y\\\": \\\"...\\\"\n"
" }\"\n"
"\n"
" case json.parse(jwk_json, verify_key.decoder()) {\n"
" Ok(verify_key) -> {\n"
" // Use this key to verify tokens from the identity provider\n"
" ywt.parse(token, using: decoder, claims: [], keys: [verify_key])\n"
" }\n"
" Error(_) -> // Handle invalid key format\n"
" }\n"
" ```\n"
"\n"
" 📝 **Note:** Always validate that keys come from trusted sources and consider\n"
" implementing key rotation mechanisms.\n"
"\n"
" <div style=\"text-align: right;\">\n"
" <a href=\"#\">\n"
" <small>Back to top ↑</small>\n"
" </a>\n"
" </div>\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", 159).
?DOC(
" Decodes a JSON Web Key Set (JWKS) into a list of verification keys.\n"
"\n"
" Use this when receiving public keys from external sources, key management services,\n"
" or when implementing key rotation with public key distribution.\n"
"\n"
" ## Security Considerations\n"
" - Only accept keys from trusted sources\n"
" - Only accepts asymmetric public keys (EC, RSA) - never processes private key material\n"
" - Does not implement key pinning or certificate validation\n"
"\n"
" ## Example\n"
" ```gleam\n"
" import gleam/json\n"
"\n"
" // Receive a public key from your identity provider\n"
" let jwk_json = \"{\\\"keys\\\": [{\\\"kty\\\": \\\"EC\\\", ...}]}\"\n"
"\n"
" case json.parse(jwk_json, verify_key.set_decoder()) {\n"
" Ok(verify_keys) -> {\n"
" ywt.parse(token, using: decoder, claims: [], keys: verify_keys)\n"
" }\n"
" Error(_) -> // Handle invalid key format\n"
" }\n"
" ```\n"
"\n"
" 📝 **Note:** Always validate that keys come from trusted sources and consider\n"
" implementing key rotation mechanisms.\n"
"\n"
" <div style=\"text-align: right;\">\n"
" <a href=\"#\">\n"
" <small>Back to top ↑</small>\n"
" </a>\n"
" </div>\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", 250).
?DOC(
" Encodes a verification key into a JSON Web Key (JWK).\n"
"\n"
" This function serializes a `VerifyKey` into the standard JWK format defined in RFC 7517,\n"
" making it suitable for key distribution, storage, or exchange with other JWT libraries\n"
" and services.\n"
"\n"
" ## Security Considerations\n"
" - Only converts verification keys (public keys and HMAC secrets for verification)\n"
" - HMAC keys will include the secret in the JWK - distribute carefully\n"
" - For asymmetric keys, only public information is included (safe to distribute)\n"
" - Include key IDs (`kid`) to enable proper key rotation and selection\n"
" - Consider key expiration and rotation policies when distributing JWKs\n"
"\n"
" ## Example\n"
" ```gleam\n"
" // Convert an ECDSA public key to JWK\n"
" let verify_key = verify_key.derived(ecdsa_sign_key)\n"
" let jwk = verify_key.to_jwk(verify_key)\n"
"\n"
" // Serve this JWK for other services to verify your tokens\n"
" json.to_string(jwk)\n"
" // Returns: {\"kty\":\"EC\",\"crv\":\"P-256\",\"x\":\"...\",\"y\":\"...\",\"alg\":\"ES256\",\"kid\":\"key1\"}\n"
" ```\n"
"\n"
" ⚠️ **Warning:** For HMAC keys, the resulting JWK contains the shared secret.\n"
" Only distribute HMAC JWKs to trusted services that need to verify tokens.\n"
"\n"
" <div style=\"text-align: right;\">\n"
" <a href=\"#\">\n"
" <small>Back to top ↑</small>\n"
" </a>\n"
" </div>\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 => 259,
value => _assert_fail,
start => 9199,
'end' => 9294,
pattern_start => 9210,
pattern_end => 9269})
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 = [{<<"alg"/utf8>>, ywt@algorithm:to_json(algorithm(Key))} |
Fields],
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", 329).
?DOC(
" Converts a list of verification keys to a JSON Web Key Set (JWKS) format.\n"
"\n"
" This creates a standard JWKS structure containing multiple keys, which is the\n"
" standard way to publish multiple verification keys for key rotation, different\n"
" algorithms, or multi-tenant scenarios.\n"
"\n"
" ## Usage\n"
" Use this to:\n"
" - Implement a JWKS endpoint (`/.well-known/jwks.json`)\n"
" - Support key rotation with multiple active keys\n"
" - Provide keys for different algorithms or use cases\n"
" - Enable external services to verify your JWTs\n"
"\n"
" ## Security Considerations\n"
" - JWKS endpoints are typically public - ensure only verification keys are included\n"
" - Implement proper caching headers for JWKS endpoints (but allow for key rotation)\n"
" - For HMAC keys, be extremely careful about distribution\n"
" - Consider rate limiting your JWKS endpoint to prevent abuse\n"
" - Include appropriate CORS headers if serving to web applications\n"
"\n"
" ## Example\n"
" ```gleam\n"
" // Create a JWKS with multiple keys for rotation\n"
" let current_key = verify_key.derived(current_signing_key)\n"
" let previous_key = verify_key.derived(previous_signing_key)\n"
" let backup_key = verify_key.derived(backup_signing_key)\n"
"\n"
" let jwks = verify_key.to_jwks([current_key, previous_key, backup_key])\n"
"\n"
" // Serve this at /.well-known/jwks.json\n"
" json.to_string(jwks)\n"
" // Returns: {\"keys\":[{\"kty\":\"EC\",...},{\"kty\":\"RSA\",...},{\"kty\":\"EC\",...}]}\n"
" ```\n"
"\n"
" 💡 **Best Practice:** Include multiple keys during rotation periods to ensure\n"
" tokens signed with old keys remain valid during the transition period.\n"
"\n"
" <div style=\"text-align: right;\">\n"
" <a href=\"#\">\n"
" <small>Back to top ↑</small>\n"
" </a>\n"
" </div>\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)}]).