@maatara/core-pqc 0.2.3 → 0.3.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/README.md CHANGED
@@ -1,196 +1,196 @@
1
- # Ma'atara Core PQC Toolkit
2
- A portable, audited Post-Quantum Cryptography toolkit for browser and Node.js environments.
3
-
4
- ## Features
5
- # Ma'atara Core PQC Toolkit
6
-
7
- A portable, audited Post-Quantum Cryptography toolkit for browser and Node.js. Includes deterministic helpers for governance multisig and NFT/art token flows.
8
-
9
- ## Features
10
-
11
- - Kyber ML-KEM-768 (key encapsulation)
12
- - Dilithium2 (sign/verify)
13
- - HKDF-SHA256 (key derivation)
14
- - AES-256-GCM (AEAD)
15
- - Base64url helpers and constant‑time compare
16
- - Deterministic preimage builders (JCS) for governance, mint, transfer, and anchor
17
- - Royalty validation and multisig attestation verification
18
-
19
- ## Install
20
-
21
- ```bash
22
- npm install @maatara/core-pqc
23
- ```
24
-
25
- Initialize WASM once on startup:
26
-
27
- ```ts
28
- import { initWasm } from '@maatara/core-pqc';
29
- await initWasm();
30
- ```
31
-
32
- ## Core PQC usage
33
-
34
- ```ts
35
- import {
36
- kyberKeygen, kyberEncaps, kyberDecaps,
37
- dilithiumKeygen, dilithiumSign, dilithiumVerify,
38
- hkdfSha256, aesGcmWrap, aesGcmUnwrap, b64uEncode
39
- } from '@maatara/core-pqc';
40
-
41
- await initWasm();
42
-
43
- // Kyber
44
- const { public_b64u, secret_b64u } = await kyberKeygen();
45
- const { kem_ct_b64u, shared_b64u } = await kyberEncaps(public_b64u);
46
- const { shared_b64u: shared2 } = await kyberDecaps(secret_b64u, kem_ct_b64u);
47
-
48
- // Dilithium
49
- const signKeys = await dilithiumKeygen();
50
- const msg = b64uEncode(new TextEncoder().encode('hello pqc'));
51
- const sig = await dilithiumSign(msg, signKeys.secret_b64u);
52
- const ok = await dilithiumVerify(msg, sig.signature_b64u, signKeys.public_b64u);
53
- ```
54
-
55
- ## Deterministic helpers (multisig and NFT)
56
-
57
- All helpers return canonical JSON and a base64url message suitable for signing with Dilithium2.
58
-
59
- ```ts
60
- import {
61
- jcsCanonicalize,
62
- buildPolicyPreimage,
63
- buildMintPreimage,
64
- buildTransferPreimage,
65
- buildAnchorPreimage,
66
- validateRoyalty,
67
- verifyAttestations,
68
- dilithiumSign
69
- } from '@maatara/core-pqc';
70
-
71
- // 1) Governance policy (multisig)
72
- const policy = { version: 'v1', threshold: 2, signers: [{ id: 'a', publicKeyB64u: '...' }, { id: 'b', publicKeyB64u: '...' }] };
73
- const { canonical: govCanon, msg_b64u: govMsg } = await buildPolicyPreimage(policy);
74
- const att = await dilithiumSign(govMsg!, signerSecret);
75
-
76
- // 2) Mint preimage (royalty optional)
77
- const header = { createdAt: Date.now(), royalty: { receiver: '0xabc...', bps: 500 } };
78
- const asset = { id: 'uuid', sha256: '...', mediaType: 'image/png' };
79
- if (!await validateRoyalty(header.royalty.receiver, header.royalty.bps)) throw new Error('Bad royalty');
80
- const { msg_b64u: mintMsg } = await buildMintPreimage(header, asset);
81
- const mintSig = await dilithiumSign(mintMsg!, signerSecret);
82
-
83
- // 3) Transfer preimage
84
- const { msg_b64u: xferMsg } = await buildTransferPreimage({ assetId: 'uuid', to: 'did:key:...' });
85
- const xferSig = await dilithiumSign(xferMsg!, signerSecret);
86
-
87
- // 4) Anchor preimage (Merkle root + chains)
88
- const { msg_b64u: anchorMsg } = await buildAnchorPreimage('user-123', 'deadbeef...', '2025-01', { apf: ['root1','root2'] });
89
-
90
- // Verify a set of governance attestations for a canonical message
91
- const validCount = await verifyAttestations(govMsg!, [
92
- { alg: 'dilithium2', publicKeyB64u: signerPub1, signatureB64u: sig1.signature_b64u },
93
- { alg: 'dilithium2', publicKeyB64u: signerPub2, signatureB64u: sig2.signature_b64u }
94
- ], [signerPub1, signerPub2]);
95
- ```
96
-
97
- ## Notes app integration (independent repo)
98
-
99
- Use the deterministic helpers to prepare messages client‑side, sign, and POST to the Core Worker endpoints.
100
-
101
- ```ts
102
- import {
103
- initWasm,
104
- buildMintPreimage,
105
- dilithiumSign,
106
- verifyAttestations
107
- } from '@maatara/core-pqc';
108
-
109
- await initWasm();
110
-
111
- // Build preimage and sign
112
- const { msg_b64u } = await buildMintPreimage(header, asset);
113
- const sig = await dilithiumSign(msg_b64u!, userSecret_b64u);
114
- const attestation = { alg: 'dilithium2', publicKeyB64u: userPublic_b64u, signatureB64u: sig.signature_b64u };
115
-
116
- // Optionally validate locally before submit
117
- const valid = await verifyAttestations(msg_b64u!, [attestation], [userPublic_b64u]);
118
- if (!valid) throw new Error('Attestation failed');
119
-
120
- // Submit to Core
121
- await fetch(`${CORE_URL}/api/assets/mint`, {
122
- method: 'POST',
123
- headers: { 'content-type': 'application/json' },
124
- body: JSON.stringify({ header, asset, attestations: [attestation] })
125
- });
126
- ```
127
-
128
- ## Build from source
129
-
130
- ```bash
131
- # Build npm wrapper
132
- cd packages/core-pqc
133
- npm run build
134
- ```
135
-
136
- WASM bindings live in `@maatara/core-pqc-wasm` (Rust + wasm-bindgen). This package consumes those exports and provides a browser/Node‑friendly API.
137
-
138
- ## Security notes
139
-
140
- - Zero‑knowledge: server never receives user secrets/DEKs
141
- - Post‑quantum primitives (Kyber, Dilithium)
142
- - Constant‑time operations where applicable
143
- - Deterministic JSON canonicalization (JCS‑like) for signature stability
144
-
145
- ## License
146
-
147
- Apache‑2.0
148
-
149
- ## Veritas Block Preimage (v2 JCS)
150
-
151
- Blocks (Veritas chain) now use a canonical JCS JSON preimage for signatures (version 2). Legacy version 1 used a plain JSON.stringify of an object with insertion ordering. The new helper ensures deterministic ordering consistent with governance/asset preimages.
152
-
153
- Helper:
154
- ```ts
155
- import { buildBlockPreimage } from '@maatara/core-pqc';
156
-
157
- const { canonical, msg_b64u } = await buildBlockPreimage({
158
- index: block.index.toString(), // string (bigint safe)
159
- timestamp: block.timestamp.toString(),
160
- previousHash: block.previousHash,
161
- dataHash: block.dataHash,
162
- metadataHash: block.metadataHash,
163
- signatureAlg: 'dilithium2',
164
- ownerPublicKey: dilithiumPublicKeyB64u,
165
- contentType: block.contentType || 'application/json',
166
- version: 2,
167
- });
168
- // Sign msg_b64u with Dilithium secret
169
- ```
170
-
171
- Canonical object fields included (lexicographically ordered by JCS):
172
- - contentType
173
- - dataHash
174
- - index (string)
175
- - metadataHash
176
- - ownerPublicKey (Dilithium public key, base64url)
177
- - previousHash
178
- - signatureAlg
179
- - timestamp (string)
180
- - version
181
-
182
- Excluded: signature, encryptionKeyHash, any transient fields.
183
-
184
- Message to sign: `base64url( UTF-8(canonical JSON) )`. No hashing layer (if you need prehash use Dilithium pre-hash mode externally before signature—current flow signs raw canonical bytes).
185
-
186
- ### Migration Notes (v1 -> v2)
187
- - Existing blocks signed under legacy scheme still verify: server falls back to legacy ordering if JCS verification fails.
188
- - New blocks should set `version: 2` and use Dilithium key for `ownerPublicKey` (not the userId hash) to assert authorship.
189
- - Enable server debug logging of preimage by setting env `DEBUG_PREIMAGE=1`.
190
- - Future breaking change removal: once all clients migrate, legacy fallback can be removed.
191
-
192
- ### Client Checklist
193
- 1. Always stringify bigint counters to decimal strings before building the preimage.
194
- 2. Provide the true Dilithium public key as `ownerPublicKey`.
195
- 3. Do not inject extra fields; they will be ignored (and change canonical ordering if you wrongly sign them locally).
196
- 4. Ensure base64url is unpadded (`-` / `_` substitutions). The helper already does this.
1
+ # Ma'atara Core PQC Toolkit
2
+ A portable, audited Post-Quantum Cryptography toolkit for browser and Node.js environments.
3
+
4
+ ## Features
5
+ # Ma'atara Core PQC Toolkit
6
+
7
+ A portable, audited Post-Quantum Cryptography toolkit for browser and Node.js. Includes deterministic helpers for governance multisig and NFT/art token flows.
8
+
9
+ ## Features
10
+
11
+ - Kyber ML-KEM-768 (key encapsulation)
12
+ - Dilithium2 (sign/verify)
13
+ - HKDF-SHA256 (key derivation)
14
+ - AES-256-GCM (AEAD)
15
+ - Base64url helpers and constant‑time compare
16
+ - Deterministic preimage builders (JCS) for governance, mint, transfer, and anchor
17
+ - Royalty validation and multisig attestation verification
18
+
19
+ ## Install
20
+
21
+ ```bash
22
+ npm install @maatara/core-pqc
23
+ ```
24
+
25
+ Initialize WASM once on startup:
26
+
27
+ ```ts
28
+ import { initWasm } from '@maatara/core-pqc';
29
+ await initWasm();
30
+ ```
31
+
32
+ ## Core PQC usage
33
+
34
+ ```ts
35
+ import {
36
+ kyberKeygen, kyberEncaps, kyberDecaps,
37
+ dilithiumKeygen, dilithiumSign, dilithiumVerify,
38
+ hkdfSha256, aesGcmWrap, aesGcmUnwrap, b64uEncode
39
+ } from '@maatara/core-pqc';
40
+
41
+ await initWasm();
42
+
43
+ // Kyber
44
+ const { public_b64u, secret_b64u } = await kyberKeygen();
45
+ const { kem_ct_b64u, shared_b64u } = await kyberEncaps(public_b64u);
46
+ const { shared_b64u: shared2 } = await kyberDecaps(secret_b64u, kem_ct_b64u);
47
+
48
+ // Dilithium
49
+ const signKeys = await dilithiumKeygen();
50
+ const msg = b64uEncode(new TextEncoder().encode('hello pqc'));
51
+ const sig = await dilithiumSign(msg, signKeys.secret_b64u);
52
+ const ok = await dilithiumVerify(msg, sig.signature_b64u, signKeys.public_b64u);
53
+ ```
54
+
55
+ ## Deterministic helpers (multisig and NFT)
56
+
57
+ All helpers return canonical JSON and a base64url message suitable for signing with Dilithium2.
58
+
59
+ ```ts
60
+ import {
61
+ jcsCanonicalize,
62
+ buildPolicyPreimage,
63
+ buildMintPreimage,
64
+ buildTransferPreimage,
65
+ buildAnchorPreimage,
66
+ validateRoyalty,
67
+ verifyAttestations,
68
+ dilithiumSign
69
+ } from '@maatara/core-pqc';
70
+
71
+ // 1) Governance policy (multisig)
72
+ const policy = { version: 'v1', threshold: 2, signers: [{ id: 'a', publicKeyB64u: '...' }, { id: 'b', publicKeyB64u: '...' }] };
73
+ const { canonical: govCanon, msg_b64u: govMsg } = await buildPolicyPreimage(policy);
74
+ const att = await dilithiumSign(govMsg!, signerSecret);
75
+
76
+ // 2) Mint preimage (royalty optional)
77
+ const header = { createdAt: Date.now(), royalty: { receiver: '0xabc...', bps: 500 } };
78
+ const asset = { id: 'uuid', sha256: '...', mediaType: 'image/png' };
79
+ if (!await validateRoyalty(header.royalty.receiver, header.royalty.bps)) throw new Error('Bad royalty');
80
+ const { msg_b64u: mintMsg } = await buildMintPreimage(header, asset);
81
+ const mintSig = await dilithiumSign(mintMsg!, signerSecret);
82
+
83
+ // 3) Transfer preimage
84
+ const { msg_b64u: xferMsg } = await buildTransferPreimage({ assetId: 'uuid', to: 'did:key:...' });
85
+ const xferSig = await dilithiumSign(xferMsg!, signerSecret);
86
+
87
+ // 4) Anchor preimage (Merkle root + chains)
88
+ const { msg_b64u: anchorMsg } = await buildAnchorPreimage('user-123', 'deadbeef...', '2025-01', { apf: ['root1','root2'] });
89
+
90
+ // Verify a set of governance attestations for a canonical message
91
+ const validCount = await verifyAttestations(govMsg!, [
92
+ { alg: 'dilithium2', publicKeyB64u: signerPub1, signatureB64u: sig1.signature_b64u },
93
+ { alg: 'dilithium2', publicKeyB64u: signerPub2, signatureB64u: sig2.signature_b64u }
94
+ ], [signerPub1, signerPub2]);
95
+ ```
96
+
97
+ ## Notes app integration (independent repo)
98
+
99
+ Use the deterministic helpers to prepare messages client‑side, sign, and POST to the Core Worker endpoints.
100
+
101
+ ```ts
102
+ import {
103
+ initWasm,
104
+ buildMintPreimage,
105
+ dilithiumSign,
106
+ verifyAttestations
107
+ } from '@maatara/core-pqc';
108
+
109
+ await initWasm();
110
+
111
+ // Build preimage and sign
112
+ const { msg_b64u } = await buildMintPreimage(header, asset);
113
+ const sig = await dilithiumSign(msg_b64u!, userSecret_b64u);
114
+ const attestation = { alg: 'dilithium2', publicKeyB64u: userPublic_b64u, signatureB64u: sig.signature_b64u };
115
+
116
+ // Optionally validate locally before submit
117
+ const valid = await verifyAttestations(msg_b64u!, [attestation], [userPublic_b64u]);
118
+ if (!valid) throw new Error('Attestation failed');
119
+
120
+ // Submit to Core
121
+ await fetch(`${CORE_URL}/api/assets/mint`, {
122
+ method: 'POST',
123
+ headers: { 'content-type': 'application/json' },
124
+ body: JSON.stringify({ header, asset, attestations: [attestation] })
125
+ });
126
+ ```
127
+
128
+ ## Build from source
129
+
130
+ ```bash
131
+ # Build npm wrapper
132
+ cd packages/core-pqc
133
+ npm run build
134
+ ```
135
+
136
+ WASM bindings live in `@maatara/core-pqc-wasm` (Rust + wasm-bindgen). This package consumes those exports and provides a browser/Node‑friendly API.
137
+
138
+ ## Security notes
139
+
140
+ - Zero‑knowledge: server never receives user secrets/DEKs
141
+ - Post‑quantum primitives (Kyber, Dilithium)
142
+ - Constant‑time operations where applicable
143
+ - Deterministic JSON canonicalization (JCS‑like) for signature stability
144
+
145
+ ## License
146
+
147
+ Apache‑2.0
148
+
149
+ ## Veritas Block Preimage (v2 JCS)
150
+
151
+ Blocks (Veritas chain) now use a canonical JCS JSON preimage for signatures (version 2). Legacy version 1 used a plain JSON.stringify of an object with insertion ordering. The new helper ensures deterministic ordering consistent with governance/asset preimages.
152
+
153
+ Helper:
154
+ ```ts
155
+ import { buildBlockPreimage } from '@maatara/core-pqc';
156
+
157
+ const { canonical, msg_b64u } = await buildBlockPreimage({
158
+ index: block.index.toString(), // string (bigint safe)
159
+ timestamp: block.timestamp.toString(),
160
+ previousHash: block.previousHash,
161
+ dataHash: block.dataHash,
162
+ metadataHash: block.metadataHash,
163
+ signatureAlg: 'dilithium2',
164
+ ownerPublicKey: dilithiumPublicKeyB64u,
165
+ contentType: block.contentType || 'application/json',
166
+ version: 2,
167
+ });
168
+ // Sign msg_b64u with Dilithium secret
169
+ ```
170
+
171
+ Canonical object fields included (lexicographically ordered by JCS):
172
+ - contentType
173
+ - dataHash
174
+ - index (string)
175
+ - metadataHash
176
+ - ownerPublicKey (Dilithium public key, base64url)
177
+ - previousHash
178
+ - signatureAlg
179
+ - timestamp (string)
180
+ - version
181
+
182
+ Excluded: signature, encryptionKeyHash, any transient fields.
183
+
184
+ Message to sign: `base64url( UTF-8(canonical JSON) )`. No hashing layer (if you need prehash use Dilithium pre-hash mode externally before signature—current flow signs raw canonical bytes).
185
+
186
+ ### Migration Notes (v1 -> v2)
187
+ - Existing blocks signed under legacy scheme still verify: server falls back to legacy ordering if JCS verification fails.
188
+ - New blocks should set `version: 2` and use Dilithium key for `ownerPublicKey` (not the userId hash) to assert authorship.
189
+ - Enable server debug logging of preimage by setting env `DEBUG_PREIMAGE=1`.
190
+ - Future breaking change removal: once all clients migrate, legacy fallback can be removed.
191
+
192
+ ### Client Checklist
193
+ 1. Always stringify bigint counters to decimal strings before building the preimage.
194
+ 2. Provide the true Dilithium public key as `ownerPublicKey`.
195
+ 3. Do not inject extra fields; they will be ignored (and change canonical ordering if you wrongly sign them locally).
196
+ 4. Ensure base64url is unpadded (`-` / `_` substitutions). The helper already does this.
package/dist/index.js CHANGED
@@ -23,7 +23,20 @@ var wasmPkg__namespace = /*#__PURE__*/_interopNamespaceDefault(wasmPkg);
23
23
 
24
24
  // Ma'atara Core PQC Toolkit
25
25
  // Post-Quantum Cryptography for Browser and Node.js
26
- // Import WASM functions (namespace import for better bundler compatibility)
26
+ //
27
+ // SECURITY NOTICE: This library implements FIPS 204 (ML-DSA) and FIPS 203 (ML-KEM)
28
+ // post-quantum cryptographic algorithms. Key material should be:
29
+ // 1. Stored securely (encrypted at rest)
30
+ // 2. Transmitted only over secure channels
31
+ // 3. Zeroized after use when possible
32
+ // 4. Protected by hardware security modules for high-value operations
33
+ //
34
+ // See docs/WASM_SECURITY_MODEL.md for browser-specific security considerations.
35
+ // ============================================================================
36
+ // SECURITY CONSTANTS
37
+ // ============================================================================
38
+ const MAX_MESSAGE_SIZE = 16 * 1024 * 1024; // 16MB - prevent memory exhaustion
39
+ const MAX_KEY_SIZE = 1024 * 1024; // 1MB - reasonable for PQC keys
27
40
  const wasm_kyber_keygen = wasmPkg__namespace.kyber_keygen;
28
41
  const wasm_kyber_encaps = wasmPkg__namespace.kyber_encaps;
29
42
  const wasm_kyber_decaps = wasmPkg__namespace.kyber_decaps;
@@ -102,6 +115,11 @@ async function aesGcmUnwrap(keyB64u, ivB64u, ctB64u, aadB64u) {
102
115
  return result;
103
116
  }
104
117
  // Dilithium functions
118
+ /**
119
+ * Generate a new ML-DSA-65 (Dilithium) keypair.
120
+ * SECURITY: Keys should be stored encrypted at rest.
121
+ * For high-value operations, consider hardware security modules.
122
+ */
105
123
  async function dilithiumKeygen() {
106
124
  await initWasm();
107
125
  const result = JSON.parse(wasm_dilithium_keygen());
@@ -109,14 +127,40 @@ async function dilithiumKeygen() {
109
127
  throw new Error(result.error);
110
128
  return result;
111
129
  }
130
+ /**
131
+ * Sign a message using ML-DSA-65 (Dilithium).
132
+ * @param messageB64u - Base64url-encoded message to sign
133
+ * @param secretB64u - Base64url-encoded secret key
134
+ * SECURITY: Secret key should be zeroized after use if possible.
135
+ */
112
136
  async function dilithiumSign(messageB64u, secretB64u) {
137
+ // Input validation - prevent memory exhaustion attacks
138
+ if (messageB64u.length > MAX_MESSAGE_SIZE) {
139
+ throw new Error(`Message exceeds maximum size of ${MAX_MESSAGE_SIZE} bytes`);
140
+ }
141
+ if (secretB64u.length > MAX_KEY_SIZE) {
142
+ throw new Error(`Secret key exceeds maximum size of ${MAX_KEY_SIZE} bytes`);
143
+ }
113
144
  await initWasm();
114
145
  const result = JSON.parse(wasm_dilithium_sign(messageB64u, secretB64u));
115
146
  if (result.error)
116
147
  throw new Error(result.error);
117
148
  return result;
118
149
  }
150
+ /**
151
+ * Verify a ML-DSA-65 (Dilithium) signature.
152
+ * @param messageB64u - Base64url-encoded message
153
+ * @param signatureB64u - Base64url-encoded signature
154
+ * @param publicB64u - Base64url-encoded public key
155
+ */
119
156
  async function dilithiumVerify(messageB64u, signatureB64u, publicB64u) {
157
+ // Input validation - prevent memory exhaustion attacks
158
+ if (messageB64u.length > MAX_MESSAGE_SIZE) {
159
+ throw new Error(`Message exceeds maximum size of ${MAX_MESSAGE_SIZE} bytes`);
160
+ }
161
+ if (signatureB64u.length > MAX_KEY_SIZE || publicB64u.length > MAX_KEY_SIZE) {
162
+ throw new Error(`Signature or public key exceeds maximum size`);
163
+ }
120
164
  await initWasm();
121
165
  const result = JSON.parse(wasm_dilithium_verify(messageB64u, signatureB64u, publicB64u));
122
166
  if (result.error)
@@ -241,6 +285,177 @@ function constantTimeEqual(a, b) {
241
285
  }
242
286
  return result === 0;
243
287
  }
288
+ /**
289
+ * Create an attestation for a threshold signature operation.
290
+ */
291
+ async function createAttestation(operation, signerKeys, expiresInSeconds = 3600) {
292
+ await initWasm();
293
+ // Build deterministic preimage for the operation
294
+ const preimage = await jcsCanonicalize({ type: operation.type, payload: operation.payload });
295
+ const preimageBytes = new TextEncoder().encode(preimage);
296
+ const operationHash = await sha256B64u(preimageBytes);
297
+ const msgB64u = b64uEncode(preimageBytes);
298
+ // Sign the preimage
299
+ const sig = await dilithiumSign(msgB64u, signerKeys.secretKeyB64u);
300
+ return {
301
+ signerKeyId: signerKeys.keyId,
302
+ signerPublicKeyB64u: signerKeys.publicKeyB64u,
303
+ operationHash,
304
+ operationType: operation.type,
305
+ attestedAt: Date.now(),
306
+ expiresAt: Date.now() + expiresInSeconds * 1000,
307
+ signatureAlg: 'dilithium2',
308
+ signatureB64u: sig.signature_b64u
309
+ };
310
+ }
311
+ /**
312
+ * Verify an attestation signature.
313
+ */
314
+ async function verifyAttestation(attestation, operation) {
315
+ await initWasm();
316
+ // Rebuild expected operation hash
317
+ const preimage = await jcsCanonicalize({ type: operation.type, payload: operation.payload });
318
+ const preimageBytes = new TextEncoder().encode(preimage);
319
+ const expectedHash = await sha256B64u(preimageBytes);
320
+ if (attestation.operationHash !== expectedHash) {
321
+ return { valid: false, error: 'Operation hash mismatch' };
322
+ }
323
+ // Check expiry
324
+ if (attestation.expiresAt < Date.now()) {
325
+ return { valid: false, error: 'Attestation expired' };
326
+ }
327
+ // Verify signature
328
+ const msgB64u = b64uEncode(preimageBytes);
329
+ const result = await dilithiumVerify(msgB64u, attestation.signatureB64u, attestation.signerPublicKeyB64u);
330
+ if (!result.is_valid) {
331
+ return { valid: false, error: 'Invalid signature' };
332
+ }
333
+ return { valid: true };
334
+ }
335
+ /**
336
+ * Verify that an aggregated signature meets the threshold policy.
337
+ */
338
+ async function verifyThreshold(aggregated, operation, policy) {
339
+ const errors = [];
340
+ // Check policy match
341
+ if (aggregated.policyId !== policy.policyId) {
342
+ errors.push('Policy ID mismatch');
343
+ }
344
+ // Verify each attestation and compute weight
345
+ let validWeight = 0;
346
+ for (const attestation of aggregated.attestations) {
347
+ const signer = policy.signers.find(s => s.publicKeyB64u === attestation.signerPublicKeyB64u);
348
+ if (!signer) {
349
+ errors.push(`Signer ${attestation.signerKeyId} not in policy`);
350
+ continue;
351
+ }
352
+ const result = await verifyAttestation(attestation, operation);
353
+ if (!result.valid) {
354
+ errors.push(`Attestation from ${attestation.signerKeyId}: ${result.error}`);
355
+ continue;
356
+ }
357
+ validWeight += signer.weight;
358
+ }
359
+ // Check threshold
360
+ if (validWeight < policy.threshold) {
361
+ errors.push(`Threshold not met: weight ${validWeight} < required ${policy.threshold}`);
362
+ }
363
+ return { valid: errors.length === 0, errors };
364
+ }
365
+ /**
366
+ * Create a provenance commitment for an asset before public disclosure.
367
+ * This establishes a timestamped claim without revealing the asset content.
368
+ */
369
+ async function createProvenanceCommitment(assetBytes, keys) {
370
+ await initWasm();
371
+ // Generate random salt
372
+ const salt = crypto.getRandomValues(new Uint8Array(32));
373
+ // Compute commitment hash = SHA256(asset || salt || publicKey)
374
+ const publicKeyBytes = b64uDecode(keys.publicKeyB64u);
375
+ const preimage = new Uint8Array(assetBytes.length + salt.length + publicKeyBytes.length);
376
+ preimage.set(assetBytes, 0);
377
+ preimage.set(salt, assetBytes.length);
378
+ preimage.set(publicKeyBytes, assetBytes.length + salt.length);
379
+ const commitmentHash = await sha256B64u(preimage);
380
+ const assetHash = await sha256B64u(assetBytes);
381
+ // Build commitment object (without signature)
382
+ const commitmentData = {
383
+ type: 'ProvenanceCommitment',
384
+ version: 1,
385
+ commitmentHash,
386
+ creatorPublicKey: keys.publicKeyB64u,
387
+ timestamp: Date.now()
388
+ };
389
+ // Sign the commitment
390
+ const canonical = await jcsCanonicalize(commitmentData);
391
+ const sig = await dilithiumSign(b64uEncode(new TextEncoder().encode(canonical)), keys.secretKeyB64u);
392
+ const commitment = {
393
+ ...commitmentData,
394
+ signature: sig.signature_b64u
395
+ };
396
+ const reveal = {
397
+ commitmentHash,
398
+ salt: b64uEncode(salt),
399
+ assetHash
400
+ };
401
+ return { commitment, reveal };
402
+ }
403
+ /**
404
+ * Verify a provenance commitment signature.
405
+ */
406
+ async function verifyProvenanceCommitment(commitment) {
407
+ await initWasm();
408
+ // Reconstruct the signed data (without signature field)
409
+ const commitmentData = {
410
+ type: commitment.type,
411
+ version: commitment.version,
412
+ commitmentHash: commitment.commitmentHash,
413
+ creatorPublicKey: commitment.creatorPublicKey,
414
+ timestamp: commitment.timestamp
415
+ };
416
+ const canonical = await jcsCanonicalize(commitmentData);
417
+ const msgB64u = b64uEncode(new TextEncoder().encode(canonical));
418
+ const result = await dilithiumVerify(msgB64u, commitment.signature, commitment.creatorPublicKey);
419
+ if (!result.is_valid) {
420
+ return { valid: false, error: 'Invalid commitment signature' };
421
+ }
422
+ return { valid: true };
423
+ }
424
+ /**
425
+ * Verify that a revealed asset matches a commitment.
426
+ */
427
+ async function verifyCommitmentReveal(commitment, assetBytes, reveal) {
428
+ await initWasm();
429
+ // First verify the commitment signature
430
+ const sigResult = await verifyProvenanceCommitment(commitment);
431
+ if (!sigResult.valid) {
432
+ return { valid: false, error: `Commitment signature invalid: ${sigResult.error}` };
433
+ }
434
+ // Verify asset hash matches
435
+ const computedAssetHash = await sha256B64u(assetBytes);
436
+ if (computedAssetHash !== reveal.assetHash) {
437
+ return { valid: false, error: 'Asset hash does not match reveal' };
438
+ }
439
+ // Reconstruct commitment hash
440
+ const salt = b64uDecode(reveal.salt);
441
+ const publicKeyBytes = b64uDecode(commitment.creatorPublicKey);
442
+ const preimage = new Uint8Array(assetBytes.length + salt.length + publicKeyBytes.length);
443
+ preimage.set(assetBytes, 0);
444
+ preimage.set(salt, assetBytes.length);
445
+ preimage.set(publicKeyBytes, assetBytes.length + salt.length);
446
+ const computedCommitmentHash = await sha256B64u(preimage);
447
+ if (computedCommitmentHash !== commitment.commitmentHash) {
448
+ return { valid: false, error: 'Commitment hash does not match reveal' };
449
+ }
450
+ return { valid: true };
451
+ }
452
+ // ============================================================================
453
+ // HELPER: SHA-256 with base64url output
454
+ // ============================================================================
455
+ async function sha256B64u(data) {
456
+ const hashBuffer = await crypto.subtle.digest('SHA-256', data);
457
+ return b64uEncode(new Uint8Array(hashBuffer));
458
+ }
244
459
 
245
460
  exports.aesGcmUnwrap = aesGcmUnwrap;
246
461
  exports.aesGcmWrap = aesGcmWrap;
@@ -252,6 +467,8 @@ exports.buildMintPreimage = buildMintPreimage;
252
467
  exports.buildPolicyPreimage = buildPolicyPreimage;
253
468
  exports.buildTransferPreimage = buildTransferPreimage;
254
469
  exports.constantTimeEqual = constantTimeEqual;
470
+ exports.createAttestation = createAttestation;
471
+ exports.createProvenanceCommitment = createProvenanceCommitment;
255
472
  exports.dilithiumKeygen = dilithiumKeygen;
256
473
  exports.dilithiumSign = dilithiumSign;
257
474
  exports.dilithiumVerify = dilithiumVerify;
@@ -262,5 +479,9 @@ exports.kyberDecaps = kyberDecaps;
262
479
  exports.kyberEncaps = kyberEncaps;
263
480
  exports.kyberKeygen = kyberKeygen;
264
481
  exports.validateRoyalty = validateRoyalty;
482
+ exports.verifyAttestation = verifyAttestation;
265
483
  exports.verifyAttestations = verifyAttestations;
484
+ exports.verifyCommitmentReveal = verifyCommitmentReveal;
485
+ exports.verifyProvenanceCommitment = verifyProvenanceCommitment;
486
+ exports.verifyThreshold = verifyThreshold;
266
487
  //# sourceMappingURL=index.js.map