@veritasacta/verify
Advanced tools
+17
| { | ||
| "sigil_version": 1, | ||
| "fingerprint": "dd0443f0", | ||
| "name": "Slow Reed", | ||
| "sigil_hash": "dd0443f0ae189b11d739e5e944b68fc955b3d777514d652a1f0af0a4e0b8666b", | ||
| "project_public_key": "fe665e861867cec7e171c0c13bbc873c3362079faef21f54df7804b7fb9ae8af", | ||
| "policy": { | ||
| "version": 2, | ||
| "package": "@veritasacta/verify", | ||
| "package_version": "0.3.0", | ||
| "source_hash": "e7af6bb636d2336bbdc07d508d5011cc12a15ad5ec83171a6cbb8f32da78645e", | ||
| "ietf_draft": "draft-farley-acta-signed-receipts-02", | ||
| "created_at": 1776079766532 | ||
| }, | ||
| "policy_hash": "789aa634fa4496b0feed688d9d96731691dc75303c722868a5311fb704feafa5", | ||
| "derived_at": "2026-04-13T11:29:26.532Z" | ||
| } |
+264
-1
@@ -23,2 +23,3 @@ #!/usr/bin/env node | ||
| import { readFileSync } from 'node:fs'; | ||
| import { createHash } from 'node:crypto'; | ||
| import { dirname, join } from 'node:path'; | ||
@@ -43,3 +44,159 @@ import { fileURLToPath } from 'node:url'; | ||
| const bold = (s) => isCI ? s : `\x1b[1m${s}\x1b[0m`; | ||
| const teal = (s) => isCI ? s : `\x1b[36m${s}\x1b[0m`; | ||
| const peach = (s) => isCI ? s : `\x1b[38;5;216m${s}\x1b[0m`; | ||
| const bgTeal = (s) => isCI ? s : `\x1b[46m${s}\x1b[0m`; | ||
| const bgPeach = (s) => isCI ? s : `\x1b[48;5;216m${s}\x1b[0m`; | ||
| // ── Sigil: Terminal Renderer ──────────────────────────────────── | ||
| // Same algorithm as @scopeblind/passport sigil.ts — deterministic | ||
| // visual identity derived from Ed25519 public key via SHA-256. | ||
| // Rendered as 11×11 Unicode block art for terminal display. | ||
| const SIGIL_DOMAIN = 'scopeblind:sigil:v1'; | ||
| const SIGIL_SEGMENTS = 6; | ||
| function sigilHashFromKey(publicKeyHex) { | ||
| return createHash('sha256') | ||
| .update(SIGIL_DOMAIN) | ||
| .update(Buffer.from(publicKeyHex, 'hex')) | ||
| .digest(); | ||
| } | ||
| function deriveSigilGrid(hash) { | ||
| const extHash = createHash('sha256').update('ext:').update(hash).digest(); | ||
| const b = (i) => i < 32 ? hash[i] : extHash[i - 32]; | ||
| let idx = 0; | ||
| const diamond = { | ||
| top: b(idx++) % 3, right: b(idx++) % 3, | ||
| bottom: b(idx++) % 3, left: b(idx++) % 3, | ||
| }; | ||
| const surround = []; | ||
| for (let i = 0; i < 4; i++) surround.push(b(idx++) % 3); | ||
| const innerRing = []; | ||
| for (let i = 0; i < SIGIL_SEGMENTS; i++) innerRing.push(b(idx++) % 3); | ||
| const midRing = []; | ||
| for (let i = 0; i < SIGIL_SEGMENTS; i++) midRing.push(b(idx++) % 3); | ||
| const outerRing = []; | ||
| for (let i = 0; i < SIGIL_SEGMENTS; i++) outerRing.push(b(idx++) % 3); | ||
| const corners = []; | ||
| for (let i = 0; i < 8; i++) corners.push(b(idx++) % 3); | ||
| return { diamond, surround, innerRing, midRing, outerRing, corners }; | ||
| } | ||
| function sigilPassesFilter(g) { | ||
| const all = [ | ||
| g.diamond.top, g.diamond.right, g.diamond.bottom, g.diamond.left, | ||
| ...g.surround, ...g.innerRing, ...g.midRing, ...g.outerRing, ...g.corners, | ||
| ]; | ||
| let p = 0, s = 0; | ||
| for (const v of all) { if (v === 1) p++; if (v === 2) s++; } | ||
| const filled = p + s; | ||
| return p >= 4 && s >= 4 && filled >= 10 && filled <= 28; | ||
| } | ||
| function deriveFilteredSigilGrid(publicKeyHex) { | ||
| let nonce = 0; | ||
| const keyBuf = Buffer.from(publicKeyHex, 'hex'); | ||
| while (nonce < 256) { | ||
| const hash = nonce === 0 | ||
| ? sigilHashFromKey(publicKeyHex) | ||
| : createHash('sha256').update(SIGIL_DOMAIN).update(keyBuf).update(Buffer.from([nonce])).digest(); | ||
| const grid = deriveSigilGrid(hash); | ||
| if (sigilPassesFilter(grid)) { | ||
| return { grid, fingerprint: hash.toString('hex').slice(0, 8) }; | ||
| } | ||
| nonce++; | ||
| } | ||
| const hash = sigilHashFromKey(publicKeyHex); | ||
| return { grid: deriveSigilGrid(hash), fingerprint: hash.toString('hex').slice(0, 8) }; | ||
| } | ||
| /** | ||
| * Render sigil as 11×11 terminal block art. | ||
| * | ||
| * Grid layout (11×11): | ||
| * Cells [0-1, 9-10] rows/cols = corners (4 quadrants, 2 splits each) | ||
| * Cells [2, 8] ring = outer ring (6 segments) | ||
| * Cells [3, 7] ring = mid ring (6 segments) | ||
| * Cells [4, 6] ring = inner ring (6 segments) | ||
| * Cell 5 ring = surround + diamond center | ||
| * | ||
| * Each pixel maps to an angular sector and radial band. | ||
| * States: 0=background, 1=primary (teal ●), 2=secondary (peach ○) | ||
| */ | ||
| function renderTerminalSigil(publicKeyHex) { | ||
| const { grid, fingerprint } = deriveFilteredSigilGrid(publicKeyHex); | ||
| const SIZE = 11; | ||
| const cx = 5, cy = 5; | ||
| const R = 5.5; // outer radius (flush with 11×11) | ||
| // Radial bands (same proportions as SVG renderer) | ||
| const outerR = R; | ||
| const midR = R * 0.72; | ||
| const innerR = R * 0.44; | ||
| const surroundR = innerR * 0.65; | ||
| const diamondR = surroundR * 0.6; | ||
| const lines = []; | ||
| for (let y = 0; y < SIZE; y++) { | ||
| let row = ' '; | ||
| for (let x = 0; x < SIZE; x++) { | ||
| const dx = x - cx; | ||
| const dy = y - cy; | ||
| const dist = Math.sqrt(dx * dx + dy * dy); | ||
| const angle = Math.atan2(dy, dx); | ||
| // Normalize angle to [0, 2π) | ||
| const normAngle = angle < 0 ? angle + 2 * Math.PI : angle; | ||
| const segIdx = Math.floor((normAngle / (2 * Math.PI)) * SIGIL_SEGMENTS) % SIGIL_SEGMENTS; | ||
| let state = 0; // background | ||
| if (dist <= diamondR) { | ||
| // Diamond center — determine quadrant | ||
| // Top: angle ∈ [-π, -π/2], Right: [-π/2, 0], Bottom: [0, π/2], Left: [π/2, π] | ||
| if (angle >= -Math.PI && angle < -Math.PI / 2) state = grid.diamond.left; | ||
| else if (angle >= -Math.PI / 2 && angle < 0) state = grid.diamond.top; | ||
| else if (angle >= 0 && angle < Math.PI / 2) state = grid.diamond.right; | ||
| else state = grid.diamond.bottom; | ||
| } else if (dist <= surroundR) { | ||
| // Surround — 4 wedges | ||
| const sIdx = Math.floor(((normAngle + Math.PI / 4) % (2 * Math.PI)) / (Math.PI / 2)) % 4; | ||
| state = grid.surround[sIdx]; | ||
| } else if (dist <= innerR) { | ||
| state = grid.innerRing[segIdx]; | ||
| } else if (dist <= midR) { | ||
| state = grid.midRing[segIdx]; | ||
| } else if (dist <= outerR) { | ||
| state = grid.outerRing[segIdx]; | ||
| } else { | ||
| // Corner region — outside the circle, inside the square | ||
| const cIdx = (y < cy ? 0 : 2) + (x < cx ? 0 : 1); // TL=0, TR=1, BL=2, BR=3 | ||
| // Split: which half of the corner? | ||
| const cornerAngle = Math.atan2(y - cy, x - cx); | ||
| const cornerMidAngles = [ | ||
| -3 * Math.PI / 4, // TL | ||
| -Math.PI / 4, // TR | ||
| 3 * Math.PI / 4, // BL | ||
| Math.PI / 4, // BR | ||
| ]; | ||
| const half = ((cIdx === 0 || cIdx === 1) | ||
| ? (cornerAngle < cornerMidAngles[cIdx] ? 0 : 1) | ||
| : (cornerAngle > cornerMidAngles[cIdx] ? 0 : 1)); | ||
| state = grid.corners[cIdx * 2 + half]; | ||
| } | ||
| if (state === 1) row += teal('█'); | ||
| else if (state === 2) row += peach('▓'); | ||
| else row += dim('·'); | ||
| } | ||
| lines.push(row); | ||
| } | ||
| lines.push(` ${dim('sigil:')} ${teal(fingerprint)}`); | ||
| return lines.join('\n'); | ||
| } | ||
| // ── CLI Argument Parsing ──────────────────────────────────────── | ||
@@ -59,2 +216,3 @@ | ||
| selfTest: false, | ||
| selfCheck: false, | ||
| }; | ||
@@ -80,2 +238,4 @@ | ||
| opts.selfTest = true; | ||
| } else if (arg === '--self-check') { | ||
| opts.selfCheck = true; | ||
| } else if (!arg.startsWith('-')) { | ||
@@ -108,3 +268,4 @@ opts.file = arg; | ||
| --verbose, -v Show detailed verification info | ||
| --self-test Verify bundled sample artifacts (proves the verifier works) | ||
| --self-test Verify bundled sample artifacts (proves the verifier works) | ||
| --self-check Verify this verifier is the canonical, unmodified release | ||
| --help, -h Show this help | ||
@@ -327,2 +488,7 @@ | ||
| if (opts.json) { | ||
| // Include sigil fingerprint in JSON output | ||
| if (result.valid && result.publicKey && result.publicKey.length === 64) { | ||
| const { fingerprint } = deriveFilteredSigilGrid(result.publicKey); | ||
| result.sigil_fingerprint = fingerprint; | ||
| } | ||
| console.log(JSON.stringify(result, null, 2)); | ||
@@ -335,2 +501,8 @@ return; | ||
| // Show sigil for valid verifications | ||
| if (result.valid && result.publicKey && result.publicKey.length === 64 && !isCI) { | ||
| console.log(''); | ||
| console.log(renderTerminalSigil(result.publicKey)); | ||
| } | ||
| console.log(`\n${icon} Signature: ${status}`); | ||
@@ -398,2 +570,8 @@ console.log(` Format: ${result.format || 'unknown'}`); | ||
| // Show sigil of the test key — visual proof of which key is being tested | ||
| if (!isCI) { | ||
| console.log(renderTerminalSigil(testKey)); | ||
| console.log(''); | ||
| } | ||
| // Test sample receipt | ||
@@ -440,2 +618,87 @@ try { | ||
| // ── Self-check: verify the verifier itself ────────────────────── | ||
| // Computes SHA-256 of the installed cli.js, compares to the Sigil | ||
| // commitment that was generated at release time. If they match, | ||
| // this is the canonical, unmodified verifier. If not, the code | ||
| // has been modified since release. | ||
| if (opts.selfCheck) { | ||
| const __dirname = dirname(fileURLToPath(import.meta.url)); | ||
| const sigilPath = join(__dirname, 'sigil.json'); | ||
| console.log(`\n${bold('@veritasacta/verify — self-check')}\n`); | ||
| // Load the Sigil commitment | ||
| let sigil; | ||
| try { | ||
| sigil = JSON.parse(readFileSync(sigilPath, 'utf-8')); | ||
| } catch { | ||
| console.log(` ${red('✗')} No sigil.json found — this verifier has no Sigil commitment.`); | ||
| console.log(` This may be a development build or a fork.`); | ||
| console.log(''); | ||
| process.exit(2); | ||
| } | ||
| // Compute the hash of the installed cli.js | ||
| const cliPath = join(__dirname, 'cli.js'); | ||
| const cliContent = readFileSync(cliPath); | ||
| const installedHash = createHash('sha256').update(cliContent).digest('hex'); | ||
| // Compare to the committed hash | ||
| const committedHash = sigil.policy?.source_hash; | ||
| const hashMatch = installedHash === committedHash; | ||
| // Re-derive the Sigil from the project key + policy | ||
| const policyJson = JSON.stringify(sigil.policy); | ||
| const policyHash = createHash('sha256').update(policyJson).digest('hex'); | ||
| const policyMatch = policyHash === sigil.policy_hash; | ||
| // Re-derive the Sigil hash | ||
| const domain = Buffer.from('scopeblind:sigil:v2'); | ||
| const pubKey = Buffer.from(sigil.project_public_key, 'hex'); | ||
| const policyBuf = Buffer.from(policyHash, 'hex'); | ||
| const input = Buffer.concat([domain, pubKey, policyBuf, Buffer.from([0])]); | ||
| const rederived = createHash('sha256').update(input).digest('hex'); | ||
| const sigilMatch = rederived === sigil.sigil_hash; | ||
| const allGood = hashMatch && policyMatch && sigilMatch; | ||
| if (allGood) { | ||
| // Show the Sigil visual of the project key | ||
| if (!isCI) { | ||
| console.log(renderTerminalSigil(sigil.project_public_key)); | ||
| console.log(''); | ||
| } | ||
| console.log(` ${green('✓')} Canonical verifier — ${green(sigil.name)}`); | ||
| console.log(` Sigil: ${teal(sigil.fingerprint)}`); | ||
| console.log(` Version: ${sigil.policy.package_version}`); | ||
| console.log(` Package: ${sigil.policy.package}`); | ||
| console.log(` Source: ${dim(installedHash.slice(0, 16) + '...')} ${green('matches commitment')}`); | ||
| console.log(` Policy: ${dim(policyHash.slice(0, 16) + '...')} ${green('matches commitment')}`); | ||
| console.log(` Sigil: ${dim(rederived.slice(0, 16) + '...')} ${green('matches commitment')}`); | ||
| console.log(''); | ||
| console.log(` ${dim('This verifier is the unmodified canonical release.')}`); | ||
| console.log(` ${dim('The source code has not been changed since it was published.')}`); | ||
| } else { | ||
| console.log(` ${red('✗')} Modified verifier — NOT the canonical release\n`); | ||
| if (!hashMatch) { | ||
| console.log(` Source: ${red('MISMATCH')}`); | ||
| console.log(` Installed: ${installedHash.slice(0, 32)}...`); | ||
| console.log(` Expected: ${committedHash?.slice(0, 32) || 'n/a'}...`); | ||
| } | ||
| if (!policyMatch) { | ||
| console.log(` Policy: ${red('MISMATCH')} — sigil.json may have been tampered with`); | ||
| } | ||
| if (!sigilMatch) { | ||
| console.log(` Sigil: ${red('MISMATCH')} — the commitment chain is broken`); | ||
| } | ||
| console.log(''); | ||
| console.log(` ${yellow('This verifier has been modified since the canonical release.')}`); | ||
| console.log(` ${yellow('It may be a fork, a development build, or a tampered copy.')}`); | ||
| console.log(` ${dim('Get the canonical verifier: npm install @veritasacta/verify')}`); | ||
| } | ||
| console.log(''); | ||
| process.exit(allGood ? 0 : 1); | ||
| } | ||
| // Read input | ||
@@ -442,0 +705,0 @@ let input; |
+5
-2
| { | ||
| "name": "@veritasacta/verify", | ||
| "version": "0.2.5", | ||
| "version": "0.3.0", | ||
| "mcpName": "io.github.tomjwxf/veritasacta-verify", | ||
@@ -13,2 +13,3 @@ "description": "CLI tool to verify signed artifacts (receipts, manifests, tickets). Works offline.", | ||
| "cli.js", | ||
| "sigil.json", | ||
| "README.md", | ||
@@ -39,3 +40,5 @@ "samples/", | ||
| "test": "node test/conformance.js", | ||
| "self-test": "node cli.js samples/sample-receipt.json --key d75a980182b10ab7d54bfed3c964073a0ee172f3daa62325af021a68f707511a && node cli.js samples/sample-bundle.json --bundle" | ||
| "self-test": "node cli.js samples/sample-receipt.json --key d75a980182b10ab7d54bfed3c964073a0ee172f3daa62325af021a68f707511a && node cli.js samples/sample-bundle.json --bundle", | ||
| "self-check": "node cli.js --self-check", | ||
| "generate-sigil": "node generate-sigil.mjs" | ||
| }, | ||
@@ -42,0 +45,0 @@ "repository": { |
+72
-34
| # @veritasacta/verify | ||
| **Verify signed artifacts offline.** No accounts. No API calls. No ScopeBlind dependency. | ||
| **Verify signed artifacts offline.** No accounts. No API calls. No trust required. | ||
| > Apache-2.0 licensed. Works offline. Requires zero trust in ScopeBlind or anyone else. | ||
| > Apache-2.0 licensed. Works offline. The verifier verifies itself. | ||
| ## Sigil: Slow Reed | ||
| ``` | ||
| Sigil: Slow Reed | ||
| Fingerprint: dd0443f0 | ||
| Version: 0.3.0 | ||
| ``` | ||
| This release carries a cryptographic Sigil — a commitment to the exact source code published in this package. Run `--self-check` to verify you have the canonical, unmodified verifier: | ||
| ```bash | ||
| npx @veritasacta/verify --self-check | ||
| ``` | ||
| ``` | ||
| @veritasacta/verify — self-check | ||
| ✓ Canonical verifier — Slow Reed | ||
| Sigil: dd0443f0 | ||
| Version: 0.3.0 | ||
| Package: @veritasacta/verify | ||
| Source: e7af6bb636d2336b... matches commitment | ||
| Policy: 789aa634fa4496b0... matches commitment | ||
| Sigil: dd0443f0ae189b11... matches commitment | ||
| This verifier is the unmodified canonical release. | ||
| The source code has not been changed since it was published. | ||
| ``` | ||
| If you see "Modified verifier — NOT the canonical release," the code has been changed. Get the canonical version: `npm install @veritasacta/verify` | ||
| **Why this matters:** any fork of this verifier can rename itself, but it cannot produce a matching Sigil without the project's private key. The `--self-check` flag lets anyone confirm they are running the real thing. | ||
| ## Prove It | ||
@@ -23,4 +56,2 @@ | ||
| That's it. The verifier works. Now verify your own receipts: | ||
| ## Usage | ||
@@ -30,3 +61,3 @@ | ||
| # Verify a receipt with a known public key | ||
| npx @veritasacta/verify receipt.json --key d75a980182b10ab7d54bfed3c964073a0ee172f3daa62325af021a68f707511a | ||
| npx @veritasacta/verify receipt.json --key <public-key-hex> | ||
@@ -44,2 +75,5 @@ # Verify a receipt, fetching key from JWKS | ||
| npx @veritasacta/verify receipt.json --key <hex> --json | ||
| # Verify the verifier itself | ||
| npx @veritasacta/verify --self-check | ||
| ``` | ||
@@ -52,3 +86,3 @@ | ||
| 1. Strips the `signature` field from the artifact | ||
| 2. Canonicalizes the remaining JSON (sorted keys, deterministic — JCS-style) | ||
| 2. Canonicalizes the remaining JSON (JCS — RFC 8785) | ||
| 3. SHA-256 hashes the canonical bytes | ||
@@ -66,2 +100,3 @@ 4. Verifies the Ed25519 signature against the hash using the public key | ||
| | Passport SignedEnvelope | `{ payload, signature: { alg, kid, sig } }` | | ||
| | IETF draft envelope | `{ payload, signature: { alg, kid, sig } }` per draft-farley-acta-signed-receipts | | ||
| | Audit bundle | `{ receipts: [...], verification: { signing_keys: [...] } }` | | ||
@@ -79,3 +114,4 @@ | ||
| | `--verbose, -v` | Show detailed verification info | | ||
| | `--self-test` | Verify bundled sample artifacts | | ||
| | `--self-test` | Verify bundled sample artifacts (proves the verifier works) | | ||
| | `--self-check` | Verify this verifier is the canonical, unmodified release | | ||
| | `--help, -h` | Show help | | ||
@@ -87,47 +123,49 @@ | ||
| |------|---------|-----------| | ||
| | `0` | Signature valid | **Proven authentic** — the Ed25519 math confirms this artifact has not been modified since signing | | ||
| | `0` | Signature valid | **Proven authentic** — the Ed25519 math confirms the artifact has not been modified since signing | | ||
| | `1` | Signature invalid | **Proven tampered** — the signature does not match the payload. This is a security event. | | ||
| | `2` | Verifier error | **Undecidable** — malformed input, missing key, unparseable JSON. The check could not be performed. | | ||
| The distinction matters: exit 1 is evidence of tampering (the math was tested and failed). | ||
| Exit 2 is an operational error (the math was never tested). These demand different responses. | ||
| The distinction matters: exit 1 is evidence of tampering. Exit 2 is an operational error. These demand different responses. | ||
| ## Where Do Receipts Come From? | ||
| ## Cross-System Interop | ||
| Receipts are generated by [protect-mcp](https://www.npmjs.com/package/protect-mcp), which wraps MCP tool servers and signs every decision (allow, deny, rate-limit, require_approval) as a v2 artifact. | ||
| This verifier accepts receipts from multiple governance frameworks: | ||
| ```bash | ||
| # Generate receipts | ||
| npx protect-mcp --policy policy.json -- node your-mcp-server.js | ||
| | System | Receipt type | Verified | | ||
| |--------|-------------|----------| | ||
| | [protect-mcp](https://www.npmjs.com/package/protect-mcp) | Cedar policy + execution receipts | Exit 0 | | ||
| | [Agent Passport System](https://www.npmjs.com/package/agent-passport-system) | Delegation + evaluation + outcome receipts | Exit 0 | | ||
| | [AgentID](https://pypi.org/project/agentid/) | Identity verification attestations | Exit 0 | | ||
| # Later, verify them | ||
| npx @veritasacta/verify receipt.json --key <gateway-public-key> | ||
| ``` | ||
| All three use the same IETF draft envelope format with JCS canonicalization and Ed25519 signatures. | ||
| ## Conformance Testing | ||
| ## Sigil Commitment | ||
| ```bash | ||
| # Run the full conformance test suite (20 tests) | ||
| npm test | ||
| ``` | ||
| The `sigil.json` file in this package contains: | ||
| Tests verify: | ||
| - All known test vectors pass (4 vectors with hash match) | ||
| - Tampered artifacts are correctly rejected | ||
| - Wrong public keys are correctly rejected | ||
| - Sample receipt and bundle verify | ||
| - Format detection works | ||
| | Field | Value | Purpose | | ||
| |-------|-------|---------| | ||
| | `fingerprint` | `dd0443f0` | Short identifier for this release's Sigil | | ||
| | `name` | Slow Reed | Human-readable name (deterministic from fingerprint) | | ||
| | `project_public_key` | `fe665e86...` | Veritas Acta project Ed25519 public key | | ||
| | `policy.source_hash` | `e7af6bb6...` | SHA-256 of cli.js at release time | | ||
| | `policy.package_version` | `0.3.0` | npm version this Sigil commits to | | ||
| | `policy.ietf_draft` | `draft-farley-acta-signed-receipts-02` | IETF spec version implemented | | ||
| The Sigil is derived from: `SHA-256("scopeblind:sigil:v2" || project_public_key || SHA-256(policy_json))`. Anyone with the public key and the source code can independently re-derive the Sigil and confirm it matches. | ||
| ## Why Trust This? | ||
| - **Self-checking** — the verifier verifies itself (`--self-check`) | ||
| - **Apache-2.0 licensed** — patent grant included, no vendor lock-in | ||
| - **Open schemas** — the artifact format is public | ||
| - **Open schemas** — the artifact format is an IETF Internet-Draft | ||
| - **Test vectors** — deterministic keypairs with known-good signatures | ||
| - **Works offline** — no API calls, no accounts, no ScopeBlind servers | ||
| - **Verification is independent** — you don't need to trust ScopeBlind | ||
| - **Works offline** — no API calls, no accounts, no servers contacted | ||
| - **Cross-system** — verifies receipts from multiple independent governance frameworks | ||
| - **Sigil'd** — every release carries a cryptographic commitment to its own source code | ||
| > Any platform can log what its agents do. Very few will let you verify those logs without trusting them. | ||
| > Any platform can log what its agents do. Very few will let you verify those logs without trusting them. Even fewer will let you verify the verifier. | ||
| ## License | ||
| Apache-2.0 — [veritasacta.com](https://veritasacta.com) | [IETF Draft](https://datatracker.ietf.org/doc/draft-farley-acta-signed-receipts/) | ||
| Apache-2.0 — [veritasacta.com](https://veritasacta.com) | [IETF Draft](https://datatracker.ietf.org/doc/draft-farley-acta-signed-receipts/) | [Protocol](https://github.com/VeritasActa/Acta) |
46459
41.36%8
14.29%932
37.06%165
29.92%