Skip to content

Bureau — Blue Team (defensive)

Ignition

Each ECU (Electronic Control Unit) in a modern vehicle has a physical-layer signature on the CAN bus that an unauthorized substitution typically does not reproduce. Odometer rollback can be detected against signed history; calibration tampering can be detected against signed firmware manifests. Ignition produces signed observations for four classes of automotive contradiction.

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

What it does

There are between 30 and 100 small computers in a typical 2020-vintage vehicle: one runs the engine, one the transmission, one the anti-lock brakes, one the airbags, one the cluster, one infotainment. Each is an ECU (Electronic Control Unit). They talk on a shared wire pair called the CAN bus (Controller Area Network – the standard automotive network, ISO 11898). Mechanics talk to ECUs through UDS (Unified Diagnostic Services, ISO 14229) – the language a scan tool uses to read trouble codes, reflash firmware, or unlock a SecurityAccess session. Every car has a unique 17-character VIN (Vehicle Identification Number) on the chassis.

Ignition is a notary for everything that happens on that network. It ingests three kinds of signed observation. An ECU fingerprint – a bus-attached witness measures each ECU's analog signature (bit-time jitter, recessive vs dominant bus voltage, edge ramp shape, idle-frame spacing) and signs it. A UDS observation – a witness signs one diagnostic exchange (service ID, payload, SecurityAccess seed/key bytes, reported calibration ID and firmware version). An odometer reading – a witness signs current kilometers and the source. The manufacturer publishes a signed firmware manifest declaring what calibration ID and SecurityAccess policy it released for each ECU. Ignition runs four contradiction checks: ECU-swap (fingerprint deviates 3+ sigma from the registered VIN's centroid – somebody swapped the silicon), UDS-spoof (a SecurityAccess key isn't derivable from the manufacturer's signed policy – somebody is talking to the ECU with a key the manufacturer never issued), odometer-rollback (a current signed reading is less than a prior signed reading), and calibration-tamper (UDS-reported calibration ID disagrees with the manifest – the ECU is running unauthorized code).

Who would use it

  • A used-car buyer who is willing to pay a premium for a vehicle with a verifiable Ignition dossier.
  • A fleet operator (taxi company, delivery company, rental car) doing pre-purchase inspection.
  • A dealership service department building a tamper-evident pre-owned-certified history.
  • An insurance adjuster post-incident: did the airbag ECU run manufacturer firmware at the time of crash?
  • A regulator (NHTSA, EPA) auditing a recall: which VINs are running calibrations the manufacturer didn't sign?
  • A criminal court: was this car's odometer rolled back before the title transfer?

What you'll need

  • The Pluck CLI installed (pnpm add -g @sizls/pluck-cli).
  • An OBD-II reader with raw CAN access. Cheap option: a OBDLink MX+ (~$140) or CarPlay-compatible Veepeak Mini (~$25) for basic UDS + odometer; for ECU-fingerprint-grade physical-layer measurement you need a CANalyst-II (~$70) or a PEAK PCAN-USB (~$200+) with a logging host.
  • An operator key for each witness host (the laptop / dongle paired to a specific shop).
  • (For UDS-spoof / calibration-tamper) the manufacturer's signed firmware manifest, ingested via the SBOM-AI pipeline.

Step-by-step

The alpha runs an in-memory demo on a synthetic VIN with a manifest, a baseline, and three contradictions. Live capture (an OBD-II + CAN-bus dongle bridge feeding signed observations into Ignition) ships in a follow-up. To see the engine work today:

Shell
pluck bureau ignition demo

You'll see something like:

ignition/demo: registering manifest + 3 baseline ECM fingerprints + 1 outlier swap + 1 UDS spoof + 1 odometer rollback...
[Bureau/Ignition] tamper proof=d4687c9183dd… kind=ecu-swap vinHash=0c92afc0d764…
[Bureau/Ignition] tamper proof=61cd25fe244a… kind=uds-spoof vinHash=0c92afc0d764…
[Bureau/Ignition] tamper proof=a5e76a32f197… kind=odometer-rollback vinHash=0c92afc0d764…
ignition/demo: tamper proofs emitted = 3

The synthesized run seeds one manufacturer firmware manifest, three baseline engine-control-module (ECM) fingerprints to establish a centroid, then ingests one outlier ECU-swap fingerprint (more than 3 standard deviations from baseline across every axis), one SecurityAccess UDS exchange with a key not derivable from the manifest's policy, and one odometer reading 20,000 km below the prior reading.

Run it yourself

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

TypeScript
// index.ts
import { createHash } from "node:crypto";
import {
  createIgnitionSystem,
  fingerprintPrivateKey,
  signCanonicalBody,
  vinHashOf,
} from "@sizls/pluck-bureau-ignition";
import { generateOperatorKey } from "@sizls/pluck-bureau-core";

const flush = (n = 12) =>
  new Promise<void>((res) => {
    let i = 0;
    const tick = () => (++i >= n ? res() : setImmediate(tick));
    setImmediate(tick);
  });

function sign<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 witnessKey = (generateOperatorKey()).privateKeyPem;
  const witnessFp = fingerprintPrivateKey(witnessKey);
  const manufacturerKey = (generateOperatorKey()).privateKeyPem;
  const manufacturerFp = fingerprintPrivateKey(manufacturerKey);

  const vinHash = vinHashOf("DEMO-VIN-1HGCM82633A123456");
  const ignition = createIgnitionSystem({
    signingKey: operator.privateKeyPem,
    disablePausePoll: true, disableLogging: true,
  });

  try {
    // Manufacturer manifest declares calibration ID + UDS policy.
    ignition.recordFirmwareManifest(sign({
      vinHash, ecuId: "ECM", calibrationId: "CAL-9F2A",
      firmwareVersion: "1.2.3", securityAccessPolicy: "sha256-mirror",
      signedAt: "2026-04-25T08:00:00Z", manufacturerFingerprint: manufacturerFp,
    }, "manifestId", manufacturerKey));

    // 3 baseline ECM fingerprints establish the centroid.
    const baseline = {
      bitTimeJitterPpm: 50, recessiveVoltageMv: 2500, dominantVoltageMv: 1500,
      riseTimeNs: 80, fallTimeNs: 80, idleFrameSpacingMs: 10,
    };
    for (let i = 0; i < 3; i++) {
      ignition.observeFingerprint(sign({
        vinHash, ecuId: "ECM",
        fingerprint: { ...baseline, bitTimeJitterPpm: 49 + i },
        observedAt: `2026-04-26T09:${i}0:00Z`, witnessFingerprint: witnessFp,
      }, "observationId", witnessKey));
    }
    // Outlier – ECU SWAP (different silicon).
    ignition.observeFingerprint(sign({
      vinHash, ecuId: "ECM",
      fingerprint: {
        bitTimeJitterPpm: 950, recessiveVoltageMv: 4200, dominantVoltageMv: 600,
        riseTimeNs: 220, fallTimeNs: 220, idleFrameSpacingMs: 27,
      },
      observedAt: "2026-04-26T11:00:00Z", witnessFingerprint: witnessFp,
    }, "observationId", witnessKey));

    // UDS spoof – key not derivable from sha256-mirror policy.
    ignition.observeUds(sign({
      vinHash, ecuId: "ECM", serviceId: "SecurityAccess",
      seedHex: "0011223344556677", keyHex: "ffeeddccbbaa9988",
      observedAt: "2026-04-26T11:05:00Z", witnessFingerprint: witnessFp,
    }, "observationId", witnessKey));

    // Odometer rollback: 50,000 → 30,000 km.
    ignition.observeOdometer(sign({
      vinHash, kilometers: 50_000, source: "cluster",
      observedAt: "2026-04-26T08:00:00Z", witnessFingerprint: witnessFp,
    }, "readingId", witnessKey));
    ignition.observeOdometer(sign({
      vinHash, kilometers: 30_000, source: "obd2",
      observedAt: "2026-04-26T11:30:00Z", witnessFingerprint: witnessFp,
    }, "readingId", witnessKey));

    await flush();
    const proofs = ignition.facts.tamperProofs();
    console.log(`tamper proofs: ${proofs.length}`);
    for (const p of proofs) console.log(`  kind=${p.kind}`);
  } finally {
    await ignition.shutdown();
  }
}

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

Run with tsx index.ts. Expected output:

tamper proofs: 3
  kind=ecu-swap
  kind=uds-spoof
  kind=odometer-rollback

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

What you get

A signed Ignition.Tamper cassette per contradiction containing:

  • The verbatim offending fingerprint / UDS observation / odometer reading, signed by the witness.
  • The manifest entry it disagrees with (for UDS-spoof / calibration-tamper).
  • The contradiction kind.
  • A Rekor uuid pinning the bundle to the public log.

The VIN is hashed (vinHash = sha256(VIN)) before signing. A Rekor verifier learns only that two observations belong to the same vehicle – never the fleet roster. A used-car buyer, fleet auditor, or court can verify the bundle independently. The math is checkable without trust in the publisher.

What it can't do

  • Centroid bootstrap. ECU-swap detection needs at least 3 baseline fingerprints before the centroid stabilizes. New VINs are observed-but-not-judged until the floor is met. The MIN_CENTROID_SAMPLES = 3 baseline is a lab-demo floor – production-grade fingerprinting needs 100+ samples per Cho/Shin USENIX 2016. The 3-sample 3σ check is high-variance.
  • Compromised witness key produces signed-but-illegitimate observations. Rotate revocation invalidates after the rotation epoch.
  • Manufacturer trust. Calibration-tamper detection cross-checks against the manufacturer's signed manifest. A manufacturer signing a fraudulent manifest is outside Ignition's threat model.
  • Bus-attached attacker with a legitimate SecurityAccess key. UDS-spoof catches keys not derivable from the manifest policy – not legitimate-but-malicious access.
  • Live capture and daemon mode ship in a follow-up.

A real-world example

A regional used-car dealership group adds Ignition to its pre-owned-certified pipeline. Each vehicle entering certification receives a 20-minute scan: OBD-II for odometer and UDS, plus a CAN-bus fingerprint of the engine and transmission control modules against the manufacturer's manifest. Six months in, an inbound trade-in produces three contradictions: an ecu-swap on the engine ECU, a calibration-tamper (the ECM is running unsigned firmware), and an odometer-rollback (the cluster reading is 36,000 km below a service-history reading from 18 months earlier). The dealer declines the trade-in and uses the signed bundle to negotiate a $4,200 reduction with the wholesaler. The auction house subsequently begins requiring Ignition dossiers across regional auction lanes.


For developers

Predicate URIs

URIWhat it attests
https://pluck.run/Ignition.Ecu/v1One witness-signed CAN physical-layer fingerprint – bit-time jitter ppm, recessive/dominant voltage mV, rise/fall ramp ns, idle-frame spacing ms, ECU id, VIN-hash.
https://pluck.run/Ignition.Uds/v1One witness-signed UDS / ISO 14229 transcript – service ID, optional SecurityAccess seed/key bytes, payload digest, reported calibration ID and firmware version.
https://pluck.run/Ignition.Odometer/v1One witness-signed odometer reading – kilometers, source, optional cluster-side signature, VIN-hash.
https://pluck.run/Ignition.Tamper/v1Signed tamper proof carrying verbatim offending observations + the manifest entry it disagrees with.

Programs composed

  • Oath – every witness host has an Oath-managed signing key
  • Custody – every fingerprint/UDS/odometer is a Custody leaf hash-linked into the per-VIN Merkle dossier
  • Rotate – when a witness key is revoked, observations after the rotation epoch are flagged
  • SBOM-AI – manufacturer firmware manifests stream in as a fact source for calibration / SecurityAccess cross-checks
  • Sigstore Rekor – proofs are notarized as DSSE in-toto envelopes

Threat model + adversary

VIN privacy: VIN is hashed before signing – verifiers learn only same-vehicle linkage, never fleet roster. Centroid bootstrap requires 3 baseline fingerprints (MIN_CENTROID_SAMPLES) before judging. Compromised witness key invalidated by Rotate revocation post-epoch. Manufacturer trust: a manufacturer signing a fraudulent manifest is outside the threat model. Bus-attached attacker with a legitimate SecurityAccess key isn't caught – UDS-spoof catches keys not derivable from manifest policy.

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
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 →