diff --git a/cli/cmd/common.go b/cli/cmd/common.go index feca136190..c27ca1301f 100644 --- a/cli/cmd/common.go +++ b/cli/cmd/common.go @@ -14,6 +14,7 @@ import ( "github.com/edgelesssys/contrast/cli/telemetry" "github.com/edgelesssys/contrast/internal/atls" + "github.com/edgelesssys/contrast/internal/attestation/certcache" "github.com/edgelesssys/contrast/internal/attestation/snp" "github.com/edgelesssys/contrast/internal/fsstore" "github.com/edgelesssys/contrast/internal/logger" @@ -88,7 +89,7 @@ func validatorsFromManifest(m *manifest.Manifest, log *slog.Logger, hostData []b } log.Debug("Using KDS cache dir", "dir", kdsDir) kdsCache := fsstore.New(kdsDir, log.WithGroup("kds-cache")) - kdsGetter := snp.NewCachedHTTPSGetter(kdsCache, snp.NeverGCTicker, log.WithGroup("kds-getter")) + kdsGetter := certcache.NewCachedHTTPSGetter(kdsCache, certcache.NeverGCTicker, log.WithGroup("kds-getter")) opts, err := m.SNPValidateOpts(kdsGetter) if err != nil { diff --git a/coordinator/internal/authority/credentials.go b/coordinator/internal/authority/credentials.go index aab02853f2..c547807e08 100644 --- a/coordinator/internal/authority/credentials.go +++ b/coordinator/internal/authority/credentials.go @@ -13,6 +13,7 @@ import ( "time" "github.com/edgelesssys/contrast/internal/atls" + "github.com/edgelesssys/contrast/internal/attestation/certcache" "github.com/edgelesssys/contrast/internal/attestation/snp" "github.com/edgelesssys/contrast/internal/logger" "github.com/edgelesssys/contrast/internal/manifest" @@ -31,13 +32,13 @@ type Credentials struct { logger *slog.Logger attestationFailuresCounter prometheus.Counter - kdsGetter *snp.CachedHTTPSGetter + kdsGetter *certcache.CachedHTTPSGetter } // Credentials creates new transport credentials that validate peers according to the latest manifest. func (a *Authority) Credentials(reg *prometheus.Registry, issuer atls.Issuer) (*Credentials, func()) { ticker := clock.RealClock{}.NewTicker(24 * time.Hour) - kdsGetter := snp.NewCachedHTTPSGetter(memstore.New[string, []byte](), ticker, logger.NewNamed(a.logger, "kds-getter")) + kdsGetter := certcache.NewCachedHTTPSGetter(memstore.New[string, []byte](), ticker, logger.NewNamed(a.logger, "kds-getter")) attestationFailuresCounter := promauto.With(reg).NewCounter(prometheus.CounterOpts{ Subsystem: "contrast_meshapi", Name: "attestation_failures_total", diff --git a/internal/attestation/snp/cached_client.go b/internal/attestation/certcache/cached_client.go similarity index 98% rename from internal/attestation/snp/cached_client.go rename to internal/attestation/certcache/cached_client.go index 142562d037..7ac2f5bdfd 100644 --- a/internal/attestation/snp/cached_client.go +++ b/internal/attestation/certcache/cached_client.go @@ -1,7 +1,7 @@ // Copyright 2024 Edgeless Systems GmbH // SPDX-License-Identifier: AGPL-3.0-only -package snp +package certcache import ( "log/slog" diff --git a/internal/attestation/snp/cached_client_test.go b/internal/attestation/certcache/cached_client_test.go similarity index 99% rename from internal/attestation/snp/cached_client_test.go rename to internal/attestation/certcache/cached_client_test.go index 20b4da9857..8515eae071 100644 --- a/internal/attestation/snp/cached_client_test.go +++ b/internal/attestation/certcache/cached_client_test.go @@ -1,7 +1,7 @@ // Copyright 2024 Edgeless Systems GmbH // SPDX-License-Identifier: AGPL-3.0-only -package snp +package certcache import ( "log/slog" diff --git a/internal/attestation/snp/validator.go b/internal/attestation/snp/validator.go index 9a7e07f9ca..51ddf5e2b2 100644 --- a/internal/attestation/snp/validator.go +++ b/internal/attestation/snp/validator.go @@ -71,7 +71,7 @@ func (v *Validator) Validate(ctx context.Context, attDocRaw []byte, nonce []byte attestation := &sevsnp.Attestation{} if err := proto.Unmarshal(attDocRaw, attestation); err != nil { - return fmt.Errorf("unmarshalling attestation: %w", err) + return fmt.Errorf("unmarshaling attestation: %w", err) } if attestation.Report == nil { @@ -79,7 +79,7 @@ func (v *Validator) Validate(ctx context.Context, attDocRaw []byte, nonce []byte } reportRaw, err := abi.ReportToAbiBytes(attestation.Report) if err != nil { - return fmt.Errorf("converting report to abi: %w", err) + return fmt.Errorf("converting report to abi format: %w", err) } v.logger.Info("Report decoded", "reportRaw", hex.EncodeToString(reportRaw)) @@ -90,7 +90,7 @@ func (v *Validator) Validate(ctx context.Context, attDocRaw []byte, nonce []byte } v.logger.Info("Successfully verified report signature") - // Validate the report data. + // Build the validation options. reportDataExpected := reportdata.Construct(peerPublicKey, nonce) v.validateOpts.ReportData = reportDataExpected[:] diff --git a/internal/attestation/tdx/Intel_SGX_Provisioning_Certification_RootCA.pem b/internal/attestation/tdx/Intel_SGX_Provisioning_Certification_RootCA.pem new file mode 100644 index 0000000000..7dee743eb0 --- /dev/null +++ b/internal/attestation/tdx/Intel_SGX_Provisioning_Certification_RootCA.pem @@ -0,0 +1,16 @@ +-----BEGIN CERTIFICATE----- +MIICjzCCAjSgAwIBAgIUImUM1lqdNInzg7SVUr9QGzknBqwwCgYIKoZIzj0EAwIw +aDEaMBgGA1UEAwwRSW50ZWwgU0dYIFJvb3QgQ0ExGjAYBgNVBAoMEUludGVsIENv +cnBvcmF0aW9uMRQwEgYDVQQHDAtTYW50YSBDbGFyYTELMAkGA1UECAwCQ0ExCzAJ +BgNVBAYTAlVTMB4XDTE4MDUyMTEwNDUxMFoXDTQ5MTIzMTIzNTk1OVowaDEaMBgG +A1UEAwwRSW50ZWwgU0dYIFJvb3QgQ0ExGjAYBgNVBAoMEUludGVsIENvcnBvcmF0 +aW9uMRQwEgYDVQQHDAtTYW50YSBDbGFyYTELMAkGA1UECAwCQ0ExCzAJBgNVBAYT +AlVTMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEC6nEwMDIYZOj/iPWsCzaEKi7 +1OiOSLRFhWGjbnBVJfVnkY4u3IjkDYYL0MxO4mqsyYjlBalTVYxFP2sJBK5zlKOB +uzCBuDAfBgNVHSMEGDAWgBQiZQzWWp00ifODtJVSv1AbOScGrDBSBgNVHR8ESzBJ +MEegRaBDhkFodHRwczovL2NlcnRpZmljYXRlcy50cnVzdGVkc2VydmljZXMuaW50 +ZWwuY29tL0ludGVsU0dYUm9vdENBLmRlcjAdBgNVHQ4EFgQUImUM1lqdNInzg7SV +Ur9QGzknBqwwDgYDVR0PAQH/BAQDAgEGMBIGA1UdEwEB/wQIMAYBAf8CAQEwCgYI +KoZIzj0EAwIDSQAwRgIhAOW/5QkR+S9CiSDcNoowLuPRLsWGf/Yi7GSX94BgwTwg +AiEA4J0lrHoMs+Xo5o/sX6O9QWxHRAvZUGOdRQ7cvqRXaqI= +-----END CERTIFICATE----- diff --git a/internal/attestation/tdx/validator.go b/internal/attestation/tdx/validator.go new file mode 100644 index 0000000000..90564e4548 --- /dev/null +++ b/internal/attestation/tdx/validator.go @@ -0,0 +1,170 @@ +// Copyright 2024 Edgeless Systems GmbH +// SPDX-License-Identifier: AGPL-3.0-only + +package tdx + +import ( + "context" + "crypto/x509" + _ "embed" + "encoding/asn1" + "encoding/hex" + "fmt" + "log/slog" + + "github.com/edgelesssys/contrast/internal/attestation/reportdata" + "github.com/edgelesssys/contrast/internal/oid" + "github.com/google/go-tdx-guest/abi" + "github.com/google/go-tdx-guest/proto/tdx" + "github.com/google/go-tdx-guest/validate" + "github.com/google/go-tdx-guest/verify" + "github.com/google/go-tdx-guest/verify/trust" + "github.com/prometheus/client_golang/prometheus" + "google.golang.org/protobuf/proto" +) + +// Even though the vendored file has "SGX" in its name, it is the general "Provisioning Certificate for ECDSA Attestation" +// from Intel and used for both SGX *and* TDX. +// +// See https://api.portal.trustedservices.intel.com/content/documentation.html#pcs for more information. +// +// File Source: https://certificates.trustedservices.intel.com/Intel_SGX_Provisioning_Certification_RootCA.pem +// +//go:embed Intel_SGX_Provisioning_Certification_RootCA.pem +var tdxRootCert []byte + +// Validator validates attestation statements. +type Validator struct { + validateOptsGen validateOptsGenerator + callbackers []validateCallbacker + certGetter trust.HTTPSGetter + logger *slog.Logger + metrics metrics +} + +type metrics struct { + attestationFailures prometheus.Counter +} + +type validateCallbacker interface { + ValidateCallback(ctx context.Context, quote *tdx.QuoteV4, validatorOID asn1.ObjectIdentifier, + reportRaw, nonce, peerPublicKey []byte) error +} + +type validateOptsGenerator interface { + TDXValidateOpts(report *tdx.QuoteV4) (*validate.Options, error) +} + +// StaticValidateOptsGenerator returns validate.Options generator that returns +// static validation options. +type StaticValidateOptsGenerator struct { + Opts *validate.Options +} + +// TDXValidateOpts return the TDX validation options. +func (v *StaticValidateOptsGenerator) TDXValidateOpts(_ *tdx.QuoteV4) (*validate.Options, error) { + return v.Opts, nil +} + +// NewValidator returns a new Validator. +func NewValidator(optsGen validateOptsGenerator, certGetter trust.HTTPSGetter, log *slog.Logger) *Validator { + return &Validator{ + validateOptsGen: optsGen, + certGetter: certGetter, + logger: log, + } +} + +// NewValidatorWithCallbacks returns a new Validator with callbacks. +func NewValidatorWithCallbacks(optsGen validateOptsGenerator, certGetter trust.HTTPSGetter, log *slog.Logger, attestationFailures prometheus.Counter, callbacks ...validateCallbacker) *Validator { + v := NewValidator(optsGen, certGetter, log) + v.callbackers = callbacks + v.metrics = metrics{attestationFailures: attestationFailures} + return v +} + +// OID returns the OID of the validator. +func (v *Validator) OID() asn1.ObjectIdentifier { + return oid.RawTDXReport +} + +// Validate a TDX attestation. +func (v *Validator) Validate(ctx context.Context, attDocRaw []byte, nonce []byte, peerPublicKey []byte) (err error) { + v.logger.Info("Validate called", "nonce", hex.EncodeToString(nonce)) + defer func() { + if err != nil { + v.logger.Error("Failed to validate attestation document", "err", err) + if v.metrics.attestationFailures != nil { + v.metrics.attestationFailures.Inc() + } + } + }() + + // Parse the attestation document. + + quote := &tdx.QuoteV4{} + if err := proto.Unmarshal(attDocRaw, quote); err != nil { + return fmt.Errorf("unmarshaling attestation: %w", err) + } + + quoteRaw, err := abi.QuoteToAbiBytes(quote) + if err != nil { + return fmt.Errorf("converting quote to abi format: %w", err) + } + v.logger.Info("Quote decoded", "quoteRaw", hex.EncodeToString(quoteRaw)) + + // Build the verification options. + + verifyOpts := verify.DefaultOptions() + rootCerts, err := trustedRoots() + if err != nil { + return fmt.Errorf("getting trusted roots: %w", err) + } + verifyOpts.TrustedRoots = rootCerts + verifyOpts.CheckRevocations = true + verifyOpts.Getter = v.certGetter + + // Verify the report signature. + + if err := verify.TdxQuote(quote, verifyOpts); err != nil { + return fmt.Errorf("verifying report signature: %w", err) + } + v.logger.Info("Successfully verified report signature") + + // Build the validation options. + + reportDataExpected := reportdata.Construct(peerPublicKey, nonce) + validateOpts, err := v.validateOptsGen.TDXValidateOpts(quote) + if err != nil { + return fmt.Errorf("generating validation options: %w", err) + } + validateOpts.TdQuoteBodyOptions.ReportData = reportDataExpected[:] + + // Validate the report data. + + if err := validate.TdxQuote(quote, validateOpts); err != nil { + return fmt.Errorf("validating report data: %w", err) + } + v.logger.Info("Successfully validated report data") + + // Run callbacks. + + for _, callbacker := range v.callbackers { + if err := callbacker.ValidateCallback( + ctx, quote, v.OID(), quoteRaw, nonce, peerPublicKey, + ); err != nil { + return fmt.Errorf("callback failed: %w", err) + } + } + + v.logger.Info("Validate finished successfully") + return nil +} + +func trustedRoots() (*x509.CertPool, error) { + rootCerts := x509.NewCertPool() + if ok := rootCerts.AppendCertsFromPEM(tdxRootCert); !ok { + return nil, fmt.Errorf("failed to append root certificate") + } + return rootCerts, nil +} diff --git a/packages/by-name/contrast/package.nix b/packages/by-name/contrast/package.nix index 6ea496d3b7..bbf72e8138 100644 --- a/packages/by-name/contrast/package.nix +++ b/packages/by-name/contrast/package.nix @@ -145,6 +145,7 @@ buildGoModule rec { (path.append root "internal/manifest/Milan.pem") (path.append root "internal/manifest/Genoa.pem") (path.append root "nodeinstaller") + (path.append root "internal/attestation/tdx/Intel_SGX_Provisioning_Certification_RootCA.pem") (fileset.difference (fileset.fileFilter (file: hasSuffix ".go" file.name) root) ( path.append root "service-mesh" ))