- Docs
- Bureau — Blue Team (defensive)
- Stingray
Bureau — Blue Team (defensive)
Stingray
An IMSI catcher (cell-site simulator) is a device that impersonates a legitimate cell tower to capture phone identifiers, then leaves before it can be characterized. Stingray turns ordinary phones and SDRs into independent witnesses; when k of n witnesses report mutually contradictory broadcasts from the same cell ID at the same time and place, a signed proof is published to a public log.
Posture: 🔵 Blue Team (defensive) · Status: alpha
What it does
A real cell tower says the same thing to every phone in earshot. A fake cell tower – an "IMSI catcher" or "cell-site simulator," widely known by the trade name Stingray – almost always slips up. It hands different broadcast information to different listeners as it tries to pull specific phones onto its frequency, or it moves around in a way a permanent tower never could.
Stingray (the program) catches the slip. Multiple receivers – your phone, a friend's phone, a fixed monitoring station – each independently observe what towers in the area are advertising. Each receiver records the cell ID, the frequency, and a hash of the broadcast. When two receivers report the same cell ID in the same neighborhood at the same minute but disagree on what was broadcast, that is a mathematical contradiction. Once enough independent witnesses (k of n, e.g. 3 of 5) co-sign the same contradiction, Stingray publishes a Suspect – the red dot on the map.
Who would use it
- A protest medic worried that police IMSI catchers are sweeping up everyone's phone identity at a demonstration.
- A journalist meeting a source who needs to prove afterwards that no rogue tower was active at the meeting site.
- A defense lawyer building a record of every cell tower that "appeared" near her client's phone during the period the prosecution claims he was elsewhere.
- A community-watch network (a neighborhood-scale equivalent of a citizen weather network) crowd-sourcing detection of unauthorized surveillance.
- A foreign correspondent in a country where law enforcement deploys IMSI catchers without judicial warrants.
What you'll need
- The Pluck CLI installed (
pnpm add -g @sizls/pluck-cli). - A software-defined radio that can listen to LTE/5G broadcast channels – an RTL-SDR Blog v4 (~$35) for LTE band scanning, or a HackRF One (~$330) for wider band coverage.
- For full deployment, two or more witnesses in the same area at the same time. The point of Stingray is corroboration; one witness is testimony, multiple witnesses is proof.
- An operator key per witness. Each phone or sensor signs its own observations independently – that's what makes the contradiction provable.
Step-by-step
The alpha runs an in-memory demo on synthetic data. The production CLI (live cellular capture from an SDR, multi-witness ingestion daemon) ships in a follow-up. To see how the constraint engine works today:
pluck bureau stingray demo
You'll see something like:
stingray/demo: ingesting 3 synthetic observations...
stingray/demo: equivocations emitted = 1
[Bureau/Stingray] red-dot suspect=c0dc5c9b72c6… pci=42 ghBucket=dr5ru window=2026-04-28T19:35:08Z
stingray/demo: suspects published = 1
This run synthesizes three witness observations. Witnesses A and B both see physical cell ID 42 in the same geohash bucket at the same wall-time but disagree on the neighbor-list hash of what tower 42 was broadcasting. Witness C sees a different tower (cell ID 99). The constraint engine detects the (A, B) equivocation, three witness signatures co-sign, the 3-of-5 quorum threshold lets the Suspect publish.
In production, the same flow runs over real captures: each witness's SDR feeds the LTE physical-cell-identity decoder, decodes the System Information Block 1 (SIB1) – the periodic broadcast that tells phones what's on this network – hashes it, and signs the result with that witness's key.
Run it yourself
Drop this into a Node 18+ project (npm install @sizls/pluck-bureau-stingray @sizls/pluck-bureau-core tsx):
// index.ts
import { createHash } from "node:crypto";
import { createStingraySystem } from "@sizls/pluck-bureau-stingray";
import {
fingerprintPrivateKey,
signCanonicalBody,
} from "@sizls/pluck-bureau-stingray";
import { generateOperatorKey } from "@sizls/pluck-bureau-core";
const hashOf = (s: string) => createHash("sha256").update(s).digest("hex");
const flush = () => new Promise((r) => setImmediate(r));
function signObs(body: any, key: string) {
const skel = { schemaVersion: 1, ...body };
const observationId = createHash("sha256")
.update(JSON.stringify(skel))
.digest("hex");
const signed = signCanonicalBody({ ...skel, observationId }, key);
return { ...skel, observationId, signature: signed.signature };
}
async function main() {
const operator = generateOperatorKey();
const witnessAKey = (generateOperatorKey()).privateKeyPem;
const witnessBKey = (generateOperatorKey()).privateKeyPem;
const witnessAFp = fingerprintPrivateKey(witnessAKey);
const witnessBFp = fingerprintPrivateKey(witnessBKey);
const stingray = createStingraySystem({
signingKey: operator.privateKeyPem,
quorumThreshold: { required: 2, outOf: 2 },
disablePausePoll: true,
disableLogging: true,
});
const base = {
pci: 42, tac: 1234, mcc: "310", mnc: "260", earfcn: 5230,
geohash: "dr5ru", observedAt: "2026-04-26T12:00:00Z", windowMs: 60_000,
rsrpGradient: [200, 180, 160, 140, 120, 100, 80, 60],
channelListDigest: hashOf("real-channels"),
sib1Hash: hashOf("real-sib1"),
ravenTileId: hashOf("raven-tile-1"),
};
try {
// Witness A and B see same tower at same place – but disagree on neighbor list.
stingray.observeTower(signObs({ ...base, neighborListDigest: hashOf("real-neighbors"), witnessFingerprint: witnessAFp }, witnessAKey));
stingray.observeTower(signObs({ ...base, neighborListDigest: hashOf("fake-neighbors"), witnessFingerprint: witnessBFp }, witnessBKey));
await flush();
const eqs = stingray.facts.equivocations();
console.log(`equivocations: ${eqs.length}`);
// Both witnesses co-sign the contradiction to satisfy 2-of-2 quorum.
for (const fp of [witnessAFp, witnessBFp]) {
stingray.receiveWitness({
schemaVersion: 1, fingerprint: fp, equivocationId: eqs[0]!.equivocationId,
signedAt: new Date().toISOString(),
signature: hashOf(`${fp}|${eqs[0]!.equivocationId}`),
});
}
await flush();
const suspects = stingray.facts.suspectedStingrays();
console.log(`suspects published: ${suspects.length}`);
if (suspects[0]) console.log(`pci=${suspects[0].pci} bucket=${suspects[0].geohashBucket}`);
} finally {
await stingray.shutdown();
}
}
main().catch((err) => { console.error(err); process.exit(1); });
Run with tsx index.ts. Expected output:
equivocations: 1
suspects published: 1
pci=42 bucket=dr5ru
▶ Open in StackBlitz – runs in your browser, no install required.
What you get
A signed Stingray.Suspect cassette containing:
- The disagreeing witness observations, each with its own signature.
- The cell ID, time window, and geohash bucket where the contradiction occurred.
- A Rekor uuid pinning the whole bundle to the public log.
The signed body never carries the IMSI, the TMSI (Temporary Mobile Subscriber Identity), or any subscriber identifier. Stingray observes the tower's behavior, not the phone's identity. A journalist or attorney can hand the bundle to any independent expert; the math is checkable without trust in the publisher.
What it can't do
- An attacker who controls k of n witnesses can manufacture false suspects. The k-of-n threshold is a tunable defense – high-stakes deployments should require larger quorums.
- Carrier maintenance can transiently look like equivocation. Real reconfigs cluster cleanly across all witnesses; equivocation does not. Time-bucket tuning (default 60 seconds) filters most operational noise.
- A sophisticated IMSI catcher that broadcasts bit-for-bit identical content but varies signal power per direction is invisible. Stingray catches contradiction in broadcast content, not signal-direction games.
- Live capture, daemon mode, and Studio routes ship in a follow-up. The alpha is in-memory only.
A real-world example
A civic group in Chicago organizes rolling demonstrations over six weeks. After the third weekend, several members report unusual account-access notifications on their devices. The group's technologist deploys eight Stingray witnesses across staging areas – laptops with RTL-SDRs running for two-hour windows during each march, each signing independently. Two weeks later, witnesses 3, 5, and 7 each report cell ID 314 in the same downtown bucket at the same minute on three separate Saturdays, with different SIB1 hashes each time. The constraint engine emits three signed Suspect cassettes, which the group publishes with their Rekor uuids. A cooperating journalist verifies the bundles independently and a subsequent freedom-of-information request returns records of police cell-site-simulator deployments on the same dates.
For developers
Predicate URIs
| URI | What it attests |
|---|---|
https://pluck.run/Stingray.Tower/v1 | One witness observed a tower with this PCI/TAC/MCC/MNC/EARFCN, broadcasting these neighbor/channel/SIB1 digests, in this geohash at this time. |
https://pluck.run/Stingray.Equivocation/v1 | Two witnesses saw the same (PCI, geohash bucket, time bucket) but reported different broadcast digests – mathematical contradiction. |
https://pluck.run/Stingray.Suspect/v1 | k-of-n distinct witnesses co-signed an equivocation chain – the red dot. |
Programs composed
- Raven – IQ-tile substrate. Every observation cites the Raven tile its RF was extracted from
- Whistle-style ephemeral witness keys (single-use, cannot be correlated across submissions)
- Pluck core's DSSE in-toto envelopes + Sigstore Rekor client
- Directive's facts/constraints/resolvers – the equivocation detector and the quorum gate are Directive constraints
- Bureau core's pause kill-switch and operator-key tooling
Threat model + adversary
An attacker controlling at least k of n witnesses can manufacture false suspects (tune quorum to threat model). Carrier reconfigurations can transiently look like equivocation (default 60-second time bucket filters noise). A Stingray that mimics a real tower bit-for-bit and varies only per-direction signal power is invisible to broadcast-content checks. Stingray assumes the witness's RF substrate (Raven tile) is honest; defense is k-of-n quorum across distinct ephemeral fingerprints.
Verify a published cassette
pluck bureau verify <bundle-dir>
cosign verify-blob \
--key <pubkey.pem> \
--signature <signature.sig> \
--type https://pluck.run/Stingray.Suspect/v1 \
<body.json>
See also
- Bureau Foundations
- Threat Model
- Verify a dossier
- Raven – RF substrate Stingray rides on
- Karma – WiFi version of the same equivocation pattern