Skip to main content

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 ML-DSA-65 SHA3-256 License: Apache-2.0 Version

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) -> FirmwareImage
  • FirmwareImage.from_file(metadata, path) -> FirmwareImage
  • FirmwareImage.hash_bytes(data) -> str — SHA3-256 hex
  • firmware.canonical_manifest_bytes() -> bytes — signed payload
  • firmware.to_dict(include_image=False) -> dict

FirmwareSigner

  • FirmwareSigner(identity) — construct from a quantumshield.AgentIdentity
  • signer.key_id -> str — SHA3-256 of public key
  • signer.sign(firmware, previous_firmware_hash="") -> SignedFirmware

FirmwareVerifier

  • FirmwareVerifier.verify(signed, actual_bytes=None, key_ring=None) -> VerificationResult
    • actual_bytes: if supplied, recomputes SHA3-256 and checks against signed.firmware.image_hash
    • key_ring: if supplied, refuses untrusted manufacturer_key_id
  • FirmwareVerifier.verify_or_raise(...) — same but raises FirmwareVerificationError

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") -> KeyRingEntry
  • ring.revoke(key_id, reason)
  • ring.get(key_id) -> KeyRingEntry (raises UnknownKeyError)
  • ring.is_trusted(key_id) -> bool
  • ring.list_entries() -> list[KeyRingEntry]
  • ring.export_json() -> str
  • KeyRing.fingerprint(public_key_hex) -> str

UpdateChain

  • chain.add(signed, allow_rollback=False) — raises UpdateChainError / FirmwareRollbackError
  • chain.current() -> SignedFirmware | None
  • chain.verify_chain() -> tuple[bool, list[str]]

MeasuredBoot

  • mb.extend(stage, content) -> str — returns new PCR hex
  • mb.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

License

Apache-2.0. See LICENSE.

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

pqc_bootloader-0.1.0.tar.gz (20.6 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

pqc_bootloader-0.1.0-py3-none-any.whl (18.7 kB view details)

Uploaded Python 3

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

Hashes for pqc_bootloader-0.1.0.tar.gz
Algorithm Hash digest
SHA256 6ed1aec404aee9698912fe7181cf0efdebb44adbcf1698663db90aa4cda19784
MD5 de0030534fb0df9f87df9945157ec207
BLAKE2b-256 2a6c3f9c440d01d233af007a7d7e01720cff286ee670e3e15349fb1e08201ce3

See more details on using hashes here.

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

Hashes for pqc_bootloader-0.1.0-py3-none-any.whl
Algorithm Hash digest
SHA256 9e6f58026d55a5a3053f90833af7e45b63a9e6c3b8f1996e57ee65b79f5e1dbc
MD5 807f614152408ebe4ca1b0aefb6aa4cb
BLAKE2b-256 6861ae8709ed026e5b78441405ce1719ed1061d3966e1db68f4812adf3fd42a8

See more details on using hashes here.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page