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
@@ -529,6 +529,30 @@ export * as VirtualAddress from './VirtualAddress.js'
529
529
  */
530
530
  export * as VirtualMaster from './VirtualMaster.js'
531
531
 
532
+ /**
533
+ * Utilities for deriving the sender tag that correlates a Zone withdrawal with
534
+ * its indexed parent-chain `WithdrawalProcessed` event.
535
+ *
536
+ * The sender tag commits to the withdrawal sender and the Zone transaction hash
537
+ * containing the `ZoneOutbox.requestWithdrawal` call.
538
+ *
539
+ * [Authenticated Withdrawals Specification](https://github.com/tempoxyz/zones/blob/main/specs/spec.md#authenticated-withdrawals)
540
+ *
541
+ * @example
542
+ * ```ts twoslash
543
+ * import { WithdrawalSenderTag } from 'ox/tempo'
544
+ *
545
+ * const senderTag = WithdrawalSenderTag.from({
546
+ * sender: '0x1234567890abcdef1234567890abcdef12345678',
547
+ * transactionHash:
548
+ * '0xabababababababababababababababababababababababababababababababab'
549
+ * })
550
+ * ```
551
+ *
552
+ * @category Reference
553
+ */
554
+ export * as WithdrawalSenderTag from './WithdrawalSenderTag.js'
555
+
532
556
  /**
533
557
  * Zone ID utilities for converting between zone IDs and zone chain IDs.
534
558
  *
package/src/version.ts CHANGED
@@ -1,2 +1,2 @@
1
1
  /** @internal */
2
- export const version = '1.4.0'
2
+ export const version = '1.6.0'
@@ -3,7 +3,9 @@ import type * as Errors from '../core/Errors.js'
3
3
  import * as Ed25519 from './Ed25519.js'
4
4
  import * as Hash from './Hash.js'
5
5
  import * as keystore from './internal/keystore.js'
6
+ import * as MlDsa44 from './MlDsa44.js'
6
7
  import * as Mnemonic from './Mnemonic.js'
8
+ import * as Secp256k1 from './Secp256k1.js'
7
9
  import * as X25519 from './X25519.js'
8
10
  import type * as internal from './internal/instantiate.js'
9
11
 
@@ -26,6 +28,7 @@ export type {
26
28
  Hash,
27
29
  HashState,
28
30
  Keystore,
31
+ MlDsa,
29
32
  Mnemonic,
30
33
  } from '../core/Engine.js'
31
34
 
@@ -56,7 +59,9 @@ export async function install(): Promise<install.ReturnType> {
56
59
  Ed25519: Ed25519.engine(),
57
60
  Hash: Hash.engine(),
58
61
  Keystore: keystore.engine(),
62
+ MlDsa44: MlDsa44.engine(),
59
63
  Mnemonic: Mnemonic.engine(),
64
+ Secp256k1: Secp256k1.engine(),
60
65
  X25519: X25519.engine(),
61
66
  })
62
67
  }
@@ -92,18 +97,23 @@ export declare namespace install {
92
97
  * @returns An engine, ready to install.
93
98
  */
94
99
  export async function engine(): Promise<engine.ReturnType> {
95
- const [ed25519, hash, keystoreEngine, mnemonic, x25519] = await Promise.all([
96
- Ed25519.engine(),
97
- Hash.engine(),
98
- keystore.engine(),
99
- Mnemonic.engine(),
100
- X25519.engine(),
101
- ])
100
+ const [ed25519, hash, keystoreEngine, mlDsa44, mnemonic, secp256k1, x25519] =
101
+ await Promise.all([
102
+ Ed25519.engine(),
103
+ Hash.engine(),
104
+ keystore.engine(),
105
+ MlDsa44.engine(),
106
+ Mnemonic.engine(),
107
+ Secp256k1.engine(),
108
+ X25519.engine(),
109
+ ])
102
110
  return {
103
111
  Ed25519: ed25519,
104
112
  Hash: hash,
105
113
  Keystore: keystoreEngine,
114
+ MlDsa44: mlDsa44,
106
115
  Mnemonic: mnemonic,
116
+ Secp256k1: secp256k1,
107
117
  X25519: x25519,
108
118
  }
109
119
  }
@@ -116,7 +126,9 @@ export declare namespace engine {
116
126
  Keystore: {
117
127
  pbkdf2Sha256: NonNullable<CoreEngine.Keystore['pbkdf2Sha256']>
118
128
  }
129
+ MlDsa44: MlDsa44.engine.ReturnType
119
130
  Mnemonic: Mnemonic.engine.ReturnType
131
+ Secp256k1: Secp256k1.engine.ReturnType
120
132
  X25519: X25519.engine.ReturnType
121
133
  }
122
134
 
@@ -0,0 +1,190 @@
1
+ import type * as Engine from '../core/Engine.js'
2
+ import * as Errors from '../core/Errors.js'
3
+ import * as internal from './internal/instantiate.js'
4
+ import * as mldsa44 from './internal/mldsa44.js'
5
+
6
+ export * from '../core/MlDsa44.js'
7
+ export { MemoryError } from './internal/instantiate.js'
8
+
9
+ const privateKeySize = 32
10
+ const publicKeySize = 1312
11
+ const signatureSize = 2420
12
+ const randomSize = 32
13
+ const maxContextSize = 255
14
+
15
+ /**
16
+ * Compiles WASM implementations of selected [`MlDsa44`](/api/MlDsa44)
17
+ * primitives, without installing them.
18
+ *
19
+ * Random key generation remains on Ox's host-backed default implementation.
20
+ *
21
+ * @example
22
+ * ```ts twoslash
23
+ * // @noErrors
24
+ * import { Engine } from 'ox'
25
+ * import { MlDsa44 } from 'ox/wasm'
26
+ *
27
+ * await Engine.install({ MlDsa44: MlDsa44.engine() })
28
+ *
29
+ * MlDsa44.getPublicKey({ privateKey: '0x...' })
30
+ * ```
31
+ *
32
+ * @returns The WASM implementation of part of the `MlDsa44` slot.
33
+ */
34
+ export async function engine(): Promise<engine.ReturnType> {
35
+ const module = await mldsa44.load()
36
+
37
+ return {
38
+ getPublicKey(privateKey) {
39
+ assertLength(privateKey, privateKeySize, 'private key')
40
+ const size = privateKeySize + publicKeySize
41
+ module.reserve(size)
42
+ const privateKeyPtr = module.heapBase
43
+ const publicKeyPtr = privateKeyPtr + privateKeySize
44
+ try {
45
+ module.view().set(privateKey, privateKeyPtr)
46
+ if (
47
+ module.exports.mldsa44_get_public_key(privateKeyPtr, publicKeyPtr) !==
48
+ 1
49
+ )
50
+ throw new InvalidInputError({ operation: 'getPublicKey' })
51
+ return module.view().slice(publicKeyPtr, publicKeyPtr + publicKeySize)
52
+ } finally {
53
+ module.exports.zero(privateKeyPtr, size)
54
+ }
55
+ },
56
+ sign(payload, privateKey, options) {
57
+ assertLength(privateKey, privateKeySize, 'private key')
58
+ const context = options.context ?? new Uint8Array(0)
59
+ assertContextLength(context)
60
+ // FIPS 204 hedged signing draws 32 bytes of randomness; the
61
+ // deterministic variant pins them to zero.
62
+ const random =
63
+ options.extraEntropy === true
64
+ ? crypto.getRandomValues(new Uint8Array(randomSize))
65
+ : options.extraEntropy === false
66
+ ? new Uint8Array(randomSize)
67
+ : options.extraEntropy
68
+ assertLength(random, randomSize, 'extra entropy')
69
+ const size =
70
+ privateKeySize +
71
+ randomSize +
72
+ context.length +
73
+ payload.length +
74
+ signatureSize
75
+ module.reserve(size)
76
+ const privateKeyPtr = module.heapBase
77
+ const randomPtr = privateKeyPtr + privateKeySize
78
+ const contextPtr = randomPtr + randomSize
79
+ const payloadPtr = contextPtr + context.length
80
+ const signaturePtr = payloadPtr + payload.length
81
+ try {
82
+ const memory = module.view()
83
+ memory.set(privateKey, privateKeyPtr)
84
+ memory.set(random, randomPtr)
85
+ memory.set(context, contextPtr)
86
+ memory.set(payload, payloadPtr)
87
+ if (
88
+ module.exports.mldsa44_sign(
89
+ privateKeyPtr,
90
+ payloadPtr,
91
+ payload.length,
92
+ contextPtr,
93
+ context.length,
94
+ randomPtr,
95
+ signaturePtr,
96
+ ) !== 1
97
+ )
98
+ throw new InvalidInputError({ operation: 'sign' })
99
+ return module.view().slice(signaturePtr, signaturePtr + signatureSize)
100
+ } finally {
101
+ module.exports.zero(privateKeyPtr, size)
102
+ }
103
+ },
104
+ verify(signature, payload, publicKey, options) {
105
+ assertLength(publicKey, publicKeySize, 'public key')
106
+ const context = options.context ?? new Uint8Array(0)
107
+ assertContextLength(context)
108
+ // A malformed signature is a verification failure, not an error.
109
+ if (signature.length !== signatureSize) return false
110
+ const size =
111
+ signatureSize + context.length + payload.length + publicKeySize
112
+ module.reserve(size)
113
+ const signaturePtr = module.heapBase
114
+ const contextPtr = signaturePtr + signatureSize
115
+ const payloadPtr = contextPtr + context.length
116
+ const publicKeyPtr = payloadPtr + payload.length
117
+ try {
118
+ const memory = module.view()
119
+ memory.set(signature, signaturePtr)
120
+ memory.set(context, contextPtr)
121
+ memory.set(payload, payloadPtr)
122
+ memory.set(publicKey, publicKeyPtr)
123
+ return (
124
+ module.exports.mldsa44_verify(
125
+ signaturePtr,
126
+ payloadPtr,
127
+ payload.length,
128
+ contextPtr,
129
+ context.length,
130
+ publicKeyPtr,
131
+ ) === 1
132
+ )
133
+ } finally {
134
+ module.exports.zero(signaturePtr, size)
135
+ }
136
+ },
137
+ }
138
+ }
139
+
140
+ export declare namespace engine {
141
+ /** Every `MlDsa44` primitive this module implements. */
142
+ type ReturnType = {
143
+ [key in 'getPublicKey' | 'sign' | 'verify']-?: NonNullable<
144
+ Engine.MlDsa[key]
145
+ >
146
+ }
147
+
148
+ type ErrorType =
149
+ | InvalidInputError
150
+ | internal.MemoryError
151
+ | Errors.GlobalErrorType
152
+ }
153
+
154
+ function assertLength(value: Uint8Array, length: number, name: string): void {
155
+ if (value.length !== length)
156
+ throw new RangeError(
157
+ `ML-DSA-44 ${name} must be ${length} bytes, got ${value.length}`,
158
+ )
159
+ }
160
+
161
+ function assertContextLength(context: Uint8Array): void {
162
+ if (context.length > maxContextSize)
163
+ throw new RangeError(
164
+ `ML-DSA-44 context must be at most ${maxContextSize} bytes, got ${context.length}`,
165
+ )
166
+ }
167
+
168
+ /**
169
+ * Thrown when mldsa-native rejects an input or fails to produce a signature.
170
+ *
171
+ * @example
172
+ * ```ts twoslash
173
+ * import { MlDsa44 } from 'ox/wasm'
174
+ *
175
+ * try {
176
+ * const engine = await MlDsa44.engine()
177
+ * engine.getPublicKey(new Uint8Array(32))
178
+ * } catch (error) {
179
+ * if (error instanceof MlDsa44.InvalidInputError)
180
+ * console.error(error.message)
181
+ * }
182
+ * ```
183
+ */
184
+ export class InvalidInputError extends Errors.BaseError {
185
+ override readonly name = 'MlDsa44.InvalidInputError'
186
+
187
+ constructor({ operation }: { operation: string }) {
188
+ super(`WASM ML-DSA-44 ${operation} received invalid input.`)
189
+ }
190
+ }
@@ -11,6 +11,8 @@ test('install and engine expose the same precise engine', () => {
11
11
  (input: Uint8Array) => Uint8Array
12
12
  >()
13
13
  expectTypeOf<Installed['Keystore']>().not.toHaveProperty('scrypt')
14
+ expectTypeOf<Installed['MlDsa44']>().not.toHaveProperty('randomSecretKey')
15
+ expectTypeOf<Installed['Secp256k1']>().not.toHaveProperty('randomSecretKey')
14
16
  })
15
17
 
16
18
  test('the WASM Engine namespace exposes synchronous core operations', () => {
@@ -10,7 +10,9 @@ describe('engine', () => {
10
10
  "Ed25519",
11
11
  "Hash",
12
12
  "Keystore",
13
+ "MlDsa44",
13
14
  "Mnemonic",
15
+ "Secp256k1",
14
16
  "X25519",
15
17
  ]
16
18
  `)
@@ -39,7 +41,9 @@ describe('install', () => {
39
41
  "Ed25519",
40
42
  "Hash",
41
43
  "Keystore",
44
+ "MlDsa44",
42
45
  "Mnemonic",
46
+ "Secp256k1",
43
47
  "X25519",
44
48
  ]
45
49
  `)
@@ -88,9 +92,21 @@ describe('install', () => {
88
92
  "Keystore": [
89
93
  "pbkdf2Sha256",
90
94
  ],
95
+ "MlDsa44": [
96
+ "getPublicKey",
97
+ "sign",
98
+ "verify",
99
+ ],
91
100
  "Mnemonic": [
92
101
  "toSeed",
93
102
  ],
103
+ "Secp256k1": [
104
+ "getPublicKey",
105
+ "getSharedSecret",
106
+ "recoverPublicKey",
107
+ "sign",
108
+ "verify",
109
+ ],
94
110
  "X25519": [
95
111
  "getPublicKey",
96
112
  "getSharedSecret",
@@ -0,0 +1,28 @@
1
+ import { expectTypeOf, test } from 'vp/test'
2
+ import type * as Engine from '../../core/Engine.js'
3
+ import * as core_MlDsa44 from '../../core/MlDsa44.js'
4
+ import * as MlDsa44 from '../MlDsa44.js'
5
+
6
+ type Slot = Awaited<ReturnType<typeof MlDsa44.engine>>
7
+
8
+ test('return type exposes only deterministic MlDsa44 primitives', () => {
9
+ expectTypeOf<Slot['getPublicKey']>().toEqualTypeOf<
10
+ NonNullable<Engine.MlDsa['getPublicKey']>
11
+ >()
12
+ expectTypeOf<Slot['sign']>().toEqualTypeOf<
13
+ NonNullable<Engine.MlDsa['sign']>
14
+ >()
15
+ expectTypeOf<Slot['verify']>().toEqualTypeOf<
16
+ NonNullable<Engine.MlDsa['verify']>
17
+ >()
18
+ expectTypeOf<Slot>().not.toHaveProperty('randomSecretKey')
19
+ })
20
+
21
+ test('the WASM namespace exposes the public MlDsa44 API', () => {
22
+ expectTypeOf(MlDsa44.getPublicKey).toEqualTypeOf(core_MlDsa44.getPublicKey)
23
+ expectTypeOf(MlDsa44.randomPrivateKey).toEqualTypeOf(
24
+ core_MlDsa44.randomPrivateKey,
25
+ )
26
+ expectTypeOf(MlDsa44.sign).toEqualTypeOf(core_MlDsa44.sign)
27
+ expectTypeOf(MlDsa44.verify).toEqualTypeOf(core_MlDsa44.verify)
28
+ })
@@ -0,0 +1,192 @@
1
+ import { ml_dsa44 } from '@noble/post-quantum/ml-dsa.js'
2
+ import { Engine } from 'ox'
3
+ import { describe, expect, test } from 'vp/test'
4
+ import * as MlDsa44 from '../MlDsa44.js'
5
+
6
+ const seed = new Uint8Array(32).fill(7)
7
+ const keys = ml_dsa44.keygen(seed)
8
+ const payload = Uint8Array.of(0xde, 0xad, 0xbe, 0xef)
9
+
10
+ function nobleSign(
11
+ payload: Uint8Array,
12
+ options: { context?: Uint8Array; extraEntropy?: Uint8Array | false } = {},
13
+ ) {
14
+ return ml_dsa44.sign(payload, keys.secretKey, {
15
+ extraEntropy: false,
16
+ ...options,
17
+ })
18
+ }
19
+
20
+ describe('engine', () => {
21
+ test('behavior: exposes only deterministic primitives', async () => {
22
+ const engine = await MlDsa44.engine()
23
+
24
+ expect(Object.keys(engine).sort()).toMatchInlineSnapshot(`
25
+ [
26
+ "getPublicKey",
27
+ "sign",
28
+ "verify",
29
+ ]
30
+ `)
31
+ })
32
+
33
+ test('behavior: matches noble public keys', async () => {
34
+ const engine = await MlDsa44.engine()
35
+
36
+ expect(engine.getPublicKey(seed)).toEqual(keys.publicKey)
37
+ })
38
+
39
+ test('behavior: matches noble signing across payload boundaries', async () => {
40
+ const engine = await MlDsa44.engine()
41
+
42
+ for (const size of [0, 1, 31, 32, 33, 64, 1024, 100_000]) {
43
+ const payload = new Uint8Array(size).fill(size % 256)
44
+ const signature = engine.sign(payload, seed, { extraEntropy: false })
45
+
46
+ expect(signature).toEqual(nobleSign(payload))
47
+ expect(engine.verify(signature, payload, keys.publicKey, {})).toBe(true)
48
+ expect(ml_dsa44.verify(signature, payload, keys.publicKey)).toBe(true)
49
+ }
50
+ })
51
+
52
+ test('behavior: matches noble signing with context and entropy', async () => {
53
+ const engine = await MlDsa44.engine()
54
+ const context = Uint8Array.of(1, 2, 3)
55
+ const entropy = new Uint8Array(32).fill(9)
56
+
57
+ for (const options of [
58
+ { context },
59
+ { extraEntropy: entropy },
60
+ { context, extraEntropy: entropy },
61
+ { context: new Uint8Array(255).fill(5) },
62
+ ] as const) {
63
+ const signature = engine.sign(payload, seed, {
64
+ extraEntropy: false,
65
+ ...options,
66
+ })
67
+
68
+ expect(signature).toEqual(nobleSign(payload, options))
69
+ expect(
70
+ engine.verify(signature, payload, keys.publicKey, {
71
+ context: options.context,
72
+ }),
73
+ ).toBe(true)
74
+ }
75
+ })
76
+
77
+ test('behavior: context binds verification', async () => {
78
+ const engine = await MlDsa44.engine()
79
+ const context = Uint8Array.of(1)
80
+ const signature = engine.sign(payload, seed, {
81
+ context,
82
+ extraEntropy: false,
83
+ })
84
+
85
+ expect(engine.verify(signature, payload, keys.publicKey, { context })).toBe(
86
+ true,
87
+ )
88
+ expect(engine.verify(signature, payload, keys.publicKey, {})).toBe(false)
89
+ expect(
90
+ engine.verify(signature, payload, keys.publicKey, {
91
+ context: Uint8Array.of(2),
92
+ }),
93
+ ).toBe(false)
94
+ })
95
+
96
+ test('behavior: generates host entropy for hedged signing', async () => {
97
+ const engine = await MlDsa44.engine()
98
+ const signatureA = engine.sign(payload, seed, { extraEntropy: true })
99
+ const signatureB = engine.sign(payload, seed, { extraEntropy: true })
100
+
101
+ expect(signatureA).not.toEqual(signatureB)
102
+ expect(engine.verify(signatureA, payload, keys.publicKey, {})).toBe(true)
103
+ expect(engine.verify(signatureB, payload, keys.publicKey, {})).toBe(true)
104
+ expect(ml_dsa44.verify(signatureA, payload, keys.publicKey)).toBe(true)
105
+ })
106
+
107
+ test('behavior: rejects corrupted and truncated signatures', async () => {
108
+ const engine = await MlDsa44.engine()
109
+ const signature = engine.sign(payload, seed, { extraEntropy: false })
110
+
111
+ for (const corrupt of [0, 1, 100, 2419]) {
112
+ const bad = signature.slice()
113
+ bad[corrupt]! ^= 1
114
+ expect(engine.verify(bad, payload, keys.publicKey, {})).toBe(false)
115
+ }
116
+ expect(engine.verify(signature.slice(1), payload, keys.publicKey, {})).toBe(
117
+ false,
118
+ )
119
+ expect(engine.verify(new Uint8Array(0), payload, keys.publicKey, {})).toBe(
120
+ false,
121
+ )
122
+ expect(engine.verify(signature, payload.slice(1), keys.publicKey, {})).toBe(
123
+ false,
124
+ )
125
+ })
126
+
127
+ test('behavior: rejects noble signatures made with other keys', async () => {
128
+ const engine = await MlDsa44.engine()
129
+ const other = ml_dsa44.keygen(new Uint8Array(32).fill(8))
130
+ const signature = engine.sign(payload, seed, { extraEntropy: false })
131
+
132
+ expect(engine.verify(signature, payload, other.publicKey, {})).toBe(false)
133
+ })
134
+
135
+ test('behavior: installs into the Engine registry', async () => {
136
+ await Engine.install({ MlDsa44: MlDsa44.engine() })
137
+ try {
138
+ expect(Engine.get().MlDsa44).toBeDefined()
139
+
140
+ const signature = MlDsa44.sign({
141
+ payload: '0xdeadbeef',
142
+ privateKey: seed,
143
+ as: 'Bytes',
144
+ })
145
+ expect(signature).toEqual(nobleSign(payload))
146
+ expect(
147
+ MlDsa44.verify({
148
+ payload: '0xdeadbeef',
149
+ publicKey: keys.publicKey,
150
+ signature,
151
+ }),
152
+ ).toBe(true)
153
+ } finally {
154
+ Engine.reset('MlDsa44')
155
+ }
156
+ })
157
+
158
+ test('error: rejects malformed inputs', async () => {
159
+ const engine = await MlDsa44.engine()
160
+
161
+ for (const size of [0, 31, 33, 2560]) {
162
+ expect(() => engine.getPublicKey(new Uint8Array(size))).toThrowError(
163
+ `ML-DSA-44 private key must be 32 bytes, got ${size}`,
164
+ )
165
+ expect(() =>
166
+ engine.sign(payload, new Uint8Array(size), { extraEntropy: false }),
167
+ ).toThrowError(`ML-DSA-44 private key must be 32 bytes, got ${size}`)
168
+ }
169
+ expect(() =>
170
+ engine.sign(payload, seed, { extraEntropy: new Uint8Array(31) }),
171
+ ).toThrowError('ML-DSA-44 extra entropy must be 32 bytes, got 31')
172
+ expect(() =>
173
+ engine.sign(payload, seed, {
174
+ context: new Uint8Array(256),
175
+ extraEntropy: false,
176
+ }),
177
+ ).toThrowError('ML-DSA-44 context must be at most 255 bytes, got 256')
178
+ expect(() =>
179
+ engine.verify(
180
+ new Uint8Array(2420),
181
+ payload,
182
+ keys.publicKey.slice(0, 100),
183
+ {},
184
+ ),
185
+ ).toThrowError('ML-DSA-44 public key must be 1312 bytes, got 100')
186
+ expect(() =>
187
+ engine.verify(new Uint8Array(2420), payload, keys.publicKey, {
188
+ context: new Uint8Array(256),
189
+ }),
190
+ ).toThrowError('ML-DSA-44 context must be at most 255 bytes, got 256')
191
+ })
192
+ })
package/src/wasm/index.ts CHANGED
@@ -105,6 +105,28 @@ export * as Keystore from './Keystore.js'
105
105
  */
106
106
  export * as Kzg from './Kzg.js'
107
107
 
108
+ /**
109
+ * WASM implementations of Ox's supported ML-DSA-44 primitives, backed by
110
+ * [mldsa-native](https://github.com/pq-code-package/mldsa-native).
111
+ *
112
+ * This provider is installed by [`Engine.install`](/api/Engine/install), or
113
+ * can be installed individually.
114
+ *
115
+ * @example
116
+ * ```ts twoslash
117
+ * // @noErrors
118
+ * import { Engine } from 'ox'
119
+ * import { MlDsa44 } from 'ox/wasm'
120
+ *
121
+ * await Engine.install({ MlDsa44: MlDsa44.engine() })
122
+ *
123
+ * MlDsa44.getPublicKey({ privateKey: '0x...' })
124
+ * ```
125
+ *
126
+ * @category Post Quantum
127
+ */
128
+ export * as MlDsa44 from './MlDsa44.js'
129
+
108
130
  /**
109
131
  * WASM implementation of Ox's BIP-39 seed derivation.
110
132
  *
@@ -128,8 +150,8 @@ export * as Mnemonic from './Mnemonic.js'
128
150
  /**
129
151
  * WASM implementations of deterministic Secp256k1 primitives.
130
152
  *
131
- * This provider is opt-in because its embedded libsecp256k1 artifact is larger
132
- * than the modules installed by [`Engine.install`](/api/Engine/install).
153
+ * This provider is installed by [`Engine.install`](/api/Engine/install), or
154
+ * can be installed individually.
133
155
  *
134
156
  * @example
135
157
  * ```ts twoslash
@@ -0,0 +1,34 @@
1
+ import {
2
+ instantiate,
3
+ memoize,
4
+ type Module as WasmModule,
5
+ } from './instantiate.js'
6
+ import { wasmBase64 } from './mldsa44.wasm.js'
7
+
8
+ export type Exports = {
9
+ mldsa44_get_public_key(seed: number, publicKey: number): number
10
+ mldsa44_sign(
11
+ seed: number,
12
+ message: number,
13
+ messageSize: number,
14
+ context: number,
15
+ contextSize: number,
16
+ random: number,
17
+ signature: number,
18
+ ): number
19
+ mldsa44_verify(
20
+ signature: number,
21
+ message: number,
22
+ messageSize: number,
23
+ context: number,
24
+ contextSize: number,
25
+ publicKey: number,
26
+ ): number
27
+ zero(pointer: number, length: number): void
28
+ }
29
+
30
+ /** Instantiated ML-DSA-44 module. */
31
+ export type Module = WasmModule<Exports>
32
+
33
+ /** Instantiates the ML-DSA-44 module once. */
34
+ export const load = memoize(() => instantiate<Exports>(wasmBase64))