@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,471 @@
|
|
|
1
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
2
|
+
/**
|
|
3
|
+
* EG-1 Conformance — the binary artifact that turns "we adopted EMILIA Gate"
|
|
4
|
+
* from a claim into a test result.
|
|
5
|
+
*
|
|
6
|
+
* The question EG-1 answers: *does your integration actually ENFORCE the gate,
|
|
7
|
+
* or are you only claiming it?* An integration earns EG-1 only if it
|
|
8
|
+
* demonstrably:
|
|
9
|
+
* 1. refuses a high-risk action with NO receipt (428);
|
|
10
|
+
* 2. refuses a software-tier receipt on a Class-A action;
|
|
11
|
+
* 3. refuses when the observed execution drifts from the authorized fields;
|
|
12
|
+
* 4. RUNS the action for a valid Class-A/quorum receipt;
|
|
13
|
+
* 5. refuses a replay of the same receipt;
|
|
14
|
+
* 6. refuses a tampered receipt;
|
|
15
|
+
* 7. emits an execution proof bound to the authorization decision;
|
|
16
|
+
* 8. produces a reliance packet whose verdict is "rely".
|
|
17
|
+
*
|
|
18
|
+
* This module is pure (no dependency on index.js, so no import cycle): it owns a
|
|
19
|
+
* throwaway issuer keypair, mints the scenario receipts, and drives any
|
|
20
|
+
* "subject" through the eight checks. A subject is an async `invoke` function
|
|
21
|
+
* representing one attempt at the guarded dangerous action:
|
|
22
|
+
*
|
|
23
|
+
* invoke({ receipt, observedAction }) -> {
|
|
24
|
+
* allowed: boolean, status: number, reason: string,
|
|
25
|
+
* // present only on an allowed run:
|
|
26
|
+
* decisionHash?: string, execution?: { authorizes_decision }, packet?: { verdict }
|
|
27
|
+
* }
|
|
28
|
+
*
|
|
29
|
+
* For integrations built on @emilia-protocol/gate, `makeGateInvoke(gate, ...)`
|
|
30
|
+
* produces a conformant `invoke` from `gate.run()`. Custom integrations (an HTTP
|
|
31
|
+
* service, a different language) implement `invoke` themselves and configure
|
|
32
|
+
* their gate to trust `harness.publicKey` for the run.
|
|
33
|
+
*/
|
|
34
|
+
import crypto from 'node:crypto';
|
|
35
|
+
|
|
36
|
+
export const EG1_VERSION = 'EG-1';
|
|
37
|
+
|
|
38
|
+
// Same sorted-key canonical JSON the receipt signature is computed over.
|
|
39
|
+
const canon = (v) => (v == null ? JSON.stringify(v)
|
|
40
|
+
: Array.isArray(v) ? `[${v.map(canon).join(',')}]`
|
|
41
|
+
: typeof v === 'object' ? `{${Object.keys(v).sort().map((k) => JSON.stringify(k) + ':' + canon(v[k])).join(',')}}`
|
|
42
|
+
: JSON.stringify(v));
|
|
43
|
+
const sha256Hex = (v) => crypto.createHash('sha256').update(v, 'utf8').digest('hex');
|
|
44
|
+
const sha256Bytes = (v) => crypto.createHash('sha256').update(v).digest();
|
|
45
|
+
|
|
46
|
+
const RP_ID = 'emiliaprotocol.ai';
|
|
47
|
+
const RP_ORIGIN = `https://www.${RP_ID}`;
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Mint a GENUINE WebAuthn ECDSA-P256 device signoff over an authorization
|
|
51
|
+
* context — the same structure @emilia-protocol/verify verifyWebAuthnSignoff
|
|
52
|
+
* checks. This is what earns a receipt its class_a tier: a real per-signer
|
|
53
|
+
* assertion, not a self-asserted `outcome` string. Used to build the Class-A and
|
|
54
|
+
* quorum evidence the EG-1 harness embeds so the Gate can CRYPTOGRAPHICALLY
|
|
55
|
+
* credit the tier.
|
|
56
|
+
* @param {{ actionHash?: string, approver?: string, issuedAtMs?: number, nonce?: string, prevContextHash?: string }} [opts]
|
|
57
|
+
*/
|
|
58
|
+
export function mintDeviceSignoff({
|
|
59
|
+
actionHash,
|
|
60
|
+
approver,
|
|
61
|
+
issuedAtMs = Date.now(),
|
|
62
|
+
nonce,
|
|
63
|
+
prevContextHash = undefined,
|
|
64
|
+
}: {
|
|
65
|
+
actionHash?: string;
|
|
66
|
+
approver?: string;
|
|
67
|
+
issuedAtMs?: number;
|
|
68
|
+
nonce?: string;
|
|
69
|
+
prevContextHash?: string;
|
|
70
|
+
} = {}) {
|
|
71
|
+
const signer = crypto.generateKeyPairSync('ec', { namedCurve: 'P-256' });
|
|
72
|
+
const context = {
|
|
73
|
+
ep_version: '1.0', context_type: 'ep.signoff.v1',
|
|
74
|
+
action_hash: actionHash,
|
|
75
|
+
policy: 'policy_eg1',
|
|
76
|
+
nonce: nonce || ('sig_' + crypto.randomBytes(16).toString('hex')),
|
|
77
|
+
approver,
|
|
78
|
+
initiator: 'ent_agent_eg1',
|
|
79
|
+
issued_at: new Date(issuedAtMs).toISOString(),
|
|
80
|
+
expires_at: new Date(issuedAtMs + 5 * 60_000).toISOString(),
|
|
81
|
+
...(prevContextHash !== undefined ? { prev_context_hash: prevContextHash } : {}),
|
|
82
|
+
};
|
|
83
|
+
const challenge = crypto.createHash('sha256').update(canon(context), 'utf8').digest().toString('base64url');
|
|
84
|
+
const clientData = Buffer.from(JSON.stringify({ type: 'webauthn.get', challenge, origin: `https://www.${RP_ID}` }), 'utf8');
|
|
85
|
+
const authData = Buffer.concat([
|
|
86
|
+
crypto.createHash('sha256').update(RP_ID, 'utf8').digest(),
|
|
87
|
+
Buffer.from([0x05]), // UP | UV
|
|
88
|
+
Buffer.from([0, 0, 0, 1]),
|
|
89
|
+
]);
|
|
90
|
+
const signed = Buffer.concat([authData, crypto.createHash('sha256').update(clientData).digest()]);
|
|
91
|
+
const signature = crypto.sign('sha256', signed, signer.privateKey).toString('base64url');
|
|
92
|
+
return {
|
|
93
|
+
signoff: {
|
|
94
|
+
'@type': 'ep.signoff',
|
|
95
|
+
context,
|
|
96
|
+
webauthn: {
|
|
97
|
+
authenticator_data: authData.toString('base64url'),
|
|
98
|
+
client_data_json: clientData.toString('base64url'),
|
|
99
|
+
signature,
|
|
100
|
+
},
|
|
101
|
+
approver_public_key: signer.publicKey.export({ type: 'spki', format: 'der' }).toString('base64url'),
|
|
102
|
+
},
|
|
103
|
+
approver_public_key: signer.publicKey.export({ type: 'spki', format: 'der' }).toString('base64url'),
|
|
104
|
+
context,
|
|
105
|
+
};
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* Mint a GENUINE EP-QUORUM-v1 evidence document: N distinct humans, each on a
|
|
110
|
+
* distinct device key, each with a real WebAuthn assertion bound to the SAME
|
|
111
|
+
* action_hash, within a window. verifyQuorum returns valid for it. This is what
|
|
112
|
+
* earns a receipt its `quorum` tier — never a bare {signers,threshold} block.
|
|
113
|
+
* @param {{ actionHash?: string, threshold?: number, approvers?: Array<{ role: string, approver: string }>, issuedAtMs?: number }} [opts]
|
|
114
|
+
*/
|
|
115
|
+
export function mintQuorumEvidence({
|
|
116
|
+
actionHash,
|
|
117
|
+
threshold = 2,
|
|
118
|
+
approvers,
|
|
119
|
+
issuedAtMs = Date.now(),
|
|
120
|
+
}: {
|
|
121
|
+
actionHash?: string;
|
|
122
|
+
threshold?: number;
|
|
123
|
+
approvers?: Array<{ role: string; approver: string }>;
|
|
124
|
+
issuedAtMs?: number;
|
|
125
|
+
} = {}) {
|
|
126
|
+
const people = approvers || Array.from({ length: threshold }, (_, i) => ({ role: `approver_${i + 1}`, approver: `ep:approver:eg1_${i + 1}` }));
|
|
127
|
+
const members = people.map((p, i) => {
|
|
128
|
+
const s = mintDeviceSignoff({ actionHash, approver: p.approver, issuedAtMs: issuedAtMs + i * 1000 });
|
|
129
|
+
return { role: p.role, approver_public_key: s.approver_public_key, signoff: { '@type': s.signoff['@type'], context: s.signoff.context, webauthn: s.signoff.webauthn } };
|
|
130
|
+
});
|
|
131
|
+
return {
|
|
132
|
+
'@type': 'ep.quorum',
|
|
133
|
+
action_hash: actionHash,
|
|
134
|
+
policy: {
|
|
135
|
+
mode: 'threshold',
|
|
136
|
+
required: threshold,
|
|
137
|
+
approvers: people,
|
|
138
|
+
distinct_humans: true,
|
|
139
|
+
window_sec: 900,
|
|
140
|
+
},
|
|
141
|
+
members,
|
|
142
|
+
};
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
// The default high-risk action EG-1 exercises: a Class-A money movement, which
|
|
146
|
+
// the default gate manifest guards (selector { protocol:'mcp', tool:'release_payment' }).
|
|
147
|
+
export const EG1_DEFAULT_SELECTOR = Object.freeze({ protocol: 'mcp', tool: 'release_payment' });
|
|
148
|
+
export const EG1_DEFAULT_ACTION = Object.freeze({
|
|
149
|
+
action_type: 'payment.release',
|
|
150
|
+
amount_usd: 40000,
|
|
151
|
+
currency: 'USD',
|
|
152
|
+
payment_instruction_id: 'pi_eg1_40000',
|
|
153
|
+
beneficiary_account_hash: 'sha256:eg1-beneficiary',
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
export const EG1_CHECKS = Object.freeze([
|
|
157
|
+
{ id: 'missing_receipt_refused', title: 'missing receipt → 428' },
|
|
158
|
+
{ id: 'software_on_classA_refused', title: 'software receipt on Class-A action → refused' },
|
|
159
|
+
{ id: 'execution_drift_refused', title: 'observed execution drift → refused' },
|
|
160
|
+
{ id: 'valid_classA_runs', title: 'valid Class-A/quorum receipt → runs' },
|
|
161
|
+
{ id: 'replay_refused', title: 'same receipt replay → refused' },
|
|
162
|
+
{ id: 'tampered_refused', title: 'tampered receipt → refused' },
|
|
163
|
+
{ id: 'execution_proof_binds', title: 'execution proof binds to authorization decision' },
|
|
164
|
+
{ id: 'reliance_packet_rely', title: 'reliance packet returns verdict "rely"' },
|
|
165
|
+
]);
|
|
166
|
+
|
|
167
|
+
/**
|
|
168
|
+
* Create an EG-1 harness: a throwaway issuer key + a receipt minter for the
|
|
169
|
+
* scenarios. Configure the subject's gate to trust `publicKey` for the run.
|
|
170
|
+
*/
|
|
171
|
+
export function createEg1Harness({
|
|
172
|
+
now = Date.now,
|
|
173
|
+
action = EG1_DEFAULT_ACTION,
|
|
174
|
+
idPrefix = 'eg1',
|
|
175
|
+
}: { now?: () => number; action?: Record<string, unknown>; idPrefix?: string } = {}) {
|
|
176
|
+
const { publicKey, privateKey } = crypto.generateKeyPairSync('ed25519');
|
|
177
|
+
const pub = publicKey.export({ type: 'spki', format: 'der' }).toString('base64url');
|
|
178
|
+
const approverA = crypto.generateKeyPairSync('ec', { namedCurve: 'P-256' });
|
|
179
|
+
const approverA2 = crypto.generateKeyPairSync('ec', { namedCurve: 'P-256' });
|
|
180
|
+
const approverB = crypto.generateKeyPairSync('ed25519');
|
|
181
|
+
const approverKeys = {
|
|
182
|
+
'ep:key:eg1:class-a': {
|
|
183
|
+
approver_id: 'ep:approver:eg1:cfo',
|
|
184
|
+
public_key: approverA.publicKey.export({ type: 'spki', format: 'der' }).toString('base64url'),
|
|
185
|
+
key_class: 'A',
|
|
186
|
+
},
|
|
187
|
+
'ep:key:eg1:class-a-2': {
|
|
188
|
+
approver_id: 'ep:approver:eg1:security-officer',
|
|
189
|
+
public_key: approverA2.publicKey.export({ type: 'spki', format: 'der' }).toString('base64url'),
|
|
190
|
+
key_class: 'A',
|
|
191
|
+
},
|
|
192
|
+
'ep:key:eg1:controller': {
|
|
193
|
+
approver_id: 'ep:approver:eg1:controller',
|
|
194
|
+
public_key: approverB.publicKey.export({ type: 'spki', format: 'der' }).toString('base64url'),
|
|
195
|
+
key_class: 'B',
|
|
196
|
+
},
|
|
197
|
+
};
|
|
198
|
+
const quorumPolicy = {
|
|
199
|
+
mode: 'threshold', required: 2, distinct_humans: true, window_sec: 900,
|
|
200
|
+
approvers: [
|
|
201
|
+
{ role: 'cfo', approver: approverKeys['ep:key:eg1:class-a'].approver_id },
|
|
202
|
+
{ role: 'security_officer', approver: approverKeys['ep:key:eg1:class-a-2'].approver_id },
|
|
203
|
+
],
|
|
204
|
+
};
|
|
205
|
+
let counter = 0;
|
|
206
|
+
const nowMs = () => (typeof now === 'function' ? now() : now);
|
|
207
|
+
|
|
208
|
+
// The action_hash the self-contained human device assertions are bound to.
|
|
209
|
+
// Derived from the action so the signoff/quorum evidence is about THIS action.
|
|
210
|
+
const actionHash = crypto.createHash('sha256').update(canon(action), 'utf8').digest('hex');
|
|
211
|
+
|
|
212
|
+
function assuranceContext(payload) {
|
|
213
|
+
return {
|
|
214
|
+
'@version': 'EP-ASSURANCE-CONTEXT-v1',
|
|
215
|
+
receipt_id: payload.receipt_id,
|
|
216
|
+
claim_hash: `sha256:${sha256Hex(canon(payload.claim))}`,
|
|
217
|
+
};
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
function classASignoff(digest, {
|
|
221
|
+
keyId = 'ep:key:eg1:class-a',
|
|
222
|
+
keyPair = approverA,
|
|
223
|
+
approver = 'ep:approver:eg1:cfo',
|
|
224
|
+
} = {}) {
|
|
225
|
+
const challenge = Buffer.from(digest).toString('base64url');
|
|
226
|
+
const clientDataJSON = Buffer.from(JSON.stringify({ type: 'webauthn.get', challenge, origin: RP_ORIGIN }), 'utf8');
|
|
227
|
+
const rpIdHash = crypto.createHash('sha256').update(RP_ID).digest();
|
|
228
|
+
const authData = Buffer.concat([rpIdHash, Buffer.from([0x05]), Buffer.from([0, 0, 0, 0])]); // UP + UV
|
|
229
|
+
const signedData = Buffer.concat([authData, sha256Bytes(clientDataJSON)]);
|
|
230
|
+
return {
|
|
231
|
+
approver,
|
|
232
|
+
approver_key_id: keyId,
|
|
233
|
+
key_class: 'A',
|
|
234
|
+
webauthn: {
|
|
235
|
+
authenticator_data: authData.toString('base64url'),
|
|
236
|
+
client_data_json: clientDataJSON.toString('base64url'),
|
|
237
|
+
signature: crypto.sign('sha256', signedData, keyPair.privateKey).toString('base64url'),
|
|
238
|
+
},
|
|
239
|
+
};
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
function softwareSignoff(digest) {
|
|
243
|
+
return {
|
|
244
|
+
approver: 'ep:approver:eg1:controller',
|
|
245
|
+
approver_key_id: 'ep:key:eg1:controller',
|
|
246
|
+
key_class: 'B',
|
|
247
|
+
signature: crypto.sign(null, digest, approverB.privateKey).toString('base64url'),
|
|
248
|
+
};
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
function assuranceProof(payload, quorum) {
|
|
252
|
+
const context = assuranceContext(payload);
|
|
253
|
+
const contextHash = `sha256:${sha256Hex(canon(context))}`;
|
|
254
|
+
const digest = Buffer.from(contextHash.replace(/^sha256:/, ''), 'hex');
|
|
255
|
+
const threshold = Number(quorum?.threshold ?? quorum?.m ?? 1);
|
|
256
|
+
const signoffs = [classASignoff(digest)];
|
|
257
|
+
if (threshold >= 2) {
|
|
258
|
+
signoffs.push(classASignoff(digest, {
|
|
259
|
+
keyId: 'ep:key:eg1:class-a-2',
|
|
260
|
+
keyPair: approverA2,
|
|
261
|
+
approver: 'ep:approver:eg1:security-officer',
|
|
262
|
+
}));
|
|
263
|
+
}
|
|
264
|
+
return {
|
|
265
|
+
'@version': 'EP-ASSURANCE-PROOF-v1',
|
|
266
|
+
context_hash: contextHash,
|
|
267
|
+
threshold: threshold >= 2 ? threshold : 1,
|
|
268
|
+
signoffs,
|
|
269
|
+
};
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
/**
|
|
273
|
+
* Mint a scenario receipt.
|
|
274
|
+
* @param {object} o
|
|
275
|
+
* @param {'allow'|'allow_with_signoff'} [o.outcome] 'allow'=software; 'allow_with_signoff'
|
|
276
|
+
* embeds a REAL WebAuthn device signoff so the receipt cryptographically earns class_a.
|
|
277
|
+
* @param {object|boolean} [o.quorum] request quorum-tier evidence. Truthy -> a REAL
|
|
278
|
+
* EP-QUORUM-v1 doc (distinct humans + distinct keys + per-signer assertions). If an
|
|
279
|
+
* object with `threshold`/`signers`, its size sets the quorum size.
|
|
280
|
+
* @param {boolean} [o.fakeQuorum] embed an UNVERIFIABLE self-asserted quorum block
|
|
281
|
+
* ({signers,threshold}) with no per-signer signatures — used to prove the Gate
|
|
282
|
+
* REFUSES it (must NOT be credited quorum). For adversarial tests only.
|
|
283
|
+
* @param {object} [o.tamper] fields assigned to the claim AFTER signing (breaks the signature)
|
|
284
|
+
* @param {object} [o.extra] extra fields merged into the claim before signing
|
|
285
|
+
*/
|
|
286
|
+
function mint({
|
|
287
|
+
outcome = 'allow_with_signoff',
|
|
288
|
+
quorum = null,
|
|
289
|
+
fakeQuorum = false,
|
|
290
|
+
tamper = null,
|
|
291
|
+
extra = {},
|
|
292
|
+
}: {
|
|
293
|
+
outcome?: 'allow' | 'allow_with_signoff';
|
|
294
|
+
quorum?: { threshold?: number; signers?: any[] } | null;
|
|
295
|
+
fakeQuorum?: boolean;
|
|
296
|
+
tamper?: Record<string, any> | null;
|
|
297
|
+
extra?: Record<string, any>;
|
|
298
|
+
} = {}) {
|
|
299
|
+
const approver = outcome === 'allow'
|
|
300
|
+
? 'ep:approver:eg1:controller'
|
|
301
|
+
: 'ep:approver:eg1:cfo';
|
|
302
|
+
const claim = { ...action, outcome, approver, ...extra };
|
|
303
|
+
const payload: {
|
|
304
|
+
receipt_id: string;
|
|
305
|
+
subject: string;
|
|
306
|
+
issuer: string;
|
|
307
|
+
created_at: string;
|
|
308
|
+
claim: Record<string, any>;
|
|
309
|
+
quorum?: any;
|
|
310
|
+
assurance_proof?: any;
|
|
311
|
+
signoff?: any;
|
|
312
|
+
approver_public_key?: string;
|
|
313
|
+
} = {
|
|
314
|
+
receipt_id: `${idPrefix}_${++counter}`,
|
|
315
|
+
subject: 'agent:eg1-conformance',
|
|
316
|
+
issuer: 'ep:org:eg1',
|
|
317
|
+
created_at: new Date(nowMs()).toISOString(),
|
|
318
|
+
claim,
|
|
319
|
+
};
|
|
320
|
+
if (fakeQuorum) {
|
|
321
|
+
// Self-asserted ONLY — NO members / NO signatures / NO pinned proof. The
|
|
322
|
+
// Gate must refuse to credit this as quorum (assurance_too_low). For
|
|
323
|
+
// adversarial tests that prove payload claims are never trusted.
|
|
324
|
+
payload.quorum = { signers: ['ep:a', 'ep:b'], threshold: 2 };
|
|
325
|
+
} else if (outcome === 'allow_with_signoff' || quorum) {
|
|
326
|
+
// Genuine, per-signer-verifiable evidence. The PRIMARY proof is the pinned
|
|
327
|
+
// EP-ASSURANCE-PROOF-v1 (verified against the harness's pinned approverKeys),
|
|
328
|
+
// which is what the EG-1 gate/custody path checks. We ALSO embed self-contained
|
|
329
|
+
// evidence (EP-QUORUM-v1 / WebAuthn device signoff) so a relying party that
|
|
330
|
+
// does NOT pin keys can still cryptographically credit the tier (DoD audit fix).
|
|
331
|
+
payload.assurance_proof = assuranceProof(payload, quorum);
|
|
332
|
+
if (quorum) {
|
|
333
|
+
const threshold = Number.isInteger(quorum.threshold) ? quorum.threshold
|
|
334
|
+
: (Array.isArray(quorum.signers) ? quorum.signers.length : 2);
|
|
335
|
+
payload.quorum = mintQuorumEvidence({
|
|
336
|
+
actionHash,
|
|
337
|
+
threshold,
|
|
338
|
+
approvers: quorumPolicy.approvers.slice(0, threshold),
|
|
339
|
+
issuedAtMs: nowMs(),
|
|
340
|
+
});
|
|
341
|
+
} else {
|
|
342
|
+
const s = mintDeviceSignoff({ actionHash, approver, issuedAtMs: nowMs() });
|
|
343
|
+
payload.signoff = s.signoff;
|
|
344
|
+
payload.approver_public_key = s.approver_public_key;
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
const value = crypto.sign(null, Buffer.from(canon(payload), 'utf8'), privateKey).toString('base64url');
|
|
348
|
+
const receipt = { '@version': 'EP-RECEIPT-v1', payload, signature: { algorithm: 'Ed25519', value } };
|
|
349
|
+
if (tamper) Object.assign(receipt.payload.claim, tamper); // tamper AFTER signing -> signature no longer binds
|
|
350
|
+
return receipt;
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
return { publicKey: pub, approverKeys, quorumPolicy, mint, action, actionHash, now: nowMs, rpId: RP_ID, allowedOrigins: [RP_ORIGIN] };
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
/**
|
|
357
|
+
* Adapt an @emilia-protocol/gate instance into an EG-1 `invoke`. The gate must
|
|
358
|
+
* have been built trusting the harness public key. Uses gate.run() so the
|
|
359
|
+
* execution proof + reliance packet are produced on the allowed path.
|
|
360
|
+
*/
|
|
361
|
+
export function makeGateInvoke(gate, { selector = EG1_DEFAULT_SELECTOR, action = EG1_DEFAULT_ACTION } = {}) {
|
|
362
|
+
if (!gate || typeof gate.run !== 'function') {
|
|
363
|
+
throw new Error('makeGateInvoke requires an EMILIA Gate instance (with .run)');
|
|
364
|
+
}
|
|
365
|
+
return async ({ receipt, observedAction }) => {
|
|
366
|
+
const out = await gate.run(
|
|
367
|
+
{ selector, receipt, observedAction: observedAction ?? action },
|
|
368
|
+
async () => ({ eg1: 'side-effect-ran' }),
|
|
369
|
+
);
|
|
370
|
+
if (!out.ok) {
|
|
371
|
+
return { allowed: false, status: out.status, reason: out.authorization?.reason ?? 'refused' };
|
|
372
|
+
}
|
|
373
|
+
return {
|
|
374
|
+
allowed: true,
|
|
375
|
+
status: 200,
|
|
376
|
+
reason: out.authorization?.reason ?? 'allow',
|
|
377
|
+
decisionHash: out.authorization?.evidence?.hash ?? null,
|
|
378
|
+
execution: out.execution ?? null,
|
|
379
|
+
packet: out.packet ?? null,
|
|
380
|
+
};
|
|
381
|
+
};
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
const pick = (r) => ({ allowed: !!r.allowed, status: r.status ?? null, reason: r.reason ?? null });
|
|
385
|
+
|
|
386
|
+
/**
|
|
387
|
+
* Drive a subject through the eight EG-1 checks and return a JSON report.
|
|
388
|
+
* @param {object} [o]
|
|
389
|
+
* @param {(scenario:object)=>Promise<object>} [o.invoke] the integration under test
|
|
390
|
+
* @param {object} [o.harness] from createEg1Harness()
|
|
391
|
+
* @param {object} [o.action] the high-risk action (defaults to the harness action)
|
|
392
|
+
*/
|
|
393
|
+
export async function runEg1({
|
|
394
|
+
invoke,
|
|
395
|
+
harness,
|
|
396
|
+
action,
|
|
397
|
+
}: {
|
|
398
|
+
invoke?: (scenario: { receipt: any; observedAction: any }) => Promise<{
|
|
399
|
+
allowed: boolean;
|
|
400
|
+
status?: number | null;
|
|
401
|
+
reason?: string | null;
|
|
402
|
+
decisionHash?: string | null;
|
|
403
|
+
execution?: { authorizes_decision?: string | null } | null;
|
|
404
|
+
packet?: { verdict?: string | null } | null;
|
|
405
|
+
}>;
|
|
406
|
+
harness?: ReturnType<typeof createEg1Harness>;
|
|
407
|
+
action?: Record<string, any>;
|
|
408
|
+
} = {}) {
|
|
409
|
+
if (typeof invoke !== 'function') throw new Error('runEg1 requires an invoke(scenario) function');
|
|
410
|
+
if (!harness || typeof harness.mint !== 'function') throw new Error('runEg1 requires a harness from createEg1Harness()');
|
|
411
|
+
const act = action || harness.action || EG1_DEFAULT_ACTION;
|
|
412
|
+
const observed = act;
|
|
413
|
+
const drift = { ...act, amount_usd: Number(act.amount_usd ?? 0) + 1 };
|
|
414
|
+
|
|
415
|
+
const results = {};
|
|
416
|
+
const set = (id, pass, observed_) => { results[id] = { pass: !!pass, observed: observed_ }; };
|
|
417
|
+
|
|
418
|
+
// 1. missing receipt → 428
|
|
419
|
+
let r = await invoke({ receipt: null, observedAction: observed });
|
|
420
|
+
set('missing_receipt_refused', !r.allowed && r.status === 428, pick(r));
|
|
421
|
+
|
|
422
|
+
// 2. software receipt on a Class-A action → refused
|
|
423
|
+
r = await invoke({ receipt: harness.mint({ outcome: 'allow' }), observedAction: observed });
|
|
424
|
+
set('software_on_classA_refused', !r.allowed && /assurance/i.test(r.reason || ''), pick(r));
|
|
425
|
+
|
|
426
|
+
// 3. observed execution drift → refused
|
|
427
|
+
r = await invoke({ receipt: harness.mint({ outcome: 'allow_with_signoff' }), observedAction: drift });
|
|
428
|
+
set('execution_drift_refused', !r.allowed && /binding/i.test(r.reason || ''), pick(r));
|
|
429
|
+
|
|
430
|
+
// 4. valid Class-A receipt → runs (capture for 5/7/8)
|
|
431
|
+
const valid = harness.mint({ outcome: 'allow_with_signoff' });
|
|
432
|
+
r = await invoke({ receipt: valid, observedAction: observed });
|
|
433
|
+
const validAllowed = r.allowed === true;
|
|
434
|
+
set('valid_classA_runs', validAllowed, pick(r));
|
|
435
|
+
|
|
436
|
+
// 7. execution proof binds to the authorization decision
|
|
437
|
+
const boundOk = validAllowed
|
|
438
|
+
&& !!r.execution && !!r.execution.authorizes_decision
|
|
439
|
+
&& !!r.decisionHash && r.execution.authorizes_decision === r.decisionHash;
|
|
440
|
+
set('execution_proof_binds', boundOk, {
|
|
441
|
+
authorizes_decision: r.execution?.authorizes_decision ?? null,
|
|
442
|
+
decision_hash: r.decisionHash ?? null,
|
|
443
|
+
});
|
|
444
|
+
|
|
445
|
+
// 8. reliance packet verdict "rely"
|
|
446
|
+
set('reliance_packet_rely', validAllowed && String(r.packet?.verdict || '').toLowerCase() === 'rely', {
|
|
447
|
+
verdict: r.packet?.verdict ?? null,
|
|
448
|
+
});
|
|
449
|
+
|
|
450
|
+
// 5. same receipt replay → refused
|
|
451
|
+
r = await invoke({ receipt: valid, observedAction: observed });
|
|
452
|
+
set('replay_refused', !r.allowed && /replay/i.test(r.reason || ''), pick(r));
|
|
453
|
+
|
|
454
|
+
// 6. tampered receipt → refused
|
|
455
|
+
r = await invoke({ receipt: harness.mint({ outcome: 'allow_with_signoff', tamper: { amount_usd: 9_999_999 } }), observedAction: observed });
|
|
456
|
+
set('tampered_refused', !r.allowed && r.status === 428, pick(r));
|
|
457
|
+
|
|
458
|
+
const checks = EG1_CHECKS.map((c) => ({ id: c.id, title: c.title, ...results[c.id] }));
|
|
459
|
+
const passedCount = checks.filter((c) => c.pass).length;
|
|
460
|
+
const passed = passedCount === checks.length;
|
|
461
|
+
return {
|
|
462
|
+
standard: EG1_VERSION,
|
|
463
|
+
passed,
|
|
464
|
+
badge: passed ? 'EG-1 Enforced' : 'EG-1 not earned',
|
|
465
|
+
summary: { passed: passedCount, total: checks.length },
|
|
466
|
+
checks,
|
|
467
|
+
generated_at: new Date(harness.now ? harness.now() : Date.now()).toISOString(),
|
|
468
|
+
};
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
export default { EG1_VERSION, EG1_CHECKS, EG1_DEFAULT_ACTION, EG1_DEFAULT_SELECTOR, createEg1Harness, makeGateInvoke, runEg1, mintDeviceSignoff, mintQuorumEvidence };
|
|
@@ -0,0 +1,195 @@
|
|
|
1
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
2
|
+
/**
|
|
3
|
+
* EMILIA Gate — enterprise entitlement layer (EP-GATE-ENTITLEMENT-v1).
|
|
4
|
+
*
|
|
5
|
+
* The license key IS an EP-style artifact: a signed entitlement — Ed25519 over
|
|
6
|
+
* canonical JSON (sorted keys, same idiom as receipts/evidence) of
|
|
7
|
+
* { org, tier, features[], limits, not_before, expires_at, kid }. Verifiers pin
|
|
8
|
+
* issuer keys by kid; nothing in the artifact is trusted until the signature
|
|
9
|
+
* verifies against a pinned key.
|
|
10
|
+
*
|
|
11
|
+
* OPEN-CORE SEMANTICS — two different fail directions, by design:
|
|
12
|
+
* - The CORE gate is never bricked. No entitlement, an expired one, a
|
|
13
|
+
* tampered one, an unknown kid — all resolve to { valid:false,
|
|
14
|
+
* tier:'community' } with a machine-readable reason. Community tier always
|
|
15
|
+
* works; a licensing failure can never block the firewall itself.
|
|
16
|
+
* - Enterprise FEATURES fail closed. `requireFeature` returns true ONLY for
|
|
17
|
+
* a cryptographically valid, in-window entitlement that explicitly lists
|
|
18
|
+
* the feature. Everything else — including community fallback — is false.
|
|
19
|
+
*
|
|
20
|
+
* Pure functions: inputs in, verdict out. Time is injected (`now`), never read
|
|
21
|
+
* from the wall clock implicitly, so verification is deterministic.
|
|
22
|
+
*/
|
|
23
|
+
import crypto from 'node:crypto';
|
|
24
|
+
import { strictJsonGate } from './strict-json.js';
|
|
25
|
+
|
|
26
|
+
export const ENTITLEMENT_VERSION = 'EP-GATE-ENTITLEMENT-v1';
|
|
27
|
+
export const ENTITLEMENT_TIERS = ['community', 'team', 'business', 'enterprise', 'regulated'];
|
|
28
|
+
|
|
29
|
+
/** The tier every failure path resolves to — the gate keeps working on it. */
|
|
30
|
+
const COMMUNITY = 'community';
|
|
31
|
+
|
|
32
|
+
/** Canonical JSON (recursive sorted keys) — matches @emilia-protocol/verify. */
|
|
33
|
+
function canonical(v) {
|
|
34
|
+
if (v === null || v === undefined) return JSON.stringify(v);
|
|
35
|
+
if (Array.isArray(v)) return `[${v.map(canonical).join(',')}]`;
|
|
36
|
+
if (typeof v === 'object') {
|
|
37
|
+
return `{${Object.keys(v).sort().map((k) => JSON.stringify(k) + ':' + canonical(v[k])).join(',')}}`;
|
|
38
|
+
}
|
|
39
|
+
return JSON.stringify(v);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function toMs(t) {
|
|
43
|
+
if (t == null) return null;
|
|
44
|
+
const ms = typeof t === 'number' ? t : Date.parse(t);
|
|
45
|
+
return Number.isFinite(ms) ? ms : null;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/** Community fallback — every refusal shape is identical and machine-readable. */
|
|
49
|
+
function community(reason, extra = {}) {
|
|
50
|
+
return { valid: false, tier: COMMUNITY, features: [], limits: null, reason, ...extra };
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Mint a signed entitlement (test/ops helper — the licensing service, not the
|
|
55
|
+
* verifier, holds the private key). Throws on invalid fields: a malformed
|
|
56
|
+
* license must never be issued, only refused.
|
|
57
|
+
* @param {crypto.KeyObject} privateKey Ed25519 private key
|
|
58
|
+
* @param {object} fields { org, tier, features?, limits?, not_before, expires_at, kid }
|
|
59
|
+
* @returns {{ '@version': string, payload: object, signature: { algorithm: 'Ed25519', value: string } }}
|
|
60
|
+
*/
|
|
61
|
+
export function mintEntitlement(privateKey, {
|
|
62
|
+
org, tier, features = [], limits = {}, not_before, expires_at, kid,
|
|
63
|
+
}: {
|
|
64
|
+
org: string;
|
|
65
|
+
tier: string;
|
|
66
|
+
features?: string[];
|
|
67
|
+
limits?: Record<string, any>;
|
|
68
|
+
not_before: string | number;
|
|
69
|
+
expires_at: string | number;
|
|
70
|
+
kid: string;
|
|
71
|
+
}) {
|
|
72
|
+
if (!org || typeof org !== 'string') throw new Error('entitlement: org is required');
|
|
73
|
+
if (!ENTITLEMENT_TIERS.includes(tier)) {
|
|
74
|
+
throw new Error(`entitlement: unknown tier "${tier}" (expected one of ${ENTITLEMENT_TIERS.join('|')})`);
|
|
75
|
+
}
|
|
76
|
+
if (!Array.isArray(features) || features.some((f) => typeof f !== 'string')) {
|
|
77
|
+
throw new Error('entitlement: features must be an array of strings');
|
|
78
|
+
}
|
|
79
|
+
if (!limits || typeof limits !== 'object' || Array.isArray(limits)) {
|
|
80
|
+
throw new Error('entitlement: limits must be an object');
|
|
81
|
+
}
|
|
82
|
+
if (toMs(not_before) == null) throw new Error('entitlement: not_before is required (ISO or ms)');
|
|
83
|
+
if (toMs(expires_at) == null) throw new Error('entitlement: expires_at is required (ISO or ms)');
|
|
84
|
+
if (!kid || typeof kid !== 'string') throw new Error('entitlement: kid is required');
|
|
85
|
+
// Snapshot features/limits into the signed payload: embedding the caller's live
|
|
86
|
+
// array/object would let a licensing service mutate them after minting and
|
|
87
|
+
// diverge the entitlement from its signature.
|
|
88
|
+
const payload = { org, tier, features: structuredClone(features), limits: structuredClone(limits), not_before, expires_at, kid };
|
|
89
|
+
const value = crypto.sign(null, Buffer.from(canonical(payload), 'utf8'), privateKey).toString('base64url');
|
|
90
|
+
return { '@version': ENTITLEMENT_VERSION, payload, signature: { algorithm: 'Ed25519', value } };
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/** Resolve a base64url SPKI-DER key for `kid` from a map or an entry list. */
|
|
94
|
+
function issuerKeyFor(issuerKeys, kid) {
|
|
95
|
+
if (!issuerKeys) return null;
|
|
96
|
+
if (Array.isArray(issuerKeys)) {
|
|
97
|
+
const e = issuerKeys.find((x) => x && x.kid === kid && typeof x.key === 'string');
|
|
98
|
+
return e ? e.key : null;
|
|
99
|
+
}
|
|
100
|
+
const k = issuerKeys[kid];
|
|
101
|
+
return typeof k === 'string' ? k : null;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* Verify an entitlement. NEVER throws for a bad artifact — every failure
|
|
106
|
+
* resolves to the community tier with a machine-readable reason, so a licensing
|
|
107
|
+
* problem degrades gracefully instead of bricking the gate. Enterprise features
|
|
108
|
+
* remain gated by `requireFeature`, which fails closed on any non-valid result.
|
|
109
|
+
*
|
|
110
|
+
* @param {object|string|null} entitlementJson the artifact (object or JSON string); absence -> community
|
|
111
|
+
* @param {object} [o]
|
|
112
|
+
* @param {object|Array<{kid:string,key:string}>} [o.issuerKeys] pinned kid -> base64url SPKI-DER public key
|
|
113
|
+
* @param {number|string|function} [o.now=Date.now] injected clock (ms, ISO, or () => ms)
|
|
114
|
+
* @returns {{ valid: boolean, tier: string, features: string[], limits: object|null, reason: string, org?: string, kid?: string, not_before?: any, expires_at?: any }}
|
|
115
|
+
*/
|
|
116
|
+
export function verifyEntitlement(entitlementJson, {
|
|
117
|
+
issuerKeys,
|
|
118
|
+
now = Date.now,
|
|
119
|
+
}: {
|
|
120
|
+
issuerKeys?: Record<string, string> | Array<{ kid: string; key: string }>;
|
|
121
|
+
now?: number | string | (() => number);
|
|
122
|
+
} = {}) {
|
|
123
|
+
// Absence is NOT an error: the open-core floor. Community keeps working.
|
|
124
|
+
if (entitlementJson == null || entitlementJson === '') return community('no_entitlement');
|
|
125
|
+
|
|
126
|
+
let doc = entitlementJson;
|
|
127
|
+
if (typeof doc === 'string') {
|
|
128
|
+
try {
|
|
129
|
+
if (Buffer.byteLength(doc, 'utf8') > 1024 * 1024 || !strictJsonGate(doc).ok) return community('entitlement_unparseable');
|
|
130
|
+
doc = JSON.parse(doc);
|
|
131
|
+
} catch { return community('entitlement_unparseable'); }
|
|
132
|
+
}
|
|
133
|
+
if (!doc || typeof doc !== 'object' || Array.isArray(doc)) return community('entitlement_malformed');
|
|
134
|
+
if (doc['@version'] !== ENTITLEMENT_VERSION) return community('unsupported_version');
|
|
135
|
+
|
|
136
|
+
const p = doc.payload;
|
|
137
|
+
const sig = doc.signature;
|
|
138
|
+
if (!p || typeof p !== 'object' || !sig || typeof sig !== 'object') return community('entitlement_malformed');
|
|
139
|
+
if (sig.algorithm !== 'Ed25519' || typeof sig.value !== 'string') return community('unsupported_algorithm');
|
|
140
|
+
if (!ENTITLEMENT_TIERS.includes(p.tier)) return community('unknown_tier');
|
|
141
|
+
if (!Array.isArray(p.features) || p.features.some((f) => typeof f !== 'string')) return community('entitlement_malformed');
|
|
142
|
+
|
|
143
|
+
// Issuer pinning: the kid must resolve to a PINNED key. An entitlement can
|
|
144
|
+
// never nominate its own key — unknown kid (or no pins at all) fails closed.
|
|
145
|
+
const keyB64 = issuerKeyFor(issuerKeys, p.kid);
|
|
146
|
+
if (!keyB64) return community('unknown_kid', { kid: p.kid ?? null });
|
|
147
|
+
|
|
148
|
+
let ok = false;
|
|
149
|
+
try {
|
|
150
|
+
const pub = crypto.createPublicKey({ key: Buffer.from(keyB64, 'base64url'), format: 'der', type: 'spki' });
|
|
151
|
+
ok = crypto.verify(null, Buffer.from(canonical(p), 'utf8'), pub, Buffer.from(sig.value, 'base64url'));
|
|
152
|
+
} catch { ok = false; }
|
|
153
|
+
// One reason covers both tampering and a wrong key: the signature does not
|
|
154
|
+
// verify against the pinned key for this kid.
|
|
155
|
+
if (!ok) return community('bad_signature', { kid: p.kid });
|
|
156
|
+
|
|
157
|
+
// Validity window — checked only AFTER the signature, so the timestamps
|
|
158
|
+
// themselves are authenticated. Both bounds are required; an unparseable
|
|
159
|
+
// window fails closed.
|
|
160
|
+
const nowMs = typeof now === 'function' ? now() : toMs(now);
|
|
161
|
+
const nbf = toMs(p.not_before);
|
|
162
|
+
const exp = toMs(p.expires_at);
|
|
163
|
+
if (nbf == null || exp == null || nowMs == null) return community('invalid_validity_window', { kid: p.kid });
|
|
164
|
+
if (nowMs < nbf) return community('not_yet_valid', { kid: p.kid, not_before: p.not_before });
|
|
165
|
+
if (nowMs > exp) return community('expired', { kid: p.kid, expires_at: p.expires_at });
|
|
166
|
+
|
|
167
|
+
return {
|
|
168
|
+
valid: true,
|
|
169
|
+
tier: p.tier,
|
|
170
|
+
features: p.features.slice(),
|
|
171
|
+
limits: (p.limits && typeof p.limits === 'object') ? { ...p.limits } : {},
|
|
172
|
+
reason: 'entitlement_verified',
|
|
173
|
+
org: p.org,
|
|
174
|
+
kid: p.kid,
|
|
175
|
+
not_before: p.not_before,
|
|
176
|
+
expires_at: p.expires_at,
|
|
177
|
+
};
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
/**
|
|
181
|
+
* Is `feature` licensed? FAIL CLOSED: true only for a valid entitlement that
|
|
182
|
+
* explicitly lists the feature. Community fallback, invalid/expired/tampered
|
|
183
|
+
* artifacts, and unlisted features are all false — no enterprise code path
|
|
184
|
+
* runs without a live license naming it.
|
|
185
|
+
* @param {object} verified the result of verifyEntitlement
|
|
186
|
+
* @param {string} feature
|
|
187
|
+
* @returns {boolean}
|
|
188
|
+
*/
|
|
189
|
+
export function requireFeature(verified, feature) {
|
|
190
|
+
if (!verified || verified.valid !== true) return false;
|
|
191
|
+
if (typeof feature !== 'string' || feature.length === 0) return false;
|
|
192
|
+
return Array.isArray(verified.features) && verified.features.includes(feature);
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
export default { mintEntitlement, verifyEntitlement, requireFeature, ENTITLEMENT_VERSION, ENTITLEMENT_TIERS };
|