README.md
| 1 | # PQC Hypervisor Attestation |
| 2 | |
| 3 |  |
| 4 |  |
| 5 |  |
| 6 |  |
| 7 |  |
| 8 | |
| 9 | **Sigstore for hypervisor memory attestation.** When an AI workload runs inside a cloud VM, the hypervisor (KVM, QEMU, Hyper-V) is transparent: nothing cryptographically proves to the tenant that model weights in RAM have not been read or rewritten by a malicious host. This library is the **post-quantum cryptographic envelope** — `MemoryRegion`, `AttestationClaim`, `AttestationReport`, ML-DSA-signed, independently verifiable — that a hypervisor project, TEE runtime, or confidential-computing framework can plug its memory-reading primitives into. The library ships pluggable backends for **AMD SEV-SNP** and **Intel TDX** as stubs, plus a reference `InMemoryBackend` for tests. |
| 10 | |
| 11 | ## The Problem |
| 12 | |
| 13 | Cloud AI inference workloads place multi-hundred-megabyte model weights in guest memory. The hypervisor can read or rewrite every page. Today's tenants rely on contractual trust ("the cloud provider said they wouldn't") plus coarse platform-level attestation (MRTD, launch digest) that only covers VM boot — not the *runtime* state of the pages you actually care about. An attacker with host compromise, or a curious operator, can: |
| 14 | |
| 15 | - Silently exfiltrate proprietary model weights. |
| 16 | - Swap in backdoored weights for a single request. |
| 17 | - Corrupt the KV cache to influence in-flight generations. |
| 18 | |
| 19 | None of this is visible to the guest without a runtime memory attestation protocol. And any attestation that signs memory state with RSA or ECDSA today is retroactively forgeable once a CRQC exists — which is why the cryptographic envelope must be post-quantum from day one. |
| 20 | |
| 21 | ## The Solution |
| 22 | |
| 23 | Every attestation is a post-quantum signed, freshness-bounded claim about specific memory regions: |
| 24 | |
| 25 | - **SHA3-256** per-region content hashes — deterministic snapshot of what lives at those pages. |
| 26 | - **ML-DSA (FIPS 204)** signature over the canonical report bytes, by a known attester DID. |
| 27 | - **Expiry and nonce** for replay resistance. |
| 28 | - **Expected-hash pinning** so a remote verifier can detect drift from trusted state. |
| 29 | - **Pluggable backends** — the library never reads memory itself; backends do, using `/dev/sev-guest`, `/dev/tdx-guest`, or whatever primitive the real TEE exposes. |
| 30 | |
| 31 | ## Installation |
| 32 | |
| 33 | ```bash |
| 34 | pip install pqc-hypervisor-attestation |
| 35 | ``` |
| 36 | |
| 37 | Development: |
| 38 | |
| 39 | ```bash |
| 40 | pip install -e ".[dev]" |
| 41 | ``` |
| 42 | |
| 43 | ## Quick Start |
| 44 | |
| 45 | ```python |
| 46 | from quantumshield.identity.agent import AgentIdentity |
| 47 | |
| 48 | from pqc_hypervisor_attestation import ( |
| 49 | AttestationVerifier, |
| 50 | Attester, |
| 51 | ContinuousAttester, |
| 52 | InMemoryBackend, |
| 53 | MemoryRegion, |
| 54 | RegionSnapshot, |
| 55 | ) |
| 56 | |
| 57 | # 1. Identity + signer. |
| 58 | identity = AgentIdentity.create("llama-host-attester", capabilities=["attest"]) |
| 59 | attester = Attester(identity) |
| 60 | |
| 61 | # 2. Backend with a pinned region. |
| 62 | backend = InMemoryBackend() |
| 63 | weights = MemoryRegion( |
| 64 | region_id="model-weights-0", |
| 65 | description="Llama weight shard 0", |
| 66 | address=0x1000, |
| 67 | size=128, |
| 68 | protection="RO", |
| 69 | ) |
| 70 | content = b"\xaa" * 128 |
| 71 | backend.register("model-serving-1", weights, content) |
| 72 | |
| 73 | # 3. Continuous attester with pinned expected hash. |
| 74 | loop = ContinuousAttester( |
| 75 | attester=attester, |
| 76 | backend=backend, |
| 77 | workload_id="model-serving-1", |
| 78 | expected_hashes={weights.region_id: RegionSnapshot.hash_bytes(content)}, |
| 79 | ) |
| 80 | |
| 81 | report = loop.attest_once() |
| 82 | result = AttestationVerifier.verify(report, strict=True) |
| 83 | assert result.valid |
| 84 | ``` |
| 85 | |
| 86 | ## Architecture |
| 87 | |
| 88 | ``` |
| 89 | +-----------------------------+ +------------------------------+ |
| 90 | | AI workload (guest VM) | | Remote verifier | |
| 91 | | | | | |
| 92 | | ContinuousAttester | | AttestationVerifier | |
| 93 | | | | | | | |
| 94 | | v | | v | |
| 95 | | AttestationBackend | | - ML-DSA signature check | |
| 96 | | (SEV-SNP | TDX | memory) | | - expiry check | |
| 97 | | | | | - expected_hash pinning | |
| 98 | | v | | | |
| 99 | | MemoryRegion ---> RegionSnapshot -> AttestationClaim | |
| 100 | | | | |
| 101 | | v | |
| 102 | | AttestationReport (bundle) | |
| 103 | | | | |
| 104 | | v | |
| 105 | | ML-DSA sign (quantumshield) | |
| 106 | +-----------------------------+ +------------------------------+ |
| 107 | | ^ |
| 108 | | signed AttestationReport | |
| 109 | +---------------------------------+ |
| 110 | ``` |
| 111 | |
| 112 | ## Cryptography |
| 113 | |
| 114 | | Primitive | Purpose | Algorithm | |
| 115 | | ------------------------- | ---------------------------------------- | ------------- | |
| 116 | | Region content hash | Fingerprint memory bytes | SHA3-256 | |
| 117 | | Report canonical digest | Input to signer | SHA3-256 | |
| 118 | | Attestation signature | Bind report to attester DID | ML-DSA-65 | |
| 119 | | Verifier trust anchor | Attester public key (from DID / keystore)| ML-DSA public | |
| 120 | |
| 121 | All signing is delegated to [`quantumshield`](https://github.com/dyber-pqc/quantumshield), which prefers real `liboqs` ML-DSA when available and falls back to transitional Ed25519. |
| 122 | |
| 123 | ## Threat Model |
| 124 | |
| 125 | | Adversary capability | Coverage | |
| 126 | | ------------------------------------------------- | ---------------------------------------------------------------- | |
| 127 | | Forges an attestation report from scratch | Blocked — requires the attester's ML-DSA private key. | |
| 128 | | Replays an old valid report to hide drift | Blocked — `expires_at` bound + per-report nonce in each claim. | |
| 129 | | Rewrites model weights in guest memory | Detected — region snapshot hash diverges from `expected_hash`. | |
| 130 | | Substitutes a newer signed report from same key | Mitigated — verifier pins expected hashes independently. | |
| 131 | | Tampers with report fields in transit | Blocked — canonical-bytes + ML-DSA binds every field. | |
| 132 | | Q-day adversary with CRQC | Out of scope for ECDSA/RSA signers; covered here by ML-DSA. | |
| 133 | | Compromises the attester key via guest escape | Out of scope — mitigate with SEV-SNP / TDX sealed key storage. | |
| 134 | |
| 135 | ## Backend Integration Guide |
| 136 | |
| 137 | The library defines a single abstract interface: |
| 138 | |
| 139 | ```python |
| 140 | class AttestationBackend(ABC): |
| 141 | name: str |
| 142 | platform: str |
| 143 | |
| 144 | def list_regions(self, workload_id: str) -> list[MemoryRegion]: ... |
| 145 | def snapshot(self, region: MemoryRegion) -> RegionSnapshot: ... |
| 146 | ``` |
| 147 | |
| 148 | Any backend that can enumerate memory ranges and hash their bytes can plug in. |
| 149 | |
| 150 | ### AMD SEV-SNP (`/dev/sev-guest`) |
| 151 | |
| 152 | The shipped `AMDSEVSNPBackend` is a stub that documents the expected behaviour. A real integration: |
| 153 | |
| 154 | 1. Reads the SEV-SNP launch digest at VM start and stores per-region base/size in its workload manifest. |
| 155 | 2. For `list_regions`, returns `MemoryRegion` entries whose `address` is the guest-physical base and `size` is the range length. |
| 156 | 3. For `snapshot`, issues the `SNP_GET_REPORT` ioctl on `/dev/sev-guest`, reads the backing pages into a buffer, and returns `RegionSnapshot.create(region_id, buffer)`. |
| 157 | |
| 158 | ### Intel TDX (`/dev/tdx-guest`) |
| 159 | |
| 160 | `IntelTDXBackend` follows the same pattern using `TDX_CMD_GET_REPORT0` and the TD's MRTD / RTMR measurements. |
| 161 | |
| 162 | ### Your own backend |
| 163 | |
| 164 | Subclass `AttestationBackend`, implement `list_regions` and `snapshot`, and pass an instance to `ContinuousAttester`. The library handles canonicalisation, signing, and verification uniformly. |
| 165 | |
| 166 | ## API Reference |
| 167 | |
| 168 | ### Data types |
| 169 | |
| 170 | | Class | Description | |
| 171 | | ------------------ | ----------------------------------------------------------------- | |
| 172 | | `MemoryRegion` | Addressable range `(region_id, address, size, protection)`. | |
| 173 | | `RegionSnapshot` | `(region_id, content_hash, size, taken_at)` SHA3-256 fingerprint. | |
| 174 | | `AttestationClaim` | One signed statement about one region at one point in time. | |
| 175 | | `AttestationReport`| Bundle of claims + ML-DSA envelope + expiry. | |
| 176 | | `VerificationResult`| Breakdown of signature / expiry / drift checks. | |
| 177 | |
| 178 | ### Signers and verifiers |
| 179 | |
| 180 | | Symbol | Purpose | |
| 181 | | ------------------------ | ----------------------------------------------------------- | |
| 182 | | `Attester` | Wraps an `AgentIdentity`; signs reports. | |
| 183 | | `AttestationVerifier` | Static verifier; returns `VerificationResult`. | |
| 184 | | `AttestationVerifier.verify_or_raise` | Raises `RegionDriftError` / `AttestationVerificationError`. | |
| 185 | | `ContinuousAttester` | Periodic loop: enumerate regions, snapshot, sign. | |
| 186 | |
| 187 | ### Backends |
| 188 | |
| 189 | | Class | Use | |
| 190 | | ------------------ | --------------------------------------------------------- | |
| 191 | | `InMemoryBackend` | Reference / tests / tutorials. | |
| 192 | | `AMDSEVSNPBackend` | Stub for AMD SEV-SNP (plug into `/dev/sev-guest`). | |
| 193 | | `IntelTDXBackend` | Stub for Intel TDX (plug into `/dev/tdx-guest`). | |
| 194 | |
| 195 | ### Exceptions |
| 196 | |
| 197 | `HypervisorAttestationError` -> `InvalidRegionError`, `AttestationVerificationError` (-> `RegionDriftError`), `BackendError` (-> `UnknownBackendError`). |
| 198 | |
| 199 | ## Why PQC Matters for Hypervisor Attestation |
| 200 | |
| 201 | Cloud confidential-computing deployments live for a decade or more. A VM image spun up in 2026 may still be attested by the same kind of signature in 2036 — long enough for a CRQC to exist. An attestation signed today with ECDSA or RSA can be **retroactively forged** by any adversary holding a recording of the public key and the signed payload: in 2035 they can manufacture a fresh, valid-looking "clean" attestation for a workload they actually tampered with in 2026, and no auditor can distinguish it from a real one. Post-quantum signatures break that chain: even with Shor's algorithm, ML-DSA remains unbroken, so historical attestations continue to bind the host to its claim. Hypervisor attestation is exactly the place that non-repudiation must survive the cryptographic transition. |
| 202 | |
| 203 | ## Examples |
| 204 | |
| 205 | * [`examples/basic_attestation.py`](examples/basic_attestation.py) — end-to-end sign-and-verify over two in-memory regions. |
| 206 | * [`examples/detect_memory_tampering.py`](examples/detect_memory_tampering.py) — mutate a region between attestations and watch drift surface. |
| 207 | * [`examples/continuous_loop_demo.py`](examples/continuous_loop_demo.py) — run the periodic attestation loop and stream reports. |
| 208 | |
| 209 | ## License |
| 210 | |
| 211 | Apache 2.0. See [LICENSE](LICENSE). |
| 212 | |