diff --git a/cmd/cosign/cli/verify/verify_blob.go b/cmd/cosign/cli/verify/verify_blob.go index bf97f9e2a86..25932f43f87 100644 --- a/cmd/cosign/cli/verify/verify_blob.go +++ b/cmd/cosign/cli/verify/verify_blob.go @@ -16,10 +16,12 @@ package verify import ( + "bytes" "context" "crypto" "crypto/x509" "encoding/base64" + "encoding/hex" "encoding/json" "errors" "fmt" @@ -27,6 +29,7 @@ import ( "io/fs" "os" "path/filepath" + "strings" "github.com/sigstore/cosign/v2/cmd/cosign/cli/options" "github.com/sigstore/cosign/v2/cmd/cosign/cli/rekor" @@ -38,6 +41,9 @@ import ( "github.com/sigstore/cosign/v2/pkg/cosign/pkcs11key" "github.com/sigstore/cosign/v2/pkg/oci/static" sigs "github.com/sigstore/cosign/v2/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" ) @@ -81,22 +87,6 @@ func (c *VerifyBlobCmd) Exec(ctx context.Context, blobRef string) error { return &options.PubKeyParseError{} } - if c.KeyOpts.NewBundleFormat || checkNewBundle(c.BundlePath) { - if options.NOf(c.RFC3161TimestampPath, c.TSACertChainPath, c.RekorURL, c.CertChain, c.CARoots, c.CAIntermediates, c.CertRef, c.SigRef, c.SCTRef) > 1 { - return fmt.Errorf("when using --new-bundle-format, please supply signed content with --bundle and verification content with --trusted-root") - } - _, err := verifyNewBundle(ctx, c.BundlePath, c.TrustedRootPath, c.KeyRef, c.Slot, c.CertVerifyOptions.CertOidcIssuer, c.CertVerifyOptions.CertOidcIssuerRegexp, c.CertVerifyOptions.CertIdentity, c.CertVerifyOptions.CertIdentityRegexp, c.CertGithubWorkflowTrigger, c.CertGithubWorkflowSHA, c.CertGithubWorkflowName, c.CertGithubWorkflowRepository, c.CertGithubWorkflowRef, blobRef, c.Sk, c.IgnoreTlog, c.UseSignedTimestamps, c.IgnoreSCT) - if err == nil { - ui.Infof(ctx, "Verified OK") - } - return err - } else if c.TrustedRootPath != "" { - return fmt.Errorf("--trusted-root only supported with --new-bundle-format") - } - - var cert *x509.Certificate - opts := make([]static.Option, 0) - var identities []cosign.Identity var err error if c.KeyRef == "" { @@ -106,16 +96,6 @@ func (c *VerifyBlobCmd) Exec(ctx context.Context, blobRef string) error { } } - sig, err := base64signature(c.SigRef, c.BundlePath) - if err != nil { - return err - } - - blobBytes, err := payloadBytes(blobRef) - if err != nil { - return err - } - co := &cosign.CheckOpts{ CertGithubWorkflowTrigger: c.CertGithubWorkflowTrigger, CertGithubWorkflowSha: c.CertGithubWorkflowSHA, @@ -127,8 +107,85 @@ func (c *VerifyBlobCmd) Exec(ctx context.Context, blobRef string) error { Offline: c.Offline, IgnoreTlog: c.IgnoreTlog, UseSignedTimestamps: c.TSACertChainPath != "" || c.UseSignedTimestamps, + NewBundleFormat: c.KeyOpts.NewBundleFormat || checkNewBundle(c.BundlePath), + } + + // Keys are optional! + var cert *x509.Certificate + opts := make([]static.Option, 0) + switch { + case c.KeyRef != "": + co.SigVerifier, err = sigs.PublicKeyFromKeyRef(ctx, c.KeyRef) + 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 + } + } + + 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 { + co.TrustedMaterial, err = loadTrustedRoot(ctx, c.TrustedRootPath) + if err != nil { + return err + } + } + + bundle, err := sgbundle.LoadJSONFromPath(c.BundlePath) + if err != nil { + return err + } + + var artifactPolicyOption sgverify.ArtifactPolicyOption + blobBytes, err := payloadBytes(blobRef) + if err != nil { + alg, digest, payloadDigestError := payloadDigest(blobRef) + if payloadDigestError != nil { + return err + } + artifactPolicyOption = sgverify.WithArtifactDigest(alg, digest) + } else { + artifactPolicyOption = sgverify.WithArtifact(bytes.NewReader(blobBytes)) + } + + _, err = cosign.VerifyNewBundle(ctx, co, artifactPolicyOption, bundle) + if err != nil { + return err + } + + ui.Infof(ctx, "Verified OK") + return nil + } + + blobBytes, err := payloadBytes(blobRef) + if err != nil { + return err } + if c.TrustedRootPath != "" { + return fmt.Errorf("--trusted-root only supported with --new-bundle-format") + } if c.RFC3161TimestampPath != "" && !co.UseSignedTimestamps { return fmt.Errorf("when specifying --rfc3161-timestamp-path, you must also specify --use-signed-timestamps or --timestamp-certificate-chain") } else if c.RFC3161TimestampPath == "" && co.UseSignedTimestamps { @@ -165,34 +222,6 @@ func (c *VerifyBlobCmd) Exec(ctx context.Context, blobRef string) error { return err } } - - // Keys are optional! - switch { - case c.KeyRef != "": - co.SigVerifier, err = sigs.PublicKeyFromKeyRef(ctx, c.KeyRef) - 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 - } - } if c.BundlePath != "" { b, err := cosign.FetchLocalSignedPayloadFromPath(c.BundlePath) if err != nil { @@ -293,6 +322,10 @@ func (c *VerifyBlobCmd) Exec(ctx context.Context, blobRef string) error { } } + sig, err := base64signature(c.SigRef, c.BundlePath) + if err != nil { + return err + } signature, err := static.NewSignature(blobBytes, sig, opts...) if err != nil { return err @@ -348,3 +381,24 @@ func payloadBytes(blobRef string) ([]byte, error) { } return blobBytes, nil } + +func payloadDigest(blobRef string) (string, []byte, error) { + hexAlg, hexDigest, ok := strings.Cut(blobRef, ":") + if !ok { + return "", nil, fmt.Errorf("invalid digest format") + } + digestBytes, err := hex.DecodeString(hexDigest) + if err != nil { + return "", nil, err + } + return hexAlg, digestBytes, nil +} + +func loadTrustedRoot(_ context.Context, trustedRootPath string) (*root.TrustedRoot, error) { + if trustedRootPath != "" { + return root.NewTrustedRootFromPath(trustedRootPath) + } + // Assume we're using public good instance; fetch via TUF + // TODO: allow custom TUF settings + return root.FetchTrustedRoot() +} diff --git a/cmd/cosign/cli/verify/verify_blob_attestation.go b/cmd/cosign/cli/verify/verify_blob_attestation.go index 9dd7629ade0..3d52db71370 100644 --- a/cmd/cosign/cli/verify/verify_blob_attestation.go +++ b/cmd/cosign/cli/verify/verify_blob_attestation.go @@ -34,6 +34,7 @@ import ( "github.com/sigstore/cosign/v2/cmd/cosign/cli/rekor" internal "github.com/sigstore/cosign/v2/internal/pkg/cosign" payloadsize "github.com/sigstore/cosign/v2/internal/pkg/cosign/payload/size" + "github.com/sigstore/cosign/v2/internal/ui" "github.com/sigstore/cosign/v2/pkg/blob" "github.com/sigstore/cosign/v2/pkg/cosign" "github.com/sigstore/cosign/v2/pkg/cosign/bundle" @@ -42,6 +43,8 @@ import ( "github.com/sigstore/cosign/v2/pkg/oci/static" "github.com/sigstore/cosign/v2/pkg/policy" sigs "github.com/sigstore/cosign/v2/pkg/signature" + sgbundle "github.com/sigstore/sigstore-go/pkg/bundle" + sgverify "github.com/sigstore/sigstore-go/pkg/verify" "github.com/sigstore/sigstore/pkg/cryptoutils" ) @@ -92,19 +95,6 @@ func (c *VerifyBlobAttestationCommand) Exec(ctx context.Context, artifactPath st return &options.KeyParseError{} } - if c.KeyOpts.NewBundleFormat || checkNewBundle(c.BundlePath) { - if options.NOf(c.RFC3161TimestampPath, c.TSACertChainPath, c.RekorURL, c.CertChain, c.CARoots, c.CAIntermediates, c.CertRef, c.SCTRef) > 1 { - return fmt.Errorf("when using --new-bundle-format, please supply signed content with --bundle and verification content with --trusted-root") - } - _, err = verifyNewBundle(ctx, c.BundlePath, c.TrustedRootPath, c.KeyRef, c.Slot, c.CertVerifyOptions.CertOidcIssuer, c.CertVerifyOptions.CertOidcIssuerRegexp, c.CertVerifyOptions.CertIdentity, c.CertVerifyOptions.CertIdentityRegexp, c.CertGithubWorkflowTrigger, c.CertGithubWorkflowSHA, c.CertGithubWorkflowName, c.CertGithubWorkflowRepository, c.CertGithubWorkflowRef, artifactPath, c.Sk, c.IgnoreTlog, c.UseSignedTimestamps, c.IgnoreSCT) - if err == nil { - fmt.Fprintln(os.Stderr, "Verified OK") - } - return err - } else if c.TrustedRootPath != "" { - return fmt.Errorf("--trusted-root only supported with --new-bundle-format") - } - var identities []cosign.Identity if c.KeyRef == "" { identities, err = c.Identities() @@ -124,8 +114,44 @@ func (c *VerifyBlobAttestationCommand) Exec(ctx context.Context, artifactPath st Offline: c.Offline, IgnoreTlog: c.IgnoreTlog, UseSignedTimestamps: c.TSACertChainPath != "" || c.UseSignedTimestamps, + NewBundleFormat: c.KeyOpts.NewBundleFormat || checkNewBundle(c.BundlePath), + } + + // Keys are optional! + var cert *x509.Certificate + opts := make([]static.Option, 0) + switch { + case c.KeyRef != "": + co.SigVerifier, err = sigs.PublicKeyFromKeyRef(ctx, c.KeyRef) + 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. } + var h v1.Hash + var digest []byte if c.CheckClaims { // Get the actual digest of the blob var payload internal.HashReader @@ -147,7 +173,7 @@ func (c *VerifyBlobAttestationCommand) Exec(ctx context.Context, artifactPath st if _, err := io.ReadAll(&payload); err != nil { return err } - digest := payload.Sum(nil) + digest = payload.Sum(nil) h = v1.Hash{ Hex: hex.EncodeToString(digest), Algorithm: "sha256", @@ -155,6 +181,35 @@ func (c *VerifyBlobAttestationCommand) Exec(ctx context.Context, artifactPath st co.ClaimVerifier = cosign.IntotoSubjectClaimVerifier } + 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 { + co.TrustedMaterial, err = loadTrustedRoot(ctx, c.TrustedRootPath) + if err != nil { + return err + } + } + + bundle, err := sgbundle.LoadJSONFromPath(c.BundlePath) + if err != nil { + return err + } + + _, err = cosign.VerifyNewBundle(ctx, co, sgverify.WithArtifactDigest(h.Algorithm, digest), bundle) + if err != nil { + return err + } + + ui.Infof(ctx, "Verified OK") + return nil + } + + if c.TrustedRootPath != "" { + return fmt.Errorf("--trusted-root only supported with --new-bundle-format") + } if c.RFC3161TimestampPath != "" && !co.UseSignedTimestamps { return fmt.Errorf("when specifying --rfc3161-timestamp-path, you must also specify --use-signed-timestamps or --timestamp-certificate-chain") } else if c.RFC3161TimestampPath == "" && co.UseSignedTimestamps { @@ -207,38 +262,6 @@ func (c *VerifyBlobAttestationCommand) Exec(ctx context.Context, artifactPath st } } - // Keys are optional! - var cert *x509.Certificate - opts := make([]static.Option, 0) - switch { - case c.KeyRef != "": - co.SigVerifier, err = sigs.PublicKeyFromKeyRef(ctx, c.KeyRef) - 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. - } if c.BundlePath != "" { b, err := cosign.FetchLocalSignedPayloadFromPath(c.BundlePath) if err != nil { diff --git a/cmd/cosign/cli/verify/verify_blob_test.go b/cmd/cosign/cli/verify/verify_blob_test.go index 6b1b127052c..bda8e375c10 100644 --- a/cmd/cosign/cli/verify/verify_blob_test.go +++ b/cmd/cosign/cli/verify/verify_blob_test.go @@ -327,7 +327,7 @@ func TestVerifyBlob(t *testing.T) { { name: "valid signature with public key - new bundle", blob: blobBytes, - signature: blobSignature, + signature: "", key: pubKeyBytes, bundlePath: makeLocalNewBundle(t, []byte(blobSignature), sha256.Sum256(blobBytes)), newBundle: true, @@ -337,12 +337,12 @@ func TestVerifyBlob(t *testing.T) { { name: "invalid signature with public key - new bundle", blob: blobBytes, - signature: otherSignature, + signature: "", key: pubKeyBytes, - bundlePath: makeLocalNewBundle(t, []byte(blobSignature), sha256.Sum256(blobBytes)), + bundlePath: makeLocalNewBundle(t, []byte(otherSignature), sha256.Sum256(blobBytes)), newBundle: true, skipTlogVerify: true, - shouldErr: false, + shouldErr: true, }, { name: "invalid signature with public key", @@ -624,7 +624,6 @@ func TestVerifyBlob(t *testing.T) { cmd.KeyOpts.TSACertChainPath = "" cmd.CertChain = "" } - err := cmd.Exec(context.Background(), blobPath) if (err != nil) != tt.shouldErr { t.Fatalf("verifyBlob()= %s, expected shouldErr=%t ", err, tt.shouldErr) diff --git a/cmd/cosign/cli/verify/verify_bundle.go b/cmd/cosign/cli/verify/verify_bundle.go index 9af34090576..3d876f9a5c5 100644 --- a/cmd/cosign/cli/verify/verify_bundle.go +++ b/cmd/cosign/cli/verify/verify_bundle.go @@ -21,12 +21,8 @@ import ( "crypto/sha256" "crypto/x509" "encoding/base64" - "encoding/hex" "encoding/json" "fmt" - "os" - "strings" - "time" "github.com/secure-systems-lab/go-securesystemslib/dsse" protobundle "github.com/sigstore/protobuf-specs/gen/pb-go/bundle/v1" @@ -36,169 +32,17 @@ import ( "github.com/sigstore/rekor/pkg/generated/client" "github.com/sigstore/rekor/pkg/tle" sgbundle "github.com/sigstore/sigstore-go/pkg/bundle" - "github.com/sigstore/sigstore-go/pkg/fulcio/certificate" - "github.com/sigstore/sigstore-go/pkg/root" - "github.com/sigstore/sigstore-go/pkg/verify" "github.com/sigstore/sigstore/pkg/cryptoutils" "github.com/sigstore/sigstore/pkg/signature" - "github.com/sigstore/cosign/v2/internal/ui" "github.com/sigstore/cosign/v2/pkg/cosign" - "github.com/sigstore/cosign/v2/pkg/cosign/pivkey" - sigs "github.com/sigstore/cosign/v2/pkg/signature" ) -type verifyTrustedMaterial struct { - root.TrustedMaterial - keyTrustedMaterial root.TrustedMaterial -} - -func (v *verifyTrustedMaterial) PublicKeyVerifier(hint string) (root.TimeConstrainedVerifier, error) { - return v.keyTrustedMaterial.PublicKeyVerifier(hint) -} - func checkNewBundle(bundlePath string) bool { _, err := sgbundle.LoadJSONFromPath(bundlePath) return err == nil } -func verifyNewBundle(ctx context.Context, bundlePath, trustedRootPath, keyRef, slot, certOIDCIssuer, certOIDCIssuerRegex, certIdentity, certIdentityRegexp, githubWorkflowTrigger, githubWorkflowSHA, githubWorkflowName, githubWorkflowRepository, githubWorkflowRef, artifactRef string, sk, ignoreTlog, useSignedTimestamps, ignoreSCT bool) (*verify.VerificationResult, error) { - bundle, err := sgbundle.LoadJSONFromPath(bundlePath) - if err != nil { - return nil, err - } - - var trustedroot *root.TrustedRoot - - if trustedRootPath == "" { - ui.Infof(ctx, "no --trusted-root specified; fetching public good instance verification material via TUF") - // Assume we're using public good instance; fetch via TUF - trustedroot, err = root.FetchTrustedRoot() - if err != nil { - return nil, err - } - } else { - trustedroot, err = root.NewTrustedRootFromPath(trustedRootPath) - if err != nil { - return nil, err - } - } - - trustedmaterial := &verifyTrustedMaterial{TrustedMaterial: trustedroot} - - // See if we need to wrap trusted root with provided key - if keyRef != "" { - signatureVerifier, err := sigs.PublicKeyFromKeyRef(ctx, keyRef) - if err != nil { - return nil, err - } - - newExpiringKey := root.NewExpiringKey(signatureVerifier, time.Time{}, time.Time{}) - trustedmaterial.keyTrustedMaterial = root.NewTrustedPublicKeyMaterial(func(_ string) (root.TimeConstrainedVerifier, error) { - return newExpiringKey, nil - }) - } else if sk { - s, err := pivkey.GetKeyWithSlot(slot) - if err != nil { - return nil, fmt.Errorf("opening piv token: %w", err) - } - defer s.Close() - signatureVerifier, err := s.Verifier() - if err != nil { - return nil, fmt.Errorf("loading public key from token: %w", err) - } - - newExpiringKey := root.NewExpiringKey(signatureVerifier, time.Time{}, time.Time{}) - trustedmaterial.keyTrustedMaterial = root.NewTrustedPublicKeyMaterial(func(_ string) (root.TimeConstrainedVerifier, error) { - return newExpiringKey, nil - }) - } - - identityPolicies := []verify.PolicyOption{} - - verificationMaterial := bundle.GetVerificationMaterial() - - if verificationMaterial == nil { - return nil, fmt.Errorf("no verification material in bundle") - } - - if verificationMaterial.GetPublicKey() != nil { - identityPolicies = append(identityPolicies, verify.WithKey()) - } else { - sanMatcher, err := verify.NewSANMatcher(certIdentity, certIdentityRegexp) - if err != nil { - return nil, err - } - - issuerMatcher, err := verify.NewIssuerMatcher(certOIDCIssuer, certOIDCIssuerRegex) - if err != nil { - return nil, err - } - - extensions := certificate.Extensions{ - GithubWorkflowTrigger: githubWorkflowTrigger, - GithubWorkflowSHA: githubWorkflowSHA, - GithubWorkflowName: githubWorkflowName, - GithubWorkflowRepository: githubWorkflowRepository, - GithubWorkflowRef: githubWorkflowRef, - } - - certIdentity, err := verify.NewCertificateIdentity(sanMatcher, issuerMatcher, extensions) - if err != nil { - return nil, err - } - - identityPolicies = append(identityPolicies, verify.WithCertificateIdentity(certIdentity)) - } - - // Make some educated guesses about verification policy - verifierConfig := []verify.VerifierOption{} - - if len(trustedroot.RekorLogs()) > 0 && !ignoreTlog { - verifierConfig = append(verifierConfig, verify.WithTransparencyLog(1), verify.WithIntegratedTimestamps(1)) - } - - if len(trustedroot.TimestampingAuthorities()) > 0 && useSignedTimestamps { - verifierConfig = append(verifierConfig, verify.WithSignedTimestamps(1)) - } - - if !ignoreSCT { - verifierConfig = append(verifierConfig, verify.WithSignedCertificateTimestamps(1)) - } - - if ignoreTlog && !useSignedTimestamps { - verifierConfig = append(verifierConfig, verify.WithCurrentTime()) - } - - // Check if artifactRef is a digest or a file path - var artifactOpt verify.ArtifactPolicyOption - if _, err := os.Stat(artifactRef); err != nil { - hexAlg, hexDigest, ok := strings.Cut(artifactRef, ":") - if !ok { - return nil, err - } - digestBytes, err := hex.DecodeString(hexDigest) - if err != nil { - return nil, err - } - artifactOpt = verify.WithArtifactDigest(hexAlg, digestBytes) - } else { - // Perform verification - payload, err := payloadBytes(artifactRef) - if err != nil { - return nil, err - } - artifactOpt = verify.WithArtifact(bytes.NewBuffer(payload)) - } - - sev, err := verify.NewSignedEntityVerifier(trustedmaterial, verifierConfig...) - if err != nil { - return nil, err - } - - return sev.Verify(bundle, verify.NewPolicy(artifactOpt, identityPolicies...)) -} - func AssembleNewBundle(ctx context.Context, sigBytes, signedTimestamp []byte, envelope *dsse.Envelope, artifactRef string, cert *x509.Certificate, ignoreTlog bool, sigVerifier signature.Verifier, pkOpts []signature.PublicKeyOption, rekorClient *client.Rekor) (*sgbundle.Bundle, error) { payload, err := payloadBytes(artifactRef) if err != nil { diff --git a/cmd/cosign/cli/verify/verify_bundle_test.go b/cmd/cosign/cli/verify/verify_bundle_test.go deleted file mode 100644 index 667781c83c7..00000000000 --- a/cmd/cosign/cli/verify/verify_bundle_test.go +++ /dev/null @@ -1,107 +0,0 @@ -// -// Copyright 2024 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 ( - "context" - "crypto" - "crypto/ecdsa" - "crypto/elliptic" - "crypto/rand" - "crypto/sha256" - "crypto/x509" - "encoding/hex" - "encoding/pem" - "fmt" - "os" - "path/filepath" - "testing" - - "github.com/sigstore/cosign/v2/pkg/signature" -) - -func TestVerifyBundleWithKey(t *testing.T) { - // First assemble bundle - ctx := context.Background() - artifact := "hello world" - digest := sha256.Sum256([]byte(artifact)) - hexDigest := hex.EncodeToString(digest[:]) - - privateKey, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) - checkErr(t, err) - sigBytes, err := privateKey.Sign(rand.Reader, digest[:], crypto.SHA256) - checkErr(t, err) - - td := t.TempDir() - artifactPath := filepath.Join(td, "artifact") - err = os.WriteFile(artifactPath, []byte(artifact), 0600) - checkErr(t, err) - - pubKeyBytes, err := x509.MarshalPKIXPublicKey(&privateKey.PublicKey) - checkErr(t, err) - pemBlock := &pem.Block{ - Type: "PUBLIC KEY", - Bytes: pubKeyBytes, - } - verifier, err := signature.LoadPublicKeyRaw( - pem.EncodeToMemory(pemBlock), crypto.SHA256, - ) - checkErr(t, err) - - bundle, err := AssembleNewBundle(ctx, sigBytes, nil, nil, artifactPath, nil, - true, verifier, nil, nil, - ) - checkErr(t, err) - - if bundle == nil { - t.Fatal("invalid bundle") - } - - // The verify assembled bundle - trustedRootPath := filepath.Join(td, "trusted_root.json") - err = os.WriteFile(trustedRootPath, []byte(`{"mediaType":"application/vnd.dev.sigstore.trustedroot+json;version=0.1"}`), 0600) - checkErr(t, err) - - publicKeyPath := filepath.Join(td, "key.pub") - err = os.WriteFile(publicKeyPath, pem.EncodeToMemory(pemBlock), 0600) - checkErr(t, err) - - bundlePath := filepath.Join(td, "bundle.sigstore.json") - bundleBytes, err := bundle.MarshalJSON() - checkErr(t, err) - err = os.WriteFile(bundlePath, bundleBytes, 0600) - checkErr(t, err) - - result, err := verifyNewBundle(ctx, bundlePath, trustedRootPath, publicKeyPath, "", "", "", "", "", "", "", "", "", "", artifactPath, false, true, false, true) - checkErr(t, err) - - if result == nil { - t.Fatal("invalid verification result") - } - - result2, err := verifyNewBundle(ctx, bundlePath, trustedRootPath, publicKeyPath, "", "", "", "", "", "", "", "", "", "", fmt.Sprintf("sha256:%s", hexDigest), false, true, false, true) - checkErr(t, err) - - if result2 == nil { - t.Fatal("invalid verification result") - } -} - -func checkErr(t *testing.T, err error) { - if err != nil { - t.Fatal(err) - } -} diff --git a/pkg/cosign/verify.go b/pkg/cosign/verify.go index 3a6ee79b461..ea2ee1c5c59 100644 --- a/pkg/cosign/verify.go +++ b/pkg/cosign/verify.go @@ -43,8 +43,13 @@ import ( "github.com/sigstore/cosign/v2/internal/pkg/cosign" "github.com/sigstore/cosign/v2/pkg/blob" cbundle "github.com/sigstore/cosign/v2/pkg/cosign/bundle" + + "github.com/sigstore/sigstore-go/pkg/fulcio/certificate" + "github.com/sigstore/cosign/v2/pkg/oci/static" "github.com/sigstore/cosign/v2/pkg/types" + "github.com/sigstore/sigstore-go/pkg/root" + "github.com/sigstore/sigstore-go/pkg/verify" "github.com/cyberphone/json-canonicalization/go/src/webpki.org/jsoncanonicalizer" "github.com/google/go-containerregistry/pkg/name" @@ -165,6 +170,89 @@ type CheckOpts struct { // Should the experimental OCI 1.1 behaviour be enabled or not. // Defaults to false. ExperimentalOCI11 bool + + // NewBundleFormat enables the new bundle format (Cosign Bundle Spec) and the new verifier. + NewBundleFormat bool + + // TrustedMaterial is the trusted material to use for verification. + // Currently, this is only applicable when NewBundleFormat is true. + TrustedMaterial root.TrustedMaterial +} + +type verifyTrustedMaterial struct { + root.TrustedMaterial + keyTrustedMaterial root.TrustedMaterial +} + +func (v *verifyTrustedMaterial) PublicKeyVerifier(hint string) (root.TimeConstrainedVerifier, error) { + return v.keyTrustedMaterial.PublicKeyVerifier(hint) +} + +// verificationOptions returns the verification options for verifying with sigstore-go. +func (co *CheckOpts) verificationOptions() (trustedMaterial root.TrustedMaterial, verifierOptions []verify.VerifierOption, policyOptions []verify.PolicyOption, err error) { + policyOptions = make([]verify.PolicyOption, 0) + + if len(co.Identities) > 0 { + var sanMatcher verify.SubjectAlternativeNameMatcher + var issuerMatcher verify.IssuerMatcher + if len(co.Identities) > 1 { + return nil, nil, nil, fmt.Errorf("unsupported: multiple identities are not supported at this time") + } + sanMatcher, err = verify.NewSANMatcher(co.Identities[0].Subject, co.Identities[0].SubjectRegExp) + if err != nil { + return nil, nil, nil, err + } + + issuerMatcher, err = verify.NewIssuerMatcher(co.Identities[0].Issuer, co.Identities[0].IssuerRegExp) + if err != nil { + return nil, nil, nil, err + } + + extensions := certificate.Extensions{ + GithubWorkflowTrigger: co.CertGithubWorkflowTrigger, + GithubWorkflowSHA: co.CertGithubWorkflowSha, + GithubWorkflowName: co.CertGithubWorkflowName, + GithubWorkflowRepository: co.CertGithubWorkflowRepository, + GithubWorkflowRef: co.CertGithubWorkflowRef, + } + + certificateIdentities, err := verify.NewCertificateIdentity(sanMatcher, issuerMatcher, extensions) + if err != nil { + return nil, nil, nil, err + } + policyOptions = []verify.PolicyOption{verify.WithCertificateIdentity(certificateIdentities)} + } + + // Wrap TrustedMaterial + vTrustedMaterial := &verifyTrustedMaterial{TrustedMaterial: co.TrustedMaterial} + + verifierOptions = make([]verify.VerifierOption, 0) + + if co.SigVerifier != nil { + // We are verifying with a public key + policyOptions = append(policyOptions, verify.WithKey()) + newExpiringKey := root.NewExpiringKey(co.SigVerifier, time.Time{}, time.Time{}) + vTrustedMaterial.keyTrustedMaterial = root.NewTrustedPublicKeyMaterial(func(_ string) (root.TimeConstrainedVerifier, error) { + return newExpiringKey, nil + }) + } else { //nolint:gocritic + // We are verifying with a certificate + if !co.IgnoreSCT { + verifierOptions = append(verifierOptions, verify.WithSignedCertificateTimestamps(1)) + } + } + + if !co.IgnoreTlog { + verifierOptions = append(verifierOptions, verify.WithTransparencyLog(1), verify.WithIntegratedTimestamps(1)) + } + if co.UseSignedTimestamps { + verifierOptions = append(verifierOptions, verify.WithSignedTimestamps(1)) + } + if co.IgnoreTlog && !co.UseSignedTimestamps { + verifierOptions = append(verifierOptions, verify.WithCurrentTime()) + } + + return vTrustedMaterial, verifierOptions, policyOptions, nil } // This is a substitutable signature verification function that can be used for verifying diff --git a/pkg/cosign/verify_bundle.go b/pkg/cosign/verify_bundle.go new file mode 100644 index 00000000000..85a9a660283 --- /dev/null +++ b/pkg/cosign/verify_bundle.go @@ -0,0 +1,35 @@ +// +// 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 cosign + +import ( + "context" + + "github.com/sigstore/sigstore-go/pkg/verify" +) + +// VerifyNewBundle verifies a SigstoreBundle with the given parameters +func VerifyNewBundle(_ context.Context, co *CheckOpts, artifactPolicyOption verify.ArtifactPolicyOption, bundle verify.SignedEntity) (*verify.VerificationResult, error) { + trustedMaterial, verifierOptions, policyOptions, err := co.verificationOptions() + if err != nil { + return nil, err + } + verifier, err := verify.NewSignedEntityVerifier(trustedMaterial, verifierOptions...) + if err != nil { + return nil, err + } + return verifier.Verify(bundle, verify.NewPolicy(artifactPolicyOption, policyOptions...)) +} diff --git a/pkg/cosign/verify_bundle_test.go b/pkg/cosign/verify_bundle_test.go new file mode 100644 index 00000000000..a1c705c0362 --- /dev/null +++ b/pkg/cosign/verify_bundle_test.go @@ -0,0 +1,386 @@ +// +// 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 cosign_test + +import ( + "bytes" + "context" + "crypto" + "crypto/ecdsa" + "crypto/elliptic" + "crypto/rand" + "crypto/sha256" + "encoding/hex" + "fmt" + "testing" + + "github.com/sigstore/cosign/v2/pkg/cosign" + protobundle "github.com/sigstore/protobuf-specs/gen/pb-go/bundle/v1" + protocommon "github.com/sigstore/protobuf-specs/gen/pb-go/common/v1" + sgbundle "github.com/sigstore/sigstore-go/pkg/bundle" + "github.com/sigstore/sigstore-go/pkg/testing/ca" + "github.com/sigstore/sigstore-go/pkg/tlog" + "github.com/sigstore/sigstore-go/pkg/verify" + "github.com/sigstore/sigstore/pkg/signature" + "github.com/stretchr/testify/assert" +) + +type bundleMutator struct { + verify.SignedEntity + + eraseTSA bool + eraseTlog bool +} + +func (b *bundleMutator) Timestamps() ([][]byte, error) { + if b.eraseTSA { + return [][]byte{}, nil + } + return b.SignedEntity.Timestamps() +} + +func (b *bundleMutator) TlogEntries() ([]*tlog.Entry, error) { + if b.eraseTlog { + return []*tlog.Entry{}, nil + } + return b.SignedEntity.TlogEntries() +} + +func TestVerifyBundle(t *testing.T) { + virtualSigstore, err := ca.NewVirtualSigstore() + assert.NoError(t, err) + virtualSigstore2, err := ca.NewVirtualSigstore() // for testing invalid trusted material + assert.NoError(t, err) + + artifact := []byte("artifact") + digest := sha256.Sum256(artifact) + digestHex := hex.EncodeToString(digest[:]) + statementFmt := `{"_type":"https://in-toto.io/Statement/v0.1","predicateType":"https://example.com/predicateType","subject":[{"name":"subject","digest":{"sha256":"%s"}}],"predicate":{}}` + statementCorrect := []byte(fmt.Sprintf(statementFmt, digestHex)) + + identity := "foo@example.com" + issuer := "example issuer" + standardIdentities := []cosign.Identity{ + { + Issuer: issuer, + Subject: identity, + }, + } + + attestation, err := virtualSigstore.Attest(identity, issuer, statementCorrect) + if err != nil { + t.Fatal(err) + } + + blobSig, err := virtualSigstore.Sign(identity, issuer, artifact) + if err != nil { + t.Fatal(err) + } + + for _, tc := range []struct { + name string + checkOpts *cosign.CheckOpts + artifactPolicyOption verify.ArtifactPolicyOption + entity verify.SignedEntity + wantErr bool + }{ + { + name: "valid", + checkOpts: &cosign.CheckOpts{ + Identities: standardIdentities, + IgnoreSCT: true, + UseSignedTimestamps: true, + TrustedMaterial: virtualSigstore, + }, + artifactPolicyOption: verify.WithArtifact(bytes.NewReader(artifact)), + entity: attestation, + wantErr: false, + }, + { + name: "valid blob signature", + checkOpts: &cosign.CheckOpts{ + Identities: standardIdentities, + IgnoreSCT: true, + UseSignedTimestamps: true, + TrustedMaterial: virtualSigstore, + }, + artifactPolicyOption: verify.WithArtifact(bytes.NewReader(artifact)), + entity: blobSig, + wantErr: false, + }, + { + name: "invalid, wrong artifact", + checkOpts: &cosign.CheckOpts{ + Identities: standardIdentities, + IgnoreSCT: true, + UseSignedTimestamps: true, + TrustedMaterial: virtualSigstore, + }, + artifactPolicyOption: verify.WithArtifact(bytes.NewReader([]byte("not the artifact"))), + entity: attestation, + wantErr: true, + }, + { + name: "invalid blob signature, wrong artifact", + checkOpts: &cosign.CheckOpts{ + Identities: standardIdentities, + IgnoreSCT: true, + UseSignedTimestamps: true, + TrustedMaterial: virtualSigstore, + }, + artifactPolicyOption: verify.WithArtifact(bytes.NewReader([]byte("not the artifact"))), + entity: blobSig, + wantErr: true, + }, + { + name: "valid, pattern match issuer", + checkOpts: &cosign.CheckOpts{ + Identities: []cosign.Identity{ + { + IssuerRegExp: ".*issuer", + Subject: "foo@example.com", + }, + }, + IgnoreSCT: true, + UseSignedTimestamps: true, + TrustedMaterial: virtualSigstore, + }, + artifactPolicyOption: verify.WithArtifact(bytes.NewReader(artifact)), + entity: attestation, + wantErr: false, + }, + { + name: "valid, pattern match subject", + checkOpts: &cosign.CheckOpts{ + Identities: []cosign.Identity{ + { + Issuer: "example issuer", + SubjectRegExp: ".*@example.com", + }, + }, + IgnoreSCT: true, + UseSignedTimestamps: true, + TrustedMaterial: virtualSigstore, + }, + artifactPolicyOption: verify.WithArtifact(bytes.NewReader(artifact)), + entity: attestation, + wantErr: false, + }, + { + name: "invalid, pattern match issuer", + checkOpts: &cosign.CheckOpts{ + Identities: []cosign.Identity{ + { + IssuerRegExp: ".* not my issuer", + Subject: "foo@example.com", + }, + }, + IgnoreSCT: true, + UseSignedTimestamps: true, + TrustedMaterial: virtualSigstore, + }, + artifactPolicyOption: verify.WithArtifact(bytes.NewReader(artifact)), + entity: attestation, + wantErr: true, + }, + { + name: "invalid, pattern match subject", + checkOpts: &cosign.CheckOpts{ + Identities: []cosign.Identity{ + { + Issuer: "example issuer", + SubjectRegExp: ".*@otherexample.com", + }, + }, + IgnoreSCT: true, + UseSignedTimestamps: true, + TrustedMaterial: virtualSigstore, + }, + artifactPolicyOption: verify.WithArtifact(bytes.NewReader(artifact)), + entity: attestation, + wantErr: true, + }, + { + name: "invalid trusted material", + checkOpts: &cosign.CheckOpts{ + Identities: standardIdentities, + IgnoreSCT: true, + TrustedMaterial: virtualSigstore2, + }, + artifactPolicyOption: verify.WithArtifact(bytes.NewReader(artifact)), + entity: attestation, + wantErr: true, + }, + { + name: "do not require tlog, missing tlog", + checkOpts: &cosign.CheckOpts{ + Identities: standardIdentities, + IgnoreSCT: true, + IgnoreTlog: true, + UseSignedTimestamps: true, + TrustedMaterial: virtualSigstore, + }, + artifactPolicyOption: verify.WithArtifact(bytes.NewReader(artifact)), + entity: &bundleMutator{SignedEntity: attestation, eraseTlog: true}, + wantErr: false, + }, + { + name: "do not require tsa, missing tsa", + checkOpts: &cosign.CheckOpts{ + Identities: standardIdentities, + IgnoreSCT: true, + IgnoreTlog: false, + UseSignedTimestamps: false, + TrustedMaterial: virtualSigstore, + }, + artifactPolicyOption: verify.WithArtifact(bytes.NewReader(artifact)), + entity: &bundleMutator{SignedEntity: attestation, eraseTSA: true}, + wantErr: false, + }, + { + name: "require tlog, missing tlog", + checkOpts: &cosign.CheckOpts{ + Identities: standardIdentities, + IgnoreSCT: true, + UseSignedTimestamps: true, + TrustedMaterial: virtualSigstore, + }, + artifactPolicyOption: verify.WithArtifact(bytes.NewReader(artifact)), + entity: &bundleMutator{SignedEntity: attestation, eraseTlog: true}, + wantErr: true, + }, + { + name: "require tsa, missing tsa", + checkOpts: &cosign.CheckOpts{ + Identities: standardIdentities, + IgnoreSCT: true, + UseSignedTimestamps: true, + TrustedMaterial: virtualSigstore, + }, + artifactPolicyOption: verify.WithArtifact(bytes.NewReader(artifact)), + entity: &bundleMutator{SignedEntity: attestation, eraseTSA: true}, + wantErr: true, + }, + } { + t.Run(tc.name, func(t *testing.T) { + _, err = cosign.VerifyNewBundle(context.Background(), tc.checkOpts, tc.artifactPolicyOption, tc.entity) + if tc.wantErr { + assert.Error(t, err) + } else { + assert.NoError(t, err) + } + }) + } +} + +func TestVerifyBundleWithSigVerifier(t *testing.T) { + virtualSigstore, err := ca.NewVirtualSigstore() + assert.NoError(t, err) + + artifact := []byte("artifact") + digest := sha256.Sum256(artifact) + + privKey, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) + assert.NoError(t, err) + + sv, err := signature.LoadECDSASignerVerifier(privKey, crypto.SHA256) + assert.NoError(t, err) + + sig, err := sv.SignMessage(bytes.NewReader(artifact)) + assert.NoError(t, err) + assert.NotNil(t, sig) + + ts, err := virtualSigstore.TimestampResponse(sig) + assert.NoError(t, err) + + b, err := sgbundle.NewBundle(&protobundle.Bundle{ + MediaType: "application/vnd.dev.sigstore.bundle+json;version=0.3", + VerificationMaterial: &protobundle.VerificationMaterial{ + Content: &protobundle.VerificationMaterial_PublicKey{ + PublicKey: &protocommon.PublicKeyIdentifier{ + Hint: "", + }, + }, + TimestampVerificationData: &protobundle.TimestampVerificationData{ + Rfc3161Timestamps: []*protocommon.RFC3161SignedTimestamp{{SignedTimestamp: ts}}, + }, + }, + Content: &protobundle.Bundle_MessageSignature{ + MessageSignature: &protocommon.MessageSignature{ + MessageDigest: &protocommon.HashOutput{ + Algorithm: protocommon.HashAlgorithm_SHA2_256, + Digest: digest[:], + }, + Signature: sig, + }, + }, + }) + assert.NoError(t, err) + assert.NotNil(t, b) + + for _, tc := range []struct { + name string + checkOpts *cosign.CheckOpts + artifactPolicyOption verify.ArtifactPolicyOption + entity verify.SignedEntity + wantErr bool + }{ + { + name: "valid", + checkOpts: &cosign.CheckOpts{ + UseSignedTimestamps: true, + IgnoreTlog: true, + TrustedMaterial: virtualSigstore, + SigVerifier: sv, + }, + artifactPolicyOption: verify.WithArtifact(bytes.NewReader(artifact)), + entity: b, + wantErr: false, + }, + { + name: "invalid, wrong artifact", + checkOpts: &cosign.CheckOpts{ + UseSignedTimestamps: true, + IgnoreTlog: true, + TrustedMaterial: virtualSigstore, + SigVerifier: sv, + }, + artifactPolicyOption: verify.WithArtifact(bytes.NewReader([]byte("wrong artifact"))), + entity: b, + wantErr: true, + }, + { + name: "invalid, sigverifier not set", + checkOpts: &cosign.CheckOpts{ + UseSignedTimestamps: true, + IgnoreTlog: true, + TrustedMaterial: virtualSigstore, + }, + artifactPolicyOption: verify.WithArtifact(bytes.NewReader([]byte("wrong artifact"))), + entity: b, + wantErr: true, + }, + } { + t.Run(tc.name, func(t *testing.T) { + _, err = cosign.VerifyNewBundle(context.Background(), tc.checkOpts, tc.artifactPolicyOption, tc.entity) + if tc.wantErr { + assert.Error(t, err) + } else { + assert.NoError(t, err) + } + }) + } +}