python-tuf/tuf/tests/system_tests/util_test_tools.py

588 lines
No EOL
18 KiB
Python
Executable file

"""
<Program Name>
util_test_tools.py
<Author>
Konstantin Andrianov
<Started>
February 19, 2012
<Copyright>
See LICENSE for licensing information.
<Purpose>
A utility modules that provides convenient methods to make the laborious
process of test construction a bit easier.
A structure that does NOT implementing TUF. A direct download over http.
Repository + Server <---------------> Client
The TUF structure is described bellow in the class and tuf_tearDown() docs.
Repository + TUF + Server <---------> TUF + Client
<Directories>
Initialized by init_repo()
The server is pointing to 'root_repo' directory, including the '/'.
root_repo
|
----------------------------------------
| | | |
reg_repo tuf_repo tuf_client downloads
'{root_repo}/downloads/': stores all direct downloads made by the client.
'{root_repo}/tuf_downloads/': stores all downloads made by the client using
tuf.
reg_repo
|
-----------------------------
| | ... |
file(1) file(2) ... file(n)
'{root_repo}/reg_repo/': main developer's repository that contains files or
updates that need to be distributed.
tuf_repo
|
--------------------------------------------
| | |
keystore metadata targets
| | |
key1.key ... role.txt ... file(1) ...
'{root_repo}/tuf_repo/': developer's tuf-repository directory containing
following subdirectories:
'{root_repo}/tuf_repo/keystore/': directory where all signing keys are
stored.
'{root_repo}/tuf_repo/metadata/': directory where all metadata signed
metadata files are stored.
'{root_repo}/tuf_repo/targets/': directory where all tuf verified files
are stored.
tuf_client
|
metadata
|
---------------------------
| |
current previous
| |
role.txt ... role.txt ...
'{root_repo}/tuf_client/': client directory containing tuf metadata.
'{root_repo}/tuf_client/metadata/current': directory where client stores
latest metadata files.
'{root_repo}/tuf_client/metadata/current': directory where client stores
previous metadata files.
<Methods>
init_repo(tuf=True):
Initializes the repositories (depicted in the diagram above) and
starts the server process. init_repo takes one boolean argument
which when True sets-up tuf repository i.e. adds all of the
directories that start with 'tuf_' in the root_repo (depicted above).
Returns a tuple - full path of the 'root_repo' directory, and the url.
This should be sufficient to construct the tests.
cleanup():
Deletes all of the created repositories and shuts down the server.
add_file_to_repository(data):
Adds a file to the 'reg_repo' directory and writes 'data' into it.
Returns full file path of the new file.
modify_file_at_repository(filepath, data):
Modifies a file at the 'reg_repo' directory by writing 'data' into it.
'filepath' has to be an existing file at the 'reg_repo' directory.
Returns full file path of the modified file.
delete_file_at_repository(filepath):
Deletes a file at the 'reg_repo' directory.
'filepath' has to be an existing file at the 'reg_repo' directory.
read_file_content(filepath):
Returns data string of the 'filepath' content.
init_tuf():
Builds tuf repository creating all necessary directories, metadata files,
and keys.
tuf_refresh_repo():
Refreshes metadata files at the 'tuf_repo' directory i.e. role.txt's at
'{root_repo}/tuf_repo/metadata/'. Following roles are refreshed:
targets, release and timestamp. Also, the whole 'reg_repo' directory is
copied to targets directory i.e. '{root_repo}/tuf_repo/targets/'.
Note: metadata files are root.txt, targets.txt, release.txt and
timestamp.txt (denoted as 'role.txt in the diagrams'). There could be
more metadata files such us mirrors.txt. The metadata files are signed
by their corresponding roles i.e. root, targets etc.
More documentation is provided in the comment and doc blocks.
"""
import os
import sys
import time
import shutil
import random
import logging
import tempfile
import subprocess
import tuf
import tuf.interposition
import tuf.util
import tuf.client.updater
import tuf.repo.signercli as signercli
import tuf.repo.signerlib as signerlib
import tuf.repo.keystore as keystore
# Disable logging for cleaner output.
def disable_logging():
logging.getLogger('tuf')
logging.disable(logging.CRITICAL)
PASSWD = 'test'
def init_repo(tuf=False, port=None):
# Temp root directory for regular and tuf repositories.
# WARNING: tuf client stores files in '{root_repo}/downloads/' directory!
# Make sure regular download are NOT stored in the that directory when
# tuf stores its downloads there. If regular download needs to happen at
# the time when tuf has or will have tuf downloads stored there, create
# a separate directory in {root_repo} to store regular downloads in.
# Ex: mkdir(root_repo, 'reg_downloads').
root_repo = tempfile.mkdtemp(dir=os.getcwd())
os.mkdir(os.path.join(root_repo, 'reg_repo'))
os.mkdir(os.path.join(root_repo, 'downloads'))
server_proc = None
if port is None:
# Start a simple server pointing to the repository directory.
port = random.randint(30000, 45000)
command = ['python', '-m', 'SimpleHTTPServer', str(port)]
server_proc = subprocess.Popen(command, stderr=subprocess.PIPE)
# Tailor url for the repository. In order to download a 'file.txt'
# from 'reg_repo' do: url+'reg_repo/file.txt'
relpath = os.path.basename(root_repo)
url = 'http://localhost:'+str(port)+'/'+relpath+'/'
# NOTE: The delay is needed to make up for asynchronous subprocess.
# Otherwise following error might be raised:
# <urlopen error [Errno 111] Connection refused>
time.sleep(.2)
keyids = None
if tuf:
keyids = init_tuf(root_repo)
create_interposition_config(root_repo, url)
return root_repo, url, server_proc, keyids
def cleanup(root_repo, server_process=None):
if server_process is not None:
if server_process.returncode is None:
server_process.kill()
print 'Server terminated.\n'
# Clear the keystore.
keystore.clear_keystore()
# Removing repository directory.
try:
shutil.rmtree(root_repo)
except OSError, e:
pass
def add_file_to_repository(directory, data='Test String'):
junk, filepath = tempfile.mkstemp(dir=directory)
fileobj = open(filepath, 'wb')
fileobj.write(data)
fileobj.close()
return filepath
def modify_file_at_repository(filepath, data='Modified String'):
if not os.path.isfile(filepath):
msg = ('Cannot modify file path '+repr(filepath)+', it does not exist.')
sys.exit(msg)
fileobj = open(filepath, 'wb')
fileobj.write(data)
fileobj.close()
return filepath
def delete_file_at_repository(filepath):
"""
<Purpose>
Attempt to delete a file at the repository setup_info['repo_path'].
"""
if not os.path.isfile(filepath):
msg = ('Cannot remove file path '+repr(filepath)+', it does not exist.')
sys.exit(msg)
os.remove(filepath)
def read_file_content(filepath):
if not os.path.isfile(filepath):
msg = ('File path '+repr(filepath)+' does not exist. '+
'Provide a valid file to read.')
sys.exit(msg)
fileobj = open(filepath, 'rb')
data = fileobj.read()
fileobj.close()
return data
def init_tuf(root_repo):
"""
<Purpose>
Setup TUF directory structure and populated it with TUF metadata and
congfiguration files.
"""
threshold = 1
# Setup TUF-repo directory structure.
tuf_repo = os.path.join(root_repo, 'tuf_repo')
keystore_dir = os.path.join(tuf_repo, 'keystore')
metadata_dir = os.path.join(tuf_repo, 'metadata')
targets_dir = os.path.join(tuf_repo, 'targets')
os.mkdir(tuf_repo)
os.mkdir(keystore_dir)
os.mkdir(metadata_dir)
shutil.copytree(os.path.join(root_repo, 'reg_repo'), targets_dir)
# Setting TUF-client directory structure.
# 'tuf.client.updater.py' expects the 'current' and 'previous'
# directories to exist under client's 'metadata' directory.
tuf_client = os.path.join(root_repo, 'tuf_client')
tuf_client_metadata_dir = os.path.join(tuf_client, 'metadata')
current_dir = os.path.join(tuf_client_metadata_dir, 'current')
previous_dir = os.path.join(tuf_client_metadata_dir, 'previous')
os.makedirs(tuf_client_metadata_dir)
# Generate at least one rsa key.
key = signerlib.generate_and_save_rsa_key(keystore_dir, PASSWD)
keyids = [key['keyid']]
# Set role info.
info = {'keyids': [key['keyid']], 'threshold': threshold}
# 'role_info' dictionary looks like this:
# {role : {'keyids : [keyid1, ...] , 'threshold' : 1}}
# In our case 'role_info[keyids]' will only have on entry since only one
# is being used.
role_info = {}
role_list = ['root', 'targets', 'release', 'timestamp']
for role in role_list:
role_info[role] = info
# At this point there is enough information to create TUF configuration
# and metadata files.
# Build the configuration file.
conf_path = signerlib.build_config_file(metadata_dir, 365, role_info)
# Generate the 'root.txt' metadata file.
signerlib.build_root_file(conf_path, keyids, metadata_dir)
# Generate the 'targets.txt' metadata file.
signerlib.build_targets_file(targets_dir, keyids, metadata_dir)
# Generate the 'release.txt' metadata file.
signerlib.build_release_file(keyids, metadata_dir)
# Generate the 'timestamp.txt' metadata file.
signerlib.build_timestamp_file(keyids, metadata_dir)
# Move the metadata to the client's 'current' and 'previous' directories.
shutil.copytree(metadata_dir, current_dir)
shutil.copytree(metadata_dir, previous_dir)
# The repository is now setup!
return keyids
def create_interposition_config(root_repo, url):
"""
<Purpose>
Create a configuration file for tuf interposition.
Usage:
from tuf.interposition import urllib_tuf
(urllib_tuf replaces urllib module)
urllib_tuf.urlretrieve(url, filename)
"""
tuf_repo = os.path.join(root_repo, 'tuf_repo')
tuf_client = os.path.join(root_repo, 'tuf_client')
# Here is a mirrors dictionary that will allow a client to seek out
# places to download the metadata and targets from.
tuf_repo_relpath = os.path.basename(tuf_repo)
tuf_url = url+tuf_repo_relpath
# Adjusting configuration file (tuf.conf.py).
tuf.conf.repository_directory = tuf_client
# In order to implement interposition we need to have a config file with
# the following dictionary JSON-serialized.
hostname = 'localhost:9999'
interposition_dict = {"configurations":
{hostname:
{"repository_directory": tuf_client+'/',
"repository_mirrors" :
{"mirror1":
{"url_prefix": tuf_url,
"metadata_path": "metadata",
"targets_path": "targets",
"confined_target_dirs": [ "" ]}}}}}
# "target_paths": [ { "(.*\\.html)": "{0}" } ]
junk, interpose_json = tempfile.mkstemp(prefix='conf_', dir=root_repo)
with open(interpose_json, 'wb') as fileobj:
tuf.util.json.dump(interposition_dict, fileobj)
tuf.interposition.configure(filename=interpose_json)
def tuf_refresh_repo(root_repo, keyids):
"""
<Purpose>
Update TUF metadata files. Call this method whenever targets files have
changed in the 'reg_repo'.
"""
reg_repo = os.path.join(root_repo, 'reg_repo')
tuf_repo = os.path.join(root_repo, 'tuf_repo')
targets_dir = os.path.join(tuf_repo, 'targets')
metadata_dir = os.path.join(tuf_repo, 'metadata')
for directory in [reg_repo, tuf_repo, targets_dir, metadata_dir]:
if not os.path.isdir(directory):
msg = ('Directory '+repr(directory)+' does not exist. '+
'Verify that all directories were setup properly.')
raise OSError(msg)
shutil.rmtree(targets_dir)
shutil.copytree(reg_repo, targets_dir)
# Regenerate the 'targets.txt' metadata file.
signerlib.build_targets_file(targets_dir, keyids, metadata_dir)
# Regenerate the 'release.txt' metadata file.
signerlib.build_release_file(keyids, metadata_dir)
# Regenerate the 'timestamp.txt' metadata file.
signerlib.build_timestamp_file(keyids, metadata_dir)
def tuf_refresh_release_timestamp(metadata_dir, keyids):
# Regenerate the 'release.txt' metadata file.
signerlib.build_release_file(keyids, metadata_dir)
def tuf_refresh_and_download():
"""
Combines tuf_refresh_repo(), tuf_refresh_client_metadata(), and
tuf_download_updates().
Returns 'tuf_downloads' directory.
"""
tuf_refresh_repo()
tuf_refresh_client_metadata()
tuf_download_updates()
return setup_info['downloads']
def _get_metadata_directory(metadata_dir):
def _mock_get_meta_dir(directory=metadata_dir):
return directory
# Patch signercli._get_metadata_directory()
signercli._get_metadata_directory = _mock_get_meta_dir
# This method patches signercli._prompt() that are called from
# make_role_metadata methods (e.g., tuf.signercli.make_root_metadata()).
def _make_metadata_mock_prompts(targets_dir, conf_path):
def _mock_prompt(msg, junk):
if msg.startswith('\nEnter the directory containing the target'):
return targets_dir
elif msg.startswith('\nEnter the configuration file path'):
return conf_path
else:
error_msg = ('Prompt: '+'\''+msg[1:]+'\''+
' did not match any predefined mock prompts.')
self.fail(error_msg)
# Patch signercli._prompt().
signercli._prompt = _mock_prompt
def _get_password(password):
# Mock '_get_password' method.
def _mock_get_password(msg, password=password):
return password
# Monkey patch '_prompt'.
signercli._get_password = _mock_get_password
def _make_role_metadata_wrapper(root_repo, func):
original_get_metadata_directory = signercli._get_metadata_directory
original_prompt = signercli._prompt
original_get_password = signercli._get_password
tuf_repo = os.path.join(root_repo, 'tuf_repo')
reg_repo = os.path.join(root_repo, 'reg_repo')
targets_dir = os.path.join(tuf_repo, 'targets')
metadata_dir = os.path.join(tuf_repo, 'metadata')
keystore_dir = os.path.join(tuf_repo, 'keystore')
conf_path = os.path.join(metadata_dir, 'config.cfg')
_get_metadata_directory(metadata_dir)
_get_password(PASSWD)
if func.__name__ == 'make_targets_metadata':
shutil.rmtree(targets_dir)
shutil.copytree(reg_repo, targets_dir)
_make_metadata_mock_prompts(targets_dir, conf_path)
else:
_make_metadata_mock_prompts(reg_repo, conf_path)
func(keystore_dir)
keystore.clear_keystore()
signercli._get_password = original_get_password
signercli._prompt = original_prompt
signercli._get_metadata_directory = original_get_metadata_directory
def make_targets_meta(root_repo):
_make_role_metadata_wrapper(root_repo, signercli.make_targets_metadata)
def make_release_meta(root_repo):
_make_role_metadata_wrapper(root_repo, signercli.make_release_metadata)
def make_timestamp_meta(root_repo):
_make_role_metadata_wrapper(root_repo, signercli.make_timestamp_metadata)
def create_delegation(tuf_repo, delegated_targets_path, keyid, keyid_password,
parent_role, new_role_name):
keystore_dir = os.path.join(tuf_repo, 'keystore')
metadata_dir = os.path.join(tuf_repo, 'metadata')
original_get_metadata_directory = signercli._get_metadata_directory
original_prompt = signercli._prompt
original_get_password = signercli._get_password
original_get_keyids = signercli._get_keyids
# Patch signercli._get_metadata_directory()
_get_metadata_directory(metadata_dir)
# Mock method for signercli._prompt().
def _mock_prompt(msg, junk, targets_path=delegated_targets_path,
parent_role=parent_role, new_role_name=new_role_name):
if msg.startswith('\nThe directory entered'):
return targets_path
elif msg.startswith('\nChoose and enter the parent'):
return parent_role
elif msg.endswith('\nEnter the delegated role\'s name: '):
return new_role_name
else:
error_msg = ('Prompt: '+'\''+msg+'\''+
' did not match any predefined mock prompts.')
sys.exit(error_msg)
# Patch signercli._prompt().
signercli._prompt = _mock_prompt
# Mock method for signercli._get_password().
def _mock_get_password(msg, keyid=keyid, password=keyid_password):
_keyid = keyid[0]
if msg.endswith('('+_keyid+'): '):
return keyid_password
else:
return PASSWD # password for targets' keyid.
# Patch signercli._get_password().
signercli._get_password = _mock_get_password
# Method to patch signercli._get_keyids()
def _mock_get_keyid(junk, keyid=keyid):
return keyid
# Patch signercli._get_keyids().
signercli._get_keyids = _mock_get_keyid
signercli.make_delegation(keystore_dir)
keystore.clear_keystore()
signercli._get_keyids = original_get_keyids
signercli._get_password = original_get_password
signercli._prompt = original_prompt
signercli._get_metadata_directory = original_get_metadata_directory