@emilia-protocol/gate 0.9.2 → 0.9.3
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/index.js +78 -8
- package/package.json +2 -2
package/index.js
CHANGED
|
@@ -163,9 +163,26 @@ export function verifyAdmissibilityAgainstPinnedProfile(pinned, presented) {
|
|
|
163
163
|
* verifyWebAuthnSignoff earns `class_a`. Used where the approver keys travel
|
|
164
164
|
* with the receipt rather than being pinned by the relying party.
|
|
165
165
|
*
|
|
166
|
+
* TRUST-LAUNDERING GUARD: an approver key carried INSIDE the receipt proves
|
|
167
|
+
* only that whoever minted the receipt also holds that key — it is NOT proof
|
|
168
|
+
* the relying party trusts that human. Crediting an elevated tier off such a
|
|
169
|
+
* key would collapse VERIFIED into ACCEPTED (any party can mint a fresh
|
|
170
|
+
* keypair, self-sign a signoff, and embed both). So path (b) elevates the
|
|
171
|
+
* tier ONLY when either: (i) the caller explicitly opts in with
|
|
172
|
+
* `allowEmbeddedApproverKeys:true` (the documented self-contained mode,
|
|
173
|
+
* DEFAULT OFF); or (ii) every embedded approver key that would earn the
|
|
174
|
+
* credit is present in the relying party's PINNED approver key set
|
|
175
|
+
* (opts.approverKeys). With no pin and no opt-in, path (b) may still VERIFY
|
|
176
|
+
* the signoff/quorum, but it does NOT elevate above `software`. Fail-closed.
|
|
177
|
+
*
|
|
166
178
|
* @param {object} doc the EP-RECEIPT-v1 document
|
|
167
179
|
* @param {object} [opts]
|
|
168
|
-
* @param {object} [opts.approverKeys] pinned approver keys for path (a)
|
|
180
|
+
* @param {object} [opts.approverKeys] pinned approver keys for path (a) and the
|
|
181
|
+
* path-(b) fallback: a receipt-embedded approver key elevates the tier only if
|
|
182
|
+
* it is one of these pinned keys (unless allowEmbeddedApproverKeys is set)
|
|
183
|
+
* @param {boolean} [opts.allowEmbeddedApproverKeys=false] explicit opt-in to the
|
|
184
|
+
* self-contained mode where an UNPINNED approver key carried inside the receipt
|
|
185
|
+
* may still elevate the path-(b) tier. DEFAULT OFF (fail-closed).
|
|
169
186
|
* @param {function} [opts.verifyAssurance] custom assurance verifier for path (a)
|
|
170
187
|
* @param {string} [opts.rpId] bind embedded device assertions to this WebAuthn RP id (path b)
|
|
171
188
|
* @param {boolean} [opts.detail] return a {tier, quorum, signoff} object instead of the string
|
|
@@ -185,30 +202,43 @@ export function receiptAssuranceTier(doc, opts = {}) {
|
|
|
185
202
|
// --- Path (b): self-contained embedded per-signer evidence (DoD audit fix). ---
|
|
186
203
|
const p = doc?.payload || {};
|
|
187
204
|
const verifyOpts = opts.rpId ? { rpId: opts.rpId } : {};
|
|
205
|
+
// The relying party's PINNED approver public keys (base64url SPKI-DER strings).
|
|
206
|
+
// An embedded approver key elevates the tier only if it is in this set, unless
|
|
207
|
+
// the caller explicitly opts into the self-contained mode.
|
|
208
|
+
const allowEmbedded = opts.allowEmbeddedApproverKeys === true;
|
|
209
|
+
const pinnedKeys = pinnedApproverKeySet(opts.approverKeys);
|
|
210
|
+
const keyIsTrusted = (k) => allowEmbedded || (typeof k === 'string' && pinnedKeys.has(k));
|
|
188
211
|
|
|
189
212
|
// quorum: a real, self-contained EP-QUORUM-v1 evidence document. Accept it
|
|
190
213
|
// under payload.quorum or payload.claim.quorum. It only counts if it is a full
|
|
191
214
|
// quorum document (policy + members with WebAuthn signoffs) AND verifyQuorum
|
|
192
215
|
// returns valid. A bare {signers,threshold} block has no members to verify and
|
|
193
|
-
// therefore CANNOT be credited quorum.
|
|
216
|
+
// therefore CANNOT be credited quorum. The cryptographic verification runs
|
|
217
|
+
// regardless (so `detail.quorum` reports validity), but the tier elevates only
|
|
218
|
+
// when every member's embedded approver key is pinned (or the caller opted in).
|
|
194
219
|
const q = p.quorum || p.claim?.quorum;
|
|
195
220
|
if (detail.tier !== 'quorum' && isQuorumEvidence(q)) {
|
|
196
221
|
const qr = verifyQuorum(q, verifyOpts);
|
|
197
|
-
|
|
198
|
-
|
|
222
|
+
const membersTrusted = allowEmbedded
|
|
223
|
+
|| (Array.isArray(q.members) && q.members.length > 0
|
|
224
|
+
&& q.members.every((m) => keyIsTrusted(m?.approver_public_key)));
|
|
225
|
+
detail.quorum = { valid: qr.valid, checks: qr.checks, embedded_keys_trusted: membersTrusted };
|
|
226
|
+
if (qr.valid && membersTrusted) detail.tier = 'quorum';
|
|
199
227
|
}
|
|
200
228
|
|
|
201
229
|
// class_a: a verifiable WebAuthn device signoff. The signoff evidence is
|
|
202
230
|
// {context, webauthn}; the approver key travels with it (signoff.approver_public_key)
|
|
203
|
-
// or alongside it (payload.approver_public_key).
|
|
231
|
+
// or alongside it (payload.approver_public_key). That key elevates the tier only
|
|
232
|
+
// when it is pinned by the relying party (or the caller opted into embedded keys).
|
|
204
233
|
if ((TIER_RANK[detail.tier] ?? 0) < TIER_RANK.class_a) {
|
|
205
234
|
const so = p.signoff || p.claim?.signoff;
|
|
206
235
|
if (isSignoffEvidence(so)) {
|
|
207
236
|
const key = so.approver_public_key || p.approver_public_key || p.claim?.approver_public_key;
|
|
208
237
|
if (key) {
|
|
209
238
|
const sr = verifyWebAuthnSignoff(so, key, verifyOpts);
|
|
210
|
-
|
|
211
|
-
|
|
239
|
+
const trusted = keyIsTrusted(key);
|
|
240
|
+
detail.signoff = { valid: sr.valid, checks: sr.checks, embedded_key_trusted: trusted };
|
|
241
|
+
if (sr.valid && trusted && (TIER_RANK[detail.tier] ?? 0) < TIER_RANK.class_a) detail.tier = 'class_a';
|
|
212
242
|
}
|
|
213
243
|
}
|
|
214
244
|
}
|
|
@@ -216,6 +246,34 @@ export function receiptAssuranceTier(doc, opts = {}) {
|
|
|
216
246
|
return opts.detail ? detail : detail.tier;
|
|
217
247
|
}
|
|
218
248
|
|
|
249
|
+
/**
|
|
250
|
+
* The set of PINNED approver public keys (base64url SPKI-DER strings) a relying
|
|
251
|
+
* party trusts, from the same `approverKeys` map path (a) uses. Accepts either a
|
|
252
|
+
* map of { keyId: { public_key } } (the EP-ASSURANCE-PROOF-v1 shape) or a plain
|
|
253
|
+
* array/set of key strings. Used to decide whether a receipt-embedded approver
|
|
254
|
+
* key may elevate the path-(b) tier. Never throws.
|
|
255
|
+
*/
|
|
256
|
+
function pinnedApproverKeySet(approverKeys) {
|
|
257
|
+
const out = new Set();
|
|
258
|
+
if (!approverKeys) return out;
|
|
259
|
+
if (approverKeys instanceof Set) {
|
|
260
|
+
for (const k of approverKeys) if (typeof k === 'string' && k) out.add(k);
|
|
261
|
+
return out;
|
|
262
|
+
}
|
|
263
|
+
if (Array.isArray(approverKeys)) {
|
|
264
|
+
for (const k of approverKeys) if (typeof k === 'string' && k) out.add(k);
|
|
265
|
+
return out;
|
|
266
|
+
}
|
|
267
|
+
if (typeof approverKeys === 'object') {
|
|
268
|
+
for (const entry of Object.values(approverKeys)) {
|
|
269
|
+
if (typeof entry === 'string' && entry) { out.add(entry); continue; }
|
|
270
|
+
const pk = entry && typeof entry === 'object' ? entry.public_key : null;
|
|
271
|
+
if (typeof pk === 'string' && pk) out.add(pk);
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
return out;
|
|
275
|
+
}
|
|
276
|
+
|
|
219
277
|
/** A quorum evidence doc must carry members with per-signer signoffs to be verifiable. */
|
|
220
278
|
function isQuorumEvidence(q) {
|
|
221
279
|
return !!q && typeof q === 'object' && q.policy && Array.isArray(q.members) && q.members.length > 0
|
|
@@ -238,8 +296,16 @@ function isSignoffEvidence(s) {
|
|
|
238
296
|
* @param {object} [opts.keyRegistry] a key registry (createKeyRegistry) for rotation + revocation;
|
|
239
297
|
* if given it supersedes trustedKeys — a receipt is verified only against keys valid (and not
|
|
240
298
|
* revoked) at its issuance time.
|
|
299
|
+
* @param {object} [opts.approverKeys] PINNED approver keys ({ keyId: { public_key, key_class } }).
|
|
300
|
+
* Used both for the pinned assurance-proof path and to authorize receipt-embedded
|
|
301
|
+
* approver keys under the self-contained embedded-evidence path.
|
|
302
|
+
* @param {boolean} [opts.allowEmbeddedApproverKeys=false] opt into the self-contained
|
|
303
|
+
* embedded-evidence mode: when true, a class_a/quorum tier may be credited from an
|
|
304
|
+
* approver key carried INSIDE the receipt even if that key is not pinned. DEFAULT OFF
|
|
305
|
+
* — with no pinned approverKeys and no opt-in, embedded evidence verifies but does not
|
|
306
|
+
* elevate above 'software' (prevents VERIFIED collapsing into ACCEPTED / trust-laundering).
|
|
241
307
|
*/
|
|
242
|
-
export function createGate({ manifest = null, trustedKeys = [], maxAgeSec = 900, store, log, allowInlineKey = false, allowEphemeralStore = false, strictEvidence = true, now = Date.now, keyRegistry = null, approverKeys = {}, approver_keys = null, verifyAssurance = null, rpId = null, requiredAdmissibilityProfile = null } = {}) {
|
|
308
|
+
export function createGate({ manifest = null, trustedKeys = [], maxAgeSec = 900, store, log, allowInlineKey = false, allowEphemeralStore = false, strictEvidence = true, now = Date.now, keyRegistry = null, approverKeys = {}, approver_keys = null, verifyAssurance = null, rpId = null, requiredAdmissibilityProfile = null, allowEmbeddedApproverKeys = false } = {}) {
|
|
243
309
|
// Production key custody: a registry (rotation + revocation) supersedes a flat
|
|
244
310
|
// trustedKeys list. A flat list is coerced to an always-valid registry, so
|
|
245
311
|
// existing callers are unchanged.
|
|
@@ -367,6 +433,10 @@ export function createGate({ manifest = null, trustedKeys = [], maxAgeSec = 900,
|
|
|
367
433
|
});
|
|
368
434
|
const tierResult = receiptAssuranceTier(receipt, {
|
|
369
435
|
rpId, detail: true, approverKeys: approver_keys || approverKeys, verifyAssurance,
|
|
436
|
+
// Trust-laundering guard: a receipt-embedded approver key does NOT elevate
|
|
437
|
+
// the tier unless it is in the pinned approverKeys set, or the operator
|
|
438
|
+
// explicitly opted into the self-contained embedded-evidence mode. DEFAULT OFF.
|
|
439
|
+
allowEmbeddedApproverKeys,
|
|
370
440
|
});
|
|
371
441
|
// Take the strongest tier either path proves.
|
|
372
442
|
const have = (TIER_RANK[assurance.have] ?? 0) >= (TIER_RANK[tierResult.tier] ?? 0)
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@emilia-protocol/gate",
|
|
3
|
-
"version": "0.9.
|
|
4
|
-
"description": "EMILIA Gate
|
|
3
|
+
"version": "0.9.3",
|
|
4
|
+
"description": "EMILIA Gate — the Trusted Action 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.",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"type": "module",
|
|
7
7
|
"main": "index.js",
|