diff --git a/cmd/cosign/cli/attest/attest.go b/cmd/cosign/cli/attest/attest.go index 6cf7c0fcbdd..4f17f0ae578 100644 --- a/cmd/cosign/cli/attest/attest.go +++ b/cmd/cosign/cli/attest/attest.go @@ -16,26 +16,16 @@ package attest import ( - "bytes" "context" _ "crypto/sha256" // for `crypto.SHA256` "encoding/json" "fmt" - "os" "time" - "github.com/google/go-containerregistry/pkg/name" v1 "github.com/google/go-containerregistry/pkg/v1" "github.com/sigstore/cosign/v3/cmd/cosign/cli/options" - "github.com/sigstore/cosign/v3/cmd/cosign/cli/rekor" - cosign_sign "github.com/sigstore/cosign/v3/cmd/cosign/cli/sign" - "github.com/sigstore/cosign/v3/internal/auth" - "github.com/sigstore/cosign/v3/internal/key" - "github.com/sigstore/cosign/v3/internal/pkg/cosign/tsa" - tsaclient "github.com/sigstore/cosign/v3/internal/pkg/cosign/tsa/client" - "github.com/sigstore/cosign/v3/internal/ui" - "github.com/sigstore/cosign/v3/pkg/cosign" + "github.com/sigstore/cosign/v3/cmd/cosign/cli/signcommon" "github.com/sigstore/cosign/v3/pkg/cosign/attestation" cbundle "github.com/sigstore/cosign/v3/pkg/cosign/bundle" cremote "github.com/sigstore/cosign/v3/pkg/cosign/remote" @@ -43,33 +33,8 @@ import ( ociremote "github.com/sigstore/cosign/v3/pkg/oci/remote" "github.com/sigstore/cosign/v3/pkg/oci/static" "github.com/sigstore/cosign/v3/pkg/types" - "github.com/sigstore/rekor/pkg/generated/client" - "github.com/sigstore/rekor/pkg/generated/models" - "github.com/sigstore/sigstore-go/pkg/sign" - "github.com/sigstore/sigstore/pkg/signature/dsse" - signatureoptions "github.com/sigstore/sigstore/pkg/signature/options" ) -type tlogUploadFn func(*client.Rekor, []byte) (*models.LogEntryAnon, error) - -func uploadToTlog(ctx context.Context, sv *cosign_sign.SignerVerifier, rekorURL string, upload tlogUploadFn) (*models.LogEntryAnon, error) { - rekorBytes, err := sv.Bytes(ctx) - if err != nil { - return nil, err - } - - rekorClient, err := rekor.NewClient(rekorURL) - if err != nil { - return nil, err - } - entry, err := upload(rekorClient, rekorBytes) - if err != nil { - return nil, err - } - fmt.Fprintln(os.Stderr, "tlog entry created with index:", *entry.LogIndex) - return entry, nil -} - // nolint type AttestCommand struct { options.KeyOpts @@ -106,14 +71,10 @@ func (c *AttestCommand) Exec(ctx context.Context, imageRef string) error { if err != nil { return err } - ref, err := name.ParseReference(imageRef, c.NameOptions()...) + ref, err := signcommon.ParseOCIReference(ctx, imageRef, c.NameOptions()...) if err != nil { return fmt.Errorf("parsing reference: %w", err) } - if _, ok := ref.(name.Digest); !ok { - msg := fmt.Sprintf(ui.TagReferenceMessage, imageRef) - ui.Warnf(ctx, msg) - } if c.Timeout != 0 { var cancelFn context.CancelFunc @@ -157,88 +118,19 @@ func (c *AttestCommand) Exec(ctx context.Context, imageRef string) error { } if c.SigningConfig != nil { - var keypair sign.Keypair - var ephemeralKeypair bool - var idToken string - var sv *cosign_sign.SignerVerifier - var err error - - if c.Sk || c.Slot != "" || c.KeyRef != "" || c.CertPath != "" { - sv, _, err = cosign_sign.SignerFromKeyOpts(ctx, c.CertPath, c.CertChainPath, c.KeyOpts) - if err != nil { - return fmt.Errorf("getting signer: %w", err) - } - keypair, err = key.NewSignerVerifierKeypair(sv, c.DefaultLoadOptions) - if err != nil { - return fmt.Errorf("creating signerverifier keypair: %w", err) - } - } else { - keypair, err = sign.NewEphemeralKeypair(nil) - if err != nil { - return fmt.Errorf("generating keypair: %w", err) - } - ephemeralKeypair = true - } - defer func() { - if sv != nil { - sv.Close() - } - }() - - if ephemeralKeypair || c.IssueCertificateForExistingKey { - idToken, err = auth.RetrieveIDToken(ctx, auth.IDTokenConfig{ - TokenOrPath: c.IDToken, - DisableProviders: c.OIDCDisableProviders, - Provider: c.OIDCProvider, - AuthFlow: c.FulcioAuthFlow, - SkipConfirm: c.SkipConfirmation, - OIDCServices: c.SigningConfig.OIDCProviderURLs(), - ClientID: c.OIDCClientID, - ClientSecret: c.OIDCClientSecret, - RedirectURL: c.OIDCRedirectURL, - }) - if err != nil { - return fmt.Errorf("retrieving ID token: %w", err) - } - } - - content := &sign.DSSEData{ - Data: payload, - PayloadType: "application/vnd.in-toto+json", - } - bundle, err := cbundle.SignData(ctx, content, keypair, idToken, c.SigningConfig, c.TrustedMaterial) - if err != nil { - return fmt.Errorf("signing bundle: %w", err) - } - - ociremoteOpts, err := c.RegistryOptions.ClientOpts(ctx) - if err != nil { - return err - } - return ociremote.WriteAttestationNewBundleFormat(digest, bundle, types.CosignSignPredicateType, ociremoteOpts...) + return signcommon.WriteNewBundleWithSigningConfig(ctx, c.KeyOpts, c.CertPath, c.CertChainPath, payload, digest, types.CosignSignPredicateType, "", c.SigningConfig, c.TrustedMaterial, ociremoteOpts...) } - sv, genKey, err := cosign_sign.SignerFromKeyOpts(ctx, c.CertPath, c.CertChainPath, c.KeyOpts) + bundleComponents, closeSV, err := signcommon.GetBundleComponents(ctx, c.CertPath, c.CertChainPath, c.KeyOpts, c.NoUpload, c.TlogUpload, payload, digest, c.RekorEntryType) if err != nil { - return fmt.Errorf("getting signer: %w", err) - } - if genKey || c.IssueCertificateForExistingKey { - sv, err = cosign_sign.KeylessSigner(ctx, c.KeyOpts, sv) - if err != nil { - return fmt.Errorf("getting Fulcio signer: %w", err) - } + return fmt.Errorf("getting bundle components: %w", err) } - defer sv.Close() - wrapped := dsse.WrapSigner(sv, types.IntotoPayloadType) - dd := cremote.NewDupeDetector(sv) + defer closeSV() - signedPayload, err := wrapped.SignMessage(bytes.NewReader(payload), signatureoptions.WithContext(ctx)) - if err != nil { - return fmt.Errorf("signing: %w", err) - } + sv := bundleComponents.SV if c.NoUpload { - fmt.Println(string(signedPayload)) + fmt.Println(string(bundleComponents.SignedPayload)) return nil } @@ -246,39 +138,9 @@ func (c *AttestCommand) Exec(ctx context.Context, imageRef string) error { if sv.Cert != nil { opts = append(opts, static.WithCertChain(sv.Cert, sv.Chain)) } - var timestampBytes []byte - var tsaPayload []byte - if c.KeyOpts.TSAServerURL != "" { - // We need to decide what signature to send to the timestamp authority. - // - // Historically, cosign sent `signedPayload`, which is the entire JSON DSSE - // Envelope. However, when sigstore clients are verifying a bundle they - // will use the DSSE Sig field, so we choose what signature to send to - // the timestamp authority based on our output format. - if c.KeyOpts.NewBundleFormat { - tsaPayload, err = cosign.GetDSSESigBytes(signedPayload) - if err != nil { - return err - } - } else { - tsaPayload = signedPayload - } - tc := tsaclient.NewTSAClient(c.KeyOpts.TSAServerURL) - if c.KeyOpts.TSAClientCert != "" { - tc = tsaclient.NewTSAClientMTLS(c.KeyOpts.TSAServerURL, - c.KeyOpts.TSAClientCACert, - c.KeyOpts.TSAClientCert, - c.KeyOpts.TSAClientKey, - c.KeyOpts.TSAServerName, - ) - } - timestampBytes, err = tsa.GetTimestampedSignature(tsaPayload, tc) - if err != nil { - return err - } - bundle := cbundle.TimestampToRFC3161Timestamp(timestampBytes) - opts = append(opts, static.WithRFC3161Timestamp(bundle)) + if bundleComponents.RFC3161Timestamp != nil { + opts = append(opts, static.WithRFC3161Timestamp(bundleComponents.RFC3161Timestamp)) } predicateType, err := options.ParsePredicateType(c.PredicateType) @@ -292,52 +154,19 @@ func (c *AttestCommand) Exec(ctx context.Context, imageRef string) error { // Add predicateType as manifest annotation opts = append(opts, static.WithAnnotations(predicateTypeAnnotation)) - // Check whether we should be uploading to the transparency log - shouldUpload, err := cosign_sign.ShouldUploadToTlog(ctx, c.KeyOpts, digest, c.TlogUpload) - if err != nil { - return fmt.Errorf("should upload to tlog: %w", err) - } - var rekorEntry *models.LogEntryAnon - if shouldUpload { - rekorEntry, err = uploadToTlog(ctx, sv, c.RekorURL, func(r *client.Rekor, b []byte) (*models.LogEntryAnon, error) { - if c.RekorEntryType == "intoto" { - return cosign.TLogUploadInTotoAttestation(ctx, r, signedPayload, b) - } else { - return cosign.TLogUploadDSSEEnvelope(ctx, r, signedPayload, b) - } - - }) - if err != nil { - return err - } - opts = append(opts, static.WithBundle(cbundle.EntryToBundle(rekorEntry))) - } - - sig, err := static.NewAttestation(signedPayload, opts...) - if err != nil { - return err + if bundleComponents.RekorEntry != nil { + opts = append(opts, static.WithBundle(cbundle.EntryToBundle(bundleComponents.RekorEntry))) } if c.KeyOpts.NewBundleFormat { - signerBytes, err := sv.Bytes(ctx) - if err != nil { - return err - } - pubKey, err := sv.PublicKey() - if err != nil { - return err - } - bundleBytes, err := cbundle.MakeNewBundle(pubKey, rekorEntry, payload, signedPayload, signerBytes, timestampBytes) - if err != nil { - return err - } - return ociremote.WriteAttestationNewBundleFormat(digest, bundleBytes, predicateType, ociremoteOpts...) + return signcommon.WriteBundle(sv, bundleComponents.RekorEntry, payload, bundleComponents.SignedPayload, bundleComponents.SignerBytes, bundleComponents.TimestampBytes, digest, predicateType, ociremoteOpts...) } // We don't actually need to access the remote entity to attach things to it // so we use a placeholder here. se := ociremote.SignedUnknown(digest, ociremoteOpts...) + dd := cremote.NewDupeDetector(sv) signOpts := []mutate.SignOption{ mutate.WithDupeDetector(dd), mutate.WithRecordCreationTimestamp(c.RecordCreationTimestamp), @@ -348,6 +177,11 @@ func (c *AttestCommand) Exec(ctx context.Context, imageRef string) error { signOpts = append(signOpts, mutate.WithReplaceOp(ro)) } + sig, err := static.NewAttestation(bundleComponents.SignedPayload, opts...) + if err != nil { + return err + } + // Attach the attestation to the entity. newSE, err := mutate.AttachAttestationToEntity(se, sig, signOpts...) if err != nil { diff --git a/cmd/cosign/cli/attest/attest_blob.go b/cmd/cosign/cli/attest/attest_blob.go index cc1c76aee91..4f0b9748658 100644 --- a/cmd/cosign/cli/attest/attest_blob.go +++ b/cmd/cosign/cli/attest/attest_blob.go @@ -21,7 +21,6 @@ import ( "encoding/base64" "encoding/hex" "encoding/json" - "errors" "fmt" "io" "os" @@ -30,25 +29,15 @@ import ( "strings" "time" + "github.com/google/go-containerregistry/pkg/name" intotov1 "github.com/in-toto/attestation/go/v1" "github.com/sigstore/cosign/v3/cmd/cosign/cli/options" - "github.com/sigstore/cosign/v3/cmd/cosign/cli/rekor" - cosign_sign "github.com/sigstore/cosign/v3/cmd/cosign/cli/sign" - "github.com/sigstore/cosign/v3/internal/auth" - "github.com/sigstore/cosign/v3/internal/key" - "github.com/sigstore/cosign/v3/internal/pkg/cosign/tsa" - tsaclient "github.com/sigstore/cosign/v3/internal/pkg/cosign/tsa/client" - "github.com/sigstore/cosign/v3/internal/ui" + "github.com/sigstore/cosign/v3/cmd/cosign/cli/signcommon" "github.com/sigstore/cosign/v3/pkg/cosign" "github.com/sigstore/cosign/v3/pkg/cosign/attestation" cbundle "github.com/sigstore/cosign/v3/pkg/cosign/bundle" - "github.com/sigstore/cosign/v3/pkg/types" - "github.com/sigstore/rekor/pkg/generated/models" - "github.com/sigstore/sigstore-go/pkg/sign" "github.com/sigstore/sigstore/pkg/cryptoutils" "github.com/sigstore/sigstore/pkg/signature" - sigstoredsse "github.com/sigstore/sigstore/pkg/signature/dsse" - signatureoptions "github.com/sigstore/sigstore/pkg/signature/options" ) // nolint @@ -94,10 +83,6 @@ func (c *AttestBlobCommand) Exec(ctx context.Context, artifactPath string) error defer cancelFn() } - if c.TSAServerURL != "" && c.RFC3161TimestampPath == "" && !c.NewBundleFormat { - return errors.New("expected either new bundle or an rfc3161-timestamp path when using a TSA server") - } - base := path.Base(artifactPath) var payload []byte @@ -158,161 +143,21 @@ func (c *AttestBlobCommand) Exec(ctx context.Context, artifactPath string) error } if c.SigningConfig != nil { - var keypair sign.Keypair - var ephemeralKeypair bool - var idToken string - var sv *cosign_sign.SignerVerifier - var err error - - if c.Sk || c.Slot != "" || c.KeyRef != "" || c.CertPath != "" { - sv, _, err = cosign_sign.SignerFromKeyOpts(ctx, c.CertPath, c.CertChainPath, c.KeyOpts) - if err != nil { - return fmt.Errorf("getting signer: %w", err) - } - keypair, err = key.NewSignerVerifierKeypair(sv, c.DefaultLoadOptions) - if err != nil { - return fmt.Errorf("creating signerverifier keypair: %w", err) - } - } else { - keypair, err = sign.NewEphemeralKeypair(nil) - if err != nil { - return fmt.Errorf("generating keypair: %w", err) - } - ephemeralKeypair = true - } - defer func() { - if sv != nil { - sv.Close() - } - }() - - if ephemeralKeypair || c.IssueCertificateForExistingKey { - idToken, err = auth.RetrieveIDToken(ctx, auth.IDTokenConfig{ - TokenOrPath: c.IDToken, - DisableProviders: c.OIDCDisableProviders, - Provider: c.OIDCProvider, - AuthFlow: c.FulcioAuthFlow, - SkipConfirm: c.SkipConfirmation, - OIDCServices: c.SigningConfig.OIDCProviderURLs(), - ClientID: c.OIDCClientID, - ClientSecret: c.OIDCClientSecret, - RedirectURL: c.OIDCRedirectURL, - }) - if err != nil { - return fmt.Errorf("retrieving ID token: %w", err) - } - } - - content := &sign.DSSEData{ - Data: payload, - PayloadType: "application/vnd.in-toto+json", - } - bundle, err := cbundle.SignData(ctx, content, keypair, idToken, c.SigningConfig, c.TrustedMaterial) - if err != nil { - return fmt.Errorf("signing bundle: %w", err) - } - if err := os.WriteFile(c.BundlePath, bundle, 0600); err != nil { - return fmt.Errorf("create bundle file: %w", err) - } - ui.Infof(ctx, "Wrote bundle to file %s", c.BundlePath) - return nil + return signcommon.WriteNewBundleWithSigningConfig(ctx, c.KeyOpts, c.CertPath, c.CertChainPath, payload, name.Digest{}, "", c.BundlePath, c.SigningConfig, c.TrustedMaterial, nil) } - sv, genKey, err := cosign_sign.SignerFromKeyOpts(ctx, c.CertPath, c.CertChainPath, c.KeyOpts) + bundleComponents, closeSV, err := signcommon.GetBundleComponents(ctx, c.CertPath, c.CertChainPath, c.KeyOpts, false, c.TlogUpload, payload, nil, c.RekorEntryType) if err != nil { - return fmt.Errorf("getting signer: %w", err) - } - if genKey || c.IssueCertificateForExistingKey { - sv, err = cosign_sign.KeylessSigner(ctx, c.KeyOpts, sv) - if err != nil { - return fmt.Errorf("getting Fulcio signer: %w", err) - } + return fmt.Errorf("getting bundle components: %w", err) } - defer sv.Close() - wrapped := sigstoredsse.WrapSigner(sv, types.IntotoPayloadType) + defer closeSV() - sig, err := wrapped.SignMessage(bytes.NewReader(payload), signatureoptions.WithContext(ctx)) - if err != nil { - return fmt.Errorf("signing: %w", err) - } + sv := bundleComponents.SV - var rfc3161Timestamp *cbundle.RFC3161Timestamp - var timestampBytes []byte - var tsaPayload []byte - var rekorEntry *models.LogEntryAnon - - if c.KeyOpts.TSAServerURL != "" { - tc := tsaclient.NewTSAClient(c.KeyOpts.TSAServerURL) - if c.TSAClientCert != "" { - tc = tsaclient.NewTSAClientMTLS(c.KeyOpts.TSAServerURL, - c.KeyOpts.TSAClientCACert, - c.KeyOpts.TSAClientCert, - c.KeyOpts.TSAClientKey, - c.KeyOpts.TSAServerName, - ) - } - // We need to decide what signature to send to the timestamp authority. - // - // Historically, cosign sent `sig`, which is the entire JSON DSSE - // Envelope. However, when sigstore clients are verifying a bundle they - // will use the DSSE Sig field, so we choose what signature to send to - // the timestamp authority based on our output format. - if c.NewBundleFormat { - tsaPayload, err = cosign.GetDSSESigBytes(sig) - if err != nil { - return err - } - } else { - tsaPayload = sig - } - timestampBytes, err = tsa.GetTimestampedSignature(tsaPayload, tc) - if err != nil { - return err - } - rfc3161Timestamp = cbundle.TimestampToRFC3161Timestamp(timestampBytes) - // TODO: Consider uploading RFC3161 TS to Rekor - - if rfc3161Timestamp == nil { - return fmt.Errorf("rfc3161 timestamp is nil") - } - - if c.RFC3161TimestampPath != "" { - ts, err := json.Marshal(rfc3161Timestamp) - if err != nil { - return err - } - if err := os.WriteFile(c.RFC3161TimestampPath, ts, 0600); err != nil { - return fmt.Errorf("create RFC3161 timestamp file: %w", err) - } - fmt.Fprintln(os.Stderr, "RFC3161 timestamp bundle written to file ", c.RFC3161TimestampPath) - } - } - - signer, err := sv.Bytes(ctx) - if err != nil { - return err - } - shouldUpload, err := cosign_sign.ShouldUploadToTlog(ctx, c.KeyOpts, nil, c.TlogUpload) - if err != nil { - return fmt.Errorf("upload to tlog: %w", err) - } signedPayload := cosign.LocalSignedPayload{} - if shouldUpload { - rekorClient, err := rekor.NewClient(c.RekorURL) - if err != nil { - return err - } - if c.RekorEntryType == "intoto" { - rekorEntry, err = cosign.TLogUploadInTotoAttestation(ctx, rekorClient, sig, signer) - } else { - rekorEntry, err = cosign.TLogUploadDSSEEnvelope(ctx, rekorClient, sig, signer) - } - if err != nil { - return err - } - fmt.Fprintln(os.Stderr, "tlog entry created with index:", *rekorEntry.LogIndex) - signedPayload.Bundle = cbundle.EntryToBundle(rekorEntry) + if bundleComponents.RekorEntry != nil { + signedPayload.Bundle = cbundle.EntryToBundle(bundleComponents.RekorEntry) } if c.BundlePath != "" { @@ -323,13 +168,13 @@ func (c *AttestBlobCommand) Exec(ctx context.Context, artifactPath string) error return err } - contents, err = cbundle.MakeNewBundle(pubKey, rekorEntry, payload, sig, signer, timestampBytes) + contents, err = cbundle.MakeNewBundle(pubKey, bundleComponents.RekorEntry, payload, bundleComponents.SignedPayload, bundleComponents.SignerBytes, bundleComponents.TimestampBytes) if err != nil { return err } } else { - signedPayload.Base64Signature = base64.StdEncoding.EncodeToString(sig) - signedPayload.Cert = base64.StdEncoding.EncodeToString(signer) + signedPayload.Base64Signature = base64.StdEncoding.EncodeToString(bundleComponents.SignedPayload) + signedPayload.Cert = base64.StdEncoding.EncodeToString(bundleComponents.SignerBytes) contents, err = json.Marshal(signedPayload) if err != nil { @@ -344,12 +189,12 @@ func (c *AttestBlobCommand) Exec(ctx context.Context, artifactPath string) error } if c.OutputSignature != "" { - if err := os.WriteFile(c.OutputSignature, sig, 0600); err != nil { + if err := os.WriteFile(c.OutputSignature, bundleComponents.SignedPayload, 0600); err != nil { return fmt.Errorf("create signature file: %w", err) } fmt.Fprintf(os.Stderr, "Signature written in %s\n", c.OutputSignature) } else { - fmt.Fprintln(os.Stdout, string(sig)) + fmt.Fprintln(os.Stdout, string(bundleComponents.SignedPayload)) } if c.OutputAttestation != "" { @@ -360,11 +205,7 @@ func (c *AttestBlobCommand) Exec(ctx context.Context, artifactPath string) error } if c.OutputCertificate != "" { - signer, err := sv.Bytes(ctx) - if err != nil { - return fmt.Errorf("error getting signer: %w", err) - } - cert, err := cryptoutils.UnmarshalCertificatesFromPEM(signer) + cert, err := cryptoutils.UnmarshalCertificatesFromPEM(bundleComponents.SignerBytes) // signer is a certificate if err != nil { fmt.Fprintln(os.Stderr, "Could not output signer certificate. Was a certificate used? ", err) @@ -375,7 +216,7 @@ func (c *AttestBlobCommand) Exec(ctx context.Context, artifactPath string) error fmt.Fprintln(os.Stderr, "Could not output signer certificate. Expected a single certificate") return nil } - bts := signer + bts := bundleComponents.SignerBytes if err := os.WriteFile(c.OutputCertificate, bts, 0600); err != nil { return fmt.Errorf("create certificate file: %w", err) } diff --git a/cmd/cosign/cli/sign/sign.go b/cmd/cosign/cli/sign/sign.go index 43e060745fb..443a62fb447 100644 --- a/cmd/cosign/cli/sign/sign.go +++ b/cmd/cosign/cli/sign/sign.go @@ -18,12 +18,8 @@ package sign import ( "bytes" "context" - "crypto" - "crypto/x509" "encoding/base64" "encoding/json" - "encoding/pem" - "errors" "fmt" "os" "path/filepath" @@ -31,15 +27,10 @@ import ( "github.com/google/go-containerregistry/pkg/name" v1 "github.com/google/go-containerregistry/pkg/v1" - "github.com/google/go-containerregistry/pkg/v1/remote" intotov1 "github.com/in-toto/attestation/go/v1" - "github.com/sigstore/cosign/v3/cmd/cosign/cli/fulcio" - "github.com/sigstore/cosign/v3/cmd/cosign/cli/fulcio/fulcioverifier" "github.com/sigstore/cosign/v3/cmd/cosign/cli/options" "github.com/sigstore/cosign/v3/cmd/cosign/cli/rekor" - "github.com/sigstore/cosign/v3/cmd/cosign/cli/sign/privacy" - "github.com/sigstore/cosign/v3/internal/auth" - "github.com/sigstore/cosign/v3/internal/key" + "github.com/sigstore/cosign/v3/cmd/cosign/cli/signcommon" icos "github.com/sigstore/cosign/v3/internal/pkg/cosign" ifulcio "github.com/sigstore/cosign/v3/internal/pkg/cosign/fulcio" ipayload "github.com/sigstore/cosign/v3/internal/pkg/cosign/payload" @@ -48,22 +39,12 @@ import ( "github.com/sigstore/cosign/v3/internal/pkg/cosign/tsa/client" "github.com/sigstore/cosign/v3/internal/ui" "github.com/sigstore/cosign/v3/pkg/cosign" - cbundle "github.com/sigstore/cosign/v3/pkg/cosign/bundle" - "github.com/sigstore/cosign/v3/pkg/cosign/pivkey" - "github.com/sigstore/cosign/v3/pkg/cosign/pkcs11key" cremote "github.com/sigstore/cosign/v3/pkg/cosign/remote" "github.com/sigstore/cosign/v3/pkg/oci" "github.com/sigstore/cosign/v3/pkg/oci/mutate" ociremote "github.com/sigstore/cosign/v3/pkg/oci/remote" "github.com/sigstore/cosign/v3/pkg/oci/walk" - sigs "github.com/sigstore/cosign/v3/pkg/signature" "github.com/sigstore/cosign/v3/pkg/types" - "github.com/sigstore/rekor/pkg/generated/models" - "github.com/sigstore/sigstore-go/pkg/sign" - "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" sigPayload "github.com/sigstore/sigstore/pkg/signature/payload" "google.golang.org/protobuf/encoding/protojson" @@ -71,49 +52,6 @@ import ( _ "github.com/sigstore/cosign/v3/pkg/providers/all" ) -func ShouldUploadToTlog(ctx context.Context, ko options.KeyOpts, ref name.Reference, tlogUpload bool) (bool, error) { - upload := shouldUploadToTlog(ctx, ko, ref, tlogUpload) - var statementErr error - if upload { - privacy.StatementOnce.Do(func() { - ui.Infof(ctx, privacy.Statement) - ui.Infof(ctx, privacy.StatementConfirmation) - if !ko.SkipConfirmation { - if err := ui.ConfirmContinue(ctx); err != nil { - statementErr = err - } - } - }) - } - return upload, statementErr -} - -func shouldUploadToTlog(ctx context.Context, ko options.KeyOpts, ref name.Reference, tlogUpload bool) bool { - // return false if not uploading to the tlog has been requested - if !tlogUpload { - return false - } - - if ko.SkipConfirmation { - return true - } - - // We don't need to validate the ref, just return true - if ref == nil { - return true - } - - // Check if the image is public (no auth in Get) - if _, err := remote.Get(ref, remote.WithContext(ctx)); err != nil { - ui.Warnf(ctx, "%q appears to be a private repository, please confirm uploading to the transparency log at %q", ref.Context().String(), ko.RekorURL) - if ui.ConfirmContinue(ctx) != nil { - ui.Infof(ctx, "not uploading to transparency log") - return false - } - } - return true -} - func GetAttachedImageRef(ref name.Reference, attachment string, opts ...ociremote.Option) (name.Reference, error) { if attachment == "" { return ref, nil @@ -124,18 +62,6 @@ func GetAttachedImageRef(ref name.Reference, attachment string, opts ...ociremot return nil, fmt.Errorf("unknown attachment type %s", attachment) } -// ParseOCIReference parses a string reference to an OCI image into a reference, warning if the reference did not include a digest. -func ParseOCIReference(ctx context.Context, refStr string, opts ...name.Option) (name.Reference, error) { - ref, err := name.ParseReference(refStr, opts...) - if err != nil { - return nil, fmt.Errorf("parsing reference: %w", err) - } - if _, ok := ref.(name.Digest); !ok { - ui.Warnf(ctx, ui.TagReferenceMessage, refStr) - } - return ref, nil -} - // nolint func SignCmd(ro *options.RootOptions, ko options.KeyOpts, signOpts options.SignOptions, imgs []string) error { if options.NOf(ko.KeyRef, ko.Sk) > 1 { @@ -171,7 +97,7 @@ func SignCmd(ro *options.RootOptions, ko options.KeyOpts, signOpts options.SignO } annotations := am.Annotations for _, inputImg := range imgs { - ref, err := ParseOCIReference(ctx, inputImg, regOpts.NameOptions()...) + ref, err := signcommon.ParseOCIReference(ctx, inputImg, regOpts.NameOptions()...) if err != nil { return err } @@ -248,145 +174,23 @@ func signDigestBundle(ctx context.Context, digest name.Digest, ko options.KeyOpt return err } - if ko.SigningConfig != nil { - var keypair sign.Keypair - var ephemeralKeypair bool - var idToken string - var sv *SignerVerifier - var err error - - if ko.Sk || ko.Slot != "" || ko.KeyRef != "" || signOpts.Cert != "" { - sv, _, err = SignerFromKeyOpts(ctx, signOpts.Cert, signOpts.CertChain, ko) - if err != nil { - return fmt.Errorf("getting signer: %w", err) - } - keypair, err = key.NewSignerVerifierKeypair(sv, ko.DefaultLoadOptions) - if err != nil { - return fmt.Errorf("creating signerverifier keypair: %w", err) - } - } else { - keypair, err = sign.NewEphemeralKeypair(nil) - if err != nil { - return fmt.Errorf("generating keypair: %w", err) - } - ephemeralKeypair = true - } - defer func() { - if sv != nil { - sv.Close() - } - }() - - if ephemeralKeypair || ko.IssueCertificateForExistingKey { - idToken, err = auth.RetrieveIDToken(ctx, auth.IDTokenConfig{ - TokenOrPath: ko.IDToken, - DisableProviders: ko.OIDCDisableProviders, - Provider: ko.OIDCProvider, - AuthFlow: ko.FulcioAuthFlow, - SkipConfirm: ko.SkipConfirmation, - OIDCServices: ko.SigningConfig.OIDCProviderURLs(), - ClientID: ko.OIDCClientID, - ClientSecret: ko.OIDCClientSecret, - RedirectURL: ko.OIDCRedirectURL, - }) - if err != nil { - return fmt.Errorf("retrieving ID token: %w", err) - } - } - - content := &sign.DSSEData{ - Data: payload, - PayloadType: "application/vnd.in-toto+json", - } - bundle, err := cbundle.SignData(ctx, content, keypair, idToken, ko.SigningConfig, ko.TrustedMaterial) - if err != nil { - return fmt.Errorf("signing bundle: %w", err) - } - - regOpts := signOpts.Registry - ociremoteOpts, err := regOpts.ClientOpts(ctx) - if err != nil { - return fmt.Errorf("constructing client options: %w", err) - } - return ociremote.WriteAttestationNewBundleFormat(digest, bundle, types.CosignSignPredicateType, ociremoteOpts...) - } - - sv, genKey, err := SignerFromKeyOpts(ctx, signOpts.Cert, signOpts.CertChain, ko) - if err != nil { - return fmt.Errorf("getting signer: %w", err) - } - if genKey || ko.IssueCertificateForExistingKey { - sv, err = KeylessSigner(ctx, ko, sv) - if err != nil { - return fmt.Errorf("getting Fulcio signer: %w", err) - } - } - defer sv.Close() - - wrapped := dsse.WrapSigner(sv, types.IntotoPayloadType) - signedPayload, err := wrapped.SignMessage(bytes.NewReader(payload), signatureoptions.WithContext(ctx)) - if err != nil { - return fmt.Errorf("signing: %w", err) - } - - var timestampBytes []byte - if ko.TSAServerURL != "" { - tsaPayload, err := cosign.GetDSSESigBytes(signedPayload) - if err != nil { - return err - } - tc := client.NewTSAClient(ko.TSAServerURL) - if ko.TSAClientCert != "" { - tc = client.NewTSAClientMTLS(ko.TSAServerURL, - ko.TSAClientCACert, - ko.TSAClientCert, - ko.TSAClientKey, - ko.TSAServerName, - ) - } - timestampBytes, err = tsa.GetTimestampedSignature(tsaPayload, tc) - if err != nil { - return err - } - } - - signerBytes, err := sv.Bytes(ctx) - if err != nil { - return err - } - - var rekorEntry *models.LogEntryAnon - shouldUpload, err := ShouldUploadToTlog(ctx, ko, digest, signOpts.TlogUpload) - if err != nil { - return fmt.Errorf("should upload to tlog: %w", err) - } - if shouldUpload { - rClient, err := rekor.NewClient(ko.RekorURL) - if err != nil { - return err - } - rekorEntry, err = cosign.TLogUploadDSSEEnvelope(ctx, rClient, signedPayload, signerBytes) - if err != nil { - return err - } - } - regOpts := signOpts.Registry ociremoteOpts, err := regOpts.ClientOpts(ctx) if err != nil { return fmt.Errorf("constructing client options: %w", err) } - pubKey, err := sv.PublicKey() - if err != nil { - return err + if ko.SigningConfig != nil { + return signcommon.WriteNewBundleWithSigningConfig(ctx, ko, signOpts.Cert, signOpts.CertChain, payload, digest, types.CosignSignPredicateType, "", ko.SigningConfig, ko.TrustedMaterial, ociremoteOpts...) } - bundleBytes, err := cbundle.MakeNewBundle(pubKey, rekorEntry, payload, signedPayload, signerBytes, timestampBytes) + bundleComponents, closeSV, err := signcommon.GetBundleComponents(ctx, signOpts.Cert, signOpts.CertChain, ko, false, signOpts.TlogUpload, payload, digest, "dsse") if err != nil { - return err + return fmt.Errorf("getting bundle components: %w", err) } - return ociremote.WriteAttestationNewBundleFormat(digest, bundleBytes, types.CosignSignPredicateType, ociremoteOpts...) + defer closeSV() + + return signcommon.WriteBundle(bundleComponents.SV, bundleComponents.RekorEntry, payload, bundleComponents.SignedPayload, bundleComponents.SignerBytes, bundleComponents.TimestampBytes, digest, types.CosignSignPredicateType, ociremoteOpts...) } func signDigest(ctx context.Context, digest name.Digest, payload []byte, ko options.KeyOpts, signOpts options.SignOptions, @@ -404,17 +208,12 @@ func signDigest(ctx context.Context, digest name.Digest, payload []byte, ko opti } } - sv, genKey, err := SignerFromKeyOpts(ctx, signOpts.Cert, signOpts.CertChain, ko) + sv, closeSV, err := signcommon.GetSignerVerifier(ctx, signOpts.Cert, signOpts.CertChain, ko) if err != nil { return fmt.Errorf("getting signer: %w", err) } - if genKey || ko.IssueCertificateForExistingKey { - sv, err = KeylessSigner(ctx, ko, sv) - if err != nil { - return fmt.Errorf("getting Fulcio signer: %w", err) - } - } - defer sv.Close() + defer closeSV() + dd := cremote.NewDupeDetector(sv) var s icos.Signer @@ -435,7 +234,7 @@ func signDigest(ctx context.Context, digest name.Digest, payload []byte, ko opti )) } } - shouldUpload, err := ShouldUploadToTlog(ctx, ko, digest, signOpts.TlogUpload) + shouldUpload, err := signcommon.ShouldUploadToTlog(ctx, ko, digest, signOpts.TlogUpload) if err != nil { return fmt.Errorf("should upload to tlog: %w", err) } @@ -540,240 +339,6 @@ func signDigest(ctx context.Context, digest name.Digest, payload []byte, ko opti return ociremote.WriteSignatures(digest.Repository, newSE, walkOpts...) } -func signerFromSecurityKey(ctx context.Context, keySlot string) (*SignerVerifier, error) { - sk, err := pivkey.GetKeyWithSlot(keySlot) - if err != nil { - return nil, err - } - sv, err := sk.SignerVerifier() - if err != nil { - sk.Close() - return nil, err - } - - // Handle the -cert flag. - // With PIV, we assume the certificate is in the same slot on the PIV - // token as the private key. If it's not there, show a warning to the - // user. - certFromPIV, err := sk.Certificate() - var pemBytes []byte - if err != nil { - ui.Warnf(ctx, "no x509 certificate retrieved from the PIV token") - } else { - pemBytes, err = cryptoutils.MarshalCertificateToPEM(certFromPIV) - if err != nil { - sk.Close() - return nil, err - } - } - - return &SignerVerifier{ - Cert: pemBytes, - SignerVerifier: sv, - close: sk.Close, - }, nil -} - -func signerFromKeyRef(ctx context.Context, certPath, certChainPath, keyRef string, passFunc cosign.PassFunc, defaultLoadOptions *[]signature.LoadOption) (*SignerVerifier, error) { - k, err := sigs.SignerVerifierFromKeyRef(ctx, keyRef, passFunc, defaultLoadOptions) - if err != nil { - return nil, fmt.Errorf("reading key: %w", err) - } - certSigner := &SignerVerifier{ - SignerVerifier: k, - } - - var leafCert *x509.Certificate - - // Attempt to extract certificate from PKCS11 token - // With PKCS11, we assume the certificate is in the same slot on the PKCS11 - // token as the private key. If it's not there, show a warning to the - // user. - if pkcs11Key, ok := k.(*pkcs11key.Key); ok { - certFromPKCS11, _ := pkcs11Key.Certificate() - certSigner.close = pkcs11Key.Close - - if certFromPKCS11 == nil { - ui.Warnf(ctx, "no x509 certificate retrieved from the PKCS11 token") - } else { - pemBytes, err := cryptoutils.MarshalCertificateToPEM(certFromPKCS11) - if err != nil { - pkcs11Key.Close() - return nil, err - } - // Check that the provided public key and certificate key match - pubKey, err := k.PublicKey() - if err != nil { - pkcs11Key.Close() - return nil, err - } - if cryptoutils.EqualKeys(pubKey, certFromPKCS11.PublicKey) != nil { - pkcs11Key.Close() - return nil, errors.New("pkcs11 key and certificate do not match") - } - leafCert = certFromPKCS11 - certSigner.Cert = pemBytes - } - } - - // Handle --cert flag - if certPath != "" { - // Allow both DER and PEM encoding - certBytes, err := os.ReadFile(certPath) - if err != nil { - return nil, fmt.Errorf("read certificate: %w", err) - } - // Handle PEM - if bytes.HasPrefix(certBytes, []byte("-----")) { - decoded, _ := pem.Decode(certBytes) - if decoded.Type != "CERTIFICATE" { - return nil, fmt.Errorf("supplied PEM file is not a certificate: %s", certPath) - } - certBytes = decoded.Bytes - } - parsedCert, err := x509.ParseCertificate(certBytes) - if err != nil { - return nil, fmt.Errorf("parse x509 certificate: %w", err) - } - pk, err := k.PublicKey() - if err != nil { - return nil, fmt.Errorf("get public key: %w", err) - } - if cryptoutils.EqualKeys(pk, parsedCert.PublicKey) != nil { - return nil, errors.New("public key in certificate does not match the provided public key") - } - pemBytes, err := cryptoutils.MarshalCertificateToPEM(parsedCert) - if err != nil { - return nil, fmt.Errorf("marshaling certificate to PEM: %w", err) - } - if certSigner.Cert != nil { - ui.Warnf(ctx, "overriding x509 certificate retrieved from the PKCS11 token") - } - leafCert = parsedCert - certSigner.Cert = pemBytes - } - - if certChainPath == "" { - return certSigner, nil - } else if certSigner.Cert == nil { - return nil, errors.New("no leaf certificate found or provided while specifying chain") - } - - // Handle --cert-chain flag - // Accept only PEM encoded certificate chain - certChainBytes, err := os.ReadFile(certChainPath) - if err != nil { - return nil, fmt.Errorf("reading certificate chain from path: %w", err) - } - certChain, err := cryptoutils.LoadCertificatesFromPEM(bytes.NewReader(certChainBytes)) - if err != nil { - return nil, fmt.Errorf("loading certificate chain: %w", err) - } - if len(certChain) == 0 { - return nil, errors.New("no certificates in certificate chain") - } - // Verify certificate chain is valid - rootPool := x509.NewCertPool() - rootPool.AddCert(certChain[len(certChain)-1]) - subPool := x509.NewCertPool() - for _, c := range certChain[:len(certChain)-1] { - subPool.AddCert(c) - } - if _, err := cosign.TrustedCert(leafCert, rootPool, subPool); err != nil { - return nil, fmt.Errorf("unable to validate certificate chain: %w", err) - } - certSigner.Chain = certChainBytes - - return certSigner, nil -} - -func signerFromNewKey() (*SignerVerifier, error) { - privKey, err := cosign.GeneratePrivateKey() - if err != nil { - return nil, fmt.Errorf("generating cert: %w", err) - } - sv, err := signature.LoadECDSASignerVerifier(privKey, crypto.SHA256) - if err != nil { - return nil, err - } - - return &SignerVerifier{ - SignerVerifier: sv, - }, nil -} - -func KeylessSigner(ctx context.Context, ko options.KeyOpts, sv *SignerVerifier) (*SignerVerifier, error) { - var ( - k *fulcio.Signer - err error - ) - - if _, ok := sv.SignerVerifier.(*signature.ED25519phSignerVerifier); ok { - return nil, fmt.Errorf("ed25519ph unsupported by Fulcio") - } - - if ko.InsecureSkipFulcioVerify { - if k, err = fulcio.NewSigner(ctx, ko, sv); err != nil { - return nil, fmt.Errorf("getting key from Fulcio: %w", err) - } - } else { - if k, err = fulcioverifier.NewSigner(ctx, ko, sv); err != nil { - return nil, fmt.Errorf("getting key from Fulcio: %w", err) - } - } - - return &SignerVerifier{ - Cert: k.Cert, - Chain: k.Chain, - SignerVerifier: k, - }, nil -} - -func SignerFromKeyOpts(ctx context.Context, certPath string, certChainPath string, ko options.KeyOpts) (*SignerVerifier, bool, error) { - var sv *SignerVerifier - var err error - genKey := false - switch { - case ko.Sk: - sv, err = signerFromSecurityKey(ctx, ko.Slot) - case ko.KeyRef != "": - sv, err = signerFromKeyRef(ctx, certPath, certChainPath, ko.KeyRef, ko.PassFunc, ko.DefaultLoadOptions) - default: - genKey = true - ui.Infof(ctx, "Generating ephemeral keys...") - sv, err = signerFromNewKey() - } - if err != nil { - return nil, false, err - } - return sv, genKey, nil -} - -type SignerVerifier struct { - Cert []byte - Chain []byte - signature.SignerVerifier - close func() -} - -func (c *SignerVerifier) Close() { - if c.close != nil { - c.close() - } -} - -func (c *SignerVerifier) Bytes(ctx context.Context) ([]byte, error) { - if c.Cert != nil { - return c.Cert, nil - } - - pemBytes, err := sigs.PublicKeyPem(c, signatureoptions.WithContext(ctx)) - if err != nil { - return nil, err - } - return pemBytes, nil -} - func fetchLocalSignedPayload(sig oci.Signature) (*cosign.LocalSignedPayload, error) { signedPayload := &cosign.LocalSignedPayload{} var err error diff --git a/cmd/cosign/cli/sign/sign_blob.go b/cmd/cosign/cli/sign/sign_blob.go index cba9af705ac..a3e462a1bae 100644 --- a/cmd/cosign/cli/sign/sign_blob.go +++ b/cmd/cosign/cli/sign/sign_blob.go @@ -30,17 +30,14 @@ import ( "google.golang.org/protobuf/encoding/protojson" "github.com/sigstore/cosign/v3/cmd/cosign/cli/options" - "github.com/sigstore/cosign/v3/cmd/cosign/cli/rekor" - "github.com/sigstore/cosign/v3/internal/auth" - "github.com/sigstore/cosign/v3/internal/key" + "github.com/sigstore/cosign/v3/cmd/cosign/cli/signcommon" internal "github.com/sigstore/cosign/v3/internal/pkg/cosign" - "github.com/sigstore/cosign/v3/internal/pkg/cosign/tsa" - "github.com/sigstore/cosign/v3/internal/pkg/cosign/tsa/client" "github.com/sigstore/cosign/v3/internal/ui" "github.com/sigstore/cosign/v3/pkg/cosign" cbundle "github.com/sigstore/cosign/v3/pkg/cosign/bundle" protobundle "github.com/sigstore/protobuf-specs/gen/pb-go/bundle/v1" protocommon "github.com/sigstore/protobuf-specs/gen/pb-go/common/v1" + rekorclient "github.com/sigstore/rekor/pkg/generated/client" "github.com/sigstore/rekor/pkg/generated/models" "github.com/sigstore/sigstore-go/pkg/sign" "github.com/sigstore/sigstore/pkg/cryptoutils" @@ -67,7 +64,7 @@ func SignBlobCmd(ro *options.RootOptions, ko options.KeyOpts, payloadPath string ctx, cancel := context.WithTimeout(context.Background(), ro.Timeout) defer cancel() - shouldUpload, err := ShouldUploadToTlog(ctx, ko, nil, tlogUpload) + shouldUpload, err := signcommon.ShouldUploadToTlog(ctx, ko, nil, tlogUpload) if err != nil { return nil, fmt.Errorf("upload to tlog: %w", err) } @@ -80,49 +77,9 @@ func SignBlobCmd(ro *options.RootOptions, ko options.KeyOpts, payloadPath string } if ko.SigningConfig != nil { - var keypair sign.Keypair - var ephemeralKeypair bool - var idToken string - var sv *SignerVerifier - var err error - - if ko.Sk || ko.Slot != "" || ko.KeyRef != "" { - sv, _, err = SignerFromKeyOpts(ctx, "", "", ko) - if err != nil { - return nil, fmt.Errorf("getting signer: %w", err) - } - keypair, err = key.NewSignerVerifierKeypair(sv, ko.DefaultLoadOptions) - if err != nil { - return nil, fmt.Errorf("creating signerverifier keypair: %w", err) - } - } else { - keypair, err = sign.NewEphemeralKeypair(nil) - if err != nil { - return nil, fmt.Errorf("generating keypair: %w", err) - } - ephemeralKeypair = true - } - defer func() { - if sv != nil { - sv.Close() - } - }() - - if ephemeralKeypair || ko.IssueCertificateForExistingKey { - idToken, err = auth.RetrieveIDToken(ctx, auth.IDTokenConfig{ - TokenOrPath: ko.IDToken, - DisableProviders: ko.OIDCDisableProviders, - Provider: ko.OIDCProvider, - AuthFlow: ko.FulcioAuthFlow, - SkipConfirm: ko.SkipConfirmation, - OIDCServices: ko.SigningConfig.OIDCProviderURLs(), - ClientID: ko.OIDCClientID, - ClientSecret: ko.OIDCClientSecret, - RedirectURL: ko.OIDCRedirectURL, - }) - if err != nil { - return nil, fmt.Errorf("retrieving ID token: %w", err) - } + keypair, idToken, err := signcommon.GetKeypairAndToken(ctx, ko, "", "") + if err != nil { + return nil, fmt.Errorf("getting keypair and token: %w", err) } payload, closePayload, err := getPayload(ctx, payloadPath, protoHashAlgoToHash(keypair.GetHashAlgorithm())) @@ -137,6 +94,7 @@ func SignBlobCmd(ro *options.RootOptions, ko options.KeyOpts, payloadPath string content := &sign.PlainData{ Data: data, } + bundle, err := cbundle.SignData(ctx, content, keypair, idToken, ko.SigningConfig, ko.TrustedMaterial) if err != nil { return nil, fmt.Errorf("signing bundle: %w", err) @@ -148,17 +106,11 @@ func SignBlobCmd(ro *options.RootOptions, ko options.KeyOpts, payloadPath string return bundle, nil } - sv, genKey, err := SignerFromKeyOpts(ctx, "", "", ko) + sv, closeSV, err := signcommon.GetSignerVerifier(ctx, "", "", ko) if err != nil { - return nil, err + return nil, fmt.Errorf("getting signer: %w", err) } - if genKey || ko.IssueCertificateForExistingKey { - sv, err = KeylessSigner(ctx, ko, sv) - if err != nil { - return nil, fmt.Errorf("getting Fulcio signer: %w", err) - } - } - defer sv.Close() + defer closeSV() hashFunction, err := getHashFunction(sv, ko.DefaultLoadOptions) if err != nil { @@ -188,63 +140,23 @@ func SignBlobCmd(ro *options.RootOptions, ko options.KeyOpts, payloadPath string digest := payload.Sum(nil) signedPayload := cosign.LocalSignedPayload{} - var rekorEntry *models.LogEntryAnon - var rfc3161Timestamp *cbundle.RFC3161Timestamp - var timestampBytes []byte - - if ko.TSAServerURL != "" { - if ko.RFC3161TimestampPath == "" && !ko.NewBundleFormat { - return nil, fmt.Errorf("must use protobuf bundle or set timestamp output path") - } - var err error - if ko.TSAClientCACert == "" && ko.TSAClientCert == "" { // no mTLS params or custom CA - timestampBytes, err = tsa.GetTimestampedSignature(sig, client.NewTSAClient(ko.TSAServerURL)) - if err != nil { - return nil, err - } - } else { - timestampBytes, err = tsa.GetTimestampedSignature(sig, client.NewTSAClientMTLS(ko.TSAServerURL, - ko.TSAClientCACert, - ko.TSAClientCert, - ko.TSAClientKey, - ko.TSAServerName, - )) - if err != nil { - return nil, err - } - } - rfc3161Timestamp = cbundle.TimestampToRFC3161Timestamp(timestampBytes) - - if rfc3161Timestamp == nil { - return nil, fmt.Errorf("rfc3161 timestamp is nil") - } + timestampBytes, _, err := signcommon.GetRFC3161Timestamp(sig, ko) + if err != nil { + return nil, fmt.Errorf("getting timestamp: %w", err) + } - if ko.RFC3161TimestampPath != "" { - ts, err := json.Marshal(rfc3161Timestamp) - if err != nil { - return nil, err - } - if err := os.WriteFile(ko.RFC3161TimestampPath, ts, 0600); err != nil { - return nil, fmt.Errorf("create RFC3161 timestamp file: %w", err) - } - ui.Infof(ctx, "RFC3161 timestamp written to file %s\n", ko.RFC3161TimestampPath) - } + signer, err := sv.Bytes(ctx) + if err != nil { + return nil, err } - if shouldUpload { - rekorBytes, err := sv.Bytes(ctx) - if err != nil { - return nil, err - } - rekorClient, err := rekor.NewClient(ko.RekorURL) - if err != nil { - return nil, err - } - rekorEntry, err = cosign.TLogUploadWithCustomHash(ctx, rekorClient, sig, &payload, rekorBytes) - if err != nil { - return nil, err - } - ui.Infof(ctx, "tlog entry created with index: %d", *rekorEntry.LogIndex) + rekorEntry, err := signcommon.UploadToTlog(ctx, ko, nil, shouldUpload, signer, func(r *rekorclient.Rekor, b []byte) (*models.LogEntryAnon, error) { + return cosign.TLogUploadWithCustomHash(ctx, r, sig, &payload, b) + }) + if err != nil { + return nil, err + } + if rekorEntry != nil { signedPayload.Bundle = cbundle.EntryToBundle(rekorEntry) } @@ -256,10 +168,6 @@ func SignBlobCmd(ro *options.RootOptions, ko options.KeyOpts, payloadPath string var hint string var rawCert []byte - signer, err := sv.Bytes(ctx) - if err != nil { - return nil, fmt.Errorf("error getting signer: %w", err) - } cert, err := cryptoutils.UnmarshalCertificatesFromPEM(signer) if err != nil || len(cert) == 0 { pubKey, err := sv.PublicKey() @@ -356,7 +264,7 @@ func SignBlobCmd(ro *options.RootOptions, ko options.KeyOpts, payloadPath string } // Extract an encoded certificate from the SignerVerifier. Returns (nil, nil) if verifier is not a certificate. -func extractCertificate(ctx context.Context, sv *SignerVerifier) ([]byte, error) { +func extractCertificate(ctx context.Context, sv *signcommon.SignerVerifier) ([]byte, error) { signer, err := sv.Bytes(ctx) if err != nil { return nil, fmt.Errorf("error getting signer: %w", err) @@ -369,7 +277,7 @@ func extractCertificate(ctx context.Context, sv *SignerVerifier) ([]byte, error) return nil, nil } -func getHashFunction(sv *SignerVerifier, defaultLoadOptions *[]signature.LoadOption) (crypto.Hash, error) { +func getHashFunction(sv *signcommon.SignerVerifier, defaultLoadOptions *[]signature.LoadOption) (crypto.Hash, error) { pubKey, err := sv.PublicKey() if err != nil { return crypto.Hash(0), fmt.Errorf("error getting public key: %w", err) diff --git a/cmd/cosign/cli/sign/sign_test.go b/cmd/cosign/cli/sign/sign_test.go index 78ad209d7cc..a8be500276f 100644 --- a/cmd/cosign/cli/sign/sign_test.go +++ b/cmd/cosign/cli/sign/sign_test.go @@ -16,98 +16,13 @@ package sign import ( - "context" - "crypto/ecdsa" - "crypto/x509" - "encoding/pem" "errors" - "os" - "reflect" - "strings" "testing" - "github.com/stretchr/testify/assert" - - "github.com/secure-systems-lab/go-securesystemslib/encrypted" "github.com/sigstore/cosign/v3/cmd/cosign/cli/generate" "github.com/sigstore/cosign/v3/cmd/cosign/cli/options" - "github.com/sigstore/cosign/v3/internal/test" - "github.com/sigstore/cosign/v3/internal/ui" - "github.com/sigstore/cosign/v3/pkg/cosign" - "github.com/sigstore/sigstore/pkg/cryptoutils" ) -func pass(s string) cosign.PassFunc { - return func(_ bool) ([]byte, error) { - return []byte(s), nil - } -} - -func generateCertificateFiles(t *testing.T, tmpDir string, pf cosign.PassFunc) (privFile, certFile, chainFile string, privKey *ecdsa.PrivateKey, cert *x509.Certificate, chain []*x509.Certificate) { - t.Helper() - - rootCert, rootKey, _ := test.GenerateRootCa() - subCert, subKey, _ := test.GenerateSubordinateCa(rootCert, rootKey) - leafCert, privKey, _ := test.GenerateLeafCert("subject", "oidc-issuer", subCert, subKey) - pemRoot := pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: rootCert.Raw}) - pemSub := pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: subCert.Raw}) - pemLeaf := pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: leafCert.Raw}) - - x509Encoded, err := x509.MarshalPKCS8PrivateKey(privKey) - if err != nil { - t.Fatalf("failed to encode private key: %v", err) - } - password := []byte{} - if pf != nil { - password, err = pf(true) - if err != nil { - t.Fatalf("failed to read password: %v", err) - } - } - - encBytes, err := encrypted.Encrypt(x509Encoded, password) - if err != nil { - t.Fatalf("failed to encrypt key: %v", err) - } - - // store in PEM format - privBytes := pem.EncodeToMemory(&pem.Block{ - Bytes: encBytes, - Type: cosign.CosignPrivateKeyPemType, - }) - - tmpPrivFile, err := os.CreateTemp(tmpDir, "cosign_test_*.key") - if err != nil { - t.Fatalf("failed to create temp key file: %v", err) - } - defer tmpPrivFile.Close() - if _, err := tmpPrivFile.Write(privBytes); err != nil { - t.Fatalf("failed to write key file: %v", err) - } - - tmpCertFile, err := os.CreateTemp(tmpDir, "cosign.crt") - if err != nil { - t.Fatalf("failed to create temp certificate file: %v", err) - } - defer tmpCertFile.Close() - if _, err := tmpCertFile.Write(pemLeaf); err != nil { - t.Fatalf("failed to write certificate file: %v", err) - } - - tmpChainFile, err := os.CreateTemp(tmpDir, "cosign_chain.crt") - if err != nil { - t.Fatalf("failed to create temp chain file: %v", err) - } - defer tmpChainFile.Close() - pemChain := pemSub - pemChain = append(pemChain, pemRoot...) - if _, err := tmpChainFile.Write(pemChain); err != nil { - t.Fatalf("failed to write chain file: %v", err) - } - - return tmpPrivFile.Name(), tmpCertFile.Name(), tmpChainFile.Name(), privKey, leafCert, []*x509.Certificate{subCert, rootCert} -} - // TestSignCmdLocalKeyAndSk verifies the SignCmd returns an error // if both a local key path and a sk are specified func TestSignCmdLocalKeyAndSk(t *testing.T) { @@ -128,104 +43,3 @@ func TestSignCmdLocalKeyAndSk(t *testing.T) { } } } - -func Test_signerFromKeyRefSuccess(t *testing.T) { - tmpDir := t.TempDir() - ctx := context.Background() - keyFile, certFile, chainFile, privKey, cert, chain := generateCertificateFiles(t, tmpDir, pass("foo")) - - signer, err := signerFromKeyRef(ctx, certFile, chainFile, keyFile, pass("foo"), nil) - if err != nil { - t.Fatalf("unexpected error generating signer: %v", err) - } - // Expect public key matches - pubKey, err := signer.PublicKey() - if err != nil { - t.Fatalf("unexpected error fetching pubkey: %v", err) - } - if !privKey.Public().(*ecdsa.PublicKey).Equal(pubKey) { - t.Fatalf("public keys must be equal") - } - // Expect certificate matches - expectedPemBytes, err := cryptoutils.MarshalCertificateToPEM(cert) - if err != nil { - t.Fatalf("unexpected error marshalling certificate: %v", err) - } - if !reflect.DeepEqual(signer.Cert, expectedPemBytes) { - t.Fatalf("certificates must match") - } - // Expect certificate chain matches - expectedPemBytesChain, err := cryptoutils.MarshalCertificatesToPEM(chain) - if err != nil { - t.Fatalf("unexpected error marshalling certificate chain: %v", err) - } - if !reflect.DeepEqual(signer.Chain, expectedPemBytesChain) { - t.Fatalf("certificate chains must match") - } -} - -func Test_signerFromKeyRefFailure(t *testing.T) { - tmpDir := t.TempDir() - ctx := context.Background() - keyFile, certFile, _, _, _, _ := generateCertificateFiles(t, tmpDir, pass("foo")) - // Second set of files - tmpDir2 := t.TempDir() - _, certFile2, chainFile2, _, _, _ := generateCertificateFiles(t, tmpDir2, pass("bar")) - - // Public keys don't match - _, err := signerFromKeyRef(ctx, certFile2, chainFile2, keyFile, pass("foo"), nil) - if err == nil || err.Error() != "public key in certificate does not match the provided public key" { - t.Fatalf("expected mismatched keys error, got %v", err) - } - // Certificate chain cannot be verified - _, err = signerFromKeyRef(ctx, certFile, chainFile2, keyFile, pass("foo"), nil) - if err == nil || !strings.Contains(err.Error(), "unable to validate certificate chain") { - t.Fatalf("expected chain verification error, got %v", err) - } - // Certificate chain specified without certificate - _, err = signerFromKeyRef(ctx, "", chainFile2, keyFile, pass("foo"), nil) - if err == nil || !strings.Contains(err.Error(), "no leaf certificate found or provided while specifying chain") { - t.Fatalf("expected no leaf error, got %v", err) - } -} - -func Test_signerFromKeyRefFailureEmptyChainFile(t *testing.T) { - tmpDir := t.TempDir() - ctx := context.Background() - keyFile, certFile, _, _, _, _ := generateCertificateFiles(t, tmpDir, pass("foo")) - - tmpChainFile, err := os.CreateTemp(tmpDir, "cosign_chain_empty.crt") - if err != nil { - t.Fatalf("failed to create temp chain file: %v", err) - } - defer tmpChainFile.Close() - if _, err := tmpChainFile.Write([]byte{}); err != nil { - t.Fatalf("failed to write chain file: %v", err) - } - - _, err = signerFromKeyRef(ctx, certFile, tmpChainFile.Name(), keyFile, pass("foo"), nil) - if err == nil || err.Error() != "no certificates in certificate chain" { - t.Fatalf("expected empty chain error, got %v", err) - } -} - -func Test_ParseOCIReference(t *testing.T) { - var tests = []struct { - ref string - expectedWarning string - }{ - {"image:bytag", "WARNING: Image reference image:bytag uses a tag, not a digest"}, - {"image:bytag@sha256:abcdef", ""}, - {"image:@sha256:abcdef", ""}, - } - for _, tt := range tests { - stderr := ui.RunWithTestCtx(func(ctx context.Context, _ ui.WriteFunc) { - ParseOCIReference(ctx, tt.ref) - }) - if len(tt.expectedWarning) > 0 { - assert.Contains(t, stderr, tt.expectedWarning, stderr, "bad warning message") - } else { - assert.Empty(t, stderr, "expected no warning") - } - } -} diff --git a/cmd/cosign/cli/signcommon/common.go b/cmd/cosign/cli/signcommon/common.go new file mode 100644 index 00000000000..6b1690693b2 --- /dev/null +++ b/cmd/cosign/cli/signcommon/common.go @@ -0,0 +1,583 @@ +// Copyright 2025 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 signcommon + +import ( + "bytes" + "context" + "crypto" + "crypto/x509" + "encoding/json" + "encoding/pem" + "errors" + "fmt" + "os" + + "github.com/google/go-containerregistry/pkg/name" + "github.com/google/go-containerregistry/pkg/v1/remote" + "github.com/sigstore/cosign/v3/cmd/cosign/cli/fulcio" + "github.com/sigstore/cosign/v3/cmd/cosign/cli/fulcio/fulcioverifier" + "github.com/sigstore/cosign/v3/cmd/cosign/cli/options" + "github.com/sigstore/cosign/v3/cmd/cosign/cli/rekor" + "github.com/sigstore/cosign/v3/cmd/cosign/cli/sign/privacy" + "github.com/sigstore/cosign/v3/internal/auth" + "github.com/sigstore/cosign/v3/internal/key" + "github.com/sigstore/cosign/v3/internal/pkg/cosign/tsa" + "github.com/sigstore/cosign/v3/internal/pkg/cosign/tsa/client" + "github.com/sigstore/cosign/v3/internal/ui" + "github.com/sigstore/cosign/v3/pkg/cosign" + cbundle "github.com/sigstore/cosign/v3/pkg/cosign/bundle" + "github.com/sigstore/cosign/v3/pkg/cosign/pivkey" + "github.com/sigstore/cosign/v3/pkg/cosign/pkcs11key" + ociremote "github.com/sigstore/cosign/v3/pkg/oci/remote" + sigs "github.com/sigstore/cosign/v3/pkg/signature" + "github.com/sigstore/cosign/v3/pkg/types" + rekorclient "github.com/sigstore/rekor/pkg/generated/client" + "github.com/sigstore/rekor/pkg/generated/models" + "github.com/sigstore/sigstore-go/pkg/root" + "github.com/sigstore/sigstore-go/pkg/sign" + "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" +) + +// SignerVerifier contains keys or certs to sign and verify. +type SignerVerifier struct { + Cert []byte + Chain []byte + signature.SignerVerifier + close func() +} + +// Close closes the key context if there is one. +func (c *SignerVerifier) Close() { + if c.close != nil { + c.close() + } +} + +// Bytes returns the raw bytes of the cert or key. +func (c *SignerVerifier) Bytes(ctx context.Context) ([]byte, error) { + if c.Cert != nil { + return c.Cert, nil + } + + pemBytes, err := sigs.PublicKeyPem(c, signatureoptions.WithContext(ctx)) + if err != nil { + return nil, err + } + return pemBytes, nil +} + +// GetKeypairAndToken creates a keypair object from provided key or cert flags or generates an ephemeral key. +// For an ephemeral key, it also uses the key to fetch an OIDC token, the pair of which are later used to get a Fulcio cert. +func GetKeypairAndToken(ctx context.Context, ko options.KeyOpts, cert, certChain string) (sign.Keypair, string, error) { + var keypair sign.Keypair + var ephemeralKeypair bool + var idToken string + var sv *SignerVerifier + var err error + + if ko.Sk || ko.Slot != "" || ko.KeyRef != "" || cert != "" { + sv, _, err = signerFromKeyOpts(ctx, cert, certChain, ko) + if err != nil { + return nil, "", fmt.Errorf("getting signer: %w", err) + } + keypair, err = key.NewSignerVerifierKeypair(sv, ko.DefaultLoadOptions) + if err != nil { + return nil, "", fmt.Errorf("creating signerverifier keypair: %w", err) + } + } else { + keypair, err = sign.NewEphemeralKeypair(nil) + if err != nil { + return nil, "", fmt.Errorf("generating keypair: %w", err) + } + ephemeralKeypair = true + } + defer func() { + if sv != nil { + sv.Close() + } + }() + + if ephemeralKeypair || ko.IssueCertificateForExistingKey { + idToken, err = auth.RetrieveIDToken(ctx, auth.IDTokenConfig{ + TokenOrPath: ko.IDToken, + DisableProviders: ko.OIDCDisableProviders, + Provider: ko.OIDCProvider, + AuthFlow: ko.FulcioAuthFlow, + SkipConfirm: ko.SkipConfirmation, + OIDCServices: ko.SigningConfig.OIDCProviderURLs(), + ClientID: ko.OIDCClientID, + ClientSecret: ko.OIDCClientSecret, + RedirectURL: ko.OIDCRedirectURL, + }) + if err != nil { + return nil, "", fmt.Errorf("retrieving ID token: %w", err) + } + } + + return keypair, idToken, nil +} + +func keylessSigner(ctx context.Context, ko options.KeyOpts, sv *SignerVerifier) (*SignerVerifier, error) { + var ( + k *fulcio.Signer + err error + ) + + if _, ok := sv.SignerVerifier.(*signature.ED25519phSignerVerifier); ok { + return nil, fmt.Errorf("ed25519ph unsupported by Fulcio") + } + + if ko.InsecureSkipFulcioVerify { + if k, err = fulcio.NewSigner(ctx, ko, sv); err != nil { + return nil, fmt.Errorf("getting key from Fulcio: %w", err) + } + } else { + if k, err = fulcioverifier.NewSigner(ctx, ko, sv); err != nil { + return nil, fmt.Errorf("getting key from Fulcio: %w", err) + } + } + + return &SignerVerifier{ + Cert: k.Cert, + Chain: k.Chain, + SignerVerifier: k, + }, nil +} + +// ShouldUploadToTlog determines whether the user wants to upload the entry to Rekor. +func ShouldUploadToTlog(ctx context.Context, ko options.KeyOpts, ref name.Reference, tlogUpload bool) (bool, error) { + upload := shouldUploadToTlog(ctx, ko, ref, tlogUpload) + var statementErr error + if upload { + privacy.StatementOnce.Do(func() { + ui.Infof(ctx, privacy.Statement) + ui.Infof(ctx, privacy.StatementConfirmation) + if !ko.SkipConfirmation { + if err := ui.ConfirmContinue(ctx); err != nil { + statementErr = err + } + } + }) + } + return upload, statementErr +} + +func shouldUploadToTlog(ctx context.Context, ko options.KeyOpts, ref name.Reference, tlogUpload bool) bool { + // return false if not uploading to the tlog has been requested + if !tlogUpload { + return false + } + + if ko.SkipConfirmation { + return true + } + + // We don't need to validate the ref, just return true + if ref == nil { + return true + } + + // Check if the image is public (no auth in Get) + if _, err := remote.Get(ref, remote.WithContext(ctx)); err != nil { + ui.Warnf(ctx, "%q appears to be a private repository, please confirm uploading to the transparency log at %q", ref.Context().String(), ko.RekorURL) + if ui.ConfirmContinue(ctx) != nil { + ui.Infof(ctx, "not uploading to transparency log") + return false + } + } + return true +} + +// GetSignerVerifier generates a SignerVerifier from provided key flags. +func GetSignerVerifier(ctx context.Context, cert, certChain string, ko options.KeyOpts) (*SignerVerifier, func(), error) { + sv, genKey, err := signerFromKeyOpts(ctx, cert, certChain, ko) + if err != nil { + return nil, nil, fmt.Errorf("getting signer from opts: %w", err) + } + if genKey || ko.IssueCertificateForExistingKey { + sv, err = keylessSigner(ctx, ko, sv) + if err != nil { + return nil, nil, fmt.Errorf("getting Fulcio signer: %w", err) + } + } + return sv, sv.Close, nil +} + +func signerFromKeyOpts(ctx context.Context, certPath string, certChainPath string, ko options.KeyOpts) (*SignerVerifier, bool, error) { + var sv *SignerVerifier + var err error + genKey := false + switch { + case ko.Sk: + sv, err = signerFromSecurityKey(ctx, ko.Slot) + case ko.KeyRef != "": + sv, err = signerFromKeyRef(ctx, certPath, certChainPath, ko.KeyRef, ko.PassFunc, ko.DefaultLoadOptions) + default: + genKey = true + ui.Infof(ctx, "Generating ephemeral keys...") + sv, err = signerFromNewKey() + } + if err != nil { + return nil, false, err + } + return sv, genKey, nil +} + +func signerFromSecurityKey(ctx context.Context, keySlot string) (*SignerVerifier, error) { + sk, err := pivkey.GetKeyWithSlot(keySlot) + if err != nil { + return nil, err + } + sv, err := sk.SignerVerifier() + if err != nil { + sk.Close() + return nil, err + } + + // Handle the -cert flag. + // With PIV, we assume the certificate is in the same slot on the PIV + // token as the private key. If it's not there, show a warning to the + // user. + certFromPIV, err := sk.Certificate() + var pemBytes []byte + if err != nil { + ui.Warnf(ctx, "no x509 certificate retrieved from the PIV token") + } else { + pemBytes, err = cryptoutils.MarshalCertificateToPEM(certFromPIV) + if err != nil { + sk.Close() + return nil, err + } + } + + return &SignerVerifier{ + Cert: pemBytes, + SignerVerifier: sv, + close: sk.Close, + }, nil +} + +func signerFromKeyRef(ctx context.Context, certPath, certChainPath, keyRef string, passFunc cosign.PassFunc, defaultLoadOptions *[]signature.LoadOption) (*SignerVerifier, error) { + k, err := sigs.SignerVerifierFromKeyRef(ctx, keyRef, passFunc, defaultLoadOptions) + if err != nil { + return nil, fmt.Errorf("reading key: %w", err) + } + certSigner := &SignerVerifier{ + SignerVerifier: k, + } + + var leafCert *x509.Certificate + + // Attempt to extract certificate from PKCS11 token + // With PKCS11, we assume the certificate is in the same slot on the PKCS11 + // token as the private key. If it's not there, show a warning to the + // user. + if pkcs11Key, ok := k.(*pkcs11key.Key); ok { + certFromPKCS11, _ := pkcs11Key.Certificate() + certSigner.close = pkcs11Key.Close + + if certFromPKCS11 == nil { + ui.Warnf(ctx, "no x509 certificate retrieved from the PKCS11 token") + } else { + pemBytes, err := cryptoutils.MarshalCertificateToPEM(certFromPKCS11) + if err != nil { + pkcs11Key.Close() + return nil, err + } + // Check that the provided public key and certificate key match + pubKey, err := k.PublicKey() + if err != nil { + pkcs11Key.Close() + return nil, err + } + if cryptoutils.EqualKeys(pubKey, certFromPKCS11.PublicKey) != nil { + pkcs11Key.Close() + return nil, errors.New("pkcs11 key and certificate do not match") + } + leafCert = certFromPKCS11 + certSigner.Cert = pemBytes + } + } + + // Handle --cert flag + if certPath != "" { + // Allow both DER and PEM encoding + certBytes, err := os.ReadFile(certPath) + if err != nil { + return nil, fmt.Errorf("read certificate: %w", err) + } + // Handle PEM + if bytes.HasPrefix(certBytes, []byte("-----")) { + decoded, _ := pem.Decode(certBytes) + if decoded.Type != "CERTIFICATE" { + return nil, fmt.Errorf("supplied PEM file is not a certificate: %s", certPath) + } + certBytes = decoded.Bytes + } + parsedCert, err := x509.ParseCertificate(certBytes) + if err != nil { + return nil, fmt.Errorf("parse x509 certificate: %w", err) + } + pk, err := k.PublicKey() + if err != nil { + return nil, fmt.Errorf("get public key: %w", err) + } + if cryptoutils.EqualKeys(pk, parsedCert.PublicKey) != nil { + return nil, errors.New("public key in certificate does not match the provided public key") + } + pemBytes, err := cryptoutils.MarshalCertificateToPEM(parsedCert) + if err != nil { + return nil, fmt.Errorf("marshaling certificate to PEM: %w", err) + } + if certSigner.Cert != nil { + ui.Warnf(ctx, "overriding x509 certificate retrieved from the PKCS11 token") + } + leafCert = parsedCert + certSigner.Cert = pemBytes + } + + if certChainPath == "" { + return certSigner, nil + } else if certSigner.Cert == nil { + return nil, errors.New("no leaf certificate found or provided while specifying chain") + } + + // Handle --cert-chain flag + // Accept only PEM encoded certificate chain + certChainBytes, err := os.ReadFile(certChainPath) + if err != nil { + return nil, fmt.Errorf("reading certificate chain from path: %w", err) + } + certChain, err := cryptoutils.LoadCertificatesFromPEM(bytes.NewReader(certChainBytes)) + if err != nil { + return nil, fmt.Errorf("loading certificate chain: %w", err) + } + if len(certChain) == 0 { + return nil, errors.New("no certificates in certificate chain") + } + // Verify certificate chain is valid + rootPool := x509.NewCertPool() + rootPool.AddCert(certChain[len(certChain)-1]) + subPool := x509.NewCertPool() + for _, c := range certChain[:len(certChain)-1] { + subPool.AddCert(c) + } + if _, err := cosign.TrustedCert(leafCert, rootPool, subPool); err != nil { + return nil, fmt.Errorf("unable to validate certificate chain: %w", err) + } + certSigner.Chain = certChainBytes + + return certSigner, nil +} + +func signerFromNewKey() (*SignerVerifier, error) { + privKey, err := cosign.GeneratePrivateKey() + if err != nil { + return nil, fmt.Errorf("generating cert: %w", err) + } + sv, err := signature.LoadECDSASignerVerifier(privKey, crypto.SHA256) + if err != nil { + return nil, err + } + + return &SignerVerifier{ + SignerVerifier: sv, + }, nil +} + +// GetRFC3161Timestamp fetches an RFC3161 timestamp as raw bytes and as a RFC3161Timestamp object. +// It either returns both objects to be assembled into a bundle by the calling function, +// or writes the formatted timestamp to the provided file path if not using the new bundle format. +func GetRFC3161Timestamp(payload []byte, ko options.KeyOpts) ([]byte, *cbundle.RFC3161Timestamp, error) { + if ko.TSAServerURL == "" { + return nil, nil, nil + } + if ko.RFC3161TimestampPath == "" && !ko.NewBundleFormat { + return nil, nil, fmt.Errorf("expected either new bundle or an rfc3161-timestamp path when using a TSA server") + } + tc := client.NewTSAClient(ko.TSAServerURL) + if ko.TSAClientCert != "" { + tc = client.NewTSAClientMTLS( + ko.TSAServerURL, + ko.TSAClientCACert, + ko.TSAClientCert, + ko.TSAClientKey, + ko.TSAServerName, + ) + } + timestampBytes, err := tsa.GetTimestampedSignature(payload, tc) + if err != nil { + return nil, nil, fmt.Errorf("getting timestamped signature: %w", err) + } + rfc3161Timestamp := cbundle.TimestampToRFC3161Timestamp(timestampBytes) + if rfc3161Timestamp == nil { + return nil, nil, fmt.Errorf("rfc3161 timestamp is nil") + } + if ko.NewBundleFormat || ko.RFC3161TimestampPath == "" { + return timestampBytes, rfc3161Timestamp, nil + } + ts, err := json.Marshal(rfc3161Timestamp) + if err != nil { + return nil, nil, fmt.Errorf("marshalling timestamp: %w", err) + } + if err := os.WriteFile(ko.RFC3161TimestampPath, ts, 0600); err != nil { + return nil, nil, fmt.Errorf("creating RFC3161 timestamp file: %w", err) + } + fmt.Fprintln(os.Stderr, "RFC3161 timestamp written to file ", ko.RFC3161TimestampPath) + return timestampBytes, rfc3161Timestamp, nil +} + +type tlogUploadFn func(*rekorclient.Rekor, []byte) (*models.LogEntryAnon, error) + +// UploadToTlog uploads an entry to rekor v1 and returns the response from rekor. +func UploadToTlog(ctx context.Context, ko options.KeyOpts, ref name.Reference, tlogUpload bool, rekorBytes []byte, upload tlogUploadFn) (*models.LogEntryAnon, error) { + shouldUpload, err := ShouldUploadToTlog(ctx, ko, ref, tlogUpload) + if err != nil { + return nil, fmt.Errorf("checking upload to tlog: %w", err) + } + if !shouldUpload { + return nil, nil + } + rekorClient, err := rekor.NewClient(ko.RekorURL) + if err != nil { + return nil, fmt.Errorf("creating rekor client: %w", err) + } + entry, err := upload(rekorClient, rekorBytes) + if err != nil { + return nil, fmt.Errorf("uploading to rekor: %w", err) + } + fmt.Fprintln(os.Stderr, "tlog entry created with index:", *entry.LogIndex) + return entry, nil +} + +// WriteBundle compiles a protobuf bundle from components and writes the bundle to the OCI remote layer. +func WriteBundle(sv *SignerVerifier, rekorEntry *models.LogEntryAnon, payload, signedPayload, signerBytes, timestampBytes []byte, digest name.Digest, predicateType string, ociremoteOpts ...ociremote.Option) error { + pubKey, err := sv.PublicKey() + if err != nil { + return err + } + bundleBytes, err := cbundle.MakeNewBundle(pubKey, rekorEntry, payload, signedPayload, signerBytes, timestampBytes) + if err != nil { + return err + } + return ociremote.WriteAttestationNewBundleFormat(digest, bundleBytes, predicateType, ociremoteOpts...) +} + +// WriteNewBundleWithSigningConfig uses signing config and trusted root to fetch responses from services for the bundle and writes the bundle to the OCI remote layer. +func WriteNewBundleWithSigningConfig(ctx context.Context, ko options.KeyOpts, cert, certChain string, payload []byte, digest name.Digest, predicateType, bundlePath string, signingConfig *root.SigningConfig, trustedMaterial root.TrustedMaterial, ociremoteOpts ...ociremote.Option) error { + keypair, idToken, err := GetKeypairAndToken(ctx, ko, cert, certChain) + if err != nil { + return fmt.Errorf("getting keypair and token: %w", err) + } + + content := &sign.DSSEData{ + Data: payload, + PayloadType: "application/vnd.in-toto+json", + } + bundle, err := cbundle.SignData(ctx, content, keypair, idToken, signingConfig, trustedMaterial) + if err != nil { + return fmt.Errorf("signing bundle: %w", err) + } + + if bundlePath != "" { + if err := os.WriteFile(bundlePath, bundle, 0600); err != nil { + return fmt.Errorf("creating bundle file: %w", err) + } + ui.Infof(ctx, "Wrote bundle to file %s", bundlePath) + return nil + } + return ociremote.WriteAttestationNewBundleFormat(digest, bundle, predicateType, ociremoteOpts...) +} + +type bundleComponents struct { + SV *SignerVerifier + SignedPayload []byte + TimestampBytes []byte + RFC3161Timestamp *cbundle.RFC3161Timestamp + SignerBytes []byte + RekorEntry *models.LogEntryAnon +} + +// GetBundleComponents fetches data needed to compose the bundle or disparate verification material for any signing command. +func GetBundleComponents(ctx context.Context, cert, certChain string, ko options.KeyOpts, noupload, tlogUpload bool, payload []byte, digest name.Reference, rekorEntryType string) (*bundleComponents, func(), error) { //nolint:revive + bc := &bundleComponents{} + var err error + var closeSV func() + bc.SV, closeSV, err = GetSignerVerifier(ctx, cert, certChain, ko) + if err != nil { + return nil, nil, fmt.Errorf("getting signer: %w", err) + } + wrapped := dsse.WrapSigner(bc.SV, types.IntotoPayloadType) + + bc.SignedPayload, err = wrapped.SignMessage(bytes.NewReader(payload), signatureoptions.WithContext(ctx)) + if err != nil { + closeSV() + return nil, nil, fmt.Errorf("signing: %w", err) + } + if noupload { + return bc, closeSV, nil + } + // We need to decide what signature to send to the timestamp authority. + // + // Historically, cosign sent `signedPayload`, which is the entire JSON DSSE + // Envelope. However, when sigstore clients are verifying a bundle they + // will use the DSSE Sig field, so we choose what signature to send to + // the timestamp authority based on our output format. + tsaPayload := bc.SignedPayload + if ko.NewBundleFormat { + tsaPayload, err = cosign.GetDSSESigBytes(bc.SignedPayload) + if err != nil { + closeSV() + return nil, nil, fmt.Errorf("getting DSSE signature: %w", err) + } + } + bc.TimestampBytes, bc.RFC3161Timestamp, err = GetRFC3161Timestamp(tsaPayload, ko) + if err != nil { + closeSV() + return nil, nil, fmt.Errorf("getting timestamp: %w", err) + } + bc.SignerBytes, err = bc.SV.Bytes(ctx) + if err != nil { + closeSV() + return nil, nil, fmt.Errorf("converting signer to bytes: %w", err) + } + bc.RekorEntry, err = UploadToTlog(ctx, ko, digest, tlogUpload, bc.SignerBytes, func(r *rekorclient.Rekor, b []byte) (*models.LogEntryAnon, error) { + if rekorEntryType == "intoto" { + return cosign.TLogUploadInTotoAttestation(ctx, r, bc.SignedPayload, b) + } + return cosign.TLogUploadDSSEEnvelope(ctx, r, bc.SignedPayload, b) + }) + if err != nil { + closeSV() + return nil, nil, fmt.Errorf("uploading to tlog: %w", err) + } + return bc, closeSV, nil +} + +// ParseOCIReference parses a string reference to an OCI image into a reference, warning if the reference did not include a digest. +func ParseOCIReference(ctx context.Context, refStr string, opts ...name.Option) (name.Reference, error) { + ref, err := name.ParseReference(refStr, opts...) + if err != nil { + return nil, fmt.Errorf("parsing reference: %w", err) + } + if _, ok := ref.(name.Digest); !ok { + ui.Warnf(ctx, ui.TagReferenceMessage, refStr) + } + return ref, nil +} diff --git a/cmd/cosign/cli/signcommon/common_test.go b/cmd/cosign/cli/signcommon/common_test.go new file mode 100644 index 00000000000..3c17baed0a0 --- /dev/null +++ b/cmd/cosign/cli/signcommon/common_test.go @@ -0,0 +1,205 @@ +// Copyright 2025 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 signcommon + +import ( + "context" + "crypto/ecdsa" + "crypto/x509" + "encoding/pem" + "os" + "reflect" + "strings" + "testing" + + "github.com/secure-systems-lab/go-securesystemslib/encrypted" + "github.com/sigstore/cosign/v3/internal/test" + "github.com/sigstore/cosign/v3/internal/ui" + "github.com/sigstore/cosign/v3/pkg/cosign" + "github.com/sigstore/sigstore/pkg/cryptoutils" + "github.com/stretchr/testify/assert" +) + +func pass(s string) cosign.PassFunc { + return func(_ bool) ([]byte, error) { + return []byte(s), nil + } +} + +func generateCertificateFiles(t *testing.T, tmpDir string, pf cosign.PassFunc) (privFile, certFile, chainFile string, privKey *ecdsa.PrivateKey, cert *x509.Certificate, chain []*x509.Certificate) { + t.Helper() + + rootCert, rootKey, _ := test.GenerateRootCa() + subCert, subKey, _ := test.GenerateSubordinateCa(rootCert, rootKey) + leafCert, privKey, _ := test.GenerateLeafCert("subject", "oidc-issuer", subCert, subKey) + pemRoot := pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: rootCert.Raw}) + pemSub := pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: subCert.Raw}) + pemLeaf := pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: leafCert.Raw}) + + x509Encoded, err := x509.MarshalPKCS8PrivateKey(privKey) + if err != nil { + t.Fatalf("failed to encode private key: %v", err) + } + password := []byte{} + if pf != nil { + password, err = pf(true) + if err != nil { + t.Fatalf("failed to read password: %v", err) + } + } + + encBytes, err := encrypted.Encrypt(x509Encoded, password) + if err != nil { + t.Fatalf("failed to encrypt key: %v", err) + } + + // store in PEM format + privBytes := pem.EncodeToMemory(&pem.Block{ + Bytes: encBytes, + Type: cosign.CosignPrivateKeyPemType, + }) + + tmpPrivFile, err := os.CreateTemp(tmpDir, "cosign_test_*.key") + if err != nil { + t.Fatalf("failed to create temp key file: %v", err) + } + defer tmpPrivFile.Close() + if _, err := tmpPrivFile.Write(privBytes); err != nil { + t.Fatalf("failed to write key file: %v", err) + } + + tmpCertFile, err := os.CreateTemp(tmpDir, "cosign.crt") + if err != nil { + t.Fatalf("failed to create temp certificate file: %v", err) + } + defer tmpCertFile.Close() + if _, err := tmpCertFile.Write(pemLeaf); err != nil { + t.Fatalf("failed to write certificate file: %v", err) + } + + tmpChainFile, err := os.CreateTemp(tmpDir, "cosign_chain.crt") + if err != nil { + t.Fatalf("failed to create temp chain file: %v", err) + } + defer tmpChainFile.Close() + pemChain := pemSub + pemChain = append(pemChain, pemRoot...) + if _, err := tmpChainFile.Write(pemChain); err != nil { + t.Fatalf("failed to write chain file: %v", err) + } + + return tmpPrivFile.Name(), tmpCertFile.Name(), tmpChainFile.Name(), privKey, leafCert, []*x509.Certificate{subCert, rootCert} +} + +func Test_signerFromKeyRefSuccess(t *testing.T) { + tmpDir := t.TempDir() + ctx := context.Background() + keyFile, certFile, chainFile, privKey, cert, chain := generateCertificateFiles(t, tmpDir, pass("foo")) + + signer, err := signerFromKeyRef(ctx, certFile, chainFile, keyFile, pass("foo"), nil) + if err != nil { + t.Fatalf("unexpected error generating signer: %v", err) + } + // Expect public key matches + pubKey, err := signer.PublicKey() + if err != nil { + t.Fatalf("unexpected error fetching pubkey: %v", err) + } + if !privKey.Public().(*ecdsa.PublicKey).Equal(pubKey) { + t.Fatalf("public keys must be equal") + } + // Expect certificate matches + expectedPemBytes, err := cryptoutils.MarshalCertificateToPEM(cert) + if err != nil { + t.Fatalf("unexpected error marshalling certificate: %v", err) + } + if !reflect.DeepEqual(signer.Cert, expectedPemBytes) { + t.Fatalf("certificates must match") + } + // Expect certificate chain matches + expectedPemBytesChain, err := cryptoutils.MarshalCertificatesToPEM(chain) + if err != nil { + t.Fatalf("unexpected error marshalling certificate chain: %v", err) + } + if !reflect.DeepEqual(signer.Chain, expectedPemBytesChain) { + t.Fatalf("certificate chains must match") + } +} + +func Test_signerFromKeyRefFailure(t *testing.T) { + tmpDir := t.TempDir() + ctx := context.Background() + keyFile, certFile, _, _, _, _ := generateCertificateFiles(t, tmpDir, pass("foo")) + // Second set of files + tmpDir2 := t.TempDir() + _, certFile2, chainFile2, _, _, _ := generateCertificateFiles(t, tmpDir2, pass("bar")) + + // Public keys don't match + _, err := signerFromKeyRef(ctx, certFile2, chainFile2, keyFile, pass("foo"), nil) + if err == nil || err.Error() != "public key in certificate does not match the provided public key" { + t.Fatalf("expected mismatched keys error, got %v", err) + } + // Certificate chain cannot be verified + _, err = signerFromKeyRef(ctx, certFile, chainFile2, keyFile, pass("foo"), nil) + if err == nil || !strings.Contains(err.Error(), "unable to validate certificate chain") { + t.Fatalf("expected chain verification error, got %v", err) + } + // Certificate chain specified without certificate + _, err = signerFromKeyRef(ctx, "", chainFile2, keyFile, pass("foo"), nil) + if err == nil || !strings.Contains(err.Error(), "no leaf certificate found or provided while specifying chain") { + t.Fatalf("expected no leaf error, got %v", err) + } +} + +func Test_signerFromKeyRefFailureEmptyChainFile(t *testing.T) { + tmpDir := t.TempDir() + ctx := context.Background() + keyFile, certFile, _, _, _, _ := generateCertificateFiles(t, tmpDir, pass("foo")) + + tmpChainFile, err := os.CreateTemp(tmpDir, "cosign_chain_empty.crt") + if err != nil { + t.Fatalf("failed to create temp chain file: %v", err) + } + defer tmpChainFile.Close() + if _, err := tmpChainFile.Write([]byte{}); err != nil { + t.Fatalf("failed to write chain file: %v", err) + } + + _, err = signerFromKeyRef(ctx, certFile, tmpChainFile.Name(), keyFile, pass("foo"), nil) + if err == nil || err.Error() != "no certificates in certificate chain" { + t.Fatalf("expected empty chain error, got %v", err) + } +} + +func Test_ParseOCIReference(t *testing.T) { + var tests = []struct { + ref string + expectedWarning string + }{ + {"image:bytag", "WARNING: Image reference image:bytag uses a tag, not a digest"}, + {"image:bytag@sha256:abcdef", ""}, + {"image:@sha256:abcdef", ""}, + } + for _, tt := range tests { + stderr := ui.RunWithTestCtx(func(ctx context.Context, _ ui.WriteFunc) { + ParseOCIReference(ctx, tt.ref) + }) + if len(tt.expectedWarning) > 0 { + assert.Contains(t, stderr, tt.expectedWarning, stderr, "bad warning message") + } else { + assert.Empty(t, stderr, "expected no warning") + } + } +} diff --git a/cmd/cosign/cli/verify/common.go b/cmd/cosign/cli/verify/common.go new file mode 100644 index 00000000000..57cecfffc23 --- /dev/null +++ b/cmd/cosign/cli/verify/common.go @@ -0,0 +1,465 @@ +// Copyright 2025 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 verify + +import ( + "bytes" + "context" + "crypto" + "crypto/x509" + "encoding/base64" + "encoding/json" + "errors" + "fmt" + "os" + "reflect" + + "github.com/sigstore/cosign/v3/cmd/cosign/cli/fulcio" + "github.com/sigstore/cosign/v3/cmd/cosign/cli/options" + "github.com/sigstore/cosign/v3/cmd/cosign/cli/rekor" + "github.com/sigstore/cosign/v3/internal/ui" + "github.com/sigstore/cosign/v3/pkg/blob" + "github.com/sigstore/cosign/v3/pkg/cosign" + "github.com/sigstore/cosign/v3/pkg/cosign/env" + "github.com/sigstore/cosign/v3/pkg/cosign/pivkey" + "github.com/sigstore/cosign/v3/pkg/cosign/pkcs11key" + "github.com/sigstore/cosign/v3/pkg/oci" + csignature "github.com/sigstore/cosign/v3/pkg/signature" + "github.com/sigstore/sigstore-go/pkg/root" + "github.com/sigstore/sigstore/pkg/cryptoutils" + "github.com/sigstore/sigstore/pkg/signature" + "github.com/sigstore/sigstore/pkg/signature/payload" +) + +// CheckSigstoreBundleUnsupportedOptions checks for incompatible settings on any Verify* command struct when NewBundleFormat is used. +func CheckSigstoreBundleUnsupportedOptions(cmd any, co *cosign.CheckOpts) error { + if !co.NewBundleFormat { + return nil + } + fieldToErr := map[string]string{ + "CertRef": "certificate must be in bundle and may not be provided using --certificate", + "CertChain": "certificate chain must be in bundle and may not be provided using --certificate-chain", + "CARoots": "CA roots/intermediates must be provided using --trusted-root", + "CAIntermedias": "CA roots/intermediates must be provided using --trusted-root", + "TSACertChainPath": "TSA certificate chain path may only be provided using --trusted-root", + "RFC3161TimestampPath": "RFC3161 timestamp may not be provided using --rfc3161-timestamp", + "SigRef": "signature may not be provided using --signature", + "SCTRef": "SCT may not be provided using --sct", + } + v := reflect.ValueOf(cmd) + for f, e := range fieldToErr { + if field := v.FieldByName(f); field.IsValid() && field.String() != "" { + return fmt.Errorf("unsupported: %s when using --new-bundle-format", e) + } + } + if co.TrustedMaterial == nil { + return fmt.Errorf("trusted root is required when using new bundle format") + } + return nil +} + +// LoadVerifierFromKeyOrCert returns either a signature.Verifier or a certificate from the provided flags to use for verifying an artifact. +// In the case of certain types of keys, it returns a close function that must be called by the calling method. +func LoadVerifierFromKeyOrCert(ctx context.Context, keyRef, slot, certRef, certChain string, hashAlgorithm crypto.Hash, sk, withGetCert bool, co *cosign.CheckOpts) (signature.Verifier, *x509.Certificate, func(), error) { + var sigVerifier signature.Verifier + var err error + switch { + case keyRef != "": + sigVerifier, err = csignature.PublicKeyFromKeyRefWithHashAlgo(ctx, keyRef, hashAlgorithm) + if err != nil { + return nil, nil, nil, fmt.Errorf("loading public key: %w", err) + } + pkcs11Key, ok := sigVerifier.(*pkcs11key.Key) + closeSV := func() {} + if ok { + closeSV = pkcs11Key.Close + } + return sigVerifier, nil, closeSV, nil + case sk: + sk, err := pivkey.GetKeyWithSlot(slot) + if err != nil { + return nil, nil, nil, fmt.Errorf("opening piv token: %w", err) + } + sigVerifier, err = sk.Verifier() + if err != nil { + sk.Close() + return nil, nil, nil, fmt.Errorf("initializing piv token verifier: %w", err) + } + return sigVerifier, nil, sk.Close, nil + case certRef != "": + cert, err := loadCertFromFileOrURL(certRef) + if err != nil { + return nil, nil, nil, fmt.Errorf("loading cert: %w", err) + } + if withGetCert { + return nil, cert, func() {}, nil + } + if certChain == "" { + sigVerifier, err = cosign.ValidateAndUnpackCert(cert, co) + if err != nil { + return nil, nil, nil, fmt.Errorf("validating cert: %w", err) + } + return sigVerifier, nil, func() {}, nil + } + chain, err := loadCertChainFromFileOrURL(certChain) + if err != nil { + return nil, nil, nil, fmt.Errorf("loading cert chain: %w", err) + } + sigVerifier, err = cosign.ValidateAndUnpackCertWithChain(cert, chain, co) + if err != nil { + return nil, nil, nil, fmt.Errorf("validating cert with chain: %w", err) + } + return sigVerifier, nil, func() {}, nil + } + return nil, nil, func() {}, nil +} + +// SetLegacyClientsAndKeys sets up TSA and rekor clients and keys for TSA, rekor, and CT log. +// It may perform an online fetch of keys, so using trusted root instead of these TUF v1 methos is recommended. +// It takes a CheckOpts as input and modifies it. +func SetLegacyClientsAndKeys(ctx context.Context, ignoreTlog, shouldVerifySCT, keylessVerification bool, rekorURL, tsaCertChain, certChain, caRoots, caIntermediates string, co *cosign.CheckOpts) error { + var err error + if !ignoreTlog && !co.NewBundleFormat && rekorURL != "" { + co.RekorClient, err = rekor.NewClient(rekorURL) + if err != nil { + return fmt.Errorf("creating rekor client: %w", err) + } + } + // If trusted material is set, we don't need to fetch disparate keys. + if co.TrustedMaterial != nil { + return nil + } + if co.UseSignedTimestamps { + tsaCertificates, err := cosign.GetTSACerts(ctx, tsaCertChain, cosign.GetTufTargets) + if err != nil { + return fmt.Errorf("loading TSA certificates: %w", err) + } + co.TSACertificate = tsaCertificates.LeafCert + co.TSARootCertificates = tsaCertificates.RootCert + co.TSAIntermediateCertificates = tsaCertificates.IntermediateCerts + } + if !ignoreTlog { + co.RekorPubKeys, err = cosign.GetRekorPubs(ctx) + if err != nil { + return fmt.Errorf("getting rekor public keys: %w", err) + } + } + if shouldVerifySCT { + co.CTLogPubKeys, err = cosign.GetCTLogPubs(ctx) + if err != nil { + return fmt.Errorf("getting ctlog public keys: %w", err) + } + } + if keylessVerification { + if err := loadCertsKeylessVerification(certChain, caRoots, caIntermediates, co); err != nil { + return fmt.Errorf("loading certs for keyless verification: %w", err) + } + } + return nil +} + +// SetTrustedMaterial sets TrustedMaterial on CheckOpts, either from the provided trusted root path or from TUF. +// It does not set TrustedMaterial if the user provided trusted material via other flags or environment variables. +func SetTrustedMaterial(ctx context.Context, trustedRootPath, certChain, caRoots, caIntermediates, tsaCertChainPath string, co *cosign.CheckOpts) error { + var err error + if trustedRootPath != "" { + co.TrustedMaterial, err = root.NewTrustedRootFromPath(trustedRootPath) + if err != nil { + return fmt.Errorf("loading trusted root: %w", err) + } + return nil + } + if options.NOf(certChain, caRoots, caIntermediates, tsaCertChainPath) == 0 && + env.Getenv(env.VariableSigstoreCTLogPublicKeyFile) == "" && + env.Getenv(env.VariableSigstoreRootFile) == "" && + env.Getenv(env.VariableSigstoreRekorPublicKey) == "" && + env.Getenv(env.VariableSigstoreTSACertificateFile) == "" { + co.TrustedMaterial, err = cosign.TrustedRoot() + if err != nil { + ui.Warnf(ctx, "Could not fetch trusted_root.json from the TUF repository. Continuing with individual targets. Error from TUF: %v", err) + } + } + return nil +} + +// PrintVerificationHeader prints boilerplate information after successful verification. +func PrintVerificationHeader(ctx context.Context, imgRef string, co *cosign.CheckOpts, bundleVerified, fulcioVerified bool) { + ui.Infof(ctx, "\nVerification for %s --", imgRef) + ui.Infof(ctx, "The following checks were performed on each of these signatures:") + if co.ClaimVerifier != nil { + if co.Annotations != nil { + ui.Infof(ctx, " - The specified annotations were verified.") + } + ui.Infof(ctx, " - The cosign claims were validated") + } + if bundleVerified { + ui.Infof(ctx, " - Existence of the claims in the transparency log was verified offline") + } else if co.RekorClient != nil { + ui.Infof(ctx, " - The claims were present in the transparency log") + ui.Infof(ctx, " - The signatures were integrated into the transparency log when the certificate was valid") + } + if co.SigVerifier != nil { + ui.Infof(ctx, " - The signatures were verified against the specified public key") + } + if fulcioVerified { + ui.Infof(ctx, " - The code-signing certificate was verified using trusted certificate authority certificates") + } +} + +// PrintVerification logs details about the verification to stdout. +func PrintVerification(ctx context.Context, verified []oci.Signature, output string) { + switch output { + case "text": + for _, sig := range verified { + if cert, err := sig.Cert(); err == nil && cert != nil { + ce := cosign.CertExtensions{Cert: cert} + sub := "" + if sans := cryptoutils.GetSubjectAlternateNames(cert); len(sans) > 0 { + sub = sans[0] + } + ui.Infof(ctx, "Certificate subject: %s", sub) + if issuerURL := ce.GetIssuer(); issuerURL != "" { + ui.Infof(ctx, "Certificate issuer URL: %s", issuerURL) + } + + if githubWorkflowTrigger := ce.GetCertExtensionGithubWorkflowTrigger(); githubWorkflowTrigger != "" { + ui.Infof(ctx, "GitHub Workflow Trigger: %s", githubWorkflowTrigger) + } + + if githubWorkflowSha := ce.GetExtensionGithubWorkflowSha(); githubWorkflowSha != "" { + ui.Infof(ctx, "GitHub Workflow SHA: %s", githubWorkflowSha) + } + if githubWorkflowName := ce.GetCertExtensionGithubWorkflowName(); githubWorkflowName != "" { + ui.Infof(ctx, "GitHub Workflow Name: %s", githubWorkflowName) + } + + if githubWorkflowRepository := ce.GetCertExtensionGithubWorkflowRepository(); githubWorkflowRepository != "" { + ui.Infof(ctx, "GitHub Workflow Repository: %s", githubWorkflowRepository) + } + + if githubWorkflowRef := ce.GetCertExtensionGithubWorkflowRef(); githubWorkflowRef != "" { + ui.Infof(ctx, "GitHub Workflow Ref: %s", githubWorkflowRef) + } + } + + p, err := sig.Payload() + if err != nil { + fmt.Fprintf(os.Stderr, "Error fetching payload: %v", err) + return + } + fmt.Println(string(p)) + } + + default: + var outputKeys []payload.SimpleContainerImage + for _, sig := range verified { + p, err := sig.Payload() + if err != nil { + fmt.Fprintf(os.Stderr, "Error fetching payload: %v", err) + return + } + + ss := payload.SimpleContainerImage{} + if err := json.Unmarshal(p, &ss); err != nil { + fmt.Println("error decoding the payload:", err.Error()) + return + } + + if cert, err := sig.Cert(); err == nil && cert != nil { + ce := cosign.CertExtensions{Cert: cert} + if ss.Optional == nil { + ss.Optional = make(map[string]interface{}) + } + sub := "" + if sans := cryptoutils.GetSubjectAlternateNames(cert); len(sans) > 0 { + sub = sans[0] + } + ss.Optional["Subject"] = sub + if issuerURL := ce.GetIssuer(); issuerURL != "" { + ss.Optional["Issuer"] = issuerURL + ss.Optional[cosign.CertExtensionOIDCIssuer] = issuerURL + } + if githubWorkflowTrigger := ce.GetCertExtensionGithubWorkflowTrigger(); githubWorkflowTrigger != "" { + ss.Optional[cosign.CertExtensionMap[cosign.CertExtensionGithubWorkflowTrigger]] = githubWorkflowTrigger + ss.Optional[cosign.CertExtensionGithubWorkflowTrigger] = githubWorkflowTrigger + } + + if githubWorkflowSha := ce.GetExtensionGithubWorkflowSha(); githubWorkflowSha != "" { + ss.Optional[cosign.CertExtensionMap[cosign.CertExtensionGithubWorkflowSha]] = githubWorkflowSha + ss.Optional[cosign.CertExtensionGithubWorkflowSha] = githubWorkflowSha + } + if githubWorkflowName := ce.GetCertExtensionGithubWorkflowName(); githubWorkflowName != "" { + ss.Optional[cosign.CertExtensionMap[cosign.CertExtensionGithubWorkflowName]] = githubWorkflowName + ss.Optional[cosign.CertExtensionGithubWorkflowName] = githubWorkflowName + } + + if githubWorkflowRepository := ce.GetCertExtensionGithubWorkflowRepository(); githubWorkflowRepository != "" { + ss.Optional[cosign.CertExtensionMap[cosign.CertExtensionGithubWorkflowRepository]] = githubWorkflowRepository + ss.Optional[cosign.CertExtensionGithubWorkflowRepository] = githubWorkflowRepository + } + + if githubWorkflowRef := ce.GetCertExtensionGithubWorkflowRef(); githubWorkflowRef != "" { + ss.Optional[cosign.CertExtensionMap[cosign.CertExtensionGithubWorkflowRef]] = githubWorkflowRef + ss.Optional[cosign.CertExtensionGithubWorkflowRef] = githubWorkflowRef + } + } + if bundle, err := sig.Bundle(); err == nil && bundle != nil { + if ss.Optional == nil { + ss.Optional = make(map[string]interface{}) + } + ss.Optional["Bundle"] = bundle + } + if rfc3161Timestamp, err := sig.RFC3161Timestamp(); err == nil && rfc3161Timestamp != nil { + if ss.Optional == nil { + ss.Optional = make(map[string]interface{}) + } + ss.Optional["RFC3161Timestamp"] = rfc3161Timestamp + } + + outputKeys = append(outputKeys, ss) + } + + b, err := json.Marshal(outputKeys) + if err != nil { + fmt.Println("error when generating the output:", err.Error()) + return + } + + fmt.Printf("\n%s\n", string(b)) + } +} + +func loadCertFromFileOrURL(path string) (*x509.Certificate, error) { + pems, err := blob.LoadFileOrURL(path) + if err != nil { + return nil, err + } + return loadCertFromPEM(pems) +} + +func loadCertFromPEM(pems []byte) (*x509.Certificate, error) { + 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") + } + return certs[0], nil +} + +func loadCertChainFromFileOrURL(path string) ([]*x509.Certificate, error) { + pems, err := blob.LoadFileOrURL(path) + if err != nil { + return nil, err + } + certs, err := cryptoutils.LoadCertificatesFromPEM(bytes.NewReader(pems)) + if err != nil { + return nil, err + } + return certs, nil +} + +func keylessVerification(keyRef string, sk bool) bool { + if keyRef != "" { + return false + } + if sk { + return false + } + return true +} + +func shouldVerifySCT(ignoreSCT bool, keyRef string, sk bool) bool { + if keyRef != "" { + return false + } + if sk { + return false + } + if ignoreSCT { + return false + } + return true +} + +// loadCertsKeylessVerification loads certificates provided as a certificate chain or CA roots + CA intermediate +// certificate files. If both certChain and caRootsFile are empty strings, the Fulcio roots are loaded. +// +// The co *cosign.CheckOpts is both input and output parameter - it gets updated +// with the root and intermediate certificates needed for verification. +func loadCertsKeylessVerification(certChainFile string, + caRootsFile string, + caIntermediatesFile string, + co *cosign.CheckOpts) error { + var err error + switch { + case certChainFile != "": + chain, err := loadCertChainFromFileOrURL(certChainFile) + if err != nil { + return err + } + co.RootCerts = x509.NewCertPool() + co.RootCerts.AddCert(chain[len(chain)-1]) + if len(chain) > 1 { + co.IntermediateCerts = x509.NewCertPool() + for _, cert := range chain[:len(chain)-1] { + co.IntermediateCerts.AddCert(cert) + } + } + case caRootsFile != "": + caRoots, err := loadCertChainFromFileOrURL(caRootsFile) + if err != nil { + return err + } + co.RootCerts = x509.NewCertPool() + if len(caRoots) > 0 { + for _, cert := range caRoots { + co.RootCerts.AddCert(cert) + } + } + if caIntermediatesFile != "" { + caIntermediates, err := loadCertChainFromFileOrURL(caIntermediatesFile) + if err != nil { + return err + } + if len(caIntermediates) > 0 { + co.IntermediateCerts = x509.NewCertPool() + for _, cert := range caIntermediates { + co.IntermediateCerts.AddCert(cert) + } + } + } + default: + // This performs an online fetch of the Fulcio roots from a TUF repository. + // This is needed for verifying keyless certificates (both online and offline). + co.RootCerts, err = fulcio.GetRoots() + if err != nil { + return fmt.Errorf("getting Fulcio roots: %w", err) + } + co.IntermediateCerts, err = fulcio.GetIntermediates() + if err != nil { + return fmt.Errorf("getting Fulcio intermediates: %w", err) + } + } + + return nil +} diff --git a/cmd/cosign/cli/verify/verify.go b/cmd/cosign/cli/verify/verify.go index 4cc76ea88b8..3607466dfc8 100644 --- a/cmd/cosign/cli/verify/verify.go +++ b/cmd/cosign/cli/verify/verify.go @@ -16,13 +16,9 @@ package verify import ( - "bytes" "context" "crypto" - "crypto/x509" - "encoding/base64" "encoding/json" - "errors" "flag" "fmt" "os" @@ -30,24 +26,14 @@ import ( "github.com/google/go-containerregistry/pkg/name" "github.com/in-toto/in-toto-golang/in_toto" - "github.com/sigstore/cosign/v3/cmd/cosign/cli/fulcio" "github.com/sigstore/cosign/v3/cmd/cosign/cli/options" - "github.com/sigstore/cosign/v3/cmd/cosign/cli/rekor" "github.com/sigstore/cosign/v3/cmd/cosign/cli/sign" cosignError "github.com/sigstore/cosign/v3/cmd/cosign/errors" - "github.com/sigstore/cosign/v3/internal/ui" - "github.com/sigstore/cosign/v3/pkg/blob" "github.com/sigstore/cosign/v3/pkg/cosign" - "github.com/sigstore/cosign/v3/pkg/cosign/env" - "github.com/sigstore/cosign/v3/pkg/cosign/pivkey" - "github.com/sigstore/cosign/v3/pkg/cosign/pkcs11key" "github.com/sigstore/cosign/v3/pkg/oci" "github.com/sigstore/cosign/v3/pkg/oci/static" sigs "github.com/sigstore/cosign/v3/pkg/signature" "github.com/sigstore/protobuf-specs/gen/pb-go/dsse" - "github.com/sigstore/sigstore-go/pkg/root" - "github.com/sigstore/sigstore/pkg/cryptoutils" - "github.com/sigstore/sigstore/pkg/signature" "github.com/sigstore/sigstore/pkg/signature/payload" ) @@ -155,171 +141,40 @@ func (c *VerifyCommand) Exec(ctx context.Context, images []string) (err error) { } } - if c.TrustedRootPath != "" { - co.TrustedMaterial, err = root.NewTrustedRootFromPath(c.TrustedRootPath) - if err != nil { - return fmt.Errorf("loading trusted root: %w", err) - } - } else if options.NOf(c.CertChain, c.CARoots, c.CAIntermediates, c.TSACertChainPath) == 0 && - env.Getenv(env.VariableSigstoreCTLogPublicKeyFile) == "" && - env.Getenv(env.VariableSigstoreRootFile) == "" && - env.Getenv(env.VariableSigstoreRekorPublicKey) == "" && - env.Getenv(env.VariableSigstoreTSACertificateFile) == "" { - // don't overrule the user's intentions if they provided their own keys - co.TrustedMaterial, err = cosign.TrustedRoot() - if err != nil { - ui.Warnf(ctx, "Could not fetch trusted_root.json from the TUF repository. Continuing with individual targets. Error from TUF: %v", err) - } + err = SetTrustedMaterial(ctx, c.TrustedRootPath, c.CertChain, c.CARoots, c.CAIntermediates, c.TSACertChainPath, co) + if err != nil { + return fmt.Errorf("setting trusted material: %w", err) } - if co.NewBundleFormat { - if c.CertRef != "" { - return fmt.Errorf("unsupported: certificate may not be provided using --certificate when using --new-bundle-format (cert must be in bundle)") - } - if c.CertChain != "" { - return fmt.Errorf("unsupported: certificate chain may not be provided using --certificate-chain when using --new-bundle-format (cert must be in bundle)") - } - if c.CARoots != "" || c.CAIntermediates != "" { - return fmt.Errorf("unsupported: CA roots/intermediates must be provided using --trusted-root when using --new-bundle-format") - } - if c.TSACertChainPath != "" { - return fmt.Errorf("unsupported: TSA certificate chain path may only be provided using --trusted-root when using --new-bundle-format") - } - if co.TrustedMaterial == nil { - return fmt.Errorf("trusted root is required when using new bundle format") - } + if err = CheckSigstoreBundleUnsupportedOptions(*c, co); err != nil { + return err } if c.CheckClaims { co.ClaimVerifier = cosign.SimpleClaimVerifier } - // If we are using signed timestamps and there is no trusted root, we need to load the TSA certificates - if co.UseSignedTimestamps && co.TrustedMaterial == nil && !co.NewBundleFormat { - tsaCertificates, err := cosign.GetTSACerts(ctx, c.TSACertChainPath, cosign.GetTufTargets) - if err != nil { - return fmt.Errorf("unable to load TSA certificates: %w", err) - } - co.TSACertificate = tsaCertificates.LeafCert - co.TSARootCertificates = tsaCertificates.RootCert - co.TSAIntermediateCertificates = tsaCertificates.IntermediateCerts + err = SetLegacyClientsAndKeys(ctx, c.IgnoreTlog, shouldVerifySCT(c.IgnoreSCT, c.KeyRef, c.Sk), keylessVerification(c.KeyRef, c.Sk), c.RekorURL, c.TSACertChainPath, c.CertChain, c.CARoots, c.CAIntermediates, co) + if err != nil { + return fmt.Errorf("setting up clients and keys: %w", err) } - if !c.IgnoreTlog && !co.NewBundleFormat { - if c.RekorURL != "" { - rekorClient, err := rekor.NewClient(c.RekorURL) - if err != nil { - return fmt.Errorf("creating Rekor client: %w", err) - } - co.RekorClient = rekorClient - } - if co.TrustedMaterial == nil { - // This performs an online fetch of the Rekor public keys, but this is needed - // for verifying tlog entries (both online and offline). - co.RekorPubKeys, err = cosign.GetRekorPubs(ctx) - if err != nil { - return fmt.Errorf("getting Rekor public keys: %w", err) - } - } - } - if co.TrustedMaterial == nil && keylessVerification(c.KeyRef, c.Sk) { - if err := loadCertsKeylessVerification(c.CertChain, c.CARoots, c.CAIntermediates, co); err != nil { - return err - } + // Keys are optional! + var closeSV func() + co.SigVerifier, _, closeSV, err = LoadVerifierFromKeyOrCert(ctx, c.KeyRef, c.Slot, c.CertRef, c.CertChain, c.HashAlgorithm, c.Sk, false, co) + if err != nil { + return fmt.Errorf("loading verifier from key opts: %w", err) } + defer closeSV() - keyRef := c.KeyRef - certRef := c.CertRef - - // Ignore Signed Certificate Timestamp if the flag is set or a key is provided - if co.TrustedMaterial == nil && shouldVerifySCT(c.IgnoreSCT, c.KeyRef, c.Sk) { - co.CTLogPubKeys, err = cosign.GetCTLogPubs(ctx) + if c.CertRef != "" && c.SCTRef != "" { + sct, err := os.ReadFile(filepath.Clean(c.SCTRef)) if err != nil { - return fmt.Errorf("getting ctlog public keys: %w", err) + return fmt.Errorf("reading sct from file: %w", err) } + co.SCT = sct } - // Keys are optional! - var pubKey signature.Verifier - switch { - case keyRef != "": - pubKey, err = sigs.PublicKeyFromKeyRefWithHashAlgo(ctx, keyRef, c.HashAlgorithm) - if err != nil { - return fmt.Errorf("loading public key: %w", err) - } - pkcs11Key, ok := pubKey.(*pkcs11key.Key) - if ok { - defer pkcs11Key.Close() - } - case c.Sk: - sk, err := pivkey.GetKeyWithSlot(c.Slot) - if err != nil { - return fmt.Errorf("opening piv token: %w", err) - } - defer sk.Close() - pubKey, err = sk.Verifier() - if err != nil { - return fmt.Errorf("initializing piv token verifier: %w", err) - } - case certRef != "": - if co.NewBundleFormat { - // This shouldn't happen because we already checked for this above in checkSigstoreBundleUnsupportedOptions - return fmt.Errorf("unsupported: certificate reference currently not supported with --new-bundle-format") - } - cert, err := loadCertFromFileOrURL(c.CertRef) - if err != nil { - return err - } - switch { - case c.CertChain == "" && co.RootCerts == nil: - // If no certChain and no CARoots are passed, the Fulcio root certificate will be used - if co.TrustedMaterial == nil { - co.RootCerts, err = fulcio.GetRoots() - if err != nil { - return fmt.Errorf("getting Fulcio roots: %w", err) - } - co.IntermediateCerts, err = fulcio.GetIntermediates() - if err != nil { - return fmt.Errorf("getting Fulcio intermediates: %w", err) - } - } - pubKey, err = cosign.ValidateAndUnpackCert(cert, co) - if err != nil { - return err - } - case c.CertChain != "": - // Verify certificate with chain - chain, err := loadCertChainFromFileOrURL(c.CertChain) - if err != nil { - return err - } - pubKey, err = cosign.ValidateAndUnpackCertWithChain(cert, chain, co) - if err != nil { - return err - } - case co.RootCerts != nil: - // Verify certificate with root (and if given, intermediate) certificate - pubKey, err = cosign.ValidateAndUnpackCert(cert, co) - if err != nil { - return err - } - default: - return errors.New("no certificate chain provided to verify certificate") - } - - if c.SCTRef != "" { - sct, err := os.ReadFile(filepath.Clean(c.SCTRef)) - if err != nil { - return fmt.Errorf("reading sct from file: %w", err) - } - co.SCT = sct - } - default: - // Do nothing. Neither keyRef, c.Sk, nor certRef were set - can happen for example when using Fulcio and TSA. - // For an example see the TestAttachWithRFC3161Timestamp test in test/e2e_test.go. - } - co.SigVerifier = pubKey - // NB: There are only 2 kinds of verification right now: // 1. You gave us the public key explicitly to verify against so co.SigVerifier is non-nil or, // 2. We’re going to find an x509 certificate on the signature and verify against @@ -383,276 +238,6 @@ func (c *VerifyCommand) Exec(ctx context.Context, images []string) (err error) { return nil } -func PrintVerificationHeader(ctx context.Context, imgRef string, co *cosign.CheckOpts, bundleVerified, fulcioVerified bool) { - ui.Infof(ctx, "\nVerification for %s --", imgRef) - ui.Infof(ctx, "The following checks were performed on each of these signatures:") - if co.ClaimVerifier != nil { - if co.Annotations != nil { - ui.Infof(ctx, " - The specified annotations were verified.") - } - ui.Infof(ctx, " - The cosign claims were validated") - } - if bundleVerified { - ui.Infof(ctx, " - Existence of the claims in the transparency log was verified offline") - } else if co.RekorClient != nil { - ui.Infof(ctx, " - The claims were present in the transparency log") - ui.Infof(ctx, " - The signatures were integrated into the transparency log when the certificate was valid") - } - if co.SigVerifier != nil { - ui.Infof(ctx, " - The signatures were verified against the specified public key") - } - if fulcioVerified { - ui.Infof(ctx, " - The code-signing certificate was verified using trusted certificate authority certificates") - } -} - -// PrintVerification logs details about the verification to stdout -func PrintVerification(ctx context.Context, verified []oci.Signature, output string) { - switch output { - case "text": - for _, sig := range verified { - if cert, err := sig.Cert(); err == nil && cert != nil { - ce := cosign.CertExtensions{Cert: cert} - sub := "" - if sans := cryptoutils.GetSubjectAlternateNames(cert); len(sans) > 0 { - sub = sans[0] - } - ui.Infof(ctx, "Certificate subject: %s", sub) - if issuerURL := ce.GetIssuer(); issuerURL != "" { - ui.Infof(ctx, "Certificate issuer URL: %s", issuerURL) - } - - if githubWorkflowTrigger := ce.GetCertExtensionGithubWorkflowTrigger(); githubWorkflowTrigger != "" { - ui.Infof(ctx, "GitHub Workflow Trigger: %s", githubWorkflowTrigger) - } - - if githubWorkflowSha := ce.GetExtensionGithubWorkflowSha(); githubWorkflowSha != "" { - ui.Infof(ctx, "GitHub Workflow SHA: %s", githubWorkflowSha) - } - if githubWorkflowName := ce.GetCertExtensionGithubWorkflowName(); githubWorkflowName != "" { - ui.Infof(ctx, "GitHub Workflow Name: %s", githubWorkflowName) - } - - if githubWorkflowRepository := ce.GetCertExtensionGithubWorkflowRepository(); githubWorkflowRepository != "" { - ui.Infof(ctx, "GitHub Workflow Repository: %s", githubWorkflowRepository) - } - - if githubWorkflowRef := ce.GetCertExtensionGithubWorkflowRef(); githubWorkflowRef != "" { - ui.Infof(ctx, "GitHub Workflow Ref: %s", githubWorkflowRef) - } - } - - p, err := sig.Payload() - if err != nil { - fmt.Fprintf(os.Stderr, "Error fetching payload: %v", err) - return - } - fmt.Println(string(p)) - } - - default: - var outputKeys []payload.SimpleContainerImage - for _, sig := range verified { - p, err := sig.Payload() - if err != nil { - fmt.Fprintf(os.Stderr, "Error fetching payload: %v", err) - return - } - - ss := payload.SimpleContainerImage{} - if err := json.Unmarshal(p, &ss); err != nil { - fmt.Println("error decoding the payload:", err.Error()) - return - } - - if cert, err := sig.Cert(); err == nil && cert != nil { - ce := cosign.CertExtensions{Cert: cert} - if ss.Optional == nil { - ss.Optional = make(map[string]interface{}) - } - sub := "" - if sans := cryptoutils.GetSubjectAlternateNames(cert); len(sans) > 0 { - sub = sans[0] - } - ss.Optional["Subject"] = sub - if issuerURL := ce.GetIssuer(); issuerURL != "" { - ss.Optional["Issuer"] = issuerURL - ss.Optional[cosign.CertExtensionOIDCIssuer] = issuerURL - } - if githubWorkflowTrigger := ce.GetCertExtensionGithubWorkflowTrigger(); githubWorkflowTrigger != "" { - ss.Optional[cosign.CertExtensionMap[cosign.CertExtensionGithubWorkflowTrigger]] = githubWorkflowTrigger - ss.Optional[cosign.CertExtensionGithubWorkflowTrigger] = githubWorkflowTrigger - } - - if githubWorkflowSha := ce.GetExtensionGithubWorkflowSha(); githubWorkflowSha != "" { - ss.Optional[cosign.CertExtensionMap[cosign.CertExtensionGithubWorkflowSha]] = githubWorkflowSha - ss.Optional[cosign.CertExtensionGithubWorkflowSha] = githubWorkflowSha - } - if githubWorkflowName := ce.GetCertExtensionGithubWorkflowName(); githubWorkflowName != "" { - ss.Optional[cosign.CertExtensionMap[cosign.CertExtensionGithubWorkflowName]] = githubWorkflowName - ss.Optional[cosign.CertExtensionGithubWorkflowName] = githubWorkflowName - } - - if githubWorkflowRepository := ce.GetCertExtensionGithubWorkflowRepository(); githubWorkflowRepository != "" { - ss.Optional[cosign.CertExtensionMap[cosign.CertExtensionGithubWorkflowRepository]] = githubWorkflowRepository - ss.Optional[cosign.CertExtensionGithubWorkflowRepository] = githubWorkflowRepository - } - - if githubWorkflowRef := ce.GetCertExtensionGithubWorkflowRef(); githubWorkflowRef != "" { - ss.Optional[cosign.CertExtensionMap[cosign.CertExtensionGithubWorkflowRef]] = githubWorkflowRef - ss.Optional[cosign.CertExtensionGithubWorkflowRef] = githubWorkflowRef - } - } - if bundle, err := sig.Bundle(); err == nil && bundle != nil { - if ss.Optional == nil { - ss.Optional = make(map[string]interface{}) - } - ss.Optional["Bundle"] = bundle - } - if rfc3161Timestamp, err := sig.RFC3161Timestamp(); err == nil && rfc3161Timestamp != nil { - if ss.Optional == nil { - ss.Optional = make(map[string]interface{}) - } - ss.Optional["RFC3161Timestamp"] = rfc3161Timestamp - } - - outputKeys = append(outputKeys, ss) - } - - b, err := json.Marshal(outputKeys) - if err != nil { - fmt.Println("error when generating the output:", err.Error()) - return - } - - fmt.Printf("\n%s\n", string(b)) - } -} - -func loadCertFromFileOrURL(path string) (*x509.Certificate, error) { - pems, err := blob.LoadFileOrURL(path) - if err != nil { - return nil, err - } - return loadCertFromPEM(pems) -} - -func loadCertFromPEM(pems []byte) (*x509.Certificate, error) { - 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") - } - return certs[0], nil -} - -func loadCertChainFromFileOrURL(path string) ([]*x509.Certificate, error) { - pems, err := blob.LoadFileOrURL(path) - if err != nil { - return nil, err - } - certs, err := cryptoutils.LoadCertificatesFromPEM(bytes.NewReader(pems)) - if err != nil { - return nil, err - } - return certs, nil -} - -func keylessVerification(keyRef string, sk bool) bool { - if keyRef != "" { - return false - } - if sk { - return false - } - return true -} - -func shouldVerifySCT(ignoreSCT bool, keyRef string, sk bool) bool { - if keyRef != "" { - return false - } - if sk { - return false - } - if ignoreSCT { - return false - } - return true -} - -// loadCertsKeylessVerification loads certificates provided as a certificate chain or CA roots + CA intermediate -// certificate files. If both certChain and caRootsFile are empty strings, the Fulcio roots are loaded. -// -// The co *cosign.CheckOpts is both input and output parameter - it gets updated -// with the root and intermediate certificates needed for verification. -func loadCertsKeylessVerification(certChainFile string, - caRootsFile string, - caIntermediatesFile string, - co *cosign.CheckOpts) error { - var err error - switch { - case certChainFile != "": - chain, err := loadCertChainFromFileOrURL(certChainFile) - if err != nil { - return err - } - co.RootCerts = x509.NewCertPool() - co.RootCerts.AddCert(chain[len(chain)-1]) - if len(chain) > 1 { - co.IntermediateCerts = x509.NewCertPool() - for _, cert := range chain[:len(chain)-1] { - co.IntermediateCerts.AddCert(cert) - } - } - case caRootsFile != "": - caRoots, err := loadCertChainFromFileOrURL(caRootsFile) - if err != nil { - return err - } - co.RootCerts = x509.NewCertPool() - if len(caRoots) > 0 { - for _, cert := range caRoots { - co.RootCerts.AddCert(cert) - } - } - if caIntermediatesFile != "" { - caIntermediates, err := loadCertChainFromFileOrURL(caIntermediatesFile) - if err != nil { - return err - } - if len(caIntermediates) > 0 { - co.IntermediateCerts = x509.NewCertPool() - for _, cert := range caIntermediates { - co.IntermediateCerts.AddCert(cert) - } - } - } - default: - // This performs an online fetch of the Fulcio roots from a TUF repository. - // This is needed for verifying keyless certificates (both online and offline). - co.RootCerts, err = fulcio.GetRoots() - if err != nil { - return fmt.Errorf("getting Fulcio roots: %w", err) - } - co.IntermediateCerts, err = fulcio.GetIntermediates() - if err != nil { - return fmt.Errorf("getting Fulcio intermediates: %w", err) - } - } - - return nil -} - func transformOutput(verified []oci.Signature, name string) (verifiedOutput []oci.Signature, err error) { for _, v := range verified { dssePayload, err := v.Payload() diff --git a/cmd/cosign/cli/verify/verify_attestation.go b/cmd/cosign/cli/verify/verify_attestation.go index cbcb19e4ef2..f1d3719eeeb 100644 --- a/cmd/cosign/cli/verify/verify_attestation.go +++ b/cmd/cosign/cli/verify/verify_attestation.go @@ -26,20 +26,13 @@ import ( "strings" "github.com/google/go-containerregistry/pkg/name" - "github.com/sigstore/cosign/v3/cmd/cosign/cli/fulcio" "github.com/sigstore/cosign/v3/cmd/cosign/cli/options" - "github.com/sigstore/cosign/v3/cmd/cosign/cli/rekor" "github.com/sigstore/cosign/v3/internal/ui" "github.com/sigstore/cosign/v3/pkg/cosign" "github.com/sigstore/cosign/v3/pkg/cosign/cue" - "github.com/sigstore/cosign/v3/pkg/cosign/env" - "github.com/sigstore/cosign/v3/pkg/cosign/pivkey" - "github.com/sigstore/cosign/v3/pkg/cosign/pkcs11key" "github.com/sigstore/cosign/v3/pkg/cosign/rego" "github.com/sigstore/cosign/v3/pkg/oci" "github.com/sigstore/cosign/v3/pkg/policy" - sigs "github.com/sigstore/cosign/v3/pkg/signature" - "github.com/sigstore/sigstore-go/pkg/root" ) // VerifyAttestationCommand verifies a signature on a supplied container image @@ -137,151 +130,34 @@ func (c *VerifyAttestationCommand) Exec(ctx context.Context, images []string) (e co.ClaimVerifier = cosign.IntotoSubjectClaimVerifier } - if c.TrustedRootPath != "" { - co.TrustedMaterial, err = root.NewTrustedRootFromPath(c.TrustedRootPath) - if err != nil { - return fmt.Errorf("loading trusted root: %w", err) - } - } else if options.NOf(c.CertChain, c.CARoots, c.CAIntermediates, c.TSACertChainPath) == 0 && - env.Getenv(env.VariableSigstoreCTLogPublicKeyFile) == "" && - env.Getenv(env.VariableSigstoreRootFile) == "" && - env.Getenv(env.VariableSigstoreRekorPublicKey) == "" && - env.Getenv(env.VariableSigstoreTSACertificateFile) == "" { - co.TrustedMaterial, err = cosign.TrustedRoot() - if err != nil { - ui.Warnf(ctx, "Could not fetch trusted_root.json from the TUF repository. Continuing with individual targets. Error from TUF: %v", err) - } - } - - if co.NewBundleFormat { - if err = checkSigstoreBundleUnsupportedOptions(c); err != nil { - return err - } - if co.TrustedMaterial == nil { - return fmt.Errorf("trusted root is required when using new bundle format") - } - } - - // Ignore Signed Certificate Timestamp if the flag is set or a key is provided - if co.TrustedMaterial == nil && shouldVerifySCT(c.IgnoreSCT, c.KeyRef, c.Sk) && !co.NewBundleFormat { - co.CTLogPubKeys, err = cosign.GetCTLogPubs(ctx) - if err != nil { - return fmt.Errorf("getting ctlog public keys: %w", err) - } + err = SetTrustedMaterial(ctx, c.TrustedRootPath, c.CertChain, c.CARoots, c.CAIntermediates, c.TSACertChainPath, co) + if err != nil { + return fmt.Errorf("setting trusted material: %w", err) } - // If we are using signed timestamps, we need to load the TSA certificates - if co.UseSignedTimestamps && co.TrustedMaterial == nil && !co.NewBundleFormat { - tsaCertificates, err := cosign.GetTSACerts(ctx, c.TSACertChainPath, cosign.GetTufTargets) - if err != nil { - return fmt.Errorf("unable to load TSA certificates: %w", err) - } - co.TSACertificate = tsaCertificates.LeafCert - co.TSARootCertificates = tsaCertificates.RootCert - co.TSAIntermediateCertificates = tsaCertificates.IntermediateCerts + if err = CheckSigstoreBundleUnsupportedOptions(*c, co); err != nil { + return err } - if !c.IgnoreTlog && !co.NewBundleFormat { - if c.RekorURL != "" { - rekorClient, err := rekor.NewClient(c.RekorURL) - if err != nil { - return fmt.Errorf("creating Rekor client: %w", err) - } - co.RekorClient = rekorClient - } - if co.TrustedMaterial == nil { - // This performs an online fetch of the Rekor public keys, but this is needed - // for verifying tlog entries (both online and offline). - co.RekorPubKeys, err = cosign.GetRekorPubs(ctx) - if err != nil { - return fmt.Errorf("getting Rekor public keys: %w", err) - } - } + err = SetLegacyClientsAndKeys(ctx, c.IgnoreTlog, shouldVerifySCT(c.IgnoreSCT, c.KeyRef, c.Sk), keylessVerification(c.KeyRef, c.Sk), c.RekorURL, c.TSACertChainPath, c.CertChain, c.CARoots, c.CAIntermediates, co) + if err != nil { + return fmt.Errorf("setting up clients and keys: %w", err) } - if co.TrustedMaterial == nil && keylessVerification(c.KeyRef, c.Sk) { - if err := loadCertsKeylessVerification(c.CertChain, c.CARoots, c.CAIntermediates, co); err != nil { - return err - } + // Keys are optional! + var closeSV func() + co.SigVerifier, _, closeSV, err = LoadVerifierFromKeyOrCert(ctx, c.KeyRef, c.Slot, c.CertRef, c.CertChain, c.HashAlgorithm, c.Sk, false, co) + if err != nil { + return fmt.Errorf("loading verifierfrom key opts: %w", err) } + defer closeSV() - keyRef := c.KeyRef - - // Keys are optional! - switch { - case keyRef != "": - co.SigVerifier, err = sigs.PublicKeyFromKeyRefWithHashAlgo(ctx, keyRef, c.HashAlgorithm) + if c.CertRef != "" && c.SCTRef != "" { + sct, err := os.ReadFile(filepath.Clean(c.SCTRef)) if err != nil { - return fmt.Errorf("loading public key: %w", err) - } - pkcs11Key, ok := co.SigVerifier.(*pkcs11key.Key) - if ok { - defer pkcs11Key.Close() + return fmt.Errorf("reading sct from file: %w", err) } - case c.Sk: - sk, err := pivkey.GetKeyWithSlot(c.Slot) - if err != nil { - return fmt.Errorf("opening piv token: %w", err) - } - defer sk.Close() - co.SigVerifier, err = sk.Verifier() - if err != nil { - return fmt.Errorf("initializing piv token verifier: %w", err) - } - case c.CertRef != "": - if co.NewBundleFormat { - // This shouldn't happen because we already checked for this above in checkSigstoreBundleUnsupportedOptions - return fmt.Errorf("unsupported: certificate reference currently not supported with --new-bundle-format") - } - cert, err := loadCertFromFileOrURL(c.CertRef) - if err != nil { - return fmt.Errorf("loading certificate from reference: %w", err) - } - if c.CertChain == "" { - // If no certChain is passed, the Fulcio root certificate will be used - if co.TrustedMaterial == nil { - co.RootCerts, err = fulcio.GetRoots() - if err != nil { - return fmt.Errorf("getting Fulcio roots: %w", err) - } - co.IntermediateCerts, err = fulcio.GetIntermediates() - if err != nil { - return fmt.Errorf("getting Fulcio intermediates: %w", err) - } - } - co.SigVerifier, err = cosign.ValidateAndUnpackCert(cert, co) - if err != nil { - return fmt.Errorf("creating certificate verifier: %w", err) - } - } else { - // Verify certificate with chain - chain, err := loadCertChainFromFileOrURL(c.CertChain) - if err != nil { - return err - } - co.SigVerifier, err = cosign.ValidateAndUnpackCertWithChain(cert, chain, co) - if err != nil { - return fmt.Errorf("creating certificate verifier: %w", err) - } - } - if c.SCTRef != "" { - sct, err := os.ReadFile(filepath.Clean(c.SCTRef)) - if err != nil { - return fmt.Errorf("reading sct from file: %w", err) - } - co.SCT = sct - } - case c.TrustedRootPath != "": - if !co.NewBundleFormat { - return fmt.Errorf("unsupported: trusted root path currently only supported with --new-bundle-format") - } - - // If a trusted root path is provided, we will use it to verify the bundle. - // Otherwise, the verifier will default to the public good instance. - // co.TrustedMaterial is already loaded from c.TrustedRootPath above, - case c.CARoots != "": - // CA roots + possible intermediates are already loaded into co.RootCerts with the call to - // loadCertsKeylessVerification above. + co.SCT = sct } // NB: There are only 2 kinds of verification right now: @@ -383,19 +259,3 @@ func (c *VerifyAttestationCommand) Exec(ctx context.Context, images []string) (e return nil } - -func checkSigstoreBundleUnsupportedOptions(c *VerifyAttestationCommand) error { - if c.CertRef != "" { - return fmt.Errorf("unsupported: certificate may not be provided using --certificate when using --new-bundle-format (cert must be in bundle)") - } - if c.CertChain != "" { - return fmt.Errorf("unsupported: certificate chain may not be provided using --certificate-chain when using --new-bundle-format (cert must be in bundle)") - } - if c.CARoots != "" || c.CAIntermediates != "" { - return fmt.Errorf("unsupported: CA roots/intermediates must be provided using --trusted-root when using --new-bundle-format") - } - if c.TSACertChainPath != "" { - return fmt.Errorf("unsupported: TSA certificate chain path may only be provided using --trusted-root when using --new-bundle-format") - } - return nil -} diff --git a/cmd/cosign/cli/verify/verify_blob.go b/cmd/cosign/cli/verify/verify_blob.go index dd06cfc5c33..bfe73b965f0 100644 --- a/cmd/cosign/cli/verify/verify_blob.go +++ b/cmd/cosign/cli/verify/verify_blob.go @@ -32,18 +32,13 @@ import ( "strings" "github.com/sigstore/cosign/v3/cmd/cosign/cli/options" - "github.com/sigstore/cosign/v3/cmd/cosign/cli/rekor" "github.com/sigstore/cosign/v3/internal/ui" "github.com/sigstore/cosign/v3/pkg/blob" "github.com/sigstore/cosign/v3/pkg/cosign" "github.com/sigstore/cosign/v3/pkg/cosign/bundle" - "github.com/sigstore/cosign/v3/pkg/cosign/env" - "github.com/sigstore/cosign/v3/pkg/cosign/pivkey" - "github.com/sigstore/cosign/v3/pkg/cosign/pkcs11key" "github.com/sigstore/cosign/v3/pkg/oci/static" sigs "github.com/sigstore/cosign/v3/pkg/signature" sgbundle "github.com/sigstore/sigstore-go/pkg/bundle" - "github.com/sigstore/sigstore-go/pkg/root" sgverify "github.com/sigstore/sigstore-go/pkg/verify" "github.com/sigstore/sigstore/pkg/cryptoutils" @@ -118,60 +113,24 @@ func (c *VerifyBlobCmd) Exec(ctx context.Context, blobRef string) error { } // Keys are optional! + var closeSV func() var cert *x509.Certificate - opts := make([]static.Option, 0) - switch { - case c.KeyRef != "": - co.SigVerifier, err = sigs.PublicKeyFromKeyRefWithHashAlgo(ctx, c.KeyRef, c.HashAlgorithm) - if err != nil { - return fmt.Errorf("loading public key: %w", err) - } - pkcs11Key, ok := co.SigVerifier.(*pkcs11key.Key) - if ok { - defer pkcs11Key.Close() - } - case c.Sk: - sk, err := pivkey.GetKeyWithSlot(c.Slot) - if err != nil { - return fmt.Errorf("opening piv token: %w", err) - } - defer sk.Close() - co.SigVerifier, err = sk.Verifier() - if err != nil { - return fmt.Errorf("loading public key from token: %w", err) - } - case c.CertRef != "": - cert, err = loadCertFromFileOrURL(c.CertRef) - if err != nil { - return err - } + co.SigVerifier, cert, closeSV, err = LoadVerifierFromKeyOrCert(ctx, c.KeyRef, c.Slot, c.CertRef, "", c.HashAlgorithm, c.Sk, true, co) + if err != nil { + return fmt.Errorf("loading verifier from key opts: %w", err) } + defer closeSV() - if c.TrustedRootPath != "" { - co.TrustedMaterial, err = root.NewTrustedRootFromPath(c.TrustedRootPath) - if err != nil { - return fmt.Errorf("loading trusted root: %w", err) - } - } else if options.NOf(c.CertChain, c.CARoots, c.CAIntermediates, c.TSACertChainPath) == 0 && - env.Getenv(env.VariableSigstoreCTLogPublicKeyFile) == "" && - env.Getenv(env.VariableSigstoreRootFile) == "" && - env.Getenv(env.VariableSigstoreRekorPublicKey) == "" && - env.Getenv(env.VariableSigstoreTSACertificateFile) == "" { - co.TrustedMaterial, err = cosign.TrustedRoot() - if err != nil { - ui.Warnf(ctx, "Could not fetch trusted_root.json from the TUF repository. Continuing with individual targets. Error from TUF: %v", err) - } + err = SetTrustedMaterial(ctx, c.TrustedRootPath, c.CertChain, c.CARoots, c.CAIntermediates, c.TSACertChainPath, co) + if err != nil { + return fmt.Errorf("setting trusted material: %w", err) } - if co.NewBundleFormat { - if options.NOf(c.RFC3161TimestampPath, c.TSACertChainPath, c.CertChain, c.CARoots, c.CAIntermediates, c.CertRef, c.SigRef, c.SCTRef) > 0 { - return fmt.Errorf("when using --new-bundle-format, please supply signed content with --bundle and verification content with --trusted-root") - } - - if co.TrustedMaterial == nil { - return fmt.Errorf("trusted root is required when using new bundle format") - } + if err = CheckSigstoreBundleUnsupportedOptions(*c, co); err != nil { + return err + } + if co.NewBundleFormat { bundle, err := sgbundle.LoadJSONFromPath(c.BundlePath) if err != nil { return err @@ -211,39 +170,13 @@ func (c *VerifyBlobCmd) Exec(ctx context.Context, blobRef string) error { } else if c.RFC3161TimestampPath == "" && co.UseSignedTimestamps { return fmt.Errorf("when specifying --use-signed-timestamps or --timestamp-certificate-chain, you must also specify --rfc3161-timestamp-path") } - if co.UseSignedTimestamps && co.TrustedMaterial == nil { - tsaCertificates, err := cosign.GetTSACerts(ctx, c.TSACertChainPath, cosign.GetTufTargets) - if err != nil { - return fmt.Errorf("unable to load TSA certificates: %w", err) - } - co.TSACertificate = tsaCertificates.LeafCert - co.TSARootCertificates = tsaCertificates.RootCert - co.TSAIntermediateCertificates = tsaCertificates.IntermediateCerts - } - if !c.IgnoreTlog { - if c.RekorURL != "" { - rekorClient, err := rekor.NewClient(c.RekorURL) - if err != nil { - return fmt.Errorf("creating Rekor client: %w", err) - } - co.RekorClient = rekorClient - } - if co.TrustedMaterial == nil { - // This performs an online fetch of the Rekor public keys, but this is needed - // for verifying tlog entries (both online and offline). - co.RekorPubKeys, err = cosign.GetRekorPubs(ctx) - if err != nil { - return fmt.Errorf("getting Rekor public keys: %w", err) - } - } + err = SetLegacyClientsAndKeys(ctx, c.IgnoreTlog, shouldVerifySCT(c.IgnoreSCT, c.KeyRef, c.Sk), keylessVerification(c.KeyRef, c.Sk), c.RekorURL, c.TSACertChainPath, c.CertChain, c.CARoots, c.CAIntermediates, co) + if err != nil { + return fmt.Errorf("setting up clients and keys: %w", err) } - if co.TrustedMaterial == nil && keylessVerification(c.KeyRef, c.Sk) { - if err := loadCertsKeylessVerification(c.CertChain, c.CARoots, c.CAIntermediates, co); err != nil { - return err - } - } + opts := make([]static.Option, 0) if c.BundlePath != "" { b, err := cosign.FetchLocalSignedPayloadFromPath(c.BundlePath) if err != nil { @@ -336,14 +269,6 @@ func (c *VerifyBlobCmd) Exec(ctx context.Context, blobRef string) error { opts = append(opts, static.WithCertChain(certPEM, chainPEM)) } - // Ignore Signed Certificate Timestamp if the flag is set or a key is provided - if co.TrustedMaterial == nil && shouldVerifySCT(c.IgnoreSCT, c.KeyRef, c.Sk) { - co.CTLogPubKeys, err = cosign.GetCTLogPubs(ctx) - if err != nil { - return fmt.Errorf("getting ctlog public keys: %w", err) - } - } - sig, err := base64signature(c.SigRef, c.BundlePath) if err != nil { return err diff --git a/cmd/cosign/cli/verify/verify_blob_attestation.go b/cmd/cosign/cli/verify/verify_blob_attestation.go index 84a8aa47eb2..8ca38177baf 100644 --- a/cmd/cosign/cli/verify/verify_blob_attestation.go +++ b/cmd/cosign/cli/verify/verify_blob_attestation.go @@ -30,21 +30,16 @@ import ( v1 "github.com/google/go-containerregistry/pkg/v1" "github.com/sigstore/cosign/v3/cmd/cosign/cli/options" - "github.com/sigstore/cosign/v3/cmd/cosign/cli/rekor" internal "github.com/sigstore/cosign/v3/internal/pkg/cosign" payloadsize "github.com/sigstore/cosign/v3/internal/pkg/cosign/payload/size" "github.com/sigstore/cosign/v3/internal/ui" "github.com/sigstore/cosign/v3/pkg/blob" "github.com/sigstore/cosign/v3/pkg/cosign" "github.com/sigstore/cosign/v3/pkg/cosign/bundle" - "github.com/sigstore/cosign/v3/pkg/cosign/env" - "github.com/sigstore/cosign/v3/pkg/cosign/pivkey" - "github.com/sigstore/cosign/v3/pkg/cosign/pkcs11key" "github.com/sigstore/cosign/v3/pkg/oci/static" "github.com/sigstore/cosign/v3/pkg/policy" sigs "github.com/sigstore/cosign/v3/pkg/signature" sgbundle "github.com/sigstore/sigstore-go/pkg/bundle" - "github.com/sigstore/sigstore-go/pkg/root" sgverify "github.com/sigstore/sigstore-go/pkg/verify" "github.com/sigstore/sigstore/pkg/cryptoutils" ) @@ -128,37 +123,13 @@ func (c *VerifyBlobAttestationCommand) Exec(ctx context.Context, artifactPath st } // Keys are optional! + var closeSV func() var cert *x509.Certificate - opts := make([]static.Option, 0) - switch { - case c.KeyRef != "": - co.SigVerifier, err = sigs.PublicKeyFromKeyRefWithHashAlgo(ctx, c.KeyRef, c.HashAlgorithm) - if err != nil { - return fmt.Errorf("loading public key: %w", err) - } - pkcs11Key, ok := co.SigVerifier.(*pkcs11key.Key) - if ok { - defer pkcs11Key.Close() - } - case c.Sk: - sk, err := pivkey.GetKeyWithSlot(c.Slot) - if err != nil { - return fmt.Errorf("opening piv token: %w", err) - } - defer sk.Close() - co.SigVerifier, err = sk.Verifier() - if err != nil { - return fmt.Errorf("loading public key from token: %w", err) - } - case c.CertRef != "": - cert, err = loadCertFromFileOrURL(c.CertRef) - if err != nil { - return err - } - case c.CARoots != "": - // CA roots + possible intermediates are already loaded into co.RootCerts with the call to - // loadCertsKeylessVerification above. + co.SigVerifier, cert, closeSV, err = LoadVerifierFromKeyOrCert(ctx, c.KeyRef, c.Slot, c.CertRef, "", c.HashAlgorithm, c.Sk, true, co) + if err != nil { + return fmt.Errorf("loading verifier from key opts: %w", err) } + defer closeSV() var h v1.Hash var digest []byte @@ -205,31 +176,16 @@ func (c *VerifyBlobAttestationCommand) Exec(ctx context.Context, artifactPath st co.ClaimVerifier = cosign.IntotoSubjectClaimVerifier } - if c.TrustedRootPath != "" { - co.TrustedMaterial, err = root.NewTrustedRootFromPath(c.TrustedRootPath) - if err != nil { - return fmt.Errorf("loading trusted root: %w", err) - } - } else if options.NOf(c.CertChain, c.CARoots, c.CAIntermediates, c.TSACertChainPath) == 0 && - env.Getenv(env.VariableSigstoreCTLogPublicKeyFile) == "" && - env.Getenv(env.VariableSigstoreRootFile) == "" && - env.Getenv(env.VariableSigstoreRekorPublicKey) == "" && - env.Getenv(env.VariableSigstoreTSACertificateFile) == "" { - co.TrustedMaterial, err = cosign.TrustedRoot() - if err != nil { - ui.Warnf(ctx, "Could not fetch trusted_root.json from the TUF repository. Continuing with individual targets. Error from TUF: %v", err) - } + err = SetTrustedMaterial(ctx, c.TrustedRootPath, c.CertChain, c.CARoots, c.CAIntermediates, c.TSACertChainPath, co) + if err != nil { + return fmt.Errorf("setting trusted material: %w", err) } - if co.NewBundleFormat { - if options.NOf(c.RFC3161TimestampPath, c.TSACertChainPath, c.CertChain, c.CARoots, c.CAIntermediates, c.CertRef, c.SCTRef) > 0 { - return fmt.Errorf("when using --new-bundle-format, please supply signed content with --bundle and verification content with --trusted-root") - } - - if co.TrustedMaterial == nil { - return fmt.Errorf("trusted root is required when using new bundle format") - } + if err = CheckSigstoreBundleUnsupportedOptions(*c, co); err != nil { + return err + } + if co.NewBundleFormat { bundle, err := sgbundle.LoadJSONFromPath(c.BundlePath) if err != nil { return err @@ -259,45 +215,10 @@ func (c *VerifyBlobAttestationCommand) Exec(ctx context.Context, artifactPath st } else if c.RFC3161TimestampPath == "" && co.UseSignedTimestamps { return fmt.Errorf("when specifying --use-signed-timestamps or --timestamp-certificate-chain, you must also specify --rfc3161-timestamp-path") } - if co.UseSignedTimestamps && co.TrustedMaterial == nil { - tsaCertificates, err := cosign.GetTSACerts(ctx, c.TSACertChainPath, cosign.GetTufTargets) - if err != nil { - return fmt.Errorf("unable to load TSA certificates: %w", err) - } - co.TSACertificate = tsaCertificates.LeafCert - co.TSARootCertificates = tsaCertificates.RootCert - co.TSAIntermediateCertificates = tsaCertificates.IntermediateCerts - } - if !c.IgnoreTlog { - if c.RekorURL != "" { - rekorClient, err := rekor.NewClient(c.RekorURL) - if err != nil { - return fmt.Errorf("creating Rekor client: %w", err) - } - co.RekorClient = rekorClient - } - if co.TrustedMaterial == nil { - // This performs an online fetch of the Rekor public keys, but this is needed - // for verifying tlog entries (both online and offline). - co.RekorPubKeys, err = cosign.GetRekorPubs(ctx) - if err != nil { - return fmt.Errorf("getting Rekor public keys: %w", err) - } - } - } - if co.TrustedMaterial == nil && keylessVerification(c.KeyRef, c.Sk) { - if err := loadCertsKeylessVerification(c.CertChain, c.CARoots, c.CAIntermediates, co); err != nil { - return err - } - } - - // Ignore Signed Certificate Timestamp if the flag is set or a key is provided - if co.TrustedMaterial == nil && shouldVerifySCT(c.IgnoreSCT, c.KeyRef, c.Sk) { - co.CTLogPubKeys, err = cosign.GetCTLogPubs(ctx) - if err != nil { - return fmt.Errorf("getting ctlog public keys: %w", err) - } + err = SetLegacyClientsAndKeys(ctx, c.IgnoreTlog, shouldVerifySCT(c.IgnoreSCT, c.KeyRef, c.Sk), keylessVerification(c.KeyRef, c.Sk), c.RekorURL, c.TSACertChainPath, c.CertChain, c.CARoots, c.CAIntermediates, co) + if err != nil { + return fmt.Errorf("setting up clients and keys: %w", err) } var encodedSig []byte @@ -308,6 +229,7 @@ func (c *VerifyBlobAttestationCommand) Exec(ctx context.Context, artifactPath st } } + opts := make([]static.Option, 0) if c.BundlePath != "" { b, err := cosign.FetchLocalSignedPayloadFromPath(c.BundlePath) if err != nil {