doc: drop documentation for legacy tools

Remove documentation for legacy client, repository/developer tool
and command line tools, which will be removed in subsequent
commits.

See #1797 and #1798 for replacing ATTACKS.md and QUICKSTART.md.

Signed-off-by: Lukas Puehringer <lukas.puehringer@nyu.edu>
This commit is contained in:
Lukas Puehringer 2022-01-25 17:43:48 +01:00
parent 8c72588662
commit d498bc01c1
10 changed files with 0 additions and 2128 deletions

View file

@ -1,447 +0,0 @@
# Command-Line Interface #
The TUF command-line interface (CLI) requires a full
[TUF installation](INSTALLATION.rst). Be sure to include the installation of
extra dependencies and C extensions (
```python3 -m pip install securesystemslib[crypto,pynacl]```).
The use of the CLI is documented with examples below.
----
# Basic Examples #
## Create a repository ##
Create a TUF repository in the current working directory. A cryptographic key
is created and set for each top-level role. The written Targets metadata does
not sign for any targets, nor does it delegate trust to any roles. The
`--init` call will also set up a client directory. By default, these
directories will be `./tufrepo` and `./tufclient`.
```Bash
$ repo.py --init
```
Optionally, the repository can be written to a specified location.
```Bash
$ repo.py --init --path </path/to/repo_dir>
```
The default top-level key files created with `--init` are saved to disk
encrypted, with a default password of 'pw'. Instead of using the default
password, the user can enter one on the command line for each top-level role.
These optional command-line options also work with other CLI actions (e.g.,
repo.py --add).
```Bash
$ repo.py --init [--targets_pw, --root_pw, --snapshot_pw, --timestamp_pw]
```
Create a bare TUF repository in the current working directory. A cryptographic
key is *not* created nor set for each top-level role.
```Bash
$ repo.py --init --bare
```
Create a TUF repository with [consistent
snapshots](https://github.com/theupdateframework/specification/blob/master/tuf-spec.md#7-consistent-snapshots)
enabled, where target filenames have their hash prepended (e.g.,
`<hash>.README.txt`), and metadata filenames have their version numbers
prepended (e.g., `<hash>.snapshot.json`).
```Bash
$ repo.py --init --consistent
```
## Add a target file ##
Copy a target file to the repo and add it to the Targets metadata (or the
Targets role specified in --role). More than one target file, or directory,
may be specified in --add. The --recursive option may be toggled to also
include files in subdirectories of a specified directory. The Snapshot
and Timestamp metadata are also updated and signed automatically, but this
behavior can be toggled off with --no_release.
```Bash
$ repo.py --add <foo.tar.gz> <bar.tar.gz>
$ repo.py --add </path/to/dir> [--recursive]
```
Similar to the --init case, the repository location can be chosen.
```Bash
$ repo.py --add <foo.tar.gz> --path </path/to/my_repo>
```
## Remove a target file ##
Remove a target file from the Targets metadata (or the Targets role specified
in --role). More than one target file or glob pattern may be specified in
--remove. The Snapshot and Timestamp metadata are also updated and signed
automatically, but this behavior can be toggled off with --no_release.
```Bash
$ repo.py --remove <glob_pattern> ...
```
Examples:
Remove all target files, that match `foo*.tgz,` from the Targets metadata.
```Bash
$ repo.py --remove "foo*.tgz"
```
Remove all target files from the `my_role` metadata.
```Bash
$ repo.py --remove "*" --role my_role --sign tufkeystore/my_role_key
```
## Generate key ##
Generate a cryptographic key. The generated key can later be used to sign
specific metadata with `--sign`. The supported key types are: `ecdsa`,
`ed25519`, and `rsa`. If a keytype is not given, an Ed25519 key is generated.
If adding a top-level key to a bare repo (i.e., repo.py --init --bare),
the filenames of the top-level keys must be "root_key," "targets_key,"
"snapshot_key," "timestamp_key." The filename can vary for any additional
top-level key.
```Bash
$ repo.py --key
$ repo.py --key <keytype>
$ repo.py --key <keytype> [--path </path/to/repo_dir> --pw [my_password],
--filename <key_filename>]
```
Instead of using a default password, the user can enter one on the command
line or be prompted for it via password masking.
```Bash
$ repo.py --key ecdsa --pw my_password
```
```Bash
$ repo.py --key rsa --pw
Enter a password for the RSA key (...):
Confirm:
```
## Sign metadata ##
Sign, with the specified key(s), the metadata of the role indicated in --role.
The Snapshot and Timestamp role are also automatically signed, if possible, but
this behavior can be disabled with --no_release.
```Bash
$ repo.py --sign </path/to/key> ... [--role <rolename>, --path </path/to/repo>]
```
For example, to sign the delegated `foo` metadata:
```Bash
$ repo.py --sign </path/to/foo_key> --role foo
```
## Trust keys ##
The Root role specifies the trusted keys of the top-level roles, including
itself. The --trust command-line option, in conjunction with --pubkeys and
--role, can be used to indicate the trusted keys of a role.
```Bash
$ repo.py --trust --pubkeys </path/to/foo_key.pub> --role <rolename>
```
For example:
```Bash
$ repo.py --init --bare
$ repo.py --trust --pubkeys tufkeystore/my_key.pub tufkeystore/my_key_too.pub
--role root
```
### Distrust keys ###
Conversely, the Root role can discontinue trust of specified key(s).
Example of how to discontinue trust of a key:
```Bash
$ repo.py --distrust --pubkeys tufkeystore/my_key_too.pub --role root
```
## Delegations ##
Delegate trust of target files from the Targets role (or the one specified in
--role) to some other role (--delegatee). --delegatee is trusted to sign for
target files that match the delegated glob pattern(s). The --delegate option
does not create metadata for the delegated role, rather it updates the
delegator's metadata to list the delegation to --delegatee. The Snapshot and
Timestamp metadata are also updated and signed automatically, but this behavior
can be toggled off with --no_release.
```Bash
$ repo.py --delegate <glob pattern> ... --delegatee <rolename> --pubkeys
</path/to/pubkey.pub> ... [--role <rolename> --terminating --threshold <X>
--sign </path/to/role_privkey>]
```
For example, to delegate trust of `foo*.gz` packages to the `foo` role:
```
$ repo.py --delegate "foo*.tgz" --delegatee foo --pubkeys tufkeystore/foo.pub
```
## Revocations ##
Revoke trust of target files from a delegated role (--delegatee). The
"targets" role performs the revocation if --role is not specified. The
--revoke option does not delete the metadata belonging to --delegatee, instead
it removes the delegation to it from the delegator's (or --role) metadata. The
Snapshot and Timestamp metadata are also updated and signed automatically, but
this behavior can be toggled off with --no_release.
```Bash
$ repo.py --revoke --delegatee <rolename> [--role <rolename>
--sign </path/to/role_privkey>]
```
## Verbosity ##
Set the verbosity of the logger (2, by default). The lower the number, the
greater the verbosity. Logger messages are saved to `tuf.log` in the current
working directory.
```Bash
$ repo.py --verbose <0-5>
```
## Clean ##
Delete the repo in the current working directory, or the one specified with
`--path`. Specifically, the `tufrepo`, `tufclient`, and `tufkeystore`
directories are deleted.
```Bash
$ repo.py --clean
$ repo.py --clean --path </path/to/dirty/repo>
```
----
# Further Examples #
## Basic Update Delivery ##
Steps:
(1) initialize a repo.
(2) delegate trust of target files to another role.
(3) add a trusted file to the delegated role.
(4) fetch the trusted file from the delegated role.
```Bash
Step (1)
$ repo.py --init
Step (2)
$ repo.py --key ed25519 --filename mykey
$ repo.py --delegate "README.*" --delegatee myrole --pubkeys tufkeystore/mykey.pub
$ repo.py --sign tufkeystore/mykey --role myrole
Enter a password for the encrypted key (tufkeystore/mykey):
$ echo "my readme text" > README.txt
Step (3)
$ repo.py --add README.txt --role myrole --sign tufkeystore/mykey
Enter a password for the encrypted key (tufkeystore/mykey):
```
Serve the repo
```Bash
$ python3 -m http.server 8001
```
```Bash
Step (4)
$ client.py --repo http://localhost:8001 README.txt
$ tree .
.
├── tuf.log
├── tufrepo
│   └── metadata
│   ├── current
│   │   ├── 1.root.json
│   │   ├── myrole.json
│   │   ├── root.json
│   │   ├── snapshot.json
│   │   ├── targets.json
│   │   └── timestamp.json
│   └── previous
│   ├── 1.root.json
│   ├── root.json
│   ├── snapshot.json
│   ├── targets.json
│   └── timestamp.json
└── tuftargets
└── README.txt
5 directories, 13 files
```
## Correcting a Key ##
The filename of the top-level keys must be "root_key," "targets_key,"
"snapshot_key," and "root_key." The filename can vary for any additional
top-level key.
Steps:
(1) initialize a repo containing default keys for the top-level roles.
(2) distrust the default key for the root role.
(3) create a new key and trust its use with the root role.
(4) sign the root metadata file.
```Bash
Step (1)
$ repo.py --init
Step (2)
$ repo.py --distrust --pubkeys tufkeystore/root_key.pub --role root
Step (3)
$ repo.py --key ed25519 --filename root_key
$ repo.py --trust --pubkeys tufkeystore/root_key.pub --role root
Step (4)
$ repo.py --sign tufkeystore/root_key --role root
Enter a password for the encrypted key (tufkeystore/root_key):
```
## More Update Delivery ##
Steps:
(1) create a bare repo.
(2) add keys to the top-level roles.
(3) delegate trust of particular target files to another role X, where role X
has a signature threshold 2 and is marked as a terminating delegation. The
keys for role X and Y should be created prior to performing the delegation.
(4) Delegate from role X to role Y.
(5) have role X sign for a file also signed by the Targets role, to demonstrate
the expected file that should be downloaded by the client.
(6) perform an update.
(7) halt the server, add README.txt to the Targets role, restart the server,
and fetch the Target's role README.txt.
(8) Add LICENSE to 'role_y' and demonstrate that the client must not fetch it
because 'role_x' is a terminating delegation (and hasn't signed for it).
```Bash
Steps (1) and (2)
$ repo.py --init --consistent --bare
$ repo.py --key ed25519 --filename root_key
$ repo.py --trust --pubkeys tufkeystore/root_key.pub --role root
$ repo.py --key ecdsa --filename targets_key
$ repo.py --trust --pubkeys tufkeystore/targets_key.pub --role targets
$ repo.py --key rsa --filename snapshot_key
$ repo.py --trust --pubkeys tufkeystore/snapshot_key.pub --role snapshot
$ repo.py --key ecdsa --filename timestamp_key
$ repo.py --trust --pubkeys tufkeystore/timestamp_key.pub --role timestamp
$ repo.py --sign tufkeystore/root_key --role root
Enter a password for the encrypted key (tufkeystore/root_key):
$ repo.py --sign tufkeystore/targets_key --role targets
Enter a password for the encrypted key (tufkeystore/targets_key):
```
```Bash
Steps (3) and (4)
$ repo.py --key ed25519 --filename key_x
$ repo.py --key ed25519 --filename key_x2
$ repo.py --delegate "README.*" "LICENSE" --delegatee role_x --pubkeys
tufkeystore/key_x.pub tufkeystore/key_x2.pub --threshold 2 --terminating
$ repo.py --sign tufkeystore/key_x tufkeystore/key_x2 --role role_x
$ repo.py --key ed25519 --filename key_y
$ repo.py --delegate "README.*" "LICENSE" --delegatee role_y --role role_x
--pubkeys tufkeystore/key_y.pub --sign tufkeystore/key_x tufkeystore/key_x2
$ repo.py --sign tufkeystore/key_y --role role_y
```
```Bash
Steps (5) and (6)
$ echo "role_x's readme" > README.txt
$ repo.py --add README.txt --role role_x --sign tufkeystore/key_x tufkeystore/key_x2
```
Serve the repo
```Bash
$ python3 -m http.server 8001
```
Fetch the role x's README.txt
```Bash
$ client.py --repo http://localhost:8001 README.txt
$ cat tuftargets/README.txt
role_x's readme
```
```Bash
Step (7)
halt the server...
$ echo "Target role's readme" > README.txt
$ repo.py --add README.txt
restart the server...
```
```Bash
$ rm -rf tuftargets/ tuf.log
$ client.py --repo http://localhost:8001 README.txt
$ cat tuftargets/README.txt
Target role's readme
```
```Bash
Step (8)
$ echo "role_y's license" > LICENSE
$ repo.py --add LICENSE --role role_y --sign tufkeystore/key_y
```
```Bash
$ rm -rf tuftargets/ tuf.log
$ client.py --repo http://localhost:8001 LICENSE
Error: 'LICENSE' not found.
```

View file

@ -1,10 +0,0 @@
Getting Started
---------------
- `Overview of TUF <https://theupdateframework.io/overview/>`_
- `Installation <INSTALLATION.rst>`_
- Beginner Tutorials (using the basic command-line interface):
- `Quickstart <QUICKSTART.md>`_
- `CLI Documentation and Examples <CLI.md>`_
- `Advanced Tutorial <TUTORIAL.md>`_
- `Guidelines for Contributors <CONTRIBUTORS.rst>`_

View file

@ -1,149 +0,0 @@
# Quickstart #
In this quickstart tutorial, we'll use the basic TUF command-line interface
(CLI), which includes the `repo.py` script and the `client.py` script, to set
up a repository with an update and metadata about that update, then download
and verify that update as a client.
Unlike the underlying TUF modules that the CLI uses, the CLI itself is a bit
bare-bones. Using the CLI is the easiest way to familiarize yourself with
how TUF works, however. It will serve as a very basic update system.
----
**Step (0)** - Make sure TUF is installed.
Make sure that TUF is installed, along with some of the optional cryptographic
libraries and C extensions. Try this command to do that:
`python3 -m pip install securesystemslib[colors,crypto,pynacl] tuf`
If you run into errors during that pip command, please consult the more
detailed [TUF Installation Instructions](INSTALLATION.rst). (There are some
system libraries that you may need to install first.)
**Step (1)** - Create a basic repository and client.
The following command will set up a basic update repository and basic client
that knows about the repository. `tufrepo`, `tufkeystore`, and
`tufclient` directories will be created in the current directory.
```Bash
$ repo.py --init
```
Four sets of keys are created in the `tufkeystore` directory. Initial metadata
about the repository is created in the `tufrepo` directory, and also provided
to the client in the `tufclient` directory.
**Step (2)** - Add an update to the repository.
We'll create a target file that will later be delivered as an update to clients.
Metadata about that file will be created and signed, and added to the
repository's metadata.
```Bash
$ echo 'Test file' > testfile
$ repo.py --add testfile
$ tree tufrepo/
tufrepo/
├── metadata
│   ├── 1.root.json
│   ├── root.json
│   ├── snapshot.json
│   ├── targets.json
│   └── timestamp.json
├── metadata.staged
│   ├── 1.root.json
│   ├── root.json
│   ├── snapshot.json
│   ├── targets.json
│   └── timestamp.json
└── targets
└── testfile
3 directories, 11 files
```
The new file `testfile` is added to the repository, and metadata is updated in
the `tufrepo` directory. The Targets metadata (`targets.json`) now includes
the file size and hashes of the `testfile` target file, and this metadata is
signed by the Targets role's key, so that clients can verify that metadata
about `testfile` and then verify `testfile` itself.
**Step (3)** - Serve the repo.
We'll host a toy http server containing the `testfile` update and the
repository's metadata.
```Bash
$ cd "tufrepo/"
$ python3 -m http.server 8001
```
**Step (4)** - Obtain and verify the `testfile` update on a client.
The client can request the package `testfile` from the repository. TUF will
download and verify metadata from the repository as necessary to determine
what the trustworthy hashes and length of `testfile` are, then download
the target `testfile` from the repository and keep it only if it matches that
trustworthy metadata.
```Bash
$ cd "../tufclient/"
$ client.py --repo http://localhost:8001 testfile
$ tree
.
├── tufrepo
│   └── metadata
│   ├── current
│   │   ├── 1.root.json
│   │   ├── root.json
│   │   ├── snapshot.json
│   │   ├── targets.json
│   │   └── timestamp.json
│   └── previous
│   ├── 1.root.json
│   ├── root.json
│   ├── snapshot.json
│   ├── targets.json
│   └── timestamp.json
└── tuftargets
└── testfile
5 directories, 11 files
```
Now that a trustworthy update target has been obtained, an updater can proceed
however it normally would to install or use the update.
----
### Next Steps
TUF provides functionality for both ends of a software update system, the
**update provider** and the **update client**.
`repo.py` made use of `tuf.repository_tool`'s functionality for an update
provider, helping you produce and sign metadata about your updates.
`client.py` made use of `tuf.client.updater`'s client-side functionality,
performing download and the critical verification steps for metadata and the
update itself.
You can look at [CLI.md](CLI.md) to toy with the TUF CLI a bit more.
After that, try out using the underlying modules for a great deal more control.
The more detailed [Advanced Tutorial](TUTORIAL.md) shows you how to use the
underlying modules, `repository_tool` and `updater`.
Ultimately, a sophisticated update client will use or re-implement those
underlying modules. The TUF design is intended to play well with any update
workflow.
Please provide feedback or questions for this or other tutorials, or
TUF in general, by checking out
[our contact info](https://github.com/theupdateframework/python-tuf#contact), or
creating [issues](https://github.com/theupdateframework/python-tuf/issues) in this
repository!

View file

@ -1,696 +0,0 @@
# Advanced Tutorial #
## Table of Contents ##
- [How to Create and Modify a TUF Repository](#how-to-create-and-modify-a-tuf-repository)
- [Overview](#overview)
- [Keys](#keys)
- [Create RSA Keys](#create-rsa-keys)
- [Import RSA Keys](#import-rsa-keys)
- [Create and Import Ed25519 Keys](#create-and-import-ed25519-keys)
- [Create Top-level Metadata](#create-top-level-metadata)
- [Create Root](#create-root)
- [Create Timestamp, Snapshot, Targets](#create-timestamp-snapshot-targets)
- [Targets](#targets)
- [Add Target Files](#add-target-files)
- [Remove Target Files](#remove-target-files)
- [Delegations](#delegations)
- [Revoke Delegated Role](#revoke-delegated-role)
- [Wrap-up](#wrap-up)
- [Delegate to Hashed Bins](#delegate-to-hashed-bins)
- [Consistent Snapshots](#consistent-snapshots)
- [How to Perform an Update](#how-to-perform-an-update)
## How to Create and Modify a TUF Repository ##
### Overview ###
A software update system must follow two steps to integrate The Update
Framework (TUF). First, it must add the framework to the client side of the
update system. The [tuf.client.updater](../tuf/client/README.md) module assists in
integrating TUF on the client side. Second, the software repository on the
server side must be modified to include a minimum of four top-level metadata
(root.json, targets.json, snapshot.json, and timestamp.json). No additional
software is required to convert a software repository to a TUF one. The
low-level repository tool that generates the required TUF metadata for a
software repository is the focus of this tutorial. There is also separate
document that [demonstrates how TUF protects against malicious
updates](../tuf/ATTACKS.md).
The [repository tool](../tuf/repository_tool.py) contains functions to generate
all of the files needed to populate and manage a TUF repository. The tool may
either be imported into a Python module, or used with the Python interpreter in
interactive mode.
A repository object that encapsulates the metadata files of the repository can
be created or loaded by the repository tool. Repository maintainers can modify
the repository object to manipulate the metadata files stored on the
repository. TUF clients use the metadata files to validate files requested and
downloaded. In addition to the repository object, where the majority of
changes are made, the repository tool provides functions to generate and
persist cryptographic keys. The framework utilizes cryptographic keys to sign
and verify metadata files.
To begin, cryptographic keys are generated with the repository tool. However,
before metadata files can be validated by clients and target files fetched in a
secure manner, public keys must be pinned to particular metadata roles and
metadata signed by role's private keys. After covering keys, the four required
top-level metadata are created next. Examples are given demonstrating the
expected work flow, where the metadata roles are created in a specific order,
keys imported and loaded, and metadata signed and written to disk. Lastly,
target files are added to the repository, and a custom delegation performed to
extend the default roles of the repository. By the end, a fully populated TUF
repository is generated that can be used by clients to securely download
updates.
### Keys ###
The repository tool supports multiple public-key algorithms, such as
[RSA](https://en.wikipedia.org/wiki/RSA_%28cryptosystem%29) and
[Ed25519](https://ed25519.cr.yp.to/), and multiple cryptography libraries.
Using [RSA-PSS](https://tools.ietf.org/html/rfc8017#section-8.1) or
[ECDSA](https://en.wikipedia.org/wiki/Elliptic_Curve_Digital_Signature_Algorithm)
signatures requires the [cryptography](https://cryptography.io/) library. If
generation of Ed25519 signatures is needed
[PyNaCl](https://github.com/pyca/pynacl) library should be installed. This
tutorial assumes both dependencies are installed: refer to
[Installation Instructions](INSTALLATION.rst#install-with-more-cryptographic-flexibility)
for details.
The Ed25519 and ECDSA keys are stored in JSON format and RSA keys are stored in PEM
format. Private keys are encrypted and passphrase-protected (strengthened with
PBKDF2-HMAC-SHA256.) Generating, importing, and loading cryptographic key
files can be done with functions available in the repository tool.
To start, a public and private RSA key pair is generated with the
`generate_and_write_rsa_keypair()` function. The keys generated next are
needed to sign the repository metadata files created in upcoming sub-sections.
Note: In the instructions below, lines that start with `>>>` denote commands
that should be entered by the reader, `#` begins the start of a comment, and
text without prepended symbols is the output of a command.
#### Create RSA Keys ####
```python
>>> from tuf.repository_tool import *
# Generate and write the first of two root keys for the TUF repository. The
# following function creates an RSA key pair, where the private key is saved to
# "root_key" and the public key to "root_key.pub" (both saved to the current
# working directory).
>>> generate_and_write_rsa_keypair(password="password", filepath="root_key", bits=2048)
# If the key length is unspecified, it defaults to 3072 bits. A length of less
# than 2048 bits raises an exception. A similar function is available to supply
# a password on the prompt. If an empty password is entered, the private key
# is saved unencrypted.
>>> generate_and_write_rsa_keypair_with_prompt(filepath="root_key2")
enter password to encrypt private key file '/path/to/root_key2'
(leave empty if key should not be encrypted):
Confirm:
```
The following four key files should now exist:
1. **root_key**
2. **root_key.pub**
3. **root_key2**
4. **root_key2.pub**
If a filepath is not given, the KEYID of the generated key is used as the
filename. The key files are written to the current working directory.
```python
# Continuing from the previous section . . .
>>> generate_and_write_rsa_keypair_with_prompt()
enter password to encrypt private key file '/path/to/KEYID'
(leave empty if key should not be encrypted):
Confirm:
```
### Import RSA Keys ###
```python
# Continuing from the previous section . . .
# Import an existing public key.
>>> public_root_key = import_rsa_publickey_from_file("root_key.pub")
# Import an existing private key. Importing a private key requires a password,
# whereas importing a public key does not.
>>> private_root_key = import_rsa_privatekey_from_file("root_key")
enter password to decrypt private key file '/path/to/root_key'
(leave empty if key not encrypted):
```
### Create and Import Ed25519 Keys ###
```Python
# Continuing from the previous section . . .
# The same generation and import functions as for rsa keys exist for ed25519
>>> generate_and_write_ed25519_keypair_with_prompt(filepath='ed25519_key')
enter password to encrypt private key file '/path/to/ed25519_key'
(leave empty if key should not be encrypted):
Confirm:
# Import the ed25519 public key just created . . .
>>> public_ed25519_key = import_ed25519_publickey_from_file('ed25519_key.pub')
# and its corresponding private key.
>>> private_ed25519_key = import_ed25519_privatekey_from_file('ed25519_key')
enter password to decrypt private key file '/path/to/ed25519_key'
(leave empty if key should not be encrypted):
```
Note: Methods are also available to generate and write keys from memory.
* generate_ed25519_key()
* generate_ecdsa_key()
* generate_rsa_key()
* import_ecdsakey_from_pem(pem)
* import_rsakey_from_pem(pem)
### Create Top-level Metadata ###
The [metadata document](METADATA.md) outlines the JSON files that must exist
on a TUF repository. The following sub-sections demonstrate the
`repository_tool.py` calls repository maintainers may issue to generate the
required roles. The top-level roles to be created are `root`, `timestamp`,
`snapshot`, and `target`.
We begin with `root`, the locus of trust that specifies the public keys of the
top-level roles, including itself.
#### Create Root ####
```python
# Continuing from the previous section . . .
# Create a new Repository object that holds the file path to the TUF repository
# and the four top-level role objects (Root, Targets, Snapshot, Timestamp).
# Metadata files are created when repository.writeall() or repository.write()
# are called. The repository directory is created if it does not exist. You
# may see log messages indicating any directories created.
>>> repository = create_new_repository("repository")
# The Repository instance, 'repository', initially contains top-level Metadata
# objects. Add one of the public keys, created in the previous section, to the
# root role. Metadata is considered valid if it is signed by the public key's
# corresponding private key.
>>> repository.root.add_verification_key(public_root_key)
# A role's verification key(s) (to be more precise, the verification key's
# keyid) may be queried. Other attributes include: signing_keys, version,
# signatures, expiration, threshold, and delegations (attribute available only
# to a Targets role).
>>> repository.root.keys
['b23514431a53676595922e955c2d547293da4a7917e3ca243a175e72bbf718df']
# Add a second public key to the root role. Although previously generated and
# saved to a file, the second public key must be imported before it can added
# to a role.
>>> public_root_key2 = import_rsa_publickey_from_file("root_key2.pub")
>>> repository.root.add_verification_key(public_root_key2)
# The threshold of each role defaults to 1. Maintainers may change the
# threshold value, but repository_tool.py validates thresholds and warns users.
# Set the threshold of the root role to 2, which means the root metadata file
# is considered valid if it's signed by at least two valid keys. We also load
# the second private key, which hasn't been imported yet.
>>> repository.root.threshold = 2
>>> private_root_key2 = import_rsa_privatekey_from_file("root_key2", password="password")
# Load the root signing keys to the repository, which writeall() or write()
# (write multiple roles, or a single role, to disk) use to sign the root
# metadata.
>>> repository.root.load_signing_key(private_root_key)
>>> repository.root.load_signing_key(private_root_key2)
# repository.status() shows missing verification and signing keys for the
# top-level roles, and whether signatures can be created (also see #955).
# This output shows that so far only the "root" role meets the key threshold and
# can successfully sign its metadata.
>>> repository.status()
'targets' role contains 0 / 1 public keys.
'snapshot' role contains 0 / 1 public keys.
'timestamp' role contains 0 / 1 public keys.
'root' role contains 2 / 2 signatures.
'targets' role contains 0 / 1 signatures.
# In the next section we update the other top-level roles and create a repository
# with valid metadata.
```
#### Create Timestamp, Snapshot, Targets
Now that `root.json` has been set, the other top-level roles may be created.
The signing keys added to these roles must correspond to the public keys
specified by the Root role.
On the client side, `root.json` must always exist. The other top-level roles,
created next, are requested by repository clients in (Root -> Timestamp ->
Snapshot -> Targets) order to ensure required metadata is downloaded in a
secure manner.
```python
# Continuing from the previous section . . .
# 'datetime' module needed to optionally set a role's expiration.
>>> import datetime
# Generate keys for the remaining top-level roles. The root keys have been set above.
>>> generate_and_write_rsa_keypair(password='password', filepath='targets_key')
>>> generate_and_write_rsa_keypair(password='password', filepath='snapshot_key')
>>> generate_and_write_rsa_keypair(password='password', filepath='timestamp_key')
# Add the verification keys of the remaining top-level roles.
>>> repository.targets.add_verification_key(import_rsa_publickey_from_file('targets_key.pub'))
>>> repository.snapshot.add_verification_key(import_rsa_publickey_from_file('snapshot_key.pub'))
>>> repository.timestamp.add_verification_key(import_rsa_publickey_from_file('timestamp_key.pub'))
# Import the signing keys of the remaining top-level roles.
>>> private_targets_key = import_rsa_privatekey_from_file('targets_key', password='password')
>>> private_snapshot_key = import_rsa_privatekey_from_file('snapshot_key', password='password')
>>> private_timestamp_key = import_rsa_privatekey_from_file('timestamp_key', password='password')
# Load the signing keys of the remaining roles so that valid signatures are
# generated when repository.writeall() is called.
>>> repository.targets.load_signing_key(private_targets_key)
>>> repository.snapshot.load_signing_key(private_snapshot_key)
>>> repository.timestamp.load_signing_key(private_timestamp_key)
# Optionally set the expiration date of the timestamp role. By default, roles
# are set to expire as follows: root(1 year), targets(3 months), snapshot(1
# week), timestamp(1 day).
>>> repository.timestamp.expiration = datetime.datetime(2080, 10, 28, 12, 8)
# Mark roles for metadata update (see #964, #958)
>>> repository.mark_dirty(['root', 'snapshot', 'targets', 'timestamp'])
# Write all metadata to "repository/metadata.staged/"
>>> repository.writeall()
```
### Targets ###
TUF makes it possible for clients to validate downloaded target files by
including a target file's length, hash(es), and filepath in metadata. The
filepaths are relative to a `targets/` directory on the software repository. A
TUF client can download a target file by first updating the latest copy of
metadata (and thus available targets), verifying that their length and hashes
are valid, and saving the target file(s) locally to complete the update
process.
In this section, the target files intended for clients are added to a
repository and listed in `targets.json` metadata.
#### Add Target Files ####
The repository maintainer adds target files to roles (e.g., `targets` and
`unclaimed`) by specifying their filepaths. The target files must exist at the
specified filepaths before the repository tool can generate and add their
(hash(es), length, and filepath) to metadata.
First, the actual target files are manually created and saved to the `targets/`
directory of the repository:
```Bash
# Create and save target files to the targets directory of the software
# repository.
$ cd repository/targets/
$ echo 'file1' > file1.txt
$ echo 'file2' > file2.txt
$ echo 'file3' > file3.txt
$ mkdir myproject; echo 'file4' > myproject/file4.txt
$ cd ../../
```
With the target files available on the `targets/` directory of the software
repository, the `add_targets()` method of a Targets role can be called to add
the target filepaths to metadata.
```python
# Continuing from the previous section . . .
# NOTE: If you exited the Python interactive interpreter above you need to
# re-import the repository_tool-functions and re-load the repository and
# signing keys.
>>> from tuf.repository_tool import *
# The 'os' module is needed to gather file attributes, which will be included
# in a custom field for some of the target files added to metadata.
>>> import os
# Load the repository created in the previous section. This repository so far
# contains metadata for the top-level roles, but no target paths are yet listed
# in targets metadata.
>>> repository = load_repository('repository')
# Create a list of all targets in the directory.
>>> list_of_targets = ['file1.txt', 'file2.txt', 'file3.txt']
# Add the list of target paths to the metadata of the top-level Targets role.
# Any target file paths that might already exist are NOT replaced, and
# add_targets() does not create or move target files on the file system. Any
# target paths added to a role must fall under the expected targets directory,
# otherwise an exception is raised. The targets added to a role should actually
# exist once writeall() or write() is called, so that the hash and size of
# these targets can be included in Targets metadata.
>>> repository.targets.add_targets(list_of_targets)
# Individual target files may also be added to roles, including custom data
# about the target. In the example below, file permissions of the target
# (octal number specifying file access for owner, group, others e.g., 0755) is
# added alongside the default fileinfo. All target objects in metadata include
# the target's filepath, hash, and length.
# Note: target path passed to add_target() method has to be relative
# to the targets directory or an exception is raised.
>>> target4_filepath = 'myproject/file4.txt'
>>> target4_abspath = os.path.abspath(os.path.join('repository', 'targets', target4_filepath))
>>> octal_file_permissions = oct(os.stat(target4_abspath).st_mode)[4:]
>>> custom_file_permissions = {'file_permissions': octal_file_permissions}
>>> repository.targets.add_target(target4_filepath, custom_file_permissions)
```
The private keys of roles affected by the changes above must now be imported and
loaded. `targets.json` must be signed because a target file was added to its
metadata. `snapshot.json` keys must be loaded and its metadata signed because
`targets.json` has changed. Similarly, since `snapshot.json` has changed, the
`timestamp.json` role must also be signed.
```Python
# Continuing from the previous section . . .
# The private key of the updated targets metadata must be re-loaded before it
# can be signed and written (Note the load_repository() call above).
>>> private_targets_key = import_rsa_privatekey_from_file('targets_key')
enter password to decrypt private key file '/path/to/targets_key'
(leave empty if key not encrypted):
>>> repository.targets.load_signing_key(private_targets_key)
# Due to the load_repository() and new versions of metadata, we must also load
# the private keys of Snapshot and Timestamp to generate a valid set of metadata.
>>> private_snapshot_key = import_rsa_privatekey_from_file('snapshot_key')
enter password to decrypt private key file '/path/to/snapshot_key'
(leave empty if key not encrypted):
>>> repository.snapshot.load_signing_key(private_snapshot_key)
>>> private_timestamp_key = import_rsa_privatekey_from_file('timestamp_key')
enter password to decrypt private key file '/path/to/timestamp_key'
(leave empty if key not encrypted):
>>> repository.timestamp.load_signing_key(private_timestamp_key)
# Mark roles for metadata update (see #964, #958)
>>> repository.mark_dirty(['snapshot', 'targets', 'timestamp'])
# Generate new versions of the modified top-level metadata (targets, snapshot,
# and timestamp).
>>> repository.writeall()
```
#### Remove Target Files ####
Target files previously added to roles may also be removed. Removing a target
file requires first removing the target from a role and then writing the
new metadata to disk.
```python
# Continuing from the previous section . . .
# Remove a target file listed in the "targets" metadata. The target file is
# not actually deleted from the file system.
>>> repository.targets.remove_target('myproject/file4.txt')
# Mark roles for metadata update (see #964, #958)
>>> repository.mark_dirty(['snapshot', 'targets', 'timestamp'])
>>> repository.writeall()
```
#### Excursion: Dump Metadata and Append Signature ####
The following two functions are intended for those that wish to independently
sign metadata. Repository maintainers can dump the portion of metadata that is
normally signed, sign it with an external signing tool, and append the
signature to already existing metadata.
First, the signable portion of metadata can be generated as follows:
```Python
>>> signable_content = dump_signable_metadata('repository/metadata.staged/timestamp.json')
```
Then, use a tool like securesystemslib to create a signature over the signable
portion. *Note, to make the signing key count towards the role's signature
threshold, it needs to be added to `root.json`, e.g. via
`repository.timestamp.add_verification_key(key)` (not shown in below snippet).*
```python
>>> from securesystemslib.formats import encode_canonical
>>> from securesystemslib.keys import create_signature
>>> private_ed25519_key = import_ed25519_privatekey_from_file('ed25519_key')
enter password to decrypt private key file '/path/to/ed25519_key'
>>> signature = create_signature(
... private_ed25519_key, encode_canonical(signable_content).encode())
```
Finally, append the signature to the metadata
```Python
>>> append_signature(signature, 'repository/metadata.staged/timestamp.json')
```
Note that the format of the signature is the format expected in metadata, which
is a dictionary that contains a KEYID, the signature itself, etc. See the
specification and [METADATA.md](METADATA.md) for a detailed example.
### Delegations ###
All of the target files available on the software repository created so far
have been added to one role (the top-level Targets role). However, what if
multiple developers are responsible for the files of a project? What if
responsibility separation is desired? Performing a delegation, where one role
delegates trust of some paths to another role, is an option for integrators
that require additional roles on top of the top-level roles available by
default.
In the next sub-section, the `unclaimed` role is delegated from the top-level
`targets` role. The `targets` role specifies the delegated role's public keys,
the paths it is trusted to provide, and its role name. <!--
TODO: Uncomment together with "Revoke Delegated Role" section below
Furthermore, the example
below demonstrates a nested delegation from `unclaimed` to `django`. Once a
role has delegated trust to another, the delegated role may independently add
targets and generate signed metadata.
-->
```python
# Continuing from the previous section . . .
# Generate a key for a new delegated role named "unclaimed".
>>> generate_and_write_rsa_keypair(password='password', filepath='unclaimed_key', bits=2048)
>>> public_unclaimed_key = import_rsa_publickey_from_file('unclaimed_key.pub')
# Make a delegation (delegate trust of 'myproject/*.txt' files) from "targets"
# to "unclaimed", where "unclaimed" initially contains zero targets.
>>> repository.targets.delegate('unclaimed', [public_unclaimed_key], ['myproject/*.txt'])
# Thereafter, we can access the delegated role by its name to e.g. add target
# files, just like we did with the top-level targets role.
>>> repository.targets("unclaimed").add_target("myproject/file4.txt")
# Load the private key of "unclaimed" so that unclaimed's metadata can be
# signed, and valid metadata created.
>>> private_unclaimed_key = import_rsa_privatekey_from_file('unclaimed_key', password='password')
>>> repository.targets("unclaimed").load_signing_key(private_unclaimed_key)
# Mark roles for metadata update (see #964, #958)
>>> repository.mark_dirty(['snapshot', 'targets','timestamp', 'unclaimed'])
>>> repository.writeall()
```
<!--
TODO: Integrate section with an updated delegation tutorial.
As it is now, it just messes up the state of the repository, i.e. marks
"unclaimed" as dirty, although there is nothing new to write.
#### Revoke Delegated Role ####
```python
# Continuing from the previous section . . .
# Create a delegated role that will be revoked in the next step...
>>> repository.targets('unclaimed').delegate("django", [public_unclaimed_key], ['bar*.tgz'])
# Revoke "django" and write the metadata of all remaining roles.
>>> repository.targets('unclaimed').revoke("django")
>>> repository.writeall()
```
-->
#### Wrap-up ####
In summary, the five steps a repository maintainer follows to create a TUF
repository are:
1. Create a directory for the software repository that holds the TUF metadata and the target files.
2. Create top-level roles (`root.json`, `snapshot.json`, `targets.json`, and `timestamp.json`.)
3. Add target files to the `targets` role.
4. Optionally, create delegated roles to distribute target files.
5. Write the changes.
The repository tool saves repository changes to a `metadata.staged` directory.
Repository maintainers may push finalized changes to the "live" repository by
copying the staged directory to its destination.
```Bash
# Copy the staged metadata directory changes to the live repository.
$ cp -r "repository/metadata.staged/" "repository/metadata/"
```
## Consistent Snapshots ##
The basic TUF repository we have generated above is adequate for repositories
that have some way of guaranteeing consistency of repository data. A community
software repository is one example where consistency of files and metadata can
become an issue. Repositories of this kind are continually updated by multiple
maintainers and software authors uploading their packages, increasing the
likelihood that a client downloading version X of a release unexpectedly
requests the target files of a version Y just released.
To guarantee consistency of metadata and target files, a repository may
optionally support multiple versions of `snapshot.json` simultaneously, where a
client with version 1 of `snapshot.json` can download `target_file.zip` and
another client with version 2 of `snapshot.json` can also download a different
`target_file.zip` (same file name, but different file digest.) If the
`consistent_snapshot` parameter of writeall() or write() are `True`, metadata
and target file names on the file system have their digests prepended (note:
target file names specified in metadata do not contain digests in their names.)
The repository maintainer is responsible for the duration of multiple versions
of metadata and target files available on a repository. Generating consistent
metadata and target files on the repository is enabled by setting the
`consistent_snapshot` argument of `writeall()` or `write()` . Note that
changing the consistent_snapshot setting involves writing a new version of
root.
<!--
TODO: Integrate section with an updated consistent snapshot tutorial.
As it is now, it just messes up the state of the repository, i.e. marks
"root" as dirty, although all other metadata needs to be re-written with
<VERSION> prefix and target files need to be re-written with <HASH> prefix in
their filenames.
```Python
# ----- Tutorial Section: Consistent Snapshots
>>> repository.root.load_signing_key(private_root_key)
>>> repository.root.load_signing_key(private_root_key2)
>>> repository.writeall(consistent_snapshot=True)
```
-->
## Delegate to Hashed Bins ##
Why use hashed bin delegations?
For software update systems with a large number of target files, delegating to
hashed bins (a special type of delegated role) might be an easier alternative
to manually performing the delegations. How many target files should each
delegated role contain? How will these delegations affect the number of
metadata that clients must additionally download in a typical update? Hashed
bin delegations are available to integrators that rather not deal with the
management of delegated roles and a great number of target files.
A large number of target files may be distributed to multiple hashed bins with
`delegate_hashed_bins()`. The metadata files of delegated roles will be nearly
equal in size (i.e., target file paths are uniformly distributed by calculating
the target filepath's digest and determining which bin it should reside in.)
The updater client will use "lazy bin walk" (visit and download the minimum
metadata required to find a target) to find a target file's hashed bin
destination. This method is intended for repositories with a large number of
target files, a way of easily distributing and managing the metadata that lists
the targets, and minimizing the number of metadata files (and size) downloaded
by the client.
The `delegate_hashed_bins()` method has the following form:
```Python
delegate_hashed_bins(list_of_targets, keys_of_hashed_bins, number_of_bins)
```
We next provide a complete example of retrieving target paths to add to hashed
bins, performing the hashed bin delegations, signing them, and delegating paths
to some role.
```Python
# Continuing from the previous section . . .
# Remove 'myproject/file4.txt' from unclaimed role and instead further delegate
# all targets in myproject/ to hashed bins.
>>> repository.targets('unclaimed').remove_target("myproject/file4.txt")
# Get a list of target paths for the hashed bins.
>>> targets = ['myproject/file4.txt']
# Delegate trust to 32 hashed bin roles. Each role is responsible for the set
# of target files, determined by the path hash prefix. TUF evenly distributes
# hexadecimal ranges over the chosen number of bins (see output).
# To initialize the bins we use one key, which TUF warns us about (see output).
# However, we can assign separate keys to each bin, with the method used in
# previous sections, accessing a bin by its hash prefix range name, e.g.:
# "repository.targets('00-07').add_verification_key('public_00-07_key')".
>>> repository.targets('unclaimed').delegate_hashed_bins(
... targets, [public_unclaimed_key], 32)
Creating hashed bin delegations.
1 total targets.
32 hashed bins.
256 total hash prefixes.
Each bin ranges over 8 hash prefixes.
Adding a verification key that has already been used. [repeated 32x]
# The hashed bin roles can also be accessed by iterating the "delegations"
# property of the delegating role, which we do here to load the signing key.
>>> for delegation in repository.targets('unclaimed').delegations:
... delegation.load_signing_key(private_unclaimed_key)
# Mark roles for metadata update (see #964, #958)
>>> repository.mark_dirty(['00-07', '08-0f', '10-17', '18-1f', '20-27', '28-2f',
... '30-37', '38-3f', '40-47', '48-4f', '50-57', '58-5f', '60-67', '68-6f',
... '70-77', '78-7f', '80-87', '88-8f', '90-97', '98-9f', 'a0-a7', 'a8-af',
... 'b0-b7', 'b8-bf', 'c0-c7', 'c8-cf', 'd0-d7', 'd8-df', 'e0-e7', 'e8-ef',
... 'f0-f7', 'f8-ff', 'snapshot', 'timestamp', 'unclaimed'])
>>> repository.writeall()
```
## How to Perform an Update ##
The following [repository tool](../tuf/repository_tool.py) function creates a directory
structure that a client downloading new software using TUF (via
[tuf/client/updater.py](../tuf/client/updater.py)) expects. The `root.json` metadata file must exist, and
also the directories that hold the metadata files downloaded from a repository.
Software updaters integrating TUF may use this directory to store TUF updates
saved on the client side.
```python
>>> from tuf.repository_tool import *
>>> create_tuf_client_directory("repository/", "client/tufrepo/")
```
`create_tuf_client_directory()` moves metadata from `repository/metadata` to
`client/` in this example. The repository in `repository/` may be the
repository example created earlier in this document.
## Test TUF Locally ##
Run the local TUF repository server.
```Bash
$ cd "repository/"; python3 -m http.server 8001
```
We next retrieve targets from the TUF repository and save them to `client/`.
The `client.py` script is available to download metadata and files from a
specified repository. In a different command-line prompt, where `tuf` is
installed . . .
```Bash
$ cd "client/"
$ ls
tufrepo/
$ client.py --repo http://localhost:8001 file1.txt
$ ls . tuftargets/
.:
tufrepo tuftargets
tuftargets/:
file1.txt
```

View file

@ -35,11 +35,6 @@
# Add any paths that contain templates here, relative to this directory.
templates_path = ['_templates']
# List of patterns, relative to source directory, that match files and
# directories to ignore when looking for source files.
# This pattern also affects html_static_path and html_extra_path.
exclude_patterns = ['GETTING_STARTED.rst']
# -- Options for HTML output -------------------------------------------------
# The theme to use for HTML and HTML Help pages. See the documentation for

Binary file not shown.

Before

Width:  |  Height:  |  Size: 90 KiB

View file

@ -1,323 +0,0 @@
# Demonstrate protection against malicious updates
## Table of Contents ##
- [Blocking Malicious Updates](#blocking-malicious-updates)
- [Arbitrary Package Attack](#arbitrary-package-attack)
- [Rollback Attack](#rollback-attack)
- [Indefinite Freeze Attack](#indefinite-freeze-attack)
- [Endless Data Attack](#endless-data-attack)
- [Compromised Key Attack](#compromised-key-attack)
- [Slow Retrieval Attack](#slow-retrieval-attack)
- [Conclusion](#conclusion)
## Blocking Malicious Updates ##
TUF protects against a number of attacks, some of which include rollback,
arbitrary package, and mix and match attacks. We begin this document on
blocking malicious updates by demonstrating how the client rejects a target
file downloaded from the software repository that doesn't match what is listed
in TUF metadata.
The following demonstration requires and operates on the repository created in
the [repository management
tutorial](https://github.com/theupdateframework/python-tuf/blob/develop/tuf/README.md).
### Arbitrary Package Attack ###
In an arbitrary package attack, an attacker installs anything they want on the
client system. That is, an attacker can provide arbitrary files in response to
download requests and the files will not be detected as illegitimate. We
simulate an arbitrary package attack by creating a "malicious" target file
that our client attempts to fetch.
```Bash
$ mv 'repository/targets/file2.txt' 'repository/targets/file2.txt.backup'
$ echo 'bad_target' > 'repository/targets/file2.txt'
```
We next reset our local timestamp (so that a new update is prompted), and
the target files previously downloaded by the client.
```Bash
$ rm -rf "client/targets/" "client/metadata/current/timestamp.json"
```
The client now performs an update and should detect the invalid target file...
Note: The following command should be executed in the "client/" directory.
```Bash
$ python3 basic_client.py --repo http://localhost:8001
Error: No working mirror was found:
localhost:8001: BadHashError()
```
The log file (tuf.log) saved to the current working directory contains more
information on the update procedure and the cause of the BadHashError.
```Bash
...
BadHashError: Observed
hash ('f569179171c86aa9ed5e8b1d6c94dfd516123189568d239ed57d818946aaabe7') !=
expected hash (u'67ee5478eaadb034ba59944eb977797b49ca6aa8d3574587f36ebcbeeb65f70e')
[2016-10-20 19:45:16,079 UTC] [tuf.client.updater] [ERROR] [_get_file:1415@updater.py]
Failed to update /file2.txt from all mirrors: {u'http://localhost:8001/targets/file2.txt': BadHashError()}
```
Note: The "malicious" target file should be removed and the original file2.txt
restored, otherwise the following examples will fail with BadHashError
exceptions:
```Bash
$ mv 'repository/targets/file2.txt.backup' 'repository/targets/file2.txt'
```
### Indefinite Freeze Attack ###
In an indefinite freeze attack, an attacker continues to present a software
update system with the same files the client has already seen. The result is
that the client does not know that new files are available. Although the
client would be unable to prevent an attacker or compromised repository from
feeding it stale metadata, it can at least detect when an attacker is doing so
indefinitely. The signed metadata used by TUF contains an "expires" field that
indicates when metadata should no longer be trusted.
In the following simulation, the client first tries to perform an update.
```Bash
$ python3 basic_client.py --repo http://localhost:8001
```
According to the logger (`tuf.log` file in the current working directory),
everything appears to be up-to-date. The remote server should also show that
the client retrieved only the timestamp.json file. Let's suppose now that an
attacker continues to feed our client the same stale metadata. If we were to
move the time to a future date that would cause metadata to expire, the TUF
framework should raise an exception or error to indicate that the metadata
should no longer be trusted.
```Bash
$ sudo date -s '2080-12-25 12:34:56'
Wed Dec 25 12:34:56 EST 2080
$ python3 basic_client.py --repo http://localhost:8001
Error: No working mirror was found:
u'localhost:8001': ExpiredMetadataError(u"Metadata u'root' expired on Tue Jan 1 00:00:00 2030 (UTC).",)
```
Note: Reset the date to continue with the rest of the attacks.
### Rollback Attack ###
In a rollback attack, an attacker presents a software update system with older
files than those the client has already seen, causing the client to use files
older than those the client knows about. We begin this example by saving the
current version of the Timestamp file available on the repository. This saved
file will later be served to the client to see if it is rejected. The client
should not accept versions of metadata that is older than previously trusted.
Navigate to the directory containing the server's files and save the current
timestamp.json to a temporary location:
```Bash
$ cp repository/metadata/timestamp.json /tmp
```
We should next generate a new Timestamp file on the repository side.
```Bash
$ python3
>>> from tuf.repository_tool import *
>>> repository = load_repository('repository')
>>> repository.timestamp.version
1
>>> repository.timestamp.version = 2
>>> repository.dirty_roles()
Dirty roles: [u'timestamp']
>>> private_timestamp_key = import_rsa_privatekey_from_file("keystore/timestamp_key")
Enter a password for the encrypted RSA file (/path/to/keystore/timestamp_key):
>>> repository.timestamp.load_signing_key(private_timestamp_key)
>>> repository.write('timestamp')
$ cp repository/metadata.staged/* repository/metadata
```
Now start the HTTP server from the directory containing the 'repository'
subdirectory.
```Bash
$ python3 -m SimpleHTTPServer 8001
```
And perform an update so that the client retrieves the updated timestamp.json.
```Bash
$ python3 basic_client.py --repo http://localhost:8001
```
Finally, move the previous timestamp.json file to the current live repository
and have the client try to download the outdated version. The client should
reject it!
```Bash
$ cp /tmp/timestamp.json repository/metadata/
$ cd repository; python3 -m SimpleHTTPServer 8001
```
On the client side, perform an update...
```Bash
$ python3 basic_client.py --repo http://localhost:8001
Error: No working mirror was found:
u'localhost:8001': ReplayedMetadataError()
```
The tuf.log file contains more information about the ReplayedMetadataError
exception and update process. Please reset timestamp.json to the latest
version, which can be found in the 'repository/metadata.staged' subdirectory.
```Bash
$ cp repository/metadata.staged/timestamp.json repository/metadata
```
### Endless Data Attack ###
In an endless data attack, an attacker responds to a file download request with
an endless stream of data, causing harm to clients (e.g., a disk partition
filling up or memory exhaustion). In this simulated attack, we append extra
data to one of the target files available on the software repository. The
client should only download the exact number of bytes it expects for a
requested target file (according to what is listed in trusted TUF metadata).
```Bash
$ cp repository/targets/file1.txt /tmp
$ python3 -c "print 'a' * 1000" >> repository/targets/file1.txt
```
Now delete the local metadata and target files on the client side so
that remote metadata and target files are downloaded again.
```Bash
$ rm -rf client/targets/
$ rm client/metadata/current/snapshot.json* client/metadata/current/timestamp.json*
```
Lastly, perform an update to verify that the file1.txt is downloaded up to the
expected size, and no more. The target file available on the software
repository does contain more data than expected, though.
```Bash
$ python3 basic_client.py --repo http://localhost:8001
```
At this point, part of the "file1.txt" file should have been fetched. That is,
up to 31 bytes of it should have been downloaded, and the rest of the maliciously
appended data ignored. If we inspect the logger, we'd discover the following:
```Bash
[2016-10-06 21:37:39,092 UTC] [tuf.download] [INFO] [_download_file:235@download.py]
Downloading: u'http://localhost:8001/targets/file1.txt'
[2016-10-06 21:37:39,145 UTC] [tuf.download] [INFO] [_check_downloaded_length:610@download.py]
Downloaded 31 bytes out of the expected 31 bytes.
[2016-10-06 21:37:39,145 UTC] [tuf.client.updater] [INFO] [_get_file:1372@updater.py]
Not decompressing http://localhost:8001/targets/file1.txt
[2016-10-06 21:37:39,145 UTC] [tuf.client.updater] [INFO] [_check_hashes:778@updater.py]
The file's sha256 hash is correct: 65b8c67f51c993d898250f40aa57a317d854900b3a04895464313e48785440da
```
Indeed, the sha256 sum of the first 31 bytes of the "file1.txt" available
on the repository should match to what is trusted. The client did not
downloaded the appended data.
Note: Restore file1.txt
```Bash
$ cp /tmp/file1.txt repository/targets/
```
### Compromised Key Attack ###
An attacker who compromise less than a given threshold of keys is limited in
scope. This includes relying on a single online key (such as only being
protected by SSL) or a single offline key (such as most software update systems
use to sign files). In this example, we attempt to sign a role file with
less-than-a-threshold number of keys. A single key (suppose this is a
compromised key) is used to demonstrate that roles must be signed with the
total number of keys required for the role. In order to compromise a role, an
attacker would have to compromise a threshold of keys. This approach of
requiring a threshold number of signatures provides compromise resilience.
Let's attempt to sign a new snapshot file with a less-than-threshold number of
keys. The client should reject the partially signed snapshot file served by
the repository (or imagine that it is a compromised software repository).
```Bash
$ python3
>>> from tuf.repository_tool import *
>>> repository = load_repository('repository')
>>> version = repository.root.version
>>> repository.root.version = version + 1
>>> private_root_key = import_rsa_privatekey_from_file("keystore/root_key", password="password")
>>> repository.root.load_signing_key(private_root_key)
>>> private_root_key2 = import_rsa_privatekey_from_file("keystore/root_key2", password="password")
>>> repository.root.load_signing_key(private_root_key2)
>>> repository.snapshot.version = 8
>>> repository.snapshot.threshold = 2
>>> private_snapshot_key = import_rsa_privatekey_from_file("keystore/snapshot_key", password="password")
>>> repository.snapshot.load_signing_key(private_snapshot_key)
>>> repository.timestamp.version = 8
>>> private_timestamp_key = import_rsa_privatekey_from_file("keystore/timestamp_key", password="password")
>>> repository.timestamp.load_signing_key(private_timestamp_key)
>>> repository.write('root')
>>> repository.write('snapshot')
>>> repository.write('timestamp')
$ cp repository/metadata.staged/* repository/metadata
```
The client now attempts to refresh the top-level metadata and the
partially written snapshot.json, which should be rejected.
```Bash
$ python3 basic_client.py --repo http://localhost:8001
Error: No working mirror was found:
u'localhost:8001': BadSignatureError()
```
### Slow Retrieval Attack ###
In a slow retrieval attack, an attacker responds to clients with a very slow
stream of data that essentially results in the client never continuing the
update process. In this example, we simulate a slow retrieval attack by
spawning a server that serves data at a slow rate to our update client data.
TUF should not be vulnerable to this attack, and the framework should raise an
exception or error when it detects that a malicious server is serving it data
at a slow enough rate.
We first spawn the server that slowly streams data to the client. The
'slow_retrieval_server_old.py' module (can be found in the tests/ directory of the
source code) should be copied over to the server's 'repository/' directory from
which to launch it.
```Bash
# Before launching the slow retrieval server, copy 'slow_retrieval_server_old.py'
# to the 'repository/' directory and run it from that directory as follows:
$ python3 slow_retrieval_server_old.py 8002 mode_2
```
The client may now make a request to the slow retrieval server on port 8002.
However, before doing so, we'll reduce (for the purposes of this demo) the
minimum average download rate allowed and download chunk size. Open the
'settings.py' module and set MIN_AVERAGE_DOWNLOAD_SPEED = 5 and CHUNK_SIZE = 1.
This should make it so that the client detects the slow retrieval server's
delayed streaming.
```Bash
$ python3 basic_client.py --verbose 1 --repo http://localhost:8002
Error: No working mirror was found:
u'localhost:8002': SlowRetrievalError()
```
The framework should detect the slow retrieval attack and raise a
SlowRetrievalError exception to the client application.
## Conclusion ##
These are just some of the attacks that TUF provides protection against. For
more attacks and updater weaknesses, please see the
[Security](https://theupdateframework.io/security/)
page.

View file

@ -1,342 +0,0 @@
# The Update Framework Developer Tool: How to Update your Project Securely on a TUF Repository
## Table of Contents
- [Overview](#overview)
- [Creating a Simple Project](#creating_a_simple_project)
- [Generating a Key](#generating_a_key)
- [The Project Class](#the_project_class)
- [Signing and Writing the Metadata](#signing_and_writing_the_metadata)
- [Loading an Existing Project](#loading_an_existing_project)
- [Delegations](#delegations)
- [Managing Keys](#managing_keys)
- [Managing Targets](#managing_targets)
<a name="overview">
## Overview
The Update Framework (TUF) is a Python-based security system for software
updates. In order to prevent your users from downloading vulnerable or malicious
code disguised as updates to your software, TUF requires that each update you
release include certain metadata verifying your authorship of the files.
The TUF developer tools are a Python Library that enables you to create and
maintain the required metadata for files hosted on a TUF Repository. (We call
these files “targets,” to distinguish them from the metadata associated with
them. Both of these together comprise a complete “project”.) You will use these
tools to generate the keys and metadata you need to claim and secure your files
on the repository, and to update the metadata and sign it with those keys
whenever you upload a new version of those files.
This document will teach you how to use these tools in two parts. The first
part walks through the creation of a minimal-complexity TUF project, which is
all you need to get started, and can be expanded later. The second part details
the full functionality of the tools, which offer a finer degree of control in
securing your project.
<a name="creating_a_simple_project">
## Creating a Simple Project
This section walks through the creation of a small example project with just
one target. Once created, this project will be fully functional, and can be
modified as needed.
<a name="generating_a_key">
### Generating a Key
First, we will need to generate a key to sign the metadata. Keys are generated
in pairs: one public and the other private. The private key is
password-protected and is used to sign metadata. The public key can be shared
freely, and is used to verify signatures made by the private key. You will need
to share your public key with the repository hosting your project so they can
verify your metadata is signed by the right person.
The generate\_and\_write\_rsa\_keypair function will create two key files named
"path/to/key.pub", which is the public key and "path/to/key", which
is the private key.
```
>>> from tuf.developer_tool import *
>>> generate_and_write_rsa_keypair_with_prompt(filepath="path/to/key")
enter password to encrypt private key file 'path/to/key'
(leave empty if key should not be encrypted):
Confirm:
>>>
```
We can also use the bits parameter to set a different key length (the default
is 3072). We can also `generate_and_write_rsa_keypair` with a `password`
parameter if a prompt is not desired.
In this example we will be using rsa keys, but ed25519 keys are also supported.
Now we have a key for our project, we can proceed to create our project.
<a name="the_project_class">
### The Project Class
The TUF developer tool is built around the Project class, which is used to
organize groups of targets associated with a single set of metadata. A single
Project instance is used to keep track of all the target files and metadata
files in one project. The Project also keeps track of the keys and signatures,
so that it can update all the metadata with the correct changes and signatures
on a single command.
Before creating a project, you must know where it will be located in the TUF
Repository. In the following example, we will create a project to be hosted as
"repo/unclaimed/example_project" within the repository, and store a local copy
of the metadata at "path/to/metadata". The project will comprise a single
target file, "local/path/to/example\_project/target\_1" locally, and we will
secure it with the key generated above.
First, we must import the generated keys. We can do that by issuing the
following command:
```
>>> public_key = import_rsa_publickey_from_file("path/to/keys.pub")
```
After importing the key, we can generate a new project with the following
command:
```
>>> project = create_new_project(project_name="example_project",
... metadata_directory="local/path/to/metadata/",
... targets_directory="local/path/to/example_project",
... location_in_repository="repo/unclaimed", key=public_key)
```
Let's list the arguments and make sense out of this rather long function call:
- create a project named example_project: the name of the metadata file will match this name
- the metadata will be located in "local/path/to/metadata", this means all of the generated files
for this project will be located here
- the targets are located in local/path/to/example project. If your targets are located in some other
place, you can point the targets directory there. Files must reside under the path local/path/to/example_project or else it won't be possible to add them.
- location\_in\_repository points to repo/unclaimed, this will be prepended to the paths in the generated metadata so the signatures all match.
Now the project is in memory and we can do different operations on it such as
adding and removing targets, delegating files, changing signatures and keys,
etc. For the moment we are interested in adding our one and only target inside
the project.
To add a target, we issue the following method:
```
>>> project.add_target("local/path/to/example_project/target_1")
```
Note that the file "target\_1" should be located in
"local/path/to/example\_project", or this method will throw an
error.
At this point, the metadata is not valid. We have assigned a key to the
project, but we have not *signed* it with that key. Signing is the process of
generating a signature with our private key so it can be verified with the
public key by the server (upon uploading) and by the clients (when updating).
<a name="signing_and_writing_the_metadata">
### Signing and Writing the Metadata ###
In order to sign the metadata, we need to import the private key corresponding
to the public key we added to the project. One the key is loaded to the project,
it will automatically be used to sign the metadata whenever it is written.
```
>>> private_key = import_rsa_privatekey_from_file("path/to/key")
Enter password for the RSA key:
>>> project.load_signing_key(private_key)
>>> project.write()
```
When all changes to the project have been written, the metadata is ready to be
uploaded to the repository, and it is safe to exit the Python interpreter, or
to delete the Project instance.
The project can be loaded later to update changes to the project. The metadata
contains checksums that have to match the actual files or else it won't be
accepted by the upstream repository.
At this point, if you have followed all the steps in this document so far
(substituting appropriate names and filepaths) you will have created a basic
TUF project, which can be expanded as needed. The simplest way to get your
project secured is to add all your files using add\_target() (or see [Managing
Keys](#managing_keys) on how to add whole directories). If your project has
several contributors, you may want to consider adding
[delegations](#delegations) to your project.
<a name="loading_an_existing_project">
## Loading an Existing Project
To make changes to existing metadata, we will need the Project again. We can
restore it with the load_project() function.
```
>>> from tuf.developer_tool import *
>>> project = load_project("local/path/to/metadata")
```
Each time the project is loaded anew, the necessary private keys must also be
loaded in order to sign metadata.
```
>>> private_key = import_rsa_privatekey_from_file("path/to/key")
Enter a password for the RSA key:
>>> project.load_signing_key(private_key)
>>> project.write()
```
If your project does not use any delegations, the five commands above are all
you need to update your project's metadata.
<a name="delegations">
## Delegations
The project we created above is secured entirely by one key. If you want to
allow someone else to update part of your project independently, you will need
to delegate a new role for them. For example, we can do the following:
```
>>> other_key = import_rsa_publickey_from_file(“another_public_key.pub”)
>>> targets = ['local/path/to/newtarget']
>>> project.delegate(“newrole”, [other_key], targets)
```
The new role is now an attribute of the Project instance, and contains the same
methods as Project. For example, we can add targets in the same way as before:
```
>>> project(“newrole”).add_target(“delegated_1”)
```
Recall that we input the other persons key as part of a list. That list can
contain any number of public keys. We can also add keys to the role after
creating it using the [add\_verification\_key()](#adding_a_key_to_a_delegation)
method.
### Delegated Paths
By default, a delegated role is permitted to add and modify targets anywhere in
the Project's targets directory. We can delegate trust of paths to a role to
limit this permission.
```
>>> project.add_paths(["delegated/filepath"], "newrole")
```
This will prevent the delegated role from signing targets whose local filepaths
do not begin with "delegated/filepath". We can delegate several filepaths to a
role by adding them to the list in the first parameter, or by invoking the
method again. A role with multiple delegated paths can add targets to any of
them.
Note that this method is invoked from the parent role (in this case, the Project)
and takes the delegated role name as an argument.
### Nested Delegations
It is possible for a delegated role to have delegations of its own. We can do
this by calling delegate() on a delegated role:
```
>>> project("newrole").delegate(“nestedrole”, [key], targets)
```
Nested delegations function no differently than first-order delegations. to
demonstrate, adding a target to nested delegation looks like this:
```
>>> project("newrole")("nestedrole").add_target("foo")
```
### Revoking Delegations
Delegations can be revoked, removing the delegated role from the project.
```
>>> project.revoke("newrole")
```
<a name="managing_keys">
## Managing Keys
This section describes the key-related functions and parameters not covered in
the [Creating a Simple Project](#creating_a_simple_project) section.
### Additional Parameters for Key Generation
When generating keys, it is possible to specify the length of the key in bits
and its password as parameters:
```
>>> generate_and_write_rsa_keypair(password="pw", filepath="path/to/key", bits=2048)
```
The bits parameter defaults to 3072, and values below 2048 will raise an error.
The password parameter is only intended to be used in scripts.
<a name="adding_a_key_to_a_delegation">
### Adding a Key to a Delegation
New verifications keys can be added to an existing delegation using
add\_verification\_key():
```
>>> project("rolename").add_verification_key(pubkey)
```
A delegation can have several verification keys at once. By default, a
delegated role with multiple keys can be written using any one of their
corresponding signing keys. To modify this behavior, you can change the
delegated role's [threshold](#delegation_thrsholds).
### Removing a Key from a Delegation
Verification keys can also be removed, like this:
```
>>> project("rolename").remove_verification_key(pubkey)
```
Remember that a project can only have one key, so this method will return an
error if there is already a key assigned to it. In order to replace a key we
must first delete the existing one and then add the new one. It is possible to
omit the key parameter in the create\_new\_project() function, and add the key
later.
### Changing the Project Key
Each Project instance can only have one verification key. This key can be
replaced by removing it and adding a new key, in that order.
```
>>> project.remove_verification_key(oldkey)
>>> project.add_verification_key(new)
```
<a name="delegation_thresholds">
### Delegation Thresholds
Every delegated role has a threshold, which determines how many of its signing
keys need to be loaded to write the role. The threshold defaults to 1, and
should not exceed the number of verification keys assigned to the role. The
threshold can be accessed as a property of a delegated role.
```
>>> project("rolename").threshold = 2
```
The above line will set the "rolename" role's threshold to 2.
<a name="managing_targets">
## Managing Targets
There are supporting functions of the targets library to make the project
maintenance easier. These functions are described in this section.
### Adding Targets by Directory
This function is especially useful when creating a new project to add all the
files contained in the targets directory. The following code block illustrates
the usage of this function:
```
>>> list_of_targets = \
... project.get_filepaths_in_directory(“path/within/targets/folder”,
... recursive_walk=False, follow_links=False)
>>> project.add_targets(list_of_targets)
```
### Deleting Targets from a Project
It is possible that we want to delete existing targets inside our project. To
stop the developer tool from tracking this file we can issue the following
command:
```
>>> project.remove_target(“target_1”)
```
Now the target file won't be part of the metadata.

View file

@ -1,5 +0,0 @@
[Quickstart](../docs/QUICKSTART.md)
[CLI](../docs/CLI.md)
[Tutorial](../docs/TUTORIAL.md)

View file

@ -1,151 +0,0 @@
# updater.py
**updater.py** is intended as the only TUF module that software update
systems need to utilize for a low-level integration. It provides a single
class representing an updater that includes methods to download, install, and
verify metadata or target files in a secure manner. Importing
**tuf.client.updater** and instantiating its main class is all that is
required by the client prior to a TUF update request. The importation and
instantiation steps allow TUF to load all of the required metadata files
and set the repository mirror information.
The **tuf.repository_tool** module can be used to create a TUF repository. See
[tuf/README](../README.md) for more information on creating TUF repositories.
## Overview of the Update Process
1. The software update system instructs TUF to check for updates.
2. TUF downloads and verifies timestamp.json.
3. If timestamp.json indicates that snapshot.json has changed, TUF downloads and
verifies snapshot.json.
4. TUF determines which metadata files listed in snapshot.json differ from those
described in the last snapshot.json that TUF has seen. If root.json has changed,
the update process starts over using the new root.json.
5. TUF provides the software update system with a list of available files
according to targets.json.
6. The software update system instructs TUF to download a specific target
file.
7. TUF downloads and verifies the file and then makes the file available to
the software update system.
If at any point in the above procedure there is a problem (i.e., if unexpired,
signed, valid metadata cannot be retrieved from the repository), the Root file
is downloaded and the process is retried once more (and only once to avoid an
infinite loop). Optionally, the software update system using the framework
can decide how to proceed rather than automatically downloading a new Root file.
## Example Client
### Refresh TUF Metadata
```Python
# The client first imports the 'updater.py' module, the only module the
# client is required to import. The client will utilize a single class
# from this module.
import tuf.client.updater
import tuf.settings
# The only other module the client interacts with is 'settings'. The
# client accesses this module solely to set the repository directory.
# This directory will hold the files downloaded from a remote repository.
tuf.settings.repositories_directory = 'path/to/local_repository'
# Next, the client creates a dictionary object containing the repository
# mirrors. The client may download content from any one of these mirrors.
# In the example below, a single mirror named 'mirror1' is defined. The
# mirror is located at 'http://localhost:8001', and all of the metadata
# and targets files can be found in the 'metadata' and 'targets' directory,
# respectively. If the client wishes to only download target files from
# specific directories on the mirror, the 'confined_target_dirs' field
# should be set. In this example, the client hasn't set confined_target_dirs,
# which is interpreted as no confinement. In other words, the client can download
# targets from any directory or subdirectories. If the client had chosen
# 'targets1/', they would have been confined to the '/targets/targets1/'
# directory on the 'http://localhost:8001' mirror.
repository_mirrors = {'mirror1': {'url_prefix': 'http://localhost:8001',
'metadata_path': 'metadata',
'targets_path': 'targets'}}
# The updater may now be instantiated. The Updater class of 'updater.py'
# is called with two arguments. The first argument assigns a name to this
# particular updater and the second argument the repository mirrors defined
# above.
updater = tuf.client.updater.Updater('updater', repository_mirrors)
# The client calls the refresh() method to ensure it has the latest
# copies of the top-level metadata files (i.e., Root, Targets, Snapshot,
# Timestamp).
updater.refresh()
```
### Download Specific Target File
```Python
# Example demonstrating an update that downloads a specific target.
# Refresh the metadata of the top-level roles (i.e., Root, Targets, Snapshot, Timestamp).
updater.refresh()
# get_one_valid_targetinfo() updates role metadata when required. In other
# words, if the client doesn't possess the metadata that lists 'LICENSE.txt',
# get_one_valid_targetinfo() will try to fetch / update it.
target = updater.get_one_valid_targetinfo('LICENSE.txt')
updated_target = updater.updated_targets([target], destination_directory)
for target in updated_target:
updater.download_target(target, destination_directory)
# Client code here may also reference target information (including 'custom')
# by directly accessing the dictionary entries of the target. The 'custom'
# entry is additional file information explicitly set by the remote repository.
target_path = target['filepath']
target_length = target['fileinfo']['length']
target_hashes = target['fileinfo']['hashes']
target_custom_data = target['fileinfo']['custom']
# Remove any files from the destination directory that are no longer being
# tracked. For example, a target file from a previous snapshot that has since
# been removed on the remote repository.
updater.remove_obsolete_targets(destination_directory)
```
### A Simple Integration Example with client.py
``` Bash
# Assume a simple TUF repository has been setup with 'repo.py'.
$ client.py --repo http://localhost:8001
# Metadata and target files are silently updated. An exception is only raised if an error,
# or attack, is detected. Inspect 'tuf.log' for the outcome of the update process.
$ cat tuf.log
[2013-12-16 16:17:05,267 UTC] [tuf.download] [INFO][_download_file:726@download.py]
Downloading: http://localhost:8001/metadata/timestamp.json
[2013-12-16 16:17:05,269 UTC] [tuf.download] [WARNING][_check_content_length:589@download.py]
reported_length (545) < required_length (2048)
[2013-12-16 16:17:05,269 UTC] [tuf.download] [WARNING][_check_downloaded_length:656@download.py]
Downloaded 545 bytes, but expected 2048 bytes. There is a difference of 1503 bytes!
[2013-12-16 16:17:05,611 UTC] [tuf.download] [INFO][_download_file:726@download.py]
Downloading: http://localhost:8001/metadata/snapshot.json
[2013-12-16 16:17:05,612 UTC] [tuf.client.updater] [INFO][_check_hashes:636@updater.py]
The file's sha256 hash is correct: 782675fadd650eeb2926d33c401b5896caacf4fd6766498baf2bce2f3b739db4
[2013-12-16 16:17:05,951 UTC] [tuf.download] [INFO][_download_file:726@download.py]
Downloading: http://localhost:8001/metadata/targets.json
[2013-12-16 16:17:05,952 UTC] [tuf.client.updater] [INFO][_check_hashes:636@updater.py]
The file's sha256 hash is correct: a5019c28a1595c43a14cad2b6252c4d1db472dd6412a9204181ad6d61b1dd69a
[2013-12-16 16:17:06,299 UTC] [tuf.download] [INFO][_download_file:726@download.py]
Downloading: http://localhost:8001/targets/file1.txt
[2013-12-16 16:17:06,303 UTC] [tuf.client.updater] [INFO][_check_hashes:636@updater.py]
The file's sha256 hash is correct: ecdc5536f73bdae8816f0ea40726ef5e9b810d914493075903bb90623d97b1d8