-module(ywt@sign_key). -compile([no_auto_import, nowarn_unused_vars, nowarn_unused_function, nowarn_nomatch, inline]). -define(FILEPATH, "src/ywt/sign_key.gleam"). -export([hs256/1, hs384/1, hs512/1, with_random_id/1, id/1, match/5, algorithm/1, decoder/0, to_jwk/1]). -export_type([sign_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 sign_key() :: {sign_ecdsa, gleam@option:option(binary()), ywt@internal@core:named_curve(), ywt@internal@core:digest_type(), bitstring(), bitstring()} | {sign_rsa_simple, gleam@option:option(binary()), ywt@internal@core:digest_type(), bigi:big_int(), bigi:big_int(), bigi:big_int(), ywt@internal@core:padding()} | {sign_rsa_full, gleam@option:option(binary()), ywt@internal@core:digest_type(), bigi:big_int(), bigi:big_int(), bigi:big_int(), bigi:big_int(), bigi:big_int(), bigi:big_int(), bigi:big_int(), bigi:big_int(), list({bigi:big_int(), bigi:big_int(), bigi:big_int()}), ywt@internal@core:padding()} | {sign_hmac, gleam@option:option(binary()), ywt@internal@core:digest_type(), bitstring()}. -file("src/ywt/sign_key.gleam", 92). ?DOC( " Creates a symmetric HMAC-SHA256 signing key for JWT authentication.\n" "\n" " This is the most commonly used algorithm for simple applications where you have a shared\n" " secret between your application and the service that will verify JWTs. The same key is\n" " used for both signing and verification.\n" "\n" " ## Security Considerations\n" " - The secret must be cryptographically random and at least 256 bits (32 bytes) long\n" " - Keep the secret absolutely confidential - anyone with access can forge tokens\n" " - Rotate secrets regularly and have a key rotation strategy\n" " - Never store secrets in code or version control\n" "\n" " ## Example\n" " ```gleam\n" " import gleam/crypto\n" "\n" " // Generate a secure random secret\n" " let secret = crypto.strong_random_bytes(32)\n" " let assert Ok(key) = sign_key.hs256(secret)\n" "\n" " // Use this key to sign JWTs\n" " ywt.encode(payload: [#(\"sub\", json.string(\"user123\"))], claims: [], key: key)\n" " ```\n" "\n" " ⚠️ **Warning:** HMAC keys are symmetric - the same key signs and verifies tokens.\n" " This means every service that needs to verify tokens must have the secret,\n" " increasing the attack surface.\n" ). -spec hs256(bitstring()) -> {ok, sign_key()} | {error, nil}. hs256(Secret) -> case erlang:bit_size(Secret) >= 256 of true -> {ok, {sign_hmac, none, sha256, Secret}}; false -> {error, nil} end. -file("src/ywt/sign_key.gleam", 127). ?DOC( " Creates a symmetric HMAC-SHA384 signing key for for JWT authentication.\n" "\n" " Choose this over HS256 when you need additional security margins or your security\n" " policy requires SHA-384.\n" "\n" " ## Security Considerations\n" " - The secret must be cryptographically random and at least 384 bits (48 bytes) long\n" " - Keep the secret absolutely confidential - anyone with access can forge tokens\n" " - Rotate secrets regularly and have a key rotation strategy\n" " - Never store secrets in code or version control\n" "\n" " ## Example\n" " ```gleam\n" " import gleam/crypto\n" "\n" " // Generate a secure random secret\n" " let secret = crypto.strong_random_bytes(48)\n" " let assert Ok(key) = sign_key.hs384(secret)\n" "\n" " // Use this key to sign JWTs\n" " ywt.encode(payload: [#(\"sub\", json.string(\"user123\"))], claims: [], key: key)\n" " ```\n" "\n" " ⚠️ **Warning:** HMAC keys are symmetric - the same key signs and verifies tokens.\n" " This means every service that needs to verify tokens must have the secret,\n" " increasing the attack surface.\n" ). -spec hs384(bitstring()) -> {ok, sign_key()} | {error, nil}. hs384(Secret) -> case erlang:bit_size(Secret) >= 384 of true -> {ok, {sign_hmac, none, sha384, Secret}}; false -> {error, nil} end. -file("src/ywt/sign_key.gleam", 161). ?DOC( " Creates a symmetric HMAC-SHA512 signing key for JWT authentication.\n" "\n" " This is the most commonly used algorithm for simple applications where you have a shared\n" " secret between your application and the service that will verify JWTs. The same key is\n" " used for both signing and verification.\n" "\n" " ## Security Considerations\n" " - The secret must be cryptographically random and at least 512 bits (64 bytes) long\n" " - Keep the secret absolutely confidential - anyone with access can forge tokens\n" " - Rotate secrets regularly and have a key rotation strategy\n" " - Never store secrets in code or version control\n" "\n" " ## Example\n" " ```gleam\n" " import gleam/crypto\n" "\n" " // Generate a secure random secret\n" " let secret = crypto.strong_random_bytes(64)\n" " let assert Ok(key) = sign_key.hs512(secret)\n" "\n" " // Use this key to sign JWTs\n" " ywt.encode(payload: [#(\"sub\", json.string(\"user123\"))], claims: [], key: key)\n" " ```\n" "\n" " ⚠️ **Warning:** HMAC keys are symmetric - the same key signs and verifies tokens.\n" " This means every service that needs to verify tokens must have the secret,\n" " increasing the attack surface.\n" ). -spec hs512(bitstring()) -> {ok, sign_key()} | {error, nil}. hs512(Secret) -> case erlang:bit_size(Secret) >= 512 of true -> {ok, {sign_hmac, none, sha512, Secret}}; false -> {error, nil} end. -file("src/ywt/sign_key.gleam", 178). ?DOC( " Assigns a random key identifier to a signing key for key management and rotation.\n" "\n" " This function generates a cryptographically random identifier and assigns it to\n" " the key's `id` field. Key identifiers enable proper key selection during JWT\n" " verification and are essential for implementing key rotation strategies.\n" "\n" " ## Usage\n" " Use this when you need to uniquely identify keys for distribution, storage,\n" " or rotation purposes. The identifier will be included in JWT headers as the\n" " `kid` (Key ID) field, allowing verifiers to select the correct key.\n" ). -spec with_random_id(sign_key()) -> sign_key(). with_random_id(Key) -> Id = {some, ywt_core_ffi:random_id()}, case Key of {sign_ecdsa, _, _, _, _, _} -> {sign_ecdsa, Id, erlang:element(3, Key), erlang:element(4, Key), erlang:element(5, Key), erlang:element(6, Key)}; {sign_hmac, _, _, _} -> {sign_hmac, Id, erlang:element(3, Key), erlang:element(4, Key)}; {sign_rsa_full, _, _, _, _, _, _, _, _, _, _, _, _} -> {sign_rsa_full, Id, erlang:element(3, Key), erlang:element(4, Key), erlang:element(5, Key), erlang:element(6, Key), erlang:element(7, Key), erlang:element(8, Key), erlang:element(9, Key), erlang:element(10, Key), erlang:element(11, Key), erlang:element(12, Key), erlang:element(13, Key)}; {sign_rsa_simple, _, _, _, _, _, _} -> {sign_rsa_simple, Id, erlang:element(3, Key), erlang:element(4, Key), erlang:element(5, Key), erlang:element(6, Key), erlang:element(7, Key)} end. -file("src/ywt/sign_key.gleam", 244). -spec ec_decoder(gleam@option:option(binary())) -> gleam@dynamic@decode:decoder(sign_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) -> gleam@dynamic@decode:field( <<"d"/utf8>>, ywt@internal@core:bits_decoder(), fun(Private_key) -> Public_key = <<16#4, X/bitstring, Y/bitstring>>, Digest_type = case Crv of secp256r1 -> sha256; secp384r1 -> sha384; secp521r1 -> sha512 end, gleam@dynamic@decode:success( {sign_ecdsa, Id, Crv, Digest_type, Public_key, Private_key} ) end ) end ) end ) end ). -file("src/ywt/sign_key.gleam", 402). -spec other_primes_info_decoder() -> gleam@dynamic@decode:decoder({bigi:big_int(), bigi:big_int(), bigi:big_int()}). other_primes_info_decoder() -> Field_decoder = ywt@internal@core:int_decoder(), gleam@dynamic@decode:field( <<"r"/utf8>>, Field_decoder, fun(Factor) -> gleam@dynamic@decode:field( <<"d"/utf8>>, Field_decoder, fun(Exponent) -> gleam@dynamic@decode:field( <<"t"/utf8>>, Field_decoder, fun(Coefficient) -> gleam@dynamic@decode:success( {Factor, Exponent, Coefficient} ) end ) end ) end ). -file("src/ywt/sign_key.gleam", 341). -spec rsa_full_decoder( gleam@option:option(binary()), ywt@internal@core:digest_type(), ywt@internal@core:padding(), bigi:big_int(), bigi:big_int(), bigi:big_int() ) -> gleam@dynamic@decode:decoder(sign_key()). rsa_full_decoder( Id, Digest_type, Padding, Public_exponent, Modulus, Private_exponent ) -> Field_decoder = ywt@internal@core:int_decoder(), gleam@dynamic@decode:field( <<"p"/utf8>>, Field_decoder, fun(First_prime_factor) -> gleam@dynamic@decode:field( <<"q"/utf8>>, Field_decoder, fun(Second_prime_factor) -> gleam@dynamic@decode:field( <<"dp"/utf8>>, Field_decoder, fun(First_factor_crt_exponent) -> gleam@dynamic@decode:field( <<"dq"/utf8>>, Field_decoder, fun(Second_factor_crt_exponent) -> gleam@dynamic@decode:field( <<"qi"/utf8>>, Field_decoder, fun(First_crt_coefficient) -> gleam@dynamic@decode:optional_field( <<"oth"/utf8>>, [], gleam@dynamic@decode:list( other_primes_info_decoder() ), fun(Other_primes_info) -> gleam@dynamic@decode:success( {sign_rsa_full, Id, Digest_type, Public_exponent, Modulus, Private_exponent, First_prime_factor, Second_prime_factor, First_factor_crt_exponent, Second_factor_crt_exponent, First_crt_coefficient, Other_primes_info, Padding} ) end ) end ) end ) end ) end ) end ). -file("src/ywt/sign_key.gleam", 424). ?DOC( " Extracts the key identifier from a signing 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" ). -spec id(sign_key()) -> {ok, binary()} | {error, nil}. id(Key) -> gleam@option:to_result(erlang:element(2, Key), nil). -file("src/ywt/sign_key.gleam", 431). ?DOC(false). -spec match( sign_key(), fun((gleam@option:option(binary()), ywt@internal@core:named_curve(), ywt@internal@core:digest_type(), bitstring(), bitstring()) -> EUY), fun((gleam@option:option(binary()), ywt@internal@core:digest_type(), bigi:big_int(), bigi:big_int(), bigi:big_int(), ywt@internal@core:padding()) -> EUY), fun((gleam@option:option(binary()), ywt@internal@core:digest_type(), bigi:big_int(), bigi:big_int(), bigi:big_int(), bigi:big_int(), bigi:big_int(), bigi:big_int(), bigi:big_int(), bigi:big_int(), list({bigi:big_int(), bigi:big_int(), bigi:big_int()}), ywt@internal@core:padding()) -> EUY), fun((gleam@option:option(binary()), ywt@internal@core:digest_type(), bitstring()) -> EUY) ) -> EUY. match(Sign_key, On_ecdsa, On_rsa_simple, On_rsa_full, On_hmac) -> case Sign_key of {sign_ecdsa, Id, Curve, Digest_type, Public_key, Private_key} -> On_ecdsa(Id, Curve, Digest_type, Public_key, Private_key); {sign_hmac, Id@1, Digest_type@1, Secret} -> On_hmac(Id@1, Digest_type@1, Secret); {sign_rsa_full, Id@2, Digest_type@2, Public_exponent, Modulus, Private_exponent, First_prime_factor, Second_prime_factor, First_factor_crt_exponent, Second_factor_crt_exponent, First_crt_coefficient, Other_primes_info, Padding} -> On_rsa_full( Id@2, Digest_type@2, Public_exponent, Modulus, Private_exponent, First_prime_factor, Second_prime_factor, First_factor_crt_exponent, Second_factor_crt_exponent, First_crt_coefficient, Other_primes_info, Padding ); {sign_rsa_simple, Id@3, Digest_type@3, Public_exponent@1, Modulus@1, Private_exponent@1, Padding@1} -> On_rsa_simple( Id@3, Digest_type@3, Public_exponent@1, Modulus@1, Private_exponent@1, Padding@1 ) end. -file("src/ywt/sign_key.gleam", 484). ?DOC(false). -spec algorithm(sign_key()) -> ywt@algorithm:algorithm(). algorithm(Key) -> case Key of {sign_ecdsa, _, _, sha256, _, _} -> es256; {sign_ecdsa, _, _, sha384, _, _} -> es384; {sign_ecdsa, _, _, sha512, _, _} -> es512; {sign_hmac, _, sha256, _} -> hs256; {sign_hmac, _, sha384, _} -> hs384; {sign_hmac, _, sha512, _} -> hs512; {sign_rsa_simple, _, sha256, _, _, _, rsa_pkcs1_padding} -> rs256; {sign_rsa_full, _, sha256, _, _, _, _, _, _, _, _, _, rsa_pkcs1_padding} -> rs256; {sign_rsa_simple, _, sha384, _, _, _, rsa_pkcs1_padding} -> rs384; {sign_rsa_full, _, sha384, _, _, _, _, _, _, _, _, _, rsa_pkcs1_padding} -> rs384; {sign_rsa_simple, _, sha512, _, _, _, rsa_pkcs1_padding} -> rs512; {sign_rsa_full, _, sha512, _, _, _, _, _, _, _, _, _, rsa_pkcs1_padding} -> rs512; {sign_rsa_simple, _, sha256, _, _, _, rsa_pkcs1_pss_padding} -> ps256; {sign_rsa_full, _, sha256, _, _, _, _, _, _, _, _, _, rsa_pkcs1_pss_padding} -> ps256; {sign_rsa_simple, _, sha384, _, _, _, rsa_pkcs1_pss_padding} -> ps384; {sign_rsa_full, _, sha384, _, _, _, _, _, _, _, _, _, rsa_pkcs1_pss_padding} -> ps384; {sign_rsa_simple, _, sha512, _, _, _, rsa_pkcs1_pss_padding} -> ps512; {sign_rsa_full, _, sha512, _, _, _, _, _, _, _, _, _, rsa_pkcs1_pss_padding} -> ps512 end. -file("src/ywt/sign_key.gleam", 235). -spec oct_decoder(gleam@option:option(binary())) -> gleam@dynamic@decode:decoder(sign_key()). oct_decoder(Id) -> gleam@dynamic@decode:field( <<"alg"/utf8>>, ywt@algorithm:decoder(), fun(Alg) -> gleam@dynamic@decode:field( <<"k"/utf8>>, ywt@internal@core:bits_decoder(), fun(K) -> Digest_type = ywt@algorithm:digest_type(Alg), gleam@dynamic@decode:success( {sign_hmac, Id, Digest_type, K} ) end ) end ). -file("src/ywt/sign_key.gleam", 287). -spec rsa_decoder(gleam@option:option(binary())) -> gleam@dynamic@decode:decoder(sign_key()). rsa_decoder(Id) -> gleam@dynamic@decode:field( <<"alg"/utf8>>, ywt@algorithm:decoder(), fun(Alg) -> Field_decoder = ywt@internal@core:int_decoder(), gleam@dynamic@decode:field( <<"e"/utf8>>, Field_decoder, fun(Public_exponent) -> gleam@dynamic@decode:field( <<"n"/utf8>>, Field_decoder, fun(Modulus) -> gleam@dynamic@decode:field( <<"d"/utf8>>, Field_decoder, fun(Private_exponent) -> 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) -> Full = rsa_full_decoder( Id, Digest_type, Padding@1, Public_exponent, Modulus, Private_exponent ), gleam@dynamic@decode:one_of( Full, [gleam@dynamic@decode:success( {sign_rsa_simple, Id, Digest_type, Public_exponent, Modulus, Private_exponent, Padding@1} )] ) end ) end ) end ) end ) end ). -file("src/ywt/sign_key.gleam", 221). ?DOC( " Decodes a JSON Web Key (JWK) containing a private key into a signing key.\n" "\n" " Use this for loading private keys from secure configuration or key management systems\n" " when implementing JWT signing services.\n" "\n" " ## Security Considerations\n" " - Only accept keys from trusted sources\n" " - Store private keys in secure key management systems, never in code\n" " - Only keep private keys in your auth service, they should never leave secure boundaries\n" " - Rotate private keys regularly and have a key rotation strategy\n" "\n" " ## Example\n" " ```gleam\n" " // Load private key from secure configuration (never hardcode!)\n" " let private_jwk = load_from_secure_vault(\"jwt-signing-key\")\n" "\n" " case json.parse(private_jwk, sign_key.decoder()) {\n" " Ok(sign_key) -> {\n" " // Use to create signed JWTs\n" " encode(payload: [#(\"sub\", json.string(\"user123\"))],\n" " claims: [],\n" " key: sign_key)\n" " }\n" " Error(_) -> // Handle key loading error\n" " }\n" " ```\n" "\n" " 🔐 **Critical:** Sign keys are the most important pieces of your security.\n" " Treat them accordingly.\n" ). -spec decoder() -> gleam@dynamic@decode:decoder(sign_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 <<"oct"/utf8>> -> oct_decoder(Id); <<"EC"/utf8>> -> ec_decoder(Id); <<"RSA"/utf8>> -> rsa_decoder(Id); _ -> gleam@dynamic@decode:failure( {sign_hmac, Id, sha256, <<>>}, <<"kty"/utf8>> ) end end ) end ). -file("src/ywt/sign_key.gleam", 535). ?DOC(false). -spec to_jwk(sign_key()) -> gleam@json:json(). to_jwk(Key) -> Fields@1 = case Key of {sign_hmac, _, _, _} -> [{<<"kty"/utf8>>, gleam@json:string(<<"oct"/utf8>>)}, {<<"k"/utf8>>, ywt@internal@core:bits_to_json(erlang:element(4, Key))}]; {sign_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/sign_key"/utf8>>, function => <<"to_jwk"/utf8>>, line => 543, value => _assert_fail, start => 19801, 'end' => 19896, pattern_start => 19812, pattern_end => 19871}) 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)}, {<<"d"/utf8>>, ywt@internal@core:bits_to_json(erlang:element(6, Key))}]; {sign_rsa_simple, _, _, _, _, _, _} -> [{<<"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))}, {<<"d"/utf8>>, ywt@internal@core:int_to_json(erlang:element(6, Key))}]; {sign_rsa_full, _, _, _, _, _, _, _, _, _, _, _, _} -> Fields = [{<<"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))}, {<<"d"/utf8>>, ywt@internal@core:int_to_json(erlang:element(6, Key))}, {<<"p"/utf8>>, ywt@internal@core:int_to_json(erlang:element(7, Key))}, {<<"q"/utf8>>, ywt@internal@core:int_to_json(erlang:element(8, Key))}, {<<"dp"/utf8>>, ywt@internal@core:int_to_json(erlang:element(9, Key))}, {<<"dq"/utf8>>, ywt@internal@core:int_to_json(erlang:element(10, Key))}, {<<"qi"/utf8>>, ywt@internal@core:int_to_json(erlang:element(11, Key))}], case erlang:element(12, Key) of [] -> Fields; [_ | _] -> [{<<"oth"/utf8>>, gleam@json:array( erlang:element(12, Key), fun(Info) -> gleam@json:object( [{<<"r"/utf8>>, ywt@internal@core:int_to_json( erlang:element(1, Info) )}, {<<"d"/utf8>>, ywt@internal@core:int_to_json( erlang:element(2, Info) )}, {<<"t"/utf8>>, ywt@internal@core:int_to_json( erlang:element(3, Info) )}] ) end )} | Fields] end end, Fields@2 = [{<<"alg"/utf8>>, ywt@algorithm:to_json(algorithm(Key))} | Fields@1], Fields@3 = case erlang:element(2, Key) of {some, Id} -> [{<<"kid"/utf8>>, gleam@json:string(Id)} | Fields@2]; none -> Fields@2 end, gleam@json:object(Fields@3).