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..08eb4c8aeae 100644 --- a/cmd/cosign/cli/verify/verify_blob.go +++ b/cmd/cosign/cli/verify/verify_blob.go @@ -39,6 +39,8 @@ 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-go/pkg/root" "github.com/sigstore/sigstore/pkg/cryptoutils" ) @@ -81,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") @@ -92,41 +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") - } - 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 == "" { - 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) - if err != nil { - return err - } - co := &cosign.CheckOpts{ CertGithubWorkflowTrigger: c.CertGithubWorkflowTrigger, CertGithubWorkflowSha: c.CertGithubWorkflowSHA, @@ -134,44 +101,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 { @@ -194,12 +126,56 @@ 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 } } + if c.BundlePath != "" { b, err := cosign.FetchLocalSignedPayloadFromPath(c.BundlePath) if err != nil { @@ -234,8 +210,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 +222,67 @@ 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 + } + + 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 + } + + 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 +338,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..6554526849d 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,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" + "github.com/sigstore/sigstore-go/pkg/root" "github.com/sigstore/sigstore/pkg/cryptoutils" ) @@ -92,29 +95,7 @@ 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") - } - 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() - if err != nil { - return err - } - } - co := &cosign.CheckOpts{ - Identities: identities, CertGithubWorkflowTrigger: c.CertGithubWorkflowTrigger, CertGithubWorkflowSha: c.CertGithubWorkflowSHA, CertGithubWorkflowName: c.CertGithubWorkflowName, @@ -124,80 +105,66 @@ func (c *VerifyBlobAttestationCommand) Exec(ctx context.Context, artifactPath st 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)) + + // Keys are optional! + switch { + case c.KeyRef != "": + co.SigVerifier, err = sigs.PublicKeyFromKeyRef(ctx, c.KeyRef) if err != nil { - return err + return fmt.Errorf("loading public key: %w", err) } - defer f.Close() - fileInfo, err := f.Stat() - if err != nil { - return err + pkcs11Key, ok := co.SigVerifier.(*pkcs11key.Key) + if ok { + defer pkcs11Key.Close() } - err = payloadsize.CheckSize(uint64(fileInfo.Size())) + case c.Sk: + sk, err := pivkey.GetKeyWithSlot(c.Slot) if err != nil { - return err - } - - payload = internal.NewHashReader(f, sha256.New()) - if _, err := io.ReadAll(&payload); err != nil { - return err + return fmt.Errorf("opening piv token: %w", err) } - digest := payload.Sum(nil) - h = v1.Hash{ - Hex: hex.EncodeToString(digest), - Algorithm: "sha256", + defer sk.Close() + co.SigVerifier, err = sk.Verifier() + if err != nil { + return fmt.Errorf("loading public key from token: %w", err) } - 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") + var trustedroot *root.TrustedRoot + co.TrustedMaterial, trustedroot, err = makeTrustedMaterial(c.TrustedRootPath, &co.SigVerifier) + if err != nil { + return err } - 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 - } + co.VerifierOptions = makeVerifierOptions(trustedroot, c.IgnoreTlog, c.UseSignedTimestamps, c.IgnoreSCT) - if !c.IgnoreTlog { - if c.RekorURL != "" { - rekorClient, err := rekor.NewClient(c.RekorURL) - if err != nil { - return fmt.Errorf("creating Rekor client: %w", err) - } - co.RekorClient = rekorClient + if 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") } - // 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) + b, err := sgbundle.LoadJSONFromPath(c.BundlePath) if err != nil { - return fmt.Errorf("getting Rekor public keys: %w", err) + return err } - } - if keylessVerification(c.KeyRef, c.Sk) { - if err := loadCertsKeylessVerification(c.CertChain, c.CARoots, c.CAIntermediates, co); err != nil { + + co.IdentityPolicies, err = makeIdentityPolicy(b, c.CertVerifyOptions, c.CertGithubWorkflowTrigger, c.CertGithubWorkflowSHA, c.CertGithubWorkflowName, c.CertGithubWorkflowRepository, c.CertGithubWorkflowRef) + if 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) + result, err := verifyNewBundle(b, co, artifactPath) if err != nil { - return fmt.Errorf("getting ctlog public keys: %w", err) + 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)) @@ -206,29 +173,7 @@ 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 { @@ -238,6 +183,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 +223,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 +235,117 @@ 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 + } + + 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 + } + + 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..1439a74b465 100644 --- a/cmd/cosign/cli/verify/verify_bundle.go +++ b/cmd/cosign/cli/verify/verify_bundle.go @@ -18,16 +18,29 @@ 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/pivkey" - sigs "github.com/sigstore/cosign/v2/pkg/signature" + "github.com/sigstore/cosign/v2/cmd/cosign/cli/options" + "github.com/sigstore/cosign/v2/pkg/cosign" ) type verifyTrustedMaterial struct { @@ -36,79 +49,230 @@ 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) +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 len(checkOpts.IdentityPolicies) == 0 { + return nil, fmt.Errorf("checkOpts IdentityPolicies must have at least 1 item") + } + + if len(checkOpts.VerifierOptions) == 0 { + return nil, fmt.Errorf("checkOpts VerfierOption must have at least 1 item") + } + + payload, err := payloadBytes(artifactRef) if err != nil { - return err + return nil, err } + buf := bytes.NewBuffer(payload) - var trustedroot *root.TrustedRoot + sev, err := verify.NewSignedEntityVerifier(checkOpts.TrustedMaterial, checkOpts.VerifierOptions...) + if err != nil { + return nil, err + } - if trustedRootPath == "" { - // Assume we're using public good instance; fetch via TUF - trustedroot, err = root.FetchTrustedRoot() + 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) { + 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 err + 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 { - trustedroot, err = root.NewTrustedRootFromPath(trustedRootPath) + 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 err + return nil, err + } + hashedBytes := sha256.Sum256(pubKeyBytes) + + pb.VerificationMaterial.Content = &protobundle.VerificationMaterial_PublicKey{ + PublicKey: &protocommon.PublicKeyIdentifier{ + Hint: base64.StdEncoding.EncodeToString(hashedBytes[:]), + }, } } - trustedmaterial := &verifyTrustedMaterial{TrustedMaterial: trustedroot} + if len(signedTimestamp) > 0 { + ts := &protocommon.RFC3161SignedTimestamp{ + SignedTimestamp: signedTimestamp, + } - // See if we need to wrap trusted root with provided key - if keyRef != "" { - signatureVerifier, err := sigs.PublicKeyFromKeyRef(ctx, keyRef) + 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 err + 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 + } } - 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) + 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 + } + + 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 fmt.Errorf("opening piv token: %w", err) + return nil, nil, err } - defer s.Close() - signatureVerifier, err := s.Verifier() + } else { + trustedroot, err = root.NewTrustedRootFromPath(trustedRootPath) if err != nil { - return fmt.Errorf("loading public key from token: %w", err) + return nil, nil, err } + } + + trustedmaterial := &verifyTrustedMaterial{TrustedMaterial: trustedroot} - newExpiringKey := root.NewExpiringKey(signatureVerifier, time.Time{}, time.Time{}) + 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 fmt.Errorf("no verification material in bundle") + 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) + sanMatcher, err := verify.NewSANMatcher(certVerifyOpts.CertIdentity, certVerifyOpts.CertIdentityRegexp) if err != nil { - return err + return nil, err } - issuerMatcher, err := verify.NewIssuerMatcher(certOIDCIssuer, certOIDCIssuerRegex) + issuerMatcher, err := verify.NewIssuerMatcher(certVerifyOpts.CertOidcIssuer, certVerifyOpts.CertOidcIssuerRegexp) if err != nil { - return err + return nil, err } extensions := certificate.Extensions{ @@ -121,12 +285,16 @@ 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)) } + return identityPolicies, nil +} + +func makeVerifierOptions(trustedroot *root.TrustedRoot, ignoreTlog, useSignedTimestamps, ignoreSCT bool) []verify.VerifierOption { // Make some educated guesses about verification policy verifierConfig := []verify.VerifierOption{} @@ -146,18 +314,5 @@ func verifyNewBundle(ctx context.Context, bundlePath, trustedRootPath, keyRef, s verifierConfig = append(verifierConfig, verify.WithoutAnyObserverTimestampsUnsafe()) } - // Perform verification - payload, err := payloadBytes(artifactRef) - if err != nil { - return err - } - buf := bytes.NewBuffer(payload) - - sev, err := verify.NewSignedEntityVerifier(trustedmaterial, verifierConfig...) - if err != nil { - return err - } - - _, err = sev.Verify(bundle, verify.NewPolicy(verify.WithArtifact(buf), identityPolicies...)) - return err + return verifierConfig } 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..ffd2c21bd83 --- /dev/null +++ b/cmd/cosign/cli/verify/verify_bundle_test.go @@ -0,0 +1,108 @@ +// +// 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/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" +) + +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) + + 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 { + 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 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 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() {