feat(attest): accept several collateral endpoints instead of one - #1141
Draft
kvinwang wants to merge 1 commit into
Draft
feat(attest): accept several collateral endpoints instead of one#1141kvinwang wants to merge 1 commit into
kvinwang wants to merge 1 commit into
Conversation
Attestation verification depends on collateral only the vendor publishes, and until now each source was configured as exactly one URL. AMD KDS is the sharper case: a single global endpoint with no official mirror, rate limited to roughly one identical request per ten seconds, and unreachable for hours at a time -- during which nothing on the host can verify an SEV-SNP quote, so no AMD guest can obtain keys. `pccs` and `amd_kds` now take either a string or a list. Endpoints are tried in order and the first answer wins. Existing configs and already-serialized `SysConfig` blobs parse unchanged, and a one-element list serializes back as a bare string, because guests hash the sys-config they were given rather than what a newer binary would have written. Failing over relaxes nothing. Collateral is vendor-signed and checked against roots compiled into the binary, so an endpoint can be slow, stale or absent but cannot forge an answer; which endpoint replied has no bearing on whether the signature checks pass. That is what makes a cache or mirror a legitimate entry in the list rather than a hole in the trust model. The two clients fail over on different rules, because they can see different things: - AMD KDS is in-tree, so it classifies. Transport failures, 408, 429 and 5xx say something about the endpoint and move on; a 404 is an answer about the chip that every mirror repeats, so it surfaces as-is rather than being retried into a pile of identical errors. - PCCS goes through `dcap-qvl`, which returns `anyhow::Error` with no status to classify on. It fails over on any error. Matching on error strings to do better would be worse than the coarse rule it replaced. Tests drive real sockets rather than asserting on the shape of the code: a 429 and a refused connection each hand off, a 404 stops with the second endpoint never contacted, and an all-fail run names every endpoint it tried.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Draft — the mechanism works and is tested, but two decisions below are worth settling before this is worth merging.
Problem
Verification depends on collateral that only the vendor publishes, and each source was configured as exactly one URL. That makes every verifier a single point of failure downstream of a service nobody here operates.
AMD KDS is the sharp case. It is a single global endpoint with no official mirror, rate limited to roughly one identical request per ten seconds, and it was completely unreachable for an entire 0.6.0-rc0 test round — TCP timeouts and 100% ICMP loss from five vantage points across four autonomous systems, while every other external service answered normally. For as long as that lasts, no KMS can verify an SEV-SNP quote, so no AMD guest can obtain keys. The rate limit bites in normal operation too: two cold-process verifications of the same chip return
429, which reads exactly like a verification failure.There is no host-side escape.
normalize_kernel_cert_tablecan read ASK and VCEK out of the SNP extended report, but stock QEMU has nocerts-path-style property onsev-snp-guestand upstreampsp-sev.hhas noSNP_SET_EXT_CONFIG, so guests ask and receive an empty table.Change
pccsandamd_kdsunder[core.attestation.urls](and[attestation.urls]fordstack-verifier) now accept either a string or a list:Endpoints are tried in order, first answer wins. This covers KMS,
dstack-verifier, gateway, and the guest's ownHostApipath that verifies the SGX key-provider quote.Backward compatibility is deliberate rather than incidental: a bare string still parses, the legacy top-level
pccs_urlstill wins when nothing else is set, and a one-element list serializes back out as a bare string — guests hash the sys-config they were handed, so a newer binary must not rewrite its shape.Why this is not a hole in the trust model
Collateral is vendor-signed and verified against roots compiled into the binary. For AMD the fetched ARK is discarded outright (
let (_fetched_ark, ask) = ...). An endpoint in this list can be slow, stale, or absent; it cannot forge. Which endpoint answered has no bearing on whether the signature checks pass — which is what makes a cache or an internal mirror a legitimate entry rather than a downgrade.Two different failover rules, on purpose
dcap-qvl, which returnsanyhow::Errorwith no status code to classify on, so it fails over on any error. Matching on error strings to do better would be worse than the coarse rule it replaces.Tests
sev-snp-qvl/src/failover_tests.rsdrives real sockets rather than asserting on the shape of the code:429hands off, and the second endpoint serves the answer404stops, and the second endpoint is never contactedPlus round-trip tests in
dstack-typespinning the string/list duality and the legacy-field precedence.Verified against the real thing
Not just unit tests. A Cloudflare Worker caching mirror in front of KDS, with
amd_kdspointed at it:dstack-verifier --verifyruns of one real SEV-SNP attestation all pass; the same sequence run directly against KDS fails on the second with the429That exercised a single mirror URL rather than the failover path, so the ordering logic here rests on the socket tests above.
Open questions
kms_urlsandgateway_urlshaveshuffle_*flags to spread load. Strict ordering is right for "prefer my cache, fall back to the vendor", wrong for "three equal mirrors". Addingshuffle_collateral_urlsis easy; I did not want to guess.vmm.tomlpass a list through to guests?cvm.pccs_urlis still a singleStringatvmm/src/app.rs, so a guest cannot be given more than one PCCS today even thoughSysConfigcan now carry several. Straightforward follow-up, kept out of this diff to keep it reviewable.Related: #1140 — nothing here checks collateral freshness, and on SEV-SNP nothing checks certificate validity at all, so an arbitrarily old cached VCEK is indistinguishable from a fresh one. Worth settling separately, but it is the reason to be careful about how long a mirror caches.