-
Notifications
You must be signed in to change notification settings - Fork 33
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
Conversation
17737b9
to
2146549
Compare
pkg/bundle/bundle.go
Outdated
@@ -320,6 +381,30 @@ func (b *Bundle) TlogEntries() ([]*tlog.Entry, error) { | |||
return tlogEntries, nil | |||
} | |||
|
|||
func (b *BundleForRekorV2) TlogV2Entries() ([]tlog.LogEntry, error) { |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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.
pkg/verify/tlog.go
Outdated
logEntriesVerified := 0 | ||
|
||
for _, entry := range entries { | ||
// TODO: validate hashedrekord/dsse obj? |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
pkg/tlog/entry.go
Outdated
|
||
return entry, nil | ||
} | ||
|
||
func ValidateLogEntryBody(entry LogEntry) error { | ||
if e, ok := entry.(*rekorV1Entry); ok { |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
pkg/verify/tlog.go
Outdated
if !bytes.Equal(entry.Signature(), entitySignature) { | ||
return fmt.Errorf("transparency log signature does not match") | ||
} | ||
protoPubKey := entry.PublicKey() |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
examples/sigstore-go-signing/main.go
Outdated
} | ||
opts.TransparencyLogs = append(opts.TransparencyLogs, sign.NewRekor(rekorOpts)) | ||
} | ||
} | ||
|
||
opts.UseRekorV2 = rekorVersion == uint32(2) |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
0912cb6
to
dabc2ef
Compare
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. |
03cf220
to
caddd08
Compare
2798478
to
e426064
Compare
e2e tests waiting on sigstore/scaffolding#1606 and #489 |
There was a problem hiding this 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))) |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
pkg/sign/transparency.go
Outdated
return nil | ||
} | ||
|
||
func (r *Rekor) getRekorV2TLE(ctx context.Context, pubKeyPEM []byte, b *protobundle.Bundle) (*protorekor.TransparencyLogEntry, error) { |
There was a problem hiding this comment.
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.
pkg/tlog/entry.go
Outdated
} | ||
entry.tle.InclusionProof = &v1.InclusionProof{ | ||
LogIndex: logIndex, | ||
RootHash: []byte(*inclusionProof.RootHash), |
There was a problem hiding this comment.
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.
pkg/tlog/entry.go
Outdated
Hashes: tle.InclusionProof.Hashes, | ||
Checkpoint: tle.InclusionProof.Checkpoint, | ||
} | ||
rootHash := make([]byte, hex.EncodedLen(len(tle.InclusionProof.RootHash))) |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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 { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
For the future, we should make https://github.com/sigstore/rekor-tiles/blob/28f3c79d9855192fde1e896c27c4b7b225c7436f/pkg/types/hashedrekord/hashedrekord.go#L68 public and reuse this
There was a problem hiding this comment.
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
.
There was a problem hiding this comment.
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) |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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)
There was a problem hiding this comment.
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.
pkg/tlog/entry.go
Outdated
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())), |
There was a problem hiding this comment.
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?).
pkg/verify/tlog.go
Outdated
|
||
var treeIDSuffixRegex = regexp.MustCompile(".* - [0-9]+$") | ||
|
||
func hasSTH(entry *tlog.Entry) bool { |
There was a problem hiding this comment.
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.
Good work! |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Looks really great!
pkg/tlog/entry.go
Outdated
InclusionProof: inclusionProof, | ||
hashes := make([][]byte, len(inclusionProof.Hashes)) | ||
for i, s := range inclusionProof.Hashes { | ||
hashes[i] = []byte(s) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Do we need to hex.DecodeString
here as well? https://github.com/sigstore/rekor/blob/c0f3b8c639cb52bb2fa760465ad7d49e2ff19570/pkg/api/entries.go#L96
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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.
There was a problem hiding this 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]>
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🎉
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