import { Bytes, Ed25519, Engine, Hex, Mnemonic, X25519 } from 'ox' import { describe, expect, test } from 'vp/test' describe('createKeyPair', () => { test('default', () => { const keyPair = Ed25519.createKeyPair() expect(keyPair).toHaveProperty('privateKey') expect(keyPair).toHaveProperty('publicKey') expect(typeof keyPair.privateKey).toBe('string') expect(typeof keyPair.publicKey).toBe('string') expect(keyPair.privateKey).toMatch(/^0x[0-9a-fA-F]{64}$/) expect(keyPair.publicKey).toMatch(/^0x[0-9a-fA-F]{64}$/) }) test('with as: Hex', () => { const keyPair = Ed25519.createKeyPair({ as: 'Hex' }) expect(keyPair).toHaveProperty('privateKey') expect(keyPair).toHaveProperty('publicKey') expect(typeof keyPair.privateKey).toBe('string') expect(typeof keyPair.publicKey).toBe('string') expect(keyPair.privateKey).toMatch(/^0x[0-9a-fA-F]{64}$/) expect(keyPair.publicKey).toMatch(/^0x[0-9a-fA-F]{64}$/) }) test('with as: Bytes', () => { const keyPair = Ed25519.createKeyPair({ as: 'Bytes' }) expect(keyPair).toHaveProperty('privateKey') expect(keyPair).toHaveProperty('publicKey') expect(keyPair.privateKey).toBeInstanceOf(Uint8Array) expect(keyPair.publicKey).toBeInstanceOf(Uint8Array) expect(keyPair.privateKey).toHaveLength(32) expect(keyPair.publicKey).toHaveLength(32) }) test('unique keys', () => { const keyPair1 = Ed25519.createKeyPair() const keyPair2 = Ed25519.createKeyPair() expect(keyPair1.privateKey).not.toBe(keyPair2.privateKey) expect(keyPair1.publicKey).not.toBe(keyPair2.publicKey) }) }) describe('fromPrf', () => { test.each([ { privateKey: '0x2c513679b40b7572cbfaf5aee41680258ab23de9ec68d6eaf243470c28e7cea3', prf: '0x0000000000000000000000000000000000000000000000000000000000000000', }, { privateKey: '0x7ad5cfcdff4bd56cf35bd854b794c9e18f9bcc3e6e12f9ecc0ef44bb23c6d920', prf: '0x000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f', }, ] as const)('vector: $prf', ({ privateKey, prf }) => { expect(Ed25519.fromPrf(prf)).toBe(privateKey) }) test('value: Bytes', () => { const prf = Bytes.fromHex( '0x000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f', ) expect(Ed25519.fromPrf(prf)).toMatchInlineSnapshot( `"0x7ad5cfcdff4bd56cf35bd854b794c9e18f9bcc3e6e12f9ecc0ef44bb23c6d920"`, ) }) test('options: as', () => { const privateKey = Ed25519.fromPrf( '0x000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f', { as: 'Bytes' }, ) expect(privateKey).toEqual( Bytes.fromHex( '0x7ad5cfcdff4bd56cf35bd854b794c9e18f9bcc3e6e12f9ecc0ef44bb23c6d920', ), ) }) test('behavior: output signs and verifies', () => { const privateKey = Ed25519.fromPrf(Bytes.random(32)) const publicKey = Ed25519.getPublicKey({ privateKey }) const payload = '0xdeadbeef' const signature = Ed25519.sign({ payload, privateKey }) expect(Ed25519.verify({ payload, publicKey, signature })).toBe(true) }) test('behavior: uses versioned label and zero counter', () => { const messages: Hex.Hex[] = [] Engine.with( { Hash: { hmacSha256: (_key, message) => { messages.push(Hex.fromBytes(message)) return new Uint8Array(32) }, }, }, () => Ed25519.fromPrf( '0x0000000000000000000000000000000000000000000000000000000000000000', ), ) expect(messages).toMatchInlineSnapshot(` [ "0x6f782e656432353531392e66726f6d5072662e763100000000", ] `) }) test('behavior: zeroes intermediate key for Hex output', () => { const candidate = new Uint8Array(32).fill(1) Engine.with( { Hash: { hmacSha256: () => candidate, }, }, () => Ed25519.fromPrf( '0x0000000000000000000000000000000000000000000000000000000000000000', ), ) expect(candidate).toEqual(new Uint8Array(32)) }) test('error: PRF output is too short', () => { expect(() => Ed25519.fromPrf(new Uint8Array(31))) .toThrowErrorMatchingInlineSnapshot(` [Ed25519.InvalidPrfSizeError: PRF output must be exactly 32 bytes. Received 31 bytes.] `) }) test('error: PRF output is too long', () => { expect(() => Ed25519.fromPrf(new Uint8Array(33))) .toThrowErrorMatchingInlineSnapshot(` [Ed25519.InvalidPrfSizeError: PRF output must be exactly 32 bytes. Received 33 bytes.] `) }) }) describe('fromMnemonic', () => { const mnemonic = 'test test test test test test test test test test test junk' test('default', () => { const privateKey = Ed25519.fromMnemonic(mnemonic) expect(privateKey).toBe(Ed25519.fromSeed(Mnemonic.toSeed(mnemonic))) expect(privateKey).toMatchInlineSnapshot( `"0x23fb07a427d2bc36141d1e6c7e56c72679739eff374a8e8def282f1048674567"`, ) }) test('options: passphrase', () => { expect( Ed25519.fromMnemonic(mnemonic, { passphrase: 'qwerty' }), ).toMatchInlineSnapshot( `"0xe888791c9d783e772d92b0bf2aa326b1cda8214a03c3c07933615b1a98fc8651"`, ) }) test('options: as', () => { const privateKey = Ed25519.fromMnemonic(mnemonic, { as: 'Bytes' }) expect(privateKey).toBeInstanceOf(Uint8Array) expect(privateKey).toHaveLength(32) }) }) describe('fromSeed', () => { const seed = '0x000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f' test('vector', () => { expect(Ed25519.fromSeed(seed)).toMatchInlineSnapshot( `"0xfd09c7b2acda46034df7e428377fdc37200094c9b51690fa1b733ee3c16a2d07"`, ) }) test('value: Bytes', () => { expect(Ed25519.fromSeed(Bytes.fromHex(seed))).toMatchInlineSnapshot( `"0xfd09c7b2acda46034df7e428377fdc37200094c9b51690fa1b733ee3c16a2d07"`, ) }) test('options: as', () => { expect(Ed25519.fromSeed(seed, { as: 'Bytes' })).toEqual( Bytes.fromHex( '0xfd09c7b2acda46034df7e428377fdc37200094c9b51690fa1b733ee3c16a2d07', ), ) }) test('behavior: output signs and verifies', () => { const privateKey = Ed25519.fromSeed(new Uint8Array(64)) const publicKey = Ed25519.getPublicKey({ privateKey }) const payload = '0xdeadbeef' const signature = Ed25519.sign({ payload, privateKey }) expect(Ed25519.verify({ payload, publicKey, signature })).toBe(true) }) test('behavior: zeroes intermediate key for Hex output', () => { const candidate = new Uint8Array(32).fill(1) Engine.with( { Hash: { hmacSha256: () => candidate, }, }, () => Ed25519.fromSeed(new Uint8Array(32)), ) expect(candidate).toEqual(new Uint8Array(32)) }) test('error: seed is too short', () => { expect(() => Ed25519.fromSeed(new Uint8Array(31))) .toThrowErrorMatchingInlineSnapshot(` [Ed25519.InvalidSeedSizeError: Seed must contain at least 32 bytes. Received 31 bytes.] `) }) }) describe('getPublicKey', () => { const privateKey = '0x9d61b19deffd5a60ba844af492ec2cc44449c5697b326919703bac031cae7f60' const expectedPublicKey = '0xd75a980182b10ab7d54bfed3c964073a0ee172f3daa62325af021a68f707511a' test('default (Hex)', () => { const publicKey = Ed25519.getPublicKey({ privateKey }) expect(publicKey).toBe(expectedPublicKey) }) test('with as: Hex', () => { const publicKey = Ed25519.getPublicKey({ privateKey, as: 'Hex' }) expect(publicKey).toBe(expectedPublicKey) }) test('with as: Bytes', () => { const publicKey = Ed25519.getPublicKey({ privateKey, as: 'Bytes' }) expect(publicKey).toBeInstanceOf(Uint8Array) expect(publicKey).toHaveLength(32) expect(Hex.fromBytes(publicKey)).toBe(expectedPublicKey) }) test('with Bytes private key', () => { const privateKeyBytes = Bytes.fromHex(privateKey) const publicKey = Ed25519.getPublicKey({ privateKey: privateKeyBytes }) expect(publicKey).toBe(expectedPublicKey) }) test('with Bytes private key, as: Bytes', () => { const privateKeyBytes = Bytes.fromHex(privateKey) const publicKey = Ed25519.getPublicKey({ privateKey: privateKeyBytes, as: 'Bytes', }) expect(publicKey).toBeInstanceOf(Uint8Array) expect(publicKey).toHaveLength(32) expect(Hex.fromBytes(publicKey)).toBe(expectedPublicKey) }) }) describe('randomPrivateKey', () => { test('default (Hex)', () => { const privateKey = Ed25519.randomPrivateKey() expect(typeof privateKey).toBe('string') expect(privateKey).toMatch(/^0x[0-9a-fA-F]{64}$/) }) test('with as: Hex', () => { const privateKey = Ed25519.randomPrivateKey({ as: 'Hex' }) expect(typeof privateKey).toBe('string') expect(privateKey).toMatch(/^0x[0-9a-fA-F]{64}$/) }) test('with as: Bytes', () => { const privateKey = Ed25519.randomPrivateKey({ as: 'Bytes' }) expect(privateKey).toBeInstanceOf(Uint8Array) expect(privateKey).toHaveLength(32) }) test('unique keys', () => { const privateKey1 = Ed25519.randomPrivateKey() const privateKey2 = Ed25519.randomPrivateKey() expect(privateKey1).not.toBe(privateKey2) }) }) describe('sign', () => { const privateKey = '0x9d61b19deffd5a60ba844af492ec2cc44449c5697b326919703bac031cae7f60' const payload = '0xdeadbeef' test('default (Hex)', () => { const signature = Ed25519.sign({ payload, privateKey }) expect(typeof signature).toBe('string') expect(signature).toMatch(/^0x[0-9a-fA-F]{128}$/) }) test('with as: Hex', () => { const signature = Ed25519.sign({ payload, privateKey, as: 'Hex' }) expect(typeof signature).toBe('string') expect(signature).toMatch(/^0x[0-9a-fA-F]{128}$/) }) test('with as: Bytes', () => { const signature = Ed25519.sign({ payload, privateKey, as: 'Bytes' }) expect(signature).toBeInstanceOf(Uint8Array) expect(signature).toHaveLength(64) }) test('with Bytes payload', () => { const payloadBytes = Bytes.fromHex(payload) const signature = Ed25519.sign({ payload: payloadBytes, privateKey }) expect(typeof signature).toBe('string') expect(signature).toMatch(/^0x[0-9a-fA-F]{128}$/) }) test('with Bytes private key', () => { const privateKeyBytes = Bytes.fromHex(privateKey) const signature = Ed25519.sign({ payload, privateKey: privateKeyBytes }) expect(typeof signature).toBe('string') expect(signature).toMatch(/^0x[0-9a-fA-F]{128}$/) }) test('with all Bytes', () => { const payloadBytes = Bytes.fromHex(payload) const privateKeyBytes = Bytes.fromHex(privateKey) const signature = Ed25519.sign({ payload: payloadBytes, privateKey: privateKeyBytes, as: 'Bytes', }) expect(signature).toBeInstanceOf(Uint8Array) expect(signature).toHaveLength(64) }) test('deterministic', () => { const signature1 = Ed25519.sign({ payload, privateKey }) const signature2 = Ed25519.sign({ payload, privateKey }) expect(signature1).toBe(signature2) }) test('known test vector', () => { // From RFC 8032 test vectors const testPrivateKey = '0x9d61b19deffd5a60ba844af492ec2cc44449c5697b326919703bac031cae7f60' const testPayload = '0x' // empty message const signature = Ed25519.sign({ payload: testPayload, privateKey: testPrivateKey, }) expect(signature).toBe( '0xe5564300c360ac729086e2cc806e828a84877f1eb8e5d974d873e065224901555fb8821590a33bacc61e39701cf9b46bd25bf5f0595bbe24655141438e7a100b', ) }) describe('integration', () => { test('sign and verify cycle', () => { const { privateKey, publicKey } = Ed25519.createKeyPair() const payload = '0x48656c6c6f2c20576f726c6421' const signature = Ed25519.sign({ payload, privateKey }) const isValid = Ed25519.verify({ payload, publicKey, signature }) expect(isValid).toBe(true) }) test('sign and verify cycle with Bytes', () => { const { privateKey, publicKey } = Ed25519.createKeyPair({ as: 'Bytes' }) const payload = Bytes.fromString('Hello, World!') const signature = Ed25519.sign({ payload, privateKey, as: 'Bytes' }) const isValid = Ed25519.verify({ payload, publicKey, signature }) expect(isValid).toBe(true) }) test('multiple signatures with same key', () => { const { privateKey, publicKey } = Ed25519.createKeyPair() const payloads = [ '0x48656c6c6f', '0x576f726c64', '0x546573744d657373616765', ] as const for (const payload of payloads) { const signature = Ed25519.sign({ payload, privateKey }) const isValid = Ed25519.verify({ payload, publicKey, signature }) expect(isValid).toBe(true) } }) }) }) describe('verify', () => { const privateKey = '0x9d61b19deffd5a60ba844af492ec2cc44449c5697b326919703bac031cae7f60' const publicKey = '0xd75a980182b10ab7d54bfed3c964073a0ee172f3daa62325af021a68f707511a' const payload = '0xdeadbeef' test('valid signature', () => { const signature = Ed25519.sign({ payload, privateKey }) const isValid = Ed25519.verify({ payload, publicKey, signature }) expect(isValid).toBe(true) }) test('invalid signature', () => { const signature = ('0x' + '00'.repeat(64)) as Hex.Hex const isValid = Ed25519.verify({ payload, publicKey, signature }) expect(isValid).toBe(false) }) test('wrong payload', () => { const signature = Ed25519.sign({ payload, privateKey }) const isValid = Ed25519.verify({ payload: '0xcafebabe', publicKey, signature, }) expect(isValid).toBe(false) }) test('wrong public key', () => { const signature = Ed25519.sign({ payload, privateKey }) const wrongPublicKey = ('0x' + '11'.repeat(32)) as Hex.Hex const isValid = Ed25519.verify({ payload, publicKey: wrongPublicKey, signature, }) expect(isValid).toBe(false) }) test('with Bytes payload', () => { const payloadBytes = Bytes.fromHex(payload) const signature = Ed25519.sign({ payload, privateKey }) const isValid = Ed25519.verify({ payload: payloadBytes, publicKey, signature, }) expect(isValid).toBe(true) }) test('with Bytes public key', () => { const publicKeyBytes = Bytes.fromHex(publicKey) const signature = Ed25519.sign({ payload, privateKey }) const isValid = Ed25519.verify({ payload, publicKey: publicKeyBytes, signature, }) expect(isValid).toBe(true) }) test('with Bytes signature', () => { const signatureHex = Ed25519.sign({ payload, privateKey }) const signatureBytes = Bytes.fromHex(signatureHex) const isValid = Ed25519.verify({ payload, publicKey, signature: signatureBytes, }) expect(isValid).toBe(true) }) test('with all Bytes', () => { const payloadBytes = Bytes.fromHex(payload) const publicKeyBytes = Bytes.fromHex(publicKey) const signatureBytes = Ed25519.sign({ payload: payloadBytes, privateKey, as: 'Bytes', }) const isValid = Ed25519.verify({ payload: payloadBytes, publicKey: publicKeyBytes, signature: signatureBytes, }) expect(isValid).toBe(true) }) test('known test vector', () => { // From RFC 8032 test vectors const testPublicKey = '0xd75a980182b10ab7d54bfed3c964073a0ee172f3daa62325af021a68f707511a' const testPayload = '0x' // empty message const testSignature = '0xe5564300c360ac729086e2cc806e828a84877f1eb8e5d974d873e065224901555fb8821590a33bacc61e39701cf9b46bd25bf5f0595bbe24655141438e7a100b' const isValid = Ed25519.verify({ payload: testPayload, publicKey: testPublicKey, signature: testSignature, }) expect(isValid).toBe(true) }) }) describe('toX25519PublicKey', () => { test('default (Hex)', () => { const { publicKey } = Ed25519.createKeyPair() const x25519PublicKey = Ed25519.toX25519PublicKey({ publicKey }) expect(typeof x25519PublicKey).toBe('string') expect(x25519PublicKey).toMatch(/^0x[0-9a-fA-F]{64}$/) }) test('with as: Bytes', () => { const { publicKey } = Ed25519.createKeyPair() const x25519PublicKey = Ed25519.toX25519PublicKey({ publicKey, as: 'Bytes', }) expect(x25519PublicKey).toBeInstanceOf(Uint8Array) expect(x25519PublicKey).toHaveLength(32) }) test('with Bytes public key', () => { const { publicKey } = Ed25519.createKeyPair({ as: 'Bytes' }) const x25519PublicKey = Ed25519.toX25519PublicKey({ publicKey }) expect(typeof x25519PublicKey).toBe('string') expect(x25519PublicKey).toMatch(/^0x[0-9a-fA-F]{64}$/) }) test('deterministic', () => { const { publicKey } = Ed25519.createKeyPair() const a = Ed25519.toX25519PublicKey({ publicKey }) const b = Ed25519.toX25519PublicKey({ publicKey }) expect(a).toBe(b) }) test('different from ed25519 public key', () => { const { publicKey } = Ed25519.createKeyPair() const x25519PublicKey = Ed25519.toX25519PublicKey({ publicKey }) expect(x25519PublicKey).not.toBe(publicKey) }) }) describe('toX25519PrivateKey', () => { test('default (Hex)', () => { const { privateKey } = Ed25519.createKeyPair() const x25519PrivateKey = Ed25519.toX25519PrivateKey({ privateKey }) expect(typeof x25519PrivateKey).toBe('string') expect(x25519PrivateKey).toMatch(/^0x[0-9a-fA-F]{64}$/) }) test('with as: Bytes', () => { const { privateKey } = Ed25519.createKeyPair() const x25519PrivateKey = Ed25519.toX25519PrivateKey({ privateKey, as: 'Bytes', }) expect(x25519PrivateKey).toBeInstanceOf(Uint8Array) expect(x25519PrivateKey).toHaveLength(32) }) test('with Bytes private key', () => { const { privateKey } = Ed25519.createKeyPair({ as: 'Bytes' }) const x25519PrivateKey = Ed25519.toX25519PrivateKey({ privateKey }) expect(typeof x25519PrivateKey).toBe('string') expect(x25519PrivateKey).toMatch(/^0x[0-9a-fA-F]{64}$/) }) test('deterministic', () => { const { privateKey } = Ed25519.createKeyPair() const a = Ed25519.toX25519PrivateKey({ privateKey }) const b = Ed25519.toX25519PrivateKey({ privateKey }) expect(a).toBe(b) }) }) describe('toX25519 integration', () => { test('converted keys produce valid X25519 shared secret', () => { const alice = Ed25519.createKeyPair() const bob = Ed25519.createKeyPair() const aliceX25519Priv = Ed25519.toX25519PrivateKey({ privateKey: alice.privateKey, }) const bobX25519Pub = Ed25519.toX25519PublicKey({ publicKey: bob.publicKey, }) const bobX25519Priv = Ed25519.toX25519PrivateKey({ privateKey: bob.privateKey, }) const aliceX25519Pub = Ed25519.toX25519PublicKey({ publicKey: alice.publicKey, }) const sharedA = X25519.getSharedSecret({ privateKey: aliceX25519Priv, publicKey: bobX25519Pub, }) const sharedB = X25519.getSharedSecret({ privateKey: bobX25519Priv, publicKey: aliceX25519Pub, }) expect(sharedA).toBe(sharedB) }) }) describe('noble export', () => { test('exports noble curves ed25519', () => { expect(Ed25519.noble).toBeDefined() expect(Ed25519.noble.sign).toBeDefined() expect(Ed25519.noble.verify).toBeDefined() expect(Ed25519.noble.getPublicKey).toBeDefined() }) }) test('exports', () => { expect(Object.keys(Ed25519)).toMatchInlineSnapshot(` [ "noble", "createKeyPair", "fromPrf", "fromMnemonic", "fromSeed", "getPublicKey", "randomPrivateKey", "sign", "verify", "toX25519PublicKey", "toX25519PrivateKey", "InvalidPrfSizeError", "InvalidSeedSizeError", ] `) })