]> git.codecow.com Git - nano25519.git/commitdiff
Refactor primitives to align with RFC 8032.
authorChris Duncan <chris@zoso.dev>
Tue, 18 Aug 2026 06:43:44 +0000 (23:43 -0700)
committerChris Duncan <chris@zoso.dev>
Tue, 18 Aug 2026 06:43:44 +0000 (23:43 -0700)
src/assembly/index.ts

index 4a45a26dce191b44a01e4d2e453927a844c6061a..42f0f2174af4a39157bdb6d57550e33e6e7f7350 100644 (file)
@@ -38,69 +38,106 @@ function clamp (k: StaticArray<u8>): void {
        k[31] |= 64
 }
 
-const crypto_derive_a: StaticArray<u8> = new StaticArray<u8>(PRIVATEKEY_BYTES)
-const crypto_derive_az: StaticArray<u8> = new StaticArray<u8>(SECRETKEY_BYTES)
+const crypto_derive_h: StaticArray<u8> = new StaticArray<u8>(SECRETKEY_BYTES)
+const crypto_derive_s: StaticArray<u8> = new StaticArray<u8>(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<u8>, seed: StaticArray<u8>): void {
-       const a = crypto_derive_a
-       const az = crypto_derive_az
-       blake2b.init().update(seed, 32).digest(az)
-       memory.copy(changetype<usize>(a), changetype<usize>(az), 32)
-       az.fill(0)
-       clamp(a)
-       ge_scalarmult_base_tobytes(pk, a)
-       a.fill(0)
+function crypto_derive (A: StaticArray<u8>, key: StaticArray<u8>): 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<usize>(s), changetype<usize>(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<u8>(32)
-const crypto_sign_z = new StaticArray<u8>(32)
-const crypto_sign_az = new StaticArray<u8>(64)
-const crypto_sign_nonce = new StaticArray<u8>(64)
-const crypto_sign_hram = new StaticArray<u8>(64)
-const crypto_sign_t = new StaticArray<u8>(PRIVATEKEY_BYTES)
-function crypto_sign (s: StaticArray<u8>, m: usize, mlen: i32, sk: StaticArray<u8>): 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<usize>(crypto_sign_t)
-
-       // Hash secret key to private scalar `a` and prefix for nonce derivation `z`
-       memory.copy(t, changetype<usize>(sk), PRIVATEKEY_BYTES)
-       blake2b.init().update(t, PRIVATEKEY_BYTES).digest(az)
-       memory.copy(changetype<usize>(a), changetype<usize>(az), 32)
-       memory.copy(changetype<usize>(a), changetype<usize>(az) + 32, 32)
-       clamp(a)
-
-       // Derive nonce from prefix `z` and message `m`
-       blake2b.init().update(z, 32).update(changetype<StaticArray<u8>>(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<StaticArray<u8>>(changetype<usize>(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<u8>(32)
+const crypto_sign_h = new StaticArray<u8>(64)
+const crypto_sign_s = new StaticArray<u8>(32)
+const crypto_sign_prefix = new StaticArray<u8>(32)
+const crypto_sign_r = new StaticArray<u8>(64)
+const crypto_sign_R = new StaticArray<u8>(64)
+const crypto_sign_k = new StaticArray<u8>(64)
+const crypto_sign_S = new StaticArray<u8>(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<u8>, M: StaticArray<u8>, mlen: i32, key: StaticArray<u8>, A: StaticArray<u8>): 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<usize>(s), changetype<usize>(h), 32)
+       clamp(s)
+       memory.copy(changetype<usize>(prefix), changetype<usize>(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<usize>(RS), changetype<usize>(R), 32)
+       R.fill(0)
+       memory.copy(changetype<usize>(RS) + 32, changetype<usize>(S), 32)
+       S.fill(0)
 }
 
 const crypto_verify_h = new StaticArray<u8>(64)
@@ -113,7 +150,8 @@ const crypto_verify_S = new StaticArray<u8>(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<u8>, m: usize, mlen: i32, pk: StaticArray<u8>): i32 {
+function crypto_verify (s: StaticArray<u8>, m: usize, mlen: i32, pub: StaticArray<u8>): i32 {
+       const M = changetype<StaticArray<u8>>(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<u8>, 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<usize>(S), changetype<usize>(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<u8>, 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<StaticArray<u8>>(OUTPUT_BUFFER)
-       const seed = changetype<StaticArray<u8>>(INPUT_BUFFER)
+       const pub = changetype<StaticArray<u8>>(OUTPUT_BUFFER)
+       const prv = changetype<StaticArray<u8>>(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<u8>(SIGNATURE_BYTES)
+const sign_prv = new StaticArray<u8>(PRIVATEKEY_BYTES)
+const sign_pub = new StaticArray<u8>(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<u8>(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<StaticArray<u8>>(INPUT_BUFFER)
+       const prv = sign_prv
+       const pub = sign_pub
+
+       memory.copy(changetype<usize>(prv), INPUT_BUFFER, PRIVATEKEY_BYTES)
+       memory.copy(changetype<usize>(pub), INPUT_BUFFER + PRIVATEKEY_BYTES, PUBLICKEY_BYTES)
+       memory.fill(INPUT_BUFFER, 0, INPUT_BUFFER_BYTES)
+
+       const m = changetype<StaticArray<u8>>(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<usize>(s), SIGNATURE_BYTES)
-       memory.fill(INPUT_BUFFER, 0, INPUT_BUFFER_BYTES)
 }
 
 const verify_s = new StaticArray<u8>(SIGNATURE_BYTES)