PQC-native signed-boot framework for AI appliances. ML-DSA-65 firmware signatures, measured-boot PCR chains, update-chain verification, manufacturer key-ring.
Project description
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.
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file pqc_bootloader-0.1.0.tar.gz.
File metadata
- Download URL: pqc_bootloader-0.1.0.tar.gz
- Upload date:
- Size: 20.6 kB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.2.0 CPython/3.11.15
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
6ed1aec404aee9698912fe7181cf0efdebb44adbcf1698663db90aa4cda19784
|
|
| MD5 |
de0030534fb0df9f87df9945157ec207
|
|
| BLAKE2b-256 |
2a6c3f9c440d01d233af007a7d7e01720cff286ee670e3e15349fb1e08201ce3
|
File details
Details for the file pqc_bootloader-0.1.0-py3-none-any.whl.
File metadata
- Download URL: pqc_bootloader-0.1.0-py3-none-any.whl
- Upload date:
- Size: 18.7 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.2.0 CPython/3.11.15
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
9e6f58026d55a5a3053f90833af7e45b63a9e6c3b8f1996e57ee65b79f5e1dbc
|
|
| MD5 |
807f614152408ebe4ca1b0aefb6aa4cb
|
|
| BLAKE2b-256 |
6861ae8709ed026e5b78441405ce1719ed1061d3966e1db68f4812adf3fd42a8
|