paulmillr / noble-curves

Audited & minimal JS implementation of elliptic curve cryptography.
https://paulmillr.com/noble
MIT License
621 stars 56 forks source link

P256 signature verification was succeed with 2 public key. #116

Closed hea9549 closed 4 months ago

hea9549 commented 4 months ago

I tried to recover public key from P256 signature. With 2 recovery id ( 0, 1 ), signature makes 2 public key. And I tried to verify signature with public key and verification was succeed all of 2 keys.

Isn’t it correct that only one of the two should succeed? Or is there some cryptographic knowledge I don't know about? Is it correct that both public keys succeed? I attach the data I used below.

original public key

asn1 der (hex) : 3059301306072a8648ce3d020106082a8648ce3d03010703420004761c025b761d7a7c6d4bfd8d944975915e3d4879a2fead6e4417bdfe5de0ac36f5730e0e8082a3a964bd1e503236b1356f086b08dc39cc24288c6ef563917c53
---
x (hex) : 761c025b761d7a7c6d4bfd8d944975915e3d4879a2fead6e4417bdfe5de0ac36
y (hex) : f5730e0e8082a3a964bd1e503236b1356f086b08dc39cc24288c6ef563917c53

Signature

raw message (hex) : 49960de5880e8c687434170f6476605b8fe4aeb9a28632c7995cf3ba831d9763050000000047e4a6a9816d7dd36ad0772e8738f9423d147ebf0253cc91c5a0def297d0decb
sha256 hashed message ( for signature recovery ) (hex) : fdb2e37b8fdc643c1c94e1a1abd1876966e8f17a2d63dd2e4acb7be2e680751a

signature asn1 der (hex) : 3046022100cf4f49499b70e7fcc76193478fedcba4735456d9ca3b4fda3feafe2c16e35b6d022100cb30d5bbbb42eca0fb4dd8b056df2e058db0b8dc63e2f52d137cb580688f4063
- r (hex) : cf4f49499b70e7fcc76193478fedcba4735456d9ca3b4fda3feafe2c16e35b6d
- s (hex) : cb30d5bbbb42eca0fb4dd8b056df2e058db0b8dc63e2f52d137cb580688f4063

Recovered Public Key

recovered public key with recovery id 0
hex : 04260e3fa4c347cccdcc12e86e2f1a543f679648a31d0b26484da39f75b16b0be06d8f8231e1f819368c6c0dc52a9e62a3e90095478ead3ba4ee62e84e90182862
- x (hex) : 260e3fa4c347cccdcc12e86e2f1a543f679648a31d0b26484da39f75b16b0be0
- y (hex) : 6d8f8231e1f819368c6c0dc52a9e62a3e90095478ead3ba4ee62e84e90182862

recovered public key with recovery id 1
hex : 04761c025b761d7a7c6d4bfd8d944975915e3d4879a2fead6e4417bdfe5de0ac36f5730e0e8082a3a964bd1e503236b1356f086b08dc39cc24288c6ef563917c53
- x (hex) : 761c025b761d7a7c6d4bfd8d944975915e3d4879a2fead6e4417bdfe5de0ac36
- y (hex) : f5730e0e8082a3a964bd1e503236b1356f086b08dc39cc24288c6ef563917c53

and all of two keys succeed to verify above signature(3046022100cf4f4....).

In order to extract the correct public key from the signature, I obtained the public key corresponding to all recovery IDs (0 to 3, a total of 4), and verified the signature with these public keys to find a successful public key. However, both public keys successfully verified their signatures. I'm curious about this.

Below is the code where I experimented with this process.

import {secp256r1} from '@noble/curves/p256'
import sha256 from 'sha256';

async function test2() {
    let pub = 'MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEdhwCW3YdenxtS_2NlEl1kV49SHmi_q1uRBe9_l3grDb1cw4OgIKjqWS9HlAyNrE1bwhrCNw5zCQojG71Y5F8Uw'
    let hexPub = Buffer.from(pub, 'base64').toString('hex')
    console.log(hexPub)
    const mayBeX = '761c025b761d7a7c6d4bfd8d944975915e3d4879a2fead6e4417bdfe5de0ac36' // i want find this public key from signature ( assume i dont know recovery id )
    const mayBeY = 'f5730e0e8082a3a964bd1e503236b1356f086b08dc39cc24288c6ef563917c53' // i want find this public key from signature ( assume i dont know recovery id )

    const testChallengeHex = '49960de5880e8c687434170f6476605b8fe4aeb9a28632c7995cf3ba831d9763050000000047e4a6a9816d7dd36ad0772e8738f9423d147ebf0253cc91c5a0def297d0decb'
    const testHexSig = '3046022100cf4f49499b70e7fcc76193478fedcba4735456d9ca3b4fda3feafe2c16e35b6d022100cb30d5bbbb42eca0fb4dd8b056df2e058db0b8dc63e2f52d137cb580688f4063'

    const msgHash = sha256(Buffer.from(testChallengeHex, 'hex'));
    let sigObj = secp256r1.Signature.fromDER(testHexSig)
    console.log('signature')
    console.log(sigObj.r.toString(16))
    console.log(sigObj.s.toString(16))

    sigObj = sigObj.addRecoveryBit(0)
    console.log(`msgHash`)
    console.log(msgHash)
    console.log('----------------')

    const recovered = sigObj.recoverPublicKey(msgHash);
    console.log(`recovered`)
    console.log(recovered.toHex(false))
    console.log(recovered.toAffine().x.toString(16))
    console.log(recovered.toAffine().y.toString(16))
    console.log('----------------')
    const verified = secp256r1.verify(sigObj, msgHash, recovered.toHex(false))
    console.log(verified)

}

test2().catch(console.log)

Thank you for your interest in my problem.

paulmillr commented 4 months ago

The recovery bit is important for proper recovery. That’s whole point of it. You need to store it somewhere after signature is done if you want to restore it. That’s what Ethereum does.

hea9549 commented 4 months ago

@paulmillr then, you mean that it is theoretically correct to verify a single signature with two public keys?

paulmillr commented 4 months ago

Correct, it's called "Exclusive Ownership", and ECDSA does not have this property.

verif 0 02260e3fa4c347cccdcc12e86e2f1a543f679648a31d0b26484da39f75b16b0be0 true
verif 1 03761c025b761d7a7c6d4bfd8d944975915e3d4879a2fead6e4417bdfe5de0ac36 true

EDDSA (ed25519) does not suffer from that problem, when implemented properly.