diff --git a/internal/attestation/azure/snp/BUILD.bazel b/internal/attestation/azure/snp/BUILD.bazel index 13e064380..7adea0c3e 100644 --- a/internal/attestation/azure/snp/BUILD.bazel +++ b/internal/attestation/azure/snp/BUILD.bazel @@ -15,11 +15,11 @@ go_library( deps = [ "//internal/attestation", "//internal/attestation/idkeydigest", + "//internal/attestation/snp", "//internal/attestation/variant", "//internal/attestation/vtpm", "//internal/cloud/azure", "//internal/config", - "//internal/constants", "@com_github_edgelesssys_go_azguestattestation//maa", "@com_github_google_go_sev_guest//abi", "@com_github_google_go_sev_guest//kds", @@ -48,9 +48,10 @@ go_test( }), deps = [ "//internal/attestation", - "//internal/attestation/azure/snp/testdata", "//internal/attestation/idkeydigest", "//internal/attestation/simulator", + "//internal/attestation/snp", + "//internal/attestation/snp/testdata", "//internal/attestation/vtpm", "//internal/config", "//internal/logger", diff --git a/internal/attestation/azure/snp/issuer.go b/internal/attestation/azure/snp/issuer.go index d527044aa..dbba2cefb 100644 --- a/internal/attestation/azure/snp/issuer.go +++ b/internal/attestation/azure/snp/issuer.go @@ -13,6 +13,7 @@ import ( "io" "github.com/edgelesssys/constellation/v2/internal/attestation" + "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" @@ -65,7 +66,7 @@ func (i *Issuer) getInstanceInfo(ctx context.Context, tpm io.ReadWriteCloser, us } } - instanceInfo := azureInstanceInfo{ + instanceInfo := snp.InstanceInfo{ VCEK: params.VcekCert, CertChain: params.VcekChain, AttestationReport: params.SNPReport, diff --git a/internal/attestation/azure/snp/issuer_test.go b/internal/attestation/azure/snp/issuer_test.go index 4f61c9fab..3ecc9e29c 100644 --- a/internal/attestation/azure/snp/issuer_test.go +++ b/internal/attestation/azure/snp/issuer_test.go @@ -15,6 +15,7 @@ import ( "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" @@ -99,7 +100,7 @@ func TestGetSNPAttestation(t *testing.T) { assert.Equal(data, maa.gotTokenData) } - var instanceInfo azureInstanceInfo + var instanceInfo snp.InstanceInfo err = json.Unmarshal(attestationJSON, &instanceInfo) require.NoError(err) diff --git a/internal/attestation/azure/snp/validator.go b/internal/attestation/azure/snp/validator.go index 4ea503f85..fa31893a4 100644 --- a/internal/attestation/azure/snp/validator.go +++ b/internal/attestation/azure/snp/validator.go @@ -15,16 +15,15 @@ import ( "encoding/base64" "encoding/binary" "encoding/json" - "encoding/pem" "errors" "fmt" "github.com/edgelesssys/constellation/v2/internal/attestation" "github.com/edgelesssys/constellation/v2/internal/attestation/idkeydigest" + "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/constellation/v2/internal/config" - "github.com/edgelesssys/constellation/v2/internal/constants" "github.com/google/go-sev-guest/abi" "github.com/google/go-sev-guest/kds" spb "github.com/google/go-sev-guest/proto/sevsnp" @@ -79,7 +78,7 @@ func NewValidator(cfg *config.AzureSEVSNP, log attestation.Logger) *Validator { log = nopAttestationLogger{} } v := &Validator{ - hclValidator: &azureInstanceInfo{}, + hclValidator: &attestationKey{}, maa: newMAAClient(), config: cfg, log: log, @@ -106,18 +105,15 @@ func (v *Validator) getTrustedKey(ctx context.Context, attDoc vtpm.AttestationDo trustedArk := (*x509.Certificate)(&v.config.AMDRootKey) // ARK, specified in the Constellation config // fallback certificates, used if not present in THIM response. - cachedCerts := sevSnpCerts{ - ask: trustedAsk, - ark: trustedArk, - } + cachedCerts := snp.NewCertificateChain(trustedAsk, trustedArk) // transform the instanceInfo received from Microsoft into a verifiable attestation report format. - var instanceInfo azureInstanceInfo + var instanceInfo snp.InstanceInfo if err := json.Unmarshal(attDoc.InstanceInfo, &instanceInfo); err != nil { return nil, fmt.Errorf("unmarshalling instanceInfo: %w", err) } - att, err := instanceInfo.attestationWithCerts(v.log, v.getter, cachedCerts) + att, err := instanceInfo.AttestationWithCerts(v.log, v.getter, cachedCerts) if err != nil { return nil, fmt.Errorf("parsing attestation report: %w", err) } @@ -192,7 +188,7 @@ func (v *Validator) getTrustedKey(ctx context.Context, attDoc vtpm.AttestationDo if err != nil { return nil, err } - if err = v.hclValidator.validateAk(instanceInfo.RuntimeData, att.Report.ReportData, pubArea.RSAParameters); err != nil { + if err = v.hclValidator.validate(instanceInfo.RuntimeData, att.Report.ReportData, pubArea.RSAParameters); err != nil { return nil, fmt.Errorf("validating HCLAkPub: %w", err) } @@ -239,201 +235,17 @@ func (v *Validator) checkIDKeyDigest(ctx context.Context, report *spb.Attestatio return nil } -// azureInstanceInfo contains the necessary information to establish trust in -// an Azure CVM. -type azureInstanceInfo struct { - // VCEK is the PEM-encoded VCEK certificate for the attestation report. - VCEK []byte - // CertChain is the PEM-encoded certificate chain for the attestation report. - CertChain []byte - // AttestationReport is the attestation report from the vTPM (NVRAM) of the CVM. - AttestationReport []byte - // RuntimeData is the Azure runtime data from the vTPM (NVRAM) of the CVM. - RuntimeData []byte - // MAAToken is the token of the MAA for the attestation report, used as a fallback - // if the IDKeyDigest cannot be verified. - MAAToken string +type attestationKey struct { + PublicPart []akPub `json:"keys"` } -// attestationWithCerts returns a formatted version of the attestation report and its certificates from the instanceInfo. -// Certificates are retrieved in the following precedence: -// 1. ASK or ARK from THIM -// 2. ASK or ARK from fallbackCerts -// 3. ASK or ARK from AMD KDS. -func (a *azureInstanceInfo) attestationWithCerts(logger attestation.Logger, getter trust.HTTPSGetter, - fallbackCerts sevSnpCerts, -) (*spb.Attestation, error) { - report, err := abi.ReportToProto(a.AttestationReport) - if err != nil { - return nil, fmt.Errorf("converting report to proto: %w", err) - } - - // Product info as reported through CPUID[EAX=1] - sevProduct := &spb.SevProduct{Name: spb.SevProduct_SEV_PRODUCT_MILAN, Stepping: 0} // Milan-B0 - productName := kds.ProductString(sevProduct) - - att := &spb.Attestation{ - Report: report, - CertificateChain: &spb.CertificateChain{}, - Product: sevProduct, - } - - // If the VCEK certificate is present, parse it and format it. - vcek, err := a.parseVCEK() - if err != nil { - logger.Warnf("Error parsing VCEK: %v", err) - } - if vcek != nil { - att.CertificateChain.VcekCert = vcek.Raw - } else { - // Otherwise, retrieve it from AMD KDS. - logger.Infof("VCEK certificate not present, falling back to retrieving it from AMD KDS") - vcekURL := kds.VCEKCertURL(productName, report.GetChipId(), kds.TCBVersion(report.GetReportedTcb())) - vcek, err := getter.Get(vcekURL) - if err != nil { - return nil, fmt.Errorf("retrieving VCEK certificate from AMD KDS: %w", err) - } - att.CertificateChain.VcekCert = vcek - } - - // If the certificate chain from THIM is present, parse it and format it. - ask, ark, err := a.parseCertChain() - if err != nil { - logger.Warnf("Error parsing certificate chain: %v", err) - } - if ask != nil { - logger.Infof("Using ASK certificate from Azure THIM") - att.CertificateChain.AskCert = ask.Raw - } - if ark != nil { - logger.Infof("Using ARK certificate from Azure THIM") - att.CertificateChain.ArkCert = ark.Raw - } - - // If a cached ASK or an ARK from the Constellation config is present, use it. - if att.CertificateChain.AskCert == nil && fallbackCerts.ask != nil { - logger.Infof("Using cached ASK certificate") - att.CertificateChain.AskCert = fallbackCerts.ask.Raw - } - if att.CertificateChain.ArkCert == nil && fallbackCerts.ark != nil { - logger.Infof("Using ARK certificate from %s", constants.ConfigFilename) - att.CertificateChain.ArkCert = fallbackCerts.ark.Raw - } - // Otherwise, retrieve it from AMD KDS. - if att.CertificateChain.AskCert == nil || att.CertificateChain.ArkCert == nil { - logger.Infof( - "Certificate chain not fully present (ARK present: %t, ASK present: %t), falling back to retrieving it from AMD KDS", - (att.CertificateChain.ArkCert != nil), - (att.CertificateChain.AskCert != nil), - ) - kdsCertChain, err := trust.GetProductChain(productName, abi.VcekReportSigner, getter) - if err != nil { - return nil, fmt.Errorf("retrieving certificate chain from AMD KDS: %w", err) - } - if att.CertificateChain.AskCert == nil && kdsCertChain.Ask != nil { - logger.Infof("Using ASK certificate from AMD KDS") - att.CertificateChain.AskCert = kdsCertChain.Ask.Raw - } - if att.CertificateChain.ArkCert == nil && kdsCertChain.Ask != nil { - logger.Infof("Using ARK certificate from AMD KDS") - att.CertificateChain.ArkCert = kdsCertChain.Ark.Raw - } - } - - return att, nil -} - -type sevSnpCerts struct { - ask *x509.Certificate - ark *x509.Certificate -} - -// parseCertChain parses the certificate chain from the instanceInfo into x509-formatted ASK and ARK certificates. -// If less than 2 certificates are present, only the present certificate is returned. -// If more than 2 certificates are present, an error is returned. -func (a *azureInstanceInfo) parseCertChain() (ask, ark *x509.Certificate, retErr error) { - rest := bytes.TrimSpace(a.CertChain) - - i := 1 - var block *pem.Block - for block, rest = pem.Decode(rest); block != nil; block, rest = pem.Decode(rest) { - if i > 2 { - retErr = fmt.Errorf("parse certificate %d: more than 2 certificates in chain", i) - return - } - - if block.Type != "CERTIFICATE" { - retErr = fmt.Errorf("parse certificate %d: expected PEM block type 'CERTIFICATE', got '%s'", i, block.Type) - return - } - - cert, err := x509.ParseCertificate(block.Bytes) - if err != nil { - retErr = fmt.Errorf("parse certificate %d: %w", i, err) - return - } - - // https://www.amd.com/content/dam/amd/en/documents/epyc-technical-docs/specifications/57230.pdf - // Table 6 and 7 - switch cert.Subject.CommonName { - case "SEV-Milan": - ask = cert - case "ARK-Milan": - ark = cert - default: - retErr = fmt.Errorf("parse certificate %d: unexpected subject CN %s", i, cert.Subject.CommonName) - return - } - - i++ - } - - switch { - case i == 1: - retErr = fmt.Errorf("no PEM blocks found") - case len(rest) != 0: - retErr = fmt.Errorf("remaining PEM block is not a valid certificate: %s", rest) - } - - return -} - -// parseVCEK parses the VCEK certificate from the instanceInfo into an x509-formatted certificate. -// If the VCEK certificate is not present, nil is returned. -func (a *azureInstanceInfo) parseVCEK() (*x509.Certificate, error) { - newlinesTrimmed := bytes.TrimSpace(a.VCEK) - if len(newlinesTrimmed) == 0 { - // VCEK is not present. - return nil, nil - } - - block, rest := pem.Decode(newlinesTrimmed) - if block == nil { - return nil, fmt.Errorf("no PEM blocks found") - } - if len(rest) != 0 { - return nil, fmt.Errorf("received more data than expected") - } - if block.Type != "CERTIFICATE" { - return nil, fmt.Errorf("expected PEM block type 'CERTIFICATE', got '%s'", block.Type) - } - - vcek, err := x509.ParseCertificate(block.Bytes) - if err != nil { - return nil, fmt.Errorf("parsing VCEK certificate: %w", err) - } - - return vcek, nil -} - -// validateAk validates that the attestation key from the TPM is trustworthy. The steps are: +// 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 *azureInstanceInfo) validateAk(runtimeDataRaw []byte, reportData []byte, rsaParameters *tpm2.RSAParams) error { - var runtimeData runtimeData - if err := json.Unmarshal(runtimeDataRaw, &runtimeData); err != nil { +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) } @@ -445,10 +257,10 @@ func (a *azureInstanceInfo) validateAk(runtimeDataRaw []byte, reportData []byte, return errors.New("unexpected runtimeData digest in TPM") } - if len(runtimeData.Keys) < 1 { + if len(a.PublicPart) < 1 { return errors.New("did not receive any keys in runtime data") } - rawN, err := base64.RawURLEncoding.DecodeString(runtimeData.Keys[0].N) + rawN, err := base64.RawURLEncoding.DecodeString(a.PublicPart[0].N) if err != nil { return fmt.Errorf("decoding modulus string: %w", err) } @@ -456,7 +268,7 @@ func (a *azureInstanceInfo) validateAk(runtimeDataRaw []byte, reportData []byte, return fmt.Errorf("unexpected modulus value in TPM") } - rawE, err := base64.RawURLEncoding.DecodeString(runtimeData.Keys[0].E) + rawE, err := base64.RawURLEncoding.DecodeString(a.PublicPart[0].E) if err != nil { return fmt.Errorf("decoding exponent string: %w", err) } @@ -478,17 +290,13 @@ func (a *azureInstanceInfo) validateAk(runtimeDataRaw []byte, reportData []byte, // 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 { - validateAk(runtimeDataRaw []byte, reportData []byte, rsaParameters *tpm2.RSAParams) error + validate(runtimeDataRaw []byte, reportData []byte, rsaParameters *tpm2.RSAParams) error } // akPub are the public parameters of an RSA attestation key. type akPub struct { - E string - N string -} - -type runtimeData struct { - Keys []akPub + E string `json:"e"` + N string `json:"n"` } // nopAttestationLogger is a no-op implementation of AttestationLogger. diff --git a/internal/attestation/azure/snp/validator_test.go b/internal/attestation/azure/snp/validator_test.go index 71b9fc954..e3443bf05 100644 --- a/internal/attestation/azure/snp/validator_test.go +++ b/internal/attestation/azure/snp/validator_test.go @@ -10,7 +10,6 @@ import ( "bytes" "context" "crypto/sha256" - "crypto/x509" "encoding/base64" "encoding/binary" "encoding/hex" @@ -19,13 +18,13 @@ import ( "fmt" "os" "regexp" - "strings" "testing" "github.com/edgelesssys/constellation/v2/internal/attestation" - "github.com/edgelesssys/constellation/v2/internal/attestation/azure/snp/testdata" "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" "github.com/edgelesssys/constellation/v2/internal/logger" @@ -69,264 +68,6 @@ func TestNewValidator(t *testing.T) { } } -// TestParseCertChain tests the parsing of the certificate chain. -func TestParseCertChain(t *testing.T) { - defaultCertChain := testdata.CertChain - askOnly := strings.Split(string(defaultCertChain), "-----END CERTIFICATE-----")[0] + "-----END CERTIFICATE-----" - arkOnly := strings.Split(string(defaultCertChain), "-----END CERTIFICATE-----")[1] + "-----END CERTIFICATE-----" - - testCases := map[string]struct { - certChain []byte - wantAsk bool - wantArk bool - wantErr bool - }{ - "success": { - certChain: defaultCertChain, - wantAsk: true, - wantArk: true, - }, - "empty cert chain": { - certChain: []byte{}, - wantErr: true, - }, - "more than two certificates": { - certChain: append(defaultCertChain, defaultCertChain...), - wantErr: true, - }, - "invalid certificate": { - certChain: []byte("invalid"), - wantErr: true, - }, - "ark missing": { - certChain: []byte(askOnly), - wantAsk: true, - }, - "ask missing": { - certChain: []byte(arkOnly), - wantArk: true, - }, - } - - for name, tc := range testCases { - t.Run(name, func(t *testing.T) { - assert := assert.New(t) - - instanceInfo := &azureInstanceInfo{ - CertChain: tc.certChain, - } - - ask, ark, err := instanceInfo.parseCertChain() - if tc.wantErr { - assert.Error(err) - } else { - assert.NoError(err) - assert.Equal(tc.wantAsk, ask != nil) - assert.Equal(tc.wantArk, ark != nil) - } - }) - } -} - -// TestParseVCEK tests the parsing of the VCEK certificate. -func TestParseVCEK(t *testing.T) { - testCases := map[string]struct { - VCEK []byte - wantVCEK bool - wantErr bool - }{ - "success": { - VCEK: testdata.AzureThimVCEK, - wantVCEK: true, - }, - "empty": { - VCEK: []byte{}, - }, - "malformed": { - VCEK: testdata.AzureThimVCEK[:len(testdata.AzureThimVCEK)-100], - wantErr: true, - }, - "invalid": { - VCEK: []byte("invalid"), - wantErr: true, - }, - } - - for name, tc := range testCases { - t.Run(name, func(t *testing.T) { - assert := assert.New(t) - - instanceInfo := &azureInstanceInfo{ - VCEK: tc.VCEK, - } - - vcek, err := instanceInfo.parseVCEK() - if tc.wantErr { - assert.Error(err) - } else { - assert.NoError(err) - assert.Equal(tc.wantVCEK, vcek != nil) - } - }) - } -} - -// TestInstanceInfoAttestation tests the basic unmarshalling of the attestation report and the ASK / ARK precedence. -func TestInstanceInfoAttestation(t *testing.T) { - defaultReport := testdata.AttestationReport - testdataArk, testdataAsk := mustCertChainToPem(t, testdata.CertChain) - exampleCert := &x509.Certificate{ - Raw: []byte{1, 2, 3}, - } - cfg := config.DefaultForAzureSEVSNP() - - testCases := map[string]struct { - report []byte - vcek []byte - certChain []byte - fallbackCerts sevSnpCerts - getter *stubHTTPSGetter - expectedArk *x509.Certificate - expectedAsk *x509.Certificate - wantErr bool - }{ - "success": { - report: defaultReport, - vcek: testdata.AzureThimVCEK, - certChain: testdata.CertChain, - expectedArk: testdataArk, - expectedAsk: testdataAsk, - }, - "retrieve vcek": { - report: defaultReport, - certChain: testdata.CertChain, - getter: newStubHTTPSGetter( - &urlResponseMatcher{ - vcekResponse: testdata.AmdKdsVCEK, - wantVcekRequest: true, - }, - nil, - ), - expectedArk: testdataArk, - expectedAsk: testdataAsk, - }, - "retrieve certchain": { - report: defaultReport, - vcek: testdata.AzureThimVCEK, - getter: newStubHTTPSGetter( - &urlResponseMatcher{ - certChainResponse: testdata.CertChain, - wantCertChainRequest: true, - }, - nil, - ), - expectedArk: testdataArk, - expectedAsk: testdataAsk, - }, - "use fallback certs": { - report: defaultReport, - vcek: testdata.AzureThimVCEK, - fallbackCerts: sevSnpCerts{ - ask: exampleCert, - ark: exampleCert, - }, - getter: newStubHTTPSGetter( - &urlResponseMatcher{}, - nil, - ), - expectedArk: exampleCert, - expectedAsk: exampleCert, - }, - "use certchain with fallback certs": { - report: defaultReport, - certChain: testdata.CertChain, - vcek: testdata.AzureThimVCEK, - fallbackCerts: sevSnpCerts{ - ask: &x509.Certificate{}, - ark: &x509.Certificate{}, - }, - getter: newStubHTTPSGetter( - &urlResponseMatcher{}, - nil, - ), - expectedArk: testdataArk, - expectedAsk: testdataAsk, - }, - "retrieve vcek and certchain": { - report: defaultReport, - getter: newStubHTTPSGetter( - &urlResponseMatcher{ - certChainResponse: testdata.CertChain, - vcekResponse: testdata.AmdKdsVCEK, - wantCertChainRequest: true, - wantVcekRequest: true, - }, - nil, - ), - expectedArk: testdataArk, - expectedAsk: testdataAsk, - }, - "report too short": { - report: defaultReport[:len(defaultReport)-100], - wantErr: true, - }, - "corrupted report": { - report: defaultReport[10 : len(defaultReport)-10], - wantErr: true, - }, - "certificate fetch error": { - report: defaultReport, - getter: newStubHTTPSGetter(nil, assert.AnError), - wantErr: true, - }, - } - for name, tc := range testCases { - t.Run(name, func(t *testing.T) { - assert := assert.New(t) - require := require.New(t) - - // This is important. Without this call, the trust module caches certificates across testcases. - defer trust.ClearProductCertCache() - - instanceInfo := azureInstanceInfo{ - AttestationReport: tc.report, - CertChain: tc.certChain, - VCEK: tc.vcek, - } - - att, err := instanceInfo.attestationWithCerts(logger.NewTest(t), tc.getter, tc.fallbackCerts) - if tc.wantErr { - assert.Error(err) - } else { - require.NoError(err) - assert.NotNil(att) - assert.NotNil(att.CertificateChain) - assert.NotNil(att.Report) - - assert.Equal(hex.EncodeToString(att.Report.IdKeyDigest[:]), "57e229e0ffe5fa92d0faddff6cae0e61c926fc9ef9afd20a8b8cfcf7129db9338cbe5bf3f6987733a2bf65d06dc38fc1") - - // This is a canary for us: If this fails in the future we possibly downgraded a SVN. - // See https://github.com/google/go-sev-guest/blob/14ac50e9ffcc05cd1d12247b710c65093beedb58/validate/validate.go#L336 for decomposition of the values. - tcbValues := kds.DecomposeTCBVersion(kds.TCBVersion(att.Report.GetLaunchTcb())) - assert.True(tcbValues.BlSpl >= cfg.BootloaderVersion.Value) - assert.True(tcbValues.TeeSpl >= cfg.TEEVersion.Value) - assert.True(tcbValues.SnpSpl >= cfg.SNPVersion.Value) - assert.True(tcbValues.UcodeSpl >= cfg.MicrocodeVersion.Value) - assert.Equal(tc.expectedArk.Raw, att.CertificateChain.ArkCert) - assert.Equal(tc.expectedAsk.Raw, att.CertificateChain.AskCert) - } - }) - } -} - -func mustCertChainToPem(t *testing.T, certchain []byte) (ark, ask *x509.Certificate) { - t.Helper() - a := azureInstanceInfo{CertChain: certchain} - ask, ark, err := a.parseCertChain() - require.NoError(t, err) - return ark, ask -} - type stubHTTPSGetter struct { urlResponseMatcher *urlResponseMatcher // maps responses to requested URLs err error @@ -488,18 +229,18 @@ func TestValidateAk(t *testing.T) { n := base64.RawURLEncoding.EncodeToString(key.PublicArea().RSAParameters.ModulusRaw) ak := akPub{E: e, N: n} - runtimeData := runtimeData{Keys: []akPub{ak}} + runtimeData := attestationKey{PublicPart: []akPub{ak}} defaultRuntimeDataRaw, err := json.Marshal(runtimeData) require.NoError(err) - defaultInstanceInfo := azureInstanceInfo{RuntimeData: defaultRuntimeDataRaw} + defaultInstanceInfo := snp.InstanceInfo{RuntimeData: defaultRuntimeDataRaw} sig := sha256.Sum256(defaultRuntimeDataRaw) defaultReportData := sig[:] defaultRsaParams := key.PublicArea().RSAParameters testCases := map[string]struct { - instanceInfo azureInstanceInfo + instanceInfo snp.InstanceInfo runtimeDataRaw []byte reportData []byte rsaParameters *tpm2.RSAParams @@ -552,7 +293,8 @@ func TestValidateAk(t *testing.T) { for name, tc := range testCases { t.Run(name, func(t *testing.T) { assert := assert.New(t) - err = tc.instanceInfo.validateAk(tc.runtimeDataRaw, tc.reportData, tc.rsaParameters) + ak := attestationKey{} + err = ak.validate(tc.runtimeDataRaw, tc.reportData, tc.rsaParameters) if tc.wantErr { assert.Error(err) } else { @@ -985,7 +727,7 @@ func TestTrustedKeyFromSNP(t *testing.T) { // This is important. Without this call, the trust module caches certificates across testcases. defer trust.ClearProductCertCache() - instanceInfo, err := newStubAzureInstanceInfo(tc.vcek, tc.certChain, tc.report, tc.runtimeData) + instanceInfo, err := newStubInstanceInfo(tc.vcek, tc.certChain, tc.report, tc.runtimeData) require.NoError(err) statement, err := json.Marshal(instanceInfo) @@ -1004,7 +746,7 @@ func TestTrustedKeyFromSNP(t *testing.T) { } validator := &Validator{ - hclValidator: &instanceInfo, + hclValidator: &stubAttestationKey{}, config: defaultCfg, log: logger.NewTest(t), getter: tc.getter, @@ -1050,25 +792,25 @@ func (v *stubAttestationValidator) SNPAttestation(attestation *spb.Attestation, return validate.SnpAttestation(attestation, options) } -type stubAzureInstanceInfo struct { +type stubInstanceInfo struct { AttestationReport []byte RuntimeData []byte VCEK []byte CertChain []byte } -func newStubAzureInstanceInfo(vcek, certChain []byte, report, runtimeData string) (stubAzureInstanceInfo, error) { +func newStubInstanceInfo(vcek, certChain []byte, report, runtimeData string) (stubInstanceInfo, error) { validReport, err := hex.DecodeString(report) if err != nil { - return stubAzureInstanceInfo{}, fmt.Errorf("invalid hex string report: %s", err) + return stubInstanceInfo{}, fmt.Errorf("invalid hex string report: %s", err) } decodedRuntime, err := hex.DecodeString(runtimeData) if err != nil { - return stubAzureInstanceInfo{}, fmt.Errorf("invalid hex string runtimeData: %s", err) + return stubInstanceInfo{}, fmt.Errorf("invalid hex string runtimeData: %s", err) } - return stubAzureInstanceInfo{ + return stubInstanceInfo{ AttestationReport: validReport, RuntimeData: decodedRuntime, VCEK: vcek, @@ -1076,9 +818,12 @@ func newStubAzureInstanceInfo(vcek, certChain []byte, report, runtimeData string }, nil } -func (s *stubAzureInstanceInfo) validateAk(runtimeDataRaw []byte, reportData []byte, _ *tpm2.RSAParams) error { - var runtimeData runtimeData - if err := json.Unmarshal(runtimeDataRaw, &runtimeData); err != nil { +type stubAttestationKey struct { + PublicPart []akPub +} + +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/snp/BUILD.bazel b/internal/attestation/snp/BUILD.bazel new file mode 100644 index 000000000..613751925 --- /dev/null +++ b/internal/attestation/snp/BUILD.bazel @@ -0,0 +1,31 @@ +load("@io_bazel_rules_go//go:def.bzl", "go_library") +load("//bazel/go:go_test.bzl", "go_test") + +go_library( + name = "snp", + srcs = ["snp.go"], + importpath = "github.com/edgelesssys/constellation/v2/internal/attestation/snp", + visibility = ["//:__subpackages__"], + deps = [ + "//internal/attestation", + "//internal/constants", + "@com_github_google_go_sev_guest//abi", + "@com_github_google_go_sev_guest//kds", + "@com_github_google_go_sev_guest//proto/sevsnp", + "@com_github_google_go_sev_guest//verify/trust", + ], +) + +go_test( + name = "snp_test", + srcs = ["snp_test.go"], + embed = [":snp"], + deps = [ + "//internal/attestation/snp/testdata", + "//internal/config", + "//internal/logger", + "@com_github_google_go_sev_guest//kds", + "@com_github_stretchr_testify//assert", + "@com_github_stretchr_testify//require", + ], +) diff --git a/internal/attestation/snp/snp.go b/internal/attestation/snp/snp.go new file mode 100644 index 000000000..281b9c662 --- /dev/null +++ b/internal/attestation/snp/snp.go @@ -0,0 +1,217 @@ +/* +Copyright (c) Edgeless Systems GmbH + +SPDX-License-Identifier: AGPL-3.0-only +*/ + +// Package SNP provides types shared by SNP-based attestation implementations. +// It ensures all issuers provide the same types to the verify command. +package snp + +import ( + "bytes" + "crypto/x509" + "encoding/pem" + "fmt" + + "github.com/edgelesssys/constellation/v2/internal/attestation" + "github.com/edgelesssys/constellation/v2/internal/constants" + "github.com/google/go-sev-guest/abi" + "github.com/google/go-sev-guest/kds" + spb "github.com/google/go-sev-guest/proto/sevsnp" + "github.com/google/go-sev-guest/verify/trust" +) + +// InstanceInfo contains the necessary information to establish trust in +// an Azure CVM. +type InstanceInfo struct { + // VCEK is the PEM-encoded VCEK certificate for the attestation report. + VCEK []byte + // CertChain is the PEM-encoded certificate chain for the attestation report. + CertChain []byte + // AttestationReport is the attestation report from the vTPM (NVRAM) of the CVM. + AttestationReport []byte + // RuntimeData is the Azure runtime data from the vTPM (NVRAM) of the CVM. + RuntimeData []byte + // MAAToken is the token of the MAA for the attestation report, used as a fallback + // if the IDKeyDigest cannot be verified. + MAAToken string +} + +// AttestationWithCerts returns a formatted version of the attestation report and its certificates from the instanceInfo. +// Certificates are retrieved in the following precedence: +// 1. ASK or ARK from issuer. On Azure: THIM. One AWS: not prefilled. +// 2. ASK or ARK from fallbackCerts. +// 3. ASK or ARK from AMD KDS. +func (a *InstanceInfo) AttestationWithCerts(logger attestation.Logger, getter trust.HTTPSGetter, + fallbackCerts CertificateChain, +) (*spb.Attestation, error) { + report, err := abi.ReportToProto(a.AttestationReport) + if err != nil { + return nil, fmt.Errorf("converting report to proto: %w", err) + } + + // Product info as reported through CPUID[EAX=1] + sevProduct := &spb.SevProduct{Name: spb.SevProduct_SEV_PRODUCT_MILAN, Stepping: 0} // Milan-B0 + productName := kds.ProductString(sevProduct) + + att := &spb.Attestation{ + Report: report, + CertificateChain: &spb.CertificateChain{}, + Product: sevProduct, + } + + // If the VCEK certificate is present, parse it and format it. + vcek, err := a.ParseVCEK() + if err != nil { + logger.Warnf("Error parsing VCEK: %v", err) + } + if vcek != nil { + att.CertificateChain.VcekCert = vcek.Raw + } else { + // Otherwise, retrieve it from AMD KDS. + logger.Infof("VCEK certificate not present, falling back to retrieving it from AMD KDS") + vcekURL := kds.VCEKCertURL(productName, report.GetChipId(), kds.TCBVersion(report.GetReportedTcb())) + vcek, err := getter.Get(vcekURL) + if err != nil { + return nil, fmt.Errorf("retrieving VCEK certificate from AMD KDS: %w", err) + } + att.CertificateChain.VcekCert = vcek + } + + // If the certificate chain from THIM is present, parse it and format it. + ask, ark, err := a.ParseCertChain() + if err != nil { + logger.Warnf("Error parsing certificate chain: %v", err) + } + if ask != nil { + logger.Infof("Using ASK certificate from Azure THIM") + att.CertificateChain.AskCert = ask.Raw + } + if ark != nil { + logger.Infof("Using ARK certificate from Azure THIM") + att.CertificateChain.ArkCert = ark.Raw + } + + // If a cached ASK or an ARK from the Constellation config is present, use it. + if att.CertificateChain.AskCert == nil && fallbackCerts.ask != nil { + logger.Infof("Using cached ASK certificate") + att.CertificateChain.AskCert = fallbackCerts.ask.Raw + } + if att.CertificateChain.ArkCert == nil && fallbackCerts.ark != nil { + logger.Infof("Using ARK certificate from %s", constants.ConfigFilename) + att.CertificateChain.ArkCert = fallbackCerts.ark.Raw + } + // Otherwise, retrieve it from AMD KDS. + if att.CertificateChain.AskCert == nil || att.CertificateChain.ArkCert == nil { + logger.Infof( + "Certificate chain not fully present (ARK present: %t, ASK present: %t), falling back to retrieving it from AMD KDS", + (att.CertificateChain.ArkCert != nil), + (att.CertificateChain.AskCert != nil), + ) + kdsCertChain, err := trust.GetProductChain(productName, abi.VcekReportSigner, getter) + if err != nil { + return nil, fmt.Errorf("retrieving certificate chain from AMD KDS: %w", err) + } + if att.CertificateChain.AskCert == nil && kdsCertChain.Ask != nil { + logger.Infof("Using ASK certificate from AMD KDS") + att.CertificateChain.AskCert = kdsCertChain.Ask.Raw + } + if att.CertificateChain.ArkCert == nil && kdsCertChain.Ask != nil { + logger.Infof("Using ARK certificate from AMD KDS") + att.CertificateChain.ArkCert = kdsCertChain.Ark.Raw + } + } + + return att, nil +} + +type CertificateChain struct { + ask *x509.Certificate + ark *x509.Certificate +} + +func NewCertificateChain(ask, ark *x509.Certificate) CertificateChain { + return CertificateChain{ + ask: ask, + ark: ark, + } +} + +// ParseCertChain parses the certificate chain from the instanceInfo into x509-formatted ASK and ARK certificates. +// If less than 2 certificates are present, only the present certificate is returned. +// If more than 2 certificates are present, an error is returned. +func (a *InstanceInfo) ParseCertChain() (ask, ark *x509.Certificate, retErr error) { + rest := bytes.TrimSpace(a.CertChain) + + i := 1 + var block *pem.Block + for block, rest = pem.Decode(rest); block != nil; block, rest = pem.Decode(rest) { + if i > 2 { + retErr = fmt.Errorf("parse certificate %d: more than 2 certificates in chain", i) + return + } + + if block.Type != "CERTIFICATE" { + retErr = fmt.Errorf("parse certificate %d: expected PEM block type 'CERTIFICATE', got '%s'", i, block.Type) + return + } + + cert, err := x509.ParseCertificate(block.Bytes) + if err != nil { + retErr = fmt.Errorf("parse certificate %d: %w", i, err) + return + } + + // https://www.amd.com/content/dam/amd/en/documents/epyc-technical-docs/specifications/57230.pdf + // Table 6 and 7 + switch cert.Subject.CommonName { + case "SEV-Milan": + ask = cert + case "ARK-Milan": + ark = cert + default: + retErr = fmt.Errorf("parse certificate %d: unexpected subject CN %s", i, cert.Subject.CommonName) + return + } + + i++ + } + + switch { + case i == 1: + retErr = fmt.Errorf("no PEM blocks found") + case len(rest) != 0: + retErr = fmt.Errorf("remaining PEM block is not a valid certificate: %s", rest) + } + + return +} + +// ParseVCEK parses the VCEK certificate from the instanceInfo into an x509-formatted certificate. +// If the VCEK certificate is not present, nil is returned. +func (a *InstanceInfo) ParseVCEK() (*x509.Certificate, error) { + newlinesTrimmed := bytes.TrimSpace(a.VCEK) + if len(newlinesTrimmed) == 0 { + // VCEK is not present. + return nil, nil + } + + block, rest := pem.Decode(newlinesTrimmed) + if block == nil { + return nil, fmt.Errorf("no PEM blocks found") + } + if len(rest) != 0 { + return nil, fmt.Errorf("received more data than expected") + } + if block.Type != "CERTIFICATE" { + return nil, fmt.Errorf("expected PEM block type 'CERTIFICATE', got '%s'", block.Type) + } + + vcek, err := x509.ParseCertificate(block.Bytes) + if err != nil { + return nil, fmt.Errorf("parsing VCEK certificate: %w", err) + } + + return vcek, nil +} diff --git a/internal/attestation/snp/snp_test.go b/internal/attestation/snp/snp_test.go new file mode 100644 index 000000000..bfc0f225b --- /dev/null +++ b/internal/attestation/snp/snp_test.go @@ -0,0 +1,315 @@ +/* +Copyright (c) Edgeless Systems GmbH + +SPDX-License-Identifier: AGPL-3.0-only +*/ + +package snp + +import ( + "crypto/x509" + "encoding/hex" + "fmt" + "regexp" + "strings" + "testing" + + "github.com/edgelesssys/constellation/v2/internal/attestation/snp/testdata" + "github.com/edgelesssys/constellation/v2/internal/config" + "github.com/edgelesssys/constellation/v2/internal/logger" + "github.com/google/go-sev-guest/kds" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +// TestParseCertChain tests the parsing of the certificate chain. +func TestParseCertChain(t *testing.T) { + defaultCertChain := testdata.CertChain + askOnly := strings.Split(string(defaultCertChain), "-----END CERTIFICATE-----")[0] + "-----END CERTIFICATE-----" + arkOnly := strings.Split(string(defaultCertChain), "-----END CERTIFICATE-----")[1] + "-----END CERTIFICATE-----" + + testCases := map[string]struct { + certChain []byte + wantAsk bool + wantArk bool + wantErr bool + }{ + "success": { + certChain: defaultCertChain, + wantAsk: true, + wantArk: true, + }, + "empty cert chain": { + certChain: []byte{}, + wantErr: true, + }, + "more than two certificates": { + certChain: append(defaultCertChain, defaultCertChain...), + wantErr: true, + }, + "invalid certificate": { + certChain: []byte("invalid"), + wantErr: true, + }, + "ark missing": { + certChain: []byte(askOnly), + wantAsk: true, + }, + "ask missing": { + certChain: []byte(arkOnly), + wantArk: true, + }, + } + + for name, tc := range testCases { + t.Run(name, func(t *testing.T) { + assert := assert.New(t) + + instanceInfo := &InstanceInfo{ + CertChain: tc.certChain, + } + + ask, ark, err := instanceInfo.ParseCertChain() + if tc.wantErr { + assert.Error(err) + } else { + assert.NoError(err) + assert.Equal(tc.wantAsk, ask != nil) + assert.Equal(tc.wantArk, ark != nil) + } + }) + } +} + +// TestParseVCEK tests the parsing of the VCEK certificate. +func TestParseVCEK(t *testing.T) { + testCases := map[string]struct { + VCEK []byte + wantVCEK bool + wantErr bool + }{ + "success": { + VCEK: testdata.AzureThimVCEK, + wantVCEK: true, + }, + "empty": { + VCEK: []byte{}, + }, + "malformed": { + VCEK: testdata.AzureThimVCEK[:len(testdata.AzureThimVCEK)-100], + wantErr: true, + }, + "invalid": { + VCEK: []byte("invalid"), + wantErr: true, + }, + } + + for name, tc := range testCases { + t.Run(name, func(t *testing.T) { + assert := assert.New(t) + + instanceInfo := &InstanceInfo{ + VCEK: tc.VCEK, + } + + vcek, err := instanceInfo.ParseVCEK() + if tc.wantErr { + assert.Error(err) + } else { + assert.NoError(err) + assert.Equal(tc.wantVCEK, vcek != nil) + } + }) + } +} + +// TestInstanceInfoAttestation tests the basic unmarshalling of the attestation report and the ASK / ARK precedence. +func TestInstanceInfoAttestation(t *testing.T) { + defaultReport := testdata.AttestationReport + testdataArk, testdataAsk := mustCertChainToPem(t, testdata.CertChain) + exampleCert := &x509.Certificate{ + Raw: []byte{1, 2, 3}, + } + cfg := config.DefaultForAzureSEVSNP() + + testCases := map[string]struct { + report []byte + vcek []byte + certChain []byte + fallbackCerts CertificateChain + getter *stubHTTPSGetter + expectedArk *x509.Certificate + expectedAsk *x509.Certificate + wantErr bool + }{ + "success": { + report: defaultReport, + vcek: testdata.AzureThimVCEK, + certChain: testdata.CertChain, + expectedArk: testdataArk, + expectedAsk: testdataAsk, + }, + "retrieve vcek": { + report: defaultReport, + certChain: testdata.CertChain, + getter: newStubHTTPSGetter( + &urlResponseMatcher{ + vcekResponse: testdata.AmdKdsVCEK, + wantVcekRequest: true, + }, + nil, + ), + expectedArk: testdataArk, + expectedAsk: testdataAsk, + }, + "retrieve certchain": { + report: defaultReport, + vcek: testdata.AzureThimVCEK, + getter: newStubHTTPSGetter( + &urlResponseMatcher{ + certChainResponse: testdata.CertChain, + wantCertChainRequest: true, + }, + nil, + ), + expectedArk: testdataArk, + expectedAsk: testdataAsk, + }, + "use fallback certs": { + report: defaultReport, + vcek: testdata.AzureThimVCEK, + fallbackCerts: NewCertificateChain(exampleCert, exampleCert), + getter: newStubHTTPSGetter( + &urlResponseMatcher{}, + nil, + ), + expectedArk: exampleCert, + expectedAsk: exampleCert, + }, + "use certchain with fallback certs": { + report: defaultReport, + certChain: testdata.CertChain, + vcek: testdata.AzureThimVCEK, + fallbackCerts: NewCertificateChain(&x509.Certificate{}, &x509.Certificate{}), + getter: newStubHTTPSGetter( + &urlResponseMatcher{}, + nil, + ), + expectedArk: testdataArk, + expectedAsk: testdataAsk, + }, + "retrieve vcek and certchain": { + report: defaultReport, + getter: newStubHTTPSGetter( + &urlResponseMatcher{ + certChainResponse: testdata.CertChain, + vcekResponse: testdata.AmdKdsVCEK, + wantCertChainRequest: true, + wantVcekRequest: true, + }, + nil, + ), + expectedArk: testdataArk, + expectedAsk: testdataAsk, + }, + "report too short": { + report: defaultReport[:len(defaultReport)-100], + wantErr: true, + }, + "corrupted report": { + report: defaultReport[10 : len(defaultReport)-10], + wantErr: true, + }, + "certificate fetch error": { + report: defaultReport, + getter: newStubHTTPSGetter(nil, assert.AnError), + wantErr: true, + }, + } + for name, tc := range testCases { + t.Run(name, func(t *testing.T) { + assert := assert.New(t) + require := require.New(t) + + instanceInfo := InstanceInfo{ + AttestationReport: tc.report, + CertChain: tc.certChain, + VCEK: tc.vcek, + } + + att, err := instanceInfo.AttestationWithCerts(logger.NewTest(t), tc.getter, tc.fallbackCerts) + if tc.wantErr { + assert.Error(err) + } else { + require.NoError(err) + assert.NotNil(att) + assert.NotNil(att.CertificateChain) + assert.NotNil(att.Report) + + assert.Equal(hex.EncodeToString(att.Report.IdKeyDigest[:]), "57e229e0ffe5fa92d0faddff6cae0e61c926fc9ef9afd20a8b8cfcf7129db9338cbe5bf3f6987733a2bf65d06dc38fc1") + + // This is a canary for us: If this fails in the future we possibly downgraded a SVN. + // See https://github.com/google/go-sev-guest/blob/14ac50e9ffcc05cd1d12247b710c65093beedb58/validate/validate.go#L336 for decomposition of the values. + tcbValues := kds.DecomposeTCBVersion(kds.TCBVersion(att.Report.GetLaunchTcb())) + assert.True(tcbValues.BlSpl >= cfg.BootloaderVersion.Value) + assert.True(tcbValues.TeeSpl >= cfg.TEEVersion.Value) + assert.True(tcbValues.SnpSpl >= cfg.SNPVersion.Value) + assert.True(tcbValues.UcodeSpl >= cfg.MicrocodeVersion.Value) + assert.Equal(tc.expectedArk.Raw, att.CertificateChain.ArkCert) + assert.Equal(tc.expectedAsk.Raw, att.CertificateChain.AskCert) + } + }) + } +} + +func mustCertChainToPem(t *testing.T, certchain []byte) (ark, ask *x509.Certificate) { + t.Helper() + a := InstanceInfo{CertChain: certchain} + ask, ark, err := a.ParseCertChain() + require.NoError(t, err) + return ark, ask +} + +type stubHTTPSGetter struct { + urlResponseMatcher *urlResponseMatcher // maps responses to requested URLs + err error +} + +func newStubHTTPSGetter(urlResponseMatcher *urlResponseMatcher, err error) *stubHTTPSGetter { + return &stubHTTPSGetter{ + urlResponseMatcher: urlResponseMatcher, + err: err, + } +} + +func (s *stubHTTPSGetter) Get(url string) ([]byte, error) { + if s.err != nil { + return nil, s.err + } + return s.urlResponseMatcher.match(url) +} + +type urlResponseMatcher struct { + certChainResponse []byte + wantCertChainRequest bool + vcekResponse []byte + wantVcekRequest bool +} + +func (m *urlResponseMatcher) match(url string) ([]byte, error) { + switch { + case url == "https://kdsintf.amd.com/vcek/v1/Milan/cert_chain": + if !m.wantCertChainRequest { + return nil, fmt.Errorf("unexpected cert_chain request") + } + return m.certChainResponse, nil + case regexp.MustCompile(`https:\/\/kdsintf.amd.com\/vcek\/v1\/Milan\/.*`).MatchString(url): + if !m.wantVcekRequest { + return nil, fmt.Errorf("unexpected VCEK request") + } + return m.vcekResponse, nil + default: + return nil, fmt.Errorf("unexpected URL: %s", url) + } +} diff --git a/internal/attestation/azure/snp/testdata/BUILD.bazel b/internal/attestation/snp/testdata/BUILD.bazel similarity index 91% rename from internal/attestation/azure/snp/testdata/BUILD.bazel rename to internal/attestation/snp/testdata/BUILD.bazel index 9b09a6fdb..54369e85a 100644 --- a/internal/attestation/azure/snp/testdata/BUILD.bazel +++ b/internal/attestation/snp/testdata/BUILD.bazel @@ -10,6 +10,6 @@ go_library( "runtimedata.bin", "vcek.pem", ], - importpath = "github.com/edgelesssys/constellation/v2/internal/attestation/azure/snp/testdata", + importpath = "github.com/edgelesssys/constellation/v2/internal/attestation/snp/testdata", visibility = ["//:__subpackages__"], ) diff --git a/internal/attestation/azure/snp/testdata/attestation.bin b/internal/attestation/snp/testdata/attestation.bin similarity index 100% rename from internal/attestation/azure/snp/testdata/attestation.bin rename to internal/attestation/snp/testdata/attestation.bin diff --git a/internal/attestation/azure/snp/testdata/certchain.pem b/internal/attestation/snp/testdata/certchain.pem similarity index 100% rename from internal/attestation/azure/snp/testdata/certchain.pem rename to internal/attestation/snp/testdata/certchain.pem diff --git a/internal/attestation/azure/snp/testdata/runtimedata.bin b/internal/attestation/snp/testdata/runtimedata.bin similarity index 100% rename from internal/attestation/azure/snp/testdata/runtimedata.bin rename to internal/attestation/snp/testdata/runtimedata.bin diff --git a/internal/attestation/azure/snp/testdata/testdata.go b/internal/attestation/snp/testdata/testdata.go similarity index 100% rename from internal/attestation/azure/snp/testdata/testdata.go rename to internal/attestation/snp/testdata/testdata.go diff --git a/internal/attestation/azure/snp/testdata/vcek.cert b/internal/attestation/snp/testdata/vcek.cert similarity index 100% rename from internal/attestation/azure/snp/testdata/vcek.cert rename to internal/attestation/snp/testdata/vcek.cert diff --git a/internal/attestation/azure/snp/testdata/vcek.pem b/internal/attestation/snp/testdata/vcek.pem similarity index 100% rename from internal/attestation/azure/snp/testdata/vcek.pem rename to internal/attestation/snp/testdata/vcek.pem