Skip to content

Bureau — Blue Team (defensive)

Knob

Bluetooth pairings (headphones, smart locks, fitness trackers, medical devices) are subject to known downgrade attacks in which a man-in-the-middle weakens the negotiated encryption (e.g., KNOB, CVE-2019-9506). Knob signs every Bluetooth pairing transcript so a downgrade is detectable in the published record.

Posture: 🔵 Blue Team (defensive)   ·   Status: alpha

What it does

Every time two Bluetooth devices pair (your phone and a smartwatch, a hospital infusion pump and a tablet, a laptop and a wireless headset), they negotiate how strong their encryption will be – they pick a key length, an authentication method, and whether to require a man-in-the-middle check. The Bluetooth standard says modern devices should always settle on the strongest options both sides support. In practice, four well-known attacks subtly downgrade the negotiation so an attacker can break or impersonate the encryption.

Knob watches the negotiation. A "witness" – a host computer that can see the pairing traffic, like a phone or a security-monitoring laptop in the same room – captures the SMP (Security Manager Protocol, the Bluetooth handshake) frames, throws away the secret bytes (it never logs the actual encryption key), keeps only the negotiated parameters, and signs the resulting transcript with its key. Knob ingests the signed transcripts and detects four specific contradictions: the KNOB key-size shrink (CVE-2019-9506 – coerced from a 16-byte key down to a 7-byte key brute-forceable in seconds), the BIAS cross-transport reuse (CVE-2020-10135 – same key reused across the classic and low-energy transports without the spec-required re-pair), the IO-cap downgrade (forcing "Just Works" mode instead of stronger numeric comparison or passkey entry), and the MITM-drop (one side asked for man-in-the-middle protection and didn't get it).

Who would use it

  • A hospital biomed-engineering team needing an audit trail of every Bluetooth pairing between bedside monitors and tablets – clinical-incident forensics requires observations that no insulin pump was paired over a downgraded link.
  • An automotive OEM running pre-shipment compliance tests on the in-vehicle Bluetooth stack to verify no downgrade is ever silently accepted.
  • An enterprise IT team auditing employee laptops + headsets for observations of evil-twin pairing attempts during travel.
  • A penetration tester delivering signed observations to a client that their conference room's BLE-enabled smart-lock pairing got downgraded under attack.
  • A regulator (FDA, NHTSA) requiring tamper-evident pairing logs as part of post-market surveillance for connected medical or vehicular devices.

What you'll need

  • The Pluck CLI installed (pnpm add -g @sizls/pluck-cli).
  • A Bluetooth host capable of sniffing SMP exchanges. The cheap path: Nordic nRF52840 dongle (~$10 on Amazon) running open-source firmware like Sniffle or NCC Group's Sniffle. Production-grade: Ellisys Bluetooth Explorer or Frontline ComProbe BPA (both $$$, used by OEM compliance labs).
  • An operator key per witness host.

Step-by-step

The alpha runs an in-memory demo on synthetic transcripts. Live capture (a Sniffle bridge feeding signed SMP frames into Knob) ships in a follow-up. To see the contradiction engine work today:

Shell
pluck bureau knob demo

You'll see something like:

knob/demo: ingesting 4 synthetic pairing transcripts (1 legit, 1 KNOB key-size, 1 BIAS pair)...
[Bureau/Knob] red-dot proof=1bdf49c5f6a0… class=knob-key-size transcripts=1
[Bureau/Knob] red-dot proof=775e1e96afa1… class=bias-cross-transport transcripts=2
knob/demo: downgrade proofs emitted = 2

The synthesized transcripts: one legitimate (16-byte LTK, Numeric Comparison, MITM honored), one KNOB-class (both endpoints supported 16 bytes but settled on 7), and a BIAS pair (the same Long-Term Key fingerprint observed first on classic Bluetooth, then on Low Energy, no re-pair in between). The constraint engine emits two downgrade proofs.

Run it yourself

Drop this into a Node 18+ project (npm install @sizls/pluck-bureau-knob @sizls/pluck-bureau-core tsx):

TypeScript
// index.ts
import { createHash } from "node:crypto";
import {
  createKnobSystem,
  fingerprintPrivateKey,
  signCanonicalBody,
} from "@sizls/pluck-bureau-knob";
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 signTranscript(body: any, key: string) {
  const skel = { schemaVersion: 1, ...body };
  const transcriptId = createHash("sha256").update(JSON.stringify(skel)).digest("hex");
  const signed = signCanonicalBody({ ...skel, transcriptId }, key);
  return { ...skel, transcriptId, signature: signed.signature };
}

async function main() {
  const operator = generateOperatorKey();
  const phoneKey = (generateOperatorKey()).privateKeyPem;
  const phoneFp = fingerprintPrivateKey(phoneKey);
  const laptopKey = (generateOperatorKey()).privateKeyPem;
  const laptopFp = fingerprintPrivateKey(laptopKey);

  const knob = createKnobSystem({
    signingKey: operator.privateKeyPem,
    disablePausePoll: true,
    disableLogging: true,
  });

  const ltkBias = hashOf("bias-shared-ltk");

  try {
    // KNOB downgrade – both ends advertised 16, negotiated 7.
    knob.observePairing(signTranscript({
      transport: "le", pairingMethod: "lesc-just-works",
      vendorOuiInitiator: "aabbcc", vendorOuiResponder: "deadbe",
      deviceLocalIdInitiator: "phone-1", deviceLocalIdResponder: "rogue-watch",
      keySize: { requestedMaxBytes: 16, responseMaxBytes: 16, negotiatedBytes: 7 },
      ioCapAdvertisedInitiator: "display-yes-no",
      ioCapAdvertisedResponder: "display-yes-no", ioCapUsed: "display-yes-no",
      mitmRequiredAdvertised: true, mitmRequiredUsed: true,
      ltkFingerprint: hashOf("knob-target-ltk"),
      observedAt: "2026-04-26T12:01:00Z",
      witnessFingerprint: phoneFp,
    }, phoneKey));

    // BIAS – same LTK fingerprint observed first on br-edr, then on le.
    for (const [transport, when] of [["br-edr", "12:02:00Z"], ["le", "12:02:30Z"]] as const) {
      knob.observePairing(signTranscript({
        transport, pairingMethod: "lesc-numeric-comparison",
        vendorOuiInitiator: "001122", vendorOuiResponder: "334455",
        deviceLocalIdInitiator: "laptop-1", deviceLocalIdResponder: "headset-1",
        keySize: { requestedMaxBytes: 16, responseMaxBytes: 16, negotiatedBytes: 16 },
        ioCapAdvertisedInitiator: "display-yes-no",
        ioCapAdvertisedResponder: "display-yes-no", ioCapUsed: "display-yes-no",
        mitmRequiredAdvertised: true, mitmRequiredUsed: true,
        ltkFingerprint: ltkBias,
        observedAt: `2026-04-26T${when}`,
        witnessFingerprint: laptopFp,
      }, laptopKey));
    }

    await flush();
    const proofs = knob.facts.downgradeProofs();
    console.log(`downgrade proofs: ${proofs.length}`);
    for (const p of proofs) console.log(`  class=${p.class}`);
  } finally {
    await knob.shutdown();
  }
}

main().catch((err) => { console.error(err); process.exit(1); });

Run with tsx index.ts. Expected output:

downgrade proofs: 2
  class=knob-key-size
  class=bias-cross-transport

▶ Open in StackBlitz – runs in your browser, no install required.

What you get

A signed Knob.KeySize / Knob.BIAS / Knob.Downgrade cassette per detected contradiction containing:

  • The verbatim SMP transcript(s), each signed by their witness host.
  • The disagreement: e.g. for KNOB, the spec-allowed 16-byte capability vs the 7-byte negotiated outcome.
  • The Long-Term Key fingerprint (sha256), never the LTK itself. BIAS detection compares fingerprints, not key material.
  • A Rekor uuid pinning the bundle to the public log.

A hospital risk officer, FDA investigator, or penetration tester can hand the bundle to any independent expert; the contradiction is mathematical (the negotiated key really is below 16 bytes; the same fingerprint really did appear across both transports).

What it can't do

  • No witness on the channel = no transcript = no proof. Coverage is an operator deployment decision.
  • An attacker who controls both endpoints AND every witness can hide a downgrade. Knob raises the cost of silence proportional to witness diversity.
  • A witness with a compromised signing key can fabricate transcripts. Rotate revocation invalidates the witness's signed history once its key is rotated.
  • Live capture and daemon mode ship in a follow-up.

A real-world example

A children's hospital adds a fleet of Bluetooth-enabled wearable monitors for inpatient cardiology. The biomed team installs three unobtrusive Knob witnesses (small Linux boxes with nRF52 dongles) on the cardiology floor. Eight weeks in, Knob fires a Knob.KeySize proof: a third-party electrophysiology consult-cart paired to a patient's monitor with a 7-byte key when the monitor advertises 16. The biomed team isolates the cart, pulls firmware, and finds an out-of-date Bluetooth stack vulnerable to CVE-2019-9506. The signed bundle goes to the device manufacturer, who issues a recall. The hospital's tamper-evident pairing record is one of the artifacts the FDA reviews when approving the corrective action.


For developers

Predicate URIs

URIWhat it attests
https://pluck.run/Knob.Pairing/v1One witnessed SMP transcript – transport, pairing method, IO-cap advertised vs used, MITM advertised vs used, key-size negotiation, LTK fingerprint, vendor-OUI of each side.
https://pluck.run/Knob.KeySize/v1KNOB-class downgrade – LTK negotiated below 16 bytes when both endpoints supported 16.
https://pluck.run/Knob.BIAS/v1BIAS-class cross-transport key reuse – same LTK fingerprint observed on both BR/EDR and LE without an intervening re-pair.
https://pluck.run/Knob.Downgrade/v1Generic IO-cap or MITM-drop downgrade – capabilities advertised in pairing-request disagree with capabilities used in pairing-response.

Programs composed

  • Oath – witness host identity is an Oath-managed signing key
  • Custody – every transcript is a Custody leaf, hash-linked into the per-device dossier
  • Rotate – when a witness key is revoked, transcripts after the revocation epoch are flagged
  • Sigstore Rekor – proofs are notarized as DSSE in-toto envelopes with a public inclusion proof

Threat model + adversary

Knob needs a witness on the channel. Vendor-OUI is recorded; per-device MAC bytes are scrubbed before signing. The LTK never lands in Rekor – only its sha256 fingerprint. Forged transcripts: a witness with a compromised signing key can fabricate; Rotate revocation invalidates after the rotation epoch. An attacker controlling both endpoints AND every witness can hide a downgrade.

Verify a published cassette

Shell
pluck bureau verify <bundle-dir>

cosign verify-attestation \
  --certificate-identity-regexp '.*' \
  --certificate-oidc-issuer-regexp '.*' \
  <envelope-hash>.intoto.jsonl

See also

Edit this page on GitHub
Previous
Cosmos
Next
Icarus

Ready to build?

Install Pluck and follow the Quick Start guide to wire MCP-first data pipelines into your agents and fleets in minutes.

Get started →