1. The Receipt Envelope
A Decision Receipt on the wire is exposed as{ context, result, chain_id, chain_step }. The cryptographic
centre of gravity is result (the EvaluationResult).
1.1 v1 vs v2 — which fields exist
Top-level (alongside the receipt, not inside
result): id (UUID),
tenant_id, actor (DecisionActor JSON), chain_id, chain_step,
parent_decision_id, created_at (recorded-at, distinct from
result.timestamp).
Migration semantics. v1 receipts continue to verify forever. There is no
cut-over date. Cross-version fallback is rejected — a v2 receipt must
verify under the v2 path; a v1 receipt must verify under the v1 path.
The same discipline governs the three shadow-mode fields, which joined the
hash payload after both schema versions had shipped: a receipt is hashed over
the keys it actually carries, so receipts minted before a field existed keep
verifying unchanged (§1.6, §2.2).
1.2 Decision Context (the input)
Type:DecisionContext.
context.evidence[] is the legacy reference-only evidence list. The richer
Evidence Manifest (§5) is a separate first-class concept and its digest is
stored on result, not context.
1.3 Source Artifact (context.source_artifact)
Type: SourceArtifact. Reference-based: stores
{ type, hash, hash_algorithm: 'SHA-256', reference_id?, filename?, byte_size? }.
Only hash is bound into integrity_hash. Type, filename and size are
convenience metadata and intentionally outside the hash.
This is the older single-artifact binding (“Trust Without Disclosure”). It
co-exists with the Evidence Manifest in v2; deprecation is out of scope.
1.4 Actor (the actor field)
Type: DecisionActor.
metadata.actor_id on context is bound into the integrity hash via
context_hash; the structured actor row is stored alongside but not
part of the integrity hash.
1.5 Action (result.action)
Type: ReceiptAction. Shape: { type, reference_id, metadata? }.
The whole action object is bound into integrity_hash — metadata
included. The payload binds action as a single value, not a selection of
its keys (§2.2), so changing any part of it changes the hash. This is unlike
context.source_artifact (§1.3), where only hash is bound.
1.6 Shadow-mode fields (result.is_shadow, result.shadow_converted, result.original_decision)
Three optional fields on result record how shadow-mode policies took part in
the decision.
DENY with at least one violation, where every
violation carries is_shadow: true, is recorded as ALERT;
shadow_converted: true and original_decision: 'DENY' are written alongside
it. DENY is the only convertible verdict — an all-shadow REVIEW stays
REVIEW. The conversion happens inside the evaluator, before the hash is
computed, so the suppressed verdict is part of the signed surface rather than a
note attached afterwards.
Participation is not suppression. is_shadow says a shadow-mode rule was
applicable; shadow_converted says a shadow-mode rule changed the verdict.
A receipt can carry is_shadow on its own — a shadow rule ran, but the outcome
was not an all-shadow DENY. violations[i].is_shadow (§1.1) is a third,
separate field: it marks one violation as coming from a shadow-mode policy, and
it is bound because the whole violations array is bound.
Where the stamp comes from. Shadow status reaches the evaluator on the
rules of the frozen policy snapshot, not from live policy state. Replaying a
receipt against its own snapshot therefore re-derives the same conversion, and
a snapshot whose shadow stamps were altered no longer reproduces the receipt.
Binding. All three are bound into integrity_hash when present, on the v1
and v2 paths alike (§2.2). None of them is named in the v2 signing envelope;
the signature covers them transitively, because the envelope binds
integrity_hash (§2.3).
2. Cryptographic Model
2.1 Canonicalisation profile
meshqu-canonical/v0 — frozen. Alphabetic key order, JS Array.sort. RFC 8785
alignment is a planned P1 follow-up that will introduce
meshqu-canonical/v1.
2.2 Integrity hash — exact payloads
v1 input —sha256(canonicalJson(payload)) where payload is, alphabetised:
undefined, and the value is then bound verbatim. shadow_converted and
original_decision are a pair — the payload carries both keys or neither, and
original_decision is coerced to null if the pair is written without it.
is_shadow dispatches independently of that pair, because the two joined
the payload at different times; a receipt may bind one, the other, both or
neither.
Where a field is absent, its key is absent. Receipts minted before a field
joined the payload — and every receipt where MeshQu had nothing to write —
therefore hash over the payloads exactly as listed above, unchanged, and go on
verifying. Stripping a field from a receipt that carries it, or adding one to a
receipt that does not, changes the recomputed hash and fails verification.
There is no cross-epoch fallback. Key order stays alphabetic however these keys
are added (§2.1).
Not in the integrity hash: transparency_anchor (computed after signing),
chain fields, evaluation_time_ms, rules_evaluated / rules_na /
na_rules, the actor row, source artifact metadata.
2.3 Signing — what is signed
- v1 signs the bare 64-char hex
integrity_hashstring, UTF-8 encoded. - v2 signs the canonical envelope, alphabetised:
signature_kid, signature_algorithm and
receipt_schema_version into the signed bytes — closing the gap where v1
stored these alongside but not inside the signature. The verifier
reconstructs envelope bytes using the receipt’s stored signature_kid /
signature_algorithm (not hardcoded) so any tampering of those fields fails
the signature check.
Hash coverage is not envelope coverage. The v2 envelope has exactly the
seven fields listed above. Every other receipt field the signature covers —
including the shadow-mode fields of §1.6 — is covered transitively, through
integrity_hash: the signature attests to the hash, and the hash is computed
over those fields. v1, which signs the bare integrity_hash string, covers
them the same way. Being inside the integrity hash and being named in the
signing envelope are separate properties; §14 lists which fields have which.
Algorithm: Ed25519 only in v1+v2. Other algorithms require a manifest-version
bump.
2.4 Policy snapshot digest (v2)
computePolicySnapshotDigest(snapshot) =
sha256(canonicalJson({ id, rules, policy_versions, created_at })).
This binds the content of the policy, not just its UUID. Approval-receipt
digests live on policy_versions[i].approval_receipt_digest, so approval
lineage is bound transitively through this single digest — there is no
separate top-level approval digest on the receipt.
2.5 Evidence manifest digest (v2, optional)
sha256(canonicalJson(manifest minus manifest_digest)). Self-describing
pattern (mirrors the bundle manifest). MeshQu computes and verifies this
digest as part of v2 receipt and bundle handling.
2.6 Active-rule projection (used everywhere rules are hashed)
projectActiveRules strips inactive rules and all metadata, keeping only
{ code, condition, severity, when? }. This single helper is used by:
computeSnapshotHash(snapshot repo + evaluator)PolicyApprovalReceipt.policy_rules_hashPolicySnapshot.policy_versions[i].policy_rules_hash- the bundle verifier when comparing per-version hashes
3. Policy Snapshot
A frozen, immutable copy of the rules that governed a decision. Identifies the policy run by both UUID and content digest in v2.id(UUID, on the receipt aspolicy_snapshot_id)rules[]— fullPolicyRule[]. Bundled inpolicy_snapshot.json; not stored on the receipt itself.policy_versions[]— list of{ policy_id, version, policy_rules_hash, approval_receipt_digest? }created_at(ISO 8601)evaluated_rules_hashon the receipt =sha256(canonicalJson(projectActiveRules(rules)))policy_snapshot_digeston the receipt (v2) =sha256(canonicalJson({ id, rules, policy_versions, created_at }))
snapshot_replay.
4. Approval Receipts (lineage)
Status: Implemented. Issuance, storage, bundle export, and both the Node and browser verifiers support approval lineage.- Policy approval receipts are a first-class artifact. Each receipt holds a
ratifier_signature(Ed25519, kid + algorithm),policy_rules_hash(viaprojectActiveRules) and a self-describingapproval_receipt_digest. - A bundle ships these in
policy_approval_receipts.json. - The verifier sub-claim
approval_lineageactivates when the receipt is v2 and the bundled snapshot has at least one non-nullpolicy_versions[i].approval_receipt_digest. For every such version the verifier checks:- The receipt is present in the bundled file (keyed by
policy_version_id). computeApprovalReceiptDigest(receipt)equals the snapshot entry’sapproval_receipt_digest.receipt.policy_rules_hashequalsentry.policy_rules_hashby string equality (do not re-hashsnapshot.rules— the precomputed per-version hashes cover only that version’s rules, and the snapshot entry is itself bound bypolicy_snapshot_digest).receipt.ratifier_signature.kidresolves in the caller-suppliedratifierTrustedRoots.- The ratifier Ed25519 signature verifies.
- The receipt is present in the bundled file (keyed by
- Reports
not_applicablefor v1 receipts and v2 receipts whose snapshot has no non-nullapproval_receipt_digestentries. - The same approval-lineage checks run in both the Node and browser verifiers.
- A missing receipt reports
approval_lineage.receipt_absent; an unreadable file or receipt reportsapproval_lineage.receipt_malformed; and a readable receipt whose digest disagrees with the signed snapshot binding reportsapproval_lineage.digest_mismatch. All three are hard failures (invalid, exit 5), but only the last establishes that a digest was compared.
5. Evidence Manifest (v2 only)
Type:EvidenceManifest. Strictly additive — does not modify v1 receipts or
source_artifact.
EvidenceItem:
custodianTrustedRoots.
The manifest can carry a mix of signed and unsigned items. The manifest digest
covers all items; the per-item signature is an additional, independent
attestation by the custodian.
Reference-based — the artifact bytes are NOT stored. Only digests,
references, and metadata.
6. Transparency Anchor (Rekor)
Type:TransparencyAnchor. Stamped onto the receipt after signing, so it
is not part of integrity_hash.
- Anchoring. Implemented. Receipt is submitted to Rekor; anchor data captured.
- Offline SET verification. Implemented. The verifier reconstructs the
canonical message Rekor signed (
{body, integratedTime, logID, logIndex}) and checks the SET against a trusted Rekor public key. - Offline subject-digest binding. Implemented when
dsse_envelopeis present. The verifier decodes the DSSE envelope, reads the in-toto Statement and assertssubject.digest.sha256 == receipt.integrity_hash. - Inclusion-proof verification. Shape-checked only. The verifier emits
transparency.inclusion_proof_invalidwhen the bundled proof’sinclusion_proofis present and malformed. The cryptographic binding comes from the SET, so the verifier also emits the advisorytransparency.inclusion_proof_unverifiedwhenever it cannot complete SET verification — for a legacy anchor lacking the new fields, and whenever the caller supplied no Rekor trust roots, which is the default. That code is therefore not a legacy-only signal.
transparency_proof.json. What the verifier reports when it does not is §6.1.
Online Rekor access splits into three layers. Read them separately — the
differences are where public copy usually goes wrong:
- Online entry-existence lookup: Implemented.
verifyRekorInclusion()queries the log by entry UUID and reports whether an entry exists there. It is exported from@meshqu/coreand tested. The shipped browser verifier also makes live, host-allowlisted calls to Rekor when validating an anchor. This confirms an entry is present; it does not verify an inclusion proof. - Bundle-verifier wiring: Inert.
BundleVerificationOptions.onlineexists but is documented as “reserved for future wiring”. Bundle verification is offline by default and setting the flag changes nothing today. - Co-signed-witness inclusion-proof verification: Planned. Verifying an inclusion proof against an independently co-signed log witness is not implemented. Offline SET verification against a pinned Rekor log key remains the guarantee MeshQu stands behind.
6.1 What a bundle reports for an anchor
The anchor is recorded on the receipt, not proved by it. It is written after signing and is deliberately not covered by the integrity hash (§14). What the bundle verifier reports therefore depends on what the bundle carries, and there are four distinct outcomes: The branches are tested in this order, so the more specific case wins:
Verdicts and exit codes are §10.5. An older anchor (no
dsse_envelope /
entry_body) falls in B: the exporter deliberately ships no proof for it,
so the verifier has nothing to check the claim against.
not_applicable means no check was required; not_checked means one was not
performed. An anchor without a bundled proof belongs in the latter category.
Note the shape of D in the default configuration: with no Rekor trust roots
supplied, a correctly anchored bundle reports transparency: invalid carrying
only the advisory transparency.inclusion_proof_unverified, which rolls up to
warn and exit 0. A sub-claim reading invalid is not by itself a failed
bundle — the failure codes decide (§10.5).
Two consequences to state plainly on any public surface:
- An anchor displayed on a receipt is a claim recorded, not a claim
verified.
provider,log_index,entry_uuidandanchored_atrender as facts on every surface that displays a receipt. Only A and D involve the verifier checking anything about them. - The verifier cannot distinguish “never anchored” from “anchor removed”.
Because the anchor is not bound by the integrity hash, a receipt whose anchor
was stripped to
nullpresents the same claim as one that was never anchored, andtransparency_anchor: nullis a legitimate state that unanchored runs export on purpose. Do not read C as evidence that no anchoring took place.
7. Decision Chains
A chain is an ordered group of receipts that together prove a workflow.7.1 Linkage on the receipt
ChainLinkage — top-level fields on the receipt:
chain_id(UUID, caller-provided)chain_step(1-based, auto-assigned if omitted)parent_decision_id(UUID, optional — explicit causal lineage)
7.2 Chain proofs
The chain-proof artifact (chain_proof.json in a bundle). Each non-first
step gets:
chain_integrity_hash= sha256 over{ chain_id, step, decision_integrity_hash, previous_chain_hash }chain_signature(Ed25519 over the chain integrity hash)
chain_integrity_hash changes.
7.3 Chain seal
The chain-seal artifact (chain_seal.json in a bundle):
seal_hash= sha256 over{ chain_id, sealed_at, steps_count, ALL receipt_hashes (ordered) }seal_signature(Ed25519 over seal hash)sealed_at(ISO 8601)
7.4 Verification result type
ChainVerificationResult exposes chain_proof: 'full' | 'partial' | 'none' | 'failed', per-step status, warnings (gap, parent_not_in_chain,
parent_step_not_lower, chain_id_mismatch, proof_boundary) and seal
status.
8. Decision Outcomes (post-decision tracking)
Separate from the receipt itself. A decision-outcome record carries:9. Audit Trail
Append-only, hash-chained. Each event carries:previous_hash- per-event
integrity_hash= sha256 over{ action, entity_type, entity_id, actor_id, tenant_id, changes, timestamp, previous_hash }
decision_recorded audit event is written when a decision is recorded.
10. Verification Bundle
A self-describing tar / JSON archive that lets a third party verify a receipt offline. See also the standalone Verification Bundle page; this section focuses on the contract relevant to the receipt.10.1 Manifest (bundle_manifest.json)
10.2 Files in a bundle
trusted_keys.json is pinned at export time. Rotating signing keys does
not retroactively invalidate already-issued bundles.
10.3 Verifier sub-claims
Every relevant sub-claim is reported (not just the first failure). Each has astatus and a failure_codes: string[]. There are four statuses:
not_applicable and not_checked are not interchangeable, and the
distinction is load-bearing rather than stylistic: the first asserts that no
check was required, the second that one was not performed. Only the second
carries verification debt.
Shadow-mode fields and the two hash-comparing sub-claims.
integrity and
snapshot_replay both recompute a hash, so both have to account for the
presence dispatch of §2.2. A fresh replay re-derives shadow conversion from the
shadow stamps on the bundled snapshot’s rules, and the replay result is then
normalised to the fields the stored receipt actually carries before the
hashes are compared. The normalisation removes the shadow
keys only — never the verdict — so a stored decision that disagrees with what
the bundled snapshot produces still fails snapshot_replay.hash_mismatch, and
a receipt whose shadow fields were stripped or added after signing fails
integrity.mismatch.
10.4 Failure codes
These are the codes this page describes. It is not the complete set.@meshqu/typesdeclares 57BundleFailureCodemembers. 42 are listed below. The other 15 are emitted by the shipped verifier and are not yet described here.@meshqu/verify-lib— the browser verifier, a deliberately independent implementation — declares its own union carrying 17 further codes. Some are advisory or apply to browser-specific checks; interpret each code together with its verification status.- Across both packages the union is 74 codes, of which 32 are not listed below.
10.5 Verdict roll-up and exit codes
The ten sub-claims roll up to one headline verdict, with precedenceinvalid > indeterminate > warn > valid:
An advisory code is one the verifier emits to disclose a scope limit rather
than to report a cryptographic failure.
transparency.inclusion_proof_unverified
and snapshot_replay.skipped_in_browser are the two this page names; the set is
not fully documented here (§10.4). A sub-claim is downgraded only when every
code on it is advisory — a real failure alongside an advisory one always wins,
so “we could not check X” can never launder “X failed”.
5 and 7 answer deliberately different questions. Exit 5 says a check ran and
the claim is refuted. Exit 7 says a claim was made and could not be established:
the artefact is not accused of anything, and it has not passed either.
Automation gating on verification should pick the question it means:
-eq 0— nothing is wrong. Rejects unchecked claims.-ne 5— nothing is proven wrong. Accepts unchecked claims.
11. Selected API endpoints
Selected receipt and decision endpoints are listed below. The API Reference has the full customer-facing endpoint set.
These public receipt, chain, and form paths do not require an API key or tenant
header. Possession of a receipt, chain, or form identifier may permit access to
the corresponding resource or submission to the form. See
Authentication → Public paths
for their access model.
Public receipt response shape:
{ context, result, chain_id, chain_step }.
The API normalises legacy policy_snapshot_hash → evaluated_rules_hash on
the way out.
12. Persistence Model (summary)
MeshQu persists each of these as a distinct artifact, surfaced through the REST API and verification bundles:- Receipts — immutable once written; hash, signature and timestamp fields cannot be updated or deleted after they are recorded.
- Chain proofs — per-step chain integrity hashes and signatures.
- Chain seals — a completeness seal per chain.
- Decision outcomes — post-decision status (override / escalate / etc).
- Policy snapshots — frozen policy bundles.
- Policy approval receipts — ratifier-signed approval records.
- Audit events — an append-only, hash-chained audit log.
13. Key Management
- Signing key kid format:
msk_v{N}(e.g.msk_v1). - Algorithm: Ed25519 only.
trusted_keys.jsoninside a bundle is informational only and is never used as a trust root. Treating it as one would let a self-signed bundle — operator-controlled keys listed in the bundle as “trusted” — verify against itself. The verifier refuses instead: with no external roots supplied it emitssignature.no_external_trust_root(§10.4) rather than falling back to the bundled listing.- Production verification requires trust roots supplied out-of-band. The
verifier resolves a receipt’s
signature_kidonly against roots its caller passes in; with none supplied it emitssignature.no_external_trust_rootrather than falling back to anything. How a verifying party obtains and pins those roots is that party’s own policy — it is not something the issuer’s own API can settle, and this page does not tell you to fetch them from one.GET /v1/.well-known/signing-keysin particular is deprecated and self-asserted — the producer vouching for itself — and must not be treated as a trust root. - The /verify demo is not an exception to that rule so much as a demonstration
of it: it verifies against a bundled demo key (
msk_demo_v2) and makes no trust-rooted claim. - Key rotation does not retroactively invalidate bundles — a rotated-out
key still verifies a receipt it signed, provided the verifier’s out-of-band
root set still carries it. The bundle’s
trusted_keys.jsonrecords which keys were current at export time, which is useful for a fingerprint UI; it is not what makes the old signature verify. - Key standing is a separate check from the signature. A signature that
verifies shows which key signed; it does not show that the key is still in
good standing. Given a verified signed key registry, the verifier also
reports where the signing key stands at the moment of verification: in good
standing, revoked, retired, expired or not yet valid. Given none, it reports
the standing as not checked. That is not a failure, and it is not
evidence the key is in good standing. No signed key registry is published,
so verify.meshqu.com, and the CLI without
--trust-registry, report not checked for every signed receipt, and verify.meshqu.com headlines such a receipt Signature Verified rather than Receipt is Valid. - Custodian (evidence) keys and ratifier (approval) keys are supplied to the
verifier out-of-band via
custodianTrustedRoots/ratifierTrustedRoots. MeshQu never holds them.
14. What Is and Isn’t Bound — quick reference
Bound into v1integrity_hash: decision, violations, context (minus
source_artifact, which is stripped whole), source_artifact.hash, action
in full — metadata included (§1.5) — policy_snapshot_id,
evaluated_rules_hash, timestamp.
Additionally bound into v2 integrity_hash: receipt_schema_version,
policy_snapshot_digest, evidence_manifest_digest (or null).
Conditionally bound into integrity_hash — v1 and v2 alike — when the field
is present on result: is_shadow, shadow_converted, original_decision
(§1.6). Presence-dispatched: absent field, absent key, unchanged legacy payload
(§2.2).
In the v2 signed envelope (§2.3) — seven fields exactly, in two groups:
- Envelope only, not in the integrity hash:
signature_kid,signature_algorithm. - In the envelope and in the integrity hash:
timestamp(bound by the v1 and v2 hash alike),receipt_schema_version,policy_snapshot_digest,evidence_manifest_digest.
integrity_hash itself, which is what makes the
signature’s coverage of every other field transitive (§2.3).
Stored on the receipt but NOT hashed: transparency_anchor, chain
linkage, evaluation_time_ms, rules_evaluated, rules_na, na_rules,
source artifact metadata (type / filename / byte_size / reference_id),
the structured actor row, decision outcomes.
Bound transitively via policy_snapshot_digest: all
policy_versions[i].approval_receipt_digest entries (when present), all
policy_versions[i].policy_rules_hash entries — therefore approval lineage.
15. Capability status
16. Terminology
17. Interpretation limits
- MeshQu binds evidence references and digests, not the underlying evidence
content. For a
source_artifact, onlysource_artifact.hashis bound into the receipt’s integrity hash; type, filename, and size are descriptive data. - Rekor anchoring and offline Signed Entry Timestamp verification are implemented. Independent online inclusion-proof verification against a co-signed live witness is not currently available.
18. Agent Licences
A first-class signed record answering: which named person authorised this API credential to act, under which ratified policy version, for which decision types, until when — and has that authorisation been withdrawn? This section describes the current signed record format and its verification behaviour.18.1 The record
Two record types share one shape and one table: a grant, and a revocation that supersedes it. Revocation is a new signed record naming the grant — never an edit or a deletion. The store is append-only. The signed preimage is exactly 14 fields:id, tenant_id,
record_type, key_id (the subject — an API key, not a person and not a
model), revokes_licence_id, granted_by (the authenticated user id captured
at issue), grant_authority (free text, stored verbatim), policy_id,
policy_version_id, policy_version_number, scope (decision types, matched
exactly), valid_from, valid_until, issued_at.
licence_digest = SHA-256 over the canonical JSON of that preimage.
grantor_signature is Ed25519 over the digest, under a ratifier key —
policy-governance authority, disjoint from receipt-signing keys by
construction. Verification recomputes the digest before checking the
signature: a genuine signature over a stale digest proves nothing about an
edited record.
18.2 Lifecycle and admissibility
Issue and revoke are human governance acts: the routes reject any API-key principal before the handler runs. A deployment resolves licence admissibility on every receipt-minting call under one of three modes —off,
record_only, enforce — where the effective mode is the maximum of the
deployment default and the tenant setting. In record_only, the admissibility
status is stamped into the decision receipt’s hashed context and nothing is
refused; in enforce, an expired, out-of-scope, revoked or missing licence
fails closed with a distinct error code and no receipt is minted. The mode
itself is stamped into the hashed context, so a record-only receipt is
distinguishable from an enforced one from the receipt alone.
18.3 Verification and the public lookup
GET /v1/receipts/:id (public, unauthenticated) resolves a licence id and
returns the grant with its superseding revocation inline (or null), so a
verifier can distinguish verifiably issued from verifiably still in force.
The open verifier (@meshqu/verify-lib/agent-licence) recomputes the digest
from the response bytes, checks the signature against the ratifier trust
roots, verifies any revocation the same way including its binding to the
grant, and reports cryptographic verdict and lifecycle as two separate
axes — a revoked licence is normally cryptographically perfect, and one
badge would have to lie about one of the two.
18.4 What a licence does and does not establish
- It evidences governed delegation by a named accountable person. It does not discharge, transfer or dilute that person’s accountability, and it is not a regulator-granted permission.
- Its subject is an API credential. It does not establish which model or agent implementation acted behind that credential.
- The signature is checked against the trust roots the verifier carries. The stronger claim — verification without trusting MeshQu — awaits the published signed key registry and may not be made until then.