-
Notifications
You must be signed in to change notification settings - Fork 20
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 TUF Client root resource syncing. #123
Conversation
working on rebase and commit signing. |
f05c4c5
to
ab4610a
Compare
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.
Sorry I put a change in here that requires you to rebase again.
I think the failure is that we only optionally include the BC provider if "java.version < 15", and really only the java 17 tests are failing. I think this library just needs to add the provider in always for this TUF stuff and we can get rid of the conditional static initializer in the key parser?
|
||
private static final int MAX_META_BYTES = 99 * 1024; // 99 KB | ||
private static final int MAX_UPDATES = | ||
1024; // Limit the update loop to retrieve a max of 1024 subsequent versions. |
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.
any reason why?
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.
added mention to spec.
// 5.3.10) Check expiration timestamp in trusted root is higher than fixed update start time. if | ||
// not, throw an error. | ||
ZonedDateTime expires = trustedRoot.getSignedMeta().getExpiresAsDate(); | ||
// If the |
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.
incomplete comment?
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.
fixed
LocalDateTime getExpires(); | ||
String getExpires(); | ||
|
||
default ZonedDateTime getExpiresAsDate() { |
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 this be a derived value?
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.
you can't map two fields to the same json element (I tried). Is that what you were suggestion?
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.
A derived value is an immutables construct that calculates this once instead of invoking every time: https://immutables.github.io/immutable.html#derived-attributes
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.
hmm. I like the idea! I'm not super psyched about changing this to an abstract class though as it blows up the whole interface hierarchy, we'd have to change all the sub types to abstract as well. WDYT?
@@ -1,130 +1,120 @@ | |||
{ |
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.
asra was saying something about just skipping the first few roots -- verifying the chain manually and only including N.root.json if you don't want to handle the timezone special cases
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 that was an option, but it seemed straight forward to make the client resilient to the different formats. 🤷
Signed-off-by: Patrick Flynn <patrick@chainguard.dev>
Signed-off-by: Patrick Flynn <patrick@chainguard.dev>
Signed-off-by: Patrick Flynn <patrick@chainguard.dev>
00bc7f6
to
fcc53af
Compare
Signed-off-by: Patrick Flynn <patrick@chainguard.dev>
Signed-off-by: Patrick Flynn <patrick@chainguard.dev>
Signed-off-by: Patrick Flynn <patrick@chainguard.dev>
Signed-off-by: Patrick Flynn <patrick@chainguard.dev>
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 good, some test resources have no newlines at EOF, is that intentional?
// spec for P-256 curve | ||
ECNamedCurveParameterSpec spec = ECNamedCurveTable.getParameterSpec("P-256"); | ||
// create a KeyFactory with ECDSA (Elliptic Curve Diffie-Hellman) algorithm and use | ||
// BouncyCastle |
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: spotless seems to have formatted this funny.
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.
fixed. sorry I thought I had caught all these.
// 5.3.1) record the time at start and use for expiration checks consistently throughout the | ||
// update. | ||
updateStartTime = ZonedDateTime.now(clock); | ||
Root trustedRoot; |
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: does trusted root really need to be defined here if you're just assigning it on the next line? (was there previously some try catch?)
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.
good point. fixed.
|
||
// 5.3.5) We've taken the liberty to modify 5.3.5 to just validate that the new root meta | ||
// matches the version we pulled based off of the pattern {version}.root.json. We know due to | ||
// the |
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: goofy comment formatting again
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.
spotless gets me again!
// 5.3.8) persist to repo | ||
Path localTrustRoot = localStore.resolve("root.json"); | ||
if (localTrustRoot.toFile().exists()) { | ||
// Backup the old root. (not sure if this is necessary) |
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.
should we add an TODO:(#issue)
to investigate 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.
Backing up the old root chain isn't strictly necessary, just the latest verified one as root.json
(as done here) is valid.
continue; | ||
} | ||
|
||
// key bytes are in Hex not Base64! TUF also lies that their key is PEM Encoded. Don't believe |
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.
lol I think @asraa said this is going to change in root 5 or 6? Should we factor that in here?
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.
Haha yes! TUF specification is PEM-encoding, but sigstore's root is currently hex. Will be fixed in v5:
sigstore/root-signing#372
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 have a TODO in #60 to fix this :). I didn't want to overly complicate an already big PR.
LocalDateTime getExpires(); | ||
String getExpires(); | ||
|
||
default ZonedDateTime getExpiresAsDate() { |
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.
A derived value is an immutables construct that calculates this once instead of invoking every time: https://immutables.github.io/immutable.html#derived-attributes
|
||
@AfterEach | ||
void clearLocalMirror() { | ||
for (File file : localMirror.toFile().listFiles()) { |
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 thought @TempDir
did cleanup itself?
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 think it's test suite scoped? Doesn't seem to clean itself between tests.
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.
Oh I see it's static and you only want to start the remote resource server once?
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 exactly.
TBH this was intentional: the protocol will check the length (and hash) of the downloaded files against the TUF metadata expected length and hashes (https://theupdateframework.github.io/specification/latest/#metapath-length) and that will change with the newline |
// use set to not count the same key multiple times towards the threshold. | ||
var goodSigs = new HashSet<>(signatures.size()); | ||
for (Signature signature : signatures) { | ||
var key = rootKeys.get(signature.getKeyId()); |
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 you need to check that this key ID is also signed off on that particular role?
e.g. we don't want to consider the timestmap key signing the snapshot role as valid, even though they are both key IDs in the root.
probably the code here should change:
var newRootKeys = newRoot.getSignedMeta().getKeys(ROLE);
// Verify our new root meta against the new root keys.
verifyDelegate(
newRootSignatures, newRootRoleMeta.getThreshold(), newRootKeys, newRootMetaBytes);
private void verifyDelegate( | ||
List<Signature> signatures, | ||
int threshold, | ||
Map<String, Key> rootKeys, |
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.
note that the role may not be the root role here. it may be the snapshot role.
nit: rename to trustedKeys?
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.
Oh damn! Thank you for noticing this! I significantly refactored the verification method as a result. PTAL. Currently working on another PR with more extensive unit testing for the verification 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.
ugh. this response was to your previous comment about making sure the keys were in the role keys. My refactoring also addresses this comment.
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 looks great so far!
Signed-off-by: Patrick Flynn <patrick@chainguard.dev>
Signed-off-by: Patrick Flynn <patrick@chainguard.dev>
Signed-off-by: Patrick Flynn <patrick@chainguard.dev>
this is ready for another look. |
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.
verification logic lgtm!
// key bytes are in Hex not Base64! | ||
// TODO(patrick): this will change in a subsequent version. Add code to handle PEM Encoded | ||
// keys as well. | ||
byte[] keyBytes = Hex.decode(key.getKeyVal().get("public")); |
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.
minor comment, and this is style, so feel free to ignore me, i'm only here for logic :)
Why not put the logic for decoding the keybytes into constructTufPublicKey
? (as in constructTufPublicKey
takes in the whole Key
?) that way swapping out the decoding/parsing of the keyval
can be contained in the Keys
package, since it will change later.
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.
It's a good comment! I left it that way because I have an eye to refactoring the Keys tool to merge the two Keys public key methods once we have PEM encoded keys here.
var key = publicKeys.get(signature.getKeyId()); | ||
if (key != null) { | ||
// key bytes are in Hex not Base64! TUF also lies that their key is PEM Encoded. Don't | ||
// believe |
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.
spotless got you again.
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.
fixed.
var goodSigs = new HashSet<>(role.getKeyids().size()); | ||
// role.getKeyIds() defines the keys allowed to sign for this role. | ||
for (String keyid : role.getKeyids()) { | ||
Optional<Signature> signatureMaybe = |
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 is implied by the optional type? honestly not sure what's convention here.
Optional<Signature> signatureMaybe = | ||
signatures.stream().filter(sig -> sig.getKeyId().equals(keyid)).findFirst(); | ||
// only verify if we find a signature that matcheds an allowed key id. | ||
if (signatureMaybe.isPresent()) { |
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.
You could make use of optional.ifPresent(sig -> ...)
if you wanted.
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.
like it!
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.
changed my mind. It can't throw the checked exceptions from a lambda. Forgot about that.
goodSigs.add(signature.getKeyId()); | ||
} | ||
} catch (SignatureException e) { | ||
throw new RuntimeException(e); |
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.
Would it make sense to add more context in this exception? Maybe the SignatureException is sufficient, but that it's failing in the TUF context?
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.
k, I moved to wrapping it with TufException
throws SignatureVerificationException, NoSuchAlgorithmException, InvalidKeyException, | ||
InvalidKeySpecException { | ||
// use set to not count the same key multiple times towards the threshold. | ||
var goodSigs = new HashSet<>(role.getKeyids().size()); |
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.
In theory it should be like role.getKeyids().size() * 4 / 3
, see https://richardstartin.github.io/posts/5-java-mundane-performance-tricks#size-hashmaps-whenever-possible
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.
nice tip!
Signed-off-by: Patrick Flynn <patrick@chainguard.dev>
Signed-off-by: Patrick Flynn <patrick@chainguard.dev>
WIP towards #60. I will update that issue with next steps as there are a few.
This PR is intentionally incomplete. I want to land code incrementally on this one as it's dense and it will benefit from incremental review.
The code is kept as straight forward as possible to help review the implementation meets spec. I intend to start factoring things out as I implement the rest of the meta and target updating.
There is a coming change in the spec to make the signing public keys PEM encoded as they were supposed to be. This code does not handle that change yet. I will add a test case and modify the certification handling in a subsequent PR.
Links to spec for review are included in the code.
Release Note
does not affect the current library behavior.
Documentation
no doc updates yet.