rust-tuf
rust-tuf copied to clipboard
Need a high level interface for creating and changing TUF metadata
We need to do a lot of manual work to creating TUF metadata, and generating new versions if we rotate keys or add/remove targets. In comparison, go-tuf has a repo library that has a bunch of simple helper functions. For example, see this script and this test.
Questions:
- should this be a library+tests?
- should it also take care of creating keys, if needed (ie using sequoia)?
I see this possible repository creation flow:
- create TUF repository:
- root key is existing or new (to be created contextually)
- at start, all the roles are using the same root key. Maximum simplicity, minimum security.
- add delegation
- one delegating key (eg root) needs to be accessible for signing the delegation (eg timestamp)
- delegation specific parameters (eg target base path, if creating a target delegation) need to be provided
- sign existing delegation
- add a signature to an existing delegation so that delegations requiring multiple signatures can reach the required validity quorum
- publish
Key rotation would happen in this way:
- new delegation is added
- signatures are accumulated until validity quorum is reached
- signatures are progressively removed from the delegation to be revoked, removal of the last signature removes also the delegation
Need to double check the TUF spec, I don't remember what specifies about rotation. Surely it's not healthy to revoke a delegation without reaching the same validity quorum that qualified it.
Hello @cavokz! I'd appreciate any help. I've started sketching out some ideas, but haven't gotten too far yet. Here's roughly the API I think we could use:
let staging = FileSystemRepository::new("...");
let committed = FileSystemRepository::new("...");
let private_keys = load_keys(...);
let config = Config::default();
// I'd love to call this Repository, but we are using that term already. Maybe we should rename it
// to `Transport` or `Store`?
//
// Anyway, this `Creator` holds all the private keys. It might internally use `Client` to download
// the metadata, and we will use the `local` store as the location for the committed metadata, and
// `staging` as the in progress metadata.
let mut creator = Creator::new(staging, committed);
// Or, if we want to pretty print metadata. The creator needs to know this because the timestamp
// role tracks the snapshot role's file hash, so before we can create a new timestamp we need to
// make sure it captures the pretty version of the snapshot metadata.
let mut creator = Creator::builder()
.pretty()
.build();
// These add the keys for verifying signatures, but doesn't change metadata.
creator.load_private_keys(Role::Root, &private_keys["root"]);
creator.load_private_keys(Role::Snapshot, &private_keys["snapshot"]);
creator.load_private_keys(Role::Targets, &private_keys["targets"]);
creator.load_private_keys(Role::Timestamp, &private_keys["timestamp"]);
// Add a trusted public signing key. Anytime we manipulate metadata, first check if the metadata is
// in the staging repo. If not, copy it from the committed repo while stripping off the signatures
// and incrementing the version number.
{
let root = creator.root();
// Rotate a key.
let key = root.add_key(Role::Root, PublicKey(...));
// save key...
root.remove_key(Role::Root, PublicKey(...));
// Write the file back to the `staging` repo.
root.finish();
}
// Adding targets. Note that this targets builder should remember the setting of consistent
// snapshot from the `staging` or `committed` root metadata, since that changes how target files
// are written to the repository.
{
let targets = creator.targets();
// shortcut for adding a simple target.
targets.add_target_from_reader("foo/bar", b"123").finish();
// more complex builder that adds custom target metadata, and explicitly sets the target
// version and expiry.
targets.add_target_from_reader("bar", b"123")
.custom(json!{
"foo": "bar",
})
.version(targets.version() + 5)
.expires(Utc.ymd(2020, 1, 1).and_hms(0, 0, 0))
.finish();
// We might not have actual targets to upload, instead just work with target descriptions.
let target_description: TargetDescription = ...;
targets.add_target_description("baz", &target_description)
.finish();
// Removes a target, without updating the version number since it changed in the last call.
targets.remove_target("baz");
targets.finish();
}
// Take a snapshot.
creator.snapshot()
.finish();
// Take a timestamp.
creator.timestamp()
.finish();
// Copy the `staging` metadata to the `committed` repo, clearing out the staging metadata.
// This should fail if we don't have enough signatures for a valid quorum.
creator.commit();
I'm not sure yet how we should handle things like delegation though.
As to your questions:
should this be a library+tests?
Yes definitely. We're trying to hold ourselves to the standard that all new code should have tests.
should it also take care of creating keys, if needed (ie using sequoia)?
PrivateKey::new
already has some facilities for creating keys, so we could use that in the basic case. That said, we ultimately should play nicely with someone being able to sign metadata with keys stored in an HSM.