From 3a8609235b3cf519dce4aef2263dcedfedff1737 Mon Sep 17 00:00:00 2001 From: Zach Steindler Date: Tue, 27 Aug 2024 09:57:06 -0400 Subject: [PATCH] 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/attest/attest_blob.go | 16 +- cmd/cosign/cli/verify/verify_blob.go | 120 ++++++---- .../cli/verify/verify_blob_attestation.go | 225 ++++++++++-------- cmd/cosign/cli/verify/verify_bundle.go | 192 +++++++++++++-- 5 files changed, 391 insertions(+), 244 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/attest/attest_blob.go b/cmd/cosign/cli/attest/attest_blob.go index 50795215e66..8bd71686015 100644 --- a/cmd/cosign/cli/attest/attest_blob.go +++ b/cmd/cosign/cli/attest/attest_blob.go @@ -165,7 +165,21 @@ func (c *AttestBlobCommand) Exec(ctx context.Context, artifactPath string) error var rekorEntry *models.LogEntryAnon if c.TSAServerURL != "" { - timestampBytes, err = tsa.GetTimestampedSignature(sig, client.NewTSAClient(c.TSAServerURL)) + // sig is the entire JSON DSSE Envelope; we just want the signature for TSA + var envelope dsse.Envelope + err = json.Unmarshal(sig, &envelope) + if err != nil { + return err + } + if len(envelope.Signatures) == 0 { + return fmt.Errorf("envelope has no signatures") + } + envelopeSigBytes, err := base64.StdEncoding.DecodeString(envelope.Signatures[0].Sig) + if err != nil { + return err + } + + timestampBytes, err = tsa.GetTimestampedSignature(envelopeSigBytes, client.NewTSAClient(c.TSAServerURL)) if err != nil { return err } 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 }