paulmillr / noble-secp256k1

Fastest 4KB JS implementation of secp256k1 signatures and ECDH
https://paulmillr.com/noble
MIT License
757 stars 114 forks source link

Signatures will randomly return as invalid. #87

Closed cmdruid closed 1 year ago

cmdruid commented 1 year ago

Hello. I believe that I have found a bug with how signatures (and possibly keys) are validated.

I have devised a test where the signature validation will fail 50% on average. I have also confirmed this behavior in two other projects, which caused me to write this test case to figure out what was going on.

Here is the code I am using to reproduce the bug:

import * as Noble from '@noble/secp256k1'
import { webcrypto as crypto } from 'crypto'

let rounds = 10, passed = 0

for (let i = 0; i < rounds; i++) {

  const randomBytes = getRandBytes(32)
  const randomData  = getRandBytes(32)

  const isValidPrivateKey = Noble.utils.isValidPrivateKey(randomBytes)
  const pubKey = Noble.getPublicKey(randomBytes)

  const signature = await Noble.schnorr.sign(randomData, randomBytes)
  const isValid = await Noble.schnorr.verify(signature, randomData, pubKey)

  if (!(isValidPrivateKey && isValid)) {
    console.log('Failed validation!')
    console.log('  message:', Noble.utils.bytesToHex(randomData))
    console.log('  Valid Key:', isValidPrivateKey, Noble.utils.bytesToHex(randomBytes))
    console.log('  Valid Sig:', isValid, Noble.utils.bytesToHex(signature))
    console.log('')
  } else { passed++ }
}

console.log(`Passed: ${passed}\nFailed: ${rounds - passed}\nTotal: ${rounds}`)

function getRandBytes(size = 32) {
  return crypto.getRandomValues(new Uint8Array(size))
}

For the above code, there are no dependencies besides the built-in webcrypto module and (version 1.7.0) the @noble/secp256k1 library.

I haven't run into this issue until recently, and I suspect it may have something to do with the random data being used for signing, but honestly I'm not sure. The random data will pass the isValidPrivateKey check 100% of the time, but the signature will fail 50% of the time on average.

paulmillr commented 1 year ago

const pubKey = Noble.getPublicKey(randomBytes) => const pubKey = Noble.schnorr.getPublicKey(randomBytes)