mirror of
https://github.com/theupdateframework/python-tuf
synced 2026-05-24 10:08:28 +00:00
371 lines
13 KiB
Python
371 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()
|