Skip to content

Commit

Permalink
feat: sign --output-certificate and verify --cert (#1095)
Browse files Browse the repository at this point in the history
* feat: sign --output-certificate and verify --cert

Signed-off-by: Carlos A Becker <caarlos0@gmail.com>
Co-authored-by: Matt Moore <mattomata@gmail.com>

* fix: merge conflict

Signed-off-by: Carlos A Becker <caarlos0@gmail.com>

Co-authored-by: Matt Moore <mattomata@gmail.com>
  • Loading branch information
caarlos0 and mattmoor authored Dec 2, 2021
1 parent 034e946 commit 9076d71
Show file tree
Hide file tree
Showing 14 changed files with 118 additions and 64 deletions.
25 changes: 17 additions & 8 deletions cmd/cosign/cli/options/sign.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,14 +21,16 @@ import (

// SignOptions is the top level wrapper for the sign command.
type SignOptions struct {
Key string
Cert string
Upload bool
Output string
PayloadPath string
Force bool
Recursive bool
Attachment string
Key string
Cert string
Upload bool
Output string // deprecated: TODO remove when the output flag is fully deprecated
OutputSignature string // TODO: this should be the root output file arg.
OutputCertificate string
PayloadPath string
Force bool
Recursive bool
Attachment string

Rekor RekorOptions
Fulcio FulcioOptions
Expand Down Expand Up @@ -58,9 +60,16 @@ func (o *SignOptions) AddFlags(cmd *cobra.Command) {
cmd.Flags().BoolVar(&o.Upload, "upload", true,
"whether to upload the signature")

// TODO: remove when output flag is fully deprecated
cmd.Flags().StringVar(&o.Output, "output", "",
"write the signature to FILE")

cmd.Flags().StringVar(&o.OutputSignature, "output-signature", "",
"write the signature to FILE")

cmd.Flags().StringVar(&o.OutputCertificate, "output-certificate", "",
"write the certificate to FILE")

cmd.Flags().StringVar(&o.PayloadPath, "payload", "",
"path to a payload file to use rather than generating one")

Expand Down
4 changes: 4 additions & 0 deletions cmd/cosign/cli/options/verify.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import (
// VerifyOptions is the top level wrapper for the `verify` command.
type VerifyOptions struct {
Key string
Cert string
CertEmail string // TODO: merge into fulcio option as read mode?
CheckClaims bool
Attachment string
Expand Down Expand Up @@ -49,6 +50,9 @@ func (o *VerifyOptions) AddFlags(cmd *cobra.Command) {
cmd.Flags().StringVar(&o.Key, "key", "",
"path to the public key file, KMS URI or Kubernetes Secret")

cmd.Flags().StringVar(&o.Cert, "cert", "",
"path to the public certificate")

cmd.Flags().StringVar(&o.CertEmail, "cert-email", "",
"the email expected in a valid fulcio cert")

Expand Down
4 changes: 3 additions & 1 deletion cmd/cosign/cli/sign.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import (

"github.com/pkg/errors"
"github.com/spf13/cobra"
"github.com/spf13/viper"

"github.com/sigstore/cosign/cmd/cosign/cli/generate"
"github.com/sigstore/cosign/cmd/cosign/cli/options"
Expand All @@ -28,6 +29,7 @@ import (

func Sign() *cobra.Command {
o := &options.SignOptions{}
viper.RegisterAlias("output", "output-signature")

cmd := &cobra.Command{
Use: "sign",
Expand Down Expand Up @@ -89,7 +91,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.Output, 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.OutputSignature, o.OutputCertificate, o.PayloadPath, o.Force, o.Recursive, o.Attachment); err != nil {
if o.Attachment == "" {
return errors.Wrapf(err, "signing %v", args)
}
Expand Down
38 changes: 32 additions & 6 deletions cmd/cosign/cli/sign/sign.go
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@ import (
rekorClient "github.com/sigstore/rekor/pkg/client"
"github.com/sigstore/sigstore/pkg/cryptoutils"
"github.com/sigstore/sigstore/pkg/signature"
signatureoptions "github.com/sigstore/sigstore/pkg/signature/options"
sigPayload "github.com/sigstore/sigstore/pkg/signature/payload"
)

Expand Down Expand Up @@ -97,7 +98,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, output string, payloadPath string, force bool, recursive bool, attachment string) error {
imgs []string, certPath string, upload bool, outputSignature, outputCertificate string, payloadPath string, force bool, recursive bool, attachment string) error {
if options.EnableExperimental() {
if options.NOf(ko.KeyRef, ko.Sk) > 1 {
return &options.KeyParseError{}
Expand Down Expand Up @@ -156,7 +157,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, output, force, dd, sv, se)
err = signDigest(ctx, digest, staticPayload, ko, regOpts, annotations, upload, outputSignature, outputCertificate, force, dd, sv, se)
if err != nil {
return errors.Wrap(err, "signing digest")
}
Expand All @@ -176,7 +177,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, output, force, dd, sv, se)
err = signDigest(ctx, digest, staticPayload, ko, regOpts, annotations, upload, outputSignature, outputCertificate, force, dd, sv, se)
if err != nil {
return errors.Wrap(err, "signing digest")
}
Expand All @@ -190,7 +191,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, output string, force bool,
regOpts options.RegistryOptions, annotations map[string]interface{}, upload bool, outputSignature, outputCertificate string, force bool,
dd mutate.DupeDetector, sv *SignerVerifier, se oci.SignedEntity) error {
var err error
// The payload can be passed to skip generation.
Expand Down Expand Up @@ -225,8 +226,8 @@ func signDigest(ctx context.Context, digest name.Digest, payload []byte, ko KeyO
return err
}

if output != "" {
out, err := os.Create(output)
if outputSignature != "" {
out, err := os.Create(outputSignature)
if err != nil {
return errors.Wrap(err, "create signature file")
}
Expand Down Expand Up @@ -260,6 +261,18 @@ func signDigest(ctx context.Context, digest name.Digest, payload []byte, ko KeyO
return err
}

if outputCertificate != "" {
rekorBytes, err := sv.Bytes(ctx)
if err != nil {
return err
}

if err := os.WriteFile(outputCertificate, rekorBytes, 0600); err != nil {
return err
}
// TODO: maybe accept a --b64 flag as well?
}

return nil
}

Expand Down Expand Up @@ -432,3 +445,16 @@ func (c *SignerVerifier) Close() {
c.close()
}
}

func (c *SignerVerifier) Bytes(ctx context.Context) ([]byte, error) {
if c.Cert != nil {
fmt.Fprintf(os.Stderr, "using ephemeral certificate:\n%s\n", string(c.Cert))
return c.Cert, nil
}

pemBytes, err := sigs.PublicKeyPem(c, signatureoptions.WithContext(ctx))
if err != nil {
return nil, err
}
return pemBytes, nil
}
14 changes: 3 additions & 11 deletions cmd/cosign/cli/sign/sign_blob.go
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,6 @@ import (

"github.com/sigstore/cosign/cmd/cosign/cli/options"
"github.com/sigstore/cosign/pkg/cosign"
"github.com/sigstore/cosign/pkg/signature"
rekorClient "github.com/sigstore/rekor/pkg/client"
signatureoptions "github.com/sigstore/sigstore/pkg/signature/options"
)
Expand Down Expand Up @@ -84,16 +83,9 @@ func SignBlobCmd(ctx context.Context, ko KeyOpts, regOpts options.RegistryOption
}

if options.EnableExperimental() {
// TODO: Refactor with sign.go
if sv.Cert != nil {
fmt.Fprintf(os.Stderr, "signing with ephemeral certificate:\n%s\n", string(sv.Cert))
rekorBytes = sv.Cert
} else {
pemBytes, err := signature.PublicKeyPem(sv, signatureoptions.WithContext(ctx))
if err != nil {
return nil, err
}
rekorBytes = pemBytes
rekorBytes, err := sv.Bytes(ctx)
if err != nil {
return nil, err
}
rekorClient, err := rekorClient.GetRekorClient(ko.RekorURL)
if err != nil {
Expand Down
2 changes: 1 addition & 1 deletion cmd/cosign/cli/sign/sign_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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")
}
Expand Down
1 change: 1 addition & 0 deletions cmd/cosign/cli/verify.go
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,7 @@ against the transparency log.`,
RegistryOptions: o.Registry,
CheckClaims: o.CheckClaims,
KeyRef: o.Key,
CertRef: o.Cert,
CertEmail: o.CertEmail,
Sk: o.SecurityKey.Use,
Slot: o.SecurityKey.Slot,
Expand Down
41 changes: 38 additions & 3 deletions cmd/cosign/cli/verify/verify.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@ package verify
import (
"context"
"crypto"
"crypto/ecdsa"
"encoding/base64"
"encoding/json"
"flag"
"fmt"
Expand All @@ -34,6 +36,7 @@ import (
"github.com/sigstore/cosign/pkg/cosign/pkcs11key"
"github.com/sigstore/cosign/pkg/oci"
sigs "github.com/sigstore/cosign/pkg/signature"
"github.com/sigstore/sigstore/pkg/cryptoutils"
"github.com/sigstore/sigstore/pkg/signature"
"github.com/sigstore/sigstore/pkg/signature/payload"
)
Expand All @@ -44,6 +47,7 @@ type VerifyCommand struct {
options.RegistryOptions
CheckClaims bool
KeyRef string
CertRef string
CertEmail string
Sk bool
Slot string
Expand Down Expand Up @@ -73,7 +77,7 @@ func (c *VerifyCommand) Exec(ctx context.Context, images []string) (err error) {
c.HashAlgorithm = crypto.SHA256
}

if !options.OneOf(c.KeyRef, c.Sk) && !options.EnableExperimental() {
if !options.OneOf(c.KeyRef, c.CertRef, c.Sk) && !options.EnableExperimental() {
return &options.KeyParseError{}
}
ociremoteOpts, err := c.ClientOpts(ctx)
Expand All @@ -94,10 +98,12 @@ func (c *VerifyCommand) Exec(ctx context.Context, images []string) (err error) {
co.RootCerts = fulcio.GetRoots()
}
keyRef := c.KeyRef
certRef := c.CertRef

// Keys are optional!
var pubKey signature.Verifier
if keyRef != "" {
switch {
case keyRef != "":
pubKey, err = sigs.PublicKeyFromKeyRefWithHashAlgo(ctx, keyRef, c.HashAlgorithm)
if err != nil {
return errors.Wrap(err, "loading public key")
Expand All @@ -106,7 +112,7 @@ func (c *VerifyCommand) Exec(ctx context.Context, images []string) (err error) {
if ok {
defer pkcs11Key.Close()
}
} else if c.Sk {
case c.Sk:
sk, err := pivkey.GetKeyWithSlot(c.Slot)
if err != nil {
return errors.Wrap(err, "opening piv token")
Expand All @@ -116,6 +122,11 @@ func (c *VerifyCommand) Exec(ctx context.Context, images []string) (err error) {
if err != nil {
return errors.Wrap(err, "initializing piv token verifier")
}
case certRef != "":
pubKey, err = loadCertFromFile(c.CertRef)
if err != nil {
return err
}
}
co.SigVerifier = pubKey

Expand Down Expand Up @@ -225,3 +236,27 @@ func PrintVerification(imgRef string, verified []oci.Signature, output string) {
fmt.Printf("\n%s\n", string(b))
}
}

func loadCertFromFile(path string) (*signature.ECDSAVerifier, error) {
pems, err := os.ReadFile(path)
if err != nil {
return nil, err
}

var out []byte
out, err = base64.StdEncoding.DecodeString(string(pems))
if err != nil {
// not a base64
out = pems
}

certs, err := cryptoutils.UnmarshalCertificatesFromPEM(out)
if err != nil {
return nil, err
}
if len(certs) == 0 {
return nil, errors.New("no certs found in pem file")
}
cert := certs[0]
return signature.LoadECDSAVerifier(cert.PublicKey.(*ecdsa.PublicKey), crypto.SHA256)
}
22 changes: 1 addition & 21 deletions cmd/cosign/cli/verify/verify_blob.go
Original file line number Diff line number Diff line change
Expand Up @@ -113,27 +113,7 @@ func VerifyBlobCmd(ctx context.Context, ko sign.KeyOpts, certRef, sigRef, blobRe
return errors.Wrap(err, "loading public key from token")
}
case certRef != "":
pems, err := os.ReadFile(certRef)
if err != nil {
return err
}

var out []byte
out, err = base64.StdEncoding.DecodeString(string(pems))
if err != nil {
// not a base64
out = pems
}

certs, err := cryptoutils.UnmarshalCertificatesFromPEM(out)
if err != nil {
return err
}
if len(certs) == 0 {
return errors.New("no certs found in pem file")
}
cert = certs[0]
pubKey, err = sigstoresigs.LoadECDSAVerifier(cert.PublicKey.(*ecdsa.PublicKey), crypto.SHA256)
pubKey, err = loadCertFromFile(certRef)
if err != nil {
return err
}
Expand Down
1 change: 1 addition & 0 deletions doc/cosign_dockerfile_verify.md

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions doc/cosign_manifest_verify.md

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 2 additions & 0 deletions doc/cosign_sign.md

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions doc/cosign_verify.md

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading

0 comments on commit 9076d71

Please sign in to comment.