-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, algorithm/1, decoder/0, parse_jwk/1, id/1, match/5, 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", 107). -spec hmac_key( gleam@option:option(binary()), ywt@internal@core:digest_type(), bitstring() ) -> {ok, sign_key()} | {error, nil}. hmac_key(Id, Digest_type, Secret) -> Minimum_bits = case Digest_type of sha256 -> 256; sha384 -> 384; sha512 -> 512 end, case erlang:bit_size(Secret) >= Minimum_bits of true -> {ok, {sign_hmac, Id, Digest_type, Secret}}; false -> {error, nil} end. -file("src/ywt/sign_key.gleam", 75). ?DOC( " Creates an HMAC-SHA256 signing key.\n" "\n" " The secret must be at least 256 bits. HMAC keys are symmetric, so every\n" " verifier that receives this key can also sign tokens. The length check does\n" " not prove the bytes are random; use `ywt.generate_key` or a CSPRNG.\n" "\n" " ```gleam\n" " let assert Ok(key) = sign_key.hs256(secret_32_bytes)\n" " ```\n" ). -spec hs256(bitstring()) -> {ok, sign_key()} | {error, nil}. hs256(Secret) -> hmac_key(none, sha256, Secret). -file("src/ywt/sign_key.gleam", 90). ?DOC( " Creates an HMAC-SHA384 signing key.\n" "\n" " The secret must be at least 384 bits. HMAC keys are symmetric, so every\n" " verifier that receives this key can also sign tokens. The length check does\n" " not prove the bytes are random; use `ywt.generate_key` or a CSPRNG.\n" "\n" " ```gleam\n" " let assert Ok(key) = sign_key.hs384(secret_48_bytes)\n" " ```\n" ). -spec hs384(bitstring()) -> {ok, sign_key()} | {error, nil}. hs384(Secret) -> hmac_key(none, sha384, Secret). -file("src/ywt/sign_key.gleam", 103). ?DOC( " Creates an HMAC-SHA512 signing key.\n" "\n" " The secret must be at least 512 bits. HMAC keys are symmetric, so every\n" " verifier that receives this key can also sign tokens. The length check does\n" " not prove the bytes are random; use `ywt.generate_key` or a CSPRNG.\n" "\n" " ```gleam\n" " let assert Ok(key) = sign_key.hs512(secret_64_bytes)\n" " ```\n" ). -spec hs512(bitstring()) -> {ok, sign_key()} | {error, nil}. hs512(Secret) -> hmac_key(none, sha512, Secret). -file("src/ywt/sign_key.gleam", 132). ?DOC( " Assigns a random key id to a signing key.\n" "\n" " The id is written to JWT headers as `kid`, letting verifiers choose the right\n" " key during rotation.\n" "\n" " ```gleam\n" " let key = sign_key.with_random_id(key)\n" " ```\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", 403). -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", 296). -spec rsa_private_parameters_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_private_parameters_decoder( Id, Digest_type, Padding, Public_exponent, Modulus, Private_exponent ) -> Field_decoder = ywt@internal@core:int_decoder(), Optional_int = gleam@dynamic@decode:map( Field_decoder, fun(Field@0) -> {some, Field@0} end ), Simple_key = {sign_rsa_simple, Id, Digest_type, Public_exponent, Modulus, Private_exponent, Padding}, gleam@dynamic@decode:optional_field( <<"p"/utf8>>, none, Optional_int, fun(First_prime_factor) -> gleam@dynamic@decode:optional_field( <<"q"/utf8>>, none, Optional_int, fun(Second_prime_factor) -> gleam@dynamic@decode:optional_field( <<"dp"/utf8>>, none, Optional_int, fun(First_factor_crt_exponent) -> gleam@dynamic@decode:optional_field( <<"dq"/utf8>>, none, Optional_int, fun(Second_factor_crt_exponent) -> gleam@dynamic@decode:optional_field( <<"qi"/utf8>>, none, Optional_int, fun(First_crt_coefficient) -> gleam@dynamic@decode:optional_field( <<"oth"/utf8>>, [], gleam@dynamic@decode:list( other_primes_info_decoder() ), fun(Other_primes_info) -> case {First_prime_factor, Second_prime_factor, First_factor_crt_exponent, Second_factor_crt_exponent, First_crt_coefficient, Other_primes_info} of {none, none, none, none, none, []} -> gleam@dynamic@decode:success( Simple_key ); {{some, First_prime_factor@1}, {some, Second_prime_factor@1}, {some, First_factor_crt_exponent@1}, {some, Second_factor_crt_exponent@1}, {some, First_crt_coefficient@1}, Other_primes_info@1} -> gleam@dynamic@decode:success( {sign_rsa_full, Id, Digest_type, Public_exponent, Modulus, Private_exponent, First_prime_factor@1, Second_prime_factor@1, First_factor_crt_exponent@1, Second_factor_crt_exponent@1, First_crt_coefficient@1, Other_primes_info@1, Padding} ); {_, _, _, _, _, _} -> gleam@dynamic@decode:failure( Simple_key, <<"RSA CRT parameters"/utf8>> ) end end ) end ) end ) end ) end ) end ). -file("src/ywt/sign_key.gleam", 486). ?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", 249). -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) -> 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(_) -> rsa_private_parameters_decoder( Id, Digest_type, Padding@1, Public_exponent, Modulus, Private_exponent ) end ) end ) end ) end ) end ) end ). -file("src/ywt/sign_key.gleam", 198). -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) -> 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) -> gleam@dynamic@decode:field( <<"d"/utf8>>, ywt@internal@core:bits_of_length_decoder( Coord_size ), fun(Private_key) -> Public_key = <<16#4, X/bitstring, Y/bitstring>>, Digest_type = case Crv of secp256r1 -> sha256; secp384r1 -> sha384; secp521r1 -> sha512 end, Sign_key = {sign_ecdsa, Id, Crv, Digest_type, Public_key, Private_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( Sign_key ); false -> gleam@dynamic@decode:failure( Sign_key, <<"alg"/utf8>> ) end end ) end ) end ) end ) end ). -file("src/ywt/sign_key.gleam", 183). -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), case ywt@algorithm:is_hmac(Alg) of true -> case hmac_key(Id, Digest_type, K) of {ok, Key} -> gleam@dynamic@decode:success(Key); {error, _} -> gleam@dynamic@decode:failure( {sign_hmac, Id, Digest_type, K}, <<"k"/utf8>> ) end; false -> gleam@dynamic@decode:failure( {sign_hmac, Id, Digest_type, K}, <<"alg"/utf8>> ) end end ) end ). -file("src/ywt/sign_key.gleam", 157). ?DOC( " Decodes a private JWK into a signing key.\n" "\n" " Only decode private keys from trusted storage. Unknown JWK fields are ignored;\n" " `key_ops` and `use` are not enforced. HMAC and RSA signing JWKs must include\n" " `alg`; EC signing JWKs may omit `alg` when the curve determines it.\n" " For raw JSON strings, prefer `parse_jwk` so duplicate member handling is\n" " consistent on all targets.\n" "\n" " ```gleam\n" " json.parse(private_jwk, sign_key.decoder())\n" " ```\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", 179). ?DOC( " Parses a private JWK into a signing 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" " sign_key.parse_jwk(private_jwk)\n" " ```\n" ). -spec parse_jwk(binary()) -> {ok, sign_key()} | {error, gleam@json:decode_error()}. parse_jwk(Private_jwk) -> ywt@internal@jose_json:parse(Private_jwk, decoder()). -file("src/ywt/sign_key.gleam", 426). ?DOC( " Returns the key id, if one is set.\n" "\n" " ```gleam\n" " sign_key.id(key)\n" " ```\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", 433). ?DOC(false). -spec match( sign_key(), fun((gleam@option:option(binary()), ywt@internal@core:named_curve(), ywt@internal@core:digest_type(), bitstring(), bitstring()) -> FBW), fun((gleam@option:option(binary()), ywt@internal@core:digest_type(), bigi:big_int(), bigi:big_int(), bigi:big_int(), ywt@internal@core:padding()) -> FBW), 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()) -> FBW), fun((gleam@option:option(binary()), ywt@internal@core:digest_type(), bitstring()) -> FBW) ) -> FBW. 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", 537). ?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 => 545, value => _assert_fail, start => 18525, 'end' => 18620, pattern_start => 18536, pattern_end => 18595}) 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).