From 83e117ca3126af0e7a9ee394013019dc14f9a2aa Mon Sep 17 00:00:00 2001 From: Chris Duncan Date: Wed, 19 Aug 2026 16:24:38 -0700 Subject: [PATCH] Extract crypto functions from API definition file. --- src/assembly/crypto_derive.ts | 40 ++++++++ src/assembly/crypto_sign.ts | 87 +++++++++++++++++ src/assembly/crypto_verify.ts | 55 +++++++++++ src/assembly/index.ts | 173 +--------------------------------- src/assembly/utils.ts | 15 +++ 5 files changed, 200 insertions(+), 170 deletions(-) create mode 100644 src/assembly/crypto_derive.ts create mode 100644 src/assembly/crypto_sign.ts create mode 100644 src/assembly/crypto_verify.ts diff --git a/src/assembly/crypto_derive.ts b/src/assembly/crypto_derive.ts new file mode 100644 index 0000000..f1bb450 --- /dev/null +++ b/src/assembly/crypto_derive.ts @@ -0,0 +1,40 @@ +//! SPDX-FileCopyrightText: 2026 Chris Duncan +//! SPDX-License-Identifier: GPL-3.0-or-later + +import { Blake2b } from './blake2b' +import { ge_scalarmult_base_tobytes } from './ge' +import { clamp } from './utils' + +const PRIVATEKEY_BYTES: i32 = 32 +const PUBLICKEY_BYTES: i32 = 32 +const SECRETKEY_BYTES: i32 = PRIVATEKEY_BYTES + PUBLICKEY_BYTES + +// crypto_hash function +const blake2b = new Blake2b() + +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. 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 + */ +export 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) +} diff --git a/src/assembly/crypto_sign.ts b/src/assembly/crypto_sign.ts new file mode 100644 index 0000000..df81ade --- /dev/null +++ b/src/assembly/crypto_sign.ts @@ -0,0 +1,87 @@ +//! SPDX-FileCopyrightText: 2026 Chris Duncan +//! SPDX-License-Identifier: GPL-3.0-or-later + +import { Blake2b } from './blake2b' +import { ge_scalarmult_base_tobytes } from './ge' +import { sc_muladd, sc_reduce } from './sc' +import { clamp } from './utils' + +const PRIVATEKEY_BYTES: i32 = 32 + +// crypto_hash function +const blake2b = new Blake2b() + +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 {StaticArray} RS 64-byte output buffer for detached signature + * @param {StaticArray} M variable-length message to be signed + * @param {i32} mlen bytelength of `m` + * @param {StaticArray} key 32-byte private key from input buffer + * @param {StaticArray} A 32-byte public key from input buffer + */ +export 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) +} diff --git a/src/assembly/crypto_verify.ts b/src/assembly/crypto_verify.ts new file mode 100644 index 0000000..9dda4a6 --- /dev/null +++ b/src/assembly/crypto_verify.ts @@ -0,0 +1,55 @@ +//! SPDX-FileCopyrightText: 2026 Chris Duncan +//! SPDX-License-Identifier: GPL-3.0-or-later + +import { Blake2b } from './blake2b' +import { ge_double_scalarmult_vartime_to_p3, ge_frombytes, ge_frombytes_negate_vartime, ge_has_small_order, ge_is_canonical } from './ge' +import { ge_p3, ge_sub_p3 } from './p' +import { sc_is_canonical, sc_reduce } from './sc' + +// crypto_hash function +const blake2b = new Blake2b() + +const crypto_verify_h = new StaticArray(64) +const crypto_verify_check = new ge_p3() +const crypto_verify_expected_r = new ge_p3() +const crypto_verify_A = new ge_p3() +const crypto_verify_sb_ah = new ge_p3() +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 + */ +export 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 + const A = crypto_verify_A + const sb_ah = crypto_verify_sb_ah + const S = crypto_verify_S + + // fail if public key `k` is non-canonical (`p = 2²⁵⁵-19 <= k`) + 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, pub) != 0) return -1 + if (ge_has_small_order(A) != 0) return -1 + + if (ge_frombytes(expected_r, s) != 0) return -1 + if (ge_has_small_order(expected_r) != 0) return -1 + + // signature is nonce point R and scalar S (R || S) + // 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(pub, 32).update(M, mlen).digest(h) + sc_reduce(h) + + ge_double_scalarmult_vartime_to_p3(sb_ah, h, A, S) + ge_sub_p3(check, expected_r, sb_ah) + + return ge_has_small_order(check) - 1 +} diff --git a/src/assembly/index.ts b/src/assembly/index.ts index 87448d0..eb18a1d 100644 --- a/src/assembly/index.ts +++ b/src/assembly/index.ts @@ -1,15 +1,13 @@ //! SPDX-FileCopyrightText: 2026 Chris Duncan //! SPDX-License-Identifier: GPL-3.0-or-later -import { Blake2b } from './blake2b' -import { ge_double_scalarmult_vartime_to_p3, ge_frombytes, ge_frombytes_negate_vartime, ge_has_small_order, ge_is_canonical, ge_scalarmult_base_tobytes } from './ge' -import { ge_p3, ge_sub_p3 } from './p' -import { sc_is_canonical, sc_muladd, sc_reduce } from './sc' +import { crypto_derive } from './crypto_derive' +import { crypto_sign } from './crypto_sign' +import { crypto_verify } from './crypto_verify' const MESSAGE_BYTES: i32 = 32 const PRIVATEKEY_BYTES: i32 = 32 const PUBLICKEY_BYTES: i32 = 32 -const SECRETKEY_BYTES: i32 = PRIVATEKEY_BYTES + PUBLICKEY_BYTES const SIGNATURE_BYTES: i32 = 64 // Static I/O buffers @@ -20,171 +18,6 @@ const INPUT_BUFFER = memory.data(INPUT_BUFFER_BYTES) const OUTPUT_BUFFER = memory.data(OUTPUT_BUFFER_BYTES) const MESSAGE_BUFFER = memory.data(MESSAGE_BUFFER_BYTES) -// crypto_hash function -const blake2b = new Blake2b() - -/** - * Clears the 3 least significant bits to guarantee multiple of 8 and prevent - * small-subgroup attacks based on Curve25519's cofactor of 8. Also sets bit 254 - * and clears bit 255 to guarantee a fixed bit length for constant time - * performance. - * @param k 32-byte scalar - */ -//@ts-expect-error -@inline -function clamp (k: StaticArray): void { - k[0] &= 248 - k[31] &= 127 - k[31] |= 64 -} - -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. 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 (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_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 {StaticArray} RS 64-byte output buffer for detached signature - * @param {StaticArray} M variable-length message to be signed - * @param {i32} mlen bytelength of `m` - * @param {StaticArray} key 32-byte private key from input buffer - * @param {StaticArray} A 32-byte public key from input buffer - */ -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) -const crypto_verify_check = new ge_p3() -const crypto_verify_expected_r = new ge_p3() -const crypto_verify_A = new ge_p3() -const crypto_verify_sb_ah = new ge_p3() -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, pub: StaticArray): i32 { - const M = changetype>(m) - const h = crypto_verify_h - const check = crypto_verify_check - const expected_r = crypto_verify_expected_r - const A = crypto_verify_A - const sb_ah = crypto_verify_sb_ah - const S = crypto_verify_S - - // fail if public key `k` is non-canonical (`p = 2²⁵⁵-19 <= k`) - 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, pub) != 0) return -1 - if (ge_has_small_order(A) != 0) return -1 - - if (ge_frombytes(expected_r, s) != 0) return -1 - if (ge_has_small_order(expected_r) != 0) return -1 - - // signature is nonce point R and scalar S (R || S) - // 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(pub, 32).update(M, mlen).digest(h) - sc_reduce(h) - - ge_double_scalarmult_vartime_to_p3(sb_ah, h, A, S) - ge_sub_p3(check, expected_r, sb_ah) - - return ge_has_small_order(check) - 1 -} - /** Returns the pointer to the static output buffer (64 bytes). */ export function getOutputPointer (): usize { return OUTPUT_BUFFER diff --git a/src/assembly/utils.ts b/src/assembly/utils.ts index 13c5357..bee890e 100644 --- a/src/assembly/utils.ts +++ b/src/assembly/utils.ts @@ -1,6 +1,21 @@ //! SPDX-FileCopyrightText: 2026 Chris Duncan //! SPDX-License-Identifier: GPL-3.0-or-later +/** + * Clears the 3 least significant bits to guarantee multiple of 8 and prevent + * small-subgroup attacks based on Curve25519's cofactor of 8. Also sets bit 254 + * and clears bit 255 to guarantee a fixed bit length for constant time + * performance. + * @param k 32-byte scalar + */ +//@ts-expect-error +@inline +export function clamp (k: StaticArray): void { + k[0] &= 248 + k[31] &= 127 + k[31] |= 64 +} + //@ts-expect-error @inline export function equal (b: i8, c: i8): u8 { -- 2.52.0