#!/usr/bin/env python """ test_tutorial.py See LICENSE-MIT OR LICENSE for licensing information. 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( "keystore/root_key", bits=2048, password="password") # Skipping user entry of password ## generate_and_write_rsa_keypair("keystore/root_key2") generate_and_write_rsa_keypair("keystore/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 [ 'keystore/root_key', 'keystore/root_key.pub', 'keystore/root_key2', 'keystore/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("keystore/root_key.pub") # Skipping user entry of password ## private_root_key = import_rsa_privatekey_from_file("keystore/root_key") private_root_key = import_rsa_privatekey_from_file( "keystore/root_key", 'password') # Skipping user entry of password ## import_rsa_privatekey_from_file('keystore/root_key') with self.assertRaises(securesystemslib.exceptions.CryptoError): import_rsa_privatekey_from_file('keystore/root_key', 'not_the_real_pw') # ----- Tutorial Section: Create and Import Ed25519 Keys # Skipping user entry of password ## generate_and_write_ed25519_keypair('keystore/ed25519_key') generate_and_write_ed25519_keypair( 'keystore/ed25519_key', password='password') public_ed25519_key = import_ed25519_publickey_from_file( 'keystore/ed25519_key.pub') # Skipping user entry of password ## private_ed25519_key = import_ed25519_privatekey_from_file('keystore/ed25519_key') private_ed25519_key = import_ed25519_privatekey_from_file( 'keystore/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("keystore/root_key2.pub") repository.root.add_verification_key(public_root_key2) repository.root.threshold = 2 private_root_key2 = import_rsa_privatekey_from_file( "keystore/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("keystore/targets_key", password="password") generate_and_write_rsa_keypair("keystore/snapshot_key", password="password") generate_and_write_rsa_keypair( "keystore/timestamp_key", password="password") repository.targets.add_verification_key(import_rsa_publickey_from_file( "keystore/targets_key.pub")) repository.snapshot.add_verification_key(import_rsa_publickey_from_file( "keystore/snapshot_key.pub")) repository.timestamp.add_verification_key(import_rsa_publickey_from_file( "keystore/timestamp_key.pub")) # Skipping user entry of password ## private_targets_key = import_rsa_privatekey_from_file("keystore/targets_key") private_targets_key = import_rsa_privatekey_from_file( "keystore/targets_key", 'password') # Skipping user entry of password ## private_snapshot_key = import_rsa_privatekey_from_file("keystore/snapshot_key") private_snapshot_key = import_rsa_privatekey_from_file( "keystore/snapshot_key", 'password') # Skipping user entry of password ## private_timestamp_key = import_rsa_privatekey_from_file("keystore/timestamp_key") private_timestamp_key = import_rsa_privatekey_from_file( "keystore/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("keystore/targets_key") private_targets_key = import_rsa_privatekey_from_file( "keystore/targets_key", 'password') repository.targets.load_signing_key(private_targets_key) # Skipping user entry of password ## private_snapshot_key = import_rsa_privatekey_from_file("keystore/snapshot_key") private_snapshot_key = import_rsa_privatekey_from_file( "keystore/snapshot_key", 'password') repository.snapshot.load_signing_key(private_snapshot_key) # Skipping user entry of password ## private_timestamp_key = import_rsa_privatekey_from_file("keystore/timestamp_key") private_timestamp_key = import_rsa_privatekey_from_file( "keystore/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( "keystore/unclaimed_key", bits=2048, password="password") public_unclaimed_key = import_rsa_publickey_from_file( "keystore/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("keystore/unclaimed_key") private_unclaimed_key = import_rsa_privatekey_from_file( "keystore/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').delegate("django", [public_unclaimed_key], ['/bar*.tgz']) repository.targets('unclaimed').revoke("django") 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 from this test and ensure expected starting state. """ print('Warning: clean test environment not yet implemented.') for directory in ['repository', 'my_repo', 'client', 'keystore', 'repository/targets/my_project']: if os.path.exists(directory): shutil.rmtree(directory) for fname in ['repository/targets/file1.txt', 'repository/targets/file2.txt', 'repository/targets/file3.txt']: if os.path.exists(fname): os.remove(fname) # Run unit test. if __name__ == '__main__': unittest.main()