diff --git a/tests/test_tutorial.py b/tests/test_tutorial.py new file mode 100644 index 00000000..b30cd6bc --- /dev/null +++ b/tests/test_tutorial.py @@ -0,0 +1,370 @@ +#!/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()