//// RFC 6979 deterministic ECDSA over NIST P-256 with SHA-256. //// //// The standard ECDSA algorithm picks a fresh random nonce `k` per //// signature; the signature varies between two signings of the //// same `(key, message)`. Erlang's `crypto:sign/4` follows that //// model — signatures are valid (server-side verify works) but //// not byte-reproducible. RFC 6979 derives `k` deterministically //// from `(d, h1)` via HMAC-DRBG, making signatures byte-for-byte //// reproducible against the reference vectors in §A.2.5 and //// against the aws-c-auth v4a corpus. //// //// **Architecture.** The HMAC-DRBG nonce derivation, modular //// arithmetic, and DER encoding are pure Gleam. The one expensive //// step — the elliptic-curve point multiplication `k·G` — is //// outsourced to Erlang's `crypto:generate_key(ecdh, secp256r1, k)` //// via the existing `aws_ffi:ecdsa_p256_public_key/1` FFI. That //// gives us the X coordinate of `k·G` from which the `r` half of //// the signature is `X mod q`. Computing `s` and DER-encoding the //// `(r, s)` pair is then pure integer arithmetic, which BEAM //// bignums handle natively. import aws/internal/crypto import gleam/bit_array /// NIST P-256 (secp256r1) curve order. Defined in NIST SP 800-186 /// §3.2.1.3. Every ECDSA-over-P256 scalar is reduced mod this. const p256_order: Int = 0xffffffff00000000ffffffffffffffffbce6faada7179e84f3b9cac2fc632551 /// Sign a 32-byte SHA-256 hash with a 32-byte P-256 private scalar. /// Returns the DER-encoded `(r, s)` blob — the same shape Erlang's /// `crypto:sign/4` returns for `ecdsa`, so callers can swap the two /// without touching downstream code. pub fn sign_p256(private_key: BitArray, message_hash: BitArray) -> BitArray { let #(r, s) = sign_p256_rs(private_key, message_hash) der_encode_sig(r, s) } /// Same signature as `sign_p256` but returns `(r_hex, s_hex)` as /// 64-character lowercase strings. Test-side helper for matching /// against the RFC 6979 reference vectors which list `r` / `s` as /// hex. pub fn sign_p256_rs_hex( private_key: BitArray, message_hash: BitArray, ) -> #(String, String) { let #(r, s) = sign_p256_rs(private_key, message_hash) #(int_to_hex_256(r), int_to_hex_256(s)) } fn sign_p256_rs(private_key: BitArray, message_hash: BitArray) -> #(Int, Int) { let assert <> = private_key let assert <> = message_hash let e_mod_q = e % p256_order sign_retry_loop(private_key, message_hash, d, e_mod_q, 0) } /// RFC 6979's outer retry loop: if `r == 0` or `s == 0` (both /// statistically impossible — ~2^-256 each — but the spec mandates /// the retry), pull a fresh `k` from the HMAC-DRBG and try again. /// The `attempt` arg threads into the nonce derivation so successive /// tries don't keep producing the same `k`. fn sign_retry_loop( private_key: BitArray, message_hash: BitArray, d: Int, e: Int, attempt: Int, ) -> #(Int, Int) { case attempt > 100 { True -> // Unreachable in practice; guard against pathological inputs. panic as "RFC 6979 sign: exceeded 100 nonce-retry attempts" False -> { let #(_, k) = derive_nonce(private_key, message_hash, attempt) // k·G via the existing scalar-multiplication FFI. The returned // uncompressed SEC1 form is `04 || X(32) || Y(32)`; we want // the X coordinate. let k_padded = pad_to_32(<>) let pubkey = ecdsa_p256_public_key(k_padded) let assert <<_:8, x_r:size(256)-big-unsigned, _:256>> = pubkey let r = x_r % p256_order case r == 0 { True -> sign_retry_loop(private_key, message_hash, d, e, attempt + 1) False -> { let k_inv = mod_inverse(k, p256_order) let s = mod_mul( k_inv, mod_add(e, mod_mul(r, d, p256_order), p256_order), p256_order, ) case s == 0 { True -> sign_retry_loop(private_key, message_hash, d, e, attempt + 1) False -> #(r, s) } } } } } } /// HMAC-DRBG nonce derivation per RFC 6979 §3.2. Returns the /// nonce both as a 32-byte big-endian bit array (for the /// EC point-multiplication FFI) and as an integer (for the /// `(r, s)` computation). `attempt` only matters when the outer /// retry loop fires; for the typical `attempt = 0` path it returns /// the canonical nonce. fn derive_nonce( d_bytes: BitArray, h1: BitArray, attempt: Int, ) -> #(BitArray, Int) { let assert <> = h1 let h1_mod_q = h1_int % p256_order let bits2octets_h1 = <> let v0 = repeat_byte(0x01, 32) let k0 = repeat_byte(0x00, 32) // Steps d-g of RFC 6979 §3.2. let k1 = crypto.hmac_sha256(k0, << v0:bits, 0:size(8), d_bytes:bits, bits2octets_h1:bits, >>) let v1 = crypto.hmac_sha256(k1, v0) let k2 = crypto.hmac_sha256(k1, << v1:bits, 1:size(8), d_bytes:bits, bits2octets_h1:bits, >>) let v2 = crypto.hmac_sha256(k2, v1) // Step h-k: each HMAC of V produces exactly 32 bytes = qlen // bits, so T = V after one round. Inner concatenation loop is // not needed for P-256 / SHA-256. nonce_loop(k2, v2, attempt) } fn nonce_loop(k: BitArray, v: BitArray, skip: Int) -> #(BitArray, Int) { let v_new = crypto.hmac_sha256(k, v) let assert <> = v_new case candidate >= 1 && candidate < p256_order && skip == 0 { True -> #(v_new, candidate) False -> { // Either the candidate is out of range, OR we're being asked // to skip `skip` valid candidates (the outer retry path). let k_next = crypto.hmac_sha256(k, <>) let v_next = crypto.hmac_sha256(k_next, v_new) let next_skip = case candidate >= 1 && candidate < p256_order { True -> skip - 1 False -> skip } nonce_loop(k_next, v_next, next_skip) } } } // ---------- modular arithmetic ---------- fn mod_add(a: Int, b: Int, m: Int) -> Int { { a + b } % m } fn mod_mul(a: Int, b: Int, m: Int) -> Int { a * b % m } /// Modular inverse via the extended Euclidean algorithm. Caller /// guarantees `gcd(a, m) == 1` — P-256's order is prime so any /// `a ∈ [1, q-1]` is coprime to `q`. fn mod_inverse(a: Int, m: Int) -> Int { let #(_g, x, _y) = ext_gcd(a, m) let result = x % m case result < 0 { True -> result + m False -> result } } fn ext_gcd(a: Int, b: Int) -> #(Int, Int, Int) { case b { 0 -> #(a, 1, 0) _ -> { let #(g, x1, y1) = ext_gcd(b, a % b) #(g, y1, x1 - { a / b } * y1) } } } // ---------- DER encoding ---------- fn der_encode_sig(r: Int, s: Int) -> BitArray { let r_int = der_int_bytes(r) let s_int = der_int_bytes(s) let inner_len = bit_array.byte_size(r_int) + bit_array.byte_size(s_int) <<0x30, inner_len:size(8), r_int:bits, s_int:bits>> } /// DER `INTEGER`: `0x02 LL `. Bytes are the minimum-length /// big-endian magnitude of `n` with a 0x00 prefix when the high bit /// of the magnitude is set (so it reads as unsigned). fn der_int_bytes(n: Int) -> BitArray { let raw = strip_leading_zeros(<>) let prefixed = case high_bit_set(raw) { True -> <<0:size(8), raw:bits>> False -> raw } <<0x02, { bit_array.byte_size(prefixed) }:size(8), prefixed:bits>> } fn strip_leading_zeros(b: BitArray) -> BitArray { case b { <<0:size(8), rest:bits>> -> case bit_array.byte_size(rest) > 0 { True -> strip_leading_zeros(rest) False -> b } _ -> b } } fn high_bit_set(b: BitArray) -> Bool { case b { <> -> first >= 0x80 _ -> False } } // ---------- bit / hex helpers ---------- fn repeat_byte(byte: Int, count: Int) -> BitArray { do_repeat(byte, count, <<>>) } fn do_repeat(byte: Int, remaining: Int, acc: BitArray) -> BitArray { case remaining { 0 -> acc _ -> do_repeat(byte, remaining - 1, <>) } } /// Bit-pattern `<>` always produces 32 /// bytes regardless of `k`'s magnitude. Provided as a function for /// signature clarity / future-proofing if we need to accept /// shorter inputs. fn pad_to_32(b: BitArray) -> BitArray { b } fn int_to_hex_256(n: Int) -> String { crypto.hex_encode(<>) } @external(erlang, "aws_ffi", "ecdsa_p256_public_key") fn ecdsa_p256_public_key(scalar: BitArray) -> BitArray