diff --git a/README.md b/README.md index 182bef1..47fefb2 100644 --- a/README.md +++ b/README.md @@ -6,11 +6,11 @@ Blockchain validation via ecrecover is also supported. # Typescript support Since version 2.0.0, we're moving entirely to Typescript. -## Breaking changes +## Version 2.0 Breaking changes * `sign()` and `multiSigSign()` return an instance of `SignatureOutput`. Each element in it has a buffer property * instead of `e` we return `challenge` for the Schnorr Challenge. To accces its value, use `challenge.buffer` * instead of `s` we return `signature` for the Schnorr Signature. To accces its value, use `signature.buffer` - * instead of `R` we return `finalPublicNonce` for the nonce. To accces its value, use `finalPublicNonce.buffer` + * instead of `R` we return `publicNonce` for the nonce. To accces its value, use `publicNonce.buffer` * `getCombinedPublicKey()` returns a `Key` class. To get the actual key, use `key.buffer` * a lot of method become static as they don't keep any state: * `verify` @@ -19,6 +19,14 @@ Since version 2.0.0, we're moving entirely to Typescript. * `getCombinedPublicKey` * `getCombinedAddress` +## Version 3.0 Breaking changes +* `finalPublicNonce`, `FinalPublicNonce` is replaced everywhere with `publicNonce`, `PublicNonce`. The old name just didn't make sense. +* `sign()` is the former `signHash()`. A sign function that accepts a plain-text message as an argument no longer exists. +* `multiSigSign()` is the former `multiSigSignHash()`. A sign function that accepts a plain-text message as an argument no longer exists. +* `verify()` is the former `verifyHash()`. A verification function that accepts a plain-text message as an argument no longer exists. + +In version 2, we had plenty of ways to sign a message. This broad a lot of confusion as to what function was the correct one to use in various situations. This lead us to believe that making things simpler and forcing a hash to be passed to the methods is the way forward. + ## Requirements: * Node: >=16.0.0, <20.0.0 @@ -32,7 +40,7 @@ cd schnorrkel.js npm i ``` -### Testing +## Testing ``` npm run test ``` @@ -44,18 +52,20 @@ We refer to Single Signatures as ones that have a single signer. Sign: ```js -import Schnorrkel from 'schnorrkel' +import Schnorrkel from '@borislav.itskov/schnorrkel.js' -const privateKey = randomBytes(32) // Buffer +const privateKey = new Key(Buffer.from(ethers.utils.randomBytes(32))) const msg = 'test message' -const {signature, finalPublicNonce} = Schnorrkel.sign(privateKey, msg) +const hash = ethers.utils.hashMessage(msg) +const {signature, publicNonce, challenge} = Schnorrkel.sign(privateKey, hash) ``` Offchain verification: +We take the `signature`, `hash` and `publicNonce` from the example above and do: ```js -const publicKey: Buffer = ... (derived from the privateKey) -// signature and finalPublicNonce come from s -const result = Schnorrkel.verify(signature, msg, finalPublicNonce, publicKey) +const publicKey = Buffer.from(secp256k1.publicKeyCreate(privateKey.buffer)) +// signature and publicNonce come from Schnorrkel.sign +const result = Schnorrkel.verify(signature, hash, publicNonce, publicKey) ``` Onchain verification: @@ -91,14 +101,18 @@ Afterwards, here is part of the code: import { ethers } from 'ethers' import secp256k1 from 'secp256k1' -const address = 'input schnorr generated address here' +const privateKey = new Key(Buffer.from(ethers.utils.randomBytes(32))) +const publicKey = secp256k1.publicKeyCreate(ethers.utils.arrayify(privateKey)) +const px = publicKey.slice(1, 33) +const pxGeneratedAddress = ethers.utils.hexlify(px) +const schnorrAddr = '0x' + pxGeneratedAddress.slice(pxGeneratedAddress.length - 40, pxGeneratedAddress.length) const factory = new ethers.ContractFactory(SchnorrAccountAbstraction.abi, SchnorrAccountAbstraction.bytecode, wallet) -const contract: any = await factory.deploy([address]) +const contract: any = await factory.deploy([schnorrAddr]) -const privateKey: Buffer = '...' const pkBuffer = new Key(Buffer.from(ethers.utils.arrayify(privateKey))) const msg = 'just a test message'; -const sig = schnorrkel.sign(msg, privateKey); +const msgHash = ethers.utils.hashMessage(msg) +const sig = Schnorrkel.sign(pkBuffer, msgHash) // wrap the result const publicKey = secp256k1.publicKeyCreate(ethers.utils.arrayify(privateKey)) @@ -111,7 +125,6 @@ const sigData = abiCoder.encode([ "bytes32", "bytes32", "bytes32", "uint8" ], [ sig.signature.buffer, parity ]); -const msgHash = ethers.utils.solidityKeccak256(['string'], [msg]); const result = await contract.isValidSignature(msgHash, sigData); ``` @@ -144,7 +157,7 @@ const publicKey1: Buffer = '...' const publicKey2: Buffer = '...' const publicKeys = [publicKey1, publicKey2]; const combinedPublicKey = schnorrkel.getCombinedPublicKey(publicKeys) -const {signature: sigOne, challenge: e, finalPublicNonce} = signerOne.multiSignMessage(msg, publicKeys, publicNonces) +const {signature: sigOne, challenge: e, publicNonce} = signerOne.multiSignMessage(msg, publicKeys, publicNonces) const {signature: sigTwo} = signerTwo.multiSignMessage(msg, publicKeys, publicNonces) const sSummed = Schnorrkel.sumSigs([sigOne, sigTwo]) ``` @@ -168,7 +181,7 @@ const result = await contract.isValidSignature(msgHash, sigData); #### verify offchain ```js -const result = schnorrkel.verify(sSummed, msg, finalPublicNonce, combinedPublicKey); +const result = schnorrkel.verify(sSummed, msg, publicNonce, combinedPublicKey); ``` You can find reference to this in `tests/schnorrkel/onchainMultiSign.test.ts` in this repository. diff --git a/package-lock.json b/package-lock.json index 680f613..bd11fdd 100644 --- a/package-lock.json +++ b/package-lock.json @@ -16,10 +16,9 @@ }, "devDependencies": { "@types/ecurve": "^1.0.0", - "@types/elliptic": "^6.4.14", - "@types/secp256k1": "^4.0.3", "@vitest/coverage-c8": "^0.29.2", "hardhat": "^2.14.0", + "secp256k1": "^5.0.0", "solc": "0.8.19", "typescript": "^4.9.5", "vitest": "^0.29.2" @@ -2135,15 +2134,6 @@ "@types/node": "*" } }, - "node_modules/@types/elliptic": { - "version": "6.4.14", - "resolved": "https://registry.npmjs.org/@types/elliptic/-/elliptic-6.4.14.tgz", - "integrity": "sha512-z4OBcDAU0GVwDTuwJzQCiL6188QvZMkvoERgcVjq0/mPM8jCfdwZ3x5zQEVoL9WCAru3aG5wl3Z5Ww5wBWn7ZQ==", - "dev": true, - "dependencies": { - "@types/bn.js": "*" - } - }, "node_modules/@types/istanbul-lib-coverage": { "version": "2.0.4", "resolved": "https://registry.npmjs.org/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.4.tgz", @@ -4692,12 +4682,14 @@ "node_modules/node-addon-api": { "version": "5.1.0", "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-5.1.0.tgz", - "integrity": "sha512-eh0GgfEkpnoWDq+VY8OyvYhFEzBk6jIYbRKdIlyTiAXIVJ8PyBaKb0rp7oDtoddbdoHWhq8wwr+XZ81F1rpNdA==" + "integrity": "sha512-eh0GgfEkpnoWDq+VY8OyvYhFEzBk6jIYbRKdIlyTiAXIVJ8PyBaKb0rp7oDtoddbdoHWhq8wwr+XZ81F1rpNdA==", + "dev": true }, "node_modules/node-gyp-build": { "version": "4.6.0", "resolved": "https://registry.npmjs.org/node-gyp-build/-/node-gyp-build-4.6.0.tgz", "integrity": "sha512-NTZVKn9IylLwUzaKjkas1e4u2DLNcV4rdYagA4PWdPwW87Bi7z+BznyKSRwS/761tV/lzCGXplWsiaMjLqP2zQ==", + "dev": true, "bin": { "node-gyp-build": "bin.js", "node-gyp-build-optional": "optional.js", @@ -5178,6 +5170,7 @@ "version": "5.0.0", "resolved": "https://registry.npmjs.org/secp256k1/-/secp256k1-5.0.0.tgz", "integrity": "sha512-TKWX8xvoGHrxVdqbYeZM9w+izTF4b9z3NhSaDkdn81btvuh+ivbIMGT/zQvDtTFWhRlThpoz6LEYTr7n8A5GcA==", + "dev": true, "hasInstallScript": true, "dependencies": { "elliptic": "^6.5.4", @@ -7519,15 +7512,6 @@ "@types/node": "*" } }, - "@types/elliptic": { - "version": "6.4.14", - "resolved": "https://registry.npmjs.org/@types/elliptic/-/elliptic-6.4.14.tgz", - "integrity": "sha512-z4OBcDAU0GVwDTuwJzQCiL6188QvZMkvoERgcVjq0/mPM8jCfdwZ3x5zQEVoL9WCAru3aG5wl3Z5Ww5wBWn7ZQ==", - "dev": true, - "requires": { - "@types/bn.js": "*" - } - }, "@types/istanbul-lib-coverage": { "version": "2.0.4", "resolved": "https://registry.npmjs.org/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.4.tgz", @@ -9483,12 +9467,14 @@ "node-addon-api": { "version": "5.1.0", "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-5.1.0.tgz", - "integrity": "sha512-eh0GgfEkpnoWDq+VY8OyvYhFEzBk6jIYbRKdIlyTiAXIVJ8PyBaKb0rp7oDtoddbdoHWhq8wwr+XZ81F1rpNdA==" + "integrity": "sha512-eh0GgfEkpnoWDq+VY8OyvYhFEzBk6jIYbRKdIlyTiAXIVJ8PyBaKb0rp7oDtoddbdoHWhq8wwr+XZ81F1rpNdA==", + "dev": true }, "node-gyp-build": { "version": "4.6.0", "resolved": "https://registry.npmjs.org/node-gyp-build/-/node-gyp-build-4.6.0.tgz", - "integrity": "sha512-NTZVKn9IylLwUzaKjkas1e4u2DLNcV4rdYagA4PWdPwW87Bi7z+BznyKSRwS/761tV/lzCGXplWsiaMjLqP2zQ==" + "integrity": "sha512-NTZVKn9IylLwUzaKjkas1e4u2DLNcV4rdYagA4PWdPwW87Bi7z+BznyKSRwS/761tV/lzCGXplWsiaMjLqP2zQ==", + "dev": true }, "normalize-path": { "version": "3.0.0", @@ -9813,6 +9799,7 @@ "version": "5.0.0", "resolved": "https://registry.npmjs.org/secp256k1/-/secp256k1-5.0.0.tgz", "integrity": "sha512-TKWX8xvoGHrxVdqbYeZM9w+izTF4b9z3NhSaDkdn81btvuh+ivbIMGT/zQvDtTFWhRlThpoz6LEYTr7n8A5GcA==", + "dev": true, "requires": { "elliptic": "^6.5.4", "node-addon-api": "^5.0.0", diff --git a/package.json b/package.json index b17f68e..e46577c 100644 --- a/package.json +++ b/package.json @@ -30,7 +30,6 @@ "devDependencies": { "solc": "0.8.19", "@types/ecurve": "^1.0.0", - "@types/elliptic": "^6.4.14", "@types/secp256k1": "^4.0.3", "@vitest/coverage-c8": "^0.29.2", "typescript": "^4.9.5", diff --git a/src/core/index.ts b/src/core/index.ts index f32b807..61e042f 100644 --- a/src/core/index.ts +++ b/src/core/index.ts @@ -1,20 +1,19 @@ import { ethers } from 'ethers' import secp256k1 from 'secp256k1' -import ecurve from 'ecurve' -import elliptic from 'elliptic' +import ecurve, { Point } from 'ecurve' import bigi from 'bigi' -import { BN } from 'bn.js' - import { InternalNoncePairs, InternalNonces, InternalPublicNonces, InternalSignature } from './types' import { KeyPair } from '../types' const curve = ecurve.getCurveByName('secp256k1') -const n = curve?.n -const EC = elliptic.ec -const ec = new EC('secp256k1') -const generatorPoint = ec.g -const _generateNonce = (): InternalNoncePairs => { +/** + * Generate two random nonces in preparation for a multisignature. + * We return along with them their public representations + * + * @returns InternalNoncePairs + */ +const generateNonce = (): InternalNoncePairs => { const k = Buffer.from(ethers.utils.randomBytes(32)) const kTwo = Buffer.from(ethers.utils.randomBytes(32)) const kPublic = Buffer.from(secp256k1.publicKeyCreate(k)) @@ -28,7 +27,18 @@ const _generateNonce = (): InternalNoncePairs => { } } -const _bCoefficient = (combinedPublicKey: Buffer, msgHash: string, publicNonces: InternalPublicNonces[]): Buffer => { +/** + * Compute the b coefficient needed for multisignature signing. + * The b coefficient is needed to prevent the DL query attack + * on the public nonces, in hand allowing us to skip the nonce + * commitment round + * + * @param combinedPublicKey - the sum of the keys of the participants + * @param msgHash - the hash that's going to be signed + * @param publicNonces - the exchanged public nonces + * @returns Buffer + */ +const bCoefficient = (combinedPublicKey: Buffer, msgHash: string, publicNonces: InternalPublicNonces[]): Buffer => { type KeyOf = keyof InternalPublicNonces const arrayColumn = (arr: Array, n: KeyOf) => arr.map(x => x[n]) const kPublicNonces = secp256k1.publicKeyCombine(arrayColumn(publicNonces, 'kPublic')) @@ -43,19 +53,28 @@ const _bCoefficient = (combinedPublicKey: Buffer, msgHash: string, publicNonces: const areBuffersSame = (buf1: Buffer, buf2: Buffer): boolean => { if (buf1.byteLength != buf2.byteLength) return false - var dv1 = Buffer.from(buf1) - var dv2 = Buffer.from(buf2) - for (var i = 0; i != buf1.byteLength; i++) { + const dv1 = Buffer.from(buf1) + const dv2 = Buffer.from(buf2) + for (let i = 0; i != buf1.byteLength; i++) { if (dv1[i] != dv2[i]) return false } return true } +/** + * Compute the schnorr challenge. + * The formula is: s = k + e*d. We're computing `e` here + * + * @param R + * @param msgHash + * @param publicKey + * @returns Buffer hash(concat(public_nonce_addr, parity, x_coord, message)) + */ const challenge = (R: Buffer, msgHash: string, publicKey: Buffer): Buffer => { // convert R to address - var R_uncomp = secp256k1.publicKeyConvert(R, false) - var R_addr = ethers.utils.arrayify(ethers.utils.keccak256(R_uncomp.slice(1, 65))).slice(12, 32) + const R_uncomp = secp256k1.publicKeyConvert(R, false) + const R_addr = ethers.utils.arrayify(ethers.utils.keccak256(R_uncomp.slice(1, 65))).slice(12, 32) // e = keccak256(address(R) || compressed publicKey || msgHash) return Buffer.from(ethers.utils.arrayify( @@ -67,38 +86,100 @@ const challenge = (R: Buffer, msgHash: string, publicKey: Buffer): Buffer => { } /** - * Sign the given hash by the private key + * A helper function that creates a key pair * - * @param Buffer privateKey - * @param string hash - * @returns InternalSignature + * @returns KeyPair */ -const internalSign = (privateKey: Buffer, hash: string): InternalSignature => { - const localPk = Buffer.from(privateKey) - const publicKey = Buffer.from(secp256k1.publicKeyCreate(localPk)) +export const generateRandomKeys = (): KeyPair => { + let privKeyBytes: Buffer + do { + privKeyBytes = Buffer.from(ethers.utils.randomBytes(32)) + } while (!secp256k1.privateKeyVerify(privKeyBytes)) - // R = G * k - var k = ethers.utils.randomBytes(32) - var R = Buffer.from(secp256k1.publicKeyCreate(k)) + const pubKey = Buffer.from(secp256k1.publicKeyCreate(privKeyBytes)) - // e = h(address(R) || compressed pubkey || m) - var e = challenge(R, hash, publicKey) + const data = { + publicKey: pubKey, + privateKey: privKeyBytes, + } - // xe = x * e - var xe = secp256k1.privateKeyTweakMul(localPk, e) + return new KeyPair(data) +} - // s = k + xe mod(n) - var s = Buffer.from(secp256k1.privateKeyTweakAdd(k, xe)) - s = bigi.fromBuffer(s).mod(n).toBuffer(32) +/** + * Generate a hash of all the public keys that are participating + * in the signing process. We need this to craft the `a` coefficient, + * which helps us prevent key cancelation attacks. + * + * @param publicKeys + * @returns string + */ +export const _generateL = (publicKeys: Array): string => { + return ethers.utils.keccak256(_concatTypedArrays(publicKeys.sort(Buffer.compare))) +} + +export const _concatTypedArrays = (publicKeys: Buffer[]): Buffer => { + const c: Buffer = Buffer.alloc(publicKeys.reduce((partialSum, publicKey) => partialSum + publicKey.length, 0)) + publicKeys.map((publicKey, index) => c.set(publicKey, (index * publicKey.length))) + return Buffer.from(c.buffer) +} + +/** + * Generate `a` coefficient to prevent key cancelation attacks. + * Hash commitment to all the public keys to prevent your key + * not participating in the multisignature. + * + * @param publicKey - the signer's public key + * @param L - review _generateL + * @returns Buffer hash(concat(L, own_public_key)) + */ +export const _aCoefficient = (publicKey: Buffer, L: string): Buffer => { + return Buffer.from(ethers.utils.arrayify(ethers.utils.solidityKeccak256( + ['bytes', 'bytes'], + [L, publicKey] + ))) +} + +/** + * Hash the privateKey so it is not in plain text in arrays. + * A separate method for easy reuse + * + * @param privateKey + * @returns string + */ +export const _hashPrivateKey = (privateKey: Buffer): string => { + return ethers.utils.keccak256(privateKey) +} + +/** + * Generate the nonces for the next signature. + * Use the hash of the private key for a unique identifier + * + * @param privateKey + * @returns + */ +export const _generateNonces = (privateKey: Buffer): { + privateNonceData: Pick, + publicNonceData: InternalPublicNonces, + hash: string, +} => { + const hash = _hashPrivateKey(privateKey) + const nonce = generateNonce() return { - finalPublicNonce: R, - challenge: e, - signature: s + hash, + privateNonceData: { + k: nonce.k, + kTwo: nonce.kTwo, + }, + publicNonceData: { + kPublic: nonce.kPublic, + kTwoPublic: nonce.kTwoPublic, + } } } -const internalMultiSigSign = (nonces: InternalNonces, combinedPublicKey: Buffer, privateKey: Buffer, hash: string, publicKeys: Buffer[], publicNonces: InternalPublicNonces[]): InternalSignature => { +export const _multiSigSign = (nonces: InternalNonces, combinedPublicKey: Buffer, privateKey: Buffer, hash: string, publicKeys: Buffer[], publicNonces: InternalPublicNonces[]): InternalSignature => { if (publicKeys.length < 2) { throw Error('At least 2 public keys should be provided') } @@ -112,7 +193,7 @@ const internalMultiSigSign = (nonces: InternalNonces, combinedPublicKey: Buffer, const publicKey = Buffer.from(secp256k1.publicKeyCreate(localPk)) const L = _generateL(publicKeys) const a = _aCoefficient(publicKey, L) - const b = _bCoefficient(combinedPublicKey, hash, publicNonces) + const b = bCoefficient(combinedPublicKey, hash, publicNonces) const effectiveNonces = publicNonces.map((batch) => { return Buffer.from(secp256k1.publicKeyCombine([batch.kPublic, secp256k1.publicKeyTweakMul(batch.kTwoPublic, b)])) @@ -143,141 +224,110 @@ const internalMultiSigSign = (nonces: InternalNonces, combinedPublicKey: Buffer, // kTwo * b const kTwoMulB = secp256k1.privateKeyTweakMul(kTwo, b) - // k + kTwoMulB + xea - const final = secp256k1.privateKeyTweakAdd(kPlusxea, kTwoMulB) - + // k + kTwoMulB + xea mod(n) + const final = Buffer.from(secp256k1.privateKeyTweakAdd(kPlusxea, kTwoMulB)) return { - // s = k + xea mod(n) - signature: bigi.fromBuffer(final).mod(n).toBuffer(32), + signature: final, challenge: e, - finalPublicNonce: R + publicNonce: R } } /** - * Verify a signature for the given hash, public nonce and public key + * Sum the passed signatures. + * mod(n) is automatically applied in the privateKeyTweakAdd function * - * @param Buffer s - * @param string hash - * @param Buffer R - * @param Buffer publicKey - * @returns boolean + * @param signatures + * @returns Buffer summed signature */ -const internalVerify = (s: Buffer, hash: string, R: Buffer, publicKey: Buffer): boolean => { - const eC = challenge(R, hash, publicKey) - const sG = generatorPoint.mul(ethers.utils.arrayify(s)) - const P = ec.keyFromPublic(publicKey).getPublic() - const bnEC = new BN(Buffer.from(eC).toString('hex'), 'hex') - const Pe = P.mul(bnEC) - const toPublicR = ec.keyFromPublic(R).getPublic() - const RplusPe = toPublicR.add(Pe) - return sG.eq(RplusPe) -} +export const _sumSigs = (signatures: Buffer[]): Buffer => { + let combined = new Uint8Array() -export const _generateL = (publicKeys: Array) => { - return ethers.utils.keccak256(_concatTypedArrays(publicKeys.sort(Buffer.compare))) -} + for (let i = 0; i < signatures.length - 1; i++) { + combined = secp256k1.privateKeyTweakAdd(signatures[i], signatures[i+1]) + } -export const _concatTypedArrays = (publicKeys: Buffer[]): Buffer => { - const c: Buffer = Buffer.alloc(publicKeys.reduce((partialSum, publicKey) => partialSum + publicKey.length, 0)) - publicKeys.map((publicKey, index) => c.set(publicKey, (index * publicKey.length))) - return Buffer.from(c.buffer) + return Buffer.from(combined) } +/** + * The verification formula is: s*G = R + H(m)*X + * s is the signature + * G is the generation point of the elliptic curve + * R is the public nonce, or the ephemeral public nonce + * H(m) is the hash of the message + * X is the public key + * + * @param s the signature + * @param hash the signed hash + * @param R the public nonce used for this signature + * @param publicKey the public key used for this signature + * @returns bool + */ +export const _verify = (s: Buffer, hash: string, R: Buffer, publicKey: Buffer): boolean => { + const eC = challenge(R, hash, publicKey) -export const _aCoefficient = (publicKey: Buffer, L: string): Buffer => { - return Buffer.from(ethers.utils.arrayify(ethers.utils.solidityKeccak256( - ['bytes', 'bytes'], - [L, publicKey] - ))) + const sG = curve.G.multiply(bigi.fromBuffer(s)) + const PasPoint = Point.decodeFrom(curve, publicKey) + const Pe = PasPoint.multiply(bigi.fromBuffer(eC)) + const RasPoint = Point.decodeFrom(curve, R) + const RplusPetest = RasPoint.add(Pe) + return sG.equals(RplusPetest) } -export const generateRandomKeys = () => { - let privKeyBytes: Buffer - do { - privKeyBytes = Buffer.from(ethers.utils.randomBytes(32)) - } while (!secp256k1.privateKeyVerify(privKeyBytes)) - - const pubKey = Buffer.from(secp256k1.publicKeyCreate(privKeyBytes)) - - const data = { - publicKey: pubKey, - privateKey: privKeyBytes, +/** + * Take the x-coordinate of the public key and transform it + * into ethereum-like address. + * This is the address returned by ecrecover on-chain schnorr verification + * + * @param combinedPublicKey + * @returns address + */ +export const _generateSchnorrAddr = (combinedPublicKey: Buffer): string => { + if (combinedPublicKey.length != 33) { + throw Error('Public key should be 33 length, 1 byte parity and 32 bytes x-coordinate') } - return new KeyPair(data) + const px = ethers.utils.hexlify(combinedPublicKey.subarray(1,33)) + return '0x' + px.slice(px.length - 40, px.length) } -export const _hashPrivateKey = (privateKey: Buffer): string => { - return ethers.utils.keccak256(privateKey) -} +export const _sign = (privateKey: Buffer, hash: string): InternalSignature => { + // if we use secp256k1 directly on the private key for operations + // different than publicKeyCreate (privateKeyTweakMul, for example), + // the private key gets modified. We do not want that and hence + // do operations with a local copy + const localPk = Buffer.from(privateKey) + const publicKey = Buffer.from(secp256k1.publicKeyCreate(localPk)) -export const _generatePublicNonces = (privateKey: Buffer): { - privateNonceData: Pick, - publicNonceData: InternalPublicNonces, - hash: string, -} => { - const hash = _hashPrivateKey(privateKey) - const nonce = _generateNonce() + // R = G * k + const k = ethers.utils.randomBytes(32) + const R = Buffer.from(secp256k1.publicKeyCreate(k)) - return { - hash, - privateNonceData: { - k: nonce.k, - kTwo: nonce.kTwo, - }, - publicNonceData: { - kPublic: nonce.kPublic, - kTwoPublic: nonce.kTwoPublic, - } - } -} + // e = h(address(R) || compressed pubkey || m) + const e = challenge(R, hash, publicKey) -export const _multiSigSign = (nonces: InternalNonces, combinedPublicKey: Buffer, privateKey: Buffer, msg: string, publicKeys: Buffer[], publicNonces: InternalPublicNonces[], hashFn: Function|null = null): InternalSignature => { - const hashMsg = hashFn ? hashFn : _hashMessage - const hash = hashMsg(msg) - return internalMultiSigSign(nonces, combinedPublicKey, privateKey, hash, publicKeys, publicNonces) -} + // xe = x * e + const xe = secp256k1.privateKeyTweakMul(localPk, e) -export const _multiSigSignHash = (nonces: InternalNonces, combinedPublicKey: Buffer, privateKey: Buffer, hash: string, publicKeys: Buffer[], publicNonces: InternalPublicNonces[]): InternalSignature => { - return internalMultiSigSign(nonces, combinedPublicKey, privateKey, hash, publicKeys, publicNonces) -} + // s = k + xe mod(n) + const s = Buffer.from(secp256k1.privateKeyTweakAdd(k, xe)) -export const _sumSigs = (signatures: Buffer[]): Buffer => { - let combined = bigi.fromBuffer(signatures[0]) - signatures.shift() - signatures.forEach(sig => { - combined = combined.add(bigi.fromBuffer(sig)) - }) - return combined.mod(n).toBuffer(32) + return { + publicNonce: R, + challenge: e, + signature: s + } } +/** + * Provide a default hash function + * It is not mandotory to use this one. + * + * @param message + * @returns string + */ export const _hashMessage = (message: string): string => { return ethers.utils.solidityKeccak256(['string'], [message]) -} - -export const _verify = (s: Buffer, msg: string, R: Buffer, publicKey: Buffer, hashFn: Function|null = null): boolean => { - const hashMsg = hashFn ? hashFn : _hashMessage - const hash = hashMsg(msg) - return internalVerify(s, hash, R, publicKey) -} - -export const _verifyHash = (s: Buffer, hash: string, R: Buffer, publicKey: Buffer): boolean => { - return internalVerify(s, hash, R, publicKey) -} - -export const _generatePk = (combinedPublicKey: Buffer): string => { - const px = ethers.utils.hexlify(combinedPublicKey.subarray(1,33)) - return '0x' + px.slice(px.length - 40, px.length) -} - -export const _sign = (privateKey: Buffer, msg: string, hashFn: Function|null = null): InternalSignature => { - const hashMsg = hashFn ? hashFn : _hashMessage - const hash = hashMsg(msg) - return internalSign(privateKey, hash) -} - -export const _signHash = (privateKey: Buffer, hash: string): InternalSignature => { - return internalSign(privateKey, hash) } \ No newline at end of file diff --git a/src/core/types.ts b/src/core/types.ts index e00a94a..75a7bd1 100644 --- a/src/core/types.ts +++ b/src/core/types.ts @@ -11,7 +11,7 @@ export interface InternalPublicNonces { } export interface InternalSignature { - finalPublicNonce: Buffer, // the final public nonce + publicNonce: Buffer, // the final public nonce challenge: Buffer, // the schnorr challenge signature: Buffer, // the signature } diff --git a/src/index.ts b/src/index.ts index 3325240..bf4d2ae 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,6 +1,6 @@ import Schnorrkel from './schnorrkel' export { default as UnsafeSchnorrkel } from './unsafe-schnorrkel' -export { Key, KeyPair, Signature, PublicNonces, Challenge, SignatureOutput, FinalPublicNonce } from './types' +export { Key, KeyPair, Signature, PublicNonces, Challenge, SignatureOutput, PublicNonce } from './types' export default Schnorrkel \ No newline at end of file diff --git a/src/schnorrkel.ts b/src/schnorrkel.ts index 873a6ab..e01750d 100644 --- a/src/schnorrkel.ts +++ b/src/schnorrkel.ts @@ -2,15 +2,24 @@ import secp256k1 from 'secp256k1' import { Key, Nonces, PublicNonces, Signature, NoncePairs } from './types' -import { _generateL, _aCoefficient, _generatePublicNonces, _multiSigSign, _hashPrivateKey, _sumSigs, _verify, _generatePk, _sign, _signHash, _verifyHash, _multiSigSignHash } from './core' +import { _generateL, _aCoefficient, _generateNonces, _multiSigSign, _hashPrivateKey, _sumSigs, _verify, _generateSchnorrAddr, _sign } from './core' import { InternalNonces, InternalPublicNonces, InternalSignature } from './core/types' -import { Challenge, FinalPublicNonce, SignatureOutput } from './types/signature' +import { Challenge, PublicNonce, SignatureOutput } from './types/signature' class Schnorrkel { protected nonces: Nonces = {} - private _setNonce(privateKey: Buffer): string { - const { publicNonceData, privateNonceData, hash } = _generatePublicNonces(privateKey) + /** + * Set the nonces for the next multisignature. + * Nonces should not be manipulated outside the library. Also, + * they should be completely random. + * + * @param privateKey - we use the private key to create + * an unique identifier hash. See _generateNonces + * @returns string identifier + */ + private setNonce(privateKey: Buffer): string { + const { publicNonceData, privateNonceData, hash } = _generateNonces(privateKey) const mappedPublicNonce: PublicNonces = { kPublic: new Key(Buffer.from(publicNonceData.kPublic)), @@ -26,10 +35,26 @@ class Schnorrkel { return hash } + /** + * Clear the nonces used in the last signature + * This is a very important step as otherwise, we go into nonce + * reuse scenario + * + * @param privateKey + */ private clearNonces(privateKey: Key): void { const x = privateKey.buffer const hash = _hashPrivateKey(x) + // this shouldn't happen, just extra safety + // clearNonces should be called after a signature has been crafted. + // If the hash is not found in the nonces by any chance after + // a signature, then the process should be stopped as we don't + // know nonces have been used for the signature + if (! this.nonces[hash]) { + throw new Error('Multisignature nonces not found') + } + delete this.nonces[hash] } @@ -56,14 +81,13 @@ class Schnorrkel { })) } - private getMultisigOutput(multiSig: InternalSignature): SignatureOutput { - return { - signature: new Signature(Buffer.from(multiSig.signature)), - finalPublicNonce: new FinalPublicNonce(Buffer.from(multiSig.finalPublicNonce)), - challenge: new Challenge(Buffer.from(multiSig.challenge)), - } - } - + /** + * Sum the public keys in a safe manner with a specific + * _aCoefficient for each publicKey + * + * @param publicKeys - the signers + * @returns Key summed public key + */ static getCombinedPublicKey(publicKeys: Array): Key { if (publicKeys.length < 2) { throw Error('At least 2 public keys should be provided') @@ -79,16 +103,50 @@ class Schnorrkel { return new Key(Buffer.from(secp256k1.publicKeyCombine(modifiedKeys))) } + /** + * The address returned by ecrecover on-chain schnorr verification + * for the given public keys + * @param publicKeys + * @returns string address + */ static getCombinedAddress(publicKeys: Array): string { - if (publicKeys.length < 2) throw Error('At least 2 public keys should be provided') + if (publicKeys.length < 2) { + throw Error('At least 2 public keys should be provided') + } const combinedPublicKey = Schnorrkel.getCombinedPublicKey(publicKeys) - const px = _generatePk(combinedPublicKey.buffer) - return px + return _generateSchnorrAddr(combinedPublicKey.buffer) } + /** + * Generate nonces for the next signature if there aren't any. + * If there are, just return them. + * This is a method you should use if you don't want to manage + * the nonces yourself + * + * @param privateKey + * @returns PublicNonces + */ + generateOrGetPublicNonces(privateKey: Key): PublicNonces { + if (this.hasNonces(privateKey)) { + return this.getPublicNonces(privateKey) + } + + return this.generatePublicNonces(privateKey) + } + + /** + * Genetate the nonces and return the public ones for a multisignature. + * This method always generates new nonces. If you want to keep + * you state, you should check with hasNonces() whether they are set. + * You need to maintain the state for the nonce exchanging phase and + * the signing phase + * + * @param privateKey + * @returns PublicNonces + */ generatePublicNonces(privateKey: Key): PublicNonces { - const hash = this._setNonce(privateKey.buffer) + const hash = this.setNonce(privateKey.buffer) const nonce = this.nonces[hash] return { @@ -97,92 +155,112 @@ class Schnorrkel { } } + /** + * Get the public nonces. + * If none are set, an error is returned + * + * @param privateKey + * @returns PublicNonces + */ getPublicNonces(privateKey: Key): PublicNonces { const hash = _hashPrivateKey(privateKey.buffer) const nonce = this.nonces[hash] + if (!nonce) { + throw new Error('Nonces not set') + } + return { kPublic: nonce.kPublic, kTwoPublic: nonce.kTwoPublic, } } + /** + * Check if there are nonces generated in the state + * + * @param privateKey + * @returns Key + */ hasNonces(privateKey: Key): boolean { const hash = _hashPrivateKey(privateKey.buffer) return hash in this.nonces } - multiSigSign(privateKey: Key, msg: string, publicKeys: Key[], publicNonces: PublicNonces[], hashFn: Function|null = null): SignatureOutput { - const combinedPublicKey = Schnorrkel.getCombinedPublicKey(publicKeys) - const mappedPublicNonce = this.getMappedPublicNonces(publicNonces) - const mappedNonces = this.getMappedNonces() - - const musigData = _multiSigSign(mappedNonces, combinedPublicKey.buffer, privateKey.buffer, msg, publicKeys.map(key => key.buffer), mappedPublicNonce, hashFn) - - // absolutely crucial to delete the nonces once a signature has been crafted with them. - // nonce reuse will lead to private key leakage! - this.clearNonces(privateKey) - - return this.getMultisigOutput(musigData) - } - - multiSigSignHash(privateKey: Key, hash: string, publicKeys: Key[], publicNonces: PublicNonces[]): SignatureOutput { + /** + * Compute a multisignature. + * The nonce exchange phase should have passed before this stage + * + * @param privateKey - the key you're signing with + * @param hash - the message of the multisignature + * @param publicKeys - the participants + * @param publicNonces - the public nonces of the participants + * @returns SignatureOutput + */ + multiSigSign(privateKey: Key, hash: string, publicKeys: Key[], publicNonces: PublicNonces[]): SignatureOutput { const combinedPublicKey = Schnorrkel.getCombinedPublicKey(publicKeys) const mappedPublicNonce = this.getMappedPublicNonces(publicNonces) const mappedNonces = this.getMappedNonces() - const musigData = _multiSigSignHash(mappedNonces, combinedPublicKey.buffer, privateKey.buffer, hash, publicKeys.map(key => key.buffer), mappedPublicNonce) + const musigData = _multiSigSign(mappedNonces, combinedPublicKey.buffer, privateKey.buffer, hash, publicKeys.map(key => key.buffer), mappedPublicNonce) - // absolutely crucial to delete the nonces once a signature has been crafted with them. + // absolutely crucial to delete the nonces once a signature has been crafted. // nonce reuse will lead to private key leakage! this.clearNonces(privateKey) - return this.getMultisigOutput(musigData) - } - - static sign(privateKey: Key, msg: string, hashFn: Function|null = null): SignatureOutput { - const output = _sign(privateKey.buffer, msg, hashFn) - return { - signature: new Signature(Buffer.from(output.signature)), - finalPublicNonce: new FinalPublicNonce(Buffer.from(output.finalPublicNonce)), - challenge: new Challenge(Buffer.from(output.challenge)), + signature: new Signature(Buffer.from(musigData.signature)), + publicNonce: new PublicNonce(Buffer.from(musigData.publicNonce)), + challenge: new Challenge(Buffer.from(musigData.challenge)), } } - static signHash(privateKey: Key, hash: string): SignatureOutput { - const output = _signHash(privateKey.buffer, hash) + /** + * Compute a single schnorr signature + * + * @param privateKey - the key you're signing with + * @param hash - the message you're signing + * @returns SignatureOutput + */ + static sign(privateKey: Key, hash: string): SignatureOutput { + const output = _sign(privateKey.buffer, hash) return { signature: new Signature(Buffer.from(output.signature)), - finalPublicNonce: new FinalPublicNonce(Buffer.from(output.finalPublicNonce)), + publicNonce: new PublicNonce(Buffer.from(output.publicNonce)), challenge: new Challenge(Buffer.from(output.challenge)), } } + /** + * Sum two signatures. + * Needed for a multisignature verification + * + * @param signatures + * @returns Signature + */ static sumSigs(signatures: Signature[]): Signature { const mappedSignatures = signatures.map(signature => signature.buffer) const sum = _sumSigs(mappedSignatures) return new Signature(Buffer.from(sum)) } + /** + * Off-chain signature verification + * + * @param signature - what we're verifying + * @param hash - the message that should have been signed + * @param publicNonce - the public version of the nonce used for the signature + * @param publicKey - the public key of the private key used for the signature + * @returns + */ static verify( - signature: Signature, - msg: string, - finalPublicNonce: FinalPublicNonce, - publicKey: Key, - hashFn: Function|null = null - ): boolean { - return _verify(signature.buffer, msg, finalPublicNonce.buffer, publicKey.buffer, hashFn) - } - - static verifyHash( signature: Signature, hash: string, - finalPublicNonce: FinalPublicNonce, + publicNonce: PublicNonce, publicKey: Key ): boolean { - return _verifyHash(signature.buffer, hash, finalPublicNonce.buffer, publicKey.buffer) + return _verify(signature.buffer, hash, publicNonce.buffer, publicKey.buffer) } } diff --git a/src/types/index.ts b/src/types/index.ts index 0b86beb..1c30887 100644 --- a/src/types/index.ts +++ b/src/types/index.ts @@ -1,4 +1,4 @@ export { Key } from './key' export { KeyPair } from './key-pair' export { PublicNonces, Nonces, NoncePairs } from './nonce' -export { Signature, Challenge, SignatureOutput, FinalPublicNonce } from './signature' +export { Signature, Challenge, SignatureOutput, PublicNonce } from './signature' diff --git a/src/types/signature.ts b/src/types/signature.ts index 4d56c11..e966317 100644 --- a/src/types/signature.ts +++ b/src/types/signature.ts @@ -1,11 +1,11 @@ export interface SignatureOutput { - finalPublicNonce: FinalPublicNonce, // the final public nonce + publicNonce: PublicNonce, // the final public nonce challenge: Challenge, // the schnorr challenge signature: Signature, // the signature } -export class FinalPublicNonce { +export class PublicNonce { readonly buffer: Buffer constructor(buffer: Buffer) { @@ -16,8 +16,8 @@ export class FinalPublicNonce { return this.buffer.toString('hex') } - static fromHex(hex: string): FinalPublicNonce { - return new FinalPublicNonce(Buffer.from(hex, 'hex')) + static fromHex(hex: string): PublicNonce { + return new PublicNonce(Buffer.from(hex, 'hex')) } } @@ -32,8 +32,8 @@ export class Challenge { return this.buffer.toString('hex') } - static fromHex(hex: string): FinalPublicNonce { - return new FinalPublicNonce(Buffer.from(hex, 'hex')) + static fromHex(hex: string): Challenge { + return new Challenge(Buffer.from(hex, 'hex')) } } @@ -48,8 +48,8 @@ export class Signature { return this.buffer.toString('hex') } - static fromHex(hex: string): FinalPublicNonce { - return new FinalPublicNonce(Buffer.from(hex, 'hex')) + static fromHex(hex: string): Signature { + return new Signature(Buffer.from(hex, 'hex')) } } diff --git a/tests/schnorrkel/getPublicNonces.test.ts b/tests/schnorrkel/getPublicNonces.test.ts index 2774ee6..42b248d 100644 --- a/tests/schnorrkel/getPublicNonces.test.ts +++ b/tests/schnorrkel/getPublicNonces.test.ts @@ -21,4 +21,10 @@ describe('testing getPublicNonces', () => { expect(retrievedPublicNonces.kPublic.buffer).to.equal(publicNonces.kPublic.buffer) expect(retrievedPublicNonces.kTwoPublic.buffer).to.equal(publicNonces.kTwoPublic.buffer) }) + it('should throw an error when calling getPublicNonces if they are not set', () => { + const schnorrkel = new Schnorrkel() + + const keyPair = generateRandomKeys() + expect(() => schnorrkel.getPublicNonces(keyPair.privateKey)).toThrowError('Nonces not set') + }) }) \ No newline at end of file diff --git a/tests/schnorrkel/multiSigSign.test.ts b/tests/schnorrkel/multiSigSign.test.ts index a879ace..2fd0925 100644 --- a/tests/schnorrkel/multiSigSign.test.ts +++ b/tests/schnorrkel/multiSigSign.test.ts @@ -19,10 +19,10 @@ describe('testing multiSigSign', () => { const publicKeys = [keyPairOne.publicKey, keyPairTwo.publicKey] const msg = 'test message' - const signature = schnorrkelOne.multiSigSign(keyPairOne.privateKey, msg, publicKeys, publicNonces) + const signature = schnorrkelOne.multiSigSign(keyPairOne.privateKey, ethers.utils.hashMessage(msg), publicKeys, publicNonces) expect(signature).toBeDefined() - expect(signature.finalPublicNonce.buffer).toHaveLength(33) + expect(signature.publicNonce.buffer).toHaveLength(33) expect(signature.signature.buffer).toHaveLength(32) expect(signature.challenge.buffer).toHaveLength(32) }) @@ -33,9 +33,10 @@ describe('testing multiSigSign', () => { const publicNonces = schnorrkel.generatePublicNonces(keyPair.privateKey) const msg = 'test message' + const msgHash = ethers.utils.hashMessage(msg) const publicKeys = [keyPair.publicKey] - expect(() => schnorrkel.multiSigSign(keyPair.privateKey, msg, publicKeys, [publicNonces])).toThrowError('At least 2 public keys should be provided') + expect(() => schnorrkel.multiSigSign(keyPair.privateKey, msgHash, publicKeys, [publicNonces])).toThrowError('At least 2 public keys should be provided') }) it('should requires nonces', () => { @@ -44,9 +45,10 @@ describe('testing multiSigSign', () => { const keyPairTwo = generateRandomKeys() const msg = 'test message' + const msgHash = ethers.utils.hashMessage(msg) const publicKeys = [keyPairOne.publicKey, keyPairTwo.publicKey] - expect(() => schnorrkel.multiSigSign(keyPairOne.privateKey, msg, publicKeys, [])).toThrowError('Nonces should be exchanged before signing') + expect(() => schnorrkel.multiSigSign(keyPairOne.privateKey, msgHash, publicKeys, [])).toThrowError('Nonces should be exchanged before signing') }) it('should generate multi signature by hash', () => { @@ -63,10 +65,10 @@ describe('testing multiSigSign', () => { const msg = 'test message' const hash = ethers.utils.solidityKeccak256(['string'], [msg]) - const signature = schnorrkelOne.multiSigSignHash(keyPairOne.privateKey, hash, publicKeys, publicNonces) + const signature = schnorrkelOne.multiSigSign(keyPairOne.privateKey, hash, publicKeys, publicNonces) expect(signature).toBeDefined() - expect(signature.finalPublicNonce.buffer).toHaveLength(33) + expect(signature.publicNonce.buffer).toHaveLength(33) expect(signature.signature.buffer).toHaveLength(32) expect(signature.challenge.buffer).toHaveLength(32) }) diff --git a/tests/schnorrkel/onchainMultiSign.test.ts b/tests/schnorrkel/onchainMultiSign.test.ts index 6f4a987..c9659f3 100644 --- a/tests/schnorrkel/onchainMultiSign.test.ts +++ b/tests/schnorrkel/onchainMultiSign.test.ts @@ -1,10 +1,9 @@ import { describe, expect, it } from 'vitest' import { ethers } from 'ethers' -import Schnorrkel, { Key } from '../../src/index' +import Schnorrkel from '../../src/index' import { compile } from '../../utils/compile.js' import { wallet2 } from '../config.js' import DefaultSigner from '../../utils/DefaultSigner' -import { _generatePk } from '../../src/core' const ERC1271_MAGICVALUE_BYTES32 = '0x1626ba7e' describe('Multi Sign Tests', function () { @@ -34,17 +33,16 @@ describe('Multi Sign Tests', function () { const { contract } = await deployContract(signerOne, signerTwo) const msg = 'just a test message' + const msgHash = ethers.utils.solidityKeccak256(['string'], [msg]) const publicKeys = [signerOne.getPublicKey(), signerTwo.getPublicKey()] const publicNonces = [signerOne.getPublicNonces(), signerTwo.getPublicNonces()] const combinedPublicKey = Schnorrkel.getCombinedPublicKey(publicKeys) - const {signature: sigOne, challenge: e, finalPublicNonce} = signerOne.multiSignMessage(msg, publicKeys, publicNonces) - const {signature: sigTwo} = signerTwo.multiSignMessage(msg, publicKeys, publicNonces) + const {signature: sigOne, challenge: e, publicNonce} = signerOne.multiSignMessage(msgHash, publicKeys, publicNonces) + const {signature: sigTwo} = signerTwo.multiSignMessage(msgHash, publicKeys, publicNonces) const sSummed = Schnorrkel.sumSigs([sigOne, sigTwo]) // the multisig px and parity const px = ethers.utils.hexlify(combinedPublicKey.buffer.slice(1, 33)) - const combinedPublicAddress = '0x' + px.slice(px.length - 40, px.length) - const parity = combinedPublicKey.buffer[0] - 2 + 27 // wrap the result @@ -55,28 +53,27 @@ describe('Multi Sign Tests', function () { sSummed.buffer, parity ]) - const msgHash = ethers.utils.solidityKeccak256(['string'], [msg]) const result = await contract.isValidSignature(msgHash, sigData) expect(result).to.equal(ERC1271_MAGICVALUE_BYTES32) }) - it('should generate a schnorr musig2 and validate it on the blockchain', async function () { + it('should generate the same sig to be sure caching does not affect validation', async function () { // deploy the contract const signerOne = new DefaultSigner(0) const signerTwo = new DefaultSigner(1) const { contract } = await deployContract(signerOne, signerTwo) const msg = 'just a test message' + const msgHash = ethers.utils.solidityKeccak256(['string'], [msg]) const publicKeys = [signerOne.getPublicKey(), signerTwo.getPublicKey()] const publicNonces = [signerOne.getPublicNonces(), signerTwo.getPublicNonces()] const combinedPublicKey = Schnorrkel.getCombinedPublicKey(publicKeys) - const {signature: sigOne, challenge: e, finalPublicNonce} = signerOne.multiSignMessage(msg, publicKeys, publicNonces) - const {signature: sigTwo} = signerTwo.multiSignMessage(msg, publicKeys, publicNonces) + const {signature: sigOne, challenge: e, publicNonce} = signerOne.multiSignMessage(msgHash, publicKeys, publicNonces) + const {signature: sigTwo} = signerTwo.multiSignMessage(msgHash, publicKeys, publicNonces) const sSummed = Schnorrkel.sumSigs([sigOne, sigTwo]) // the multisig px and parity const px = ethers.utils.hexlify(combinedPublicKey.buffer.slice(1, 33)) - const combinedPublicAddress = '0x' + px.slice(px.length - 40, px.length) const parity = combinedPublicKey.buffer[0] - 2 + 27 @@ -88,7 +85,6 @@ describe('Multi Sign Tests', function () { sSummed.buffer, parity ]) - const msgHash = ethers.utils.solidityKeccak256(['string'], [msg]) const result = await contract.isValidSignature(msgHash, sigData) expect(result).to.equal(ERC1271_MAGICVALUE_BYTES32) }) @@ -101,15 +97,16 @@ describe('Multi Sign Tests', function () { const signerThree = new DefaultSigner(2) const msg = 'just a test message' + const msgHash = ethers.utils.solidityKeccak256(['string'], [msg]) const publicKeys = [signerOne.getPublicKey(), signerThree.getPublicKey()] const publicNonces = [signerOne.getPublicNonces(), signerThree.getPublicNonces()] const combinedPublicKey = Schnorrkel.getCombinedPublicKey(publicKeys) -// finalPublicNonce: FinalPublicNonce, // the final public nonce +// publicNonce: publicNonce, // the final public nonce // challenge: Challenge, // the schnorr challenge // signature: Signature, // the signature - const {signature: sigOne, challenge: e} = signerOne.multiSignMessage(msg, publicKeys, publicNonces) - const {signature: sigTwo} = signerThree.multiSignMessage(msg, publicKeys, publicNonces) + const {signature: sigOne, challenge: e} = signerOne.multiSignMessage(msgHash, publicKeys, publicNonces) + const {signature: sigTwo} = signerThree.multiSignMessage(msgHash, publicKeys, publicNonces) const sSummed = Schnorrkel.sumSigs([sigOne, sigTwo]) // the multisig px and parity @@ -124,7 +121,6 @@ describe('Multi Sign Tests', function () { sSummed.buffer, parity ]) - const msgHash = ethers.utils.solidityKeccak256(['string'], [msg]) const result = await contract.isValidSignature(msgHash, sigData) expect(result).to.equal('0xffffffff') }) @@ -136,10 +132,11 @@ describe('Multi Sign Tests', function () { const { contract } = await deployContract(signerOne, signerTwo) const msg = 'just a test message' + const msgHash = ethers.utils.solidityKeccak256(['string'], [msg]) const publicKeys = [signerOne.getPublicKey(), signerTwo.getPublicKey()] const publicNonces = [signerOne.getPublicNonces(), signerTwo.getPublicNonces()] const combinedPublicKey = Schnorrkel.getCombinedPublicKey(publicKeys) - const {signature: sigOne, challenge: e} = signerOne.multiSignMessage(msg, publicKeys, publicNonces) + const {signature: sigOne, challenge: e} = signerOne.multiSignMessage(msgHash, publicKeys, publicNonces) // the multisig px and parity const px = combinedPublicKey.buffer.slice(1,33) @@ -153,7 +150,6 @@ describe('Multi Sign Tests', function () { sigOne.buffer, parity ]) - const msgHash = ethers.utils.solidityKeccak256(['string'], [msg]) const result = await contract.isValidSignature(msgHash, sigData) expect(result).to.equal('0xffffffff') }) @@ -162,13 +158,14 @@ describe('Multi Sign Tests', function () { // deploy the contract const signerOne = new DefaultSigner(0) const signerTwo = new DefaultSigner(1) - const { contract } = await deployContract(signerOne, signerTwo) + await deployContract(signerOne, signerTwo) const msg = 'just a test message' + const msgHash = ethers.utils.solidityKeccak256(['string'], [msg]) const publicKeys = [signerOne.getPublicKey(), signerTwo.getPublicKey()] const publicNonces = [signerOne.getPublicNonces(), signerTwo.getPublicNonces()] - const {signature: s, challenge: e} = signerOne.multiSignMessage(msg, publicKeys, publicNonces) - expect(signerOne.multiSignMessage.bind(signerOne, msg, publicKeys, publicNonces)).to.throw('Nonces should be exchanged before signing') + signerOne.multiSignMessage(msgHash, publicKeys, publicNonces) + expect(signerOne.multiSignMessage.bind(signerOne, msgHash, publicKeys, publicNonces)).to.throw('Nonces should be exchanged before signing') }) it('should fail if only one signer tries to sign the transaction providing 2 messages', async function () { @@ -178,13 +175,14 @@ describe('Multi Sign Tests', function () { const { contract } = await deployContract(signerOne, signerTwo) const msg = 'just a test message' + const msgHash = ethers.utils.solidityKeccak256(['string'], [msg]) const publicKeys = [signerOne.getPublicKey(), signerTwo.getPublicKey()] const signerTwoNonces = signerTwo.getPublicNonces() const publicNonces = [signerOne.getPublicNonces(), signerTwoNonces] const combinedPublicKey = Schnorrkel.getCombinedPublicKey(publicKeys) - const {signature: sigOne, challenge: e} = signerOne.multiSignMessage(msg, publicKeys, publicNonces) + const {signature: sigOne, challenge: e} = signerOne.multiSignMessage(msgHash, publicKeys, publicNonces) const publicNoncesTwo = [signerOne.getPublicNonces(), signerTwoNonces] - const {signature: sigTwo} = signerOne.multiSignMessage(msg, publicKeys, publicNoncesTwo) + const {signature: sigTwo} = signerOne.multiSignMessage(msgHash, publicKeys, publicNoncesTwo) const sSummed = Schnorrkel.sumSigs([sigOne, sigTwo]) // the multisig px and parity @@ -199,7 +197,6 @@ describe('Multi Sign Tests', function () { sSummed.buffer, parity ]) - const msgHash = ethers.utils.solidityKeccak256(['string'], [msg]) const result = await contract.isValidSignature(msgHash, sigData) expect(result).to.equal('0xffffffff') }) @@ -211,11 +208,12 @@ describe('Multi Sign Tests', function () { const { contract } = await deployContract(signerOne, signerTwo) const msg = 'just a test message' + const msgHash = ethers.utils.solidityKeccak256(['string'], [msg]) const publicKeys = [signerTwo.getPublicKey(), signerOne.getPublicKey()] const publicNonces = [signerOne.getPublicNonces(), signerTwo.getPublicNonces()] const combinedPublicKey = Schnorrkel.getCombinedPublicKey(publicKeys) - const {signature: sigOne, challenge: e} = signerOne.multiSignMessage(msg, publicKeys, publicNonces) - const {signature: sigTwo} = signerTwo.multiSignMessage(msg, publicKeys, publicNonces) + const {signature: sigOne, challenge: e} = signerOne.multiSignMessage(msgHash, publicKeys, publicNonces) + const {signature: sigTwo} = signerTwo.multiSignMessage(msgHash, publicKeys, publicNonces) const sSummed = Schnorrkel.sumSigs([sigOne, sigTwo]) // the multisig px and parity @@ -230,7 +228,6 @@ describe('Multi Sign Tests', function () { sSummed.buffer, parity ]) - const msgHash = ethers.utils.solidityKeccak256(['string'], [msg]) const result = await contract.isValidSignature(msgHash, sigData) expect(result).to.equal(ERC1271_MAGICVALUE_BYTES32) }) @@ -251,11 +248,11 @@ describe('Multi Sign Tests', function () { expect(e.message).to.equal('At least 2 public keys should be provided') } - const msg = 'just a test message' + const msgHash = ethers.utils.hashMessage('just a test message') const publicKeys = [signerOne.getPublicKey()] const publicNonces = [signerOne.getPublicNonces(), signerTwo.getPublicNonces()] try { - signerOne.multiSignMessage(msg, publicKeys, publicNonces) + signerOne.multiSignMessage(msgHash, publicKeys, publicNonces) } catch (e: any) { expect(e.message).to.equal('At least 2 public keys should be provided') } diff --git a/tests/schnorrkel/onchainSingleSign.test.ts b/tests/schnorrkel/onchainSingleSign.test.ts index 1f5c1ea..fa50808 100644 --- a/tests/schnorrkel/onchainSingleSign.test.ts +++ b/tests/schnorrkel/onchainSingleSign.test.ts @@ -31,8 +31,9 @@ describe('Single Sign Tests', function () { // sign const msg = 'just a test message' - const pkBuffer = new Key(Buffer.from(ethers.utils.arrayify(pk1))) - const sig = Schnorrkel.sign(pkBuffer, msg) + const msgHash = ethers.utils.hashMessage(msg) + const privateKey = new Key(Buffer.from(ethers.utils.arrayify(pk1))) + const sig = Schnorrkel.sign(privateKey, msgHash) // wrap the result const publicKey = secp256k1.publicKeyCreate(ethers.utils.arrayify(pk1)) @@ -45,7 +46,6 @@ describe('Single Sign Tests', function () { sig.signature.buffer, parity ]) - const msgHash = ethers.utils.solidityKeccak256(['string'], [msg]) const result = await contract.isValidSignature(msgHash, sigData) expect(result).to.equal(ERC1271_MAGICVALUE_BYTES32) }) diff --git a/tests/schnorrkel/sign.test.ts b/tests/schnorrkel/sign.test.ts index b5958a7..99cda3b 100644 --- a/tests/schnorrkel/sign.test.ts +++ b/tests/schnorrkel/sign.test.ts @@ -5,27 +5,28 @@ import { generateRandomKeys } from '../../src/core' import { ethers } from 'ethers' describe('testing sign', () => { - it('should sign a message', () => { + it('should sign a message with solidityKeccak256', () => { const keyPair = generateRandomKeys() const msg = 'test message' - const signature = Schnorrkel.sign(keyPair.privateKey, msg) + const hash = ethers.utils.solidityKeccak256(['string'], [msg]) + const signature = Schnorrkel.sign(keyPair.privateKey, hash) expect(signature).toBeDefined() - expect(signature.finalPublicNonce.buffer).toHaveLength(33) + expect(signature.publicNonce.buffer).toHaveLength(33) expect(signature.signature.buffer).toHaveLength(32) expect(signature.challenge.buffer).toHaveLength(32) }) - it('should sign a hash', () => { + it('should sign a message with keccak256', () => { const keyPair = generateRandomKeys() const msg = 'test message' - const hash = ethers.utils.solidityKeccak256(['string'], [msg]) - const signature = Schnorrkel.signHash(keyPair.privateKey, hash) + const hash = ethers.utils.hashMessage(msg) + const signature = Schnorrkel.sign(keyPair.privateKey, hash) expect(signature).toBeDefined() - expect(signature.finalPublicNonce.buffer).toHaveLength(33) + expect(signature.publicNonce.buffer).toHaveLength(33) expect(signature.signature.buffer).toHaveLength(32) expect(signature.challenge.buffer).toHaveLength(32) }) diff --git a/tests/schnorrkel/sumSigs.test.ts b/tests/schnorrkel/sumSigs.test.ts index 7e3a9e0..4688968 100644 --- a/tests/schnorrkel/sumSigs.test.ts +++ b/tests/schnorrkel/sumSigs.test.ts @@ -2,6 +2,7 @@ import { describe, expect, it } from 'vitest' import Schnorrkel from '../../src/index' import { _hashPrivateKey, generateRandomKeys } from '../../src/core' +import { ethers } from 'ethers' describe('testing sumSigs', () => { @@ -18,8 +19,9 @@ describe('testing sumSigs', () => { const publicKeys = [keyPairOne.publicKey, keyPairTwo.publicKey] const msg = 'test message' - const signatureOne = schnorrkelOne.multiSigSign(keyPairOne.privateKey, msg, publicKeys, publicNonces) - const signatureTwo = schnorrkelTwo.multiSigSign(keyPairTwo.privateKey, msg, publicKeys, publicNonces) + const msgHash = ethers.utils.solidityKeccak256(['string'],[msg]) + const signatureOne = schnorrkelOne.multiSigSign(keyPairOne.privateKey, msgHash, publicKeys, publicNonces) + const signatureTwo = schnorrkelTwo.multiSigSign(keyPairTwo.privateKey, msgHash, publicKeys, publicNonces) const signatures = [signatureOne.signature, signatureTwo.signature] const signature = Schnorrkel.sumSigs(signatures) diff --git a/tests/schnorrkel/verify.test.ts b/tests/schnorrkel/verify.test.ts index 0cef04f..1007cef 100644 --- a/tests/schnorrkel/verify.test.ts +++ b/tests/schnorrkel/verify.test.ts @@ -9,22 +9,24 @@ describe('testing verify', () => { const privateKey = new Key(Buffer.from(ethers.utils.randomBytes(32))) const msg = 'test message' - const signature = Schnorrkel.sign(privateKey, msg) + const msgHash = ethers.utils.solidityKeccak256(['string'], [msg]) + const signature = Schnorrkel.sign(privateKey, msgHash) const publicKey = ethers.utils.arrayify( ethers.utils.computePublicKey(ethers.utils.computePublicKey(privateKey.buffer, false), true) ) expect(signature).toBeDefined() - expect(signature.finalPublicNonce.buffer).toHaveLength(33) + expect(signature.publicNonce.buffer).toHaveLength(33) expect(signature.signature.buffer).toHaveLength(32) expect(signature.challenge.buffer).toHaveLength(32) - const result = Schnorrkel.verify(signature.signature, msg, signature.finalPublicNonce, new Key(Buffer.from(publicKey))) + const result = Schnorrkel.verify(signature.signature, ethers.utils.solidityKeccak256(['string'], [msg]), signature.publicNonce, new Key(Buffer.from(publicKey))) expect(result).toEqual(true) const secondMsg = 'this is another msg' - const secondSig = Schnorrkel.sign(privateKey, secondMsg) - const secondRes = Schnorrkel.verify(secondSig.signature, secondMsg, secondSig.finalPublicNonce, new Key(Buffer.from(publicKey))) + const secondMsgHash = ethers.utils.solidityKeccak256(['string'], [secondMsg]) + const secondSig = Schnorrkel.sign(privateKey, secondMsgHash) + const secondRes = Schnorrkel.verify(secondSig.signature, ethers.utils.solidityKeccak256(['string'], [secondMsg]), secondSig.publicNonce, new Key(Buffer.from(publicKey))) expect(secondRes).toEqual(true) }) it('should sum signatures and verify them', () => { @@ -42,12 +44,13 @@ describe('testing verify', () => { const combinedPublicKey = Schnorrkel.getCombinedPublicKey(publicKeys) const msg = 'test message' - const signatureOne = schnorrkelOne.multiSigSign(keyPairOne.privateKey, msg, publicKeys, publicNonces) - const signatureTwo = schnorrkelTwo.multiSigSign(keyPairTwo.privateKey, msg, publicKeys, publicNonces) + const msgHash = ethers.utils.solidityKeccak256(['string'], [msg]) + const signatureOne = schnorrkelOne.multiSigSign(keyPairOne.privateKey, msgHash, publicKeys, publicNonces) + const signatureTwo = schnorrkelTwo.multiSigSign(keyPairTwo.privateKey, msgHash, publicKeys, publicNonces) const signatures = [signatureOne.signature, signatureTwo.signature] const signaturesSummed = Schnorrkel.sumSigs(signatures) - const result = Schnorrkel.verify(signaturesSummed, msg, signatureTwo.finalPublicNonce, combinedPublicKey) + const result = Schnorrkel.verify(signaturesSummed, ethers.utils.solidityKeccak256(['string'], [msg]), signatureTwo.publicNonce, combinedPublicKey) expect(result).toEqual(true) }) @@ -67,44 +70,44 @@ describe('testing verify', () => { const combinedPublicKey = Schnorrkel.getCombinedPublicKey(publicKeys) const msg = 'test message' + const msgHash = ethers.utils.solidityKeccak256(['string'], [msg]) const pkOneCache = new Key(Buffer.from(keyPairOne.privateKey.buffer)) expect(pkOneCache.buffer).toEqual(keyPairOne.privateKey.buffer) - const signatureOne = schnorrkelOne.multiSigSign(keyPairOne.privateKey, msg, publicKeys, publicNonces) + const signatureOne = schnorrkelOne.multiSigSign(keyPairOne.privateKey, msgHash, publicKeys, publicNonces) expect(pkOneCache.buffer).toEqual(keyPairOne.privateKey.buffer) const pkTwoCache = new Key(Buffer.from(keyPairTwo.privateKey.buffer)) expect(pkTwoCache.buffer).toEqual(keyPairTwo.privateKey.buffer) - const signatureTwo = schnorrkelTwo.multiSigSign(keyPairTwo.privateKey, msg, publicKeys, publicNonces) + const signatureTwo = schnorrkelTwo.multiSigSign(keyPairTwo.privateKey, msgHash, publicKeys, publicNonces) expect(pkTwoCache.buffer).toEqual(keyPairTwo.privateKey.buffer) const signatures = [signatureOne.signature, signatureTwo.signature] const signaturesSummed = Schnorrkel.sumSigs(signatures) - const result = Schnorrkel.verify(signaturesSummed, msg, signatureTwo.finalPublicNonce, combinedPublicKey) + const result = Schnorrkel.verify(signaturesSummed, msgHash, signatureTwo.publicNonce, combinedPublicKey) expect(result).toEqual(true) }) - it('should verify a schnorr signature with a custom hash function', () => { + it('should verify a schnorr signature with sha256 hash function', () => { const privateKey = new Key(Buffer.from(ethers.utils.randomBytes(32))) const abiCoder = new ethers.utils.AbiCoder() const msg = abiCoder.encode(['string'], ['test message']) - const hashFn = ethers.utils.keccak256 - const signature = Schnorrkel.sign(privateKey, msg, hashFn) + const msgHash = ethers.utils.sha256(ethers.utils.toUtf8Bytes(msg)) + const signature = Schnorrkel.sign(privateKey, msgHash) const publicKey = ethers.utils.arrayify( ethers.utils.computePublicKey(ethers.utils.computePublicKey(privateKey.buffer, false), true) ) expect(signature).toBeDefined() - expect(signature.finalPublicNonce.buffer).toHaveLength(33) + expect(signature.publicNonce.buffer).toHaveLength(33) expect(signature.signature.buffer).toHaveLength(32) expect(signature.challenge.buffer).toHaveLength(32) const result = Schnorrkel.verify( signature.signature, - msg, - signature.finalPublicNonce, - new Key(Buffer.from(publicKey)), - hashFn + msgHash, + signature.publicNonce, + new Key(Buffer.from(publicKey)) ) expect(result).toEqual(true) }) @@ -124,36 +127,36 @@ describe('testing verify', () => { const abiCoder = new ethers.utils.AbiCoder() const msg = abiCoder.encode(['string'], ['test message']) - const hashFn = ethers.utils.keccak256 - const signatureOne = schnorrkelOne.multiSigSign(keyPairOne.privateKey, msg, publicKeys, publicNonces, hashFn) - const signatureTwo = schnorrkelTwo.multiSigSign(keyPairTwo.privateKey, msg, publicKeys, publicNonces, hashFn) + const msgHash = ethers.utils.keccak256(msg) + const signatureOne = schnorrkelOne.multiSigSign(keyPairOne.privateKey, msgHash, publicKeys, publicNonces) + const signatureTwo = schnorrkelTwo.multiSigSign(keyPairTwo.privateKey, msgHash, publicKeys, publicNonces) const signatures = [signatureOne.signature, signatureTwo.signature] const signaturesSummed = Schnorrkel.sumSigs(signatures) - const result = Schnorrkel.verify(signaturesSummed, msg, signatureTwo.finalPublicNonce, combinedPublicKey, hashFn) + const result = Schnorrkel.verify(signaturesSummed, ethers.utils.keccak256(msg), signatureTwo.publicNonce, combinedPublicKey) expect(result).toEqual(true) }) - it('should verify a signature hash', () => { + it('should verify a signature for a msg hashed with solidityKeccak256', () => { const privateKey = new Key(Buffer.from(ethers.utils.randomBytes(32))) const msg = 'test message' const hash = ethers.utils.solidityKeccak256(['string'], [msg]) - const signature = Schnorrkel.signHash(privateKey, hash) + const signature = Schnorrkel.sign(privateKey, hash) const publicKey = ethers.utils.arrayify( ethers.utils.computePublicKey(ethers.utils.computePublicKey(privateKey.buffer, false), true) ) expect(signature).toBeDefined() - expect(signature.finalPublicNonce.buffer).toHaveLength(33) + expect(signature.publicNonce.buffer).toHaveLength(33) expect(signature.signature.buffer).toHaveLength(32) expect(signature.challenge.buffer).toHaveLength(32) - const result = Schnorrkel.verifyHash(signature.signature, hash, signature.finalPublicNonce, new Key(Buffer.from(publicKey))) + const result = Schnorrkel.verify(signature.signature, hash, signature.publicNonce, new Key(Buffer.from(publicKey))) expect(result).toEqual(true) }) - it('should verify a multi signature hash', () => { + it('should verify a multi signature for a msg hashed with solidityKeccak256', () => { const schnorrkelOne = new Schnorrkel() const schnorrkelTwo = new Schnorrkel() @@ -167,13 +170,13 @@ describe('testing verify', () => { const msg = 'test message' const hash = ethers.utils.solidityKeccak256(['string'], [msg]) - const signature = schnorrkelOne.multiSigSignHash(keyPairOne.privateKey, hash, publicKeys, publicNonces) - const signatureTwo = schnorrkelTwo.multiSigSignHash(keyPairTwo.privateKey, hash, publicKeys, publicNonces) + const signature = schnorrkelOne.multiSigSign(keyPairOne.privateKey, hash, publicKeys, publicNonces) + const signatureTwo = schnorrkelTwo.multiSigSign(keyPairTwo.privateKey, hash, publicKeys, publicNonces) const signatures = [signature.signature, signatureTwo.signature] const signaturesSummed = Schnorrkel.sumSigs(signatures) const combinedPublicKey = Schnorrkel.getCombinedPublicKey(publicKeys) - const result = Schnorrkel.verifyHash(signaturesSummed, hash, signature.finalPublicNonce, combinedPublicKey) + const result = Schnorrkel.verify(signaturesSummed, hash, signature.publicNonce, combinedPublicKey) expect(result).toEqual(true) }) }) \ No newline at end of file