From b6fac96e846a98b514e05f6a77ee567bf86ae265 Mon Sep 17 00:00:00 2001 From: Slavek Kabrda Date: Tue, 20 Aug 2024 13:57:45 +0200 Subject: [PATCH] Allow signing local image without registry access Signed-off-by: Slavek Kabrda --- cmd/cosign/cli/sign/sign.go | 25 +++++++++++++++++-------- test/e2e_test.go | 30 ++++++++++++++++++++++++++++++ test/helpers.go | 16 ++++++++++++++++ 3 files changed, 63 insertions(+), 8 deletions(-) diff --git a/cmd/cosign/cli/sign/sign.go b/cmd/cosign/cli/sign/sign.go index 1289e7b1bb4..b4b282eb012 100644 --- a/cmd/cosign/cli/sign/sign.go +++ b/cmd/cosign/cli/sign/sign.go @@ -180,11 +180,15 @@ func SignCmd(ro *options.RootOptions, ko options.KeyOpts, signOpts options.SignO } if digest, ok := ref.(name.Digest); ok && !signOpts.Recursive { - se, err := ociremote.SignedEntity(ref, opts...) - if _, isEntityNotFoundErr := err.(*ociremote.EntityNotFoundError); isEntityNotFoundErr { - se = ociremote.SignedUnknown(digest) - } else if err != nil { - return fmt.Errorf("accessing image: %w", err) + var se *oci.SignedEntity = nil + if signOpts.Upload { + tmpse, err := ociremote.SignedEntity(ref, opts...) + if _, isEntityNotFoundErr := err.(*ociremote.EntityNotFoundError); isEntityNotFoundErr { + tmpse = ociremote.SignedUnknown(digest) + } else if err != nil { + return fmt.Errorf("accessing image: %w", err) + } + se = &tmpse } err = signDigest(ctx, digest, staticPayload, ko, signOpts, annotations, dd, sv, se) if err != nil { @@ -205,7 +209,7 @@ func SignCmd(ro *options.RootOptions, ko options.KeyOpts, signOpts options.SignO return fmt.Errorf("computing digest: %w", err) } digest := ref.Context().Digest(d.String()) - err = signDigest(ctx, digest, staticPayload, ko, signOpts, annotations, dd, sv, se) + err = signDigest(ctx, digest, staticPayload, ko, signOpts, annotations, dd, sv, &se) if err != nil { return fmt.Errorf("signing digest: %w", err) } @@ -220,7 +224,7 @@ func SignCmd(ro *options.RootOptions, ko options.KeyOpts, signOpts options.SignO func signDigest(ctx context.Context, digest name.Digest, payload []byte, ko options.KeyOpts, signOpts options.SignOptions, annotations map[string]interface{}, - dd mutate.DupeDetector, sv *SignerVerifier, se oci.SignedEntity) error { + dd mutate.DupeDetector, sv *SignerVerifier, se *oci.SignedEntity) error { var err error // The payload can be passed to skip generation. if len(payload) == 0 { @@ -234,6 +238,11 @@ func signDigest(ctx context.Context, digest name.Digest, payload []byte, ko opti } } + if se == nil && signOpts.Upload { + // this will only happen if this function is used wrong by the caller + return fmt.Errorf("can't upload signature without OCI repository reference") + } + var s icos.Signer s = ipayload.NewSigner(sv) if sv.Cert != nil { @@ -329,7 +338,7 @@ func signDigest(ctx context.Context, digest name.Digest, payload []byte, ko opti } // Attach the signature to the entity. - newSE, err := mutate.AttachSignatureToEntity(se, ociSig, mutate.WithDupeDetector(dd), mutate.WithRecordCreationTimestamp(signOpts.RecordCreationTimestamp)) + newSE, err := mutate.AttachSignatureToEntity(*se, ociSig, mutate.WithDupeDetector(dd), mutate.WithRecordCreationTimestamp(signOpts.RecordCreationTimestamp)) if err != nil { return err } diff --git a/test/e2e_test.go b/test/e2e_test.go index 97444ad002a..c416836f0e5 100644 --- a/test/e2e_test.go +++ b/test/e2e_test.go @@ -2511,3 +2511,33 @@ func getOIDCToken() (string, error) { } return string(body), nil } + +func TestSignVerifyLocalImageNoUpload(t *testing.T) { + td := t.TempDir() + + imgName := path.Join("unavailable.repo.sigstore.dev", "cosign-e2e@sha256:2bbea7758536b170efcb168dc7cea3379908c2649af3e75ebac10161ddd513c2") + + _, privKeyPath, pubKeyPath := keypair(t, td) + + // Verify should fail at first + mustErr(verify(pubKeyPath, imgName, true, nil, "", true), t) + + // Now sign the image + outputSignature := filepath.Join(td, "signature") + outputCertificate := filepath.Join(td, "certificate") + ko := options.KeyOpts{ + KeyRef: privKeyPath, + PassFunc: passFunc, + SkipConfirmation: true, + } + so := options.SignOptions{ + OutputCertificate: outputCertificate, + OutputSignature: outputSignature, + Upload: false, + TlogUpload: false, + } + must(sign.SignCmd(ro, ko, so, []string{imgName}), t) + + // Now verify should work! + must(verifyImageLocally(pubKeyPath, imgName, outputSignature, outputCertificate, true), t) +} diff --git a/test/helpers.go b/test/helpers.go index 2db7092674f..f1ad7168aa8 100644 --- a/test/helpers.go +++ b/test/helpers.go @@ -254,6 +254,22 @@ var verifyOffline = func(keyRef, imageRef string, checkClaims bool, annotations return cmd.Exec(context.Background(), args) } +var verifyImageLocally = func(keyRef, imageRef, sigFile, certFile string, skipTlogVerify bool) error { + cmd := cliverify.VerifyCommand{ + KeyRef: keyRef, + MaxWorkers: 10, + IgnoreTlog: skipTlogVerify, + SignatureRef: sigFile, + CertVerifyOptions: options.CertVerifyOptions{ + Cert: certFile, + }, + } + + args := []string{imageRef} + + return cmd.Exec(context.Background(), args) +} + var ro = &options.RootOptions{Timeout: options.DefaultTimeout} func keypair(t *testing.T, td string) (*cosign.KeysBytes, string, string) {