mirror of
https://github.com/theupdateframework/python-tuf
synced 2026-05-24 10:08:28 +00:00
Merge branch 'SantiagoTorres-developer-tools' of https://github.com/vladimir-v-diaz/tuf into develop
This commit is contained in:
commit
59b45fdfe5
17 changed files with 1929 additions and 0 deletions
107
tests/repository_data/generate_project_data.py
Executable file
107
tests/repository_data/generate_project_data.py
Executable file
|
|
@ -0,0 +1,107 @@
|
|||
#!/usr/bin/env python
|
||||
|
||||
"""
|
||||
<Program Name>
|
||||
generate_project_data.py
|
||||
|
||||
<Author>
|
||||
Santiago Torres <torresariass@gmail.com>
|
||||
|
||||
<Copyright>
|
||||
See LICENSE for licensing information.
|
||||
|
||||
<Purpose>
|
||||
Generate a pre-fabricated set of metadata files for 'test_developer_tool.py'
|
||||
test cases.
|
||||
"""
|
||||
|
||||
import shutil
|
||||
import datetime
|
||||
import optparse
|
||||
import os
|
||||
|
||||
import tuf.util
|
||||
from tuf.developer_tool import *
|
||||
|
||||
parser = optparse.OptionParser()
|
||||
|
||||
parser.add_option("-d","--dry-run", action='store_true', dest="dry_run",
|
||||
help="Do not write the files, just run", default=False)
|
||||
(options, args) = parser.parse_args()
|
||||
|
||||
|
||||
project_key_file = 'keystore/root_key'
|
||||
targets_key_file = 'keystore/targets_key'
|
||||
delegation_key_file = 'keystore/delegation_key'
|
||||
|
||||
# The files we use for signing in the unit tests should exist, if they are not
|
||||
# populated, run 'generate.py'.
|
||||
assert os.path.exists(project_key_file)
|
||||
assert os.path.exists(targets_key_file)
|
||||
assert os.path.exists(delegation_key_file)
|
||||
|
||||
# Import the public keys. These keys are needed so that metadata roles are
|
||||
# assigned verification keys, which clients use to verify the signatures created
|
||||
# by the corresponding private keys.
|
||||
project_public = import_rsa_publickey_from_file(project_key_file + '.pub')
|
||||
targets_public = import_rsa_publickey_from_file(targets_key_file + '.pub')
|
||||
delegation_public = import_rsa_publickey_from_file(delegation_key_file + '.pub')
|
||||
|
||||
# Import the private keys. These private keys are needed to generate the
|
||||
# signatures included in metadata.
|
||||
project_private = import_rsa_privatekey_from_file(project_key_file, 'password')
|
||||
targets_private = import_rsa_privatekey_from_file(targets_key_file, 'password')
|
||||
delegation_private = import_rsa_privatekey_from_file(delegation_key_file, 'password')
|
||||
|
||||
os.mkdir("project")
|
||||
os.mkdir("project/targets")
|
||||
|
||||
# Create the target files (downloaded by clients) whose file size and digest
|
||||
# are specified in the 'targets.json' file.
|
||||
target1_filepath = 'project/targets/file1.txt'
|
||||
tuf.util.ensure_parent_dir(target1_filepath)
|
||||
target2_filepath = 'project/targets/file2.txt'
|
||||
tuf.util.ensure_parent_dir(target2_filepath)
|
||||
target3_filepath = 'project/targets/file3.txt'
|
||||
tuf.util.ensure_parent_dir(target2_filepath)
|
||||
|
||||
if not options.dry_run:
|
||||
with open(target1_filepath, 'wt') as file_object:
|
||||
file_object.write('This is an example target file.')
|
||||
|
||||
with open(target2_filepath, 'wt') as file_object:
|
||||
file_object.write('This is an another example target file.')
|
||||
|
||||
with open(target3_filepath, 'wt') as file_object:
|
||||
file_object.write('This is role1\'s target file.')
|
||||
|
||||
|
||||
project = create_new_project("test-flat", 'project/test-flat', 'prefix',
|
||||
'project/targets')
|
||||
|
||||
# Add target files to the top-level projects role. These target files should
|
||||
# already exist.
|
||||
project.add_target(target1_filepath)
|
||||
project.add_target(target2_filepath)
|
||||
|
||||
# Add one key to the project.
|
||||
project.add_verification_key(project_public)
|
||||
project.load_signing_key(project_private)
|
||||
|
||||
# Add the delegated role keys.
|
||||
project.delegate('role1', [delegation_public], [target3_filepath])
|
||||
project('role1').load_signing_key(delegation_private)
|
||||
|
||||
# Set the project expiration time far into the future so that its metadata does
|
||||
# not expire anytime soon, or else the tests fail. Unit tests may modify the
|
||||
# expiration datetimes (of the copied files), if they wish.
|
||||
project.expiration = datetime.datetime(2030, 1, 1, 0, 0)
|
||||
project('role1').expiration = datetime.datetime(2030, 1, 1, 0, 0)
|
||||
|
||||
# Compress the project role metadata so that the unit tests have a pre-generated
|
||||
# example of compressed metadata.
|
||||
project.compressions = ['gz']
|
||||
|
||||
# Create the actual metadata files, which are saved to 'metadata.staged'.
|
||||
if not options.dry_run:
|
||||
project.write()
|
||||
1
tests/repository_data/project/targets/file1.txt
Normal file
1
tests/repository_data/project/targets/file1.txt
Normal file
|
|
@ -0,0 +1 @@
|
|||
This is an example target file.
|
||||
1
tests/repository_data/project/targets/file2.txt
Normal file
1
tests/repository_data/project/targets/file2.txt
Normal file
|
|
@ -0,0 +1 @@
|
|||
This is an another example target file.
|
||||
1
tests/repository_data/project/targets/file3.txt
Normal file
1
tests/repository_data/project/targets/file3.txt
Normal file
|
|
@ -0,0 +1 @@
|
|||
This is role1's target file.
|
||||
1
tests/repository_data/project/test-flat/project.cfg
Normal file
1
tests/repository_data/project/test-flat/project.cfg
Normal file
|
|
@ -0,0 +1 @@
|
|||
{"project_name": "test-flat", "targets_location": "/home/santiago/Documents/v2014/TUF/tuf/tests/repository_data/project/targets", "prefix": "prefix", "metadata_location": "test-flat", "threshold": 1, "public_keys": {"6986b667c736a3b37471e030cf4ce7aa6c7e0d530325e64c2660276b77be3754": {"keytype": "rsa", "keyval": {"public": "-----BEGIN PUBLIC KEY-----\nMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA7J15ZaeDQPrhQsRj29wB\nPhibH+Do59xsT2396L+uCg793gZlar5wZN2eHSh725cNQWyTAa9LwG+lXaKMukQ+\n8176CKR2J5sv3DezrGVu3x8V1qhyJyy79FlNZRVYTVqNaYzvJzxsVnFPpg7f8B7C\nffiqWJr9XkpqwRlCpxooXm4hplZ7uek5Ku21CzQ4OWg7hbuc+ZjCGzpXfm8NuosU\n7TipnKGpEt0Agiph5g6TB2/scoeFar1CKMONIl80maxzAQk+xkWgiJ00+Z2qFCsx\nESfis/YkILS6RMFyZz7oa1WwMtUjYmrsRuz+jlFcbNuxZpIkaISiG9a2YdGcJ1Aj\n3QIDAQAB\n-----END PUBLIC KEY-----"}}}, "layout_type": "flat"}
|
||||
BIN
tests/repository_data/project/test-flat/test-flat.json
Normal file
BIN
tests/repository_data/project/test-flat/test-flat.json
Normal file
Binary file not shown.
BIN
tests/repository_data/project/test-flat/test-flat/role1.json
Normal file
BIN
tests/repository_data/project/test-flat/test-flat/role1.json
Normal file
Binary file not shown.
Binary file not shown.
Binary file not shown.
1
tests/repository_data/project/test-repo/project.cfg
Normal file
1
tests/repository_data/project/test-repo/project.cfg
Normal file
|
|
@ -0,0 +1 @@
|
|||
{"project_name": "test-repo-like", "targets_location": "targets", "prefix": "prefix", "metadata_location": "metadata", "threshold": 1, "public_keys": {"6986b667c736a3b37471e030cf4ce7aa6c7e0d530325e64c2660276b77be3754": {"keytype": "rsa", "keyval": {"public": "-----BEGIN PUBLIC KEY-----\nMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA7J15ZaeDQPrhQsRj29wB\nPhibH+Do59xsT2396L+uCg793gZlar5wZN2eHSh725cNQWyTAa9LwG+lXaKMukQ+\n8176CKR2J5sv3DezrGVu3x8V1qhyJyy79FlNZRVYTVqNaYzvJzxsVnFPpg7f8B7C\nffiqWJr9XkpqwRlCpxooXm4hplZ7uek5Ku21CzQ4OWg7hbuc+ZjCGzpXfm8NuosU\n7TipnKGpEt0Agiph5g6TB2/scoeFar1CKMONIl80maxzAQk+xkWgiJ00+Z2qFCsx\nESfis/YkILS6RMFyZz7oa1WwMtUjYmrsRuz+jlFcbNuxZpIkaISiG9a2YdGcJ1Aj\n3QIDAQAB\n-----END PUBLIC KEY-----"}}}, "layout_type": "repo-like"}
|
||||
|
|
@ -0,0 +1 @@
|
|||
This is an example target file.
|
||||
|
|
@ -0,0 +1 @@
|
|||
This is an another example target file.
|
||||
|
|
@ -0,0 +1 @@
|
|||
This is role1's target file.
|
||||
442
tests/test_developer_tool.py
Executable file
442
tests/test_developer_tool.py
Executable file
|
|
@ -0,0 +1,442 @@
|
|||
#!/usr/bin/env python
|
||||
|
||||
"""
|
||||
<Program Name>
|
||||
test_developer_tool.py.
|
||||
|
||||
<Authors>
|
||||
Santiago Torres Arias <torresariass@gmail.com
|
||||
Zane Fisher <zanefisher@gmail.com>
|
||||
|
||||
<Copyright>
|
||||
See LICENSE for licensing inforation.
|
||||
|
||||
<Purpose>
|
||||
Unit test for the 'developer_tool.py' module.
|
||||
"""
|
||||
|
||||
import os
|
||||
import time
|
||||
import datetime
|
||||
import unittest
|
||||
import logging
|
||||
import tempfile
|
||||
import shutil
|
||||
|
||||
import tuf
|
||||
import tuf.log
|
||||
import tuf.formats
|
||||
import tuf.roledb
|
||||
import tuf.keydb
|
||||
import tuf.developer_tool as developer_tool
|
||||
|
||||
from tuf.developer_tool import METADATA_DIRECTORY_NAME
|
||||
from tuf.developer_tool import TARGETS_DIRECTORY_NAME
|
||||
|
||||
logger = logging.getLogger("tuf.test_developer_tool")
|
||||
|
||||
developer_tool.disable_console_log_messages()
|
||||
|
||||
class TestProject(unittest.TestCase):
|
||||
|
||||
tmp_dir = None
|
||||
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
cls.tmp_dir = tempfile.mkdtemp(dir = os.getcwd())
|
||||
|
||||
@classmethod
|
||||
def tearDownClass(cls):
|
||||
shutil.rmtree(cls.tmp_dir)
|
||||
|
||||
def setUp(self):
|
||||
# called before every test case
|
||||
pass
|
||||
|
||||
def tearDown(self):
|
||||
# called after every test case
|
||||
tuf.roledb.clear_roledb()
|
||||
tuf.keydb.clear_keydb()
|
||||
pass
|
||||
|
||||
def test_create_new_project(self):
|
||||
# Test cases for the create_new_project function. In this test we will
|
||||
# check input, correct file creation and format. We also check
|
||||
# that a proper object is generated. We will use the normal layout for this
|
||||
# test suite.
|
||||
|
||||
# Create a local subfolder for this test.
|
||||
local_tmp = tempfile.mkdtemp(dir = self.tmp_dir)
|
||||
|
||||
# These are the usual values we will be throwing to the function, however
|
||||
# we will swap these for nulls or malformed values every now and then to
|
||||
# test input.
|
||||
project_name = "test_suite"
|
||||
metadata_directory = local_tmp
|
||||
location_in_repository = '/prefix'
|
||||
targets_directory = None
|
||||
key = None
|
||||
|
||||
# Create a blank project.
|
||||
project = developer_tool.create_new_project(project_name, metadata_directory,
|
||||
location_in_repository)
|
||||
|
||||
self.assertTrue(isinstance(project, developer_tool.Project))
|
||||
self.assertTrue(project.layout_type == 'repo-like')
|
||||
self.assertTrue(project._prefix == location_in_repository)
|
||||
self.assertTrue(project._project_name == project_name)
|
||||
self.assertTrue(project._metadata_directory ==
|
||||
os.path.join(metadata_directory,METADATA_DIRECTORY_NAME))
|
||||
self.assertTrue(project._targets_directory ==
|
||||
os.path.join(metadata_directory,TARGETS_DIRECTORY_NAME))
|
||||
|
||||
# Create a blank project without a prefix.
|
||||
project = developer_tool.create_new_project(project_name, metadata_directory)
|
||||
self.assertTrue(isinstance(project, developer_tool.Project))
|
||||
self.assertTrue(project.layout_type == 'repo-like')
|
||||
self.assertTrue(project._prefix == '')
|
||||
self.assertTrue(project._project_name == project_name)
|
||||
self.assertTrue(project._metadata_directory ==
|
||||
os.path.join(metadata_directory,METADATA_DIRECTORY_NAME))
|
||||
self.assertTrue(project._targets_directory ==
|
||||
os.path.join(metadata_directory,TARGETS_DIRECTORY_NAME))
|
||||
|
||||
# Create a blank project without a valid metadata directory.
|
||||
self.assertRaises(tuf.FormatError, developer_tool.create_new_project,
|
||||
0, metadata_directory, location_in_repository)
|
||||
self.assertRaises(tuf.FormatError, developer_tool.create_new_project,
|
||||
project_name, 0, location_in_repository)
|
||||
self.assertRaises(tuf.FormatError, developer_tool.create_new_project,
|
||||
project_name, metadata_directory, 0)
|
||||
|
||||
|
||||
# Create a new project with a flat layout.
|
||||
targets_directory = tempfile.mkdtemp(dir = local_tmp)
|
||||
metadata_directory = tempfile.mkdtemp(dir = local_tmp)
|
||||
project = developer_tool.create_new_project(project_name, metadata_directory,
|
||||
location_in_repository, targets_directory)
|
||||
self.assertTrue(isinstance(project, developer_tool.Project))
|
||||
self.assertTrue(project.layout_type == 'flat')
|
||||
self.assertTrue(project._prefix == location_in_repository)
|
||||
self.assertTrue(project._project_name == project_name)
|
||||
self.assertTrue(project._metadata_directory == metadata_directory)
|
||||
self.assertTrue(project._targets_directory == targets_directory)
|
||||
|
||||
# Finally, check that if targets_directory is set, it is valid.
|
||||
self.assertRaises(tuf.FormatError, developer_tool.create_new_project,
|
||||
project_name, metadata_directory, location_in_repository, 0)
|
||||
|
||||
# Copy a key to our workspace and create a new project with it.
|
||||
keystore_path = os.path.join('repository_data','keystore')
|
||||
|
||||
# I will use the same key as the one provided in the repository
|
||||
# tool tests for the root role, but this is not a root role...
|
||||
root_key_path = os.path.join(keystore_path,'root_key.pub')
|
||||
project_key = developer_tool.import_rsa_publickey_from_file(root_key_path)
|
||||
|
||||
# Test create new project with a key added by default.
|
||||
project = developer_tool.create_new_project(project_name, metadata_directory,
|
||||
location_in_repository, targets_directory, project_key)
|
||||
|
||||
self.assertTrue(isinstance(project, developer_tool.Project))
|
||||
self.assertTrue(project.layout_type == 'flat')
|
||||
self.assertTrue(project._prefix == location_in_repository)
|
||||
self.assertTrue(project._project_name == project_name)
|
||||
self.assertTrue(project._metadata_directory == metadata_directory)
|
||||
self.assertTrue(project._targets_directory == targets_directory)
|
||||
self.assertTrue(len(project.keys) == 1)
|
||||
self.assertTrue(project.keys[0] == project_key['keyid'])
|
||||
|
||||
# Set as readonly and try to write a repo.
|
||||
shutil.rmtree(targets_directory)
|
||||
os.chmod(local_tmp, 0o0555)
|
||||
|
||||
tuf.roledb.clear_roledb()
|
||||
tuf.keydb.clear_keydb()
|
||||
self.assertRaises(OSError, developer_tool.create_new_project ,project_name,
|
||||
metadata_directory, location_in_repository, targets_directory,
|
||||
project_key)
|
||||
|
||||
os.chmod(local_tmp, 0o0777)
|
||||
|
||||
shutil.rmtree(metadata_directory)
|
||||
os.chmod(local_tmp, 0o0555)
|
||||
|
||||
tuf.roledb.clear_roledb()
|
||||
tuf.keydb.clear_keydb()
|
||||
self.assertRaises(OSError, developer_tool.create_new_project ,project_name,
|
||||
metadata_directory, location_in_repository, targets_directory,
|
||||
project_key)
|
||||
|
||||
|
||||
os.chmod(local_tmp, 0o0777)
|
||||
shutil.rmtree(local_tmp)
|
||||
|
||||
|
||||
|
||||
def test_load_project(self):
|
||||
# This test case will first try to load an existing project and test for
|
||||
# verify the loaded object. It will next try to load a nonexisting project
|
||||
# and expect a correct error handler. Finally, it will try to overwrite the
|
||||
# existing prefix of the loaded project.
|
||||
|
||||
# Create a local subfolder for this test.
|
||||
local_tmp = tempfile.mkdtemp(dir = self.tmp_dir)
|
||||
|
||||
# Test non-existent project filepath.
|
||||
nonexistent_path = os.path.join(local_tmp, "nonexistent")
|
||||
self.assertRaises(IOError, developer_tool.load_project, nonexistent_path)
|
||||
|
||||
# Copy the pregenerated metadata.
|
||||
project_data_filepath = os.path.join('repository_data', 'project')
|
||||
target_project_data_filepath = os.path.join(local_tmp, 'project')
|
||||
shutil.copytree("repository_data/project", target_project_data_filepath)
|
||||
|
||||
# Properly load a project.
|
||||
repo_filepath = os.path.join(local_tmp, 'project', 'test-repo')
|
||||
project = developer_tool.load_project(repo_filepath)
|
||||
|
||||
self.assertTrue(project.layout_type == "repo-like")
|
||||
|
||||
repo_filepath = os.path.join(local_tmp, 'project', 'test-flat')
|
||||
new_targets_path = os.path.join(local_tmp, 'project', 'targets')
|
||||
project = developer_tool.load_project(repo_filepath,
|
||||
new_targets_location = new_targets_path)
|
||||
self.assertTrue(project._targets_directory == new_targets_path)
|
||||
self.assertTrue(project.layout_type == 'flat')
|
||||
|
||||
|
||||
# Load a project overwriting the prefix.
|
||||
project = developer_tool.load_project(repo_filepath, prefix='new')
|
||||
self.assertTrue(project._prefix == 'new')
|
||||
|
||||
# Load a project with a file missing.
|
||||
file_to_corrupt = os.path.join(repo_filepath, 'test-flat', 'role1.json')
|
||||
with open(file_to_corrupt, 'wt') as fp:
|
||||
fp.write("this is not a json file")
|
||||
|
||||
self.assertRaises(tuf.Error, developer_tool.load_project, repo_filepath)
|
||||
|
||||
|
||||
|
||||
|
||||
def test_add_verification_keys(self):
|
||||
# create a new project instance
|
||||
project = developer_tool.Project("test_verification_keys", "somepath",
|
||||
"someotherpath", "prefix")
|
||||
|
||||
# add invalid verification key
|
||||
self.assertRaises(tuf.FormatError, project.add_verification_key, "invalid")
|
||||
|
||||
# add verification key
|
||||
# - load it first
|
||||
keystore_path = os.path.join('repository_data','keystore')
|
||||
first_verification_key_path = os.path.join(keystore_path,'root_key.pub')
|
||||
first_verification_key = \
|
||||
developer_tool.import_rsa_publickey_from_file(first_verification_key_path)
|
||||
|
||||
project.add_verification_key(first_verification_key)
|
||||
|
||||
|
||||
# add another verification key (should expect exception)
|
||||
second_verification_key_path = os.path.join(keystore_path,'snapshot_key.pub')
|
||||
second_verification_key = \
|
||||
developer_tool.import_rsa_publickey_from_file(second_verification_key_path)
|
||||
|
||||
self.assertRaises(tuf.Error,
|
||||
project.add_verification_key,(second_verification_key))
|
||||
|
||||
|
||||
|
||||
# add a verification key for the delegation
|
||||
project.delegate("somedelegation", [], [])
|
||||
project("somedelegation").add_verification_key(first_verification_key)
|
||||
project("somedelegation").add_verification_key(second_verification_key)
|
||||
|
||||
|
||||
# add another delegation of the delegation
|
||||
project("somedelegation").delegate("somesubdelegation", [], [])
|
||||
project("somedelegation")("somesubdelegation").add_verification_key(
|
||||
first_verification_key)
|
||||
project("somedelegation")("somesubdelegation").add_verification_key(
|
||||
second_verification_key)
|
||||
|
||||
|
||||
def test_write(self):
|
||||
|
||||
# create tmp directory
|
||||
local_tmp = tempfile.mkdtemp(dir=self.tmp_dir)
|
||||
|
||||
# create new project inside tmp directory
|
||||
project = developer_tool.create_new_project("test_write", local_tmp,
|
||||
"prefix");
|
||||
|
||||
# create some target files inside the tmp directory
|
||||
target_filepath = os.path.join(local_tmp, "targets", "test_target")
|
||||
with open(target_filepath, "wt") as fp:
|
||||
fp.write("testing file")
|
||||
|
||||
|
||||
# add the targets
|
||||
project.add_target(target_filepath)
|
||||
|
||||
# add verification keys
|
||||
keystore_path = os.path.join('repository_data','keystore')
|
||||
project_key_path = os.path.join(keystore_path,'root_key.pub')
|
||||
project_key = \
|
||||
developer_tool.import_rsa_publickey_from_file(project_key_path)
|
||||
|
||||
|
||||
# call status (for the sake of doing it)
|
||||
project.status()
|
||||
|
||||
project.add_verification_key(project_key)
|
||||
|
||||
|
||||
# add another verification key (should expect exception)
|
||||
delegation_key_path = os.path.join(keystore_path,'snapshot_key.pub')
|
||||
delegation_key = \
|
||||
developer_tool.import_rsa_publickey_from_file(delegation_key_path)
|
||||
|
||||
# add a subdelegation
|
||||
subdelegation_key_path = os.path.join(keystore_path,'timestamp_key.pub')
|
||||
subdelegation_key = \
|
||||
developer_tool.import_rsa_publickey_from_file(subdelegation_key_path)
|
||||
|
||||
# add a delegation
|
||||
project.delegate("delegation", [delegation_key], [])
|
||||
project("delegation").delegate("subdelegation", [subdelegation_key], [])
|
||||
|
||||
# call write (except)
|
||||
self.assertRaises(tuf.Error, project.write, ())
|
||||
|
||||
# call status (for the sake of doing it)
|
||||
project.status()
|
||||
|
||||
# load private keys
|
||||
project_private_key_path = os.path.join(keystore_path, 'root_key')
|
||||
project_private_key = \
|
||||
developer_tool.import_rsa_privatekey_from_file(project_private_key_path,
|
||||
'password')
|
||||
|
||||
delegation_private_key_path = os.path.join(keystore_path, 'snapshot_key')
|
||||
delegation_private_key = \
|
||||
developer_tool.import_rsa_privatekey_from_file(delegation_private_key_path,
|
||||
'password')
|
||||
|
||||
subdelegation_private_key_path = \
|
||||
os.path.join(keystore_path, 'timestamp_key')
|
||||
subdelegation_private_key = \
|
||||
developer_tool.import_rsa_privatekey_from_file(subdelegation_private_key_path,
|
||||
'password')
|
||||
|
||||
# Test partial write
|
||||
# backup everything (again)
|
||||
# + backup targets:
|
||||
targets_backup = project.target_files
|
||||
|
||||
# + backup delegations
|
||||
delegations_backup = \
|
||||
tuf.roledb.get_delegated_rolenames(project._project_name)
|
||||
|
||||
# + backup layout type
|
||||
layout_type_backup = project.layout_type
|
||||
|
||||
# + backup keyids
|
||||
keys_backup = project.keys
|
||||
delegation_keys_backup = project("delegation").keys
|
||||
|
||||
# + backup the prefix.
|
||||
prefix_backup = project._prefix
|
||||
|
||||
# + backup the name.
|
||||
name_backup = project._project_name
|
||||
|
||||
# Set the compressions, we will be checking this part here too
|
||||
project.compressions = ['gz']
|
||||
project('delegation').compressions = project.compressions
|
||||
|
||||
# Write and reload.
|
||||
self.assertRaises(tuf.Error, project.write)
|
||||
project.write(write_partial=True)
|
||||
|
||||
project = developer_tool.load_project(local_tmp)
|
||||
|
||||
# Check against backup.
|
||||
self.assertEqual(project.target_files, list(targets_backup.keys()))
|
||||
new_delegations = tuf.roledb.get_delegated_rolenames(project._project_name)
|
||||
self.assertEqual(new_delegations, delegations_backup)
|
||||
self.assertEqual(project.layout_type, layout_type_backup)
|
||||
self.assertEqual(project.keys, keys_backup)
|
||||
self.assertEqual(project("delegation").keys, delegation_keys_backup)
|
||||
self.assertEqual(project._prefix, prefix_backup)
|
||||
self.assertEqual(project._project_name, name_backup)
|
||||
|
||||
|
||||
|
||||
roleinfo = tuf.roledb.get_roleinfo(project._project_name)
|
||||
|
||||
self.assertEqual(roleinfo['partial_loaded'], True)
|
||||
|
||||
|
||||
|
||||
# Load_signing_keys.
|
||||
project("delegation").load_signing_key(delegation_private_key)
|
||||
|
||||
project.status()
|
||||
|
||||
project("delegation")("subdelegation").load_signing_key(
|
||||
subdelegation_private_key)
|
||||
|
||||
project.status()
|
||||
|
||||
project.load_signing_key(project_private_key)
|
||||
|
||||
# Backup everything.
|
||||
# + backup targets.
|
||||
targets_backup = project.target_files
|
||||
|
||||
# + backup delegations.
|
||||
delegations_backup = \
|
||||
tuf.roledb.get_delegated_rolenames(project._project_name)
|
||||
|
||||
# + backup layout type.
|
||||
layout_type_backup = project.layout_type
|
||||
|
||||
# + backup keyids
|
||||
keys_backup = project.keys
|
||||
delegation_keys_backup = project("delegation").keys
|
||||
|
||||
# + backup the prefix.
|
||||
prefix_backup = project._prefix
|
||||
|
||||
# + backup the name.
|
||||
name_backup = project._project_name
|
||||
|
||||
# Call status (for the sake of doing it.)
|
||||
project.status()
|
||||
|
||||
# Call write.
|
||||
project.write()
|
||||
|
||||
# Call load.
|
||||
project = developer_tool.load_project(local_tmp)
|
||||
|
||||
|
||||
# Check against backup.
|
||||
self.assertEqual(project.target_files, targets_backup)
|
||||
|
||||
new_delegations = tuf.roledb.get_delegated_rolenames(project._project_name)
|
||||
self.assertEqual(new_delegations, delegations_backup)
|
||||
self.assertEqual(project.layout_type, layout_type_backup)
|
||||
self.assertEqual(project.keys, keys_backup)
|
||||
self.assertEqual(project("delegation").keys, delegation_keys_backup)
|
||||
self.assertEqual(project._prefix, prefix_backup)
|
||||
self.assertEqual(project._project_name, name_backup)
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
unittest.main()
|
||||
326
tuf/README-developer-tools.md
Normal file
326
tuf/README-developer-tools.md
Normal file
|
|
@ -0,0 +1,326 @@
|
|||
# Developing for a TUF repository #
|
||||
|
||||
## Table of Contents ##
|
||||
- [Overview](#overview)
|
||||
- [Creating a Simple Project](#creating_a_simple_project)
|
||||
- [Generating a Key](#generating_a_key)
|
||||
- [The Project Class](#the_project_class)
|
||||
- [Signing and Writing the Metadata](#signing_and_writing_the_metadata)
|
||||
- [Loading an Existing Project](#loading_an_existing_project)
|
||||
- [Delegations](#delegations)
|
||||
- [Managing Keys](#managing_keys)
|
||||
- [Managing Targets](#managing_targets)
|
||||
|
||||
<a name="overview">
|
||||
## Overview
|
||||
The TUF developer tool is a Python library that enables developers to create
|
||||
and maintain the required metadata for files hosted in a TUF Repository. The
|
||||
main concern when generating metadata for a TUF repository is generating
|
||||
information that matches the future location of the files in the repository. We
|
||||
use the developer tools to generate valid information so that the project and
|
||||
its metadata can be applied to the TUF project transparently.
|
||||
|
||||
This document has two parts. The first part walks through the creation of a
|
||||
prototypal TUF project. The second part demonstrates the full capabilities of
|
||||
the TUF developer tool, which can be used to expand the project from the first
|
||||
part to meet the developer's needs.
|
||||
|
||||
<a name="creating_a_simple_project">
|
||||
## Creating a Simple project
|
||||
The following section describes a very basic example usage of the developer
|
||||
tools with a one-file project.
|
||||
|
||||
<a name="generating_a_key">
|
||||
### Generating a Key
|
||||
First, we will need to generate a key to sign the metadata. Keys are generated
|
||||
in pairs: one public and the other private. The private key is
|
||||
password-protected and is used to sign metadata. The public key can be shared
|
||||
freely, and is used to verify signatures made by the private key.
|
||||
|
||||
The generate\_and\_write\_rsa\_keypair function will create two key files named
|
||||
"path/to/key.pub", which is the public key and "path/to/key", which
|
||||
is the private key.
|
||||
|
||||
```
|
||||
>>> from tuf.developer_tool import *
|
||||
>>> generate_and_write_rsa_keypair("path/to/key")
|
||||
Enter a password for the RSA key:
|
||||
Confirm:
|
||||
>>>
|
||||
```
|
||||
|
||||
We can also use the bits parameter to set a different key length (the default
|
||||
is 3072). We can also provide the password parameter in order to suppress the
|
||||
password prompt.
|
||||
|
||||
In this example we will be using rsa keys, but ed25519 keys are also supported.
|
||||
|
||||
Now we have a key for our project, we can proceed to create our project.
|
||||
|
||||
<a name="the_project_class">
|
||||
### The Project Class
|
||||
The TUF developer tool is built around the Project class, which is used to
|
||||
organize groups of targets associated with a single set of metadata. Each
|
||||
Project instance keeps track of which target files are associated with a single
|
||||
set of metadata. Each Project instance keeps track of which target files are
|
||||
signed and which need signing, which keys are used to sign metadata. It also
|
||||
keeps track of delegated roles, which are covered later.
|
||||
|
||||
Before creating a project, you must know where it will be located in the TUF
|
||||
Repository. In the following example, we will create a project to be hosted as
|
||||
"repo/unclaimed/example_project" within the repository, and store a local copy
|
||||
of the metadata at "path/to/metadata". The project will comprise a single
|
||||
target file, "local/path/to/example\_project/target\_1" locally, and we will
|
||||
secure it with the key generated above.
|
||||
|
||||
First, we must import the generated keys. We can do that by issuing the
|
||||
following:
|
||||
|
||||
```
|
||||
>>> public_key = import_rsa_publickey_from_file("path/to/keys.pub")
|
||||
```
|
||||
|
||||
After importing the key, we can generate a new project with the following
|
||||
command:
|
||||
|
||||
```
|
||||
>>> project = create_new_project(name="example_project",
|
||||
... metadata_directory="local/path/to/metadata/",
|
||||
... targets_directory="local/path/to/example_project",
|
||||
... location_in_repository="repo/unclaimed", key=public_key)
|
||||
```
|
||||
|
||||
Let's list the arguments and make sense out of this rather long function call:
|
||||
|
||||
- create a project named example_project: the name of the metadata file will match this name
|
||||
- the metadata will be located in "local/path/to/metadata", this means all of the generated files
|
||||
for this project will be located here
|
||||
- the targets are located in local/path/to/example project. If your targets are located in some other
|
||||
place, you can point the targets directory there. Files must reside under the path local/path/to/example_project or else it won't be possible to add them.
|
||||
- location\_in\_repository points to repo/unclaimed, this will be prepended to the paths in the generated metadata so the signatures all match.
|
||||
|
||||
Now the project is in memory and we can do different operations on it such as
|
||||
adding and removing targets, delegating files, changing signatures and keys,
|
||||
etc. For the moment we are interested in adding our one and only target inside
|
||||
the project.
|
||||
|
||||
To add a target, we issue the following method:
|
||||
|
||||
```
|
||||
>>> project.add_target("target_1")
|
||||
```
|
||||
|
||||
Have in mind the file "target\_1" should be located in
|
||||
"local/path/to/example\_project" or else the adding procedure will throw an
|
||||
error.
|
||||
|
||||
At this point, the metadata is not valid. We have assigned a key to the
|
||||
project, but we have not *signed* it with that key. Signing is the process of
|
||||
generating a signature with our private key so it can be verified with the
|
||||
public key by the server (upon uploading) and by the clients (when updating).
|
||||
|
||||
<a name="signing_and_writing_the_metadata">
|
||||
### Signing and Writing the Metadata ###
|
||||
In order to sign the metadata, we need to import the private key corresponding
|
||||
to the public key we added to the project. One the key is loaded to the project,
|
||||
it will automatically be used to sign the metadata whenever it is written.
|
||||
|
||||
```
|
||||
>>> private_key = import_rsa_privatekey_from_file("path/to/key")
|
||||
Enter password for the RSA key:
|
||||
>>> project.load_signing_key(private_key)
|
||||
>>> project.write()
|
||||
```
|
||||
|
||||
When all changes to a project have been written, the Project instance can
|
||||
safely be deleted.
|
||||
|
||||
The project can be loaded later to update changes to the project. The metadata
|
||||
contains checksums that have to match the actual files or else it won't be
|
||||
accepted by the upstream repository.
|
||||
|
||||
At this point, if you have followed all the steps in this document so far
|
||||
(substituting appropriate names and filepaths) you will have created a basic
|
||||
TUF project, which can be expanded as needed. The simplest way to get your
|
||||
project secured is to add all your files using add\_target() (or see [Managing
|
||||
Keys](#managing_keys) on how to add whole directories). If your project has
|
||||
several contributors, you may want to consider adding
|
||||
[delegations](#delegations) to your project.
|
||||
|
||||
<a name="loading_an_existing_project">
|
||||
## Loading an Existing Project
|
||||
To make changes to existing metadata, we will need the Project again. We can
|
||||
restore it with the load_project() function.
|
||||
|
||||
```
|
||||
>>> from tuf.developer_tool import *
|
||||
>>> project = load_project("local/path/to/metadata")
|
||||
```
|
||||
Each time the project is loaded anew, the necessary private keys must also be
|
||||
loaded in order to sign metadata.
|
||||
|
||||
```
|
||||
>>> private_key = import_rsa_privatekey_from_file("path/to/key")
|
||||
Enter a password for the RSA key:
|
||||
>>> project.load_signing_key(private_key)
|
||||
>>> project.write()
|
||||
```
|
||||
|
||||
<a name="delegations">
|
||||
## Delegations
|
||||
|
||||
The project we created above is secured entirely by one key. If you want to
|
||||
allow someone else to update part of your project independently, you will need
|
||||
to delegate a new role for them. For example, we can do the following:
|
||||
|
||||
```
|
||||
>>> other_key = import_rsa_publickey_from_file(“another_public_key.pub”)
|
||||
>>> project.delegate(“newrole”, [other_key], targets)
|
||||
```
|
||||
|
||||
The new role is now an attribute of the Project instance, and contains the same
|
||||
methods as Project. For example, we can add targets in the same way as before:
|
||||
|
||||
```
|
||||
>>> project(“newrole”).add_target(“delegated_1”)
|
||||
```
|
||||
|
||||
Recall that we input the other person’s key as part of a list. That list can
|
||||
contain any number of public keys. We can also add keys to the role after
|
||||
creating it using the [add\_verification\_key()](#adding_a_key_to_a_delegation)
|
||||
method.
|
||||
|
||||
### Restricted Paths
|
||||
|
||||
By default, a delegated role is permitted to add and modify targets anywhere in
|
||||
the Project's targets directory. We can assign restricted paths to a delegated
|
||||
role to limit this permission.
|
||||
|
||||
```
|
||||
>>> project.add_restricted_paths(["restricted/filepath"], "newrole")
|
||||
```
|
||||
|
||||
This will prevent the delegated role from signing targets whose local filepaths
|
||||
do not begin with "restricted/filepath". We can assign several restricted
|
||||
filepaths to a role by adding them to the list in the first parameter, or by
|
||||
invoking the method again. A role with multiple restricted paths can add
|
||||
targets to any of them.
|
||||
|
||||
Note that this method is invoked the parent role (in this case, the Project)
|
||||
and takes the delegated role name as an argument.
|
||||
|
||||
### Nested Delegations
|
||||
|
||||
It is possible for a delegated role to have delegations of its own. We can do
|
||||
this by calling delegate() on a delegated role:
|
||||
|
||||
```
|
||||
>>> project("newrole").delegate(“nestedrole”, [key], targets)
|
||||
```
|
||||
|
||||
Nested delegations function no differently than first-order delegations. to
|
||||
demonstrate, adding a target to nested delegation looks like this:
|
||||
|
||||
```
|
||||
>>> project("newrole")("nestedrole").add_target("foo")
|
||||
```
|
||||
|
||||
### Revoking Delegations
|
||||
Delegations can be revoked, removing the delegated role from the project.
|
||||
|
||||
```
|
||||
>>> project.revoke("newrole")
|
||||
```
|
||||
|
||||
<a name="managing_keys">
|
||||
## Managing Keys
|
||||
This section describes the key-related functions and parameters not covered in
|
||||
the [Creating a Simple Project](#creating_a_simple_project) section.
|
||||
|
||||
### Additional Parameters for Key Generation
|
||||
When generating keys, it is possible to specify the length of the key in bits
|
||||
and its password as parameters:
|
||||
|
||||
```
|
||||
>>> generate_and_write_rsa_keypair("path/to/key",bits=2048, password="pw")
|
||||
```
|
||||
The bits parameter defaults to 3072, and values below 2048 will raise an error.
|
||||
The password parameter is only intended to be used in scripts.
|
||||
|
||||
<a name="adding_a_key_to_a_delegation">
|
||||
### Adding a Key to a Delegation
|
||||
New verifications keys can be added to an existing delegation using
|
||||
add\_verification\_key():
|
||||
|
||||
```
|
||||
>>> project("rolename").add_verification_key(pubkey)
|
||||
```
|
||||
|
||||
A delegation can have several verification keys at once. By default, a
|
||||
delegated role with multiple keys can be written using any one of their
|
||||
corresponding signing keys. To modify this behavior, you can change the
|
||||
delegated role's [threshold](#delegation_thrsholds).
|
||||
|
||||
### Removing a Key from a Delegation
|
||||
Verification keys can also be removed, like this:
|
||||
|
||||
```
|
||||
>>> project("rolename").remove_verification_key(pubkey)
|
||||
```
|
||||
|
||||
Remember that a project can only have one key, so this method will return an
|
||||
error if there is already a key assigned to it. In order to replace a key we
|
||||
must first delete the existing one and then add the new one. It is possible to
|
||||
omit the key parameter in the create\_new\_project() function, and add the key
|
||||
later.
|
||||
|
||||
### Changing the Project Key
|
||||
Each Project instance can only have one verification key. This key can be
|
||||
replaced by removing it and adding a new key, in that order.
|
||||
|
||||
```
|
||||
>>> project.remove_verification_key(oldkey)
|
||||
>>> project.add_verification_key(new)
|
||||
```
|
||||
|
||||
<a name="delegation_thresholds">
|
||||
### Delegation Thresholds
|
||||
|
||||
Every delegated role has a threshold, which determines how many of its signing
|
||||
keys need to be loaded to write the role. The threshold defaults to 1, and
|
||||
should not exceed the number of verification keys assigned to the role. The
|
||||
threshold can be accessed as a property of a delegated role.
|
||||
|
||||
```
|
||||
>>> project("rolename").threshold = 2
|
||||
```
|
||||
|
||||
The above line will set the "rolename" role's threshold to 2.
|
||||
|
||||
<a name="managing_targets">
|
||||
## Managing Targets
|
||||
There are supporting functions of the targets library to make the project
|
||||
maintenance easier. These functions are described in this section.
|
||||
|
||||
### Adding Targets by Directory
|
||||
This function is especially useful when creating a new project to add all the
|
||||
files contained in the targets directory. The following code block illustrates
|
||||
the usage of this function:
|
||||
|
||||
```
|
||||
>>> list_of_targets = \
|
||||
... project.get_filepaths_in_directory(“path/within/targets/folder”,
|
||||
... recursive_walk=False, follow_links=False)
|
||||
>>> project.add_targets(list_of_targets)
|
||||
```
|
||||
|
||||
### Deleting Targets from a Project
|
||||
It is possible that we want to delete existing targets inside our project. To
|
||||
stop the developer tool from tracking this file we can issue the following
|
||||
command:
|
||||
|
||||
```
|
||||
>>> project.remove_target(“target_1”)
|
||||
```
|
||||
|
||||
Now the target file won't be part of the metadata.
|
||||
1034
tuf/developer_tool.py
Executable file
1034
tuf/developer_tool.py
Executable file
File diff suppressed because it is too large
Load diff
|
|
@ -457,6 +457,18 @@
|
|||
expires = ISO8601_DATETIME_SCHEMA,
|
||||
meta = FILEDICT_SCHEMA)
|
||||
|
||||
# project.cfg file: stores information about the project in a json dictionary
|
||||
PROJECT_CFG_SCHEMA = SCHEMA.Object(
|
||||
object_name = 'PROJECT_CFG_SCHEMA',
|
||||
project_name = SCHEMA.AnyString(),
|
||||
layout_type = SCHEMA.OneOf([SCHEMA.String('repo-like'), SCHEMA.String('flat')]),
|
||||
targets_location = PATH_SCHEMA,
|
||||
metadata_location = PATH_SCHEMA,
|
||||
prefix = PATH_SCHEMA,
|
||||
public_keys = KEYDICT_SCHEMA,
|
||||
threshold = SCHEMA.Integer(lo = 0, hi = 2)
|
||||
)
|
||||
|
||||
# A schema containing information a repository mirror may require,
|
||||
# such as a url, the path of the directory metadata files, etc.
|
||||
MIRROR_SCHEMA = SCHEMA.Object(
|
||||
|
|
|
|||
Loading…
Reference in a new issue