From d523afc3f98c7115c79888dcf5f409ad4fce8c99 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Wei=C3=9Fe?= Date: Tue, 16 Jan 2024 15:41:02 +0100 Subject: [PATCH] Implement Azure TDX attestation primitives MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Daniel Weiße --- go.mod | 2 +- internal/attestation/azure/BUILD.bazel | 19 ++ internal/attestation/azure/azure.go | 92 +++++++++ internal/attestation/azure/azure_test.go | 165 ++++++++++++++++ internal/attestation/azure/snp/BUILD.bazel | 2 +- internal/attestation/azure/snp/issuer.go | 16 +- internal/attestation/azure/snp/issuer_test.go | 44 ----- internal/attestation/azure/snp/validator.go | 76 +------- .../attestation/azure/snp/validator_test.go | 110 +---------- internal/attestation/azure/tdx/BUILD.bazel | 26 +++ internal/attestation/azure/tdx/issuer.go | 176 ++++++++++++++++++ internal/attestation/azure/tdx/tdx.go | 25 +++ internal/attestation/azure/tdx/validator.go | 122 ++++++++++++ internal/attestation/choose/BUILD.bazel | 1 + internal/attestation/choose/choose.go | 5 + internal/attestation/variant/variant.go | 2 + internal/config/attestation.go | 10 +- internal/config/azure.go | 33 ++++ internal/config/config.go | 36 +++- internal/config/config_doc.go | 51 +++++ 20 files changed, 770 insertions(+), 243 deletions(-) create mode 100644 internal/attestation/azure/azure_test.go create mode 100644 internal/attestation/azure/tdx/BUILD.bazel create mode 100644 internal/attestation/azure/tdx/issuer.go create mode 100644 internal/attestation/azure/tdx/tdx.go create mode 100644 internal/attestation/azure/tdx/validator.go diff --git a/go.mod b/go.mod index 538e48de3e..740732d1cd 100644 --- a/go.mod +++ b/go.mod @@ -258,7 +258,7 @@ require ( github.com/google/go-attestation v0.5.0 // indirect github.com/google/go-cmp v0.6.0 // indirect github.com/google/go-containerregistry v0.15.2 // indirect - github.com/google/go-tdx-guest v0.2.3-0.20231011100059-4cf02bed9d33 // indirect + github.com/google/go-tdx-guest v0.2.3-0.20231011100059-4cf02bed9d33 github.com/google/go-tspi v0.3.0 // indirect github.com/google/gofuzz v1.2.0 // indirect github.com/google/logger v1.1.1 // indirect diff --git a/internal/attestation/azure/BUILD.bazel b/internal/attestation/azure/BUILD.bazel index 93cc51ed5d..57f5d242b3 100644 --- a/internal/attestation/azure/BUILD.bazel +++ b/internal/attestation/azure/BUILD.bazel @@ -1,8 +1,27 @@ load("@io_bazel_rules_go//go:def.bzl", "go_library") +load("//bazel/go:go_test.bzl", "go_test") go_library( name = "azure", srcs = ["azure.go"], importpath = "github.com/edgelesssys/constellation/v2/internal/attestation/azure", visibility = ["//:__subpackages__"], + deps = [ + "@com_github_google_go_tpm//legacy/tpm2", + "@com_github_google_go_tpm_tools//client", + ], +) + +go_test( + name = "azure_test", + srcs = ["azure_test.go"], + embed = [":azure"], + deps = [ + "//internal/attestation/simulator", + "//internal/attestation/snp", + "@com_github_google_go_tpm//legacy/tpm2", + "@com_github_google_go_tpm_tools//client", + "@com_github_stretchr_testify//assert", + "@com_github_stretchr_testify//require", + ], ) diff --git a/internal/attestation/azure/azure.go b/internal/attestation/azure/azure.go index 53e6a9466c..411ad7ce48 100644 --- a/internal/attestation/azure/azure.go +++ b/internal/attestation/azure/azure.go @@ -18,3 +18,95 @@ Constellation supports multiple attestation technologies on Azure. Basic TPM attestation. */ package azure + +import ( + "bytes" + "crypto/sha256" + "encoding/base64" + "encoding/binary" + "encoding/json" + "errors" + "fmt" + "io" + + tpmclient "github.com/google/go-tpm-tools/client" + "github.com/google/go-tpm/legacy/tpm2" +) + +const ( + // tpmAkIdx is the NV index of the attestation key used by Azure VMs. + tpmAkIdx = 0x81000003 +) + +// GetAttestationKey reads the attestation key put into the TPM during early boot. +func GetAttestationKey(tpm io.ReadWriter) (*tpmclient.Key, error) { + ak, err := tpmclient.LoadCachedKey(tpm, tpmAkIdx, tpmclient.NullSession{}) + if err != nil { + return nil, fmt.Errorf("reading HCL attestation key from TPM: %w", err) + } + + return ak, nil +} + +// HCLAkValidator validates an attestation key issued by the Host Compatibility Layer (HCL). +// The HCL is written by Azure, and sits between the Hypervisor and CVM OS. +// The HCL runs in the protected context of the CVM. +type HCLAkValidator struct{} + +// Validate validates that the attestation key from the TPM is trustworthy. The steps are: +// 1. runtime data read from the TPM has the same sha256 digest as reported in `report_data` of the SNP report or `TdQuoteBody.ReportData` of the TDX report. +// 2. modulus reported in runtime data matches modulus from key at idx 0x81000003. +// 3. exponent reported in runtime data matches exponent from key at idx 0x81000003. +// The function is currently tested manually on a Azure Ubuntu CVM. +func (a *HCLAkValidator) Validate(runtimeDataRaw []byte, reportData []byte, rsaParameters *tpm2.RSAParams) error { + var rtData runtimeData + if err := json.Unmarshal(runtimeDataRaw, &rtData); err != nil { + return fmt.Errorf("unmarshalling json: %w", err) + } + + sum := sha256.Sum256(runtimeDataRaw) + if len(reportData) < len(sum) { + return fmt.Errorf("reportData has unexpected size: %d", len(reportData)) + } + if !bytes.Equal(sum[:], reportData[:len(sum)]) { + return errors.New("unexpected runtimeData digest in TPM") + } + + if len(rtData.PublicPart) < 1 { + return errors.New("did not receive any keys in runtime data") + } + rawN, err := base64.RawURLEncoding.DecodeString(rtData.PublicPart[0].N) + if err != nil { + return fmt.Errorf("decoding modulus string: %w", err) + } + if !bytes.Equal(rawN, rsaParameters.ModulusRaw) { + return fmt.Errorf("unexpected modulus value in TPM") + } + + rawE, err := base64.RawURLEncoding.DecodeString(rtData.PublicPart[0].E) + if err != nil { + return fmt.Errorf("decoding exponent string: %w", err) + } + paddedRawE := make([]byte, 4) + copy(paddedRawE, rawE) + exponent := binary.LittleEndian.Uint32(paddedRawE) + + // According to this comment [1] the TPM uses "0" to represent the default exponent "65537". + // The go tpm library also reports the exponent as 0. Thus we have to handle it specially. + // [1] https://github.com/tpm2-software/tpm2-tools/pull/1973#issue-596685005 + if !((exponent == 65537 && rsaParameters.ExponentRaw == 0) || exponent == rsaParameters.ExponentRaw) { + return fmt.Errorf("unexpected N value in TPM") + } + + return nil +} + +type runtimeData struct { + PublicPart []akPub `json:"keys"` +} + +// akPub are the public parameters of an RSA attestation key. +type akPub struct { + E string `json:"e"` + N string `json:"n"` +} diff --git a/internal/attestation/azure/azure_test.go b/internal/attestation/azure/azure_test.go new file mode 100644 index 0000000000..0e23f1fceb --- /dev/null +++ b/internal/attestation/azure/azure_test.go @@ -0,0 +1,165 @@ +/* +Copyright (c) Edgeless Systems GmbH + +SPDX-License-Identifier: AGPL-3.0-only +*/ + +package azure + +import ( + "bytes" + "crypto/sha256" + "encoding/base64" + "encoding/binary" + "encoding/json" + "os" + "testing" + + "github.com/edgelesssys/constellation/v2/internal/attestation/simulator" + "github.com/edgelesssys/constellation/v2/internal/attestation/snp" + "github.com/google/go-tpm-tools/client" + tpmclient "github.com/google/go-tpm-tools/client" + "github.com/google/go-tpm/legacy/tpm2" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +// TestValidateAk tests the attestation key validation with a simulated TPM device. +func TestValidateAk(t *testing.T) { + cgo := os.Getenv("CGO_ENABLED") + if cgo == "0" { + t.Skip("skipping test because CGO is disabled and tpm simulator requires it") + } + + int32ToBytes := func(val uint32) []byte { + r := make([]byte, 4) + binary.PutUvarint(r, uint64(val)) + return r + } + + require := require.New(t) + + tpm, err := simulator.OpenSimulatedTPM() + require.NoError(err) + defer tpm.Close() + key, err := client.AttestationKeyRSA(tpm) + require.NoError(err) + defer key.Close() + + e := base64.RawURLEncoding.EncodeToString(int32ToBytes(key.PublicArea().RSAParameters.ExponentRaw)) + n := base64.RawURLEncoding.EncodeToString(key.PublicArea().RSAParameters.ModulusRaw) + ak := akPub{E: e, N: n} + rtData := runtimeData{PublicPart: []akPub{ak}} + + defaultRuntimeDataRaw, err := json.Marshal(rtData) + require.NoError(err) + defaultInstanceInfo := snp.InstanceInfo{Azure: &snp.AzureInstanceInfo{RuntimeData: defaultRuntimeDataRaw}} + + sig := sha256.Sum256(defaultRuntimeDataRaw) + defaultReportData := sig[:] + defaultRsaParams := key.PublicArea().RSAParameters + + testCases := map[string]struct { + instanceInfo snp.InstanceInfo + runtimeDataRaw []byte + reportData []byte + rsaParameters *tpm2.RSAParams + wantErr bool + }{ + "success": { + instanceInfo: defaultInstanceInfo, + runtimeDataRaw: defaultRuntimeDataRaw, + reportData: defaultReportData, + rsaParameters: defaultRsaParams, + }, + "invalid json": { + instanceInfo: defaultInstanceInfo, + runtimeDataRaw: []byte(""), + reportData: defaultReportData, + rsaParameters: defaultRsaParams, + wantErr: true, + }, + "invalid hash": { + instanceInfo: defaultInstanceInfo, + runtimeDataRaw: defaultRuntimeDataRaw, + reportData: bytes.Repeat([]byte{0}, 64), + rsaParameters: defaultRsaParams, + wantErr: true, + }, + "invalid E": { + instanceInfo: defaultInstanceInfo, + runtimeDataRaw: defaultRuntimeDataRaw, + reportData: defaultReportData, + rsaParameters: func() *tpm2.RSAParams { + tmp := *defaultRsaParams + tmp.ExponentRaw = 1 + return &tmp + }(), + wantErr: true, + }, + "invalid N": { + instanceInfo: defaultInstanceInfo, + runtimeDataRaw: defaultRuntimeDataRaw, + reportData: defaultReportData, + rsaParameters: func() *tpm2.RSAParams { + tmp := *defaultRsaParams + tmp.ModulusRaw = []byte{0, 1, 2, 3} + return &tmp + }(), + wantErr: true, + }, + } + + for name, tc := range testCases { + t.Run(name, func(t *testing.T) { + assert := assert.New(t) + ak := HCLAkValidator{} + err = ak.Validate(tc.runtimeDataRaw, tc.reportData, tc.rsaParameters) + if tc.wantErr { + assert.Error(err) + } else { + assert.NoError(err) + } + }) + } +} + +// TestGetHCLAttestationKey is a basic smoke test that only checks if GetAttestationKey can be run error free. +// Testing anything else will only verify that the simulator works as expected, since GetAttestationKey +// only retrieves the attestation key from the TPM. +func TestGetHCLAttestationKey(t *testing.T) { + cgo := os.Getenv("CGO_ENABLED") + if cgo == "0" { + t.Skip("skipping test because CGO is disabled and tpm simulator requires it") + } + require := require.New(t) + assert := assert.New(t) + + tpm, err := simulator.OpenSimulatedTPM() + require.NoError(err) + defer tpm.Close() + + // we should receive an error if no key was saved at index `tpmAkIdx` + _, err = GetAttestationKey(tpm) + assert.Error(err) + + // create a key at the index + tpmAk, err := tpmclient.NewCachedKey(tpm, tpm2.HandleOwner, tpm2.Public{ + Type: tpm2.AlgRSA, + NameAlg: tpm2.AlgSHA256, + Attributes: tpm2.FlagFixedTPM | tpm2.FlagFixedParent | tpm2.FlagSensitiveDataOrigin | tpm2.FlagUserWithAuth | tpm2.FlagNoDA | tpm2.FlagRestricted | tpm2.FlagSign, + RSAParameters: &tpm2.RSAParams{ + Sign: &tpm2.SigScheme{ + Alg: tpm2.AlgRSASSA, + Hash: tpm2.AlgSHA256, + }, + KeyBits: 2048, + }, + }, tpmAkIdx) + require.NoError(err) + defer tpmAk.Close() + + // we should now be able to retrieve the key + _, err = GetAttestationKey(tpm) + assert.NoError(err) +} diff --git a/internal/attestation/azure/snp/BUILD.bazel b/internal/attestation/azure/snp/BUILD.bazel index 7adea0c3e5..43cdd1afa7 100644 --- a/internal/attestation/azure/snp/BUILD.bazel +++ b/internal/attestation/azure/snp/BUILD.bazel @@ -14,6 +14,7 @@ go_library( visibility = ["//:__subpackages__"], deps = [ "//internal/attestation", + "//internal/attestation/azure", "//internal/attestation/idkeydigest", "//internal/attestation/snp", "//internal/attestation/variant", @@ -28,7 +29,6 @@ go_library( "@com_github_google_go_sev_guest//verify", "@com_github_google_go_sev_guest//verify/trust", "@com_github_google_go_tpm//legacy/tpm2", - "@com_github_google_go_tpm_tools//client", "@com_github_google_go_tpm_tools//proto/attest", ], ) diff --git a/internal/attestation/azure/snp/issuer.go b/internal/attestation/azure/snp/issuer.go index 3280a731c3..f0e8bb6f06 100644 --- a/internal/attestation/azure/snp/issuer.go +++ b/internal/attestation/azure/snp/issuer.go @@ -13,15 +13,13 @@ import ( "io" "github.com/edgelesssys/constellation/v2/internal/attestation" + "github.com/edgelesssys/constellation/v2/internal/attestation/azure" "github.com/edgelesssys/constellation/v2/internal/attestation/snp" "github.com/edgelesssys/constellation/v2/internal/attestation/variant" "github.com/edgelesssys/constellation/v2/internal/attestation/vtpm" "github.com/edgelesssys/go-azguestattestation/maa" - tpmclient "github.com/google/go-tpm-tools/client" ) -const tpmAkIdx = 0x81000003 - // Issuer for Azure TPM attestation. type Issuer struct { variant.AzureSEVSNP @@ -40,7 +38,7 @@ func NewIssuer(log attestation.Logger) *Issuer { i.Issuer = vtpm.NewIssuer( vtpm.OpenVTPM, - getAttestationKey, + azure.GetAttestationKey, i.getInstanceInfo, log, ) @@ -83,16 +81,6 @@ func (i *Issuer) getInstanceInfo(ctx context.Context, tpm io.ReadWriteCloser, us return statement, nil } -// getAttestationKey reads the attestation key put into the TPM during early boot. -func getAttestationKey(tpm io.ReadWriter) (*tpmclient.Key, error) { - ak, err := tpmclient.LoadCachedKey(tpm, tpmAkIdx, tpmclient.NullSession{}) - if err != nil { - return nil, fmt.Errorf("reading HCL attestation key from TPM: %w", err) - } - - return ak, nil -} - type imdsAPI interface { getMAAURL(ctx context.Context) (string, error) } diff --git a/internal/attestation/azure/snp/issuer_test.go b/internal/attestation/azure/snp/issuer_test.go index 81f6d6df1e..224937be24 100644 --- a/internal/attestation/azure/snp/issuer_test.go +++ b/internal/attestation/azure/snp/issuer_test.go @@ -11,14 +11,10 @@ import ( "encoding/json" "errors" "io" - "os" "testing" - "github.com/edgelesssys/constellation/v2/internal/attestation/simulator" "github.com/edgelesssys/constellation/v2/internal/attestation/snp" "github.com/edgelesssys/go-azguestattestation/maa" - tpmclient "github.com/google/go-tpm-tools/client" - "github.com/google/go-tpm/legacy/tpm2" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) @@ -113,46 +109,6 @@ func TestGetSNPAttestation(t *testing.T) { } } -// TestGetHCLAttestationKey is a basic smoke test that only checks if getAkPub can be run error free. -// Testing anything else will only verify that the simulator works as expected, since getAkPub -// only retrieves the attestation key from the TPM. -func TestGetHCLAttestationKey(t *testing.T) { - cgo := os.Getenv("CGO_ENABLED") - if cgo == "0" { - t.Skip("skipping test because CGO is disabled and tpm simulator requires it") - } - require := require.New(t) - assert := assert.New(t) - - tpm, err := simulator.OpenSimulatedTPM() - require.NoError(err) - defer tpm.Close() - - // we should receive an error if no key was saved at index `tpmAkIdx` - _, err = getAttestationKey(tpm) - assert.Error(err) - - // create a key at the index - tpmAk, err := tpmclient.NewCachedKey(tpm, tpm2.HandleOwner, tpm2.Public{ - Type: tpm2.AlgRSA, - NameAlg: tpm2.AlgSHA256, - Attributes: tpm2.FlagFixedTPM | tpm2.FlagFixedParent | tpm2.FlagSensitiveDataOrigin | tpm2.FlagUserWithAuth | tpm2.FlagNoDA | tpm2.FlagRestricted | tpm2.FlagSign, - RSAParameters: &tpm2.RSAParams{ - Sign: &tpm2.SigScheme{ - Alg: tpm2.AlgRSASSA, - Hash: tpm2.AlgSHA256, - }, - KeyBits: 2048, - }, - }, tpmAkIdx) - require.NoError(err) - defer tpmAk.Close() - - // we should now be able to retrieve the key - _, err = getAttestationKey(tpm) - assert.NoError(err) -} - type stubImdsClient struct { maaURL string apiError error diff --git a/internal/attestation/azure/snp/validator.go b/internal/attestation/azure/snp/validator.go index 856e528ffa..0d971a4187 100644 --- a/internal/attestation/azure/snp/validator.go +++ b/internal/attestation/azure/snp/validator.go @@ -10,15 +10,13 @@ import ( "bytes" "context" "crypto" - "crypto/sha256" "crypto/x509" - "encoding/base64" - "encoding/binary" "encoding/json" "errors" "fmt" "github.com/edgelesssys/constellation/v2/internal/attestation" + "github.com/edgelesssys/constellation/v2/internal/attestation/azure" "github.com/edgelesssys/constellation/v2/internal/attestation/idkeydigest" "github.com/edgelesssys/constellation/v2/internal/attestation/snp" "github.com/edgelesssys/constellation/v2/internal/attestation/variant" @@ -78,7 +76,7 @@ func NewValidator(cfg *config.AzureSEVSNP, log attestation.Logger) *Validator { log = nopAttestationLogger{} } v := &Validator{ - hclValidator: &attestationKey{}, + hclValidator: &azure.HCLAkValidator{}, maa: newMAAClient(), config: cfg, log: log, @@ -191,7 +189,7 @@ func (v *Validator) getTrustedKey(ctx context.Context, attDoc vtpm.AttestationDo if err != nil { return nil, err } - if err = v.hclValidator.validate(instanceInfo.Azure.RuntimeData, att.Report.ReportData, pubArea.RSAParameters); err != nil { + if err = v.hclValidator.Validate(instanceInfo.Azure.RuntimeData, att.Report.ReportData, pubArea.RSAParameters); err != nil { return nil, fmt.Errorf("validating HCLAkPub: %w", err) } @@ -238,70 +236,6 @@ func (v *Validator) checkIDKeyDigest(ctx context.Context, report *spb.Attestatio return nil } -type attestationKey struct { - PublicPart []akPub `json:"keys"` -} - -// validate validates that the attestation key from the TPM is trustworthy. The steps are: -// 1. runtime data read from the TPM has the same sha256 digest as reported in `report_data` of the SNP report. -// 2. modulus reported in runtime data matches modulus from key at idx 0x81000003. -// 3. exponent reported in runtime data matches exponent from key at idx 0x81000003. -// The function is currently tested manually on a Azure Ubuntu CVM. -func (a *attestationKey) validate(runtimeDataRaw []byte, reportData []byte, rsaParameters *tpm2.RSAParams) error { - if err := json.Unmarshal(runtimeDataRaw, a); err != nil { - return fmt.Errorf("unmarshalling json: %w", err) - } - - sum := sha256.Sum256(runtimeDataRaw) - if len(reportData) < len(sum) { - return fmt.Errorf("reportData has unexpected size: %d", len(reportData)) - } - if !bytes.Equal(sum[:], reportData[:len(sum)]) { - return errors.New("unexpected runtimeData digest in TPM") - } - - if len(a.PublicPart) < 1 { - return errors.New("did not receive any keys in runtime data") - } - rawN, err := base64.RawURLEncoding.DecodeString(a.PublicPart[0].N) - if err != nil { - return fmt.Errorf("decoding modulus string: %w", err) - } - if !bytes.Equal(rawN, rsaParameters.ModulusRaw) { - return fmt.Errorf("unexpected modulus value in TPM") - } - - rawE, err := base64.RawURLEncoding.DecodeString(a.PublicPart[0].E) - if err != nil { - return fmt.Errorf("decoding exponent string: %w", err) - } - paddedRawE := make([]byte, 4) - copy(paddedRawE, rawE) - exponent := binary.LittleEndian.Uint32(paddedRawE) - - // According to this comment [1] the TPM uses "0" to represent the default exponent "65537". - // The go tpm library also reports the exponent as 0. Thus we have to handle it specially. - // [1] https://github.com/tpm2-software/tpm2-tools/pull/1973#issue-596685005 - if !((exponent == 65537 && rsaParameters.ExponentRaw == 0) || exponent == rsaParameters.ExponentRaw) { - return fmt.Errorf("unexpected N value in TPM") - } - - return nil -} - -// hclAkValidator validates an attestation key issued by the Host Compatibility Layer (HCL). -// The HCL is written by Azure, and sits between the Hypervisor and CVM OS. -// The HCL runs in the protected context of the CVM. -type hclAkValidator interface { - validate(runtimeDataRaw []byte, reportData []byte, rsaParameters *tpm2.RSAParams) error -} - -// akPub are the public parameters of an RSA attestation key. -type akPub struct { - E string `json:"e"` - N string `json:"n"` -} - // nopAttestationLogger is a no-op implementation of AttestationLogger. type nopAttestationLogger struct{} @@ -314,3 +248,7 @@ func (nopAttestationLogger) Warnf(string, ...interface{}) {} type maaValidator interface { validateToken(ctx context.Context, maaURL string, token string, extraData []byte) error } + +type hclAkValidator interface { + Validate(runtimeDataRaw []byte, reportData []byte, rsaParameters *tpm2.RSAParams) error +} diff --git a/internal/attestation/azure/snp/validator_test.go b/internal/attestation/azure/snp/validator_test.go index 2b2c9a2414..64d6f03e2f 100644 --- a/internal/attestation/azure/snp/validator_test.go +++ b/internal/attestation/azure/snp/validator_test.go @@ -10,8 +10,6 @@ import ( "bytes" "context" "crypto/sha256" - "encoding/base64" - "encoding/binary" "encoding/hex" "encoding/json" "errors" @@ -23,7 +21,6 @@ import ( "github.com/edgelesssys/constellation/v2/internal/attestation" "github.com/edgelesssys/constellation/v2/internal/attestation/idkeydigest" "github.com/edgelesssys/constellation/v2/internal/attestation/simulator" - "github.com/edgelesssys/constellation/v2/internal/attestation/snp" "github.com/edgelesssys/constellation/v2/internal/attestation/snp/testdata" "github.com/edgelesssys/constellation/v2/internal/attestation/vtpm" "github.com/edgelesssys/constellation/v2/internal/config" @@ -203,107 +200,6 @@ func (v *stubMaaValidator) validateToken(_ context.Context, _ string, _ string, return v.validateTokenErr } -// TestValidateAk tests the attestation key validation with a simulated TPM device. -func TestValidateAk(t *testing.T) { - cgo := os.Getenv("CGO_ENABLED") - if cgo == "0" { - t.Skip("skipping test because CGO is disabled and tpm simulator requires it") - } - - int32ToBytes := func(val uint32) []byte { - r := make([]byte, 4) - binary.PutUvarint(r, uint64(val)) - return r - } - - require := require.New(t) - - tpm, err := simulator.OpenSimulatedTPM() - require.NoError(err) - defer tpm.Close() - key, err := client.AttestationKeyRSA(tpm) - require.NoError(err) - defer key.Close() - - e := base64.RawURLEncoding.EncodeToString(int32ToBytes(key.PublicArea().RSAParameters.ExponentRaw)) - n := base64.RawURLEncoding.EncodeToString(key.PublicArea().RSAParameters.ModulusRaw) - - ak := akPub{E: e, N: n} - runtimeData := attestationKey{PublicPart: []akPub{ak}} - - defaultRuntimeDataRaw, err := json.Marshal(runtimeData) - require.NoError(err) - defaultInstanceInfo := snp.InstanceInfo{Azure: &snp.AzureInstanceInfo{RuntimeData: defaultRuntimeDataRaw}} - - sig := sha256.Sum256(defaultRuntimeDataRaw) - defaultReportData := sig[:] - defaultRsaParams := key.PublicArea().RSAParameters - - testCases := map[string]struct { - instanceInfo snp.InstanceInfo - runtimeDataRaw []byte - reportData []byte - rsaParameters *tpm2.RSAParams - wantErr bool - }{ - "success": { - instanceInfo: defaultInstanceInfo, - runtimeDataRaw: defaultRuntimeDataRaw, - reportData: defaultReportData, - rsaParameters: defaultRsaParams, - }, - "invalid json": { - instanceInfo: defaultInstanceInfo, - runtimeDataRaw: []byte(""), - reportData: defaultReportData, - rsaParameters: defaultRsaParams, - wantErr: true, - }, - "invalid hash": { - instanceInfo: defaultInstanceInfo, - runtimeDataRaw: defaultRuntimeDataRaw, - reportData: bytes.Repeat([]byte{0}, 64), - rsaParameters: defaultRsaParams, - wantErr: true, - }, - "invalid E": { - instanceInfo: defaultInstanceInfo, - runtimeDataRaw: defaultRuntimeDataRaw, - reportData: defaultReportData, - rsaParameters: func() *tpm2.RSAParams { - tmp := *defaultRsaParams - tmp.ExponentRaw = 1 - return &tmp - }(), - wantErr: true, - }, - "invalid N": { - instanceInfo: defaultInstanceInfo, - runtimeDataRaw: defaultRuntimeDataRaw, - reportData: defaultReportData, - rsaParameters: func() *tpm2.RSAParams { - tmp := *defaultRsaParams - tmp.ModulusRaw = []byte{0, 1, 2, 3} - return &tmp - }(), - wantErr: true, - }, - } - - for name, tc := range testCases { - t.Run(name, func(t *testing.T) { - assert := assert.New(t) - ak := attestationKey{} - err = ak.validate(tc.runtimeDataRaw, tc.reportData, tc.rsaParameters) - if tc.wantErr { - assert.Error(err) - } else { - assert.NoError(err) - } - }) - } -} - // TestGetTrustedKey tests the verification and validation of attestation report. func TestTrustedKeyFromSNP(t *testing.T) { cgo := os.Getenv("CGO_ENABLED") @@ -824,11 +720,9 @@ func newStubInstanceInfo(vcek, certChain []byte, report, runtimeData string) (st }, nil } -type stubAttestationKey struct { - PublicPart []akPub -} +type stubAttestationKey struct{} -func (s *stubAttestationKey) validate(runtimeDataRaw []byte, reportData []byte, _ *tpm2.RSAParams) error { +func (s *stubAttestationKey) Validate(runtimeDataRaw []byte, reportData []byte, _ *tpm2.RSAParams) error { if err := json.Unmarshal(runtimeDataRaw, s); err != nil { return fmt.Errorf("unmarshalling json: %w", err) } diff --git a/internal/attestation/azure/tdx/BUILD.bazel b/internal/attestation/azure/tdx/BUILD.bazel new file mode 100644 index 0000000000..45deddd1c0 --- /dev/null +++ b/internal/attestation/azure/tdx/BUILD.bazel @@ -0,0 +1,26 @@ +load("@io_bazel_rules_go//go:def.bzl", "go_library") + +go_library( + name = "tdx", + srcs = [ + "issuer.go", + "tdx.go", + "validator.go", + ], + importpath = "github.com/edgelesssys/constellation/v2/internal/attestation/azure/tdx", + visibility = ["//:__subpackages__"], + deps = [ + "//internal/attestation", + "//internal/attestation/azure", + "//internal/attestation/variant", + "//internal/attestation/vtpm", + "//internal/config", + "@com_github_google_go_tdx_guest//abi", + "@com_github_google_go_tdx_guest//proto/tdx", + "@com_github_google_go_tdx_guest//validate", + "@com_github_google_go_tdx_guest//verify", + "@com_github_google_go_tdx_guest//verify/trust", + "@com_github_google_go_tpm//legacy/tpm2", + "@com_github_google_go_tpm_tools//proto/attest", + ], +) diff --git a/internal/attestation/azure/tdx/issuer.go b/internal/attestation/azure/tdx/issuer.go new file mode 100644 index 0000000000..0b8d838a3e --- /dev/null +++ b/internal/attestation/azure/tdx/issuer.go @@ -0,0 +1,176 @@ +/* +Copyright (c) Edgeless Systems GmbH + +SPDX-License-Identifier: AGPL-3.0-only +*/ + +package tdx + +import ( + "bytes" + "context" + "encoding/base64" + "encoding/binary" + "encoding/json" + "fmt" + "io" + "net/http" + + "github.com/edgelesssys/constellation/v2/internal/attestation" + "github.com/edgelesssys/constellation/v2/internal/attestation/azure" + "github.com/edgelesssys/constellation/v2/internal/attestation/variant" + "github.com/edgelesssys/constellation/v2/internal/attestation/vtpm" + "github.com/google/go-tpm/legacy/tpm2" +) + +const ( + imdsURL = "http://169.254.169.254/acc/tdquote" + indexHCLReport = 0x1400001 + hclDataOffset = 1216 + hclReportTypeOffset = 8 + hclReportTypeOffsetStart = hclDataOffset + hclReportTypeOffset + hclRequestDataSizeOffset = 16 + runtimeDataSizeOffset = hclDataOffset + hclRequestDataSizeOffset + hclRequestDataOffset = 20 + runtimeDataOffset = hclDataOffset + hclRequestDataOffset + tdReportSize = 1024 + hwReportStart = 32 + hwReportEnd = 1216 +) + +const ( + hclReportTypeInvalid uint32 = iota + hclReportTypeReserved + hclReportTypeSNP + hclReportTypeTVM + hclReportTypeTDX +) + +// Issuer for Azure confidential VM attestation using TDX. +type Issuer struct { + variant.AzureTDX + *vtpm.Issuer + + quoteGetter quoteGetter +} + +// NewIssuer initializes a new Azure Issuer. +func NewIssuer(log attestation.Logger) *Issuer { + i := &Issuer{ + quoteGetter: imdsQuoteGetter{ + client: &http.Client{Transport: &http.Transport{Proxy: nil}}, + }, + } + + i.Issuer = vtpm.NewIssuer( + vtpm.OpenVTPM, + azure.GetAttestationKey, + i.getInstanceInfo, + log, + ) + return i +} + +func (i *Issuer) getInstanceInfo(ctx context.Context, tpm io.ReadWriteCloser, _ []byte) ([]byte, error) { + // Read HCL report from TPM + report, err := tpm2.NVReadEx(tpm, indexHCLReport, tpm2.HandleOwner, "", 0) + if err != nil { + return nil, err + } + + // Parse the report and get quote + instanceInfo, err := i.getHCLReport(ctx, report) + if err != nil { + return nil, fmt.Errorf("getting HCL report: %w", err) + } + + instanceInfoJSON, err := json.Marshal(instanceInfo) + if err != nil { + return nil, fmt.Errorf("marshalling instance info: %w", err) + } + return instanceInfoJSON, nil +} + +func (i *Issuer) getHCLReport(ctx context.Context, report []byte) (instanceInfo, error) { + // First, ensure the extracted report is actually for TDX + if len(report) < runtimeDataSizeOffset+4 { + return instanceInfo{}, fmt.Errorf("invalid HCL report: expected at least %d bytes to read HCL report type, got %d", runtimeDataSizeOffset+4, len(report)) + } + reportType := binary.LittleEndian.Uint32(report[hclReportTypeOffsetStart : hclReportTypeOffsetStart+4]) + if reportType != hclReportTypeTDX { + return instanceInfo{}, fmt.Errorf("invalid HCL report type: expected TDX (%d), got %d", hclReportTypeTDX, reportType) + } + + // We need the td report (generally called HW report in Azure's samples) from the HCL report to send to the IMDS API + if len(report) < hwReportStart+tdReportSize { + return instanceInfo{}, fmt.Errorf("invalid HCL report: expected at least %d bytes to read td report, got %d", hwReportStart+tdReportSize, len(report)) + } + hwReport := report[hwReportStart : hwReportStart+tdReportSize] + + // We also need the runtime data to verify the attestation key later on the validator side + if len(report) < runtimeDataSizeOffset+4 { + return instanceInfo{}, fmt.Errorf("invalid HCL report: expected at least %d bytes to read runtime data size, got %d", runtimeDataSizeOffset+4, len(report)) + } + runtimeDataSize := binary.LittleEndian.Uint32(report[runtimeDataSizeOffset : runtimeDataSizeOffset+4]) + if len(report) < runtimeDataOffset+int(runtimeDataSize) { + return instanceInfo{}, fmt.Errorf("invalid HCL report: expected at least %d bytes to read runtime data, got %d", runtimeDataOffset+int(runtimeDataSize), len(report)) + } + runtimeData := report[runtimeDataOffset : runtimeDataOffset+runtimeDataSize] + + quote, err := i.quoteGetter.getQuote(ctx, hwReport) + if err != nil { + return instanceInfo{}, fmt.Errorf("getting quote: %w", err) + } + + return instanceInfo{ + AttestationReport: quote, + RuntimeData: runtimeData, + }, nil +} + +type imdsQuoteGetter struct { + client *http.Client +} + +func (i imdsQuoteGetter) getQuote(ctx context.Context, hwReport []byte) ([]byte, error) { + encodedReportJSON, err := json.Marshal(quoteRequest{ + Report: base64.RawURLEncoding.EncodeToString(hwReport), + }) + if err != nil { + return nil, fmt.Errorf("marshalling encoded report: %w", err) + } + + req, err := http.NewRequestWithContext(ctx, http.MethodPost, imdsURL, bytes.NewReader(encodedReportJSON)) + if err != nil { + return nil, fmt.Errorf("creating request: %w", err) + } + req.Header.Add("Content-Type", "application/json") + + res, err := i.client.Do(req) + if err != nil { + return nil, fmt.Errorf("sending request: %w", err) + } + defer res.Body.Close() + + if res.StatusCode != http.StatusOK { + return nil, fmt.Errorf("unexpected status code: %d", res.StatusCode) + } + var quoteRes quoteResponse + if err := json.NewDecoder(res.Body).Decode("eRes); err != nil { + return nil, fmt.Errorf("decoding response: %w", err) + } + + return base64.RawURLEncoding.DecodeString(quoteRes.Quote) +} + +type quoteRequest struct { + Report string `json:"report"` +} + +type quoteResponse struct { + Quote string `json:"quote"` +} + +type quoteGetter interface { + getQuote(ctx context.Context, encodedHWReport []byte) ([]byte, error) +} diff --git a/internal/attestation/azure/tdx/tdx.go b/internal/attestation/azure/tdx/tdx.go new file mode 100644 index 0000000000..815a43ae20 --- /dev/null +++ b/internal/attestation/azure/tdx/tdx.go @@ -0,0 +1,25 @@ +/* +Copyright (c) Edgeless Systems GmbH + +SPDX-License-Identifier: AGPL-3.0-only +*/ + +/* +package tdx implements attestation for TDX on Azure. + +Quotes are generated using an Azure provided vTPM and the IMDS API. +They are verified using the go-tdx-guest library. + +More specifically: +- The vTPM is used to collected a TPM attestation and a Hardware Compatibility Layer (HCL) report. +- The HCL report is sent to the IMDS API to generate a TDX quote. +- The quote is verified using the go-tdx-guest library. +- The quote's report data can be used to verify the TPM's attestation key. +- The attestation key can be used to verify the TPM attestation. +*/ +package tdx + +type instanceInfo struct { + AttestationReport []byte + RuntimeData []byte +} diff --git a/internal/attestation/azure/tdx/validator.go b/internal/attestation/azure/tdx/validator.go new file mode 100644 index 0000000000..53ab301735 --- /dev/null +++ b/internal/attestation/azure/tdx/validator.go @@ -0,0 +1,122 @@ +/* +Copyright (c) Edgeless Systems GmbH + +SPDX-License-Identifier: AGPL-3.0-only +*/ + +package tdx + +import ( + "context" + "crypto" + "crypto/x509" + "encoding/json" + "fmt" + + "github.com/edgelesssys/constellation/v2/internal/attestation" + "github.com/edgelesssys/constellation/v2/internal/attestation/azure" + "github.com/edgelesssys/constellation/v2/internal/attestation/variant" + "github.com/edgelesssys/constellation/v2/internal/attestation/vtpm" + "github.com/edgelesssys/constellation/v2/internal/config" + "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/google/go-tpm-tools/proto/attest" + "github.com/google/go-tpm/legacy/tpm2" +) + +// Validator for Azure confidential VM attestation using TDX. +type Validator struct { + variant.AzureTDX + *vtpm.Validator + cfg *config.AzureTDX + + getter trust.HTTPSGetter + hclValidator hclAkValidator +} + +// NewValidator returns a new Validator for Azure confidential VM attestation using TDX. +func NewValidator(cfg *config.AzureTDX, log attestation.Logger) *Validator { + v := &Validator{ + cfg: cfg, + getter: trust.DefaultHTTPSGetter(), + hclValidator: &azure.HCLAkValidator{}, + } + + v.Validator = vtpm.NewValidator( + cfg.Measurements, + v.getTrustedTPMKey, + func(vtpm.AttestationDocument, *attest.MachineState) error { + return nil + }, + log, + ) + + return v +} + +func (v *Validator) getTrustedTPMKey(_ context.Context, attDoc vtpm.AttestationDocument, _ []byte) (crypto.PublicKey, error) { + var instanceInfo instanceInfo + if err := json.Unmarshal(attDoc.InstanceInfo, &instanceInfo); err != nil { + return nil, err + } + + quote, err := abi.QuoteToProto(instanceInfo.AttestationReport) + if err != nil { + return nil, err + } + + if err := v.validateQuote(quote); err != nil { + return nil, err + } + + // Decode the public area of the attestation key and validate its trustworthiness. + pubArea, err := tpm2.DecodePublic(attDoc.Attestation.AkPub) + if err != nil { + return nil, err + } + if err = v.hclValidator.Validate(instanceInfo.RuntimeData, quote.TdQuoteBody.ReportData, pubArea.RSAParameters); err != nil { + return nil, fmt.Errorf("validating HCLAkPub: %w", err) + } + + return pubArea.Key() +} + +func (v *Validator) validateQuote(tdxQuote *tdx.QuoteV4) error { + roots := x509.NewCertPool() + roots.AddCert((*x509.Certificate)(&v.cfg.IntelRootKey)) + + if err := verify.TdxQuote(tdxQuote, &verify.Options{ + // TODO: Re-enable CRL checking once issues on Azure's side are resolved. + // CheckRevocations: true, + // GetCollateral: true, + TrustedRoots: roots, + Getter: v.getter, + }); err != nil { + return err + } + + // TODO: enable checks for QeVendorID, MrSeam, and Xfam + if err := validate.TdxQuote(tdxQuote, &validate.Options{ + HeaderOptions: validate.HeaderOptions{ + MinimumQeSvn: v.cfg.QESVN, + MinimumPceSvn: v.cfg.PCESVN, + // QeVendorID: v.cfg.QEVendorID[:], + }, + TdQuoteBodyOptions: validate.TdQuoteBodyOptions{ + MinimumTeeTcbSvn: v.cfg.TEETCBSVN[:], + // MrSeam: v.cfg.MRSeam[:], + // Xfam: v.cfg.XFAM[:], + }, + }); err != nil { + return err + } + + return nil +} + +type hclAkValidator interface { + Validate(runtimeDataRaw []byte, reportData []byte, rsaParameters *tpm2.RSAParams) error +} diff --git a/internal/attestation/choose/BUILD.bazel b/internal/attestation/choose/BUILD.bazel index f57356c504..dfb1938e42 100644 --- a/internal/attestation/choose/BUILD.bazel +++ b/internal/attestation/choose/BUILD.bazel @@ -12,6 +12,7 @@ go_library( "//internal/attestation/aws/nitrotpm", "//internal/attestation/aws/snp", "//internal/attestation/azure/snp", + "//internal/attestation/azure/tdx", "//internal/attestation/azure/trustedlaunch", "//internal/attestation/gcp", "//internal/attestation/qemu", diff --git a/internal/attestation/choose/choose.go b/internal/attestation/choose/choose.go index a745518600..3ce936085f 100644 --- a/internal/attestation/choose/choose.go +++ b/internal/attestation/choose/choose.go @@ -14,6 +14,7 @@ import ( "github.com/edgelesssys/constellation/v2/internal/attestation/aws/nitrotpm" awssnp "github.com/edgelesssys/constellation/v2/internal/attestation/aws/snp" azuresnp "github.com/edgelesssys/constellation/v2/internal/attestation/azure/snp" + azuretdx "github.com/edgelesssys/constellation/v2/internal/attestation/azure/tdx" "github.com/edgelesssys/constellation/v2/internal/attestation/azure/trustedlaunch" "github.com/edgelesssys/constellation/v2/internal/attestation/gcp" "github.com/edgelesssys/constellation/v2/internal/attestation/qemu" @@ -33,6 +34,8 @@ func Issuer(attestationVariant variant.Variant, log attestation.Logger) (atls.Is return trustedlaunch.NewIssuer(log), nil case variant.AzureSEVSNP{}: return azuresnp.NewIssuer(log), nil + case variant.AzureTDX{}: + return azuretdx.NewIssuer(log), nil case variant.GCPSEVES{}: return gcp.NewIssuer(log), nil case variant.QEMUVTPM{}: @@ -57,6 +60,8 @@ func Validator(cfg config.AttestationCfg, log attestation.Logger) (atls.Validato return trustedlaunch.NewValidator(cfg, log), nil case *config.AzureSEVSNP: return azuresnp.NewValidator(cfg, log), nil + case *config.AzureTDX: + return azuretdx.NewValidator(cfg, log), nil case *config.GCPSEVES: return gcp.NewValidator(cfg, log), nil case *config.QEMUVTPM: diff --git a/internal/attestation/variant/variant.go b/internal/attestation/variant/variant.go index c33ee1262f..3a42233cad 100644 --- a/internal/attestation/variant/variant.go +++ b/internal/attestation/variant/variant.go @@ -114,6 +114,8 @@ func FromString(oid string) (Variant, error) { return AzureSEVSNP{}, nil case azureTrustedLaunch: return AzureTrustedLaunch{}, nil + case azureTDX: + return AzureTDX{}, nil case qemuVTPM: return QEMUVTPM{}, nil case qemuTDX: diff --git a/internal/config/attestation.go b/internal/config/attestation.go index 7821a63b57..dc4d8fb83c 100644 --- a/internal/config/attestation.go +++ b/internal/config/attestation.go @@ -17,8 +17,12 @@ import ( "github.com/edgelesssys/constellation/v2/internal/attestation/variant" ) -// arkPEM is the PEM encoded AMD root key. Received from the AMD Key Distribution System API (KDS). -const arkPEM = `-----BEGIN CERTIFICATE-----\nMIIGYzCCBBKgAwIBAgIDAQAAMEYGCSqGSIb3DQEBCjA5oA8wDQYJYIZIAWUDBAIC\nBQChHDAaBgkqhkiG9w0BAQgwDQYJYIZIAWUDBAICBQCiAwIBMKMDAgEBMHsxFDAS\nBgNVBAsMC0VuZ2luZWVyaW5nMQswCQYDVQQGEwJVUzEUMBIGA1UEBwwLU2FudGEg\nQ2xhcmExCzAJBgNVBAgMAkNBMR8wHQYDVQQKDBZBZHZhbmNlZCBNaWNybyBEZXZp\nY2VzMRIwEAYDVQQDDAlBUkstTWlsYW4wHhcNMjAxMDIyMTcyMzA1WhcNNDUxMDIy\nMTcyMzA1WjB7MRQwEgYDVQQLDAtFbmdpbmVlcmluZzELMAkGA1UEBhMCVVMxFDAS\nBgNVBAcMC1NhbnRhIENsYXJhMQswCQYDVQQIDAJDQTEfMB0GA1UECgwWQWR2YW5j\nZWQgTWljcm8gRGV2aWNlczESMBAGA1UEAwwJQVJLLU1pbGFuMIICIjANBgkqhkiG\n9w0BAQEFAAOCAg8AMIICCgKCAgEA0Ld52RJOdeiJlqK2JdsVmD7FktuotWwX1fNg\nW41XY9Xz1HEhSUmhLz9Cu9DHRlvgJSNxbeYYsnJfvyjx1MfU0V5tkKiU1EesNFta\n1kTA0szNisdYc9isqk7mXT5+KfGRbfc4V/9zRIcE8jlHN61S1ju8X93+6dxDUrG2\nSzxqJ4BhqyYmUDruPXJSX4vUc01P7j98MpqOS95rORdGHeI52Naz5m2B+O+vjsC0\n60d37jY9LFeuOP4Meri8qgfi2S5kKqg/aF6aPtuAZQVR7u3KFYXP59XmJgtcog05\ngmI0T/OitLhuzVvpZcLph0odh/1IPXqx3+MnjD97A7fXpqGd/y8KxX7jksTEzAOg\nbKAeam3lm+3yKIcTYMlsRMXPcjNbIvmsBykD//xSniusuHBkgnlENEWx1UcbQQrs\n+gVDkuVPhsnzIRNgYvM48Y+7LGiJYnrmE8xcrexekBxrva2V9TJQqnN3Q53kt5vi\nQi3+gCfmkwC0F0tirIZbLkXPrPwzZ0M9eNxhIySb2npJfgnqz55I0u33wh4r0ZNQ\neTGfw03MBUtyuzGesGkcw+loqMaq1qR4tjGbPYxCvpCq7+OgpCCoMNit2uLo9M18\nfHz10lOMT8nWAUvRZFzteXCm+7PHdYPlmQwUw3LvenJ/ILXoQPHfbkH0CyPfhl1j\nWhJFZasCAwEAAaN+MHwwDgYDVR0PAQH/BAQDAgEGMB0GA1UdDgQWBBSFrBrRQ/fI\nrFXUxR1BSKvVeErUUzAPBgNVHRMBAf8EBTADAQH/MDoGA1UdHwQzMDEwL6AtoCuG\nKWh0dHBzOi8va2RzaW50Zi5hbWQuY29tL3ZjZWsvdjEvTWlsYW4vY3JsMEYGCSqG\nSIb3DQEBCjA5oA8wDQYJYIZIAWUDBAICBQChHDAaBgkqhkiG9w0BAQgwDQYJYIZI\nAWUDBAICBQCiAwIBMKMDAgEBA4ICAQC6m0kDp6zv4Ojfgy+zleehsx6ol0ocgVel\nETobpx+EuCsqVFRPK1jZ1sp/lyd9+0fQ0r66n7kagRk4Ca39g66WGTJMeJdqYriw\nSTjjDCKVPSesWXYPVAyDhmP5n2v+BYipZWhpvqpaiO+EGK5IBP+578QeW/sSokrK\ndHaLAxG2LhZxj9aF73fqC7OAJZ5aPonw4RE299FVarh1Tx2eT3wSgkDgutCTB1Yq\nzT5DuwvAe+co2CIVIzMDamYuSFjPN0BCgojl7V+bTou7dMsqIu/TW/rPCX9/EUcp\nKGKqPQ3P+N9r1hjEFY1plBg93t53OOo49GNI+V1zvXPLI6xIFVsh+mto2RtgEX/e\npmMKTNN6psW88qg7c1hTWtN6MbRuQ0vm+O+/2tKBF2h8THb94OvvHHoFDpbCELlq\nHnIYhxy0YKXGyaW1NjfULxrrmxVW4wcn5E8GddmvNa6yYm8scJagEi13mhGu4Jqh\n3QU3sf8iUSUr09xQDwHtOQUVIqx4maBZPBtSMf+qUDtjXSSq8lfWcd8bLr9mdsUn\nJZJ0+tuPMKmBnSH860llKk+VpVQsgqbzDIvOLvD6W1Umq25boxCYJ+TuBoa4s+HH\nCViAvgT9kf/rBq1d+ivj6skkHxuzcxbk1xv6ZGxrteJxVH7KlX7YRdZ6eARKwLe4\nAFZEAwoKCQ==\n-----END CERTIFICATE-----\n` +const ( + // arkPEM is the PEM encoded AMD root key certificate. Received from the AMD Key Distribution System API (KDS). + arkPEM = `-----BEGIN CERTIFICATE-----\nMIIGYzCCBBKgAwIBAgIDAQAAMEYGCSqGSIb3DQEBCjA5oA8wDQYJYIZIAWUDBAIC\nBQChHDAaBgkqhkiG9w0BAQgwDQYJYIZIAWUDBAICBQCiAwIBMKMDAgEBMHsxFDAS\nBgNVBAsMC0VuZ2luZWVyaW5nMQswCQYDVQQGEwJVUzEUMBIGA1UEBwwLU2FudGEg\nQ2xhcmExCzAJBgNVBAgMAkNBMR8wHQYDVQQKDBZBZHZhbmNlZCBNaWNybyBEZXZp\nY2VzMRIwEAYDVQQDDAlBUkstTWlsYW4wHhcNMjAxMDIyMTcyMzA1WhcNNDUxMDIy\nMTcyMzA1WjB7MRQwEgYDVQQLDAtFbmdpbmVlcmluZzELMAkGA1UEBhMCVVMxFDAS\nBgNVBAcMC1NhbnRhIENsYXJhMQswCQYDVQQIDAJDQTEfMB0GA1UECgwWQWR2YW5j\nZWQgTWljcm8gRGV2aWNlczESMBAGA1UEAwwJQVJLLU1pbGFuMIICIjANBgkqhkiG\n9w0BAQEFAAOCAg8AMIICCgKCAgEA0Ld52RJOdeiJlqK2JdsVmD7FktuotWwX1fNg\nW41XY9Xz1HEhSUmhLz9Cu9DHRlvgJSNxbeYYsnJfvyjx1MfU0V5tkKiU1EesNFta\n1kTA0szNisdYc9isqk7mXT5+KfGRbfc4V/9zRIcE8jlHN61S1ju8X93+6dxDUrG2\nSzxqJ4BhqyYmUDruPXJSX4vUc01P7j98MpqOS95rORdGHeI52Naz5m2B+O+vjsC0\n60d37jY9LFeuOP4Meri8qgfi2S5kKqg/aF6aPtuAZQVR7u3KFYXP59XmJgtcog05\ngmI0T/OitLhuzVvpZcLph0odh/1IPXqx3+MnjD97A7fXpqGd/y8KxX7jksTEzAOg\nbKAeam3lm+3yKIcTYMlsRMXPcjNbIvmsBykD//xSniusuHBkgnlENEWx1UcbQQrs\n+gVDkuVPhsnzIRNgYvM48Y+7LGiJYnrmE8xcrexekBxrva2V9TJQqnN3Q53kt5vi\nQi3+gCfmkwC0F0tirIZbLkXPrPwzZ0M9eNxhIySb2npJfgnqz55I0u33wh4r0ZNQ\neTGfw03MBUtyuzGesGkcw+loqMaq1qR4tjGbPYxCvpCq7+OgpCCoMNit2uLo9M18\nfHz10lOMT8nWAUvRZFzteXCm+7PHdYPlmQwUw3LvenJ/ILXoQPHfbkH0CyPfhl1j\nWhJFZasCAwEAAaN+MHwwDgYDVR0PAQH/BAQDAgEGMB0GA1UdDgQWBBSFrBrRQ/fI\nrFXUxR1BSKvVeErUUzAPBgNVHRMBAf8EBTADAQH/MDoGA1UdHwQzMDEwL6AtoCuG\nKWh0dHBzOi8va2RzaW50Zi5hbWQuY29tL3ZjZWsvdjEvTWlsYW4vY3JsMEYGCSqG\nSIb3DQEBCjA5oA8wDQYJYIZIAWUDBAICBQChHDAaBgkqhkiG9w0BAQgwDQYJYIZI\nAWUDBAICBQCiAwIBMKMDAgEBA4ICAQC6m0kDp6zv4Ojfgy+zleehsx6ol0ocgVel\nETobpx+EuCsqVFRPK1jZ1sp/lyd9+0fQ0r66n7kagRk4Ca39g66WGTJMeJdqYriw\nSTjjDCKVPSesWXYPVAyDhmP5n2v+BYipZWhpvqpaiO+EGK5IBP+578QeW/sSokrK\ndHaLAxG2LhZxj9aF73fqC7OAJZ5aPonw4RE299FVarh1Tx2eT3wSgkDgutCTB1Yq\nzT5DuwvAe+co2CIVIzMDamYuSFjPN0BCgojl7V+bTou7dMsqIu/TW/rPCX9/EUcp\nKGKqPQ3P+N9r1hjEFY1plBg93t53OOo49GNI+V1zvXPLI6xIFVsh+mto2RtgEX/e\npmMKTNN6psW88qg7c1hTWtN6MbRuQ0vm+O+/2tKBF2h8THb94OvvHHoFDpbCELlq\nHnIYhxy0YKXGyaW1NjfULxrrmxVW4wcn5E8GddmvNa6yYm8scJagEi13mhGu4Jqh\n3QU3sf8iUSUr09xQDwHtOQUVIqx4maBZPBtSMf+qUDtjXSSq8lfWcd8bLr9mdsUn\nJZJ0+tuPMKmBnSH860llKk+VpVQsgqbzDIvOLvD6W1Umq25boxCYJ+TuBoa4s+HH\nCViAvgT9kf/rBq1d+ivj6skkHxuzcxbk1xv6ZGxrteJxVH7KlX7YRdZ6eARKwLe4\nAFZEAwoKCQ==\n-----END CERTIFICATE-----\n` + // tdxRootPEM is the PEM encoded Intel TDX root key certificate. Receieved from the Intel Provisioning Certification Service (PCS). + tdxRootPEM = `-----BEGIN CERTIFICATE-----\nMIICjzCCAjSgAwIBAgIUImUM1lqdNInzg7SVUr9QGzknBqwwCgYIKoZIzj0EAwIw\naDEaMBgGA1UEAwwRSW50ZWwgU0dYIFJvb3QgQ0ExGjAYBgNVBAoMEUludGVsIENv\ncnBvcmF0aW9uMRQwEgYDVQQHDAtTYW50YSBDbGFyYTELMAkGA1UECAwCQ0ExCzAJ\nBgNVBAYTAlVTMB4XDTE4MDUyMTEwNDUxMFoXDTQ5MTIzMTIzNTk1OVowaDEaMBgG\nA1UEAwwRSW50ZWwgU0dYIFJvb3QgQ0ExGjAYBgNVBAoMEUludGVsIENvcnBvcmF0\naW9uMRQwEgYDVQQHDAtTYW50YSBDbGFyYTELMAkGA1UECAwCQ0ExCzAJBgNVBAYT\nAlVTMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEC6nEwMDIYZOj/iPWsCzaEKi7\n1OiOSLRFhWGjbnBVJfVnkY4u3IjkDYYL0MxO4mqsyYjlBalTVYxFP2sJBK5zlKOB\nuzCBuDAfBgNVHSMEGDAWgBQiZQzWWp00ifODtJVSv1AbOScGrDBSBgNVHR8ESzBJ\nMEegRaBDhkFodHRwczovL2NlcnRpZmljYXRlcy50cnVzdGVkc2VydmljZXMuaW50\nZWwuY29tL0ludGVsU0dYUm9vdENBLmRlcjAdBgNVHQ4EFgQUImUM1lqdNInzg7SV\nUr9QGzknBqwwDgYDVR0PAQH/BAQDAgEGMBIGA1UdEwEB/wQIMAYBAf8CAQEwCgYI\nKoZIzj0EAwIDSQAwRgIhAOW/5QkR+S9CiSDcNoowLuPRLsWGf/Yi7GSX94BgwTwg\nAiEA4J0lrHoMs+Xo5o/sX6O9QWxHRAvZUGOdRQ7cvqRXaqI=\n-----END CERTIFICATE-----\n` +) // AttestationCfg is the common interface for passing attestation configs. type AttestationCfg interface { @@ -44,6 +48,8 @@ func UnmarshalAttestationConfig(data []byte, attestVariant variant.Variant) (Att return unmarshalTypedConfig[*AzureSEVSNP](data) case variant.AzureTrustedLaunch{}: return unmarshalTypedConfig[*AzureTrustedLaunch](data) + case variant.AzureTDX{}: + return unmarshalTypedConfig[*AzureTDX](data) case variant.GCPSEVES{}: return unmarshalTypedConfig[*GCPSEVES](data) case variant.QEMUVTPM{}: diff --git a/internal/config/azure.go b/internal/config/azure.go index 0f1dbcf66b..fa06a57bc3 100644 --- a/internal/config/azure.go +++ b/internal/config/azure.go @@ -132,3 +132,36 @@ func (c AzureTrustedLaunch) EqualTo(other AttestationCfg) (bool, error) { } return c.Measurements.EqualTo(otherCfg.Measurements), nil } + +// DefaultForAzureTDX returns the default configuration for Azure TDX attestation. +func DefaultForAzureTDX() *AzureTDX { + return &AzureTDX{ + Measurements: measurements.DefaultsFor(cloudprovider.Azure, variant.AzureTDX{}), + // TODO: Set default values for version numbers once enabled. + IntelRootKey: mustParsePEM(tdxRootPEM), + } +} + +// GetVariant returns azure-tdx as the variant. +func (AzureTDX) GetVariant() variant.Variant { + return variant.AzureTDX{} +} + +// GetMeasurements returns the measurements used for attestation. +func (c AzureTDX) GetMeasurements() measurements.M { + return c.Measurements +} + +// SetMeasurements updates a config's measurements using the given measurements. +func (c *AzureTDX) SetMeasurements(m measurements.M) { + c.Measurements = m +} + +// EqualTo returns true if the config is equal to the given config. +func (c AzureTDX) EqualTo(other AttestationCfg) (bool, error) { + otherCfg, ok := other.(*AzureTDX) + if !ok { + return false, fmt.Errorf("cannot compare %T with %T", c, other) + } + return c.Measurements.EqualTo(otherCfg.Measurements), nil +} diff --git a/internal/config/config.go b/internal/config/config.go index abb283b77f..17bc391ff9 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -986,10 +986,6 @@ func (c GCPSEVES) EqualTo(other AttestationCfg) (bool, error) { return c.Measurements.EqualTo(otherCfg.Measurements), nil } -func toPtr[T any](v T) *T { - return &v -} - // QEMUVTPM is the configuration for QEMU vTPM attestation. type QEMUVTPM struct { // description: | @@ -1119,6 +1115,38 @@ type AzureTrustedLaunch struct { Measurements measurements.M `json:"measurements" yaml:"measurements" validate:"required,no_placeholders"` } +// AzureTDX is the configuration for Azure TDX attestation. +type AzureTDX struct { + // description: | + // Expected TDX measurements. + Measurements measurements.M `json:"measurements" yaml:"measurements" validate:"required,no_placeholders"` + // description: | + // Minimum required QE security version number (SVN). + QESVN uint16 `json:"qeSVN" yaml:"qeSVN"` + // description: | + // Minimum required PCE security version number (SVN). + PCESVN uint16 `json:"pceSVN" yaml:"pceSVN"` + // description: | + // Component-wise minimum required TEE_TCB security version number (SVN). + TEETCBSVN [16]byte `json:"teeTCBSVN" yaml:"teeTCBSVN"` + // description: | + // Expected QE_VENDOR_ID field. + QEVendorID [16]byte `json:"qeVendorID" yaml:"qeVendorID"` + // description: | + // Expected MR_SEAM value. + MRSeam [48]byte `json:"mrSeam" yaml:"mrSeam"` + // description: | + // Expected XFAM field. + XFAM [8]byte `json:"xfam" yaml:"xfam"` + // description: | + // Intel Root Key certificate used to verify the TDX certificate chain. + IntelRootKey Certificate `json:"intelRootKey" yaml:"intelRootKey"` +} + +func toPtr[T any](v T) *T { + return &v +} + // sevsnpMarshaller is used to marshall "latest" versions with resolved version numbers. type sevsnpMarshaller interface { // getToMarshallLatestWithResolvedVersions brings the attestation config into a state where marshalling uses the numerical version numbers for "latest" versions. diff --git a/internal/config/config_doc.go b/internal/config/config_doc.go index b1972a94ea..87f4592acd 100644 --- a/internal/config/config_doc.go +++ b/internal/config/config_doc.go @@ -29,6 +29,7 @@ var ( AWSNitroTPMDoc encoder.Doc AzureSEVSNPDoc encoder.Doc AzureTrustedLaunchDoc encoder.Doc + AzureTDXDoc encoder.Doc ) func init() { @@ -697,6 +698,51 @@ func init() { AzureTrustedLaunchDoc.Fields[0].Note = "" AzureTrustedLaunchDoc.Fields[0].Description = "Expected TPM measurements." AzureTrustedLaunchDoc.Fields[0].Comments[encoder.LineComment] = "Expected TPM measurements." + + AzureTDXDoc.Type = "AzureTDX" + AzureTDXDoc.Comments[encoder.LineComment] = "AzureTDX is the configuration for Azure TDX attestation." + AzureTDXDoc.Description = "AzureTDX is the configuration for Azure TDX attestation." + AzureTDXDoc.Fields = make([]encoder.Doc, 8) + AzureTDXDoc.Fields[0].Name = "measurements" + AzureTDXDoc.Fields[0].Type = "M" + AzureTDXDoc.Fields[0].Note = "" + AzureTDXDoc.Fields[0].Description = "Expected TDX measurements." + AzureTDXDoc.Fields[0].Comments[encoder.LineComment] = "Expected TDX measurements." + AzureTDXDoc.Fields[1].Name = "qeSVN" + AzureTDXDoc.Fields[1].Type = "uint16" + AzureTDXDoc.Fields[1].Note = "" + AzureTDXDoc.Fields[1].Description = "Minimum required QE security version number (SVN)." + AzureTDXDoc.Fields[1].Comments[encoder.LineComment] = "Minimum required QE security version number (SVN)." + AzureTDXDoc.Fields[2].Name = "pceSVN" + AzureTDXDoc.Fields[2].Type = "uint16" + AzureTDXDoc.Fields[2].Note = "" + AzureTDXDoc.Fields[2].Description = "Minimum required PCE security version number (SVN)." + AzureTDXDoc.Fields[2].Comments[encoder.LineComment] = "Minimum required PCE security version number (SVN)." + AzureTDXDoc.Fields[3].Name = "teeTCBSVN" + AzureTDXDoc.Fields[3].Type = "[]byte" + AzureTDXDoc.Fields[3].Note = "" + AzureTDXDoc.Fields[3].Description = "Component-wise minimum required TEE_TCB security version number (SVN)." + AzureTDXDoc.Fields[3].Comments[encoder.LineComment] = "Component-wise minimum required TEE_TCB security version number (SVN)." + AzureTDXDoc.Fields[4].Name = "qeVendorID" + AzureTDXDoc.Fields[4].Type = "[]byte" + AzureTDXDoc.Fields[4].Note = "" + AzureTDXDoc.Fields[4].Description = "Expected QE_VENDOR_ID field." + AzureTDXDoc.Fields[4].Comments[encoder.LineComment] = "Expected QE_VENDOR_ID field." + AzureTDXDoc.Fields[5].Name = "mrSeam" + AzureTDXDoc.Fields[5].Type = "[]byte" + AzureTDXDoc.Fields[5].Note = "" + AzureTDXDoc.Fields[5].Description = "Expected MR_SEAM value." + AzureTDXDoc.Fields[5].Comments[encoder.LineComment] = "Expected MR_SEAM value." + AzureTDXDoc.Fields[6].Name = "xfam" + AzureTDXDoc.Fields[6].Type = "[]byte" + AzureTDXDoc.Fields[6].Note = "" + AzureTDXDoc.Fields[6].Description = "Expected XFAM field." + AzureTDXDoc.Fields[6].Comments[encoder.LineComment] = "Expected XFAM field." + AzureTDXDoc.Fields[7].Name = "intelRootKey" + AzureTDXDoc.Fields[7].Type = "Certificate" + AzureTDXDoc.Fields[7].Note = "" + AzureTDXDoc.Fields[7].Description = "Intel Root Key certificate used to verify the TDX certificate chain." + AzureTDXDoc.Fields[7].Comments[encoder.LineComment] = "Intel Root Key certificate used to verify the TDX certificate chain." } func (_ Config) Doc() *encoder.Doc { @@ -771,6 +817,10 @@ func (_ AzureTrustedLaunch) Doc() *encoder.Doc { return &AzureTrustedLaunchDoc } +func (_ AzureTDX) Doc() *encoder.Doc { + return &AzureTDXDoc +} + // GetConfigurationDoc returns documentation for the file ./config_doc.go. func GetConfigurationDoc() *encoder.FileDoc { return &encoder.FileDoc{ @@ -795,6 +845,7 @@ func GetConfigurationDoc() *encoder.FileDoc { &AWSNitroTPMDoc, &AzureSEVSNPDoc, &AzureTrustedLaunchDoc, + &AzureTDXDoc, }, } }