]> git.codecow.com Git - nano25519.git/commitdiff
Refactor signing to derive the public key from the private key input and compare...
authorChris Duncan <chris@codecow.com>
Thu, 27 Aug 2026 01:22:27 +0000 (18:22 -0700)
committerChris Duncan <chris@codecow.com>
Thu, 27 Aug 2026 01:22:27 +0000 (18:22 -0700)
src/assembly/crypto_sign.ts
test/node.mjs

index 66e96647165361a9c370c620acb5ba526aa2e1cd..50f6b121678a713f2f55f407b61f8c3b733971a7 100644 (file)
@@ -1,17 +1,18 @@
 //! 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)
@@ -20,29 +21,38 @@ const R = new StaticArray<u8>(64)
 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)
index 9459b087158614e5600d370f4882358834925c3b..0c9b9517c49611be9dcd86840cd1b1652914d170 100644 (file)
@@ -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