mirror of
https://github.com/theupdateframework/python-tuf
synced 2026-05-24 10:08:28 +00:00
450 lines
15 KiB
Python
Executable file
450 lines
15 KiB
Python
Executable file
#!/usr/bin/env python
|
|
|
|
"""
|
|
<Program Name>
|
|
quickstart.py
|
|
|
|
<Author>
|
|
Vladimir Diaz <vladimir.v.diaz@gmail.com>
|
|
|
|
<Started>
|
|
June 2012. Based on a previous version by Geremy Condra.
|
|
|
|
<Copyright>
|
|
See LICENSE for licensing information.
|
|
|
|
<Purpose>
|
|
This script acts as a handy quickstart for TUF, helping project and
|
|
repository maintainers get into the game as quickly and painlessly as
|
|
possible. 'quickstart.py' creates the metadata files for all the top-level
|
|
roles (along with their respective cryptographic keys), all of the
|
|
target files specified by the user, and a configuration file named
|
|
'config.cfg'. The user may then use the 'signercli' script to modify,
|
|
if they wish, the basic repository created by 'quickstart.py'.
|
|
|
|
If executed successfully, 'quickstart.py' saves the 'repository', 'keystore',
|
|
and 'client' directories to the current directory. The 'repository' directory
|
|
should be transferred to the server responding to TUF repository requests.
|
|
'keystore' and the individual encrypted key files should be securely stored
|
|
and managed by the repository maintainer; these files will be needed again
|
|
when modifying the metadata files. 'client' should be initially distributed
|
|
to users by the software updater utilizing TUF.
|
|
|
|
The Update Framework may be tested locally with the output of 'quickstart.py'
|
|
in two easy steps.
|
|
|
|
# If you need a basic server for testing purposes
|
|
$ cd repository; python -m SimpleHTTPServer 8001
|
|
|
|
# This next step is performed by the client. Here we are using the basic
|
|
# client, which will securely update all target files. In a new terminal ...
|
|
$ cd client; python basic_client.py --repo http://localhost:8001
|
|
|
|
# You can also test a custom client by running the 'example_client.py' script
|
|
# provided with TUF.
|
|
$ cd client; python example_client.py
|
|
|
|
|
|
'quickstart.py' is invoked once to set up the repository. 'signercli.py' is
|
|
used to update the repository on the server. In the case of updated targets,
|
|
the repository maintainer would simply add/delete target files from the
|
|
'targets' directory on the server and execute the following three commands to
|
|
generate updated metadata files:
|
|
|
|
$ python signercli.py --maketargets ./keystore
|
|
$ python signercli.py --makerelease ./keystore
|
|
$ python signercli.py --maketimestamp ./keystore
|
|
|
|
The next time the client queries the server, the top-level metadata files are
|
|
updated and any updated target files downloaded.
|
|
|
|
|
|
<Usage>
|
|
$ python quickstart.py --<option> argument
|
|
|
|
Examples:
|
|
$ python quickstart.py --project ./project-files/
|
|
$ python quickstart.py --project ./project-files/ --verbose 1
|
|
|
|
|
|
'quickstart.py' will request threshold values for the top-level roles.
|
|
For recommended values and more information on the files generated by
|
|
this script, consult the documentation provided in the 'docs' directory.
|
|
|
|
<Options>
|
|
--verbose:
|
|
Set the verbosity level of logging messages. Accepts values 1-5.
|
|
The lower the setting, the greater the verbosity.
|
|
|
|
--project:
|
|
Specify the project directory containing the target files to be
|
|
served by the TUF repository.
|
|
|
|
"""
|
|
|
|
import datetime
|
|
import getpass
|
|
import sys
|
|
import os
|
|
import optparse
|
|
import ConfigParser
|
|
import shutil
|
|
import tempfile
|
|
import logging
|
|
import errno
|
|
|
|
import tuf
|
|
import tuf.repo.signerlib
|
|
import tuf.repo.keystore
|
|
import tuf.formats
|
|
import tuf.util
|
|
import tuf.log
|
|
|
|
# See 'log.py' to learn how logging is handled in TUF.
|
|
logger = logging.getLogger('tuf')
|
|
|
|
# Set the default file names for the top-level roles.
|
|
# For instance: in 'signerlib.py', ROOT_FILENAME = 'root.txt'.
|
|
ROOT_FILENAME = tuf.repo.signerlib.ROOT_FILENAME
|
|
TARGETS_FILENAME = tuf.repo.signerlib.TARGETS_FILENAME
|
|
RELEASE_FILENAME = tuf.repo.signerlib.RELEASE_FILENAME
|
|
TIMESTAMP_FILENAME = tuf.repo.signerlib.TIMESTAMP_FILENAME
|
|
|
|
# The maximum number of attempts the user has to enter
|
|
# valid input.
|
|
MAX_INPUT_ATTEMPTS = 3
|
|
|
|
|
|
def _prompt(message, result_type=str):
|
|
"""
|
|
Prompt the user for input by printing 'message', converting
|
|
the input to 'result_type', and returning the value to the
|
|
caller.
|
|
|
|
"""
|
|
|
|
return result_type(raw_input(message))
|
|
|
|
|
|
|
|
|
|
|
|
def _get_password(prompt='Password: ', confirm=False):
|
|
"""
|
|
Return the password entered by the user. If 'confirm'
|
|
is True, the user is asked to enter the previously
|
|
entered password once again. If they match, the
|
|
password is returned to the caller.
|
|
|
|
"""
|
|
|
|
while True:
|
|
# getpass() prompts the user for a password without echoing
|
|
# the user input.
|
|
password = getpass.getpass(prompt, sys.stderr)
|
|
if not confirm:
|
|
return password
|
|
password2 = getpass.getpass('Confirm: ', sys.stderr)
|
|
if password == password2:
|
|
return password
|
|
else:
|
|
print 'Mismatch; try again.'
|
|
|
|
|
|
|
|
|
|
|
|
def build_repository(project_directory):
|
|
"""
|
|
<Purpose>
|
|
Build a basic TUF repository. All of the required files needed by a
|
|
repository mirror are created, such as the metadata files of the top-level
|
|
roles, cryptographic keys, and the directories containing all of the target
|
|
files.
|
|
|
|
<Arguments>
|
|
project_directory:
|
|
The directory containing the target files to be copied over to the
|
|
targets directory of the repository.
|
|
|
|
<Exceptions>
|
|
tuf.RepositoryError, if there was an error building the repository.
|
|
|
|
<Side Effects>
|
|
The repository files created are written to disk to the current
|
|
working directory.
|
|
|
|
<Returns>
|
|
None.
|
|
|
|
"""
|
|
|
|
# Do the arguments have the correct format?
|
|
# Raise 'tuf.RepositoryError' if there is a mismatch.
|
|
try:
|
|
tuf.formats.PATH_SCHEMA.check_match(project_directory)
|
|
except tuf.FormatError, e:
|
|
message = str(e)
|
|
raise tuf.RepositoryError(message)
|
|
|
|
# Verify the 'project_directory' argument.
|
|
project_directory = os.path.abspath(project_directory)
|
|
try:
|
|
tuf.repo.signerlib.check_directory(project_directory)
|
|
except (tuf.FormatError, tuf.Error), e:
|
|
message = str(e)
|
|
raise tuf.RepositoryError(message)
|
|
|
|
# Handle the expiration time. The expiration date determines when
|
|
# the top-level roles expire.
|
|
message = '\nWhen would you like your certificates to expire? (mm/dd/yyyy): '
|
|
timeout = None
|
|
for attempt in range(MAX_INPUT_ATTEMPTS):
|
|
# Get the difference between the user's entered expiration date and today's
|
|
# date. Convert and store the difference to total days till expiration.
|
|
try:
|
|
input_date = _prompt(message)
|
|
expiration_date = datetime.datetime.strptime(input_date, '%m/%d/%Y')
|
|
time_difference = expiration_date - datetime.datetime.now()
|
|
timeout = time_difference.days
|
|
if timeout < 1:
|
|
raise ValueError
|
|
break
|
|
except ValueError, e:
|
|
logger.error('Invalid expiration date entered')
|
|
timeout = None
|
|
continue
|
|
|
|
# Was a valid value for 'timeout' set?
|
|
if timeout is None:
|
|
raise tuf.RepositoryError('Could not get a valid expiration date\n')
|
|
|
|
# Build the repository directories.
|
|
metadata_directory = None
|
|
targets_directory = None
|
|
|
|
# Save the repository directory to the current directory, with
|
|
# an initial name of 'repository'. The repository maintainer
|
|
# may opt to rename this directory and should transfer it elsewhere,
|
|
# such as the webserver that will respond to TUF requests.
|
|
repository_directory = os.path.join(os.getcwd(), 'repository')
|
|
|
|
# Copy the files from the project directory to the repository's targets
|
|
# directory. The targets directory will hold all the individual
|
|
# target files.
|
|
targets_directory = os.path.join(repository_directory, 'targets')
|
|
temporary_directory = tempfile.mkdtemp()
|
|
temporary_targets = os.path.join(temporary_directory, 'targets')
|
|
shutil.copytree(project_directory, temporary_targets)
|
|
|
|
# Remove the log file created by the tuf logger, if it exists.
|
|
# It might exist if the current directory was specified as the
|
|
# project directory on the command-line.
|
|
log_filename = tuf.log._DEFAULT_LOG_FILENAME
|
|
if log_filename in os.listdir(temporary_targets):
|
|
log_file = os.path.join(temporary_targets, log_filename)
|
|
os.remove(log_file)
|
|
|
|
# Try to create the repository directory.
|
|
try:
|
|
os.mkdir(repository_directory)
|
|
# 'OSError' raised if the directory cannot be created.
|
|
except OSError, e:
|
|
message = 'Trying to create a new repository over an old repository '+\
|
|
'installation. Remove '+repr(repository_directory)+' before '+\
|
|
'trying again.'
|
|
if e.errno == errno.EEXIST:
|
|
raise tuf.RepositoryError(message)
|
|
else:
|
|
raise
|
|
|
|
# Move the temporary targets directory into place now that repository
|
|
# directory has been created and remove previously created temporary
|
|
# directory.
|
|
shutil.move(temporary_targets, targets_directory)
|
|
os.rmdir(temporary_directory)
|
|
|
|
# Try to create the metadata directory that will hold all of the
|
|
# metadata files, such as 'root.txt' and 'release.txt'.
|
|
try:
|
|
metadata_directory = os.path.join(repository_directory, 'metadata')
|
|
logger.info('Creating '+repr(metadata_directory))
|
|
os.mkdir(metadata_directory)
|
|
except OSError, e:
|
|
if e.errno == errno.EEXIST:
|
|
pass
|
|
else:
|
|
raise
|
|
|
|
# Set the keystore directory.
|
|
keystore_directory = os.path.join(os.getcwd(), 'keystore')
|
|
|
|
# Try to create the keystore directory.
|
|
try:
|
|
os.mkdir(keystore_directory)
|
|
# 'OSError' raised if the directory cannot be created.
|
|
except OSError, e:
|
|
if e.errno == EEXIST:
|
|
pass
|
|
else:
|
|
raise
|
|
|
|
# Build the keystore and save the generated keys.
|
|
role_info = {}
|
|
for role in ['root', 'targets', 'release', 'timestamp']:
|
|
# Ensure the user inputs a valid threshold value.
|
|
role_threshold = None
|
|
for attempt in range(MAX_INPUT_ATTEMPTS):
|
|
message = '\nEnter the desired threshold for the role '+repr(role)+': '
|
|
|
|
# Check for non-integers and values less than one.
|
|
try:
|
|
role_threshold = _prompt(message, int)
|
|
if not tuf.formats.THRESHOLD_SCHEMA.matches(role_threshold):
|
|
raise ValueError
|
|
break
|
|
except ValueError, e:
|
|
logger.warning('Invalid role threshold entered')
|
|
role_threshold = None
|
|
continue
|
|
|
|
# Did the user input a valid threshold value?
|
|
if role_threshold is None:
|
|
raise tuf.RepositoryError('Could not build the keystore\n')
|
|
|
|
# Retrieve the password(s) for 'role', generate the key(s),
|
|
# and save them to the keystore.
|
|
for threshold in range(role_threshold):
|
|
message = 'Enter a password for '+repr(role)+' ('+str(threshold+1)+'): '
|
|
password = _get_password(message, confirm=True)
|
|
key = tuf.repo.signerlib.generate_and_save_rsa_key(keystore_directory,
|
|
password)
|
|
try:
|
|
role_info[role]['keyids'].append(key['keyid'])
|
|
except KeyError:
|
|
info = {'keyids': [key['keyid']], 'threshold': role_threshold}
|
|
role_info[role] = info
|
|
|
|
# At this point the keystore is built and the 'role_info' dictionary
|
|
# looks something like this:
|
|
# {'keyids : [keyid1, keyid2] , 'threshold' : 2}
|
|
|
|
# Build the configuration file.
|
|
config_filepath = tuf.repo.signerlib.build_config_file(repository_directory,
|
|
timeout, role_info)
|
|
|
|
# Generate the 'root.txt' metadata file.
|
|
root_keyids = role_info['root']['keyids']
|
|
tuf.repo.signerlib.build_root_file(config_filepath, root_keyids,
|
|
metadata_directory)
|
|
|
|
# Generate the 'targets.txt' metadata file.
|
|
targets_keyids = role_info['targets']['keyids']
|
|
tuf.repo.signerlib.build_targets_file(targets_directory, targets_keyids,
|
|
metadata_directory)
|
|
|
|
# Generate the 'release.txt' metadata file.
|
|
release_keyids = role_info['release']['keyids']
|
|
tuf.repo.signerlib.build_release_file(release_keyids, metadata_directory)
|
|
|
|
# Generate the 'timestamp.txt' metadata file.
|
|
timestamp_keyids = role_info['timestamp']['keyids']
|
|
tuf.repo.signerlib.build_timestamp_file(timestamp_keyids, metadata_directory)
|
|
|
|
# Generate the 'client' directory containing the metadata of the created
|
|
# repository. 'tuf.client.updater.py' expects the 'current' and 'previous'
|
|
# directories to exist under 'metadata'.
|
|
client_metadata_directory = os.path.join(os.getcwd(), 'client', 'metadata')
|
|
try:
|
|
os.makedirs(client_metadata_directory)
|
|
except OSError, e:
|
|
message = 'Cannot create a fresh client metadata directory: '+\
|
|
repr(client_metadata_directory)+'. The client metadata '+\
|
|
'will need to be manually created. See the README file.'
|
|
if e.errno == errno.EEXIST:
|
|
logger.warn(message)
|
|
else:
|
|
raise
|
|
|
|
# Move the metadata to the client's 'current' and 'previous' directories.
|
|
client_current = os.path.join(client_metadata_directory, 'current')
|
|
client_previous = os.path.join(client_metadata_directory, 'previous')
|
|
shutil.copytree(metadata_directory, client_current)
|
|
shutil.copytree(metadata_directory, client_previous)
|
|
|
|
|
|
|
|
|
|
|
|
def parse_options():
|
|
"""
|
|
<Purpose>
|
|
Parse the command-line options and set the logging level,
|
|
as specified by the user using the '--verbose' option.
|
|
The user must also set the '--project' option. If unset,
|
|
the current directory is used as the location of the project
|
|
files. The project files are copied by 'quickstart.py' and
|
|
saved to the repository's targets directory.
|
|
|
|
<Arguments>
|
|
None.
|
|
|
|
<Exceptions>
|
|
None.
|
|
|
|
<Side Effects>
|
|
Sets the logging level for TUF logging.
|
|
|
|
<Returns>
|
|
The 'options.PROJECT_DIRECTORY' string.
|
|
|
|
"""
|
|
|
|
parser = optparse.OptionParser()
|
|
|
|
# Add the options supported by 'quickstart' to the option parser.
|
|
parser.add_option('--verbose', dest='VERBOSE', type=int, default=3,
|
|
help='Set the verbosity level of logging messages.'
|
|
'The lower the setting, the greater the verbosity.')
|
|
|
|
parser.add_option('--project', dest='PROJECT_DIRECTORY', type='string',
|
|
default='.', help='Specify the directory containing the '
|
|
'project files to host on the TUF repository.')
|
|
|
|
options, args = parser.parse_args()
|
|
|
|
# Set the logging level.
|
|
if options.VERBOSE == 5:
|
|
tuf.log.set_log_level(logging.CRITICAL)
|
|
elif options.VERBOSE == 4:
|
|
tuf.log.set_log_level(logging.ERROR)
|
|
elif options.VERBOSE == 3:
|
|
tuf.log.set_log_level(logging.WARNING)
|
|
elif options.VERBOSE == 2:
|
|
tuf.log.set_log_level(logging.INFO)
|
|
elif options.VERBOSE == 1:
|
|
tuf.log.set_log_level(logging.DEBUG)
|
|
else:
|
|
tuf.log.set_log_level(logging.NOTSET)
|
|
|
|
# Return the directory containing the project files. These files
|
|
# are copied over to the targets directory of the repository.
|
|
return options.PROJECT_DIRECTORY
|
|
|
|
|
|
|
|
if __name__ == '__main__':
|
|
|
|
# Parse the options and set the logging level.
|
|
project_directory = parse_options()
|
|
|
|
# Build the repository. The top-level metadata files, cryptographic keys,
|
|
# target files, and the configuration file are created.
|
|
try:
|
|
build_repository(project_directory)
|
|
except tuf.RepositoryError, e:
|
|
sys.stderr.write(str(e)+'\n')
|
|
sys.exit(1)
|
|
|
|
print '\nSuccessfully created the repository.'
|
|
sys.exit(0)
|