@emilia-protocol/gate 0.9.5 → 0.11.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 +103 -0
- package/LICENSE +190 -0
- package/README.md +95 -16
- package/action-control-manifest.js +26 -0
- package/action-escrow-custodian.js +395 -0
- package/action-escrow-evidence.js +859 -0
- package/action-escrow-package.js +1669 -0
- package/action-escrow-postgres.js +338 -0
- package/action-escrow-state.js +397 -0
- package/action-escrow-verifiers.js +332 -0
- package/action-escrow.js +2448 -0
- package/adapters/_kit.js +47 -5
- package/adapters/aws.js +15 -4
- package/adapters/github-demo.mjs +25 -22
- package/adapters/github.js +1 -1
- package/adapters/jira.js +1 -0
- package/adapters/linear.js +1 -0
- package/adapters/salesforce.js +1 -0
- package/adapters/stripe.js +1 -1
- package/adapters/supabase.js +26 -4
- package/adapters/vercel.js +33 -4
- package/aec-execution.js +1 -3
- package/breakglass.js +285 -51
- package/control-plane.js +341 -0
- package/coverage.js +722 -0
- package/custody-demo.mjs +13 -3
- package/demo.mjs +9 -3
- package/deploy/helm/README.md +16 -8
- package/deploy/helm/emilia-gate/Chart.yaml +3 -1
- package/deploy/helm/emilia-gate/templates/NOTES.txt +3 -2
- package/deploy/helm/emilia-gate/templates/deployment.yaml +55 -1
- package/deploy/helm/emilia-gate/values.yaml +18 -2
- package/deploy/helm/emilia-gate-service/Chart.yaml +11 -0
- package/deploy/helm/emilia-gate-service/README.md +81 -0
- package/deploy/helm/emilia-gate-service/templates/NOTES.txt +12 -0
- package/deploy/helm/emilia-gate-service/templates/_helpers.tpl +83 -0
- package/deploy/helm/emilia-gate-service/templates/deployment.yaml +153 -0
- package/deploy/helm/emilia-gate-service/templates/migration-job.yaml +73 -0
- package/deploy/helm/emilia-gate-service/templates/networkpolicy.yaml +113 -0
- package/deploy/helm/emilia-gate-service/templates/pdb.yaml +14 -0
- package/deploy/helm/emilia-gate-service/templates/service.yaml +20 -0
- package/deploy/helm/emilia-gate-service/templates/serviceaccount.yaml +8 -0
- package/deploy/helm/emilia-gate-service/tests/fixtures/001_gate.sql +26 -0
- package/deploy/helm/emilia-gate-service/tests/fixtures/002_runtime_access.sql +32 -0
- package/deploy/helm/emilia-gate-service/tests/fixtures/gate.config.mjs +29 -0
- package/deploy/helm/emilia-gate-service/tests/render-check.sh +145 -0
- package/deploy/helm/emilia-gate-service/values.schema.json +145 -0
- package/deploy/helm/emilia-gate-service/values.yaml +166 -0
- package/deploy/sql/001-runtime.sql +707 -0
- package/deploy/terraform/README.md +24 -14
- package/deploy/terraform/main.tf +59 -0
- package/deploy/terraform/service/README.md +81 -0
- package/deploy/terraform/service/main.tf +718 -0
- package/deploy/terraform/service/outputs.tf +19 -0
- package/deploy/terraform/service/tests/validate.sh +24 -0
- package/deploy/terraform/service/tests/validation.tftest.hcl +168 -0
- package/deploy/terraform/service/variables.tf +459 -0
- package/deploy/terraform/service/versions.tf +10 -0
- package/deploy/terraform/variables.tf +95 -2
- package/deployment-attestation.js +248 -0
- package/eg1-conformance.js +46 -12
- package/enterprise.js +6 -2
- package/ep-assure.mjs +3 -2
- package/evidence-postgres.js +357 -0
- package/evidence.js +10 -3
- package/execution-binding.js +141 -26
- package/index.js +556 -115
- package/key-registry.js +51 -19
- package/mcp.js +4 -2
- package/network-witness.js +500 -0
- package/package.json +39 -5
- package/reliance-kernel.js +5 -6
- package/reliance-packet.js +43 -10
- package/reports/assurance-package.js +45 -19
- package/reports/reperform.js +78 -18
- package/reports/underwriter.js +1 -1
- package/settlement.js +300 -0
- package/store-postgres.js +14 -0
- package/store.js +26 -5
- package/strict-json.js +103 -0
- package/witness-postgres.js +97 -0
package/package.json
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@emilia-protocol/gate",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.11.0",
|
|
4
4
|
"repository": {
|
|
5
5
|
"type": "git",
|
|
6
6
|
"url": "https://github.com/emiliaprotocol/emilia-protocol.git",
|
|
7
7
|
"directory": "packages/gate"
|
|
8
8
|
},
|
|
9
|
-
"description": "EMILIA Gate \u2014 the
|
|
9
|
+
"description": "EMILIA Gate \u2014 the Consequence Firewall. Deny-by-default enforcement for consequential machine actions: an action runs only with a valid, in-scope, sufficiently-assured, non-replayed EMILIA authorization receipt (proof a named human authorized this exact action). Composes require-receipt + verify; adds assurance-tier enforcement, one-time consumption (replay defense), a tamper-evident evidence log, default high-risk action packs, execution-field binding, reliance packets, an MCP drop-in, a GitHub system-of-record adapter, and EG-1 conformance. Framework-agnostic, fails closed. Reference implementation, experimental.",
|
|
10
10
|
"license": "Apache-2.0",
|
|
11
11
|
"type": "module",
|
|
12
12
|
"main": "index.js",
|
|
@@ -31,6 +31,11 @@
|
|
|
31
31
|
"./package.json": "./package.json",
|
|
32
32
|
"./enterprise": "./enterprise.js",
|
|
33
33
|
"./metering": "./metering.js",
|
|
34
|
+
"./network-witness": "./network-witness.js",
|
|
35
|
+
"./deployment-attestation": "./deployment-attestation.js",
|
|
36
|
+
"./coverage": "./coverage.js",
|
|
37
|
+
"./settlement": "./settlement.js",
|
|
38
|
+
"./control-plane": "./control-plane.js",
|
|
34
39
|
"./siem": "./siem.js",
|
|
35
40
|
"./roster": "./roster.js",
|
|
36
41
|
"./reports/art14": "./reports/art14.js",
|
|
@@ -39,7 +44,16 @@
|
|
|
39
44
|
"./breakglass": "./breakglass.js",
|
|
40
45
|
"./challenge-store": "./challenge-store.js",
|
|
41
46
|
"./store-postgres": "./store-postgres.js",
|
|
47
|
+
"./evidence-postgres": "./evidence-postgres.js",
|
|
48
|
+
"./witness-postgres": "./witness-postgres.js",
|
|
42
49
|
"./evidence": "./evidence.js",
|
|
50
|
+
"./action-escrow": "./action-escrow.js",
|
|
51
|
+
"./action-escrow-evidence": "./action-escrow-evidence.js",
|
|
52
|
+
"./action-escrow-state": "./action-escrow-state.js",
|
|
53
|
+
"./action-escrow-postgres": "./action-escrow-postgres.js",
|
|
54
|
+
"./action-escrow-verifiers": "./action-escrow-verifiers.js",
|
|
55
|
+
"./action-escrow-custodian": "./action-escrow-custodian.js",
|
|
56
|
+
"./action-escrow-package": "./action-escrow-package.js",
|
|
43
57
|
"./reports/auditor-workpaper": "./reports/auditor-workpaper.js",
|
|
44
58
|
"./reports/reperform": "./reports/reperform.js",
|
|
45
59
|
"./reports/external-verification": "./reports/external-verification.js",
|
|
@@ -52,9 +66,20 @@
|
|
|
52
66
|
},
|
|
53
67
|
"files": [
|
|
54
68
|
"index.js",
|
|
69
|
+
"strict-json.js",
|
|
55
70
|
"store.js",
|
|
56
71
|
"challenge-store.js",
|
|
72
|
+
"store-postgres.js",
|
|
73
|
+
"evidence-postgres.js",
|
|
74
|
+
"witness-postgres.js",
|
|
57
75
|
"evidence.js",
|
|
76
|
+
"action-escrow.js",
|
|
77
|
+
"action-escrow-evidence.js",
|
|
78
|
+
"action-escrow-state.js",
|
|
79
|
+
"action-escrow-postgres.js",
|
|
80
|
+
"action-escrow-verifiers.js",
|
|
81
|
+
"action-escrow-custodian.js",
|
|
82
|
+
"action-escrow-package.js",
|
|
58
83
|
"action-packs.js",
|
|
59
84
|
"action-control-manifest.js",
|
|
60
85
|
"execution-binding.js",
|
|
@@ -86,16 +111,22 @@
|
|
|
86
111
|
"adapters/salesforce.js",
|
|
87
112
|
"demo.mjs",
|
|
88
113
|
"custody-demo.mjs",
|
|
114
|
+
"LICENSE",
|
|
115
|
+
"CHANGELOG.md",
|
|
89
116
|
"README.md",
|
|
90
117
|
"enterprise.js",
|
|
91
118
|
"metering.js",
|
|
119
|
+
"network-witness.js",
|
|
120
|
+
"deployment-attestation.js",
|
|
121
|
+
"coverage.js",
|
|
122
|
+
"settlement.js",
|
|
123
|
+
"control-plane.js",
|
|
92
124
|
"siem.js",
|
|
93
125
|
"roster.js",
|
|
94
126
|
"reports/art14.js",
|
|
95
127
|
"reports/underwriter.js",
|
|
96
128
|
"metrics.js",
|
|
97
129
|
"breakglass.js",
|
|
98
|
-
"store-postgres.js",
|
|
99
130
|
"deploy/helm/emilia-gate/Chart.yaml",
|
|
100
131
|
"deploy/helm/emilia-gate/values.yaml",
|
|
101
132
|
"deploy/helm/emilia-gate/templates/deployment.yaml",
|
|
@@ -104,11 +135,14 @@
|
|
|
104
135
|
"deploy/helm/emilia-gate/templates/servicemonitor.yaml",
|
|
105
136
|
"deploy/helm/emilia-gate/templates/NOTES.txt",
|
|
106
137
|
"deploy/helm/README.md",
|
|
138
|
+
"deploy/helm/emilia-gate-service",
|
|
107
139
|
"deploy/terraform/main.tf",
|
|
108
140
|
"deploy/terraform/variables.tf",
|
|
109
141
|
"deploy/terraform/outputs.tf",
|
|
110
142
|
"deploy/terraform/versions.tf",
|
|
111
143
|
"deploy/terraform/README.md",
|
|
144
|
+
"deploy/terraform/service",
|
|
145
|
+
"deploy/sql/001-runtime.sql",
|
|
112
146
|
"reports/auditor-workpaper.js",
|
|
113
147
|
"reports/reperform.js",
|
|
114
148
|
"reports/external-verification.js"
|
|
@@ -120,8 +154,8 @@
|
|
|
120
154
|
"cf1": "node cf1.mjs"
|
|
121
155
|
},
|
|
122
156
|
"dependencies": {
|
|
123
|
-
"@emilia-protocol/require-receipt": "
|
|
124
|
-
"@emilia-protocol/verify": "
|
|
157
|
+
"@emilia-protocol/require-receipt": "0.6.1",
|
|
158
|
+
"@emilia-protocol/verify": "3.11.0"
|
|
125
159
|
},
|
|
126
160
|
"keywords": [
|
|
127
161
|
"emilia",
|
package/reliance-kernel.js
CHANGED
|
@@ -15,15 +15,14 @@
|
|
|
15
15
|
* of its own — it enforces the one the pure offline verifier computed.
|
|
16
16
|
*/
|
|
17
17
|
import { createEvidenceLog } from './evidence.js';
|
|
18
|
+
import {
|
|
19
|
+
evaluateReliance,
|
|
20
|
+
RELIANCE_PROFILE_VERSION,
|
|
21
|
+
RELIANCE_VERDICTS,
|
|
22
|
+
} from '@emilia-protocol/verify/reliance';
|
|
18
23
|
|
|
19
24
|
const RECEIPT_REQUIRED_STATUS = 428;
|
|
20
25
|
|
|
21
|
-
// Same cross-package resolution the Gate uses: prefer the published verifier,
|
|
22
|
-
// fall back to the in-repo source so the monorepo builds without a node_modules
|
|
23
|
-
// link. evaluateReliance is pure/offline; no DB, no network.
|
|
24
|
-
const { evaluateReliance, RELIANCE_VERDICTS, RELIANCE_PROFILE_VERSION } = await import('@emilia-protocol/verify/reliance')
|
|
25
|
-
.catch(() => import('../verify/reliance.js'));
|
|
26
|
-
|
|
27
26
|
export { RELIANCE_VERDICTS, RELIANCE_PROFILE_VERSION };
|
|
28
27
|
|
|
29
28
|
/** Build the 428 refusal for a non-`rely` verdict. */
|
package/reliance-packet.js
CHANGED
|
@@ -51,17 +51,41 @@ function normalizeAdmissibility(adm) {
|
|
|
51
51
|
};
|
|
52
52
|
}
|
|
53
53
|
|
|
54
|
-
function evidenceStatus(evidence) {
|
|
55
|
-
if (!evidence)
|
|
56
|
-
|
|
57
|
-
|
|
54
|
+
async function evidenceStatus(evidence) {
|
|
55
|
+
if (!evidence) {
|
|
56
|
+
return { ok: false, length: null, head: null, reason: 'evidence_verification_unavailable' };
|
|
57
|
+
}
|
|
58
|
+
try {
|
|
59
|
+
const status = typeof evidence.verify === 'function'
|
|
60
|
+
? await evidence.verify()
|
|
61
|
+
: await evidence;
|
|
62
|
+
if (!status || typeof status !== 'object' || Array.isArray(status)) {
|
|
63
|
+
return { ok: false, length: null, head: null, reason: 'evidence_verification_malformed' };
|
|
64
|
+
}
|
|
65
|
+
if (status.ok !== true) {
|
|
66
|
+
return {
|
|
67
|
+
ok: false,
|
|
68
|
+
length: Number.isSafeInteger(status.length) ? status.length : null,
|
|
69
|
+
head: typeof status.head === 'string' ? status.head : null,
|
|
70
|
+
reason: status.reason || 'evidence_verification_rejected',
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
return {
|
|
74
|
+
...status,
|
|
75
|
+
ok: true,
|
|
76
|
+
length: Number.isSafeInteger(status.length) ? status.length : null,
|
|
77
|
+
head: typeof status.head === 'string' ? status.head : null,
|
|
78
|
+
};
|
|
79
|
+
} catch {
|
|
80
|
+
return { ok: false, length: null, head: null, reason: 'evidence_verification_failed' };
|
|
81
|
+
}
|
|
58
82
|
}
|
|
59
83
|
|
|
60
84
|
function check(id, ok, detail = null) {
|
|
61
85
|
return { id, ok, ...(detail ? { detail } : {}) };
|
|
62
86
|
}
|
|
63
87
|
|
|
64
|
-
export function buildReliancePacket({
|
|
88
|
+
export async function buildReliancePacket({
|
|
65
89
|
decision,
|
|
66
90
|
execution = null,
|
|
67
91
|
evidence = null,
|
|
@@ -70,12 +94,17 @@ export function buildReliancePacket({
|
|
|
70
94
|
admissibility = null,
|
|
71
95
|
verifier = '@emilia-protocol/gate',
|
|
72
96
|
} = {}) {
|
|
73
|
-
const evidenceCheck = evidenceStatus(evidence);
|
|
97
|
+
const evidenceCheck = await evidenceStatus(evidence);
|
|
74
98
|
const decisionHash = decision?.evidence?.hash || decision?.hash || null;
|
|
75
|
-
const executionBound =
|
|
99
|
+
const executionBound = Boolean(
|
|
100
|
+
execution
|
|
101
|
+
&& execution.kind === 'execution'
|
|
102
|
+
&& decisionHash
|
|
103
|
+
&& execution.authorizes_decision === decisionHash,
|
|
104
|
+
);
|
|
76
105
|
const bindingCheck = binding || decision?.evidence?.execution_binding || decision?.execution_binding || null;
|
|
77
106
|
const allowed = decision?.allow === true;
|
|
78
|
-
const evidenceOk = evidenceCheck.ok
|
|
107
|
+
const evidenceOk = evidenceCheck.ok === true;
|
|
79
108
|
const bindingOk = bindingCheck ? bindingCheck.ok === true : true;
|
|
80
109
|
|
|
81
110
|
// Admissibility block (relying-party evaluator output, computed OFFLINE against
|
|
@@ -96,6 +125,10 @@ export function buildReliancePacket({
|
|
|
96
125
|
action: decision?.action || null,
|
|
97
126
|
receipt_id: decision?.evidence?.receipt_id || decision?.receipt_id || null,
|
|
98
127
|
subject: decision?.evidence?.subject || null,
|
|
128
|
+
policy_id: decision?.evidence?.evaluated_policy_id || null,
|
|
129
|
+
policy_hash: decision?.evidence?.evaluated_policy_hash || null,
|
|
130
|
+
tenant_id: decision?.evidence?.evaluated_tenant_id || null,
|
|
131
|
+
approvers: decision?.evidence?.evaluated_approvers || [],
|
|
99
132
|
required_tier: decision?.evidence?.required_tier || decision?.required_tier || null,
|
|
100
133
|
observed_tier: decision?.evidence?.have_tier || decision?.have_tier || null,
|
|
101
134
|
decision_hash: decisionHash,
|
|
@@ -117,8 +150,8 @@ export function buildReliancePacket({
|
|
|
117
150
|
check('assurance_sufficient', allowed || decision?.reason !== 'assurance_too_low', decision?.reason === 'assurance_too_low' ? 'receipt tier below action requirement' : null),
|
|
118
151
|
check('receipt_one_time_consumed', allowed || decision?.reason === 'replay_refused' ? decision?.reason !== 'replay_refused' : null),
|
|
119
152
|
check('execution_fields_bound', bindingCheck ? bindingCheck.ok === true : null, bindingCheck ? { missing_observed_fields: bindingCheck.missing_observed_fields || [], mismatched_fields: bindingCheck.mismatched_fields || [] } : 'no material execution-field binding required by this action'),
|
|
120
|
-
check('execution_attests_decision',
|
|
121
|
-
check('evidence_log_intact', evidenceCheck.ok ===
|
|
153
|
+
check('execution_attests_decision', executionBound, execution ? null : 'no execution record supplied'),
|
|
154
|
+
check('evidence_log_intact', evidenceCheck.ok === true, evidenceCheck.reason || null),
|
|
122
155
|
// Admissibility verdict against the relying party's PINNED profile. null when
|
|
123
156
|
// no profile/verdict was supplied (this reliance did not gate on admissibility);
|
|
124
157
|
// true only for a recognized 'admissible' verdict; false otherwise (fail closed).
|
|
@@ -26,15 +26,11 @@
|
|
|
26
26
|
* SUPPORTS a re-performance procedure; it never issues an opinion.
|
|
27
27
|
*/
|
|
28
28
|
import { hashCanonical } from '../execution-binding.js';
|
|
29
|
+
import { evaluateReliance, RELIANCE_VERDICTS } from '@emilia-protocol/verify/reliance';
|
|
29
30
|
|
|
30
31
|
export const ASSURANCE_PACKAGE_VERSION = 'EP-ASSURANCE-PACKAGE-v1';
|
|
31
32
|
export const ASSURANCE_REPERFORMANCE_VERSION = 'EP-ASSURANCE-REPERFORMANCE-v1';
|
|
32
33
|
|
|
33
|
-
// Cross-package resolution, same pattern the Gate uses: the pure offline reliance
|
|
34
|
-
// verifier. No DB, no network.
|
|
35
|
-
const { evaluateReliance, RELIANCE_VERDICTS } = await import('@emilia-protocol/verify/reliance')
|
|
36
|
-
.catch(() => import('../../verify/reliance.js'));
|
|
37
|
-
|
|
38
34
|
export { RELIANCE_VERDICTS };
|
|
39
35
|
|
|
40
36
|
/**
|
|
@@ -70,6 +66,33 @@ function toIso(now) {
|
|
|
70
66
|
return new Date(ms == null ? 0 : ms).toISOString();
|
|
71
67
|
}
|
|
72
68
|
|
|
69
|
+
function portableJsonCopy(value, active = new Set()) {
|
|
70
|
+
if (value === null || typeof value === 'string' || typeof value === 'boolean') return value;
|
|
71
|
+
if (typeof value === 'number') {
|
|
72
|
+
if (!Number.isFinite(value) || Object.is(value, -0)) {
|
|
73
|
+
throw new TypeError('assurance-package: value is not canonical JSON');
|
|
74
|
+
}
|
|
75
|
+
return value;
|
|
76
|
+
}
|
|
77
|
+
if (typeof value !== 'object') {
|
|
78
|
+
throw new TypeError('assurance-package: value is not JSON');
|
|
79
|
+
}
|
|
80
|
+
if (active.has(value)) throw new TypeError('assurance-package: cyclic value');
|
|
81
|
+
active.add(value);
|
|
82
|
+
try {
|
|
83
|
+
if (Array.isArray(value)) return value.map((entry) => portableJsonCopy(entry, active));
|
|
84
|
+
const prototype = Object.getPrototypeOf(value);
|
|
85
|
+
if (prototype !== Object.prototype && prototype !== null) {
|
|
86
|
+
throw new TypeError('assurance-package: value must use plain JSON objects');
|
|
87
|
+
}
|
|
88
|
+
return Object.fromEntries(
|
|
89
|
+
Object.entries(value).map(([key, entry]) => [key, portableJsonCopy(entry, active)]),
|
|
90
|
+
);
|
|
91
|
+
} finally {
|
|
92
|
+
active.delete(value);
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
73
96
|
/**
|
|
74
97
|
* Bundle N automated reliance decisions + the evidence each relied on into one
|
|
75
98
|
* portable, content-addressed assurance package. Does NOT re-perform (that is the
|
|
@@ -87,18 +110,19 @@ function toIso(now) {
|
|
|
87
110
|
export function buildAssurancePackage(decisions = [], { profile, organization = null, now = 0 } = {}) {
|
|
88
111
|
if (!Array.isArray(decisions)) throw new Error('assurance-package: decisions must be an array');
|
|
89
112
|
const items = decisions.map((d, i) => {
|
|
90
|
-
const
|
|
113
|
+
const source = portableJsonCopy(d ?? {});
|
|
114
|
+
const decision_id = source.decision_id ?? `decision-${i}`;
|
|
91
115
|
return {
|
|
92
116
|
decision_id,
|
|
93
|
-
action:
|
|
94
|
-
policy_hash:
|
|
95
|
-
stated_verdict: typeof
|
|
117
|
+
action: source.action ?? null,
|
|
118
|
+
policy_hash: source.action?.policy_hash ?? null,
|
|
119
|
+
stated_verdict: typeof source.stated_verdict === 'string' ? source.stated_verdict : null,
|
|
96
120
|
evidence: {
|
|
97
|
-
receipt:
|
|
98
|
-
quorum:
|
|
99
|
-
authority_proof:
|
|
100
|
-
revocation_state:
|
|
101
|
-
consumption:
|
|
121
|
+
receipt: source.receipt ?? null,
|
|
122
|
+
quorum: source.quorum ?? null,
|
|
123
|
+
authority_proof: source.authority_proof ?? null,
|
|
124
|
+
revocation_state: source.revocation_state ?? null,
|
|
125
|
+
consumption: source.consumption ?? null,
|
|
102
126
|
},
|
|
103
127
|
};
|
|
104
128
|
});
|
|
@@ -106,11 +130,12 @@ export function buildAssurancePackage(decisions = [], { profile, organization =
|
|
|
106
130
|
const exceptions = items.filter((it) => it.stated_verdict && it.stated_verdict !== 'rely')
|
|
107
131
|
.map((it) => ({ decision_id: it.decision_id, stated_verdict: it.stated_verdict, control_id: controlForVerdict(it.stated_verdict) }));
|
|
108
132
|
|
|
133
|
+
const profileCopy = profile == null ? null : portableJsonCopy(profile);
|
|
109
134
|
const body = {
|
|
110
135
|
'@version': ASSURANCE_PACKAGE_VERSION,
|
|
111
|
-
organization,
|
|
112
|
-
reliance_profile:
|
|
113
|
-
profile_hash:
|
|
136
|
+
organization: organization == null ? null : portableJsonCopy(organization),
|
|
137
|
+
reliance_profile: profileCopy,
|
|
138
|
+
profile_hash: profileCopy ? hashCanonical(profileCopy) : null,
|
|
114
139
|
control_catalog: RELIANCE_CONTROL_CATALOG,
|
|
115
140
|
decisions: items,
|
|
116
141
|
exception_history: exceptions,
|
|
@@ -140,15 +165,16 @@ export function buildAssurancePackage(decisions = [], { profile, organization =
|
|
|
140
165
|
* @param {object} [opts.approverKeys] auditor-pinned approver keys (out of band)
|
|
141
166
|
* @param {string} [opts.logPublicKey] auditor-pinned transparency-log key
|
|
142
167
|
* @param {string} [opts.rpId]
|
|
168
|
+
* @param {string[]} [opts.allowedOrigins]
|
|
143
169
|
* @param {object} [opts.revokerKeys]
|
|
144
170
|
* @param {(key:object)=>boolean} [opts.isConsumed] auditor-owned consumption lookup
|
|
145
171
|
* @param {number|string|Date} [opts.now] reliance-evaluation clock (pin for determinism)
|
|
146
172
|
* @returns {object} EP-ASSURANCE-REPERFORMANCE-v1
|
|
147
173
|
*/
|
|
148
|
-
export function reperformAssurancePackage(pkg, { approverKeys = {}, logPublicKey = null, rpId = null, revokerKeys = {}, isConsumed, now = 0 } = {}) {
|
|
174
|
+
export function reperformAssurancePackage(pkg, { approverKeys = {}, logPublicKey = null, rpId = null, allowedOrigins = [], revokerKeys = {}, isConsumed, now = 0 } = {}) {
|
|
149
175
|
if (!pkg || pkg['@version'] !== ASSURANCE_PACKAGE_VERSION) throw new Error('assurance-reperform: not an EP-ASSURANCE-PACKAGE-v1');
|
|
150
176
|
const profile = pkg.reliance_profile;
|
|
151
|
-
const evalOpts = { approverKeys, logPublicKey, rpId, revokerKeys, ...(typeof isConsumed === 'function' ? { isConsumed } : {}) };
|
|
177
|
+
const evalOpts = { approverKeys, logPublicKey, rpId, allowedOrigins, revokerKeys, ...(typeof isConsumed === 'function' ? { isConsumed } : {}) };
|
|
152
178
|
const relianceNow = typeof now === 'function' ? now() : now;
|
|
153
179
|
|
|
154
180
|
const results = (Array.isArray(pkg.decisions) ? pkg.decisions : []).map((it) => {
|
package/reports/reperform.js
CHANGED
|
@@ -37,15 +37,10 @@
|
|
|
37
37
|
* Deterministic: pure over (entries, issuerKeys); time enters only through the
|
|
38
38
|
* injectable `now` (generated_at). No sampling, no randomness.
|
|
39
39
|
*/
|
|
40
|
+
import crypto from 'node:crypto';
|
|
40
41
|
import { createEvidenceLog } from '../evidence.js';
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
// packages/gate/index.js: prefer the published package, fall back to the
|
|
44
|
-
// in-repo source so the monorepo works without a node_modules link.
|
|
45
|
-
const { verifyEmiliaReceipt } = await import('@emilia-protocol/require-receipt')
|
|
46
|
-
.catch(() => import('../../require-receipt/index.js'));
|
|
47
|
-
const { verifyWebAuthnSignoff, verifyQuorum } = await import('@emilia-protocol/verify')
|
|
48
|
-
.catch(() => import('../../verify/index.js'));
|
|
42
|
+
import { validatePinnedQuorumPolicy, verifyEmiliaReceipt } from '@emilia-protocol/require-receipt';
|
|
43
|
+
import { verifyQuorum, verifyWebAuthnSignoff } from '@emilia-protocol/verify';
|
|
49
44
|
|
|
50
45
|
export const REPERFORMANCE_VERSION = 'EP-GATE-REPERFORMANCE-v1';
|
|
51
46
|
|
|
@@ -107,7 +102,28 @@ function collectMaterial(e) {
|
|
|
107
102
|
* what IS re-performed is the cryptography (signature, issuer trust, action
|
|
108
103
|
* binding, ceremony checks).
|
|
109
104
|
*/
|
|
110
|
-
function
|
|
105
|
+
function spkiFingerprint(value) {
|
|
106
|
+
try {
|
|
107
|
+
const key = crypto.createPublicKey({ key: Buffer.from(value, 'base64url'), format: 'der', type: 'spki' });
|
|
108
|
+
return crypto.createHash('sha256').update(key.export({ type: 'spki', format: 'der' })).digest('hex');
|
|
109
|
+
} catch { return null; }
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
function pinnedApproverEntry(approverKeys, approver, carriedKey = null) {
|
|
113
|
+
if (!approverKeys || typeof approverKeys !== 'object' || Array.isArray(approverKeys)) return null;
|
|
114
|
+
const candidates = Object.values(approverKeys).filter((entry) => entry && typeof entry === 'object'
|
|
115
|
+
&& entry.approver_id === approver && typeof entry.public_key === 'string');
|
|
116
|
+
if (carriedKey) {
|
|
117
|
+
const fingerprint = spkiFingerprint(carriedKey);
|
|
118
|
+
const matches = candidates.filter((entry) => fingerprint && spkiFingerprint(entry.public_key) === fingerprint);
|
|
119
|
+
return matches.length === 1 ? matches[0] : null;
|
|
120
|
+
}
|
|
121
|
+
return candidates.length === 1 ? candidates[0] : null;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
function reverifyMaterial(m, {
|
|
125
|
+
issuerKeys, action, rpId, allowedOrigins, approverKeys, quorumPolicy, quorumPolicies,
|
|
126
|
+
}) {
|
|
111
127
|
if (m.type === 'receipt') {
|
|
112
128
|
const r = verifyEmiliaReceipt(m.doc, {
|
|
113
129
|
trustedKeys: issuerKeys, allowInlineKey: false, action, maxAgeSec: 0,
|
|
@@ -115,14 +131,33 @@ function reverifyMaterial(m, { issuerKeys, action, rpId }) {
|
|
|
115
131
|
return r.ok ? null : `receipt:${r.reason}`;
|
|
116
132
|
}
|
|
117
133
|
if (m.type === 'signoff') {
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
const
|
|
134
|
+
if (typeof rpId !== 'string' || !rpId || !Array.isArray(allowedOrigins) || allowedOrigins.length === 0) {
|
|
135
|
+
return 'signoff:relying_party_scope_unpinned';
|
|
136
|
+
}
|
|
137
|
+
const approver = m.doc?.context?.approver;
|
|
138
|
+
const entry = pinnedApproverEntry(approverKeys, approver, m.key);
|
|
139
|
+
if (!entry) return 'signoff:approver_key_unpinned_or_ambiguous';
|
|
140
|
+
const r = verifyWebAuthnSignoff(m.doc, entry.public_key, { rpId, allowedOrigins });
|
|
122
141
|
return r.valid ? null : `signoff:${r.error ? `error:${r.error}` : `checks_failed:${failingChecks(r.checks)}`}`;
|
|
123
142
|
}
|
|
124
143
|
if (m.type === 'quorum') {
|
|
125
|
-
|
|
144
|
+
if (typeof rpId !== 'string' || !rpId || !Array.isArray(allowedOrigins) || allowedOrigins.length === 0) {
|
|
145
|
+
return 'quorum:relying_party_scope_unpinned';
|
|
146
|
+
}
|
|
147
|
+
const pinnedPolicy = (quorumPolicies && typeof quorumPolicies === 'object' ? quorumPolicies[action] : null)
|
|
148
|
+
|| quorumPolicy;
|
|
149
|
+
const policy = validatePinnedQuorumPolicy(pinnedPolicy);
|
|
150
|
+
if (!policy.ok) return `quorum:${policy.reason}`;
|
|
151
|
+
const members = Array.isArray(m.doc?.members) ? m.doc.members.map((member) => {
|
|
152
|
+
const entry = pinnedApproverEntry(
|
|
153
|
+
approverKeys,
|
|
154
|
+
member?.signoff?.context?.approver,
|
|
155
|
+
member?.approver_public_key,
|
|
156
|
+
);
|
|
157
|
+
return entry ? { ...member, approver_public_key: entry.public_key } : null;
|
|
158
|
+
}) : [];
|
|
159
|
+
if (members.length === 0 || members.some((member) => !member)) return 'quorum:approver_key_unpinned_or_ambiguous';
|
|
160
|
+
const r = verifyQuorum({ ...m.doc, policy: policy.policy, members }, { rpId, allowedOrigins });
|
|
126
161
|
return r.valid ? null : `quorum:checks_failed:${failingChecks(r.checks)}`;
|
|
127
162
|
}
|
|
128
163
|
/* c8 ignore next */
|
|
@@ -154,12 +189,24 @@ function reverifyMaterial(m, { issuerKeys, action, rpId }) {
|
|
|
154
189
|
* @param {string[]} [o.issuerKeys=[]] pinned base64url SPKI issuer keys, sourced
|
|
155
190
|
* by the AUDITOR out of band — never from the entries themselves
|
|
156
191
|
* @param {number|function} [o.now=Date.now] clock for generated_at (pin for determinism)
|
|
157
|
-
* @param {
|
|
192
|
+
* @param {object} [o.approverKeys={}] auditor-pinned identity-bound approver keys
|
|
193
|
+
* @param {string} [o.rpId] bind carried WebAuthn assertions to this relying-party id
|
|
194
|
+
* @param {string[]} [o.allowedOrigins=[]] exact accepted WebAuthn origins
|
|
195
|
+
* @param {object} [o.quorumPolicy] auditor-pinned global organizational quorum rule
|
|
196
|
+
* @param {object} [o.quorumPolicies] action_type -> auditor-pinned quorum rule
|
|
158
197
|
* @returns {Promise<object>} EP-GATE-REPERFORMANCE-v1 document:
|
|
159
198
|
* { chain: {ok, entries, head}, receipts: {reverified, failed, not_reverifiable},
|
|
160
199
|
* counts: {allows, denies, replays_blocked, by_action_type}, ... }
|
|
161
200
|
*/
|
|
162
|
-
export async function reperformEvidence(entries = [], {
|
|
201
|
+
export async function reperformEvidence(entries = [], {
|
|
202
|
+
issuerKeys = [],
|
|
203
|
+
approverKeys = {},
|
|
204
|
+
now = Date.now,
|
|
205
|
+
rpId = null,
|
|
206
|
+
allowedOrigins = [],
|
|
207
|
+
quorumPolicy = null,
|
|
208
|
+
quorumPolicies = {},
|
|
209
|
+
} = {}) {
|
|
163
210
|
if (!Array.isArray(entries)) {
|
|
164
211
|
throw new Error('reperform: entries must be an array (evidence.all() or a full export)');
|
|
165
212
|
}
|
|
@@ -227,7 +274,13 @@ export async function reperformEvidence(entries = [], { issuerKeys = [], now = D
|
|
|
227
274
|
let allOk = true;
|
|
228
275
|
for (const m of material) {
|
|
229
276
|
const reason = reverifyMaterial(m, {
|
|
230
|
-
issuerKeys,
|
|
277
|
+
issuerKeys,
|
|
278
|
+
approverKeys,
|
|
279
|
+
action: typeof e.action === 'string' && e.action ? e.action : null,
|
|
280
|
+
rpId,
|
|
281
|
+
allowedOrigins,
|
|
282
|
+
quorumPolicy,
|
|
283
|
+
quorumPolicies,
|
|
231
284
|
});
|
|
232
285
|
if (reason) { allOk = false; receipts.failed.push({ hash, reason }); }
|
|
233
286
|
}
|
|
@@ -255,7 +308,8 @@ export async function reperformEvidence(entries = [], { issuerKeys = [], now = D
|
|
|
255
308
|
reperforms:
|
|
256
309
|
'Independent recomputation from the supplied evidence entries: the hash chain is rebuilt '
|
|
257
310
|
+ 'and checked with the evidence log\'s own verifier, carried receipt/signoff/quorum material '
|
|
258
|
-
+ 'is cryptographically re-verified against auditor-pinned issuer
|
|
311
|
+
+ 'is cryptographically re-verified against auditor-pinned issuer and identity-bound approver keys, '
|
|
312
|
+
+ 'WebAuthn scope, and organizational quorum policy; decision counts '
|
|
259
313
|
+ 'are recomputed from scratch without reference to any report builder.',
|
|
260
314
|
does_not_establish: [
|
|
261
315
|
'Completeness of the log: a chain rewritten in full, or entries withheld before they were supplied, is not detectable from the entries alone. Anchor the head hash externally to close this.',
|
|
@@ -268,6 +322,12 @@ export async function reperformEvidence(entries = [], { issuerKeys = [], now = D
|
|
|
268
322
|
input: {
|
|
269
323
|
entries_supplied: entries.length,
|
|
270
324
|
issuer_keys_pinned: issuerKeys.length,
|
|
325
|
+
approver_keys_pinned: approverKeys && typeof approverKeys === 'object'
|
|
326
|
+
? Object.keys(approverKeys).length : 0,
|
|
327
|
+
relying_party_scope_pinned: typeof rpId === 'string' && rpId.length > 0
|
|
328
|
+
&& Array.isArray(allowedOrigins) && allowedOrigins.length > 0,
|
|
329
|
+
quorum_policies_pinned: (quorumPolicy ? 1 : 0)
|
|
330
|
+
+ (quorumPolicies && typeof quorumPolicies === 'object' ? Object.keys(quorumPolicies).length : 0),
|
|
271
331
|
expects: 'the complete evidence log from genesis (evidence.all() or a full export)',
|
|
272
332
|
},
|
|
273
333
|
chain,
|
package/reports/underwriter.js
CHANGED
|
@@ -168,7 +168,7 @@ export function buildUnderwriterAttestation(entries = [], {
|
|
|
168
168
|
status: 'Not an insurance document. This attestation carries no coverage effect until adopted by the carrier.',
|
|
169
169
|
},
|
|
170
170
|
control_in_force: {
|
|
171
|
-
control: 'EMILIA Gate —
|
|
171
|
+
control: 'EMILIA Gate — Consequence Firewall',
|
|
172
172
|
mode: 'deny_by_default',
|
|
173
173
|
statement: 'Guarded actions execute only with a valid, in-scope, sufficiently-assured, fresh, one-time authorization receipt; absent or insufficient proof is refused, and every decision is appended to a hash-chained evidence log.',
|
|
174
174
|
guarded_decisions: guarded.length,
|