Skip to content

Bureau — Blue Team (defensive)

Raven

Raven publishes a tamper-evident public record of which radio frequencies were active in a given location and time window. Stingray, Karma, Celeste, and Cosmos all consume Raven's signed snapshots as their RF substrate.

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

What it does

A USB software-defined radio captures a few seconds of IQ data and Raven reduces it to a 32-bin Welch power spectral density "fingerprint" representing the energy in each frequency band. Raven signs the fingerprint with the operator's key and publishes it to the public Sigstore Rekor log – an append-only record any party can read and no party can rewrite.

Raven does not publish the raw recording, which would expose voice, Bluetooth pairings, and other protocol-layer content. The fingerprint is lossy by design: two honest receivers in the same place and time window produce the same fingerprint byte-for-byte; two receivers disagreeing in the same window is a contradiction. Other Bureau programs (Stingray for cell, Karma for WiFi, Celeste for GPS, Cosmos for satellite) plug protocol-specific decoders on top of Raven's signed snapshots to label contradictions by mechanism.

Who would use it

  • A protester organizer who wants a tamper-evident record of what radios were active during a march.
  • A journalist documenting an alleged surveillance incident and needing third-party-verifiable observations of the radio environment was consistent with their account.
  • A community broadcaster proving to a regulator that nobody was on their licensed frequency during a coverage gap.
  • A researcher building public spectrum-occupancy datasets that other researchers can cryptographically trust.
  • An infrastructure auditor producing court-admissible records of the RF environment around a substation, port, or airport.

What you'll need

  • The Pluck CLI installed (pnpm add -g @sizls/pluck-cli).
  • A software-defined radio. The cheapest entry is an RTL-SDR Blog v4 USB dongle (~$35 on Amazon) which works on Mac, Linux, and Windows. For wider band coverage, a HackRF One (~$330) or a USRP B200mini are common upgrades.
  • A capture tool that ships with your SDR – rtl_sdr for the RTL-SDR Blog, hackrf_transfer for the HackRF, uhd_rx_cfile for USRP. They produce .cu8 (8-bit) or .cf32 (32-bit float) IQ files Raven reads natively.
  • An operator key. pluck bureau init --keys ./keys generates one.

Step-by-step

  1. Capture a few seconds of RF on the band you care about. For a 915 MHz industrial-band recording with an RTL-SDR:

    Shell
    rtl_sdr -f 915000000 -s 1000000 -n 10000000 capture.cu8
    
  2. Look up your geohash – a short string that names a roughly-1km square of the planet. geohash.softeng.co will give you one for any address; dr5ru is midtown Manhattan.

  3. Run Raven:

    Shell
    pluck bureau raven sweep \
      --input capture.cu8 \
      --geohash dr5ru \
      --band-center 915000000 \
      --bandwidth 200000 \
      --sample-rate 1000000 \
      --keys ./keys \
      --accept-public \
      --out ./.raven
    

    You'll see something like:

    raven/sweep: tile=8b9c1d… sweepId=4e2f70… → rekor uuid 24296f… (logIndex=18472301)
    raven/sweep: wrote ./.raven/4e2f70….sweep.json
    
  4. The .sweep.json file in ./.raven/ is your dossier. The rekor uuid is a permanent public address – anyone, anywhere, can verify your claim with pluck bureau raven verify ./.raven/<sweepId>.sweep.json or by looking up the uuid on search.sigstore.dev.

  5. To compare a current sweep against a baseline (e.g. "is anything new on the air tonight that wasn't here last week?"):

    Shell
    pluck bureau raven anomaly ./current.sweep.json \
      --baseline ./baseline.sweep.json \
      --threshold 0.3 \
      --keys ./keys \
      --out ./.raven/markers
    

Run it yourself

Raven's primary surface is the CLI shown above (pluck bureau raven sweep / anomaly / replay / verify). For programmatic use, the library exposes pure helpers (buildIqTile, computeWelchDigest, buildSweep, verifySweepLocal) and a Directive system factory (createRavenSystem) for in-process IQ ingestion. Drop this into a Node 18+ project (npm install @sizls/pluck-bureau-raven @sizls/pluck-bureau-core tsx):

TypeScript
// index.ts
import { createRavenSystem } from "@sizls/pluck-bureau-raven";
import {
  computeWelchDigest,
  verifySweepLocal,
} from "@sizls/pluck-bureau-raven";
import { generateOperatorKey } from "@sizls/pluck-bureau-core";

async function main() {
  const operator = generateOperatorKey();

  // Synthetic IQ – 1MS/s, 100ms window of complex Gaussian noise.
  const sampleRateHz = 1_000_000;
  const durationMs = 100;
  const samples = (sampleRateHz * durationMs) / 1000;
  const iq = new Float32Array(samples * 2);
  for (let i = 0; i < iq.length; i++) iq[i] = (Math.random() - 0.5) * 0.1;

  // Use the pure Welch helper directly – deterministic 32-bin PSD digest.
  const welch = computeWelchDigest({
    iqBuffer: iq, sampleRateHz,
    band: { centerHz: 915_000_000, bandwidthHz: 200_000 },
  });
  console.log(`welch bins: ${welch.bins.length}, digest: ${welch.digest.slice(0, 12)}...`);

  // Or wire a full Directive-backed system (constraints, plugins, observability).
  const raven = createRavenSystem({
    signingKey: operator.privateKeyPem,
    acceptPublic: false,           // local-only – don't post to Sigstore
    disablePausePoll: true,
    disableLogging: true,
  });

  try {
    await raven.ingestTile({
      iqBuffer: iq, sampleRateHz,
      geohash: "dr5ru",
      band: { centerHz: 915_000_000, bandwidthHz: 200_000 },
      startedAt: "2026-04-26T12:00:00Z",
      durationMs,
    });

    const sweep = await raven.finalizeSweep({
      startedAt: "2026-04-26T12:00:00Z",
      endedAt:   "2026-04-26T12:00:01Z",
    });
    console.log(`tiles: ${sweep.tiles.length} merkleRoot: ${sweep.merkleRoot.slice(0, 12)}...`);

    // Local structural verify (no Rekor lookup).
    const result = verifySweepLocal(sweep);
    console.log(`verify: ${result.ok ? "PASS" : `FAIL (${result.reason})`}`);
  } finally {
    await raven.shutdown();
  }
}

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

Run with tsx index.ts. Expected output:

welch bins: 32, digest: 1f2c8e9a4b30...
tiles: 1 merkleRoot: a01b3c4d5e6f...
verify: PASS

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

What you get

A .sweep.json containing:

  • The 32-number Welch PSD fingerprint per IQ tile.
  • A geohash, time window, and band you tuned to.
  • A Merkle root chaining all the tiles in the sweep.
  • A signature using your operator key.
  • A Rekor uuid + log index pinning the whole bundle to a public, append-only timestamping log.

Anyone can independently verify the bundle is internally consistent (signatures match the public key) and externally consistent (the Rekor entry exists, with the timestamp claimed). They cannot, from the bundle alone, learn what was actually transmitted – only its energy footprint.

What it can't do

  • A single SDR is testimony, not proof. A dishonest operator can sign anything they want. Raven's quorum mode (required N of M) lets two or more receivers in the same place co-sign – that turns testimony into machine-checkable contradiction-or-confirmation.
  • Raven trusts your clock and your geohash claim. A liar can claim Tokyo at midnight while sitting in Lagos at noon. Use Celeste alongside Raven for verifiable time + position anchors.
  • The fingerprint is intentionally lossy. Raven cannot tell what protocol was on the air, only that energy was there. Stingray/Karma/Celeste/Cosmos sit on top to label.
  • No live mode in alpha. The current CLI processes captured files. A pause-aware daemon for continuous monitoring ships in a follow-up.

A real-world example

A community organizer in Oakland is preparing for a Saturday demonstration and wants verifiable evidence of the RF environment near the staging area between 10am and 11am. She runs an RTL-SDR Blog v4 on her laptop at the staging point, captures a 60-second IQ file every ten minutes, and runs raven sweep on each. She publishes the seven resulting Rekor uuids on the organization's blog. After the event, two researchers download the bundles and independently verify the timestamps and signatures. When questions later arise about official RF activity at 10:15am, the published sweeps – entered into the public log before any dispute – provide a third-party-verifiable record of the observed RF environment that can be cross-referenced against agency filings.


For developers

Predicate URIs

URIWhat it attests
https://pluck.run/Raven.Tile/v1One operator observed RF in geohash G during time window W on band B; here is the deterministic fingerprint.
https://pluck.run/Raven.Sweep/v1One operator bundles N tiles, with this Merkle root, as one continuous observation.
https://pluck.run/Raven.MerkleLeaf/v1One leaf belongs in this forest at this index, chained to this previous leaf.
https://pluck.run/Raven.Anomaly/v1A sweep diverged from a baseline by spectral distance D; here is the broad classification.

Raven never attests who was transmitting, whether they were authorized, or whether the operator's clock is honest. Time-source attestation lives in Celeste and Cosmos.

Programs composed

  • Pluck core (@sizls/pluck) – DSSE in-toto envelopes, Rekor client, IQ file parser
  • Bureau core (@sizls/pluck-bureau-core) – operator key generation, pause kill-switch, redact policy
  • Directive (@directive-run/core) – facts, derivations, constraints, resolvers, effects, plugins
  • Sigstore Rekor – the public append-only transparency log
  • Substrate consumed by Stingray, Karma, Celeste, and Cosmos

Threat model + adversary

Single-receiver sweeps are testimony – a dishonest operator with one SDR can sign whatever fingerprint they want. Cross-receiver quorum (quorum: { required, outOf }) converts testimony into machine-checkable proof. Raw IQ buffers cap at 256 MiB per call; tiles per sweep cap at 10,000; geohash precision is restricted to 4–9. Welch determinism holds only when receivers run the same window type, segment length, and overlap – Raven locks these defaults, custom configs break cross-receiver verification by design.

Verify a published cassette

Shell
pluck bureau verify <bundle-dir>

# Or with cosign directly
cosign verify-blob \
  --key <pubkey.pem> \
  --signature <signature.sig> \
  --type https://pluck.run/Raven.Sweep/v1 \
  <body.json>

See also

Edit this page on GitHub
Previous
Custody

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 →