From 943e82438021a1ee75d9e96f6a773e948a8583a4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Furkan=20T=C3=BCrkal?= Date: Thu, 18 Nov 2021 03:56:26 +0300 Subject: [PATCH] feat: add output flag for signCmd (#1066) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat: add output flag for signCmd Fixes #1059 Signed-off-by: Furkan Co-authored-off-by: Batuhan * docs: regenerate doc gen Signed-off-by: Batuhan Apaydın Co-authored-by: Batuhan Apaydın --- cmd/cosign/cli/options/sign.go | 4 ++++ cmd/cosign/cli/sign.go | 2 +- cmd/cosign/cli/sign/sign.go | 23 +++++++++++++++++++---- cmd/cosign/cli/sign/sign_test.go | 2 +- doc/cosign_sign.md | 1 + test/e2e_test.go | 22 +++++++++++----------- 6 files changed, 37 insertions(+), 17 deletions(-) diff --git a/cmd/cosign/cli/options/sign.go b/cmd/cosign/cli/options/sign.go index fd005c93b72..38b59d44f7e 100644 --- a/cmd/cosign/cli/options/sign.go +++ b/cmd/cosign/cli/options/sign.go @@ -24,6 +24,7 @@ type SignOptions struct { Key string Cert string Upload bool + Output string PayloadPath string Force bool Recursive bool @@ -57,6 +58,9 @@ func (o *SignOptions) AddFlags(cmd *cobra.Command) { cmd.Flags().BoolVar(&o.Upload, "upload", true, "whether to upload the signature") + cmd.Flags().StringVar(&o.Output, "output", "", + "write the signature to FILE") + cmd.Flags().StringVar(&o.PayloadPath, "payload", "", "path to a payload file to use rather than generating one") diff --git a/cmd/cosign/cli/sign.go b/cmd/cosign/cli/sign.go index 31bc945962e..0af804f2722 100644 --- a/cmd/cosign/cli/sign.go +++ b/cmd/cosign/cli/sign.go @@ -89,7 +89,7 @@ func Sign() *cobra.Command { if err != nil { return err } - if err := sign.SignCmd(cmd.Context(), ko, o.Registry, annotationsMap.Annotations, args, o.Cert, o.Upload, o.PayloadPath, o.Force, o.Recursive, o.Attachment); err != nil { + if err := sign.SignCmd(cmd.Context(), ko, o.Registry, annotationsMap.Annotations, args, o.Cert, o.Upload, o.Output, o.PayloadPath, o.Force, o.Recursive, o.Attachment); err != nil { if o.Attachment == "" { return errors.Wrapf(err, "signing %v", args) } diff --git a/cmd/cosign/cli/sign/sign.go b/cmd/cosign/cli/sign/sign.go index e6b22e3406a..8fe35f7b86e 100644 --- a/cmd/cosign/cli/sign/sign.go +++ b/cmd/cosign/cli/sign/sign.go @@ -125,7 +125,7 @@ func GetAttachedImageRef(ref name.Reference, attachment string, opts ...ociremot // nolint func SignCmd(ctx context.Context, ko KeyOpts, regOpts options.RegistryOptions, annotations map[string]interface{}, - imgs []string, certPath string, upload bool, payloadPath string, force bool, recursive bool, attachment string) error { + imgs []string, certPath string, upload bool, output string, payloadPath string, force bool, recursive bool, attachment string) error { if options.EnableExperimental() { if options.NOf(ko.KeyRef, ko.Sk) > 1 { return &options.KeyParseError{} @@ -177,7 +177,7 @@ func SignCmd(ctx context.Context, ko KeyOpts, regOpts options.RegistryOptions, a if err != nil { return errors.Wrap(err, "accessing image") } - err = signDigest(ctx, digest, staticPayload, ko, regOpts, annotations, upload, force, dd, sv, se) + err = signDigest(ctx, digest, staticPayload, ko, regOpts, annotations, upload, output, force, dd, sv, se) if err != nil { return errors.Wrap(err, "signing digest") } @@ -197,7 +197,7 @@ func SignCmd(ctx context.Context, ko KeyOpts, regOpts options.RegistryOptions, a } digest := ref.Context().Digest(d.String()) - err = signDigest(ctx, digest, staticPayload, ko, regOpts, annotations, upload, force, dd, sv, se) + err = signDigest(ctx, digest, staticPayload, ko, regOpts, annotations, upload, output, force, dd, sv, se) if err != nil { return errors.Wrap(err, "signing digest") } @@ -211,7 +211,7 @@ func SignCmd(ctx context.Context, ko KeyOpts, regOpts options.RegistryOptions, a } func signDigest(ctx context.Context, digest name.Digest, payload []byte, ko KeyOpts, - regOpts options.RegistryOptions, annotations map[string]interface{}, upload bool, force bool, + regOpts options.RegistryOptions, annotations map[string]interface{}, upload bool, output string, force bool, dd mutate.DupeDetector, sv *CertSignVerifier, se oci.SignedEntity) error { var err error // The payload can be passed to skip generation. @@ -236,6 +236,21 @@ func signDigest(ctx context.Context, digest name.Digest, payload []byte, ko KeyO return nil } + if output != "" { + f, err := os.Create(output) + if err != nil { + return errors.Wrap(err, "create signature file") + } + defer f.Close() + _, err = f.Write([]byte(b64sig)) + + if err != nil { + return errors.Wrap(err, "write signature to file") + } + + fmt.Fprintf(os.Stderr, "Signature wrote to the file %s\n", f.Name()) + } + opts := []static.Option{} if sv.Cert != nil { opts = append(opts, static.WithCertChain(sv.Cert, sv.Chain)) diff --git a/cmd/cosign/cli/sign/sign_test.go b/cmd/cosign/cli/sign/sign_test.go index 56c5706d212..b8eac27691b 100644 --- a/cmd/cosign/cli/sign/sign_test.go +++ b/cmd/cosign/cli/sign/sign_test.go @@ -37,7 +37,7 @@ func TestSignCmdLocalKeyAndSk(t *testing.T) { Sk: true, }, } { - err := SignCmd(ctx, ko, options.RegistryOptions{}, nil, nil, "", false, "", false, false, "") + err := SignCmd(ctx, ko, options.RegistryOptions{}, nil, nil, "", false, "", "", false, false, "") if (errors.Is(err, &options.KeyParseError{}) == false) { t.Fatal("expected KeyParseError") } diff --git a/doc/cosign_sign.md b/doc/cosign_sign.md index 33b95861a64..2692cb29c97 100644 --- a/doc/cosign_sign.md +++ b/doc/cosign_sign.md @@ -64,6 +64,7 @@ cosign sign [flags] --oidc-client-id string [EXPERIMENTAL] OIDC client ID for application (default "sigstore") --oidc-client-secret string [EXPERIMENTAL] OIDC client secret for application --oidc-issuer string [EXPERIMENTAL] OIDC provider to be used to issue ID token (default "https://oauth2.sigstore.dev/auth") + --output string write the signature to FILE --payload string path to a payload file to use rather than generating one -r, --recursive if a multi-arch image is specified, additionally sign each discrete image --rekor-url string [EXPERIMENTAL] address of rekor STL server (default "https://rekor.sigstore.dev") diff --git a/test/e2e_test.go b/test/e2e_test.go index a78781ca979..56b6eeefc98 100644 --- a/test/e2e_test.go +++ b/test/e2e_test.go @@ -106,7 +106,7 @@ func TestSignVerify(t *testing.T) { // Now sign the image ko := sign.KeyOpts{KeyRef: privKeyPath, PassFunc: passFunc} - must(sign.SignCmd(ctx, ko, options.RegistryOptions{}, nil, []string{imgName}, "", true, "", false, false, ""), t) + must(sign.SignCmd(ctx, ko, options.RegistryOptions{}, nil, []string{imgName}, "", true, "", "", false, false, ""), t) // Now verify and download should work! must(verify(pubKeyPath, imgName, true, nil, ""), t) @@ -117,7 +117,7 @@ func TestSignVerify(t *testing.T) { // Sign the image with an annotation annotations := map[string]interface{}{"foo": "bar"} - must(sign.SignCmd(ctx, ko, options.RegistryOptions{}, annotations, []string{imgName}, "", true, "", false, false, ""), t) + must(sign.SignCmd(ctx, ko, options.RegistryOptions{}, annotations, []string{imgName}, "", true, "", "", false, false, ""), t) // It should match this time. must(verify(pubKeyPath, imgName, true, map[string]interface{}{"foo": "bar"}, ""), t) @@ -141,7 +141,7 @@ func TestSignVerifyClean(t *testing.T) { // Now sign the image ko := sign.KeyOpts{KeyRef: privKeyPath, PassFunc: passFunc} - must(sign.SignCmd(ctx, ko, options.RegistryOptions{}, nil, []string{imgName}, "", true, "", false, false, ""), t) + must(sign.SignCmd(ctx, ko, options.RegistryOptions{}, nil, []string{imgName}, "", true, "", "", false, false, ""), t) // Now verify and download should work! must(verify(pubKeyPath, imgName, true, nil, ""), t) @@ -233,7 +233,7 @@ func TestBundle(t *testing.T) { } // Sign the image - must(sign.SignCmd(ctx, ko, options.RegistryOptions{}, nil, []string{imgName}, "", true, "", false, false, ""), t) + must(sign.SignCmd(ctx, ko, options.RegistryOptions{}, nil, []string{imgName}, "", true, "", "", false, false, ""), t) // Make sure verify works must(verify(pubKeyPath, imgName, true, nil, ""), t) @@ -263,14 +263,14 @@ func TestDuplicateSign(t *testing.T) { // Now sign the image ko := sign.KeyOpts{KeyRef: privKeyPath, PassFunc: passFunc} - must(sign.SignCmd(ctx, ko, options.RegistryOptions{}, nil, []string{imgName}, "", true, "", false, false, ""), t) + must(sign.SignCmd(ctx, ko, options.RegistryOptions{}, nil, []string{imgName}, "", true, "", "", false, false, ""), t) // Now verify and download should work! must(verify(pubKeyPath, imgName, true, nil, ""), t) must(download.SignatureCmd(ctx, options.RegistryOptions{}, imgName), t) // Signing again should work just fine... - must(sign.SignCmd(ctx, ko, options.RegistryOptions{}, nil, []string{imgName}, "", true, "", false, false, ""), t) + must(sign.SignCmd(ctx, ko, options.RegistryOptions{}, nil, []string{imgName}, "", true, "", "", false, false, ""), t) se, err := ociremote.SignedEntity(ref, ociremote.WithRemoteOptions(registryClientOpts(ctx)...)) must(err, t) @@ -362,14 +362,14 @@ func TestMultipleSignatures(t *testing.T) { // Now sign the image with one key ko := sign.KeyOpts{KeyRef: priv1, PassFunc: passFunc} - must(sign.SignCmd(ctx, ko, options.RegistryOptions{}, nil, []string{imgName}, "", true, "", false, false, ""), t) + must(sign.SignCmd(ctx, ko, options.RegistryOptions{}, nil, []string{imgName}, "", true, "", "", false, false, ""), t) // Now verify should work with that one, but not the other must(verify(pub1, imgName, true, nil, ""), t) mustErr(verify(pub2, imgName, true, nil, ""), t) // Now sign with the other key too ko.KeyRef = priv2 - must(sign.SignCmd(ctx, ko, options.RegistryOptions{}, nil, []string{imgName}, "", true, "", false, false, ""), t) + must(sign.SignCmd(ctx, ko, options.RegistryOptions{}, nil, []string{imgName}, "", true, "", "", false, false, ""), t) // Now verify should work with both must(verify(pub1, imgName, true, nil, ""), t) @@ -657,7 +657,7 @@ func TestAttachSBOM(t *testing.T) { // Now sign the sbom with one key ko1 := sign.KeyOpts{KeyRef: privKeyPath1, PassFunc: passFunc} - must(sign.SignCmd(ctx, ko1, options.RegistryOptions{}, nil, []string{imgName}, "", true, "", false, false, "sbom"), t) + must(sign.SignCmd(ctx, ko1, options.RegistryOptions{}, nil, []string{imgName}, "", true, "", "", false, false, "sbom"), t) // Now verify should work with that one, but not the other must(verify(pubKeyPath1, imgName, true, nil, "sbom"), t) @@ -695,7 +695,7 @@ func TestTlog(t *testing.T) { PassFunc: passFunc, RekorURL: rekorURL, } - must(sign.SignCmd(ctx, ko, options.RegistryOptions{}, nil, []string{imgName}, "", true, "", false, false, ""), t) + must(sign.SignCmd(ctx, ko, options.RegistryOptions{}, nil, []string{imgName}, "", true, "", "", false, false, ""), t) // Now verify should work! must(verify(pubKeyPath, imgName, true, nil, ""), t) @@ -707,7 +707,7 @@ func TestTlog(t *testing.T) { mustErr(verify(pubKeyPath, imgName, true, nil, ""), t) // Sign again with the tlog env var on - must(sign.SignCmd(ctx, ko, options.RegistryOptions{}, nil, []string{imgName}, "", true, "", false, false, ""), t) + must(sign.SignCmd(ctx, ko, options.RegistryOptions{}, nil, []string{imgName}, "", true, "", "", false, false, ""), t) // And now verify works! must(verify(pubKeyPath, imgName, true, nil, ""), t) }