- Docs
- Bureau — Blue Team (defensive)
- Ignition
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:
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):
// 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 = 3baseline 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
| URI | What it attests |
|---|---|
https://pluck.run/Ignition.Ecu/v1 | One 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/v1 | One 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/v1 | One witness-signed odometer reading – kilometers, source, optional cluster-side signature, VIN-hash. |
https://pluck.run/Ignition.Tamper/v1 | Signed 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
pluck bureau verify <bundle-dir>
cosign verify-attestation \
--certificate-identity-regexp '.*' \
--certificate-oidc-issuer-regexp '.*' \
<envelope-hash>.intoto.jsonl
See also
- Bureau Foundations
- Threat Model
- Verify a dossier
- Oath – operator/witness key management
- Rotate – key revocation ledger
- Custody – chain-of-custody Merkle dossiers
- SBOM-AI – signed firmware/software bill-of-materials feed
- Cosmos – sibling centroid-based fingerprint Bureau (RF, not CAN)