mirror of
https://github.com/theupdateframework/python-tuf
synced 2026-05-24 10:08:28 +00:00
running test_tutorial.py attempts the commands replicated from TUTORIAL.md. This should help us avoid breaking the tutorial with future changes without noticing by having automated testing run the tutorial and produce helpful output. NOTE that this test currently fails because the tutorial is currently broken! Signed-off-by: Sebastien Awwad <sebastien.awwad@gmail.com>
370 lines
13 KiB
Python
370 lines
13 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(
|
|
"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()
|