diff --git a/cmd/cosign/cli/attest/attest_blob.go b/cmd/cosign/cli/attest/attest_blob.go new file mode 100644 index 00000000000..a5dd8ad43b8 --- /dev/null +++ b/cmd/cosign/cli/attest/attest_blob.go @@ -0,0 +1,153 @@ +package attest + +import ( + "bytes" + "context" + "crypto" + "encoding/hex" + "encoding/json" + "fmt" + "io" + "os" + "path" + "path/filepath" + "strings" + "time" + + "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" + "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" + "github.com/sigstore/sigstore/pkg/signature/dsse" + signatureoptions "github.com/sigstore/sigstore/pkg/signature/options" +) + +func AttestBlobCmd(ctx context.Context, ko options.KeyOpts, artifactPath string, artifactHash string, certPath string, certChainPath string, noUpload bool, predicatePath string, force bool, predicateType string, replace bool, timeout time.Duration) 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{} + } + } + + var artifact []byte + var hexDigest string + var err error + + if artifactHash == "" { + if artifactPath == "-" { + artifact, err = io.ReadAll(os.Stdin) + } else { + fmt.Fprintln(os.Stderr, "Using payload from:", artifactPath) + artifact, err = os.ReadFile(filepath.Clean(artifactPath)) + } + if err != nil { + return err + } else if timeout != 0 { + var cancelFn context.CancelFunc + ctx, cancelFn = context.WithTimeout(ctx, timeout) + defer cancelFn() + } + } + + sv, err := sign.SignerFromKeyOpts(ctx, certPath, certChainPath, ko) + if err != nil { + return errors.Wrap(err, "getting signer") + } + defer sv.Close() + //pub, err := sv.PublicKey() + if err != nil { + return err + } + /*pem, err := cryptoutils.MarshalPublicKeyToPEM(pub) + if err != nil { + return errors.Wrap(err, "key to pem") + }*/ + + if timeout != 0 { + var cancelFn context.CancelFunc + ctx, cancelFn = context.WithTimeout(ctx, timeout) + defer cancelFn() + } + + if artifactHash == "" { + digest, _, err := signature.ComputeDigestForSigning(bytes.NewReader(artifact), crypto.SHA256, []crypto.Hash{crypto.SHA256, crypto.SHA384}) + if err != nil { + return err + } + hexDigest = strings.ToLower(hex.EncodeToString(digest)) + } else { + hexDigest = artifactHash + } + wrapped := dsse.WrapSigner(sv, types.IntotoPayloadType) + + fmt.Fprintln(os.Stderr, "Using payload from:", predicatePath) + predicate, err := os.Open(predicatePath) + if err != nil { + return err + } + defer predicate.Close() + + base := path.Base(artifactPath) + + sh, err := attestation.GenerateStatement(attestation.GenerateOpts{ + Predicate: predicate, + Type: predicateType, + Digest: hexDigest, + Repo: base, + }) + 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 noUpload { + fmt.Println(string(signedPayload)) + return nil + } + + opts := []static.Option{static.WithLayerMediaType(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 options.EnableExperimental() { + fmt.Println("Uploading to Rekor") + /*r, err := rc.GetRekorClient(ko.RekorURL) + if err != nil { + return err + }*/ + _, err := uploadToTlog(ctx, sv, ko.RekorURL, func(r *client.Rekor, b []byte) (*models.LogEntryAnon, error) { + return cosign.TLogUploadInTotoAttestation(ctx, r, signedPayload, b) + }) + if err != nil { + return err + } + /*l, err := cosign.TLogUploadInTotoAttestation(ctx, r, signedPayload, pem) + if err != nil { + return err + }*/ + + //fmt.Fprintln(os.Stderr, "Log id:", *bundle.LogIndex) + } + return err +} diff --git a/cmd/cosign/cli/attestblob.go b/cmd/cosign/cli/attestblob.go new file mode 100644 index 00000000000..f3c5407b2fb --- /dev/null +++ b/cmd/cosign/cli/attestblob.go @@ -0,0 +1,68 @@ +package cli + +import ( + "github.com/pkg/errors" + "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/spf13/cobra" +) + +func AttestBlob() *cobra.Command { + o := &options.AttestBlobOptions{} + + cmd := &cobra.Command{ + Use: "attest-blob", + Short: "Attest the supplied blob.", + Example: ` cosign attest-blob --key | [--predicate ] [--a key=value] [--no-upload=true|false] [--f] [--r] + + # attach an attestation to a blob Google sign-in (experimental) + COSIGN_EXPERIMENTAL=1 cosign attest-blob --timeout 90s --predicate --type + + # attach an attestation to a blob with a local key pair file + cosign attest-blob --predicate --type --key cosign.key + + # attach an attestation to a blob with a key pair stored in Azure Key Vault + cosign attest-blob --predicate --type --key azurekms://[VAULT_NAME][VAULT_URI]/[KEY] + + # attach an attestation to a blob with a key pair stored in AWS KMS + cosign attest-blob --predicate --type --key awskms://[ENDPOINT]/[ID/ALIAS/ARN] + + # attach an attestation to a blob with a key pair stored in Google Cloud KMS + cosign attest-blob --predicate --type --key gcpkms://projects/[PROJECT]/locations/global/keyRings/[KEYRING]/cryptoKeys/[KEY]/versions/[VERSION] + + # attach an attestation to a blob with a key pair stored in Hashicorp Vault + cosign attest-blob --predicate --type --key hashivault://[KEY] `, + + Args: cobra.MinimumNArgs(1), + RunE: func(cmd *cobra.Command, args []string) error { + oidcClientSecret, err := o.OIDC.ClientSecret() + if err != nil { + return err + } + ko := options.KeyOpts{ + KeyRef: o.Key, + PassFunc: generate.GetPass, + Sk: o.SecurityKey.Use, + Slot: o.SecurityKey.Slot, + FulcioURL: o.Fulcio.URL, + IDToken: o.Fulcio.IdentityToken, + InsecureSkipFulcioVerify: o.Fulcio.InsecureSkipFulcioVerify, + RekorURL: o.Rekor.URL, + OIDCIssuer: o.OIDC.Issuer, + OIDCClientID: o.OIDC.ClientID, + OIDCClientSecret: oidcClientSecret, + OIDCRedirectURL: o.OIDC.RedirectURL, + } + for _, artifact := range args { + if err := attest.AttestBlobCmd(cmd.Context(), ko, artifact, o.Hash, o.Cert, o.CertChain, o.NoUpload, + o.Predicate.Path, o.Force, o.Predicate.Type, o.Replace, ro.Timeout); err != nil { + return errors.Wrapf(err, "attesting %s", artifact) + } + } + return nil + }, + } + o.AddFlags(cmd) + return cmd +} diff --git a/cmd/cosign/cli/commands.go b/cmd/cosign/cli/commands.go index 2b483e71f4e..9d58c29b442 100644 --- a/cmd/cosign/cli/commands.go +++ b/cmd/cosign/cli/commands.go @@ -70,6 +70,7 @@ func New() *cobra.Command { // Add sub-commands. cmd.AddCommand(Attach()) cmd.AddCommand(Attest()) + cmd.AddCommand(AttestBlob()) cmd.AddCommand(Clean()) cmd.AddCommand(Tree()) cmd.AddCommand(Completion()) diff --git a/cmd/cosign/cli/options/attestblob.go b/cmd/cosign/cli/options/attestblob.go new file mode 100644 index 00000000000..2f8c784ffd2 --- /dev/null +++ b/cmd/cosign/cli/options/attestblob.go @@ -0,0 +1,79 @@ +// +// 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 ( + "time" + + "github.com/spf13/cobra" +) + +// AttestOptions is the top level wrapper for the attest command. +type AttestBlobOptions struct { + Key string + Cert string + CertChain string + NoUpload bool + Force bool + Recursive bool + Replace bool + Timeout time.Duration + Hash string + + Rekor RekorOptions + Fulcio FulcioOptions + OIDC OIDCOptions + SecurityKey SecurityKeyOptions + Predicate PredicateLocalOptions +} + +var _ Interface = (*AttestOptions)(nil) + +// AddFlags implements Interface +func (o *AttestBlobOptions) AddFlags(cmd *cobra.Command) { + o.SecurityKey.AddFlags(cmd) + o.Predicate.AddFlags(cmd) + o.Fulcio.AddFlags(cmd) + o.OIDC.AddFlags(cmd) + o.Rekor.AddFlags(cmd) + + 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().StringVar(&o.CertChain, "cert-chain", "", + "path to a list of CA X.509 certificates in PEM format which will be needed "+ + "when building the certificate chain for the signing certificate. "+ + "Must start with the parent intermediate CA certificate of the "+ + "signing certificate and end with the root certificate. Included in the OCI Signature") + + cmd.Flags().BoolVar(&o.NoUpload, "no-upload", false, + "do not upload the generated attestation") + + cmd.Flags().BoolVarP(&o.Force, "force", "f", false, + "skip warnings and confirmations") + + cmd.Flags().BoolVarP(&o.Replace, "replace", "", false, + "") + + cmd.Flags().DurationVar(&o.Timeout, "timeout", time.Second*30, + "HTTP Timeout defaults to 30 seconds") + + cmd.Flags().StringVar(&o.Hash, "hash", "", + "hash of blob in hexadecimal (base16). Used if you want to sign an artifact stored elsewhere and have the hash") +}