@emilia-protocol/gate 0.10.0 → 0.13.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 +91 -0
- package/README.md +272 -0
- package/action-control-manifest.js +3 -268
- package/action-escrow-custodian.js +4 -0
- package/action-escrow-evidence.js +4 -0
- package/action-escrow-package.js +4 -0
- package/action-escrow-postgres.js +4 -0
- package/action-escrow-state.js +4 -0
- package/action-escrow-verifiers.js +4 -0
- package/action-escrow.js +4 -0
- 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 +1002 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +1736 -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/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 +47 -4
- 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 +2122 -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/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 -85
- 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,1105 @@
|
|
|
1
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Receipt-program execution profile for EMILIA Gate.
|
|
5
|
+
*
|
|
6
|
+
* This module is intentionally a composition kernel, not a second policy
|
|
7
|
+
* engine or ledger. Gate remains the authorization/effect boundary, the
|
|
8
|
+
* capability store remains the atomic budget/replay authority, CAID remains
|
|
9
|
+
* material-action identity, and the Gate evidence log remains the execution
|
|
10
|
+
* history. The kernel freezes those inputs into one signed, offline-checkable
|
|
11
|
+
* execution certificate.
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import {
|
|
15
|
+
createHash,
|
|
16
|
+
createPrivateKey,
|
|
17
|
+
createPublicKey,
|
|
18
|
+
sign,
|
|
19
|
+
verify,
|
|
20
|
+
} from 'node:crypto';
|
|
21
|
+
|
|
22
|
+
import { canonicalize } from './execution-binding.js';
|
|
23
|
+
import { canonicalEvidenceJson, verifyEvidenceRecord } from './evidence.js';
|
|
24
|
+
|
|
25
|
+
export const RECEIPT_PROGRAM_VERSION = 'EP-RECEIPT-PROGRAM-v1';
|
|
26
|
+
export const RECEIPT_PROGRAM_CERTIFICATE_VERSION = 'EP-RECEIPT-PROGRAM-CERTIFICATE-v1';
|
|
27
|
+
export const RECEIPT_PROGRAM_SIGNATURE_ALGORITHM = 'Ed25519';
|
|
28
|
+
|
|
29
|
+
const CAID_RE = /^caid:1:[a-z][a-z0-9.-]*\.[1-9][0-9]*:jcs-sha256:[A-Za-z0-9_-]{43}$/;
|
|
30
|
+
const SHA256_RE = /^sha256:[0-9a-f]{64}$/;
|
|
31
|
+
const MAX_ID_BYTES = 256;
|
|
32
|
+
const MAX_CANONICAL_BYTES = 1024 * 1024;
|
|
33
|
+
const MAX_PROGRAM_BYTES = 512 * 1024;
|
|
34
|
+
const MAX_RESULT_BYTES = 128 * 1024;
|
|
35
|
+
const MAX_CERTIFICATE_CORE_BYTES = 768 * 1024;
|
|
36
|
+
const FORBIDDEN_RUNTIME_TRUST_FIELDS = new Set([
|
|
37
|
+
'allowEphemeralState',
|
|
38
|
+
'certificatePrivateKey',
|
|
39
|
+
'certificateSigner',
|
|
40
|
+
'effectTimeoutMs',
|
|
41
|
+
'gate',
|
|
42
|
+
'now',
|
|
43
|
+
'operationIdField',
|
|
44
|
+
'projectResult',
|
|
45
|
+
'resolveCaid',
|
|
46
|
+
'trustedCertificateKeys',
|
|
47
|
+
]);
|
|
48
|
+
|
|
49
|
+
function sha256(value: any): string {
|
|
50
|
+
return createHash('sha256').update(value).digest('hex');
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function canonicalDigest(value: any): string {
|
|
54
|
+
return `sha256:${sha256(Buffer.from(canonicalize(value), 'utf8'))}`;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function isRecord(value: any): boolean {
|
|
58
|
+
if (value === null || typeof value !== 'object' || Array.isArray(value)) return false;
|
|
59
|
+
const prototype = Object.getPrototypeOf(value);
|
|
60
|
+
return prototype === Object.prototype || prototype === null;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function isDataRecord(value: any): boolean {
|
|
64
|
+
if (!isRecord(value)) return false;
|
|
65
|
+
return Reflect.ownKeys(value).every((key) => {
|
|
66
|
+
if (typeof key !== 'string') return false;
|
|
67
|
+
const descriptor = Object.getOwnPropertyDescriptor(value, key);
|
|
68
|
+
return descriptor?.enumerable === true && Object.hasOwn(descriptor, 'value');
|
|
69
|
+
});
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function hasExactKeys(value: any, keys: readonly string[]): boolean {
|
|
73
|
+
if (!isDataRecord(value)) return false;
|
|
74
|
+
const actual = Object.keys(value).sort();
|
|
75
|
+
const expected = [...keys].sort();
|
|
76
|
+
return actual.length === expected.length
|
|
77
|
+
&& actual.every((key, index) => key === expected[index]);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function validInstant(value: any): boolean {
|
|
81
|
+
if (typeof value !== 'string') return false;
|
|
82
|
+
const timestamp = Date.parse(value);
|
|
83
|
+
return Number.isFinite(timestamp) && new Date(timestamp).toISOString() === value;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function deepFreeze(value: any): any {
|
|
87
|
+
if (!value || typeof value !== 'object') return value;
|
|
88
|
+
const stack = [value];
|
|
89
|
+
const seen = new WeakSet();
|
|
90
|
+
while (stack.length) {
|
|
91
|
+
const current = stack.pop();
|
|
92
|
+
if (!current || typeof current !== 'object' || seen.has(current)) continue;
|
|
93
|
+
seen.add(current);
|
|
94
|
+
for (const child of Object.values(current)) stack.push(child);
|
|
95
|
+
Object.freeze(current);
|
|
96
|
+
}
|
|
97
|
+
return value;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
function cloneCanonical(value: any, label: string, freeze = true, maxBytes = MAX_CANONICAL_BYTES): any {
|
|
101
|
+
let snapshot;
|
|
102
|
+
try {
|
|
103
|
+
// Validate the caller's graph before cloning. The canonicalizer inspects
|
|
104
|
+
// descriptors without invoking accessors and bounds depth, nodes, strings,
|
|
105
|
+
// aliases, arrays, numbers, and Unicode.
|
|
106
|
+
const canonical = canonicalize(value);
|
|
107
|
+
snapshot = structuredClone(value);
|
|
108
|
+
if (Buffer.byteLength(canonical, 'utf8') > maxBytes) {
|
|
109
|
+
throw new Error('canonical value exceeds byte limit');
|
|
110
|
+
}
|
|
111
|
+
} catch {
|
|
112
|
+
throw new TypeError(`${label} must be bounded canonical JSON`);
|
|
113
|
+
}
|
|
114
|
+
return freeze ? deepFreeze(snapshot) : snapshot;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
function placeholderId(value: any): string {
|
|
118
|
+
return typeof value === 'string' && value.length > 0
|
|
119
|
+
&& Buffer.byteLength(value, 'utf8') <= MAX_ID_BYTES
|
|
120
|
+
? value : 'invalid';
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
function boundedId(value: any, label: string): string {
|
|
124
|
+
if (typeof value !== 'string' || value.length === 0
|
|
125
|
+
|| Buffer.byteLength(value, 'utf8') > MAX_ID_BYTES) {
|
|
126
|
+
throw new TypeError(`${label} must be a non-empty string of at most ${MAX_ID_BYTES} bytes`);
|
|
127
|
+
}
|
|
128
|
+
return value;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
function keyObject(value: any, label: string): any {
|
|
132
|
+
try {
|
|
133
|
+
const key = value?.type === 'private' ? value : createPrivateKey(value);
|
|
134
|
+
if (key.asymmetricKeyType !== 'ed25519') throw new Error('wrong key type');
|
|
135
|
+
return key;
|
|
136
|
+
} catch {
|
|
137
|
+
throw new TypeError(`${label} must be an Ed25519 private key`);
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
function publicKeyB64u(privateKey: any): string {
|
|
142
|
+
return createPublicKey(privateKey).export({ type: 'spki', format: 'der' }).toString('base64url');
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
function publicKeyObject(publicKey: any): any {
|
|
146
|
+
const bytes = Buffer.from(publicKey, 'base64url');
|
|
147
|
+
if (bytes.toString('base64url') !== publicKey) throw new Error('non-canonical public key');
|
|
148
|
+
const key = createPublicKey({
|
|
149
|
+
key: Buffer.from(publicKey, 'base64url'),
|
|
150
|
+
type: 'spki',
|
|
151
|
+
format: 'der',
|
|
152
|
+
});
|
|
153
|
+
if (key.asymmetricKeyType !== 'ed25519') throw new Error('wrong public key type');
|
|
154
|
+
return key;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
function canonicalSignature(value: any): { bytes: Buffer; encoded: string } {
|
|
158
|
+
const bytes = Buffer.isBuffer(value)
|
|
159
|
+
? Buffer.from(value)
|
|
160
|
+
: typeof value === 'string' ? Buffer.from(value, 'base64url') : null;
|
|
161
|
+
if (!bytes || bytes.length !== 64) throw new Error('certificate signer returned an invalid signature');
|
|
162
|
+
const encoded = bytes.toString('base64url');
|
|
163
|
+
if (typeof value === 'string' && encoded !== value) {
|
|
164
|
+
throw new Error('certificate signer returned a non-canonical signature');
|
|
165
|
+
}
|
|
166
|
+
return { bytes, encoded };
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
function configureCertificateSigner({
|
|
170
|
+
certificatePrivateKey,
|
|
171
|
+
certificateSigner,
|
|
172
|
+
allowEphemeralState,
|
|
173
|
+
}: any): any {
|
|
174
|
+
if (certificateSigner !== undefined && certificatePrivateKey !== undefined) {
|
|
175
|
+
throw new TypeError('configure exactly one certificate signer');
|
|
176
|
+
}
|
|
177
|
+
if (certificateSigner !== undefined) {
|
|
178
|
+
if (!isDataRecord(certificateSigner)) {
|
|
179
|
+
throw new TypeError('certificateSigner must be a data object');
|
|
180
|
+
}
|
|
181
|
+
const publicKey = certificateSigner.publicKey ?? certificateSigner.publicKeySpkiB64u;
|
|
182
|
+
if (typeof publicKey !== 'string' || typeof certificateSigner.sign !== 'function'
|
|
183
|
+
|| typeof certificateSigner.keyId !== 'string') {
|
|
184
|
+
throw new TypeError('certificateSigner requires keyId, publicKey, and async sign(bytes)');
|
|
185
|
+
}
|
|
186
|
+
publicKeyObject(publicKey);
|
|
187
|
+
if (!allowEphemeralState && !['kms', 'hsm'].includes(certificateSigner.custody)) {
|
|
188
|
+
throw new Error('receipt program production certificate signer custody must be kms or hsm');
|
|
189
|
+
}
|
|
190
|
+
return Object.freeze({
|
|
191
|
+
keyId: boundedId(certificateSigner.keyId, 'certificateSigner.keyId'),
|
|
192
|
+
publicKey,
|
|
193
|
+
sign: certificateSigner.sign,
|
|
194
|
+
});
|
|
195
|
+
}
|
|
196
|
+
if (!allowEphemeralState) {
|
|
197
|
+
throw new Error('receipt program production mode requires an external KMS/HSM certificate signer');
|
|
198
|
+
}
|
|
199
|
+
const privateKey = keyObject(certificatePrivateKey, 'certificatePrivateKey');
|
|
200
|
+
return Object.freeze({
|
|
201
|
+
keyId: 'local-dev',
|
|
202
|
+
publicKey: publicKeyB64u(privateKey),
|
|
203
|
+
sign: async (bytes: Buffer) => sign(null, bytes, privateKey),
|
|
204
|
+
});
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
function instant(now: any): string {
|
|
208
|
+
const value = typeof now === 'function' ? now() : now;
|
|
209
|
+
if (!Number.isFinite(Number(value))) throw new TypeError('receipt program clock must return a finite value');
|
|
210
|
+
return new Date(Number(value)).toISOString();
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
function valueAtPath(value: any, path: string): any {
|
|
214
|
+
let current = value;
|
|
215
|
+
for (const segment of path.split('.')) {
|
|
216
|
+
if (!isRecord(current) || !Object.prototype.hasOwnProperty.call(current, segment)) return undefined;
|
|
217
|
+
current = current[segment];
|
|
218
|
+
}
|
|
219
|
+
return current;
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
function normalizeResolvedCaid(value: any): string | null {
|
|
223
|
+
if (typeof value === 'string') return value;
|
|
224
|
+
if (isRecord(value) && value.ok === true && typeof value.caid === 'string') return value.caid;
|
|
225
|
+
return null;
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
function opcodeSteps(opcodes: readonly string[]): any {
|
|
229
|
+
return opcodes.map((opcode, sequence) => Object.freeze({ sequence, opcode }));
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
function operationFromExecution(record: any): any {
|
|
233
|
+
return record?.detail?.capability?.operation_id ?? null;
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
function evidenceReference(record: any): any {
|
|
237
|
+
if (!record) return null;
|
|
238
|
+
return deepFreeze({
|
|
239
|
+
seq: Number.isSafeInteger(record.seq) ? record.seq : null,
|
|
240
|
+
record_id: typeof record.record_id === 'string' ? record.record_id : null,
|
|
241
|
+
hash: typeof record.hash === 'string' ? record.hash : null,
|
|
242
|
+
prev_hash: typeof record.prev_hash === 'string' ? record.prev_hash : null,
|
|
243
|
+
kind: typeof record.kind === 'string' ? record.kind : null,
|
|
244
|
+
allow: typeof record.allow === 'boolean' ? record.allow : null,
|
|
245
|
+
outcome: typeof record.outcome === 'string' ? record.outcome : null,
|
|
246
|
+
authorizes_decision: typeof record.authorizes_decision === 'string'
|
|
247
|
+
? record.authorizes_decision : null,
|
|
248
|
+
observed_action_hash: typeof record.observed_action_hash === 'string'
|
|
249
|
+
? record.observed_action_hash : null,
|
|
250
|
+
operation_id: operationFromExecution(record),
|
|
251
|
+
});
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
function certificateCore({
|
|
255
|
+
context,
|
|
256
|
+
program,
|
|
257
|
+
programDigest,
|
|
258
|
+
outcome,
|
|
259
|
+
reason,
|
|
260
|
+
result,
|
|
261
|
+
authorizationRef,
|
|
262
|
+
executionRef,
|
|
263
|
+
steps,
|
|
264
|
+
startedAt,
|
|
265
|
+
completedAt,
|
|
266
|
+
}: any): any {
|
|
267
|
+
return {
|
|
268
|
+
'@version': RECEIPT_PROGRAM_CERTIFICATE_VERSION,
|
|
269
|
+
context,
|
|
270
|
+
program,
|
|
271
|
+
program_digest: programDigest,
|
|
272
|
+
outcome,
|
|
273
|
+
reason,
|
|
274
|
+
result,
|
|
275
|
+
result_digest: result === null ? null : canonicalDigest(result),
|
|
276
|
+
authorization_ref: authorizationRef,
|
|
277
|
+
execution_ref: executionRef,
|
|
278
|
+
steps,
|
|
279
|
+
started_at: startedAt,
|
|
280
|
+
completed_at: completedAt,
|
|
281
|
+
};
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
async function signCertificate(core: any, signer: any): Promise<any> {
|
|
285
|
+
if (Buffer.byteLength(canonicalize(core), 'utf8') > MAX_CERTIFICATE_CORE_BYTES) {
|
|
286
|
+
throw new Error('receipt program certificate exceeds byte limit');
|
|
287
|
+
}
|
|
288
|
+
const stateRoot = canonicalDigest(core);
|
|
289
|
+
const signed = { ...core, state_root: stateRoot };
|
|
290
|
+
const signedBytes = Buffer.from(canonicalize(signed), 'utf8');
|
|
291
|
+
const signature = canonicalSignature(await signer.sign(signedBytes));
|
|
292
|
+
if (!verify(null, signedBytes, publicKeyObject(signer.publicKey), signature.bytes)) {
|
|
293
|
+
throw new Error('certificate signer returned a signature that does not verify');
|
|
294
|
+
}
|
|
295
|
+
return deepFreeze({
|
|
296
|
+
...signed,
|
|
297
|
+
signature: {
|
|
298
|
+
algorithm: RECEIPT_PROGRAM_SIGNATURE_ALGORITHM,
|
|
299
|
+
public_key: signer.publicKey,
|
|
300
|
+
value: signature.encoded,
|
|
301
|
+
},
|
|
302
|
+
});
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
function makeResult(certificate: any): any {
|
|
306
|
+
return Object.freeze({
|
|
307
|
+
ok: certificate.outcome === 'executed',
|
|
308
|
+
outcome: certificate.outcome,
|
|
309
|
+
reason: certificate.reason,
|
|
310
|
+
result: certificate.result,
|
|
311
|
+
certificate,
|
|
312
|
+
});
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
function failReason(error: any, fallback: string): string {
|
|
316
|
+
return typeof error?.message === 'string' && error.message.startsWith('receipt_program:')
|
|
317
|
+
? error.message.slice('receipt_program:'.length)
|
|
318
|
+
: fallback;
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
/**
|
|
322
|
+
* Build a receipt-program kernel over an already configured Gate.
|
|
323
|
+
* Trust configuration is constructor-pinned and cannot be supplied per run.
|
|
324
|
+
*
|
|
325
|
+
* options.gate: configured EMILIA Gate
|
|
326
|
+
* options.resolveCaid: synchronous pinned CAID resolver, (action) => string|object
|
|
327
|
+
* options.operationIdField: dot-path to the stable operation id in observed action
|
|
328
|
+
* options.certificatePrivateKey: test/demo-only Ed25519 operator key
|
|
329
|
+
* options.certificateSigner: external KMS/HSM signer
|
|
330
|
+
* options.certificateContext: pinned issuer, tenant, environment, audience, and key id
|
|
331
|
+
* options.projectResult: pinned disclosure projection, (result) => any|Promise<any>
|
|
332
|
+
* options.effectTimeoutMs: provider deadline in milliseconds (default 30000)
|
|
333
|
+
* options.allowEphemeralState: explicit test/demo opt-in (default false)
|
|
334
|
+
* options.now: number|(() => number), default Date.now
|
|
335
|
+
*/
|
|
336
|
+
export function createReceiptProgramKernel({
|
|
337
|
+
gate,
|
|
338
|
+
resolveCaid,
|
|
339
|
+
operationIdField,
|
|
340
|
+
certificatePrivateKey,
|
|
341
|
+
certificateSigner,
|
|
342
|
+
certificateContext,
|
|
343
|
+
projectResult = null,
|
|
344
|
+
effectTimeoutMs = 30_000,
|
|
345
|
+
allowEphemeralState = false,
|
|
346
|
+
now = Date.now,
|
|
347
|
+
}: any = {}) {
|
|
348
|
+
if (!gate || typeof gate.run !== 'function' || !gate.evidence) {
|
|
349
|
+
throw new TypeError('createReceiptProgramKernel requires a configured Gate with an evidence log');
|
|
350
|
+
}
|
|
351
|
+
if (typeof resolveCaid !== 'function') {
|
|
352
|
+
throw new TypeError('createReceiptProgramKernel requires a pinned synchronous CAID resolver');
|
|
353
|
+
}
|
|
354
|
+
boundedId(operationIdField, 'operationIdField');
|
|
355
|
+
if (!allowEphemeralState && (gate.evidence.durable !== true || gate.evidence.strict !== true
|
|
356
|
+
|| gate.evidence.forkAware !== true || gate.evidence.atomicAppend !== true
|
|
357
|
+
|| gate.capabilityStore?.durable !== true)) {
|
|
358
|
+
throw new Error('receipt program production mode requires a durable atomic evidence log and durable capability store');
|
|
359
|
+
}
|
|
360
|
+
if (!Number.isSafeInteger(effectTimeoutMs) || effectTimeoutMs < 1 || effectTimeoutMs > 600_000) {
|
|
361
|
+
throw new TypeError('effectTimeoutMs must be an integer from 1 to 600000');
|
|
362
|
+
}
|
|
363
|
+
if (!allowEphemeralState && typeof projectResult !== 'function') {
|
|
364
|
+
throw new Error('receipt program production mode requires a pinned projectResult function');
|
|
365
|
+
}
|
|
366
|
+
if (projectResult !== null && typeof projectResult !== 'function') {
|
|
367
|
+
throw new TypeError('projectResult must be a function');
|
|
368
|
+
}
|
|
369
|
+
const signer = configureCertificateSigner({
|
|
370
|
+
certificatePrivateKey,
|
|
371
|
+
certificateSigner,
|
|
372
|
+
allowEphemeralState,
|
|
373
|
+
});
|
|
374
|
+
const context = cloneCanonical(certificateContext, 'certificateContext');
|
|
375
|
+
if (!hasExactKeys(context, ['issuer', 'tenant', 'environment', 'audience', 'key_id'])) {
|
|
376
|
+
throw new TypeError('certificateContext must contain exactly issuer, tenant, environment, audience, and key_id');
|
|
377
|
+
}
|
|
378
|
+
for (const field of ['issuer', 'tenant', 'environment', 'audience', 'key_id']) {
|
|
379
|
+
boundedId(context[field], `certificateContext.${field}`);
|
|
380
|
+
}
|
|
381
|
+
if (context.key_id !== signer.keyId) {
|
|
382
|
+
throw new Error('certificateContext.key_id must equal the configured signer keyId');
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
async function issueCertificate(input: any): Promise<any> {
|
|
386
|
+
const core = certificateCore({ ...input, context });
|
|
387
|
+
let certificate;
|
|
388
|
+
try {
|
|
389
|
+
certificate = await signCertificate(core, signer);
|
|
390
|
+
} catch {
|
|
391
|
+
return Object.freeze({
|
|
392
|
+
ok: false,
|
|
393
|
+
outcome: input.outcome,
|
|
394
|
+
reason: 'certificate_signing_failed',
|
|
395
|
+
result: input.result,
|
|
396
|
+
certificate: null,
|
|
397
|
+
certificate_evidence: null,
|
|
398
|
+
});
|
|
399
|
+
}
|
|
400
|
+
try {
|
|
401
|
+
const certificateEvidence = await gate.evidence.record({
|
|
402
|
+
kind: 'receipt_program_certificate',
|
|
403
|
+
program_digest: certificate.program_digest,
|
|
404
|
+
operation_id: certificate.program.operation_id,
|
|
405
|
+
outcome: certificate.outcome,
|
|
406
|
+
state_root: certificate.state_root,
|
|
407
|
+
certificate,
|
|
408
|
+
});
|
|
409
|
+
return Object.freeze({
|
|
410
|
+
...makeResult(certificate),
|
|
411
|
+
certificate_evidence: deepFreeze(structuredClone(certificateEvidence)),
|
|
412
|
+
});
|
|
413
|
+
} catch {
|
|
414
|
+
return Object.freeze({
|
|
415
|
+
ok: false,
|
|
416
|
+
outcome: input.outcome,
|
|
417
|
+
reason: 'certificate_persistence_failed',
|
|
418
|
+
result: input.result,
|
|
419
|
+
certificate,
|
|
420
|
+
certificate_evidence: null,
|
|
421
|
+
});
|
|
422
|
+
}
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
async function refuseEarly({ program, programDigest, reason, startedAt, matched = false }: any): Promise<any> {
|
|
426
|
+
return issueCertificate({
|
|
427
|
+
program,
|
|
428
|
+
programDigest,
|
|
429
|
+
outcome: 'refused',
|
|
430
|
+
reason,
|
|
431
|
+
result: null,
|
|
432
|
+
authorizationRef: null,
|
|
433
|
+
executionRef: null,
|
|
434
|
+
steps: opcodeSteps(matched
|
|
435
|
+
? ['RECEIPT', 'MATCH', 'REFUSE', 'CERTIFY']
|
|
436
|
+
: ['RECEIPT', 'REFUSE', 'CERTIFY']),
|
|
437
|
+
startedAt,
|
|
438
|
+
completedAt: instant(now),
|
|
439
|
+
});
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
return Object.freeze({
|
|
443
|
+
version: RECEIPT_PROGRAM_VERSION,
|
|
444
|
+
signer_public_key: signer.publicKey,
|
|
445
|
+
certificate_context: context,
|
|
446
|
+
|
|
447
|
+
/**
|
|
448
|
+
* Execute one consequential receipt instruction through Gate.
|
|
449
|
+
* The effect MUST return a bounded canonical-JSON evidence projection, not
|
|
450
|
+
* a raw provider object. A projection failure occurs after provider entry
|
|
451
|
+
* and is therefore committed as indeterminate.
|
|
452
|
+
*/
|
|
453
|
+
async run(request: any = {}, effect: any): Promise<any> {
|
|
454
|
+
const startedAt = instant(now);
|
|
455
|
+
if (!isRecord(request) || typeof effect !== 'function') {
|
|
456
|
+
throw new TypeError('receipt program run requires an object request and effect function');
|
|
457
|
+
}
|
|
458
|
+
const capabilityDescriptor = Object.getOwnPropertyDescriptor(request, 'capability');
|
|
459
|
+
if (!isDataRecord(request) || !capabilityDescriptor
|
|
460
|
+
|| !isDataRecord(capabilityDescriptor.value)) {
|
|
461
|
+
const placeholder = deepFreeze({
|
|
462
|
+
'@version': RECEIPT_PROGRAM_VERSION,
|
|
463
|
+
program_id: 'invalid',
|
|
464
|
+
instruction_id: 'invalid',
|
|
465
|
+
operation_id: 'invalid',
|
|
466
|
+
caid: 'invalid',
|
|
467
|
+
action_digest: null,
|
|
468
|
+
capability_receipt_digest: null,
|
|
469
|
+
selector: {},
|
|
470
|
+
observed_action: {},
|
|
471
|
+
});
|
|
472
|
+
return refuseEarly({
|
|
473
|
+
program: placeholder,
|
|
474
|
+
programDigest: canonicalDigest(placeholder),
|
|
475
|
+
reason: 'program_invalid',
|
|
476
|
+
startedAt,
|
|
477
|
+
});
|
|
478
|
+
}
|
|
479
|
+
for (const field of FORBIDDEN_RUNTIME_TRUST_FIELDS) {
|
|
480
|
+
if (Object.prototype.hasOwnProperty.call(request, field)) {
|
|
481
|
+
const placeholder = deepFreeze({
|
|
482
|
+
'@version': RECEIPT_PROGRAM_VERSION,
|
|
483
|
+
program_id: placeholderId(request.programId),
|
|
484
|
+
instruction_id: placeholderId(request.instructionId),
|
|
485
|
+
operation_id: placeholderId(request.capability?.operationId),
|
|
486
|
+
caid: placeholderId(request.caid),
|
|
487
|
+
action_digest: null,
|
|
488
|
+
capability_receipt_digest: null,
|
|
489
|
+
selector: {},
|
|
490
|
+
observed_action: {},
|
|
491
|
+
});
|
|
492
|
+
return refuseEarly({
|
|
493
|
+
program: placeholder,
|
|
494
|
+
programDigest: canonicalDigest(placeholder),
|
|
495
|
+
reason: 'runtime_trust_configuration_refused',
|
|
496
|
+
startedAt,
|
|
497
|
+
});
|
|
498
|
+
}
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
let program: any;
|
|
502
|
+
let programDigest: any;
|
|
503
|
+
let operationId: any;
|
|
504
|
+
let actionDigest: any;
|
|
505
|
+
let executionCapability: any;
|
|
506
|
+
let executionReceipt: any = null;
|
|
507
|
+
try {
|
|
508
|
+
const programId = boundedId(request.programId, 'programId');
|
|
509
|
+
const instructionId = boundedId(request.instructionId, 'instructionId');
|
|
510
|
+
operationId = boundedId(request.capability?.operationId, 'capability.operationId');
|
|
511
|
+
if (!CAID_RE.test(request.caid)) throw new Error('receipt_program:caid_invalid');
|
|
512
|
+
const observedAction = cloneCanonical(request.observedAction, 'observedAction');
|
|
513
|
+
const selector = cloneCanonical(request.selector ?? {}, 'selector');
|
|
514
|
+
const capabilityReceipt = cloneCanonical(request.capability?.capabilityReceipt, 'capabilityReceipt');
|
|
515
|
+
const capabilityProjection = cloneCanonical(request.capability?.action, 'capability.action');
|
|
516
|
+
const secret = Buffer.isBuffer(request.capability?.secret)
|
|
517
|
+
? Buffer.from(request.capability.secret)
|
|
518
|
+
: request.capability?.secret;
|
|
519
|
+
const shares = request.capability?.shares === undefined
|
|
520
|
+
? undefined : cloneCanonical(request.capability.shares, 'capability.shares');
|
|
521
|
+
executionCapability = Object.freeze({
|
|
522
|
+
capabilityReceipt: structuredClone(capabilityReceipt),
|
|
523
|
+
...(shares === undefined ? { secret } : { shares: structuredClone(shares) }),
|
|
524
|
+
action: structuredClone(capabilityProjection),
|
|
525
|
+
operationId,
|
|
526
|
+
});
|
|
527
|
+
executionReceipt = request.receipt === undefined || request.receipt === null
|
|
528
|
+
? null : cloneCanonical(request.receipt, 'receipt');
|
|
529
|
+
const actionOperationId = valueAtPath(observedAction, operationIdField);
|
|
530
|
+
actionDigest = canonicalDigest(observedAction);
|
|
531
|
+
program = deepFreeze({
|
|
532
|
+
'@version': RECEIPT_PROGRAM_VERSION,
|
|
533
|
+
program_id: programId,
|
|
534
|
+
instruction_id: instructionId,
|
|
535
|
+
operation_id: operationId,
|
|
536
|
+
operation_id_field: operationIdField,
|
|
537
|
+
caid: request.caid,
|
|
538
|
+
action_digest: actionDigest,
|
|
539
|
+
capability_receipt_digest: canonicalDigest(capabilityReceipt),
|
|
540
|
+
capability_projection: capabilityProjection,
|
|
541
|
+
selector,
|
|
542
|
+
observed_action: observedAction,
|
|
543
|
+
});
|
|
544
|
+
if (Buffer.byteLength(canonicalize(program), 'utf8') > MAX_PROGRAM_BYTES) {
|
|
545
|
+
throw new Error('receipt_program:program_too_large');
|
|
546
|
+
}
|
|
547
|
+
programDigest = canonicalDigest(program);
|
|
548
|
+
if (actionOperationId !== operationId) {
|
|
549
|
+
return refuseEarly({ program, programDigest, reason: 'program_operation_binding_failed', startedAt });
|
|
550
|
+
}
|
|
551
|
+
const resolved = resolveCaid(structuredClone(observedAction));
|
|
552
|
+
if (resolved && typeof resolved.then === 'function') {
|
|
553
|
+
return refuseEarly({ program, programDigest, reason: 'caid_resolver_async_refused', startedAt });
|
|
554
|
+
}
|
|
555
|
+
if (normalizeResolvedCaid(resolved) !== request.caid) {
|
|
556
|
+
return refuseEarly({ program, programDigest, reason: 'caid_mismatch', startedAt });
|
|
557
|
+
}
|
|
558
|
+
if (request.expectedProgramDigest !== undefined && request.expectedProgramDigest !== programDigest) {
|
|
559
|
+
return refuseEarly({
|
|
560
|
+
program,
|
|
561
|
+
programDigest,
|
|
562
|
+
reason: 'program_digest_mismatch',
|
|
563
|
+
startedAt,
|
|
564
|
+
matched: true,
|
|
565
|
+
});
|
|
566
|
+
}
|
|
567
|
+
} catch (error) {
|
|
568
|
+
const placeholder = deepFreeze({
|
|
569
|
+
'@version': RECEIPT_PROGRAM_VERSION,
|
|
570
|
+
program_id: placeholderId(request.programId),
|
|
571
|
+
instruction_id: placeholderId(request.instructionId),
|
|
572
|
+
operation_id: placeholderId(request.capability?.operationId),
|
|
573
|
+
caid: placeholderId(request.caid),
|
|
574
|
+
action_digest: null,
|
|
575
|
+
capability_receipt_digest: null,
|
|
576
|
+
selector: {},
|
|
577
|
+
observed_action: {},
|
|
578
|
+
});
|
|
579
|
+
return refuseEarly({
|
|
580
|
+
program: placeholder,
|
|
581
|
+
programDigest: canonicalDigest(placeholder),
|
|
582
|
+
reason: failReason(error, 'program_invalid'),
|
|
583
|
+
startedAt,
|
|
584
|
+
});
|
|
585
|
+
}
|
|
586
|
+
|
|
587
|
+
let projectedResult: any = null;
|
|
588
|
+
let effectEntered = false;
|
|
589
|
+
let gateResult: any = null;
|
|
590
|
+
let caught: any = null;
|
|
591
|
+
try {
|
|
592
|
+
gateResult = await gate.run({
|
|
593
|
+
selector: program.selector,
|
|
594
|
+
receipt: executionReceipt,
|
|
595
|
+
observedAction: program.observed_action,
|
|
596
|
+
capability: executionCapability,
|
|
597
|
+
}, async (authorization: any, operation: any) => {
|
|
598
|
+
effectEntered = true;
|
|
599
|
+
// Gate owns these values. Give provider code frozen copies so it can
|
|
600
|
+
// neither rewrite the decision later used for evidence nor mutate
|
|
601
|
+
// the operation context Gate retains.
|
|
602
|
+
const authorizationSnapshot = deepFreeze(structuredClone(authorization));
|
|
603
|
+
const operationSnapshot = deepFreeze(structuredClone(operation));
|
|
604
|
+
const abortController = new AbortController();
|
|
605
|
+
const providerOperation = Object.freeze({
|
|
606
|
+
...operationSnapshot,
|
|
607
|
+
signal: abortController.signal,
|
|
608
|
+
});
|
|
609
|
+
let rawResult;
|
|
610
|
+
let timeoutId: any;
|
|
611
|
+
try {
|
|
612
|
+
const provider = Promise.resolve().then(
|
|
613
|
+
() => effect(authorizationSnapshot, providerOperation),
|
|
614
|
+
);
|
|
615
|
+
const deadline = new Promise((_, reject) => {
|
|
616
|
+
timeoutId = setTimeout(() => {
|
|
617
|
+
abortController.abort('receipt_program_effect_timeout');
|
|
618
|
+
reject(new Error('receipt_program_effect_timeout'));
|
|
619
|
+
}, effectTimeoutMs);
|
|
620
|
+
});
|
|
621
|
+
rawResult = await Promise.race([provider, deadline]);
|
|
622
|
+
} catch (cause) {
|
|
623
|
+
const error: any = new Error('receipt_program_provider_failed');
|
|
624
|
+
error.cause = cause;
|
|
625
|
+
throw error;
|
|
626
|
+
} finally {
|
|
627
|
+
if (timeoutId !== undefined) clearTimeout(timeoutId);
|
|
628
|
+
}
|
|
629
|
+
const projection = projectResult === null
|
|
630
|
+
? rawResult : await projectResult(rawResult);
|
|
631
|
+
projectedResult = cloneCanonical(
|
|
632
|
+
projection,
|
|
633
|
+
'receipt program effect result',
|
|
634
|
+
true,
|
|
635
|
+
MAX_RESULT_BYTES,
|
|
636
|
+
);
|
|
637
|
+
if (!isRecord(projectedResult)) {
|
|
638
|
+
throw new TypeError('receipt program effect result must be a canonical JSON object');
|
|
639
|
+
}
|
|
640
|
+
return structuredClone(projectedResult);
|
|
641
|
+
});
|
|
642
|
+
} catch (error) {
|
|
643
|
+
caught = error;
|
|
644
|
+
}
|
|
645
|
+
|
|
646
|
+
const caughtOutcome = caught?.emiliaGateOutcome ?? null;
|
|
647
|
+
let executionRecord: any = gateResult?.execution
|
|
648
|
+
?? (gateResult?.evidence?.kind === 'execution' ? gateResult.evidence : null)
|
|
649
|
+
?? caughtOutcome?.execution
|
|
650
|
+
?? null;
|
|
651
|
+
let authorizationRecord: any = gateResult?.authorization?.evidence
|
|
652
|
+
?? caughtOutcome?.authorizationEvidence
|
|
653
|
+
?? null;
|
|
654
|
+
const executionRef = evidenceReference(executionRecord);
|
|
655
|
+
const authorizationRef = evidenceReference(authorizationRecord);
|
|
656
|
+
|
|
657
|
+
if (caughtOutcome?.reason === 'execution_evidence_unavailable' && !executionRecord) {
|
|
658
|
+
return Object.freeze({
|
|
659
|
+
ok: false,
|
|
660
|
+
outcome: caughtOutcome.outcome ?? (effectEntered ? 'indeterminate' : 'refused'),
|
|
661
|
+
reason: 'execution_evidence_unavailable',
|
|
662
|
+
result: caughtOutcome.result ?? projectedResult,
|
|
663
|
+
certificate: null,
|
|
664
|
+
certificate_evidence: null,
|
|
665
|
+
});
|
|
666
|
+
}
|
|
667
|
+
|
|
668
|
+
if (gateResult?.ok === true && caught === null) {
|
|
669
|
+
return issueCertificate({
|
|
670
|
+
program,
|
|
671
|
+
programDigest,
|
|
672
|
+
outcome: 'executed',
|
|
673
|
+
reason: null,
|
|
674
|
+
result: projectedResult,
|
|
675
|
+
authorizationRef,
|
|
676
|
+
executionRef,
|
|
677
|
+
steps: opcodeSteps(['RECEIPT', 'MATCH', 'RESERVE', 'EXECUTE', 'COMMIT', 'CERTIFY']),
|
|
678
|
+
startedAt,
|
|
679
|
+
completedAt: instant(now),
|
|
680
|
+
});
|
|
681
|
+
}
|
|
682
|
+
|
|
683
|
+
if (caughtOutcome?.outcome === 'executed') {
|
|
684
|
+
if (!executionRecord) {
|
|
685
|
+
return Object.freeze({
|
|
686
|
+
ok: false,
|
|
687
|
+
outcome: 'executed',
|
|
688
|
+
reason: 'execution_evidence_unavailable',
|
|
689
|
+
result: caughtOutcome.result ?? projectedResult,
|
|
690
|
+
certificate: null,
|
|
691
|
+
certificate_evidence: null,
|
|
692
|
+
});
|
|
693
|
+
}
|
|
694
|
+
return issueCertificate({
|
|
695
|
+
program,
|
|
696
|
+
programDigest,
|
|
697
|
+
outcome: 'executed',
|
|
698
|
+
reason: null,
|
|
699
|
+
result: caughtOutcome.result ?? projectedResult,
|
|
700
|
+
authorizationRef,
|
|
701
|
+
executionRef,
|
|
702
|
+
steps: opcodeSteps(['RECEIPT', 'MATCH', 'RESERVE', 'EXECUTE', 'COMMIT', 'CERTIFY']),
|
|
703
|
+
startedAt,
|
|
704
|
+
completedAt: instant(now),
|
|
705
|
+
});
|
|
706
|
+
}
|
|
707
|
+
|
|
708
|
+
const evidenceIndeterminate = executionRecord?.outcome === 'indeterminate';
|
|
709
|
+
if (effectEntered || evidenceIndeterminate) {
|
|
710
|
+
return issueCertificate({
|
|
711
|
+
program,
|
|
712
|
+
programDigest,
|
|
713
|
+
outcome: 'indeterminate',
|
|
714
|
+
reason: executionRecord ? 'effect_indeterminate' : 'execution_evidence_unavailable',
|
|
715
|
+
result: null,
|
|
716
|
+
authorizationRef,
|
|
717
|
+
executionRef,
|
|
718
|
+
steps: opcodeSteps(['RECEIPT', 'MATCH', 'RESERVE', 'EXECUTE', 'COMMIT_INDETERMINATE', 'HALT', 'CERTIFY']),
|
|
719
|
+
startedAt,
|
|
720
|
+
completedAt: instant(now),
|
|
721
|
+
});
|
|
722
|
+
}
|
|
723
|
+
|
|
724
|
+
const reason = gateResult?.capability?.reason
|
|
725
|
+
?? gateResult?.refusal?.reason
|
|
726
|
+
?? gateResult?.authorization?.reason
|
|
727
|
+
?? failReason(caught, 'gate_refused');
|
|
728
|
+
return issueCertificate({
|
|
729
|
+
program,
|
|
730
|
+
programDigest,
|
|
731
|
+
outcome: 'refused',
|
|
732
|
+
reason,
|
|
733
|
+
result: null,
|
|
734
|
+
authorizationRef,
|
|
735
|
+
executionRef: null,
|
|
736
|
+
steps: opcodeSteps(['RECEIPT', 'MATCH', 'REFUSE', 'CERTIFY']),
|
|
737
|
+
startedAt,
|
|
738
|
+
completedAt: instant(now),
|
|
739
|
+
});
|
|
740
|
+
},
|
|
741
|
+
|
|
742
|
+
/**
|
|
743
|
+
* Explicit crash-recovery path. It scans the durable evidence history only
|
|
744
|
+
* when requested and returns every independently verified certificate for
|
|
745
|
+
* one program digest; it never guesses which attempt a caller intended.
|
|
746
|
+
*/
|
|
747
|
+
async recoverCertificates(programDigest: any): Promise<any> {
|
|
748
|
+
if (!SHA256_RE.test(programDigest)) {
|
|
749
|
+
return Object.freeze({ ok: false, reason: 'program_digest_invalid', certificates: [] });
|
|
750
|
+
}
|
|
751
|
+
let records;
|
|
752
|
+
try { records = await gate.evidence.all(); } catch {
|
|
753
|
+
return Object.freeze({ ok: false, reason: 'certificate_recovery_unavailable', certificates: [] });
|
|
754
|
+
}
|
|
755
|
+
if (!Array.isArray(records)) {
|
|
756
|
+
return Object.freeze({ ok: false, reason: 'certificate_recovery_malformed', certificates: [] });
|
|
757
|
+
}
|
|
758
|
+
const recovered: any[] = [];
|
|
759
|
+
for (const record of records) {
|
|
760
|
+
if (record?.kind !== 'receipt_program_certificate'
|
|
761
|
+
|| record.program_digest !== programDigest) continue;
|
|
762
|
+
const checked = verifyReceiptProgramCertificate(record.certificate, {
|
|
763
|
+
trustedCertificateKeys: { [context.key_id]: signer.publicKey },
|
|
764
|
+
resolveCaid,
|
|
765
|
+
expectedContext: context,
|
|
766
|
+
certificateEvidence: record,
|
|
767
|
+
verifyCertificateInclusion: (candidate: any) => (
|
|
768
|
+
canonicalEvidenceJson(candidate) === canonicalEvidenceJson(record)
|
|
769
|
+
),
|
|
770
|
+
requireAtomicCertificateEvidence: !allowEphemeralState,
|
|
771
|
+
});
|
|
772
|
+
if (!checked.ok) {
|
|
773
|
+
return Object.freeze({ ok: false, reason: 'certificate_recovery_invalid', certificates: [] });
|
|
774
|
+
}
|
|
775
|
+
recovered.push(deepFreeze({
|
|
776
|
+
certificate: structuredClone(record.certificate),
|
|
777
|
+
certificate_evidence: structuredClone(record),
|
|
778
|
+
verification: checked,
|
|
779
|
+
}));
|
|
780
|
+
}
|
|
781
|
+
return Object.freeze({
|
|
782
|
+
ok: true,
|
|
783
|
+
reason: null,
|
|
784
|
+
certificates: Object.freeze(recovered),
|
|
785
|
+
});
|
|
786
|
+
},
|
|
787
|
+
});
|
|
788
|
+
}
|
|
789
|
+
|
|
790
|
+
function verificationFailure(reason: string): any {
|
|
791
|
+
return Object.freeze({ ok: false, reason });
|
|
792
|
+
}
|
|
793
|
+
|
|
794
|
+
/**
|
|
795
|
+
* Verify the certificate's operator signature, content addresses, program
|
|
796
|
+
* binding, and Gate evidence linkage. This proves exact certificate integrity
|
|
797
|
+
* under a pinned operator key; it does not prove an external provider told the
|
|
798
|
+
* truth or replace verification of the referenced receipt/capability artifacts.
|
|
799
|
+
*
|
|
800
|
+
* options.trustedCertificateKeys?: Record<string, string>
|
|
801
|
+
* options.resolveCaid?: ((action: any) => any)|null
|
|
802
|
+
* options.expectedContext?: object|null
|
|
803
|
+
* options.certificateEvidence?: any
|
|
804
|
+
* options.verifyCertificateInclusion?: (((record: any, expectation: any) => any)|null)
|
|
805
|
+
* options.requireAtomicCertificateEvidence?: boolean
|
|
806
|
+
*/
|
|
807
|
+
export function verifyReceiptProgramCertificate(certificate: any, {
|
|
808
|
+
trustedCertificateKeys = {},
|
|
809
|
+
resolveCaid = null,
|
|
810
|
+
expectedContext = null,
|
|
811
|
+
certificateEvidence = null,
|
|
812
|
+
verifyCertificateInclusion = null,
|
|
813
|
+
requireAtomicCertificateEvidence = false,
|
|
814
|
+
}: any = {}): any {
|
|
815
|
+
try {
|
|
816
|
+
const versionDescriptor = isDataRecord(certificate)
|
|
817
|
+
? Object.getOwnPropertyDescriptor(certificate, '@version') : null;
|
|
818
|
+
if (versionDescriptor?.value !== RECEIPT_PROGRAM_CERTIFICATE_VERSION) {
|
|
819
|
+
return verificationFailure('certificate_version_invalid');
|
|
820
|
+
}
|
|
821
|
+
const snapshot = cloneCanonical(certificate, 'certificate', false);
|
|
822
|
+
const completeCertificate = structuredClone(snapshot);
|
|
823
|
+
if (!hasExactKeys(snapshot, [
|
|
824
|
+
'@version',
|
|
825
|
+
'context',
|
|
826
|
+
'program',
|
|
827
|
+
'program_digest',
|
|
828
|
+
'outcome',
|
|
829
|
+
'reason',
|
|
830
|
+
'result',
|
|
831
|
+
'result_digest',
|
|
832
|
+
'authorization_ref',
|
|
833
|
+
'execution_ref',
|
|
834
|
+
'steps',
|
|
835
|
+
'started_at',
|
|
836
|
+
'completed_at',
|
|
837
|
+
'state_root',
|
|
838
|
+
'signature',
|
|
839
|
+
])) return verificationFailure('certificate_schema_invalid');
|
|
840
|
+
const signature = snapshot.signature;
|
|
841
|
+
if (!hasExactKeys(signature, ['algorithm', 'public_key', 'value'])
|
|
842
|
+
|| signature.algorithm !== RECEIPT_PROGRAM_SIGNATURE_ALGORITHM
|
|
843
|
+
|| typeof signature.public_key !== 'string' || typeof signature.value !== 'string') {
|
|
844
|
+
return verificationFailure('certificate_signature_invalid');
|
|
845
|
+
}
|
|
846
|
+
const claimedKeyId = snapshot.context?.key_id;
|
|
847
|
+
if (!isDataRecord(trustedCertificateKeys)
|
|
848
|
+
|| typeof claimedKeyId !== 'string'
|
|
849
|
+
|| trustedCertificateKeys[claimedKeyId] !== signature.public_key) {
|
|
850
|
+
return verificationFailure('certificate_signer_not_trusted');
|
|
851
|
+
}
|
|
852
|
+
delete snapshot.signature;
|
|
853
|
+
const signatureBytes = Buffer.from(signature.value, 'base64url');
|
|
854
|
+
if (signatureBytes.length !== 64 || signatureBytes.toString('base64url') !== signature.value
|
|
855
|
+
|| !verify(
|
|
856
|
+
null,
|
|
857
|
+
Buffer.from(canonicalize(snapshot), 'utf8'),
|
|
858
|
+
publicKeyObject(signature.public_key),
|
|
859
|
+
signatureBytes,
|
|
860
|
+
)) return verificationFailure('certificate_signature_invalid');
|
|
861
|
+
|
|
862
|
+
const stateRoot = snapshot.state_root;
|
|
863
|
+
delete snapshot.state_root;
|
|
864
|
+
if (!SHA256_RE.test(stateRoot) || canonicalDigest(snapshot) !== stateRoot) {
|
|
865
|
+
return verificationFailure('certificate_state_root_mismatch');
|
|
866
|
+
}
|
|
867
|
+
const signed = { ...snapshot, state_root: stateRoot };
|
|
868
|
+
const certificateCoreSnapshot: any = { ...signed };
|
|
869
|
+
delete certificateCoreSnapshot.state_root;
|
|
870
|
+
if (Buffer.byteLength(canonicalize(certificateCoreSnapshot), 'utf8')
|
|
871
|
+
> MAX_CERTIFICATE_CORE_BYTES) {
|
|
872
|
+
return verificationFailure('certificate_size_invalid');
|
|
873
|
+
}
|
|
874
|
+
if (!hasExactKeys(signed.context, ['issuer', 'tenant', 'environment', 'audience', 'key_id'])
|
|
875
|
+
|| Object.values(signed.context).some((value) => typeof value !== 'string'
|
|
876
|
+
|| value.length === 0 || Buffer.byteLength(value, 'utf8') > MAX_ID_BYTES)) {
|
|
877
|
+
return verificationFailure('certificate_context_invalid');
|
|
878
|
+
}
|
|
879
|
+
if (!isRecord(expectedContext)) {
|
|
880
|
+
return verificationFailure('certificate_context_required');
|
|
881
|
+
}
|
|
882
|
+
let expectedContextSnapshot;
|
|
883
|
+
try { expectedContextSnapshot = cloneCanonical(expectedContext, 'expectedContext'); } catch {
|
|
884
|
+
return verificationFailure('certificate_context_invalid');
|
|
885
|
+
}
|
|
886
|
+
if (canonicalize(expectedContextSnapshot) !== canonicalize(signed.context)) {
|
|
887
|
+
return verificationFailure('certificate_context_mismatch');
|
|
888
|
+
}
|
|
889
|
+
if (!validInstant(signed.started_at) || !validInstant(signed.completed_at)
|
|
890
|
+
|| Date.parse(signed.completed_at) < Date.parse(signed.started_at)) {
|
|
891
|
+
return verificationFailure('certificate_time_invalid');
|
|
892
|
+
}
|
|
893
|
+
const program = signed.program;
|
|
894
|
+
if (!isRecord(program) || program['@version'] !== RECEIPT_PROGRAM_VERSION) {
|
|
895
|
+
return verificationFailure('certificate_program_invalid');
|
|
896
|
+
}
|
|
897
|
+
if (Buffer.byteLength(canonicalize(program), 'utf8') > MAX_PROGRAM_BYTES) {
|
|
898
|
+
return verificationFailure('certificate_program_invalid');
|
|
899
|
+
}
|
|
900
|
+
if (!SHA256_RE.test(signed.program_digest) || canonicalDigest(program) !== signed.program_digest) {
|
|
901
|
+
return verificationFailure('certificate_program_digest_mismatch');
|
|
902
|
+
}
|
|
903
|
+
if (!['executed', 'indeterminate', 'refused'].includes(signed.outcome)) {
|
|
904
|
+
return verificationFailure('certificate_outcome_invalid');
|
|
905
|
+
}
|
|
906
|
+
if ((signed.outcome === 'executed' && signed.reason !== null)
|
|
907
|
+
|| (signed.outcome !== 'executed' && typeof signed.reason !== 'string')) {
|
|
908
|
+
return verificationFailure('certificate_reason_invalid');
|
|
909
|
+
}
|
|
910
|
+
const completeProgram = hasExactKeys(program, [
|
|
911
|
+
'@version',
|
|
912
|
+
'program_id',
|
|
913
|
+
'instruction_id',
|
|
914
|
+
'operation_id',
|
|
915
|
+
'operation_id_field',
|
|
916
|
+
'caid',
|
|
917
|
+
'action_digest',
|
|
918
|
+
'capability_receipt_digest',
|
|
919
|
+
'capability_projection',
|
|
920
|
+
'selector',
|
|
921
|
+
'observed_action',
|
|
922
|
+
])
|
|
923
|
+
&& [program.program_id, program.instruction_id, program.operation_id, program.operation_id_field]
|
|
924
|
+
.every((value) => typeof value === 'string' && value.length > 0
|
|
925
|
+
&& Buffer.byteLength(value, 'utf8') <= MAX_ID_BYTES)
|
|
926
|
+
&& isRecord(program.capability_projection)
|
|
927
|
+
&& isRecord(program.selector)
|
|
928
|
+
&& isRecord(program.observed_action);
|
|
929
|
+
const executableProgram = completeProgram
|
|
930
|
+
&& CAID_RE.test(program.caid)
|
|
931
|
+
&& SHA256_RE.test(program.action_digest)
|
|
932
|
+
&& SHA256_RE.test(program.capability_receipt_digest)
|
|
933
|
+
&& valueAtPath(program.observed_action, program.operation_id_field) === program.operation_id
|
|
934
|
+
&& canonicalDigest(program.observed_action) === program.action_digest;
|
|
935
|
+
if (signed.outcome !== 'refused' && !executableProgram) {
|
|
936
|
+
return verificationFailure('certificate_program_invalid');
|
|
937
|
+
}
|
|
938
|
+
if (executableProgram) {
|
|
939
|
+
if (typeof resolveCaid !== 'function') {
|
|
940
|
+
return verificationFailure('certificate_caid_resolver_required');
|
|
941
|
+
}
|
|
942
|
+
const resolved = resolveCaid(structuredClone(program.observed_action));
|
|
943
|
+
if (resolved && typeof resolved.then === 'function') {
|
|
944
|
+
return verificationFailure('certificate_caid_resolver_async_refused');
|
|
945
|
+
}
|
|
946
|
+
const resolvedCaid = normalizeResolvedCaid(resolved);
|
|
947
|
+
if (signed.outcome === 'refused' && signed.reason === 'caid_mismatch') {
|
|
948
|
+
if (resolvedCaid === program.caid) return verificationFailure('certificate_refusal_evidence_mismatch');
|
|
949
|
+
} else if (resolvedCaid !== program.caid) {
|
|
950
|
+
return verificationFailure('certificate_caid_mismatch');
|
|
951
|
+
}
|
|
952
|
+
}
|
|
953
|
+
if (!Array.isArray(signed.steps) || signed.steps.length === 0
|
|
954
|
+
|| signed.steps.some((step: any, index: number) => !hasExactKeys(step, ['sequence', 'opcode'])
|
|
955
|
+
|| step.sequence !== index || typeof step.opcode !== 'string')) {
|
|
956
|
+
return verificationFailure('certificate_steps_invalid');
|
|
957
|
+
}
|
|
958
|
+
const expectedOpcodes = signed.outcome === 'executed'
|
|
959
|
+
? ['RECEIPT', 'MATCH', 'RESERVE', 'EXECUTE', 'COMMIT', 'CERTIFY']
|
|
960
|
+
: signed.outcome === 'indeterminate'
|
|
961
|
+
? ['RECEIPT', 'MATCH', 'RESERVE', 'EXECUTE', 'COMMIT_INDETERMINATE', 'HALT', 'CERTIFY']
|
|
962
|
+
: executableProgram && !['caid_mismatch', 'caid_resolver_async_refused'].includes(signed.reason)
|
|
963
|
+
? ['RECEIPT', 'MATCH', 'REFUSE', 'CERTIFY']
|
|
964
|
+
: ['RECEIPT', 'REFUSE', 'CERTIFY'];
|
|
965
|
+
if (canonicalize(signed.steps.map((step: any) => step.opcode)) !== canonicalize(expectedOpcodes)) {
|
|
966
|
+
return verificationFailure('certificate_steps_outcome_mismatch');
|
|
967
|
+
}
|
|
968
|
+
if (signed.result === null) {
|
|
969
|
+
if (signed.result_digest !== null) return verificationFailure('certificate_result_digest_mismatch');
|
|
970
|
+
} else {
|
|
971
|
+
if (!isRecord(signed.result)
|
|
972
|
+
|| Buffer.byteLength(canonicalize(signed.result), 'utf8') > MAX_RESULT_BYTES) {
|
|
973
|
+
return verificationFailure('certificate_result_invalid');
|
|
974
|
+
}
|
|
975
|
+
if (!SHA256_RE.test(signed.result_digest)
|
|
976
|
+
|| canonicalDigest(signed.result) !== signed.result_digest) {
|
|
977
|
+
return verificationFailure('certificate_result_digest_mismatch');
|
|
978
|
+
}
|
|
979
|
+
}
|
|
980
|
+
const authorization = signed.authorization_ref;
|
|
981
|
+
const execution = signed.execution_ref;
|
|
982
|
+
const validReference = (reference: any, kind: string): boolean => reference === null || (hasExactKeys(reference, [
|
|
983
|
+
'seq', 'record_id', 'hash', 'prev_hash', 'kind', 'allow', 'outcome',
|
|
984
|
+
'authorizes_decision', 'observed_action_hash', 'operation_id',
|
|
985
|
+
])
|
|
986
|
+
&& Number.isSafeInteger(reference.seq) && reference.seq >= 0
|
|
987
|
+
&& (reference.record_id === null || (typeof reference.record_id === 'string'
|
|
988
|
+
&& reference.record_id.length > 0
|
|
989
|
+
&& Buffer.byteLength(reference.record_id, 'utf8') <= MAX_ID_BYTES))
|
|
990
|
+
&& /^[0-9a-f]{64}$/.test(reference.hash)
|
|
991
|
+
&& (reference.prev_hash === 'genesis' || /^[0-9a-f]{64}$/.test(reference.prev_hash))
|
|
992
|
+
&& reference.kind === kind
|
|
993
|
+
&& (reference.allow === null || typeof reference.allow === 'boolean')
|
|
994
|
+
&& (reference.outcome === null || (typeof reference.outcome === 'string'
|
|
995
|
+
&& reference.outcome.length > 0
|
|
996
|
+
&& Buffer.byteLength(reference.outcome, 'utf8') <= MAX_ID_BYTES))
|
|
997
|
+
&& (reference.authorizes_decision === null || /^[0-9a-f]{64}$/.test(reference.authorizes_decision))
|
|
998
|
+
&& (reference.observed_action_hash === null || /^[0-9a-f]{64}$/.test(reference.observed_action_hash))
|
|
999
|
+
&& (reference.operation_id === null || (typeof reference.operation_id === 'string'
|
|
1000
|
+
&& reference.operation_id.length > 0
|
|
1001
|
+
&& Buffer.byteLength(reference.operation_id, 'utf8') <= MAX_ID_BYTES)));
|
|
1002
|
+
if (!validReference(authorization, 'decision')) return verificationFailure('certificate_authorization_ref_invalid');
|
|
1003
|
+
if (!validReference(execution, 'execution')) return verificationFailure('certificate_execution_ref_invalid');
|
|
1004
|
+
if (authorization && (typeof authorization.allow !== 'boolean'
|
|
1005
|
+
|| (authorization.allow === true && typeof authorization.outcome !== 'string'))) {
|
|
1006
|
+
return verificationFailure('certificate_authorization_ref_invalid');
|
|
1007
|
+
}
|
|
1008
|
+
if (execution && (execution.allow !== null
|
|
1009
|
+
|| !['executed', 'indeterminate'].includes(execution.outcome))) {
|
|
1010
|
+
return verificationFailure('certificate_execution_ref_invalid');
|
|
1011
|
+
}
|
|
1012
|
+
if (authorization && execution && execution.seq <= authorization.seq) {
|
|
1013
|
+
return verificationFailure('certificate_evidence_order_invalid');
|
|
1014
|
+
}
|
|
1015
|
+
if (authorization && execution && execution.authorizes_decision !== authorization.hash) {
|
|
1016
|
+
return verificationFailure('certificate_evidence_link_mismatch');
|
|
1017
|
+
}
|
|
1018
|
+
const bareActionDigest = executableProgram ? program.action_digest.slice('sha256:'.length) : null;
|
|
1019
|
+
if (authorization && authorization.observed_action_hash !== bareActionDigest) {
|
|
1020
|
+
return verificationFailure('certificate_authorization_binding_mismatch');
|
|
1021
|
+
}
|
|
1022
|
+
if (signed.outcome !== 'refused' && authorization?.allow !== true) {
|
|
1023
|
+
return verificationFailure('certificate_authorization_binding_mismatch');
|
|
1024
|
+
}
|
|
1025
|
+
if (execution && (execution.observed_action_hash !== bareActionDigest
|
|
1026
|
+
|| execution.operation_id !== program.operation_id)) {
|
|
1027
|
+
return verificationFailure('certificate_execution_binding_mismatch');
|
|
1028
|
+
}
|
|
1029
|
+
if (signed.outcome === 'executed') {
|
|
1030
|
+
if (signed.result === null || !authorization || !execution || execution.outcome !== 'executed') {
|
|
1031
|
+
return verificationFailure('certificate_executed_evidence_incomplete');
|
|
1032
|
+
}
|
|
1033
|
+
} else if (signed.result !== null) {
|
|
1034
|
+
return verificationFailure('certificate_nonexecuted_result_present');
|
|
1035
|
+
}
|
|
1036
|
+
if (signed.outcome === 'indeterminate') {
|
|
1037
|
+
if (execution === null && signed.reason !== 'execution_evidence_unavailable') {
|
|
1038
|
+
return verificationFailure('certificate_indeterminate_evidence_incomplete');
|
|
1039
|
+
}
|
|
1040
|
+
if (execution !== null && execution.outcome !== 'indeterminate') {
|
|
1041
|
+
return verificationFailure('certificate_indeterminate_evidence_mismatch');
|
|
1042
|
+
}
|
|
1043
|
+
}
|
|
1044
|
+
let certificatePersisted = false;
|
|
1045
|
+
if (certificateEvidence !== null) {
|
|
1046
|
+
const expectedEntry = {
|
|
1047
|
+
kind: 'receipt_program_certificate',
|
|
1048
|
+
program_digest: signed.program_digest,
|
|
1049
|
+
operation_id: program.operation_id,
|
|
1050
|
+
outcome: signed.outcome,
|
|
1051
|
+
state_root: stateRoot,
|
|
1052
|
+
certificate: completeCertificate,
|
|
1053
|
+
};
|
|
1054
|
+
if (!verifyEvidenceRecord(certificateEvidence, {
|
|
1055
|
+
atomicRequired: requireAtomicCertificateEvidence,
|
|
1056
|
+
expectedEntry,
|
|
1057
|
+
})) return verificationFailure('certificate_evidence_invalid');
|
|
1058
|
+
if (typeof verifyCertificateInclusion !== 'function') {
|
|
1059
|
+
return verificationFailure('certificate_evidence_inclusion_verifier_required');
|
|
1060
|
+
}
|
|
1061
|
+
let included;
|
|
1062
|
+
try {
|
|
1063
|
+
included = verifyCertificateInclusion(structuredClone(certificateEvidence), deepFreeze({
|
|
1064
|
+
expected_entry: structuredClone(expectedEntry),
|
|
1065
|
+
expected_context: structuredClone(expectedContextSnapshot),
|
|
1066
|
+
program_digest: signed.program_digest,
|
|
1067
|
+
state_root: stateRoot,
|
|
1068
|
+
atomic_required: requireAtomicCertificateEvidence,
|
|
1069
|
+
}));
|
|
1070
|
+
} catch {
|
|
1071
|
+
return verificationFailure('certificate_evidence_not_included');
|
|
1072
|
+
}
|
|
1073
|
+
if (included && typeof included.then === 'function') {
|
|
1074
|
+
return verificationFailure('certificate_evidence_inclusion_async_refused');
|
|
1075
|
+
}
|
|
1076
|
+
if (included !== true) return verificationFailure('certificate_evidence_not_included');
|
|
1077
|
+
certificatePersisted = true;
|
|
1078
|
+
} else if (requireAtomicCertificateEvidence) {
|
|
1079
|
+
return verificationFailure('certificate_evidence_required');
|
|
1080
|
+
}
|
|
1081
|
+
return Object.freeze({
|
|
1082
|
+
ok: true,
|
|
1083
|
+
certificate_valid: true,
|
|
1084
|
+
execution_succeeded: signed.outcome === 'executed',
|
|
1085
|
+
reason: null,
|
|
1086
|
+
outcome: signed.outcome,
|
|
1087
|
+
program_digest: signed.program_digest,
|
|
1088
|
+
state_root: stateRoot,
|
|
1089
|
+
signer: signature.public_key,
|
|
1090
|
+
evidence_complete: Boolean(authorization && (signed.outcome === 'refused' || execution)),
|
|
1091
|
+
certificate_persisted: certificatePersisted,
|
|
1092
|
+
caid_reperformed: executableProgram,
|
|
1093
|
+
});
|
|
1094
|
+
} catch {
|
|
1095
|
+
return verificationFailure('certificate_malformed');
|
|
1096
|
+
}
|
|
1097
|
+
}
|
|
1098
|
+
|
|
1099
|
+
export default {
|
|
1100
|
+
RECEIPT_PROGRAM_VERSION,
|
|
1101
|
+
RECEIPT_PROGRAM_CERTIFICATE_VERSION,
|
|
1102
|
+
RECEIPT_PROGRAM_SIGNATURE_ALGORITHM,
|
|
1103
|
+
createReceiptProgramKernel,
|
|
1104
|
+
verifyReceiptProgramCertificate,
|
|
1105
|
+
};
|