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?
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.