From 2b730a7a06cf34ff4d0741d3960c5b1ccab8d2a2 Mon Sep 17 00:00:00 2001 From: Chris Duncan Date: Sun, 6 Sep 2026 00:31:48 -0700 Subject: [PATCH] Require output buffer arg in order to receive output in bytes, else return string regardless of input type. --- src/assembly/crypto_verify.ts | 22 ++++----- src/assembly/index.ts | 10 ++--- src/index.ts | 84 +++++++++++------------------------ src/lib/derive.ts | 14 +++--- src/lib/sign.ts | 14 +++--- src/lib/verify.ts | 4 +- test/node.mjs | 15 +++++-- 7 files changed, 66 insertions(+), 97 deletions(-) diff --git a/src/assembly/crypto_verify.ts b/src/assembly/crypto_verify.ts index 9c98c97..ccd1965 100644 --- a/src/assembly/crypto_verify.ts +++ b/src/assembly/crypto_verify.ts @@ -40,12 +40,12 @@ export function crypto_verify_decodepubkey (pub: StaticArray): ge_p3 | null * * Call `crypto_verify_decodepubkey` first in order to get decode the public key * to `A` for the scalar multiplication step before checking `sB = R + hA`. - * @returns -1 if signature fails to verify, else return 0 if signature is good + * @returns 0 if signature fails to verify, else return 1 if signature is good */ export function crypto_verify_donna (sig: StaticArray, M: StaticArray, A: ge_p3, pub: StaticArray): i32 { // fail if private scalar `S` is out of range (`2²⁵³ ≤ S`) - if ((sig[63] & 224) != 0) return -1 + if ((sig[63] & 224) != 0) return 0 memory.copy(changetype(S), changetype(sig) + 32, 32) @@ -59,7 +59,7 @@ export function crypto_verify_donna (sig: StaticArray, M: StaticArray, A ge_double_scalarmult_vartime_to_p3(sb_ah, h, A, S) ge_p3_tobytes(check_r, sb_ah) - return equalbytes(sig, check_r, KEY_BYTELENGTH) - 1 + return equalbytes(sig, check_r, KEY_BYTELENGTH) } /** @@ -68,25 +68,25 @@ export function crypto_verify_donna (sig: StaticArray, M: StaticArray, A * * Unlike `crypto_verify_donna`, this function is intended for single-use * verifications and thus can compute and set `A` itself. - * @returns -1 if signature fails to verify, else return 0 if signature is good + * @returns 0 if signature fails to verify, else return 1 if signature is good */ export function crypto_verify_sodium (sig: StaticArray, M: StaticArray, mlen: i32, pub: StaticArray): i32 { // fail if public key `k` is non-canonical (`p = 2²⁵⁵-19 ≤ k`) - if (!ge_is_canonical(pub)) return -1 + if (!ge_is_canonical(pub)) return 0 // fail if public key cannot be decoded - if (ge_frombytes_negate_vartime(A, pub) != 0) return -1 + if (ge_frombytes_negate_vartime(A, pub) != 0) return 0 // fail if public key `k` is small order - if (ge_has_small_order(A) != 0) return -1 + if (ge_has_small_order(A) != 0) return 0 // fail if private scalar `S` is non-canonical (`L ≤ S`) memory.copy(changetype(S), changetype(sig) + 32, 32) - if (!sc_is_canonical(S)) return -1 + if (!sc_is_canonical(S)) return 0 - if (ge_frombytes(expected_r, sig) != 0) return -1 - if (ge_has_small_order(expected_r) != 0) return -1 + if (ge_frombytes(expected_r, sig) != 0) return 0 + if (ge_has_small_order(expected_r) != 0) return 0 // signature is nonce point R and scalar S (R || S) // data to hash is nonce point R, public key A, and message M @@ -98,7 +98,7 @@ export function crypto_verify_sodium (sig: StaticArray, M: StaticArray, 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 + return ge_has_small_order(check) // ge_p3_tobytes(check_r, sb_ah) // return equalbytes(s, check_r, 32) - 1 diff --git a/src/assembly/index.ts b/src/assembly/index.ts index 16d1fea..dfc27d1 100644 --- a/src/assembly/index.ts +++ b/src/assembly/index.ts @@ -18,7 +18,7 @@ const INPUT_SIG = new StaticArray(SIGNATURE_BYTELENGTH) const OUTPUT_DERIVE = new StaticArray(KEY_BYTELENGTH) const OUTPUT_SIGN = new StaticArray(SIGNATURE_BYTELENGTH) -const OUTPUT_VERIFY = new StaticArray(MAX_VERIFY_BLOCKS).fill(255) +const OUTPUT_VERIFY = new StaticArray(MAX_VERIFY_BLOCKS) /** Returns a pointer to the static message input buffer. */ export function ptrInputMsg (): usize { @@ -145,8 +145,8 @@ export function sign (mlen: i32): void { * @returns {boolean} True if message was signed by public key's private key */ export function verify (mlen: i32): void { - // Set all output to false so errors fail closed - OUTPUT_VERIFY.fill(255) + // Clear output buffer so errors do not retain stale prior data + OUTPUT_VERIFY.fill(0) // Clear local buffers pub.fill(0) @@ -192,8 +192,8 @@ export function verify (mlen: i32): void { * @param {i32} count Number of signatures to verify, up to 64 */ export function verify_blocks (count: i32): void { - // Set all output to false so errors fail closed - OUTPUT_VERIFY.fill(255) + // Clear output buffer so errors do not retain stale prior data + OUTPUT_VERIFY.fill(0) // Clear local buffers h.fill(0) diff --git a/src/index.ts b/src/index.ts index 8d00b9e..38cb81c 100644 --- a/src/index.ts +++ b/src/index.ts @@ -6,103 +6,69 @@ import * as nano25519 from './lib' export { constants } from './lib' /** - * Nano public key derivation using WebAssembly. - * @param {Uint8Array} prv - 32-byte private key + * Nano public key derivation using WebAssembly. If byte output is required, + * pass a target buffer as a second argument. + * @param {(Uint8Array | string)} prv - 32-byte private key * @returns 32-byte public key */ -export function derive (prv: Uint8Array): Uint8Array -/** - * Nano public key derivation using WebAssembly. - * @param {string} prv - 64-character hexadecimal private key - * @returns 64-character hexadecimal public key - */ -export function derive (prv: string): string +export function derive (prv: Uint8Array | string): string /** * Nano public key derivation using WebAssembly. Instead of allocating an output * buffer internally for the return value, public key bytes are written to the * the user-supplied output buffer. - * @param {Uint8Array} prv - 32-byte private key + * @param {(Uint8Array | string)} prv - 32-byte private key * @param {Uint8Array} out - buffer to receive 32-byte public key */ -export function derive (prv: Uint8Array, out: Uint8Array): void -export function derive (prv: string | Uint8Array, out?: Uint8Array): string | Uint8Array | void { +export function derive (prv: Uint8Array | string, out: Uint8Array): void +export function derive (prv: Uint8Array | string, out?: Uint8Array): string | void { return nano25519.derive(prv, out) } /** * Signing using WebAssembly. To sign Nano blocks, the message should be a * 32-byte block hash. - * @param {Uint8Array} msg - Variable-byte-length message up to 64 KiB - * @param {Uint8Array} prv - 32-byte private key - * @param {Uint8Array} pub - 32-byte public key + * @param {(Uint8Array | string)} msg - Variable-byte-length message up to 64 KiB + * @param {(Uint8Array | string)} prv - 32-byte private key + * @param {(Uint8Array | string)} pub - 32-byte public key * @returns 64-byte detached signature */ -export function sign (msg: Uint8Array, prv: Uint8Array, pub: Uint8Array): Uint8Array -/** - * Signing using WebAssembly. To sign Nano blocks, the message should be a - * 64-character hexadecimal block hash. - * @param {string} msg - Variable-length hexadecimal message up to 64 KiB - * @param {string} prv - 64-character hexadecimal private key - * @param {string} pub - 64-character hexadecimal public key - * @returns 128-character hexadecimal detached signature - */ -export function sign (msg: string, prv: string, pub: string): string +export function sign (msg: Uint8Array | string, prv: Uint8Array | string, pub: Uint8Array | string): string /** * Signing using WebAssembly. To sign Nano blocks, the message should be a * 32-byte block hash. Instead of allocating an output buffer internally for the * return value, signature bytes are written to the the user-supplied output * buffer. - * @param {Uint8Array} msg - Variable-byte-length message up to 64 KiB - * @param {Uint8Array} prv - 32-byte private key - * @param {Uint8Array} pub - 32-byte public key - * @param {Uint8Array} out - buffer to receive 64-byte detached signature + * @param {(Uint8Array | string)} msg - Variable-byte-length message up to 64 KiB + * @param {(Uint8Array | string)} prv - 32-byte private key + * @param {(Uint8Array | string)} pub - 32-byte public key + * @param {(Uint8Array | string)} out - buffer to receive 64-byte detached signature */ -export function sign (msg: Uint8Array, prv: Uint8Array, pub: Uint8Array, out: Uint8Array): void -export function sign (msg: string | Uint8Array, prv: string | Uint8Array, pub: string | Uint8Array, out?: Uint8Array): string | Uint8Array | void { +export function sign (msg: Uint8Array | string, prv: Uint8Array | string, pub: Uint8Array | string, out: Uint8Array): void +export function sign (msg: Uint8Array | string, prv: Uint8Array | string, pub: Uint8Array | string, out?: Uint8Array): string | void { return nano25519.sign(msg, prv, pub, out) } /** * Signature verification using WebAssembly. To verify Nano block signatures, * use `verify_blocks()` instead. - * @param {Uint8Array} sig - 64-byte detached signature - * @param {Uint8Array} msg - Variable-byte-length message up to 64 KiB - * @param {Uint8Array} pub - 32-byte public key - * @returns true if signature matches block hash and public key, else false - */ -export function verify (sig: Uint8Array, msg: Uint8Array, pub: Uint8Array): boolean -/** - * Signature verification using WebAssembly. To verify Nano block signatures, - * the message should be a 64-character block hash. - * @param {string} sig - 128-character hexadecimal detached signature - * @param {string} msg - Variable-length message up to 64 KiB - * @param {string} pub - 64-character hexadecimal public key + * @param {(Uint8Array | string)} sig - 64-byte detached signature + * @param {(Uint8Array | string)} msg - Variable-byte-length message up to 64 KiB + * @param {(Uint8Array | string)} pub - 32-byte public key * @returns true if signature matches block hash and public key, else false */ -export function verify (sig: string, msg: string, pub: string): boolean -export function verify (sig: string | Uint8Array, msg: string | Uint8Array, pub: string | Uint8Array): boolean { +export function verify (sig: Uint8Array | string, msg: Uint8Array | string, pub: Uint8Array | string): boolean { return nano25519.verify(sig, msg, pub) } /** * Nano block signature bulk verification using WebAssembly. - * @param {Uint8Array} publicKey - 32-byte public key - * @param {object[]} blocks - Array of up to 64 block signature/hash pairs - * @param {Uint8Array} blocks.signature - 64-byte block signature - * @param {Uint8Array} blocks.hash - 32-byte block hash - * @returns Array of results with, for each case, true if signature matches block hash and public key, else false - */ -export function verify_blocks (publicKey: Uint8Array, blocks: { hash: Uint8Array, signature: Uint8Array }[]): boolean[] -/** - * Nano block signature bulk verification using WebAssembly. - * @param {string} publicKey - 64-character hexadecimal public key + * @param {(Uint8Array | string)} publicKey - 32-byte public key * @param {object[]} blocks - Array of up to 64 block signature/hash pairs - * @param {string} blocks.signature - 128-character hexadecimal block signature - * @param {string} blocks.hash - 64-character hexadecimal block hash + * @param {(Uint8Array | string)} blocks.signature - 64-byte block signature + * @param {(Uint8Array | string)} blocks.hash - 32-byte block hash * @returns Array of results with, for each case, true if signature matches block hash and public key, else false */ -export function verify_blocks (publicKey: string, blocks: { signature: string, hash: string }[]): boolean[] -export function verify_blocks (publicKey: string | Uint8Array, blocks: { signature: string | Uint8Array, hash: string | Uint8Array }[]): boolean[] { +export function verify_blocks (publicKey: Uint8Array | string, blocks: { signature: Uint8Array | string, hash: Uint8Array | string }[]): boolean[] { return nano25519.verify_blocks(publicKey, blocks) } diff --git a/src/lib/derive.ts b/src/lib/derive.ts index 1a1288f..115a0af 100644 --- a/src/lib/derive.ts +++ b/src/lib/derive.ts @@ -5,7 +5,7 @@ import { Mutex, Nano25519TypeError, Pointers, clearMemory, constants, exports, i const { KEY_BYTELENGTH, } = constants -export function derive (prv: unknown, out?: unknown): string | Uint8Array | void { +export function derive (prv: unknown, out?: unknown): string | void { if (typeof out !== 'undefined' && !(isBytes(out) && out.byteLength === KEY_BYTELENGTH)) { throw new TypeError('Derive output buffer must be 32-byte Uint8Array') } @@ -24,21 +24,19 @@ export function derive (prv: unknown, out?: unknown): string | Uint8Array | void { +export function sign (msg: unknown, prv: unknown, pub: unknown, out?: unknown): string | void { if (typeof out !== 'undefined' && !(isBytes(out) && out.byteLength === SIGNATURE_BYTELENGTH)) { throw new TypeError(`Sign output buffer must be ${SIGNATURE_BYTELENGTH}-byte Uint8Array`) } @@ -32,22 +32,20 @@ export function sign (msg: unknown, prv: unknown, pub: unknown, out?: unknown): for (let i = 0; i < SIGNATURE_BYTELENGTH; i++) { signature[i] = buffer[Pointers.OUTPUT_SIGN + i] } - if (typeof prv === 'string') { + if (out != null) { + out.set(signature) + return void 0 + } else { let hex = '' for (const byte of signature) { hex += byte.toString(16).padStart(2, '0') } return hex - } else if (isBytes(prv) && out != null) { - out.set(signature) - signature.fill(0) - return - } else { - return signature } } finally { privateKey.fill(0) publicKey.fill(0) + signature.fill(0) clearMemory() } } diff --git a/src/lib/verify.ts b/src/lib/verify.ts index 34dfd03..9d4c088 100644 --- a/src/lib/verify.ts +++ b/src/lib/verify.ts @@ -26,7 +26,7 @@ export function verify (sig: unknown, msg: unknown, pub: unknown): boolean { throw new Nano25519TypeError('WASM memory buffer detached') } const verified = buffer[Pointers.OUTPUT_VERIFY] - return verified === 0 + return verified === 1 } finally { clearMemory() } @@ -78,7 +78,7 @@ export function verify_blocks (pub: unknown, data: unknown): boolean[] { for (let i = 0; i < count; i++) { verified[i] = buffer[Pointers.OUTPUT_VERIFY + i] } - return [...verified].map(v => v === 0) + return [...verified].map(v => v === 1) } finally { clearMemory() } diff --git a/test/node.mjs b/test/node.mjs index dfa215c..5cf43de 100644 --- a/test/node.mjs +++ b/test/node.mjs @@ -268,13 +268,20 @@ failures += +!test // Check byte inputs result = derive(NANO_ORG_VECTOR.privateKeyBytes) -test = [...result].map(b => b.toString(16).padStart(2, '0')).join('').toLowerCase() === NANO_ORG_VECTOR.publicKey.toLowerCase() +test = result.toLowerCase() === NANO_ORG_VECTOR.publicKey.toLowerCase() check(`derive from private key bytes ${NANO_ORG_VECTOR.privateKey}`, test) passes += +test failures += +!test +outbuf = new Uint8Array(32) +derive(NANO_ORG_VECTOR.privateKeyBytes, outbuf) +test = [...outbuf].map(b => b.toString(16).padStart(2, '0')).join('').toLowerCase() === NANO_ORG_VECTOR.publicKey.toLowerCase() +check(`derive from private key bytes to output buffer`, test) +passes += +test +failures += +!test + result = sign(NANO_ORG_VECTOR.blockHashBytes, NANO_ORG_VECTOR.privateKeyBytes, NANO_ORG_VECTOR.publicKeyBytes) -test = [...result].map(b => b.toString(16).padStart(2, '0')).join('').toLowerCase() === NANO_ORG_VECTOR.signature.toLowerCase() +test = result.toLowerCase() === NANO_ORG_VECTOR.signature.toLowerCase() check(`sign message bytes ${NANO_ORG_VECTOR.blockHash}`, test) passes += +test failures += +!test @@ -371,13 +378,13 @@ for (const { privateKey, publicKey, message, signature } of PYTHON_ED25519_BLAKE const signatureBytes = new Uint8Array(signature.match(/.{2}/g)?.slice(0, 64).map(b => parseInt(b, 16)) ?? []) result = derive(privateKeyBytes) - test = [...result].map(b => b.toString(16).padStart(2, '0')).join('').toLowerCase() === publicKey + test = result.toLowerCase() === publicKey check(`derive from private key bytes ${privateKey}`, test) passes += +test failures += +!test result = sign(messageBytes, privateKeyBytes, publicKeyBytes) - test = [...result].map(b => b.toString(16).padStart(2, '0')).join('').toLowerCase() === signature.slice(0, 128) + test = result.toLowerCase() === signature.slice(0, 128) check(`sign message bytes length ${messageBytes.byteLength}`, test) passes += +test failures += +!test -- 2.52.0