Threat Model¶
BIJOTEL is a tamper-evident HMAC audit chain for LLM applications. This page documents exactly what BIJOTEL does protect against, what it does not, and where to reach for stronger guarantees.
The framing follows the same M2 principle the rest of the docs use: reality beats marketing. If a threat sits outside our scope, that's stated explicitly here rather than implied away.
What BIJOTEL protects against¶
Post-factum database tampering¶
An attacker who gains read/write access to chain.db after an entry
has been sealed cannot:
- Modify any entry's body without breaking the HMAC chain at the exact
seq. - Delete entries without creating a gap detected by
bijotel verify. - Insert fake entries without the HMAC secret.
- Alter
canonical_bodywhile keeping the chain links valid — the v2.0.3+ verifier re-hashes the body and compares againstcanonical_hash. Tampering at this layer raisesExport INVALID: canonical_body tampered at seq=N.
Verification:
bijotel verify --db chain.dbwalks the full SQLite chain.bijotel verify-export some_export.jsonverifies an exported archive with no SQLite or filesystem dependency on the auditor side.
Cross-version and cross-architecture portability¶
Chain exports are bit-identical across platforms. A chain sealed on x86_64 (e.g. GENA, Nuremberg) verifies identically on aarch64 (e.g. ARA, Helsinki). JCS canonicalization (RFC 8785) makes the hash inputs deterministic regardless of CPU, Python build, or JSON serializer.
Evidence: Round 2 Test D verified 5,687 entries cross-architecture at ~15,328 entries/sec on a stock ARM64 instance.
Silent model degradation¶
The F12 regression detector watches statistical drift in token counts, costs, and latency using z-score and IQR methods over the sealed chain.
Evidence: an hourly /regression/run cron on GENA fires against
the live chain; baselines are recomputed automatically and anomalies
appear in /api/regression/latest.
Prompt injection and jailbreak attempts¶
The F11 PolicyEngine evaluates prompts pre-call against 50 patterns
in 7 categories (instruction override, system prompt extraction, role
override, jailbreak framing, encoding bypass, hypothetical framing,
multi-turn priming). The ast_safety_check rule additionally inspects
code blocks structurally with tree-sitter-bash and Python's ast
module.
Evidence: 100% detection on the R1 probe corpus with 0 false positives on the benign corpus. Probes and corpus are checked into the repository for replay.
What BIJOTEL does NOT protect against¶
Insider with the HMAC secret¶
BIJOTEL's HMAC chain uses a symmetric secret. Anyone who possesses
BIJOTEL_HMAC_SECRET can append valid entries to the chain. This is
the same trust model as TLS pre-shared keys or git's SSH deploy keys.
BIJOTEL's HMAC tamper-evidence is for post-factum modification by a party that doesn't hold the secret, not for malicious operators who do.
Mitigation:
- Treat the secret like a database credential.
- Store it in a secrets manager (Vault, AWS Secrets Manager, GCP Secret
Manager) — not in repo config or
.envfiles in production. - Rotate periodically. The verifier handles rotation boundaries
correctly:
bijotel verifyreturns the exactseqwhere the old secret stops verifying. See the Secret Rotation Playbook. - For external auditors: use v2.1.0+ Ed25519 signed exports. An auditor never needs the seal-time HMAC secret — they verify the export with the operator's public key only, and cannot forge entries. See "Auditor verification without the HMAC secret" below.
Secret leakage¶
If the HMAC secret is leaked, an attacker can:
- Generate valid chain entries from scratch.
- Forge exports that pass
verify-exportagainst the same secret.
They still cannot:
- Modify existing entries in a chain they don't have write access to.
- Break the linkage of an already-distributed export without detection.
Mitigation: rotate the secret on suspicion and re-seal new entries
under the new key. Older entries verify under the old key by design;
the boundary seq is detectable.
Database deletion or filesystem loss¶
If chain.db is deleted entirely, the audit trail is lost. BIJOTEL is
an integrity layer, not a backup system.
Mitigation: standard backup discipline — periodic snapshots,
periodic bijotel export of the chain to an external location, and
ideally a 3-2-1 backup posture for the seal host.
Lost spans on disk pressure or write failure¶
If on_end() fails (disk full, SQLite locked beyond retry, container
killed mid-write), the span is logged at ERROR level but not sealed
into the chain. The host application continues unaffected (crash
isolation, v0.6.0+).
The result is a gap in the audit trail, not a host crash. The chain
itself remains VALID — the gap is detectable by inspecting seq
numbering. Round 3 Test B1 (kill -9 mid-write) and Test B2
(read-only DB) confirmed this isolation behaviour empirically.
Multi-writer contention at extreme scale¶
SQLite with WAL + BEGIN IMMEDIATE handles concurrent writers cleanly
up to roughly 200 spans/sec on a single chain file (Round 3 Test D2).
Past that, contention shows up as BUSY retries that eat into latency
budget. At extreme scale (≫1,000 concurrent writers on one chain),
consider PostgreSQL with the chain table or partition by writer
identity.
Paraphrase and multimodal attacks¶
The F11 detector is regex-based. Synonym-substitution paraphrases, unicode confusables outside the catalogued set, or multimodal attacks (prompts embedded in images, audio, or files) are outside F11 scope.
For defence in depth, layer F11 with a model-based guard such as Lakera Guard, Prompt Security, or Rebuff. F11 catches the cheap surface forms quickly and cheaply; the model-based guard catches the long tail.
Formal correctness of agent actions¶
BIJOTEL proves integrity of the audit log (the log wasn't tampered). It does not prove correctness of what the agent did. A perfectly sealed chain can document a perfectly disastrous decision.
For formal verification of agent actions (Z3 SMT solver, OPA/Rego policy decisions over actions, eBPF syscall enforcement, ZK proofs over training data), see substrate-guard. BIJOTEL and substrate-guard are designed to be deployed together when the application demands both properties.
BIJOTEL vs substrate-guard — scope boundary¶
| Claim | BIJOTEL | substrate-guard |
|---|---|---|
| "Log wasn't tampered" | ✅ HMAC chain | ✅ HMAC chain (same primitive) |
| "Agent action was safe" | ❌ | ✅ Z3 SMT + OPA/Rego |
| "No unauthorized syscalls" | ❌ | ✅ eBPF kernel layer |
| "Training data compliant" | ❌ | ✅ ZK-SNM |
| "Signed by hardware" | ❌ | ✅ Ed25519 / TPM attestation |
| "Works offline (CRDT)" | ❌ | ✅ |
"pip install one-liner" |
✅ | ❌ |
| "Bundled REST API + dashboard" | ✅ | ❌ |
BIJOTEL is the PyPI-installable subset focused on LLM observability + forensic chain — the demonstrator of bijuteria #11 (Forensic-First Architecture) at scale. The remaining safety bijuterii (#1 Z3,
6 ZK, #8 eBPF, #12 hardware trust) live in substrate-guard, not¶
here. The README and CHANGELOG state this explicitly; this page exists so that scope distinction is visible from the docs site too.
Secret rotation¶
The chain verifier handles HMAC secret rotation correctly: it detects
the exact seq where the old secret stops verifying and the new
secret takes over. Round 2 Test E3 confirmed boundary detection at
seq=51 with zero false positives on either half of the chain.
The full procedure — when to rotate, how to record the boundary, how to verify both halves of the chain, how to hand a rotated chain to an auditor — lives in the dedicated Secret Rotation Playbook.
In one paragraph: generate a fresh 64-hex secret, write down the
current chain length (that's the boundary), swap
BIJOTEL_HMAC_SECRET, restart the sealing process, then verify each
half of the chain with its corresponding secret. The old half stays
verifiable under the old key; the new half under the new key. There
is no re-signing.
Auditor verification without the HMAC secret¶
Added in v2.1.0. Pre-v2.1.0 the auditor needed the HMAC secret to verify an export — which made the auditor a potential forger. v2.1.0 adds Ed25519 asymmetric signatures on exports as an outer attestation layer over the unchanged HMAC chain.
# Operator side (once):
bijotel keygen --output-dir ./keys
# → keys/bijotel_private.pem (keep secret)
# → keys/bijotel_public.pem (share with auditors)
# Operator side (each export):
bijotel export --db chain.db -o export.json \
--sign-key keys/bijotel_private.pem
# Auditor side (with only the public key — NO HMAC secret):
bijotel verify-export export.json \
--public-key bijotel_public.pem
# → Export VALID — Ed25519 signature verified
The auditor verifies the export's Ed25519 signature, then checks that
each entry's canonical body still hashes to its stored
canonical_hash and that the prev_hash chain links remain
consistent. They never see the seal-time HMAC secret, so they cannot
mint new entries that would verify under it.
Empirical evidence (2026-05-26): a 6,341-entry chain signed on
GENA (x86_64, Nuremberg) verified bit-identically on ARA (aarch64,
Helsinki) using only the operator's public key. No HMAC secret left
the operator host. See tests/test_export_signed.py for the property
tests covering signature tamper, key-swap attack, and canonical-body
tamper under auditor mode.
The v1 export format (bijotel-chain-v1) is still produced when
--sign-key is not supplied — backward-compatible with every reader
since v1.1.
Reporting a finding¶
Security issues should go to contact@aisophical.com rather than the
public GitHub issue tracker. See
SECURITY.md
for the disclosure window.