python-tuf/tests/test_tutorial.py
Sebastien Awwad 56f14c4342 Remove add_restricted_paths from TUTORIAL.md and test_tutorial.py
add_restricted_paths was renamed to add_path; however, this
function represents a problematic element of TUF that assumes
that roles are have a single delegator and delegatee, and that
one can refer to a role's expected keys without being concerned
about any delegation metadata....

So this is being removed from the tutorial. In time, add_paths
will either be removed or changed (to expect a delegator role
and a delegatee role, not just a delegatee role).

This comment does not do justice to the issue: please see TUF
GitHub Issue #660:
https://github.com/theupdateframework/tuf/issues/660

Signed-off-by: Sebastien Awwad <sebastien.awwad@gmail.com>
2019-12-16 15:16:24 +01:00

373 lines
12 KiB
Python

#!/usr/bin/env python
"""
<Program Name>
test_tutorial.py
<Copyright>
See LICENSE-MIT OR LICENSE for licensing information.
<Purpose>
Regression test for the TUF tutorial as laid out in TUTORIAL.md.
This simply runs the tutorial and checks some results.
"""
# Help with Python 3 compatibility, where the print statement is a function, an
# implicit relative import is invalid, and the '/' operator performs true
# division. Example: print 'hello world' raises a 'SyntaxError' exception.
from __future__ import print_function
from __future__ import absolute_import
from __future__ import division
from __future__ import unicode_literals
import unittest
import datetime # part of TUTORIAL.md
import os # part of TUTORIAL.md, but also needed separately
import shutil
from tuf.repository_tool import * # part of TUTORIAL.md
import securesystemslib.exceptions
class TestTutorial(unittest.TestCase):
def setUp(self):
clean_test_environment()
def tearDown(self):
clean_test_environment()
def test_tutorial(self):
"""
Run the TUTORIAL.md tutorial.
Note that anywhere the tutorial provides a command that prompts for the
user to enter a passphrase/password, this test is changed to simply provide
that as an argument. It's not worth trying to arrange automated testing of
the interactive password entry process here. Anywhere user entry has been
skipped from the tutorial instructions, "# Skipping user entry of password"
is written, with the original line below it, starting with ##.
"""
repo = create_new_repository("my_repo")
# ----- Tutorial Section: Keys
generate_and_write_rsa_keypair('root_key', bits=2048, password='password')
# Skipping user entry of password
## generate_and_write_rsa_keypair('root_key2')
generate_and_write_rsa_keypair('root_key2', password='password')
# Tutorial tells users to expect these files to exist:
# ['root_key', 'root_key.pub', 'root_key2', 'root_key2.pub']
for fname in ['root_key', 'root_key.pub', 'root_key2', 'root_key2.pub']:
self.assertTrue(os.path.exists(fname))
# Note: Skipping the creation of an effectively-randomly named key file, as
# that is harder to clean up.
## generate_and_write_rsa_keypair()
# ----- Tutorial Section: Import RSA Keys
public_root_key = import_rsa_publickey_from_file('root_key.pub')
# Skipping user entry of password
## private_root_key = import_rsa_privatekey_from_file('root_key')
private_root_key = import_rsa_privatekey_from_file('root_key', 'password')
# Skipping user entry of password
## import_rsa_privatekey_from_file('root_key')
with self.assertRaises(securesystemslib.exceptions.CryptoError):
import_rsa_privatekey_from_file('root_key', 'not_the_real_pw')
# ----- Tutorial Section: Create and Import Ed25519 Keys
# Skipping user entry of password
## generate_and_write_ed25519_keypair('ed25519_key')
generate_and_write_ed25519_keypair('ed25519_key', password='password')
public_ed25519_key = import_ed25519_publickey_from_file('ed25519_key.pub')
# Skipping user entry of password
## private_ed25519_key = import_ed25519_privatekey_from_file('ed25519_key')
private_ed25519_key = import_ed25519_privatekey_from_file(
'ed25519_key', 'password')
# ----- Tutorial Section: Create Top-level Metadata
repository = create_new_repository("repository/")
repository.root.add_verification_key(public_root_key)
self.assertTrue(repository.root.keys)
public_root_key2 = import_rsa_publickey_from_file('root_key2.pub')
repository.root.add_verification_key(public_root_key2)
repository.root.threshold = 2
private_root_key2 = import_rsa_privatekey_from_file(
'root_key2', password='password')
repository.root.load_signing_key(private_root_key)
repository.root.load_signing_key(private_root_key2)
# TODO: dirty_roles() doesn't return the list of dirty roles; it just
# prints the list. It should probably it should return it as well.
# If that's not changed, perhaps we should test the print output from the
# dirty_roles() statement here.
repository.dirty_roles()
# self.assertEqual(repository.dirty_roles(), ['root'])
# TODO: status() should return some sort of value that indicates what
# it prints. It's currently just printing status information.
# If that's not changed, perhaps we should test the print output from the
# status() statement here.
repository.status()
generate_and_write_rsa_keypair('targets_key', password='password')
generate_and_write_rsa_keypair('snapshot_key', password='password')
generate_and_write_rsa_keypair('timestamp_key', password='password')
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'))
# Skipping user entry of password
## private_targets_key = import_rsa_privatekey_from_file('targets_key')
private_targets_key = import_rsa_privatekey_from_file(
'targets_key', 'password')
# Skipping user entry of password
## private_snapshot_key = import_rsa_privatekey_from_file('snapshot_key')
private_snapshot_key = import_rsa_privatekey_from_file(
'snapshot_key', 'password')
# Skipping user entry of password
## private_timestamp_key = import_rsa_privatekey_from_file('timestamp_key')
private_timestamp_key = import_rsa_privatekey_from_file(
'timestamp_key', 'password')
repository.targets.load_signing_key(private_targets_key)
repository.snapshot.load_signing_key(private_snapshot_key)
repository.timestamp.load_signing_key(private_timestamp_key)
repository.timestamp.expiration = datetime.datetime(2080, 10, 28, 12, 8)
repository.writeall()
# ----- Tutorial Section: Targets
# These next commands in the tutorial are shown as bash commands, so I'll
# just simulate this with some Python commands.
## $ cd repository/targets/
## $ echo 'file1' > file1.txt
## $ echo 'file2' > file2.txt
## $ echo 'file3' > file3.txt
## $ mkdir myproject; echo 'file4' > myproject/file4.txt
## $ cd ../../
with open('repository/targets/file1.txt', 'w') as fobj:
fobj.write('file1')
with open('repository/targets/file2.txt', 'w') as fobj:
fobj.write('file2')
with open('repository/targets/file3.txt', 'w') as fobj:
fobj.write('file3')
os.mkdir('repository/targets/myproject')
with open('repository/targets/myproject/file4.txt', 'w') as fobj:
fobj.write('file4')
repository = load_repository("repository/")
list_of_targets = repository.get_filepaths_in_directory(
"repository/targets/", recursive_walk=False, followlinks=True)
self.assertEqual(sorted(list_of_targets),
['repository/targets/file1.txt', 'repository/targets/file2.txt',
'repository/targets/file3.txt'])
repository.targets.add_targets(list_of_targets)
target4_filepath = "repository/targets/myproject/file4.txt"
octal_file_permissions = oct(os.stat(target4_filepath).st_mode)[4:]
custom_file_permissions = {'file_permissions': octal_file_permissions}
repository.targets.add_target(target4_filepath, custom_file_permissions)
# Skipping user entry of password
## private_targets_key = import_rsa_privatekey_from_file('targets_key')
private_targets_key = import_rsa_privatekey_from_file(
'targets_key', 'password')
repository.targets.load_signing_key(private_targets_key)
# Skipping user entry of password
## private_snapshot_key = import_rsa_privatekey_from_file('snapshot_key')
private_snapshot_key = import_rsa_privatekey_from_file(
'snapshot_key', 'password')
repository.snapshot.load_signing_key(private_snapshot_key)
# Skipping user entry of password
## private_timestamp_key = import_rsa_privatekey_from_file('timestamp_key')
private_timestamp_key = import_rsa_privatekey_from_file(
'timestamp_key', 'password')
repository.timestamp.load_signing_key(private_timestamp_key)
# TODO: dirty_roles() doesn't return the list of dirty roles; it just
# prints the list. It should probably it should return it as well.
# If that's not changed, perhaps we should test the print output from the
# dirty_roles() statement here.
repository.dirty_roles()
# self.assertEqual(
# repository.dirty_roles(), ['timestamp', 'snapshot', 'targets'])
repository.writeall()
repository.targets.remove_target("repository/targets/file3.txt")
self.assertTrue(os.path.exists('repository/targets/file3.txt'))
repository.writeall()
signable_content = dump_signable_metadata('targets.json')
append_signature(signature, 'targets.json')
# ----- Tutorial Section: Delegations
generate_and_write_rsa_keypair(
'unclaimed_key', bits=2048, password='password')
public_unclaimed_key = import_rsa_publickey_from_file('unclaimed_key.pub')
repository.targets.delegate(
"unclaimed", [public_unclaimed_key], ['/foo*.tgz'])
# Skipping user entry of password
## private_unclaimed_key = import_rsa_privatekey_from_file('unclaimed_key')
private_unclaimed_key = import_rsa_privatekey_from_file(
'unclaimed_key', 'password')
repository.targets("unclaimed").load_signing_key(private_unclaimed_key)
repository.targets("unclaimed").version = 2
# TODO: dirty_roles() doesn't return the list of dirty roles; it just
# prints the list. It should probably it should return it as well.
# If that's not changed, perhaps we should test the print output from the
# dirty_roles() statement here.
repository.dirty_roles()
# self.assertEqual(repository.dirty_roles(),
# ['timestamp', 'snapshot', 'targets', 'unclaimed'])
repository.writeall()
repository.targets('unclaimed').revoke("django")
repository.targets('unclaimed').delegate('django', [public_unclaimed_key], ['/bar*.tgz'])
repository.writeall()
# Copying metadata to live repository not done here, as it is not tested
# or worked with further in the tutorial (so we'd just copy and then
# delete.)
# ----- Tutorial Section: Consistent Snapshots
repository.writeall(consistent_snapshot=True)
# ----- Tutorial Section: Delegate to Hashed Bins
targets = repository.get_filepaths_in_directory(
'repository/targets/myproject', recursive_walk=True)
for delegation in repository.targets('unclaimed').delegations:
delegation.load_signing_key(private_unclaimed_key)
# ----- Tutorial Section: How to Perform an Update
# A separate tutorial is linked to for client use. That is not tested here.
create_tuf_client_directory("repository/", "client/")
# ----- Tutorial Section: Test TUF Locally
# TODO: Run subprocess to simulate the following bash instructions:
# $ cd "repository/"; python -m SimpleHTTPServer 8001
# If running Python 3:
# $ 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 . . .
# $ cd "client/"
# $ ls
# metadata/
# $ client.py --repo http://localhost:8001 file1.txt
# $ ls . targets/
# .:
# metadata targets
# targets/:
# file1.txt
def clean_test_environment():
"""
Delete temporary files and directories from this test (or with the same name
as those created by this test...).
"""
for directory in ['repository', 'my_repo', 'client',
'repository/targets/my_project']:
if os.path.exists(directory):
shutil.rmtree(directory)
for fname in [
os.path.join('repository', 'targets', 'file1.txt'),
os.path.join('repository', 'targets', 'file2.txt'),
os.path.join('repository', 'targets', 'file3.txt'),
'root_key',
'root_key.pub',
'root_key2',
'root_key2.pub',
'ed25519_key',
'ed25519_key.pub',
'targets_key',
'targets_key.pub',
'snapshot_key',
'snapshot_key.pub',
'timestamp_key',
'timestamp_key.pub',
'unclaimed_key',
'unclaimed_key.pub']:
if os.path.exists(fname):
os.remove(fname)
# Run unit test.
if __name__ == '__main__':
unittest.main()