diff --git a/test-vectors/README.md b/test-vectors/README.md new file mode 100644 index 0000000..1378f2d --- /dev/null +++ b/test-vectors/README.md @@ -0,0 +1,49 @@ +# Test vectors + +Reference fixtures for Knowledge Unit interoperability. + +Shipped to support the cross-verify work tracked in +[issue #2](https://github.com/VeritasActa/verify/issues/2) with +[agent-passport-system](https://www.npmjs.com/package/agent-passport-system). + +## Files + +| File | Purpose | +|------|---------| +| [`jcs-test-vectors.json`](jcs-test-vectors.json) | JCS canonicalization conformance. 9 cases. Feed each `input` to your canonicalizer and compare the output string and SHA-256 to the expected values. Includes the AIP-0001 ASCII-only key restriction case. | +| [`cross-verify-bundle.json`](cross-verify-bundle.json) | One complete Knowledge Unit: 4 models × 2 rounds + 1 synthesis + 1 aggregate = 10 Ed25519-signed receipts, hash-chained via `payload.previousReceiptHash`. Includes an `external_receipts.aps` slot for an APS `DecisionLineageReceipt` to drop in. | +| [`selective-disclosure-salted-commit.json`](selective-disclosure-salted-commit.json) | AIP-0002 salted SHA-256 commitment reference. The Grok-4.20 round-1 dissenting response is published with `response.position` and `response.confidence` replaced by commitments. The unsigned `witness` block carries salts + plaintext to unlock them. | +| [`generate.mjs`](generate.mjs) | The generator. Deterministic: same seed produces byte-identical output. Run `node generate.mjs ` from a workspace with `@veritasacta/artifacts` installed. | + +## Verification + +```bash +# Individual receipt +npx @veritasacta/verify test-vectors/cross-verify-bundle.json --bundle +# → Bundle: VALID +# Total: 10 Passed: 10 Failed: 0 + +# Selective-disclosure receipt (redacted shape, signature over redacted payload) +npx @veritasacta/verify test-vectors/selective-disclosure-salted-commit.json +# Note: CLI input shape here is the `redacted_receipt` field; pass it directly +# to the CLI if your tooling needs a single-artifact input. +``` + +## Interop expectations + +For a third-party canonicalizer + verifier to pass: + +1. For each entry in `jcs-test-vectors.json`, output bytes equal `canonical` and SHA-256 equals `sha256`. +2. For the ASCII-only case, the canonicalizer MUST throw when a key contains non-ASCII. +3. For `cross-verify-bundle.json`, every receipt individually verifies under the + `verification.signing_keys[].kid` that matches its `kid` field. +4. For `selective-disclosure-salted-commit.json`, the redacted receipt verifies + without the witness, and for each entry in `witness.disclosures`: + `sha256(salt + ":" + JSON.stringify(plaintext))` equals `expected_commitment`. + +## Determinism + +All signing keys derive from a fixed seed via SHA-256. Salts and timestamps are +constants. Re-running `generate.mjs` produces byte-identical files. If a later +`@veritasacta/artifacts` version changes canonicalization in a non-compatible +way, these fixtures will reveal it immediately. diff --git a/test-vectors/cross-verify-bundle.json b/test-vectors/cross-verify-bundle.json new file mode 100644 index 0000000..cabc406 --- /dev/null +++ b/test-vectors/cross-verify-bundle.json @@ -0,0 +1,338 @@ +{ + "format": "veritasacta:knowledge-unit-bundle:v1", + "spec": "draft-farley-acta-knowledge-units-00", + "ku_id": "ku_4b3f7c2a9d8e1f05", + "generated_at": "2026-04-17T12:00:00Z", + "description": "Complete Knowledge Unit deliberation: 4 models × 2 rounds + 1 synthesis + 1 aggregate = 10 Ed25519-signed receipts. Hash-chained via payload.previousReceiptHash over JCS-canonical bytes. Every receipt is individually verifiable with @veritasacta/verify; the bundle is verifiable with --bundle.", + "verification": { + "signing_keys": [ + { + "kty": "OKP", + "crv": "Ed25519", + "kid": "Z-LI5A7pV_q5L0741YBXwcOCo1U13ONImZ4OkQnKkg0", + "x": "tiTKo_UAbc-jbX6rtdupskLamLZPIXSC30y6dPPHRWQ", + "use": "sig", + "issuer": "ku:model:claude-opus-4.6" + }, + { + "kty": "OKP", + "crv": "Ed25519", + "kid": "yH110mxzFmSFg-rqk93G6orppuIMvxJaBBCbtB2BF1Y", + "x": "wLeHCJz2lKADNwV1pNMEtFbsbostfaXVjtfhEcauPs4", + "use": "sig", + "issuer": "ku:model:gpt-5" + }, + { + "kty": "OKP", + "crv": "Ed25519", + "kid": "EOahLGtg57mQJhgTwQc-cTmZZPTVn_NGal6SH5taqJw", + "x": "zwJOHnOSOTkkh37-GuNxC41o54UX-Yjnn6FN_1JKJDo", + "use": "sig", + "issuer": "ku:model:gemini-2.5-pro" + }, + { + "kty": "OKP", + "crv": "Ed25519", + "kid": "MdZxWfPWCPrjb4blQEWBKFyk9FkazXCgk2K7QFiuYRw", + "x": "nQIE0pSO6yXnABIt3SdC01oyyLa-J1i9Wf5o1lDUtoc", + "use": "sig", + "issuer": "ku:model:grok-4.20" + }, + { + "kty": "OKP", + "crv": "Ed25519", + "kid": "YJMzwAk4gkCxTzWPmVAlcLtr517adDfl9gxGCLP4bDw", + "x": "Uy9EM6Buox0IJ9Ry0QiPtPuxj-7oZedl3_ogRzpXSd4", + "use": "sig", + "issuer": "ku:arbiter:scopeblind-reference" + } + ] + }, + "external_receipts": { + "aps": { + "description": "Drop-in slot for an APS DecisionLineageReceipt that references ku_id=ku_4b3f7c2a9d8e1f05. When populated, both @veritasacta/verify and agent-passport-system verifiers should exit 0 over the same bundle.", + "ku_id": "ku_4b3f7c2a9d8e1f05", + "expected_fields": [ + "subject", + "scope", + "ku_id", + "receipt_hash", + "signature" + ], + "receipt_uri": null, + "receipt": null + } + }, + "receipts": [ + { + "v": 2, + "type": "veritasacta:knowledge_unit:round_response", + "algorithm": "ed25519", + "kid": "Z-LI5A7pV_q5L0741YBXwcOCo1U13ONImZ4OkQnKkg0", + "issuer": "ku:model:claude-opus-4.6", + "issued_at": "2026-04-17T12:00:01Z", + "payload": { + "ku_id": "ku_4b3f7c2a9d8e1f05", + "sequence": 1, + "round": 1, + "model": { + "id": "claude-opus-4.6", + "vendor": "anthropic" + }, + "topic_digest": "a0431c892bf3d1d67c63726c9a7cf00c6270b39098c8e9cdcfdf26bb73825b24", + "response": { + "position": "quadratic", + "confidence": 0.72 + } + }, + "signature": "b51bdf0dd826b63dd84d0e0d50e3700b48a5213adeebaefff7db9c3b109ba935dc70e78189fc0d306b10aa6337e32a8e74e92300e6e7eae91f76a282c2a8b609" + }, + { + "v": 2, + "type": "veritasacta:knowledge_unit:round_response", + "algorithm": "ed25519", + "kid": "yH110mxzFmSFg-rqk93G6orppuIMvxJaBBCbtB2BF1Y", + "issuer": "ku:model:gpt-5", + "issued_at": "2026-04-17T12:00:02Z", + "payload": { + "ku_id": "ku_4b3f7c2a9d8e1f05", + "sequence": 2, + "round": 1, + "model": { + "id": "gpt-5", + "vendor": "openai" + }, + "topic_digest": "a0431c892bf3d1d67c63726c9a7cf00c6270b39098c8e9cdcfdf26bb73825b24", + "response": { + "position": "quadratic", + "confidence": 0.65 + }, + "previousReceiptHash": "sha256:2b06eb3385bcfa7bf29d59ae693c28124062fdb9b548a9710c1ee44f2eeae192" + }, + "signature": "14fa53712789d72d727ea7c99cf109181b42de2b6baaa76c97f8c0ac1633e667d2888672a181ddf896636e3cabb441ec68d3f4bc23f85a9f47ccc3980619680f" + }, + { + "v": 2, + "type": "veritasacta:knowledge_unit:round_response", + "algorithm": "ed25519", + "kid": "EOahLGtg57mQJhgTwQc-cTmZZPTVn_NGal6SH5taqJw", + "issuer": "ku:model:gemini-2.5-pro", + "issued_at": "2026-04-17T12:00:03Z", + "payload": { + "ku_id": "ku_4b3f7c2a9d8e1f05", + "sequence": 3, + "round": 1, + "model": { + "id": "gemini-2.5-pro", + "vendor": "google" + }, + "topic_digest": "a0431c892bf3d1d67c63726c9a7cf00c6270b39098c8e9cdcfdf26bb73825b24", + "response": { + "position": "depends_on_sequence_length", + "confidence": 0.8 + }, + "previousReceiptHash": "sha256:a54dde3a9dd2501476b0e9613990333371a29b2b669c9da02e348a86e6b2ff83" + }, + "signature": "fc702aab98cdf8f35c88b90da54776c037bcd53cee00183f4b994367c642558be7c6b1113ca370e24719f0f883a5772e358fb21ec58fa1b7d726e1c03d12930c" + }, + { + "v": 2, + "type": "veritasacta:knowledge_unit:round_response", + "algorithm": "ed25519", + "kid": "MdZxWfPWCPrjb4blQEWBKFyk9FkazXCgk2K7QFiuYRw", + "issuer": "ku:model:grok-4.20", + "issued_at": "2026-04-17T12:00:04Z", + "payload": { + "ku_id": "ku_4b3f7c2a9d8e1f05", + "sequence": 4, + "round": 1, + "model": { + "id": "grok-4.20", + "vendor": "xai" + }, + "topic_digest": "a0431c892bf3d1d67c63726c9a7cf00c6270b39098c8e9cdcfdf26bb73825b24", + "response": { + "position": "linear", + "confidence": 0.58 + }, + "previousReceiptHash": "sha256:76cce8f734ae646b56f4df333a15a400f254dcacece06e7b2bf26c97438b8bbc" + }, + "signature": "143109c124d8e8c857e1d0e5bc4da432be49a46fec0f77d34ee046e487efd4c2cca6e93697c56b8cec499a23e150dc7a4ea432328c7822092d15055ecf668e06" + }, + { + "v": 2, + "type": "veritasacta:knowledge_unit:round_response", + "algorithm": "ed25519", + "kid": "Z-LI5A7pV_q5L0741YBXwcOCo1U13ONImZ4OkQnKkg0", + "issuer": "ku:model:claude-opus-4.6", + "issued_at": "2026-04-17T12:00:05Z", + "payload": { + "ku_id": "ku_4b3f7c2a9d8e1f05", + "sequence": 5, + "round": 2, + "model": { + "id": "claude-opus-4.6", + "vendor": "anthropic" + }, + "topic_digest": "a0431c892bf3d1d67c63726c9a7cf00c6270b39098c8e9cdcfdf26bb73825b24", + "response": { + "position": "quadratic", + "confidence": 0.74, + "dissent_from": [ + "grok-4.20" + ] + }, + "previousReceiptHash": "sha256:653a70f396a16d2114b42996305f514b35d5ba59e6b37527b6311aa4d9e79aeb" + }, + "signature": "cdccd1f18914a542625b2f9916729b7584e590c3f224cb8901de3547565f94696893da3db5ce10057334206b8865aef917dbe4e050633761d0c99cc5526dde03" + }, + { + "v": 2, + "type": "veritasacta:knowledge_unit:round_response", + "algorithm": "ed25519", + "kid": "yH110mxzFmSFg-rqk93G6orppuIMvxJaBBCbtB2BF1Y", + "issuer": "ku:model:gpt-5", + "issued_at": "2026-04-17T12:00:06Z", + "payload": { + "ku_id": "ku_4b3f7c2a9d8e1f05", + "sequence": 6, + "round": 2, + "model": { + "id": "gpt-5", + "vendor": "openai" + }, + "topic_digest": "a0431c892bf3d1d67c63726c9a7cf00c6270b39098c8e9cdcfdf26bb73825b24", + "response": { + "position": "depends_on_sequence_length", + "confidence": 0.7, + "updated_from_round1": true + }, + "previousReceiptHash": "sha256:69eaa88bb7b4d0c66e9649096dae4aad20b84f7d9862b404f46a37e0a96e8240" + }, + "signature": "5db1986b447ae954ee425449c238550244821fb7fd9b928c477375b23b38126cc9aa4f394b727e97ada3ed1646dc1ca0b7dc6ee2f5f390d5c7e8a4dc95ae9c04" + }, + { + "v": 2, + "type": "veritasacta:knowledge_unit:round_response", + "algorithm": "ed25519", + "kid": "EOahLGtg57mQJhgTwQc-cTmZZPTVn_NGal6SH5taqJw", + "issuer": "ku:model:gemini-2.5-pro", + "issued_at": "2026-04-17T12:00:07Z", + "payload": { + "ku_id": "ku_4b3f7c2a9d8e1f05", + "sequence": 7, + "round": 2, + "model": { + "id": "gemini-2.5-pro", + "vendor": "google" + }, + "topic_digest": "a0431c892bf3d1d67c63726c9a7cf00c6270b39098c8e9cdcfdf26bb73825b24", + "response": { + "position": "depends_on_sequence_length", + "confidence": 0.85 + }, + "previousReceiptHash": "sha256:18ac3cef4d85205ebe2886b8b94b368ae8d28585088762e91df80ab3031a42d6" + }, + "signature": "f6483e7dc42969ac5536f6b65a9ff43305ba2f7404215368e13203f7d5500fb4e2b7537d56a63f1af3f459d0d50b124a43736e7013566548f70958ce4481540b" + }, + { + "v": 2, + "type": "veritasacta:knowledge_unit:round_response", + "algorithm": "ed25519", + "kid": "MdZxWfPWCPrjb4blQEWBKFyk9FkazXCgk2K7QFiuYRw", + "issuer": "ku:model:grok-4.20", + "issued_at": "2026-04-17T12:00:08Z", + "payload": { + "ku_id": "ku_4b3f7c2a9d8e1f05", + "sequence": 8, + "round": 2, + "model": { + "id": "grok-4.20", + "vendor": "xai" + }, + "topic_digest": "a0431c892bf3d1d67c63726c9a7cf00c6270b39098c8e9cdcfdf26bb73825b24", + "response": { + "position": "linear", + "confidence": 0.55, + "dissent_from": [ + "claude-opus-4.6", + "gpt-5", + "gemini-2.5-pro" + ] + }, + "previousReceiptHash": "sha256:b69cef75b1f0eeb36ece82755df449ccea4dde04915268c254fef33508ce016f" + }, + "signature": "1801e655c32aff17298399d36471c4049ea75631047858160c9f698cb2dc83981d94bdeda7f5d74d0fb54664cafce206ca25c2d73d51b6a25cb28610de4b840d" + }, + { + "v": 2, + "type": "veritasacta:knowledge_unit:synthesis", + "algorithm": "ed25519", + "kid": "YJMzwAk4gkCxTzWPmVAlcLtr517adDfl9gxGCLP4bDw", + "issuer": "ku:arbiter:scopeblind-reference", + "issued_at": "2026-04-17T12:00:09Z", + "payload": { + "ku_id": "ku_4b3f7c2a9d8e1f05", + "sequence": 9, + "round": 3, + "synthesis": { + "consensus": "depends_on_sequence_length", + "consensus_models": [ + "gpt-5", + "gemini-2.5-pro" + ], + "dissenting_positions": [ + { + "position": "quadratic", + "models": [ + "claude-opus-4.6" + ], + "mean_confidence": 0.74 + }, + { + "position": "linear", + "models": [ + "grok-4.20" + ], + "mean_confidence": 0.55 + } + ], + "synthesis_confidence": 0.68 + }, + "topic_digest": "a0431c892bf3d1d67c63726c9a7cf00c6270b39098c8e9cdcfdf26bb73825b24", + "previousReceiptHash": "sha256:b744f32a94d8f1f7727e9020308a8b1db87aef8c80b53c7644398ae0078317bc" + }, + "signature": "f6023315011bc437c0a1a6222c008affb7e14d264c4c89ad26efd12031461888858bb429eb54b2e26bbe093db53df83e80562d0199f8bdf4ed057178f947840d" + }, + { + "v": 2, + "type": "veritasacta:knowledge_unit:aggregate", + "algorithm": "ed25519", + "kid": "YJMzwAk4gkCxTzWPmVAlcLtr517adDfl9gxGCLP4bDw", + "issuer": "ku:arbiter:scopeblind-reference", + "issued_at": "2026-04-17T12:00:10Z", + "payload": { + "ku_id": "ku_4b3f7c2a9d8e1f05", + "sequence": 10, + "aggregate": { + "binds_receipts": [ + "sha256:2b06eb3385bcfa7bf29d59ae693c28124062fdb9b548a9710c1ee44f2eeae192", + "sha256:a54dde3a9dd2501476b0e9613990333371a29b2b669c9da02e348a86e6b2ff83", + "sha256:76cce8f734ae646b56f4df333a15a400f254dcacece06e7b2bf26c97438b8bbc", + "sha256:653a70f396a16d2114b42996305f514b35d5ba59e6b37527b6311aa4d9e79aeb", + "sha256:69eaa88bb7b4d0c66e9649096dae4aad20b84f7d9862b404f46a37e0a96e8240", + "sha256:18ac3cef4d85205ebe2886b8b94b368ae8d28585088762e91df80ab3031a42d6", + "sha256:b69cef75b1f0eeb36ece82755df449ccea4dde04915268c254fef33508ce016f", + "sha256:b744f32a94d8f1f7727e9020308a8b1db87aef8c80b53c7644398ae0078317bc", + "sha256:b0b54512737617227b1ac7569c06e71fbe8dd944011050c3ceaeac5e79526122" + ], + "round_count": 3, + "model_count": 4, + "deliberation_outcome": "synthesis_with_recorded_dissent" + }, + "previousReceiptHash": "sha256:b0b54512737617227b1ac7569c06e71fbe8dd944011050c3ceaeac5e79526122" + }, + "signature": "f160552fbdf28e326eacd639649fc8fe18af6dbe9a2d7a415fa424f4a638dedf77d7fa591a350fcf11ede65fbef07e3d7ea484b2ee2c7de47459097f75a03704" + } + ] +} diff --git a/test-vectors/generate.mjs b/test-vectors/generate.mjs new file mode 100644 index 0000000..afdeaa2 --- /dev/null +++ b/test-vectors/generate.mjs @@ -0,0 +1,456 @@ +/** + * Generator for VeritasActa/verify test-vectors. + * + * Produces three deterministic JSON files: + * 1. jcs-test-vectors.json - JCS canonicalization conformance + * 2. cross-verify-bundle.json - 10-receipt Knowledge Unit bundle + * 3. selective-disclosure-salted-commit.json - AIP-0002 reference + * + * Determinism: + * Ed25519 keys derive from SHA-256(SEED || role). + * Salts and timestamps are fixed constants. + * Running this script should always produce byte-identical output. + */ + +import { + canonicalize, + canonicalHash, + createSignedArtifact, + getPublicKey, + publicKeyToJWK, + computeKid, + bytesToHex, + hexToBytes, + sha256, +} from '@veritasacta/artifacts'; +import { writeFileSync } from 'node:fs'; +import { createHash } from 'node:crypto'; + +// ─── Deterministic seed ────────────────────────────────────────── +const SEED = 'veritasacta:verify:test-vectors:2026-04-17'; + +function deriveKey(role) { + const seed = sha256(new TextEncoder().encode(`${SEED}:${role}`)); + return bytesToHex(seed); +} + +function sha256hex(s) { + return createHash('sha256').update(s).digest('hex'); +} + +// ═══════════════════════════════════════════════════════════════════ +// 1. jcs-test-vectors.json +// ═══════════════════════════════════════════════════════════════════ + +const JCS_CASES = [ + { + name: 'empty_object', + description: 'Empty object serializes as {} with no whitespace.', + input: {}, + }, + { + name: 'single_key_value', + description: 'Single key-value pair, no sorting needed.', + input: { message: 'hello' }, + }, + { + name: 'nested_key_sorting', + description: 'Nested objects are sorted recursively by key name.', + input: { + b: { z: 1, a: 2 }, + a: 1, + }, + }, + { + name: 'array_order_preserved', + description: 'Array element order is preserved (not sorted).', + input: { list: [3, 1, 2, 'c', 'a', 'b'] }, + }, + { + name: 'number_serialization', + description: 'Numbers serialize with JSON.stringify defaults. No trailing zeros, no +e notation for integers in this range.', + input: { int: 42, neg: -17, zero: 0, frac: 0.5 }, + }, + { + name: 'mixed_primitives', + description: 'null, true, false, string, empty array, empty object all serialize correctly.', + input: { + nul: null, + t: true, + f: false, + s: 'hi', + empty_arr: [], + empty_obj: {}, + }, + }, + { + name: 'unicode_in_values_ok', + description: 'Non-ASCII characters in values are permitted. Only keys are restricted to ASCII (AIP-0001 §JCS Canonicalization).', + input: { greeting: 'héllo wörld', emoji: '✓' }, + }, + { + name: 'receipt_shaped_payload', + description: 'Realistic receipt-shaped payload: decision, policy digest, scope, nested metadata. The shape a real KU receipt payload takes.', + input: { + type: 'veritasacta:knowledge_unit:receipt', + ku_id: 'ku_4b3f7c2a', + sequence: 1, + model: { name: 'claude-opus-4.6', vendor: 'anthropic' }, + response_digest: 'sha256:f7e2d4c1b8a9', + policy_digest: 'sha256:abcdef0123456789', + decision: 'allow', + }, + }, + { + name: 'ascii_only_key_rejected', + description: 'Non-ASCII characters in keys are rejected at ingest per AIP-0001 §JCS Canonicalization. This sidesteps the Unicode normalization surface (NFC vs NFD, combining marks, bidi). Matches APS v1.41.0 canonicalizer behavior.', + input: { 'héllo': 'world' }, + expected_error: 'non-ASCII key', + }, +]; + +function buildJcsVectors() { + const vectors = []; + for (const c of JCS_CASES) { + if (c.expected_error) { + let error = null; + try { + canonicalize(c.input); + } catch (e) { + error = e.message; + } + vectors.push({ + name: c.name, + description: c.description, + input: c.input, + expected: { error: error || 'ERROR_NOT_THROWN' }, + }); + } else { + const canonical = canonicalize(c.input); + vectors.push({ + name: c.name, + description: c.description, + input: c.input, + canonical, + sha256: sha256hex(canonical), + }); + } + } + return { + format: 'veritasacta:jcs-test-vectors:v1', + spec: 'AIP-0001 §JCS Canonicalization', + normative_reference: 'RFC 8785 (with ASCII-only key restriction)', + generated_at: '2026-04-17T00:00:00Z', + generator: '@veritasacta/artifacts canonicalize() v0.2.2', + how_to_use: [ + 'For each vector, feed `input` to your JCS canonicalizer.', + 'Compare your output string byte-for-byte to `canonical`.', + 'Compute SHA-256 of your output and compare to `sha256` (lowercase hex).', + 'For `ascii_only_key_rejected`, your canonicalizer MUST throw when a key contains non-ASCII.', + ], + vectors, + }; +} + +// ═══════════════════════════════════════════════════════════════════ +// 2. cross-verify-bundle.json +// ═══════════════════════════════════════════════════════════════════ + +const MODELS = [ + { id: 'claude-opus-4.6', vendor: 'anthropic', role: 'model-claude' }, + { id: 'gpt-5', vendor: 'openai', role: 'model-gpt' }, + { id: 'gemini-2.5-pro', vendor: 'google', role: 'model-gemini' }, + { id: 'grok-4.20', vendor: 'xai', role: 'model-grok' }, +]; + +// Fixed deliberation content (stand-in for real model outputs). +// Each response is summarized as a digest; full content is out-of-band. +const ROUND_CONTENT = { + topic: 'Given fixed FLOPs budget, does quadratic attention or linear attention yield better sample efficiency at 70B scale?', + round1: { + 'claude-opus-4.6': { position: 'quadratic', confidence: 0.72 }, + 'gpt-5': { position: 'quadratic', confidence: 0.65 }, + 'gemini-2.5-pro': { position: 'depends_on_sequence_length', confidence: 0.80 }, + 'grok-4.20': { position: 'linear', confidence: 0.58 }, + }, + round2: { + 'claude-opus-4.6': { position: 'quadratic', confidence: 0.74, dissent_from: ['grok-4.20'] }, + 'gpt-5': { position: 'depends_on_sequence_length', confidence: 0.70, updated_from_round1: true }, + 'gemini-2.5-pro': { position: 'depends_on_sequence_length', confidence: 0.85 }, + 'grok-4.20': { position: 'linear', confidence: 0.55, dissent_from: ['claude-opus-4.6', 'gpt-5', 'gemini-2.5-pro'] }, + }, +}; + +const KU_ID = 'ku_4b3f7c2a9d8e1f05'; +const BUNDLE_ISSUED = '2026-04-17T12:00:00Z'; + +function buildReceipt({ type, payload, signerKey, kid, issuer, issuedAt }) { + const { artifact } = createSignedArtifact(type, payload, signerKey, { + kid, + issuer, + issued_at: issuedAt, + }); + return artifact; +} + +function buildBundle() { + // Keys: one per model + one arbiter for synthesis/aggregate + const keys = {}; + for (const m of MODELS) { + const priv = deriveKey(m.role); + const pub = getPublicKey(priv); + keys[m.id] = { priv, pub, kid: computeKid(pub), issuer: `ku:model:${m.id}` }; + } + const arbiterPriv = deriveKey('arbiter'); + const arbiterPub = getPublicKey(arbiterPriv); + keys['__arbiter__'] = { + priv: arbiterPriv, + pub: arbiterPub, + kid: computeKid(arbiterPub), + issuer: 'ku:arbiter:scopeblind-reference', + }; + + const receipts = []; + let sequence = 0; + let prevHash = null; + + function nextReceipt(modelKey, payload, type) { + sequence += 1; + const k = keys[modelKey]; + const fullPayload = { + ku_id: KU_ID, + sequence, + ...payload, + ...(prevHash ? { previousReceiptHash: `sha256:${prevHash}` } : {}), + }; + const issuedAt = `2026-04-17T12:00:${String(sequence).padStart(2, '0')}Z`; + const artifact = buildReceipt({ + type, + payload: fullPayload, + signerKey: k.priv, + kid: k.kid, + issuer: k.issuer, + issuedAt, + }); + prevHash = canonicalHash(artifact); + receipts.push(artifact); + return artifact; + } + + // Round 1: four independent responses + for (const m of MODELS) { + nextReceipt( + m.id, + { + round: 1, + model: { id: m.id, vendor: m.vendor }, + topic_digest: sha256hex(ROUND_CONTENT.topic), + response: ROUND_CONTENT.round1[m.id], + }, + 'veritasacta:knowledge_unit:round_response', + ); + } + + // Round 2: four cross-critiques + for (const m of MODELS) { + nextReceipt( + m.id, + { + round: 2, + model: { id: m.id, vendor: m.vendor }, + topic_digest: sha256hex(ROUND_CONTENT.topic), + response: ROUND_CONTENT.round2[m.id], + }, + 'veritasacta:knowledge_unit:round_response', + ); + } + + // Round 3: synthesis (arbiter) + nextReceipt( + '__arbiter__', + { + round: 3, + synthesis: { + consensus: 'depends_on_sequence_length', + consensus_models: ['gpt-5', 'gemini-2.5-pro'], + dissenting_positions: [ + { position: 'quadratic', models: ['claude-opus-4.6'], mean_confidence: 0.74 }, + { position: 'linear', models: ['grok-4.20'], mean_confidence: 0.55 }, + ], + synthesis_confidence: 0.68, + }, + topic_digest: sha256hex(ROUND_CONTENT.topic), + }, + 'veritasacta:knowledge_unit:synthesis', + ); + + // Aggregate: binds all 9 prior receipts by hash + const priorHashes = receipts.map((r) => `sha256:${canonicalHash(r)}`); + nextReceipt( + '__arbiter__', + { + aggregate: { + binds_receipts: priorHashes, + round_count: 3, + model_count: MODELS.length, + deliberation_outcome: 'synthesis_with_recorded_dissent', + }, + }, + 'veritasacta:knowledge_unit:aggregate', + ); + + // Verification keys (JWK set) + const signingKeys = []; + for (const m of MODELS) { + const k = keys[m.id]; + signingKeys.push({ ...publicKeyToJWK(k.pub, k.kid), issuer: k.issuer }); + } + signingKeys.push({ + ...publicKeyToJWK(keys.__arbiter__.pub, keys.__arbiter__.kid), + issuer: keys.__arbiter__.issuer, + }); + + return { + format: 'veritasacta:knowledge-unit-bundle:v1', + spec: 'draft-farley-acta-knowledge-units-00', + ku_id: KU_ID, + generated_at: BUNDLE_ISSUED, + description: + 'Complete Knowledge Unit deliberation: 4 models × 2 rounds + 1 synthesis + 1 aggregate = 10 Ed25519-signed receipts. Hash-chained via payload.previousReceiptHash over JCS-canonical bytes. Every receipt is individually verifiable with @veritasacta/verify; the bundle is verifiable with --bundle.', + verification: { + signing_keys: signingKeys, + }, + external_receipts: { + aps: { + description: + 'Drop-in slot for an APS DecisionLineageReceipt that references ku_id=' + + KU_ID + + '. When populated, both @veritasacta/verify and agent-passport-system verifiers should exit 0 over the same bundle.', + ku_id: KU_ID, + expected_fields: ['subject', 'scope', 'ku_id', 'receipt_hash', 'signature'], + receipt_uri: null, + receipt: null, + }, + }, + receipts, + }; +} + +// ═══════════════════════════════════════════════════════════════════ +// 3. selective-disclosure-salted-commit.json +// ═══════════════════════════════════════════════════════════════════ + +function buildSelectiveDisclosure() { + // Take grok-4.20's Round 1 dissenting response. + // Redact the response.position and response.confidence under AIP-0002 salted commits. + const priv = deriveKey('model-grok'); + const pub = getPublicKey(priv); + const kid = computeKid(pub); + const issuer = 'ku:model:grok-4.20'; + + // Fixed salts for determinism. + const saltPosition = 'sdc_salt_8f3b2a1c9d6e4702'; + const saltConfidence = 'sdc_salt_11ea5f0c7b4dc893'; + + const positionPlain = 'linear'; + const confidencePlain = 0.58; + + // Commitment algorithm: sha256(salt + ":" + JSON.stringify(plaintext)). + // JSON.stringify gives a canonical encoding for both string and number values: + // "linear" → "\"linear\"" 0.58 → "0.58" + const commitPosition = sha256hex(`${saltPosition}:${JSON.stringify(positionPlain)}`); + const commitConfidence = sha256hex(`${saltConfidence}:${JSON.stringify(confidencePlain)}`); + + // Redacted receipt: published publicly (consensus-visible, dissent-hidden). + const redactedPayload = { + ku_id: KU_ID, + sequence: 4, + round: 1, + model: { id: 'grok-4.20', vendor: 'xai' }, + topic_digest: sha256hex(ROUND_CONTENT.topic), + response: { + _redacted: { + scheme: 'veritasacta:aip-0002:salted-sha256-commit', + fields: { + position: { + commitment: `sha256:${commitPosition}`, + commit_algorithm: 'sha256(salt || ":" || json(value))', + }, + confidence: { + commitment: `sha256:${commitConfidence}`, + commit_algorithm: 'sha256(salt || ":" || json(value))', + }, + }, + }, + }, + }; + + const redactedArtifact = buildReceipt({ + type: 'veritasacta:knowledge_unit:round_response', + payload: redactedPayload, + signerKey: priv, + kid, + issuer, + issuedAt: '2026-04-17T12:00:04Z', + }); + + // Disclosure witness: salts + plaintext values that unlock the commitments. + // Shipped separately (e.g. to an auditor with need-to-know). + const witness = { + ku_id: KU_ID, + sequence: 4, + disclosures: { + 'response.position': { + salt: saltPosition, + plaintext: positionPlain, + expected_commitment: `sha256:${commitPosition}`, + verify: 'sha256(salt || ":" || json(plaintext)) === expected_commitment', + }, + 'response.confidence': { + salt: saltConfidence, + plaintext: confidencePlain, + expected_commitment: `sha256:${commitConfidence}`, + verify: 'sha256(salt || ":" || json(plaintext)) === expected_commitment', + }, + }, + }; + + return { + format: 'veritasacta:selective-disclosure-salted-commit:v1', + spec: 'AIP-0002 §Salted SHA-256 Commitments', + generated_at: BUNDLE_ISSUED, + description: + 'Demonstrates AIP-0002 redaction: a dissenting round-1 response is published with position and confidence fields replaced by salted SHA-256 commitments. The unsigned `witness` block carries the salts + plaintext values that reveal the commitments to a need-to-know recipient. Complementary to Merkle-tree selective disclosure (APS-style): simpler and zero-dependency, but subset boundary must be fixed at commit time.', + how_to_use: [ + 'Verify `redacted_receipt` with @veritasacta/verify — the Ed25519 signature is over the redacted payload, so verification succeeds without access to the witness.', + 'For each entry in `witness.disclosures`, compute sha256(salt + ":" + JSON.stringify(plaintext)) and compare to `expected_commitment`. If bytes match, the disclosure is authentic.', + 'The receipt is publishable; the witness stays with parties authorized to see the dissenting response.', + ], + verification: { + signing_keys: [{ ...publicKeyToJWK(pub, kid), issuer }], + }, + redacted_receipt: redactedArtifact, + witness, + }; +} + +// ═══════════════════════════════════════════════════════════════════ +// Emit files +// ═══════════════════════════════════════════════════════════════════ + +const jcsVecs = buildJcsVectors(); +const bundle = buildBundle(); +const sdc = buildSelectiveDisclosure(); + +const outDir = process.argv[2] || '.'; +writeFileSync(`${outDir}/jcs-test-vectors.json`, JSON.stringify(jcsVecs, null, 2) + '\n'); +writeFileSync(`${outDir}/cross-verify-bundle.json`, JSON.stringify(bundle, null, 2) + '\n'); +writeFileSync( + `${outDir}/selective-disclosure-salted-commit.json`, + JSON.stringify(sdc, null, 2) + '\n', +); + +console.log('Wrote:'); +console.log(` ${outDir}/jcs-test-vectors.json (${jcsVecs.vectors.length} cases)`); +console.log(` ${outDir}/cross-verify-bundle.json (${bundle.receipts.length} receipts, ${bundle.verification.signing_keys.length} keys)`); +console.log(` ${outDir}/selective-disclosure-salted-commit.json (${Object.keys(sdc.witness.disclosures).length} redacted fields)`); diff --git a/test-vectors/jcs-test-vectors.json b/test-vectors/jcs-test-vectors.json new file mode 100644 index 0000000..85065f3 --- /dev/null +++ b/test-vectors/jcs-test-vectors.json @@ -0,0 +1,124 @@ +{ + "format": "veritasacta:jcs-test-vectors:v1", + "spec": "AIP-0001 §JCS Canonicalization", + "normative_reference": "RFC 8785 (with ASCII-only key restriction)", + "generated_at": "2026-04-17T00:00:00Z", + "generator": "@veritasacta/artifacts canonicalize() v0.2.2", + "how_to_use": [ + "For each vector, feed `input` to your JCS canonicalizer.", + "Compare your output string byte-for-byte to `canonical`.", + "Compute SHA-256 of your output and compare to `sha256` (lowercase hex).", + "For `ascii_only_key_rejected`, your canonicalizer MUST throw when a key contains non-ASCII." + ], + "vectors": [ + { + "name": "empty_object", + "description": "Empty object serializes as {} with no whitespace.", + "input": {}, + "canonical": "{}", + "sha256": "44136fa355b3678a1146ad16f7e8649e94fb4fc21fe77e8310c060f61caaff8a" + }, + { + "name": "single_key_value", + "description": "Single key-value pair, no sorting needed.", + "input": { + "message": "hello" + }, + "canonical": "{\"message\":\"hello\"}", + "sha256": "9b2d43affbf49a367028df2e1414f84c0e099ac98c3d54a8a80157fd7771af25" + }, + { + "name": "nested_key_sorting", + "description": "Nested objects are sorted recursively by key name.", + "input": { + "b": { + "z": 1, + "a": 2 + }, + "a": 1 + }, + "canonical": "{\"a\":1,\"b\":{\"a\":2,\"z\":1}}", + "sha256": "8ac1db126bc92aaa214c532c8f8a53af00864832ac425cb098da92b51d3d2d2c" + }, + { + "name": "array_order_preserved", + "description": "Array element order is preserved (not sorted).", + "input": { + "list": [ + 3, + 1, + 2, + "c", + "a", + "b" + ] + }, + "canonical": "{\"list\":[3,1,2,\"c\",\"a\",\"b\"]}", + "sha256": "20d99e7ed3450679efc1818b4b50e843599b1c04fce06f680da13b9be53a29a4" + }, + { + "name": "number_serialization", + "description": "Numbers serialize with JSON.stringify defaults. No trailing zeros, no +e notation for integers in this range.", + "input": { + "int": 42, + "neg": -17, + "zero": 0, + "frac": 0.5 + }, + "canonical": "{\"frac\":0.5,\"int\":42,\"neg\":-17,\"zero\":0}", + "sha256": "10657fc7d412f3ec88b9cb7fe576f65ee3979fa0fc2b866e3821b082e4d25100" + }, + { + "name": "mixed_primitives", + "description": "null, true, false, string, empty array, empty object all serialize correctly.", + "input": { + "nul": null, + "t": true, + "f": false, + "s": "hi", + "empty_arr": [], + "empty_obj": {} + }, + "canonical": "{\"empty_arr\":[],\"empty_obj\":{},\"f\":false,\"nul\":null,\"s\":\"hi\",\"t\":true}", + "sha256": "35b357d82e7c449132b6903fca471282b3a14b35e7c5ee44b7299fd688ebfdb2" + }, + { + "name": "unicode_in_values_ok", + "description": "Non-ASCII characters in values are permitted. Only keys are restricted to ASCII (AIP-0001 §JCS Canonicalization).", + "input": { + "greeting": "héllo wörld", + "emoji": "✓" + }, + "canonical": "{\"emoji\":\"✓\",\"greeting\":\"héllo wörld\"}", + "sha256": "1c36d840ffd41d2ee929ba53205490755018d76cc41f4172acb078d428ed3b4a" + }, + { + "name": "receipt_shaped_payload", + "description": "Realistic receipt-shaped payload: decision, policy digest, scope, nested metadata. The shape a real KU receipt payload takes.", + "input": { + "type": "veritasacta:knowledge_unit:receipt", + "ku_id": "ku_4b3f7c2a", + "sequence": 1, + "model": { + "name": "claude-opus-4.6", + "vendor": "anthropic" + }, + "response_digest": "sha256:f7e2d4c1b8a9", + "policy_digest": "sha256:abcdef0123456789", + "decision": "allow" + }, + "canonical": "{\"decision\":\"allow\",\"ku_id\":\"ku_4b3f7c2a\",\"model\":{\"name\":\"claude-opus-4.6\",\"vendor\":\"anthropic\"},\"policy_digest\":\"sha256:abcdef0123456789\",\"response_digest\":\"sha256:f7e2d4c1b8a9\",\"sequence\":1,\"type\":\"veritasacta:knowledge_unit:receipt\"}", + "sha256": "1339c990ba18600a700f8116eb7d4b5982a260cf99661866996cc94d83d056a0" + }, + { + "name": "ascii_only_key_rejected", + "description": "Non-ASCII characters in keys are rejected at ingest per AIP-0001 §JCS Canonicalization. This sidesteps the Unicode normalization surface (NFC vs NFD, combining marks, bidi). Matches APS v1.41.0 canonicalizer behavior.", + "input": { + "héllo": "world" + }, + "expected": { + "error": "Non-ASCII key \"héllo\" in artifact payload. Only ASCII keys are permitted." + } + } + ] +} diff --git a/test-vectors/selective-disclosure-salted-commit.json b/test-vectors/selective-disclosure-salted-commit.json new file mode 100644 index 0000000..71620e3 --- /dev/null +++ b/test-vectors/selective-disclosure-salted-commit.json @@ -0,0 +1,75 @@ +{ + "format": "veritasacta:selective-disclosure-salted-commit:v1", + "spec": "AIP-0002 §Salted SHA-256 Commitments", + "generated_at": "2026-04-17T12:00:00Z", + "description": "Demonstrates AIP-0002 redaction: a dissenting round-1 response is published with position and confidence fields replaced by salted SHA-256 commitments. The unsigned `witness` block carries the salts + plaintext values that reveal the commitments to a need-to-know recipient. Complementary to Merkle-tree selective disclosure (APS-style): simpler and zero-dependency, but subset boundary must be fixed at commit time.", + "how_to_use": [ + "Verify `redacted_receipt` with @veritasacta/verify — the Ed25519 signature is over the redacted payload, so verification succeeds without access to the witness.", + "For each entry in `witness.disclosures`, compute sha256(salt + \":\" + JSON.stringify(plaintext)) and compare to `expected_commitment`. If bytes match, the disclosure is authentic.", + "The receipt is publishable; the witness stays with parties authorized to see the dissenting response." + ], + "verification": { + "signing_keys": [ + { + "kty": "OKP", + "crv": "Ed25519", + "kid": "MdZxWfPWCPrjb4blQEWBKFyk9FkazXCgk2K7QFiuYRw", + "x": "nQIE0pSO6yXnABIt3SdC01oyyLa-J1i9Wf5o1lDUtoc", + "use": "sig", + "issuer": "ku:model:grok-4.20" + } + ] + }, + "redacted_receipt": { + "v": 2, + "type": "veritasacta:knowledge_unit:round_response", + "algorithm": "ed25519", + "kid": "MdZxWfPWCPrjb4blQEWBKFyk9FkazXCgk2K7QFiuYRw", + "issuer": "ku:model:grok-4.20", + "issued_at": "2026-04-17T12:00:04Z", + "payload": { + "ku_id": "ku_4b3f7c2a9d8e1f05", + "sequence": 4, + "round": 1, + "model": { + "id": "grok-4.20", + "vendor": "xai" + }, + "topic_digest": "a0431c892bf3d1d67c63726c9a7cf00c6270b39098c8e9cdcfdf26bb73825b24", + "response": { + "_redacted": { + "scheme": "veritasacta:aip-0002:salted-sha256-commit", + "fields": { + "position": { + "commitment": "sha256:d2c291baf9d6a744e179fbf710bac30dd44dc5fef60e0934b5a16341d9b71149", + "commit_algorithm": "sha256(salt || \":\" || json(value))" + }, + "confidence": { + "commitment": "sha256:3d666f5630fa1c09843635e2487c77f8e02d175b817ebebdc146a6d048106a32", + "commit_algorithm": "sha256(salt || \":\" || json(value))" + } + } + } + } + }, + "signature": "21b69679da11974e95fc6e90cd07aeff5d439f8fd060ce0fecc4250858cff1dc953644b7cca66e6cbd772eba3f02bd1f6cbeddd21e14719377caed7bcddf0d0a" + }, + "witness": { + "ku_id": "ku_4b3f7c2a9d8e1f05", + "sequence": 4, + "disclosures": { + "response.position": { + "salt": "sdc_salt_8f3b2a1c9d6e4702", + "plaintext": "linear", + "expected_commitment": "sha256:d2c291baf9d6a744e179fbf710bac30dd44dc5fef60e0934b5a16341d9b71149", + "verify": "sha256(salt || \":\" || json(plaintext)) === expected_commitment" + }, + "response.confidence": { + "salt": "sdc_salt_11ea5f0c7b4dc893", + "plaintext": 0.58, + "expected_commitment": "sha256:3d666f5630fa1c09843635e2487c77f8e02d175b817ebebdc146a6d048106a32", + "verify": "sha256(salt || \":\" || json(plaintext)) === expected_commitment" + } + } + } +}