From a04f3868f2234e9587b6bed504efc9e17f3e7501 Mon Sep 17 00:00:00 2001 From: Chris Duncan Date: Mon, 17 Aug 2026 23:43:44 -0700 Subject: [PATCH] Refactor primitives to align with RFC 8032. --- src/assembly/index.ts | 183 ++++++++++++++++++++++++++---------------- 1 file changed, 114 insertions(+), 69 deletions(-) diff --git a/src/assembly/index.ts b/src/assembly/index.ts index 4a45a26..42f0f21 100644 --- a/src/assembly/index.ts +++ b/src/assembly/index.ts @@ -38,69 +38,106 @@ function clamp (k: StaticArray): void { k[31] |= 64 } -const crypto_derive_a: StaticArray = new StaticArray(PRIVATEKEY_BYTES) -const crypto_derive_az: StaticArray = new StaticArray(SECRETKEY_BYTES) +const crypto_derive_h: StaticArray = new StaticArray(SECRETKEY_BYTES) +const crypto_derive_s: StaticArray = new StaticArray(PRIVATEKEY_BYTES) /** * Hash seed to 32-byte scalar `a`, clamp it, then point-multiply it by the - * Ed25519 base point. - * @param pk 32-byte output buffer for the compressed public key - * @param seed 32-byte input buffer for the securely-random secret seed value + * Ed25519 base point. The Nano specification uses BLAKE2b as the hash function + * instead of SHA-512 specified by RFC 8032. + * + * https://www.rfc-editor.org/info/rfc8032/#section-5.1.5 + * + * @param A 32-byte output buffer for the compressed public key + * @param key 32-byte private key "seed" of cryptographically secure random data */ -function crypto_derive (pk: StaticArray, seed: StaticArray): void { - const a = crypto_derive_a - const az = crypto_derive_az - blake2b.init().update(seed, 32).digest(az) - memory.copy(changetype(a), changetype(az), 32) - az.fill(0) - clamp(a) - ge_scalarmult_base_tobytes(pk, a) - a.fill(0) +function crypto_derive (A: StaticArray, key: StaticArray): void { + const h = crypto_derive_h + const s = crypto_derive_s + // Hash private key to 64-byte buffer `h` + blake2b.init().update(key, 32).digest(h) + // Keep only the lower 32 bytes as secret scalar `s` for deriving public key + memory.copy(changetype(s), changetype(h), 32) + h.fill(0) + // Clear lowest three bits and highest bit, then set second highest bit + clamp(s) + // Perform fixed-base scalar multiplication `[s]B`, output to public key `A` + ge_scalarmult_base_tobytes(A, s) + s.fill(0) } -const crypto_sign_a = new StaticArray(32) -const crypto_sign_z = new StaticArray(32) -const crypto_sign_az = new StaticArray(64) -const crypto_sign_nonce = new StaticArray(64) -const crypto_sign_hram = new StaticArray(64) -const crypto_sign_t = new StaticArray(PRIVATEKEY_BYTES) -function crypto_sign (s: StaticArray, m: usize, mlen: i32, sk: StaticArray): void { - const a = crypto_sign_a - const z = crypto_sign_z - const az = crypto_sign_az - const nonce = crypto_sign_nonce - const hram = crypto_sign_hram - const t = changetype(crypto_sign_t) - - // Hash secret key to private scalar `a` and prefix for nonce derivation `z` - memory.copy(t, changetype(sk), PRIVATEKEY_BYTES) - blake2b.init().update(t, PRIVATEKEY_BYTES).digest(az) - memory.copy(changetype(a), changetype(az), 32) - memory.copy(changetype(a), changetype(az) + 32, 32) - clamp(a) - - // Derive nonce from prefix `z` and message `m` - blake2b.init().update(z, 32).update(changetype>(m), mlen).digest(nonce) - sc_reduce(nonce) - - // Compute R = rB, output to bytes s - ge_scalarmult_base_tobytes(s, nonce) - - // Concatenate public key `A` and message `M` from parameter arguments: - // `A = sk[32,63], M = m` - // Compute challenge hash using `s = (R || A || M)` - blake2b.init().update(s, 32).update(sk + 32, 32).update(m, mlen).digest(hram) - sc_reduce(hram) - - // Compute `S = (r + h*a) mod L` and construct final signature `s = (R || S)` - sc_muladd(changetype>(changetype(s) + 32), az, hram, nonce) - - // Clean up sensitive data - a.fill(0) - z.fill(0) - az.fill(0) - hram.fill(0) - nonce.fill(0) - memory.fill(t, 0, PRIVATEKEY_BYTES) +const crypto_sign_A = new StaticArray(32) +const crypto_sign_h = new StaticArray(64) +const crypto_sign_s = new StaticArray(32) +const crypto_sign_prefix = new StaticArray(32) +const crypto_sign_r = new StaticArray(64) +const crypto_sign_R = new StaticArray(64) +const crypto_sign_k = new StaticArray(64) +const crypto_sign_S = new StaticArray(64) +/** + * Sign a message with a secret key. The Nano specification uses BLAKE2b as the + * hash function instead of SHA-512 specified by RFC 8032. + * + * In this implementation, the secret key is a 64-byte concatenation of the + * private key and its public key in order to (1) ensure the user has a full + * correct keypair for data integrity, and (2) improve performance by avoiding + * expensive point multiplication and instead taking a public key that can be + * computed once and cached. This behavior deviates from RFC 8032 which + * indicates the public key should be recomputed. + * + * https://www.rfc-editor.org/info/rfc8032/#section-5.1.6 + * + * @param RS 64-byte output buffer for detached signature + * @param M variable-length message to be signed + * @param mlen bytelength of `m` + * @param sk 64-byte input buffer of private+public key concatenation + */ +function crypto_sign (RS: StaticArray, M: StaticArray, mlen: i32, key: StaticArray, A: StaticArray): void { + const h = crypto_sign_h + const s = crypto_sign_s + const prefix = crypto_sign_prefix + const r = crypto_sign_r + const R = crypto_sign_R + const k = crypto_sign_k + const S = crypto_sign_S + + // Hash private key to `h` + blake2b.init().update(key, PRIVATEKEY_BYTES).digest(h) + key.fill(0) + + // Split `h` into clamped secret scalar `s` and nonce prefix + memory.copy(changetype(s), changetype(h), 32) + clamp(s) + memory.copy(changetype(prefix), changetype(h) + 32, 32) + h.fill(0) + + // Hash prefix and message `M` to nonce `r` + blake2b.init().update(prefix, 32).update(M, mlen).digest(r) + prefix.fill(0) + + // Reduce `r` modulo `L`, the group order of the base point `B` + sc_reduce(r) + + // Perform fixed-base scalar multiplication `[r]B`, output to point `R` + ge_scalarmult_base_tobytes(R, r) + + // Compute challenge hash `blake2b(R || A || M)`, output to `k` + blake2b.init().update(R, 32).update(A, 32).update(M, mlen).digest(k) + A.fill(0) + + // Reduce `k` modulo `L` for efficiency + sc_reduce(k) + + // Compute `S = (k * s + r) mod L` + sc_muladd(S, k, s, r) + k.fill(0) + s.fill(0) + r.fill(0) + + // Construct final signature `(R || S)` + memory.copy(changetype(RS), changetype(R), 32) + R.fill(0) + memory.copy(changetype(RS) + 32, changetype(S), 32) + S.fill(0) } const crypto_verify_h = new StaticArray(64) @@ -113,7 +150,8 @@ const crypto_verify_S = new StaticArray(32) * Verify signature `s` was made by signing message `m` using public key `pk`. * @returns -1 if signature fails to verify, else return 0 if signature is good */ -function crypto_verify (s: StaticArray, m: usize, mlen: i32, pk: StaticArray): i32 { +function crypto_verify (s: StaticArray, m: usize, mlen: i32, pub: StaticArray): i32 { + const M = changetype>(m) const h = crypto_verify_h const check = crypto_verify_check const expected_r = crypto_verify_expected_r @@ -122,13 +160,13 @@ function crypto_verify (s: StaticArray, m: usize, mlen: i32, pk: StaticArray const S = crypto_verify_S // fail if public key `k` is non-canonical (`p = 2²⁵⁵-19 <= k`) - if (!ge_is_canonical(pk)) return -1 + if (!ge_is_canonical(pub)) return -1 // fail if private scalar `S` is non-canonical (`L <= S`) memory.copy(changetype(S), changetype(s) + 32, 32) if (!sc_is_canonical(S)) return -1 - if (ge_frombytes_negate_vartime(A, pk) != 0) return -1 + if (ge_frombytes_negate_vartime(A, pub) != 0) return -1 if (ge_has_small_order(A) != 0) return -1 if (ge_frombytes(expected_r, s) != 0) return -1 @@ -138,7 +176,7 @@ function crypto_verify (s: StaticArray, m: usize, mlen: i32, pk: StaticArray // data to hash is nonce point R, public key A, and message M // from parameter arguments: R = s[0,32], A = pk, M = m // R, S, A, and M are all 32-byte values in this implementation - blake2b.init().update(s, 32).update(pk, 32).update(m, mlen).digest(h) + blake2b.init().update(s, 32).update(pub, 32).update(M, mlen).digest(h) sc_reduce(h) ge_double_scalarmult_vartime_to_p3(sb_ah, h, A, S) @@ -167,14 +205,16 @@ export function getMessagePointer (): usize { * static output buffer. */ export function derive (): void { - const pk = changetype>(OUTPUT_BUFFER) - const seed = changetype>(INPUT_BUFFER) + const pub = changetype>(OUTPUT_BUFFER) + const prv = changetype>(INPUT_BUFFER) memory.fill(OUTPUT_BUFFER, 0, OUTPUT_BUFFER_BYTES) - crypto_derive(pk, seed) + crypto_derive(pub, prv) memory.fill(INPUT_BUFFER, 0, INPUT_BUFFER_BYTES) } const sign_s = new StaticArray(SIGNATURE_BYTES) +const sign_prv = new StaticArray(PRIVATEKEY_BYTES) +const sign_pub = new StaticArray(PUBLICKEY_BYTES) /** * Retrieve up to 32 KiB of data from the message buffer and a 64-byte secret * key from the input buffer, and then sign the message using a secret key. The @@ -185,13 +225,18 @@ const sign_s = new StaticArray(SIGNATURE_BYTES) export function sign (mlen: i32): void { if (mlen < 0 || mlen > 32768) throw new Error() const s = sign_s - const m = MESSAGE_BUFFER - const sk = changetype>(INPUT_BUFFER) + const prv = sign_prv + const pub = sign_pub + + memory.copy(changetype(prv), INPUT_BUFFER, PRIVATEKEY_BYTES) + memory.copy(changetype(pub), INPUT_BUFFER + PRIVATEKEY_BYTES, PUBLICKEY_BYTES) + memory.fill(INPUT_BUFFER, 0, INPUT_BUFFER_BYTES) + + const m = changetype>(MESSAGE_BUFFER) memory.fill(OUTPUT_BUFFER, 0, OUTPUT_BUFFER_BYTES) - crypto_sign(s, m, mlen, sk) + crypto_sign(s, m, mlen, prv, pub) memory.copy(OUTPUT_BUFFER, changetype(s), SIGNATURE_BYTES) - memory.fill(INPUT_BUFFER, 0, INPUT_BUFFER_BYTES) } const verify_s = new StaticArray(SIGNATURE_BYTES) -- 2.52.0