Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support trusted root in cosign verification #3854

Open
wants to merge 2 commits into
base: main
Choose a base branch
from

Conversation

steiza
Copy link
Member

@steiza steiza commented Aug 27, 2024

Working towards #3700

Summary

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 are using an old bundle, or using disparate signed material on disk.

This implements trusted root support for those cases by assembling the materials into a protobuf bundle, thus fixing some TODOs from when we added protobuf bundle support.

Generally to test I would recommend signing something and then verifying it with a trusted root, like:

$ go run cmd/cosign/main.go sign-blob --identity-token='...' --output-certificate=detached.crt --output-signature=detached.sig ../sigstore-go/examples/sigstore-go-signing/hello_world.txt

$ go run cmd/cosign/main.go verify-blob --trusted-root=trusted_root.public.json --certificate=detached.crt --signature=detached.sig --certificate-oidc-issuer="https://token.actions.githubusercontent.com" --certificate-identity="https://github.com/sigstore-conformance/extremely-dangerous-public-oidc-beacon/.github/workflows/extremely-dangerous-oidc-beacon.yml@refs/heads/main" ../sigstore-go/examples/sigstore-go-signing/hello_world.txt

Or:

$ go run cmd/cosign/main.go attest-blob --identity-token='...' --bundle=old_bundle_attestation.json --type=something --predicate="../sigstore-go/examples/sigstore-go-signing/intoto.txt" ../sigstore-go/examples/sigstore-go-signing/hello_world.txt

$ go run cmd/cosign/main.go verify-blob-attestation --trusted-root=trusted_root.public.json --bundle=old_bundle_attestation.json --certificate-oidc-issuer="https://token.actions.githubusercontent.com" --certificate-identity="https://github.com/sigstore-conformance/extremely-dangerous-public-oidc-beacon/.github/workflows/extremely-dangerous-oidc-beacon.yml@refs/heads/main" --type=something ../sigstore-go/examples/sigstore-go-signing/hello_world.txt

It's also a good idea to test with a timestamp authority, although you need to create a trusted root file that corresponds to your chosen timestamp authority. For example:

$ go run cmd/cosign/main.go attest-blob --key=cosign.key --tlog-upload=false --timestamp-server-url="https://timestamp.githubapp.com/api/v1/timestamp" --rfc3161-timestamp-bundle="attest.ts" --output-signature=attest.sig --tlog-upload=false --type=something --predicate="../sigstore-go/examples/sigstore-go-signing/intoto.txt" ../sigstore-go/examples/sigstore-go-signing/hello_world.txt

$ go run cmd/cosign/main.go verify-blob-attestation --trusted-root=trusted_root_with_timestamps.public.json --use-signed-timestamps=true --insecure-ignore-tlog=true --key=cosign.pub --rfc3161-timestamp="attest.ts" --signature=attest.sig --type=something ../sigstore-go/examples/sigstore-go-signing/hello_world.txt

Release Note

  • Added support for using a protobuf trusted root to verify signed material, either on disk or in a legacy cosign bundle format (i.e. not the protobuf bundle format).

Documentation

N/A?

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 <steiza@github.com>
Copy link

codecov bot commented Aug 27, 2024

Codecov Report

Attention: Patch coverage is 32.44147% with 202 lines in your changes missing coverage. Please review.

Project coverage is 36.71%. Comparing base (2ef6022) to head (bdb56ca).
Report is 215 commits behind head on main.

Files with missing lines Patch % Lines
cmd/cosign/cli/verify/verify_bundle.go 24.64% 97 Missing and 10 partials ⚠️
cmd/cosign/cli/verify/verify_blob_attestation.go 40.19% 49 Missing and 12 partials ⚠️
cmd/cosign/cli/verify/verify_blob.go 40.38% 23 Missing and 8 partials ⚠️
cmd/conformance/main.go 0.00% 3 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main    #3854      +/-   ##
==========================================
- Coverage   40.10%   36.71%   -3.39%     
==========================================
  Files         155      203      +48     
  Lines       10044    12880    +2836     
==========================================
+ Hits         4028     4729     +701     
- Misses       5530     7556    +2026     
- Partials      486      595     +109     

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

Copy link
Member

@codysoyland codysoyland left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice to see some progress made here! I would love to have some tests on this before merging, but I do have some ideas on this topic that we could perhaps postpone and iterate later after merging this. Basically, I would like to come to some agreement on how we treat cosign's public API surface, now that sigstore-go is becoming the standard Go sigstore implementation. Cosign has a number of public API functions in the main package github.com/sigstore/cosign/v2/pkg/cosign, including cosign.VerifyBlobSignature, which this PR does not affect. I kind of want to shift this new bundle logic out of the cli/verify package and into the main cosign package to centralize verification logic. Of course, we would need to decide if we add new funcs there (e.g. cosign.VerifyBlobSignatureWithBundle) or try to enhance the existing ones. I do like the idea of API continuity -- adding new fields to CheckOpts for the trusted root (and perhaps some of the verification option types from sigstore-go), and deprecating several of the existing fields. Existing API users of cosign would benefit from a smoother transition to bundles/trusted roots this way. However, I can see how we might instead deprecate these public API funcs and point people at sigstore-go. Of course, for container image verification, the set of tradeoffs is different, because as we discussed, we don't want to add container image verification to sigstore-go. I have been looking at updating cosign's container image verification to use bundles/trusted roots, and IMO, a more iterative approach to updating existing public APIs would be cleaner. Would love to hear your and @haydentherapper's thoughts especially on this.

@@ -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
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would probably put this change in its own PR as a bug fix (would be good to have in release notes).

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've split this out into #3877

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) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would really like to see tests for this. I would take a look at the tests in verify_blob.go -- I authored the keylessStack test helper there (it was bundled into an advisory so my name is not in git-blame) which lets you do integration-y tests without running the e2e tests. This func should be covered if we assemble a trusted root there and try to verify some blobs.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I added a happy-path test for assembleNewBundle and verifyNewBundle.

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) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Would it be better to assemble a CheckOpts and pass it in here instead of continuing to grow this func signature? I have some more thoughts on this in the main review.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think I'm going to leave this as-is for now, but definitely open to refactoring this to whatever is decided in #3879.

@cmurphy
Copy link
Contributor

cmurphy commented Sep 11, 2024

This addresses verify-blob*, what about container image verification? A trust root still needs to be provided in that case, right? Can #3700 be considered "Fixed" without that?

@codysoyland
Copy link
Member

I have written up some more thoughts on what I wrote above regarding cosign's public API. Would love to have a discussion there: #3879

Also remove fix that is being handled in sigstore#3877

Signed-off-by: Zach Steindler <steiza@github.com>
@cmurphy cmurphy mentioned this pull request Sep 13, 2024
1 task
@codysoyland
Copy link
Member

Thanks for adding tests! Generally it's awesome to see the trusted root supported in verification. But I do have some concerns though that I'm not entirely sure how to address. My main concern is over the assembly/verification of a bundle with sigstore-go whenever a TrustedRoot is provided on these lines.

@cmurphy is working on TUF v2 support including support for the TrustedRoot, and her approach is to assemble the fields in CheckOpts from the TrustedRoot file from TUF. I think there's a good opportunity for code reuse here, as we can support the TrustedRoot using cosign's legacy code paths. I think that's a lot safer and less prone to bugs, and I would feel good about the consistent behavior when using the trusted root via TUF and via a CLI flag. I think it's important to preserve cosign's legacy verification logic as we switch to the TrustedRoot, as some of the features in cosign are difficult or unsupported in sigstore-go right now, for example the legacy container image verification code will be pretty difficult to implement correctly in sigstore-go.

Longer term, I think some variant of my proposal in #3879 of adding a fields for root.TrustedMaterial to CheckOpts will be the best way to support trusted root for cosign's legacy verification. Currently, cosign centralizes all verification in verifyInternal, and adding first-class support for the trusted root there makes a lot sense to me, but it does bring challenges.

Would love to hear @haydentherapper and @cmurphy's thoughts on this prior to approving. Personally I would like to see the TUF v2 support and this PR share some logic, but I could be convinced otherwise.

@steiza
Copy link
Member Author

steiza commented Sep 16, 2024

Yeah, I think @cmurphy @codysoyland @haydentherapper and I need to align on approach here.

Essentially, I think the question boils down to "how should cosign perform verification when it has a trusted root file (obtained via TUF or otherwise supplied directly)?"

Option 1 is to take the trusted root contents, map it to CheckOpts, and use the existing cosign verification paths. That's what @cmurphy is doing with #3844 and @codysoyland is proposing with #3879.

Option 2 is to use sigstore-go to perform verification when there is a trusted root, which is what I'm proposing with this pull request.

The benefit of option 1 is lots of parts of cosign understand / expect CheckOpts, and so it's easy for the changes to be connected to many different parts of cosign.

A disadvantage of option 1 is that many parts of the existing cosign verification path assume there's only one set of verification material, whereas a trusted root has several sets of verification material with overlapping time ranges. I don't have a full list of places that would need to be updated, but looking through pkg/cosign/verify for example:

A benefit of option 2 is that sigstore-go today supports trusted roots, and has had substantial testing and usage of those codepaths.

Disadvantages of option 2 are:

@cmurphy
Copy link
Contributor

cmurphy commented Sep 17, 2024

Support for trusted roots needs to be added per-command

Along these lines, there are potential pitfalls of missing commands that need a trust root, such as for signing, and needing to duplicate code if new commands are added.

@haydentherapper
Copy link
Contributor

haydentherapper commented Sep 18, 2024

Chiming in on this discussion, I haven't read over the code yet, sorry in advance if I say anything inaccurate related to the PR.

Part of the goal of "theseus" was to slowly replace parts of Cosign such that users should be minimally impacted with each major change, but after enough major revisions, we'll have Cosign into the state that we want [1].

With the previous PRs @steiza authored, Zach brought Cosign up to parity with the other Sigstore clients when using the public good instance. Implementing this by only using sigstore-go APIs and controlling execution of this code with a dedicated flag was the right choice, as this made it quicker to integrate and start getting users used to the "new" (new to Cosign users) bundle formats.

While using sigstore-go directly would let us iterate faster, this sounds more like a rewrite of Cosign than a slow, deliberate migration that lets us make small breaking changes over a number of major releases. For some context, Cosign v2 had soooo many breaking changes and we got a lot of community feedback around this. I think we'd still have users on Cosign v1 if we hadn't had to make a breaking change with the TUF metadata that forced users onto v2 (which we also received feedback on!). I want to avoid this mistake with Cosign v3, preserving as much of the same functionality as possible while making small breaking changes that users should be fine with (e.g removing all CLI flags for individual trust root material in favor of the trust root bundle - same functionality, different format, and we can provide migration tools).

One of the other benefits of #3879 is it being easier to review and know we have maintained parity. We have great e2e testing in Cosign now, and ideally our refactors will largely not touch e2e testing (except when handling a new format) which will give us confidence that we haven't broken any existing features.

I also recognize that the verification logic in Cosign is not up to par with the specification - we should discuss this more. This may be an area where we need to work around Cosign's limitations and document the gaps (e.g lack of validity window verification, assumptions that aren't present in in the spec). From there we can choose to either implement these gaps, or push forward for v3 knowing we have these gaps, and then tee up those fixes (either by directly using sigstore-go or implementing the gaps) for v4. Again my goal is small, deliberate changes that give us confidence we aren't unexpectedly changing Cosign's behavior and especially aren't changing it without a migration path.

[1] We need to agree on what the end goals are as well, but I roughly would summarize it as:

  1. the CLI interface is greatly simplified when using PGI,
  2. we retain support for self-managed keys/private PKI (possibly under a dedicated CLI subcommand),
  3. blob signing code duplication between Cosign and sigstore-go is reduced,
  4. code duplication is reduced across all commands,
  5. the bundle spec for containers is implemented,
  6. Cosign as an API - container signing and private PKI only, blob signing should use sigstore-go (this needs more discussion)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants