- Docs
- Bureau — Blue Team (defensive)
- Sigil
Bureau — Blue Team (defensive)
Sigil
A signed observatory for tap-to-pay, tap-to-enter, and tap-to-board readers. Each tap is recorded as a tamper-evident receipt, and four well-known card-fraud patterns produce verifiable contradictions in the published record.
Posture: 🔵 Blue Team (defensive) · Status: alpha
What it does
Office badges, hotel key cards, and contactless credit cards are routinely cloned with inexpensive tooling such as Proxmark and Flipper Zero. Sigil instruments contactless readers and converts four common fraud patterns – UID clones, transcript replays, EMV counter rollbacks, and roaming terminals – into signed contradictions suitable for evidentiary use.
The underlying property is that an attacker who copies the bytes on a card cannot perfectly replicate the EM-coupling profile of the original chip, and cannot move EMV counters backwards without producing a contradiction. Sigil collects reader-side observations, monitors for these contradictions, and signs each detection. Verification does not require participation from the card brand, bank, or payment processor.
Who would use it
- A transit-system fraud team investigating a clone-ring that's been riding the L for months, paying with cloned tap-to-pay cards.
- A corporate access-control team at a tech company in San Francisco that just discovered three duplicate badges in its ingress logs and wants to know which physical reader saw the clone first.
- A payment-network compliance officer at Visa who needs cryptographic observations that a merchant's terminal is being relocated (terminal-mismatch) – the kind of fraud that's hard to prove without a signed observation chain.
- A retail-loss-prevention investigator at a department store who wants to bring a chain-of-custody-grade case against a serial card-skimmer.
- A research engineer at the EMVCo working group who wants to demonstrate ATC-rollback exploits in a public, signed corpus that vendors can't dispute.
What you'll need
- Node.js 18+ and the Pluck CLI:
npm install -g @sizls/pluck-cli - An operator key:
pluck bureau keys generate --out ~/.pluck/keys(one-time) - For real readers: a Proxmark3 RDV4 (~$300), Flipper Zero (~$170), or any EMV-certified PCSC reader paired with the Sigil reader-side library
- For the alpha demo: nothing – runs in-memory on synthetic data
Step-by-step
The alpha runs in-memory on synthetic data. The production CLI (real reader integration, daemon mode, capture/verify/replay) ships in a follow-up.
1. Run the demo
pluck bureau sigil demo
Output:
sigil/demo: ingesting signed observations (1 legit + 1 UID-clone + 1 APDU-replay + 1 counter-rollback)...
sigil/demo: tamper proofs emitted = 3
sigil/demo: proofId=12fae3… kind=uid-clone uid=04a1b2c3d4e5f6
sigil/demo: proofId=8e44b1… kind=apdu-replay uid=04a1b2c3d4e5f6
sigil/demo: proofId=b71c08… kind=counter-rollback uid=04a1b2c3d4e5f6
sigil/demo: proofs notarized (stub) = 3
The demo synthesizes four reader observations of the same card UID: one legitimate baseline, one clone (same UID, very different reader-side timing centroid – clones don't match the original's EM-coupling profile), one APDU-replay (identical transcript bytes at two timestamps – EMV nonces guarantee this never happens for live cards), and one counter-rollback (the card's transaction counter went backwards – EMV requires it to be strictly monotonic). Three contradictions get signed.
2. Look at a tamper proof
Each proof carries the offending observations verbatim – a clone proof, for example, contains both reader observations of the same UID, with their timing distributions, so a third party can recompute the centroid distance and confirm the contradiction.
cat ./sigil-out/proofs/proof-uid-clone-12fae3.json
3. Verify
pluck bureau verify ./sigil-out
The verifier walks each proof, checks the reader's signature, recomputes the math (centroid distance, transcript byte equality, ATC ordering), and prints PASS / FAIL.
Run it yourself
Drop this into a Node 18+ project (npm install @sizls/pluck-bureau-sigil @sizls/pluck-bureau-core tsx):
// index.ts
import { createHash } from "node:crypto";
import {
createSigilSystem,
fingerprintPrivateKey,
signCanonicalBody,
} from "@sizls/pluck-bureau-sigil";
import { generateOperatorKey } from "@sizls/pluck-bureau-core";
const digest = (s: string) => createHash("sha256").update(s).digest("hex");
const flush = (n = 20) =>
new Promise<void>((res) => {
let i = 0;
const tick = () => (++i >= n ? res() : setImmediate(tick));
setImmediate(tick);
});
function build<T extends Record<string, unknown>>(body: T, idKey: string, key: string): any {
const skel = { schemaVersion: 1, ...body };
const id = createHash("sha256").update(JSON.stringify(skel)).digest("hex");
const signed = signCanonicalBody({ ...skel, [idKey]: id }, key);
return { ...skel, [idKey]: id, signature: signed.signature };
}
async function main() {
const operator = generateOperatorKey();
const reader = generateOperatorKey();
const readerKey = reader.privateKeyPem;
const readerFingerprint = fingerprintPrivateKey(readerKey);
const uid = "04a1b2c3d4e5f6";
const sigil = createSigilSystem({
signingKey: operator.privateKeyPem,
disablePausePoll: true, disableLogging: true,
});
const manufacturerBlockDigest = digest(`mfr:block:${uid}`);
const keyDiversificationDigest = digest(`keydiv:${uid}`);
try {
// Legit baseline – tight timing centroid (mean 1200µs, stddev 30µs).
sigil.observeTag(build({
uid, readerFingerprint, manufacturerBlockDigest, keyDiversificationDigest,
timing: { meanUs: 1_200, stddevUs: 30, sampleCount: 16 },
observedAt: "2026-04-26T00:00:00.000Z",
}, "observationId", readerKey));
sigil.observeApdu(build({
uid, readerFingerprint,
requestDigest: digest("req:legit"), responseDigest: digest("resp:legit"),
atc: 0x10, observedAt: "2026-04-26T00:00:00.050Z",
}, "transcriptId", readerKey));
// UID-CLONE – same UID, very different timing centroid (~23σ apart).
sigil.observeTag(build({
uid, readerFingerprint, manufacturerBlockDigest, keyDiversificationDigest,
timing: { meanUs: 1_900, stddevUs: 30, sampleCount: 16 },
observedAt: "2026-04-26T00:00:01.000Z",
}, "observationId", readerKey));
// APDU-REPLAY – identical request/response bytes at a later time.
sigil.observeApdu(build({
uid, readerFingerprint,
requestDigest: digest("req:legit"), responseDigest: digest("resp:legit"),
atc: 0x12, observedAt: "2026-04-26T00:00:02.000Z",
}, "transcriptId", readerKey));
// COUNTER-ROLLBACK – ATC dropped from 0x10 to 0x05.
sigil.observeApdu(build({
uid, readerFingerprint,
requestDigest: digest("req:rollback"), responseDigest: digest("resp:rollback"),
atc: 0x05, observedAt: "2026-04-26T00:00:03.000Z",
}, "transcriptId", readerKey));
await flush();
const proofs = sigil.facts.tamperProofs();
console.log(`tamper proofs: ${proofs.length}`);
for (const p of proofs) console.log(` kind=${p.kind}`);
} finally {
await sigil.shutdown();
}
}
main().catch((err) => { console.error(err); process.exit(1); });
Run with tsx index.ts. Expected output:
tamper proofs: 3
kind=uid-clone
kind=apdu-replay
kind=counter-rollback
▶ Open in StackBlitz – runs in your browser, no install required.
What you get
For each detected fraud pattern: a signed SigilTamperProof cassette (a .intoto.jsonl Sigstore-compatible file) carrying the offending observations verbatim. A fraud investigator can hand it to a magistrate and say "this card was cloned" – and the magistrate can verify the math themselves with cosign verify-blob. No card brand, no bank, no payment processor has to play along.
The cassettes also chain into per-card timelines: every observation of UID 04a1b2c3d4e5f6 is in one place, with every contradiction flagged. That's the artifact insurance, banks, and prosecutors actually want.
What it can't do
- Sigil produces observations after the tap, not real-time blocking. Stopping a fraudulent transaction is your existing fraud engine's job; Sigil produces the cryptographic chain-of-custody for what already happened.
- A skilled cloner who matches the original card's EM-coupling profile to within 3σ won't trip
uid-clone. They'll still tripapdu-replayandcounter-rollbackthe moment they reuse a transcript or roll the ATC backwards. - Sigil doesn't see PAN (Primary Account Number) or cardholder name – only digests, the UID, and the EMV ATC. That's by design (PII safety) but it means a Sigil proof can't directly identify a cardholder; it identifies a card.
- The alpha is in-memory only. Real reader integration ships next quarter; today you can demo the math.
A real-world example
In April 2026, a fraud-prevention team at a regional transit agency observes three duplicate tap-to-pay charges in their reconciliation system within six hours, all on the same UID and from three different stations. The existing fraud engine flagged the duplicates but did not distinguish which reader saw the original card and which saw the clone.
The team feeds the reader-side capture logs through Sigil and obtains a signed uid-clone proof in under a minute: the timing centroid at one station reader (4.7 µs ± 0.6) is 14σ from the centroid at a second station reader (8.1 µs ± 0.4) for the same UID inside the same hour. The first reader observed the original card; the second observed the clone.
The signed cassette is filed with the state's attorney three days later as the primary exhibit. The Rekor entry is independently verifiable, and the clone-ring case proceeds.
For developers
Predicate URIs
| URI | What it attests |
|---|---|
https://pluck.run/Sigil.Tag/v1 | One reader-signed tap (UID, manufacturer-block digest, key-diversification digest, reader-side timing distribution, geohash, observedAt). |
https://pluck.run/Sigil.Apdu/v1 | One reader-signed APDU exchange (UID, requestDigest, responseDigest, EMV ATC, observedAt). |
https://pluck.run/Sigil.Replay/v1 | One Bureau-signed replay/clone proof carrying verbatim offending observations. |
https://pluck.run/Sigil.Terminal/v1 | One reader-witnessed terminal observation (terminalId, declaredGeohash vs observedGeohash, manufacturer cert chain). |
https://pluck.run/Sigil.Tamper/v1 | One Bureau-signed generic tamper proof (uid-clone, apdu-replay, counter-rollback, or terminal-mismatch). |
Programs composed
- Pluck
dowse– the reader-side cassette wrapper that carries signed APDU exchanges to the Bureau path - Custody – per-card chain-of-custody so a single UID's observations reach Rekor without operator-side cherry-picking
- Raven / Celeste – provide the time anchor
observedAtinstants are checked against - Sigstore Rekor – proofs are notarized as DSSE in-toto envelopes with a public inclusion proof
Threat model + adversary
- Up to 20,000 tag observations, 20,000 APDU transcripts, 1,024 terminal observations, and 1,024 tamper proofs kept in memory before FIFO eviction.
- Latency ceiling 1×10⁹ µs, ATC ceiling 0xFFFF (EMV spec maximum), UID ceiling 20 hex chars (10-byte triple UID).
- A skilled attacker matching the original's EM-coupling profile to within 3σ won't trip
uid-clone. The other three detectors (APDU replay, counter rollback, terminal mismatch) catch all known clone classes regardless. - PAN, cardholder name, and full APDU bytes never appear in a signed body. UID and ATC do – UIDs are sensitive long-lived identifiers;
redactBureauPayloadstrips them from log summaries before attest. - Sigil's chain-of-custody assumes the reader's signing key is honest. A compromised reader can sign whatever observation it wants. Operators should pair Sigil with hardware-attested reader keys for upstream protection.
Verify a published cassette
pluck bureau verify <bundle-dir>
# Or with cosign directly
cosign verify-blob \
--key <pubkey.pem> \
--signature <signature.sig> \
--type https://pluck.run/Sigil.Tamper/v1 \
<body.json>
The package exports validateSigilTamperProof, verifyCanonicalBody, transcriptCanonicalDigest, and predicateTypeForTamperKind for verifier authors.
See also
- Bureau Foundations
- Threat Model
- Verify a dossier
- Operator Duties
- Custody – per-card chain-of-custody
- Celeste – GPS-anchored time witness
- Raven – RF substrate witness
- Hive – sibling IoT pairing-forensics Bureau