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 371d2932f0..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 -- --snp-report-path "${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/cli/internal/cmd/BUILD.bazel b/cli/internal/cmd/BUILD.bazel index 7beeb2f238..9b5a8ca95f 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", @@ -87,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/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/cli/internal/cmd/verify.go b/cli/internal/cmd/verify.go index b8f31b7007..8c0b465e03 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" @@ -29,6 +24,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" @@ -39,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" @@ -112,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": @@ -212,9 +205,8 @@ func (c *verifyCmd) verify(cmd *cobra.Command, verifyClient verifyClient, factor attDocOutput, err := formatter.format( cmd.Context(), rawAttestationDoc, - conf.Provider.Azure == nil, - attConfig.GetMeasurements(), - maaURL, + (conf.Provider.Azure == nil && conf.Provider.AWS == nil), + attConfig, ) if err != nil { return fmt.Errorf("printing attestation document: %w", err) @@ -261,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 { @@ -271,36 +262,22 @@ 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) { - 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, attestationCfg, 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 @@ -312,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") @@ -326,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") @@ -336,104 +313,24 @@ 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 { 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) - } - - var instanceInfo azureInstanceInfo - if err := json.Unmarshal(instanceInfoString, &instanceInfo); err != nil { - return "", fmt.Errorf("unmarshal instance info: %w", err) + return "", fmt.Errorf("unmarshalling 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, attestationCfg, 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. @@ -464,139 +361,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 { @@ -609,16 +373,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 @@ -673,184 +427,15 @@ 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) (azureInstanceInfo, error) { - var doc attestationDoc - if err := json.Unmarshal([]byte(docString), &doc); err != nil { - return azureInstanceInfo{}, 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 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 } diff --git a/cli/internal/cmd/verify_test.go b/cli/internal/cmd/verify_test.go index f381c86b83..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 { @@ -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/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. 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 a64c3fdbd2..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( @@ -11,13 +10,19 @@ go_binary( go_library( name = "cli_lib", srcs = [ + "aws.go", + "azure.go", "delete.go", "main.go", + "upload.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", "//internal/logger", @@ -32,20 +37,6 @@ go_library( ], ) -go_test( - name = "cli_test", - srcs = [ - "delete_test.go", - "main_test.go", - ], - embed = [":cli_lib"], - deps = [ - "//internal/verify", - "@com_github_stretchr_testify//assert", - "@com_github_stretchr_testify//require", - ], -) - sh_template( name = "cli_e2e_test", data = [":cli"], diff --git a/internal/api/attestationconfigapi/cli/aws.go b/internal/api/attestationconfigapi/cli/aws.go new file mode 100644 index 0000000000..578caadc4f --- /dev/null +++ b/internal/api/attestationconfigapi/cli/aws.go @@ -0,0 +1,24 @@ +/* +Copyright (c) Edgeless Systems GmbH + +SPDX-License-Identifier: AGPL-3.0-only +*/ + +package main + +import ( + "context" + "fmt" + + "github.com/edgelesssys/constellation/v2/internal/api/attestationconfigapi" + "github.com/edgelesssys/constellation/v2/internal/attestation/variant" + "github.com/edgelesssys/constellation/v2/internal/cloud/cloudprovider" +) + +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) + } + + return client.DeleteSEVSNPVersion(ctx, variant.AWSSEVSNP{}, cfg.version) +} diff --git a/internal/api/attestationconfigapi/cli/azure.go b/internal/api/attestationconfigapi/cli/azure.go new file mode 100644 index 0000000000..924fbe19e9 --- /dev/null +++ b/internal/api/attestationconfigapi/cli/azure.go @@ -0,0 +1,57 @@ +/* +Copyright (c) Edgeless Systems GmbH + +SPDX-License-Identifier: AGPL-3.0-only +*/ + +package main + +import ( + "context" + "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/attestation/variant" + "github.com/edgelesssys/constellation/v2/internal/cloud/cloudprovider" + "github.com/edgelesssys/constellation/v2/internal/staticupload" +) + +func deleteAzure(ctx context.Context, client *attestationconfigapi.Client, cfg deleteConfig) error { + if cfg.provider != cloudprovider.Azure && cfg.kind != snpReport { + return fmt.Errorf("provider %s and kind %s not supported", cfg.provider, cfg.kind) + } + + return client.DeleteSEVSNPVersion(ctx, variant.AzureSEVSNP{}, cfg.version) +} + +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), + }) + 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..faedabd118 100644 --- a/internal/api/attestationconfigapi/cli/delete.go +++ b/internal/api/attestationconfigapi/cli/delete.go @@ -6,14 +6,13 @@ SPDX-License-Identifier: AGPL-3.0-only package main import ( - "context" "errors" "fmt" + "path" - "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/attestation/variant" + "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 +22,41 @@ 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: "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, } - 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", - 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) - return cmd -} -type deleteCmd struct { - attestationClient deleteClient -} - -type deleteClient interface { - DeleteAzureSEVSNPVersion(ctx context.Context, versionStr string) error -} + cmd.AddCommand(recursivelyCmd) -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) + return cmd } -func runDelete(cmd *cobra.Command, _ []string) (retErr error) { +func runDelete(cmd *cobra.Command, args []string) (retErr error) { log := logger.New(logger.PlainLog, zap.DebugLevel).Named("attestationconfigapi") - 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") + 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) 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 +70,30 @@ 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") +func runRecursiveDelete(cmd *cobra.Command, args []string) (retErr error) { + // 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 { - return fmt.Errorf("getting region: %w", err) + return fmt.Errorf("creating delete config: %w", err) } - bucket, err := cmd.Flags().GetString("bucket") - if err != nil { - return fmt.Errorf("getting bucket: %w", err) - } - - testing, err := cmd.Flags().GetBool("testing") - if err != nil { - return fmt.Errorf("getting testing flag: %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 +104,60 @@ 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), - }) + + 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 err + return deleteConfig{}, fmt.Errorf("getting region: %w", 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} + bucket, err := cmd.Flags().GetString("bucket") + if err != nil { + return deleteConfig{}, fmt.Errorf("getting bucket: %w", err) } - 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 - } + + testing, err := cmd.Flags().GetBool("testing") + if err != nil { + return deleteConfig{}, fmt.Errorf("getting testing flag: %w", err) } - return nil + 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 } diff --git a/internal/api/attestationconfigapi/cli/delete_test.go b/internal/api/attestationconfigapi/cli/delete_test.go deleted file mode 100644 index adca530602..0000000000 --- a/internal/api/attestationconfigapi/cli/delete_test.go +++ /dev/null @@ -1,38 +0,0 @@ -/* -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..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 --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} --force --snp-report-path "$current_report_path" --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} --snp-report-path "$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} --snp-report-path "$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} --snp-report-path "$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 --version "$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 diff --git a/internal/api/attestationconfigapi/cli/main.go b/internal/api/attestationconfigapi/cli/main.go index 59b5bc18f1..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/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" ) const ( @@ -57,191 +47,17 @@ 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.", - - 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. "+ - "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, - ), - PreRunE: envCheck, - RunE: runCmd, + 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.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."+ - " 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 -} - -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 runCmd(cmd *cobra.Command, _ []string) (retErr error) { - ctx := cmd.Context() - log := logger.New(logger.PlainLog, zap.DebugLevel).Named("attestationconfigapi") - - flags, err := parseCliFlags(cmd) - 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) - - 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 { - retErr = errors.Join(retErr, fmt.Errorf("failed to invalidate cache: %w", err)) - } - }() - - if err != nil { - 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, - } -} - -type config struct { - snpReportPath string - uploadDate time.Time - cosignPublicKey string - region string - bucket string - distribution string - url string - force bool - 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) - } - - dateStr, err := cmd.Flags().GetString("upload-date") - if err != nil { - return config{}, 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) - } - } - - region, err := cmd.Flags().GetString("region") - if err != nil { - return config{}, fmt.Errorf("getting region: %w", err) - } - - bucket, err := cmd.Flags().GetString("bucket") - if err != nil { - return config{}, fmt.Errorf("getting bucket: %w", err) - } - - testing, err := cmd.Flags().GetBool("testing") - if err != nil { - return config{}, 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) - } + rootCmd.AddCommand(newUploadCmd()) + rootCmd.AddCommand(newDeleteCmd()) - cacheWindowSize, err := cmd.Flags().GetInt("cache-window-size") - if err != nil { - return config{}, fmt.Errorf("getting cache window size: %w", err) - } - return config{ - snpReportPath: snpReportFilePath, - uploadDate: uploadDate, - cosignPublicKey: apiCfg.cosignPublicKey, - region: region, - bucket: bucket, - url: apiCfg.url, - distribution: apiCfg.distribution, - force: force, - cacheWindowSize: cacheWindowSize, - }, nil + return rootCmd } type apiConfig struct { @@ -256,9 +72,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 deleted file mode 100644 index c1c1f09358..0000000000 --- a/internal/api/attestationconfigapi/cli/main_test.go +++ /dev/null @@ -1,32 +0,0 @@ -/* -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/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/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 + } +} 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..4cc4bcad68 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..68098a3ad6 --- /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/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/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..3280a731c3 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,12 +66,14 @@ func (i *Issuer) getInstanceInfo(ctx context.Context, tpm io.ReadWriteCloser, us } } - instanceInfo := azureInstanceInfo{ - VCEK: params.VcekCert, + instanceInfo := snp.InstanceInfo{ + 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 4f61c9fabd..81f6d6df1e 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,15 +100,15 @@ 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) - 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/testdata/testdata.go b/internal/attestation/azure/snp/testdata/testdata.go deleted file mode 100644 index 9e547a5746..0000000000 --- a/internal/attestation/azure/snp/testdata/testdata.go +++ /dev/null @@ -1,35 +0,0 @@ -/* -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" - -// AttestationBytes is an example attestation report from a Constellation VM. -// -//go:embed attestation.bin -var AttestationReport []byte - -// AzureThimVCEK is an example VCEK certificate (PEM, as returned from Azure THIM) for the AttestationReport. -// -//go:embed vcek.pem -var AzureThimVCEK []byte - -// AmdKdsVCEK is an example VCEK certificate (DER, as returned from AMD KDS) for the AttestationReport. -// -//go:embed vcek.cert -var AmdKdsVCEK []byte - -// RuntimeData is an example runtime data from the TPM for the AttestationReport. -// -//go:embed runtimedata.bin -var RuntimeData []byte - -// CertChain is a valid certificate chain (PEM, as returned from Azure THIM) for the VCEK certificate. -// -//go:embed certchain.pem -var CertChain []byte diff --git a/internal/attestation/azure/snp/validator.go b/internal/attestation/azure/snp/validator.go index 4ea503f85c..856e528ffa 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.getter, cachedCerts, v.log) if err != nil { return nil, fmt.Errorf("parsing attestation report: %w", err) } @@ -183,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) } @@ -192,7 +191,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.Azure.RuntimeData, att.Report.ReportData, pubArea.RSAParameters); err != nil { return nil, fmt.Errorf("validating HCLAkPub: %w", err) } @@ -239,201 +238,17 @@ func (v *Validator) checkIDKeyDigest(ctx context.Context, report *spb.Attestatio return nil } -// azureInstanceInfo contains the necessary information to establish trust in -// an Azure CVM. -type azureInstanceInfo struct { - // VCEK is the PEM-encoded VCEK certificate for the attestation report. - VCEK []byte - // CertChain is the PEM-encoded certificate chain for the attestation report. - CertChain []byte - // AttestationReport is the attestation report from the vTPM (NVRAM) of the CVM. - AttestationReport []byte - // RuntimeData is the Azure runtime data from the vTPM (NVRAM) of the CVM. - RuntimeData []byte - // MAAToken is the token of the MAA for the attestation report, used as a fallback - // if the IDKeyDigest cannot be verified. - MAAToken string +type attestationKey struct { + PublicPart []akPub `json:"keys"` } -// attestationWithCerts returns a formatted version of the attestation report and its certificates from the instanceInfo. -// Certificates are retrieved in the following precedence: -// 1. ASK or ARK from THIM -// 2. ASK or ARK from fallbackCerts -// 3. ASK or ARK from AMD KDS. -func (a *azureInstanceInfo) attestationWithCerts(logger attestation.Logger, getter trust.HTTPSGetter, - fallbackCerts sevSnpCerts, -) (*spb.Attestation, error) { - report, err := abi.ReportToProto(a.AttestationReport) - if err != nil { - return nil, fmt.Errorf("converting report to proto: %w", err) - } - - // Product info as reported through CPUID[EAX=1] - sevProduct := &spb.SevProduct{Name: spb.SevProduct_SEV_PRODUCT_MILAN, Stepping: 0} // Milan-B0 - productName := kds.ProductString(sevProduct) - - att := &spb.Attestation{ - Report: report, - CertificateChain: &spb.CertificateChain{}, - Product: sevProduct, - } - - // If the VCEK certificate is present, parse it and format it. - vcek, err := a.parseVCEK() - if err != nil { - logger.Warnf("Error parsing VCEK: %v", err) - } - if vcek != nil { - att.CertificateChain.VcekCert = vcek.Raw - } else { - // Otherwise, retrieve it from AMD KDS. - logger.Infof("VCEK certificate not present, falling back to retrieving it from AMD KDS") - vcekURL := kds.VCEKCertURL(productName, report.GetChipId(), kds.TCBVersion(report.GetReportedTcb())) - vcek, err := getter.Get(vcekURL) - if err != nil { - return nil, fmt.Errorf("retrieving VCEK certificate from AMD KDS: %w", err) - } - att.CertificateChain.VcekCert = vcek - } - - // If the certificate chain from THIM is present, parse it and format it. - ask, ark, err := a.parseCertChain() - if err != nil { - logger.Warnf("Error parsing certificate chain: %v", err) - } - if ask != nil { - logger.Infof("Using ASK certificate from Azure THIM") - att.CertificateChain.AskCert = ask.Raw - } - if ark != nil { - logger.Infof("Using ARK certificate from Azure THIM") - att.CertificateChain.ArkCert = ark.Raw - } - - // If a cached ASK or an ARK from the Constellation config is present, use it. - if att.CertificateChain.AskCert == nil && fallbackCerts.ask != nil { - logger.Infof("Using cached ASK certificate") - att.CertificateChain.AskCert = fallbackCerts.ask.Raw - } - if att.CertificateChain.ArkCert == nil && fallbackCerts.ark != nil { - logger.Infof("Using ARK certificate from %s", constants.ConfigFilename) - att.CertificateChain.ArkCert = fallbackCerts.ark.Raw - } - // Otherwise, retrieve it from AMD KDS. - if att.CertificateChain.AskCert == nil || att.CertificateChain.ArkCert == nil { - logger.Infof( - "Certificate chain not fully present (ARK present: %t, ASK present: %t), falling back to retrieving it from AMD KDS", - (att.CertificateChain.ArkCert != nil), - (att.CertificateChain.AskCert != nil), - ) - kdsCertChain, err := trust.GetProductChain(productName, abi.VcekReportSigner, getter) - if err != nil { - return nil, fmt.Errorf("retrieving certificate chain from AMD KDS: %w", err) - } - if att.CertificateChain.AskCert == nil && kdsCertChain.Ask != nil { - logger.Infof("Using ASK certificate from AMD KDS") - att.CertificateChain.AskCert = kdsCertChain.Ask.Raw - } - if att.CertificateChain.ArkCert == nil && kdsCertChain.Ask != nil { - logger.Infof("Using ARK certificate from AMD KDS") - att.CertificateChain.ArkCert = kdsCertChain.Ark.Raw - } - } - - return att, nil -} - -type sevSnpCerts struct { - ask *x509.Certificate - ark *x509.Certificate -} - -// parseCertChain parses the certificate chain from the instanceInfo into x509-formatted ASK and ARK certificates. -// If less than 2 certificates are present, only the present certificate is returned. -// If more than 2 certificates are present, an error is returned. -func (a *azureInstanceInfo) parseCertChain() (ask, ark *x509.Certificate, retErr error) { - rest := bytes.TrimSpace(a.CertChain) - - i := 1 - var block *pem.Block - for block, rest = pem.Decode(rest); block != nil; block, rest = pem.Decode(rest) { - if i > 2 { - retErr = fmt.Errorf("parse certificate %d: more than 2 certificates in chain", i) - return - } - - if block.Type != "CERTIFICATE" { - retErr = fmt.Errorf("parse certificate %d: expected PEM block type 'CERTIFICATE', got '%s'", i, block.Type) - return - } - - cert, err := x509.ParseCertificate(block.Bytes) - if err != nil { - retErr = fmt.Errorf("parse certificate %d: %w", i, err) - return - } - - // https://www.amd.com/content/dam/amd/en/documents/epyc-technical-docs/specifications/57230.pdf - // Table 6 and 7 - switch cert.Subject.CommonName { - case "SEV-Milan": - ask = cert - case "ARK-Milan": - ark = cert - default: - retErr = fmt.Errorf("parse certificate %d: unexpected subject CN %s", i, cert.Subject.CommonName) - return - } - - i++ - } - - switch { - case i == 1: - retErr = fmt.Errorf("no PEM blocks found") - case len(rest) != 0: - retErr = fmt.Errorf("remaining PEM block is not a valid certificate: %s", rest) - } - - return -} - -// parseVCEK parses the VCEK certificate from the instanceInfo into an x509-formatted certificate. -// If the VCEK certificate is not present, nil is returned. -func (a *azureInstanceInfo) parseVCEK() (*x509.Certificate, error) { - newlinesTrimmed := bytes.TrimSpace(a.VCEK) - if len(newlinesTrimmed) == 0 { - // VCEK is not present. - return nil, nil - } - - block, rest := pem.Decode(newlinesTrimmed) - if block == nil { - return nil, fmt.Errorf("no PEM blocks found") - } - if len(rest) != 0 { - return nil, fmt.Errorf("received more data than expected") - } - if block.Type != "CERTIFICATE" { - return nil, fmt.Errorf("expected PEM block type 'CERTIFICATE', got '%s'", block.Type) - } - - vcek, err := x509.ParseCertificate(block.Bytes) - if err != nil { - return nil, fmt.Errorf("parsing VCEK certificate: %w", err) - } - - return vcek, nil -} - -// validateAk validates that the attestation key from the TPM is trustworthy. The steps are: +// validate validates that the attestation key from the TPM is trustworthy. The steps are: // 1. runtime data read from the TPM has the same sha256 digest as reported in `report_data` of the SNP report. // 2. modulus reported in runtime data matches modulus from key at idx 0x81000003. // 3. exponent reported in runtime data matches exponent from key at idx 0x81000003. // The function is currently tested manually on a Azure Ubuntu CVM. -func (a *azureInstanceInfo) validateAk(runtimeDataRaw []byte, reportData []byte, rsaParameters *tpm2.RSAParams) error { - var runtimeData runtimeData - if err := json.Unmarshal(runtimeDataRaw, &runtimeData); err != nil { +func (a *attestationKey) validate(runtimeDataRaw []byte, reportData []byte, rsaParameters *tpm2.RSAParams) error { + if err := json.Unmarshal(runtimeDataRaw, a); err != nil { return fmt.Errorf("unmarshalling json: %w", err) } @@ -445,10 +260,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 +271,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 +293,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..2b2c9a2414 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{Azure: &snp.AzureInstanceInfo{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,35 +792,44 @@ func (v *stubAttestationValidator) SNPAttestation(attestation *spb.Attestation, return validate.SnpAttestation(attestation, options) } -type stubAzureInstanceInfo struct { +type stubInstanceInfo struct { AttestationReport []byte - RuntimeData []byte - VCEK []byte + ReportSigner []byte CertChain []byte + Azure *stubAzureInstanceInfo +} + +type stubAzureInstanceInfo struct { + RuntimeData []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, + ReportSigner: vcek, CertChain: certChain, + Azure: &stubAzureInstanceInfo{ + RuntimeData: decodedRuntime, + }, }, 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..700a3aa865 --- /dev/null +++ b/internal/attestation/snp/BUILD.bazel @@ -0,0 +1,32 @@ +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_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 new file mode 100644 index 0000000000..e7b285ba61 --- /dev/null +++ b/internal/attestation/snp/snp.go @@ -0,0 +1,260 @@ +/* +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" +) + +// 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. + 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 + // if the IDKeyDigest cannot be verified. + 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(getter trust.HTTPSGetter, + fallbackCerts CertificateChain, logger attestation.Logger, +) (*spb.Attestation, error) { + report, err := abi.ReportToProto(a.AttestationReport) + if err != nil { + return nil, fmt.Errorf("converting report to proto: %w", err) + } + + productName := kds.ProductString(Product()) + + att := &spb.Attestation{ + Report: report, + CertificateChain: &spb.CertificateChain{}, + Product: Product(), + } + + // Add VCEK/VLEK to attestation object. + signingInfo, err := a.addReportSigner(att, report, productName, getter, logger) + if err != nil { + return nil, fmt.Errorf("adding report signer: %w", err) + } + + // 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, signingInfo, 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 +} + +// 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, + 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", "SEV-VLEK-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 +} + +// 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. + 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) + } + + reportSigner, err := x509.ParseCertificate(block.Bytes) + if err != nil { + return nil, fmt.Errorf("parsing VCEK certificate: %w", err) + } + + return reportSigner, nil +} diff --git a/internal/attestation/snp/snp_test.go b/internal/attestation/snp/snp_test.go new file mode 100644 index 0000000000..0179ac05b4 --- /dev/null +++ b/internal/attestation/snp/snp_test.go @@ -0,0 +1,343 @@ +/* +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/google/go-sev-guest/verify/trust" + "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{ + ReportSigner: tc.VCEK, + } + + vcek, err := instanceInfo.ParseReportSigner() + if tc.wantErr { + assert.Error(err) + } else { + assert.NoError(err) + assert.Equal(tc.wantVCEK, vcek != nil) + } + }) + } +} + +// 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}, + } + cfg := config.DefaultForAzureSEVSNP() + + testCases := map[string]struct { + report []byte + idkeydigest string + reportSigner []byte + certChain []byte + fallbackCerts CertificateChain + getter *stubHTTPSGetter + expectedArk *x509.Certificate + expectedAsk *x509.Certificate + wantErr bool + }{ + "success": { + 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, + idkeydigest: "57e229e0ffe5fa92d0faddff6cae0e61c926fc9ef9afd20a8b8cfcf7129db9338cbe5bf3f6987733a2bf65d06dc38fc1", + certChain: testdata.CertChain, + getter: newStubHTTPSGetter( + &urlResponseMatcher{ + vcekResponse: testdata.AmdKdsVCEK, + wantVcekRequest: true, + }, + nil, + ), + expectedArk: testdataArk, + expectedAsk: testdataAsk, + }, + "retrieve certchain": { + report: defaultReport, + idkeydigest: "57e229e0ffe5fa92d0faddff6cae0e61c926fc9ef9afd20a8b8cfcf7129db9338cbe5bf3f6987733a2bf65d06dc38fc1", + reportSigner: testdata.AzureThimVCEK, + getter: newStubHTTPSGetter( + &urlResponseMatcher{ + certChainResponse: testdata.CertChain, + wantCertChainRequest: true, + }, + nil, + ), + expectedArk: testdataArk, + expectedAsk: testdataAsk, + }, + "use fallback certs": { + report: defaultReport, + idkeydigest: "57e229e0ffe5fa92d0faddff6cae0e61c926fc9ef9afd20a8b8cfcf7129db9338cbe5bf3f6987733a2bf65d06dc38fc1", + reportSigner: testdata.AzureThimVCEK, + fallbackCerts: NewCertificateChain(exampleCert, exampleCert), + getter: newStubHTTPSGetter( + &urlResponseMatcher{}, + nil, + ), + expectedArk: exampleCert, + expectedAsk: exampleCert, + }, + "use certchain with fallback certs": { + report: defaultReport, + idkeydigest: "57e229e0ffe5fa92d0faddff6cae0e61c926fc9ef9afd20a8b8cfcf7129db9338cbe5bf3f6987733a2bf65d06dc38fc1", + certChain: testdata.CertChain, + reportSigner: testdata.AzureThimVCEK, + fallbackCerts: NewCertificateChain(&x509.Certificate{}, &x509.Certificate{}), + getter: newStubHTTPSGetter( + &urlResponseMatcher{}, + nil, + ), + expectedArk: testdataArk, + expectedAsk: testdataAsk, + }, + "retrieve vcek and certchain": { + report: defaultReport, + idkeydigest: "57e229e0ffe5fa92d0faddff6cae0e61c926fc9ef9afd20a8b8cfcf7129db9338cbe5bf3f6987733a2bf65d06dc38fc1", + 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, + ReportSigner: tc.reportSigner, + } + + defer trust.ClearProductCertCache() + att, err := instanceInfo.AttestationWithCerts(tc.getter, tc.fallbackCerts, logger.NewTest(t)) + if tc.wantErr { + assert.Error(err) + } else { + require.NoError(err) + assert.NotNil(att) + assert.NotNil(att.CertificateChain) + assert.NotNil(att.Report) + + 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. + 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 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|vlek)\/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 82% rename from internal/attestation/azure/snp/testdata/BUILD.bazel rename to internal/attestation/snp/testdata/BUILD.bazel index 9b09a6fdbd..c460ed611d 100644 --- a/internal/attestation/azure/snp/testdata/BUILD.bazel +++ b/internal/attestation/snp/testdata/BUILD.bazel @@ -9,7 +9,9 @@ go_library( "certchain.pem", "runtimedata.bin", "vcek.pem", + "vlek.pem", + "vlekcertchain.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/snp/testdata/testdata.go b/internal/attestation/snp/testdata/testdata.go new file mode 100644 index 0000000000..c749dd8998 --- /dev/null +++ b/internal/attestation/snp/testdata/testdata.go @@ -0,0 +1,48 @@ +/* +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" + +// 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 +var AzureThimVCEK []byte + +// AmdKdsVCEK is an example VCEK certificate (DER, as returned from AMD KDS) for the AttestationReport. +// +//go:embed vcek.cert +var AmdKdsVCEK []byte + +// RuntimeData is an example runtime data from the TPM for the AttestationReport. +// +//go:embed runtimedata.bin +var RuntimeData []byte + +// CertChain is a valid certificate chain (PEM, as returned from Azure THIM) for the VCEK certificate. +// +//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/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 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..7821a63b57 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" @@ -16,6 +17,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. @@ -64,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 embedded Raw values 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/aws.go b/internal/config/aws.go new file mode 100644 index 0000000000..f10fd4f86a --- /dev/null +++ b/internal/config/aws.go @@ -0,0 +1,117 @@ +/* +Copyright (c) Edgeless Systems GmbH + +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" +) + +// 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) + } + + 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 { + // 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 fmt.Errorf("fetching latest TCB versions from configapi: %w", 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. +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..c6690e9724 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), } } @@ -70,16 +69,21 @@ 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) + // 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.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.go b/internal/config/config.go index 8c896583e9..96af3578d3 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{})}, @@ -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 { @@ -915,80 +921,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 +1036,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"` +} + +// 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/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, diff --git a/internal/verify/BUILD.bazel b/internal/verify/BUILD.bazel index 1d02507041..c8927a51bf 100644 --- a/internal/verify/BUILD.bazel +++ b/internal/verify/BUILD.bazel @@ -1,9 +1,28 @@ 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"], importpath = "github.com/edgelesssys/constellation/v2/internal/verify", visibility = ["//:__subpackages__"], - deps = ["@com_github_golang_jwt_jwt_v5//:jwt"], + deps = [ + "//internal/attestation/snp", + "//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", + ], +) + +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/verify.go b/internal/verify/verify.go index 4b7bb53c95..fa41079f02 100644 --- a/internal/verify/verify.go +++ b/internal/verify/verify.go @@ -9,29 +9,309 @@ 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/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"` - VCEK []Certificate `json:"vcek"` - CertChain []Certificate `json:"cert_chain"` - MAAToken MaaTokenClaims `json:"maa_token"` + SNPReport SNPReport `json:"snp_report"` + ReportSigner []Certificate `json:"report_signer"` + 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, attestationCfg config.AttestationCfg, 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 = vlekCert + case abi.VcekReportSigner.String(): + 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 %s: %w", certTypeName, err) + } + + // check if issuer included certChain before parsing. If not included, manually collect from the cluster. + rawCerts := instanceInfo.CertChain + if certTypeName == vlekCert { + rawCerts, err = getCertChain(attestationCfg) + if err != nil { + return Report{}, fmt.Errorf("getting certificate chain cache: %w", err) + } + } + + certChain, err := newCertificates(certificateChain, rawCerts, log) + if err != nil { + return Report{}, fmt.Errorf("parsing certificate chain: %w", err) + } + + var azure *AzureReportAddition + var aws *AWSReportAddition + if instanceInfo.Azure != nil { + 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) + } + azure = &AzureReportAddition{ + MAAToken: maaToken, + } + } + + return Report{ + SNPReport: snpReport, + ReportSigner: reportSigner, + CertChain: certChain, + AzureReportAddition: azure, + AWSReportAddition: aws, + }, 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 { + 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,omitempty"` + CspID string `json:"csp_id,omitempty"` + 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) + } + + certX509, err := x509.ParseCertificate(block.Bytes) + if err != nil { + return certs, fmt.Errorf("parse %s: %w", certTypeName, err) + } + + 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 %s extensions: %w", certTypeName, err) + } + } + 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 { + 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 == 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 %s extensions: %w", c.CertTypeName, 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 +326,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 +403,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 +600,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()) + } + }) + } +} 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 }