A Go implementation of tuftool - a command-line utility for creating and managing The Update Framework (TUF) repositories.
tufcli is a reimplementation of the Rust-based tuftool in Go, providing the same functionality for managing TUF repositories. TUF (The Update Framework) is a framework for securing software update systems.
go build -o tufcli .# Show help
./tufcli --help
# Show version
./tufcli --version
# Set log level
./tufcli --log-level debug <command>create- Create a new TUF repositoryclone- Clone a TUF repository, including metadata and targetsupdate- Update a TUF repository's metadata and optionally add targetsdownload- Download a TUF repository's targetstransfer-metadata- Transfer metadata from one root of trust to another
root- Manipulate root.json metadata fileinit- Initialize a new root.json fileadd-key- Add a key to root.jsonremove-key- Remove keys from rolesexpire- Set the expiration dateset-threshold- Set signature threshold for a rolebump-version- Increment version numberset-version- Set specific version numbergen-rsa-key- Generate RSA keypair and add to rolessign- Sign root.json with private keys
delegation- Manage delegated roles (requires--signing-roleflag)create-role- Create a delegated roleadd-role- Add delegated roleadd-key- Add a key to a delegated roleremove-key- Remove a key from a delegated roleremove- Remove a roleupdate-delegated-targets- Update delegated targets
signing-config- Manage Sigstore signing configuration files (signing_config.v0.2.json)create- Create a new signing config (empty, from--base-config, or--with-default-services)add-url- Add a service URL (ca, oidc, rekor, tsa) with replace-or-append semanticsremove-url- Remove a service URL by exact matchset-config- Set service selection policy (ALL, ANY, EXACT) for rekor or tsainspect- Display signing config contents (text or JSON format)
-
rhtas- Manage RHTAS TUF repositories with Sigstore-specific targetsHandles Fulcio, CTLog, Rekor, and TSA targets, and generates the associated
trusted_root.jsonandsigning_config.v0.2.jsonmetadata bundles.Required flags:
--root(-r),--key(-k),--outdir(-o)Flag Description --set-fulcio-targetAdd Fulcio certificate chain (with --fulcio-uri,--fulcio-status,--oidc-uri)--set-ctlog-targetAdd CTLog public key (with --ctlog-uri,--ctlog-status)--set-rekor-targetAdd Rekor public key (with --rekor-uri,--rekor-status)--set-tsa-targetAdd TSA certificate chain (with --tsa-uri,--tsa-status)--delete-fulcio-targetRemove a Fulcio target (repeatable) --delete-ctlog-targetRemove a CTLog target (repeatable) --delete-rekor-targetRemove a Rekor target (repeatable) --delete-tsa-targetRemove a TSA target (repeatable) --force-versionEnable explicit version overrides --targets-versionSet targets.json version (requires --force-version)--snapshot-versionSet snapshot.json version (requires --force-version)--timestamp-versionSet timestamp.json version (requires --force-version)--targets-expiresSet targets metadata expiration --snapshot-expiresSet snapshot metadata expiration --timestamp-expiresSet timestamp metadata expiration --operatorOperator name for signing config (default: sigstore.dev)--metadata-url(-m)Base URL of existing TUF repo to load metadata from ( file://orhttps://)--allow-expired-repoAllow loading expired metadata (unsafe, for testing) --follow(-f)Follow symbolic links when copying target files --target-path-existsBehavior when target exists: skip(default),replace, orfail--incoming-metadata(-i)Path or URL to incoming delegated targets metadata --roleDelegated role name (requires --incoming-metadata)
Create a new TUF repository with signed metadata and target files.
Required flags: --root (-r), --key (-k), --outdir (-o), --add-targets (-t)
| Flag | Description |
|---|---|
--root (-r) |
Path to root.json file for the repository |
--key (-k) |
Key files to sign with (repeatable) |
--outdir (-o) |
Output directory for the repository |
--add-targets (-t) |
Directory of targets to add |
--targets-expires |
Expiration of targets.json (RFC 3339 or relative like in 7 days) |
--targets-version |
Version of targets.json |
--snapshot-expires |
Expiration of snapshot.json |
--snapshot-version |
Version of snapshot.json |
--timestamp-expires |
Expiration of timestamp.json |
--timestamp-version |
Version of timestamp.json |
--follow (-f) |
Follow symbolic links when adding targets |
--target-path-exists |
Behavior when target exists: skip (default), replace, or fail |
# Create a TUF repo with targets
./tufcli create \
--root root.json \
--key keys/root.pem \
--key keys/snapshot.pem \
--key keys/targets.pem \
--key keys/timestamp.pem \
--add-targets input/ \
--targets-expires 'in 3 weeks' \
--targets-version 1 \
--snapshot-expires 'in 3 weeks' \
--snapshot-version 1 \
--timestamp-expires 'in 1 week' \
--timestamp-version 1 \
--outdir repo/
# Create with empty targets directory (metadata-only bootstrap)
./tufcli create \
--root root.json \
--key key.pem \
--add-targets empty-dir/ \
--targets-expires 'in 52 weeks' \
--targets-version 1 \
--snapshot-expires 'in 52 weeks' \
--snapshot-version 1 \
--timestamp-expires 'in 52 weeks' \
--timestamp-version 1 \
--outdir repo/Download targets from a TUF repository after verifying metadata integrity through the full TUF client workflow (root rotation, timestamp, snapshot, and targets verification).
The output directory must not already exist.
Required flags: --metadata-url (-m), --targets-url (-t)
| Flag | Description |
|---|---|
--root (-r) |
Path to root.json file for the repository |
--metadata-url (-m) |
TUF repository metadata base URL (required) |
--targets-url (-t) |
TUF repository targets base URL (required) |
--target-name (-n) |
Download only these targets (repeatable) |
--root-version (-v) |
Remote root.json version number (default: 1) |
--allow-expired-repo |
Allow download for expired metadata (unsafe, for testing only) |
--allow-root-download |
Allow downloading root.json from the repository (unsafe) |
# Download all targets
./tufcli download /tmp/outdir \
-r root.json \
-m https://tuf.example.com/metadata \
-t https://tuf.example.com/targets
# Download specific targets
./tufcli download /tmp/outdir \
-r root.json \
-m https://tuf.example.com/metadata \
-t https://tuf.example.com/targets \
-n trusted_root.json -n signing_config.v0.2.json
# Download without a local root (unsafe, for testing)
./tufcli download /tmp/outdir \
-m https://tuf.example.com/metadata \
-t https://tuf.example.com/targets \
--allow-root-download
# Download with expired metadata (unsafe, for testing)
./tufcli download /tmp/outdir \
-r root.json \
-m https://tuf.example.com/metadata \
-t https://tuf.example.com/targets \
--allow-expired-repoCreate and manage Sigstore signing configuration files standalone, without requiring a TUF repository. Output is byte-identical to cosign signing-config create.
# Create an empty signing config
./tufcli signing-config create -o signing_config.v0.2.json
# Add service endpoints
./tufcli signing-config add-url -c signing_config.v0.2.json \
-t ca -u https://fulcio.example.com \
--start-time 2025-01-01T00:00:00Z --operator example.com
./tufcli signing-config add-url -c signing_config.v0.2.json \
-t rekor -u https://rekor.example.com \
--start-time 2025-01-01T00:00:00Z --operator example.com
./tufcli signing-config add-url -c signing_config.v0.2.json \
-t oidc -u https://oauth2.example.com \
--start-time 2025-01-01T00:00:00Z --operator example.com
./tufcli signing-config add-url -c signing_config.v0.2.json \
-t tsa -u https://tsa.example.com/api/v1/timestamp \
--start-time 2025-01-01T00:00:00Z --operator example.com
# Set service selection policies
./tufcli signing-config set-config -c signing_config.v0.2.json -t rekor -s ANY
./tufcli signing-config set-config -c signing_config.v0.2.json -t tsa -s EXACT -n 1
# Inspect
./tufcli signing-config inspect -c signing_config.v0.2.json
./tufcli signing-config inspect -c signing_config.v0.2.json -f json
# Clone and modify a deployed config
./tufcli signing-config create -o modified.json --base-config deployed_config.json
./tufcli signing-config remove-url -c modified.json -t oidc -u https://old-provider.com
# Fetch public Sigstore defaults
./tufcli signing-config create -o defaults.json --with-default-servicesRoot metadata commands are complete and tested. RHTAS commands are complete and tested. Download command is complete and tested. Create command is complete and tested. Clone command is complete and tested. Update command is complete and tested. Transfer-metadata command is complete and tested. Signing config commands are complete and tested. Delegation commands are not yet implemented.
tufcli can be used as a Go library. Every CLI command has a corresponding public API under pkg/.
go get github.com/securesign/tufcli| Package | Import path | Description |
|---|---|---|
| rootmeta | github.com/securesign/tufcli/pkg/rootmeta |
Root.json lifecycle: init, keys, thresholds, expiration, signing |
| create | github.com/securesign/tufcli/pkg/repo/create |
Create new TUF repositories |
| update | github.com/securesign/tufcli/pkg/repo/update |
Update existing repositories (targets, versions, expirations) |
| clone | github.com/securesign/tufcli/pkg/repo/clone |
Clone remote TUF repositories |
| download | github.com/securesign/tufcli/pkg/repo/download |
Download targets with full TUF verification |
| transfer | github.com/securesign/tufcli/pkg/repo/transfer |
Transfer metadata between roots of trust |
| rhtas | github.com/securesign/tufcli/pkg/rhtas |
RHTAS Sigstore target management (Fulcio, Rekor, TSA) |
| signingconfig | github.com/securesign/tufcli/pkg/signingconfig |
Sigstore signing config management (cosign-compatible) |
package main
import (
"fmt"
"os"
"time"
"github.com/securesign/tufcli/pkg/repo/create"
"github.com/securesign/tufcli/pkg/rootmeta"
)
func main() {
// 1. Initialize root.json
rootmeta.Init(rootmeta.InitOptions{Path: "root.json"})
rootmeta.Expire(rootmeta.ExpireOptions{Path: "root.json", Expires: time.Now().AddDate(1, 0, 0)})
for _, role := range []string{"root", "snapshot", "targets", "timestamp"} {
rootmeta.SetThreshold(rootmeta.SetThresholdOptions{Path: "root.json", Role: role, Threshold: 1})
}
// 2. Generate a signing key
keyID, _ := rootmeta.GenRsaKey(rootmeta.GenRsaKeyOptions{
Path: "root.json",
KeyPath: "key.pem",
Bits: 2048,
Roles: []string{"root", "snapshot", "targets", "timestamp"},
})
fmt.Printf("Generated key: %s\n", keyID)
// 3. Sign root.json
rootmeta.Sign(rootmeta.SignOptions{Path: "root.json", KeyPaths: []string{"key.pem"}})
// 4. Create a repository with targets
os.MkdirAll("targets-input", 0755)
os.WriteFile("targets-input/artifact.txt", []byte("hello"), 0644)
create.Create(&create.Options{
RootPath: "root.json",
KeyPaths: []string{"key.pem"},
OutDir: "my-repo",
AddTargetsDir: "targets-input",
TargetsExpires: time.Now().AddDate(0, 0, 21),
TargetsVersion: 1,
SnapshotExpires: time.Now().AddDate(0, 0, 21),
SnapshotVersion: 1,
TimestampExpires: time.Now().AddDate(0, 0, 7),
TimestampVersion: 1,
})
fmt.Println("Repository created at my-repo/")
}package main
import (
"fmt"
"time"
"github.com/securesign/tufcli/pkg/signingconfig"
)
func main() {
signingconfig.Create(signingconfig.CreateOptions{Output: "signing_config.v0.2.json"})
signingconfig.AddURL(signingconfig.AddURLOptions{
ConfigPath: "signing_config.v0.2.json",
Type: "ca",
URL: "https://fulcio.example.com",
APIVersion: 1,
Operator: "example.com",
StartTime: time.Now(),
})
output, _ := signingconfig.Inspect(signingconfig.InspectOptions{
ConfigPath: "signing_config.v0.2.json",
Format: "text",
})
fmt.Print(output)
}The clone, download, and transfer commands accept an Output io.Writer field
for controlling progress messages. Library consumers can suppress output or redirect it:
import (
"bytes"
"io"
"github.com/securesign/tufcli/pkg/repo/clone"
)
// Silent mode — no progress output
clone.Clone(&clone.Options{Output: io.Discard, ...})
// Capture output into a buffer
var buf bytes.Buffer
clone.Clone(&clone.Options{Output: &buf, ...})
// Default (Output not set) — writes to os.Stderr, same as CLI
clone.Clone(&clone.Options{...})This implementation targets TUF specification version 1.0.0.
MIT OR Apache-2.0
This is a Go port of the original Rust implementation available at: https://github.com/awslabs/tough
The following is an example of how you can create and download a TUF repository using tufcli.
export WRK="${HOME}/examples"
mkdir -p "${WRK}"
# we will store our root.json in $WRK/root
mkdir "${WRK}/root"
# save the path to the root.json we are about to create, we will use it a lot
export ROOT="${WRK}/root/root.json"
# we will store our signing keys in $WRK/keys
mkdir "${WRK}/keys"
# instantiate a new root.json
./tufcli root init --path "${ROOT}"
# set the root file's expiration date
./tufcli root expire --path "${ROOT}" --time "in 1 year"
# set the signing threshold for each of the standard signing roles.Each of the following roles must have at least 1 valid signature
./tufcli root set-threshold --path "${ROOT}" --role root --threshold 1
./tufcli root set-threshold --path "${ROOT}" --role snapshot --threshold 1
./tufcli root set-threshold --path "${ROOT}" --role targets --threshold 1
./tufcli root set-threshold --path "${ROOT}" --role timestamp --threshold 1
# create an RSA key and store it as a file. this requires openssl on your system
# this command both creates the key and adds it to root.json for the root role
# for this example we will re-use the same key for the other standard roles
./tufcli root gen-rsa-key --path "${ROOT}" --output "${WRK}/keys/root.pem" --role root --role snapshot --role targets --role timestamp --bits 2048
# sign root.json
./tufcli root sign --path "${ROOT}" --key "${WRK}/keys/root.pem" Now that we have a root.json file, we can create and sign a TUF repository.
# create a directory to hold the targets that we will sign. we call this the
# 'input' directory because these are the targets that we want to put into
# our TUF repo
mkdir -p "${WRK}/input"
# create an empty TUF repo
./tufcli create \
--root "${ROOT}" \
--key "${WRK}/keys/root.pem" \
--add-targets "${WRK}/input" \
--targets-expires 'in 3 weeks' \
--targets-version 1 \
--snapshot-expires 'in 3 weeks' \
--snapshot-version 1 \
--timestamp-expires 'in 1 week' \
--timestamp-version 1 \
--outdir "${WRK}/tuf-repo"# Create Rekor public key
openssl ecparam -genkey -name prime256v1 -noout -out ${WRK}/input/rekor.pem
openssl ec -in ${WRK}/input/rekor.pem -pubout -out ${WRK}/input/rekor.pub
rm ${WRK}/input/rekor.pem
# Add a Rekor transparency log
./tufcli rhtas \
--root "${ROOT}" \
--key "${WRK}/keys/root.pem" \
--outdir "${WRK}/tuf-repo" \
--set-rekor-target "${WRK}/input/rekor.pub" \
--rekor-uri https://rekor.sigstore.dev \
--metadata-url file:///$WRK/tuf-repo/
# Delete a target
./tufcli rhtas \
--root "${ROOT}" \
--key "${WRK}/keys/root.pem" \
--delete-fulcio-target "fulcio-chain.pem" \
--outdir "${WRK}/tuf-repo" \
--metadata-url file:///$WRK/tuf-repo/
# Custom expiration and version
./tufcli rhtas \
-r root.json -k key.pem -o repo/ \
--set-fulcio-target fulcio-chain.pem \
--targets-expires "in 365 days" \
--snapshot-expires "in 90 days" \
--timestamp-expires "in 1 day"# Update metadata expiration dates
./tufcli update \
--root "${ROOT}" \
--key "${WRK}/keys/root.pem" \
--targets-expires 'in 3 weeks' \
--snapshot-expires 'in 3 weeks' \
--timestamp-expires 'in 1 week' \
--outdir "${WRK}/tuf-repo" \
--metadata-url file:///$WRK/tuf-repoClone copies both metadata and targets into separate directories. Use
--metadata-only to skip target downloads.
# Clone metadata and targets
./tufcli clone \
--root "${WRK}/tuf-repo/root.json" \
--metadata-url "file://${WRK}/tuf-repo/" \
--targets-url "file://${WRK}/tuf-repo/targets" \
--metadata-dir "${WRK}/tuf-clone/metadata" \
--targets-dir "${WRK}/tuf-clone/targets"
# Clone metadata only (no targets)
./tufcli clone \
--root "${WRK}/tuf-repo/root.json" \
--metadata-url "file://${WRK}/tuf-repo/" \
--metadata-dir "${WRK}/tuf-clone-metadata" \
--metadata-onlyTransfer metadata from an existing repository to a new root of trust. This copies all target entries (metadata only, not files) and signs under the new root's keys.
# Create a new root for the transfer destination
export NEW_ROOT="${WRK}/new-root/root.json"
mkdir -p "${WRK}/new-root"
./tufcli root init --path "${NEW_ROOT}"
./tufcli root expire --path "${NEW_ROOT}" --time "in 1 year"
./tufcli root set-threshold --path "${NEW_ROOT}" --role root --threshold 1
./tufcli root set-threshold --path "${NEW_ROOT}" --role snapshot --threshold 1
./tufcli root set-threshold --path "${NEW_ROOT}" --role targets --threshold 1
./tufcli root set-threshold --path "${NEW_ROOT}" --role timestamp --threshold 1
./tufcli root gen-rsa-key --path "${NEW_ROOT}" --output "${WRK}/new-root/key.pem" \
--role root --role snapshot --role targets --role timestamp --bits 2048
./tufcli root sign --path "${NEW_ROOT}" --key "${WRK}/new-root/key.pem"
# Transfer metadata from old repo to new root
./tufcli transfer-metadata \
--current-root "${WRK}/tuf-repo/root.json" \
--new-root "${NEW_ROOT}" \
--key "${WRK}/new-root/key.pem" \
--metadata-url "file://${WRK}/tuf-repo/" \
--targets-url "file://${WRK}/tuf-repo/targets" \
--targets-expires 'in 3 weeks' \
--targets-version 1 \
--snapshot-expires 'in 3 weeks' \
--snapshot-version 1 \
--timestamp-expires 'in 1 week' \
--timestamp-version 1 \
--outdir "${WRK}/transferred-repo"Now that we have created TUF repo, we can inspect it using download command. Download command is usually used to download a remote repo using HTTP/S url, but for this example we will use a file based url to download from local repo.
# download tuf repo
./tufcli download \
--root "${WRK}/tuf-repo/root.json" \
-t "file://${WRK}/tuf-repo/targets" \
-m "file://${WRK}/tuf-repo/" \
"${WRK}/tuf-download"# Initialize root metadata
./tufcli root init --path "${ROOT}"
# Add existing key to all roles
./tufcli root add-key --path "${ROOT}" --key "${WRK}/keys/root.pem" --role root --role snapshot --role targets --role timestamp
# Set thresholds
./tufcli root set-threshold --path "${ROOT}" --role root --threshold 1
./tufcli root set-threshold --path "${ROOT}" --role snapshot --threshold 1
./tufcli root set-threshold --path "${ROOT}" --role targets --threshold 1
./tufcli root set-threshold --path "${ROOT}" --role timestamp --threshold 1
# Set expiration and sign
./tufcli root expire --path "${ROOT}" --time "in 1 year"
./tufcli root sign --path "${ROOT}" --key "${WRK}/keys/root.pem"