2018-08-20 17:44:10 +00:00
#!/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 .
2018-08-20 22:18:44 +00:00
This essentially runs the tutorial and checks some results .
There are a few deviations from the TUTORIAL . md instructions :
- steps that involve user input ( like passphrases ) are modified slightly
to not require user input
- use of path separators ' / ' is replaced by join ( ) calls . ( We assume that
when following the tutorial , users will correctly deal with path
separators for their system if they happen to be using non - Linux systems . )
- shell instructions are mimicked using Python commands
2018-08-20 17:44:10 +00:00
"""
# 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
2018-08-20 21:56:10 +00:00
generate_and_write_rsa_keypair ( ' root_key ' , bits = 2048 , password = ' password ' )
2018-08-20 17:44:10 +00:00
# Skipping user entry of password
2018-08-20 21:56:10 +00:00
## generate_and_write_rsa_keypair('root_key2')
generate_and_write_rsa_keypair ( ' root_key2 ' , password = ' password ' )
2018-08-20 17:44:10 +00:00
# Tutorial tells users to expect these files to exist:
# ['root_key', 'root_key.pub', 'root_key2', 'root_key2.pub']
2018-08-20 21:56:10 +00:00
for fname in [ ' root_key ' , ' root_key.pub ' , ' root_key2 ' , ' root_key2.pub ' ] :
2018-08-20 17:44:10 +00:00
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
2018-08-20 21:56:10 +00:00
public_root_key = import_rsa_publickey_from_file ( ' root_key.pub ' )
2018-08-20 17:44:10 +00:00
# Skipping user entry of password
2018-08-20 21:56:10 +00:00
## private_root_key = import_rsa_privatekey_from_file('root_key')
private_root_key = import_rsa_privatekey_from_file ( ' root_key ' , ' password ' )
2018-08-20 17:44:10 +00:00
# Skipping user entry of password
2018-08-20 21:56:10 +00:00
## import_rsa_privatekey_from_file('root_key')
2018-08-20 17:44:10 +00:00
with self . assertRaises ( securesystemslib . exceptions . CryptoError ) :
2018-08-20 21:56:10 +00:00
import_rsa_privatekey_from_file ( ' root_key ' , ' not_the_real_pw ' )
2018-08-20 17:44:10 +00:00
# ----- Tutorial Section: Create and Import Ed25519 Keys
# Skipping user entry of password
2018-08-20 21:56:10 +00:00
## generate_and_write_ed25519_keypair('ed25519_key')
generate_and_write_ed25519_keypair ( ' ed25519_key ' , password = ' password ' )
2018-08-20 17:44:10 +00:00
2018-08-20 21:56:10 +00:00
public_ed25519_key = import_ed25519_publickey_from_file ( ' ed25519_key.pub ' )
2018-08-20 17:44:10 +00:00
# Skipping user entry of password
2018-08-20 21:56:10 +00:00
## private_ed25519_key = import_ed25519_privatekey_from_file('ed25519_key')
2018-08-20 17:44:10 +00:00
private_ed25519_key = import_ed25519_privatekey_from_file (
2018-08-20 21:56:10 +00:00
' ed25519_key ' , ' password ' )
2018-08-20 17:44:10 +00:00
# ----- Tutorial Section: Create Top-level Metadata
2018-08-20 22:08:17 +00:00
repository = create_new_repository ( ' repository ' )
2018-08-20 17:44:10 +00:00
repository . root . add_verification_key ( public_root_key )
self . assertTrue ( repository . root . keys )
2018-08-20 21:56:10 +00:00
public_root_key2 = import_rsa_publickey_from_file ( ' root_key2.pub ' )
2018-08-20 17:44:10 +00:00
repository . root . add_verification_key ( public_root_key2 )
repository . root . threshold = 2
private_root_key2 = import_rsa_privatekey_from_file (
2018-08-20 21:56:10 +00:00
' root_key2 ' , password = ' password ' )
2018-08-20 17:44:10 +00:00
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 ( )
2018-08-20 21:56:10 +00:00
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 ' )
2018-08-20 17:44:10 +00:00
repository . targets . add_verification_key ( import_rsa_publickey_from_file (
2018-08-20 21:56:10 +00:00
' targets_key.pub ' ) )
2018-08-20 17:44:10 +00:00
repository . snapshot . add_verification_key ( import_rsa_publickey_from_file (
2018-08-20 21:56:10 +00:00
' snapshot_key.pub ' ) )
2018-08-20 17:44:10 +00:00
repository . timestamp . add_verification_key ( import_rsa_publickey_from_file (
2018-08-20 21:56:10 +00:00
' timestamp_key.pub ' ) )
2018-08-20 17:44:10 +00:00
# Skipping user entry of password
2018-08-20 21:56:10 +00:00
## private_targets_key = import_rsa_privatekey_from_file('targets_key')
2018-08-20 17:44:10 +00:00
private_targets_key = import_rsa_privatekey_from_file (
2018-08-20 21:56:10 +00:00
' targets_key ' , ' password ' )
2018-08-20 17:44:10 +00:00
# Skipping user entry of password
2018-08-20 21:56:10 +00:00
## private_snapshot_key = import_rsa_privatekey_from_file('snapshot_key')
2018-08-20 17:44:10 +00:00
private_snapshot_key = import_rsa_privatekey_from_file (
2018-08-20 21:56:10 +00:00
' snapshot_key ' , ' password ' )
2018-08-20 17:44:10 +00:00
# Skipping user entry of password
2018-08-20 21:56:10 +00:00
## private_timestamp_key = import_rsa_privatekey_from_file('timestamp_key')
2018-08-20 17:44:10 +00:00
private_timestamp_key = import_rsa_privatekey_from_file (
2018-08-20 21:56:10 +00:00
' timestamp_key ' , ' password ' )
2018-08-20 17:44:10 +00:00
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 ../../
2018-08-20 22:14:35 +00:00
with open ( os . path . join ( ' repository ' , ' targets ' , ' file1.txt ' ) , ' w ' ) as fobj :
2018-08-20 17:44:10 +00:00
fobj . write ( ' file1 ' )
2018-08-20 22:14:35 +00:00
with open ( os . path . join ( ' repository ' , ' targets ' , ' file2.txt ' ) , ' w ' ) as fobj :
2018-08-20 17:44:10 +00:00
fobj . write ( ' file2 ' )
2018-08-20 22:14:35 +00:00
with open ( os . path . join ( ' repository ' , ' targets ' , ' file3.txt ' ) , ' w ' ) as fobj :
2018-08-20 17:44:10 +00:00
fobj . write ( ' file3 ' )
2018-08-20 22:14:35 +00:00
os . mkdir ( os . path . join ( ' repository ' , ' targets ' , ' myproject ' ) )
with open ( os . path . join ( ' repository ' , ' targets ' , ' myproject ' , ' file4.txt ' ) ,
' w ' ) as fobj :
2018-08-20 17:44:10 +00:00
fobj . write ( ' file4 ' )
2018-08-20 22:08:17 +00:00
repository = load_repository ( ' repository ' )
2018-08-20 17:44:10 +00:00
list_of_targets = repository . get_filepaths_in_directory (
2018-08-20 22:14:35 +00:00
os . path . join ( ' repository ' , ' targets ' ) , recursive_walk = False , followlinks = True )
2018-08-20 17:44:10 +00:00
2018-08-20 22:21:53 +00:00
# Ensure that we have absolute paths. (Harmless before and after PR #774,
# which fixes the issue with non-absolute paths coming from
# get_filepaths_in_directory.)
list_of_targets_temp = [ ]
for t in list_of_targets :
list_of_targets_temp . append ( os . path . abspath ( t ) )
list_of_targets = list_of_targets_temp
self . assertEqual ( sorted ( list_of_targets ) , [
os . path . abspath ( os . path . join ( ' repository ' , ' targets ' , ' file1.txt ' ) ) ,
os . path . abspath ( os . path . join ( ' repository ' , ' targets ' , ' file2.txt ' ) ) ,
os . path . abspath ( os . path . join ( ' repository ' , ' targets ' , ' file3.txt ' ) ) ] )
2018-08-20 17:44:10 +00:00
repository . targets . add_targets ( list_of_targets )
2018-08-20 22:21:53 +00:00
self . assertTrue ( ' file1.txt ' in repository . targets . target_files )
self . assertTrue ( ' file2.txt ' in repository . targets . target_files )
self . assertTrue ( ' file3.txt ' in repository . targets . target_files )
2018-08-20 22:14:35 +00:00
target4_filepath = os . path . abspath ( os . path . join (
' repository ' , ' targets ' , ' myproject ' , ' file4.txt ' ) )
2018-08-20 17:44:10 +00:00
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 )
2018-08-20 22:21:53 +00:00
self . assertTrue ( os . path . join (
' myproject ' , ' file4.txt ' ) in repository . targets . target_files )
2018-08-20 17:44:10 +00:00
# Skipping user entry of password
2018-08-20 21:56:10 +00:00
## private_targets_key = import_rsa_privatekey_from_file('targets_key')
2018-08-20 17:44:10 +00:00
private_targets_key = import_rsa_privatekey_from_file (
2018-08-20 21:56:10 +00:00
' targets_key ' , ' password ' )
2018-08-20 17:44:10 +00:00
repository . targets . load_signing_key ( private_targets_key )
# Skipping user entry of password
2018-08-20 21:56:10 +00:00
## private_snapshot_key = import_rsa_privatekey_from_file('snapshot_key')
2018-08-20 17:44:10 +00:00
private_snapshot_key = import_rsa_privatekey_from_file (
2018-08-20 21:56:10 +00:00
' snapshot_key ' , ' password ' )
2018-08-20 17:44:10 +00:00
repository . snapshot . load_signing_key ( private_snapshot_key )
# Skipping user entry of password
2018-08-20 21:56:10 +00:00
## private_timestamp_key = import_rsa_privatekey_from_file('timestamp_key')
2018-08-20 17:44:10 +00:00
private_timestamp_key = import_rsa_privatekey_from_file (
2018-08-20 21:56:10 +00:00
' timestamp_key ' , ' password ' )
2018-08-20 17:44:10 +00:00
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 ( )
2018-08-20 22:18:44 +00:00
repository . targets . remove_target ( ' file3.txt ' )
2018-08-20 22:14:35 +00:00
self . assertTrue ( os . path . exists ( os . path . join (
' repository ' , ' targets ' , ' file3.txt ' ) ) )
2018-08-20 17:44:10 +00:00
repository . writeall ( )
2018-08-20 22:20:09 +00:00
signable_content = dump_signable_metadata (
os . path . join ( ' repository ' , ' metadata.staged ' , ' targets.json ' ) )
append_signature (
{ ' keyid ' : ' 99aabb ' , ' sig ' : ' 000000 ' } ,
os . path . join ( ' repository ' , ' metadata.staged ' , ' targets.json ' ) )
2018-08-20 17:44:10 +00:00
# ----- Tutorial Section: Delegations
generate_and_write_rsa_keypair (
2018-08-20 21:56:10 +00:00
' unclaimed_key ' , bits = 2048 , password = ' password ' )
public_unclaimed_key = import_rsa_publickey_from_file ( ' unclaimed_key.pub ' )
2018-08-20 17:44:10 +00:00
repository . targets . delegate (
2018-08-20 22:08:17 +00:00
' unclaimed ' , [ public_unclaimed_key ] , [ ' foo*.tgz ' ] )
2018-08-20 17:44:10 +00:00
# Skipping user entry of password
2018-08-20 21:56:10 +00:00
## private_unclaimed_key = import_rsa_privatekey_from_file('unclaimed_key')
2018-08-20 17:44:10 +00:00
private_unclaimed_key = import_rsa_privatekey_from_file (
2018-08-20 21:56:10 +00:00
' unclaimed_key ' , ' password ' )
2018-08-20 17:44:10 +00:00
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 ( )
2018-08-20 22:08:17 +00:00
repository . targets ( ' unclaimed ' ) . delegate ( ' django ' , [ public_unclaimed_key ] , [ ' bar*.tgz ' ] )
repository . targets ( ' unclaimed ' ) . revoke ( ' django ' )
2018-08-20 17:44:10 +00:00
repository . writeall ( )
2018-08-20 22:05:37 +00:00
# Simulate the following shell command:
## $ cp -r "repository/metadata.staged/" "repository/metadata/"
shutil . copytree (
os . path . join ( ' repository ' , ' metadata.staged ' ) ,
os . path . join ( ' repository ' , ' metadata ' ) )
2018-08-20 17:44:10 +00:00
# ----- Tutorial Section: Consistent Snapshots
2018-08-20 22:12:07 +00:00
repository . root . load_signing_key ( private_root_key )
repository . root . load_signing_key ( private_root_key2 )
2018-08-20 17:44:10 +00:00
repository . writeall ( consistent_snapshot = True )
# ----- Tutorial Section: Delegate to Hashed Bins
targets = repository . get_filepaths_in_directory (
2018-08-20 22:14:35 +00:00
os . path . join ( ' repository ' , ' targets ' , ' myproject ' ) , recursive_walk = True )
2018-08-20 17:44:10 +00:00
for delegation in repository . targets ( ' unclaimed ' ) . delegations :
delegation . load_signing_key ( private_unclaimed_key )
# ----- 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 ( ) :
"""
2018-08-20 21:56:10 +00:00
Delete temporary files and directories from this test ( or with the same name
as those created by this test . . . ) .
2018-08-20 17:44:10 +00:00
"""
2018-08-20 21:56:10 +00:00
for directory in [ ' repository ' , ' my_repo ' , ' client ' ,
2018-08-20 17:44:10 +00:00
' repository/targets/my_project ' ] :
if os . path . exists ( directory ) :
shutil . rmtree ( directory )
2018-08-20 21:56:10 +00:00
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 ' ] :
2018-08-20 17:44:10 +00:00
if os . path . exists ( fname ) :
os . remove ( fname )
# Run unit test.
if __name__ == ' __main__ ' :
unittest . main ( )