From 849a87a43f45ba16c2e7efd4b2c46d96c990e8a6 Mon Sep 17 00:00:00 2001 From: Scott Nichols Date: Fri, 24 Sep 2021 14:13:51 -0700 Subject: [PATCH] Migrating attest to cobra, moving public-key impl to folder (#781) Signed-off-by: Scott Nichols --- cmd/cosign/cli/attest.go | 201 +++------------ cmd/cosign/cli/attest/attest.go | 228 ++++++++++++++++++ cmd/cosign/cli/commands.go | 8 +- cmd/cosign/cli/options/attest.go | 61 +++++ cmd/cosign/cli/options/predicate.go | 35 +++ cmd/cosign/cli/public_key.go | 111 +-------- cmd/cosign/cli/publickey/public_key.go | 127 ++++++++++ .../cli/{ => publickey}/public_key_test.go | 2 +- cmd/cosign/main.go | 5 +- test/e2e_test.go | 8 +- 10 files changed, 497 insertions(+), 289 deletions(-) create mode 100644 cmd/cosign/cli/attest/attest.go create mode 100644 cmd/cosign/cli/options/attest.go create mode 100644 cmd/cosign/cli/options/predicate.go create mode 100644 cmd/cosign/cli/publickey/public_key.go rename cmd/cosign/cli/{ => publickey}/public_key_test.go (99%) diff --git a/cmd/cosign/cli/attest.go b/cmd/cosign/cli/attest.go index 4fcaec2be8c..cc1ae4b7b12 100644 --- a/cmd/cosign/cli/attest.go +++ b/cmd/cosign/cli/attest.go @@ -16,215 +16,68 @@ package cli import ( - "bytes" "context" - _ "crypto/sha256" // for `crypto.SHA256` - "encoding/base64" - "encoding/json" "flag" - "fmt" - "os" - "github.com/google/go-containerregistry/pkg/name" - v1 "github.com/google/go-containerregistry/pkg/v1" - "github.com/in-toto/in-toto-golang/in_toto" - "github.com/peterbourgon/ff/v3/ffcli" "github.com/pkg/errors" + "github.com/spf13/cobra" + "github.com/sigstore/cosign/cmd/cosign/cli/attest" "github.com/sigstore/cosign/cmd/cosign/cli/generate" "github.com/sigstore/cosign/cmd/cosign/cli/options" "github.com/sigstore/cosign/cmd/cosign/cli/sign" - "github.com/sigstore/cosign/pkg/cosign" - "github.com/sigstore/cosign/pkg/cosign/attestation" - cremote "github.com/sigstore/cosign/pkg/cosign/remote" - "github.com/sigstore/cosign/pkg/oci/mutate" - ociremote "github.com/sigstore/cosign/pkg/oci/remote" - "github.com/sigstore/cosign/pkg/oci/static" - "github.com/sigstore/cosign/pkg/types" - "github.com/sigstore/rekor/pkg/generated/client" - "github.com/sigstore/rekor/pkg/generated/models" - "github.com/sigstore/sigstore/pkg/signature/dsse" - signatureoptions "github.com/sigstore/sigstore/pkg/signature/options" ) -func Attest() *ffcli.Command { - var ( - flagset = flag.NewFlagSet("cosign attest", flag.ExitOnError) - key = flagset.String("key", "", "path to the private key file, KMS URI or Kubernetes Secret") - cert = flagset.String("cert", "", "Path to the x509 certificate to include in the Signature") - upload = flagset.Bool("upload", true, "whether to upload the signature") - sk = flagset.Bool("sk", false, "whether to use a hardware security key") - slot = flagset.String("slot", "", "security key slot to use for generated key (default: signature) (authentication|signature|card-authentication|key-management)") - predicatePath = flagset.String("predicate", "", "path to the predicate file.") - force = flagset.Bool("f", false, "skip warnings and confirmations") - idToken = flagset.String("identity-token", "", "[EXPERIMENTAL] identity token to use for certificate from fulcio") - predicateType = flagset.String("type", "custom", "specify predicate type (default: custom) (slsaprovenance|link|spdx)") - regOpts options.RegistryOpts - ) - options.ApplyRegistryFlags(®Opts, flagset) - return &ffcli.Command{ - Name: "attest", - ShortUsage: "cosign attest -key | [-predicate ] [-a key=value] [-upload=true|false] [-f] [-r] ", - ShortHelp: `Attest the supplied container image.`, - LongHelp: `Attest the supplied container image. +func addAttest(topLevel *cobra.Command) { + o := &options.AttestOptions{} -EXAMPLES + cmd := &cobra.Command{ + Use: "attest", + Short: "Attest the supplied container image.\ncosign attest --key | [--predicate ] [--a key=value] [--upload=true|false] [--f] [--r] ", + Long: "Attest the supplied container image.", + Example: ` # attach an attestation to a container image Google sign-in (experimental) - COSIGN_EXPERIMENTAL=1 cosign attest -predicate -type + COSIGN_EXPERIMENTAL=1 cosign attest --predicate --type # attach an attestation to a container image with a local key pair file - cosign attest -predicate -type -key cosign.key + cosign attest --predicate --type --key cosign.key # attach an attestation to a container image with a key pair stored in Azure Key Vault - cosign attest -predicate -type -key azurekms://[VAULT_NAME][VAULT_URI]/[KEY] + cosign attest --predicate --type --key azurekms://[VAULT_NAME][VAULT_URI]/[KEY] # attach an attestation to a container image with a key pair stored in AWS KMS - cosign attest -predicate -type -key awskms://[ENDPOINT]/[ID/ALIAS/ARN] + cosign attest --predicate --type --key awskms://[ENDPOINT]/[ID/ALIAS/ARN] # attach an attestation to a container image with a key pair stored in Google Cloud KMS - cosign attest -predicate -type -key gcpkms://projects/[PROJECT]/locations/global/keyRings/[KEYRING]/cryptoKeys/[KEY]/versions/[VERSION] + cosign attest --predicate --type --key gcpkms://projects/[PROJECT]/locations/global/keyRings/[KEYRING]/cryptoKeys/[KEY]/versions/[VERSION] # attach an attestation to a container image with a key pair stored in Hashicorp Vault - cosign attest -predicate -type -key hashivault://[KEY] + cosign attest --predicate --type --key hashivault://[KEY] # attach an attestation to a container image which does not fully support OCI media types - COSIGN_DOCKER_MEDIA_TYPES=1 cosign attest -predicate -type -key cosign.key legacy-registry.example.com/my/image - `, - FlagSet: flagset, - Exec: func(ctx context.Context, args []string) error { + COSIGN_DOCKER_MEDIA_TYPES=1 cosign attest --predicate --type --key cosign.key legacy-registry.example.com/my/image`, + + RunE: func(cmd *cobra.Command, args []string) error { if len(args) == 0 { return flag.ErrHelp } ko := sign.KeyOpts{ - KeyRef: *key, - PassFunc: generate.GetPass, - Sk: *sk, - Slot: *slot, - IDToken: *idToken, + KeyRef: o.Key, + PassFunc: generate.GetPass, + Sk: o.SecurityKey.Use, + Slot: o.SecurityKey.Slot, + IDToken: o.Fulcio.IdentityToken, + FulcioURL: o.Fulcio.URL, } for _, img := range args { - if err := AttestCmd(ctx, ko, regOpts, img, *cert, *upload, *predicatePath, *force, *predicateType); err != nil { + if err := attest.AttestCmd(context.Background(), ko, o.RegistryOpts, img, o.Cert, o.Upload, o.Predicate.Path, o.Force, o.Predicate.Type); err != nil { return errors.Wrapf(err, "signing %s", img) } } return nil }, } -} - -const ( - predicateCustom = "custom" - predicateSlsa = "slsaprovenance" - predicateSpdx = "spdx" - predicateLink = "link" -) - -var predicateTypeMap = map[string]string{ - predicateCustom: attestation.CosignCustomProvenanceV01, - predicateSlsa: in_toto.PredicateSLSAProvenanceV01, - predicateSpdx: in_toto.PredicateSPDX, - predicateLink: in_toto.PredicateLinkV1, -} - -func AttestCmd(ctx context.Context, ko sign.KeyOpts, regOpts options.RegistryOpts, imageRef string, certPath string, - upload bool, predicatePath string, force bool, predicateType string) error { - // A key file or token is required unless we're in experimental mode! - if options.EnableExperimental() { - if options.NOf(ko.KeyRef, ko.Sk) > 1 { - return &options.KeyParseError{} - } - } else { - if !options.OneOf(ko.KeyRef, ko.Sk) { - return &options.KeyParseError{} - } - } - - predicateURI, ok := predicateTypeMap[predicateType] - if !ok { - return fmt.Errorf("invalid predicate type: %s", predicateType) - } - - ref, err := name.ParseReference(imageRef) - if err != nil { - return errors.Wrap(err, "parsing reference") - } - digest, err := ociremote.ResolveDigest(ref, regOpts.ClientOpts(ctx)...) - if err != nil { - return err - } - h, _ := v1.NewHash(digest.Identifier()) - // Overwrite "ref" with a digest to avoid a race where we use a tag - // multiple times, and it potentially points to different things at - // each access. - ref = digest // nolint - - sv, err := sign.SignerFromKeyOpts(ctx, certPath, ko) - if err != nil { - return errors.Wrap(err, "getting signer") - } - wrapped := dsse.WrapSigner(sv, predicateURI) - dd := cremote.NewDupeDetector(sv) - - fmt.Fprintln(os.Stderr, "Using payload from:", predicatePath) - sh, err := attestation.GenerateStatement(attestation.GenerateOpts{ - Path: predicatePath, - Type: predicateType, - Digest: h.Hex, - Repo: digest.Repository.String(), - }) - if err != nil { - return err - } - - payload, err := json.Marshal(sh) - if err != nil { - return err - } - signedPayload, err := wrapped.SignMessage(bytes.NewReader(payload), signatureoptions.WithContext(ctx)) - if err != nil { - return errors.Wrap(err, "signing") - } - - if !upload { - fmt.Println(base64.StdEncoding.EncodeToString(signedPayload)) - return nil - } - - opts := []static.Option{static.WithMediaType(types.DssePayloadType)} - if sv.Cert != nil { - opts = append(opts, static.WithCertChain(sv.Cert, sv.Chain)) - } - - // Check whether we should be uploading to the transparency log - if uploadTLog, err := sign.ShouldUploadToTlog(digest, force, ko.RekorURL); err != nil { - return err - } else if uploadTLog { - bundle, err := sign.UploadToTlog(ctx, sv, ko.RekorURL, func(r *client.Rekor, b []byte) (*models.LogEntryAnon, error) { - return cosign.TLogUploadInTotoAttestation(r, signedPayload, b) - }) - if err != nil { - return err - } - opts = append(opts, static.WithBundle(bundle)) - } - - sig, err := static.NewAttestation(signedPayload, opts...) - if err != nil { - return err - } - - se, err := ociremote.SignedEntity(digest, regOpts.ClientOpts(ctx)...) - if err != nil { - return err - } - - // Attach the attestation to the entity. - newSE, err := mutate.AttachAttestationToEntity(se, sig, mutate.WithDupeDetector(dd)) - if err != nil { - return err - } - - // Publish the attestations associated with this entity - return ociremote.WriteAttestations(digest.Repository, newSE, regOpts.ClientOpts(ctx)...) + options.AddAttestOptions(cmd, o) + topLevel.AddCommand(cmd) } diff --git a/cmd/cosign/cli/attest/attest.go b/cmd/cosign/cli/attest/attest.go new file mode 100644 index 00000000000..ae07ac51adf --- /dev/null +++ b/cmd/cosign/cli/attest/attest.go @@ -0,0 +1,228 @@ +// +// Copyright 2021 The Sigstore Authors. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package attest + +import ( + "bytes" + "context" + _ "crypto/sha256" // for `crypto.SHA256` + "encoding/base64" + "encoding/json" + "flag" + "fmt" + "os" + + "github.com/google/go-containerregistry/pkg/name" + v1 "github.com/google/go-containerregistry/pkg/v1" + "github.com/in-toto/in-toto-golang/in_toto" + "github.com/peterbourgon/ff/v3/ffcli" + "github.com/pkg/errors" + + "github.com/sigstore/cosign/cmd/cosign/cli/options" + "github.com/sigstore/cosign/cmd/cosign/cli/sign" + "github.com/sigstore/cosign/pkg/cosign" + "github.com/sigstore/cosign/pkg/cosign/attestation" + cremote "github.com/sigstore/cosign/pkg/cosign/remote" + "github.com/sigstore/cosign/pkg/oci/mutate" + ociremote "github.com/sigstore/cosign/pkg/oci/remote" + "github.com/sigstore/cosign/pkg/oci/static" + "github.com/sigstore/cosign/pkg/types" + "github.com/sigstore/rekor/pkg/generated/client" + "github.com/sigstore/rekor/pkg/generated/models" + "github.com/sigstore/sigstore/pkg/signature/dsse" + signatureoptions "github.com/sigstore/sigstore/pkg/signature/options" +) + +// Attest subcommand for ffcli. +// Deprecated: this will be deleted when the migration from ffcli to cobra is done. +// nolint +func Attest() *ffcli.Command { + var ( + flagset = flag.NewFlagSet("cosign attest", flag.ExitOnError) + key = flagset.String("key", "", "path to the private key file, KMS URI or Kubernetes Secret") + cert = flagset.String("cert", "", "Path to the x509 certificate to include in the Signature") + upload = flagset.Bool("upload", true, "whether to upload the signature") + sk = flagset.Bool("sk", false, "whether to use a hardware security key") + slot = flagset.String("slot", "", "security key slot to use for generated key (default: signature) (authentication|signature|card-authentication|key-management)") + predicatePath = flagset.String("predicate", "", "path to the predicate file.") + force = flagset.Bool("f", false, "skip warnings and confirmations") + idToken = flagset.String("identity-token", "", "[EXPERIMENTAL] identity token to use for certificate from fulcio") + predicateType = flagset.String("type", "custom", "specify predicate type (default: custom) (slsaprovenance|link|spdx)") + regOpts options.RegistryOpts + ) + options.ApplyRegistryFlags(®Opts, flagset) + return &ffcli.Command{ + Name: "attest", + ShortUsage: "cosign attest -key | [-predicate ] [-a key=value] [-upload=true|false] [-f] [-r] ", + ShortHelp: `Attest the supplied container image.`, + LongHelp: `Attest the supplied container image. + +EXAMPLES + # attach an attestation to a container image Google sign-in (experimental) + COSIGN_EXPERIMENTAL=1 cosign attest -predicate -type + + # attach an attestation to a container image with a local key pair file + cosign attest -predicate -type -key cosign.key + + # attach an attestation to a container image with a key pair stored in Azure Key Vault + cosign attest -predicate -type -key azurekms://[VAULT_NAME][VAULT_URI]/[KEY] + + # attach an attestation to a container image with a key pair stored in AWS KMS + cosign attest -predicate -type -key awskms://[ENDPOINT]/[ID/ALIAS/ARN] + + # attach an attestation to a container image with a key pair stored in Google Cloud KMS + cosign attest -predicate -type -key gcpkms://projects/[PROJECT]/locations/global/keyRings/[KEYRING]/cryptoKeys/[KEY]/versions/[VERSION] + + # attach an attestation to a container image with a key pair stored in Hashicorp Vault + cosign attest -predicate -type -key hashivault://[KEY] + + # attach an attestation to a container image which does not fully support OCI media types + COSIGN_DOCKER_MEDIA_TYPES=1 cosign attest -predicate -type -key cosign.key legacy-registry.example.com/my/image + `, + FlagSet: flagset, + Exec: func(ctx context.Context, args []string) error { + _ = flagset + _ = key + _ = cert + _ = upload + _ = sk + _ = slot + _ = predicatePath + _ = force + _ = idToken + _ = predicateType + _ = regOpts + panic("this command is now implemented in cobra.") + }, + } +} + +const ( + predicateCustom = "custom" + predicateSlsa = "slsaprovenance" + predicateSpdx = "spdx" + predicateLink = "link" +) + +var predicateTypeMap = map[string]string{ + predicateCustom: attestation.CosignCustomProvenanceV01, + predicateSlsa: in_toto.PredicateSLSAProvenanceV01, + predicateSpdx: in_toto.PredicateSPDX, + predicateLink: in_toto.PredicateLinkV1, +} + +//nolint +func AttestCmd(ctx context.Context, ko sign.KeyOpts, regOpts options.RegistryOpts, imageRef string, certPath string, + upload bool, predicatePath string, force bool, predicateType string) error { + // A key file or token is required unless we're in experimental mode! + if options.EnableExperimental() { + if options.NOf(ko.KeyRef, ko.Sk) > 1 { + return &options.KeyParseError{} + } + } else { + if !options.OneOf(ko.KeyRef, ko.Sk) { + return &options.KeyParseError{} + } + } + + predicateURI, ok := predicateTypeMap[predicateType] + if !ok { + return fmt.Errorf("invalid predicate type: %s", predicateType) + } + + ref, err := name.ParseReference(imageRef) + if err != nil { + return errors.Wrap(err, "parsing reference") + } + digest, err := ociremote.ResolveDigest(ref, regOpts.ClientOpts(ctx)...) + if err != nil { + return err + } + h, _ := v1.NewHash(digest.Identifier()) + // Overwrite "ref" with a digest to avoid a race where we use a tag + // multiple times, and it potentially points to different things at + // each access. + ref = digest // nolint + + sv, err := sign.SignerFromKeyOpts(ctx, certPath, ko) + if err != nil { + return errors.Wrap(err, "getting signer") + } + wrapped := dsse.WrapSigner(sv, predicateURI) + dd := cremote.NewDupeDetector(sv) + + fmt.Fprintln(os.Stderr, "Using payload from:", predicatePath) + sh, err := attestation.GenerateStatement(attestation.GenerateOpts{ + Path: predicatePath, + Type: predicateType, + Digest: h.Hex, + Repo: digest.Repository.String(), + }) + if err != nil { + return err + } + + payload, err := json.Marshal(sh) + if err != nil { + return err + } + signedPayload, err := wrapped.SignMessage(bytes.NewReader(payload), signatureoptions.WithContext(ctx)) + if err != nil { + return errors.Wrap(err, "signing") + } + + if !upload { + fmt.Println(base64.StdEncoding.EncodeToString(signedPayload)) + return nil + } + + opts := []static.Option{static.WithMediaType(types.DssePayloadType)} + if sv.Cert != nil { + opts = append(opts, static.WithCertChain(sv.Cert, sv.Chain)) + } + + // Check whether we should be uploading to the transparency log + if uploadTLog, err := sign.ShouldUploadToTlog(digest, force, ko.RekorURL); err != nil { + return err + } else if uploadTLog { + bundle, err := sign.UploadToTlog(ctx, sv, ko.RekorURL, func(r *client.Rekor, b []byte) (*models.LogEntryAnon, error) { + return cosign.TLogUploadInTotoAttestation(r, signedPayload, b) + }) + if err != nil { + return err + } + opts = append(opts, static.WithBundle(bundle)) + } + + sig, err := static.NewAttestation(signedPayload, opts...) + if err != nil { + return err + } + + se, err := ociremote.SignedEntity(digest, regOpts.ClientOpts(ctx)...) + if err != nil { + return err + } + + // Attach the attestation to the entity. + newSE, err := mutate.AttachAttestationToEntity(se, sig, mutate.WithDupeDetector(dd)) + if err != nil { + return err + } + + // Publish the attestations associated with this entity + return ociremote.WriteAttestations(digest.Repository, newSE, regOpts.ClientOpts(ctx)...) +} diff --git a/cmd/cosign/cli/commands.go b/cmd/cosign/cli/commands.go index fbba0e173c4..4c61885a2ed 100644 --- a/cmd/cosign/cli/commands.go +++ b/cmd/cosign/cli/commands.go @@ -19,6 +19,7 @@ import ( "context" "flag" "fmt" + "os" "github.com/google/go-containerregistry/pkg/logs" @@ -27,12 +28,14 @@ import ( "github.com/spf13/cobra" "github.com/sigstore/cosign/cmd/cosign/cli/attach" + "github.com/sigstore/cosign/cmd/cosign/cli/attest" "github.com/sigstore/cosign/cmd/cosign/cli/dockerfile" "github.com/sigstore/cosign/cmd/cosign/cli/download" "github.com/sigstore/cosign/cmd/cosign/cli/generate" "github.com/sigstore/cosign/cmd/cosign/cli/manifest" "github.com/sigstore/cosign/cmd/cosign/cli/options" "github.com/sigstore/cosign/cmd/cosign/cli/pivcli" + "github.com/sigstore/cosign/cmd/cosign/cli/publickey" "github.com/sigstore/cosign/cmd/cosign/cli/sign" "github.com/sigstore/cosign/cmd/cosign/cli/upload" "github.com/sigstore/cosign/cmd/cosign/cli/verify" @@ -50,12 +53,12 @@ func New() *cobra.Command { ShortUsage: "cosign [flags] ", Subcommands: []*ffcli.Command{ // Key Management - PublicKey(), + publickey.PublicKey(), generate.GenerateKeyPair(), // Signing sign.Sign(), sign.SignBlob(), - Attest(), + attest.Attest(), generate.Generate(), verify.Verify(), verify.VerifyAttestation(), @@ -120,6 +123,7 @@ func New() *cobra.Command { addSign(cmd) addSignBlob(cmd) addGenerateKeyPair(cmd) + addAttest(cmd) return cmd } diff --git a/cmd/cosign/cli/options/attest.go b/cmd/cosign/cli/options/attest.go new file mode 100644 index 00000000000..5066e05a65d --- /dev/null +++ b/cmd/cosign/cli/options/attest.go @@ -0,0 +1,61 @@ +// +// Copyright 2021 The Sigstore Authors. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package options + +import ( + "github.com/spf13/cobra" +) + +// AttestOptions is the top level wrapper for the attest command. +type AttestOptions struct { + Key string + Cert string + Upload bool + Force bool + Recursive bool + + Fulcio FulcioOptions + SecurityKey SecurityKeyOptions + Predicate PredicateOptions + RegistryOpts RegistryOpts +} + +// AddAttestOptions adds the sign command options to cmd. +func AddAttestOptions(cmd *cobra.Command, o *AttestOptions) { + cmd.Flags().StringVar(&o.Key, "key", "", + "path to the private key file, KMS URI or Kubernetes Secret") + + cmd.Flags().StringVar(&o.Cert, "cert", "", + "path to the x509 certificate to include in the Signature") + + cmd.Flags().BoolVar(&o.Upload, "upload", true, + "whether to upload the signature") + + cmd.Flags().BoolVarP(&o.Force, "force", "f", false, + "skip warnings and confirmations") + + cmd.Flags().BoolVarP(&o.Recursive, "recursive", "r", false, + "if a multi-arch image is specified, additionally sign each discrete image") + + cmd.Flags().BoolVar(&o.RegistryOpts.AllowInsecure, "allow-insecure-registry", false, + "whether to allow insecure connections to registries. Don't use this for anything but testing") + + AddSecurityKeyOptions(cmd, &o.SecurityKey) + + AddPredicateOptions(cmd, &o.Predicate) + + AddFulcioOptions(cmd, &o.Fulcio) +} diff --git a/cmd/cosign/cli/options/predicate.go b/cmd/cosign/cli/options/predicate.go new file mode 100644 index 00000000000..aae7103d755 --- /dev/null +++ b/cmd/cosign/cli/options/predicate.go @@ -0,0 +1,35 @@ +// +// Copyright 2021 The Sigstore Authors. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package options + +import ( + "github.com/spf13/cobra" +) + +// PredicateOptions is the wrapper for predicate related options. +type PredicateOptions struct { + Path string + Type string +} + +// AddPredicateOptions adds the predicate related options to cmd. +func AddPredicateOptions(cmd *cobra.Command, o *PredicateOptions) { + cmd.Flags().StringVar(&o.Path, "predicate", "", + "path to the predicate file.") + + cmd.Flags().StringVar(&o.Path, "type", "custom", + "specify predicate type (default: custom) (slsaprovenance|link|spdx)") +} diff --git a/cmd/cosign/cli/public_key.go b/cmd/cosign/cli/public_key.go index 135c137c4f4..b90845dc1cf 100644 --- a/cmd/cosign/cli/public_key.go +++ b/cmd/cosign/cli/public_key.go @@ -17,76 +17,15 @@ package cli import ( "context" - "flag" - "fmt" - "io" "os" - "github.com/peterbourgon/ff/v3/ffcli" - "github.com/pkg/errors" "github.com/spf13/cobra" "github.com/sigstore/cosign/cmd/cosign/cli/generate" "github.com/sigstore/cosign/cmd/cosign/cli/options" - "github.com/sigstore/cosign/pkg/cosign" - "github.com/sigstore/cosign/pkg/cosign/pivkey" - sigs "github.com/sigstore/cosign/pkg/signature" - "github.com/sigstore/sigstore/pkg/signature" - signatureoptions "github.com/sigstore/sigstore/pkg/signature/options" + "github.com/sigstore/cosign/cmd/cosign/cli/publickey" ) -type NamedWriter struct { - Name string - io.Writer -} - -// PublicKey subcommand for ffcli. -// Deprecated: this will be deleted when the migration from ffcli to cobra is done. -func PublicKey() *ffcli.Command { - var ( - flagset = flag.NewFlagSet("cosign public-key", flag.ExitOnError) - key = flagset.String("key", "", "path to the private key file, public key URL, or KMS URI") - sk = flagset.Bool("sk", false, "whether to use a hardware security key") - slot = flagset.String("slot", "", "security key slot to use for generated key (default: signature) (authentication|signature|card-authentication|key-management)") - outFile = flagset.String("outfile", "", "file to write public key") - ) - - return &ffcli.Command{ - Name: "public-key", - ShortUsage: "cosign public-key gets a public key from the key-pair", - ShortHelp: "Gets a public key from the key-pair", - LongHelp: `Gets a public key from the key-pair and -writes to a specified file. By default, it will write to standard out. - -EXAMPLES - # extract public key from private key to a specified out file. - cosign public-key -key -outfile - - # extract public key from URL. - cosign public-key -key https://host.for/ -outfile - - # extract public key from Azure Key Vault - cosign public-key -key azurekms://[VAULT_NAME][VAULT_URI]/[KEY] - - # extract public key from AWS KMS - cosign public-key -key awskms://[ENDPOINT]/[ID/ALIAS/ARN] - - # extract public key from Google Cloud KMS - cosign public-key -key gcpkms://projects/[PROJECT]/locations/global/keyRings/[KEYRING]/cryptoKeys/[KEY] - - # extract public key from Hashicorp Vault KMS - cosign public-key -key hashivault://[KEY]`, - FlagSet: flagset, - Exec: func(ctx context.Context, args []string) error { - _ = key - _ = sk - _ = slot - _ = outFile - panic("this command is now implemented in cobra.") - }, - } -} - func addPublicKey(topLevel *cobra.Command) { o := &options.PublicKeyOptions{} @@ -118,7 +57,7 @@ func addPublicKey(topLevel *cobra.Command) { return &options.KeyParseError{} } - writer := NamedWriter{Name: "", Writer: nil} + writer := publickey.NamedWriter{Name: "", Writer: nil} var f *os.File // Open output file for public key if specified. if o.OutFile != "" { @@ -133,57 +72,15 @@ func addPublicKey(topLevel *cobra.Command) { } else { writer.Writer = os.Stdout } - pk := Pkopts{ + pk := publickey.Pkopts{ KeyRef: o.Key, Sk: o.SecurityKey.Use, Slot: o.SecurityKey.Slot, } - return GetPublicKey(context.Background(), pk, writer, generate.GetPass) + return publickey.GetPublicKey(context.Background(), pk, writer, generate.GetPass) }, } options.AddPublicKeyOptions(cmd, o) topLevel.AddCommand(cmd) } - -type Pkopts struct { - KeyRef string - Sk bool - Slot string -} - -func GetPublicKey(ctx context.Context, opts Pkopts, writer NamedWriter, pf cosign.PassFunc) error { - var k signature.PublicKeyProvider - switch { - case opts.KeyRef != "": - s, err := sigs.SignerFromKeyRef(ctx, opts.KeyRef, pf) - if err != nil { - return err - } - k = s - case opts.Sk: - sk, err := pivkey.GetKeyWithSlot(opts.Slot) - if err != nil { - return errors.Wrap(err, "opening piv token") - } - defer sk.Close() - pk, err := sk.Verifier() - if err != nil { - return errors.Wrap(err, "initializing piv token verifier") - } - k = pk - } - - pemBytes, err := sigs.PublicKeyPem(k, signatureoptions.WithContext(ctx)) - if err != nil { - return err - } - - if _, err := writer.Write(pemBytes); err != nil { - return err - } - if writer.Name != "" { - fmt.Fprintln(os.Stderr, "Public key written to ", writer.Name) - } - return nil -} diff --git a/cmd/cosign/cli/publickey/public_key.go b/cmd/cosign/cli/publickey/public_key.go new file mode 100644 index 00000000000..93640a03b5d --- /dev/null +++ b/cmd/cosign/cli/publickey/public_key.go @@ -0,0 +1,127 @@ +// +// Copyright 2021 The Sigstore Authors. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package publickey + +import ( + "context" + "flag" + "fmt" + "io" + "os" + + "github.com/peterbourgon/ff/v3/ffcli" + "github.com/pkg/errors" + + "github.com/sigstore/cosign/pkg/cosign" + "github.com/sigstore/cosign/pkg/cosign/pivkey" + sigs "github.com/sigstore/cosign/pkg/signature" + "github.com/sigstore/sigstore/pkg/signature" + signatureoptions "github.com/sigstore/sigstore/pkg/signature/options" +) + +type NamedWriter struct { + Name string + io.Writer +} + +// PublicKey subcommand for ffcli. +// Deprecated: this will be deleted when the migration from ffcli to cobra is done. +func PublicKey() *ffcli.Command { + var ( + flagset = flag.NewFlagSet("cosign public-key", flag.ExitOnError) + key = flagset.String("key", "", "path to the private key file, public key URL, or KMS URI") + sk = flagset.Bool("sk", false, "whether to use a hardware security key") + slot = flagset.String("slot", "", "security key slot to use for generated key (default: signature) (authentication|signature|card-authentication|key-management)") + outFile = flagset.String("outfile", "", "file to write public key") + ) + + return &ffcli.Command{ + Name: "public-key", + ShortUsage: "cosign public-key gets a public key from the key-pair", + ShortHelp: "Gets a public key from the key-pair", + LongHelp: `Gets a public key from the key-pair and +writes to a specified file. By default, it will write to standard out. + +EXAMPLES + # extract public key from private key to a specified out file. + cosign public-key -key -outfile + + # extract public key from URL. + cosign public-key -key https://host.for/ -outfile + + # extract public key from Azure Key Vault + cosign public-key -key azurekms://[VAULT_NAME][VAULT_URI]/[KEY] + + # extract public key from AWS KMS + cosign public-key -key awskms://[ENDPOINT]/[ID/ALIAS/ARN] + + # extract public key from Google Cloud KMS + cosign public-key -key gcpkms://projects/[PROJECT]/locations/global/keyRings/[KEYRING]/cryptoKeys/[KEY] + + # extract public key from Hashicorp Vault KMS + cosign public-key -key hashivault://[KEY]`, + FlagSet: flagset, + Exec: func(ctx context.Context, args []string) error { + _ = key + _ = sk + _ = slot + _ = outFile + panic("this command is now implemented in cobra.") + }, + } +} + +type Pkopts struct { + KeyRef string + Sk bool + Slot string +} + +func GetPublicKey(ctx context.Context, opts Pkopts, writer NamedWriter, pf cosign.PassFunc) error { + var k signature.PublicKeyProvider + switch { + case opts.KeyRef != "": + s, err := sigs.SignerFromKeyRef(ctx, opts.KeyRef, pf) + if err != nil { + return err + } + k = s + case opts.Sk: + sk, err := pivkey.GetKeyWithSlot(opts.Slot) + if err != nil { + return errors.Wrap(err, "opening piv token") + } + defer sk.Close() + pk, err := sk.Verifier() + if err != nil { + return errors.Wrap(err, "initializing piv token verifier") + } + k = pk + } + + pemBytes, err := sigs.PublicKeyPem(k, signatureoptions.WithContext(ctx)) + if err != nil { + return err + } + + if _, err := writer.Write(pemBytes); err != nil { + return err + } + if writer.Name != "" { + fmt.Fprintln(os.Stderr, "Public key written to ", writer.Name) + } + return nil +} diff --git a/cmd/cosign/cli/public_key_test.go b/cmd/cosign/cli/publickey/public_key_test.go similarity index 99% rename from cmd/cosign/cli/public_key_test.go rename to cmd/cosign/cli/publickey/public_key_test.go index 7bae9002855..8da0d2c6859 100644 --- a/cmd/cosign/cli/public_key_test.go +++ b/cmd/cosign/cli/publickey/public_key_test.go @@ -13,7 +13,7 @@ // See the License for the specific language governing permissions and // limitations under the License. -package cli +package publickey import ( "bytes" diff --git a/cmd/cosign/main.go b/cmd/cosign/main.go index 335024be4d0..86739869ebb 100644 --- a/cmd/cosign/main.go +++ b/cmd/cosign/main.go @@ -49,8 +49,9 @@ func main() { // escape the remaining args to let them be passed to cobra. if len(os.Args) > 1 { switch os.Args[1] { - case "public-key", "generate-key-pair": - case "sign", "sign-blob": + case "public-key", "generate-key-pair", + "sign", "sign-blob", + "attest": // cobra. default: // ffcli diff --git a/test/e2e_test.go b/test/e2e_test.go index 42f2c706038..2c50cc261cc 100644 --- a/test/e2e_test.go +++ b/test/e2e_test.go @@ -42,9 +42,11 @@ import ( "github.com/sigstore/cosign/cmd/cosign/cli" "github.com/sigstore/cosign/cmd/cosign/cli/attach" + "github.com/sigstore/cosign/cmd/cosign/cli/attest" "github.com/sigstore/cosign/cmd/cosign/cli/download" "github.com/sigstore/cosign/cmd/cosign/cli/generate" "github.com/sigstore/cosign/cmd/cosign/cli/options" + "github.com/sigstore/cosign/cmd/cosign/cli/publickey" "github.com/sigstore/cosign/cmd/cosign/cli/sign" "github.com/sigstore/cosign/cmd/cosign/cli/upload" cliverify "github.com/sigstore/cosign/cmd/cosign/cli/verify" @@ -178,7 +180,7 @@ func TestAttestVerify(t *testing.T) { // Now attest the image ko := sign.KeyOpts{KeyRef: privKeyPath, PassFunc: passFunc} - must(cli.AttestCmd(ctx, ko, options.RegistryOpts{}, imgName, "", true, ap, false, "custom"), t) + must(attest.AttestCmd(ctx, ko, options.RegistryOpts{}, imgName, "", true, ap, false, "custom"), t) // Now verify and download should work! must(verifyAttestation.Exec(ctx, []string{imgName}), t) @@ -701,10 +703,10 @@ func TestGetPublicKeyCustomOut(t *testing.T) { outWriter, err := os.OpenFile(outPath, os.O_WRONLY|os.O_CREATE, 0600) must(err, t) - pk := cli.Pkopts{ + pk := publickey.Pkopts{ KeyRef: privKeyPath, } - must(cli.GetPublicKey(ctx, pk, cli.NamedWriter{Name: outPath, Writer: outWriter}, passFunc), t) + must(publickey.GetPublicKey(ctx, pk, publickey.NamedWriter{Name: outPath, Writer: outWriter}, passFunc), t) output, err := ioutil.ReadFile(outPath) must(err, t)