From 140de08e1d20d293c5433d048332974cd6df75d0 Mon Sep 17 00:00:00 2001 From: Zach Steindler Date: Tue, 27 Aug 2024 09:57:06 -0400 Subject: [PATCH 1/4] For #3700: support trusted root in cosign verification We recently added partial trusted root support to cosign when you are verifying a protobuf bundle, but this did not cover the case where you aren't using a bundle. This implements trusted root support for those cases by assembling the disparate signed material into a bundle, fixing some TODOs from when we added protobuf bundle support. Signed-off-by: Zach Steindler --- cmd/conformance/main.go | 82 +------ cmd/cosign/cli/verify/verify_blob.go | 120 ++++++---- .../cli/verify/verify_blob_attestation.go | 225 ++++++++++-------- cmd/cosign/cli/verify/verify_bundle.go | 192 +++++++++++++-- 4 files changed, 376 insertions(+), 243 deletions(-) diff --git a/cmd/conformance/main.go b/cmd/conformance/main.go index 420e16bd631..f4cf0468e39 100644 --- a/cmd/conformance/main.go +++ b/cmd/conformance/main.go @@ -15,9 +15,7 @@ package main import ( - "crypto/sha256" "encoding/base64" - "encoding/pem" "fmt" "log" "os" @@ -25,10 +23,7 @@ import ( "path/filepath" "strings" - protobundle "github.com/sigstore/protobuf-specs/gen/pb-go/bundle/v1" - protocommon "github.com/sigstore/protobuf-specs/gen/pb-go/common/v1" "github.com/sigstore/sigstore-go/pkg/bundle" - "google.golang.org/protobuf/encoding/protojson" ) var bundlePath *string @@ -111,83 +106,12 @@ func main() { case "verify": args = append(args, "verify-blob") - // TODO: for now, we handle `verify` by constructing a bundle - // (see https://github.com/sigstore/cosign/issues/3700) - // - // Today cosign only supports `--trusted-root` with the new bundle - // format. When cosign supports `--trusted-root` with detached signed - // material, we can supply this content with `--certificate` - // and `--signature` instead. - fileBytes, err := os.ReadFile(os.Args[len(os.Args)-1]) - if err != nil { - log.Fatal(err) - } - - fileDigest := sha256.Sum256(fileBytes) - - pb := protobundle.Bundle{ - MediaType: "application/vnd.dev.sigstore.bundle+json;version=0.1", - } - - if signaturePath != nil { - sig, err := os.ReadFile(*signaturePath) - if err != nil { - log.Fatal(err) - } - - sigBytes, err := base64.StdEncoding.DecodeString(string(sig)) - if err != nil { - log.Fatal(err) - } - - pb.Content = &protobundle.Bundle_MessageSignature{ - MessageSignature: &protocommon.MessageSignature{ - MessageDigest: &protocommon.HashOutput{ - Algorithm: protocommon.HashAlgorithm_SHA2_256, - Digest: fileDigest[:], - }, - Signature: sigBytes, - }, - } - } if certPath != nil { - cert, err := os.ReadFile(*certPath) - if err != nil { - log.Fatal(err) - } - - pemCert, _ := pem.Decode(cert) - if pemCert == nil { - log.Fatalf("unable to load cerficate from %s", *certPath) - } - - signingCert := protocommon.X509Certificate{ - RawBytes: pemCert.Bytes, - } - - pb.VerificationMaterial = &protobundle.VerificationMaterial{ - Content: &protobundle.VerificationMaterial_X509CertificateChain{ - X509CertificateChain: &protocommon.X509CertificateChain{ - Certificates: []*protocommon.X509Certificate{&signingCert}, - }, - }, - } + args = append(args, "--certificate", *certPath) } - - bundleFile, err := os.CreateTemp(os.TempDir(), "bundle.sigstore.json") - if err != nil { - log.Fatal(err) - } - bundleFileName := bundleFile.Name() - pbBytes, err := protojson.Marshal(&pb) - if err != nil { - log.Fatal(err) - } - if err := os.WriteFile(bundleFileName, pbBytes, 0600); err != nil { - log.Fatal(err) + if signaturePath != nil { + args = append(args, "--signature", *signaturePath) } - bundlePath = &bundleFileName - args = append(args, "--insecure-ignore-tlog") case "verify-bundle": args = append(args, "verify-blob") diff --git a/cmd/cosign/cli/verify/verify_blob.go b/cmd/cosign/cli/verify/verify_blob.go index 79475c90d80..be72e5e73be 100644 --- a/cmd/cosign/cli/verify/verify_blob.go +++ b/cmd/cosign/cli/verify/verify_blob.go @@ -39,6 +39,7 @@ import ( "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/pkg/cryptoutils" ) @@ -96,33 +97,25 @@ func (c *VerifyBlobCmd) Exec(ctx context.Context, blobRef string) error { 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) + b, err := sgbundle.LoadJSONFromPath(c.BundlePath) + if err != nil { + return err + } + _, err = verifyNewBundle(ctx, b, 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 == "" { - identities, err = c.Identities() - if err != nil { - return err - } - } - sig, err := base64signature(c.SigRef, c.BundlePath) if err != nil { return err } - - blobBytes, err := payloadBytes(blobRef) + sigBytes, err := base64.StdEncoding.DecodeString(sig) if err != nil { return err } @@ -134,44 +127,9 @@ func (c *VerifyBlobCmd) Exec(ctx context.Context, blobRef string) error { CertGithubWorkflowRepository: c.CertGithubWorkflowRepository, CertGithubWorkflowRef: c.CertGithubWorkflowRef, IgnoreSCT: c.IgnoreSCT, - Identities: identities, Offline: c.Offline, IgnoreTlog: c.IgnoreTlog, } - if c.RFC3161TimestampPath != "" && !(c.TSACertChainPath != "" || c.UseSignedTimestamps) { - return fmt.Errorf("either TSA certificate chain path must be provided or use-signed-timestamps must be set when using RFC3161 timestamp path") - } - if c.TSACertChainPath != "" || c.UseSignedTimestamps { - tsaCertificates, err := c.loadTSACertificates(ctx) - if err != nil { - return 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 - } - // 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 keylessVerification(c.KeyRef, c.Sk) { - if err := loadCertsKeylessVerification(c.CertChain, c.CARoots, c.CAIntermediates, co); err != nil { - return err - } - } // Keys are optional! switch { @@ -200,6 +158,7 @@ func (c *VerifyBlobCmd) Exec(ctx context.Context, blobRef string) error { return err } } + if c.BundlePath != "" { b, err := cosign.FetchLocalSignedPayloadFromPath(c.BundlePath) if err != nil { @@ -234,8 +193,9 @@ func (c *VerifyBlobCmd) Exec(ctx context.Context, blobRef string) error { } opts = append(opts, static.WithBundle(b.Bundle)) } + + var rfc3161Timestamp bundle.RFC3161Timestamp if c.RFC3161TimestampPath != "" { - var rfc3161Timestamp bundle.RFC3161Timestamp ts, err := blob.LoadFileOrURL(c.RFC3161TimestampPath) if err != nil { return err @@ -245,6 +205,62 @@ func (c *VerifyBlobCmd) Exec(ctx context.Context, blobRef string) error { } opts = append(opts, static.WithRFC3161Timestamp(&rfc3161Timestamp)) } + + 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 + } + // 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 c.TrustedRootPath != "" { + b, err := assembleNewBundle(ctx, sigBytes, rfc3161Timestamp.SignedRFC3161Timestamp, nil, blobRef, cert, c.IgnoreTlog, co.SigVerifier, co.PKOpts, co.RekorClient) + if err != nil { + return err + } + + _, err = verifyNewBundle(ctx, b, 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 + } + + if c.KeyRef == "" { + co.Identities, err = c.Identities() + if err != nil { + return err + } + } + + if c.RFC3161TimestampPath != "" && !(c.TSACertChainPath != "" || c.UseSignedTimestamps) { + return fmt.Errorf("either TSA certificate chain path must be provided or use-signed-timestamps must be set when using RFC3161 timestamp path") + } + if c.TSACertChainPath != "" || c.UseSignedTimestamps { + tsaCertificates, err := c.loadTSACertificates(ctx) + if err != nil { + return err + } + co.TSACertificate = tsaCertificates.LeafCert + co.TSARootCertificates = tsaCertificates.RootCert + co.TSAIntermediateCertificates = tsaCertificates.IntermediateCerts + } + + if keylessVerification(c.KeyRef, c.Sk) { + if err := loadCertsKeylessVerification(c.CertChain, c.CARoots, c.CAIntermediates, co); err != nil { + return err + } + } + // Set an SCT if provided via the CLI. if c.SCTRef != "" { sct, err := os.ReadFile(filepath.Clean(c.SCTRef)) @@ -300,6 +316,10 @@ func (c *VerifyBlobCmd) Exec(ctx context.Context, blobRef string) error { } } + blobBytes, err := payloadBytes(blobRef) + if err != nil { + return err + } signature, err := static.NewSignature(blobBytes, sig, opts...) 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 3f2c33cc63b..c33be7c6498 100644 --- a/cmd/cosign/cli/verify/verify_blob_attestation.go +++ b/cmd/cosign/cli/verify/verify_blob_attestation.go @@ -30,6 +30,7 @@ import ( "path/filepath" v1 "github.com/google/go-containerregistry/pkg/v1" + "github.com/secure-systems-lab/go-securesystemslib/dsse" "github.com/sigstore/cosign/v2/cmd/cosign/cli/options" "github.com/sigstore/cosign/v2/cmd/cosign/cli/rekor" internal "github.com/sigstore/cosign/v2/internal/pkg/cosign" @@ -42,6 +43,7 @@ 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" "github.com/sigstore/sigstore/pkg/cryptoutils" ) @@ -96,107 +98,23 @@ func (c *VerifyBlobAttestationCommand) Exec(ctx context.Context, artifactPath st 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() + b, err := sgbundle.LoadJSONFromPath(c.BundlePath) if err != nil { return err } - } - - co := &cosign.CheckOpts{ - Identities: identities, - CertGithubWorkflowTrigger: c.CertGithubWorkflowTrigger, - CertGithubWorkflowSha: c.CertGithubWorkflowSHA, - CertGithubWorkflowName: c.CertGithubWorkflowName, - CertGithubWorkflowRepository: c.CertGithubWorkflowRepository, - CertGithubWorkflowRef: c.CertGithubWorkflowRef, - IgnoreSCT: c.IgnoreSCT, - Offline: c.Offline, - IgnoreTlog: c.IgnoreTlog, - } - var h v1.Hash - if c.CheckClaims { - // Get the actual digest of the blob - var payload internal.HashReader - f, err := os.Open(filepath.Clean(artifactPath)) + result, err := verifyNewBundle(ctx, b, 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 { return err } - defer f.Close() - fileInfo, err := f.Stat() - if err != nil { - return err - } - err = payloadsize.CheckSize(uint64(fileInfo.Size())) - if err != nil { - return err - } - - payload = internal.NewHashReader(f, sha256.New()) - if _, err := io.ReadAll(&payload); err != nil { - return err - } - digest := payload.Sum(nil) - h = v1.Hash{ - Hex: hex.EncodeToString(digest), - Algorithm: "sha256", - } - co.ClaimVerifier = cosign.IntotoSubjectClaimVerifier - } - - // Set up TSA, Fulcio roots and tlog public keys and clients. - if c.RFC3161TimestampPath != "" && !(c.TSACertChainPath != "" || c.UseSignedTimestamps) { - return fmt.Errorf("either TSA certificate chain path must be provided or use-signed-timestamps must be set when using RFC3161 timestamp path") - } - - if c.TSACertChainPath != "" || c.UseSignedTimestamps { - tsaCertificates, err := cosign.GetTSACerts(ctx, c.TSACertChainPath, cosign.GetTufTargets) - if err != nil { - return fmt.Errorf("unable to load or get 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 - } - // 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 keylessVerification(c.KeyRef, c.Sk) { - if err := loadCertsKeylessVerification(c.CertChain, c.CARoots, c.CAIntermediates, co); err != nil { - return err + if c.PredicateType != "" && result.Statement.GetPredicateType() != c.PredicateType { + return fmt.Errorf("invalid predicate type, expected %s got %s", c.PredicateType, result.Statement.GetPredicateType()) } + fmt.Fprintln(os.Stderr, "Verified OK") + return nil } - // Ignore Signed Certificate Timestamp if the flag is set or a key is provided - if 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) - } - } + var cert *x509.Certificate + opts := make([]static.Option, 0) var encodedSig []byte if c.SignaturePath != "" { @@ -206,9 +124,18 @@ func (c *VerifyBlobAttestationCommand) Exec(ctx context.Context, artifactPath st } } + co := &cosign.CheckOpts{ + CertGithubWorkflowTrigger: c.CertGithubWorkflowTrigger, + CertGithubWorkflowSha: c.CertGithubWorkflowSHA, + CertGithubWorkflowName: c.CertGithubWorkflowName, + CertGithubWorkflowRepository: c.CertGithubWorkflowRepository, + CertGithubWorkflowRef: c.CertGithubWorkflowRef, + IgnoreSCT: c.IgnoreSCT, + Offline: c.Offline, + IgnoreTlog: c.IgnoreTlog, + } + // Keys are optional! - var cert *x509.Certificate - opts := make([]static.Option, 0) switch { case c.KeyRef != "": co.SigVerifier, err = sigs.PublicKeyFromKeyRef(ctx, c.KeyRef) @@ -238,6 +165,7 @@ func (c *VerifyBlobAttestationCommand) Exec(ctx context.Context, artifactPath st // 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 { @@ -277,8 +205,9 @@ func (c *VerifyBlobAttestationCommand) Exec(ctx context.Context, artifactPath st } opts = append(opts, static.WithBundle(b.Bundle)) } + + var rfc3161Timestamp bundle.RFC3161Timestamp if c.RFC3161TimestampPath != "" { - var rfc3161Timestamp bundle.RFC3161Timestamp ts, err := blob.LoadFileOrURL(c.RFC3161TimestampPath) if err != nil { return err @@ -288,6 +217,112 @@ func (c *VerifyBlobAttestationCommand) Exec(ctx context.Context, artifactPath st } opts = append(opts, static.WithRFC3161Timestamp(&rfc3161Timestamp)) } + + 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 + } + // 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 c.TrustedRootPath != "" { + var envelope dsse.Envelope + err = json.Unmarshal(encodedSig, &envelope) + if err != nil { + return nil + } + + b, err := assembleNewBundle(ctx, encodedSig, rfc3161Timestamp.SignedRFC3161Timestamp, &envelope, artifactPath, cert, c.IgnoreTlog, co.SigVerifier, co.PKOpts, co.RekorClient) + if err != nil { + return err + } + + result, err := verifyNewBundle(ctx, b, 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 { + return err + } + if c.PredicateType != "" && result.Statement.GetPredicateType() != c.PredicateType { + return fmt.Errorf("invalid predicate type, expected %s got %s", c.PredicateType, result.Statement.GetPredicateType()) + } + fmt.Fprintln(os.Stderr, "Verified OK") + return nil + } + + if c.KeyRef == "" { + co.Identities, err = c.Identities() + if err != nil { + return err + } + } + + var h v1.Hash + if c.CheckClaims { + // Get the actual digest of the blob + var payload internal.HashReader + f, err := os.Open(filepath.Clean(artifactPath)) + if err != nil { + return err + } + defer f.Close() + fileInfo, err := f.Stat() + if err != nil { + return err + } + err = payloadsize.CheckSize(uint64(fileInfo.Size())) + if err != nil { + return err + } + + payload = internal.NewHashReader(f, sha256.New()) + if _, err := io.ReadAll(&payload); err != nil { + return err + } + digest := payload.Sum(nil) + h = v1.Hash{ + Hex: hex.EncodeToString(digest), + Algorithm: "sha256", + } + co.ClaimVerifier = cosign.IntotoSubjectClaimVerifier + } + + // Set up TSA, Fulcio roots and tlog public keys and clients. + if c.RFC3161TimestampPath != "" && !(c.TSACertChainPath != "" || c.UseSignedTimestamps) { + return fmt.Errorf("either TSA certificate chain path must be provided or use-signed-timestamps must be set when using RFC3161 timestamp path") + } + + if c.TSACertChainPath != "" || c.UseSignedTimestamps { + tsaCertificates, err := cosign.GetTSACerts(ctx, c.TSACertChainPath, cosign.GetTufTargets) + if err != nil { + return fmt.Errorf("unable to load or get TSA certificates: %w", err) + } + co.TSACertificate = tsaCertificates.LeafCert + co.TSARootCertificates = tsaCertificates.RootCert + co.TSAIntermediateCertificates = tsaCertificates.IntermediateCerts + } + + if 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 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) + } + } + // Set an SCT if provided via the CLI. if c.SCTRef != "" { sct, err := os.ReadFile(filepath.Clean(c.SCTRef)) diff --git a/cmd/cosign/cli/verify/verify_bundle.go b/cmd/cosign/cli/verify/verify_bundle.go index 01921be7ffb..71f6ff691ef 100644 --- a/cmd/cosign/cli/verify/verify_bundle.go +++ b/cmd/cosign/cli/verify/verify_bundle.go @@ -18,14 +18,28 @@ package verify import ( "bytes" "context" + "crypto/sha256" + "crypto/x509" + "encoding/base64" "fmt" "time" + "github.com/secure-systems-lab/go-securesystemslib/dsse" + protobundle "github.com/sigstore/protobuf-specs/gen/pb-go/bundle/v1" + protocommon "github.com/sigstore/protobuf-specs/gen/pb-go/common/v1" + protodsse "github.com/sigstore/protobuf-specs/gen/pb-go/dsse" + protorekor "github.com/sigstore/protobuf-specs/gen/pb-go/rekor/v1" + "github.com/sigstore/rekor/pkg/generated/client" + "github.com/sigstore/rekor/pkg/generated/models" + "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/pkg/cosign" "github.com/sigstore/cosign/v2/pkg/cosign/pivkey" sigs "github.com/sigstore/cosign/v2/pkg/signature" ) @@ -36,27 +50,26 @@ type verifyTrustedMaterial struct { } func (v *verifyTrustedMaterial) PublicKeyVerifier(hint string) (root.TimeConstrainedVerifier, error) { + if v.keyTrustedMaterial == nil { + return nil, fmt.Errorf("no key in trusted material to verify with") + } return v.keyTrustedMaterial.PublicKeyVerifier(hint) } -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) error { - bundle, err := sgbundle.LoadJSONFromPath(bundlePath) - if err != nil { - return err - } - +func verifyNewBundle(ctx context.Context, bundle *sgbundle.Bundle, trustedRootPath, keyRef, slot, certOIDCIssuer, certOIDCIssuerRegex, certIdentity, certIdentityRegexp, githubWorkflowTrigger, githubWorkflowSHA, githubWorkflowName, githubWorkflowRepository, githubWorkflowRef, artifactRef string, sk, ignoreTlog, useSignedTimestamps, ignoreSCT bool) (*verify.VerificationResult, error) { var trustedroot *root.TrustedRoot + var err error if trustedRootPath == "" { // Assume we're using public good instance; fetch via TUF trustedroot, err = root.FetchTrustedRoot() if err != nil { - return err + return nil, err } } else { trustedroot, err = root.NewTrustedRootFromPath(trustedRootPath) if err != nil { - return err + return nil, err } } @@ -66,7 +79,7 @@ func verifyNewBundle(ctx context.Context, bundlePath, trustedRootPath, keyRef, s if keyRef != "" { signatureVerifier, err := sigs.PublicKeyFromKeyRef(ctx, keyRef) if err != nil { - return err + return nil, err } newExpiringKey := root.NewExpiringKey(signatureVerifier, time.Time{}, time.Time{}) @@ -76,12 +89,12 @@ func verifyNewBundle(ctx context.Context, bundlePath, trustedRootPath, keyRef, s } else if sk { s, err := pivkey.GetKeyWithSlot(slot) if err != nil { - return fmt.Errorf("opening piv token: %w", err) + return nil, fmt.Errorf("opening piv token: %w", err) } defer s.Close() signatureVerifier, err := s.Verifier() if err != nil { - return fmt.Errorf("loading public key from token: %w", err) + return nil, fmt.Errorf("loading public key from token: %w", err) } newExpiringKey := root.NewExpiringKey(signatureVerifier, time.Time{}, time.Time{}) @@ -95,7 +108,7 @@ func verifyNewBundle(ctx context.Context, bundlePath, trustedRootPath, keyRef, s verificationMaterial := bundle.GetVerificationMaterial() if verificationMaterial == nil { - return fmt.Errorf("no verification material in bundle") + return nil, fmt.Errorf("no verification material in bundle") } if verificationMaterial.GetPublicKey() != nil { @@ -103,12 +116,12 @@ func verifyNewBundle(ctx context.Context, bundlePath, trustedRootPath, keyRef, s } else { sanMatcher, err := verify.NewSANMatcher(certIdentity, certIdentityRegexp) if err != nil { - return err + return nil, err } issuerMatcher, err := verify.NewIssuerMatcher(certOIDCIssuer, certOIDCIssuerRegex) if err != nil { - return err + return nil, err } extensions := certificate.Extensions{ @@ -121,7 +134,7 @@ func verifyNewBundle(ctx context.Context, bundlePath, trustedRootPath, keyRef, s certIdentity, err := verify.NewCertificateIdentity(sanMatcher, issuerMatcher, extensions) if err != nil { - return err + return nil, err } identityPolicies = append(identityPolicies, verify.WithCertificateIdentity(certIdentity)) @@ -149,15 +162,156 @@ func verifyNewBundle(ctx context.Context, bundlePath, trustedRootPath, keyRef, s // Perform verification payload, err := payloadBytes(artifactRef) if err != nil { - return err + return nil, err } buf := bytes.NewBuffer(payload) sev, err := verify.NewSignedEntityVerifier(trustedmaterial, verifierConfig...) if err != nil { - return err + return nil, err + } + + return sev.Verify(bundle, verify.NewPolicy(verify.WithArtifact(buf), 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 { + return nil, err + } + buf := bytes.NewBuffer(payload) + digest := sha256.Sum256(buf.Bytes()) + + pb := &protobundle.Bundle{ + MediaType: "application/vnd.dev.sigstore.bundle+json;version=0.3", + VerificationMaterial: &protobundle.VerificationMaterial{}, + } + + if envelope != nil && len(envelope.Signatures) > 0 { + sigDecode, err := base64.StdEncoding.DecodeString(envelope.Signatures[0].Sig) + if err != nil { + return nil, err + } + + sig := &protodsse.Signature{ + Sig: sigDecode, + } + + payloadDecode, err := base64.StdEncoding.DecodeString(envelope.Payload) + if err != nil { + return nil, err + } + + pb.Content = &protobundle.Bundle_DsseEnvelope{ + DsseEnvelope: &protodsse.Envelope{ + Payload: payloadDecode, + PayloadType: envelope.PayloadType, + Signatures: []*protodsse.Signature{sig}, + }, + } + } else { + pb.Content = &protobundle.Bundle_MessageSignature{ + MessageSignature: &protocommon.MessageSignature{ + MessageDigest: &protocommon.HashOutput{ + Algorithm: protocommon.HashAlgorithm_SHA2_256, + Digest: digest[:], + }, + Signature: sigBytes, + }, + } + } + + if cert != nil { + pb.VerificationMaterial.Content = &protobundle.VerificationMaterial_Certificate{ + Certificate: &protocommon.X509Certificate{ + RawBytes: cert.Raw, + }, + } + } else if sigVerifier != nil { + pub, err := sigVerifier.PublicKey(pkOpts...) + if err != nil { + return nil, err + } + pubKeyBytes, err := x509.MarshalPKIXPublicKey(pub) + if err != nil { + return nil, err + } + hashedBytes := sha256.Sum256(pubKeyBytes) + + pb.VerificationMaterial.Content = &protobundle.VerificationMaterial_PublicKey{ + PublicKey: &protocommon.PublicKeyIdentifier{ + Hint: base64.StdEncoding.EncodeToString(hashedBytes[:]), + }, + } + } + + if len(signedTimestamp) > 0 { + ts := &protocommon.RFC3161SignedTimestamp{ + SignedTimestamp: signedTimestamp, + } + + pb.VerificationMaterial.TimestampVerificationData = &protobundle.TimestampVerificationData{ + Rfc3161Timestamps: []*protocommon.RFC3161SignedTimestamp{ts}, + } + } + + if !ignoreTlog { + var pem []byte + var err error + if cert != nil { + pem, err = cryptoutils.MarshalCertificateToPEM(cert) + if err != nil { + return nil, err + } + } else if sigVerifier != nil { + pub, err := sigVerifier.PublicKey(pkOpts...) + if err != nil { + return nil, err + } + pem, err = cryptoutils.MarshalPublicKeyToPEM(pub) + if err != nil { + return nil, err + } + } + var sigB64 string + var payload []byte + if envelope != nil { + payload = sigBytes + } else { + sigB64 = base64.StdEncoding.EncodeToString(sigBytes) + payload = buf.Bytes() + } + + tlogEntries, err := cosign.FindTlogEntry(ctx, rekorClient, sigB64, payload, pem) + if err != nil { + return nil, err + } + if len(tlogEntries) == 0 { + return nil, fmt.Errorf("unable to find tlog entry") + } + // Attempt to verify with the earliest integrated entry + var earliestLogEntry models.LogEntryAnon + var earliestLogEntryTime *time.Time + for _, e := range tlogEntries { + entryTime := time.Unix(*e.IntegratedTime, 0) + if earliestLogEntryTime == nil || entryTime.Before(*earliestLogEntryTime) { + earliestLogEntryTime = &entryTime + earliestLogEntry = e + } + } + + tlogEntry, err := tle.GenerateTransparencyLogEntry(earliestLogEntry) + if err != nil { + return nil, err + } + + pb.VerificationMaterial.TlogEntries = []*protorekor.TransparencyLogEntry{tlogEntry} + } + + b, err := sgbundle.NewBundle(pb) + if err != nil { + return nil, err } - _, err = sev.Verify(bundle, verify.NewPolicy(verify.WithArtifact(buf), identityPolicies...)) - return err + return b, nil } From d38c01b1130e2849fe325d94c0df142aa7a1a657 Mon Sep 17 00:00:00 2001 From: Zach Steindler Date: Thu, 12 Sep 2024 17:04:55 -0400 Subject: [PATCH 2/4] Add happy path testing to cmd/cosign/cli/verify/verify_bundle.go Also remove fix that is being handled in #3877 Signed-off-by: Zach Steindler --- cmd/cosign/cli/verify/verify_bundle_test.go | 91 +++++++++++++++++++++ 1 file changed, 91 insertions(+) create mode 100644 cmd/cosign/cli/verify/verify_bundle_test.go diff --git a/cmd/cosign/cli/verify/verify_bundle_test.go b/cmd/cosign/cli/verify/verify_bundle_test.go new file mode 100644 index 00000000000..04dfc82334b --- /dev/null +++ b/cmd/cosign/cli/verify/verify_bundle_test.go @@ -0,0 +1,91 @@ +// +// 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/pem" + "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)) + + 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) + + result, err := verifyNewBundle(ctx, bundle, trustedRootPath, publicKeyPath, "", "", "", "", "", "", "", "", "", "", artifactPath, false, true, false, true) + checkErr(t, err) + + if result == nil { + t.Fatal("invalid verification result") + } +} + +func checkErr(t *testing.T, err error) { + if err != nil { + t.Fatal(err) + } +} From d3222eeb34217eb01861d190ad518850d2467f02 Mon Sep 17 00:00:00 2001 From: Zach Steindler Date: Mon, 23 Sep 2024 16:17:02 -0400 Subject: [PATCH 3/4] Reorganizing verify_bundle.go based on pull request feedback Also align with https://github.com/sigstore/cosign/issues/3879 Signed-off-by: Zach Steindler --- cmd/cosign/cli/verify/verify_blob.go | 82 ++++--- .../cli/verify/verify_blob_attestation.go | 85 +++++--- cmd/cosign/cli/verify/verify_bundle.go | 205 +++++++++--------- cmd/cosign/cli/verify/verify_bundle_test.go | 19 +- pkg/cosign/verify.go | 15 ++ 5 files changed, 242 insertions(+), 164 deletions(-) diff --git a/cmd/cosign/cli/verify/verify_blob.go b/cmd/cosign/cli/verify/verify_blob.go index be72e5e73be..08eb4c8aeae 100644 --- a/cmd/cosign/cli/verify/verify_blob.go +++ b/cmd/cosign/cli/verify/verify_blob.go @@ -40,6 +40,7 @@ import ( sigs "github.com/sigstore/cosign/v2/pkg/signature" sgbundle "github.com/sigstore/sigstore-go/pkg/bundle" + "github.com/sigstore/sigstore-go/pkg/root" "github.com/sigstore/sigstore/pkg/cryptoutils" ) @@ -82,7 +83,7 @@ func (c *VerifyBlobCmd) loadTSACertificates(ctx context.Context) (*cosign.TSACer } // nolint -func (c *VerifyBlobCmd) Exec(ctx context.Context, blobRef string) error { +func (c *VerifyBlobCmd) Exec(ctx context.Context, blobRef string) (err error) { // Require a certificate/key OR a local bundle file that has the cert. if options.NOf(c.KeyRef, c.CertRef, c.Sk, c.BundlePath) == 0 { return fmt.Errorf("provide a key with --key or --sk, a certificate to verify against with --certificate, or a bundle with --bundle") @@ -93,33 +94,6 @@ func (c *VerifyBlobCmd) Exec(ctx context.Context, blobRef string) error { return &options.PubKeyParseError{} } - if c.KeyOpts.NewBundleFormat { - 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") - } - b, err := sgbundle.LoadJSONFromPath(c.BundlePath) - if err != nil { - return err - } - _, err = verifyNewBundle(ctx, b, 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 - } - - var cert *x509.Certificate - opts := make([]static.Option, 0) - - sig, err := base64signature(c.SigRef, c.BundlePath) - if err != nil { - return err - } - sigBytes, err := base64.StdEncoding.DecodeString(sig) - if err != nil { - return err - } - co := &cosign.CheckOpts{ CertGithubWorkflowTrigger: c.CertGithubWorkflowTrigger, CertGithubWorkflowSha: c.CertGithubWorkflowSHA, @@ -152,7 +126,50 @@ func (c *VerifyBlobCmd) Exec(ctx context.Context, blobRef string) error { if err != nil { return fmt.Errorf("loading public key from token: %w", err) } - case c.CertRef != "": + } + + var trustedroot *root.TrustedRoot + co.TrustedMaterial, trustedroot, err = makeTrustedMaterial(c.TrustedRootPath, &co.SigVerifier) + if err != nil { + return err + } + + co.VerifierOptions = makeVerifierOptions(trustedroot, c.IgnoreTlog, c.UseSignedTimestamps, c.IgnoreSCT) + + if c.KeyOpts.NewBundleFormat { + 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") + } + b, err := sgbundle.LoadJSONFromPath(c.BundlePath) + if err != nil { + return err + } + + co.IdentityPolicies, err = makeIdentityPolicy(b, c.CertVerifyOptions, c.CertGithubWorkflowTrigger, c.CertGithubWorkflowSHA, c.CertGithubWorkflowName, c.CertGithubWorkflowRepository, c.CertGithubWorkflowRef) + if err != nil { + return err + } + + _, err = verifyNewBundle(b, co, blobRef) + if err == nil { + ui.Infof(ctx, "Verified OK") + } + return err + } + + var cert *x509.Certificate + opts := make([]static.Option, 0) + + sig, err := base64signature(c.SigRef, c.BundlePath) + if err != nil { + return err + } + sigBytes, err := base64.StdEncoding.DecodeString(sig) + if err != nil { + return err + } + + if c.CertRef != "" { cert, err = loadCertFromFileOrURL(c.CertRef) if err != nil { return err @@ -228,7 +245,12 @@ func (c *VerifyBlobCmd) Exec(ctx context.Context, blobRef string) error { return err } - _, err = verifyNewBundle(ctx, b, 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) + co.IdentityPolicies, err = makeIdentityPolicy(b, c.CertVerifyOptions, c.CertGithubWorkflowTrigger, c.CertGithubWorkflowSHA, c.CertGithubWorkflowName, c.CertGithubWorkflowRepository, c.CertGithubWorkflowRef) + + if err != nil { + return err + } + _, err = verifyNewBundle(b, co, blobRef) if err == nil { ui.Infof(ctx, "Verified OK") } diff --git a/cmd/cosign/cli/verify/verify_blob_attestation.go b/cmd/cosign/cli/verify/verify_blob_attestation.go index c33be7c6498..6554526849d 100644 --- a/cmd/cosign/cli/verify/verify_blob_attestation.go +++ b/cmd/cosign/cli/verify/verify_blob_attestation.go @@ -44,6 +44,7 @@ import ( "github.com/sigstore/cosign/v2/pkg/policy" sigs "github.com/sigstore/cosign/v2/pkg/signature" sgbundle "github.com/sigstore/sigstore-go/pkg/bundle" + "github.com/sigstore/sigstore-go/pkg/root" "github.com/sigstore/sigstore/pkg/cryptoutils" ) @@ -94,36 +95,6 @@ func (c *VerifyBlobAttestationCommand) Exec(ctx context.Context, artifactPath st return &options.KeyParseError{} } - if c.KeyOpts.NewBundleFormat { - 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") - } - b, err := sgbundle.LoadJSONFromPath(c.BundlePath) - if err != nil { - return err - } - result, err := verifyNewBundle(ctx, b, 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 { - return err - } - if c.PredicateType != "" && result.Statement.GetPredicateType() != c.PredicateType { - return fmt.Errorf("invalid predicate type, expected %s got %s", c.PredicateType, result.Statement.GetPredicateType()) - } - fmt.Fprintln(os.Stderr, "Verified OK") - return nil - } - - var cert *x509.Certificate - opts := make([]static.Option, 0) - - var encodedSig []byte - if c.SignaturePath != "" { - encodedSig, err = os.ReadFile(filepath.Clean(c.SignaturePath)) - if err != nil { - return fmt.Errorf("reading %s: %w", c.SignaturePath, err) - } - } - co := &cosign.CheckOpts{ CertGithubWorkflowTrigger: c.CertGithubWorkflowTrigger, CertGithubWorkflowSha: c.CertGithubWorkflowSHA, @@ -156,6 +127,53 @@ func (c *VerifyBlobAttestationCommand) Exec(ctx context.Context, artifactPath st if err != nil { return fmt.Errorf("loading public key from token: %w", err) } + } + + var trustedroot *root.TrustedRoot + co.TrustedMaterial, trustedroot, err = makeTrustedMaterial(c.TrustedRootPath, &co.SigVerifier) + if err != nil { + return err + } + + co.VerifierOptions = makeVerifierOptions(trustedroot, c.IgnoreTlog, c.UseSignedTimestamps, c.IgnoreSCT) + + if c.KeyOpts.NewBundleFormat { + 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") + } + b, err := sgbundle.LoadJSONFromPath(c.BundlePath) + if err != nil { + return err + } + + co.IdentityPolicies, err = makeIdentityPolicy(b, c.CertVerifyOptions, c.CertGithubWorkflowTrigger, c.CertGithubWorkflowSHA, c.CertGithubWorkflowName, c.CertGithubWorkflowRepository, c.CertGithubWorkflowRef) + if err != nil { + return err + } + + result, err := verifyNewBundle(b, co, artifactPath) + if err != nil { + return err + } + if c.PredicateType != "" && result.Statement.GetPredicateType() != c.PredicateType { + return fmt.Errorf("invalid predicate type, expected %s got %s", c.PredicateType, result.Statement.GetPredicateType()) + } + fmt.Fprintln(os.Stderr, "Verified OK") + return nil + } + + var cert *x509.Certificate + opts := make([]static.Option, 0) + + var encodedSig []byte + if c.SignaturePath != "" { + encodedSig, err = os.ReadFile(filepath.Clean(c.SignaturePath)) + if err != nil { + return fmt.Errorf("reading %s: %w", c.SignaturePath, err) + } + } + + switch { case c.CertRef != "": cert, err = loadCertFromFileOrURL(c.CertRef) if err != nil { @@ -246,7 +264,12 @@ func (c *VerifyBlobAttestationCommand) Exec(ctx context.Context, artifactPath st return err } - result, err := verifyNewBundle(ctx, b, 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) + co.IdentityPolicies, err = makeIdentityPolicy(b, c.CertVerifyOptions, c.CertGithubWorkflowTrigger, c.CertGithubWorkflowSHA, c.CertGithubWorkflowName, c.CertGithubWorkflowRepository, c.CertGithubWorkflowRef) + if err != nil { + return err + } + + result, err := verifyNewBundle(b, co, artifactPath) if err != nil { return err } diff --git a/cmd/cosign/cli/verify/verify_bundle.go b/cmd/cosign/cli/verify/verify_bundle.go index 71f6ff691ef..1439a74b465 100644 --- a/cmd/cosign/cli/verify/verify_bundle.go +++ b/cmd/cosign/cli/verify/verify_bundle.go @@ -39,9 +39,8 @@ import ( "github.com/sigstore/sigstore/pkg/cryptoutils" "github.com/sigstore/sigstore/pkg/signature" + "github.com/sigstore/cosign/v2/cmd/cosign/cli/options" "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 { @@ -56,122 +55,31 @@ func (v *verifyTrustedMaterial) PublicKeyVerifier(hint string) (root.TimeConstra return v.keyTrustedMaterial.PublicKeyVerifier(hint) } -func verifyNewBundle(ctx context.Context, bundle *sgbundle.Bundle, trustedRootPath, keyRef, slot, certOIDCIssuer, certOIDCIssuerRegex, certIdentity, certIdentityRegexp, githubWorkflowTrigger, githubWorkflowSHA, githubWorkflowName, githubWorkflowRepository, githubWorkflowRef, artifactRef string, sk, ignoreTlog, useSignedTimestamps, ignoreSCT bool) (*verify.VerificationResult, error) { - var trustedroot *root.TrustedRoot - var err error - - if trustedRootPath == "" { - // 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") +func verifyNewBundle(bundle *sgbundle.Bundle, checkOpts *cosign.CheckOpts, artifactRef string) (*verify.VerificationResult, error) { + if checkOpts.TrustedMaterial == nil { + return nil, fmt.Errorf("checkOpts must have TrustedMaterial set") } - 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 len(checkOpts.IdentityPolicies) == 0 { + return nil, fmt.Errorf("checkOpts IdentityPolicies must have at least 1 item") } - if !ignoreSCT { - verifierConfig = append(verifierConfig, verify.WithSignedCertificateTimestamps(1)) - } - - if ignoreTlog && !useSignedTimestamps { - verifierConfig = append(verifierConfig, verify.WithoutAnyObserverTimestampsUnsafe()) + if len(checkOpts.VerifierOptions) == 0 { + return nil, fmt.Errorf("checkOpts VerfierOption must have at least 1 item") } - // Perform verification payload, err := payloadBytes(artifactRef) if err != nil { return nil, err } buf := bytes.NewBuffer(payload) - sev, err := verify.NewSignedEntityVerifier(trustedmaterial, verifierConfig...) + sev, err := verify.NewSignedEntityVerifier(checkOpts.TrustedMaterial, checkOpts.VerifierOptions...) if err != nil { return nil, err } - return sev.Verify(bundle, verify.NewPolicy(verify.WithArtifact(buf), identityPolicies...)) + return sev.Verify(bundle, verify.NewPolicy(verify.WithArtifact(buf), checkOpts.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) { @@ -315,3 +223,96 @@ func assembleNewBundle(ctx context.Context, sigBytes, signedTimestamp []byte, en return b, nil } + +func makeTrustedMaterial(trustedRootPath string, sigVerifier *signature.Verifier) (root.TrustedMaterial, *root.TrustedRoot, error) { + var trustedroot *root.TrustedRoot + var err error + + if trustedRootPath == "" { + // Assume we're using public good instance; fetch via TUF + trustedroot, err = root.FetchTrustedRoot() + if err != nil { + return nil, nil, err + } + } else { + trustedroot, err = root.NewTrustedRootFromPath(trustedRootPath) + if err != nil { + return nil, nil, err + } + } + + trustedmaterial := &verifyTrustedMaterial{TrustedMaterial: trustedroot} + + if sigVerifier != nil { + newExpiringKey := root.NewExpiringKey(*sigVerifier, time.Time{}, time.Time{}) + trustedmaterial.keyTrustedMaterial = root.NewTrustedPublicKeyMaterial(func(_ string) (root.TimeConstrainedVerifier, error) { + return newExpiringKey, nil + }) + } + + return trustedmaterial, trustedroot, nil +} + +func makeIdentityPolicy(bundle *sgbundle.Bundle, certVerifyOpts options.CertVerifyOptions, githubWorkflowTrigger, githubWorkflowSHA, githubWorkflowName, githubWorkflowRepository, githubWorkflowRef string) ([]verify.PolicyOption, error) { + 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(certVerifyOpts.CertIdentity, certVerifyOpts.CertIdentityRegexp) + if err != nil { + return nil, err + } + + issuerMatcher, err := verify.NewIssuerMatcher(certVerifyOpts.CertOidcIssuer, certVerifyOpts.CertOidcIssuerRegexp) + 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)) + } + + return identityPolicies, nil +} + +func makeVerifierOptions(trustedroot *root.TrustedRoot, ignoreTlog, useSignedTimestamps, ignoreSCT bool) []verify.VerifierOption { + // 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.WithoutAnyObserverTimestampsUnsafe()) + } + + return verifierConfig +} diff --git a/cmd/cosign/cli/verify/verify_bundle_test.go b/cmd/cosign/cli/verify/verify_bundle_test.go index 04dfc82334b..ffd2c21bd83 100644 --- a/cmd/cosign/cli/verify/verify_bundle_test.go +++ b/cmd/cosign/cli/verify/verify_bundle_test.go @@ -28,6 +28,10 @@ import ( "path/filepath" "testing" + "github.com/sigstore/sigstore-go/pkg/root" + + "github.com/sigstore/cosign/v2/cmd/cosign/cli/options" + "github.com/sigstore/cosign/v2/pkg/cosign" "github.com/sigstore/cosign/v2/pkg/signature" ) @@ -76,7 +80,20 @@ func TestVerifyBundleWithKey(t *testing.T) { err = os.WriteFile(publicKeyPath, pem.EncodeToMemory(pemBlock), 0600) checkErr(t, err) - result, err := verifyNewBundle(ctx, bundle, trustedRootPath, publicKeyPath, "", "", "", "", "", "", "", "", "", "", artifactPath, false, true, false, true) + co := &cosign.CheckOpts{} + co.SigVerifier, err = signature.PublicKeyFromKeyRef(ctx, publicKeyPath) + checkErr(t, err) + + var trustedroot *root.TrustedRoot + co.TrustedMaterial, trustedroot, err = makeTrustedMaterial(trustedRootPath, &co.SigVerifier) + checkErr(t, err) + + co.IdentityPolicies, err = makeIdentityPolicy(bundle, options.CertVerifyOptions{}, "", "", "", "", "") + checkErr(t, err) + + co.VerifierOptions = makeVerifierOptions(trustedroot, true, false, true) + + result, err := verifyNewBundle(bundle, co, artifactPath) checkErr(t, err) if result == nil { diff --git a/pkg/cosign/verify.go b/pkg/cosign/verify.go index 3ab5d76026a..d7d1559b0a6 100644 --- a/pkg/cosign/verify.go +++ b/pkg/cosign/verify.go @@ -45,6 +45,8 @@ import ( cbundle "github.com/sigstore/cosign/v2/pkg/cosign/bundle" "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" @@ -163,6 +165,19 @@ type CheckOpts struct { // Should the experimental OCI 1.1 behaviour be enabled or not. // Defaults to false. ExperimentalOCI11 bool + + // These items are for verifying new bundles or verifying with a trusted root + + // The verification material to use, including public key material + TrustedMaterial root.TrustedMaterial + + // Options around the identity of the verification material, + // particularly for verifying certificates + IdentityPolicies []verify.PolicyOption + + // Options around what sort of verification material you're expecting: + // transparency logs, signed timestamps, certificate transparency logs, etc + VerifierOptions []verify.VerifierOption } // This is a substitutable signature verification function that can be used for verifying From 97d9ca6660b96b7aadd4c4e408d5a5756d98aeb2 Mon Sep 17 00:00:00 2001 From: Zach Steindler Date: Tue, 24 Sep 2024 10:43:52 -0400 Subject: [PATCH 4/4] Use custom predicate type that cosign will not modify Signed-off-by: Zach Steindler --- test/e2e_test.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/e2e_test.go b/test/e2e_test.go index 598d7faa5c0..30d8662b11f 100644 --- a/test/e2e_test.go +++ b/test/e2e_test.go @@ -870,7 +870,7 @@ func TestAttestationBlobRFC3161Timestamp(t *testing.T) { blob := "someblob" predicate := `{ "buildType": "x", "builder": { "id": "2" }, "recipe": {} }` - predicateType := "slsaprovenance" + predicateType := "custompredicate" td := t.TempDir() t.Cleanup(func() {