diff --git a/cmd/cosign/cli/attest/attest_blob.go b/cmd/cosign/cli/attest/attest_blob.go index 8034825f457e..d3cdd1c2a264 100644 --- a/cmd/cosign/cli/attest/attest_blob.go +++ b/cmd/cosign/cli/attest/attest_blob.go @@ -18,6 +18,7 @@ import ( "bytes" "context" "crypto" + "encoding/base64" "encoding/hex" "encoding/json" "fmt" @@ -26,42 +27,70 @@ import ( "path" "path/filepath" "strings" + "time" "github.com/pkg/errors" "github.com/sigstore/cosign/cmd/cosign/cli/options" + "github.com/sigstore/cosign/cmd/cosign/cli/rekor" "github.com/sigstore/cosign/cmd/cosign/cli/sign" + "github.com/sigstore/cosign/internal/pkg/cosign/tsa" "github.com/sigstore/cosign/pkg/cosign" "github.com/sigstore/cosign/pkg/cosign/attestation" + cbundle "github.com/sigstore/cosign/pkg/cosign/bundle" "github.com/sigstore/cosign/pkg/types" + "github.com/sigstore/sigstore/pkg/cryptoutils" "github.com/sigstore/sigstore/pkg/signature" "github.com/sigstore/sigstore/pkg/signature/dsse" signatureoptions "github.com/sigstore/sigstore/pkg/signature/options" + tsaclient "github.com/sigstore/timestamp-authority/pkg/client" ) // nolint type AttestBlobCommand struct { - KeyRef string + options.KeyOpts + options.RootOptions + + CertPath string + CertChainPath string + ArtifactHash string PredicatePath string PredicateType string + TlogUpload bool + Timeout time.Duration + OutputSignature string OutputAttestation string - - PassFunc cosign.PassFunc + OutputCertificate string } // nolint func (c *AttestBlobCommand) Exec(ctx context.Context, artifactPath string) error { - // TODO: Add in experimental keyless mode - if !options.OneOf(c.KeyRef) { + // We can't have both a key and a security key + if options.NOf(c.KeyRef, c.Sk) > 1 { return &options.KeyParseError{} } + if c.Timeout != 0 { + var cancelFn context.CancelFunc + ctx, cancelFn = context.WithTimeout(ctx, c.Timeout) + defer cancelFn() + } + + if c.TSAServerURL != "" && c.RFC3161TimestampPath == "" { + return errors.New("expected an rfc3161-timestamp path when using a TSA server") + } + + sv, err := sign.SignerFromKeyOpts(ctx, c.CertPath, c.CertChainPath, c.KeyOpts) + if err != nil { + return fmt.Errorf("getting signer: %w", err) + } + defer sv.Close() + var artifact []byte var hexDigest string - var err error if c.ArtifactHash == "" { if artifactPath == "-" { @@ -75,17 +104,6 @@ func (c *AttestBlobCommand) Exec(ctx context.Context, artifactPath string) error } } - ko := options.KeyOpts{ - KeyRef: c.KeyRef, - PassFunc: c.PassFunc, - } - - sv, err := sign.SignerFromKeyOpts(ctx, "", "", ko) - if err != nil { - return errors.Wrap(err, "getting signer") - } - defer sv.Close() - if c.ArtifactHash == "" { digest, _, err := signature.ComputeDigestForSigning(bytes.NewReader(artifact), crypto.SHA256, []crypto.Hash{crypto.SHA256, crypto.SHA384}) if err != nil { @@ -126,6 +144,54 @@ func (c *AttestBlobCommand) Exec(ctx context.Context, artifactPath string) error return errors.Wrap(err, "signing") } + signedPayload := cosign.LocalSignedPayload{} + if c.TSAServerURL != "" { + clientTSA, err := tsaclient.GetTimestampClient(c.TSAServerURL) + if err != nil { + return fmt.Errorf("failed to create TSA client: %w", err) + } + rfc3161Timestamp, err := tsa.GetTimestampedSignature(sig, clientTSA) + if err != nil { + return err + } + if err := os.WriteFile(c.RFC3161TimestampPath, rfc3161Timestamp, 0600); err != nil { + return fmt.Errorf("create rfc3161 timestamp file: %w", err) + } + fmt.Printf("RF3161 timestamp bundle wrote in the file %s\n", c.RFC3161TimestampPath) + } + + var rekorBytes []byte + if sign.ShouldUploadToTlog(ctx, c.KeyOpts, nil, c.TlogUpload) { + rekorBytes, err = sv.Bytes(ctx) + if err != nil { + return err + } + rekorClient, err := rekor.NewClient(c.RekorURL) + if err != nil { + return err + } + entry, err := cosign.TLogUploadInTotoAttestation(ctx, rekorClient, sig, rekorBytes) + if err != nil { + return err + } + fmt.Fprintln(os.Stderr, "tlog entry created with index:", *entry.LogIndex) + signedPayload.Bundle = cbundle.EntryToBundle(entry) + } + + if c.BundlePath != "" { + signedPayload.Base64Signature = base64.StdEncoding.EncodeToString(sig) + signedPayload.Cert = base64.StdEncoding.EncodeToString(rekorBytes) + + contents, err := json.Marshal(signedPayload) + if err != nil { + return err + } + if err := os.WriteFile(c.BundlePath, contents, 0600); err != nil { + return fmt.Errorf("create bundle file: %w", err) + } + fmt.Printf("Bundle wrote in the file %s\n", c.BundlePath) + } + if c.OutputSignature != "" { if err := os.WriteFile(c.OutputSignature, sig, 0600); err != nil { return fmt.Errorf("create signature file: %w", err) @@ -142,5 +208,21 @@ func (c *AttestBlobCommand) Exec(ctx context.Context, artifactPath string) error fmt.Fprintf(os.Stderr, "Attestation written in %s\n", c.OutputAttestation) } + if c.OutputCertificate != "" { + signer, err := sv.Bytes(ctx) + if err != nil { + return fmt.Errorf("error getting signer: %w", err) + } + cert, err := cryptoutils.UnmarshalCertificatesFromPEM(signer) + // signer is a certificate + if err == nil && len(cert) == 1 { + bts := signer + if err := os.WriteFile(c.OutputCertificate, bts, 0600); err != nil { + return fmt.Errorf("create certificate file: %w", err) + } + fmt.Printf("Certificate wrote in the file %s\n", c.OutputCertificate) + } + } + return nil } diff --git a/cmd/cosign/cli/attest_blob.go b/cmd/cosign/cli/attest_blob.go index 0eca6c8c810d..3867476d95fa 100644 --- a/cmd/cosign/cli/attest_blob.go +++ b/cmd/cosign/cli/attest_blob.go @@ -16,6 +16,7 @@ package cli import ( "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" ) @@ -46,13 +47,41 @@ func AttestBlob() *cobra.Command { Args: cobra.ExactArgs(1), PersistentPreRun: options.BindViper, 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, + OIDCProvider: o.OIDC.Provider, + SkipConfirmation: o.SkipConfirmation, + TSAServerURL: o.TSAServerURL, + RFC3161TimestampPath: o.RFC3161TimestampPath, + BundlePath: o.BundlePath, + } v := attest.AttestBlobCommand{ - KeyRef: o.Key, + KeyOpts: ko, + CertPath: o.Cert, + CertChainPath: o.CertChain, ArtifactHash: o.Hash, + TlogUpload: o.TlogUpload, PredicateType: o.Predicate.Type, PredicatePath: o.Predicate.Path, OutputSignature: o.OutputSignature, OutputAttestation: o.OutputAttestation, + OutputCertificate: o.OutputCertificate, + Timeout: ro.Timeout, } return v.Exec(cmd.Context(), args[0]) }, diff --git a/cmd/cosign/cli/options/attest_blob.go b/cmd/cosign/cli/options/attest_blob.go index d530d122dffd..3a942760cd12 100644 --- a/cmd/cosign/cli/options/attest_blob.go +++ b/cmd/cosign/cli/options/attest_blob.go @@ -20,12 +20,27 @@ import ( // AttestOptions is the top level wrapper for the attest command. type AttestBlobOptions struct { - Key string - Hash string + Key string + Cert string + CertChain string + + SkipConfirmation bool + TlogUpload bool + TSAServerURL string + RFC3161TimestampPath string + + Hash string + Predicate PredicateLocalOptions + OutputSignature string OutputAttestation string + OutputCertificate string + BundlePath string - Predicate PredicateLocalOptions + Rekor RekorOptions + Fulcio FulcioOptions + OIDC OIDCOptions + SecurityKey SecurityKeyOptions } var _ Interface = (*AttestOptions)(nil) @@ -33,9 +48,25 @@ var _ Interface = (*AttestOptions)(nil) // AddFlags implements Interface func (o *AttestBlobOptions) AddFlags(cmd *cobra.Command) { o.Predicate.AddFlags(cmd) + o.Rekor.AddFlags(cmd) + o.Fulcio.AddFlags(cmd) + o.OIDC.AddFlags(cmd) + o.SecurityKey.AddFlags(cmd) cmd.Flags().StringVar(&o.Key, "key", "", "path to the private key file, KMS URI or Kubernetes Secret") + _ = cmd.Flags().SetAnnotation("key", cobra.BashCompFilenameExt, []string{"key"}) + + cmd.Flags().StringVar(&o.Cert, "certificate", "", + "path to the X.509 certificate in PEM format to include in the OCI Signature") + _ = cmd.Flags().SetAnnotation("certificate", cobra.BashCompFilenameExt, []string{"cert"}) + + cmd.Flags().StringVar(&o.CertChain, "certificate-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().SetAnnotation("certificate-chain", cobra.BashCompFilenameExt, []string{"cert"}) cmd.Flags().StringVar(&o.OutputSignature, "output-signature", "", "write the signature to FILE") @@ -44,6 +75,27 @@ func (o *AttestBlobOptions) AddFlags(cmd *cobra.Command) { cmd.Flags().StringVar(&o.OutputAttestation, "output-attestation", "", "write the attestation to FILE") + cmd.Flags().StringVar(&o.OutputCertificate, "output-certificate", "", + "write the certificate to FILE") + _ = cmd.Flags().SetAnnotation("key", cobra.BashCompFilenameExt, []string{}) + + cmd.Flags().StringVar(&o.BundlePath, "bundle", "", + "write everything required to verify the blob to a FILE") + _ = cmd.Flags().SetAnnotation("bundle", cobra.BashCompFilenameExt, []string{}) + 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") + + cmd.Flags().BoolVarP(&o.SkipConfirmation, "yes", "y", false, + "skip confirmation prompts for non-destructive operations") + + cmd.Flags().BoolVar(&o.TlogUpload, "tlog-upload", false, + "whether or not to upload to the tlog") + + cmd.Flags().StringVar(&o.TSAServerURL, "timestamp-server-url", "", + "url to the Timestamp RFC3161 server, default none") + + cmd.Flags().StringVar(&o.RFC3161TimestampPath, "rfc3161-timestamp-bundle", "", + "write everything required to verify the blob to a FILE") + _ = cmd.Flags().SetAnnotation("rfc3161-timestamp-bundle", cobra.BashCompFilenameExt, []string{}) } diff --git a/doc/cosign_attest-blob.md b/doc/cosign_attest-blob.md index 78b1aee319d1..9f6e125283b3 100644 --- a/doc/cosign_attest-blob.md +++ b/doc/cosign_attest-blob.md @@ -30,13 +30,33 @@ cosign attest-blob [flags] ### Options ``` - --hash string hash of blob in hexadecimal (base16). Used if you want to sign an artifact stored elsewhere and have the hash - -h, --help help for attest-blob - --key string path to the private key file, KMS URI or Kubernetes Secret - --output-attestation string write the attestation to FILE - --output-signature string write the signature to FILE - --predicate string path to the predicate file. - --type string specify a predicate type (slsaprovenance|link|spdx|spdxjson|cyclonedx|vuln|custom) or an URI (default "custom") + --bundle string write everything required to verify the blob to a FILE + --certificate string path to the X.509 certificate in PEM format to include in the OCI Signature + --certificate-chain string 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 + --fulcio-url string [EXPERIMENTAL] address of sigstore PKI server (default "https://fulcio.sigstore.dev") + --hash string hash of blob in hexadecimal (base16). Used if you want to sign an artifact stored elsewhere and have the hash + -h, --help help for attest-blob + --identity-token string [EXPERIMENTAL] identity token to use for certificate from fulcio + --insecure-skip-verify [EXPERIMENTAL] skip verifying fulcio published to the SCT (this should only be used for testing). + --key string path to the private key file, KMS URI or Kubernetes Secret + --oidc-client-id string [EXPERIMENTAL] OIDC client ID for application (default "sigstore") + --oidc-client-secret-file string [EXPERIMENTAL] Path to file containing OIDC client secret for application + --oidc-disable-ambient-providers [EXPERIMENTAL] Disable ambient OIDC providers. When true, ambient credentials will not be read + --oidc-issuer string [EXPERIMENTAL] OIDC provider to be used to issue ID token (default "https://oauth2.sigstore.dev/auth") + --oidc-provider string [EXPERIMENTAL] Specify the provider to get the OIDC token from (Optional). If unset, all options will be tried. Options include: [spiffe, google, github, filesystem] + --oidc-redirect-url string [EXPERIMENTAL] OIDC redirect URL (Optional). The default oidc-redirect-url is 'http://localhost:0/auth/callback'. + --output-attestation string write the attestation to FILE + --output-certificate string write the certificate to FILE + --output-signature string write the signature to FILE + --predicate string path to the predicate file. + --rekor-url string [EXPERIMENTAL] address of rekor STL server (default "https://rekor.sigstore.dev") + --rfc3161-timestamp-bundle string write everything required to verify the blob to a FILE + --sk whether to use a hardware security key + --slot string security key slot to use for generated key (default: signature) (authentication|signature|card-authentication|key-management) + --timestamp-server-url string url to the Timestamp RFC3161 server, default none + --tlog-upload whether or not to upload to the tlog + --type string specify a predicate type (slsaprovenance|link|spdx|spdxjson|cyclonedx|vuln|custom) or an URI (default "custom") + -y, --yes skip confirmation prompts for non-destructive operations ``` ### Options inherited from parent commands