-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:8, _/bitstring>> ->
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>`. 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(<<N:256/big>>),
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 `<<k:size(256)-big-unsigned>>` 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:256/big-unsigned>> -> Candidate;
_assert_fail ->
erlang:error(#{gleam_error => let_assert,
message => <<"Pattern match failed, no pattern matched the value."/utf8>>,
file => <<?FILEPATH/utf8>>,
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_new/bitstring, 0:8>>),
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, <<Acc/bitstring, Byte:8>>)
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:256/big-unsigned>> -> H1_int;
_assert_fail ->
erlang:error(#{gleam_error => let_assert,
message => <<"Pattern match failed, no pattern matched the value."/utf8>>,
file => <<?FILEPATH/utf8>>,
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 = <<H1_mod_q:256/big>>,
V0 = repeat_byte(16#01, 32),
K0 = repeat_byte(16#00, 32),
K1 = aws_ffi:hmac_sha256(
K0,
<<V0/bitstring, 0:8, D_bytes/bitstring, Bits2octets_h1/bitstring>>
),
V1 = aws_ffi:hmac_sha256(K1, V0),
K2 = aws_ffi:hmac_sha256(
K1,
<<V1/bitstring, 1:8, D_bytes/bitstring, Bits2octets_h1/bitstring>>
),
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 => <<?FILEPATH/utf8>>,
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(<<K:256/big>>),
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 => <<?FILEPATH/utf8>>,
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:256/big-unsigned>> -> D;
_assert_fail ->
erlang:error(#{gleam_error => let_assert,
message => <<"Pattern match failed, no pattern matched the value."/utf8>>,
file => <<?FILEPATH/utf8>>,
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:256/big-unsigned>> -> E;
_assert_fail@1 ->
erlang:error(#{gleam_error => let_assert,
message => <<"Pattern match failed, no pattern matched the value."/utf8>>,
file => <<?FILEPATH/utf8>>,
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(<<N:256/big>>).
-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)}.