Skip to content

vtpm: RFC — persistent vTPM state via a pluggable external TPM endpoint#1091

Draft
Goodleon-Y wants to merge 8 commits into
coconut-svsm:mainfrom
Goodleon-Y:vtpm/tpm-sealing
Draft

vtpm: RFC — persistent vTPM state via a pluggable external TPM endpoint#1091
Goodleon-Y wants to merge 8 commits into
coconut-svsm:mainfrom
Goodleon-Y:vtpm/tpm-sealing

Conversation

@Goodleon-Y

@Goodleon-Y Goodleon-Y commented May 25, 2026

Copy link
Copy Markdown

[Draft PR — RFC for design feedback, not requesting merge.]

Background

SEV-SNP does not currently provide trusted access from a CVM to
platform devices, including a physical TPM (discrete, firmware, or
otherwise). As a result, the in-CVM vTPM at VMPL0 cannot use a
physical TPM as a backing root of trust and cannot cross a cold boot
on its own. Today, the upstream path being developed by Stefano
Garzarella and others addresses this with encrypted-block + KBS:
durable storage for the vTPM blob plus a remote attestation-gated
key release for decryption. That path fits cloud deployments where
a KBS is already part of the trust infrastructure.

This RFC explores a complementary path for deployments where an
online KBS is unavailable, not a fit for the deployment model, or
overkill for the workload:

  • on-prem / private cloud where a TPM is already the platform root of
    trust
  • disconnected, long-running, or independent-audit workloads
  • bring-up / development environments where setting up a KBS is
    disproportionate to the task

The approach: keep the vTPM in-CVM at VMPL0 (no change to today's
attestation surface), but route only its persistent state through a
seal/unseal cycle against an external TPM 2.0 endpoint reached
over an adversarial host channel. The endpoint is intentionally
abstract: a TPM 2.0 command/response pair is all the proxy assumes,
so the same code path can target:

  • a software vTPM (swtpm) for development and CI
  • a host-side firmware TPM (AMD-SP fTPM, Intel PTT) where one already
    exists on the platform
  • a physical discrete TPM for deployments that want a hardware-rooted
    persistence anchor maintained outside the CVM
  • a TPM-as-a-service endpoint where the operator wants centralized
    key custody without a full KBS stack

The choice is a deployment decision; the SVSM code is the same.

This is not a replacement for the KBS-stateful vTPM work. The two
approaches target different deployment regimes and can coexist on the
same SVSM build via different backends behind the SealedBlobStore
trait introduced here.

What this RFC adds

  • libtcgtpm (d62abbb): getter/setter accessors for TPM 2.0
    reference globals (gp, gc, gr) plus a bulk serialize/deserialize
    pair. Useful for any path that needs to lift the reference
    implementation's persistent state out of the in-CVM binary, not just
    this RFC.
  • vtpm/proxy.rs (48abdc3, 6c08cd2): a small TpmTransport
    trait with a MockTransport for unit tests and a VSOCK
    implementation (feature-gated). The TPM2 command builder
    (build_get_random, build_nv_define_space, build_nv_increment,
    build_nv_read, build_create_primary, build_create, build_load,
    build_seal, build_unseal, build_flush_context) is generic over
    the transport, so the proxy makes no assumption about which TPM 2.0
    endpoint is on the other side.
  • vtpm/sealed_store.rs (0bdc9df): a SealedBlobStore trait with
    StaticBufStore (in-memory, test-only), a stubbed IgvmVarStore,
    and a VSOCK-backed VsockHostStore (feature-gated). Intent is to
    align with the upcoming block-layer abstraction so the same trait
    can later wrap a block device, and to leave room for a KBS-backed
    implementation to drop in as a peer backend.
  • vtpm/sealed.rs (72f12ef): two-layer container.
    AES-256-GCM-encrypt the state payload (~1 KB at Tier-1.5, growing
    to ~19 KB once Tier-2 NV-block persistence is enabled), then
    TPM2_Seal only the 60-byte key bundle
    (aes_key[32] || gcm_tag[16] || nonce[12]). Keeps the TPM-sealed
    payload well under RSA-2048 size limits regardless of the
    underlying state size. The blob format is documented in
    kernel/src/vtpm/sealed.rs and is versioned. The current revision
    carries only the AES-256-GCM payload and the TPM-sealed key bundle;
    a counter-binding field for cross-tenant replay isolation on a
    shared NV counter backend is a planned format addition, not part of
    this revision (see open questions 5 and 8).
  • vtpm/state.rs (143ab03): Rust wrappers around the new
    libtcgtpm accessors, plus a bulk serialize/deserialize path that
    matches the C-side layout.
  • vtpm boot integration (6f5db7b): two boot modes plumbed
    through kernel/src/svsm.rs:
    • Provision — manufacture vTPM, extract state, seal via the
      configured TPM endpoint, persist the blob.
    • Recover — load the blob, unseal via the configured TPM endpoint,
      inject state, signal NV-on + light power-on.
  • vtpm/reseal.rs + Tier-2 persistence (b1fbbd1): full
    guest-state persistence path covering DA lockout, PCR allocation,
    orderly-shutdown state, and the platform NV memory (TPM reference
    implementation's flat 16 KB s_NV[] buffer that backs every user NV
    index, evict object, and DA bookkeeping field). The libtcgtpm
    accessors gain section 0x1A (s_NV dump); the SVSM kernel gains
    kernel/src/vtpm/reseal.rs registering a runtime hook in the
    command path that, on a successful guest-side
    TPM2_Shutdown(SU_STATE), re-extracts internal state, freshly
    re-encrypts under a new AES key + nonce (no key material is cached
    across calls), and pushes a new SealedBlob via VsockHostStore.
    Sealed blob grows from ~2.7 KB (Tier-1.5) to ~19 KB (Tier-2). The
    hook is feature-gated behind vtpm-persist; the previous Tier-1.5
    build path is unchanged when the feature is off.

Known limitations / open questions

  1. Storage backend: ships with StaticBufStore (test-only) and
    stubbed IgvmVarStore. Production needs either an IGVM-variable
    path, integration with the upcoming SVSM block layer, or a
    KBS-backed implementation. I deferred picking one so the trait
    shape can be reviewed first.
  2. vm_id: currently zero in the boot wiring; should derive from
    IGVM guest_context once that surface stabilizes.
  3. Transport: VSOCK implementation is feature-gated; the
    TpmTransport trait is intentionally minimal so alternative
    transports (virtio-serial, MMIO, in-process for swtpm co-tenants,
    etc.) are welcome.
  4. State extraction: reaches into the Microsoft TPM reference
    globals via the libtcgtpm accessors added in commit 1. A
    TPM2_ContextSave-based path was considered but cannot reach
    hierarchy seeds and auth values by design. Would appreciate
    guidance on whether the maintainers prefer the direct-accessor
    approach or want a higher-level abstraction.
  5. HMAC channel auth: the transport layer assumes an adversarial
    host channel, but per-session HMAC binding is only sketched.
    Enforcement would need a session-key provisioning path, which I
    left out of this RFC to keep the diff small.
  6. vTPM in user-mode: per DEVELOPMENT-PLAN.md, the vTPM is
    slated to move to a user-mode task. The boot integration in commit
    7 is intentionally a thin shim (vtpm_init_sealed) so it can be
    re-targeted to a user-mode init task. Happy to refactor if there
    is a preferred timeline for that move.
  7. Tier-2 re-seal trigger boundary: runtime re-seal fires on
    TPM2_Shutdown(SU_STATE) only. A guest that does not issue
    Shutdown (forced poweroff, hypervisor-side kill, guest crash,
    sudden power loss) loses that session's NV writes. This matches
    TCG semantics for ungraceful power loss but is worth flagging as
    an API contract.
  8. Tier-2 NV-counter / blob-save ordering: rollback/replay
    resistance is sketched, not wired in this revision. The intended
    shape is to bind a monotonic counter from the external endpoint's
    NV into the blob and check it on unseal, so a stale or rolled-back
    blob fails closed. With that in place, the ordering matters: if the
    NV counter is advanced inside seal_state before the new SealedBlob
    is committed via the configured SealedBlobStore backend and the
    store-side write then fails, a later cold boot would see a counter
    ahead of the persisted blob and fall back to Provision instead of
    recovering. A save-then-commit ordering (defer nv_increment until
    the store has acknowledged the new blob) avoids that window. This is
    noted now so the counter-check and the commit ordering land
    together; both are tracked as follow-up alongside open question 5.

Seeking feedback on overall direction, whether SealedBlobStore
should live alongside the block-layer abstraction, the
TpmTransport shape (and whether keeping it endpoint-agnostic is
the right call), the libtcgtpm accessor surface, and whether the
Tier-2 runtime re-seal hook belongs in the kernel command path or
should be lifted to the planned user-mode vTPM task.

Commit series

d62abbb libtcgtpm: Expose direct accessors for TPM persistent globals
48abdc3 vtpm: Add pluggable transport trait with mock implementation
6c08cd2 vtpm: Add VSOCK transport for TPM proxying
0bdc9df vtpm: Add pluggable storage trait for sealed vTPM state
72f12ef vtpm: Add two-layer SealedBlob (AES-256-GCM + TPM2_Seal)
143ab03 vtpm: Add TPM internal state extract/inject via libtcgtpm accessors
6f5db7b vtpm/svsm: Wire TPM-rooted persistence boot mode
b1fbbd1 vtpm: Add runtime re-seal hook and NV-block persistence

Each commit compiles and passes cargo clippy --features vtpm (and
--features vtpm,vsock, --features vtpm,vtpm-persist where
relevant) on its own. make igvm produces the three IGVM binaries
(coconut-qemu.igvm, coconut-hyperv.igvm, coconut-vanadium.igvm)
at HEAD with both FEATURES=vtpm and
FEATURES=vtpm,vtpm-persist.

End-to-end validated on an AMD EPYC server with SEV-SNP enabled,
against two TPM 2.0 endpoints sharing the same IGVM binary
(SHA-256 6c1b09cd...):

  1. swtpm as the endpoint (software reference, via
    VSOCK→socat→TCP:2321): functional correctness baseline.
  2. Physical discrete TPM — Infineon SLB 9672 SPI module
    (vendor "IFX"/"SLB9672", fw 1.84) on a Gigabyte CTM012 carrier,
    reached via /dev/tpmrm0 and a host-side TPM-over-VSOCK bridge
    daemon that replaces socat/swtpm on the host side.

Both runs exercise the same L0–L5 guest validation suite and the
same Provision → cold-boot → Recover sequence:

  • vTPM identity preserved across cold boot: EK name and EK
    qualified name are byte-identical between Provision and
    Recover (000bdb93ab66... / 000b0ae7f061... on SLB 9672;
    000b92934628... / 000bfca90d16... on swtpm).
  • Guest-runtime NV writes persist: a user NV index at
    0x1500016 written under Provision is readable after a
    cold reboot under Recover (Tier-2 / A3 path).
  • The A3 runtime re-seal hook fires on guest-side
    TPM2_Shutdown(SU_STATE) and commits a fresh SealedBlob
    before SVSM exits (observed in serial log on both endpoints).

Sealed-blob sizes match across backends (~19.1 KB Provision,
~18.8 KB Recover; the observed sub-kilobyte delta between the two
cold-boot samples is consistent with an NV-section repack and is
not endpoint-specific). On SLB 9672 the external-TPM-side latency
is dominated by RSA-2048 TPM2_CreatePrimary (~6.6 s per call on
this device); the critical unseal step itself measures ~126 ms.
Per-command latency CSV (bridge-side wall clock) is included in
the artifact dump.

Reproduction artifacts (IGVM SHA-256, Launch Digest, boot/validate
logs, sealed blobs, TPM-endpoint latency CSV) available on request.

All commits are DCO-signed.

Adds a small C-level accessor layer (state_accessor.{c,h}) that exposes
the TPM 2.0 Reference Implementation's persistent globals (gp, gc, gr)
via stable getter/setter functions plus a bulk serialize/deserialize
pair. The libtcgtpm Makefile is extended to compile state_accessor.c
into a separate libStateAccessor.a archive, and build.rs is updated to
link it alongside libcrt/libcrypto/libtcgtpm.

This is the foundation for any out-of-CVM TPM state persistence path
(state dump/restore, sealed-blob snapshotting, or KBS-stateful vTPM).

Signed-off-by: Goodleon-Y <goodleon3@126.com>
Introduces TpmTransport — a small trait abstracting any TPM 2.0
command/response endpoint outside the CVM (software vTPM, physical
discrete TPM, KBS-backed TPM service, ...). Adds:

  * TpmProxy<T: TpmTransport>   — constructs raw TPM 2.0 command
    buffers (CreatePrimary, Create, Load, Unseal, FlushContext, NV_*)
    and dispatches them over the supplied transport.
  * MockTransport                 — records sent commands and returns
    canned responses; used by unit tests of TpmProxy logic without
    needing a real TPM.

The transport boundary is deliberately backend-agnostic to align with
the upcoming "Move vTPM to user-mode" effort and any future block-layer
or KBS-stateful TPM persistence approach.

Signed-off-by: Goodleon-Y <goodleon3@126.com>
Implements TpmTransport over AF_VSOCK using SVSM's existing
VsockStream. Each TPM command opens a fresh connection, sends the
command frame, reads the response header (10 bytes: tag(2)+size(4)+rc(4))
to learn the total length, then reads the body. One-command-per-
connection avoids carrying TPM session state across the boundary.

Gated behind the existing 'vsock' feature. Default builds are
unaffected.

The host side needs a lightweight forwarder, e.g.:
  socat VSOCK-LISTEN:9999,fork OPEN:/dev/tpm0

Signed-off-by: Goodleon-Y <goodleon3@126.com>
Introduces SealedBlobStore — a backend-agnostic trait for persisting
the sealed vTPM state blob across cold boots. Three bundled
implementations:

  * StaticBufStore  — in-CVM static buffer. Survives warm reboots
                      within a single CVM lifetime; zeroed on cold
                      boot. Suitable for development and bring-up.
  * IgvmVarStore    — stub for an IGVM variable-backed store
                      (production target; load/save plumbing TBD).
  * VsockHostStore  — AF_VSOCK to a host-side persistence helper
                      (feature-gated 'vsock'). Two ports separate
                      the load and save directions so the helper can
                      implement them as independent services.

The trait is intentionally backend-agnostic to align with the upcoming
COCONUT-SVSM block-layer abstraction and any KBS-stateful TPM service.

Signed-off-by: Goodleon-Y <goodleon3@126.com>
Adds VtpmState (serializable view of the persistent TPM state needed
across cold boots) and SealedBlob — a packed binary container
combining two encryption layers:

  Layer 1: AES-256-GCM over the serialized VtpmState. Key + nonce are
           derived from a caller-supplied entropy bundle (the kernel
           init path uses dual-source HKDF).

  Layer 2: TPM2_Seal of a small (60-byte) key bundle holding the AES
           key, GCM tag and nonce. This keeps the TPM-sealed payload
           well below RSA-2048 size limits while still binding the
           bulk data to a TPM-resident parent key.

The packed container is self-describing (magic + version + counter +
PCR digest + length-prefixed sections + SHA-256 trailer) so it can be
written through SealedBlobStore (commit 4) and read back unchanged.

Anti-rollback support: an HMAC-bound monotonic counter is part of the
header so storage backends that share a counter across tenants
(NV index pool) can still distinguish per-VM blob versions.

Signed-off-by: Goodleon-Y <goodleon3@126.com>
Adds VtpmInternalState plus extract_vtpm_state() / inject_vtpm_state()
that call into the C-side accessors exposed by libtcgtpm
(state_accessor.{c,h}, commit 1) to read and write the TPM 2.0
Reference Implementation's persistent globals (gp, gc, gr).

A bulk-serialization path is also wired up:
  * extract_serialized_state() / inject_serialized_state() round-trip
    the entire set of accessor fields as one opaque buffer, which is
    what the seal/unseal flow stores inside a SealedBlob.
  * A command-based fallback (extract_vtpm_state_via_commands) is
    sketched for environments where the direct accessors are
    unavailable.

This is the bridge that lets the Rust persistence layer recover a
manufactured vTPM byte-for-byte after a cold boot.

Signed-off-by: Goodleon-Y <goodleon3@126.com>
Wires the seal/unseal lifecycle into kernel init using the previously
added components:

  * Adds VtpmBootMode (Provision/Recover) and vtpm_init_sealed<T>
    in vtpm/mod.rs. Provision manufactures the vTPM, extracts the
    serialized internal state, AES-256-GCMs it, TPM2_Seals the 60-byte
    key bundle and returns a SealedBlob ready for storage. Recover
    reverses the operation: unpack -> TPM2_Unseal -> AES decrypt ->
    inject_serialized_state -> light power-on.

  * Replaces the previous `static mut SEALED_BLOB_BUF: [u8; 4096]`
    scaffolding in svsm.rs with a `&dyn SealedBlobStore` wired through
    StaticBufStore. Whether the blob comes from a static buffer (test),
    an IGVM variable (production), or a vsock-backed host helper is now
    a backend selection rather than a code change.

Entropy comes from RDSEED inside the CVM; key material is zeroized
after the sealing step via volatile writes.

The boot integration is intentionally kept narrow so that a future
user-mode vTPM placement (per the upstream "Move vTPM to user-mode"
plan) can reuse the same persistence trait by selecting a different
SealedBlobStore impl.

Signed-off-by: Goodleon-Y <goodleon3@126.com>
Adds a runtime re-seal path that fires on a successful guest
TPM2_Shutdown (cmdCode 0x145, rc=0) and the supporting Tier-2
NV-block extraction needed to round-trip the simulator's persistent
state across a cold boot.

  * libtcgtpm: expose get_nv_blob() / apply_nv_blob() accessors so
    the in-process TPM Reference Implementation's 16 KiB NV image
    (STATE_NV_BACKUP_SIZE) can be captured at re-seal time and
    replayed on Recover. The bulk serialization wrapper grows a new
    tagged section (0x12) that carries the NV blob alongside the
    existing gp/gc/gr globals.

  * vtpm::reseal: new module hosting trigger_if_shutdown() and
    do_reseal(). The hook is invoked from TcgTpm::send_tpm_command
    *outside* the VTPM SpinLock — re-seal talks to the TPM endpoint
    through a separate TpmProxy<VsockTransport> and reads internal
    state through the libtcgtpm C FFI, so it never re-acquires VTPM.
    Re-seal failures are logged but never propagated to the guest;
    fail-closed enforcement remains on the next cold-boot unseal
    path (NV-counter check + AES-GCM tag verify).

  * vtpm::mod / svsm: wire the optional vtpm-persist feature so the
    TPM endpoint and host blob store can be reached over VSOCK
    during cold-boot Provision/Recover and runtime re-seal. The
    persistence path is opt-in and never silently falls back to
    ephemeral — an unreachable endpoint is a hard error so the
    security model that treats the physical TPM as a separate root
    of trust cannot be downgraded by a channel-only attacker.

  * cpu::apic / sev::snp_apic: convert the previously panicking
    icr_write() to a logged warning so a transient HV write error
    on the persistence path does not crash the boot CPU.

Signed-off-by: Goodleon-Y <goodleon3@126.com>
@joergroedel

Copy link
Copy Markdown
Member

Thanks for your submission. Reading through the description above I have a couple of questions around security properties of the host-side parts and actual use-cases for these changes. Can you please join one of the next community calls to discuss these open questions?

@Goodleon-Y

Copy link
Copy Markdown
Author

Thanks for your submission. Reading through the description above I have a couple of questions around security properties of the host-side parts and actual use-cases for these changes. Can you please join one of the next community calls to discuss these open questions?

Hi Joerg, thanks for going through this.

On the call: I'd rather answer in writing here first, so the technical details stay on the list and are easy to reference, and I can be more precise that way. Glad to join a later call if that's still helpful.

On the host-side security. The host components (the VSOCK helper that stores the blob and the bridge to the external TPM) are untrusted, at the same level as the rest of the SEV-SNP host. What the guest relies on today is confidentiality and integrity of the state at rest: the payload is AES-256-GCM encrypted and the AES key is TPM2_Sealed to the external endpoint, so the host only ever handles ciphertext plus a sealed key blob it cannot open. A host that flips bytes in either part fails the GCM tag or the unseal, so silent tampering is caught.

What a host can still do is withhold the blob or hand back an older one. As I flagged in open questions 5 and 8, rollback/replay resistance and channel authentication are the parts I left sketched rather than built in this RFC. The shape I'm aiming for is to bind a monotonic counter from the endpoint's NV into the blob and check it on unseal, so a stale or rolled-back blob fails closed, and to wrap the proxy channel in a salted HMAC session so the host can't splice or forge proxy traffic. I'd be glad to turn those two from a sketch into a concrete design note here if that's the most useful next step. With them in place the host reduces to untrusted storage and transport whose only remaining power is denial of service.

On the use-case, to be more concrete than the list above: the target is an on-prem or edge confidential VM that needs a stable TPM-rooted identity across reboots (its EK/AK, sealed secrets, measured-boot NV) in a setting where there is no KBS and no guarantee the network is up at boot. Today such a VM either loses vTPM state on every cold boot or has to re-enroll out of band. If the platform already has a discrete TPM, this lets that TPM be the persistence anchor without standing up KBS infrastructure. It is meant to sit beside the encrypted-block + KBS path, not replace it.

Happy to go deeper on either. Thanks again.

@stefano-garzarella

Copy link
Copy Markdown
Member

@Goodleon-Y thanks for this RFC, the design is interesting, but I have a question about the threat model, specifically around the trust boundary between the CVM and the TPM endpoint.

The core issue is that the TPM endpoint (whether swtpm or a physical TPM) is reachable only through the host. The host mediates every TPM command and response, and the AES-256 key is exposed in plaintext on the VSOCK channel in both directions during the seal (this may be not a problem if the sealing is done in a trusted environment) but also during the unseal IIUC.

This is not just a VSOCK problem: encrypting the VSOCK channel (e.g. with TLS) would not help, because the host-side process that terminates the channel (the socat/bridge to /dev/tpm0 or swtpm) still handles the commands and responses in cleartext before forwarding them. The host is root on that process, so it can read its memory, ptrace it, or replace it entirely.

Could you clarify the intended threat model?

  1. Is the host assumed to be trusted for confidentiality (just not for integrity, which AES-GCM covers)?
  2. If not, how would you prevent the host from reading the unsealed key material? IIUC in the current architecture the TPM endpoint terminates on the host, so any channel encryption just moves the plaintext exposure from shared memory to the bridge process.
  3. For the provisioning-on-a-trusted-host scenario: even if the blob is sealed to a specific physical TPM, the host retains the ability to unseal it at any time (especially with the current zero PCR policy). Is this considered acceptable?

For comparison, in the KBS-based approach the CVM generates a key pair, includes the public key in the SNP attestation report (which is signed by the hardware), and the KBS encrypts the secret to that public key after verifying the attestation. The host relays the encrypted blob but never sees the plaintext key, because only the CVM holds the private key.

I think understanding the intended trust boundary would help evaluate where this approach fits and what security guarantees it actually provides.

As Joerg suggested, I think joining one of the community calls would be really valuable to discuss these points. It would also be a good chance to get broader input from other contributors.

@Goodleon-Y

Copy link
Copy Markdown
Author

@stefano-garzarella, thanks for the careful read, this is exactly the boundary that matters. You're right, in the RFC the AES key is sealed to the TPM, so on unseal it comes back through the host bridge in cleartext, which makes the host trusted for confidentiality. I've reworked the key schedule so it isn't.

The AES key is now derived inside the CVM from two independent halves, and is never sealed or put on the channel:

aes_key = HKDF(salt ; k_psp* ‖ k_dtpm)

k_dtpm (32B random) is the only half sealed to the TPM, and the only key material that crosses the channel; assume the host unseals it at will, on its own it is useless. k_psp* is derived in-CVM via MSG_KEY_REQ from the SNP platform key, bound to GUEST_FIELD_MEASUREMENT at VMPL0. It never leaves the CVM, never hits the channel, and never touches the TPM.

So sniffing VSOCK, reading the bridge process, or unsealing the TPM blob all yield only k_dtpm. On your three questions: (1) no, the host is no longer trusted for confidentiality; (2) the only key material it can reach is k_dtpm; the protecting half is PSP-derived in-CVM and never crosses the boundary; (3) yes, the host can unseal the TPM part whenever it likes, and that is now harmless: the measurement binding moved out of the host-controlled PCR policy into the SNP derivation of k_psp*.

The host can't derive k_psp* itself: MSG_KEY_REQ is VMPCK-authenticated, so it can't issue it for the victim guest, and the MEASUREMENT mixing means any guest it does launch derives a different key. It is the same "host never sees the key" property as the KBS path, rooted in the platform key rather than a broker, which is the point when there is no broker to reach.

Validated on real SEV-SNP with a discrete TPM (Infineon SLB 9672), not swtpm: k_psp* derives through MSG_KEY_REQ against the PSP, re-derives byte-identically across a cold boot (a clean unseal is the witness, since the GCM tag only verifies on a matching key), and a build with a different launch measurement cannot recover another measurement's blob, so it fails closed.

The known cost of binding to MEASUREMENT is that an SVSM/TCB upgrade changes k_psp*, so state has to be migrated to the new measurement across an upgrade rather than recovered directly.

So it stays complementary to the encrypted-block + KBS path, for deployments with no online broker. Happy to take this to a call, and to share the reworked design or branch if that's useful.

@stefano-garzarella

Copy link
Copy Markdown
Member

@Goodleon-Y But if k_psp* already provides confidentiality, measurement binding, and platform binding... what does k_dtpm add?

Could we just encrypt the vTPM state with the PSP-derived key and store it anywhere, without involving an external TPM at all?

@Goodleon-Y

Copy link
Copy Markdown
Author

@stefano-garzarella For confidentiality against the host, k_psp* is enough on its own and k_dtpm adds nothing there. It's there for two other reasons.

Rollback: SNP derivation gives you a key but no host-resettable monotonic state, so encrypting with the PSP key and storing the blob anywhere doesn't stop the host withholding it or serving an older one. The NV counter does: we bind its value into the blob and require strict equality on unseal, so a stale or rolled-back blob fails closed. That's why it's a TPM and not just a file.

Second root: k_dtpm keeps the other half of the key in the discrete TPM, independent of the PSP's derivation, so the state is exposed only if both roots break rather than the PSP alone. On a platform that already trusts a discrete TPM, anchoring the long-term vTPM identity there too costs no extra hardware, and the host driving the TPM still only sees that sealed half.

So encrypt-with-PSP-and-store is valid and simpler if you accept being rollback-able and resting the long-term identity's confidentiality on the single PSP root. The dTPM removes both, with hardware on-prem and edge boxes already have; its cost (MEASUREMENT binding, so upgrades need a migration) I noted before.

@stefano-garzarella

Copy link
Copy Markdown
Member

@stefano-garzarella For confidentiality against the host, k_psp* is enough on its own and k_dtpm adds nothing there. It's there for two other reasons.

Rollback: SNP derivation gives you a key but no host-resettable monotonic state, so encrypting with the PSP key and storing the blob anywhere doesn't stop the host withholding it or serving an older one. The NV counter does: we bind its value into the blob and require strict equality on unseal, so a stale or rolled-back blob fails closed. That's why it's a TPM and not just a file.

Sorry, the TPM is host-controlled, no? So how can you trust the NV counter? As you mention the TPM can be a software TPM emulated by the host, so the host can do whatever, no?

Second root: k_dtpm keeps the other half of the key in the discrete TPM, independent of the PSP's derivation, so the state is exposed only if both roots break rather than the PSP alone. On a platform that already trusts a discrete TPM, anchoring the long-term vTPM identity there too costs no extra hardware, and the host driving the TPM still only sees that sealed half.

Okay, so you must trust the TPM attached to the host. How?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants