mirror of
https://github.com/theupdateframework/python-tuf
synced 2026-05-24 10:08:28 +00:00
While it may be nice, the use of a keystore/ directory for the temporary keys created in the tutorial complicates the code a good bit when it's done in a portable way (tons of os.path.join() calls), and that's not worth it. It also is a slight complication in a tutorial that profits from being as simple as possible. Tests will be run in multiple environments (including non-Linux environments) and to leave so many extra subdirectory uses in the tutorial means that the tutorial test will deviate over a large number of lines from the TUTORIAL.md content it is intended to test, which would be bad. This commit adjusts both the tutorial doc and the regression test for the tutorial. Signed-off-by: Sebastien Awwad <sebastien.awwad@gmail.com>
376 lines
12 KiB
Python
376 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)
|
|
|
|
repository.targets('unclaimed').add_restricted_paths(
|
|
'repository/targets/myproject/*', 'django')
|
|
|
|
|
|
|
|
# ----- 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()
|