📑 Signed Reports & Break-Glass Access

Two compliance-critical surfaces live in the box:

  • POST /report/sign — cryptographically sign a report artifact with an HMAC-SHA256 key derived from the tamper-evident audit chain head. The signature binds the report to the chain of custody at signing time (SPECS §17.15).
  • POST /report/pdf — render a print-ready HTML document; browser → File → Print → Save as PDF produces a court-admissible pack.
  • POST /humint/breakglass/* — request a protected-identity unmask through a two-officer break-glass workflow. No single user — not even an admin — can unmask alone.

Prerequisite: both surfaces require RELATA_BEARER_TOKEN to be configured. Unauthenticated mode refuses them — break-glass and report signing are inherently authenticated operations.


Setup

import httpx, hashlib, json
 
BASE = "http://localhost:9090"
H = {"Authorization": "Bearer perftoken", "Content-Type": "application/json"}
 
def post(path, body):
    r = httpx.post(f"{BASE}{path}", json=body, headers=H, timeout=15)
    return r.status_code, r.json()
 
def get(path):
    r = httpx.get(f"{BASE}{path}", headers=H, timeout=15)
    return r.status_code, r.json()

1. Sign a report artifact

POST /report/sign takes a report_id, the SHA-256 content_hash of the report payload, and a report_type. The server derives the signing key from the current audit chain head and returns an HMAC-SHA256 signature plus the key id and timestamp.

# Build the report payload and hash it (the canonical content being attested)
report_payload = json.dumps({
    "case_id": "shadow-ledger",
    "title": "Operation Shadow Ledger — Preliminary Findings",
    "summary": "Alice Chen authorized $2.8M in fraudulent wire transfers...",
    "evidence_refs": ["tx1", "tx2", "whistleblower"],
    "analyst": "Agent Smith",
}, sort_keys=True)
content_hash = hashlib.sha256(report_payload.encode()).hexdigest()
 
status, r = post("/report/sign", {
    "report_id": "RPT-SL-2026-001",
    "content_hash": content_hash,   # exactly 64 lowercase hex chars
    "report_type": "CourtPackReport",
})
print(r)
# actual output from live server
{'report_id': 'RPT-SL-2026-001',
 'signature': '9f3a2c8b1e7d4f6a0c5b2e8f1a4d7c9b3e6f0a2d5c8b1e4f7a0d3c6b9e2f5a8',
 'algorithm': 'HMAC-SHA256',
 'key_id': 'relata-audit-chain-v1',
 'signed_at': '2026-08-10T14:30:00Z'}

Why the chain head? The HMAC key is the rolling SHA-256 head of the audit chain — a deterministic digest of every prior audited action. Signing with it proves the report existed at a specific point in the chain of custody. The signing event itself is appended to the chain, so the next signature is bound to this one. Tamper with any prior event and every subsequent signature breaks.


2. Render as PDF-ready HTML

POST /report/pdf returns styled, print-ready text/html — the structured report body plus the signature block. Open it in a browser and print to PDF for a court-admissible document.

resp = httpx.post(f"{BASE}/report/pdf", json={
    "report_id": "RPT-SL-2026-001",
    "report_type": "CourtPackReport",
    "title": "Operation Shadow Ledger — Preliminary Findings",
    "sections": [
        {"heading": "Executive Summary",
         "content": "Alice Chen authorized $2.8M in fraudulent wire transfers..."},
        {"heading": "Evidence Chain",
         "content": "tx1 ($2.3M) → tx2 ($850K) → tx3 ($120K cash). See whistleblower complaint."},
    ],
    "entities": ["Alice Chen", "Pacific Trust 7742", "David Kim"],
    "signed_by": "Agent Smith",
    "classification": "CONFIDENTIAL",
}, headers=H, timeout=15)
 
# Save the HTML, open in a browser, File → Print → Save as PDF
open("shadow-ledger.html", "w").write(resp.text)
print(resp.status_code, len(resp.text), "bytes")
# actual output from live server
200 14832 bytes

The HTML carries the HMAC signature and classification banner; the text/html content type means no binary PDF generation happens server-side — you control the final render via the browser's print engine.


3. Break-glass: request identity unmasking (two-person rule)

Protected HUMINT sources are stored masked (e.g. SRC-0042). Unmasking the real identity requires two distinct officers to approve — the requester cannot approve their own request, and synthetic principals (demo-* / dev-* / uid-*) are forbidden from approving at all.

3.1 Officer 1 — submit the request

POST /humint/breakglass/request takes the masked source_id, a purpose, and a free-text justification captured for audit.

status, r = post("/humint/breakglass/request", {
    "source_id": "SRC-0042",
    "purpose": "humint_unmask",
    "justification": "Need real identity for court filing — case #SL-2026-001",
})
print(r)
# actual output from live server
{'request_id': 'BGX-019fe25b-7f0a-4c2b-9d33-e05a0c44b820',
 'status': 'pending',
 'message': 'Pending approval from 2 distinct officers',
 'created_at': '2026-08-10T14:35:00Z',
 'expires_at': '2026-08-10T18:35:00Z'}

The request_id is a server-generated BGX-<uuid> (the caller supplies no entropy to the derivation). The request expires after 4 hours if not fully approved.

3.2 Officer 2 — approve (first approval)

A different officer (different principal, same agency) calls POST /humint/breakglass/approve with the request_id.

# Officer 2's session (different bearer principal)
H2 = {"Authorization": "Bearer officer2-token", "Content-Type": "application/json"}
r = httpx.post(f"{BASE}/humint/breakglass/approve",
               json={"request_id": request_id}, headers=H2, timeout=15).json()
print(r)
# actual output from live server
{'approved': False, 'approvals': 1, 'needed': 1}

One approval down, one to go. A third call from a second distinct approver tips approved to true.

3.3 Officer 3 — approve (second approval, tips to approved)

# Officer 3's session (a third distinct principal)
H3 = {"Authorization": "Bearer officer3-token", "Content-Type": "application/json"}
r = httpx.post(f"{BASE}/humint/breakglass/approve",
               json={"request_id": request_id}, headers=H3, timeout=15).json()
print(r)
# actual output from live server
{'approved': True, 'approvals': 2, 'needed': 0}

3.4 Poll status (and retrieve the reveal)

Any officer in the same agency can poll GET /humint/breakglass/status/:request_id. Once approved, the response carries the revealed source identity.

status, r = get(f"/humint/breakglass/status/{request_id}")
print(r)
# actual output from live server
{'status': 'approved', 'approved': True,
 'source_id': 'SRC-0042',
 'reveal': 'Source identity: SRC-0042 — identity reveal not yet stored in engine',
 'approvals': 2,
 'approved_by': ['officer2', 'officer3'],
 'created_at': '2026-08-10T14:35:00Z'}
Break-glass rules and failure modes
RuleEffect
Two distinct approvals requiredRequest stays pending until approvals >= 2
Requester cannot self-approve403 requester cannot approve their own break-glass request
Same-agency onlyApprover's tenant must match the requester's tenant (403 otherwise)
Synthetic principals blockeddemo-* / dev-* / uid-* principals cannot approve (403)
Idempotent approvalsA repeat approval from the same officer returns the current tally, not an error
4-hour expiryA pending request past expires_at returns 410 Gone and flips to expired
WAL-ahead durabilityThe request + each approval revision are WAL-persisted before the in-memory state updates — the audit trail survives a restart

Every request and approval is appended to the tamper-evident audit chain as BREAKGLASS_REQUEST / BREAKGLASS_APPROVE entries.


Summary: the compliance surface

EndpointPurposeOutput
POST /report/signBind a report to the audit chain{signature, algorithm, key_id, signed_at}
POST /report/pdfRender print-ready HTMLtext/html (browser → Print → PDF)
POST /humint/breakglass/requestRequest a source unmask{request_id, status: pending, expires_at}
POST /humint/breakglass/approveSecond officer approves{approved, approvals, needed}
GET /humint/breakglass/status/:idPoll / retrieve reveal{status, approved, reveal, approved_by}

Key concept — break-glass is two-person. No single user — not even an admin — can unmask a protected identity alone. Every request and approval is audit-logged on the tamper-evident chain, WAL-persisted, and tenant-scoped.


Next: Server Introspection & Interop — the self-describing /specs catalogue, type detail, STIX import, and typed CDR ingest.