The Update Framework (TUF) for Python
python-tuf is the Python reference implementation of The Update Framework (TUF), a framework for securing software update systems against various supply chain attacks. It provides APIs for both client-side artifact verification (`tuf.ngclient`) and repository-side metadata management (`tuf.api.metadata`). The library is actively maintained and currently at version 6.0.0, with a release cadence that addresses security fixes and implements new specification features.
Common errors
-
ModuleNotFoundError: No module named 'requests'
cause Upgrading to `tuf` v6.0.0 without accounting for the switch from `requests` to `urllib3` as the default HTTP client for `ngclient`.fixIf your application explicitly relies on the `RequestsFetcher` (which is now deprecated), install `requests` explicitly: `pip install requests`. Otherwise, `tuf.ngclient` will use `urllib3` by default. -
RepositoryError: Local root.json is invalid.
cause The initial root metadata file provided to the `Updater` is malformed, corrupted, or not a valid TUF root metadata file.fixEnsure the `initial_root_metadata` argument to `Updater` (or the file path provided) points to a correctly formatted and trusted `root.json` file. Validate the JSON structure and its contents against the TUF specification. -
Permission denied: '.../.sigstore/root.json'
cause The TUF client attempts to write or update metadata in a directory where the running user lacks write permissions, or the cached root metadata is in a read-only location.fixEnsure the `repository_dir` provided to `Updater` is writable by the user running the client. For the initial trusted `root.json`, store it in a securely managed, typically read-only, path and pass its content via `initial_root_metadata`. -
ValueError: unrecognized metadata type "<UNKNOWN_TYPE>"
cause Attempting to load or parse a TUF metadata file (`root.json`, `targets.json`, etc.) with an invalid `_type` field within its signed payload, or a corrupted file.fixVerify the integrity and correctness of the metadata file. Ensure it conforms to the TUF specification for the respective role (e.g., `_type` should be 'root', 'targets', 'snapshot', or 'timestamp').
Warnings
- breaking As of v6.0.0, `tuf.ngclient` uses `urllib3` as the default HTTP library instead of `requests`. This change removes implicit dependencies on `requests`, `idna`, `charset-normalizer`, and `certifi`.
- breaking tuf v5.0.0 introduced `securesystemslib v1.0.0` as a minimum requirement. This caused a minor break in the DSSE (Dead Simple Signing Envelope) API, particularly affecting users who directly depend on `securesystemslib`.
- breaking TUF v4.0.0 changed the API for `Metadata API` users, specifically `Root.get_verification_result()` and `Targets.get_verification_result()`.
- gotcha The `tuf.repository` module is explicitly stated in the GitHub README as *not* being part of the `python-tuf` stable API.
- gotcha The initial trusted root metadata (`root.json`) is the critical trust anchor. It should be provided to the client application and stored in a *read-only* location to prevent tampering.
Install
-
pip install tuf -
pip install "securesystemslib[crypto]" tuf
Imports
- Updater
from tuf.ngclient import Updater
- Metadata
from tuf.api.metadata import Metadata
- Root
from tuf.api.metadata import Root
- TargetFile
from tuf.api.metadata import TargetFile
Quickstart
import os
import shutil
from pathlib import Path
from tuf.ngclient import Updater
# --- Configuration (replace with your actual repository details) ---
REPO_METADATA_URL = os.environ.get('TUF_METADATA_URL', 'http://localhost:8000/metadata/')
REPO_TARGETS_URL = os.environ.get('TUF_TARGETS_URL', 'http://localhost:8000/targets/')
# Ensure a clean client state for demonstration
LOCAL_CACHE_DIR = Path('./client_cache')
LOCAL_DOWNLOAD_DIR = Path('./client_downloads')
ROOT_METADATA_PATH = Path('./initial_root.json')
if LOCAL_CACHE_DIR.exists():
shutil.rmtree(LOCAL_CACHE_DIR)
if LOCAL_DOWNLOAD_DIR.exists():
shutil.rmtree(LOCAL_DOWNLOAD_DIR)
LOCAL_CACHE_DIR.mkdir(parents=True, exist_ok=True)
LOCAL_DOWNLOAD_DIR.mkdir(parents=True, exist_ok=True)
# --- Simulate pre-packaged initial root metadata ---
# In a real application, this 'root.json' would be securely bundled
# with your application and stored in a read-only location.
# For this example, let's create a dummy one if it doesn't exist.
# You would typically copy a valid initial_root.json here.
if not ROOT_METADATA_PATH.exists():
print("WARNING: Creating a dummy initial_root.json. "
"In production, this file must be a trusted, pre-packaged root metadata.")
with open(ROOT_METADATA_PATH, 'w') as f:
f.write('{}') # Placeholder, will cause errors if not a real root.
print(f"Initializing TUF Updater with metadata_url='{REPO_METADATA_URL}' and targets_url='{REPO_TARGETS_URL}'")
print(f"Local cache: {LOCAL_CACHE_DIR}, Downloads: {LOCAL_DOWNLOAD_DIR}")
try:
# Initialize TUF Updater
updater = Updater(
repository_dir=str(LOCAL_CACHE_DIR), # Local directory for storing metadata
metadata_base_url=REPO_METADATA_URL,
target_base_url=REPO_TARGETS_URL,
initial_root_metadata=ROOT_METADATA_PATH.read_bytes() # Bootstrap trust from bundled root
)
# Refresh top-level metadata (root, timestamp, snapshot, targets)
print("Refreshing top-level metadata...")
updater.refresh()
print("Metadata refreshed successfully.")
# Get information about a target file
TARGET_NAME = "example_target.txt" # Replace with an actual target name on your repo
print(f"Getting target info for '{TARGET_NAME}'...")
target_info = updater.get_targetinfo(TARGET_NAME)
if target_info:
print(f"Found target '{TARGET_NAME}' (size: {target_info.length} bytes, hashes: {target_info.hashes}).")
# Download the target file
target_path = LOCAL_DOWNLOAD_DIR / TARGET_NAME
print(f"Downloading target to '{target_path}'...")
updater.download_target(target_info, str(target_path))
print(f"Target '{TARGET_NAME}' downloaded and verified successfully!")
else:
print(f"Target '{TARGET_NAME}' not found or could not be verified.")
except Exception as e:
print(f"An error occurred: {e}")
print("Please ensure a TUF repository is running at the configured URLs "
"and that 'initial_root.json' is a valid, trusted root metadata file.")