From 6e951501b013f9eec417f0a1e1e451cda7848c43 Mon Sep 17 00:00:00 2001 From: Otto Bittner Date: Mon, 30 Oct 2023 12:31:42 +0100 Subject: [PATCH 01/12] attestation: add snp package The package holds code shared between SNP-based attestation implementations on AWS and Azure . --- internal/attestation/azure/snp/BUILD.bazel | 5 +- internal/attestation/azure/snp/issuer.go | 3 +- internal/attestation/azure/snp/issuer_test.go | 3 +- internal/attestation/azure/snp/validator.go | 226 +------------ .../attestation/azure/snp/validator_test.go | 295 ++-------------- internal/attestation/snp/BUILD.bazel | 31 ++ internal/attestation/snp/snp.go | 217 ++++++++++++ internal/attestation/snp/snp_test.go | 315 ++++++++++++++++++ .../{azure => }/snp/testdata/BUILD.bazel | 2 +- .../{azure => }/snp/testdata/attestation.bin | Bin .../{azure => }/snp/testdata/certchain.pem | 0 .../{azure => }/snp/testdata/runtimedata.bin | 0 .../{azure => }/snp/testdata/testdata.go | 0 .../{azure => }/snp/testdata/vcek.cert | Bin .../{azure => }/snp/testdata/vcek.pem | 0 15 files changed, 608 insertions(+), 489 deletions(-) create mode 100644 internal/attestation/snp/BUILD.bazel create mode 100644 internal/attestation/snp/snp.go create mode 100644 internal/attestation/snp/snp_test.go rename internal/attestation/{azure => }/snp/testdata/BUILD.bazel (91%) rename internal/attestation/{azure => }/snp/testdata/attestation.bin (100%) rename internal/attestation/{azure => }/snp/testdata/certchain.pem (100%) rename internal/attestation/{azure => }/snp/testdata/runtimedata.bin (100%) rename internal/attestation/{azure => }/snp/testdata/testdata.go (100%) rename internal/attestation/{azure => }/snp/testdata/vcek.cert (100%) rename internal/attestation/{azure => }/snp/testdata/vcek.pem (100%) diff --git a/internal/attestation/azure/snp/BUILD.bazel b/internal/attestation/azure/snp/BUILD.bazel index 13e0643809..7adea0c3e5 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 d527044aa4..dbba2cefb9 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 4f61c9fabd..3ecc9e29cd 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 4ea503f85c..fa31893a42 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 -} - -// 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 +type attestationKey struct { + PublicPart []akPub `json:"keys"` } -// 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 71b9fc9543..e3443bf051 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 0000000000..613751925e --- /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 0000000000..281b9c662f --- /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 0000000000..bfc0f225be --- /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 9b09a6fdbd..54369e85a4 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 From 91f7708e0238690a6339bee6a62719570fa73587 Mon Sep 17 00:00:00 2001 From: Otto Bittner Date: Mon, 30 Oct 2023 12:39:15 +0100 Subject: [PATCH 02/12] cli: use new instance info struct in verify This ensure that issuer and verify (as consumer) use the same types for marshalling/unmarshalling. --- cli/internal/cmd/BUILD.bazel | 1 + cli/internal/cmd/verify.go | 27 +++++++++------------------ 2 files changed, 10 insertions(+), 18 deletions(-) diff --git a/cli/internal/cmd/BUILD.bazel b/cli/internal/cmd/BUILD.bazel index 7beeb2f238..3f927b33f0 100644 --- a/cli/internal/cmd/BUILD.bazel +++ b/cli/internal/cmd/BUILD.bazel @@ -58,6 +58,7 @@ go_library( "//internal/api/versionsapi", "//internal/atls", "//internal/attestation/measurements", + "//internal/attestation/snp", "//internal/attestation/variant", "//internal/attestation/vtpm", "//internal/cloud/cloudprovider", diff --git a/cli/internal/cmd/verify.go b/cli/internal/cmd/verify.go index b8f31b7007..42bb0f8a3a 100644 --- a/cli/internal/cmd/verify.go +++ b/cli/internal/cmd/verify.go @@ -29,6 +29,7 @@ import ( "github.com/edgelesssys/constellation/v2/internal/api/attestationconfigapi" "github.com/edgelesssys/constellation/v2/internal/atls" "github.com/edgelesssys/constellation/v2/internal/attestation/measurements" + "github.com/edgelesssys/constellation/v2/internal/attestation/snp" "github.com/edgelesssys/constellation/v2/internal/attestation/vtpm" "github.com/edgelesssys/constellation/v2/internal/cloud/cloudprovider" "github.com/edgelesssys/constellation/v2/internal/config" @@ -282,7 +283,7 @@ func (f *jsonAttestationDocFormatter) format(ctx context.Context, docString stri return "", fmt.Errorf("parsing SNP report: %w", err) } - vcek, err := newCertificates("VCEK certificate", instanceInfo.Vcek, f.log) + vcek, err := newCertificates("VCEK certificate", instanceInfo.VCEK, f.log) if err != nil { return "", fmt.Errorf("parsing VCEK certificate: %w", err) } @@ -348,12 +349,12 @@ func (f *defaultAttestationDocFormatter) format(ctx context.Context, docString s return "", fmt.Errorf("decode instance info: %w", err) } - var instanceInfo azureInstanceInfo + var instanceInfo snp.InstanceInfo if err := json.Unmarshal(instanceInfoString, &instanceInfo); err != nil { return "", fmt.Errorf("unmarshal instance info: %w", err) } - if err := f.parseCerts(b, "VCEK certificate", instanceInfo.Vcek); err != nil { + if err := f.parseCerts(b, "VCEK certificate", instanceInfo.VCEK); err != nil { return "", fmt.Errorf("print VCEK certificate: %w", err) } if err := f.parseCerts(b, "Certificate chain", instanceInfo.CertChain); err != nil { @@ -609,16 +610,6 @@ type attestationDoc struct { UserData string `json:"UserData"` } -// azureInstanceInfo is the b64-decoded InstanceInfo field of the attestation document. -// as of now (2023-04-03), it only contains interesting data on Azure. -type azureInstanceInfo struct { - Vcek []byte - CertChain []byte - AttestationReport []byte - RuntimeData []byte - MAAToken string -} - type constellationVerifier struct { dialer grpcInsecureDialer log debugLog @@ -837,20 +828,20 @@ func newTCBVersion(tcbVersion kds.TCBVersion) (res verify.TCBVersion) { } } -func extractAzureInstanceInfo(docString string) (azureInstanceInfo, error) { +func extractAzureInstanceInfo(docString string) (snp.InstanceInfo, error) { var doc attestationDoc if err := json.Unmarshal([]byte(docString), &doc); err != nil { - return azureInstanceInfo{}, fmt.Errorf("unmarshal attestation document: %w", err) + return snp.InstanceInfo{}, fmt.Errorf("unmarshal attestation document: %w", err) } instanceInfoString, err := base64.StdEncoding.DecodeString(doc.InstanceInfo) if err != nil { - return azureInstanceInfo{}, fmt.Errorf("decode instance info: %w", err) + return snp.InstanceInfo{}, fmt.Errorf("decode instance info: %w", err) } - var instanceInfo azureInstanceInfo + var instanceInfo snp.InstanceInfo if err := json.Unmarshal(instanceInfoString, &instanceInfo); err != nil { - return azureInstanceInfo{}, fmt.Errorf("unmarshal instance info: %w", err) + return snp.InstanceInfo{}, fmt.Errorf("unmarshal instance info: %w", err) } return instanceInfo, nil } From 80feadc683df6c9b253b5fedadb452c2df61d119 Mon Sep 17 00:00:00 2001 From: Otto Bittner Date: Tue, 7 Nov 2023 15:19:31 +0100 Subject: [PATCH 03/12] verify: move CSP-specific code to internal/verify With the introduction of SNP-based attestation on AWS some of the information in the report (MAAToken) is not applicable to all attestation reports anymore. Thus, make verify cmd CSP-agnostic and move CSP-specific logic to internal/verify. Also make internal/attestation/snp CSP aware. --- cli/internal/cmd/BUILD.bazel | 3 - cli/internal/cmd/verify.go | 430 +-------------- cli/internal/cmd/verify_test.go | 76 --- internal/attestation/azure/snp/issuer.go | 8 +- internal/attestation/azure/snp/issuer_test.go | 6 +- internal/attestation/azure/snp/validator.go | 7 +- .../attestation/azure/snp/validator_test.go | 16 +- internal/attestation/snp/snp.go | 20 +- internal/attestation/snp/snp_test.go | 4 +- internal/verify/BUILD.bazel | 26 +- internal/verify/certchain.go | 29 + internal/verify/verify.go | 503 +++++++++++++++++- internal/verify/verify_test.go | 64 +++ 13 files changed, 663 insertions(+), 529 deletions(-) create mode 100644 internal/verify/certchain.go create mode 100644 internal/verify/verify_test.go diff --git a/cli/internal/cmd/BUILD.bazel b/cli/internal/cmd/BUILD.bazel index 3f927b33f0..9b5a8ca95f 100644 --- a/cli/internal/cmd/BUILD.bazel +++ b/cli/internal/cmd/BUILD.bazel @@ -88,9 +88,6 @@ go_library( "//internal/verify", "//internal/versions", "//verify/verifyproto", - "@com_github_golang_jwt_jwt_v5//:jwt", - "@com_github_google_go_sev_guest//abi", - "@com_github_google_go_sev_guest//kds", "@com_github_google_go_tpm_tools//proto/tpm", "@com_github_google_uuid//:uuid", "@com_github_mattn_go_isatty//:go-isatty", diff --git a/cli/internal/cmd/verify.go b/cli/internal/cmd/verify.go index 42bb0f8a3a..59d3bd5708 100644 --- a/cli/internal/cmd/verify.go +++ b/cli/internal/cmd/verify.go @@ -9,16 +9,11 @@ package cmd import ( "bytes" "context" - "crypto/x509" "encoding/base64" "encoding/json" - "encoding/pem" "errors" "fmt" - "io" "net" - "net/http" - "net/url" "sort" "strconv" "strings" @@ -40,9 +35,6 @@ import ( "github.com/edgelesssys/constellation/v2/internal/state" "github.com/edgelesssys/constellation/v2/internal/verify" "github.com/edgelesssys/constellation/v2/verify/verifyproto" - "github.com/golang-jwt/jwt/v5" - "github.com/google/go-sev-guest/abi" - "github.com/google/go-sev-guest/kds" "github.com/spf13/afero" "github.com/spf13/cobra" "github.com/spf13/pflag" @@ -213,7 +205,7 @@ func (c *verifyCmd) verify(cmd *cobra.Command, verifyClient verifyClient, factor attDocOutput, err := formatter.format( cmd.Context(), rawAttestationDoc, - conf.Provider.Azure == nil, + (conf.Provider.Azure == nil && conf.Provider.AWS == nil), attConfig.GetMeasurements(), maaURL, ) @@ -274,34 +266,20 @@ type jsonAttestationDocFormatter struct { func (f *jsonAttestationDocFormatter) format(ctx context.Context, docString string, _ bool, _ measurements.M, attestationServiceURL string, ) (string, error) { - instanceInfo, err := extractAzureInstanceInfo(docString) - if err != nil { - return "", fmt.Errorf("unmarshal instance info: %w", err) - } - snpReport, err := newSNPReport(instanceInfo.AttestationReport) - if err != nil { - return "", fmt.Errorf("parsing SNP report: %w", err) + var doc attestationDoc + if err := json.Unmarshal([]byte(docString), &doc); err != nil { + return "", fmt.Errorf("unmarshal attestation document: %w", err) } - vcek, err := newCertificates("VCEK certificate", instanceInfo.VCEK, f.log) + instanceInfo, err := extractInstanceInfo(doc) if err != nil { - return "", fmt.Errorf("parsing VCEK certificate: %w", err) + return "", fmt.Errorf("unmarshalling instance info: %w", err) } - certChain, err := newCertificates("Certificate chain", instanceInfo.CertChain, f.log) + report, err := verify.NewReport(ctx, instanceInfo, attestationServiceURL, f.log) if err != nil { - return "", fmt.Errorf("parsing certificate chain: %w", err) - } - maaToken, err := newMAAToken(ctx, instanceInfo.MAAToken, attestationServiceURL) - if err != nil { - return "", fmt.Errorf("parsing MAA token: %w", err) + return "", fmt.Errorf("parsing SNP report: %w", err) } - report := verify.Report{ - SNPReport: snpReport, - VCEK: vcek, - CertChain: certChain, - MAAToken: maaToken, - } jsonBytes, err := json.Marshal(report) return string(jsonBytes), err @@ -344,97 +322,17 @@ func (f *defaultAttestationDocFormatter) format(ctx context.Context, docString s return b.String(), nil } - instanceInfoString, err := base64.StdEncoding.DecodeString(doc.InstanceInfo) + instanceInfo, err := extractInstanceInfo(doc) if err != nil { - return "", fmt.Errorf("decode instance info: %w", err) + return "", fmt.Errorf("unmarshalling instance info: %w", err) } - var instanceInfo snp.InstanceInfo - if err := json.Unmarshal(instanceInfoString, &instanceInfo); err != nil { - return "", fmt.Errorf("unmarshal instance info: %w", err) - } - - if err := f.parseCerts(b, "VCEK certificate", instanceInfo.VCEK); err != nil { - return "", fmt.Errorf("print VCEK certificate: %w", err) - } - if err := f.parseCerts(b, "Certificate chain", instanceInfo.CertChain); err != nil { - return "", fmt.Errorf("print certificate chain: %w", err) - } - snpReport, err := newSNPReport(instanceInfo.AttestationReport) + report, err := verify.NewReport(ctx, instanceInfo, attestationServiceURL, f.log) if err != nil { return "", fmt.Errorf("parsing SNP report: %w", err) } - f.buildSNPReport(b, snpReport) - if err := parseMAAToken(ctx, b, instanceInfo.MAAToken, attestationServiceURL); err != nil { - return "", fmt.Errorf("print MAA token: %w", err) - } - return b.String(), nil -} - -// parseCerts parses the PEM certificates and writes their details to the output builder. -func (f *defaultAttestationDocFormatter) parseCerts(b *strings.Builder, certTypeName string, cert []byte) error { - newlinesTrimmed := strings.TrimSpace(string(cert)) - formattedCert := strings.ReplaceAll(newlinesTrimmed, "\n", "\n\t\t") + "\n" - b.WriteString(fmt.Sprintf("\tRaw %s:\n\t\t%s", certTypeName, formattedCert)) - - f.log.Debugf("Decoding PEM certificate: %s", certTypeName) - i := 1 - var rest []byte - var block *pem.Block - for block, rest = pem.Decode([]byte(newlinesTrimmed)); block != nil; block, rest = pem.Decode(rest) { - f.log.Debugf("Parsing PEM block: %d", i) - if block.Type != "CERTIFICATE" { - return fmt.Errorf("parse %s: expected PEM block type 'CERTIFICATE', got '%s'", certTypeName, block.Type) - } - - cert, err := x509.ParseCertificate(block.Bytes) - if err != nil { - return fmt.Errorf("parse %s: %w", certTypeName, err) - } - - writeIndentfln(b, 1, "%s (%d):", certTypeName, i) - writeIndentfln(b, 2, "Serial Number: %s", cert.SerialNumber) - writeIndentfln(b, 2, "Subject: %s", cert.Subject) - writeIndentfln(b, 2, "Issuer: %s", cert.Issuer) - writeIndentfln(b, 2, "Not Before: %s", cert.NotBefore) - writeIndentfln(b, 2, "Not After: %s", cert.NotAfter) - writeIndentfln(b, 2, "Signature Algorithm: %s", cert.SignatureAlgorithm) - writeIndentfln(b, 2, "Public Key Algorithm: %s", cert.PublicKeyAlgorithm) - - if certTypeName == "VCEK certificate" { - // Extensions documented in Table 8 and Table 9 of - // https://www.amd.com/system/files/TechDocs/57230.pdf - vcekExts, err := kds.VcekCertificateExtensions(cert) - if err != nil { - return fmt.Errorf("parsing VCEK certificate extensions: %w", err) - } - - writeIndentfln(b, 2, "Struct version: %d", vcekExts.StructVersion) - writeIndentfln(b, 2, "Product name: %s", vcekExts.ProductName) - tcb := kds.DecomposeTCBVersion(vcekExts.TCBVersion) - writeIndentfln(b, 2, "Secure Processor bootloader SVN: %d", tcb.BlSpl) - writeIndentfln(b, 2, "Secure Processor operating system SVN: %d", tcb.TeeSpl) - writeIndentfln(b, 2, "SVN 4 (reserved): %d", tcb.Spl4) - writeIndentfln(b, 2, "SVN 5 (reserved): %d", tcb.Spl5) - writeIndentfln(b, 2, "SVN 6 (reserved): %d", tcb.Spl6) - writeIndentfln(b, 2, "SVN 7 (reserved): %d", tcb.Spl7) - writeIndentfln(b, 2, "SEV-SNP firmware SVN: %d", tcb.SnpSpl) - writeIndentfln(b, 2, "Microcode SVN: %d", tcb.UcodeSpl) - writeIndentfln(b, 2, "Hardware ID: %x", vcekExts.HWID) - } - - i++ - } - - if i == 1 { - return fmt.Errorf("parse %s: no PEM blocks found", certTypeName) - } - if len(rest) != 0 { - return fmt.Errorf("parse %s: remaining PEM block is not a valid certificate: %s", certTypeName, rest) - } - - return nil + return report.FormatString(b) } // parseQuotes parses the base64-encoded quotes and writes their details to the output builder. @@ -465,139 +363,6 @@ func (f *defaultAttestationDocFormatter) parseQuotes(b *strings.Builder, quotes return nil } -func (f *defaultAttestationDocFormatter) buildSNPReport(b *strings.Builder, report verify.SNPReport) { - writeTCB := func(tcb verify.TCBVersion) { - writeIndentfln(b, 3, "Secure Processor bootloader SVN: %d", tcb.Bootloader) - writeIndentfln(b, 3, "Secure Processor operating system SVN: %d", tcb.TEE) - writeIndentfln(b, 3, "SVN 4 (reserved): %d", tcb.Spl4) - writeIndentfln(b, 3, "SVN 5 (reserved): %d", tcb.Spl5) - writeIndentfln(b, 3, "SVN 6 (reserved): %d", tcb.Spl6) - writeIndentfln(b, 3, "SVN 7 (reserved): %d", tcb.Spl7) - writeIndentfln(b, 3, "SEV-SNP firmware SVN: %d", tcb.SNP) - writeIndentfln(b, 3, "Microcode SVN: %d", tcb.Microcode) - } - - writeIndentfln(b, 1, "SNP Report:") - writeIndentfln(b, 2, "Version: %d", report.Version) - writeIndentfln(b, 2, "Guest SVN: %d", report.GuestSvn) - writeIndentfln(b, 2, "Policy:") - writeIndentfln(b, 3, "ABI Minor: %d", report.PolicyABIMinor) - writeIndentfln(b, 3, "ABI Major: %d", report.PolicyABIMajor) - writeIndentfln(b, 3, "Symmetric Multithreading enabled: %t", report.PolicySMT) - writeIndentfln(b, 3, "Migration agent enabled: %t", report.PolicyMigrationAgent) - writeIndentfln(b, 3, "Debugging enabled (host decryption of VM): %t", report.PolicyDebug) - writeIndentfln(b, 3, "Single socket enabled: %t", report.PolicySingleSocket) - writeIndentfln(b, 2, "Family ID: %x", report.FamilyID) - writeIndentfln(b, 2, "Image ID: %x", report.ImageID) - writeIndentfln(b, 2, "VMPL: %d", report.Vmpl) - writeIndentfln(b, 2, "Signature Algorithm: %d", report.SignatureAlgo) - writeIndentfln(b, 2, "Current TCB:") - writeTCB(report.CurrentTCB) - writeIndentfln(b, 2, "Platform Info:") - writeIndentfln(b, 3, "Symmetric Multithreading enabled (SMT): %t", report.PlatformInfo.SMT) - writeIndentfln(b, 3, "Transparent secure memory encryption (TSME): %t", report.PlatformInfo.TSME) - writeIndentfln(b, 2, "Signer Info:") - writeIndentfln(b, 3, "Author Key Enabled: %t", report.SignerInfo.AuthorKey) - writeIndentfln(b, 3, "Chip ID Masking: %t", report.SignerInfo.MaskChipKey) - writeIndentfln(b, 3, "Signing Type: %s", report.SignerInfo.SigningKey) - writeIndentfln(b, 2, "Report Data: %x", report.ReportData) - writeIndentfln(b, 2, "Measurement: %x", report.Measurement) - writeIndentfln(b, 2, "Host Data: %x", report.HostData) - writeIndentfln(b, 2, "ID Key Digest: %x", report.IDKeyDigest) - writeIndentfln(b, 2, "Author Key Digest: %x", report.AuthorKeyDigest) - writeIndentfln(b, 2, "Report ID: %x", report.ReportID) - writeIndentfln(b, 2, "Report ID MA: %x", report.ReportIDMa) - writeIndentfln(b, 2, "Reported TCB:") - writeTCB(report.ReportedTCB) - writeIndentfln(b, 2, "Chip ID: %x", report.ChipID) - writeIndentfln(b, 2, "Committed TCB:") - writeTCB(report.CommittedTCB) - writeIndentfln(b, 2, "Current Build: %d", report.CurrentBuild) - writeIndentfln(b, 2, "Current Minor: %d", report.CurrentMinor) - writeIndentfln(b, 2, "Current Major: %d", report.CurrentMajor) - writeIndentfln(b, 2, "Committed Build: %d", report.CommittedBuild) - writeIndentfln(b, 2, "Committed Minor: %d", report.CommittedMinor) - writeIndentfln(b, 2, "Committed Major: %d", report.CommittedMajor) - writeIndentfln(b, 2, "Launch TCB:") - writeTCB(report.LaunchTCB) - writeIndentfln(b, 2, "Signature (DER):") - writeIndentfln(b, 3, "%x", report.Signature) -} - -func parseMAAToken(ctx context.Context, b *strings.Builder, rawToken, attestationServiceURL string) error { - var claims verify.MaaTokenClaims - _, err := jwt.ParseWithClaims(rawToken, &claims, keyFromJKUFunc(ctx, attestationServiceURL), jwt.WithIssuedAt()) - if err != nil { - return fmt.Errorf("parsing token: %w", err) - } - - out, err := json.MarshalIndent(claims, "\t\t", " ") - if err != nil { - return fmt.Errorf("marshaling claims: %w", err) - } - - b.WriteString("\tMicrosoft Azure Attestation Token:\n\t") - b.WriteString(string(out)) - return nil -} - -// keyFromJKUFunc returns a function that gets the JSON Web Key URI from the token -// and fetches the key from that URI. The keys are then parsed, and the key with -// the kid that matches the token header is returned. -func keyFromJKUFunc(ctx context.Context, webKeysURLBase string) func(token *jwt.Token) (any, error) { - return func(token *jwt.Token) (any, error) { - webKeysURL, err := url.JoinPath(webKeysURLBase, "certs") - if err != nil { - return nil, fmt.Errorf("joining web keys base URL with path: %w", err) - } - - if token.Header["alg"] != "RS256" { - return nil, fmt.Errorf("invalid signing algorithm: %s", token.Header["alg"]) - } - kid, ok := token.Header["kid"].(string) - if !ok { - return nil, fmt.Errorf("invalid kid: %v", token.Header["kid"]) - } - jku, ok := token.Header["jku"].(string) - if !ok { - return nil, fmt.Errorf("invalid jku: %v", token.Header["jku"]) - } - if jku != webKeysURL { - return nil, fmt.Errorf("jku from token (%s) does not match configured attestation service (%s)", jku, webKeysURL) - } - - keySetBytes, err := httpGet(ctx, jku) - if err != nil { - return nil, fmt.Errorf("getting signing keys from jku %s: %w", jku, err) - } - - var rawKeySet struct { - Keys []struct { - X5c [][]byte - Kid string - } - } - - if err := json.Unmarshal(keySetBytes, &rawKeySet); err != nil { - return nil, err - } - - for _, key := range rawKeySet.Keys { - if key.Kid != kid { - continue - } - cert, err := x509.ParseCertificate(key.X5c[0]) - if err != nil { - return nil, fmt.Errorf("parsing certificate: %w", err) - } - - return cert.PublicKey, nil - } - - return nil, fmt.Errorf("no key found for kid %s", kid) - } -} - // attestationDoc is the attestation document returned by the verifier. type attestationDoc struct { Attestation struct { @@ -664,176 +429,7 @@ func writeIndentfln(b *strings.Builder, indentLvl int, format string, args ...an b.WriteString(fmt.Sprintf(format+"\n", args...)) } -func httpGet(ctx context.Context, url string) ([]byte, error) { - req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, http.NoBody) - if err != nil { - return nil, err - } - resp, err := http.DefaultClient.Do(req) - if err != nil { - return nil, err - } - defer resp.Body.Close() - if resp.StatusCode != http.StatusOK { - return nil, errors.New(resp.Status) - } - body, err := io.ReadAll(resp.Body) - if err != nil { - return nil, err - } - return body, nil -} - -func newCertificates(certTypeName string, cert []byte, log debugLog) (certs []verify.Certificate, err error) { - newlinesTrimmed := strings.TrimSpace(string(cert)) - - log.Debugf("Decoding PEM certificate: %s", certTypeName) - i := 1 - var rest []byte - var block *pem.Block - for block, rest = pem.Decode([]byte(newlinesTrimmed)); block != nil; block, rest = pem.Decode(rest) { - log.Debugf("Parsing PEM block: %d", i) - if block.Type != "CERTIFICATE" { - return certs, fmt.Errorf("parse %s: expected PEM block type 'CERTIFICATE', got '%s'", certTypeName, block.Type) - } - - cert, err := x509.ParseCertificate(block.Bytes) - if err != nil { - return certs, fmt.Errorf("parse %s: %w", certTypeName, err) - } - if certTypeName == "VCEK certificate" { - vcekExts, err := kds.VcekCertificateExtensions(cert) - if err != nil { - return certs, fmt.Errorf("parsing VCEK certificate extensions: %w", err) - } - certPEM := pem.EncodeToMemory(&pem.Block{ - Type: "CERTIFICATE", - Bytes: cert.Raw, - }) - certs = append(certs, verify.Certificate{ - CertificatePEM: string(certPEM), - CertTypeName: certTypeName, - StructVersion: vcekExts.StructVersion, - ProductName: vcekExts.ProductName, - TCBVersion: newTCBVersion(vcekExts.TCBVersion), - HardwareID: vcekExts.HWID, - }) - } else { - certPEM := pem.EncodeToMemory(&pem.Block{ - Type: "CERTIFICATE", - Bytes: cert.Raw, - }) - certs = append(certs, verify.Certificate{ - CertificatePEM: string(certPEM), - CertTypeName: certTypeName, - }) - } - i++ - } - if i == 1 { - return certs, fmt.Errorf("parse %s: no PEM blocks found", certTypeName) - } - if len(rest) != 0 { - return certs, fmt.Errorf("parse %s: remaining PEM block is not a valid certificate: %s", certTypeName, rest) - } - return certs, nil -} - -func newSNPReport(reportBytes []byte) (res verify.SNPReport, err error) { - report, err := abi.ReportToProto(reportBytes) - if err != nil { - return res, fmt.Errorf("parsing report to proto: %w", err) - } - - policy, err := abi.ParseSnpPolicy(report.Policy) - if err != nil { - return res, fmt.Errorf("parsing policy: %w", err) - } - - platformInfo, err := abi.ParseSnpPlatformInfo(report.PlatformInfo) - if err != nil { - return res, fmt.Errorf("parsing platform info: %w", err) - } - - signature, err := abi.ReportToSignatureDER(reportBytes) - if err != nil { - return res, fmt.Errorf("parsing signature: %w", err) - } - - signerInfo, err := abi.ParseSignerInfo(report.SignerInfo) - if err != nil { - return res, fmt.Errorf("parsing signer info: %w", err) - } - return verify.SNPReport{ - Version: report.Version, - GuestSvn: report.GuestSvn, - PolicyABIMinor: policy.ABIMinor, - PolicyABIMajor: policy.ABIMajor, - PolicySMT: policy.SMT, - PolicyMigrationAgent: policy.MigrateMA, - PolicyDebug: policy.Debug, - PolicySingleSocket: policy.SingleSocket, - FamilyID: report.FamilyId, - ImageID: report.ImageId, - Vmpl: report.Vmpl, - SignatureAlgo: report.SignatureAlgo, - CurrentTCB: newTCBVersion(kds.TCBVersion(report.CurrentTcb)), - PlatformInfo: verify.PlatformInfo{ - SMT: platformInfo.SMTEnabled, - TSME: platformInfo.TSMEEnabled, - }, - SignerInfo: verify.SignerInfo{ - AuthorKey: signerInfo.AuthorKeyEn, - MaskChipKey: signerInfo.MaskChipKey, - SigningKey: signerInfo.SigningKey.String(), - }, - ReportData: report.ReportData, - Measurement: report.Measurement, - HostData: report.HostData, - IDKeyDigest: report.IdKeyDigest, - AuthorKeyDigest: report.AuthorKeyDigest, - ReportID: report.ReportId, - ReportIDMa: report.ReportIdMa, - ReportedTCB: newTCBVersion(kds.TCBVersion(report.ReportedTcb)), - ChipID: report.ChipId, - CommittedTCB: newTCBVersion(kds.TCBVersion(report.CommittedTcb)), - CurrentBuild: report.CurrentBuild, - CurrentMinor: report.CurrentMinor, - CurrentMajor: report.CurrentMajor, - CommittedBuild: report.CommittedBuild, - CommittedMinor: report.CommittedMinor, - CommittedMajor: report.CommittedMajor, - LaunchTCB: newTCBVersion(kds.TCBVersion(report.LaunchTcb)), - Signature: signature, - }, nil -} - -func newMAAToken(ctx context.Context, rawToken, attestationServiceURL string) (verify.MaaTokenClaims, error) { - var claims verify.MaaTokenClaims - _, err := jwt.ParseWithClaims(rawToken, &claims, keyFromJKUFunc(ctx, attestationServiceURL), jwt.WithIssuedAt()) - return claims, err -} - -func newTCBVersion(tcbVersion kds.TCBVersion) (res verify.TCBVersion) { - tcb := kds.DecomposeTCBVersion(tcbVersion) - return verify.TCBVersion{ - Bootloader: tcb.BlSpl, - TEE: tcb.TeeSpl, - SNP: tcb.SnpSpl, - Microcode: tcb.UcodeSpl, - Spl4: tcb.Spl4, - Spl5: tcb.Spl5, - Spl6: tcb.Spl6, - Spl7: tcb.Spl7, - } -} - -func extractAzureInstanceInfo(docString string) (snp.InstanceInfo, error) { - var doc attestationDoc - if err := json.Unmarshal([]byte(docString), &doc); err != nil { - return snp.InstanceInfo{}, fmt.Errorf("unmarshal attestation document: %w", err) - } - +func extractInstanceInfo(doc attestationDoc) (snp.InstanceInfo, error) { instanceInfoString, err := base64.StdEncoding.DecodeString(doc.InstanceInfo) if err != nil { return snp.InstanceInfo{}, fmt.Errorf("decode instance info: %w", err) diff --git a/cli/internal/cmd/verify_test.go b/cli/internal/cmd/verify_test.go index f381c86b83..a874125c31 100644 --- a/cli/internal/cmd/verify_test.go +++ b/cli/internal/cmd/verify_test.go @@ -268,82 +268,6 @@ func TestFormat(t *testing.T) { } } -func TestParseCerts(t *testing.T) { - validCert := `-----BEGIN CERTIFICATE----- -MIIFTDCCAvugAwIBAgIBADBGBgkqhkiG9w0BAQowOaAPMA0GCWCGSAFlAwQCAgUA -oRwwGgYJKoZIhvcNAQEIMA0GCWCGSAFlAwQCAgUAogMCATCjAwIBATB7MRQwEgYD -VQQLDAtFbmdpbmVlcmluZzELMAkGA1UEBhMCVVMxFDASBgNVBAcMC1NhbnRhIENs -YXJhMQswCQYDVQQIDAJDQTEfMB0GA1UECgwWQWR2YW5jZWQgTWljcm8gRGV2aWNl -czESMBAGA1UEAwwJU0VWLU1pbGFuMB4XDTIyMTEyMzIyMzM0N1oXDTI5MTEyMzIy -MzM0N1owejEUMBIGA1UECwwLRW5naW5lZXJpbmcxCzAJBgNVBAYTAlVTMRQwEgYD -VQQHDAtTYW50YSBDbGFyYTELMAkGA1UECAwCQ0ExHzAdBgNVBAoMFkFkdmFuY2Vk -IE1pY3JvIERldmljZXMxETAPBgNVBAMMCFNFVi1WQ0VLMHYwEAYHKoZIzj0CAQYF -K4EEACIDYgAEVGm4GomfpkiziqEYP61nfKaz5OjDLr8Y0POrv4iAnFVHAmBT81Ms -gfSLKL5r3V3mNzl1Zh7jwSBft14uhGdwpARoK0YNQc4OvptqVIiv2RprV53DMzge -rtwiumIargiCo4IBFjCCARIwEAYJKwYBBAGceAEBBAMCAQAwFwYJKwYBBAGceAEC -BAoWCE1pbGFuLUIwMBEGCisGAQQBnHgBAwEEAwIBAzARBgorBgEEAZx4AQMCBAMC -AQAwEQYKKwYBBAGceAEDBAQDAgEAMBEGCisGAQQBnHgBAwUEAwIBADARBgorBgEE -AZx4AQMGBAMCAQAwEQYKKwYBBAGceAEDBwQDAgEAMBEGCisGAQQBnHgBAwMEAwIB -CDARBgorBgEEAZx4AQMIBAMCAXMwTQYJKwYBBAGceAEEBEB80kCZ1oAyCjWC6w3m -xOz+i4t6dFjk/Bqhm7+Jscf8D62CXtlwcKc4aM9CdO4LuKlwpdTU80VNQc6ZEuMF -VzbRMEYGCSqGSIb3DQEBCjA5oA8wDQYJYIZIAWUDBAICBQChHDAaBgkqhkiG9w0B -AQgwDQYJYIZIAWUDBAICBQCiAwIBMKMDAgEBA4ICAQCN1qBYOywoZWGnQvk6u0Oh -5zkEKykXU6sK8hA6L65rQcqWUjEHDa9AZUpx3UuCmpPc24dx6DTHc58M7TxcyKry -8s4CvruBKFbQ6B8MHnH6k07MzsmiBnsiIhAscZ0ipGm6h8e/VM/6ULrAcVSxZ+Mh -D/IogZAuCQARsGQ4QYXBT8Qc5mLnTkx30m1rZVlp1VcN4ngOo/1tz1jj1mfpG2zv -wNcQa9LwAzRLnnmLpxXA2OMbl7AaTWQenpL9rzBON2sg4OBl6lVhaSU0uBbFyCmR -RvBqKC0iDD6TvyIikkMq05v5YwIKFYw++ICndz+fKcLEULZbziAsZ52qjM8iPVHC -pN0yhVOr2g22F9zxlGH3WxTl9ymUytuv3vJL/aJiQM+n/Ri90Sc05EK4oIJ3+BS8 -yu5cVy9o2cQcOcQ8rhQh+Kv1sR9xrs25EXZF8KEETfhoJnN6KY1RwG7HsOfAQ3dV -LWInQRaC/8JPyVS2zbd0+NRBJOnq4/quv/P3C4SBP98/ZuGrqN59uifyqC3Kodkl -WkG/2UdhiLlCmOtsU+BYDZrSiYK1R9FNnlQCOGrkuVxpDwa2TbbvEEzQP7RXxotA -KlxejvrY4VuK8agNqvffVofbdIIperK65K4+0mYIb+A6fU8QQHlCbti4ERSZ6UYD -F/SjRih31+SAtWb42jueAA== ------END CERTIFICATE----- -` - validCertExpected := "\tRaw Some Cert:\n\t\t-----BEGIN CERTIFICATE-----\n\t\tMIIFTDCCAvugAwIBAgIBADBGBgkqhkiG9w0BAQowOaAPMA0GCWCGSAFlAwQCAgUA\n\t\toRwwGgYJKoZIhvcNAQEIMA0GCWCGSAFlAwQCAgUAogMCATCjAwIBATB7MRQwEgYD\n\t\tVQQLDAtFbmdpbmVlcmluZzELMAkGA1UEBhMCVVMxFDASBgNVBAcMC1NhbnRhIENs\n\t\tYXJhMQswCQYDVQQIDAJDQTEfMB0GA1UECgwWQWR2YW5jZWQgTWljcm8gRGV2aWNl\n\t\tczESMBAGA1UEAwwJU0VWLU1pbGFuMB4XDTIyMTEyMzIyMzM0N1oXDTI5MTEyMzIy\n\t\tMzM0N1owejEUMBIGA1UECwwLRW5naW5lZXJpbmcxCzAJBgNVBAYTAlVTMRQwEgYD\n\t\tVQQHDAtTYW50YSBDbGFyYTELMAkGA1UECAwCQ0ExHzAdBgNVBAoMFkFkdmFuY2Vk\n\t\tIE1pY3JvIERldmljZXMxETAPBgNVBAMMCFNFVi1WQ0VLMHYwEAYHKoZIzj0CAQYF\n\t\tK4EEACIDYgAEVGm4GomfpkiziqEYP61nfKaz5OjDLr8Y0POrv4iAnFVHAmBT81Ms\n\t\tgfSLKL5r3V3mNzl1Zh7jwSBft14uhGdwpARoK0YNQc4OvptqVIiv2RprV53DMzge\n\t\trtwiumIargiCo4IBFjCCARIwEAYJKwYBBAGceAEBBAMCAQAwFwYJKwYBBAGceAEC\n\t\tBAoWCE1pbGFuLUIwMBEGCisGAQQBnHgBAwEEAwIBAzARBgorBgEEAZx4AQMCBAMC\n\t\tAQAwEQYKKwYBBAGceAEDBAQDAgEAMBEGCisGAQQBnHgBAwUEAwIBADARBgorBgEE\n\t\tAZx4AQMGBAMCAQAwEQYKKwYBBAGceAEDBwQDAgEAMBEGCisGAQQBnHgBAwMEAwIB\n\t\tCDARBgorBgEEAZx4AQMIBAMCAXMwTQYJKwYBBAGceAEEBEB80kCZ1oAyCjWC6w3m\n\t\txOz+i4t6dFjk/Bqhm7+Jscf8D62CXtlwcKc4aM9CdO4LuKlwpdTU80VNQc6ZEuMF\n\t\tVzbRMEYGCSqGSIb3DQEBCjA5oA8wDQYJYIZIAWUDBAICBQChHDAaBgkqhkiG9w0B\n\t\tAQgwDQYJYIZIAWUDBAICBQCiAwIBMKMDAgEBA4ICAQCN1qBYOywoZWGnQvk6u0Oh\n\t\t5zkEKykXU6sK8hA6L65rQcqWUjEHDa9AZUpx3UuCmpPc24dx6DTHc58M7TxcyKry\n\t\t8s4CvruBKFbQ6B8MHnH6k07MzsmiBnsiIhAscZ0ipGm6h8e/VM/6ULrAcVSxZ+Mh\n\t\tD/IogZAuCQARsGQ4QYXBT8Qc5mLnTkx30m1rZVlp1VcN4ngOo/1tz1jj1mfpG2zv\n\t\twNcQa9LwAzRLnnmLpxXA2OMbl7AaTWQenpL9rzBON2sg4OBl6lVhaSU0uBbFyCmR\n\t\tRvBqKC0iDD6TvyIikkMq05v5YwIKFYw++ICndz+fKcLEULZbziAsZ52qjM8iPVHC\n\t\tpN0yhVOr2g22F9zxlGH3WxTl9ymUytuv3vJL/aJiQM+n/Ri90Sc05EK4oIJ3+BS8\n\t\tyu5cVy9o2cQcOcQ8rhQh+Kv1sR9xrs25EXZF8KEETfhoJnN6KY1RwG7HsOfAQ3dV\n\t\tLWInQRaC/8JPyVS2zbd0+NRBJOnq4/quv/P3C4SBP98/ZuGrqN59uifyqC3Kodkl\n\t\tWkG/2UdhiLlCmOtsU+BYDZrSiYK1R9FNnlQCOGrkuVxpDwa2TbbvEEzQP7RXxotA\n\t\tKlxejvrY4VuK8agNqvffVofbdIIperK65K4+0mYIb+A6fU8QQHlCbti4ERSZ6UYD\n\t\tF/SjRih31+SAtWb42jueAA==\n\t\t-----END CERTIFICATE-----\n\tSome Cert (1):\n\t\tSerial Number: 0\n\t\tSubject: CN=SEV-VCEK,OU=Engineering,O=Advanced Micro Devices,L=Santa Clara,ST=CA,C=US\n\t\tIssuer: CN=SEV-Milan,OU=Engineering,O=Advanced Micro Devices,L=Santa Clara,ST=CA,C=US\n\t\tNot Before: 2022-11-23 22:33:47 +0000 UTC\n\t\tNot After: 2029-11-23 22:33:47 +0000 UTC\n\t\tSignature Algorithm: SHA384-RSAPSS\n\t\tPublic Key Algorithm: ECDSA\n" - - testCases := map[string]struct { - cert []byte - expected string - wantErr bool - }{ - "one cert": { - cert: []byte(validCert), - expected: validCertExpected, - }, - "one cert with extra newlines": { - cert: []byte("\n\n" + validCert + "\n\n"), - expected: validCertExpected, - }, - "invalid cert": { - cert: []byte("invalid"), - wantErr: true, - }, - "no cert": { - wantErr: true, - }, - } - - for name, tc := range testCases { - t.Run(name, func(t *testing.T) { - assert := assert.New(t) - - b := &strings.Builder{} - formatter := &defaultAttestationDocFormatter{ - log: logger.NewTest(t), - } - err := formatter.parseCerts(b, "Some Cert", tc.cert) - if tc.wantErr { - assert.Error(err) - } else { - assert.NoError(err) - assert.Equal(tc.expected, b.String()) - } - }) - } -} - func TestVerifyClient(t *testing.T) { testCases := map[string]struct { attestationDoc atls.FakeAttestationDoc diff --git a/internal/attestation/azure/snp/issuer.go b/internal/attestation/azure/snp/issuer.go index dbba2cefb9..3280a731c3 100644 --- a/internal/attestation/azure/snp/issuer.go +++ b/internal/attestation/azure/snp/issuer.go @@ -67,11 +67,13 @@ func (i *Issuer) getInstanceInfo(ctx context.Context, tpm io.ReadWriteCloser, us } instanceInfo := snp.InstanceInfo{ - VCEK: params.VcekCert, + ReportSigner: params.VcekCert, CertChain: params.VcekChain, AttestationReport: params.SNPReport, - RuntimeData: params.RuntimeData, - MAAToken: maaToken, + Azure: &snp.AzureInstanceInfo{ + RuntimeData: params.RuntimeData, + MAAToken: maaToken, + }, } statement, err := json.Marshal(instanceInfo) if err != nil { diff --git a/internal/attestation/azure/snp/issuer_test.go b/internal/attestation/azure/snp/issuer_test.go index 3ecc9e29cd..81f6d6df1e 100644 --- a/internal/attestation/azure/snp/issuer_test.go +++ b/internal/attestation/azure/snp/issuer_test.go @@ -104,11 +104,11 @@ func TestGetSNPAttestation(t *testing.T) { err = json.Unmarshal(attestationJSON, &instanceInfo) require.NoError(err) - assert.Equal(params.VcekCert, instanceInfo.VCEK) + assert.Equal(params.VcekCert, instanceInfo.ReportSigner) assert.Equal(params.VcekChain, instanceInfo.CertChain) assert.Equal(params.SNPReport, instanceInfo.AttestationReport) - assert.Equal(params.RuntimeData, instanceInfo.RuntimeData) - assert.Equal(tc.maaToken, instanceInfo.MAAToken) + assert.Equal(params.RuntimeData, instanceInfo.Azure.RuntimeData) + assert.Equal(tc.maaToken, instanceInfo.Azure.MAAToken) }) } } diff --git a/internal/attestation/azure/snp/validator.go b/internal/attestation/azure/snp/validator.go index fa31893a42..5c7651d89d 100644 --- a/internal/attestation/azure/snp/validator.go +++ b/internal/attestation/azure/snp/validator.go @@ -179,7 +179,10 @@ func (v *Validator) getTrustedKey(ctx context.Context, attDoc vtpm.AttestationDo } // Custom check of the IDKeyDigests, taking care of the WarnOnly / MAAFallback cases, // but also double-checking the IDKeyDigests if the enforcement policy is set to Equal. - if err := v.checkIDKeyDigest(ctx, att, instanceInfo.MAAToken, extraData); err != nil { + if instanceInfo.Azure == nil { + return nil, errors.New("missing Azure info from instanceInfo") + } + if err := v.checkIDKeyDigest(ctx, att, instanceInfo.Azure.MAAToken, extraData); err != nil { return nil, fmt.Errorf("checking IDKey digests: %w", err) } @@ -188,7 +191,7 @@ func (v *Validator) getTrustedKey(ctx context.Context, attDoc vtpm.AttestationDo if err != nil { return nil, err } - if err = v.hclValidator.validate(instanceInfo.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) } diff --git a/internal/attestation/azure/snp/validator_test.go b/internal/attestation/azure/snp/validator_test.go index e3443bf051..2b2c9a2414 100644 --- a/internal/attestation/azure/snp/validator_test.go +++ b/internal/attestation/azure/snp/validator_test.go @@ -233,7 +233,7 @@ func TestValidateAk(t *testing.T) { defaultRuntimeDataRaw, err := json.Marshal(runtimeData) require.NoError(err) - defaultInstanceInfo := snp.InstanceInfo{RuntimeData: defaultRuntimeDataRaw} + defaultInstanceInfo := snp.InstanceInfo{Azure: &snp.AzureInstanceInfo{RuntimeData: defaultRuntimeDataRaw}} sig := sha256.Sum256(defaultRuntimeDataRaw) defaultReportData := sig[:] @@ -794,9 +794,13 @@ func (v *stubAttestationValidator) SNPAttestation(attestation *spb.Attestation, type stubInstanceInfo struct { AttestationReport []byte - RuntimeData []byte - VCEK []byte + ReportSigner []byte CertChain []byte + Azure *stubAzureInstanceInfo +} + +type stubAzureInstanceInfo struct { + RuntimeData []byte } func newStubInstanceInfo(vcek, certChain []byte, report, runtimeData string) (stubInstanceInfo, error) { @@ -812,9 +816,11 @@ func newStubInstanceInfo(vcek, certChain []byte, report, runtimeData string) (st return stubInstanceInfo{ AttestationReport: validReport, - RuntimeData: decodedRuntime, - VCEK: vcek, + ReportSigner: vcek, CertChain: certChain, + Azure: &stubAzureInstanceInfo{ + RuntimeData: decodedRuntime, + }, }, nil } diff --git a/internal/attestation/snp/snp.go b/internal/attestation/snp/snp.go index 281b9c662f..a25c6f5d0a 100644 --- a/internal/attestation/snp/snp.go +++ b/internal/attestation/snp/snp.go @@ -22,15 +22,21 @@ import ( "github.com/google/go-sev-guest/verify/trust" ) -// InstanceInfo contains the necessary information to establish trust in -// an Azure CVM. +// InstanceInfo contains the necessary information to establish trust in a SNP 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. + // ReportSigner is the PEM-encoded ReportSigner/VLEK certificate for the attestation report. + // Public key that validates the report's signature. + ReportSigner []byte + // CertChain is the PEM-encoded certificate chain for the attestation report (ASK+ARK). + // Intermediate key that validates the ReportSigner and root key. CertChain []byte // AttestationReport is the attestation report from the vTPM (NVRAM) of the CVM. AttestationReport []byte + Azure *AzureInstanceInfo +} + +// AzureInstanceInfo contains Azure specific information related to SNP attestation. +type AzureInstanceInfo struct { // 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 @@ -126,11 +132,13 @@ func (a *InstanceInfo) AttestationWithCerts(logger attestation.Logger, getter tr return att, nil } +// CertificateChain stores an AMD signing key (ASK) and AMD root key (ARK) certificate. type CertificateChain struct { ask *x509.Certificate ark *x509.Certificate } +// NewCertificateChain returns a new CertificateChain with the given ASK and ARK certificates. func NewCertificateChain(ask, ark *x509.Certificate) CertificateChain { return CertificateChain{ ask: ask, @@ -191,7 +199,7 @@ func (a *InstanceInfo) ParseCertChain() (ask, ark *x509.Certificate, retErr erro // 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) + newlinesTrimmed := bytes.TrimSpace(a.ReportSigner) if len(newlinesTrimmed) == 0 { // VCEK is not present. return nil, nil diff --git a/internal/attestation/snp/snp_test.go b/internal/attestation/snp/snp_test.go index bfc0f225be..ac9443869e 100644 --- a/internal/attestation/snp/snp_test.go +++ b/internal/attestation/snp/snp_test.go @@ -110,7 +110,7 @@ func TestParseVCEK(t *testing.T) { assert := assert.New(t) instanceInfo := &InstanceInfo{ - VCEK: tc.VCEK, + ReportSigner: tc.VCEK, } vcek, err := instanceInfo.ParseVCEK() @@ -235,7 +235,7 @@ func TestInstanceInfoAttestation(t *testing.T) { instanceInfo := InstanceInfo{ AttestationReport: tc.report, CertChain: tc.certChain, - VCEK: tc.vcek, + ReportSigner: tc.vcek, } att, err := instanceInfo.AttestationWithCerts(logger.NewTest(t), tc.getter, tc.fallbackCerts) diff --git a/internal/verify/BUILD.bazel b/internal/verify/BUILD.bazel index 1d02507041..c52589085a 100644 --- a/internal/verify/BUILD.bazel +++ b/internal/verify/BUILD.bazel @@ -1,9 +1,31 @@ load("@io_bazel_rules_go//go:def.bzl", "go_library") +load("//bazel/go:go_test.bzl", "go_test") go_library( name = "verify", - srcs = ["verify.go"], + srcs = [ + "certchain.go", + "verify.go", + ], importpath = "github.com/edgelesssys/constellation/v2/internal/verify", visibility = ["//:__subpackages__"], - deps = ["@com_github_golang_jwt_jwt_v5//:jwt"], + deps = [ + "//internal/attestation/snp", + "//internal/constants", + "//internal/kubernetes/kubectl", + "@com_github_golang_jwt_jwt_v5//:jwt", + "@com_github_google_go_sev_guest//abi", + "@com_github_google_go_sev_guest//kds", + ], +) + +go_test( + name = "verify_test", + srcs = ["verify_test.go"], + embed = [":verify"], + deps = [ + "//internal/attestation/snp/testdata", + "//internal/logger", + "@com_github_stretchr_testify//assert", + ], ) diff --git a/internal/verify/certchain.go b/internal/verify/certchain.go new file mode 100644 index 0000000000..c3629032f9 --- /dev/null +++ b/internal/verify/certchain.go @@ -0,0 +1,29 @@ +package verify + +import ( + "context" + "fmt" + + "github.com/edgelesssys/constellation/v2/internal/constants" + "github.com/edgelesssys/constellation/v2/internal/kubernetes/kubectl" +) + +func getCertChainCache(ctx context.Context, kubectl *kubectl.Kubectl, log debugLog) ([]byte, error) { + log.Debugf("Retrieving certificate chain from cache") + cm, err := kubectl.GetConfigMap(ctx, constants.ConstellationNamespace, constants.SevSnpCertCacheConfigMapName) + if err != nil { + return nil, fmt.Errorf("getting certificate chain cache configmap: %w", err) + } + + var result []byte + ask, ok := cm.Data[constants.CertCacheAskKey] + if ok { + result = append(result, ask...) + } + ark, ok := cm.Data[constants.CertCacheArkKey] + if ok { + result = append(result, ark...) + } + + return result, nil +} diff --git a/internal/verify/verify.go b/internal/verify/verify.go index 4b7bb53c95..b1e87aad82 100644 --- a/internal/verify/verify.go +++ b/internal/verify/verify.go @@ -9,29 +9,256 @@ Package verify provides the types for the verify report in JSON format. The package provides an interface for constellation verify and the attestationconfigapi upload tool through JSON serialization. +It exposes a CSP-agnostic interface for printing Reports that may include CSP-specific information. */ package verify import ( + "context" + "crypto/x509" + "encoding/json" + "encoding/pem" + "errors" + "fmt" + "io" + "net/http" + "net/url" + "strings" + + "github.com/edgelesssys/constellation/v2/internal/attestation/snp" + "github.com/edgelesssys/constellation/v2/internal/constants" + "github.com/edgelesssys/constellation/v2/internal/kubernetes/kubectl" "github.com/golang-jwt/jwt/v5" + "github.com/google/go-sev-guest/abi" + "github.com/google/go-sev-guest/kds" ) // Report contains the entire data reported by constellation verify. type Report struct { - SNPReport SNPReport `json:"snp_report"` - VCEK []Certificate `json:"vcek"` - CertChain []Certificate `json:"cert_chain"` - MAAToken MaaTokenClaims `json:"maa_token"` + SNPReport SNPReport `json:"snp_report"` + ReportSigner []Certificate `json:"vcek"` + CertChain []Certificate `json:"cert_chain"` + *AzureReportAddition `json:"azure,omitempty"` + *AWSReportAddition `json:"aws,omitempty"` +} + +// AzureReportAddition contains attestation report data specific to Azure. +type AzureReportAddition struct { + MAAToken MaaTokenClaims `json:"maa_token"` +} + +// AWSReportAddition contains attestation report data specific to AWS. +type AWSReportAddition struct{} + +// NewReport transforms a snp.InstanceInfo object into a Report. +func NewReport(ctx context.Context, instanceInfo snp.InstanceInfo, attestationServiceURL string, log debugLog) (Report, error) { + snpReport, err := newSNPReport(instanceInfo.AttestationReport) + if err != nil { + return Report{}, fmt.Errorf("parsing SNP report: %w", err) + } + + var certTypeName string + switch snpReport.SignerInfo.SigningKey { + case abi.VlekReportSigner.String(): + certTypeName = "VLEK certificate" + case abi.VcekReportSigner.String(): + certTypeName = "VCEK certificate" + default: + return Report{}, errors.New("unknown report signer") + } + + reportSigner, err := newCertificates(certTypeName, instanceInfo.ReportSigner, log) + if err != nil { + return Report{}, fmt.Errorf("parsing VCEK certificate: %w", err) + } + + // check if issuer included certChain before parsing. If not included, manually collect from the cluster. + var pemCerts []byte + if instanceInfo.CertChain == nil { + client, err := kubectl.NewFromConfig(constants.AdminConfFilename) + if err != nil { + return Report{}, fmt.Errorf("creating kubectl client: %w", err) + } + pemCerts, err = getCertChainCache(ctx, client, log) + if err != nil { + return Report{}, fmt.Errorf("getting certificate chain cache: %w", err) + } + } else { + pemCerts = instanceInfo.CertChain + } + + certChain, err := newCertificates("Certificate chain", pemCerts, log) + if err != nil { + return Report{}, fmt.Errorf("parsing certificate chain: %w", err) + } + + var azure *AzureReportAddition + var aws *AWSReportAddition + if instanceInfo.Azure != nil { + maaToken, err := newMAAToken(ctx, instanceInfo.Azure.MAAToken, attestationServiceURL) + if err != nil { + return Report{}, fmt.Errorf("parsing MAA token: %w", err) + } + azure = &AzureReportAddition{ + MAAToken: maaToken, + } + } + + return Report{ + SNPReport: snpReport, + ReportSigner: reportSigner, + CertChain: certChain, + AzureReportAddition: azure, + AWSReportAddition: aws, + }, nil +} + +// FormatString builds a string representation of a report that is inteded for console output. +func (r *Report) FormatString(b *strings.Builder) (string, error) { + if len(r.ReportSigner) != 1 { + return "", fmt.Errorf("expected exactly one report signing certificate, found %d", len(r.ReportSigner)) + } + + if err := formatCertificates(b, r.ReportSigner); err != nil { + return "", fmt.Errorf("building report signing certificate string: %w", err) + } + + if err := formatCertificates(b, r.CertChain); err != nil { + return "", fmt.Errorf("building certificate chain string: %w", err) + } + + r.SNPReport.formatString(b) + if r.AzureReportAddition != nil { + if err := r.AzureReportAddition.MAAToken.formatString(b); err != nil { + return "", fmt.Errorf("error building MAAToken string : %w", err) + } + } + + return b.String(), nil +} + +func formatCertificates(b *strings.Builder, certs []Certificate) error { + for i, cert := range certs { + if i == 0 { + b.WriteString(fmt.Sprintf("\tRaw %s:\n", cert.CertTypeName)) + } + newlinesTrimmed := strings.TrimSpace(cert.CertificatePEM) + formattedCert := strings.ReplaceAll(newlinesTrimmed, "\n", "\n\t\t") + "\n" + b.WriteString(fmt.Sprintf("\t\t%s", formattedCert)) + } + for i, cert := range certs { + // Use 1-based indexing for user output. + if err := cert.formatString(b, i+1); err != nil { + return fmt.Errorf("error printing certificate chain: %w", err) + } + } + + return nil } // Certificate contains the certificate data and additional information. type Certificate struct { - CertificatePEM string `json:"certificate"` - CertTypeName string `json:"cert_type_name"` - StructVersion uint8 `json:"struct_version"` - ProductName string `json:"product_name"` - HardwareID []byte `json:"hardware_id"` - TCBVersion TCBVersion `json:"tcb_version"` + x509.Certificate `json:"-"` + CertificatePEM string `json:"certificate"` + CertTypeName string `json:"cert_type_name"` + StructVersion uint8 `json:"struct_version"` + ProductName string `json:"product_name"` + HardwareID []byte `json:"hardware_id"` + TCBVersion TCBVersion `json:"tcb_version"` +} + +// newCertificates parses a list of PEM encoded certificate and returns a slice of Certificate objects. +func newCertificates(certTypeName string, cert []byte, log debugLog) (certs []Certificate, err error) { + newlinesTrimmed := strings.TrimSpace(string(cert)) + + log.Debugf("Decoding PEM certificate: %s", certTypeName) + i := 1 + var rest []byte + var block *pem.Block + for block, rest = pem.Decode([]byte(newlinesTrimmed)); block != nil; block, rest = pem.Decode(rest) { + log.Debugf("Parsing PEM block: %d", i) + if block.Type != "CERTIFICATE" { + return certs, fmt.Errorf("parse %s: expected PEM block type 'CERTIFICATE', got '%s'", certTypeName, block.Type) + } + + cert, err := x509.ParseCertificate(block.Bytes) + if err != nil { + return certs, fmt.Errorf("parse %s: %w", certTypeName, err) + } + if certTypeName == "VCEK certificate" { + vcekExts, err := kds.VcekCertificateExtensions(cert) + if err != nil { + return certs, fmt.Errorf("parsing VCEK certificate extensions: %w", err) + } + certPEM := pem.EncodeToMemory(&pem.Block{ + Type: "CERTIFICATE", + Bytes: cert.Raw, + }) + certs = append(certs, Certificate{ + Certificate: *cert, + CertificatePEM: string(certPEM), + CertTypeName: certTypeName, + StructVersion: vcekExts.StructVersion, + ProductName: vcekExts.ProductName, + TCBVersion: newTCBVersion(vcekExts.TCBVersion), + HardwareID: vcekExts.HWID, + }) + } else { + certPEM := pem.EncodeToMemory(&pem.Block{ + Type: "CERTIFICATE", + Bytes: cert.Raw, + }) + certs = append(certs, Certificate{ + Certificate: *cert, + CertificatePEM: string(certPEM), + CertTypeName: certTypeName, + }) + } + i++ + } + if i == 1 { + return certs, fmt.Errorf("parse %s: no PEM blocks found", certTypeName) + } + if len(rest) != 0 { + return certs, fmt.Errorf("parse %s: remaining PEM block is not a valid certificate: %s", certTypeName, rest) + } + return certs, nil +} + +// formatString builds a string representation of a certificate that is inteded for console output. +func (c *Certificate) formatString(b *strings.Builder, idx int) error { + writeIndentfln(b, 1, "%s (%d):", c.CertTypeName, idx) + writeIndentfln(b, 2, "Serial Number: %s", c.Certificate.SerialNumber) + writeIndentfln(b, 2, "Subject: %s", c.Certificate.Subject) + writeIndentfln(b, 2, "Issuer: %s", c.Certificate.Issuer) + writeIndentfln(b, 2, "Not Before: %s", c.Certificate.NotBefore) + writeIndentfln(b, 2, "Not After: %s", c.Certificate.NotAfter) + writeIndentfln(b, 2, "Signature Algorithm: %s", c.Certificate.SignatureAlgorithm) + writeIndentfln(b, 2, "Public Key Algorithm: %s", c.Certificate.PublicKeyAlgorithm) + + if c.CertTypeName == "VCEK certificate" { + // Extensions documented in Table 8 and Table 9 of + // https://www.amd.com/system/files/TechDocs/57230.pdf + vcekExts, err := kds.VcekCertificateExtensions(&c.Certificate) + if err != nil { + return fmt.Errorf("parsing VCEK certificate extensions: %w", err) + } + + writeIndentfln(b, 2, "Struct version: %d", vcekExts.StructVersion) + writeIndentfln(b, 2, "Product name: %s", vcekExts.ProductName) + tcb := kds.DecomposeTCBVersion(vcekExts.TCBVersion) + writeIndentfln(b, 2, "Secure Processor bootloader SVN: %d", tcb.BlSpl) + writeIndentfln(b, 2, "Secure Processor operating system SVN: %d", tcb.TeeSpl) + writeIndentfln(b, 2, "SVN 4 (reserved): %d", tcb.Spl4) + writeIndentfln(b, 2, "SVN 5 (reserved): %d", tcb.Spl5) + writeIndentfln(b, 2, "SVN 6 (reserved): %d", tcb.Spl6) + writeIndentfln(b, 2, "SVN 7 (reserved): %d", tcb.Spl7) + writeIndentfln(b, 2, "SEV-SNP firmware SVN: %d", tcb.SnpSpl) + writeIndentfln(b, 2, "Microcode SVN: %d", tcb.UcodeSpl) + writeIndentfln(b, 2, "Hardware ID: %x", vcekExts.HWID) + } + + return nil } // TCBVersion contains the TCB version data. @@ -46,6 +273,33 @@ type TCBVersion struct { Spl7 uint8 `json:"spl7"` } +// formatString builds a string representation of a TCB version that is inteded for console output. +func (t *TCBVersion) formatString(b *strings.Builder) { + writeIndentfln(b, 3, "Secure Processor bootloader SVN: %d", t.Bootloader) + writeIndentfln(b, 3, "Secure Processor operating system SVN: %d", t.TEE) + writeIndentfln(b, 3, "SVN 4 (reserved): %d", t.Spl4) + writeIndentfln(b, 3, "SVN 5 (reserved): %d", t.Spl5) + writeIndentfln(b, 3, "SVN 6 (reserved): %d", t.Spl6) + writeIndentfln(b, 3, "SVN 7 (reserved): %d", t.Spl7) + writeIndentfln(b, 3, "SEV-SNP firmware SVN: %d", t.SNP) + writeIndentfln(b, 3, "Microcode SVN: %d", t.Microcode) +} + +// newTCBVersion creates a TCB version from a kds.TCBVersion. +func newTCBVersion(tcbVersion kds.TCBVersion) TCBVersion { + tcb := kds.DecomposeTCBVersion(tcbVersion) + return TCBVersion{ + Bootloader: tcb.BlSpl, + TEE: tcb.TeeSpl, + SNP: tcb.SnpSpl, + Microcode: tcb.UcodeSpl, + Spl4: tcb.Spl4, + Spl5: tcb.Spl5, + Spl6: tcb.Spl6, + Spl7: tcb.Spl7, + } +} + // PlatformInfo contains the platform information. type PlatformInfo struct { SMT bool `json:"smt"` @@ -96,6 +350,125 @@ type SNPReport struct { Signature []byte `json:"signature"` } +// newSNPReport parses a marshalled SNP report and returns a SNPReport object. +func newSNPReport(reportBytes []byte) (SNPReport, error) { + report, err := abi.ReportToProto(reportBytes) + if err != nil { + return SNPReport{}, fmt.Errorf("parsing report to proto: %w", err) + } + + policy, err := abi.ParseSnpPolicy(report.Policy) + if err != nil { + return SNPReport{}, fmt.Errorf("parsing policy: %w", err) + } + + platformInfo, err := abi.ParseSnpPlatformInfo(report.PlatformInfo) + if err != nil { + return SNPReport{}, fmt.Errorf("parsing platform info: %w", err) + } + + signature, err := abi.ReportToSignatureDER(reportBytes) + if err != nil { + return SNPReport{}, fmt.Errorf("parsing signature: %w", err) + } + + signerInfo, err := abi.ParseSignerInfo(report.SignerInfo) + if err != nil { + return SNPReport{}, fmt.Errorf("parsing signer info: %w", err) + } + return SNPReport{ + Version: report.Version, + GuestSvn: report.GuestSvn, + PolicyABIMinor: policy.ABIMinor, + PolicyABIMajor: policy.ABIMajor, + PolicySMT: policy.SMT, + PolicyMigrationAgent: policy.MigrateMA, + PolicyDebug: policy.Debug, + PolicySingleSocket: policy.SingleSocket, + FamilyID: report.FamilyId, + ImageID: report.ImageId, + Vmpl: report.Vmpl, + SignatureAlgo: report.SignatureAlgo, + CurrentTCB: newTCBVersion(kds.TCBVersion(report.CurrentTcb)), + PlatformInfo: PlatformInfo{ + SMT: platformInfo.SMTEnabled, + TSME: platformInfo.TSMEEnabled, + }, + SignerInfo: SignerInfo{ + AuthorKey: signerInfo.AuthorKeyEn, + MaskChipKey: signerInfo.MaskChipKey, + SigningKey: signerInfo.SigningKey.String(), + }, + ReportData: report.ReportData, + Measurement: report.Measurement, + HostData: report.HostData, + IDKeyDigest: report.IdKeyDigest, + AuthorKeyDigest: report.AuthorKeyDigest, + ReportID: report.ReportId, + ReportIDMa: report.ReportIdMa, + ReportedTCB: newTCBVersion(kds.TCBVersion(report.ReportedTcb)), + ChipID: report.ChipId, + CommittedTCB: newTCBVersion(kds.TCBVersion(report.CommittedTcb)), + CurrentBuild: report.CurrentBuild, + CurrentMinor: report.CurrentMinor, + CurrentMajor: report.CurrentMajor, + CommittedBuild: report.CommittedBuild, + CommittedMinor: report.CommittedMinor, + CommittedMajor: report.CommittedMajor, + LaunchTCB: newTCBVersion(kds.TCBVersion(report.LaunchTcb)), + Signature: signature, + }, nil +} + +// formatString builds a string representation of a SNP report that is inteded for console output. +func (s *SNPReport) formatString(b *strings.Builder) { + writeIndentfln(b, 1, "SNP Report:") + writeIndentfln(b, 2, "Version: %d", s.Version) + writeIndentfln(b, 2, "Guest SVN: %d", s.GuestSvn) + writeIndentfln(b, 2, "Policy:") + writeIndentfln(b, 3, "ABI Minor: %d", s.PolicyABIMinor) + writeIndentfln(b, 3, "ABI Major: %d", s.PolicyABIMajor) + writeIndentfln(b, 3, "Symmetric Multithreading enabled: %t", s.PolicySMT) + writeIndentfln(b, 3, "Migration agent enabled: %t", s.PolicyMigrationAgent) + writeIndentfln(b, 3, "Debugging enabled (host decryption of VM): %t", s.PolicyDebug) + writeIndentfln(b, 3, "Single socket enabled: %t", s.PolicySingleSocket) + writeIndentfln(b, 2, "Family ID: %x", s.FamilyID) + writeIndentfln(b, 2, "Image ID: %x", s.ImageID) + writeIndentfln(b, 2, "VMPL: %d", s.Vmpl) + writeIndentfln(b, 2, "Signature Algorithm: %d", s.SignatureAlgo) + writeIndentfln(b, 2, "Current TCB:") + s.CurrentTCB.formatString(b) + writeIndentfln(b, 2, "Platform Info:") + writeIndentfln(b, 3, "Symmetric Multithreading enabled (SMT): %t", s.PlatformInfo.SMT) + writeIndentfln(b, 3, "Transparent secure memory encryption (TSME): %t", s.PlatformInfo.TSME) + writeIndentfln(b, 2, "Signer Info:") + writeIndentfln(b, 3, "Author Key Enabled: %t", s.SignerInfo.AuthorKey) + writeIndentfln(b, 3, "Chip ID Masking: %t", s.SignerInfo.MaskChipKey) + writeIndentfln(b, 3, "Signing Type: %s", s.SignerInfo.SigningKey) + writeIndentfln(b, 2, "Report Data: %x", s.ReportData) + writeIndentfln(b, 2, "Measurement: %x", s.Measurement) + writeIndentfln(b, 2, "Host Data: %x", s.HostData) + writeIndentfln(b, 2, "ID Key Digest: %x", s.IDKeyDigest) + writeIndentfln(b, 2, "Author Key Digest: %x", s.AuthorKeyDigest) + writeIndentfln(b, 2, "Report ID: %x", s.ReportID) + writeIndentfln(b, 2, "Report ID MA: %x", s.ReportIDMa) + writeIndentfln(b, 2, "Reported TCB:") + s.ReportedTCB.formatString(b) + writeIndentfln(b, 2, "Chip ID: %x", s.ChipID) + writeIndentfln(b, 2, "Committed TCB:") + s.CommittedTCB.formatString(b) + writeIndentfln(b, 2, "Current Build: %d", s.CurrentBuild) + writeIndentfln(b, 2, "Current Minor: %d", s.CurrentMinor) + writeIndentfln(b, 2, "Current Major: %d", s.CurrentMajor) + writeIndentfln(b, 2, "Committed Build: %d", s.CommittedBuild) + writeIndentfln(b, 2, "Committed Minor: %d", s.CommittedMinor) + writeIndentfln(b, 2, "Committed Major: %d", s.CommittedMajor) + writeIndentfln(b, 2, "Launch TCB:") + s.LaunchTCB.formatString(b) + writeIndentfln(b, 2, "Signature (DER):") + writeIndentfln(b, 3, "%x", s.Signature) +} + // MaaTokenClaims contains the MAA token claims. type MaaTokenClaims struct { jwt.RegisteredClaims @@ -174,3 +547,113 @@ type MaaTokenClaims struct { } `json:"x-ms-runtime,omitempty"` XMsVer string `json:"x-ms-ver,omitempty"` } + +// newMAAToken parses a MAA token and returns a MaaTokenClaims object. +func newMAAToken(ctx context.Context, rawToken, attestationServiceURL string) (MaaTokenClaims, error) { + var claims MaaTokenClaims + _, err := jwt.ParseWithClaims(rawToken, &claims, keyFromJKUFunc(ctx, attestationServiceURL), jwt.WithIssuedAt()) + return claims, err +} + +// formatString builds a string representation of a MAA token that is inteded for console output. +func (m *MaaTokenClaims) formatString(b *strings.Builder) error { + out, err := json.MarshalIndent(m, "\t\t", " ") + if err != nil { + return fmt.Errorf("marshaling claims: %w", err) + } + + b.WriteString("\tMicrosoft Azure Attestation Token:\n\t") + b.WriteString(string(out)) + + return nil +} + +// writeIndentfln writes a formatted string to the builder with the given indentation level +// and a newline at the end. +func writeIndentfln(b *strings.Builder, indentLvl int, format string, args ...any) { + for i := 0; i < indentLvl; i++ { + b.WriteByte('\t') + } + b.WriteString(fmt.Sprintf(format+"\n", args...)) +} + +// keyFromJKUFunc returns a function that gets the JSON Web Key URI from the token +// and fetches the key from that URI. The keys are then parsed, and the key with +// the kid that matches the token header is returned. +func keyFromJKUFunc(ctx context.Context, webKeysURLBase string) func(token *jwt.Token) (any, error) { + return func(token *jwt.Token) (any, error) { + webKeysURL, err := url.JoinPath(webKeysURLBase, "certs") + if err != nil { + return nil, fmt.Errorf("joining web keys base URL with path: %w", err) + } + + if token.Header["alg"] != "RS256" { + return nil, fmt.Errorf("invalid signing algorithm: %s", token.Header["alg"]) + } + kid, ok := token.Header["kid"].(string) + if !ok { + return nil, fmt.Errorf("invalid kid: %v", token.Header["kid"]) + } + jku, ok := token.Header["jku"].(string) + if !ok { + return nil, fmt.Errorf("invalid jku: %v", token.Header["jku"]) + } + if jku != webKeysURL { + return nil, fmt.Errorf("jku from token (%s) does not match configured attestation service (%s)", jku, webKeysURL) + } + + keySetBytes, err := httpGet(ctx, jku) + if err != nil { + return nil, fmt.Errorf("getting signing keys from jku %s: %w", jku, err) + } + + var rawKeySet struct { + Keys []struct { + X5c [][]byte + Kid string + } + } + + if err := json.Unmarshal(keySetBytes, &rawKeySet); err != nil { + return nil, err + } + + for _, key := range rawKeySet.Keys { + if key.Kid != kid { + continue + } + cert, err := x509.ParseCertificate(key.X5c[0]) + if err != nil { + return nil, fmt.Errorf("parsing certificate: %w", err) + } + + return cert.PublicKey, nil + } + + return nil, fmt.Errorf("no key found for kid %s", kid) + } +} + +func httpGet(ctx context.Context, url string) ([]byte, error) { + req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, http.NoBody) + if err != nil { + return nil, err + } + resp, err := http.DefaultClient.Do(req) + if err != nil { + return nil, err + } + defer resp.Body.Close() + if resp.StatusCode != http.StatusOK { + return nil, errors.New(resp.Status) + } + body, err := io.ReadAll(resp.Body) + if err != nil { + return nil, err + } + return body, nil +} + +type debugLog interface { + Debugf(format string, args ...any) +} diff --git a/internal/verify/verify_test.go b/internal/verify/verify_test.go new file mode 100644 index 0000000000..b0fdf3c5bb --- /dev/null +++ b/internal/verify/verify_test.go @@ -0,0 +1,64 @@ +/* +Copyright (c) Edgeless Systems GmbH + +SPDX-License-Identifier: AGPL-3.0-only +*/ +package verify + +import ( + "strings" + "testing" + + "github.com/edgelesssys/constellation/v2/internal/attestation/snp/testdata" + "github.com/edgelesssys/constellation/v2/internal/logger" + "github.com/stretchr/testify/assert" +) + +func TestParseCerts(t *testing.T) { + validCertExpected := "\tRaw Some Cert:\n\t\t-----BEGIN CERTIFICATE-----\n\t\tMIIFTDCCAvugAwIBAgIBADBGBgkqhkiG9w0BAQowOaAPMA0GCWCGSAFlAwQCAgUA\n\t\toRwwGgYJKoZIhvcNAQEIMA0GCWCGSAFlAwQCAgUAogMCATCjAwIBATB7MRQwEgYD\n\t\tVQQLDAtFbmdpbmVlcmluZzELMAkGA1UEBhMCVVMxFDASBgNVBAcMC1NhbnRhIENs\n\t\tYXJhMQswCQYDVQQIDAJDQTEfMB0GA1UECgwWQWR2YW5jZWQgTWljcm8gRGV2aWNl\n\t\tczESMBAGA1UEAwwJU0VWLU1pbGFuMB4XDTIzMDgzMDEyMTUyNFoXDTMwMDgzMDEy\n\t\tMTUyNFowejEUMBIGA1UECwwLRW5naW5lZXJpbmcxCzAJBgNVBAYTAlVTMRQwEgYD\n\t\tVQQHDAtTYW50YSBDbGFyYTELMAkGA1UECAwCQ0ExHzAdBgNVBAoMFkFkdmFuY2Vk\n\t\tIE1pY3JvIERldmljZXMxETAPBgNVBAMMCFNFVi1WQ0VLMHYwEAYHKoZIzj0CAQYF\n\t\tK4EEACIDYgAEhPX8Cl9uA7PxqNGzeqamJNYJLx/VFE/s3+8qOWtaztKNcn1PaAI4\n\t\tndE+yaVfMHsiA8CLTylumpWXcVBHPYV9kPEVrtozhvrrT5Oii9OpZPYHJ7/WPVmM\n\t\tJ3K8/Iz3AshTo4IBFjCCARIwEAYJKwYBBAGceAEBBAMCAQAwFwYJKwYBBAGceAEC\n\t\tBAoWCE1pbGFuLUIwMBEGCisGAQQBnHgBAwEEAwIBAjARBgorBgEEAZx4AQMCBAMC\n\t\tAQAwEQYKKwYBBAGceAEDBAQDAgEAMBEGCisGAQQBnHgBAwUEAwIBADARBgorBgEE\n\t\tAZx4AQMGBAMCAQAwEQYKKwYBBAGceAEDBwQDAgEAMBEGCisGAQQBnHgBAwMEAwIB\n\t\tBjARBgorBgEEAZx4AQMIBAMCAV0wTQYJKwYBBAGceAEEBECeRKrvAs/Kb926ymac\n\t\tbP0p4auNl+vJOYVxKKy7E7h0DfMUNtNOhuX4rgzf6zoOGF20beysF2zHfXYcIqG5\n\t\t3PJbMEYGCSqGSIb3DQEBCjA5oA8wDQYJYIZIAWUDBAICBQChHDAaBgkqhkiG9w0B\n\t\tAQgwDQYJYIZIAWUDBAICBQCiAwIBMKMDAgEBA4ICAQBoVGgDdFV9gWPHaEOBrHzd\n\t\tWVYyuuMBH340DDSXbCGlPR6rhgja0qALmkUPG50REQGvoPsikAskwqhzRG2XEDO2\n\t\tb6+fRPIq3DjEbz/8V89IiYiOZI/ycFACi3EEVECAWbzjXSfiOio1NfbniXP6tWzW\n\t\tD/8xpd/8N8166bHpgNgMl9pX4i0I9vaTl3qH+jBuSMZ5Q4heTHLB+v4V7q+H6SZo\n\t\t7htqpaI3keLEhQL/pCP72udMPAzU+/5W/x/t/LD6SbQcQQoHbWDU6kgTDuXabDxl\n\t\tA4JoEZfatr+/TO6jKQcGtqOLKT8JFGcigUlBi/TBVP+Xs8E4CWYGZZiTpYoLwNAu\n\t\tyuKOP9VVFViSCqPvzpNs2G+e0zXg2w3te7oMw/l0bD8iQCAS8rR0+r+8pZL4e010\n\t\tKLZ3yEfA0moXef66k5xyf4y37ZIP189wz6qJ+YXqOujDmeTomCU0SnZXlri6GhbF\n\t\t19rp2z5/lsZG+W27CRxvzTB3hk+ukZr35vCqNq4Rs+c7/hYcYzzyZ4ysATwdglNF\n\t\tWddfVw5Qunlu6Ngxr84ifz3HrnUx9bR5DzmFbztrb7IbkZhq7GjImwJULub1viyg\n\t\tYFa7X3p8b1WllienSEfvbadobbS9HeuLUrWyh0kZjQnz+0Q1UB1/zlzokeQmAYCf\n\t\t8H3kABPv6hqrFftRNbargQ==\n\t\t-----END CERTIFICATE-----\n\tSome Cert (1):\n\t\tSerial Number: 0\n\t\tSubject: CN=SEV-VCEK,OU=Engineering,O=Advanced Micro Devices,L=Santa Clara,ST=CA,C=US\n\t\tIssuer: CN=SEV-Milan,OU=Engineering,O=Advanced Micro Devices,L=Santa Clara,ST=CA,C=US\n\t\tNot Before: 2023-08-30 12:15:24 +0000 UTC\n\t\tNot After: 2030-08-30 12:15:24 +0000 UTC\n\t\tSignature Algorithm: SHA384-RSAPSS\n\t\tPublic Key Algorithm: ECDSA\n" + + testCases := map[string]struct { + cert []byte + expected string + wantErr bool + }{ + "one cert": { + cert: testdata.AzureThimVCEK, + expected: validCertExpected, + }, + "one cert with extra newlines": { + cert: []byte("\n\n" + string(testdata.AzureThimVCEK) + "\n\n"), + expected: validCertExpected, + }, + "invalid cert": { + cert: []byte("invalid"), + wantErr: true, + }, + "no cert": { + wantErr: true, + }, + } + + for name, tc := range testCases { + t.Run(name, func(t *testing.T) { + assert := assert.New(t) + + b := &strings.Builder{} + + certs, err := newCertificates("Some Cert", tc.cert, logger.NewTest(t)) + if err != nil { + assert.True(tc.wantErr) + return + } + + err = formatCertificates(b, certs) + + if tc.wantErr { + assert.Error(err) + } else { + assert.NoError(err) + assert.Equal(tc.expected, b.String()) + } + }) + } +} From 79b5cc3f71a5ddd448e2a486fcd1bc9e3c248f6e Mon Sep 17 00:00:00 2001 From: Otto Bittner Date: Mon, 6 Nov 2023 14:22:44 +0100 Subject: [PATCH 04/12] attestation: use SNP-based attestation for AWS SNP --- internal/attestation/aws/snp/BUILD.bazel | 24 +- internal/attestation/aws/snp/errors.go | 48 +++ internal/attestation/aws/snp/issuer.go | 83 +++++- internal/attestation/aws/snp/issuer_test.go | 87 +----- .../attestation/aws/snp/testdata/BUILD.bazel | 13 + .../aws/snp/testdata/certchain.pem | 75 +++++ .../attestation/aws/snp/testdata/report.txt | 1 + .../attestation/aws/snp/testdata/testdata.go | 28 ++ .../attestation/aws/snp/testdata/vlek.pem | 30 ++ internal/attestation/aws/snp/validator.go | 199 ++++++++++--- .../attestation/aws/snp/validator_test.go | 281 ++++++++++++++---- internal/attestation/azure/snp/validator.go | 2 +- internal/attestation/snp/BUILD.bazel | 1 + internal/attestation/snp/snp.go | 82 +++-- internal/attestation/snp/snp_test.go | 70 +++-- internal/attestation/snp/testdata/BUILD.bazel | 2 + internal/attestation/snp/testdata/testdata.go | 15 +- internal/attestation/snp/testdata/vlek.pem | 30 ++ .../snp/testdata/vlekcertchain.pem | 75 +++++ internal/config/BUILD.bazel | 1 + internal/config/attestation.go | 3 + internal/config/aws.go | 75 +++++ internal/config/azure.go | 3 +- internal/config/config.go | 108 ++----- internal/config/config_doc.go | 118 +++++--- joinservice/internal/certcache/certcache.go | 24 +- joinservice/internal/watcher/validator.go | 8 +- 27 files changed, 1095 insertions(+), 391 deletions(-) create mode 100644 internal/attestation/aws/snp/errors.go create mode 100644 internal/attestation/aws/snp/testdata/BUILD.bazel create mode 100644 internal/attestation/aws/snp/testdata/certchain.pem create mode 100644 internal/attestation/aws/snp/testdata/report.txt create mode 100644 internal/attestation/aws/snp/testdata/testdata.go create mode 100644 internal/attestation/aws/snp/testdata/vlek.pem create mode 100644 internal/attestation/snp/testdata/vlek.pem create mode 100644 internal/attestation/snp/testdata/vlekcertchain.pem create mode 100644 internal/config/aws.go diff --git a/internal/attestation/aws/snp/BUILD.bazel b/internal/attestation/aws/snp/BUILD.bazel index 660618e4ef..f089643078 100644 --- a/internal/attestation/aws/snp/BUILD.bazel +++ b/internal/attestation/aws/snp/BUILD.bazel @@ -4,6 +4,7 @@ load("//bazel/go:go_test.bzl", "go_test") go_library( name = "snp", srcs = [ + "errors.go", "issuer.go", "snp.go", "validator.go", @@ -12,12 +13,17 @@ go_library( visibility = ["//:__subpackages__"], deps = [ "//internal/attestation", + "//internal/attestation/snp", "//internal/attestation/variant", "//internal/attestation/vtpm", "//internal/config", - "@com_github_aws_aws_sdk_go_v2_config//:config", - "@com_github_aws_aws_sdk_go_v2_feature_ec2_imds//:imds", - "@com_github_aws_aws_sdk_go_v2_service_ec2//:ec2", + "@com_github_google_go_sev_guest//abi", + "@com_github_google_go_sev_guest//client", + "@com_github_google_go_sev_guest//kds", + "@com_github_google_go_sev_guest//proto/sevsnp", + "@com_github_google_go_sev_guest//validate", + "@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", @@ -37,12 +43,16 @@ go_test( "//conditions:default": ["disable_tpm_simulator"], }), deps = [ + "//internal/attestation", + "//internal/attestation/aws/snp/testdata", "//internal/attestation/simulator", + "//internal/attestation/snp", "//internal/attestation/vtpm", - "@com_github_aws_aws_sdk_go_v2_feature_ec2_imds//:imds", - "@com_github_aws_aws_sdk_go_v2_service_ec2//:ec2", - "@com_github_aws_aws_sdk_go_v2_service_ec2//types", - "@com_github_aws_smithy_go//middleware", + "//internal/config", + "//internal/logger", + "@com_github_google_go_sev_guest//abi", + "@com_github_google_go_sev_guest//proto/sevsnp", + "@com_github_google_go_sev_guest//verify", "@com_github_google_go_tpm_tools//client", "@com_github_google_go_tpm_tools//proto/attest", "@com_github_stretchr_testify//assert", diff --git a/internal/attestation/aws/snp/errors.go b/internal/attestation/aws/snp/errors.go new file mode 100644 index 0000000000..2b07870b7d --- /dev/null +++ b/internal/attestation/aws/snp/errors.go @@ -0,0 +1,48 @@ +/* +Copyright (c) Edgeless Systems GmbH + +SPDX-License-Identifier: AGPL-3.0-only +*/ + +package snp + +import "fmt" + +// decodeError is used to signal an error during decoding of a public key. +// It only wrapps an error. +type decodeError struct { + inner error +} + +// newDecodeError an error in a DecodeError. +func newDecodeError(err error) *decodeError { + return &decodeError{inner: err} +} + +func (e *decodeError) Error() string { + return fmt.Sprintf("decoding public key: %v", e.inner) +} + +func (e *decodeError) Unwrap() error { + return e.inner +} + +// validationError is used to signal an invalid SNP report. +// It only wrapps an error. +// Used during testing to error conditions more precisely. +type validationError struct { + inner error +} + +// newValidationError wraps an error in a ValidationError. +func newValidationError(err error) *validationError { + return &validationError{inner: err} +} + +func (e *validationError) Error() string { + return e.inner.Error() +} + +func (e *validationError) Unwrap() error { + return e.inner +} diff --git a/internal/attestation/aws/snp/issuer.go b/internal/attestation/aws/snp/issuer.go index 067b309b54..e3d58ab79d 100644 --- a/internal/attestation/aws/snp/issuer.go +++ b/internal/attestation/aws/snp/issuer.go @@ -8,15 +8,20 @@ package snp import ( "context" + "crypto/sha512" + "crypto/x509" "encoding/json" + "encoding/pem" "fmt" "io" - "github.com/aws/aws-sdk-go-v2/feature/ec2/imds" "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/google/go-sev-guest/abi" + sevclient "github.com/google/go-sev-guest/client" "github.com/google/go-tpm-tools/client" tpmclient "github.com/google/go-tpm-tools/client" ) @@ -33,7 +38,7 @@ func NewIssuer(log attestation.Logger) *Issuer { Issuer: vtpm.NewIssuer( vtpm.OpenVTPM, getAttestationKey, - getInstanceInfo(imds.New(imds.Options{})), + getInstanceInfo, log, ), } @@ -43,24 +48,76 @@ func NewIssuer(log attestation.Logger) *Issuer { func getAttestationKey(tpm io.ReadWriter) (*tpmclient.Key, error) { tpmAk, err := client.AttestationKeyRSA(tpm) if err != nil { - return nil, fmt.Errorf("error creating RSA Endorsement key: %w", err) + return nil, fmt.Errorf("creating RSA Endorsement key: %w", err) } return tpmAk, nil } -// getInstanceInfo returns information about the current instance using the aws Metadata SDK. +// getInstanceInfo generates an extended SNP report, i.e. the report and any loaded certificates. +// Report generation is triggered by sending ioctl syscalls to the SNP guest device, the AMD PSP generates the report. // The returned bytes will be written into the attestation document. -func getInstanceInfo(client awsMetaData) func(context.Context, io.ReadWriteCloser, []byte) ([]byte, error) { - return func(ctx context.Context, _ io.ReadWriteCloser, _ []byte) ([]byte, error) { - ec2InstanceIdentityOutput, err := client.GetInstanceIdentityDocument(ctx, &imds.GetInstanceIdentityDocumentInput{}) - if err != nil { - return nil, fmt.Errorf("fetching instance identity document: %w", err) - } - return json.Marshal(ec2InstanceIdentityOutput.InstanceIdentityDocument) +func getInstanceInfo(_ context.Context, tpm io.ReadWriteCloser, _ []byte) ([]byte, error) { + tpmAk, err := client.AttestationKeyRSA(tpm) + if err != nil { + return nil, fmt.Errorf("creating RSA Endorsement key: %w", err) + } + + encoded, err := x509.MarshalPKIXPublicKey(tpmAk.PublicKey()) + if err != nil { + return nil, fmt.Errorf("marshalling public key: %w", err) + } + + akDigest := sha512.Sum512(encoded) + + device, err := sevclient.OpenDevice() + if err != nil { + return nil, fmt.Errorf("opening sev device: %w", err) + } + defer device.Close() + + report, certs, err := sevclient.GetRawExtendedReportAtVmpl(device, akDigest, 0) + if err != nil { + return nil, fmt.Errorf("getting extended report: %w", err) + } + + vlek, err := pemEncodedVLEK(certs) + if err != nil { + return nil, fmt.Errorf("parsing vlek: %w", err) + } + + raw, err := json.Marshal(snp.InstanceInfo{AttestationReport: report, ReportSigner: vlek}) + if err != nil { + return nil, fmt.Errorf("marshalling instance info: %w", err) } + + return raw, nil } -type awsMetaData interface { - GetInstanceIdentityDocument(context.Context, *imds.GetInstanceIdentityDocumentInput, ...func(*imds.Options)) (*imds.GetInstanceIdentityDocumentOutput, error) +// pemEncodedVLEK takes a marshalled SNP certificate table and returns the PEM-encoded VLEK certificate. +// AMD documentation on certificate tables can be found in section 4.1.8.1, revision 2.03 "SEV-ES Guest-Hypervisor Communication Block Standardization". +// https://www.amd.com/content/dam/amd/en/documents/epyc-technical-docs/specifications/56421.pdf +func pemEncodedVLEK(certs []byte) ([]byte, error) { + certTable := abi.CertTable{} + if err := certTable.Unmarshal(certs); err != nil { + return nil, fmt.Errorf("unmarshalling SNP certificate table: %w", err) + } + + vlekRaw, err := certTable.GetByGUIDString(abi.VlekGUID) + if err != nil { + return nil, fmt.Errorf("getting VLEK certificate: %w", err) + } + + // An optional check for certificate well-formedness. vlekRaw == cert.Raw. + cert, err := x509.ParseCertificate(vlekRaw) + if err != nil { + return nil, fmt.Errorf("parsing certificate: %w", err) + } + + certPEM := pem.EncodeToMemory(&pem.Block{ + Type: "CERTIFICATE", + Bytes: cert.Raw, + }) + + return certPEM, nil } diff --git a/internal/attestation/aws/snp/issuer_test.go b/internal/attestation/aws/snp/issuer_test.go index 548b26f286..3f2f246990 100644 --- a/internal/attestation/aws/snp/issuer_test.go +++ b/internal/attestation/aws/snp/issuer_test.go @@ -7,13 +7,9 @@ SPDX-License-Identifier: AGPL-3.0-only package snp import ( - "context" - "errors" "os" "testing" - "github.com/aws/aws-sdk-go-v2/feature/ec2/imds" - "github.com/aws/smithy-go/middleware" "github.com/edgelesssys/constellation/v2/internal/attestation/simulator" tpmclient "github.com/google/go-tpm-tools/client" "github.com/stretchr/testify/assert" @@ -25,6 +21,7 @@ func TestGetAttestationKey(t *testing.T) { if cgo == "0" { t.Skip("skipping test because CGO is disabled and tpm simulator requires it") } + require := require.New(t) assert := assert.New(t) @@ -32,7 +29,7 @@ func TestGetAttestationKey(t *testing.T) { require.NoError(err) defer tpm.Close() - // create the attestation ket in RSA format + // create the attestation key in RSA format tpmAk, err := tpmclient.AttestationKeyRSA(tpm) assert.NoError(err) assert.NotNil(tpmAk) @@ -45,83 +42,3 @@ func TestGetAttestationKey(t *testing.T) { // if everything worked fine, tpmAk and getAk are the same key assert.Equal(tpmAk, getAk) } - -func TestGetInstanceInfo(t *testing.T) { - cgo := os.Getenv("CGO_ENABLED") - if cgo == "0" { - t.Skip("skipping test because CGO is disabled and tpm simulator requires it") - } - testCases := map[string]struct { - client stubMetadataAPI - wantErr bool - }{ - "invalid region": { - client: stubMetadataAPI{ - instanceDoc: imds.InstanceIdentityDocument{ - Region: "invalid-region", - }, - instanceErr: errors.New("failed"), - }, - wantErr: true, - }, - "valid region": { - client: stubMetadataAPI{ - instanceDoc: imds.InstanceIdentityDocument{ - Region: "us-east-2", - }, - }, - }, - "invalid imageID": { - client: stubMetadataAPI{ - instanceDoc: imds.InstanceIdentityDocument{ - ImageID: "ami-fail", - }, - instanceErr: errors.New("failed"), - }, - wantErr: true, - }, - "valid imageID": { - client: stubMetadataAPI{ - instanceDoc: imds.InstanceIdentityDocument{ - ImageID: "ami-09e7c7f5617a47830", - }, - }, - }, - } - - for name, tc := range testCases { - t.Run(name, func(t *testing.T) { - assert := assert.New(t) - - tpm, err := simulator.OpenSimulatedTPM() - assert.NoError(err) - defer tpm.Close() - - instanceInfoFunc := getInstanceInfo(&tc.client) - assert.NotNil(instanceInfoFunc) - - info, err := instanceInfoFunc(context.Background(), tpm, nil) - if tc.wantErr { - assert.Error(err) - assert.Nil(info) - } else { - assert.Nil(err) - assert.NotNil(info) - } - }) - } -} - -type stubMetadataAPI struct { - instanceDoc imds.InstanceIdentityDocument - instanceErr error -} - -func (c *stubMetadataAPI) GetInstanceIdentityDocument(context.Context, *imds.GetInstanceIdentityDocumentInput, ...func(*imds.Options)) (*imds.GetInstanceIdentityDocumentOutput, error) { - output := &imds.InstanceIdentityDocument{} - - return &imds.GetInstanceIdentityDocumentOutput{ - InstanceIdentityDocument: *output, - ResultMetadata: middleware.Metadata{}, - }, c.instanceErr -} diff --git a/internal/attestation/aws/snp/testdata/BUILD.bazel b/internal/attestation/aws/snp/testdata/BUILD.bazel new file mode 100644 index 0000000000..ece972c67e --- /dev/null +++ b/internal/attestation/aws/snp/testdata/BUILD.bazel @@ -0,0 +1,13 @@ +load("@io_bazel_rules_go//go:def.bzl", "go_library") + +go_library( + name = "testdata", + srcs = ["testdata.go"], + embedsrcs = [ + "certchain.pem", + "vlek.pem", + "report.txt", + ], + importpath = "github.com/edgelesssys/constellation/v2/internal/attestation/aws/snp/testdata", + visibility = ["//:__subpackages__"], +) diff --git a/internal/attestation/aws/snp/testdata/certchain.pem b/internal/attestation/aws/snp/testdata/certchain.pem new file mode 100644 index 0000000000..fd5ff868f9 --- /dev/null +++ b/internal/attestation/aws/snp/testdata/certchain.pem @@ -0,0 +1,75 @@ +-----BEGIN CERTIFICATE----- +MIIGjzCCBD6gAwIBAgIDAQEBMEYGCSqGSIb3DQEBCjA5oA8wDQYJYIZIAWUDBAIC +BQChHDAaBgkqhkiG9w0BAQgwDQYJYIZIAWUDBAICBQCiAwIBMKMDAgEBMHsxFDAS +BgNVBAsMC0VuZ2luZWVyaW5nMQswCQYDVQQGEwJVUzEUMBIGA1UEBwwLU2FudGEg +Q2xhcmExCzAJBgNVBAgMAkNBMR8wHQYDVQQKDBZBZHZhbmNlZCBNaWNybyBEZXZp +Y2VzMRIwEAYDVQQDDAlBUkstTWlsYW4wHhcNMjIxMTE2MjI0NTI0WhcNNDcxMTE2 +MjI0NTI0WjCBgDEUMBIGA1UECwwLRW5naW5lZXJpbmcxCzAJBgNVBAYTAlVTMRQw +EgYDVQQHDAtTYW50YSBDbGFyYTELMAkGA1UECAwCQ0ExHzAdBgNVBAoMFkFkdmFu +Y2VkIE1pY3JvIERldmljZXMxFzAVBgNVBAMMDlNFVi1WTEVLLU1pbGFuMIICIjAN +BgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEA1EUWkz5FTPz+uWT2hCEyisam8FRu +XZAmS3l+rXgSCeS1Q0+1olcnFSJpiwfssfhoutJqePyicu+OhkX131PMeO/VOtH3 +upK4YNJmq36IJp7ZWIm5nK2fJNkYEHW0m/NXcIA9U2iHl5bAQ5cbGp97/FaOJ4Vm +GoTMV658Yox/plFmZRFfRcsw2hyNhqUl1gzdpnIIgPkygUovFEgaa0IVSgGLHQhZ +QiebNLLSVWRVReve0t94zlRIRRdrz84cckP9H9DTAUMyQaxSZbPINKbV6TPmtrwA +V9UP1Qq418xn9I+C0SsWutP/5S1OiL8OTzQ4CvgbHOfd2F3yVv4xDBza4SelF2ig +oDf+BF4XI/IIHJL2N5uKy3+gkSB2Xl6prohgVmqRFvBW9OTCEa32WhXu0t1Z1abE +KDZ3LpZt9/Crg6zyPpXDLR/tLHHpSaPRj7CTzHieKMTz+Q6RrCCQcHGfaAD/ETNY +56aHvNJRZgbzXDUJvnLr3dYyOvvn/DtKhCSimJynn7Len4ArDVQVwXRPe3hR/asC +E2CajT7kGC1AOtUzQuIKZS2D0Qk74g297JhLHpEBlQiyjRJ+LCWZNx9uJcixGyza +v6fiOWx4U8uWhRzHs8nvDAdcS4LW31tPlA9BeOK/BGimQTu7hM5MDFZL0C9dWK5p +uCUJex6I2vSqvycCAwEAAaOBozCBoDAdBgNVHQ4EFgQUNuJXE6qi45/CgqkKRPtV +LObC7pEwHwYDVR0jBBgwFoAUhawa0UP3yKxV1MUdQUir1XhK1FMwEgYDVR0TAQH/ +BAgwBgEB/wIBADAOBgNVHQ8BAf8EBAMCAQQwOgYDVR0fBDMwMTAvoC2gK4YpaHR0 +cHM6Ly9rZHNpbnRmLmFtZC5jb20vdmxlay92MS9NaWxhbi9jcmwwRgYJKoZIhvcN +AQEKMDmgDzANBglghkgBZQMEAgIFAKEcMBoGCSqGSIb3DQEBCDANBglghkgBZQME +AgIFAKIDAgEwowMCAQEDggIBAI7ayEXDNj1rCVnjQFb6L91NNOmEIOmi6XtopAqr +8fj7wqXap1MY82Y0AIi1K9R7C7G1sCmY8QyEyX0zqHsoNbU2IMcSdZrIp8neT8af +v8tPt7qoW3hZ+QQRMtgVkVVrjJZelvlB74xr5ifDcDiBd2vu/C9IqoQS4pVBKNSF +pofzjtYKvebBBBXxeM2b901UxNgVjCY26TtHEWN9cA6cDVqDDCCL6uOeR9UOvKDS +SqlM6nXldSj7bgK7Wh9M9587IwRvNZluXc1CDiKMZybLdSKOlyMJH9ss1GPn0eBV +EhVjf/gttn7HrcQ9xJZVXyDtL3tkGzemrPK14NOYzmph6xr1iiedAzOVpNdPiEXn +2lvas0P4TD9UgBh0Y7xyf2yENHiSgJT4T8Iktm/TSzuh4vqkQ72A1HdNTGjoZcfz +KCsQJ/YuFICeaNxw5cIAGBK/o+6Ek32NPv5XtixNOhEx7GsaVRG05bq5oTt14b4h +KYhqV1CDrX5hiVRpFFDs/sAGfgTzLdiGXLcvYAUz1tCKIT/eQS9c4/yitn4F3mCP +d4uQB+fggMtK0qPRthpFtc2SqVCTvHnhxyXqo7GpXMsssgLgKNwaFPe2+Ld5OwPR +6Pokji9h55m05Dxob8XtD4gW6oFLo9Icg7XqdOr9Iip5RBIPxy7rKk/ReqGs9KH7 +0YPk +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIIGYzCCBBKgAwIBAgIDAQAAMEYGCSqGSIb3DQEBCjA5oA8wDQYJYIZIAWUDBAIC +BQChHDAaBgkqhkiG9w0BAQgwDQYJYIZIAWUDBAICBQCiAwIBMKMDAgEBMHsxFDAS +BgNVBAsMC0VuZ2luZWVyaW5nMQswCQYDVQQGEwJVUzEUMBIGA1UEBwwLU2FudGEg +Q2xhcmExCzAJBgNVBAgMAkNBMR8wHQYDVQQKDBZBZHZhbmNlZCBNaWNybyBEZXZp +Y2VzMRIwEAYDVQQDDAlBUkstTWlsYW4wHhcNMjAxMDIyMTcyMzA1WhcNNDUxMDIy +MTcyMzA1WjB7MRQwEgYDVQQLDAtFbmdpbmVlcmluZzELMAkGA1UEBhMCVVMxFDAS +BgNVBAcMC1NhbnRhIENsYXJhMQswCQYDVQQIDAJDQTEfMB0GA1UECgwWQWR2YW5j +ZWQgTWljcm8gRGV2aWNlczESMBAGA1UEAwwJQVJLLU1pbGFuMIICIjANBgkqhkiG +9w0BAQEFAAOCAg8AMIICCgKCAgEA0Ld52RJOdeiJlqK2JdsVmD7FktuotWwX1fNg +W41XY9Xz1HEhSUmhLz9Cu9DHRlvgJSNxbeYYsnJfvyjx1MfU0V5tkKiU1EesNFta +1kTA0szNisdYc9isqk7mXT5+KfGRbfc4V/9zRIcE8jlHN61S1ju8X93+6dxDUrG2 +SzxqJ4BhqyYmUDruPXJSX4vUc01P7j98MpqOS95rORdGHeI52Naz5m2B+O+vjsC0 +60d37jY9LFeuOP4Meri8qgfi2S5kKqg/aF6aPtuAZQVR7u3KFYXP59XmJgtcog05 +gmI0T/OitLhuzVvpZcLph0odh/1IPXqx3+MnjD97A7fXpqGd/y8KxX7jksTEzAOg +bKAeam3lm+3yKIcTYMlsRMXPcjNbIvmsBykD//xSniusuHBkgnlENEWx1UcbQQrs ++gVDkuVPhsnzIRNgYvM48Y+7LGiJYnrmE8xcrexekBxrva2V9TJQqnN3Q53kt5vi +Qi3+gCfmkwC0F0tirIZbLkXPrPwzZ0M9eNxhIySb2npJfgnqz55I0u33wh4r0ZNQ +eTGfw03MBUtyuzGesGkcw+loqMaq1qR4tjGbPYxCvpCq7+OgpCCoMNit2uLo9M18 +fHz10lOMT8nWAUvRZFzteXCm+7PHdYPlmQwUw3LvenJ/ILXoQPHfbkH0CyPfhl1j +WhJFZasCAwEAAaN+MHwwDgYDVR0PAQH/BAQDAgEGMB0GA1UdDgQWBBSFrBrRQ/fI +rFXUxR1BSKvVeErUUzAPBgNVHRMBAf8EBTADAQH/MDoGA1UdHwQzMDEwL6AtoCuG +KWh0dHBzOi8va2RzaW50Zi5hbWQuY29tL3ZjZWsvdjEvTWlsYW4vY3JsMEYGCSqG +SIb3DQEBCjA5oA8wDQYJYIZIAWUDBAICBQChHDAaBgkqhkiG9w0BAQgwDQYJYIZI +AWUDBAICBQCiAwIBMKMDAgEBA4ICAQC6m0kDp6zv4Ojfgy+zleehsx6ol0ocgVel +ETobpx+EuCsqVFRPK1jZ1sp/lyd9+0fQ0r66n7kagRk4Ca39g66WGTJMeJdqYriw +STjjDCKVPSesWXYPVAyDhmP5n2v+BYipZWhpvqpaiO+EGK5IBP+578QeW/sSokrK +dHaLAxG2LhZxj9aF73fqC7OAJZ5aPonw4RE299FVarh1Tx2eT3wSgkDgutCTB1Yq +zT5DuwvAe+co2CIVIzMDamYuSFjPN0BCgojl7V+bTou7dMsqIu/TW/rPCX9/EUcp +KGKqPQ3P+N9r1hjEFY1plBg93t53OOo49GNI+V1zvXPLI6xIFVsh+mto2RtgEX/e +pmMKTNN6psW88qg7c1hTWtN6MbRuQ0vm+O+/2tKBF2h8THb94OvvHHoFDpbCELlq +HnIYhxy0YKXGyaW1NjfULxrrmxVW4wcn5E8GddmvNa6yYm8scJagEi13mhGu4Jqh +3QU3sf8iUSUr09xQDwHtOQUVIqx4maBZPBtSMf+qUDtjXSSq8lfWcd8bLr9mdsUn +JZJ0+tuPMKmBnSH860llKk+VpVQsgqbzDIvOLvD6W1Umq25boxCYJ+TuBoa4s+HH +CViAvgT9kf/rBq1d+ivj6skkHxuzcxbk1xv6ZGxrteJxVH7KlX7YRdZ6eARKwLe4 +AFZEAwoKCQ== +-----END CERTIFICATE----- diff --git a/internal/attestation/aws/snp/testdata/report.txt b/internal/attestation/aws/snp/testdata/report.txt new file mode 100644 index 0000000000..efd90375f8 --- /dev/null +++ b/internal/attestation/aws/snp/testdata/report.txt @@ -0,0 +1 @@ +AgAAAAAAAAAAAAMAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAEAAAADAAAAAAAK0QMAAAAAAAAABAAAAAAAAAADJjVhPI4zH6KeCWNxkQ/mofaTg92gLJRhQApwtm2Ho9pd2GMAJSK+Q6/DTywjOYm9bkAeNR0Q18yADW9d/PAZJayBD1xHUIkPsaFY8JeWLgTU1/tkDR0IqZgpz0pwVDpHzG+xkrvpCqcTFCNhpmFVAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACOsAob9aWVVnjx8VNbU/bqGewnLGnBSZbJu8smGfzcN///////////////////////////////////////////AwAAAAAACnMAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAwAAAAAACqkBNgEAATYBAAMAAAAAAAqpAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAm8z1/Oxcd+Bhdxd1okDoZ9gMiYw5Y/fp74hylcA2Eu+XPt5p+7fqqG7d7YLdJtTuAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAIOZBrwmRpIFfKDCywiFaiILyguTq/6vefDmdzNBKiRKtjdNiHa0hNgeQFGHspRcZAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA= diff --git a/internal/attestation/aws/snp/testdata/testdata.go b/internal/attestation/aws/snp/testdata/testdata.go new file mode 100644 index 0000000000..61d14f1549 --- /dev/null +++ b/internal/attestation/aws/snp/testdata/testdata.go @@ -0,0 +1,28 @@ +/* +Copyright (c) Edgeless Systems GmbH + +SPDX-License-Identifier: AGPL-3.0-only +*/ + +// Package testdata contains testing data for an attestation process. +package testdata + +import _ "embed" + +// SNPReport holds a valid VLEK-signed SNP report. +// +//go:embed report.txt +var SNPReport string + +// AKDigest holds the AK digest embedded in SNPReport.REPORT_DATA. +const AKDigest = "032635613c8e331fa29e096371910fe6a1f69383dda02c9461400a70b66d87a3da5dd863002522be43afc34f2c233989bd6e401e351d10d7cc800d6f5dfcf019" + +// VLEK for SNPReport. +// +//go:embed vlek.pem +var VLEK []byte + +// CertChain is a valid certificate chain for the VLEK certificate. Queried from AMD KDS. +// +//go:embed certchain.pem +var CertChain []byte diff --git a/internal/attestation/aws/snp/testdata/vlek.pem b/internal/attestation/aws/snp/testdata/vlek.pem new file mode 100644 index 0000000000..406a84235d --- /dev/null +++ b/internal/attestation/aws/snp/testdata/vlek.pem @@ -0,0 +1,30 @@ +-----BEGIN CERTIFICATE----- +MIIFLDCCAtugAwIBAgIBADBGBgkqhkiG9w0BAQowOaAPMA0GCWCGSAFlAwQCAgUA +oRwwGgYJKoZIhvcNAQEIMA0GCWCGSAFlAwQCAgUAogMCATCjAwIBATCBgDEUMBIG +A1UECwwLRW5naW5lZXJpbmcxCzAJBgNVBAYTAlVTMRQwEgYDVQQHDAtTYW50YSBD +bGFyYTELMAkGA1UECAwCQ0ExHzAdBgNVBAoMFkFkdmFuY2VkIE1pY3JvIERldmlj +ZXMxFzAVBgNVBAMMDlNFVi1WTEVLLU1pbGFuMB4XDTIzMDcxOTA4MjkyOFoXDTI0 +MDcxOTA4MjkyOFowejEUMBIGA1UECwwLRW5naW5lZXJpbmcxCzAJBgNVBAYTAlVT +MRQwEgYDVQQHDAtTYW50YSBDbGFyYTELMAkGA1UECAwCQ0ExHzAdBgNVBAoMFkFk +dmFuY2VkIE1pY3JvIERldmljZXMxETAPBgNVBAMMCFNFVi1WTEVLMHYwEAYHKoZI +zj0CAQYFK4EEACIDYgAEXFl4NHpiQCuZXIrehIEk/5XNIdMvo24wyaezN+0FouYB +9Z23nL523gpJUlT+mvb5ZMybh5tO1nBGFMOKwzP9dnSBwTs0qn57Ts9OTpW57EAo +Mx4SI7g1yz/mt4e6hma4o4HxMIHuMBAGCSsGAQQBnHgBAQQDAgEAMBQGCSsGAQQB +nHgBAgQHFgVNaWxhbjARBgorBgEEAZx4AQMBBAMCAQMwEQYKKwYBBAGceAEDAgQD +AgEAMBEGCisGAQQBnHgBAwQEAwIBADARBgorBgEEAZx4AQMFBAMCAQAwEQYKKwYB +BAGceAEDBgQDAgEAMBEGCisGAQQBnHgBAwcEAwIBADARBgorBgEEAZx4AQMDBAMC +AQowEQYKKwYBBAGceAEDCAQDAgFzMCwGCSsGAQQBnHgBBQQfFh1DTj1jYy11cy1l +YXN0LTIuYW1hem9uYXdzLmNvbTBGBgkqhkiG9w0BAQowOaAPMA0GCWCGSAFlAwQC +AgUAoRwwGgYJKoZIhvcNAQEIMA0GCWCGSAFlAwQCAgUAogMCATCjAwIBAQOCAgEA +E2CR10QkVTofcjmQbuu787J+H+OjzQLPIi/dUbP/LvZdYi/eWglYQPRbYxhxnIi1 +PB9R9c7LLhbNRhroog+TzrxyKLibEAW3rwn2iygPnsIemyL89wqtPNqEKNjhBXsb +s/0bmf0rNJ3lugssCAzrIStkx8at0K/099BEs4FuUM5u97HVy+jqLdRa2XOHMgGa +K7sNdR4swuLhfts9gOOX8ntJ+XkxtUx2mz449fXn8KN70mKa2YShhNd2JWJmv1jW +K0I1UxVVwIOHBn/W8fQL5a061oRQQaW5+wPRTys0iEMmLU7+plC8LNWeEq93TfFY +eUZ9EzinZ5S7z+c8J1FVWYNHGJauWj4lkjf+XGUZqXwTCPzou6tYJqqwWQEUUxXC +M3QKgbkIGWg4WKHIAXGChbM86JLY0W6VueOHyu4S1Z4i81IcDp4cs83WxYWfCpKH +Fq3Si2BhzZ0YGgK25JCkomh5Yf7dlsByyuQssf3TCqNmOfSFOTLvxfwTvLD5Omlm +O1mPI0YaoZya4WcPxbpWS+2Em23/5inQvT+ZhvMNkljD2NVbhLVGP1v4YR+T2zaC +0qJ4YYJ2ERQTnEUlKnlF9bm6PwZSRHupK6ecsGjH+Bz5hBPbT09nEpJf0bWkzVSA +AY8POFt3zBJiqONQuOlBpXzqKRKvFYQVEaX2EXQ+W6s= +-----END CERTIFICATE----- diff --git a/internal/attestation/aws/snp/validator.go b/internal/attestation/aws/snp/validator.go index 7ecc52885e..22d8b814b1 100644 --- a/internal/attestation/aws/snp/validator.go +++ b/internal/attestation/aws/snp/validator.go @@ -9,99 +9,210 @@ package snp import ( "context" "crypto" + "crypto/sha512" + "crypto/x509" "encoding/json" "fmt" - awsConfig "github.com/aws/aws-sdk-go-v2/config" - "github.com/aws/aws-sdk-go-v2/feature/ec2/imds" - "github.com/aws/aws-sdk-go-v2/service/ec2" "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/constellation/v2/internal/config" + "github.com/google/go-sev-guest/abi" + "github.com/google/go-sev-guest/kds" + "github.com/google/go-sev-guest/proto/sevsnp" + "github.com/google/go-sev-guest/validate" + "github.com/google/go-sev-guest/verify" + "github.com/google/go-sev-guest/verify/trust" "github.com/google/go-tpm-tools/proto/attest" "github.com/google/go-tpm/legacy/tpm2" ) // Validator for AWS TPM attestation. type Validator struct { + // Embed variant to identify the Validator using varaint.OID(). variant.AWSSEVSNP + // Embed validator to implement Validate method for aTLS handshake. *vtpm.Validator - getDescribeClient func(context.Context, string) (awsMetadataAPI, error) + // cfg contains version numbers required for the SNP report validation. + cfg *config.AWSSEVSNP + // reportValidator validates a SNP report. reportValidator is required for testing. + reportValidator snpReportValidator + // log is used for logging. + log attestation.Logger } // NewValidator create a new Validator structure and returns it. func NewValidator(cfg *config.AWSSEVSNP, log attestation.Logger) *Validator { - v := &Validator{} + v := &Validator{ + cfg: cfg, + reportValidator: &awsValidator{httpsGetter: trust.DefaultHTTPSGetter(), verifier: &reportVerifierImpl{}, validator: &reportValidatorImpl{}}, + log: log, + } + v.Validator = vtpm.NewValidator( cfg.Measurements, - getTrustedKey, - v.tpmEnabled, + v.getTrustedKey, + func(vtpm.AttestationDocument, *attest.MachineState) error { return nil }, log, ) - v.getDescribeClient = getEC2Client return v } -// getTrustedKeys return the public area of the provides attestation key. -// Normally, here the trust of this key should be verified, but currently AWS does not provide this feature. -func getTrustedKey(_ context.Context, attDoc vtpm.AttestationDocument, _ []byte) (crypto.PublicKey, error) { - // Copied from https://github.com/edgelesssys/constellation/blob/main/internal/attestation/qemu/validator.go +// getTrustedKeys return the public area of the provided attestation key (AK). +// Ideally, the AK should be bound to the TPM via an endorsement key, but currently AWS does not provide one. +// The AK's digest is written to the SNP report's userdata field during report generation. +// The AK is trusted if the report can be verified and the AK's digest matches the digest of the AK in attDoc. +func (v *Validator) getTrustedKey(_ context.Context, attDoc vtpm.AttestationDocument, _ []byte) (crypto.PublicKey, error) { pubArea, err := tpm2.DecodePublic(attDoc.Attestation.AkPub) if err != nil { - return nil, err + return nil, newDecodeError(err) + } + + pubKey, err := pubArea.Key() + if err != nil { + return nil, fmt.Errorf("getting public key: %w", err) + } + + akDigest, err := sha512sum(pubKey) + if err != nil { + return nil, fmt.Errorf("calculating hash of attestation key: %w", err) + } + + if err := v.reportValidator.validate(attDoc, (*x509.Certificate)(&v.cfg.AMDSigningKey), (*x509.Certificate)(&v.cfg.AMDRootKey), akDigest, v.cfg, v.log); err != nil { + return nil, fmt.Errorf("validating SNP report: %w", err) } return pubArea.Key() } -// tpmEnabled verifies if the virtual machine has the tpm2.0 feature enabled. -func (v *Validator) tpmEnabled(attestation vtpm.AttestationDocument, _ *attest.MachineState) error { - // https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/verify-nitrotpm-support-on-ami.html - // 1. Get the vm's ami (from IdentiTyDocument.imageId) - // 2. Check the value of key "TpmSupport": {"Value": "v2.0"}" - ctx := context.Background() - - idDocument := imds.InstanceIdentityDocument{} - err := json.Unmarshal(attestation.InstanceInfo, &idDocument) +// sha512sum PEM-encodes a public key and calculates the SHA512 hash of the encoded key. +func sha512sum(key crypto.PublicKey) ([64]byte, error) { + pub, err := x509.MarshalPKIXPublicKey(key) if err != nil { - return err + return [64]byte{}, fmt.Errorf("marshalling public key: %w", err) + } + + return sha512.Sum512(pub), nil +} + +// snpReportValidator validates a given SNP report. +type snpReportValidator interface { + validate(attestation vtpm.AttestationDocument, ask *x509.Certificate, ark *x509.Certificate, ak [64]byte, config *config.AWSSEVSNP, log attestation.Logger) error +} + +// awsValidator implements the validation for AWS SNP attestation. +// The properties exist for unittesting. +type awsValidator struct { + verifier reportVerifier + validator reportValidator + httpsGetter trust.HTTPSGetter +} + +type reportVerifier interface { + SnpAttestation(att *sevsnp.Attestation, opts *verify.Options) error +} +type reportValidator interface { + SnpAttestation(att *sevsnp.Attestation, opts *validate.Options) error +} + +type reportValidatorImpl struct{} + +func (r *reportValidatorImpl) SnpAttestation(att *sevsnp.Attestation, opts *validate.Options) error { + return validate.SnpAttestation(att, opts) +} + +type reportVerifierImpl struct{} + +func (r *reportVerifierImpl) SnpAttestation(att *sevsnp.Attestation, opts *verify.Options) error { + return verify.SnpAttestation(att, opts) +} + +// validate the report by checking if it has a valid VLEK signature. +// The certificate chain ARK -> ASK -> VLEK is also validated. +// Checks that the report's userData matches the connection's userData. +func (a *awsValidator) validate(attestation vtpm.AttestationDocument, ask *x509.Certificate, ark *x509.Certificate, akDigest [64]byte, config *config.AWSSEVSNP, log attestation.Logger) error { + var info snp.InstanceInfo + if err := json.Unmarshal(attestation.InstanceInfo, &info); err != nil { + return newValidationError(fmt.Errorf("unmarshalling instance info: %w", err)) } - imageID := idDocument.ImageID + certchain := snp.NewCertificateChain(ask, ark) - client, err := v.getDescribeClient(ctx, idDocument.Region) + att, err := info.AttestationWithCerts(a.httpsGetter, certchain, log) if err != nil { - return err + return newValidationError(fmt.Errorf("getting attestation with certs: %w", err)) } - // Currently, there seems to be a problem with retrieving image attributes directly. - // Alternatively, parse it from the general output. - imageOutput, err := client.DescribeImages(ctx, &ec2.DescribeImagesInput{ImageIds: []string{imageID}}) + + verifyOpts, err := getVerifyOpts(att) if err != nil { - return err + return newValidationError(fmt.Errorf("getting verify options: %w", err)) } - if len(imageOutput.Images) == 0 { - return fmt.Errorf("aws image %s not found", imageID) + + if err := a.verifier.SnpAttestation(att, verifyOpts); err != nil { + return newValidationError(fmt.Errorf("verifying SNP attestation: %w", err)) } - if len(imageOutput.Images) > 1 { - return fmt.Errorf("found multiple image references for image ID %s", imageID) + + validateOpts := &validate.Options{ + // Check that the attestation key's digest is included in the report. + ReportData: akDigest[:], + GuestPolicy: abi.SnpPolicy{ + Debug: false, // Debug means the VM can be decrypted by the host for debugging purposes and thus is not allowed. + SMT: true, // Allow Simultaneous Multi-Threading (SMT). Normally, we would want to disable SMT + // but AWS machines are currently facing issues if it's disabled. + }, + VMPL: new(int), // Checks that Virtual Machine Privilege Level (VMPL) is 0. + // This checks that the reported LaunchTCB version is equal or greater than the minimum specified in the config. + // We don't specify Options.MinimumTCB as it only restricts the allowed TCB for Current_ and Reported_TCB. + // Because we allow Options.ProvisionalFirmware, there is not security gained in also checking Current_ and Reported_TCB. + // We always have to check Launch_TCB as this value indicated the smallest TCB version a VM has seen during + // it's lifetime. + MinimumLaunchTCB: kds.TCBParts{ + BlSpl: config.BootloaderVersion.Value, // Bootloader + TeeSpl: config.TEEVersion.Value, // TEE (Secure OS) + SnpSpl: config.SNPVersion.Value, // SNP + UcodeSpl: config.MicrocodeVersion.Value, // Microcode + }, + // Check that CurrentTCB >= CommittedTCB. + PermitProvisionalFirmware: true, } - if imageOutput.Images[0].TpmSupport == "v2.0" { - return nil + // Checks if the attestation report matches the given constraints. + // Some constraints are implicitly checked by validate.SnpAttestation: + // - the report is not expired + if err := a.validator.SnpAttestation(att, validateOpts); err != nil { + return newValidationError(fmt.Errorf("validating SNP attestation: %w", err)) } - return fmt.Errorf("aws image %s does not support TPM v2.0", imageID) + return nil } -func getEC2Client(ctx context.Context, region string) (awsMetadataAPI, error) { - client, err := awsConfig.LoadDefaultConfig(ctx, awsConfig.WithRegion(region)) +func getVerifyOpts(att *sevsnp.Attestation) (*verify.Options, error) { + ask, err := x509.ParseCertificate(att.CertificateChain.AskCert) if err != nil { - return nil, err + return &verify.Options{}, fmt.Errorf("parsing VLEK certificate: %w", err) + } + ark, err := x509.ParseCertificate(att.CertificateChain.ArkCert) + if err != nil { + return &verify.Options{}, fmt.Errorf("parsing VLEK certificate: %w", err) + } + + verifyOpts := &verify.Options{ + DisableCertFetching: true, + TrustedRoots: map[string][]*trust.AMDRootCerts{ + "Milan": { + { + Product: "Milan", + ProductCerts: &trust.ProductCerts{ + // When using a VLEK signer, the intermediate certificate has to be stored in Asvk instead of Ask. + Asvk: ask, + Ark: ark, + }, + }, + }, + }, } - return ec2.NewFromConfig(client), nil -} -type awsMetadataAPI interface { - DescribeImages(ctx context.Context, params *ec2.DescribeImagesInput, optFns ...func(*ec2.Options)) (*ec2.DescribeImagesOutput, error) + return verifyOpts, nil } diff --git a/internal/attestation/aws/snp/validator_test.go b/internal/attestation/aws/snp/validator_test.go index 0eeb826e30..84804a8865 100644 --- a/internal/attestation/aws/snp/validator_test.go +++ b/internal/attestation/aws/snp/validator_test.go @@ -7,41 +7,66 @@ SPDX-License-Identifier: AGPL-3.0-only package snp import ( + "bytes" "context" + "crypto" + "crypto/x509" + "encoding/base64" + "encoding/hex" "encoding/json" + "encoding/pem" "errors" + "fmt" + "regexp" "testing" - "github.com/aws/aws-sdk-go-v2/feature/ec2/imds" - "github.com/aws/aws-sdk-go-v2/service/ec2" - "github.com/aws/aws-sdk-go-v2/service/ec2/types" + "github.com/edgelesssys/constellation/v2/internal/attestation" + "github.com/edgelesssys/constellation/v2/internal/attestation/aws/snp/testdata" + "github.com/edgelesssys/constellation/v2/internal/attestation/snp" "github.com/edgelesssys/constellation/v2/internal/attestation/vtpm" + "github.com/edgelesssys/constellation/v2/internal/config" + "github.com/edgelesssys/constellation/v2/internal/logger" + "github.com/google/go-sev-guest/abi" + "github.com/google/go-sev-guest/proto/sevsnp" + spb "github.com/google/go-sev-guest/proto/sevsnp" + "github.com/google/go-sev-guest/verify" "github.com/google/go-tpm-tools/proto/attest" "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" ) -func TestGeTrustedKey(t *testing.T) { +func TestGetTrustedKey(t *testing.T) { + validator := func() *Validator { return &Validator{reportValidator: stubawsValidator{}} } testCases := map[string]struct { - akPub []byte - info []byte - wantErr bool + akPub []byte + info []byte + wantErr bool + assertCorrectError func(error) }{ - "nul byte docs": { + "null byte docs": { akPub: []byte{0x00, 0x00, 0x00, 0x00}, info: []byte{0x00, 0x00, 0x00, 0x00}, wantErr: true, + assertCorrectError: func(err error) { + target := &decodeError{} + assert.ErrorAs(t, err, &target) + }, }, "nil": { akPub: nil, info: nil, wantErr: true, + assertCorrectError: func(err error) { + target := &decodeError{} + assert.ErrorAs(t, err, &target) + }, }, } for name, tc := range testCases { t.Run(name, func(t *testing.T) { assert := assert.New(t) - out, err := getTrustedKey( + out, err := validator().getTrustedKey( context.Background(), vtpm.AttestationDocument{ Attestation: &attest.Attestation{ @@ -54,6 +79,7 @@ func TestGeTrustedKey(t *testing.T) { if tc.wantErr { assert.Error(err) + tc.assertCorrectError(err) } else { assert.NoError(err) } @@ -63,48 +89,60 @@ func TestGeTrustedKey(t *testing.T) { } } -func TestTpmEnabled(t *testing.T) { - idDocNoTPM := imds.InstanceIdentityDocument{ - ImageID: "ami-tpm-disabled", - } - userDataNoTPM, _ := json.Marshal(idDocNoTPM) - attDocNoTPM := vtpm.AttestationDocument{ - InstanceInfo: userDataNoTPM, - } +// TestValidateSNPReport has to setup the following to run ValidateSNPReport: +// - parse ARK certificate from constants.go. +// - parse cached ASK certificate. +// - parse cached SNP report. +// - parse cached AK hash. Hash and SNP report have to match. +// - parse cache VLEK cert. +func TestValidateSNPReport(t *testing.T) { + require := require.New(t) + certs, err := loadCerts(testdata.CertChain) + require.NoError(err) + ark := certs[1] + ask := certs[0] - idDocTPM := imds.InstanceIdentityDocument{ - ImageID: "ami-tpm-enabled", - } - userDataTPM, _ := json.Marshal(idDocTPM) - attDocTPM := vtpm.AttestationDocument{ - InstanceInfo: userDataTPM, + // reportTransformer unpacks the base64 encoded report, applies the given transformations and re-encodes it. + reportTransformer := func(reportHex string, transformations func(*spb.Report)) string { + rawReport, err := base64.StdEncoding.DecodeString(reportHex) + require.NoError(err) + report, err := abi.ReportToProto(rawReport) + require.NoError(err) + transformations(report) + reportBytes, err := abi.ReportToAbiBytes(report) + require.NoError(err) + return base64.StdEncoding.EncodeToString(reportBytes) } testCases := map[string]struct { - attDoc vtpm.AttestationDocument - awsAPI awsMetadataAPI - wantErr bool + ak string + report string + reportTransformer func(string, func(*spb.Report)) string + verifier reportVerifier + validator reportValidator + wantErr bool }{ - "ami with tpm": { - attDoc: attDocNoTPM, - awsAPI: &stubDescribeAPI{describeImagesTPMSupport: "v2.0"}, + "success": { + ak: testdata.AKDigest, + report: testdata.SNPReport, + verifier: &reportVerifierImpl{}, + validator: &reportValidatorImpl{}, }, - "ami without tpm": { - attDoc: attDocTPM, - awsAPI: &stubDescribeAPI{describeImagesTPMSupport: "v1.0"}, - wantErr: true, + "invalid report data": { + ak: testdata.AKDigest, + report: reportTransformer(testdata.SNPReport, func(r *spb.Report) { + r.ReportData = make([]byte, 64) + }), + verifier: &stubReportVerifier{}, + validator: &reportValidatorImpl{}, + wantErr: true, }, - "ami undefined": { - attDoc: vtpm.AttestationDocument{}, - awsAPI: &stubDescribeAPI{describeImagesErr: errors.New("failed")}, - wantErr: true, - }, - "invalid json instanceIdentityDocument": { - attDoc: vtpm.AttestationDocument{ - UserData: []byte("{invalid}"), - }, - awsAPI: &stubDescribeAPI{describeImagesErr: errors.New("failed")}, - wantErr: true, + "invalid report signature": { + ak: testdata.AKDigest, + report: reportTransformer(testdata.SNPReport, func(r *spb.Report) { r.Signature[0]++ }), + verifier: &reportVerifierImpl{}, + validator: &reportValidatorImpl{}, + wantErr: true, }, } @@ -112,35 +150,156 @@ func TestTpmEnabled(t *testing.T) { t.Run(name, func(t *testing.T) { assert := assert.New(t) - v := Validator{ - getDescribeClient: func(context.Context, string) (awsMetadataAPI, error) { - return tc.awsAPI, nil - }, - } + hash, err := hex.DecodeString(tc.ak) + require.NoError(err) - err := v.tpmEnabled(tc.attDoc, nil) + report, err := base64.StdEncoding.DecodeString(tc.report) + require.NoError(err) + + info := snp.InstanceInfo{AttestationReport: report, ReportSigner: testdata.VLEK} + infoMarshalled, err := json.Marshal(info) + require.NoError(err) + + v := awsValidator{httpsGetter: newStubHTTPSGetter(&urlResponseMatcher{}, nil), verifier: tc.verifier, validator: tc.validator} + err = v.validate(vtpm.AttestationDocument{InstanceInfo: infoMarshalled}, ask, ark, [64]byte(hash), config.DefaultForAWSSEVSNP(), logger.NewTest(t)) if tc.wantErr { assert.Error(err) } else { - assert.Nil(err) + assert.NoError(err) } }) } } -type stubDescribeAPI struct { - describeImagesErr error - describeImagesTPMSupport string +type stubHTTPSGetter struct { + urlResponseMatcher *urlResponseMatcher // maps responses to requested URLs + err error } -func (a *stubDescribeAPI) DescribeImages( - _ context.Context, _ *ec2.DescribeImagesInput, _ ...func(*ec2.Options), -) (*ec2.DescribeImagesOutput, error) { - output := &ec2.DescribeImagesOutput{ - Images: []types.Image{ - {TpmSupport: types.TpmSupportValues(a.describeImagesTPMSupport)}, +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) + } +} + +func TestSha512sum(t *testing.T) { + testCases := map[string]struct { + key string + hash string + match bool + }{ + "success": { + // Generated using: rsa.GenerateKey(rand.Reader, 1024). + key: "30819f300d06092a864886f70d010101050003818d0030818902818100d4b2f072a32fa98456eb7f5938e2ff361fb64d698ea91e003d34bfc5374b814c16ba9ae3ec392ef6d48cf79b63067e338aa941219a7bcdf18aa43cd38bbe5567504838a3b1dca482035458853c5a171709dfae9df551815010bdfbc6df733cde84c4f7a5b0591d9cda9db087fb411ee3e2a4f19ad50c8331712ecdc5dd7ce34b0203010001", + hash: "2d6fe5ec59d7240b8a4c27c2ff27ba1071105fa50d45543768fcbabf9ee3cb8f8fa0afa51e08e053af30f6d11066ebfd47e75bda5ccc085c115d7e1896f3c62f", + match: true, + }, + "mismatching hash": { + key: "30819f300d06092a864886f70d010101050003818d0030818902818100d4b2f072a32fa98456eb7f5938e2ff361fb64d698ea91e003d34bfc5374b814c16ba9ae3ec392ef6d48cf79b63067e338aa941219a7bcdf18aa43cd38bbe5567504838a3b1dca482035458853c5a171709dfae9df551815010bdfbc6df733cde84c4f7a5b0591d9cda9db087fb411ee3e2a4f19ad50c8331712ecdc5dd7ce34b0203010001", + hash: "00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000", + match: false, }, } - return output, a.describeImagesErr + for name, tc := range testCases { + t.Run(name, func(t *testing.T) { + assert := assert.New(t) + require := require.New(t) + + newKey, err := loadKeyFromHex(tc.key) + require.NoError(err) + + // Function under test: + hash, err := sha512sum(newKey) + assert.NoError(err) + + expected, err := hex.DecodeString(tc.hash) + require.NoError(err) + + if tc.match { + assert.True(bytes.Equal(expected, hash[:]), fmt.Sprintf("expected hash %x, got %x", expected, hash)) + } else { + assert.False(bytes.Equal(expected, hash[:]), fmt.Sprintf("expected mismatching hashes, got %x", hash)) + } + }) + } +} + +func loadKeyFromHex(key string) (crypto.PublicKey, error) { + decoded, err := hex.DecodeString(key) + if err != nil { + return nil, err + } + + return x509.ParsePKIXPublicKey(decoded) +} + +// loadCachedCertChain loads a valid ARK and ASK from the testdata folder. +func loadCerts(pemData []byte) ([]*x509.Certificate, error) { + var certs []*x509.Certificate + + for len(pemData) > 0 { + var block *pem.Block + block, pemData = pem.Decode(pemData) + if block == nil { + break + } + + cert, err := x509.ParseCertificate(block.Bytes) + if err != nil { + return nil, err + } + + certs = append(certs, cert) + } + + if len(certs) == 0 { + return nil, errors.New("no valid certificates found") + } + + return certs, nil +} + +type stubawsValidator struct{} + +func (stubawsValidator) validate(_ vtpm.AttestationDocument, _ *x509.Certificate, _ *x509.Certificate, _ [64]byte, _ *config.AWSSEVSNP, _ attestation.Logger) error { + return nil +} + +type stubReportVerifier struct{} + +func (stubReportVerifier) SnpAttestation(_ *sevsnp.Attestation, _ *verify.Options) error { + return nil } diff --git a/internal/attestation/azure/snp/validator.go b/internal/attestation/azure/snp/validator.go index 5c7651d89d..856e528ffa 100644 --- a/internal/attestation/azure/snp/validator.go +++ b/internal/attestation/azure/snp/validator.go @@ -113,7 +113,7 @@ func (v *Validator) getTrustedKey(ctx context.Context, attDoc vtpm.AttestationDo return nil, fmt.Errorf("unmarshalling instanceInfo: %w", err) } - att, err := instanceInfo.AttestationWithCerts(v.log, v.getter, cachedCerts) + att, err := instanceInfo.AttestationWithCerts(v.getter, cachedCerts, v.log) if err != nil { return nil, fmt.Errorf("parsing attestation report: %w", err) } diff --git a/internal/attestation/snp/BUILD.bazel b/internal/attestation/snp/BUILD.bazel index 613751925e..700a3aa865 100644 --- a/internal/attestation/snp/BUILD.bazel +++ b/internal/attestation/snp/BUILD.bazel @@ -25,6 +25,7 @@ go_test( "//internal/config", "//internal/logger", "@com_github_google_go_sev_guest//kds", + "@com_github_google_go_sev_guest//verify/trust", "@com_github_stretchr_testify//assert", "@com_github_stretchr_testify//require", ], diff --git a/internal/attestation/snp/snp.go b/internal/attestation/snp/snp.go index a25c6f5d0a..79e9866c56 100644 --- a/internal/attestation/snp/snp.go +++ b/internal/attestation/snp/snp.go @@ -24,8 +24,7 @@ import ( // InstanceInfo contains the necessary information to establish trust in a SNP CVM. type InstanceInfo struct { - // ReportSigner is the PEM-encoded ReportSigner/VLEK certificate for the attestation report. - // Public key that validates the report's signature. + // ReportSigner is the PEM-encoded certificate used to validate the attestation report's signature. ReportSigner []byte // CertChain is the PEM-encoded certificate chain for the attestation report (ASK+ARK). // Intermediate key that validates the ReportSigner and root key. @@ -44,13 +43,56 @@ type AzureInstanceInfo struct { MAAToken string } +// addReportSigner parses the reportSigner certificate (VCEK/VLEK) from a and adds it to the attestation proto att. +// If reportSigner is empty and a VLEK is required, an error is returned. +// If reportSigner is empty and a VCEK is required, the VCEK is retrieved from AMD KDS. +func (a *InstanceInfo) addReportSigner(att *spb.Attestation, report *spb.Report, productName string, getter trust.HTTPSGetter, logger attestation.Logger) (abi.ReportSigner, error) { + // If the VCEK certificate is present, parse it and format it. + reportSigner, err := a.ParseReportSigner() + if err != nil { + logger.Warnf("Error parsing report signer: %v", err) + } + + signerInfo, err := abi.ParseSignerInfo(report.GetSignerInfo()) + if err != nil { + return abi.NoneReportSigner, fmt.Errorf("parsing signer info: %w", err) + } + + switch signerInfo.SigningKey { + case abi.VlekReportSigner: + if reportSigner == nil { + return abi.NoneReportSigner, fmt.Errorf("VLEK certificate required but not present") + } + att.CertificateChain.VlekCert = reportSigner.Raw + + case abi.VcekReportSigner: + var vcekData []byte + + // If no VCEK is present, fetch it from AMD. + if reportSigner == nil { + logger.Infof("VCEK certificate not present, falling back to retrieving it from AMD KDS") + vcekURL := kds.VCEKCertURL(productName, report.GetChipId(), kds.TCBVersion(report.GetReportedTcb())) + vcekData, err = getter.Get(vcekURL) + if err != nil { + return abi.NoneReportSigner, fmt.Errorf("retrieving VCEK certificate from AMD KDS: %w", err) + } + } else { + vcekData = reportSigner.Raw + } + + att.CertificateChain.VcekCert = vcekData + } + + return signerInfo.SigningKey, nil +} + // 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, +func (a *InstanceInfo) AttestationWithCerts(getter trust.HTTPSGetter, + fallbackCerts CertificateChain, logger attestation.Logger, ) (*spb.Attestation, error) { report, err := abi.ReportToProto(a.AttestationReport) if err != nil { @@ -67,22 +109,10 @@ func (a *InstanceInfo) AttestationWithCerts(logger attestation.Logger, getter tr Product: sevProduct, } - // If the VCEK certificate is present, parse it and format it. - vcek, err := a.ParseVCEK() + // Add VCEK/VLEK to attestation object. + signingInfo, err := a.addReportSigner(att, report, productName, getter, logger) 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 + return nil, fmt.Errorf("adding report signer: %w", err) } // If the certificate chain from THIM is present, parse it and format it. @@ -115,7 +145,7 @@ func (a *InstanceInfo) AttestationWithCerts(logger attestation.Logger, getter tr (att.CertificateChain.ArkCert != nil), (att.CertificateChain.AskCert != nil), ) - kdsCertChain, err := trust.GetProductChain(productName, abi.VcekReportSigner, getter) + kdsCertChain, err := trust.GetProductChain(productName, signingInfo, getter) if err != nil { return nil, fmt.Errorf("retrieving certificate chain from AMD KDS: %w", err) } @@ -174,7 +204,7 @@ func (a *InstanceInfo) ParseCertChain() (ask, ark *x509.Certificate, retErr erro // 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": + case "SEV-Milan", "SEV-VLEK-Milan": ask = cert case "ARK-Milan": ark = cert @@ -196,9 +226,9 @@ func (a *InstanceInfo) ParseCertChain() (ask, ark *x509.Certificate, retErr erro 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) { +// ParseReportSigner parses the VCEK/VLEK certificate from the instanceInfo into an x509-formatted certificate. +// If no certificate is present, nil is returned. +func (a *InstanceInfo) ParseReportSigner() (*x509.Certificate, error) { newlinesTrimmed := bytes.TrimSpace(a.ReportSigner) if len(newlinesTrimmed) == 0 { // VCEK is not present. @@ -216,10 +246,10 @@ func (a *InstanceInfo) ParseVCEK() (*x509.Certificate, error) { return nil, fmt.Errorf("expected PEM block type 'CERTIFICATE', got '%s'", block.Type) } - vcek, err := x509.ParseCertificate(block.Bytes) + reportSigner, err := x509.ParseCertificate(block.Bytes) if err != nil { return nil, fmt.Errorf("parsing VCEK certificate: %w", err) } - return vcek, nil + return reportSigner, nil } diff --git a/internal/attestation/snp/snp_test.go b/internal/attestation/snp/snp_test.go index ac9443869e..0179ac05b4 100644 --- a/internal/attestation/snp/snp_test.go +++ b/internal/attestation/snp/snp_test.go @@ -18,6 +18,7 @@ import ( "github.com/edgelesssys/constellation/v2/internal/config" "github.com/edgelesssys/constellation/v2/internal/logger" "github.com/google/go-sev-guest/kds" + "github.com/google/go-sev-guest/verify/trust" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) @@ -113,7 +114,7 @@ func TestParseVCEK(t *testing.T) { ReportSigner: tc.VCEK, } - vcek, err := instanceInfo.ParseVCEK() + vcek, err := instanceInfo.ParseReportSigner() if tc.wantErr { assert.Error(err) } else { @@ -124,10 +125,13 @@ func TestParseVCEK(t *testing.T) { } } -// TestInstanceInfoAttestation tests the basic unmarshalling of the attestation report and the ASK / ARK precedence. -func TestInstanceInfoAttestation(t *testing.T) { +// TestAttestationWithCerts tests the basic unmarshalling of the attestation report and the ASK / ARK precedence. +func TestAttestationWithCerts(t *testing.T) { defaultReport := testdata.AttestationReport + vlekReport, err := hex.DecodeString(testdata.AttestationReportVLEK) + require.NoError(t, err) testdataArk, testdataAsk := mustCertChainToPem(t, testdata.CertChain) + testdataArvk, testdataAsvk := mustCertChainToPem(t, testdata.VlekCertChain) exampleCert := &x509.Certificate{ Raw: []byte{1, 2, 3}, } @@ -135,7 +139,8 @@ func TestInstanceInfoAttestation(t *testing.T) { testCases := map[string]struct { report []byte - vcek []byte + idkeydigest string + reportSigner []byte certChain []byte fallbackCerts CertificateChain getter *stubHTTPSGetter @@ -144,15 +149,33 @@ func TestInstanceInfoAttestation(t *testing.T) { wantErr bool }{ "success": { - report: defaultReport, - vcek: testdata.AzureThimVCEK, - certChain: testdata.CertChain, - expectedArk: testdataArk, - expectedAsk: testdataAsk, + report: defaultReport, + idkeydigest: "57e229e0ffe5fa92d0faddff6cae0e61c926fc9ef9afd20a8b8cfcf7129db9338cbe5bf3f6987733a2bf65d06dc38fc1", + reportSigner: testdata.AzureThimVCEK, + certChain: testdata.CertChain, + expectedArk: testdataArk, + expectedAsk: testdataAsk, + }, + "vlek success": { + report: vlekReport, + idkeydigest: "000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000", + reportSigner: testdata.Vlek, + expectedArk: testdataArvk, + expectedAsk: testdataAsvk, + getter: newStubHTTPSGetter( + &urlResponseMatcher{ + certChainResponse: testdata.VlekCertChain, + vcekResponse: testdata.Vlek, + wantCertChainRequest: true, + wantVcekRequest: true, + }, + nil, + ), }, "retrieve vcek": { - report: defaultReport, - certChain: testdata.CertChain, + report: defaultReport, + idkeydigest: "57e229e0ffe5fa92d0faddff6cae0e61c926fc9ef9afd20a8b8cfcf7129db9338cbe5bf3f6987733a2bf65d06dc38fc1", + certChain: testdata.CertChain, getter: newStubHTTPSGetter( &urlResponseMatcher{ vcekResponse: testdata.AmdKdsVCEK, @@ -164,8 +187,9 @@ func TestInstanceInfoAttestation(t *testing.T) { expectedAsk: testdataAsk, }, "retrieve certchain": { - report: defaultReport, - vcek: testdata.AzureThimVCEK, + report: defaultReport, + idkeydigest: "57e229e0ffe5fa92d0faddff6cae0e61c926fc9ef9afd20a8b8cfcf7129db9338cbe5bf3f6987733a2bf65d06dc38fc1", + reportSigner: testdata.AzureThimVCEK, getter: newStubHTTPSGetter( &urlResponseMatcher{ certChainResponse: testdata.CertChain, @@ -178,7 +202,8 @@ func TestInstanceInfoAttestation(t *testing.T) { }, "use fallback certs": { report: defaultReport, - vcek: testdata.AzureThimVCEK, + idkeydigest: "57e229e0ffe5fa92d0faddff6cae0e61c926fc9ef9afd20a8b8cfcf7129db9338cbe5bf3f6987733a2bf65d06dc38fc1", + reportSigner: testdata.AzureThimVCEK, fallbackCerts: NewCertificateChain(exampleCert, exampleCert), getter: newStubHTTPSGetter( &urlResponseMatcher{}, @@ -189,8 +214,9 @@ func TestInstanceInfoAttestation(t *testing.T) { }, "use certchain with fallback certs": { report: defaultReport, + idkeydigest: "57e229e0ffe5fa92d0faddff6cae0e61c926fc9ef9afd20a8b8cfcf7129db9338cbe5bf3f6987733a2bf65d06dc38fc1", certChain: testdata.CertChain, - vcek: testdata.AzureThimVCEK, + reportSigner: testdata.AzureThimVCEK, fallbackCerts: NewCertificateChain(&x509.Certificate{}, &x509.Certificate{}), getter: newStubHTTPSGetter( &urlResponseMatcher{}, @@ -200,7 +226,8 @@ func TestInstanceInfoAttestation(t *testing.T) { expectedAsk: testdataAsk, }, "retrieve vcek and certchain": { - report: defaultReport, + report: defaultReport, + idkeydigest: "57e229e0ffe5fa92d0faddff6cae0e61c926fc9ef9afd20a8b8cfcf7129db9338cbe5bf3f6987733a2bf65d06dc38fc1", getter: newStubHTTPSGetter( &urlResponseMatcher{ certChainResponse: testdata.CertChain, @@ -235,10 +262,11 @@ func TestInstanceInfoAttestation(t *testing.T) { instanceInfo := InstanceInfo{ AttestationReport: tc.report, CertChain: tc.certChain, - ReportSigner: tc.vcek, + ReportSigner: tc.reportSigner, } - att, err := instanceInfo.AttestationWithCerts(logger.NewTest(t), tc.getter, tc.fallbackCerts) + defer trust.ClearProductCertCache() + att, err := instanceInfo.AttestationWithCerts(tc.getter, tc.fallbackCerts, logger.NewTest(t)) if tc.wantErr { assert.Error(err) } else { @@ -247,7 +275,7 @@ func TestInstanceInfoAttestation(t *testing.T) { assert.NotNil(att.CertificateChain) assert.NotNil(att.Report) - assert.Equal(hex.EncodeToString(att.Report.IdKeyDigest[:]), "57e229e0ffe5fa92d0faddff6cae0e61c926fc9ef9afd20a8b8cfcf7129db9338cbe5bf3f6987733a2bf65d06dc38fc1") + assert.Equal(tc.idkeydigest, hex.EncodeToString(att.Report.IdKeyDigest[:])) // 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. @@ -299,12 +327,12 @@ type urlResponseMatcher struct { func (m *urlResponseMatcher) match(url string) ([]byte, error) { switch { - case url == "https://kdsintf.amd.com/vcek/v1/Milan/cert_chain": + case regexp.MustCompile(`https:\/\/kdsintf.amd.com\/(vcek|vlek)\/v1\/Milan\/cert_chain`).MatchString(url): 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): + case regexp.MustCompile(`https:\/\/kdsintf.amd.com\/(vcek|vlek)\/v1\/Milan\/.*`).MatchString(url): if !m.wantVcekRequest { return nil, fmt.Errorf("unexpected VCEK request") } diff --git a/internal/attestation/snp/testdata/BUILD.bazel b/internal/attestation/snp/testdata/BUILD.bazel index 54369e85a4..c460ed611d 100644 --- a/internal/attestation/snp/testdata/BUILD.bazel +++ b/internal/attestation/snp/testdata/BUILD.bazel @@ -9,6 +9,8 @@ go_library( "certchain.pem", "runtimedata.bin", "vcek.pem", + "vlek.pem", + "vlekcertchain.pem", ], importpath = "github.com/edgelesssys/constellation/v2/internal/attestation/snp/testdata", visibility = ["//:__subpackages__"], diff --git a/internal/attestation/snp/testdata/testdata.go b/internal/attestation/snp/testdata/testdata.go index 9e547a5746..c749dd8998 100644 --- a/internal/attestation/snp/testdata/testdata.go +++ b/internal/attestation/snp/testdata/testdata.go @@ -9,11 +9,14 @@ package testdata import _ "embed" -// AttestationBytes is an example attestation report from a Constellation VM. +// AttestationReport is an example attestation report from a Constellation VM. // //go:embed attestation.bin var AttestationReport []byte +// AttestationReportVLEK is an example attestation report signed by a VLEK. +const AttestationReportVLEK = "02000000000000000000030000000000000000000000000000000000000000000000000000000000000000000000000000000000010000000300000000000ace0300000000000000040000000000000044a93ab043ad14ece9bfa97305d95302c9cc6ed95e17efaf7348ed7a7603e1ca89d12758e089d2abcf5a4dd16a99e3cb4cba8f0b8e8cb8eac3e926f1d2b5cfecc2c84b9364fc9f0f54b04534768c860c6e0e386ad98b96e8b98eca46ac8971d05c531ba48373f054c880cfd1f4a0a84e00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000008c5d6770df734a203cd061a3698e702caed25e7f744dc060eb9dcba0f2e4bdb2ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff0300000000000a73000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000300000000000a7301360100013601000300000000000a73000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000b9853dac65f127574c6a578c11885e1887d4c7ae446237d4273715dd8c05cfe4bd49facc1392f2ca7354c8f0d34d65500000000000000000000000000000000000000000000000004013481e9c6a6bb112818aeba3bd178d788dedf62600b8c7892a8d3df4d880265010e7d833201156364a001e62f47b570000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000" + // AzureThimVCEK is an example VCEK certificate (PEM, as returned from Azure THIM) for the AttestationReport. // //go:embed vcek.pem @@ -33,3 +36,13 @@ var RuntimeData []byte // //go:embed certchain.pem var CertChain []byte + +// VlekCertChain is a valid certificate chain (PEM) for the VLEK certificate. +// +//go:embed vlekcertchain.pem +var VlekCertChain []byte + +// Vlek is a valid VLEK certificate (PEM). +// +//go:embed vlek.pem +var Vlek []byte diff --git a/internal/attestation/snp/testdata/vlek.pem b/internal/attestation/snp/testdata/vlek.pem new file mode 100644 index 0000000000..406a84235d --- /dev/null +++ b/internal/attestation/snp/testdata/vlek.pem @@ -0,0 +1,30 @@ +-----BEGIN CERTIFICATE----- +MIIFLDCCAtugAwIBAgIBADBGBgkqhkiG9w0BAQowOaAPMA0GCWCGSAFlAwQCAgUA +oRwwGgYJKoZIhvcNAQEIMA0GCWCGSAFlAwQCAgUAogMCATCjAwIBATCBgDEUMBIG +A1UECwwLRW5naW5lZXJpbmcxCzAJBgNVBAYTAlVTMRQwEgYDVQQHDAtTYW50YSBD +bGFyYTELMAkGA1UECAwCQ0ExHzAdBgNVBAoMFkFkdmFuY2VkIE1pY3JvIERldmlj +ZXMxFzAVBgNVBAMMDlNFVi1WTEVLLU1pbGFuMB4XDTIzMDcxOTA4MjkyOFoXDTI0 +MDcxOTA4MjkyOFowejEUMBIGA1UECwwLRW5naW5lZXJpbmcxCzAJBgNVBAYTAlVT +MRQwEgYDVQQHDAtTYW50YSBDbGFyYTELMAkGA1UECAwCQ0ExHzAdBgNVBAoMFkFk +dmFuY2VkIE1pY3JvIERldmljZXMxETAPBgNVBAMMCFNFVi1WTEVLMHYwEAYHKoZI +zj0CAQYFK4EEACIDYgAEXFl4NHpiQCuZXIrehIEk/5XNIdMvo24wyaezN+0FouYB +9Z23nL523gpJUlT+mvb5ZMybh5tO1nBGFMOKwzP9dnSBwTs0qn57Ts9OTpW57EAo +Mx4SI7g1yz/mt4e6hma4o4HxMIHuMBAGCSsGAQQBnHgBAQQDAgEAMBQGCSsGAQQB +nHgBAgQHFgVNaWxhbjARBgorBgEEAZx4AQMBBAMCAQMwEQYKKwYBBAGceAEDAgQD +AgEAMBEGCisGAQQBnHgBAwQEAwIBADARBgorBgEEAZx4AQMFBAMCAQAwEQYKKwYB +BAGceAEDBgQDAgEAMBEGCisGAQQBnHgBAwcEAwIBADARBgorBgEEAZx4AQMDBAMC +AQowEQYKKwYBBAGceAEDCAQDAgFzMCwGCSsGAQQBnHgBBQQfFh1DTj1jYy11cy1l +YXN0LTIuYW1hem9uYXdzLmNvbTBGBgkqhkiG9w0BAQowOaAPMA0GCWCGSAFlAwQC +AgUAoRwwGgYJKoZIhvcNAQEIMA0GCWCGSAFlAwQCAgUAogMCATCjAwIBAQOCAgEA +E2CR10QkVTofcjmQbuu787J+H+OjzQLPIi/dUbP/LvZdYi/eWglYQPRbYxhxnIi1 +PB9R9c7LLhbNRhroog+TzrxyKLibEAW3rwn2iygPnsIemyL89wqtPNqEKNjhBXsb +s/0bmf0rNJ3lugssCAzrIStkx8at0K/099BEs4FuUM5u97HVy+jqLdRa2XOHMgGa +K7sNdR4swuLhfts9gOOX8ntJ+XkxtUx2mz449fXn8KN70mKa2YShhNd2JWJmv1jW +K0I1UxVVwIOHBn/W8fQL5a061oRQQaW5+wPRTys0iEMmLU7+plC8LNWeEq93TfFY +eUZ9EzinZ5S7z+c8J1FVWYNHGJauWj4lkjf+XGUZqXwTCPzou6tYJqqwWQEUUxXC +M3QKgbkIGWg4WKHIAXGChbM86JLY0W6VueOHyu4S1Z4i81IcDp4cs83WxYWfCpKH +Fq3Si2BhzZ0YGgK25JCkomh5Yf7dlsByyuQssf3TCqNmOfSFOTLvxfwTvLD5Omlm +O1mPI0YaoZya4WcPxbpWS+2Em23/5inQvT+ZhvMNkljD2NVbhLVGP1v4YR+T2zaC +0qJ4YYJ2ERQTnEUlKnlF9bm6PwZSRHupK6ecsGjH+Bz5hBPbT09nEpJf0bWkzVSA +AY8POFt3zBJiqONQuOlBpXzqKRKvFYQVEaX2EXQ+W6s= +-----END CERTIFICATE----- diff --git a/internal/attestation/snp/testdata/vlekcertchain.pem b/internal/attestation/snp/testdata/vlekcertchain.pem new file mode 100644 index 0000000000..fd5ff868f9 --- /dev/null +++ b/internal/attestation/snp/testdata/vlekcertchain.pem @@ -0,0 +1,75 @@ +-----BEGIN CERTIFICATE----- +MIIGjzCCBD6gAwIBAgIDAQEBMEYGCSqGSIb3DQEBCjA5oA8wDQYJYIZIAWUDBAIC +BQChHDAaBgkqhkiG9w0BAQgwDQYJYIZIAWUDBAICBQCiAwIBMKMDAgEBMHsxFDAS +BgNVBAsMC0VuZ2luZWVyaW5nMQswCQYDVQQGEwJVUzEUMBIGA1UEBwwLU2FudGEg +Q2xhcmExCzAJBgNVBAgMAkNBMR8wHQYDVQQKDBZBZHZhbmNlZCBNaWNybyBEZXZp +Y2VzMRIwEAYDVQQDDAlBUkstTWlsYW4wHhcNMjIxMTE2MjI0NTI0WhcNNDcxMTE2 +MjI0NTI0WjCBgDEUMBIGA1UECwwLRW5naW5lZXJpbmcxCzAJBgNVBAYTAlVTMRQw +EgYDVQQHDAtTYW50YSBDbGFyYTELMAkGA1UECAwCQ0ExHzAdBgNVBAoMFkFkdmFu +Y2VkIE1pY3JvIERldmljZXMxFzAVBgNVBAMMDlNFVi1WTEVLLU1pbGFuMIICIjAN +BgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEA1EUWkz5FTPz+uWT2hCEyisam8FRu +XZAmS3l+rXgSCeS1Q0+1olcnFSJpiwfssfhoutJqePyicu+OhkX131PMeO/VOtH3 +upK4YNJmq36IJp7ZWIm5nK2fJNkYEHW0m/NXcIA9U2iHl5bAQ5cbGp97/FaOJ4Vm +GoTMV658Yox/plFmZRFfRcsw2hyNhqUl1gzdpnIIgPkygUovFEgaa0IVSgGLHQhZ +QiebNLLSVWRVReve0t94zlRIRRdrz84cckP9H9DTAUMyQaxSZbPINKbV6TPmtrwA +V9UP1Qq418xn9I+C0SsWutP/5S1OiL8OTzQ4CvgbHOfd2F3yVv4xDBza4SelF2ig +oDf+BF4XI/IIHJL2N5uKy3+gkSB2Xl6prohgVmqRFvBW9OTCEa32WhXu0t1Z1abE +KDZ3LpZt9/Crg6zyPpXDLR/tLHHpSaPRj7CTzHieKMTz+Q6RrCCQcHGfaAD/ETNY +56aHvNJRZgbzXDUJvnLr3dYyOvvn/DtKhCSimJynn7Len4ArDVQVwXRPe3hR/asC +E2CajT7kGC1AOtUzQuIKZS2D0Qk74g297JhLHpEBlQiyjRJ+LCWZNx9uJcixGyza +v6fiOWx4U8uWhRzHs8nvDAdcS4LW31tPlA9BeOK/BGimQTu7hM5MDFZL0C9dWK5p +uCUJex6I2vSqvycCAwEAAaOBozCBoDAdBgNVHQ4EFgQUNuJXE6qi45/CgqkKRPtV +LObC7pEwHwYDVR0jBBgwFoAUhawa0UP3yKxV1MUdQUir1XhK1FMwEgYDVR0TAQH/ +BAgwBgEB/wIBADAOBgNVHQ8BAf8EBAMCAQQwOgYDVR0fBDMwMTAvoC2gK4YpaHR0 +cHM6Ly9rZHNpbnRmLmFtZC5jb20vdmxlay92MS9NaWxhbi9jcmwwRgYJKoZIhvcN +AQEKMDmgDzANBglghkgBZQMEAgIFAKEcMBoGCSqGSIb3DQEBCDANBglghkgBZQME +AgIFAKIDAgEwowMCAQEDggIBAI7ayEXDNj1rCVnjQFb6L91NNOmEIOmi6XtopAqr +8fj7wqXap1MY82Y0AIi1K9R7C7G1sCmY8QyEyX0zqHsoNbU2IMcSdZrIp8neT8af +v8tPt7qoW3hZ+QQRMtgVkVVrjJZelvlB74xr5ifDcDiBd2vu/C9IqoQS4pVBKNSF +pofzjtYKvebBBBXxeM2b901UxNgVjCY26TtHEWN9cA6cDVqDDCCL6uOeR9UOvKDS +SqlM6nXldSj7bgK7Wh9M9587IwRvNZluXc1CDiKMZybLdSKOlyMJH9ss1GPn0eBV +EhVjf/gttn7HrcQ9xJZVXyDtL3tkGzemrPK14NOYzmph6xr1iiedAzOVpNdPiEXn +2lvas0P4TD9UgBh0Y7xyf2yENHiSgJT4T8Iktm/TSzuh4vqkQ72A1HdNTGjoZcfz +KCsQJ/YuFICeaNxw5cIAGBK/o+6Ek32NPv5XtixNOhEx7GsaVRG05bq5oTt14b4h +KYhqV1CDrX5hiVRpFFDs/sAGfgTzLdiGXLcvYAUz1tCKIT/eQS9c4/yitn4F3mCP +d4uQB+fggMtK0qPRthpFtc2SqVCTvHnhxyXqo7GpXMsssgLgKNwaFPe2+Ld5OwPR +6Pokji9h55m05Dxob8XtD4gW6oFLo9Icg7XqdOr9Iip5RBIPxy7rKk/ReqGs9KH7 +0YPk +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIIGYzCCBBKgAwIBAgIDAQAAMEYGCSqGSIb3DQEBCjA5oA8wDQYJYIZIAWUDBAIC +BQChHDAaBgkqhkiG9w0BAQgwDQYJYIZIAWUDBAICBQCiAwIBMKMDAgEBMHsxFDAS +BgNVBAsMC0VuZ2luZWVyaW5nMQswCQYDVQQGEwJVUzEUMBIGA1UEBwwLU2FudGEg +Q2xhcmExCzAJBgNVBAgMAkNBMR8wHQYDVQQKDBZBZHZhbmNlZCBNaWNybyBEZXZp +Y2VzMRIwEAYDVQQDDAlBUkstTWlsYW4wHhcNMjAxMDIyMTcyMzA1WhcNNDUxMDIy +MTcyMzA1WjB7MRQwEgYDVQQLDAtFbmdpbmVlcmluZzELMAkGA1UEBhMCVVMxFDAS +BgNVBAcMC1NhbnRhIENsYXJhMQswCQYDVQQIDAJDQTEfMB0GA1UECgwWQWR2YW5j +ZWQgTWljcm8gRGV2aWNlczESMBAGA1UEAwwJQVJLLU1pbGFuMIICIjANBgkqhkiG +9w0BAQEFAAOCAg8AMIICCgKCAgEA0Ld52RJOdeiJlqK2JdsVmD7FktuotWwX1fNg +W41XY9Xz1HEhSUmhLz9Cu9DHRlvgJSNxbeYYsnJfvyjx1MfU0V5tkKiU1EesNFta +1kTA0szNisdYc9isqk7mXT5+KfGRbfc4V/9zRIcE8jlHN61S1ju8X93+6dxDUrG2 +SzxqJ4BhqyYmUDruPXJSX4vUc01P7j98MpqOS95rORdGHeI52Naz5m2B+O+vjsC0 +60d37jY9LFeuOP4Meri8qgfi2S5kKqg/aF6aPtuAZQVR7u3KFYXP59XmJgtcog05 +gmI0T/OitLhuzVvpZcLph0odh/1IPXqx3+MnjD97A7fXpqGd/y8KxX7jksTEzAOg +bKAeam3lm+3yKIcTYMlsRMXPcjNbIvmsBykD//xSniusuHBkgnlENEWx1UcbQQrs ++gVDkuVPhsnzIRNgYvM48Y+7LGiJYnrmE8xcrexekBxrva2V9TJQqnN3Q53kt5vi +Qi3+gCfmkwC0F0tirIZbLkXPrPwzZ0M9eNxhIySb2npJfgnqz55I0u33wh4r0ZNQ +eTGfw03MBUtyuzGesGkcw+loqMaq1qR4tjGbPYxCvpCq7+OgpCCoMNit2uLo9M18 +fHz10lOMT8nWAUvRZFzteXCm+7PHdYPlmQwUw3LvenJ/ILXoQPHfbkH0CyPfhl1j +WhJFZasCAwEAAaN+MHwwDgYDVR0PAQH/BAQDAgEGMB0GA1UdDgQWBBSFrBrRQ/fI +rFXUxR1BSKvVeErUUzAPBgNVHRMBAf8EBTADAQH/MDoGA1UdHwQzMDEwL6AtoCuG +KWh0dHBzOi8va2RzaW50Zi5hbWQuY29tL3ZjZWsvdjEvTWlsYW4vY3JsMEYGCSqG +SIb3DQEBCjA5oA8wDQYJYIZIAWUDBAICBQChHDAaBgkqhkiG9w0BAQgwDQYJYIZI +AWUDBAICBQCiAwIBMKMDAgEBA4ICAQC6m0kDp6zv4Ojfgy+zleehsx6ol0ocgVel +ETobpx+EuCsqVFRPK1jZ1sp/lyd9+0fQ0r66n7kagRk4Ca39g66WGTJMeJdqYriw +STjjDCKVPSesWXYPVAyDhmP5n2v+BYipZWhpvqpaiO+EGK5IBP+578QeW/sSokrK +dHaLAxG2LhZxj9aF73fqC7OAJZ5aPonw4RE299FVarh1Tx2eT3wSgkDgutCTB1Yq +zT5DuwvAe+co2CIVIzMDamYuSFjPN0BCgojl7V+bTou7dMsqIu/TW/rPCX9/EUcp +KGKqPQ3P+N9r1hjEFY1plBg93t53OOo49GNI+V1zvXPLI6xIFVsh+mto2RtgEX/e +pmMKTNN6psW88qg7c1hTWtN6MbRuQ0vm+O+/2tKBF2h8THb94OvvHHoFDpbCELlq +HnIYhxy0YKXGyaW1NjfULxrrmxVW4wcn5E8GddmvNa6yYm8scJagEi13mhGu4Jqh +3QU3sf8iUSUr09xQDwHtOQUVIqx4maBZPBtSMf+qUDtjXSSq8lfWcd8bLr9mdsUn +JZJ0+tuPMKmBnSH860llKk+VpVQsgqbzDIvOLvD6W1Umq25boxCYJ+TuBoa4s+HH +CViAvgT9kf/rBq1d+ivj6skkHxuzcxbk1xv6ZGxrteJxVH7KlX7YRdZ6eARKwLe4 +AFZEAwoKCQ== +-----END CERTIFICATE----- diff --git a/internal/config/BUILD.bazel b/internal/config/BUILD.bazel index 7c94b72fac..c6b99c5143 100644 --- a/internal/config/BUILD.bazel +++ b/internal/config/BUILD.bazel @@ -6,6 +6,7 @@ go_library( srcs = [ "attestation.go", "attestationversion.go", + "aws.go", "azure.go", "config.go", "config_doc.go", diff --git a/internal/config/attestation.go b/internal/config/attestation.go index afab4211d9..4e05c33b30 100644 --- a/internal/config/attestation.go +++ b/internal/config/attestation.go @@ -16,6 +16,9 @@ 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` + // AttestationCfg is the common interface for passing attestation configs. type AttestationCfg interface { // GetMeasurements returns the measurements that should be used for attestation. diff --git a/internal/config/aws.go b/internal/config/aws.go new file mode 100644 index 0000000000..01a0843ce4 --- /dev/null +++ b/internal/config/aws.go @@ -0,0 +1,75 @@ +/* +Copyright (c) Edgeless Systems GmbH + +SPDX-License-Identifier: AGPL-3.0-only +*/ +package config + +import ( + "fmt" + + "github.com/edgelesssys/constellation/v2/internal/attestation/measurements" + "github.com/edgelesssys/constellation/v2/internal/attestation/variant" + "github.com/edgelesssys/constellation/v2/internal/cloud/cloudprovider" +) + +// DefaultForAWSSEVSNP provides a valid default configuration for AWS SEV-SNP attestation. +func DefaultForAWSSEVSNP() *AWSSEVSNP { + return &AWSSEVSNP{ + Measurements: measurements.DefaultsFor(cloudprovider.AWS, variant.AWSSEVSNP{}), + BootloaderVersion: NewLatestPlaceholderVersion(), + TEEVersion: NewLatestPlaceholderVersion(), + SNPVersion: NewLatestPlaceholderVersion(), + MicrocodeVersion: NewLatestPlaceholderVersion(), + AMDRootKey: mustParsePEM(arkPEM), + } +} + +// GetVariant returns aws-sev-snp as the variant. +func (AWSSEVSNP) GetVariant() variant.Variant { + return variant.AWSSEVSNP{} +} + +// GetMeasurements returns the measurements used for attestation. +func (c AWSSEVSNP) GetMeasurements() measurements.M { + return c.Measurements +} + +// SetMeasurements updates a config's measurements using the given measurements. +func (c *AWSSEVSNP) SetMeasurements(m measurements.M) { + c.Measurements = m +} + +// EqualTo returns true if the config is equal to the given config. +func (c AWSSEVSNP) EqualTo(other AttestationCfg) (bool, error) { + otherCfg, ok := other.(*AWSSEVSNP) + if !ok { + return false, fmt.Errorf("cannot compare %T with %T", c, other) + } + + return c.Measurements.EqualTo(otherCfg.Measurements), nil +} + +// GetVariant returns aws-nitro-tpm as the variant. +func (AWSNitroTPM) GetVariant() variant.Variant { + return variant.AWSNitroTPM{} +} + +// GetMeasurements returns the measurements used for attestation. +func (c AWSNitroTPM) GetMeasurements() measurements.M { + return c.Measurements +} + +// SetMeasurements updates a config's measurements using the given measurements. +func (c *AWSNitroTPM) SetMeasurements(m measurements.M) { + c.Measurements = m +} + +// EqualTo returns true if the config is equal to the given config. +func (c AWSNitroTPM) EqualTo(other AttestationCfg) (bool, error) { + otherCfg, ok := other.(*AWSNitroTPM) + 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/azure.go b/internal/config/azure.go index 473dcd5cfe..00ad189ef9 100644 --- a/internal/config/azure.go +++ b/internal/config/azure.go @@ -30,8 +30,7 @@ func DefaultForAzureSEVSNP() *AzureSEVSNP { AcceptedKeyDigests: idkeydigest.DefaultList(), EnforcementPolicy: idkeydigest.MAAFallback, }, - // AMD root key. Received from the AMD Key Distribution System API (KDS). - AMDRootKey: mustParsePEM(`-----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`), + AMDRootKey: mustParsePEM(arkPEM), } } diff --git a/internal/config/config.go b/internal/config/config.go index 8c896583e9..aa3ea141c5 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -382,7 +382,7 @@ func Default() *Config { // AWS uses aws-nitro-tpm as attestation variant // AWS will have aws-sev-snp as attestation variant Attestation: AttestationConfig{ - AWSSEVSNP: &AWSSEVSNP{Measurements: measurements.DefaultsFor(cloudprovider.AWS, variant.AWSSEVSNP{})}, + AWSSEVSNP: DefaultForAWSSEVSNP(), AWSNitroTPM: &AWSNitroTPM{Measurements: measurements.DefaultsFor(cloudprovider.AWS, variant.AWSNitroTPM{})}, AzureSEVSNP: DefaultForAzureSEVSNP(), AzureTrustedLaunch: &AzureTrustedLaunch{Measurements: measurements.DefaultsFor(cloudprovider.Azure, variant.AzureTrustedLaunch{})}, @@ -915,80 +915,6 @@ func (c *Config) setCSPNodeGroupDefaults(csp cloudprovider.Provider) { } } -// AWSSEVSNP is the configuration for AWS SEV-SNP attestation. -type AWSSEVSNP struct { - // description: | - // Expected TPM measurements. - Measurements measurements.M `json:"measurements" yaml:"measurements" validate:"required,no_placeholders"` - // TODO (derpsteb): reenable launchMeasurement once SNP is fixed on AWS. - // description: | - // Expected launch measurement in SNP report. - // LaunchMeasurement measurements.Measurement `json:"launchMeasurement" yaml:"launchMeasurement" validate:"required"` -} - -// GetVariant returns aws-sev-snp as the variant. -func (AWSSEVSNP) GetVariant() variant.Variant { - return variant.AWSSEVSNP{} -} - -// GetMeasurements returns the measurements used for attestation. -func (c AWSSEVSNP) GetMeasurements() measurements.M { - return c.Measurements -} - -// SetMeasurements updates a config's measurements using the given measurements. -func (c *AWSSEVSNP) SetMeasurements(m measurements.M) { - c.Measurements = m -} - -// EqualTo returns true if the config is equal to the given config. -func (c AWSSEVSNP) EqualTo(other AttestationCfg) (bool, error) { - otherCfg, ok := other.(*AWSSEVSNP) - if !ok { - return false, fmt.Errorf("cannot compare %T with %T", c, other) - } - // TODO(derpsteb): reenable launchMeasurement once SNP is fixed on AWS. - // if !bytes.Equal(c.LaunchMeasurement.Expected, otherCfg.LaunchMeasurement.Expected) { - // return false, nil - // } - // if c.LaunchMeasurement.ValidationOpt != otherCfg.LaunchMeasurement.ValidationOpt { - // return false, nil - // } - - return c.Measurements.EqualTo(otherCfg.Measurements), nil -} - -// AWSNitroTPM is the configuration for AWS Nitro TPM attestation. -type AWSNitroTPM struct { - // description: | - // Expected TPM measurements. - Measurements measurements.M `json:"measurements" yaml:"measurements" validate:"required,no_placeholders"` -} - -// GetVariant returns aws-nitro-tpm as the variant. -func (AWSNitroTPM) GetVariant() variant.Variant { - return variant.AWSNitroTPM{} -} - -// GetMeasurements returns the measurements used for attestation. -func (c AWSNitroTPM) GetMeasurements() measurements.M { - return c.Measurements -} - -// SetMeasurements updates a config's measurements using the given measurements. -func (c *AWSNitroTPM) SetMeasurements(m measurements.M) { - c.Measurements = m -} - -// EqualTo returns true if the config is equal to the given config. -func (c AWSNitroTPM) EqualTo(other AttestationCfg) (bool, error) { - otherCfg, ok := other.(*AWSNitroTPM) - if !ok { - return false, fmt.Errorf("cannot compare %T with %T", c, other) - } - return c.Measurements.EqualTo(otherCfg.Measurements), nil -} - // SNPFirmwareSignerConfig is the configuration for validating the firmware signer. type SNPFirmwareSignerConfig struct { // description: | @@ -1104,6 +1030,38 @@ func (c QEMUTDX) EqualTo(other AttestationCfg) (bool, error) { return c.Measurements.EqualTo(otherCfg.Measurements), nil } +// AWSSEVSNP is the configuration for AWS SEV-SNP attestation. +type AWSSEVSNP struct { + // description: | + // Expected TPM measurements. + Measurements measurements.M `json:"measurements" yaml:"measurements" validate:"required,no_placeholders"` + // description: | + // Lowest acceptable bootloader version. + BootloaderVersion AttestationVersion `json:"bootloaderVersion" yaml:"bootloaderVersion"` + // description: | + // Lowest acceptable TEE version. + TEEVersion AttestationVersion `json:"teeVersion" yaml:"teeVersion"` + // description: | + // Lowest acceptable SEV-SNP version. + SNPVersion AttestationVersion `json:"snpVersion" yaml:"snpVersion"` + // description: | + // Lowest acceptable microcode version. + MicrocodeVersion AttestationVersion `json:"microcodeVersion" yaml:"microcodeVersion"` + // description: | + // AMD Root Key certificate used to verify the SEV-SNP certificate chain. + AMDRootKey Certificate `json:"amdRootKey" yaml:"amdRootKey"` + // description: | + // AMD Signing Key certificate used to verify the SEV-SNP VCEK / VLEK certificate. + AMDSigningKey Certificate `json:"amdSigningKey,omitempty" yaml:"amdSigningKey,omitempty" validate:"len=0"` +} + +// AWSNitroTPM is the configuration for AWS Nitro TPM attestation. +type AWSNitroTPM struct { + // description: | + // Expected TPM measurements. + Measurements measurements.M `json:"measurements" yaml:"measurements" validate:"required,no_placeholders"` +} + // AzureSEVSNP is the configuration for Azure SEV-SNP attestation. type AzureSEVSNP struct { // description: | diff --git a/internal/config/config_doc.go b/internal/config/config_doc.go index 31ca913a35..c82bcdee09 100644 --- a/internal/config/config_doc.go +++ b/internal/config/config_doc.go @@ -21,12 +21,12 @@ var ( AttestationConfigDoc encoder.Doc NodeGroupDoc encoder.Doc UnsupportedAppRegistrationErrorDoc encoder.Doc - AWSSEVSNPDoc encoder.Doc - AWSNitroTPMDoc encoder.Doc SNPFirmwareSignerConfigDoc encoder.Doc GCPSEVESDoc encoder.Doc QEMUVTPMDoc encoder.Doc QEMUTDXDoc encoder.Doc + AWSSEVSNPDoc encoder.Doc + AWSNitroTPMDoc encoder.Doc AzureSEVSNPDoc encoder.Doc AzureTrustedLaunchDoc encoder.Doc ) @@ -480,38 +480,6 @@ func init() { UnsupportedAppRegistrationErrorDoc.Description = "UnsupportedAppRegistrationError is returned when the config contains configuration related to now unsupported app registrations." UnsupportedAppRegistrationErrorDoc.Fields = make([]encoder.Doc, 0) - AWSSEVSNPDoc.Type = "AWSSEVSNP" - AWSSEVSNPDoc.Comments[encoder.LineComment] = "AWSSEVSNP is the configuration for AWS SEV-SNP attestation." - AWSSEVSNPDoc.Description = "AWSSEVSNP is the configuration for AWS SEV-SNP attestation." - AWSSEVSNPDoc.AppearsIn = []encoder.Appearance{ - { - TypeName: "AttestationConfig", - FieldName: "awsSEVSNP", - }, - } - AWSSEVSNPDoc.Fields = make([]encoder.Doc, 1) - AWSSEVSNPDoc.Fields[0].Name = "measurements" - AWSSEVSNPDoc.Fields[0].Type = "M" - AWSSEVSNPDoc.Fields[0].Note = "" - AWSSEVSNPDoc.Fields[0].Description = "Expected TPM measurements." - AWSSEVSNPDoc.Fields[0].Comments[encoder.LineComment] = "Expected TPM measurements." - - AWSNitroTPMDoc.Type = "AWSNitroTPM" - AWSNitroTPMDoc.Comments[encoder.LineComment] = "AWSNitroTPM is the configuration for AWS Nitro TPM attestation." - AWSNitroTPMDoc.Description = "AWSNitroTPM is the configuration for AWS Nitro TPM attestation." - AWSNitroTPMDoc.AppearsIn = []encoder.Appearance{ - { - TypeName: "AttestationConfig", - FieldName: "awsNitroTPM", - }, - } - AWSNitroTPMDoc.Fields = make([]encoder.Doc, 1) - AWSNitroTPMDoc.Fields[0].Name = "measurements" - AWSNitroTPMDoc.Fields[0].Type = "M" - AWSNitroTPMDoc.Fields[0].Note = "" - AWSNitroTPMDoc.Fields[0].Description = "Expected TPM measurements." - AWSNitroTPMDoc.Fields[0].Comments[encoder.LineComment] = "Expected TPM measurements." - SNPFirmwareSignerConfigDoc.Type = "SNPFirmwareSignerConfig" SNPFirmwareSignerConfigDoc.Comments[encoder.LineComment] = "SNPFirmwareSignerConfig is the configuration for validating the firmware signer." SNPFirmwareSignerConfigDoc.Description = "SNPFirmwareSignerConfig is the configuration for validating the firmware signer." @@ -586,6 +554,68 @@ func init() { QEMUTDXDoc.Fields[0].Description = "Expected TDX measurements." QEMUTDXDoc.Fields[0].Comments[encoder.LineComment] = "Expected TDX measurements." + AWSSEVSNPDoc.Type = "AWSSEVSNP" + AWSSEVSNPDoc.Comments[encoder.LineComment] = "AWSSEVSNP is the configuration for AWS SEV-SNP attestation." + AWSSEVSNPDoc.Description = "AWSSEVSNP is the configuration for AWS SEV-SNP attestation." + AWSSEVSNPDoc.AppearsIn = []encoder.Appearance{ + { + TypeName: "AttestationConfig", + FieldName: "awsSEVSNP", + }, + } + AWSSEVSNPDoc.Fields = make([]encoder.Doc, 7) + AWSSEVSNPDoc.Fields[0].Name = "measurements" + AWSSEVSNPDoc.Fields[0].Type = "M" + AWSSEVSNPDoc.Fields[0].Note = "" + AWSSEVSNPDoc.Fields[0].Description = "Expected TPM measurements." + AWSSEVSNPDoc.Fields[0].Comments[encoder.LineComment] = "Expected TPM measurements." + AWSSEVSNPDoc.Fields[1].Name = "bootloaderVersion" + AWSSEVSNPDoc.Fields[1].Type = "AttestationVersion" + AWSSEVSNPDoc.Fields[1].Note = "" + AWSSEVSNPDoc.Fields[1].Description = "Lowest acceptable bootloader version." + AWSSEVSNPDoc.Fields[1].Comments[encoder.LineComment] = "Lowest acceptable bootloader version." + AWSSEVSNPDoc.Fields[2].Name = "teeVersion" + AWSSEVSNPDoc.Fields[2].Type = "AttestationVersion" + AWSSEVSNPDoc.Fields[2].Note = "" + AWSSEVSNPDoc.Fields[2].Description = "Lowest acceptable TEE version." + AWSSEVSNPDoc.Fields[2].Comments[encoder.LineComment] = "Lowest acceptable TEE version." + AWSSEVSNPDoc.Fields[3].Name = "snpVersion" + AWSSEVSNPDoc.Fields[3].Type = "AttestationVersion" + AWSSEVSNPDoc.Fields[3].Note = "" + AWSSEVSNPDoc.Fields[3].Description = "Lowest acceptable SEV-SNP version." + AWSSEVSNPDoc.Fields[3].Comments[encoder.LineComment] = "Lowest acceptable SEV-SNP version." + AWSSEVSNPDoc.Fields[4].Name = "microcodeVersion" + AWSSEVSNPDoc.Fields[4].Type = "AttestationVersion" + AWSSEVSNPDoc.Fields[4].Note = "" + AWSSEVSNPDoc.Fields[4].Description = "Lowest acceptable microcode version." + AWSSEVSNPDoc.Fields[4].Comments[encoder.LineComment] = "Lowest acceptable microcode version." + AWSSEVSNPDoc.Fields[5].Name = "amdRootKey" + AWSSEVSNPDoc.Fields[5].Type = "Certificate" + AWSSEVSNPDoc.Fields[5].Note = "" + AWSSEVSNPDoc.Fields[5].Description = "AMD Root Key certificate used to verify the SEV-SNP certificate chain." + AWSSEVSNPDoc.Fields[5].Comments[encoder.LineComment] = "AMD Root Key certificate used to verify the SEV-SNP certificate chain." + AWSSEVSNPDoc.Fields[6].Name = "amdSigningKey" + AWSSEVSNPDoc.Fields[6].Type = "Certificate" + AWSSEVSNPDoc.Fields[6].Note = "" + AWSSEVSNPDoc.Fields[6].Description = "AMD Signing Key certificate used to verify the SEV-SNP VCEK / VLEK certificate." + AWSSEVSNPDoc.Fields[6].Comments[encoder.LineComment] = "AMD Signing Key certificate used to verify the SEV-SNP VCEK / VLEK certificate." + + AWSNitroTPMDoc.Type = "AWSNitroTPM" + AWSNitroTPMDoc.Comments[encoder.LineComment] = "AWSNitroTPM is the configuration for AWS Nitro TPM attestation." + AWSNitroTPMDoc.Description = "AWSNitroTPM is the configuration for AWS Nitro TPM attestation." + AWSNitroTPMDoc.AppearsIn = []encoder.Appearance{ + { + TypeName: "AttestationConfig", + FieldName: "awsNitroTPM", + }, + } + AWSNitroTPMDoc.Fields = make([]encoder.Doc, 1) + AWSNitroTPMDoc.Fields[0].Name = "measurements" + AWSNitroTPMDoc.Fields[0].Type = "M" + AWSNitroTPMDoc.Fields[0].Note = "" + AWSNitroTPMDoc.Fields[0].Description = "Expected TPM measurements." + AWSNitroTPMDoc.Fields[0].Comments[encoder.LineComment] = "Expected TPM measurements." + AzureSEVSNPDoc.Type = "AzureSEVSNP" AzureSEVSNPDoc.Comments[encoder.LineComment] = "AzureSEVSNP is the configuration for Azure SEV-SNP attestation." AzureSEVSNPDoc.Description = "AzureSEVSNP is the configuration for Azure SEV-SNP attestation." @@ -694,14 +724,6 @@ func (_ UnsupportedAppRegistrationError) Doc() *encoder.Doc { return &UnsupportedAppRegistrationErrorDoc } -func (_ AWSSEVSNP) Doc() *encoder.Doc { - return &AWSSEVSNPDoc -} - -func (_ AWSNitroTPM) Doc() *encoder.Doc { - return &AWSNitroTPMDoc -} - func (_ SNPFirmwareSignerConfig) Doc() *encoder.Doc { return &SNPFirmwareSignerConfigDoc } @@ -718,6 +740,14 @@ func (_ QEMUTDX) Doc() *encoder.Doc { return &QEMUTDXDoc } +func (_ AWSSEVSNP) Doc() *encoder.Doc { + return &AWSSEVSNPDoc +} + +func (_ AWSNitroTPM) Doc() *encoder.Doc { + return &AWSNitroTPMDoc +} + func (_ AzureSEVSNP) Doc() *encoder.Doc { return &AzureSEVSNPDoc } @@ -742,12 +772,12 @@ func GetConfigurationDoc() *encoder.FileDoc { &AttestationConfigDoc, &NodeGroupDoc, &UnsupportedAppRegistrationErrorDoc, - &AWSSEVSNPDoc, - &AWSNitroTPMDoc, &SNPFirmwareSignerConfigDoc, &GCPSEVESDoc, &QEMUVTPMDoc, &QEMUTDXDoc, + &AWSSEVSNPDoc, + &AWSNitroTPMDoc, &AzureSEVSNPDoc, &AzureTrustedLaunchDoc, }, diff --git a/joinservice/internal/certcache/certcache.go b/joinservice/internal/certcache/certcache.go index bffa596e98..fa14ca2054 100644 --- a/joinservice/internal/certcache/certcache.go +++ b/joinservice/internal/certcache/certcache.go @@ -46,22 +46,26 @@ func NewClient(log *logger.Logger, kubeClient kubeClient, attVariant variant.Var // and returns the cached certificates, if applicable. // If the certificate chain cache already exists, nothing is done. func (c *Client) CreateCertChainCache(ctx context.Context) (*CachedCerts, error) { + var reportSigner abi.ReportSigner switch c.attVariant { case variant.AzureSEVSNP{}: - c.log.Debugf("Creating Azure SEV-SNP certificate chain cache") - ask, ark, err := c.createCertChainCache(ctx, abi.VcekReportSigner) - if err != nil { - return nil, fmt.Errorf("creating Azure SEV-SNP certificate chain cache: %w", err) - } - return &CachedCerts{ - ask: ask, - ark: ark, - }, nil - // TODO(derpsteb): Add AWS + reportSigner = abi.VcekReportSigner + case variant.AWSSEVSNP{}: + reportSigner = abi.VlekReportSigner default: c.log.Debugf("No certificate chain caching possible for attestation variant %s", c.attVariant) return nil, nil } + + c.log.Debugf("Creating %s certificate chain cache", c.attVariant) + ask, ark, err := c.createCertChainCache(ctx, reportSigner) + if err != nil { + return nil, fmt.Errorf("creating %s certificate chain cache: %w", c.attVariant, err) + } + return &CachedCerts{ + ask: ask, + ark: ark, + }, nil } // CachedCerts contains the cached certificates. diff --git a/joinservice/internal/watcher/validator.go b/joinservice/internal/watcher/validator.go index 1cb2041746..65fc9dd76b 100644 --- a/joinservice/internal/watcher/validator.go +++ b/joinservice/internal/watcher/validator.go @@ -105,8 +105,14 @@ func (u *Updatable) configWithCerts(cfg config.AttestationCfg) (config.Attestati } c.AMDSigningKey = config.Certificate(ask) return c, nil + case *config.AWSSEVSNP: + ask, err := u.getCachedAskCert() + if err != nil { + return nil, fmt.Errorf("getting cached ASK certificate: %w", err) + } + c.AMDSigningKey = config.Certificate(ask) + return c, nil } - // TODO(derpsteb): Add AWS SEV-SNP return cfg, nil } From 8738bc5874aebd8e68a2c82c56f954eade802060 Mon Sep 17 00:00:00 2001 From: Otto Bittner Date: Tue, 7 Nov 2023 12:17:08 +0100 Subject: [PATCH 05/12] verify: query vlek ASK from KDS if not set The user can choose to supply an intermediate certificate through the config, like they can for the root key. If none is supplied, the KDS is queried for a valid ASK. --- cli/internal/cmd/verify.go | 22 +++-- cli/internal/cmd/verify_test.go | 4 +- internal/attestation/snp/snp.go | 13 ++- internal/config/attestation.go | 6 ++ internal/config/config.go | 2 +- internal/verify/BUILD.bazel | 9 +- internal/verify/certchain.go | 29 ------- internal/verify/verify.go | 149 ++++++++++++++++++++++---------- 8 files changed, 132 insertions(+), 102 deletions(-) delete mode 100644 internal/verify/certchain.go diff --git a/cli/internal/cmd/verify.go b/cli/internal/cmd/verify.go index 59d3bd5708..8c0b465e03 100644 --- a/cli/internal/cmd/verify.go +++ b/cli/internal/cmd/verify.go @@ -105,8 +105,8 @@ func runVerify(cmd *cobra.Command, _ []string) error { log: log, } formatterFactory := func(output string, provider cloudprovider.Provider, log debugLog) (attestationDocFormatter, error) { - if output == "json" && provider != cloudprovider.Azure { - return nil, errors.New("json output is only supported for Azure") + if output == "json" && (provider != cloudprovider.Azure && provider != cloudprovider.AWS) { + return nil, errors.New("json output is only supported for Azure and AWS") } switch output { case "json": @@ -206,8 +206,7 @@ func (c *verifyCmd) verify(cmd *cobra.Command, verifyClient verifyClient, factor cmd.Context(), rawAttestationDoc, (conf.Provider.Azure == nil && conf.Provider.AWS == nil), - attConfig.GetMeasurements(), - maaURL, + attConfig, ) if err != nil { return fmt.Errorf("printing attestation document: %w", err) @@ -254,8 +253,7 @@ func (c *verifyCmd) validateEndpointFlag(cmd *cobra.Command, stateFile *state.St // an attestationDocFormatter formats the attestation document. type attestationDocFormatter interface { // format returns the raw or formatted attestation doc depending on the rawOutput argument. - format(ctx context.Context, docString string, PCRsOnly bool, expectedPCRs measurements.M, - attestationServiceURL string) (string, error) + format(ctx context.Context, docString string, PCRsOnly bool, attestationCfg config.AttestationCfg) (string, error) } type jsonAttestationDocFormatter struct { @@ -264,7 +262,7 @@ type jsonAttestationDocFormatter struct { // format returns the json formatted attestation doc. func (f *jsonAttestationDocFormatter) format(ctx context.Context, docString string, _ bool, - _ measurements.M, attestationServiceURL string, + attestationCfg config.AttestationCfg, ) (string, error) { var doc attestationDoc if err := json.Unmarshal([]byte(docString), &doc); err != nil { @@ -275,7 +273,7 @@ func (f *jsonAttestationDocFormatter) format(ctx context.Context, docString stri if err != nil { return "", fmt.Errorf("unmarshalling instance info: %w", err) } - report, err := verify.NewReport(ctx, instanceInfo, attestationServiceURL, f.log) + report, err := verify.NewReport(ctx, instanceInfo, attestationCfg, f.log) if err != nil { return "", fmt.Errorf("parsing SNP report: %w", err) } @@ -291,7 +289,7 @@ type rawAttestationDocFormatter struct { // format returns the raw attestation doc. func (f *rawAttestationDocFormatter) format(_ context.Context, docString string, _ bool, - _ measurements.M, _ string, + _ config.AttestationCfg, ) (string, error) { b := &strings.Builder{} b.WriteString("Attestation Document:\n") @@ -305,7 +303,7 @@ type defaultAttestationDocFormatter struct { // format returns the formatted attestation doc. func (f *defaultAttestationDocFormatter) format(ctx context.Context, docString string, PCRsOnly bool, - expectedPCRs measurements.M, attestationServiceURL string, + attestationCfg config.AttestationCfg, ) (string, error) { b := &strings.Builder{} b.WriteString("Attestation Document:\n") @@ -315,7 +313,7 @@ func (f *defaultAttestationDocFormatter) format(ctx context.Context, docString s return "", fmt.Errorf("unmarshal attestation document: %w", err) } - if err := f.parseQuotes(b, doc.Attestation.Quotes, expectedPCRs); err != nil { + if err := f.parseQuotes(b, doc.Attestation.Quotes, attestationCfg.GetMeasurements()); err != nil { return "", fmt.Errorf("parse quote: %w", err) } if PCRsOnly { @@ -327,7 +325,7 @@ func (f *defaultAttestationDocFormatter) format(ctx context.Context, docString s return "", fmt.Errorf("unmarshalling instance info: %w", err) } - report, err := verify.NewReport(ctx, instanceInfo, attestationServiceURL, f.log) + report, err := verify.NewReport(ctx, instanceInfo, attestationCfg, f.log) if err != nil { return "", fmt.Errorf("parsing SNP report: %w", err) } diff --git a/cli/internal/cmd/verify_test.go b/cli/internal/cmd/verify_test.go index a874125c31..7896eb567a 100644 --- a/cli/internal/cmd/verify_test.go +++ b/cli/internal/cmd/verify_test.go @@ -233,7 +233,7 @@ type stubAttDocFormatter struct { formatErr error } -func (f *stubAttDocFormatter) format(_ context.Context, _ string, _ bool, _ measurements.M, _ string) (string, error) { +func (f *stubAttDocFormatter) format(_ context.Context, _ string, _ bool, _ config.AttestationCfg) (string, error) { return "", f.formatErr } @@ -258,7 +258,7 @@ func TestFormat(t *testing.T) { for name, tc := range testCases { t.Run(name, func(t *testing.T) { - _, err := tc.formatter.format(context.Background(), tc.doc, false, nil, "") + _, err := tc.formatter.format(context.Background(), tc.doc, false, nil) if tc.wantErr { assert.Error(t, err) } else { diff --git a/internal/attestation/snp/snp.go b/internal/attestation/snp/snp.go index 79e9866c56..e7b285ba61 100644 --- a/internal/attestation/snp/snp.go +++ b/internal/attestation/snp/snp.go @@ -22,6 +22,13 @@ import ( "github.com/google/go-sev-guest/verify/trust" ) +// Product returns the SEV product info currently supported by Constellation's SNP attestation. +func Product() *spb.SevProduct { + // sevProduct is the product info of the SEV platform as reported through CPUID[EAX=1]. + // It may become necessary in the future to differentiate among CSP vendors. + return &spb.SevProduct{Name: spb.SevProduct_SEV_PRODUCT_MILAN, Stepping: 0} // Milan-B0 +} + // InstanceInfo contains the necessary information to establish trust in a SNP CVM. type InstanceInfo struct { // ReportSigner is the PEM-encoded certificate used to validate the attestation report's signature. @@ -99,14 +106,12 @@ func (a *InstanceInfo) AttestationWithCerts(getter trust.HTTPSGetter, 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) + productName := kds.ProductString(Product()) att := &spb.Attestation{ Report: report, CertificateChain: &spb.CertificateChain{}, - Product: sevProduct, + Product: Product(), } // Add VCEK/VLEK to attestation object. diff --git a/internal/config/attestation.go b/internal/config/attestation.go index 4e05c33b30..a80d069db8 100644 --- a/internal/config/attestation.go +++ b/internal/config/attestation.go @@ -7,6 +7,7 @@ SPDX-License-Identifier: AGPL-3.0-only package config import ( + "bytes" "crypto/x509" "encoding/json" "encoding/pem" @@ -67,6 +68,11 @@ func unmarshalTypedConfig[T AttestationCfg](data []byte) (AttestationCfg, error) // Certificate is a wrapper around x509.Certificate allowing custom marshaling. type Certificate x509.Certificate +// Equal returns true if the certificates are equal. +func (c Certificate) Equal(other Certificate) bool { + return bytes.Equal(c.Raw, other.Raw) +} + // MarshalJSON marshals the certificate to PEM. func (c Certificate) MarshalJSON() ([]byte, error) { if len(c.Raw) == 0 { diff --git a/internal/config/config.go b/internal/config/config.go index aa3ea141c5..976bd86b17 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -1052,7 +1052,7 @@ type AWSSEVSNP struct { AMDRootKey Certificate `json:"amdRootKey" yaml:"amdRootKey"` // description: | // AMD Signing Key certificate used to verify the SEV-SNP VCEK / VLEK certificate. - AMDSigningKey Certificate `json:"amdSigningKey,omitempty" yaml:"amdSigningKey,omitempty" validate:"len=0"` + AMDSigningKey Certificate `json:"amdSigningKey,omitempty" yaml:"amdSigningKey,omitempty"` } // AWSNitroTPM is the configuration for AWS Nitro TPM attestation. diff --git a/internal/verify/BUILD.bazel b/internal/verify/BUILD.bazel index c52589085a..c8927a51bf 100644 --- a/internal/verify/BUILD.bazel +++ b/internal/verify/BUILD.bazel @@ -3,19 +3,16 @@ load("//bazel/go:go_test.bzl", "go_test") go_library( name = "verify", - srcs = [ - "certchain.go", - "verify.go", - ], + srcs = ["verify.go"], importpath = "github.com/edgelesssys/constellation/v2/internal/verify", visibility = ["//:__subpackages__"], deps = [ "//internal/attestation/snp", - "//internal/constants", - "//internal/kubernetes/kubectl", + "//internal/config", "@com_github_golang_jwt_jwt_v5//:jwt", "@com_github_google_go_sev_guest//abi", "@com_github_google_go_sev_guest//kds", + "@com_github_google_go_sev_guest//verify/trust", ], ) diff --git a/internal/verify/certchain.go b/internal/verify/certchain.go deleted file mode 100644 index c3629032f9..0000000000 --- a/internal/verify/certchain.go +++ /dev/null @@ -1,29 +0,0 @@ -package verify - -import ( - "context" - "fmt" - - "github.com/edgelesssys/constellation/v2/internal/constants" - "github.com/edgelesssys/constellation/v2/internal/kubernetes/kubectl" -) - -func getCertChainCache(ctx context.Context, kubectl *kubectl.Kubectl, log debugLog) ([]byte, error) { - log.Debugf("Retrieving certificate chain from cache") - cm, err := kubectl.GetConfigMap(ctx, constants.ConstellationNamespace, constants.SevSnpCertCacheConfigMapName) - if err != nil { - return nil, fmt.Errorf("getting certificate chain cache configmap: %w", err) - } - - var result []byte - ask, ok := cm.Data[constants.CertCacheAskKey] - if ok { - result = append(result, ask...) - } - ark, ok := cm.Data[constants.CertCacheArkKey] - if ok { - result = append(result, ark...) - } - - return result, nil -} diff --git a/internal/verify/verify.go b/internal/verify/verify.go index b1e87aad82..fa41079f02 100644 --- a/internal/verify/verify.go +++ b/internal/verify/verify.go @@ -26,17 +26,23 @@ import ( "strings" "github.com/edgelesssys/constellation/v2/internal/attestation/snp" - "github.com/edgelesssys/constellation/v2/internal/constants" - "github.com/edgelesssys/constellation/v2/internal/kubernetes/kubectl" + "github.com/edgelesssys/constellation/v2/internal/config" "github.com/golang-jwt/jwt/v5" "github.com/google/go-sev-guest/abi" "github.com/google/go-sev-guest/kds" + "github.com/google/go-sev-guest/verify/trust" +) + +const ( + vcekCert = "VCEK certificate" + vlekCert = "VLEK certificate" + certificateChain = "certificate chain" ) // Report contains the entire data reported by constellation verify. type Report struct { SNPReport SNPReport `json:"snp_report"` - ReportSigner []Certificate `json:"vcek"` + ReportSigner []Certificate `json:"report_signer"` CertChain []Certificate `json:"cert_chain"` *AzureReportAddition `json:"azure,omitempty"` *AWSReportAddition `json:"aws,omitempty"` @@ -51,7 +57,7 @@ type AzureReportAddition struct { type AWSReportAddition struct{} // NewReport transforms a snp.InstanceInfo object into a Report. -func NewReport(ctx context.Context, instanceInfo snp.InstanceInfo, attestationServiceURL string, log debugLog) (Report, error) { +func NewReport(ctx context.Context, instanceInfo snp.InstanceInfo, attestationCfg config.AttestationCfg, log debugLog) (Report, error) { snpReport, err := newSNPReport(instanceInfo.AttestationReport) if err != nil { return Report{}, fmt.Errorf("parsing SNP report: %w", err) @@ -60,34 +66,28 @@ func NewReport(ctx context.Context, instanceInfo snp.InstanceInfo, attestationSe var certTypeName string switch snpReport.SignerInfo.SigningKey { case abi.VlekReportSigner.String(): - certTypeName = "VLEK certificate" + certTypeName = vlekCert case abi.VcekReportSigner.String(): - certTypeName = "VCEK certificate" + certTypeName = vcekCert default: return Report{}, errors.New("unknown report signer") } reportSigner, err := newCertificates(certTypeName, instanceInfo.ReportSigner, log) if err != nil { - return Report{}, fmt.Errorf("parsing VCEK certificate: %w", err) + return Report{}, fmt.Errorf("parsing %s: %w", certTypeName, err) } // check if issuer included certChain before parsing. If not included, manually collect from the cluster. - var pemCerts []byte - if instanceInfo.CertChain == nil { - client, err := kubectl.NewFromConfig(constants.AdminConfFilename) - if err != nil { - return Report{}, fmt.Errorf("creating kubectl client: %w", err) - } - pemCerts, err = getCertChainCache(ctx, client, log) + rawCerts := instanceInfo.CertChain + if certTypeName == vlekCert { + rawCerts, err = getCertChain(attestationCfg) if err != nil { return Report{}, fmt.Errorf("getting certificate chain cache: %w", err) } - } else { - pemCerts = instanceInfo.CertChain } - certChain, err := newCertificates("Certificate chain", pemCerts, log) + certChain, err := newCertificates(certificateChain, rawCerts, log) if err != nil { return Report{}, fmt.Errorf("parsing certificate chain: %w", err) } @@ -95,7 +95,11 @@ func NewReport(ctx context.Context, instanceInfo snp.InstanceInfo, attestationSe var azure *AzureReportAddition var aws *AWSReportAddition if instanceInfo.Azure != nil { - maaToken, err := newMAAToken(ctx, instanceInfo.Azure.MAAToken, attestationServiceURL) + cfg, ok := attestationCfg.(*config.AzureSEVSNP) + if !ok { + return Report{}, fmt.Errorf("expected config type *config.AzureSEVSNP, got %T", attestationCfg) + } + maaToken, err := newMAAToken(ctx, instanceInfo.Azure.MAAToken, cfg.FirmwareSignerConfig.MAAURL) if err != nil { return Report{}, fmt.Errorf("parsing MAA token: %w", err) } @@ -113,6 +117,46 @@ func NewReport(ctx context.Context, instanceInfo snp.InstanceInfo, attestationSe }, nil } +// inverse of newCertificates. +// ideally, duplicate encoding/decoding would be removed. +// AWS specific. +func getCertChain(cfg config.AttestationCfg) ([]byte, error) { + awsCfg, ok := cfg.(*config.AWSSEVSNP) + if !ok { + return nil, fmt.Errorf("expected config type *config.AWSSEVSNP, got %T", cfg) + } + + if awsCfg.AMDRootKey.Equal(config.Certificate{}) { + return nil, errors.New("no AMD root key configured") + } + + if awsCfg.AMDSigningKey.Equal(config.Certificate{}) { + certs, err := trust.GetProductChain(kds.ProductString(snp.Product()), abi.VlekReportSigner, trust.DefaultHTTPSGetter()) + if err != nil { + return nil, fmt.Errorf("getting product certificate chain: %w", err) + } + // we want an ASVK, but GetProductChain currently does not use the ASVK field. + if certs.Ask == nil { + return nil, errors.New("no ASVK certificate available") + } + awsCfg.AMDSigningKey = config.Certificate(*certs.Ask) + } + + // ARK + certChain := pem.EncodeToMemory(&pem.Block{ + Type: "CERTIFICATE", + Bytes: awsCfg.AMDRootKey.Raw, + }) + + // append ASK + certChain = append(certChain, pem.EncodeToMemory(&pem.Block{ + Type: "CERTIFICATE", + Bytes: awsCfg.AMDSigningKey.Raw, + })...) + + return certChain, nil +} + // FormatString builds a string representation of a report that is inteded for console output. func (r *Report) FormatString(b *strings.Builder) (string, error) { if len(r.ReportSigner) != 1 { @@ -163,7 +207,8 @@ type Certificate struct { CertTypeName string `json:"cert_type_name"` StructVersion uint8 `json:"struct_version"` ProductName string `json:"product_name"` - HardwareID []byte `json:"hardware_id"` + HardwareID []byte `json:"hardware_id,omitempty"` + CspID string `json:"csp_id,omitempty"` TCBVersion TCBVersion `json:"tcb_version"` } @@ -181,39 +226,47 @@ func newCertificates(certTypeName string, cert []byte, log debugLog) (certs []Ce return certs, fmt.Errorf("parse %s: expected PEM block type 'CERTIFICATE', got '%s'", certTypeName, block.Type) } - cert, err := x509.ParseCertificate(block.Bytes) + certX509, err := x509.ParseCertificate(block.Bytes) if err != nil { return certs, fmt.Errorf("parse %s: %w", certTypeName, err) } - if certTypeName == "VCEK certificate" { - vcekExts, err := kds.VcekCertificateExtensions(cert) + + var ext *kds.Extensions + switch certTypeName { + case vcekCert: + ext, err = kds.VcekCertificateExtensions(certX509) + if err != nil { + return certs, fmt.Errorf("parsing %s extensions: %w", certTypeName, err) + } + case vlekCert: + ext, err = kds.VlekCertificateExtensions(certX509) if err != nil { - return certs, fmt.Errorf("parsing VCEK certificate extensions: %w", err) + return certs, fmt.Errorf("parsing %s extensions: %w", certTypeName, err) } - certPEM := pem.EncodeToMemory(&pem.Block{ - Type: "CERTIFICATE", - Bytes: cert.Raw, - }) - certs = append(certs, Certificate{ - Certificate: *cert, - CertificatePEM: string(certPEM), - CertTypeName: certTypeName, - StructVersion: vcekExts.StructVersion, - ProductName: vcekExts.ProductName, - TCBVersion: newTCBVersion(vcekExts.TCBVersion), - HardwareID: vcekExts.HWID, - }) - } else { - certPEM := pem.EncodeToMemory(&pem.Block{ - Type: "CERTIFICATE", - Bytes: cert.Raw, - }) - certs = append(certs, Certificate{ - Certificate: *cert, - CertificatePEM: string(certPEM), - CertTypeName: certTypeName, - }) } + certPEM := pem.EncodeToMemory(&pem.Block{ + Type: "CERTIFICATE", + Bytes: certX509.Raw, + }) + cert := Certificate{ + Certificate: *certX509, + CertificatePEM: string(certPEM), + CertTypeName: certTypeName, + } + + if ext != nil { + cert.StructVersion = ext.StructVersion + cert.ProductName = ext.ProductName + cert.TCBVersion = newTCBVersion(ext.TCBVersion) + if ext.HWID != nil { + cert.HardwareID = ext.HWID + } else { + cert.CspID = ext.CspID + } + } + + certs = append(certs, cert) + i++ } if i == 1 { @@ -236,12 +289,12 @@ func (c *Certificate) formatString(b *strings.Builder, idx int) error { writeIndentfln(b, 2, "Signature Algorithm: %s", c.Certificate.SignatureAlgorithm) writeIndentfln(b, 2, "Public Key Algorithm: %s", c.Certificate.PublicKeyAlgorithm) - if c.CertTypeName == "VCEK certificate" { + if c.CertTypeName == vcekCert { // Extensions documented in Table 8 and Table 9 of // https://www.amd.com/system/files/TechDocs/57230.pdf vcekExts, err := kds.VcekCertificateExtensions(&c.Certificate) if err != nil { - return fmt.Errorf("parsing VCEK certificate extensions: %w", err) + return fmt.Errorf("parsing %s extensions: %w", c.CertTypeName, err) } writeIndentfln(b, 2, "Struct version: %d", vcekExts.StructVersion) From 18eef9a0d7262cdbbd803fcdd95eabfba7db153c Mon Sep 17 00:00:00 2001 From: Otto Bittner Date: Thu, 9 Nov 2023 09:59:19 +0100 Subject: [PATCH 06/12] api: refactor attestationcfgapi cli The cli now takes CSP and object kind as argument. Also made upload an explicit command and the report path/version an argument. Previously the report was a flag. The CSP was hardcoded. There was only one object kind (snp-report). --- .github/actions/e2e_verify/action.yml | 2 +- .../api/attestationconfigapi/cli/BUILD.bazel | 7 +- internal/api/attestationconfigapi/cli/aws.go | 23 +++ .../api/attestationconfigapi/cli/azure.go | 104 +++++++++++ .../api/attestationconfigapi/cli/delete.go | 148 +++++++-------- .../attestationconfigapi/cli/delete_test.go | 32 ---- .../attestationconfigapi/cli/e2e/test.sh.in | 12 +- internal/api/attestationconfigapi/cli/main.go | 173 +++++++----------- .../api/attestationconfigapi/cli/main_test.go | 26 --- .../api/attestationconfigapi/cli/validargs.go | 53 ++++++ 10 files changed, 333 insertions(+), 247 deletions(-) create mode 100644 internal/api/attestationconfigapi/cli/aws.go create mode 100644 internal/api/attestationconfigapi/cli/azure.go create mode 100644 internal/api/attestationconfigapi/cli/validargs.go diff --git a/.github/actions/e2e_verify/action.yml b/.github/actions/e2e_verify/action.yml index 371d2932f0..c18938becb 100644 --- a/.github/actions/e2e_verify/action.yml +++ b/.github/actions/e2e_verify/action.yml @@ -93,5 +93,5 @@ runs: for file in $(ls snp-report-*.json); do path=$(realpath "${file}") cat "${path}" - bazel run //internal/api/attestationconfigapi/cli -- --snp-report-path "${path}" + bazel run //internal/api/attestationconfigapi/cli -- upload azure snp-report "${path}" done diff --git a/internal/api/attestationconfigapi/cli/BUILD.bazel b/internal/api/attestationconfigapi/cli/BUILD.bazel index a64c3fdbd2..ed7640a69f 100644 --- a/internal/api/attestationconfigapi/cli/BUILD.bazel +++ b/internal/api/attestationconfigapi/cli/BUILD.bazel @@ -11,13 +11,18 @@ go_binary( go_library( name = "cli_lib", srcs = [ + "aws.go", + "azure.go", "delete.go", "main.go", + "objectkind_string.go", + "validargs.go", ], importpath = "github.com/edgelesssys/constellation/v2/internal/api/attestationconfigapi/cli", visibility = ["//visibility:private"], deps = [ "//internal/api/attestationconfigapi", + "//internal/cloud/cloudprovider", "//internal/constants", "//internal/file", "//internal/logger", @@ -40,9 +45,9 @@ go_test( ], embed = [":cli_lib"], deps = [ + "//internal/cloud/cloudprovider", "//internal/verify", "@com_github_stretchr_testify//assert", - "@com_github_stretchr_testify//require", ], ) diff --git a/internal/api/attestationconfigapi/cli/aws.go b/internal/api/attestationconfigapi/cli/aws.go new file mode 100644 index 0000000000..ac7fe39102 --- /dev/null +++ b/internal/api/attestationconfigapi/cli/aws.go @@ -0,0 +1,23 @@ +/* +Copyright (c) Edgeless Systems GmbH + +SPDX-License-Identifier: AGPL-3.0-only +*/ + +package main + +import ( + "context" + + "github.com/edgelesssys/constellation/v2/internal/api/attestationconfigapi" + "github.com/edgelesssys/constellation/v2/internal/file" + "github.com/edgelesssys/constellation/v2/internal/logger" +) + +func uploadAWS(_ context.Context, _ *attestationconfigapi.Client, _ uploadConfig, _ file.Handler, _ *logger.Logger) error { + return nil +} + +func deleteAWS(_ context.Context, _ *attestationconfigapi.Client, _ deleteConfig) error { + return nil +} diff --git a/internal/api/attestationconfigapi/cli/azure.go b/internal/api/attestationconfigapi/cli/azure.go new file mode 100644 index 0000000000..93b363cb3a --- /dev/null +++ b/internal/api/attestationconfigapi/cli/azure.go @@ -0,0 +1,104 @@ +/* +Copyright (c) Edgeless Systems GmbH + +SPDX-License-Identifier: AGPL-3.0-only +*/ + +package main + +import ( + "context" + "errors" + "fmt" + + "github.com/aws/aws-sdk-go-v2/service/s3" + s3types "github.com/aws/aws-sdk-go-v2/service/s3/types" + "github.com/aws/aws-sdk-go/aws" + "github.com/edgelesssys/constellation/v2/internal/api/attestationconfigapi" + "github.com/edgelesssys/constellation/v2/internal/cloud/cloudprovider" + "github.com/edgelesssys/constellation/v2/internal/file" + "github.com/edgelesssys/constellation/v2/internal/logger" + "github.com/edgelesssys/constellation/v2/internal/staticupload" + "github.com/edgelesssys/constellation/v2/internal/verify" +) + +func uploadAzure(ctx context.Context, client *attestationconfigapi.Client, cfg uploadConfig, fs file.Handler, log *logger.Logger) error { + if cfg.kind != snpReport { + return fmt.Errorf("kind %s not supported", cfg.kind) + } + + log.Infof("Reading SNP report from file: %s", cfg.path) + var report verify.Report + if err := fs.ReadJSON(cfg.path, &report); err != nil { + return fmt.Errorf("reading snp report: %w", err) + } + + inputVersion := convertTCBVersionToAzureVersion(report.SNPReport.LaunchTCB) + log.Infof("Input report: %+v", inputVersion) + + latestAPIVersionAPI, err := attestationconfigapi.NewFetcherWithCustomCDNAndCosignKey(cfg.url, cfg.cosignPublicKey).FetchAzureSEVSNPVersionLatest(ctx) + if err != nil { + if errors.Is(err, attestationconfigapi.ErrNoVersionsFound) { + log.Infof("No versions found in API, but assuming that we are uploading the first version.") + } else { + return fmt.Errorf("fetching latest version: %w", err) + } + } + latestAPIVersion := latestAPIVersionAPI.AzureSEVSNPVersion + if err := client.UploadAzureSEVSNPVersionLatest(ctx, inputVersion, latestAPIVersion, cfg.uploadDate, cfg.force); err != nil { + if errors.Is(err, attestationconfigapi.ErrNoNewerVersion) { + log.Infof("Input version: %+v is not newer than latest API version: %+v", inputVersion, latestAPIVersion) + return nil + } + return fmt.Errorf("updating latest version: %w", err) + } + + return nil +} + +func convertTCBVersionToAzureVersion(tcb verify.TCBVersion) attestationconfigapi.AzureSEVSNPVersion { + return attestationconfigapi.AzureSEVSNPVersion{ + Bootloader: tcb.Bootloader, + TEE: tcb.TEE, + SNP: tcb.SNP, + Microcode: tcb.Microcode, + } +} + +func deleteAzure(ctx context.Context, client *attestationconfigapi.Client, cfg deleteConfig) error { + if cfg.provider == cloudprovider.Azure && cfg.kind == snpReport { + return client.DeleteAzureSEVSNPVersion(ctx, cfg.version) + } + + return fmt.Errorf("provider %s and kind %s not supported", cfg.provider, cfg.kind) +} + +func deleteRecursiveAzure(ctx context.Context, client *staticupload.Client, cfg deleteConfig) error { + path := "constellation/v1/attestation/azure-sev-snp" + resp, err := client.ListObjectsV2(ctx, &s3.ListObjectsV2Input{ + Bucket: aws.String(cfg.bucket), + Prefix: aws.String(path), + }) + if err != nil { + return err + } + + // Delete all objects in the path. + objIDs := make([]s3types.ObjectIdentifier, len(resp.Contents)) + for i, obj := range resp.Contents { + objIDs[i] = s3types.ObjectIdentifier{Key: obj.Key} + } + if len(objIDs) > 0 { + _, err = client.DeleteObjects(ctx, &s3.DeleteObjectsInput{ + Bucket: aws.String(cfg.bucket), + Delete: &s3types.Delete{ + Objects: objIDs, + Quiet: true, + }, + }) + if err != nil { + return err + } + } + return nil +} diff --git a/internal/api/attestationconfigapi/cli/delete.go b/internal/api/attestationconfigapi/cli/delete.go index 3622b0d984..749cc2bfb9 100644 --- a/internal/api/attestationconfigapi/cli/delete.go +++ b/internal/api/attestationconfigapi/cli/delete.go @@ -6,14 +6,11 @@ SPDX-License-Identifier: AGPL-3.0-only package main import ( - "context" "errors" "fmt" - "github.com/aws/aws-sdk-go-v2/service/s3" - s3types "github.com/aws/aws-sdk-go-v2/service/s3/types" - "github.com/aws/aws-sdk-go/aws" "github.com/edgelesssys/constellation/v2/internal/api/attestationconfigapi" + "github.com/edgelesssys/constellation/v2/internal/cloud/cloudprovider" "github.com/edgelesssys/constellation/v2/internal/logger" "github.com/edgelesssys/constellation/v2/internal/staticupload" "github.com/spf13/cobra" @@ -23,62 +20,83 @@ import ( // newDeleteCmd creates the delete command. func newDeleteCmd() *cobra.Command { cmd := &cobra.Command{ - Use: "delete", - Short: "delete a specific version from the config api", + Use: "delete {azure|aws} {snp-report|guest-firmware} ", + Short: "Upload an object to the attestationconfig API", + Long: "Delete a specific object version from the config api. is the name of the object to delete (without .json suffix)", + Args: cobra.MatchAll(cobra.ExactArgs(3), isCloudProvider(0), isValidKind(1)), PreRunE: envCheck, RunE: runDelete, } - cmd.Flags().StringP("version", "v", "", "Name of the version to delete (without .json suffix)") - must(cmd.MarkFlagRequired("version")) recursivelyCmd := &cobra.Command{ - Use: "recursive", - Short: "delete all objects from the API path", + Use: "recursive {azure|aws} {snp-report|guest-firmware}", + Short: "delete all objects from the API path constellation/v1/attestation/azure-sev-snp", + Long: "Currently only implemented for azure & snp-report. Delete all objects from the API path constellation/v1/attestation/azure-sev-snp", + Args: cobra.MatchAll(cobra.ExactArgs(2), isCloudProvider(0), isValidKind(1)), RunE: runRecursiveDelete, } - cmd.AddCommand(recursivelyCmd) - return cmd -} -type deleteCmd struct { - attestationClient deleteClient -} + cmd.AddCommand(recursivelyCmd) -type deleteClient interface { - DeleteAzureSEVSNPVersion(ctx context.Context, versionStr string) error + return cmd } -func (d deleteCmd) delete(cmd *cobra.Command) error { - version, err := cmd.Flags().GetString("version") - if err != nil { - return err - } - return d.attestationClient.DeleteAzureSEVSNPVersion(cmd.Context(), version) +type deleteConfig struct { + provider cloudprovider.Provider + kind objectKind + version string + region string + bucket string + url string + distribution string + cosignPublicKey string } -func runDelete(cmd *cobra.Command, _ []string) (retErr error) { - log := logger.New(logger.PlainLog, zap.DebugLevel).Named("attestationconfigapi") - +func newDeleteConfig(cmd *cobra.Command, args [3]string) (deleteConfig, error) { region, err := cmd.Flags().GetString("region") if err != nil { - return fmt.Errorf("getting region: %w", err) + return deleteConfig{}, fmt.Errorf("getting region: %w", err) } bucket, err := cmd.Flags().GetString("bucket") if err != nil { - return fmt.Errorf("getting bucket: %w", err) + return deleteConfig{}, fmt.Errorf("getting bucket: %w", err) } testing, err := cmd.Flags().GetBool("testing") if err != nil { - return fmt.Errorf("getting testing flag: %w", err) + return deleteConfig{}, fmt.Errorf("getting testing flag: %w", err) } apiCfg := getAPIEnvironment(testing) + provider := cloudprovider.FromString(args[0]) + kind := kindFromString(args[1]) + version := args[2] + + return deleteConfig{ + provider: provider, + kind: kind, + version: version, + region: region, + bucket: bucket, + url: apiCfg.url, + distribution: apiCfg.distribution, + cosignPublicKey: apiCfg.cosignPublicKey, + }, nil +} + +func runDelete(cmd *cobra.Command, args []string) (retErr error) { + log := logger.New(logger.PlainLog, zap.DebugLevel).Named("attestationconfigapi") + + deleteCfg, err := newDeleteConfig(cmd, ([3]string)(args[:3])) + if err != nil { + return fmt.Errorf("creating delete config: %w", err) + } + cfg := staticupload.Config{ - Bucket: bucket, - Region: region, - DistributionID: apiCfg.distribution, + Bucket: deleteCfg.bucket, + Region: deleteCfg.region, + DistributionID: deleteCfg.distribution, } client, clientClose, err := attestationconfigapi.NewClient(cmd.Context(), cfg, []byte(cosignPwd), []byte(privateKey), false, 1, log) @@ -92,34 +110,29 @@ func runDelete(cmd *cobra.Command, _ []string) (retErr error) { } }() - deleteCmd := deleteCmd{ - attestationClient: client, + switch deleteCfg.provider { + case cloudprovider.AWS: + return deleteAWS(cmd.Context(), client, deleteCfg) + case cloudprovider.Azure: + return deleteAzure(cmd.Context(), client, deleteCfg) + default: + return fmt.Errorf("unsupported cloud provider: %s", deleteCfg.provider) } - return deleteCmd.delete(cmd) } -func runRecursiveDelete(cmd *cobra.Command, _ []string) (retErr error) { - region, err := cmd.Flags().GetString("region") - if err != nil { - return fmt.Errorf("getting region: %w", err) - } - - bucket, err := cmd.Flags().GetString("bucket") - if err != nil { - return fmt.Errorf("getting bucket: %w", err) - } - - testing, err := cmd.Flags().GetBool("testing") +func runRecursiveDelete(cmd *cobra.Command, args []string) (retErr error) { + // newDeleteConfig expects 3 args, so we pass "all" for the version argument. + args = append(args, "all") + deleteCfg, err := newDeleteConfig(cmd, ([3]string)(args[:3])) if err != nil { - return fmt.Errorf("getting testing flag: %w", err) + return fmt.Errorf("creating delete config: %w", err) } - apiCfg := getAPIEnvironment(testing) log := logger.New(logger.PlainLog, zap.DebugLevel).Named("attestationconfigapi") client, closeFn, err := staticupload.New(cmd.Context(), staticupload.Config{ - Bucket: bucket, - Region: region, - DistributionID: apiCfg.distribution, + Bucket: deleteCfg.bucket, + Region: deleteCfg.region, + DistributionID: deleteCfg.distribution, }, log) if err != nil { return fmt.Errorf("create static upload client: %w", err) @@ -130,31 +143,10 @@ func runRecursiveDelete(cmd *cobra.Command, _ []string) (retErr error) { retErr = errors.Join(retErr, fmt.Errorf("failed to close client: %w", err)) } }() - path := "constellation/v1/attestation/azure-sev-snp" - resp, err := client.ListObjectsV2(cmd.Context(), &s3.ListObjectsV2Input{ - Bucket: aws.String(bucket), - Prefix: aws.String(path), - }) - if err != nil { - return err - } - // Delete all objects in the path. - objIDs := make([]s3types.ObjectIdentifier, len(resp.Contents)) - for i, obj := range resp.Contents { - objIDs[i] = s3types.ObjectIdentifier{Key: obj.Key} + if deleteCfg.provider != cloudprovider.Azure || deleteCfg.kind != snpReport { + return fmt.Errorf("provider %s and kind %s not supported", deleteCfg.provider, deleteCfg.kind) } - if len(objIDs) > 0 { - _, err = client.DeleteObjects(cmd.Context(), &s3.DeleteObjectsInput{ - Bucket: aws.String(bucket), - Delete: &s3types.Delete{ - Objects: objIDs, - Quiet: true, - }, - }) - if err != nil { - return err - } - } - return nil + + return deleteRecursiveAzure(cmd.Context(), client, deleteCfg) } diff --git a/internal/api/attestationconfigapi/cli/delete_test.go b/internal/api/attestationconfigapi/cli/delete_test.go index adca530602..2a483e9356 100644 --- a/internal/api/attestationconfigapi/cli/delete_test.go +++ b/internal/api/attestationconfigapi/cli/delete_test.go @@ -4,35 +4,3 @@ Copyright (c) Edgeless Systems GmbH SPDX-License-Identifier: AGPL-3.0-only */ package main - -import ( - "context" - "errors" - "testing" - - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" -) - -func TestDeleteVersion(t *testing.T) { - client := &fakeAttestationClient{} - sut := deleteCmd{ - attestationClient: client, - } - cmd := newDeleteCmd() - require.NoError(t, cmd.Flags().Set("version", "2021-01-01")) - assert.NoError(t, sut.delete(cmd)) - assert.True(t, client.isCalled) -} - -type fakeAttestationClient struct { - isCalled bool -} - -func (f *fakeAttestationClient) DeleteAzureSEVSNPVersion(_ context.Context, version string) error { - if version == "2021-01-01" { - f.isCalled = true - return nil - } - return errors.New("version does not exist") -} diff --git a/internal/api/attestationconfigapi/cli/e2e/test.sh.in b/internal/api/attestationconfigapi/cli/e2e/test.sh.in index 1b501d3fd0..013a7ae77d 100755 --- a/internal/api/attestationconfigapi/cli/e2e/test.sh.in +++ b/internal/api/attestationconfigapi/cli/e2e/test.sh.in @@ -28,7 +28,7 @@ readonly tmpdir registerExitHandler "rm -rf $tmpdir" # empty the bucket version state -${configapi_cli} delete recursive --region "$region" --bucket "$bucket" +${configapi_cli} delete recursive azure snp-report --region "$region" --bucket "$bucket" # the high version numbers ensure that it's newer than the current latest value readonly current_report_path="$tmpdir/currentSnpReport.json" @@ -57,7 +57,7 @@ cat << EOF > "$current_report_path" } EOF # upload a fake latest version for the fetcher -${configapi_cli} --force --snp-report-path "$current_report_path" --upload-date "2000-01-01-01-01" --region "$region" --bucket "$bucket" +${configapi_cli} upload azure snp-report "$current_report_path" --force --upload-date "2000-01-01-01-01" --region "$region" --bucket "$bucket" # the high version numbers ensure that it's newer than the current latest value readonly report_path="$tmpdir/snpReport.json" @@ -115,11 +115,11 @@ EOF # report 3 versions with different dates to fill the reporter cache readonly date_oldest="2023-02-01-03-04" -${configapi_cli} --snp-report-path "$older_report_path" --upload-date "$date_oldest" --region "$region" --bucket "$bucket" --cache-window-size 3 +${configapi_cli} upload azure snp-report "$older_report_path" --upload-date "$date_oldest" --region "$region" --bucket "$bucket" --cache-window-size 3 readonly date_older="2023-02-02-03-04" -${configapi_cli} --snp-report-path "$older_report_path" --upload-date "$date_older" --region "$region" --bucket "$bucket" --cache-window-size 3 +${configapi_cli} upload azure snp-report "$older_report_path" --upload-date "$date_older" --region "$region" --bucket "$bucket" --cache-window-size 3 readonly date="2023-02-03-03-04" -${configapi_cli} --snp-report-path "$report_path" --upload-date "$date" --region "$region" --bucket "$bucket" --cache-window-size 3 +${configapi_cli} upload azure snp-report "$report_path" --upload-date "$date" --region "$region" --bucket "$bucket" --cache-window-size 3 # expect that $date_oldest is served as latest version baseurl="https://d33dzgxuwsgbpw.cloudfront.net/constellation/v1/attestation/azure-sev-snp" @@ -165,7 +165,7 @@ if [[ $http_code -ne 404 ]]; then exit 1 fi -${configapi_cli} delete --version "$date_oldest" --region "$region" --bucket "$bucket" +${configapi_cli} delete azure snp-report "$date_oldest" --region "$region" --bucket "$bucket" # Omit -f to check for 404. We want to check that a file was deleted, therefore we expect the query to fail. http_code=$(curl -sSL -w '%{http_code}\n' -o /dev/null ${baseurl}/${date_oldest}.json) diff --git a/internal/api/attestationconfigapi/cli/main.go b/internal/api/attestationconfigapi/cli/main.go index 59b5bc18f1..cc4ef74c7f 100644 --- a/internal/api/attestationconfigapi/cli/main.go +++ b/internal/api/attestationconfigapi/cli/main.go @@ -21,11 +21,11 @@ import ( "time" "github.com/edgelesssys/constellation/v2/internal/api/attestationconfigapi" + "github.com/edgelesssys/constellation/v2/internal/cloud/cloudprovider" "github.com/edgelesssys/constellation/v2/internal/constants" "github.com/edgelesssys/constellation/v2/internal/file" "github.com/edgelesssys/constellation/v2/internal/logger" "github.com/edgelesssys/constellation/v2/internal/staticupload" - "github.com/edgelesssys/constellation/v2/internal/verify" "github.com/spf13/afero" "github.com/spf13/cobra" "go.uber.org/zap" @@ -56,29 +56,40 @@ func main() { // newRootCmd creates the root command. func newRootCmd() *cobra.Command { - rootCmd := &cobra.Command{ - Use: "COSIGN_PASSWORD=$CPW COSIGN_PRIVATE_KEY=$CKEY upload --version-file $FILE", - Short: "Upload a set of versions specific to the azure-sev-snp attestation variant to the config api.", + rootCmd := &cobra.Command{} + rootCmd.PersistentFlags().StringP("region", "r", awsRegion, "region of the targeted bucket.") + rootCmd.PersistentFlags().StringP("bucket", "b", awsBucket, "bucket targeted by all operations.") + rootCmd.PersistentFlags().Bool("testing", false, "upload to S3 test bucket.") + + rootCmd.AddCommand(newUploadCmd()) + rootCmd.AddCommand(newDeleteCmd()) + + return rootCmd +} + +func newUploadCmd() *cobra.Command { + uploadCmd := &cobra.Command{ + Use: "upload {azure|aws} {snp-report|guest-firmware} ", + Short: "Upload an object to the attestationconfig API", - Long: fmt.Sprintf("The CLI uploads an observed version number specific to the azure-sev-snp attestation variant to a cache directory. The CLI then determines the lowest version within the cache-window present in the cache and writes that value to the config api if necessary. "+ + Long: fmt.Sprintf("Upload a new object to the attestationconfig API. For snp-reports the new object is added to a cache folder first."+ + "The CLI then determines the lowest version within the cache-window present in the cache and writes that value to the config api if necessary. "+ + "For guest-firmware objects the object is added to the API directly. "+ "Please authenticate with AWS through your preferred method (e.g. environment variables, CLI)"+ "to be able to upload to S3. Set the %s and %s environment variables to authenticate with cosign.", envCosignPrivateKey, envCosignPwd, ), + + Args: cobra.MatchAll(cobra.ExactArgs(3), isCloudProvider(0), isValidKind(1)), PreRunE: envCheck, - RunE: runCmd, + RunE: runUpload, } - rootCmd.Flags().StringP("snp-report-path", "t", "", "File path to a file containing the Constellation verify output.") - rootCmd.Flags().StringP("upload-date", "d", "", "upload a version with this date as version name.") - rootCmd.Flags().BoolP("force", "f", false, "Use force to manually push a new latest version."+ + uploadCmd.Flags().StringP("upload-date", "d", "", "upload a version with this date as version name.") + uploadCmd.Flags().BoolP("force", "f", false, "Use force to manually push a new latest version."+ " The version gets saved to the cache but the version selection logic is skipped.") - rootCmd.Flags().IntP("cache-window-size", "s", versionWindowSize, "Number of versions to be considered for the latest version.") - rootCmd.PersistentFlags().StringP("region", "r", awsRegion, "region of the targeted bucket.") - rootCmd.PersistentFlags().StringP("bucket", "b", awsBucket, "bucket targeted by all operations.") - rootCmd.PersistentFlags().Bool("testing", false, "upload to S3 test bucket.") - must(rootCmd.MarkFlagRequired("snp-report-path")) - rootCmd.AddCommand(newDeleteCmd()) - return rootCmd + uploadCmd.Flags().IntP("cache-window-size", "s", versionWindowSize, "Number of versions to be considered for the latest version.") + + return uploadCmd } func envCheck(_ *cobra.Command, _ []string) error { @@ -90,38 +101,29 @@ func envCheck(_ *cobra.Command, _ []string) error { return nil } -func runCmd(cmd *cobra.Command, _ []string) (retErr error) { +func runUpload(cmd *cobra.Command, args []string) (retErr error) { ctx := cmd.Context() log := logger.New(logger.PlainLog, zap.DebugLevel).Named("attestationconfigapi") - flags, err := parseCliFlags(cmd) + log.Infof("%s", args) + uploadCfg, err := newConfig(cmd, ([3]string)(args[:3])) if err != nil { return fmt.Errorf("parsing cli flags: %w", err) } - cfg := staticupload.Config{ - Bucket: flags.bucket, - Region: flags.region, - DistributionID: flags.distribution, - } - - log.Infof("Reading SNP report from file: %s", flags.snpReportPath) + client, clientClose, err := attestationconfigapi.NewClient( + ctx, + staticupload.Config{ + Bucket: uploadCfg.bucket, + Region: uploadCfg.region, + DistributionID: uploadCfg.distribution, + }, + []byte(cosignPwd), + []byte(privateKey), + false, + uploadCfg.cacheWindowSize, + log) - fs := file.NewHandler(afero.NewOsFs()) - var report verify.Report - if err := fs.ReadJSON(flags.snpReportPath, &report); err != nil { - return fmt.Errorf("reading snp report: %w", err) - } - snpReport := report.SNPReport - if !allEqual(snpReport.LaunchTCB, snpReport.CommittedTCB, snpReport.ReportedTCB) { - return fmt.Errorf("TCB versions are not equal: \nLaunchTCB:%+v\nCommitted TCB:%+v\nReportedTCB:%+v", - snpReport.LaunchTCB, snpReport.CommittedTCB, snpReport.ReportedTCB) - } - inputVersion := convertTCBVersionToAzureVersion(snpReport.LaunchTCB) - log.Infof("Input report: %+v", inputVersion) - - client, clientClose, err := attestationconfigapi.NewClient(ctx, cfg, - []byte(cosignPwd), []byte(privateKey), false, flags.cacheWindowSize, log) defer func() { err := clientClose(cmd.Context()) if err != nil { @@ -133,51 +135,20 @@ func runCmd(cmd *cobra.Command, _ []string) (retErr error) { return fmt.Errorf("creating client: %w", err) } - latestAPIVersionAPI, err := attestationconfigapi.NewFetcherWithCustomCDNAndCosignKey(flags.url, flags.cosignPublicKey).FetchAzureSEVSNPVersionLatest(ctx) - if err != nil { - if errors.Is(err, attestationconfigapi.ErrNoVersionsFound) { - log.Infof("No versions found in API, but assuming that we are uploading the first version.") - } else { - return fmt.Errorf("fetching latest version: %w", err) - } - } - latestAPIVersion := latestAPIVersionAPI.AzureSEVSNPVersion - if err := client.UploadAzureSEVSNPVersionLatest(ctx, inputVersion, latestAPIVersion, flags.uploadDate, flags.force); err != nil { - if errors.Is(err, attestationconfigapi.ErrNoNewerVersion) { - log.Infof("Input version: %+v is not newer than latest API version: %+v", inputVersion, latestAPIVersion) - return nil - } - return fmt.Errorf("updating latest version: %w", err) - } - return nil -} - -func allEqual(args ...verify.TCBVersion) bool { - if len(args) < 2 { - return true - } - - firstArg := args[0] - for _, arg := range args[1:] { - if arg != firstArg { - return false - } - } - - return true -} - -func convertTCBVersionToAzureVersion(tcb verify.TCBVersion) attestationconfigapi.AzureSEVSNPVersion { - return attestationconfigapi.AzureSEVSNPVersion{ - Bootloader: tcb.Bootloader, - TEE: tcb.TEE, - SNP: tcb.SNP, - Microcode: tcb.Microcode, + switch uploadCfg.provider { + case cloudprovider.AWS: + return uploadAWS(ctx, client, uploadCfg, file.NewHandler(afero.NewOsFs()), log) + case cloudprovider.Azure: + return uploadAzure(ctx, client, uploadCfg, file.NewHandler(afero.NewOsFs()), log) + default: + return fmt.Errorf("unsupported cloud provider: %s", uploadCfg.provider) } } -type config struct { - snpReportPath string +type uploadConfig struct { + provider cloudprovider.Provider + kind objectKind + path string uploadDate time.Time cosignPublicKey string region string @@ -188,51 +159,53 @@ type config struct { cacheWindowSize int } -func parseCliFlags(cmd *cobra.Command) (config, error) { - snpReportFilePath, err := cmd.Flags().GetString("snp-report-path") - if err != nil { - return config{}, fmt.Errorf("getting maa claims path: %w", err) - } - +func newConfig(cmd *cobra.Command, args [3]string) (uploadConfig, error) { dateStr, err := cmd.Flags().GetString("upload-date") if err != nil { - return config{}, fmt.Errorf("getting upload date: %w", err) + return uploadConfig{}, fmt.Errorf("getting upload date: %w", err) } uploadDate := time.Now() if dateStr != "" { uploadDate, err = time.Parse(attestationconfigapi.VersionFormat, dateStr) if err != nil { - return config{}, fmt.Errorf("parsing date: %w", err) + return uploadConfig{}, fmt.Errorf("parsing date: %w", err) } } region, err := cmd.Flags().GetString("region") if err != nil { - return config{}, fmt.Errorf("getting region: %w", err) + return uploadConfig{}, fmt.Errorf("getting region: %w", err) } bucket, err := cmd.Flags().GetString("bucket") if err != nil { - return config{}, fmt.Errorf("getting bucket: %w", err) + return uploadConfig{}, fmt.Errorf("getting bucket: %w", err) } testing, err := cmd.Flags().GetBool("testing") if err != nil { - return config{}, fmt.Errorf("getting testing flag: %w", err) + return uploadConfig{}, fmt.Errorf("getting testing flag: %w", err) } apiCfg := getAPIEnvironment(testing) force, err := cmd.Flags().GetBool("force") if err != nil { - return config{}, fmt.Errorf("getting force: %w", err) + return uploadConfig{}, fmt.Errorf("getting force: %w", err) } cacheWindowSize, err := cmd.Flags().GetInt("cache-window-size") if err != nil { - return config{}, fmt.Errorf("getting cache window size: %w", err) + return uploadConfig{}, fmt.Errorf("getting cache window size: %w", err) } - return config{ - snpReportPath: snpReportFilePath, + + provider := cloudprovider.FromString(args[0]) + kind := kindFromString(args[1]) + path := args[2] + + return uploadConfig{ + provider: provider, + kind: kind, + path: path, uploadDate: uploadDate, cosignPublicKey: apiCfg.cosignPublicKey, region: region, @@ -256,9 +229,3 @@ func getAPIEnvironment(testing bool) apiConfig { } return apiConfig{url: constants.CDNRepositoryURL, distribution: constants.CDNDefaultDistributionID, cosignPublicKey: constants.CosignPublicKeyReleases} } - -func must(err error) { - if err != nil { - panic(err) - } -} diff --git a/internal/api/attestationconfigapi/cli/main_test.go b/internal/api/attestationconfigapi/cli/main_test.go index c1c1f09358..2a483e9356 100644 --- a/internal/api/attestationconfigapi/cli/main_test.go +++ b/internal/api/attestationconfigapi/cli/main_test.go @@ -4,29 +4,3 @@ Copyright (c) Edgeless Systems GmbH SPDX-License-Identifier: AGPL-3.0-only */ package main - -import ( - "testing" - - "github.com/edgelesssys/constellation/v2/internal/verify" - "github.com/stretchr/testify/assert" -) - -func TestAllEqual(t *testing.T) { - // Test case 1: One input arg - assert.True(t, allEqual(verify.TCBVersion{Bootloader: 1, Microcode: 2, SNP: 3, TEE: 4}), "Expected allEqual to return true for one input arg, but got false") - - // Test case 2: Three input args that are equal - assert.True(t, allEqual( - verify.TCBVersion{Bootloader: 1, Microcode: 2, SNP: 3, TEE: 4}, - verify.TCBVersion{Bootloader: 1, Microcode: 2, SNP: 3, TEE: 4}, - verify.TCBVersion{Bootloader: 1, Microcode: 2, SNP: 3, TEE: 4}, - ), "Expected allEqual to return true for three equal input args, but got false") - - // Test case 3: Three input args where second and third element are different - assert.False(t, allEqual( - verify.TCBVersion{Bootloader: 2, Microcode: 2, SNP: 3, TEE: 4}, - verify.TCBVersion{Bootloader: 2, Microcode: 2, SNP: 3, TEE: 4}, - verify.TCBVersion{Bootloader: 2, Microcode: 3, SNP: 3, TEE: 4}, - ), "Expected allEqual to return false for three input args with different second and third elements, but got true") -} diff --git a/internal/api/attestationconfigapi/cli/validargs.go b/internal/api/attestationconfigapi/cli/validargs.go new file mode 100644 index 0000000000..c077c58e0e --- /dev/null +++ b/internal/api/attestationconfigapi/cli/validargs.go @@ -0,0 +1,53 @@ +/* +Copyright (c) Edgeless Systems GmbH + +SPDX-License-Identifier: AGPL-3.0-only +*/ + +package main + +import ( + "fmt" + "strings" + + "github.com/edgelesssys/constellation/v2/internal/cloud/cloudprovider" + "github.com/spf13/cobra" +) + +func isCloudProvider(arg int) cobra.PositionalArgs { + return func(cmd *cobra.Command, args []string) error { + if provider := cloudprovider.FromString(args[arg]); provider == cloudprovider.Unknown { + return fmt.Errorf("argument %s isn't a valid cloud provider", args[arg]) + } + return nil + } +} + +func isValidKind(arg int) cobra.PositionalArgs { + return func(cmd *cobra.Command, args []string) error { + if kind := kindFromString(args[arg]); kind == unknown { + return fmt.Errorf("argument %s isn't a valid kind", args[arg]) + } + return nil + } +} + +// objectKind encodes the available actions. +type objectKind string + +const ( + // unknown is the default objectKind and does nothing. + unknown objectKind = "unknown-kind" + snpReport objectKind = "snp-report" + guestFirmware objectKind = "guest-firmware" +) + +func kindFromString(s string) objectKind { + lower := strings.ToLower(s) + switch objectKind(lower) { + case snpReport, guestFirmware: + return objectKind(lower) + default: + return unknown + } +} From 7e8f91540e425c2e8192f01e4b1a405e9a7766c1 Mon Sep 17 00:00:00 2001 From: Otto Bittner Date: Tue, 14 Nov 2023 10:03:01 +0100 Subject: [PATCH 07/12] api: refactor attestationconfigapi client/fetcher There is now one SEVSNPVersions type that has a variant property. That property is used to build the correct JSON path. The surrounding methods handling the version objects are also updated to receive a variant argument and work for multiple variants. This simplifies adding AWS support. --- .../cmd/configfetchmeasurements_test.go | 21 ++-- cli/internal/cmd/iamupgradeapply_test.go | 9 +- internal/api/attestationconfigapi/BUILD.bazel | 5 +- internal/api/attestationconfigapi/azure.go | 84 ------------- .../api/attestationconfigapi/cli/BUILD.bazel | 7 +- .../api/attestationconfigapi/cli/azure.go | 13 +- internal/api/attestationconfigapi/client.go | 77 ++++++------ .../api/attestationconfigapi/client_test.go | 22 ++-- internal/api/attestationconfigapi/fetcher.go | 51 ++++---- .../api/attestationconfigapi/fetcher_test.go | 62 ++++++---- internal/api/attestationconfigapi/reporter.go | 69 ++++++----- .../api/attestationconfigapi/reporter_test.go | 14 +-- internal/api/attestationconfigapi/snp.go | 113 ++++++++++++++++++ internal/api/attestationconfigapi/snp_test.go | 77 ++++++++++++ internal/config/azure.go | 6 +- internal/config/config_test.go | 41 ++++--- 16 files changed, 410 insertions(+), 261 deletions(-) delete mode 100644 internal/api/attestationconfigapi/azure.go create mode 100644 internal/api/attestationconfigapi/snp.go create mode 100644 internal/api/attestationconfigapi/snp_test.go diff --git a/cli/internal/cmd/configfetchmeasurements_test.go b/cli/internal/cmd/configfetchmeasurements_test.go index 6dd230ef1b..7f4aff0763 100644 --- a/cli/internal/cmd/configfetchmeasurements_test.go +++ b/cli/internal/cmd/configfetchmeasurements_test.go @@ -17,6 +17,7 @@ import ( "github.com/edgelesssys/constellation/v2/internal/api/attestationconfigapi" "github.com/edgelesssys/constellation/v2/internal/api/versionsapi" + "github.com/edgelesssys/constellation/v2/internal/attestation/variant" "github.com/edgelesssys/constellation/v2/internal/cloud/cloudprovider" "github.com/edgelesssys/constellation/v2/internal/config" "github.com/edgelesssys/constellation/v2/internal/constants" @@ -294,25 +295,23 @@ func TestConfigFetchMeasurements(t *testing.T) { type stubAttestationFetcher struct{} -func (f stubAttestationFetcher) FetchAzureSEVSNPVersionList(_ context.Context, _ attestationconfigapi.AzureSEVSNPVersionList) (attestationconfigapi.AzureSEVSNPVersionList, error) { - return attestationconfigapi.AzureSEVSNPVersionList( - []string{}, - ), nil +func (f stubAttestationFetcher) FetchSEVSNPVersionList(_ context.Context, _ attestationconfigapi.SEVSNPVersionList) (attestationconfigapi.SEVSNPVersionList, error) { + return attestationconfigapi.SEVSNPVersionList{}, nil } -func (f stubAttestationFetcher) FetchAzureSEVSNPVersion(_ context.Context, _ attestationconfigapi.AzureSEVSNPVersionAPI) (attestationconfigapi.AzureSEVSNPVersionAPI, error) { - return attestationconfigapi.AzureSEVSNPVersionAPI{ - AzureSEVSNPVersion: testCfg, +func (f stubAttestationFetcher) FetchSEVSNPVersion(_ context.Context, _ attestationconfigapi.SEVSNPVersionAPI) (attestationconfigapi.SEVSNPVersionAPI, error) { + return attestationconfigapi.SEVSNPVersionAPI{ + SEVSNPVersion: testCfg, }, nil } -func (f stubAttestationFetcher) FetchAzureSEVSNPVersionLatest(_ context.Context) (attestationconfigapi.AzureSEVSNPVersionAPI, error) { - return attestationconfigapi.AzureSEVSNPVersionAPI{ - AzureSEVSNPVersion: testCfg, +func (f stubAttestationFetcher) FetchSEVSNPVersionLatest(_ context.Context, _ variant.Variant) (attestationconfigapi.SEVSNPVersionAPI, error) { + return attestationconfigapi.SEVSNPVersionAPI{ + SEVSNPVersion: testCfg, }, nil } -var testCfg = attestationconfigapi.AzureSEVSNPVersion{ +var testCfg = attestationconfigapi.SEVSNPVersion{ Microcode: 93, TEE: 0, SNP: 6, diff --git a/cli/internal/cmd/iamupgradeapply_test.go b/cli/internal/cmd/iamupgradeapply_test.go index 4a6a580969..81d0e14b0d 100644 --- a/cli/internal/cmd/iamupgradeapply_test.go +++ b/cli/internal/cmd/iamupgradeapply_test.go @@ -14,6 +14,7 @@ import ( "github.com/edgelesssys/constellation/v2/cli/internal/terraform" "github.com/edgelesssys/constellation/v2/internal/api/attestationconfigapi" + "github.com/edgelesssys/constellation/v2/internal/attestation/variant" "github.com/edgelesssys/constellation/v2/internal/cloud/cloudprovider" "github.com/edgelesssys/constellation/v2/internal/config" "github.com/edgelesssys/constellation/v2/internal/constants" @@ -170,14 +171,14 @@ type stubConfigFetcher struct { fetchLatestErr error } -func (s *stubConfigFetcher) FetchAzureSEVSNPVersion(context.Context, attestationconfigapi.AzureSEVSNPVersionAPI) (attestationconfigapi.AzureSEVSNPVersionAPI, error) { +func (s *stubConfigFetcher) FetchSEVSNPVersion(context.Context, attestationconfigapi.SEVSNPVersionAPI) (attestationconfigapi.SEVSNPVersionAPI, error) { panic("not implemented") } -func (s *stubConfigFetcher) FetchAzureSEVSNPVersionList(context.Context, attestationconfigapi.AzureSEVSNPVersionList) (attestationconfigapi.AzureSEVSNPVersionList, error) { +func (s *stubConfigFetcher) FetchSEVSNPVersionList(context.Context, attestationconfigapi.SEVSNPVersionList) (attestationconfigapi.SEVSNPVersionList, error) { panic("not implemented") } -func (s *stubConfigFetcher) FetchAzureSEVSNPVersionLatest(context.Context) (attestationconfigapi.AzureSEVSNPVersionAPI, error) { - return attestationconfigapi.AzureSEVSNPVersionAPI{}, s.fetchLatestErr +func (s *stubConfigFetcher) FetchSEVSNPVersionLatest(context.Context, variant.Variant) (attestationconfigapi.SEVSNPVersionAPI, error) { + return attestationconfigapi.SEVSNPVersionAPI{}, s.fetchLatestErr } diff --git a/internal/api/attestationconfigapi/BUILD.bazel b/internal/api/attestationconfigapi/BUILD.bazel index da62b490a2..037f482c94 100644 --- a/internal/api/attestationconfigapi/BUILD.bazel +++ b/internal/api/attestationconfigapi/BUILD.bazel @@ -5,10 +5,10 @@ go_library( name = "attestationconfigapi", srcs = [ "attestationconfigapi.go", - "azure.go", "client.go", "fetcher.go", "reporter.go", + "snp.go", ], importpath = "github.com/edgelesssys/constellation/v2/internal/api/attestationconfigapi", visibility = ["//:__subpackages__"], @@ -31,10 +31,13 @@ go_test( "client_test.go", "fetcher_test.go", "reporter_test.go", + "snp_test.go", ], embed = [":attestationconfigapi"], deps = [ + "//internal/attestation/variant", "//internal/constants", "@com_github_stretchr_testify//assert", + "@com_github_stretchr_testify//require", ], ) diff --git a/internal/api/attestationconfigapi/azure.go b/internal/api/attestationconfigapi/azure.go deleted file mode 100644 index f915420a1a..0000000000 --- a/internal/api/attestationconfigapi/azure.go +++ /dev/null @@ -1,84 +0,0 @@ -/* -Copyright (c) Edgeless Systems GmbH - -SPDX-License-Identifier: AGPL-3.0-only -*/ - -package attestationconfigapi - -import ( - "fmt" - "path" - "sort" - "strings" - - "github.com/edgelesssys/constellation/v2/internal/attestation/variant" -) - -// attestationURLPath is the URL path to the attestation versions. -const attestationURLPath = "constellation/v1/attestation" - -// AzureSEVSNPVersionType is the type of the version to be requested. -type AzureSEVSNPVersionType string - -// AzureSEVSNPVersion tracks the latest version of each component of the Azure SEVSNP. -type AzureSEVSNPVersion struct { - // Bootloader is the latest version of the Azure SEVSNP bootloader. - Bootloader uint8 `json:"bootloader"` - // TEE is the latest version of the Azure SEVSNP TEE. - TEE uint8 `json:"tee"` - // SNP is the latest version of the Azure SEVSNP SNP. - SNP uint8 `json:"snp"` - // Microcode is the latest version of the Azure SEVSNP microcode. - Microcode uint8 `json:"microcode"` -} - -// AzureSEVSNPVersionAPI is the request to get the version information of the specific version in the config api. -type AzureSEVSNPVersionAPI struct { - Version string `json:"-"` - AzureSEVSNPVersion -} - -// JSONPath returns the path to the JSON file for the request to the config api. -func (i AzureSEVSNPVersionAPI) JSONPath() string { - return path.Join(attestationURLPath, variant.AzureSEVSNP{}.String(), i.Version) -} - -// ValidateRequest validates the request. -func (i AzureSEVSNPVersionAPI) ValidateRequest() error { - if !strings.HasSuffix(i.Version, ".json") { - return fmt.Errorf("version has no .json suffix") - } - return nil -} - -// Validate is a No-Op at the moment. -func (i AzureSEVSNPVersionAPI) Validate() error { - return nil -} - -// AzureSEVSNPVersionList is the request to list all versions in the config api. -type AzureSEVSNPVersionList []string - -// JSONPath returns the path to the JSON file for the request to the config api. -func (i AzureSEVSNPVersionList) JSONPath() string { - return path.Join(attestationURLPath, variant.AzureSEVSNP{}.String(), "list") -} - -// ValidateRequest is a NoOp as there is no input. -func (i AzureSEVSNPVersionList) ValidateRequest() error { - return nil -} - -// SortAzureSEVSNPVersionList sorts the list of versions in reverse order. -func SortAzureSEVSNPVersionList(versions AzureSEVSNPVersionList) { - sort.Sort(sort.Reverse(sort.StringSlice(versions))) -} - -// Validate validates the response. -func (i AzureSEVSNPVersionList) Validate() error { - if len(i) < 1 { - return fmt.Errorf("no versions found in /list") - } - return nil -} diff --git a/internal/api/attestationconfigapi/cli/BUILD.bazel b/internal/api/attestationconfigapi/cli/BUILD.bazel index ed7640a69f..0eb5017b87 100644 --- a/internal/api/attestationconfigapi/cli/BUILD.bazel +++ b/internal/api/attestationconfigapi/cli/BUILD.bazel @@ -15,13 +15,13 @@ go_library( "azure.go", "delete.go", "main.go", - "objectkind_string.go", "validargs.go", ], importpath = "github.com/edgelesssys/constellation/v2/internal/api/attestationconfigapi/cli", visibility = ["//visibility:private"], deps = [ "//internal/api/attestationconfigapi", + "//internal/attestation/variant", "//internal/cloud/cloudprovider", "//internal/constants", "//internal/file", @@ -44,11 +44,6 @@ go_test( "main_test.go", ], embed = [":cli_lib"], - deps = [ - "//internal/cloud/cloudprovider", - "//internal/verify", - "@com_github_stretchr_testify//assert", - ], ) sh_template( diff --git a/internal/api/attestationconfigapi/cli/azure.go b/internal/api/attestationconfigapi/cli/azure.go index 93b363cb3a..32555bc4f2 100644 --- a/internal/api/attestationconfigapi/cli/azure.go +++ b/internal/api/attestationconfigapi/cli/azure.go @@ -15,6 +15,7 @@ import ( s3types "github.com/aws/aws-sdk-go-v2/service/s3/types" "github.com/aws/aws-sdk-go/aws" "github.com/edgelesssys/constellation/v2/internal/api/attestationconfigapi" + "github.com/edgelesssys/constellation/v2/internal/attestation/variant" "github.com/edgelesssys/constellation/v2/internal/cloud/cloudprovider" "github.com/edgelesssys/constellation/v2/internal/file" "github.com/edgelesssys/constellation/v2/internal/logger" @@ -36,7 +37,7 @@ func uploadAzure(ctx context.Context, client *attestationconfigapi.Client, cfg u inputVersion := convertTCBVersionToAzureVersion(report.SNPReport.LaunchTCB) log.Infof("Input report: %+v", inputVersion) - latestAPIVersionAPI, err := attestationconfigapi.NewFetcherWithCustomCDNAndCosignKey(cfg.url, cfg.cosignPublicKey).FetchAzureSEVSNPVersionLatest(ctx) + latestAPIVersionAPI, err := attestationconfigapi.NewFetcherWithCustomCDNAndCosignKey(cfg.url, cfg.cosignPublicKey).FetchSEVSNPVersionLatest(ctx, variant.AzureSEVSNP{}) if err != nil { if errors.Is(err, attestationconfigapi.ErrNoVersionsFound) { log.Infof("No versions found in API, but assuming that we are uploading the first version.") @@ -44,8 +45,8 @@ func uploadAzure(ctx context.Context, client *attestationconfigapi.Client, cfg u return fmt.Errorf("fetching latest version: %w", err) } } - latestAPIVersion := latestAPIVersionAPI.AzureSEVSNPVersion - if err := client.UploadAzureSEVSNPVersionLatest(ctx, inputVersion, latestAPIVersion, cfg.uploadDate, cfg.force); err != nil { + latestAPIVersion := latestAPIVersionAPI.SEVSNPVersion + if err := client.UploadSEVSNPVersionLatest(ctx, variant.AzureSEVSNP{}, inputVersion, latestAPIVersion, cfg.uploadDate, cfg.force); err != nil { if errors.Is(err, attestationconfigapi.ErrNoNewerVersion) { log.Infof("Input version: %+v is not newer than latest API version: %+v", inputVersion, latestAPIVersion) return nil @@ -56,8 +57,8 @@ func uploadAzure(ctx context.Context, client *attestationconfigapi.Client, cfg u return nil } -func convertTCBVersionToAzureVersion(tcb verify.TCBVersion) attestationconfigapi.AzureSEVSNPVersion { - return attestationconfigapi.AzureSEVSNPVersion{ +func convertTCBVersionToAzureVersion(tcb verify.TCBVersion) attestationconfigapi.SEVSNPVersion { + return attestationconfigapi.SEVSNPVersion{ Bootloader: tcb.Bootloader, TEE: tcb.TEE, SNP: tcb.SNP, @@ -67,7 +68,7 @@ func convertTCBVersionToAzureVersion(tcb verify.TCBVersion) attestationconfigapi func deleteAzure(ctx context.Context, client *attestationconfigapi.Client, cfg deleteConfig) error { if cfg.provider == cloudprovider.Azure && cfg.kind == snpReport { - return client.DeleteAzureSEVSNPVersion(ctx, cfg.version) + return client.DeleteSEVSNPVersion(ctx, variant.AzureSEVSNP{}, cfg.version) } return fmt.Errorf("provider %s and kind %s not supported", cfg.provider, cfg.kind) diff --git a/internal/api/attestationconfigapi/client.go b/internal/api/attestationconfigapi/client.go index f6ecf96d6f..9b4575a4c5 100644 --- a/internal/api/attestationconfigapi/client.go +++ b/internal/api/attestationconfigapi/client.go @@ -48,24 +48,25 @@ func NewClient(ctx context.Context, cfg staticupload.Config, cosignPwd, privateK return repo, clientClose, nil } -// uploadAzureSEVSNPVersion uploads the latest version numbers of the Azure SEVSNP. Then version name is the UTC timestamp of the date. The /list entry stores the version name + .json suffix. -func (a Client) uploadAzureSEVSNPVersion(ctx context.Context, version AzureSEVSNPVersion, date time.Time) error { - versions, err := a.List(ctx, variant.AzureSEVSNP{}) +// uploadSEVSNPVersion uploads the latest version numbers of the Azure SEVSNP. Then version name is the UTC timestamp of the date. The /list entry stores the version name + .json suffix. +func (a Client) uploadSEVSNPVersion(ctx context.Context, attestation variant.Variant, version SEVSNPVersion, date time.Time) error { + versions, err := a.List(ctx, attestation) if err != nil { return fmt.Errorf("fetch version list: %w", err) } - ops := a.constructUploadCmd(version, versions, date) + ops := a.constructUploadCmd(attestation, version, versions, date) return executeAllCmds(ctx, a.s3Client, ops) } -// DeleteAzureSEVSNPVersion deletes the given version (without .json suffix) from the API. -func (a Client) DeleteAzureSEVSNPVersion(ctx context.Context, versionStr string) error { - versions, err := a.List(ctx, variant.AzureSEVSNP{}) +// DeleteSEVSNPVersion deletes the given version (without .json suffix) from the API. +func (a Client) DeleteSEVSNPVersion(ctx context.Context, attestation variant.Variant, versionStr string) error { + versions, err := a.List(ctx, attestation) if err != nil { return fmt.Errorf("fetch version list: %w", err) } - ops, err := a.deleteAzureSEVSNPVersion(versions, versionStr) + + ops, err := a.deleteSEVSNPVersion(versions, versionStr) if err != nil { return err } @@ -73,25 +74,30 @@ func (a Client) DeleteAzureSEVSNPVersion(ctx context.Context, versionStr string) } // List returns the list of versions for the given attestation variant. -func (a Client) List(ctx context.Context, attestation variant.Variant) ([]string, error) { - if attestation.Equal(variant.AzureSEVSNP{}) { - versions, err := apiclient.Fetch(ctx, a.s3Client, AzureSEVSNPVersionList{}) - if err != nil { - var notFoundErr *apiclient.NotFoundError - if errors.As(err, ¬FoundErr) { - return nil, nil - } - return nil, err +func (a Client) List(ctx context.Context, attestation variant.Variant) (SEVSNPVersionList, error) { + if !attestation.Equal(variant.AzureSEVSNP{}) && !attestation.Equal(variant.AWSSEVSNP{}) { + return SEVSNPVersionList{}, fmt.Errorf("unsupported attestation variant: %s", attestation) + } + + versions, err := apiclient.Fetch(ctx, a.s3Client, SEVSNPVersionList{variant: attestation}) + if err != nil { + var notFoundErr *apiclient.NotFoundError + if errors.As(err, ¬FoundErr) { + return SEVSNPVersionList{variant: attestation}, nil } - return versions, nil + return SEVSNPVersionList{}, err } - return nil, fmt.Errorf("unsupported attestation variant: %s", attestation) + + versions.variant = attestation + + return versions, nil } -func (a Client) deleteAzureSEVSNPVersion(versions AzureSEVSNPVersionList, versionStr string) (ops []crudCmd, err error) { +func (a Client) deleteSEVSNPVersion(versions SEVSNPVersionList, versionStr string) (ops []crudCmd, err error) { versionStr = versionStr + ".json" ops = append(ops, deleteCmd{ - apiObject: AzureSEVSNPVersionAPI{ + apiObject: SEVSNPVersionAPI{ + Variant: versions.variant, Version: versionStr, }, }) @@ -107,36 +113,42 @@ func (a Client) deleteAzureSEVSNPVersion(versions AzureSEVSNPVersionList, versio return ops, nil } -func (a Client) constructUploadCmd(versions AzureSEVSNPVersion, versionNames []string, date time.Time) []crudCmd { +func (a Client) constructUploadCmd(attestation variant.Variant, version SEVSNPVersion, versionNames SEVSNPVersionList, date time.Time) []crudCmd { + if !attestation.Equal(versionNames.variant) { + return nil + } + dateStr := date.Format(VersionFormat) + ".json" var res []crudCmd res = append(res, putCmd{ - apiObject: AzureSEVSNPVersionAPI{Version: dateStr, AzureSEVSNPVersion: versions}, + apiObject: SEVSNPVersionAPI{Version: dateStr, Variant: attestation, SEVSNPVersion: version}, signer: a.signer, }) - newVersions := addVersion(versionNames, dateStr) + versionNames.addVersion(dateStr) + res = append(res, putCmd{ - apiObject: AzureSEVSNPVersionList(newVersions), + apiObject: versionNames, signer: a.signer, }) return res } -func removeVersion(versions AzureSEVSNPVersionList, versionStr string) (removedVersions AzureSEVSNPVersionList, err error) { +func removeVersion(list SEVSNPVersionList, versionStr string) (removedVersions SEVSNPVersionList, err error) { + versions := list.List() for i, v := range versions { if v == versionStr { if i == len(versions)-1 { - removedVersions = versions[:i] + removedVersions = SEVSNPVersionList{list: versions[:i], variant: list.variant} } else { - removedVersions = append(versions[:i], versions[i+1:]...) + removedVersions = SEVSNPVersionList{list: append(versions[:i], versions[i+1:]...), variant: list.variant} } return removedVersions, nil } } - return nil, fmt.Errorf("version %s not found in list %v", versionStr, versions) + return SEVSNPVersionList{}, fmt.Errorf("version %s not found in list %v", versionStr, versions) } type crudCmd interface { @@ -168,10 +180,3 @@ func executeAllCmds(ctx context.Context, client *apiclient.Client, cmds []crudCm } return nil } - -func addVersion(versions []string, newVersion string) []string { - versions = append(versions, newVersion) - versions = variant.RemoveDuplicate(versions) - SortAzureSEVSNPVersionList(versions) - return versions -} diff --git a/internal/api/attestationconfigapi/client_test.go b/internal/api/attestationconfigapi/client_test.go index dd7e494641..9cae1bc5a7 100644 --- a/internal/api/attestationconfigapi/client_test.go +++ b/internal/api/attestationconfigapi/client_test.go @@ -9,6 +9,7 @@ import ( "testing" "time" + "github.com/edgelesssys/constellation/v2/internal/attestation/variant" "github.com/stretchr/testify/assert" ) @@ -17,20 +18,21 @@ func TestUploadAzureSEVSNP(t *testing.T) { bucketID: "bucket", signer: fakeSigner{}, } - version := AzureSEVSNPVersion{} + version := SEVSNPVersion{} date := time.Date(2023, 1, 1, 1, 1, 1, 1, time.UTC) - ops := sut.constructUploadCmd(version, []string{"2021-01-01-01-01.json", "2019-01-01-01-01.json"}, date) + ops := sut.constructUploadCmd(variant.AzureSEVSNP{}, version, SEVSNPVersionList{list: []string{"2021-01-01-01-01.json", "2019-01-01-01-01.json"}, variant: variant.AzureSEVSNP{}}, date) dateStr := "2023-01-01-01-01.json" assert := assert.New(t) assert.Contains(ops, putCmd{ - apiObject: AzureSEVSNPVersionAPI{ - Version: dateStr, - AzureSEVSNPVersion: version, + apiObject: SEVSNPVersionAPI{ + Variant: variant.AzureSEVSNP{}, + Version: dateStr, + SEVSNPVersion: version, }, signer: fakeSigner{}, }) assert.Contains(ops, putCmd{ - apiObject: AzureSEVSNPVersionList([]string{"2023-01-01-01-01.json", "2021-01-01-01-01.json", "2019-01-01-01-01.json"}), + apiObject: SEVSNPVersionList{variant: variant.AzureSEVSNP{}, list: []string{"2023-01-01-01-01.json", "2021-01-01-01-01.json", "2019-01-01-01-01.json"}}, signer: fakeSigner{}, }) } @@ -39,20 +41,20 @@ func TestDeleteAzureSEVSNPVersions(t *testing.T) { sut := Client{ bucketID: "bucket", } - versions := AzureSEVSNPVersionList([]string{"2023-01-01.json", "2021-01-01.json", "2019-01-01.json"}) + versions := SEVSNPVersionList{list: []string{"2023-01-01.json", "2021-01-01.json", "2019-01-01.json"}} - ops, err := sut.deleteAzureSEVSNPVersion(versions, "2021-01-01") + ops, err := sut.deleteSEVSNPVersion(versions, "2021-01-01") assert := assert.New(t) assert.NoError(err) assert.Contains(ops, deleteCmd{ - apiObject: AzureSEVSNPVersionAPI{ + apiObject: SEVSNPVersionAPI{ Version: "2021-01-01.json", }, }) assert.Contains(ops, putCmd{ - apiObject: AzureSEVSNPVersionList([]string{"2023-01-01.json", "2019-01-01.json"}), + apiObject: SEVSNPVersionList{list: []string{"2023-01-01.json", "2019-01-01.json"}}, }) } diff --git a/internal/api/attestationconfigapi/fetcher.go b/internal/api/attestationconfigapi/fetcher.go index 3ea9c54302..490edd3f5c 100644 --- a/internal/api/attestationconfigapi/fetcher.go +++ b/internal/api/attestationconfigapi/fetcher.go @@ -12,6 +12,7 @@ import ( "fmt" apifetcher "github.com/edgelesssys/constellation/v2/internal/api/fetcher" + "github.com/edgelesssys/constellation/v2/internal/attestation/variant" "github.com/edgelesssys/constellation/v2/internal/constants" "github.com/edgelesssys/constellation/v2/internal/sigstore" ) @@ -23,9 +24,9 @@ var ErrNoVersionsFound = errors.New("no versions found") // Fetcher fetches config API resources without authentication. type Fetcher interface { - FetchAzureSEVSNPVersion(ctx context.Context, azureVersion AzureSEVSNPVersionAPI) (AzureSEVSNPVersionAPI, error) - FetchAzureSEVSNPVersionList(ctx context.Context, attestation AzureSEVSNPVersionList) (AzureSEVSNPVersionList, error) - FetchAzureSEVSNPVersionLatest(ctx context.Context) (AzureSEVSNPVersionAPI, error) + FetchSEVSNPVersion(ctx context.Context, version SEVSNPVersionAPI) (SEVSNPVersionAPI, error) + FetchSEVSNPVersionList(ctx context.Context, list SEVSNPVersionList) (SEVSNPVersionList, error) + FetchSEVSNPVersionLatest(ctx context.Context, attesation variant.Variant) (SEVSNPVersionAPI, error) } // fetcher fetches AttestationCfg API resources without authentication. @@ -64,35 +65,45 @@ func newFetcherWithClientAndVerifier(client apifetcher.HTTPClient, cosignVerifie return &fetcher{HTTPClient: client, verifier: cosignVerifier, cdnURL: url} } -// FetchAzureSEVSNPVersionList fetches the version list information from the config API. -func (f *fetcher) FetchAzureSEVSNPVersionList(ctx context.Context, attestation AzureSEVSNPVersionList) (AzureSEVSNPVersionList, error) { +// FetchSEVSNPVersionList fetches the version list information from the config API. +func (f *fetcher) FetchSEVSNPVersionList(ctx context.Context, list SEVSNPVersionList) (SEVSNPVersionList, error) { // TODO(derpsteb): Replace with FetchAndVerify once we move to v2 of the config API. - return apifetcher.Fetch(ctx, f.HTTPClient, f.cdnURL, attestation) + fetchedList, err := apifetcher.Fetch(ctx, f.HTTPClient, f.cdnURL, list) + if err != nil { + return list, fmt.Errorf("fetching version list: %w", err) + } + + // Need to set this explicitly as the variant is not part of the marshalled JSON. + fetchedList.variant = list.variant + + return fetchedList, nil } -// FetchAzureSEVSNPVersion fetches the version information from the config API. -func (f *fetcher) FetchAzureSEVSNPVersion(ctx context.Context, azureVersion AzureSEVSNPVersionAPI) (AzureSEVSNPVersionAPI, error) { - fetchedVersion, err := apifetcher.FetchAndVerify(ctx, f.HTTPClient, f.cdnURL, azureVersion, f.verifier) +// FetchSEVSNPVersion fetches the version information from the config API. +func (f *fetcher) FetchSEVSNPVersion(ctx context.Context, version SEVSNPVersionAPI) (SEVSNPVersionAPI, error) { + fetchedVersion, err := apifetcher.FetchAndVerify(ctx, f.HTTPClient, f.cdnURL, version, f.verifier) if err != nil { - return fetchedVersion, fmt.Errorf("fetching version %s: %w", azureVersion.Version, err) + return fetchedVersion, fmt.Errorf("fetching version %s: %w", version.Version, err) } + + // Need to set this explicitly as the variant is not part of the marshalled JSON. + fetchedVersion.Variant = version.Variant + return fetchedVersion, nil } -// FetchAzureSEVSNPVersionLatest returns the latest versions of the given type. -func (f *fetcher) FetchAzureSEVSNPVersionLatest(ctx context.Context) (res AzureSEVSNPVersionAPI, err error) { - var list AzureSEVSNPVersionList - list, err = f.FetchAzureSEVSNPVersionList(ctx, list) +// FetchSEVSNPVersionLatest returns the latest versions of the given type. +func (f *fetcher) FetchSEVSNPVersionLatest(ctx context.Context, attesation variant.Variant) (res SEVSNPVersionAPI, err error) { + list, err := f.FetchSEVSNPVersionList(ctx, SEVSNPVersionList{variant: attesation}) if err != nil { return res, ErrNoVersionsFound } - if len(list) < 1 { - return res, ErrNoVersionsFound - } - getVersionRequest := AzureSEVSNPVersionAPI{ - Version: list[0], // latest version is first in list + + getVersionRequest := SEVSNPVersionAPI{ + Version: list.List()[0], // latest version is first in list + Variant: attesation, } - res, err = f.FetchAzureSEVSNPVersion(ctx, getVersionRequest) + res, err = f.FetchSEVSNPVersion(ctx, getVersionRequest) if err != nil { return res, err } diff --git a/internal/api/attestationconfigapi/fetcher_test.go b/internal/api/attestationconfigapi/fetcher_test.go index 6e3f51eecf..cb9fd86ebc 100644 --- a/internal/api/attestationconfigapi/fetcher_test.go +++ b/internal/api/attestationconfigapi/fetcher_test.go @@ -16,48 +16,65 @@ import ( "testing" "time" + "github.com/edgelesssys/constellation/v2/internal/attestation/variant" "github.com/edgelesssys/constellation/v2/internal/constants" "github.com/stretchr/testify/assert" ) -func TestFetchLatestAzureSEVSNPVersion(t *testing.T) { +func TestFetchLatestSEVSNPVersion(t *testing.T) { latestStr := "2023-06-11-14-09.json" olderStr := "2019-01-01-01-01.json" testcases := map[string]struct { fetcherVersions []string timeAtTest time.Time wantErr bool - want AzureSEVSNPVersionAPI + attestation variant.Variant + expectedVersion func() SEVSNPVersionAPI + olderVersion func() SEVSNPVersionAPI + latestVersion func() SEVSNPVersionAPI }{ - "get latest version": { + "get latest version azure": { fetcherVersions: []string{latestStr, olderStr}, - want: latestVersion, + attestation: variant.AzureSEVSNP{}, + expectedVersion: func() SEVSNPVersionAPI { tmp := latestVersion; tmp.Variant = variant.AzureSEVSNP{}; return tmp }, + olderVersion: func() SEVSNPVersionAPI { tmp := olderVersion; tmp.Variant = variant.AzureSEVSNP{}; return tmp }, + latestVersion: func() SEVSNPVersionAPI { tmp := latestVersion; tmp.Variant = variant.AzureSEVSNP{}; return tmp }, + }, + "get latest version aws": { + fetcherVersions: []string{latestStr, olderStr}, + attestation: variant.AWSSEVSNP{}, + expectedVersion: func() SEVSNPVersionAPI { tmp := latestVersion; tmp.Variant = variant.AWSSEVSNP{}; return tmp }, + olderVersion: func() SEVSNPVersionAPI { tmp := olderVersion; tmp.Variant = variant.AWSSEVSNP{}; return tmp }, + latestVersion: func() SEVSNPVersionAPI { tmp := latestVersion; tmp.Variant = variant.AWSSEVSNP{}; return tmp }, }, } for name, tc := range testcases { t.Run(name, func(t *testing.T) { client := &http.Client{ Transport: &fakeConfigAPIHandler{ + attestation: tc.attestation, versions: tc.fetcherVersions, - latestVersion: latestStr, - olderVersion: olderStr, + latestDate: latestStr, + latestVersion: tc.latestVersion(), + olderDate: olderStr, + olderVersion: tc.olderVersion(), }, } fetcher := newFetcherWithClientAndVerifier(client, dummyVerifier{}, constants.CDNRepositoryURL) - res, err := fetcher.FetchAzureSEVSNPVersionLatest(context.Background()) + res, err := fetcher.FetchSEVSNPVersionLatest(context.Background(), tc.attestation) assert := assert.New(t) if tc.wantErr { assert.Error(err) } else { assert.NoError(err) - assert.Equal(tc.want, res) + assert.Equal(tc.expectedVersion(), res) } }) } } -var latestVersion = AzureSEVSNPVersionAPI{ - AzureSEVSNPVersion: AzureSEVSNPVersion{ +var latestVersion = SEVSNPVersionAPI{ + SEVSNPVersion: SEVSNPVersion{ Microcode: 93, TEE: 0, SNP: 6, @@ -65,8 +82,8 @@ var latestVersion = AzureSEVSNPVersionAPI{ }, } -var olderVersion = AzureSEVSNPVersionAPI{ - AzureSEVSNPVersion: AzureSEVSNPVersion{ +var olderVersion = SEVSNPVersionAPI{ + SEVSNPVersion: SEVSNPVersion{ Microcode: 1, TEE: 0, SNP: 1, @@ -75,14 +92,17 @@ var olderVersion = AzureSEVSNPVersionAPI{ } type fakeConfigAPIHandler struct { + attestation variant.Variant versions []string - latestVersion string - olderVersion string + latestDate string + latestVersion SEVSNPVersionAPI + olderDate string + olderVersion SEVSNPVersionAPI } // RoundTrip resolves the request and returns a dummy response. func (f *fakeConfigAPIHandler) RoundTrip(req *http.Request) (*http.Response, error) { - if req.URL.Path == "/constellation/v1/attestation/azure-sev-snp/list" { + if req.URL.Path == fmt.Sprintf("/constellation/v1/attestation/%s/list", f.attestation.String()) { res := &http.Response{} bt, err := json.Marshal(f.versions) if err != nil { @@ -93,9 +113,9 @@ func (f *fakeConfigAPIHandler) RoundTrip(req *http.Request) (*http.Response, err res.Header.Set("Content-Type", "application/json") res.StatusCode = http.StatusOK return res, nil - } else if req.URL.Path == fmt.Sprintf("/constellation/v1/attestation/azure-sev-snp/%s", f.latestVersion) { + } else if req.URL.Path == fmt.Sprintf("/constellation/v1/attestation/%s/%s", f.attestation.String(), f.latestDate) { res := &http.Response{} - bt, err := json.Marshal(latestVersion) + bt, err := json.Marshal(f.latestVersion) if err != nil { return nil, err } @@ -103,22 +123,22 @@ func (f *fakeConfigAPIHandler) RoundTrip(req *http.Request) (*http.Response, err res.StatusCode = http.StatusOK return res, nil - } else if req.URL.Path == fmt.Sprintf("/constellation/v1/attestation/azure-sev-snp/%s", f.olderVersion) { + } else if req.URL.Path == fmt.Sprintf("/constellation/v1/attestation/%s/%s", f.attestation.String(), f.olderDate) { res := &http.Response{} - bt, err := json.Marshal(olderVersion) + bt, err := json.Marshal(f.olderVersion) if err != nil { return nil, err } res.Body = io.NopCloser(bytes.NewReader(bt)) res.StatusCode = http.StatusOK return res, nil - } else if req.URL.Path == fmt.Sprintf("/constellation/v1/attestation/azure-sev-snp/%s.sig", f.latestVersion) { + } else if req.URL.Path == fmt.Sprintf("/constellation/v1/attestation/%s/%s.sig", f.attestation.String(), f.latestDate) { res := &http.Response{} res.Body = io.NopCloser(bytes.NewReader([]byte("null"))) res.StatusCode = http.StatusOK return res, nil - } else if req.URL.Path == fmt.Sprintf("/constellation/v1/attestation/azure-sev-snp/%s.sig", f.olderVersion) { + } else if req.URL.Path == fmt.Sprintf("/constellation/v1/attestation/%s/%s.sig", f.attestation.String(), f.olderDate) { res := &http.Response{} res.Body = io.NopCloser(bytes.NewReader([]byte("null"))) res.StatusCode = http.StatusOK diff --git a/internal/api/attestationconfigapi/reporter.go b/internal/api/attestationconfigapi/reporter.go index d9452d7b59..7d3e9ded7f 100644 --- a/internal/api/attestationconfigapi/reporter.go +++ b/internal/api/attestationconfigapi/reporter.go @@ -6,6 +6,8 @@ SPDX-License-Identifier: AGPL-3.0-only /* The reporter contains the logic to determine a latest version for Azure SEVSNP based on cached version values observed on CVM instances. +Some code in this file (e.g. listing cached files) does not rely on dedicated API objects and instead uses the AWS SDK directly, +for no other reason than original development speed. */ package attestationconfigapi @@ -28,25 +30,27 @@ import ( // cachedVersionsSubDir is the subdirectory in the bucket where the cached versions are stored. const cachedVersionsSubDir = "cached-versions" -var reportVersionDir = path.Join(attestationURLPath, variant.AzureSEVSNP{}.String(), cachedVersionsSubDir) - // ErrNoNewerVersion is returned if the input version is not newer than the latest API version. var ErrNoNewerVersion = errors.New("input version is not newer than latest API version") -// UploadAzureSEVSNPVersionLatest saves the given version to the cache, determines the smallest +func reportVersionDir(attestation variant.Variant) string { + return path.Join(attestationURLPath, attestation.String(), cachedVersionsSubDir) +} + +// UploadSEVSNPVersionLatest saves the given version to the cache, determines the smallest // TCB version in the cache among the last cacheWindowSize versions and updates // the latest version in the API if there is an update. // force can be used to bypass the validation logic against the cached versions. -func (c Client) UploadAzureSEVSNPVersionLatest(ctx context.Context, inputVersion, - latestAPIVersion AzureSEVSNPVersion, now time.Time, force bool, +func (c Client) UploadSEVSNPVersionLatest(ctx context.Context, attestation variant.Variant, inputVersion, + latestAPIVersion SEVSNPVersion, now time.Time, force bool, ) error { - if err := c.cacheAzureSEVSNPVersion(ctx, inputVersion, now); err != nil { + if err := c.cacheSEVSNPVersion(ctx, attestation, inputVersion, now); err != nil { return fmt.Errorf("reporting version: %w", err) } if force { - return c.uploadAzureSEVSNPVersion(ctx, inputVersion, now) + return c.uploadSEVSNPVersion(ctx, attestation, inputVersion, now) } - versionDates, err := c.listCachedVersions(ctx) + versionDates, err := c.listCachedVersions(ctx, attestation) if err != nil { return fmt.Errorf("list reported versions: %w", err) } @@ -54,7 +58,7 @@ func (c Client) UploadAzureSEVSNPVersionLatest(ctx context.Context, inputVersion c.s3Client.Logger.Warnf("Skipping version update, found %d, expected %d reported versions.", len(versionDates), c.cacheWindowSize) return nil } - minVersion, minDate, err := c.findMinVersion(ctx, versionDates) + minVersion, minDate, err := c.findMinVersion(ctx, attestation, versionDates) if err != nil { return fmt.Errorf("get minimal version: %w", err) } @@ -72,27 +76,27 @@ func (c Client) UploadAzureSEVSNPVersionLatest(ctx context.Context, inputVersion if err != nil { return fmt.Errorf("parsing date: %w", err) } - if err := c.uploadAzureSEVSNPVersion(ctx, minVersion, t); err != nil { + if err := c.uploadSEVSNPVersion(ctx, attestation, minVersion, t); err != nil { return fmt.Errorf("uploading version: %w", err) } c.s3Client.Logger.Infof("Successfully uploaded new Azure SEV-SNP version: %+v", minVersion) return nil } -// cacheAzureSEVSNPVersion uploads the latest observed version numbers of the Azure SEVSNP. This version is used to later report the latest version numbers to the API. -func (c Client) cacheAzureSEVSNPVersion(ctx context.Context, version AzureSEVSNPVersion, date time.Time) error { +// cacheSEVSNPVersion uploads the latest observed version numbers of the Azure SEVSNP. This version is used to later report the latest version numbers to the API. +func (c Client) cacheSEVSNPVersion(ctx context.Context, attestation variant.Variant, version SEVSNPVersion, date time.Time) error { dateStr := date.Format(VersionFormat) + ".json" res := putCmd{ - apiObject: reportedAzureSEVSNPVersionAPI{Version: dateStr, AzureSEVSNPVersion: version}, + apiObject: reportedSEVSNPVersionAPI{Version: dateStr, variant: attestation, SEVSNPVersion: version}, signer: c.signer, } return res.Execute(ctx, c.s3Client) } -func (c Client) listCachedVersions(ctx context.Context) ([]string, error) { +func (c Client) listCachedVersions(ctx context.Context, attestation variant.Variant) ([]string, error) { list, err := c.s3Client.ListObjectsV2(ctx, &s3.ListObjectsV2Input{ Bucket: aws.String(c.bucketID), - Prefix: aws.String(reportVersionDir), + Prefix: aws.String(reportVersionDir(attestation)), }) if err != nil { return nil, fmt.Errorf("list objects: %w", err) @@ -108,28 +112,30 @@ func (c Client) listCachedVersions(ctx context.Context) ([]string, error) { } // findMinVersion finds the minimal version of the given version dates among the latest values in the version window size. -func (c Client) findMinVersion(ctx context.Context, versionDates []string) (AzureSEVSNPVersion, string, error) { - var minimalVersion *AzureSEVSNPVersion +func (c Client) findMinVersion(ctx context.Context, attesation variant.Variant, versionDates []string) (SEVSNPVersion, string, error) { + var minimalVersion *SEVSNPVersion var minimalDate string sort.Sort(sort.Reverse(sort.StringSlice(versionDates))) // sort in reverse order to slice the latest versions versionDates = versionDates[:c.cacheWindowSize] sort.Strings(versionDates) // sort with oldest first to to take the minimal version with the oldest date for _, date := range versionDates { - obj, err := client.Fetch(ctx, c.s3Client, reportedAzureSEVSNPVersionAPI{Version: date + ".json"}) + obj, err := client.Fetch(ctx, c.s3Client, reportedSEVSNPVersionAPI{Version: date + ".json", variant: attesation}) if err != nil { - return AzureSEVSNPVersion{}, "", fmt.Errorf("get object: %w", err) + return SEVSNPVersion{}, "", fmt.Errorf("get object: %w", err) } + // Need to set this explicitly as the variant is not part of the marshalled JSON. + obj.variant = attesation if minimalVersion == nil { - minimalVersion = &obj.AzureSEVSNPVersion + minimalVersion = &obj.SEVSNPVersion minimalDate = date } else { - shouldUpdateMinimal, err := isInputNewerThanOtherVersion(*minimalVersion, obj.AzureSEVSNPVersion) + shouldUpdateMinimal, err := isInputNewerThanOtherVersion(*minimalVersion, obj.SEVSNPVersion) if err != nil { continue } if shouldUpdateMinimal { - minimalVersion = &obj.AzureSEVSNPVersion + minimalVersion = &obj.SEVSNPVersion minimalDate = date } } @@ -138,7 +144,7 @@ func (c Client) findMinVersion(ctx context.Context, versionDates []string) (Azur } // isInputNewerThanOtherVersion compares all version fields and returns true if any input field is newer. -func isInputNewerThanOtherVersion(input, other AzureSEVSNPVersion) (bool, error) { +func isInputNewerThanOtherVersion(input, other SEVSNPVersion) (bool, error) { if input == other { return false, nil } @@ -157,19 +163,20 @@ func isInputNewerThanOtherVersion(input, other AzureSEVSNPVersion) (bool, error) return true, nil } -// reportedAzureSEVSNPVersionAPI is the request to get the version information of the specific version in the config api. -type reportedAzureSEVSNPVersionAPI struct { - Version string `json:"-"` - AzureSEVSNPVersion +// reportedSEVSNPVersionAPI is the request to get the version information of the specific version in the config api. +type reportedSEVSNPVersionAPI struct { + Version string `json:"-"` + variant variant.Variant `json:"-"` + SEVSNPVersion } // JSONPath returns the path to the JSON file for the request to the config api. -func (i reportedAzureSEVSNPVersionAPI) JSONPath() string { - return path.Join(reportVersionDir, i.Version) +func (i reportedSEVSNPVersionAPI) JSONPath() string { + return path.Join(reportVersionDir(i.variant), i.Version) } // ValidateRequest validates the request. -func (i reportedAzureSEVSNPVersionAPI) ValidateRequest() error { +func (i reportedSEVSNPVersionAPI) ValidateRequest() error { if !strings.HasSuffix(i.Version, ".json") { return fmt.Errorf("version has no .json suffix") } @@ -177,6 +184,6 @@ func (i reportedAzureSEVSNPVersionAPI) ValidateRequest() error { } // Validate is a No-Op at the moment. -func (i reportedAzureSEVSNPVersionAPI) Validate() error { +func (i reportedSEVSNPVersionAPI) Validate() error { return nil } diff --git a/internal/api/attestationconfigapi/reporter_test.go b/internal/api/attestationconfigapi/reporter_test.go index bfca2f6455..ea37d2d2f7 100644 --- a/internal/api/attestationconfigapi/reporter_test.go +++ b/internal/api/attestationconfigapi/reporter_test.go @@ -11,8 +11,8 @@ import ( ) func TestIsInputNewerThanLatestAPI(t *testing.T) { - newTestCfg := func() AzureSEVSNPVersion { - return AzureSEVSNPVersion{ + newTestCfg := func() SEVSNPVersion { + return SEVSNPVersion{ Microcode: 93, TEE: 0, SNP: 6, @@ -21,13 +21,13 @@ func TestIsInputNewerThanLatestAPI(t *testing.T) { } testCases := map[string]struct { - latest AzureSEVSNPVersion - input AzureSEVSNPVersion + latest SEVSNPVersion + input SEVSNPVersion expect bool errMsg string }{ "input is older than latest": { - input: func(c AzureSEVSNPVersion) AzureSEVSNPVersion { + input: func(c SEVSNPVersion) SEVSNPVersion { c.Microcode-- return c }(newTestCfg()), @@ -36,7 +36,7 @@ func TestIsInputNewerThanLatestAPI(t *testing.T) { errMsg: "input Microcode version: 92 is older than latest API version: 93", }, "input has greater and smaller version field than latest": { - input: func(c AzureSEVSNPVersion) AzureSEVSNPVersion { + input: func(c SEVSNPVersion) SEVSNPVersion { c.Microcode++ c.Bootloader-- return c @@ -46,7 +46,7 @@ func TestIsInputNewerThanLatestAPI(t *testing.T) { errMsg: "input Bootloader version: 1 is older than latest API version: 2", }, "input is newer than latest": { - input: func(c AzureSEVSNPVersion) AzureSEVSNPVersion { + input: func(c SEVSNPVersion) SEVSNPVersion { c.TEE++ return c }(newTestCfg()), diff --git a/internal/api/attestationconfigapi/snp.go b/internal/api/attestationconfigapi/snp.go new file mode 100644 index 0000000000..dc53211a6c --- /dev/null +++ b/internal/api/attestationconfigapi/snp.go @@ -0,0 +1,113 @@ +/* +Copyright (c) Edgeless Systems GmbH + +SPDX-License-Identifier: AGPL-3.0-only +*/ + +package attestationconfigapi + +import ( + "encoding/json" + "fmt" + "path" + "sort" + "strings" + + "github.com/edgelesssys/constellation/v2/internal/attestation/variant" +) + +// attestationURLPath is the URL path to the attestation versions. +const attestationURLPath = "constellation/v1/attestation" + +// SEVSNPVersion tracks the latest version of each component of the Azure SEVSNP. +type SEVSNPVersion struct { + // Bootloader is the latest version of the Azure SEVSNP bootloader. + Bootloader uint8 `json:"bootloader"` + // TEE is the latest version of the Azure SEVSNP TEE. + TEE uint8 `json:"tee"` + // SNP is the latest version of the Azure SEVSNP SNP. + SNP uint8 `json:"snp"` + // Microcode is the latest version of the Azure SEVSNP microcode. + Microcode uint8 `json:"microcode"` +} + +// SEVSNPVersionAPI is the request to get the version information of the specific version in the config api. +// Because variant is not part of the marshalled JSON, fetcher and client methods need to fill the variant property. +// Once we switch to v2 of the API we should embed the variant in the object. +// That would remove the possibility of some fetcher/client code forgetting to set the variant. +type SEVSNPVersionAPI struct { + Version string `json:"-"` + Variant variant.Variant `json:"-"` + SEVSNPVersion +} + +// JSONPath returns the path to the JSON file for the request to the config api. +func (i SEVSNPVersionAPI) JSONPath() string { + return path.Join(attestationURLPath, i.Variant.String(), i.Version) +} + +// ValidateRequest validates the request. +func (i SEVSNPVersionAPI) ValidateRequest() error { + if !strings.HasSuffix(i.Version, ".json") { + return fmt.Errorf("version has no .json suffix") + } + return nil +} + +// Validate is a No-Op at the moment. +func (i SEVSNPVersionAPI) Validate() error { + return nil +} + +// SEVSNPVersionList is the request to list all versions in the config api. +// Because variant is not part of the marshalled JSON, fetcher and client methods need to fill the variant property. +// Once we switch to v2 of the API we could embed the variant in the object and remove some code from fetcher & client. +// That would remove the possibility of some fetcher/client code forgetting to set the variant. +type SEVSNPVersionList struct { + variant variant.Variant + list []string +} + +// MarshalJSON marshals the i's list property to JSON. +func (i SEVSNPVersionList) MarshalJSON() ([]byte, error) { + return json.Marshal(i.list) +} + +// UnmarshalJSON unmarshals a list of strings into i's list property. +func (i *SEVSNPVersionList) UnmarshalJSON(data []byte) error { + return json.Unmarshal(data, &i.list) +} + +// List returns i's list property. +func (i SEVSNPVersionList) List() []string { return i.list } + +// JSONPath returns the path to the JSON file for the request to the config api. +func (i SEVSNPVersionList) JSONPath() string { + return path.Join(attestationURLPath, i.variant.String(), "list") +} + +// ValidateRequest is a NoOp as there is no input. +func (i SEVSNPVersionList) ValidateRequest() error { + return nil +} + +// SortReverse sorts the list of versions in reverse order. +func (i *SEVSNPVersionList) SortReverse() { + sort.Sort(sort.Reverse(sort.StringSlice(i.list))) +} + +// addVersion adds new to i's list and sorts the element in descending order. +func (i *SEVSNPVersionList) addVersion(new string) { + i.list = append(i.list, new) + i.list = variant.RemoveDuplicate(i.list) + + i.SortReverse() +} + +// Validate validates the response. +func (i SEVSNPVersionList) Validate() error { + if len(i.list) < 1 { + return fmt.Errorf("no versions found in /list") + } + return nil +} diff --git a/internal/api/attestationconfigapi/snp_test.go b/internal/api/attestationconfigapi/snp_test.go new file mode 100644 index 0000000000..2fe3ea8c9d --- /dev/null +++ b/internal/api/attestationconfigapi/snp_test.go @@ -0,0 +1,77 @@ +/* +Copyright (c) Edgeless Systems GmbH + +SPDX-License-Identifier: AGPL-3.0-only +*/ + +package attestationconfigapi + +import ( + "testing" + + "github.com/edgelesssys/constellation/v2/internal/attestation/variant" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestSEVSNPVersionListMarshalUnmarshalJSON(t *testing.T) { + tests := map[string]struct { + input SEVSNPVersionList + output SEVSNPVersionList + wantDiff bool + }{ + "success": { + input: SEVSNPVersionList{list: []string{"v1", "v2"}}, + output: SEVSNPVersionList{list: []string{"v1", "v2"}}, + }, + "variant is lost": { + input: SEVSNPVersionList{list: []string{"v1", "v2"}, variant: variant.AzureSEVSNP{}}, + output: SEVSNPVersionList{list: []string{"v1", "v2"}}, + }, + "wrong order": { + input: SEVSNPVersionList{list: []string{"v1", "v2"}}, + output: SEVSNPVersionList{list: []string{"v2", "v1"}}, + wantDiff: true, + }, + } + + for name, tc := range tests { + t.Run(name, func(t *testing.T) { + inputRaw, err := tc.input.MarshalJSON() + require.NoError(t, err) + + var actual SEVSNPVersionList + err = actual.UnmarshalJSON(inputRaw) + require.NoError(t, err) + + if tc.wantDiff { + assert.NotEqual(t, tc.output, actual, "Objects are equal, expected unequal") + } else { + assert.Equal(t, tc.output, actual, "Objects are not equal, expected equal") + } + }) + } +} + +func TestSEVSNPVersionListAddVersion(t *testing.T) { + tests := map[string]struct { + versions []string + new string + expected []string + }{ + "success": { + versions: []string{"v1", "v2"}, + new: "v3", + expected: []string{"v3", "v2", "v1"}, + }, + } + + for name, tc := range tests { + t.Run(name, func(t *testing.T) { + v := SEVSNPVersionList{list: tc.versions} + v.addVersion(tc.new) + + assert.Equal(t, tc.expected, v.list) + }) + } +} diff --git a/internal/config/azure.go b/internal/config/azure.go index 00ad189ef9..4bab0e9f03 100644 --- a/internal/config/azure.go +++ b/internal/config/azure.go @@ -69,16 +69,16 @@ func (c AzureSEVSNP) EqualTo(old AttestationCfg) (bool, error) { // FetchAndSetLatestVersionNumbers fetches the latest version numbers from the configapi and sets them. func (c *AzureSEVSNP) FetchAndSetLatestVersionNumbers(ctx context.Context, fetcher attestationconfigapi.Fetcher) error { - versions, err := fetcher.FetchAzureSEVSNPVersionLatest(ctx) + versions, err := fetcher.FetchSEVSNPVersionLatest(ctx, variant.AzureSEVSNP{}) if err != nil { return err } // set number and keep isLatest flag - c.mergeWithLatestVersion(versions.AzureSEVSNPVersion) + c.mergeWithLatestVersion(versions.SEVSNPVersion) return nil } -func (c *AzureSEVSNP) mergeWithLatestVersion(latest attestationconfigapi.AzureSEVSNPVersion) { +func (c *AzureSEVSNP) mergeWithLatestVersion(latest attestationconfigapi.SEVSNPVersion) { if c.BootloaderVersion.WantLatest { c.BootloaderVersion.Value = latest.Bootloader } diff --git a/internal/config/config_test.go b/internal/config/config_test.go index 7c4c5c0159..9f31e6957c 100644 --- a/internal/config/config_test.go +++ b/internal/config/config_test.go @@ -23,6 +23,7 @@ import ( "github.com/edgelesssys/constellation/v2/internal/api/attestationconfigapi" "github.com/edgelesssys/constellation/v2/internal/attestation/measurements" + "github.com/edgelesssys/constellation/v2/internal/attestation/variant" "github.com/edgelesssys/constellation/v2/internal/cloud/cloudprovider" "github.com/edgelesssys/constellation/v2/internal/config/instancetypes" "github.com/edgelesssys/constellation/v2/internal/constants" @@ -51,10 +52,10 @@ func TestDefaultConfigMarshalsLatestVersion(t *testing.T) { var mp configMap require.NoError(yaml.Unmarshal(bt, &mp)) assert := assert.New(t) - assert.Equal("latest", mp.getAzureSEVSNPVersion("microcodeVersion")) - assert.Equal("latest", mp.getAzureSEVSNPVersion("teeVersion")) - assert.Equal("latest", mp.getAzureSEVSNPVersion("snpVersion")) - assert.Equal("latest", mp.getAzureSEVSNPVersion("bootloaderVersion")) + assert.Equal("latest", mp.getSEVSNPVersion("microcodeVersion")) + assert.Equal("latest", mp.getSEVSNPVersion("teeVersion")) + assert.Equal("latest", mp.getSEVSNPVersion("snpVersion")) + assert.Equal("latest", mp.getSEVSNPVersion("bootloaderVersion")) } func TestGetAttestationConfigMarshalsNumericalVersion(t *testing.T) { @@ -88,9 +89,9 @@ func TestNew(t *testing.T) { conf := Default() // default configures latest version modifyConfigForAzureToPassValidate(conf) m := getConfigAsMap(conf, t) - m.setAzureSEVSNPVersion("microcodeVersion", "Latest") // check uppercase also works - m.setAzureSEVSNPVersion("teeVersion", 2) - m.setAzureSEVSNPVersion("bootloaderVersion", 1) + m.setSEVSNPVersion("microcodeVersion", "Latest") // check uppercase also works + m.setSEVSNPVersion("teeVersion", 2) + m.setSEVSNPVersion("bootloaderVersion", 1) return m }(), @@ -181,7 +182,7 @@ func TestReadConfigFile(t *testing.T) { config: func() configMap { conf := Default() m := getConfigAsMap(conf, t) - m.setAzureSEVSNPVersion("microcodeVersion", "1a") + m.setSEVSNPVersion("microcodeVersion", "1a") return m }(), configName: constants.ConfigFilename, @@ -1053,7 +1054,7 @@ func TestIsAppClientIDError(t *testing.T) { // configMap is used to un-/marshal the config as an unstructured map. type configMap map[string]interface{} -func (c configMap) setAzureSEVSNPVersion(versionType string, value interface{}) { +func (c configMap) setSEVSNPVersion(versionType string, value interface{}) { c["attestation"].(configMap)["azureSEVSNP"].(configMap)[versionType] = value } @@ -1061,7 +1062,7 @@ func (c configMap) setAzureProvider(azureProviderField string, value interface{} c["provider"].(configMap)["azure"].(configMap)[azureProviderField] = value } -func (c configMap) getAzureSEVSNPVersion(versionType string) interface{} { +func (c configMap) getSEVSNPVersion(versionType string) interface{} { return c["attestation"].(configMap)["azureSEVSNP"].(configMap)[versionType] } @@ -1079,25 +1080,23 @@ func getConfigAsMap(conf *Config, t *testing.T) (res configMap) { type stubAttestationFetcher struct{} -func (f stubAttestationFetcher) FetchAzureSEVSNPVersionList(_ context.Context, _ attestationconfigapi.AzureSEVSNPVersionList) (attestationconfigapi.AzureSEVSNPVersionList, error) { - return attestationconfigapi.AzureSEVSNPVersionList( - []string{}, - ), nil +func (f stubAttestationFetcher) FetchSEVSNPVersionList(_ context.Context, _ attestationconfigapi.SEVSNPVersionList) (attestationconfigapi.SEVSNPVersionList, error) { + return attestationconfigapi.SEVSNPVersionList{}, nil } -func (f stubAttestationFetcher) FetchAzureSEVSNPVersion(_ context.Context, _ attestationconfigapi.AzureSEVSNPVersionAPI) (attestationconfigapi.AzureSEVSNPVersionAPI, error) { - return attestationconfigapi.AzureSEVSNPVersionAPI{ - AzureSEVSNPVersion: testCfg, +func (f stubAttestationFetcher) FetchSEVSNPVersion(_ context.Context, _ attestationconfigapi.SEVSNPVersionAPI) (attestationconfigapi.SEVSNPVersionAPI, error) { + return attestationconfigapi.SEVSNPVersionAPI{ + SEVSNPVersion: testCfg, }, nil } -func (f stubAttestationFetcher) FetchAzureSEVSNPVersionLatest(_ context.Context) (attestationconfigapi.AzureSEVSNPVersionAPI, error) { - return attestationconfigapi.AzureSEVSNPVersionAPI{ - AzureSEVSNPVersion: testCfg, +func (f stubAttestationFetcher) FetchSEVSNPVersionLatest(_ context.Context, _ variant.Variant) (attestationconfigapi.SEVSNPVersionAPI, error) { + return attestationconfigapi.SEVSNPVersionAPI{ + SEVSNPVersion: testCfg, }, nil } -var testCfg = attestationconfigapi.AzureSEVSNPVersion{ +var testCfg = attestationconfigapi.SEVSNPVersion{ Microcode: 93, TEE: 0, SNP: 6, From 242c8dddc637de67e3a831b9bd9a863360dff0b4 Mon Sep 17 00:00:00 2001 From: Otto Bittner Date: Wed, 8 Nov 2023 13:54:18 +0100 Subject: [PATCH 08/12] config: fetch latest AWS TCB values --- internal/config/attestation.go | 2 +- internal/config/aws.go | 39 +++++++++++++++++++++++++++++++++- internal/config/config.go | 6 ++++++ 3 files changed, 45 insertions(+), 2 deletions(-) diff --git a/internal/config/attestation.go b/internal/config/attestation.go index a80d069db8..7821a63b57 100644 --- a/internal/config/attestation.go +++ b/internal/config/attestation.go @@ -68,7 +68,7 @@ func unmarshalTypedConfig[T AttestationCfg](data []byte) (AttestationCfg, error) // Certificate is a wrapper around x509.Certificate allowing custom marshaling. type Certificate x509.Certificate -// Equal returns true if the certificates are equal. +// Equal returns true if the embedded Raw values are equal. func (c Certificate) Equal(other Certificate) bool { return bytes.Equal(c.Raw, other.Raw) } diff --git a/internal/config/aws.go b/internal/config/aws.go index 01a0843ce4..65fa0001c8 100644 --- a/internal/config/aws.go +++ b/internal/config/aws.go @@ -6,8 +6,11 @@ SPDX-License-Identifier: AGPL-3.0-only package config import ( + "bytes" + "context" "fmt" + "github.com/edgelesssys/constellation/v2/internal/api/attestationconfigapi" "github.com/edgelesssys/constellation/v2/internal/attestation/measurements" "github.com/edgelesssys/constellation/v2/internal/attestation/variant" "github.com/edgelesssys/constellation/v2/internal/cloud/cloudprovider" @@ -47,7 +50,41 @@ func (c AWSSEVSNP) EqualTo(other AttestationCfg) (bool, error) { return false, fmt.Errorf("cannot compare %T with %T", c, other) } - return c.Measurements.EqualTo(otherCfg.Measurements), nil + measurementsEqual := c.Measurements.EqualTo(otherCfg.Measurements) + bootloaderEqual := c.BootloaderVersion == otherCfg.BootloaderVersion + teeEqual := c.TEEVersion == otherCfg.TEEVersion + snpEqual := c.SNPVersion == otherCfg.SNPVersion + microcodeEqual := c.MicrocodeVersion == otherCfg.MicrocodeVersion + rootKeyEqual := bytes.Equal(c.AMDRootKey.Raw, otherCfg.AMDRootKey.Raw) + signingKeyEqual := bytes.Equal(c.AMDSigningKey.Raw, otherCfg.AMDSigningKey.Raw) + + return measurementsEqual && bootloaderEqual && teeEqual && snpEqual && microcodeEqual && rootKeyEqual && signingKeyEqual, nil +} + +// FetchAndSetLatestVersionNumbers fetches the latest version numbers from the configapi and sets them. +func (c *AWSSEVSNP) FetchAndSetLatestVersionNumbers(ctx context.Context, fetcher attestationconfigapi.Fetcher) error { + versions, err := fetcher.FetchSEVSNPVersionLatest(ctx, variant.AWSSEVSNP{}) + if err != nil { + return err + } + // set number and keep isLatest flag + c.mergeWithLatestVersion(versions.SEVSNPVersion) + return nil +} + +func (c *AWSSEVSNP) mergeWithLatestVersion(latest attestationconfigapi.SEVSNPVersion) { + if c.BootloaderVersion.WantLatest { + c.BootloaderVersion.Value = latest.Bootloader + } + if c.TEEVersion.WantLatest { + c.TEEVersion.Value = latest.TEE + } + if c.SNPVersion.WantLatest { + c.SNPVersion.Value = latest.SNP + } + if c.MicrocodeVersion.WantLatest { + c.MicrocodeVersion.Value = latest.Microcode + } } // GetVariant returns aws-nitro-tpm as the variant. diff --git a/internal/config/config.go b/internal/config/config.go index 976bd86b17..96af3578d3 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -463,6 +463,12 @@ func New(fileHandler file.Handler, name string, fetcher attestationconfigapi.Fet } } + if aws := c.Attestation.AWSSEVSNP; aws != nil { + if err := aws.FetchAndSetLatestVersionNumbers(context.Background(), fetcher); err != nil { + return c, err + } + } + // Read secrets from env-vars. clientSecretValue := os.Getenv(constants.EnvVarAzureClientSecretValue) if clientSecretValue != "" && c.Provider.Azure != nil { From 2d8c4478ab4a1984d9f8c7720cb272ca311268d1 Mon Sep 17 00:00:00 2001 From: Otto Bittner Date: Tue, 14 Nov 2023 13:24:25 +0100 Subject: [PATCH 09/12] api: add support to upload AWS TCB values The attestationconfig api CLI now uploads SNP TCB versions for AWS. --- .../api/attestationconfigapi/cli/BUILD.bazel | 11 +- internal/api/attestationconfigapi/cli/aws.go | 15 +- .../api/attestationconfigapi/cli/azure.go | 56 +---- .../api/attestationconfigapi/cli/delete.go | 119 ++++----- .../attestationconfigapi/cli/delete_test.go | 6 - .../attestationconfigapi/cli/e2e/test.sh.in | 2 +- internal/api/attestationconfigapi/cli/main.go | 165 +------------ .../api/attestationconfigapi/cli/main_test.go | 6 - .../api/attestationconfigapi/cli/upload.go | 228 ++++++++++++++++++ internal/api/attestationconfigapi/reporter.go | 2 +- internal/api/attestationconfigapi/snp.go | 8 +- 11 files changed, 316 insertions(+), 302 deletions(-) delete mode 100644 internal/api/attestationconfigapi/cli/delete_test.go delete mode 100644 internal/api/attestationconfigapi/cli/main_test.go create mode 100644 internal/api/attestationconfigapi/cli/upload.go diff --git a/internal/api/attestationconfigapi/cli/BUILD.bazel b/internal/api/attestationconfigapi/cli/BUILD.bazel index 0eb5017b87..c235cfd7cd 100644 --- a/internal/api/attestationconfigapi/cli/BUILD.bazel +++ b/internal/api/attestationconfigapi/cli/BUILD.bazel @@ -1,5 +1,4 @@ load("@io_bazel_rules_go//go:def.bzl", "go_binary", "go_library") -load("//bazel/go:go_test.bzl", "go_test") load("//bazel/sh:def.bzl", "sh_template") go_binary( @@ -15,6 +14,7 @@ go_library( "azure.go", "delete.go", "main.go", + "upload.go", "validargs.go", ], importpath = "github.com/edgelesssys/constellation/v2/internal/api/attestationconfigapi/cli", @@ -37,15 +37,6 @@ go_library( ], ) -go_test( - name = "cli_test", - srcs = [ - "delete_test.go", - "main_test.go", - ], - embed = [":cli_lib"], -) - sh_template( name = "cli_e2e_test", data = [":cli"], diff --git a/internal/api/attestationconfigapi/cli/aws.go b/internal/api/attestationconfigapi/cli/aws.go index ac7fe39102..578caadc4f 100644 --- a/internal/api/attestationconfigapi/cli/aws.go +++ b/internal/api/attestationconfigapi/cli/aws.go @@ -8,16 +8,17 @@ package main import ( "context" + "fmt" "github.com/edgelesssys/constellation/v2/internal/api/attestationconfigapi" - "github.com/edgelesssys/constellation/v2/internal/file" - "github.com/edgelesssys/constellation/v2/internal/logger" + "github.com/edgelesssys/constellation/v2/internal/attestation/variant" + "github.com/edgelesssys/constellation/v2/internal/cloud/cloudprovider" ) -func uploadAWS(_ context.Context, _ *attestationconfigapi.Client, _ uploadConfig, _ file.Handler, _ *logger.Logger) error { - return nil -} +func deleteAWS(ctx context.Context, client *attestationconfigapi.Client, cfg deleteConfig) error { + if cfg.provider != cloudprovider.AWS || cfg.kind != snpReport { + return fmt.Errorf("provider %s and kind %s not supported", cfg.provider, cfg.kind) + } -func deleteAWS(_ context.Context, _ *attestationconfigapi.Client, _ deleteConfig) error { - return nil + return client.DeleteSEVSNPVersion(ctx, variant.AWSSEVSNP{}, cfg.version) } diff --git a/internal/api/attestationconfigapi/cli/azure.go b/internal/api/attestationconfigapi/cli/azure.go index 32555bc4f2..924fbe19e9 100644 --- a/internal/api/attestationconfigapi/cli/azure.go +++ b/internal/api/attestationconfigapi/cli/azure.go @@ -8,7 +8,6 @@ package main import ( "context" - "errors" "fmt" "github.com/aws/aws-sdk-go-v2/service/s3" @@ -17,65 +16,18 @@ import ( "github.com/edgelesssys/constellation/v2/internal/api/attestationconfigapi" "github.com/edgelesssys/constellation/v2/internal/attestation/variant" "github.com/edgelesssys/constellation/v2/internal/cloud/cloudprovider" - "github.com/edgelesssys/constellation/v2/internal/file" - "github.com/edgelesssys/constellation/v2/internal/logger" "github.com/edgelesssys/constellation/v2/internal/staticupload" - "github.com/edgelesssys/constellation/v2/internal/verify" ) -func uploadAzure(ctx context.Context, client *attestationconfigapi.Client, cfg uploadConfig, fs file.Handler, log *logger.Logger) error { - if cfg.kind != snpReport { - return fmt.Errorf("kind %s not supported", cfg.kind) - } - - log.Infof("Reading SNP report from file: %s", cfg.path) - var report verify.Report - if err := fs.ReadJSON(cfg.path, &report); err != nil { - return fmt.Errorf("reading snp report: %w", err) - } - - inputVersion := convertTCBVersionToAzureVersion(report.SNPReport.LaunchTCB) - log.Infof("Input report: %+v", inputVersion) - - latestAPIVersionAPI, err := attestationconfigapi.NewFetcherWithCustomCDNAndCosignKey(cfg.url, cfg.cosignPublicKey).FetchSEVSNPVersionLatest(ctx, variant.AzureSEVSNP{}) - if err != nil { - if errors.Is(err, attestationconfigapi.ErrNoVersionsFound) { - log.Infof("No versions found in API, but assuming that we are uploading the first version.") - } else { - return fmt.Errorf("fetching latest version: %w", err) - } - } - latestAPIVersion := latestAPIVersionAPI.SEVSNPVersion - if err := client.UploadSEVSNPVersionLatest(ctx, variant.AzureSEVSNP{}, inputVersion, latestAPIVersion, cfg.uploadDate, cfg.force); err != nil { - if errors.Is(err, attestationconfigapi.ErrNoNewerVersion) { - log.Infof("Input version: %+v is not newer than latest API version: %+v", inputVersion, latestAPIVersion) - return nil - } - return fmt.Errorf("updating latest version: %w", err) - } - - return nil -} - -func convertTCBVersionToAzureVersion(tcb verify.TCBVersion) attestationconfigapi.SEVSNPVersion { - return attestationconfigapi.SEVSNPVersion{ - Bootloader: tcb.Bootloader, - TEE: tcb.TEE, - SNP: tcb.SNP, - Microcode: tcb.Microcode, - } -} - func deleteAzure(ctx context.Context, client *attestationconfigapi.Client, cfg deleteConfig) error { - if cfg.provider == cloudprovider.Azure && cfg.kind == snpReport { - return client.DeleteSEVSNPVersion(ctx, variant.AzureSEVSNP{}, cfg.version) + if cfg.provider != cloudprovider.Azure && cfg.kind != snpReport { + return fmt.Errorf("provider %s and kind %s not supported", cfg.provider, cfg.kind) } - return fmt.Errorf("provider %s and kind %s not supported", cfg.provider, cfg.kind) + return client.DeleteSEVSNPVersion(ctx, variant.AzureSEVSNP{}, cfg.version) } -func deleteRecursiveAzure(ctx context.Context, client *staticupload.Client, cfg deleteConfig) error { - path := "constellation/v1/attestation/azure-sev-snp" +func deleteRecursive(ctx context.Context, path string, client *staticupload.Client, cfg deleteConfig) error { resp, err := client.ListObjectsV2(ctx, &s3.ListObjectsV2Input{ Bucket: aws.String(cfg.bucket), Prefix: aws.String(path), diff --git a/internal/api/attestationconfigapi/cli/delete.go b/internal/api/attestationconfigapi/cli/delete.go index 749cc2bfb9..faedabd118 100644 --- a/internal/api/attestationconfigapi/cli/delete.go +++ b/internal/api/attestationconfigapi/cli/delete.go @@ -8,8 +8,10 @@ package main import ( "errors" "fmt" + "path" "github.com/edgelesssys/constellation/v2/internal/api/attestationconfigapi" + "github.com/edgelesssys/constellation/v2/internal/attestation/variant" "github.com/edgelesssys/constellation/v2/internal/cloud/cloudprovider" "github.com/edgelesssys/constellation/v2/internal/logger" "github.com/edgelesssys/constellation/v2/internal/staticupload" @@ -21,19 +23,21 @@ import ( func newDeleteCmd() *cobra.Command { cmd := &cobra.Command{ Use: "delete {azure|aws} {snp-report|guest-firmware} ", - Short: "Upload an object to the attestationconfig API", + Short: "Delete an object from the attestationconfig API", Long: "Delete a specific object version from the config api. is the name of the object to delete (without .json suffix)", + Example: "COSIGN_PASSWORD=$CPW COSIGN_PRIVATE_KEY=$CKEY cli delete azure snp-report 1.0.0", Args: cobra.MatchAll(cobra.ExactArgs(3), isCloudProvider(0), isValidKind(1)), PreRunE: envCheck, RunE: runDelete, } recursivelyCmd := &cobra.Command{ - Use: "recursive {azure|aws} {snp-report|guest-firmware}", - Short: "delete all objects from the API path constellation/v1/attestation/azure-sev-snp", - Long: "Currently only implemented for azure & snp-report. Delete all objects from the API path constellation/v1/attestation/azure-sev-snp", - Args: cobra.MatchAll(cobra.ExactArgs(2), isCloudProvider(0), isValidKind(1)), - RunE: runRecursiveDelete, + Use: "recursive {azure|aws}", + Short: "delete all objects from the API path constellation/v1/attestation/", + Long: "Delete all objects from the API path constellation/v1/attestation/", + Example: "COSIGN_PASSWORD=$CPW COSIGN_PRIVATE_KEY=$CKEY cli delete recursive azure", + Args: cobra.MatchAll(cobra.ExactArgs(1), isCloudProvider(0)), + RunE: runRecursiveDelete, } cmd.AddCommand(recursivelyCmd) @@ -41,50 +45,6 @@ func newDeleteCmd() *cobra.Command { return cmd } -type deleteConfig struct { - provider cloudprovider.Provider - kind objectKind - version string - region string - bucket string - url string - distribution string - cosignPublicKey string -} - -func newDeleteConfig(cmd *cobra.Command, args [3]string) (deleteConfig, error) { - region, err := cmd.Flags().GetString("region") - if err != nil { - return deleteConfig{}, fmt.Errorf("getting region: %w", err) - } - - bucket, err := cmd.Flags().GetString("bucket") - if err != nil { - return deleteConfig{}, fmt.Errorf("getting bucket: %w", err) - } - - testing, err := cmd.Flags().GetBool("testing") - if err != nil { - return deleteConfig{}, fmt.Errorf("getting testing flag: %w", err) - } - apiCfg := getAPIEnvironment(testing) - - provider := cloudprovider.FromString(args[0]) - kind := kindFromString(args[1]) - version := args[2] - - return deleteConfig{ - provider: provider, - kind: kind, - version: version, - region: region, - bucket: bucket, - url: apiCfg.url, - distribution: apiCfg.distribution, - cosignPublicKey: apiCfg.cosignPublicKey, - }, nil -} - func runDelete(cmd *cobra.Command, args []string) (retErr error) { log := logger.New(logger.PlainLog, zap.DebugLevel).Named("attestationconfigapi") @@ -121,7 +81,8 @@ func runDelete(cmd *cobra.Command, args []string) (retErr error) { } func runRecursiveDelete(cmd *cobra.Command, args []string) (retErr error) { - // newDeleteConfig expects 3 args, so we pass "all" for the version argument. + // newDeleteConfig expects 3 args, so we pass "all" for the version argument and "snp-report" as kind. + args = append(args, "snp-report") args = append(args, "all") deleteCfg, err := newDeleteConfig(cmd, ([3]string)(args[:3])) if err != nil { @@ -144,9 +105,59 @@ func runRecursiveDelete(cmd *cobra.Command, args []string) (retErr error) { } }() - if deleteCfg.provider != cloudprovider.Azure || deleteCfg.kind != snpReport { - return fmt.Errorf("provider %s and kind %s not supported", deleteCfg.provider, deleteCfg.kind) + var deletePath string + switch deleteCfg.provider { + case cloudprovider.AWS: + deletePath = path.Join(attestationconfigapi.AttestationURLPath, variant.AWSSEVSNP{}.String()) + case cloudprovider.Azure: + deletePath = path.Join(attestationconfigapi.AttestationURLPath, variant.AzureSEVSNP{}.String()) + default: + return fmt.Errorf("unsupported cloud provider: %s", deleteCfg.provider) + } + + return deleteRecursive(cmd.Context(), deletePath, client, deleteCfg) +} + +type deleteConfig struct { + provider cloudprovider.Provider + kind objectKind + version string + region string + bucket string + url string + distribution string + cosignPublicKey string +} + +func newDeleteConfig(cmd *cobra.Command, args [3]string) (deleteConfig, error) { + region, err := cmd.Flags().GetString("region") + if err != nil { + return deleteConfig{}, fmt.Errorf("getting region: %w", err) + } + + bucket, err := cmd.Flags().GetString("bucket") + if err != nil { + return deleteConfig{}, fmt.Errorf("getting bucket: %w", err) + } + + testing, err := cmd.Flags().GetBool("testing") + if err != nil { + return deleteConfig{}, fmt.Errorf("getting testing flag: %w", err) } + apiCfg := getAPIEnvironment(testing) - return deleteRecursiveAzure(cmd.Context(), client, deleteCfg) + provider := cloudprovider.FromString(args[0]) + kind := kindFromString(args[1]) + version := args[2] + + return deleteConfig{ + provider: provider, + kind: kind, + version: version, + region: region, + bucket: bucket, + url: apiCfg.url, + distribution: apiCfg.distribution, + cosignPublicKey: apiCfg.cosignPublicKey, + }, nil } diff --git a/internal/api/attestationconfigapi/cli/delete_test.go b/internal/api/attestationconfigapi/cli/delete_test.go deleted file mode 100644 index 2a483e9356..0000000000 --- a/internal/api/attestationconfigapi/cli/delete_test.go +++ /dev/null @@ -1,6 +0,0 @@ -/* -Copyright (c) Edgeless Systems GmbH - -SPDX-License-Identifier: AGPL-3.0-only -*/ -package main diff --git a/internal/api/attestationconfigapi/cli/e2e/test.sh.in b/internal/api/attestationconfigapi/cli/e2e/test.sh.in index 013a7ae77d..baf4fc4699 100755 --- a/internal/api/attestationconfigapi/cli/e2e/test.sh.in +++ b/internal/api/attestationconfigapi/cli/e2e/test.sh.in @@ -28,7 +28,7 @@ readonly tmpdir registerExitHandler "rm -rf $tmpdir" # empty the bucket version state -${configapi_cli} delete recursive azure snp-report --region "$region" --bucket "$bucket" +${configapi_cli} delete recursive azure --region "$region" --bucket "$bucket" # the high version numbers ensure that it's newer than the current latest value readonly current_report_path="$tmpdir/currentSnpReport.json" diff --git a/internal/api/attestationconfigapi/cli/main.go b/internal/api/attestationconfigapi/cli/main.go index cc4ef74c7f..e6e951f1b1 100644 --- a/internal/api/attestationconfigapi/cli/main.go +++ b/internal/api/attestationconfigapi/cli/main.go @@ -15,20 +15,10 @@ Any version update is then pushed to the API. package main import ( - "errors" - "fmt" "os" - "time" - "github.com/edgelesssys/constellation/v2/internal/api/attestationconfigapi" - "github.com/edgelesssys/constellation/v2/internal/cloud/cloudprovider" "github.com/edgelesssys/constellation/v2/internal/constants" - "github.com/edgelesssys/constellation/v2/internal/file" - "github.com/edgelesssys/constellation/v2/internal/logger" - "github.com/edgelesssys/constellation/v2/internal/staticupload" - "github.com/spf13/afero" "github.com/spf13/cobra" - "go.uber.org/zap" ) const ( @@ -56,7 +46,10 @@ func main() { // newRootCmd creates the root command. func newRootCmd() *cobra.Command { - rootCmd := &cobra.Command{} + rootCmd := &cobra.Command{ + Short: "CLI to interact with the attestationconfig API", + Long: "CLI to interact with the attestationconfig API. Allows uploading new TCB versions, deleting specific versions and deleting all versions. Uploaded objects are signed with cosign.", + } rootCmd.PersistentFlags().StringP("region", "r", awsRegion, "region of the targeted bucket.") rootCmd.PersistentFlags().StringP("bucket", "b", awsBucket, "bucket targeted by all operations.") rootCmd.PersistentFlags().Bool("testing", false, "upload to S3 test bucket.") @@ -67,156 +60,6 @@ func newRootCmd() *cobra.Command { return rootCmd } -func newUploadCmd() *cobra.Command { - uploadCmd := &cobra.Command{ - Use: "upload {azure|aws} {snp-report|guest-firmware} ", - Short: "Upload an object to the attestationconfig API", - - Long: fmt.Sprintf("Upload a new object to the attestationconfig API. For snp-reports the new object is added to a cache folder first."+ - "The CLI then determines the lowest version within the cache-window present in the cache and writes that value to the config api if necessary. "+ - "For guest-firmware objects the object is added to the API directly. "+ - "Please authenticate with AWS through your preferred method (e.g. environment variables, CLI)"+ - "to be able to upload to S3. Set the %s and %s environment variables to authenticate with cosign.", - envCosignPrivateKey, envCosignPwd, - ), - - Args: cobra.MatchAll(cobra.ExactArgs(3), isCloudProvider(0), isValidKind(1)), - PreRunE: envCheck, - RunE: runUpload, - } - uploadCmd.Flags().StringP("upload-date", "d", "", "upload a version with this date as version name.") - uploadCmd.Flags().BoolP("force", "f", false, "Use force to manually push a new latest version."+ - " The version gets saved to the cache but the version selection logic is skipped.") - uploadCmd.Flags().IntP("cache-window-size", "s", versionWindowSize, "Number of versions to be considered for the latest version.") - - return uploadCmd -} - -func envCheck(_ *cobra.Command, _ []string) error { - if os.Getenv(envCosignPrivateKey) == "" || os.Getenv(envCosignPwd) == "" { - return fmt.Errorf("please set both %s and %s environment variables", envCosignPrivateKey, envCosignPwd) - } - cosignPwd = os.Getenv(envCosignPwd) - privateKey = os.Getenv(envCosignPrivateKey) - return nil -} - -func runUpload(cmd *cobra.Command, args []string) (retErr error) { - ctx := cmd.Context() - log := logger.New(logger.PlainLog, zap.DebugLevel).Named("attestationconfigapi") - - log.Infof("%s", args) - uploadCfg, err := newConfig(cmd, ([3]string)(args[:3])) - if err != nil { - return fmt.Errorf("parsing cli flags: %w", err) - } - - client, clientClose, err := attestationconfigapi.NewClient( - ctx, - staticupload.Config{ - Bucket: uploadCfg.bucket, - Region: uploadCfg.region, - DistributionID: uploadCfg.distribution, - }, - []byte(cosignPwd), - []byte(privateKey), - false, - uploadCfg.cacheWindowSize, - log) - - defer func() { - err := clientClose(cmd.Context()) - if err != nil { - retErr = errors.Join(retErr, fmt.Errorf("failed to invalidate cache: %w", err)) - } - }() - - if err != nil { - return fmt.Errorf("creating client: %w", err) - } - - switch uploadCfg.provider { - case cloudprovider.AWS: - return uploadAWS(ctx, client, uploadCfg, file.NewHandler(afero.NewOsFs()), log) - case cloudprovider.Azure: - return uploadAzure(ctx, client, uploadCfg, file.NewHandler(afero.NewOsFs()), log) - default: - return fmt.Errorf("unsupported cloud provider: %s", uploadCfg.provider) - } -} - -type uploadConfig struct { - provider cloudprovider.Provider - kind objectKind - path string - uploadDate time.Time - cosignPublicKey string - region string - bucket string - distribution string - url string - force bool - cacheWindowSize int -} - -func newConfig(cmd *cobra.Command, args [3]string) (uploadConfig, error) { - dateStr, err := cmd.Flags().GetString("upload-date") - if err != nil { - return uploadConfig{}, fmt.Errorf("getting upload date: %w", err) - } - uploadDate := time.Now() - if dateStr != "" { - uploadDate, err = time.Parse(attestationconfigapi.VersionFormat, dateStr) - if err != nil { - return uploadConfig{}, fmt.Errorf("parsing date: %w", err) - } - } - - region, err := cmd.Flags().GetString("region") - if err != nil { - return uploadConfig{}, fmt.Errorf("getting region: %w", err) - } - - bucket, err := cmd.Flags().GetString("bucket") - if err != nil { - return uploadConfig{}, fmt.Errorf("getting bucket: %w", err) - } - - testing, err := cmd.Flags().GetBool("testing") - if err != nil { - return uploadConfig{}, fmt.Errorf("getting testing flag: %w", err) - } - apiCfg := getAPIEnvironment(testing) - - force, err := cmd.Flags().GetBool("force") - if err != nil { - return uploadConfig{}, fmt.Errorf("getting force: %w", err) - } - - cacheWindowSize, err := cmd.Flags().GetInt("cache-window-size") - if err != nil { - return uploadConfig{}, fmt.Errorf("getting cache window size: %w", err) - } - - provider := cloudprovider.FromString(args[0]) - kind := kindFromString(args[1]) - path := args[2] - - return uploadConfig{ - provider: provider, - kind: kind, - path: path, - uploadDate: uploadDate, - cosignPublicKey: apiCfg.cosignPublicKey, - region: region, - bucket: bucket, - url: apiCfg.url, - distribution: apiCfg.distribution, - force: force, - cacheWindowSize: cacheWindowSize, - }, nil -} - type apiConfig struct { url string distribution string diff --git a/internal/api/attestationconfigapi/cli/main_test.go b/internal/api/attestationconfigapi/cli/main_test.go deleted file mode 100644 index 2a483e9356..0000000000 --- a/internal/api/attestationconfigapi/cli/main_test.go +++ /dev/null @@ -1,6 +0,0 @@ -/* -Copyright (c) Edgeless Systems GmbH - -SPDX-License-Identifier: AGPL-3.0-only -*/ -package main diff --git a/internal/api/attestationconfigapi/cli/upload.go b/internal/api/attestationconfigapi/cli/upload.go new file mode 100644 index 0000000000..831f99da7d --- /dev/null +++ b/internal/api/attestationconfigapi/cli/upload.go @@ -0,0 +1,228 @@ +/* +Copyright (c) Edgeless Systems GmbH + +SPDX-License-Identifier: AGPL-3.0-only +*/ +package main + +import ( + "context" + "errors" + "fmt" + "os" + "time" + + "github.com/edgelesssys/constellation/v2/internal/api/attestationconfigapi" + "github.com/edgelesssys/constellation/v2/internal/attestation/variant" + "github.com/edgelesssys/constellation/v2/internal/cloud/cloudprovider" + "github.com/edgelesssys/constellation/v2/internal/file" + "github.com/edgelesssys/constellation/v2/internal/logger" + "github.com/edgelesssys/constellation/v2/internal/staticupload" + "github.com/edgelesssys/constellation/v2/internal/verify" + "github.com/spf13/afero" + "github.com/spf13/cobra" + "go.uber.org/zap" +) + +func newUploadCmd() *cobra.Command { + uploadCmd := &cobra.Command{ + Use: "upload {azure|aws} {snp-report|guest-firmware} ", + Short: "Upload an object to the attestationconfig API", + + Long: fmt.Sprintf("Upload a new object to the attestationconfig API. For snp-reports the new object is added to a cache folder first."+ + "The CLI then determines the lowest version within the cache-window present in the cache and writes that value to the config api if necessary. "+ + "For guest-firmware objects the object is added to the API directly. "+ + "Please authenticate with AWS through your preferred method (e.g. environment variables, CLI)"+ + "to be able to upload to S3. Set the %s and %s environment variables to authenticate with cosign.", + envCosignPrivateKey, envCosignPwd, + ), + Example: "COSIGN_PASSWORD=$CPW COSIGN_PRIVATE_KEY=$CKEY cli upload azure snp-report /some/path/report.json", + + Args: cobra.MatchAll(cobra.ExactArgs(3), isCloudProvider(0), isValidKind(1)), + PreRunE: envCheck, + RunE: runUpload, + } + uploadCmd.Flags().StringP("upload-date", "d", "", "upload a version with this date as version name.") + uploadCmd.Flags().BoolP("force", "f", false, "Use force to manually push a new latest version."+ + " The version gets saved to the cache but the version selection logic is skipped.") + uploadCmd.Flags().IntP("cache-window-size", "s", versionWindowSize, "Number of versions to be considered for the latest version.") + + return uploadCmd +} + +func envCheck(_ *cobra.Command, _ []string) error { + if os.Getenv(envCosignPrivateKey) == "" || os.Getenv(envCosignPwd) == "" { + return fmt.Errorf("please set both %s and %s environment variables", envCosignPrivateKey, envCosignPwd) + } + cosignPwd = os.Getenv(envCosignPwd) + privateKey = os.Getenv(envCosignPrivateKey) + return nil +} + +func runUpload(cmd *cobra.Command, args []string) (retErr error) { + ctx := cmd.Context() + log := logger.New(logger.PlainLog, zap.DebugLevel).Named("attestationconfigapi") + + uploadCfg, err := newConfig(cmd, ([3]string)(args[:3])) + if err != nil { + return fmt.Errorf("parsing cli flags: %w", err) + } + + client, clientClose, err := attestationconfigapi.NewClient( + ctx, + staticupload.Config{ + Bucket: uploadCfg.bucket, + Region: uploadCfg.region, + DistributionID: uploadCfg.distribution, + }, + []byte(cosignPwd), + []byte(privateKey), + false, + uploadCfg.cacheWindowSize, + log) + + defer func() { + err := clientClose(cmd.Context()) + if err != nil { + retErr = errors.Join(retErr, fmt.Errorf("failed to invalidate cache: %w", err)) + } + }() + + if err != nil { + return fmt.Errorf("creating client: %w", err) + } + + var attesation variant.Variant + switch uploadCfg.provider { + case cloudprovider.AWS: + attesation = variant.AWSSEVSNP{} + case cloudprovider.Azure: + attesation = variant.AzureSEVSNP{} + default: + return fmt.Errorf("unsupported cloud provider: %s", uploadCfg.provider) + } + + return uploadReport(ctx, attesation, client, uploadCfg, file.NewHandler(afero.NewOsFs()), log) +} + +func uploadReport(ctx context.Context, + attestation variant.Variant, + client *attestationconfigapi.Client, + cfg uploadConfig, + fs file.Handler, + log *logger.Logger, +) error { + if cfg.kind != snpReport { + return fmt.Errorf("kind %s not supported", cfg.kind) + } + + log.Infof("Reading SNP report from file: %s", cfg.path) + var report verify.Report + if err := fs.ReadJSON(cfg.path, &report); err != nil { + return fmt.Errorf("reading snp report: %w", err) + } + + inputVersion := convertTCBVersionToSNPVersion(report.SNPReport.LaunchTCB) + log.Infof("Input report: %+v", inputVersion) + + latestAPIVersionAPI, err := attestationconfigapi.NewFetcherWithCustomCDNAndCosignKey(cfg.url, cfg.cosignPublicKey).FetchSEVSNPVersionLatest(ctx, attestation) + if err != nil { + if errors.Is(err, attestationconfigapi.ErrNoVersionsFound) { + log.Infof("No versions found in API, but assuming that we are uploading the first version.") + } else { + return fmt.Errorf("fetching latest version: %w", err) + } + } + + latestAPIVersion := latestAPIVersionAPI.SEVSNPVersion + if err := client.UploadSEVSNPVersionLatest(ctx, attestation, inputVersion, latestAPIVersion, cfg.uploadDate, cfg.force); err != nil { + if errors.Is(err, attestationconfigapi.ErrNoNewerVersion) { + log.Infof("Input version: %+v is not newer than latest API version: %+v", inputVersion, latestAPIVersion) + return nil + } + return fmt.Errorf("updating latest version: %w", err) + } + + return nil +} + +func convertTCBVersionToSNPVersion(tcb verify.TCBVersion) attestationconfigapi.SEVSNPVersion { + return attestationconfigapi.SEVSNPVersion{ + Bootloader: tcb.Bootloader, + TEE: tcb.TEE, + SNP: tcb.SNP, + Microcode: tcb.Microcode, + } +} + +type uploadConfig struct { + provider cloudprovider.Provider + kind objectKind + path string + uploadDate time.Time + cosignPublicKey string + region string + bucket string + distribution string + url string + force bool + cacheWindowSize int +} + +func newConfig(cmd *cobra.Command, args [3]string) (uploadConfig, error) { + dateStr, err := cmd.Flags().GetString("upload-date") + if err != nil { + return uploadConfig{}, fmt.Errorf("getting upload date: %w", err) + } + uploadDate := time.Now() + if dateStr != "" { + uploadDate, err = time.Parse(attestationconfigapi.VersionFormat, dateStr) + if err != nil { + return uploadConfig{}, fmt.Errorf("parsing date: %w", err) + } + } + + region, err := cmd.Flags().GetString("region") + if err != nil { + return uploadConfig{}, fmt.Errorf("getting region: %w", err) + } + + bucket, err := cmd.Flags().GetString("bucket") + if err != nil { + return uploadConfig{}, fmt.Errorf("getting bucket: %w", err) + } + + testing, err := cmd.Flags().GetBool("testing") + if err != nil { + return uploadConfig{}, fmt.Errorf("getting testing flag: %w", err) + } + apiCfg := getAPIEnvironment(testing) + + force, err := cmd.Flags().GetBool("force") + if err != nil { + return uploadConfig{}, fmt.Errorf("getting force: %w", err) + } + + cacheWindowSize, err := cmd.Flags().GetInt("cache-window-size") + if err != nil { + return uploadConfig{}, fmt.Errorf("getting cache window size: %w", err) + } + + provider := cloudprovider.FromString(args[0]) + kind := kindFromString(args[1]) + path := args[2] + + return uploadConfig{ + provider: provider, + kind: kind, + path: path, + uploadDate: uploadDate, + cosignPublicKey: apiCfg.cosignPublicKey, + region: region, + bucket: bucket, + url: apiCfg.url, + distribution: apiCfg.distribution, + force: force, + cacheWindowSize: cacheWindowSize, + }, nil +} diff --git a/internal/api/attestationconfigapi/reporter.go b/internal/api/attestationconfigapi/reporter.go index 7d3e9ded7f..4cc4bcad68 100644 --- a/internal/api/attestationconfigapi/reporter.go +++ b/internal/api/attestationconfigapi/reporter.go @@ -34,7 +34,7 @@ const cachedVersionsSubDir = "cached-versions" var ErrNoNewerVersion = errors.New("input version is not newer than latest API version") func reportVersionDir(attestation variant.Variant) string { - return path.Join(attestationURLPath, attestation.String(), cachedVersionsSubDir) + return path.Join(AttestationURLPath, attestation.String(), cachedVersionsSubDir) } // UploadSEVSNPVersionLatest saves the given version to the cache, determines the smallest diff --git a/internal/api/attestationconfigapi/snp.go b/internal/api/attestationconfigapi/snp.go index dc53211a6c..68098a3ad6 100644 --- a/internal/api/attestationconfigapi/snp.go +++ b/internal/api/attestationconfigapi/snp.go @@ -16,8 +16,8 @@ import ( "github.com/edgelesssys/constellation/v2/internal/attestation/variant" ) -// attestationURLPath is the URL path to the attestation versions. -const attestationURLPath = "constellation/v1/attestation" +// AttestationURLPath is the URL path to the attestation versions. +const AttestationURLPath = "constellation/v1/attestation" // SEVSNPVersion tracks the latest version of each component of the Azure SEVSNP. type SEVSNPVersion struct { @@ -43,7 +43,7 @@ type SEVSNPVersionAPI struct { // JSONPath returns the path to the JSON file for the request to the config api. func (i SEVSNPVersionAPI) JSONPath() string { - return path.Join(attestationURLPath, i.Variant.String(), i.Version) + return path.Join(AttestationURLPath, i.Variant.String(), i.Version) } // ValidateRequest validates the request. @@ -83,7 +83,7 @@ func (i SEVSNPVersionList) List() []string { return i.list } // JSONPath returns the path to the JSON file for the request to the config api. func (i SEVSNPVersionList) JSONPath() string { - return path.Join(attestationURLPath, i.variant.String(), "list") + return path.Join(AttestationURLPath, i.variant.String(), "list") } // ValidateRequest is a NoOp as there is no input. From d7ab9bd460fd319bb05d8439f0d5eddadf735a8e Mon Sep 17 00:00:00 2001 From: Otto Bittner Date: Tue, 14 Nov 2023 13:25:03 +0100 Subject: [PATCH 10/12] config: only fetch TCB values from api if wanted If no TCB value is set to `latest`, the fetcher is now no longer called. --- internal/config/aws.go | 7 ++++++- internal/config/azure.go | 7 ++++++- 2 files changed, 12 insertions(+), 2 deletions(-) diff --git a/internal/config/aws.go b/internal/config/aws.go index 65fa0001c8..f10fd4f86a 100644 --- a/internal/config/aws.go +++ b/internal/config/aws.go @@ -63,9 +63,14 @@ func (c AWSSEVSNP) EqualTo(other AttestationCfg) (bool, error) { // FetchAndSetLatestVersionNumbers fetches the latest version numbers from the configapi and sets them. func (c *AWSSEVSNP) FetchAndSetLatestVersionNumbers(ctx context.Context, fetcher attestationconfigapi.Fetcher) error { + // Only talk to the API if at least one version number is set to latest. + if !(c.BootloaderVersion.WantLatest || c.TEEVersion.WantLatest || c.SNPVersion.WantLatest || c.MicrocodeVersion.WantLatest) { + return nil + } + versions, err := fetcher.FetchSEVSNPVersionLatest(ctx, variant.AWSSEVSNP{}) if err != nil { - return err + return fmt.Errorf("fetching latest TCB versions from configapi: %w", err) } // set number and keep isLatest flag c.mergeWithLatestVersion(versions.SEVSNPVersion) diff --git a/internal/config/azure.go b/internal/config/azure.go index 4bab0e9f03..c6690e9724 100644 --- a/internal/config/azure.go +++ b/internal/config/azure.go @@ -69,9 +69,14 @@ func (c AzureSEVSNP) EqualTo(old AttestationCfg) (bool, error) { // FetchAndSetLatestVersionNumbers fetches the latest version numbers from the configapi and sets them. func (c *AzureSEVSNP) FetchAndSetLatestVersionNumbers(ctx context.Context, fetcher attestationconfigapi.Fetcher) error { + // Only talk to the API if at least one version number is set to latest. + if !(c.BootloaderVersion.WantLatest || c.TEEVersion.WantLatest || c.SNPVersion.WantLatest || c.MicrocodeVersion.WantLatest) { + return nil + } + versions, err := fetcher.FetchSEVSNPVersionLatest(ctx, variant.AzureSEVSNP{}) if err != nil { - return err + return fmt.Errorf("fetching latest TCB versions from configapi: %w", err) } // set number and keep isLatest flag c.mergeWithLatestVersion(versions.SEVSNPVersion) From ece82f3b119b64b71b47b372297367ca2ca51004 Mon Sep 17 00:00:00 2001 From: Otto Bittner Date: Tue, 14 Nov 2023 13:25:52 +0100 Subject: [PATCH 11/12] ci: call TCB upload step for AWS --- .../actions/constellation_destroy/action.yml | 30 ++++++++++ .../e2e_attestationconfigapi/action.yml | 5 +- .github/actions/e2e_verify/action.yml | 15 +++-- .../workflows/e2e-attestationconfigapi.yml | 6 ++ .github/workflows/e2e-test-daily.yml | 3 + .github/workflows/e2e-test-release.yml | 3 + .github/workflows/e2e-test-weekly.yml | 3 + .github/workflows/e2e-test.yml | 5 +- .github/workflows/e2e-upgrade.yml | 3 + .../attestationconfigapi/cli/e2e/test.sh.in | 59 ++++++++++++------- 10 files changed, 104 insertions(+), 28 deletions(-) diff --git a/.github/actions/constellation_destroy/action.yml b/.github/actions/constellation_destroy/action.yml index ac4892fc83..1cd89d0f6d 100644 --- a/.github/actions/constellation_destroy/action.yml +++ b/.github/actions/constellation_destroy/action.yml @@ -8,6 +8,15 @@ inputs: selfManagedInfra: description: "Use self-managed infrastructure instead of infrastructure created by the Constellation CLI." required: true + gcpClusterDeleteServiceAccount: + description: "Service account with permissions to delete a Constellation cluster on GCP." + required: true + azureClusterDeleteCredentials: + description: "Azure credentials authorized to delete a Constellation cluster." + required: true + cloudProvider: + description: "Either 'aws', 'azure' or 'gcp'." + required: true runs: using: "composite" @@ -41,6 +50,27 @@ runs: fi echo "::endgroup::" + - name: Login to GCP (Cluster service account) + if: inputs.cloudProvider == 'gcp' + uses: ./.github/actions/login_gcp + with: + service_account: ${{ inputs.gcpClusterDeleteServiceAccount }} + + - name: Login to AWS (Cluster role) + if: inputs.cloudProvider == 'aws' + uses: aws-actions/configure-aws-credentials@010d0da01d0b5a38af31e9c3470dbfdabdecca3a # v4.0.1 + with: + role-to-assume: arn:aws:iam::795746500882:role/GithubActionsE2ECluster + aws-region: eu-central-1 + # extend token expiry to 6 hours to ensure constellation can terminate + role-duration-seconds: 21600 + + - name: Login to Azure (Cluster service principal) + if: inputs.cloudProvider == 'azure' + uses: ./.github/actions/login_azure + with: + azure_credentials: ${{ inputs.azureClusterDeleteCredentials }} + - name: Constellation terminate if: inputs.selfManagedInfra != 'true' shell: bash diff --git a/.github/actions/e2e_attestationconfigapi/action.yml b/.github/actions/e2e_attestationconfigapi/action.yml index 955adf4bfb..add2894768 100644 --- a/.github/actions/e2e_attestationconfigapi/action.yml +++ b/.github/actions/e2e_attestationconfigapi/action.yml @@ -2,6 +2,9 @@ name: E2E Attestationconfig API Test description: "Test the attestationconfig CLI is functional." inputs: + csp: + description: "Cloud provider to run tests against" + default: "azure" buildBuddyApiKey: description: "BuildBuddy API key for caching Bazel artifacts" required: true @@ -33,4 +36,4 @@ runs: COSIGN_PRIVATE_KEY: ${{ inputs.cosignPrivateKey }} COSIGN_PASSWORD: ${{ inputs.cosignPassword }} run: | - bazel run //internal/api/attestationconfigapi/cli:cli_e2e_test + bazel run //internal/api/attestationconfigapi/cli:cli_e2e_test -- ${{ inputs.csp }} diff --git a/.github/actions/e2e_verify/action.yml b/.github/actions/e2e_verify/action.yml index c18938becb..b9b17a10de 100644 --- a/.github/actions/e2e_verify/action.yml +++ b/.github/actions/e2e_verify/action.yml @@ -66,8 +66,8 @@ runs: forwarderPID=$! sleep 5 - if [[ ${{ inputs.cloudProvider }} == "azure" ]]; then - echo "Extracting Azure TCB versions for API update" + if [[ ${{ inputs.cloudProvider }} == "azure" || ${{ inputs.cloudProvider }} == "aws" ]]; then + echo "Extracting TCB versions for API update" constellation verify --cluster-id "${clusterID}" --node-endpoint localhost:9090 -o json > "snp-report-${node}.json" else constellation verify --cluster-id "${clusterID}" --node-endpoint localhost:9090 @@ -84,14 +84,19 @@ runs: aws-region: eu-central-1 - name: Upload extracted TCBs - if: github.ref_name == 'main' && inputs.cloudProvider == 'azure' + if: github.ref_name == 'main' && (inputs.cloudProvider == 'azure' || inputs.cloudProvider == 'aws') shell: bash env: COSIGN_PASSWORD: ${{ inputs.cosignPassword }} COSIGN_PRIVATE_KEY: ${{ inputs.cosignPrivateKey }} run: | - for file in $(ls snp-report-*.json); do + reports=(snp-report-*.json) + if [ -z ${#reports[@]} ]; then + exit 1 + fi + + for file in "${reports[@]}"; do path=$(realpath "${file}") cat "${path}" - bazel run //internal/api/attestationconfigapi/cli -- upload azure snp-report "${path}" + bazel run //internal/api/attestationconfigapi/cli -- upload ${{ inputs.cloudProvider }} snp-report "${path}" done diff --git a/.github/workflows/e2e-attestationconfigapi.yml b/.github/workflows/e2e-attestationconfigapi.yml index ee2582bebf..7fcca9028b 100644 --- a/.github/workflows/e2e-attestationconfigapi.yml +++ b/.github/workflows/e2e-attestationconfigapi.yml @@ -16,6 +16,11 @@ on: jobs: e2e-api: + strategy: + fail-fast: false + max-parallel: 1 + matrix: + csp: ["azure", "aws"] runs-on: ubuntu-22.04 permissions: id-token: write @@ -35,3 +40,4 @@ jobs: buildBuddyApiKey: ${{ secrets.BUILDBUDDY_ORG_API_KEY }} cosignPrivateKey: ${{ secrets.COSIGN_DEV_PRIVATE_KEY }} cosignPassword: ${{ secrets.COSIGN_DEV_PASSWORD }} + csp: ${{ matrix.csp }} diff --git a/.github/workflows/e2e-test-daily.yml b/.github/workflows/e2e-test-daily.yml index a9f77c31fd..47ad5a605b 100644 --- a/.github/workflows/e2e-test-daily.yml +++ b/.github/workflows/e2e-test-daily.yml @@ -99,6 +99,9 @@ jobs: with: kubeconfig: ${{ steps.e2e_test.outputs.kubeconfig }} selfManagedInfra: "false" + cloudProvider: ${{ matrix.provider }} + azureClusterDeleteCredentials: ${{ secrets.AZURE_E2E_CLUSTER_CREDENTIALS }} + gcpClusterDeleteServiceAccount: "constellation-e2e-cluster@constellation-331613.iam.gserviceaccount.com" - name: Always delete IAM configuration if: always() diff --git a/.github/workflows/e2e-test-release.yml b/.github/workflows/e2e-test-release.yml index 681a340d2b..2a9989d211 100644 --- a/.github/workflows/e2e-test-release.yml +++ b/.github/workflows/e2e-test-release.yml @@ -248,6 +248,9 @@ jobs: with: kubeconfig: ${{ steps.e2e_test.outputs.kubeconfig }} selfManagedInfra: ${{ matrix.selfManagedInfra == 'true' }} + cloudProvider: ${{ matrix.provider }} + azureClusterDeleteCredentials: ${{ secrets.AZURE_E2E_CLUSTER_CREDENTIALS }} + gcpClusterDeleteServiceAccount: "constellation-e2e-cluster@constellation-331613.iam.gserviceaccount.com" - name: Always delete IAM configuration if: always() diff --git a/.github/workflows/e2e-test-weekly.yml b/.github/workflows/e2e-test-weekly.yml index 02661cfc42..9fda7d3db2 100644 --- a/.github/workflows/e2e-test-weekly.yml +++ b/.github/workflows/e2e-test-weekly.yml @@ -267,6 +267,9 @@ jobs: with: kubeconfig: ${{ steps.e2e_test.outputs.kubeconfig }} selfManagedInfra: ${{ matrix.selfManagedInfra == 'true' }} + cloudProvider: ${{ matrix.provider }} + azureClusterDeleteCredentials: ${{ secrets.AZURE_E2E_CLUSTER_CREDENTIALS }} + gcpClusterDeleteServiceAccount: "constellation-e2e-cluster@constellation-331613.iam.gserviceaccount.com" - name: Always delete IAM configuration if: always() diff --git a/.github/workflows/e2e-test.yml b/.github/workflows/e2e-test.yml index 99dda6b18d..1d0b41f733 100644 --- a/.github/workflows/e2e-test.yml +++ b/.github/workflows/e2e-test.yml @@ -165,7 +165,7 @@ jobs: uses: actions/checkout@f43a0e5ff2bd294095638e18286ca9a3d1956744 # v3.6.0 with: ref: ${{ inputs.git-ref }} - + - name: Get Latest Image id: find-latest-image uses: ./.github/actions/find_latest_image @@ -246,6 +246,9 @@ jobs: with: kubeconfig: ${{ steps.e2e_test.outputs.kubeconfig }} selfManagedInfra: ${{ inputs.selfManagedInfra }} + cloudProvider: ${{ inputs.cloudProvider }} + azureClusterDeleteCredentials: ${{ secrets.AZURE_E2E_CLUSTER_CREDENTIALS }} + gcpClusterDeleteServiceAccount: "constellation-e2e-cluster@constellation-331613.iam.gserviceaccount.com" - name: Always delete IAM configuration if: always() diff --git a/.github/workflows/e2e-upgrade.yml b/.github/workflows/e2e-upgrade.yml index 70d10f9d81..79256fea55 100644 --- a/.github/workflows/e2e-upgrade.yml +++ b/.github/workflows/e2e-upgrade.yml @@ -290,6 +290,9 @@ jobs: with: kubeconfig: ${{ steps.e2e_test.outputs.kubeconfig }} selfManagedInfra: "false" + cloudProvider: ${{ inputs.cloudProvider }} + azureClusterDeleteCredentials: ${{ secrets.AZURE_E2E_CLUSTER_CREDENTIALS }} + gcpClusterDeleteServiceAccount: "constellation-e2e-cluster@constellation-331613.iam.gserviceaccount.com" - name: Always delete IAM configuration if: always() diff --git a/internal/api/attestationconfigapi/cli/e2e/test.sh.in b/internal/api/attestationconfigapi/cli/e2e/test.sh.in index baf4fc4699..773443df45 100755 --- a/internal/api/attestationconfigapi/cli/e2e/test.sh.in +++ b/internal/api/attestationconfigapi/cli/e2e/test.sh.in @@ -19,6 +19,22 @@ configapi_cli=$(realpath @@CONFIGAPI_CLI@@) stat "${configapi_cli}" >> /dev/null configapi_cli="${configapi_cli} --testing" ###### script body ###### +function variant() { + if [[ $1 == "aws" ]]; then + echo "aws-sev-snp" + return 0 + elif [[ $1 == "azure" ]]; then + echo "azure-sev-snp" + return 0 + else + echo "Unknown CSP: $1" + exit 1 + fi +} + +csp=$1 +readonly csp +attestationType=$(variant "$csp") readonly region="eu-west-1" readonly bucket="resource-api-testing" @@ -28,7 +44,7 @@ readonly tmpdir registerExitHandler "rm -rf $tmpdir" # empty the bucket version state -${configapi_cli} delete recursive azure --region "$region" --bucket "$bucket" +${configapi_cli} delete recursive "$csp" --region "$region" --bucket "$bucket" # the high version numbers ensure that it's newer than the current latest value readonly current_report_path="$tmpdir/currentSnpReport.json" @@ -57,7 +73,7 @@ cat << EOF > "$current_report_path" } EOF # upload a fake latest version for the fetcher -${configapi_cli} upload azure snp-report "$current_report_path" --force --upload-date "2000-01-01-01-01" --region "$region" --bucket "$bucket" +${configapi_cli} upload "$csp" snp-report "$current_report_path" --force --upload-date "2000-01-01-01-01" --region "$region" --bucket "$bucket" # the high version numbers ensure that it's newer than the current latest value readonly report_path="$tmpdir/snpReport.json" @@ -115,16 +131,17 @@ EOF # report 3 versions with different dates to fill the reporter cache readonly date_oldest="2023-02-01-03-04" -${configapi_cli} upload azure snp-report "$older_report_path" --upload-date "$date_oldest" --region "$region" --bucket "$bucket" --cache-window-size 3 +${configapi_cli} upload "$csp" snp-report "$older_report_path" --upload-date "$date_oldest" --region "$region" --bucket "$bucket" --cache-window-size 3 readonly date_older="2023-02-02-03-04" -${configapi_cli} upload azure snp-report "$older_report_path" --upload-date "$date_older" --region "$region" --bucket "$bucket" --cache-window-size 3 +${configapi_cli} upload "$csp" snp-report "$older_report_path" --upload-date "$date_older" --region "$region" --bucket "$bucket" --cache-window-size 3 readonly date="2023-02-03-03-04" -${configapi_cli} upload azure snp-report "$report_path" --upload-date "$date" --region "$region" --bucket "$bucket" --cache-window-size 3 +${configapi_cli} upload "$csp" snp-report "$report_path" --upload-date "$date" --region "$region" --bucket "$bucket" --cache-window-size 3 # expect that $date_oldest is served as latest version -baseurl="https://d33dzgxuwsgbpw.cloudfront.net/constellation/v1/attestation/azure-sev-snp" -if ! curl -fsSL ${baseurl}/${date_oldest}.json > version.json; then - echo "Checking for uploaded version file constellation/v1/attestation/azure-sev-snp/${date_oldest}.json: request returned ${?}" +basepath="constellation/v1/attestation/${attestationType}" +baseurl="https://d33dzgxuwsgbpw.cloudfront.net/${basepath}" +if ! curl -fsSL "${baseurl}"/${date_oldest}.json > version.json; then + echo "Checking for uploaded version file ${basepath}/${date_oldest}.json: request returned ${?}" exit 1 fi # check that version values are equal to expected @@ -135,13 +152,13 @@ if ! cmp -s <(echo -n '{"bootloader":255,"tee":255,"snp":255,"microcode":254}') echo '{"bootloader":255,"tee":255,"snp":255,"microcode":254}' exit 1 fi -if ! curl -fsSL ${baseurl}/${date_oldest}.json.sig > /dev/null; then - echo "Checking for uploaded version signature file constellation/v1/attestation/azure-sev-snp/${date_oldest}.json.sig: request returned ${?}" +if ! curl -fsSL "${baseurl}"/${date_oldest}.json.sig > /dev/null; then + echo "Checking for uploaded version signature file ${basepath}/${date_oldest}.json.sig: request returned ${?}" exit 1 fi # check list endpoint -if ! curl -fsSL ${baseurl}/list > list.json; then - echo "Checking for uploaded list file constellation/v1/attestation/azure-sev-snp/list: request returned ${?}" +if ! curl -fsSL "${baseurl}"/list > list.json; then + echo "Checking for uploaded list file ${basepath}/list: request returned ${?}" exit 1 fi # check that version values are equal to expected @@ -154,28 +171,28 @@ if ! cmp -s <(echo -n '["2023-02-01-03-04.json","2000-01-01-01-01.json"]') list. fi # check that the other versions are not uploaded -http_code=$(curl -sSL -w '%{http_code}\n' -o /dev/null ${baseurl}/${date_older}.json) +http_code=$(curl -sSL -w '%{http_code}\n' -o /dev/null "${baseurl}"/${date_older}.json) if [[ $http_code -ne 404 ]]; then - echo "Expected HTTP code 404 for: constellation/v1/attestation/azure-sev-snp/${date_older}.json, but got ${http_code}" + echo "Expected HTTP code 404 for: ${basepath}/${date_older}.json, but got ${http_code}" exit 1 fi -http_code=$(curl -sSL -w '%{http_code}\n' -o /dev/null ${baseurl}/${date}.json.sig) +http_code=$(curl -sSL -w '%{http_code}\n' -o /dev/null "${baseurl}"/${date}.json.sig) if [[ $http_code -ne 404 ]]; then - echo "Expected HTTP code 404 for: constellation/v1/attestation/azure-sev-snp/${date}.json, but got ${http_code}" + echo "Expected HTTP code 404 for: ${basepath}/${date}.json, but got ${http_code}" exit 1 fi -${configapi_cli} delete azure snp-report "$date_oldest" --region "$region" --bucket "$bucket" +${configapi_cli} delete "$csp" snp-report "$date_oldest" --region "$region" --bucket "$bucket" # Omit -f to check for 404. We want to check that a file was deleted, therefore we expect the query to fail. -http_code=$(curl -sSL -w '%{http_code}\n' -o /dev/null ${baseurl}/${date_oldest}.json) +http_code=$(curl -sSL -w '%{http_code}\n' -o /dev/null "${baseurl}"/${date_oldest}.json) if [[ $http_code -ne 404 ]]; then - echo "Expected HTTP code 404 for: constellation/v1/attestation/azure-sev-snp/${date_oldest}.json, but got ${http_code}" + echo "Expected HTTP code 404 for: ${basepath}/${date_oldest}.json, but got ${http_code}" exit 1 fi # Omit -f to check for 404. We want to check that a file was deleted, therefore we expect the query to fail. -http_code=$(curl -sSL -w '%{http_code}\n' -o /dev/null ${baseurl}/${date_oldest}.json.sig) +http_code=$(curl -sSL -w '%{http_code}\n' -o /dev/null "${baseurl}"/${date_oldest}.json.sig) if [[ $http_code -ne 404 ]]; then - echo "Expected HTTP code 404 for: constellation/v1/attestation/azure-sev-snp/${date_oldest}.json, but got ${http_code}" + echo "Expected HTTP code 404 for: ${basepath}/${date_oldest}.json, but got ${http_code}" exit 1 fi From 1891fcf3361c044e2eb74c2641070dfe3e4029e7 Mon Sep 17 00:00:00 2001 From: Otto Bittner Date: Wed, 15 Nov 2023 12:12:40 +0100 Subject: [PATCH 12/12] docs: explain config options for AWS SNP --- docs/docs/architecture/attestation.md | 19 ++++++++++++++++++- 1 file changed, 18 insertions(+), 1 deletion(-) diff --git a/docs/docs/architecture/attestation.md b/docs/docs/architecture/attestation.md index fd42508fd7..04b85d8ad1 100644 --- a/docs/docs/architecture/attestation.md +++ b/docs/docs/architecture/attestation.md @@ -256,7 +256,24 @@ There is no additional configuration available for GCP. -There is no additional configuration available for AWS. +On AWS, AMD SEV-SNP is used to provide runtime encryption to the VMs. +An SEV-SNP attestation report is used to establish trust in the VM and it's vTPM. +You may customize certain parameters for verification of the attestation statement using the Constellation config file. + +* TCB versions + + You can set the minimum version numbers of components in the SEV-SNP TCB. + Use the latest versions to enforce that only machines with the most recent firmware updates are allowed to join the cluster. + Alternatively, you can set a lower minimum version to allow slightly out-of-date machines to still be able to join the cluster. + +* AMD Root Key Certificate + + This certificate is the root of trust for verifying the SEV-SNP certificate chain. + +* AMD Signing Key Certificate + + This is the intermediate certificate for verifying the SEV-SNP report's signature. + If it's not specified, the CLI fetches it from the AMD key distribution server.