diff --git a/.github/actions/e2e_verify/action.yml b/.github/actions/e2e_verify/action.yml index 07abb7a889..6b0bcf885b 100644 --- a/.github/actions/e2e_verify/action.yml +++ b/.github/actions/e2e_verify/action.yml @@ -94,13 +94,11 @@ runs: COSIGN_PASSWORD: ${{ inputs.cosignPassword }} COSIGN_PRIVATE_KEY: ${{ inputs.cosignPrivateKey }} run: | - reports=(attestation-report-*.json) - if [ -z ${#reports[@]} ]; then - exit 1 - fi + reports=attestation-report-*.json - for file in "${reports[@]}"; do - path=$(realpath "${file}") - cat "${path}" - bazel run //internal/api/attestationconfigapi/cli -- upload ${{ inputs.attestationVariant }} attestation-report "${path}" - done + report=$(bazel run //internal/api/attestationconfigapi/cli -- compare ${{ inputs.attestationVariant }} ${reports}) + + path=$(realpath "${report}") + cat "${path}" + + bazel run //internal/api/attestationconfigapi/cli -- upload ${{ inputs.attestationVariant }} attestation-report "${path}" diff --git a/internal/api/attestationconfigapi/cli/BUILD.bazel b/internal/api/attestationconfigapi/cli/BUILD.bazel index 32ec8ec2bf..df2856aeba 100644 --- a/internal/api/attestationconfigapi/cli/BUILD.bazel +++ b/internal/api/attestationconfigapi/cli/BUILD.bazel @@ -10,6 +10,7 @@ go_binary( go_library( name = "cli_lib", srcs = [ + "compare.go", "delete.go", "main.go", "upload.go", diff --git a/internal/api/attestationconfigapi/cli/client/reporter.go b/internal/api/attestationconfigapi/cli/client/reporter.go index 7e40d6a3e1..295c7b2b74 100644 --- a/internal/api/attestationconfigapi/cli/client/reporter.go +++ b/internal/api/attestationconfigapi/cli/client/reporter.go @@ -31,6 +31,25 @@ func reportVersionDir(attestation variant.Variant) string { return path.Join(attestationconfigapi.AttestationURLPath, attestation.String(), cachedVersionsSubDir) } +// IsInputNewerThanOtherVersion compares the input version with the other version and returns true if the input version is newer. +// This function panics if the input versions are not TDX or SEV-SNP versions. +func IsInputNewerThanOtherVersion(variant variant.Variant, inputVersion, otherVersion any) bool { + var result bool + actionForVariant(variant, + func() { + input := inputVersion.(attestationconfigapi.TDXVersion) + other := otherVersion.(attestationconfigapi.TDXVersion) + result = isInputNewerThanOtherTDXVersion(input, other) + }, + func() { + input := inputVersion.(attestationconfigapi.SEVSNPVersion) + other := otherVersion.(attestationconfigapi.SEVSNPVersion) + result = isInputNewerThanOtherSEVSNPVersion(input, other) + }, + ) + return result +} + // UploadLatestVersion 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. @@ -90,7 +109,7 @@ func (c Client) UploadLatestVersion( } c.log.Info(fmt.Sprintf("Found minimal version: %+v with date: %s", minVersion, minDate)) - if !isInputNewerThanOtherVersion(attestationVariant, minVersion, latestVersionInAPI) { + if !IsInputNewerThanOtherVersion(attestationVariant, minVersion, latestVersionInAPI) { c.log.Info(fmt.Sprintf("Input version: %+v is not newer than latest API version: %+v. Skipping list update", minVersion, latestVersionInAPI)) return ErrNoNewerVersion } @@ -200,7 +219,7 @@ func findMinimalVersion[T attestationconfigapi.TDXVersion | attestationconfigapi // If the current minimal version has newer versions than the one we just fetched, // update the minimal version to the older version. - if isInputNewerThanOtherVersion(variant, *minimalVersion, obj.getVersion()) { + if IsInputNewerThanOtherVersion(variant, *minimalVersion, obj.getVersion()) { v := obj.getVersion().(T) minimalVersion = &v minimalDate = date @@ -210,23 +229,6 @@ func findMinimalVersion[T attestationconfigapi.TDXVersion | attestationconfigapi return *minimalVersion, minimalDate, nil } -func isInputNewerThanOtherVersion(variant variant.Variant, inputVersion, otherVersion any) bool { - var result bool - actionForVariant(variant, - func() { - input := inputVersion.(attestationconfigapi.TDXVersion) - other := otherVersion.(attestationconfigapi.TDXVersion) - result = isInputNewerThanOtherTDXVersion(input, other) - }, - func() { - input := inputVersion.(attestationconfigapi.SEVSNPVersion) - other := otherVersion.(attestationconfigapi.SEVSNPVersion) - result = isInputNewerThanOtherSEVSNPVersion(input, other) - }, - ) - return result -} - type apiVersionObject struct { version string `json:"-"` variant variant.Variant `json:"-"` diff --git a/internal/api/attestationconfigapi/cli/compare.go b/internal/api/attestationconfigapi/cli/compare.go new file mode 100644 index 0000000000..49988fefbb --- /dev/null +++ b/internal/api/attestationconfigapi/cli/compare.go @@ -0,0 +1,101 @@ +/* +Copyright (c) Edgeless Systems GmbH + +SPDX-License-Identifier: AGPL-3.0-only +*/ +package main + +import ( + "fmt" + "os" + "slices" + + "github.com/edgelesssys/constellation/v2/internal/api/attestationconfigapi/cli/client" + "github.com/edgelesssys/constellation/v2/internal/attestation/variant" + "github.com/edgelesssys/constellation/v2/internal/file" + "github.com/edgelesssys/constellation/v2/internal/verify" + "github.com/google/go-tdx-guest/proto/tdx" + "github.com/spf13/afero" + "github.com/spf13/cobra" +) + +func newCompareCmd() *cobra.Command { + cmd := &cobra.Command{ + Use: "compare VARIANT FILE [FILE...]", + Short: "Returns the minimum version of all given attestation reports.", + Long: "Compare a list of attestation reports and return the report with the minimum version.", + Example: "cli compare azure-sev-snp report1.json report2.json", + Args: cobra.MatchAll(cobra.MinimumNArgs(2), arg0isAttestationVariant()), + RunE: runCompare, + } + + return cmd +} + +func runCompare(cmd *cobra.Command, args []string) error { + cmd.SetOut(os.Stdout) + + variant, err := variant.FromString(args[0]) + if err != nil { + return fmt.Errorf("parsing variant: %w", err) + } + + return compare(cmd, variant, args[1:], file.NewHandler(afero.NewOsFs())) +} + +func compare(cmd *cobra.Command, attestationVariant variant.Variant, files []string, fs file.Handler) (retErr error) { + if !slices.Contains([]variant.Variant{variant.AWSSEVSNP{}, variant.AzureSEVSNP{}, variant.GCPSEVSNP{}, variant.AzureTDX{}}, attestationVariant) { + return fmt.Errorf("variant %s not supported", attestationVariant) + } + + lowestVersion, err := compareVersions(attestationVariant, files, fs) + if err != nil { + return fmt.Errorf("comparing versions: %w", err) + } + + cmd.Println(lowestVersion) + return nil +} + +func compareVersions(attestationVariant variant.Variant, files []string, fs file.Handler) (string, error) { + readReport := readSNPReport + if attestationVariant.Equal(variant.AzureTDX{}) { + readReport = readTDXReport + } + + lowestVersion := files[0] + lowestReport, err := readReport(files[0], fs) + if err != nil { + return "", fmt.Errorf("reading tdx report: %w", err) + } + + for _, file := range files[1:] { + report, err := readReport(file, fs) + if err != nil { + return "", fmt.Errorf("reading tdx report: %w", err) + } + + if client.IsInputNewerThanOtherVersion(attestationVariant, lowestReport, report) { + lowestVersion = file + lowestReport = report + } + } + + return lowestVersion, nil +} + +func readSNPReport(file string, fs file.Handler) (any, error) { + var report verify.Report + if err := fs.ReadJSON(file, &report); err != nil { + return nil, fmt.Errorf("reading snp report: %w", err) + } + return convertTCBVersionToSNPVersion(report.SNPReport.LaunchTCB), nil +} + +func readTDXReport(file string, fs file.Handler) (any, error) { + var report *tdx.QuoteV4 + if err := fs.ReadJSON(file, &report); err != nil { + return nil, fmt.Errorf("reading tdx report: %w", err) + } + return convertQuoteToTDXVersion(report), nil +} diff --git a/internal/api/attestationconfigapi/cli/delete.go b/internal/api/attestationconfigapi/cli/delete.go index 013c69481d..a800f7e800 100644 --- a/internal/api/attestationconfigapi/cli/delete.go +++ b/internal/api/attestationconfigapi/cli/delete.go @@ -26,11 +26,11 @@ import ( // newDeleteCmd creates the delete command. func newDeleteCmd() *cobra.Command { cmd := &cobra.Command{ - Use: "delete {aws-sev-snp|azure-sev-snp|azure-tdx|gcp-sev-snp} {attestation-report|guest-firmware} ", + Use: "delete VARIANT KIND ", 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-sev-snp attestation-report 1.0.0", - Args: cobra.MatchAll(cobra.ExactArgs(3), isAttestationVariant(0), isValidKind(1)), + Args: cobra.MatchAll(cobra.ExactArgs(3), arg0isAttestationVariant(), isValidKind(1)), PreRunE: envCheck, RunE: runDelete, } @@ -40,7 +40,7 @@ func newDeleteCmd() *cobra.Command { 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-sev-snp", - Args: cobra.MatchAll(cobra.ExactArgs(1), isAttestationVariant(0)), + Args: cobra.MatchAll(cobra.ExactArgs(1), arg0isAttestationVariant()), RunE: runRecursiveDelete, } diff --git a/internal/api/attestationconfigapi/cli/main.go b/internal/api/attestationconfigapi/cli/main.go index e6e951f1b1..ee1b0c354c 100644 --- a/internal/api/attestationconfigapi/cli/main.go +++ b/internal/api/attestationconfigapi/cli/main.go @@ -27,8 +27,11 @@ const ( distributionID = constants.CDNDefaultDistributionID envCosignPwd = "COSIGN_PASSWORD" envCosignPrivateKey = "COSIGN_PRIVATE_KEY" - // versionWindowSize defines the number of versions to be considered for the latest version. Each week 5 versions are uploaded for each node of the verify cluster. - versionWindowSize = 15 + // versionWindowSize defines the number of versions to be considered for the latest version. + // Through our weekly e2e tests, each week 2 versions are uploaded: + // One from a stable release, and one from a debug image. + // A window size of 6 ensures we update only after a version has been "stable" for 3 weeks. + versionWindowSize = 6 ) var ( @@ -56,6 +59,7 @@ func newRootCmd() *cobra.Command { rootCmd.AddCommand(newUploadCmd()) rootCmd.AddCommand(newDeleteCmd()) + rootCmd.AddCommand(newCompareCmd()) return rootCmd } diff --git a/internal/api/attestationconfigapi/cli/upload.go b/internal/api/attestationconfigapi/cli/upload.go index 54edb01c65..4032f46266 100644 --- a/internal/api/attestationconfigapi/cli/upload.go +++ b/internal/api/attestationconfigapi/cli/upload.go @@ -29,7 +29,7 @@ import ( func newUploadCmd() *cobra.Command { uploadCmd := &cobra.Command{ - Use: "upload {aws-sev-snp|azure-sev-snp|azure-tdx|gcp-sev-snp} {attestation-report|guest-firmware} ", + Use: "upload VARIANT KIND FILE", 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.\n"+ @@ -41,7 +41,7 @@ func newUploadCmd() *cobra.Command { ), Example: "COSIGN_PASSWORD=$CPW COSIGN_PRIVATE_KEY=$CKEY cli upload azure-sev-snp attestation-report /some/path/report.json", - Args: cobra.MatchAll(cobra.ExactArgs(3), isAttestationVariant(0), isValidKind(1)), + Args: cobra.MatchAll(cobra.ExactArgs(3), arg0isAttestationVariant(), isValidKind(1)), PreRunE: envCheck, RunE: runUpload, } @@ -120,24 +120,20 @@ func uploadReport( latestVersion = latestVersionInAPI.SEVSNPVersion log.Info(fmt.Sprintf("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) + newVersion, err = readSNPReport(cfg.path, fs) + if err != nil { + return err } - - newVersion = convertTCBVersionToSNPVersion(report.SNPReport.LaunchTCB) log.Info(fmt.Sprintf("Input SNP report: %+v", newVersion)) case variant.AzureTDX{}: latestVersion = latestVersionInAPI.TDXVersion log.Info(fmt.Sprintf("Reading TDX report from file: %s", cfg.path)) - var report *tdx.QuoteV4 - if err := fs.ReadJSON(cfg.path, &report); err != nil { - return fmt.Errorf("reading tdx report: %w", err) + newVersion, err = readTDXReport(cfg.path, fs) + if err != nil { + return err } - - newVersion = convertQuoteToTDXVersion(report) log.Info(fmt.Sprintf("Input TDX report: %+v", newVersion)) default: diff --git a/internal/api/attestationconfigapi/cli/validargs.go b/internal/api/attestationconfigapi/cli/validargs.go index 0c77ce0512..b5366b0f96 100644 --- a/internal/api/attestationconfigapi/cli/validargs.go +++ b/internal/api/attestationconfigapi/cli/validargs.go @@ -7,6 +7,7 @@ SPDX-License-Identifier: AGPL-3.0-only package main import ( + "errors" "fmt" "strings" @@ -14,17 +15,17 @@ import ( "github.com/spf13/cobra" ) -func isAttestationVariant(arg int) cobra.PositionalArgs { +func arg0isAttestationVariant() cobra.PositionalArgs { return func(_ *cobra.Command, args []string) error { - attestationVariant, err := variant.FromString(args[arg]) + attestationVariant, err := variant.FromString(args[0]) if err != nil { - return fmt.Errorf("argument %s isn't a valid attestation variant", args[arg]) + return errors.New("argument 0 isn't a valid attestation variant") } switch attestationVariant { case variant.AWSSEVSNP{}, variant.AzureSEVSNP{}, variant.AzureTDX{}, variant.GCPSEVSNP{}: return nil default: - return fmt.Errorf("argument %s isn't a supported attestation variant", args[arg]) + return errors.New("argument 0 isn't a supported attestation variant") } } } @@ -32,7 +33,7 @@ func isAttestationVariant(arg int) cobra.PositionalArgs { func isValidKind(arg int) cobra.PositionalArgs { return func(_ *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 fmt.Errorf("argument %s isn't a valid kind: must be one of [%q, %q]", args[arg], attestationReport, guestFirmware) } return nil }