From 4a76d0020bea3478711c8cda01ebe951c99034e9 Mon Sep 17 00:00:00 2001 From: Mauren Berti Date: Thu, 20 Jul 2023 15:00:36 -0400 Subject: [PATCH] feat: implement Builder/ClusterBuilder signing * Implement general logic for finding secrets and signing Builders and ClusterBuilders using cosign. * Create new Fetcher interface to aid testing. * Refactor the cosign ImageSigner to remove unused logger field. * Fix typo in constant name for image signer. * Add unit and e2e tests to verify changes. Signed-off-by: Mauren Berti --- cmd/completion/main.go | 4 +- cmd/controller/main.go | 13 +- pkg/apis/build/v1alpha2/builder_lifecycle.go | 6 +- pkg/apis/build/v1alpha2/builder_types.go | 7 + pkg/cnb/create_builder.go | 18 +- pkg/cnb/create_builder_test.go | 87 +- pkg/cosign/image_signer.go | 184 ++- pkg/cosign/image_signer_test.go | 320 +++-- pkg/cosign/testing/test_util.go | 60 + pkg/cosign/util/constants.go | 12 + pkg/reconciler/builder/builder.go | 18 +- pkg/reconciler/builder/builder_test.go | 59 +- .../clusterbuilder/clusterbuilder.go | 16 +- .../clusterbuilder/clusterbuilder_test.go | 40 +- .../testhelpers/fake_builder_creator.go | 4 +- pkg/secret/secretfakes/fake_fetcher.go | 27 + test/cosign_e2e_test.go | 1123 +++++++++++++++++ test/execute_build_test.go | 25 +- 18 files changed, 1875 insertions(+), 148 deletions(-) create mode 100644 pkg/cosign/testing/test_util.go create mode 100644 pkg/cosign/util/constants.go create mode 100644 pkg/secret/secretfakes/fake_fetcher.go create mode 100644 test/cosign_e2e_test.go diff --git a/cmd/completion/main.go b/cmd/completion/main.go index 2e5f32429..f0feec6d2 100644 --- a/cmd/completion/main.go +++ b/cmd/completion/main.go @@ -9,6 +9,8 @@ import ( "path/filepath" "strings" + "github.com/sigstore/cosign/v2/pkg/oci/remote" + "github.com/BurntSushi/toml" "github.com/buildpacks/lifecycle/platform/files" "github.com/google/go-containerregistry/pkg/authn" @@ -154,7 +156,7 @@ func main() { func signImage(report files.Report, keychain authn.Keychain) error { if hasCosign() { - cosignSigner := cosign.NewImageSigner(logger, sign.SignCmd) + cosignSigner := cosign.NewImageSigner(sign.SignCmd, remote.SignatureTag) annotations, err := mapKeyValueArgs(cosignAnnotations) if err != nil { diff --git a/cmd/controller/main.go b/cmd/controller/main.go index edc305ba6..70f258fd4 100644 --- a/cmd/controller/main.go +++ b/cmd/controller/main.go @@ -10,6 +10,12 @@ import ( "os" "time" + "github.com/pivotal/kpack/pkg/secret" + + "github.com/pivotal/kpack/pkg/cosign" + "github.com/sigstore/cosign/v2/cmd/cosign/cli/sign" + ociremote "github.com/sigstore/cosign/v2/pkg/oci/remote" + "github.com/Masterminds/semver/v3" "go.uber.org/zap" "golang.org/x/sync/errgroup" @@ -194,18 +200,21 @@ func main() { KpackVersion: cmd.Identifer, LifecycleProvider: lifecycleProvider, KeychainFactory: keychainFactory, + ImageSigner: cosign.NewImageSigner(sign.SignCmd, ociremote.SignatureTag), } podProgressLogger := &buildchange.ProgressLogger{ K8sClient: k8sClient, } + secretFetcher := &secret.Fetcher{Client: k8sClient} + buildController := build.NewController(ctx, options, k8sClient, buildInformer, podInformer, metadataRetriever, buildpodGenerator, podProgressLogger, keychainFactory, *injectedSidecarSupport) imageController := image.NewController(ctx, options, k8sClient, imageInformer, buildInformer, duckBuilderInformer, sourceResolverInformer, pvcInformer, *enablePriorityClasses) sourceResolverController := sourceresolver.NewController(ctx, options, sourceResolverInformer, gitResolver, blobResolver, registryResolver) - builderController, builderResync := builder.NewController(ctx, options, builderInformer, builderCreator, keychainFactory, clusterStoreInformer, buildpackInformer, clusterBuildpackInformer, clusterStackInformer) + builderController, builderResync := builder.NewController(ctx, options, builderInformer, builderCreator, keychainFactory, clusterStoreInformer, buildpackInformer, clusterBuildpackInformer, clusterStackInformer, secretFetcher) buildpackController := buildpack.NewController(ctx, options, keychainFactory, buildpackInformer, remoteStoreReader) - clusterBuilderController, clusterBuilderResync := clusterbuilder.NewController(ctx, options, clusterBuilderInformer, builderCreator, keychainFactory, clusterStoreInformer, clusterBuildpackInformer, clusterStackInformer) + clusterBuilderController, clusterBuilderResync := clusterbuilder.NewController(ctx, options, clusterBuilderInformer, builderCreator, keychainFactory, clusterStoreInformer, clusterBuildpackInformer, clusterStackInformer, secretFetcher) clusterBuildpackController := clusterbuildpack.NewController(ctx, options, keychainFactory, clusterBuildpackInformer, remoteStoreReader) clusterStoreController := clusterstore.NewController(ctx, options, keychainFactory, clusterStoreInformer, remoteStoreReader) clusterStackController := clusterstack.NewController(ctx, options, keychainFactory, clusterStackInformer, remoteStackReader) diff --git a/pkg/apis/build/v1alpha2/builder_lifecycle.go b/pkg/apis/build/v1alpha2/builder_lifecycle.go index df199de2e..5a642f3e5 100644 --- a/pkg/apis/build/v1alpha2/builder_lifecycle.go +++ b/pkg/apis/build/v1alpha2/builder_lifecycle.go @@ -16,6 +16,7 @@ type BuilderRecord struct { ObservedStoreGeneration int64 ObservedStackGeneration int64 OS string + SignaturePaths []CosignSignature } func (bs *BuilderStatus) BuilderRecord(record BuilderRecord) { @@ -33,10 +34,11 @@ func (bs *BuilderStatus) BuilderRecord(record BuilderRecord) { bs.ObservedStoreGeneration = record.ObservedStoreGeneration bs.ObservedStackGeneration = record.ObservedStackGeneration bs.OS = record.OS + bs.SignaturePaths = record.SignaturePaths } -func (cb *BuilderStatus) ErrorCreate(err error) { - cb.Status = corev1alpha1.Status{ +func (bs *BuilderStatus) ErrorCreate(err error) { + bs.Status = corev1alpha1.Status{ Conditions: corev1alpha1.Conditions{ { Type: corev1alpha1.ConditionReady, diff --git a/pkg/apis/build/v1alpha2/builder_types.go b/pkg/apis/build/v1alpha2/builder_types.go index b81ae4023..3281722cd 100644 --- a/pkg/apis/build/v1alpha2/builder_types.go +++ b/pkg/apis/build/v1alpha2/builder_types.go @@ -55,6 +55,12 @@ type NamespacedBuilderSpec struct { BackwardsCompatibleServiceAccount string `json:"serviceAccount,omitempty"` } +// +k8s:openapi-gen=true +type CosignSignature struct { + SigningSecret string `json:"signingSecret"` + TargetDigest string `json:"targetDigest"` +} + // +k8s:openapi-gen=true type BuilderStatus struct { corev1alpha1.Status `json:",inline"` @@ -65,6 +71,7 @@ type BuilderStatus struct { ObservedStackGeneration int64 `json:"observedStackGeneration,omitempty"` ObservedStoreGeneration int64 `json:"observedStoreGeneration,omitempty"` OS string `json:"os,omitempty"` + SignaturePaths []CosignSignature `json:"signaturePaths,omitempty"` } // +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object diff --git a/pkg/cnb/create_builder.go b/pkg/cnb/create_builder.go index 21c05c197..f225d2d2e 100644 --- a/pkg/cnb/create_builder.go +++ b/pkg/cnb/create_builder.go @@ -5,6 +5,8 @@ import ( "github.com/google/go-containerregistry/pkg/authn" ggcrv1 "github.com/google/go-containerregistry/pkg/v1" + "github.com/pivotal/kpack/pkg/cosign" + corev1 "k8s.io/api/core/v1" buildapi "github.com/pivotal/kpack/pkg/apis/build/v1alpha2" corev1alpha1 "github.com/pivotal/kpack/pkg/apis/core/v1alpha1" @@ -25,10 +27,12 @@ type RemoteBuilderCreator struct { LifecycleProvider LifecycleProvider KpackVersion string KeychainFactory registry.KeychainFactory + ImageSigner cosign.BuilderSigner } -func (r *RemoteBuilderCreator) CreateBuilder(ctx context.Context, builderKeychain authn.Keychain, stackKeychain authn.Keychain, fetcher RemoteBuildpackFetcher, clusterStack *buildapi.ClusterStack, spec buildapi.BuilderSpec) (buildapi.BuilderRecord, error) { +func (r *RemoteBuilderCreator) CreateBuilder(ctx context.Context, builderKeychain authn.Keychain, stackKeychain authn.Keychain, fetcher RemoteBuildpackFetcher, clusterStack *buildapi.ClusterStack, spec buildapi.BuilderSpec, serviceAccountSecrets []*corev1.Secret) (buildapi.BuilderRecord, error) { buildImage, _, err := r.RegistryClient.Fetch(stackKeychain, clusterStack.Status.BuildImage.LatestImage) + if err != nil { return buildapi.BuilderRecord{}, err } @@ -76,6 +80,17 @@ func (r *RemoteBuilderCreator) CreateBuilder(ctx context.Context, builderKeychai return buildapi.BuilderRecord{}, err } + var ( + signaturePaths = make([]buildapi.CosignSignature, 0) + ) + + if len(serviceAccountSecrets) > 0 { + signaturePaths, err = r.ImageSigner.SignBuilder(ctx, identifier, serviceAccountSecrets, builderKeychain) + if err != nil { + return buildapi.BuilderRecord{}, err + } + } + builder := buildapi.BuilderRecord{ Image: identifier, Stack: corev1alpha1.BuildStack{ @@ -87,6 +102,7 @@ func (r *RemoteBuilderCreator) CreateBuilder(ctx context.Context, builderKeychai ObservedStackGeneration: clusterStack.Status.ObservedGeneration, ObservedStoreGeneration: fetcher.ClusterStoreObservedGeneration(), OS: config.OS, + SignaturePaths: signaturePaths, } return builder, nil diff --git a/pkg/cnb/create_builder_test.go b/pkg/cnb/create_builder_test.go index 5fd32923d..a35d16e4d 100644 --- a/pkg/cnb/create_builder_test.go +++ b/pkg/cnb/create_builder_test.go @@ -177,6 +177,12 @@ func testCreateBuilderOs(os string, t *testing.T, when spec.G, it spec.S) { KpackVersion: "v1.2.3 (git sha: abcdefg123456)", KeychainFactory: keychainFactory, LifecycleProvider: lifecycleProvider, + ImageSigner: &fakeBuilderSigner{ + signBuilder: func(ctx context.Context, s string, secrets []*corev1.Secret, keychain authn.Keychain) ([]buildapi.CosignSignature, error) { + // no-op + return nil, nil + }, + }, } addBuildpack = func(t *testing.T, id, version, homepage, api string, stacks []corev1alpha1.BuildpackStack) { @@ -346,7 +352,7 @@ func testCreateBuilderOs(os string, t *testing.T, when spec.G, it spec.S) { }) it("creates a custom builder", func() { - builderRecord, err := subject.CreateBuilder(ctx, builderKeychain, stackKeychain, fetcher, stack, clusterBuilderSpec) + builderRecord, err := subject.CreateBuilder(ctx, builderKeychain, stackKeychain, fetcher, stack, clusterBuilderSpec, []*corev1.Secret{}) require.NoError(t, err) assert.Len(t, builderRecord.Buildpacks, 4) @@ -636,11 +642,12 @@ func testCreateBuilderOs(os string, t *testing.T, when spec.G, it spec.S) { }) it("creates images deterministically ", func() { - original, err := subject.CreateBuilder(ctx, builderKeychain, stackKeychain, fetcher, stack, clusterBuilderSpec) + original, err := subject.CreateBuilder(ctx, builderKeychain, stackKeychain, fetcher, stack, clusterBuilderSpec, []*corev1.Secret{}) require.NoError(t, err) for i := 1; i <= 50; i++ { - other, err := subject.CreateBuilder(ctx, builderKeychain, stackKeychain, fetcher, stack, clusterBuilderSpec) + other, err := subject.CreateBuilder(ctx, builderKeychain, stackKeychain, fetcher, stack, clusterBuilderSpec, []*corev1.Secret{}) + require.NoError(t, err) require.Equal(t, original.Image, other.Image) @@ -670,7 +677,7 @@ func testCreateBuilderOs(os string, t *testing.T, when spec.G, it spec.S) { }, } - _, err := subject.CreateBuilder(ctx, builderKeychain, stackKeychain, fetcher, stack, clusterBuilderSpec) + _, err := subject.CreateBuilder(ctx, builderKeychain, stackKeychain, fetcher, stack, clusterBuilderSpec, []*corev1.Secret{}) require.EqualError(t, err, "validating buildpack io.buildpack.unsupported.stack@v4: stack io.buildpacks.stacks.some-stack is not supported") }) @@ -694,7 +701,7 @@ func testCreateBuilderOs(os string, t *testing.T, when spec.G, it spec.S) { }}, }} - _, err := subject.CreateBuilder(ctx, builderKeychain, stackKeychain, fetcher, stack, clusterBuilderSpec) + _, err := subject.CreateBuilder(ctx, builderKeychain, stackKeychain, fetcher, stack, clusterBuilderSpec, []*corev1.Secret{}) require.EqualError(t, err, "validating buildpack io.buildpack.unsupported.mixin@v4: stack missing mixin(s): something-missing-mixin, something-missing-mixin2") }) @@ -739,7 +746,7 @@ func testCreateBuilderOs(os string, t *testing.T, when spec.G, it spec.S) { }}, }} - _, err := subject.CreateBuilder(ctx, builderKeychain, stackKeychain, fetcher, stack, clusterBuilderSpec) + _, err := subject.CreateBuilder(ctx, builderKeychain, stackKeychain, fetcher, stack, clusterBuilderSpec, []*corev1.Secret{}) require.Nil(t, err) }) @@ -764,7 +771,7 @@ func testCreateBuilderOs(os string, t *testing.T, when spec.G, it spec.S) { }}, }} - _, err := subject.CreateBuilder(ctx, builderKeychain, nil, fetcher, stack, clusterBuilderSpec) + _, err := subject.CreateBuilder(ctx, builderKeychain, nil, fetcher, stack, clusterBuilderSpec, []*corev1.Secret{}) require.Error(t, err, "validating buildpack io.buildpack.relaxed.old.mixin@v4: stack missing mixin(s): build:common-mixin, run:common-mixin, another-common-mixin") }) @@ -787,7 +794,7 @@ func testCreateBuilderOs(os string, t *testing.T, when spec.G, it spec.S) { }}, }} - _, err := subject.CreateBuilder(ctx, builderKeychain, stackKeychain, fetcher, stack, clusterBuilderSpec) + _, err := subject.CreateBuilder(ctx, builderKeychain, stackKeychain, fetcher, stack, clusterBuilderSpec, []*corev1.Secret{}) require.EqualError(t, err, "validating buildpack io.buildpack.unsupported.buildpack.api@v4: unsupported buildpack api: 0.1, expecting: 0.2, 0.3") }) @@ -830,7 +837,7 @@ func testCreateBuilderOs(os string, t *testing.T, when spec.G, it spec.S) { }}, }} - _, err := subject.CreateBuilder(ctx, builderKeychain, stackKeychain, fetcher, stack, clusterBuilderSpec) + _, err := subject.CreateBuilder(ctx, builderKeychain, stackKeychain, fetcher, stack, clusterBuilderSpec, []*corev1.Secret{}) require.NoError(t, err) }) }) @@ -857,13 +864,73 @@ func testCreateBuilderOs(os string, t *testing.T, when spec.G, it spec.S) { }, } - _, err := subject.CreateBuilder(ctx, builderKeychain, stackKeychain, fetcher, stack, clusterBuilderSpec) + _, err := subject.CreateBuilder(ctx, builderKeychain, stackKeychain, fetcher, stack, clusterBuilderSpec, []*corev1.Secret{}) require.EqualError(t, err, "unsupported platform apis in kpack lifecycle: 0.1, 0.2, 0.999, expecting one of: 0.3, 0.4, 0.5, 0.6, 0.7, 0.8") }) }) + + when("signing a builder image", func() { + it("does not populate the signature paths when no secrets were present", func() { + builderRecord, err := subject.CreateBuilder(ctx, builderKeychain, stackKeychain, fetcher, stack, clusterBuilderSpec, []*corev1.Secret{}) + require.NoError(t, err) + require.NotNil(t, builderRecord) + require.Empty(t, builderRecord.SignaturePaths) + }) + + it("returns an error if signing fails", func() { + subject.ImageSigner = &fakeBuilderSigner{ + signBuilder: func(ctx context.Context, s string, secrets []*corev1.Secret, keychain authn.Keychain) ([]buildapi.CosignSignature, error) { + return nil, fmt.Errorf("failed to sign builder") + }, + } + + fakeSecret := corev1.Secret{ + ObjectMeta: metav1.ObjectMeta{ + Name: "cosign-creds", + Namespace: "test-namespace", + }, + } + + _, err := subject.CreateBuilder(ctx, builderKeychain, stackKeychain, fetcher, stack, clusterBuilderSpec, []*corev1.Secret{&fakeSecret}) + require.Error(t, err) + }) + + it("populates the signature paths when signing succeeds", func() { + fakeSecret := corev1.Secret{ + ObjectMeta: metav1.ObjectMeta{ + Name: "cosign-creds", + Namespace: "test-namespace", + }, + } + + subject.ImageSigner = &fakeBuilderSigner{ + signBuilder: func(ctx context.Context, s string, secrets []*corev1.Secret, keychain authn.Keychain) ([]buildapi.CosignSignature, error) { + return []buildapi.CosignSignature{ + { + SigningSecret: fmt.Sprintf("k8s://%s/%s", fakeSecret.Namespace, fakeSecret.Name), + TargetDigest: "registry.local/test-image:signature-tag", + }, + }, nil + }, + } + + builderRecord, err := subject.CreateBuilder(ctx, builderKeychain, stackKeychain, fetcher, stack, clusterBuilderSpec, []*corev1.Secret{&fakeSecret}) + require.NoError(t, err) + require.NotNil(t, builderRecord) + require.NotEmpty(t, builderRecord.SignaturePaths) + }) + }) }) } +type fakeBuilderSigner struct { + signBuilder func(context.Context, string, []*corev1.Secret, authn.Keychain) ([]buildapi.CosignSignature, error) +} + +func (s *fakeBuilderSigner) SignBuilder(ctx context.Context, imageReference string, signingSecrets []*corev1.Secret, builderKeychain authn.Keychain) ([]buildapi.CosignSignature, error) { + return s.signBuilder(ctx, imageReference, signingSecrets, builderKeychain) +} + type fakeLifecycleProvider struct { metadata LifecycleMetadata layers map[string]v1.Layer diff --git a/pkg/cosign/image_signer.go b/pkg/cosign/image_signer.go index 53b214831..f873043c2 100644 --- a/pkg/cosign/image_signer.go +++ b/pkg/cosign/image_signer.go @@ -1,37 +1,46 @@ package cosign import ( + "context" "fmt" - "log" "os" "github.com/buildpacks/lifecycle/platform/files" + "io/ioutil" + + cosignutil "github.com/pivotal/kpack/pkg/cosign/util" + + "github.com/google/go-containerregistry/pkg/authn" + "github.com/google/go-containerregistry/pkg/name" + "github.com/google/go-containerregistry/pkg/v1/remote" + "github.com/pivotal/kpack/pkg/apis/build/v1alpha2" "github.com/pkg/errors" - "github.com/sigstore/cosign/v2/cmd/cosign/cli/options" + cosignoptions "github.com/sigstore/cosign/v2/cmd/cosign/cli/options" + cosignremote "github.com/sigstore/cosign/v2/pkg/oci/remote" + corev1 "k8s.io/api/core/v1" ) -type SignFunc func( - ro *options.RootOptions, ko options.KeyOpts, signOpts options.SignOptions, imgs []string, -) error +type SignFunc func(*cosignoptions.RootOptions, cosignoptions.KeyOpts, cosignoptions.SignOptions, []string) error -type ImageSigner struct { - Logger *log.Logger - signFunc SignFunc +type FetchSignatureFunc func(name.Reference, ...cosignremote.Option) (name.Tag, error) + +type BuilderSigner interface { + SignBuilder(context.Context, string, []*corev1.Secret, authn.Keychain) ([]v1alpha2.CosignSignature, error) } -const ( - cosignRepositoryEnv = "COSIGN_REPOSITORY" - cosignDockerMediaTypesEnv = "COSIGN_DOCKER_MEDIA_TYPES" -) +type ImageSigner struct { + signFunc SignFunc + fetchSignatureFunc FetchSignatureFunc +} -func NewImageSigner(logger *log.Logger, signFunc SignFunc) *ImageSigner { +func NewImageSigner(signFunc SignFunc, fetchSignatureFunc FetchSignatureFunc) *ImageSigner { return &ImageSigner{ - Logger: logger, - signFunc: signFunc, + signFunc: signFunc, + fetchSignatureFunc: fetchSignatureFunc, } } -func (s *ImageSigner) Sign(ro *options.RootOptions, report files.Report, secretLocation string, annotations, cosignRepositories, cosignDockerMediaTypes map[string]interface{}) error { +func (s *ImageSigner) Sign(ro *cosignoptions.RootOptions, report files.Report, secretLocation string, annotations, cosignRepositories, cosignDockerMediaTypes map[string]interface{}) error { cosignSecrets, err := findCosignSecrets(secretLocation) if err != nil { return errors.Errorf("no keys found for cosign signing: %v\n", err) @@ -60,12 +69,12 @@ func (s *ImageSigner) Sign(ro *options.RootOptions, report files.Report, secretL return nil } -func (s *ImageSigner) sign(ro *options.RootOptions, refImage, digest, secretLocation, cosignSecret string, annotations, cosignRepositories, cosignDockerMediaTypes map[string]interface{}) error { +func (s *ImageSigner) sign(ro *cosignoptions.RootOptions, refImage, digest, secretLocation, cosignSecret string, annotations, cosignRepositories, cosignDockerMediaTypes map[string]interface{}) error { cosignKeyFile := fmt.Sprintf("%s/%s/cosign.key", secretLocation, cosignSecret) cosignPasswordFile := fmt.Sprintf("%s/%s/cosign.password", secretLocation, cosignSecret) - ko := options.KeyOpts{KeyRef: cosignKeyFile, PassFunc: func(bool) ([]byte, error) { - content, err := os.ReadFile(cosignPasswordFile) + ko := cosignoptions.KeyOpts{KeyRef: cosignKeyFile, PassFunc: func(bool) ([]byte, error) { + content, err := ioutil.ReadFile(cosignPasswordFile) // When password file is not available, default empty password is used if err != nil { return []byte(""), nil @@ -75,17 +84,17 @@ func (s *ImageSigner) sign(ro *options.RootOptions, refImage, digest, secretLoca }} if cosignRepository, ok := cosignRepositories[cosignSecret]; ok { - if err := os.Setenv(cosignRepositoryEnv, fmt.Sprintf("%s", cosignRepository)); err != nil { - return errors.Errorf("failed setting %s env variable: %v", cosignRepositoryEnv, err) + if err := os.Setenv(cosignutil.CosignRepositoryEnv, fmt.Sprintf("%s", cosignRepository)); err != nil { + return errors.Errorf("failed setting %s env variable: %v", cosignutil.CosignRepositoryEnv, err) } - defer os.Unsetenv(cosignRepositoryEnv) + defer os.Unsetenv(cosignutil.CosignRepositoryEnv) } if cosignDockerMediaType, ok := cosignDockerMediaTypes[cosignSecret]; ok { - if err := os.Setenv(cosignDockerMediaTypesEnv, fmt.Sprintf("%s", cosignDockerMediaType)); err != nil { + if err := os.Setenv(cosignutil.CosignDockerMediaTypesEnv, fmt.Sprintf("%s", cosignDockerMediaType)); err != nil { return errors.Errorf("failed setting COSIGN_DOCKER_MEDIA_TYPES env variable: %v", err) } - defer os.Unsetenv(cosignDockerMediaTypesEnv) + defer os.Unsetenv(cosignutil.CosignDockerMediaTypesEnv) } var cosignAnnotations []string @@ -93,9 +102,9 @@ func (s *ImageSigner) sign(ro *options.RootOptions, refImage, digest, secretLoca cosignAnnotations = append(cosignAnnotations, fmt.Sprintf("%s=%s", key, value)) } - signOptions := options.SignOptions{ - Registry: options.RegistryOptions{KubernetesKeychain: true}, - AnnotationOptions: options.AnnotationOptions{ + signOptions := cosignoptions.SignOptions{ + Registry: cosignoptions.RegistryOptions{KubernetesKeychain: true}, + AnnotationOptions: cosignoptions.AnnotationOptions{ Annotations: cosignAnnotations, }, Upload: true, @@ -114,6 +123,127 @@ func (s *ImageSigner) sign(ro *options.RootOptions, refImage, digest, secretLoca return nil } +func (s *ImageSigner) SignBuilder( + ctx context.Context, + imageReference string, + serviceAccountSecrets []*corev1.Secret, + builderKeychain authn.Keychain, +) ([]v1alpha2.CosignSignature, error) { + signaturePaths := make([]v1alpha2.CosignSignature, 0) + cosignSecrets := filterCosignSecrets(serviceAccountSecrets) + + for _, cosignSecret := range cosignSecrets { + keyRef := fmt.Sprintf("k8s://%s/%s", cosignSecret.Namespace, cosignSecret.Name) + keyOpts := cosignoptions.KeyOpts{ + KeyRef: keyRef, + PassFunc: func(bool) ([]byte, error) { + if password, ok := cosignSecret.Data[cosignutil.SecretDataCosignPassword]; ok { + return password, nil + } + + return []byte(""), nil + }, + } + + if cosignRepository, ok := cosignSecret.Annotations[cosignutil.RepositoryAnnotationPrefix]; ok { + if err := os.Setenv(cosignutil.CosignRepositoryEnv, cosignRepository); err != nil { + return nil, fmt.Errorf("failed setting %s env variable: %w", cosignutil.CosignRepositoryEnv, err) + } + } + + if cosignDockerMediaType, ok := cosignSecret.Annotations[cosignutil.DockerMediaTypesAnnotationPrefix]; ok { + if err := os.Setenv(cosignutil.CosignDockerMediaTypesEnv, cosignDockerMediaType); err != nil { + return nil, fmt.Errorf("failed setting %s env variable: %w", cosignutil.CosignDockerMediaTypesEnv, err) + } + } + + registryOptions := cosignoptions.RegistryOptions{KubernetesKeychain: true, Keychain: builderKeychain} + + signOptions := cosignoptions.SignOptions{ + Registry: registryOptions, + AnnotationOptions: cosignoptions.AnnotationOptions{}, + Upload: true, + Recursive: false, + TlogUpload: false, + } + + rootOptions := cosignoptions.RootOptions{Timeout: cosignoptions.DefaultTimeout} + + if err := s.signFunc( + &rootOptions, + keyOpts, + signOptions, + []string{imageReference}); err != nil { + return nil, fmt.Errorf("unable to sign image with specified key from secret %s in namespace %s: %w", cosignSecret.Name, cosignSecret.Namespace, err) + } + + reference, err := name.ParseReference(imageReference) + if err != nil { + return nil, fmt.Errorf("failed to parse reference: %w", err) + } + + registryOpts, err := registryOptions.ClientOpts(ctx) + if err != nil { + return nil, err + } + + signatureTag, err := s.fetchSignatureFunc(reference, registryOpts...) + if err != nil { + return nil, err + } + + image, err := remote.Image(signatureTag, remote.WithAuthFromKeychain(builderKeychain)) + if err != nil { + return nil, err + } + + digest, err := image.Digest() + if err != nil { + return nil, err + } + + signaturePaths = append( + signaturePaths, + v1alpha2.CosignSignature{ + SigningSecret: keyRef, + TargetDigest: signatureTag.Digest(digest.String()).String(), + }, + ) + + if _, found := os.LookupEnv(cosignutil.CosignDockerMediaTypesEnv); found { + err = os.Unsetenv(cosignutil.CosignDockerMediaTypesEnv) + if err != nil { + return nil, fmt.Errorf("failed to cleanup environment variable %s: %w", cosignutil.CosignDockerMediaTypesEnv, err) + } + } + + if _, found := os.LookupEnv(cosignutil.CosignRepositoryEnv); found { + err = os.Unsetenv(cosignutil.CosignRepositoryEnv) + if err != nil { + return nil, fmt.Errorf("failed to cleanup environment variable %s: %w", cosignutil.CosignRepositoryEnv, err) + } + } + } + + return signaturePaths, nil +} + +func filterCosignSecrets(serviceAccountSecrets []*corev1.Secret) []*corev1.Secret { + cosignSecrets := make([]*corev1.Secret, 0) + + for _, cosignSecret := range serviceAccountSecrets { + _, passwordOk := cosignSecret.Data[cosignutil.SecretDataCosignPassword] + _, keyOk := cosignSecret.Data[cosignutil.SecretDataCosignKey] + + if passwordOk && keyOk { + cosignSecrets = append(cosignSecrets, cosignSecret) + } + } + + // successful + return cosignSecrets +} + func findCosignSecrets(secretLocation string) ([]string, error) { var result []string diff --git a/pkg/cosign/image_signer_test.go b/pkg/cosign/image_signer_test.go index 6a4f91a49..24a023601 100644 --- a/pkg/cosign/image_signer_test.go +++ b/pkg/cosign/image_signer_test.go @@ -3,8 +3,8 @@ package cosign import ( "bufio" "context" - "crypto" "fmt" + "io/ioutil" "log" "net/http/httptest" "net/url" @@ -14,25 +14,36 @@ import ( "strings" "testing" + cosigntesting "github.com/pivotal/kpack/pkg/cosign/testing" + cosignutil "github.com/pivotal/kpack/pkg/cosign/util" + "github.com/BurntSushi/toml" "github.com/buildpacks/lifecycle/platform/files" "github.com/google/go-containerregistry/pkg/authn" "github.com/google/go-containerregistry/pkg/name" "github.com/google/go-containerregistry/pkg/registry" + v1 "github.com/google/go-containerregistry/pkg/v1" "github.com/google/go-containerregistry/pkg/v1/random" "github.com/google/go-containerregistry/pkg/v1/remote" + registry2 "github.com/pivotal/kpack/pkg/registry" + "github.com/pivotal/kpack/pkg/registry/registryfakes" "github.com/sclevine/spec" "github.com/sigstore/cosign/v2/cmd/cosign/cli/download" "github.com/sigstore/cosign/v2/cmd/cosign/cli/options" "github.com/sigstore/cosign/v2/cmd/cosign/cli/sign" - verifypkg "github.com/sigstore/cosign/v2/cmd/cosign/cli/verify" sigstoreCosign "github.com/sigstore/cosign/v2/pkg/cosign" ociremote "github.com/sigstore/cosign/v2/pkg/oci/remote" - "github.com/sigstore/cosign/v2/pkg/signature" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" + corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" ) +var fetchSignatureFunc = func(_ name.Reference, options ...ociremote.Option) (name.Tag, error) { + tag, _ := name.NewTag("test", nil) + return tag, nil +} + func TestImageSigner(t *testing.T) { spec.Run(t, "Test Cosign Image Signer Main", testImageSigner) } @@ -43,20 +54,22 @@ func testImageSigner(t *testing.T, when spec.G, it spec.S) { report files.Report reader *os.File writer *os.File - expectedImageName string imageDigest string + hash v1.Hash stopRegistry func() imageCleanup func() repo string + expectedImageName string ) it.Before(func() { _, reader, writer = mockLogger(t) - repo, stopRegistry = reg(t) + repo, stopRegistry = fakeRegistry(t) expectedImageName = path.Join(repo, "test-cosign-image") - imageDigest, imageCleanup = pushRandomImage(t, expectedImageName) + hash, imageCleanup = pushRandomImage(t, expectedImageName) + imageDigest = hash.String() }) it.After(func() { @@ -80,16 +93,16 @@ func testImageSigner(t *testing.T, when spec.G, it spec.S) { // Override secretLocation for test secretLocation = createCosignKeyFiles(t) - secretKey1 = path.Join(secretLocation, "secret-name-1", "cosign.key") - publicKey1 = path.Join(secretLocation, "secret-name-1", "cosign.pub") - publicKey2 = path.Join(secretLocation, "secret-name-2", "cosign.pub") - passwordFile1 = path.Join(secretLocation, "secret-name-1", "cosign.password") - passwordFile2 = path.Join(secretLocation, "secret-name-2", "cosign.password") + secretKey1 = path.Join(secretLocation, "secret-name-1", cosignutil.SecretDataCosignKey) + publicKey1 = path.Join(secretLocation, "secret-name-1", cosignutil.SecretDataCosignPublicKey) + publicKey2 = path.Join(secretLocation, "secret-name-2", cosignutil.SecretDataCosignPublicKey) + passwordFile1 = path.Join(secretLocation, "secret-name-1", cosignutil.SecretDataCosignPassword) + passwordFile2 = path.Join(secretLocation, "secret-name-2", cosignutil.SecretDataCosignPassword) report = createReportToml(t, expectedImageName, imageDigest) - os.Unsetenv(cosignRepositoryEnv) - os.Unsetenv(cosignDockerMediaTypesEnv) + os.Unsetenv(cosignutil.CosignRepositoryEnv) + os.Unsetenv(cosignutil.CosignDockerMediaTypesEnv) }) it("signs images", func() { @@ -132,7 +145,7 @@ func testImageSigner(t *testing.T, when spec.G, it spec.S) { imgs) } - signer := NewImageSigner(log.New(writer, "", 0), cliSignCmd) + signer := NewImageSigner(cliSignCmd, fetchSignatureFunc) err := signer.Sign(ro, report, secretLocation, nil, nil, nil) assert.Nil(t, err) @@ -140,10 +153,10 @@ func testImageSigner(t *testing.T, when spec.G, it spec.S) { assert.Equal(t, 1, password1Count) assert.Equal(t, 1, password2Count) - err = verify(publicKey1, expectedImageName, nil) + err = cosigntesting.Verify(t, publicKey1, expectedImageName, nil) assert.Nil(t, err) - err = verify(publicKey2, expectedImageName, nil) + err = cosigntesting.Verify(t, publicKey2, expectedImageName, nil) assert.Nil(t, err) err = download.SignatureCmd(context.Background(), options.RegistryOptions{}, expectedImageName) @@ -178,28 +191,28 @@ func testImageSigner(t *testing.T, when spec.G, it spec.S) { ) } - signer := NewImageSigner(log.New(writer, "", 0), cliSignCmd) + signer := NewImageSigner(cliSignCmd, fetchSignatureFunc) err := signer.Sign(ro, report, secretLocation, expectedAnnotation, nil, nil) assert.Nil(t, err) assert.Equal(t, 2, cliSignCmdCallCount) // Should error when validating annotations that dont exist - err = verify(publicKey1, expectedImageName, unexpectedAnnotation) + err = cosigntesting.Verify(t, publicKey1, expectedImageName, unexpectedAnnotation) assert.Error(t, err) - err = verify(publicKey2, expectedImageName, unexpectedAnnotation) + err = cosigntesting.Verify(t, publicKey2, expectedImageName, unexpectedAnnotation) assert.Error(t, err) // Should not error when validating annotations that exist - err = verify(publicKey1, expectedImageName, expectedAnnotation) + err = cosigntesting.Verify(t, publicKey1, expectedImageName, expectedAnnotation) assert.Nil(t, err) - err = verify(publicKey2, expectedImageName, expectedAnnotation) + err = cosigntesting.Verify(t, publicKey2, expectedImageName, expectedAnnotation) assert.Nil(t, err) // Should not error when not validating annotations - err = verify(publicKey1, expectedImageName, nil) + err = cosigntesting.Verify(t, publicKey1, expectedImageName, nil) assert.Nil(t, err) - err = verify(publicKey2, expectedImageName, nil) + err = cosigntesting.Verify(t, publicKey2, expectedImageName, nil) assert.Nil(t, err) err = download.SignatureCmd(context.Background(), options.RegistryOptions{}, expectedImageName) @@ -224,7 +237,7 @@ func testImageSigner(t *testing.T, when spec.G, it spec.S) { os.Mkdir(filepath.Join(secretLocation, "secret-name-0"), 0700) expectedErrorMessage := fmt.Sprintf("unable to sign image with %s/cosign.key: getting signer: reading key: open %s/cosign.key: no such file or directory", emptyKey, emptyKey) - signer := NewImageSigner(log.New(writer, "", 0), cliSignCmd) + signer := NewImageSigner(cliSignCmd, fetchSignatureFunc) err := signer.Sign(ro, report, secretLocation, nil, nil, nil) assert.Error(t, err) assert.Equal(t, expectedErrorMessage, err.Error()) @@ -250,7 +263,7 @@ func testImageSigner(t *testing.T, when spec.G, it spec.S) { os.Mkdir(filepath.Join(secretLocation, "secret-name-3"), 0700) expectedErrorMessage := fmt.Sprintf("unable to sign image with %s/cosign.key: getting signer: reading key: open %s/cosign.key: no such file or directory", emptyKey, emptyKey) - signer := NewImageSigner(log.New(writer, "", 0), cliSignCmd) + signer := NewImageSigner(cliSignCmd, fetchSignatureFunc) err := signer.Sign(ro, report, secretLocation, nil, nil, nil) assert.Error(t, err) assert.Equal(t, expectedErrorMessage, err.Error()) @@ -258,21 +271,21 @@ func testImageSigner(t *testing.T, when spec.G, it spec.S) { }) it("sets COSIGN_REPOSITORY environment variable", func() { - altRepo, altStopRegistry := reg(t) + altRepo, altStopRegistry := fakeRegistry(t) defer altStopRegistry() altImageName := path.Join(altRepo, "test-cosign-image-alt") cliSignCmdCallCount := 0 - assert.Empty(t, len(os.Getenv(cosignRepositoryEnv))) + assert.Empty(t, len(os.Getenv(cosignutil.CosignRepositoryEnv))) cliSignCmd := func( ro *options.RootOptions, ko options.KeyOpts, signOpts options.SignOptions, imgs []string, ) error { t.Helper() if strings.Contains(ko.KeyRef, "secret-name-2") { - assert.Equal(t, altImageName, os.Getenv(cosignRepositoryEnv)) + assert.Equal(t, altImageName, os.Getenv(cosignutil.CosignRepositoryEnv)) } else { - assertUnset(t, cosignRepositoryEnv) + assertUnset(t, cosignutil.CosignRepositoryEnv) } cliSignCmdCallCount++ @@ -288,42 +301,42 @@ func testImageSigner(t *testing.T, when spec.G, it spec.S) { "secret-name-2": altImageName, } - signer := NewImageSigner(log.New(writer, "", 0), cliSignCmd) + signer := NewImageSigner(cliSignCmd, fetchSignatureFunc) err := signer.Sign(ro, report, secretLocation, nil, cosignRepositories, nil) assert.Nil(t, err) assert.Equal(t, 2, cliSignCmdCallCount) - assertUnset(t, cosignRepositoryEnv) + assertUnset(t, cosignutil.CosignRepositoryEnv) - err = verify(publicKey1, expectedImageName, nil) + err = cosigntesting.Verify(t, publicKey1, expectedImageName, nil) assert.Nil(t, err) - err = verify(publicKey2, expectedImageName, nil) + err = cosigntesting.Verify(t, publicKey2, expectedImageName, nil) assert.Error(t, err) err = download.SignatureCmd(context.Background(), options.RegistryOptions{}, expectedImageName) assert.Nil(t, err) // Required to set COSIGN_REPOSITORY env variable to validate signature // on a registry that does not contain the image - os.Setenv(cosignRepositoryEnv, altImageName) - defer os.Unsetenv(cosignRepositoryEnv) - err = verify(publicKey1, expectedImageName, nil) + os.Setenv(cosignutil.CosignRepositoryEnv, altImageName) + defer os.Unsetenv(cosignutil.CosignRepositoryEnv) + err = cosigntesting.Verify(t, publicKey1, expectedImageName, nil) assert.Error(t, err) - err = verify(publicKey2, expectedImageName, nil) + err = cosigntesting.Verify(t, publicKey2, expectedImageName, nil) assert.Nil(t, err) }) it("sets COSIGN_DOCKER_MEDIA_TYPES environment variable", func() { cliSignCmdCallCount := 0 - assertUnset(t, cosignDockerMediaTypesEnv) + assertUnset(t, cosignutil.CosignDockerMediaTypesEnv) cliSignCmd := func( ro *options.RootOptions, ko options.KeyOpts, signOpts options.SignOptions, imgs []string, ) error { t.Helper() if strings.Contains(ko.KeyRef, "secret-name-1") { - assert.Equal(t, "1", os.Getenv(cosignDockerMediaTypesEnv)) + assert.Equal(t, "1", os.Getenv(cosignutil.CosignDockerMediaTypesEnv)) } else { - assertUnset(t, cosignDockerMediaTypesEnv) + assertUnset(t, cosignutil.CosignDockerMediaTypesEnv) } cliSignCmdCallCount++ @@ -334,25 +347,25 @@ func testImageSigner(t *testing.T, when spec.G, it spec.S) { "secret-name-1": "1", } - signer := NewImageSigner(log.New(writer, "", 0), cliSignCmd) + signer := NewImageSigner(cliSignCmd, fetchSignatureFunc) err := signer.Sign(ro, report, secretLocation, nil, nil, cosignDockerMediaTypes) assert.Nil(t, err) assert.Equal(t, 2, cliSignCmdCallCount) - assertUnset(t, cosignDockerMediaTypesEnv) + assertUnset(t, cosignutil.CosignDockerMediaTypesEnv) }) it("sets both COSIGN_REPOSITORY and COSIGN_DOCKER_MEDIA_TYPES environment variable", func() { cliSignCmdCallCount := 0 - assertUnset(t, cosignDockerMediaTypesEnv) - assertUnset(t, cosignRepositoryEnv) + assertUnset(t, cosignutil.CosignDockerMediaTypesEnv) + assertUnset(t, cosignutil.CosignRepositoryEnv) cliSignCmd := func( ro *options.RootOptions, ko options.KeyOpts, signOpts options.SignOptions, imgs []string, ) error { t.Helper() - assert.Equal(t, "1", os.Getenv(cosignDockerMediaTypesEnv)) - assert.Equal(t, "registry.example.com/fakeproject", os.Getenv(cosignRepositoryEnv)) + assert.Equal(t, "1", os.Getenv(cosignutil.CosignDockerMediaTypesEnv)) + assert.Equal(t, "registry.example.com/fakeproject", os.Getenv(cosignutil.CosignRepositoryEnv)) cliSignCmdCallCount++ return nil } @@ -367,13 +380,13 @@ func testImageSigner(t *testing.T, when spec.G, it spec.S) { "secret-name-2": "1", } - signer := NewImageSigner(log.New(writer, "", 0), cliSignCmd) + signer := NewImageSigner(cliSignCmd, fetchSignatureFunc) err := signer.Sign(ro, report, secretLocation, nil, cosignRepositories, cosignDockerMediaTypes) assert.Nil(t, err) assert.Equal(t, 2, cliSignCmdCallCount) - assertUnset(t, cosignDockerMediaTypesEnv) - assertUnset(t, cosignRepositoryEnv) + assertUnset(t, cosignutil.CosignDockerMediaTypesEnv) + assertUnset(t, cosignutil.CosignRepositoryEnv) }) }) @@ -391,7 +404,7 @@ func testImageSigner(t *testing.T, when spec.G, it spec.S) { return nil } - signer := NewImageSigner(log.New(writer, "", 0), cliSignCmd) + signer := NewImageSigner(cliSignCmd, fetchSignatureFunc) err := signer.Sign(ro, report, secretLocation, nil, nil, nil) require.Error(t, err, "no keys found for cosign signing") assert.Equal(t, 0, cliSignCmdCallCount) @@ -409,7 +422,7 @@ func testImageSigner(t *testing.T, when spec.G, it spec.S) { return nil } - signer := NewImageSigner(log.New(writer, "", 0), cliSignCmd) + signer := NewImageSigner(cliSignCmd, fetchSignatureFunc) err := signer.Sign(ro, report, secretLocation, nil, nil, nil) require.Error(t, err, "no keys found for cosign signing: open /fake/location/that/doesnt/exist: no such file or directory") assert.Equal(t, 0, cliSignCmdCallCount) @@ -427,7 +440,7 @@ func testImageSigner(t *testing.T, when spec.G, it spec.S) { return nil } - signer := NewImageSigner(log.New(writer, "", 0), cliSignCmd) + signer := NewImageSigner(cliSignCmd, fetchSignatureFunc) err := signer.Sign(ro, report, secretLocation, nil, nil, nil) require.Error(t, err, "no image found in report to sign") assert.Equal(t, 0, cliSignCmdCallCount) @@ -439,7 +452,7 @@ func testImageSigner(t *testing.T, when spec.G, it spec.S) { it("signs an image", func() { secretLocation := t.TempDir() - repo, stop := reg(t) + repo, stop := fakeRegistry(t) defer stop() imgName := path.Join(repo, "cosign-e2e") @@ -449,12 +462,12 @@ func testImageSigner(t *testing.T, when spec.G, it spec.S) { password := "" keypair(t, secretLocation, "secret-name-1", password) - privKeyPath := path.Join(secretLocation, "secret-name-1", "cosign.key") - pubKeyPath := path.Join(secretLocation, "secret-name-1", "cosign.pub") + privKeyPath := path.Join(secretLocation, "secret-name-1", cosignutil.SecretDataCosignKey) + pubKeyPath := path.Join(secretLocation, "secret-name-1", cosignutil.SecretDataCosignPublicKey) ctx := context.Background() // Verify+download should fail at first - err := verify(pubKeyPath, imgName, nil) + err := cosigntesting.Verify(t, pubKeyPath, imgName, nil) assert.Error(t, err) err = download.SignatureCmd(ctx, options.RegistryOptions{}, imgName) assert.Error(t, err) @@ -479,12 +492,159 @@ func testImageSigner(t *testing.T, when spec.G, it spec.S) { assert.Nil(t, err) // Verify+download should pass - err = verify(pubKeyPath, imgName, nil) + err = cosigntesting.Verify(t, pubKeyPath, imgName, nil) assert.Nil(t, err) err = download.SignatureCmd(ctx, options.RegistryOptions{}, imgName) assert.Nil(t, err) }) }) + + when("#SignBuilder", func() { + const ( + cosignSecretName = "cosign-creds" + testNamespaceName = "test-namespace" + cosignServiceAccountName = "cosign-sa" + ) + + it("resolves the digest of a signature correctly", func() { + var ( + signCallCount = 0 + fetchSignatureCallCount = 0 + ) + + fakeImageSignatureTag := fmt.Sprintf("%s:%s", expectedImageName, "test.sig") + digest, cleanup := pushRandomImage(t, fakeImageSignatureTag) + defer cleanup() + + fakeImageSigner := &ImageSigner{ + signFunc: func(rootOptions *options.RootOptions, opts options.KeyOpts, signOptions options.SignOptions, i []string) error { + t.Helper() + + signCallCount++ + return nil + }, + fetchSignatureFunc: func(reference name.Reference, option ...ociremote.Option) (name.Tag, error) { + t.Helper() + + fetchSignatureCallCount++ + return name.NewTag(fakeImageSignatureTag) + }, + } + + fakeSecret := cosigntesting.GenerateFakeKeyPair(t, cosignSecretName, testNamespaceName, "", nil) + cosignCreds := []*corev1.Secret{&fakeSecret} + cosignSA := corev1.ServiceAccount{ + ObjectMeta: metav1.ObjectMeta{ + Name: cosignServiceAccountName, + Namespace: testNamespaceName, + }, + Secrets: []corev1.ObjectReference{ + { + Name: fakeSecret.Name, + }, + }, + } + + secretRef := registry2.SecretRef{ + ServiceAccount: cosignSA.Name, + Namespace: cosignSA.Namespace, + } + + keychainFactory := ®istryfakes.FakeKeychainFactory{} + fakeKeychain := ®istryfakes.FakeKeychain{} + keychainFactory.AddKeychainForSecretRef(t, secretRef, fakeKeychain) + + signaturePaths, err := fakeImageSigner.SignBuilder(context.Background(), expectedImageName, cosignCreds, fakeKeychain) + require.NoError(t, err) + require.NotEmpty(t, signaturePaths) + require.NotNil(t, signaturePaths[0]) + + assert.Contains(t, signaturePaths[0].TargetDigest, digest.String()) + assert.Contains(t, signaturePaths[0].SigningSecret, fakeSecret.Namespace) + assert.Contains(t, signaturePaths[0].SigningSecret, fakeSecret.Name) + + require.Equal(t, 1, signCallCount) + require.Equal(t, 1, fetchSignatureCallCount) + }) + + it("sets environment variables when needed", func() { + var ( + signCallCount = 0 + fetchSignatureCallCount = 0 + signaturesPath = path.Join(repo, "signatures") + dockerMediaTypesValue = "1" + ) + + fakeImageSignatureTag := fmt.Sprintf("%s:%s", signaturesPath, "test.sig") + digest, cleanup := pushRandomImage(t, fakeImageSignatureTag) + defer cleanup() + + fakeImageSigner := &ImageSigner{ + signFunc: func(rootOptions *options.RootOptions, opts options.KeyOpts, signOptions options.SignOptions, i []string) error { + t.Helper() + + value, found := os.LookupEnv(cosignutil.CosignRepositoryEnv) + require.True(t, found) + require.NotNil(t, value) + assert.Equal(t, signaturesPath, value) + + value, found = os.LookupEnv(cosignutil.CosignDockerMediaTypesEnv) + require.True(t, found) + require.NotNil(t, value) + assert.Equal(t, dockerMediaTypesValue, value) + + signCallCount++ + return nil + }, + fetchSignatureFunc: func(reference name.Reference, option ...ociremote.Option) (name.Tag, error) { + t.Helper() + + fetchSignatureCallCount++ + return name.NewTag(fakeImageSignatureTag) + }, + } + + annotations := map[string]string{ + "kpack.io/cosign.repository": signaturesPath, + "kpack.io/cosign.docker-media-types": dockerMediaTypesValue, + } + + fakeSecret := cosigntesting.GenerateFakeKeyPair(t, cosignSecretName, testNamespaceName, "", annotations) + cosignCreds := []*corev1.Secret{&fakeSecret} + cosignSA := corev1.ServiceAccount{ + ObjectMeta: metav1.ObjectMeta{ + Name: cosignServiceAccountName, + Namespace: testNamespaceName, + }, + Secrets: []corev1.ObjectReference{ + { + Name: fakeSecret.Name, + }, + }, + } + + secretRef := registry2.SecretRef{ + ServiceAccount: cosignSA.Name, + Namespace: cosignSA.Namespace, + } + + keychainFactory := ®istryfakes.FakeKeychainFactory{} + fakeKeychain := ®istryfakes.FakeKeychain{} + keychainFactory.AddKeychainForSecretRef(t, secretRef, fakeKeychain) + + signaturePaths, err := fakeImageSigner.SignBuilder(context.Background(), expectedImageName, cosignCreds, fakeKeychain) + require.NoError(t, err) + require.NotEmpty(t, signaturePaths) + require.NotNil(t, signaturePaths[0]) + + assert.Contains(t, signaturePaths[0].TargetDigest, digest.String()) + assert.Contains(t, signaturePaths[0].SigningSecret, fakeSecret.Namespace) + assert.Contains(t, signaturePaths[0].SigningSecret, fakeSecret.Name) + + require.Equal(t, 1, signCallCount) + require.Equal(t, 1, fetchSignatureCallCount) + }) + }) } func mockLogger(t *testing.T) (*bufio.Scanner, *os.File, *os.File) { @@ -540,7 +700,7 @@ func assertUnset(t *testing.T, envName string, msg ...string) { assert.Equal(t, "", value) } -func reg(t *testing.T) (string, func()) { +func fakeRegistry(t *testing.T) (string, func()) { r := httptest.NewServer(registry.New()) u, err := url.Parse(r.URL) assert.Nil(t, err) @@ -548,24 +708,20 @@ func reg(t *testing.T) (string, func()) { return u.Host, r.Close } -func pushRandomImage(t *testing.T, imageRef string) (string, func()) { +func pushRandomImage(t *testing.T, imageRef string) (v1.Hash, func()) { ref, err := name.ParseReference(imageRef, name.WeakValidation) - assert.Nil(t, err) + require.NoError(t, err) img, err := random.Image(512, 5) - assert.Nil(t, err) + require.NoError(t, err) regClientOpts := registryClientOpts(context.Background()) err = remote.Write(ref, img, regClientOpts...) - assert.Nil(t, err) - - _, err = remote.Get(ref, regClientOpts...) - assert.Nil(t, err) + require.NoError(t, err) - imgHash, err := img.Digest() - assert.Nil(t, err) - imgDigest := imgHash.String() + resp, err := remote.Get(ref, regClientOpts...) + require.NoError(t, err) cleanup := func() { _ = remote.Delete(ref, regClientOpts...) @@ -573,7 +729,7 @@ func pushRandomImage(t *testing.T, imageRef string) (string, func()) { _ = remote.Delete(ref, regClientOpts...) } - return imgDigest, cleanup + return resp.Digest, cleanup } func registryClientOpts(ctx context.Context) []remote.Option { @@ -584,6 +740,8 @@ func registryClientOpts(ctx context.Context) []remote.Option { } func keypair(t *testing.T, dirPath, secretName, password string) { + t.Helper() + passFunc := func(_ bool) ([]byte, error) { return []byte(password), nil } @@ -594,30 +752,16 @@ func keypair(t *testing.T, dirPath, secretName, password string) { err = os.Mkdir(filepath.Join(dirPath, secretName), 0700) assert.Nil(t, err) - privKeyPath := filepath.Join(dirPath, secretName, "cosign.key") - err = os.WriteFile(privKeyPath, keys.PrivateBytes, 0600) + privKeyPath := filepath.Join(dirPath, secretName, cosignutil.SecretDataCosignKey) + err = ioutil.WriteFile(privKeyPath, keys.PrivateBytes, 0600) assert.Nil(t, err) - pubKeyPath := filepath.Join(dirPath, secretName, "cosign.pub") - err = os.WriteFile(pubKeyPath, keys.PublicBytes, 0600) + pubKeyPath := filepath.Join(dirPath, secretName, cosignutil.SecretDataCosignPublicKey) + err = ioutil.WriteFile(pubKeyPath, keys.PublicBytes, 0600) assert.Nil(t, err) - passwordPath := filepath.Join(dirPath, secretName, "cosign.password") + passwordPath := filepath.Join(dirPath, secretName, cosignutil.SecretDataCosignPassword) passwordBytes, _ := passFunc(true) err = os.WriteFile(passwordPath, passwordBytes, 0600) assert.Nil(t, err) } - -func verify(keyRef, imageRef string, annotations map[string]interface{}) error { - cmd := verifypkg.VerifyCommand{ - KeyRef: keyRef, - Annotations: signature.AnnotationsMap{Annotations: annotations}, - CheckClaims: true, - HashAlgorithm: crypto.SHA256, - IgnoreTlog: true, - } - - args := []string{imageRef} - - return cmd.Exec(context.Background(), args) -} diff --git a/pkg/cosign/testing/test_util.go b/pkg/cosign/testing/test_util.go new file mode 100644 index 000000000..90dc2c854 --- /dev/null +++ b/pkg/cosign/testing/test_util.go @@ -0,0 +1,60 @@ +package testing + +import ( + "context" + "crypto" + "testing" + + cosignutil "github.com/pivotal/kpack/pkg/cosign/util" + + cosignVerify "github.com/sigstore/cosign/v2/cmd/cosign/cli/verify" + "github.com/sigstore/cosign/v2/pkg/cosign" + "github.com/sigstore/cosign/v2/pkg/signature" + "github.com/stretchr/testify/require" + corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" +) + +func GenerateFakeKeyPair(t *testing.T, secretName string, secretNamespace string, password string, annotations map[string]string) corev1.Secret { + t.Helper() + + passFunc := func(_ bool) ([]byte, error) { + return []byte(password), nil + } + + keys, err := cosign.GenerateKeyPair(passFunc) + require.NoError(t, err) + + data := map[string][]byte{ + cosignutil.SecretDataCosignPublicKey: keys.PublicBytes, + cosignutil.SecretDataCosignKey: keys.PrivateBytes, + cosignutil.SecretDataCosignPassword: []byte(password), + } + + secret := corev1.Secret{ + ObjectMeta: metav1.ObjectMeta{ + Name: secretName, + Namespace: secretNamespace, + Annotations: annotations, + }, + Data: data, + } + + return secret +} + +func Verify(t *testing.T, keyRef, imageRef string, annotations map[string]interface{}) error { + t.Helper() + + cmd := cosignVerify.VerifyCommand{ + KeyRef: keyRef, + Annotations: signature.AnnotationsMap{Annotations: annotations}, + CheckClaims: true, + HashAlgorithm: crypto.SHA256, + IgnoreTlog: true, + } + + args := []string{imageRef} + + return cmd.Exec(context.Background(), args) +} diff --git a/pkg/cosign/util/constants.go b/pkg/cosign/util/constants.go new file mode 100644 index 000000000..96efabd5f --- /dev/null +++ b/pkg/cosign/util/constants.go @@ -0,0 +1,12 @@ +package cosignutil + +const ( + CosignRepositoryEnv = "COSIGN_REPOSITORY" + CosignDockerMediaTypesEnv = "COSIGN_DOCKER_MEDIA_TYPES" + + SecretDataCosignKey = "cosign.key" + SecretDataCosignPassword = "cosign.password" + SecretDataCosignPublicKey = "cosign.pub" + DockerMediaTypesAnnotationPrefix = "kpack.io/cosign.docker-media-types" + RepositoryAnnotationPrefix = "kpack.io/cosign.repository" +) diff --git a/pkg/reconciler/builder/builder.go b/pkg/reconciler/builder/builder.go index 6f5279ca2..07ffbc766 100644 --- a/pkg/reconciler/builder/builder.go +++ b/pkg/reconciler/builder/builder.go @@ -3,6 +3,8 @@ package builder import ( "context" + corev1 "k8s.io/api/core/v1" + "go.uber.org/zap" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/labels" @@ -33,7 +35,11 @@ const ( ) type BuilderCreator interface { - CreateBuilder(ctx context.Context, builderKeychain authn.Keychain, keychain authn.Keychain, fetcher cnb.RemoteBuildpackFetcher, clusterStack *buildapi.ClusterStack, spec buildapi.BuilderSpec) (buildapi.BuilderRecord, error) + CreateBuilder(ctx context.Context, builderKeychain authn.Keychain, keychain authn.Keychain, fetcher cnb.RemoteBuildpackFetcher, clusterStack *buildapi.ClusterStack, spec buildapi.BuilderSpec, serviceAccountSecrets []*corev1.Secret) (buildapi.BuilderRecord, error) +} + +type Fetcher interface { + SecretsForServiceAccount(context.Context, string, string) ([]*corev1.Secret, error) } func NewController( @@ -46,6 +52,7 @@ func NewController( buildpackInformer buildinformers.BuildpackInformer, clusterBuildpackInformer buildinformers.ClusterBuildpackInformer, clusterStackInformer buildinformers.ClusterStackInformer, + secretFetcher Fetcher, ) (*controller.Impl, func()) { c := &Reconciler{ Client: opt.Client, @@ -56,6 +63,7 @@ func NewController( BuildpackLister: buildpackInformer.Lister(), ClusterBuildpackLister: clusterBuildpackInformer.Lister(), ClusterStackLister: clusterStackInformer.Lister(), + SecretFetcher: secretFetcher, } logger := opt.Logger.With( @@ -108,6 +116,7 @@ type Reconciler struct { BuildpackLister buildlisters.BuildpackLister ClusterBuildpackLister buildlisters.ClusterBuildpackLister ClusterStackLister buildlisters.ClusterStackLister + SecretFetcher Fetcher } func (c *Reconciler) Reconcile(ctx context.Context, key string) error { @@ -224,7 +233,12 @@ func (c *Reconciler) reconcileBuilder(ctx context.Context, builder *buildapi.Bui fetcher := cnb.NewRemoteBuildpackFetcher(c.KeychainFactory, clusterStore, buildpacks, clusterBuildpacks) - buildRecord, err := c.BuilderCreator.CreateBuilder(ctx, builderKeychain, stackKeychain, fetcher, clusterStack, builder.Spec.BuilderSpec) + serviceAccountSecrets, err := c.SecretFetcher.SecretsForServiceAccount(ctx, builder.Spec.ServiceAccount(), builder.Namespace) + if err != nil { + return buildapi.BuilderRecord{}, err + } + + buildRecord, err := c.BuilderCreator.CreateBuilder(ctx, builderKeychain, stackKeychain, fetcher, clusterStack, builder.Spec.BuilderSpec, serviceAccountSecrets) if err != nil { return buildapi.BuilderRecord{}, err } diff --git a/pkg/reconciler/builder/builder_test.go b/pkg/reconciler/builder/builder_test.go index 3b7c1718b..3db00fcf5 100644 --- a/pkg/reconciler/builder/builder_test.go +++ b/pkg/reconciler/builder/builder_test.go @@ -4,6 +4,10 @@ import ( "errors" "testing" + "github.com/pivotal/kpack/pkg/secret/secretfakes" + + k8sfake "k8s.io/client-go/kubernetes/fake" + "github.com/sclevine/spec" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" @@ -42,15 +46,19 @@ func testBuilderReconciler(t *testing.T, when spec.G, it spec.S) { ) var ( - builderCreator = &testhelpers.FakeBuilderCreator{} - keychainFactory = ®istryfakes.FakeKeychainFactory{} - fakeTracker = &testhelpers.FakeTracker{} + builderCreator = &testhelpers.FakeBuilderCreator{} + keychainFactory = ®istryfakes.FakeKeychainFactory{} + fakeTracker = &testhelpers.FakeTracker{} + fakeSecretFetcher = &secretfakes.FakeFetchSecret{ + FakeSecrets: []*corev1.Secret{}, + } ) rt := testhelpers.ReconcilerTester(t, func(t *testing.T, row *rtesting.TableRow) (reconciler controller.Reconciler, lists rtesting.ActionRecorderList, list rtesting.EventList) { listers := testhelpers.NewListers(row.Objects) fakeClient := fake.NewSimpleClientset(listers.BuildServiceObjects()...) + k8sfakeClient := k8sfake.NewSimpleClientset(listers.GetKubeObjects()...) r := &builder.Reconciler{ Client: fakeClient, BuilderLister: listers.GetBuilderLister(), @@ -61,10 +69,30 @@ func testBuilderReconciler(t *testing.T, when spec.G, it spec.S) { BuildpackLister: listers.GetBuildpackLister(), ClusterBuildpackLister: listers.GetClusterBuildpackLister(), ClusterStackLister: listers.GetClusterStackLister(), + SecretFetcher: fakeSecretFetcher, } - return &kreconciler.NetworkErrorReconciler{Reconciler: r}, rtesting.ActionRecorderList{fakeClient}, rtesting.EventList{Recorder: record.NewFakeRecorder(10)} + return &kreconciler.NetworkErrorReconciler{Reconciler: r}, rtesting.ActionRecorderList{fakeClient, k8sfakeClient}, rtesting.EventList{Recorder: record.NewFakeRecorder(10)} }) + signingSecret := corev1.Secret{ + ObjectMeta: metav1.ObjectMeta{ + Name: "some-secret-name", + Namespace: testNamespace, + }, + } + + serviceAccount := corev1.ServiceAccount{ + ObjectMeta: metav1.ObjectMeta{ + Name: "some-sa-name", + Namespace: signingSecret.Namespace, + }, + Secrets: []corev1.ObjectReference{ + { + Name: signingSecret.Name, + }, + }, + } + clusterStore := &buildapi.ClusterStore{ ObjectMeta: metav1.ObjectMeta{ Name: "some-store", @@ -165,18 +193,24 @@ func testBuilderReconciler(t *testing.T, when spec.G, it spec.S) { }, }}, }, - ServiceAccountName: "some-service-account", + ServiceAccountName: serviceAccount.Name, }, } secretRef := registry.SecretRef{ - ServiceAccount: builder.Spec.ServiceAccount(), - Namespace: builder.Namespace, + ServiceAccount: serviceAccount.Name, + Namespace: serviceAccount.Namespace, + } + + saSecretRef := registry.SecretRef{ + ServiceAccount: "some-service-account", + Namespace: testNamespace, } when("#Reconcile", func() { it.Before(func() { keychainFactory.AddKeychainForSecretRef(t, secretRef, ®istryfakes.FakeKeychain{}) + keychainFactory.AddKeychainForSecretRef(t, saSecretRef, ®istryfakes.FakeKeychain{}) }) it("saves metadata to the status", func() { @@ -243,6 +277,8 @@ func testBuilderReconciler(t *testing.T, when spec.G, it spec.S) { builder, buildpack, clusterBuildpack, + &signingSecret, + &serviceAccount, }, WantErr: false, WantStatusUpdates: []clientgotesting.UpdateActionImpl{ @@ -259,6 +295,7 @@ func testBuilderReconciler(t *testing.T, when spec.G, it spec.S) { Fetcher: expectedFetcher, ClusterStack: clusterStack, BuilderSpec: builder.Spec.BuilderSpec, + SigningSecrets: []*corev1.Secret{}, }}, builderCreator.CreateBuilderCalls) }) @@ -302,6 +339,8 @@ func testBuilderReconciler(t *testing.T, when spec.G, it spec.S) { buildpack, clusterBuildpack, expectedBuilder, + &signingSecret, + &serviceAccount, }, WantErr: false, }) @@ -365,6 +404,8 @@ func testBuilderReconciler(t *testing.T, when spec.G, it spec.S) { clusterStack, clusterStore, builder, + &signingSecret, + &serviceAccount, }, WantErr: false, }) @@ -379,6 +420,8 @@ func testBuilderReconciler(t *testing.T, when spec.G, it spec.S) { clusterStack, clusterStore, builder, + &signingSecret, + &serviceAccount, }, WantErr: true, WantStatusUpdates: []clientgotesting.UpdateActionImpl{ @@ -432,6 +475,8 @@ func testBuilderReconciler(t *testing.T, when spec.G, it spec.S) { notReadyClusterStack, clusterStore, builder, + &signingSecret, + &serviceAccount, }, WantErr: true, WantStatusUpdates: []clientgotesting.UpdateActionImpl{ diff --git a/pkg/reconciler/clusterbuilder/clusterbuilder.go b/pkg/reconciler/clusterbuilder/clusterbuilder.go index fc3564acd..7da247731 100644 --- a/pkg/reconciler/clusterbuilder/clusterbuilder.go +++ b/pkg/reconciler/clusterbuilder/clusterbuilder.go @@ -34,7 +34,11 @@ const ( ) type BuilderCreator interface { - CreateBuilder(ctx context.Context, builderKeychain authn.Keychain, stackKeychain authn.Keychain, fetcher cnb.RemoteBuildpackFetcher, clusterStack *buildapi.ClusterStack, spec buildapi.BuilderSpec) (buildapi.BuilderRecord, error) + CreateBuilder(ctx context.Context, builderKeychain authn.Keychain, stackKeychain authn.Keychain, fetcher cnb.RemoteBuildpackFetcher, clusterStack *buildapi.ClusterStack, spec buildapi.BuilderSpec, serviceAccountSecrets []*corev1.Secret) (buildapi.BuilderRecord, error) +} + +type Fetcher interface { + SecretsForServiceAccount(context.Context, string, string) ([]*corev1.Secret, error) } func NewController( @@ -46,6 +50,7 @@ func NewController( clusterStoreInformer buildinformers.ClusterStoreInformer, clusterBuildpackInformer buildinformers.ClusterBuildpackInformer, clusterStackInformer buildinformers.ClusterStackInformer, + secretFetcher Fetcher, ) (*controller.Impl, func()) { c := &Reconciler{ Client: opt.Client, @@ -55,6 +60,7 @@ func NewController( ClusterStoreLister: clusterStoreInformer.Lister(), ClusterBuildpackLister: clusterBuildpackInformer.Lister(), ClusterStackLister: clusterStackInformer.Lister(), + SecretFetcher: secretFetcher, } logger := opt.Logger.With( @@ -102,6 +108,7 @@ type Reconciler struct { ClusterStoreLister buildlisters.ClusterStoreLister ClusterBuildpackLister buildlisters.ClusterBuildpackLister ClusterStackLister buildlisters.ClusterStackLister + SecretFetcher Fetcher } func (c *Reconciler) Reconcile(ctx context.Context, key string) error { @@ -209,7 +216,12 @@ func (c *Reconciler) reconcileBuilder(ctx context.Context, builder *buildapi.Clu fetcher := cnb.NewRemoteBuildpackFetcher(c.KeychainFactory, clusterStore, nil, clusterBuildpacks) - buildRecord, err := c.BuilderCreator.CreateBuilder(ctx, builderKeychain, stackKeychain, fetcher, clusterStack, builder.Spec.BuilderSpec) + serviceAccountSecrets, err := c.SecretFetcher.SecretsForServiceAccount(ctx, builder.Spec.ServiceAccountRef.Name, builder.Spec.ServiceAccountRef.Namespace) + if err != nil { + return buildapi.BuilderRecord{}, err + } + + buildRecord, err := c.BuilderCreator.CreateBuilder(ctx, builderKeychain, stackKeychain, fetcher, clusterStack, builder.Spec.BuilderSpec, serviceAccountSecrets) if err != nil { return buildapi.BuilderRecord{}, err } diff --git a/pkg/reconciler/clusterbuilder/clusterbuilder_test.go b/pkg/reconciler/clusterbuilder/clusterbuilder_test.go index e15afa58b..42f6d48e7 100644 --- a/pkg/reconciler/clusterbuilder/clusterbuilder_test.go +++ b/pkg/reconciler/clusterbuilder/clusterbuilder_test.go @@ -5,6 +5,8 @@ import ( "errors" "testing" + "github.com/pivotal/kpack/pkg/secret/secretfakes" + "github.com/sclevine/spec" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" @@ -41,9 +43,12 @@ func testClusterBuilderReconciler(t *testing.T, when spec.G, it spec.S) { ) var ( - builderCreator = &testhelpers.FakeBuilderCreator{} - keychainFactory = ®istryfakes.FakeKeychainFactory{} - fakeTracker = &testhelpers.FakeTracker{} + builderCreator = &testhelpers.FakeBuilderCreator{} + keychainFactory = ®istryfakes.FakeKeychainFactory{} + fakeTracker = &testhelpers.FakeTracker{} + fakeSecretFetcher = &secretfakes.FakeFetchSecret{ + FakeSecrets: []*corev1.Secret{}, + } ) rt := testhelpers.ReconcilerTester(t, @@ -59,10 +64,30 @@ func testClusterBuilderReconciler(t *testing.T, when spec.G, it spec.S) { ClusterStoreLister: listers.GetClusterStoreLister(), ClusterBuildpackLister: listers.GetClusterBuildpackLister(), ClusterStackLister: listers.GetClusterStackLister(), + SecretFetcher: fakeSecretFetcher, } return &kreconciler.NetworkErrorReconciler{Reconciler: r}, rtesting.ActionRecorderList{fakeClient}, rtesting.EventList{Recorder: record.NewFakeRecorder(10)} }) + signingSecret := corev1.Secret{ + ObjectMeta: metav1.ObjectMeta{ + Name: "some-secret-name", + Namespace: "some-sa-namespace", + }, + } + + serviceAccount := corev1.ServiceAccount{ + ObjectMeta: metav1.ObjectMeta{ + Name: "some-sa-name", + Namespace: signingSecret.Namespace, + }, + Secrets: []corev1.ObjectReference{ + { + Name: signingSecret.Name, + }, + }, + } + clusterStore := &buildapi.ClusterStore{ ObjectMeta: metav1.ObjectMeta{ Name: "some-store", @@ -239,6 +264,8 @@ func testClusterBuilderReconciler(t *testing.T, when spec.G, it spec.S) { clusterStore, builder, clusterBuildpack, + &signingSecret, + &serviceAccount, }, WantErr: false, WantStatusUpdates: []clientgotesting.UpdateActionImpl{ @@ -255,6 +282,7 @@ func testClusterBuilderReconciler(t *testing.T, when spec.G, it spec.S) { Fetcher: expectedFetcher, ClusterStack: clusterStack, BuilderSpec: builder.Spec.BuilderSpec, + SigningSecrets: []*corev1.Secret{}, }}, builderCreator.CreateBuilderCalls) }) @@ -297,6 +325,8 @@ func testClusterBuilderReconciler(t *testing.T, when spec.G, it spec.S) { clusterStack, clusterStore, expectedBuilder, + &signingSecret, + &serviceAccount, }, WantErr: false, }) @@ -354,6 +384,8 @@ func testClusterBuilderReconciler(t *testing.T, when spec.G, it spec.S) { clusterStack, clusterStore, builder, + &signingSecret, + &serviceAccount, }, WantErr: false, }) @@ -386,6 +418,8 @@ func testClusterBuilderReconciler(t *testing.T, when spec.G, it spec.S) { clusterStack, clusterStore, builder, + &signingSecret, + &serviceAccount, }, WantErr: true, WantStatusUpdates: []clientgotesting.UpdateActionImpl{ diff --git a/pkg/reconciler/testhelpers/fake_builder_creator.go b/pkg/reconciler/testhelpers/fake_builder_creator.go index 65623f5cb..f7a07f53c 100644 --- a/pkg/reconciler/testhelpers/fake_builder_creator.go +++ b/pkg/reconciler/testhelpers/fake_builder_creator.go @@ -25,9 +25,10 @@ type CreateBuilderArgs struct { Fetcher cnb.RemoteBuildpackFetcher ClusterStack *buildapi.ClusterStack BuilderSpec buildapi.BuilderSpec + SigningSecrets []*corev1.Secret } -func (f *FakeBuilderCreator) CreateBuilder(ctx context.Context, builderKeychain authn.Keychain, stackKeychain authn.Keychain, fetcher cnb.RemoteBuildpackFetcher, clusterStack *buildapi.ClusterStack, spec buildapi.BuilderSpec) (buildapi.BuilderRecord, error) { +func (f *FakeBuilderCreator) CreateBuilder(ctx context.Context, builderKeychain authn.Keychain, stackKeychain authn.Keychain, fetcher cnb.RemoteBuildpackFetcher, clusterStack *buildapi.ClusterStack, spec buildapi.BuilderSpec, signingSecrets []*corev1.Secret) (buildapi.BuilderRecord, error) { f.CreateBuilderCalls = append(f.CreateBuilderCalls, CreateBuilderArgs{ Context: ctx, BuilderKeychain: builderKeychain, @@ -35,6 +36,7 @@ func (f *FakeBuilderCreator) CreateBuilder(ctx context.Context, builderKeychain Fetcher: fetcher, ClusterStack: clusterStack, BuilderSpec: spec, + SigningSecrets: signingSecrets, }) return f.Record, f.CreateErr diff --git a/pkg/secret/secretfakes/fake_fetcher.go b/pkg/secret/secretfakes/fake_fetcher.go new file mode 100644 index 000000000..29656db3b --- /dev/null +++ b/pkg/secret/secretfakes/fake_fetcher.go @@ -0,0 +1,27 @@ +package secretfakes + +import ( + "context" + + corev1 "k8s.io/api/core/v1" +) + +type FakeFetchSecret struct { + FakeSecrets []*corev1.Secret + ShouldError bool + ErrorOut error + + SecretsForServiceAccountFunc func(context.Context, string, string) ([]*corev1.Secret, error) +} + +func (f *FakeFetchSecret) SecretsForServiceAccount(ctx context.Context, serviceAccount, namespace string) ([]*corev1.Secret, error) { + if f.SecretsForServiceAccountFunc != nil { + return f.SecretsForServiceAccount(ctx, serviceAccount, namespace) + } + + if f.ShouldError { + return nil, f.ErrorOut + } + + return f.FakeSecrets, nil +} diff --git a/test/cosign_e2e_test.go b/test/cosign_e2e_test.go new file mode 100644 index 000000000..665d9b1e8 --- /dev/null +++ b/test/cosign_e2e_test.go @@ -0,0 +1,1123 @@ +package test + +import ( + "context" + "fmt" + "testing" + + cosigntesting "github.com/pivotal/kpack/pkg/cosign/testing" + cosignutil "github.com/pivotal/kpack/pkg/cosign/util" + + buildapi "github.com/pivotal/kpack/pkg/apis/build/v1alpha2" + corev1alpha1 "github.com/pivotal/kpack/pkg/apis/core/v1alpha1" + "github.com/sclevine/spec" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + corev1 "k8s.io/api/core/v1" + "k8s.io/apimachinery/pkg/api/errors" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" +) + +func testSignBuilder(t *testing.T, _ spec.G, it spec.S) { + const ( + testNamespace = "test" + dockerSecret = "docker-secret" + serviceAccountName = "image-service-account" + clusterStoreName = "store" + buildpackName = "buildpack" + clusterBuildpackName = "cluster-buildpack" + clusterStackName = "stack" + builderName = "custom-signed-builder" + clusterBuilderName = "custom-signed-cluster-builder" + cosignSecretName = "cosign-creds" + secretRefFormat = "k8s://%s/%s" + ) + + var ( + cfg config + clients *clients + ctx = context.Background() + builtImages map[string]struct{} + ) + + it.Before(func() { + cfg = loadConfig(t) + builtImages = map[string]struct{}{} + + var err error + clients, err = newClients(t) + require.NoError(t, err) + + err = clients.client.KpackV1alpha2().ClusterStores().Delete(ctx, clusterStoreName, metav1.DeleteOptions{}) + if !errors.IsNotFound(err) { + require.NoError(t, err) + } + + err = clients.client.KpackV1alpha2().Buildpacks(testNamespace).Delete(ctx, buildpackName, metav1.DeleteOptions{}) + if !errors.IsNotFound(err) { + require.NoError(t, err) + } + + err = clients.client.KpackV1alpha2().ClusterBuildpacks().Delete(ctx, clusterBuildpackName, metav1.DeleteOptions{}) + if !errors.IsNotFound(err) { + require.NoError(t, err) + } + + err = clients.client.KpackV1alpha2().ClusterStacks().Delete(ctx, clusterStackName, metav1.DeleteOptions{}) + if !errors.IsNotFound(err) { + require.NoError(t, err) + } + + err = clients.client.KpackV1alpha2().ClusterBuilders().Delete(ctx, clusterBuilderName, metav1.DeleteOptions{}) + if !errors.IsNotFound(err) { + require.NoError(t, err) + } + + deleteNamespace(t, ctx, clients, testNamespace) + + _, err = clients.k8sClient.CoreV1().Namespaces().Create(ctx, &corev1.Namespace{ + ObjectMeta: metav1.ObjectMeta{ + Name: testNamespace, + Labels: readNamespaceLabelsFromEnv(), + }, + }, metav1.CreateOptions{}) + require.NoError(t, err) + }) + + it.After(func() { + for tag := range builtImages { + deleteImageTag(t, tag) + } + }) + + it.Before(func() { + secret, err := cfg.makeRegistrySecret(dockerSecret, testNamespace) + require.NoError(t, err) + + _, err = clients.k8sClient.CoreV1().Secrets(testNamespace).Create(ctx, secret, metav1.CreateOptions{}) + require.NoError(t, err) + + _, err = clients.k8sClient.CoreV1().ServiceAccounts(testNamespace).Create(ctx, &corev1.ServiceAccount{ + ObjectMeta: metav1.ObjectMeta{ + Name: serviceAccountName, + }, + Secrets: []corev1.ObjectReference{ + { + Name: dockerSecret, + }, + }, + ImagePullSecrets: []corev1.LocalObjectReference{ + { + Name: dockerSecret, + }, + }, + }, metav1.CreateOptions{}) + require.NoError(t, err) + + _, err = clients.client.KpackV1alpha2().ClusterStores().Create(ctx, &buildapi.ClusterStore{ + ObjectMeta: metav1.ObjectMeta{ + Name: clusterStoreName, + }, + Spec: buildapi.ClusterStoreSpec{ + Sources: []corev1alpha1.ImageSource{ + {Image: "gcr.io/paketo-buildpacks/bellsoft-liberica"}, + {Image: "gcr.io/paketo-buildpacks/gradle"}, + {Image: "gcr.io/paketo-buildpacks/syft"}, + {Image: "gcr.io/paketo-buildpacks/executable-jar"}, + {Image: "gcr.io/paketo-buildpacks/dist-zip"}, + {Image: "gcr.io/paketo-buildpacks/spring-boot"}, + }, + }, + }, metav1.CreateOptions{}) + require.NoError(t, err) + + _, err = clients.client.KpackV1alpha2().Buildpacks(testNamespace).Create(ctx, &buildapi.Buildpack{ + ObjectMeta: metav1.ObjectMeta{ + Name: buildpackName, + }, + Spec: buildapi.BuildpackSpec{ + ImageSource: corev1alpha1.ImageSource{ + Image: "gcr.io/paketo-buildpacks/bellsoft-liberica", + }, + }, + }, metav1.CreateOptions{}) + require.NoError(t, err) + + _, err = clients.client.KpackV1alpha2().ClusterBuildpacks().Create(ctx, &buildapi.ClusterBuildpack{ + ObjectMeta: metav1.ObjectMeta{ + Name: clusterBuildpackName, + }, + Spec: buildapi.ClusterBuildpackSpec{ + ImageSource: corev1alpha1.ImageSource{ + Image: "gcr.io/paketo-buildpacks/nodejs", + }, + }, + }, metav1.CreateOptions{}) + require.NoError(t, err) + + _, err = clients.client.KpackV1alpha2().ClusterStacks().Create(ctx, &buildapi.ClusterStack{ + ObjectMeta: metav1.ObjectMeta{ + Name: clusterStackName, + }, + Spec: buildapi.ClusterStackSpec{ + Id: "io.buildpacks.stacks.bionic", + BuildImage: buildapi.ClusterStackSpecImage{ + Image: "gcr.io/paketo-buildpacks/build:base-cnb", + }, + RunImage: buildapi.ClusterStackSpecImage{ + Image: "gcr.io/paketo-buildpacks/run:base-cnb", + }, + }, + }, metav1.CreateOptions{}) + require.NoError(t, err) + }) + + it("Signs a Builder image successfully when the key is not password-protected", func() { + cosignCredSecret := cosigntesting.GenerateFakeKeyPair(t, cosignSecretName, testNamespace, "", nil) + _, err := clients.k8sClient.CoreV1().Secrets(testNamespace).Create(ctx, &cosignCredSecret, metav1.CreateOptions{}) + require.NoError(t, err) + + serviceAccount, err := clients.k8sClient.CoreV1().ServiceAccounts(testNamespace).Get(ctx, serviceAccountName, metav1.GetOptions{}) + require.NoError(t, err) + + if serviceAccount.Secrets == nil { + serviceAccount.Secrets = make([]corev1.ObjectReference, 0) + } + serviceAccount.Secrets = append(serviceAccount.Secrets, corev1.ObjectReference{Name: cosignCredSecret.Name}) + + _, err = clients.k8sClient.CoreV1().ServiceAccounts(testNamespace).Update(ctx, serviceAccount, metav1.UpdateOptions{}) + require.NoError(t, err) + + builder, err := clients.client.KpackV1alpha2().Builders(testNamespace).Create(ctx, &buildapi.Builder{ + ObjectMeta: metav1.ObjectMeta{ + Name: builderName, + Namespace: testNamespace, + }, + Spec: buildapi.NamespacedBuilderSpec{ + BuilderSpec: buildapi.BuilderSpec{ + Tag: cfg.newImageTag(), + Stack: corev1.ObjectReference{ + Name: clusterStackName, + Kind: "ClusterStack", + }, + Store: corev1.ObjectReference{ + Name: clusterStoreName, + Kind: "ClusterStore", + }, + Order: []buildapi.BuilderOrderEntry{ + { + Group: []buildapi.BuilderBuildpackRef{ + { + BuildpackRef: corev1alpha1.BuildpackRef{ + BuildpackInfo: corev1alpha1.BuildpackInfo{ + Id: "paketo-buildpacks/nodejs", + }, + }, + }, + }, + }, + { + Group: []buildapi.BuilderBuildpackRef{ + { + ObjectReference: corev1.ObjectReference{ + Name: buildpackName, + Kind: "Buildpack", + }, + BuildpackRef: corev1alpha1.BuildpackRef{ + BuildpackInfo: corev1alpha1.BuildpackInfo{ + Id: "paketo-buildpacks/bellsoft-liberica", + }, + }, + }, + { + BuildpackRef: corev1alpha1.BuildpackRef{ + BuildpackInfo: corev1alpha1.BuildpackInfo{ + Id: "paketo-buildpacks/gradle", + }, + Optional: true, + }, + }, + { + BuildpackRef: corev1alpha1.BuildpackRef{ + BuildpackInfo: corev1alpha1.BuildpackInfo{ + Id: "paketo-buildpacks/syft", + }, + }, + }, + { + BuildpackRef: corev1alpha1.BuildpackRef{ + BuildpackInfo: corev1alpha1.BuildpackInfo{ + Id: "paketo-buildpacks/executable-jar", + }, + }, + }, + { + BuildpackRef: corev1alpha1.BuildpackRef{ + BuildpackInfo: corev1alpha1.BuildpackInfo{ + Id: "paketo-buildpacks/dist-zip", + }, + }, + }, + { + BuildpackRef: corev1alpha1.BuildpackRef{ + BuildpackInfo: corev1alpha1.BuildpackInfo{ + Id: "paketo-buildpacks/spring-boot", + }, + }, + }, + }, + }, + }, + }, + ServiceAccountName: serviceAccountName, + }, + }, metav1.CreateOptions{}) + require.NoError(t, err) + + waitUntilReady(t, ctx, clients, builder) + + updatedBuilder, err := clients.client.KpackV1alpha2().Builders(testNamespace).Get(ctx, builderName, metav1.GetOptions{}) + require.NoError(t, err) + + assert.NotEmpty(t, updatedBuilder.Status.SignaturePaths) + assert.NotNil(t, updatedBuilder.Status.SignaturePaths[0]) + + err = cosigntesting.Verify(t, fmt.Sprintf(secretRefFormat, testNamespace, cosignSecretName), updatedBuilder.Status.LatestImage, nil) + require.NoError(t, err) + }) + + it("Signs a Builder image successfully when the key is password-protected", func() { + const CosignKeyPassword = "password" + + cosignCredSecret := cosigntesting.GenerateFakeKeyPair(t, cosignSecretName, testNamespace, CosignKeyPassword, nil) + _, err := clients.k8sClient.CoreV1().Secrets(testNamespace).Create(ctx, &cosignCredSecret, metav1.CreateOptions{}) + require.NoError(t, err) + + serviceAccount, err := clients.k8sClient.CoreV1().ServiceAccounts(testNamespace).Get(ctx, serviceAccountName, metav1.GetOptions{}) + require.NoError(t, err) + + if serviceAccount.Secrets == nil { + serviceAccount.Secrets = make([]corev1.ObjectReference, 0) + } + serviceAccount.Secrets = append(serviceAccount.Secrets, corev1.ObjectReference{Name: cosignCredSecret.Name}) + + _, err = clients.k8sClient.CoreV1().ServiceAccounts(testNamespace).Update(ctx, serviceAccount, metav1.UpdateOptions{}) + require.NoError(t, err) + + builder, err := clients.client.KpackV1alpha2().Builders(testNamespace).Create(ctx, &buildapi.Builder{ + ObjectMeta: metav1.ObjectMeta{ + Name: builderName, + Namespace: testNamespace, + }, + Spec: buildapi.NamespacedBuilderSpec{ + BuilderSpec: buildapi.BuilderSpec{ + Tag: cfg.newImageTag(), + Stack: corev1.ObjectReference{ + Name: clusterStackName, + Kind: "ClusterStack", + }, + Store: corev1.ObjectReference{ + Name: clusterStoreName, + Kind: "ClusterStore", + }, + Order: []buildapi.BuilderOrderEntry{ + { + Group: []buildapi.BuilderBuildpackRef{ + { + BuildpackRef: corev1alpha1.BuildpackRef{ + BuildpackInfo: corev1alpha1.BuildpackInfo{ + Id: "paketo-buildpacks/nodejs", + }, + }, + }, + }, + }, + { + Group: []buildapi.BuilderBuildpackRef{ + { + ObjectReference: corev1.ObjectReference{ + Name: buildpackName, + Kind: "Buildpack", + }, + BuildpackRef: corev1alpha1.BuildpackRef{ + BuildpackInfo: corev1alpha1.BuildpackInfo{ + Id: "paketo-buildpacks/bellsoft-liberica", + }, + }, + }, + { + BuildpackRef: corev1alpha1.BuildpackRef{ + BuildpackInfo: corev1alpha1.BuildpackInfo{ + Id: "paketo-buildpacks/gradle", + }, + Optional: true, + }, + }, + { + BuildpackRef: corev1alpha1.BuildpackRef{ + BuildpackInfo: corev1alpha1.BuildpackInfo{ + Id: "paketo-buildpacks/syft", + }, + }, + }, + { + BuildpackRef: corev1alpha1.BuildpackRef{ + BuildpackInfo: corev1alpha1.BuildpackInfo{ + Id: "paketo-buildpacks/executable-jar", + }, + }, + }, + { + BuildpackRef: corev1alpha1.BuildpackRef{ + BuildpackInfo: corev1alpha1.BuildpackInfo{ + Id: "paketo-buildpacks/dist-zip", + }, + }, + }, + { + BuildpackRef: corev1alpha1.BuildpackRef{ + BuildpackInfo: corev1alpha1.BuildpackInfo{ + Id: "paketo-buildpacks/spring-boot", + }, + }, + }, + }, + }, + }, + }, + ServiceAccountName: serviceAccountName, + }, + }, metav1.CreateOptions{}) + require.NoError(t, err) + + waitUntilReady(t, ctx, clients, builder) + + updatedBuilder, err := clients.client.KpackV1alpha2().Builders(testNamespace).Get(ctx, builderName, metav1.GetOptions{}) + require.NoError(t, err) + + assert.NotEmpty(t, updatedBuilder.Status.SignaturePaths) + assert.NotNil(t, updatedBuilder.Status.SignaturePaths[0]) + + err = cosigntesting.Verify(t, fmt.Sprintf(secretRefFormat, testNamespace, cosignSecretName), updatedBuilder.Status.LatestImage, nil) + require.NoError(t, err) + }) + + it("Generates more than one signature for a Builder image successfully when multiple secrets are present", func() { + const CosignKeyPassword = "password" + const cosignSecretName1 = "cosign-credentials-1" + const cosignSecretName2 = "cosign-credentials-2" + + cosignCredSecret1 := cosigntesting.GenerateFakeKeyPair(t, cosignSecretName1, testNamespace, CosignKeyPassword, nil) + _, err := clients.k8sClient.CoreV1().Secrets(testNamespace).Create(ctx, &cosignCredSecret1, metav1.CreateOptions{}) + require.NoError(t, err) + + cosignCredSecret2 := cosigntesting.GenerateFakeKeyPair(t, cosignSecretName2, testNamespace, CosignKeyPassword, nil) + _, err = clients.k8sClient.CoreV1().Secrets(testNamespace).Create(ctx, &cosignCredSecret2, metav1.CreateOptions{}) + require.NoError(t, err) + + serviceAccount, err := clients.k8sClient.CoreV1().ServiceAccounts(testNamespace).Get(ctx, serviceAccountName, metav1.GetOptions{}) + require.NoError(t, err) + + if serviceAccount.Secrets == nil { + serviceAccount.Secrets = make([]corev1.ObjectReference, 0) + } + serviceAccount.Secrets = append(serviceAccount.Secrets, + corev1.ObjectReference{Name: cosignCredSecret1.Name}, + corev1.ObjectReference{Name: cosignCredSecret2.Name}) + + _, err = clients.k8sClient.CoreV1().ServiceAccounts(testNamespace).Update(ctx, serviceAccount, metav1.UpdateOptions{}) + require.NoError(t, err) + + builder, err := clients.client.KpackV1alpha2().Builders(testNamespace).Create(ctx, &buildapi.Builder{ + ObjectMeta: metav1.ObjectMeta{ + Name: builderName, + Namespace: testNamespace, + }, + Spec: buildapi.NamespacedBuilderSpec{ + BuilderSpec: buildapi.BuilderSpec{ + Tag: cfg.newImageTag(), + Stack: corev1.ObjectReference{ + Name: clusterStackName, + Kind: "ClusterStack", + }, + Store: corev1.ObjectReference{ + Name: clusterStoreName, + Kind: "ClusterStore", + }, + Order: []buildapi.BuilderOrderEntry{ + { + Group: []buildapi.BuilderBuildpackRef{ + { + BuildpackRef: corev1alpha1.BuildpackRef{ + BuildpackInfo: corev1alpha1.BuildpackInfo{ + Id: "paketo-buildpacks/nodejs", + }, + }, + }, + }, + }, + { + Group: []buildapi.BuilderBuildpackRef{ + { + ObjectReference: corev1.ObjectReference{ + Name: buildpackName, + Kind: "Buildpack", + }, + BuildpackRef: corev1alpha1.BuildpackRef{ + BuildpackInfo: corev1alpha1.BuildpackInfo{ + Id: "paketo-buildpacks/bellsoft-liberica", + }, + }, + }, + { + BuildpackRef: corev1alpha1.BuildpackRef{ + BuildpackInfo: corev1alpha1.BuildpackInfo{ + Id: "paketo-buildpacks/gradle", + }, + Optional: true, + }, + }, + { + BuildpackRef: corev1alpha1.BuildpackRef{ + BuildpackInfo: corev1alpha1.BuildpackInfo{ + Id: "paketo-buildpacks/syft", + }, + }, + }, + { + BuildpackRef: corev1alpha1.BuildpackRef{ + BuildpackInfo: corev1alpha1.BuildpackInfo{ + Id: "paketo-buildpacks/executable-jar", + }, + }, + }, + { + BuildpackRef: corev1alpha1.BuildpackRef{ + BuildpackInfo: corev1alpha1.BuildpackInfo{ + Id: "paketo-buildpacks/dist-zip", + }, + }, + }, + { + BuildpackRef: corev1alpha1.BuildpackRef{ + BuildpackInfo: corev1alpha1.BuildpackInfo{ + Id: "paketo-buildpacks/spring-boot", + }, + }, + }, + }, + }, + }, + }, + ServiceAccountName: serviceAccountName, + }, + }, metav1.CreateOptions{}) + require.NoError(t, err) + + waitUntilReady(t, ctx, clients, builder) + + updatedBuilder, err := clients.client.KpackV1alpha2().Builders(testNamespace).Get(ctx, builderName, metav1.GetOptions{}) + require.NoError(t, err) + + assert.NotEmpty(t, updatedBuilder.Status.SignaturePaths) + assert.Equal(t, 2, len(updatedBuilder.Status.SignaturePaths)) + assert.NotNil(t, updatedBuilder.Status.SignaturePaths[0]) + assert.NotNil(t, updatedBuilder.Status.SignaturePaths[1]) + + // tag is assigned to a single signature, but both are still verifiable + err = cosigntesting.Verify(t, fmt.Sprintf(secretRefFormat, testNamespace, cosignSecretName1), updatedBuilder.Status.LatestImage, nil) + require.NoError(t, err) + + err = cosigntesting.Verify(t, fmt.Sprintf(secretRefFormat, testNamespace, cosignSecretName2), updatedBuilder.Status.LatestImage, nil) + require.NoError(t, err) + }) + + it("Saves a failure in the Builder record when signing fails", func() { + const CosignKeyPassword = "password" + const invalidPassword = "wrong-password" + const expectedErrorMessage = "unable to sign" + + cosignCredSecret := cosigntesting.GenerateFakeKeyPair(t, cosignSecretName, testNamespace, CosignKeyPassword, nil) + cosignCredSecret.Data[cosignutil.SecretDataCosignPassword] = []byte(invalidPassword) + + _, err := clients.k8sClient.CoreV1().Secrets(testNamespace).Create(ctx, &cosignCredSecret, metav1.CreateOptions{}) + require.NoError(t, err) + + serviceAccount, err := clients.k8sClient.CoreV1().ServiceAccounts(testNamespace).Get(ctx, serviceAccountName, metav1.GetOptions{}) + require.NoError(t, err) + + if serviceAccount.Secrets == nil { + serviceAccount.Secrets = make([]corev1.ObjectReference, 0) + } + serviceAccount.Secrets = append(serviceAccount.Secrets, + corev1.ObjectReference{Name: cosignCredSecret.Name}) + + _, err = clients.k8sClient.CoreV1().ServiceAccounts(testNamespace).Update(ctx, serviceAccount, metav1.UpdateOptions{}) + require.NoError(t, err) + + builder, err := clients.client.KpackV1alpha2().Builders(testNamespace).Create(ctx, &buildapi.Builder{ + ObjectMeta: metav1.ObjectMeta{ + Name: builderName, + Namespace: testNamespace, + }, + Spec: buildapi.NamespacedBuilderSpec{ + BuilderSpec: buildapi.BuilderSpec{ + Tag: cfg.newImageTag(), + Stack: corev1.ObjectReference{ + Name: clusterStackName, + Kind: "ClusterStack", + }, + Store: corev1.ObjectReference{ + Name: clusterStoreName, + Kind: "ClusterStore", + }, + Order: []buildapi.BuilderOrderEntry{ + { + Group: []buildapi.BuilderBuildpackRef{ + { + BuildpackRef: corev1alpha1.BuildpackRef{ + BuildpackInfo: corev1alpha1.BuildpackInfo{ + Id: "paketo-buildpacks/nodejs", + }, + }, + }, + }, + }, + { + Group: []buildapi.BuilderBuildpackRef{ + { + ObjectReference: corev1.ObjectReference{ + Name: buildpackName, + Kind: "Buildpack", + }, + BuildpackRef: corev1alpha1.BuildpackRef{ + BuildpackInfo: corev1alpha1.BuildpackInfo{ + Id: "paketo-buildpacks/bellsoft-liberica", + }, + }, + }, + { + BuildpackRef: corev1alpha1.BuildpackRef{ + BuildpackInfo: corev1alpha1.BuildpackInfo{ + Id: "paketo-buildpacks/gradle", + }, + Optional: true, + }, + }, + { + BuildpackRef: corev1alpha1.BuildpackRef{ + BuildpackInfo: corev1alpha1.BuildpackInfo{ + Id: "paketo-buildpacks/syft", + }, + }, + }, + { + BuildpackRef: corev1alpha1.BuildpackRef{ + BuildpackInfo: corev1alpha1.BuildpackInfo{ + Id: "paketo-buildpacks/executable-jar", + }, + }, + }, + { + BuildpackRef: corev1alpha1.BuildpackRef{ + BuildpackInfo: corev1alpha1.BuildpackInfo{ + Id: "paketo-buildpacks/dist-zip", + }, + }, + }, + { + BuildpackRef: corev1alpha1.BuildpackRef{ + BuildpackInfo: corev1alpha1.BuildpackInfo{ + Id: "paketo-buildpacks/spring-boot", + }, + }, + }, + }, + }, + }, + }, + ServiceAccountName: serviceAccountName, + }, + }, metav1.CreateOptions{}) + require.NoError(t, err) + + waitUntilFailed(t, ctx, clients, expectedErrorMessage, builder) + + updatedBuilder, err := clients.client.KpackV1alpha2().Builders(testNamespace).Get(ctx, builderName, metav1.GetOptions{}) + require.NoError(t, err) + require.NotNil(t, updatedBuilder.Status) + + readyConditionBuilder := updatedBuilder.Status.GetCondition(corev1alpha1.ConditionReady) + require.False(t, readyConditionBuilder.IsTrue()) + require.Contains(t, readyConditionBuilder.Message, expectedErrorMessage) + }) + + it("Signs a ClusterBuilder image successfully when the key is not password-protected", func() { + cosignCredSecret := cosigntesting.GenerateFakeKeyPair(t, cosignSecretName, testNamespace, "", nil) + _, err := clients.k8sClient.CoreV1().Secrets(testNamespace).Create(ctx, &cosignCredSecret, metav1.CreateOptions{}) + require.NoError(t, err) + + serviceAccount, err := clients.k8sClient.CoreV1().ServiceAccounts(testNamespace).Get(ctx, serviceAccountName, metav1.GetOptions{}) + require.NoError(t, err) + + if serviceAccount.Secrets == nil { + serviceAccount.Secrets = make([]corev1.ObjectReference, 0) + } + serviceAccount.Secrets = append(serviceAccount.Secrets, corev1.ObjectReference{Name: cosignCredSecret.Name}) + + _, err = clients.k8sClient.CoreV1().ServiceAccounts(testNamespace).Update(ctx, serviceAccount, metav1.UpdateOptions{}) + require.NoError(t, err) + + clusterBuilder, err := clients.client.KpackV1alpha2().ClusterBuilders().Create(ctx, &buildapi.ClusterBuilder{ + ObjectMeta: metav1.ObjectMeta{ + Name: clusterBuilderName, + }, + Spec: buildapi.ClusterBuilderSpec{ + BuilderSpec: buildapi.BuilderSpec{ + Tag: cfg.newImageTag(), + Stack: corev1.ObjectReference{ + Name: clusterStackName, + Kind: "ClusterStack", + }, + Store: corev1.ObjectReference{ + Name: clusterStoreName, + Kind: "ClusterStore", + }, + Order: []buildapi.BuilderOrderEntry{ + { + Group: []buildapi.BuilderBuildpackRef{ + { + ObjectReference: corev1.ObjectReference{ + Name: clusterBuildpackName, + Kind: "ClusterBuildpack", + }, + }, + }, + }, + { + Group: []buildapi.BuilderBuildpackRef{ + { + BuildpackRef: corev1alpha1.BuildpackRef{ + BuildpackInfo: corev1alpha1.BuildpackInfo{ + Id: "paketo-buildpacks/bellsoft-liberica", + }, + }, + }, + { + BuildpackRef: corev1alpha1.BuildpackRef{ + BuildpackInfo: corev1alpha1.BuildpackInfo{ + Id: "paketo-buildpacks/gradle", + }, + Optional: true, + }, + }, + { + BuildpackRef: corev1alpha1.BuildpackRef{ + BuildpackInfo: corev1alpha1.BuildpackInfo{ + Id: "paketo-buildpacks/syft", + }, + }, + }, + { + BuildpackRef: corev1alpha1.BuildpackRef{ + BuildpackInfo: corev1alpha1.BuildpackInfo{ + Id: "paketo-buildpacks/executable-jar", + }, + }, + }, + { + BuildpackRef: corev1alpha1.BuildpackRef{ + BuildpackInfo: corev1alpha1.BuildpackInfo{ + Id: "paketo-buildpacks/dist-zip", + }, + }, + }, + { + BuildpackRef: corev1alpha1.BuildpackRef{ + BuildpackInfo: corev1alpha1.BuildpackInfo{ + Id: "paketo-buildpacks/spring-boot", + }, + }, + }, + }, + }, + }, + }, + ServiceAccountRef: corev1.ObjectReference{ + Namespace: testNamespace, + Name: serviceAccountName, + }, + }, + }, metav1.CreateOptions{}) + require.NoError(t, err) + + waitUntilReady(t, ctx, clients, clusterBuilder) + + updatedBuilder, err := clients.client.KpackV1alpha2().ClusterBuilders().Get(ctx, clusterBuilderName, metav1.GetOptions{}) + require.NoError(t, err) + + assert.NotEmpty(t, updatedBuilder.Status.SignaturePaths) + assert.NotNil(t, updatedBuilder.Status.SignaturePaths[0]) + + err = cosigntesting.Verify(t, fmt.Sprintf(secretRefFormat, testNamespace, cosignSecretName), updatedBuilder.Status.LatestImage, nil) + require.NoError(t, err) + }) + + it("Signs a ClusterBuilder image successfully when the key is password-protected", func() { + const CosignKeyPassword = "password" + + cosignCredSecret := cosigntesting.GenerateFakeKeyPair(t, cosignSecretName, testNamespace, CosignKeyPassword, nil) + _, err := clients.k8sClient.CoreV1().Secrets(testNamespace).Create(ctx, &cosignCredSecret, metav1.CreateOptions{}) + require.NoError(t, err) + + serviceAccount, err := clients.k8sClient.CoreV1().ServiceAccounts(testNamespace).Get(ctx, serviceAccountName, metav1.GetOptions{}) + require.NoError(t, err) + + if serviceAccount.Secrets == nil { + serviceAccount.Secrets = make([]corev1.ObjectReference, 0) + } + serviceAccount.Secrets = append(serviceAccount.Secrets, corev1.ObjectReference{Name: cosignCredSecret.Name}) + + _, err = clients.k8sClient.CoreV1().ServiceAccounts(testNamespace).Update(ctx, serviceAccount, metav1.UpdateOptions{}) + require.NoError(t, err) + + clusterBuilder, err := clients.client.KpackV1alpha2().ClusterBuilders().Create(ctx, &buildapi.ClusterBuilder{ + ObjectMeta: metav1.ObjectMeta{ + Name: clusterBuilderName, + }, + Spec: buildapi.ClusterBuilderSpec{ + BuilderSpec: buildapi.BuilderSpec{ + Tag: cfg.newImageTag(), + Stack: corev1.ObjectReference{ + Name: clusterStackName, + Kind: "ClusterStack", + }, + Store: corev1.ObjectReference{ + Name: clusterStoreName, + Kind: "ClusterStore", + }, + Order: []buildapi.BuilderOrderEntry{ + { + Group: []buildapi.BuilderBuildpackRef{ + { + ObjectReference: corev1.ObjectReference{ + Name: clusterBuildpackName, + Kind: "ClusterBuildpack", + }, + }, + }, + }, + { + Group: []buildapi.BuilderBuildpackRef{ + { + BuildpackRef: corev1alpha1.BuildpackRef{ + BuildpackInfo: corev1alpha1.BuildpackInfo{ + Id: "paketo-buildpacks/bellsoft-liberica", + }, + }, + }, + { + BuildpackRef: corev1alpha1.BuildpackRef{ + BuildpackInfo: corev1alpha1.BuildpackInfo{ + Id: "paketo-buildpacks/gradle", + }, + Optional: true, + }, + }, + { + BuildpackRef: corev1alpha1.BuildpackRef{ + BuildpackInfo: corev1alpha1.BuildpackInfo{ + Id: "paketo-buildpacks/syft", + }, + }, + }, + { + BuildpackRef: corev1alpha1.BuildpackRef{ + BuildpackInfo: corev1alpha1.BuildpackInfo{ + Id: "paketo-buildpacks/executable-jar", + }, + }, + }, + { + BuildpackRef: corev1alpha1.BuildpackRef{ + BuildpackInfo: corev1alpha1.BuildpackInfo{ + Id: "paketo-buildpacks/dist-zip", + }, + }, + }, + { + BuildpackRef: corev1alpha1.BuildpackRef{ + BuildpackInfo: corev1alpha1.BuildpackInfo{ + Id: "paketo-buildpacks/spring-boot", + }, + }, + }, + }, + }, + }, + }, + ServiceAccountRef: corev1.ObjectReference{ + Namespace: testNamespace, + Name: serviceAccountName, + }, + }, + }, metav1.CreateOptions{}) + require.NoError(t, err) + + waitUntilReady(t, ctx, clients, clusterBuilder) + + updatedBuilder, err := clients.client.KpackV1alpha2().ClusterBuilders().Get(ctx, clusterBuilderName, metav1.GetOptions{}) + require.NoError(t, err) + + assert.NotEmpty(t, updatedBuilder.Status.SignaturePaths) + assert.NotNil(t, updatedBuilder.Status.SignaturePaths[0]) + + err = cosigntesting.Verify(t, fmt.Sprintf(secretRefFormat, testNamespace, cosignSecretName), updatedBuilder.Status.LatestImage, nil) + require.NoError(t, err) + }) + + it("Generates more than one signature for a ClusterBuilder image successfully when multiple secrets are present", func() { + const cosignKeyPassword = "password" + const cosignSecretName1 = "cosign-credentials-1" + const cosignSecretName2 = "cosign-credentials-2" + + cosignCredSecret1 := cosigntesting.GenerateFakeKeyPair(t, cosignSecretName1, testNamespace, cosignKeyPassword, nil) + _, err := clients.k8sClient.CoreV1().Secrets(testNamespace).Create(ctx, &cosignCredSecret1, metav1.CreateOptions{}) + require.NoError(t, err) + + cosignCredSecret2 := cosigntesting.GenerateFakeKeyPair(t, cosignSecretName2, testNamespace, cosignKeyPassword, nil) + _, err = clients.k8sClient.CoreV1().Secrets(testNamespace).Create(ctx, &cosignCredSecret2, metav1.CreateOptions{}) + require.NoError(t, err) + + serviceAccount, err := clients.k8sClient.CoreV1().ServiceAccounts(testNamespace).Get(ctx, serviceAccountName, metav1.GetOptions{}) + require.NoError(t, err) + + if serviceAccount.Secrets == nil { + serviceAccount.Secrets = make([]corev1.ObjectReference, 0) + } + serviceAccount.Secrets = append( + serviceAccount.Secrets, + corev1.ObjectReference{Name: cosignCredSecret1.Name}, + corev1.ObjectReference{Name: cosignCredSecret2.Name}) + + _, err = clients.k8sClient.CoreV1().ServiceAccounts(testNamespace).Update(ctx, serviceAccount, metav1.UpdateOptions{}) + require.NoError(t, err) + + clusterBuilder, err := clients.client.KpackV1alpha2().ClusterBuilders().Create(ctx, &buildapi.ClusterBuilder{ + ObjectMeta: metav1.ObjectMeta{ + Name: clusterBuilderName, + }, + Spec: buildapi.ClusterBuilderSpec{ + BuilderSpec: buildapi.BuilderSpec{ + Tag: cfg.newImageTag(), + Stack: corev1.ObjectReference{ + Name: clusterStackName, + Kind: "ClusterStack", + }, + Store: corev1.ObjectReference{ + Name: clusterStoreName, + Kind: "ClusterStore", + }, + Order: []buildapi.BuilderOrderEntry{ + { + Group: []buildapi.BuilderBuildpackRef{ + { + ObjectReference: corev1.ObjectReference{ + Name: clusterBuildpackName, + Kind: "ClusterBuildpack", + }, + }, + }, + }, + { + Group: []buildapi.BuilderBuildpackRef{ + { + BuildpackRef: corev1alpha1.BuildpackRef{ + BuildpackInfo: corev1alpha1.BuildpackInfo{ + Id: "paketo-buildpacks/bellsoft-liberica", + }, + }, + }, + { + BuildpackRef: corev1alpha1.BuildpackRef{ + BuildpackInfo: corev1alpha1.BuildpackInfo{ + Id: "paketo-buildpacks/gradle", + }, + Optional: true, + }, + }, + { + BuildpackRef: corev1alpha1.BuildpackRef{ + BuildpackInfo: corev1alpha1.BuildpackInfo{ + Id: "paketo-buildpacks/syft", + }, + }, + }, + { + BuildpackRef: corev1alpha1.BuildpackRef{ + BuildpackInfo: corev1alpha1.BuildpackInfo{ + Id: "paketo-buildpacks/executable-jar", + }, + }, + }, + { + BuildpackRef: corev1alpha1.BuildpackRef{ + BuildpackInfo: corev1alpha1.BuildpackInfo{ + Id: "paketo-buildpacks/dist-zip", + }, + }, + }, + { + BuildpackRef: corev1alpha1.BuildpackRef{ + BuildpackInfo: corev1alpha1.BuildpackInfo{ + Id: "paketo-buildpacks/spring-boot", + }, + }, + }, + }, + }, + }, + }, + ServiceAccountRef: corev1.ObjectReference{ + Namespace: testNamespace, + Name: serviceAccountName, + }, + }, + }, metav1.CreateOptions{}) + require.NoError(t, err) + + waitUntilReady(t, ctx, clients, clusterBuilder) + + updatedBuilder, err := clients.client.KpackV1alpha2().ClusterBuilders().Get(ctx, clusterBuilderName, metav1.GetOptions{}) + require.NoError(t, err) + + assert.NotEmpty(t, updatedBuilder.Status.SignaturePaths) + assert.Equal(t, 2, len(updatedBuilder.Status.SignaturePaths)) + assert.NotNil(t, updatedBuilder.Status.SignaturePaths[0]) + assert.NotNil(t, updatedBuilder.Status.SignaturePaths[1]) + + err = cosigntesting.Verify(t, fmt.Sprintf(secretRefFormat, testNamespace, cosignSecretName1), updatedBuilder.Status.LatestImage, nil) + require.NoError(t, err) + + err = cosigntesting.Verify(t, fmt.Sprintf(secretRefFormat, testNamespace, cosignSecretName2), updatedBuilder.Status.LatestImage, nil) + require.NoError(t, err) + }) + + it("Saves a failure in the ClusterBuilder record when signing fails", func() { + const cosignKeyPassword = "password" + const invalidPassword = "wrong-password" + const expectedErrorMessage = "unable to sign" + + cosignCredSecret := cosigntesting.GenerateFakeKeyPair(t, cosignSecretName, testNamespace, cosignKeyPassword, nil) + cosignCredSecret.Data[cosignutil.SecretDataCosignPassword] = []byte(invalidPassword) + + _, err = clients.k8sClient.CoreV1().Secrets(testNamespace).Create(ctx, &cosignCredSecret, metav1.CreateOptions{}) + require.NoError(t, err) + + serviceAccount, err := clients.k8sClient.CoreV1().ServiceAccounts(testNamespace).Get(ctx, serviceAccountName, metav1.GetOptions{}) + require.NoError(t, err) + + if serviceAccount.Secrets == nil { + serviceAccount.Secrets = make([]corev1.ObjectReference, 0) + } + serviceAccount.Secrets = append( + serviceAccount.Secrets, + corev1.ObjectReference{Name: cosignCredSecret.Name}) + + _, err = clients.k8sClient.CoreV1().ServiceAccounts(testNamespace).Update(ctx, serviceAccount, metav1.UpdateOptions{}) + require.NoError(t, err) + + clusterBuilder, err := clients.client.KpackV1alpha2().ClusterBuilders().Create(ctx, &buildapi.ClusterBuilder{ + ObjectMeta: metav1.ObjectMeta{ + Name: clusterBuilderName, + }, + Spec: buildapi.ClusterBuilderSpec{ + BuilderSpec: buildapi.BuilderSpec{ + Tag: cfg.newImageTag(), + Stack: corev1.ObjectReference{ + Name: clusterStackName, + Kind: "ClusterStack", + }, + Store: corev1.ObjectReference{ + Name: clusterStoreName, + Kind: "ClusterStore", + }, + Order: []buildapi.BuilderOrderEntry{ + { + Group: []buildapi.BuilderBuildpackRef{ + { + ObjectReference: corev1.ObjectReference{ + Name: clusterBuildpackName, + Kind: "ClusterBuildpack", + }, + }, + }, + }, + { + Group: []buildapi.BuilderBuildpackRef{ + { + BuildpackRef: corev1alpha1.BuildpackRef{ + BuildpackInfo: corev1alpha1.BuildpackInfo{ + Id: "paketo-buildpacks/bellsoft-liberica", + }, + }, + }, + { + BuildpackRef: corev1alpha1.BuildpackRef{ + BuildpackInfo: corev1alpha1.BuildpackInfo{ + Id: "paketo-buildpacks/gradle", + }, + Optional: true, + }, + }, + { + BuildpackRef: corev1alpha1.BuildpackRef{ + BuildpackInfo: corev1alpha1.BuildpackInfo{ + Id: "paketo-buildpacks/syft", + }, + }, + }, + { + BuildpackRef: corev1alpha1.BuildpackRef{ + BuildpackInfo: corev1alpha1.BuildpackInfo{ + Id: "paketo-buildpacks/executable-jar", + }, + }, + }, + { + BuildpackRef: corev1alpha1.BuildpackRef{ + BuildpackInfo: corev1alpha1.BuildpackInfo{ + Id: "paketo-buildpacks/dist-zip", + }, + }, + }, + { + BuildpackRef: corev1alpha1.BuildpackRef{ + BuildpackInfo: corev1alpha1.BuildpackInfo{ + Id: "paketo-buildpacks/spring-boot", + }, + }, + }, + }, + }, + }, + }, + ServiceAccountRef: corev1.ObjectReference{ + Namespace: testNamespace, + Name: serviceAccountName, + }, + }, + }, metav1.CreateOptions{}) + require.NoError(t, err) + + waitUntilFailed(t, ctx, clients, expectedErrorMessage, clusterBuilder) + + updatedBuilder, err := clients.client.KpackV1alpha2().ClusterBuilders().Get(ctx, clusterBuilderName, metav1.GetOptions{}) + require.NoError(t, err) + require.NotNil(t, updatedBuilder.Status) + + readyConditionBuilder := updatedBuilder.Status.GetCondition(corev1alpha1.ConditionReady) + require.False(t, readyConditionBuilder.IsTrue()) + require.Contains(t, readyConditionBuilder.Message, expectedErrorMessage) + }) +} diff --git a/test/execute_build_test.go b/test/execute_build_test.go index 6be2d8452..55120506c 100644 --- a/test/execute_build_test.go +++ b/test/execute_build_test.go @@ -35,13 +35,14 @@ import ( "github.com/pivotal/kpack/pkg/registry" ) -func TestCreateImage(t *testing.T) { +func TestKpackE2E(t *testing.T) { rand.Seed(time.Now().Unix()) spec.Run(t, "CreateImage", testCreateImage) + spec.Run(t, "SignBuilder", testSignBuilder) } -func testCreateImage(t *testing.T, when spec.G, it spec.S) { +func testCreateImage(t *testing.T, _ spec.G, it spec.S) { const ( testNamespace = "test" dockerSecret = "docker-secret" @@ -657,6 +658,26 @@ func waitUntilReady(t *testing.T, ctx context.Context, clients *clients, objects } } +func waitUntilFailed(t *testing.T, ctx context.Context, clients *clients, expectedMessage string, objects ...kmeta.OwnerRefable) { + for _, ob := range objects { + namespace := ob.GetObjectMeta().GetNamespace() + name := ob.GetObjectMeta().GetName() + gvr, _ := meta.UnsafeGuessKindToResource(ob.GetGroupVersionKind()) + + eventually(t, func() bool { + unstructured, err := clients.dynamicClient.Resource(gvr).Namespace(namespace).Get(ctx, name, metav1.GetOptions{}) + require.NoError(t, err) + + kResource := &duckv1.KResource{} + err = duck.FromUnstructured(unstructured, kResource) + require.NoError(t, err) + + condition := kResource.Status.GetCondition(apis.ConditionReady) + return condition.IsFalse() && "" != condition.Message && strings.Contains(condition.Message, expectedMessage) + }, 1*time.Second, 8*time.Minute) + } +} + func validateImageCreate(t *testing.T, clients *clients, image *buildapi.Image, expectedResources corev1.ResourceRequirements) string { ctx, cancel := context.WithCancel(context.Background()) defer cancel()