README.md
13.5 KB · 246 lines · markdown Raw
1 # pqc-bootloader
2
3 [![PQC Native](https://img.shields.io/badge/PQC-Native-6f42c1)](https://csrc.nist.gov/projects/post-quantum-cryptography)
4 [![ML-DSA-65](https://img.shields.io/badge/Signature-ML--DSA--65-0a7)](https://csrc.nist.gov/pubs/fips/204/final)
5 [![SHA3-256](https://img.shields.io/badge/Hash-SHA3--256-0a7)](https://csrc.nist.gov/pubs/fips/202/final)
6 [![License: Apache-2.0](https://img.shields.io/badge/License-Apache--2.0-blue)](LICENSE)
7 [![Version](https://img.shields.io/badge/version-0.1.0-lightgrey)](pyproject.toml)
8
9 **PQC-native signed-boot framework for AI appliances.** Edge inference servers deployed in hospitals, factories, and military installations have 10-15 year operational lifespans. Firmware signed today with RSA-2048 or ECDSA-P256 is a Harvest-Now-Decrypt-Later target: a cryptographically relevant quantum computer (CRQC) in the 2030-2035 window can forge a signature on a malicious firmware image and push it into a fleet of appliances that still believe the original root-of-trust is valid. `pqc-bootloader` is a drop-in cryptographic layer that replaces `RSA_verify()` in your bootloader with `ML_DSA_verify()`, ships a manufacturer key-ring, enforces non-rollback via an update chain, and produces a TPM-style measured-boot PCR — so an appliance built today still has a defensible root-of-trust in 2040.
10
11 ## Install
12
13 ```bash
14 pip install pqc-bootloader
15 ```
16
17 ## Quick start
18
19 ```python
20 from quantumshield.identity.agent import AgentIdentity
21 from pqc_bootloader import (
22 FirmwareImage, FirmwareMetadata, TargetDevice,
23 FirmwareSigner, FirmwareVerifier,
24 KeyRing, MeasuredBoot, BootStage, BootAttestationLog,
25 )
26
27 # --- manufacturer ---
28 mfr = AgentIdentity.create("acme-appliance-vendor")
29 signer = FirmwareSigner(mfr)
30
31 metadata = FirmwareMetadata(
32 name="acme-inference-os", version="1.2.3",
33 target=TargetDevice.AI_INFERENCE_APPLIANCE,
34 )
35 firmware = FirmwareImage.from_bytes(metadata, open("firmware.bin", "rb").read())
36 signed = signer.sign(firmware)
37
38 # --- appliance ---
39 ring = KeyRing()
40 ring.add(mfr.signing_keypair.public_key.hex(),
41 mfr.signing_keypair.algorithm.value, "Acme Inc.")
42
43 result = FirmwareVerifier.verify(signed,
44 actual_bytes=firmware.image_bytes,
45 key_ring=ring)
46 assert result.valid
47
48 mb = MeasuredBoot()
49 mb.extend(BootStage.BOOTLOADER, open("/boot/bootloader.bin", "rb").read())
50 mb.extend(BootStage.KERNEL, open("/boot/vmlinuz", "rb").read())
51 mb.extend(BootStage.INITRD, open("/boot/initrd.img", "rb").read())
52
53 log = BootAttestationLog()
54 log.log_accept(firmware.metadata.name, firmware.metadata.version,
55 firmware.image_hash, pcr_value_after=mb.pcr_value)
56 ```
57
58 ## Architecture
59
60 ```
61 +------------------+ ML-DSA-65 sign +--------------------+
62 | Manufacturer |-------------------------------->| SignedFirmware |
63 | AgentIdentity | (metadata + SHA3-256 hash) | .to_dict() bytes |
64 +------------------+ +--------------------+
65 |
66 v
67 +----------------------------------+
68 | Distribution (OTA / USB / CDN) |
69 +----------------------------------+
70 |
71 v
72 +----------------+ reads +-----------+ check +-------------------+
73 | Boot ROM |------------>| KeyRing |<-----------| manufacturer_key_id|
74 | (U-Boot/GRUB | | allow-list| +-------------------+
75 | fork) | +-----------+ |
76 +----------------+ | |
77 v v
78 +------------------+ +-----------------------+
79 | FirmwareVerifier |<----| SHA3-256 hash recompute|
80 | (ML_DSA_verify) | +-----------------------+
81 +------------------+
82 |
83 +--------------+--------------+
84 | |
85 ACCEPT v v REJECT
86 +-------------------+ +-------------------+
87 | MeasuredBoot | | BootAttestationLog|
88 | PCR chain | | log_reject(...) |
89 | bootloader-kernel| | -> halt/fallback |
90 | -initrd-userspace| +-------------------+
91 +-------------------+
92 |
93 v
94 +-------------------+
95 | BootAttestationLog|
96 | log_accept(..., |
97 | pcr_value_after) |
98 +-------------------+
99 ```
100
101 ## Cryptography
102
103 | Primitive | Algorithm | Role |
104 | ------------- | ------------------------ | ------------------------------------ |
105 | Signature | ML-DSA-65 (FIPS 204) | Firmware manifest signatures |
106 | Hash | SHA3-256 (FIPS 202) | Firmware image hash + PCR extend |
107 | Identity | `quantumshield` DID | Manufacturer + device identifiers |
108 | Key fingerprint | SHA3-256 of public key | `manufacturer_key_id` in KeyRing |
109
110 The manufacturer signs a *canonical manifest* (JSON, sort_keys, no whitespace) covering the firmware metadata, SHA3-256 image hash, and image size — not the image bytes themselves. The bootloader recomputes the manifest from the delivered blob, then verifies the ML-DSA signature over that manifest. This means the signature is small and constant-size regardless of firmware size (which can be hundreds of MB for inference OSes with bundled model weights).
111
112 ## Threat model
113
114 | Threat | Mitigation |
115 | ------------------------------- | -------------------------------------------------------------------------- |
116 | **Firmware HNDL** | ML-DSA-65 signatures are quantum-safe; CRQC cannot forge them |
117 | **Rogue update** (signed by attacker with their own key) | `KeyRing` allow-list; untrusted `manufacturer_key_id` rejected |
118 | **Rollback attack** (legit older firmware re-deployed to re-introduce CVE) | `UpdateChain.add()` blocks when `new.version < prev.version` unless `allow_rollback=True` |
119 | **Stolen manufacturer key** | `KeyRing.revoke(key_id, reason)` marks entry; `is_trusted()` returns False. Rotate to a new manufacturer key and re-sign in-field firmware. |
120 | **Measured-boot tamper** | `MeasuredBoot.extend()` chains `SHA3(prev_pcr \|\| measurement)`; any swap of bootloader/kernel/initrd/userspace yields a different final PCR, detectable by remote attestation |
121 | **Image hash substitution** (manifest signed, but delivered image is different) | `FirmwareVerifier.verify(signed, actual_bytes=...)` recomputes SHA3-256 over the delivered blob and rejects on mismatch |
122 | **Manifest-only replay** (copy metadata from one device's firmware to another) | `target` + `min_hardware_revision` fields in `FirmwareMetadata` are part of the signed manifest |
123
124 ## Key-ring lifecycle
125
126 ```python
127 from pqc_bootloader import KeyRing
128
129 ring = KeyRing()
130
131 # 1. provisioning (at factory burn-in)
132 entry = ring.add(
133 public_key_hex=mfr_pubkey_hex,
134 algorithm="ML-DSA-65",
135 manufacturer="Acme Appliances Inc.",
136 role="firmware-signer",
137 )
138 ring.add(supplier_pubkey_hex, "ML-DSA-65", "Contoso Systems") # multi-vendor supply chain
139
140 # 2. check at boot
141 if ring.is_trusted(signed.manufacturer_key_id):
142 ...
143
144 # 3. revocation (e.g. HSM compromise disclosed)
145 ring.revoke(entry.key_id, reason="Acme HSM compromise CVE-2032-00001")
146
147 # 4. export for audit / mirroring
148 print(ring.export_json())
149 ```
150
151 The key-ring is designed to live in OTP / fuses or in a sealed TPM NV-index. Revocation entries persist; a revoked key is never re-trusted — rotate to a fresh key and re-sign in-field firmware instead.
152
153 ## Integration guide
154
155 `pqc-bootloader` is a cryptographic library. Real integration involves forking one of the classical signed-boot stacks:
156
157 | Stack | What to replace |
158 | ------------ | ------------------------------------------------------------------------ |
159 | **U-Boot** | `FIT_SIGNATURE_ALGO` hook: swap `rsa,sha256` for a custom `mldsa,sha3-256` that shells out to a small C binding around `FirmwareVerifier.verify`. Pin the manufacturer public key in `u-boot.dtb`. |
160 | **GRUB 2** | Replace `grub-pgp` verifier with a PQC verifier module; the `KeyRing` exports a GPG-compatible JSON that your module parses. |
161 | **coreboot** | Vboot v2: replace `RSA2048EXP3` kernel vboot key with an ML-DSA-65 key; update `firmware/2lib/2rsa.c` signature-verify call-site. |
162 | **UEFI Secure Boot** | Add ML-DSA-65 as an allowed signature algorithm in `db`; bootloader consumes `pqc-bootloader` output envelopes. |
163
164 In all cases the library gives you (a) the wire-format (`SignedFirmware.to_dict() / from_dict`), (b) the canonical manifest bytes (`FirmwareImage.canonical_manifest_bytes`), and (c) the cryptographic primitives (via `quantumshield.core.signatures.verify`). The bootloader-specific work is wiring these into the existing verify call-site.
165
166 ## API reference
167
168 ### `FirmwareMetadata`
169
170 Dataclass. `name`, `version`, `target` (`TargetDevice` enum), plus optional `kernel_version`, `architecture`, `build_id`, `release_notes_url`, `min_hardware_revision`, `security_level`.
171
172 ### `FirmwareImage`
173
174 - `FirmwareImage.from_bytes(metadata, data) -> FirmwareImage`
175 - `FirmwareImage.from_file(metadata, path) -> FirmwareImage`
176 - `FirmwareImage.hash_bytes(data) -> str` — SHA3-256 hex
177 - `firmware.canonical_manifest_bytes() -> bytes` — signed payload
178 - `firmware.to_dict(include_image=False) -> dict`
179
180 ### `FirmwareSigner`
181
182 - `FirmwareSigner(identity)` — construct from a `quantumshield.AgentIdentity`
183 - `signer.key_id -> str` — SHA3-256 of public key
184 - `signer.sign(firmware, previous_firmware_hash="") -> SignedFirmware`
185
186 ### `FirmwareVerifier`
187
188 - `FirmwareVerifier.verify(signed, actual_bytes=None, key_ring=None) -> VerificationResult`
189 - `actual_bytes`: if supplied, recomputes SHA3-256 and checks against `signed.firmware.image_hash`
190 - `key_ring`: if supplied, refuses untrusted `manufacturer_key_id`
191 - `FirmwareVerifier.verify_or_raise(...)` — same but raises `FirmwareVerificationError`
192
193 `VerificationResult` fields: `valid`, `signature_valid`, `hash_consistent`, `key_trusted`, `signer_did`, `firmware_name`, `error`.
194
195 ### `KeyRing`
196
197 - `ring.add(public_key_hex, algorithm, manufacturer, role="firmware-signer") -> KeyRingEntry`
198 - `ring.revoke(key_id, reason)`
199 - `ring.get(key_id) -> KeyRingEntry` (raises `UnknownKeyError`)
200 - `ring.is_trusted(key_id) -> bool`
201 - `ring.list_entries() -> list[KeyRingEntry]`
202 - `ring.export_json() -> str`
203 - `KeyRing.fingerprint(public_key_hex) -> str`
204
205 ### `UpdateChain`
206
207 - `chain.add(signed, allow_rollback=False)` — raises `UpdateChainError` / `FirmwareRollbackError`
208 - `chain.current() -> SignedFirmware | None`
209 - `chain.verify_chain() -> tuple[bool, list[str]]`
210
211 ### `MeasuredBoot`
212
213 - `mb.extend(stage, content) -> str` — returns new PCR hex
214 - `mb.reset()`
215 - `mb.pcr_value: str` / `mb.measurements: list[PCRMeasurement]`
216
217 `BootStage` enum: `ROM | BOOTLOADER | KERNEL | INITRD | USERSPACE | MODEL_WEIGHTS`.
218
219 ### `BootAttestationLog`
220
221 - `log.log_accept(firmware_name, firmware_version, firmware_hash, reason="", device_id="", pcr_value_after="")`
222 - `log.log_reject(firmware_name, firmware_version, firmware_hash, reason, device_id="")`
223 - `log.entries(limit=100, decision=None) -> list[BootAttemptEntry]`
224 - `log.export_json() -> str`
225
226 ### Exceptions
227
228 `BootloaderError` > `{FirmwareVerificationError, UnknownKeyError, UpdateChainError, MeasuredBootError, KeyRingError}`, plus `FirmwareRollbackError(UpdateChainError)`.
229
230 ## Why PQC for bootloaders
231
232 - **Deployment lifespan vs. quantum timeline.** NIST expects ML-DSA migration mandatory for federal signed firmware by 2030-2033. Medical imaging systems, factory PLCs, and military embedded platforms built today will still be in service in 2038-2040. Signing those with RSA/ECDSA is a shipped-in-vault HNDL target.
233 - **One-shot root of trust.** Unlike TLS, bootloader keys usually can't be rotated over the air — they're burned into fuses. A bootloader signed with a classical key you can't rotate is a permanent liability.
234 - **Supply-chain blast radius.** A forged firmware signature doesn't compromise one session; it owns the device for its operational life. An adversary harvesting today's signed update and forging it at Q-day can replace the kernel on every deployed unit at once.
235 - **Measured boot is orthogonal to signing.** Even with PQC signatures, an attacker who tampers with the kernel after verify-and-load is caught by the PCR chain — which remote attestation consumers (RA verifiers, TEE attestors) can validate.
236
237 ## Examples
238
239 - [`examples/sign_and_boot.py`](examples/sign_and_boot.py) — end-to-end factory sign -> appliance boot -> measured PCR -> audit accept
240 - [`examples/rogue_firmware_rejected.py`](examples/rogue_firmware_rejected.py) — attacker-signed firmware rejected by key-ring
241 - [`examples/update_rollback_blocked.py`](examples/update_rollback_blocked.py) — `UpdateChain` blocks v1.0 -> v0.9
242
243 ## License
244
245 Apache-2.0. See [LICENSE](LICENSE).
246