@emilia-protocol/gate 0.11.0 → 0.14.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 +80 -0
- package/README.md +310 -0
- package/action-control-manifest.js +3 -268
- package/action-escrow-custodian.js +3 -394
- package/action-escrow-evidence.js +3 -858
- package/action-escrow-package.js +3 -1668
- package/action-escrow-postgres.js +3 -337
- package/action-escrow-state.js +3 -396
- package/action-escrow-verifiers.js +3 -331
- package/action-escrow.js +3 -2447
- package/action-packs.js +3 -140
- package/adapters/_kit.js +3 -104
- package/adapters/aws.js +3 -108
- package/adapters/cloudflare.js +3 -54
- package/adapters/gcp.js +3 -70
- package/adapters/github.js +3 -109
- package/adapters/jira.js +3 -55
- package/adapters/k8s.js +3 -70
- package/adapters/linear.js +3 -55
- package/adapters/salesforce.js +3 -55
- package/adapters/stripe.js +3 -81
- package/adapters/supabase.js +3 -121
- package/adapters/terraform.js +3 -59
- package/adapters/vercel.js +3 -84
- package/aec-execution.js +3 -312
- package/breakglass.js +3 -582
- package/capability-receipt.js +4 -0
- package/cf1-conformance.js +3 -106
- package/challenge-store.js +3 -69
- package/control-plane.js +3 -340
- package/coverage.js +3 -721
- package/demo.mjs +1 -1
- package/deployment-attestation.js +3 -247
- package/dist/action-control-manifest.d.ts +82 -0
- package/dist/action-control-manifest.d.ts.map +1 -0
- package/dist/action-control-manifest.js +422 -0
- package/dist/action-control-manifest.js.map +1 -0
- package/dist/action-escrow-custodian.d.ts +74 -0
- package/dist/action-escrow-custodian.d.ts.map +1 -0
- package/dist/action-escrow-custodian.js +363 -0
- package/dist/action-escrow-custodian.js.map +1 -0
- package/dist/action-escrow-evidence.d.ts +130 -0
- package/dist/action-escrow-evidence.d.ts.map +1 -0
- package/dist/action-escrow-evidence.js +1012 -0
- package/dist/action-escrow-evidence.js.map +1 -0
- package/dist/action-escrow-package.d.ts +10 -0
- package/dist/action-escrow-package.d.ts.map +1 -0
- package/dist/action-escrow-package.js +1583 -0
- package/dist/action-escrow-package.js.map +1 -0
- package/dist/action-escrow-postgres.d.ts +62 -0
- package/dist/action-escrow-postgres.d.ts.map +1 -0
- package/dist/action-escrow-postgres.js +339 -0
- package/dist/action-escrow-postgres.js.map +1 -0
- package/dist/action-escrow-state.d.ts +141 -0
- package/dist/action-escrow-state.d.ts.map +1 -0
- package/dist/action-escrow-state.js +413 -0
- package/dist/action-escrow-state.js.map +1 -0
- package/dist/action-escrow-verifiers.d.ts +65 -0
- package/dist/action-escrow-verifiers.d.ts.map +1 -0
- package/dist/action-escrow-verifiers.js +421 -0
- package/dist/action-escrow-verifiers.js.map +1 -0
- package/dist/action-escrow.d.ts +112 -0
- package/dist/action-escrow.d.ts.map +1 -0
- package/dist/action-escrow.js +2661 -0
- package/dist/action-escrow.js.map +1 -0
- package/dist/action-packs.d.ts +54 -0
- package/dist/action-packs.d.ts.map +1 -0
- package/dist/action-packs.js +143 -0
- package/dist/action-packs.js.map +1 -0
- package/dist/adapters/_kit.d.ts +50 -0
- package/dist/adapters/_kit.d.ts.map +1 -0
- package/dist/adapters/_kit.js +98 -0
- package/dist/adapters/_kit.js.map +1 -0
- package/dist/adapters/aws.d.ts +158 -0
- package/dist/adapters/aws.d.ts.map +1 -0
- package/dist/adapters/aws.js +136 -0
- package/dist/adapters/aws.js.map +1 -0
- package/dist/adapters/cloudflare.d.ts +116 -0
- package/dist/adapters/cloudflare.d.ts.map +1 -0
- package/dist/adapters/cloudflare.js +75 -0
- package/dist/adapters/cloudflare.js.map +1 -0
- package/dist/adapters/gcp.d.ts +139 -0
- package/dist/adapters/gcp.d.ts.map +1 -0
- package/dist/adapters/gcp.js +69 -0
- package/dist/adapters/gcp.js.map +1 -0
- package/dist/adapters/github.d.ts +123 -0
- package/dist/adapters/github.d.ts.map +1 -0
- package/dist/adapters/github.js +105 -0
- package/dist/adapters/github.js.map +1 -0
- package/dist/adapters/jira.d.ts +109 -0
- package/dist/adapters/jira.d.ts.map +1 -0
- package/dist/adapters/jira.js +54 -0
- package/dist/adapters/jira.js.map +1 -0
- package/dist/adapters/k8s.d.ts +139 -0
- package/dist/adapters/k8s.d.ts.map +1 -0
- package/dist/adapters/k8s.js +69 -0
- package/dist/adapters/k8s.js.map +1 -0
- package/dist/adapters/linear.d.ts +109 -0
- package/dist/adapters/linear.d.ts.map +1 -0
- package/dist/adapters/linear.js +54 -0
- package/dist/adapters/linear.js.map +1 -0
- package/dist/adapters/salesforce.d.ts +109 -0
- package/dist/adapters/salesforce.d.ts.map +1 -0
- package/dist/adapters/salesforce.js +54 -0
- package/dist/adapters/salesforce.js.map +1 -0
- package/dist/adapters/stripe.d.ts +116 -0
- package/dist/adapters/stripe.d.ts.map +1 -0
- package/dist/adapters/stripe.js +77 -0
- package/dist/adapters/stripe.js.map +1 -0
- package/dist/adapters/supabase.d.ts +127 -0
- package/dist/adapters/supabase.d.ts.map +1 -0
- package/dist/adapters/supabase.js +112 -0
- package/dist/adapters/supabase.js.map +1 -0
- package/dist/adapters/terraform.d.ts +109 -0
- package/dist/adapters/terraform.d.ts.map +1 -0
- package/dist/adapters/terraform.js +58 -0
- package/dist/adapters/terraform.js.map +1 -0
- package/dist/adapters/vercel.d.ts +114 -0
- package/dist/adapters/vercel.d.ts.map +1 -0
- package/dist/adapters/vercel.js +81 -0
- package/dist/adapters/vercel.js.map +1 -0
- package/dist/aec-execution.d.ts +107 -0
- package/dist/aec-execution.d.ts.map +1 -0
- package/dist/aec-execution.js +334 -0
- package/dist/aec-execution.js.map +1 -0
- package/dist/breakglass.d.ts +205 -0
- package/dist/breakglass.d.ts.map +1 -0
- package/dist/breakglass.js +627 -0
- package/dist/breakglass.js.map +1 -0
- package/dist/capability-receipt.d.ts +601 -0
- package/dist/capability-receipt.d.ts.map +1 -0
- package/dist/capability-receipt.js +1177 -0
- package/dist/capability-receipt.js.map +1 -0
- package/dist/cf1-conformance.d.ts +83 -0
- package/dist/cf1-conformance.d.ts.map +1 -0
- package/dist/cf1-conformance.js +94 -0
- package/dist/cf1-conformance.js.map +1 -0
- package/dist/challenge-store.d.ts +20 -0
- package/dist/challenge-store.d.ts.map +1 -0
- package/dist/challenge-store.js +62 -0
- package/dist/challenge-store.js.map +1 -0
- package/dist/control-plane.d.ts +136 -0
- package/dist/control-plane.d.ts.map +1 -0
- package/dist/control-plane.js +349 -0
- package/dist/control-plane.js.map +1 -0
- package/dist/coverage.d.ts +156 -0
- package/dist/coverage.d.ts.map +1 -0
- package/dist/coverage.js +768 -0
- package/dist/coverage.js.map +1 -0
- package/dist/deployment-attestation.d.ts +93 -0
- package/dist/deployment-attestation.d.ts.map +1 -0
- package/dist/deployment-attestation.js +262 -0
- package/dist/deployment-attestation.js.map +1 -0
- package/dist/eg1-conformance.d.ts +286 -0
- package/dist/eg1-conformance.d.ts.map +1 -0
- package/dist/eg1-conformance.js +379 -0
- package/dist/eg1-conformance.js.map +1 -0
- package/dist/enterprise.d.ts +88 -0
- package/dist/enterprise.d.ts.map +1 -0
- package/dist/enterprise.js +194 -0
- package/dist/enterprise.js.map +1 -0
- package/dist/evidence-postgres.d.ts +123 -0
- package/dist/evidence-postgres.d.ts.map +1 -0
- package/dist/evidence-postgres.js +343 -0
- package/dist/evidence-postgres.js.map +1 -0
- package/dist/evidence.d.ts +113 -0
- package/dist/evidence.d.ts.map +1 -0
- package/dist/evidence.js +369 -0
- package/dist/evidence.js.map +1 -0
- package/dist/execution-binding.d.ts +49 -0
- package/dist/execution-binding.d.ts.map +1 -0
- package/dist/execution-binding.js +243 -0
- package/dist/execution-binding.js.map +1 -0
- package/dist/formal-runtime-map.d.ts +42 -0
- package/dist/formal-runtime-map.d.ts.map +1 -0
- package/dist/formal-runtime-map.js +23 -0
- package/dist/formal-runtime-map.js.map +1 -0
- package/dist/index.d.ts +1003 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +1737 -0
- package/dist/index.js.map +1 -0
- package/dist/key-registry.d.ts +60 -0
- package/dist/key-registry.d.ts.map +1 -0
- package/dist/key-registry.js +144 -0
- package/dist/key-registry.js.map +1 -0
- package/dist/mcp.d.ts +53 -0
- package/dist/mcp.d.ts.map +1 -0
- package/dist/mcp.js +112 -0
- package/dist/mcp.js.map +1 -0
- package/dist/metering.d.ts +80 -0
- package/dist/metering.d.ts.map +1 -0
- package/dist/metering.js +227 -0
- package/dist/metering.js.map +1 -0
- package/dist/metrics.d.ts +68 -0
- package/dist/metrics.d.ts.map +1 -0
- package/dist/metrics.js +242 -0
- package/dist/metrics.js.map +1 -0
- package/dist/network-witness.d.ts +167 -0
- package/dist/network-witness.d.ts.map +1 -0
- package/dist/network-witness.js +520 -0
- package/dist/network-witness.js.map +1 -0
- package/dist/proposal-to-effect.d.ts +146 -0
- package/dist/proposal-to-effect.d.ts.map +1 -0
- package/dist/proposal-to-effect.js +427 -0
- package/dist/proposal-to-effect.js.map +1 -0
- package/dist/protocol-state.d.ts +141 -0
- package/dist/protocol-state.d.ts.map +1 -0
- package/dist/protocol-state.js +106 -0
- package/dist/protocol-state.js.map +1 -0
- package/dist/receipt-program.d.ts +59 -0
- package/dist/receipt-program.d.ts.map +1 -0
- package/dist/receipt-program.js +1053 -0
- package/dist/receipt-program.js.map +1 -0
- package/dist/reliance-kernel.d.ts +66 -0
- package/dist/reliance-kernel.d.ts.map +1 -0
- package/dist/reliance-kernel.js +103 -0
- package/dist/reliance-kernel.js.map +1 -0
- package/dist/reliance-packet.d.ts +67 -0
- package/dist/reliance-packet.d.ts.map +1 -0
- package/dist/reliance-packet.js +176 -0
- package/dist/reliance-packet.js.map +1 -0
- package/dist/remedy-program-postgres.d.ts +37 -0
- package/dist/remedy-program-postgres.d.ts.map +1 -0
- package/dist/remedy-program-postgres.js +291 -0
- package/dist/remedy-program-postgres.js.map +1 -0
- package/dist/remedy-program-receipt.d.ts +97 -0
- package/dist/remedy-program-receipt.d.ts.map +1 -0
- package/dist/remedy-program-receipt.js +817 -0
- package/dist/remedy-program-receipt.js.map +1 -0
- package/dist/remedy-program.d.ts +76 -0
- package/dist/remedy-program.d.ts.map +1 -0
- package/dist/remedy-program.js +1346 -0
- package/dist/remedy-program.js.map +1 -0
- package/dist/reports/art14.d.ts +101 -0
- package/dist/reports/art14.d.ts.map +1 -0
- package/dist/reports/art14.js +340 -0
- package/dist/reports/art14.js.map +1 -0
- package/dist/reports/assurance-package.d.ts +197 -0
- package/dist/reports/assurance-package.d.ts.map +1 -0
- package/dist/reports/assurance-package.js +313 -0
- package/dist/reports/assurance-package.js.map +1 -0
- package/dist/reports/auditor-workpaper.d.ts +194 -0
- package/dist/reports/auditor-workpaper.d.ts.map +1 -0
- package/dist/reports/auditor-workpaper.js +545 -0
- package/dist/reports/auditor-workpaper.js.map +1 -0
- package/dist/reports/external-verification.d.ts +76 -0
- package/dist/reports/external-verification.d.ts.map +1 -0
- package/dist/reports/external-verification.js +209 -0
- package/dist/reports/external-verification.js.map +1 -0
- package/dist/reports/reperform.d.ts +122 -0
- package/dist/reports/reperform.d.ts.map +1 -0
- package/dist/reports/reperform.js +432 -0
- package/dist/reports/reperform.js.map +1 -0
- package/dist/reports/underwriter.d.ts +127 -0
- package/dist/reports/underwriter.d.ts.map +1 -0
- package/dist/reports/underwriter.js +348 -0
- package/dist/reports/underwriter.js.map +1 -0
- package/dist/retention.d.ts +88 -0
- package/dist/retention.d.ts.map +1 -0
- package/dist/retention.js +90 -0
- package/dist/retention.js.map +1 -0
- package/dist/roster.d.ts +113 -0
- package/dist/roster.d.ts.map +1 -0
- package/dist/roster.js +261 -0
- package/dist/roster.js.map +1 -0
- package/dist/runtime-monitor.d.ts +132 -0
- package/dist/runtime-monitor.d.ts.map +1 -0
- package/dist/runtime-monitor.js +197 -0
- package/dist/runtime-monitor.js.map +1 -0
- package/dist/settlement.d.ts +68 -0
- package/dist/settlement.d.ts.map +1 -0
- package/dist/settlement.js +306 -0
- package/dist/settlement.js.map +1 -0
- package/dist/siem.d.ts +261 -0
- package/dist/siem.d.ts.map +1 -0
- package/dist/siem.js +247 -0
- package/dist/siem.js.map +1 -0
- package/dist/store-postgres.d.ts +107 -0
- package/dist/store-postgres.d.ts.map +1 -0
- package/dist/store-postgres.js +152 -0
- package/dist/store-postgres.js.map +1 -0
- package/dist/store.d.ts +95 -0
- package/dist/store.d.ts.map +1 -0
- package/dist/store.js +202 -0
- package/dist/store.js.map +1 -0
- package/dist/strict-json.d.ts +14 -0
- package/dist/strict-json.d.ts.map +1 -0
- package/dist/strict-json.js +132 -0
- package/dist/strict-json.js.map +1 -0
- package/dist/trust-program-adapters.d.ts +122 -0
- package/dist/trust-program-adapters.d.ts.map +1 -0
- package/dist/trust-program-adapters.js +802 -0
- package/dist/trust-program-adapters.js.map +1 -0
- package/dist/trust-program-postgres.d.ts +41 -0
- package/dist/trust-program-postgres.d.ts.map +1 -0
- package/dist/trust-program-postgres.js +346 -0
- package/dist/trust-program-postgres.js.map +1 -0
- package/dist/trust-program-revocation.d.ts +206 -0
- package/dist/trust-program-revocation.d.ts.map +1 -0
- package/dist/trust-program-revocation.js +600 -0
- package/dist/trust-program-revocation.js.map +1 -0
- package/dist/trust-program.d.ts +197 -0
- package/dist/trust-program.d.ts.map +1 -0
- package/dist/trust-program.js +1351 -0
- package/dist/trust-program.js.map +1 -0
- package/dist/witness-postgres.d.ts +43 -0
- package/dist/witness-postgres.d.ts.map +1 -0
- package/dist/witness-postgres.js +92 -0
- package/dist/witness-postgres.js.map +1 -0
- package/dist/zk-range-proof.d.ts +78 -0
- package/dist/zk-range-proof.d.ts.map +1 -0
- package/dist/zk-range-proof.js +228 -0
- package/dist/zk-range-proof.js.map +1 -0
- package/eg1-conformance.js +3 -404
- package/enterprise.js +3 -180
- package/ep-assure.mjs +1 -0
- package/evidence-postgres.js +3 -356
- package/evidence.js +4 -360
- package/execution-binding.js +3 -212
- package/formal-runtime-map.js +4 -0
- package/index.js +4 -1249
- package/key-registry.js +3 -134
- package/mcp.js +3 -109
- package/metering.js +3 -226
- package/metrics.js +3 -231
- package/network-witness.js +3 -499
- package/package.json +35 -4
- package/proposal-to-effect.js +4 -0
- package/receipt-program.js +4 -0
- package/reliance-kernel.js +3 -112
- package/reliance-packet.js +3 -175
- package/remedy-program-postgres.js +4 -0
- package/remedy-program-receipt.js +4 -0
- package/remedy-program.js +4 -0
- package/reports/art14.js +0 -0
- package/reports/assurance-package.js +2 -312
- package/reports/auditor-workpaper.js +3 -537
- package/reports/external-verification.js +3 -222
- package/reports/reperform.js +3 -424
- package/reports/underwriter.js +3 -339
- package/retention.js +3 -84
- package/roster.js +3 -264
- package/runtime-monitor.js +4 -0
- package/settlement.js +3 -299
- package/siem.js +3 -235
- package/src/action-control-manifest.ts +424 -0
- package/src/action-escrow-custodian.ts +427 -0
- package/src/action-escrow-evidence.ts +1178 -0
- package/src/action-escrow-package.ts +1821 -0
- package/src/action-escrow-postgres.ts +366 -0
- package/src/action-escrow-state.ts +498 -0
- package/src/action-escrow-verifiers.ts +484 -0
- package/src/action-escrow.ts +3039 -0
- package/src/action-packs.ts +170 -0
- package/src/adapters/_kit.ts +117 -0
- package/src/adapters/aws.ts +142 -0
- package/src/adapters/cloudflare.ts +78 -0
- package/src/adapters/gcp.ts +71 -0
- package/src/adapters/github.ts +110 -0
- package/src/adapters/jira.ts +56 -0
- package/src/adapters/k8s.ts +71 -0
- package/src/adapters/linear.ts +56 -0
- package/src/adapters/salesforce.ts +56 -0
- package/src/adapters/stripe.ts +82 -0
- package/src/adapters/supabase.ts +122 -0
- package/src/adapters/terraform.ts +60 -0
- package/src/adapters/vercel.ts +85 -0
- package/src/aec-execution.ts +354 -0
- package/src/breakglass.ts +647 -0
- package/src/capability-receipt.ts +1258 -0
- package/src/cf1-conformance.ts +120 -0
- package/src/challenge-store.ts +70 -0
- package/src/control-plane.ts +374 -0
- package/src/coverage.ts +832 -0
- package/src/deployment-attestation.ts +259 -0
- package/src/eg1-conformance.ts +471 -0
- package/src/enterprise.ts +195 -0
- package/src/evidence-postgres.ts +374 -0
- package/src/evidence.ts +367 -0
- package/src/execution-binding.ts +236 -0
- package/src/formal-runtime-map.ts +24 -0
- package/src/index.ts +2127 -0
- package/src/key-registry.ts +150 -0
- package/src/mcp.ts +120 -0
- package/src/metering.ts +237 -0
- package/src/metrics.ts +232 -0
- package/src/network-witness.ts +585 -0
- package/src/proposal-to-effect.ts +571 -0
- package/src/protocol-state.ts +143 -0
- package/src/protocol-state.typecheck.ts +94 -0
- package/src/receipt-program.ts +1105 -0
- package/src/reliance-kernel.ts +119 -0
- package/src/reliance-packet.ts +206 -0
- package/src/remedy-program-postgres.ts +367 -0
- package/src/remedy-program-receipt.ts +891 -0
- package/src/remedy-program.ts +1388 -0
- package/src/reports/art14.ts +361 -0
- package/src/reports/assurance-package.ts +330 -0
- package/src/reports/auditor-workpaper.ts +558 -0
- package/src/reports/external-verification.ts +232 -0
- package/src/reports/reperform.ts +441 -0
- package/src/reports/underwriter.ts +356 -0
- package/src/retention.ts +100 -0
- package/src/roster.ts +273 -0
- package/src/runtime-monitor.ts +217 -0
- package/src/settlement.ts +313 -0
- package/src/siem.ts +247 -0
- package/src/store-postgres.ts +167 -0
- package/src/store.ts +214 -0
- package/src/strict-json.ts +115 -0
- package/src/trust-program-adapters.ts +847 -0
- package/src/trust-program-postgres.ts +429 -0
- package/src/trust-program-revocation.ts +751 -0
- package/src/trust-program.ts +1460 -0
- package/src/witness-postgres.ts +106 -0
- package/src/zk-range-proof.ts +250 -0
- package/store-postgres.js +3 -161
- package/store.js +4 -198
- package/strict-json.js +3 -102
- package/trust-program-adapters.js +4 -0
- package/trust-program-postgres.js +4 -0
- package/trust-program-revocation.js +4 -0
- package/trust-program.js +4 -0
- package/witness-postgres.js +3 -96
- package/zk-range-proof.js +4 -0
|
@@ -0,0 +1,232 @@
|
|
|
1
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
2
|
+
/**
|
|
3
|
+
* EP-EXTERNAL-VERIFICATION-STATEMENT-v1.
|
|
4
|
+
*
|
|
5
|
+
* A signed statement a NON-EMILIA verifier can issue after it re-performs an
|
|
6
|
+
* evidence log, replays an admissibility profile, or runs a conformance harness.
|
|
7
|
+
* This is the missing adoption rail between "our verifier works" and "an
|
|
8
|
+
* outside party says exactly what they checked."
|
|
9
|
+
*
|
|
10
|
+
* Scope is intentionally narrow:
|
|
11
|
+
* - the statement signs a procedure, inputs, result, and limitations;
|
|
12
|
+
* - it does NOT authorize an action;
|
|
13
|
+
* - it does NOT certify business correctness;
|
|
14
|
+
* - acceptance is by a relying party pinning the external verifier key.
|
|
15
|
+
*/
|
|
16
|
+
import crypto from 'node:crypto';
|
|
17
|
+
// In-package canonicalize (byte-identical to lib/canonical-json.js): reports must
|
|
18
|
+
// never import outside the package root or the published tarball cannot resolve it.
|
|
19
|
+
import { canonicalize } from '../execution-binding.js';
|
|
20
|
+
|
|
21
|
+
export const EXTERNAL_VERIFICATION_STATEMENT_VERSION = 'EP-EXTERNAL-VERIFICATION-STATEMENT-v1';
|
|
22
|
+
export const EXTERNAL_VERIFICATION_DOMAIN = 'EP-EXTERNAL-VERIFICATION-STATEMENT-v1\0';
|
|
23
|
+
|
|
24
|
+
const SHA256_RE = /^sha256:[0-9a-f]{64}$/i;
|
|
25
|
+
|
|
26
|
+
function sha256hex(bytes) {
|
|
27
|
+
return crypto.createHash('sha256').update(bytes).digest('hex');
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function publicKeyToB64u(key) {
|
|
31
|
+
return crypto.createPublicKey(key).export({ type: 'spki', format: 'der' }).toString('base64url');
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function keyIdFor(publicKeyB64u) {
|
|
35
|
+
return `ep:external-verifier-key:sha256:${sha256hex(Buffer.from(publicKeyB64u, 'base64url')).slice(0, 16)}`;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function signingBytes(unsignedStatement) {
|
|
39
|
+
return Buffer.from(EXTERNAL_VERIFICATION_DOMAIN + canonicalize(unsignedStatement), 'utf8');
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function unsigned(statement) {
|
|
43
|
+
if (!statement || typeof statement !== 'object' || Array.isArray(statement)) {
|
|
44
|
+
throw new Error('statement must be an object');
|
|
45
|
+
}
|
|
46
|
+
const { signature: _signature, ...body } = statement;
|
|
47
|
+
return body;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/** Digest of the signed statement body, excluding the signature envelope. */
|
|
51
|
+
export function externalVerificationDigest(statement) {
|
|
52
|
+
return `sha256:${sha256hex(signingBytes(unsigned(statement)))}`;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function normalizeChecks(checks) {
|
|
56
|
+
if (!Array.isArray(checks)) return [];
|
|
57
|
+
return checks.map((c) => ({
|
|
58
|
+
id: String(c?.id ?? ''),
|
|
59
|
+
ok: c?.ok === true,
|
|
60
|
+
...(c?.detail !== undefined ? { detail: c.detail } : {}),
|
|
61
|
+
})).filter((c) => c.id);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Build and sign an external-verifier statement.
|
|
66
|
+
*
|
|
67
|
+
* @param {object} args
|
|
68
|
+
* @param {object} args.verifier {id, name?, organization?}
|
|
69
|
+
* @param {object} args.subject what was checked, e.g. {kind:'evidence_log', head:'sha256:...'}
|
|
70
|
+
* @param {object} args.procedure {id, version?, tool?, command?}
|
|
71
|
+
* @param {object} args.result {status, checks?, artifact_digest?}
|
|
72
|
+
* @param {object} [args.inputs] stable digests/ids the procedure consumed
|
|
73
|
+
* @param {string[]} [args.limitations] honest non-claims
|
|
74
|
+
* @param {string|number} [args.generated_at] ISO or epoch millis
|
|
75
|
+
* @param {crypto.KeyObject} privateKey Ed25519 private key
|
|
76
|
+
*/
|
|
77
|
+
export function signExternalVerificationStatement(args, privateKey) {
|
|
78
|
+
if (!privateKey) throw new Error('privateKey is required');
|
|
79
|
+
const generatedAt = args?.generated_at !== undefined
|
|
80
|
+
? new Date(args.generated_at).toISOString()
|
|
81
|
+
: new Date().toISOString();
|
|
82
|
+
const publicKey = publicKeyToB64u(privateKey);
|
|
83
|
+
const body = {
|
|
84
|
+
'@version': EXTERNAL_VERIFICATION_STATEMENT_VERSION,
|
|
85
|
+
generated_at: generatedAt,
|
|
86
|
+
verifier: {
|
|
87
|
+
id: args?.verifier?.id ?? keyIdFor(publicKey),
|
|
88
|
+
...(args?.verifier?.name ? { name: args.verifier.name } : {}),
|
|
89
|
+
...(args?.verifier?.organization ? { organization: args.verifier.organization } : {}),
|
|
90
|
+
},
|
|
91
|
+
subject: args?.subject ?? {},
|
|
92
|
+
procedure: args?.procedure ?? {},
|
|
93
|
+
inputs: args?.inputs ?? {},
|
|
94
|
+
result: {
|
|
95
|
+
status: String(args?.result?.status ?? 'unknown'),
|
|
96
|
+
checks: normalizeChecks(args?.result?.checks),
|
|
97
|
+
...(args?.result?.artifact_digest ? { artifact_digest: args.result.artifact_digest } : {}),
|
|
98
|
+
},
|
|
99
|
+
limitations: Array.isArray(args?.limitations) && args.limitations.length
|
|
100
|
+
? args.limitations.map(String)
|
|
101
|
+
: [
|
|
102
|
+
'This statement records the external verifier procedure and result; it does not authorize the action.',
|
|
103
|
+
'It does not certify business correctness, legal compliance, or human wisdom.',
|
|
104
|
+
'Acceptance depends on the relying party pinning the verifier key out of band.',
|
|
105
|
+
'The statement carries no expiry and no consumer binding; it is replayable verbatim, and generated_at is asserted by the signer, not verified.',
|
|
106
|
+
],
|
|
107
|
+
};
|
|
108
|
+
|
|
109
|
+
const digest = externalVerificationDigest(body);
|
|
110
|
+
const sig = crypto.sign(null, signingBytes(body), privateKey).toString('base64url');
|
|
111
|
+
return Object.freeze({
|
|
112
|
+
...body,
|
|
113
|
+
signature: {
|
|
114
|
+
algorithm: 'Ed25519',
|
|
115
|
+
key_id: keyIdFor(publicKey),
|
|
116
|
+
public_key: publicKey,
|
|
117
|
+
statement_digest: digest,
|
|
118
|
+
signature_b64u: sig,
|
|
119
|
+
},
|
|
120
|
+
});
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* Verify a signed external-verifier statement against pinned verifier keys.
|
|
125
|
+
*
|
|
126
|
+
* @param {object} statement
|
|
127
|
+
* @param {{pinnedVerifierKeys?:Array<{verifier_id?:string,key_id?:string,public_key:string}>}} [opts]
|
|
128
|
+
*/
|
|
129
|
+
export function verifyExternalVerificationStatement(statement, opts: {
|
|
130
|
+
pinnedVerifierKeys?: Array<{ verifier_id?: string; key_id?: string; public_key: string }>;
|
|
131
|
+
} = {}): {
|
|
132
|
+
verified: boolean;
|
|
133
|
+
accepted: boolean;
|
|
134
|
+
checks: Record<string, boolean>;
|
|
135
|
+
reason?: string;
|
|
136
|
+
statement_digest?: string;
|
|
137
|
+
verifier_id?: string;
|
|
138
|
+
key_id?: string;
|
|
139
|
+
} {
|
|
140
|
+
const fail = (reason, extra = {}) => ({
|
|
141
|
+
verified: false,
|
|
142
|
+
accepted: false,
|
|
143
|
+
checks: {
|
|
144
|
+
version: statement?.['@version'] === EXTERNAL_VERIFICATION_STATEMENT_VERSION,
|
|
145
|
+
signature: false,
|
|
146
|
+
pinned_verifier_key: false,
|
|
147
|
+
statement_digest: false,
|
|
148
|
+
},
|
|
149
|
+
reason,
|
|
150
|
+
...extra,
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
if (statement?.['@version'] !== EXTERNAL_VERIFICATION_STATEMENT_VERSION) {
|
|
154
|
+
return fail('unsupported_version');
|
|
155
|
+
}
|
|
156
|
+
const sig = statement.signature;
|
|
157
|
+
if (!sig || sig.algorithm !== 'Ed25519' || typeof sig.public_key !== 'string' || typeof sig.signature_b64u !== 'string') {
|
|
158
|
+
return fail('signature_missing_or_malformed');
|
|
159
|
+
}
|
|
160
|
+
if (typeof sig.statement_digest !== 'string' || !SHA256_RE.test(sig.statement_digest)) {
|
|
161
|
+
return fail('statement_digest_missing_or_malformed');
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
let digest;
|
|
165
|
+
try {
|
|
166
|
+
digest = externalVerificationDigest(statement);
|
|
167
|
+
} catch {
|
|
168
|
+
return fail('statement_uncanonicalizable');
|
|
169
|
+
}
|
|
170
|
+
if (digest !== sig.statement_digest) {
|
|
171
|
+
return fail('statement_digest_mismatch', { statement_digest: digest });
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
const pinned = Array.isArray(opts.pinnedVerifierKeys) ? opts.pinnedVerifierKeys : [];
|
|
175
|
+
const verifierId = statement.verifier?.id ?? null;
|
|
176
|
+
// key_id is always DERIVED from the carried public key. The envelope's key_id is
|
|
177
|
+
// outside the signed bytes, so it is attacker-malleable; if present it must match
|
|
178
|
+
// the derived value or the statement is refused.
|
|
179
|
+
const keyId = keyIdFor(sig.public_key);
|
|
180
|
+
if (sig.key_id !== undefined && sig.key_id !== keyId) {
|
|
181
|
+
return fail('key_id_mismatch', { statement_digest: digest });
|
|
182
|
+
}
|
|
183
|
+
// A pin grants an identity, not just a key: every usable pin entry must name the
|
|
184
|
+
// verifier_id it vouches for. A pin that matches the key but omits verifier_id
|
|
185
|
+
// (or names a different one) never binds the statement's claimed identity.
|
|
186
|
+
const keyMatched = pinned.filter((k) => k?.public_key === sig.public_key
|
|
187
|
+
&& (k.key_id === undefined || k.key_id === keyId));
|
|
188
|
+
const pin = keyMatched.find((k) => typeof k.verifier_id === 'string' && k.verifier_id === verifierId);
|
|
189
|
+
if (!pin) {
|
|
190
|
+
return {
|
|
191
|
+
verified: false,
|
|
192
|
+
accepted: false,
|
|
193
|
+
checks: { version: true, signature: false, pinned_verifier_key: false, statement_digest: true },
|
|
194
|
+
reason: keyMatched.length ? 'pin_missing_or_mismatched_verifier_id' : 'verifier_key_not_pinned',
|
|
195
|
+
statement_digest: digest,
|
|
196
|
+
};
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
let ok = false;
|
|
200
|
+
try {
|
|
201
|
+
const publicKey = crypto.createPublicKey({ key: Buffer.from(sig.public_key, 'base64url'), type: 'spki', format: 'der' });
|
|
202
|
+
ok = crypto.verify(null, signingBytes(unsigned(statement)), publicKey, Buffer.from(sig.signature_b64u, 'base64url'));
|
|
203
|
+
} catch {
|
|
204
|
+
ok = false;
|
|
205
|
+
}
|
|
206
|
+
if (!ok) {
|
|
207
|
+
return {
|
|
208
|
+
verified: false,
|
|
209
|
+
accepted: false,
|
|
210
|
+
checks: { version: true, signature: false, pinned_verifier_key: true, statement_digest: true },
|
|
211
|
+
reason: 'signature_invalid',
|
|
212
|
+
statement_digest: digest,
|
|
213
|
+
};
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
return {
|
|
217
|
+
verified: true,
|
|
218
|
+
accepted: true,
|
|
219
|
+
checks: { version: true, signature: true, pinned_verifier_key: true, statement_digest: true },
|
|
220
|
+
verifier_id: verifierId,
|
|
221
|
+
key_id: keyId,
|
|
222
|
+
statement_digest: digest,
|
|
223
|
+
};
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
export default {
|
|
227
|
+
EXTERNAL_VERIFICATION_STATEMENT_VERSION,
|
|
228
|
+
EXTERNAL_VERIFICATION_DOMAIN,
|
|
229
|
+
externalVerificationDigest,
|
|
230
|
+
signExternalVerificationStatement,
|
|
231
|
+
verifyExternalVerificationStatement,
|
|
232
|
+
};
|
|
@@ -0,0 +1,441 @@
|
|
|
1
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
2
|
+
/**
|
|
3
|
+
* EMILIA Gate — independent re-performance verifier (EP-GATE-REPERFORMANCE-v1).
|
|
4
|
+
*
|
|
5
|
+
* The auditor's "trust nothing, recompute everything" tool. Re-performance is
|
|
6
|
+
* the highest form of audit evidence: instead of reading the deployer's report
|
|
7
|
+
* and believing it, the auditor takes the raw evidence entries and REDOES the
|
|
8
|
+
* work — rebuilds the hash chain link by link and checks it with the evidence
|
|
9
|
+
* log's own verifier, cryptographically re-verifies every receipt / signoff /
|
|
10
|
+
* quorum document an entry carries, recomputes the decision counts from
|
|
11
|
+
* scratch, and diffs those recomputed numbers against what a report pack
|
|
12
|
+
* claims. Vendor logs assert; this recomputes.
|
|
13
|
+
*
|
|
14
|
+
* INDEPENDENCE BY CONSTRUCTION: counts are recomputed here from the raw
|
|
15
|
+
* entries — this module deliberately does NOT import metering.js or any report
|
|
16
|
+
* builder, so a bug in a report builder cannot hide from its own cross-check.
|
|
17
|
+
* The only imports are the primitives being re-driven: the evidence log's real
|
|
18
|
+
* verify() and the protocol verifiers (verifyEmiliaReceipt /
|
|
19
|
+
* verifyWebAuthnSignoff / verifyQuorum).
|
|
20
|
+
*
|
|
21
|
+
* FAIL CLOSED everywhere:
|
|
22
|
+
* - one broken chain link fails the chain from that point — the remainder is
|
|
23
|
+
* not vouched for;
|
|
24
|
+
* - one failed re-verification is a NAMED failure ({hash, reason}), never
|
|
25
|
+
* absorbed into a pass count;
|
|
26
|
+
* - an entry that references a receipt but does not carry the verifiable
|
|
27
|
+
* material is counted `not_reverifiable` — never silently passed;
|
|
28
|
+
* - a reported pack of unknown @version is refused, never fuzzily matched.
|
|
29
|
+
*
|
|
30
|
+
* HONESTY BOUNDARY (carried inside the artifact): this SUPPORTS an auditor's
|
|
31
|
+
* re-performance procedure. It does not conclude, opine, or certify — and it
|
|
32
|
+
* cannot establish log COMPLETENESS (a chain rewritten in full, or entries
|
|
33
|
+
* withheld before supply, is invisible from the entries alone; anchor the head
|
|
34
|
+
* hash externally to close that), nor runtime freshness / one-time consumption
|
|
35
|
+
* (properties of the moment of decision, not re-performable after the fact).
|
|
36
|
+
*
|
|
37
|
+
* Deterministic: pure over (entries, issuerKeys); time enters only through the
|
|
38
|
+
* injectable `now` (generated_at). No sampling, no randomness.
|
|
39
|
+
*/
|
|
40
|
+
import crypto from 'node:crypto';
|
|
41
|
+
import { createEvidenceLog } from '../evidence.js';
|
|
42
|
+
import { validatePinnedQuorumPolicy, verifyEmiliaReceipt } from '@emilia-protocol/require-receipt';
|
|
43
|
+
import { verifyQuorum, verifyWebAuthnSignoff } from '@emilia-protocol/verify';
|
|
44
|
+
|
|
45
|
+
export const REPERFORMANCE_VERSION = 'EP-GATE-REPERFORMANCE-v1';
|
|
46
|
+
|
|
47
|
+
// Wire-format identifiers of the packs compareToReported() can cross-check.
|
|
48
|
+
// Restated locally — NOT imported from the builders — so this module remains a
|
|
49
|
+
// standalone auditor tool with zero dependency on the code it checks.
|
|
50
|
+
const USAGE_PACK_VERSION = 'EP-GATE-USAGE-v1';
|
|
51
|
+
const UNDERWRITER_PACK_VERSION = 'EP-GATE-UNDERWRITER-ATTESTATION-v1';
|
|
52
|
+
|
|
53
|
+
// A verifiable payload carried by an evidence entry (see collectMaterial below).
|
|
54
|
+
type CarriedMaterial = { type: string; doc: any; key?: any };
|
|
55
|
+
|
|
56
|
+
/** Deterministic key-sorted copy — output is byte-stable regardless of entry order. */
|
|
57
|
+
function sortedCounts(obj) {
|
|
58
|
+
const out = {};
|
|
59
|
+
for (const k of Object.keys(obj).sort()) out[k] = obj[k];
|
|
60
|
+
return out;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/** The checks a verifier reported false, as one stable, named string. */
|
|
64
|
+
function failingChecks(checks) {
|
|
65
|
+
const bad = Object.keys(checks || {}).filter((k) => checks[k] === false).sort();
|
|
66
|
+
return bad.length ? bad.join('+') : 'unspecified';
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* The verifiable payloads an entry CARRIES (not the verification RESULTS the
|
|
71
|
+
* gate recorded about them — `tier_evidence` / `rejected` are the gate's own
|
|
72
|
+
* conclusions and are deliberately ignored: re-performance trusts nothing the
|
|
73
|
+
* gate concluded, only material it can re-verify itself).
|
|
74
|
+
*
|
|
75
|
+
* Recognized carriers:
|
|
76
|
+
* - entry.receipt full EP-RECEIPT-v1 document
|
|
77
|
+
* - entry.signoff / payload.signoff WebAuthn device-signoff evidence,
|
|
78
|
+
* with the approver key at entry.approver_public_key or
|
|
79
|
+
* receipt.payload.approver_public_key
|
|
80
|
+
* - entry.quorum / payload.quorum full EP-QUORUM-v1 document
|
|
81
|
+
*/
|
|
82
|
+
function collectMaterial(e) {
|
|
83
|
+
const out: CarriedMaterial[] = [];
|
|
84
|
+
const receipt = e.receipt;
|
|
85
|
+
const payload = receipt && typeof receipt === 'object' && receipt.payload
|
|
86
|
+
&& typeof receipt.payload === 'object' ? receipt.payload : null;
|
|
87
|
+
if (receipt !== undefined && receipt !== null) out.push({ type: 'receipt', doc: receipt });
|
|
88
|
+
const signoff = e.signoff ?? payload?.signoff;
|
|
89
|
+
if (signoff !== undefined && signoff !== null) {
|
|
90
|
+
out.push({
|
|
91
|
+
type: 'signoff',
|
|
92
|
+
doc: signoff,
|
|
93
|
+
key: e.approver_public_key ?? payload?.approver_public_key ?? null,
|
|
94
|
+
});
|
|
95
|
+
}
|
|
96
|
+
const quorum = e.quorum ?? payload?.quorum;
|
|
97
|
+
if (quorum !== undefined && quorum !== null) out.push({ type: 'quorum', doc: quorum });
|
|
98
|
+
return out;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* Re-verify one carried payload. Returns null on pass, a NAMED reason on fail.
|
|
103
|
+
* Freshness is intentionally not re-enforced (maxAgeSec: 0): it was a runtime
|
|
104
|
+
* property of the decision moment and cannot be re-performed at audit time —
|
|
105
|
+
* what IS re-performed is the cryptography (signature, issuer trust, action
|
|
106
|
+
* binding, ceremony checks).
|
|
107
|
+
*/
|
|
108
|
+
function spkiFingerprint(value) {
|
|
109
|
+
try {
|
|
110
|
+
const key = crypto.createPublicKey({ key: Buffer.from(value, 'base64url'), format: 'der', type: 'spki' });
|
|
111
|
+
return crypto.createHash('sha256').update(key.export({ type: 'spki', format: 'der' })).digest('hex');
|
|
112
|
+
} catch { return null; }
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
function pinnedApproverEntry(approverKeys: Record<string, any> | null | undefined, approver: any, carriedKey: any = null) {
|
|
116
|
+
if (!approverKeys || typeof approverKeys !== 'object' || Array.isArray(approverKeys)) return null;
|
|
117
|
+
const candidates = Object.values(approverKeys).filter((entry) => entry && typeof entry === 'object'
|
|
118
|
+
&& entry.approver_id === approver && typeof entry.public_key === 'string');
|
|
119
|
+
if (carriedKey) {
|
|
120
|
+
const fingerprint = spkiFingerprint(carriedKey);
|
|
121
|
+
const matches = candidates.filter((entry) => fingerprint && spkiFingerprint(entry.public_key) === fingerprint);
|
|
122
|
+
return matches.length === 1 ? matches[0] : null;
|
|
123
|
+
}
|
|
124
|
+
return candidates.length === 1 ? candidates[0] : null;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
function reverifyMaterial(m, {
|
|
128
|
+
issuerKeys, action, rpId, allowedOrigins, approverKeys, quorumPolicy, quorumPolicies,
|
|
129
|
+
}) {
|
|
130
|
+
if (m.type === 'receipt') {
|
|
131
|
+
const r = verifyEmiliaReceipt(m.doc, {
|
|
132
|
+
trustedKeys: issuerKeys, allowInlineKey: false, action, maxAgeSec: 0,
|
|
133
|
+
});
|
|
134
|
+
return r.ok ? null : `receipt:${r.reason}`;
|
|
135
|
+
}
|
|
136
|
+
if (m.type === 'signoff') {
|
|
137
|
+
if (typeof rpId !== 'string' || !rpId || !Array.isArray(allowedOrigins) || allowedOrigins.length === 0) {
|
|
138
|
+
return 'signoff:relying_party_scope_unpinned';
|
|
139
|
+
}
|
|
140
|
+
const approver = m.doc?.context?.approver;
|
|
141
|
+
const entry = pinnedApproverEntry(approverKeys, approver, m.key);
|
|
142
|
+
if (!entry) return 'signoff:approver_key_unpinned_or_ambiguous';
|
|
143
|
+
const r = verifyWebAuthnSignoff(m.doc, entry.public_key, { rpId, allowedOrigins });
|
|
144
|
+
return r.valid ? null : `signoff:${r.error ? `error:${r.error}` : `checks_failed:${failingChecks(r.checks)}`}`;
|
|
145
|
+
}
|
|
146
|
+
if (m.type === 'quorum') {
|
|
147
|
+
if (typeof rpId !== 'string' || !rpId || !Array.isArray(allowedOrigins) || allowedOrigins.length === 0) {
|
|
148
|
+
return 'quorum:relying_party_scope_unpinned';
|
|
149
|
+
}
|
|
150
|
+
const pinnedPolicy = (quorumPolicies && typeof quorumPolicies === 'object' ? quorumPolicies[action] : null)
|
|
151
|
+
|| quorumPolicy;
|
|
152
|
+
const policy = validatePinnedQuorumPolicy(pinnedPolicy);
|
|
153
|
+
if (!policy.ok) return `quorum:${policy.reason}`;
|
|
154
|
+
const members = Array.isArray(m.doc?.members) ? m.doc.members.map((member) => {
|
|
155
|
+
const entry = pinnedApproverEntry(
|
|
156
|
+
approverKeys,
|
|
157
|
+
member?.signoff?.context?.approver,
|
|
158
|
+
member?.approver_public_key,
|
|
159
|
+
);
|
|
160
|
+
return entry ? { ...member, approver_public_key: entry.public_key } : null;
|
|
161
|
+
}) : [];
|
|
162
|
+
if (members.length === 0 || members.some((member) => !member)) return 'quorum:approver_key_unpinned_or_ambiguous';
|
|
163
|
+
const r = verifyQuorum({ ...m.doc, policy: policy.policy, members }, { rpId, allowedOrigins });
|
|
164
|
+
return r.valid ? null : `quorum:checks_failed:${failingChecks(r.checks)}`;
|
|
165
|
+
}
|
|
166
|
+
/* c8 ignore next */
|
|
167
|
+
return `unknown_material:${m.type}`; // unreachable; fail closed regardless
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/**
|
|
171
|
+
* Re-perform the evidence: rebuild the chain, re-verify carried cryptographic
|
|
172
|
+
* material, recompute the counts. Async because the chain is rebuilt through
|
|
173
|
+
* the evidence log's own (async) record().
|
|
174
|
+
*
|
|
175
|
+
* The entries MUST be the complete log from genesis (`evidence.all()` or a
|
|
176
|
+
* full export). A partial slice fails the chain (fail closed) because its
|
|
177
|
+
* first link cannot chain from 'genesis'.
|
|
178
|
+
*
|
|
179
|
+
* Chain method — drives evidence.js's REAL verify(), twice over:
|
|
180
|
+
* 1. every supplied entry body (its own seq/prev_hash included, its hash
|
|
181
|
+
* stripped) is re-recorded into a fresh createEvidenceLog(), which
|
|
182
|
+
* recomputes the canonical-JSON sha256 for that exact body; a recomputed
|
|
183
|
+
* hash that differs from the SUPPLIED hash is a tampered/forged entry —
|
|
184
|
+
* broken from that point;
|
|
185
|
+
* 2. the rebuilt log's own verify() then walks the whole chain, catching
|
|
186
|
+
* link-level attacks the per-entry recompute cannot (a removed entry, or
|
|
187
|
+
* an entry rewritten WITH a consistently recomputed hash — its successor's
|
|
188
|
+
* prev_hash no longer matches).
|
|
189
|
+
*
|
|
190
|
+
* @param {Array<object>} entries the full evidence log (evidence.all())
|
|
191
|
+
* @param {object} [o]
|
|
192
|
+
* @param {string[]} [o.issuerKeys=[]] pinned base64url SPKI issuer keys, sourced
|
|
193
|
+
* by the AUDITOR out of band — never from the entries themselves
|
|
194
|
+
* @param {number|function} [o.now=Date.now] clock for generated_at (pin for determinism)
|
|
195
|
+
* @param {object} [o.approverKeys={}] auditor-pinned identity-bound approver keys
|
|
196
|
+
* @param {string|null} [o.rpId] bind carried WebAuthn assertions to this relying-party id
|
|
197
|
+
* @param {string[]} [o.allowedOrigins=[]] exact accepted WebAuthn origins
|
|
198
|
+
* @param {object} [o.quorumPolicy] auditor-pinned global organizational quorum rule
|
|
199
|
+
* @param {object} [o.quorumPolicies] action_type -> auditor-pinned quorum rule
|
|
200
|
+
* @returns {Promise<object>} EP-GATE-REPERFORMANCE-v1 document:
|
|
201
|
+
* { chain: {ok, entries, head}, receipts: {reverified, failed, not_reverifiable},
|
|
202
|
+
* counts: {allows, denies, replays_blocked, by_action_type}, ... }
|
|
203
|
+
*/
|
|
204
|
+
export async function reperformEvidence(entries: any[] = [], {
|
|
205
|
+
issuerKeys = [],
|
|
206
|
+
approverKeys = {},
|
|
207
|
+
now = Date.now,
|
|
208
|
+
rpId = null,
|
|
209
|
+
allowedOrigins = [],
|
|
210
|
+
quorumPolicy = null,
|
|
211
|
+
quorumPolicies = {},
|
|
212
|
+
}: {
|
|
213
|
+
issuerKeys?: string[];
|
|
214
|
+
approverKeys?: Record<string, any>;
|
|
215
|
+
now?: number | (() => number);
|
|
216
|
+
rpId?: string | null;
|
|
217
|
+
allowedOrigins?: string[];
|
|
218
|
+
quorumPolicy?: any;
|
|
219
|
+
quorumPolicies?: Record<string, any>;
|
|
220
|
+
} = {}) {
|
|
221
|
+
if (!Array.isArray(entries)) {
|
|
222
|
+
throw new Error('reperform: entries must be an array (evidence.all() or a full export)');
|
|
223
|
+
}
|
|
224
|
+
if (!Array.isArray(issuerKeys)) {
|
|
225
|
+
throw new Error('reperform: issuerKeys must be an array of base64url SPKI issuer keys');
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
/* ------------------------------- chain -------------------------------- */
|
|
229
|
+
const rebuilt = createEvidenceLog();
|
|
230
|
+
let broken: { at: number | null; seq: number | null; reason: string | undefined } | null = null;
|
|
231
|
+
for (let i = 0; i < entries.length; i++) {
|
|
232
|
+
const e = entries[i];
|
|
233
|
+
if (!e || typeof e !== 'object' || Array.isArray(e)) { broken = { at: i, seq: null, reason: 'not_an_object' }; break; }
|
|
234
|
+
const seq = Number.isInteger(e.seq) ? e.seq : null;
|
|
235
|
+
if (seq === null) { broken = { at: i, seq: null, reason: 'missing_seq' }; break; }
|
|
236
|
+
if (typeof e.prev_hash !== 'string' || e.prev_hash.length === 0) { broken = { at: i, seq, reason: 'missing_prev_hash' }; break; }
|
|
237
|
+
if (typeof e.hash !== 'string' || e.hash.length === 0) { broken = { at: i, seq, reason: 'missing_hash' }; break; }
|
|
238
|
+
// Re-record the EXACT supplied body (own seq/prev_hash override the fresh
|
|
239
|
+
// log's defaults via the record() spread) so the recomputed hash is over
|
|
240
|
+
// byte-identical canonical material.
|
|
241
|
+
const { hash: suppliedHash, ...body } = e;
|
|
242
|
+
const rec = await rebuilt.record(body);
|
|
243
|
+
if (rec.hash !== suppliedHash) { broken = { at: i, seq, reason: 'hash_mismatch' }; break; }
|
|
244
|
+
}
|
|
245
|
+
// Drive the REAL evidence.js verifier over the rebuilt chain — the link walk
|
|
246
|
+
// (prev_hash continuity from 'genesis') is its judgment, not a reimplementation.
|
|
247
|
+
const rv = rebuilt.verify();
|
|
248
|
+
if (!broken && !rv.ok) broken = { at: rv.at ?? null, seq: rv.at ?? null, reason: rv.reason };
|
|
249
|
+
const chain = {
|
|
250
|
+
ok: !broken,
|
|
251
|
+
entries: entries.length,
|
|
252
|
+
// A broken chain vouches for NO head — even a valid prefix's.
|
|
253
|
+
head: !broken ? (rv.head ?? null) : null,
|
|
254
|
+
...(broken ? { broken_at: broken.at, broken_seq: broken.seq, reason: broken.reason } : {}),
|
|
255
|
+
};
|
|
256
|
+
|
|
257
|
+
/* ------------------- receipts + counts (from scratch) ------------------ */
|
|
258
|
+
const receipts: {
|
|
259
|
+
reverified: number;
|
|
260
|
+
failed: { hash: string | null; reason: string }[];
|
|
261
|
+
not_reverifiable: number;
|
|
262
|
+
no_receipt_presented: number;
|
|
263
|
+
} = { reverified: 0, failed: [], not_reverifiable: 0, no_receipt_presented: 0 };
|
|
264
|
+
const warnings: { index: number; reason: string }[] = [];
|
|
265
|
+
const byAction = Object.create(null);
|
|
266
|
+
let allows = 0;
|
|
267
|
+
let denies = 0;
|
|
268
|
+
let replaysBlocked = 0;
|
|
269
|
+
|
|
270
|
+
entries.forEach((e, index) => {
|
|
271
|
+
if (!e || typeof e !== 'object' || Array.isArray(e)) { warnings.push({ index, reason: 'not_an_object' }); return; }
|
|
272
|
+
if (typeof e.kind !== 'string' || e.kind.length === 0) { warnings.push({ index, reason: 'missing_kind' }); return; }
|
|
273
|
+
|
|
274
|
+
// Counts: guarded decisions only ('not_guarded' pass-throughs ran without
|
|
275
|
+
// the control; execution records are provenance, not enforcement). Fail
|
|
276
|
+
// closed: only a literal `true` counts as an allow.
|
|
277
|
+
if (e.kind === 'decision' && e.reason !== 'not_guarded') {
|
|
278
|
+
if (e.allow === true) allows += 1; else denies += 1;
|
|
279
|
+
if (e.reason === 'replay_refused') replaysBlocked += 1;
|
|
280
|
+
const action = typeof e.action === 'string' && e.action ? e.action : 'unknown';
|
|
281
|
+
byAction[action] = (byAction[action] || 0) + 1;
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
// Re-verification: any entry that carries material gets it re-verified;
|
|
285
|
+
// ALL carried payloads must verify for the entry to count as reverified —
|
|
286
|
+
// each failure is a named {hash, reason}, never absorbed.
|
|
287
|
+
const hash = typeof e.hash === 'string' && e.hash ? e.hash : null;
|
|
288
|
+
const material = collectMaterial(e);
|
|
289
|
+
if (material.length > 0) {
|
|
290
|
+
let allOk = true;
|
|
291
|
+
for (const m of material) {
|
|
292
|
+
const reason = reverifyMaterial(m, {
|
|
293
|
+
issuerKeys,
|
|
294
|
+
approverKeys,
|
|
295
|
+
action: typeof e.action === 'string' && e.action ? e.action : null,
|
|
296
|
+
rpId,
|
|
297
|
+
allowedOrigins,
|
|
298
|
+
quorumPolicy,
|
|
299
|
+
quorumPolicies,
|
|
300
|
+
});
|
|
301
|
+
if (reason) { allOk = false; receipts.failed.push({ hash, reason }); }
|
|
302
|
+
}
|
|
303
|
+
if (allOk) receipts.reverified += 1;
|
|
304
|
+
return;
|
|
305
|
+
}
|
|
306
|
+
// No material carried. A decision that REFERENCES a receipt (receipt_id
|
|
307
|
+
// recorded) but does not carry it cannot be re-performed — surfaced as
|
|
308
|
+
// not_reverifiable, never silently passed. A decision where no receipt was
|
|
309
|
+
// presented at all (pass-through, receipt_required refusal) has nothing to
|
|
310
|
+
// re-verify and is reported separately for transparency.
|
|
311
|
+
if (e.kind === 'decision') {
|
|
312
|
+
if (e.receipt_id !== null && e.receipt_id !== undefined) receipts.not_reverifiable += 1;
|
|
313
|
+
else receipts.no_receipt_presented += 1;
|
|
314
|
+
}
|
|
315
|
+
});
|
|
316
|
+
|
|
317
|
+
const nowMs = typeof now === 'function' ? now() : now;
|
|
318
|
+
|
|
319
|
+
return {
|
|
320
|
+
'@version': REPERFORMANCE_VERSION,
|
|
321
|
+
product: 'EMILIA Gate',
|
|
322
|
+
generated_at: new Date(nowMs).toISOString(),
|
|
323
|
+
honesty: {
|
|
324
|
+
reperforms:
|
|
325
|
+
'Independent recomputation from the supplied evidence entries: the hash chain is rebuilt '
|
|
326
|
+
+ 'and checked with the evidence log\'s own verifier, carried receipt/signoff/quorum material '
|
|
327
|
+
+ 'is cryptographically re-verified against auditor-pinned issuer and identity-bound approver keys, '
|
|
328
|
+
+ 'WebAuthn scope, and organizational quorum policy; decision counts '
|
|
329
|
+
+ 'are recomputed from scratch without reference to any report builder.',
|
|
330
|
+
does_not_establish: [
|
|
331
|
+
'Completeness of the log: a chain rewritten in full, or entries withheld before they were supplied, is not detectable from the entries alone. Anchor the head hash externally to close this.',
|
|
332
|
+
'Runtime freshness or one-time consumption: those were properties of the moment of decision and cannot be re-performed after the fact; cryptographic material is re-verified, runtime state is not.',
|
|
333
|
+
'Issuer and approver key custody, enrollment, or identity proofing, which remain external trust roots the auditor supplies out of band.',
|
|
334
|
+
'The business correctness or wisdom of any authorized action.',
|
|
335
|
+
],
|
|
336
|
+
status: 'Support for an audit re-performance procedure. This document does not conclude, opine, or certify; any conclusion is the auditor\'s.',
|
|
337
|
+
},
|
|
338
|
+
input: {
|
|
339
|
+
entries_supplied: entries.length,
|
|
340
|
+
issuer_keys_pinned: issuerKeys.length,
|
|
341
|
+
approver_keys_pinned: approverKeys && typeof approverKeys === 'object'
|
|
342
|
+
? Object.keys(approverKeys).length : 0,
|
|
343
|
+
relying_party_scope_pinned: typeof rpId === 'string' && rpId.length > 0
|
|
344
|
+
&& Array.isArray(allowedOrigins) && allowedOrigins.length > 0,
|
|
345
|
+
quorum_policies_pinned: (quorumPolicy ? 1 : 0)
|
|
346
|
+
+ (quorumPolicies && typeof quorumPolicies === 'object' ? Object.keys(quorumPolicies).length : 0),
|
|
347
|
+
expects: 'the complete evidence log from genesis (evidence.all() or a full export)',
|
|
348
|
+
},
|
|
349
|
+
chain,
|
|
350
|
+
receipts,
|
|
351
|
+
counts: {
|
|
352
|
+
allows,
|
|
353
|
+
denies,
|
|
354
|
+
replays_blocked: replaysBlocked,
|
|
355
|
+
by_action_type: sortedCounts(byAction),
|
|
356
|
+
},
|
|
357
|
+
integrity_warnings: warnings,
|
|
358
|
+
};
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
/** Extract the recomputed counts, fail closed on anything unrecognized. */
|
|
362
|
+
function countsFrom(recomputed) {
|
|
363
|
+
if (!recomputed || typeof recomputed !== 'object') {
|
|
364
|
+
throw new Error('compareToReported: recomputed must be the reperformEvidence result (or its counts)');
|
|
365
|
+
}
|
|
366
|
+
const c = recomputed['@version'] === REPERFORMANCE_VERSION ? recomputed.counts : recomputed;
|
|
367
|
+
if (!c || typeof c !== 'object') {
|
|
368
|
+
throw new Error('compareToReported: recomputed carries no counts');
|
|
369
|
+
}
|
|
370
|
+
for (const k of ['allows', 'denies', 'replays_blocked']) {
|
|
371
|
+
if (!Number.isFinite(c[k]) || c[k] < 0) {
|
|
372
|
+
throw new Error(`compareToReported: recomputed.${k} must be a finite number >= 0 — refusing to compare`);
|
|
373
|
+
}
|
|
374
|
+
}
|
|
375
|
+
const by = c.by_action_type;
|
|
376
|
+
return { ...c, by_action_type: by && typeof by === 'object' && !Array.isArray(by) ? by : {} };
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
/**
|
|
380
|
+
* Diff recomputed counts against a reported pack — the auditor's tie-out.
|
|
381
|
+
*
|
|
382
|
+
* Accepts an EP-GATE-USAGE-v1 pack (meterUsage output or the signed-ready
|
|
383
|
+
* buildUsageStatement body) or an EP-GATE-UNDERWRITER-ATTESTATION-v1 pack.
|
|
384
|
+
* Only the OVERLAPPING NUMERIC fields are compared; an unknown pack @version
|
|
385
|
+
* is refused (fail closed), never fuzzily matched. A reported field that is
|
|
386
|
+
* missing or non-numeric is itself a named drift — a stripped pack can never
|
|
387
|
+
* silently match.
|
|
388
|
+
*
|
|
389
|
+
* The comparison is meaningful only when the recomputation ran over exactly
|
|
390
|
+
* the entries the reported pack was built from (same slice, same window);
|
|
391
|
+
* scoping the slice is the auditor's procedure, not this function's.
|
|
392
|
+
*
|
|
393
|
+
* @param {object} recomputed reperformEvidence() result (or its .counts)
|
|
394
|
+
* @param {object} reportedPack usage or underwriter pack
|
|
395
|
+
* @returns {{match: boolean, pack_version: string, drift: Array<{field, reported, recomputed}>}}
|
|
396
|
+
*/
|
|
397
|
+
export function compareToReported(recomputed, reportedPack) {
|
|
398
|
+
const counts = countsFrom(recomputed);
|
|
399
|
+
if (!reportedPack || typeof reportedPack !== 'object') {
|
|
400
|
+
throw new Error('compareToReported: reportedPack must be an object');
|
|
401
|
+
}
|
|
402
|
+
const version = reportedPack['@version'];
|
|
403
|
+
const drift: { field: string; reported: any; recomputed: any }[] = [];
|
|
404
|
+
const check = (field, reported, rec) => {
|
|
405
|
+
if (!Number.isFinite(reported) || reported !== rec) {
|
|
406
|
+
drift.push({ field, reported: reported === undefined ? null : reported, recomputed: rec });
|
|
407
|
+
}
|
|
408
|
+
};
|
|
409
|
+
|
|
410
|
+
if (version === USAGE_PACK_VERSION) {
|
|
411
|
+
check('allows', reportedPack.allows, counts.allows);
|
|
412
|
+
check('denies', reportedPack.denies, counts.denies);
|
|
413
|
+
check('replays_blocked', reportedPack.replays_blocked, counts.replays_blocked);
|
|
414
|
+
check('protected_actions', reportedPack.protected_actions, counts.allows + counts.denies);
|
|
415
|
+
const reportedBy = reportedPack.by_action_type;
|
|
416
|
+
const byOk = reportedBy && typeof reportedBy === 'object' && !Array.isArray(reportedBy);
|
|
417
|
+
const keys = new Set([
|
|
418
|
+
...Object.keys(counts.by_action_type),
|
|
419
|
+
...(byOk ? Object.keys(reportedBy) : []),
|
|
420
|
+
]);
|
|
421
|
+
for (const k of [...keys].sort()) {
|
|
422
|
+
// An absent key in a well-formed histogram means zero occurrences.
|
|
423
|
+
const rep = byOk && reportedBy[k] !== undefined ? reportedBy[k] : 0;
|
|
424
|
+
check(`by_action_type.${k}`, rep, counts.by_action_type[k] ?? 0);
|
|
425
|
+
}
|
|
426
|
+
} else if (version === UNDERWRITER_PACK_VERSION) {
|
|
427
|
+
check('volume.guarded_decisions', reportedPack.volume?.guarded_decisions, counts.allows + counts.denies);
|
|
428
|
+
check('volume.allowed', reportedPack.volume?.allowed, counts.allows);
|
|
429
|
+
check('volume.denied', reportedPack.volume?.denied, counts.denies);
|
|
430
|
+
check('denials.total', reportedPack.denials?.total, counts.denies);
|
|
431
|
+
check('replay.attempts_blocked', reportedPack.replay?.attempts_blocked, counts.replays_blocked);
|
|
432
|
+
} else {
|
|
433
|
+
throw new Error(
|
|
434
|
+
`compareToReported: unknown pack @version ${JSON.stringify(version ?? null)} — refusing to compare (fail closed)`,
|
|
435
|
+
);
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
return { match: drift.length === 0, pack_version: version, drift };
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
export default { REPERFORMANCE_VERSION, reperformEvidence, compareToReported };
|