Exosphere.ATProto.Identity.DID.PLC.AuditLog (Exosphere v0.6.0)

Copy Markdown View Source

Validation of a did:plc audit log — the operation history the directory serves from /:did/log/audit.

A log is a chain, not a list. Each operation names its predecessor by CID, the first derives the DID from its own bytes, and every operation is signed by one of the rotation keys its predecessor declared. Validating a log means walking that chain and checking each link.

Nullification

Rotation keys are ordered by descending authority, and a higher-authority key may fork the log from an earlier point, nullifying the operations it displaces. Two rules bound that power:

  • the forking operation must be signed by a strictly higher-authority key than the one that signed the first operation it nullifies; and
  • it must land within 72 hours of that operation — inclusive, as log_nullification_at_exactly_72h.json pins down.

Nullified operations stay in the log, flagged; they simply stop being part of the active chain, and nothing may build on them.

Tombstones

A plc_tombstone permanently deactivates the DID. Nothing may follow one — though a tombstone may itself be nullified, which is what log_nullified_tombstone.json covers.

Why validation is two passes

Signatures and chaining are checked for every operation; the shape rules (rotationKeys of 1–5, no duplicates, and so on) are checked only for operations that survive on the active chain.

That split is not fussiness — it is what the corpus requires. log_nullification.json is a valid log whose middle operation carries rotationKeys: []: someone emptied their rotation keys, locking the identity, and a higher-authority key forked it away inside the recovery window. That is precisely the situation the window exists for, so the log is valid even though a displaced operation in it is not something we would ever emit. log_empty_rotation_keys.json is the same operation left standing on the active chain, and is invalid.

(The directory has historically accepted operations that do not satisfy its own rules — see did-method-plc issue #109 — so "the directory stored it" is not evidence of validity.)

Strictness vs the directory

Shape rules follow the spec, which in places is stricter than what the production directory actually enforces: the spec allows at most 5 rotation keys while the directory accepts up to 10 (MAX_ROTATION_ENTRIES in its constraints.ts), and the spec's 7,500-byte operation cap is enforced at 4,000. A legitimate directory-stored log can therefore fail validation here. That is deliberate — this module validates what an operation should be, not everything the directory has ever admitted — but callers screening arbitrary directory logs should know the divergence exists.

Malformed input

Entries are validated before they are trusted: cid must be present (and is recomputed from the operation bytes — a log is a content-addressed chain, so a reported CID that does not match its own operation is malformed however well it links), operation must be a map, every entry's did must agree with the DID the genesis operation derives, and timestamps must be strictly increasing along the chain — the reference implementation enforces the same.

Summary

Functions

Validate a complete audit log.

Validate a log and check the computed nullification against the flags the directory reported, so a disagreement is an error rather than a silent divergence.

Types

entry()

@type entry() :: %{required(String.t()) => term()}

Functions

validate(entries)

@spec validate([entry()]) :: {:ok, MapSet.t(String.t())} | {:error, term()}

Validate a complete audit log.

Returns {:ok, nullified_cids} — the set of operation CIDs the log's own rules nullify — or {:error, reason}.

The returned set is what lets a caller cross-check the directory's own nullified flags rather than trusting them.

validate_against_flags(entries)

@spec validate_against_flags([entry()]) :: :ok | {:error, term()}

Validate a log and check the computed nullification against the flags the directory reported, so a disagreement is an error rather than a silent divergence.