pqc-bootloader
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.
Install
pip install pqc-bootloader
Quick start
from quantumshield.identity.agent import AgentIdentity
from pqc_bootloader import (
FirmwareImage, FirmwareMetadata, TargetDevice,
FirmwareSigner, FirmwareVerifier,
KeyRing, MeasuredBoot, BootStage, BootAttestationLog,
)
# --- manufacturer ---
mfr = AgentIdentity.create("acme-appliance-vendor")
signer = FirmwareSigner(mfr)
metadata = FirmwareMetadata(
name="acme-inference-os", version="1.2.3",
target=TargetDevice.AI_INFERENCE_APPLIANCE,
)
firmware = FirmwareImage.from_bytes(metadata, open("firmware.bin", "rb").read())
signed = signer.sign(firmware)
# --- appliance ---
ring = KeyRing()
ring.add(mfr.signing_keypair.public_key.hex(),
mfr.signing_keypair.algorithm.value, "Acme Inc.")
result = FirmwareVerifier.verify(signed,
actual_bytes=firmware.image_bytes,
key_ring=ring)
assert result.valid
mb = MeasuredBoot()
mb.extend(BootStage.BOOTLOADER, open("/boot/bootloader.bin", "rb").read())
mb.extend(BootStage.KERNEL, open("/boot/vmlinuz", "rb").read())
mb.extend(BootStage.INITRD, open("/boot/initrd.img", "rb").read())
log = BootAttestationLog()
log.log_accept(firmware.metadata.name, firmware.metadata.version,
firmware.image_hash, pcr_value_after=mb.pcr_value)
Architecture
+------------------+ ML-DSA-65 sign +--------------------+
| Manufacturer |-------------------------------->| SignedFirmware |
| AgentIdentity | (metadata + SHA3-256 hash) | .to_dict() bytes |
+------------------+ +--------------------+
|
v
+----------------------------------+
| Distribution (OTA / USB / CDN) |
+----------------------------------+
|
v
+----------------+ reads +-----------+ check +-------------------+
| Boot ROM |------------>| KeyRing |<-----------| manufacturer_key_id|
| (U-Boot/GRUB | | allow-list| +-------------------+
| fork) | +-----------+ |
+----------------+ | |
v v
+------------------+ +-----------------------+
| FirmwareVerifier |<----| SHA3-256 hash recompute|
| (ML_DSA_verify) | +-----------------------+
+------------------+
|
+--------------+--------------+
| |
ACCEPT v v REJECT
+-------------------+ +-------------------+
| MeasuredBoot | | BootAttestationLog|
| PCR chain | | log_reject(...) |
| bootloader-kernel| | -> halt/fallback |
| -initrd-userspace| +-------------------+
+-------------------+
|
v
+-------------------+
| BootAttestationLog|
| log_accept(..., |
| pcr_value_after) |
+-------------------+
Cryptography
| Primitive | Algorithm | Role |
|---|---|---|
| Signature | ML-DSA-65 (FIPS 204) | Firmware manifest signatures |
| Hash | SHA3-256 (FIPS 202) | Firmware image hash + PCR extend |
| Identity | quantumshield DID |
Manufacturer + device identifiers |
| Key fingerprint | SHA3-256 of public key | manufacturer_key_id in KeyRing |
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).
Threat model
| Threat | Mitigation |
|---|---|
| Firmware HNDL | ML-DSA-65 signatures are quantum-safe; CRQC cannot forge them |
| Rogue update (signed by attacker with their own key) | KeyRing allow-list; untrusted manufacturer_key_id rejected |
| Rollback attack (legit older firmware re-deployed to re-introduce CVE) | UpdateChain.add() blocks when new.version < prev.version unless allow_rollback=True |
| 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. |
| 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 |
| 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 |
| 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 |
Key-ring lifecycle
from pqc_bootloader import KeyRing
ring = KeyRing()
# 1. provisioning (at factory burn-in)
entry = ring.add(
public_key_hex=mfr_pubkey_hex,
algorithm="ML-DSA-65",
manufacturer="Acme Appliances Inc.",
role="firmware-signer",
)
ring.add(supplier_pubkey_hex, "ML-DSA-65", "Contoso Systems") # multi-vendor supply chain
# 2. check at boot
if ring.is_trusted(signed.manufacturer_key_id):
...
# 3. revocation (e.g. HSM compromise disclosed)
ring.revoke(entry.key_id, reason="Acme HSM compromise CVE-2032-00001")
# 4. export for audit / mirroring
print(ring.export_json())
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.
Integration guide
pqc-bootloader is a cryptographic library. Real integration involves forking one of the classical signed-boot stacks:
| Stack | What to replace |
|---|---|
| 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. |
| GRUB 2 | Replace grub-pgp verifier with a PQC verifier module; the KeyRing exports a GPG-compatible JSON that your module parses. |
| coreboot | Vboot v2: replace RSA2048EXP3 kernel vboot key with an ML-DSA-65 key; update firmware/2lib/2rsa.c signature-verify call-site. |
| UEFI Secure Boot | Add ML-DSA-65 as an allowed signature algorithm in db; bootloader consumes pqc-bootloader output envelopes. |
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.
API reference
FirmwareMetadata
Dataclass. name, version, target (TargetDevice enum), plus optional kernel_version, architecture, build_id, release_notes_url, min_hardware_revision, security_level.
FirmwareImage
FirmwareImage.from_bytes(metadata, data) -> FirmwareImageFirmwareImage.from_file(metadata, path) -> FirmwareImageFirmwareImage.hash_bytes(data) -> str— SHA3-256 hexfirmware.canonical_manifest_bytes() -> bytes— signed payloadfirmware.to_dict(include_image=False) -> dict
FirmwareSigner
FirmwareSigner(identity)— construct from aquantumshield.AgentIdentitysigner.key_id -> str— SHA3-256 of public keysigner.sign(firmware, previous_firmware_hash="") -> SignedFirmware
FirmwareVerifier
FirmwareVerifier.verify(signed, actual_bytes=None, key_ring=None) -> VerificationResultactual_bytes: if supplied, recomputes SHA3-256 and checks againstsigned.firmware.image_hashkey_ring: if supplied, refuses untrustedmanufacturer_key_id
FirmwareVerifier.verify_or_raise(...)— same but raisesFirmwareVerificationError
VerificationResult fields: valid, signature_valid, hash_consistent, key_trusted, signer_did, firmware_name, error.
KeyRing
ring.add(public_key_hex, algorithm, manufacturer, role="firmware-signer") -> KeyRingEntryring.revoke(key_id, reason)ring.get(key_id) -> KeyRingEntry(raisesUnknownKeyError)ring.is_trusted(key_id) -> boolring.list_entries() -> list[KeyRingEntry]ring.export_json() -> strKeyRing.fingerprint(public_key_hex) -> str
UpdateChain
chain.add(signed, allow_rollback=False)— raisesUpdateChainError/FirmwareRollbackErrorchain.current() -> SignedFirmware | Nonechain.verify_chain() -> tuple[bool, list[str]]
MeasuredBoot
mb.extend(stage, content) -> str— returns new PCR hexmb.reset()mb.pcr_value: str/mb.measurements: list[PCRMeasurement]
BootStage enum: ROM | BOOTLOADER | KERNEL | INITRD | USERSPACE | MODEL_WEIGHTS.
BootAttestationLog
log.log_accept(firmware_name, firmware_version, firmware_hash, reason="", device_id="", pcr_value_after="")log.log_reject(firmware_name, firmware_version, firmware_hash, reason, device_id="")log.entries(limit=100, decision=None) -> list[BootAttemptEntry]log.export_json() -> str
Exceptions
BootloaderError > {FirmwareVerificationError, UnknownKeyError, UpdateChainError, MeasuredBootError, KeyRingError}, plus FirmwareRollbackError(UpdateChainError).
Why PQC for bootloaders
- 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.
- 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.
- 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.
- 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.
Examples
examples/sign_and_boot.py— end-to-end factory sign -> appliance boot -> measured PCR -> audit acceptexamples/rogue_firmware_rejected.py— attacker-signed firmware rejected by key-ringexamples/update_rollback_blocked.py—UpdateChainblocks v1.0 -> v0.9
License
Apache-2.0. See LICENSE.
Release files for pqc-bootloader 0.1.0
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | |
|---|---|---|---|
| pqc_bootloader-0.1.0.tar.gz | 20.6 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| pqc_bootloader-0.1.0-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 39.3 kB
Release files / pqc_bootloader-0.1.0.tar.gz
| Download URL | pqc_bootloader-0.1.0.tar.gz |
|---|---|
| Size | 20.6 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
6ed1aec404aee9698912fe7181cf0efdebb44adbcf1698663db90aa4cda19784
|
|
BLAKE2b-256 checksum How to use checksums |
2a6c3f9c440d01d233af007a7d7e01720cff286ee670e3e15349fb1e08201ce3
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/6.2.0 CPython/3.11.15
|
Release files / pqc_bootloader-0.1.0-py3-none-any.whl
| Download URL | pqc_bootloader-0.1.0-py3-none-any.whl |
|---|---|
| Size | 18.7 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
9e6f58026d55a5a3053f90833af7e45b63a9e6c3b8f1996e57ee65b79f5e1dbc
|
|
BLAKE2b-256 checksum How to use checksums |
6861ae8709ed026e5b78441405ce1719ed1061d3966e1db68f4812adf3fd42a8
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/6.2.0 CPython/3.11.15
|