Skip to main content

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).

  • seq is a strict monotonic uint64 per tenant; gaps are tamper evidence
  • prev_hash = sha256(canonical_json(prev_entry_without_entry_hash)) chains entries
  • entry_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

ClassRetentionUse case
soc2-7y7 yearsSOC2 Type II evidence; default for charge events.
eidas-10y10 yearseIDAS 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

  1. Pull the published JWKS at /oauth/jwks.json.
  2. Verify the chain-head JWS against the signer_kid.
  3. Re-compute entry_hash for each returned entry and check the chain back from head_entry_hash.
  4. If any prev_hash mismatches: tamper detected.
  5. If seq has a gap: tamper detected.