-module(aws@internal@ecdsa_deterministic). -compile([no_auto_import, nowarn_unused_vars, nowarn_unused_function, nowarn_nomatch, inline]). -define(FILEPATH, "src/aws/internal/ecdsa_deterministic.gleam"). -export([sign_p256/2, sign_p256_rs_hex/2]). -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( " RFC 6979 deterministic ECDSA over NIST P-256 with SHA-256.\n" "\n" " The standard ECDSA algorithm picks a fresh random nonce `k` per\n" " signature; the signature varies between two signings of the\n" " same `(key, message)`. Erlang's `crypto:sign/4` follows that\n" " model — signatures are valid (server-side verify works) but\n" " not byte-reproducible. RFC 6979 derives `k` deterministically\n" " from `(d, h1)` via HMAC-DRBG, making signatures byte-for-byte\n" " reproducible against the reference vectors in §A.2.5 and\n" " against the aws-c-auth v4a corpus.\n" "\n" " **Architecture.** The HMAC-DRBG nonce derivation, modular\n" " arithmetic, and DER encoding are pure Gleam. The one expensive\n" " step — the elliptic-curve point multiplication `k·G` — is\n" " outsourced to Erlang's `crypto:generate_key(ecdh, secp256r1, k)`\n" " via the existing `aws_ffi:ecdsa_p256_public_key/1` FFI. That\n" " gives us the X coordinate of `k·G` from which the `r` half of\n" " the signature is `X mod q`. Computing `s` and DER-encoding the\n" " `(r, s)` pair is then pure integer arithmetic, which BEAM\n" " bignums handle natively.\n" ). -file("src/aws/internal/ecdsa_deterministic.gleam", 222). -spec high_bit_set(bitstring()) -> boolean(). high_bit_set(B) -> case B of <> -> First >= 16#80; _ -> false end. -file("src/aws/internal/ecdsa_deterministic.gleam", 211). -spec strip_leading_zeros(bitstring()) -> bitstring(). strip_leading_zeros(B) -> case B of <<0:8, Rest/bitstring>> -> case erlang:byte_size(Rest) > 0 of true -> strip_leading_zeros(Rest); false -> B end; _ -> B end. -file("src/aws/internal/ecdsa_deterministic.gleam", 202). ?DOC( " DER `INTEGER`: `0x02 LL `. Bytes are the minimum-length\n" " big-endian magnitude of `n` with a 0x00 prefix when the high bit\n" " of the magnitude is set (so it reads as unsigned).\n" ). -spec der_int_bytes(integer()) -> bitstring(). der_int_bytes(N) -> Raw = strip_leading_zeros(<>), Prefixed = case high_bit_set(Raw) of true -> <<0:8, Raw/bitstring>>; false -> Raw end, <<16#02, ((erlang:byte_size(Prefixed))):8, Prefixed/bitstring>>. -file("src/aws/internal/ecdsa_deterministic.gleam", 192). -spec der_encode_sig(integer(), integer()) -> bitstring(). der_encode_sig(R, S) -> R_int = der_int_bytes(R), S_int = der_int_bytes(S), Inner_len = erlang:byte_size(R_int) + erlang:byte_size(S_int), <<16#30, Inner_len:8, R_int/bitstring, S_int/bitstring>>. -file("src/aws/internal/ecdsa_deterministic.gleam", 164). -spec mod_mul(integer(), integer(), integer()) -> integer(). mod_mul(A, B, M) -> case M of 0 -> 0; Gleam@denominator -> A * B rem Gleam@denominator end. -file("src/aws/internal/ecdsa_deterministic.gleam", 160). -spec mod_add(integer(), integer(), integer()) -> integer(). mod_add(A, B, M) -> case M of 0 -> 0; Gleam@denominator -> (A + B) rem Gleam@denominator end. -file("src/aws/internal/ecdsa_deterministic.gleam", 180). -spec ext_gcd(integer(), integer()) -> {integer(), integer(), integer()}. ext_gcd(A, B) -> case B of 0 -> {A, 1, 0}; _ -> {G, X1, Y1} = ext_gcd(B, case B of 0 -> 0; Gleam@denominator -> A rem Gleam@denominator end), {G, Y1, X1 - ((case B of 0 -> 0; Gleam@denominator@1 -> A div Gleam@denominator@1 end) * Y1)} end. -file("src/aws/internal/ecdsa_deterministic.gleam", 171). ?DOC( " Modular inverse via the extended Euclidean algorithm. Caller\n" " guarantees `gcd(a, m) == 1` — P-256's order is prime so any\n" " `a ∈ [1, q-1]` is coprime to `q`.\n" ). -spec mod_inverse(integer(), integer()) -> integer(). mod_inverse(A, M) -> {_, X, _} = ext_gcd(A, M), Result = case M of 0 -> 0; Gleam@denominator -> X rem Gleam@denominator end, case Result < 0 of true -> Result + M; false -> Result end. -file("src/aws/internal/ecdsa_deterministic.gleam", 246). ?DOC( " Bit-pattern `<>` always produces 32\n" " bytes regardless of `k`'s magnitude. Provided as a function for\n" " signature clarity / future-proofing if we need to accept\n" " shorter inputs.\n" ). -spec pad_to_32(bitstring()) -> bitstring(). pad_to_32(B) -> B. -file("src/aws/internal/ecdsa_deterministic.gleam", 139). -spec nonce_loop(bitstring(), bitstring(), integer()) -> {bitstring(), integer()}. nonce_loop(K, V, Skip) -> V_new = aws_ffi:hmac_sha256(K, V), Candidate@1 = case V_new of <> -> Candidate; _assert_fail -> erlang:error(#{gleam_error => let_assert, message => <<"Pattern match failed, no pattern matched the value."/utf8>>, file => <>, module => <<"aws/internal/ecdsa_deterministic"/utf8>>, function => <<"nonce_loop"/utf8>>, line => 141, value => _assert_fail, start => 5392, 'end' => 5447, pattern_start => 5403, pattern_end => 5439}) end, case ((Candidate@1 >= 1) andalso (Candidate@1 < 16#ffffffff00000000ffffffffffffffffbce6faada7179e84f3b9cac2fc632551)) andalso (Skip =:= 0) of true -> {V_new, Candidate@1}; false -> K_next = aws_ffi:hmac_sha256(K, <>), V_next = aws_ffi:hmac_sha256(K_next, V_new), Next_skip = case (Candidate@1 >= 1) andalso (Candidate@1 < 16#ffffffff00000000ffffffffffffffffbce6faada7179e84f3b9cac2fc632551) of true -> Skip - 1; false -> Skip end, nonce_loop(K_next, V_next, Next_skip) end. -file("src/aws/internal/ecdsa_deterministic.gleam", 235). -spec do_repeat(integer(), integer(), bitstring()) -> bitstring(). do_repeat(Byte, Remaining, Acc) -> case Remaining of 0 -> Acc; _ -> do_repeat(Byte, Remaining - 1, <>) end. -file("src/aws/internal/ecdsa_deterministic.gleam", 231). -spec repeat_byte(integer(), integer()) -> bitstring(). repeat_byte(Byte, Count) -> do_repeat(Byte, Count, <<>>). -file("src/aws/internal/ecdsa_deterministic.gleam", 109). ?DOC( " HMAC-DRBG nonce derivation per RFC 6979 §3.2. Returns the\n" " nonce both as a 32-byte big-endian bit array (for the\n" " EC point-multiplication FFI) and as an integer (for the\n" " `(r, s)` computation). `attempt` only matters when the outer\n" " retry loop fires; for the typical `attempt = 0` path it returns\n" " the canonical nonce.\n" ). -spec derive_nonce(bitstring(), bitstring(), integer()) -> {bitstring(), integer()}. derive_nonce(D_bytes, H1, Attempt) -> H1_int@1 = case H1 of <> -> H1_int; _assert_fail -> erlang:error(#{gleam_error => let_assert, message => <<"Pattern match failed, no pattern matched the value."/utf8>>, file => <>, module => <<"aws/internal/ecdsa_deterministic"/utf8>>, function => <<"derive_nonce"/utf8>>, line => 114, value => _assert_fail, start => 4544, 'end' => 4593, pattern_start => 4555, pattern_end => 4588}) end, H1_mod_q = case 16#ffffffff00000000ffffffffffffffffbce6faada7179e84f3b9cac2fc632551 of 0 -> 0; Gleam@denominator -> H1_int@1 rem Gleam@denominator end, Bits2octets_h1 = <>, V0 = repeat_byte(16#01, 32), K0 = repeat_byte(16#00, 32), K1 = aws_ffi:hmac_sha256( K0, <> ), V1 = aws_ffi:hmac_sha256(K1, V0), K2 = aws_ffi:hmac_sha256( K1, <> ), V2 = aws_ffi:hmac_sha256(K2, V1), nonce_loop(K2, V2, Attempt). -file("src/aws/internal/ecdsa_deterministic.gleam", 62). ?DOC( " RFC 6979's outer retry loop: if `r == 0` or `s == 0` (both\n" " statistically impossible — ~2^-256 each — but the spec mandates\n" " the retry), pull a fresh `k` from the HMAC-DRBG and try again.\n" " The `attempt` arg threads into the nonce derivation so successive\n" " tries don't keep producing the same `k`.\n" ). -spec sign_retry_loop(bitstring(), bitstring(), integer(), integer(), integer()) -> {integer(), integer()}. sign_retry_loop(Private_key, Message_hash, D, E, Attempt) -> case Attempt > 100 of true -> erlang:error(#{gleam_error => panic, message => <<"RFC 6979 sign: exceeded 100 nonce-retry attempts"/utf8>>, file => <>, module => <<"aws/internal/ecdsa_deterministic"/utf8>>, function => <<"sign_retry_loop"/utf8>>, line => 72}); false -> {_, K} = derive_nonce(Private_key, Message_hash, Attempt), K_padded = pad_to_32(<>), Pubkey = aws_ffi:ecdsa_p256_public_key(K_padded), X_r@1 = case Pubkey of <<_:8, X_r:256/big-unsigned, _:256>> -> X_r; _assert_fail -> erlang:error(#{gleam_error => let_assert, message => <<"Pattern match failed, no pattern matched the value."/utf8>>, file => <>, module => <<"aws/internal/ecdsa_deterministic"/utf8>>, function => <<"sign_retry_loop"/utf8>>, line => 80, value => _assert_fail, start => 3492, 'end' => 3554, pattern_start => 3503, pattern_end => 3545}) end, R = case 16#ffffffff00000000ffffffffffffffffbce6faada7179e84f3b9cac2fc632551 of 0 -> 0; Gleam@denominator -> X_r@1 rem Gleam@denominator end, case R =:= 0 of true -> sign_retry_loop( Private_key, Message_hash, D, E, Attempt + 1 ); false -> K_inv = mod_inverse( K, 16#ffffffff00000000ffffffffffffffffbce6faada7179e84f3b9cac2fc632551 ), S = mod_mul( K_inv, mod_add( E, mod_mul( R, D, 16#ffffffff00000000ffffffffffffffffbce6faada7179e84f3b9cac2fc632551 ), 16#ffffffff00000000ffffffffffffffffbce6faada7179e84f3b9cac2fc632551 ), 16#ffffffff00000000ffffffffffffffffbce6faada7179e84f3b9cac2fc632551 ), case S =:= 0 of true -> sign_retry_loop( Private_key, Message_hash, D, E, Attempt + 1 ); false -> {R, S} end end end. -file("src/aws/internal/ecdsa_deterministic.gleam", 50). -spec sign_p256_rs(bitstring(), bitstring()) -> {integer(), integer()}. sign_p256_rs(Private_key, Message_hash) -> D@1 = case Private_key of <> -> D; _assert_fail -> erlang:error(#{gleam_error => let_assert, message => <<"Pattern match failed, no pattern matched the value."/utf8>>, file => <>, module => <<"aws/internal/ecdsa_deterministic"/utf8>>, function => <<"sign_p256_rs"/utf8>>, line => 51, value => _assert_fail, start => 2316, 'end' => 2369, pattern_start => 2327, pattern_end => 2355}) end, E@1 = case Message_hash of <> -> E; _assert_fail@1 -> erlang:error(#{gleam_error => let_assert, message => <<"Pattern match failed, no pattern matched the value."/utf8>>, file => <>, module => <<"aws/internal/ecdsa_deterministic"/utf8>>, function => <<"sign_p256_rs"/utf8>>, line => 52, value => _assert_fail@1, start => 2372, 'end' => 2426, pattern_start => 2383, pattern_end => 2411}) end, E_mod_q = case 16#ffffffff00000000ffffffffffffffffbce6faada7179e84f3b9cac2fc632551 of 0 -> 0; Gleam@denominator -> E@1 rem Gleam@denominator end, sign_retry_loop(Private_key, Message_hash, D@1, E_mod_q, 0). -file("src/aws/internal/ecdsa_deterministic.gleam", 33). ?DOC( " Sign a 32-byte SHA-256 hash with a 32-byte P-256 private scalar.\n" " Returns the DER-encoded `(r, s)` blob — the same shape Erlang's\n" " `crypto:sign/4` returns for `ecdsa`, so callers can swap the two\n" " without touching downstream code.\n" ). -spec sign_p256(bitstring(), bitstring()) -> bitstring(). sign_p256(Private_key, Message_hash) -> {R, S} = sign_p256_rs(Private_key, Message_hash), der_encode_sig(R, S). -file("src/aws/internal/ecdsa_deterministic.gleam", 250). -spec int_to_hex_256(integer()) -> binary(). int_to_hex_256(N) -> aws_ffi:hex_encode(<>). -file("src/aws/internal/ecdsa_deterministic.gleam", 42). ?DOC( " Same signature as `sign_p256` but returns `(r_hex, s_hex)` as\n" " 64-character lowercase strings. Test-side helper for matching\n" " against the RFC 6979 reference vectors which list `r` / `s` as\n" " hex.\n" ). -spec sign_p256_rs_hex(bitstring(), bitstring()) -> {binary(), binary()}. sign_p256_rs_hex(Private_key, Message_hash) -> {R, S} = sign_p256_rs(Private_key, Message_hash), {int_to_hex_256(R), int_to_hex_256(S)}.