xoonya 3.0.1 → 3.1.1
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/.env.example +8 -0
- package/README.md +107 -1
- package/RELEASE_NOTES.md +12 -9
- package/SERVICE_CONTRACT.md +12 -0
- package/START_HERE.md +9 -0
- package/XOONYA_NATIVE_PROOF_FORMAT.md +52 -9
- package/index.js +12 -3
- package/lib/adapter_certification_contract.js +90 -0
- package/lib/authority_binding_lifecycle_contract.js +182 -0
- package/lib/crypto_agility_matrix.js +125 -0
- package/lib/machine_identity_registry_contract.js +199 -0
- package/lib/offline_verifiability_recovery_contract.js +155 -0
- package/lib/operational_receipt_contract.js +165 -0
- package/lib/public_surface_policy_contract.js +111 -0
- package/lib/signed_audit_provenance_contract.js +93 -0
- package/lib/trust_portability_boundary_contract.js +66 -0
- package/package.json +5 -2
- package/proof_sdk.js +327 -31
- package/reference/python/__pycache__/xnya_reference.cpython-314.pyc +0 -0
- package/scripts/verify_control_plane_contracts.js +34 -0
- package/scripts/verify_control_plane_routes.js +70 -0
- package/scripts/verify_forgery_resistance.js +281 -0
- package/scripts/verify_service.js +98 -0
- package/server.js +242 -16
|
@@ -0,0 +1,281 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
// Forgery-resistance checks for the xoonya proof engine.
|
|
4
|
+
//
|
|
5
|
+
// This script talks to proof_sdk.js directly (no HTTP server) and pins down:
|
|
6
|
+
// (a) key substitution: a proof signed by key B that claims key A's identity
|
|
7
|
+
// self-verifies WITHOUT a trust root (known limitation, documented here)
|
|
8
|
+
// and is rejected WITH a trust root pinned to key A;
|
|
9
|
+
// (b) external methods are structural-only and fail under strict mode;
|
|
10
|
+
// (c) the default digest method is digest-only and fails under strict mode;
|
|
11
|
+
// (d) hybrid ed25519+ml-dsa-65: tampered PQ signature is rejected, and a
|
|
12
|
+
// wrong pinned PQ ref yields native_hybrid_public_key_not_trusted;
|
|
13
|
+
// (e) results are deterministic across repeated runs for the same inputs.
|
|
14
|
+
|
|
15
|
+
const assert = require("assert");
|
|
16
|
+
const crypto = require("crypto");
|
|
17
|
+
const sdk = require("../proof_sdk.js");
|
|
18
|
+
|
|
19
|
+
function canonicalize(value) {
|
|
20
|
+
if (Array.isArray(value)) {
|
|
21
|
+
return `[${value.map((item) => canonicalize(item)).join(",")}]`;
|
|
22
|
+
}
|
|
23
|
+
if (value && typeof value === "object") {
|
|
24
|
+
const keys = Object.keys(value).sort();
|
|
25
|
+
return `{${keys.map((key) => `${JSON.stringify(key)}:${canonicalize(value[key])}`).join(",")}}`;
|
|
26
|
+
}
|
|
27
|
+
return JSON.stringify(value);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function pem(key, type) {
|
|
31
|
+
return key.export({ type, format: "pem" });
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function tamperBase64url(value) {
|
|
35
|
+
const last = value.slice(-1);
|
|
36
|
+
return `${value.slice(0, -1)}${last === "A" ? "B" : "A"}`;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const event = {
|
|
40
|
+
version: "canonical-event-bundle/v1",
|
|
41
|
+
domain_id: "forgery.resistance",
|
|
42
|
+
sequence: "1",
|
|
43
|
+
actor_id: "alice",
|
|
44
|
+
adapter_source: "verify_forgery_resistance",
|
|
45
|
+
event_type: "proof.forgery.probe",
|
|
46
|
+
event_hash: "cd".repeat(32)
|
|
47
|
+
};
|
|
48
|
+
const ALICE_IDENTITY_REF = "did:xoonya:alice";
|
|
49
|
+
|
|
50
|
+
function signedReceiptFor(keyPair) {
|
|
51
|
+
return sdk.emitReceipt(event, {
|
|
52
|
+
agent_identity: "alice",
|
|
53
|
+
algorithm: "xoonya-native-signed-v1",
|
|
54
|
+
identity_ref: ALICE_IDENTITY_REF,
|
|
55
|
+
signature_suite: "ed25519",
|
|
56
|
+
private_key_pem: pem(keyPair.privateKey, "pkcs8"),
|
|
57
|
+
public_key_pem: pem(keyPair.publicKey, "spki")
|
|
58
|
+
});
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function bundleFor(receipt) {
|
|
62
|
+
return sdk.buildProofBundle({ ceb: event, receipt });
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function checkKeySubstitution() {
|
|
66
|
+
const keyA = crypto.generateKeyPairSync("ed25519");
|
|
67
|
+
const keyB = crypto.generateKeyPairSync("ed25519");
|
|
68
|
+
const receiptA = signedReceiptFor(keyA);
|
|
69
|
+
const receiptB = signedReceiptFor(keyB);
|
|
70
|
+
const refA = receiptA.native_proof.public_key_ref;
|
|
71
|
+
const refB = receiptB.native_proof.public_key_ref;
|
|
72
|
+
assert.notStrictEqual(refA, refB, "distinct keys must produce distinct public_key_ref values");
|
|
73
|
+
assert.strictEqual(receiptB.native_proof.identity_ref, ALICE_IDENTITY_REF, "key B proof claims alice's identity_ref");
|
|
74
|
+
|
|
75
|
+
// Known limitation: without a verifier-supplied trust root, public_key_ref is only
|
|
76
|
+
// self-bound to the carried key material, so key B can claim alice's identity.
|
|
77
|
+
const substitutionWithoutRoot = sdk.verifyProofBundle(bundleFor(receiptB));
|
|
78
|
+
assert.strictEqual(substitutionWithoutRoot.valid, true, "key substitution self-verifies without a trust root (documented limitation)");
|
|
79
|
+
assert.strictEqual(substitutionWithoutRoot.verification_class, "cryptographic", "native signed class must be cryptographic");
|
|
80
|
+
assert.strictEqual(substitutionWithoutRoot.cryptographically_verified, true, "native signed proof must report cryptographic verification");
|
|
81
|
+
assert.strictEqual(substitutionWithoutRoot.trust_root_applied, false, "trust_root_applied must be false without a trust root");
|
|
82
|
+
assert.strictEqual(substitutionWithoutRoot.public_key_ref, refB, "verify result must surface the signing key ref so callers can pin it");
|
|
83
|
+
|
|
84
|
+
const trustRoot = { trusted_public_key_refs: { [ALICE_IDENTITY_REF]: refA } };
|
|
85
|
+
const substitutionWithRoot = sdk.verifyProofBundle(bundleFor(receiptB), trustRoot);
|
|
86
|
+
assert.strictEqual(substitutionWithRoot.valid, false, "key substitution must be rejected with a pinned trust root");
|
|
87
|
+
assert.strictEqual(substitutionWithRoot.reason, "native_public_key_not_trusted", "key substitution rejection reason mismatch");
|
|
88
|
+
assert.strictEqual(substitutionWithRoot.trust_root_applied, true, "trust_root_applied must be true with a trust root");
|
|
89
|
+
assert.strictEqual(substitutionWithRoot.cryptographically_verified, false, "rejected result must not claim cryptographic verification");
|
|
90
|
+
|
|
91
|
+
const genuineWithRoot = sdk.verifyProofBundle(bundleFor(receiptA), trustRoot);
|
|
92
|
+
assert.strictEqual(genuineWithRoot.valid, true, "genuine key A proof must verify with trust root pinned to key A");
|
|
93
|
+
assert.strictEqual(genuineWithRoot.trust_root_applied, true, "genuine trust_root_applied mismatch");
|
|
94
|
+
|
|
95
|
+
const genuineArrayRoot = sdk.verifyProofBundle(bundleFor(receiptA), { trusted_public_key_refs: { [ALICE_IDENTITY_REF]: [refB, refA] } });
|
|
96
|
+
assert.strictEqual(genuineArrayRoot.valid, true, "array-valued trust root must accept any listed ref");
|
|
97
|
+
|
|
98
|
+
const expectedRefRoot = sdk.verifyProofBundle(bundleFor(receiptB), { expected_public_key_ref: refA });
|
|
99
|
+
assert.strictEqual(expectedRefRoot.reason, "native_public_key_not_trusted", "expected_public_key_ref must reject key B");
|
|
100
|
+
assert.strictEqual(sdk.verifyProofBundle(bundleFor(receiptA), { expected_public_key_ref: refA }).valid, true, "expected_public_key_ref must accept key A");
|
|
101
|
+
|
|
102
|
+
const unknownIdentityRoot = sdk.verifyProofBundle(bundleFor(receiptA), { trusted_public_key_refs: { "did:xoonya:bob": refA } });
|
|
103
|
+
assert.strictEqual(unknownIdentityRoot.reason, "native_public_key_not_trusted", "identity absent from trust root must fail closed");
|
|
104
|
+
|
|
105
|
+
const hybridOnlyRoot = sdk.verifyProofBundle(bundleFor(receiptA), { expected_pq_public_key_ref: "ml-dsa-65-spki-sha256:" + "00".repeat(32) });
|
|
106
|
+
assert.strictEqual(hybridOnlyRoot.reason, "native_public_key_not_trusted", "a PQ-only pin against a classical-only proof must fail closed");
|
|
107
|
+
|
|
108
|
+
const strictGenuine = sdk.verifyProofBundle(bundleFor(receiptA), { ...trustRoot, strict: true });
|
|
109
|
+
assert.strictEqual(strictGenuine.valid, true, "cryptographic class must pass strict mode");
|
|
110
|
+
return { refA, refB };
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
function checkExternalStructural() {
|
|
114
|
+
const methods = ["jwt-assertion-v1", "did-vc-v1", "sd-jwt-vc-v1", "signed-envelope-v1", "partner-proof-envelope-v1", "detached-assertion-v1"];
|
|
115
|
+
for (const methodId of methods) {
|
|
116
|
+
const receipt = sdk.emitReceipt(event, {
|
|
117
|
+
agent_identity: "alice",
|
|
118
|
+
algorithm: methodId,
|
|
119
|
+
signature: `garbage-signature-${crypto.randomBytes(8).toString("hex")}`
|
|
120
|
+
});
|
|
121
|
+
const relaxed = sdk.verifyProofBundle(bundleFor(receipt));
|
|
122
|
+
assert.strictEqual(relaxed.valid, true, `${methodId} garbage signature is accepted structurally by default`);
|
|
123
|
+
assert.strictEqual(relaxed.verification_class, "structural", `${methodId} class must be structural`);
|
|
124
|
+
assert.strictEqual(relaxed.cryptographically_verified, false, `${methodId} must not claim cryptographic verification`);
|
|
125
|
+
const strict = sdk.verifyProofBundle(bundleFor(receipt), { strict: true });
|
|
126
|
+
assert.strictEqual(strict.valid, false, `${methodId} must fail under strict verification`);
|
|
127
|
+
assert.strictEqual(strict.reason, "external_verification_required", `${methodId} strict rejection reason mismatch`);
|
|
128
|
+
assert.strictEqual(strict.verification_class, "structural", `${methodId} strict class mismatch`);
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
function checkDigestDefault() {
|
|
133
|
+
const receipt = sdk.emitReceipt(event, { agent_identity: "alice" });
|
|
134
|
+
assert.strictEqual(receipt.algorithm, "xoonya-native-sha256-v1", "default receipt method mismatch");
|
|
135
|
+
const relaxed = sdk.verifyProofBundle(bundleFor(receipt));
|
|
136
|
+
assert.strictEqual(relaxed.valid, true, "digest bundle verifies by default");
|
|
137
|
+
assert.strictEqual(relaxed.verification_class, "digest", "digest class mismatch");
|
|
138
|
+
assert.strictEqual(relaxed.cryptographically_verified, false, "digest must not claim cryptographic verification");
|
|
139
|
+
|
|
140
|
+
// Anyone can forge a digest receipt for any identity: there is no key involved.
|
|
141
|
+
const forged = sdk.emitReceipt(event, { agent_identity: "alice" });
|
|
142
|
+
assert.strictEqual(forged.signature, receipt.signature, "digest receipts are reproducible by anyone who knows the event");
|
|
143
|
+
|
|
144
|
+
const strict = sdk.verifyProofBundle(bundleFor(receipt), { strict: true });
|
|
145
|
+
assert.strictEqual(strict.valid, false, "digest bundle must fail under strict verification");
|
|
146
|
+
assert.strictEqual(strict.reason, "digest_only_not_cryptographically_verified", "strict digest reason mismatch");
|
|
147
|
+
|
|
148
|
+
const previous = process.env.XOONYA_STRICT_VERIFY;
|
|
149
|
+
process.env.XOONYA_STRICT_VERIFY = "1";
|
|
150
|
+
try {
|
|
151
|
+
const envStrict = sdk.verifyProofBundle(bundleFor(receipt));
|
|
152
|
+
assert.strictEqual(envStrict.valid, false, "XOONYA_STRICT_VERIFY=1 must enable strict mode globally");
|
|
153
|
+
assert.strictEqual(envStrict.strict, true, "env strict flag must echo in the result");
|
|
154
|
+
} finally {
|
|
155
|
+
if (previous === undefined) delete process.env.XOONYA_STRICT_VERIFY;
|
|
156
|
+
else process.env.XOONYA_STRICT_VERIFY = previous;
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
function checkHybrid() {
|
|
161
|
+
const classical = crypto.generateKeyPairSync("ed25519");
|
|
162
|
+
const pq = crypto.generateKeyPairSync("ml-dsa-65");
|
|
163
|
+
const receipt = sdk.emitReceipt(event, {
|
|
164
|
+
agent_identity: "alice",
|
|
165
|
+
algorithm: "xoonya-native-hybrid-v1",
|
|
166
|
+
identity_ref: ALICE_IDENTITY_REF,
|
|
167
|
+
classical_signature_suite: "ed25519",
|
|
168
|
+
pq_signature_suite: "ml-dsa-65",
|
|
169
|
+
classical_public_key_pem: pem(classical.publicKey, "spki"),
|
|
170
|
+
classical_private_key_pem: pem(classical.privateKey, "pkcs8"),
|
|
171
|
+
pq_public_key_pem: pem(pq.publicKey, "spki"),
|
|
172
|
+
pq_private_key_pem: pem(pq.privateKey, "pkcs8")
|
|
173
|
+
});
|
|
174
|
+
const bundle = bundleFor(receipt);
|
|
175
|
+
const genuine = sdk.verifyProofBundle(bundle);
|
|
176
|
+
assert.strictEqual(genuine.valid, true, "hybrid bundle must verify");
|
|
177
|
+
assert.strictEqual(genuine.verification_class, "cryptographic", "hybrid class must be cryptographic");
|
|
178
|
+
assert.strictEqual(genuine.cryptographically_verified, true, "hybrid must report cryptographic verification");
|
|
179
|
+
assert.strictEqual(genuine.classical_public_key_ref, receipt.native_proof.classical_public_key_ref, "hybrid classical ref surfaced");
|
|
180
|
+
assert.strictEqual(genuine.pq_public_key_ref, receipt.native_proof.pq_public_key_ref, "hybrid pq ref surfaced");
|
|
181
|
+
|
|
182
|
+
const tamperedPqSignature = tamperBase64url(receipt.native_proof.proof_material.pq_signature);
|
|
183
|
+
const tamperedReceipt = {
|
|
184
|
+
...receipt,
|
|
185
|
+
native_proof: {
|
|
186
|
+
...receipt.native_proof,
|
|
187
|
+
proof_material: { ...receipt.native_proof.proof_material, pq_signature: tamperedPqSignature }
|
|
188
|
+
},
|
|
189
|
+
signature: `xoonya-native-hybrid-v1:${receipt.native_proof.hybrid_suite}:${receipt.native_proof.proof_material.classical_signature}.${tamperedPqSignature}`
|
|
190
|
+
};
|
|
191
|
+
const tampered = sdk.verifyProofBundle(bundleFor(tamperedReceipt));
|
|
192
|
+
assert.strictEqual(tampered.valid, false, "tampered PQ signature must be rejected");
|
|
193
|
+
assert.strictEqual(tampered.reason, "native_hybrid_pq_signature_invalid", "tampered PQ signature reason mismatch");
|
|
194
|
+
assert.strictEqual(tampered.cryptographically_verified, false, "tampered hybrid must not claim cryptographic verification");
|
|
195
|
+
|
|
196
|
+
const wrongPqRef = `ml-dsa-65-spki-sha256:${"11".repeat(32)}`;
|
|
197
|
+
const wrongPqPinned = sdk.verifyProofBundle(bundle, {
|
|
198
|
+
trusted_public_key_refs: { [ALICE_IDENTITY_REF]: { classical: genuine.classical_public_key_ref, pq: wrongPqRef } }
|
|
199
|
+
});
|
|
200
|
+
assert.strictEqual(wrongPqPinned.valid, false, "wrong pinned PQ ref must be rejected");
|
|
201
|
+
assert.strictEqual(wrongPqPinned.reason, "native_hybrid_public_key_not_trusted", "wrong PQ pin reason mismatch");
|
|
202
|
+
assert.strictEqual(wrongPqPinned.trust_root_applied, true, "hybrid trust_root_applied mismatch");
|
|
203
|
+
|
|
204
|
+
const wrongExpectedPq = sdk.verifyProofBundle(bundle, { expected_pq_public_key_ref: wrongPqRef });
|
|
205
|
+
assert.strictEqual(wrongExpectedPq.reason, "native_hybrid_public_key_not_trusted", "expected_pq_public_key_ref mismatch must be rejected");
|
|
206
|
+
|
|
207
|
+
const rightPinned = sdk.verifyProofBundle(bundle, {
|
|
208
|
+
trusted_public_key_refs: { [ALICE_IDENTITY_REF]: { classical: genuine.classical_public_key_ref, pq: genuine.pq_public_key_ref } },
|
|
209
|
+
strict: true
|
|
210
|
+
});
|
|
211
|
+
assert.strictEqual(rightPinned.valid, true, "correctly pinned hybrid must verify under strict mode");
|
|
212
|
+
|
|
213
|
+
const classicalOnlyPin = sdk.verifyProofBundle(bundle, { trusted_public_key_refs: { [ALICE_IDENTITY_REF]: genuine.classical_public_key_ref } });
|
|
214
|
+
assert.strictEqual(classicalOnlyPin.valid, true, "string-valued trust root pins the classical lane of a hybrid proof");
|
|
215
|
+
|
|
216
|
+
const otherClassical = crypto.generateKeyPairSync("ed25519");
|
|
217
|
+
const otherClassicalRef = sdk.emitReceipt(event, {
|
|
218
|
+
agent_identity: "alice",
|
|
219
|
+
algorithm: "xoonya-native-signed-v1",
|
|
220
|
+
identity_ref: ALICE_IDENTITY_REF,
|
|
221
|
+
signature_suite: "ed25519",
|
|
222
|
+
private_key_pem: pem(otherClassical.privateKey, "pkcs8"),
|
|
223
|
+
public_key_pem: pem(otherClassical.publicKey, "spki")
|
|
224
|
+
}).native_proof.public_key_ref;
|
|
225
|
+
const wrongClassicalPin = sdk.verifyProofBundle(bundle, { trusted_public_key_refs: { [ALICE_IDENTITY_REF]: otherClassicalRef } });
|
|
226
|
+
assert.strictEqual(wrongClassicalPin.reason, "native_hybrid_public_key_not_trusted", "wrong classical pin on hybrid must be rejected");
|
|
227
|
+
return bundle;
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
function checkDeterminism(hybridBundle, refs) {
|
|
231
|
+
const keyA = crypto.generateKeyPairSync("ed25519");
|
|
232
|
+
const signedBundle = bundleFor(signedReceiptFor(keyA));
|
|
233
|
+
const digestBundle = bundleFor(sdk.emitReceipt(event, { agent_identity: "alice" }));
|
|
234
|
+
const externalBundle = bundleFor(sdk.emitReceipt(event, { agent_identity: "alice", algorithm: "jwt-assertion-v1", signature: "garbage" }));
|
|
235
|
+
const cases = [
|
|
236
|
+
[signedBundle, {}],
|
|
237
|
+
[signedBundle, { strict: true }],
|
|
238
|
+
[signedBundle, { trusted_public_key_refs: { [ALICE_IDENTITY_REF]: refs.refA } }],
|
|
239
|
+
[digestBundle, {}],
|
|
240
|
+
[digestBundle, { strict: true }],
|
|
241
|
+
[externalBundle, { strict: true }],
|
|
242
|
+
[hybridBundle, {}],
|
|
243
|
+
[hybridBundle, { expected_pq_public_key_ref: "nope" }]
|
|
244
|
+
];
|
|
245
|
+
for (const [bundle, options] of cases) {
|
|
246
|
+
const first = canonicalize(sdk.verifyProofBundle(bundle, options));
|
|
247
|
+
const second = canonicalize(sdk.verifyProofBundle(bundle, options));
|
|
248
|
+
assert.strictEqual(first, second, `verification result must be deterministic for options ${JSON.stringify(options)}`);
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
function main() {
|
|
253
|
+
const classes = sdk.listProofVerificationClasses().map((entry) => entry.id).sort();
|
|
254
|
+
assert.deepStrictEqual(classes, ["anchored_binding", "cryptographic", "digest", "structural"], "verification class registry mismatch");
|
|
255
|
+
assert.deepStrictEqual(
|
|
256
|
+
sdk.PROOF_VERIFICATION_STRICT_REJECTION_CODES.slice(),
|
|
257
|
+
["digest_only_not_cryptographically_verified", "external_verification_required"],
|
|
258
|
+
"strict rejection code registry mismatch"
|
|
259
|
+
);
|
|
260
|
+
assert.deepStrictEqual(
|
|
261
|
+
sdk.PROOF_VERIFICATION_TRUST_ROOT_REJECTION_CODES.slice(),
|
|
262
|
+
["native_public_key_not_trusted", "native_hybrid_public_key_not_trusted"],
|
|
263
|
+
"trust root rejection code registry mismatch"
|
|
264
|
+
);
|
|
265
|
+
|
|
266
|
+
const refs = checkKeySubstitution();
|
|
267
|
+
checkExternalStructural();
|
|
268
|
+
checkDigestDefault();
|
|
269
|
+
const hybridBundle = checkHybrid();
|
|
270
|
+
checkDeterminism(hybridBundle, refs);
|
|
271
|
+
|
|
272
|
+
console.log("xoonya verify:forgery-resistance passed");
|
|
273
|
+
console.log("key substitution without trust root: accepted (documented limitation; pin with trusted_public_key_refs)");
|
|
274
|
+
console.log("key substitution with trust root: native_public_key_not_trusted");
|
|
275
|
+
console.log("external methods under strict: external_verification_required");
|
|
276
|
+
console.log("digest method under strict: digest_only_not_cryptographically_verified");
|
|
277
|
+
console.log("hybrid tampered pq signature: native_hybrid_pq_signature_invalid");
|
|
278
|
+
console.log("hybrid wrong pq pin: native_hybrid_public_key_not_trusted");
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
main();
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
const assert = require('assert');
|
|
2
|
+
const http = require('http');
|
|
2
3
|
const { createServer } = require('../server.js');
|
|
3
4
|
const sdk = require('../index.js');
|
|
4
5
|
|
|
@@ -1495,6 +1496,103 @@ async function main() {
|
|
|
1495
1496
|
});
|
|
1496
1497
|
assert.strictEqual(proofBundleVerify.status, 200, 'proof bundle verify route failed');
|
|
1497
1498
|
assert.strictEqual(proofBundleVerify.json.valid, true, 'proof bundle verify payload invalid');
|
|
1499
|
+
assert.strictEqual(proofBundleVerify.json.verification_class, 'digest', 'legacy proof bundle verify class mismatch');
|
|
1500
|
+
assert.strictEqual(proofBundleVerify.json.cryptographically_verified, false, 'legacy proof bundle must not claim cryptographic verification');
|
|
1501
|
+
assert.strictEqual(proofBundleVerify.json.strict, false, 'proof bundle verify strict flag default mismatch');
|
|
1502
|
+
assert.strictEqual(proofBundleVerify.json.trust_root_applied, false, 'proof bundle verify trust root flag default mismatch');
|
|
1503
|
+
|
|
1504
|
+
const digestReceipt = sdk.emitReceipt(canonicalEvent, { agent_identity: 'service-digest-agent' });
|
|
1505
|
+
const digestBundle = sdk.buildProofBundle({ ceb: canonicalEvent, receipt: digestReceipt });
|
|
1506
|
+
const digestVerifyDefault = await postJson(baseUrl, '/v1/proof-bundles/verify', { bundle: digestBundle });
|
|
1507
|
+
assert.strictEqual(digestVerifyDefault.status, 200, 'digest proof bundle verify route failed');
|
|
1508
|
+
assert.strictEqual(digestVerifyDefault.json.valid, true, 'digest proof bundle must verify in default mode');
|
|
1509
|
+
assert.strictEqual(digestVerifyDefault.json.verification_class, 'digest', 'digest proof bundle class mismatch');
|
|
1510
|
+
const digestVerifyStrict = await postJson(baseUrl, '/v1/proof-bundles/verify', {
|
|
1511
|
+
bundle: digestBundle,
|
|
1512
|
+
options: { strict: true },
|
|
1513
|
+
});
|
|
1514
|
+
assert.strictEqual(digestVerifyStrict.status, 200, 'strict digest proof bundle verify route failed');
|
|
1515
|
+
assert.strictEqual(digestVerifyStrict.json.valid, false, 'digest proof bundle must fail under strict verification');
|
|
1516
|
+
assert.strictEqual(digestVerifyStrict.json.reason, 'digest_only_not_cryptographically_verified', 'strict digest rejection reason mismatch');
|
|
1517
|
+
assert.strictEqual(digestVerifyStrict.json.strict, true, 'strict flag must echo in verify result');
|
|
1518
|
+
assert.strictEqual(digestVerifyStrict.json.cryptographically_verified, false, 'strict digest result must not claim cryptographic verification');
|
|
1519
|
+
|
|
1520
|
+
const optionsResponse = await fetch(`${baseUrl}/v1/proof-bundles/verify`, { method: 'OPTIONS' });
|
|
1521
|
+
assert.strictEqual(optionsResponse.status, 204, 'OPTIONS must return 204');
|
|
1522
|
+
assert.strictEqual(/POST/.test(optionsResponse.headers.get('allow') || ''), true, 'OPTIONS must advertise POST in allow header');
|
|
1523
|
+
const headResponse = await fetch(`${baseUrl}/health`, { method: 'HEAD' });
|
|
1524
|
+
assert.strictEqual(headResponse.status, 200, 'HEAD on a known GET route must return 200');
|
|
1525
|
+
assert.strictEqual((await headResponse.text()).length, 0, 'HEAD response must not carry a body');
|
|
1526
|
+
assert.strictEqual(headResponse.headers.get('x-content-type-options'), 'nosniff', 'nosniff header missing');
|
|
1527
|
+
assert.strictEqual(digestVerifyStrict.status, 200, 'post response status mismatch');
|
|
1528
|
+
const postHeadersResponse = await fetch(`${baseUrl}/v1/proof-bundles/verify`, {
|
|
1529
|
+
method: 'POST',
|
|
1530
|
+
headers: { 'content-type': 'application/json' },
|
|
1531
|
+
body: JSON.stringify({ bundle: digestBundle }),
|
|
1532
|
+
});
|
|
1533
|
+
assert.strictEqual(postHeadersResponse.headers.get('cache-control'), 'no-store', 'POST responses must be no-store');
|
|
1534
|
+
await postHeadersResponse.text();
|
|
1535
|
+
const badEncodingResponse = await fetch(`${baseUrl}/v1/integrations/%E0%A4%A`);
|
|
1536
|
+
assert.strictEqual(badEncodingResponse.status, 400, 'invalid percent-encoding must return 400');
|
|
1537
|
+
assert.strictEqual((await badEncodingResponse.json()).reason, 'invalid_path_encoding', 'invalid path encoding reason mismatch');
|
|
1538
|
+
const oversizedResponse = await new Promise((resolve, reject) => {
|
|
1539
|
+
const request = http.request(
|
|
1540
|
+
{
|
|
1541
|
+
host: '127.0.0.1',
|
|
1542
|
+
port: address.port,
|
|
1543
|
+
method: 'POST',
|
|
1544
|
+
path: '/v1/proof-bundles/verify',
|
|
1545
|
+
headers: { 'content-type': 'application/json', 'content-length': String(2 * 1024 * 1024) },
|
|
1546
|
+
},
|
|
1547
|
+
(response) => {
|
|
1548
|
+
let raw = '';
|
|
1549
|
+
response.setEncoding('utf8');
|
|
1550
|
+
response.on('data', (chunk) => { raw += chunk; });
|
|
1551
|
+
response.on('end', () => {
|
|
1552
|
+
request.destroy();
|
|
1553
|
+
resolve({ status: response.statusCode, json: JSON.parse(raw) });
|
|
1554
|
+
});
|
|
1555
|
+
}
|
|
1556
|
+
);
|
|
1557
|
+
request.on('error', reject);
|
|
1558
|
+
request.flushHeaders();
|
|
1559
|
+
});
|
|
1560
|
+
assert.strictEqual(oversizedResponse.status, 400, 'oversized declared content-length must be rejected before the body is read');
|
|
1561
|
+
assert.strictEqual(oversizedResponse.json.reason, 'request body too large', 'oversized body reason mismatch');
|
|
1562
|
+
|
|
1563
|
+
const previousPrivateKeyPolicy = process.env.XOONYA_ACCEPT_PRIVATE_KEY_MATERIAL;
|
|
1564
|
+
process.env.XOONYA_ACCEPT_PRIVATE_KEY_MATERIAL = 'false';
|
|
1565
|
+
try {
|
|
1566
|
+
const privateKeyEmit = await postJson(baseUrl, '/v1/receipts/emit', {
|
|
1567
|
+
event: canonicalEvent,
|
|
1568
|
+
options: {
|
|
1569
|
+
algorithm: 'xoonya-native-signed-v1',
|
|
1570
|
+
signature_suite: 'ed25519',
|
|
1571
|
+
private_key_pem: '-----BEGIN PRIVATE KEY-----\nMC4CAQAwBQYDK2VwBCIEIOPZUHPWLt9QpTQ3ub3+RyJXNZrJYHxfDJEcUZhrXIz7\n-----END PRIVATE KEY-----\n',
|
|
1572
|
+
public_key_pem: '-----BEGIN PUBLIC KEY-----\nMCowBQYDK2VwAyEArnC5ARNJQFbPtc6pMrFUz2p043Nbds48Dz66+i1R5gk=\n-----END PUBLIC KEY-----\n',
|
|
1573
|
+
},
|
|
1574
|
+
});
|
|
1575
|
+
assert.strictEqual(privateKeyEmit.status, 400, 'private key material must be rejected when policy disallows it');
|
|
1576
|
+
assert.strictEqual(privateKeyEmit.json.reason, 'private_key_material_not_accepted', 'private key rejection reason mismatch');
|
|
1577
|
+
assert.strictEqual(privateKeyEmit.json.field, 'options.private_key_pem', 'private key rejection field mismatch');
|
|
1578
|
+
const privateKeyBuild = await postJson(baseUrl, '/v1/proof-bundles/build', {
|
|
1579
|
+
ceb: canonicalEvent,
|
|
1580
|
+
receipt_options: { classical_private_key_pem: 'x', pq_private_key_pem: 'y' },
|
|
1581
|
+
});
|
|
1582
|
+
assert.strictEqual(privateKeyBuild.status, 400, 'build route must reject private key material when policy disallows it');
|
|
1583
|
+
assert.strictEqual(privateKeyBuild.json.reason, 'private_key_material_not_accepted', 'build private key rejection reason mismatch');
|
|
1584
|
+
const noKeyEmit = await postJson(baseUrl, '/v1/receipts/emit', {
|
|
1585
|
+
event: canonicalEvent,
|
|
1586
|
+
options: { legacy_identity_material_hex: identityMaterialHex },
|
|
1587
|
+
});
|
|
1588
|
+
assert.strictEqual(noKeyEmit.status, 200, 'emit without private key material must still succeed under restrictive policy');
|
|
1589
|
+
} finally {
|
|
1590
|
+
if (previousPrivateKeyPolicy === undefined) {
|
|
1591
|
+
delete process.env.XOONYA_ACCEPT_PRIVATE_KEY_MATERIAL;
|
|
1592
|
+
} else {
|
|
1593
|
+
process.env.XOONYA_ACCEPT_PRIVATE_KEY_MATERIAL = previousPrivateKeyPolicy;
|
|
1594
|
+
}
|
|
1595
|
+
}
|
|
1498
1596
|
const defaultProofLoopRun = await postJson(baseUrl, '/v1/proof-center/default-loops/run', {
|
|
1499
1597
|
loop_ref: 'default.proof.bundle.loop',
|
|
1500
1598
|
event: canonicalEvent,
|