ox 1.1.0 → 1.1.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +27 -0
- package/dist/core/Engine.d.ts +43 -11
- package/dist/core/Engine.d.ts.map +1 -1
- package/dist/core/Engine.js +40 -10
- package/dist/core/Engine.js.map +1 -1
- package/dist/core/internal/engine.d.ts +8 -8
- package/dist/core/internal/engine.d.ts.map +1 -1
- package/dist/index.d.ts +5 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +5 -2
- package/dist/index.js.map +1 -1
- package/dist/node/Ed25519.d.ts +33 -0
- package/dist/node/Ed25519.d.ts.map +1 -0
- package/dist/node/Ed25519.js +67 -0
- package/dist/node/Ed25519.js.map +1 -0
- package/dist/node/Engine.d.ts +29 -16
- package/dist/node/Engine.d.ts.map +1 -1
- package/dist/node/Engine.js +38 -14
- package/dist/node/Engine.js.map +1 -1
- package/dist/node/Hash.d.ts +14 -16
- package/dist/node/Hash.d.ts.map +1 -1
- package/dist/node/Hash.js +14 -16
- package/dist/node/Hash.js.map +1 -1
- package/dist/node/Keystore.d.ts +36 -0
- package/dist/node/Keystore.d.ts.map +1 -0
- package/dist/node/Keystore.js +72 -0
- package/dist/node/Keystore.js.map +1 -0
- package/dist/node/Mnemonic.d.ts +35 -0
- package/dist/node/Mnemonic.d.ts.map +1 -0
- package/dist/node/Mnemonic.js +46 -0
- package/dist/node/Mnemonic.js.map +1 -0
- package/dist/node/P256.d.ts +32 -0
- package/dist/node/P256.d.ts.map +1 -0
- package/dist/node/P256.js +34 -0
- package/dist/node/P256.js.map +1 -0
- package/dist/node/X25519.d.ts +31 -0
- package/dist/node/X25519.d.ts.map +1 -0
- package/dist/node/X25519.js +71 -0
- package/dist/node/X25519.js.map +1 -0
- package/dist/node/index.d.ts +95 -8
- package/dist/node/index.d.ts.map +1 -1
- package/dist/node/index.js +95 -8
- package/dist/node/index.js.map +1 -1
- package/dist/wasm/Ed25519.d.ts +35 -0
- package/dist/wasm/Ed25519.d.ts.map +1 -0
- package/dist/wasm/Ed25519.js +88 -0
- package/dist/wasm/Ed25519.js.map +1 -0
- package/dist/wasm/Engine.d.ts +35 -26
- package/dist/wasm/Engine.d.ts.map +1 -1
- package/dist/wasm/Engine.js +44 -25
- package/dist/wasm/Engine.js.map +1 -1
- package/dist/wasm/Hash.d.ts +13 -19
- package/dist/wasm/Hash.d.ts.map +1 -1
- package/dist/wasm/Hash.js +21 -27
- package/dist/wasm/Hash.js.map +1 -1
- package/dist/wasm/Keystore.d.ts +37 -0
- package/dist/wasm/Keystore.d.ts.map +1 -0
- package/dist/wasm/Keystore.js +34 -0
- package/dist/wasm/Keystore.js.map +1 -0
- package/dist/wasm/Mnemonic.d.ts +33 -0
- package/dist/wasm/Mnemonic.d.ts.map +1 -0
- package/dist/wasm/Mnemonic.js +30 -0
- package/dist/wasm/Mnemonic.js.map +1 -0
- package/dist/wasm/X25519.d.ts +33 -0
- package/dist/wasm/X25519.d.ts.map +1 -0
- package/dist/wasm/X25519.js +53 -0
- package/dist/wasm/X25519.js.map +1 -0
- package/dist/wasm/index.d.ts +81 -12
- package/dist/wasm/index.d.ts.map +1 -1
- package/dist/wasm/index.js +81 -12
- package/dist/wasm/index.js.map +1 -1
- package/dist/wasm/internal/blake3.d.ts +11 -0
- package/dist/wasm/internal/blake3.d.ts.map +1 -0
- package/dist/wasm/internal/blake3.js +31 -0
- package/dist/wasm/internal/blake3.js.map +1 -0
- package/dist/wasm/internal/blake3.wasm.d.ts +3 -0
- package/dist/wasm/internal/blake3.wasm.d.ts.map +1 -0
- package/dist/wasm/internal/blake3.wasm.js +10 -0
- package/dist/wasm/internal/blake3.wasm.js.map +1 -0
- package/dist/wasm/internal/crypto25519.d.ts +16 -0
- package/dist/wasm/internal/crypto25519.d.ts.map +1 -0
- package/dist/wasm/internal/crypto25519.js +5 -0
- package/dist/wasm/internal/crypto25519.js.map +1 -0
- package/dist/wasm/internal/crypto25519.wasm.d.ts +3 -0
- package/dist/wasm/internal/crypto25519.wasm.d.ts.map +1 -0
- package/dist/wasm/internal/crypto25519.wasm.js +10 -0
- package/dist/wasm/internal/crypto25519.wasm.js.map +1 -0
- package/dist/wasm/internal/ed25519.d.ts +4 -0
- package/dist/wasm/internal/ed25519.d.ts.map +1 -0
- package/dist/wasm/internal/ed25519.js +21 -0
- package/dist/wasm/internal/ed25519.js.map +1 -0
- package/dist/wasm/internal/hashes.d.ts +12 -0
- package/dist/wasm/internal/hashes.d.ts.map +1 -0
- package/dist/wasm/internal/hashes.js +8 -0
- package/dist/wasm/internal/hashes.js.map +1 -0
- package/dist/wasm/internal/hashes.wasm.d.ts +4 -2
- package/dist/wasm/internal/hashes.wasm.d.ts.map +1 -1
- package/dist/wasm/internal/hashes.wasm.js +4 -2
- package/dist/wasm/internal/hashes.wasm.js.map +1 -1
- package/dist/wasm/internal/mnemonic.d.ts +4 -0
- package/dist/wasm/internal/mnemonic.d.ts.map +1 -0
- package/dist/wasm/internal/mnemonic.js +45 -0
- package/dist/wasm/internal/mnemonic.js.map +1 -0
- package/dist/wasm/internal/pbkdf2.d.ts +12 -0
- package/dist/wasm/internal/pbkdf2.d.ts.map +1 -0
- package/dist/wasm/internal/pbkdf2.js +51 -0
- package/dist/wasm/internal/pbkdf2.js.map +1 -0
- package/dist/wasm/internal/x25519.d.ts +4 -0
- package/dist/wasm/internal/x25519.d.ts.map +1 -0
- package/dist/wasm/internal/x25519.js +21 -0
- package/dist/wasm/internal/x25519.js.map +1 -0
- package/package.json +46 -1
- package/src/core/Engine.ts +84 -11
- package/src/core/_test/Engine.test-d.ts +56 -0
- package/src/core/_test/Engine.test.ts +95 -0
- package/src/core/_test/Hash.vectors.test.ts +8 -0
- package/src/core/internal/engine.ts +8 -8
- package/src/index.ts +5 -2
- package/src/node/Ed25519.ts +87 -0
- package/src/node/Engine.ts +79 -23
- package/src/node/Hash.ts +22 -27
- package/src/node/Keystore.ts +103 -0
- package/src/node/Mnemonic.ts +68 -0
- package/src/node/P256.ts +48 -0
- package/src/node/X25519.ts +93 -0
- package/src/node/_test/Curves.fuzz.ts +76 -0
- package/src/node/_test/Ed25519.test-d.ts +35 -0
- package/src/node/_test/Ed25519.test.ts +220 -0
- package/src/node/_test/Engine.test-d.ts +28 -0
- package/src/node/_test/Engine.test.ts +62 -22
- package/src/node/_test/Hash.test-d.ts +15 -9
- package/src/node/_test/Hash.test.ts +10 -11
- package/src/node/_test/Keystore.fuzz.ts +58 -0
- package/src/node/_test/Keystore.test-d.ts +43 -0
- package/src/node/_test/Keystore.test.ts +198 -0
- package/src/node/_test/Mnemonic.fuzz.ts +31 -0
- package/src/node/_test/Mnemonic.test-d.ts +21 -0
- package/src/node/_test/Mnemonic.test.ts +98 -0
- package/src/node/_test/P256.test-d.ts +29 -0
- package/src/node/_test/P256.test.ts +102 -0
- package/src/node/_test/X25519.test-d.ts +28 -0
- package/src/node/_test/X25519.test.ts +239 -0
- package/src/node/index.ts +100 -8
- package/src/version.ts +1 -1
- package/src/wasm/Ed25519.ts +114 -0
- package/src/wasm/Engine.ts +76 -31
- package/src/wasm/Hash.bench.ts +2 -2
- package/src/wasm/Hash.ts +29 -50
- package/src/wasm/Keystore.ts +48 -0
- package/src/wasm/Mnemonic.ts +44 -0
- package/src/wasm/X25519.ts +70 -0
- package/src/wasm/_test/Crypto25519.conformance.ts +57 -0
- package/src/wasm/_test/Crypto25519.fuzz.ts +187 -0
- package/src/wasm/_test/Ed25519.test-d.ts +28 -0
- package/src/wasm/_test/Ed25519.test.ts +168 -0
- package/src/wasm/_test/Ed25519.vectors.test.ts +73 -0
- package/src/wasm/_test/Engine.test-d.ts +28 -0
- package/src/wasm/_test/Engine.test.ts +59 -25
- package/src/wasm/_test/Hash.browser.test.ts +11 -7
- package/src/wasm/_test/Hash.fuzz.ts +58 -23
- package/src/wasm/_test/Hash.test-d.ts +15 -10
- package/src/wasm/_test/Hash.test.ts +148 -48
- package/src/wasm/_test/Hash.vectors.test.ts +15 -8
- package/src/wasm/_test/Keystore.browser.test.ts +42 -0
- package/src/wasm/_test/Keystore.fuzz.ts +113 -0
- package/src/wasm/_test/Keystore.test-d.ts +33 -0
- package/src/wasm/_test/Keystore.test.ts +192 -0
- package/src/wasm/_test/Keystore.vectors.test.ts +21 -0
- package/src/wasm/_test/Mnemonic.test-d.ts +17 -0
- package/src/wasm/_test/Mnemonic.test.ts +110 -0
- package/src/wasm/_test/X25519.test-d.ts +21 -0
- package/src/wasm/_test/X25519.test.ts +194 -0
- package/src/wasm/_test/fixtures.ts +4 -0
- package/src/wasm/index.ts +85 -12
- package/src/wasm/internal/blake3.ts +45 -0
- package/src/wasm/internal/blake3.wasm.ts +11 -0
- package/src/wasm/internal/crypto25519.ts +43 -0
- package/src/wasm/internal/crypto25519.wasm.ts +11 -0
- package/src/wasm/internal/ed25519.ts +31 -0
- package/src/wasm/internal/hashes.ts +29 -0
- package/src/wasm/internal/hashes.wasm.ts +5 -2
- package/src/wasm/internal/mnemonic.ts +56 -0
- package/src/wasm/internal/pbkdf2.ts +86 -0
- package/src/wasm/internal/x25519.ts +29 -0
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
import { fc, test } from '@fast-check/vitest'
|
|
2
|
+
import { ed25519, x25519 } from '@noble/curves/ed25519.js'
|
|
3
|
+
import { mnemonicToSeedSync } from '@scure/bip39'
|
|
4
|
+
import { beforeAll, describe, expect } from 'vp/test'
|
|
5
|
+
import { numRuns } from '../../../test/fuzz/numRuns.js'
|
|
6
|
+
import * as Ed25519 from '../Ed25519.js'
|
|
7
|
+
import * as Mnemonic from '../Mnemonic.js'
|
|
8
|
+
import * as X25519 from '../X25519.js'
|
|
9
|
+
import { x25519LowOrder } from './fixtures.js'
|
|
10
|
+
|
|
11
|
+
let ed25519Engine: Ed25519.engine.ReturnType
|
|
12
|
+
let mnemonicEngine: Mnemonic.engine.ReturnType
|
|
13
|
+
let x25519Engine: X25519.engine.ReturnType
|
|
14
|
+
|
|
15
|
+
beforeAll(async () => {
|
|
16
|
+
ed25519Engine = await Ed25519.engine()
|
|
17
|
+
mnemonicEngine = await Mnemonic.engine()
|
|
18
|
+
x25519Engine = await X25519.engine()
|
|
19
|
+
})
|
|
20
|
+
|
|
21
|
+
const arbitraryKey = fc.uint8Array({ maxLength: 32, minLength: 32 })
|
|
22
|
+
const arbitraryPayload = fc.oneof(
|
|
23
|
+
fc.uint8Array({ maxLength: 512 }),
|
|
24
|
+
fc
|
|
25
|
+
.constantFrom(0, 1, 31, 32, 63, 64, 65, 127, 128, 129, 1024, 4096)
|
|
26
|
+
.chain((size) => fc.uint8Array({ maxLength: size, minLength: size })),
|
|
27
|
+
)
|
|
28
|
+
const arbitraryWord = fc.oneof(
|
|
29
|
+
fc
|
|
30
|
+
.string({ maxLength: 16, minLength: 1 })
|
|
31
|
+
.filter((value) => !value.normalize('NFKD').includes(' ')),
|
|
32
|
+
fc.constantFrom('café', 'cafe\u0301', 'あおぞら', '㍍ガバヴァ'),
|
|
33
|
+
)
|
|
34
|
+
const arbitraryMnemonic = fc
|
|
35
|
+
.tuple(
|
|
36
|
+
fc.constantFrom(12, 15, 18, 21, 24),
|
|
37
|
+
fc.array(arbitraryWord, { maxLength: 24, minLength: 24 }),
|
|
38
|
+
)
|
|
39
|
+
.map(([count, words]) => words.slice(0, count).join(' '))
|
|
40
|
+
|
|
41
|
+
describe('Ed25519', () => {
|
|
42
|
+
test.prop(
|
|
43
|
+
{ payload: arbitraryPayload, privateKey: arbitraryKey },
|
|
44
|
+
{ numRuns },
|
|
45
|
+
)('matches the default for arbitrary subviews', (options) => {
|
|
46
|
+
const payload = offsetView(options.payload)
|
|
47
|
+
const privateKey = offsetView(options.privateKey)
|
|
48
|
+
const publicKey = ed25519Engine.getPublicKey(privateKey)
|
|
49
|
+
const signature = ed25519Engine.sign(payload, privateKey)
|
|
50
|
+
|
|
51
|
+
expect(publicKey).toEqual(ed25519.getPublicKey(privateKey))
|
|
52
|
+
expect(signature).toEqual(ed25519.sign(payload, privateKey))
|
|
53
|
+
expect(ed25519Engine.verify(signature, payload, publicKey)).toBe(true)
|
|
54
|
+
expect(
|
|
55
|
+
ed25519Engine.verify(signature, Uint8Array.of(...payload, 1), publicKey),
|
|
56
|
+
).toBe(false)
|
|
57
|
+
expect(ed25519Engine.toMontgomerySecret(privateKey)).toEqual(
|
|
58
|
+
ed25519.utils.toMontgomerySecret(privateKey),
|
|
59
|
+
)
|
|
60
|
+
})
|
|
61
|
+
|
|
62
|
+
test.prop(
|
|
63
|
+
{
|
|
64
|
+
payload: arbitraryPayload,
|
|
65
|
+
publicKey: arbitraryKey,
|
|
66
|
+
signature: fc.uint8Array({ maxLength: 64, minLength: 64 }),
|
|
67
|
+
},
|
|
68
|
+
{ numRuns },
|
|
69
|
+
)('verification matches ZIP-215 for arbitrary encodings', (options) => {
|
|
70
|
+
expect(
|
|
71
|
+
ed25519Engine.verify(
|
|
72
|
+
options.signature,
|
|
73
|
+
options.payload,
|
|
74
|
+
options.publicKey,
|
|
75
|
+
),
|
|
76
|
+
).toBe(
|
|
77
|
+
ed25519.verify(options.signature, options.payload, options.publicKey),
|
|
78
|
+
)
|
|
79
|
+
})
|
|
80
|
+
})
|
|
81
|
+
|
|
82
|
+
describe('X25519', () => {
|
|
83
|
+
test.prop(
|
|
84
|
+
{ privateKey: arbitraryKey, publicKeySeed: arbitraryKey },
|
|
85
|
+
{ numRuns },
|
|
86
|
+
)('matches the default for arbitrary subviews', (options) => {
|
|
87
|
+
const privateKey = offsetView(options.privateKey)
|
|
88
|
+
const publicKey = offsetView(x25519.getPublicKey(options.publicKeySeed))
|
|
89
|
+
|
|
90
|
+
expect(x25519Engine.getPublicKey(privateKey)).toEqual(
|
|
91
|
+
x25519.getPublicKey(privateKey),
|
|
92
|
+
)
|
|
93
|
+
expect(x25519Engine.getSharedSecret(privateKey, publicKey)).toEqual(
|
|
94
|
+
x25519.getSharedSecret(privateKey, publicKey),
|
|
95
|
+
)
|
|
96
|
+
})
|
|
97
|
+
|
|
98
|
+
test.prop(
|
|
99
|
+
{
|
|
100
|
+
highBit: fc.boolean(),
|
|
101
|
+
privateKey: arbitraryKey,
|
|
102
|
+
vector: fc.constantFrom(...x25519LowOrder),
|
|
103
|
+
},
|
|
104
|
+
{ numRuns },
|
|
105
|
+
)(
|
|
106
|
+
'rejects arbitrary private keys paired with low-order points',
|
|
107
|
+
(options) => {
|
|
108
|
+
const publicKey = fromHex(options.vector.publicKey)
|
|
109
|
+
if (options.highBit) publicKey[31]! |= 0x80
|
|
110
|
+
expect(() =>
|
|
111
|
+
x25519Engine.getSharedSecret(options.privateKey, publicKey),
|
|
112
|
+
).toThrow()
|
|
113
|
+
},
|
|
114
|
+
)
|
|
115
|
+
})
|
|
116
|
+
|
|
117
|
+
describe('Mnemonic', () => {
|
|
118
|
+
test.prop(
|
|
119
|
+
{
|
|
120
|
+
mnemonic: arbitraryMnemonic,
|
|
121
|
+
passphrase: fc.string({ maxLength: 64 }),
|
|
122
|
+
},
|
|
123
|
+
{ numRuns },
|
|
124
|
+
)('matches the default for arbitrary normalized text', (options) => {
|
|
125
|
+
expect(mnemonicEngine.toSeed(options.mnemonic, options.passphrase)).toEqual(
|
|
126
|
+
mnemonicToSeedSync(options.mnemonic, options.passphrase),
|
|
127
|
+
)
|
|
128
|
+
})
|
|
129
|
+
})
|
|
130
|
+
|
|
131
|
+
describe('memory', () => {
|
|
132
|
+
test.prop(
|
|
133
|
+
{
|
|
134
|
+
operations: fc.array(
|
|
135
|
+
fc.record({
|
|
136
|
+
payload: arbitraryPayload,
|
|
137
|
+
privateKey: arbitraryKey,
|
|
138
|
+
publicKeySeed: arbitraryKey,
|
|
139
|
+
}),
|
|
140
|
+
{ maxLength: 8, minLength: 2 },
|
|
141
|
+
),
|
|
142
|
+
},
|
|
143
|
+
{ numRuns },
|
|
144
|
+
)(
|
|
145
|
+
'interleaved providers remain isolated and outputs stay owned',
|
|
146
|
+
(options) => {
|
|
147
|
+
const held: Uint8Array[] = []
|
|
148
|
+
const snapshots: Uint8Array[] = []
|
|
149
|
+
|
|
150
|
+
for (const operation of options.operations) {
|
|
151
|
+
const edPublicKey = ed25519Engine.getPublicKey(operation.privateKey)
|
|
152
|
+
const signature = ed25519Engine.sign(
|
|
153
|
+
operation.payload,
|
|
154
|
+
operation.privateKey,
|
|
155
|
+
)
|
|
156
|
+
const xPublicKey = x25519.getPublicKey(operation.publicKeySeed)
|
|
157
|
+
const sharedSecret = x25519Engine.getSharedSecret(
|
|
158
|
+
operation.privateKey,
|
|
159
|
+
xPublicKey,
|
|
160
|
+
)
|
|
161
|
+
expect(
|
|
162
|
+
ed25519Engine.verify(signature, operation.payload, edPublicKey),
|
|
163
|
+
).toBe(true)
|
|
164
|
+
expect(sharedSecret).toEqual(
|
|
165
|
+
x25519.getSharedSecret(operation.privateKey, xPublicKey),
|
|
166
|
+
)
|
|
167
|
+
held.push(signature, sharedSecret)
|
|
168
|
+
snapshots.push(signature.slice(), sharedSecret.slice())
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
expect(held).toEqual(snapshots)
|
|
172
|
+
},
|
|
173
|
+
)
|
|
174
|
+
})
|
|
175
|
+
|
|
176
|
+
function offsetView(value: Uint8Array): Uint8Array {
|
|
177
|
+
const bytes = new Uint8Array(value.length + 4).fill(0xff)
|
|
178
|
+
bytes.set(value, 2)
|
|
179
|
+
return bytes.subarray(2, -2)
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
function fromHex(value: string): Uint8Array {
|
|
183
|
+
const bytes = new Uint8Array(value.length / 2)
|
|
184
|
+
for (let index = 0; index < bytes.length; index++)
|
|
185
|
+
bytes[index] = Number.parseInt(value.slice(index * 2, index * 2 + 2), 16)
|
|
186
|
+
return bytes
|
|
187
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { expectTypeOf, test } from 'vp/test'
|
|
2
|
+
import type * as Engine from '../../core/Engine.js'
|
|
3
|
+
import * as core_Ed25519 from '../../core/Ed25519.js'
|
|
4
|
+
import * as Ed25519 from '../Ed25519.js'
|
|
5
|
+
|
|
6
|
+
type Slot = Awaited<ReturnType<typeof Ed25519.engine>>
|
|
7
|
+
|
|
8
|
+
test('return type exposes only implemented Ed25519 primitives', () => {
|
|
9
|
+
expectTypeOf<Slot['getPublicKey']>().toEqualTypeOf<
|
|
10
|
+
NonNullable<Engine.Eddsa['getPublicKey']>
|
|
11
|
+
>()
|
|
12
|
+
expectTypeOf<Slot['sign']>().toEqualTypeOf<
|
|
13
|
+
NonNullable<Engine.Eddsa['sign']>
|
|
14
|
+
>()
|
|
15
|
+
expectTypeOf<Slot['toMontgomerySecret']>().toEqualTypeOf<
|
|
16
|
+
NonNullable<Engine.Eddsa['toMontgomerySecret']>
|
|
17
|
+
>()
|
|
18
|
+
expectTypeOf<Slot['verify']>().toEqualTypeOf<
|
|
19
|
+
NonNullable<Engine.Eddsa['verify']>
|
|
20
|
+
>()
|
|
21
|
+
expectTypeOf<Slot>().not.toHaveProperty('randomSecretKey')
|
|
22
|
+
expectTypeOf<Slot>().not.toHaveProperty('toMontgomery')
|
|
23
|
+
})
|
|
24
|
+
|
|
25
|
+
test('the WASM namespace exposes the public Ed25519 API', () => {
|
|
26
|
+
expectTypeOf(Ed25519.getPublicKey).toEqualTypeOf(core_Ed25519.getPublicKey)
|
|
27
|
+
expectTypeOf<typeof Ed25519>().not.toHaveProperty('create')
|
|
28
|
+
})
|
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
import { ed25519 } from '@noble/curves/ed25519.js'
|
|
2
|
+
import { describe, expect, test } from 'vp/test'
|
|
3
|
+
import * as Ed25519 from '../Ed25519.js'
|
|
4
|
+
import * as crypto25519 from '../internal/crypto25519.js'
|
|
5
|
+
import { wasmBase64 } from '../internal/crypto25519.wasm.js'
|
|
6
|
+
import * as ed25519_internal from '../internal/ed25519.js'
|
|
7
|
+
import * as internal from '../internal/instantiate.js'
|
|
8
|
+
|
|
9
|
+
const privateKey = fromHex(
|
|
10
|
+
'9d61b19deffd5a60ba844af492ec2cc44449c5697b326919703bac031cae7f60',
|
|
11
|
+
)
|
|
12
|
+
|
|
13
|
+
describe('engine', () => {
|
|
14
|
+
test('behavior: exposes only semantics-compatible primitives', async () => {
|
|
15
|
+
const engine = await Ed25519.engine()
|
|
16
|
+
|
|
17
|
+
expect(Object.keys(engine).sort()).toMatchInlineSnapshot(`
|
|
18
|
+
[
|
|
19
|
+
"getPublicKey",
|
|
20
|
+
"sign",
|
|
21
|
+
"toMontgomerySecret",
|
|
22
|
+
"verify",
|
|
23
|
+
]
|
|
24
|
+
`)
|
|
25
|
+
})
|
|
26
|
+
|
|
27
|
+
test('behavior: signs and verifies boundary-sized messages', async () => {
|
|
28
|
+
const engine = await Ed25519.engine()
|
|
29
|
+
|
|
30
|
+
for (const size of [0, 1, 31, 32, 63, 64, 65, 127, 128, 129, 1024]) {
|
|
31
|
+
const payload = Uint8Array.from(
|
|
32
|
+
{ length: size },
|
|
33
|
+
(_, index) => (index * 29 + size) % 251,
|
|
34
|
+
)
|
|
35
|
+
const signature = engine.sign(payload, privateKey)
|
|
36
|
+
const publicKey = engine.getPublicKey(privateKey)
|
|
37
|
+
|
|
38
|
+
expect(signature).toEqual(ed25519.sign(payload, privateKey))
|
|
39
|
+
expect(engine.verify(signature, payload, publicKey)).toBe(true)
|
|
40
|
+
expect(
|
|
41
|
+
engine.verify(signature, Uint8Array.of(...payload, 1), publicKey),
|
|
42
|
+
).toBe(false)
|
|
43
|
+
}
|
|
44
|
+
})
|
|
45
|
+
|
|
46
|
+
test('behavior: matches private-key conversion', async () => {
|
|
47
|
+
const engine = await Ed25519.engine()
|
|
48
|
+
|
|
49
|
+
expect(engine.toMontgomerySecret(privateKey)).toEqual(
|
|
50
|
+
ed25519.utils.toMontgomerySecret(privateKey),
|
|
51
|
+
)
|
|
52
|
+
})
|
|
53
|
+
|
|
54
|
+
test('behavior: respects subviews and leaves inputs immutable', async () => {
|
|
55
|
+
const engine = await Ed25519.engine()
|
|
56
|
+
const key = offsetView(privateKey)
|
|
57
|
+
const payload = offsetView(new Uint8Array(257).fill(0xa5))
|
|
58
|
+
const keyBefore = key.slice()
|
|
59
|
+
const payloadBefore = payload.slice()
|
|
60
|
+
const publicKey = offsetView(engine.getPublicKey(key))
|
|
61
|
+
const signature = offsetView(engine.sign(payload, key))
|
|
62
|
+
const publicKeyBefore = publicKey.slice()
|
|
63
|
+
const signatureBefore = signature.slice()
|
|
64
|
+
|
|
65
|
+
expect(engine.getPublicKey(key)).toEqual(ed25519.getPublicKey(key))
|
|
66
|
+
expect(engine.verify(signature, payload, publicKey)).toBe(true)
|
|
67
|
+
expect(engine.toMontgomerySecret(key)).toEqual(
|
|
68
|
+
ed25519.utils.toMontgomerySecret(key),
|
|
69
|
+
)
|
|
70
|
+
expect({ key, payload, publicKey, signature }).toEqual({
|
|
71
|
+
key: keyBefore,
|
|
72
|
+
payload: payloadBefore,
|
|
73
|
+
publicKey: publicKeyBefore,
|
|
74
|
+
signature: signatureBefore,
|
|
75
|
+
})
|
|
76
|
+
})
|
|
77
|
+
|
|
78
|
+
test('behavior: rejects malformed key and signature lengths', async () => {
|
|
79
|
+
const engine = await Ed25519.engine()
|
|
80
|
+
const publicKey = engine.getPublicKey(privateKey)
|
|
81
|
+
|
|
82
|
+
for (const size of [0, 1, 31, 33, 63, 64]) {
|
|
83
|
+
if (size !== 32) {
|
|
84
|
+
expect(() => engine.getPublicKey(new Uint8Array(size))).toThrowError(
|
|
85
|
+
`Ed25519 private key must be 32 bytes, got ${size}`,
|
|
86
|
+
)
|
|
87
|
+
expect(() =>
|
|
88
|
+
engine.sign(new Uint8Array(), new Uint8Array(size)),
|
|
89
|
+
).toThrowError(`Ed25519 private key must be 32 bytes, got ${size}`)
|
|
90
|
+
}
|
|
91
|
+
if (size !== 64)
|
|
92
|
+
expect(() =>
|
|
93
|
+
engine.verify(new Uint8Array(size), new Uint8Array(), publicKey),
|
|
94
|
+
).toThrowError(`Ed25519 signature must be 64 bytes, got ${size}`)
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
expect(() =>
|
|
98
|
+
engine.verify(new Uint8Array(64), new Uint8Array(), new Uint8Array(31)),
|
|
99
|
+
).toThrowError('Ed25519 public key must be 32 bytes, got 31')
|
|
100
|
+
})
|
|
101
|
+
|
|
102
|
+
test('behavior: outputs are owned across calls and memory growth', async () => {
|
|
103
|
+
const engine = await Ed25519.engine()
|
|
104
|
+
const publicKey = engine.getPublicKey(privateKey)
|
|
105
|
+
const signature = engine.sign(new Uint8Array(), privateKey)
|
|
106
|
+
const snapshots = [publicKey.slice(), signature.slice()]
|
|
107
|
+
|
|
108
|
+
engine.sign(new Uint8Array(1024 * 1024).fill(0x5a), privateKey)
|
|
109
|
+
|
|
110
|
+
expect([publicKey, signature]).toEqual(snapshots)
|
|
111
|
+
expect(engine.getPublicKey(privateKey) === publicKey).toBe(false)
|
|
112
|
+
})
|
|
113
|
+
|
|
114
|
+
test('behavior: clears the shared staging region', async () => {
|
|
115
|
+
const engine = await Ed25519.engine()
|
|
116
|
+
const payload = new Uint8Array(1024).fill(0xa5)
|
|
117
|
+
engine.sign(payload, privateKey)
|
|
118
|
+
|
|
119
|
+
const module = await crypto25519.load()
|
|
120
|
+
const size = 32 + payload.length + 64
|
|
121
|
+
expect(
|
|
122
|
+
module
|
|
123
|
+
.view()
|
|
124
|
+
.subarray(module.heapBase, module.heapBase + size)
|
|
125
|
+
.every((byte) => byte === 0),
|
|
126
|
+
).toBe(true)
|
|
127
|
+
})
|
|
128
|
+
|
|
129
|
+
test('behavior: clears the complete region after a late trap', async () => {
|
|
130
|
+
const module = await internal.instantiate<crypto25519.Exports>(wasmBase64)
|
|
131
|
+
const payload = new Uint8Array(1024).fill(0xa5)
|
|
132
|
+
const exports: crypto25519.Exports = {
|
|
133
|
+
...module.exports,
|
|
134
|
+
ed25519_sign(...parameters) {
|
|
135
|
+
module.exports.ed25519_sign(...parameters)
|
|
136
|
+
throw new WebAssembly.RuntimeError('forced late trap')
|
|
137
|
+
},
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
expect(() =>
|
|
141
|
+
ed25519_internal.sign({ ...module, exports }, payload, privateKey),
|
|
142
|
+
).toThrow(WebAssembly.RuntimeError)
|
|
143
|
+
expect(
|
|
144
|
+
module
|
|
145
|
+
.view()
|
|
146
|
+
.subarray(module.heapBase, module.heapBase + 32 + payload.length + 64)
|
|
147
|
+
.every((byte) => byte === 0),
|
|
148
|
+
).toBe(true)
|
|
149
|
+
})
|
|
150
|
+
|
|
151
|
+
test('behavior: rejects a workspace beyond wasm32 without growing', async () => {
|
|
152
|
+
const module = await internal.instantiate<crypto25519.Exports>(wasmBase64)
|
|
153
|
+
const memory = module.view().length
|
|
154
|
+
|
|
155
|
+
expect(() => module.reserve(0x1_0000_0000)).toThrow(internal.MemoryError)
|
|
156
|
+
expect(module.view()).toHaveLength(memory)
|
|
157
|
+
})
|
|
158
|
+
})
|
|
159
|
+
|
|
160
|
+
function fromHex(value: string): Uint8Array {
|
|
161
|
+
return Uint8Array.from(Buffer.from(value, 'hex'))
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
function offsetView(value: Uint8Array): Uint8Array {
|
|
165
|
+
const bytes = new Uint8Array(value.length + 4).fill(0xff)
|
|
166
|
+
bytes.set(value, 2)
|
|
167
|
+
return bytes.subarray(2, -2)
|
|
168
|
+
}
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import * as fs from 'node:fs'
|
|
2
|
+
import { describe, expect, test } from 'vp/test'
|
|
3
|
+
import * as Ed25519 from '../Ed25519.js'
|
|
4
|
+
|
|
5
|
+
type Zip215Vector = {
|
|
6
|
+
sig_bytes: string
|
|
7
|
+
valid_legacy: boolean
|
|
8
|
+
valid_zip215: boolean
|
|
9
|
+
vk_bytes: string
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
const zip215 = JSON.parse(
|
|
13
|
+
fs.readFileSync(
|
|
14
|
+
new URL('../../../test/vectors/ed25519/zip215.json', import.meta.url),
|
|
15
|
+
'utf8',
|
|
16
|
+
),
|
|
17
|
+
) as readonly Zip215Vector[]
|
|
18
|
+
|
|
19
|
+
const rfc8032 = [
|
|
20
|
+
{
|
|
21
|
+
message: '',
|
|
22
|
+
publicKey:
|
|
23
|
+
'd75a980182b10ab7d54bfed3c964073a0ee172f3daa62325af021a68f707511a',
|
|
24
|
+
seed: '9d61b19deffd5a60ba844af492ec2cc44449c5697b326919703bac031cae7f60',
|
|
25
|
+
signature:
|
|
26
|
+
'e5564300c360ac729086e2cc806e828a84877f1eb8e5d974d873e065224901555fb8821590a33bacc61e39701cf9b46bd25bf5f0595bbe24655141438e7a100b',
|
|
27
|
+
},
|
|
28
|
+
{
|
|
29
|
+
message: '72',
|
|
30
|
+
publicKey:
|
|
31
|
+
'3d4017c3e843895a92b70aa74d1b7ebc9c982ccf2ec4968cc0cd55f12af4660c',
|
|
32
|
+
seed: '4ccd089b28ff96da9db6c346ec114e0f5b8a319f35aba624da8cf6ed4fb8a6fb',
|
|
33
|
+
signature:
|
|
34
|
+
'92a009a9f0d4cab8720e820b5f642540a2b27b5416503f8fb3762223ebdb69da085ac1e43e15996e458f3613d0f11d8c387b2eaeb4302aeeb00d291612bb0c00',
|
|
35
|
+
},
|
|
36
|
+
] as const
|
|
37
|
+
|
|
38
|
+
describe('engine', () => {
|
|
39
|
+
test('vectors: matches RFC 8032 key, signature, and verification cases', async () => {
|
|
40
|
+
const engine = await Ed25519.engine()
|
|
41
|
+
|
|
42
|
+
for (const vector of rfc8032) {
|
|
43
|
+
const seed = fromHex(vector.seed)
|
|
44
|
+
const message = fromHex(vector.message)
|
|
45
|
+
const publicKey = fromHex(vector.publicKey)
|
|
46
|
+
const signature = fromHex(vector.signature)
|
|
47
|
+
|
|
48
|
+
expect(engine.getPublicKey(seed)).toEqual(publicKey)
|
|
49
|
+
expect(engine.sign(message, seed)).toEqual(signature)
|
|
50
|
+
expect(engine.verify(signature, message, publicKey)).toBe(true)
|
|
51
|
+
}
|
|
52
|
+
})
|
|
53
|
+
|
|
54
|
+
test('vectors: matches all 196 ZIP-215 compliance cases', async () => {
|
|
55
|
+
const engine = await Ed25519.engine()
|
|
56
|
+
const message = new TextEncoder().encode('Zcash')
|
|
57
|
+
|
|
58
|
+
expect(zip215).toHaveLength(196)
|
|
59
|
+
for (const vector of zip215)
|
|
60
|
+
expect(
|
|
61
|
+
engine.verify(
|
|
62
|
+
fromHex(vector.sig_bytes),
|
|
63
|
+
message,
|
|
64
|
+
fromHex(vector.vk_bytes),
|
|
65
|
+
),
|
|
66
|
+
JSON.stringify(vector),
|
|
67
|
+
).toBe(vector.valid_zip215)
|
|
68
|
+
})
|
|
69
|
+
})
|
|
70
|
+
|
|
71
|
+
function fromHex(value: string): Uint8Array {
|
|
72
|
+
return Uint8Array.from(Buffer.from(value, 'hex'))
|
|
73
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { Engine } from 'ox/wasm'
|
|
2
|
+
import { expectTypeOf, test } from 'vp/test'
|
|
3
|
+
import * as CoreEngine from '../../core/Engine.js'
|
|
4
|
+
|
|
5
|
+
type Installed = Awaited<ReturnType<typeof Engine.install>>
|
|
6
|
+
type Uninstalled = Awaited<ReturnType<typeof Engine.engine>>
|
|
7
|
+
|
|
8
|
+
test('install and engine expose the same precise engine', () => {
|
|
9
|
+
expectTypeOf<Installed>().toEqualTypeOf<Uninstalled>()
|
|
10
|
+
expectTypeOf<Installed['Hash']['sha256']>().toEqualTypeOf<
|
|
11
|
+
(input: Uint8Array) => Uint8Array
|
|
12
|
+
>()
|
|
13
|
+
})
|
|
14
|
+
|
|
15
|
+
test('the WASM Engine namespace exposes synchronous core operations', () => {
|
|
16
|
+
expectTypeOf(Engine.get).toEqualTypeOf(CoreEngine.get)
|
|
17
|
+
expectTypeOf(Engine.InvalidSlotValueError).toEqualTypeOf(
|
|
18
|
+
CoreEngine.InvalidSlotValueError,
|
|
19
|
+
)
|
|
20
|
+
expectTypeOf(Engine.reset).toEqualTypeOf(CoreEngine.reset)
|
|
21
|
+
expectTypeOf(Engine.set).toEqualTypeOf(CoreEngine.set)
|
|
22
|
+
expectTypeOf(Engine.with).toEqualTypeOf(CoreEngine.with)
|
|
23
|
+
})
|
|
24
|
+
|
|
25
|
+
test('the replaced aliases are absent', () => {
|
|
26
|
+
expectTypeOf<typeof Engine>().not.toHaveProperty('create')
|
|
27
|
+
expectTypeOf<typeof Engine>().not.toHaveProperty('load')
|
|
28
|
+
})
|
|
@@ -1,62 +1,96 @@
|
|
|
1
|
-
import { Engine as CoreEngine
|
|
2
|
-
import { Engine } from 'ox/wasm'
|
|
1
|
+
import { Engine as CoreEngine } from 'ox'
|
|
2
|
+
import { Engine, Hash } from 'ox/wasm'
|
|
3
3
|
import { describe, expect, test } from 'vp/test'
|
|
4
4
|
|
|
5
|
-
describe('
|
|
5
|
+
describe('engine', () => {
|
|
6
6
|
test('behavior: does not install', async () => {
|
|
7
|
-
const engine = await Engine.
|
|
7
|
+
const engine = await Engine.engine()
|
|
8
8
|
expect(Object.keys(engine)).toMatchInlineSnapshot(`
|
|
9
9
|
[
|
|
10
|
+
"Ed25519",
|
|
10
11
|
"Hash",
|
|
12
|
+
"Keystore",
|
|
13
|
+
"Mnemonic",
|
|
14
|
+
"X25519",
|
|
11
15
|
]
|
|
12
16
|
`)
|
|
13
|
-
//
|
|
14
|
-
//
|
|
17
|
+
// Benches, differential tests, and composition need an implementation
|
|
18
|
+
// value without touching global state.
|
|
15
19
|
expect(CoreEngine.get()).toMatchInlineSnapshot('{}')
|
|
16
20
|
})
|
|
17
21
|
|
|
18
22
|
test('behavior: installs for the duration of a call', async () => {
|
|
19
23
|
// `with` is the case that genuinely needs a value: there is nothing
|
|
20
24
|
// installed for `set` to merge into.
|
|
21
|
-
const wasm = await Engine.
|
|
22
|
-
const inside = CoreEngine.with(wasm, () => Hash.
|
|
23
|
-
expect(inside).toEqual(Hash.
|
|
25
|
+
const wasm = await Engine.engine()
|
|
26
|
+
const inside = CoreEngine.with(wasm, () => Hash.sha256('0xdeadbeef'))
|
|
27
|
+
expect(inside).toEqual(Hash.sha256('0xdeadbeef'))
|
|
24
28
|
expect(CoreEngine.get()).toMatchInlineSnapshot('{}')
|
|
25
29
|
})
|
|
26
30
|
})
|
|
27
31
|
|
|
28
|
-
describe('
|
|
32
|
+
describe('install', () => {
|
|
29
33
|
test('behavior: merges with a later set, rather than being replaced', async () => {
|
|
30
|
-
await Engine.
|
|
31
|
-
CoreEngine.set({
|
|
34
|
+
await Engine.install()
|
|
35
|
+
CoreEngine.set({ Bls: { randomSecretKey: () => new Uint8Array(32) } })
|
|
32
36
|
expect(Object.keys(CoreEngine.get()).sort()).toMatchInlineSnapshot(`
|
|
33
37
|
[
|
|
38
|
+
"Bls",
|
|
39
|
+
"Ed25519",
|
|
34
40
|
"Hash",
|
|
41
|
+
"Keystore",
|
|
35
42
|
"Mnemonic",
|
|
43
|
+
"X25519",
|
|
36
44
|
]
|
|
37
45
|
`)
|
|
38
46
|
})
|
|
39
47
|
|
|
40
48
|
test('behavior: installs, and returns what it installed', async () => {
|
|
41
|
-
const before = Hash.
|
|
49
|
+
const before = Hash.sha256('0xdeadbeef')
|
|
42
50
|
|
|
43
|
-
const engine = await Engine.
|
|
51
|
+
const engine = await Engine.install()
|
|
44
52
|
|
|
45
53
|
expect(Object.keys(CoreEngine.get())).toEqual(Object.keys(engine))
|
|
46
54
|
// Installing changes which implementation runs, never the answer.
|
|
47
|
-
expect(Hash.
|
|
55
|
+
expect(Hash.sha256('0xdeadbeef')).toEqual(before)
|
|
56
|
+
expect(Engine.get()).toEqual(CoreEngine.get())
|
|
48
57
|
})
|
|
49
58
|
|
|
50
59
|
test('behavior: routes every slot it reports', async () => {
|
|
51
|
-
await Engine.
|
|
52
|
-
expect(
|
|
53
|
-
.
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
60
|
+
await Engine.install()
|
|
61
|
+
expect(
|
|
62
|
+
Object.fromEntries(
|
|
63
|
+
Object.entries(CoreEngine.get()).map(([slot, value]) => [
|
|
64
|
+
slot,
|
|
65
|
+
Object.keys(value ?? {}).sort(),
|
|
66
|
+
]),
|
|
67
|
+
),
|
|
68
|
+
).toMatchInlineSnapshot(`
|
|
69
|
+
{
|
|
70
|
+
"Ed25519": [
|
|
71
|
+
"getPublicKey",
|
|
72
|
+
"sign",
|
|
73
|
+
"toMontgomerySecret",
|
|
74
|
+
"verify",
|
|
75
|
+
],
|
|
76
|
+
"Hash": [
|
|
77
|
+
"blake3",
|
|
78
|
+
"hmacSha256",
|
|
79
|
+
"keccak256",
|
|
80
|
+
"ripemd160",
|
|
81
|
+
"sha256",
|
|
82
|
+
],
|
|
83
|
+
"Keystore": [
|
|
84
|
+
"pbkdf2Sha256",
|
|
85
|
+
],
|
|
86
|
+
"Mnemonic": [
|
|
87
|
+
"toSeed",
|
|
88
|
+
],
|
|
89
|
+
"X25519": [
|
|
90
|
+
"getPublicKey",
|
|
91
|
+
"getSharedSecret",
|
|
92
|
+
],
|
|
93
|
+
}
|
|
94
|
+
`)
|
|
61
95
|
})
|
|
62
96
|
})
|
|
@@ -1,10 +1,11 @@
|
|
|
1
|
+
import { blake3 } from '@noble/hashes/blake3.js'
|
|
1
2
|
import { keccak_256 } from '@noble/hashes/sha3.js'
|
|
2
3
|
import { describe, expect, test, vi } from 'vp/test'
|
|
3
4
|
import * as Engine from '../../core/Engine.js'
|
|
4
5
|
import * as Hash from '../../core/Hash.js'
|
|
5
6
|
import * as WasmHash from '../Hash.js'
|
|
6
7
|
|
|
7
|
-
describe('
|
|
8
|
+
describe('engine', () => {
|
|
8
9
|
test('behavior: instantiates asynchronously', async () => {
|
|
9
10
|
// Chrome refuses to compile modules above a few kilobytes synchronously on
|
|
10
11
|
// the main thread, so `new WebAssembly.Module` must never be reached. A spy
|
|
@@ -13,12 +14,13 @@ describe('create', () => {
|
|
|
13
14
|
const Module = vi.spyOn(globalThis.WebAssembly, 'Module')
|
|
14
15
|
const Instance = vi.spyOn(globalThis.WebAssembly, 'Instance')
|
|
15
16
|
try {
|
|
16
|
-
const engine = await WasmHash.
|
|
17
|
+
const engine = await WasmHash.engine()
|
|
17
18
|
expect(Module).not.toHaveBeenCalled()
|
|
18
19
|
expect(Instance).not.toHaveBeenCalled()
|
|
19
20
|
|
|
20
21
|
const input = new Uint8Array(32).fill(1)
|
|
21
|
-
expect(engine.
|
|
22
|
+
expect(engine.keccak256(input)).toEqual(keccak_256(input))
|
|
23
|
+
expect(engine.blake3(input)).toEqual(blake3(input))
|
|
22
24
|
} finally {
|
|
23
25
|
Module.mockRestore()
|
|
24
26
|
Instance.mockRestore()
|
|
@@ -26,19 +28,21 @@ describe('create', () => {
|
|
|
26
28
|
})
|
|
27
29
|
|
|
28
30
|
test('behavior: grows memory in a real browser, then hashes a small input', async () => {
|
|
29
|
-
const engine = await WasmHash.
|
|
31
|
+
const engine = await WasmHash.engine()
|
|
30
32
|
|
|
31
33
|
const large = new Uint8Array(3 * 1024 * 1024).fill(7)
|
|
32
|
-
expect(engine.
|
|
34
|
+
expect(engine.keccak256(large)).toEqual(keccak_256(large))
|
|
35
|
+
expect(engine.blake3(large)).toEqual(blake3(large))
|
|
33
36
|
|
|
34
37
|
// `memory.grow` detached the previous `ArrayBuffer`; a retained view would
|
|
35
38
|
// now read zero bytes.
|
|
36
39
|
const small = new Uint8Array(32).fill(1)
|
|
37
|
-
expect(engine.
|
|
40
|
+
expect(engine.keccak256(small)).toEqual(keccak_256(small))
|
|
41
|
+
expect(engine.blake3(small)).toEqual(blake3(small))
|
|
38
42
|
})
|
|
39
43
|
|
|
40
44
|
test('behavior: ox uses the WASM implementation once installed', async () => {
|
|
41
|
-
Engine.
|
|
45
|
+
await Engine.install({ Hash: WasmHash.engine() })
|
|
42
46
|
try {
|
|
43
47
|
expect(Hash.keccak256('0xdeadbeef')).toBe(
|
|
44
48
|
'0xd4fd4e189132273036449fc9e11198c739161b4c0116a9a2dccdfa1c492006f1',
|