Skip to content

Internals

Read from the source at commit 9a3c304. Every claim here points at a file and line.

Code map

PathResponsibility
tuf/api/metadata.pyMetadata[T] wrapper around a signed payload (metadata.py:81)
tuf/api/_payload.pyRole payload types and the signature-verification primitives (largest file)
tuf/api/serialization/json.pyThe built-in JSON (de)serializer
tuf/api/dsse.pyDSSE envelope support
tuf/ngclient/updater.pyUpdater, the detailed client workflow
tuf/ngclient/_internal/trusted_metadata_set.pyTrustedMetadataSet, the trust-set state machine
tuf/ngclient/urllib3_fetcher.pyDefault HTTP fetcher behind FetcherInterface
tuf/repository/_repository.pyRepository(ABC), base class for repository tooling

Core data structures

The role types live in tuf/api/_payload.py. Signed is the abstract base for all role metadata, holding version, spec_version, expires, and is_expired() (_payload.py:84, _payload.py:251).

A path worth tracing

Follow the metadata verification path end to end for the timestamp role.

Step 1, the entry point. Fetched bytes reach a role-specific update method; for timestamp it is TrustedMetadataSet.update_timestamp() (trusted_metadata_set.py:204). It first checks that the final root is not expired (trusted_metadata_set.py:230).

Step 2, deserialize and verify signatures. The method delegates to _load_data, which branches on envelope type: classic metadata goes through _load_from_metadata() (trusted_metadata_set.py:457) and DSSE through _load_from_simple_envelope() (trusted_metadata_set.py:484). Both call delegator.verify_delegate(role_name, signed_bytes, signatures) (trusted_metadata_set.py:479).

Step 3, threshold signature check. The core is _DelegatorMixin.get_verification_result() (_payload.py:429):

python
sig = signatures[keyid]
try:
    key.verify_signature(sig, payload)
    signed[keyid] = key
except sslib_exceptions.UnverifiedSignatureError:
    unsigned[keyid] = key
    logger.info("Key %s failed to verify %s", keyid, delegated_role)

return VerificationResult(role.threshold, signed, unsigned)

A result is verified when len(self.signed) >= self.threshold (_payload.py:355). If the threshold is not met, verify_delegate() raises UnsignedMetadataError (_payload.py:500).

Step 4, rollback and ordering enforcement. This is where the state machine earns its keep. For timestamp, a new version below the trusted one raises BadVersionNumberError, and an equal version raises EqualVersionNumberError to keep the old one (trusted_metadata_set.py:242). Snapshot updates are matched against the hash and length recorded in timestamp's snapshot_meta and reject any deleted or version-rolled-back targets meta (trusted_metadata_set.py:341). The target file itself is verified after download by TargetFile.verify_length_and_hashes() (_payload.py:1661).

Things that surprised me

The state machine deliberately allows "intermediate" metadata to load even when it is expired or version-mismatched, then raises later. update_timestamp loads the new timestamp and only afterward calls _check_final_timestamp() to raise on expiry (trusted_metadata_set.py:259, trusted_metadata_set.py:270). The inline comment is explicit:

python
# expiry not checked to allow old timestamp to be used for rollback
# protection of new timestamp: expiry is checked in update_snapshot()

A naive implementation that rejected expired metadata immediately would throw away the very baseline it needs to detect a rollback. update_snapshot follows the same pattern with _check_final_snapshot() (trusted_metadata_set.py:357), and intermediate roots are accepted even when expired, with only the final root's expiry enforced inside update_timestamp.