Skip to content

Add support for Rekor V2 for signing and verification #481

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

Merged
merged 1 commit into from
Jun 27, 2025

Conversation

cmurphy
Copy link
Contributor

@cmurphy cmurphy commented May 16, 2025

Add support for handling and uploading a Rekor v2 entry. Replace
reliance on rekor v1 with the TransparencyLogEntry from protobuf-specs
as much as possible to reduce divergence in how different entries are
handled. Add new functions where needed to avoid breaking backwards
compatibility with existing functions.

Partial sigstore/rekor-tiles#349

Summary

Release Note

Documentation

@cmurphy cmurphy force-pushed the rekor-v2 branch 2 times, most recently from 17737b9 to 2146549 Compare May 19, 2025 21:10
@@ -320,6 +381,30 @@ func (b *Bundle) TlogEntries() ([]*tlog.Entry, error) {
return tlogEntries, nil
}

func (b *BundleForRekorV2) TlogV2Entries() ([]tlog.LogEntry, error) {
Copy link
Contributor

Choose a reason for hiding this comment

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

What's the reason for a second TlogEntries function?

Can we reuse the existing and when constructing the bundle, have a validation step that the expected fields are unset?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

The original one uses the rekor v1 generated models to parse and marshal the entry object. It's completely incompatible with the rekor v2 protobuf types.

logEntriesVerified := 0

for _, entry := range entries {
// TODO: validate hashedrekord/dsse obj?
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 have the artifact, we should verify that the artifact hash matches the hashedrekord hash/dsse payload hash. This was something missing from the current validation function since we can't verify dsse envelope hashes, but this is now resolved since we've dropped the envelope hash.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Maybe I'm missing it but I don't think we have access to the artifact here. We just have the bundle.


return entry, nil
}

func ValidateLogEntryBody(entry LogEntry) error {
if e, ok := entry.(*rekorV1Entry); ok {
Copy link
Contributor

Choose a reason for hiding this comment

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

Ideally we don't have to depend on Rekor v1. What do you think about creating a small Rekor entry verification library that doesn't depend on Rekor v1's generated types, unmarshals canonicalized bodies, and handles validation? We could replace calls to UnmarshalEntry.

I'm thinking something like:

type RekorEntry interface {
  Kind() string
  Version() string
  Validate() error // validates expected fields are set
  Digest() []byte // return payload hash for DSSE/intoto
  Verifiers() []Verifier // from rekor-tiles
  Signature() []byte
}

And then have implementations of the interface for a minimal set of Rekor entries, HashedRekordV001, HashedRekordV002, DSSEV001, DSSEV002 and IntotoV002.

Then have one function like UnmarshalRekorEntry that parses a canonicalized body into an RekorEntry given its kind and version, and gracefully handles unknown kinds/versions. (Or we can recreate the Rekor v1 types and typeimpls, though I think it's a little overkill and some switch statements would be cleaner)

We probably would need to do some copying from Rekor v1, though maybe there isn't actually too much to copy, I think we just need each entry defined as a struct with json tags to handle unmarshalling the canonicalized body.

Where this lives, here or in sigstore/sigstore?

I think this library would be quite useful across Go clients.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

To the extent possible I've changed this to use the TLE instead of the rekor v1 OpenAPI models so that the divergence is minimized. I do not think a separate library would be appropriate. Sigstore-go is the only thing that would be using it and cosign eventually should be consuming sigstore-go.

if !bytes.Equal(entry.Signature(), entitySignature) {
return fmt.Errorf("transparency log signature does not match")
}
protoPubKey := entry.PublicKey()
Copy link
Contributor

Choose a reason for hiding this comment

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

IIRC this is confusingly named (to mirror rekor v1), this is a key or certificate. It'd be nice to rename this or change this to return a verifier, and conditionally compare either a certificate or key to what's in the bundle.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

This cannot be changed, it's a public method.

}
opts.TransparencyLogs = append(opts.TransparencyLogs, sign.NewRekor(rekorOpts))
}
}

opts.UseRekorV2 = rekorVersion == uint32(2)
Copy link
Contributor

Choose a reason for hiding this comment

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

We also need to enforce that if Rekor v2 is used to sign, there is at least one timestamp authority.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

sigstore-go verification policies are composable, it's up to the caller to decide whether to check for a timestamp or not. The bundle signer policies already enforce that there is a timestamp somewhere, and if the entry comes from rekor v2 there will be no integrated timestamp, so it will fail unless there is a TSA timestamp.

@cmurphy cmurphy force-pushed the rekor-v2 branch 2 times, most recently from 0912cb6 to dabc2ef Compare June 4, 2025 23:37
@cmurphy
Copy link
Contributor Author

cmurphy commented Jun 4, 2025

Refactored to reduce the amount of v1/v2 divergence to a point I'm happier with. Still need to address time, DSSE, and the verify example.

@cmurphy cmurphy force-pushed the rekor-v2 branch 5 times, most recently from 03cf220 to caddd08 Compare June 10, 2025 17:36
@cmurphy cmurphy force-pushed the rekor-v2 branch 3 times, most recently from 2798478 to e426064 Compare June 17, 2025 22:40
@cmurphy
Copy link
Contributor Author

cmurphy commented Jun 17, 2025

e2e tests waiting on sigstore/scaffolding#1606 and #489

@cmurphy cmurphy changed the title DRAFT: Add support for Rekor V2 for verification Add support for Rekor V2 for signing and verification Jun 18, 2025
@cmurphy cmurphy marked this pull request as ready for review June 18, 2025 17:20
@cmurphy cmurphy requested a review from a team as a code owner June 18, 2025 17:20
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.

Great work on this!

// Rekor v2 requires a timestamp authority, it will not provide integrated timestamps.
// Verification will fail if a timestamp authority is not provided for Rekor v2.
if len(opts.TimestampAuthorities) == 0 {
verifierOptions = append(verifierOptions, verify.WithIntegratedTimestamps(len(opts.TransparencyLogs)))
Copy link
Contributor

Choose a reason for hiding this comment

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

Is it possible to use the Rekor API version to return an error here rather than fail verification later? This is functional so if not, that's fine, but this will surface an error that won't make the issue obvious to the caller.

Copy link
Contributor Author

@cmurphy cmurphy Jun 18, 2025

Choose a reason for hiding this comment

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

I tried but I could not find a good way to do that, there is no way to pass through the rekor API version without changing the method signature or a type definition. It could be deduced by looking at the KindVersion the tlog entry and checking if it is a hashedrekord v002 or dsse v002 type, but I don't think that's a foolproof indicator (what if we add more types to Rekor v2? what if we add those v002 types to Rekor v1? the type of entry is not technically related to whether a RFC3161 timestamp is required). I actually like the way this worked out because it's clear what the reality is, that if you're using Rekor v1 you need an integrated timestamp and if you're using Rekor v2 you need an external timestamp, and it will be clear in the verification result whether you did it correctly.

return nil
}

func (r *Rekor) getRekorV2TLE(ctx context.Context, pubKeyPEM []byte, b *protobundle.Bundle) (*protorekor.TransparencyLogEntry, error) {
Copy link
Contributor

Choose a reason for hiding this comment

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

nit, I'd rather use something like keyOrCertPEM in all the places that we pass either a key or certificate. The current name is because Rekor v1 tacked on certificates later on into the key field. I'd say the variable name change would help clarify that it's not just a key.

Can also refactor this in another PR.

}
entry.tle.InclusionProof = &v1.InclusionProof{
LogIndex: logIndex,
RootHash: []byte(*inclusionProof.RootHash),
Copy link
Contributor

Choose a reason for hiding this comment

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

The root hash and inclusion proof hashes are hex-encoded (parsing example), unless these are decoded elsewhere earlier in the stack, so we'll need to hex-decode for the TLE struct.

Hashes: tle.InclusionProof.Hashes,
Checkpoint: tle.InclusionProof.Checkpoint,
}
rootHash := make([]byte, hex.EncodedLen(len(tle.InclusionProof.RootHash)))
Copy link
Contributor

Choose a reason for hiding this comment

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

Same here, we don't need to hex-encode.

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 was facing problems in the unit tests where different tests expected different TLE objects to have the root hash hex encoded or not. This configuration was the only way I could get it to work. I will try to track down exactly why this was happening, but I think the fact that the unit and e2e tests are passing indicate that it does have to be hex encoded.

Copy link
Contributor

Choose a reason for hiding this comment

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

That might be a bug in the unit tests parsing the fields of the TLE. I know we also had a bug in rekor-tiles, but that's been fixed (sigstore/rekor-tiles#295). Hex encodings should only be needed when working with the OpenAPI generated objects.

return nil
}

func validateHashedRekordV002Entry(hr *rekortilespb.HashedRekordLogEntryV002) error {
Copy link
Contributor

Choose a reason for hiding this comment

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

Copy link
Contributor Author

Choose a reason for hiding this comment

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

We can't use that here because that validates a HashedRekordRequestV002, here we have a HashedRekordLogEntryV002.

Copy link
Contributor

Choose a reason for hiding this comment

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

Ah yea. So the opposite, we should move this and create a entry verifier library in rekor-tiles. Linking to sigstore/rekor-tiles#234 to figure this out later.

}

func (entry *Entry) Body() any {
return entry.logEntryAnon.Body
return base64.StdEncoding.EncodeToString(entry.tle.CanonicalizedBody)
Copy link
Contributor

Choose a reason for hiding this comment

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

We should revisit this method. It returning any makes it a bit hard to use in practice. Should this return a byte array that clients have to parse? Since it's always JSON, should this method unmarshal the body into a struct with version, kind, and spec?

Looks like Body() isn't called outside of tests, so I'd be fine with a breaking API change for this method.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Looks like Body() isn't called outside of tests, so I'd be fine with a breaking API change for this method.

I disagree with this. The definition of an exported method is that it could be called by anything outside the code base. I was very careful not to break any APIs in any of these changes. I'd be fine with adding a new BodyJSON method and marking this one as deprecated (in a separate PR), but not with changing this one.

Copy link
Contributor

Choose a reason for hiding this comment

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

Given the newness of the library, I don't feel as strongly around breaking changes, but I get what you're saying. We have a number of functions we've already marked as deprecated that we'll clean up in a later version, so let's do the same here, deprecate Body() and create a new function. Up to you for what this should return - I would find it most useful to return an unmarshaled struct with kind, version, and spec, or the marshaled JSON (and entry.logEntryAnon.Body can either be a JSON byte array, or a base64-encoded string of the JSON body (code), so we need to handle that)

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Can we do that in a separate PR? It's not really related to rekor v2 IMO. Then we can debate there what the right thing to return is.

Checkpoint: swag.String(entry.tle.GetInclusionProof().GetCheckpoint().GetEnvelope()),
Hashes: hashes,
LogIndex: swag.Int64(entry.tle.GetInclusionProof().GetLogIndex()),
RootHash: swag.String(string(entry.tle.GetInclusionProof().GetRootHash())),
Copy link
Contributor

Choose a reason for hiding this comment

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

This will need to be hex-encoded...sigh, these hex encodings are fun. Glad to not be working around OpenAPI v2 limitations now (iirc, there was something with byte arrays being unsupported?).


var treeIDSuffixRegex = regexp.MustCompile(".* - [0-9]+$")

func hasSTH(entry *tlog.Entry) bool {
Copy link
Contributor

Choose a reason for hiding this comment

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

Can we name this to be about rekor v1? In either v1 or v2, there should be a checkpoint/signed tree head.

@kommendorkapten
Copy link
Member

Good work!

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.

Looks really great!

InclusionProof: inclusionProof,
hashes := make([][]byte, len(inclusionProof.Hashes))
for i, s := range inclusionProof.Hashes {
hashes[i] = []byte(s)
Copy link
Contributor

Choose a reason for hiding this comment

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

Copy link
Contributor

Choose a reason for hiding this comment

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

Can we also see if we have test coverage for verifying proofs from both Rekor v1 and v2?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Can we also see if we have test coverage for verifying proofs from both Rekor v1 and v2?

This is mostly covered by the e2e tests which exercise both v1 and v2. They didn't catch this bug because this is now a deprecated function. I added a new unit test which tests this path and catches this bug.

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.

Great work! Just needs a go mod tidy

Add support for handling and uploading a Rekor v2 entry. Replace
reliance on rekor v1 with the TransparencyLogEntry from protobuf-specs
as much as possible to reduce divergence in how different entries are
handled. Add new functions where needed to avoid breaking backwards
compatibility with existing functions.

Signed-off-by: Colleen Murphy <[email protected]>
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.

🎉

@haydentherapper haydentherapper merged commit 2aab0d7 into sigstore:main Jun 27, 2025
12 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.

3 participants