javascriptpythoncryptographysecp256k1

secp256k1/schnorr libraries won't talk to each other


I'm currently generating a private key in the browser and deriving its public key using the noble-secp256k1 javascript library:

const privKey = secp.utils.randomPrivateKey()
const pubKey = Buffer.from(secp.schnorr.getPublicKey(privKey)).toString('hex')

I then send the public key to my server, which uses the secp256k1 library to verify a payload signature I pass along as well. This fails when I try to instantiate the public key:

pub_key = secp256k1.PublicKey(binascii.unhexlify(hex_pub_key), raw=True)

This works if I build a key pair using the python library (python -m secp256k1 privkey -p), but if I send the key generated on the the client the server raises an error:

Exception: unknown public key size (expected 33 or 65)

The python library generates a 66-character hex-encoded public key. The client generates a 64-character hex-encoded public key using the secp.schnorr.getPublicKey method, and a 130-character hex-encoded public key using the secp.getPublicKey method. Is there a way to get my python library to accept the schnorr pubkey generated on the frontend? Is there anywhere I can read about what this semi-overlap between secp256k1 and schnorr is all about?


Solution

  • The 32 bytes public key of the NodeJS library (noble-secp256k1) has to be extended to 33 bytes with a leading 0x02 and can then be imported by the Python library (secp256k1):

    ...we pick that option for P, and thus our X-only public keys become equivalent to a compressed public key that is the X-only key prefixed by the byte 0x02...

    from the documentation of BIP0340, sec. Design, Implicit Y coordinates.


    Test:

    The following NodeJS code uses the NodeJS library and generates a key pair and a Schnorr signature:

    var secp = require("@noble/secp256k1");
    
    (async () => {
        const privateKey = secp.utils.randomPrivateKey();
        const publicKey = secp.schnorr.getPublicKey(privateKey) 
        const msgHash = await secp.utils.sha256("hello world");
        const signature = await secp.schnorr.sign(msgHash, privateKey);
        const isValid = await secp.schnorr.verify(signature, msgHash, publicKey);  
        console.log("Public key (hex):", Buffer.from(publicKey).toString('hex'))
        console.log("Signature (hex) :", Buffer.from(signature).toString('hex'))
        console.log("Verified        :", isValid);
    })();
    

    with the following possible output:

    Public key (hex): f9a10a9bbb93e14a35d82c514f4eb052734ba55b93f6553f12366d6e887b76ee
    Signature (hex) : b08a0e9d02da2bbb3e2220b90e591c82ebcfc337aaac36ebe2f91eec288c79b3b9513b1018126526f99697abb78c60041f0683bbce6760b8ff76cb53a4c87137
    Verified        : true
    

    The following Python code uses the Python library and verifies the Schnorr signature with the key exported by the NodeJS code, which is extended by a leading 0x02:

    import secp256k1
    import hashlib
    
    publicKeyHexFromNodeJS = 'f9a10a9bbb93e14a35d82c514f4eb052734ba55b93f6553f12366d6e887b76ee'
    signatureHexFromNodeJS = 'b08a0e9d02da2bbb3e2220b90e591c82ebcfc337aaac36ebe2f91eec288c79b3b9513b1018126526f99697abb78c60041f0683bbce6760b8ff76cb53a4c87137'
    
    digest = hashlib.sha256()
    digest.update(b'hello world')
    msgHash = digest.digest()
    
    publicKey = secp256k1.PublicKey(bytes.fromhex('02' + publicKeyHexFromNodeJS), raw=True)
    signature = bytes.fromhex(signatureHexFromNodeJS)
    isValid = publicKey.schnorr_verify(msgHash, signature, '', raw=True)
    print("Verified: " + str(isValid))
    

    With the output:

    Verified: True
    

    i.e. a successful verification.


    Note: Even if a 0x03 (instead of a 0x02) is prepended, verification is successful. Probably the library only checks if the key is compressed (0x02 or 0x03), and if so, the leading byte is simply truncated.