mirror of
https://github.com/luxfi/cc.git
synced 2026-09-24 00:24:54 +00:00
Fold Dispatch's hard Kind switch into the init() registry SGX introduced.
The registry (registry map, registerVerifier, RegisteredVerifier) moves out
of sgx.go into verifier.go as framework; Dispatch becomes one registry
lookup, fail-closed (ErrUnsupportedKind) for an unregistered Kind. The
switch — including Nitro's added case — is deleted: there is now exactly one
routing path.
Each Kind file is self-contained: its string const, its Verifier impl, its
per-call options, and its 'func init() { registerVerifier(...) }' live
together. SEVSNP/TDX/NVTrust/Nitro consts move out of verifier.go beside
their verifiers (SGX already did this); NVTrust's per-call WithNVTrustRIM /
WithNVTrustTrustRoots move to nvtrust.go for symmetry with WithNitro*. The
shared config keeps the per-Kind per-call policy fields (kdsGetter is shared
by SEV+TDX; nvtrust RIM/roots; nitro roots/PCRs/maxAge); NVTrust's
deployment trust anchors stay on the struct via NewNVTrust. NVTrust
registers its fail-closed zero value, matching prior Dispatch semantics.
registerVerifier panics on a duplicate Kind — a duplicate registration is a
programmer error that must fail loud at startup, never silently shadow a
verifier on the trust path.
doc.go: drop the now-false TDX/NVTrust STUB labels (both real), document all
five shipped Kinds + the registry + the pending ARMCCA slot, and the TDX
MRTD+RTMR measurement fold. verifier_test.go: retire the stub tests (done by
the TDX merge) and add TestRegistry_RoutesEveryKind asserting registry
routing for all five Kinds and fail-closed for the unregistered armcca slot.
go build/vet/test + go test -race ./attest/... all green; gofmt clean.
205 lines
7.2 KiB
Go
205 lines
7.2 KiB
Go
// Copyright (c) 2026, Lux Industries Inc.
|
|
// SPDX-License-Identifier: BSD-3-Clause
|
|
|
|
package attest
|
|
|
|
import (
|
|
"context"
|
|
"crypto/subtle"
|
|
"encoding/hex"
|
|
"errors"
|
|
"fmt"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/google/go-sev-guest/abi"
|
|
spb "github.com/google/go-sev-guest/proto/sevsnp"
|
|
"github.com/google/go-sev-guest/verify"
|
|
"github.com/google/go-sev-guest/verify/trust"
|
|
)
|
|
|
|
// KindSEVSNP is the AMD SEV-SNP raw attestation report framing: a 0x4A0-byte
|
|
// ABI report carrying no certificate chain (the VCEK leaf is fetched from the
|
|
// AMD KDS at verify time).
|
|
const KindSEVSNP Kind = "sev_snp"
|
|
|
|
// SEVSNP is the production SEV-SNP verifier. It accepts a raw 0x4A0-byte
|
|
// AMD SEV-SNP attestation report (no embedded cert chain) and:
|
|
//
|
|
// 1. Parses the report via go-sev-guest/abi.ReportToProto.
|
|
// 2. Fetches the VCEK leaf from the AMD KDS at
|
|
// kdsintf.amd.com/vcek/v1/<product>/<hwid>?... using the report's
|
|
// CHIP_ID + reported TCB. The product CA bundle (ARK→ASK) is
|
|
// embedded in go-sev-guest/verify/trust.
|
|
// 3. Validates the chain to AMD's pinned root and checks the report
|
|
// signature against the VCEK public key.
|
|
// 4. Extracts MEASUREMENT, REPORT_DATA, HOST_DATA, CHIP_ID, TCB.
|
|
// 5. Enforces caller-supplied policy options (expected report data,
|
|
// expected measurement).
|
|
//
|
|
// Production path requires network reachability to kdsintf.amd.com. In
|
|
// tests the WithKDSGetter option installs a SimpleGetter that replays
|
|
// pre-fetched bytes from testdata/.
|
|
type SEVSNP struct{}
|
|
|
|
// Verify implements Verifier for SEV-SNP raw reports.
|
|
func (SEVSNP) Verify(ctx context.Context, evidence []byte, opts ...Option) (*VerifiedReport, error) {
|
|
if len(evidence) != abi.ReportSize {
|
|
return nil, fmt.Errorf("%w: sev-snp expects %d bytes, got %d",
|
|
ErrInvalidEvidence, abi.ReportSize, len(evidence))
|
|
}
|
|
|
|
cfg := applyOptions(opts...)
|
|
|
|
// Parse first so we fail fast on malformed framing.
|
|
report, err := abi.ReportToProto(evidence)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("%w: %v", ErrInvalidEvidence, err)
|
|
}
|
|
|
|
// Build verify.Options. We rely on the embedded ARK/ASK trusted
|
|
// roots inside go-sev-guest; the only thing we override is the
|
|
// HTTPS getter (so tests can replay KDS responses) and Now.
|
|
vopts := &verify.Options{}
|
|
if cfg.kdsGetter != nil {
|
|
g, ok := cfg.kdsGetter.(trust.HTTPSGetter)
|
|
if !ok {
|
|
return nil, fmt.Errorf("%w: kds getter does not implement trust.HTTPSGetter",
|
|
ErrInvalidEvidence)
|
|
}
|
|
vopts.Getter = g
|
|
} else {
|
|
vopts.Getter = trust.DefaultHTTPSGetter()
|
|
}
|
|
if !cfg.now.IsZero() {
|
|
vopts.Now = cfg.now
|
|
} else {
|
|
vopts.Now = time.Now()
|
|
}
|
|
|
|
// Chain + signature verify. This:
|
|
// - fetches VCEK from KDS by CHIP_ID + TCB
|
|
// - validates ARK→ASK→VCEK
|
|
// - validates VCEK extensions match the report's TCB
|
|
// - validates the report signature against VCEK
|
|
if err := verify.RawSnpReportContext(ctx, evidence, vopts); err != nil {
|
|
// go-sev-guest reports a single error string. Classify by
|
|
// substring so callers can distinguish chain vs signature
|
|
// failures via errors.Is. This is conservative — when in
|
|
// doubt, treat as ChainInvalid (refuse).
|
|
msg := err.Error()
|
|
switch {
|
|
case strings.Contains(msg, "could not interpret report bytes"):
|
|
return nil, fmt.Errorf("%w: %v", ErrInvalidEvidence, err)
|
|
case strings.Contains(msg, "signature"):
|
|
return nil, fmt.Errorf("%w: %v", ErrSignatureInvalid, err)
|
|
default:
|
|
return nil, fmt.Errorf("%w: %v", ErrChainInvalid, err)
|
|
}
|
|
}
|
|
|
|
// Apply caller policy on the verified report fields.
|
|
if cfg.expectedReportData != nil {
|
|
if subtle.ConstantTimeCompare(cfg.expectedReportData, report.GetReportData()) != 1 {
|
|
return nil, fmt.Errorf("%w: report_data mismatch (got %s)",
|
|
ErrPolicy, hex.EncodeToString(report.GetReportData()))
|
|
}
|
|
}
|
|
if cfg.expectedMeasurement != nil {
|
|
if subtle.ConstantTimeCompare(cfg.expectedMeasurement, report.GetMeasurement()) != 1 {
|
|
return nil, fmt.Errorf("%w: measurement mismatch (got %s)",
|
|
ErrPolicy, hex.EncodeToString(report.GetMeasurement()))
|
|
}
|
|
}
|
|
|
|
out := &VerifiedReport{
|
|
Kind: KindSEVSNP,
|
|
Vendor: "amd.sev.snp",
|
|
Measurement: cloneBytes(report.GetMeasurement()),
|
|
ReportData: cloneBytes(report.GetReportData()),
|
|
HostData: cloneBytes(report.GetHostData()),
|
|
ChipID: cloneBytes(report.GetChipId()),
|
|
IssuedAt: vopts.Now.UTC(),
|
|
Extra: buildSEVExtra(report),
|
|
}
|
|
// CompositeHash binds verifier kind + the canonical fields the
|
|
// chain validated. Re-deriving on the consumer side reproduces the
|
|
// same hash iff the same evidence was verified by an honest
|
|
// verifier with the same policy.
|
|
out.CompositeHash = computeCompositeHash(KindSEVSNP, canonicalSEVBytes(report))
|
|
return out, nil
|
|
}
|
|
|
|
// buildSEVExtra collects sev-snp-specific fields that don't fit the
|
|
// common shape. Keys are stable wire identifiers; values are hex.
|
|
func buildSEVExtra(report *spb.Report) map[string]string {
|
|
extra := make(map[string]string, 6)
|
|
extra["sev_snp.policy"] = fmt.Sprintf("%#x", report.GetPolicy())
|
|
extra["sev_snp.vmpl"] = fmt.Sprintf("%d", report.GetVmpl())
|
|
extra["sev_snp.current_tcb"] = fmt.Sprintf("%#x", report.GetCurrentTcb())
|
|
extra["sev_snp.committed_tcb"] = fmt.Sprintf("%#x", report.GetCommittedTcb())
|
|
extra["sev_snp.platform_info"] = fmt.Sprintf("%#x", report.GetPlatformInfo())
|
|
if id := report.GetFamilyId(); len(id) > 0 {
|
|
extra["sev_snp.family_id"] = hex.EncodeToString(id)
|
|
}
|
|
if id := report.GetImageId(); len(id) > 0 {
|
|
extra["sev_snp.image_id"] = hex.EncodeToString(id)
|
|
}
|
|
return extra
|
|
}
|
|
|
|
// canonicalSEVBytes returns the bytes that participate in CompositeHash.
|
|
// We hash the verifier-extracted fields (not the raw report) so that any
|
|
// pre-verify field shuffling at lower layers cannot change the root
|
|
// without changing what the verifier saw. Field order is fixed and
|
|
// matches the public ABI ordering of the report.
|
|
func canonicalSEVBytes(report *spb.Report) []byte {
|
|
// Length: 8(policy) + 8(current_tcb) + 8(committed_tcb)
|
|
// + 16(family) + 16(image) + 32(host) + 48(meas)
|
|
// + 64(report_data) + 64(chip_id) = 264 bytes max.
|
|
buf := make([]byte, 0, 264)
|
|
appendU64 := func(v uint64) {
|
|
var b [8]byte
|
|
for i := 0; i < 8; i++ {
|
|
b[i] = byte(v >> (8 * i))
|
|
}
|
|
buf = append(buf, b[:]...)
|
|
}
|
|
appendU64(report.GetPolicy())
|
|
appendU64(report.GetCurrentTcb())
|
|
appendU64(report.GetCommittedTcb())
|
|
buf = append(buf, fixedSize(report.GetFamilyId(), 16)...)
|
|
buf = append(buf, fixedSize(report.GetImageId(), 16)...)
|
|
buf = append(buf, fixedSize(report.GetHostData(), 32)...)
|
|
buf = append(buf, fixedSize(report.GetMeasurement(), 48)...)
|
|
buf = append(buf, fixedSize(report.GetReportData(), 64)...)
|
|
buf = append(buf, fixedSize(report.GetChipId(), 64)...)
|
|
return buf
|
|
}
|
|
|
|
// fixedSize returns b padded or truncated to size n. Zero-pads when
|
|
// short. This guarantees canonicalSEVBytes is a fixed-shape blob.
|
|
func fixedSize(b []byte, n int) []byte {
|
|
out := make([]byte, n)
|
|
copy(out, b)
|
|
return out
|
|
}
|
|
|
|
func cloneBytes(b []byte) []byte {
|
|
if len(b) == 0 {
|
|
return nil
|
|
}
|
|
out := make([]byte, len(b))
|
|
copy(out, b)
|
|
return out
|
|
}
|
|
|
|
func init() { registerVerifier(KindSEVSNP, SEVSNP{}) }
|
|
|
|
// Compile-time guard: SEVSNP satisfies Verifier.
|
|
var _ Verifier = SEVSNP{}
|
|
|
|
// Sentinel for unused import resilience: keep the errors import live in
|
|
// case future SEV-specific sentinels are added here. (No-op at runtime.)
|
|
var _ = errors.New
|