Skip to content

Commit

Permalink
refactor: add zilliqa schnorr signing
Browse files Browse the repository at this point in the history
  • Loading branch information
feri42 committed Nov 25, 2024
1 parent 5543631 commit 2269512
Show file tree
Hide file tree
Showing 5 changed files with 122 additions and 1 deletion.
48 changes: 48 additions & 0 deletions features/keychain/module/__tests__/schnorr-z.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
import KeyIdentifier from '@exodus/key-identifier'

Check notice

Code scanning / CodeQL

Unused variable, import, function or class Note test

Unused import KeyIdentifier.

jest.doMock('node:crypto', () => ({
__esModule: true,
...jest.requireActual('crypto'),
randomBytes: jest.fn(),
}))

const { schnorrZ } = await import('../crypto/schnorr-z.js')
const { create } = await import('../crypto/secp256k1.js')
const { randomBytes } = await import('node:crypto')

const extraEntropy = Buffer.from(
'1230000000000000000000000000000000000000000000000000000000000000',
'hex'
)

randomBytes.mockImplementation(() => {
// fix entropy to assert signatures
return extraEntropy
})

const fixture = {
priv: 'e7ef6cc440b01b2473540541aee345f6a6585e532a2590e2ab8a2bef379b1b0a',
pub: '03d219e7afe97792ce85e5ac18c79d86a3658f7f5ec7b53bc3f3ed334cc98b0366',
buffer:
'0881800410011a14a54e49719267e8312510d7b78598cef16ff127ce22230a210246e7178dc8253201101e18fd6f6eb9972451d121fc57aa2a06dd5c111e58dc6a2a120a100000000000000000000000000000001432120a100000000000000000000000000000000a38e901',
sig: 'a019058cd148c2821a3a98c9ffaf2d9c5a4a68b1ca3a844c8c51ca95d7a60ad12863cf7f6c6bee55e5447ce621dc8808cc429576636556a4f22de0d702e69c9c',
}

describe('SchnorrZ signer', async () => {
const getPrivateHDKey = () => ({
privateKey: Buffer.from(fixture.priv, 'hex'),
publicKey: Buffer.from(fixture.pub, 'hex'),
})
const secp256k1Signer = create({ getPrivateHDKey })

const { privateKey } = getPrivateHDKey()

const result0 = schnorrZ({ data: Buffer.from(fixture.buffer, 'hex'), privateKey })
expect(result0.toString('hex')).toBe(fixture.sig)

const result = await secp256k1Signer.signSchnorrZ({
keyId: { keyType: 'secp256k1' },
data: Buffer.from(fixture.buffer, 'hex'),
})
expect(result.toString('hex')).toBe(fixture.sig)
})
63 changes: 63 additions & 0 deletions features/keychain/module/crypto/schnorr-z.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
import { hashSync } from '@exodus/crypto/hash'
import { hmacSync } from '@exodus/crypto/hmac'
import { randomBytes } from '@exodus/crypto/randomBytes'
import * as secp256k1 from '@noble/secp256k1'

function singleRoundHmacDRBG(nonce) {
const seed = randomBytes(32)
let K = Buffer.alloc(32, 0)
let V = Buffer.alloc(32, 1)
K = hmacSync('sha256', K, [V, new Uint8Array([0]), seed, nonce])
V = hmacSync('sha256', K, V)
K = hmacSync('sha256', K, [V, new Uint8Array([1]), seed, nonce])
V = hmacSync('sha256', K, V)
return hmacSync('sha256', K, V)
}

/**
*
* // Based on The ZILLIQA Technical Whitepaper, Appendix A
* // https://docs.zilliqa.com/whitepaper.pdf
* // ---
* // Algorithm is as follows
* // 1. k = rand(1, n)
* // 2. Q = [k]G
* // 3. r = H(Q || pk || m) mod n
* // 4. If r = 0 Goto 1
* // 5. s = k - r * sk mod n
* // 6. If s = 0 Goto 1
* // 7. mu = (r, s)
* // 8. return mu.
*
* @param {Buffer} data
* @param {Buffer} privateKey
* @returns {string}
*/
export function schnorrZ({ data, privateKey }) {
const { utils, Signature, CURVE, getPublicKey } = secp256k1
const big = (buf) => BigInt('0x' + buf.toString('hex'))

const pk = getPublicKey(privateKey, true)

// eslint-disable-next-line no-constant-condition
while (true) {
// 1. k comes from drbg until satisfies 0 < k < n
const k = singleRoundHmacDRBG(data)
const kn = big(k)
if (!(kn > BigInt(0) && kn < CURVE.n)) continue // this is rechecked below

const Q = getPublicKey(k, true) // 2. This is Q = G * k multiplication. Also checks 0 < k < n and throws

const r = utils.mod(big(hashSync('sha256', [Q, pk, data])), CURVE.n) // 3
if (r === BigInt(0)) continue // 4

const s = utils.mod(kn - r * big(privateKey), CURVE.n) // 5
if (s === BigInt(0)) continue // 6

const sig = new Signature(r, s) // 7
return Buffer.from(sig.toCompactRawBytes()) // 8
}

// eslint-disable-next-line no-unreachable
throw new Error('Makes Flow happy')
}
9 changes: 9 additions & 0 deletions features/keychain/module/crypto/secp256k1.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { mapValues } from '@exodus/basic-utils'
import BN from 'bn.js'

import { tweakPrivateKey } from './tweak.js'
import { schnorrZ } from './schnorr-z.js'

export const create = ({ getPrivateHDKey }) => {
const createInstance = () => ({
Expand Down Expand Up @@ -51,6 +52,14 @@ export const create = ({ getPrivateHDKey }) => {
const privateKey = tweak ? tweakPrivateKey({ hdkey, tweak }) : hdkey.privateKey
return secp256k1.schnorrSign({ data, privateKey, extraEntropy, format: 'buffer' })
},
signSchnorrZ: async ({ seedId, keyId, data }) => {
assert(
keyId.keyType === 'secp256k1',
`SchnorrZ signatures are not supported for ${keyId.keyType}`
)
const { privateKey } = getPrivateHDKey({ seedId, keyId })
return schnorrZ({ data, privateKey })
},
})

// For backwards compatibility
Expand Down
1 change: 1 addition & 0 deletions features/keychain/module/keychain.js
Original file line number Diff line number Diff line change
Expand Up @@ -181,6 +181,7 @@ export class Keychain {
}
}

// @deprecated use keychain.(secp256k1|ed25519|sodium).sign* instead
async signTx({ seedId, keyIds, signTxCallback, unsignedTx }) {
this.#assertPrivateKeysUnlocked(seedId ? [seedId] : undefined)
assert(typeof signTxCallback === 'function', 'signTxCallback must be a function')
Expand Down
2 changes: 1 addition & 1 deletion features/keychain/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -37,14 +37,14 @@
"@exodus/key-utils": "^3.7.0",
"@exodus/slip10": "^2.2.0",
"@exodus/sodium-crypto": "^3.1.0",
"@noble/secp256k1": "^1.7.1",
"bn.js": "^5.2.1",
"buffer-json": "^2.0.0",
"json-stable-stringify": "^1.0.1",
"minimalistic-assert": "^1.0.1"
},
"devDependencies": {
"@exodus/key-ids": "^1.0.0",
"@noble/secp256k1": "^1.7.1",
"bip39": "2.6.0",
"eslint": "^8.44.0",
"events": "^3.3.0"
Expand Down

0 comments on commit 2269512

Please sign in to comment.