- Docs
- Bureau — Blue Team (defensive)
- Karma
Bureau — Blue Team (defensive)
Karma
An "evil twin" is a malicious WiFi access point that broadcasts the same SSID as a network the user trusts. Karma observes WiFi beacons from multiple independent witnesses and flags access points that share an SSID but disagree on cryptographic details (BSSID, cipher suite, vendor firmware fingerprint) that a legitimate roaming partner would match.
Posture: 🔵 Blue Team (defensive) · Status: alpha
What it does
An "evil twin" is a malicious WiFi access point that broadcasts the same network name (the SSID – what you see in your phone's WiFi list, like "Cafe Karma" or "Airport-Free-WiFi") as a real network you trust. Your laptop, by default, will happily connect to whichever access point with that name has the strongest signal. The evil twin captures your traffic, prompts you for credentials it has no right to, or sits as a downgrade attack vector.
Real access points sharing an SSID – the eight ceiling-mounted units in your office, two routers in your house bridged together – also share a lot of cryptographic detail. The exact WPA2/WPA3 cipher suite. The vendor-specific signature of their firmware (Cisco gear advertises differently than Aruba differently than a $30 consumer router). The country code, beacon interval, capability bitfield. Real roaming partners agree on all of these because the same network admin configured them. An evil twin gets the SSID right and almost never gets the rest right. Karma watches multiple receivers – phones, laptops, fixed sensors – observe every beacon they see, and when two of them report the same SSID at the same place and time but with different cryptographic details, that's a contradiction. Karma signs it as an EvilTwin proof.
Who would use it
- A traveling journalist who wants a tamper-evident record of the WiFi environment in the hotel suite where they met a source.
- A startup security team running a bug-bounty event in a coworking space, monitoring for opportunistic evil twins targeting visiting attendees.
- An IT department auditing a remote office and wanting machine-checkable observations of any rogue APs that appeared during the audit window.
- A retailer documenting that no rogue checkout-bypass APs are advertising the store's guest network during business hours.
- A security researcher publishing a real-world dataset of evil-twin attacks for academic study.
What you'll need
- The Pluck CLI installed (
pnpm add -g @sizls/pluck-cli). - A WiFi adapter that supports monitor mode – the cheap option is an Alfa AWUS036ACM USB adapter (~$50 on Amazon), which works with
airodump-ngon Linux/macOS. Built-in laptop WiFi rarely works for capture. - Two or more witness positions in the same area at the same time. One witness is testimony; multiple is proof.
- An operator key per witness.
Step-by-step
The alpha runs an in-memory demo on synthetic beacons. Live capture (an airodump-ng bridge feeding signed beacons into Karma's witness pipeline) ships in a follow-up. To see the contradiction engine work today:
pluck bureau karma demo
You'll see something like:
karma/demo: ingesting 4 synthetic beacons (A=real, B=evil twin, C=other geohash, D=roaming partner)...
[Bureau/Karma] red-dot proof=af46189feb7e… ssid=Cafe Karma bucket=Cafe Karma|dr5ru|2026-04-26T12:00:00Z
karma/demo: evil-twin proofs emitted = 2
karma/demo: disagreement = {"bssid":true,"rsnAkm":true,"vendorIe":true}
The synthesized beacons are: A is a legitimate "Cafe Karma" AP. B shares the SSID and bucket but has a different MAC address (BSSID), a different cipher-suite hash (RSN/AKM = Robust Security Network / Authentication and Key Management), and a different vendor-firmware fingerprint – that's the evil twin. C shares the SSID but is in a different geohash (out of bucket – must not trigger). D is a legitimate roaming partner – same SSID, same security, different MAC (must not trigger). Karma correctly fires only on the (A, B) pair.
In production, the same logic runs over live airodump-ng captures: every observed beacon is decoded, hashed, and signed by the witness host. The Karma constraint engine ingests the signed beacons and emits proofs.
Run it yourself
Drop this into a Node 18+ project (npm install @sizls/pluck-bureau-karma @sizls/pluck-bureau-core tsx):
// index.ts
import { createHash } from "node:crypto";
import {
createKarmaSystem,
fingerprintPrivateKey,
signRawDigest,
} from "@sizls/pluck-bureau-karma";
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 signBeacon(body: any, key: string) {
const skel = { schemaVersion: 1, ...body };
const observationId = createHash("sha256")
.update(JSON.stringify(skel))
.digest("hex");
const signed = signRawDigest({ ...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 karma = createKarmaSystem({
signingKey: operator.privateKeyPem,
disablePausePoll: true,
disableLogging: true,
});
const common = {
ssid: "Cafe Karma", ssidHidden: false, channel: 6, countryCode: "US",
beaconIntervalTu: 100, capabilityInfo: 0x1011, geohash: "dr5ru",
rssiGradient: [200, 180, 160, 140, 120, 100, 80, 60],
observedAt: "2026-04-26T12:00:00Z",
};
try {
// Real AP – Witness A signs.
karma.observeBeacon(signBeacon({
...common, bssid: "aa:bb:cc:11:22:33",
rsnAkmDigest: hashOf("real-rsn-wpa3"),
vendorIeDigest: hashOf("real-vendor-cisco"),
witnessFingerprint: witnessAFp,
}, witnessAKey));
// Evil twin – same SSID + bucket, different BSSID/RSN/vendor.
karma.observeBeacon(signBeacon({
...common, bssid: "11:22:33:44:55:66",
rsnAkmDigest: hashOf("evil-twin-rsn-open"),
vendorIeDigest: hashOf("evil-twin-vendor-pineapple"),
witnessFingerprint: witnessBFp,
}, witnessBKey));
await flush();
const proofs = karma.facts.evilTwinPairs();
console.log(`evil-twin proofs: ${proofs.length}`);
if (proofs[0]) console.log(`ssid=${proofs[0].ssid} bucket=${proofs[0].bucketKey}`);
} finally {
await karma.shutdown();
}
}
main().catch((err) => { console.error(err); process.exit(1); });
Run with tsx index.ts. Expected output:
evil-twin proofs: 1
ssid=Cafe Karma bucket=dr5ru:1745668800000
▶ Open in StackBlitz – runs in your browser, no install required.
What you get
A signed Karma.EvilTwin cassette containing:
- Both disagreeing beacon observations, each signed by their witness.
- The disagreement matrix (which fields disagreed: BSSID, security cipher, vendor firmware fingerprint).
- The bucket key (
SSID|geohash|time-window). - A Rekor uuid pinning the bundle to the public log.
Karma never carries client MAC addresses, probe requests, or association data. It observes the access point's broadcast, not the station. A bundle is sufficient for an IT auditor to confront a venue operator with concrete observations; for a journalist to publish a defensible story; for a court to admit evidence whose math any independent expert can check.
What it can't do
- An attacker who can spoof the BSSID + cipher suite + vendor fingerprint exactly is invisible. Real enterprise gear is hard to spoof exactly because vendor-firmware fingerprints come from firmware. Consumer evil-twin kits don't bother. State-actor adversaries with real hardware can defeat the detector.
- Karma doesn't catch BSSID-only impersonation when only the attacker is in earshot. Karma needs at least one real beacon to contradict against.
- Network admins legitimately reconfiguring an SSID create transient false positives. The default sliding window (one hour) and the requirement for two distinct witnesses in the same time bucket filters most of this.
- Live capture and daemon mode ship in a follow-up.
A real-world example
A regional credit union runs an unannounced security audit at their busiest branch. Two contractors place unobtrusive Raspberry Pi units running Karma witnesses in the lobby – one near the ATM, one in the loan-officer waiting area. They run for four business hours. Karma flags two evil-twin proofs: both targeting the SSID "CU-Members," advertised from a parked car in the lot. The disagreement matrix shows the cloned APs got the SSID right and the encryption-suite hash wrong (they used WPA2 personal where the real AP advertises WPA2 enterprise). The signed bundles, with their Rekor uuids, get attached to the audit report. Compliance escalates to law enforcement; the contractors hand over the cassettes; an independent cybersecurity firm hired by the regulator re-runs the verification on a second machine and reaches the same conclusion without touching the credit union's network.
For developers
Predicate URIs
| URI | What it attests |
|---|---|
https://pluck.run/Karma.Beacon/v1 | One witness observed an 802.11 beacon with this SSID/BSSID/RSN-AKM/vendor-IE/country/channel/capability in this geohash at this time. |
https://pluck.run/Karma.EvilTwin/v1 | Two witnesses saw the same SSID + geohash + time bucket with different (BSSID, RSN-AKM, vendor-IE) – mathematical contradiction. |
https://pluck.run/Karma.Deauth/v1 | One witness counted N deauth frames from BSSID X within window W in geohash G – burst exceeds threshold. |
Programs composed
- Raven – IQ-tile substrate the beacon was decoded from
- Oath – operator-key provenance
- Pluck core's DSSE in-toto envelopes + Sigstore Rekor client
- Directive constraints – evil-twin detector + sliding-window deauth aggregation
- Bureau core's pause kill-switch (Karma pauses if
karmaOR upstreamravenis paused)
Threat model + adversary
An attacker who can spoof BSSID + RSN-AKM + vendor-IE exactly is invisible. Vendor-IE blobs are firmware-derived, so consumer kits typically miss them; state-actor adversaries with real Cisco hardware on the same firmware version can defeat the detector. Karma doesn't catch BSSID-only impersonation when only the attacker's AP is in earshot of all witnesses. Hidden-SSID networks (zero-length SSID) are bucketed and contradicted, but verifiers must not assume two empty SSIDs in the same bucket are the same network.
Verify a published cassette
pluck bureau verify <bundle-dir>
cosign verify-blob \
--key <pubkey.pem> \
--signature <signature.sig> \
--type https://pluck.run/Karma.EvilTwin/v1 \
<body.json>
See also
- Bureau Foundations
- Threat Model
- Verify a dossier
- Raven – RF substrate Karma rides on
- Stingray – cellular version of the same equivocation pattern