ox 1.4.0 → 1.6.0
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 +12 -0
- package/dist/core/Engine.d.ts +1 -1
- package/dist/core/Engine.d.ts.map +1 -1
- package/dist/core/Engine.js.map +1 -1
- package/dist/core/MlDsa44.d.ts +389 -0
- package/dist/core/MlDsa44.d.ts.map +1 -0
- package/dist/core/MlDsa44.js +206 -0
- package/dist/core/MlDsa44.js.map +1 -0
- package/dist/core/internal/engine.d.ts +35 -1
- package/dist/core/internal/engine.d.ts.map +1 -1
- package/dist/core/internal/engine.js +2 -0
- package/dist/core/internal/engine.js.map +1 -1
- package/dist/core/internal/mlDsa44.d.ts +10 -0
- package/dist/core/internal/mlDsa44.d.ts.map +1 -0
- package/dist/core/internal/mlDsa44.js +41 -0
- package/dist/core/internal/mlDsa44.js.map +1 -0
- package/dist/index.d.ts +39 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +39 -0
- package/dist/index.js.map +1 -1
- package/dist/tempo/WithdrawalSenderTag.d.ts +36 -0
- package/dist/tempo/WithdrawalSenderTag.d.ts.map +1 -0
- package/dist/tempo/WithdrawalSenderTag.js +31 -0
- package/dist/tempo/WithdrawalSenderTag.js.map +1 -0
- package/dist/tempo/index.d.ts +23 -0
- package/dist/tempo/index.d.ts.map +1 -1
- package/dist/tempo/index.js +23 -0
- package/dist/tempo/index.js.map +1 -1
- package/dist/wasm/Engine.d.ts +5 -1
- package/dist/wasm/Engine.d.ts.map +1 -1
- package/dist/wasm/Engine.js +9 -1
- package/dist/wasm/Engine.js.map +1 -1
- package/dist/wasm/MlDsa44.d.ts +55 -0
- package/dist/wasm/MlDsa44.d.ts.map +1 -0
- package/dist/wasm/MlDsa44.js +143 -0
- package/dist/wasm/MlDsa44.js.map +1 -0
- package/dist/wasm/index.d.ts +23 -2
- package/dist/wasm/index.d.ts.map +1 -1
- package/dist/wasm/index.js +23 -2
- package/dist/wasm/index.js.map +1 -1
- package/dist/wasm/internal/mldsa44.d.ts +12 -0
- package/dist/wasm/internal/mldsa44.d.ts.map +1 -0
- package/dist/wasm/internal/mldsa44.js +5 -0
- package/dist/wasm/internal/mldsa44.js.map +1 -0
- package/dist/wasm/internal/mldsa44.wasm.d.ts +3 -0
- package/dist/wasm/internal/mldsa44.wasm.d.ts.map +1 -0
- package/dist/wasm/internal/mldsa44.wasm.js +10 -0
- package/dist/wasm/internal/mldsa44.wasm.js.map +1 -0
- package/dist/zod/TypedData.d.ts +2 -2
- package/package.json +19 -2
- package/src/core/Engine.ts +1 -0
- package/src/core/MlDsa44.ts +415 -0
- package/src/core/_test/Engine.test.ts +2 -2
- package/src/core/_test/MlDsa44.test-d.ts +56 -0
- package/src/core/_test/MlDsa44.test.ts +393 -0
- package/src/core/_test/index.test.ts +1 -0
- package/src/core/internal/engine.ts +49 -0
- package/src/core/internal/mlDsa44.ts +74 -0
- package/src/index.ts +41 -0
- package/src/tempo/WithdrawalSenderTag.test-d.ts +13 -0
- package/src/tempo/WithdrawalSenderTag.test.ts +22 -0
- package/src/tempo/WithdrawalSenderTag.ts +47 -0
- package/src/tempo/index.ts +24 -0
- package/src/version.ts +1 -1
- package/src/wasm/Engine.ts +19 -7
- package/src/wasm/MlDsa44.ts +190 -0
- package/src/wasm/_test/Engine.test-d.ts +2 -0
- package/src/wasm/_test/Engine.test.ts +16 -0
- package/src/wasm/_test/MlDsa44.test-d.ts +28 -0
- package/src/wasm/_test/MlDsa44.test.ts +192 -0
- package/src/wasm/index.ts +24 -2
- package/src/wasm/internal/mldsa44.ts +34 -0
- package/src/wasm/internal/mldsa44.wasm.ts +11 -0
- package/wasm/vendor/mldsa-native/LICENSE +286 -0
|
@@ -0,0 +1,393 @@
|
|
|
1
|
+
import { Bytes, Engine, Hash, Hex, MlDsa44 } from 'ox'
|
|
2
|
+
import { describe, expect, test } from 'vp/test'
|
|
3
|
+
|
|
4
|
+
const privateKey = Hex.fromBytes(new Uint8Array(32).fill(7))
|
|
5
|
+
const payload = '0xdeadbeef'
|
|
6
|
+
|
|
7
|
+
describe('createKeyPair', () => {
|
|
8
|
+
test('default', () => {
|
|
9
|
+
const keyPair = MlDsa44.createKeyPair()
|
|
10
|
+
|
|
11
|
+
expect(keyPair).toHaveProperty('privateKey')
|
|
12
|
+
expect(keyPair).toHaveProperty('publicKey')
|
|
13
|
+
expect(keyPair.privateKey).toMatch(/^0x[0-9a-f]{64}$/)
|
|
14
|
+
expect(keyPair.publicKey).toMatch(/^0x[0-9a-f]{2624}$/)
|
|
15
|
+
})
|
|
16
|
+
|
|
17
|
+
test('behavior: as Bytes', () => {
|
|
18
|
+
const keyPair = MlDsa44.createKeyPair({ as: 'Bytes' })
|
|
19
|
+
|
|
20
|
+
expect(keyPair.privateKey).toBeInstanceOf(Uint8Array)
|
|
21
|
+
expect(keyPair.publicKey).toBeInstanceOf(Uint8Array)
|
|
22
|
+
expect(keyPair.privateKey).toHaveLength(MlDsa44.privateKeySize)
|
|
23
|
+
expect(keyPair.publicKey).toHaveLength(MlDsa44.publicKeySize)
|
|
24
|
+
})
|
|
25
|
+
|
|
26
|
+
test('behavior: unique keys', () => {
|
|
27
|
+
const keyPair1 = MlDsa44.createKeyPair()
|
|
28
|
+
const keyPair2 = MlDsa44.createKeyPair()
|
|
29
|
+
|
|
30
|
+
expect(keyPair1.privateKey).not.toBe(keyPair2.privateKey)
|
|
31
|
+
expect(keyPair1.publicKey).not.toBe(keyPair2.publicKey)
|
|
32
|
+
})
|
|
33
|
+
|
|
34
|
+
test('behavior: public key is derivable from private key', () => {
|
|
35
|
+
const { privateKey, publicKey } = MlDsa44.createKeyPair()
|
|
36
|
+
|
|
37
|
+
expect(MlDsa44.getPublicKey({ privateKey })).toEqual(publicKey)
|
|
38
|
+
})
|
|
39
|
+
})
|
|
40
|
+
|
|
41
|
+
describe('fromPrf', () => {
|
|
42
|
+
test.each([
|
|
43
|
+
{
|
|
44
|
+
privateKey:
|
|
45
|
+
'0x72bd14bc34a4c7ce64bd5dbfe0d432a1ab34cf1c364216c471d68a073d4fbb5b',
|
|
46
|
+
prf: '0x0000000000000000000000000000000000000000000000000000000000000000',
|
|
47
|
+
},
|
|
48
|
+
{
|
|
49
|
+
privateKey:
|
|
50
|
+
'0xcc686ddfafe85330d7f270bf2fab31202b499a7e920ddbbd82cbb53759d4925c',
|
|
51
|
+
prf: '0x000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f',
|
|
52
|
+
},
|
|
53
|
+
] as const)('vector: $prf', ({ privateKey, prf }) => {
|
|
54
|
+
expect(MlDsa44.fromPrf(prf)).toBe(privateKey)
|
|
55
|
+
})
|
|
56
|
+
|
|
57
|
+
test('value: Bytes', () => {
|
|
58
|
+
const prf = Bytes.fromHex(
|
|
59
|
+
'0x000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f',
|
|
60
|
+
)
|
|
61
|
+
expect(MlDsa44.fromPrf(prf)).toMatchInlineSnapshot(
|
|
62
|
+
`"0xcc686ddfafe85330d7f270bf2fab31202b499a7e920ddbbd82cbb53759d4925c"`,
|
|
63
|
+
)
|
|
64
|
+
})
|
|
65
|
+
|
|
66
|
+
test('options: as', () => {
|
|
67
|
+
const privateKey = MlDsa44.fromPrf(
|
|
68
|
+
'0x000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f',
|
|
69
|
+
{ as: 'Bytes' },
|
|
70
|
+
)
|
|
71
|
+
expect(privateKey).toEqual(
|
|
72
|
+
Bytes.fromHex(
|
|
73
|
+
'0xcc686ddfafe85330d7f270bf2fab31202b499a7e920ddbbd82cbb53759d4925c',
|
|
74
|
+
),
|
|
75
|
+
)
|
|
76
|
+
})
|
|
77
|
+
|
|
78
|
+
test('behavior: output signs and verifies', () => {
|
|
79
|
+
const privateKey = MlDsa44.fromPrf(Bytes.random(32))
|
|
80
|
+
const publicKey = MlDsa44.getPublicKey({ privateKey })
|
|
81
|
+
const signature = MlDsa44.sign({ payload, privateKey })
|
|
82
|
+
|
|
83
|
+
expect(MlDsa44.verify({ payload, publicKey, signature })).toBe(true)
|
|
84
|
+
})
|
|
85
|
+
|
|
86
|
+
test('behavior: uses versioned label and zero counter', () => {
|
|
87
|
+
const messages: Hex.Hex[] = []
|
|
88
|
+
Engine.with(
|
|
89
|
+
{
|
|
90
|
+
Hash: {
|
|
91
|
+
hmacSha256: (_key, message) => {
|
|
92
|
+
messages.push(Hex.fromBytes(message))
|
|
93
|
+
return new Uint8Array(32)
|
|
94
|
+
},
|
|
95
|
+
},
|
|
96
|
+
},
|
|
97
|
+
() =>
|
|
98
|
+
MlDsa44.fromPrf(
|
|
99
|
+
'0x0000000000000000000000000000000000000000000000000000000000000000',
|
|
100
|
+
),
|
|
101
|
+
)
|
|
102
|
+
|
|
103
|
+
expect(messages).toMatchInlineSnapshot(`
|
|
104
|
+
[
|
|
105
|
+
"0x6f782e6d6c64736134342e66726f6d5072662e763100000000",
|
|
106
|
+
]
|
|
107
|
+
`)
|
|
108
|
+
})
|
|
109
|
+
|
|
110
|
+
test('behavior: zeroes intermediate key for Hex output', () => {
|
|
111
|
+
const candidate = new Uint8Array(32).fill(1)
|
|
112
|
+
Engine.with(
|
|
113
|
+
{
|
|
114
|
+
Hash: {
|
|
115
|
+
hmacSha256: () => candidate,
|
|
116
|
+
},
|
|
117
|
+
},
|
|
118
|
+
() =>
|
|
119
|
+
MlDsa44.fromPrf(
|
|
120
|
+
'0x0000000000000000000000000000000000000000000000000000000000000000',
|
|
121
|
+
),
|
|
122
|
+
)
|
|
123
|
+
|
|
124
|
+
expect(candidate).toEqual(new Uint8Array(32))
|
|
125
|
+
})
|
|
126
|
+
|
|
127
|
+
test('error: PRF output is too short', () => {
|
|
128
|
+
expect(() => MlDsa44.fromPrf(new Uint8Array(31)))
|
|
129
|
+
.toThrowErrorMatchingInlineSnapshot(`
|
|
130
|
+
[MlDsa44.InvalidPrfSizeError: PRF output must be exactly 32 bytes. Received 31 bytes.]
|
|
131
|
+
`)
|
|
132
|
+
})
|
|
133
|
+
|
|
134
|
+
test('error: PRF output is too long', () => {
|
|
135
|
+
expect(() => MlDsa44.fromPrf(new Uint8Array(33)))
|
|
136
|
+
.toThrowErrorMatchingInlineSnapshot(`
|
|
137
|
+
[MlDsa44.InvalidPrfSizeError: PRF output must be exactly 32 bytes. Received 33 bytes.]
|
|
138
|
+
`)
|
|
139
|
+
})
|
|
140
|
+
})
|
|
141
|
+
|
|
142
|
+
describe('getPublicKey', () => {
|
|
143
|
+
test('default', () => {
|
|
144
|
+
const publicKey = MlDsa44.getPublicKey({ privateKey })
|
|
145
|
+
|
|
146
|
+
expect(Hex.size(publicKey)).toBe(MlDsa44.publicKeySize)
|
|
147
|
+
expect(Hash.keccak256(publicKey)).toMatchInlineSnapshot(
|
|
148
|
+
`"0xe7b0dd35ffdccafa66d393e2afa66e22f658b9b72cbe63a2f51f25f8b65db4ad"`,
|
|
149
|
+
)
|
|
150
|
+
})
|
|
151
|
+
|
|
152
|
+
test('behavior: as Bytes', () => {
|
|
153
|
+
const publicKey = MlDsa44.getPublicKey({ privateKey, as: 'Bytes' })
|
|
154
|
+
|
|
155
|
+
expect(publicKey).toBeInstanceOf(Uint8Array)
|
|
156
|
+
expect(publicKey).toHaveLength(MlDsa44.publicKeySize)
|
|
157
|
+
})
|
|
158
|
+
|
|
159
|
+
test('error: invalid private key size', () => {
|
|
160
|
+
expect(() =>
|
|
161
|
+
MlDsa44.getPublicKey({ privateKey: new Uint8Array(31) }),
|
|
162
|
+
).toThrowError()
|
|
163
|
+
})
|
|
164
|
+
})
|
|
165
|
+
|
|
166
|
+
describe('randomPrivateKey', () => {
|
|
167
|
+
test('default', () => {
|
|
168
|
+
const privateKey = MlDsa44.randomPrivateKey()
|
|
169
|
+
|
|
170
|
+
expect(privateKey).toMatch(/^0x[0-9a-f]{64}$/)
|
|
171
|
+
})
|
|
172
|
+
|
|
173
|
+
test('behavior: as Bytes', () => {
|
|
174
|
+
const privateKey = MlDsa44.randomPrivateKey({ as: 'Bytes' })
|
|
175
|
+
|
|
176
|
+
expect(privateKey).toBeInstanceOf(Uint8Array)
|
|
177
|
+
expect(privateKey).toHaveLength(MlDsa44.privateKeySize)
|
|
178
|
+
})
|
|
179
|
+
})
|
|
180
|
+
|
|
181
|
+
describe('sign', () => {
|
|
182
|
+
test('default', () => {
|
|
183
|
+
const signature = MlDsa44.sign({ payload, privateKey })
|
|
184
|
+
|
|
185
|
+
expect(Hex.size(signature)).toBe(MlDsa44.signatureSize)
|
|
186
|
+
expect(Hash.keccak256(signature)).toMatchInlineSnapshot(
|
|
187
|
+
`"0x74ab423fe51d6e0ea20262b692822fcc3acfb25d1999d51254bb495bd5937ee5"`,
|
|
188
|
+
)
|
|
189
|
+
})
|
|
190
|
+
|
|
191
|
+
test('behavior: deterministic by default', () => {
|
|
192
|
+
expect(MlDsa44.sign({ payload, privateKey })).toBe(
|
|
193
|
+
MlDsa44.sign({ payload, privateKey }),
|
|
194
|
+
)
|
|
195
|
+
})
|
|
196
|
+
|
|
197
|
+
test('behavior: as Bytes', () => {
|
|
198
|
+
const signature = MlDsa44.sign({ payload, privateKey, as: 'Bytes' })
|
|
199
|
+
|
|
200
|
+
expect(signature).toBeInstanceOf(Uint8Array)
|
|
201
|
+
expect(signature).toHaveLength(MlDsa44.signatureSize)
|
|
202
|
+
})
|
|
203
|
+
|
|
204
|
+
test('behavior: Bytes payload and private key', () => {
|
|
205
|
+
const signature = MlDsa44.sign({
|
|
206
|
+
payload: Bytes.fromHex(payload),
|
|
207
|
+
privateKey: Bytes.fromHex(privateKey),
|
|
208
|
+
})
|
|
209
|
+
|
|
210
|
+
expect(signature).toBe(MlDsa44.sign({ payload, privateKey }))
|
|
211
|
+
})
|
|
212
|
+
|
|
213
|
+
test('behavior: context changes the signature', () => {
|
|
214
|
+
const signature = MlDsa44.sign({ payload, privateKey, context: '0x0102' })
|
|
215
|
+
|
|
216
|
+
expect(Hash.keccak256(signature)).toMatchInlineSnapshot(
|
|
217
|
+
`"0x3e071d2ce96153573364c0b698796534ca6b527c808dc7b0d67c7e95a5031191"`,
|
|
218
|
+
)
|
|
219
|
+
expect(signature).not.toBe(MlDsa44.sign({ payload, privateKey }))
|
|
220
|
+
})
|
|
221
|
+
|
|
222
|
+
test('behavior: extra entropy', () => {
|
|
223
|
+
const entropy = Hex.fromBytes(new Uint8Array(32).fill(9))
|
|
224
|
+
const signature = MlDsa44.sign({
|
|
225
|
+
payload,
|
|
226
|
+
privateKey,
|
|
227
|
+
extraEntropy: entropy,
|
|
228
|
+
})
|
|
229
|
+
|
|
230
|
+
expect(Hash.keccak256(signature)).toMatchInlineSnapshot(
|
|
231
|
+
`"0x10acdef5788ed190658943c03546af20df32064da4d0e795503440e37089d188"`,
|
|
232
|
+
)
|
|
233
|
+
|
|
234
|
+
const hedged1 = MlDsa44.sign({ payload, privateKey, extraEntropy: true })
|
|
235
|
+
const hedged2 = MlDsa44.sign({ payload, privateKey, extraEntropy: true })
|
|
236
|
+
expect(hedged1).not.toBe(hedged2)
|
|
237
|
+
|
|
238
|
+
const publicKey = MlDsa44.getPublicKey({ privateKey })
|
|
239
|
+
expect(MlDsa44.verify({ payload, publicKey, signature: hedged1 })).toBe(
|
|
240
|
+
true,
|
|
241
|
+
)
|
|
242
|
+
expect(MlDsa44.verify({ payload, publicKey, signature: hedged2 })).toBe(
|
|
243
|
+
true,
|
|
244
|
+
)
|
|
245
|
+
})
|
|
246
|
+
|
|
247
|
+
test('error: context too large', () => {
|
|
248
|
+
expect(() =>
|
|
249
|
+
MlDsa44.sign({ payload, privateKey, context: new Uint8Array(256) }),
|
|
250
|
+
).toThrowErrorMatchingInlineSnapshot(
|
|
251
|
+
`[MlDsa44.InvalidContextSizeError: Context must be at most 255 bytes. Received 256 bytes.]`,
|
|
252
|
+
)
|
|
253
|
+
})
|
|
254
|
+
|
|
255
|
+
test('error: invalid private key size', () => {
|
|
256
|
+
expect(() =>
|
|
257
|
+
MlDsa44.sign({ payload, privateKey: new Uint8Array(33) }),
|
|
258
|
+
).toThrowError()
|
|
259
|
+
})
|
|
260
|
+
})
|
|
261
|
+
|
|
262
|
+
describe('verify', () => {
|
|
263
|
+
test('default', () => {
|
|
264
|
+
const publicKey = MlDsa44.getPublicKey({ privateKey })
|
|
265
|
+
const signature = MlDsa44.sign({ payload, privateKey })
|
|
266
|
+
|
|
267
|
+
expect(MlDsa44.verify({ payload, publicKey, signature })).toBe(true)
|
|
268
|
+
})
|
|
269
|
+
|
|
270
|
+
test('behavior: rejects wrong payload', () => {
|
|
271
|
+
const publicKey = MlDsa44.getPublicKey({ privateKey })
|
|
272
|
+
const signature = MlDsa44.sign({ payload, privateKey })
|
|
273
|
+
|
|
274
|
+
expect(
|
|
275
|
+
MlDsa44.verify({ payload: '0xbeefdead', publicKey, signature }),
|
|
276
|
+
).toBe(false)
|
|
277
|
+
})
|
|
278
|
+
|
|
279
|
+
test('behavior: rejects corrupted signature', () => {
|
|
280
|
+
const publicKey = MlDsa44.getPublicKey({ privateKey })
|
|
281
|
+
const signature = MlDsa44.sign({ payload, privateKey, as: 'Bytes' })
|
|
282
|
+
signature[0]! ^= 1
|
|
283
|
+
|
|
284
|
+
expect(MlDsa44.verify({ payload, publicKey, signature })).toBe(false)
|
|
285
|
+
})
|
|
286
|
+
|
|
287
|
+
test('behavior: rejects malformed signature', () => {
|
|
288
|
+
const publicKey = MlDsa44.getPublicKey({ privateKey })
|
|
289
|
+
|
|
290
|
+
expect(
|
|
291
|
+
MlDsa44.verify({ payload, publicKey, signature: '0xdeadbeef' }),
|
|
292
|
+
).toBe(false)
|
|
293
|
+
})
|
|
294
|
+
|
|
295
|
+
test('behavior: rejects wrong public key', () => {
|
|
296
|
+
const signature = MlDsa44.sign({ payload, privateKey })
|
|
297
|
+
const { publicKey } = MlDsa44.createKeyPair()
|
|
298
|
+
|
|
299
|
+
expect(MlDsa44.verify({ payload, publicKey, signature })).toBe(false)
|
|
300
|
+
})
|
|
301
|
+
|
|
302
|
+
test('behavior: context must match', () => {
|
|
303
|
+
const publicKey = MlDsa44.getPublicKey({ privateKey })
|
|
304
|
+
const signature = MlDsa44.sign({ payload, privateKey, context: '0x0102' })
|
|
305
|
+
|
|
306
|
+
expect(
|
|
307
|
+
MlDsa44.verify({ payload, publicKey, signature, context: '0x0102' }),
|
|
308
|
+
).toBe(true)
|
|
309
|
+
expect(MlDsa44.verify({ payload, publicKey, signature })).toBe(false)
|
|
310
|
+
expect(
|
|
311
|
+
MlDsa44.verify({ payload, publicKey, signature, context: '0x0103' }),
|
|
312
|
+
).toBe(false)
|
|
313
|
+
})
|
|
314
|
+
|
|
315
|
+
test('error: context too large', () => {
|
|
316
|
+
const publicKey = MlDsa44.getPublicKey({ privateKey })
|
|
317
|
+
const signature = MlDsa44.sign({ payload, privateKey })
|
|
318
|
+
|
|
319
|
+
expect(() =>
|
|
320
|
+
MlDsa44.verify({
|
|
321
|
+
payload,
|
|
322
|
+
publicKey,
|
|
323
|
+
signature,
|
|
324
|
+
context: new Uint8Array(256),
|
|
325
|
+
}),
|
|
326
|
+
).toThrowErrorMatchingInlineSnapshot(
|
|
327
|
+
`[MlDsa44.InvalidContextSizeError: Context must be at most 255 bytes. Received 256 bytes.]`,
|
|
328
|
+
)
|
|
329
|
+
})
|
|
330
|
+
})
|
|
331
|
+
|
|
332
|
+
describe('engine', () => {
|
|
333
|
+
test('behavior: routes through installed overrides', () => {
|
|
334
|
+
const calls: string[] = []
|
|
335
|
+
const signature = new Uint8Array(MlDsa44.signatureSize).fill(1)
|
|
336
|
+
|
|
337
|
+
const result = Engine.with(
|
|
338
|
+
{
|
|
339
|
+
MlDsa44: {
|
|
340
|
+
sign: (payload, privateKey) => {
|
|
341
|
+
calls.push(`sign:${payload.length}:${privateKey.length}`)
|
|
342
|
+
return signature
|
|
343
|
+
},
|
|
344
|
+
verify: () => {
|
|
345
|
+
calls.push('verify')
|
|
346
|
+
return true
|
|
347
|
+
},
|
|
348
|
+
},
|
|
349
|
+
},
|
|
350
|
+
() => {
|
|
351
|
+
const signed = MlDsa44.sign({ payload, privateKey, as: 'Bytes' })
|
|
352
|
+
const verified = MlDsa44.verify({
|
|
353
|
+
payload,
|
|
354
|
+
publicKey: new Uint8Array(MlDsa44.publicKeySize),
|
|
355
|
+
signature: signed,
|
|
356
|
+
})
|
|
357
|
+
return { signed, verified }
|
|
358
|
+
},
|
|
359
|
+
)
|
|
360
|
+
|
|
361
|
+
expect(calls).toEqual(['sign:4:32', 'verify'])
|
|
362
|
+
expect(result.signed).toEqual(signature)
|
|
363
|
+
expect(result.verified).toBe(true)
|
|
364
|
+
})
|
|
365
|
+
|
|
366
|
+
test('behavior: overrides clear on reset', () => {
|
|
367
|
+
Engine.set({ MlDsa44: { getPublicKey: () => new Uint8Array(1312) } })
|
|
368
|
+
try {
|
|
369
|
+
expect(MlDsa44.getPublicKey({ privateKey })).toBe(
|
|
370
|
+
Hex.fromBytes(new Uint8Array(1312)),
|
|
371
|
+
)
|
|
372
|
+
} finally {
|
|
373
|
+
Engine.reset('MlDsa44')
|
|
374
|
+
}
|
|
375
|
+
expect(Hash.keccak256(MlDsa44.getPublicKey({ privateKey }))).toBe(
|
|
376
|
+
'0xe7b0dd35ffdccafa66d393e2afa66e22f658b9b72cbe63a2f51f25f8b65db4ad',
|
|
377
|
+
)
|
|
378
|
+
})
|
|
379
|
+
})
|
|
380
|
+
|
|
381
|
+
describe('constants', () => {
|
|
382
|
+
test('sizes', () => {
|
|
383
|
+
expect(MlDsa44.privateKeySize).toBe(32)
|
|
384
|
+
expect(MlDsa44.publicKeySize).toBe(1312)
|
|
385
|
+
expect(MlDsa44.signatureSize).toBe(2420)
|
|
386
|
+
expect(MlDsa44.noble.lengths).toMatchObject({
|
|
387
|
+
publicKey: 1312,
|
|
388
|
+
secretKey: 2560,
|
|
389
|
+
seed: 32,
|
|
390
|
+
signature: 2420,
|
|
391
|
+
})
|
|
392
|
+
})
|
|
393
|
+
})
|
|
@@ -158,6 +158,51 @@ export type Eddsa = {
|
|
|
158
158
|
| undefined
|
|
159
159
|
}
|
|
160
160
|
|
|
161
|
+
/**
|
|
162
|
+
* Module-lattice digital signatures (ML-DSA, FIPS 204). Used by the `MlDsa44`
|
|
163
|
+
* slot.
|
|
164
|
+
*
|
|
165
|
+
* Private keys cross this boundary in their 32-byte seed form (`ξ`), never as
|
|
166
|
+
* the expanded secret key -- the seed is the canonical interchange form, and an
|
|
167
|
+
* engine expands it internally.
|
|
168
|
+
*
|
|
169
|
+
* @internal
|
|
170
|
+
*/
|
|
171
|
+
export type MlDsa = {
|
|
172
|
+
/** Derives the public key from a 32-byte private-key seed. */
|
|
173
|
+
getPublicKey?: ((privateKey: Uint8Array) => Uint8Array) | undefined
|
|
174
|
+
/** Generates a random 32-byte private-key seed. */
|
|
175
|
+
randomSecretKey?: (() => Uint8Array) | undefined
|
|
176
|
+
/** Signs a payload. */
|
|
177
|
+
sign?:
|
|
178
|
+
| ((
|
|
179
|
+
payload: Uint8Array,
|
|
180
|
+
privateKey: Uint8Array,
|
|
181
|
+
options: {
|
|
182
|
+
/** Context string for domain separation, at most 255 bytes. */
|
|
183
|
+
context?: Uint8Array | undefined
|
|
184
|
+
/**
|
|
185
|
+
* Extra entropy for hedged signing: exactly 32 bytes, `true` for 32
|
|
186
|
+
* random bytes, or `false` for the deterministic variant.
|
|
187
|
+
*/
|
|
188
|
+
extraEntropy: boolean | Uint8Array
|
|
189
|
+
},
|
|
190
|
+
) => Uint8Array)
|
|
191
|
+
| undefined
|
|
192
|
+
/** Verifies a signature. */
|
|
193
|
+
verify?:
|
|
194
|
+
| ((
|
|
195
|
+
signature: Uint8Array,
|
|
196
|
+
payload: Uint8Array,
|
|
197
|
+
publicKey: Uint8Array,
|
|
198
|
+
options: {
|
|
199
|
+
/** Context string for domain separation, at most 255 bytes. */
|
|
200
|
+
context?: Uint8Array | undefined
|
|
201
|
+
},
|
|
202
|
+
) => boolean)
|
|
203
|
+
| undefined
|
|
204
|
+
}
|
|
205
|
+
|
|
161
206
|
/**
|
|
162
207
|
* Montgomery-curve key agreement (X25519).
|
|
163
208
|
*
|
|
@@ -305,6 +350,8 @@ export type Engine = {
|
|
|
305
350
|
Hash?: Hash | undefined
|
|
306
351
|
/** Implementation for [`Keystore`](/api/Keystore). */
|
|
307
352
|
Keystore?: Keystore | undefined
|
|
353
|
+
/** Implementation for [`MlDsa44`](/api/MlDsa44). */
|
|
354
|
+
MlDsa44?: MlDsa | undefined
|
|
308
355
|
/** Implementation for [`Mnemonic`](/api/Mnemonic). */
|
|
309
356
|
Mnemonic?: Mnemonic | undefined
|
|
310
357
|
/** Implementation for [`P256`](/api/P256). */
|
|
@@ -338,6 +385,7 @@ export const slots = [
|
|
|
338
385
|
'Ed25519',
|
|
339
386
|
'Hash',
|
|
340
387
|
'Keystore',
|
|
388
|
+
'MlDsa44',
|
|
341
389
|
'Mnemonic',
|
|
342
390
|
'P256',
|
|
343
391
|
'Secp256k1',
|
|
@@ -385,6 +433,7 @@ export const primitives = {
|
|
|
385
433
|
'scrypt',
|
|
386
434
|
'scryptAsync',
|
|
387
435
|
],
|
|
436
|
+
MlDsa44: ['getPublicKey', 'randomSecretKey', 'sign', 'verify'],
|
|
388
437
|
Mnemonic: ['toSeed'],
|
|
389
438
|
P256: [
|
|
390
439
|
'getPublicKey',
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import { ml_dsa44 } from '@noble/post-quantum/ml-dsa.js'
|
|
2
|
+
import { randomBytes } from '@noble/post-quantum/utils.js'
|
|
3
|
+
import { type Complete, type MlDsa, overrides } from './engine.js'
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Resolvers for the `MlDsa44` slot, and ox's defaults for it, backed by
|
|
7
|
+
* `@noble/post-quantum`.
|
|
8
|
+
*
|
|
9
|
+
* Declaring the defaults against the slot contract is what keeps them honest: a
|
|
10
|
+
* default that goes missing, or whose signature drifts, fails to compile rather
|
|
11
|
+
* than failing at the call site.
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
const getPublicKeyDefault: Complete<MlDsa>['getPublicKey'] = (privateKey) => {
|
|
15
|
+
const { publicKey, secretKey } = ml_dsa44.keygen(privateKey)
|
|
16
|
+
secretKey.fill(0)
|
|
17
|
+
return publicKey
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
const randomSecretKeyDefault: Complete<MlDsa>['randomSecretKey'] = () =>
|
|
21
|
+
randomBytes(32)
|
|
22
|
+
|
|
23
|
+
const signDefault: Complete<MlDsa>['sign'] = (payload, privateKey, options) => {
|
|
24
|
+
const { context, extraEntropy } = options
|
|
25
|
+
const { secretKey } = ml_dsa44.keygen(privateKey)
|
|
26
|
+
try {
|
|
27
|
+
// `true` is noble's default (fresh 32 random bytes), spelled by omission.
|
|
28
|
+
return ml_dsa44.sign(payload, secretKey, {
|
|
29
|
+
...(context ? { context } : {}),
|
|
30
|
+
...(extraEntropy === true ? {} : { extraEntropy }),
|
|
31
|
+
})
|
|
32
|
+
} finally {
|
|
33
|
+
secretKey.fill(0)
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const verifyDefault: Complete<MlDsa>['verify'] = (
|
|
38
|
+
signature,
|
|
39
|
+
payload,
|
|
40
|
+
publicKey,
|
|
41
|
+
options,
|
|
42
|
+
) =>
|
|
43
|
+
ml_dsa44.verify(
|
|
44
|
+
signature,
|
|
45
|
+
payload,
|
|
46
|
+
publicKey,
|
|
47
|
+
options.context ? { context: options.context } : {},
|
|
48
|
+
)
|
|
49
|
+
|
|
50
|
+
/** @internal */
|
|
51
|
+
export const getPublicKey: Complete<MlDsa>['getPublicKey'] = (privateKey) =>
|
|
52
|
+
(overrides.MlDsa44?.getPublicKey ?? getPublicKeyDefault)(privateKey)
|
|
53
|
+
|
|
54
|
+
/** @internal */
|
|
55
|
+
export const randomSecretKey: Complete<MlDsa>['randomSecretKey'] = () =>
|
|
56
|
+
(overrides.MlDsa44?.randomSecretKey ?? randomSecretKeyDefault)()
|
|
57
|
+
|
|
58
|
+
/** @internal */
|
|
59
|
+
export const sign: Complete<MlDsa>['sign'] = (payload, privateKey, options) =>
|
|
60
|
+
(overrides.MlDsa44?.sign ?? signDefault)(payload, privateKey, options)
|
|
61
|
+
|
|
62
|
+
/** @internal */
|
|
63
|
+
export const verify: Complete<MlDsa>['verify'] = (
|
|
64
|
+
signature,
|
|
65
|
+
payload,
|
|
66
|
+
publicKey,
|
|
67
|
+
options,
|
|
68
|
+
) =>
|
|
69
|
+
(overrides.MlDsa44?.verify ?? verifyDefault)(
|
|
70
|
+
signature,
|
|
71
|
+
payload,
|
|
72
|
+
publicKey,
|
|
73
|
+
options,
|
|
74
|
+
)
|
package/src/index.ts
CHANGED
|
@@ -1915,6 +1915,47 @@ export * as Kzg from './core/Kzg.js'
|
|
|
1915
1915
|
* @category Execution Spec
|
|
1916
1916
|
*/
|
|
1917
1917
|
export * as Log from './core/Log.js'
|
|
1918
|
+
|
|
1919
|
+
/**
|
|
1920
|
+
* Utilities for working with ML-DSA-44 signatures and key pairs, as defined in [FIPS 204](https://csrc.nist.gov/pubs/fips/204/final).
|
|
1921
|
+
*
|
|
1922
|
+
* ML-DSA is a post-quantum digital signature scheme, standardized by NIST as the
|
|
1923
|
+
* primary quantum-resistant replacement for elliptic-curve signatures. The `44`
|
|
1924
|
+
* parameter set targets NIST security category 2 (128-bit classical security).
|
|
1925
|
+
*
|
|
1926
|
+
* Private keys are the 32-byte FIPS 204 seed. Public keys are 1,312 bytes and
|
|
1927
|
+
* signatures are 2,420 bytes.
|
|
1928
|
+
*
|
|
1929
|
+
* @example
|
|
1930
|
+
* ### Creating Key Pairs
|
|
1931
|
+
*
|
|
1932
|
+
* ```ts twoslash
|
|
1933
|
+
* import { MlDsa44 } from 'ox'
|
|
1934
|
+
*
|
|
1935
|
+
* const { privateKey, publicKey } = MlDsa44.createKeyPair()
|
|
1936
|
+
* ```
|
|
1937
|
+
*
|
|
1938
|
+
* @example
|
|
1939
|
+
* ### Signing & Verifying
|
|
1940
|
+
*
|
|
1941
|
+
* ```ts twoslash
|
|
1942
|
+
* import { MlDsa44 } from 'ox'
|
|
1943
|
+
*
|
|
1944
|
+
* const { privateKey, publicKey } = MlDsa44.createKeyPair()
|
|
1945
|
+
* const payload = '0xdeadbeef'
|
|
1946
|
+
*
|
|
1947
|
+
* const signature = MlDsa44.sign({ payload, privateKey })
|
|
1948
|
+
* const isValid = MlDsa44.verify({
|
|
1949
|
+
* payload,
|
|
1950
|
+
* publicKey,
|
|
1951
|
+
* signature
|
|
1952
|
+
* })
|
|
1953
|
+
* ```
|
|
1954
|
+
*
|
|
1955
|
+
* @category Post Quantum
|
|
1956
|
+
*/
|
|
1957
|
+
export * as MlDsa44 from './core/MlDsa44.js'
|
|
1958
|
+
|
|
1918
1959
|
/**
|
|
1919
1960
|
* Utility functions for generating and working with [BIP-39](https://github.com/bitcoin/bips/blob/master/bip-0039.mediawiki) mnemonics.
|
|
1920
1961
|
*
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import type { Hex } from 'ox'
|
|
2
|
+
import { WithdrawalSenderTag } from 'ox/tempo'
|
|
3
|
+
import { expectTypeOf, test } from 'vp/test'
|
|
4
|
+
|
|
5
|
+
test('from', () => {
|
|
6
|
+
expectTypeOf(
|
|
7
|
+
WithdrawalSenderTag.from({
|
|
8
|
+
sender: '0x1234567890abcdef1234567890abcdef12345678',
|
|
9
|
+
transactionHash:
|
|
10
|
+
'0xabababababababababababababababababababababababababababababababab',
|
|
11
|
+
}),
|
|
12
|
+
).toEqualTypeOf<Hex.Hex>()
|
|
13
|
+
})
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { WithdrawalSenderTag } from 'ox/tempo'
|
|
2
|
+
import { expect, test } from 'vp/test'
|
|
3
|
+
|
|
4
|
+
test('from', () => {
|
|
5
|
+
const senderTag = WithdrawalSenderTag.from({
|
|
6
|
+
sender: '0x1234567890abcdef1234567890abcdef12345678',
|
|
7
|
+
transactionHash:
|
|
8
|
+
'0xabababababababababababababababababababababababababababababababab',
|
|
9
|
+
})
|
|
10
|
+
expect(senderTag).toMatchInlineSnapshot(
|
|
11
|
+
`"0x3362fade7333b56b9f3582089ec5915b8a6f6ac13e73a7f90c169e3eb81d8a5e"`,
|
|
12
|
+
)
|
|
13
|
+
})
|
|
14
|
+
|
|
15
|
+
test('deterministic', () => {
|
|
16
|
+
const value = {
|
|
17
|
+
sender: '0x1234567890abcdef1234567890abcdef12345678',
|
|
18
|
+
transactionHash:
|
|
19
|
+
'0xabababababababababababababababababababababababababababababababab',
|
|
20
|
+
} as const
|
|
21
|
+
expect(WithdrawalSenderTag.from(value)).toBe(WithdrawalSenderTag.from(value))
|
|
22
|
+
})
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import * as AbiParameters from '../core/AbiParameters.js'
|
|
2
|
+
import * as Address from '../core/Address.js'
|
|
3
|
+
import * as Hash from '../core/Hash.js'
|
|
4
|
+
import * as Hex from '../core/Hex.js'
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Derives the sender tag that identifies the indexed parent-chain
|
|
8
|
+
* `WithdrawalProcessed` event for a Zone withdrawal.
|
|
9
|
+
*
|
|
10
|
+
* The `transactionHash` is the Zone transaction containing the
|
|
11
|
+
* `ZoneOutbox.requestWithdrawal` call. The protocol defines the sender tag as
|
|
12
|
+
* `keccak256(abi.encodePacked(sender, transactionHash))`.
|
|
13
|
+
*
|
|
14
|
+
* [Authenticated Withdrawals Specification](https://github.com/tempoxyz/zones/blob/main/specs/spec.md#authenticated-withdrawals)
|
|
15
|
+
*
|
|
16
|
+
* @example
|
|
17
|
+
* ```ts twoslash
|
|
18
|
+
* import { WithdrawalSenderTag } from 'ox/tempo'
|
|
19
|
+
*
|
|
20
|
+
* const senderTag = WithdrawalSenderTag.from({
|
|
21
|
+
* sender: '0x1234567890abcdef1234567890abcdef12345678',
|
|
22
|
+
* transactionHash:
|
|
23
|
+
* '0xabababababababababababababababababababababababababababababababab'
|
|
24
|
+
* })
|
|
25
|
+
* ```
|
|
26
|
+
*
|
|
27
|
+
* @param value - Withdrawal sender and Zone transaction hash.
|
|
28
|
+
* @returns The sender tag.
|
|
29
|
+
*/
|
|
30
|
+
export function from(value: from.Value): Hex.Hex {
|
|
31
|
+
const { sender, transactionHash } = value
|
|
32
|
+
return Hash.keccak256(
|
|
33
|
+
AbiParameters.encodePacked(
|
|
34
|
+
['address', 'bytes32'],
|
|
35
|
+
[sender, transactionHash],
|
|
36
|
+
),
|
|
37
|
+
)
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export declare namespace from {
|
|
41
|
+
export type Value = {
|
|
42
|
+
/** Address that requested the withdrawal. */
|
|
43
|
+
sender: Address.Address
|
|
44
|
+
/** Hash of the Zone transaction containing `ZoneOutbox.requestWithdrawal`. */
|
|
45
|
+
transactionHash: Hex.Hex
|
|
46
|
+
}
|
|
47
|
+
}
|