A hash-chained audit log is the deterministic version of “we have logs.” Every event in the log carries the SHA-256 hash of the previous event; the chain breaks deterministically if any event is modified or removed. For EU AI Act Article 12, GDPR Article 30, and SS1/23 Principle 5 evidence, this is the substrate.

The implementation is small — about 200 lines of Java for the core. This article walks through what’s in those lines, what the gotchas are, and where RFC 9421 HTTP Message Signatures fit in the broader trust story.

The shape of an event #

The Regulus event is a JSON-serializable record:

public record RegulusEvent(
    Instant ts,
    String agent,
    String session,
    Decision decision,
    String clause,
    List<String> frameworkCitations,
    String jurisdiction,
    String principal,
    Integer modelTier,
    String prevHash,
    String hash
) {}

Eleven fields. The first nine are the substantive content. The last two are the integrity link.

Computing the hash #

The hash is SHA-256 over the canonicalised event minus its own hash field, concatenated with the prevHash:

public static String computeHash(RegulusEvent event, String prevHash) {
  byte[] eventBytes = canonicalize(event.withoutHash());
  MessageDigest sha256 = MessageDigest.getInstance("SHA-256");
  sha256.update(prevHash.getBytes(StandardCharsets.UTF_8));
  sha256.update(eventBytes);
  return Base64.getEncoder().encodeToString(sha256.digest());
}

The canonicalize function is the load-bearing part. Two events that contain logically identical data must produce identical bytes, or verification fails. The Regulus implementation uses RFC 8785 JSON Canonicalization Scheme (JCS) — alphabetical key ordering, no whitespace, exact-form numbers.

Appending an event #

Every new event reads the previous event’s hash and uses it as input to its own hash computation:

public class HashChainedEventEmitter {
  private final EventStore store;

  public synchronized void emit(RegulusEvent.Builder builder) {
    String prevHash = store.getLastHash().orElse("ROOT");
    RegulusEvent partial = builder.prevHash(prevHash).build();
    String hash = computeHash(partial, prevHash);
    RegulusEvent final_ = partial.withHash(hash);
    store.append(final_);
  }
}

The synchronized keyword matters. Concurrent appenders would race on the prev-hash read; the chain would fork. For high-throughput scenarios, you need a more sophisticated approach (per-scope chains, sharded by agent name or session ID) — the basic implementation here assumes a single emitter per chain.

Offline verification #

Verification walks the chain. For each event, recompute the hash from the stored prev_hash and the canonicalised event content; compare against the stored hash.

public class ChainVerifier {
  public Result verify(Stream<RegulusEvent> events) {
    String expectedPrev = "ROOT";
    int eventsSeen = 0;
    for (RegulusEvent event : events.toList()) {
      if (!event.prevHash().equals(expectedPrev)) {
        return Result.failed(eventsSeen, "broken link");
      }
      String recomputed = computeHash(event.withoutHash(), event.prevHash());
      if (!recomputed.equals(event.hash())) {
        return Result.failed(eventsSeen, "tampered event");
      }
      expectedPrev = event.hash();
      eventsSeen++;
    }
    return Result.passed(eventsSeen);
  }
}

The regulus audit verify CLI is a thin wrapper over this verifier. Run it against a chain file and it tells you:

$ regulus audit verify chain-2026-06-01.jsonl
✓ 4,128 events
✓ chain intact (root → 7a82e9d4...)
✓ 0 broken links
✓ 0 tampered events

If something’s wrong:

$ regulus audit verify tampered.jsonl
✗ 4,128 events
✗ chain broken at event 2,047 (tampered event)
  expected hash: e9d4f72a...
  computed hash: 8b1f4d3c...

The auditor doesn’t take your word that the chain is intact. They run the verifier.

Gotchas #

Three things to watch:

1. Canonicalization edge cases #

JSON canonicalization is harder than it looks. Floats are the classic problem (1.0 vs 1, 1e0 vs 1). Use integers wherever possible in the event schema; for floats, pin the serialization format. The Regulus implementation uses Jackson with a custom Number module that always emits integers as integers and floats with a fixed-precision representation.

2. Clock skew #

Events carry a wall-clock timestamp. If your emitters’ clocks drift, events can land in non-monotonic order, which doesn’t break the hash chain but does break user expectations of ordering. Use NTP; keep emitter clocks tight.

3. Chain scope #

A “chain” is per-scope. Regulus runs one chain per emitter — one chain per agent process, typically. Cross-scope analysis (across sessions, across agents) requires merging chains by timestamp, which loses the integrity property within the merged view (each scope’s chain remains verifiable independently, but the merged view doesn’t have its own chain).

For high-stakes deployments (lots of agents emitting in parallel), the right pattern is: one chain per agent, but a periodic “super-chain” event that captures the latest hash of each agent chain. The super-chain is small (one entry per agent per hour) and provides cross-agent integrity.

Where RFC 9421 fits #

RFC 9421 — HTTP Message Signatures — solves a different problem. The hash chain is intra-organisation evidence integrity. RFC 9421 attaches cryptographic identity to outbound HTTP calls — useful when agents call other agents across organisational boundaries.

The Regulus A2A envelope wraps outbound agent-to-agent calls with RFC 9421 signatures over (@method, @target-uri, @body, @created). The receiving agent verifies before invoking. Replay protection via a per-keypair monotonic nonce window.

The two compose. An A2A call:

  1. The sending agent emits an A2A_SEND event to its own audit chain (intra-org evidence).
  2. The outbound HTTP request carries an RFC 9421 signature (cross-org evidence).
  3. The receiving agent verifies the signature.
  4. The receiving agent emits an A2A_RECEIVE event to its own chain (intra-org evidence at the receiving side).

Both organisations end up with their own intra-org evidence chains; the cross-org link is the RFC 9421 signature in the HTTP envelope.

Status today #

The Regulus hash chain has been shipping since v0.1.0 and is production-stable. The RFC 9421 surface is wired in v0.2.0 but the signing implementation is still in flight (current shipping code raises UnsupportedOperationException if production-mode signing is requested). Replay protection and canonicalization are verifiable today; Ed25519 signing lands in v0.3.

What this gives you #

Three audit-walkthrough outcomes:

  1. External auditor can verify the chain offline. No need to trust Regulus’s word for it.
  2. Tampering is deterministically detected. No silent failures.
  3. Per-event timestamping is monotonic within a scope, which gives clean per-event historical context for any later investigation.

For the audit-plugin reference, see the audit plugin page. For the broader EU AI Act Article 12 context, see Article 9 in code.