/* Copyright (c) Edgeless Systems GmbH SPDX-License-Identifier: AGPL-3.0-only */ package cmd import ( "context" "fmt" "net/http" "net/url" "path" "strings" "time" "github.com/edgelesssys/constellation/v2/internal/attestation/measurements" "github.com/edgelesssys/constellation/v2/internal/cloud/cloudprovider" "github.com/edgelesssys/constellation/v2/internal/config" "github.com/edgelesssys/constellation/v2/internal/constants" "github.com/edgelesssys/constellation/v2/internal/file" "github.com/edgelesssys/constellation/v2/internal/sigstore" "github.com/edgelesssys/constellation/v2/internal/versionsapi" "github.com/spf13/afero" "github.com/spf13/cobra" ) func newConfigFetchMeasurementsCmd() *cobra.Command { cmd := &cobra.Command{ Use: "fetch-measurements", Short: "Fetch measurements for configured cloud provider and image", Long: "Fetch measurements for configured cloud provider and image.\n\n" + "A config needs to be generated first.", Args: cobra.ExactArgs(0), RunE: runConfigFetchMeasurements, } cmd.Flags().StringP("url", "u", "", "alternative URL to fetch measurements from") cmd.Flags().StringP("signature-url", "s", "", "alternative URL to fetch measurements' signature from") return cmd } type fetchMeasurementsFlags struct { measurementsURL *url.URL signatureURL *url.URL configPath string force bool } type configFetchMeasurementsCmd struct { log debugLog } func runConfigFetchMeasurements(cmd *cobra.Command, args []string) error { log, err := newCLILogger(cmd) if err != nil { return fmt.Errorf("creating logger: %w", err) } defer log.Sync() fileHandler := file.NewHandler(afero.NewOsFs()) rekor, err := sigstore.NewRekor() if err != nil { return fmt.Errorf("constructing Rekor client: %w", err) } cfm := &configFetchMeasurementsCmd{log: log} return cfm.configFetchMeasurements(cmd, rekor, []byte(constants.CosignPublicKey), fileHandler, http.DefaultClient) } func (cfm *configFetchMeasurementsCmd) configFetchMeasurements( cmd *cobra.Command, verifier rekorVerifier, cosignPublicKey []byte, fileHandler file.Handler, client *http.Client, ) error { flags, err := cfm.parseFetchMeasurementsFlags(cmd) if err != nil { return err } cfm.log.Debugf("Using flags %v", flags) cfm.log.Debugf("Loading configuration file from %q", flags.configPath) conf, err := config.New(fileHandler, flags.configPath, flags.force) if err != nil { return config.DisplayValidationErrors(cmd.ErrOrStderr(), err) } if !conf.IsReleaseImage() { cmd.PrintErrln("Configured image doesn't look like a released production image. Double check image before deploying to production.") } cfm.log.Debugf("Creating context") ctx, cancel := context.WithTimeout(context.Background(), time.Minute) defer cancel() cfm.log.Debugf("Updating URLs") if err := flags.updateURLs(conf); err != nil { return err } cfm.log.Debugf("Fetching and verifying measurements") var fetchedMeasurements measurements.M hash, err := fetchedMeasurements.FetchAndVerify( ctx, client, flags.measurementsURL, flags.signatureURL, cosignPublicKey, measurements.WithMetadata{ CSP: conf.GetProvider(), Image: conf.Image, }, ) if err != nil { return err } cfm.log.Debugf("Fetched and verified measurements, hash is %s", hash) if err := verifyWithRekor(cmd.Context(), verifier, hash); err != nil { cmd.PrintErrf("Ignoring Rekor related error: %v\n", err) cmd.PrintErrln("Make sure the downloaded measurements are trustworthy!") } cfm.log.Debugf("Verified measurements with Rekor, updating measurements in configuration") conf.UpdateMeasurements(fetchedMeasurements) if err := fileHandler.WriteYAML(flags.configPath, conf, file.OptOverwrite); err != nil { return err } cfm.log.Debugf("Wrote configuration to YAML") return nil } // parseURLFlag checks that flag can be parsed as URL. // If no value was provided for flag, nil is returned. func (cfm *configFetchMeasurementsCmd) parseURLFlag(cmd *cobra.Command, flag string) (*url.URL, error) { rawURL, err := cmd.Flags().GetString(flag) if err != nil { return nil, fmt.Errorf("parsing config generate flags '%s': %w", flag, err) } cfm.log.Debugf("Flag %s has raw URL %q", flag, rawURL) if rawURL != "" { cfm.log.Debugf("Parsing raw URL") return url.Parse(rawURL) } return nil, nil } func (cfm *configFetchMeasurementsCmd) parseFetchMeasurementsFlags(cmd *cobra.Command) (*fetchMeasurementsFlags, error) { measurementsURL, err := cfm.parseURLFlag(cmd, "url") if err != nil { return &fetchMeasurementsFlags{}, err } cfm.log.Debugf("Parsed measurements URL as %v", measurementsURL) measurementsSignatureURL, err := cfm.parseURLFlag(cmd, "signature-url") if err != nil { return &fetchMeasurementsFlags{}, err } cfm.log.Debugf("Parsed measurements signature URL as %v", measurementsSignatureURL) config, err := cmd.Flags().GetString("config") if err != nil { return &fetchMeasurementsFlags{}, fmt.Errorf("parsing config path argument: %w", err) } cfm.log.Debugf("Configuration path is %q", config) force, err := cmd.Flags().GetBool("force") if err != nil { return &fetchMeasurementsFlags{}, fmt.Errorf("parsing force argument: %w", err) } return &fetchMeasurementsFlags{ measurementsURL: measurementsURL, signatureURL: measurementsSignatureURL, configPath: config, force: force, }, nil } func (f *fetchMeasurementsFlags) updateURLs(conf *config.Config) error { if f.measurementsURL == nil { url, err := measurementURL(conf.GetProvider(), conf.Image, "measurements.json") if err != nil { return err } f.measurementsURL = url } if f.signatureURL == nil { url, err := measurementURL(conf.GetProvider(), conf.Image, "measurements.json.sig") if err != nil { return err } f.signatureURL = url } return nil } func measurementURL(provider cloudprovider.Provider, image, file string) (*url.URL, error) { ver, err := versionsapi.NewVersionFromShortPath(image, versionsapi.VersionKindImage) if err != nil { return nil, fmt.Errorf("creating version from image name: %w", err) } artifactBaseURL := ver.ArtifactURL() url, err := url.Parse(artifactBaseURL) if err != nil { return nil, fmt.Errorf("parsing artifact base URL %s: %w", artifactBaseURL, err) } url.Path = path.Join(url.Path, "image", "csp", strings.ToLower(provider.String()), file) return url, nil }