From 292dd33249b9501c9469e05546d4f42786c38f9a Mon Sep 17 00:00:00 2001 From: Nikita Skovoroda Date: Wed, 2 Oct 2024 15:07:58 +0300 Subject: [PATCH] feat: switch to exodus/crypto for secp256k1 (#152) --- .../keychain/module/__tests__/ecdsa.test.js | 25 +++++++++- features/keychain/module/crypto/secp256k1.js | 47 ++++++++++++------- features/keychain/package.json | 4 +- yarn.lock | 2 - 4 files changed, 54 insertions(+), 24 deletions(-) diff --git a/features/keychain/module/__tests__/ecdsa.test.js b/features/keychain/module/__tests__/ecdsa.test.js index 629ecf23..1e50819b 100644 --- a/features/keychain/module/__tests__/ecdsa.test.js +++ b/features/keychain/module/__tests__/ecdsa.test.js @@ -123,9 +123,32 @@ describe('EcDSA Signer Signature Encoding', () => { expect(signature.toString('hex')).toBe(expected.binary) }) + it('sig|rec encoding', async () => { + const signature = await keychain.secp256k1.signBuffer({ seedId, keyId, data, enc: 'sig|rec' }) + expect(signature instanceof Buffer).toBe(true) + expect(signature.toString('hex')).toBe(expected.binary) + }) + + it('rec|sig encoding', async () => { + const signature = await keychain.secp256k1.signBuffer({ seedId, keyId, data, enc: 'rec|sig' }) + expect(signature instanceof Buffer).toBe(true) + const recsig = expected.binary.slice(128) + expected.binary.slice(0, 128) + expect(signature.toString('hex')).toBe(recsig) + }) + + it('sig,rec encoding', async () => { + const res = await keychain.secp256k1.signBuffer({ seedId, keyId, data, enc: 'sig,rec' }) + expect(typeof res === 'object').toBe(true) + const { signature, recovery } = res + expect(signature instanceof Buffer).toBe(true) + expect(signature.toString('hex')).toBe(expected.binary.slice(0, 128)) + expect(recovery === 0 || recovery === 1).toBe(true) + expect(recovery).toBe(Buffer.from(expected.binary, 'hex')[64]) + }) + it('Raw encoding', async () => { const signature = await keychain.secp256k1.signBuffer({ seedId, keyId, data, enc: 'raw' }) - expect(typeof signature === 'object') + expect(typeof signature === 'object').toBe(true) expect(Object.getOwnPropertyNames(signature)).toStrictEqual(['r', 's', 'recoveryParam']) const r = Buffer.from(signature.r.toArray('be', 32)) const s = Buffer.from(signature.s.toArray('be', 32)) diff --git a/features/keychain/module/crypto/secp256k1.js b/features/keychain/module/crypto/secp256k1.js index 4fa326f8..08906bbe 100644 --- a/features/keychain/module/crypto/secp256k1.js +++ b/features/keychain/module/crypto/secp256k1.js @@ -1,34 +1,45 @@ import assert from 'minimalistic-assert' import * as secp256k1 from '@exodus/crypto/secp256k1' -import elliptic from '@exodus/elliptic' import { mapValues } from '@exodus/basic-utils' +import BN from 'bn.js' import { tweakPrivateKey } from './tweak.js' -const encodeSignature = ({ signature, enc }) => { - if (enc === 'der') return Buffer.from(signature.toDER()) - - if (enc === 'raw') return { ...signature } - - const r = Buffer.from(signature.r.toArray('be', 32)) - const s = Buffer.from(signature.s.toArray('be', 32)) - return Buffer.concat([r, s, Buffer.from([signature.recoveryParam])]) -} - export const create = ({ getPrivateHDKey }) => { - const EC = elliptic.ec - const curve = new EC('secp256k1') - const createInstance = () => ({ - signBuffer: async ({ seedId, keyId, data, enc = 'der' }) => { + signBuffer: async ({ seedId, keyId, data, enc = 'der', extraEntropy = null }) => { assert( keyId.keyType === 'secp256k1', `ECDSA signatures are not supported for ${keyId.keyType}` ) - assert(['der', 'raw', 'binary'].includes(enc), 'signBuffer: invalid enc') + if (enc === 'binary') enc = 'sig|rec' + assert( + ['der', 'raw', 'sig', 'sig|rec', 'rec|sig', 'sig,rec'].includes(enc), + 'signBuffer: invalid enc' + ) const { privateKey } = getPrivateHDKey({ seedId, keyId }) - const signature = curve.sign(data, privateKey, { canonical: true }) - return encodeSignature({ signature, enc }) + const res = await secp256k1.ecdsaSignHash({ + hash: data, + privateKey, + extraEntropy, + der: enc === 'der', + recovery: enc !== 'der' && enc !== 'sig', + format: 'buffer', + }) + if (enc === 'der' || enc === 'sig' || enc === 'sig,rec') return res + const { signature, recovery } = res + if (enc === 'sig|rec') return Buffer.concat([signature, new Uint8Array([recovery])]) + if (enc === 'rec|sig') return Buffer.concat([new Uint8Array([recovery]), signature]) + if (enc === 'raw') { + // Deprecated, compatibility mode with manual signature encoding + return { + r: new BN(signature.subarray(0, signature.length / 2)), + s: new BN(signature.subarray(signature.length / 2)), + recoveryParam: recovery, + } + } + + throw new Error('Unreachable') }, signSchnorr: async ({ seedId, keyId, data, tweak, extraEntropy }) => { assert( diff --git a/features/keychain/package.json b/features/keychain/package.json index 606c181c..d7c38abb 100644 --- a/features/keychain/package.json +++ b/features/keychain/package.json @@ -33,13 +33,12 @@ "@exodus/basic-utils": "^3.0.1", "@exodus/bip32": "^2.1.0", "@exodus/crypto": "^1.0.0-rc.13", - "@exodus/elliptic": "^6.5.4-precomputed", "@exodus/key-identifier": "^1.1.2", "@exodus/key-utils": "^3.5.1", "@exodus/slip10": "^2.0.0", "@exodus/sodium-crypto": "^3.1.0", + "bn.js": "^5.2.1", "buffer-json": "^2.0.0", - "create-hash": "^1.2.0", "json-stable-stringify": "^1.0.1", "minimalistic-assert": "^1.0.1" }, @@ -47,7 +46,6 @@ "@exodus/key-ids": "^1.0.0", "@noble/secp256k1": "^1.7.1", "bip39": "2.6.0", - "bn.js": "^5.2.1", "eslint": "^8.44.0", "events": "^3.3.0" }, diff --git a/yarn.lock b/yarn.lock index fab73cb8..3bda95df 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2555,7 +2555,6 @@ __metadata: "@exodus/basic-utils": ^3.0.1 "@exodus/bip32": ^2.1.0 "@exodus/crypto": ^1.0.0-rc.13 - "@exodus/elliptic": ^6.5.4-precomputed "@exodus/key-identifier": ^1.1.2 "@exodus/key-ids": ^1.0.0 "@exodus/key-utils": ^3.5.1 @@ -2565,7 +2564,6 @@ __metadata: bip39: 2.6.0 bn.js: ^5.2.1 buffer-json: ^2.0.0 - create-hash: ^1.2.0 eslint: ^8.44.0 events: ^3.3.0 json-stable-stringify: ^1.0.1