Skip to main content

src/aws@internal@ecdsa_deterministic.erl

-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)}.