diff --git a/cmd/cosign/cli/options/verify.go b/cmd/cosign/cli/options/verify.go index b14f06626fb..570303a40e4 100644 --- a/cmd/cosign/cli/options/verify.go +++ b/cmd/cosign/cli/options/verify.go @@ -42,7 +42,7 @@ func (o *CommonVerifyOptions) AddFlags(cmd *cobra.Command) { "Optionally may contain intermediate CA certificates, and may contain the leaf TSA certificate if not present in the timestamp") cmd.Flags().BoolVar(&o.UseSignedTimestamps, "use-signed-timestamps", false, - "use signed timestamps if available") + "verify rfc3161 timestamps") cmd.Flags().BoolVar(&o.IgnoreTlog, "insecure-ignore-tlog", false, "ignore transparency log verification, to be used when an artifact signature has not been uploaded to the transparency log. Artifacts "+ diff --git a/cmd/cosign/cli/verify.go b/cmd/cosign/cli/verify.go index 3d360794d46..6ed33467b44 100644 --- a/cmd/cosign/cli/verify.go +++ b/cmd/cosign/cli/verify.go @@ -137,6 +137,7 @@ against the transparency log.`, Offline: o.CommonVerifyOptions.Offline, TSACertChainPath: o.CommonVerifyOptions.TSACertChainPath, IgnoreTlog: o.CommonVerifyOptions.IgnoreTlog, + UseSignedTimestamps: o.CommonVerifyOptions.UseSignedTimestamps, MaxWorkers: o.CommonVerifyOptions.MaxWorkers, ExperimentalOCI11: o.CommonVerifyOptions.ExperimentalOCI11, } @@ -244,6 +245,7 @@ against the transparency log.`, Offline: o.CommonVerifyOptions.Offline, TSACertChainPath: o.CommonVerifyOptions.TSACertChainPath, IgnoreTlog: o.CommonVerifyOptions.IgnoreTlog, + UseSignedTimestamps: o.CommonVerifyOptions.UseSignedTimestamps, MaxWorkers: o.CommonVerifyOptions.MaxWorkers, } diff --git a/cmd/cosign/cli/verify/verify.go b/cmd/cosign/cli/verify/verify.go index 434f2501003..4fb4e45c82d 100644 --- a/cmd/cosign/cli/verify/verify.go +++ b/cmd/cosign/cli/verify/verify.go @@ -143,6 +143,7 @@ func (c *VerifyCommand) Exec(ctx context.Context, images []string) (err error) { Identities: identities, Offline: c.Offline, IgnoreTlog: c.IgnoreTlog, + UseSignedTimestamps: c.UseSignedTimestamps, MaxWorkers: c.MaxWorkers, ExperimentalOCI11: c.ExperimentalOCI11, ExpectSigstoreBundle: c.ExpectSigstoreBundle, diff --git a/cmd/cosign/cli/verify/verify_attestation.go b/cmd/cosign/cli/verify/verify_attestation.go index c9e8164f216..b65cd99a4ad 100644 --- a/cmd/cosign/cli/verify/verify_attestation.go +++ b/cmd/cosign/cli/verify/verify_attestation.go @@ -119,21 +119,29 @@ func (c *VerifyAttestationCommand) Exec(ctx context.Context, images []string) (e Identities: identities, Offline: c.Offline, IgnoreTlog: c.IgnoreTlog, + UseSignedTimestamps: c.UseSignedTimestamps, MaxWorkers: c.MaxWorkers, ExpectSigstoreBundle: c.ExpectSigstoreBundle, } if c.CheckClaims { co.ClaimVerifier = cosign.IntotoSubjectClaimVerifier } + + if c.ExpectSigstoreBundle { + if err = checkSigstoreBundleUnsupportedOptions(c); 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) { + if shouldVerifySCT(c.IgnoreSCT, c.KeyRef, c.Sk) && !c.ExpectSigstoreBundle { co.CTLogPubKeys, err = cosign.GetCTLogPubs(ctx) if err != nil { return fmt.Errorf("getting ctlog public keys: %w", err) } } - if c.TSACertChainPath != "" || c.UseSignedTimestamps { + if c.TSACertChainPath != "" || c.UseSignedTimestamps && !c.ExpectSigstoreBundle { tsaCertificates, err := c.loadTSACertificates(ctx) if err != nil { return fmt.Errorf("unable to load TSA certificates: %w", err) @@ -143,7 +151,7 @@ func (c *VerifyAttestationCommand) Exec(ctx context.Context, images []string) (e co.TSAIntermediateCertificates = tsaCertificates.IntermediateCerts } - if !c.IgnoreTlog { + if !c.IgnoreTlog && !co.ExpectSigstoreBundle { if c.RekorURL != "" { rekorClient, err := rekor.NewClient(c.RekorURL) if err != nil { @@ -189,6 +197,10 @@ func (c *VerifyAttestationCommand) Exec(ctx context.Context, images []string) (e return fmt.Errorf("initializing piv token verifier: %w", err) } case c.CertRef != "": + if c.ExpectSigstoreBundle { + // This shouldn't happen because we already checked for this above in checkSigstoreBundleUnsupportedOptions + return fmt.Errorf("unsupported: certificate reference currently not supported with --expect-sigstore-bundle") + } cert, err := loadCertFromFileOrURL(c.CertRef) if err != nil { return fmt.Errorf("loading certificate from reference: %w", err) @@ -229,9 +241,14 @@ func (c *VerifyAttestationCommand) Exec(ctx context.Context, images []string) (e if !c.ExpectSigstoreBundle { return fmt.Errorf("unsupported: trusted root path currently only supported with --expect-sigstore-bundle") } - co.TrustedMaterial, err = root.NewTrustedRootFromPath(c.TrustedRootPath) - if err != nil { - return fmt.Errorf("loading trusted root: %w", err) + + // If a trusted root path is provided, we will use it to verify the bundle. + // Otherwise, the verifier will default to the public good instance. + if c.TrustedRootPath == "" { + co.TrustedMaterial, err = root.NewTrustedRootFromPath(c.TrustedRootPath) + if err != nil { + return fmt.Errorf("creating trusted root from path: %w", err) + } } case c.CARoots != "": // CA roots + possible intermediates are already loaded into co.RootCerts with the call to @@ -337,3 +354,19 @@ func (c *VerifyAttestationCommand) Exec(ctx context.Context, images []string) (e return nil } + +func checkSigstoreBundleUnsupportedOptions(c *VerifyAttestationCommand) error { + if c.Cert != "" || c.CertRef != "" { + return fmt.Errorf("unsupported: certificate may not be provided using --cert when using --expect-sigstore-bundle (cert must be in bundle)") + } + if c.CertChain != "" { + return fmt.Errorf("unsupported: certificate chain may not be provided using --cert-chain when using --expect-sigstore-bundle (cert must be in bundle)") + } + if c.CARoots != "" || c.CAIntermediates != "" { + return fmt.Errorf("unsupported: CA roots/intermediates must be provided using --trusted-root when using --expect-sigstore-bundle") + } + if c.TSACertChainPath != "" { + return fmt.Errorf("unsupported: TSA certificate chain path may only be provided using --trusted-root when using --expect-sigstore-bundle") + } + return nil +} diff --git a/cmd/cosign/cli/verify/verify_blob.go b/cmd/cosign/cli/verify/verify_blob.go index 79475c90d80..1b063c95ef8 100644 --- a/cmd/cosign/cli/verify/verify_blob.go +++ b/cmd/cosign/cli/verify/verify_blob.go @@ -137,6 +137,7 @@ func (c *VerifyBlobCmd) Exec(ctx context.Context, blobRef string) error { Identities: identities, Offline: c.Offline, IgnoreTlog: c.IgnoreTlog, + UseSignedTimestamps: c.UseSignedTimestamps, } 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") diff --git a/cmd/cosign/cli/verify/verify_blob_attestation.go b/cmd/cosign/cli/verify/verify_blob_attestation.go index 3f2c33cc63b..6b458a37a5e 100644 --- a/cmd/cosign/cli/verify/verify_blob_attestation.go +++ b/cmd/cosign/cli/verify/verify_blob_attestation.go @@ -123,6 +123,7 @@ func (c *VerifyBlobAttestationCommand) Exec(ctx context.Context, artifactPath st IgnoreSCT: c.IgnoreSCT, Offline: c.Offline, IgnoreTlog: c.IgnoreTlog, + UseSignedTimestamps: c.UseSignedTimestamps, } var h v1.Hash if c.CheckClaims { diff --git a/pkg/cosign/verify.go b/pkg/cosign/verify.go index 8b29c515d37..50a09534806 100644 --- a/pkg/cosign/verify.go +++ b/pkg/cosign/verify.go @@ -44,12 +44,15 @@ import ( "github.com/sigstore/cosign/v2/pkg/blob" cbundle "github.com/sigstore/cosign/v2/pkg/cosign/bundle" + "github.com/sigstore/sigstore-go/pkg/fulcio/certificate" + ocibundle "github.com/sigstore/cosign/v2/pkg/oci/bundle" "github.com/sigstore/cosign/v2/pkg/oci/static" "github.com/sigstore/cosign/v2/pkg/types" protobundle "github.com/sigstore/protobuf-specs/gen/pb-go/bundle/v1" sgbundle "github.com/sigstore/sigstore-go/pkg/bundle" "github.com/sigstore/sigstore-go/pkg/root" + "github.com/sigstore/sigstore-go/pkg/verify" sgverify "github.com/sigstore/sigstore-go/pkg/verify" "github.com/cyberphone/json-canonicalization/go/src/webpki.org/jsoncanonicalizer" @@ -170,6 +173,9 @@ type CheckOpts struct { // IgnoreTlog skip tlog verification IgnoreTlog bool + // UseSignedTimestamps use signed timestamps if available + UseSignedTimestamps bool + // The amount of maximum workers for parallel executions. // Defaults to 10. MaxWorkers int @@ -184,9 +190,93 @@ type CheckOpts struct { // Currently, this is only applicable when ExpectSigstoreBundle is true. TrustedMaterial root.TrustedMaterial - // TODO: Add the following, deprecate overlapping fields - //CertificateIdentities verify.CertificateIdentities - //VerifierOptions []verify.VerifierOption + // VerifierOptions are the options to be passed to the verifier. + VerifierOptions []verify.VerifierOption + + // PolicyOptions are the policy options to be passed to the verifier. + PolicyOptions []verify.PolicyOption +} + +type verifyTrustedMaterial struct { + root.TrustedMaterial + keyTrustedMaterial root.TrustedMaterial +} + +func (v *verifyTrustedMaterial) PublicKeyVerifier(hint string) (root.TimeConstrainedVerifier, error) { + return v.keyTrustedMaterial.PublicKeyVerifier(hint) +} + +// SigstoreGoOptions returns the verification options for verifying with sigstore-go. +func (co *CheckOpts) SigstoreGoOptions() (trustedMaterial root.TrustedMaterial, verifierOptions []verify.VerifierOption, policyOptions []verify.PolicyOption, err error) { + var sanMatcher verify.SubjectAlternativeNameMatcher + var issuerMatcher verify.IssuerMatcher + + if len(co.Identities) > 0 { + if len(co.Identities) > 1 { + return nil, nil, nil, fmt.Errorf("unsupported: multiple identities are not supported at this time") + } + sanMatcher, err = verify.NewSANMatcher(co.Identities[0].Subject, co.Identities[0].SubjectRegExp) + if err != nil { + return nil, nil, nil, err + } + + issuerMatcher, err = verify.NewIssuerMatcher(co.Identities[0].Issuer, co.Identities[0].IssuerRegExp) + if err != nil { + return nil, nil, nil, err + } + } + + extensions := certificate.Extensions{ + GithubWorkflowTrigger: co.CertGithubWorkflowTrigger, + GithubWorkflowSHA: co.CertGithubWorkflowSha, + GithubWorkflowName: co.CertGithubWorkflowName, + GithubWorkflowRepository: co.CertGithubWorkflowRepository, + GithubWorkflowRef: co.CertGithubWorkflowRef, + } + + certificateIdentities, err := verify.NewCertificateIdentity(sanMatcher, issuerMatcher, extensions) + if err != nil { + return nil, nil, nil, err + } + + policyOptions = append(policyOptions, co.PolicyOptions...) + policyOptions = append(policyOptions, verify.WithCertificateIdentity(certificateIdentities)) + + // Wrap TrustedMaterial + vTrustedMaterial := &verifyTrustedMaterial{TrustedMaterial: co.TrustedMaterial} + + // If TrustedMaterial is not set, fetch it from TUF + if vTrustedMaterial.TrustedMaterial == nil { + vTrustedMaterial.TrustedMaterial, err = root.FetchTrustedRoot() + if err != nil { + return nil, nil, nil, err + } + } + + if co.SigVerifier != nil { + policyOptions = append(policyOptions, verify.WithKey()) + newExpiringKey := root.NewExpiringKey(co.SigVerifier, time.Time{}, time.Time{}) + vTrustedMaterial.keyTrustedMaterial = root.NewTrustedPublicKeyMaterial(func(_ string) (root.TimeConstrainedVerifier, error) { + return newExpiringKey, nil + }) + } + + // Make some educated guesses about verification policy + verifierOptions = append(verifierOptions, co.VerifierOptions...) + if !co.IgnoreTlog { + verifierOptions = append(verifierOptions, verify.WithTransparencyLog(1), verify.WithIntegratedTimestamps(1)) + } + if co.UseSignedTimestamps { + verifierOptions = append(verifierOptions, verify.WithSignedTimestamps(1)) + } + if !co.IgnoreSCT { + verifierOptions = append(verifierOptions, verify.WithSignedCertificateTimestamps(1)) + } + if co.IgnoreSCT && !co.UseSignedTimestamps { + verifierOptions = append(verifierOptions, verify.WithoutAnyObserverTimestampsUnsafe()) + } + + return vTrustedMaterial, verifierOptions, policyOptions, nil } // This is a substitutable signature verification function that can be used for verifying @@ -1463,12 +1553,7 @@ func verifyImageSignaturesExperimentalOCI(ctx context.Context, signedImgRef name return verifySignatures(ctx, sigs, h, co) } -func getBundles(ctx context.Context, signedImgRef name.Reference, co *CheckOpts) ([]*sgbundle.Bundle, *v1.Hash, error) { - // Enforce this up front. - if co.TrustedMaterial == nil { - return nil, nil, errors.New("Sigstore bundle verification requires TrustedMaterial") - } - +func getBundles(_ context.Context, signedImgRef name.Reference, co *CheckOpts) ([]*sgbundle.Bundle, *v1.Hash, error) { // This is a carefully optimized sequence for fetching the signatures of the // entity that minimizes registry requests when supplied with a digest input digest, err := ociremote.ResolveDigest(signedImgRef, co.RegistryClientOpts...) @@ -1517,31 +1602,11 @@ func verifyImageAttestationsSigstoreBundle(ctx context.Context, signedImgRef nam return nil, false, err } - // TODO: build verifierConfig from CheckOpts - verifierConfig := []sgverify.VerifierOption{ - sgverify.WithSignedCertificateTimestamps(1), - sgverify.WithTransparencyLog(1), - sgverify.WithIntegratedTimestamps(1), - } - - sev, err := sgverify.NewSignedEntityVerifier(co.TrustedMaterial, verifierConfig...) - if err != nil { - return nil, false, err - } - - policyOptions := make([]sgverify.PolicyOption, 0, len(co.Identities)) - for _, i := range co.Identities { - id, err := sgverify.NewShortCertificateIdentity(i.Issuer, i.IssuerRegExp, i.Subject, i.SubjectRegExp) - if err != nil { - return nil, false, err - } - policyOptions = append(policyOptions, sgverify.WithCertificateIdentity(id)) - } - policy := sgverify.NewPolicy(sgverify.WithArtifactDigest(hash.Algorithm, digestBytes), policyOptions...) + artifactPolicyOption := sgverify.WithArtifactDigest(hash.Algorithm, digestBytes) checkedSignatures = make([]oci.Signature, 0, len(bundles)) for _, bundle := range bundles { - _, err := sev.Verify(bundle, policy) + _, err := VerifyNewBundle(ctx, co, artifactPolicyOption, bundle) if err != nil { continue } diff --git a/pkg/cosign/verify_bundle.go b/pkg/cosign/verify_bundle.go new file mode 100644 index 00000000000..d4da34addae --- /dev/null +++ b/pkg/cosign/verify_bundle.go @@ -0,0 +1,36 @@ +// +// 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 cosign + +import ( + "context" + + "github.com/sigstore/sigstore-go/pkg/verify" + // sigs "github.com/sigstore/cosign/v2/pkg/signature" +) + +// VerifyNewBundle verifies a SigstoreBundle with the given parameters +func VerifyNewBundle(_ context.Context, co *CheckOpts, artifactPolicyOption verify.ArtifactPolicyOption, bundle verify.SignedEntity) (*verify.VerificationResult, error) { + trustedMaterial, verifierOptions, policyOptions, err := co.SigstoreGoOptions() + if err != nil { + return nil, err + } + verifier, err := verify.NewSignedEntityVerifier(trustedMaterial, verifierOptions...) + if err != nil { + return nil, err + } + return verifier.Verify(bundle, verify.NewPolicy(artifactPolicyOption, policyOptions...)) +}