Skip to content

Commit 5d40ffa

Browse files
authored
Merge pull request #1034 from joshuagl/joshuagl/abstract-files-fixes
Fix and better test abstract files and directories support
2 parents 95d08cc + 5e5c598 commit 5d40ffa

File tree

3 files changed

+209
-23
lines changed

3 files changed

+209
-23
lines changed

tests/test_repository_lib.py

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -512,6 +512,7 @@ def test_generate_timestamp_metadata(self):
512512
version = 1
513513
expiration_date = '1985-10-21T13:20:00Z'
514514

515+
storage_backend = securesystemslib.storage.FilesystemBackend()
515516
# Load a valid repository so that top-level roles exist in roledb and
516517
# generate_snapshot_metadata() has roles to specify in snapshot metadata.
517518
repository = repo_tool.Repository(repository_directory, metadata_directory,
@@ -521,20 +522,20 @@ def test_generate_timestamp_metadata(self):
521522
repository_name)
522523

523524
timestamp_metadata = repo_lib.generate_timestamp_metadata(snapshot_filename,
524-
version, expiration_date, repository_name)
525+
version, expiration_date, storage_backend, repository_name)
525526
self.assertTrue(tuf.formats.TIMESTAMP_SCHEMA.matches(timestamp_metadata))
526527

527528

528529
# Test improperly formatted arguments.
529530
self.assertRaises(securesystemslib.exceptions.FormatError,
530531
repo_lib.generate_timestamp_metadata, 3, version, expiration_date,
531-
repository_name)
532+
storage_backend, repository_name)
532533
self.assertRaises(securesystemslib.exceptions.FormatError,
533534
repo_lib.generate_timestamp_metadata, snapshot_filename, '3',
534-
expiration_date, repository_name)
535+
expiration_date, storage_backend, repository_name)
535536
self.assertRaises(securesystemslib.exceptions.FormatError,
536537
repo_lib.generate_timestamp_metadata, snapshot_filename, version, '3',
537-
repository_name)
538+
storage_backend, repository_name)
538539

539540

540541

tests/test_repository_tool.py

Lines changed: 181 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -116,6 +116,22 @@ def test_init(self):
116116

117117

118118

119+
def create_repository_directory(self):
120+
# Create a repository directory and copy in test targets data
121+
temporary_directory = tempfile.mkdtemp(dir=self.temporary_directory)
122+
targets_directory = os.path.join(temporary_directory, 'repository',
123+
repo_tool.TARGETS_DIRECTORY_NAME)
124+
original_targets_directory = os.path.join('repository_data',
125+
'repository', 'targets')
126+
shutil.copytree(original_targets_directory, targets_directory)
127+
128+
# In this case, create_new_repository() creates the 'repository/'
129+
# sub-directory in 'temporary_directory' if it does not exist.
130+
return os.path.join(temporary_directory, 'repository')
131+
132+
133+
134+
119135
def test_writeall(self):
120136
# Test creation of a TUF repository.
121137
#
@@ -129,16 +145,7 @@ def test_writeall(self):
129145
# Copy the target files from 'tuf/tests/repository_data' so that writeall()
130146
# has target fileinfo to include in metadata.
131147
repository_name = 'test_repository'
132-
temporary_directory = tempfile.mkdtemp(dir=self.temporary_directory)
133-
targets_directory = os.path.join(temporary_directory, 'repository',
134-
repo_tool.TARGETS_DIRECTORY_NAME)
135-
original_targets_directory = os.path.join('repository_data',
136-
'repository', 'targets')
137-
shutil.copytree(original_targets_directory, targets_directory)
138-
139-
# In this case, create_new_repository() creates the 'repository/'
140-
# sub-directory in 'temporary_directory' if it does not exist.
141-
repository_directory = os.path.join(temporary_directory, 'repository')
148+
repository_directory = self.create_repository_directory()
142149
metadata_directory = os.path.join(repository_directory,
143150
repo_tool.METADATA_STAGED_DIRECTORY_NAME)
144151

@@ -550,6 +557,170 @@ def test_get_filepaths_in_directory(self):
550557

551558

552559

560+
def test_writeall_abstract_storage(self):
561+
# Test creation of a TUF repository with a custom storage backend to ensure
562+
# that functions relying on a storage backend being supplied operate
563+
# correctly
564+
565+
566+
class TestStorageBackend(securesystemslib.storage.StorageBackendInterface):
567+
"""
568+
An implementation of securesystemslib.storage.StorageBackendInterface
569+
which mutates filenames on put()/get(), translating filename in memory
570+
to filename + '.tst' on-disk, such that trying to read the
571+
expected/canonical file paths from local storage doesn't find the TUF
572+
metadata files.
573+
"""
574+
575+
from contextlib import contextmanager
576+
577+
578+
@contextmanager
579+
def get(self, filepath):
580+
file_object = open(filepath + '.tst', 'rb')
581+
yield file_object
582+
file_object.close()
583+
584+
585+
def put(self, fileobj, filepath):
586+
if not fileobj.closed:
587+
fileobj.seek(0)
588+
589+
with open(filepath + '.tst', 'wb') as destination_file:
590+
shutil.copyfileobj(fileobj, destination_file)
591+
destination_file.flush()
592+
os.fsync(destination_file.fileno())
593+
594+
595+
def remove(self, filepath):
596+
os.remove(filepath + '.tst')
597+
598+
599+
def getsize(self, filepath):
600+
return os.path.getsize(filepath + '.tst')
601+
602+
603+
def create_folder(self, filepath):
604+
if not filepath:
605+
return
606+
try:
607+
os.makedirs(filepath)
608+
except OSError as err:
609+
pass
610+
611+
612+
def list_folder(self, filepath):
613+
contents = []
614+
files = os.listdir(filepath)
615+
616+
for fi in files:
617+
if fi.endswith('.tst'):
618+
contents.append(fi.split('.tst')[0])
619+
else:
620+
contents.append(fi)
621+
622+
return contents
623+
624+
625+
626+
# Set up the repository directory
627+
repository_name = 'test_repository'
628+
repository_directory = self.create_repository_directory()
629+
metadata_directory = os.path.join(repository_directory,
630+
repo_tool.METADATA_STAGED_DIRECTORY_NAME)
631+
targets_directory = os.path.join(repository_directory,
632+
repo_tool.TARGETS_DIRECTORY_NAME)
633+
634+
# TestStorageBackend expects all files on disk to have an additional '.tst'
635+
# file extension
636+
for target in os.listdir(targets_directory):
637+
src = os.path.join(targets_directory, target)
638+
dst = os.path.join(targets_directory, target + '.tst')
639+
os.rename(src, dst)
640+
641+
# (0) Create a repository with TestStorageBackend()
642+
storage_backend = TestStorageBackend()
643+
repository = repo_tool.create_new_repository(repository_directory,
644+
repository_name,
645+
storage_backend)
646+
647+
# (1) Load the public and private keys of the top-level roles, and one
648+
# delegated role.
649+
keystore_directory = os.path.join('repository_data', 'keystore')
650+
651+
# Load the public keys.
652+
root_pubkey_path = os.path.join(keystore_directory, 'root_key.pub')
653+
targets_pubkey_path = os.path.join(keystore_directory, 'targets_key.pub')
654+
snapshot_pubkey_path = os.path.join(keystore_directory, 'snapshot_key.pub')
655+
timestamp_pubkey_path = os.path.join(keystore_directory, 'timestamp_key.pub')
656+
657+
root_pubkey = repo_tool.import_rsa_publickey_from_file(root_pubkey_path)
658+
targets_pubkey = \
659+
repo_tool.import_ed25519_publickey_from_file(targets_pubkey_path)
660+
snapshot_pubkey = \
661+
repo_tool.import_ed25519_publickey_from_file(snapshot_pubkey_path)
662+
timestamp_pubkey = \
663+
repo_tool.import_ed25519_publickey_from_file(timestamp_pubkey_path)
664+
665+
# Load the private keys.
666+
root_privkey_path = os.path.join(keystore_directory, 'root_key')
667+
targets_privkey_path = os.path.join(keystore_directory, 'targets_key')
668+
snapshot_privkey_path = os.path.join(keystore_directory, 'snapshot_key')
669+
timestamp_privkey_path = os.path.join(keystore_directory, 'timestamp_key')
670+
671+
root_privkey = \
672+
repo_tool.import_rsa_privatekey_from_file(root_privkey_path, 'password')
673+
targets_privkey = \
674+
repo_tool.import_ed25519_privatekey_from_file(targets_privkey_path,
675+
'password')
676+
snapshot_privkey = \
677+
repo_tool.import_ed25519_privatekey_from_file(snapshot_privkey_path,
678+
'password')
679+
timestamp_privkey = \
680+
repo_tool.import_ed25519_privatekey_from_file(timestamp_privkey_path,
681+
'password')
682+
683+
684+
# (2) Add top-level verification keys.
685+
repository.root.add_verification_key(root_pubkey)
686+
repository.targets.add_verification_key(targets_pubkey)
687+
repository.snapshot.add_verification_key(snapshot_pubkey)
688+
repository.timestamp.add_verification_key(timestamp_pubkey)
689+
690+
691+
# (3) Load top-level signing keys.
692+
repository.root.load_signing_key(root_privkey)
693+
repository.targets.load_signing_key(targets_privkey)
694+
repository.snapshot.load_signing_key(snapshot_privkey)
695+
repository.timestamp.load_signing_key(timestamp_privkey)
696+
697+
698+
# (4) Add target files.
699+
target1 = 'file1.txt'
700+
target2 = 'file2.txt'
701+
target3 = 'file3.txt'
702+
repository.targets.add_target(target1)
703+
repository.targets.add_target(target2)
704+
repository.targets.add_target(target3)
705+
706+
# (6) Write repository.
707+
repository.writeall()
708+
709+
710+
# Ensure all of the metadata files exist at the mutated file location and
711+
# that those files are valid metadata
712+
for role in ['root.json.tst', 'targets.json.tst', 'snapshot.json.tst',
713+
'timestamp.json.tst']:
714+
role_filepath = os.path.join(metadata_directory, role)
715+
self.assertTrue(os.path.exists(role_filepath))
716+
717+
role_signable = securesystemslib.util.load_json_file(role_filepath)
718+
# Raise 'securesystemslib.exceptions.FormatError' if 'role_signable' is
719+
# an invalid signable.
720+
tuf.formats.check_signable_object_format(role_signable)
721+
722+
723+
553724

554725

555726
class TestMetadata(unittest.TestCase):

tuf/repository_lib.py

Lines changed: 23 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -135,7 +135,7 @@ def _generate_and_write_metadata(rolename, metadata_filename,
135135
elif rolename == 'timestamp':
136136
snapshot_filename = filenames['snapshot']
137137
metadata = generate_timestamp_metadata(snapshot_filename, roleinfo['version'],
138-
roleinfo['expires'], repository_name)
138+
roleinfo['expires'], storage_backend, repository_name)
139139

140140
_log_warning_if_expires_soon(TIMESTAMP_FILENAME, roleinfo['expires'],
141141
TIMESTAMP_EXPIRES_WARN_SECONDS)
@@ -1173,8 +1173,14 @@ def generate_targets_metadata(targets_directory, target_files, version,
11731173
11741174
target_files:
11751175
The target files tracked by 'targets.json'. 'target_files' is a
1176-
dictionary of target paths that are relative to the targets directory and
1177-
a fileinfo dict matching tuf.formats.LOOSE_FILEINFO_SCHEMA
1176+
dictionary mapping target paths (relative to the targets directory) to
1177+
a dict matching tuf.formats.LOOSE_FILEINFO_SCHEMA. LOOSE_FILEINFO_SCHEMA
1178+
can support multiple different value patterns:
1179+
1) an empty dictionary - for when fileinfo should be generated
1180+
2) a dictionary matching tuf.formats.CUSTOM_SCHEMA - for when fileinfo
1181+
should be generated, with the supplied custom metadata attached
1182+
3) a dictionary matching tuf.formats.FILEINFO_SCHEMA - for when full
1183+
fileinfo is provided in conjunction with use_existing_fileinfo
11781184
11791185
version:
11801186
The metadata version number. Clients use the version number to
@@ -1192,6 +1198,9 @@ def generate_targets_metadata(targets_directory, target_files, version,
11921198
write_consistent_targets:
11931199
Boolean that indicates whether file digests should be prepended to the
11941200
target files.
1201+
NOTE: it is an error for write_consistent_targets to be True when
1202+
use_existing_fileinfo is also True. We can not create consistent targets
1203+
for a target file where the fileinfo isn't generated by tuf.
11951204
11961205
use_existing_fileinfo:
11971206
Boolean that indicates whether to use the complete fileinfo, including
@@ -1253,20 +1262,25 @@ def generate_targets_metadata(targets_directory, target_files, version,
12531262
filedict = {}
12541263

12551264
if use_existing_fileinfo:
1265+
# Use the provided fileinfo dicts, conforming to FILEINFO_SCHEMA, rather than
1266+
# generating fileinfo
12561267
for target, fileinfo in six.iteritems(target_files):
12571268

12581269
# Ensure all fileinfo entries in target_files have a non-empty hashes dict
12591270
if not fileinfo.get('hashes', None):
12601271
raise securesystemslib.exceptions.Error('use_existing_hashes option set'
12611272
' but no hashes exist in roledb for ' + repr(target))
12621273

1274+
# and a non-empty length
12631275
if fileinfo.get('length', -1) < 0:
12641276
raise securesystemslib.exceptions.Error('use_existing_hashes option set'
12651277
' but fileinfo\'s length is not set')
12661278

12671279
filedict[target] = fileinfo
12681280

12691281
else:
1282+
# Generate the fileinfo dicts by accessing the target files on storage.
1283+
# Default to accessing files on local storage.
12701284
if storage_backend is None:
12711285
storage_backend = securesystemslib.storage.FilesystemBackend()
12721286

@@ -1472,7 +1486,7 @@ def generate_snapshot_metadata(metadata_directory, version, expiration_date,
14721486

14731487

14741488
def generate_timestamp_metadata(snapshot_filename, version, expiration_date,
1475-
repository_name):
1489+
storage_backend, repository_name):
14761490
"""
14771491
<Purpose>
14781492
Generate the timestamp metadata object. The 'snapshot.json' file must
@@ -1492,14 +1506,14 @@ def generate_timestamp_metadata(snapshot_filename, version, expiration_date,
14921506
The expiration date of the metadata file, conformant to
14931507
'securesystemslib.formats.ISO8601_DATETIME_SCHEMA'.
14941508
1495-
repository_name:
1496-
The name of the repository. If not supplied, 'rolename' is added to the
1497-
'default' repository.
1498-
14991509
storage_backend:
15001510
An object which implements
15011511
securesystemslib.storage.StorageBackendInterface.
15021512
1513+
repository_name:
1514+
The name of the repository. If not supplied, 'rolename' is added to the
1515+
'default' repository.
1516+
15031517
<Exceptions>
15041518
securesystemslib.exceptions.FormatError, if the generated timestamp metadata
15051519
object cannot be formatted correctly, or one of the arguments is improperly
@@ -1524,7 +1538,7 @@ def generate_timestamp_metadata(snapshot_filename, version, expiration_date,
15241538
# Retrieve the versioninfo of the Snapshot metadata file.
15251539
snapshot_fileinfo = {}
15261540
length, hashes = securesystemslib.util.get_file_details(snapshot_filename,
1527-
tuf.settings.FILE_HASH_ALGORITHMS)
1541+
tuf.settings.FILE_HASH_ALGORITHMS, storage_backend)
15281542
snapshot_version = get_metadata_versioninfo('snapshot', repository_name)
15291543
snapshot_fileinfo[SNAPSHOT_FILENAME] = \
15301544
tuf.formats.make_fileinfo(length, hashes, version=snapshot_version['version'])

0 commit comments

Comments
 (0)