Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

attestation: add TDX validator #768

Merged
merged 3 commits into from
Aug 20, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion cli/cmd/common.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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 {
Expand Down
5 changes: 3 additions & 2 deletions coordinator/internal/authority/credentials.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand All @@ -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",
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
// Copyright 2024 Edgeless Systems GmbH
// SPDX-License-Identifier: AGPL-3.0-only

package snp
package certcache

import (
"log/slog"
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
// Copyright 2024 Edgeless Systems GmbH
// SPDX-License-Identifier: AGPL-3.0-only

package snp
package certcache

import (
"log/slog"
Expand Down
6 changes: 3 additions & 3 deletions internal/attestation/snp/validator.go
Original file line number Diff line number Diff line change
Expand Up @@ -71,15 +71,15 @@ 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 {
return fmt.Errorf("attestation missing report")
}
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))

Expand All @@ -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[:]
Expand Down
Original file line number Diff line number Diff line change
@@ -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-----
170 changes: 170 additions & 0 deletions internal/attestation/tdx/validator.go
Original file line number Diff line number Diff line change
@@ -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
msanft marked this conversation as resolved.
Show resolved Hide resolved

// 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 {
Comment on lines +105 to +106
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why does the Issuer return a marshalled proto quote, instead of just the raw quote?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not sure actually. I think the raw report should even be shorter, as it's non-self-describing too

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
}
1 change: 1 addition & 0 deletions packages/by-name/contrast/package.nix
Original file line number Diff line number Diff line change
Expand Up @@ -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"
))
Expand Down