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.
Files changed (74) hide show
  1. package/CHANGELOG.md +12 -0
  2. package/dist/core/Engine.d.ts +1 -1
  3. package/dist/core/Engine.d.ts.map +1 -1
  4. package/dist/core/Engine.js.map +1 -1
  5. package/dist/core/MlDsa44.d.ts +389 -0
  6. package/dist/core/MlDsa44.d.ts.map +1 -0
  7. package/dist/core/MlDsa44.js +206 -0
  8. package/dist/core/MlDsa44.js.map +1 -0
  9. package/dist/core/internal/engine.d.ts +35 -1
  10. package/dist/core/internal/engine.d.ts.map +1 -1
  11. package/dist/core/internal/engine.js +2 -0
  12. package/dist/core/internal/engine.js.map +1 -1
  13. package/dist/core/internal/mlDsa44.d.ts +10 -0
  14. package/dist/core/internal/mlDsa44.d.ts.map +1 -0
  15. package/dist/core/internal/mlDsa44.js +41 -0
  16. package/dist/core/internal/mlDsa44.js.map +1 -0
  17. package/dist/index.d.ts +39 -0
  18. package/dist/index.d.ts.map +1 -1
  19. package/dist/index.js +39 -0
  20. package/dist/index.js.map +1 -1
  21. package/dist/tempo/WithdrawalSenderTag.d.ts +36 -0
  22. package/dist/tempo/WithdrawalSenderTag.d.ts.map +1 -0
  23. package/dist/tempo/WithdrawalSenderTag.js +31 -0
  24. package/dist/tempo/WithdrawalSenderTag.js.map +1 -0
  25. package/dist/tempo/index.d.ts +23 -0
  26. package/dist/tempo/index.d.ts.map +1 -1
  27. package/dist/tempo/index.js +23 -0
  28. package/dist/tempo/index.js.map +1 -1
  29. package/dist/wasm/Engine.d.ts +5 -1
  30. package/dist/wasm/Engine.d.ts.map +1 -1
  31. package/dist/wasm/Engine.js +9 -1
  32. package/dist/wasm/Engine.js.map +1 -1
  33. package/dist/wasm/MlDsa44.d.ts +55 -0
  34. package/dist/wasm/MlDsa44.d.ts.map +1 -0
  35. package/dist/wasm/MlDsa44.js +143 -0
  36. package/dist/wasm/MlDsa44.js.map +1 -0
  37. package/dist/wasm/index.d.ts +23 -2
  38. package/dist/wasm/index.d.ts.map +1 -1
  39. package/dist/wasm/index.js +23 -2
  40. package/dist/wasm/index.js.map +1 -1
  41. package/dist/wasm/internal/mldsa44.d.ts +12 -0
  42. package/dist/wasm/internal/mldsa44.d.ts.map +1 -0
  43. package/dist/wasm/internal/mldsa44.js +5 -0
  44. package/dist/wasm/internal/mldsa44.js.map +1 -0
  45. package/dist/wasm/internal/mldsa44.wasm.d.ts +3 -0
  46. package/dist/wasm/internal/mldsa44.wasm.d.ts.map +1 -0
  47. package/dist/wasm/internal/mldsa44.wasm.js +10 -0
  48. package/dist/wasm/internal/mldsa44.wasm.js.map +1 -0
  49. package/dist/zod/TypedData.d.ts +2 -2
  50. package/package.json +19 -2
  51. package/src/core/Engine.ts +1 -0
  52. package/src/core/MlDsa44.ts +415 -0
  53. package/src/core/_test/Engine.test.ts +2 -2
  54. package/src/core/_test/MlDsa44.test-d.ts +56 -0
  55. package/src/core/_test/MlDsa44.test.ts +393 -0
  56. package/src/core/_test/index.test.ts +1 -0
  57. package/src/core/internal/engine.ts +49 -0
  58. package/src/core/internal/mlDsa44.ts +74 -0
  59. package/src/index.ts +41 -0
  60. package/src/tempo/WithdrawalSenderTag.test-d.ts +13 -0
  61. package/src/tempo/WithdrawalSenderTag.test.ts +22 -0
  62. package/src/tempo/WithdrawalSenderTag.ts +47 -0
  63. package/src/tempo/index.ts +24 -0
  64. package/src/version.ts +1 -1
  65. package/src/wasm/Engine.ts +19 -7
  66. package/src/wasm/MlDsa44.ts +190 -0
  67. package/src/wasm/_test/Engine.test-d.ts +2 -0
  68. package/src/wasm/_test/Engine.test.ts +16 -0
  69. package/src/wasm/_test/MlDsa44.test-d.ts +28 -0
  70. package/src/wasm/_test/MlDsa44.test.ts +192 -0
  71. package/src/wasm/index.ts +24 -2
  72. package/src/wasm/internal/mldsa44.ts +34 -0
  73. package/src/wasm/internal/mldsa44.wasm.ts +11 -0
  74. 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
+ })
@@ -48,6 +48,7 @@ test('exports', () => {
48
48
  "Keystore",
49
49
  "Kzg",
50
50
  "Log",
51
+ "MlDsa44",
51
52
  "Mnemonic",
52
53
  "P256",
53
54
  "PersonalMessage",
@@ -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
+ }