README.md
| 1 | # pqc-bootloader |
| 2 | |
| 3 | [](https://csrc.nist.gov/projects/post-quantum-cryptography) |
| 4 | [](https://csrc.nist.gov/pubs/fips/204/final) |
| 5 | [](https://csrc.nist.gov/pubs/fips/202/final) |
| 6 | [](LICENSE) |
| 7 | [](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 | |