DIDMARC

DID Mapping Alignment, Reporting & Conformance

A tamper-proof, independent record that proves an atproto identity's history has not been quietly rewritten, hidden, or forked.

Open the console →

The gap a signature leaves

An atproto identity is a signed, hash-linked log of operations, served from a single directory. Every operation is self-certifying, so a signature vouches for the operation it's attached to. It can't tell you whether the directory showed you all of them, the latest ones, or the same ones it showed everyone else.

What a signature proves

This operation is authentic, signed by a key that was valid at the time. PLC already gives you this, per operation.

What it can't

That you were shown the complete, current, and non-equivocated history. No signature can speak for the operations it isn't on, including the ones you never saw.

What a directory can do to you

Every operation can be perfectly signed and the directory can still deceive you, because it alone decides which operations you see. None of these attacks needs a stolen key — just control of the directory a resolver trusts:

Omission: undo a fix

Your account key is stolen, and you rotate it out with a new signed operation. The directory serves your history with that one operation missing. To the rest of the network the stolen key is still live, and the thief keeps signing as you.

The lie is the operation left out.

Erasure: make someone vanish

The directory returns nothing for a DID that exists. Every app that looks the person up sees no such identity. Handles dangle, posts won't resolve, and there's no error to appeal.

Absence carries no proof to check.

Split-view: two faces

Show the victim their real history, while showing one targeted service a forked version that points their account at a server the attacker runs. Neither side ever sees the other's copy.

No single viewer holds both halves.

Each of these is a real, ground-truthed scenario in the adversarial report. DIDMARC catches them with an independent, on-chain copy of the whole, current history: a resolver can tell when the directory's answer doesn't match it.

How it works

Each opted-in DID's full operation log lives on Starknet, block-timestamped and append-only. The network proves the record and settles it to Ethereum, so there is no bespoke prover and no trusted snapshot. On the read side the witness re-derives that DID's confirmed head from the on-chain log, fetches the document the directory serves, and compares the two to produce a status. plc.directory stays canonical. DIDMARC is a read-side witness any resolver turns on by itself, with no coordination and no protocol change.

The graduated status

Never a false diverged. A directory that's merely running ahead, or a contradiction that plain clock skew could explain, stays at tentative-consistent. One false alarm and operators switch the check off, so diverged is reserved for real contradictions.

Turn it on

The witness is a small TypeScript library with no external dependencies. Hand it a DID, an anchor reader, and a directory client, and it hands back a status your resolver acts on.

import { check, AnchorReader, DirectoryClient } from "@plc-anchor/witness";

const anchor = AnchorReader.http(process.env.STARKNET_RPC, CONTRACT_ADDR);
const { status, reason } = await check("did:plc:…", anchor, new DirectoryClient());

switch (status) {
  case "confirmed":            // proven and immutable; trust the directory's answer
  case "tentative-consistent": // agrees so far, nothing immutable yet; proceed
  case "absent":               // not anchored; no opinion, resolve as usual
    break;
  case "diverged":             // proven head contradicted; the alarm fired
    quarantine(reason);
}

Prefer the shell? The CLI exits 2 on diverged and 0 otherwise, so it drops straight into a health check or a CI gate:

STARKNET_RPC=https://your-node node cli.ts did:plc:… # [--contract 0x…] [--directory URL]

Adopt it like DMARC: log the status first, then quarantine on diverged, then refuse to relay. Each step is one resolver's own decision, at its own pace.

Adversarial deep-dive

The alarm has to fire on real attacks and stay silent on benign disagreement. The adversarial report tests both, as real signed did:plc chains run against the shipping witness. Four of its scenarios:

Equivocationdiverged

A mirror serves your history with one confirmed operation quietly omitted. The witness re-derives the confirmed head from the on-chain log, finds it missing from the directory's answer, and flags the mismatch.

Vanishing DIDdiverged

The directory returns nothing at all for an anchored DID. The anchor proves the identity exists with a confirmed head, so the empty answer is a contradiction, not an absence, and the alarm fires.

Window-edge skewtentative

A head mismatch that ordinary clock skew near the 72-hour boundary could explain stays tentative-consistent, never an alarm. This rule is what keeps diverged free of false positives.

Anchor self-faulttentative

Even when the anchor's own on-chain log is corrupt and yields no confirmed head, the witness degrades to tentative-consistent. A broken witness never accuses an honest directory.

Write-side attacks fail the same way. A forged signature, a weaker-key recovery, and a late recovery can each be paid for and stored on chain permissionlessly, but the witness's read-side re-derivation drops every one, so a forged operation never reaches a resolver. Scenarios 9 through 12 cover these, including the capture attempt that motivated the imported-history guard.