-
Notifications
You must be signed in to change notification settings - Fork 56
Root hash signature verification #527
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
Why have the What's been throwing me off w.r.t. expanding the |
The types under |
In this context, I think what we need to do is add a new |
Ah, OK -- so that extends to a lot of the extremely close types, same with |
OK, this if falling into place now that I actually have a More of a nit: would this be appropriate to live in |
Yep, exactly; there's a roughly 1:1 correspondence between many |
I think I see how to translate the Go implementation just based on skimming some Go docs, but I'm hardly proficient at Go. I'll get my best effort up but would appreciate a second glance. |
3e3a2d7
to
5113033
Compare
I'm unsure how to handle the contents of the
I've been trying to determine what these strings indicate based on the And in terms of usage, should this string be parsed into a pydantic-verified class or something? |
It took a little bit of digging, but it looks like a checkpoint is a
Yes! This should be one or more Pydantic data models. |
Specifically, here's the type SignedNote struct {
// Textual representation of a note to sign.
Note string
// Signatures are one or more signature lines covering the payload
Signatures []note.Signature
} and a type SignedCheckpoint struct {
Checkpoint
SignedNote
} where unmarshalling is defined as: func (r *SignedCheckpoint) UnmarshalText(data []byte) error {
s := SignedNote{}
if err := s.UnmarshalText([]byte(data)); err != nil {
return fmt.Errorf("unmarshalling signed note: %w", err)
}
c := Checkpoint{}
if err := c.UnmarshalCheckpoint([]byte(s.Note)); err != nil {
return fmt.Errorf("unmarshalling checkpoint: %w", err)
}
*r = SignedCheckpoint{Checkpoint: c, SignedNote: s}
return nil
} and the underlying type Checkpoint struct {
// Origin is the unique identifier/version string
Origin string
// Size is the number of entries in the log at this checkpoint.
Size uint64
// Hash is the hash which commits to the contents of the entire log.
Hash []byte
// OtherContent is any additional data to be included in the signed payload; each element is assumed to be one line
OtherContent []string
}
// UnmarshalText parses the common formatted checkpoint data and stores the result
// in the Checkpoint.
//
// The supplied data is expected to begin with the following 3 lines of text,
// each followed by a newline:
// <ecosystem/version string>
// <decimal representation of log size>
// <base64 representation of root hash>
// <optional non-empty line of other content>...
// <optional non-empty line of other content>...
//
// This will discard any content found after the checkpoint (including signatures)
func (c *Checkpoint) UnmarshalCheckpoint(data []byte) error {
l := bytes.Split(data, []byte("\n"))
if len(l) < 4 {
return errors.New("invalid checkpoint - too few newlines")
}
origin := string(l[0])
if len(origin) == 0 {
return errors.New("invalid checkpoint - empty ecosystem")
}
size, err := strconv.ParseUint(string(l[1]), 10, 64)
if err != nil {
return fmt.Errorf("invalid checkpoint - size invalid: %w", err)
}
h, err := base64.StdEncoding.DecodeString(string(l[2]))
if err != nil {
return fmt.Errorf("invalid checkpoint - invalid hash: %w", err)
}
*c = Checkpoint{
Origin: origin,
Size: size,
Hash: h,
}
if len(l) >= 3 {
for _, line := range l[3:] {
if len(line) == 0 {
break
}
c.OtherContent = append(c.OtherContent, string(line))
}
}
return nil
} |
sigstore/transparency.py
Outdated
|
||
signatures: list[Signature] = [] | ||
|
||
sig_parser = re.compile(r"\u2014 (\S+) (\S+)\n") |
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'm not sure if a regex is the right tool here. This is mostly intended to follow the Go implementation, which does something similar with fscanf
. It does enforce the line-oriented format more succinctly than doing something like... breaking on spaces and asserting the formatting is correct. However I'm not sure how well this would handle a malformed input.
For the same example,
Is parsed into
|
What I've been hitting my head against is gettin the inclusion proof's member root hash into the same "format" as the parsed What I had to learn is that Now everything lines up 🙂 |
28d9f75
to
2eb51ca
Compare
@@ -133,3 +135,33 @@ def verify_merkle_inclusion(entry: LogEntry) -> None: | |||
f"Inclusion proof contains invalid root hash: expected {inclusion_proof}, calculated " | |||
f"{calc_hash}" | |||
) | |||
|
|||
|
|||
def verify_checkpoint(client: RekorClient, entry: LogEntry) -> None: |
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.
verifier.py
has a block intended for oneline verifications that previously called into the verify_merkle_inclusion
here. So, by adding an additional parallel test, this seemed like the natural place for this function to live -- however, it's not particularly "merkle" related.
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.
Yeah, I think this should probably live under _internal.rekor.checkpoint
.
if inclusion_proof.checkpoint is None: | ||
return | ||
|
||
# verififcaiton occurs in two stages: |
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.
# verififcaiton occurs in two stages: | |
# verification occurs in two stages: |
if inclusion_proof.checkpoint is None: | ||
return |
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.
Shouldn't this be an error case? We probably shouldn't silently accept inclusion proofs that don't contain a checkpoint.
signed_checkpoint = SignedCheckpoint.from_text(inclusion_proof.checkpoint) | ||
|
||
# FIXME(jl): produces an invalid signature exception -- am I using the keyring correctly? | ||
# signed_checkpoint.signed_note.verify(client) |
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 might be missing it, but where is this verify
method defined? I don't see it declared on SignedNote
.
Signed-off-by: Jack Leightcap <[email protected]> TASK(jl): `checkpoint` breakout Signed-off-by: Jack Leightcap <[email protected]>
Signed-off-by: Jack Leightcap <[email protected]>
Signed-off-by: Jack Leightcap <[email protected]>
Signed-off-by: Jack Leightcap <[email protected]>
Signed-off-by: Jack Leightcap <[email protected]>
Signed-off-by: Jack Leightcap <[email protected]>
Signed-off-by: Jack Leightcap <[email protected]>
Signed-off-by: Jack Leightcap <[email protected]>
Signed-off-by: Jack Leightcap <[email protected]>
Signed-off-by: Jack Leightcap <[email protected]>
Signed-off-by: Jack Leightcap <[email protected]>
Superseded by #634. |
Closes #248.