diff --git a/docs/reference/component-cli_component-archive.md b/docs/reference/component-cli_component-archive.md index fea7e240..c9b68522 100644 --- a/docs/reference/component-cli_component-archive.md +++ b/docs/reference/component-cli_component-archive.md @@ -41,5 +41,6 @@ component-cli component-archive [component-archive-path] [ctf-path] [flags] * [component-cli component-archive export](component-cli_component-archive_export.md) - Exports a component archive as defined by CTF * [component-cli component-archive remote](component-cli_component-archive_remote.md) - command to interact with component descriptors stored in an oci registry * [component-cli component-archive resources](component-cli_component-archive_resources.md) - command to modify resources of a component descriptor +* [component-cli component-archive signature](component-cli_component-archive_signature.md) - [EXPERIMENTAL] command to work with signatures and digests in component descriptors * [component-cli component-archive sources](component-cli_component-archive_sources.md) - command to modify sources of a component descriptor diff --git a/docs/reference/component-cli_component-archive_signature.md b/docs/reference/component-cli_component-archive_signature.md new file mode 100644 index 00000000..9770db92 --- /dev/null +++ b/docs/reference/component-cli_component-archive_signature.md @@ -0,0 +1,29 @@ +## component-cli component-archive signature + +[EXPERIMENTAL] command to work with signatures and digests in component descriptors + +### Options + +``` + -h, --help help for signature +``` + +### Options inherited from parent commands + +``` + --cli logger runs as cli logger. enables cli logging + --dev enable development logging which result in console encoding, enabled stacktrace and enabled caller + --disable-caller disable the caller of logs (default true) + --disable-stacktrace disable the stacktrace of error logs (default true) + --disable-timestamp disable timestamp output (default true) + -v, --verbosity int number for the log level verbosity (default 1) +``` + +### SEE ALSO + +* [component-cli component-archive](component-cli_component-archive.md) - +* [component-cli component-archive signature add-digests](component-cli_component-archive_signature_add-digests.md) - fetch the component descriptor from an oci registry and add digests +* [component-cli component-archive signature check-digests](component-cli_component-archive_signature_check-digests.md) - fetch the component descriptor from an oci registry and check digests +* [component-cli component-archive signature sign](component-cli_component-archive_signature_sign.md) - command to sign component descriptors +* [component-cli component-archive signature verify](component-cli_component-archive_signature_verify.md) - command to verify the signature of a component descriptor + diff --git a/docs/reference/component-cli_component-archive_signature_add-digests.md b/docs/reference/component-cli_component-archive_signature_add-digests.md new file mode 100644 index 00000000..d8de3dd0 --- /dev/null +++ b/docs/reference/component-cli_component-archive_signature_add-digests.md @@ -0,0 +1,43 @@ +## component-cli component-archive signature add-digests + +fetch the component descriptor from an oci registry and add digests + +### Synopsis + + + fetch the component descriptor from an oci registry and add digests. Optionally resolve and digest the referenced component descriptors. + + +``` +component-cli component-archive signature add-digests BASE_URL COMPONENT_NAME VERSION [flags] +``` + +### Options + +``` + --allow-plain-http allows the fallback to http if the oci registry does not support https + --cc-config string path to the local concourse config file + --force force overwrite of already existing component descriptors + -h, --help help for add-digests + --insecure-skip-tls-verify If true, the server's certificate will not be checked for validity. This will make your HTTPS connections insecure + --recursive recursively upload all referenced component descriptors + --registry-config string path to the dockerconfig.json with the oci registry authentication information + --skip-access-types strings comma separated list of access types that will not be digested + --upload-base-url string target repository context to upload the signed cd +``` + +### Options inherited from parent commands + +``` + --cli logger runs as cli logger. enables cli logging + --dev enable development logging which result in console encoding, enabled stacktrace and enabled caller + --disable-caller disable the caller of logs (default true) + --disable-stacktrace disable the stacktrace of error logs (default true) + --disable-timestamp disable timestamp output (default true) + -v, --verbosity int number for the log level verbosity (default 1) +``` + +### SEE ALSO + +* [component-cli component-archive signature](component-cli_component-archive_signature.md) - [EXPERIMENTAL] command to work with signatures and digests in component descriptors + diff --git a/docs/reference/component-cli_component-archive_signature_check-digests.md b/docs/reference/component-cli_component-archive_signature_check-digests.md new file mode 100644 index 00000000..b3a46789 --- /dev/null +++ b/docs/reference/component-cli_component-archive_signature_check-digests.md @@ -0,0 +1,34 @@ +## component-cli component-archive signature check-digests + +fetch the component descriptor from an oci registry and check digests + +``` +component-cli component-archive signature check-digests BASE_URL COMPONENT_NAME VERSION [flags] +``` + +### Options + +``` + --allow-plain-http allows the fallback to http if the oci registry does not support https + --cc-config string path to the local concourse config file + -h, --help help for check-digests + --insecure-skip-tls-verify If true, the server's certificate will not be checked for validity. This will make your HTTPS connections insecure + --registry-config string path to the dockerconfig.json with the oci registry authentication information + --skip-access-types strings comma separated list of access types that will be ignored for digest verification +``` + +### Options inherited from parent commands + +``` + --cli logger runs as cli logger. enables cli logging + --dev enable development logging which result in console encoding, enabled stacktrace and enabled caller + --disable-caller disable the caller of logs (default true) + --disable-stacktrace disable the stacktrace of error logs (default true) + --disable-timestamp disable timestamp output (default true) + -v, --verbosity int number for the log level verbosity (default 1) +``` + +### SEE ALSO + +* [component-cli component-archive signature](component-cli_component-archive_signature.md) - [EXPERIMENTAL] command to work with signatures and digests in component descriptors + diff --git a/docs/reference/component-cli_component-archive_signature_sign.md b/docs/reference/component-cli_component-archive_signature_sign.md new file mode 100644 index 00000000..bbce591d --- /dev/null +++ b/docs/reference/component-cli_component-archive_signature_sign.md @@ -0,0 +1,26 @@ +## component-cli component-archive signature sign + +command to sign component descriptors + +### Options + +``` + -h, --help help for sign +``` + +### Options inherited from parent commands + +``` + --cli logger runs as cli logger. enables cli logging + --dev enable development logging which result in console encoding, enabled stacktrace and enabled caller + --disable-caller disable the caller of logs (default true) + --disable-stacktrace disable the stacktrace of error logs (default true) + --disable-timestamp disable timestamp output (default true) + -v, --verbosity int number for the log level verbosity (default 1) +``` + +### SEE ALSO + +* [component-cli component-archive signature](component-cli_component-archive_signature.md) - [EXPERIMENTAL] command to work with signatures and digests in component descriptors +* [component-cli component-archive signature sign rsa](component-cli_component-archive_signature_sign_rsa.md) - fetch the component descriptor from an oci registry and sign it using RSASSA-PKCS1-V1_5-SIGN + diff --git a/docs/reference/component-cli_component-archive_signature_sign_rsa.md b/docs/reference/component-cli_component-archive_signature_sign_rsa.md new file mode 100644 index 00000000..fa420381 --- /dev/null +++ b/docs/reference/component-cli_component-archive_signature_sign_rsa.md @@ -0,0 +1,39 @@ +## component-cli component-archive signature sign rsa + +fetch the component descriptor from an oci registry and sign it using RSASSA-PKCS1-V1_5-SIGN + +``` +component-cli component-archive signature sign rsa BASE_URL COMPONENT_NAME VERSION [flags] +``` + +### Options + +``` + --allow-plain-http allows the fallback to http if the oci registry does not support https + --cc-config string path to the local concourse config file + --force force overwrite of already existing component descriptors + -h, --help help for rsa + --insecure-skip-tls-verify If true, the server's certificate will not be checked for validity. This will make your HTTPS connections insecure + --private-key string path to private key file used for signing + --recursive recursively sign and upload all referenced component descriptors + --registry-config string path to the dockerconfig.json with the oci registry authentication information + --signature-name string name of the signature + --skip-access-types strings comma separated list of access types that will not be digested and signed + --upload-base-url string target repository context to upload the signed cd +``` + +### Options inherited from parent commands + +``` + --cli logger runs as cli logger. enables cli logging + --dev enable development logging which result in console encoding, enabled stacktrace and enabled caller + --disable-caller disable the caller of logs (default true) + --disable-stacktrace disable the stacktrace of error logs (default true) + --disable-timestamp disable timestamp output (default true) + -v, --verbosity int number for the log level verbosity (default 1) +``` + +### SEE ALSO + +* [component-cli component-archive signature sign](component-cli_component-archive_signature_sign.md) - command to sign component descriptors + diff --git a/docs/reference/component-cli_component-archive_signature_verify.md b/docs/reference/component-cli_component-archive_signature_verify.md new file mode 100644 index 00000000..684cce71 --- /dev/null +++ b/docs/reference/component-cli_component-archive_signature_verify.md @@ -0,0 +1,26 @@ +## component-cli component-archive signature verify + +command to verify the signature of a component descriptor + +### Options + +``` + -h, --help help for verify +``` + +### Options inherited from parent commands + +``` + --cli logger runs as cli logger. enables cli logging + --dev enable development logging which result in console encoding, enabled stacktrace and enabled caller + --disable-caller disable the caller of logs (default true) + --disable-stacktrace disable the stacktrace of error logs (default true) + --disable-timestamp disable timestamp output (default true) + -v, --verbosity int number for the log level verbosity (default 1) +``` + +### SEE ALSO + +* [component-cli component-archive signature](component-cli_component-archive_signature.md) - [EXPERIMENTAL] command to work with signatures and digests in component descriptors +* [component-cli component-archive signature verify rsa](component-cli_component-archive_signature_verify_rsa.md) - fetch the component descriptor from an oci registry and verify its integrity based on a RSASSA-PKCS1-V1_5-SIGN signature + diff --git a/docs/reference/component-cli_component-archive_signature_verify_rsa.md b/docs/reference/component-cli_component-archive_signature_verify_rsa.md new file mode 100644 index 00000000..1b8a4945 --- /dev/null +++ b/docs/reference/component-cli_component-archive_signature_verify_rsa.md @@ -0,0 +1,36 @@ +## component-cli component-archive signature verify rsa + +fetch the component descriptor from an oci registry and verify its integrity based on a RSASSA-PKCS1-V1_5-SIGN signature + +``` +component-cli component-archive signature verify rsa BASE_URL COMPONENT_NAME VERSION [flags] +``` + +### Options + +``` + --allow-plain-http allows the fallback to http if the oci registry does not support https + --cc-config string path to the local concourse config file + -h, --help help for rsa + --insecure-skip-tls-verify If true, the server's certificate will not be checked for validity. This will make your HTTPS connections insecure + --public-key string path to public key file + --registry-config string path to the dockerconfig.json with the oci registry authentication information + --signature-name string name of the signature to verify + --skip-access-types strings comma separated list of access types that will be ignored for verification +``` + +### Options inherited from parent commands + +``` + --cli logger runs as cli logger. enables cli logging + --dev enable development logging which result in console encoding, enabled stacktrace and enabled caller + --disable-caller disable the caller of logs (default true) + --disable-stacktrace disable the stacktrace of error logs (default true) + --disable-timestamp disable timestamp output (default true) + -v, --verbosity int number for the log level verbosity (default 1) +``` + +### SEE ALSO + +* [component-cli component-archive signature verify](component-cli_component-archive_signature_verify.md) - command to verify the signature of a component descriptor + diff --git a/go.mod b/go.mod index d59ffda1..e0bb0c77 100644 --- a/go.mod +++ b/go.mod @@ -7,7 +7,7 @@ require ( github.com/docker/cli v20.10.0-rc1+incompatible github.com/docker/docker v1.4.2-0.20200203170920-46ec8731fbce // indirect github.com/drone/envsubst v1.0.2 - github.com/gardener/component-spec/bindings-go v0.0.53 + github.com/gardener/component-spec/bindings-go v0.0.57 github.com/gardener/image-vector v0.10.0 github.com/ghodss/yaml v1.0.0 github.com/go-logr/logr v0.4.0 diff --git a/go.sum b/go.sum index baa0281b..fd66d923 100644 --- a/go.sum +++ b/go.sum @@ -291,8 +291,8 @@ github.com/fsnotify/fsnotify v1.4.9 h1:hsms1Qyu0jgnwNXIxa+/V/PDsU6CfLf6CNO8H7IWo github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ= github.com/fullsailor/pkcs7 v0.0.0-20190404230743-d7302db945fa/go.mod h1:KnogPXtdwXqoenmZCw6S+25EAm2MkxbG0deNDu4cbSA= github.com/gardener/component-spec/bindings-go v0.0.52/go.mod h1:kQFMTWowNAp9tOp6aImQa/NoLzfvX29jN5Qgud9rpQU= -github.com/gardener/component-spec/bindings-go v0.0.53 h1:9M6YOs8FYDSmGJqNqdCFolo3buuQalJSuUy7X8FIXm4= -github.com/gardener/component-spec/bindings-go v0.0.53/go.mod h1:kQFMTWowNAp9tOp6aImQa/NoLzfvX29jN5Qgud9rpQU= +github.com/gardener/component-spec/bindings-go v0.0.57 h1:3EYfNnIIlG/9mFI7gY+jKsGV7Gm44DdUXqRMwnUdP5M= +github.com/gardener/component-spec/bindings-go v0.0.57/go.mod h1:kQFMTWowNAp9tOp6aImQa/NoLzfvX29jN5Qgud9rpQU= github.com/gardener/image-vector v0.10.0 h1:Ysg3hxfiGUG/doajiZ0nQuUaJYwfO5BZCOcijL3tRuo= github.com/gardener/image-vector v0.10.0/go.mod h1:32SHGcbmmueeK9VkawsFcEbsoENXQPIuuYiFBUP+vMQ= github.com/garyburd/redigo v0.0.0-20150301180006-535138d7bcd7/go.mod h1:NR3MbYisc3/PwhQ00EMzDiPmrwpPxAn5GI05/YaO1SY= diff --git a/ociclient/mock/client_mock.go b/ociclient/mock/client_mock.go index 08616ed5..0e1757bf 100644 --- a/ociclient/mock/client_mock.go +++ b/ociclient/mock/client_mock.go @@ -83,6 +83,41 @@ func (mr *MockClientMockRecorder) GetOCIArtifact(arg0, arg1 interface{}) *gomock return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetOCIArtifact", reflect.TypeOf((*MockClient)(nil).GetOCIArtifact), arg0, arg1) } +// GetRawManifest mocks base method. +func (m *MockClient) GetRawManifest(arg0 context.Context, arg1 string) (v1.Descriptor, []byte, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetRawManifest", arg0, arg1) + ret0, _ := ret[0].(v1.Descriptor) + ret1, _ := ret[1].([]byte) + ret2, _ := ret[2].(error) + return ret0, ret1, ret2 +} + +// GetRawManifest indicates an expected call of GetRawManifest. +func (mr *MockClientMockRecorder) GetRawManifest(arg0, arg1 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetRawManifest", reflect.TypeOf((*MockClient)(nil).GetRawManifest), arg0, arg1) +} + +// PushBlob mocks base method. +func (m *MockClient) PushBlob(arg0 context.Context, arg1 string, arg2 v1.Descriptor, arg3 ...ociclient.PushOption) error { + m.ctrl.T.Helper() + varargs := []interface{}{arg0, arg1, arg2} + for _, a := range arg3 { + varargs = append(varargs, a) + } + ret := m.ctrl.Call(m, "PushBlob", varargs...) + ret0, _ := ret[0].(error) + return ret0 +} + +// PushBlob indicates an expected call of PushBlob. +func (mr *MockClientMockRecorder) PushBlob(arg0, arg1, arg2 interface{}, arg3 ...interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + varargs := append([]interface{}{arg0, arg1, arg2}, arg3...) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "PushBlob", reflect.TypeOf((*MockClient)(nil).PushBlob), varargs...) +} + // PushManifest mocks base method. func (m *MockClient) PushManifest(arg0 context.Context, arg1 string, arg2 *v1.Manifest, arg3 ...ociclient.PushOption) error { m.ctrl.T.Helper() @@ -121,6 +156,25 @@ func (mr *MockClientMockRecorder) PushOCIArtifact(arg0, arg1, arg2 interface{}, return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "PushOCIArtifact", reflect.TypeOf((*MockClient)(nil).PushOCIArtifact), varargs...) } +// PushRawManifest mocks base method. +func (m *MockClient) PushRawManifest(arg0 context.Context, arg1 string, arg2 v1.Descriptor, arg3 []byte, arg4 ...ociclient.PushOption) error { + m.ctrl.T.Helper() + varargs := []interface{}{arg0, arg1, arg2, arg3} + for _, a := range arg4 { + varargs = append(varargs, a) + } + ret := m.ctrl.Call(m, "PushRawManifest", varargs...) + ret0, _ := ret[0].(error) + return ret0 +} + +// PushRawManifest indicates an expected call of PushRawManifest. +func (mr *MockClientMockRecorder) PushRawManifest(arg0, arg1, arg2, arg3 interface{}, arg4 ...interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + varargs := append([]interface{}{arg0, arg1, arg2, arg3}, arg4...) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "PushRawManifest", reflect.TypeOf((*MockClient)(nil).PushRawManifest), varargs...) +} + // Resolve mocks base method. func (m *MockClient) Resolve(arg0 context.Context, arg1 string) (string, v1.Descriptor, error) { m.ctrl.T.Helper() diff --git a/pkg/commands/componentarchive/componentarchive.go b/pkg/commands/componentarchive/componentarchive.go index 3e18f7d7..8ca902c8 100644 --- a/pkg/commands/componentarchive/componentarchive.go +++ b/pkg/commands/componentarchive/componentarchive.go @@ -21,6 +21,7 @@ import ( "github.com/gardener/component-cli/pkg/commands/componentarchive/componentreferences" "github.com/gardener/component-cli/pkg/commands/componentarchive/remote" "github.com/gardener/component-cli/pkg/commands/componentarchive/resources" + "github.com/gardener/component-cli/pkg/commands/componentarchive/signature" "github.com/gardener/component-cli/pkg/commands/componentarchive/sources" ctfcmd "github.com/gardener/component-cli/pkg/commands/ctf" "github.com/gardener/component-cli/pkg/componentarchive" @@ -74,6 +75,7 @@ func NewComponentArchiveCommand(ctx context.Context) *cobra.Command { cmd.AddCommand(resources.NewResourcesCommand(ctx)) cmd.AddCommand(componentreferences.NewCompRefCommand(ctx)) cmd.AddCommand(sources.NewSourcesCommand(ctx)) + cmd.AddCommand(signature.NewSignatureCommand(ctx)) return cmd } diff --git a/pkg/commands/componentarchive/signature/add_digests.go b/pkg/commands/componentarchive/signature/add_digests.go new file mode 100644 index 00000000..5a8c5b0e --- /dev/null +++ b/pkg/commands/componentarchive/signature/add_digests.go @@ -0,0 +1,155 @@ +package signature + +import ( + "context" + "errors" + "fmt" + "os" + "path/filepath" + + cdv2 "github.com/gardener/component-spec/bindings-go/apis/v2" + "github.com/gardener/component-spec/bindings-go/ctf" + cdoci "github.com/gardener/component-spec/bindings-go/oci" + "github.com/go-logr/logr" + "github.com/mandelsoft/vfs/pkg/osfs" + "github.com/mandelsoft/vfs/pkg/vfs" + "github.com/spf13/cobra" + "github.com/spf13/pflag" + + ociopts "github.com/gardener/component-cli/ociclient/options" + "github.com/gardener/component-cli/pkg/commands/constants" + "github.com/gardener/component-cli/pkg/logger" + "github.com/gardener/component-cli/pkg/signatures" +) + +type AddDigestsOptions struct { + // BaseUrl is the oci registry where the component is stored. + BaseUrl string + // ComponentName is the unique name of the component in the registry. + ComponentName string + // Version is the component Version in the oci registry. + Version string + + // UploadBaseUrl is the base url where the digested component descriptor will be uploaded + UploadBaseUrl string + + // Force to overwrite component descriptors on upload + Force bool + + // Recursive to digest and upload all referenced component descriptors + Recursive bool + + // SkipAccessTypes defines the access types that will be ignored for adding digests + SkipAccessTypes []string + + // OciOptions contains all exposed options to configure the oci client. + OciOptions ociopts.Options +} + +func NewAddDigestsCommand(ctx context.Context) *cobra.Command { + opts := &AddDigestsOptions{} + cmd := &cobra.Command{ + Use: "add-digests BASE_URL COMPONENT_NAME VERSION", + Args: cobra.ExactArgs(3), + Short: "fetch the component descriptor from an oci registry and add digests", + Long: ` + fetch the component descriptor from an oci registry and add digests. Optionally resolve and digest the referenced component descriptors. +`, + Run: func(cmd *cobra.Command, args []string) { + if err := opts.Complete(args); err != nil { + fmt.Println(err.Error()) + os.Exit(1) + } + + if err := opts.Run(ctx, logger.Log, osfs.New()); err != nil { + fmt.Println(err.Error()) + os.Exit(1) + } + }, + } + + opts.AddFlags(cmd.Flags()) + + return cmd +} + +func (o *AddDigestsOptions) Run(ctx context.Context, log logr.Logger, fs vfs.FileSystem) error { + repoCtx := cdv2.NewOCIRegistryRepository(o.BaseUrl, "") + + ociClient, cache, err := o.OciOptions.Build(log, fs) + if err != nil { + return fmt.Errorf("unable to build oci client: %s", err.Error()) + } + + cdresolver := cdoci.NewResolver(ociClient) + rootCd, blobResolver, err := cdresolver.ResolveWithBlobResolver(ctx, repoCtx, o.ComponentName, o.Version) + if err != nil { + return fmt.Errorf("unable to to fetch component descriptor %s:%s: %w", o.ComponentName, o.Version, err) + } + + blobResolvers := map[string]ctf.BlobResolver{} + blobResolvers[fmt.Sprintf("%s:%s", rootCd.Name, rootCd.Version)] = blobResolver + + cds, err := signatures.RecursivelyAddDigestsToCd(rootCd, *repoCtx, ociClient, blobResolvers, context.TODO(), o.SkipAccessTypes) + if err != nil { + return fmt.Errorf("failed adding digests to cd: %w", err) + } + + targetRepoCtx := cdv2.NewOCIRegistryRepository(o.UploadBaseUrl, "") + + if o.Recursive { + for _, cd := range cds { + logger.Log.Info(fmt.Sprintf("Uploading to %s %s %s", o.UploadBaseUrl, cd.Name, cd.Version)) + + if err := signatures.UploadCDPreservingLocalOciBlobs(ctx, *cd, *targetRepoCtx, ociClient, cache, blobResolvers, o.Force, log); err != nil { + return fmt.Errorf("failed uploading cd: %w", err) + } + } + } else { + if err := signatures.UploadCDPreservingLocalOciBlobs(ctx, *rootCd, *targetRepoCtx, ociClient, cache, blobResolvers, o.Force, log); err != nil { + return fmt.Errorf("failed uploading cd: %w", err) + } + } + + return nil +} + +func (o *AddDigestsOptions) Complete(args []string) error { + o.BaseUrl = args[0] + o.ComponentName = args[1] + o.Version = args[2] + + cliHomeDir, err := constants.CliHomeDir() + if err != nil { + return err + } + + o.OciOptions.CacheDir = filepath.Join(cliHomeDir, "components") + if err := os.MkdirAll(o.OciOptions.CacheDir, os.ModePerm); err != nil { + return fmt.Errorf("unable to create cache directory %s: %w", o.OciOptions.CacheDir, err) + } + + if len(o.BaseUrl) == 0 { + return errors.New("the base url must be defined") + } + if len(o.ComponentName) == 0 { + return errors.New("a component name must be defined") + } + if len(o.Version) == 0 { + return errors.New("a component version must be defined") + } + + if o.UploadBaseUrl == "" { + return errors.New("upload-base-url must be defined") + } + + return nil +} + +func (o *AddDigestsOptions) AddFlags(fs *pflag.FlagSet) { + fs.StringVar(&o.UploadBaseUrl, "upload-base-url", "", "target repository context to upload the signed cd") + fs.StringSliceVar(&o.SkipAccessTypes, "skip-access-types", []string{}, "comma separated list of access types that will not be digested") + fs.BoolVar(&o.Force, "force", false, "force overwrite of already existing component descriptors") + fs.BoolVar(&o.Recursive, "recursive", false, "recursively upload all referenced component descriptors") + o.OciOptions.AddFlags(fs) +} diff --git a/pkg/commands/componentarchive/signature/check_digests.go b/pkg/commands/componentarchive/signature/check_digests.go new file mode 100644 index 00000000..e82591b4 --- /dev/null +++ b/pkg/commands/componentarchive/signature/check_digests.go @@ -0,0 +1,116 @@ +package signature + +import ( + "context" + "errors" + "fmt" + "os" + "path/filepath" + + cdv2 "github.com/gardener/component-spec/bindings-go/apis/v2" + cdoci "github.com/gardener/component-spec/bindings-go/oci" + "github.com/go-logr/logr" + "github.com/mandelsoft/vfs/pkg/osfs" + "github.com/mandelsoft/vfs/pkg/vfs" + "github.com/spf13/cobra" + "github.com/spf13/pflag" + + ociopts "github.com/gardener/component-cli/ociclient/options" + "github.com/gardener/component-cli/pkg/commands/componentarchive/signature/verify" + "github.com/gardener/component-cli/pkg/commands/constants" + "github.com/gardener/component-cli/pkg/logger" +) + +type CheckDigestsOptions struct { + // BaseUrl is the oci registry where the component is stored. + BaseUrl string + // ComponentName is the unique name of the component in the registry. + ComponentName string + // Version is the component Version in the oci registry. + Version string + + // SkipAccessTypes defines the access types that will be ignored for checking digests + SkipAccessTypes []string + + // OciOptions contains all exposed options to configure the oci client. + OciOptions ociopts.Options +} + +func NewCheckDigest(ctx context.Context) *cobra.Command { + opts := &CheckDigestsOptions{} + cmd := &cobra.Command{ + Use: "check-digests BASE_URL COMPONENT_NAME VERSION", + Args: cobra.ExactArgs(3), + Short: "fetch the component descriptor from an oci registry and check digests", + Run: func(cmd *cobra.Command, args []string) { + if err := opts.Complete(args); err != nil { + fmt.Println(err.Error()) + os.Exit(1) + } + + if err := opts.Run(ctx, logger.Log, osfs.New()); err != nil { + fmt.Println(err.Error()) + os.Exit(1) + } + }, + } + + opts.AddFlags(cmd.Flags()) + + return cmd +} + +func (o *CheckDigestsOptions) Run(ctx context.Context, log logr.Logger, fs vfs.FileSystem) error { + repoCtx := cdv2.NewOCIRegistryRepository(o.BaseUrl, "") + + ociClient, _, err := o.OciOptions.Build(log, fs) + if err != nil { + return fmt.Errorf("unable to build oci client: %s", err.Error()) + } + + cdresolver := cdoci.NewResolver(ociClient) + cd, err := cdresolver.Resolve(ctx, repoCtx, o.ComponentName, o.Version) + if err != nil { + return fmt.Errorf("unable to to fetch component descriptor %s:%s: %w", o.ComponentName, o.Version, err) + } + + // check componentReferences and resources + if err := verify.CheckCdDigests(cd, *repoCtx, ociClient, context.TODO(), o.SkipAccessTypes); err != nil { + return fmt.Errorf("failed checking cd: %w", err) + } + + return nil +} + +//Complete validates the arguments and flags from the command line +func (o *CheckDigestsOptions) Complete(args []string) error { + o.BaseUrl = args[0] + o.ComponentName = args[1] + o.Version = args[2] + + cliHomeDir, err := constants.CliHomeDir() + if err != nil { + return err + } + + o.OciOptions.CacheDir = filepath.Join(cliHomeDir, "components") + if err := os.MkdirAll(o.OciOptions.CacheDir, os.ModePerm); err != nil { + return fmt.Errorf("unable to create cache directory %s: %w", o.OciOptions.CacheDir, err) + } + + if len(o.BaseUrl) == 0 { + return errors.New("the base url must be defined") + } + if len(o.ComponentName) == 0 { + return errors.New("a component name must be defined") + } + if len(o.Version) == 0 { + return errors.New("a component's Version must be defined") + } + return nil +} + +func (o *CheckDigestsOptions) AddFlags(fs *pflag.FlagSet) { + fs.StringSliceVar(&o.SkipAccessTypes, "skip-access-types", []string{}, "comma separated list of access types that will be ignored for digest verification") + o.OciOptions.AddFlags(fs) +} diff --git a/pkg/commands/componentarchive/signature/sign/rsa.go b/pkg/commands/componentarchive/signature/sign/rsa.go new file mode 100644 index 00000000..21de90e3 --- /dev/null +++ b/pkg/commands/componentarchive/signature/sign/rsa.go @@ -0,0 +1,75 @@ +package sign + +import ( + "context" + "errors" + "fmt" + "os" + + cdv2Sign "github.com/gardener/component-spec/bindings-go/apis/v2/signatures" + + "github.com/go-logr/logr" + "github.com/mandelsoft/vfs/pkg/osfs" + "github.com/mandelsoft/vfs/pkg/vfs" + "github.com/spf13/cobra" + "github.com/spf13/pflag" + + "github.com/gardener/component-cli/pkg/logger" +) + +type RSASignOptions struct { + // PathToPrivateKey for RSA signing + PathToPrivateKey string + + GenericSignOptions +} + +// NewGetCommand shows definitions and their configuration. +func NewRSASignCommand(ctx context.Context) *cobra.Command { + opts := &RSASignOptions{} + cmd := &cobra.Command{ + Use: "rsa BASE_URL COMPONENT_NAME VERSION", + Args: cobra.ExactArgs(3), + Short: "fetch the component descriptor from an oci registry and sign it using RSASSA-PKCS1-V1_5-SIGN", + Run: func(cmd *cobra.Command, args []string) { + if err := opts.Complete(args); err != nil { + fmt.Println(err.Error()) + os.Exit(1) + } + + if err := opts.Run(ctx, logger.Log, osfs.New()); err != nil { + fmt.Println(err.Error()) + os.Exit(1) + } + }, + } + + opts.AddFlags(cmd.Flags()) + + return cmd +} + +func (o *RSASignOptions) Run(ctx context.Context, log logr.Logger, fs vfs.FileSystem) error { + signer, err := cdv2Sign.CreateRsaSignerFromKeyFile(o.PathToPrivateKey) + if err != nil { + return fmt.Errorf("failed creating rsa signer: %w", err) + } + return o.SignAndUploadWithSigner(ctx, log, fs, signer) +} + +func (o *RSASignOptions) Complete(args []string) error { + if err := o.GenericSignOptions.Complete(args); err != nil { + return err + } + + if o.PathToPrivateKey == "" { + return errors.New("a path to private key file must be given as flag") + } + + return nil +} + +func (o *RSASignOptions) AddFlags(fs *pflag.FlagSet) { + o.GenericSignOptions.AddFlags(fs) + fs.StringVar(&o.PathToPrivateKey, "private-key", "", "path to private key file used for signing") +} diff --git a/pkg/commands/componentarchive/signature/sign/sign.go b/pkg/commands/componentarchive/signature/sign/sign.go new file mode 100644 index 00000000..2bd0df28 --- /dev/null +++ b/pkg/commands/componentarchive/signature/sign/sign.go @@ -0,0 +1,166 @@ +package sign + +import ( + "context" + "errors" + "fmt" + "os" + "path/filepath" + + cdv2 "github.com/gardener/component-spec/bindings-go/apis/v2" + cdv2Sign "github.com/gardener/component-spec/bindings-go/apis/v2/signatures" + "github.com/gardener/component-spec/bindings-go/ctf" + cdoci "github.com/gardener/component-spec/bindings-go/oci" + "github.com/go-logr/logr" + "github.com/mandelsoft/vfs/pkg/vfs" + "github.com/spf13/cobra" + "github.com/spf13/pflag" + + ociopts "github.com/gardener/component-cli/ociclient/options" + "github.com/gardener/component-cli/pkg/commands/constants" + "github.com/gardener/component-cli/pkg/logger" + "github.com/gardener/component-cli/pkg/signatures" +) + +// NewSignCommand creates a new command to interact with signatures. +func NewSignCommand(ctx context.Context) *cobra.Command { + cmd := &cobra.Command{ + Use: "sign", + Short: "command to sign component descriptors", + } + + cmd.AddCommand(NewRSASignCommand(ctx)) + return cmd +} + +type GenericSignOptions struct { + // BaseUrl is the oci registry where the component is stored. + BaseUrl string + // ComponentName is the unique name of the component in the registry. + ComponentName string + // Version is the component Version in the oci registry. + Version string + + // SignatureName defines the name for the generated signature + SignatureName string + + // UploadBaseUrlForSigned is the base url where the signed component descriptor will be uploaded + UploadBaseUrlForSigned string + + // Force to overwrite component descriptors on upload + Force bool + + // RecursiveSigning to enable/disable signing and uploading of all referenced components + RecursiveSigning bool + + // SkipAccessTypes defines the access types that will be ignored for signing + SkipAccessTypes []string + + // OciOptions contains all exposed options to configure the oci client. + OciOptions ociopts.Options +} + +//Complete validates the arguments and flags from the command line +func (o *GenericSignOptions) Complete(args []string) error { + o.BaseUrl = args[0] + o.ComponentName = args[1] + o.Version = args[2] + + cliHomeDir, err := constants.CliHomeDir() + if err != nil { + return err + } + + o.OciOptions.CacheDir = filepath.Join(cliHomeDir, "components") + if err := os.MkdirAll(o.OciOptions.CacheDir, os.ModePerm); err != nil { + return fmt.Errorf("unable to create cache directory %s: %w", o.OciOptions.CacheDir, err) + } + + if len(o.BaseUrl) == 0 { + return errors.New("the base url must be defined") + } + if len(o.ComponentName) == 0 { + return errors.New("a component name must be defined") + } + if len(o.Version) == 0 { + return errors.New("a component's Version must be defined") + } + if o.UploadBaseUrlForSigned == "" { + return errors.New("upload-base-url must be defined") + } + if o.SignatureName == "" { + return errors.New("a signature name must be provided") + } + return nil +} + +func (o *GenericSignOptions) AddFlags(fs *pflag.FlagSet) { + fs.StringVar(&o.SignatureName, "signature-name", "", "name of the signature") + fs.StringVar(&o.UploadBaseUrlForSigned, "upload-base-url", "", "target repository context to upload the signed cd") + fs.StringSliceVar(&o.SkipAccessTypes, "skip-access-types", []string{}, "comma separated list of access types that will not be digested and signed") + fs.BoolVar(&o.Force, "force", false, "force overwrite of already existing component descriptors") + fs.BoolVar(&o.RecursiveSigning, "recursive", false, "recursively sign and upload all referenced component descriptors") + o.OciOptions.AddFlags(fs) +} + +func (o *GenericSignOptions) SignAndUploadWithSigner(ctx context.Context, log logr.Logger, fs vfs.FileSystem, signer cdv2Sign.Signer) error { + repoCtx := cdv2.NewOCIRegistryRepository(o.BaseUrl, "") + + ociClient, cache, err := o.OciOptions.Build(log, fs) + if err != nil { + return fmt.Errorf("unable to build oci client: %s", err.Error()) + } + + cdresolver := cdoci.NewResolver(ociClient) + cd, blobResolver, err := cdresolver.ResolveWithBlobResolver(ctx, repoCtx, o.ComponentName, o.Version) + if err != nil { + return fmt.Errorf("unable to to fetch component descriptor %s:%s: %w", o.ComponentName, o.Version, err) + } + + blobResolvers := map[string]ctf.BlobResolver{} + blobResolvers[fmt.Sprintf("%s:%s", cd.Name, cd.Version)] = blobResolver + + digestedCds, err := signatures.RecursivelyAddDigestsToCd(cd, *repoCtx, ociClient, blobResolvers, context.TODO(), o.SkipAccessTypes) + if err != nil { + return fmt.Errorf("failed adding digests to cd: %w", err) + } + + targetRepoCtx := cdv2.NewOCIRegistryRepository(o.UploadBaseUrlForSigned, "") + + if o.RecursiveSigning { + for _, digestedCd := range digestedCds { + hasher, err := cdv2Sign.HasherForName(cdv2Sign.SHA256) + if err != nil { + return fmt.Errorf("failed creating hasher: %w", err) + } + + if err := cdv2Sign.SignComponentDescriptor(digestedCd, signer, *hasher, o.SignatureName); err != nil { + return fmt.Errorf("failed signing component descriptor: %w", err) + } + logger.Log.Info(fmt.Sprintf("CD Signed %s %s", digestedCd.Name, digestedCd.Version)) + + logger.Log.Info(fmt.Sprintf("Uploading to %s %s %s", o.UploadBaseUrlForSigned, digestedCd.Name, digestedCd.Version)) + + if err := signatures.UploadCDPreservingLocalOciBlobs(ctx, *digestedCd, *targetRepoCtx, ociClient, cache, blobResolvers, o.Force, log); err != nil { + return fmt.Errorf("failed uploading cd: %w", err) + } + } + } else { + hasher, err := cdv2Sign.HasherForName(cdv2Sign.SHA256) + if err != nil { + return fmt.Errorf("failed creating hasher: %w", err) + } + + if err := cdv2Sign.SignComponentDescriptor(cd, signer, *hasher, o.SignatureName); err != nil { + return fmt.Errorf("failed signing component descriptor: %w", err) + } + logger.Log.Info(fmt.Sprintf("CD Signed %s %s", cd.Name, cd.Version)) + + logger.Log.Info(fmt.Sprintf("Uploading to %s %s %s", o.UploadBaseUrlForSigned, cd.Name, cd.Version)) + + if err := signatures.UploadCDPreservingLocalOciBlobs(ctx, *cd, *targetRepoCtx, ociClient, cache, blobResolvers, o.Force, log); err != nil { + return fmt.Errorf("failed uploading cd: %w", err) + } + } + return nil +} diff --git a/pkg/commands/componentarchive/signature/signature.go b/pkg/commands/componentarchive/signature/signature.go new file mode 100644 index 00000000..8873bc3c --- /dev/null +++ b/pkg/commands/componentarchive/signature/signature.go @@ -0,0 +1,25 @@ +package signature + +import ( + "context" + + "github.com/spf13/cobra" + + "github.com/gardener/component-cli/pkg/commands/componentarchive/signature/sign" + "github.com/gardener/component-cli/pkg/commands/componentarchive/signature/verify" +) + +// NewSignatureCommand creates a new command to interact with signatures. +func NewSignatureCommand(ctx context.Context) *cobra.Command { + cmd := &cobra.Command{ + Use: "signature", + Short: "[EXPERIMENTAL] command to work with signatures and digests in component descriptors", + } + + cmd.AddCommand(NewAddDigestsCommand(ctx)) + cmd.AddCommand(NewCheckDigest(ctx)) + cmd.AddCommand(sign.NewSignCommand(ctx)) + cmd.AddCommand(verify.NewVerifyCommand(ctx)) + + return cmd +} diff --git a/pkg/commands/componentarchive/signature/verify/rsa.go b/pkg/commands/componentarchive/signature/verify/rsa.go new file mode 100644 index 00000000..f45d2034 --- /dev/null +++ b/pkg/commands/componentarchive/signature/verify/rsa.go @@ -0,0 +1,77 @@ +package verify + +import ( + "context" + "errors" + "fmt" + "os" + + cdv2Sign "github.com/gardener/component-spec/bindings-go/apis/v2/signatures" + + "github.com/go-logr/logr" + "github.com/mandelsoft/vfs/pkg/osfs" + "github.com/mandelsoft/vfs/pkg/vfs" + "github.com/spf13/cobra" + "github.com/spf13/pflag" + + "github.com/gardener/component-cli/pkg/logger" +) + +type VerifyOptions struct { + // PathToPublicKey for RSA verification + PathToPublicKey string + + GenericVerifyOptions +} + +func NewRSAVerifyCommand(ctx context.Context) *cobra.Command { + opts := &VerifyOptions{} + cmd := &cobra.Command{ + Use: "rsa BASE_URL COMPONENT_NAME VERSION", + Args: cobra.ExactArgs(3), + Short: "fetch the component descriptor from an oci registry and verify its integrity based on a RSASSA-PKCS1-V1_5-SIGN signature", + Run: func(cmd *cobra.Command, args []string) { + if err := opts.Complete(args); err != nil { + fmt.Println(err.Error()) + os.Exit(1) + } + + if err := opts.Run(ctx, logger.Log, osfs.New()); err != nil { + fmt.Println(err.Error()) + os.Exit(1) + } + }, + } + + opts.AddFlags(cmd.Flags()) + + return cmd +} + +func (o *VerifyOptions) Run(ctx context.Context, log logr.Logger, fs vfs.FileSystem) error { + verifier, err := cdv2Sign.CreateRsaVerifierFromKeyFile(o.PathToPublicKey) + if err != nil { + return fmt.Errorf("failed creating rsa verifier: %w", err) + } + + if err := o.GenericVerifyOptions.VerifyWithVerifier(ctx, log, fs, verifier); err != nil { + return fmt.Errorf("failed verifying cd: %w", err) + } + return nil +} + +func (o *VerifyOptions) Complete(args []string) error { + if err := o.GenericVerifyOptions.Complete(args); err != nil { + return err + } + if o.PathToPublicKey == "" { + return errors.New("a path to public key file must be given as flag") + } + + return nil +} + +func (o *VerifyOptions) AddFlags(fs *pflag.FlagSet) { + o.GenericVerifyOptions.AddFlags(fs) + fs.StringVar(&o.PathToPublicKey, "public-key", "", "path to public key file") +} diff --git a/pkg/commands/componentarchive/signature/verify/verify.go b/pkg/commands/componentarchive/signature/verify/verify.go new file mode 100644 index 00000000..791e309f --- /dev/null +++ b/pkg/commands/componentarchive/signature/verify/verify.go @@ -0,0 +1,278 @@ +package verify + +import ( + "context" + "errors" + "fmt" + "os" + "path/filepath" + "reflect" + + cdv2 "github.com/gardener/component-spec/bindings-go/apis/v2" + cdv2Sign "github.com/gardener/component-spec/bindings-go/apis/v2/signatures" + cdoci "github.com/gardener/component-spec/bindings-go/oci" + "github.com/go-logr/logr" + "github.com/mandelsoft/vfs/pkg/vfs" + + "github.com/gardener/component-cli/ociclient" + ociopts "github.com/gardener/component-cli/ociclient/options" + "github.com/gardener/component-cli/pkg/commands/constants" + "github.com/gardener/component-cli/pkg/logger" + "github.com/gardener/component-cli/pkg/signatures" + + "github.com/spf13/cobra" + "github.com/spf13/pflag" +) + +// NewVerifyCommand creates a new command to verify signatures. +func NewVerifyCommand(ctx context.Context) *cobra.Command { + cmd := &cobra.Command{ + Use: "verify", + Short: "command to verify the signature of a component descriptor", + } + + cmd.AddCommand(NewRSAVerifyCommand(ctx)) + return cmd +} + +type GenericVerifyOptions struct { + // BaseUrl is the oci registry where the component is stored. + BaseUrl string + // ComponentName is the unique name of the component in the registry. + ComponentName string + // Version is the component version in the oci registry. + Version string + + // SignatureName selects the matching signature to verify + SignatureName string + + // SkipAccessTypes defines the access types that will be ignored for verification + SkipAccessTypes []string + + // OciOptions contains all exposed options to configure the oci client. + OciOptions ociopts.Options +} + +//Complete validates the arguments and flags from the command line +func (o *GenericVerifyOptions) Complete(args []string) error { + o.BaseUrl = args[0] + o.ComponentName = args[1] + o.Version = args[2] + + cliHomeDir, err := constants.CliHomeDir() + if err != nil { + return err + } + + o.OciOptions.CacheDir = filepath.Join(cliHomeDir, "components") + if err := os.MkdirAll(o.OciOptions.CacheDir, os.ModePerm); err != nil { + return fmt.Errorf("unable to create cache directory %s: %w", o.OciOptions.CacheDir, err) + } + + if len(o.BaseUrl) == 0 { + return errors.New("the base url must be defined") + } + if len(o.ComponentName) == 0 { + return errors.New("a component name must be defined") + } + if len(o.Version) == 0 { + return errors.New("a component's version must be defined") + } + if o.SignatureName == "" { + return errors.New("a signature name must be provided") + } + return nil +} + +func (o *GenericVerifyOptions) AddFlags(fs *pflag.FlagSet) { + fs.StringVar(&o.SignatureName, "signature-name", "", "name of the signature to verify") + fs.StringSliceVar(&o.SkipAccessTypes, "skip-access-types", []string{}, "comma separated list of access types that will be ignored for verification") + o.OciOptions.AddFlags(fs) +} + +func (o *GenericVerifyOptions) VerifyWithVerifier(ctx context.Context, log logr.Logger, fs vfs.FileSystem, verifier cdv2Sign.Verifier) error { + repoCtx := cdv2.NewOCIRegistryRepository(o.BaseUrl, "") + + ociClient, _, err := o.OciOptions.Build(log, fs) + if err != nil { + return fmt.Errorf("unable to build oci client: %s", err.Error()) + } + + cdresolver := cdoci.NewResolver(ociClient) + cd, err := cdresolver.Resolve(ctx, repoCtx, o.ComponentName, o.Version) + if err != nil { + return fmt.Errorf("unable to to fetch component descriptor %s:%s: %w", o.ComponentName, o.Version, err) + } + + // check componentReferences and resources + if err := CheckCdDigests(cd, *repoCtx, ociClient, context.TODO(), o.SkipAccessTypes); err != nil { + return fmt.Errorf("failed checking cd: %w", err) + } + + // check if digest is correctly signed + if err = cdv2Sign.VerifySignedComponentDescriptor(cd, verifier, o.SignatureName); err != nil { + return fmt.Errorf("signature invalid for digest: %w", err) + } + + // check if digest matches the normalised component descriptor + hasher, err := cdv2Sign.HasherForName(cdv2Sign.SHA256) + if err != nil { + return fmt.Errorf("failed creating hasher: %w", err) + } + hashCd, err := cdv2Sign.HashForComponentDescriptor(*cd, *hasher) + if err != nil { + return fmt.Errorf("failed hashing cd %s:%s: %w", cd.Name, cd.Version, err) + } + + matchingSignature, err := cdv2Sign.SelectSignatureByName(cd, o.SignatureName) + if err != nil { + return fmt.Errorf("failed selecting signature %s: %w", o.SignatureName, err) + } + + if hashCd.HashAlgorithm != matchingSignature.Digest.HashAlgorithm || hashCd.NormalisationAlgorithm != matchingSignature.Digest.NormalisationAlgorithm || hashCd.Value != matchingSignature.Digest.Value { + return fmt.Errorf("failed verifiying signature: signed normalised digest does not match calculated digest") + } + + log.Info(fmt.Sprintf("Signature %s is valid and digest of normalised cd matches calculated digest", o.SignatureName)) + return nil + +} + +func CheckCdDigests(cd *cdv2.ComponentDescriptor, repoContext cdv2.OCIRegistryRepository, ociClient ociclient.Client, ctx context.Context, skipAccessTypes []string) error { + skipAccessTypesMap := map[string]bool{} + for _, v := range skipAccessTypes { + skipAccessTypesMap[v] = true + } + for _, reference := range cd.ComponentReferences { + ociRef, err := cdoci.OCIRef(repoContext, reference.Name, reference.Version) + if err != nil { + return fmt.Errorf("invalid component reference: %w", err) + } + + cdresolver := cdoci.NewResolver(ociClient) + childCd, err := cdresolver.Resolve(ctx, &repoContext, reference.ComponentName, reference.Version) + if err != nil { + return fmt.Errorf("unable to to fetch component descriptor %s: %w", ociRef, err) + } + + if reference.Digest == nil || reference.Digest.HashAlgorithm == "" || reference.Digest.NormalisationAlgorithm == "" || reference.Digest.Value == "" { + return fmt.Errorf("component reference is missing digest %s:%s", reference.ComponentName, reference.Version) + } + + hasherForCdReference, err := cdv2Sign.HasherForName(reference.Digest.HashAlgorithm) + if err != nil { + return fmt.Errorf("failed creating hasher for algorithm %s for referenceCd %s %s: %w", reference.Digest.HashAlgorithm, reference.Name, reference.Version, err) + } + + digest, err := recursivelyCheckCdsDigests(childCd, repoContext, ociClient, ctx, hasherForCdReference, skipAccessTypes) + if err != nil { + return fmt.Errorf("checking of component reference %s:%s failed: %w", reference.ComponentName, reference.Version, err) + } + + if !reflect.DeepEqual(reference.Digest, digest) { + return fmt.Errorf("component reference digest for %s:%s is different to stored one", reference.ComponentName, reference.Version) + } + + } + for _, resource := range cd.Resources { + log := logger.Log.WithValues("componentDescriptor", cd, "resource.name", resource.Name, "resource.version", resource.Version, "resource.extraIdentity", resource.ExtraIdentity) + + //skip ignored access type + if _, ok := skipAccessTypesMap[resource.Access.Type]; ok { + log.Info("skipping resource as defined in --skip-access-types") + continue + } + if resource.Digest == nil || resource.Digest.HashAlgorithm == "" || resource.Digest.NormalisationAlgorithm == "" || resource.Digest.Value == "" { + return fmt.Errorf("resource is missing digest %s:%s", resource.Name, resource.Version) + } + + hasher, err := cdv2Sign.HasherForName(resource.Digest.HashAlgorithm) + if err != nil { + return fmt.Errorf("failed creating hasher for algorithm %s for resource %s %s: %w", resource.Digest.HashAlgorithm, resource.Name, resource.Version, err) + } + digester := signatures.NewDigester(ociClient, *hasher, skipAccessTypes) + + digest, err := digester.DigestForResource(ctx, *cd, resource) + if err != nil { + return fmt.Errorf("failed creating digest for resource %s: %w", resource.Name, err) + } + + if !reflect.DeepEqual(resource.Digest, digest) { + return fmt.Errorf("resource digest is different to stored one %s:%s", resource.Name, resource.Version) + } + + } + return nil +} + +func recursivelyCheckCdsDigests(cd *cdv2.ComponentDescriptor, repoContext cdv2.OCIRegistryRepository, ociClient ociclient.Client, ctx context.Context, hasherForCd *cdv2Sign.Hasher, skipAccessTypes []string) (*cdv2.DigestSpec, error) { + skipAccessTypesMap := map[string]bool{} + for _, v := range skipAccessTypes { + skipAccessTypesMap[v] = true + } + + for referenceIndex, reference := range cd.ComponentReferences { + reference := reference + + ociRef, err := cdoci.OCIRef(repoContext, reference.Name, reference.Version) + if err != nil { + return nil, fmt.Errorf("invalid component reference: %w", err) + } + + cdresolver := cdoci.NewResolver(ociClient) + childCd, err := cdresolver.Resolve(ctx, &repoContext, reference.ComponentName, reference.Version) + if err != nil { + return nil, fmt.Errorf("unable to to fetch component descriptor %s: %w", ociRef, err) + } + + hasher, err := cdv2Sign.HasherForName(cdv2Sign.SHA256) + if err != nil { + return nil, fmt.Errorf("failed creating hasher: %w", err) + } + + digest, err := recursivelyCheckCdsDigests(childCd, repoContext, ociClient, ctx, hasher, skipAccessTypes) + if err != nil { + return nil, fmt.Errorf("unable to resolve component reference to %s:%s: %w", reference.ComponentName, reference.Version, err) + } + reference.Digest = digest + cd.ComponentReferences[referenceIndex] = reference + } + for resourceIndex, resource := range cd.Resources { + resource := resource + log := logger.Log.WithValues("componentDescriptor", cd, "resource.name", resource.Name, "resource.version", resource.Version, "resource.extraIdentity", resource.ExtraIdentity) + + //skip ignored access type + if _, ok := skipAccessTypesMap[resource.Access.Type]; ok { + log.Info("skipping resource as defined in --skip-access-types") + continue + } + + hasher, err := cdv2Sign.HasherForName(cdv2Sign.SHA256) + if err != nil { + return nil, fmt.Errorf("failed creating hasher: %w", err) + } + + digester := signatures.NewDigester(ociClient, *hasher, skipAccessTypes) + + digest, err := digester.DigestForResource(ctx, *cd, resource) + if err != nil { + return nil, fmt.Errorf("failed creating digest for resource %s: %w", resource.Name, err) + } + + // For better user information, log resource with mismatching digest. + // Since we do not trust the digest data in this cd, it is only for information purpose. + // The mismatch will be noted in the propagated cd reference digest in the root cd. + if resource.Digest != nil && !reflect.DeepEqual(resource.Digest, digest) { + log.Info(fmt.Sprintf("digest in (untrusted) cd %+v mismatches with calculated digest %+v ", resource.Digest, digest)) + } + + resource.Digest = digest + cd.Resources[resourceIndex] = resource + } + + hashCd, err := cdv2Sign.HashForComponentDescriptor(*cd, *hasherForCd) + if err != nil { + return nil, fmt.Errorf("failed hashing cd %s:%s: %w", cd.Name, cd.Version, err) + } + return hashCd, nil +} diff --git a/pkg/signatures/digester.go b/pkg/signatures/digester.go new file mode 100644 index 00000000..c0cc732b --- /dev/null +++ b/pkg/signatures/digester.go @@ -0,0 +1,157 @@ +package signatures + +import ( + "context" + "encoding/hex" + "fmt" + "io" + "io/ioutil" + "net/http" + + "github.com/gardener/component-cli/ociclient" + "github.com/gardener/component-cli/pkg/logger" + + cdv2 "github.com/gardener/component-spec/bindings-go/apis/v2" + "github.com/gardener/component-spec/bindings-go/apis/v2/signatures" + cdoci "github.com/gardener/component-spec/bindings-go/oci" +) + +type Digester struct { + ociClient ociclient.Client + hasher signatures.Hasher + skipAccessTypes map[string]bool +} + +func NewDigester(ociClient ociclient.Client, hasher signatures.Hasher, skipAccessTypes []string) *Digester { + skipAccessTypesMap := map[string]bool{} + for _, v := range skipAccessTypes { + skipAccessTypesMap[v] = true + } + return &Digester{ + ociClient: ociClient, + hasher: hasher, + skipAccessTypes: skipAccessTypesMap, + } + +} + +func (d *Digester) DigestForResource(ctx context.Context, cd cdv2.ComponentDescriptor, res cdv2.Resource) (*cdv2.DigestSpec, error) { + //skip ignored access type + if _, ok := d.skipAccessTypes[res.Access.Type]; ok { + return nil, nil + } + + switch res.Access.Type { + case cdv2.OCIRegistryType: + return d.digestForOciArtifact(ctx, cd, res) + case cdv2.LocalOCIBlobType: + return d.digestForLocalOciBlob(ctx, cd, res) + case cdv2.S3AccessType: + return d.digestForS3Access(ctx, cd, res) + case "None": + logger.Log.V(5).Info(fmt.Sprintf("access type %s found in %s %s", res.Access.Type, cd.Name, cd.Version)) + return nil, nil + default: + return nil, fmt.Errorf("access type %s not supported", res.Access.Type) + } +} + +func (d *Digester) digestForLocalOciBlob(ctx context.Context, componentDescriptor cdv2.ComponentDescriptor, res cdv2.Resource) (*cdv2.DigestSpec, error) { + if res.Access.GetType() != cdv2.LocalOCIBlobType { + return nil, fmt.Errorf("unsupported access type: %s", res.Access.Type) + } + + repoctx := cdv2.OCIRegistryRepository{} + if err := componentDescriptor.GetEffectiveRepositoryContext().DecodeInto(&repoctx); err != nil { + return nil, fmt.Errorf("unable to decode repository context: %w", err) + } + + tmpfile, err := ioutil.TempFile("", "") + if err != nil { + return nil, fmt.Errorf("unable to create tempfile: %w", err) + } + defer tmpfile.Close() + + resolver := cdoci.NewResolver(d.ociClient) + _, blobResolver, err := resolver.ResolveWithBlobResolver(ctx, &repoctx, componentDescriptor.Name, componentDescriptor.Version) + if err != nil { + return nil, fmt.Errorf("unable to resolve component descriptor: %w", err) + } + if _, err := blobResolver.Resolve(ctx, res, tmpfile); err != nil { + return nil, fmt.Errorf("unable to resolve blob: %w", err) + } + + if _, err := tmpfile.Seek(0, io.SeekStart); err != nil { + return nil, fmt.Errorf("unable to seek to beginning of tempfile: %w", err) + } + d.hasher.HashFunction.Reset() + + if _, err := io.Copy(d.hasher.HashFunction, tmpfile); err != nil { + return nil, fmt.Errorf("unable to hash blob: %w", err) + } + return &cdv2.DigestSpec{ + HashAlgorithm: d.hasher.AlgorithmName, + NormalisationAlgorithm: string(cdv2.GenericBlobDigestV1), + Value: hex.EncodeToString((d.hasher.HashFunction.Sum(nil))), + }, nil +} + +func (d *Digester) digestForOciArtifact(ctx context.Context, componentDescriptor cdv2.ComponentDescriptor, res cdv2.Resource) (*cdv2.DigestSpec, error) { + if res.Access.GetType() != cdv2.OCIRegistryType { + return nil, fmt.Errorf("unsupported access type: %s", res.Access.Type) + } + + ociAccess := &cdv2.OCIRegistryAccess{} + if err := res.Access.DecodeInto(ociAccess); err != nil { + return nil, fmt.Errorf("unable to decode resource access: %w", err) + } + + _, bytes, err := d.ociClient.GetRawManifest(ctx, ociAccess.ImageReference) + if err != nil { + return nil, fmt.Errorf("failed getting oci raw manifest: %w", err) + } + + d.hasher.HashFunction.Reset() + if _, err = d.hasher.HashFunction.Write(bytes); err != nil { + return nil, fmt.Errorf("failed hashing oci raw manifest, %w", err) + } + + return &cdv2.DigestSpec{ + HashAlgorithm: d.hasher.AlgorithmName, + NormalisationAlgorithm: string(cdv2.ManifestDigestV1), + Value: hex.EncodeToString((d.hasher.HashFunction.Sum(nil))), + }, nil +} + +func (d *Digester) digestForS3Access(ctx context.Context, componentDescriptor cdv2.ComponentDescriptor, res cdv2.Resource) (*cdv2.DigestSpec, error) { + log := logger.Log.WithValues("componentDescriptor", componentDescriptor.ComponentSpec.ObjectMeta, "resource.name", res.Name, "resource.version", res.Version, "resource.extraIdentity", res.ExtraIdentity) + + if res.Access.GetType() != cdv2.S3AccessType { + return nil, fmt.Errorf("unsupported access type for s3 Access Digester: %s", res.Access.Type) + } + s3Access := &cdv2.S3Access{} + if err := res.Access.DecodeInto(s3Access); err != nil { + return nil, fmt.Errorf("unable to decode resource access: %w", err) + } + + url := fmt.Sprintf("https://%s.s3.amazonaws.com/%s", s3Access.BucketName, s3Access.ObjectKey) + resp, err := http.Get(url) + if err != nil { + return nil, fmt.Errorf("unable to access s3 access with url %s: %w", url, err) + } + defer resp.Body.Close() + if resp.StatusCode != http.StatusOK { + return nil, fmt.Errorf("unable to access s3 access with url %s, response code %d", url, resp.StatusCode) + } + log.V(5).Info(fmt.Sprintf("downloading and hashing %s bytes from s3 access", resp.Header.Get("Content-Length"))) + d.hasher.HashFunction.Reset() + if _, err := io.Copy(d.hasher.HashFunction, resp.Body); err != nil { + return nil, fmt.Errorf("unable to hash s3 access with url %s and hash function %s: %w", url, d.hasher.AlgorithmName, err) + } + return &cdv2.DigestSpec{ + HashAlgorithm: d.hasher.AlgorithmName, + NormalisationAlgorithm: string(cdv2.GenericBlobDigestV1), + Value: hex.EncodeToString((d.hasher.HashFunction.Sum(nil))), + }, nil + +} diff --git a/pkg/signatures/util.go b/pkg/signatures/util.go new file mode 100644 index 00000000..8dccfcbf --- /dev/null +++ b/pkg/signatures/util.go @@ -0,0 +1,160 @@ +package signatures + +import ( + "context" + "fmt" + "io" + + "github.com/gardener/component-spec/bindings-go/ctf" + "github.com/go-logr/logr" + "github.com/opencontainers/go-digest" + + "github.com/gardener/component-cli/ociclient" + "github.com/gardener/component-cli/pkg/components" + + cdv2 "github.com/gardener/component-spec/bindings-go/apis/v2" + v2 "github.com/gardener/component-spec/bindings-go/apis/v2" + cdv2Sign "github.com/gardener/component-spec/bindings-go/apis/v2/signatures" + cdoci "github.com/gardener/component-spec/bindings-go/oci" + ocispecv1 "github.com/opencontainers/image-spec/specs-go/v1" + + ociCache "github.com/gardener/component-cli/ociclient/cache" +) + +func RecursivelyAddDigestsToCd(cd *cdv2.ComponentDescriptor, repoContext cdv2.OCIRegistryRepository, ociClient ociclient.Client, blobResolvers map[string]ctf.BlobResolver, ctx context.Context, skipAccessTypes []string) ([]*cdv2.ComponentDescriptor, error) { + cdsWithHashes := []*cdv2.ComponentDescriptor{} + + cdResolver := func(c context.Context, cd cdv2.ComponentDescriptor, cr cdv2.ComponentReference) (*cdv2.DigestSpec, error) { + ociRef, err := cdoci.OCIRef(repoContext, cr.Name, cr.Version) + if err != nil { + return nil, fmt.Errorf("invalid component reference: %w", err) + } + + cdresolver := cdoci.NewResolver(ociClient) + childCd, blobResolver, err := cdresolver.ResolveWithBlobResolver(ctx, &repoContext, cr.ComponentName, cr.Version) + if err != nil { + return nil, fmt.Errorf("unable to to fetch component descriptor %s: %w", ociRef, err) + } + blobResolvers[fmt.Sprintf("%s:%s", childCd.Name, childCd.Version)] = blobResolver + + cds, err := RecursivelyAddDigestsToCd(childCd, repoContext, ociClient, blobResolvers, ctx, skipAccessTypes) + if err != nil { + return nil, fmt.Errorf("failed resolving referenced cd %s:%s: %w", cr.Name, cr.Version, err) + } + cdsWithHashes = append(cdsWithHashes, cds...) + + hasher, err := cdv2Sign.HasherForName(cdv2Sign.SHA256) + if err != nil { + return nil, fmt.Errorf("failed creating hasher: %w", err) + } + hashCd, err := cdv2Sign.HashForComponentDescriptor(*childCd, *hasher) + if err != nil { + return nil, fmt.Errorf("failed hashing referenced cd %s:%s: %w", cr.Name, cr.Version, err) + } + return hashCd, nil + } + + hasher, err := cdv2Sign.HasherForName(cdv2Sign.SHA256) + if err != nil { + return nil, fmt.Errorf("failed creating hasher: %w", err) + } + digester := NewDigester(ociClient, *hasher, skipAccessTypes) + if err := cdv2Sign.AddDigestsToComponentDescriptor(context.TODO(), cd, cdResolver, digester.DigestForResource); err != nil { + return nil, fmt.Errorf("failed adding digests to cd %s:%s: %w", cd.Name, cd.Version, err) + } + cdsWithHashes = append(cdsWithHashes, cd) + return cdsWithHashes, nil +} + +func UploadCDPreservingLocalOciBlobs(ctx context.Context, cd v2.ComponentDescriptor, targetRepository cdv2.OCIRegistryRepository, ociClient ociclient.ExtendedClient, cache ociCache.Cache, blobResolvers map[string]ctf.BlobResolver, force bool, log logr.Logger) error { + // check if the component descriptor already exists and skip if not forced to overwrite + if !force { + cdresolver := cdoci.NewResolver(ociClient) + if _, err := cdresolver.Resolve(ctx, &targetRepository, cd.Name, cd.Version); err == nil { + log.V(3).Info(fmt.Sprintf("Component Descriptor %s %s already exists in %s. Skip uploading cd", cd.Name, cd.Version, targetRepository.BaseURL)) + return nil + } + } + + if err := cdv2.InjectRepositoryContext(&cd, &targetRepository); err != nil { + return fmt.Errorf("unble to inject target repository: %w", err) + } + + // add all localOciBlobs to the layers + var layers []ocispecv1.Descriptor + blobToResource := map[string]*cdv2.Resource{} + + //get the blob resolver used for downloading + blobResolver, ok := blobResolvers[fmt.Sprintf("%s:%s", cd.Name, cd.Version)] + if !ok { + return fmt.Errorf("no blob resolver found for %s %s", cd.Name, cd.Version) + } + + for _, res := range cd.Resources { + if res.Access.Type == cdv2.LocalOCIBlobType { + localBlob := &cdv2.LocalOCIBlobAccess{} + if err := res.Access.DecodeInto(localBlob); err != nil { + return fmt.Errorf("unable to decode resource %s: %w", res.Name, err) + } + blobInfo, err := blobResolver.Info(ctx, res) + if err != nil { + return fmt.Errorf("unable to get blob info for resource %s: %w", res.Name, err) + } + d, err := digest.Parse(blobInfo.Digest) + if err != nil { + return fmt.Errorf("unable to parse digest for resource %s: %w", res.Name, err) + } + layers = append(layers, ocispecv1.Descriptor{ + MediaType: blobInfo.MediaType, + Digest: d, + Size: blobInfo.Size, + Annotations: map[string]string{ + "resource": res.Name, + }, + }) + blobToResource[blobInfo.Digest] = res.DeepCopy() + + } + } + manifest, err := cdoci.NewManifestBuilder(cache, ctf.NewComponentArchive(&cd, nil)).Build(ctx) + if err != nil { + return fmt.Errorf("unable to build oci artifact for component acrchive: %w", err) + } + manifest.Layers = append(manifest.Layers, layers...) + + ref, err := components.OCIRef(&targetRepository, cd.Name, cd.Version) + if err != nil { + return fmt.Errorf("invalid component reference: %w", err) + } + + store := ociclient.GenericStore(func(ctx context.Context, desc ocispecv1.Descriptor, writer io.Writer) error { + log := log.WithValues("digest", desc.Digest.String(), "mediaType", desc.MediaType) + res, ok := blobToResource[desc.Digest.String()] + if !ok { + // default to cache + log.V(5).Info("copying resource from cache") + rc, err := cache.Get(desc) + if err != nil { + return err + } + defer func() { + if err := rc.Close(); err != nil { + log.Error(err, "unable to close blob reader") + } + }() + if _, err := io.Copy(writer, rc); err != nil { + return err + } + return nil + } + log.V(5).Info("copying resource", "resource", res.Name) + _, err := blobResolver.Resolve(ctx, *res, writer) + return err + }) + log.V(3).Info("Upload component.", "ref", ref) + if err := ociClient.PushManifest(ctx, ref, manifest, ociclient.WithStore(store)); err != nil { + return fmt.Errorf("failed pushing manifest: %w", err) + } + return nil + +} diff --git a/vendor/github.com/gardener/component-spec/bindings-go/apis/v2/accesstypes.go b/vendor/github.com/gardener/component-spec/bindings-go/apis/v2/accesstypes.go index cd34d0be..fa27dbb5 100644 --- a/vendor/github.com/gardener/component-spec/bindings-go/apis/v2/accesstypes.go +++ b/vendor/github.com/gardener/component-spec/bindings-go/apis/v2/accesstypes.go @@ -219,3 +219,31 @@ func NewGitHubAccess(url, ref, commit string) *GitHubAccess { func (a GitHubAccess) GetType() string { return GitHubAccessType } + +// S3AccessType is the type of a s3 access. +const S3AccessType = "s3" + +// S3AccessType describes a s3 resource access. +type S3Access struct { + ObjectType `json:",inline"` + + // BucketName is the name of the s3 bucket. + BucketName string `json:"bucketName"` + // ObjectKey describes the referenced object. + ObjectKey string `json:"objectKey"` +} + +// NewS3Access creates a new s3 accessor +func NewS3Access(bucketName, objectKey string) *S3Access { + return &S3Access{ + ObjectType: ObjectType{ + Type: S3AccessType, + }, + BucketName: bucketName, + ObjectKey: objectKey, + } +} + +func (a S3Access) GetType() string { + return S3AccessType +} diff --git a/vendor/github.com/gardener/component-spec/bindings-go/apis/v2/cdutils/utils.go b/vendor/github.com/gardener/component-spec/bindings-go/apis/v2/cdutils/utils.go index fbc8a756..6696ede4 100644 --- a/vendor/github.com/gardener/component-spec/bindings-go/apis/v2/cdutils/utils.go +++ b/vendor/github.com/gardener/component-spec/bindings-go/apis/v2/cdutils/utils.go @@ -50,7 +50,7 @@ func MergeIdentityObjectMeta(a, b v2.IdentityObjectMeta) v2.IdentityObjectMeta { } for _, label := range b.Labels { - if idx := GetLabelIdx(b.Labels, label.Name); idx != -1 { + if idx := GetLabelIdx(a.Labels, label.Name); idx != -1 { a.Labels[idx] = label } else { a.Labels = append(a.Labels, label) diff --git a/vendor/github.com/gardener/component-spec/bindings-go/apis/v2/componentdescriptor.go b/vendor/github.com/gardener/component-spec/bindings-go/apis/v2/componentdescriptor.go index c388a7c2..42b40cac 100644 --- a/vendor/github.com/gardener/component-spec/bindings-go/apis/v2/componentdescriptor.go +++ b/vendor/github.com/gardener/component-spec/bindings-go/apis/v2/componentdescriptor.go @@ -71,6 +71,9 @@ type ComponentDescriptor struct { Metadata Metadata `json:"meta"` // Spec contains the specification of the component. ComponentSpec `json:"component"` + + // Signatures contains a list of signatures for the ComponentDescriptor + Signatures []Signature `json:"signatures,omitempty"` } // ComponentSpec defines a virtual component with @@ -351,6 +354,10 @@ type SourceRef struct { type Resource struct { IdentityObjectMeta `json:",inline"` + // Digest is the optional digest of the referenced resource. + // +optional + Digest *DigestSpec `json:"digest,omitempty"` + // Relation describes the relation of the resource to the component. // Can be a local or external resource Relation ResourceRelation `json:"relation,omitempty"` @@ -377,6 +384,9 @@ type ComponentReference struct { // ExtraIdentity is the identity of an object. // An additional label with key "name" ist not allowed ExtraIdentity Identity `json:"extraIdentity,omitempty"` + // Digest is the optional digest of the referenced component. + // +optional + Digest *DigestSpec `json:"digest,omitempty"` // Labels defines an optional set of additional labels // describing the object. // +optional @@ -427,3 +437,38 @@ func (o *ComponentReference) GetIdentity() Identity { func (o *ComponentReference) GetIdentityDigest() []byte { return o.GetIdentity().Digest() } + +// DigestSpec defines the digest and algorithm. +// +k8s:deepcopy-gen=true +// +k8s:openapi-gen=true +type DigestSpec struct { + HashAlgorithm string `json:"hashAlgorithm"` + NormalisationAlgorithm string `json:"normalisationAlgorithm"` + Value string `json:"value"` +} + +// SignatureSpec defines the signature and algorithm. +// +k8s:deepcopy-gen=true +// +k8s:openapi-gen=true +type SignatureSpec struct { + Algorithm string `json:"algorithm"` + Value string `json:"value"` +} + +// NormalisationAlgorithm types and versions the algorithm used for digest generation. +type NormalisationAlgorithm string + +const ( + JsonNormalisationV1 NormalisationAlgorithm = "jsonNormalisation/V1" + ManifestDigestV1 NormalisationAlgorithm = "manifestDigest/V1" + GenericBlobDigestV1 NormalisationAlgorithm = "genericBlobDigest/V1" +) + +// Signature defines a digest and corresponding signature, identifyable by name. +// +k8s:deepcopy-gen=true +// +k8s:openapi-gen=true +type Signature struct { + Name string `json:"name"` + Digest DigestSpec `json:"digest"` + Signature SignatureSpec `json:"signature"` +} diff --git a/vendor/github.com/gardener/component-spec/bindings-go/apis/v2/default.go b/vendor/github.com/gardener/component-spec/bindings-go/apis/v2/default.go index 407df9b9..32236395 100644 --- a/vendor/github.com/gardener/component-spec/bindings-go/apis/v2/default.go +++ b/vendor/github.com/gardener/component-spec/bindings-go/apis/v2/default.go @@ -56,6 +56,9 @@ func DefaultResources(component *ComponentDescriptor) { id := string(res.GetIdentityDigest()) if _, ok := resourceIDs[id]; ok { identity := res.ExtraIdentity + if identity == nil { + identity = make(Identity) + } identity[SystemIdentityVersion] = res.GetVersion() if id != string(identity.Digest()) { diff --git a/vendor/github.com/gardener/component-spec/bindings-go/apis/v2/signatures/normalize.go b/vendor/github.com/gardener/component-spec/bindings-go/apis/v2/signatures/normalize.go new file mode 100644 index 00000000..aa10eaae --- /dev/null +++ b/vendor/github.com/gardener/component-spec/bindings-go/apis/v2/signatures/normalize.go @@ -0,0 +1,233 @@ +package signatures + +import ( + "bytes" + "context" + "encoding/hex" + "encoding/json" + "fmt" + "sort" + + v2 "github.com/gardener/component-spec/bindings-go/apis/v2" +) + +// Entry is used for normalisation and has to contain one key +type Entry map[string]interface{} + +// AddDigestsToComponentDescriptor adds digest to componentReferences and resources as returned in the resolver functions +func AddDigestsToComponentDescriptor(ctx context.Context, cd *v2.ComponentDescriptor, + compRefResolver func(context.Context, v2.ComponentDescriptor, v2.ComponentReference) (*v2.DigestSpec, error), + resResolver func(context.Context, v2.ComponentDescriptor, v2.Resource) (*v2.DigestSpec, error)) error { + + for i, reference := range cd.ComponentReferences { + if reference.Digest == nil || reference.Digest.HashAlgorithm == "" || reference.Digest.NormalisationAlgorithm == "" || reference.Digest.Value == "" { + digest, err := compRefResolver(ctx, *cd, reference) + if err != nil { + return fmt.Errorf("failed resolving componentReference for %s:%s: %w", reference.Name, reference.Version, err) + } + cd.ComponentReferences[i].Digest = digest + } + } + + for i, res := range cd.Resources { + if res.Digest == nil || res.Digest.HashAlgorithm == "" || res.Digest.NormalisationAlgorithm == "" || res.Digest.Value == "" { + digest, err := resResolver(ctx, *cd, res) + if err != nil { + return fmt.Errorf("failed resolving resource for %s:%s: %w", res.Name, res.Version, err) + } + cd.Resources[i].Digest = digest + } + } + return nil +} + +// HashForComponentDescriptor return the hash for the component-descriptor, if it is normaliseable +// (= componentReferences and resources contain digest field) +func HashForComponentDescriptor(cd v2.ComponentDescriptor, hash Hasher) (*v2.DigestSpec, error) { + normalisedComponentDescriptor, err := normalizeComponentDescriptor(cd) + if err != nil { + return nil, fmt.Errorf("failed normalising component descriptor %w", err) + } + hash.HashFunction.Reset() + if _, err = hash.HashFunction.Write(normalisedComponentDescriptor); err != nil { + return nil, fmt.Errorf("failed hashing the normalisedComponentDescriptorJson: %w", err) + } + return &v2.DigestSpec{ + HashAlgorithm: hash.AlgorithmName, + NormalisationAlgorithm: string(v2.JsonNormalisationV1), + Value: hex.EncodeToString(hash.HashFunction.Sum(nil)), + }, nil +} + +func normalizeComponentDescriptor(cd v2.ComponentDescriptor) ([]byte, error) { + if err := isNormaliseableUnsafe(cd); err != nil { + return nil, fmt.Errorf("can not normalise component-descriptor %s:%s: %w", cd.Name, cd.Version, err) + } + + meta := []Entry{ + {"schemaVersion": cd.Metadata.Version}, + } + + componentReferences := []interface{}{} + for _, ref := range cd.ComponentSpec.ComponentReferences { + extraIdentity := buildExtraIdentity(ref.ExtraIdentity) + + digest := []Entry{ + {"hashAlgorithm": ref.Digest.HashAlgorithm}, + {"normalisationAlgorithm": ref.Digest.NormalisationAlgorithm}, + {"value": ref.Digest.Value}, + } + + componentReference := []Entry{ + {"name": ref.Name}, + {"version": ref.Version}, + {"extraIdentity": extraIdentity}, + {"digest": digest}, + } + componentReferences = append(componentReferences, componentReference) + } + + resources := []interface{}{} + for _, res := range cd.ComponentSpec.Resources { + extraIdentity := buildExtraIdentity(res.ExtraIdentity) + + //ignore access.type=None for normalisation and hash calculation + if res.Access == nil || res.Access.Type == "None" { + resource := []Entry{ + {"name": res.Name}, + {"version": res.Version}, + {"extraIdentity": extraIdentity}, + } + resources = append(resources, resource) + continue + } + + //ignore a resource without digests + if res.Digest == nil { + resource := []Entry{ + {"name": res.Name}, + {"version": res.Version}, + {"extraIdentity": extraIdentity}, + } + resources = append(resources, resource) + continue + } + + digest := []Entry{ + {"hashAlgorithm": res.Digest.HashAlgorithm}, + {"normalisationAlgorithm": res.Digest.NormalisationAlgorithm}, + {"value": res.Digest.Value}, + } + + resource := []Entry{ + {"name": res.Name}, + {"version": res.Version}, + {"extraIdentity": extraIdentity}, + {"digest": digest}, + } + resources = append(resources, resource) + } + + componentSpec := []Entry{ + {"name": cd.ComponentSpec.Name}, + {"version": cd.ComponentSpec.Version}, + {"componentReferences": componentReferences}, + {"resources": resources}, + } + + normalizedComponentDescriptor := []Entry{ + {"meta": meta}, + {"component": componentSpec}, + } + + if err := deepSort(normalizedComponentDescriptor); err != nil { + return nil, fmt.Errorf("failed sorting during normalisation: %w", err) + } + + byteBuffer := bytes.NewBuffer([]byte{}) + encoder := json.NewEncoder(byteBuffer) + encoder.SetEscapeHTML(false) + + if err := encoder.Encode(normalizedComponentDescriptor); err != nil { + return nil, err + } + + normalizedJson := byteBuffer.Bytes() + + // encoder.Encode appends a newline that we do not want + if normalizedJson[len(normalizedJson)-1] == 10 { + normalizedJson = normalizedJson[:len(normalizedJson)-1] + } + + return normalizedJson, nil +} + +func buildExtraIdentity(identity v2.Identity) []Entry { + var extraIdentities []Entry + for k, v := range identity { + extraIdentities = append(extraIdentities, Entry{k: v}) + } + return extraIdentities +} + +// deepSort sorts Entry, []Enry and [][]Entry interfaces recursively, lexicographicly by key(Entry). +func deepSort(in interface{}) error { + switch castIn := in.(type) { + case []Entry: + // sort the values recursively for every entry + for _, entry := range castIn { + val := getOnlyValueInEntry(entry) + if err := deepSort(val); err != nil { + return err + } + } + // sort the entries based on the key + sort.SliceStable(castIn, func(i, j int) bool { + return getOnlyKeyInEntry(castIn[i]) < getOnlyKeyInEntry(castIn[j]) + }) + case Entry: + val := getOnlyValueInEntry(castIn) + if err := deepSort(val); err != nil { + return err + } + case []interface{}: + for _, v := range castIn { + if err := deepSort(v); err != nil { + return err + } + } + case string: + break + default: + return fmt.Errorf("unknown type in sorting. This should not happen") + } + return nil +} + +func getOnlyKeyInEntry(entry Entry) string { + var key string + for k := range entry { + key = k + } + return key +} + +func getOnlyValueInEntry(entry Entry) interface{} { + var value interface{} + for _, v := range entry { + value = v + } + return value +} + +// isNormaliseableUnsafe checks if componentReferences contain digest. It does not check resources for containing digests. +// Does NOT verify if the digests are correct +func isNormaliseableUnsafe(cd v2.ComponentDescriptor) error { + // check for digests on component references + for _, reference := range cd.ComponentReferences { + if reference.Digest == nil || reference.Digest.HashAlgorithm == "" || reference.Digest.NormalisationAlgorithm == "" || reference.Digest.Value == "" { + return fmt.Errorf("missing digest in componentReference for %s:%s", reference.Name, reference.Version) + } + } + return nil +} diff --git a/vendor/github.com/gardener/component-spec/bindings-go/apis/v2/signatures/rsa.go b/vendor/github.com/gardener/component-spec/bindings-go/apis/v2/signatures/rsa.go new file mode 100644 index 00000000..4315ef94 --- /dev/null +++ b/vendor/github.com/gardener/component-spec/bindings-go/apis/v2/signatures/rsa.go @@ -0,0 +1,120 @@ +package signatures + +import ( + "crypto" + "crypto/rsa" + "crypto/x509" + "encoding/hex" + "encoding/pem" + "fmt" + "io/ioutil" + "strings" + + v2 "github.com/gardener/component-spec/bindings-go/apis/v2" +) + +// RsaSigner is a signatures.Signer compatible struct to sign with RSASSA-PKCS1-V1_5-SIGN. +type RsaSigner struct { + privateKey rsa.PrivateKey +} + +// CreateRsaSignerFromKeyFile creates an Instance of RsaSigner with the given private key. +// The private key has to be in the PKCS #1, ASN.1 DER form, see x509.ParsePKCS1PrivateKey. +func CreateRsaSignerFromKeyFile(pathToPrivateKey string) (*RsaSigner, error) { + privKeyFile, err := ioutil.ReadFile(pathToPrivateKey) + if err != nil { + return nil, fmt.Errorf("failed opening private key file %w", err) + } + + block, _ := pem.Decode([]byte(privKeyFile)) + if block == nil { + return nil, fmt.Errorf("failed decoding PEM formatted block in key %w", err) + } + key, err := x509.ParsePKCS1PrivateKey(block.Bytes) + if err != nil { + return nil, fmt.Errorf("failed parsing key %w", err) + } + return &RsaSigner{ + privateKey: *key, + }, nil +} + +// Sign returns the signature for the data for the component-descriptor. +func (s RsaSigner) Sign(componentDescriptor v2.ComponentDescriptor, digest v2.DigestSpec) (*v2.SignatureSpec, error) { + decodedHash, err := hex.DecodeString(digest.Value) + if err != nil { + return nil, fmt.Errorf("failed decoding hash to bytes") + } + hashType, err := hashAlgorithmLookup(digest.HashAlgorithm) + if err != nil { + return nil, fmt.Errorf("failed looking up hash algorithm") + } + signature, err := rsa.SignPKCS1v15(nil, &s.privateKey, hashType, decodedHash) + if err != nil { + return nil, fmt.Errorf("failed signing hash, %w", err) + } + return &v2.SignatureSpec{ + Algorithm: "RSASSA-PKCS1-V1_5-SIGN", + Value: hex.EncodeToString(signature), + }, nil +} + +// maps a hashing algorithm string to crypto.Hash +func hashAlgorithmLookup(algorithm string) (crypto.Hash, error) { + switch strings.ToLower(algorithm) { + case SHA256: + return crypto.SHA256, nil + } + return 0, fmt.Errorf("hash Algorithm %s not found", algorithm) +} + +// RsaVerifier is a signatures.Verifier compatible struct to verify RSASSA-PKCS1-V1_5-SIGN signatures. +type RsaVerifier struct { + publicKey rsa.PublicKey +} + +// CreateRsaVerifierFromKeyFile creates an Instance of RsaVerifier with the given rsa public key. +// The private key has to be in the PKIX, ASN.1 DER form, see x509.ParsePKIXPublicKey. +func CreateRsaVerifierFromKeyFile(pathToPublicKey string) (*RsaVerifier, error) { + publicKey, err := ioutil.ReadFile(pathToPublicKey) + if err != nil { + return nil, fmt.Errorf("failed opening public key file %w", err) + } + block, _ := pem.Decode([]byte(publicKey)) + if block == nil { + return nil, fmt.Errorf("failed decoding PEM formatted block in key %w", err) + } + untypedKey, err := x509.ParsePKIXPublicKey(block.Bytes) + if err != nil { + return nil, fmt.Errorf("failed parsing key %w", err) + } + switch key := untypedKey.(type) { + case *rsa.PublicKey: + return &RsaVerifier{ + publicKey: *key, + }, nil + default: + return nil, fmt.Errorf("public key format is not supported. Only rsa.PublicKey is supported") + } +} + +// Verify checks the signature, returns an error on verification failure +func (v RsaVerifier) Verify(componentDescriptor v2.ComponentDescriptor, signature v2.Signature) error { + decodedHash, err := hex.DecodeString(signature.Digest.Value) + if err != nil { + return fmt.Errorf("failed decoding hash %s: %w", signature.Digest.Value, err) + } + decodedSignature, err := hex.DecodeString(signature.Signature.Value) + if err != nil { + return fmt.Errorf("failed decoding hash %s: %w", signature.Digest.Value, err) + } + algorithm, err := hashAlgorithmLookup(signature.Digest.HashAlgorithm) + if err != nil { + return fmt.Errorf("failed looking up hash algorithm for %s: %w", signature.Digest.HashAlgorithm, err) + } + err = rsa.VerifyPKCS1v15(&v.publicKey, algorithm, decodedHash, decodedSignature) + if err != nil { + return fmt.Errorf("signature verification failed, %w", err) + } + return nil +} diff --git a/vendor/github.com/gardener/component-spec/bindings-go/apis/v2/signatures/sign.go b/vendor/github.com/gardener/component-spec/bindings-go/apis/v2/signatures/sign.go new file mode 100644 index 00000000..6e161f04 --- /dev/null +++ b/vendor/github.com/gardener/component-spec/bindings-go/apis/v2/signatures/sign.go @@ -0,0 +1,72 @@ +package signatures + +import ( + "fmt" + + v2 "github.com/gardener/component-spec/bindings-go/apis/v2" +) + +// SignComponentDescriptor signs the given component-descriptor with the signer. +// The component-descriptor has to contain digests for componentReferences and resources. +func SignComponentDescriptor(cd *v2.ComponentDescriptor, signer Signer, hasher Hasher, signatureName string) error { + hashedDigest, err := HashForComponentDescriptor(*cd, hasher) + if err != nil { + return fmt.Errorf("failed getting hash for cd: %w", err) + } + + signature, err := signer.Sign(*cd, *hashedDigest) + if err != nil { + return fmt.Errorf("failed signing hash of normalised component descriptor, %w", err) + } + cd.Signatures = append(cd.Signatures, v2.Signature{ + Name: signatureName, + Digest: *hashedDigest, + Signature: *signature, + }) + return nil +} + +// VerifySignedComponentDescriptor verifies the signature (selected by signatureName) and hash of the component-descriptor (as specified in the signature). +// Returns error if verification fails. +func VerifySignedComponentDescriptor(cd *v2.ComponentDescriptor, verifier Verifier, signatureName string) error { + //find matching signature + + matchingSignature, err := SelectSignatureByName(cd, signatureName) + if err != nil { + return fmt.Errorf("failed checking signature: %w", err) + } + + //Verify hash with signature + err = verifier.Verify(*cd, *matchingSignature) + if err != nil { + return fmt.Errorf("failed verifying: %w", err) + } + + //get hasher by algorithm name + hasher, err := HasherForName(matchingSignature.Digest.HashAlgorithm) + if err != nil { + return fmt.Errorf("failed creating hasher for %s: %w", matchingSignature.Digest.HashAlgorithm, err) + } + + //Verify normalised cd to given (and verified) hash + hashCd, err := HashForComponentDescriptor(*cd, *hasher) + if err != nil { + return fmt.Errorf("failed getting hash for cd: %w", err) + } + if hashCd.Value != matchingSignature.Digest.Value { + return fmt.Errorf("normalised component-descriptor does not match signed hash") + } + + return nil +} + +// SelectSignatureByName returns the Signature (Digest and SigantureSpec) matching the given name +func SelectSignatureByName(cd *v2.ComponentDescriptor, signatureName string) (*v2.Signature, error) { + for _, signature := range cd.Signatures { + if signature.Name == signatureName { + return &signature, nil + } + } + return nil, fmt.Errorf("signature with name %s not found in component-descriptor", signatureName) + +} diff --git a/vendor/github.com/gardener/component-spec/bindings-go/apis/v2/signatures/types.go b/vendor/github.com/gardener/component-spec/bindings-go/apis/v2/signatures/types.go new file mode 100644 index 00000000..e1ffdd3a --- /dev/null +++ b/vendor/github.com/gardener/component-spec/bindings-go/apis/v2/signatures/types.go @@ -0,0 +1,49 @@ +package signatures + +import ( + "context" + "crypto/sha256" + "fmt" + "hash" + "strings" + + v2 "github.com/gardener/component-spec/bindings-go/apis/v2" +) + +// Signer interface is used to implement different signing algorithms. +// Each Signer should have a matching Verifier. +type Signer interface { + // Sign returns the signature for the data for the component-descriptor + Sign(componentDescriptor v2.ComponentDescriptor, digest v2.DigestSpec) (*v2.SignatureSpec, error) +} + +// Verifier interface is used to implement different verification algorithms. +// Each Verifier should have a matching Signer. +type Verifier interface { + // Verify checks the signature, returns an error on verification failure + Verify(componentDescriptor v2.ComponentDescriptor, signature v2.Signature) error +} + +// Hasher encapsulates a hash.Hash interface with an algorithm name. +type Hasher struct { + HashFunction hash.Hash + AlgorithmName string +} + +const SHA256 = "sha256" + +// HasherForName creates a Hasher instance for the algorithmName. +func HasherForName(algorithmName string) (*Hasher, error) { + switch strings.ToLower(algorithmName) { + case SHA256: + return &Hasher{ + HashFunction: sha256.New(), + AlgorithmName: SHA256, + }, nil + } + return nil, fmt.Errorf("hash algorithm %s not found/implemented", algorithmName) +} + +type ResourceDigester interface { + DigestForResource(ctx context.Context, componentDescriptor v2.ComponentDescriptor, resource v2.Resource, hasher Hasher) (*v2.DigestSpec, error) +} diff --git a/vendor/github.com/gardener/component-spec/bindings-go/apis/v2/unstructured.go b/vendor/github.com/gardener/component-spec/bindings-go/apis/v2/unstructured.go index b1b078b5..771f3208 100644 --- a/vendor/github.com/gardener/component-spec/bindings-go/apis/v2/unstructured.go +++ b/vendor/github.com/gardener/component-spec/bindings-go/apis/v2/unstructured.go @@ -84,7 +84,7 @@ func NewUnstructuredType(ttype string, data map[string]interface{}) *Unstructure type UnstructuredTypedObject struct { ObjectType `json:",inline"` Raw []byte `json:"-"` - Object map[string]interface{} `json:"object"` + Object map[string]interface{} `json:"-"` } func (u *UnstructuredTypedObject) SetType(ttype string) { @@ -164,3 +164,6 @@ func (u *UnstructuredTypedObject) MarshalJSON() ([]byte, error) { } return data, nil } + +func (_ UnstructuredTypedObject) OpenAPISchemaType() []string { return []string{"object"} } +func (_ UnstructuredTypedObject) OpenAPISchemaFormat() string { return "" } diff --git a/vendor/modules.txt b/vendor/modules.txt index 9b6888d8..a361461a 100644 --- a/vendor/modules.txt +++ b/vendor/modules.txt @@ -45,12 +45,13 @@ github.com/drone/envsubst/parse github.com/drone/envsubst/path # github.com/fsnotify/fsnotify v1.4.9 github.com/fsnotify/fsnotify -# github.com/gardener/component-spec/bindings-go v0.0.53 +# github.com/gardener/component-spec/bindings-go v0.0.57 ## explicit github.com/gardener/component-spec/bindings-go/apis github.com/gardener/component-spec/bindings-go/apis/v2 github.com/gardener/component-spec/bindings-go/apis/v2/cdutils github.com/gardener/component-spec/bindings-go/apis/v2/jsonscheme +github.com/gardener/component-spec/bindings-go/apis/v2/signatures github.com/gardener/component-spec/bindings-go/apis/v2/validation github.com/gardener/component-spec/bindings-go/codec github.com/gardener/component-spec/bindings-go/ctf