From 4aebb5ae59989d3b875a2219d3352bf81d2b9602 Mon Sep 17 00:00:00 2001 From: Chris Duncan Date: Wed, 26 Aug 2026 18:22:27 -0700 Subject: [PATCH] Refactor signing to derive the public key from the private key input and compare to the public key input. --- src/assembly/crypto_sign.ts | 38 +++++++++++++++++++++++-------------- test/node.mjs | 16 ++++++++++++++++ 2 files changed, 40 insertions(+), 14 deletions(-) diff --git a/src/assembly/crypto_sign.ts b/src/assembly/crypto_sign.ts index 66e9664..50f6b12 100644 --- a/src/assembly/crypto_sign.ts +++ b/src/assembly/crypto_sign.ts @@ -1,17 +1,18 @@ //! SPDX-FileCopyrightText: 2026 Chris Duncan //! SPDX-License-Identifier: GPL-3.0-or-later +import { KEY_BYTELENGTH } from '.' import { Blake2b } from './blake2b' +import { crypto_derive } from './crypto_derive' 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() // algorithm variables +const A = new StaticArray(32) const h = new StaticArray(64) const s = new StaticArray(32) const prefix = new StaticArray(32) @@ -20,29 +21,38 @@ const R = new StaticArray(64) const k = new StaticArray(64) const S = new StaticArray(64) /** - * Sign a message with a secret key. The Nano specification uses BLAKE2b as the + * Sign a message with a private 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. + * In this implementation, both the private key and the public key are required. + * This ensures the user has a full correct keypair for data integrity. It also + * offers a small performance improvement in the case of an invalid public key + * by throwing early and avoiding expensive point multiplication; RFC 8032 + * indicates the public key should be recomputed from the private key when + * signing anyway, so there is no penalty for checking key validity first. * * 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 + * @param {StaticArray} prv 32-byte private key from input buffer + * @param {StaticArray} pub 32-byte public key from input buffer */ -export function crypto_sign (RS: StaticArray, M: StaticArray, mlen: i32, key: StaticArray, A: StaticArray): void { +export function crypto_sign (RS: StaticArray, M: StaticArray, mlen: i32, prv: StaticArray, pub: StaticArray): void { + // Derive `A` from private key and throw if it does not match public key + crypto_derive(A, prv) + let c = 0 + for (let i = 0; i < KEY_BYTELENGTH; i++) { + c |= A[i] ^ pub[i] + } + if (c != 0) { + throw new Error('Invalid public key') + } // Hash private key to `h` - blake2b.init().update(key, PRIVATEKEY_BYTES).digest(h) - key.fill(0) + blake2b.init().update(prv, KEY_BYTELENGTH).digest(h) + prv.fill(0) // Split `h` into clamped secret scalar `s` and nonce prefix memory.copy(changetype(s), changetype(h), 32) diff --git a/test/node.mjs b/test/node.mjs index 9459b08..0c9b951 100644 --- a/test/node.mjs +++ b/test/node.mjs @@ -174,6 +174,22 @@ check(`sign message too long (65538 '1's)`, test) passes += +test failures += +!test +try { + result = sign(NANO_ORG_VECTOR.blockHash, NANO_ORG_VECTOR.privateKey, NANO_ORG_VECTOR.publicKey.replace('3', '1')) + result = false +} catch (err) { + if (err != null && typeof err === 'object' && 'message' in err && typeof err.message === 'string') { + console.log(err.message.substring(0, 38)) + result = err.message.includes('Nano25519WasmError') && err.message.includes('Invalid public key') + } else { + result = false + } +} +test = result === true +check(`sign with wrong public key`, test) +passes += +test +failures += +!test + // Check verification of live cemented block from small-order public key result = verify(PROBLEM_VECTOR.signature, PROBLEM_VECTOR.blockHash, PROBLEM_VECTOR.publicKey) test = result === true -- 2.52.0