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 TUF Client root resource syncing. #123

Merged
merged 14 commits into from
Sep 13, 2022
Merged

Add TUF Client root resource syncing. #123

merged 14 commits into from
Sep 13, 2022

Conversation

patflynn
Copy link
Collaborator

@patflynn patflynn commented Sep 9, 2022

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.

@patflynn
Copy link
Collaborator Author

patflynn commented Sep 9, 2022

working on rebase and commit signing.

@patflynn patflynn changed the title Tuf root update Add TUF Client root resource syncing. Sep 9, 2022
Copy link
Member

@loosebazooka loosebazooka left a 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.
Copy link
Member

Choose a reason for hiding this comment

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

any reason why?

Copy link
Collaborator Author

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
Copy link
Member

Choose a reason for hiding this comment

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

incomplete comment?

Copy link
Collaborator Author

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() {
Copy link
Member

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?

Copy link
Collaborator Author

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?

Copy link
Member

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

Copy link
Collaborator Author

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 @@
{
Copy link
Member

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

Copy link
Collaborator Author

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. 🤷

Patrick Flynn added 3 commits September 12, 2022 08:55
Signed-off-by: Patrick Flynn <patrick@chainguard.dev>
Signed-off-by: Patrick Flynn <patrick@chainguard.dev>
Signed-off-by: Patrick Flynn <patrick@chainguard.dev>
Patrick Flynn added 4 commits September 12, 2022 09:04
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>
loosebazooka
loosebazooka previously approved these changes Sep 12, 2022
Copy link
Member

@loosebazooka loosebazooka left a 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
Copy link
Member

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.

Copy link
Collaborator Author

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;
Copy link
Member

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?)

Copy link
Collaborator Author

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
Copy link
Member

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

Copy link
Collaborator Author

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)
Copy link
Member

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?

Copy link

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
Copy link
Member

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?

Copy link

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

Copy link
Collaborator Author

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() {
Copy link
Member

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()) {
Copy link
Member

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?

Copy link
Collaborator Author

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.

Copy link
Member

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?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

yeah exactly.

@asraa
Copy link

asraa commented Sep 12, 2022

looks good, some test resources have no newlines at EOF, is that intentional?

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());
Copy link

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,
Copy link

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?

Copy link
Collaborator Author

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.

Copy link
Collaborator Author

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.

Copy link

@asraa asraa 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 so far!

Signed-off-by: Patrick Flynn <patrick@chainguard.dev>
Patrick Flynn added 3 commits September 12, 2022 17:03
Signed-off-by: Patrick Flynn <patrick@chainguard.dev>
Signed-off-by: Patrick Flynn <patrick@chainguard.dev>
Signed-off-by: Patrick Flynn <patrick@chainguard.dev>
@patflynn
Copy link
Collaborator Author

this is ready for another look.

Copy link

@asraa asraa left a 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"));
Copy link

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.

Copy link
Collaborator Author

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.

loosebazooka
loosebazooka previously approved these changes Sep 13, 2022
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
Copy link
Member

Choose a reason for hiding this comment

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

spotless got you again.

Copy link
Collaborator Author

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 =
Copy link
Member

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()) {
Copy link
Member

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.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

like it!

Copy link
Collaborator Author

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);
Copy link
Member

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?

Copy link
Collaborator Author

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());
Copy link
Collaborator

Choose a reason for hiding this comment

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

Copy link
Collaborator Author

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>
Signed-off-by: Patrick Flynn <patrick@chainguard.dev>
@patflynn patflynn merged commit 70704c1 into main Sep 13, 2022
@patflynn patflynn deleted the tuf-root-update branch September 13, 2022 20:39
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.

4 participants