Audit-chain entry envelope
The rule
Every SSF event written to the immutable audit chain is wrapped in this envelope. The chain head is
signed hourly (cron oid4pay_audit_chain_signer); individual entries are not signed (per-entry signing would
be prohibitively expensive at network scale).
seqis a strict monotonic uint64 per tenant; gaps are tamper evidenceprev_hash = sha256(canonical_json(prev_entry_without_entry_hash))chains entriesentry_hash = sha256(canonical_json(this_entry_without_entry_hash))is computed locally and verified on read
Envelope
{
"v": 1,
"seq": 4593821,
"prev_hash": "sha256-base64url",
"entry_hash": "sha256-base64url",
"ts": "2026-05-14T14:33:09.123Z",
"ts_monotonic_ns": 1684931589123456789,
"tenant_id": "merchant-uuid-or-platform",
"actor": {
"type": "agent_client | account | merchant | system",
"id": "opaque pseudonymous id",
"ip_subnet": "10.10.0.0/16"
},
"event": "oid4ac.mandate.issued | oid4ac.payment.succeeded | ...",
"payload": {
"// per-event claim subset; PII allow-list enforced": ""
},
"sig_chain_head_seq": 4593700,
"sig_chain_head_jti": "opaque",
"retention_class": "soc2-7y | eidas-10y"
}Chain head signing
// Hourly cron: oid4pay_audit_chain_signer
// Reads the latest seq + entry_hash per tenant; produces a JWS:
{
"v": 1,
"tenant_id": "merchant-uuid-or-platform",
"head_seq": 4593821,
"head_entry_hash": "sha256-base64url",
"signed_at": "2026-05-14T15:00:00Z",
"signer_kid": "audit-2026-Q2"
}
// JWS alg=EdDSA, signed by a key dedicated to audit signing (separation of
// duties; never reused for JWT-AT or mandate signing).Retention classes
| Class | Retention | Use case |
|---|---|---|
soc2-7y | 7 years | SOC2 Type II evidence; default for charge events. |
eidas-10y | 10 years | eIDAS qualified-trust evidence; used for mandate issuance and revocation. |
Worked example: per-tenant audit pull
curl -sS https://as.oid4pay.com/audit/tenant/<merchant_id>/range \
-H "Authorization: Bearer <merchant JWT-AT scoped to audit:read>" \
-d "since=2026-05-01T00:00:00Z" \
-d "until=2026-05-14T00:00:00Z"
200 OK
{
"tenant_id": "merchant-uuid",
"entries": [
{ "seq": 4593800, "prev_hash": "...", "entry_hash": "...", "event": "oid4ac.payment.succeeded", "payload": { ... } },
...
],
"chain_head_signature": {
"head_seq": 4593821,
"head_entry_hash": "...",
"jws": "<Ed25519 JWS>"
}
}Auditor verification
- Pull the published JWKS at
/oauth/jwks.json. - Verify the chain-head JWS against the
signer_kid. - Re-compute
entry_hashfor each returned entry and check the chain back fromhead_entry_hash. - If any
prev_hashmismatches: tamper detected. - If
seqhas a gap: tamper detected.