This proposes a change to the TUF spec to support a tamper-evident public log of changes to the root authority in a TUF repository and to allow trust-pinning for delegated roles.
From the TUF spec:
To replace a compromised root key or any other top-level role key, the root role signs a new root.json file that lists the updated trusted keys for the role. When replacing root keys, an application will sign the new root.json file with both the new and old root keys until all clients are known to have obtained the new root.json file (a safe assumption is that this will be a very long time or never).
Currently, rotating the root keys requires keeping older certs around in the root metadata. Although this does successfully allow rotation, there are a couple of problems:
- Supporting even moderately aggressive rotation schemes (say, having a root threshold of 2/3 available keys and rotating those keys quarterly) results in a large number of old keys that need to be stored in the root.json file
- Having a large number of old, possibly compromised keys in the root.json makes the metadata more complex and less (human) parsable, possibly increasing the likelihood of implementation errors.
Note that this may be less of a concern in TUF as-specified:
Periodically, the software update system using the framework instructs the framework to check each repository for updates.
If clients can be assumed to be constantly polling, there may be an opportunity to remove older keys. But this is not the case for package managers in general - current partial/full implementations in PyPI, RubyGems, and Notary do not have "periodic" polling as one of their properties.
The canonical (server) TUF metadata repository should version the root.json file any time root keys change, and should point to a "parent" root.json.
{
"signed": {
"_type": "Root",
// ...
"parent": "123" // sha256 of previous root.json
},
"parentSignatures" {
// current metadata signed with parent keys/threshold
},
"signatures": [
// current metadata signed with current keys/threshold
]
}
When a client gets new metadata with a parent
and parentSignatures
block, it does the following:
- requests the parent metadata file from the server by hash
- verifies the hash
- using the public keys and threshold of the parent, calculates the signatures in
parentSignatures
of the new metadata - repeats the process if the parent also has a
parent
block, until either the hash matches the current local metadata or the pulled metadata doesn't have a parent block.
Rotating a single root key from A
to B
- switch public key in public portion to point to
B.pub
- calculate hash of original metadata and put it in the
parent
block of the new metadata - sign new metadata with
B.key
and put insignatures
block. - sign new metadata with
A.key
and put inparentSignatures
block. - publish new metadata
- old metadata should still be accessible by hash (not name)
Trust pinning is unspecified in TUF, though it does exist in Notary, which requires pinning an x509 cert or CA.
Using a public log on delegations would allow trust pinning with key rotation. Instead of pinning to a CA or cert, a pubic key (ideally, a set of public keys) would be pinned.
If trust is pinned for a delegation, we require that there is a valid parent chain back to the pinned public keys. This works analagously to the root key rotation case.
An interesting property that falls out of this is that users can decide where to root their trust (the registry or any delegated target owner) without losing the TUF freshness/rotation ability.
We leave the semantics of specifying pinned trust unspecified for now.
Rotating a pinned delegation key
- trust is pinned for a delegation (e.g.
targets/namespace
) tonamespace.pub
- the namespace owner generates a new keypair and new namespace metadata
- the namespace owner calculates the hash of original metadata and puts it in the
parent
block of the new metadata - the namespace owner signs the new metadata with
new_namespace.pub
and puts that signature in thesignatures
block - the namespace owner signs the new metadata with
namespace.pub
, the old key, and puts that signature in theparentSignatures
block