//! SPDX-FileCopyrightText: 2026 Chris Duncan <chris@codecow.com>
//! 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<u8>(32)
const h = new StaticArray<u8>(64)
const s = new StaticArray<u8>(32)
const prefix = new StaticArray<u8>(32)
const k = new StaticArray<u8>(64)
const S = new StaticArray<u8>(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<u8>} RS 64-byte output buffer for detached signature
* @param {StaticArray<u8>} M variable-length message to be signed
* @param {i32} mlen bytelength of `m`
- * @param {StaticArray<u8>} key 32-byte private key from input buffer
- * @param {StaticArray<u8>} A 32-byte public key from input buffer
+ * @param {StaticArray<u8>} prv 32-byte private key from input buffer
+ * @param {StaticArray<u8>} pub 32-byte public key from input buffer
*/
-export function crypto_sign (RS: StaticArray<u8>, M: StaticArray<u8>, mlen: i32, key: StaticArray<u8>, A: StaticArray<u8>): void {
+export function crypto_sign (RS: StaticArray<u8>, M: StaticArray<u8>, mlen: i32, prv: StaticArray<u8>, pub: StaticArray<u8>): 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<usize>(s), changetype<usize>(h), 32)
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