diff --git a/image/upload/internal/cmd/BUILD.bazel b/image/upload/internal/cmd/BUILD.bazel index 5e99b3370..e2729b184 100644 --- a/image/upload/internal/cmd/BUILD.bazel +++ b/image/upload/internal/cmd/BUILD.bazel @@ -9,6 +9,11 @@ go_library( "flags.go", "gcp.go", "image.go", + "info.go", + "measurements.go", + "measurementsenvelope.go", + "measurementsmerge.go", + "measurementsupload.go", "must.go", "nop.go", "openstack.go", @@ -19,6 +24,7 @@ go_library( importpath = "github.com/edgelesssys/constellation/v2/image/upload/internal/cmd", visibility = ["//image/upload:__subpackages__"], deps = [ + "//internal/attestation/measurements", "//internal/cloud/cloudprovider", "//internal/logger", "//internal/osimage", @@ -26,6 +32,8 @@ go_library( "//internal/osimage/aws", "//internal/osimage/azure", "//internal/osimage/gcp", + "//internal/osimage/imageinfo", + "//internal/osimage/measurementsuploader", "//internal/osimage/nop", "//internal/osimage/secureboot", "//internal/versionsapi", diff --git a/image/upload/internal/cmd/flags.go b/image/upload/internal/cmd/flags.go index 37a6f57ab..59ec163e1 100644 --- a/image/upload/internal/cmd/flags.go +++ b/image/upload/internal/cmd/flags.go @@ -7,6 +7,7 @@ SPDX-License-Identifier: AGPL-3.0-only package cmd import ( + "errors" "os" "path/filepath" "time" @@ -198,3 +199,143 @@ func parseGCPFlags(cmd *cobra.Command) (gcpFlags, error) { gcpBucket: gcpBucket, }, nil } + +type s3Flags struct { + region string + bucket string + logLevel zapcore.Level +} + +func parseS3Flags(cmd *cobra.Command) (s3Flags, error) { + region, err := cmd.Flags().GetString("region") + if err != nil { + return s3Flags{}, err + } + bucket, err := cmd.Flags().GetString("bucket") + if err != nil { + return s3Flags{}, err + } + verbose, err := cmd.Flags().GetBool("verbose") + if err != nil { + return s3Flags{}, err + } + logLevel := zapcore.InfoLevel + if verbose { + logLevel = zapcore.DebugLevel + } + + return s3Flags{ + region: region, + bucket: bucket, + logLevel: logLevel, + }, nil +} + +type measurementsFlags struct { + s3Flags + measurementsPath string + signaturePath string +} + +func parseUploadMeasurementsFlags(cmd *cobra.Command) (measurementsFlags, error) { + s3, err := parseS3Flags(cmd) + if err != nil { + return measurementsFlags{}, err + } + + measurementsPath, err := cmd.Flags().GetString("measurements") + if err != nil { + return measurementsFlags{}, err + } + signaturePath, err := cmd.Flags().GetString("signature") + if err != nil { + return measurementsFlags{}, err + } + + return measurementsFlags{ + s3Flags: s3, + measurementsPath: measurementsPath, + signaturePath: signaturePath, + }, nil +} + +type mergeMeasurementsFlags struct { + out string + logLevel zapcore.Level +} + +func parseMergeMeasurementsFlags(cmd *cobra.Command) (mergeMeasurementsFlags, error) { + out, err := cmd.Flags().GetString("out") + if err != nil { + return mergeMeasurementsFlags{}, err + } + verbose, err := cmd.Flags().GetBool("verbose") + if err != nil { + return mergeMeasurementsFlags{}, err + } + logLevel := zapcore.InfoLevel + if verbose { + logLevel = zapcore.DebugLevel + } + + return mergeMeasurementsFlags{ + out: out, + logLevel: logLevel, + }, nil +} + +type envelopeMeasurementsFlags struct { + version versionsapi.Version + csp cloudprovider.Provider + attestationVariant string + in, out string + logLevel zapcore.Level +} + +func parseEnvelopeMeasurementsFlags(cmd *cobra.Command) (envelopeMeasurementsFlags, error) { + version, err := cmd.Flags().GetString("version") + if err != nil { + return envelopeMeasurementsFlags{}, err + } + ver, err := versionsapi.NewVersionFromShortPath(version, versionsapi.VersionKindImage) + if err != nil { + return envelopeMeasurementsFlags{}, err + } + csp, err := cmd.Flags().GetString("csp") + if err != nil { + return envelopeMeasurementsFlags{}, err + } + provider := cloudprovider.FromString(csp) + attestationVariant, err := cmd.Flags().GetString("attestation-variant") + if err != nil { + return envelopeMeasurementsFlags{}, err + } + if provider == cloudprovider.Unknown { + return envelopeMeasurementsFlags{}, errors.New("unknown cloud provider") + } + in, err := cmd.Flags().GetString("in") + if err != nil { + return envelopeMeasurementsFlags{}, err + } + out, err := cmd.Flags().GetString("out") + if err != nil { + return envelopeMeasurementsFlags{}, err + } + verbose, err := cmd.Flags().GetBool("verbose") + if err != nil { + return envelopeMeasurementsFlags{}, err + } + logLevel := zapcore.InfoLevel + if verbose { + logLevel = zapcore.DebugLevel + } + + return envelopeMeasurementsFlags{ + version: ver, + csp: provider, + attestationVariant: attestationVariant, + in: in, + out: out, + logLevel: logLevel, + }, nil +} diff --git a/image/upload/internal/cmd/info.go b/image/upload/internal/cmd/info.go new file mode 100644 index 000000000..43cf62830 --- /dev/null +++ b/image/upload/internal/cmd/info.go @@ -0,0 +1,83 @@ +/* +Copyright (c) Edgeless Systems GmbH + +SPDX-License-Identifier: AGPL-3.0-only +*/ + +package cmd + +import ( + "encoding/json" + "fmt" + "os" + + "github.com/edgelesssys/constellation/v2/internal/logger" + infoupload "github.com/edgelesssys/constellation/v2/internal/osimage/imageinfo" + "github.com/edgelesssys/constellation/v2/internal/versionsapi" + "github.com/spf13/cobra" +) + +// NewInfoCmd creates a new info parent command. +func NewInfoCmd() *cobra.Command { + cmd := &cobra.Command{ + Use: "info [flags] ...", + Short: "Uploads OS image info to S3", + Long: "Uploads OS image info to S3.", + Args: cobra.MinimumNArgs(1), + RunE: runInfo, + } + + cmd.SetOut(os.Stdout) + + cmd.Flags().String("region", "eu-central-1", "AWS region of the archive S3 bucket") + cmd.Flags().String("bucket", "cdn-constellation-backend", "S3 bucket name of the archive") + cmd.Flags().Bool("verbose", false, "Enable verbose output") + + return cmd +} + +func runInfo(cmd *cobra.Command, args []string) error { + workdir := os.Getenv("BUILD_WORKING_DIRECTORY") + if len(workdir) > 0 { + must(os.Chdir(workdir)) + } + + flags, err := parseS3Flags(cmd) + if err != nil { + return err + } + + log := logger.New(logger.PlainLog, flags.logLevel) + log.Debugf("Parsed flags: %+v", flags) + info, err := readInfoArgs(args) + if err != nil { + return err + } + + uploadC, err := infoupload.New(cmd.Context(), flags.region, flags.bucket, log) + if err != nil { + return fmt.Errorf("uploading image info: %w", err) + } + + url, err := uploadC.Upload(cmd.Context(), info) + if err != nil { + return fmt.Errorf("uploading image info: %w", err) + } + log.Infof("Uploaded image info to %s", url) + return nil +} + +func readInfoArgs(paths []string) (versionsapi.ImageInfo, error) { + infos := make([]versionsapi.ImageInfo, len(paths)) + for i, path := range paths { + f, err := os.Open(path) + if err != nil { + return versionsapi.ImageInfo{}, err + } + defer f.Close() + if err := json.NewDecoder(f).Decode(&infos[i]); err != nil { + return versionsapi.ImageInfo{}, err + } + } + return versionsapi.MergeImageInfos(infos...) +} diff --git a/image/upload/internal/cmd/measurements.go b/image/upload/internal/cmd/measurements.go new file mode 100644 index 000000000..e117b88d9 --- /dev/null +++ b/image/upload/internal/cmd/measurements.go @@ -0,0 +1,32 @@ +/* +Copyright (c) Edgeless Systems GmbH + +SPDX-License-Identifier: AGPL-3.0-only +*/ + +package cmd + +import ( + "os" + + "github.com/spf13/cobra" +) + +// NewMeasurementsCmd creates a new measurements command. Measurements needs another +// verb, and does nothing on its own. +func NewMeasurementsCmd() *cobra.Command { + cmd := &cobra.Command{ + Use: "measurements", + Short: "Handle OS image measurements", + Long: "Handle OS image measurements.", + Args: cobra.ExactArgs(0), + } + + cmd.SetOut(os.Stdout) + + cmd.AddCommand(newMeasurementsUploadCmd()) + cmd.AddCommand(newMeasurementsMergeCmd()) + cmd.AddCommand(newMeasurementsEnvelopeCmd()) + + return cmd +} diff --git a/image/upload/internal/cmd/measurementsenvelope.go b/image/upload/internal/cmd/measurementsenvelope.go new file mode 100644 index 000000000..ff9c0a981 --- /dev/null +++ b/image/upload/internal/cmd/measurementsenvelope.go @@ -0,0 +1,101 @@ +/* +Copyright (c) Edgeless Systems GmbH + +SPDX-License-Identifier: AGPL-3.0-only +*/ + +package cmd + +import ( + "encoding/json" + "fmt" + "os" + + "github.com/edgelesssys/constellation/v2/internal/attestation/measurements" + "github.com/edgelesssys/constellation/v2/internal/logger" + "github.com/spf13/cobra" +) + +// newMeasurementsEnvelopeCmd creates a new envelope command. +func newMeasurementsEnvelopeCmd() *cobra.Command { + cmd := &cobra.Command{ + Use: "envelope", + Short: "Envelope OS image measurements", + Long: "Envelope OS image measurements for one variant to follow the measurements v2 format.", + Args: cobra.ExactArgs(0), + RunE: runEnvelopeMeasurements, + } + + cmd.SetOut(os.Stdout) + cmd.Flags().String("version", "", "Shortname of the os image version.") + cmd.Flags().String("csp", "", "CSP of this image measurement.") + cmd.Flags().String("attestation-variant", "", "Attestation variant of the image measurements.") + cmd.Flags().String("in", "", "Path to read the raw measurements from.") + cmd.Flags().String("out", "", "Optional path to write the enveloped result to. If not set, the result is written to stdout.") + cmd.Flags().Bool("verbose", false, "Enable verbose output") + + must(cmd.MarkFlagRequired("version")) + must(cmd.MarkFlagRequired("csp")) + must(cmd.MarkFlagRequired("attestation-variant")) + must(cmd.MarkFlagRequired("in")) + + return cmd +} + +func runEnvelopeMeasurements(cmd *cobra.Command, _ []string) error { + workdir := os.Getenv("BUILD_WORKING_DIRECTORY") + if len(workdir) > 0 { + must(os.Chdir(workdir)) + } + + flags, err := parseEnvelopeMeasurementsFlags(cmd) + if err != nil { + return err + } + + log := logger.New(logger.PlainLog, flags.logLevel) + log.Debugf("Parsed flags: %+v", flags) + + f, err := os.Open(flags.in) + if err != nil { + return fmt.Errorf("enveloping measurements: opening input file: %w", err) + } + defer f.Close() + var measuremnt rawMeasurements + if err := json.NewDecoder(f).Decode(&measuremnt); err != nil { + return fmt.Errorf("enveloping measurements: reading input file: %w", err) + } + + enveloped := measurements.ImageMeasurementsV2{ + Ref: flags.version.Ref, + Stream: flags.version.Stream, + Version: flags.version.Version, + List: []measurements.ImageMeasurementsV2Entry{ + { + CSP: flags.csp, + AttestationVariant: flags.attestationVariant, + Measurements: measuremnt.Measurements, + }, + }, + } + + out := cmd.OutOrStdout() + if len(flags.out) > 0 { + outF, err := os.Create(flags.out) + if err != nil { + return fmt.Errorf("enveloping measurements: opening output file: %w", err) + } + defer outF.Close() + out = outF + } + + if err := json.NewEncoder(out).Encode(enveloped); err != nil { + return fmt.Errorf("enveloping measurements: writing output file: %w", err) + } + log.Infof("Enveloped image measurements") + return nil +} + +type rawMeasurements struct { + Measurements measurements.M `json:"measurements"` +} diff --git a/image/upload/internal/cmd/measurementsmerge.go b/image/upload/internal/cmd/measurementsmerge.go new file mode 100644 index 000000000..758f54e5d --- /dev/null +++ b/image/upload/internal/cmd/measurementsmerge.go @@ -0,0 +1,85 @@ +/* +Copyright (c) Edgeless Systems GmbH + +SPDX-License-Identifier: AGPL-3.0-only +*/ + +package cmd + +import ( + "encoding/json" + "fmt" + "os" + + "github.com/edgelesssys/constellation/v2/internal/attestation/measurements" + "github.com/edgelesssys/constellation/v2/internal/logger" + "github.com/spf13/cobra" +) + +// newMeasurementsMergeCmd creates a new merge command. +func newMeasurementsMergeCmd() *cobra.Command { + cmd := &cobra.Command{ + Use: "merge [flags] ...", + Short: "Merge OS image measurements", + Long: "Merge OS image measurements.", + Args: cobra.MinimumNArgs(1), + RunE: runMergeMeasurements, + } + + cmd.SetOut(os.Stdout) + cmd.Flags().String("out", "", "Optional path to write the merge result to. If not set, the result is written to stdout.") + cmd.Flags().Bool("verbose", false, "Enable verbose output") + + return cmd +} + +func runMergeMeasurements(cmd *cobra.Command, args []string) error { + workdir := os.Getenv("BUILD_WORKING_DIRECTORY") + if len(workdir) > 0 { + must(os.Chdir(workdir)) + } + + flags, err := parseMergeMeasurementsFlags(cmd) + if err != nil { + return err + } + + log := logger.New(logger.PlainLog, flags.logLevel) + log.Debugf("Parsed flags: %+v", flags) + + mergedMeasurements, err := readMeasurementsArgs(args) + if err != nil { + return fmt.Errorf("merging measurements: reading input files: %w", err) + } + + out := cmd.OutOrStdout() + if len(flags.out) > 0 { + outF, err := os.Create(flags.out) + if err != nil { + return fmt.Errorf("merging measurements: opening output file: %w", err) + } + defer outF.Close() + out = outF + } + + if err := json.NewEncoder(out).Encode(mergedMeasurements); err != nil { + return fmt.Errorf("merging measurements: writing output file: %w", err) + } + log.Infof("Merged image measurements") + return nil +} + +func readMeasurementsArgs(paths []string) (measurements.ImageMeasurementsV2, error) { + measuremnts := make([]measurements.ImageMeasurementsV2, len(paths)) + for i, path := range paths { + f, err := os.Open(path) + if err != nil { + return measurements.ImageMeasurementsV2{}, err + } + defer f.Close() + if err := json.NewDecoder(f).Decode(&measuremnts[i]); err != nil { + return measurements.ImageMeasurementsV2{}, err + } + } + return measurements.MergeImageMeasurementsV2(measuremnts...) +} diff --git a/image/upload/internal/cmd/measurementsupload.go b/image/upload/internal/cmd/measurementsupload.go new file mode 100644 index 000000000..131074db3 --- /dev/null +++ b/image/upload/internal/cmd/measurementsupload.go @@ -0,0 +1,78 @@ +/* +Copyright (c) Edgeless Systems GmbH + +SPDX-License-Identifier: AGPL-3.0-only +*/ + +package cmd + +import ( + "fmt" + "os" + + "github.com/edgelesssys/constellation/v2/internal/logger" + "github.com/edgelesssys/constellation/v2/internal/osimage/measurementsuploader" + "github.com/spf13/cobra" +) + +// newMeasurementsUploadCmd creates a new upload command. +func newMeasurementsUploadCmd() *cobra.Command { + cmd := &cobra.Command{ + Use: "upload", + Short: "Uploads OS image measurements to S3", + Long: "Uploads OS image measurements to S3.", + Args: cobra.ExactArgs(0), + RunE: runMeasurementsUpload, + } + + cmd.SetOut(os.Stdout) + + cmd.Flags().String("measurements", "", "Path to measurements file to upload") + cmd.Flags().String("signature", "", "Path to signature file to upload") + cmd.Flags().String("region", "eu-central-1", "AWS region of the archive S3 bucket") + cmd.Flags().String("bucket", "cdn-constellation-backend", "S3 bucket name of the archive") + cmd.Flags().Bool("verbose", false, "Enable verbose output") + + must(cmd.MarkFlagRequired("measurements")) + must(cmd.MarkFlagRequired("signature")) + + return cmd +} + +func runMeasurementsUpload(cmd *cobra.Command, _ []string) error { + workdir := os.Getenv("BUILD_WORKING_DIRECTORY") + if len(workdir) > 0 { + must(os.Chdir(workdir)) + } + + flags, err := parseUploadMeasurementsFlags(cmd) + if err != nil { + return err + } + + log := logger.New(logger.PlainLog, flags.logLevel) + log.Debugf("Parsed flags: %+v", flags) + + uploadC, err := measurementsuploader.New(cmd.Context(), flags.region, flags.bucket, log) + if err != nil { + return fmt.Errorf("uploading image info: %w", err) + } + + measurements, err := os.Open(flags.measurementsPath) + if err != nil { + return fmt.Errorf("uploading image measurements: opening measurements file: %w", err) + } + defer measurements.Close() + signature, err := os.Open(flags.signaturePath) + if err != nil { + return fmt.Errorf("uploading image measurements: opening signature file: %w", err) + } + defer signature.Close() + + measurementsURL, signatureURL, err := uploadC.Upload(cmd.Context(), measurements, signature) + if err != nil { + return fmt.Errorf("uploading image info: %w", err) + } + log.Infof("Uploaded image measurements to %s (and signature to %s)", measurementsURL, signatureURL) + return nil +} diff --git a/image/upload/upload.go b/image/upload/upload.go index 83b6540fa..a26c79121 100644 --- a/image/upload/upload.go +++ b/image/upload/upload.go @@ -41,6 +41,8 @@ func newRootCmd() *cobra.Command { rootCmd.SetOut(os.Stdout) rootCmd.AddCommand(cmd.NewImageCmd()) + rootCmd.AddCommand(cmd.NewInfoCmd()) + rootCmd.AddCommand(cmd.NewMeasurementsCmd()) return rootCmd } diff --git a/internal/osimage/archive/BUILD.bazel b/internal/osimage/archive/BUILD.bazel index 2a23a3efb..8cfa40529 100644 --- a/internal/osimage/archive/BUILD.bazel +++ b/internal/osimage/archive/BUILD.bazel @@ -6,6 +6,7 @@ go_library( importpath = "github.com/edgelesssys/constellation/v2/internal/osimage/archive", visibility = ["//:__subpackages__"], deps = [ + "//internal/constants", "//internal/logger", "//internal/versionsapi", "@com_github_aws_aws_sdk_go_v2_config//:config", diff --git a/internal/osimage/archive/archive.go b/internal/osimage/archive/archive.go index 4014bacea..2f1ce6974 100644 --- a/internal/osimage/archive/archive.go +++ b/internal/osimage/archive/archive.go @@ -16,6 +16,7 @@ import ( s3manager "github.com/aws/aws-sdk-go-v2/feature/s3/manager" "github.com/aws/aws-sdk-go-v2/service/s3" s3types "github.com/aws/aws-sdk-go-v2/service/s3/types" + "github.com/edgelesssys/constellation/v2/internal/constants" "github.com/edgelesssys/constellation/v2/internal/logger" "github.com/edgelesssys/constellation/v2/internal/versionsapi" ) @@ -58,11 +59,9 @@ func (a *Archivist) Archive(ctx context.Context, version versionsapi.Version, cs Body: img, ChecksumAlgorithm: s3types.ChecksumAlgorithmSha256, }) - return baseURL + key, err + return constants.CDNRepositoryURL + "/" + key, err } type uploadClient interface { Upload(ctx context.Context, input *s3.PutObjectInput, opts ...func(*s3manager.Uploader)) (*s3manager.UploadOutput, error) } - -const baseURL = "https://cdn.confidential.cloud/" diff --git a/internal/osimage/imageinfo/BUILD.bazel b/internal/osimage/imageinfo/BUILD.bazel new file mode 100644 index 000000000..92decbc45 --- /dev/null +++ b/internal/osimage/imageinfo/BUILD.bazel @@ -0,0 +1,17 @@ +load("@io_bazel_rules_go//go:def.bzl", "go_library") + +go_library( + name = "imageinfo", + srcs = ["imageinfo.go"], + importpath = "github.com/edgelesssys/constellation/v2/internal/osimage/imageinfo", + visibility = ["//:__subpackages__"], + deps = [ + "//internal/constants", + "//internal/logger", + "//internal/versionsapi", + "@com_github_aws_aws_sdk_go_v2_config//:config", + "@com_github_aws_aws_sdk_go_v2_feature_s3_manager//:manager", + "@com_github_aws_aws_sdk_go_v2_service_s3//:s3", + "@com_github_aws_aws_sdk_go_v2_service_s3//types", + ], +) diff --git a/internal/osimage/imageinfo/imageinfo.go b/internal/osimage/imageinfo/imageinfo.go new file mode 100644 index 000000000..e87eb9ff0 --- /dev/null +++ b/internal/osimage/imageinfo/imageinfo.go @@ -0,0 +1,78 @@ +/* +Copyright (c) Edgeless Systems GmbH + +SPDX-License-Identifier: AGPL-3.0-only +*/ + +// package imageinfo is used to upload image info JSON files to S3. +package imageinfo + +import ( + "bytes" + "context" + "encoding/json" + "net/url" + + awsconfig "github.com/aws/aws-sdk-go-v2/config" + s3manager "github.com/aws/aws-sdk-go-v2/feature/s3/manager" + "github.com/aws/aws-sdk-go-v2/service/s3" + s3types "github.com/aws/aws-sdk-go-v2/service/s3/types" + "github.com/edgelesssys/constellation/v2/internal/constants" + "github.com/edgelesssys/constellation/v2/internal/logger" + "github.com/edgelesssys/constellation/v2/internal/versionsapi" +) + +// Uploader uploads image info to S3. +type Uploader struct { + uploadClient uploadClient + // bucket is the name of the S3 bucket to use. + bucket string + + log *logger.Logger +} + +// New creates a new Uploader. +func New(ctx context.Context, region, bucket string, log *logger.Logger) (*Uploader, error) { + cfg, err := awsconfig.LoadDefaultConfig(ctx, awsconfig.WithRegion(region)) + if err != nil { + return nil, err + } + s3client := s3.NewFromConfig(cfg) + uploadClient := s3manager.NewUploader(s3client) + + return &Uploader{ + uploadClient: uploadClient, + bucket: bucket, + log: log, + }, nil +} + +// Upload marshals the image info to JSON and uploads it to S3. +func (a *Uploader) Upload(ctx context.Context, imageInfo versionsapi.ImageInfo) (string, error) { + ver := versionsapi.Version{ + Ref: imageInfo.Ref, + Stream: imageInfo.Stream, + Version: imageInfo.Version, + Kind: versionsapi.VersionKindImage, + } + key, err := url.JoinPath(ver.ArtifactPath(versionsapi.APIV2), ver.Kind.String(), "info.json") + if err != nil { + return "", err + } + a.log.Debugf("Archiving image info to s3://%v/%v", a.bucket, key) + buf := &bytes.Buffer{} + if err := json.NewEncoder(buf).Encode(imageInfo); err != nil { + return "", err + } + _, err = a.uploadClient.Upload(ctx, &s3.PutObjectInput{ + Bucket: &a.bucket, + Key: &key, + Body: buf, + ChecksumAlgorithm: s3types.ChecksumAlgorithmSha256, + }) + return constants.CDNRepositoryURL + "/" + key, err +} + +type uploadClient interface { + Upload(ctx context.Context, input *s3.PutObjectInput, opts ...func(*s3manager.Uploader)) (*s3manager.UploadOutput, error) +} diff --git a/internal/osimage/measurementsuploader/BUILD.bazel b/internal/osimage/measurementsuploader/BUILD.bazel new file mode 100644 index 000000000..189835a05 --- /dev/null +++ b/internal/osimage/measurementsuploader/BUILD.bazel @@ -0,0 +1,18 @@ +load("@io_bazel_rules_go//go:def.bzl", "go_library") + +go_library( + name = "measurementsuploader", + srcs = ["measurementsuploader.go"], + importpath = "github.com/edgelesssys/constellation/v2/internal/osimage/measurementsuploader", + visibility = ["//:__subpackages__"], + deps = [ + "//internal/attestation/measurements", + "//internal/constants", + "//internal/logger", + "//internal/versionsapi", + "@com_github_aws_aws_sdk_go_v2_config//:config", + "@com_github_aws_aws_sdk_go_v2_feature_s3_manager//:manager", + "@com_github_aws_aws_sdk_go_v2_service_s3//:s3", + "@com_github_aws_aws_sdk_go_v2_service_s3//types", + ], +) diff --git a/internal/osimage/measurementsuploader/measurementsuploader.go b/internal/osimage/measurementsuploader/measurementsuploader.go new file mode 100644 index 000000000..53964b7ae --- /dev/null +++ b/internal/osimage/measurementsuploader/measurementsuploader.go @@ -0,0 +1,99 @@ +/* +Copyright (c) Edgeless Systems GmbH + +SPDX-License-Identifier: AGPL-3.0-only +*/ + +// package measurementsuploader is used to upload measurements (v2) JSON files (and signatures) to S3. +package measurementsuploader + +import ( + "context" + "encoding/json" + "fmt" + "io" + "net/url" + + awsconfig "github.com/aws/aws-sdk-go-v2/config" + s3manager "github.com/aws/aws-sdk-go-v2/feature/s3/manager" + "github.com/aws/aws-sdk-go-v2/service/s3" + s3types "github.com/aws/aws-sdk-go-v2/service/s3/types" + "github.com/edgelesssys/constellation/v2/internal/attestation/measurements" + "github.com/edgelesssys/constellation/v2/internal/constants" + "github.com/edgelesssys/constellation/v2/internal/logger" + "github.com/edgelesssys/constellation/v2/internal/versionsapi" +) + +// Uploader uploads image info to S3. +type Uploader struct { + uploadClient uploadClient + // bucket is the name of the S3 bucket to use. + bucket string + + log *logger.Logger +} + +// New creates a new Uploader. +func New(ctx context.Context, region, bucket string, log *logger.Logger) (*Uploader, error) { + cfg, err := awsconfig.LoadDefaultConfig(ctx, awsconfig.WithRegion(region)) + if err != nil { + return nil, err + } + s3client := s3.NewFromConfig(cfg) + uploadClient := s3manager.NewUploader(s3client) + + return &Uploader{ + uploadClient: uploadClient, + bucket: bucket, + log: log, + }, nil +} + +// Upload uploads the measurements v2 JSON file and its signature to S3. +func (a *Uploader) Upload(ctx context.Context, rawMeasurement, signature io.ReadSeeker) (string, string, error) { + // parse the measurements to get the ref, stream, and version + var measurements measurements.ImageMeasurementsV2 + if err := json.NewDecoder(rawMeasurement).Decode(&measurements); err != nil { + return "", "", err + } + if _, err := rawMeasurement.Seek(0, io.SeekStart); err != nil { + return "", "", err + } + + ver := versionsapi.Version{ + Ref: measurements.Ref, + Stream: measurements.Stream, + Version: measurements.Version, + Kind: versionsapi.VersionKindImage, + } + key, err := url.JoinPath(ver.ArtifactPath(versionsapi.APIV2), ver.Kind.String(), "measurements.json") + if err != nil { + return "", "", err + } + sigKey, err := url.JoinPath(ver.ArtifactPath(versionsapi.APIV2), ver.Kind.String(), "measurements.json.sig") + if err != nil { + return "", "", err + } + a.log.Debugf("Archiving image measurements to s3://%v/%v and s3://%v/%v", a.bucket, key, a.bucket, sigKey) + if _, err = a.uploadClient.Upload(ctx, &s3.PutObjectInput{ + Bucket: &a.bucket, + Key: &key, + Body: rawMeasurement, + ChecksumAlgorithm: s3types.ChecksumAlgorithmSha256, + }); err != nil { + return "", "", fmt.Errorf("uploading measurements: %w", err) + } + if _, err = a.uploadClient.Upload(ctx, &s3.PutObjectInput{ + Bucket: &a.bucket, + Key: &sigKey, + Body: signature, + ChecksumAlgorithm: s3types.ChecksumAlgorithmSha256, + }); err != nil { + return "", "", fmt.Errorf("uploading measurements signature: %w", err) + } + return constants.CDNRepositoryURL + "/" + key, constants.CDNRepositoryURL + "/" + sigKey, nil +} + +type uploadClient interface { + Upload(ctx context.Context, input *s3.PutObjectInput, opts ...func(*s3manager.Uploader)) (*s3manager.UploadOutput, error) +}