@oxdeai/conformance 1.3.1 → 2.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +88 -0
- package/LICENSE +213 -0
- package/README.md +217 -5
- package/dist/evidence-metadata.json +3489 -0
- package/dist/scripts/build-evidence-metadata.d.mts +1 -0
- package/dist/scripts/build-evidence-metadata.mjs +7 -0
- package/dist/scripts/build-evidence-metadata.mjs.map +1 -0
- package/dist/scripts/extract-vectors.js +228 -22
- package/dist/scripts/extract-vectors.js.map +1 -1
- package/dist/src/evidenceScope.d.mts +68 -0
- package/dist/src/evidenceScope.mjs +127 -0
- package/dist/src/evidenceScope.mjs.map +1 -0
- package/dist/src/fixtures/conformance-engine-secret.fixture.d.ts +5 -0
- package/dist/src/fixtures/conformance-engine-secret.fixture.js +7 -0
- package/dist/src/fixtures/conformance-engine-secret.fixture.js.map +1 -0
- package/dist/src/fixtures/ed25519.test-only.fixture.d.ts +1 -1
- package/dist/src/fixtures/ed25519.test-only.fixture.js +2 -1
- package/dist/src/fixtures/ed25519.test-only.fixture.js.map +1 -1
- package/dist/src/fixtures/krl-ed25519.test-only.fixture.d.ts +13 -0
- package/dist/src/fixtures/krl-ed25519.test-only.fixture.js +19 -0
- package/dist/src/fixtures/krl-ed25519.test-only.fixture.js.map +1 -0
- package/dist/src/trustedTimeConformance.d.ts +32 -0
- package/dist/src/trustedTimeConformance.js +484 -0
- package/dist/src/trustedTimeConformance.js.map +1 -0
- package/dist/src/validate-trusted-time.d.ts +1 -0
- package/dist/src/validate-trusted-time.js +14 -0
- package/dist/src/validate-trusted-time.js.map +1 -0
- package/dist/src/validate.d.ts +1 -0
- package/dist/src/validate.js +723 -31
- package/dist/src/validate.js.map +1 -1
- package/package.json +38 -8
- package/vectors/audit-chain.json +2 -2
- package/vectors/authorization-payload.json +46 -34
- package/vectors/authorization-signature-verification.json +39 -1
- package/vectors/authorization-verification.json +54 -1
- package/vectors/clock-semantics-verification.json +136 -0
- package/vectors/delegation-chain-verification.json +366 -0
- package/vectors/delegation-parent-hash.json +54 -0
- package/vectors/delegation-signature-verification.json +317 -0
- package/vectors/delegation-verification.json +252 -0
- package/vectors/envelope-verification.json +3 -3
- package/vectors/key-lifecycle-verification.json +131 -0
- package/vectors/profile-c-state-verification.json +217 -0
- package/vectors/signed-krl-verification.json +103 -0
- package/vectors/trusted-time.json +50 -0
- package/dist/extract-vectors.js +0 -416
- package/scripts/extract-vectors.ts +0 -466
- package/src/fixtures/README.md +0 -11
- package/src/fixtures/ed25519.test-only.fixture.ts +0 -14
- package/src/validate.ts +0 -828
- package/tsconfig.json +0 -14
package/dist/src/validate.js
CHANGED
|
@@ -1,8 +1,15 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
3
|
+
import { evidenceScope } from "./evidenceScope.mjs";
|
|
1
4
|
import { readFileSync } from "node:fs";
|
|
2
5
|
import { resolve } from "node:path";
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
6
|
+
import { fileURLToPath } from "node:url";
|
|
7
|
+
import { createHash, sign as nodeSign } from "node:crypto";
|
|
8
|
+
import { encodeCanonicalState, encodeEnvelope, PolicyEngine, RECOMMENDED_TRUSTED_TIME_PROFILE, sha256HexFromJson, signAuthorizationEd25519, signEnvelopeEd25519, verifyAuthorization, verifyAuditEvents, verifyEnvelope, verifySnapshot, createDelegation, verifyDelegation, verifyDelegationChain, signEd25519, SIGNING_DOMAINS, signedKrlSigningPayload, verifySignedKrl, } from "@oxdeai/core";
|
|
5
9
|
import { TEST_ONLY_ED25519_PRIVATE_KEY_PEM_DO_NOT_USE_IN_PRODUCTION, TEST_ONLY_ED25519_PUBLIC_KEY_PEM_DO_NOT_USE_IN_PRODUCTION, } from "./fixtures/ed25519.test-only.fixture.js";
|
|
10
|
+
import { KRL_TEST_ONLY_ED25519_PRIVATE_KEY_PEM_DO_NOT_USE_IN_PRODUCTION, KRL_TEST_ONLY_ED25519_PUBLIC_KEY_PEM_DO_NOT_USE_IN_PRODUCTION, } from "./fixtures/krl-ed25519.test-only.fixture.js";
|
|
11
|
+
import { CONFORMANCE_ENGINE_SECRET } from "./fixtures/conformance-engine-secret.fixture.js";
|
|
12
|
+
import { runTrustedTimeConformance } from "./trustedTimeConformance.js";
|
|
6
13
|
const TEST_KEYSET = {
|
|
7
14
|
issuer: "oxdeai.policy-engine",
|
|
8
15
|
version: "1",
|
|
@@ -14,7 +21,7 @@ const TEST_KEYSET = {
|
|
|
14
21
|
}
|
|
15
22
|
]
|
|
16
23
|
};
|
|
17
|
-
const CORE_ENGINE_SECRET =
|
|
24
|
+
const CORE_ENGINE_SECRET = CONFORMANCE_ENGINE_SECRET;
|
|
18
25
|
const CORE_POLICY_ID = "a".repeat(64);
|
|
19
26
|
const INTENT_BINDING_FIELDS = [
|
|
20
27
|
"intent_id",
|
|
@@ -32,12 +39,13 @@ const INTENT_BINDING_FIELDS = [
|
|
|
32
39
|
"tool",
|
|
33
40
|
"tool_call"
|
|
34
41
|
];
|
|
35
|
-
function makeEngine() {
|
|
42
|
+
function makeEngine(authorizationTtlSeconds = 60) {
|
|
36
43
|
return new PolicyEngine({
|
|
37
44
|
policy_version: "v1.0.0",
|
|
38
|
-
engine_secret: CORE_ENGINE_SECRET,
|
|
39
|
-
authorization_ttl_seconds:
|
|
40
|
-
policyId: CORE_POLICY_ID
|
|
45
|
+
engine_secret: CORE_ENGINE_SECRET, // now typed as string
|
|
46
|
+
authorization_ttl_seconds: authorizationTtlSeconds,
|
|
47
|
+
policyId: CORE_POLICY_ID,
|
|
48
|
+
...RECOMMENDED_TRUSTED_TIME_PROFILE
|
|
41
49
|
});
|
|
42
50
|
}
|
|
43
51
|
function makeBaseState() {
|
|
@@ -151,6 +159,23 @@ function b64ToBytes(value) {
|
|
|
151
159
|
function hexSha256Utf8(value) {
|
|
152
160
|
return createHash("sha256").update(value, "utf8").digest("hex");
|
|
153
161
|
}
|
|
162
|
+
// Returns the raw signature bytes string regardless of which shape is present.
|
|
163
|
+
// Used wherever the conformance layer needs to inspect or compare signature material.
|
|
164
|
+
function getSignatureBytes(signature) {
|
|
165
|
+
return typeof signature === "string" ? signature : signature.sig;
|
|
166
|
+
}
|
|
167
|
+
// Corrupts the signature bytes by replacing the last two characters with "aa".
|
|
168
|
+
// Works for both the legacy bare-string shape and the normative nested-object shape.
|
|
169
|
+
// All mutation-test sites must go through this helper; no direct .slice() on signature.
|
|
170
|
+
function mutateSignature(auth) {
|
|
171
|
+
if (typeof auth.signature === "string") {
|
|
172
|
+
return { ...auth, signature: auth.signature.slice(0, -2) + "aa" };
|
|
173
|
+
}
|
|
174
|
+
return {
|
|
175
|
+
...auth,
|
|
176
|
+
signature: { ...auth.signature, sig: auth.signature.sig.slice(0, -2) + "aa" },
|
|
177
|
+
};
|
|
178
|
+
}
|
|
154
179
|
function isPlainObject(v) {
|
|
155
180
|
return typeof v === "object" && v !== null && !Array.isArray(v);
|
|
156
181
|
}
|
|
@@ -185,12 +210,15 @@ const coreAdapter = {
|
|
|
185
210
|
}
|
|
186
211
|
return sha256HexFromJson(binding);
|
|
187
212
|
},
|
|
188
|
-
evaluateAuthorization(intent) {
|
|
213
|
+
evaluateAuthorization(intent, evaluationTime) {
|
|
189
214
|
const engine = makeEngine();
|
|
190
|
-
const out = engine.evaluatePure(intent, makeBaseState());
|
|
215
|
+
const out = engine.evaluatePure(intent, makeBaseState(), evaluationTime);
|
|
191
216
|
if (out.decision !== "ALLOW") {
|
|
192
217
|
throw new Error(`expected ALLOW, got DENY: ${out.reasons.join(",")}`);
|
|
193
218
|
}
|
|
219
|
+
// Cast to AuthorizationLike: runtime object retains internal engine fields
|
|
220
|
+
// (state_snapshot_hash, expires_at, engine_signature) even though the
|
|
221
|
+
// TypeScript type was narrowed to AuthorizationV1 at the evaluatePure boundary.
|
|
194
222
|
return { authorization: out.authorization, policyId: engine.computePolicyId() };
|
|
195
223
|
},
|
|
196
224
|
encodeSnapshot(state) {
|
|
@@ -207,16 +235,29 @@ const coreAdapter = {
|
|
|
207
235
|
verifyEnvelope,
|
|
208
236
|
verifyAuthorization
|
|
209
237
|
};
|
|
238
|
+
const here = fileURLToPath(new URL(".", import.meta.url));
|
|
239
|
+
const exercised = new Map();
|
|
240
|
+
const caseResults = new Map();
|
|
241
|
+
const jsonOutput = process.argv.includes("--json");
|
|
242
|
+
function recordCase(label, passed) {
|
|
243
|
+
const id = label.split(/[ :]/)[0];
|
|
244
|
+
caseResults.set(id, (caseResults.get(id) ?? true) && passed);
|
|
245
|
+
}
|
|
210
246
|
function loadJson(name) {
|
|
211
|
-
const p = resolve(
|
|
212
|
-
|
|
247
|
+
const p = resolve(here, "../../vectors", name);
|
|
248
|
+
const data = JSON.parse(readFileSync(p, "utf8"));
|
|
249
|
+
exercised.set(`packages/conformance/vectors/${name}`, data);
|
|
250
|
+
return data;
|
|
213
251
|
}
|
|
214
252
|
function pass(ctx, msg) {
|
|
215
253
|
ctx.passed += 1;
|
|
216
|
-
|
|
254
|
+
recordCase(msg, true);
|
|
255
|
+
if (!jsonOutput)
|
|
256
|
+
console.log(`PASS ${msg}`);
|
|
217
257
|
}
|
|
218
258
|
function fail(ctx, msg) {
|
|
219
259
|
ctx.failures.push(msg);
|
|
260
|
+
recordCase(msg, false);
|
|
220
261
|
console.error(`FAIL ${msg}`);
|
|
221
262
|
}
|
|
222
263
|
function eq(ctx, label, actual, expected) {
|
|
@@ -246,14 +287,20 @@ function validateAuthorizationVectors(ctx, adapter) {
|
|
|
246
287
|
const file = loadJson("authorization-payload.json");
|
|
247
288
|
for (const v of file.vectors) {
|
|
248
289
|
const id = String(v.id);
|
|
249
|
-
const
|
|
250
|
-
const
|
|
290
|
+
const input = asRecord(v.input);
|
|
291
|
+
const intent = parseIntent(input.intent);
|
|
292
|
+
const evaluationTime = Number(input.evaluation_time);
|
|
293
|
+
const effectiveTtl = Number(input.effective_ttl);
|
|
294
|
+
eq(ctx, `${id} intent_timestamp`, intent.timestamp, Number(input.intent_timestamp));
|
|
295
|
+
const { authorization, policyId } = adapter.evaluateAuthorization(intent, evaluationTime);
|
|
251
296
|
const expected = asRecord(v.expected);
|
|
252
297
|
eq(ctx, `${id} intent_hash`, authorization.intent_hash, String(expected.intent_hash));
|
|
253
298
|
if (expected.state_hash !== undefined) {
|
|
254
299
|
eq(ctx, `${id} state_hash`, authorization.state_snapshot_hash, String(expected.state_hash));
|
|
255
300
|
}
|
|
256
|
-
eq(ctx, `${id}
|
|
301
|
+
eq(ctx, `${id} issued_at`, authorization.issued_at, Number(expected.expected_issued_at));
|
|
302
|
+
eq(ctx, `${id} expiry`, authorization.expires_at, Number(expected.expected_expiry));
|
|
303
|
+
eq(ctx, `${id} effective_ttl`, authorization.expires_at - authorization.issued_at, effectiveTtl);
|
|
257
304
|
const payload = adapter.canonicalJson({
|
|
258
305
|
expires_at: authorization.expires_at,
|
|
259
306
|
intent_hash: authorization.intent_hash,
|
|
@@ -282,6 +329,263 @@ function validateAuthorizationVerificationVectors(ctx, adapter) {
|
|
|
282
329
|
eq(ctx, `${id} violations`, got.violations, expected.violations ?? []);
|
|
283
330
|
}
|
|
284
331
|
}
|
|
332
|
+
function validateClockSemanticsVectors(ctx, adapter) {
|
|
333
|
+
const file = loadJson("clock-semantics-verification.json");
|
|
334
|
+
for (const v of file.vectors) {
|
|
335
|
+
const id = String(v.id);
|
|
336
|
+
const input = asRecord(v.input);
|
|
337
|
+
const auth = {
|
|
338
|
+
alg: "HMAC-SHA256",
|
|
339
|
+
kid: "legacy",
|
|
340
|
+
signature: "legacy-placeholder",
|
|
341
|
+
...asRecord(input.auth)
|
|
342
|
+
};
|
|
343
|
+
const opts = (input.opts ? asRecord(input.opts) : {});
|
|
344
|
+
const expected = asRecord(v.expected);
|
|
345
|
+
const got = adapter.verifyAuthorization(auth, opts);
|
|
346
|
+
eq(ctx, `${id} status`, got.status, String(expected.status));
|
|
347
|
+
eq(ctx, `${id} violations`, got.violations, expected.violations ?? []);
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
// ── Delegation conformance helpers ────────────────────────────────────────────
|
|
351
|
+
const DELEGATION_T_ISSUED = 1_000_000;
|
|
352
|
+
const DELEGATION_T_NOW = 1_001_000;
|
|
353
|
+
const DELEGATION_T_DEL_EXP = 1_002_000;
|
|
354
|
+
const DELEGATION_T_PAR_EXP = 1_003_000;
|
|
355
|
+
// Delegation signatures are verified against the delegating principal's KeySet.
|
|
356
|
+
// issuer = parent.audience = "parent-agent" (not the PDP issuer).
|
|
357
|
+
const DELEGATION_TEST_KEYSET = {
|
|
358
|
+
issuer: "parent-agent",
|
|
359
|
+
version: "1",
|
|
360
|
+
keys: [{
|
|
361
|
+
kid: "2026-01",
|
|
362
|
+
alg: "Ed25519",
|
|
363
|
+
public_key: TEST_ONLY_ED25519_PUBLIC_KEY_PEM_DO_NOT_USE_IN_PRODUCTION,
|
|
364
|
+
}],
|
|
365
|
+
};
|
|
366
|
+
function makeParentAuth() {
|
|
367
|
+
return signAuthorizationEd25519({
|
|
368
|
+
auth_id: "f".repeat(64),
|
|
369
|
+
issuer: "oxdeai.policy-engine",
|
|
370
|
+
audience: "parent-agent",
|
|
371
|
+
intent_hash: "a".repeat(64),
|
|
372
|
+
state_hash: "b".repeat(64),
|
|
373
|
+
policy_id: "c".repeat(64),
|
|
374
|
+
decision: "ALLOW",
|
|
375
|
+
issued_at: DELEGATION_T_ISSUED,
|
|
376
|
+
expiry: DELEGATION_T_PAR_EXP,
|
|
377
|
+
kid: "2026-01",
|
|
378
|
+
}, TEST_ONLY_ED25519_PRIVATE_KEY_PEM_DO_NOT_USE_IN_PRODUCTION);
|
|
379
|
+
}
|
|
380
|
+
function makeSignedDelegationBase(parent) {
|
|
381
|
+
return createDelegation(parent, {
|
|
382
|
+
delegatee: "child-agent",
|
|
383
|
+
scope: { tools: ["provision_gpu"] },
|
|
384
|
+
expiry: DELEGATION_T_DEL_EXP,
|
|
385
|
+
kid: "2026-01",
|
|
386
|
+
delegationId: "d1d1d1d1-0000-0000-0000-c0nf0rm4nce",
|
|
387
|
+
issuedAt: DELEGATION_T_ISSUED,
|
|
388
|
+
}, TEST_ONLY_ED25519_PRIVATE_KEY_PEM_DO_NOT_USE_IN_PRODUCTION);
|
|
389
|
+
}
|
|
390
|
+
function parseDelegationScope(input) {
|
|
391
|
+
if (input === undefined || input === null)
|
|
392
|
+
return {};
|
|
393
|
+
const r = asRecord(input);
|
|
394
|
+
const scope = {};
|
|
395
|
+
if (r.tools !== undefined)
|
|
396
|
+
scope.tools = r.tools;
|
|
397
|
+
if (r.max_amount !== undefined)
|
|
398
|
+
scope.max_amount = BigInt(String(r.max_amount));
|
|
399
|
+
if (r.max_actions !== undefined)
|
|
400
|
+
scope.max_actions = Number(r.max_actions);
|
|
401
|
+
if (r.max_depth !== undefined)
|
|
402
|
+
scope.max_depth = Number(r.max_depth);
|
|
403
|
+
return scope;
|
|
404
|
+
}
|
|
405
|
+
function parseDelegationInput(input) {
|
|
406
|
+
const r = asRecord(input);
|
|
407
|
+
return {
|
|
408
|
+
delegation_id: String(r.delegation_id),
|
|
409
|
+
issuer: String(r.issuer),
|
|
410
|
+
audience: String(r.audience),
|
|
411
|
+
parent_auth_hash: String(r.parent_auth_hash),
|
|
412
|
+
delegator: String(r.delegator),
|
|
413
|
+
delegatee: String(r.delegatee),
|
|
414
|
+
scope: parseDelegationScope(r.scope),
|
|
415
|
+
policy_id: String(r.policy_id),
|
|
416
|
+
issued_at: Number(r.issued_at),
|
|
417
|
+
expiry: Number(r.expiry),
|
|
418
|
+
alg: "Ed25519",
|
|
419
|
+
kid: String(r.kid),
|
|
420
|
+
signature: String(r.signature),
|
|
421
|
+
};
|
|
422
|
+
}
|
|
423
|
+
function validateDelegationParentHashVectors(ctx) {
|
|
424
|
+
const file = loadJson("delegation-parent-hash.json");
|
|
425
|
+
const seen = {};
|
|
426
|
+
for (const v of file.vectors) {
|
|
427
|
+
const id = String(v.id);
|
|
428
|
+
const input = asRecord(v.input);
|
|
429
|
+
const parent = asRecord(input.parent);
|
|
430
|
+
const hash = hexSha256Utf8(canonicalJson(parent));
|
|
431
|
+
const expected = String(asRecord(v.expected).parent_auth_hash);
|
|
432
|
+
eq(ctx, `${id} parent_auth_hash`, hash, expected);
|
|
433
|
+
seen[id] = hash;
|
|
434
|
+
if (v.invariant) {
|
|
435
|
+
const inv = String(v.invariant);
|
|
436
|
+
if (inv.startsWith("equals ")) {
|
|
437
|
+
const refId = inv.slice("equals ".length);
|
|
438
|
+
if (refId in seen) {
|
|
439
|
+
eq(ctx, `${id} invariant`, hash, seen[refId]);
|
|
440
|
+
}
|
|
441
|
+
}
|
|
442
|
+
}
|
|
443
|
+
}
|
|
444
|
+
}
|
|
445
|
+
function validateDelegationVerificationVectors(ctx) {
|
|
446
|
+
const file = loadJson("delegation-verification.json");
|
|
447
|
+
for (const v of file.vectors) {
|
|
448
|
+
const id = String(v.id);
|
|
449
|
+
const input = asRecord(v.input);
|
|
450
|
+
const delegation = parseDelegationInput(input.delegation);
|
|
451
|
+
const optsRaw = input.opts ? asRecord(input.opts) : {};
|
|
452
|
+
const opts = {};
|
|
453
|
+
if (optsRaw.now !== undefined)
|
|
454
|
+
opts.now = Number(optsRaw.now);
|
|
455
|
+
if (optsRaw.expectedDelegatee !== undefined)
|
|
456
|
+
opts.expectedDelegatee = String(optsRaw.expectedDelegatee);
|
|
457
|
+
if (optsRaw.expectedPolicyId !== undefined)
|
|
458
|
+
opts.expectedPolicyId = String(optsRaw.expectedPolicyId);
|
|
459
|
+
if (optsRaw.requireSignatureVerification !== undefined)
|
|
460
|
+
opts.requireSignatureVerification = Boolean(optsRaw.requireSignatureVerification);
|
|
461
|
+
if (optsRaw.consumedDelegationIds !== undefined)
|
|
462
|
+
opts.consumedDelegationIds = optsRaw.consumedDelegationIds;
|
|
463
|
+
if (optsRaw.parentScope !== undefined)
|
|
464
|
+
opts.parentScope = parseDelegationScope(optsRaw.parentScope);
|
|
465
|
+
const got = verifyDelegation(delegation, opts);
|
|
466
|
+
const expected = asRecord(v.expected);
|
|
467
|
+
eq(ctx, `${id} status`, got.status, String(expected.status));
|
|
468
|
+
eq(ctx, `${id} violations`, got.violations, expected.violations ?? []);
|
|
469
|
+
}
|
|
470
|
+
}
|
|
471
|
+
function buildDelegationChainCases() {
|
|
472
|
+
const parent = makeParentAuth();
|
|
473
|
+
const delegation = makeSignedDelegationBase(parent);
|
|
474
|
+
const opts = { now: DELEGATION_T_NOW };
|
|
475
|
+
// valid
|
|
476
|
+
const valid = verifyDelegationChain(delegation, parent, opts);
|
|
477
|
+
// parent-hash-mismatch: different parent with same audience/policy so only hash differs
|
|
478
|
+
const otherParent = signAuthorizationEd25519({
|
|
479
|
+
auth_id: "e".repeat(64), // different → different hash
|
|
480
|
+
issuer: "oxdeai.policy-engine",
|
|
481
|
+
audience: "parent-agent", // same → delegator check passes
|
|
482
|
+
intent_hash: "a".repeat(64),
|
|
483
|
+
state_hash: "b".repeat(64),
|
|
484
|
+
policy_id: "c".repeat(64), // same → policy check passes
|
|
485
|
+
decision: "ALLOW",
|
|
486
|
+
issued_at: DELEGATION_T_ISSUED,
|
|
487
|
+
expiry: DELEGATION_T_PAR_EXP,
|
|
488
|
+
kid: "2026-01",
|
|
489
|
+
}, TEST_ONLY_ED25519_PRIVATE_KEY_PEM_DO_NOT_USE_IN_PRODUCTION);
|
|
490
|
+
const hashMismatch = verifyDelegationChain(delegation, otherParent, opts);
|
|
491
|
+
// delegator-mismatch: tamper delegator field after signing (chain check catches before sig)
|
|
492
|
+
const delegatorMismatch = verifyDelegationChain({ ...delegation, delegator: "wrong-agent" }, parent, opts);
|
|
493
|
+
// parent-expired: now === parent.expiry → DELEGATION_PARENT_EXPIRED
|
|
494
|
+
const parentExpired = verifyDelegationChain(delegation, parent, { now: DELEGATION_T_PAR_EXP });
|
|
495
|
+
// expiry-exceeds-parent: delegation.expiry > parent.expiry
|
|
496
|
+
const exceedsDelegation = createDelegation(parent, {
|
|
497
|
+
delegatee: "child-agent",
|
|
498
|
+
scope: {},
|
|
499
|
+
expiry: DELEGATION_T_PAR_EXP + 1,
|
|
500
|
+
kid: "2026-01",
|
|
501
|
+
issuedAt: DELEGATION_T_ISSUED,
|
|
502
|
+
}, TEST_ONLY_ED25519_PRIVATE_KEY_PEM_DO_NOT_USE_IN_PRODUCTION);
|
|
503
|
+
const expiryExceeds = verifyDelegationChain(exceedsDelegation, parent, opts);
|
|
504
|
+
// multi-hop: delegation passed as its own parent (has delegation_id field)
|
|
505
|
+
const multiHop = verifyDelegationChain(delegation, delegation, opts);
|
|
506
|
+
// policy-id-mismatch: tamper policy_id after signing (chain check catches before sig)
|
|
507
|
+
const policyMismatch = verifyDelegationChain({ ...delegation, policy_id: "d".repeat(64) }, parent, opts);
|
|
508
|
+
return [valid, hashMismatch, delegatorMismatch, parentExpired, expiryExceeds, multiHop, policyMismatch];
|
|
509
|
+
}
|
|
510
|
+
function validateDelegationChainVectors(ctx) {
|
|
511
|
+
const file = loadJson("delegation-chain-verification.json");
|
|
512
|
+
const actual = buildDelegationChainCases();
|
|
513
|
+
for (let i = 0; i < file.vectors.length; i++) {
|
|
514
|
+
const id = String(file.vectors[i].id);
|
|
515
|
+
const expected = asRecord(file.vectors[i].expected);
|
|
516
|
+
const got = actual[i];
|
|
517
|
+
eq(ctx, `${id} status`, got.status, String(expected.status));
|
|
518
|
+
eq(ctx, `${id} violations`, got.violations, expected.violations ?? []);
|
|
519
|
+
}
|
|
520
|
+
}
|
|
521
|
+
function buildDelegationSignatureCases() {
|
|
522
|
+
const parent = makeParentAuth();
|
|
523
|
+
const delegation = makeSignedDelegationBase(parent);
|
|
524
|
+
const chainOpts = {
|
|
525
|
+
now: DELEGATION_T_NOW,
|
|
526
|
+
trustedKeySets: DELEGATION_TEST_KEYSET,
|
|
527
|
+
requireSignatureVerification: true,
|
|
528
|
+
};
|
|
529
|
+
// valid
|
|
530
|
+
const valid = verifyDelegationChain(delegation, parent, chainOpts);
|
|
531
|
+
// tampered-signature: corrupt last bytes
|
|
532
|
+
const tamperedSig = verifyDelegationChain({ ...delegation, signature: delegation.signature.slice(0, -4) + "AAAA" }, parent, chainOpts);
|
|
533
|
+
// wrong-kid: key not found in trustedKeySets
|
|
534
|
+
const wrongKid = verifyDelegationChain({ ...delegation, kid: "unknown-kid" }, parent, chainOpts);
|
|
535
|
+
// tampered-field: mutate delegatee after signing → sig covers original payload
|
|
536
|
+
const tamperedField = verifyDelegationChain({ ...delegation, delegatee: "evil-agent" }, parent, chainOpts);
|
|
537
|
+
// expired: correctly signed but expiry already past
|
|
538
|
+
const expiredDelegation = createDelegation(parent, {
|
|
539
|
+
delegatee: "child-agent",
|
|
540
|
+
scope: {},
|
|
541
|
+
expiry: DELEGATION_T_NOW - 1,
|
|
542
|
+
kid: "2026-01",
|
|
543
|
+
issuedAt: DELEGATION_T_ISSUED,
|
|
544
|
+
}, TEST_ONLY_ED25519_PRIVATE_KEY_PEM_DO_NOT_USE_IN_PRODUCTION);
|
|
545
|
+
const expired = verifyDelegationChain(expiredDelegation, parent, chainOpts);
|
|
546
|
+
return [valid, tamperedSig, wrongKid, tamperedField, expired];
|
|
547
|
+
}
|
|
548
|
+
function validateDelegationSignatureVectors(ctx) {
|
|
549
|
+
const file = loadJson("delegation-signature-verification.json");
|
|
550
|
+
const actual = buildDelegationSignatureCases();
|
|
551
|
+
for (let i = 0; i < file.vectors.length; i++) {
|
|
552
|
+
const id = String(file.vectors[i].id);
|
|
553
|
+
const expected = asRecord(file.vectors[i].expected);
|
|
554
|
+
const got = actual[i];
|
|
555
|
+
eq(ctx, `${id} status`, got.status, String(expected.status));
|
|
556
|
+
eq(ctx, `${id} violations`, got.violations, expected.violations ?? []);
|
|
557
|
+
}
|
|
558
|
+
}
|
|
559
|
+
/**
|
|
560
|
+
* Builds a signed AuthorizationV1 in Sift-compatible wire format:
|
|
561
|
+
* - alg = "ed25519" (lowercase, nested signature object)
|
|
562
|
+
* - expires_at (not expiry)
|
|
563
|
+
* - base64url-encoded signature bytes
|
|
564
|
+
* - signing preimage: canonicalJson(unsigned) with no domain prefix
|
|
565
|
+
*
|
|
566
|
+
* The verifier accepts this via the ed25519 branch + verifyEd25519Raw fallback
|
|
567
|
+
* + base64url decoding in decodeSignatureBytes.
|
|
568
|
+
*/
|
|
569
|
+
function makeSiftWireFormatAuth(now = 1730000000) {
|
|
570
|
+
const unsigned = {
|
|
571
|
+
auth_id: "f".repeat(64),
|
|
572
|
+
issuer: "oxdeai.policy-engine",
|
|
573
|
+
audience: "merchant-gateway",
|
|
574
|
+
intent_hash: "a".repeat(64),
|
|
575
|
+
state_hash: "b".repeat(64),
|
|
576
|
+
policy_id: "c".repeat(64),
|
|
577
|
+
decision: "ALLOW",
|
|
578
|
+
issued_at: now,
|
|
579
|
+
expires_at: now + 60,
|
|
580
|
+
signature: { alg: "ed25519", kid: "2026-01" },
|
|
581
|
+
};
|
|
582
|
+
const preimage = Buffer.from(canonicalJson(unsigned), "utf8");
|
|
583
|
+
const sigBytes = nodeSign(null, preimage, TEST_ONLY_ED25519_PRIVATE_KEY_PEM_DO_NOT_USE_IN_PRODUCTION);
|
|
584
|
+
return {
|
|
585
|
+
...unsigned,
|
|
586
|
+
signature: { alg: "ed25519", kid: "2026-01", sig: sigBytes.toString("base64url") },
|
|
587
|
+
};
|
|
588
|
+
}
|
|
285
589
|
function makeSignedAuthorizationBase(now = 1730000000) {
|
|
286
590
|
return signAuthorizationEd25519({
|
|
287
591
|
auth_id: "f".repeat(64),
|
|
@@ -313,7 +617,7 @@ function validateAuthorizationSignatureVectors(ctx, adapter) {
|
|
|
313
617
|
consumedAuthIds: []
|
|
314
618
|
};
|
|
315
619
|
if (mode === "invalid-signature") {
|
|
316
|
-
auth =
|
|
620
|
+
auth = mutateSignature(auth);
|
|
317
621
|
}
|
|
318
622
|
else if (mode === "wrong-kid") {
|
|
319
623
|
auth = { ...auth, kid: "unknown-kid" };
|
|
@@ -347,6 +651,15 @@ function validateAuthorizationSignatureVectors(ctx, adapter) {
|
|
|
347
651
|
else if (mode === "unknown-alg") {
|
|
348
652
|
auth = { ...auth, alg: "Unknown" };
|
|
349
653
|
}
|
|
654
|
+
else if (mode === "sift-wire-format") {
|
|
655
|
+
auth = makeSiftWireFormatAuth();
|
|
656
|
+
}
|
|
657
|
+
else if (mode === "unsupported-alg-EdDSA") {
|
|
658
|
+
auth = { ...auth, alg: "EdDSA" };
|
|
659
|
+
}
|
|
660
|
+
else if (mode === "unsupported-alg-ED25519") {
|
|
661
|
+
auth = { ...auth, alg: "ED25519" };
|
|
662
|
+
}
|
|
350
663
|
const got = adapter.verifyAuthorization(auth, opts);
|
|
351
664
|
eq(ctx, `${id} status`, got.status, String(expected.status));
|
|
352
665
|
eq(ctx, `${id} violations`, got.violations, expected.violations ?? []);
|
|
@@ -426,7 +739,7 @@ function buildEnvelopeCases(adapter) {
|
|
|
426
739
|
tool_call: true,
|
|
427
740
|
type: "EXECUTE"
|
|
428
741
|
});
|
|
429
|
-
const out = engine.evaluatePure(intent, state);
|
|
742
|
+
const out = engine.evaluatePure(intent, state, intent.timestamp);
|
|
430
743
|
if (out.decision !== "ALLOW")
|
|
431
744
|
throw new Error("expected ALLOW for envelope cases");
|
|
432
745
|
const events = engine.audit.snapshot();
|
|
@@ -460,11 +773,11 @@ function buildEnvelopeCases(adapter) {
|
|
|
460
773
|
});
|
|
461
774
|
const corrupt = new Uint8Array([1, 2, 3, 4, 5]);
|
|
462
775
|
return [
|
|
463
|
-
adapter.verifyEnvelope(withCheckpoint, { expectedPolicyId: engine.computePolicyId(), mode: "strict" }),
|
|
776
|
+
adapter.verifyEnvelope(withCheckpoint, { expectedPolicyId: engine.computePolicyId(), mode: "strict", trustedKeySets: TEST_KEYSET }),
|
|
464
777
|
adapter.verifyEnvelope(valid, { expectedPolicyId: engine.computePolicyId(), mode: "best-effort" }),
|
|
465
778
|
adapter.verifyEnvelope(mismatched, { expectedPolicyId: engine.computePolicyId(), mode: "best-effort" }),
|
|
466
779
|
adapter.verifyEnvelope(corrupt, { expectedPolicyId: engine.computePolicyId(), mode: "best-effort" }),
|
|
467
|
-
adapter.verifyEnvelope(valid, { expectedPolicyId: engine.computePolicyId(), mode: "strict" })
|
|
780
|
+
adapter.verifyEnvelope(valid, { expectedPolicyId: engine.computePolicyId(), mode: "strict", trustedKeySets: TEST_KEYSET })
|
|
468
781
|
];
|
|
469
782
|
}
|
|
470
783
|
function validateEnvelopeVectors(ctx, adapter) {
|
|
@@ -639,24 +952,403 @@ function validateAuditVerificationVectors(ctx, adapter) {
|
|
|
639
952
|
eq(ctx, `${id} violations`, got.violations, expected.violations ?? []);
|
|
640
953
|
}
|
|
641
954
|
}
|
|
955
|
+
// ── Profile C: Semantic State Verification ────────────────────────────────────
|
|
956
|
+
//
|
|
957
|
+
// Tests the guard's step 10 behavior: computeStateHash(liveState) must equal
|
|
958
|
+
// authorization.state_hash. Two hash strategies are modelled:
|
|
959
|
+
// "core" — sha256HexFromJson (standard OxDeAI; matches stateSnapshotHash for flat objects)
|
|
960
|
+
// "provider" — SHA-256 of "PROVIDER:" + canonicalJson (simulates an external provider
|
|
961
|
+
// algorithm such as siftCanonicalJsonHash; distinct from "core")
|
|
962
|
+
// "throws" — always throws (models a broken or unavailable hash implementation)
|
|
963
|
+
//
|
|
964
|
+
// Encoding B vectors additionally exercise selected Profile-C semantics: Sift-compatible wire format
|
|
965
|
+
// (alg="ed25519", expires_at, base64url sig) + live-state semantic verification.
|
|
966
|
+
function profileCCoreHash(state) {
|
|
967
|
+
return sha256HexFromJson(state);
|
|
968
|
+
}
|
|
969
|
+
function profileCProviderHash(state) {
|
|
970
|
+
// Deterministic but distinct from coreHash — simulates an external provider algorithm.
|
|
971
|
+
return createHash("sha256")
|
|
972
|
+
.update("PROVIDER:" + canonicalJson(state), "utf8")
|
|
973
|
+
.digest("hex");
|
|
974
|
+
}
|
|
975
|
+
function profileCGetHashFn(strategy) {
|
|
976
|
+
if (strategy === "core")
|
|
977
|
+
return profileCCoreHash;
|
|
978
|
+
if (strategy === "provider")
|
|
979
|
+
return profileCProviderHash;
|
|
980
|
+
if (strategy === "throws")
|
|
981
|
+
return (_s) => { throw new Error("hash backend unavailable"); };
|
|
982
|
+
throw new Error(`unknown hash_strategy: ${strategy}`);
|
|
983
|
+
}
|
|
984
|
+
// ── Key lifecycle constants ───────────────────────────────────────────────────
|
|
985
|
+
const KL_SIGNING_NOW = 1730000000;
|
|
986
|
+
const KL_VERIFY_NOW = 1730000010;
|
|
987
|
+
const KL_PAST = KL_SIGNING_NOW - 7200; // 2 h before signing
|
|
988
|
+
const KL_FUTURE = KL_SIGNING_NOW + 7200; // 2 h after signing
|
|
989
|
+
function makeKLAuth() {
|
|
990
|
+
return signAuthorizationEd25519({
|
|
991
|
+
auth_id: "e".repeat(64),
|
|
992
|
+
issuer: "kl.issuer",
|
|
993
|
+
audience: "kl.audience",
|
|
994
|
+
intent_hash: "a".repeat(64),
|
|
995
|
+
state_hash: "b".repeat(64),
|
|
996
|
+
policy_id: "c".repeat(64),
|
|
997
|
+
decision: "ALLOW",
|
|
998
|
+
issued_at: KL_SIGNING_NOW,
|
|
999
|
+
expiry: KL_SIGNING_NOW + 300,
|
|
1000
|
+
kid: "kl-key-001",
|
|
1001
|
+
}, TEST_ONLY_ED25519_PRIVATE_KEY_PEM_DO_NOT_USE_IN_PRODUCTION);
|
|
1002
|
+
}
|
|
1003
|
+
function makeKLKeyEntry(overrides = {}) {
|
|
1004
|
+
return {
|
|
1005
|
+
kid: "kl-key-001",
|
|
1006
|
+
alg: "Ed25519",
|
|
1007
|
+
public_key: TEST_ONLY_ED25519_PUBLIC_KEY_PEM_DO_NOT_USE_IN_PRODUCTION,
|
|
1008
|
+
...overrides,
|
|
1009
|
+
};
|
|
1010
|
+
}
|
|
1011
|
+
function makeKLKeyset(entry) {
|
|
1012
|
+
return { issuer: "kl.issuer", version: "1", keys: [entry] };
|
|
1013
|
+
}
|
|
1014
|
+
function klVerifyOpts(keyset) {
|
|
1015
|
+
return {
|
|
1016
|
+
now: KL_VERIFY_NOW,
|
|
1017
|
+
expectedIssuer: "kl.issuer",
|
|
1018
|
+
expectedAudience: "kl.audience",
|
|
1019
|
+
expectedPolicyId: "c".repeat(64),
|
|
1020
|
+
trustedKeySets: keyset,
|
|
1021
|
+
requireSignatureVerification: true,
|
|
1022
|
+
consumedAuthIds: [],
|
|
1023
|
+
};
|
|
1024
|
+
}
|
|
1025
|
+
function validateKeyLifecycleVectors(ctx, adapter) {
|
|
1026
|
+
const file = loadJson("key-lifecycle-verification.json");
|
|
1027
|
+
const auth = makeKLAuth();
|
|
1028
|
+
for (const v of file.vectors) {
|
|
1029
|
+
const id = String(v.id);
|
|
1030
|
+
const mode = String(v.mode);
|
|
1031
|
+
const expected = asRecord(v.expected);
|
|
1032
|
+
let keyset;
|
|
1033
|
+
if (mode === "key-active") {
|
|
1034
|
+
keyset = makeKLKeyset(makeKLKeyEntry({ status: "active" }));
|
|
1035
|
+
}
|
|
1036
|
+
else if (mode === "key-revoked") {
|
|
1037
|
+
keyset = makeKLKeyset(makeKLKeyEntry({ status: "revoked" }));
|
|
1038
|
+
}
|
|
1039
|
+
else if (mode === "key-not-before-future") {
|
|
1040
|
+
keyset = makeKLKeyset(makeKLKeyEntry({ status: "active", not_before: KL_FUTURE }));
|
|
1041
|
+
}
|
|
1042
|
+
else if (mode === "key-not-after-past") {
|
|
1043
|
+
keyset = makeKLKeyset(makeKLKeyEntry({ status: "active", not_after: KL_PAST }));
|
|
1044
|
+
}
|
|
1045
|
+
else if (mode === "key-valid-window") {
|
|
1046
|
+
keyset = makeKLKeyset(makeKLKeyEntry({ status: "active", not_before: KL_PAST, not_after: KL_FUTURE }));
|
|
1047
|
+
}
|
|
1048
|
+
else if (mode === "key-expired-window") {
|
|
1049
|
+
keyset = makeKLKeyset(makeKLKeyEntry({ status: "active", not_before: KL_PAST - 3600, not_after: KL_PAST }));
|
|
1050
|
+
}
|
|
1051
|
+
else if (mode === "key-retired-within-window") {
|
|
1052
|
+
keyset = makeKLKeyset(makeKLKeyEntry({ status: "retired", not_before: KL_PAST, not_after: KL_FUTURE }));
|
|
1053
|
+
}
|
|
1054
|
+
else if (mode === "key-retired-past-window") {
|
|
1055
|
+
keyset = makeKLKeyset(makeKLKeyEntry({ status: "retired", not_after: KL_PAST }));
|
|
1056
|
+
}
|
|
1057
|
+
else if (mode === "key-revoked-valid-window") {
|
|
1058
|
+
keyset = makeKLKeyset(makeKLKeyEntry({ status: "revoked", not_before: KL_PAST, not_after: KL_FUTURE }));
|
|
1059
|
+
}
|
|
1060
|
+
else if (mode === "wrong-kid-known-issuer") {
|
|
1061
|
+
keyset = makeKLKeyset(makeKLKeyEntry({ kid: "different-key-id", status: "active" }));
|
|
1062
|
+
}
|
|
1063
|
+
else {
|
|
1064
|
+
fail(ctx, `${id}: unknown mode "${mode}"`);
|
|
1065
|
+
continue;
|
|
1066
|
+
}
|
|
1067
|
+
const got = adapter.verifyAuthorization(auth, klVerifyOpts(keyset));
|
|
1068
|
+
eq(ctx, `${id} status`, got.status, String(expected.status));
|
|
1069
|
+
eq(ctx, `${id} violations`, got.violations, expected.violations ?? []);
|
|
1070
|
+
}
|
|
1071
|
+
}
|
|
1072
|
+
function validateProfileCStateVerificationVectors(ctx, adapter) {
|
|
1073
|
+
const file = loadJson("profile-c-state-verification.json");
|
|
1074
|
+
for (const v of file.vectors) {
|
|
1075
|
+
const id = String(v.id);
|
|
1076
|
+
const mode = String(v.mode);
|
|
1077
|
+
const expected = asRecord(v.expected);
|
|
1078
|
+
const expectedStatus = String(expected.status);
|
|
1079
|
+
// ── Pure state-hash comparison modes ─────────────────────────────────────
|
|
1080
|
+
// These modes test the semantic comparison (step 10) in isolation.
|
|
1081
|
+
// They do not involve a real AuthorizationV1 artifact — they model the
|
|
1082
|
+
// guard's computeStateHash(liveState) == authorization.state_hash logic.
|
|
1083
|
+
if (mode === "live-state-match" ||
|
|
1084
|
+
mode === "live-state-mismatch" ||
|
|
1085
|
+
mode === "hash-strategy-mismatch" ||
|
|
1086
|
+
mode === "toctou-stale-state") {
|
|
1087
|
+
const stateInput = v.state_input;
|
|
1088
|
+
const liveStateInput = (v.live_state_input ?? v.state_input);
|
|
1089
|
+
const signingStrategy = String((v.signing_strategy ?? v.hash_strategy) ?? "core");
|
|
1090
|
+
const verifyStrategy = String((v.verify_strategy ?? v.hash_strategy) ?? "core");
|
|
1091
|
+
const signingHashFn = profileCGetHashFn(signingStrategy);
|
|
1092
|
+
const committedHash = signingHashFn(stateInput);
|
|
1093
|
+
let liveHash;
|
|
1094
|
+
let threw = false;
|
|
1095
|
+
try {
|
|
1096
|
+
liveHash = profileCGetHashFn(verifyStrategy)(liveStateInput);
|
|
1097
|
+
}
|
|
1098
|
+
catch {
|
|
1099
|
+
threw = true;
|
|
1100
|
+
}
|
|
1101
|
+
if (threw) {
|
|
1102
|
+
eq(ctx, `${id} outcome`, "compute-error", expectedStatus);
|
|
1103
|
+
}
|
|
1104
|
+
else if (liveHash === committedHash) {
|
|
1105
|
+
eq(ctx, `${id} outcome`, "ok", expectedStatus);
|
|
1106
|
+
}
|
|
1107
|
+
else {
|
|
1108
|
+
eq(ctx, `${id} outcome`, "state-hash-mismatch", expectedStatus);
|
|
1109
|
+
}
|
|
1110
|
+
continue;
|
|
1111
|
+
}
|
|
1112
|
+
// ── compute-throws mode ───────────────────────────────────────────────────
|
|
1113
|
+
if (mode === "compute-throws") {
|
|
1114
|
+
let threw = false;
|
|
1115
|
+
try {
|
|
1116
|
+
profileCGetHashFn("throws")(null);
|
|
1117
|
+
}
|
|
1118
|
+
catch {
|
|
1119
|
+
threw = true;
|
|
1120
|
+
}
|
|
1121
|
+
eq(ctx, `${id} throws`, threw, true);
|
|
1122
|
+
eq(ctx, `${id} outcome`, threw ? "compute-error" : "ok", expectedStatus);
|
|
1123
|
+
continue;
|
|
1124
|
+
}
|
|
1125
|
+
// ── Encoding B modes ──────────────────────────────────────────────────────
|
|
1126
|
+
// These consume a committed Sift-compatible (Encoding B) AuthorizationV1 artifact
|
|
1127
|
+
// with state_hash = signingHashFn(state_input), verify the signature
|
|
1128
|
+
// (exercising the ed25519/expires_at/base64url path), then simulate
|
|
1129
|
+
// Profile C step 10: computeStateHash(live_state_input) == authorization.state_hash.
|
|
1130
|
+
if (mode === "encoding-b-live-state-match" ||
|
|
1131
|
+
mode === "encoding-b-live-state-mismatch" ||
|
|
1132
|
+
mode === "encoding-b-hash-strategy-mismatch") {
|
|
1133
|
+
const stateInput = v.state_input;
|
|
1134
|
+
const liveStateInput = (v.live_state_input ?? v.state_input);
|
|
1135
|
+
const signingStrategy = String((v.signing_strategy ?? v.hash_strategy) ?? "provider");
|
|
1136
|
+
const verifyStrategy = String((v.verify_strategy ?? v.hash_strategy) ?? "provider");
|
|
1137
|
+
const committedHash = profileCGetHashFn(signingStrategy)(stateInput);
|
|
1138
|
+
// Consume the committed authority fixture preserved by the one-way projection.
|
|
1139
|
+
// Do not synthesize/re-sign an artifact: Go and Python verify these same bytes.
|
|
1140
|
+
const auth = v.auth;
|
|
1141
|
+
const opts = asRecord(v.opts);
|
|
1142
|
+
const trustedKey = asRecord(opts.trustedKey);
|
|
1143
|
+
eq(ctx, `${id} committed state hash`, auth.state_hash, committedHash);
|
|
1144
|
+
const sigResult = adapter.verifyAuthorization(auth, {
|
|
1145
|
+
now: Number(opts.now),
|
|
1146
|
+
expectedIssuer: "oxdeai.policy-engine",
|
|
1147
|
+
expectedAudience: "merchant-gateway",
|
|
1148
|
+
trustedKeySets: {
|
|
1149
|
+
issuer: "oxdeai.policy-engine",
|
|
1150
|
+
version: "1",
|
|
1151
|
+
keys: [{ kid: String(trustedKey.kid), alg: trustedKey.alg, public_key: String(trustedKey.public_key) }],
|
|
1152
|
+
},
|
|
1153
|
+
requireSignatureVerification: true,
|
|
1154
|
+
consumedAuthIds: [],
|
|
1155
|
+
});
|
|
1156
|
+
if (sigResult.status !== "ok") {
|
|
1157
|
+
fail(ctx, `${id} signature: expected ok, got ${sigResult.status} — ${JSON.stringify(sigResult.violations)}`);
|
|
1158
|
+
continue;
|
|
1159
|
+
}
|
|
1160
|
+
pass(ctx, `${id} signature`);
|
|
1161
|
+
// Step 2: semantic state verification (Profile C step 10).
|
|
1162
|
+
let liveHash;
|
|
1163
|
+
let threw = false;
|
|
1164
|
+
try {
|
|
1165
|
+
liveHash = profileCGetHashFn(verifyStrategy)(liveStateInput);
|
|
1166
|
+
}
|
|
1167
|
+
catch {
|
|
1168
|
+
threw = true;
|
|
1169
|
+
}
|
|
1170
|
+
let outcome;
|
|
1171
|
+
if (threw) {
|
|
1172
|
+
outcome = "compute-error";
|
|
1173
|
+
}
|
|
1174
|
+
else if (liveHash === committedHash) {
|
|
1175
|
+
outcome = "ok";
|
|
1176
|
+
}
|
|
1177
|
+
else {
|
|
1178
|
+
outcome = "state-hash-mismatch";
|
|
1179
|
+
}
|
|
1180
|
+
eq(ctx, `${id} outcome`, outcome, expectedStatus);
|
|
1181
|
+
continue;
|
|
1182
|
+
}
|
|
1183
|
+
fail(ctx, `${id}: unknown mode "${mode}"`);
|
|
1184
|
+
}
|
|
1185
|
+
}
|
|
1186
|
+
// ── SignedKRLV1 verification ───────────────────────────────────────────────────
|
|
1187
|
+
const KRL_ISSUER = "krl.issuer";
|
|
1188
|
+
const KRL_KID = "krl-2026-01";
|
|
1189
|
+
const KRL_T_ISSUED = 1_730_000_000;
|
|
1190
|
+
const KRL_T_NOW = 1_730_000_010;
|
|
1191
|
+
const KRL_T_NOT_AFTER = 1_730_003_600;
|
|
1192
|
+
const KRL_TRUSTED_KEYSET = {
|
|
1193
|
+
issuer: KRL_ISSUER,
|
|
1194
|
+
version: "1",
|
|
1195
|
+
keys: [{
|
|
1196
|
+
kid: KRL_KID,
|
|
1197
|
+
alg: "Ed25519",
|
|
1198
|
+
public_key: KRL_TEST_ONLY_ED25519_PUBLIC_KEY_PEM_DO_NOT_USE_IN_PRODUCTION,
|
|
1199
|
+
}],
|
|
1200
|
+
};
|
|
1201
|
+
function buildValidKrlBase() {
|
|
1202
|
+
return {
|
|
1203
|
+
version: "SignedKRLV1",
|
|
1204
|
+
issuer: KRL_ISSUER,
|
|
1205
|
+
krl_version: 1,
|
|
1206
|
+
issued_at: KRL_T_ISSUED,
|
|
1207
|
+
not_after: KRL_T_NOT_AFTER,
|
|
1208
|
+
revoked_kids: ["kid-provider-1", "kid-provider-2"],
|
|
1209
|
+
};
|
|
1210
|
+
}
|
|
1211
|
+
function signValidKrl(override) {
|
|
1212
|
+
const base = { ...buildValidKrlBase(), ...override };
|
|
1213
|
+
const envelope = {
|
|
1214
|
+
...base,
|
|
1215
|
+
signature: { alg: "Ed25519", kid: KRL_KID, sig: "" },
|
|
1216
|
+
};
|
|
1217
|
+
const payload = signedKrlSigningPayload(envelope);
|
|
1218
|
+
const sig = signEd25519(SIGNING_DOMAINS.KRL_V1, payload, KRL_TEST_ONLY_ED25519_PRIVATE_KEY_PEM_DO_NOT_USE_IN_PRODUCTION);
|
|
1219
|
+
return { ...envelope, signature: { alg: "Ed25519", kid: KRL_KID, sig } };
|
|
1220
|
+
}
|
|
1221
|
+
function validateSignedKrlVectors(ctx) {
|
|
1222
|
+
const file = loadJson("signed-krl-verification.json");
|
|
1223
|
+
for (const v of file.vectors) {
|
|
1224
|
+
const id = String(v.id);
|
|
1225
|
+
const mode = String(v.mode);
|
|
1226
|
+
const expected = asRecord(v.expected);
|
|
1227
|
+
let result;
|
|
1228
|
+
if (mode === "valid") {
|
|
1229
|
+
// Valid KRL — signature verifies, not expired
|
|
1230
|
+
const krl = signValidKrl();
|
|
1231
|
+
result = verifySignedKrl(krl, { now: KRL_T_NOW, trustedKeySets: KRL_TRUSTED_KEYSET });
|
|
1232
|
+
}
|
|
1233
|
+
else if (mode === "invalid-signature") {
|
|
1234
|
+
// Tamper last 4 base64 characters of the signature
|
|
1235
|
+
const krl = signValidKrl();
|
|
1236
|
+
const tampered = {
|
|
1237
|
+
...krl,
|
|
1238
|
+
signature: { ...krl.signature, sig: krl.signature.sig.slice(0, -4) + "AAAA" },
|
|
1239
|
+
};
|
|
1240
|
+
result = verifySignedKrl(tampered, { now: KRL_T_NOW, trustedKeySets: KRL_TRUSTED_KEYSET });
|
|
1241
|
+
}
|
|
1242
|
+
else if (mode === "expired") {
|
|
1243
|
+
// Verification time is one second past not_after
|
|
1244
|
+
const krl = signValidKrl();
|
|
1245
|
+
result = verifySignedKrl(krl, { now: KRL_T_NOT_AFTER + 1, trustedKeySets: KRL_TRUSTED_KEYSET });
|
|
1246
|
+
}
|
|
1247
|
+
else if (mode === "malformed-revoked-kids") {
|
|
1248
|
+
// revoked_kids is a string, not an array — structural malformation
|
|
1249
|
+
const raw = {
|
|
1250
|
+
...buildValidKrlBase(),
|
|
1251
|
+
revoked_kids: "not-an-array",
|
|
1252
|
+
signature: { alg: "Ed25519", kid: KRL_KID, sig: "placeholder" },
|
|
1253
|
+
};
|
|
1254
|
+
result = verifySignedKrl(raw, { now: KRL_T_NOW, trustedKeySets: KRL_TRUSTED_KEYSET });
|
|
1255
|
+
}
|
|
1256
|
+
else if (mode === "duplicate-revoked-kids") {
|
|
1257
|
+
// Valid signature but revoked_kids has duplicate entries
|
|
1258
|
+
const krl = signValidKrl({ revoked_kids: ["kid-1", "kid-1"] });
|
|
1259
|
+
result = verifySignedKrl(krl, { now: KRL_T_NOW, trustedKeySets: KRL_TRUSTED_KEYSET });
|
|
1260
|
+
}
|
|
1261
|
+
else if (mode === "unknown-signing-kid") {
|
|
1262
|
+
// kid tampered after signing — not found in trusted key sets
|
|
1263
|
+
const krl = signValidKrl();
|
|
1264
|
+
const wrongKid = {
|
|
1265
|
+
...krl,
|
|
1266
|
+
signature: { ...krl.signature, kid: "unknown-kid" },
|
|
1267
|
+
};
|
|
1268
|
+
result = verifySignedKrl(wrongKid, { now: KRL_T_NOW, trustedKeySets: KRL_TRUSTED_KEYSET });
|
|
1269
|
+
}
|
|
1270
|
+
else if (mode === "signing-key-inactive") {
|
|
1271
|
+
// The correct kid exists in the trusted set but is marked revoked
|
|
1272
|
+
const krl = signValidKrl();
|
|
1273
|
+
const revokedKeyset = {
|
|
1274
|
+
...KRL_TRUSTED_KEYSET,
|
|
1275
|
+
keys: [{ ...KRL_TRUSTED_KEYSET.keys[0], status: "revoked" }],
|
|
1276
|
+
};
|
|
1277
|
+
result = verifySignedKrl(krl, { now: KRL_T_NOW, trustedKeySets: revokedKeyset });
|
|
1278
|
+
}
|
|
1279
|
+
else if (mode === "unsupported-alg") {
|
|
1280
|
+
// signature.alg is not "Ed25519" — structural KRL_UNSUPPORTED_ALG before sig check
|
|
1281
|
+
const raw = {
|
|
1282
|
+
...buildValidKrlBase(),
|
|
1283
|
+
signature: { alg: "HMAC-SHA256", kid: KRL_KID, sig: "placeholder" },
|
|
1284
|
+
};
|
|
1285
|
+
result = verifySignedKrl(raw, { now: KRL_T_NOW, trustedKeySets: KRL_TRUSTED_KEYSET });
|
|
1286
|
+
}
|
|
1287
|
+
else if (mode === "version-regression") {
|
|
1288
|
+
// krl_version=1 but caller knows previous version was 5
|
|
1289
|
+
const krl = signValidKrl(); // krl_version = 1
|
|
1290
|
+
result = verifySignedKrl(krl, {
|
|
1291
|
+
now: KRL_T_NOW,
|
|
1292
|
+
trustedKeySets: KRL_TRUSTED_KEYSET,
|
|
1293
|
+
previousKrlVersionByIssuer: { [KRL_ISSUER]: 5 },
|
|
1294
|
+
});
|
|
1295
|
+
}
|
|
1296
|
+
else {
|
|
1297
|
+
fail(ctx, `${id}: unknown mode "${mode}"`);
|
|
1298
|
+
continue;
|
|
1299
|
+
}
|
|
1300
|
+
eq(ctx, `${id} status`, result.status, String(expected.status));
|
|
1301
|
+
eq(ctx, `${id} violations`, result.violations, expected.violations ?? []);
|
|
1302
|
+
}
|
|
1303
|
+
}
|
|
642
1304
|
function main() {
|
|
643
1305
|
const ctx = { failures: [], passed: 0 };
|
|
644
1306
|
const adapter = coreAdapter;
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
1307
|
+
if (!jsonOutput)
|
|
1308
|
+
console.log(`Running conformance validation with ${adapter.name}`);
|
|
1309
|
+
try {
|
|
1310
|
+
validateIntentHashVectors(ctx, adapter);
|
|
1311
|
+
validateAuthorizationVectors(ctx, adapter);
|
|
1312
|
+
const trustedTime = runTrustedTimeConformance(loadJson("trusted-time.json"), line => {
|
|
1313
|
+
const match = /^(PASS|FAIL) ([^ :]+)/.exec(line);
|
|
1314
|
+
if (match)
|
|
1315
|
+
recordCase(match[2], match[1] === "PASS");
|
|
1316
|
+
if (!jsonOutput)
|
|
1317
|
+
console.log(line);
|
|
1318
|
+
});
|
|
1319
|
+
ctx.passed += trustedTime.passed;
|
|
1320
|
+
ctx.failures.push(...trustedTime.failures);
|
|
1321
|
+
validateAuthorizationVerificationVectors(ctx, adapter);
|
|
1322
|
+
validateAuthorizationSignatureVectors(ctx, adapter);
|
|
1323
|
+
validateSnapshotVectors(ctx, adapter);
|
|
1324
|
+
validateAuditChainVectors(ctx, adapter);
|
|
1325
|
+
validateAuditVerificationVectors(ctx, adapter);
|
|
1326
|
+
validateEnvelopeVectors(ctx, adapter);
|
|
1327
|
+
validateEnvelopeSignatureVectors(ctx, adapter);
|
|
1328
|
+
validateDelegationParentHashVectors(ctx);
|
|
1329
|
+
validateDelegationVerificationVectors(ctx);
|
|
1330
|
+
validateDelegationChainVectors(ctx);
|
|
1331
|
+
validateDelegationSignatureVectors(ctx);
|
|
1332
|
+
validateKeyLifecycleVectors(ctx, adapter);
|
|
1333
|
+
validateClockSemanticsVectors(ctx, adapter);
|
|
1334
|
+
validateProfileCStateVerificationVectors(ctx, adapter);
|
|
1335
|
+
validateSignedKrlVectors(ctx);
|
|
1336
|
+
}
|
|
1337
|
+
catch (error) {
|
|
1338
|
+
ctx.failures.push(error instanceof Error ? error.message : String(error));
|
|
1339
|
+
}
|
|
1340
|
+
const scope = evidenceScope({ consumer: "packages/conformance/src/validate.ts", runtime: "TypeScript", selections: [...exercised].map(([representation, data]) => {
|
|
1341
|
+
const ids = data.vectors.map(v => String(v.id)).filter(id => caseResults.has(id));
|
|
1342
|
+
return { representation, data, consumer: "packages/conformance/src/validate.ts", runtime: "TypeScript", caseIds: ids, passedCaseIds: ids.filter(id => caseResults.get(id)) };
|
|
1343
|
+
}) });
|
|
1344
|
+
console.log(JSON.stringify({ result: ctx.failures.length ? "FAIL" : "PASS", passed: ctx.passed, failures: ctx.failures, evidenceScope: scope }));
|
|
655
1345
|
if (ctx.failures.length > 0) {
|
|
656
1346
|
console.error(`\nConformance failed: ${ctx.failures.length} assertion(s)`);
|
|
657
|
-
process.
|
|
1347
|
+
process.exitCode = 1;
|
|
1348
|
+
return;
|
|
658
1349
|
}
|
|
659
|
-
|
|
1350
|
+
if (!jsonOutput)
|
|
1351
|
+
console.log(`\nConformance passed: ${ctx.passed} assertions (declared evidenceScope only)`);
|
|
660
1352
|
}
|
|
661
1353
|
main();
|
|
662
1354
|
//# sourceMappingURL=validate.js.map
|