@emilia-protocol/verify
v8.0.0
Published
Offline verification for native evidence joins, AEB lifecycle records, and action-bound EMILIA receipts.
Downloads
4,656
Maintainers
Readme
@emilia-protocol/verify
Offline verification for AEB evidence joins, lifecycle records, and EP receipts.
Verify native evidence projections, exact-action joins, nonauthorizing AEB records, Ed25519-signed receipts, Merkle anchor proofs, and commitments without calling EMILIA infrastructure. No API key or account is required.
This is the core primitive that makes EP a protocol, not an API.
Install
npm install @emilia-protocol/verifyUpgrading from 6.x
Version 7 uses EP-AEC-EVALUATOR-08-v1 for structured authorization-chain
evaluation. Its replay() reports a stored -07 record as
comparison: 'UNSUPPORTED_REVISION' and matches: false. The returned
result is a fresh -08 evaluation, not confirmation of the historical record.
Keep old replay records unchanged and retain the exact 6.x evaluator and trust
inputs if you need to reproduce them. Do not rewrite their revision or digest.
The legacy verifyAuthorizationChain API retains its combined validity result.
Quick Start
import { verifyReceipt } from '@emilia-protocol/verify';
// Load a receipt document (EP-RECEIPT-v1 format)
const receipt = JSON.parse(fs.readFileSync('receipt.json', 'utf8'));
// Get the signer's public key (from /.well-known/ep-keys.json)
const publicKey = 'MFYwEAYHKoZIzj0CAQYFK4EEAA...'; // base64url SPKI DER
const result = verifyReceipt(receipt, publicKey);
console.log(result);
// { valid: true, checks: { version: true, signature: true, anchor: null } }In the browser, edge, or Deno
The default entry uses Node's crypto. For any runtime with the W3C Web Crypto
API — every modern browser, Deno, Cloudflare Workers, Vercel Edge — import the
/web build instead. Same inputs, same { valid, checks } output (proven
byte-for-byte in web.test.js); the functions are async because Web Crypto is.
import { verifyReceipt, verifyWebAuthnSignoff } from '@emilia-protocol/verify/web';
const r = await verifyReceipt(receipt, publicKey); // Ed25519
const s = await verifyWebAuthnSignoff(signoff, approverKey, // ECDSA P-256
{ rpId: 'emiliaprotocol.ai' });This is what powers emiliaprotocol.ai/verify:
a relying party verifies a receipt entirely in their own tab — nothing uploaded,
no server trusted. Receipts use Ed25519; Class-A device signoffs use ECDSA P-256
over a WebAuthn assertion (the /web build converts the DER signature to the raw
form Web Crypto expects). Call isSupported() to feature-detect.
API
Gate Qualification v2
@emilia-protocol/verify/gate-qualification verifies a closed, signed
qualification graph offline and returns QUALIFIED, NOT_QUALIFIED, or
INDETERMINATE. The caller supplies the trusted keys, trusted time, expected
candidate, assignment, policy, protected-request and current-status bindings;
the evidence graph cannot select its own trust policy.
import {
evaluateQualification,
} from '@emilia-protocol/verify/gate-qualification';
const decision = evaluateQualification(bundle, relyingPartyContext);@emilia-protocol/verify/gate-qualification-promptfoo converts a complete,
immutably pinned Promptfoo v3 result into EVALUATION_ONLY evidence. It does
not emit a Qualification Statement and never authorizes provider entry.
import {
adaptPromptfooQualificationArtifact,
} from '@emilia-protocol/verify/gate-qualification-promptfoo';Both modules are also re-exported from the package root. A QUALIFIED result
is non-authorizing: it does not grant permission, reserve resources, consume
authority, invoke a provider, or establish legality or business suitability.
AEB evidence boundary — @emilia-protocol/verify/aeb
The stable ./aeb facade exports the adapter contract and lifecycle evidence
records. Existing narrow subpaths remain supported.
import {
evaluateAebEvidence,
verifyAebEvaluation,
} from '@emilia-protocol/verify/aeb';The direct handoff below serves the direct native path of AEB-06, which was posted on 2026-09-24 as an individual Internet-Draft and is not adopted by any working group. The handoff itself is a repository implementation profile; AEB-06 does not specify it. These source labels and same-repository vectors do not establish native-protocol conformance or independent interoperability.
When a native system already authorized the final operation, the handoff replaces a manufactured CAID or AEC layer with a shorter signed statement:
import {
issueAebNativeAuthorizationHandoff,
verifyAebNativeAuthorizationHandoff,
} from '@emilia-protocol/verify/aeb';The relying party pins the gateway key, native source profile, issuer,
audience, executor, and provider. A successful check means that the pinned
gateway signed this exact native PERMIT and action binding. It does not prove
the wisdom of the native decision or a provider effect.
The result's native_replay_identity is derived locally from the matched
pin's authority namespace and the native authorization ID, and
replay_identity_key is its relying-party-scoped key. The namespace is the
issuer unless the pin declares authority_namespace, in which case the issuer
string is not an input; the system and profile labels are never inputs. A
replay fence must hold replay_identity_key. The result also reports
native_replay_unit and replay_key exactly as 4.1.0 did (the label-bearing
wire replay_unit and its key); a fence may hold replay_key beside the
identity key to keep grants recorded by 4.1.0-based code, never alone,
because it changes when one grant is relabelled. legacy_replay_keys lists
the 4.1.0 key of the grant under every pinned label and issuer spelling that
shares the matched pin's namespace; a fence that holds all of them refuses a
grant that 4.1.0-based code consumed under any of those labels.
One issuer has exactly one namespace in a pin set. Pins whose issuers are
different spellings of one issuer (URI scheme case, URL host case, a trailing
dot on the host, a default port, trailing slashes, dot segments in the path,
an http or https URL written without //, URN namespace-identifier case,
DID method-name case, did:web host case and trailing dots, or SPIFFE
trust-domain case and trailing dots) must all declare the same namespace; pins for one issuer must all declare a
namespace or all omit it; and one exact issuer declared under two different
namespaces is refused. verifyAebNativeAuthorizationPins() reports a refused
pin set before use with one native_pins_* reason. The handoff verifier
refuses such a pin set as native_handoff_schema_invalid, except that a pin
set declaring no namespace is accepted as 4.1.0 accepted it and, when it
aliases one issuer, yields a null native_replay_identity and
replay_identity_key. The signed AEB-NATIVE-AUTHORIZATION-HANDOFF-v1 wire
is unchanged from 4.1.0: handoffs issued by either version verify under the
other, and the carried replay_unit is never used as the replay identity.
Normalization cannot find every alias: two issuer strings that denote one
authority but do not normalize equal need one explicitly shared namespace.
evaluateAebEvidence() is the composed kernel. It verifies native evidence
under relying-party-pinned adapters, keeps native verification separate from
local authorization, and always requires a CAID for the action and an AEC
composition of the verified legs under a pinned requirement. Its execution API
then reserves the accepted native replay identities before provider entry. The
conditional path, with no CAID or AEC, is the native handoff above together
with Gate's createNativeConsequenceBoundary().
Use createAebNativeVerificationAttestationAdapter() when a native protocol
verifier runs at a workload gateway. Its signed attestation binds the native
artifact digest, protocol, audience, subject, evidence role, mapper, resolver,
CAID, and normalized-action digest. Presenter-selected roots, mappers, profiles,
and unsigned gateway headers are not trusted.
InMemoryAebConsumptionStore is test-only. Fleet execution uses
authorizeAebExecutionDurable() and reconcileAebExecutionDurable() with the
durable, ownership-fenced store contract implemented by
@emilia-protocol/gate.
A reserve call that throws, or answers anything other than true,
'RESERVED', false, 'CONSUMPTION_CONFLICT', or
'NATIVE_REPLAY_CONFLICT', may have reserved the row, and
authorizeAebExecutionDurable() never reports it as a clean refusal on
the strength of a read that shows the row AVAILABLE, because a write
still in flight can land after that read. It returns
RECONCILIATION_REQUIRED with consumption_reservation_unconfirmed,
unless the store's optional state() shows the key CONSUMED or
RELEASED_NOT_ENTERED, permanent states that this call's reserve cannot
have written or changed, which is REFUSED with consumption_conflict.
Crossing Lab adapter workbench
emilia-verify crossing-lab scaffolds, seals, and runs a native AebAdapter
entirely offline. The workspace carries a real AEB-ADAPTER-v1 pinned
configuration, one native artifact, a pinned status snapshot whose native
authentication is the adapter/integrator's responsibility, an exact expected
action, an explicit reviewed hostile action, a CAID mapping profile, and a
named evidence requirement:
npx @emilia-protocol/verify crossing-lab init ./my-native-protocol
npx @emilia-protocol/verify crossing-lab seal ./my-native-protocol
npx @emilia-protocol/verify crossing-lab run ./my-native-protocol \
--out ./crossing-lab-report.jsonEvery adapter row comes from the canonical evaluateAebEvidence evaluator and
retains its signed AEB-EVALUATION-v1. The signature uses a fixed published
self-test key and provides no evaluator or operator attribution. The adapter
runs in a bounded child with no ambient network, write, or child-process
permission. Custom dependencies must first be bundled into the one pinned ESM
adapter file. The Crossing Lab subcommand requires Node 26 or another Node
permission runtime that recognizes --allow-net; it refuses older runtimes as
an operational error. The report is a
deterministic local adapter compatibility self-test, not certification,
authorization, native-specification correctness, independent interoperability,
deployment evidence, or execution evidence. See
docs/AEB-CROSSING-LAB.md.
@emilia-protocol/verify/aeb-acceptance-profile publishes the relying party's
content-addressed foreign-proof allowlist, exact AEB configuration,
requirement, registry, action type, and required evidence roles. The same
profile has fixed monitor and enforce semantics: monitor can report that
pre-consumption checks pass but cannot authorize or reserve evidence; enforce
requires an execution-time verification bound to the exact evaluation record,
local authorization, and atomic one-time consumption. An atomic replay
conflict can be decided only in enforce mode.
@emilia-protocol/verify/aeb-execution-conditions evaluates an opaque,
human-approved predicate-set commitment at the execution boundary. The exact
action, approval evidence, basis, presentation, resolver profile, source trust,
freshness, and enforcement strength are relying-party pinned. observed and
leased resolutions can satisfy the conditions axis but cannot claim
prevention; only compare-and-set and provider-enforced resolutions carrying
enforcement evidence can do so. ADMIT is scoped to execution conditions and
does not establish authorization or physical truth. When supplied to
authorizeAebExecution or authorizeAebExecutionDurable, only a valid ADMIT
result can reach the one-time reservation; predicate failure refuses, and
uncertainty routes to reconciliation required before authority is reserved.
Four revision-pinned foreign-proof adapters are available:
aeb-oasnt-adapterverifies the source-locked OASNT-02 compact authorization token against an enrolled hardware-attested P-256 key and recomputes its action, display, and protected-request commitments. OASNT's native CAID namespace remains distinct from the EMILIA CAID projection. A local vector corpus exercises the OASNT-CAID-01 single-use boundary and executor-owned dual-profile join: pre-reservation refusal leaves authority available,NOT_COMMITTEDreleases an open reservation, committed admission consumes it, and the two profile-specific identifiers are never compared as direct join keys. This is same-repository implementation evidence, not a claim that an OASNT-CAID revision for OASNT-02 has been published. Seedocs/standards-engagement/OASNT-CAID-AEB-COMPOSITION.mdin the repository for the exact two-source compatibility boundary.aeb-aps-adapterverifies the APS-03 signed ActionIntent and PolicyDecision chain, preserves the completeaps-action-ref-v2material, recomputes the decision reference, and delegates authority-chain semantics to a separately pinned pure verifier.aeb-mcgraw-delegation-adapterverifies deterministic COSE_Sign1 Budget proof claims for the exact protected HTTP request. ML-DSA-65 verification and delegation-chain semantics are supplied by separately pinned pure backends, so the package retains its zero-runtime-dependency boundary.aeb-oauth-transaction-challenge-adapterrequires both the protected resource's signed challenge and the authorization server's access token, verifies their transaction and actor linkage, and delegates RAR narrowing to a separately pinned pure verifier. A challenge or pending transaction ID is never treated as approval, and the adapter does not infer a human approver.
All four outputs remain evidence, not final authority. AEB composes the roles; the customer-owned local Gate decides whether the exact action may execute.
@emilia-protocol/verify/aeb-native-adapters supplies concrete AgentROA and
ORPRG adapters. Both use relying-party-pinned roots, profiles, status, and
expected actions. ORPRG uses non-mutating native inspection: it verifies the
permit and exposes its native replay unit, while the Gate atomically fences
that replay unit before any effect. Inspection is never reported as a final
native ALLOW.
@emilia-protocol/verify/authorization-server-confirmation verifies a closed
EdDSA Authorization Server grant under relying-party-pinned issuer, key,
audience, Resource Server key and freshness limits, while preserving signed
policy, directory-snapshot, exact-action and human-evidence commitments. It
emits the separate
authorization-server-confirmation role. An AEB evidence-binding term then
requires its signed human-evidence digest and human subject to match a
separately verified human-authorization leg. A valid AS signature is evidence
only: it never emits SATISFIED or AUTHORIZED, and an agent-orchestrator
signature cannot substitute for the AS. The signed grant distinguishes token
issuance time from the time the AS observed its directory snapshot, and the
relying party pins the maximum acceptable snapshot age. This prevents a fresh
token from laundering stale directory state into a claim of current standing;
it does not prove HR-system freshness or instantaneous employment status.
@emilia-protocol/verify/authorization-bundle verifies the closed
EP-AUTHORIZATION-BUNDLE-v1 pre-execution human-evidence object. The relying
party supplies the exact action, audience, independently native-verified
authorization binding,
policy-selected approver set, accepted key classes, current policy result, and
any required status or presentation verifiers. The result is exactly
SATISFIED, REFUSE, or INDETERMINATE; even SATISFIED explicitly sets
authorization_decision: false. A separate compare-and-set helper binds one
bundle digest to one native grant, but the caller must perform that transition
atomically in its own durable authoritative store. The core is transport
neutral. @emilia-protocol/verify/oauth-rar-authorization-binding is one
optional closed profile that validates and compares an OAuth/RAR projection
after the caller has verified the native transaction; it does not validate an
OAuth token or make a grant decision. The package includes 24
same-repository hostile cases; they are not an external interoperability claim.
@emilia-protocol/verify/policy-decision-evidence is the distribution bridge
for existing local policy engines. It projects an exact OPA boolean or Cerbos
effect into a short-lived Ed25519 statement, verifies it under a
relying-party-pinned bridge key, pins the accepted engine and policy digest,
and maps the signed action through CAID. Its evidence role is normally
machine-policy-decision. A machine ALLOW satisfies only that role; it is
not human intent, authorization, admission, complete-mediation proof, or an
effect receipt. Consequential requirements should compose it with a separate
human-authorization leg:
requirements: {
'human-plus-local-policy': {
'@version': 'AEB-REQUIREMENT-v1',
all_of: ['human-authorization', 'machine-policy-decision'],
terms: [{ type: 'one-time-consumption' }],
},
}The bridge signs what the integration observed; it does not make OPA or Cerbos independent witnesses. Keep the bridge key behind the credential-owning enforcement point and use Gate's durable consumption path for execution.
@emilia-protocol/verify/aeb-psea-adapter adds an optional, revision-pinned
adapter for draft-yossif-psea-02. It verifies strict ES256 compact JWS/EAT
proofs against enrolled P-256 keys; rejects unknown headers and claims; binds
issuer, audience, operation, tier, nonce, UV, UEID, lifetime, attestation
appraisal, and the JCS action hash; and projects the exact action into CAID.
verifyAndCommitPseaProof() atomically advances the native counter and consumes
the jti through a caller-supplied durable store before Gate admission. The
adapter does not mint PSEA proofs, identify a named human beyond pinned
enrollment, establish WYSIWYS, or replace AEC/Gate authorization and outcome
reconciliation. The hostile fixture set is
conformance/vectors/psea-aeb.v1.json; it is an EMILIA adapter suite, not an
independent PSEA interoperability claim.
@emilia-protocol/verify/fido-ap2-bridge verifies a relying-party-pinned
WebAuthn ES256/P-256 human-authorization ceremony over one closed, immediate
AP2 v0.2 CheckoutMandate/PaymentMandate projection. The signed context
commits to exact canonical SD-JWT token strings, both disclosure-resolved
verified payloads, the checkout-hash algorithm derived from the exact issuer
token, the merchant checkout JWT,
native-verification attestation, readable disclosure, CAID, normalized action,
tenant, relying party, audience, operation, frozen provider request,
provider/account, approver, nonce, and deadline. Native AP2 verification stays
a separate authoritative attestation leg: it must verify the merchant checkout
JWS, current checkout state, hash/transaction linkage, disclosed claims, and
credential scope. The bridge does not reinterpret those credentials or turn a
valid WebAuthn assertion into local authorization.
The pinned immediate-payment subset follows AP2 v0.2 literally: an immediate
payment omits execution_date, and optional merchant, instrument, PISP, and
risk members are accepted only with their schema-defined types. null does
not stand in for an omitted AP2 optional member. Bridge and AEB instants are
limited to millisecond precision so JavaScript comparison never truncates a
future not-before value into the present.
The closed WebAuthn profile accepts only an enrolled ES256/P-256 key, a 37-byte
extension-free assertion with UP and UV, approved origin/RP bindings, and
relying-party-pinned backup policy. The default
above-enrollment-and-one-time policy also requires a counter above
enrollment; Gate compares and advances the durable RP/credential head
atomically with admission. The explicit not-relied-upon policy supports
authenticators such as synced platform passkeys whose counters remain zero;
under that policy the counter supplies no clone-detection claim and Gate does
not create a monotonic-counter resource. Exact ceremony binding, replay
resources, and one-time provider admission remain required under both modes.
Legacy, open, recurring, unknown, stale, or materially lossy AP2 semantics fail
closed. One-time execution custody remains the Gate Qualification v2
AdmissionStore boundary. A verified assertion proves the signed ceremony
occurred; it does not prove legal consent, human comprehension, current
authorization, admission, or replay prevention.
createFidoAp2NativeSourceBinding() domain-separates commitments to the exact
byte sequences and strict payload digests accepted by the native verifier.
Those commitments prevent a splice only when the caller supplies the exact
tokens and disclosure-resolved payloads accepted by its authoritative native
AP2 verifier and independently reprojects them before reliance. The
corresponding Gate 0.22.2 bridge performs that reprojection at admission; this
pure helper alone is not admission or native AP2 verification.
A2A receipt binding — @emilia-protocol/verify/a2a-receipt-binding
createA2AReceiptPresentation() carries an EMILIA receipt on A2A v1.0's
official namespaced Message.extensions / Message.metadata extension point.
The closed profile binds the complete base receipt, exact semantic action,
pre-task initiating Message, server-issued Task and context, proof-retry
Message, selected Agent Card, and target interface. It also emits the
EP-RECEIPT-EXTENSIONS-v1 companion index required by Receipts-10.
The Task ID cannot be present in the initiating Message because A2A assigns it at the server. The bridge therefore preserves both phases instead of retroactively treating the first attempt as authorized: the original Message is committed by digest, then the signed companion binds that request and the receipt to the returned Task/context and the exact retry Message.
verifyA2AReceiptPresentation() requires a relying-party-pinned Ed25519 binder,
the exact Task snapshot obtained by the caller over authenticated A2A transport,
the exact initiating Message, Agent Card and interface pins, current time,
extension negotiation, locally expected
action/CAID, and a caller-supplied receipt verifier that returns the digest and
CAID it actually verified. It rejects task, context, message, target, action,
receipt, metadata, protocol-version, and validity-window substitution. A valid
result is correlation evidence only: A2A server authentication, receipt
verification, local authorization, one-time consumption, execution, and
outcome proof remain separate decisions. Raw unsigned A2A objects never become
authority.
Agent Edge Continuity — @emilia-protocol/verify/agent-edge-continuity
EP-AGENT-EDGE-CONTINUITY-v1 carries one material action across user,
harness, model, MCP tool, A2A handoff, and effect boundaries without turning
provenance into authority. Every envelope binds the relying party, pinned
configuration, initiator, executor, CAID, normalized action, proposal, and
operation.
Verification is offline and relying-party controlled. Signer pins constrain each key by status, validity, source, and edge; topology pins constrain roots, transitions, execution edges, path depth, lifetime, and age.
The single-process authorizeAgentContinuityExecution() is for reference
tests. Fleet execution uses authorizeAgentContinuityExecutionDurable(),
which atomically fences AEB native replay identities plus every continuity ID
and handoff nonce. Historical AEB verification, a post-effect envelope before
reservation, or an insecure store cannot authorize execution.
The outcome edge is evidence only. Proposal-to-Effect custody keeps an
INDETERMINATE action locked and requires authenticated reconciliation.
Signed current status — @emilia-protocol/verify/status
EP-STATUS-v1 verifies fresh current/revoked state under a separately pinned
EP-REVOKER-AUTHORITY-v1 certificate. Sequence and predecessor-digest binding
reject rollback or resurrection, terminal revocation cannot be undone, and an
unavailable or stale status authority produces indeterminate, never a
fabricated revoked: false.
verifyReceipt(doc, publicKeyBase64url)
Verify an EP-RECEIPT-v1 document. Performs three independent checks:
- Version — Document format is EP-RECEIPT-v1
- Signature — Ed25519 signature over canonical payload
- Anchor (if present) — Merkle proof reconstructs claimed root
Returns { valid, checks, error? }.
verifyMerkleAnchor(leafHash, proof, expectedRoot)
Verify a Merkle inclusion proof. The root can be independently checked on Base L2 via Basescan.
Returns boolean.
verifyCommitmentProof(proof, publicKeyBase64url)
Verify an EP-PROOF-v1 commitment proof. Checks expiry and signature.
Returns { valid, claim, error? }.
verifyReceiptBundle(bundle, publicKeyBase64url)
Verify all receipts in an EP-BUNDLE-v1 document.
Returns { valid, total, verified, failed }.
verifyWebAuthnSignoff(signoff, approverPublicKeySpkiB64u, { rpId? })
Verify a Class-A (device-bound key) signoff fully offline: the WebAuthn challenge equals SHA-256(JCS(context)) for the exact signed context, the authenticator asserted user presence + verification, and the ECDSA P-256 signature verifies against the enrolled approver key.
Returns { valid, checks, error? }.
verifyResolutionReceipt(receipt, opts) - @emilia-protocol/verify/resolution
Verify an additive EP-RESOLUTION-v1 record for a briefing-and-binding
envelope. The signed context preserves approved, declined, amended, and
rejected as distinct outcomes and binds the source envelope digest, exact
action digest, principal, initiator, nonce, and validity window.
The relying party supplies the exact bindingMoment, expectedActionHash,
role-scoped principalKeys, rpId, and an exact allowedOrigins list. Before
an authentic approval returns authorizes_action: true, it must additionally
supply expectedSelectedOption, expectedNonce, expectedInitiator, and an
in-window evaluationTime. Callers gating execution test authorizes_action,
not merely valid; authentic negative outcomes are evidence and never authority.
Returns { valid, authorizes_action, outcome, requires_successor, checks, reason? }.
verifyTrustReceipt(receipt, { approverKeys, logPublicKey, now })
Authenticity is not admission or replay prevention. This pure offline verifier never authorizes an effect and never atomically consumes a receipt. Every result carries
decision_scope.authenticity_only: true,admission_authorized: false, and an explicit replay/revocation status. Consequential execution must use the credential-owning Gate /makeReceiptGate()with one shared atomic consumption store.
The full offline verification algorithm from the Internet-Draft (draft-schrock-ep-authorization-receipts, Section 6.3) over a Section 6.2 Trust Receipt — all six steps, no network:
- Recompute the action hash from the canonical Action Object
- Recompute each context hash; confirm it commits to the action hash, the policy hash, and a distinct approver
- Verify each signoff signature (Class-A WebAuthn or Class-B Ed25519) against the pinned approver key, checking the key's validity window and refusing any key directory entry carrying
compromised_at - Separation of duties — initiator in no approver slot, approvers pairwise distinct, approval count ≥
required_approvals - Merkle inclusion of the receipt leaf against the checkpoint root, and the
checkpoint signature against the trusted log key. The signature is Ed25519
over the raw 32-byte SHA-256 digest of the UTF-8 JCS serialization of the
checkpoint after removing
log_signature(exact current-profile object:{log_key_id, root_hash, tree_size}). It is not a signature over the JSON text or an encoded digest string. signed_at/committed_atwithin[issued_at, expires_at]
Returns { valid, checks, errors, attestation, strict, decision_scope } and
fails closed on missing cryptographic input. valid: true means the supplied
artifact passed the requested authenticity checks; it does not mean the
action is currently authorized or unused.
verifyTrustReceipt authenticates each presented Authorization Context, but it
does not evaluate the companion EP-QUORUM set-level policy. Its always-present
decision_scope.quorum_ordering reports whether a prev_context_hash or
prev_signoff_hash was
present and names verifyQuorum as the required verifier. A green base receipt
result MUST NOT be described as proof of threshold, roster order, or ordered
chain linkage unless verifyQuorum also accepts the exact members and pinned
policy.
The strong ordered profile is EP-QUORUM-SIGNOFF-CHAIN-v1. Set
ordered_chain: true, pin that exact ordered_chain_profile, and pass the
entire trusted policy as verifyQuorum(document, { expectedPolicy, ...opts }).
Each successor's signed prev_signoff_hash is lowercase SHA-256 over
UTF8("EP-QUORUM-SIGNOFF-CHAIN-v1") || 0x00 || UTF8(JCS(previous.signoff)).
The first context omits that field, and every context omits prev_context_hash.
completedSignoffHash is exported from the quorum subpath for producers.
Legacy context-only chains cannot satisfy the new strong profile and require
fresh signatures. The result proves dependency on completed prior proofs,
not trusted wall-clock time or human comprehension. Unpinned verification
establishes only internal consistency; a supplied policy is not its own authority.
valid_from / valid_to express ordinary issuance and rotation windows.
compromised_at is different: its presence is a terminal relying-party directory
fact, so a stolen key cannot evade it by signing a backdated issued_at. When a
relying party supplies its own RFC 3339 now, the verifier also refuses an
issued_at, signed_at, or consumption.committed_at after that verifier
decision time. Omitting now preserves
offline historical verification; trusted timestamp evidence is still required
when a deployment needs to prove when a receipt was actually created.
evaluateReliance treats its input.now as the authoritative decision clock
and forwards that exact instant into receipt verification; callers do not need,
and cannot use, a separate opts.now to weaken the temporal check.
For a current reliance decision, set verificationMode: 'current' and pass the
relying party's trusted now. Current mode requires every signing key to remain
current at that decision time, so a presenter cannot evade an expired
valid_to by backdating the receipt. If an operator narrows valid_to into the
past, also pass the previous directory as previousApproverKeys: the
transition fails closed unless the new entry carries compromised_at (or the
relying party records the explicit
allowRetroactiveExpiryWithoutCompromise: true exception).
const result = verifyTrustReceipt(receipt, {
approverKeys: currentDirectory,
previousApproverKeys: priorDirectory,
logPublicKey,
verificationMode: 'current',
now: relyingPartyClock,
revocationStatements,
revokerKeys,
});When revocationStatements is absent, decision_scope.revocation_status is
unknown, never “not revoked.” An authentic exact-target revocation refuses;
a malformed exact-target statement is indeterminate and also refuses.
Current-mode financial actions at or above USD 100,000 require a pinned RFC
3161 timestampProof. The built-in trigger recognizes payment, transfer, wire,
disbursement, purchase, refund, and financial action types. Other deployments
can require the same control explicitly with requireTimestampProof: true.
Class-A WebAuthn verification surfaces sign_count, backup eligibility, backup
state, and a counter status under webauthn_signoffs. Pass the previously
stored counter in webauthnSignCounts[keyId]. The default observe policy
reports a non-advancing counter; webauthnCounterPolicy: 'enforce' refuses it.
A zero counter remains unsupported, because authenticators are permitted not
to implement signature counters. A non-advancing nonzero counter is a signal
of possible cloning, malfunction, or reordered assertions—not proof by itself.
Strict verifier mode — requires 1.5.0
For deployment gates and hostile-environment verification, opt into strict mode:
const r = verifyTrustReceipt(receipt, {
approverKeys,
logPublicKey,
strict: true,
rpId: 'www.emiliaprotocol.ai',
expectedPolicyHash: 'sha256:...',
});Strict mode preserves the frozen Section 6.3 checks object, then adds
r.strict as a second gate. When strict: true, valid requires both the base
checks and:
pinned_keys— every signer and the log are locally pinned.rp_id— Class-A WebAuthnrpIdHashmatches the caller-pinned RP ID.user_presence/user_verification— Class-A signoffs asserted UP + UV.key_windows— every approver key has parseablevalid_from/valid_toand was valid atissued_at.policy_hash— every context matchesexpectedPolicyHash.no_unsigned— critical action, context, signoff, consumption, and log proof fields are present.
Without strict: true, strict is { enabled: false, valid: true, checks: {}, errors: [] }, so existing verification and conformance semantics are unchanged.
verifyOutcomeBinding(receipt, attestation, opts) — experimental
Verify an executor-signed EP-OUTCOME-ATTESTATION-v1 against the exact Trust
Receipt, signed predicted effects, action hash, receipt bytes, and consumption
nonce it names:
import {
buildOutcomeAttestation,
trustReceiptDigest,
verifyOutcomeBinding,
} from '@emilia-protocol/verify';
const attestation = buildOutcomeAttestation({
receipt_id: receipt.receipt_id,
receipt_digest: trustReceiptDigest(receipt),
action_hash: receipt.action_hash,
consumption_nonce: receipt.consumption.nonce,
execution_id: 'exec_123',
executor_id: 'ep:executor:payments',
executed_at: new Date().toISOString(),
observed_effects,
signer: executorSigner,
});
const result = verifyOutcomeBinding(receipt, attestation, {
receiptOptions: { approverKeys, logPublicKey },
executorKeys: {
'ep:executor:payments': { public_key: executorPublicKey },
},
policyPredictedEffects: optionalAdditionalConstraints,
});The executor signs observations, never the human-approved prediction. Signed
predictions come only from the fully verified receipt; relying-party policy may
add constraints but cannot replace or loosen them. If the policy field is
supplied but is not an array, verification refuses instead of treating it as
absent. result.outcome_binding
preserves in_bounds, divergent, and incomparable as distinct results, and
valid is true only for a fully bound, verified, in-bounds result.
result.result_digest commits to the exact receipt, attestation, signed
predictions, supplied policy predictions, checks, reasons, and typed outcome;
two different signed inputs do not share a digest merely because they reach the
same reduced verdict.
verifyOutcomeObservationSet(predictions, observations, opts) — experimental
Reconcile executor, system-of-record, and independent-observer claims without
letting the presenter select the trust policy. Each sourceKeys pin carries the
canonical Ed25519 public key, role, source class, control_domain_id, status,
validity interval, and optional compromise time. sourceRequirements can
require a distinct-source quorum by canonical key and declared control domain;
observationWindows binds the accepted interval and maximum attestation delay.
const result = verifyOutcomeObservationSet(predictions, observations, {
sourceKeys,
sourceRequirements: [{
role: 'independent_observer',
source_class: 'revenue_meter',
min_distinct_sources: 2,
distinct_by: ['key', 'control_domain'],
}],
observationWindows: [{
role: 'independent_observer',
source_class: 'revenue_meter',
relation: 'exact',
not_before,
not_after,
max_attestation_delay_ms: 30_000,
}],
now,
expectedReceiptId,
expectedReceiptDigest,
expectedActionHash,
expectedActionCaid,
expectedConsumptionNonce,
expectedOperationId,
});The same canonical key cannot fill executor and independent-observer roles,
even under alternate text encodings. Different keys in one declared control
domain also do not establish independence. A declared control domain is a
relying-party input, not proof of organizational separation or physical truth.
Missing, non-current, non-distinct, stale, or window-mismatched evidence returns
lifecycle_state: 'indeterminate'; it never authorizes blind replay.
Advisory: the PIP-007 initiator escalation attestation — requires 1.4.0
When the contexts carry a PIP-007 initiator_attestation, the result includes an advisory report:
const r = verifyTrustReceipt(receipt, { approverKeys, logPublicKey });
r.attestation; // { present, consistent, issues: [] }present— a context carries an attestation.consistent— it is present in every context with an identical canonical form (the cross-context identity rule the protocol flags to catch a divide-and-misinform orchestrator showing different approvers different reasons).issues— any PIP-007 §1 malformations: unknown members, astatementover 280 characters,escalation_triggerofpolicy_rulewithout apolicy_basis, or a bad enum value.
The advisory never affects valid or any member of checks — by design (PIP-007 §2): a receipt carrying a malformed attestation still verifies cryptographically, exactly as it does on a verifier that predates this PIP. The attestation is a claim by the initiator — identified but never trusted — so a policy engine MUST NOT use it to relax any check or raise any trust score.
Transparency, revocation, time, and consumption checks
The optional evidence checks extend verifyTrustReceipt in the same shape as
priorCheckpoint: each runs only when its evidence or requirement is supplied,
adds one member to checks, folds into valid by conjunction, and fails closed
with a distinct reason. The always-present decision_scope is intentionally
outside the cryptographic checks: it prevents callers from mistaking offline
authenticity for current admission or atomic replay protection.
const r = verifyTrustReceipt(receipt, {
approverKeys, logPublicKey,
// 1. Witness quorum (EP-WITNESS-v1): k distinct pinned witnesses cosigned the head.
witnessQuorum: { cosignatures, pinnedWitnessKeys, k: 2 },
// 2. Trusted-time proof (RFC 3161): a pinned TSA timestamped a digest you choose.
timestampProof: { token, expectedDigest, pinnedTsaKeys },
// 3. Currency (EP-CURRENCY-v1): passes ONLY on a proven-fresh signed head.
currency: { now, maxStalenessSeconds, freshHead, freshHeadRequired },
// 4. Consumption proof (EP-SMT-CONSUME-v1): a nonce went absent -> present once.
consumptionProof: bundle,
// 5. Initiator-software attestation (EP-INITIATOR-ATTESTATION-v1).
requireInitiatorAttestation: true,
// 6. Current exact-target revocation statements from pinned revokers.
revocationStatements,
revokerKeys,
});
// checks.witness_quorum / .timestamp_proof / .currency / .consumption /
// .initiator_attestation / .revocation are added only when active, and the full
// module result is surfaced under the matching top-level member.Honesty boundaries (also stated in each module):
- Witness quorum proves
ktrusted witnesses saw one head (the local, single-view half of equivocation detection). It does not prove no different head was shown elsewhere; that cross-view gossip is the deployment's responsibility. - Timestamp proof proves a TSA asserted the digest existed at
gen_time(the bytes predategen_time). It is authentic-as-of-token only and says nothing about current TSA-certificate validity or revocation, and it does not prove the action was correct or authorized. - Currency is a separate axis from offline authenticity.
checks.currencypasses only on statusfresh; bothstaleand the honest offline defaultunknownfail the opted-in gate, because offline verification can never establish currency. Readresult.currency.currency_at_Tto tellunknown(offline only) apart fromstale. - Consumption proof proves the tree-shaped consumption facts only. Checkpoint signatures and currency of the later head are the caller's responsibility.
- Initiator attestation says which software asked; it does not prove the software behaved (the labels are self-asserted, and the digest is authentic-as-supplied, not proof of correct execution).
- Revocation statements prove only what the presented, pinned statements
establish. Absence from a supplied list is not proof of current
non-revocation; without a current authenticated status source the result
remains
unknown.
Both the witness and consumption profiles now ship a verifier and a reference emitter, so the emit/verify loop is closed at reference level. A third party can PRODUCE these artifacts, not only check them:
- Witness (EP-WITNESS-v1). The reference witness emitter is the cosigner service in
witness/(witness/server.mjs). It imports the signing digest and domain tag from this package (witness.js), so a cosignature it emits is byte-identical to whatverifyWitnessCosignature()/requireWitnessQuorum()check. - Consumption (EP-SMT-CONSUME-v1). The reference issuer-side emitter is
ReferenceConsumptionTreeinconsumption-proof.js, exported as@emilia-protocol/verify/consumption-proof.js. It maintains the sparse consumption tree and emits the non-inclusion / inclusion sub-proofs in the exact wire formatverifyConsumptionProof()accepts, so anyone can reproduce a full bundle.
Reference emitters pin the wire format; they are not production infrastructure. A production issuer maintains its own sparse consumption ledger (not the in-memory reference tree), and the security of the witness leg comes from RUNNING several independent witnesses under separate operators and comparing their views. That ecosystem step is deployment, not reference code.
All five of these profiles (EP-WITNESS-v1, EP-CURRENCY-v1, EP-SMT-CONSUME-v1, EP-INITIATOR-ATTESTATION-v1, and timestamp proof (RFC 3161)) are now ported to Python (packages/python-verify) and Go (packages/go-verify) and run cross-language in conformance/run.mjs over shared vector suites (currency.v1.json, initiator-attestation.v1.json, consumption-proof.v1.json, witness.v1.json, timestamp-proof.v1.json), where the JavaScript, Python, and Go verifiers must agree. The RFC 3161 timestamp-proof ports keep the package's dependency posture: the JS minimal DER/CMS reader was hand-ported to pure Python (with cryptography used only for the RSA/ECDSA signature verify, so no new dependency) and to pure-stdlib Go, and all three lanes agree over real openssl-minted TimeStampTokens, including the exact per-vector refusal path. As always, this is one team's three-language ports (a consistency check), not clean-room independent implementations.
Federation (PIP-006) — requires 1.3.0
Cross-operator verification: accept a receipt issued by a different EP operator using only its published discovery surfaces.
import { verifyFederatedReceipt, verifyFederatedReceiptOffline } from '@emilia-protocol/verify';
// Online: resolves the issuer's keys from a caller-pinned discovery URL and
// checks its revocation surface. Treat receipt.signature.key_discovery as a
// hint, not a trust root.
const verdict = await verifyFederatedReceipt(receipt, {
keyDiscoveryUrl: 'https://op-a.example/.well-known/ep-keys.json',
expectedSigner: 'ep:operator:op-a',
networkBoundary: {
resolveAddresses: resolveEveryAddress,
fetchPinned: fetchWithoutReresolving,
},
statusVerifier: verifyPinnedCurrentStatus,
});
// { accepted, verified, revoked, signer, keyMatched: 'current'|'historical', checks }
// If the live revocation surface is unavailable, a valid signature remains
// verified:true but accepted is false until status can be confirmed.
// Air-gapped: supply the issuer's ep-keys.json + revocation set yourself.
const offline = verifyFederatedReceiptOffline(receipt, discoveryDoc, { revokedReceiptIds });The network boundary must reject the whole DNS answer set unless every address
is public, connect directly to one approved address without re-resolving, retain
hostname TLS/SNI validation, report the connected address, and refuse
redirects. A plain injected fetch is deliberately insufficient against DNS
rebinding.
resolveOperatorKeys(discoveryDoc, signerId) is also exported (current keys
first, then historical_keys whose signed issued_at is no later than a valid
retired_at). See
docs/FEDERATION-REGISTRY.md for the operator discovery convention.
Design Principles
- Zero dependencies — Only
node:crypto. No supply chain risk. - Offline-first — Core verification makes no network calls. The optional federation online path requires an explicit resolver plus pinned transport; no EP-operated server is required.
- Deterministic — Canonical JSON serialization for reproducible signatures.
- Auditable — A few small files, ~1,000 lines total. Read the entire thing in an hour.
How It Works
Receipt Document (EP-RECEIPT-v1)
├── payload (canonical JSON)
├── signature
│ ├── algorithm: "Ed25519"
│ ├── signer: "ep_entity_..."
│ └── value: base64url signature
└── anchor (optional)
├── leaf_hash: SHA-256 of receipt
├── merkle_proof: [{hash, position}, ...]
├── merkle_root: root hash
└── chain: "base-sepolia"
Verification:
1. Canonicalize payload → sorted-key JSON
2. Verify Ed25519(canonical_payload, signature, public_key)
3. If anchor: reconstruct Merkle root from proof, compareGetting Public Keys
Signer public keys are discoverable at /.well-known/ep-keys.json on any EP operator:
curl https://ep.example.com/.well-known/ep-keys.jsonCarrier-neutral action-crossing records
aeb-crossing-record.js issues and verifies EP-AEB-CROSSING-RECORD-v1: a
signed record that one relying-party boundary evaluated one exact action under
one verified native authority instance. The record binds the native replay
unit, content-addressed mapping profile, exact action, admission and
consumption evidence, audience, executor, and owning state domain.
import {
mapWimseOAuthCrossingAuthority,
issueAebCrossingRecord,
verifyAebCrossingRecord,
} from '@emilia-protocol/verify/aeb-crossing-record';Native authority is an open set behind one closed projection contract. The package includes reference mappings for a WIMSE/OAuth authorization-server decision and an EMILIA bounded-capability receipt. Those mappings do not claim native semantic equivalence and do not produce identical record bytes. A conformant boundary can narrow native acceptance to a local refusal; it can never broaden rejected, stale, or indeterminate authority into admission.
The record requires Ed25519 and ML-DSA-65 over the same signed body. Verifying the record proves only that the pinned record signer made the recorded claim. It never authorizes a new crossing, proves a physical effect, or replaces the native authority system. Run the deterministic hostile suite with:
npm run conformance:composition:crossing-recordVersion 2 admission-domain binding
EP-AEB-CROSSING-RECORD-v2 uses a distinct signing domain and adds a typed
digest over relying_party_id, audience, executor_id, and
state_domain_id. The v2 verifier recomputes that digest from the signed
record. Generic v2 issuance also requires the evaluated action and admission
domain as separate inputs and refuses a mismatch before signing.
Use issueAebCrossingRecordV2 and verifyAebCrossingRecordV2 for new records
that need this guarantee. Keep using the v1 verifier for historical v1
records. Relabeling does not upgrade a v1 record, and neither version is an
authorization for a later crossing. See
docs/protocol/aeb-crossing-record-v2.md
for migration and retirement rules.
A crossing record and a lifecycle index cite an evaluation digest, but the
verifiers join that evaluation to the record only when you pass it as
evaluation; the result then reports evaluation_binding as BOUND or
MISMATCH. Without it the digest is an unverified pointer and the binding is
INDETERMINATE. BOUND is a join, not authentication, so verify the
evaluation itself with verifyAebEvaluation() or verifyAebEvaluationV2().
See evaluation binding.
Reliance gap reports (acceptance preflight)
reliance-gap.js wraps the reliance kernel (reliance.js) into a diagnostic:
given a de-identified action packet and a relying party's pinned
EP-RELIANCE-PROFILE-v1, it emits one deterministic EP-RELIANCE-GAP-REPORT-v1
with the kernel verdict passed through verbatim, a missing-evidence list
(each entry: requirement, why it matters, how to close it), the JCS+sha256
action digest, the pinned profile digest, a plain-language control mapping
(authority, identity, freshness, revocation, consumption, signoff, audit
trail), a closed limitations list, and the exact command that reproduces the
report offline.
import { buildRelianceGapReport } from '@emilia-protocol/verify/reliance-gap';
const report = buildRelianceGapReport(
{ action, evidence, context }, // the packet
profile, // the relying party's pinned rule
{ now: '2026-07-08T15:00:00Z' }, // evaluation time (never the wall clock)
);The packet's evidence is an array of artifacts, either { type, artifact }
envelopes or bare artifacts detected by shape (receipt, quorum,
authority_proof, revocation_state, consumption). Artifact types with no
registered verifier are recorded as unverifiable_present and never count
toward satisfaction. The packet's context carries the relying party's
verification material: approver_keys, log_public_key, rp_id,
revoker_keys. The profile argument accepts a bare profile or a signed
EP-RELIANCE-PROFILE-REGISTRY-v1 entry (unwrapped; the entry's profile_id is
reported).
Determinism contract: no wall-clock reads (evaluation time comes only from
opts.now or packet.evaluated_at; absent both, the builder refuses with a
reason), keys sorted, arrays stable, so the same inputs reproduce the same
bytes. buildMultiPartyRelianceGapReport evaluates the SAME packet against
several profiles and emits one combined EP-RELIANCE-GAP-MULTI-v1 report.
From the CLI:
npx @emilia-protocol/verify reliance-gap packet.json --profile profile.json
npx @emilia-protocol/verify reliance-gap packet.json --profiles ./profiles \
--now 2026-07-08T15:00:00Z --out report.jsonExit codes: 0 = rely (all rely in --profiles mode), 2 = any
do_not_rely_*, 1 = operational error. Fully offline; no network access.
A worked five-relying-party example lives in examples/reliance-gap/ at the
repository root. A single gap report is the per-action preflight;
EP-ASSURANCE-PACKAGE-v1 (packages/gate/reports/assurance-package.js)
bundles a population of such reliance decisions so an independent assurer can
re-perform every verdict offline.
Structured Authorization Evidence Chains
Import createAuthorizationChainEvaluator from
@emilia-protocol/verify/evidence-chain. Construct it with a closed
EP-AEC-REQUIREMENT-v1 requirement and receiver-owned nativeVerifiers.
Each registration pins its native profile, trust snapshot and optional
mapping. These inputs must not come from the presented chain.
Call evaluate(chain, { expectedAction, verificationTime }) using the
executor's expected action and trusted explicit time. The result reports
satisfied, authorization_decision: false, and an EP-AEC-REPLAY-v1
record. replay(chain, recordedReplay, inputs) re-verifies the original
evidence under the constructor's pins; a saved positive result is not trusted
evidence. Required subject thresholds count only native-verified identities,
and required relations must bind the target evidence digest.
Replay records carry algorithm_revision EP-AEC-EVALUATOR-08-v1. replay
compares the complete record only when it was made by that revision and
reports comparison as MATCH, MISMATCH, UNSUPPORTED_REVISION or
RECORD_INVALID. A stored EP-AEC-EVALUATOR-07-v1 record is reported
UNSUPPORTED_REVISION: it is not relabeled or recomputed under -08, and the
returned result is a new -08 record with its own digest.
Requirement expressions
The requirement expression is parsed once, when the evaluator is
constructed, and evaluation runs on that tree. A malformed expression or one
over the fixed caps (4096 UTF-8 octets, 256 tokens counting identifiers,
operators and parentheses, 32 levels of nesting) is refused at construction
with aec_requirement_invalid; no replay record exists for it. A
requirement that is not strict I-JSON, such as one whose expression holds a
lone surrogate, is refused at construction by the strict JSON check instead.
The lexer
takes the longest identifier run before classifying it, so aORb and ORb
are identifiers and only an exact AND or OR is an operator; lowercase
and and or are ordinary identifiers, and identifiers are case-sensitive.
Only space, tab, CR and LF separate tokens. Operators group left to right:
a OR b AND c means ((a OR b) AND c). AND and OR remain valid native
component types; only the expression reserves them.
compileAecRequirementExpression(expression) and
evaluateAecRequirementExpression(expression, eligibleTypes) expose the same
parse as diagnostics: syntax VALID or INVALID, the refusal class
(syntax or limit), the Boolean value, the canonical fully parenthesized
parse and a parse identity (sha256: over EP-AEC-EXPRESSION-PARSE-v1, one
zero octet and the canonical parse). The evaluator exposes the same values as
requirement_expression. The parse identity says how an implementation
grouped the expression; a matching parse identity does not guarantee a
matching verdict, and it never travels in the v1 requirement or replay
objects. The requirement profile digest still commits to the expression
exactly as stored. The frozen corpus is
conformance/vectors/aec-expression.v1.json.
A native verifier callback returns two separate results, as AEC-07 Section 6
requires. verified says the artifact's cryptographic and structural checks
passed under the verification key. accepted says the relying party's pinned
trust inputs for that component type (trust anchors or key directory entry
status, issuer, audience, key class, native policy, validity at the
verification time) accept the verified artifact. verified: null says the
check could not run, for example because no key could be resolved for a format
that names its key by reference (reason: 'key_unresolved'); the fact records
NOT_EVALUATED, not FAILED. A callback that evaluated the checks but cannot
tell which of the two a failure belongs to returns verified: false. accepted: true without verified: true is refused
as native_result_inconsistent, and the single valid Boolean of 5.x is
refused as native_result_legacy_valid_field. Each replay fact records
native_verification (VERIFIED, FAILED or NOT_EVALUATED) and
acceptance (ACCEPTED, REJECTED or NOT_EVALUATED) as separate fields, and
a component is eligible only when both are positive. Action mapping runs only
on an accepted artifact. The built-in ep-quorum verifier checks integrity
under the keys the quorum carries; the built-in ep-receipt,
ep-authorization-bundle and platform-attestation verifiers resolve each key
by reference from the relying party's key material, check signatures under the
resolved public key only, and treat the directory entry's status as an
acceptance input. An unresolvable key is NOT_EVALUATED. The built-ins check
pins against the action the artifact commits to, so an accepted artifact for
another action fails material-action matching rather than acceptance.
The older verifyAuthorizationChain string-requirement API is a separate
legacy interface, not the complete structured AEC-05 contract. Its component
rows keep one combined valid flag for verification and acceptance. The explicit
ep-authorization-bundle role is an additive AEC-06 profile; it must not be
substituted for the terminal ep-receipt role. Neither evaluator reserves
authority or authorizes execution. Custom native verification and mapping
callbacks are trusted code; isolate untrusted synchronous code in a worker
or process. See aec-current-profile.test.ts for executable configurations
and refusal cases.
License
Apache-2.0
