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

Add the ability to contruct TrustRoot from targets #247

Merged
merged 6 commits into from
Aug 21, 2024

Conversation

bkabrda
Copy link
Contributor

@bkabrda bkabrda commented Jul 30, 2024

Summary

This PR:

  • Adds the ability to construct a TrustedRoot instance from certificate authorities for fulcio/tsa and transparency logs for ctlog and rekor
  • Adds JSON serialization logic for the TrustedRoot structure

This was requested in sigstore/scaffolding#1191 (review) by @haydentherapper.

Release Note

  • Added NewTrusted function to enable creating TrustedRoot from CertificateAuthority and TransparencyLogs instances
  • Added functionality to serialize TrustedRoot to JSON

Documentation

I don't think this needs additional documentation, the added public function has a godoc that I think is sufficient.

Signed-off-by: Slavek Kabrda <bkabrda@redhat.com>

// NewTrustedRootFromTargets initializes a TrustedRoot object from a mediaType string and
// a slice of targets. These targets are expected to be PEM-encoded public keys/certificate chains.
// This method of constructing the TrustedRoot has some shortcomings which can be aided by manually
Copy link
Contributor

Choose a reason for hiding this comment

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

If we don't address these, the concern would be that one would construct a trust root with NewTrustedRootProtobuf(json), then marshal it with MarshalJSON, and get back a different trust root.

We should have a test that demonstrates JSON-input -> TrustedRoot struct -> JSON-output, where JSON-input == JSON-output.

What if the API takes in a TrustedRoot struct, fully populated, and outputs JSON? And if certain fields are missing, then we can have reasonable defaults (e.g if TransparencyLog.HashFunc is not set, then pick SHA256)

Copy link
Contributor Author

Choose a reason for hiding this comment

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

So there's couple different things to note here:

  • The only thing that this new method does is it constructs a TrustedRoot from given targets; this does not concern the JSON representation of the TrustedRoot. On the contrary this is most useful when there is no TrustedRoot yet and one needs to be constructed from public keys/certs.
  • However, I understand your concern about having the ability to marshal TrustedRoot - which was also added by this PR - I think the only problem right now is the "Uri" argument which I left unfilled for CertificateAuthority struct, so I probably need to fix that and I can also add a test that you suggest.
  • I think taking in a TrustedRoot struct fully populated defeats the purpose of this PR - that won't solve a lot of what I need in Add functionality to generate trusted_root.json by the TUF server scaffolding#1191 and most of the code will need to stay there to create the TrustedRoot struct in the first place.

So what I propose is adding the test as you suggested, and fixing any issues that pop up, but otherwise leaving this PR as is. Does that sound good?

}
}

// TODO: should this rather be the innermost cert?
Copy link
Contributor

Choose a reason for hiding this comment

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

That's a good point. I think it should be the most restrictive validity period of all of the CA certificates, which is likely the intermediate lowest in the chain.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Sounds good, I can fix that.

Copy link
Contributor Author

@bkabrda bkabrda Jul 31, 2024

Choose a reason for hiding this comment

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

Done in ab939cc

Signed-off-by: Slavek Kabrda <bkabrda@redhat.com>
@bkabrda
Copy link
Contributor Author

bkabrda commented Jul 31, 2024

I updated the PR to add a test for deserializing and then again serializing a TrustedRoot instance and fixed couple minor issues related to that (plus linting issues). The test is failing now, only because the time instances are serialized in a different way - the provided sample has a format of "2021-03-07T03:20:29.000Z", while timestamppb serializes as "2021-03-07T03:20:29Z" (no 000Z at the end).

I don't immediately see a good way to fix the time serialization format, but I'll keep looking into it. I think this is a difference that we could potentially live with, but I also understand if you think this must be fixed before merging - let me know. Thanks!

@bkabrda
Copy link
Contributor Author

bkabrda commented Aug 1, 2024

FWIW, it looks like the .000 in the serialized time is intentionally stripped. I think changing that would be extremely hard and I don't think it's worth it (especially if technically we're emitting JSON that is the same, this is just a matter of formatting). I'll fix the test so that it ignores the .000 differences.

Signed-off-by: Slavek Kabrda <bkabrda@redhat.com>
Copy link
Contributor

@cmurphy cmurphy left a comment

Choose a reason for hiding this comment

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

I don't think this needs additional documentation, the added public function has a godoc that I think is sufficient.

I think adding a short usage example in the docs/ folder would be great actually, just to make it a little more discoverable when people are wondering what different things sigstore-go can do.

Signed-off-by: Slavek Kabrda <bkabrda@redhat.com>
@bkabrda
Copy link
Contributor Author

bkabrda commented Aug 2, 2024

Good point @cmurphy - I added a simple example of a binary that can construct and pretty print a trusted_root.json file - let me know what you think!

// - Merkle Tree hash function is hardcoded to SHA256, as this is not derivable from the public key.
// - Each certificate chain is expected to be given as a single target, where there is a newline
// between individual certificates. It is expected that the certificate chain is ordered (root last).
func NewTrustedRootFromTargets(mediaType string, targets []RawTrustedRootTarget) (*TrustedRoot, 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 think there's a lot of use-cases for making it easier to construct a trusted root, and the challenge here is going to be to make an API that's flexible enough to cover those use-cases. If you're in something like cosign verify-blob with --certificate-chain and --timestamp-certificate-chain, it's totally fine to throw together something quickly to verify. But if you're the public good instance (or a private instance) you're going to want something that supports rotating key material with expiry times.

In pkg/root/trusted_root.go we already have these nice intermediate representations of CertificateAuthority and TransparencyLog, so what if we added there something like:

func NewTrustedRoot(
    certificateAuthorities []CertificateAuthorities, 
    certificateTransparencyLogs []TransparencyLog,
    timestampAuthorities []CertificateAuthorities,
    transparencyLogs []TransparencyLog
) TrustedRoot

This API would be similar to sign.BundleOptions, which is nice.

So if you're in cosign verify-blob and all you have is a PEM-encoded byte string from --certificate-authority, you could construct a minimal CertificateAuthority to pass in to NewTrustedRoot, but if you had a start and end time that certificate was valid for, you could also supply that information. But (crucially!) then we don't have to guess as to what the configuration should be, and we don't risk returning a half-configured object that the caller then has to go and fix up.

That would be enough if you just need a TrustedRoot to call into sigstore-go, but if you want to output a JSON file we might need one more step. I love the idea of adding a function like:

func (tr *TrustedRoot) MarshalJSON() ([]byte, error)

but instead of the current implementation of constructProtoTrustRoot(), maybe we could see if TrustedRoot.trustedRoot is already set up (like if it came from NewTrustedRootFromProtobuf()) or not (like if it came from the proposed NewTrustedRoot()) and only populate content in TrustedRoot.trustedRoot if it hasn't already been set up. This would ensure that if you load from a JSON file and call MarshalJSON() you get the same content back.

Does that make sense? It might make sense to get feedback on the API and approach from a few folks and settle on a direction before we move ahead with the implementation.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Thanks for your input! I've been thinking about this and I have this alternative implementation proposal:

  • Instead of having the current NewTrustedRootFromTargets function, I will add NewTrustedRoot as you suggested.
  • I will also expose functions that can instantiate CertificateAuthority and TransparencyLog from certificate/public key material. These functions already exist in this PR, but are private. I'll revisit them and ensure their API is sane, name them better etc, so that they can be exposed. This will make sure that my usecase of easily instantiating the whole TrustedRoot object from the certificate/public key material is still very easy using this library.
  • In terms of the MarshalJSON call, it's important to note that we publicly allow users to access and modify the structures that form the TrustedRoot object via TimestampingAuthorities, FulcioCertificateAuthorities functions etc. This means that if we just use the current trustedRoot, we could be serializing an outdated representation of TrustedRoot.

Does that make sense? Also CC @haydentherapper, I think this should also address your original concern. Let me know if this makes sense.

Copy link
Member

Choose a reason for hiding this comment

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

I will also expose functions that can instantiate CertificateAuthority and TransparencyLog from certificate/public key material.

I think any helper functions like this should live outside of sigstore-go. It might seem silly, but one of the design goals of sigstore-go is to have opinionated interfaces that get wrapped by things that call sigstore-go to be more user-friendly. A great example of this is cosign, where you can supply information via a URL, a path on disk, a string of encoded data, etc. We want that flexibility to live in cosign, instead of having several similar functions in sigstore-go that support slightly different data input.

In terms of the MarshalJSON call, it's important to note that we publicly allow users to access and modify the structures that form the TrustedRoot object via TimestampingAuthorities, FulcioCertificateAuthorities functions etc. This means that if we just use the current trustedRoot, we could be serializing an outdated representation of TrustedRoot.

Got it - makes sense! Like you say, we should make sure whatever we marshal reflects the current state of the object.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I think any helper functions like this should live outside of sigstore-go. It might seem silly, but one of the design goals of sigstore-go is to have opinionated interfaces that get wrapped by things that call sigstore-go to be more user-friendly. A great example of this is cosign, where you can supply information via a URL, a path on disk, a string of encoded data, etc. We want that flexibility to live in cosign, instead of having several similar functions in sigstore-go that support slightly different data input.

I certainly don't want to go against the design goal that you folks have. TBH I wasn't even aware that this was the design goal - is there a good document that outlines design goals?

The only reason why I opened this PR in here was that @haydentherapper suggested it in sigstore/scaffolding#1191 (comment). I can definitely change the PR in the way you suggested, but I would like Hayden to comment on here to ensure I'm doing changes that everybody will agree with conceptually.

Signed-off-by: Slavek Kabrda <bkabrda@redhat.com>
@bkabrda
Copy link
Contributor Author

bkabrda commented Aug 9, 2024

@steiza @haydentherapper I believe I have now addressed all your concerns and that this should be acceptable. Can I ask you for another round of reviews?

FWIW I removed the example, because the only really useful added feature is the serialization, which I don't think needs an example. I will also change the description to match what the PR currently does.

steiza
steiza previously approved these changes Aug 9, 2024
Copy link
Member

@steiza steiza left a comment

Choose a reason for hiding this comment

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

This looks great - thank you!

return fmt.Errorf("failed converting rekor log %s to protobuf: %w", logID, err)
}
tr.trustedRoot.Tlogs = append(tr.trustedRoot.Tlogs, tlProto)
// ensure stable sorting of the slice
Copy link
Member

Choose a reason for hiding this comment

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

Very nice!

return &caProto, nil
}

func transparencyLogToProtobufTL(tl TransparencyLog) (*prototrustroot.TransparencyLogInstance, error) {
Copy link
Member

@kommendorkapten kommendorkapten Aug 21, 2024

Choose a reason for hiding this comment

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

Suggested change
func transparencyLogToProtobufTL(tl TransparencyLog) (*prototrustroot.TransparencyLogInstance, error) {
func transparencyLogToProtobufTL(tl *TransparencyLog) (*prototrustroot.TransparencyLogInstance, error) {

Any reason not to receive this via a pointer to avoid excessive copying?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

You're absolutely right, I missed that. I will fix it.

return nil
}

func certificateAuthorityToProtobufCA(ca CertificateAuthority) (*prototrustroot.CertificateAuthority, error) {
Copy link
Member

Choose a reason for hiding this comment

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

Suggested change
func certificateAuthorityToProtobufCA(ca CertificateAuthority) (*prototrustroot.CertificateAuthority, error) {
func certificateAuthorityToProtobufCA(ca *CertificateAuthority) (*prototrustroot.CertificateAuthority, error) {

Same here on receiving via value or pointer.

if err != nil {
return fmt.Errorf("failed converting fulcio cert chain to protobuf: %w", err)
}
tr.trustedRoot.CertificateAuthorities = append(tr.trustedRoot.CertificateAuthorities, caProto)
Copy link
Member

Choose a reason for hiding this comment

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

We probably want to sort the certificate authorities too (and TSAs) similar to what you did for the tlogs.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Ah, you're right, I forgot to do that. I'll submit a fix for it as well.

Signed-off-by: Slavek Kabrda <bkabrda@redhat.com>
@bkabrda
Copy link
Contributor Author

bkabrda commented Aug 21, 2024

@kommendorkapten thanks again for your review, I just pushed a commit that addresses your points. I also refactored the sorting functionality to functions to avoid copy-paste code (and I realized it was mistakenly called in every iteration of the loops, while we just need to do it after the loops, so I fixed that as well). Let me know if this looks good now!

@@ -51,6 +51,7 @@ type CertificateAuthority struct {
Leaf *x509.Certificate
ValidityPeriodStart time.Time
ValidityPeriodEnd time.Time
URI string
Copy link
Contributor

Choose a reason for hiding this comment

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

FYI @cmurphy, about the URI being added for TSAs

Copy link
Contributor

@haydentherapper haydentherapper left a comment

Choose a reason for hiding this comment

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

Thanks!

@haydentherapper haydentherapper merged commit 2198ac3 into sigstore:main Aug 21, 2024
10 checks passed
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.

5 participants