ox
Version:
Ethereum Standard Library
613 lines (530 loc) • 17.5 kB
text/typescript
import { Bytes, Hex, P256 } from 'ox'
import { describe, expect, test } from 'vp/test'
import { accounts } from '../../../test/constants/accounts.js'
describe('getPublicKey', () => {
const privateKey =
'0xdde57ae9b9ed6f76fa5358c24d5ca2057ebc1ece18b7273121450a29c96ec8e5'
test('default', () => {
{
const publicKey = P256.getPublicKey({
privateKey,
})
expect(publicKey).toMatchInlineSnapshot(
`
{
"prefix": 4,
"x": "0x1753ed8e23fd6e17922ebdeed8ebe8043e34cd10118271cf2acdee88c1d58307",
"y": "0xc3357f052ea5e9a67625fa723ca0e7bc8ce9d069bea5b8b397137f991284b68c",
}
`,
)
}
{
const publicKey = P256.getPublicKey({
privateKey: Bytes.fromHex(accounts[0].privateKey),
})
expect(publicKey).toMatchInlineSnapshot(
`
{
"prefix": 4,
"x": "0xa43b66d1eaee03f07d64920491f8b3487a90f527f2342c8caccd55d506508449",
"y": "0x6c57d409d6db06faefd8a0aa1106acd69501134e11cf74b2e95c81b451da3433",
}
`,
)
}
})
})
describe('createKeyPair', () => {
test('default', () => {
const keyPair = P256.createKeyPair()
expect(keyPair).toHaveProperty('privateKey')
expect(keyPair).toHaveProperty('publicKey')
expect(typeof keyPair.privateKey).toBe('string')
expect(keyPair.privateKey).toMatch(/^0x[0-9a-f]{64}$/)
expect(keyPair.privateKey.length).toBe(66)
expect(keyPair.publicKey).toHaveProperty('prefix')
expect(keyPair.publicKey).toHaveProperty('x')
expect(keyPair.publicKey).toHaveProperty('y')
expect(keyPair.publicKey.prefix).toBe(4)
expect(typeof keyPair.publicKey.x).toBe('string')
expect(typeof keyPair.publicKey.y).toBe('string')
})
test('behavior: deterministic public key derivation', () => {
const keyPair = P256.createKeyPair()
const derivedPublicKey = P256.getPublicKey({
privateKey: keyPair.privateKey,
})
expect(keyPair.publicKey).toEqual(derivedPublicKey)
})
test('behavior: unique key pairs', () => {
const keyPair1 = P256.createKeyPair()
const keyPair2 = P256.createKeyPair()
expect(keyPair1.privateKey).not.toEqual(keyPair2.privateKey)
expect(keyPair1.publicKey).not.toEqual(keyPair2.publicKey)
})
test('behavior: valid for signing and verification', () => {
const keyPair = P256.createKeyPair()
const payload = '0xdeadbeef'
const signature = P256.sign({ payload, privateKey: keyPair.privateKey })
const isValid = P256.verify({
publicKey: keyPair.publicKey,
payload,
signature,
})
expect(isValid).toBe(true)
})
test('behavior: valid for ECDH key agreement', () => {
const keyPairA = P256.createKeyPair()
const keyPairB = P256.createKeyPair()
const sharedSecretA = P256.getSharedSecret({
privateKey: keyPairA.privateKey,
publicKey: keyPairB.publicKey,
})
const sharedSecretB = P256.getSharedSecret({
privateKey: keyPairB.privateKey,
publicKey: keyPairA.publicKey,
})
expect(sharedSecretA).toEqual(sharedSecretB)
expect(typeof sharedSecretA).toBe('string')
expect(sharedSecretA).toMatch(/^0x[0-9a-f]{66}$/)
})
test('options: as (Hex)', () => {
const keyPair = P256.createKeyPair({ as: 'Hex' })
expect(typeof keyPair.privateKey).toBe('string')
expect(keyPair.privateKey).toMatch(/^0x[0-9a-f]{64}$/)
expect(keyPair.privateKey.length).toBe(66)
})
test('options: as (Bytes)', () => {
const keyPair = P256.createKeyPair({ as: 'Bytes' })
expect(keyPair.privateKey).toBeInstanceOf(Uint8Array)
expect(keyPair.privateKey.length).toBe(32)
expect(keyPair.publicKey).toHaveProperty('prefix')
expect(keyPair.publicKey.prefix).toBe(4)
})
test('behavior: bytes format works with other functions', () => {
const keyPair = P256.createKeyPair({ as: 'Bytes' })
const derivedPublicKey = P256.getPublicKey({
privateKey: keyPair.privateKey,
})
expect(keyPair.publicKey).toEqual(derivedPublicKey)
})
test('behavior: public key recovery', () => {
const keyPair = P256.createKeyPair()
const payload = '0xdeadbeef'
const signature = P256.sign({ payload, privateKey: keyPair.privateKey })
const recoveredPublicKey = P256.recoverPublicKey({ payload, signature })
expect(recoveredPublicKey).toEqual(keyPair.publicKey)
})
})
describe('getSharedSecret', () => {
test('default', () => {
const { privateKey: privateKeyA, publicKey: publicKeyA } =
P256.createKeyPair()
const { privateKey: privateKeyB, publicKey: publicKeyB } =
P256.createKeyPair()
// Compute shared secret from A's perspective
const sharedSecretA = P256.getSharedSecret({
privateKey: privateKeyA,
publicKey: publicKeyB,
})
// Compute shared secret from B's perspective
const sharedSecretB = P256.getSharedSecret({
privateKey: privateKeyB,
publicKey: publicKeyA,
})
// ECDH property: both should be equal
expect(sharedSecretA).toEqual(sharedSecretB)
expect(typeof sharedSecretA).toBe('string')
expect(sharedSecretA).toMatch(/^0x[0-9a-f]{66}$/)
})
test('behavior: known test vectors', () => {
// Use fixed keys for deterministic testing
const privateKeyA =
'0xdde57ae9b9ed6f76fa5358c24d5ca2057ebc1ece18b7273121450a29c96ec8e5'
const privateKeyB =
'0x1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef'
const publicKeyA = P256.getPublicKey({ privateKey: privateKeyA })
const publicKeyB = P256.getPublicKey({ privateKey: privateKeyB })
const sharedSecretA = P256.getSharedSecret({
privateKey: privateKeyA,
publicKey: publicKeyB,
})
const sharedSecretB = P256.getSharedSecret({
privateKey: privateKeyB,
publicKey: publicKeyA,
})
expect(sharedSecretA).toEqual(sharedSecretB)
expect(sharedSecretA).toMatchInlineSnapshot(
'"0x02e0ad69b35f127c5d81d1aababa10ec6b3579ba9413d9b561203ddfa1df257af6"',
)
})
test('behavior: different input types', () => {
const { privateKey: privateKeyA } = P256.createKeyPair()
const { publicKey: publicKeyB } = P256.createKeyPair()
// Test with Hex private key
const sharedSecret1 = P256.getSharedSecret({
privateKey: privateKeyA,
publicKey: publicKeyB,
})
// Test with Bytes private key
const sharedSecret2 = P256.getSharedSecret({
privateKey: Bytes.fromHex(privateKeyA),
publicKey: publicKeyB,
})
expect(sharedSecret1).toEqual(sharedSecret2)
})
test('behavior: uncompressed public key', () => {
const { privateKey: privateKeyA } = P256.createKeyPair()
const { publicKey: publicKeyB } = P256.createKeyPair()
// Ensure the public key is uncompressed (prefix 4)
expect(publicKeyB.prefix).toBe(4)
const sharedSecret = P256.getSharedSecret({
privateKey: privateKeyA,
publicKey: publicKeyB,
})
expect(typeof sharedSecret).toBe('string')
expect(sharedSecret).toMatch(/^0x[0-9a-f]{66}$/)
})
test('options: as', () => {
const { privateKey: privateKeyA } = P256.createKeyPair()
const { publicKey: publicKeyB } = P256.createKeyPair()
// Test Hex output (default)
const sharedSecretHex = P256.getSharedSecret({
privateKey: privateKeyA,
publicKey: publicKeyB,
})
// Test Bytes output
const sharedSecretBytes = P256.getSharedSecret({
privateKey: privateKeyA,
publicKey: publicKeyB,
as: 'Bytes',
})
// Verify formats
expect(typeof sharedSecretHex).toBe('string')
expect(sharedSecretHex).toMatch(/^0x[0-9a-f]{66}$/)
expect(sharedSecretBytes).toBeInstanceOf(Uint8Array)
expect(sharedSecretBytes.length).toBe(33) // 33 bytes for compressed point
// Verify they represent the same data
expect(Hex.fromBytes(sharedSecretBytes)).toEqual(sharedSecretHex)
})
test('behavior: deterministic', () => {
const privateKeyA = P256.randomPrivateKey()
const privateKeyB = P256.randomPrivateKey()
const publicKeyB = P256.getPublicKey({ privateKey: privateKeyB })
const sharedSecret1 = P256.getSharedSecret({
privateKey: privateKeyA,
publicKey: publicKeyB,
})
const sharedSecret2 = P256.getSharedSecret({
privateKey: privateKeyA,
publicKey: publicKeyB,
})
// Should be deterministic - same inputs produce same output
expect(sharedSecret1).toEqual(sharedSecret2)
})
test('behavior: different key pairs produce different secrets', () => {
const privateKeyA = P256.randomPrivateKey()
const privateKeyB = P256.randomPrivateKey()
const privateKeyC = P256.randomPrivateKey()
const publicKeyB = P256.getPublicKey({ privateKey: privateKeyB })
const publicKeyC = P256.getPublicKey({ privateKey: privateKeyC })
const sharedSecretAB = P256.getSharedSecret({
privateKey: privateKeyA,
publicKey: publicKeyB,
})
const sharedSecretAC = P256.getSharedSecret({
privateKey: privateKeyA,
publicKey: publicKeyC,
})
// Different key pairs should produce different shared secrets
expect(sharedSecretAB).not.toEqual(sharedSecretAC)
})
test('error: invalid private key', () => {
const publicKeyB = P256.getPublicKey({
privateKey: P256.randomPrivateKey(),
})
expect(() =>
P256.getSharedSecret({
privateKey: '0x00',
publicKey: publicKeyB,
}),
).toThrow()
})
test('error: invalid public key', () => {
const privateKeyA = P256.randomPrivateKey()
const invalidPublicKey = {
prefix: 4,
x: '0x0000000000000000000000000000000000000000000000000000000000000000',
y: '0x0000000000000000000000000000000000000000000000000000000000000000',
} as const
expect(() =>
P256.getSharedSecret({
privateKey: privateKeyA,
publicKey: invalidPublicKey,
}),
).toThrow()
})
})
describe('randomPrivateKey', () => {
test('default', () => {
const privateKey = P256.randomPrivateKey()
expect(privateKey.length).toBe(66)
})
test('options: as', () => {
const privateKey = P256.randomPrivateKey({ as: 'Bytes' })
expect(privateKey.length).toBe(32)
})
})
describe('recoverPublicKey', () => {
const privateKey = P256.randomPrivateKey()
test('default', () => {
const payload = '0xdeadbeef'
const signature = P256.sign({ payload, privateKey })
expect(P256.recoverPublicKey({ payload, signature })).toStrictEqual(
P256.getPublicKey({ privateKey }),
)
})
})
describe('sign', () => {
const privateKey =
'0xdde57ae9b9ed6f76fa5358c24d5ca2057ebc1ece18b7273121450a29c96ec8e5'
const publicKey = P256.getPublicKey({ privateKey })
test('default', async () => {
{
const signature = P256.sign({
payload:
'0xd9eba16ed0ecae432b71fe008c98cc872bb4cc214d3220a36f365326cf807d68',
privateKey,
})
expect(signature).toMatchInlineSnapshot(
`
{
"r": "0x38cfa1c681a8a8a2dedc9657eda39fce932517ddf6e9595b7da89b400a3af75d",
"s": "0x61fc9d2f84a253fe75e8fbd4a6d24a8d5f29bf664ebe5009f827a246807cc400",
"yParity": 1,
}
`,
)
expect(
P256.verify({
publicKey,
payload:
'0xd9eba16ed0ecae432b71fe008c98cc872bb4cc214d3220a36f365326cf807d68',
signature,
}),
).toBe(true)
expect(
P256.verify({
publicKey,
payload: '0xbeef',
signature,
}),
).toBe(false)
}
{
const signature = P256.sign({
payload: Bytes.fromHex(
'0xdeadbeef1aaaa22111231241220000123aaaaabbccababab2211',
),
privateKey,
})
expect(signature).toMatchInlineSnapshot(
`
{
"r": "0xf130f7c7f3c62f6cf141874ccb17c9bd8eba4ea80c078f2354f9bb664137a314",
"s": "0x0ecf68f998506c55189f0542198848b979bd727999a60ac573910c5962bf5ae4",
"yParity": 0,
}
`,
)
expect(
P256.verify({
publicKey,
payload: '0xdeadbeef1aaaa22111231241220000123aaaaabbccababab2211',
signature,
}),
).toBe(true)
expect(
P256.verify({
publicKey,
payload: '0xbeef',
signature,
}),
).toBe(false)
}
})
test('options: hash', () => {
const signature = P256.sign({
hash: true,
payload:
'0xd9eba16ed0ecae432b71fe008c98cc872bb4cc214d3220a36f365326cf807d68',
privateKey,
})
expect(signature).toMatchInlineSnapshot(
`
{
"r": "0x5550cfd3b935570c4e9ace7d36d733368c7a7f755de10c783020b0ff0b74e95a",
"s": "0x0071e8aca10952c56847d3a599b62ba0d2ab42985b993f77d762514ee427bbe4",
"yParity": 0,
}
`,
)
expect(
P256.verify({
hash: true,
publicKey,
payload:
'0xd9eba16ed0ecae432b71fe008c98cc872bb4cc214d3220a36f365326cf807d68',
signature,
}),
).toBe(true)
expect(
P256.verify({
hash: true,
publicKey,
payload: '0xbeef',
signature,
}),
).toBe(false)
})
test('options: extraEntropy', () => {
{
const signature_1 = P256.sign({
extraEntropy: false,
payload:
'0xd9eba16ed0ecae432b71fe008c98cc872bb4cc214d3220a36f365326cf807d68',
privateKey: accounts[0].privateKey,
})
const signature_2 = P256.sign({
extraEntropy: false,
payload:
'0xd9eba16ed0ecae432b71fe008c98cc872bb4cc214d3220a36f365326cf807d68',
privateKey: accounts[0].privateKey,
})
expect(signature_1).toEqual(signature_2)
}
{
const signature_1 = P256.sign({
extraEntropy: Hex.random(32),
payload:
'0xd9eba16ed0ecae432b71fe008c98cc872bb4cc214d3220a36f365326cf807d68',
privateKey: accounts[0].privateKey,
})
const signature_2 = P256.sign({
extraEntropy: Hex.random(32),
payload:
'0xd9eba16ed0ecae432b71fe008c98cc872bb4cc214d3220a36f365326cf807d68',
privateKey: accounts[0].privateKey,
})
expect(signature_1).not.toEqual(signature_2)
}
{
const signature_1 = P256.sign({
extraEntropy: Bytes.random(32),
payload:
'0xd9eba16ed0ecae432b71fe008c98cc872bb4cc214d3220a36f365326cf807d68',
privateKey: accounts[0].privateKey,
})
const signature_2 = P256.sign({
extraEntropy: Bytes.random(32),
payload:
'0xd9eba16ed0ecae432b71fe008c98cc872bb4cc214d3220a36f365326cf807d68',
privateKey: accounts[0].privateKey,
})
expect(signature_1).not.toEqual(signature_2)
}
})
})
describe('verify', () => {
const privateKey = accounts[0].privateKey
test('default', () => {
const payload = '0xdeadbeef'
const { r, s } = P256.sign({ payload, privateKey })
const publicKey = P256.getPublicKey({ privateKey })
expect(P256.verify({ publicKey, payload, signature: { r, s } })).toBe(true)
})
test('behavior: bytes payload', () => {
const payload = '0xdeadbeef'
const { r, s } = P256.sign({ payload, privateKey })
const publicKey = P256.getPublicKey({ privateKey })
expect(
P256.verify({
publicKey,
payload: Bytes.fromHex(payload),
signature: { r, s },
}),
).toBe(true)
})
test('options: hash', () => {
const payload = '0xdeadbeef'
const { r, s } = P256.sign({ hash: true, payload, privateKey })
const publicKey = P256.getPublicKey({ privateKey })
expect(
P256.verify({ hash: true, publicKey, payload, signature: { r, s } }),
).toBe(true)
})
})
test('exports', () => {
expect(Object.keys(P256)).toMatchInlineSnapshot(`
[
"noble",
"createKeyPair",
"getPublicKey",
"getSharedSecret",
"randomPrivateKey",
"recoverPublicKey",
"sign",
"verify",
]
`)
})
describe('as option', () => {
test("sign + getPublicKey + recoverPublicKey: as 'Hex'", () => {
const { privateKey, publicKey } = P256.createKeyPair()
const sigHex = P256.sign({
payload: '0xdeadbeef',
privateKey,
as: 'Hex',
})
expect(typeof sigHex).toBe('string')
const pkHex = P256.getPublicKey({ privateKey, as: 'Hex' })
expect(typeof pkHex).toBe('string')
const recoveredHex = P256.recoverPublicKey({
payload: '0xdeadbeef',
signature: sigHex,
as: 'Hex',
})
expect(recoveredHex).toBe(pkHex)
expect(
P256.verify({ payload: '0xdeadbeef', publicKey, signature: sigHex }),
).toBe(true)
})
test("sign + getPublicKey: as 'Bytes'", () => {
const { privateKey } = P256.createKeyPair()
const sigBytes = P256.sign({
payload: '0xdeadbeef',
privateKey,
as: 'Bytes',
})
expect(sigBytes).toBeInstanceOf(Uint8Array)
const pkBytes = P256.getPublicKey({ privateKey, as: 'Bytes' })
expect(pkBytes).toBeInstanceOf(Uint8Array)
})
test('verify accepts Hex signature + Hex publicKey', () => {
const { privateKey } = P256.createKeyPair()
const pkHex = P256.getPublicKey({ privateKey, as: 'Hex' })
const sigHex = P256.sign({
payload: '0xdeadbeef',
privateKey,
as: 'Hex',
})
expect(
P256.verify({
payload: '0xdeadbeef',
publicKey: pkHex,
signature: sigHex,
}),
).toBe(true)
})
})