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
"""
import unittest
import datetime # part of TUTORIAL.md
import os # part of TUTORIAL.md, but also needed separately
import shutil
2019-11-20 13:28:04 +00:00
import tempfile
2019-11-20 13:31:48 +00:00
import sys
2021-08-24 14:50:55 +00:00
import unittest . mock as mock
2018-08-20 17:44:10 +00:00
from tuf . repository_tool import * # part of TUTORIAL.md
2020-11-19 12:45:09 +00:00
from tests import utils
2020-09-15 15:05:51 +00:00
2018-08-20 17:44:10 +00:00
import securesystemslib . exceptions
2019-11-20 13:31:48 +00:00
from securesystemslib . formats import encode_canonical # part of TUTORIAL.md
from securesystemslib . keys import create_signature # part of TUTORIAL.md
2018-08-20 17:44:10 +00:00
class TestTutorial ( unittest . TestCase ) :
def setUp ( self ) :
2019-11-20 13:28:04 +00:00
self . working_dir = os . getcwd ( )
self . test_dir = os . path . realpath ( tempfile . mkdtemp ( ) )
os . chdir ( self . test_dir )
2018-08-20 17:44:10 +00:00
def tearDown ( self ) :
2019-11-20 13:28:04 +00:00
os . chdir ( self . working_dir )
shutil . rmtree ( self . test_dir )
2018-08-20 17:44:10 +00:00
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 ##.
"""
# ----- Tutorial Section: Keys
2020-10-28 11:25:13 +00:00
generate_and_write_rsa_keypair ( password = ' password ' , filepath = ' root_key ' , bits = 2048 )
2018-08-20 17:44:10 +00:00
# Skipping user entry of password
2020-10-28 11:25:13 +00:00
## generate_and_write_rsa_keypair_with_prompt('root_key2')
generate_and_write_rsa_keypair ( password = ' password ' , filepath = ' root_key2 ' )
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 ) )
2019-11-20 13:31:48 +00:00
# Generate key pair at /path/to/KEYID
fname = generate_and_write_rsa_keypair ( password = " password " )
self . assertTrue ( os . path . exists ( fname ) )
2018-08-20 17:44:10 +00:00
# ----- 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
2020-10-28 11:25:13 +00:00
## generate_and_write_ed25519_keypair_with_prompt('ed25519_key')
generate_and_write_ed25519_keypair ( password = ' password ' , filepath = ' ed25519_key ' )
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 )
2019-12-11 16:10:48 +00:00
# NOTE: The tutorial does not call dirty_roles anymore due to #964 and
# #958. We still call it here to see if roles are dirty as expected.
2019-11-20 13:31:48 +00:00
with mock . patch ( " tuf.repository_tool.logger " ) as mock_logger :
repository . dirty_roles ( )
# Concat strings to avoid Python2/3 unicode prefix problems ('' vs. u'')
mock_logger . info . assert_called_with ( " Dirty roles: " + str ( [ ' root ' ] ) )
# Patch logger to assert that it accurately logs the repo's status. Since
# the logger is called multiple times, we have to assert for the accurate
# sequence of calls or rather its call arguments.
with mock . patch ( " tuf.repository_lib.logger " ) as mock_logger :
repository . status ( )
# Concat strings to avoid Python2/3 unicode prefix problems ('' vs. u'')
self . assertListEqual ( [
repr ( ' targets ' ) + " role contains 0 / 1 public keys. " ,
repr ( ' snapshot ' ) + " role contains 0 / 1 public keys. " ,
repr ( ' timestamp ' ) + " role contains 0 / 1 public keys. " ,
repr ( ' root ' ) + " role contains 2 / 2 signatures. " ,
repr ( ' targets ' ) + " role contains 0 / 1 signatures. "
] , [ args [ 0 ] for args , _ in mock_logger . info . call_args_list ] )
2018-08-20 17:44:10 +00:00
2020-10-28 11:25:13 +00:00
generate_and_write_rsa_keypair ( password = ' password ' , filepath = ' targets_key ' )
generate_and_write_rsa_keypair ( password = ' password ' , filepath = ' snapshot_key ' )
generate_and_write_rsa_keypair ( password = ' password ' , filepath = ' timestamp_key ' )
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 )
2019-12-11 16:10:48 +00:00
# NOTE: The tutorial does not call dirty_roles anymore due to #964 and
# #958. We still call it here to see if roles are dirty as expected.
2019-11-20 13:31:48 +00:00
with mock . patch ( " tuf.repository_tool.logger " ) as mock_logger :
repository . dirty_roles ( )
# Concat strings to avoid Python2/3 unicode prefix problems ('' vs. u'')
mock_logger . info . assert_called_with ( " Dirty roles: " +
str ( [ ' root ' , ' snapshot ' , ' targets ' , ' timestamp ' ] ) )
2018-08-20 17:44:10 +00:00
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
2020-04-07 11:08:16 +00:00
# TODO: replace the hard-coded list of targets with a helper
# method that returns a list of normalized relative target paths
2020-04-03 13:50:22 +00:00
list_of_targets = [ ' file1.txt ' , ' file2.txt ' , ' 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 )
2020-04-07 11:08:16 +00:00
target4_filepath = ' myproject/file4.txt '
target4_abspath = os . path . abspath ( os . path . join (
' repository ' , ' targets ' , target4_filepath ) )
octal_file_permissions = oct ( os . stat ( target4_abspath ) . st_mode ) [ 4 : ]
2018-08-20 17:44:10 +00:00
custom_file_permissions = { ' file_permissions ' : octal_file_permissions }
2020-04-07 11:08:16 +00:00
repository . targets . add_target ( target4_filepath , custom_file_permissions )
2018-08-21 18:50:25 +00:00
# Note that target filepaths specified in the repo use '/' even on Windows.
# (This is important to make metadata platform-independent.)
self . assertTrue (
2020-04-07 11:08:16 +00:00
os . path . join ( target4_filepath ) 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 )
2019-12-11 16:10:48 +00:00
# NOTE: The tutorial does not call dirty_roles anymore due to #964 and
# #958. We still call it here to see if roles are dirty as expected.
2019-11-20 13:31:48 +00:00
with mock . patch ( " tuf.repository_tool.logger " ) as mock_logger :
repository . dirty_roles ( )
# Concat strings to avoid Python2/3 unicode prefix problems ('' vs. u'')
mock_logger . info . assert_called_with (
" Dirty roles: " + str ( [ ' snapshot ' , ' targets ' , ' timestamp ' ] ) )
2018-08-20 17:44:10 +00:00
repository . writeall ( )
2019-11-20 13:31:48 +00:00
repository . targets . remove_target ( ' myproject/file4.txt ' )
2018-08-20 22:14:35 +00:00
self . assertTrue ( os . path . exists ( os . path . join (
2019-11-20 13:31:48 +00:00
' repository ' , ' targets ' , ' myproject ' , ' file4.txt ' ) ) )
2019-12-11 16:10:48 +00:00
# NOTE: The tutorial does not call dirty_roles anymore due to #964 and
# #958. We still call it here to see if roles are dirty as expected.
2019-11-20 13:31:48 +00:00
with mock . patch ( " tuf.repository_tool.logger " ) as mock_logger :
repository . dirty_roles ( )
# Concat strings to avoid Python2/3 unicode prefix problems ('' vs. u'')
mock_logger . info . assert_called_with (
" Dirty roles: " + str ( [ ' targets ' ] ) )
2018-08-20 17:44:10 +00:00
2019-11-20 13:31:48 +00:00
repository . mark_dirty ( [ ' snapshot ' , ' timestamp ' ] )
2018-08-20 17:44:10 +00:00
repository . writeall ( )
2019-11-20 13:31:48 +00:00
# ----- Tutorial Section: Excursion: Dump Metadata and Append Signature
2018-08-20 22:20:09 +00:00
signable_content = dump_signable_metadata (
2019-11-20 13:31:48 +00:00
os . path . join ( ' repository ' , ' metadata.staged ' , ' timestamp.json ' ) )
# Skipping user entry of password
## private_ed25519_key = import_ed25519_privatekey_from_file('ed25519_key')
private_ed25519_key = import_ed25519_privatekey_from_file ( ' ed25519_key ' , ' password ' )
signature = create_signature (
private_ed25519_key , encode_canonical ( signable_content ) . encode ( ) )
2018-08-20 22:20:09 +00:00
append_signature (
2019-11-20 13:31:48 +00:00
signature ,
os . path . join ( ' repository ' , ' metadata.staged ' , ' timestamp.json ' ) )
2018-08-20 17:44:10 +00:00
# ----- Tutorial Section: Delegations
generate_and_write_rsa_keypair (
2020-10-28 11:25:13 +00:00
password = ' password ' , filepath = ' unclaimed_key ' , bits = 2048 )
2018-08-20 21:56:10 +00:00
public_unclaimed_key = import_rsa_publickey_from_file ( ' unclaimed_key.pub ' )
2018-08-20 17:44:10 +00:00
repository . targets . delegate (
2019-11-20 13:31:48 +00:00
' unclaimed ' , [ public_unclaimed_key ] , [ ' myproject/*.txt ' ] )
repository . targets ( " unclaimed " ) . add_target ( " myproject/file4.txt " )
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 )
2019-12-11 16:10:48 +00:00
# NOTE: The tutorial does not call dirty_roles anymore due to #964 and
# #958. We still call it here to see if roles are dirty as expected.
2019-11-20 13:31:48 +00:00
with mock . patch ( " tuf.repository_tool.logger " ) as mock_logger :
repository . dirty_roles ( )
# Concat strings to avoid Python2/3 unicode prefix problems ('' vs. u'')
mock_logger . info . assert_called_with (
" Dirty roles: " + str ( [ ' targets ' , ' unclaimed ' ] ) )
2018-08-20 17:44:10 +00:00
2019-11-20 13:31:48 +00:00
repository . mark_dirty ( [ " snapshot " , " timestamp " ] )
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: Delegate to Hashed Bins
2019-12-11 16:10:48 +00:00
repository . targets ( ' unclaimed ' ) . remove_target ( " myproject/file4.txt " )
2018-08-20 17:44:10 +00:00
2020-04-03 13:50:22 +00:00
targets = [ ' myproject/file4.txt ' ]
2018-08-20 17:44:10 +00:00
2019-11-30 11:41:18 +00:00
# Patch logger to assert that it accurately logs the output of hashed bin
# delegation. The logger is called multiple times, first with info level
# then with warning level. So we have to assert for the accurate sequence
# of calls or rather its call arguments.
with mock . patch ( " tuf.repository_tool.logger " ) as mock_logger :
repository . targets ( ' unclaimed ' ) . delegate_hashed_bins (
targets , [ public_unclaimed_key ] , 32 )
self . assertListEqual ( [
2020-03-26 22:45:20 +00:00
" Creating hashed bin delegations. \n "
" 1 total targets. \n "
" 32 hashed bins. \n "
" 256 total hash prefixes. \n "
2019-11-30 11:41:18 +00:00
" Each bin ranges over 8 hash prefixes. "
] + [ " Adding a verification key that has already been used. " ] * 32 ,
[
args [ 0 ] for args , _ in
mock_logger . info . call_args_list + mock_logger . warning . call_args_list
] )
2018-08-20 17:44:10 +00:00
for delegation in repository . targets ( ' unclaimed ' ) . delegations :
delegation . load_signing_key ( private_unclaimed_key )
2019-12-11 16:10:48 +00:00
# NOTE: The tutorial does not call dirty_roles anymore due to #964 and
# #958. We still call it here to see if roles are dirty as expected.
2019-11-20 13:31:48 +00:00
with mock . patch ( " tuf.repository_tool.logger " ) as mock_logger :
repository . dirty_roles ( )
# Concat strings to avoid Python2/3 unicode prefix problems ('' vs. u'')
mock_logger . info . assert_called_with (
" Dirty roles: " + str ( [ ' 00-07 ' , ' 08-0f ' , ' 10-17 ' , ' 18-1f ' , ' 20-27 ' ,
' 28-2f ' , ' 30-37 ' , ' 38-3f ' , ' 40-47 ' , ' 48-4f ' , ' 50-57 ' , ' 58-5f ' ,
' 60-67 ' , ' 68-6f ' , ' 70-77 ' , ' 78-7f ' , ' 80-87 ' , ' 88-8f ' , ' 90-97 ' ,
' 98-9f ' , ' a0-a7 ' , ' a8-af ' , ' b0-b7 ' , ' b8-bf ' , ' c0-c7 ' , ' c8-cf ' ,
' d0-d7 ' , ' d8-df ' , ' e0-e7 ' , ' e8-ef ' , ' f0-f7 ' , ' f8-ff ' , ' unclaimed ' ] ) )
repository . mark_dirty ( [ " snapshot " , " timestamp " ] )
repository . writeall ( )
2018-08-20 17:44:10 +00:00
# ----- Tutorial Section: How to Perform an Update
# A separate tutorial is linked to for client use. That is not tested here.
2019-11-20 13:31:48 +00:00
create_tuf_client_directory ( " repository/ " , " client/tufrepo/ " )
2018-08-20 17:44:10 +00:00
# ----- Tutorial Section: Test TUF Locally
# TODO: Run subprocess to simulate the following bash instructions:
# $ 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
# Run unit test.
if __name__ == ' __main__ ' :
2020-09-15 15:05:51 +00:00
utils . configure_test_logging ( sys . argv )
2018-08-20 17:44:10 +00:00
unittest . main ( )