From a3fbb50b2a5afcd4b72a5ff353a82f9a4463dcf6 Mon Sep 17 00:00:00 2001 From: zanefisher Date: Thu, 12 Sep 2013 14:53:57 -0400 Subject: [PATCH 01/95] Update extraneous dependecies test to expect the new NoWorkingMirrorError. --- .../test_extraneous_dependencies_attack.py | 61 ++++++++++++------- 1 file changed, 40 insertions(+), 21 deletions(-) diff --git a/tests/integration/test_extraneous_dependencies_attack.py b/tests/integration/test_extraneous_dependencies_attack.py index 2813bdfd..2b7799f7 100755 --- a/tests/integration/test_extraneous_dependencies_attack.py +++ b/tests/integration/test_extraneous_dependencies_attack.py @@ -43,7 +43,7 @@ import tempfile import tuf -import tuf.interposition.urllib_tuf as urllib_tuf +from tuf.interposition import urllib_tuf import tuf.tests.util_test_tools as util_test_tools @@ -52,8 +52,8 @@ class ExtraneousDependencyAlert(Exception): -# Interpret the contents of the file it downloads as a list of dependent -# files from the same repository. +# Interpret anything following 'requires:' in the contents of the file it +# downloads as a comma-separated list of dependent files from the same repository. def _download(url, filename, directory, TUF=False): destination = os.path.join(directory, filename) if TUF: @@ -61,8 +61,11 @@ def _download(url, filename, directory, TUF=False): else: urllib.urlretrieve(url, destination) - if util_test_tools.read_file_content(destination) != '': - required_files = util_test_tools.read_file_content(destination).split(',') + file_contents = util_test_tools.read_file_content(destination) + + # Parse the list of required files (if it exists) and download them. + if file_contents.find('requires:') != -1: + required_files = file_contents[file_contents.find('requires:') + 9:].split(',') for required_filename in required_files: required_file_url = os.path.dirname(url)+os.sep+required_filename _download(required_file_url, required_filename, directory, TUF) @@ -81,7 +84,7 @@ def test_extraneous_dependency_attack(TUF=False): """ - ERROR_MSG = 'Extraneous Dependency Attack was Successful!\n' + ERROR_MSG = 'Extraneous Dependency Attack was Successful!' try: @@ -93,20 +96,27 @@ def test_extraneous_dependency_attack(TUF=False): targets_dir = os.path.join(tuf_repo, 'targets') # Add files to 'repo' directory: {root_repo}. - good_dependency_filepath = util_test_tools.add_file_to_repository(reg_repo, '') + good_dependency_filepath = util_test_tools.add_file_to_repository(reg_repo, + 'the file you need') good_dependency_basename = os.path.basename(good_dependency_filepath) - bad_dependency_filepath = util_test_tools.add_file_to_repository(reg_repo, '') + bad_dependency_filepath = util_test_tools.add_file_to_repository(reg_repo, + 'the file you don\'t need') bad_dependency_basename = os.path.basename(bad_dependency_filepath) # The dependent file lists the good dependency. dependent_filepath = util_test_tools.add_file_to_repository(reg_repo, - good_dependency_basename) + 'requires:'+good_dependency_basename) dependent_basename = os.path.basename(dependent_filepath) url_to_repo = url+'reg_repo/'+dependent_basename - modified_dependency_list = good_dependency_basename+','+\ - bad_dependency_basename + + # List the bad dependency first. If an attacker modifies a target by + # simply appending the file contents, tuf.download will ignore the appended + # data, downloading only as much data as the TUF metadata says the target + # should contain. + modified_dependency_list = bad_dependency_basename+','+\ + good_dependency_basename if TUF: # Update TUF metadata before attacker modifies anything. @@ -122,11 +132,11 @@ def test_extraneous_dependency_attack(TUF=False): # Attacker adds the dependency in the targets repository. target = os.path.join(targets_dir, dependent_basename) util_test_tools.modify_file_at_repository(target, - modified_dependency_list) + 'requires:'+modified_dependency_list) # Attacker adds the dependency in the regular repository. util_test_tools.modify_file_at_repository(dependent_filepath, - modified_dependency_list) + 'requires:'+modified_dependency_list) # End of Setup. @@ -136,11 +146,14 @@ def test_extraneous_dependency_attack(TUF=False): _download(url=url_to_repo, filename=dependent_basename, directory=downloads, TUF=TUF) - except tuf.DownloadError: - # If tuf.DownloadError is raised, this means that TUF has prevented - # the download of an unrecognized file. Enable the logging to see, - # what actually happened. - pass + except tuf.NoWorkingMirrorError, error: + # We only set up one mirror, so if it fails, we expect a + # NoWorkingMirrorError. If TUF has worked as intended, the mirror error + # contained within should be a BadHashError. + mirror_error = \ + error.mirror_errors[url+'tuf_repo/targets/'+dependent_basename] + + assert isinstance(mirror_error, tuf.BadHashError) else: # Check if the legitimate dependency was downloaded. @@ -152,7 +165,8 @@ def test_extraneous_dependency_attack(TUF=False): raise ExtraneousDependencyAlert(ERROR_MSG) finally: - util_test_tools.cleanup(root_repo, server_proc) + pass + # util_test_tools.cleanup(root_repo, server_proc) @@ -162,12 +176,17 @@ def test_extraneous_dependency_attack(TUF=False): except ExtraneousDependencyAlert, error: print error +else: + print 'Extraneous dependency attack failed.' - -print 'Attempting extraneous dependency attack with TUF:' +print '\nAttempting extraneous dependency attack with TUF:' try: test_extraneous_dependency_attack(TUF=True) except ExtraneousDependencyAlert, error: print error +else: + print 'Extraneous dependency attack failed.' + +print '' From d8841544e4dd52f5214da185c324f6dcecdb859d Mon Sep 17 00:00:00 2001 From: vladdd Date: Fri, 13 Sep 2013 09:54:10 -0400 Subject: [PATCH 02/95] Fix test_updater.py test case failure Rebuild server repository in test_5_all_targets() - other test cases may not have properly restored the contents of the repository. --- tests/unit/test_updater.py | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/unit/test_updater.py b/tests/unit/test_updater.py index 770e36e6..0b8385ab 100755 --- a/tests/unit/test_updater.py +++ b/tests/unit/test_updater.py @@ -898,6 +898,7 @@ def test_5_all_targets(self): # returns each filepath listed in 'self.all_role_paths' in the listed # order. self._mock_download_url_to_tempfileobj(self.all_role_paths) + setup.build_server_repository(self.server_repo_dir, self.targets_dir) # Update top-level metadata. self.Repository.refresh() From f2517e1717ec32d2dff7c704bc9ac6cf4bc8de12 Mon Sep 17 00:00:00 2001 From: zanefisher Date: Fri, 13 Sep 2013 13:38:50 -0400 Subject: [PATCH 03/95] Clean up extraneous dependencies test following Vlad's review. --- .../test_extraneous_dependencies_attack.py | 18 +++++++----------- 1 file changed, 7 insertions(+), 11 deletions(-) diff --git a/tests/integration/test_extraneous_dependencies_attack.py b/tests/integration/test_extraneous_dependencies_attack.py index 2b7799f7..c3b5b326 100755 --- a/tests/integration/test_extraneous_dependencies_attack.py +++ b/tests/integration/test_extraneous_dependencies_attack.py @@ -38,12 +38,10 @@ import os -import shutil import urllib -import tempfile import tuf -from tuf.interposition import urllib_tuf +import tuf.interposition import tuf.tests.util_test_tools as util_test_tools @@ -57,7 +55,7 @@ class ExtraneousDependencyAlert(Exception): def _download(url, filename, directory, TUF=False): destination = os.path.join(directory, filename) if TUF: - urllib_tuf.urlretrieve(url, destination) + tuf.interposition.urllib_tuf.urlretrieve(url, destination) else: urllib.urlretrieve(url, destination) @@ -75,7 +73,7 @@ def _download(url, filename, directory, TUF=False): def test_extraneous_dependency_attack(TUF=False): """ - Illustrate arbitrary package attack vulnerability. + Illustrate extraneous dependency attack vulnerability. TUF: @@ -165,8 +163,7 @@ def test_extraneous_dependency_attack(TUF=False): raise ExtraneousDependencyAlert(ERROR_MSG) finally: - pass - # util_test_tools.cleanup(root_repo, server_proc) + util_test_tools.cleanup(root_repo, server_proc) @@ -178,9 +175,9 @@ def test_extraneous_dependency_attack(TUF=False): print error else: print 'Extraneous dependency attack failed.' +print - -print '\nAttempting extraneous dependency attack with TUF:' +print 'Attempting extraneous dependency attack with TUF:' try: test_extraneous_dependency_attack(TUF=True) @@ -188,5 +185,4 @@ def test_extraneous_dependency_attack(TUF=False): print error else: print 'Extraneous dependency attack failed.' - -print '' +print From a397e208cdb11de6bf6918e1f6efc8979d46ebfe Mon Sep 17 00:00:00 2001 From: zanefisher Date: Fri, 13 Sep 2013 15:30:11 -0400 Subject: [PATCH 04/95] Rename boolean perameter 'TUF' to 'using_tuf' in every test. --- .../test_arbitrary_package_attack.py | 46 +++++++++++-------- tests/integration/test_delegations.py | 2 +- tests/integration/test_endless_data_attack.py | 22 ++++----- .../test_extraneous_dependencies_attack.py | 30 ++++++------ .../test_indefinite_freeze_attack.py | 20 ++++---- .../integration/test_mix_and_match_attack.py | 28 +++++------ tests/integration/test_replay_attack.py | 24 +++++----- .../integration/test_slow_retrieval_attack.py | 20 ++++---- tests/unit/test_util_test_tools.py | 2 +- tuf/tests/util_test_tools.py | 6 +-- 10 files changed, 105 insertions(+), 95 deletions(-) diff --git a/tests/integration/test_arbitrary_package_attack.py b/tests/integration/test_arbitrary_package_attack.py index 8164c678..e512efe8 100755 --- a/tests/integration/test_arbitrary_package_attack.py +++ b/tests/integration/test_arbitrary_package_attack.py @@ -30,12 +30,10 @@ """ import os -import shutil import urllib -import tempfile import tuf -import tuf.interposition.urllib_tuf as urllib_tuf +import tuf.interposition import tuf.tests.util_test_tools as util_test_tools @@ -45,9 +43,9 @@ class ArbitraryPackageAlert(Exception): -def _download(url, filename, tuf=False): - if tuf: - urllib_tuf.urlretrieve(url, filename) +def _download(url, filename, using_tuf=False): + if using_tuf: + tuf.interposition.urllib_tuf.urlretrieve(url, filename) else: urllib.urlretrieve(url, filename) @@ -56,10 +54,10 @@ def _download(url, filename, tuf=False): -def test_arbitrary_package_attack(TUF=False): +def test_arbitrary_package_attack(using_tuf=False): """ - TUF: + using_tuf: If set to 'False' all directories that start with 'tuf_' are ignored, indicating that tuf is not implemented. @@ -73,7 +71,7 @@ def test_arbitrary_package_attack(TUF=False): try: # Setup. - root_repo, url, server_proc, keyids = util_test_tools.init_repo(tuf=TUF) + root_repo, url, server_proc, keyids = util_test_tools.init_repo(using_tuf) reg_repo = os.path.join(root_repo, 'reg_repo') tuf_repo = os.path.join(root_repo, 'tuf_repo') downloads = os.path.join(root_repo, 'downloads') @@ -85,7 +83,7 @@ def test_arbitrary_package_attack(TUF=False): url_to_repo = url+'reg_repo/'+file_basename downloaded_file = os.path.join(downloads, file_basename) - if TUF: + if using_tuf: # Update TUF metadata before attacker modifies anything. util_test_tools.tuf_refresh_repo(root_repo, keyids) @@ -108,13 +106,16 @@ def test_arbitrary_package_attack(TUF=False): try: # Client downloads (tries to download) the file. - _download(url=url_to_repo, filename=downloaded_file, tuf=TUF) + _download(url=url_to_repo, filename=downloaded_file, using_tuf) - except tuf.DownloadError: - # If tuf.DownloadError is raised, this means that TUF has prevented - # the download of an unrecognized file. Enable the logging to see, - # what actually happened. - pass + except tuf.NoWorkingMirrorError: + # We only set up one mirror, so if it fails, we expect a + # NoWorkingMirrorError. If TUF has worked as intended, the mirror error + # contained within should be a BadHashError. + mirror_error = \ + error.mirror_errors[url+'tuf_repo/targets/'+dependent_basename] + + assert isinstance(mirror_error, tuf.BadHashError) else: # Check whether the attack succeeded by inspecting the content of the @@ -131,17 +132,24 @@ def test_arbitrary_package_attack(TUF=False): - +print 'Attempting arbitrary package attack without TUF:' try: - test_arbitrary_package_attack(TUF=False) + test_arbitrary_package_attack(using_tuf=False) except ArbitraryPackageAlert, error: print error +else: + print 'Extraneous dependency attack failed.' +print +print 'Attempting arbitrary package attack with TUF:' try: - test_arbitrary_package_attack(TUF=True) + test_arbitrary_package_attack(using_tuf=True) except ArbitraryPackageAlert, error: print error +else: + print 'Extraneous dependency attack failed.' +print diff --git a/tests/integration/test_delegations.py b/tests/integration/test_delegations.py index 94f84917..d1528b42 100755 --- a/tests/integration/test_delegations.py +++ b/tests/integration/test_delegations.py @@ -107,7 +107,7 @@ def setUp(self): version = version+1 expiration = tuf.formats.format_time(time.time()+86400) - root_repo, url, server_proc, keyids = util_test_tools.init_repo(tuf=True) + root_repo, url, server_proc, keyids = util_test_tools.init_repo(using_tuf=True) # Server side repository. tuf_repo = os.path.join(root_repo, 'tuf_repo') diff --git a/tests/integration/test_endless_data_attack.py b/tests/integration/test_endless_data_attack.py index 3f1e0ac8..6af5dafd 100755 --- a/tests/integration/test_endless_data_attack.py +++ b/tests/integration/test_endless_data_attack.py @@ -47,15 +47,15 @@ class EndlessDataAttack(Exception): -def _download(url, filename, TUF=False): - if TUF: +def _download(url, filename, using_tuf=False): + if using_tuf: urllib_tuf.urlretrieve(url, filename) else: urllib.urlretrieve(url, filename) -def test_arbitrary_package_attack(TUF=False, TIMESTAMP=False): +def test_arbitrary_package_attack(using_tuf=False, TIMESTAMP=False): """ TUF: @@ -72,7 +72,7 @@ def test_arbitrary_package_attack(TUF=False, TIMESTAMP=False): try: # Setup. - root_repo, url, server_proc, keyids = util_test_tools.init_repo(tuf=TUF) + root_repo, url, server_proc, keyids = util_test_tools.init_repo(using_tuf) reg_repo = os.path.join(root_repo, 'reg_repo') tuf_repo = os.path.join(root_repo, 'tuf_repo') downloads = os.path.join(root_repo, 'downloads') @@ -91,7 +91,7 @@ def test_arbitrary_package_attack(TUF=False, TIMESTAMP=False): noisy_data = 'X'*100000 - if TUF: + if using_tuf: # Update TUF metadata before attacker modifies anything. util_test_tools.tuf_refresh_repo(root_repo, keyids) # Modify the url. Remember that the interposition will intercept @@ -126,11 +126,11 @@ def test_arbitrary_package_attack(TUF=False, TIMESTAMP=False): # Client downloads (tries to download) the file. try: - _download(url=url_to_repo, filename=downloaded_file, TUF=TUF) + _download(url=url_to_repo, filename=downloaded_file, using_tuf) except Exception, exception: # Because we are extending the true timestamp TUF metadata with invalid # JSON, we except to catch an error about invalid metadata JSON. - if TUF and TIMESTAMP: + if using_tuf and TIMESTAMP: endless_data_attack = False for mirror_url, mirror_error in exception.mirror_errors.iteritems(): @@ -146,7 +146,7 @@ def test_arbitrary_package_attack(TUF=False, TIMESTAMP=False): # When we test downloading "endless" timestamp with TUF, we want to skip # the following test because downloading the timestamp should have failed. - if not (TUF and TIMESTAMP): + if not (using_tuf and TIMESTAMP): # Check whether the attack succeeded by inspecting the content of the # update. The update should contain 'Test A'. Technically it suffices # to check whether the file was downloaded or not. @@ -162,12 +162,12 @@ def test_arbitrary_package_attack(TUF=False, TIMESTAMP=False): try: - test_arbitrary_package_attack(TUF=False, TIMESTAMP=False) + test_arbitrary_package_attack(using_tuf=False, TIMESTAMP=False) except EndlessDataAttack, error: print('Endless data attack worked on download without TUF!') try: - test_arbitrary_package_attack(TUF=True, TIMESTAMP=False) + test_arbitrary_package_attack(using_tuf=True, TIMESTAMP=False) except EndlessDataAttack, error: print('Endless data attack worked on download without TUF!') print(str(error)) @@ -177,7 +177,7 @@ def test_arbitrary_package_attack(TUF=False, TIMESTAMP=False): try: # This test fails because the timestamp metadata has been extended with # random data from its true length, thereby resulting in invalid JSON. - test_arbitrary_package_attack(TUF=True, TIMESTAMP=True) + test_arbitrary_package_attack(using_tuf=True, TIMESTAMP=True) except EndlessDataAttack, error: print('Endless data attack worked on download without TUF!') print(str(error)) diff --git a/tests/integration/test_extraneous_dependencies_attack.py b/tests/integration/test_extraneous_dependencies_attack.py index c3b5b326..f30a35ba 100755 --- a/tests/integration/test_extraneous_dependencies_attack.py +++ b/tests/integration/test_extraneous_dependencies_attack.py @@ -52,9 +52,9 @@ class ExtraneousDependencyAlert(Exception): # Interpret anything following 'requires:' in the contents of the file it # downloads as a comma-separated list of dependent files from the same repository. -def _download(url, filename, directory, TUF=False): +def _download(url, filename, directory, using_tuf=False): destination = os.path.join(directory, filename) - if TUF: + if using_tuf: tuf.interposition.urllib_tuf.urlretrieve(url, destination) else: urllib.urlretrieve(url, destination) @@ -66,17 +66,17 @@ def _download(url, filename, directory, TUF=False): required_files = file_contents[file_contents.find('requires:') + 9:].split(',') for required_filename in required_files: required_file_url = os.path.dirname(url)+os.sep+required_filename - _download(required_file_url, required_filename, directory, TUF) + _download(required_file_url, required_filename, directory, using_tuf) -def test_extraneous_dependency_attack(TUF=False): +def test_extraneous_dependency_attack(using_tuf=False): """ Illustrate extraneous dependency attack vulnerability. - TUF: + using_tuf: If set to 'False' all directories that start with 'tuf_' are ignored, indicating that tuf is not implemented. @@ -87,7 +87,7 @@ def test_extraneous_dependency_attack(TUF=False): try: # Setup. - root_repo, url, server_proc, keyids = util_test_tools.init_repo(tuf=TUF) + root_repo, url, server_proc, keyids = util_test_tools.init_repo(using_tuf) reg_repo = os.path.join(root_repo, 'reg_repo') tuf_repo = os.path.join(root_repo, 'tuf_repo') downloads = os.path.join(root_repo, 'downloads') @@ -116,7 +116,7 @@ def test_extraneous_dependency_attack(TUF=False): modified_dependency_list = bad_dependency_basename+','+\ good_dependency_basename - if TUF: + if using_tuf: # Update TUF metadata before attacker modifies anything. util_test_tools.tuf_refresh_repo(root_repo, keyids) @@ -127,12 +127,14 @@ def test_extraneous_dependency_attack(TUF=False): # path relative to 'targets_dir'. url_to_repo = 'http://localhost:9999/'+dependent_basename - # Attacker adds the dependency in the targets repository. - target = os.path.join(targets_dir, dependent_basename) - util_test_tools.modify_file_at_repository(target, + # Attacker modifies the depenent file in the targets repository, adding + # the bad dependency to its list. + dependent_target_filepath = os.path.join(targets_dir, dependent_basename) + util_test_tools.modify_file_at_repository(dependent_target_filepath, 'requires:'+modified_dependency_list) - # Attacker adds the dependency in the regular repository. + # Attacker modifies the depenent file in the regular repository, adding + # the bad dependency to its list. util_test_tools.modify_file_at_repository(dependent_filepath, 'requires:'+modified_dependency_list) @@ -142,7 +144,7 @@ def test_extraneous_dependency_attack(TUF=False): try: # Client downloads (tries to download) the file. _download(url=url_to_repo, filename=dependent_basename, - directory=downloads, TUF=TUF) + directory=downloads, using_tuf) except tuf.NoWorkingMirrorError, error: # We only set up one mirror, so if it fails, we expect a @@ -169,7 +171,7 @@ def test_extraneous_dependency_attack(TUF=False): print 'Attempting extraneous dependency attack without TUF:' try: - test_extraneous_dependency_attack(TUF=False) + test_extraneous_dependency_attack(using_tuf=False) except ExtraneousDependencyAlert, error: print error @@ -179,7 +181,7 @@ def test_extraneous_dependency_attack(TUF=False): print 'Attempting extraneous dependency attack with TUF:' try: - test_extraneous_dependency_attack(TUF=True) + test_extraneous_dependency_attack(using_tuf=True) except ExtraneousDependencyAlert, error: print error diff --git a/tests/integration/test_indefinite_freeze_attack.py b/tests/integration/test_indefinite_freeze_attack.py index b422fae5..21a3ac4f 100755 --- a/tests/integration/test_indefinite_freeze_attack.py +++ b/tests/integration/test_indefinite_freeze_attack.py @@ -61,8 +61,8 @@ def _remake_timestamp(metadata_dir, keyids): -def _download(url, filename, tuf=False): - if tuf: +def _download(url, filename, using_tuf=False): + if using_tuf: urllib_tuf.urlretrieve(url, filename) else: @@ -72,10 +72,10 @@ def _download(url, filename, tuf=False): -def test_indefinite_freeze_attack(TUF=False): +def test_indefinite_freeze_attack(using_tuf=False): """ - TUF: + using_tuf: If set to 'False' all directories that start with 'tuf_' are ignored, indicating that tuf is not implemented. @@ -88,7 +88,7 @@ def test_indefinite_freeze_attack(TUF=False): try: # Setup. - root_repo, url, server_proc, keyids = util_test_tools.init_repo(tuf=TUF) + root_repo, url, server_proc, keyids = util_test_tools.init_repo(using_tuf) reg_repo = os.path.join(root_repo, 'reg_repo') tuf_repo = os.path.join(root_repo, 'tuf_repo') metadata_dir = os.path.join(tuf_repo, 'metadata') @@ -100,7 +100,7 @@ def test_indefinite_freeze_attack(TUF=False): url_to_repo = url+'reg_repo/'+file_basename downloaded_file = os.path.join(downloads, file_basename) - if TUF: + if using_tuf: print 'TUF ...' # Update TUF metadata before attacker modifies anything. @@ -119,7 +119,7 @@ def test_indefinite_freeze_attack(TUF=False): # Client performs initial download. try: - _download(url=url_to_repo, filename=downloaded_file, tuf=TUF) + _download(url=url_to_repo, filename=downloaded_file, using_tuf) except tuf.ExpiredMetadataError: msg = ('Metadata has expired too soon, extend expiration period. '+ 'Current expiration is set to: '+repr(EXPIRATION)+' second(s).') @@ -130,7 +130,7 @@ def test_indefinite_freeze_attack(TUF=False): # Try downloading again, this should raise an error. try: - _download(url=url_to_repo, filename=downloaded_file, tuf=TUF) + _download(url=url_to_repo, filename=downloaded_file, using_tuf) except tuf.ExpiredMetadataError, error: pass else: @@ -145,12 +145,12 @@ def test_indefinite_freeze_attack(TUF=False): try: - test_indefinite_freeze_attack(TUF=False) + test_indefinite_freeze_attack(using_tuf=False) except IndefiniteFreezeAttackAlert, error: print error try: - test_indefinite_freeze_attack(TUF=True) + test_indefinite_freeze_attack(using_tuf=True) except IndefiniteFreezeAttackAlert, error: print error diff --git a/tests/integration/test_mix_and_match_attack.py b/tests/integration/test_mix_and_match_attack.py index da8e170c..9ebc6340 100755 --- a/tests/integration/test_mix_and_match_attack.py +++ b/tests/integration/test_mix_and_match_attack.py @@ -40,7 +40,7 @@ import tempfile import tuf -import tuf.interposition.urllib_tuf as urllib_tuf +import tuf.interposition import tuf.tests.util_test_tools as util_test_tools @@ -48,16 +48,16 @@ class MixAndMatchAttackAlert(Exception): pass -def _download(url, filename, tuf=False): - if tuf: - urllib_tuf.urlretrieve(url, filename) +def _download(url, filename, using_tuf=False): + if using_tuf: + tuf.interposition.urllib_tuf.urlretrieve(url, filename) else: urllib.urlretrieve(url, filename) -def test_mix_and_match_attack(TUF=False): +def test_mix_and_match_attack(using_tuf=False): """ Attack design: There are 3 stages: @@ -81,7 +81,7 @@ def test_mix_and_match_attack(TUF=False): try: # Setup / Stage 1 # --------------- - root_repo, url, server_proc, keyids = util_test_tools.init_repo(tuf=TUF) + root_repo, url, server_proc, keyids = util_test_tools.init_repo(using_tuf) reg_repo = os.path.join(root_repo, 'reg_repo') downloads = os.path.join(root_repo, 'downloads') evil_dir = tempfile.mkdtemp(dir=root_repo) @@ -97,7 +97,7 @@ def test_mix_and_match_attack(TUF=False): unpatched_file = os.path.join(evil_dir, file_basename) - if TUF: + if using_tuf: print 'TUF ...' tuf_repo = os.path.join(root_repo, 'tuf_repo') tuf_targets = os.path.join(tuf_repo, 'targets') @@ -126,7 +126,7 @@ def test_mix_and_match_attack(TUF=False): # Client's initial download. - _download(url=url_to_file, filename=downloaded_file, tuf=TUF) + _download(url=url_to_file, filename=downloaded_file, using_tuf) # Stage 2 # ------- @@ -135,11 +135,11 @@ def test_mix_and_match_attack(TUF=False): # Updating tuf repository. This will copy files from regular repository # into tuf repository and refresh the metadata - if TUF: + if using_tuf: util_test_tools.tuf_refresh_repo(root_repo, keyids) # Client downloads the patched file. - _download(url=url_to_file, filename=downloaded_file, tuf=TUF) + _download(url=url_to_file, filename=downloaded_file, using_tuf) downloaded_content = util_test_tools.read_file_content(downloaded_file) @@ -150,7 +150,7 @@ def test_mix_and_match_attack(TUF=False): # Updating tuf repository. This will copy files from regular repository # into tuf repository and refresh the metadata - if TUF: + if using_tuf: util_test_tools.tuf_refresh_repo(root_repo, keyids) # Attacker replaces the metadata and the target file. @@ -163,7 +163,7 @@ def test_mix_and_match_attack(TUF=False): # Client tries to downloads the newly patched file. try: - _download(url=url_to_file, filename=downloaded_file, tuf=TUF) + _download(url=url_to_file, filename=downloaded_file, using_tuf) except tuf.MetadataNotAvailableError: pass @@ -182,12 +182,12 @@ def test_mix_and_match_attack(TUF=False): try: - test_mix_and_match_attack(TUF=False) + test_mix_and_match_attack(using_tuf=False) except MixAndMatchAttackAlert, error: print error try: - test_mix_and_match_attack(TUF=True) + test_mix_and_match_attack(using_tuf=True) except MixAndMatchAttackAlert, error: print error diff --git a/tests/integration/test_replay_attack.py b/tests/integration/test_replay_attack.py index b9088264..746a47e0 100755 --- a/tests/integration/test_replay_attack.py +++ b/tests/integration/test_replay_attack.py @@ -52,8 +52,8 @@ class ReplayAttackAlert(Exception): -def _download(url, filename, tuf=False): - if tuf: +def _download(url, filename, using_tuf=False): + if using_tuf: urllib_tuf.urlretrieve(url, filename) else: @@ -63,10 +63,10 @@ def _download(url, filename, tuf=False): -def test_replay_attack(TUF=False): +def test_replay_attack(using_tuf=False): """ - TUF: + using_tuf: If set to 'False' all directories that start with 'tuf_' are ignored, indicating that tuf is not implemented. @@ -80,7 +80,7 @@ def test_replay_attack(TUF=False): try: # Setup. - root_repo, url, server_proc, keyids = util_test_tools.init_repo(tuf=TUF) + root_repo, url, server_proc, keyids = util_test_tools.init_repo(using_tuf) reg_repo = os.path.join(root_repo, 'reg_repo') tuf_repo = os.path.join(root_repo, 'tuf_repo') downloads = os.path.join(root_repo, 'downloads') @@ -97,7 +97,7 @@ def test_replay_attack(TUF=False): vulnerable_file = os.path.join(evil_dir, file_basename) shutil.copy(filepath, evil_dir) - if TUF: + if using_tuf: print 'TUF ...' # Update TUF metadata before attacker modifies anything. @@ -114,7 +114,7 @@ def test_replay_attack(TUF=False): # Client performs initial update. - _download(url=url_to_repo, filename=downloaded_file, tuf=TUF) + _download(url=url_to_repo, filename=downloaded_file, using_tuf) # Downloads are stored in the same directory '{root_repo}/downloads/' # for regular and tuf clients. @@ -127,12 +127,12 @@ def test_replay_attack(TUF=False): # Updating tuf repository. This will copy files from regular repository # into tuf repository and refresh the metadata - if TUF: + if using_tuf: util_test_tools.tuf_refresh_repo(root_repo, keyids) # Client downloads the patched file. - _download(url=url_to_repo, filename=downloaded_file, tuf=TUF) + _download(url=url_to_repo, filename=downloaded_file, using_tuf) # Content of the downloaded file. downloaded_content = util_test_tools.read_file_content(downloaded_file) @@ -157,7 +157,7 @@ def test_replay_attack(TUF=False): # Client downloads the file once more. - _download(url=url_to_repo, filename=downloaded_file, tuf=TUF) + _download(url=url_to_repo, filename=downloaded_file, using_tuf) # Check whether the attack succeeded by inspecting the content of the # update. The update should contain 'Test NOT A'. @@ -174,13 +174,13 @@ def test_replay_attack(TUF=False): try: - test_replay_attack(TUF=False) + test_replay_attack(using_tuf=False) except ReplayAttackAlert, error: print error try: - test_replay_attack(TUF=True) + test_replay_attack(using_tuf=True) except ReplayAttackAlert, error: print error diff --git a/tests/integration/test_slow_retrieval_attack.py b/tests/integration/test_slow_retrieval_attack.py index 9d515be2..cee4234c 100755 --- a/tests/integration/test_slow_retrieval_attack.py +++ b/tests/integration/test_slow_retrieval_attack.py @@ -57,8 +57,8 @@ class SlowRetrievalAttackAlert(Exception): pass -def _download(url, filename, TUF=False): - if TUF: +def _download(url, filename, using_tuf=False): + if using_tuf: try: urllib_tuf.urlretrieve(url, filename) except tuf.NoWorkingMirrorError, exception: @@ -82,10 +82,10 @@ def _download(url, filename, TUF=False): -def test_slow_retrieval_attack(TUF=False, mode=None): +def test_slow_retrieval_attack(using_tuf=False, mode=None): WAIT_TIME = 60 # Number of seconds to wait until download completes. - ERROR_MSG = 'Slow retrieval attack succeeded (TUF: '+str(TUF)+', mode: '+\ + ERROR_MSG = 'Slow retrieval attack succeeded (using_tuf: '+str(using_tuf)+', mode: '+\ str(mode)+').' # Launch the server. @@ -97,7 +97,7 @@ def test_slow_retrieval_attack(TUF=False, mode=None): try: # Setup. root_repo, url, server_proc, keyids = \ - util_test_tools.init_repo(tuf=TUF, port=port) + util_test_tools.init_repo(using_tuf, port=port) reg_repo = os.path.join(root_repo, 'reg_repo') downloads = os.path.join(root_repo, 'downloads') @@ -108,7 +108,7 @@ def test_slow_retrieval_attack(TUF=False, mode=None): downloaded_file = os.path.join(downloads, file_basename) - if TUF: + if using_tuf: tuf_repo = os.path.join(root_repo, 'tuf_repo') # Update TUF metadata before attacker modifies anything. @@ -147,25 +147,25 @@ def test_slow_retrieval_attack(TUF=False, mode=None): # mode_2: During the download process, the server blocks the download # by sending just several characters every few seconds. try: - test_slow_retrieval_attack(TUF=False, mode = "mode_1") + test_slow_retrieval_attack(using_tuf=False, mode = "mode_1") except SlowRetrievalAttackAlert, error: print(error) print() try: - test_slow_retrieval_attack(TUF=False, mode = "mode_2") + test_slow_retrieval_attack(using_tuf=False, mode = "mode_2") except SlowRetrievalAttackAlert, error: print(error) print() try: - test_slow_retrieval_attack(TUF=True, mode = "mode_1") + test_slow_retrieval_attack(using_tuf=True, mode = "mode_1") except SlowRetrievalAttackAlert, error: print(error) print() try: - test_slow_retrieval_attack(TUF=True, mode = "mode_2") + test_slow_retrieval_attack(using_tuf=True, mode = "mode_2") except SlowRetrievalAttackAlert, error: print(error) print() diff --git a/tests/unit/test_util_test_tools.py b/tests/unit/test_util_test_tools.py index 825f4aab..0acf5131 100755 --- a/tests/unit/test_util_test_tools.py +++ b/tests/unit/test_util_test_tools.py @@ -34,7 +34,7 @@ def setUp(self): tuf.repo.keystore.clear_keystore() # Unpacking necessary parameters returned from init_repo() - essential_params = util_test_tools.init_repo(tuf=True) + essential_params = util_test_tools.init_repo(using_tuf=True) self.root_repo = essential_params[0] self.url = essential_params[1] self.server_proc = essential_params[2] diff --git a/tuf/tests/util_test_tools.py b/tuf/tests/util_test_tools.py index d79cfeed..14345a34 100644 --- a/tuf/tests/util_test_tools.py +++ b/tuf/tests/util_test_tools.py @@ -81,7 +81,7 @@ previous metadata files. - init_repo(tuf=True): + init_repo(using_tuf=True): Initializes the repositories (depicted in the diagram above) and starts the server process. init_repo takes one boolean argument which when True sets-up tuf repository i.e. adds all of the @@ -158,7 +158,7 @@ def disable_console_logging(): tuf.log.logger.removeHandler(tuf.log.console_handler) -def init_repo(tuf=False, port=None): +def init_repo(using_tuf=False, port=None): # Temp root directory for regular and tuf repositories. # WARNING: tuf client stores files in '{root_repo}/downloads/' directory! # Make sure regular download are NOT stored in the that directory when @@ -188,7 +188,7 @@ def init_repo(tuf=False, port=None): time.sleep(.2) keyids = None - if tuf: + if using_tuf: disable_console_logging() keyids = init_tuf(root_repo) create_interposition_config(root_repo, url) From 3d924ff038c24cd4f72dc99daafbf1398290b483 Mon Sep 17 00:00:00 2001 From: ttgump Date: Fri, 13 Sep 2013 16:46:13 -0400 Subject: [PATCH 05/95] Merge branch 'demo2', remote-tracking branch 'upstream/demo2' into demo2 From bf17b004235bbe92dbca91976f0a6188996a552a Mon Sep 17 00:00:00 2001 From: dachshund Date: Fri, 13 Sep 2013 17:01:18 -0400 Subject: [PATCH 06/95] Add a filter to the logging console handler to simplify exception text. --- tuf/log.py | 62 +++++++++++++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 59 insertions(+), 3 deletions(-) diff --git a/tuf/log.py b/tuf/log.py index 0667e4ad..77487e36 100755 --- a/tuf/log.py +++ b/tuf/log.py @@ -73,6 +73,8 @@ '[%(funcName)s:%(lineno)s@%(filename)s] %(message)s' +# Ask all Formatter instances to talk GMT. +# http://docs.python.org/2/library/logging.html#logging.Formatter.formatException logging.Formatter.converter = time.gmtime formatter = logging.Formatter(_FORMAT_STRING) @@ -104,6 +106,51 @@ +class ConsoleFilter(logging.Filter): + def filter(self, record): + """ + + Use Vinay Sajip's recommendation from Python issue #6435 to modify a + LogRecord object. This is meant to be used with our console handler. + + http://stackoverflow.com/q/6177520 + http://stackoverflow.com/q/5875225 + http://bugs.python.org/issue6435 + http://docs.python.org/2/howto/logging-cookbook.html#filters-contextual + http://docs.python.org/2/library/logging.html#logrecord-attributes + + + record: + A logging.LogRecord object. + + + None. + + + Replaces the LogRecord exception text attribute. + + + True. + + """ + + # If this LogRecord object has an exception, then we will replace its text. + if record.exc_info: + # We place the record's cached exception text (which usually contains the + # exception traceback) with much simpler exception information. This is + # most useful for the console handler, which we do not wish to deluge + # with too much data. Assuming that this filter is not applied to the + # file logging handler, the user may always consult the file log for the + # original exception traceback. + record.exc_text = str(record.exc_info) + + # Always return True to signal that any given record must be formatted. + return True + + + + + def set_log_level(log_level=_DEFAULT_LOG_LEVEL): """ @@ -200,7 +247,6 @@ def set_console_log_level(log_level=_DEFAULT_CONSOLE_LOG_LEVEL): - def add_console_handler(log_level=_DEFAULT_CONSOLE_LOG_LEVEL): """ @@ -222,15 +268,25 @@ def add_console_handler(log_level=_DEFAULT_CONSOLE_LOG_LEVEL): None. """ - + # Assign to the global console_handler object. + global console_handler + # Does 'log_level' have the correct format? # Raise 'tuf.FormatError' if there is a mismatch. tuf.formats.LENGTH_SCHEMA.check_match(log_level) # Set the console handler for the logger. The built-in console handler will # log messages to 'sys.stderr' and capture 'log_level' messages. - global console_handler console_handler = logging.StreamHandler() + # Get our filter for the console handler. + console_filter = ConsoleFilter() + console_handler.setLevel(log_level) console_handler.setFormatter(formatter) + console_handler.addFilter(console_filter) logger.addHandler(console_handler) + + + + + From 4d7d4bab8d0970923ca62906465b644bc214188f Mon Sep 17 00:00:00 2001 From: dachshund Date: Fri, 13 Sep 2013 17:34:51 -0400 Subject: [PATCH 07/95] Offer option to remove console handler; fix variable shadow bug. The rest of the integration tests will need updates due to the fix of the variable shadow bug. --- tuf/log.py | 47 ++++++++++++++++++++++++++---------- tuf/tests/util_test_tools.py | 11 +++------ 2 files changed, 38 insertions(+), 20 deletions(-) diff --git a/tuf/log.py b/tuf/log.py index 77487e36..7987ec94 100755 --- a/tuf/log.py +++ b/tuf/log.py @@ -78,11 +78,11 @@ logging.Formatter.converter = time.gmtime formatter = logging.Formatter(_FORMAT_STRING) -# Set the handlers for the logger. The console handler is unset by default. A +# Set the handlers for the logger. The console handler is unset by default. A # module importing 'log.py' should explicitly set the console handler if -# outputting log messages to the screen is needed. Adding a console handler -# can be done with tuf.log.add_console_handler(). Logging messages to a file -# *is* set by default. +# outputting log messages to the screen is needed. Adding a console handler can +# be done with tuf.log.add_console_handler(). Logging messages to a file *is* +# set by default. console_handler = None # Set the built-in file handler. Messages will be logged to @@ -275,16 +275,37 @@ def add_console_handler(log_level=_DEFAULT_CONSOLE_LOG_LEVEL): # Raise 'tuf.FormatError' if there is a mismatch. tuf.formats.LENGTH_SCHEMA.check_match(log_level) - # Set the console handler for the logger. The built-in console handler will - # log messages to 'sys.stderr' and capture 'log_level' messages. - console_handler = logging.StreamHandler() - # Get our filter for the console handler. - console_filter = ConsoleFilter() + if not console_handler: + # Set the console handler for the logger. The built-in console handler will + # log messages to 'sys.stderr' and capture 'log_level' messages. + # NOTE: This is not thread-safe. + console_handler = logging.StreamHandler() + # Get our filter for the console handler. + console_filter = ConsoleFilter() - console_handler.setLevel(log_level) - console_handler.setFormatter(formatter) - console_handler.addFilter(console_filter) - logger.addHandler(console_handler) + console_handler.setLevel(log_level) + console_handler.setFormatter(formatter) + console_handler.addFilter(console_filter) + logger.addHandler(console_handler) + logger.debug('Added a console handler.') + else: + logger.warn('We already have a console handler.') + + + + + +def remove_console_handler(): + # Assign to the global console_handler object. + global console_handler + + if console_handler: + logger.removeHandler(console_handler) + # NOTE: This is not thread-safe. + console_handler = None + logger.debug('Removed a console handler.') + else: + logger.warn('We do not have a console handler.') diff --git a/tuf/tests/util_test_tools.py b/tuf/tests/util_test_tools.py index d79cfeed..c259b103 100644 --- a/tuf/tests/util_test_tools.py +++ b/tuf/tests/util_test_tools.py @@ -154,11 +154,7 @@ tuf_configurations = None -def disable_console_logging(): - tuf.log.logger.removeHandler(tuf.log.console_handler) - - -def init_repo(tuf=False, port=None): +def init_repo(using_tuf=False, port=None): # Temp root directory for regular and tuf repositories. # WARNING: tuf client stores files in '{root_repo}/downloads/' directory! # Make sure regular download are NOT stored in the that directory when @@ -188,8 +184,9 @@ def init_repo(tuf=False, port=None): time.sleep(.2) keyids = None - if tuf: - disable_console_logging() + if using_tuf: + # We remove the console handler so that tests are silent by default. + tuf.log.remove_console_handler() keyids = init_tuf(root_repo) create_interposition_config(root_repo, url) From e059cf814b9ad47355edeb62673ec7cb3e2ccae9 Mon Sep 17 00:00:00 2001 From: zanefisher Date: Fri, 13 Sep 2013 17:39:29 -0400 Subject: [PATCH 08/95] More integration test tidying-up --- tests/integration/test_endless_data_attack.py | 6 +++--- tests/integration/test_indefinite_freeze_attack.py | 8 ++++---- tests/integration/test_mix_and_match_attack.py | 6 +++--- tests/integration/test_replay_attack.py | 10 +++++----- tests/integration/test_slow_retrieval_attack.py | 6 +++--- 5 files changed, 18 insertions(+), 18 deletions(-) diff --git a/tests/integration/test_endless_data_attack.py b/tests/integration/test_endless_data_attack.py index 6af5dafd..d54de3c7 100755 --- a/tests/integration/test_endless_data_attack.py +++ b/tests/integration/test_endless_data_attack.py @@ -37,7 +37,7 @@ import urllib import tuf -import tuf.interposition.urllib_tuf as urllib_tuf +import tuf.interposition import tuf.tests.util_test_tools as util_test_tools @@ -49,7 +49,7 @@ class EndlessDataAttack(Exception): def _download(url, filename, using_tuf=False): if using_tuf: - urllib_tuf.urlretrieve(url, filename) + tuf.interposition.urllib_tuf.urlretrieve(url, filename) else: urllib.urlretrieve(url, filename) @@ -126,7 +126,7 @@ def test_arbitrary_package_attack(using_tuf=False, TIMESTAMP=False): # Client downloads (tries to download) the file. try: - _download(url=url_to_repo, filename=downloaded_file, using_tuf) + _download(url_to_repo, downloaded_file, using_tuf) except Exception, exception: # Because we are extending the true timestamp TUF metadata with invalid # JSON, we except to catch an error about invalid metadata JSON. diff --git a/tests/integration/test_indefinite_freeze_attack.py b/tests/integration/test_indefinite_freeze_attack.py index 21a3ac4f..a1550b97 100755 --- a/tests/integration/test_indefinite_freeze_attack.py +++ b/tests/integration/test_indefinite_freeze_attack.py @@ -28,7 +28,7 @@ import tuf import tuf.formats -import tuf.interposition.urllib_tuf as urllib_tuf +import tuf.interposition import tuf.repo.signerlib as signerlib import tuf.tests.util_test_tools as util_test_tools @@ -63,7 +63,7 @@ def _remake_timestamp(metadata_dir, keyids): def _download(url, filename, using_tuf=False): if using_tuf: - urllib_tuf.urlretrieve(url, filename) + tuf.interposition.urllib_tuf.urlretrieve(url, filename) else: urllib.urlretrieve(url, filename) @@ -119,7 +119,7 @@ def test_indefinite_freeze_attack(using_tuf=False): # Client performs initial download. try: - _download(url=url_to_repo, filename=downloaded_file, using_tuf) + _download(url_to_repo, downloaded_file, using_tuf) except tuf.ExpiredMetadataError: msg = ('Metadata has expired too soon, extend expiration period. '+ 'Current expiration is set to: '+repr(EXPIRATION)+' second(s).') @@ -130,7 +130,7 @@ def test_indefinite_freeze_attack(using_tuf=False): # Try downloading again, this should raise an error. try: - _download(url=url_to_repo, filename=downloaded_file, using_tuf) + _download(url_to_repo, downloaded_file, using_tuf) except tuf.ExpiredMetadataError, error: pass else: diff --git a/tests/integration/test_mix_and_match_attack.py b/tests/integration/test_mix_and_match_attack.py index 9ebc6340..fad46ade 100755 --- a/tests/integration/test_mix_and_match_attack.py +++ b/tests/integration/test_mix_and_match_attack.py @@ -126,7 +126,7 @@ def test_mix_and_match_attack(using_tuf=False): # Client's initial download. - _download(url=url_to_file, filename=downloaded_file, using_tuf) + _download(url_to_file, downloaded_file, using_tuf) # Stage 2 # ------- @@ -139,7 +139,7 @@ def test_mix_and_match_attack(using_tuf=False): util_test_tools.tuf_refresh_repo(root_repo, keyids) # Client downloads the patched file. - _download(url=url_to_file, filename=downloaded_file, using_tuf) + _download(url_to_file, downloaded_file, using_tuf) downloaded_content = util_test_tools.read_file_content(downloaded_file) @@ -163,7 +163,7 @@ def test_mix_and_match_attack(using_tuf=False): # Client tries to downloads the newly patched file. try: - _download(url=url_to_file, filename=downloaded_file, using_tuf) + _download(url_to_file, downloaded_file, using_tuf) except tuf.MetadataNotAvailableError: pass diff --git a/tests/integration/test_replay_attack.py b/tests/integration/test_replay_attack.py index 746a47e0..504b6165 100755 --- a/tests/integration/test_replay_attack.py +++ b/tests/integration/test_replay_attack.py @@ -38,7 +38,7 @@ import urllib import tempfile -import tuf.interposition.urllib_tuf as urllib_tuf +import tuf.interposition import tuf.tests.util_test_tools as util_test_tools @@ -54,7 +54,7 @@ class ReplayAttackAlert(Exception): def _download(url, filename, using_tuf=False): if using_tuf: - urllib_tuf.urlretrieve(url, filename) + tuf.interposition.urllib_tuf.urlretrieve(url, filename) else: urllib.urlretrieve(url, filename) @@ -114,7 +114,7 @@ def test_replay_attack(using_tuf=False): # Client performs initial update. - _download(url=url_to_repo, filename=downloaded_file, using_tuf) + _download(url_to_repo, downloaded_file, using_tuf) # Downloads are stored in the same directory '{root_repo}/downloads/' # for regular and tuf clients. @@ -132,7 +132,7 @@ def test_replay_attack(using_tuf=False): # Client downloads the patched file. - _download(url=url_to_repo, filename=downloaded_file, using_tuf) + _download(url_to_repo, downloaded_file, using_tuf) # Content of the downloaded file. downloaded_content = util_test_tools.read_file_content(downloaded_file) @@ -157,7 +157,7 @@ def test_replay_attack(using_tuf=False): # Client downloads the file once more. - _download(url=url_to_repo, filename=downloaded_file, using_tuf) + _download(url_to_repo, downloaded_file, using_tuf) # Check whether the attack succeeded by inspecting the content of the # update. The update should contain 'Test NOT A'. diff --git a/tests/integration/test_slow_retrieval_attack.py b/tests/integration/test_slow_retrieval_attack.py index cee4234c..7596cbb8 100755 --- a/tests/integration/test_slow_retrieval_attack.py +++ b/tests/integration/test_slow_retrieval_attack.py @@ -49,7 +49,7 @@ import urllib -import tuf.interposition.urllib_tuf as urllib_tuf +import tuf.interposition import tuf.tests.util_test_tools as util_test_tools @@ -60,7 +60,7 @@ class SlowRetrievalAttackAlert(Exception): def _download(url, filename, using_tuf=False): if using_tuf: try: - urllib_tuf.urlretrieve(url, filename) + tuf.interposition.urllib_tuf.urlretrieve(url, filename) except tuf.NoWorkingMirrorError, exception: slow_retrieval = False for mirror_url, mirror_error in exception.mirror_errors.iteritems(): @@ -124,7 +124,7 @@ def test_slow_retrieval_attack(using_tuf=False, mode=None): # Client tries to download. # NOTE: if TUF is enabled the metadata files will be downloaded first. - proc = Process(target=_download, args=(url_to_file, downloaded_file, TUF)) + proc = Process(target=_download, args=(url_to_file, downloaded_file, using_tuf)) proc.start() proc.join(WAIT_TIME) From edec089965f461460a23c1765caa52d3ede6a63b Mon Sep 17 00:00:00 2001 From: dachshund Date: Fri, 13 Sep 2013 17:39:57 -0400 Subject: [PATCH 09/95] Fix variable shadow bug, use absolute import in two integration tests. --- tests/integration/test_endless_data_attack.py | 157 +++++++++--------- .../integration/test_slow_retrieval_attack.py | 6 +- 2 files changed, 80 insertions(+), 83 deletions(-) diff --git a/tests/integration/test_endless_data_attack.py b/tests/integration/test_endless_data_attack.py index 3f1e0ac8..b5d9c958 100755 --- a/tests/integration/test_endless_data_attack.py +++ b/tests/integration/test_endless_data_attack.py @@ -37,7 +37,7 @@ import urllib import tuf -import tuf.interposition.urllib_tuf as urllib_tuf +import tuf.interposition import tuf.tests.util_test_tools as util_test_tools @@ -49,13 +49,13 @@ class EndlessDataAttack(Exception): def _download(url, filename, TUF=False): if TUF: - urllib_tuf.urlretrieve(url, filename) + tuf.interposition.urllib_tuf.urlretrieve(url, filename) else: urllib.urlretrieve(url, filename) -def test_arbitrary_package_attack(TUF=False, TIMESTAMP=False): +def test_endless_data_attack(TUF=False, TIMESTAMP=False): """ TUF: @@ -69,105 +69,102 @@ def test_arbitrary_package_attack(TUF=False, TIMESTAMP=False): ERROR_MSG = 'Endless Data Attack was Successful!\n' + # Setup. + root_repo, url, server_proc, keyids = util_test_tools.init_repo(using_tuf=TUF) + reg_repo = os.path.join(root_repo, 'reg_repo') + tuf_repo = os.path.join(root_repo, 'tuf_repo') + downloads = os.path.join(root_repo, 'downloads') + tuf_targets = os.path.join(tuf_repo, 'targets') - try: - # Setup. - root_repo, url, server_proc, keyids = util_test_tools.init_repo(tuf=TUF) - reg_repo = os.path.join(root_repo, 'reg_repo') - tuf_repo = os.path.join(root_repo, 'tuf_repo') - downloads = os.path.join(root_repo, 'downloads') - tuf_targets = os.path.join(tuf_repo, 'targets') + # Original data. + INTENDED_DATA = 'Test A' - # Original data. - INTENDED_DATA = 'Test A' - - # Add a file to 'repo' directory: {root_repo} - filepath = util_test_tools.add_file_to_repository(reg_repo, INTENDED_DATA) - file_basename = os.path.basename(filepath) - url_to_repo = url+'reg_repo/'+file_basename - downloaded_file = os.path.join(downloads, file_basename) - # We do not deliver truly endless data, but we will extend the original - # file by many bytes. - noisy_data = 'X'*100000 + # Add a file to 'repo' directory: {root_repo} + filepath = util_test_tools.add_file_to_repository(reg_repo, INTENDED_DATA) + file_basename = os.path.basename(filepath) + url_to_repo = url+'reg_repo/'+file_basename + downloaded_file = os.path.join(downloads, file_basename) + # We do not deliver truly endless data, but we will extend the original + # file by many bytes. + noisy_data = 'X'*100000 - if TUF: - # Update TUF metadata before attacker modifies anything. - util_test_tools.tuf_refresh_repo(root_repo, keyids) - # Modify the url. Remember that the interposition will intercept - # urls that have 'localhost:9999' hostname, which was specified in - # the json interposition configuration file. Look for 'hostname' - # in 'util_test_tools.py'. Further, the 'file_basename' is the target - # path relative to 'targets_dir'. - url_to_repo = 'http://localhost:9999/'+file_basename + if TUF: + # Update TUF metadata before attacker modifies anything. + util_test_tools.tuf_refresh_repo(root_repo, keyids) + # Modify the url. Remember that the interposition will intercept + # urls that have 'localhost:9999' hostname, which was specified in + # the json interposition configuration file. Look for 'hostname' + # in 'util_test_tools.py'. Further, the 'file_basename' is the target + # path relative to 'targets_dir'. + url_to_repo = 'http://localhost:9999/'+file_basename - # Attacker modifies the file at the targets repository. - target = os.path.join(tuf_targets, file_basename) - original_data = util_test_tools.read_file_content(target) - larger_original_data = original_data + noisy_data - util_test_tools.modify_file_at_repository(target, larger_original_data) - - # Attacker modifies the timestamp.txt metadata. - if TIMESTAMP: - metadata = os.path.join(tuf_repo, 'metadata') - timestamp = os.path.join(metadata, 'timestamp.txt') - original_data = util_test_tools.read_file_content(timestamp) - larger_original_data = original_data + noisy_data - util_test_tools.modify_file_at_repository(timestamp, - larger_original_data) - - # Attacker modifies the file at the regular repository. - original_data = util_test_tools.read_file_content(filepath) + # Attacker modifies the file at the targets repository. + target = os.path.join(tuf_targets, file_basename) + original_data = util_test_tools.read_file_content(target) larger_original_data = original_data + noisy_data - util_test_tools.modify_file_at_repository(filepath, larger_original_data) + util_test_tools.modify_file_at_repository(target, larger_original_data) - # End Setup. + # Attacker modifies the timestamp.txt metadata. + if TIMESTAMP: + metadata = os.path.join(tuf_repo, 'metadata') + timestamp = os.path.join(metadata, 'timestamp.txt') + original_data = util_test_tools.read_file_content(timestamp) + larger_original_data = original_data + noisy_data + util_test_tools.modify_file_at_repository(timestamp, + larger_original_data) + + # Attacker modifies the file at the regular repository. + original_data = util_test_tools.read_file_content(filepath) + larger_original_data = original_data + noisy_data + util_test_tools.modify_file_at_repository(filepath, larger_original_data) + + # End Setup. - # Client downloads (tries to download) the file. - try: - _download(url=url_to_repo, filename=downloaded_file, TUF=TUF) - except Exception, exception: - # Because we are extending the true timestamp TUF metadata with invalid - # JSON, we except to catch an error about invalid metadata JSON. - if TUF and TIMESTAMP: - endless_data_attack = False + # Client downloads (tries to download) the file. + try: + _download(url=url_to_repo, filename=downloaded_file, TUF=TUF) + except Exception, exception: + # Because we are extending the true timestamp TUF metadata with invalid + # JSON, we except to catch an error about invalid metadata JSON. + if TUF and TIMESTAMP: + endless_data_attack = False - for mirror_url, mirror_error in exception.mirror_errors.iteritems(): - if isinstance(mirror_error, tuf.InvalidMetadataJSONError): - endless_data_attack = True - break + for mirror_url, mirror_error in exception.mirror_errors.iteritems(): + if isinstance(mirror_error, tuf.InvalidMetadataJSONError): + endless_data_attack = True + break - # In case we did not detect what was likely an endless data attack, we - # reraise the exception to indicate that endless data attack detection - # failed. - if not endless_data_attack: raise - else: raise + # In case we did not detect what was likely an endless data attack, we + # reraise the exception to indicate that endless data attack detection + # failed. + if not endless_data_attack: raise + else: raise - # When we test downloading "endless" timestamp with TUF, we want to skip - # the following test because downloading the timestamp should have failed. - if not (TUF and TIMESTAMP): - # Check whether the attack succeeded by inspecting the content of the - # update. The update should contain 'Test A'. Technically it suffices - # to check whether the file was downloaded or not. - downloaded_content = util_test_tools.read_file_content(downloaded_file) - if downloaded_content != INTENDED_DATA: - raise EndlessDataAttack(ERROR_MSG) + # When we test downloading "endless" timestamp with TUF, we want to skip + # the following test because downloading the timestamp should have failed. + if not (TUF and TIMESTAMP): + # Check whether the attack succeeded by inspecting the content of the + # update. The update should contain 'Test A'. Technically it suffices + # to check whether the file was downloaded or not. + downloaded_content = util_test_tools.read_file_content(downloaded_file) + if downloaded_content != INTENDED_DATA: + raise EndlessDataAttack(ERROR_MSG) - finally: - util_test_tools.cleanup(root_repo, server_proc) + util_test_tools.cleanup(root_repo, server_proc) try: - test_arbitrary_package_attack(TUF=False, TIMESTAMP=False) + test_endless_data_attack(TUF=False, TIMESTAMP=False) except EndlessDataAttack, error: print('Endless data attack worked on download without TUF!') try: - test_arbitrary_package_attack(TUF=True, TIMESTAMP=False) + test_endless_data_attack(TUF=True, TIMESTAMP=False) except EndlessDataAttack, error: print('Endless data attack worked on download without TUF!') print(str(error)) @@ -177,7 +174,7 @@ def test_arbitrary_package_attack(TUF=False, TIMESTAMP=False): try: # This test fails because the timestamp metadata has been extended with # random data from its true length, thereby resulting in invalid JSON. - test_arbitrary_package_attack(TUF=True, TIMESTAMP=True) + test_endless_data_attack(TUF=True, TIMESTAMP=True) except EndlessDataAttack, error: print('Endless data attack worked on download without TUF!') print(str(error)) diff --git a/tests/integration/test_slow_retrieval_attack.py b/tests/integration/test_slow_retrieval_attack.py index 9d515be2..36870499 100755 --- a/tests/integration/test_slow_retrieval_attack.py +++ b/tests/integration/test_slow_retrieval_attack.py @@ -49,7 +49,7 @@ import urllib -import tuf.interposition.urllib_tuf as urllib_tuf +import tuf.interposition import tuf.tests.util_test_tools as util_test_tools @@ -60,7 +60,7 @@ class SlowRetrievalAttackAlert(Exception): def _download(url, filename, TUF=False): if TUF: try: - urllib_tuf.urlretrieve(url, filename) + tuf.interposition.urllib_tuf.urlretrieve(url, filename) except tuf.NoWorkingMirrorError, exception: slow_retrieval = False for mirror_url, mirror_error in exception.mirror_errors.iteritems(): @@ -97,7 +97,7 @@ def test_slow_retrieval_attack(TUF=False, mode=None): try: # Setup. root_repo, url, server_proc, keyids = \ - util_test_tools.init_repo(tuf=TUF, port=port) + util_test_tools.init_repo(using_tuf=TUF, port=port) reg_repo = os.path.join(root_repo, 'reg_repo') downloads = os.path.join(root_repo, 'downloads') From 4c10490b1c56ad93747f72ddd78b7bb9c6ca5842 Mon Sep 17 00:00:00 2001 From: dachshund Date: Fri, 13 Sep 2013 17:59:31 -0400 Subject: [PATCH 10/95] Remove executable bits on some non-executable files. --- tuf/tests/repository_setup.py | 0 tuf/tests/unittest_toolbox.py | 0 2 files changed, 0 insertions(+), 0 deletions(-) mode change 100755 => 100644 tuf/tests/repository_setup.py mode change 100755 => 100644 tuf/tests/unittest_toolbox.py diff --git a/tuf/tests/repository_setup.py b/tuf/tests/repository_setup.py old mode 100755 new mode 100644 diff --git a/tuf/tests/unittest_toolbox.py b/tuf/tests/unittest_toolbox.py old mode 100755 new mode 100644 From f0258ea82cc1a4e621f511b39c50de46e96166f6 Mon Sep 17 00:00:00 2001 From: zhengyuyu Date: Fri, 13 Sep 2013 18:02:19 -0400 Subject: [PATCH 11/95] Fix test_delegations.py Made changes that ensure it raises correct exception based on the refactored version of updater.py and download.py --- tests/integration/test_delegations.py | 24 ++- tests/integration/test_endless_data_attack.py | 157 +++++++++--------- .../integration/test_slow_retrieval_attack.py | 6 +- tuf/tests/repository_setup.py | 0 tuf/tests/unittest_toolbox.py | 0 5 files changed, 101 insertions(+), 86 deletions(-) mode change 100755 => 100644 tuf/tests/repository_setup.py mode change 100755 => 100644 tuf/tests/unittest_toolbox.py diff --git a/tests/integration/test_delegations.py b/tests/integration/test_delegations.py index 94f84917..f7cb3695 100755 --- a/tests/integration/test_delegations.py +++ b/tests/integration/test_delegations.py @@ -107,7 +107,7 @@ def setUp(self): version = version+1 expiration = tuf.formats.format_time(time.time()+86400) - root_repo, url, server_proc, keyids = util_test_tools.init_repo(tuf=True) + root_repo, url, server_proc, keyids = util_test_tools.init_repo(using_tuf=True) # Server side repository. tuf_repo = os.path.join(root_repo, 'tuf_repo') @@ -338,9 +338,17 @@ def make_targets_metadata(self): def test_that_initial_update_fails_with_undelegated_signing_of_targets(self): # Expect to see a particular exception on initial update. - self.assertRaises(tuf.MetadataNotAvailableError, self.do_update) + with self.assertRaises(tuf.NoWorkingMirrorError) as contextManager: + self.do_update() + exception = contextManager.exception + ForbiddenTargetError = False + for mirror_url, mirror_error in exception.mirror_errors.iteritems(): + if isinstance(mirror_error, tuf.ForbiddenTargetError): + ForbiddenTargetError = True + break + self.assertEqual(ForbiddenTargetError, True) @@ -456,7 +464,17 @@ def make_targets_metadata(self): def test_that_initial_update_fails_with_many_roles_sharing_a_target(self): # Expect to see a particular exception on initial update. - self.assertRaises(tuf.DownloadError, self.do_update) + with self.assertRaises(tuf.NoWorkingMirrorError) as contextManager: + self.do_update() + + exception = contextManager.exception + BadHashError = False + for mirror_url, mirror_error in exception.mirror_errors.iteritems(): + if isinstance(mirror_error, tuf.BadHashError): + BadHashError = True + break + + self.assertEqual(BadHashError, True) diff --git a/tests/integration/test_endless_data_attack.py b/tests/integration/test_endless_data_attack.py index 3f1e0ac8..b5d9c958 100755 --- a/tests/integration/test_endless_data_attack.py +++ b/tests/integration/test_endless_data_attack.py @@ -37,7 +37,7 @@ import urllib import tuf -import tuf.interposition.urllib_tuf as urllib_tuf +import tuf.interposition import tuf.tests.util_test_tools as util_test_tools @@ -49,13 +49,13 @@ class EndlessDataAttack(Exception): def _download(url, filename, TUF=False): if TUF: - urllib_tuf.urlretrieve(url, filename) + tuf.interposition.urllib_tuf.urlretrieve(url, filename) else: urllib.urlretrieve(url, filename) -def test_arbitrary_package_attack(TUF=False, TIMESTAMP=False): +def test_endless_data_attack(TUF=False, TIMESTAMP=False): """ TUF: @@ -69,105 +69,102 @@ def test_arbitrary_package_attack(TUF=False, TIMESTAMP=False): ERROR_MSG = 'Endless Data Attack was Successful!\n' + # Setup. + root_repo, url, server_proc, keyids = util_test_tools.init_repo(using_tuf=TUF) + reg_repo = os.path.join(root_repo, 'reg_repo') + tuf_repo = os.path.join(root_repo, 'tuf_repo') + downloads = os.path.join(root_repo, 'downloads') + tuf_targets = os.path.join(tuf_repo, 'targets') - try: - # Setup. - root_repo, url, server_proc, keyids = util_test_tools.init_repo(tuf=TUF) - reg_repo = os.path.join(root_repo, 'reg_repo') - tuf_repo = os.path.join(root_repo, 'tuf_repo') - downloads = os.path.join(root_repo, 'downloads') - tuf_targets = os.path.join(tuf_repo, 'targets') + # Original data. + INTENDED_DATA = 'Test A' - # Original data. - INTENDED_DATA = 'Test A' - - # Add a file to 'repo' directory: {root_repo} - filepath = util_test_tools.add_file_to_repository(reg_repo, INTENDED_DATA) - file_basename = os.path.basename(filepath) - url_to_repo = url+'reg_repo/'+file_basename - downloaded_file = os.path.join(downloads, file_basename) - # We do not deliver truly endless data, but we will extend the original - # file by many bytes. - noisy_data = 'X'*100000 + # Add a file to 'repo' directory: {root_repo} + filepath = util_test_tools.add_file_to_repository(reg_repo, INTENDED_DATA) + file_basename = os.path.basename(filepath) + url_to_repo = url+'reg_repo/'+file_basename + downloaded_file = os.path.join(downloads, file_basename) + # We do not deliver truly endless data, but we will extend the original + # file by many bytes. + noisy_data = 'X'*100000 - if TUF: - # Update TUF metadata before attacker modifies anything. - util_test_tools.tuf_refresh_repo(root_repo, keyids) - # Modify the url. Remember that the interposition will intercept - # urls that have 'localhost:9999' hostname, which was specified in - # the json interposition configuration file. Look for 'hostname' - # in 'util_test_tools.py'. Further, the 'file_basename' is the target - # path relative to 'targets_dir'. - url_to_repo = 'http://localhost:9999/'+file_basename + if TUF: + # Update TUF metadata before attacker modifies anything. + util_test_tools.tuf_refresh_repo(root_repo, keyids) + # Modify the url. Remember that the interposition will intercept + # urls that have 'localhost:9999' hostname, which was specified in + # the json interposition configuration file. Look for 'hostname' + # in 'util_test_tools.py'. Further, the 'file_basename' is the target + # path relative to 'targets_dir'. + url_to_repo = 'http://localhost:9999/'+file_basename - # Attacker modifies the file at the targets repository. - target = os.path.join(tuf_targets, file_basename) - original_data = util_test_tools.read_file_content(target) - larger_original_data = original_data + noisy_data - util_test_tools.modify_file_at_repository(target, larger_original_data) - - # Attacker modifies the timestamp.txt metadata. - if TIMESTAMP: - metadata = os.path.join(tuf_repo, 'metadata') - timestamp = os.path.join(metadata, 'timestamp.txt') - original_data = util_test_tools.read_file_content(timestamp) - larger_original_data = original_data + noisy_data - util_test_tools.modify_file_at_repository(timestamp, - larger_original_data) - - # Attacker modifies the file at the regular repository. - original_data = util_test_tools.read_file_content(filepath) + # Attacker modifies the file at the targets repository. + target = os.path.join(tuf_targets, file_basename) + original_data = util_test_tools.read_file_content(target) larger_original_data = original_data + noisy_data - util_test_tools.modify_file_at_repository(filepath, larger_original_data) + util_test_tools.modify_file_at_repository(target, larger_original_data) - # End Setup. + # Attacker modifies the timestamp.txt metadata. + if TIMESTAMP: + metadata = os.path.join(tuf_repo, 'metadata') + timestamp = os.path.join(metadata, 'timestamp.txt') + original_data = util_test_tools.read_file_content(timestamp) + larger_original_data = original_data + noisy_data + util_test_tools.modify_file_at_repository(timestamp, + larger_original_data) + + # Attacker modifies the file at the regular repository. + original_data = util_test_tools.read_file_content(filepath) + larger_original_data = original_data + noisy_data + util_test_tools.modify_file_at_repository(filepath, larger_original_data) + + # End Setup. - # Client downloads (tries to download) the file. - try: - _download(url=url_to_repo, filename=downloaded_file, TUF=TUF) - except Exception, exception: - # Because we are extending the true timestamp TUF metadata with invalid - # JSON, we except to catch an error about invalid metadata JSON. - if TUF and TIMESTAMP: - endless_data_attack = False + # Client downloads (tries to download) the file. + try: + _download(url=url_to_repo, filename=downloaded_file, TUF=TUF) + except Exception, exception: + # Because we are extending the true timestamp TUF metadata with invalid + # JSON, we except to catch an error about invalid metadata JSON. + if TUF and TIMESTAMP: + endless_data_attack = False - for mirror_url, mirror_error in exception.mirror_errors.iteritems(): - if isinstance(mirror_error, tuf.InvalidMetadataJSONError): - endless_data_attack = True - break + for mirror_url, mirror_error in exception.mirror_errors.iteritems(): + if isinstance(mirror_error, tuf.InvalidMetadataJSONError): + endless_data_attack = True + break - # In case we did not detect what was likely an endless data attack, we - # reraise the exception to indicate that endless data attack detection - # failed. - if not endless_data_attack: raise - else: raise + # In case we did not detect what was likely an endless data attack, we + # reraise the exception to indicate that endless data attack detection + # failed. + if not endless_data_attack: raise + else: raise - # When we test downloading "endless" timestamp with TUF, we want to skip - # the following test because downloading the timestamp should have failed. - if not (TUF and TIMESTAMP): - # Check whether the attack succeeded by inspecting the content of the - # update. The update should contain 'Test A'. Technically it suffices - # to check whether the file was downloaded or not. - downloaded_content = util_test_tools.read_file_content(downloaded_file) - if downloaded_content != INTENDED_DATA: - raise EndlessDataAttack(ERROR_MSG) + # When we test downloading "endless" timestamp with TUF, we want to skip + # the following test because downloading the timestamp should have failed. + if not (TUF and TIMESTAMP): + # Check whether the attack succeeded by inspecting the content of the + # update. The update should contain 'Test A'. Technically it suffices + # to check whether the file was downloaded or not. + downloaded_content = util_test_tools.read_file_content(downloaded_file) + if downloaded_content != INTENDED_DATA: + raise EndlessDataAttack(ERROR_MSG) - finally: - util_test_tools.cleanup(root_repo, server_proc) + util_test_tools.cleanup(root_repo, server_proc) try: - test_arbitrary_package_attack(TUF=False, TIMESTAMP=False) + test_endless_data_attack(TUF=False, TIMESTAMP=False) except EndlessDataAttack, error: print('Endless data attack worked on download without TUF!') try: - test_arbitrary_package_attack(TUF=True, TIMESTAMP=False) + test_endless_data_attack(TUF=True, TIMESTAMP=False) except EndlessDataAttack, error: print('Endless data attack worked on download without TUF!') print(str(error)) @@ -177,7 +174,7 @@ def test_arbitrary_package_attack(TUF=False, TIMESTAMP=False): try: # This test fails because the timestamp metadata has been extended with # random data from its true length, thereby resulting in invalid JSON. - test_arbitrary_package_attack(TUF=True, TIMESTAMP=True) + test_endless_data_attack(TUF=True, TIMESTAMP=True) except EndlessDataAttack, error: print('Endless data attack worked on download without TUF!') print(str(error)) diff --git a/tests/integration/test_slow_retrieval_attack.py b/tests/integration/test_slow_retrieval_attack.py index 9d515be2..36870499 100755 --- a/tests/integration/test_slow_retrieval_attack.py +++ b/tests/integration/test_slow_retrieval_attack.py @@ -49,7 +49,7 @@ import urllib -import tuf.interposition.urllib_tuf as urllib_tuf +import tuf.interposition import tuf.tests.util_test_tools as util_test_tools @@ -60,7 +60,7 @@ class SlowRetrievalAttackAlert(Exception): def _download(url, filename, TUF=False): if TUF: try: - urllib_tuf.urlretrieve(url, filename) + tuf.interposition.urllib_tuf.urlretrieve(url, filename) except tuf.NoWorkingMirrorError, exception: slow_retrieval = False for mirror_url, mirror_error in exception.mirror_errors.iteritems(): @@ -97,7 +97,7 @@ def test_slow_retrieval_attack(TUF=False, mode=None): try: # Setup. root_repo, url, server_proc, keyids = \ - util_test_tools.init_repo(tuf=TUF, port=port) + util_test_tools.init_repo(using_tuf=TUF, port=port) reg_repo = os.path.join(root_repo, 'reg_repo') downloads = os.path.join(root_repo, 'downloads') diff --git a/tuf/tests/repository_setup.py b/tuf/tests/repository_setup.py old mode 100755 new mode 100644 diff --git a/tuf/tests/unittest_toolbox.py b/tuf/tests/unittest_toolbox.py old mode 100755 new mode 100644 From 3cf472757abc5201898b29e64f96e042108b50aa Mon Sep 17 00:00:00 2001 From: ttgump Date: Fri, 13 Sep 2013 19:14:32 -0400 Subject: [PATCH 12/95] test_mix_and_match_attack and test_indefinite_freeze_attack refactory fix --- .../test_indefinite_freeze_attack.py | 40 +++++++++---------- .../integration/test_mix_and_match_attack.py | 20 +++++----- 2 files changed, 30 insertions(+), 30 deletions(-) diff --git a/tests/integration/test_indefinite_freeze_attack.py b/tests/integration/test_indefinite_freeze_attack.py index b422fae5..2b88e00c 100755 --- a/tests/integration/test_indefinite_freeze_attack.py +++ b/tests/integration/test_indefinite_freeze_attack.py @@ -28,7 +28,7 @@ import tuf import tuf.formats -import tuf.interposition.urllib_tuf as urllib_tuf +import tuf.interposition import tuf.repo.signerlib as signerlib import tuf.tests.util_test_tools as util_test_tools @@ -61,9 +61,9 @@ def _remake_timestamp(metadata_dir, keyids): -def _download(url, filename, tuf=False): - if tuf: - urllib_tuf.urlretrieve(url, filename) +def _download(url, filename, using_tuf=False): + if using_tuf: + tuf.interposition.urllib_tuf.urlretrieve(url, filename) else: urllib.urlretrieve(url, filename) @@ -117,26 +117,24 @@ def test_indefinite_freeze_attack(TUF=False): _remake_timestamp(metadata_dir, keyids) - # Client performs initial download. + # Client performs initial download. If the computer is slow, it may + # take longer time than expiration time. In this case you will see + # an ExpiredMetadataError. try: - _download(url=url_to_repo, filename=downloaded_file, tuf=TUF) - except tuf.ExpiredMetadataError: - msg = ('Metadata has expired too soon, extend expiration period. '+ - 'Current expiration is set to: '+repr(EXPIRATION)+' second(s).') - sys.exit(msg) - - # Expire timestamp. - time.sleep(EXPIRATION) - - # Try downloading again, this should raise an error. - try: - _download(url=url_to_repo, filename=downloaded_file, tuf=TUF) - except tuf.ExpiredMetadataError, error: - pass + _download(url=url_to_repo, filename=downloaded_file, using_tuf=TUF) + except: + print ('Initial download failed! It may be because your machine is busy. Try it later.') else: - raise IndefiniteFreezeAttackAlert(ERROR_MSG) - + # Expire timestamp. + time.sleep(EXPIRATION) + # Try downloading again, this should raise an error. + try: + _download(url=url_to_repo, filename=downloaded_file, using_tuf=TUF) + except tuf.ExpiredMetadataError, error: + print('Caught an expiration error!') + else: + raise IndefiniteFreezeAttackAlert(ERROR_MSG) finally: util_test_tools.cleanup(root_repo, server_proc) diff --git a/tests/integration/test_mix_and_match_attack.py b/tests/integration/test_mix_and_match_attack.py index da8e170c..88cbaeb8 100755 --- a/tests/integration/test_mix_and_match_attack.py +++ b/tests/integration/test_mix_and_match_attack.py @@ -40,7 +40,7 @@ import tempfile import tuf -import tuf.interposition.urllib_tuf as urllib_tuf +import tuf.interposition import tuf.tests.util_test_tools as util_test_tools @@ -48,9 +48,9 @@ class MixAndMatchAttackAlert(Exception): pass -def _download(url, filename, tuf=False): - if tuf: - urllib_tuf.urlretrieve(url, filename) +def _download(url, filename, using_tuf=False): + if using_tuf: + tuf.interposition.urllib_tuf.urlretrieve(url, filename) else: urllib.urlretrieve(url, filename) @@ -126,7 +126,7 @@ def test_mix_and_match_attack(TUF=False): # Client's initial download. - _download(url=url_to_file, filename=downloaded_file, tuf=TUF) + _download(url=url_to_file, filename=downloaded_file, using_tuf=TUF) # Stage 2 # ------- @@ -139,7 +139,7 @@ def test_mix_and_match_attack(TUF=False): util_test_tools.tuf_refresh_repo(root_repo, keyids) # Client downloads the patched file. - _download(url=url_to_file, filename=downloaded_file, tuf=TUF) + _download(url=url_to_file, filename=downloaded_file, using_tuf=TUF) downloaded_content = util_test_tools.read_file_content(downloaded_file) @@ -163,9 +163,11 @@ def test_mix_and_match_attack(TUF=False): # Client tries to downloads the newly patched file. try: - _download(url=url_to_file, filename=downloaded_file, tuf=TUF) - except tuf.MetadataNotAvailableError: - pass + _download(url=url_to_file, filename=downloaded_file, using_tuf=TUF) + except tuf.NoWorkingMirrorError as errors: + for mirror_url, mirror_error in errors.mirror_errors.iteritems(): + if type(mirror_error) == tuf.BadHashError: + print 'Catched a Bad Hash Error!' # Check whether the attack succeeded by inspecting the content of the # update. The update should contain 'Test NOT A'. From d2b202bc6152336f4c39ce66d20f9cfdcd3fd51b Mon Sep 17 00:00:00 2001 From: dachshund Date: Fri, 13 Sep 2013 23:47:37 -0400 Subject: [PATCH 13/95] Simplify exception text in console handler. --- tuf/__init__.py | 6 +++--- tuf/client/updater.py | 6 +++--- tuf/log.py | 7 +++++-- 3 files changed, 11 insertions(+), 8 deletions(-) diff --git a/tuf/__init__.py b/tuf/__init__.py index 503e4fcf..13964980 100755 --- a/tuf/__init__.py +++ b/tuf/__init__.py @@ -134,9 +134,9 @@ def __init__(self, metadata_role, previous_version, current_version): def __str__(self): - return str(self.metadata_role)+' is older than the version currently'+\ - 'installed.\nDownloaded version: '+repr(self.previous_version)+'\n'+\ - 'Current version: '+repr(self.current_version) + return 'Downloaded '+str(self.metadata_role)+' is older ('+\ + str(self.previous_version)+') than the version currently '+\ + 'installed ('+repr(self.current_version)+').' diff --git a/tuf/client/updater.py b/tuf/client/updater.py index 1d924f8c..a10a10e7 100755 --- a/tuf/client/updater.py +++ b/tuf/client/updater.py @@ -1021,7 +1021,7 @@ def __get_file(self, filepath, verify_uncompressed_file, file_type, except Exception, exception: # Remember the error from this mirror, and "reset" the target file. - logger.exception('Download failed from '+file_mirror+'.') + logger.exception('Update failed from '+file_mirror+'.') file_mirror_errors[file_mirror] = exception file_object = None else: @@ -1030,8 +1030,8 @@ def __get_file(self, filepath, verify_uncompressed_file, file_type, if file_object: return file_object else: - logger.exception('Failed to download {0}: {1}'.format(filepath, - file_mirror_errors)) + logger.exception('Failed to update {0} from all mirrors: {1}'.format( + filepath, file_mirror_errors)) raise tuf.NoWorkingMirrorError(file_mirror_errors) diff --git a/tuf/log.py b/tuf/log.py index 7987ec94..347cca63 100755 --- a/tuf/log.py +++ b/tuf/log.py @@ -141,8 +141,11 @@ def filter(self, record): # most useful for the console handler, which we do not wish to deluge # with too much data. Assuming that this filter is not applied to the # file logging handler, the user may always consult the file log for the - # original exception traceback. - record.exc_text = str(record.exc_info) + # original exception traceback. The exc_info is explained here: + # http://docs.python.org/2/library/sys.html#sys.exc_info + exc_type, exc_value, exc_traceback = record.exc_info + # Simply set the class name as the exception text. + record.exc_text = exc_type.__name__ # Always return True to signal that any given record must be formatted. return True From 6d7d645f23d4dfdfae059d5288ff3a6b4a44e010 Mon Sep 17 00:00:00 2001 From: dachshund Date: Sat, 14 Sep 2013 00:07:26 -0400 Subject: [PATCH 14/95] Add a log message. --- tuf/client/updater.py | 1 + 1 file changed, 1 insertion(+) diff --git a/tuf/client/updater.py b/tuf/client/updater.py index a10a10e7..fc1f9d3c 100755 --- a/tuf/client/updater.py +++ b/tuf/client/updater.py @@ -1744,6 +1744,7 @@ def _ensure_not_expired(self, metadata_role): # returned by time.time() (i.e., current time), before comparing. if tuf.formats.parse_time(expires) < time.time(): message = 'Metadata '+repr(rolepath)+' expired on '+repr(expires)+'.' + logger.error(message) raise tuf.ExpiredMetadataError(message) From cd805e528de2b16b8b160950ec3552be80c5653a Mon Sep 17 00:00:00 2001 From: ttgump Date: Sat, 14 Sep 2013 12:34:01 -0700 Subject: [PATCH 15/95] indention fix --- tests/integration/test_mix_and_match_attack.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/integration/test_mix_and_match_attack.py b/tests/integration/test_mix_and_match_attack.py index 88cbaeb8..da7d1dfa 100755 --- a/tests/integration/test_mix_and_match_attack.py +++ b/tests/integration/test_mix_and_match_attack.py @@ -166,8 +166,8 @@ def test_mix_and_match_attack(TUF=False): _download(url=url_to_file, filename=downloaded_file, using_tuf=TUF) except tuf.NoWorkingMirrorError as errors: for mirror_url, mirror_error in errors.mirror_errors.iteritems(): - if type(mirror_error) == tuf.BadHashError: - print 'Catched a Bad Hash Error!' + if type(mirror_error) == tuf.BadHashError: + print 'Catched a Bad Hash Error!' # Check whether the attack succeeded by inspecting the content of the # update. The update should contain 'Test NOT A'. From 4c2f87887068fb6a34e6ec5a28aa384b9fcd5ddf Mon Sep 17 00:00:00 2001 From: ttgump Date: Sat, 14 Sep 2013 15:46:25 -0400 Subject: [PATCH 16/95] util_test_tools.init_repo parameter fix --- tests/integration/test_indefinite_freeze_attack.py | 2 +- tests/integration/test_mix_and_match_attack.py | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/tests/integration/test_indefinite_freeze_attack.py b/tests/integration/test_indefinite_freeze_attack.py index 2b88e00c..bb1a4a8c 100755 --- a/tests/integration/test_indefinite_freeze_attack.py +++ b/tests/integration/test_indefinite_freeze_attack.py @@ -88,7 +88,7 @@ def test_indefinite_freeze_attack(TUF=False): try: # Setup. - root_repo, url, server_proc, keyids = util_test_tools.init_repo(tuf=TUF) + root_repo, url, server_proc, keyids = util_test_tools.init_repo(using_tuf=TUF) reg_repo = os.path.join(root_repo, 'reg_repo') tuf_repo = os.path.join(root_repo, 'tuf_repo') metadata_dir = os.path.join(tuf_repo, 'metadata') diff --git a/tests/integration/test_mix_and_match_attack.py b/tests/integration/test_mix_and_match_attack.py index da7d1dfa..5bf98db9 100755 --- a/tests/integration/test_mix_and_match_attack.py +++ b/tests/integration/test_mix_and_match_attack.py @@ -81,7 +81,7 @@ def test_mix_and_match_attack(TUF=False): try: # Setup / Stage 1 # --------------- - root_repo, url, server_proc, keyids = util_test_tools.init_repo(tuf=TUF) + root_repo, url, server_proc, keyids = util_test_tools.init_repo(using_tuf=TUF) reg_repo = os.path.join(root_repo, 'reg_repo') downloads = os.path.join(root_repo, 'downloads') evil_dir = tempfile.mkdtemp(dir=root_repo) @@ -166,8 +166,8 @@ def test_mix_and_match_attack(TUF=False): _download(url=url_to_file, filename=downloaded_file, using_tuf=TUF) except tuf.NoWorkingMirrorError as errors: for mirror_url, mirror_error in errors.mirror_errors.iteritems(): - if type(mirror_error) == tuf.BadHashError: - print 'Catched a Bad Hash Error!' + if type(mirror_error) == tuf.BadHashError: + print 'Catched a Bad Hash Error!' # Check whether the attack succeeded by inspecting the content of the # update. The update should contain 'Test NOT A'. From 284998fc643e93cca9de856224740c22cf6531f7 Mon Sep 17 00:00:00 2001 From: zanefisher Date: Sat, 14 Sep 2013 16:47:04 -0400 Subject: [PATCH 17/95] Bring back tuf/log.py from edec089965f461460a23c1765caa52d3ede6a63b. --- tuf/log.py | 101 ++++++++++++++++++++++++++++++++++++++++++++++------- 1 file changed, 89 insertions(+), 12 deletions(-) diff --git a/tuf/log.py b/tuf/log.py index 0667e4ad..7987ec94 100755 --- a/tuf/log.py +++ b/tuf/log.py @@ -73,14 +73,16 @@ '[%(funcName)s:%(lineno)s@%(filename)s] %(message)s' +# Ask all Formatter instances to talk GMT. +# http://docs.python.org/2/library/logging.html#logging.Formatter.formatException logging.Formatter.converter = time.gmtime formatter = logging.Formatter(_FORMAT_STRING) -# Set the handlers for the logger. The console handler is unset by default. A +# Set the handlers for the logger. The console handler is unset by default. A # module importing 'log.py' should explicitly set the console handler if -# outputting log messages to the screen is needed. Adding a console handler -# can be done with tuf.log.add_console_handler(). Logging messages to a file -# *is* set by default. +# outputting log messages to the screen is needed. Adding a console handler can +# be done with tuf.log.add_console_handler(). Logging messages to a file *is* +# set by default. console_handler = None # Set the built-in file handler. Messages will be logged to @@ -104,6 +106,51 @@ +class ConsoleFilter(logging.Filter): + def filter(self, record): + """ + + Use Vinay Sajip's recommendation from Python issue #6435 to modify a + LogRecord object. This is meant to be used with our console handler. + + http://stackoverflow.com/q/6177520 + http://stackoverflow.com/q/5875225 + http://bugs.python.org/issue6435 + http://docs.python.org/2/howto/logging-cookbook.html#filters-contextual + http://docs.python.org/2/library/logging.html#logrecord-attributes + + + record: + A logging.LogRecord object. + + + None. + + + Replaces the LogRecord exception text attribute. + + + True. + + """ + + # If this LogRecord object has an exception, then we will replace its text. + if record.exc_info: + # We place the record's cached exception text (which usually contains the + # exception traceback) with much simpler exception information. This is + # most useful for the console handler, which we do not wish to deluge + # with too much data. Assuming that this filter is not applied to the + # file logging handler, the user may always consult the file log for the + # original exception traceback. + record.exc_text = str(record.exc_info) + + # Always return True to signal that any given record must be formatted. + return True + + + + + def set_log_level(log_level=_DEFAULT_LOG_LEVEL): """ @@ -200,7 +247,6 @@ def set_console_log_level(log_level=_DEFAULT_CONSOLE_LOG_LEVEL): - def add_console_handler(log_level=_DEFAULT_CONSOLE_LOG_LEVEL): """ @@ -222,15 +268,46 @@ def add_console_handler(log_level=_DEFAULT_CONSOLE_LOG_LEVEL): None. """ - + # Assign to the global console_handler object. + global console_handler + # Does 'log_level' have the correct format? # Raise 'tuf.FormatError' if there is a mismatch. tuf.formats.LENGTH_SCHEMA.check_match(log_level) - # Set the console handler for the logger. The built-in console handler will - # log messages to 'sys.stderr' and capture 'log_level' messages. + if not console_handler: + # Set the console handler for the logger. The built-in console handler will + # log messages to 'sys.stderr' and capture 'log_level' messages. + # NOTE: This is not thread-safe. + console_handler = logging.StreamHandler() + # Get our filter for the console handler. + console_filter = ConsoleFilter() + + console_handler.setLevel(log_level) + console_handler.setFormatter(formatter) + console_handler.addFilter(console_filter) + logger.addHandler(console_handler) + logger.debug('Added a console handler.') + else: + logger.warn('We already have a console handler.') + + + + + +def remove_console_handler(): + # Assign to the global console_handler object. global console_handler - console_handler = logging.StreamHandler() - console_handler.setLevel(log_level) - console_handler.setFormatter(formatter) - logger.addHandler(console_handler) + + if console_handler: + logger.removeHandler(console_handler) + # NOTE: This is not thread-safe. + console_handler = None + logger.debug('Removed a console handler.') + else: + logger.warn('We do not have a console handler.') + + + + + From f3a8f60583a2b86394c8471f17c4cb4dac4a23b3 Mon Sep 17 00:00:00 2001 From: zanefisher Date: Sat, 14 Sep 2013 16:58:01 -0400 Subject: [PATCH 18/95] Clean up after botched merge (oops). Add try/finally to endless data test to ensure temp files are always removed. --- .../test_arbitrary_package_attack.py | 7 +- tests/integration/test_endless_data_attack.py | 172 +++++++++--------- 2 files changed, 90 insertions(+), 89 deletions(-) diff --git a/tests/integration/test_arbitrary_package_attack.py b/tests/integration/test_arbitrary_package_attack.py index 1854e34a..91953986 100755 --- a/tests/integration/test_arbitrary_package_attack.py +++ b/tests/integration/test_arbitrary_package_attack.py @@ -56,14 +56,13 @@ def _download(url, filename, using_tuf=False): def test_arbitrary_package_attack(using_tuf=False): """ + + Illustrate arbitrary package attack vulnerability. + using_tuf: If set to 'False' all directories that start with 'tuf_' are ignored, indicating that tuf is not implemented. - - - Illustrate arbitrary package attack vulnerability. - """ ERROR_MSG = 'Arbitrary Package Attack was Successful!' diff --git a/tests/integration/test_endless_data_attack.py b/tests/integration/test_endless_data_attack.py index c8c0b093..5b11df75 100755 --- a/tests/integration/test_endless_data_attack.py +++ b/tests/integration/test_endless_data_attack.py @@ -55,104 +55,106 @@ def _download(url, filename, using_tuf=False): -def test_arbitrary_package_attack(using_tuf=False, TIMESTAMP=False): +def test_endless_data_attack(using_tuf=False, TIMESTAMP=False): """ - - TUF: - If set to 'False' all directories that start with 'tuf_' are ignored, - indicating that tuf is not implemented. - Illustrate endless data attack vulnerability. + + using_tuf: + If set to 'False' all directories that start with 'tuf_' are ignored, + indicating that tuf is not implemented. + """ ERROR_MSG = 'Endless Data Attack was Successful!\n' - # Setup. - root_repo, url, server_proc, keyids = util_test_tools.init_repo(using_tuf=TUF) - reg_repo = os.path.join(root_repo, 'reg_repo') - tuf_repo = os.path.join(root_repo, 'tuf_repo') - downloads = os.path.join(root_repo, 'downloads') - tuf_targets = os.path.join(tuf_repo, 'targets') - - # Original data. - INTENDED_DATA = 'Test A' - - # Add a file to 'repo' directory: {root_repo} - filepath = util_test_tools.add_file_to_repository(reg_repo, INTENDED_DATA) - file_basename = os.path.basename(filepath) - url_to_repo = url+'reg_repo/'+file_basename - downloaded_file = os.path.join(downloads, file_basename) - # We do not deliver truly endless data, but we will extend the original - # file by many bytes. - noisy_data = 'X'*100000 - - - if using_tuf: - # Update TUF metadata before attacker modifies anything. - util_test_tools.tuf_refresh_repo(root_repo, keyids) - # Modify the url. Remember that the interposition will intercept - # urls that have 'localhost:9999' hostname, which was specified in - # the json interposition configuration file. Look for 'hostname' - # in 'util_test_tools.py'. Further, the 'file_basename' is the target - # path relative to 'targets_dir'. - url_to_repo = 'http://localhost:9999/'+file_basename - - # Attacker modifies the file at the targets repository. - target = os.path.join(tuf_targets, file_basename) - original_data = util_test_tools.read_file_content(target) - larger_original_data = original_data + noisy_data - util_test_tools.modify_file_at_repository(target, larger_original_data) - - # Attacker modifies the timestamp.txt metadata. - if TIMESTAMP: - metadata = os.path.join(tuf_repo, 'metadata') - timestamp = os.path.join(metadata, 'timestamp.txt') - original_data = util_test_tools.read_file_content(timestamp) - larger_original_data = original_data + noisy_data - util_test_tools.modify_file_at_repository(timestamp, - larger_original_data) - - # Attacker modifies the file at the regular repository. - original_data = util_test_tools.read_file_content(filepath) - larger_original_data = original_data + noisy_data - util_test_tools.modify_file_at_repository(filepath, larger_original_data) - - # End Setup. - - - # Client downloads (tries to download) the file. try: - _download(url_to_repo, downloaded_file, using_tuf) - except Exception, exception: - # Because we are extending the true timestamp TUF metadata with invalid - # JSON, we except to catch an error about invalid metadata JSON. - if TUF and TIMESTAMP: - endless_data_attack = False + # Setup. + root_repo, url, server_proc, keyids = util_test_tools.init_repo(using_tuf) + reg_repo = os.path.join(root_repo, 'reg_repo') + tuf_repo = os.path.join(root_repo, 'tuf_repo') + downloads = os.path.join(root_repo, 'downloads') + tuf_targets = os.path.join(tuf_repo, 'targets') - for mirror_url, mirror_error in exception.mirror_errors.iteritems(): - if isinstance(mirror_error, tuf.InvalidMetadataJSONError): - endless_data_attack = True - break + # Original data. + INTENDED_DATA = 'Test A' - # In case we did not detect what was likely an endless data attack, we - # reraise the exception to indicate that endless data attack detection - # failed. - if not endless_data_attack: raise - else: raise + # Add a file to 'repo' directory: {root_repo} + filepath = util_test_tools.add_file_to_repository(reg_repo, INTENDED_DATA) + file_basename = os.path.basename(filepath) + url_to_repo = url+'reg_repo/'+file_basename + downloaded_file = os.path.join(downloads, file_basename) + # We do not deliver truly endless data, but we will extend the original + # file by many bytes. + noisy_data = 'X'*100000 - # When we test downloading "endless" timestamp with TUF, we want to skip - # the following test because downloading the timestamp should have failed. - if not (using_tuf and TIMESTAMP): - # Check whether the attack succeeded by inspecting the content of the - # update. The update should contain 'Test A'. Technically it suffices - # to check whether the file was downloaded or not. - downloaded_content = util_test_tools.read_file_content(downloaded_file) - if downloaded_content != INTENDED_DATA: - raise EndlessDataAttack(ERROR_MSG) - util_test_tools.cleanup(root_repo, server_proc) + if using_tuf: + # Update TUF metadata before attacker modifies anything. + util_test_tools.tuf_refresh_repo(root_repo, keyids) + # Modify the url. Remember that the interposition will intercept + # urls that have 'localhost:9999' hostname, which was specified in + # the json interposition configuration file. Look for 'hostname' + # in 'util_test_tools.py'. Further, the 'file_basename' is the target + # path relative to 'targets_dir'. + url_to_repo = 'http://localhost:9999/'+file_basename + + # Attacker modifies the file at the targets repository. + target = os.path.join(tuf_targets, file_basename) + original_data = util_test_tools.read_file_content(target) + larger_original_data = original_data + noisy_data + util_test_tools.modify_file_at_repository(target, larger_original_data) + + # Attacker modifies the timestamp.txt metadata. + if TIMESTAMP: + metadata = os.path.join(tuf_repo, 'metadata') + timestamp = os.path.join(metadata, 'timestamp.txt') + original_data = util_test_tools.read_file_content(timestamp) + larger_original_data = original_data + noisy_data + util_test_tools.modify_file_at_repository(timestamp, + larger_original_data) + + # Attacker modifies the file at the regular repository. + original_data = util_test_tools.read_file_content(filepath) + larger_original_data = original_data + noisy_data + util_test_tools.modify_file_at_repository(filepath, larger_original_data) + + # End Setup. + + + # Client downloads (tries to download) the file. + try: + _download(url_to_repo, downloaded_file, using_tuf) + except Exception, exception: + # Because we are extending the true timestamp TUF metadata with invalid + # JSON, we except to catch an error about invalid metadata JSON. + if using_tuf and TIMESTAMP: + endless_data_attack = False + + for mirror_url, mirror_error in exception.mirror_errors.iteritems(): + if isinstance(mirror_error, tuf.InvalidMetadataJSONError): + endless_data_attack = True + break + + # In case we did not detect what was likely an endless data attack, we + # reraise the exception to indicate that endless data attack detection + # failed. + if not endless_data_attack: raise + else: raise + + # When we test downloading "endless" timestamp with TUF, we want to skip + # the following test because downloading the timestamp should have failed. + if not (using_tuf and TIMESTAMP): + # Check whether the attack succeeded by inspecting the content of the + # update. The update should contain 'Test A'. Technically it suffices + # to check whether the file was downloaded or not. + downloaded_content = util_test_tools.read_file_content(downloaded_file) + if downloaded_content != INTENDED_DATA: + raise EndlessDataAttack(ERROR_MSG) + + finally: + util_test_tools.cleanup(root_repo, server_proc) From b9ec0a0b263f4a2f9a0398cdb88c330f9244adfd Mon Sep 17 00:00:00 2001 From: vladimir-v-diaz Date: Mon, 16 Sep 2013 09:05:53 -0400 Subject: [PATCH 19/95] Update test_util_test_tools.py following change to util_test_tools.init_repo() A util_test_tools.init_repo() parameter name was changed from 'tuf' to 'using_tuf', however, test_util_test_tools was not updated following the name change. --- tests/unit/test_util_test_tools.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/unit/test_util_test_tools.py b/tests/unit/test_util_test_tools.py index 825f4aab..0acf5131 100755 --- a/tests/unit/test_util_test_tools.py +++ b/tests/unit/test_util_test_tools.py @@ -34,7 +34,7 @@ def setUp(self): tuf.repo.keystore.clear_keystore() # Unpacking necessary parameters returned from init_repo() - essential_params = util_test_tools.init_repo(tuf=True) + essential_params = util_test_tools.init_repo(using_tuf=True) self.root_repo = essential_params[0] self.url = essential_params[1] self.server_proc = essential_params[2] From 2baaa58a2098205f06e264fd076f16e009153420 Mon Sep 17 00:00:00 2001 From: ttgump Date: Mon, 16 Sep 2013 10:57:23 -0400 Subject: [PATCH 20/95] fetch upstream --- tests/unit/test_util_test_tools.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/unit/test_util_test_tools.py b/tests/unit/test_util_test_tools.py index 825f4aab..0acf5131 100755 --- a/tests/unit/test_util_test_tools.py +++ b/tests/unit/test_util_test_tools.py @@ -34,7 +34,7 @@ def setUp(self): tuf.repo.keystore.clear_keystore() # Unpacking necessary parameters returned from init_repo() - essential_params = util_test_tools.init_repo(tuf=True) + essential_params = util_test_tools.init_repo(using_tuf=True) self.root_repo = essential_params[0] self.url = essential_params[1] self.server_proc = essential_params[2] From 017c9a378f920ec0e45d83fd56b5ef7d58ec1ce2 Mon Sep 17 00:00:00 2001 From: ttgump Date: Mon, 16 Sep 2013 11:12:18 -0400 Subject: [PATCH 21/95] fix of connection refused --- tests/integration/test_indefinite_freeze_attack.py | 6 +++--- tests/integration/test_mix_and_match_attack.py | 3 +++ 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/tests/integration/test_indefinite_freeze_attack.py b/tests/integration/test_indefinite_freeze_attack.py index bb1a4a8c..2ad23f3c 100755 --- a/tests/integration/test_indefinite_freeze_attack.py +++ b/tests/integration/test_indefinite_freeze_attack.py @@ -130,11 +130,11 @@ def test_indefinite_freeze_attack(TUF=False): # Try downloading again, this should raise an error. try: - _download(url=url_to_repo, filename=downloaded_file, using_tuf=TUF) + _download(url=url_to_repo, filename=downloaded_file, using_tuf=TUF) except tuf.ExpiredMetadataError, error: - print('Caught an expiration error!') + print('Caught an expiration error!') else: - raise IndefiniteFreezeAttackAlert(ERROR_MSG) + raise IndefiniteFreezeAttackAlert(ERROR_MSG) finally: util_test_tools.cleanup(root_repo, server_proc) diff --git a/tests/integration/test_mix_and_match_attack.py b/tests/integration/test_mix_and_match_attack.py index 5bf98db9..db62a727 100755 --- a/tests/integration/test_mix_and_match_attack.py +++ b/tests/integration/test_mix_and_match_attack.py @@ -38,6 +38,7 @@ import shutil import urllib import tempfile +import time import tuf import tuf.interposition @@ -125,6 +126,8 @@ def test_mix_and_match_attack(TUF=False): url_to_file = 'http://localhost:9999/'+file_basename + # Wait for some time to let program set up local http server + time.sleep(1) # Client's initial download. _download(url=url_to_file, filename=downloaded_file, using_tuf=TUF) From 22f8c03d63a369cb7214375fab437468d8ade791 Mon Sep 17 00:00:00 2001 From: vladdd Date: Tue, 17 Sep 2013 10:11:23 -0400 Subject: [PATCH 22/95] Update key size comments for rsa_key.py & signerlib.py - issue #112 --- tuf/repo/signerlib.py | 6 +++++- tuf/rsa_key.py | 19 +++++++++++++------ 2 files changed, 18 insertions(+), 7 deletions(-) diff --git a/tuf/repo/signerlib.py b/tuf/repo/signerlib.py index 2698518f..3f44ea5e 100755 --- a/tuf/repo/signerlib.py +++ b/tuf/repo/signerlib.py @@ -37,7 +37,8 @@ json = tuf.util.import_json() -# Recommended RSA key sizes: http://www.rsa.com/rsalabs/node.asp?id=2004 +# Recommended RSA key sizes: +# http://www.emc.com/emc-plus/rsa-labs/historical/twirl-and-rsa-key-size.htm#table1 # According to the document above, revised May 6, 2003, RSA keys of # size 3072 provide security through 2031 and beyond. 2048-bit keys # are the recommended minimum and are good from the present through 2030. @@ -746,6 +747,9 @@ def generate_and_save_rsa_key(keystore_directory, password, bits: The key size, or key length, of the RSA key. + If 'bits' is unspecified, a 3072-bit RSA key is generated, which is the + key size recommended by TUF, although 2048-bit keys are accepted + (minimum key size). tuf.FormatError, if 'bits' or 'password' does not have the diff --git a/tuf/rsa_key.py b/tuf/rsa_key.py index b61b7d51..589b56d5 100755 --- a/tuf/rsa_key.py +++ b/tuf/rsa_key.py @@ -69,7 +69,8 @@ _KEY_ID_HASH_ALGORITHM = 'sha256' -# Recommended RSA key sizes: http://www.rsa.com/rsalabs/node.asp?id=2004 +# Recommended RSA key sizes: +# http://www.emc.com/emc-plus/rsa-labs/historical/twirl-and-rsa-key-size.htm#table1 # According to the document above, revised May 6, 2003, RSA keys of # size 3072 provide security through 2031 and beyond. _DEFAULT_RSA_KEY_BITS = 3072 @@ -87,16 +88,21 @@ def generate(bits=_DEFAULT_RSA_KEY_BITS): 'private': '-----BEGIN RSA PRIVATE KEY----- ...'}} The public and private keys are in PEM format and stored as strings. + + Although the crytography library called sets a 1024-bit minimum key size, + generate() enforces a minimum key size of 2048 bits. If 'bits' is + unspecified, a 3072-bit RSA key is generated, which is the key size + recommended by TUF. bits: - The key size, or key length, of the RSA key. 'bits' must be 1024, or + The key size, or key length, of the RSA key. 'bits' must be 2048, or greater, and a multiple of 256. ValueError, if an exception occurs after calling the RSA key generation - routine. 'bits' must be 1024, or greater, and a multiple of 256. - Raised by Cryptography library. + routine. 'bits' must be a multiple of 256. The 'ValueError' exception is + raised by the key generation function of the cryptography library called. tuf.FormatError, if 'bits' does not contain the correct format. @@ -121,8 +127,9 @@ def generate(bits=_DEFAULT_RSA_KEY_BITS): keytype = 'rsa' # Generate the public and private RSA keys. The PyCrypto module performs - # the actual key generation. Raise 'ValueError' if 'bits' is less than 1024 - # or not a multiple of 256. + # the actual key generation. Raise 'ValueError' if 'bits' is less than 1024 + # or not a multiple of 256, although a 2048-bit minimum is enforced by + # tuf.formats.RSAKEYBITS_SCHEMA.check_match(). rsa_key_object = Crypto.PublicKey.RSA.generate(bits) # Extract the public & private halves of the RSA key and generate their From 76326f82d134670ec1ccefa3ae746bd4456aed83 Mon Sep 17 00:00:00 2001 From: dachshund Date: Tue, 17 Sep 2013 20:02:54 -0400 Subject: [PATCH 23/95] Merge #108. --- tests/integration/test_delegations.py | 41 +++++++++++++++++---------- 1 file changed, 26 insertions(+), 15 deletions(-) diff --git a/tests/integration/test_delegations.py b/tests/integration/test_delegations.py index f7cb3695..bd373f0e 100755 --- a/tests/integration/test_delegations.py +++ b/tests/integration/test_delegations.py @@ -331,24 +331,31 @@ def make_targets_metadata(self): self.T2_metadata =\ make_metadata(self.tuf_repo, self.signed_targets[self.T2], version, expiration) - self.T3_metadata = \ + self.T3_metadata =\ make_metadata(self.tuf_repo, self.signed_targets[self.T3], version, expiration) def test_that_initial_update_fails_with_undelegated_signing_of_targets(self): - # Expect to see a particular exception on initial update. - with self.assertRaises(tuf.NoWorkingMirrorError) as contextManager: + """We expect to see ForbiddenTargetError on initial update because + delegated targets roles sign for targets that they were not delegated + to.""" + + # http://docs.python.org/2/library/unittest.html#unittest.TestCase.assertRaises + with self.assertRaises(tuf.NoWorkingMirrorError) as context_manager: self.do_update() - exception = contextManager.exception - ForbiddenTargetError = False - for mirror_url, mirror_error in exception.mirror_errors.iteritems(): + mirror_errors = context_manager.exception.mirror_errors + forbidden_target_error = False + + for mirror_url, mirror_error in mirror_errors.iteritems(): if isinstance(mirror_error, tuf.ForbiddenTargetError): - ForbiddenTargetError = True + forbidden_target_error = True break - self.assertEqual(ForbiddenTargetError, True) + self.assertEqual(forbidden_target_error, True) + + @@ -463,18 +470,22 @@ def make_targets_metadata(self): def test_that_initial_update_fails_with_many_roles_sharing_a_target(self): - # Expect to see a particular exception on initial update. - with self.assertRaises(tuf.NoWorkingMirrorError) as contextManager: + """We expect to see BadHashError on initial update because the hash + metadata mismatches the target.""" + + # http://docs.python.org/2/library/unittest.html#unittest.TestCase.assertRaises + with self.assertRaises(tuf.NoWorkingMirrorError) as context_manager: self.do_update() - exception = contextManager.exception - BadHashError = False - for mirror_url, mirror_error in exception.mirror_errors.iteritems(): + mirror_errors = context_manager.exception.mirror_errors + bad_hash_error = False + + for mirror_url, mirror_error in mirror_errors.iteritems(): if isinstance(mirror_error, tuf.BadHashError): - BadHashError = True + bad_hash_error = True break - self.assertEqual(BadHashError, True) + self.assertEqual(bad_hash_error, True) From 071190d18bb911e536a043ba58b11da5d4dd838f Mon Sep 17 00:00:00 2001 From: vladdd Date: Tue, 17 Sep 2013 20:20:55 -0400 Subject: [PATCH 24/95] Fix docstring indentation in test_arbitrary_package_attack.py --- .../test_arbitrary_package_attack.py | 20 +++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/tests/integration/test_arbitrary_package_attack.py b/tests/integration/test_arbitrary_package_attack.py index 91953986..c45b2198 100755 --- a/tests/integration/test_arbitrary_package_attack.py +++ b/tests/integration/test_arbitrary_package_attack.py @@ -15,17 +15,17 @@ Simulate an arbitrary package attack. A simple client update vs. client update implementing TUF. -Note: The interposition provided by 'tuf.interposition' is used to intercept -all calls made by urllib/urillib2 to certain hostnames specified in -the interposition configuration file. Look up interposition.py for more -information and illustration of a sample contents of the interposition -configuration file. Interposition was meant to make TUF integration with an -existing software updater an easy process. This allows for more flexibility -to the existing software updater. However, if you are planning to solely use -TUF there should be no need for interposition, all necessary calls will be -generated from within TUF. + Note: The interposition provided by 'tuf.interposition' is used to intercept + all calls made by urllib/urillib2 to certain hostnames specified in + the interposition configuration file. Look up interposition.py for more + information and illustration of a sample contents of the interposition + configuration file. Interposition was meant to make TUF integration with an + existing software updater an easy process. This allows for more flexibility + to the existing software updater. However, if you are planning to solely use + TUF there should be no need for interposition, all necessary calls will be + generated from within TUF. -Note: There is no difference between 'updates' and 'target' files. + There is no difference between 'updates' and 'target' files. """ From 7eb834ab8482f0a8b6a0f363cec7ba1ee1fc12ee Mon Sep 17 00:00:00 2001 From: dachshund Date: Tue, 17 Sep 2013 22:59:26 -0400 Subject: [PATCH 25/95] Fix replay attack integration test. --- tests/integration/test_replay_attack.py | 127 +++++++++++++----------- 1 file changed, 71 insertions(+), 56 deletions(-) diff --git a/tests/integration/test_replay_attack.py b/tests/integration/test_replay_attack.py index b9088264..dc78a3f5 100755 --- a/tests/integration/test_replay_attack.py +++ b/tests/integration/test_replay_attack.py @@ -1,3 +1,5 @@ +#!/usr/bin/env python + """ test_replay_attack.py @@ -38,23 +40,18 @@ import urllib import tempfile -import tuf.interposition.urllib_tuf as urllib_tuf +import tuf.interposition import tuf.tests.util_test_tools as util_test_tools -class TestSetupError(Exception): - pass +class TestSetupError(Exception): pass +class ReplayAttackAlert(Exception): pass -class ReplayAttackAlert(Exception): - pass - - - -def _download(url, filename, tuf=False): - if tuf: - urllib_tuf.urlretrieve(url, filename) +def _download(url, filename, using_tuf=False): + if using_tuf: + tuf.interposition.urllib_tuf.urlretrieve(url, filename) else: urllib.urlretrieve(url, filename) @@ -63,10 +60,10 @@ def _download(url, filename, tuf=False): -def test_replay_attack(TUF=False): +def test_replay_attack(using_tuf=False): """ - TUF: + using_tuf: If set to 'False' all directories that start with 'tuf_' are ignored, indicating that tuf is not implemented. @@ -76,32 +73,35 @@ def test_replay_attack(TUF=False): """ ERROR_MSG = '\tReplay Attack was Successful!\n\n' - + FIRST_CONTENT = 'Test A' + SECOND_CONTENT = 'Test B' try: # Setup. - root_repo, url, server_proc, keyids = util_test_tools.init_repo(tuf=TUF) + root_repo, url, server_proc, keyids = \ + util_test_tools.init_repo(using_tuf=using_tuf) reg_repo = os.path.join(root_repo, 'reg_repo') tuf_repo = os.path.join(root_repo, 'tuf_repo') + tuf_repo_copy = os.path.join(root_repo, 'tuf_repo_copy') downloads = os.path.join(root_repo, 'downloads') tuf_targets = os.path.join(tuf_repo, 'targets') # Add file to 'repo' directory: {root_repo} - filepath = util_test_tools.add_file_to_repository(reg_repo, 'Test A') + filepath = util_test_tools.add_file_to_repository(reg_repo, FIRST_CONTENT) file_basename = os.path.basename(filepath) url_to_repo = url+'reg_repo/'+file_basename downloaded_file = os.path.join(downloads, file_basename) # Attacker saves the original file into 'evil_dir'. evil_dir = tempfile.mkdtemp(dir=root_repo) - vulnerable_file = os.path.join(evil_dir, file_basename) + original_file = os.path.join(evil_dir, file_basename) shutil.copy(filepath, evil_dir) - if TUF: - print 'TUF ...' - + if using_tuf: # Update TUF metadata before attacker modifies anything. util_test_tools.tuf_refresh_repo(root_repo, keyids) + # Copy the first version of the repository for replay later. + shutil.copytree(tuf_repo, tuf_repo_copy) # Modify the url. Remember that the interposition will intercept # urls that have 'localhost:9999' hostname, which was specified in @@ -109,62 +109,66 @@ def test_replay_attack(TUF=False): # in 'util_test_tools.py'. Further, the 'file_basename' is the target # path relative to 'targets_dir'. url_to_repo = 'http://localhost:9999/'+file_basename - # End of Setup. - # Client performs initial update. - _download(url=url_to_repo, filename=downloaded_file, tuf=TUF) + _download(url=url_to_repo, filename=downloaded_file, using_tuf=using_tuf) # Downloads are stored in the same directory '{root_repo}/downloads/' # for regular and tuf clients. downloaded_content = util_test_tools.read_file_content(downloaded_file) - if 'Test A' != downloaded_content: - raise TestSetupError('[Initial Updata] Failed to download the file.') + if FIRST_CONTENT != downloaded_content: + raise TestSetupError('[Initial Update] Failed to download the file.') # Developer patches the file and updates the repository. - util_test_tools.modify_file_at_repository(filepath, 'Test NOT A') + util_test_tools.modify_file_at_repository(filepath, SECOND_CONTENT) # Updating tuf repository. This will copy files from regular repository # into tuf repository and refresh the metadata - if TUF: + if using_tuf: util_test_tools.tuf_refresh_repo(root_repo, keyids) - # Client downloads the patched file. - _download(url=url_to_repo, filename=downloaded_file, tuf=TUF) + _download(url=url_to_repo, filename=downloaded_file, using_tuf=using_tuf) # Content of the downloaded file. downloaded_content = util_test_tools.read_file_content(downloaded_file) - if 'Test NOT A' != downloaded_content: + if SECOND_CONTENT != downloaded_content: raise TestSetupError('[Update] Failed to update the file.') # Attacker tries to be clever, he manages to modifies regular and tuf # targets directory by replacing a patched file with an old one. - if os.path.isdir(tuf_targets): - target = os.path.join(tuf_targets, file_basename) - util_test_tools.delete_file_at_repository(target) - shutil.copy(vulnerable_file, tuf_targets) - # Verify that 'target' is an old, un-patched file. - target = os.path.join(tuf_targets, file_basename) - target_content = util_test_tools.read_file_content(target) - if 'Test A' != target_content: - raise TestSetupError("The 'target' file contains new data!") - + if using_tuf: + # Delete the current TUF repository... + shutil.rmtree(tuf_repo) + # ...and replace it with a previous copy. + shutil.move(tuf_repo_copy, tuf_repo) else: util_test_tools.delete_file_at_repository(filepath) - shutil.copy(vulnerable_file, reg_repo) + shutil.copy(original_file, reg_repo) + try: + # Client downloads the file once more. + _download(url=url_to_repo, filename=downloaded_file, using_tuf=using_tuf) + except tuf.NoWorkingMirrorError, exception: + replayed_metadata_attack = False - # Client downloads the file once more. - _download(url=url_to_repo, filename=downloaded_file, tuf=TUF) - - # Check whether the attack succeeded by inspecting the content of the - # update. The update should contain 'Test NOT A'. - downloaded_content = util_test_tools.read_file_content(downloaded_file) - if 'Test NOT A' != downloaded_content: - raise ReplayAttackAlert(ERROR_MSG) + for mirror_url, mirror_error in exception.mirror_errors.iteritems(): + if isinstance(mirror_error, tuf.ReplayedMetadataError): + replayed_metadata_attack = True + break + # In case we did not detect what was likely a replayed metadata attack, + # we reraise the exception to indicate that replayed metadata attack + # detection failed. + if not replayed_metadata_attack: raise + else: + # Check whether the attack succeeded by inspecting the content of the + # update. The update should contain 'Test NOT A'. + downloaded_content = util_test_tools.read_file_content(downloaded_file) + # If we ended up downloading replayed content, then we failed. + if FIRST_CONTENT == downloaded_content: + raise ReplayAttackAlert(ERROR_MSG) finally: util_test_tools.cleanup(root_repo, server_proc) @@ -174,13 +178,24 @@ def test_replay_attack(TUF=False): try: - test_replay_attack(TUF=False) -except ReplayAttackAlert, error: - print error + test_replay_attack(using_tuf=False) +except ReplayAttackAlert, exception: + print('Download without TUF fell prey to replayed metadata attack.') + + try: + test_replay_attack(using_tuf=True) + except ReplayAttackAlert, exception: + print('Download with TUF fell prey to replayed metadata attack!') + except Exception, exception: + print('Download with TUF failed due to: '+str(exception)) + else: + print('Download with TUF defended against replayed metadata attack.') +except Exception, exception: + print('Download without TUF failed due to: '+str(exception)) +else: + print('Download without TUF did NOT fail due to replayed metadata attack!') + + -try: - test_replay_attack(TUF=True) -except ReplayAttackAlert, error: - print error From f9a1ac9a4c4a7d6ab5c88d5946b8c4971eba8f7e Mon Sep 17 00:00:00 2001 From: dachshund Date: Tue, 17 Sep 2013 23:36:48 -0400 Subject: [PATCH 26/95] Better string representation of NoWorkingMirrorError. --- tuf/__init__.py | 18 +++++++++++++++++- 1 file changed, 17 insertions(+), 1 deletion(-) diff --git a/tuf/__init__.py b/tuf/__init__.py index 13964980..aaa49338 100755 --- a/tuf/__init__.py +++ b/tuf/__init__.py @@ -21,6 +21,8 @@ """ +import urlparse + # Import 'tuf.formats' if a module tries to import the # entire tuf package (i.e., from tuf import *). __all__ = ['formats'] @@ -278,7 +280,21 @@ def __init__(self, mirror_errors): self.mirror_errors = mirror_errors def __str__(self): - return str(self.mirror_errors) + all_errors = 'No working mirror was found:' + + for mirror_url, mirror_error in self.mirror_errors.iteritems(): + try: + # http://docs.python.org/2/library/urlparse.html#urlparse.urlparse + mirror_url_tokens = urlparse.urlparse(mirror_url) + except: + logging.exception('Failed to parse mirror URL: '+str(mirror_url)) + mirror_netloc = mirror_url + else: + mirror_netloc = mirror_url_tokens.netloc + + all_errors += '\n '+str(mirror_netloc)+': '+str(mirror_error) + + return all_errors From 8d3bffa10c2edb8899d087fc2fcf071bd74ba375 Mon Sep 17 00:00:00 2001 From: dachshund Date: Wed, 18 Sep 2013 03:12:39 -0400 Subject: [PATCH 27/95] Better error formatting. --- tuf/__init__.py | 22 +++++++++++++++++++--- tuf/client/updater.py | 14 +++++++------- 2 files changed, 26 insertions(+), 10 deletions(-) diff --git a/tuf/__init__.py b/tuf/__init__.py index aaa49338..4be6efda 100755 --- a/tuf/__init__.py +++ b/tuf/__init__.py @@ -80,7 +80,14 @@ class UnsupportedAlgorithmError(Error): class BadHashError(Error): """Indicate an error while checking the value a hash object.""" - pass + + def __init__(self, expected_hash, observed_hash): + self.expected_hash = expected_hash + self.observed_hash = observed_hash + + def __str__(self): + return 'Observed hash ('+str(self.observed_hash)+\ + ') != expected hash ('+str(self.expected_hash)+')' @@ -120,7 +127,12 @@ class ForbiddenTargetError(RepositoryError): class ExpiredMetadataError(Error): """Indicate that a TUF Metadata file has expired.""" - pass + + def __init__(self, expiry_time): + self.expiry_time = expiry_time # UTC + + def __str__(self): + return 'Metadata expired on '+str(self.expiry_time)+'.' @@ -154,8 +166,12 @@ class CryptoError(Error): class BadSignatureError(CryptoError): """Indicate that some metadata file had a bad signature.""" - pass + def __init__(self, metadata_role_name): + self.metadata_role_name = metadata_role_name + + def __str__(self): + return str(self.metadata_role_name)+' metadata bad signature!' diff --git a/tuf/client/updater.py b/tuf/client/updater.py index fc1f9d3c..3b875fb3 100755 --- a/tuf/client/updater.py +++ b/tuf/client/updater.py @@ -627,8 +627,7 @@ def __check_hashes(self, file_object, trusted_hashes): digest_object.update(file_object.read()) computed_hash = digest_object.hexdigest() if trusted_hash != computed_hash: - raise tuf.BadHashError('Hashes do not match! Expected '+ - trusted_hash+' got '+computed_hash) + raise tuf.BadHashError(trusted_hash, computed_hash) else: logger.info('The file\'s '+algorithm+' hash is correct: '+trusted_hash) @@ -835,7 +834,7 @@ def __verify_uncompressed_metadata_file(self, metadata_file_object, # Verify the signature on the downloaded metadata object. valid = tuf.sig.verify(metadata_signable, metadata_role) if not valid: - raise tuf.BadSignatureError() + raise tuf.BadSignatureError(metadata_role) @@ -1742,10 +1741,11 @@ def _ensure_not_expired(self, metadata_role): # an exception. 'expires' is in YYYY-MM-DD HH:MM:SS format, so # convert it to seconds since the epoch, which is the time format # returned by time.time() (i.e., current time), before comparing. - if tuf.formats.parse_time(expires) < time.time(): - message = 'Metadata '+repr(rolepath)+' expired on '+repr(expires)+'.' - logger.error(message) - raise tuf.ExpiredMetadataError(message) + current_time = time.time() + expiry_time = tuf.formats.parse_time(expires) + if expiry_time < current_time: + logger.error('Metadata '+repr(rolepath)+' expired on '+repr(expires)+'.') + raise tuf.ExpiredMetadataError(expires) From 4f7e8f5fba1ec97c9a0bf494c7cd1c84ce73c0aa Mon Sep 17 00:00:00 2001 From: dachshund Date: Wed, 18 Sep 2013 03:18:51 -0400 Subject: [PATCH 28/95] Fix typo. --- tuf/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tuf/__init__.py b/tuf/__init__.py index 4be6efda..2e2db6b0 100755 --- a/tuf/__init__.py +++ b/tuf/__init__.py @@ -171,7 +171,7 @@ def __init__(self, metadata_role_name): self.metadata_role_name = metadata_role_name def __str__(self): - return str(self.metadata_role_name)+' metadata bad signature!' + return str(self.metadata_role_name)+' metadata has bad signature!' From 5622e0c622b7c83b0f09b2b757dbe19e5d5b368b Mon Sep 17 00:00:00 2001 From: vladdd Date: Tue, 8 Oct 2013 13:09:59 -0400 Subject: [PATCH 29/95] Commence configurable crypto changes Refactored the majority of affected modules. Added optimized version of the reference implementation of ed25519. --- ed25519/.gitignore | 1 + ed25519/.travis.yml | 28 ++ ed25519/__init__.py | 0 ed25519/ed25519.py | 199 +++++++----- ed25519/runtests.sh | 8 + ed25519/science.py | 32 ++ ed25519/setup.py | 11 + ed25519/sign.py | 3 + ed25519/signfast.py | 48 +++ ed25519/tox.ini | 5 + tuf/client/updater.py | 38 +-- tuf/conf.py | 5 + tuf/ed25519_key.py | 618 ------------------------------------ tuf/ed25519_keys.py | 374 ++++++++++++++++++++++ tuf/evp.py | 425 +++++++++++++++++++++++++ tuf/keydb.py | 6 +- tuf/{rsa_key.py => keys.py} | 456 ++++++++++++-------------- tuf/pycrypto_keys.py | 403 +++++++++++++++++++++++ tuf/sig.py | 14 +- 19 files changed, 1678 insertions(+), 996 deletions(-) create mode 100644 ed25519/.gitignore create mode 100644 ed25519/.travis.yml mode change 100755 => 100644 ed25519/__init__.py mode change 100755 => 100644 ed25519/ed25519.py create mode 100755 ed25519/runtests.sh create mode 100644 ed25519/science.py create mode 100644 ed25519/setup.py create mode 100644 ed25519/signfast.py create mode 100644 ed25519/tox.ini delete mode 100755 tuf/ed25519_key.py create mode 100755 tuf/ed25519_keys.py create mode 100755 tuf/evp.py rename tuf/{rsa_key.py => keys.py} (60%) create mode 100755 tuf/pycrypto_keys.py diff --git a/ed25519/.gitignore b/ed25519/.gitignore new file mode 100644 index 00000000..0d20b648 --- /dev/null +++ b/ed25519/.gitignore @@ -0,0 +1 @@ +*.pyc diff --git a/ed25519/.travis.yml b/ed25519/.travis.yml new file mode 100644 index 00000000..8518ca28 --- /dev/null +++ b/ed25519/.travis.yml @@ -0,0 +1,28 @@ +language: python +python: 2.7 +env: + - TOXENV=py26 + - TOXENV=py27 + #- TOXENV=py32 + #- TOXENV=py33 + - TOXENV=pypy + +install: + # Add the PyPy repository + - "if [[ $TOXENV == 'pypy' ]]; then sudo add-apt-repository -y ppa:pypy/ppa; fi" + # Upgrade PyPy + - "if [[ $TOXENV == 'pypy' ]]; then sudo apt-get -y install pypy; fi" + # This is required because we need to get rid of the Travis installed PyPy + # or it'll take precedence over the PPA installed one. + - "if [[ $TOXENV == 'pypy' ]]; then sudo rm -rf /usr/local/pypy/bin; fi" + - pip install tox + +script: + - tox + +notifications: + irc: + channels: + - "irc.freenode.org#cryptography-dev" + use_notice: true + skip_join: true diff --git a/ed25519/__init__.py b/ed25519/__init__.py old mode 100755 new mode 100644 diff --git a/ed25519/ed25519.py b/ed25519/ed25519.py old mode 100755 new mode 100644 index 7f8613b8..b7e9ff04 --- a/ed25519/ed25519.py +++ b/ed25519/ed25519.py @@ -1,104 +1,161 @@ import hashlib + b = 256 -q = 2**255 - 19 -l = 2**252 + 27742317777372353535851937790883648493 +q = 2 ** 255 - 19 +l = 2 ** 252 + 27742317777372353535851937790883648493 + def H(m): - return hashlib.sha512(m).digest() + return hashlib.sha512(m).digest() -def expmod(b,e,m): - if e == 0: return 1 - t = expmod(b,e/2,m)**2 % m - if e & 1: t = (t*b) % m - return t -def inv(x): - return expmod(x,q-2,q) +def pow2(x, p): + """== pow(x, 2**p, q)""" + while p > 0: + x = x * x % q + p -= 1 + return x + +def inv(z): + """$= z^{-1} \mod q$, for z != 0""" + # Adapted from curve25519_athlon.c in djb's Curve25519. + z2 = z * z % q # 2 + z9 = pow2(z2, 2) * z % q # 9 + z11 = z9 * z2 % q # 11 + z2_5_0 = (z11*z11)%q * z9 % q # 31 == 2^5 - 2^0 + z2_10_0 = pow2(z2_5_0, 5) * z2_5_0 % q # 2^10 - 2^0 + z2_20_0 = pow2(z2_10_0, 10) * z2_10_0 % q # ... + z2_40_0 = pow2(z2_20_0, 20) * z2_20_0 % q + z2_50_0 = pow2(z2_40_0, 10) * z2_10_0 % q + z2_100_0 = pow2(z2_50_0, 50) * z2_50_0 % q + z2_200_0 = pow2(z2_100_0, 100) * z2_100_0 % q + z2_250_0 = pow2(z2_200_0, 50) * z2_50_0 % q # 2^250 - 2^0 + return pow2(z2_250_0, 5) * z11 % q # 2^255 - 2^5 + 11 = q - 2 + d = -121665 * inv(121666) -I = expmod(2,(q-1)/4,q) +I = pow(2, (q - 1) / 4, q) + def xrecover(y): - xx = (y*y-1) * inv(d*y*y+1) - x = expmod(xx,(q+3)/8,q) - if (x*x - xx) % q != 0: x = (x*I) % q - if x % 2 != 0: x = q-x - return x + xx = (y * y - 1) * inv(d * y * y + 1) + x = pow(xx, (q + 3) / 8, q) + + if (x * x - xx) % q != 0: + x = (x * I) % q + + if x % 2 != 0: + x = q-x + + return x + By = 4 * inv(5) Bx = xrecover(By) -B = [Bx % q,By % q] +B = (Bx % q, By % q) -def edwards(P,Q): - x1 = P[0] - y1 = P[1] - x2 = Q[0] - y2 = Q[1] - x3 = (x1*y2+x2*y1) * inv(1+d*x1*x2*y1*y2) - y3 = (y1*y2+x1*x2) * inv(1-d*x1*x2*y1*y2) - return [x3 % q,y3 % q] -def scalarmult(P,e): - if e == 0: return [0,1] - Q = scalarmult(P,e/2) - Q = edwards(Q,Q) - if e & 1: Q = edwards(Q,P) - return Q +def edwards(P, Q): + x1, y1 = P + x2, y2 = Q + x3 = (x1 * y2 + x2 * y1) * inv(1 + d * x1 * x2 * y1 * y2) + y3 = (y1 * y2 + x1 * x2) * inv(1 - d * x1 * x2 * y1 * y2) + + return (x3 % q, y3 % q) + + +def scalarmult(P, e): + if e == 0: + return (0, 1) + + Q = scalarmult(P, e / 2) + Q = edwards(Q, Q) + + if e & 1: + Q = edwards(Q, P) + + return Q + def encodeint(y): - bits = [(y >> i) & 1 for i in range(b)] - return ''.join([chr(sum([bits[i * 8 + j] << j for j in range(8)])) for i in range(b/8)]) + bits = [(y >> i) & 1 for i in range(b)] + return ''.join([ + chr(sum([bits[i * 8 + j] << j for j in range(8)])) + for i in range(b/8) + ]) + def encodepoint(P): - x = P[0] - y = P[1] - bits = [(y >> i) & 1 for i in range(b - 1)] + [x & 1] - return ''.join([chr(sum([bits[i * 8 + j] << j for j in range(8)])) for i in range(b/8)]) + x = P[0] + y = P[1] + bits = [(y >> i) & 1 for i in range(b - 1)] + [x & 1] + return ''.join([ + chr(sum([bits[i * 8 + j] << j for j in range(8)])) + for i in range(b/8) + ]) + + +def bit(h, i): + return (ord(h[i / 8]) >> (i % 8)) & 1 -def bit(h,i): - return (ord(h[i/8]) >> (i%8)) & 1 def publickey(sk): - h = H(sk) - a = 2**(b-2) + sum(2**i * bit(h,i) for i in range(3,b-2)) - A = scalarmult(B,a) - return encodepoint(A) + h = H(sk) + a = 2 ** (b - 2) + sum(2 ** i * bit(h, i) for i in range(3, b - 2)) + A = scalarmult(B, a) + return encodepoint(A) + def Hint(m): - h = H(m) - return sum(2**i * bit(h,i) for i in range(2*b)) + h = H(m) + return sum(2 ** i * bit(h, i) for i in range(2 * b)) + + +def signature(m, sk, pk): + h = H(sk) + a = 2 ** (b - 2) + sum(2 ** i * bit(h, i) for i in range(3, b - 2)) + r = Hint(''.join([h[j] for j in range(b / 8, b / 4)]) + m) + R = scalarmult(B, r) + S = (r + Hint(encodepoint(R) + pk + m) * a) % l + return encodepoint(R) + encodeint(S) -def signature(m,sk,pk): - h = H(sk) - a = 2**(b-2) + sum(2**i * bit(h,i) for i in range(3,b-2)) - r = Hint(''.join([h[i] for i in range(b/8,b/4)]) + m) - R = scalarmult(B,r) - S = (r + Hint(encodepoint(R) + pk + m) * a) % l - return encodepoint(R) + encodeint(S) def isoncurve(P): - x = P[0] - y = P[1] - return (-x*x + y*y - 1 - d*x*x*y*y) % q == 0 + x, y = P + return (-x * x + y * y - 1 - d * x * x * y * y) % q == 0 + def decodeint(s): - return sum(2**i * bit(s,i) for i in range(0,b)) + return sum(2 ** i * bit(s, i) for i in range(0, b)) + def decodepoint(s): - y = sum(2**i * bit(s,i) for i in range(0,b-1)) - x = xrecover(y) - if x & 1 != bit(s,b-1): x = q-x - P = [x,y] - if not isoncurve(P): raise Exception("decoding point that is not on curve") - return P + y = sum(2 ** i * bit(s, i) for i in range(0, b - 1)) + x = xrecover(y) -def checkvalid(s,m,pk): - if len(s) != b/4: raise Exception("signature length is wrong") - if len(pk) != b/8: raise Exception("public-key length is wrong") - R = decodepoint(s[0:b/8]) - A = decodepoint(pk) - S = decodeint(s[b/8:b/4]) - h = Hint(encodepoint(R) + pk + m) - if scalarmult(B,S) != edwards(R,scalarmult(A,h)): - raise Exception("signature does not pass verification") + if x & 1 != bit(s, b-1): + x = q-x + + P = (x, y) + + if not isoncurve(P): + raise Exception("decoding point that is not on curve") + + return P + + +def checkvalid(s, m, pk): + if len(s) != b / 4: + raise Exception("signature length is wrong") + + if len(pk) != b / 8: + raise Exception("public-key length is wrong") + + R = decodepoint(s[:b / 8]) + A = decodepoint(pk) + S = decodeint(s[b / 8:b / 4]) + h = Hint(encodepoint(R) + pk + m) + + if scalarmult(B, S) != edwards(R, scalarmult(A, h)): + raise Exception("signature does not pass verification") diff --git a/ed25519/runtests.sh b/ed25519/runtests.sh new file mode 100755 index 00000000..4dec6743 --- /dev/null +++ b/ed25519/runtests.sh @@ -0,0 +1,8 @@ +#!/bin/sh +set -e + +python -u signfast.py < sign.input + +if [[ $TEST == 'slow' ]]; then + python -u sign.py < sign.input +fi diff --git a/ed25519/science.py b/ed25519/science.py new file mode 100644 index 00000000..18a2fcc2 --- /dev/null +++ b/ed25519/science.py @@ -0,0 +1,32 @@ +import os +import timeit + +import ed25519 + + +seed = os.urandom(32) + +data = "The quick brown fox jumps over the lazy dog" +private_key = seed +public_key = ed25519.publickey(seed) +signature = ed25519.signature(data, private_key, public_key) + + +print('Time generate') +print(timeit.timeit("ed25519.publickey(seed)", + setup="from __main__ import ed25519, seed", + number=10, +)) + +print('\nTime create signature') +print(timeit.timeit("ed25519.signature(data, private_key, public_key)", + setup="from __main__ import ed25519, data, private_key, public_key", + number=10, +)) + + +print('\nTime verify signature') +print(timeit.timeit("ed25519.checkvalid(signature, data, public_key)", + setup="from __main__ import ed25519, signature, data, public_key", + number=10, +)) diff --git a/ed25519/setup.py b/ed25519/setup.py new file mode 100644 index 00000000..d65e3d2a --- /dev/null +++ b/ed25519/setup.py @@ -0,0 +1,11 @@ +from setuptools import setup + + +setup( + name="ed25519", + version="1.0", + + py_modules="ed25519", + + zip_safe=False, +) diff --git a/ed25519/sign.py b/ed25519/sign.py index be099ad2..18eea684 100644 --- a/ed25519/sign.py +++ b/ed25519/sign.py @@ -1,3 +1,5 @@ +from __future__ import print_function + import sys import binascii import ed25519 @@ -16,6 +18,7 @@ while 1: line = sys.stdin.readline() if not line: break + print(".", end="") x = line.split(':') sk = binascii.unhexlify(x[0][0:64]) pk = ed25519.publickey(sk) diff --git a/ed25519/signfast.py b/ed25519/signfast.py new file mode 100644 index 00000000..630c92ca --- /dev/null +++ b/ed25519/signfast.py @@ -0,0 +1,48 @@ +from __future__ import print_function + +import sys +import binascii +import ed25519 + +# examples of inputs: see sign.input +# should produce no output: python sign.py < sign.input + +# warning: currently 37 seconds/line on a fast machine + +# fields on each input line: sk, pk, m, sm +# each field hex +# each field colon-terminated +# sk includes pk at end +# sm includes m at end + +MAX = 10 + +i = 0 +while 1: + if i >= MAX: + break + i += 1 + line = sys.stdin.readline() + if not line: break + print(".", end="") + x = line.split(':') + sk = binascii.unhexlify(x[0][0:64]) + pk = ed25519.publickey(sk) + m = binascii.unhexlify(x[2]) + s = ed25519.signature(m,sk,pk) + ed25519.checkvalid(s,m,pk) + forgedsuccess = 0 + try: + if len(m) == 0: + forgedm = "x" + else: + forgedmlen = len(m) + forgedm = ''.join([chr(ord(m[i])+(i==forgedmlen-1)) for i in range(forgedmlen)]) + ed25519.checkvalid(s,forgedm,pk) + forgedsuccess = 1 + except: + pass + assert not forgedsuccess + assert x[0] == binascii.hexlify(sk + pk) + assert x[1] == binascii.hexlify(pk) + assert x[3] == binascii.hexlify(s + m) diff --git a/ed25519/tox.ini b/ed25519/tox.ini new file mode 100644 index 00000000..1fdba166 --- /dev/null +++ b/ed25519/tox.ini @@ -0,0 +1,5 @@ +[tox] +envlist = py26,py27,pypy,py32,py33 + +[testenv] +commands = ./runtests.sh diff --git a/tuf/client/updater.py b/tuf/client/updater.py index 3b875fb3..331d20be 100755 --- a/tuf/client/updater.py +++ b/tuf/client/updater.py @@ -97,7 +97,6 @@ # The updated target files are saved locally to 'destination_directory'. for target in updated_targets: updater.download_target(target, destination_directory) - """ import errno @@ -194,7 +193,6 @@ class Updater(object): Any files located in 'destination_directory' that were previously served by the repository but have since been removed, can be deleted from disk by the client by calling this method. - """ def __init__(self, updater_name, repository_mirrors): @@ -251,7 +249,6 @@ def __init__(self, updater_name, repository_mirrors): None. - """ # Do the arguments have the correct format? @@ -327,7 +324,6 @@ def __init__(self, updater_name, repository_mirrors): def __str__(self): """ The string representation of an Updater object. - """ return self.name @@ -370,7 +366,6 @@ def _load_metadata_from_file(self, metadata_set, metadata_role): None. - """ # Ensure we have a valid metadata set. @@ -435,7 +430,6 @@ def _rebuild_key_and_role_db(self): None. - """ # Clobbering this means all delegated metadata files are rendered outdated @@ -475,7 +469,6 @@ def _import_delegations(self, parent_role): None. - """ current_parent_metadata = self.metadata['current'][parent_role] @@ -493,7 +486,7 @@ def _import_delegations(self, parent_role): # and load them. for keyid, keyinfo in keys_info.items(): if keyinfo['keytype'] == 'rsa': - rsa_key = tuf.rsa_key.create_from_metadata_format(keyinfo) + rsa_key = tuf.keys.create_from_metadata_format(keyinfo) # We specify the keyid to ensure that it's the correct keyid # for the key. @@ -556,7 +549,6 @@ def refresh(self): None. - """ # The timestamp role does not have signed metadata about it; otherwise we @@ -617,7 +609,6 @@ def __check_hashes(self, file_object, trusted_hashes): None. - """ # Verify each trusted hash of 'trusted_hashes'. Raise exception if @@ -700,7 +691,6 @@ def __soft_check_compressed_file_length(self, file_object, None. - """ observed_length = file_object.get_compressed_length() @@ -745,7 +735,6 @@ def get_target_file(self, target_filepath, compressed_file_length, A tuf.util.TempFile file-like object containing the target. - """ def verify_uncompressed_target_file(target_file_object): @@ -801,7 +790,6 @@ def __verify_uncompressed_metadata_file(self, metadata_file_object, None. - """ metadata = metadata_file_object.read() @@ -871,7 +859,6 @@ def unsafely_get_metadata_file(self, metadata_role, metadata_filepath, A tuf.util.TempFile file-like object containing the metadata. - """ def unsafely_verify_uncompressed_metadata_file(metadata_file_object): @@ -926,7 +913,6 @@ def safely_get_metadata_file(self, metadata_role, metadata_filepath, A tuf.util.TempFile file-like object containing the metadata. - """ def safely_verify_uncompressed_metadata_file(metadata_file_object): @@ -992,7 +978,6 @@ def __get_file(self, filepath, verify_uncompressed_file, file_type, A tuf.util.TempFile file-like object containing the metadata or target. - """ file_mirrors = tuf.mirrors.get_list_of_mirrors(file_type, filepath, @@ -1085,7 +1070,6 @@ def _update_metadata(self, metadata_role, fileinfo, compression=None): None. - """ # Construct the metadata filename as expected by the download/mirror modules. @@ -1234,7 +1218,6 @@ def _update_metadata_if_changed(self, metadata_role, referenced_metadata='releas None. - """ uncompressed_metadata_filename = metadata_role + '.txt' @@ -1373,7 +1356,6 @@ def _ensure_all_targets_allowed(self, metadata_role, metadata_object): None. - """ # Return if 'metadata_role' is 'targets'. 'targets' is not @@ -1536,7 +1518,6 @@ def _fileinfo_has_changed(self, metadata_filename, new_fileinfo): Boolean. True if the fileinfo has changed, false otherwise. - """ # If there is no fileinfo currently stored for 'metadata_filename', @@ -1595,7 +1576,6 @@ def _update_fileinfo(self, metadata_filename): None. - """ # In case we delayed loading the metadata and didn't do it in @@ -1640,7 +1620,6 @@ def _move_current_to_previous(self, metadata_role): None. - """ # Get the 'current' and 'previous' full file paths for 'metadata_role' @@ -1685,7 +1664,6 @@ def _delete_metadata(self, metadata_role): None. - """ # The root metadata role is never deleted without a replacement. @@ -1723,7 +1701,6 @@ def _ensure_not_expired(self, metadata_role): None. - """ # Construct the full metadata filename and the location of its @@ -1779,7 +1756,6 @@ def all_targets(self): A list of targets, conformant to 'tuf.formats.TARGETFILES_SCHEMA'. - """ # Load the most up-to-date targets of the 'targets' role and all @@ -1834,7 +1810,6 @@ def _refresh_targets_metadata(self, rolename='targets', include_delegations=Fals None. - """ roles_to_update = [] @@ -1886,7 +1861,6 @@ def _refresh_targets_metadata(self, rolename='targets', include_delegations=Fals def refresh_targets_metadata_chain(self, rolename): """ Proof-of-concept. - """ # List of parent roles to update. @@ -1993,7 +1967,6 @@ def _targets_of_role(self, rolename, targets=None, skip_refresh=False): A list of dict objects containing the target information of all the targets of 'rolename'. Conformant to 'tuf.formats.TARGETFILES_SCHEMA'. - """ if targets is None: @@ -2063,7 +2036,6 @@ def targets_of_role(self, rolename='targets'): A list of targets, conformant to 'tuf.formats.TARGETFILES_SCHEMA'. - """ # Does 'rolename' have the correct format? @@ -2104,7 +2076,6 @@ def target(self, target_filepath): The target information for 'target_filepath', conformant to 'tuf.formats.TARGETFILE_SCHEMA'. - """ # Does 'target_filepath' have the correct format? @@ -2152,7 +2123,6 @@ def _preorder_depth_first_walk(self, target_filepath): The target information for 'target_filepath', conformant to 'tuf.formats.TARGETFILE_SCHEMA'. - """ target = None @@ -2234,7 +2204,6 @@ def _get_target_from_targets_role(self, role_name, targets, target_filepath): The target information for 'target_filepath', conformant to 'tuf.formats.TARGETFILE_SCHEMA'. - """ target = None @@ -2295,7 +2264,6 @@ def _visit_child_role(self, child_role, target_filepath): 'target_filepath', then we return the role name of 'child_role'. Otherwise, we return None. - """ child_role_name = child_role['name'] @@ -2367,7 +2335,6 @@ def _get_target_hash(self, target_filepath, hash_function='sha256'): The hash of 'target_filepath'. - """ # Calculate the hash of the filepath to determine which bin to find the @@ -2417,7 +2384,6 @@ def remove_obsolete_targets(self, destination_directory): None. - """ # Does 'destination_directory' have the correct format? @@ -2480,7 +2446,6 @@ def updated_targets(self, targets, destination_directory): A list of targets, conformant to 'tuf.formats.TARGETFILES_SCHEMA'. - """ # Do the arguments have the correct format? @@ -2545,7 +2510,6 @@ def download_target(self, target, destination_directory): None. - """ # Do the arguments have the correct format? diff --git a/tuf/conf.py b/tuf/conf.py index 249ab870..9d271b86 100755 --- a/tuf/conf.py +++ b/tuf/conf.py @@ -65,3 +65,8 @@ # iteration setting used by the old '.key'. # https://en.wikipedia.org/wiki/PBKDF2 PBKDF2_ITERATIONS = 100000 + +# +# Supported Cryptography libraries: +# 'pycrypto', 'ed25519'. +CRYPTO_LIBRARY = 'ed25519' diff --git a/tuf/ed25519_key.py b/tuf/ed25519_key.py deleted file mode 100755 index 54d15f68..00000000 --- a/tuf/ed25519_key.py +++ /dev/null @@ -1,618 +0,0 @@ -""" - - ed25519_key.py - - - Vladimir Diaz - - - September 24, 2013. - - - See LICENSE for licensing information. - - - The goal of this module is to support ed25519 signatures. ed25519 is an - elliptic-curve public key signature scheme, its main strength being small - signatures (64 bytes) and small public keys (32 bytes). - http://ed25519.cr.yp.to/ - - 'tuf/ed25519_key.py' calls 'ed25519/ed25519.py', which is the pure Python - implementation of ed25519 provided by the author: - http://ed25519.cr.yp.to/software.html - Optionally, ed25519 cryptographic operations may be executed by PyNaCl, which - provides Python bindings to the NaCl library and is much faster than the pure - python implementation. PyNaCl relies on the C library, libsodium. - - https://github.com/dstufft/pynacl - https://github.com/jedisct1/libsodium - http://nacl.cr.yp.to/ - - The ed25519-related functions included here are generate(), create_signature() - and verify_signature(). The 'ed25519' and PyNaCl (i.e., 'nacl') modules used - by ed25519_key.py generate the actual ed25519 keys and the functions listed - above can be viewed as an easy-to-use public interface. Additional functions - contained here include create_in_metadata_format() and - create_from_metadata_format(). These last two functions produce or use - ed25519 keys compatible with the key structures listed in TUF Metadata files. - The generate() function returns a dictionary containing all the information - needed of ed25519 keys, such as public/private keys and a keyID identifier. - create_signature() and verify_signature() are supplemental functions used for - generating ed25519 signatures and verifying them. - - Key IDs are used as identifiers for keys (e.g., RSA key). They are the - hexadecimal representation of the hash of key object (specifically, the key - object containing only the public key). Review 'ed25519_key.py' and the - '_get_keyid()' function to see precisely how keyids are generated. One may - get the keyid of a key object by simply accessing the dictionary's 'keyid' - key (i.e., ed25519_key_dict['keyid']). - """ - -# Help with Python 3 compatability, 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 - -# Required for hexadecimal conversions. Signatures and public/private keys are -# hexlified. -import binascii - -# Generate OS-specific randomness (os.urandom) suitable for cryptographic use. -# http://docs.python.org/2/library/os.html#miscellaneous-functions -import os - -# Import the python implementation of the ed25519 algorithm that is provided by -# the author. Note: This implementation is very slow and does not include -# protection against side-channel attacks according to the author. Verifying -# signatures can take approximately 9 seconds on a intel core 2 duo @ -# 2.2 ghz x 2). Optionally, the PyNaCl module may be used to speed up ed25519 -# cryptographic operations. -# http://ed25519.cr.yp.to/software.html -try: - import nacl.signing - import nacl.encoding -except ImportError: - pass - -# The pure Python implementation of ed25519. -import ed25519.ed25519 - - -import tuf - -# Digest objects needed to generate hashes. -import tuf.hash - -# Perform object format-checking. -import tuf.formats - -# The default hash algorithm to use when generating KeyIDs. -_KEY_ID_HASH_ALGORITHM = 'sha256' - -# Supported ed25519 signing methods. 'ed25519-python' is the pure Python -# implementation signing method. 'ed25519-pynacl' (i.e., 'nacl' module) is the -# (libsodium+Python bindings) implementation signing method. -_SUPPORTED_ED25519_SIGNING_METHODS = ['ed25519-python', 'ed25519-pynacl'] - - -def generate(use_pynacl=False): - """ - - Generate an ed25519 seed key ('sk') and public key ('pk'). - In addition, a keyid used as an identifier for ed25519 keys is generated. - The object returned conforms to 'tuf.formats.ED25519KEY_SCHEMA' and has the - form: - {'keytype': 'ed25519', - 'keyid': keyid, - 'keyval': {'public': '876f5584a9db99b8546c0d8608d6...', - 'private': 'bf7336055c7638276efe9afe039...'}} - - The public and private keys are strings. An ed25519 seed key is a random - 32-byte value and public key 32 bytes, although both are hexlified. - - >>> ed25519_key = generate() - >>> tuf.formats.ED25519KEY_SCHEMA.matches(ed25519_key) - True - >>> len(ed25519_key['keyval']['public']) - 64 - >>> len(ed25519_key['keyval']['private']) - 64 - >>> ed25519_key_pynacl = generate(use_pynacl=True) - >>> tuf.formats.ED25519KEY_SCHEMA.matches(ed25519_key_pynacl) - True - >>> len(ed25519_key_pynacl['keyval']['public']) - 64 - >>> len(ed25519_key_pynacl['keyval']['private']) - 64 - - - use_pynacl: - True, if the ed25519 keys should be generated with PyNaCl. False, if the - keys should be generated with the pure Python implementation of ed25519 - (much slower). - - - NotImplementedError, if a randomness source is not found. - - - The ed25519 keys are generated by first creating a random 32-byte value - 'sk' with os.urandom() and then calling ed25519's ed25519.25519.publickey(sk) - or PyNaCl's nacl.signing.SigningKey(). - - - A dictionary containing the ed25519 keys and other identifying information. - Conforms to 'tuf.formats.ED25519KEY_SCHEMA'. - """ - - # Begin building the ed25519 key dictionary. - ed25519_key_dict = {} - keytype = 'ed25519' - - # Generate ed25519's seed key by calling os.urandom(). The random bytes - # returned should be suitable for cryptographic use and is OS-specific. - # Raise 'NotImplementedError' if a randomness source is not found. - # ed25519 seed keys are fixed at 32 bytes (256-bit keys). - # http://blog.mozilla.org/warner/2011/11/29/ed25519-keys/ - seed = os.urandom(32) - public = None - - if use_pynacl: - # Generate the public key. PyNaCl (i.e., 'nacl' module) performs - # the actual key generation. - nacl_key = nacl.signing.SigningKey(seed) - public = str(nacl_key.verify_key) - - # Use the pure Python implementation of ed25519. - else: - public = ed25519.ed25519.publickey(seed) - - # Generate the keyid for the ed25519 key dict. 'key_value' corresponds to the - # 'keyval' entry of the 'ED25519KEY_SCHEMA' dictionary. The seed (private) - # key information is not included in the generation of the 'keyid' identifier. - key_value = {'public': binascii.hexlify(public), - 'private': ''} - keyid = _get_keyid(key_value) - - # Build the 'ed25519_key_dict' dictionary. Update 'key_value' with the - # ed25519 seed key prior to adding 'key_value' to 'ed25519_key_dict'. - key_value['private'] = binascii.hexlify(seed) - - ed25519_key_dict['keytype'] = keytype - ed25519_key_dict['keyid'] = keyid - ed25519_key_dict['keyval'] = key_value - - return ed25519_key_dict - - - - - -def create_in_metadata_format(key_value, private=False): - """ - - Return a dictionary conformant to 'tuf.formats.KEY_SCHEMA'. - If 'private' is True, include the private key. The dictionary - returned has the form: - {'keytype': 'ed25519', - 'keyval': {'public': '876f5584a9db99b8546c0d8608d6...', - 'private': 'bf7336055c7638276efe9afe039...'}} - - or if 'private' is False: - - {'keytype': 'ed25519', - 'keyval': {'public': '876f5584a9db99b8546c0d8608d6...', - 'private': ''}} - - The private and public keys are 32 bytes, although hexlified. - - ed25519 keys are stored in Metadata files (e.g., root.txt) in the format - returned by this function. - - >>> ed25519_key = generate() - >>> key_val = ed25519_key['keyval'] - >>> ed25519_metadata = create_in_metadata_format(key_val, private=True) - >>> tuf.formats.KEY_SCHEMA.matches(ed25519_metadata) - True - - - key_value: - A dictionary containing a seed and public ed25519 key. - 'key_value' is of the form: - - {'public': '876f5584a9db99b8546c0d8608d6...', - 'private': 'bf7336055c7638276efe9afe039...'} - - conformat to 'tuf.formats.KEYVAL_SCHEMA'. - - private: - Indicates if the private key should be included in the - returned dictionary. - - - tuf.FormatError, if 'key_value' does not conform to - 'tuf.formats.KEYVAL_SCHEMA'. - - - None. - - - A 'KEY_SCHEMA' dictionary. - """ - - # Does 'key_value' have the correct format? - # This check will ensure 'key_value' has the appropriate number - # of objects and object types, and that all dict keys are properly named. - # Raise 'tuf.FormatError' if the check fails. - tuf.formats.KEYVAL_SCHEMA.check_match(key_value) - - if private is True and len(key_value['private']): - return {'keytype': 'ed25519', 'keyval': key_value} - else: - public_key_value = {'public': key_value['public'], 'private': ''} - return {'keytype': 'ed25519', 'keyval': public_key_value} - - - - - -def create_from_metadata_format(key_metadata): - """ - - Construct an ed25519 key dictionary (i.e., tuf.formats.ED25519KEY_SCHEMA) - from 'key_metadata'. The dict returned by this function has the exact - format as the dict returned by generate(). It is of the form: - - {'keytype': 'ed25519', - 'keyid': keyid, - 'keyval': {'public': '876f5584a9db99b8546c0d8608d6...', - 'private': 'bf7336055c7638276efe9afe039...'}} - - The public and private keys are 32-byte strings, although hexlified. - - ed25519 key dictionaries in 'ED25519KEY_SCHEMA' format should be used by - modules storing a collection of keys, such as a keydb keystore. - ed25519 keys as stored in metadata files use a different format, so this - function should be called if an ed25519 key is extracted from one of these - metadata files and needs converting. Generate() creates an entirely - new key and returns it in the format appropriate for 'keydb.py' and - 'keystore.py'. - - >>> ed25519_key = generate() - >>> key_val = ed25519_key['keyval'] - >>> ed25519_metadata = create_in_metadata_format(key_val, private=True) - >>> ed25519_key_2 = create_from_metadata_format(ed25519_metadata) - >>> tuf.formats.ED25519KEY_SCHEMA.matches(ed25519_key_2) - True - >>> ed25519_key == ed25519_key_2 - True - - - key_metadata: - The ed25519 key dictionary as stored in Metadata files, conforming to - 'tuf.formats.KEY_SCHEMA'. It has the form: - - {'keytype': 'ed25519', - 'keyval': {'public': '876f5584a9db99b8546c0d8608d6...', - 'private': 'bf7336055c7638276efe9afe039...'}} - - - tuf.FormatError, if 'key_metadata' does not conform to - 'tuf.formats.KEY_SCHEMA'. - - - None. - - - A dictionary containing the ed25519 keys and other identifying information. - """ - - # Does 'key_metadata' have the correct format? - # This check will ensure 'key_metadata' has the appropriate number - # of objects and object types, and that all dict keys are properly named. - # Raise 'tuf.FormatError' if the check fails. - tuf.formats.KEY_SCHEMA.check_match(key_metadata) - - # Construct the dictionary to be returned. - ed25519_key_dict = {} - keytype = 'ed25519' - key_value = key_metadata['keyval'] - - # Convert 'key_value' to 'tuf.formats.KEY_SCHEMA' and generate its hash - # The hash is in hexdigest form. _get_keyid() ensures the private key - # information is not included. - keyid = _get_keyid(key_value) - - # We now have all the required key values. Build 'ed25519_key_dict'. - ed25519_key_dict['keytype'] = keytype - ed25519_key_dict['keyid'] = keyid - ed25519_key_dict['keyval'] = key_value - - return ed25519_key_dict - - - - - -def _get_keyid(key_value): - """Return the keyid for 'key_value'.""" - - # 'keyid' will be generated from an object conformant to 'KEY_SCHEMA', - # which is the format Metadata files (e.g., root.txt) store keys. - # 'create_in_metadata_format()' returns the object needed by _get_keyid(). - ed25519_key_meta = create_in_metadata_format(key_value, private=False) - - # Convert the ed25519 key to JSON Canonical format suitable for adding - # to digest objects. - ed25519_key_update_data = tuf.formats.encode_canonical(ed25519_key_meta) - - # Create a digest object and call update(), using the JSON - # canonical format of 'ed25519_key_meta' as the update data. - digest_object = tuf.hash.digest(_KEY_ID_HASH_ALGORITHM) - digest_object.update(ed25519_key_update_data) - - # 'keyid' becomes the hexadecimal representation of the hash. - keyid = digest_object.hexdigest() - - return keyid - - - - - -def create_signature(ed25519_key_dict, data, use_pynacl=False): - """ - - Return a signature dictionary of the form: - {'keyid': 'a0469d9491e3c0b42dd41fe3455359dbacb3306b6e8fb59...', - 'method': 'ed25519-python', - 'sig': '4b3829671b2c6b90034518a918d2447c722474c878c2431dd...'} - - Note: 'method' may also be 'ed25519-pynacl', if the signature was created - by the 'nacl' module. - - The signing process will use the public and seed key - ed25519_key_dict['keyval']['private'], - ed25519_key_dict['keyval']['public'] - - and 'data' to generate the signature. - - >>> ed25519_key_dict = generate() - >>> data = 'The quick brown fox jumps over the lazy dog.' - >>> signature = create_signature(ed25519_key_dict, data) - >>> tuf.formats.SIGNATURE_SCHEMA.matches(signature) - True - >>> len(signature['sig']) - 128 - >>> signature_pynacl = create_signature(ed25519_key_dict, data, True) - >>> tuf.formats.SIGNATURE_SCHEMA.matches(signature_pynacl) - True - >>> len(signature_pynacl['sig']) - 128 - - - ed25519_key_dict: - A dictionary containing the ed25519 keys and other identifying information. - 'ed25519_key_dict' has the form: - - {'keytype': 'ed25519', - 'keyid': keyid, - 'keyval': {'public': '876f5584a9db99b8546c0d8608d6...', - 'private': 'bf7336055c7638276efe9afe039...'}} - - The public and private keys are 32-byte strings, although hexlified. - - data: - Data object used by create_signature() to generate the signature. - - use_pynacl: - True, if the ed25519 signature should be generated with PyNaCl. False, - if the signature should be generated with the pure Python implementation - of ed25519 (much slower). - - - TypeError, if a private key is not defined for 'ed25519_key_dict'. - - tuf.FormatError, if an incorrect format is found for 'ed25519_key_dict'. - - tuf.CryptoError, if a signature cannot be created. - - - ed25519.ed25519.signature() or nacl.signing.SigningKey.sign() called to - generate the actual signature. - - - A signature dictionary conformat to 'tuf.format.SIGNATURE_SCHEMA'. - ed25519 signatures are 64 bytes, however, the hexlified signature is - stored in the dictionary returned. - """ - - # Does 'ed25519_key_dict' have the correct format? - # This check will ensure 'ed25519_key_dict' has the appropriate number - # of objects and object types, and that all dict keys are properly named. - # Raise 'tuf.FormatError' if the check fails. - tuf.formats.ED25519KEY_SCHEMA.check_match(ed25519_key_dict) - - # Signing the 'data' object requires a seed and public key. - # 'ed25519.ed25519.py' generates the actual 64-byte signature in pure Python. - # nacl.signing.SigningKey.sign() generates the signature if 'use_pynacl' - # is True. - signature = {} - private_key = ed25519_key_dict['keyval']['private'] - public_key = ed25519_key_dict['keyval']['public'] - private_key = binascii.unhexlify(private_key) - public_key = binascii.unhexlify(public_key) - - keyid = ed25519_key_dict['keyid'] - method = None - sig = None - - # Verify the signature, but only if the private key has been set. The private - # key is a NULL string if unset. Although it may be clearer to explicit check - # that 'private_key' is not '', we can/should check for a value and not - # compare identities with the 'is' keyword. - if len(private_key): - if use_pynacl: - method = 'ed25519-pynacl' - try: - nacl_key = nacl.signing.SigningKey(private_key) - nacl_sig = nacl_key.sign(data) - sig = nacl_sig.signature - except (ValueError, nacl.signing.CryptoError): - message = 'An "ed25519-pynacl" signature could not be created.' - raise tuf.CryptoError(message) - - # Generate an "ed25519-python" (i.e., pure python implementation) signature. - else: - # ed25519.ed25519.signature() requires both the seed and public keys. - # It calculates the SHA512 of the seed key, which is 32 bytes. - method = 'ed25519-python' - try: - sig = ed25519.ed25519.signature(data, private_key, public_key) - except Exception, e: - message = 'An "ed25519-python" signature could not be generated.' - raise tuf.CryptoError(message) - - # Raise an exception since the private key is not defined. - else: - message = 'The required private key is not defined for "ed25519_key_dict".' - raise TypeError(message) - - # Build the signature dictionary to be returned. - # The hexadecimal representation of 'sig' is stored in the signature. - signature['keyid'] = keyid - signature['method'] = method - signature['sig'] = binascii.hexlify(sig) - - return signature - - - - - -def verify_signature(ed25519_key_dict, signature, data, use_pynacl=False): - """ - - Determine whether the seed key belonging to 'ed25519_key_dict' produced - 'signature'. verify_signature() will use the public key found in - 'ed25519_key_dict', the 'method' and 'sig' objects contained in 'signature', - and 'data' to complete the verification. Type-checking performed on both - 'ed25519_key_dict' and 'signature'. - - >>> ed25519_key_dict = generate() - >>> data = 'The quick brown fox jumps over the lazy dog.' - >>> signature = create_signature(ed25519_key_dict, data) - >>> verify_signature(ed25519_key_dict, signature, data) - True - >>> verify_signature(ed25519_key_dict, signature, data, True) - True - >>> bad_data = 'The sly brown fox jumps over the lazy dog.' - >>> bad_signature = create_signature(ed25519_key_dict, bad_data) - >>> verify_signature(ed25519_key_dict, bad_signature, data, True) - False - - - ed25519_key_dict: - A dictionary containing the ed25519 keys and other identifying - information. 'ed25519_key_dict' has the form: - - {'keytype': 'ed25519', - 'keyid': 'a0469d9491e3c0b42dd41fe3455359dbacb3306b6e8fb59...', - 'keyval': {'public': '876f5584a9db99b8546c0d8608d6...', - 'private': 'bf7336055c7638276efe9afe039...'}} - - The public and private keys are 32-byte strings, although hexlified. - - signature: - The signature dictionary produced by tuf.ed25519_key.create_signature(). - 'signature' has the form: - - {'keyid': 'a0469d9491e3c0b42dd41fe3455359dbacb3306b6e8fb59...', - 'method': 'ed25519-python', - 'sig': '4b3829671b2c6b90034518a918d2447c722474c878c2431dd...'} - - Conformant to 'tuf.formats.SIGNATURE_SCHEMA'. - - data: - Data object used by tuf.ed25519_key.create_signature() to generate - 'signature'. 'data' is needed here to verify the signature. - - use_pynacl: - True, if the ed25519 signature should be verified with PyNaCl. False, - if the signature should be verified with the pure Python implementation - of ed25519 (much slower). - - - tuf.UnknownMethodError. Raised if the signing method used by - 'signature' is not one supported by tuf.ed25519_key.create_signature(). - - tuf.FormatError. Raised if either 'ed25519_key_dict' - or 'signature' do not match their respective tuf.formats schema. - 'ed25519_key_dict' must conform to 'tuf.formats.ED25519KEY_SCHEMA'. - 'signature' must conform to 'tuf.formats.SIGNATURE_SCHEMA'. - - - ed25519.ed25519.checkvalid() called to do the actual verification. - nacl.signing.VerifyKey.verify() called if 'use_pynacl' is True. - - - Boolean. True if the signature is valid, False otherwise. - """ - - # Does 'ed25519_key_dict' have the correct format? - # This check will ensure 'ed25519_key_dict' has the appropriate number - # of objects and object types, and that all dict keys are properly named. - # Raise 'tuf.FormatError' if the check fails. - tuf.formats.ED25519KEY_SCHEMA.check_match(ed25519_key_dict) - - # Does 'signature' have the correct format? - tuf.formats.SIGNATURE_SCHEMA.check_match(signature) - - # Using the public key belonging to 'ed25519_key_dict' - # (i.e., ed25519_key_dict['keyval']['public']), verify whether 'signature' - # was produced by ed25519_key_dict's corresponding seed key - # ed25519_key_dict['keyval']['private']. Before returning the Boolean result, - # ensure 'ed25519-python' or 'ed25519-pynacl' was used as the signing method. - method = signature['method'] - sig = signature['sig'] - sig = binascii.unhexlify(sig) - public = ed25519_key_dict['keyval']['public'] - public = binascii.unhexlify(public) - valid_signature = False - - if method in _SUPPORTED_ED25519_SIGNING_METHODS: - if use_pynacl: - try: - nacl_verify_key = nacl.signing.VerifyKey(public) - nacl_message = nacl_verify_key.verify(data, sig) - if nacl_message == data: - valid_signature = True - except nacl.signing.BadSignatureError: - pass - - # Verify signature with 'ed25519-python' (i.e., pure Python implementation). - else: - try: - ed25519.ed25519.checkvalid(sig, data, public) - valid_signature = True - - # The pure Python implementation raises 'Exception' if 'signature' is - # invalid. - except Exception, e: - pass - else: - message = 'Unsupported ed25519 signing method: '+repr(method)+'.\n'+ \ - 'Supported methods: '+repr(_SUPPORTED_ED25519_SIGNING_METHODS)+'.' - raise tuf.UnknownMethodError(message) - - return valid_signature - - - -if __name__ == '__main__': - # The interactive sessions of the documentation strings can - # be tested by running 'ed25519_key.py' as a standalone module. - # python -B ed25519_key.py - import doctest - doctest.testmod() diff --git a/tuf/ed25519_keys.py b/tuf/ed25519_keys.py new file mode 100755 index 00000000..ea7a0735 --- /dev/null +++ b/tuf/ed25519_keys.py @@ -0,0 +1,374 @@ +""" + + ed25519_keys.py + + + Vladimir Diaz + + + September 24, 2013. + + + See LICENSE for licensing information. + + + The goal of this module is to support ed25519 signatures. ed25519 is an + elliptic-curve public key signature scheme, its main strength being small + signatures (64 bytes) and small public keys (32 bytes). + http://ed25519.cr.yp.to/ + + 'tuf/ed25519.py' calls 'ed25519/ed25519.py', which is the pure Python + implementation of ed25519 that has been optimized for a faster runtime. + The Python reference implementation is concise but very slow. + http://ed25519.cr.yp.to/software.html + https://github.com/pyca/ed25519 + + Optionally, ed25519 cryptographic operations may be executed by PyNaCl, which + is a Python binding to the NaCl library and is faster than the pure + python implementation. PyNaCl relies on the libsodium C library. + + https://github.com/pyca/pynacl + https://github.com/jedisct1/libsodium + http://nacl.cr.yp.to/ + + The ed25519-related functions included here are generate(), create_signature() + and verify_signature(). The 'ed25519' and PyNaCl (i.e., 'nacl') modules used + by ed25519_key.py generate the actual ed25519 keys and the functions listed + above can be viewed as an easy-to-use public interface. Additional functions + contained here include create_in_metadata_format() and + create_from_metadata_format(). These last two functions produce or use + ed25519 keys compatible with the key structures listed in TUF Metadata files. + The generate() function returns a dictionary containing all the information + needed of ed25519 keys, such as public/private keys and a keyID identifier. + create_signature() and verify_signature() are supplemental functions used for + generating ed25519 signatures and verifying them. + """ + +# Help with Python 3 compatability, 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 + +# Required for hexadecimal conversions. Signatures and public/private keys are +# hexlified. +import binascii + +# Generate OS-specific randomness (os.urandom) suitable for cryptographic use. +# http://docs.python.org/2/library/os.html#miscellaneous-functions +import os + +# Import the python implementation of the ed25519 algorithm that is provided by +# the author. Note: This implementation is very slow and does not include +# protection against side-channel attacks according to the author. Verifying +# signatures can take approximately 9 seconds on a intel core 2 duo @ +# 2.2 ghz x 2). Optionally, the PyNaCl module may be used to speed up ed25519 +# cryptographic operations. +# http://ed25519.cr.yp.to/software.html +try: + import nacl.signing + import nacl.encoding +except ImportError: + pass + +# The pure Python implementation of ed25519. +import ed25519.ed25519 + +import tuf # Digest objects needed to generate hashes. +import tuf.hash + +# Perform object format-checking. +import tuf.formats + +# The default hash algorithm to use when generating KeyIDs. +_KEY_ID_HASH_ALGORITHM = 'sha256' + +# Supported ed25519 signing methods. 'ed25519-python' is the pure Python +# implementation signing method. 'ed25519-pynacl' (i.e., 'nacl' module) is the +# (libsodium+Python bindings) implementation signing method. +_SUPPORTED_ED25519_SIGNING_METHODS = ['ed25519-python', 'ed25519-pynacl'] + + +def generate_public_and_private(use_pynacl=False): + """ + + Generate an ed25519 seed key ('sk') and public key ('pk'). + In addition, a keyid used as an identifier for ed25519 keys is generated. + The object returned conforms to 'tuf.formats.ED25519KEY_SCHEMA' and has the + form: + {'keytype': 'ed25519', + 'keyid': keyid, + 'keyval': {'public': '876f5584a9db99b8546c0d8608d6...', + 'private': 'bf7336055c7638276efe9afe039...'}} + + The public and private keys are strings. An ed25519 seed key is a random + 32-byte value. Public keys are also 32 bytes. + + >>> public, private = generate_public_and_private(use_pynacl=False) + >>> len(public) + 32 + >>> len(private) + 32 + >>> public, private = generate_public_and_private(use_pynacl=True) + >>> len(public) + 32 + >>> len(private) + 32 + + + use_pynacl: + True, if the ed25519 keys should be generated with PyNaCl. False, if the + keys should be generated with the pure Python implementation of ed25519 + (much slower). + + + NotImplementedError, if a randomness source is not found. + + + The ed25519 keys are generated by first creating a random 32-byte value + 'sk' with os.urandom() and then calling ed25519's ed25519.25519.publickey(sk) + or PyNaCl's nacl.signing.SigningKey(). + + + A dictionary containing the ed25519 keys and other identifying information. + Conforms to 'tuf.formats.ED25519KEY_SCHEMA'. + """ + + # Generate ed25519's seed key by calling os.urandom(). The random bytes + # returned should be suitable for cryptographic use and is OS-specific. + # Raise 'NotImplementedError' if a randomness source is not found. + # ed25519 seed keys are fixed at 32 bytes (256-bit keys). + # http://blog.mozilla.org/warner/2011/11/29/ed25519-keys/ + seed = os.urandom(32) + public = None + + if use_pynacl: + # Generate the public key. PyNaCl (i.e., 'nacl' module) performs + # the actual key generation. + nacl_key = nacl.signing.SigningKey(seed) + public = str(nacl_key.verify_key) + + # Use the pure Python implementation of ed25519. + else: + public = ed25519.ed25519.publickey(seed) + + return public, seed + + + + + +def create_signature(public_key, private_key, data, use_pynacl=False): + """ + + Return a signature dictionary of the form: + {'keyid': 'a0469d9491e3c0b42dd41fe3455359dbacb3306b6e8fb59...', + 'method': 'ed25519-python', + 'sig': '4b3829671b2c6b90034518a918d2447c722474c878c2431dd...'} + + Note: 'method' may also be 'ed25519-pynacl', if the signature was created + by the 'nacl' module. + + The signing process will use the public and seed key + ed25519_key_dict['keyval']['private'], + ed25519_key_dict['keyval']['public'] + + and 'data' to generate the signature. + + >>> public, private = generate_public_and_private(use_pynacl=False) + >>> data = 'The quick brown fox jumps over the lazy dog' + >>> signature, method = create_signature(public, private, data, use_pynacl=False) + >>> len(signature) + 64 + >>> method == 'ed25519-python' + True + >>> signature, method = create_signature(public, private, data, use_pynacl=True) + >>> len(signature) + 64 + >>> method == 'ed25519-pynacl' + True + + + public: + The ed25519 public key, which is a 32-byte string. + + private: + The ed25519 private key, which is a 32-byte string. + + data: + Data object used by create_signature() to generate the signature. + + use_pynacl: + True, if the ed25519 signature should be generated with PyNaCl. False, + if the signature should be generated with the pure Python implementation + of ed25519 (much slower). + + + TypeError, if a private key is not defined for 'ed25519_key_dict'. + + tuf.FormatError, if an incorrect format is found for 'ed25519_key_dict'. + + tuf.CryptoError, if a signature cannot be created. + + + ed25519.ed25519.signature() or nacl.signing.SigningKey.sign() called to + generate the actual signature. + + + A signature dictionary conformat to 'tuf.format.SIGNATURE_SCHEMA'. + ed25519 signatures are 64 bytes, however, the hexlified signature is + stored in the dictionary returned. + """ + + # Signing the 'data' object requires a seed and public key. + # 'ed25519.ed25519.py' generates the actual 64-byte signature in pure Python. + # nacl.signing.SigningKey.sign() generates the signature if 'use_pynacl' + # is True. + public = public_key + private = private_key + + method = None + signature = None + + # Verify the signature, but only if the private key has been set. The private + # key is a NULL string if unset. Although it may be clearer to explicit check + # that 'private_key' is not '', we can/should check for a value and not + # compare identities with the 'is' keyword. + if len(private_key): + if use_pynacl: + method = 'ed25519-pynacl' + try: + nacl_key = nacl.signing.SigningKey(private) + nacl_sig = nacl_key.sign(data) + signature = nacl_sig.signature + except (ValueError, nacl.signing.CryptoError): + message = 'An "ed25519-pynacl" signature could not be created.' + raise tuf.CryptoError(message) + + # Generate an "ed25519-python" (i.e., pure python implementation) signature. + else: + # ed25519.ed25519.signature() requires both the seed and public keys. + # It calculates the SHA512 of the seed key, which is 32 bytes. + method = 'ed25519-python' + try: + signature = ed25519.ed25519.signature(data, private, public) + except Exception, e: + message = 'An "ed25519-python" signature could not be generated.' + raise tuf.CryptoError(message) + + # Raise an exception since the private key is not defined. + else: + message = 'The required "private_key" key is not defined.' + raise TypeError(message) + + return signature, method + + + + + +def verify_signature(public_key, method, signature, data, use_pynacl=False): + """ + + Determine whether the seed key belonging to 'ed25519_key_dict' produced + 'signature'. verify_signature() will use the public key found in + 'ed25519_key_dict', the 'method' and 'sig' objects contained in 'signature', + and 'data' to complete the verification. Type-checking performed on both + 'ed25519_key_dict' and 'signature'. + + >>> public, private = generate_public_and_private(use_pynacl=False) + >>> data = 'The quick brown fox jumps over the lazy dog' + >>> signature, method = create_signature(public, private, data, use_pynacl=False) + >>> verify_signature(public, method, signature, data, use_pynacl=False) + True + >>> verify_signature(public, method, signature, data, use_pynacl=True) + True + >>> bad_data = 'The sly brown fox jumps over the lazy dog' + >>> bad_signature, method = create_signature(public, private, bad_data, use_pynacl=False) + >>> verify_signature(public, method, bad_signature, data, use_pynacl=False) + False + + + public_key: + The public key is a 32-byte string. + + signature: + The signature dictionary produced by tuf.ed25519_key.create_signature(). + 'signature' has the form: + + {'keyid': 'a0469d9491e3c0b42dd41fe3455359dbacb3306b6e8fb59...', + 'method': 'ed25519-python', + 'sig': '4b3829671b2c6b90034518a918d2447c722474c878c2431dd...'} + + Conformant to 'tuf.formats.SIGNATURE_SCHEMA'. + + data: + Data object used by tuf.ed25519_key.create_signature() to generate + 'signature'. 'data' is needed here to verify the signature. + + use_pynacl: + True, if the ed25519 signature should be verified with PyNaCl. False, + if the signature should be verified with the pure Python implementation + of ed25519 (much slower). + + + tuf.UnknownMethodError. Raised if the signing method used by + 'signature' is not one supported by tuf.ed25519_key.create_signature(). + + tuf.FormatError. Raised if either 'ed25519_key_dict' + or 'signature' do not match their respective tuf.formats schema. + 'ed25519_key_dict' must conform to 'tuf.formats.ED25519KEY_SCHEMA'. + 'signature' must conform to 'tuf.formats.SIGNATURE_SCHEMA'. + + + ed25519.ed25519.checkvalid() called to do the actual verification. + nacl.signing.VerifyKey.verify() called if 'use_pynacl' is True. + + + Boolean. True if the signature is valid, False otherwise. + """ + + # Using the public key belonging to 'ed25519_key_dict' + # (i.e., ed25519_key_dict['keyval']['public']), verify whether 'signature' + # was produced by ed25519_key_dict's corresponding seed key + # ed25519_key_dict['keyval']['private']. Before returning the Boolean result, + # ensure 'ed25519-python' or 'ed25519-pynacl' was used as the signing method. + public = public_key + valid_signature = False + + if method in _SUPPORTED_ED25519_SIGNING_METHODS: + if use_pynacl: + try: + nacl_verify_key = nacl.signing.VerifyKey(public) + nacl_message = nacl_verify_key.verify(data, signature) + if nacl_message == data: + valid_signature = True + except nacl.signing.BadSignatureError: + pass + + # Verify signature with 'ed25519-python' (i.e., pure Python implementation). + else: + try: + ed25519.ed25519.checkvalid(signature, data, public) + valid_signature = True + + # The pure Python implementation raises 'Exception' if 'signature' is + # invalid. + except Exception, e: + pass + else: + message = 'Unsupported ed25519 signing method: '+repr(method)+'.\n'+ \ + 'Supported methods: '+repr(_SUPPORTED_ED25519_SIGNING_METHODS)+'.' + raise tuf.UnknownMethodError(message) + + return valid_signature + + + +if __name__ == '__main__': + # The interactive sessions of the documentation strings can + # be tested by running 'ed25519.py' as a standalone module. + # python -B ed25519.py + import doctest + doctest.testmod() diff --git a/tuf/evp.py b/tuf/evp.py new file mode 100755 index 00000000..60895bbd --- /dev/null +++ b/tuf/evp.py @@ -0,0 +1,425 @@ +""" + + evp.py + + + Vladimir Diaz + + + October 2013. + + + See LICENSE for licensing information. + + + The goal of this module is to support public-key cryptography using + the RSA algorithm. The RSA-related functions provided include + generate(), create_signature(), and verify_signature(). The 'evpy' package + used by 'rsa_key.py' generates the actual RSA keys and the functions listed + above can be viewed as an easy-to-use public interface. Additional functions + contained here include create_in_metadata_format() and + create_from_metadata_format(). These last two functions produce or use RSA + keys compatible with the key structures listed in TUF Metadata files. + The generate() function returns a dictionary containing all the information + needed of RSA keys, such as public and private keys, keyIDs, and an iden- + fier. create_signature() and verify_signature() are supplemental functions + used for generating RSA signatures and verifying them. + + Key IDs are used as identifiers for keys (e.g., RSA key). They are the + hexadecimal representation of the hash of key object (specifically, the key + object containing only the public key). See 'rsa_key.py' and the + '_get_keyid()' function to see precisely how keyids are generated. One may + get the keyid of a key object by simply accessing the dictionary's 'keyid' + key (i.e., rsakey['keyid']). + +""" + + +# Required for hexadecimal conversions. +import binascii + +# Needed to generate, sign, and verify RSA keys. +import evpy.signature +import evpy.envelope + +# Digest objects needed to generate hashes. +import tuf.hash + +# Perform object format-checking. +import tuf.formats + + +_KEY_ID_HASH_ALGORITHM = 'sha256' + +# Recommended RSA key sizes: http://www.rsa.com/rsalabs/node.asp?id=2004 +# According to the document above, revised May 6, 2003, RSA keys of +# size 3072 provide security through 2031 and beyond. +_DEFAULT_RSA_KEY_BITS = 3072 + + +def generate(bits=_DEFAULT_RSA_KEY_BITS): + """ + + Generate public and private RSA keys, with modulus length 'bits'. + In addition, a keyid used as an identifier for RSA keys is generated. + The object returned conforms to tuf.formats.RSAKEY_SCHEMA and as the form: + {'keytype': 'rsa', + 'keyid': keyid, + 'keyval': {'public': '-----BEGIN RSA PUBLIC KEY----- ...', + 'private': '-----BEGIN RSA PRIVATE KEY----- ...'}} + + The public and private keys are in PEM format and stored as strings. + + + bits: + The key size, or key length, of the RSA key. + + + tuf.CryptoError, if an exception occurs after calling evpy.envelope.keygen(). + + tuf.FormatError, if 'bits' does not contain the correct format. + + + The RSA keys are generated by calling evpy.envelope.keygen(). + + + A dictionary containing the RSA keys and other identifying information. + + """ + + + # Does 'bits' have the correct format? + # This check will ensure 'bits' conforms to 'tuf.formats.RSAKEYBITS_SCHEMA'. + # 'bits' must be an integer object, with a minimum value of 2048. + # Raise 'tuf.FormatError' if the check fails. + tuf.formats.RSAKEYBITS_SCHEMA.check_match(bits) + + # Begin building the RSA key dictionary. + rsakey_dict = {} + keytype = 'rsa' + + # Generate the public and private keys. 'public_key' and 'private_key' + # will both be strings containing RSA keys in PEM format. + # The evpy.envelope module performs the actual key generation. The + # evpy.envelope.keygen() function returns a (public, private) tuple. + + try: + public_key, private_key = evpy.envelope.keygen(bits, pem=True) + except (evpy.envelope.EnvelopeError, evpy.envelope.KeygenError, MemoryError), e: + raise tuf.CryptoError(e) + + # Generate the keyid for the RSA key. 'key_value' corresponds to the + # 'keyval' entry of the RSAKEY_SCHEMA dictionary. + key_value = {'public': public_key, + 'private': ''} + + keyid = _get_keyid(key_value) + + # Build the 'rsakey_dict' dictionary. + # Update 'key_value' with the RSA private key prior to adding + # 'key_value' to 'rsakey_dict'. + key_value['private'] = private_key + + rsakey_dict['keytype'] = keytype + rsakey_dict['keyid'] = keyid + rsakey_dict['keyval'] = key_value + + return rsakey_dict + + + + + +def create_in_metadata_format(key_value, private=False): + """ + + Return a dictionary conformant to tuf.formats.KEY_SCHEMA. + If 'private' is True, include the private key. The dictionary + returned has the form: + {'keytype': 'rsa', + 'keyval': {'public': '-----BEGIN RSA PUBLIC KEY----- ...', + 'private': '-----BEGIN RSA PRIVATE KEY----- ...'}} + or + + {'keytype': 'rsa', + 'keyval': {'public': '-----BEGIN RSA PUBLIC KEY----- ...', + 'private': ''}} if 'private' is False. + + The private and public keys are in PEM format. + + RSA keys are stored in Metadata files (e.g., root.txt) in the format + returned by this function. + + + key_value: + A dictionary containing a private and public RSA key. + 'key_value' is of the form: + + {'public': '-----BEGIN RSA PUBLIC KEY----- ...', + 'private': '-----BEGIN RSA PRIVATE KEY----- ...'}}, + conformat to tuf.formats.KEYVAL_SCHEMA. + + private: + Indicates if the private key should be included in the + returned dictionary. + + + tuf.FormatError, if 'key_value' does not conform to + tuf.formats.KEYVAL_SCHEMA. + + + None. + + + An KEY_SCHEMA dictionary. + + """ + + + # Does 'key_value' have the correct format? + # This check will ensure 'key_value' has the appropriate number + # of objects and object types, and that all dict keys are properly named. + # Raise 'tuf.FormatError' if the check fails. + tuf.formats.KEYVAL_SCHEMA.check_match(key_value) + + if private and key_value['private']: + return {'keytype': 'rsa', 'keyval': key_value} + else: + public_key_value = {'public': key_value['public'], 'private': ''} + return {'keytype': 'rsa', 'keyval': public_key_value} + + + + + +def create_from_metadata_format(key_metadata): + """ + + Construct an RSA key dictionary (i.e., tuf.formats.RSAKEY_SCHEMA) + from 'key_metadata'. The dict returned by this function has the exact + format as the dict returned by generate(). It is of the form: + + {'keytype': 'rsa', + 'keyid': keyid, + 'keyval': {'public': '-----BEGIN RSA PUBLIC KEY----- ...', + 'private': '-----BEGIN RSA PRIVATE KEY----- ...'}} + + The public and private keys are in PEM format and stored as strings. + + RSA key dictionaries in RSAKEY_SCHEMA format should be used by + modules storing a collection of keys, such as a keydb and keystore. + RSA keys as stored in metadata files use a different format, so this + function should be called if an RSA key is extracted from one of these + metadata files and needs converting. Generate() creates an entirely + new key and returns it in the format appropriate for keydb and keystore. + + + key_metadata: + The RSA key dictionary as stored in Metadata files, conforming to + tuf.formats.KEY_SCHEMA. + + + tuf.FormatError, if 'key_metadata' does not conform to + tuf.formats.KEY_SCHEMA. + + + None. + + + A dictionary containing the RSA keys and other identifying information. + + """ + + + # Does 'key_metadata' have the correct format? + # This check will ensure 'key_metadata' has the appropriate number + # of objects and object types, and that all dict keys are properly named. + # Raise 'tuf.FormatError' if the check fails. + tuf.formats.KEY_SCHEMA.check_match(key_metadata) + + # Construct the dictionary to be returned. + rsakey_dict = {} + keytype = 'rsa' + key_value = key_metadata['keyval'] + + keyid = _get_keyid(key_value) + + # We now have all the required key values. + # Build 'rsakey_dict'. + rsakey_dict['keytype'] = keytype + rsakey_dict['keyid'] = keyid + rsakey_dict['keyval'] = key_value + + return rsakey_dict + + + + + +def _get_keyid(key_value): + """Return the keyid for 'key_value'.""" + + # 'keyid' will be generated from an object conformant to KEY_SCHEMA, + # which is the format Metadata files (e.g., root.txt) store keys. + # 'create_in_metadata_format()' returns the object needed by _get_keyid(). + rsakey_meta = create_in_metadata_format(key_value, private=False) + + # Convert the RSA key to JSON Canonical format suitable for adding + # to digest objects. + rsakey_update_data = tuf.formats.encode_canonical(rsakey_meta) + + # Create a digest object and call update(), using the JSON + # canonical format of 'rskey_meta' as the update data. + digest_object = tuf.hash.digest(_KEY_ID_HASH_ALGORITHM) + digest_object.update(rsakey_update_data) + + # 'keyid' becomes the hexadecimal representation of the hash. + keyid = digest_object.hexdigest() + + return keyid + + + + + +def create_signature(rsakey_dict, data): + """ + + Return a signature dictionary of the form: + {'keyid': keyid, 'method': 'evp', 'sig': sig}. + + The signing process will use the private key + rsakey_dict['keyval']['private'] and 'data' to generate the signature. + + + rsakey_dict: + A dictionary containing the RSA keys and other identifying information. + 'rsakey_dict' has the form: + + {'keytype': 'rsa', + 'keyid': keyid, + 'keyval': {'public': '-----BEGIN RSA PUBLIC KEY----- ...', + 'private': '-----BEGIN RSA PRIVATE KEY----- ...'}} + + The public and private keys are in PEM format and stored as strings. + + data: + Data object used by create_signature() to generate the signature. + + + TypeError, if a private key is not defined for 'rsakey_dict'. + + tuf.FormatError, if an incorrect format is found for the + 'rsakey_dict' object. + + + evpy.signature.sign() called to perform the actual signing. + + + A signature dictionary conformat to tuf.format.SIGNATURE_SCHEMA. + + """ + + + # Does 'rsakey_dict' have the correct format? + # This check will ensure 'rsakey_dict' has the appropriate number + # of objects and object types, and that all dict keys are properly named. + # Raise 'tuf.FormatError' if the check fails. + tuf.formats.RSAKEY_SCHEMA.check_match(rsakey_dict) + + # Signing the 'data' object requires a private key. + # The 'evp' (i.e., evpy) signing method is the only method + # currently supported. + signature = {} + private_key = rsakey_dict['keyval']['private'] + keyid = rsakey_dict['keyid'] + method = 'evp' + + if private_key: + sig = evpy.signature.sign(data, key=private_key) + else: + raise TypeError('The required private key is not defined for rsakey_dict.') + + # Build the signature dictionary to be returned. + # The hexadecimal representation of 'sig' is stored in the signature. + signature['keyid'] = keyid + signature['method'] = method + signature['sig'] = binascii.hexlify(sig) + + return signature + + + + + +def verify_signature(rsakey_dict, signature, data): + """ + + Determine whether the private key belonging to 'rsakey_dict' produced + 'signature'. verify_signature() will use the public key found in + 'rsakey_dict', the 'method' and 'sig' objects contained in 'signature', + and 'data' to complete the verification. Type-checking performed on both + 'rsakey_dict' and 'signature'. + + + rsakey_dict: + A dictionary containing the RSA keys and other identifying information. + 'rsakey_dict' has the form: + + {'keytype': 'rsa', + 'keyid': keyid, + 'keyval': {'public': '-----BEGIN RSA PUBLIC KEY----- ...', + 'private': '-----BEGIN RSA PRIVATE KEY----- ...'}} + + The public and private keys are in PEM format and stored as strings. + + signature: + The signature dictionary produced by tuf.rsa_key.create_signature(). + 'signature' has the form: + {'keyid': keyid, 'method': 'method', 'sig': sig}. Conformant to + tuf.formats.SIGNATURE_SCHEMA. + + data: + Data object used by tuf.rsa_key.create_signature() to generate + 'signature'. 'data' is needed here to verify the signature. + + + tuf.UnknownMethodError. Raised if the signing method used by + 'signature' is not one supported by tuf.rsa_key.create_signature(). + + tuf.FormatError. Raised if either 'rsakey_dict' + or 'signature' do not match their respective tuf.formats schema. + 'rsakey_dict' must conform to tuf.formats.RSAKEY_SCHEMA. + 'signature' must conform to tuf.formats.SIGNATURE_SCHEMA. + + + evpy.signature_verify() called to do the actual verification. + + + Boolean. + + """ + + + # Does 'rsakey_dict' have the correct format? + # This check will ensure 'rsakey_dict' has the appropriate number + # of objects and object types, and that all dict keys are properly named. + # Raise 'tuf.FormatError' if the check fails. + tuf.formats.RSAKEY_SCHEMA.check_match(rsakey_dict) + + # Does 'signature' have the correct format? + tuf.formats.SIGNATURE_SCHEMA.check_match(signature) + + # Using the public key belonging to 'rsakey_dict' + # (i.e., rsakey_dict['keyval']['public']), verify whether 'signature' + # was produced by rsakey_dict's corresponding private key + # rsakey_dict['keyval']['private']. Before returning the Boolean result, + # ensure 'evp' was used as the signing method. + + method = signature['method'] + sig = signature['sig'] + public_key = rsakey_dict['keyval']['public'] + + if method != 'evp': + raise tuf.UnknownMethodError(method) + return evpy.signature.verify(data, binascii.unhexlify(sig), key=public_key) diff --git a/tuf/keydb.py b/tuf/keydb.py index 601993b3..95a9f896 100755 --- a/tuf/keydb.py +++ b/tuf/keydb.py @@ -33,7 +33,7 @@ import tuf import tuf.formats -import tuf.rsa_key +import tuf.keys # See 'log.py' to learn how logging is handled in TUF. logger = logging.getLogger('tuf.keydb') @@ -88,7 +88,7 @@ def create_keydb_from_root_metadata(root_metadata): # 'key_metadata' is stored in 'KEY_SCHEMA' format. Call # create_from_metadata_format() to get the key in 'RSAKEY_SCHEMA' # format, which is the format expected by 'add_rsakey()'. - rsakey_dict = tuf.rsa_key.create_from_metadata_format(key_metadata) + rsakey_dict = tuf.keys.rsa_keys.create_from_metadata_format(key_metadata) try: add_rsakey(rsakey_dict, keyid) # 'tuf.Error' raised if keyid does not match the keyid for 'rsakey_dict'. @@ -138,9 +138,7 @@ def add_rsakey(rsakey_dict, keyid=None): None. - """ - # Does 'rsakey_dict' have the correct format? # This check will ensure 'rsakey_dict' has the appropriate number of objects diff --git a/tuf/rsa_key.py b/tuf/keys.py similarity index 60% rename from tuf/rsa_key.py rename to tuf/keys.py index 8d737391..0bec296d 100755 --- a/tuf/rsa_key.py +++ b/tuf/keys.py @@ -1,12 +1,12 @@ """ - rsa_key.py + keys.py Vladimir Diaz - March 9, 2012. Based on a previous version of this module by Geremy Condra. + October 4, 2013. See LICENSE for licensing information. @@ -33,32 +33,36 @@ '_get_keyid()' function to see precisely how keyids are generated. One may get the keyid of a key object by simply accessing the dictionary's 'keyid' key (i.e., rsakey['keyid']). - """ - # Required for hexadecimal conversions. Signatures are hexlified. import binascii -# Crypto.PublicKey (i.e., PyCrypto public-key cryptography) provides algorithms -# such as Digital Signature Algorithm (DSA) and the ElGamal encryption system. -# 'Crypto.PublicKey.RSA' is needed here to generate, sign, and verify RSA keys. -import Crypto.PublicKey.RSA +# +_SUPPORTED_CRYPTO_LIBRARIES = \ + ['pycrypto', 'ed25519', 'evp'] -# PyCrypto requires 'Crypto.Hash' hash objects to generate PKCS#1 PSS -# signatures (i.e., Crypto.Signature.PKCS1_PSS). -import Crypto.Hash.SHA256 +# +_available_crypto_libraries = ['ed25519-python'] + +try: + import Crypto + import tuf.pycrypto_keys.py + _available_crypto_libraries.append('pycrypto') +except ImportError: + pass + +try: + import nacl + _available_crypto_libraries.append('ed25519-pynacl') +except ImportError: + pass + +import tuf.ed25519_keys -# RSA's probabilistic signature scheme with appendix (RSASSA-PSS). -# PKCS#1 v1.5 is provided for compatability with existing applications, but -# RSASSA-PSS is encouraged for newer applications. RSASSA-PSS generates -# a random salt to ensure the signature generated is probabilistic rather than -# deterministic, like PKCS#1 v1.5. -# http://en.wikipedia.org/wiki/RSA-PSS#Schemes -# https://tools.ietf.org/html/rfc3447#section-8.1 -import Crypto.Signature.PKCS1_PSS import tuf +import tuf.conf # Digest objects needed to generate hashes. import tuf.hash @@ -75,8 +79,11 @@ # size 3072 provide security through 2031 and beyond. _DEFAULT_RSA_KEY_BITS = 3072 +# The crypto library to use in 'keys.py'. +_CRYPTO_LIBRARY = tuf.conf.CRYPTO_LIBRARY -def generate(bits=_DEFAULT_RSA_KEY_BITS): + +def generate_rsa_key(bits=_DEFAULT_RSA_KEY_BITS): """ Generate public and private RSA keys, with modulus length 'bits'. @@ -112,43 +119,45 @@ def generate(bits=_DEFAULT_RSA_KEY_BITS): A dictionary containing the RSA keys and other identifying information. - """ - # Does 'bits' have the correct format? # This check will ensure 'bits' conforms to 'tuf.formats.RSAKEYBITS_SCHEMA'. # 'bits' must be an integer object, with a minimum value of 2048. # Raise 'tuf.FormatError' if the check fails. tuf.formats.RSAKEYBITS_SCHEMA.check_match(bits) - + + # Raise 'tuf.Error' if 'tuf.conf.CRYPTO_LIBRARY' is not supported or could + # not be imported. + _check_crypto_library() + + # Check for valid crypto library # Begin building the RSA key dictionary. rsakey_dict = {} keytype = 'rsa' - + public = None + private = None + # Generate the public and private RSA keys. The PyCrypto module performs # the actual key generation. Raise 'ValueError' if 'bits' is less than 1024 # or not a multiple of 256, although a 2048-bit minimum is enforced by # tuf.formats.RSAKEYBITS_SCHEMA.check_match(). - rsa_key_object = Crypto.PublicKey.RSA.generate(bits) - - # Extract the public & private halves of the RSA key and generate their - # PEM-formatted representations. The dictionary returned contains the - # private and public RSA keys in PEM format, as strings. - private_key_pem = rsa_key_object.exportKey(format='PEM') - rsa_pubkey = rsa_key_object.publickey() - public_key_pem = rsa_pubkey.exportKey(format='PEM') - + if _CRYPTO_LIBRARY == 'pycrypto': + public, private = tuf.pycrypto.generate_rsa_public_and_private(bits) + else: + message = 'Invalid crypto library: '+repr(_CRYPTO_LIBRARY)+'.' + raise ValueError(message) + # Generate the keyid for the RSA key. 'key_value' corresponds to the # 'keyval' entry of the 'RSAKEY_SCHEMA' dictionary. The private key # information is not included in the generation of the 'keyid' identifier. - key_value = {'public': public_key_pem, + key_value = {'public': public, 'private': ''} keyid = _get_keyid(key_value) # Build the 'rsakey_dict' dictionary. Update 'key_value' with the RSA # private key prior to adding 'key_value' to 'rsakey_dict'. - key_value['private'] = private_key_pem + key_value['private'] = private rsakey_dict['keytype'] = keytype rsakey_dict['keyid'] = keyid @@ -160,7 +169,90 @@ def generate(bits=_DEFAULT_RSA_KEY_BITS): -def create_in_metadata_format(key_value, private=False): +def generate_ed25519_key(): + """ + + Generate public and private RSA keys, with modulus length 'bits'. + In addition, a keyid used as an identifier for RSA keys is generated. + The object returned conforms to 'tuf.formats.RSAKEY_SCHEMA' and as the form: + {'keytype': 'ed25519', + 'keyid': keyid, + 'keyval': {'public': '-----BEGIN RSA PUBLIC KEY----- ...', + 'private': '-----BEGIN RSA PRIVATE KEY----- ...'}} + + The public and private keys are in PEM format and stored as strings. + + Although the crytography library called sets a 1024-bit minimum key size, + generate() enforces a minimum key size of 2048 bits. If 'bits' is + unspecified, a 3072-bit RSA key is generated, which is the key size + recommended by TUF. + + + bits: + The key size, or key length, of the RSA key. 'bits' must be 2048, or + greater, and a multiple of 256. + + + ValueError, if an exception occurs after calling the RSA key generation + routine. 'bits' must be a multiple of 256. The 'ValueError' exception is + raised by the key generation function of the cryptography library called. + + tuf.FormatError, if 'bits' does not contain the correct format. + + + The RSA keys are generated by calling PyCrypto's + Crypto.PublicKey.RSA.generate(). + + + A dictionary containing the RSA keys and other identifying information. + """ + + # Raise 'tuf.Error' if 'tuf.conf.CRYPTO_LIBRARY' is not supported or could + # not be imported. + _check_crypto_library() + + # Check for valid crypto library + # Begin building the RSA key dictionary. + ed25519_key = {} + keytype = 'ed25519' + public = None + private = None + + # Generate the public and private RSA keys. The PyCrypto module performs + # the actual key generation. Raise 'ValueError' if 'bits' is less than 1024 + # or not a multiple of 256, although a 2048-bit minimum is enforced by + # tuf.formats.RSAKEYBITS_SCHEMA.check_match(). + if 'ed25519-pynacl' in _available_crypto_libraries: + public, private = tuf.ed25519_keys.generate_public_and_private(use_pynacl=True) + elif 'ed25519-python' in _available_crypto_libraries: + public, private = tuf.ed25519_keys.generate_public_and_private(use_pynacl=False) + else: + message = 'A supported method of generating ed25519 keys not available\n'+\ + 'Available crypto libraries: '+repr(_available_crypto_libraries)+'.' + raise ValueError(message) + + # Generate the keyid for the RSA key. 'key_value' corresponds to the + # 'keyval' entry of the 'RSAKEY_SCHEMA' dictionary. The private key + # information is not included in the generation of the 'keyid' identifier. + key_value = {'public': public, + 'private': ''} + keyid = _get_keyid(key_value) + + # Build the 'rsakey_dict' dictionary. Update 'key_value' with the RSA + # private key prior to adding 'key_value' to 'rsakey_dict'. + key_value['private'] = private + + ed25519_key['keytype'] = keytype + ed25519_key['keyid'] = keyid + ed25519_key['keyval'] = key_value + + return ed25519_key + + + + + +def create_in_metadata_format(key_type, key_value, private=False): """ Return a dictionary conformant to 'tuf.formats.KEY_SCHEMA'. @@ -182,6 +274,9 @@ def create_in_metadata_format(key_value, private=False): returned by this function. + key_type: + 'rsa' or 'ed25519'. + key_value: A dictionary containing a private and public RSA key. 'key_value' is of the form: @@ -203,9 +298,7 @@ def create_in_metadata_format(key_value, private=False): An 'KEY_SCHEMA' dictionary. - """ - # Does 'key_value' have the correct format? # This check will ensure 'key_value' has the appropriate number @@ -214,10 +307,10 @@ def create_in_metadata_format(key_value, private=False): tuf.formats.KEYVAL_SCHEMA.check_match(key_value) if private is True and key_value['private']: - return {'keytype': 'rsa', 'keyval': key_value} + return {'keytype': key_type, 'keyval': key_value} else: public_key_value = {'public': key_value['public'], 'private': ''} - return {'keytype': 'rsa', 'keyval': public_key_value} + return {'keytype': key_type, 'keyval': public_key_value} @@ -263,10 +356,8 @@ def create_from_metadata_format(key_metadata): A dictionary containing the RSA keys and other identifying information. - """ - # Does 'key_metadata' have the correct format? # This check will ensure 'key_metadata' has the appropriate number # of objects and object types, and that all dict keys are properly named. @@ -274,8 +365,8 @@ def create_from_metadata_format(key_metadata): tuf.formats.KEY_SCHEMA.check_match(key_metadata) # Construct the dictionary to be returned. - rsakey_dict = {} - keytype = 'rsa' + key_dict = {} + keytype = key_metadata['keytype'] key_value = key_metadata['keyval'] # Convert 'key_value' to 'tuf.formats.KEY_SCHEMA' and generate its hash @@ -319,7 +410,25 @@ def _get_keyid(key_value): -def create_signature(rsakey_dict, data): +def _check_crypto_library(): + """ check """ + + if _CRYPTO_LIBRARY not in _SUPPORTED_CRYPTO_LIBRARIES: + message = 'The '+repr(_CRYPTO_LIBRARY)+' crypto library specified'+ \ + ' in "tuf.conf.CRYPTO_LIBRARY" is not supported.\n'+ \ + 'Supported crypto libraries: '+repr(_SUPPORTED_CRYPTO_LIBRARIES)+'.' + raise tuf.CryptoError(message) + + if _CRYPTO_LIBRARY not in _AVAILABLE_CRYPTO_LIBRARIES: + message = 'The '+repr(_CRYPTO_LIBRARY)+' crypto library specified'+ \ + 'in "tuf.conf.CRYPTO_LIBRARY" could not be imported.' + raise tuf.CryptoError(message) + + + + + +def create_signature(key_type, key_dict, data): """ Return a signature dictionary of the form: @@ -334,6 +443,9 @@ def create_signature(rsakey_dict, data): http://www.ietf.org/rfc/rfc3447.txt + key_type: + 'rsa' or 'ed25519'. + rsakey_dict: A dictionary containing the RSA keys and other identifying information. 'rsakey_dict' has the form: @@ -360,43 +472,41 @@ def create_signature(rsakey_dict, data): A signature dictionary conformat to 'tuf.format.SIGNATURE_SCHEMA'. - """ - # Does 'rsakey_dict' have the correct format? # This check will ensure 'rsakey_dict' has the appropriate number # of objects and object types, and that all dict keys are properly named. # Raise 'tuf.FormatError' if the check fails. - tuf.formats.RSAKEY_SCHEMA.check_match(rsakey_dict) + tuf.formats.NAME_SCHEMA.check_match(key_type) + if key_type == 'rsa': + tuf.formats.RSAKEY_SCHEMA.check_match(key_dict) + elif key_type == 'ed25519': + tuf.formats.ED25519KEY_SCHEMA.check_match(key_dict) + else: + raise TypeError('Invalid key type.') # Signing the 'data' object requires a private key. # The 'PyCrypto-PKCS#1 PSS' (i.e., PyCrypto module) signing method is the # only method currently supported. signature = {} - private_key = rsakey_dict['keyval']['private'] - keyid = rsakey_dict['keyid'] - method = 'PyCrypto-PKCS#1 PSS' + public = key_dict['keyval']['public'] + private = key_dict['keyval']['private'] + keyid = key_dict['keyid'] + method = None sig = None - # Verify the signature, but only if the private key has been set. The private - # key is a NULL string if unset. Although it may be clearer to explicit check - # that 'private_key' is not '', we can/should check for a value and not - # compare identities with the 'is' keyword. - if len(private_key): - # Calculate the SHA256 hash of 'data' and generate the hash's PKCS1-PSS - # signature. - try: - rsa_key_object = Crypto.PublicKey.RSA.importKey(private_key) - sha256_object = Crypto.Hash.SHA256.new(data) - pkcs1_pss_signer = Crypto.Signature.PKCS1_PSS.new(rsa_key_object) - sig = pkcs1_pss_signer.sign(sha256_object) - except (ValueError, IndexError, TypeError), e: - message = 'An RSA signature could not be generated.' - raise tuf.CryptoError(message) + if key_type == 'rsa': + if _CRYPTO_LIBRARY == 'pycrypto': + sig, method = tuf.pycrypto_keys.create_signature(private, data) + elif key_type == 'ed25519': + if 'ed25519-pynacl' in _available_crypto_libraries: + sig, method = tuf.ed25519_keys.create_signature(public, private, data, use_pynacl=True) + else: + sig, method = tuf.ed25519_keys.create_signature(public, private, data, use_pynacl=False) else: - raise TypeError('The required private key is not defined for "rsakey_dict".') - + raise TypeError('Invalid key type.') + # Build the signature dictionary to be returned. # The hexadecimal representation of 'sig' is stored in the signature. signature['keyid'] = keyid @@ -409,7 +519,7 @@ def create_signature(rsakey_dict, data): -def verify_signature(rsakey_dict, signature, data): +def verify_signature(key_type, key_dict, signature, data): """ Determine whether the private key belonging to 'rsakey_dict' produced @@ -419,6 +529,9 @@ def verify_signature(rsakey_dict, signature, data): 'rsakey_dict' and 'signature'. + key_type: + 'rsa' or 'ed25519' + rsakey_dict: A dictionary containing the RSA keys and other identifying information. 'rsakey_dict' has the form: @@ -454,19 +567,23 @@ def verify_signature(rsakey_dict, signature, data): Boolean. True if the signature is valid, False otherwise. - """ - # Does 'rsakey_dict' have the correct format? # This check will ensure 'rsakey_dict' has the appropriate number # of objects and object types, and that all dict keys are properly named. # Raise 'tuf.FormatError' if the check fails. - tuf.formats.RSAKEY_SCHEMA.check_match(rsakey_dict) + tuf.formats.NAME_SCHEMA.check_match(key_type) + if key_type == 'rsa': + tuf.formats.RSAKEY_SCHEMA.check_match(rsakey_dict) + elif key_type == 'ed25519': + tuf.formats.ED25519KEY_SCHEMA.check_match(rsakey_dict) + raise TypeError('Invalid key type.') # Does 'signature' have the correct format? tuf.formats.SIGNATURE_SCHEMA.check_match(signature) + # Using the public key belonging to 'rsakey_dict' # (i.e., rsakey_dict['keyval']['public']), verify whether 'signature' # was produced by rsakey_dict's corresponding private key @@ -474,191 +591,18 @@ def verify_signature(rsakey_dict, signature, data): # ensure 'PyCrypto-PKCS#1 PSS' was used as the signing method. method = signature['method'] sig = signature['sig'] - public_key = rsakey_dict['keyval']['public'] + public = rsakey_dict['keyval']['public'] valid_signature = False - - if method == 'PyCrypto-PKCS#1 PSS': - try: - rsa_key_object = Crypto.PublicKey.RSA.importKey(public_key) - pkcs1_pss_verifier = Crypto.Signature.PKCS1_PSS.new(rsa_key_object) - sha256_object = Crypto.Hash.SHA256.new(data) - - # The metadata stores signatures in hex. Unhexlify and verify the - # signature. - signature = binascii.unhexlify(sig) - valid_signature = pkcs1_pss_verifier.verify(sha256_object, signature) - except (ValueError, IndexError, TypeError), e: - message = 'The RSA signature could not be verified.' - raise tuf.CryptoError(message) + + if key_type == 'rsa': + if _CRYPTO_LIBRARY == 'pycrypto': + valid_signature = tuf.pycrypto_keys.verify_signature(sig, method, public, data) + elif key_type == 'ed25519': + if 'ed25519-pynacl' in _available_crypto_libraries: + valid_signature = tuf.ed25519_keys.verify_signature(public, method, sig, data, use_pynacl=True) + else: + valid_signature = tuf.ed25519_keys.verify_signature(public, method, sig, data, use_pynacl=False) else: - raise tuf.UnknownMethodError(method) + raise TypeError('Unsupported key type.') return valid_signature - - - - - -def create_encrypted_pem(rsakey_dict, passphrase): - """ - - Return a string in PEM format, where the private part of the RSA key is - encrypted. The private part of the RSA key is encrypted by the Triple - Data Encryption Algorithm (3DES) and Cipher-block chaining (CBC) for the - mode of operation. Password-Based Key Derivation Function 1 (PBKF1) + MD5 - is used to strengthen 'passphrase'. - - https://en.wikipedia.org/wiki/Triple_DES - https://en.wikipedia.org/wiki/PBKDF2 - - - rsakey_dict: - A dictionary containing the RSA keys and other identifying information. - 'rsakey_dict' has the form: - - {'keytype': 'rsa', - 'keyid': keyid, - 'keyval': {'public': '-----BEGIN RSA PUBLIC KEY----- ...', - 'private': '-----BEGIN RSA PRIVATE KEY----- ...'}} - - The public and private keys are in PEM format and stored as strings. - - passphrase: - The passphrase, or password, to encrypt the private part of the RSA - key. 'passphrase' is not used directly as the encryption key, a stronger - encryption key is derived from it. - - - TypeError, if a private key is not defined for 'rsakey_dict'. - - tuf.FormatError, if an incorrect format is found for 'rsakey_dict'. - - - PyCrypto's Crypto.PublicKey.RSA.exportKey() called to perform the actual - generation of the PEM-formatted output. - - - A string in PEM format, where the private RSA key is encrypted. - - """ - - # Does 'rsakey_dict' have the correct format? - # This check will ensure 'rsakey_dict' has the appropriate number - # of objects and object types, and that all dict keys are properly named. - # Raise 'tuf.FormatError' if the check fails. - tuf.formats.RSAKEY_SCHEMA.check_match(rsakey_dict) - - # Does 'signature' have the correct format? - tuf.formats.PASSWORD_SCHEMA.check_match(passphrase) - - # Extract the private key from 'rsakey_dict', which is stored in PEM format - # and unencrypted. The extracted key will be imported and converted to - # PyCrypto's RSA key object (i.e., Crypto.PublicKey.RSA).Use PyCrypto's - # exportKey method, with a passphrase specified, to create the string. - # PyCrypto uses PBKDF1+MD5 to strengthen 'passphrase', and 3DES with CBC mode - # for encryption. - private_key = rsakey_dict['keyval']['private'] - try: - rsa_key_object = Crypto.PublicKey.RSA.importKey(private_key) - rsakey_pem_encrypted = rsa_key_object.exportKey(format='PEM', - passphrase=passphrase) - except (ValueError, IndexError, TypeError), e: - message = 'An encrypted RSA key in PEM format could not be generated.' - raise tuf.CryptoError(message) - - return rsakey_pem_encrypted - - - - - -def create_from_encrypted_pem(encrypted_pem, passphrase): - """ - - Return an RSA key in 'tuf.formats.RSAKEY_SCHEMA' format, which has the - form: - {'keytype': 'rsa', - 'keyid': keyid, - 'keyval': {'public': '-----BEGIN RSA PUBLIC KEY----- ...', - 'private': '-----BEGIN RSA PRIVATE KEY----- ...'}} - - The RSAKEY_SCHEMA object is generated from a byte string in PEM format, - where the private part of the RSA key is encrypted. PyCrypto's importKey - method is used, where a passphrase is specified. PyCrypto uses PBKDF1+MD5 - to strengthen 'passphrase', and 3DES with CBC mode for encryption/decryption. - Alternatively, key data may be encrypted with AES-CTR-Mode and the passphrase - strengthened with PBKDF2+SHA256. See 'keystore.py'. - - - encrypted_pem: - A byte string in PEM format, where the private key is encrypted. It has - the form: - - '-----BEGIN RSA PRIVATE KEY-----\n - Proc-Type: 4,ENCRYPTED\nDEK-Info: DES-EDE3-CBC ...' - - passphrase: - The passphrase, or password, to decrypt the private part of the RSA - key. 'passphrase' is not directly used as the encryption key, instead - it is used to derive a stronger symmetric key. - - - TypeError, if a private key is not defined for 'rsakey_dict'. - - tuf.FormatError, if an incorrect format is found for the - 'rsakey_dict' object. - - - PyCrypto's 'Crypto.PublicKey.RSA.importKey()' called to perform the actual - conversion from an encrypted RSA private key. - - - A dictionary in 'tuf.formats.RSAKEY_SCHEMA' format. - - """ - - # Does 'encryped_pem' have the correct format? - # This check will ensure 'encrypted_pem' has the appropriate number - # of objects and object types, and that all dict keys are properly named. - # Raise 'tuf.FormatError' if the check fails. - tuf.formats.PEMRSA_SCHEMA.check_match(encrypted_pem) - - # Does 'passphrase' have the correct format? - tuf.formats.PASSWORD_SCHEMA.check_match(passphrase) - - keytype = 'rsa' - rsakey_dict = {} - - try: - rsa_key_object = Crypto.PublicKey.RSA.importKey(encrypted_pem, passphrase) - except (ValueError, IndexError, TypeError), e: - message = 'An RSA key object could not be generated from the encrypted '+\ - 'PEM string.' - # Raise 'tuf.CryptoError' instead of PyCrypto's exception to avoid - # revealing sensitive error, such as a decryption error due to an - # invalid passphrase. - raise tuf.CryptoError(message) - - # Extract the public & private halves of the RSA key and generate their - # PEM-formatted representations. The dictionary returned contains the - # private and public RSA keys in PEM format, as strings. - private_key_pem = rsa_key_object.exportKey(format='PEM') - rsa_pubkey = rsa_key_object.publickey() - public_key_pem = rsa_pubkey.exportKey(format='PEM') - - # Generate the keyid for the RSA key. 'key_value' corresponds to the - # 'keyval' entry of the 'RSAKEY_SCHEMA' dictionary. The private key - # information is not included in the generation of the 'keyid' identifier. - key_value = {'public': public_key_pem, - 'private': ''} - keyid = _get_keyid(key_value) - - # Build the 'rsakey_dict' dictionary. Update 'key_value' with the RSA - # private key prior to adding 'key_value' to 'rsakey_dict'. - key_value['private'] = private_key_pem - - rsakey_dict['keytype'] = keytype - rsakey_dict['keyid'] = keyid - rsakey_dict['keyval'] = key_value - - return rsakey_dict diff --git a/tuf/pycrypto_keys.py b/tuf/pycrypto_keys.py new file mode 100755 index 00000000..31790f72 --- /dev/null +++ b/tuf/pycrypto_keys.py @@ -0,0 +1,403 @@ +""" + + pycrypto_keys.py + + + Vladimir Diaz + + + March 7, 2013. + + + See LICENSE for licensing information. + + + The goal of this module is to support public-key cryptography using the RSA + algorithm. The RSA-related functions provided include generate(), + create_signature(), and verify_signature(). The create_encrypted_pem() and + create_from_encrypted_pem() functions are optional, and may be used save a + generated RSA key to a file. The 'PyCrypto' package used by 'rsa_key.py' + generates the actual RSA keys and the functions listed above can be viewed + as an easy-to-use public interface. Additional functions contained here + include create_in_metadata_format() and create_from_metadata_format(). These + last two functions produce or use RSA keys compatible with the key structures + listed in TUF Metadata files. The generate() function returns a dictionary + containing all the information needed of RSA keys, such as public and private= + keys, keyIDs, and an idenfier. create_signature() and verify_signature() are + supplemental functions used for generating RSA signatures and verifying them. + https://en.wikipedia.org/wiki/RSA_(algorithm) + """ + +# Crypto.PublicKey (i.e., PyCrypto public-key cryptography) provides algorithms +# such as Digital Signature Algorithm (DSA) and the ElGamal encryption system. +# 'Crypto.PublicKey.RSA' is needed here to generate, sign, and verify RSA keys. +import Crypto.PublicKey.RSA + +# PyCrypto requires 'Crypto.Hash' hash objects to generate PKCS#1 PSS +# signatures (i.e., Crypto.Signature.PKCS1_PSS). +import Crypto.Hash.SHA256 + +# RSA's probabilistic signature scheme with appendix (RSASSA-PSS). +# PKCS#1 v1.5 is provided for compatability with existing applications, but +# RSASSA-PSS is encouraged for newer applications. RSASSA-PSS generates +# a random salt to ensure the signature generated is probabilistic rather than +# deterministic, like PKCS#1 v1.5. +# http://en.wikipedia.org/wiki/RSA-PSS#Schemes +# https://tools.ietf.org/html/rfc3447#section-8.1 +import Crypto.Signature.PKCS1_PSS + +import tuf + +# Digest objects needed to generate hashes. +import tuf.hash + +# Perform object format-checking. +import tuf.formats + +# Recommended RSA key sizes: +# http://www.emc.com/emc-plus/rsa-labs/historical/twirl-and-rsa-key-size.htm#table1 +# According to the document above, revised May 6, 2003, RSA keys of +# size 3072 provide security through 2031 and beyond. +_DEFAULT_RSA_KEY_BITS = 3072 + + +def generate_rsa_public_and_private_keys(bits=_DEFAULT_RSA_KEY_BITS): + """ + + Generate public and private RSA keys, with modulus length 'bits'. + In addition, a keyid used as an identifier for RSA keys is generated. + The object returned conforms to 'tuf.formats.RSAKEY_SCHEMA' and as the form: + {'keytype': 'rsa', + 'keyid': keyid, + 'keyval': {'public': '-----BEGIN RSA PUBLIC KEY----- ...', + 'private': '-----BEGIN RSA PRIVATE KEY----- ...'}} + + The public and private keys are in PEM format and stored as strings. + + Although the crytography library called sets a 1024-bit minimum key size, + generate() enforces a minimum key size of 2048 bits. If 'bits' is + unspecified, a 3072-bit RSA key is generated, which is the key size + recommended by TUF. + + + bits: + The key size, or key length, of the RSA key. 'bits' must be 2048, or + greater, and a multiple of 256. + + + ValueError, if an exception occurs after calling the RSA key generation + routine. 'bits' must be a multiple of 256. The 'ValueError' exception is + raised by the key generation function of the cryptography library called. + + tuf.FormatError, if 'bits' does not contain the correct format. + + + The RSA keys are generated by calling PyCrypto's + Crypto.PublicKey.RSA.generate(). + + + A dictionary containing the RSA keys and other identifying information. + """ + + # Does 'bits' have the correct format? + # This check will ensure 'bits' conforms to 'tuf.formats.RSAKEYBITS_SCHEMA'. + # 'bits' must be an integer object, with a minimum value of 2048. + # Raise 'tuf.FormatError' if the check fails. + tuf.formats.RSAKEYBITS_SCHEMA.check_match(bits) + + # Generate the public and private RSA keys. The PyCrypto module performs + # the actual key generation. Raise 'ValueError' if 'bits' is less than 1024 + # or not a multiple of 256, although a 2048-bit minimum is enforced by + # tuf.formats.RSAKEYBITS_SCHEMA.check_match(). + rsa_key_object = Crypto.PublicKey.RSA.generate(bits) + + # Extract the public & private halves of the RSA key and generate their + # PEM-formatted representations. The dictionary returned contains the + # private and public RSA keys in PEM format, as strings. + private_key = rsa_key_object.exportKey(format='PEM') + rsa_pubkey = rsa_key_object.publickey() + public_key = rsa_pubkey.exportKey(format='PEM') + + return public_key, private_key + + + + + +def create_signature(private_key, data): + """ + + Return a signature dictionary of the form: + {'keyid': keyid, + 'method': 'PyCrypto-PKCS#1 PPS', + 'sig': sig}. + + The signing process will use the private key + rsakey_dict['keyval']['private'] and 'data' to generate the signature. + + RFC3447 - RSASSA-PSS + http://www.ietf.org/rfc/rfc3447.txt + + + private_key: + The private key is a string in PEM format. + + data: + Data object used by create_signature() to generate the signature. + + + TypeError, if a private key is not defined for 'rsakey_dict'. + + tuf.FormatError, if an incorrect format is found for 'private_key'. + + tuf.CryptoError, + + + PyCrypto's 'Crypto.Signature.PKCS1_PSS' called to generate the signature. + + + A (signature, method) tuple, where + """ + + # Signing the 'data' object requires a private key. + # The 'PyCrypto-PKCS#1 PSS' (i.e., PyCrypto module) signing method is the + # only method currently supported. + private = private_key + method = 'PyCrypto-PKCS#1 PSS' + signature = None + + # Verify the signature, but only if the private key has been set. The private + # key is a NULL string if unset. Although it may be clearer to explicit check + # that 'private_key' is not '', we can/should check for a value and not + # compare identities with the 'is' keyword. + if len(private_key): + # Calculate the SHA256 hash of 'data' and generate the hash's PKCS1-PSS + # signature. + try: + rsa_key_object = Crypto.PublicKey.RSA.importKey(private_key) + sha256_object = Crypto.Hash.SHA256.new(data) + pkcs1_pss_signer = Crypto.Signature.PKCS1_PSS.new(rsa_key_object) + sig = pkcs1_pss_signer.sign(sha256_object) + except (ValueError, IndexError, TypeError), e: + message = 'An RSA signature could not be generated.' + raise tuf.CryptoError(message) + else: + raise TypeError('The required private key is not defined for "rsakey_dict".') + + return signature, method + + + + + +def verify_signature(signature, signature_method, public_key, data): + """ + + Determine whether the private key belonging to 'rsakey_dict' produced + 'signature'. verify_signature() will use the public key found in + 'rsakey_dict', the 'method' and 'sig' objects contained in 'signature', + and 'data' to complete the verification. Type-checking performed on both + 'rsakey_dict' and 'signature'. + + + signature: + The signature dictionary produced by tuf.rsa_key.create_signature(). + 'signature' has the form: + {'keyid': keyid, 'method': 'method', 'sig': sig}. Conformant to + 'tuf.formats.SIGNATURE_SCHEMA'. + + signature_method: + + public_key: + + data: + Data object used by tuf.rsa_key.create_signature() to generate + 'signature'. 'data' is needed here to verify the signature. + + + tuf.UnknownMethodError. Raised if the signing method used by + 'signature' is not one supported by tuf.rsa_key.create_signature(). + + tuf.FormatError. Raised if either 'rsakey_dict' + or 'signature' do not match their respective tuf.formats schema. + 'rsakey_dict' must conform to 'tuf.formats.RSAKEY_SCHEMA'. + 'signature' must conform to 'tuf.formats.SIGNATURE_SCHEMA'. + + + Crypto.Signature.PKCS1_PSS.verify() called to do the actual verification. + + + Boolean. True if the signature is valid, False otherwise. + """ + + # Using the public key belonging to 'rsakey_dict' + # (i.e., rsakey_dict['keyval']['public']), verify whether 'signature' + # was produced by rsakey_dict's corresponding private key + # rsakey_dict['keyval']['private']. Before returning the Boolean result, + # ensure 'PyCrypto-PKCS#1 PSS' was used as the signing method. + signature = signature + method = signature_method + public = public_key + valid_signature = False + + if method == 'PyCrypto-PKCS#1 PSS': + try: + rsa_key_object = Crypto.PublicKey.RSA.importKey(public_key) + pkcs1_pss_verifier = Crypto.Signature.PKCS1_PSS.new(rsa_key_object) + sha256_object = Crypto.Hash.SHA256.new(data) + + valid_signature = pkcs1_pss_verifier.verify(sha256_object, signature) + except (ValueError, IndexError, TypeError), e: + message = 'The RSA signature could not be verified.' + raise tuf.CryptoError(message) + else: + raise tuf.UnknownMethodError(method) + + return valid_signature + + + + + +def create_rsa_encrypted_pem(private_key, passphrase): + """ + + Return a string in PEM format, where the private part of the RSA key is + encrypted. The private part of the RSA key is encrypted by the Triple + Data Encryption Algorithm (3DES) and Cipher-block chaining (CBC) for the + mode of operation. Password-Based Key Derivation Function 1 (PBKF1) + MD5 + is used to strengthen 'passphrase'. + + https://en.wikipedia.org/wiki/Triple_DES + https://en.wikipedia.org/wiki/PBKDF2 + + + private_key: + The public and private keys are in PEM format and stored as strings. + + passphrase: + The passphrase, or password, to encrypt the private part of the RSA + key. 'passphrase' is not used directly as the encryption key, a stronger + encryption key is derived from it. + + + TypeError, if a private key is not defined for 'rsakey_dict'. + + tuf.FormatError, if an incorrect format is found for 'rsakey_dict'. + + + PyCrypto's Crypto.PublicKey.RSA.exportKey() called to perform the actual + generation of the PEM-formatted output. + + + A string in PEM format, where the private RSA key is encrypted. + """ + + # Does 'rsakey_dict' have the correct format? + # This check will ensure 'rsakey_dict' has the appropriate number + # of objects and object types, and that all dict keys are properly named. + # Raise 'tuf.FormatError' if the check fails. + tuf.formats.RSAKEY_SCHEMA.check_match(rsakey_dict) + + # Does 'signature' have the correct format? + tuf.formats.PASSWORD_SCHEMA.check_match(passphrase) + + # Extract the private key from 'rsakey_dict', which is stored in PEM format + # and unencrypted. The extracted key will be imported and converted to + # PyCrypto's RSA key object (i.e., Crypto.PublicKey.RSA).Use PyCrypto's + # exportKey method, with a passphrase specified, to create the string. + # PyCrypto uses PBKDF1+MD5 to strengthen 'passphrase', and 3DES with CBC mode + # for encryption. + try: + rsa_key_object = Crypto.PublicKey.RSA.importKey(private_key) + encrypted_pem = rsa_key_object.exportKey(format='PEM', passphrase=passphrase) + except (ValueError, IndexError, TypeError), e: + message = 'An encrypted RSA key in PEM format could not be generated.' + raise tuf.CryptoError(message) + + return encrypted_pem + + + + + +def create_rsa_from_encrypted_pem(encrypted_pem, passphrase): + """ + + Return an RSA key in 'tuf.formats.RSAKEY_SCHEMA' format, which has the + form: + {'keytype': 'rsa', + 'keyid': keyid, + 'keyval': {'public': '-----BEGIN RSA PUBLIC KEY----- ...', + 'private': '-----BEGIN RSA PRIVATE KEY----- ...'}} + + The RSAKEY_SCHEMA object is generated from a byte string in PEM format, + where the private part of the RSA key is encrypted. PyCrypto's importKey + method is used, where a passphrase is specified. PyCrypto uses PBKDF1+MD5 + to strengthen 'passphrase', and 3DES with CBC mode for encryption/decryption. + Alternatively, key data may be encrypted with AES-CTR-Mode and the passphrase + strengthened with PBKDF2+SHA256. See 'keystore.py'. + + + encrypted_pem: + A byte string in PEM format, where the private key is encrypted. It has + the form: + + '-----BEGIN RSA PRIVATE KEY-----\n + Proc-Type: 4,ENCRYPTED\nDEK-Info: DES-EDE3-CBC ...' + + passphrase: + The passphrase, or password, to decrypt the private part of the RSA + key. 'passphrase' is not directly used as the encryption key, instead + it is used to derive a stronger symmetric key. + + + TypeError, if a private key is not defined for 'rsakey_dict'. + + tuf.FormatError, if an incorrect format is found for the + 'rsakey_dict' object. + + + PyCrypto's 'Crypto.PublicKey.RSA.importKey()' called to perform the actual + conversion from an encrypted RSA private key. + + + A dictionary in 'tuf.formats.RSAKEY_SCHEMA' format. + """ + + # Does 'encryped_pem' have the correct format? + # This check will ensure 'encrypted_pem' has the appropriate number + # of objects and object types, and that all dict keys are properly named. + # Raise 'tuf.FormatError' if the check fails. + tuf.formats.PEMRSA_SCHEMA.check_match(encrypted_pem) + + # Does 'passphrase' have the correct format? + tuf.formats.PASSWORD_SCHEMA.check_match(passphrase) + + try: + rsa_key_object = Crypto.PublicKey.RSA.importKey(encrypted_pem, passphrase) + except (ValueError, IndexError, TypeError), e: + message = 'An RSA key object could not be generated from the encrypted '+\ + 'PEM string.' + # Raise 'tuf.CryptoError' instead of PyCrypto's exception to avoid + # revealing sensitive error, such as a decryption error due to an + # invalid passphrase. + raise tuf.CryptoError(message) + + # Extract the public and private halves of the RSA key and generate their + # PEM-formatted representations. The dictionary returned contains the + # private and public RSA keys in PEM format, as strings. + private = rsa_key_object.exportKey(format='PEM') + rsa_pubkey = rsa_key_object.publickey() + public = rsa_pubkey.exportKey(format='PEM') + + return public, private + + + +if __name__ == '__main__': + # The interactive sessions of the documentation strings can + # be tested by running 'pycrypto_keys.py' as a standalone module. + # python -B pycrypto_keys.py + import doctest + doctest.testmod() diff --git a/tuf/sig.py b/tuf/sig.py index ce5b9f56..57853b98 100755 --- a/tuf/sig.py +++ b/tuf/sig.py @@ -33,7 +33,6 @@ that will determine if a role still has a sufficient number of valid keys. If a caller needs to update the signatures of a 'signable' object, there is also a function for that. - """ import tuf @@ -76,7 +75,6 @@ def get_signature_status(signable, role=None): A dictionary representing the status of the signatures in 'signable'. Conformant to tuf.formats.SIGNATURESTATUS_SCHEMA. - """ # Does 'signable' have the correct format? @@ -125,7 +123,7 @@ def get_signature_status(signable, role=None): # Identify key using an unknown key signing method. try: - valid_sig = tuf.rsa_key.verify_signature(key, signature, data) + valid_sig = tuf.keys.verify_signature(key['keytype'], key, signature, data) except tuf.UnknownMethodError: unknown_method_sigs.append(keyid) continue @@ -201,7 +199,6 @@ def verify(signable, role): Boolean. True if the number of good signatures >= the role's threshold, False otherwise. - """ # Retrieve the signature status. tuf.sig.get_signature_status() raises @@ -243,10 +240,8 @@ def may_need_new_keys(signature_status): Boolean. - """ - # Does 'signature_status' have the correct format? # This check will ensure 'signature_status' has the appropriate number # of objects and object types, and that all dict keys are properly named. @@ -281,11 +276,11 @@ def generate_rsa_signature(signed, rsakey_dict): signed: - The data used by 'tuf.rsa_key.create_signature()' to generate signatures. + The data used by 'tuf.keys.create_signature()' to generate signatures. It is stored in the 'signed' field of 'signable'. rsakey_dict: - The RSA key, a tuf.formats.RSAKEY_SCHEMA dictionary. + The RSA key, a 'tuf.formats.RSAKEY_SCHEMA' dictionary. Used here to produce 'keyid', 'method', and 'sig'. @@ -300,7 +295,6 @@ def generate_rsa_signature(signed, rsakey_dict): Signature dictionary conformant to tuf.formats.SIGNATURE_SCHEMA. Has the form: {'keyid': keyid, 'method': 'evp', 'sig': sig} - """ # We need 'signed' in canonical JSON format to generate @@ -309,6 +303,6 @@ def generate_rsa_signature(signed, rsakey_dict): # Generate the RSA signature. # Raises tuf.FormatError and TypeError. - signature = tuf.rsa_key.create_signature(rsakey_dict, signed) + signature = tuf.keys.create_signature('rsa', rsakey_dict, signed) return signature From 4c866bc38445f517e8e7e69f1a3523f72845b2cf Mon Sep 17 00:00:00 2001 From: vladdd Date: Tue, 8 Oct 2013 13:32:07 -0400 Subject: [PATCH 30/95] Continue configurable crypto design changes Add new schema to formats.py and simplify input validation in keys.py --- tuf/client/updater.py | 1 + tuf/formats.py | 7 +++++++ tuf/keys.py | 32 +++++++------------------------- tuf/sig.py | 4 ++-- 4 files changed, 17 insertions(+), 27 deletions(-) diff --git a/tuf/client/updater.py b/tuf/client/updater.py index 331d20be..3623cedf 100755 --- a/tuf/client/updater.py +++ b/tuf/client/updater.py @@ -110,6 +110,7 @@ import tuf.download import tuf.formats import tuf.hash +import tuf.keys import tuf.keydb import tuf.log import tuf.mirrors diff --git a/tuf/formats.py b/tuf/formats.py index 2a85e957..27c06f29 100755 --- a/tuf/formats.py +++ b/tuf/formats.py @@ -161,6 +161,13 @@ keytype=SCHEMA.AnyString(), keyval=KEYVAL_SCHEMA) +# An RSA key. +ANYKEY_SCHEMA = SCHEMA.Object( + object_name='anykey', + keytype=SCHEMA.OneOf([SCHEMA.String('rsa'), SCHEMA.String('ed25519')]), + keyid=KEYID_SCHEMA, + keyval=KEYVAL_SCHEMA) + # An RSA key. RSAKEY_SCHEMA = SCHEMA.Object( object_name='rsakey', diff --git a/tuf/keys.py b/tuf/keys.py index 0bec296d..a347a5d1 100755 --- a/tuf/keys.py +++ b/tuf/keys.py @@ -40,7 +40,7 @@ # _SUPPORTED_CRYPTO_LIBRARIES = \ - ['pycrypto', 'ed25519', 'evp'] + ['pycrypto', 'ed25519'] # _available_crypto_libraries = ['ed25519-python'] @@ -428,7 +428,7 @@ def _check_crypto_library(): -def create_signature(key_type, key_dict, data): +def create_signature(key_dict, data): """ Return a signature dictionary of the form: @@ -443,10 +443,7 @@ def create_signature(key_type, key_dict, data): http://www.ietf.org/rfc/rfc3447.txt - key_type: - 'rsa' or 'ed25519'. - - rsakey_dict: + key_dict: A dictionary containing the RSA keys and other identifying information. 'rsakey_dict' has the form: @@ -478,13 +475,7 @@ def create_signature(key_type, key_dict, data): # This check will ensure 'rsakey_dict' has the appropriate number # of objects and object types, and that all dict keys are properly named. # Raise 'tuf.FormatError' if the check fails. - tuf.formats.NAME_SCHEMA.check_match(key_type) - if key_type == 'rsa': - tuf.formats.RSAKEY_SCHEMA.check_match(key_dict) - elif key_type == 'ed25519': - tuf.formats.ED25519KEY_SCHEMA.check_match(key_dict) - else: - raise TypeError('Invalid key type.') + tuf.formats.ANYKEY_SCHEMA.check_match(key_dict) # Signing the 'data' object requires a private key. # The 'PyCrypto-PKCS#1 PSS' (i.e., PyCrypto module) signing method is the @@ -519,7 +510,7 @@ def create_signature(key_type, key_dict, data): -def verify_signature(key_type, key_dict, signature, data): +def verify_signature(key_dict, signature, data): """ Determine whether the private key belonging to 'rsakey_dict' produced @@ -529,10 +520,7 @@ def verify_signature(key_type, key_dict, signature, data): 'rsakey_dict' and 'signature'. - key_type: - 'rsa' or 'ed25519' - - rsakey_dict: + key_dict: A dictionary containing the RSA keys and other identifying information. 'rsakey_dict' has the form: @@ -573,16 +561,10 @@ def verify_signature(key_type, key_dict, signature, data): # This check will ensure 'rsakey_dict' has the appropriate number # of objects and object types, and that all dict keys are properly named. # Raise 'tuf.FormatError' if the check fails. - tuf.formats.NAME_SCHEMA.check_match(key_type) - if key_type == 'rsa': - tuf.formats.RSAKEY_SCHEMA.check_match(rsakey_dict) - elif key_type == 'ed25519': - tuf.formats.ED25519KEY_SCHEMA.check_match(rsakey_dict) - raise TypeError('Invalid key type.') + tuf.formats.ANYKEY_SCHEMA.check_match(key_dict) # Does 'signature' have the correct format? tuf.formats.SIGNATURE_SCHEMA.check_match(signature) - # Using the public key belonging to 'rsakey_dict' # (i.e., rsakey_dict['keyval']['public']), verify whether 'signature' diff --git a/tuf/sig.py b/tuf/sig.py index 57853b98..3ae7d9f6 100755 --- a/tuf/sig.py +++ b/tuf/sig.py @@ -123,7 +123,7 @@ def get_signature_status(signable, role=None): # Identify key using an unknown key signing method. try: - valid_sig = tuf.keys.verify_signature(key['keytype'], key, signature, data) + valid_sig = tuf.keys.verify_signature(key, signature, data) except tuf.UnknownMethodError: unknown_method_sigs.append(keyid) continue @@ -303,6 +303,6 @@ def generate_rsa_signature(signed, rsakey_dict): # Generate the RSA signature. # Raises tuf.FormatError and TypeError. - signature = tuf.keys.create_signature('rsa', rsakey_dict, signed) + signature = tuf.keys.create_signature(rsakey_dict, signed) return signature From a395607534256385e22ff147dd81b60f8a6ee187 Mon Sep 17 00:00:00 2001 From: vladdd Date: Tue, 8 Oct 2013 13:45:37 -0400 Subject: [PATCH 31/95] Add updated schema description of new schema in formats.py --- tuf/formats.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/tuf/formats.py b/tuf/formats.py index 27c06f29..ccb5c645 100755 --- a/tuf/formats.py +++ b/tuf/formats.py @@ -161,7 +161,9 @@ keytype=SCHEMA.AnyString(), keyval=KEYVAL_SCHEMA) -# An RSA key. +# A TUF key object. This schema simplifies validation of keys that may be +# one of the supported key types. +# Supported key types: 'rsa', 'ed25519'. ANYKEY_SCHEMA = SCHEMA.Object( object_name='anykey', keytype=SCHEMA.OneOf([SCHEMA.String('rsa'), SCHEMA.String('ed25519')]), From d0c5b719e31c313af16803210cc9fb0d90a2339e Mon Sep 17 00:00:00 2001 From: vladdd Date: Tue, 8 Oct 2013 13:52:33 -0400 Subject: [PATCH 32/95] Support adding any key type in keydb.py Fix docstrings for whitespace consistency. --- tuf/keydb.py | 20 +++++++------------- 1 file changed, 7 insertions(+), 13 deletions(-) diff --git a/tuf/keydb.py b/tuf/keydb.py index 95a9f896..09817d82 100755 --- a/tuf/keydb.py +++ b/tuf/keydb.py @@ -25,10 +25,8 @@ and the '_get_keyid()' function to learn precisely how keyids are generated. One may get the keyid of a key object by simply accessing the dictionary's 'keyid' key (i.e., rsakey['keyid']). - """ - import logging import tuf @@ -68,7 +66,6 @@ def create_keydb_from_root_metadata(root_metadata): None. - """ # Does 'root_metadata' have the correct format? @@ -105,7 +102,7 @@ def create_keydb_from_root_metadata(root_metadata): -def add_rsakey(rsakey_dict, keyid=None): +def add_key(key_dict, keyid=None): """ Add 'rsakey_dict' to the key database while avoiding duplicates. @@ -113,7 +110,7 @@ def add_rsakey(rsakey_dict, keyid=None): and raise an exception if it is not. - rsakey_dict: + key_dict: A dictionary conformant to 'tuf.formats.RSAKEY_SCHEMA'. It has the form: {'keytype': 'rsa', @@ -144,7 +141,7 @@ def add_rsakey(rsakey_dict, keyid=None): # This check will ensure 'rsakey_dict' has the appropriate number of objects # and object types, and that all dict keys are properly named. # Raise 'tuf.FormatError if the check fails. - tuf.formats.RSAKEY_SCHEMA.check_match(rsakey_dict) + tuf.formats.ANYKEY_SCHEMA.check_match(key_dict) # Does 'keyid' have the correct format? if keyid is not None: @@ -152,16 +149,16 @@ def add_rsakey(rsakey_dict, keyid=None): tuf.formats.KEYID_SCHEMA.check_match(keyid) # Check if the keyid found in 'rsakey_dict' matches 'keyid'. - if keyid != rsakey_dict['keyid']: - raise tuf.Error('Incorrect keyid '+rsakey_dict['keyid']+' expected '+keyid) + if keyid != key_dict['keyid']: + raise tuf.Error('Incorrect keyid '+key_dict['keyid']+' expected '+keyid) # Check if the keyid belonging to 'rsakey_dict' is not already # available in the key database before returning. - keyid = rsakey_dict['keyid'] + keyid = key_dict['keyid'] if keyid in _keydb_dict: raise tuf.KeyAlreadyExistsError('Key: '+keyid) - _keydb_dict[keyid] = rsakey_dict + _keydb_dict[keyid] = key_dict @@ -188,7 +185,6 @@ def get_key(keyid): The key matching 'keyid'. In the case of RSA keys, a dictionary conformant to 'tuf.formats.RSAKEY_SCHEMA' is returned. - """ # Does 'keyid' have the correct format? @@ -227,7 +223,6 @@ def remove_key(keyid): None. - """ # Does 'keyid' have the correct format? @@ -262,7 +257,6 @@ def clear_keydb(): None. - """ _keydb_dict.clear() From 28b3b52422c2e6575882ad88cc4ad40cf3af02a4 Mon Sep 17 00:00:00 2001 From: vladdd Date: Tue, 8 Oct 2013 14:00:46 -0400 Subject: [PATCH 33/95] Modify updater.py to support multiple key types --- tuf/client/updater.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/tuf/client/updater.py b/tuf/client/updater.py index 3623cedf..25e8fb6f 100755 --- a/tuf/client/updater.py +++ b/tuf/client/updater.py @@ -486,13 +486,13 @@ def _import_delegations(self, parent_role): # Iterate through the keys of the delegated roles of 'parent_role' # and load them. for keyid, keyinfo in keys_info.items(): - if keyinfo['keytype'] == 'rsa': - rsa_key = tuf.keys.create_from_metadata_format(keyinfo) + if keyinfo['keytype'] in ['rsa', 'ed25519']: + key = tuf.keys.create_from_metadata_format(keyinfo) # We specify the keyid to ensure that it's the correct keyid # for the key. try: - tuf.keydb.add_rsakey(rsa_key, keyid) + tuf.keydb.add_key(key, keyid) except tuf.KeyAlreadyExistsError: pass except (tuf.FormatError, tuf.Error), e: From 372908caa6514150e4dff2a3671bcfa0a189876b Mon Sep 17 00:00:00 2001 From: vladdd Date: Tue, 8 Oct 2013 14:19:24 -0400 Subject: [PATCH 34/95] Modify signerlib.py to support multiple key types Fix docstrings for whitespace consistency. --- tuf/repo/signerlib.py | 36 ++++-------------------------------- 1 file changed, 4 insertions(+), 32 deletions(-) diff --git a/tuf/repo/signerlib.py b/tuf/repo/signerlib.py index 3f44ea5e..6144b8e4 100755 --- a/tuf/repo/signerlib.py +++ b/tuf/repo/signerlib.py @@ -16,7 +16,6 @@ These functions contain code that can extract or create needed repository data, such as the extraction of role and keyid information from a config file, and the generation of actual metadata content. - """ import gzip @@ -27,7 +26,7 @@ import tuf import tuf.formats import tuf.hash -import tuf.rsa_key +import tuf.keys import tuf.repo.keystore import tuf.sig import tuf.util @@ -81,7 +80,6 @@ def read_config_file(filename): A dictionary containing the data loaded from the configuration file. - """ # Does 'filename' have the correct format? @@ -151,7 +149,6 @@ def get_metadata_file_info(filename): A dictionary conformant to 'tuf.formats.FILEINFO_SCHEMA'. This dictionary contains the length, hashes, and custom data about the 'filename' metadata file. - """ # Does 'filename' have the correct format? @@ -204,7 +201,6 @@ def get_metadata_filenames(metadata_directory=None): A dictionary containing the expected filenames of the top-level metadata files, such as 'root.txt' and 'release.txt'. - """ if metadata_directory is None: @@ -255,7 +251,6 @@ def generate_root_metadata(config_filepath, version): A root 'signable' object conformant to 'tuf.formats.SIGNABLE_SCHEMA'. - """ # Does 'config_filepath' have the correct format? @@ -290,8 +285,8 @@ def generate_root_metadata(config_filepath, version): keyid = key['keyid'] # This appears to be a new keyid. Let's generate the key for it. if keyid not in keydict: - if key['keytype'] == 'rsa': - keydict[keyid] = tuf.rsa_key.create_in_metadata_format(key['keyval']) + if key['keytype'] in ['rsa', 'ed25519']: + keydict[keyid] = tuf.keys.create_in_metadata_format(key['keyval']) # This is not a recognized key. Raise an exception. else: raise tuf.Error('Unsupported keytype: '+keyid) @@ -364,7 +359,6 @@ def generate_targets_metadata(repository_directory, target_files, version, A targets 'signable' object, conformant to 'tuf.formats.SIGNABLE_SCHEMA'. - """ # Do the arguments have the correct format. @@ -433,7 +427,6 @@ def generate_release_metadata(metadata_directory, version, expiration_date): The release 'signable' object, conformant to 'tuf.formats.SIGNABLE_SCHEMA'. - """ # Does 'metadata_directory' have the correct format? @@ -510,7 +503,6 @@ def generate_timestamp_metadata(release_filename, version, A timestamp 'signable' object, conformant to 'tuf.formats.SIGNABLE_SCHEMA'. - """ # Do the arguments have the correct format? @@ -575,7 +567,6 @@ def write_metadata_file(metadata, filename, compression=None): The path to the written metadata file. - """ # Are the arguments properly formatted? @@ -645,7 +636,6 @@ def read_metadata_file(filename): The metadata object. - """ return tuf.util.load_json_file(filename) @@ -684,7 +674,6 @@ def sign_metadata(metadata, keyids, filename): A signable object conformant to 'tuf.formats.SIGNABLE_SCHEMA'. - """ # Does 'keyids' and 'filename' have the correct format? @@ -767,7 +756,6 @@ def generate_and_save_rsa_key(keystore_directory, password, 'keyid': keyid, 'keyval': {'public': '-----BEGIN RSA PUBLIC KEY----- ...', 'private': '-----BEGIN RSA PRIVATE KEY----- ...'}} - """ # Are the arguments correctly formatted? @@ -778,7 +766,7 @@ def generate_and_save_rsa_key(keystore_directory, password, keystore_directory = check_directory(keystore_directory) # tuf.FormatError or tuf.CryptoError raised. - rsakey = tuf.rsa_key.generate(bits) + rsakey = tuf.keys.generate_rsa_key(bits) logger.info('Generated a new key: '+rsakey['keyid']) @@ -820,7 +808,6 @@ def check_directory(directory): The normalized absolutized path of 'directory'. - """ # Does 'directory' have the correct format? @@ -868,7 +855,6 @@ def get_target_keyids(metadata_directory): A dictionary containing the role information extracted from the metadata. Ex: {'targets':[keyid1, ...], 'targets/role1':[keyid], ...} - """ # Does 'metadata_directory' have the correct format? @@ -964,7 +950,6 @@ def build_config_file(config_file_directory, timeout, role_info): The normalized absolutized path of the saved configuration file. - """ # Do the arguments have the correct format? @@ -1054,7 +1039,6 @@ def build_root_file(config_filepath, root_keyids, metadata_directory, version): The path for the written root metadata file. - """ # Do the arguments have the correct format? @@ -1116,7 +1100,6 @@ def build_targets_file(target_paths, targets_keyids, metadata_directory, The path for the written targets metadata file. - """ # Do the arguments have the correct format? @@ -1207,7 +1190,6 @@ def build_release_file(release_keyids, metadata_directory, The path for the written release metadata file. - """ # Do the arguments have the correct format? @@ -1282,7 +1264,6 @@ def build_timestamp_file(timestamp_keyids, metadata_directory, The path for the written timestamp metadata file. - """ # Do the arguments have the correct format? @@ -1370,7 +1351,6 @@ def build_delegated_role_file(delegated_targets_directory, delegated_keyids, The path for the written targets metadata file. - """ # Do the arguments have the correct format? @@ -1432,7 +1412,6 @@ def find_delegated_role(roles, delegated_role): None, if the role with the given name does not exist, or its unique index in the list of roles. - """ # Check argument types. @@ -1487,7 +1466,6 @@ def accept_any_file(full_target_path): True. - """ return True @@ -1524,7 +1502,6 @@ def get_targets(files_directory, recursive_walk=False, followlinks=True, A list of absolute paths to target files in the given files_directory. - """ targets = [] @@ -1546,8 +1523,3 @@ def get_targets(files_directory, recursive_walk=False, followlinks=True, del dirnames[:] return targets - - - - - From 46d07be5adb93e1a19792bc338decee2cef0658f Mon Sep 17 00:00:00 2001 From: vladdd Date: Wed, 9 Oct 2013 08:15:09 -0400 Subject: [PATCH 35/95] Continue configurable crypto changes: add keys.py doctest --- tuf/conf.py | 4 +-- tuf/keys.py | 73 ++++++++++++++++++++++++++++++++++++++++++----------- 2 files changed, 60 insertions(+), 17 deletions(-) diff --git a/tuf/conf.py b/tuf/conf.py index 9d271b86..060101bc 100755 --- a/tuf/conf.py +++ b/tuf/conf.py @@ -68,5 +68,5 @@ # # Supported Cryptography libraries: -# 'pycrypto', 'ed25519'. -CRYPTO_LIBRARY = 'ed25519' +# 'pycrypto', 'ed25519-python', 'ed25519-pynacl'. +CRYPTO_LIBRARY = 'pycrypto' diff --git a/tuf/keys.py b/tuf/keys.py index a347a5d1..acc84c49 100755 --- a/tuf/keys.py +++ b/tuf/keys.py @@ -35,21 +35,22 @@ key (i.e., rsakey['keyid']). """ -# Required for hexadecimal conversions. Signatures are hexlified. +# Required for hexadecimal conversions. Signatures and public/private keys are +# hexlified. import binascii # -_SUPPORTED_CRYPTO_LIBRARIES = \ - ['pycrypto', 'ed25519'] +_SUPPORTED_CRYPTO_LIBRARIES = ['pycrypto', 'ed25519-python', 'ed25519-pynacl'] # _available_crypto_libraries = ['ed25519-python'] try: - import Crypto + #import Crypto import tuf.pycrypto_keys.py _available_crypto_libraries.append('pycrypto') except ImportError: + print('Could not import pycrypto') pass try: @@ -100,6 +101,14 @@ def generate_rsa_key(bits=_DEFAULT_RSA_KEY_BITS): generate() enforces a minimum key size of 2048 bits. If 'bits' is unspecified, a 3072-bit RSA key is generated, which is the key size recommended by TUF. + + >>> rsa_key = generate_rsa_key(bits=2048) + >>> tuf.formats.RSAKEY_SCHEMA.matches(rsa_key) + True + >>> len(ed25519_key['keyval']['public']) + 64 + >>> len(ed25519_key['keyval']['private']) + 64 bits: @@ -151,13 +160,13 @@ def generate_rsa_key(bits=_DEFAULT_RSA_KEY_BITS): # Generate the keyid for the RSA key. 'key_value' corresponds to the # 'keyval' entry of the 'RSAKEY_SCHEMA' dictionary. The private key # information is not included in the generation of the 'keyid' identifier. - key_value = {'public': public, + key_value = {'public': binascii.hexlify(public), 'private': ''} keyid = _get_keyid(key_value) # Build the 'rsakey_dict' dictionary. Update 'key_value' with the RSA # private key prior to adding 'key_value' to 'rsakey_dict'. - key_value['private'] = private + key_value['private'] = binascii.hexlify(private) rsakey_dict['keytype'] = keytype rsakey_dict['keyid'] = keyid @@ -187,11 +196,17 @@ def generate_ed25519_key(): unspecified, a 3072-bit RSA key is generated, which is the key size recommended by TUF. - - bits: - The key size, or key length, of the RSA key. 'bits' must be 2048, or - greater, and a multiple of 256. + >>> ed25519_key = generate_ed25519_key() + >>> tuf.formats.ED25519KEY_SCHEMA.matches(ed25519_key) + True + >>> len(ed25519_key['keyval']['public']) + 64 + >>> len(ed25519_key['keyval']['private']) + 64 + + None. + ValueError, if an exception occurs after calling the RSA key generation routine. 'bits' must be a multiple of 256. The 'ValueError' exception is @@ -234,13 +249,13 @@ def generate_ed25519_key(): # Generate the keyid for the RSA key. 'key_value' corresponds to the # 'keyval' entry of the 'RSAKEY_SCHEMA' dictionary. The private key # information is not included in the generation of the 'keyid' identifier. - key_value = {'public': public, + key_value = {'public': binascii.hexlify(public), 'private': ''} keyid = _get_keyid(key_value) # Build the 'rsakey_dict' dictionary. Update 'key_value' with the RSA # private key prior to adding 'key_value' to 'rsakey_dict'. - key_value['private'] = private + key_value['private'] = binascii.hexlify(private) ed25519_key['keytype'] = keytype ed25519_key['keyid'] = keyid @@ -272,6 +287,12 @@ def create_in_metadata_format(key_type, key_value, private=False): RSA keys are stored in Metadata files (e.g., root.txt) in the format returned by this function. + + >>> ed25519_key = generate_ed25519_key() + >>> key_val = ed25519_key['keyval'] + >>> ed25519_metadata = create_in_metadata_format(key_val, private=True) + >>> tuf.formats.KEY_SCHEMA.matches(ed25519_metadata) + True key_type: @@ -337,6 +358,15 @@ def create_from_metadata_format(key_metadata): metadata files and needs converting. Generate() creates an entirely new key and returns it in the format appropriate for 'keydb.py' and 'keystore.py'. + + >>> ed25519_key = generate_ed25519_key() + >>> key_val = ed25519_key['keyval'] + >>> ed25519_metadata = create_in_metadata_format(key_val, private=True) + >>> ed25519_key_2 = create_from_metadata_format(ed25519_metadata) + >>> tuf.formats.ED25519KEY_SCHEMA.matches(ed25519_key_2) + True + >>> ed25519_key == ed25519_key_2 + True key_metadata: @@ -419,9 +449,9 @@ def _check_crypto_library(): 'Supported crypto libraries: '+repr(_SUPPORTED_CRYPTO_LIBRARIES)+'.' raise tuf.CryptoError(message) - if _CRYPTO_LIBRARY not in _AVAILABLE_CRYPTO_LIBRARIES: + if _CRYPTO_LIBRARY not in _available_crypto_libraries: message = 'The '+repr(_CRYPTO_LIBRARY)+' crypto library specified'+ \ - 'in "tuf.conf.CRYPTO_LIBRARY" could not be imported.' + ' in "tuf.conf.CRYPTO_LIBRARY" could not be imported.' raise tuf.CryptoError(message) @@ -440,7 +470,20 @@ def create_signature(key_dict, data): rsakey_dict['keyval']['private'] and 'data' to generate the signature. RFC3447 - RSASSA-PSS - http://www.ietf.org/rfc/rfc3447.txt + http://www.ietf.org/rfc/rfc3447. + + >>> ed25519_key_dict = generate_ed25519_key() + >>> data = 'The quick brown fox jumps over the lazy dog.' + >>> signature = create_signature(ed25519_key_dict, data) + >>> tuf.formats.SIGNATURE_SCHEMA.matches(signature) + True + >>> len(signature['sig']) + 128 + >>> rsa_key_dict = generate_rsaed25519_key() + >>> data = 'The quick brown fox jumps over the lazy dog.' + >>> signature = create_signature(ed25519_key_dict, data) + >>> tuf.formats.SIGNATURE_SCHEMA.matches(signature) + True key_dict: From 115d844a576473f1764df8c140143e6d26e390a5 Mon Sep 17 00:00:00 2001 From: vladdd Date: Wed, 9 Oct 2013 09:19:08 -0400 Subject: [PATCH 36/95] Fix import, doctests, and function parameters in keys.py --- tuf/keys.py | 63 ++++++++++++++++++++++++++------------------ tuf/pycrypto_keys.py | 2 +- 2 files changed, 38 insertions(+), 27 deletions(-) diff --git a/tuf/keys.py b/tuf/keys.py index acc84c49..e0be882f 100755 --- a/tuf/keys.py +++ b/tuf/keys.py @@ -46,11 +46,10 @@ _available_crypto_libraries = ['ed25519-python'] try: - #import Crypto - import tuf.pycrypto_keys.py + import Crypto + import tuf.pycrypto_keys _available_crypto_libraries.append('pycrypto') except ImportError: - print('Could not import pycrypto') pass try: @@ -105,9 +104,9 @@ def generate_rsa_key(bits=_DEFAULT_RSA_KEY_BITS): >>> rsa_key = generate_rsa_key(bits=2048) >>> tuf.formats.RSAKEY_SCHEMA.matches(rsa_key) True - >>> len(ed25519_key['keyval']['public']) + >>> len(rsa_key['keyval']['public']) 64 - >>> len(ed25519_key['keyval']['private']) + >>> len(rsa_key['keyval']['private']) 64 @@ -152,7 +151,7 @@ def generate_rsa_key(bits=_DEFAULT_RSA_KEY_BITS): # or not a multiple of 256, although a 2048-bit minimum is enforced by # tuf.formats.RSAKEYBITS_SCHEMA.check_match(). if _CRYPTO_LIBRARY == 'pycrypto': - public, private = tuf.pycrypto.generate_rsa_public_and_private(bits) + public, private = tuf.pycrypto_keys.generate_rsa_public_and_private(bits) else: message = 'Invalid crypto library: '+repr(_CRYPTO_LIBRARY)+'.' raise ValueError(message) @@ -160,13 +159,13 @@ def generate_rsa_key(bits=_DEFAULT_RSA_KEY_BITS): # Generate the keyid for the RSA key. 'key_value' corresponds to the # 'keyval' entry of the 'RSAKEY_SCHEMA' dictionary. The private key # information is not included in the generation of the 'keyid' identifier. - key_value = {'public': binascii.hexlify(public), + key_value = {'public': public, 'private': ''} - keyid = _get_keyid(key_value) + keyid = _get_keyid(keytype, key_value) # Build the 'rsakey_dict' dictionary. Update 'key_value' with the RSA # private key prior to adding 'key_value' to 'rsakey_dict'. - key_value['private'] = binascii.hexlify(private) + key_value['private'] = private rsakey_dict['keytype'] = keytype rsakey_dict['keyid'] = keyid @@ -251,7 +250,7 @@ def generate_ed25519_key(): # information is not included in the generation of the 'keyid' identifier. key_value = {'public': binascii.hexlify(public), 'private': ''} - keyid = _get_keyid(key_value) + keyid = _get_keyid(keytype, key_value) # Build the 'rsakey_dict' dictionary. Update 'key_value' with the RSA # private key prior to adding 'key_value' to 'rsakey_dict'. @@ -267,7 +266,7 @@ def generate_ed25519_key(): -def create_in_metadata_format(key_type, key_value, private=False): +def create_in_metadata_format(keytype, key_value, private=False): """ Return a dictionary conformant to 'tuf.formats.KEY_SCHEMA'. @@ -290,7 +289,8 @@ def create_in_metadata_format(key_type, key_value, private=False): >>> ed25519_key = generate_ed25519_key() >>> key_val = ed25519_key['keyval'] - >>> ed25519_metadata = create_in_metadata_format(key_val, private=True) + >>> keytype = ed25519_key['keytype'] + >>> ed25519_metadata = create_in_metadata_format(keytype, key_val, private=True) >>> tuf.formats.KEY_SCHEMA.matches(ed25519_metadata) True @@ -328,10 +328,10 @@ def create_in_metadata_format(key_type, key_value, private=False): tuf.formats.KEYVAL_SCHEMA.check_match(key_value) if private is True and key_value['private']: - return {'keytype': key_type, 'keyval': key_value} + return {'keytype': keytype, 'keyval': key_value} else: public_key_value = {'public': key_value['public'], 'private': ''} - return {'keytype': key_type, 'keyval': public_key_value} + return {'keytype': keytype, 'keyval': public_key_value} @@ -361,7 +361,8 @@ def create_from_metadata_format(key_metadata): >>> ed25519_key = generate_ed25519_key() >>> key_val = ed25519_key['keyval'] - >>> ed25519_metadata = create_in_metadata_format(key_val, private=True) + >>> keytype = ed25519_key['keytype'] + >>> ed25519_metadata = create_in_metadata_format(keytype, key_val, private=True) >>> ed25519_key_2 = create_from_metadata_format(ed25519_metadata) >>> tuf.formats.ED25519KEY_SCHEMA.matches(ed25519_key_2) True @@ -401,26 +402,26 @@ def create_from_metadata_format(key_metadata): # Convert 'key_value' to 'tuf.formats.KEY_SCHEMA' and generate its hash # The hash is in hexdigest form. - keyid = _get_keyid(key_value) + keyid = _get_keyid(keytype, key_value) # We now have all the required key values. Build 'rsakey_dict'. - rsakey_dict['keytype'] = keytype - rsakey_dict['keyid'] = keyid - rsakey_dict['keyval'] = key_value + key_dict['keytype'] = keytype + key_dict['keyid'] = keyid + key_dict['keyval'] = key_value - return rsakey_dict + return key_dict -def _get_keyid(key_value): +def _get_keyid(keytype, key_value): """Return the keyid for 'key_value'.""" # 'keyid' will be generated from an object conformant to KEY_SCHEMA, # which is the format Metadata files (e.g., root.txt) store keys. # 'create_in_metadata_format()' returns the object needed by _get_keyid(). - rsakey_meta = create_in_metadata_format(key_value, private=False) + rsakey_meta = create_in_metadata_format(keytype, key_value, private=False) # Convert the RSA key to JSON Canonical format suitable for adding # to digest objects. @@ -479,9 +480,9 @@ def create_signature(key_dict, data): True >>> len(signature['sig']) 128 - >>> rsa_key_dict = generate_rsaed25519_key() + >>> rsa_key_dict = generate_rsa_key() >>> data = 'The quick brown fox jumps over the lazy dog.' - >>> signature = create_signature(ed25519_key_dict, data) + >>> signature = create_signature(rsa_key_dict, data) >>> tuf.formats.SIGNATURE_SCHEMA.matches(signature) True @@ -524,16 +525,17 @@ def create_signature(key_dict, data): # The 'PyCrypto-PKCS#1 PSS' (i.e., PyCrypto module) signing method is the # only method currently supported. signature = {} + keytype = key_dict['keytype'] public = key_dict['keyval']['public'] private = key_dict['keyval']['private'] keyid = key_dict['keyid'] method = None sig = None - if key_type == 'rsa': + if keytype == 'rsa': if _CRYPTO_LIBRARY == 'pycrypto': sig, method = tuf.pycrypto_keys.create_signature(private, data) - elif key_type == 'ed25519': + elif keytype == 'ed25519': if 'ed25519-pynacl' in _available_crypto_libraries: sig, method = tuf.ed25519_keys.create_signature(public, private, data, use_pynacl=True) else: @@ -631,3 +633,12 @@ def verify_signature(key_dict, signature, data): raise TypeError('Unsupported key type.') return valid_signature + + + +if __name__ == '__main__': + # The interactive sessions of the documentation strings can + # be tested by running 'keys.py' as a standalone module. + # python -B keys.py + import doctest + doctest.testmod() diff --git a/tuf/pycrypto_keys.py b/tuf/pycrypto_keys.py index 31790f72..09ce75bb 100755 --- a/tuf/pycrypto_keys.py +++ b/tuf/pycrypto_keys.py @@ -61,7 +61,7 @@ _DEFAULT_RSA_KEY_BITS = 3072 -def generate_rsa_public_and_private_keys(bits=_DEFAULT_RSA_KEY_BITS): +def generate_rsa_public_and_private(bits=_DEFAULT_RSA_KEY_BITS): """ Generate public and private RSA keys, with modulus length 'bits'. From cc87d4fdb84104307bb41b2c810eb1c5b331f7c7 Mon Sep 17 00:00:00 2001 From: vladdd Date: Wed, 9 Oct 2013 11:21:09 -0400 Subject: [PATCH 37/95] Add missing doctests to keys.py and pycrypto_keys.py --- tuf/keys.py | 53 ++++++++++++++++++++++++++++---------------- tuf/pycrypto_keys.py | 53 +++++++++++++++++++++++++++++++++++++++----- 2 files changed, 82 insertions(+), 24 deletions(-) diff --git a/tuf/keys.py b/tuf/keys.py index e0be882f..61d94083 100755 --- a/tuf/keys.py +++ b/tuf/keys.py @@ -104,10 +104,12 @@ def generate_rsa_key(bits=_DEFAULT_RSA_KEY_BITS): >>> rsa_key = generate_rsa_key(bits=2048) >>> tuf.formats.RSAKEY_SCHEMA.matches(rsa_key) True - >>> len(rsa_key['keyval']['public']) - 64 - >>> len(rsa_key['keyval']['private']) - 64 + >>> public = rsa_key['keyval']['public'] + >>> private = rsa_key['keyval']['private'] + >>> tuf.formats.PEMRSA_SCHEMA.matches(public) + True + >>> tuf.formats.PEMRSA_SCHEMA.matches(private) + True bits: @@ -237,9 +239,11 @@ def generate_ed25519_key(): # or not a multiple of 256, although a 2048-bit minimum is enforced by # tuf.formats.RSAKEYBITS_SCHEMA.check_match(). if 'ed25519-pynacl' in _available_crypto_libraries: - public, private = tuf.ed25519_keys.generate_public_and_private(use_pynacl=True) + public, private = \ + tuf.ed25519_keys.generate_public_and_private(use_pynacl=True) elif 'ed25519-python' in _available_crypto_libraries: - public, private = tuf.ed25519_keys.generate_public_and_private(use_pynacl=False) + public, private = \ + tuf.ed25519_keys.generate_public_and_private(use_pynacl=False) else: message = 'A supported method of generating ed25519 keys not available\n'+\ 'Available crypto libraries: '+repr(_available_crypto_libraries)+'.' @@ -290,7 +294,8 @@ def create_in_metadata_format(keytype, key_value, private=False): >>> ed25519_key = generate_ed25519_key() >>> key_val = ed25519_key['keyval'] >>> keytype = ed25519_key['keytype'] - >>> ed25519_metadata = create_in_metadata_format(keytype, key_val, private=True) + >>> ed25519_metadata = \ + create_in_metadata_format(keytype, key_val, private=True) >>> tuf.formats.KEY_SCHEMA.matches(ed25519_metadata) True @@ -362,7 +367,8 @@ def create_from_metadata_format(key_metadata): >>> ed25519_key = generate_ed25519_key() >>> key_val = ed25519_key['keyval'] >>> keytype = ed25519_key['keytype'] - >>> ed25519_metadata = create_in_metadata_format(keytype, key_val, private=True) + >>> ed25519_metadata = \ + create_in_metadata_format(keytype, key_val, private=True) >>> ed25519_key_2 = create_from_metadata_format(ed25519_metadata) >>> tuf.formats.ED25519KEY_SCHEMA.matches(ed25519_key_2) True @@ -473,16 +479,16 @@ def create_signature(key_dict, data): RFC3447 - RSASSA-PSS http://www.ietf.org/rfc/rfc3447. - >>> ed25519_key_dict = generate_ed25519_key() - >>> data = 'The quick brown fox jumps over the lazy dog.' - >>> signature = create_signature(ed25519_key_dict, data) + >>> ed25519_key = generate_ed25519_key() + >>> data = 'The quick brown fox jumps over the lazy dog' + >>> signature = create_signature(ed25519_key, data) >>> tuf.formats.SIGNATURE_SCHEMA.matches(signature) True >>> len(signature['sig']) 128 - >>> rsa_key_dict = generate_rsa_key() - >>> data = 'The quick brown fox jumps over the lazy dog.' - >>> signature = create_signature(rsa_key_dict, data) + >>> rsa_key = generate_rsa_key() + >>> data = 'The quick brown fox jumps over the lazy dog' + >>> signature = create_signature(rsa_key, data) >>> tuf.formats.SIGNATURE_SCHEMA.matches(signature) True @@ -537,9 +543,13 @@ def create_signature(key_dict, data): sig, method = tuf.pycrypto_keys.create_signature(private, data) elif keytype == 'ed25519': if 'ed25519-pynacl' in _available_crypto_libraries: - sig, method = tuf.ed25519_keys.create_signature(public, private, data, use_pynacl=True) + public = binascii.unhexlify(public) + private = binascii.unhexlify(private) + sig, method = tuf.ed25519_keys.create_signature(public, private, + data, use_pynacl=True) else: - sig, method = tuf.ed25519_keys.create_signature(public, private, data, use_pynacl=False) + sig, method = tuf.ed25519_keys.create_signature(public, private, + data, use_pynacl=False) else: raise TypeError('Invalid key type.') @@ -623,12 +633,17 @@ def verify_signature(key_dict, signature, data): if key_type == 'rsa': if _CRYPTO_LIBRARY == 'pycrypto': - valid_signature = tuf.pycrypto_keys.verify_signature(sig, method, public, data) + valid_signature = tuf.pycrypto_keys.verify_signature(sig, method, + public, data) elif key_type == 'ed25519': if 'ed25519-pynacl' in _available_crypto_libraries: - valid_signature = tuf.ed25519_keys.verify_signature(public, method, sig, data, use_pynacl=True) + valid_signature = tuf.ed25519_keys.verify_signature(public, + method, sig, data, + use_pynacl=True) else: - valid_signature = tuf.ed25519_keys.verify_signature(public, method, sig, data, use_pynacl=False) + valid_signature = tuf.ed25519_keys.verify_signature(public, + method, sig, data, + use_pynacl=False) else: raise TypeError('Unsupported key type.') diff --git a/tuf/pycrypto_keys.py b/tuf/pycrypto_keys.py index 09ce75bb..5d1e1bc6 100755 --- a/tuf/pycrypto_keys.py +++ b/tuf/pycrypto_keys.py @@ -78,7 +78,13 @@ def generate_rsa_public_and_private(bits=_DEFAULT_RSA_KEY_BITS): generate() enforces a minimum key size of 2048 bits. If 'bits' is unspecified, a 3072-bit RSA key is generated, which is the key size recommended by TUF. - + + >>> public, private = generate_rsa_public_and_private(2048) + >>> tuf.formats.PEMRSA_SCHEMA.matches(public) + True + >>> tuf.formats.PEMRSA_SCHEMA.matches(private) + True + bits: The key size, or key length, of the RSA key. 'bits' must be 2048, or @@ -137,6 +143,16 @@ def create_signature(private_key, data): RFC3447 - RSASSA-PSS http://www.ietf.org/rfc/rfc3447.txt + + >>> public, private = generate_rsa_public_and_private(2048) + >>> data = 'The quick brown fox jumps over the lazy dog' + >>> signature, method = create_signature(private, data) + >>> tuf.formats.NAME_SCHEMA.matches(method) + True + >>> method == 'PyCrypto-PKCS#1 PSS' + True + >>> signature is not None + True private_key: @@ -162,7 +178,6 @@ def create_signature(private_key, data): # Signing the 'data' object requires a private key. # The 'PyCrypto-PKCS#1 PSS' (i.e., PyCrypto module) signing method is the # only method currently supported. - private = private_key method = 'PyCrypto-PKCS#1 PSS' signature = None @@ -177,7 +192,7 @@ def create_signature(private_key, data): rsa_key_object = Crypto.PublicKey.RSA.importKey(private_key) sha256_object = Crypto.Hash.SHA256.new(data) pkcs1_pss_signer = Crypto.Signature.PKCS1_PSS.new(rsa_key_object) - sig = pkcs1_pss_signer.sign(sha256_object) + signature = pkcs1_pss_signer.sign(sha256_object) except (ValueError, IndexError, TypeError), e: message = 'An RSA signature could not be generated.' raise tuf.CryptoError(message) @@ -198,6 +213,14 @@ def verify_signature(signature, signature_method, public_key, data): 'rsakey_dict', the 'method' and 'sig' objects contained in 'signature', and 'data' to complete the verification. Type-checking performed on both 'rsakey_dict' and 'signature'. + + >>> public, private = generate_rsa_public_and_private(2048) + >>> data = 'The quick brown fox jumps over the lazy dog' + >>> signature, method = create_signature(private, data) + >>> verify_signature(signature, method, public, data) + True + >>> verify_signature(signature, method, public, 'bad_data') + False signature: @@ -271,6 +294,12 @@ def create_rsa_encrypted_pem(private_key, passphrase): https://en.wikipedia.org/wiki/Triple_DES https://en.wikipedia.org/wiki/PBKDF2 + >>> public, private = generate_rsa_public_and_private(2048) + >>> passphrase = 'secret' + >>> encrypted_pem = create_rsa_encrypted_pem(private, passphrase) + >>> tuf.formats.PEMRSA_SCHEMA.matches(encrypted_pem) + True + private_key: The public and private keys are in PEM format and stored as strings. @@ -297,7 +326,7 @@ def create_rsa_encrypted_pem(private_key, passphrase): # This check will ensure 'rsakey_dict' has the appropriate number # of objects and object types, and that all dict keys are properly named. # Raise 'tuf.FormatError' if the check fails. - tuf.formats.RSAKEY_SCHEMA.check_match(rsakey_dict) + tuf.formats.PEMRSA_SCHEMA.check_match(private_key) # Does 'signature' have the correct format? tuf.formats.PASSWORD_SCHEMA.check_match(passphrase) @@ -321,7 +350,7 @@ def create_rsa_encrypted_pem(private_key, passphrase): -def create_rsa_from_encrypted_pem(encrypted_pem, passphrase): +def create_rsa_public_and_private_from_encrypted_pem(encrypted_pem, passphrase): """ Return an RSA key in 'tuf.formats.RSAKEY_SCHEMA' format, which has the @@ -338,6 +367,20 @@ def create_rsa_from_encrypted_pem(encrypted_pem, passphrase): Alternatively, key data may be encrypted with AES-CTR-Mode and the passphrase strengthened with PBKDF2+SHA256. See 'keystore.py'. + >>> public, private = generate_rsa_public_and_private(2048) + >>> passphrase = 'secret' + >>> encrypted_pem = create_rsa_encrypted_pem(private, passphrase) + >>> returned_public, returned_private = \ + create_rsa_public_and_private_from_encrypted_pem(encrypted_pem, passphrase) + >>> tuf.formats.PEMRSA_SCHEMA.matches(returned_public) + True + >>> tuf.formats.PEMRSA_SCHEMA.matches(returned_private) + True + >>> public == returned_public + True + >>> private == returned_private + True + encrypted_pem: A byte string in PEM format, where the private key is encrypted. It has From 37b665bf9a5990b5627ba3a8c377803ece930af7 Mon Sep 17 00:00:00 2001 From: vladdd Date: Wed, 9 Oct 2013 13:37:26 -0400 Subject: [PATCH 38/95] Modify behavior of configurable crypto and update conf.py Add missing doctest and minor edits. --- tuf/conf.py | 12 ++++----- tuf/ed25519_keys.py | 4 +-- tuf/keys.py | 60 ++++++++++++++++++++++++++++++++------------- 3 files changed, 51 insertions(+), 25 deletions(-) diff --git a/tuf/conf.py b/tuf/conf.py index 060101bc..d310d87b 100755 --- a/tuf/conf.py +++ b/tuf/conf.py @@ -13,10 +13,8 @@ A central location for TUF configuration settings. - """ - # Set a directory that should be used for all temporary files. If this # is None, then the system default will be used. The system default # will also be used if a directory path set here is invalid or @@ -66,7 +64,9 @@ # https://en.wikipedia.org/wiki/PBKDF2 PBKDF2_ITERATIONS = 100000 -# -# Supported Cryptography libraries: -# 'pycrypto', 'ed25519-python', 'ed25519-pynacl'. -CRYPTO_LIBRARY = 'pycrypto' +# The user client may set the cryptography library used by The Update Framework +# updater, or the software updater integrating TUF. The repository tools may +# also choose which crypto library to use when generating keys and performing +# other cryptographic operations. +# Supported cryptography libraries: ['pycrypto', 'ed25519', 'pynacl'] +CRYPTO_LIBRARY = 'pynacl' diff --git a/tuf/ed25519_keys.py b/tuf/ed25519_keys.py index ea7a0735..2aaf0c13 100755 --- a/tuf/ed25519_keys.py +++ b/tuf/ed25519_keys.py @@ -368,7 +368,7 @@ def verify_signature(public_key, method, signature, data, use_pynacl=False): if __name__ == '__main__': # The interactive sessions of the documentation strings can - # be tested by running 'ed25519.py' as a standalone module. - # python -B ed25519.py + # be tested by running 'ed25519_keys.py' as a standalone module. + # python -B ed25519_keys.py import doctest doctest.testmod() diff --git a/tuf/keys.py b/tuf/keys.py index 61d94083..3868fe89 100755 --- a/tuf/keys.py +++ b/tuf/keys.py @@ -40,10 +40,10 @@ import binascii # -_SUPPORTED_CRYPTO_LIBRARIES = ['pycrypto', 'ed25519-python', 'ed25519-pynacl'] +_SUPPORTED_CRYPTO_LIBRARIES = ['pycrypto', 'ed25519', 'pynacl'] # -_available_crypto_libraries = ['ed25519-python'] +_available_crypto_libraries = ['ed25519'] try: import Crypto @@ -54,7 +54,7 @@ try: import nacl - _available_crypto_libraries.append('ed25519-pynacl') + _available_crypto_libraries.append('pynacl') except ImportError: pass @@ -238,16 +238,12 @@ def generate_ed25519_key(): # the actual key generation. Raise 'ValueError' if 'bits' is less than 1024 # or not a multiple of 256, although a 2048-bit minimum is enforced by # tuf.formats.RSAKEYBITS_SCHEMA.check_match(). - if 'ed25519-pynacl' in _available_crypto_libraries: + if 'pynacl' in _available_crypto_libraries: public, private = \ tuf.ed25519_keys.generate_public_and_private(use_pynacl=True) - elif 'ed25519-python' in _available_crypto_libraries: + else: public, private = \ tuf.ed25519_keys.generate_public_and_private(use_pynacl=False) - else: - message = 'A supported method of generating ed25519 keys not available\n'+\ - 'Available crypto libraries: '+repr(_available_crypto_libraries)+'.' - raise ValueError(message) # Generate the keyid for the RSA key. 'key_value' corresponds to the # 'keyval' entry of the 'RSAKEY_SCHEMA' dictionary. The private key @@ -486,7 +482,7 @@ def create_signature(key_dict, data): True >>> len(signature['sig']) 128 - >>> rsa_key = generate_rsa_key() + >>> rsa_key = generate_rsa_key(2048) >>> data = 'The quick brown fox jumps over the lazy dog' >>> signature = create_signature(rsa_key, data) >>> tuf.formats.SIGNATURE_SCHEMA.matches(signature) @@ -526,6 +522,10 @@ def create_signature(key_dict, data): # of objects and object types, and that all dict keys are properly named. # Raise 'tuf.FormatError' if the check fails. tuf.formats.ANYKEY_SCHEMA.check_match(key_dict) + + # Raise 'tuf.Error' if 'tuf.conf.CRYPTO_LIBRARY' is not supported or could + # not be imported. + _check_crypto_library() # Signing the 'data' object requires a private key. # The 'PyCrypto-PKCS#1 PSS' (i.e., PyCrypto module) signing method is the @@ -541,10 +541,14 @@ def create_signature(key_dict, data): if keytype == 'rsa': if _CRYPTO_LIBRARY == 'pycrypto': sig, method = tuf.pycrypto_keys.create_signature(private, data) + else: + message = 'Unsupported "tuf.conf.CRYPTO_LIBRARY": '+\ + repr(_CRYPTO_LIBRARY)+'.' + raise tuf.Error(message) elif keytype == 'ed25519': - if 'ed25519-pynacl' in _available_crypto_libraries: - public = binascii.unhexlify(public) - private = binascii.unhexlify(private) + public = binascii.unhexlify(public) + private = binascii.unhexlify(private) + if _CRYPTO_LIBRARY == 'pynacl' and 'pynacl' in _available_crypto_libraries: sig, method = tuf.ed25519_keys.create_signature(public, private, data, use_pynacl=True) else: @@ -574,6 +578,21 @@ def verify_signature(key_dict, signature, data): and 'data' to complete the verification. Type-checking performed on both 'rsakey_dict' and 'signature'. + >>> ed25519_key = generate_ed25519_key() + >>> data = 'The quick brown fox jumps over the lazy dog' + >>> signature = create_signature(ed25519_key, data) + >>> verify_signature(ed25519_key, signature, data) + True + >>> verify_signature(ed25519_key, signature, 'bad_data') + False + >>> rsa_key = generate_rsa_key() + >>> signature = create_signature(rsa_key, data) + >>> verify_signature(rsa_key, signature, data) + True + >>> verify_signature(rsa_key, signature, 'bad_data') + False + + key_dict: A dictionary containing the RSA keys and other identifying information. @@ -628,15 +647,22 @@ def verify_signature(key_dict, signature, data): # ensure 'PyCrypto-PKCS#1 PSS' was used as the signing method. method = signature['method'] sig = signature['sig'] - public = rsakey_dict['keyval']['public'] + sig = binascii.unhexlify(sig) + public = key_dict['keyval']['public'] + keytype = key_dict['keytype'] valid_signature = False - if key_type == 'rsa': + if keytype == 'rsa': if _CRYPTO_LIBRARY == 'pycrypto': valid_signature = tuf.pycrypto_keys.verify_signature(sig, method, public, data) - elif key_type == 'ed25519': - if 'ed25519-pynacl' in _available_crypto_libraries: + else: + message = 'Unsupported "tuf.conf.CRYPTO_LIBRARY": '+\ + repr(_CRYPTO_LIBRARY)+'.' + raise tuf.Error(message) + elif keytype == 'ed25519': + public = binascii.unhexlify(public) + if _CRYPTO_LIBRARY == 'pynacl' and 'pynacl' in _available_crypto_libraries: valid_signature = tuf.ed25519_keys.verify_signature(public, method, sig, data, use_pynacl=True) From 3bbacd06a80185529cdb67eefd8ce779d091be39 Mon Sep 17 00:00:00 2001 From: vladdd Date: Wed, 9 Oct 2013 13:56:47 -0400 Subject: [PATCH 39/95] Fix docstring whitespace in formats.py --- tuf/formats.py | 16 ---------------- 1 file changed, 16 deletions(-) diff --git a/tuf/formats.py b/tuf/formats.py index ccb5c645..6a58737f 100755 --- a/tuf/formats.py +++ b/tuf/formats.py @@ -57,10 +57,8 @@ Example: signable_object = make_signable(unsigned_object) - """ - import binascii import calendar import re @@ -388,7 +386,6 @@ class MetaFile(object): and ReleaseFile all inherit from MetaFile. The __eq__, __ne__, perform 'equal' and 'not equal' comparisons between Metadata File objects. - """ info = None @@ -406,7 +403,6 @@ def __getattr__(self, name): Allow all metafile objects to have their interesting attributes referred to directly without the info dict. The info dict is just to be able to do the __eq__ comparison generically. - """ if name in self.info: @@ -650,7 +646,6 @@ def format_time(timestamp): A string in 'YYYY-MM-DD HH:MM:SS UTC' format. - """ try: @@ -682,7 +677,6 @@ def parse_time(string): A timestamp (e.g., 499137660). - """ # Is 'string' properly formatted? @@ -720,7 +714,6 @@ def format_base64(data): A base64-encoded string. - """ try: @@ -751,7 +744,6 @@ def parse_base64(base64_string): A byte string representing the parsed based64 encoding of 'base64_string'. - """ if not isinstance(base64_string, basestring): @@ -796,7 +788,6 @@ def make_signable(object): A dict in 'SIGNABLE_SCHEMA' format. - """ if not isinstance(object, dict) or 'signed' not in object: @@ -837,7 +828,6 @@ def make_fileinfo(length, hashes, custom=None): A dictionary conformant to 'FILEINFO_SCHEMA', representing the file information of a metadata or target file. - """ fileinfo = {'length' : length, 'hashes' : hashes} @@ -894,7 +884,6 @@ def make_role_metadata(keyids, threshold, name=None, paths=None, A properly formatted role meta dict, conforming to 'ROLE_SCHEMA'. - """ role_meta = {} @@ -955,7 +944,6 @@ def get_role_class(expected_rolename): The class corresponding to 'expected_rolename'. E.g., 'Release' as an argument to this function causes 'ReleaseFile' to be returned. - """ # Does 'expected_rolename' have the correct type? @@ -998,7 +986,6 @@ def expected_meta_rolename(meta_rolename): A string (e.g., 'Root', 'Targets'). - """ # Does 'meta_rolename' have the correct type? @@ -1038,7 +1025,6 @@ def check_signable_object_format(object): A string representing the signing role (e.g., 'root', 'targets'). The role string is returned with characters all lower case. - """ # Does 'object' have the correct type? @@ -1082,7 +1068,6 @@ def _canonical_string_encoder(string): A string with the canonical-encoded 'string' embedded. - """ string = '"%s"' % re.sub(r'(["\\])', r'\\\1', string) @@ -1187,7 +1172,6 @@ def encode_canonical(object, output_function=None): A string representing the 'object' encoded in canonical JSON form. - """ result = None From 31d603c710d11a1aececc96a6365097e11285968 Mon Sep 17 00:00:00 2001 From: vladdd Date: Thu, 10 Oct 2013 12:19:46 -0400 Subject: [PATCH 40/95] Update all unit tests affected by configurable crypto Add separate 'tuf.conf.py' options for key types. --- tests/unit/test_keydb.py | 50 +++++++++++++------------- tests/unit/test_keystore.py | 6 ++-- tests/unit/test_quickstart.py | 5 ++- tests/unit/test_roledb.py | 5 ++- tests/unit/test_sig.py | 34 +++++++++--------- tuf/conf.py | 10 +++--- tuf/keydb.py | 8 ++--- tuf/keys.py | 67 +++++++++++++++++++++-------------- tuf/pycrypto_keys.py | 62 ++++++++++++++++++-------------- tuf/repo/keystore.py | 25 ++++++------- tuf/repo/signercli.py | 34 ++++-------------- tuf/repo/signerlib.py | 4 ++- tuf/tests/repository_setup.py | 3 -- tuf/tests/unittest_toolbox.py | 8 ++--- 14 files changed, 156 insertions(+), 165 deletions(-) diff --git a/tests/unit/test_keydb.py b/tests/unit/test_keydb.py index c7330875..fe57b776 100755 --- a/tests/unit/test_keydb.py +++ b/tests/unit/test_keydb.py @@ -13,7 +13,6 @@ Unit test for 'keydb.py'. - """ import unittest @@ -21,7 +20,7 @@ import tuf import tuf.formats -import tuf.rsa_key +import tuf.keys import tuf.keydb import tuf.log @@ -31,7 +30,7 @@ # Generate the three keys to use in our test cases. KEYS = [] for junk in range(3): - KEYS.append(tuf.rsa_key.generate(2048)) + KEYS.append(tuf.keys.generate_rsa_key(2048)) @@ -89,7 +88,7 @@ def test_get_key(self): - def test_add_rsakey(self): + def test_add_key(self): # Test conditions using valid 'keyid' arguments. rsakey = KEYS[0] keyid = KEYS[0]['keyid'] @@ -97,9 +96,9 @@ def test_add_rsakey(self): keyid2 = KEYS[1]['keyid'] rsakey3 = KEYS[2] keyid3 = KEYS[2]['keyid'] - self.assertEqual(None, tuf.keydb.add_rsakey(rsakey, keyid)) - self.assertEqual(None, tuf.keydb.add_rsakey(rsakey2, keyid2)) - self.assertEqual(None, tuf.keydb.add_rsakey(rsakey3)) + self.assertEqual(None, tuf.keydb.add_key(rsakey, keyid)) + self.assertEqual(None, tuf.keydb.add_key(rsakey2, keyid2)) + self.assertEqual(None, tuf.keydb.add_key(rsakey3)) self.assertEqual(rsakey, tuf.keydb.get_key(keyid)) self.assertEqual(rsakey2, tuf.keydb.get_key(keyid2)) @@ -109,26 +108,26 @@ def test_add_rsakey(self): tuf.keydb.clear_keydb() rsakey3['keytype'] = 'bad_keytype' - self.assertRaises(tuf.FormatError, tuf.keydb.add_rsakey, None, keyid) - self.assertRaises(tuf.FormatError, tuf.keydb.add_rsakey, '', keyid) - self.assertRaises(tuf.FormatError, tuf.keydb.add_rsakey, ['123'], keyid) - self.assertRaises(tuf.FormatError, tuf.keydb.add_rsakey, {'a': 'b'}, keyid) - self.assertRaises(tuf.FormatError, tuf.keydb.add_rsakey, rsakey, {'keyid': ''}) - self.assertRaises(tuf.FormatError, tuf.keydb.add_rsakey, rsakey, 123) - self.assertRaises(tuf.FormatError, tuf.keydb.add_rsakey, rsakey, False) - self.assertRaises(tuf.FormatError, tuf.keydb.add_rsakey, rsakey, ['keyid']) - self.assertRaises(tuf.FormatError, tuf.keydb.add_rsakey, rsakey3, keyid3) + self.assertRaises(tuf.FormatError, tuf.keydb.add_key, None, keyid) + self.assertRaises(tuf.FormatError, tuf.keydb.add_key, '', keyid) + self.assertRaises(tuf.FormatError, tuf.keydb.add_key, ['123'], keyid) + self.assertRaises(tuf.FormatError, tuf.keydb.add_key, {'a': 'b'}, keyid) + self.assertRaises(tuf.FormatError, tuf.keydb.add_key, rsakey, {'keyid': ''}) + self.assertRaises(tuf.FormatError, tuf.keydb.add_key, rsakey, 123) + self.assertRaises(tuf.FormatError, tuf.keydb.add_key, rsakey, False) + self.assertRaises(tuf.FormatError, tuf.keydb.add_key, rsakey, ['keyid']) + self.assertRaises(tuf.FormatError, tuf.keydb.add_key, rsakey3, keyid3) rsakey3['keytype'] = 'rsa' # Test conditions where keyid does not match the rsakey. - self.assertRaises(tuf.Error, tuf.keydb.add_rsakey, rsakey, keyid2) - self.assertRaises(tuf.Error, tuf.keydb.add_rsakey, rsakey2, keyid) + self.assertRaises(tuf.Error, tuf.keydb.add_key, rsakey, keyid2) + self.assertRaises(tuf.Error, tuf.keydb.add_key, rsakey2, keyid) # Test conditions using keyids that have already been added. - tuf.keydb.add_rsakey(rsakey, keyid) - tuf.keydb.add_rsakey(rsakey2, keyid2) - self.assertRaises(tuf.KeyAlreadyExistsError, tuf.keydb.add_rsakey, rsakey) - self.assertRaises(tuf.KeyAlreadyExistsError, tuf.keydb.add_rsakey, rsakey2) + tuf.keydb.add_key(rsakey, keyid) + tuf.keydb.add_key(rsakey2, keyid2) + self.assertRaises(tuf.KeyAlreadyExistsError, tuf.keydb.add_key, rsakey) + self.assertRaises(tuf.KeyAlreadyExistsError, tuf.keydb.add_key, rsakey2) @@ -140,12 +139,13 @@ def test_remove_key(self): keyid2 = KEYS[1]['keyid'] rsakey3 = KEYS[2] keyid3 = KEYS[2]['keyid'] - tuf.keydb.add_rsakey(rsakey, keyid) - tuf.keydb.add_rsakey(rsakey2, keyid2) - tuf.keydb.add_rsakey(rsakey3, keyid3) + tuf.keydb.add_key(rsakey, keyid) + tuf.keydb.add_key(rsakey2, keyid2) + tuf.keydb.add_key(rsakey3, keyid3) self.assertEqual(None, tuf.keydb.remove_key(keyid)) self.assertEqual(None, tuf.keydb.remove_key(keyid2)) + # Ensure the keys were actually removed. self.assertRaises(tuf.UnknownKeyError, tuf.keydb.get_key, keyid) self.assertRaises(tuf.UnknownKeyError, tuf.keydb.get_key, keyid2) diff --git a/tests/unit/test_keystore.py b/tests/unit/test_keystore.py index 5eed223a..e1a8c7f2 100755 --- a/tests/unit/test_keystore.py +++ b/tests/unit/test_keystore.py @@ -13,7 +13,6 @@ Unit test for keystore.py. - """ import unittest @@ -25,7 +24,7 @@ import tuf import tuf.repo.keystore -import tuf.rsa_key +import tuf.keys import tuf.formats import tuf.util import tuf.log @@ -56,7 +55,7 @@ for i in range(3): # Populating the original 'RSAKEYS' and 'PASSWDS' lists. - RSAKEYS.append(tuf.rsa_key.generate()) + RSAKEYS.append(tuf.keys.generate_rsa_key()) PASSWDS.append('passwd_'+str(i)) # Saving original copies of 'RSAKEYS' and 'PASSWDS' to temp variables @@ -350,6 +349,7 @@ def tearDownModule(): tuf.repo.keystore.clear_keystore() + # Run the unit tests. if __name__ == '__main__': unittest.main() diff --git a/tests/unit/test_quickstart.py b/tests/unit/test_quickstart.py index fd7e5515..ebb3432e 100755 --- a/tests/unit/test_quickstart.py +++ b/tests/unit/test_quickstart.py @@ -18,7 +18,6 @@ Given that all message prompts don't change - this will work pretty well for running quickstart without having to manually enter input to prompts every time you want to run quickstart. - """ import os @@ -72,7 +71,7 @@ def test_2_build_repository(self): proj_files = self.make_temp_directory_with_data_files() proj_dir = os.path.join(proj_files[0], 'targets') - input_dict = {'expiration':'12/12/2013', + input_dict = {'expiration':'12/12/2018', 'root':{'threshold':1, 'password':'pass'}, 'targets':{'threshold':1, 'password':'pass'}, 'release':{'threshold':1, 'password':'pass'}, @@ -128,7 +127,7 @@ def _remove_repository_directories(repo_dir, keystore_dir, client_dir): _remove_repository_directories(repo_dir, keystore_dir, client_dir) # Restore expiration. - input_dict['expiration'] = '10/10/2013' + input_dict['expiration'] = '10/10/2018' # Supplying bogus 'root' threshold. Doing this for all roles slows # the test significantly. diff --git a/tests/unit/test_roledb.py b/tests/unit/test_roledb.py index 0b0259b5..cb048854 100755 --- a/tests/unit/test_roledb.py +++ b/tests/unit/test_roledb.py @@ -13,7 +13,6 @@ Unit test for 'roledb.py'. - """ @@ -22,7 +21,7 @@ import tuf import tuf.formats -import tuf.rsa_key +import tuf.keys import tuf.roledb import tuf.log @@ -32,7 +31,7 @@ # Generate the three keys to use in our test cases. KEYS = [] for junk in range(3): - KEYS.append(tuf.rsa_key.generate(2048)) + KEYS.append(tuf.keys.generate_rsa_key(2048)) diff --git a/tests/unit/test_sig.py b/tests/unit/test_sig.py index efdbe516..db8fe579 100755 --- a/tests/unit/test_sig.py +++ b/tests/unit/test_sig.py @@ -14,10 +14,8 @@ Test cases for for sig.py. - """ - import unittest import logging @@ -26,7 +24,7 @@ import tuf.formats import tuf.keydb import tuf.roledb -import tuf.rsa_key +import tuf.keys import tuf.sig logger = logging.getLogger('tuf.test_sig') @@ -34,7 +32,7 @@ # Setup the keys to use in our test cases. KEYS = [] for _ in range(3): - KEYS.append(tuf.rsa_key.generate(2048)) + KEYS.append(tuf.keys.generate_rsa_key(2048)) @@ -55,7 +53,7 @@ def test_get_signature_status_no_role(self): signable['signatures'].append(tuf.sig.generate_rsa_signature( signable['signed'], KEYS[0])) - tuf.keydb.add_rsakey(KEYS[0]) + tuf.keydb.add_key(KEYS[0]) # No specific role we're considering. sig_status = tuf.sig.get_signature_status(signable, None) @@ -82,7 +80,7 @@ def test_get_signature_status_bad_sig(self): signable['signed'], KEYS[0])) signable['signed'] += 'signature no longer matches signed data' - tuf.keydb.add_rsakey(KEYS[0]) + tuf.keydb.add_key(KEYS[0]) threshold = 1 roleinfo = tuf.formats.make_role_metadata( [KEYS[0]['keyid']], threshold) @@ -112,7 +110,7 @@ def test_get_signature_status_unknown_method(self): signable['signed'], KEYS[0])) signable['signatures'][0]['method'] = 'fake-sig-method' - tuf.keydb.add_rsakey(KEYS[0]) + tuf.keydb.add_key(KEYS[0]) threshold = 1 roleinfo = tuf.formats.make_role_metadata( [KEYS[0]['keyid']], threshold) @@ -142,7 +140,7 @@ def test_get_signature_status_single_key(self): signable['signatures'].append(tuf.sig.generate_rsa_signature( signable['signed'], KEYS[0])) - tuf.keydb.add_rsakey(KEYS[0]) + tuf.keydb.add_key(KEYS[0]) threshold = 1 roleinfo = tuf.formats.make_role_metadata( [KEYS[0]['keyid']], threshold) @@ -171,7 +169,7 @@ def test_get_signature_status_below_threshold(self): signable['signatures'].append(tuf.sig.generate_rsa_signature( signable['signed'], KEYS[0])) - tuf.keydb.add_rsakey(KEYS[0]) + tuf.keydb.add_key(KEYS[0]) threshold = 2 roleinfo = tuf.formats.make_role_metadata( [KEYS[0]['keyid'], @@ -205,8 +203,8 @@ def test_get_signature_status_below_threshold_unrecognized_sigs(self): signable['signatures'].append(tuf.sig.generate_rsa_signature( signable['signed'], KEYS[2])) - tuf.keydb.add_rsakey(KEYS[0]) - tuf.keydb.add_rsakey(KEYS[1]) + tuf.keydb.add_key(KEYS[0]) + tuf.keydb.add_key(KEYS[1]) threshold = 2 roleinfo = tuf.formats.make_role_metadata( [KEYS[0]['keyid'], @@ -242,8 +240,8 @@ def test_get_signature_status_below_threshold_unauthorized_sigs(self): signable['signatures'].append(tuf.sig.generate_rsa_signature( signable['signed'], KEYS[1])) - tuf.keydb.add_rsakey(KEYS[0]) - tuf.keydb.add_rsakey(KEYS[1]) + tuf.keydb.add_key(KEYS[0]) + tuf.keydb.add_key(KEYS[1]) threshold = 2 roleinfo = tuf.formats.make_role_metadata( [KEYS[0]['keyid'], KEYS[2]['keyid']], threshold) @@ -278,7 +276,7 @@ def test_check_signatures_no_role(self): signable['signatures'].append(tuf.sig.generate_rsa_signature( signable['signed'], KEYS[0])) - tuf.keydb.add_rsakey(KEYS[0]) + tuf.keydb.add_key(KEYS[0]) # No specific role we're considering. It's invalid to use the # function tuf.sig.verify() without a role specified because @@ -295,7 +293,7 @@ def test_verify_single_key(self): signable['signatures'].append(tuf.sig.generate_rsa_signature( signable['signed'], KEYS[0])) - tuf.keydb.add_rsakey(KEYS[0]) + tuf.keydb.add_key(KEYS[0]) threshold = 1 roleinfo = tuf.formats.make_role_metadata( [KEYS[0]['keyid']], threshold) @@ -321,8 +319,8 @@ def test_verify_unrecognized_sig(self): signable['signatures'].append(tuf.sig.generate_rsa_signature( signable['signed'], KEYS[2])) - tuf.keydb.add_rsakey(KEYS[0]) - tuf.keydb.add_rsakey(KEYS[1]) + tuf.keydb.add_key(KEYS[0]) + tuf.keydb.add_key(KEYS[1]) threshold = 2 roleinfo = tuf.formats.make_role_metadata( [KEYS[0]['keyid'], KEYS[1]['keyid']], threshold) @@ -363,7 +361,7 @@ def test_may_need_new_keys(self): signable['signatures'].append(tuf.sig.generate_rsa_signature( signable['signed'], KEYS[0])) - tuf.keydb.add_rsakey(KEYS[1]) + tuf.keydb.add_key(KEYS[1]) threshold = 1 roleinfo = tuf.formats.make_role_metadata( [KEYS[1]['keyid']], threshold) diff --git a/tuf/conf.py b/tuf/conf.py index d310d87b..fb62141b 100755 --- a/tuf/conf.py +++ b/tuf/conf.py @@ -66,7 +66,9 @@ # The user client may set the cryptography library used by The Update Framework # updater, or the software updater integrating TUF. The repository tools may -# also choose which crypto library to use when generating keys and performing -# other cryptographic operations. -# Supported cryptography libraries: ['pycrypto', 'ed25519', 'pynacl'] -CRYPTO_LIBRARY = 'pynacl' +# Supported RSA cryptography libraries: ['pycrypto'] +RSA_CRYPTO_LIBRARY = 'pycrypto' + +# Supported ed25519 cryptography libraries: ['pynacl', 'ed25519'] +ED25519_CRYPTO_LIBRARY = 'pynacl' + diff --git a/tuf/keydb.py b/tuf/keydb.py index 09817d82..6cae0272 100755 --- a/tuf/keydb.py +++ b/tuf/keydb.py @@ -60,7 +60,7 @@ def create_keydb_from_root_metadata(root_metadata): A function to add the key to the database is called. In the case of RSA - keys, this function is add_rsakey(). + keys, this function is add_key(). The old keydb key database is replaced. @@ -84,10 +84,10 @@ def create_keydb_from_root_metadata(root_metadata): if key_metadata['keytype'] == 'rsa': # 'key_metadata' is stored in 'KEY_SCHEMA' format. Call # create_from_metadata_format() to get the key in 'RSAKEY_SCHEMA' - # format, which is the format expected by 'add_rsakey()'. - rsakey_dict = tuf.keys.rsa_keys.create_from_metadata_format(key_metadata) + # format, which is the format expected by 'add_key()'. + rsakey_dict = tuf.keys.create_from_metadata_format(key_metadata) try: - add_rsakey(rsakey_dict, keyid) + add_key(rsakey_dict, keyid) # 'tuf.Error' raised if keyid does not match the keyid for 'rsakey_dict'. except tuf.Error, e: logger.error(e) diff --git a/tuf/keys.py b/tuf/keys.py index 3868fe89..49c78a35 100755 --- a/tuf/keys.py +++ b/tuf/keys.py @@ -40,7 +40,10 @@ import binascii # -_SUPPORTED_CRYPTO_LIBRARIES = ['pycrypto', 'ed25519', 'pynacl'] +_SUPPORTED_RSA_CRYPTO_LIBRARIES = ['pycrypto'] + +# +_SUPPORTED_ED25519_CRYPTO_LIBRARIES = ['ed25519', 'pynacl'] # _available_crypto_libraries = ['ed25519'] @@ -79,8 +82,9 @@ # size 3072 provide security through 2031 and beyond. _DEFAULT_RSA_KEY_BITS = 3072 -# The crypto library to use in 'keys.py'. -_CRYPTO_LIBRARY = tuf.conf.CRYPTO_LIBRARY +# The crypto libraries used in 'keys.py'. +_RSA_CRYPTO_LIBRARY = tuf.conf.RSA_CRYPTO_LIBRARY +_ED25519_CRYPTO_LIBRARY = tuf.conf.ED25519_CRYPTO_LIBRARY def generate_rsa_key(bits=_DEFAULT_RSA_KEY_BITS): @@ -139,7 +143,7 @@ def generate_rsa_key(bits=_DEFAULT_RSA_KEY_BITS): # Raise 'tuf.Error' if 'tuf.conf.CRYPTO_LIBRARY' is not supported or could # not be imported. - _check_crypto_library() + _check_crypto_libraries() # Check for valid crypto library # Begin building the RSA key dictionary. @@ -152,10 +156,10 @@ def generate_rsa_key(bits=_DEFAULT_RSA_KEY_BITS): # the actual key generation. Raise 'ValueError' if 'bits' is less than 1024 # or not a multiple of 256, although a 2048-bit minimum is enforced by # tuf.formats.RSAKEYBITS_SCHEMA.check_match(). - if _CRYPTO_LIBRARY == 'pycrypto': + if _RSA_CRYPTO_LIBRARY == 'pycrypto': public, private = tuf.pycrypto_keys.generate_rsa_public_and_private(bits) else: - message = 'Invalid crypto library: '+repr(_CRYPTO_LIBRARY)+'.' + message = 'Invalid crypto library: '+repr(_RSA_CRYPTO_LIBRARY)+'.' raise ValueError(message) # Generate the keyid for the RSA key. 'key_value' corresponds to the @@ -225,7 +229,7 @@ def generate_ed25519_key(): # Raise 'tuf.Error' if 'tuf.conf.CRYPTO_LIBRARY' is not supported or could # not be imported. - _check_crypto_library() + _check_crypto_libraries() # Check for valid crypto library # Begin building the RSA key dictionary. @@ -443,18 +447,29 @@ def _get_keyid(keytype, key_value): -def _check_crypto_library(): +def _check_crypto_libraries(): """ check """ - if _CRYPTO_LIBRARY not in _SUPPORTED_CRYPTO_LIBRARIES: - message = 'The '+repr(_CRYPTO_LIBRARY)+' crypto library specified'+ \ - ' in "tuf.conf.CRYPTO_LIBRARY" is not supported.\n'+ \ - 'Supported crypto libraries: '+repr(_SUPPORTED_CRYPTO_LIBRARIES)+'.' + if _RSA_CRYPTO_LIBRARY not in _SUPPORTED_RSA_CRYPTO_LIBRARIES: + message = 'The '+repr(_RSA_CRYPTO_LIBRARY)+' crypto library specified'+ \ + ' in "tuf.conf.RSA_CRYPTO_LIBRARY" is not supported.\n'+ \ + 'Supported crypto libraries: '+repr(_SUPPORTED_RSA_CRYPTO_LIBRARIES)+'.' raise tuf.CryptoError(message) - if _CRYPTO_LIBRARY not in _available_crypto_libraries: - message = 'The '+repr(_CRYPTO_LIBRARY)+' crypto library specified'+ \ - ' in "tuf.conf.CRYPTO_LIBRARY" could not be imported.' + if _ED25519_CRYPTO_LIBRARY not in _SUPPORTED_ED25519_CRYPTO_LIBRARIES: + message = 'The '+repr(_ED25519_CRYPTO_LIBRARY)+' crypto library specified'+ \ + ' in "tuf.conf.ED25519_CRYPTO_LIBRARY" is not supported.\n'+ \ + 'Supported crypto libraries: '+repr(_SUPPORTED_ED25519_CRYPTO_LIBRARIES)+'.' + raise tuf.CryptoError(message) + + if _RSA_CRYPTO_LIBRARY not in _available_crypto_libraries: + message = 'The '+repr(_RSA_CRYPTO_LIBRARY)+' crypto library specified'+ \ + ' in "tuf.conf.RSA_CRYPTO_LIBRARY" could not be imported.' + raise tuf.CryptoError(message) + + if _ED25519_CRYPTO_LIBRARY not in _available_crypto_libraries: + message = 'The '+repr(_ED25519_CRYPTO_LIBRARY)+' crypto library specified'+ \ + ' in "tuf.conf.ED25519_CRYPTO_LIBRARY" could not be imported.' raise tuf.CryptoError(message) @@ -525,7 +540,7 @@ def create_signature(key_dict, data): # Raise 'tuf.Error' if 'tuf.conf.CRYPTO_LIBRARY' is not supported or could # not be imported. - _check_crypto_library() + _check_crypto_libraries() # Signing the 'data' object requires a private key. # The 'PyCrypto-PKCS#1 PSS' (i.e., PyCrypto module) signing method is the @@ -539,23 +554,23 @@ def create_signature(key_dict, data): sig = None if keytype == 'rsa': - if _CRYPTO_LIBRARY == 'pycrypto': + if _RSA_CRYPTO_LIBRARY == 'pycrypto': sig, method = tuf.pycrypto_keys.create_signature(private, data) else: - message = 'Unsupported "tuf.conf.CRYPTO_LIBRARY": '+\ - repr(_CRYPTO_LIBRARY)+'.' - raise tuf.Error(message) + message = 'Unsupported "tuf.conf.RSA_CRYPTO_LIBRARY": '+\ + repr(_RSA_CRYPTO_LIBRARY)+'.' + raise tuf.Error(message) elif keytype == 'ed25519': public = binascii.unhexlify(public) private = binascii.unhexlify(private) - if _CRYPTO_LIBRARY == 'pynacl' and 'pynacl' in _available_crypto_libraries: + if _ED25519_CRYPTO_LIBRARY == 'pynacl' and 'pynacl' in _available_crypto_libraries: sig, method = tuf.ed25519_keys.create_signature(public, private, data, use_pynacl=True) else: sig, method = tuf.ed25519_keys.create_signature(public, private, data, use_pynacl=False) else: - raise TypeError('Invalid key type.') + raise TypeError('Invalid key type.') # Build the signature dictionary to be returned. # The hexadecimal representation of 'sig' is stored in the signature. @@ -653,16 +668,16 @@ def verify_signature(key_dict, signature, data): valid_signature = False if keytype == 'rsa': - if _CRYPTO_LIBRARY == 'pycrypto': + if _RSA_CRYPTO_LIBRARY == 'pycrypto': valid_signature = tuf.pycrypto_keys.verify_signature(sig, method, public, data) else: - message = 'Unsupported "tuf.conf.CRYPTO_LIBRARY": '+\ - repr(_CRYPTO_LIBRARY)+'.' + message = 'Unsupported "tuf.conf.RSA_CRYPTO_LIBRARY": '+\ + repr(_RSA_CRYPTO_LIBRARY)+'.' raise tuf.Error(message) elif keytype == 'ed25519': public = binascii.unhexlify(public) - if _CRYPTO_LIBRARY == 'pynacl' and 'pynacl' in _available_crypto_libraries: + if _RSA_CRYPTO_LIBRARY == 'pynacl' and 'pynacl' in _available_crypto_libraries: valid_signature = tuf.ed25519_keys.verify_signature(public, method, sig, data, use_pynacl=True) diff --git a/tuf/pycrypto_keys.py b/tuf/pycrypto_keys.py index 5d1e1bc6..0244f371 100755 --- a/tuf/pycrypto_keys.py +++ b/tuf/pycrypto_keys.py @@ -6,25 +6,32 @@ Vladimir Diaz - March 7, 2013. + October 7, 2013. See LICENSE for licensing information. - The goal of this module is to support public-key cryptography using the RSA - algorithm. The RSA-related functions provided include generate(), - create_signature(), and verify_signature(). The create_encrypted_pem() and - create_from_encrypted_pem() functions are optional, and may be used save a - generated RSA key to a file. The 'PyCrypto' package used by 'rsa_key.py' + The goal of this module is to support public-key cryptography, RSA + algorithm, and the PyCrypto library. The RSA-related functions provided + include: + generate_rsa_public_and_private() + create_signature() + verify_signature() + + The optional functions include: + create_rsa_encrypted_pem() + create_rsa_public_and_private_from_encrypted_pem() + + These last two functions may be used save a + generated RSA key to a file. 'PyCrypto' (i.e., Crypto module) package used by 'rsa_key.py' generates the actual RSA keys and the functions listed above can be viewed - as an easy-to-use public interface. Additional functions contained here - include create_in_metadata_format() and create_from_metadata_format(). These - last two functions produce or use RSA keys compatible with the key structures - listed in TUF Metadata files. The generate() function returns a dictionary + as an easy-to-use public interface. + The generate() function returns a dictionary containing all the information needed of RSA keys, such as public and private= keys, keyIDs, and an idenfier. create_signature() and verify_signature() are supplemental functions used for generating RSA signatures and verifying them. + https://en.wikipedia.org/wiki/RSA_(algorithm) """ @@ -64,20 +71,21 @@ def generate_rsa_public_and_private(bits=_DEFAULT_RSA_KEY_BITS): """ - Generate public and private RSA keys, with modulus length 'bits'. - In addition, a keyid used as an identifier for RSA keys is generated. - The object returned conforms to 'tuf.formats.RSAKEY_SCHEMA' and as the form: - {'keytype': 'rsa', - 'keyid': keyid, - 'keyval': {'public': '-----BEGIN RSA PUBLIC KEY----- ...', - 'private': '-----BEGIN RSA PRIVATE KEY----- ...'}} - - The public and private keys are in PEM format and stored as strings. + Generate public and private RSA keys with modulus length 'bits'. + The public and private keys returned conform to 'tuf.formats.PEMRSA_SCHEMA' + and have the form: + '-----BEGIN RSA PUBLIC KEY----- ...' - Although the crytography library called sets a 1024-bit minimum key size, - generate() enforces a minimum key size of 2048 bits. If 'bits' is - unspecified, a 3072-bit RSA key is generated, which is the key size - recommended by TUF. + or + + '-----BEGIN RSA PRIVATE KEY----- ...' + + The public and private keys are returned as strings in PEM format. + + Although PyCrypto sets a 1024-bit minimum key size, + generate_rsa_public_and_private() enforces a minimum key size of 2048 bits. + If 'bits' is unspecified, a 3072-bit RSA key is generated, which is the key + size recommended by TUF. >>> public, private = generate_rsa_public_and_private(2048) >>> tuf.formats.PEMRSA_SCHEMA.matches(public) @@ -93,7 +101,7 @@ def generate_rsa_public_and_private(bits=_DEFAULT_RSA_KEY_BITS): ValueError, if an exception occurs after calling the RSA key generation routine. 'bits' must be a multiple of 256. The 'ValueError' exception is - raised by the key generation function of the cryptography library called. + raised by the PyCrypto key generation function. tuf.FormatError, if 'bits' does not contain the correct format. @@ -120,11 +128,11 @@ def generate_rsa_public_and_private(bits=_DEFAULT_RSA_KEY_BITS): # Extract the public & private halves of the RSA key and generate their # PEM-formatted representations. The dictionary returned contains the # private and public RSA keys in PEM format, as strings. - private_key = rsa_key_object.exportKey(format='PEM') + private = rsa_key_object.exportKey(format='PEM') rsa_pubkey = rsa_key_object.publickey() - public_key = rsa_pubkey.exportKey(format='PEM') + public = rsa_pubkey.exportKey(format='PEM') - return public_key, private_key + return public, private diff --git a/tuf/repo/keystore.py b/tuf/repo/keystore.py index 8f5492e5..ce0c1f4d 100755 --- a/tuf/repo/keystore.py +++ b/tuf/repo/keystore.py @@ -34,7 +34,6 @@ algorithm. User passwords are strengthened with PBKDF2, currently set to 100,000 passphrase iterations. The previous evpy implementation used 1,000 iterations. - """ import os @@ -68,7 +67,7 @@ # the AES algorithm to perform cipher block operations on them. import Crypto.Util.Counter -import tuf.rsa_key +import tuf.keys import tuf.util import tuf.conf @@ -104,6 +103,9 @@ # https://en.wikipedia.org/wiki/PBKDF2 _PBKDF2_ITERATIONS = tuf.conf.PBKDF2_ITERATIONS +# +_SUPPORTED_KEY_TYPES = ['rsa', 'ed25519'] + # A user password is read and a derived key generated. The derived key returned # by the key derivation function (PBKDF2) is saved in '_derived_keys', along # with the salt and iterations used, which has the form: @@ -159,7 +161,6 @@ def add_rsakey(rsakey_dict, password, keyid=None): None. - """ # Does 'rsakey_dict' have the correct format? @@ -235,7 +236,6 @@ def load_keystore_from_keyfiles(directory_name, keyids, passwords): A list containing the keyids of the loaded keys. - """ # Does 'directory_name' have the correct format? @@ -286,11 +286,11 @@ def load_keystore_from_keyfiles(directory_name, keyids, passwords): # Create the key based on its key type. RSA keys currently # supported. - if keydata['keytype'] == 'rsa': + if keydata['keytype'] in _SUPPORTED_KEY_TYPES: # 'keydata' is stored in KEY_SCHEMA format. Call # create_from_metadata_format() to get the key in RSAKEY_SCHEMA # format, which is the format expected by 'add_rsakey()'. - rsa_key = tuf.rsa_key.create_from_metadata_format(keydata) + rsa_key = tuf.keys.create_from_metadata_format(keydata) # Ensure the keyid for 'rsa_key' is one of the keys specified in # 'keyids'. If not, do not load the key. @@ -343,7 +343,6 @@ def save_keystore_to_keyfiles(directory_name): None. - """ # Does 'directory_name' have the correct format? @@ -365,9 +364,11 @@ def save_keystore_to_keyfiles(directory_name): file_object = open(basefilename, 'w') # Determine the appropriate format to save the key based on its key type. - if key['keytype'] == 'rsa': + if key['keytype'] in _SUPPORTED_KEY_TYPES: + keytype = key['keytype'] + keyval = key['keyval'] key_metadata_format = \ - tuf.rsa_key.create_in_metadata_format(key['keyval'], private=True) + tuf.keys.create_in_metadata_format(keytype, keyval, private=True) else: logger.warn('The keystore has a key with an unrecognized key type.') continue @@ -402,7 +403,6 @@ def clear_keystore(): None. - """ _keystore.clear() @@ -442,7 +442,6 @@ def change_password(keyid, old_password, new_password): None. - """ # Does 'keyid' have the correct format? @@ -506,7 +505,6 @@ def get_key(keyid): The key belonging to 'keyid' (e.g., RSA key). - """ # Does 'keyid' have the correct format? @@ -530,7 +528,6 @@ def _generate_derived_key(password, salt=None, iterations=None): Derivation Function (PBKDF2). PyCrypto's PBKDF2 implementation is currently used. 'salt' may be specified so that a previous derived key may be regenerated. - """ if salt is None: @@ -584,7 +581,6 @@ def _encrypt(key_data, derived_key_information): 'iterations': '...'} 'tuf.CryptoError' raised if the encryption fails. - """ # Generate a random initialization vector (IV). The 'iv' is treated as the @@ -650,7 +646,6 @@ def _decrypt(file_contents, password): The corresponding decryption routine for _encrypt(). 'tuf.CryptoError' raised if the decryption fails. - """ # Extract the salt, iterations, hmac, initialization vector, and ciphertext diff --git a/tuf/repo/signercli.py b/tuf/repo/signercli.py index 5c36377b..8d5c4b03 100755 --- a/tuf/repo/signercli.py +++ b/tuf/repo/signercli.py @@ -46,7 +46,6 @@ See the parse_options() function for the full list of supported options. - """ import os @@ -92,7 +91,6 @@ def _get_password(prompt='Password: ', confirm=False): is True, the user is asked to enter the previously entered password once again. If they match, the password is returned to the caller. - """ while True: @@ -131,7 +129,6 @@ def _get_metadata_directory(): returned to the caller. 'tuf.FormatError' is raised if the directory is not properly formatted, and 'tuf.Error' if it does not exist. - """ metadata_directory = _prompt('\nEnter the metadata directory: ', str) @@ -151,7 +148,6 @@ def _list_keyids(keystore_directory, metadata_directory): It is assumed the directory arguments exist and have been validated by the caller. The keyids are listed without the '.key' extension, along with their associated roles. - """ # Determine the 'root.txt' filename. This metadata file is needed @@ -233,7 +229,6 @@ def _get_keyids(keystore_directory): key files are stored in encrypted form, the user is asked to enter the password that was used to encrypt the key file. - """ # The keyids list containing the keys loaded. @@ -288,7 +283,6 @@ def _get_all_config_keyids(config_filepath, keystore_directory): loaded_keyids = {'root': [1233d3d, 598djdks, ..], 'release': [sdfsd323, sdsd9090s, ..] ...} - """ # Save the 'load_keystore_from_keyfiles' function call. @@ -338,7 +332,6 @@ def _get_role_config_keyids(config_filepath, keystore_directory, role): tuf.Error, if the required keys could not be loaded. - """ # Save the 'load_keystore_from_keyfiles' function call. @@ -409,7 +402,6 @@ def _get_metadata_version(metadata_filename): 'metadata_filename' does not exist, return a version value of 1. Raise 'tuf.RepositoryError' if 'metadata_filename' cannot be read or validated. - """ # If 'metadata_filename' does not exist on the repository, this means @@ -442,7 +434,6 @@ def _get_metadata_expiration(): tuf.RepositoryError, if the entered expiration date is invalid. - """ message = '\nCurrent time: '+tuf.formats.format_time(time.time())+'.\n'+\ @@ -487,7 +478,6 @@ def change_password(keystore_directory): None. - """ # Save the 'load_keystore_from_keyfiles' function call. @@ -563,7 +553,6 @@ def generate_rsa_key(keystore_directory): None. - """ # Save a reference to the generate_and_save_rsa_key() function. @@ -612,7 +601,6 @@ def list_signing_keys(keystore_directory): None. - """ # Verify the 'keystore_directory' argument. @@ -654,7 +642,6 @@ def dump_key(keystore_directory): None. - """ # Save the 'load_keystore_from_keyfiles' function call. @@ -704,8 +691,10 @@ def dump_key(keystore_directory): # Retrieve the key metadata according to the keytype. if key['keytype'] == 'rsa': - key_metadata = tuf.rsa_key.create_in_metadata_format(key['keyval'], - private=show_private) + keytype = key['keytype'] + keyval = key['keyval'] + key_metadata = tuf.keys.create_in_metadata_format(keytype, keyval, + private=show_private) else: message = 'The keystore contains an invalid key type.' raise tuf.RepositoryError(message) @@ -737,7 +726,6 @@ def make_root_metadata(keystore_directory): None. - """ # Verify the 'keystore_directory' argument. @@ -804,7 +792,6 @@ def make_targets_metadata(keystore_directory): None. - """ # Verify the 'keystore_directory' argument. @@ -886,7 +873,6 @@ def make_release_metadata(keystore_directory): None. - """ # Verify the 'keystore_directory' argument. @@ -951,7 +937,6 @@ def make_timestamp_metadata(keystore_directory): None. - """ # Verify the 'keystore_directory' argument. @@ -1017,7 +1002,6 @@ def sign_metadata_file(keystore_directory): None. - """ # Verify the 'keystore_directory' argument. @@ -1084,7 +1068,6 @@ def make_delegation(keystore_directory): None. - """ # Verify the 'keystore_directory' argument. @@ -1154,7 +1137,6 @@ def _load_parent_role(metadata_directory, keystore_directory, targets_roles): list of known targets roles and asked to enter the parent role to load. Ensure the parent role is loaded properly and return a string containing the parent role's full rolename and a list of keyids belonging to the parent. - """ # 'load_key' is a reference to the 'load_keystore_from_keyfiles function'. @@ -1210,7 +1192,6 @@ def _get_delegated_role(keystore_directory, metadata_directory): a list of keyids available in the keystore and asked to enter the keyid belonging to the delegated role. Return a string containing the delegated role's full rolename and its keyids. - """ # Retrieve the delegated rolename from the user (e.g., 'role1'). @@ -1240,7 +1221,6 @@ def _make_delegated_metadata(metadata_directory, delegated_targets, role. Determine the target files from the paths in 'delegated_targets' and the other information needed to generate the targets metadata file for delegated_role'. Return the delegated paths to the caller. - """ repository_directory, junk = os.path.split(metadata_directory) @@ -1336,7 +1316,6 @@ def _update_parent_metadata(metadata_directory, delegated_role, metadata file is updated with the key and role information belonging to the newly added delegated role. Finally, the metadata file is signed and written to the metadata directory. - """ # According to the specification, the 'paths' and 'path_hash_prefixes' @@ -1376,8 +1355,9 @@ def _update_parent_metadata(metadata_directory, delegated_role, # Retrieve the key belonging to 'delegated_keyid' from the keystore. role_key = tuf.repo.keystore.get_key(delegated_keyid) if role_key['keytype'] == 'rsa': + keytype = role_key['keytype'] keyval = role_key['keyval'] - keys[delegated_keyid] = tuf.rsa_key.create_in_metadata_format(keyval) + keys[delegated_keyid] = tuf.keys.create_in_metadata_format(keytype, keyval) else: message = 'Invalid keytype encountered: '+delegated_keyid+'\n' raise tuf.RepositoryError(message) @@ -1450,7 +1430,6 @@ def process_option(options): None. - """ # Determine which option was chosen and call its corresponding @@ -1506,7 +1485,6 @@ def parse_options(): The options object returned by the parser's parse_args() method. - """ usage = 'usage: %prog [option] ' diff --git a/tuf/repo/signerlib.py b/tuf/repo/signerlib.py index 6144b8e4..5df17363 100755 --- a/tuf/repo/signerlib.py +++ b/tuf/repo/signerlib.py @@ -286,7 +286,9 @@ def generate_root_metadata(config_filepath, version): # This appears to be a new keyid. Let's generate the key for it. if keyid not in keydict: if key['keytype'] in ['rsa', 'ed25519']: - keydict[keyid] = tuf.keys.create_in_metadata_format(key['keyval']) + keytype = key['keytype'] + keyval = key['keyval'] + keydict[keyid] = tuf.keys.create_in_metadata_format(keytype, keyval) # This is not a recognized key. Raise an exception. else: raise tuf.Error('Unsupported keytype: '+keyid) diff --git a/tuf/tests/repository_setup.py b/tuf/tests/repository_setup.py index 066ca61e..778621d8 100644 --- a/tuf/tests/repository_setup.py +++ b/tuf/tests/repository_setup.py @@ -14,7 +14,6 @@ To provide a quick repository structure to be used in conjunction with test modules like test_updater.py for instance. - """ import os @@ -24,7 +23,6 @@ import tempfile import tuf.formats -import tuf.rsa_key as rsa_key import tuf.repo.keystore as keystore import tuf.repo.signerlib as signerlib import tuf.repo.signercli as signercli @@ -276,7 +274,6 @@ def create_repositories(): A dictionary of all repositories, with the following keys: (main_repository, client_repository, server_repository) - """ # Ensure the keyids for the required roles are loaded. Role keyids are diff --git a/tuf/tests/unittest_toolbox.py b/tuf/tests/unittest_toolbox.py index ff23af49..785fffcb 100644 --- a/tuf/tests/unittest_toolbox.py +++ b/tuf/tests/unittest_toolbox.py @@ -15,7 +15,6 @@ Provides an array of various methods for unit testing. Use it instead of actual unittest module. This module builds on unittest module. Specifically, Modified_TestCase is a derived class from unittest.TestCase. - """ import os @@ -27,7 +26,7 @@ import string import ConfigParser -import tuf.rsa_key as rsa_key +import tuf.keys import tuf.repo.keystore as keystore # Modify the number of iterations (from the higher default count) so the unit @@ -100,7 +99,6 @@ def setUp(): random_string(length=7): Generate a 'length' long string of random characters. - """ # List of all top level roles. @@ -222,6 +220,7 @@ def make_temp_config_file(self, suffix='', directory=None, config_dict={}, expir dictionary in it using ConfigParser. It then returns the temp file path, dictionary tuple. """ + config = ConfigParser.RawConfigParser() if not config_dict: # Using the fact that empty sequences are false. @@ -288,7 +287,6 @@ def make_temp_directory_with_data_files(self, _current_dir=None, Returns: ('/tmp/tmp_dir_Test_random/', [targets/tmp_random1.txt, targets/tmp_random2.txt, targets/more_targets/tmp_random3.txt]) - """ if not _current_dir: @@ -385,7 +383,7 @@ def generate_rsakey(): 'private': '-----BEGIN RSA PRIVATE KEY----- ...'}} """ - rsakey = rsa_key.generate() + rsakey = tuf.keys.generate_rsa_key() keyid = rsakey['keyid'] Modified_TestCase.rsa_keyids.append(keyid) password = Modified_TestCase.random_string() From ae2e7489b143da620457ebf5fd0a89cd716243e6 Mon Sep 17 00:00:00 2001 From: vladdd Date: Thu, 10 Oct 2013 13:56:35 -0400 Subject: [PATCH 41/95] Add new keytype schema in formats.py --- tuf/formats.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/tuf/formats.py b/tuf/formats.py index 6a58737f..8a51cb60 100755 --- a/tuf/formats.py +++ b/tuf/formats.py @@ -153,6 +153,10 @@ public=SCHEMA.AnyString(), private=SCHEMA.AnyString()) +# Supported TUF key types. +KEYTYPE_SCHEMA = SCHEMA.OneOf( + [SCHEMA.String('rsa'), SCHEMA.String('ed25519')]) + # A generic key. All TUF keys should be saved to metadata files in this format. KEY_SCHEMA = SCHEMA.Object( object_name='key', @@ -164,7 +168,7 @@ # Supported key types: 'rsa', 'ed25519'. ANYKEY_SCHEMA = SCHEMA.Object( object_name='anykey', - keytype=SCHEMA.OneOf([SCHEMA.String('rsa'), SCHEMA.String('ed25519')]), + keytype=KEYTYPE_SCHEMA, keyid=KEYID_SCHEMA, keyval=KEYVAL_SCHEMA) From a091b1f84bf86fe75f1a1b641eecbbaedbabd2bf Mon Sep 17 00:00:00 2001 From: vladdd Date: Thu, 10 Oct 2013 14:01:55 -0400 Subject: [PATCH 42/95] Add test_keys.py and update keys.py --- tests/unit/test_keys.py | 198 ++++++++++++++++++++++++++++ tests/unit/test_rsa_key.py | 258 ------------------------------------- tuf/keys.py | 7 +- 3 files changed, 203 insertions(+), 260 deletions(-) create mode 100755 tests/unit/test_keys.py delete mode 100755 tests/unit/test_rsa_key.py diff --git a/tests/unit/test_keys.py b/tests/unit/test_keys.py new file mode 100755 index 00000000..3297680f --- /dev/null +++ b/tests/unit/test_keys.py @@ -0,0 +1,198 @@ +""" + + test_keys.py + + + Konstantin Andrianov + + + April 24, 2012. + + + See LICENSE for licensing information. + + + Test cases for test_keys.py. + + + I'm using 'global rsakey_dict' - there is no harm in doing so since + in order to modify the global variable in any method, python requires + explicit indication to modify i.e. declaring 'global' in each method + that modifies the global variable 'rsakey_dict'. +""" + +import unittest +import logging + +import tuf +import tuf.log +import tuf.formats +import tuf.keys + +logger = logging.getLogger('tuf.test_keys') + +KEYS = tuf.keys +FORMAT_ERROR_MSG = 'tuf.FormatError was raised! Check object\'s format.' +DATA = 'SOME DATA REQUIRING AUTHENTICITY.' + + +rsakey_dict = KEYS.generate_rsa_key() +temp_key_info_vals = rsakey_dict.values() +temp_key_vals = rsakey_dict['keyval'].values() + + +class TestKeys(unittest.TestCase): + def setUp(self): + rsakey_dict['keytype']=temp_key_info_vals[0] + rsakey_dict['keyid']=temp_key_info_vals[1] + rsakey_dict['keyval']=temp_key_info_vals[2] + rsakey_dict['keyval']['public']=temp_key_vals[0] + rsakey_dict['keyval']['private']=temp_key_vals[1] + + + def test_generate_rsa_key(self): + _rsakey_dict = KEYS.generate_rsa_key() + + # Check if the format of the object returned by generate() corresponds + # to RSAKEY_SCHEMA format. + self.assertEqual(None, tuf.formats.RSAKEY_SCHEMA.check_match(_rsakey_dict), + FORMAT_ERROR_MSG) + + # Passing a bit value that is <2048 to generate() - should raise + # 'tuf.FormatError'. + self.assertRaises(tuf.FormatError, KEYS.generate_rsa_key, 555) + + # Passing a string instead of integer for a bit value. + self.assertRaises(tuf.FormatError, KEYS.generate_rsa_key, 'bits') + + # NOTE if random bit value >=2048 (not 4096) is passed generate(bits) + # does not raise any errors and returns a valid key. + self.assertTrue(tuf.formats.RSAKEY_SCHEMA.matches(KEYS.generate_rsa_key(2048))) + self.assertTrue(tuf.formats.RSAKEY_SCHEMA.matches(KEYS.generate_rsa_key(4096))) + + def test_create_in_metadata_format(self): + keyvalue = rsakey_dict['keyval'] + keytype = rsakey_dict['keytype'] + key_meta = KEYS.create_in_metadata_format(keytype, keyvalue) + + # Check if the format of the object returned by this function corresponds + # to KEY_SCHEMA format. + self.assertEqual(None, + tuf.formats.KEY_SCHEMA.check_match(key_meta), + FORMAT_ERROR_MSG) + key_meta = KEYS.create_in_metadata_format(keytype, keyvalue, private=True) + + # Check if the format of the object returned by this function corresponds + # to KEY_SCHEMA format. + self.assertEqual(None, tuf.formats.KEY_SCHEMA.check_match(key_meta), + FORMAT_ERROR_MSG) + + # Supplying a 'bad' keyvalue. + self.assertRaises(tuf.FormatError, KEYS.create_in_metadata_format, + 'bad_keytype', keyvalue) + + del keyvalue['public'] + self.assertRaises(tuf.FormatError, KEYS.create_in_metadata_format, + keytype, keyvalue) + + + + def test_create_from_metadata_format(self): + # Reconfiguring rsakey_dict to conform to KEY_SCHEMA + # i.e. {keytype: 'rsa', keyval: {public: pub_key, private: priv_key}} + #keyid = rsakey_dict['keyid'] + del rsakey_dict['keyid'] + + rsakey_dict_from_meta = KEYS.create_from_metadata_format(rsakey_dict) + + # Check if the format of the object returned by this function corresponds + # to RSAKEY_SCHEMA format. + self.assertEqual(None, + tuf.formats.RSAKEY_SCHEMA.check_match(rsakey_dict_from_meta), + FORMAT_ERROR_MSG) + + # Supplying a wrong number of arguments. + self.assertRaises(TypeError, KEYS.create_from_metadata_format) + args = (rsakey_dict, rsakey_dict) + self.assertRaises(TypeError, KEYS.create_from_metadata_format, *args) + + # Supplying a malformed argument to the function - should get FormatError + del rsakey_dict['keyval'] + self.assertRaises(tuf.FormatError, KEYS.create_from_metadata_format, + rsakey_dict) + + + def test_helper_get_keyid(self): + keytype = rsakey_dict['keytype'] + keyvalue = rsakey_dict['keyval'] + + # Check format of 'keytype'. + self.assertEqual(None, tuf.formats.KEYTYPE_SCHEMA.check_match(keytype), + FORMAT_ERROR_MSG) + + # Check format of 'keyvalue'. + self.assertEqual(None, tuf.formats.KEYVAL_SCHEMA.check_match(keyvalue), + FORMAT_ERROR_MSG) + + keyid = KEYS._get_keyid(keytype, keyvalue) + + # Check format of 'keyid' - the output of '_get_keyid()' function. + self.assertEqual(None, tuf.formats.KEYID_SCHEMA.check_match(keyid), + FORMAT_ERROR_MSG) + + + def test_create_signature(self): + # Creating a signature for 'DATA'. + signature = KEYS.create_signature(rsakey_dict, DATA) + + # Check format of output. + self.assertEqual(None, + tuf.formats.SIGNATURE_SCHEMA.check_match(signature), + FORMAT_ERROR_MSG) + + # Removing private key from 'rsakey_dict' - should raise a TypeError. + rsakey_dict['keyval']['private'] = '' + + args = (rsakey_dict, DATA) + self.assertRaises(TypeError, KEYS.create_signature, *args) + + # Supplying an incorrect number of arguments. + self.assertRaises(TypeError, KEYS.create_signature) + + + def test_verify_signature(self): + # Creating a signature 'signature' of 'DATA' to be verified. + signature = KEYS.create_signature(rsakey_dict, DATA) + + # Verifying the 'signature' of 'DATA'. + verified = KEYS.verify_signature(rsakey_dict, signature, DATA) + self.assertTrue(verified, "Incorrect signature.") + + # Testing an invalid 'signature'. Same 'signature' is passed, with + # 'DATA' different than the original 'DATA' that was used + # in creating the 'signature'. Function should return 'False'. + + # Modifying 'DATA'. + _DATA = '1111'+DATA+'1111' + + # Verifying the 'signature' of modified '_DATA'. + verified = KEYS.verify_signature(rsakey_dict, signature, _DATA) + self.assertFalse(verified, + 'Returned \'True\' on an incorrect signature.') + + # Modifying 'signature' to pass an incorrect method since only + # 'PyCrypto-PKCS#1 PSS' + # is accepted. + signature['method'] = 'Biff' + + args = (rsakey_dict, signature, DATA) + self.assertRaises(tuf.UnknownMethodError, KEYS.verify_signature, *args) + + # Passing incorrect number of arguments. + self.assertRaises(TypeError, KEYS.verify_signature) + + + +# Run the unit tests. +if __name__ == '__main__': + unittest.main() diff --git a/tests/unit/test_rsa_key.py b/tests/unit/test_rsa_key.py deleted file mode 100755 index 881ee1d7..00000000 --- a/tests/unit/test_rsa_key.py +++ /dev/null @@ -1,258 +0,0 @@ -""" - - test_rsa_key.py - - - Konstantin Andrianov - - - April 24, 2012. - - - See LICENSE for licensing information. - - - Test cases for rsa_key.py. - - - I'm using 'global rsakey_dict' - there is no harm in doing so since - in order to modify the global variable in any method, python requires - explicit indication to modify i.e. declaring 'global' in each method - that modifies the global variable 'rsakey_dict'. - -""" - -import unittest -import logging - -import tuf -import tuf.log -import tuf.formats -import tuf.rsa_key - -logger = logging.getLogger('tuf.test_rsa_key') - -RSA_KEY = tuf.rsa_key -FORMAT_ERROR_MSG = 'tuf.FormatError was raised! Check object\'s format.' -DATA = 'SOME DATA REQUIRING AUTHENTICITY.' - - -rsakey_dict = RSA_KEY.generate() -temp_key_info_vals = rsakey_dict.values() -temp_key_vals = rsakey_dict['keyval'].values() - - -class TestRsa_key(unittest.TestCase): - def setUp(self): - rsakey_dict['keytype']=temp_key_info_vals[0] - rsakey_dict['keyid']=temp_key_info_vals[1] - rsakey_dict['keyval']=temp_key_info_vals[2] - rsakey_dict['keyval']['public']=temp_key_vals[0] - rsakey_dict['keyval']['private']=temp_key_vals[1] - - - def test_generate(self): - _rsakey_dict = RSA_KEY.generate() - - # Check if the format of the object returned by generate() corresponds - # to RSAKEY_SCHEMA format. - self.assertEqual(None, tuf.formats.RSAKEY_SCHEMA.check_match(_rsakey_dict), - FORMAT_ERROR_MSG) - - # Passing a bit value that is <2048 to generate() - should raise - # 'tuf.FormatError'. - self.assertRaises(tuf.FormatError, RSA_KEY.generate, 555) - - # Passing a string instead of integer for a bit value. - self.assertRaises(tuf.FormatError, RSA_KEY.generate, 'bits') - - # NOTE if random bit value >=2048 (not 4096) is passed generate(bits) - # does not raise any errors and returns a valid key. - self.assertTrue(tuf.formats.RSAKEY_SCHEMA.matches(RSA_KEY.generate(2048))) - self.assertTrue(tuf.formats.RSAKEY_SCHEMA.matches(RSA_KEY.generate(4096))) - - def test_create_in_metadata_format(self): - key_value = rsakey_dict['keyval'] - key_meta = RSA_KEY.create_in_metadata_format(key_value) - - # Check if the format of the object returned by this function corresponds - # to KEY_SCHEMA format. - self.assertEqual(None, - tuf.formats.KEY_SCHEMA.check_match(key_meta), - FORMAT_ERROR_MSG) - key_meta = RSA_KEY.create_in_metadata_format(key_value, private=True) - - # Check if the format of the object returned by this function corresponds - # to KEY_SCHEMA format. - self.assertEqual(None, tuf.formats.KEY_SCHEMA.check_match(key_meta), - FORMAT_ERROR_MSG) - - # Supplying a 'bad' key_value. - del key_value['public'] - self.assertRaises(tuf.FormatError, RSA_KEY.create_in_metadata_format, - key_value) - - - def test_create_from_metadata_format(self): - # Reconfiguring rsakey_dict to conform to KEY_SCHEMA - # i.e. {keytype: 'rsa', keyval: {public: pub_key, private: priv_key}} - #keyid = rsakey_dict['keyid'] - del rsakey_dict['keyid'] - - rsakey_dict_from_meta = RSA_KEY.create_from_metadata_format(rsakey_dict) - - # Check if the format of the object returned by this function corresponds - # to RSAKEY_SCHEMA format. - self.assertEqual(None, - tuf.formats.RSAKEY_SCHEMA.check_match(rsakey_dict_from_meta), - FORMAT_ERROR_MSG) - - # Supplying a wrong number of arguments. - self.assertRaises(TypeError, RSA_KEY.create_from_metadata_format) - args = (rsakey_dict, rsakey_dict) - self.assertRaises(TypeError, RSA_KEY.create_from_metadata_format, *args) - - # Supplying a malformed argument to the function - should get FormatError - del rsakey_dict['keyval'] - self.assertRaises(tuf.FormatError, RSA_KEY.create_from_metadata_format, - rsakey_dict) - - - def test_helper_get_keyid(self): - key_value = rsakey_dict['keyval'] - - # Check format of 'key_value'. - self.assertEqual(None, tuf.formats.KEYVAL_SCHEMA.check_match(key_value), - FORMAT_ERROR_MSG) - - keyid = RSA_KEY._get_keyid(key_value) - - # Check format of 'keyid' - the output of '_get_keyid()' function. - self.assertEqual(None, tuf.formats.KEYID_SCHEMA.check_match(keyid), - FORMAT_ERROR_MSG) - - - def test_createsignature(self): - # Creating a signature for 'DATA'. - signature = RSA_KEY.create_signature(rsakey_dict, DATA) - - # Check format of output. - self.assertEqual(None, - tuf.formats.SIGNATURE_SCHEMA.check_match(signature), - FORMAT_ERROR_MSG) - - # Removing private key from 'rsakey_dict' - should raise a TypeError. - rsakey_dict['keyval']['private'] = '' - - args = (rsakey_dict, DATA) - self.assertRaises(TypeError, RSA_KEY.create_signature, *args) - - # Supplying an incorrect number of arguments. - self.assertRaises(TypeError, RSA_KEY.create_signature) - - - def test_verify_signature(self): - # Creating a signature 'signature' of 'DATA' to be verified. - signature = RSA_KEY.create_signature(rsakey_dict, DATA) - - # Verifying the 'signature' of 'DATA'. - verified = RSA_KEY.verify_signature(rsakey_dict, signature, DATA) - self.assertTrue(verified, "Incorrect signature.") - - # Testing an invalid 'signature'. Same 'signature' is passed, with - # 'DATA' different than the original 'DATA' that was used - # in creating the 'signature'. Function should return 'False'. - - # Modifying 'DATA'. - _DATA = '1111'+DATA+'1111' - - # Verifying the 'signature' of modified '_DATA'. - verified = RSA_KEY.verify_signature(rsakey_dict, signature, _DATA) - self.assertFalse(verified, - 'Returned \'True\' on an incorrect signature.') - - # Modifying 'signature' to pass an incorrect method since only - # 'PyCrypto-PKCS#1 PSS' - # is accepted. - signature['method'] = 'Biff' - - args = (rsakey_dict, signature, DATA) - self.assertRaises(tuf.UnknownMethodError, RSA_KEY.verify_signature, *args) - - # Passing incorrect number of arguments. - self.assertRaises(TypeError,RSA_KEY.verify_signature) - - - def test_create_encrypted_pem(self): - passphrase = 'pw' - - # Check format of 'rsakey_dict'. - self.assertEqual(None, tuf.formats.RSAKEY_SCHEMA.check_match(rsakey_dict), - FORMAT_ERROR_MSG) - - # Check format of 'passphrase'. - self.assertEqual(None, tuf.formats.PASSWORD_SCHEMA.check_match(passphrase), - FORMAT_ERROR_MSG) - - # Generate the encrypted PEM string of 'rsakey_dict'. - pem_rsakey = tuf.rsa_key.create_encrypted_pem(rsakey_dict, passphrase) - - # Check for invalid arguments. - self.assertRaises(tuf.FormatError, - tuf.rsa_key.create_encrypted_pem, 'Biff', passphrase) - self.assertRaises(tuf.FormatError, - tuf.rsa_key.create_encrypted_pem, rsakey_dict, ['pw']) - - - - def test_create_from_encrypted_pem(self): - passphrase = 'pw' - - # Check format of 'rsakey_dict'. - self.assertEqual(None, tuf.formats.RSAKEY_SCHEMA.check_match(rsakey_dict), - FORMAT_ERROR_MSG) - - # Check format of 'passphrase'. - self.assertEqual(None, tuf.formats.PASSWORD_SCHEMA.check_match(passphrase), - FORMAT_ERROR_MSG) - - # Generate the encrypted PEM string of 'rsakey_dict'. - pem_rsakey = tuf.rsa_key.create_encrypted_pem(rsakey_dict, passphrase) - - # Decrypt 'pem_rsakey' and verify the decrypted object is properly - # formatted. - decrypted_rsakey = tuf.rsa_key.create_from_encrypted_pem(pem_rsakey, - passphrase) - self.assertEqual(None, tuf.formats.RSAKEY_SCHEMA.check_match(decrypted_rsakey), - FORMAT_ERROR_MSG) - - # Does 'decrypted_rsakey' match the original 'rsakey_dict'. - self.assertEqual(rsakey_dict, decrypted_rsakey) - - # Attempt decryption of 'pem_rsakey' using an incorrect passphrase. - self.assertRaises(tuf.CryptoError, - tuf.rsa_key.create_from_encrypted_pem, pem_rsakey, - 'bad_pw') - # Check for non-encrypted PEM string. create_from_encrypted_pem()/PyCrypto - # returns a tuf.formats.RSAKEY_SCHEMA object if PEM formatted string is - # not actually encrypted but still a valid PEM string. - non_encrypted_private_key = rsakey_dict['keyval']['private'] - decrypted_non_encrypted = tuf.rsa_key.create_from_encrypted_pem( - non_encrypted_private_key, passphrase) - self.assertEqual(None, tuf.formats.RSAKEY_SCHEMA.check_match( - decrypted_non_encrypted), FORMAT_ERROR_MSG) - - # Check for invalid arguments. - self.assertRaises(tuf.FormatError, - tuf.rsa_key.create_from_encrypted_pem, 123, passphrase) - self.assertRaises(tuf.FormatError, - tuf.rsa_key.create_from_encrypted_pem, pem_rsakey, ['pw']) - self.assertRaises(tuf.CryptoError, - tuf.rsa_key.create_from_encrypted_pem, 'invalid_pem', - passphrase) - - - -# Run the unit tests. -if __name__ == '__main__': - unittest.main() diff --git a/tuf/keys.py b/tuf/keys.py index 49c78a35..5f1062cf 100755 --- a/tuf/keys.py +++ b/tuf/keys.py @@ -326,10 +326,13 @@ def create_in_metadata_format(keytype, key_value, private=False): An 'KEY_SCHEMA' dictionary. """ - # Does 'key_value' have the correct format? - # This check will ensure 'key_value' has the appropriate number + # Does 'keytype' have the correct format? + # This check will ensure 'keytype' has the appropriate number # of objects and object types, and that all dict keys are properly named. # Raise 'tuf.FormatError' if the check fails. + tuf.formats.KEYTYPE_SCHEMA.check_match(keytype) + + # Does 'key_value' have the correct format? tuf.formats.KEYVAL_SCHEMA.check_match(key_value) if private is True and key_value['private']: From ac6dade0dccd3c000e1df94dbdb93d4a0bb5075b Mon Sep 17 00:00:00 2001 From: vladdd Date: Thu, 10 Oct 2013 14:56:56 -0400 Subject: [PATCH 43/95] Move test cases to test_pycrypto_keys.py --- tests/unit/test_pycrypto_keys.py | 121 +++++++++++++++++++++++++++++++ tuf/conf.py | 1 - 2 files changed, 121 insertions(+), 1 deletion(-) create mode 100755 tests/unit/test_pycrypto_keys.py diff --git a/tests/unit/test_pycrypto_keys.py b/tests/unit/test_pycrypto_keys.py new file mode 100755 index 00000000..451b5db4 --- /dev/null +++ b/tests/unit/test_pycrypto_keys.py @@ -0,0 +1,121 @@ +""" + + test_pycrypto_keys.py + + + Vladimir Diaz + + + October 10, 2013. + + + See LICENSE for licensing information. + + + Test cases for test_pycrypto_keys.py. +""" + +import unittest +import logging + +import tuf +import tuf.log +import tuf.formats +import tuf.pycrypto_keys + +logger = logging.getLogger('tuf.test_pycrypto_keys') + +FORMAT_ERROR_MSG = 'tuf.FormatError raised. Check object\'s format.' +public_rsa, private_rsa = tuf.pycrypto_keys.generate_rsa_public_and_private() + + +class TestPycrypto_keys(unittest.TestCase): + def setUp(self): + pass + + + def test_generate_rsa_public_and_private(self): + pass + + + def test_create_signature(self): + pass + + + def test_verify_signature(self): + pass + + + def test_create_rsa_encrypted_pem(self): + passphrase = 'pw' + + # Check format of 'public_rsa'. + self.assertEqual(None, tuf.formats.PEMRSA_SCHEMA.check_match(public_rsa), + FORMAT_ERROR_MSG) + + # Check format of 'passphrase'. + self.assertEqual(None, tuf.formats.PASSWORD_SCHEMA.check_match(passphrase), + FORMAT_ERROR_MSG) + + # Generate the encrypted PEM string of 'public_rsa'. + pem_rsakey = tuf.pycrypto_keys.create_rsa_encrypted_pem(private_rsa, passphrase) + + # Check format of 'pem_rsakey'. + self.assertEqual(None, tuf.formats.PEMRSA_SCHEMA.check_match(pem_rsakey), + FORMAT_ERROR_MSG) + + # Check for invalid arguments. + self.assertRaises(tuf.FormatError, + tuf.pycrypto_keys.create_rsa_encrypted_pem, 1, passphrase) + self.assertRaises(tuf.FormatError, + tuf.pycrypto_keys.create_rsa_encrypted_pem, private_rsa, ['pw']) + + + + def test_create_rsa_public_and_private_from_encrypted_pem(self): + passphrase = 'pw' + + # Generate the encrypted PEM string of 'public_rsa'. + pem_rsakey = tuf.pycrypto_keys.create_rsa_encrypted_pem(private_rsa, passphrase) + + # Check format of 'passphrase'. + self.assertEqual(None, tuf.formats.PASSWORD_SCHEMA.check_match(passphrase), + FORMAT_ERROR_MSG) + + # Decrypt 'pem_rsakey' and verify the decrypted object is properly + # formatted. + decrypted_rsakey = tuf.pycrypto_keys.create_rsa_public_and_private_from_encrypted_pem(pem_rsakey, + passphrase) + self.assertEqual(None, tuf.formats.RSAKEY_SCHEMA.check_match(decrypted_rsakey), + FORMAT_ERROR_MSG) + + # Does 'decrypted_rsakey' match the original 'rsakey_dict'. + self.assertEqual(rsakey_dict, decrypted_rsakey) + + # Attempt decryption of 'pem_rsakey' using an incorrect passphrase. + self.assertRaises(tuf.CryptoError, + tuf.pycrypto_keys.create_rsa_public_and_private_from_encrypted_pem, pem_rsakey, + 'bad_pw') + # Check for non-encrypted PEM string. create_rsa_public_and_private_from_encrypted_pem()/PyCrypto + # returns a tuf.formats.RSAKEY_SCHEMA object if PEM formatted string is + # not actually encrypted but still a valid PEM string. + non_encrypted_private_key = rsakey_dict['keyval']['private'] + decrypted_non_encrypted = tuf.pycrypto_keys.create_rsa_public_and_private_from_encrypted_pem( + non_encrypted_private_key, passphrase) + self.assertEqual(None, tuf.formats.RSAKEY_SCHEMA.check_match( + decrypted_non_encrypted), FORMAT_ERROR_MSG) + + # Check for invalid arguments. + self.assertRaises(tuf.FormatError, + tuf.pycrypto_keys.create_rsa_public_and_private_from_encrypted_pem, 123, passphrase) + self.assertRaises(tuf.FormatError, + tuf.pycrypto_keys.create_rsa_public_and_private_from_encrypted_pem, pem_rsakey, ['pw']) + self.assertRaises(tuf.CryptoError, + tuf.pycrypto_keys.create_rsa_public_and_private_from_encrypted_pem, 'invalid_pem', + passphrase) + + + +# Run the unit tests. +if __name__ == '__main__': + unittest.main() diff --git a/tuf/conf.py b/tuf/conf.py index fb62141b..f25d2075 100755 --- a/tuf/conf.py +++ b/tuf/conf.py @@ -71,4 +71,3 @@ # Supported ed25519 cryptography libraries: ['pynacl', 'ed25519'] ED25519_CRYPTO_LIBRARY = 'pynacl' - From 8d33d7244dd28e4ce180a7fa78d391ee94e91254 Mon Sep 17 00:00:00 2001 From: vladdd Date: Fri, 11 Oct 2013 08:38:26 -0400 Subject: [PATCH 44/95] Update object schema names in formats.py Modify the object name of a schema so that FormatError error messages are clearer about which schema is expected. --- tuf/formats.py | 40 ++++++++++++++++++++-------------------- 1 file changed, 20 insertions(+), 20 deletions(-) diff --git a/tuf/formats.py b/tuf/formats.py index 8a51cb60..481768b5 100755 --- a/tuf/formats.py +++ b/tuf/formats.py @@ -107,7 +107,7 @@ # A dictionary holding version information. VERSION_SCHEMA = SCHEMA.Object( - object_name='version', + object_name='VERSION_SCHEMA', major=SCHEMA.Integer(lo=0), minor=SCHEMA.Integer(lo=0), fix=SCHEMA.Integer(lo=0)) @@ -149,7 +149,7 @@ # key identifier ('rsa', 233df889cb). For RSA keys, the key value is a pair of # public and private keys in PEM Format stored as strings. KEYVAL_SCHEMA = SCHEMA.Object( - object_name='keyval', + object_name='KEYVAL_SCHEMA', public=SCHEMA.AnyString(), private=SCHEMA.AnyString()) @@ -159,7 +159,7 @@ # A generic key. All TUF keys should be saved to metadata files in this format. KEY_SCHEMA = SCHEMA.Object( - object_name='key', + object_name='KEY_SCHEMA', keytype=SCHEMA.AnyString(), keyval=KEYVAL_SCHEMA) @@ -167,21 +167,21 @@ # one of the supported key types. # Supported key types: 'rsa', 'ed25519'. ANYKEY_SCHEMA = SCHEMA.Object( - object_name='anykey', + object_name='ANYKEY_SCHEMA', keytype=KEYTYPE_SCHEMA, keyid=KEYID_SCHEMA, keyval=KEYVAL_SCHEMA) # An RSA key. RSAKEY_SCHEMA = SCHEMA.Object( - object_name='rsakey', + object_name='RSAKEY_SCHEMA', keytype=SCHEMA.String('rsa'), keyid=KEYID_SCHEMA, keyval=KEYVAL_SCHEMA) # An ed25519 key. ED25519KEY_SCHEMA = SCHEMA.Object( - object_name='ed25519key', + object_name='ED25519KEY_SCHEMA', keytype=SCHEMA.String('ed25519'), keyid=KEYID_SCHEMA, keyval=KEYVAL_SCHEMA) @@ -190,7 +190,7 @@ # This schema allows the storage of multiple hashes for the same file # (e.g., sha256 and sha512 may be computed for the same file and stored). FILEINFO_SCHEMA = SCHEMA.Object( - object_name='fileinfo', + object_name='FILEINFO_SCHEMA', length=LENGTH_SCHEMA, hashes=HASHDICT_SCHEMA, custom=SCHEMA.Optional(SCHEMA.Object())) @@ -203,7 +203,7 @@ # A dict holding a target file. TARGETFILE_SCHEMA = SCHEMA.Object( - object_name='targetfile', + object_name='TARGETFILE_SCHEMA', filepath=RELPATH_SCHEMA, fileinfo=FILEINFO_SCHEMA) TARGETFILES_SCHEMA = SCHEMA.ListOf(TARGETFILE_SCHEMA) @@ -217,7 +217,7 @@ # one can imagine that maybe a key wants to sign multiple times with different # signature methods. SIGNATURE_SCHEMA = SCHEMA.Object( - object_name='signature', + object_name='SIGNATURE_SCHEMA', keyid=KEYID_SCHEMA, method=SIG_METHOD_SCHEMA, sig=HEX_SCHEMA) @@ -228,7 +228,7 @@ # valid? This SCHEMA holds this information. See 'sig.py' for # more information. SIGNATURESTATUS_SCHEMA = SCHEMA.Object( - object_name='signaturestatus', + object_name='SIGNATURESTATUS_SCHEMA', threshold=SCHEMA.Integer(), good_sigs=SCHEMA.ListOf(KEYID_SCHEMA), bad_sigs=SCHEMA.ListOf(KEYID_SCHEMA), @@ -238,7 +238,7 @@ # A signable object. Holds the signing role and its associated signatures. SIGNABLE_SCHEMA = SCHEMA.Object( - object_name='signable', + object_name='SIGNABLE_SCHEMA', signed=SCHEMA.Any(), signatures=SCHEMA.ListOf(SIGNATURE_SCHEMA)) @@ -262,7 +262,7 @@ # 'remote_directory' entries. See 'tuf/pushtools/pushtoolslib.py' and # 'tuf/pushtools/push.py'. SCPCONFIG_SCHEMA = SCHEMA.Object( - object_name='scp_config', + object_name='SCPCONFIG_SCHEMA', general=SCHEMA.Object( object_name='[general]', transfer_module=SCHEMA.String('scp'), @@ -282,7 +282,7 @@ # 'backup_directory' entries. # see 'tuf/pushtools/pushtoolslib.py' and 'tuf/pushtools/receive/receive.py' RECEIVECONFIG_SCHEMA = SCHEMA.Object( - object_name='receive_config', + object_name='RECEIVECONFIG_SCHEMA', general=SCHEMA.Object( object_name='[general]', pushroots=SCHEMA.ListOf(PATH_SCHEMA), @@ -299,7 +299,7 @@ # Role object in {'keyids': [keydids..], 'name': 'ABC', 'threshold': 1, # 'paths':[filepaths..]} # format. ROLE_SCHEMA = SCHEMA.Object( - object_name='role', + object_name='ROLE_SCHEMA', keyids=SCHEMA.ListOf(KEYID_SCHEMA), name=SCHEMA.Optional(ROLENAME_SCHEMA), threshold=THRESHOLD_SCHEMA, @@ -317,7 +317,7 @@ # The root: indicates root keys and top-level roles. ROOT_SCHEMA = SCHEMA.Object( - object_name='root', + object_name='ROOT_SCHEMA', _type=SCHEMA.String('Root'), version=METADATAVERSION_SCHEMA, expires=TIME_SCHEMA, @@ -326,7 +326,7 @@ # Targets. Indicates targets and delegates target paths to other roles. TARGETS_SCHEMA = SCHEMA.Object( - object_name='targets', + object_name='TARGETS_SCHEMA', _type=SCHEMA.String('Targets'), version=METADATAVERSION_SCHEMA, expires=TIME_SCHEMA, @@ -337,7 +337,7 @@ # A Release: indicates the latest versions of all metadata (except timestamp). RELEASE_SCHEMA = SCHEMA.Object( - object_name='release', + object_name='RELEASE_SCHEMA', _type=SCHEMA.String('Release'), version=METADATAVERSION_SCHEMA, expires=TIME_SCHEMA, @@ -345,7 +345,7 @@ # A Timestamp: indicates the latest version of the release file. TIMESTAMP_SCHEMA = SCHEMA.Object( - object_name='timestamp', + object_name='TIMESTAMP_SCHEMA', _type=SCHEMA.String('Timestamp'), version=METADATAVERSION_SCHEMA, expires=TIME_SCHEMA, @@ -354,7 +354,7 @@ # 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( - object_name='mirror', + object_name='MIRROR_SCHEMA', url_prefix=URL_SCHEMA, metadata_path=RELPATH_SCHEMA, targets_path=RELPATH_SCHEMA, @@ -372,7 +372,7 @@ # A Mirrorlist: indicates all the live mirrors, and what documents they # serve. MIRRORLIST_SCHEMA = SCHEMA.Object( - object_name='mirrorlist', + object_name='MIRRORLIST_SCHEMA', _type=SCHEMA.String('Mirrors'), version=METADATAVERSION_SCHEMA, expires=TIME_SCHEMA, From 2f61272c2804d61a752e6c62a3fa4cf3175bbdd7 Mon Sep 17 00:00:00 2001 From: vladdd Date: Fri, 11 Oct 2013 09:07:25 -0400 Subject: [PATCH 45/95] Update test cases moved over to test_pycrypto_keys.py test cases updated with configurable crypto changes. --- tests/unit/test_pycrypto_keys.py | 63 +++++++++++++++++++------------- 1 file changed, 37 insertions(+), 26 deletions(-) diff --git a/tests/unit/test_pycrypto_keys.py b/tests/unit/test_pycrypto_keys.py index 451b5db4..d00ca309 100755 --- a/tests/unit/test_pycrypto_keys.py +++ b/tests/unit/test_pycrypto_keys.py @@ -21,12 +21,12 @@ import tuf import tuf.log import tuf.formats -import tuf.pycrypto_keys +import tuf.pycrypto_keys as pycrypto logger = logging.getLogger('tuf.test_pycrypto_keys') FORMAT_ERROR_MSG = 'tuf.FormatError raised. Check object\'s format.' -public_rsa, private_rsa = tuf.pycrypto_keys.generate_rsa_public_and_private() +public_rsa, private_rsa = pycrypto.generate_rsa_public_and_private() class TestPycrypto_keys(unittest.TestCase): @@ -58,7 +58,7 @@ def test_create_rsa_encrypted_pem(self): FORMAT_ERROR_MSG) # Generate the encrypted PEM string of 'public_rsa'. - pem_rsakey = tuf.pycrypto_keys.create_rsa_encrypted_pem(private_rsa, passphrase) + pem_rsakey = pycrypto.create_rsa_encrypted_pem(private_rsa, passphrase) # Check format of 'pem_rsakey'. self.assertEqual(None, tuf.formats.PEMRSA_SCHEMA.check_match(pem_rsakey), @@ -66,17 +66,16 @@ def test_create_rsa_encrypted_pem(self): # Check for invalid arguments. self.assertRaises(tuf.FormatError, - tuf.pycrypto_keys.create_rsa_encrypted_pem, 1, passphrase) + pycrypto.create_rsa_encrypted_pem, 1, passphrase) self.assertRaises(tuf.FormatError, - tuf.pycrypto_keys.create_rsa_encrypted_pem, private_rsa, ['pw']) - + pycrypto.create_rsa_encrypted_pem, private_rsa, ['pw']) def test_create_rsa_public_and_private_from_encrypted_pem(self): passphrase = 'pw' - # Generate the encrypted PEM string of 'public_rsa'. - pem_rsakey = tuf.pycrypto_keys.create_rsa_encrypted_pem(private_rsa, passphrase) + # Generate the encrypted PEM string of 'private_rsa'. + pem_rsakey = pycrypto.create_rsa_encrypted_pem(private_rsa, passphrase) # Check format of 'passphrase'. self.assertEqual(None, tuf.formats.PASSWORD_SCHEMA.check_match(passphrase), @@ -84,35 +83,47 @@ def test_create_rsa_public_and_private_from_encrypted_pem(self): # Decrypt 'pem_rsakey' and verify the decrypted object is properly # formatted. - decrypted_rsakey = tuf.pycrypto_keys.create_rsa_public_and_private_from_encrypted_pem(pem_rsakey, + public_decrypted, private_decrypted = \ + pycrypto.create_rsa_public_and_private_from_encrypted_pem(pem_rsakey, passphrase) - self.assertEqual(None, tuf.formats.RSAKEY_SCHEMA.check_match(decrypted_rsakey), + self.assertEqual(None, + tuf.formats.PEMRSA_SCHEMA.check_match(public_decrypted), + FORMAT_ERROR_MSG) + + self.assertEqual(None, + tuf.formats.PEMRSA_SCHEMA.check_match(private_decrypted), FORMAT_ERROR_MSG) - # Does 'decrypted_rsakey' match the original 'rsakey_dict'. - self.assertEqual(rsakey_dict, decrypted_rsakey) + # Does 'public_decrypted' and 'private_decrypted' match the originals? + self.assertEqual(public_rsa, public_decrypted) + self.assertEqual(private_rsa, private_decrypted) # Attempt decryption of 'pem_rsakey' using an incorrect passphrase. self.assertRaises(tuf.CryptoError, - tuf.pycrypto_keys.create_rsa_public_and_private_from_encrypted_pem, pem_rsakey, - 'bad_pw') - # Check for non-encrypted PEM string. create_rsa_public_and_private_from_encrypted_pem()/PyCrypto - # returns a tuf.formats.RSAKEY_SCHEMA object if PEM formatted string is - # not actually encrypted but still a valid PEM string. - non_encrypted_private_key = rsakey_dict['keyval']['private'] - decrypted_non_encrypted = tuf.pycrypto_keys.create_rsa_public_and_private_from_encrypted_pem( - non_encrypted_private_key, passphrase) - self.assertEqual(None, tuf.formats.RSAKEY_SCHEMA.check_match( - decrypted_non_encrypted), FORMAT_ERROR_MSG) + pycrypto.create_rsa_public_and_private_from_encrypted_pem, + pem_rsakey, 'bad_pw') + + # Check for non-encrypted PEM strings. + # create_rsa_public_and_private_from_encrypted_pem() + # returns a tuple of tuf.formats.PEMRSA_SCHEMA objects if the PEM formatted + # string is not actually encrypted but still a valid PEM string. + pub, priv = pycrypto.create_rsa_public_and_private_from_encrypted_pem( + private_rsa, passphrase) + self.assertEqual(None, tuf.formats.PEMRSA_SCHEMA.check_match(pub), + FORMAT_ERROR_MSG) + self.assertEqual(None, tuf.formats.PEMRSA_SCHEMA.check_match(priv), + FORMAT_ERROR_MSG) # Check for invalid arguments. self.assertRaises(tuf.FormatError, - tuf.pycrypto_keys.create_rsa_public_and_private_from_encrypted_pem, 123, passphrase) + pycrypto.create_rsa_public_and_private_from_encrypted_pem, + 123, passphrase) self.assertRaises(tuf.FormatError, - tuf.pycrypto_keys.create_rsa_public_and_private_from_encrypted_pem, pem_rsakey, ['pw']) + pycrypto.create_rsa_public_and_private_from_encrypted_pem, + pem_rsakey, ['pw']) self.assertRaises(tuf.CryptoError, - tuf.pycrypto_keys.create_rsa_public_and_private_from_encrypted_pem, 'invalid_pem', - passphrase) + pycrypto.create_rsa_public_and_private_from_encrypted_pem, + 'invalid_pem', passphrase) From 81a15147b2a4402d2a08ab9fca335ba8e018f578 Mon Sep 17 00:00:00 2001 From: vladdd Date: Fri, 11 Oct 2013 10:15:09 -0400 Subject: [PATCH 46/95] Add remaining test cases in test_pycrypto_keys.py --- tests/unit/test_pycrypto_keys.py | 74 ++++++++++++++++++++++++++++++-- tuf/pycrypto_keys.py | 14 +++++- 2 files changed, 83 insertions(+), 5 deletions(-) diff --git a/tests/unit/test_pycrypto_keys.py b/tests/unit/test_pycrypto_keys.py index d00ca309..fe854e67 100755 --- a/tests/unit/test_pycrypto_keys.py +++ b/tests/unit/test_pycrypto_keys.py @@ -25,8 +25,8 @@ logger = logging.getLogger('tuf.test_pycrypto_keys') -FORMAT_ERROR_MSG = 'tuf.FormatError raised. Check object\'s format.' public_rsa, private_rsa = pycrypto.generate_rsa_public_and_private() +FORMAT_ERROR_MSG = 'tuf.FormatError raised. Check object\'s format.' class TestPycrypto_keys(unittest.TestCase): @@ -35,18 +35,83 @@ def setUp(self): def test_generate_rsa_public_and_private(self): - pass + pub, priv = pycrypto.generate_rsa_public_and_private() + + # Check format of 'pub' and 'priv'. + self.assertEqual(None, tuf.formats.PEMRSA_SCHEMA.check_match(pub), + FORMAT_ERROR_MSG) + self.assertEqual(None, tuf.formats.PEMRSA_SCHEMA.check_match(priv), + FORMAT_ERROR_MSG) + # Check for invalid bits argument. bit >= 2048 and a multiple of 256. + self.assertRaises(tuf.FormatError, + pycrypto.generate_rsa_public_and_private, 1024) + + self.assertRaises(ValueError, + pycrypto.generate_rsa_public_and_private, 2049) + + self.assertRaises(tuf.FormatError, + pycrypto.generate_rsa_public_and_private, '2048') + def test_create_signature(self): - pass + global private_rsa + data = 'The quick brown fox jumps over the lazy dog' + signature, method = pycrypto.create_signature(private_rsa, data) + + # Verify format of returned values. + self.assertNotEqual(None, signature) + self.assertEqual(None, tuf.formats.NAME_SCHEMA.check_match(method), + FORMAT_ERROR_MSG) + self.assertEqual('PyCrypto-PKCS#1 PSS', method) + + # Check for improperly formatted argument. + self.assertRaises(tuf.FormatError, + pycrypto.create_signature, 123, data) + + # Check for invalid 'data'. + self.assertRaises(tuf.CryptoError, + pycrypto.create_signature, private_rsa, 123) def test_verify_signature(self): - pass + global public_rsa + global private_rsa + data = 'The quick brown fox jumps over the lazy dog' + signature, method = pycrypto.create_signature(private_rsa, data) + + valid_signature = pycrypto.verify_signature(signature, method, public_rsa, + data) + self.assertEqual(True, valid_signature) + + # Check for improperly formatted arguments. + self.assertRaises(tuf.FormatError, pycrypto.verify_signature, signature, + 123, public_rsa, data) + + self.assertRaises(tuf.FormatError, pycrypto.verify_signature, signature, + method, 123, data) + + # Check for invalid signature and data. + self.assertRaises(tuf.CryptoError, pycrypto.verify_signature, 123, method, + public_rsa, data) + + self.assertRaises(tuf.CryptoError, pycrypto.verify_signature, signature, + method, public_rsa, 123) + + self.assertEqual(False, pycrypto.verify_signature(signature, method, + public_rsa, 'mismatched data')) + + mismatched_signature, method = pycrypto.create_signature(private_rsa, + 'mismatched data') + + self.assertEqual(False, pycrypto.verify_signature(mismatched_signature, + method, public_rsa, data)) + def test_create_rsa_encrypted_pem(self): + global public_rsa + global private_rsa passphrase = 'pw' # Check format of 'public_rsa'. @@ -72,6 +137,7 @@ def test_create_rsa_encrypted_pem(self): def test_create_rsa_public_and_private_from_encrypted_pem(self): + global private_rsa passphrase = 'pw' # Generate the encrypted PEM string of 'private_rsa'. diff --git a/tuf/pycrypto_keys.py b/tuf/pycrypto_keys.py index 0244f371..1dc49473 100755 --- a/tuf/pycrypto_keys.py +++ b/tuf/pycrypto_keys.py @@ -182,6 +182,11 @@ def create_signature(private_key, data): A (signature, method) tuple, where """ + + # Does 'private_key' have the correct format? + # This check will ensure 'private_key' conforms to 'tuf.formats.PEMRSA_SCHEMA'. + # Raise 'tuf.FormatError' if the check fails. + tuf.formats.PEMRSA_SCHEMA.check_match(private_key) # Signing the 'data' object requires a private key. # The 'PyCrypto-PKCS#1 PSS' (i.e., PyCrypto module) signing method is the @@ -260,6 +265,14 @@ def verify_signature(signature, signature_method, public_key, data): Boolean. True if the signature is valid, False otherwise. """ + + # Does 'public_key' have the correct format? + # This check will ensure 'public_key' conforms to 'tuf.formats.PEMRSA_SCHEMA'. + # Raise 'tuf.FormatError' if the check fails. + tuf.formats.PEMRSA_SCHEMA.check_match(public_key) + + # Does 'signature_method' have the correct format? + tuf.formats.NAME_SCHEMA.check_match(signature_method) # Using the public key belonging to 'rsakey_dict' # (i.e., rsakey_dict['keyval']['public']), verify whether 'signature' @@ -276,7 +289,6 @@ def verify_signature(signature, signature_method, public_key, data): rsa_key_object = Crypto.PublicKey.RSA.importKey(public_key) pkcs1_pss_verifier = Crypto.Signature.PKCS1_PSS.new(rsa_key_object) sha256_object = Crypto.Hash.SHA256.new(data) - valid_signature = pkcs1_pss_verifier.verify(sha256_object, signature) except (ValueError, IndexError, TypeError), e: message = 'The RSA signature could not be verified.' From 8372100de8fe1e25f51a75955fa07eeebe580b9d Mon Sep 17 00:00:00 2001 From: vladdd Date: Fri, 11 Oct 2013 12:03:21 -0400 Subject: [PATCH 47/95] Add a LengthString schema type to schema.py Update docstring whitespace. --- tuf/schema.py | 65 +++++++++++++++++++++++++++++++++------------------ 1 file changed, 42 insertions(+), 23 deletions(-) diff --git a/tuf/schema.py b/tuf/schema.py index acd50b63..60ad5349 100755 --- a/tuf/schema.py +++ b/tuf/schema.py @@ -38,7 +38,6 @@ criteria. See 'tuf.formats.py' and the rest of this module for extensive examples. Anything related to the checking of TUF objects and their formats can be found in 'formats.py'. - """ @@ -55,7 +54,6 @@ class Schema: that are encodable in JSON. 'Schema' is the base class for the other classes defined in this module. All derived classes should implement check_match(). - """ def matches(self, object): @@ -64,7 +62,6 @@ def matches(self, object): Return True if 'object' matches this schema, False if it doesn't. If the caller wishes to signal an error on a failed match, check_match() should be called, which will raise a 'tuf.FormatError' exception. - """ try: @@ -82,7 +79,6 @@ def check_match(self, object): implement check_match(). If 'object' matches the schema, check_match() should simply return. If 'object' does not match the schema, 'tuf.FormatError' should be raised. - """ raise NotImplementedError() @@ -110,7 +106,6 @@ class Any(Schema): True >>> schema.matches([1, 'list']) True - """ def __init__(self): @@ -143,7 +138,6 @@ class String(Schema): True >>> schema.matches('Not hi') False - """ def __init__(self, string): @@ -187,7 +181,6 @@ class AnyString(Schema): True >>> schema.matches({}) False - """ def __init__(self): @@ -202,6 +195,48 @@ def check_match(self, object): +class LengthString(Schema): + """ + + Matches any string of a specified length. The argument object + must be a string. At instantiation, the string length is set + and any future comparisons are checked against this internal + string value length. + + Supported methods include + matches(): returns a Boolean result. + check_match(): raises 'tuf.FormatError' on a mismatch. + + + + >>> schema = LengthString(5) + >>> schema.matches('Hello') + True + >>> schema.matches('Hi') + False + """ + + def __init__(self, length): + if isinstance(length, bool) or not isinstance(length, (int, long)): + # We need to check for bool as a special case, since bool + # is for historical reasons a subtype of int. + raise tuf.FormatError('Got '+repr(length)+' instead of an integer.') + + self._string_length = length + + + def check_match(self, object): + if not isinstance(object, basestring): + raise tuf.FormatError('Expected a string but got '+repr(object)) + + if len(object) != self._string_length: + raise tuf.FormatError('Expected a string of length '+ + repr(self._string_length)) + + + + + class OneOf(Schema): """ @@ -229,7 +264,6 @@ class OneOf(Schema): True >>> schema.matches(['Hi']) False - """ def __init__(self, alternatives): @@ -275,7 +309,6 @@ class AllOf(Schema): False >>> schema.matches('a') True - """ def __init__(self, required_schemas): @@ -314,7 +347,6 @@ class Boolean(Schema): True >>> schema.matches(11) False - """ def __init__(self): @@ -367,7 +399,6 @@ class ListOf(Schema): True >>> schema.matches([3]*11) False - """ def __init__(self, schema, min_count=0, max_count=sys.maxint, list_name='list'): @@ -380,7 +411,6 @@ def __init__(self, schema, min_count=0, max_count=sys.maxint, list_name='list'): min_count: The minimum number of sub-schema in 'schema'. max_count: The maximum number of sub-schema in 'schema'. list_name: A string identifier for the ListOf object. - """ if not isinstance(schema, Schema): @@ -443,7 +473,6 @@ class Integer(Schema): True >>> Integer(lo=10, hi=30).matches(5) False - """ def __init__(self, lo= -sys.maxint, hi=sys.maxint): @@ -454,7 +483,6 @@ def __init__(self, lo= -sys.maxint, hi=sys.maxint): lo: The minimum value the int object argument can be. hi: The maximum value the int object argument can be. - """ self._lo = lo @@ -502,7 +530,6 @@ class DictOf(Schema): False >>> schema.matches({'a': ['x', 'y'], 'e' : ['', ''], 'd' : ['a', 'b']}) False - """ def __init__(self, key_schema, value_schema): @@ -513,7 +540,6 @@ def __init__(self, key_schema, value_schema): key_schema: The dictionary's key. value_schema: The dictionary's value. - """ if not isinstance(key_schema, Schema): @@ -564,7 +590,6 @@ class Optional(Schema): False >>> schema.matches({'k1': 'X'}) True - """ def __init__(self, schema): @@ -604,7 +629,6 @@ class Object(Schema): False >>> schema.matches({'a':'ZYYY'}) False - """ def __init__(self, object_name='object', **required): @@ -616,7 +640,6 @@ def __init__(self, object_name='object', **required): object_name: A string identifier for the object argument. A variable number of keyword arguments is accepted. - """ # Ensure valid arguments. @@ -713,7 +736,6 @@ class Struct(Schema): False >>> schema.matches(['X', 3, 'A']) False - """ def __init__(self, sub_schemas, optional_schemas=[], allow_more=False, @@ -727,7 +749,6 @@ def __init__(self, sub_schemas, optional_schemas=[], allow_more=False, optional_schemas: The optional list of schemas. allow_more: Specifies that an optional list of types is allowed. struct_name: A string identifier for the Struct object. - """ # Ensure each item of the list contains the expected object type. @@ -792,7 +813,6 @@ class RegularExpression(Schema): False >>> schema.matches([33, 'Hello']) False - """ def __init__(self, pattern=None, modifiers=0, re_object=None, re_name=None): @@ -805,7 +825,6 @@ def __init__(self, pattern=None, modifiers=0, re_object=None, re_name=None): modifiers: Flags to use when compiling the pattern. re_object: A compiled regular expression object. re_name: Identifier for the regular expression object. - """ if not isinstance(pattern, basestring): From b831016abf64d250d29e98e164ff154d3b545d34 Mon Sep 17 00:00:00 2001 From: vladdd Date: Fri, 11 Oct 2013 12:06:20 -0400 Subject: [PATCH 48/95] Add ed25519 schemas to formats.py New ed25519 schemas to validate public and seed keys, and signatures. --- tuf/formats.py | 16 +++++++++++++--- 1 file changed, 13 insertions(+), 3 deletions(-) diff --git a/tuf/formats.py b/tuf/formats.py index 481768b5..9fe6f470 100755 --- a/tuf/formats.py +++ b/tuf/formats.py @@ -157,7 +157,8 @@ KEYTYPE_SCHEMA = SCHEMA.OneOf( [SCHEMA.String('rsa'), SCHEMA.String('ed25519')]) -# A generic key. All TUF keys should be saved to metadata files in this format. +# A generic TUF key. All TUF keys should be saved to metadata files in this +# format. KEY_SCHEMA = SCHEMA.Object( object_name='KEY_SCHEMA', keytype=SCHEMA.AnyString(), @@ -172,14 +173,23 @@ keyid=KEYID_SCHEMA, keyval=KEYVAL_SCHEMA) -# An RSA key. +# An RSA TUF key. RSAKEY_SCHEMA = SCHEMA.Object( object_name='RSAKEY_SCHEMA', keytype=SCHEMA.String('rsa'), keyid=KEYID_SCHEMA, keyval=KEYVAL_SCHEMA) -# An ed25519 key. +# An ED25519 raw public key, which must be 32 bytes. +ED25519PUBLIC_SCHEMA = SCHEMA.LengthString(32) + +# An ED25519 raw seed key, which must be 32 bytes. +ED25519SEED_SCHEMA = SCHEMA.LengthString(32) + +# An ED25519 raw signature, which must be 64 bytes. +ED25519SIGNATURE_SCHEMA = SCHEMA.LengthString(64) + +# An ed25519 TUF key. ED25519KEY_SCHEMA = SCHEMA.Object( object_name='ED25519KEY_SCHEMA', keytype=SCHEMA.String('ed25519'), From f76dfd4f4acb93bff5406113ce16c73a4829dcca Mon Sep 17 00:00:00 2001 From: vladdd Date: Fri, 11 Oct 2013 12:20:18 -0400 Subject: [PATCH 49/95] Validate arguments and update doctests in ed2519_keys.py Validate arguments using the newly added ed25519 schemas to formats.py. --- tuf/ed25519_keys.py | 74 ++++++++++++++++++++++++++++++++++----------- 1 file changed, 56 insertions(+), 18 deletions(-) diff --git a/tuf/ed25519_keys.py b/tuf/ed25519_keys.py index 2aaf0c13..c72acb27 100755 --- a/tuf/ed25519_keys.py +++ b/tuf/ed25519_keys.py @@ -106,15 +106,15 @@ def generate_public_and_private(use_pynacl=False): 32-byte value. Public keys are also 32 bytes. >>> public, private = generate_public_and_private(use_pynacl=False) - >>> len(public) - 32 - >>> len(private) - 32 + >>> tuf.formats.ED25519PUBLIC_SCHEMA.matches(public) + True + >>> tuf.formats.ED25519SEED_SCHEMA.matches(private) + True >>> public, private = generate_public_and_private(use_pynacl=True) - >>> len(public) - 32 - >>> len(private) - 32 + >>> tuf.formats.ED25519PUBLIC_SCHEMA.matches(public) + True + >>> tuf.formats.ED25519SEED_SCHEMA.matches(private) + True use_pynacl: @@ -123,17 +123,24 @@ def generate_public_and_private(use_pynacl=False): (much slower). + tuf.FormatError, if 'use_pynacl' is not a Boolean. + NotImplementedError, if a randomness source is not found. The ed25519 keys are generated by first creating a random 32-byte value - 'sk' with os.urandom() and then calling ed25519's ed25519.25519.publickey(sk) - or PyNaCl's nacl.signing.SigningKey(). + 'sk' with os.urandom() and then calling ed25519's + ed25519.25519.publickey(sk) or PyNaCl's nacl.signing.SigningKey(). A dictionary containing the ed25519 keys and other identifying information. Conforms to 'tuf.formats.ED25519KEY_SCHEMA'. """ + + # Does 'use_pynacl' have the correct format? + # This check will ensure 'use_pynacl' conforms to 'tuf.formats.TOGGLE_SCHEMA'. + # Raise 'tuf.FormatError' if the check fails. + tuf.formats.TOGGLE_SCHEMA.check_match(use_pynacl) # Generate ed25519's seed key by calling os.urandom(). The random bytes # returned should be suitable for cryptographic use and is OS-specific. @@ -178,14 +185,16 @@ def create_signature(public_key, private_key, data, use_pynacl=False): >>> public, private = generate_public_and_private(use_pynacl=False) >>> data = 'The quick brown fox jumps over the lazy dog' - >>> signature, method = create_signature(public, private, data, use_pynacl=False) - >>> len(signature) - 64 + >>> signature, method = \ + create_signature(public, private, data, use_pynacl=False) + >>> tuf.formats.ED25519SIGNATURE_SCHEMA.matches(signature) + True >>> method == 'ed25519-python' True - >>> signature, method = create_signature(public, private, data, use_pynacl=True) - >>> len(signature) - 64 + >>> signature, method = \ + create_signature(public, private, data, use_pynacl=True) + >>> tuf.formats.ED25519SIGNATURE_SCHEMA.matches(signature) + True >>> method == 'ed25519-pynacl' True @@ -221,6 +230,18 @@ def create_signature(public_key, private_key, data, use_pynacl=False): stored in the dictionary returned. """ + # Does 'public_key' have the correct format? + # This check will ensure 'public_key' conforms to + # 'tuf.formats.ED25519PUBLIC_SCHEMA', which must have length 32 bytes. + # Raise 'tuf.FormatError' if the check fails. + tuf.formats.ED25519PUBLIC_SCHEMA.check_match(public_key) + + # Is 'private_key' properly formatted? + tuf.formats.ED25519SEED_SCHEMA.check_match(private_key) + + # Is 'use_pynacl' properly formatted? + tuf.formats.TOGGLE_SCHEMA.check_match(use_pynacl) + # Signing the 'data' object requires a seed and public key. # 'ed25519.ed25519.py' generates the actual 64-byte signature in pure Python. # nacl.signing.SigningKey.sign() generates the signature if 'use_pynacl' @@ -279,13 +300,15 @@ def verify_signature(public_key, method, signature, data, use_pynacl=False): >>> public, private = generate_public_and_private(use_pynacl=False) >>> data = 'The quick brown fox jumps over the lazy dog' - >>> signature, method = create_signature(public, private, data, use_pynacl=False) + >>> signature, method = \ + create_signature(public, private, data, use_pynacl=False) >>> verify_signature(public, method, signature, data, use_pynacl=False) True >>> verify_signature(public, method, signature, data, use_pynacl=True) True >>> bad_data = 'The sly brown fox jumps over the lazy dog' - >>> bad_signature, method = create_signature(public, private, bad_data, use_pynacl=False) + >>> bad_signature, method = \ + create_signature(public, private, bad_data, use_pynacl=False) >>> verify_signature(public, method, bad_signature, data, use_pynacl=False) False @@ -328,6 +351,21 @@ def verify_signature(public_key, method, signature, data, use_pynacl=False): Boolean. True if the signature is valid, False otherwise. """ + + # Does 'public_key' have the correct format? + # This check will ensure 'public_key' conforms to + # 'tuf.formats.ED25519PUBLIC_SCHEMA', which must have length 32 bytes. + # Raise 'tuf.FormatError' if the check fails. + tuf.formats.ED25519PUBLIC_SCHEMA.check_match(public_key) + + # Is 'method' properly formatted? + tuf.formats.NAME_SCHEMA.check_match(method) + + # Is 'signature' properly formatted? + tuf.formats.ED25519SIGNATURE_SCHEMA.check_match(signature) + + # Is 'use_pynacl' properly formatted? + tuf.formats.TOGGLE_SCHEMA.check_match(use_pynacl) # Using the public key belonging to 'ed25519_key_dict' # (i.e., ed25519_key_dict['keyval']['public']), verify whether 'signature' From 7e948f342c86b413a668d2bde594ca7dd7655811 Mon Sep 17 00:00:00 2001 From: vladdd Date: Fri, 11 Oct 2013 13:01:46 -0400 Subject: [PATCH 50/95] Add initial test_ed25519_keys.py test_verify_signature() incomplete. --- tests/unit/test_ed25519_keys.py | 117 ++++++++++++++++++++++++++++++++ 1 file changed, 117 insertions(+) create mode 100755 tests/unit/test_ed25519_keys.py diff --git a/tests/unit/test_ed25519_keys.py b/tests/unit/test_ed25519_keys.py new file mode 100755 index 00000000..9a03605e --- /dev/null +++ b/tests/unit/test_ed25519_keys.py @@ -0,0 +1,117 @@ +""" + + test_ed25519_keys.py + + + Vladimir Diaz + + + October 11, 2013. + + + See LICENSE for licensing information. + + + Test cases for test_ed25519_keys.py. +""" + +import unittest +import logging + +import tuf +import tuf.log +import tuf.formats +import tuf.ed25519_keys as ed25519 + +logger = logging.getLogger('tuf.test_ed25519_keys') + +public, private = ed25519.generate_public_and_private() +FORMAT_ERROR_MSG = 'tuf.FormatError raised. Check object\'s format.' + + +class TestEd25519_keys(unittest.TestCase): + def setUp(self): + pass + + + def test_generate_public_and_private(self): + pub, priv = ed25519.generate_public_and_private() + + # Check format of 'pub' and 'priv'. + self.assertEqual(True, tuf.formats.ED25519PUBLIC_SCHEMA.matches(pub)) + self.assertEqual(True, tuf.formats.ED25519SEED_SCHEMA.matches(priv)) + + # Check for invalid argument. + self.assertRaises(tuf.FormatError, + ed25519.generate_public_and_private, 'True') + + self.assertRaises(tuf.FormatError, + ed25519.generate_public_and_private, 2048) + + + def test_create_signature(self): + global public + global private + data = 'The quick brown fox jumps over the lazy dog' + signature, method = ed25519.create_signature(public, private, data) + + # Verify format of returned values. + self.assertEqual(True, + tuf.formats.ED25519SIGNATURE_SCHEMA.matches(signature)) + + self.assertEqual(True, tuf.formats.NAME_SCHEMA.matches(method)) + self.assertEqual('ed25519-python', method) + + # Check for improperly formatted argument. + self.assertRaises(tuf.FormatError, + ed25519.create_signature, 123, private, data) + + self.assertRaises(tuf.FormatError, + ed25519.create_signature, public, 123, data) + + # Check for invalid 'data'. + self.assertRaises(tuf.CryptoError, + ed25519.create_signature, public, private, 123) + + + def test_verify_signature(self): + global public + global private + data = 'The quick brown fox jumps over the lazy dog' + signature, method = ed25519.create_signature(public, private, data) + + valid_signature = ed25519.verify_signature(public, method, signature, data) + self.assertEqual(True, valid_signature) + + # Check for improperly formatted arguments. + self.assertRaises(tuf.FormatError, ed25519.verify_signature, 123, method, + signature, data) + + self.assertRaises(tuf.FormatError, ed25519.verify_signature, public, 123, + signature, data) + + self.assertRaises(tuf.FormatError, ed25519.verify_signature, method, + '123', data) + + + # Check for invalid signature and data. + self.assertRaises(tuf.CryptoError, ed25519.verify_signature, public, method, + public_rsa, data) + + self.assertRaises(tuf.CryptoError, ed25519.verify_signature, signature, + method, public_rsa, 123) + + self.assertEqual(False, ed25519.verify_signature(public, method, signature, + 'mismatched data')) + + mismatched_signature, method = ed25519.create_signature(private_rsa, + 'mismatched data') + + self.assertEqual(False, ed25519.verify_signature(mismatched_signature, + method, public_rsa, data)) + + + +# Run the unit tests. +if __name__ == '__main__': + unittest.main() From 7e593963a98a56cc354092dcb851ddfc9122d9cf Mon Sep 17 00:00:00 2001 From: vladdd Date: Mon, 14 Oct 2013 08:41:38 -0400 Subject: [PATCH 51/95] Complete test_verify_signature() in test_ed25519_keys.py --- tests/unit/test_ed25519_keys.py | 38 +++++++++++++++++++-------------- 1 file changed, 22 insertions(+), 16 deletions(-) diff --git a/tests/unit/test_ed25519_keys.py b/tests/unit/test_ed25519_keys.py index 9a03605e..bdda7823 100755 --- a/tests/unit/test_ed25519_keys.py +++ b/tests/unit/test_ed25519_keys.py @@ -87,28 +87,34 @@ def test_verify_signature(self): self.assertRaises(tuf.FormatError, ed25519.verify_signature, 123, method, signature, data) + # Signature method improperly formatted. self.assertRaises(tuf.FormatError, ed25519.verify_signature, public, 123, signature, data) - - self.assertRaises(tuf.FormatError, ed25519.verify_signature, method, - '123', data) - + + # Signature not a string. + self.assertRaises(tuf.FormatError, ed25519.verify_signature, public, method, + 123, data) + + # Invalid signature length, which must be exactly 64 bytes.. + self.assertRaises(tuf.FormatError, ed25519.verify_signature, public, method, + 'bad_signature', data) # Check for invalid signature and data. - self.assertRaises(tuf.CryptoError, ed25519.verify_signature, public, method, - public_rsa, data) + # Mismatched data. + self.assertEqual(False, ed25519.verify_signature(public, method, + signature, '123')) + + # Mismatched signature. + bad_signature = 'a'*64 + self.assertEqual(False, ed25519.verify_signature(public, method, + bad_signature, data)) - self.assertRaises(tuf.CryptoError, ed25519.verify_signature, signature, - method, public_rsa, 123) + # Generated signature created with different data. + new_signature, method = ed25519.create_signature(public, private, + 'mismatched data') - self.assertEqual(False, ed25519.verify_signature(public, method, signature, - 'mismatched data')) - - mismatched_signature, method = ed25519.create_signature(private_rsa, - 'mismatched data') - - self.assertEqual(False, ed25519.verify_signature(mismatched_signature, - method, public_rsa, data)) + self.assertEqual(False, ed25519.verify_signature(public, method, + new_signature, data)) From 2244b6cabcbe1c80faf38f66d90555b73fce725c Mon Sep 17 00:00:00 2001 From: vladdd Date: Wed, 16 Oct 2013 12:46:57 -0400 Subject: [PATCH 52/95] Merge changes following Monzur's review of ed25519_key.py Updates to docstrings and comments. --- tuf/ed25519_keys.py | 236 ++++++++++++++++++++++---------------------- 1 file changed, 116 insertions(+), 120 deletions(-) diff --git a/tuf/ed25519_keys.py b/tuf/ed25519_keys.py index c72acb27..7b1fdb27 100755 --- a/tuf/ed25519_keys.py +++ b/tuf/ed25519_keys.py @@ -17,15 +17,19 @@ signatures (64 bytes) and small public keys (32 bytes). http://ed25519.cr.yp.to/ - 'tuf/ed25519.py' calls 'ed25519/ed25519.py', which is the pure Python - implementation of ed25519 that has been optimized for a faster runtime. - The Python reference implementation is concise but very slow. + 'tuf/ed25519_keys.py' calls 'ed25519/ed25519.py', which is the pure Python + implementation of ed25519 optimized for a faster runtime. + The Python reference implementation is concise, but very slow (verifying + signatures takes ~9 seconds on an Intel core 2 duo @ 2.2 ghz x 2). The + optimized version can verify signatures in ~2 seconds. + http://ed25519.cr.yp.to/software.html https://github.com/pyca/ed25519 Optionally, ed25519 cryptographic operations may be executed by PyNaCl, which - is a Python binding to the NaCl library and is faster than the pure - python implementation. PyNaCl relies on the libsodium C library. + is a Python binding to the NaCl library and is faster than the pure python + implementation. Verifying signatures can take approximately 0.0009 seconds. + PyNaCl relies on the libsodium C library. https://github.com/pyca/pynacl https://github.com/jedisct1/libsodium @@ -33,57 +37,57 @@ The ed25519-related functions included here are generate(), create_signature() and verify_signature(). The 'ed25519' and PyNaCl (i.e., 'nacl') modules used - by ed25519_key.py generate the actual ed25519 keys and the functions listed - above can be viewed as an easy-to-use public interface. Additional functions - contained here include create_in_metadata_format() and - create_from_metadata_format(). These last two functions produce or use - ed25519 keys compatible with the key structures listed in TUF Metadata files. - The generate() function returns a dictionary containing all the information - needed of ed25519 keys, such as public/private keys and a keyID identifier. - create_signature() and verify_signature() are supplemental functions used for - generating ed25519 signatures and verifying them. + by ed25519_keys.py generate the actual ed25519 keys and the functions listed + above can be viewed as an easy-to-use public interface. """ -# Help with Python 3 compatability, where the print statement is a function, an +# 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 -# Required for hexadecimal conversions. Signatures and public/private keys are -# hexlified. +# 'binascii' required for hexadecimal conversions. Signatures and +# public/private keys are hexlified. import binascii -# Generate OS-specific randomness (os.urandom) suitable for cryptographic use. +# 'os' required to generate OS-specific randomness (os.urandom) suitable for +# cryptographic use. # http://docs.python.org/2/library/os.html#miscellaneous-functions import os -# Import the python implementation of the ed25519 algorithm that is provided by -# the author. Note: This implementation is very slow and does not include -# protection against side-channel attacks according to the author. Verifying -# signatures can take approximately 9 seconds on a intel core 2 duo @ -# 2.2 ghz x 2). Optionally, the PyNaCl module may be used to speed up ed25519 -# cryptographic operations. -# http://ed25519.cr.yp.to/software.html +# Import the python implementation of the ed25519 algorithm provided by pyca, +# which is an optimized version of the one provided by ed25519's authors. +# Note: The pure Python version do not include protection against side-channel +# attacks. Verifying signatures can take approximately 2 seconds on a intel +# core 2 duo @ 2.2 ghz x 2). Optionally, the PyNaCl module may be used to +# speed up ed25519 cryptographic operations. +# http://ed25519.cr.yp.to/software.html +# https://github.com/pyca/ed25519 +# https://github.com/pyca/pynacl +# +# PyNaCl's 'cffi' dependency may thrown an 'IOError' exception when +# importing 'nacl.signing'. try: import nacl.signing import nacl.encoding -except ImportError: +except (ImportError, IOError): pass -# The pure Python implementation of ed25519. +# The optimized pure Python implementation of ed25519 provided by TUF. If +# PyNaCl cannot be imported and an attempt to use is made in this module, a +# 'tuf.UnsupportedLibraryError' exception is raised. import ed25519.ed25519 -import tuf # Digest objects needed to generate hashes. +import tuf + +# Digest objects needed to generate hashes. import tuf.hash # Perform object format-checking. import tuf.formats -# The default hash algorithm to use when generating KeyIDs. -_KEY_ID_HASH_ALGORITHM = 'sha256' - # Supported ed25519 signing methods. 'ed25519-python' is the pure Python # implementation signing method. 'ed25519-pynacl' (i.e., 'nacl' module) is the # (libsodium+Python bindings) implementation signing method. @@ -93,17 +97,15 @@ def generate_public_and_private(use_pynacl=False): """ - Generate an ed25519 seed key ('sk') and public key ('pk'). - In addition, a keyid used as an identifier for ed25519 keys is generated. - The object returned conforms to 'tuf.formats.ED25519KEY_SCHEMA' and has the - form: - {'keytype': 'ed25519', - 'keyid': keyid, - 'keyval': {'public': '876f5584a9db99b8546c0d8608d6...', - 'private': 'bf7336055c7638276efe9afe039...'}} + Generate a pair of ed25519 public and private keys. + The public and private keys returned conform to + 'tuf.formats.ED25519PULIC_SCHEMA' and 'tuf.formats.ED25519SEED_SCHEMA', + respectively, and have the form: - The public and private keys are strings. An ed25519 seed key is a random - 32-byte value. Public keys are also 32 bytes. + '\xa2F\x99\xe0\x86\x80%\xc8\xee\x11\xb95T\xd9\...' + + An ed25519 seed key is a random 32-byte string. Public keys are also 32 + bytes. >>> public, private = generate_public_and_private(use_pynacl=False) >>> tuf.formats.ED25519PUBLIC_SCHEMA.matches(public) @@ -120,21 +122,24 @@ def generate_public_and_private(use_pynacl=False): use_pynacl: True, if the ed25519 keys should be generated with PyNaCl. False, if the keys should be generated with the pure Python implementation of ed25519 - (much slower). + (slower). tuf.FormatError, if 'use_pynacl' is not a Boolean. - NotImplementedError, if a randomness source is not found. + tuf.UnsupportedLibraryError, if the PyNaCl ('nacl') module is unavailable + and 'use_pynacl' is True. + + NotImplementedError, if a randomness source is not found by 'os.urandom'. - The ed25519 keys are generated by first creating a random 32-byte value - 'sk' with os.urandom() and then calling ed25519's - ed25519.25519.publickey(sk) or PyNaCl's nacl.signing.SigningKey(). + The ed25519 keys are generated by first creating a random 32-byte seed + with os.urandom() and then calling ed25519's + ed25519.25519.publickey(seed) or PyNaCl's nacl.signing.SigningKey(). - A dictionary containing the ed25519 keys and other identifying information. - Conforms to 'tuf.formats.ED25519KEY_SCHEMA'. + A (public, private) tuple that conform to 'tuf.formats.ED25519PUBLIC_SCHEMA' + and 'tuf.formats.ED25519SEED_SCHEMA', respectively. """ # Does 'use_pynacl' have the correct format? @@ -153,9 +158,13 @@ def generate_public_and_private(use_pynacl=False): if use_pynacl: # Generate the public key. PyNaCl (i.e., 'nacl' module) performs # the actual key generation. + try: nacl_key = nacl.signing.SigningKey(seed) public = str(nacl_key.verify_key) - + except NameError: + message = 'The PyNaCl library and/or its dependencies unavailable.' + raise tuf.UnsupportedLibraryError(message) + # Use the pure Python implementation of ed25519. else: public = ed25519.ed25519.publickey(seed) @@ -169,20 +178,16 @@ def generate_public_and_private(use_pynacl=False): def create_signature(public_key, private_key, data, use_pynacl=False): """ - Return a signature dictionary of the form: - {'keyid': 'a0469d9491e3c0b42dd41fe3455359dbacb3306b6e8fb59...', - 'method': 'ed25519-python', - 'sig': '4b3829671b2c6b90034518a918d2447c722474c878c2431dd...'} - - Note: 'method' may also be 'ed25519-pynacl', if the signature was created - by the 'nacl' module. - - The signing process will use the public and seed key - ed25519_key_dict['keyval']['private'], - ed25519_key_dict['keyval']['public'] - - and 'data' to generate the signature. + Return a (signature, method) tuple, where the method is either: + 'ed25519-python' if the signature is generated by the pure python + implemenation, or 'ed25519-pynacl' if generated by 'nacl'. + signature conforms to 'tuf.formats.ED25519SIGNATURE_SCHEMA', and has the + form: + '\xae\xd7\x9f\xaf\x95{bP\x9e\xa8YO Z\x86\x9d...' + + A signature is a 64-byte string. + >>> public, private = generate_public_and_private(use_pynacl=False) >>> data = 'The quick brown fox jumps over the lazy dog' >>> signature, method = \ @@ -214,9 +219,7 @@ def create_signature(public_key, private_key, data, use_pynacl=False): of ed25519 (much slower). - TypeError, if a private key is not defined for 'ed25519_key_dict'. - - tuf.FormatError, if an incorrect format is found for 'ed25519_key_dict'. + tuf.FormatError, if the arguments are improperly formatted. tuf.CryptoError, if a signature cannot be created. @@ -252,37 +255,36 @@ def create_signature(public_key, private_key, data, use_pynacl=False): method = None signature = None - # Verify the signature, but only if the private key has been set. The private - # key is a NULL string if unset. Although it may be clearer to explicit check - # that 'private_key' is not '', we can/should check for a value and not - # compare identities with the 'is' keyword. - if len(private_key): - if use_pynacl: - method = 'ed25519-pynacl' - try: - nacl_key = nacl.signing.SigningKey(private) - nacl_sig = nacl_key.sign(data) - signature = nacl_sig.signature - except (ValueError, nacl.signing.CryptoError): - message = 'An "ed25519-pynacl" signature could not be created.' - raise tuf.CryptoError(message) - - # Generate an "ed25519-python" (i.e., pure python implementation) signature. - else: - # ed25519.ed25519.signature() requires both the seed and public keys. - # It calculates the SHA512 of the seed key, which is 32 bytes. - method = 'ed25519-python' - try: - signature = ed25519.ed25519.signature(data, private, public) - except Exception, e: - message = 'An "ed25519-python" signature could not be generated.' - raise tuf.CryptoError(message) - - # Raise an exception since the private key is not defined. + # The private and public keys have been validated above by 'tuf.formats' and + # should be 32-byte strings. + if use_pynacl: + method = 'ed25519-pynacl' + try: + nacl_key = nacl.signing.SigningKey(private) + nacl_sig = nacl_key.sign(data) + signature = nacl_sig.signature + + except NameError: + message = 'The PyNaCl library and/or its dependencies unavailable.' + raise tuf.UnsupportedLibraryError(message) + + except (ValueError, nacl.signing.CryptoError): + message = 'An "ed25519-pynacl" signature could not be created.' + raise tuf.CryptoError(message) + + # Generate an "ed25519-python" (i.e., pure python implementation) signature. else: - message = 'The required "private_key" key is not defined.' - raise TypeError(message) - + # ed25519.ed25519.signature() requires both the seed and public keys. + # It calculates the SHA512 of the seed key, which is 32 bytes. + method = 'ed25519-python' + try: + signature = ed25519.ed25519.signature(data, private, public) + + # 'Exception' raised by ed25519.py for any exception that may occur. + except Exception, e: + message = 'An "ed25519-python" signature could not be generated.' + raise tuf.CryptoError(message) + return signature, method @@ -292,11 +294,9 @@ def create_signature(public_key, private_key, data, use_pynacl=False): def verify_signature(public_key, method, signature, data, use_pynacl=False): """ - Determine whether the seed key belonging to 'ed25519_key_dict' produced - 'signature'. verify_signature() will use the public key found in - 'ed25519_key_dict', the 'method' and 'sig' objects contained in 'signature', - and 'data' to complete the verification. Type-checking performed on both - 'ed25519_key_dict' and 'signature'. + Determine whether the private key corresponding to 'public_key' produced + 'signature'. verify_signature() will use the public key, the 'method' and + 'sig', and 'data' arguments to complete the verification. >>> public, private = generate_public_and_private(use_pynacl=False) >>> data = 'The quick brown fox jumps over the lazy dog' @@ -315,34 +315,28 @@ def verify_signature(public_key, method, signature, data, use_pynacl=False): public_key: The public key is a 32-byte string. + + method: + 'ed25519-python' if the signature was generated by the pure python + implementation and 'ed25519-pynacl' if generated by 'nacl'. signature: - The signature dictionary produced by tuf.ed25519_key.create_signature(). - 'signature' has the form: - - {'keyid': 'a0469d9491e3c0b42dd41fe3455359dbacb3306b6e8fb59...', - 'method': 'ed25519-python', - 'sig': '4b3829671b2c6b90034518a918d2447c722474c878c2431dd...'} - - Conformant to 'tuf.formats.SIGNATURE_SCHEMA'. + The signature is a 64-byte string. data: - Data object used by tuf.ed25519_key.create_signature() to generate + Data object used by tuf.ed25519_keys.create_signature() to generate 'signature'. 'data' is needed here to verify the signature. use_pynacl: - True, if the ed25519 signature should be verified with PyNaCl. False, + True, if the ed25519 signature should be verified by PyNaCl. False, if the signature should be verified with the pure Python implementation - of ed25519 (much slower). + of ed25519 (slower). tuf.UnknownMethodError. Raised if the signing method used by - 'signature' is not one supported by tuf.ed25519_key.create_signature(). + 'signature' is not one supported by tuf.ed25519_keys.create_signature(). - tuf.FormatError. Raised if either 'ed25519_key_dict' - or 'signature' do not match their respective tuf.formats schema. - 'ed25519_key_dict' must conform to 'tuf.formats.ED25519KEY_SCHEMA'. - 'signature' must conform to 'tuf.formats.SIGNATURE_SCHEMA'. + tuf.FormatError. Raised if the arguments are improperly formatted. ed25519.ed25519.checkvalid() called to do the actual verification. @@ -367,11 +361,10 @@ def verify_signature(public_key, method, signature, data, use_pynacl=False): # Is 'use_pynacl' properly formatted? tuf.formats.TOGGLE_SCHEMA.check_match(use_pynacl) - # Using the public key belonging to 'ed25519_key_dict' - # (i.e., ed25519_key_dict['keyval']['public']), verify whether 'signature' - # was produced by ed25519_key_dict's corresponding seed key - # ed25519_key_dict['keyval']['private']. Before returning the Boolean result, + # Verify 'signature'. Before returning the Boolean result, # ensure 'ed25519-python' or 'ed25519-pynacl' was used as the signing method. + # Raise 'tuf.UnsupportedLibraryError' if 'use_pynacl' is True but 'nacl' is + # unavailable. public = public_key valid_signature = False @@ -382,6 +375,9 @@ def verify_signature(public_key, method, signature, data, use_pynacl=False): nacl_message = nacl_verify_key.verify(data, signature) if nacl_message == data: valid_signature = True + except NameError: + message = 'The PyNaCl library and/or its dependencies unavailable.' + raise tuf.UnsupportedLibraryError(message) except nacl.signing.BadSignatureError: pass From 42ea506dc1bf4e50c3a187dbfa53f2ae2a79c969 Mon Sep 17 00:00:00 2001 From: vladdd Date: Wed, 16 Oct 2013 13:07:10 -0400 Subject: [PATCH 53/95] Update time_ed25519.py following configurable crypto changes --- tuf/time_ed25519.py | 30 ++++++++++++++++++------------ 1 file changed, 18 insertions(+), 12 deletions(-) diff --git a/tuf/time_ed25519.py b/tuf/time_ed25519.py index ded684a1..89e8e726 100755 --- a/tuf/time_ed25519.py +++ b/tuf/time_ed25519.py @@ -3,30 +3,36 @@ import timeit import tuf -from tuf.ed25519_key import * +from tuf.ed25519_keys import * use_pynacl = False if '--pynacl' in sys.argv: use_pynacl = True -print('Time generate()') -print(timeit.timeit('generate(use_pynacl)', - setup='from __main__ import generate, use_pynacl', +print('Time generate_public_and_private()') +print(timeit.timeit('generate_public_and_private(use_pynacl)', + setup='from __main__ import generate_public_and_private, \ + use_pynacl', number=1)) print('\nTime create_signature()') -print(timeit.timeit('create_signature(ed25519_key, data, use_pynacl)', - setup='from __main__ import generate, create_signature, \ +print(timeit.timeit('create_signature(public, private, data, use_pynacl)', + setup='from __main__ import generate_public_and_private, \ + create_signature, \ use_pynacl; \ - ed25519_key = generate(use_pynacl);\ + public, private = \ + generate_public_and_private(use_pynacl); \ data = "The quick brown fox jumps over the lazy dog"', number=1)) print('\nTime verify_signature()') -print(timeit.timeit('verify_signature(ed25519_key, signature, data, use_pynacl)', - setup='from __main__ import generate, create_signature, \ - verify_signature, use_pynacl;\ - ed25519_key = generate(use_pynacl);\ +print(timeit.timeit('verify_signature(public, method, signature, data, use_pynacl)', + setup='from __main__ import generate_public_and_private, \ + create_signature, \ + verify_signature, use_pynacl; \ + public, private = \ + generate_public_and_private(use_pynacl); \ data = "The quick brown fox jumps over the lazy dog";\ - signature = create_signature(ed25519_key, data, use_pynacl)', + signature, method = \ + create_signature(public, private, data, use_pynacl)', number=1)) From 7ae7f2ddc2abaf7fda57b2e8231838e795dca506 Mon Sep 17 00:00:00 2001 From: vladdd Date: Thu, 17 Oct 2013 12:54:08 -0400 Subject: [PATCH 54/95] Add new tuf.formats.py schema for pycrypto_keys.py Remove extra whitespace in __init__.py --- tuf/__init__.py | 1 - tuf/formats.py | 3 +++ 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/tuf/__init__.py b/tuf/__init__.py index 70324314..9a5b0f4e 100755 --- a/tuf/__init__.py +++ b/tuf/__init__.py @@ -18,7 +18,6 @@ The names chosen for TUF Exception classes should end in 'Error' except where there is a good reason not to, and provide that reason in those cases. - """ import urlparse diff --git a/tuf/formats.py b/tuf/formats.py index 9fe6f470..3d3c0833 100755 --- a/tuf/formats.py +++ b/tuf/formats.py @@ -136,6 +136,9 @@ # The minimum number of bits for an RSA key. Must be 2048 bits and greater. RSAKEYBITS_SCHEMA = SCHEMA.Integer(lo=2048) +# A PyCrypto signature. +PYCRYPTOSIGNATURE_SCHEMA = SCHEMA.AnyString() + # An RSA key in PEM format. PEMRSA_SCHEMA = SCHEMA.AnyString() From 8a7d0d4baf95fa6fb861e50d3b4205625469b0a5 Mon Sep 17 00:00:00 2001 From: vladdd Date: Thu, 17 Oct 2013 12:56:58 -0400 Subject: [PATCH 55/95] Update docstrings and comments in pycrypto_keys.py Minor change to function names and argument validation. --- tuf/pycrypto_keys.py | 198 +++++++++++++++++++++---------------------- 1 file changed, 99 insertions(+), 99 deletions(-) diff --git a/tuf/pycrypto_keys.py b/tuf/pycrypto_keys.py index 1dc49473..0a68e5d4 100755 --- a/tuf/pycrypto_keys.py +++ b/tuf/pycrypto_keys.py @@ -12,32 +12,26 @@ See LICENSE for licensing information. - The goal of this module is to support public-key cryptography, RSA - algorithm, and the PyCrypto library. The RSA-related functions provided - include: + The goal of this module is to support public-key cryptography and RSA + keys through the PyCrypto library. The RSA-related functions provided: generate_rsa_public_and_private() - create_signature() - verify_signature() - - The optional functions include: + create_rsa_signature() + verify_rsa_signature() create_rsa_encrypted_pem() create_rsa_public_and_private_from_encrypted_pem() - These last two functions may be used save a - generated RSA key to a file. 'PyCrypto' (i.e., Crypto module) package used by 'rsa_key.py' - generates the actual RSA keys and the functions listed above can be viewed - as an easy-to-use public interface. - The generate() function returns a dictionary - containing all the information needed of RSA keys, such as public and private= - keys, keyIDs, and an idenfier. create_signature() and verify_signature() are - supplemental functions used for generating RSA signatures and verifying them. + PyCrypto (i.e., the 'Crypto' package) performs the actual cryptographic + operations and the functions listed above can be viewed as an easy-to-use + public interface. https://en.wikipedia.org/wiki/RSA_(algorithm) + https://github.com/dlitz/pycrypto """ -# Crypto.PublicKey (i.e., PyCrypto public-key cryptography) provides algorithms -# such as Digital Signature Algorithm (DSA) and the ElGamal encryption system. -# 'Crypto.PublicKey.RSA' is needed here to generate, sign, and verify RSA keys. +# Crypto.PublicKey (i.e., PyCrypto's public-key cryptography modules) supports +# algorithms like the Digital Signature Algorithm (DSA) and the ElGamal +# encryption system. 'Crypto.PublicKey.RSA' is needed here to generate, sign, +# and verify RSA keys. import Crypto.PublicKey.RSA # PyCrypto requires 'Crypto.Hash' hash objects to generate PKCS#1 PSS @@ -45,7 +39,7 @@ import Crypto.Hash.SHA256 # RSA's probabilistic signature scheme with appendix (RSASSA-PSS). -# PKCS#1 v1.5 is provided for compatability with existing applications, but +# PKCS#1 v1.5 is available for compatibility with existing applications, but # RSASSA-PSS is encouraged for newer applications. RSASSA-PSS generates # a random salt to ensure the signature generated is probabilistic rather than # deterministic, like PKCS#1 v1.5. @@ -53,6 +47,7 @@ # https://tools.ietf.org/html/rfc3447#section-8.1 import Crypto.Signature.PKCS1_PSS +# Import the TUF package and TUF-defined exceptions in __init__.py. import tuf # Digest objects needed to generate hashes. @@ -99,18 +94,17 @@ def generate_rsa_public_and_private(bits=_DEFAULT_RSA_KEY_BITS): greater, and a multiple of 256. - ValueError, if an exception occurs after calling the RSA key generation - routine. 'bits' must be a multiple of 256. The 'ValueError' exception is - raised by the PyCrypto key generation function. - tuf.FormatError, if 'bits' does not contain the correct format. + + ValueError, if an exception occurs in the RSA key generation routine. + 'bits' must be a multiple of 256. The 'ValueError' exception is raised by + the PyCrypto key generation function. - The RSA keys are generated by calling PyCrypto's - Crypto.PublicKey.RSA.generate(). + The RSA keys are generated by PyCrypto's Crypto.PublicKey.RSA.generate(). - A dictionary containing the RSA keys and other identifying information. + A (public, private) tuple containing the RSA keys in PEM format. """ # Does 'bits' have the correct format? @@ -126,8 +120,8 @@ def generate_rsa_public_and_private(bits=_DEFAULT_RSA_KEY_BITS): rsa_key_object = Crypto.PublicKey.RSA.generate(bits) # Extract the public & private halves of the RSA key and generate their - # PEM-formatted representations. The dictionary returned contains the - # private and public RSA keys in PEM format, as strings. + # PEM-formatted representations. Return the key pair as a (public, private) + # tuple, where each RSA is a string in PEM format. private = rsa_key_object.exportKey(format='PEM') rsa_pubkey = rsa_key_object.publickey() public = rsa_pubkey.exportKey(format='PEM') @@ -138,49 +132,48 @@ def generate_rsa_public_and_private(bits=_DEFAULT_RSA_KEY_BITS): -def create_signature(private_key, data): +def create_rsa_signature(private_key, data): """ - Return a signature dictionary of the form: - {'keyid': keyid, - 'method': 'PyCrypto-PKCS#1 PPS', - 'sig': sig}. + Generate an RSASSA-PSS signature. The signature, and the method (signature + algorithm) used, is returned as a (signature, method) tuple. - The signing process will use the private key - rsakey_dict['keyval']['private'] and 'data' to generate the signature. + The signing process will use 'private_key' and 'data' to generate the + signature. RFC3447 - RSASSA-PSS http://www.ietf.org/rfc/rfc3447.txt >>> public, private = generate_rsa_public_and_private(2048) >>> data = 'The quick brown fox jumps over the lazy dog' - >>> signature, method = create_signature(private, data) + >>> signature, method = create_rsa_signature(private, data) >>> tuf.formats.NAME_SCHEMA.matches(method) True >>> method == 'PyCrypto-PKCS#1 PSS' True - >>> signature is not None + >>> tuf.formats.PYCRYPTOSIGNATURE_SCHEMA.matches(method) True private_key: - The private key is a string in PEM format. + The private RSA key, a string in PEM format. data: - Data object used by create_signature() to generate the signature. + Data object used by create_rsa_signature() to generate the signature. - TypeError, if a private key is not defined for 'rsakey_dict'. + tuf.FormatError, if 'private_key' is improperly formatted. + + TypeError, if 'private_key' is unset. - tuf.FormatError, if an incorrect format is found for 'private_key'. - - tuf.CryptoError, + tuf.CryptoError, if the signature cannot be generated. PyCrypto's 'Crypto.Signature.PKCS1_PSS' called to generate the signature. - A (signature, method) tuple, where + A (signature, method) tuple, where the signature is a string and the method + is 'PyCrypto-PKCS#1 PSS'. """ # Does 'private_key' have the correct format? @@ -210,7 +203,7 @@ def create_signature(private_key, data): message = 'An RSA signature could not be generated.' raise tuf.CryptoError(message) else: - raise TypeError('The required private key is not defined for "rsakey_dict".') + raise TypeError('The required private key is unset.') return signature, method @@ -218,33 +211,32 @@ def create_signature(private_key, data): -def verify_signature(signature, signature_method, public_key, data): +def verify_rsa_signature(signature, signature_method, public_key, data): """ - Determine whether the private key belonging to 'rsakey_dict' produced - 'signature'. verify_signature() will use the public key found in - 'rsakey_dict', the 'method' and 'sig' objects contained in 'signature', - and 'data' to complete the verification. Type-checking performed on both - 'rsakey_dict' and 'signature'. + Determine whether the corresponding private key of 'public_key' produced + 'signature'. verify_signature() will use the public key, signature method, + and 'data' to complete the verification. >>> public, private = generate_rsa_public_and_private(2048) >>> data = 'The quick brown fox jumps over the lazy dog' - >>> signature, method = create_signature(private, data) - >>> verify_signature(signature, method, public, data) + >>> signature, method = create_rsa_signature(private, data) + >>> verify_rsa_signature(signature, method, public, data) True - >>> verify_signature(signature, method, public, 'bad_data') + >>> verify_rsa_signature(signature, method, public, 'bad_data') False signature: - The signature dictionary produced by tuf.rsa_key.create_signature(). - 'signature' has the form: - {'keyid': keyid, 'method': 'method', 'sig': sig}. Conformant to - 'tuf.formats.SIGNATURE_SCHEMA'. - + An RSASSA PSS signature as a string. This is the signature returned + by create_rsa_signature(). + signature_method: + A string that indicates the signature algorithm used to generate + 'signature'. 'PyCrypto-PKCS#1 PSS' is currently supported. public_key: + The RSA public key, a string in PEM format. data: Data object used by tuf.rsa_key.create_signature() to generate @@ -254,10 +246,8 @@ def verify_signature(signature, signature_method, public_key, data): tuf.UnknownMethodError. Raised if the signing method used by 'signature' is not one supported by tuf.rsa_key.create_signature(). - tuf.FormatError. Raised if either 'rsakey_dict' - or 'signature' do not match their respective tuf.formats schema. - 'rsakey_dict' must conform to 'tuf.formats.RSAKEY_SCHEMA'. - 'signature' must conform to 'tuf.formats.SIGNATURE_SCHEMA'. + tuf.FormatError. Raised if 'signature', 'signature_method', or 'public_key' + is improperly formatted. Crypto.Signature.PKCS1_PSS.verify() called to do the actual verification. @@ -274,16 +264,19 @@ def verify_signature(signature, signature_method, public_key, data): # Does 'signature_method' have the correct format? tuf.formats.NAME_SCHEMA.check_match(signature_method) - # Using the public key belonging to 'rsakey_dict' - # (i.e., rsakey_dict['keyval']['public']), verify whether 'signature' - # was produced by rsakey_dict's corresponding private key - # rsakey_dict['keyval']['private']. Before returning the Boolean result, - # ensure 'PyCrypto-PKCS#1 PSS' was used as the signing method. + # Does 'signature' have the correct format? + tuf.formats.PYCRYPTOSIGNATURE_SCHEMA.check_match(signature) + + # Verify whether the private key of 'public_key' produced the signature. + # Before returning the Boolean result, ensure 'PyCrypto-PKCS#1 PSS' was used + # as the signing method. signature = signature method = signature_method public = public_key valid_signature = False + # Verify the signature with PyCrypto if the signature method is valid, else + # raise 'tuf.UnknownMethodError'. if method == 'PyCrypto-PKCS#1 PSS': try: rsa_key_object = Crypto.PublicKey.RSA.importKey(public_key) @@ -322,7 +315,7 @@ def create_rsa_encrypted_pem(private_key, passphrase): private_key: - The public and private keys are in PEM format and stored as strings. + The private key string in PEM format. passphrase: The passphrase, or password, to encrypt the private part of the RSA @@ -330,9 +323,11 @@ def create_rsa_encrypted_pem(private_key, passphrase): encryption key is derived from it. - TypeError, if a private key is not defined for 'rsakey_dict'. + tuf.FormatError, if the arguments are improperly formatted. - tuf.FormatError, if an incorrect format is found for 'rsakey_dict'. + tuf.CryptoError, if an RSA key in encrypted PEM format cannot be created. + + TypeError, 'private_key' is unset. PyCrypto's Crypto.PublicKey.RSA.exportKey() called to perform the actual @@ -340,29 +335,34 @@ def create_rsa_encrypted_pem(private_key, passphrase): A string in PEM format, where the private RSA key is encrypted. + Conforms to 'tuf.formats.PEMRSA_SCHEMA'. """ - # Does 'rsakey_dict' have the correct format? - # This check will ensure 'rsakey_dict' has the appropriate number + # Does 'private_key' have the correct format? + # This check will ensure 'private_key' has the appropriate number # of objects and object types, and that all dict keys are properly named. # Raise 'tuf.FormatError' if the check fails. tuf.formats.PEMRSA_SCHEMA.check_match(private_key) - # Does 'signature' have the correct format? + # Does 'passphrase' have the correct format? tuf.formats.PASSWORD_SCHEMA.check_match(passphrase) - # Extract the private key from 'rsakey_dict', which is stored in PEM format - # and unencrypted. The extracted key will be imported and converted to - # PyCrypto's RSA key object (i.e., Crypto.PublicKey.RSA).Use PyCrypto's - # exportKey method, with a passphrase specified, to create the string. - # PyCrypto uses PBKDF1+MD5 to strengthen 'passphrase', and 3DES with CBC mode - # for encryption. - try: - rsa_key_object = Crypto.PublicKey.RSA.importKey(private_key) - encrypted_pem = rsa_key_object.exportKey(format='PEM', passphrase=passphrase) - except (ValueError, IndexError, TypeError), e: - message = 'An encrypted RSA key in PEM format could not be generated.' - raise tuf.CryptoError(message) + # 'private_key' is in PEM format and unencrypted. The extracted key will be + # imported and converted to PyCrypto's RSA key object + # (i.e., Crypto.PublicKey.RSA). Use PyCrypto's exportKey method, with a + # passphrase specified, to create the string. PyCrypto uses PBKDF1+MD5 to + # strengthen 'passphrase', and 3DES with CBC mode for encryption. + # 'private_key' may still be a NULL string after the tuf.formats check. + if len(private_key): + try: + rsa_key_object = Crypto.PublicKey.RSA.importKey(private_key) + encrypted_pem = rsa_key_object.exportKey(format='PEM', passphrase=passphrase) + except (ValueError, IndexError, TypeError), e: + message = 'An encrypted RSA key in PEM format could not be generated.' + raise tuf.CryptoError(message) + else: + raise TypeError('The required private key is unset.') + return encrypted_pem @@ -373,15 +373,18 @@ def create_rsa_encrypted_pem(private_key, passphrase): def create_rsa_public_and_private_from_encrypted_pem(encrypted_pem, passphrase): """ - Return an RSA key in 'tuf.formats.RSAKEY_SCHEMA' format, which has the - form: - {'keytype': 'rsa', - 'keyid': keyid, - 'keyval': {'public': '-----BEGIN RSA PUBLIC KEY----- ...', - 'private': '-----BEGIN RSA PRIVATE KEY----- ...'}} + Generate public and private RSA keys from an encrypted PEM. + The public and private keys returned conform to 'tuf.formats.PEMRSA_SCHEMA' + and have the form: + '-----BEGIN RSA PUBLIC KEY----- ...' + + or + + '-----BEGIN RSA PRIVATE KEY----- ...' - The RSAKEY_SCHEMA object is generated from a byte string in PEM format, - where the private part of the RSA key is encrypted. PyCrypto's importKey + The public and private keys are returned as strings in PEM format. + + The private key part of 'encrypted_pem' is encrypted. PyCrypto's importKey method is used, where a passphrase is specified. PyCrypto uses PBKDF1+MD5 to strengthen 'passphrase', and 3DES with CBC mode for encryption/decryption. Alternatively, key data may be encrypted with AES-CTR-Mode and the passphrase @@ -415,17 +418,14 @@ def create_rsa_public_and_private_from_encrypted_pem(encrypted_pem, passphrase): it is used to derive a stronger symmetric key. - TypeError, if a private key is not defined for 'rsakey_dict'. - - tuf.FormatError, if an incorrect format is found for the - 'rsakey_dict' object. + tuf.FormatError, if the arguments are improperly formatted. PyCrypto's 'Crypto.PublicKey.RSA.importKey()' called to perform the actual conversion from an encrypted RSA private key. - A dictionary in 'tuf.formats.RSAKEY_SCHEMA' format. + A (public, private) tuple containing the RSA keys in PEM format. """ # Does 'encryped_pem' have the correct format? From 05f7826b59fb553878977268453fee0395c17f9d Mon Sep 17 00:00:00 2001 From: vladdd Date: Thu, 17 Oct 2013 13:01:32 -0400 Subject: [PATCH 56/95] Update test_pycrypto_keys.py after pycrypto_keys.py changes --- tests/unit/test_pycrypto_keys.py | 30 +++++++++++++++--------------- 1 file changed, 15 insertions(+), 15 deletions(-) diff --git a/tests/unit/test_pycrypto_keys.py b/tests/unit/test_pycrypto_keys.py index fe854e67..eaa322b0 100755 --- a/tests/unit/test_pycrypto_keys.py +++ b/tests/unit/test_pycrypto_keys.py @@ -54,10 +54,10 @@ def test_generate_rsa_public_and_private(self): pycrypto.generate_rsa_public_and_private, '2048') - def test_create_signature(self): + def test_create_rsa_signature(self): global private_rsa data = 'The quick brown fox jumps over the lazy dog' - signature, method = pycrypto.create_signature(private_rsa, data) + signature, method = pycrypto.create_rsa_signature(private_rsa, data) # Verify format of returned values. self.assertNotEqual(None, signature) @@ -67,44 +67,44 @@ def test_create_signature(self): # Check for improperly formatted argument. self.assertRaises(tuf.FormatError, - pycrypto.create_signature, 123, data) + pycrypto.create_rsa_signature, 123, data) # Check for invalid 'data'. self.assertRaises(tuf.CryptoError, - pycrypto.create_signature, private_rsa, 123) + pycrypto.create_rsa_signature, private_rsa, 123) - def test_verify_signature(self): + def test_verify_rsa_signature(self): global public_rsa global private_rsa data = 'The quick brown fox jumps over the lazy dog' - signature, method = pycrypto.create_signature(private_rsa, data) + signature, method = pycrypto.create_rsa_signature(private_rsa, data) - valid_signature = pycrypto.verify_signature(signature, method, public_rsa, + valid_signature = pycrypto.verify_rsa_signature(signature, method, public_rsa, data) self.assertEqual(True, valid_signature) # Check for improperly formatted arguments. - self.assertRaises(tuf.FormatError, pycrypto.verify_signature, signature, + self.assertRaises(tuf.FormatError, pycrypto.verify_rsa_signature, signature, 123, public_rsa, data) - self.assertRaises(tuf.FormatError, pycrypto.verify_signature, signature, + self.assertRaises(tuf.FormatError, pycrypto.verify_rsa_signature, signature, method, 123, data) - # Check for invalid signature and data. - self.assertRaises(tuf.CryptoError, pycrypto.verify_signature, 123, method, + self.assertRaises(tuf.FormatError, pycrypto.verify_rsa_signature, 123, method, public_rsa, data) - self.assertRaises(tuf.CryptoError, pycrypto.verify_signature, signature, + # Check for invalid signature and data. + self.assertRaises(tuf.CryptoError, pycrypto.verify_rsa_signature, signature, method, public_rsa, 123) - self.assertEqual(False, pycrypto.verify_signature(signature, method, + self.assertEqual(False, pycrypto.verify_rsa_signature(signature, method, public_rsa, 'mismatched data')) - mismatched_signature, method = pycrypto.create_signature(private_rsa, + mismatched_signature, method = pycrypto.create_rsa_signature(private_rsa, 'mismatched data') - self.assertEqual(False, pycrypto.verify_signature(mismatched_signature, + self.assertEqual(False, pycrypto.verify_rsa_signature(mismatched_signature, method, public_rsa, data)) From 60574503fc28493911e6e6aa6cc663d3f6007fb9 Mon Sep 17 00:00:00 2001 From: vladdd Date: Thu, 17 Oct 2013 13:12:55 -0400 Subject: [PATCH 57/95] Update keys.py with modified pycrypto_keys.py function names --- tuf/keys.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/tuf/keys.py b/tuf/keys.py index 5f1062cf..925b339f 100755 --- a/tuf/keys.py +++ b/tuf/keys.py @@ -558,7 +558,7 @@ def create_signature(key_dict, data): if keytype == 'rsa': if _RSA_CRYPTO_LIBRARY == 'pycrypto': - sig, method = tuf.pycrypto_keys.create_signature(private, data) + sig, method = tuf.pycrypto_keys.create_rsa_signature(private, data) else: message = 'Unsupported "tuf.conf.RSA_CRYPTO_LIBRARY": '+\ repr(_RSA_CRYPTO_LIBRARY)+'.' @@ -672,8 +672,8 @@ def verify_signature(key_dict, signature, data): if keytype == 'rsa': if _RSA_CRYPTO_LIBRARY == 'pycrypto': - valid_signature = tuf.pycrypto_keys.verify_signature(sig, method, - public, data) + valid_signature = tuf.pycrypto_keys.verify_rsa_signature(sig, method, + public, data) else: message = 'Unsupported "tuf.conf.RSA_CRYPTO_LIBRARY": '+\ repr(_RSA_CRYPTO_LIBRARY)+'.' From 760cd62d4e6d06c8165257df334e8da1db8016e7 Mon Sep 17 00:00:00 2001 From: vladdd Date: Thu, 17 Oct 2013 14:05:26 -0400 Subject: [PATCH 58/95] Rename functions in keys.py and update test_keys.py create_in_metadata_format --> format_keyval_to_metadata create_from_metadata_format --> format_metadata_to_key --- tests/unit/test_keys.py | 20 ++++++++++---------- tuf/keys.py | 16 ++++++++-------- 2 files changed, 18 insertions(+), 18 deletions(-) diff --git a/tests/unit/test_keys.py b/tests/unit/test_keys.py index 3297680f..5523378b 100755 --- a/tests/unit/test_keys.py +++ b/tests/unit/test_keys.py @@ -70,17 +70,17 @@ def test_generate_rsa_key(self): self.assertTrue(tuf.formats.RSAKEY_SCHEMA.matches(KEYS.generate_rsa_key(2048))) self.assertTrue(tuf.formats.RSAKEY_SCHEMA.matches(KEYS.generate_rsa_key(4096))) - def test_create_in_metadata_format(self): + def test_format_keyval_to_metadata(self): keyvalue = rsakey_dict['keyval'] keytype = rsakey_dict['keytype'] - key_meta = KEYS.create_in_metadata_format(keytype, keyvalue) + key_meta = KEYS.format_keyval_to_metadata(keytype, keyvalue) # Check if the format of the object returned by this function corresponds # to KEY_SCHEMA format. self.assertEqual(None, tuf.formats.KEY_SCHEMA.check_match(key_meta), FORMAT_ERROR_MSG) - key_meta = KEYS.create_in_metadata_format(keytype, keyvalue, private=True) + key_meta = KEYS.format_keyval_to_metadata(keytype, keyvalue, private=True) # Check if the format of the object returned by this function corresponds # to KEY_SCHEMA format. @@ -88,22 +88,22 @@ def test_create_in_metadata_format(self): FORMAT_ERROR_MSG) # Supplying a 'bad' keyvalue. - self.assertRaises(tuf.FormatError, KEYS.create_in_metadata_format, + self.assertRaises(tuf.FormatError, KEYS.format_keyval_to_metadata, 'bad_keytype', keyvalue) del keyvalue['public'] - self.assertRaises(tuf.FormatError, KEYS.create_in_metadata_format, + self.assertRaises(tuf.FormatError, KEYS.format_keyval_to_metadata, keytype, keyvalue) - def test_create_from_metadata_format(self): + def test_format_metadata_to_key(self): # Reconfiguring rsakey_dict to conform to KEY_SCHEMA # i.e. {keytype: 'rsa', keyval: {public: pub_key, private: priv_key}} #keyid = rsakey_dict['keyid'] del rsakey_dict['keyid'] - rsakey_dict_from_meta = KEYS.create_from_metadata_format(rsakey_dict) + rsakey_dict_from_meta = KEYS.format_metadata_to_key(rsakey_dict) # Check if the format of the object returned by this function corresponds # to RSAKEY_SCHEMA format. @@ -112,13 +112,13 @@ def test_create_from_metadata_format(self): FORMAT_ERROR_MSG) # Supplying a wrong number of arguments. - self.assertRaises(TypeError, KEYS.create_from_metadata_format) + self.assertRaises(TypeError, KEYS.format_metadata_to_key) args = (rsakey_dict, rsakey_dict) - self.assertRaises(TypeError, KEYS.create_from_metadata_format, *args) + self.assertRaises(TypeError, KEYS.format_metadata_to_key, *args) # Supplying a malformed argument to the function - should get FormatError del rsakey_dict['keyval'] - self.assertRaises(tuf.FormatError, KEYS.create_from_metadata_format, + self.assertRaises(tuf.FormatError, KEYS.format_metadata_to_key, rsakey_dict) diff --git a/tuf/keys.py b/tuf/keys.py index 925b339f..dd0facbd 100755 --- a/tuf/keys.py +++ b/tuf/keys.py @@ -19,7 +19,7 @@ generated RSA key to a file. The 'PyCrypto' package used by 'rsa_key.py' generates the actual RSA keys and the functions listed above can be viewed as an easy-to-use public interface. Additional functions contained here - include create_in_metadata_format() and create_from_metadata_format(). These + include format_keyval_to_metadata() and format_metadata_to_key(). These last two functions produce or use RSA keys compatible with the key structures listed in TUF Metadata files. The generate() function returns a dictionary containing all the information needed of RSA keys, such as public and private= @@ -270,7 +270,7 @@ def generate_ed25519_key(): -def create_in_metadata_format(keytype, key_value, private=False): +def format_keyval_to_metadata(keytype, key_value, private=False): """ Return a dictionary conformant to 'tuf.formats.KEY_SCHEMA'. @@ -295,7 +295,7 @@ def create_in_metadata_format(keytype, key_value, private=False): >>> key_val = ed25519_key['keyval'] >>> keytype = ed25519_key['keytype'] >>> ed25519_metadata = \ - create_in_metadata_format(keytype, key_val, private=True) + format_keyval_to_metadata(keytype, key_val, private=True) >>> tuf.formats.KEY_SCHEMA.matches(ed25519_metadata) True @@ -345,7 +345,7 @@ def create_in_metadata_format(keytype, key_value, private=False): -def create_from_metadata_format(key_metadata): +def format_metadata_to_key(key_metadata): """ Construct an RSA key dictionary (i.e., tuf.formats.RSAKEY_SCHEMA) @@ -371,8 +371,8 @@ def create_from_metadata_format(key_metadata): >>> key_val = ed25519_key['keyval'] >>> keytype = ed25519_key['keytype'] >>> ed25519_metadata = \ - create_in_metadata_format(keytype, key_val, private=True) - >>> ed25519_key_2 = create_from_metadata_format(ed25519_metadata) + format_keyval_to_metadata(keytype, key_val, private=True) + >>> ed25519_key_2 = format_metadata_to_key(ed25519_metadata) >>> tuf.formats.ED25519KEY_SCHEMA.matches(ed25519_key_2) True >>> ed25519_key == ed25519_key_2 @@ -429,8 +429,8 @@ def _get_keyid(keytype, key_value): # 'keyid' will be generated from an object conformant to KEY_SCHEMA, # which is the format Metadata files (e.g., root.txt) store keys. - # 'create_in_metadata_format()' returns the object needed by _get_keyid(). - rsakey_meta = create_in_metadata_format(keytype, key_value, private=False) + # 'format_keyval_to_metadata()' returns the object needed by _get_keyid(). + rsakey_meta = format_keyval_to_metadata(keytype, key_value, private=False) # Convert the RSA key to JSON Canonical format suitable for adding # to digest objects. From 45af91191a1f7ef5818c30b6dd171611250abca1 Mon Sep 17 00:00:00 2001 From: vladdd Date: Fri, 18 Oct 2013 14:01:51 -0400 Subject: [PATCH 59/95] Update docstrings and comments in keys.py Configurable crypto changes previously implemented but the docstrings and comments of keys.py still needed updating. Minor edit to test_keys.py and a note added about a missing test case. --- tests/unit/test_keys.py | 17 +- tuf/keys.py | 386 +++++++++++++++++++++++----------------- 2 files changed, 231 insertions(+), 172 deletions(-) diff --git a/tests/unit/test_keys.py b/tests/unit/test_keys.py index 5523378b..758c1009 100755 --- a/tests/unit/test_keys.py +++ b/tests/unit/test_keys.py @@ -3,22 +3,17 @@ test_keys.py - Konstantin Andrianov + Vladimir Diaz - April 24, 2012. + October 10, 2013. See LICENSE for licensing information. Test cases for test_keys.py. - - - I'm using 'global rsakey_dict' - there is no harm in doing so since - in order to modify the global variable in any method, python requires - explicit indication to modify i.e. declaring 'global' in each method - that modifies the global variable 'rsakey_dict'. + TODO: test case for ed25519 key generation and refactor. """ import unittest @@ -49,7 +44,7 @@ def setUp(self): rsakey_dict['keyval']['public']=temp_key_vals[0] rsakey_dict['keyval']['private']=temp_key_vals[1] - + def test_generate_rsa_key(self): _rsakey_dict = KEYS.generate_rsa_key() @@ -69,7 +64,8 @@ def test_generate_rsa_key(self): # does not raise any errors and returns a valid key. self.assertTrue(tuf.formats.RSAKEY_SCHEMA.matches(KEYS.generate_rsa_key(2048))) self.assertTrue(tuf.formats.RSAKEY_SCHEMA.matches(KEYS.generate_rsa_key(4096))) - + + def test_format_keyval_to_metadata(self): keyvalue = rsakey_dict['keyval'] keytype = rsakey_dict['keytype'] @@ -96,7 +92,6 @@ def test_format_keyval_to_metadata(self): keytype, keyvalue) - def test_format_metadata_to_key(self): # Reconfiguring rsakey_dict to conform to KEY_SCHEMA # i.e. {keytype: 'rsa', keyval: {public: pub_key, private: priv_key}} diff --git a/tuf/keys.py b/tuf/keys.py index dd0facbd..87177c9f 100755 --- a/tuf/keys.py +++ b/tuf/keys.py @@ -12,20 +12,29 @@ See LICENSE for licensing information. - The goal of this module is to support public-key cryptography using the RSA - algorithm. The RSA-related functions provided include generate(), - create_signature(), and verify_signature(). The create_encrypted_pem() and - create_from_encrypted_pem() functions are optional, and may be used save a - generated RSA key to a file. The 'PyCrypto' package used by 'rsa_key.py' - generates the actual RSA keys and the functions listed above can be viewed - as an easy-to-use public interface. Additional functions contained here - include format_keyval_to_metadata() and format_metadata_to_key(). These - last two functions produce or use RSA keys compatible with the key structures - listed in TUF Metadata files. The generate() function returns a dictionary - containing all the information needed of RSA keys, such as public and private= - keys, keyIDs, and an idenfier. create_signature() and verify_signature() are - supplemental functions used for generating RSA signatures and verifying them. + The goal of this module is to centralize cryptographic key routines and their + supported operations (e.g., creating and verifying signatures). This module + is designed to support multiple public-key algorithms, such as RSA and + ED25519, and multiple cryptography libraries. Which cryptography library to + use is determined by the default, or user modified, values set in + 'tuf.conf.py' + + The (RSA and ED25519)-related functions provided include generate_rsa_key(), + generate_ed5519_key(), create_signature(), and verify_signature(). + The cryptography libraries called by 'tuf.keys.py' generate the actual TUF + keys and the functions listed above can be viewed as an easy-to-use public + interface. + + Additional functions contained here include format_keyval_to_metadata() and + format_metadata_to_key(). These last two functions produce or use TUF keys + compatible with the key structures listed in TUF Metadata files. The key + generation functions return a dictionary containing all the information needed + of TUF keys, such as public and private keys and a keyID. create_signature() + and verify_signature() are supplemental functions used for generating + signatures and verifying them. + https://en.wikipedia.org/wiki/RSA_(algorithm) + http://ed25519.cr.yp.to/ Key IDs are used as identifiers for keys (e.g., RSA key). They are the hexadecimal representation of the hash of key object (specifically, the key @@ -39,15 +48,22 @@ # hexlified. import binascii -# +# 'pycrypto' is the only currently supported library for the creation of RSA +# keys. https://github.com/dlitz/pycrypto _SUPPORTED_RSA_CRYPTO_LIBRARIES = ['pycrypto'] -# +# The currently supported libraries for the creation of ed25519 keys and +# signatures. The 'pynacl' library should be installed and used over the slower +# python implementation of ed25519. The python implementation will be used +# if 'pynacl' is unavailable. _SUPPORTED_ED25519_CRYPTO_LIBRARIES = ['ed25519', 'pynacl'] -# +# Track which libraries are imported and thus available. A optimized version +# of the ed25519 python implementation is provided by TUF and avaialable by +# default. https://github.com/pyca/ed25519 _available_crypto_libraries = ['ed25519'] +# Import the PyCrypto library so that RSA keys are supported. try: import Crypto import tuf.pycrypto_keys @@ -55,25 +71,33 @@ except ImportError: pass +# Import the PyNaCl library, if available. It is recommended this library be +# used over the pure python implementation of ed25519, due to its speedier +# routines and side-channel protections available in the libsodium library. try: import nacl _available_crypto_libraries.append('pynacl') except ImportError: pass +# The optimized version of the ed25519 library provided by default is imported +# regardless of the availability of PyNaCl. import tuf.ed25519_keys +# Import the TUF package and TUF-defined exceptions in __init__.py. import tuf + +# Import the cryptography library settings. import tuf.conf # Digest objects needed to generate hashes. import tuf.hash -# Perform object format-checking. +# Perform format checks of argument objects. import tuf.formats - +# The hash algorithm to use in the generation of keyids. _KEY_ID_HASH_ALGORITHM = 'sha256' # Recommended RSA key sizes: @@ -82,7 +106,9 @@ # size 3072 provide security through 2031 and beyond. _DEFAULT_RSA_KEY_BITS = 3072 -# The crypto libraries used in 'keys.py'. +# The crypto libraries to use in 'keys.py', set by default or by the user. +# The following cryptography libraries are currently supported: +# ['pycrypto', 'pynacl', 'ed25519'] _RSA_CRYPTO_LIBRARY = tuf.conf.RSA_CRYPTO_LIBRARY _ED25519_CRYPTO_LIBRARY = tuf.conf.ED25519_CRYPTO_LIBRARY @@ -90,18 +116,19 @@ def generate_rsa_key(bits=_DEFAULT_RSA_KEY_BITS): """ - Generate public and private RSA keys, with modulus length 'bits'. - In addition, a keyid used as an identifier for RSA keys is generated. - The object returned conforms to 'tuf.formats.RSAKEY_SCHEMA' and as the form: + Generate public and private RSA keys, with modulus length 'bits'. In + addition, a keyid identifier for the RSA key is generated. The object + returned conforms to 'tuf.formats.RSAKEY_SCHEMA' and has the + form: {'keytype': 'rsa', 'keyid': keyid, 'keyval': {'public': '-----BEGIN RSA PUBLIC KEY----- ...', 'private': '-----BEGIN RSA PRIVATE KEY----- ...'}} - The public and private keys are in PEM format and stored as strings. + The public and private keys are strings in PEM format. - Although the crytography library called sets a 1024-bit minimum key size, - generate() enforces a minimum key size of 2048 bits. If 'bits' is + Although the PyCrypto crytography library called sets a 1024-bit minimum + key size, generate() enforces a minimum key size of 2048 bits. If 'bits' is unspecified, a 3072-bit RSA key is generated, which is the key size recommended by TUF. @@ -121,18 +148,23 @@ def generate_rsa_key(bits=_DEFAULT_RSA_KEY_BITS): greater, and a multiple of 256. + tuf.FormatError, if 'bits' is improperly or invalid (i.e., not an integer + and not at least 2048). + + tuf.UnsupportedLibraryError, if any of the cryptography libraries specified + in 'tuf.conf.py' are unsupported or unavailable. + ValueError, if an exception occurs after calling the RSA key generation routine. 'bits' must be a multiple of 256. The 'ValueError' exception is raised by the key generation function of the cryptography library called. - tuf.FormatError, if 'bits' does not contain the correct format. - The RSA keys are generated by calling PyCrypto's Crypto.PublicKey.RSA.generate(). A dictionary containing the RSA keys and other identifying information. + Conforms to 'tuf.formats.RSAKEY_SCHEMA'. """ # Does 'bits' have the correct format? @@ -141,11 +173,11 @@ def generate_rsa_key(bits=_DEFAULT_RSA_KEY_BITS): # Raise 'tuf.FormatError' if the check fails. tuf.formats.RSAKEYBITS_SCHEMA.check_match(bits) - # Raise 'tuf.Error' if 'tuf.conf.CRYPTO_LIBRARY' is not supported or could - # not be imported. + # Raise 'tuf.UnsupportedLibraryError' if the following libraries, specified in + # 'tuf.conf', are unsupported or unavailable: + # 'tuf.conf.RSA_CRYPTO_LIBRARY' and 'tuf.conf.ED25519_CRYPTO_LIBRARY'. _check_crypto_libraries() - # Check for valid crypto library # Begin building the RSA key dictionary. rsakey_dict = {} keytype = 'rsa' @@ -160,9 +192,9 @@ def generate_rsa_key(bits=_DEFAULT_RSA_KEY_BITS): public, private = tuf.pycrypto_keys.generate_rsa_public_and_private(bits) else: message = 'Invalid crypto library: '+repr(_RSA_CRYPTO_LIBRARY)+'.' - raise ValueError(message) + raise tuf.UnsupportedLibraryError(message) - # Generate the keyid for the RSA key. 'key_value' corresponds to the + # Generate the keyid of the RSA key. 'key_value' corresponds to the # 'keyval' entry of the 'RSAKEY_SCHEMA' dictionary. The private key # information is not included in the generation of the 'keyid' identifier. key_value = {'public': public, @@ -186,21 +218,19 @@ def generate_rsa_key(bits=_DEFAULT_RSA_KEY_BITS): def generate_ed25519_key(): """ - Generate public and private RSA keys, with modulus length 'bits'. - In addition, a keyid used as an identifier for RSA keys is generated. - The object returned conforms to 'tuf.formats.RSAKEY_SCHEMA' and as the form: + Generate public and private ED25519 keys, both of length 32-bytes, although + they are hexlified to 64 bytes. + In addition, a keyid identifier generated for the returned ED25519 object. + The object returned conforms to 'tuf.formats.ED25519KEY_SCHEMA' and as the + form: {'keytype': 'ed25519', - 'keyid': keyid, - 'keyval': {'public': '-----BEGIN RSA PUBLIC KEY----- ...', - 'private': '-----BEGIN RSA PRIVATE KEY----- ...'}} + 'keyid': 'f30a0870d026980100c0573bd557394f8c1bbd6...', + 'keyval': {'public': '9ccf3f02b17f82febf5dd3bab878b767d8408...', + 'private': 'ab310eae0e229a0eceee3947b6e0205dfab3...'}} - The public and private keys are in PEM format and stored as strings. + The public and private keys are strings in PEM format and stored in the + 'keyval' field of the returned dictionary. - Although the crytography library called sets a 1024-bit minimum key size, - generate() enforces a minimum key size of 2048 bits. If 'bits' is - unspecified, a 3072-bit RSA key is generated, which is the key size - recommended by TUF. - >>> ed25519_key = generate_ed25519_key() >>> tuf.formats.ED25519KEY_SCHEMA.matches(ed25519_key) True @@ -213,35 +243,32 @@ def generate_ed25519_key(): None. - ValueError, if an exception occurs after calling the RSA key generation - routine. 'bits' must be a multiple of 256. The 'ValueError' exception is - raised by the key generation function of the cryptography library called. - - tuf.FormatError, if 'bits' does not contain the correct format. - + tuf.UnsupportedLibraryError, if an unsupported or unavailable library is + detected. + - The RSA keys are generated by calling PyCrypto's - Crypto.PublicKey.RSA.generate(). + The ED25519 keys are generated by calling either the optimized pure Python + implementation of ed25519, or the ed25519 routines provided by 'pynacl'. - A dictionary containing the RSA keys and other identifying information. + A dictionary containing the ED25519 keys and other identifying information. + Conforms to 'tuf.formats.ED25519KEY_SCHEMA'. """ - - # Raise 'tuf.Error' if 'tuf.conf.CRYPTO_LIBRARY' is not supported or could - # not be imported. + + # Raise 'tuf.UnsupportedLibraryError' if the following libraries, specified + # in 'tuf.conf', are unsupported or unavailable: + # 'tuf.conf.RSA_CRYPTO_LIBRARY' and 'tuf.conf.ED25519_CRYPTO_LIBRARY'. _check_crypto_libraries() - # Check for valid crypto library - # Begin building the RSA key dictionary. + # Begin building the ED25519 key dictionary. ed25519_key = {} keytype = 'ed25519' public = None private = None - # Generate the public and private RSA keys. The PyCrypto module performs - # the actual key generation. Raise 'ValueError' if 'bits' is less than 1024 - # or not a multiple of 256, although a 2048-bit minimum is enforced by - # tuf.formats.RSAKEYBITS_SCHEMA.check_match(). + # Generate the public and private ED25519 keys. Use the 'pynacl' library + # if available, otherwise fall back to optimized pure python implementation + # provided by pyca and available in TUF. if 'pynacl' in _available_crypto_libraries: public, private = \ tuf.ed25519_keys.generate_public_and_private(use_pynacl=True) @@ -249,15 +276,15 @@ def generate_ed25519_key(): public, private = \ tuf.ed25519_keys.generate_public_and_private(use_pynacl=False) - # Generate the keyid for the RSA key. 'key_value' corresponds to the - # 'keyval' entry of the 'RSAKEY_SCHEMA' dictionary. The private key + # Generate the keyid of the ED25519 key. 'key_value' corresponds to the + # 'keyval' entry of the 'ED25519KEY_SCHEMA' dictionary. The private key # information is not included in the generation of the 'keyid' identifier. key_value = {'public': binascii.hexlify(public), 'private': ''} keyid = _get_keyid(keytype, key_value) - # Build the 'rsakey_dict' dictionary. Update 'key_value' with the RSA - # private key prior to adding 'key_value' to 'rsakey_dict'. + # Build the 'ed25519_key' dictionary. Update 'key_value' with the ED25519 + # private key prior to adding 'key_value' to 'ed25519_key'. key_value['private'] = binascii.hexlify(private) ed25519_key['keytype'] = keytype @@ -276,19 +303,17 @@ def format_keyval_to_metadata(keytype, key_value, private=False): Return a dictionary conformant to 'tuf.formats.KEY_SCHEMA'. If 'private' is True, include the private key. The dictionary returned has the form: - {'keytype': 'rsa', - 'keyval': {'public': '-----BEGIN RSA PUBLIC KEY----- ...', - 'private': '-----BEGIN RSA PRIVATE KEY----- ...'}} + {'keytype': keytype, + 'keyval': {'public': '...', + 'private': '...'}} or if 'private' is False: - {'keytype': 'rsa', - 'keyval': {'public': '-----BEGIN RSA PUBLIC KEY----- ...', + {'keytype': keytype, + 'keyval': {'public': '...', 'private': ''}} - The private and public keys are in PEM format. - - RSA keys are stored in Metadata files (e.g., root.txt) in the format + TUF keys are stored in Metadata files (e.g., root.txt) in the format returned by this function. >>> ed25519_key = generate_ed25519_key() @@ -301,19 +326,20 @@ def format_keyval_to_metadata(keytype, key_value, private=False): key_type: - 'rsa' or 'ed25519'. + The 'rsa' or 'ed25519' strings. key_value: - A dictionary containing a private and public RSA key. + A dictionary containing a private and public keys. 'key_value' is of the form: - {'public': '-----BEGIN RSA PUBLIC KEY----- ...', - 'private': '-----BEGIN RSA PRIVATE KEY----- ...'}}, - conformat to 'tuf.formats.KEYVAL_SCHEMA'. + {'public': '...', + 'private': '...'}}, + + conformant to 'tuf.formats.KEYVAL_SCHEMA'. private: - Indicates if the private key should be included in the - returned dictionary. + Indicates if the private key should be included in the dictionary + returned. tuf.FormatError, if 'key_value' does not conform to @@ -323,7 +349,7 @@ def format_keyval_to_metadata(keytype, key_value, private=False): None. - An 'KEY_SCHEMA' dictionary. + A 'tuf.formats.KEY_SCHEMA' dictionary. """ # Does 'keytype' have the correct format? @@ -348,24 +374,23 @@ def format_keyval_to_metadata(keytype, key_value, private=False): def format_metadata_to_key(key_metadata): """ - Construct an RSA key dictionary (i.e., tuf.formats.RSAKEY_SCHEMA) - from 'key_metadata'. The dict returned by this function has the exact - format as the dict returned by generate(). It is of the form: + Construct a TUF key dictionary (e.g., tuf.formats.RSAKEY_SCHEMA) + according to the keytype of 'key_metadata'. The dict returned by this + function has the exact format as the dict returned by one of the key + generations functions, like generate_ed25519_key(). The dict returned + has the form: - {'keytype': 'rsa', - 'keyid': keyid, - 'keyval': {'public': '-----BEGIN RSA PUBLIC KEY----- ...', - 'private': '-----BEGIN RSA PRIVATE KEY----- ...'}} + {'keytype': keytype, + 'keyid': 'f30a0870d026980100c0573bd557394f8c1bbd6...', + 'keyval': {'public': '...', + 'private': '...'}} - The public and private keys are in PEM format and stored as strings. - - RSA key dictionaries in RSAKEY_SCHEMA format should be used by - modules storing a collection of keys, such as a keydb and keystore. - RSA keys as stored in metadata files use a different format, so this - function should be called if an RSA key is extracted from one of these - metadata files and needs converting. Generate() creates an entirely - new key and returns it in the format appropriate for 'keydb.py' and - 'keystore.py'. + For example, RSA key dictionaries in RSAKEY_SCHEMA format should be used by + modules storing a collection of keys, such as with keydb.py. RSA keys as + stored in metadata files use a different format, so this function should be + called if an RSA key is extracted from one of these metadata files and need + converting. The key generation functions create an entirely new key and + return it in the format appropriate for 'keydb.py'. >>> ed25519_key = generate_ed25519_key() >>> key_val = ed25519_key['keyval'] @@ -380,7 +405,7 @@ def format_metadata_to_key(key_metadata): key_metadata: - The RSA key dictionary as stored in Metadata files, conforming to + The TUF key dictionary as stored in Metadata files, conforming to 'tuf.formats.KEY_SCHEMA'. It has the form: {'keytype': '...', @@ -395,7 +420,8 @@ def format_metadata_to_key(key_metadata): None. - A dictionary containing the RSA keys and other identifying information. + In the case of an RSA key, a dictionary conformant to + 'tuf.formats.RSAKEY_SCHEMA'. """ # Does 'key_metadata' have the correct format? @@ -413,7 +439,7 @@ def format_metadata_to_key(key_metadata): # The hash is in hexdigest form. keyid = _get_keyid(keytype, key_value) - # We now have all the required key values. Build 'rsakey_dict'. + # All the required key values gathered. Build 'key_dict'. key_dict['keytype'] = keytype key_dict['keyid'] = keyid key_dict['keyval'] = key_value @@ -430,16 +456,16 @@ def _get_keyid(keytype, key_value): # 'keyid' will be generated from an object conformant to KEY_SCHEMA, # which is the format Metadata files (e.g., root.txt) store keys. # 'format_keyval_to_metadata()' returns the object needed by _get_keyid(). - rsakey_meta = format_keyval_to_metadata(keytype, key_value, private=False) + key_meta = format_keyval_to_metadata(keytype, key_value, private=False) - # Convert the RSA key to JSON Canonical format suitable for adding + # Convert the TUF key to JSON Canonical format, suitable for adding # to digest objects. - rsakey_update_data = tuf.formats.encode_canonical(rsakey_meta) + key_update_data = tuf.formats.encode_canonical(key_meta) # Create a digest object and call update(), using the JSON # canonical format of 'rskey_meta' as the update data. digest_object = tuf.hash.digest(_KEY_ID_HASH_ALGORITHM) - digest_object.update(rsakey_update_data) + digest_object.update(key_update_data) # 'keyid' becomes the hexadecimal representation of the hash. keyid = digest_object.hexdigest() @@ -451,8 +477,16 @@ def _get_keyid(keytype, key_value): def _check_crypto_libraries(): - """ check """ - + """ Ensure all the crypto libraries specified in tuf.conf are available. """ + + # The checks below all raise 'tuf.CryptoError' if the RSA and ED25519 + # crypto libraries specified in 'tuf.conf.py' are not supported or + # unavailable. The appropriate error message is added to the exception. + # The funcions of this module that depend on user-installed crypto libraries + # should call this private function to ensure the called routine does not fail + # with unpredictable exceptions in the event of a missing library. + # The supported and available lists checked are populated when 'tuf.keys.py' + # is imported. if _RSA_CRYPTO_LIBRARY not in _SUPPORTED_RSA_CRYPTO_LIBRARIES: message = 'The '+repr(_RSA_CRYPTO_LIBRARY)+' crypto library specified'+ \ ' in "tuf.conf.RSA_CRYPTO_LIBRARY" is not supported.\n'+ \ @@ -460,7 +494,7 @@ def _check_crypto_libraries(): raise tuf.CryptoError(message) if _ED25519_CRYPTO_LIBRARY not in _SUPPORTED_ED25519_CRYPTO_LIBRARIES: - message = 'The '+repr(_ED25519_CRYPTO_LIBRARY)+' crypto library specified'+ \ + message = 'The '+repr(_ED25519_CRYPTO_LIBRARY)+' crypto library specified'+\ ' in "tuf.conf.ED25519_CRYPTO_LIBRARY" is not supported.\n'+ \ 'Supported crypto libraries: '+repr(_SUPPORTED_ED25519_CRYPTO_LIBRARIES)+'.' raise tuf.CryptoError(message) @@ -471,7 +505,7 @@ def _check_crypto_libraries(): raise tuf.CryptoError(message) if _ED25519_CRYPTO_LIBRARY not in _available_crypto_libraries: - message = 'The '+repr(_ED25519_CRYPTO_LIBRARY)+' crypto library specified'+ \ + message = 'The '+repr(_ED25519_CRYPTO_LIBRARY)+' crypto library specified'+\ ' in "tuf.conf.ED25519_CRYPTO_LIBRARY" could not be imported.' raise tuf.CryptoError(message) @@ -483,15 +517,25 @@ def create_signature(key_dict, data): """ Return a signature dictionary of the form: - {'keyid': keyid, - 'method': 'PyCrypto-PKCS#1 PPS', - 'sig': sig}. + {'keyid': 'f30a0870d026980100c0573bd557394f8c1bbd6...', + 'method': '...', + 'sig': '...'}. - The signing process will use the private key - rsakey_dict['keyval']['private'] and 'data' to generate the signature. + The signing process will use the private key in + key_dict['keyval']['private'] and 'data' to generate the signature. + The following signature methods are supported: + + 'PyCrypto-PKCS#1 PSS' RFC3447 - RSASSA-PSS http://www.ietf.org/rfc/rfc3447. + + 'ed25519-python or 'ed25519-pynacl' + ed25519 - high-speed high security signatures + http://ed25519.cr.yp.to/ + + Which signature to generate is determined by the key type of 'key_dict' + and the available cryptography library specified in 'tuf.conf'. >>> ed25519_key = generate_ed25519_key() >>> data = 'The quick brown fox jumps over the lazy dog' @@ -508,46 +552,52 @@ def create_signature(key_dict, data): key_dict: - A dictionary containing the RSA keys and other identifying information. - 'rsakey_dict' has the form: + A dictionary containing the TUF keys. An example RSA key dict has the + form: {'keytype': 'rsa', - 'keyid': keyid, + 'keyid': 'f30a0870d026980100c0573bd557394f8c1bbd6...', 'keyval': {'public': '-----BEGIN RSA PUBLIC KEY----- ...', 'private': '-----BEGIN RSA PRIVATE KEY----- ...'}} - The public and private keys are in PEM format and stored as strings. + The public and private keys are strings in PEM format. data: Data object used by create_signature() to generate the signature. - TypeError, if a private key is not defined for 'rsakey_dict'. + tuf.FormatError, if 'key_dict' is improperly formatted. + + tuf.UnsupportedLibraryError, if an unsupported or unavailable library is + detected. - tuf.FormatError, if an incorrect format is found for the - 'rsakey_dict' object. + TypeError, if 'key_dict' contains an invalid keytype. - PyCrypto's 'Crypto.Signature.PKCS1_PSS' called to perform the actual - signing. + The cryptography library specified in 'tuf.conf' called to perform the + actual signing routine. A signature dictionary conformat to 'tuf.format.SIGNATURE_SCHEMA'. """ - # Does 'rsakey_dict' have the correct format? - # This check will ensure 'rsakey_dict' has the appropriate number - # of objects and object types, and that all dict keys are properly named. + # Does 'key_dict' have the correct format? + # This check will ensure 'key_dict' has the appropriate number of objects + # and object types, and that all dict keys are properly named. # Raise 'tuf.FormatError' if the check fails. + # The key type of 'key_dict' must be either 'rsa' or 'ed25519'. tuf.formats.ANYKEY_SCHEMA.check_match(key_dict) - # Raise 'tuf.Error' if 'tuf.conf.CRYPTO_LIBRARY' is not supported or could - # not be imported. + # Raise 'tuf.UnsupportedLibraryError' if the following libraries, specified + # in 'tuf.conf', are unsupported or unavailable: + # 'tuf.conf.RSA_CRYPTO_LIBRARY' and 'tuf.conf.ED25519_CRYPTO_LIBRARY'. _check_crypto_libraries() # Signing the 'data' object requires a private key. - # The 'PyCrypto-PKCS#1 PSS' (i.e., PyCrypto module) signing method is the - # only method currently supported. + # The 'PyCrypto-PKCS#1 PSS' (i.e., PyCrypto module), + # 'ed25519-pynacl' (i.e., 'nacl'), and 'ed25519-python (i.e., optimized pure + # python implementation of ed25519) are the only signing methods currently + # supported. signature = {} keytype = key_dict['keytype'] public = key_dict['keyval']['public'] @@ -555,20 +605,26 @@ def create_signature(key_dict, data): keyid = key_dict['keyid'] method = None sig = None - + + # Call the appropriate cryptography libraries for the supported key types, + # otherwise raise an exception. if keytype == 'rsa': if _RSA_CRYPTO_LIBRARY == 'pycrypto': sig, method = tuf.pycrypto_keys.create_rsa_signature(private, data) else: message = 'Unsupported "tuf.conf.RSA_CRYPTO_LIBRARY": '+\ repr(_RSA_CRYPTO_LIBRARY)+'.' - raise tuf.Error(message) + raise tuf.UnsupportedLibraryError(message) + elif keytype == 'ed25519': public = binascii.unhexlify(public) private = binascii.unhexlify(private) - if _ED25519_CRYPTO_LIBRARY == 'pynacl' and 'pynacl' in _available_crypto_libraries: + if _ED25519_CRYPTO_LIBRARY == 'pynacl' \ + and 'pynacl' in _available_crypto_libraries: sig, method = tuf.ed25519_keys.create_signature(public, private, data, use_pynacl=True) + + # Fall back to using the optimized pure python implementation of ed25519. else: sig, method = tuf.ed25519_keys.create_signature(public, private, data, use_pynacl=False) @@ -590,11 +646,10 @@ def create_signature(key_dict, data): def verify_signature(key_dict, signature, data): """ - Determine whether the private key belonging to 'rsakey_dict' produced + Determine whether the private key belonging to 'key_dict' produced 'signature'. verify_signature() will use the public key found in - 'rsakey_dict', the 'method' and 'sig' objects contained in 'signature', - and 'data' to complete the verification. Type-checking performed on both - 'rsakey_dict' and 'signature'. + 'key_dict', the 'method' and 'sig' objects contained in 'signature', + and 'data' to complete the verification. >>> ed25519_key = generate_ed25519_key() >>> data = 'The quick brown fox jumps over the lazy dog' @@ -610,47 +665,52 @@ def verify_signature(key_dict, signature, data): >>> verify_signature(rsa_key, signature, 'bad_data') False - key_dict: - A dictionary containing the RSA keys and other identifying information. - 'rsakey_dict' has the form: + A dictionary containing the TUF keys and other identifying information. + If 'key_dict' is an RSA key, it has the form: {'keytype': 'rsa', - 'keyid': keyid, + 'keyid': 'f30a0870d026980100c0573bd557394f8c1bbd6...', 'keyval': {'public': '-----BEGIN RSA PUBLIC KEY----- ...', 'private': '-----BEGIN RSA PRIVATE KEY----- ...'}} - The public and private keys are in PEM format and stored as strings. + The public and private keys are strings in PEM format. signature: - The signature dictionary produced by tuf.rsa_key.create_signature(). + The signature dictionary produced by one of the key generation functions. 'signature' has the form: - {'keyid': keyid, 'method': 'method', 'sig': sig}. Conformant to - 'tuf.formats.SIGNATURE_SCHEMA'. + + {'keyid': 'f30a0870d026980100c0573bd557394f8c1bbd6...', + 'method': 'method', + 'sig': sig}. + + Conformant to 'tuf.formats.SIGNATURE_SCHEMA'. data: Data object used by tuf.rsa_key.create_signature() to generate 'signature'. 'data' is needed here to verify the signature. - tuf.UnknownMethodError. Raised if the signing method used by - 'signature' is not one supported by tuf.rsa_key.create_signature(). + tuf.FormatError, raised if either 'key_dict' or 'signature' are improperly + formatted. - tuf.FormatError. Raised if either 'rsakey_dict' - or 'signature' do not match their respective tuf.formats schema. - 'rsakey_dict' must conform to 'tuf.formats.RSAKEY_SCHEMA'. - 'signature' must conform to 'tuf.formats.SIGNATURE_SCHEMA'. + tuf.UnsupportedLibraryError, if an unsupported or unavailable library is + detected. + + tuf.UnknownMethodError. Raised if the signing method used by + 'signature' is not one supported. - Crypto.Signature.PKCS1_PSS.verify() called to do the actual verification. + The cryptography library specified in 'tuf.conf' called to do the actual + verification. Boolean. True if the signature is valid, False otherwise. """ - # Does 'rsakey_dict' have the correct format? - # This check will ensure 'rsakey_dict' has the appropriate number + # Does 'key_dict' have the correct format? + # This check will ensure 'key_dict' has the appropriate number # of objects and object types, and that all dict keys are properly named. # Raise 'tuf.FormatError' if the check fails. tuf.formats.ANYKEY_SCHEMA.check_match(key_dict) @@ -658,11 +718,10 @@ def verify_signature(key_dict, signature, data): # Does 'signature' have the correct format? tuf.formats.SIGNATURE_SCHEMA.check_match(signature) - # Using the public key belonging to 'rsakey_dict' + # Using the public key belonging to 'key_dict' # (i.e., rsakey_dict['keyval']['public']), verify whether 'signature' - # was produced by rsakey_dict's corresponding private key - # rsakey_dict['keyval']['private']. Before returning the Boolean result, - # ensure 'PyCrypto-PKCS#1 PSS' was used as the signing method. + # was produced by key_dict's corresponding private key + # key_dict['keyval']['private']. method = signature['method'] sig = signature['sig'] sig = binascii.unhexlify(sig) @@ -670,6 +729,8 @@ def verify_signature(key_dict, signature, data): keytype = key_dict['keytype'] valid_signature = False + # Call the appropriate cryptography libraries for the supported key types, + # otherwise raise an exception. if keytype == 'rsa': if _RSA_CRYPTO_LIBRARY == 'pycrypto': valid_signature = tuf.pycrypto_keys.verify_rsa_signature(sig, method, @@ -677,13 +738,16 @@ def verify_signature(key_dict, signature, data): else: message = 'Unsupported "tuf.conf.RSA_CRYPTO_LIBRARY": '+\ repr(_RSA_CRYPTO_LIBRARY)+'.' - raise tuf.Error(message) + raise tuf.UnsupportedLibraryError(message) + elif keytype == 'ed25519': public = binascii.unhexlify(public) - if _RSA_CRYPTO_LIBRARY == 'pynacl' and 'pynacl' in _available_crypto_libraries: + if _RSA_CRYPTO_LIBRARY == 'pynacl' and \ + 'pynacl' in _available_crypto_libraries: valid_signature = tuf.ed25519_keys.verify_signature(public, method, sig, data, use_pynacl=True) + # Fall back to the optimized pure python implementation of ed25519. else: valid_signature = tuf.ed25519_keys.verify_signature(public, method, sig, data, @@ -698,6 +762,6 @@ def verify_signature(key_dict, signature, data): if __name__ == '__main__': # The interactive sessions of the documentation strings can # be tested by running 'keys.py' as a standalone module. - # python -B keys.py + # python keys.py import doctest doctest.testmod() From 5eb0858e45d422e40dc44afe1f02b053fac86789 Mon Sep 17 00:00:00 2001 From: vladdd Date: Tue, 22 Oct 2013 14:01:06 -0400 Subject: [PATCH 60/95] Add import and export functions for passphrase-protected pem files in keys.py --- tuf/keys.py | 179 ++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 179 insertions(+) diff --git a/tuf/keys.py b/tuf/keys.py index 87177c9f..37aeba9a 100755 --- a/tuf/keys.py +++ b/tuf/keys.py @@ -759,6 +759,185 @@ def verify_signature(key_dict, signature, data): + + +def import_rsakey_from_encrypted_pem(encrypted_pem, password): + """ + + Generate public and private RSA keys, with modulus length 'bits'. In + addition, a keyid identifier for the RSA key is generated. The object + returned conforms to 'tuf.formats.RSAKEY_SCHEMA' and has the + form: + {'keytype': 'rsa', + 'keyid': keyid, + 'keyval': {'public': '-----BEGIN RSA PUBLIC KEY----- ...', + 'private': '-----BEGIN RSA PRIVATE KEY----- ...'}} + + The public and private keys are strings in PEM format. + + Although the PyCrypto crytography library called sets a 1024-bit minimum + key size, generate() enforces a minimum key size of 2048 bits. If 'bits' is + unspecified, a 3072-bit RSA key is generated, which is the key size + recommended by TUF. + + >>> rsa_key = generate_rsa_key() + >>> private = rsa_key['keyval']['private'] + >>> passphrase = 'secret' + >>> encrypted_pem = create_rsa_encrypted_pem(private, passphrase) + >>> rsa_key2 = import_rsakey_from_encrypted_pem(encrypted_pem, passphrase) + >>> rsa_key == rsa_key2 + True + + + encrypted_pem: + The key size, or key length, of the RSA key. 'bits' must be 2048, or + greater, and a multiple of 256. + + password: + + + tuf.FormatError, if 'bits' is improperly or invalid (i.e., not an integer + and not at least 2048). + + tuf.UnsupportedLibraryError, if any of the cryptography libraries specified + in 'tuf.conf.py' are unsupported or unavailable. + + ValueError, if an exception occurs after calling the RSA key generation + routine. 'bits' must be a multiple of 256. The 'ValueError' exception is + raised by the key generation function of the cryptography library called. + + + The RSA keys are generated by calling PyCrypto's + Crypto.PublicKey.RSA.generate(). + + + A dictionary containing the RSA keys and other identifying information. + Conforms to 'tuf.formats.RSAKEY_SCHEMA'. + """ + + # Does 'encrypted_pem' have the correct format? + # This check will ensure 'encrypted_pem' conforms to + # 'tuf.formats.PEMRSA_SCHEMA'. + tuf.formats.PEMRSA_SCHEMA.check_match(encrypted_pem) + + # Does 'password' have the correct format? + tuf.formats.PASSWORD_SCHEMA.check_match(password) + + # Raise 'tuf.UnsupportedLibraryError' if the following libraries, specified in + # 'tuf.conf', are unsupported or unavailable: + # 'tuf.conf.RSA_CRYPTO_LIBRARY' and 'tuf.conf.ED25519_CRYPTO_LIBRARY'. + _check_crypto_libraries() + + # Begin building the RSA key dictionary. + rsakey_dict = {} + keytype = 'rsa' + public = None + private = None + + # Generate the public and private RSA keys. The PyCrypto module performs + # the actual key generation. Raise 'ValueError' if 'bits' is less than 1024 + # or not a multiple of 256, although a 2048-bit minimum is enforced by + # tuf.formats.RSAKEYBITS_SCHEMA.check_match(). + if _RSA_CRYPTO_LIBRARY == 'pycrypto': + public, private = \ + tuf.pycrypto_keys.create_rsa_public_and_private_from_encrypted_pem(encrypted_pem, + password) + else: + message = 'Invalid crypto library: '+repr(_RSA_CRYPTO_LIBRARY)+'.' + raise tuf.UnsupportedLibraryError(message) + + # Generate the keyid of the RSA key. 'key_value' corresponds to the + # 'keyval' entry of the 'RSAKEY_SCHEMA' dictionary. The private key + # information is not included in the generation of the 'keyid' identifier. + key_value = {'public': public, + 'private': ''} + keyid = _get_keyid(keytype, key_value) + + # Build the 'rsakey_dict' dictionary. Update 'key_value' with the RSA + # private key prior to adding 'key_value' to 'rsakey_dict'. + key_value['private'] = private + + rsakey_dict['keytype'] = keytype + rsakey_dict['keyid'] = keyid + rsakey_dict['keyval'] = key_value + + return rsakey_dict + + + + + +def create_rsa_encrypted_pem(private_key, passphrase): + """ + + Return a string in PEM format, where the private part of the RSA key is + encrypted. The private part of the RSA key is encrypted by the Triple + Data Encryption Algorithm (3DES) and Cipher-block chaining (CBC) for the + mode of operation. Password-Based Key Derivation Function 1 (PBKF1) + MD5 + is used to strengthen 'passphrase'. + + https://en.wikipedia.org/wiki/Triple_DES + https://en.wikipedia.org/wiki/PBKDF2 + + >>> rsa_key = generate_rsa_key() + >>> private = rsa_key['keyval']['private'] + >>> passphrase = 'secret' + >>> encrypted_pem = create_rsa_encrypted_pem(private, passphrase) + >>> tuf.formats.PEMRSA_SCHEMA.matches(encrypted_pem) + True + + + private_key: + The private key string in PEM format. + + passphrase: + The passphrase, or password, to encrypt the private part of the RSA + key. 'passphrase' is not used directly as the encryption key, a stronger + encryption key is derived from it. + + + tuf.FormatError, if the arguments are improperly formatted. + + tuf.CryptoError, if an RSA key in encrypted PEM format cannot be created. + + TypeError, 'private_key' is unset. + + + PyCrypto's Crypto.PublicKey.RSA.exportKey() called to perform the actual + generation of the PEM-formatted output. + + + A string in PEM format, where the private RSA key is encrypted. + Conforms to 'tuf.formats.PEMRSA_SCHEMA'. + """ + + # Does 'private_key' have the correct format? + # This check will ensure 'private_key' has the appropriate number + # of objects and object types, and that all dict keys are properly named. + # Raise 'tuf.FormatError' if the check fails. + tuf.formats.PEMRSA_SCHEMA.check_match(private_key) + + # Does 'passphrase' have the correct format? + tuf.formats.PASSWORD_SCHEMA.check_match(passphrase) + + encrypted_pem = None + + # Generate the public and private RSA keys. The PyCrypto module performs + # the actual key generation. Raise 'ValueError' if 'bits' is less than 1024 + # or not a multiple of 256, although a 2048-bit minimum is enforced by + # tuf.formats.RSAKEYBITS_SCHEMA.check_match(). + if _RSA_CRYPTO_LIBRARY == 'pycrypto': + encrypted_pem = \ + tuf.pycrypto_keys.create_rsa_encrypted_pem(private_key, passphrase) + else: + message = 'Invalid crypto library: '+repr(_RSA_CRYPTO_LIBRARY)+'.' + raise tuf.UnsupportedLibraryError(message) + + return encrypted_pem + + + + if __name__ == '__main__': # The interactive sessions of the documentation strings can # be tested by running 'keys.py' as a standalone module. From df8d84d3dad52fbb6e251b2bfca507c8bae48494 Mon Sep 17 00:00:00 2001 From: vladdd Date: Tue, 22 Oct 2013 14:02:01 -0400 Subject: [PATCH 61/95] [WIP] Add libtuftools.py skeleton --- tuf/repo/libtuftools.py | 1432 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 1432 insertions(+) create mode 100755 tuf/repo/libtuftools.py diff --git a/tuf/repo/libtuftools.py b/tuf/repo/libtuftools.py new file mode 100755 index 00000000..f902f9ab --- /dev/null +++ b/tuf/repo/libtuftools.py @@ -0,0 +1,1432 @@ +""" + + libtuftools.py + + + Vladimir Diaz + + + October 19, 2013 + + + See LICENSE for licensing information. + + +""" + +import getpass +import sys + +import tuf +import tuf.formats +import tuf.keys + + +# Recommended RSA key sizes: +# http://www.emc.com/emc-plus/rsa-labs/historical/twirl-and-rsa-key-size.htm#table1 +# According to the document above, revised May 6, 2003, RSA keys of +# size 3072 provide security through 2031 and beyond. 2048-bit keys +# are the recommended minimum and are good from the present through 2030. +DEFAULT_RSA_KEY_BITS = 3072 + +# The metadata filenames for the top-level roles. +ROOT_FILENAME = 'root.json' +TARGETS_FILENAME = 'targets.json' +RELEASE_FILENAME = 'release.json' +TIMESTAMP_FILENAME = 'timestamp.json' + +# Expiration date, in seconds, of the top-level roles (excluding 'Root'). +# The expiration time of the 'Root' role is set by the user. A metadata +# expiration date is set by taking the current time and adding the expiration +# seconds listed below. +# Initial 'targets.txt' expiration time of 3 months. +TARGETS_EXPIRATION = 7889230 + +# Initial 'release.txt' expiration time of 1 week. +RELEASE_EXPIRATION = 604800 + +# Initial 'timestamp.txt' expiration time of 1 day. +TIMESTAMP_EXPIRATION = 86400 + + +class Repository: + """ + + + + + + + + + + Repository object. + """ + + def __init__(self): + self.root + self.release + self.timestamp + self.targets + + + + def write(self): + """ + + Write all the Metadata objects' JSON contents to the corresponding files. + + + + + + + + + """ + + + + + +class Metadata: + """ + + Write all the Metadata objects' JSON contents to the corresponding files. + + + + + + + + + """ + + def __init__(self): + + # This gets modified when methods are called and attributes changed. + self._JSON_contents + + # Reference to Repository object. + self._repository + + self.expiration + + + + + def refresh(self, object): + """ + + + >>> + >>> + >>> + + + + + + + + + """ + + raise NotImplementedError() + + + + + +class Root(Metadata): + """ + + + >>> + >>> + >>> + + + + + + + + + """ + + def __init__(self): + + self.root_keys + self.root_threshold + self.timestamp_keys + self.release_keys + self.targets_keys + self.default_expiration + + + + def write(self): + pass + + + + + +class Timestamp(Metadata): + """ + + + >>> + >>> + >>> + + + + + + + + + """ + + def __init__(self): + pass + + + def refresh(self): + pass + + + + + +class Release(Metadata): + """ + + + >>> + >>> + >>> + + + + + + + + + """ + + def __init__(self): + pass + + + def refresh(self): + pass + + + + + +class Targets(Metadata): + """ + + + >>> + >>> + >>> + + + + + + + + + """ + + def __init__(self): + + self.target_list + self.delegation_list + + + + def refresh(self): + pass + + + + + def add_target(self, filepath): + """ + + Takes a filepath relative to the targets directory. Regular expresssion + would be useful here. + + >>> + >>> + >>> + + + filepath: + + + + + + + """ + + + + + + def remove_target(self, filepath): + """ + + Takes a filepath relative to the targets directory. Regular expresssion + would be useful here. + + >>> + >>> + >>> + + + filepath: + + + + + + + """ + + + + + + def delegate(self, rolename, public_keys, targets): + """ + + 'targets' is a list of target filepaths, and can be empty. + + >>> + >>> + >>> + + + rolename: + + public_keys: + + targets: + + + + + + + """ + + + + + + def revoke(self, rolename): + """ + + + >>> + >>> + >>> + + + rolename: + + + + + + + """ + + + + + +def _prompt(message, result_type=str): + """ + Prompt the user for input by printing 'message', converting + the input to 'result_type', and returning the value to the + caller. + """ + + return result_type(raw_input(message)) + + + + + +def _get_password(prompt='Password: ', confirm=False): + """ + Return the password entered by the user. If 'confirm' + is True, the user is asked to enter the previously + entered password once again. If they match, the + password is returned to the caller. + """ + + while True: + # getpass() prompts the user for a password without echoing + # the user input. + password = getpass.getpass(prompt, sys.stderr) + if not confirm: + return password + password2 = getpass.getpass('Confirm: ', sys.stderr) + if password == password2: + return password + else: + print 'Mismatch; try again.' + + + + + +def create_new_repository(repository_directory): + """ + + Create a new repository with barebones metadata and return a Repository + object representing it. + + + repository_directory: + + + + + + + libtuftools.Repository object. + """ + + # Build the repository directories. + metadata_directory = None + targets_directory = None + + # Save the repository directory to the current directory, with + # an initial name of 'repository'. The repository maintainer + # may opt to rename this directory and should transfer it elsewhere, + # such as the webserver that will respond to TUF requests. + repository_directory = os.path.join(os.getcwd(), 'repository') + + # Copy the files from the project directory to the repository's targets + # directory. The targets directory will hold all the individual + # target files. + targets_directory = os.path.join(repository_directory, 'targets') + temporary_directory = tempfile.mkdtemp() + temporary_targets = os.path.join(temporary_directory, 'targets') + shutil.copytree(project_directory, temporary_targets) + + # Remove the log file created by the tuf logger, if it exists. + # It might exist if the current directory was specified as the + # project directory on the command-line. + log_filename = tuf.log._DEFAULT_LOG_FILENAME + if log_filename in os.listdir(temporary_targets): + log_file = os.path.join(temporary_targets, log_filename) + os.remove(log_file) + + # Try to create the repository directory. + try: + os.mkdir(repository_directory) + # 'OSError' raised if the directory cannot be created. + except OSError, e: + message = 'Trying to create a new repository over an old repository '+\ + 'installation. Remove '+repr(repository_directory)+' before '+\ + 'trying again.' + if e.errno == errno.EEXIST: + raise tuf.RepositoryError(message) + else: + raise + + # Move the temporary targets directory into place now that repository + # directory has been created and remove previously created temporary + # directory. + shutil.move(temporary_targets, targets_directory) + os.rmdir(temporary_directory) + + # Try to create the metadata directory that will hold all of the + # metadata files, such as 'root.txt' and 'release.txt'. + try: + metadata_directory = os.path.join(repository_directory, 'metadata') + message = 'Creating '+repr(metadata_directory) + logger.info(message) + os.mkdir(metadata_directory) + except OSError, e: + if e.errno == errno.EEXIST: + pass + else: + raise + + # At this point the keystore is built and the 'role_info' dictionary + # looks something like this: + # {'keyids : [keyid1, keyid2] , 'threshold' : 2} + + # Generate the 'root.txt' metadata file. + # Newly created metadata start at version 1. The expiration date for the + # 'Root' role is extracted from the configuration file that was set, above, + # by the user. + root_keyids = role_info['root']['keyids'] + tuf.repo.signerlib.build_root_file(config_filepath, root_keyids, + metadata_directory, 1) + + # Generate the 'targets.txt' metadata file. + targets_keyids = role_info['targets']['keyids'] + expiration_date = tuf.formats.format_time(time.time()+TARGETS_EXPIRATION) + tuf.repo.signerlib.build_targets_file([targets_directory], targets_keyids, + metadata_directory, 1, + expiration_date) + + # Generate the 'release.txt' metadata file. + release_keyids = role_info['release']['keyids'] + expiration_date = tuf.formats.format_time(time.time()+RELEASE_EXPIRATION) + tuf.repo.signerlib.build_release_file(release_keyids, metadata_directory, + 1, expiration_date) + + # Generate the 'timestamp.txt' metadata file. + timestamp_keyids = role_info['timestamp']['keyids'] + expiration_date = tuf.formats.format_time(time.time()+TIMESTAMP_EXPIRATION) + tuf.repo.signerlib.build_timestamp_file(timestamp_keyids, metadata_directory, + 1, expiration_date) + + + + + +def open_repository(filepath): + """ + + Return a repository object that represents an existing repository. + + + filepath: + + + + + + + Repository object. + """ + + + + +def generate_and_write_rsa_keypair(filepath, bits=DEFAULT_RSA_KEY_BITS, + password=None): + """ + + Return a repository object that represents an existing repository. + + + filepath: + The public and private key files are saved to .pub, , + respectively. + + bits: + The number of bits of the generated RSA key. + + password: + + + + + + + """ + + # Does 'filepath' have the correct format? + # Raise 'tuf.FormatError' if there is a mismatch. + tuf.formats.PATH_SCHEMA.check_match(filepath) + + # Does 'bits' have the correct format? + tuf.formats.RSAKEYBITS_SCHEMA.check_match(bits) + + # If the caller does not provide a password argument, prompt for one. + if password is None: + message = 'Enter a password for the RSA key: ' + password = _get_password(message, confirm=True) + + # Does 'password' have the correct format? + tuf.formats.PASSWORD_SCHEMA.check_match(password) + + rsa_key = tuf.keys.generate_rsa_key(bits) + public = rsa_key['keyval']['public'] + private = rsa_key['keyval']['private'] + encrypted_pem = tuf.keys.create_rsa_encrypted_pem(private, password) + + # Write public key (i.e., 'public', which is in PEM format) to + # '.pub' + with open(filepath+'.pub', 'w') as file_object: + file_object.write(public) + + # Write the private key in encrypted PEM format to ''. + with open(filepath, 'w') as file_object: + file_object.write(encrypted_pem) + + + + + +def import_rsa_privatekey_from_file(filepath, password=None): + """ + + + + filepath: + file, an RSA encrypted PEM file. + + password: + The passphrase to decrypt 'filepath'. + + + + + + + """ + + # Does 'filepath' have the correct format? + # Raise 'tuf.FormatError' if there is a mismatch. + tuf.formats.PATH_SCHEMA.check_match(filepath) + + # If the caller does not provide a password argument, prompt for one. + if password is None: + message = 'Enter a password for the RSA key: ' + password = _get_password(message, confirm=True) + + # Does 'password' have the correct format? + tuf.formats.PASSWORD_SCHEMA.check_match(password) + + encrypted_pem = None + + with open(filepath, 'rb') as file_object: + encrypted_pem = file_object.read() + + rsa_key = tuf.keys.import_rsakey_from_encrypted_pem(encrypted_pem, password) + + return rsa_key + + + + + +def import_rsa_publickey_from_file(filepath): + """ + + + + filepath: + .pub file, an RSA PEM file. + + + + + + + """ + + # Does 'filepath' have the correct format? + # Raise 'tuf.FormatError' if there is a mismatch. + tuf.formats.PATH_SCHEMA.check_match(filepath) + + with open(filepath, 'rb') as file_object: + rsa_pubkey_pem = file_object.read() + + tuf.formats.PEMRSA_SCHEMA.check_match(rsa_pubkey_pem) + + rsa_key = tuf.keys.import_rsakey_from_encrypted_pem(encrypted_pem, password) + + return rsa_key + + + + +def get_metadata_filenames(metadata_directory=None): + """ + + Return a dictionary containing the filenames of the top-level roles. + If 'metadata_directory' is set to 'metadata', the dictionary + returned would contain: + + filenames = {'root': 'metadata/root.json', + 'targets': 'metadata/targets.json', + 'release': 'metadata/release.json', + 'timestamp': 'metadata/timestamp.json'} + + If the metadata directory is not set by the caller, the current + directory is used. + + + metadata_directory: + The directory containing the metadata files. + + + tuf.FormatError, if 'metadata_directory' is improperly formatted. + + + None. + + + A dictionary containing the expected filenames of the top-level + metadata files, such as 'root.json' and 'release.json'. + """ + + if metadata_directory is None: + metadata_directory = '.' + + # Does 'metadata_directory' have the correct format? + # Raise 'tuf.FormatError' if there is a mismatch. + tuf.formats.PATH_SCHEMA.check_match(metadata_directory) + + filenames = {} + filenames['root'] = os.path.join(metadata_directory, ROOT_FILENAME) + filenames['targets'] = os.path.join(metadata_directory, TARGETS_FILENAME) + filenames['release'] = os.path.join(metadata_directory, RELEASE_FILENAME) + filenames['timestamp'] = os.path.join(metadata_directory, TIMESTAMP_FILENAME) + + return filenames + + + + + +def get_metadata_file_info(filename): + """ + + Retrieve the file information for 'filename'. The object returned + conforms to 'tuf.formats.FILEINFO_SCHEMA'. The information + generated for 'filename' is stored in metadata files like 'targets.txt'. + The fileinfo object returned has the form: + fileinfo = {'length': 1024, + 'hashes': {'sha256': 1233dfba312, ...}, + 'custom': {...}} + + + filename: + The metadata file whose file information is needed. + + + tuf.FormatError, if 'filename' is improperly formatted. + + tuf.Error, if 'filename' doesn't exist. + + + The file is opened and information about the file is generated, + such as file size and its hash. + + + A dictionary conformant to 'tuf.formats.FILEINFO_SCHEMA'. This + dictionary contains the length, hashes, and custom data about + the 'filename' metadata file. + """ + + # Does 'filename' have the correct format? + # Raise 'tuf.FormatError' if there is a mismatch. + tuf.formats.PATH_SCHEMA.check_match(filename) + + if not os.path.isfile(filename): + message = repr(filename)+' is not a file.' + raise tuf.Error(message) + + # Note: 'filehashes' is a dictionary of the form + # {'sha256': 1233dfba312, ...}. 'custom' is an optional + # dictionary that a client might define to include additional + # file information, such as the file's author, version/revision + # numbers, etc. + filesize, filehashes = tuf.util.get_file_details(filename) + custom = None + + return tuf.formats.make_fileinfo(filesize, filehashes, custom) + + + + + +def generate_root_metadata(config_filepath, version): + """ + + Create the root metadata. 'config_filepath' is read + and the information contained in this file will be + used to generate the root metadata object. + + + config_filepath: + The file containing metadata information such as the keyids + of the top-level roles and expiration data. 'config_filepath' + is an absolute path. + + version: + The metadata version number. Clients use the version number to + determine if the downloaded version is newer than the one currently trusted. + + + tuf.FormatError, if the generated root metadata object could not + be generated with the correct format. + + tuf.Error, if an error is encountered while generating the root + metadata object. + + + 'config_filepath' is read and its contents stored. + + + A root 'signable' object conformant to 'tuf.formats.SIGNABLE_SCHEMA'. + """ + + # Does 'config_filepath' have the correct format? + # Raise 'tuf.FormatError' if the match fails. + tuf.formats.PATH_SCHEMA.check_match(config_filepath) + tuf.formats.METADATAVERSION_SCHEMA.check_match(version) + + # 'tuf.Error' raised if 'config_filepath' cannot be read. + config = read_config_file(config_filepath) + + # The role and key dictionaries to be saved in the root metadata object. + roledict = {} + keydict = {} + + # Extract the role, threshold, and keyid information from the config. + # The necessary role metadata is generated from this information. + for rolename in ['root', 'targets', 'release', 'timestamp']: + # If a top-level role is missing from the config, raise an exception. + if rolename not in config: + raise tuf.Error('No '+rolename+' section found in config file.') + keyids = [] + # Generate keys for the keyids listed by the role being processed. + for config_keyid in config[rolename]['keyids']: + key = tuf.repo.keystore.get_key(config_keyid) + + # If 'key' is an RSA key, it would conform to 'tuf.formats.RSAKEY_SCHEMA', + # and have the form: + # {'keytype': 'rsa', + # 'keyid': keyid, + # 'keyval': {'public': '-----BEGIN RSA PUBLIC KEY----- ...', + # 'private': '-----BEGIN RSA PRIVATE KEY----- ...'}} + keyid = key['keyid'] + # This appears to be a new keyid. Let's generate the key for it. + if keyid not in keydict: + if key['keytype'] in ['rsa', 'ed25519']: + keytype = key['keytype'] + keyval = key['keyval'] + keydict[keyid] = tuf.keys.create_in_metadata_format(keytype, keyval) + # This is not a recognized key. Raise an exception. + else: + raise tuf.Error('Unsupported keytype: '+keyid) + # Do we have a duplicate? Raise an exception if so. + if keyid in keyids: + raise tuf.Error('Same keyid listed twice: '+keyid) + # Add the loaded keyid for the role being processed. + keyids.append(keyid) + # Generate and store the role data belonging to the processed role. + role_metadata = tuf.formats.make_role_metadata(keyids, config[rolename]['threshold']) + roledict[rolename] = role_metadata + + # Extract the expiration information from the config. The root metadata + # object stores this expiration information in total seconds. + expiration = config['expiration'] + expiration_seconds = (expiration['seconds'] + 60 * expiration['minutes'] + + 3600 * expiration['hours'] + + 3600 * 24 * expiration['days']) + + # Generate the root metadata object. + root_metadata = tuf.formats.RootFile.make_metadata(version, expiration_seconds, + keydict, roledict) + + # Note: make_signable() returns the following dictionary: + # {'signed' : role_metadata, 'signatures' : []} + return tuf.formats.make_signable(root_metadata) + + + + + +def generate_targets_metadata(repository_directory, target_files, version, + expiration_date): + """ + + Generate the targets metadata object. The targets must exist at the same + path they should on the repo. 'target_files' is a list of targets. We're + not worrying about custom metadata at the moment. It is allowed to not + provide keys. + + + target_files: + The target files tracked by 'targets.txt'. 'target_files' is a list of + paths/directories of target files that are relative to the repository + (e.g., ['targets/file1.txt', ...]). If the target files are saved in + the root folder 'targets' on the repository, then 'targets' must be + included in the target paths. The repository does not have to name + this folder 'targets'. + + repository_directory: + The directory (absolute path) containing the metadata and target + directories. + + version: + The metadata version number. Clients use the version number to + determine if the downloaded version is newer than the one currently trusted. + + expiration_date: + The expiration date, in UTC, of the metadata file. + Conformant to 'tuf.formats.TIME_SCHEMA'. + + + tuf.FormatError, if an error occurred trying to generate the targets + metadata object. + + tuf.Error, if any of the target files could not be read. + + + The target files are read and file information generated about them. + + + A targets 'signable' object, conformant to 'tuf.formats.SIGNABLE_SCHEMA'. + """ + + # Do the arguments have the correct format. + # Raise 'tuf.FormatError' if there is a mismatch. + tuf.formats.PATHS_SCHEMA.check_match(target_files) + tuf.formats.PATH_SCHEMA.check_match(repository_directory) + tuf.formats.METADATAVERSION_SCHEMA.check_match(version) + tuf.formats.TIME_SCHEMA.check_match(expiration_date) + + filedict = {} + + repository_directory = check_directory(repository_directory) + + # Generate the file info for all the target files listed in 'target_files'. + for target in target_files: + # Strip 'targets/' from from 'target' and keep the rest (e.g., + # 'targets/more_targets/somefile.txt' -> 'more_targets/somefile.txt' + relative_targetpath = os.path.sep.join(target.split(os.path.sep)[1:]) + target_path = os.path.join(repository_directory, target) + if not os.path.exists(target_path): + message = repr(target_path)+' could not be read. Unable to generate '+\ + 'targets metadata.' + raise tuf.Error(message) + filedict[relative_targetpath] = get_metadata_file_info(target_path) + + # Generate the targets metadata object. + targets_metadata = tuf.formats.TargetsFile.make_metadata(version, + expiration_date, + filedict) + + return tuf.formats.make_signable(targets_metadata) + + + + + +def generate_release_metadata(metadata_directory, version, expiration_date): + """ + + Create the release metadata. The minimum metadata must exist + (i.e., 'root.txt' and 'targets.txt'). This will also look through + the 'targets/' directory in 'metadata_directory' and the resulting + release file will list all the delegated roles. + + + metadata_directory: + The directory containing the 'root.txt' and 'targets.txt' metadata + files. + + version: + The metadata version number. Clients use the version number to + determine if the downloaded version is newer than the one currently trusted. + + expiration_date: + The expiration date, in UTC, of the metadata file. + Conformant to 'tuf.formats.TIME_SCHEMA'. + + + tuf.FormatError, if 'metadata_directory' is improperly formatted. + + tuf.Error, if an error occurred trying to generate the release metadata + object. + + + The 'root.txt' and 'targets.txt' files are read. + + + The release 'signable' object, conformant to 'tuf.formats.SIGNABLE_SCHEMA'. + """ + + # Does 'metadata_directory' have the correct format? + # Raise 'tuf.FormatError' if there is a mismatch. + tuf.formats.PATH_SCHEMA.check_match(metadata_directory) + tuf.formats.METADATAVERSION_SCHEMA.check_match(version) + tuf.formats.TIME_SCHEMA.check_match(expiration_date) + + metadata_directory = check_directory(metadata_directory) + + # Retrieve the full filepath of the root and targets metadata file. + root_filename = os.path.join(metadata_directory, 'root.txt') + targets_filename = os.path.join(metadata_directory, 'targets.txt') + + # Retrieve the file info of 'root.txt' and 'targets.txt'. This file + # information includes data such as file length, hashes of the file, etc. + filedict = {} + filedict['root.txt'] = get_metadata_file_info(root_filename) + filedict['targets.txt'] = get_metadata_file_info(targets_filename) + + # Walk the 'targets/' directory and generate the file info for all + # the files listed there. This information is stored in the 'meta' + # field of the release metadata object. + targets_metadata = os.path.join(metadata_directory, 'targets') + if os.path.exists(targets_metadata) and os.path.isdir(targets_metadata): + for directory_path, junk, files in os.walk(targets_metadata): + # 'files' here is a list of target file names. + for basename in files: + metadata_path = os.path.join(directory_path, basename) + metadata_name = metadata_path[len(metadata_directory):].lstrip(os.path.sep) + filedict[metadata_name] = get_metadata_file_info(metadata_path) + + # Generate the release metadata object. + release_metadata = tuf.formats.ReleaseFile.make_metadata(version, + expiration_date, + filedict) + + return tuf.formats.make_signable(release_metadata) + + + + + +def generate_timestamp_metadata(release_filename, version, + expiration_date, compressions=()): + """ + + Generate the timestamp metadata object. The 'release.txt' file must exist. + + + release_filename: + The required filename of the release metadata file. + + version: + The metadata version number. Clients use the version number to + determine if the downloaded version is newer than the one currently trusted. + + expiration_date: + The expiration date, in UTC, of the metadata file. + Conformant to 'tuf.formats.TIME_SCHEMA'. + + compressions: + Compression extensions (e.g., 'gz'). If 'release.txt' is also saved in + compressed form, these compression extensions should be stored in + 'compressions' so the compressed timestamp files can be added to the + timestamp metadata object. + + + tuf.FormatError, if the generated timestamp metadata object could + not be formatted correctly. + + + None. + + + A timestamp 'signable' object, conformant to 'tuf.formats.SIGNABLE_SCHEMA'. + """ + + # Do the arguments have the correct format? + # Raise 'tuf.FormatError' if there is mismatch. + tuf.formats.PATH_SCHEMA.check_match(release_filename) + tuf.formats.METADATAVERSION_SCHEMA.check_match(version) + tuf.formats.TIME_SCHEMA.check_match(expiration_date) + + # Retrieve the file info for the release metadata file. + # This file information contains hashes, file length, custom data, etc. + fileinfo = {} + fileinfo['release.txt'] = get_metadata_file_info(release_filename) + + # Save the file info of the compressed versions of 'timestamp.txt'. + for file_extension in compressions: + compressed_filename = release_filename + '.' + file_extension + try: + compressed_fileinfo = get_metadata_file_info(compressed_filename) + except: + logger.warn('Could not get fileinfo about '+str(compressed_filename)) + else: + logger.info('Including fileinfo about '+str(compressed_filename)) + fileinfo['release.txt.' + file_extension] = compressed_fileinfo + + # Generate the timestamp metadata object. + timestamp_metadata = tuf.formats.TimestampFile.make_metadata(version, + expiration_date, + fileinfo) + + return tuf.formats.make_signable(timestamp_metadata) + + + + + +def write_metadata_file(metadata, filename, compression=None): + """ + + Create the file containing the metadata. + + + metadata: + The object that will be saved to 'filename'. + + filename: + The filename (absolute path) of the metadata to be + written (e.g., 'root.txt'). + + compression: + Specify an algorithm as a string to compress the file; otherwise, the + file will be left uncompressed. Available options are 'gz' (gzip). + + + tuf.FormatError, if the arguments are improperly formatted. + + tuf.Error, if 'filename' doesn't exist. + + Any other runtime (e.g. IO) exception. + + + The 'filename' file is created or overwritten if it exists. + + + The path to the written metadata file. + """ + + # Are the arguments properly formatted? + # Raise 'tuf.FormatError' if there is a mismatch. + tuf.formats.SIGNABLE_SCHEMA.check_match(metadata) + tuf.formats.PATH_SCHEMA.check_match(filename) + + # Verify 'filename' directory. + check_directory(os.path.dirname(filename)) + + # We choose a file-like object that depends on the compression algorithm. + file_object = None + # We may modify the filename, depending on the compression algorithm, so we + # store it separately. + filename_with_compression = filename + + # Take care of compression. + if compression is None: + logger.info('No compression for '+str(filename)) + file_object = open(filename_with_compression, 'w') + elif compression == 'gz': + logger.info('gzip compression for '+str(filename)) + filename_with_compression += '.gz' + file_object = gzip.open(filename_with_compression, 'w') + else: + raise tuf.FormatError('Unknown compression algorithm: '+str(compression)) + + try: + tuf.formats.PATH_SCHEMA.check_match(filename_with_compression) + logger.info('Writing to '+str(filename_with_compression)) + + # The metadata object is saved to 'file_object'. The keys + # of the objects are sorted and indentation is used. + json.dump(metadata, file_object, indent=1, sort_keys=True) + + file_object.write('\n') + except: + # Raise any runtime exception. + raise + else: + # Otherwise, return the written filename. + return filename_with_compression + finally: + # Always close the file. + file_object.close() + + + + + +def generate_and_save_rsa_key(keystore_directory, password, + bits=DEFAULT_RSA_KEY_BITS): + """ + + Generate a new RSA key and save it as an encrypted key file + to 'keystore_directory'. The encrypted key file is named: + .key. 'password' is used as the encryption key. + + + keystore_directory: + The directory to save the generated encrypted key file. + + password: + The password used to encrypt the RSA key file. + + bits: + The key size, or key length, of the RSA key. + If 'bits' is unspecified, a 3072-bit RSA key is generated, which is the + key size recommended by TUF, although 2048-bit keys are accepted + (minimum key size). + + + tuf.FormatError, if 'bits' or 'password' does not have the + correct format. + + tuf.CryptoError, if there was an error while generating the key. + + + An encrypted key file is created in 'keystore_directory'. + + + The generated RSA key. + The object returned conforms to 'tuf.formats.RSAKEY_SCHEMA' of the form: + {'keytype': 'rsa', + 'keyid': keyid, + 'keyval': {'public': '-----BEGIN RSA PUBLIC KEY----- ...', + 'private': '-----BEGIN RSA PRIVATE KEY----- ...'}} + """ + + # Are the arguments correctly formatted? + # Raise 'tuf.FormatError' if there is a mismatch. + tuf.formats.PATH_SCHEMA.check_match(keystore_directory) + tuf.formats.PASSWORD_SCHEMA.check_match(password) + + keystore_directory = check_directory(keystore_directory) + + # tuf.FormatError or tuf.CryptoError raised. + rsakey = tuf.keys.generate_rsa_key(bits) + + logger.info('Generated a new key: '+rsakey['keyid']) + + # Store the generated RSA key in the keystore and save the + # key file '.key' in 'keystore_directory'. + try: + tuf.repo.keystore.add_rsakey(rsakey, password) + tuf.repo.keystore.save_keystore_to_keyfiles(keystore_directory) + except tuf.FormatError: + raise + except tuf.KeyAlreadyExistsError: + logger.warn('The generated RSA key already exists.') + + return rsakey + + + + + +def check_directory(directory): + """ + + Ensure 'directory' is valid and it exists. This is not a security check, + but a way for the caller to determine the cause of an invalid directory + provided by the user. If the directory argument is valid, it is returned + normalized and as an absolute path. + + + directory: + The directory to check. + + + tuf.Error, if 'directory' could not be validated. + + tuf.FormatError, if 'directory' is not properly formatted. + + + None. + + + The normalized absolutized path of 'directory'. + """ + + # Does 'directory' have the correct format? + # Raise 'tuf.FormatError' if there is a mismatch. + tuf.formats.PATH_SCHEMA.check_match(directory) + + # Check if the directory exists. + if not os.path.isdir(directory): + raise tuf.Error(repr(directory)+' directory does not exist') + + directory = os.path.abspath(directory) + + return directory + + + + + +def build_delegated_role_file(delegated_targets_directory, delegated_keyids, + metadata_directory, delegation_metadata_directory, + delegation_role_name, version, expiration_date): + """ + + Build the targets metadata file using the signing keys in + 'delegated_keyids'. The generated metadata file is saved to + 'metadata_directory'. The target files located in 'targets_directory' will + be tracked by the built targets metadata. + + + delegated_targets_directory: + The directory (absolute path) containing all the delegated target + files. + + delegated_keyids: + The list of keyids to be used as the signing keys for the delegated + role file. + + metadata_directory: + The metadata directory (absolute path) containing all the metadata files. + + delegation_metadata_directory: + The location of the delegated role's metadata. + + delegation_role_name: + The delegated role's file name ending in '.txt'. Ex: 'role1.txt'. + + version: + The metadata version number. Clients use the version number to + determine if the downloaded version is newer than the one currently + trusted. + + expiration_date: + The expiration date, in UTC, of the metadata file. + Conformant to 'tuf.formats.TIME_SCHEMA'. + + + tuf.FormatError, if any of the arguments are improperly formatted. + + tuf.Error, if there was an error while building the targets file. + + + The targets metadata file is written to a file. + + + The path for the written targets metadata file. + """ + + # Do the arguments have the correct format? + # Raise 'tuf.FormatError' if there is a mismatch. + tuf.formats.PATH_SCHEMA.check_match(delegated_targets_directory) + tuf.formats.KEYIDS_SCHEMA.check_match(delegated_keyids) + tuf.formats.PATH_SCHEMA.check_match(metadata_directory) + tuf.formats.PATH_SCHEMA.check_match(delegation_metadata_directory) + tuf.formats.NAME_SCHEMA.check_match(delegation_role_name) + + # Check if 'targets_directory' and 'metadata_directory' are valid. + targets_directory = check_directory(delegated_targets_directory) + metadata_directory = check_directory(metadata_directory) + + repository_directory, junk = os.path.split(metadata_directory) + repository_directory_length = len(repository_directory) + + # Get the list of targets. + targets = [] + for root, directories, files in os.walk(targets_directory): + for target_file in files: + # Note: '+1' in the line below is there to remove '/'. + filename = os.path.join(root, target_file)[repository_directory_length+1:] + targets.append(filename) + + # Create the targets metadata object. + targets_metadata = generate_targets_metadata(repository_directory, targets, + version, expiration_date) + + # Sign it. + targets_filepath = os.path.join(delegation_metadata_directory, + delegation_role_name) + signable = sign_metadata(targets_metadata, delegated_keyids, targets_filepath) + + return write_metadata_file(signable, targets_filepath) + + + + + + +def accept_any_file(full_target_path): + """ + + Simply accept any given file. + + + full_target_path: + The absolute path to a target file. + + + None. + + + None. + + + True. + """ + + return True + + + + + +def get_targets(files_directory, recursive_walk=False, followlinks=True, + file_predicate=accept_any_file): + """ + + Walk the given files_directory to build a list of target files in it. + + + files_directory: + The path to a directory of target files. + + recursive_walk: + To recursively walk the directory, set recursive_walk=True. + + followlinks: + To follow symbolic links, set followlinks=True. + + file_predicate: + To filter a file based on a predicate, set file_predicate to a function + which accepts a full path to a file and returns a Boolean. + + + Python IO exceptions. + + + None. + + + A list of absolute paths to target files in the given files_directory. + """ + + targets = [] + + # FIXME: We need a way to tell Python 2, but not Python 3, to return + # filenames in Unicode; see #61 and: + # http://docs.python.org/2/howto/unicode.html#unicode-filenames + + for dirpath, dirnames, filenames in os.walk(files_directory, + followlinks=followlinks): + for filename in filenames: + full_target_path = os.path.join(dirpath, filename) + if file_predicate(full_target_path): + targets.append(full_target_path) + + # Prune the subdirectories to walk right now if we do not wish to + # recursively walk files_directory. + if recursive_walk is False: + del dirnames[:] + + return targets + + + + +if __name__ == '__main__': + # The interactive sessions of the documentation strings can + # be tested by running libtuftools.py as a standalone module. + # python libtuftools.py. + import doctest + doctest.testmod() From b4db0f1770c5d1ee25c27df30074d112030b5118 Mon Sep 17 00:00:00 2001 From: vladdd Date: Tue, 29 Oct 2013 15:23:26 -0400 Subject: [PATCH 62/95] [WIP] Continue libtuf.py implementation --- tuf/formats.py | 8 +- tuf/keys.py | 77 ++- tuf/{repo/libtuftools.py => libtuf.py} | 856 ++++++++++++++----------- tuf/roledb.py | 101 ++- 4 files changed, 655 insertions(+), 387 deletions(-) rename tuf/{repo/libtuftools.py => libtuf.py} (65%) diff --git a/tuf/formats.py b/tuf/formats.py index 3d3c0833..5b610f2a 100755 --- a/tuf/formats.py +++ b/tuf/formats.py @@ -491,14 +491,10 @@ def from_metadata(object): @staticmethod - def make_metadata(version, expiration_seconds, keydict, roledict): - # Is 'expiration_seconds' properly formatted? - # Raise 'tuf.FormatError' if not. - LENGTH_SCHEMA.check_match(expiration_seconds) - + def make_metadata(version, expiration_date, keydict, roledict): result = {'_type' : 'Root'} result['version'] = version - result['expires'] = format_time(time.time() + expiration_seconds) + result['expires'] = expiration_date result['keys'] = keydict result['roles'] = roledict diff --git a/tuf/keys.py b/tuf/keys.py index 37aeba9a..f0a94e87 100755 --- a/tuf/keys.py +++ b/tuf/keys.py @@ -784,7 +784,7 @@ def import_rsakey_from_encrypted_pem(encrypted_pem, password): >>> private = rsa_key['keyval']['private'] >>> passphrase = 'secret' >>> encrypted_pem = create_rsa_encrypted_pem(private, passphrase) - >>> rsa_key2 = import_rsakey_from_encrypted_pem(encrypted_pem, passphrase) + >>> rsa_key2 = import_key_from_encrypted_pem(encrypted_pem, passphrase) >>> rsa_key == rsa_key2 True @@ -867,6 +867,81 @@ def import_rsakey_from_encrypted_pem(encrypted_pem, password): +def format_rsakey_from_pem(pem): + """ + + Generate public and private RSA keys, with modulus length 'bits'. In + addition, a keyid identifier for the RSA key is generated. The object + returned conforms to 'tuf.formats.RSAKEY_SCHEMA' and has the + form: + {'keytype': 'rsa', + 'keyid': keyid, + 'keyval': {'public': '-----BEGIN RSA PUBLIC KEY----- ...', + 'private': ''}} + + The public and private keys are strings in PEM format. + + Although the PyCrypto crytography library called sets a 1024-bit minimum + key size, generate() enforces a minimum key size of 2048 bits. If 'bits' is + unspecified, a 3072-bit RSA key is generated, which is the key size + recommended by TUF. + + >>> + >>> + >>> + + + pem: + The key size, or key length, of the RSA key. 'bits' must be 2048, or + greater, and a multiple of 256. + + + tuf.FormatError, if 'bits' is improperly or invalid (i.e., not an integer + and not at least 2048). + + tuf.UnsupportedLibraryError, if any of the cryptography libraries specified + in 'tuf.conf.py' are unsupported or unavailable. + + ValueError, if an exception occurs after calling the RSA key generation + routine. 'bits' must be a multiple of 256. The 'ValueError' exception is + raised by the key generation function of the cryptography library called. + + + The RSA keys are generated by calling PyCrypto's + Crypto.PublicKey.RSA.generate(). + + + A dictionary containing the RSA keys and other identifying information. + Conforms to 'tuf.formats.RSAKEY_SCHEMA'. + """ + + # Does 'pem' have the correct format? + # This check will ensure 'pem' conforms to + # 'tuf.formats.PEMRSA_SCHEMA'. + tuf.formats.PEMRSA_SCHEMA.check_match(pem) + + # Begin building the RSA key dictionary. + rsakey_dict = {} + keytype = 'rsa' + public = pem + + # Generate the keyid of the RSA key. 'key_value' corresponds to the + # 'keyval' entry of the 'RSAKEY_SCHEMA' dictionary. The private key + # information is not included in the generation of the 'keyid' identifier. + key_value = {'public': public, + 'private': ''} + keyid = _get_keyid(keytype, key_value) + + rsakey_dict['keytype'] = keytype + rsakey_dict['keyid'] = keyid + rsakey_dict['keyval'] = key_value + + return rsakey_dict + + + + + def create_rsa_encrypted_pem(private_key, passphrase): """ diff --git a/tuf/repo/libtuftools.py b/tuf/libtuf.py similarity index 65% rename from tuf/repo/libtuftools.py rename to tuf/libtuf.py index f902f9ab..a0df30d9 100755 --- a/tuf/repo/libtuftools.py +++ b/tuf/libtuf.py @@ -1,6 +1,6 @@ """ - libtuftools.py + libtuf.py Vladimir Diaz @@ -14,14 +14,24 @@ """ -import getpass +import os +import errno import sys +import time +import getpass +import logging import tuf import tuf.formats +import tuf.keydb +import tuf.roledb import tuf.keys +import tuf.log +# See 'log.py' to learn how logging is handled in TUF. +logger = logging.getLogger('tuf.libtuf') + # Recommended RSA key sizes: # http://www.emc.com/emc-plus/rsa-labs/historical/twirl-and-rsa-key-size.htm#table1 # According to the document above, revised May 6, 2003, RSA keys of @@ -29,16 +39,23 @@ # are the recommended minimum and are good from the present through 2030. DEFAULT_RSA_KEY_BITS = 3072 -# The metadata filenames for the top-level roles. -ROOT_FILENAME = 'root.json' -TARGETS_FILENAME = 'targets.json' -RELEASE_FILENAME = 'release.json' -TIMESTAMP_FILENAME = 'timestamp.json' +# The metadata filenames of the top-level roles. +ROOT_FILENAME = 'root.txt' +TARGETS_FILENAME = 'targets.txt' +RELEASE_FILENAME = 'release.txt' +TIMESTAMP_FILENAME = 'timestamp.txt' -# Expiration date, in seconds, of the top-level roles (excluding 'Root'). -# The expiration time of the 'Root' role is set by the user. A metadata +# The targets and metadata directory names. +METADATA_DIRECTORY_NAME = 'metadata' +TARGETS_DIRECTORY_NAME = 'targets' + +# Expiration date delta, in seconds, of the top-level roles. A metadata # expiration date is set by taking the current time and adding the expiration # seconds listed below. + +# Initial 'root.txt' expiration time of 1 year. +ROOT_EXPIRATION = 31556900 + # Initial 'targets.txt' expiration time of 3 months. TARGETS_EXPIRATION = 7889230 @@ -63,18 +80,24 @@ class Repository: Repository object. """ - def __init__(self): - self.root - self.release - self.timestamp - self.targets + def __init__(self, repository_directory, metadata_directory, targets_directory): + + self.repository_directory = repository_directory + self.metadata_directory = metadata_directory + self.targets_directory = targets_directory + + # Set the top-level role objects. + self.root = Root() + self.release = Release() + self.timestamp = Timestamp() + self.targets = Targets() def write(self): """ - Write all the Metadata objects' JSON contents to the corresponding files. + Write all the JSON Metadata objects to their corresponding files. @@ -84,12 +107,119 @@ def write(self): """ + + # At this point the keystore is built and the 'role_info' dictionary + # looks something like this: + # {'keyids : [keyid1, keyid2] , 'threshold' : 2} + filenames = get_metadata_filenames(self.metadata_directory) + + # Generate the 'root.txt' metadata file. + # Newly created metadata start at version 1. The expiration date for the + # 'Root' role is extracted from the configuration file that was set, above, + # by the user. + root_keyids = tuf.roledb.get_role_keyids(self.root.rolename) + root_version = self.root.version + root_expiration = self.root.expiration + if root_expiration is None: + root_expiration = tuf.formats.format_time(time.time()+ROOT_EXPIRATION) + root_metadata = generate_root_metadata(root_version, root_expiration) + root_filename = filenames[ROOT_FILENAME] + write_metadata_file(root_metadata, root_filename, compression=None) + + # Generate the 'targets.txt' metadata file. + targets_keyids = tuf.roledb.get_role_keyids(self.targets.rolename) + targets_version = self.targets.version + targets_expiration = self.targets.expiration + targets_files = self.targets.target_files + if targets_expiration is None: + targets_expiration = \ + tuf.formats.format_time(time.time()+TARGETS_EXPIRATION) + targets_metadata = generate_targets_metadata(self.repository_directory, + targets_files, targets.version, + targets_expiration) + targets_filename = filenames[TARGETS_FILENAME] + write_metadata_file(targets_metadata, targets_filename, compression=None) + + # Generate the 'release.txt' metadata file. + release_keyids = tuf.roledb.get_role_keyids(self.release.rolename) + release_version = self.release.version + release_expiration = self.release.expiration + release_files = self.release.target_files + if release_expiration is None: + release_expiration = \ + tuf.formats.format_time(time.time()+RELEASE_EXPIRATION) + release_metadata = generate_release_metadata(self.metadata_directory, + release_version, + release_expiration) + release_filename = filenames[RELEASE_FILENAME] + write_metadata_file(release_metadata, release_filename, compression=None) + + # Generate the 'timestamp.txt' metadata file. + timestamp_keyids = tuf.roledb.get_role_keyids(self.timestamp.rolename) + timestamp_version = self.timestamp.version + timestamp_expiration = self.timestamp.expiration + timestamp_files = self.timestamp.target_files + if timestamp_expiration is None: + timestamp_expiration = \ + tuf.formats.format_time(time.time()+TIMESTAMP_EXPIRATION) + timestamp_metadata = generate_timestamp_metadata(release_filename, + timestamp_version, + timestamp_expiration, + compressions=()) + release_filename = filenames[RELEASE_FILENAME] + write_metadata_file(release_metadata, release_filename, compression=None) + + + + def get_filepaths_in_directory(files_directory, recursive_walk=False, + followlinks=True): + """ + + Walk the given files_directory to build a list of target files in it. + + + files_directory: + The path to a directory of target files. + + recursive_walk: + To recursively walk the directory, set recursive_walk=True. + + followlinks: + To follow symbolic links, set followlinks=True. + + + Python IO exceptions. + + + None. + + + A list of absolute paths to target files in the given files_directory. + """ + + targets = [] + + # FIXME: We need a way to tell Python 2, but not Python 3, to return + # filenames in Unicode; see #61 and: + # http://docs.python.org/2/howto/unicode.html#unicode-filenames + for dirpath, dirnames, filenames in os.walk(files_directory, + followlinks=followlinks): + for filename in filenames: + full_target_path = os.path.join(dirpath, filename) + targets.append(full_target_path) + + # Prune the subdirectories to walk right now if we do not wish to + # recursively walk files_directory. + if recursive_walk is False: + del dirnames[:] + + return targets -class Metadata: +class Metadata(object): """ Write all the Metadata objects' JSON contents to the corresponding files. @@ -104,19 +234,82 @@ class Metadata: """ def __init__(self): + self.rolename = None + self.version = 1 + self.threshold = 1 + self.keys = [] + self.signing_keys = [] + self.expiration = None + + + + def add_key(self, key): + """ + + + >>> + >>> + >>> + + + key: + tuf.formats.ANYKEY_SCHEMA + + + + + + + None. + """ - # This gets modified when methods are called and attributes changed. - self._JSON_contents + tuf.formats.ANYKEY_SCHEMA.check_match(key) - # Reference to Repository object. - self._repository - - self.expiration + try: + tuf.keydb.add_key(key) + except tuf.KeyAlreadyExistsError, e: + pass + + keyid = key['keyid'] + roleinfo = tuf.roledb.get_roleinfo(self.rolename) + roleinfo['keyids'].append(keyid) + tuf.roledb.update_roleinfo(self.rolename, roleinfo) + self.keys.append(keyid) - def refresh(self, object): + def set_threshold(self, threshold): + """ + + + >>> + >>> + >>> + + + threshold: + tuf.formats.THRESHOLD_SCHEMA + + + + + + + None. + """ + + tuf.formats.THRESHOLD_SCHEMA.check_match(threshold) + + roleinfo = tuf.roledb.get_roleinfo(self.rolename) + roleinfo['threshold'] = threshold + + tuf.roledb.update_roleinfo(self.rolename, roleinfo) + self.threshold = threshold + + + + def write_partial(self, object): """ @@ -158,16 +351,15 @@ class Root(Metadata): def __init__(self): - self.root_keys - self.root_threshold - self.timestamp_keys - self.release_keys - self.targets_keys - self.default_expiration - + super(Root, self).__init__() + + self.rolename = 'root' + + roleinfo = {'keyids': self.keys, 'threshold': self.threshold} + tuf.roledb.add_role(self.rolename, roleinfo) - def write(self): + def write_partial(self): pass @@ -192,10 +384,17 @@ class Timestamp(Metadata): """ def __init__(self): - pass + + super(Timestamp, self).__init__() + + self.rolename = 'timestamp' + + roleinfo = {'keyids': self.keys, 'threshold': self.threshold} + tuf.roledb.add_role(self.rolename, roleinfo) - def refresh(self): + + def write_partial(self): pass @@ -220,10 +419,17 @@ class Release(Metadata): """ def __init__(self): - pass + + super(Release, self).__init__() + + self.rolename = 'release' + + roleinfo = {'keyids': self.keys, 'threshold': self.threshold} + tuf.roledb.add_role(self.rolename, roleinfo) - def refresh(self): + + def write_partial(self): pass @@ -249,12 +455,18 @@ class Targets(Metadata): def __init__(self): - self.target_list - self.delegation_list + super(Targets, self).__init__() + self.rolename = 'targets' + self.target_files = [] + self.delegations = {} + + roleinfo = {'keyids': self.keys, 'threshold': self.threshold} + tuf.roledb.add_role(self.rolename, roleinfo) - def refresh(self): + + def write_partial(self): pass @@ -389,7 +601,47 @@ def _get_password(prompt='Password: ', confirm=False): return password else: print 'Mismatch; try again.' + + + + + +def _check_directory(directory): + """ + + Ensure 'directory' is valid and it exists. This is not a security check, + but a way for the caller to determine the cause of an invalid directory + provided by the user. If the directory argument is valid, it is returned + normalized and as an absolute path. + + + directory: + The directory to check. + + + tuf.Error, if 'directory' could not be validated. + + tuf.FormatError, if 'directory' is not properly formatted. + + + None. + + + The normalized absolutized path of 'directory'. + """ + + # Does 'directory' have the correct format? + # Raise 'tuf.FormatError' if there is a mismatch. + tuf.formats.PATH_SCHEMA.check_match(directory) + + # Check if the directory exists. + if not os.path.isdir(directory): + raise tuf.Error(repr(directory)+' directory does not exist') + + directory = os.path.abspath(directory) + return directory + @@ -398,7 +650,7 @@ def create_new_repository(repository_directory): """ Create a new repository with barebones metadata and return a Repository - object representing it. + object. repository_directory: @@ -408,58 +660,33 @@ def create_new_repository(repository_directory): - libtuftools.Repository object. + libtuf.Repository object. """ - - # Build the repository directories. + + tuf.formats + # Create the repository, metadata, and target directories. + repository_directory = os.path.abspath(repository_directory) metadata_directory = None targets_directory = None - - # Save the repository directory to the current directory, with - # an initial name of 'repository'. The repository maintainer - # may opt to rename this directory and should transfer it elsewhere, - # such as the webserver that will respond to TUF requests. - repository_directory = os.path.join(os.getcwd(), 'repository') - # Copy the files from the project directory to the repository's targets - # directory. The targets directory will hold all the individual - # target files. - targets_directory = os.path.join(repository_directory, 'targets') - temporary_directory = tempfile.mkdtemp() - temporary_targets = os.path.join(temporary_directory, 'targets') - shutil.copytree(project_directory, temporary_targets) - - # Remove the log file created by the tuf logger, if it exists. - # It might exist if the current directory was specified as the - # project directory on the command-line. - log_filename = tuf.log._DEFAULT_LOG_FILENAME - if log_filename in os.listdir(temporary_targets): - log_file = os.path.join(temporary_targets, log_filename) - os.remove(log_file) - - # Try to create the repository directory. + # Try to create 'repository_directory' if it does not exist. try: - os.mkdir(repository_directory) - # 'OSError' raised if the directory cannot be created. + os.makedirs(repository_directory) + # 'OSError' raised if the leaf directory already exists or cannot be created. except OSError, e: - message = 'Trying to create a new repository over an old repository '+\ - 'installation. Remove '+repr(repository_directory)+' before '+\ - 'trying again.' if e.errno == errno.EEXIST: - raise tuf.RepositoryError(message) + pass else: raise - - # Move the temporary targets directory into place now that repository - # directory has been created and remove previously created temporary - # directory. - shutil.move(temporary_targets, targets_directory) - os.rmdir(temporary_directory) + # + metadata_directory = \ + os.path.join(repository_directory, METADATA_DIRECTORY_NAME) + targets_directory = \ + os.path.join(repository_directory, TARGETS_DIRECTORY_NAME) - # Try to create the metadata directory that will hold all of the - # metadata files, such as 'root.txt' and 'release.txt'. + # Try to create the metadata directory that will hold all of the metadata + # files, such as 'root.txt' and 'release.txt'. try: - metadata_directory = os.path.join(repository_directory, 'metadata') message = 'Creating '+repr(metadata_directory) logger.info(message) os.mkdir(metadata_directory) @@ -468,43 +695,26 @@ def create_new_repository(repository_directory): pass else: raise - - # At this point the keystore is built and the 'role_info' dictionary - # looks something like this: - # {'keyids : [keyid1, keyid2] , 'threshold' : 2} - - # Generate the 'root.txt' metadata file. - # Newly created metadata start at version 1. The expiration date for the - # 'Root' role is extracted from the configuration file that was set, above, - # by the user. - root_keyids = role_info['root']['keyids'] - tuf.repo.signerlib.build_root_file(config_filepath, root_keyids, - metadata_directory, 1) - - # Generate the 'targets.txt' metadata file. - targets_keyids = role_info['targets']['keyids'] - expiration_date = tuf.formats.format_time(time.time()+TARGETS_EXPIRATION) - tuf.repo.signerlib.build_targets_file([targets_directory], targets_keyids, - metadata_directory, 1, - expiration_date) - - # Generate the 'release.txt' metadata file. - release_keyids = role_info['release']['keyids'] - expiration_date = tuf.formats.format_time(time.time()+RELEASE_EXPIRATION) - tuf.repo.signerlib.build_release_file(release_keyids, metadata_directory, - 1, expiration_date) - - # Generate the 'timestamp.txt' metadata file. - timestamp_keyids = role_info['timestamp']['keyids'] - expiration_date = tuf.formats.format_time(time.time()+TIMESTAMP_EXPIRATION) - tuf.repo.signerlib.build_timestamp_file(timestamp_keyids, metadata_directory, - 1, expiration_date) + + # Try to create the targets directory that will hold all of the target files. + try: + message = 'Creating '+repr(targets_directory) + logger.info(message) + os.mkdir(targets_directory) + except OSError, e: + if e.errno == errno.EEXIST: + pass + else: + raise + + repository = Repository(repository_directory, metadata_directory, + targets_directory) + + return repository - - -def open_repository(filepath): +def load_repository(filepath): """ Return a repository object that represents an existing repository. @@ -644,11 +854,42 @@ def import_rsa_publickey_from_file(filepath): with open(filepath, 'rb') as file_object: rsa_pubkey_pem = file_object.read() - tuf.formats.PEMRSA_SCHEMA.check_match(rsa_pubkey_pem) + rsakey_dict = tuf.keys.format_rsakey_from_pem(rsa_pubkey_pem) - rsa_key = tuf.keys.import_rsakey_from_encrypted_pem(encrypted_pem, password) + return rsakey_dict + + + + + +def expiration_date_utc(input_date_utc): + """ + + + + input_date_utc: + + + tuf.FormatError, if 'input_date_utc' is invalid. + + + None. + + + """ - return rsa_key + tuf.formats.TIME_SCHEMA.check_match(input_date_utc) + + try: + unix_timestamp = tuf.formats.parse_time(input_date_utc+' UTC') + except (tuf.FormatError, ValueError), e: + raise tuf.FormatError('Invalid date entered.') + + if unix_timestamp < time.time(): + message = 'The expiration date must occur after the current date.' + raise tuf.FormatError(message) + + return input_date_utc @@ -660,10 +901,10 @@ def get_metadata_filenames(metadata_directory=None): If 'metadata_directory' is set to 'metadata', the dictionary returned would contain: - filenames = {'root': 'metadata/root.json', - 'targets': 'metadata/targets.json', - 'release': 'metadata/release.json', - 'timestamp': 'metadata/timestamp.json'} + filenames = {'root': 'metadata/root.txt', + 'targets': 'metadata/targets.txt', + 'release': 'metadata/release.txt', + 'timestamp': 'metadata/timestamp.txt'} If the metadata directory is not set by the caller, the current directory is used. @@ -680,7 +921,7 @@ def get_metadata_filenames(metadata_directory=None): A dictionary containing the expected filenames of the top-level - metadata files, such as 'root.json' and 'release.json'. + metadata files, such as 'root.txt' and 'release.txt'. """ if metadata_directory is None: @@ -691,10 +932,10 @@ def get_metadata_filenames(metadata_directory=None): tuf.formats.PATH_SCHEMA.check_match(metadata_directory) filenames = {} - filenames['root'] = os.path.join(metadata_directory, ROOT_FILENAME) - filenames['targets'] = os.path.join(metadata_directory, TARGETS_FILENAME) - filenames['release'] = os.path.join(metadata_directory, RELEASE_FILENAME) - filenames['timestamp'] = os.path.join(metadata_directory, TIMESTAMP_FILENAME) + filenames[ROOT_FILENAME] = os.path.join(metadata_directory, ROOT_FILENAME) + filenames[TARGETS_FILENAME] = os.path.join(metadata_directory, TARGETS_FILENAME) + filenames[RELEASE_FILENAME] = os.path.join(metadata_directory, RELEASE_FILENAME) + filenames[TIMESTAMP_FILENAME] = os.path.join(metadata_directory, TIMESTAMP_FILENAME) return filenames @@ -754,22 +995,20 @@ def get_metadata_file_info(filename): -def generate_root_metadata(config_filepath, version): +def generate_root_metadata(version, expiration_date): """ - Create the root metadata. 'config_filepath' is read - and the information contained in this file will be - used to generate the root metadata object. + Create the root metadata. 'tuf.roledb' and 'tuf.roledb' are read and the + information returned by these modules are used to generate the root metadata + object. - config_filepath: - The file containing metadata information such as the keyids - of the top-level roles and expiration data. 'config_filepath' - is an absolute path. - version: The metadata version number. Clients use the version number to - determine if the downloaded version is newer than the one currently trusted. + determine if the downloaded version is newer than the one currently + trusted. + + expiration_date: tuf.FormatError, if the generated root metadata object could not @@ -779,19 +1018,16 @@ def generate_root_metadata(config_filepath, version): metadata object. - 'config_filepath' is read and its contents stored. + The contents of 'tuf.keydb' and 'tuf.roledb' are read. A root 'signable' object conformant to 'tuf.formats.SIGNABLE_SCHEMA'. """ - # Does 'config_filepath' have the correct format? - # Raise 'tuf.FormatError' if the match fails. - tuf.formats.PATH_SCHEMA.check_match(config_filepath) + # Do the arguments have the correct format? + # Raise 'tuf.FormatError' if any of the arguments are improperly formatted. tuf.formats.METADATAVERSION_SCHEMA.check_match(version) - - # 'tuf.Error' raised if 'config_filepath' cannot be read. - config = read_config_file(config_filepath) + tuf.formats.TIME_SCHEMA.check_match(expiration_date) # The role and key dictionaries to be saved in the root metadata object. roledict = {} @@ -800,13 +1036,15 @@ def generate_root_metadata(config_filepath, version): # Extract the role, threshold, and keyid information from the config. # The necessary role metadata is generated from this information. for rolename in ['root', 'targets', 'release', 'timestamp']: - # If a top-level role is missing from the config, raise an exception. - if rolename not in config: - raise tuf.Error('No '+rolename+' section found in config file.') + + # If a top-level role is missing from 'tuf.roledb', raise an exception. + if not tuf.roledb.role_exists(rolename): + raise tuf.Error(repr(rolename)+' not in "tuf.roledb".') + keyids = [] # Generate keys for the keyids listed by the role being processed. - for config_keyid in config[rolename]['keyids']: - key = tuf.repo.keystore.get_key(config_keyid) + for keyid in tuf.roledb.get_role_keyids(rolename): + key = tuf.keydb.get_key(keyid) # If 'key' is an RSA key, it would conform to 'tuf.formats.RSAKEY_SCHEMA', # and have the form: @@ -815,33 +1053,33 @@ def generate_root_metadata(config_filepath, version): # 'keyval': {'public': '-----BEGIN RSA PUBLIC KEY----- ...', # 'private': '-----BEGIN RSA PRIVATE KEY----- ...'}} keyid = key['keyid'] - # This appears to be a new keyid. Let's generate the key for it. if keyid not in keydict: + + # This appears to be a new keyid. Let's generate the key for it. if key['keytype'] in ['rsa', 'ed25519']: keytype = key['keytype'] keyval = key['keyval'] - keydict[keyid] = tuf.keys.create_in_metadata_format(keytype, keyval) + keydict[keyid] = \ + tuf.keys.format_keyval_to_metadata(keytype, keyval) + # This is not a recognized key. Raise an exception. else: raise tuf.Error('Unsupported keytype: '+keyid) - # Do we have a duplicate? Raise an exception if so. + + # Do we have a duplicate? if keyid in keyids: raise tuf.Error('Same keyid listed twice: '+keyid) + # Add the loaded keyid for the role being processed. keyids.append(keyid) + # Generate and store the role data belonging to the processed role. - role_metadata = tuf.formats.make_role_metadata(keyids, config[rolename]['threshold']) + role_threshold = tuf.roledb.get_role_threshold(rolename) + role_metadata = tuf.formats.make_role_metadata(keyids, role_threshold) roledict[rolename] = role_metadata - # Extract the expiration information from the config. The root metadata - # object stores this expiration information in total seconds. - expiration = config['expiration'] - expiration_seconds = (expiration['seconds'] + 60 * expiration['minutes'] + - 3600 * expiration['hours'] + - 3600 * 24 * expiration['days']) - # Generate the root metadata object. - root_metadata = tuf.formats.RootFile.make_metadata(version, expiration_seconds, + root_metadata = tuf.formats.RootFile.make_metadata(version, expiration_date, keydict, roledict) # Note: make_signable() returns the following dictionary: @@ -876,7 +1114,8 @@ def generate_targets_metadata(repository_directory, target_files, version, version: The metadata version number. Clients use the version number to - determine if the downloaded version is newer than the one currently trusted. + determine if the downloaded version is newer than the one currently + trusted. expiration_date: The expiration date, in UTC, of the metadata file. @@ -904,7 +1143,7 @@ def generate_targets_metadata(repository_directory, target_files, version, filedict = {} - repository_directory = check_directory(repository_directory) + repository_directory = _check_directory(repository_directory) # Generate the file info for all the target files listed in 'target_files'. for target in target_files: @@ -944,7 +1183,8 @@ def generate_release_metadata(metadata_directory, version, expiration_date): version: The metadata version number. Clients use the version number to - determine if the downloaded version is newer than the one currently trusted. + determine if the downloaded version is newer than the one currently + trusted. expiration_date: The expiration date, in UTC, of the metadata file. @@ -969,17 +1209,17 @@ def generate_release_metadata(metadata_directory, version, expiration_date): tuf.formats.METADATAVERSION_SCHEMA.check_match(version) tuf.formats.TIME_SCHEMA.check_match(expiration_date) - metadata_directory = check_directory(metadata_directory) + metadata_directory = _check_directory(metadata_directory) # Retrieve the full filepath of the root and targets metadata file. - root_filename = os.path.join(metadata_directory, 'root.txt') - targets_filename = os.path.join(metadata_directory, 'targets.txt') + root_filename = os.path.join(metadata_directory, ROOT_FILENAME) + targets_filename = os.path.join(metadata_directory, TARGETS_FILENAME) # Retrieve the file info of 'root.txt' and 'targets.txt'. This file # information includes data such as file length, hashes of the file, etc. filedict = {} - filedict['root.txt'] = get_metadata_file_info(root_filename) - filedict['targets.txt'] = get_metadata_file_info(targets_filename) + filedict[ROOT_FILENAME] = get_metadata_file_info(root_filename) + filedict[TARGETS_FILENAME] = get_metadata_file_info(targets_filename) # Walk the 'targets/' directory and generate the file info for all # the files listed there. This information is stored in the 'meta' @@ -1016,7 +1256,8 @@ def generate_timestamp_metadata(release_filename, version, version: The metadata version number. Clients use the version number to - determine if the downloaded version is newer than the one currently trusted. + determine if the downloaded version is newer than the one currently + trusted. expiration_date: The expiration date, in UTC, of the metadata file. @@ -1048,7 +1289,7 @@ def generate_timestamp_metadata(release_filename, version, # Retrieve the file info for the release metadata file. # This file information contains hashes, file length, custom data, etc. fileinfo = {} - fileinfo['release.txt'] = get_metadata_file_info(release_filename) + fileinfo[RELEASE_FILENAME] = get_metadata_file_info(release_filename) # Save the file info of the compressed versions of 'timestamp.txt'. for file_extension in compressions: @@ -1059,7 +1300,7 @@ def generate_timestamp_metadata(release_filename, version, logger.warn('Could not get fileinfo about '+str(compressed_filename)) else: logger.info('Including fileinfo about '+str(compressed_filename)) - fileinfo['release.txt.' + file_extension] = compressed_fileinfo + fileinfo[RELEASE_FILENAME+'.' + file_extension] = compressed_fileinfo # Generate the timestamp metadata object. timestamp_metadata = tuf.formats.TimestampFile.make_metadata(version, @@ -1072,6 +1313,81 @@ def generate_timestamp_metadata(release_filename, version, +def sign_metadata(metadata, keyids, filename): + """ + + Sign a metadata object. If any of the keyids have already signed the file, + the old signature will be replaced. The keys in 'keyids' must already be + loaded in the keystore. + + + metadata: + The metadata object to sign. For example, 'metadata' might correspond to + 'tuf.formats.ROOT_SCHEMA' or 'tuf.formats.TARGETS_SCHEMA'. + + keyids: + The keyids list of the signing keys. + + filename: + The intended filename of the signed metadata object. + For example, 'root.txt' or 'targets.txt'. This function + does NOT save the signed metadata to this filename. + + + tuf.FormatError, if a valid 'signable' object could not be generated. + + tuf.Error, if an invalid keytype was found in the keystore. + + + None. + + + A signable object conformant to 'tuf.formats.SIGNABLE_SCHEMA'. + """ + + # Does 'keyids' and 'filename' have the correct format? + # Raise 'tuf.FormatError' if there is a mismatch. + tuf.formats.KEYIDS_SCHEMA.check_match(keyids) + tuf.formats.PATH_SCHEMA.check_match(filename) + + # Make sure the metadata is in 'signable' format. That is, + # it contains a 'signatures' field containing the result + # of signing the 'signed' field of 'metadata' with each + # keyid of 'keyids'. + signable = tuf.formats.make_signable(metadata) + + # Sign the metadata with each keyid in 'keyids'. + for keyid in keyids: + # Load the signing key. + key = tuf.repo.keystore.get_key(keyid) + logger.info('Signing '+repr(filename)+' with '+key['keyid']) + + # Create a new signature list. If 'keyid' is encountered, + # do not add it to new list. + signatures = [] + for signature in signable['signatures']: + if not keyid == signature['keyid']: + signatures.append(signature) + signable['signatures'] = signatures + + # Generate the signature using the appropriate signing method. + if key['keytype'] == 'rsa': + signed = signable['signed'] + signature = tuf.sig.generate_rsa_signature(signed, key) + signable['signatures'].append(signature) + else: + raise tuf.Error('The keystore contains a key with an invalid key type') + + # Raise 'tuf.FormatError' if the resulting 'signable' is not formatted + # correctly. + tuf.formats.check_signable_object_format(signable) + + return signable + + + + + def write_metadata_file(metadata, filename, compression=None): """ @@ -1109,7 +1425,7 @@ def write_metadata_file(metadata, filename, compression=None): tuf.formats.PATH_SCHEMA.check_match(filename) # Verify 'filename' directory. - check_directory(os.path.dirname(filename)) + _check_directory(os.path.dirname(filename)) # We choose a file-like object that depends on the compression algorithm. file_object = None @@ -1151,113 +1467,6 @@ def write_metadata_file(metadata, filename, compression=None): -def generate_and_save_rsa_key(keystore_directory, password, - bits=DEFAULT_RSA_KEY_BITS): - """ - - Generate a new RSA key and save it as an encrypted key file - to 'keystore_directory'. The encrypted key file is named: - .key. 'password' is used as the encryption key. - - - keystore_directory: - The directory to save the generated encrypted key file. - - password: - The password used to encrypt the RSA key file. - - bits: - The key size, or key length, of the RSA key. - If 'bits' is unspecified, a 3072-bit RSA key is generated, which is the - key size recommended by TUF, although 2048-bit keys are accepted - (minimum key size). - - - tuf.FormatError, if 'bits' or 'password' does not have the - correct format. - - tuf.CryptoError, if there was an error while generating the key. - - - An encrypted key file is created in 'keystore_directory'. - - - The generated RSA key. - The object returned conforms to 'tuf.formats.RSAKEY_SCHEMA' of the form: - {'keytype': 'rsa', - 'keyid': keyid, - 'keyval': {'public': '-----BEGIN RSA PUBLIC KEY----- ...', - 'private': '-----BEGIN RSA PRIVATE KEY----- ...'}} - """ - - # Are the arguments correctly formatted? - # Raise 'tuf.FormatError' if there is a mismatch. - tuf.formats.PATH_SCHEMA.check_match(keystore_directory) - tuf.formats.PASSWORD_SCHEMA.check_match(password) - - keystore_directory = check_directory(keystore_directory) - - # tuf.FormatError or tuf.CryptoError raised. - rsakey = tuf.keys.generate_rsa_key(bits) - - logger.info('Generated a new key: '+rsakey['keyid']) - - # Store the generated RSA key in the keystore and save the - # key file '.key' in 'keystore_directory'. - try: - tuf.repo.keystore.add_rsakey(rsakey, password) - tuf.repo.keystore.save_keystore_to_keyfiles(keystore_directory) - except tuf.FormatError: - raise - except tuf.KeyAlreadyExistsError: - logger.warn('The generated RSA key already exists.') - - return rsakey - - - - - -def check_directory(directory): - """ - - Ensure 'directory' is valid and it exists. This is not a security check, - but a way for the caller to determine the cause of an invalid directory - provided by the user. If the directory argument is valid, it is returned - normalized and as an absolute path. - - - directory: - The directory to check. - - - tuf.Error, if 'directory' could not be validated. - - tuf.FormatError, if 'directory' is not properly formatted. - - - None. - - - The normalized absolutized path of 'directory'. - """ - - # Does 'directory' have the correct format? - # Raise 'tuf.FormatError' if there is a mismatch. - tuf.formats.PATH_SCHEMA.check_match(directory) - - # Check if the directory exists. - if not os.path.isdir(directory): - raise tuf.Error(repr(directory)+' directory does not exist') - - directory = os.path.abspath(directory) - - return directory - - - - - def build_delegated_role_file(delegated_targets_directory, delegated_keyids, metadata_directory, delegation_metadata_directory, delegation_role_name, version, expiration_date): @@ -1316,8 +1525,8 @@ def build_delegated_role_file(delegated_targets_directory, delegated_keyids, tuf.formats.NAME_SCHEMA.check_match(delegation_role_name) # Check if 'targets_directory' and 'metadata_directory' are valid. - targets_directory = check_directory(delegated_targets_directory) - metadata_directory = check_directory(metadata_directory) + targets_directory = _check_directory(delegated_targets_directory) + metadata_directory = _check_directory(metadata_directory) repository_directory, junk = os.path.split(metadata_directory) repository_directory_length = len(repository_directory) @@ -1343,90 +1552,9 @@ def build_delegated_role_file(delegated_targets_directory, delegated_keyids, - - - -def accept_any_file(full_target_path): - """ - - Simply accept any given file. - - - full_target_path: - The absolute path to a target file. - - - None. - - - None. - - - True. - """ - - return True - - - - - -def get_targets(files_directory, recursive_walk=False, followlinks=True, - file_predicate=accept_any_file): - """ - - Walk the given files_directory to build a list of target files in it. - - - files_directory: - The path to a directory of target files. - - recursive_walk: - To recursively walk the directory, set recursive_walk=True. - - followlinks: - To follow symbolic links, set followlinks=True. - - file_predicate: - To filter a file based on a predicate, set file_predicate to a function - which accepts a full path to a file and returns a Boolean. - - - Python IO exceptions. - - - None. - - - A list of absolute paths to target files in the given files_directory. - """ - - targets = [] - - # FIXME: We need a way to tell Python 2, but not Python 3, to return - # filenames in Unicode; see #61 and: - # http://docs.python.org/2/howto/unicode.html#unicode-filenames - - for dirpath, dirnames, filenames in os.walk(files_directory, - followlinks=followlinks): - for filename in filenames: - full_target_path = os.path.join(dirpath, filename) - if file_predicate(full_target_path): - targets.append(full_target_path) - - # Prune the subdirectories to walk right now if we do not wish to - # recursively walk files_directory. - if recursive_walk is False: - del dirnames[:] - - return targets - - - - if __name__ == '__main__': # The interactive sessions of the documentation strings can - # be tested by running libtuftools.py as a standalone module. - # python libtuftools.py. + # be tested by running libtuf.py as a standalone module. + # python libtuf.py. import doctest doctest.testmod() diff --git a/tuf/roledb.py b/tuf/roledb.py index 4f114dfc..6fa01bcf 100755 --- a/tuf/roledb.py +++ b/tuf/roledb.py @@ -26,7 +26,6 @@ {'rolename': {'keyids': ['34345df32093bd12...'], 'threshold': 1 'paths': ['path/to/role.txt']}} - """ import logging @@ -66,7 +65,6 @@ def create_roledb_from_root_metadata(root_metadata): None. - """ # Does 'root_metadata' have the correct object format? @@ -128,7 +126,6 @@ def add_role(rolename, roleinfo, require_parent=True): None. - """ # Does 'rolename' have the correct object format? @@ -163,6 +160,60 @@ def add_role(rolename, roleinfo, require_parent=True): +def update_roleinfo(rolename, roleinfo): + """ + + + + rolename: + An object representing the role's name, conformant to 'ROLENAME_SCHEMA' + (e.g., 'root', 'release', 'timestamp'). + + roleinfo: + An object representing the role associated with 'rolename', conformant to + ROLE_SCHEMA. 'roleinfo' has the form: + {'keyids': ['34345df32093bd12...'], + 'threshold': 1} + + The 'target' role has an additional 'paths' key. Its value is a list of + strings representing the path of the target file(s). + + + tuf.FormatError, if 'rolename' or 'roleinfo' does not have the correct + object format. + + tuf.UnknownRoleError, if 'rolename' cannot be found in the role database. + + tuf.InvalidNameError, if 'rolename' is improperly formatted. + + + The role database is modified. + + + None. + """ + + # Does 'rolename' have the correct object format? + # This check will ensure 'rolename' has the appropriate number of objects + # and object types, and that all dict keys are properly named. + tuf.formats.ROLENAME_SCHEMA.check_match(rolename) + + # Does 'roleinfo' have the correct object format? + tuf.formats.ROLE_SCHEMA.check_match(roleinfo) + + # Raises tuf.InvalidNameError. + _validate_rolename(rolename) + + if rolename not in _roledb_dict: + raise tuf.UnknownRoleError('Role does not exist: '+rolename) + + _roledb_dict[rolename] = roleinfo + + + + + + def get_parent_rolename(rolename): """ @@ -187,7 +238,6 @@ def get_parent_rolename(rolename): A string representing the name of the parent role. - """ # Raises tuf.FormatError, tuf.UnknownRoleError, or tuf.InvalidNameError. @@ -228,7 +278,6 @@ def get_all_parent_roles(rolename): A list containing all the parent roles. - """ # Raises tuf.FormatError, tuf.UnknownRoleError, or tuf.InvalidNameError. @@ -279,7 +328,6 @@ def role_exists(rolename): Boolean. True if 'rolename' is found in the role database, False otherwise. - """ # Raise tuf.FormatError, tuf.InvalidNameError. @@ -318,7 +366,6 @@ def remove_role(rolename): None. - """ # Raises tuf.FormatError, tuf.UnknownRoleError, or tuf.InvalidNameError. @@ -358,7 +405,6 @@ def remove_delegated_roles(rolename): None. - """ # Raises tuf.FormatError, tuf.UnknownRoleError, or tuf.InvalidNameError. @@ -390,7 +436,6 @@ def get_rolenames(): A list of rolenames. - """ return _roledb_dict.keys() @@ -399,6 +444,37 @@ def get_rolenames(): +def get_roleinfo(rolename): + """ + + Return the roleinfo of 'rolename'. + {'keyids': ['34345df32093bd12...'], + 'threshold': 1 + + + rolename: + + + tuf.FormatError, if 'rolename' is improperly formatted. + + tuf.UnknownRoleError, if 'rolename' does not exist. + + + None. + + + The roleinfo of 'rolename'. + """ + + # Raises tuf.FormatError, tuf.UnknownRoleError, or tuf.InvalidNameError. + _check_rolename(rolename) + + return _roledb_dict[rolename] + + + + + def get_role_keyids(rolename): """ @@ -426,7 +502,6 @@ def get_role_keyids(rolename): A list of keyids. - """ # Raises tuf.FormatError, tuf.UnknownRoleError, or tuf.InvalidNameError. @@ -462,7 +537,6 @@ def get_role_threshold(rolename): A threshold integer value. - """ # Raises tuf.FormatError, tuf.UnknownRoleError, or tuf.InvalidNameError. @@ -498,7 +572,6 @@ def get_role_paths(rolename): A list of paths. - """ # Raises tuf.FormatError, tuf.UnknownRoleError, or tuf.InvalidNameError. @@ -541,7 +614,6 @@ def get_delegated_rolenames(rolename): A list of rolenames. Note that the rolenames are *NOT* sorted by order of delegation! - """ # Raises tuf.FormatError, tuf.UnknownRoleError, or tuf.InvalidNameError. @@ -578,7 +650,6 @@ def clear_roledb(): None. - """ _roledb_dict.clear() @@ -593,7 +664,6 @@ def _check_rolename(rolename): 'tuf.formats.ROLENAME_SCHEMA', tuf.UnknownRoleError if 'rolename' is not found in the role database, or tuf.InvalidNameError if 'rolename' is not formatted correctly. - """ # Does 'rolename' have the correct object format? @@ -616,7 +686,6 @@ def _validate_rolename(rolename): Raise tuf.InvalidNameError if 'rolename' is not formatted correctly. It is assumed 'rolename' has been checked against 'ROLENAME_SCHEMA' prior to calling this function. - """ if rolename == '': From 298dc46ebaccf4b625db5be1e4c9b225ed932f94 Mon Sep 17 00:00:00 2001 From: vladdd Date: Wed, 30 Oct 2013 10:32:56 -0400 Subject: [PATCH 63/95] Remove roleinfo+Metadata.keys side effect Updating the Metadata.keys attribute should not modify the keyids of the role in tuf.roledb.py --- tuf/libtuf.py | 34 +++++++++++++++++----------------- 1 file changed, 17 insertions(+), 17 deletions(-) diff --git a/tuf/libtuf.py b/tuf/libtuf.py index a0df30d9..e888d99a 100755 --- a/tuf/libtuf.py +++ b/tuf/libtuf.py @@ -20,9 +20,11 @@ import time import getpass import logging +import json import tuf import tuf.formats +import tuf.util import tuf.keydb import tuf.roledb import tuf.keys @@ -112,6 +114,10 @@ def write(self): # looks something like this: # {'keyids : [keyid1, keyid2] , 'threshold' : 2} filenames = get_metadata_filenames(self.metadata_directory) + root_filename = filenames[ROOT_FILENAME] + targets_filename = filenames[TARGETS_FILENAME] + release_filename = filenames[RELEASE_FILENAME] + timestamp_filename = filenames[TIMESTAMP_FILENAME] # Generate the 'root.txt' metadata file. # Newly created metadata start at version 1. The expiration date for the @@ -123,7 +129,6 @@ def write(self): if root_expiration is None: root_expiration = tuf.formats.format_time(time.time()+ROOT_EXPIRATION) root_metadata = generate_root_metadata(root_version, root_expiration) - root_filename = filenames[ROOT_FILENAME] write_metadata_file(root_metadata, root_filename, compression=None) # Generate the 'targets.txt' metadata file. @@ -135,30 +140,26 @@ def write(self): targets_expiration = \ tuf.formats.format_time(time.time()+TARGETS_EXPIRATION) targets_metadata = generate_targets_metadata(self.repository_directory, - targets_files, targets.version, + targets_files, targets_version, targets_expiration) - targets_filename = filenames[TARGETS_FILENAME] write_metadata_file(targets_metadata, targets_filename, compression=None) # Generate the 'release.txt' metadata file. release_keyids = tuf.roledb.get_role_keyids(self.release.rolename) release_version = self.release.version release_expiration = self.release.expiration - release_files = self.release.target_files if release_expiration is None: release_expiration = \ tuf.formats.format_time(time.time()+RELEASE_EXPIRATION) release_metadata = generate_release_metadata(self.metadata_directory, release_version, release_expiration) - release_filename = filenames[RELEASE_FILENAME] write_metadata_file(release_metadata, release_filename, compression=None) # Generate the 'timestamp.txt' metadata file. timestamp_keyids = tuf.roledb.get_role_keyids(self.timestamp.rolename) timestamp_version = self.timestamp.version timestamp_expiration = self.timestamp.expiration - timestamp_files = self.timestamp.target_files if timestamp_expiration is None: timestamp_expiration = \ tuf.formats.format_time(time.time()+TIMESTAMP_EXPIRATION) @@ -166,8 +167,7 @@ def write(self): timestamp_version, timestamp_expiration, compressions=()) - release_filename = filenames[RELEASE_FILENAME] - write_metadata_file(release_metadata, release_filename, compression=None) + write_metadata_file(timestamp_metadata, timestamp_filename, compression=None) @@ -237,8 +237,9 @@ def __init__(self): self.rolename = None self.version = 1 self.threshold = 1 - self.keys = [] - self.signing_keys = [] + self.role_keys = [] + self.signing_keys = [] + self.signatures = [] self.expiration = None @@ -273,10 +274,9 @@ def add_key(self, key): keyid = key['keyid'] roleinfo = tuf.roledb.get_roleinfo(self.rolename) roleinfo['keyids'].append(keyid) - tuf.roledb.update_roleinfo(self.rolename, roleinfo) - self.keys.append(keyid) - + + self.role_keys.append(keyid) def set_threshold(self, threshold): @@ -355,7 +355,7 @@ def __init__(self): self.rolename = 'root' - roleinfo = {'keyids': self.keys, 'threshold': self.threshold} + roleinfo = {'keyids': [], 'threshold': 1} tuf.roledb.add_role(self.rolename, roleinfo) @@ -389,7 +389,7 @@ def __init__(self): self.rolename = 'timestamp' - roleinfo = {'keyids': self.keys, 'threshold': self.threshold} + roleinfo = {'keyids': [], 'threshold': 1} tuf.roledb.add_role(self.rolename, roleinfo) @@ -424,7 +424,7 @@ def __init__(self): self.rolename = 'release' - roleinfo = {'keyids': self.keys, 'threshold': self.threshold} + roleinfo = {'keyids': [], 'threshold': 1} tuf.roledb.add_role(self.rolename, roleinfo) @@ -460,7 +460,7 @@ def __init__(self): self.target_files = [] self.delegations = {} - roleinfo = {'keyids': self.keys, 'threshold': self.threshold} + roleinfo = {'keyids': [], 'threshold': 1} tuf.roledb.add_role(self.rolename, roleinfo) From 4bd0b6d07e164ec1f1906805c13c7f27378c9370 Mon Sep 17 00:00:00 2001 From: vladdd Date: Tue, 5 Nov 2013 08:22:21 -0500 Subject: [PATCH 64/95] Continue delegate() changes --- tuf/formats.py | 41 ++++- tuf/libtuf.py | 473 ++++++++++++++++++++++++++++++++++++++++++------- tuf/roledb.py | 46 +++-- 3 files changed, 475 insertions(+), 85 deletions(-) diff --git a/tuf/formats.py b/tuf/formats.py index 5b610f2a..1cbea598 100755 --- a/tuf/formats.py +++ b/tuf/formats.py @@ -74,8 +74,12 @@ # easily backwards compatible with clients that are already deployed. # A date in 'YYYY-MM-DD HH:MM:SS UTC' format. +# TODO: Support timestamps according to the ISO 8601 standard. TIME_SCHEMA = SCHEMA.RegularExpression(r'\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2} UTC') +# A date in 'YYYY-MM-DD HH:MM:SS UTC' format. +DATETIME_SCHEMA = SCHEMA.RegularExpression(r'\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}') + # A hexadecimal value in '23432df87ab..' format. HASH_SCHEMA = SCHEMA.RegularExpression(r'[a-fA-F0-9]+') @@ -176,6 +180,9 @@ keyid=KEYID_SCHEMA, keyval=KEYVAL_SCHEMA) +# A list of TUF key objects. +ANYKEYLIST_SCHEMA = SCHEMA.ListOf(ANYKEY_SCHEMA) + # An RSA TUF key. RSAKEY_SCHEMA = SCHEMA.Object( object_name='RSAKEY_SCHEMA', @@ -306,15 +313,16 @@ # A path hash prefix is a hexadecimal string. PATH_HASH_PREFIX_SCHEMA = HEX_SCHEMA + # A list of path hash prefixes. PATH_HASH_PREFIXES_SCHEMA = SCHEMA.ListOf(PATH_HASH_PREFIX_SCHEMA) # Role object in {'keyids': [keydids..], 'name': 'ABC', 'threshold': 1, -# 'paths':[filepaths..]} # format. +# 'paths':[filepaths..]} format. ROLE_SCHEMA = SCHEMA.Object( object_name='ROLE_SCHEMA', - keyids=SCHEMA.ListOf(KEYID_SCHEMA), name=SCHEMA.Optional(ROLENAME_SCHEMA), + keyids=SCHEMA.ListOf(KEYID_SCHEMA), threshold=THRESHOLD_SCHEMA, paths=SCHEMA.Optional(RELPATHS_SCHEMA), path_hash_prefixes=SCHEMA.Optional(PATH_HASH_PREFIXES_SCHEMA)) @@ -328,7 +336,24 @@ # Like ROLEDICT_SCHEMA, except that ROLE_SCHEMA instances are stored in order. ROLELIST_SCHEMA = SCHEMA.ListOf(ROLE_SCHEMA) -# The root: indicates root keys and top-level roles. +# The delegated roles of a Targets role (a parent). +DELEGATIONS_SCHEMA = SCHEMA.Object( + keys=KEYDICT_SCHEMA, + roles=ROLELIST_SCHEMA) + +# tuf.roledb +ROLEDB_SCHEMA = SCHEMA.Object( + object_name='ROLEDB_SCHEMA', + keyids=SCHEMA.ListOf(KEYID_SCHEMA), + threshold=THRESHOLD_SCHEMA, + signatures=SCHEMA.Optional(SCHEMA.ListOf(SIGNATURE_SCHEMA)), + paths=SCHEMA.Optional(RELPATHS_SCHEMA), + path_hash_prefixes=SCHEMA.Optional(PATH_HASH_PREFIXES_SCHEMA), + delegations=SCHEMA.Optional(SCHEMA.Object( + keys=KEYDICT_SCHEMA, + roles=SCHEMA.ListOf(ROLENAME_SCHEMA)))) + +# Root role: indicates root keys and top-level roles. ROOT_SCHEMA = SCHEMA.Object( object_name='ROOT_SCHEMA', _type=SCHEMA.String('Root'), @@ -337,18 +362,16 @@ keys=KEYDICT_SCHEMA, roles=ROLEDICT_SCHEMA) -# Targets. Indicates targets and delegates target paths to other roles. +# Targets role: Indicates targets and delegates target paths to other roles. TARGETS_SCHEMA = SCHEMA.Object( object_name='TARGETS_SCHEMA', _type=SCHEMA.String('Targets'), version=METADATAVERSION_SCHEMA, expires=TIME_SCHEMA, targets=FILEDICT_SCHEMA, - delegations=SCHEMA.Optional(SCHEMA.Object( - keys=KEYDICT_SCHEMA, - roles=ROLELIST_SCHEMA))) + delegations=SCHEMA.Optional(DELEGATIONS_SCHEMA)) -# A Release: indicates the latest versions of all metadata (except timestamp). +# Release role: indicates the latest versions of all metadata (except timestamp). RELEASE_SCHEMA = SCHEMA.Object( object_name='RELEASE_SCHEMA', _type=SCHEMA.String('Release'), @@ -356,7 +379,7 @@ expires=TIME_SCHEMA, meta=FILEDICT_SCHEMA) -# A Timestamp: indicates the latest version of the release file. +# Timestamp role: indicates the latest version of the release file. TIMESTAMP_SCHEMA = SCHEMA.Object( object_name='TIMESTAMP_SCHEMA', _type=SCHEMA.String('Timestamp'), diff --git a/tuf/libtuf.py b/tuf/libtuf.py index e888d99a..fe0e7acc 100755 --- a/tuf/libtuf.py +++ b/tuf/libtuf.py @@ -28,6 +28,7 @@ import tuf.keydb import tuf.roledb import tuf.keys +import tuf.sig import tuf.log @@ -67,14 +68,25 @@ # Initial 'timestamp.txt' expiration time of 1 day. TIMESTAMP_EXPIRATION = 86400 +# The suffix added to metadata filenames of partially written metadata. +# Partial metadata may contain insufficient number of signatures and require +# multiple repository maintainers to independently sign them. +PARTIAL_METADATA_SUFFIX = '.partial' -class Repository: + +class Repository(object): """ + repository_directory: + + metadata_directory: + + targets_directory: + tuf.FormatError, if the arguments are improperly formatted. @@ -83,19 +95,42 @@ class Repository: """ def __init__(self, repository_directory, metadata_directory, targets_directory): + + # Do the arguments have the correct format? + # Raise 'tuf.FormatError' if any of the arguments are improperly formatted. + tuf.formats.PATH_SCHEMA.check_match(repository_directory) + tuf.formats.PATH_SCHEMA.check_match(metadata_directory) + tuf.formats.PATH_SCHEMA.check_match(targets_directory) - self.repository_directory = repository_directory - self.metadata_directory = metadata_directory - self.targets_directory = targets_directory + self._repository_directory = repository_directory + self._metadata_directory = metadata_directory + self._targets_directory = targets_directory # Set the top-level role objects. self.root = Root() self.release = Release() self.timestamp = Timestamp() - self.targets = Targets() + self.targets = Targets('targets', self._targets_directory) + def status(self): + """ + + + + None. + + + + + + + """ + + # tuf.sig + + def write(self): """ @@ -113,7 +148,7 @@ def write(self): # At this point the keystore is built and the 'role_info' dictionary # looks something like this: # {'keyids : [keyid1, keyid2] , 'threshold' : 2} - filenames = get_metadata_filenames(self.metadata_directory) + filenames = get_metadata_filenames(self._metadata_directory) root_filename = filenames[ROOT_FILENAME] targets_filename = filenames[TARGETS_FILENAME] release_filename = filenames[RELEASE_FILENAME] @@ -139,7 +174,7 @@ def write(self): if targets_expiration is None: targets_expiration = \ tuf.formats.format_time(time.time()+TARGETS_EXPIRATION) - targets_metadata = generate_targets_metadata(self.repository_directory, + targets_metadata = generate_targets_metadata(self._repository_directory, targets_files, targets_version, targets_expiration) write_metadata_file(targets_metadata, targets_filename, compression=None) @@ -151,7 +186,7 @@ def write(self): if release_expiration is None: release_expiration = \ tuf.formats.format_time(time.time()+RELEASE_EXPIRATION) - release_metadata = generate_release_metadata(self.metadata_directory, + release_metadata = generate_release_metadata(self._metadata_directory, release_version, release_expiration) write_metadata_file(release_metadata, release_filename, compression=None) @@ -168,9 +203,27 @@ def write(self): timestamp_expiration, compressions=()) write_metadata_file(timestamp_metadata, timestamp_filename, compression=None) + + + + def partial_write(): + """ + + + + + + + + + + None. + """ - - + #PARTIAL_METADATA_SUFFIX + + + def get_filepaths_in_directory(files_directory, recursive_walk=False, followlinks=True): """ @@ -234,13 +287,14 @@ class Metadata(object): """ def __init__(self): - self.rolename = None - self.version = 1 - self.threshold = 1 - self.role_keys = [] - self.signing_keys = [] - self.signatures = [] - self.expiration = None + self._rolename = None + self._signing_keys = [] + + self._version = 1 + self._threshold = 1 + self._role_keys = [] + self._signatures = [] + self._expiration = None @@ -272,14 +326,26 @@ def add_key(self, key): pass keyid = key['keyid'] - roleinfo = tuf.roledb.get_roleinfo(self.rolename) + roleinfo = tuf.roledb.get_roleinfo(self._rolename) roleinfo['keyids'].append(keyid) - tuf.roledb.update_roleinfo(self.rolename, roleinfo) + tuf.roledb.update_roleinfo(self._rolename, roleinfo) - self.role_keys.append(keyid) - - - def set_threshold(self, threshold): + self._role_keys.append(keyid) + + + + @property + def threshold(self): + """ + + """ + + return self._threshold + + + + @threshold.setter + def threshold(self, threshold): """ @@ -292,8 +358,10 @@ def set_threshold(self, threshold): tuf.formats.THRESHOLD_SCHEMA + tuf.FormatError, if the argument is improperly formatted. + Modifies the threshold attribute of the Repository object. None. @@ -301,14 +369,81 @@ def set_threshold(self, threshold): tuf.formats.THRESHOLD_SCHEMA.check_match(threshold) - roleinfo = tuf.roledb.get_roleinfo(self.rolename) + roleinfo = tuf.roledb.get_roleinfo(self._rolename) roleinfo['threshold'] = threshold - tuf.roledb.update_roleinfo(self.rolename, roleinfo) - self.threshold = threshold + tuf.roledb.update_roleinfo(self._rolename, roleinfo) + self._threshold = threshold - + @property + def expiration(self): + """ + + + >>> + >>> + >>> + + + None. + + + None. + + + None. + + + The role's expiration datetime, conformant to tuf.formats.DATETIME_SCHEMA. + """ + + return self._expiration + + + + @expiration.setter + def expiration(self, expiration_datetime_utc): + """ + + + >>> + >>> + >>> + + + expiration_datetime_utc: + tuf.formats.DATETIME_SCHEMA + + + tuf.FormatError, if the argument is improperly formatted. + + + Modifies the expiration attribute of the Repository object. + + + None. + """ + + # Does 'expiration_datetime_utc' have the correct format? + # Raise 'tuf.FormatError' if there is a mismatch. + tuf.formats.DATETIME_SCHEMA.check_match(expiration_datetime_utc) + + expiration_datetime_utc = expiration_datetime_utc+' UTC' + try: + unix_timestamp = tuf.formats.parse_time(expiration_datetime_utc) + except (tuf.FormatError, ValueError), e: + message = 'Invalid datetime argument: '+repr(expiration_datetime_utc) + raise tuf.FormatError(message) + + if unix_timestamp < time.time(): + message = 'The expiration date must occur after the current date.' + raise tuf.FormatError(message) + + self._expiration = expiration_datetime_utc + + + def write_partial(self, object): """ @@ -353,10 +488,10 @@ def __init__(self): super(Root, self).__init__() - self.rolename = 'root' + self._rolename = 'root' roleinfo = {'keyids': [], 'threshold': 1} - tuf.roledb.add_role(self.rolename, roleinfo) + tuf.roledb.add_role(self._rolename, roleinfo) def write_partial(self): @@ -387,10 +522,10 @@ def __init__(self): super(Timestamp, self).__init__() - self.rolename = 'timestamp' + self._rolename = 'timestamp' roleinfo = {'keyids': [], 'threshold': 1} - tuf.roledb.add_role(self.rolename, roleinfo) + tuf.roledb.add_role(self._rolename, roleinfo) @@ -422,10 +557,10 @@ def __init__(self): super(Release, self).__init__() - self.rolename = 'release' + self._rolename = 'release' roleinfo = {'keyids': [], 'threshold': 1} - tuf.roledb.add_role(self.rolename, roleinfo) + tuf.roledb.add_role(self._rolename, roleinfo) @@ -445,24 +580,65 @@ class Targets(Metadata): >>> + targets_directory: + The targets directory of the Repository object. + tuf.FormatError, if the targets directory argument is improerly formatted. - + Mofifies the roleinfo of the targets role in 'tuf.roledb'. + + None. """ - def __init__(self): + def __init__(self, rolename, targets_directory): + + # Do the arguments have the correct format? + # Raise 'tuf.FormatError' if any are improperly formatted. + tuf.formats.ROLENAME_SCHEMA.check_match(rolename) + tuf.formats.PATH_SCHEMA.check_match(targets_directory) - super(Targets, self).__init__() - self.rolename = 'targets' - self.target_files = [] - self.delegations = {} + super(Targets, self).__init__() + self._targets_directory = targets_directory + self._rolename = rolename + self._target_files = [] + self._delegations = {} - roleinfo = {'keyids': [], 'threshold': 1} - tuf.roledb.add_role(self.rolename, roleinfo) + roleinfo = {'keyids': [], 'threshold': 1, 'paths': [], + 'path_hash_prefixes': [], + 'delegations': {'keys': {}, + 'roles': []}} + tuf.roledb.add_role(self._rolename, roleinfo) + + + + @property + def target_files(self): + """ + + + >>> + >>> + >>> + + + targets_directory: + The targets directory of the Repository object. + + + tuf.FormatError, if the targets directory argument is improerly formatted. + + + Mofifies the roleinfo of the targets role in 'tuf.roledb'. + + + None. + """ + + return self._target_files @@ -475,8 +651,11 @@ def write_partial(self): def add_target(self, filepath): """ - Takes a filepath relative to the targets directory. Regular expresssion - would be useful here. + Add a filepath (relative to 'self.targets_directory') to the Targets + object. This function does not actually create 'filepath' on the file + system. 'filepath' must already exist on the file system. + + Support regular expresssions? >>> >>> @@ -486,20 +665,116 @@ def add_target(self, filepath): filepath: + tuf.FormatError, if 'filepath' is improperly formatted. + Adds 'filepath' to this role's list of targets. This role's + 'tuf.roledb.py' is also updated. + None. """ + + # Does 'filepath' have the correct format? + # Raise 'tuf.FormatError' if there is a mismatch. + tuf.formats.PATH_SCHEMA.check_match(filepath) + + filepath = os.path.abspath(filepath) + + if not os.path.commonprefix([self._targets_directory, filepath]) == \ + self._targets_directory: + message = repr(filepath)+' is not under the Repository\'s targets '+\ + 'directory: '+repr(self._targets_directory) + raise tuf.Error(message) + + # TODO: Ensure is an allowed target path according to the parent's + # delegation. + """ + for child_target in actual_child_targets: + for allowed_child_path in allowed_child_paths: + prefix = os.path.commonprefix([child_target, allowed_child_path]) + if prefix == allowed_child_path: + break + """ + + # Add 'filepath' (i.e., relative to the targets directory) to the role's + # list of targets. + if os.path.isfile(filepath): + + # Update the role's 'tuf.roledb.py' entry and 'self._target_files'. + targets_directory_length = len(self._targets_directory) + roleinfo = tuf.roledb.get_roleinfo(self._rolename) + roleinfo['paths'].append(filepath[targets_directory_length+1:]) + tuf.roledb.update_roleinfo(self._rolename, roleinfo) + self._target_files.append(filepath) + + else: + message = repr(filepath)+' is not a valid file.' + raise tuf.Error(message) + + - + def add_targets(self, list_of_targets): + """ + + Add a list of target filepaths (all relative to 'self.targets_directory'). + This function does not actually create files on the file system. The + list of target must already exist. + + >>> + >>> + >>> + + + list_of_targets: + + + + + + + None. + """ + + # Does 'list_of_targets' have the correct format? + # Raise 'tuf.FormatError' if it is improperly formatted. + tuf.formats.RELPATHS_SCHEMA.check_match(list_of_targets) + + # TODO: Ensure list of targets allowed paths according to the parent's + # delegation. + + # TODO: Update the tuf.roledb entry. + targets_directory_length = len(self._targets_directory) + absolute_paths_list_of_targets = [] + relative_list_of_targets = [] + + for target in list_of_targets: + filepath = os.path.abspath(filepath) + + if not os.path.commonprefix([self._targets_directory, filepath]) == \ + self._targets_directory: + message = repr(filepath)+' is not under the Repository\'s targets '+\ + 'directory: '+repr(self._targets_directory) + raise tuf.Error(message) + if os.path.isfile(filepath): + absolute_paths_list_of_targets.append(filepath) + relative_list_of_targets.append(filepath[targets_directory_length+1:]) + else: + message = repr(filepath)+' is not a valid file.' + raise tuf.Error(message) + + # Update the role's target_files and its 'tuf.roledb.py' entry. + self._target_files.extend(absolute_list_of_targets) + roleinfo = tuf.roledb.get_roleinfo(self._rolename) + roleinfo['paths'].extend(relative_list_of_targets) + tuf.roledb.update_roleinfo(self._rolename, roleinfo) def remove_target(self, filepath): """ - Takes a filepath relative to the targets directory. Regular expresssion + Takes a filepath relative to the targets directory. Regular expresssions would be useful here. >>> @@ -510,17 +785,19 @@ def remove_target(self, filepath): filepath: + tuf.FormatError, if 'filepath' is improperly formatted. - + Modifies the target role's 'tuf.roledb.py' entry. + None. """ - def delegate(self, rolename, public_keys, targets): + def delegate(self, rolename, public_keys, list_of_targets, restricted_paths=None): """ 'targets' is a list of target filepaths, and can be empty. @@ -534,16 +811,64 @@ def delegate(self, rolename, public_keys, targets): public_keys: - targets: + list_of_targets: + + restricted_paths: + tuf.FormatError, if any of the arguments are improperly formatted. + A new Target object is created for 'rolename' that is accessible to the + caller (i.e., targets.unclaimed.). The 'tuf.keydb.py' and + 'tuf.roledb.py' stores are updated with 'public_keys'. + None. """ - - + + # Do the arguments have the correct format? + # Raise 'tuf.FormatError' if any of the arguments are improperly formatted. + tuf.formats.ROLENAME_SCHEMA.check_match(rolename) + tuf.formats.ANYKEYLIST_SCHEMA.check_match(public_keys) + tuf.formats.RELPATHS_SCHEMA.check_match(list_of_targets) + + # Validate 'list_of_targets' + # Ensure 'restricted_paths' is allowed by current role according to the + # parent. + + # Update the 'delegations' field of the current role. + + full_rolename = self._rolename+'/'+rolename + keyids = [] + + # Add public keys to tuf.keydb + for key in public_keys: + + try: + tuf.keydb.add_key(key) + except tuf.KeyAlreadyExistsError, e: + pass + + keyid = key['keyid'] + keyids.append(keyid) + + # Add role to 'tuf.roledb.py' + roleinfo = {'keyids': keyids, + 'threshold': 1, + 'signatures': [], + 'paths': list_of_targets, + 'delegations': {'keys': {}, + 'roles': []}} + tuf.roledb.add_role(full_rolename, roleinfo) + + new_targets_object = Targets(rolename, self._targets_directory) + + # Update 'new_targets_object' attributes. + for key in public_keys: + new_targets_object.add_key(key) + + self.__setattr__(rolename, new_targets_object) @@ -557,16 +882,27 @@ def revoke(self, rolename): rolename: + Not the full rolename ('Django' in 'targets/unclaimed/Django') of the role the + parent role (this role) wants to revoke. + tuf.FormatError, if 'rolename' is improperly formatted. + None. """ + + tuf.formats.ROLENAME_SCHEMA.check_match(rolename) + self.__delattr__(rolename) + # Remove from this Target's delegations dict. + # Remove from 'tuf.roledb.py' + + # Remove def _prompt(message, result_type=str): @@ -662,8 +998,9 @@ def create_new_repository(repository_directory): libtuf.Repository object. """ - - tuf.formats + + tuf.formats.PATH_SCHEMA.check_match(repository_directory) + # Create the repository, metadata, and target directories. repository_directory = os.path.abspath(repository_directory) metadata_directory = None @@ -678,6 +1015,7 @@ def create_new_repository(repository_directory): pass else: raise + # metadata_directory = \ os.path.join(repository_directory, METADATA_DIRECTORY_NAME) @@ -714,20 +1052,22 @@ def create_new_repository(repository_directory): -def load_repository(filepath): +def load_repository(repository_directory, partial_metadata_suffix=None): """ Return a repository object that represents an existing repository. - filepath: + repository_directory: + + partial_metadata_suffix: - Repository object. + libtuf.Repository object. """ @@ -862,26 +1202,27 @@ def import_rsa_publickey_from_file(filepath): -def expiration_date_utc(input_date_utc): +def expiration_datetime_utc(input_datetime_utc): """ + TODO: return 'input_datetime_utc' in ISO 8601 format. - input_date_utc: + input_datetime_utc: - tuf.FormatError, if 'input_date_utc' is invalid. + tuf.FormatError, if 'input_datetime_utc' is invalid. None. """ - - tuf.formats.TIME_SCHEMA.check_match(input_date_utc) - + if not tuf.formats.DATETIME_SCHEMA.matches(input_datetime_utc): + message = 'The datetime argument must be in "YYYY-MM-DD HH:MM:SS" format.' + raise tuf.FormatError(message) try: - unix_timestamp = tuf.formats.parse_time(input_date_utc+' UTC') + unix_timestamp = tuf.formats.parse_time(input_datetime_utc+' UTC') except (tuf.FormatError, ValueError), e: raise tuf.FormatError('Invalid date entered.') @@ -889,7 +1230,7 @@ def expiration_date_utc(input_date_utc): message = 'The expiration date must occur after the current date.' raise tuf.FormatError(message) - return input_date_utc + return input_datetime_utc+' UTC' @@ -998,7 +1339,7 @@ def get_metadata_file_info(filename): def generate_root_metadata(version, expiration_date): """ - Create the root metadata. 'tuf.roledb' and 'tuf.roledb' are read and the + Create the root metadata. 'tuf.roledb.py' and 'tuf.keydb.py' are read and the information returned by these modules are used to generate the root metadata object. @@ -1018,7 +1359,7 @@ def generate_root_metadata(version, expiration_date): metadata object. - The contents of 'tuf.keydb' and 'tuf.roledb' are read. + The contents of 'tuf.keydb.py' and 'tuf.roledb.py' are read. A root 'signable' object conformant to 'tuf.formats.SIGNABLE_SCHEMA'. @@ -1037,7 +1378,7 @@ def generate_root_metadata(version, expiration_date): # The necessary role metadata is generated from this information. for rolename in ['root', 'targets', 'release', 'timestamp']: - # If a top-level role is missing from 'tuf.roledb', raise an exception. + # If a top-level role is missing from 'tuf.roledb.py', raise an exception. if not tuf.roledb.role_exists(rolename): raise tuf.Error(repr(rolename)+' not in "tuf.roledb".') diff --git a/tuf/roledb.py b/tuf/roledb.py index 6fa01bcf..eabf50e9 100755 --- a/tuf/roledb.py +++ b/tuf/roledb.py @@ -23,9 +23,16 @@ The role database is a dictionary conformant to 'tuf.formats.ROLEDICT_SCHEMA' and has the form: + {'rolename': {'keyids': ['34345df32093bd12...'], 'threshold': 1 - 'paths': ['path/to/role.txt']}} + 'signatures': ['abcd3452...'], + 'paths': ['path/to/role.txt'], + 'path_hash_prefixes': ['ab34df13'], + 'delegations': {'keys': {}, 'roles': {}}} + + The 'name', 'paths', 'path_hash_prefixes', and 'delegations' dict keys are + optional. """ import logging @@ -102,10 +109,17 @@ def add_role(rolename, roleinfo, require_parent=True): roleinfo: An object representing the role associated with 'rolename', conformant to - ROLE_SCHEMA. 'roleinfo' has the form: + ROLEDB_SCHEMA. 'roleinfo' has the form: {'keyids': ['34345df32093bd12...'], - 'threshold': 1} + 'threshold': 1, + 'signatures': ['ab23dfc32'] + 'paths': ['path/to/target1', 'path/to/target2', ...], + 'path_hash_prefixes': ['a324fcd...', ...], + 'delegations': {'keys': } + The 'paths', 'path_hash_prefixes', and 'delegations' dict keys are + optional. + The 'target' role has an additional 'paths' key. Its value is a list of strings representing the path of the target file(s). @@ -134,7 +148,7 @@ def add_role(rolename, roleinfo, require_parent=True): tuf.formats.ROLENAME_SCHEMA.check_match(rolename) # Does 'roleinfo' have the correct object format? - tuf.formats.ROLE_SCHEMA.check_match(roleinfo) + tuf.formats.ROLEDB_SCHEMA.check_match(roleinfo) # Does 'require_parent' have the correct format? tuf.formats.TOGGLE_SCHEMA.check_match(require_parent) @@ -171,9 +185,14 @@ def update_roleinfo(rolename, roleinfo): roleinfo: An object representing the role associated with 'rolename', conformant to - ROLE_SCHEMA. 'roleinfo' has the form: - {'keyids': ['34345df32093bd12...'], - 'threshold': 1} + ROLEDB_SCHEMA. 'roleinfo' has the form: + {'name': 'role_name', + 'keyids': ['34345df32093bd12...'], + 'threshold': 1, + 'paths': ['path/to/target1', 'path/to/target2', ...], + 'path_hash_prefixes': ['a324fcd...', ...]} + + The 'name', 'paths', and 'path_hash_prefixes' dict keys are optional. The 'target' role has an additional 'paths' key. Its value is a list of strings representing the path of the target file(s). @@ -199,7 +218,7 @@ def update_roleinfo(rolename, roleinfo): tuf.formats.ROLENAME_SCHEMA.check_match(rolename) # Does 'roleinfo' have the correct object format? - tuf.formats.ROLE_SCHEMA.check_match(roleinfo) + tuf.formats.ROLEDB_SCHEMA.check_match(roleinfo) # Raises tuf.InvalidNameError. _validate_rolename(rolename) @@ -448,11 +467,18 @@ def get_roleinfo(rolename): """ Return the roleinfo of 'rolename'. - {'keyids': ['34345df32093bd12...'], - 'threshold': 1 + {'name': 'role_name', + 'keyids': ['34345df32093bd12...'], + 'threshold': 1, + 'paths': ['path/to/target1', 'path/to/target2', ...], + 'path_hash_prefixes': ['a324fcd...', ...]} + + The 'name', 'paths', and 'path_hash_prefixes' dict keys are optional. rolename: + An object representing the role's name, conformant to 'ROLENAME_SCHEMA' + (e.g., 'root', 'release', 'timestamp'). tuf.FormatError, if 'rolename' is improperly formatted. From 01deddfd18bccbe6361c6a26a2040a44a18896eb Mon Sep 17 00:00:00 2001 From: vladdd Date: Tue, 12 Nov 2013 15:00:26 -0500 Subject: [PATCH 65/95] Initial implementation of the repository tools. Delegations and repository loading now implemented. Updates to comments, docstrings, and a unit test needed. --- tuf/__init__.py | 8 + tuf/client/updater.py | 2 +- tuf/formats.py | 18 +- tuf/keydb.py | 9 +- tuf/libtuf.py | 1344 ++++++++++++++++++++++++++++++++++------- tuf/roledb.py | 34 +- tuf/schema.py | 2 +- tuf/util.py | 2 +- 8 files changed, 1191 insertions(+), 228 deletions(-) diff --git a/tuf/__init__.py b/tuf/__init__.py index 9a5b0f4e..9fc02fdf 100755 --- a/tuf/__init__.py +++ b/tuf/__init__.py @@ -116,6 +116,14 @@ class RepositoryError(Error): +class InsufficientKeysError(Error): + """Indicate that metadata role lacks a threshold of pubic or private keys.""" + pass + + + + + class ForbiddenTargetError(RepositoryError): """Indicate that a role signed for a target that it was not delegated to.""" pass diff --git a/tuf/client/updater.py b/tuf/client/updater.py index 25e8fb6f..7d250b68 100755 --- a/tuf/client/updater.py +++ b/tuf/client/updater.py @@ -487,7 +487,7 @@ def _import_delegations(self, parent_role): # and load them. for keyid, keyinfo in keys_info.items(): if keyinfo['keytype'] in ['rsa', 'ed25519']: - key = tuf.keys.create_from_metadata_format(keyinfo) + key = tuf.keys.format_metadata_to_key(keyinfo) # We specify the keyid to ensure that it's the correct keyid # for the key. diff --git a/tuf/formats.py b/tuf/formats.py index 1cbea598..426bcd1b 100755 --- a/tuf/formats.py +++ b/tuf/formats.py @@ -242,6 +242,9 @@ method=SIG_METHOD_SCHEMA, sig=HEX_SCHEMA) +# List of SIGNATURE_SCHEMA. +SIGNATURES_SCHEMA = SCHEMA.ListOf(SIGNATURE_SCHEMA) + # A schema holding the result of checking the signatures of a particular # 'SIGNABLE_SCHEMA' role. # For example, how many of the signatures for the 'Target' role are @@ -302,8 +305,7 @@ # 'backup_directory' entries. # see 'tuf/pushtools/pushtoolslib.py' and 'tuf/pushtools/receive/receive.py' RECEIVECONFIG_SCHEMA = SCHEMA.Object( - object_name='RECEIVECONFIG_SCHEMA', - general=SCHEMA.Object( + object_name='RECEIVECONFIG_SCHEMA', general=SCHEMA.Object( object_name='[general]', pushroots=SCHEMA.ListOf(PATH_SCHEMA), repository_directory=PATH_SCHEMA, @@ -341,17 +343,23 @@ keys=KEYDICT_SCHEMA, roles=ROLELIST_SCHEMA) +# The number of seconds before metadata expires. The minimum is 86400 seconds +# (= 1 day). This schema is used for the initial expiration date. Repository +# maintainers may later modify this value (TIME_SCHEMA). +EXPIRATION_SCHEMA = SCHEMA.Integer(lo=86400) + # tuf.roledb ROLEDB_SCHEMA = SCHEMA.Object( object_name='ROLEDB_SCHEMA', keyids=SCHEMA.ListOf(KEYID_SCHEMA), + signing_keyids=SCHEMA.Optional(SCHEMA.ListOf(KEYID_SCHEMA)), threshold=THRESHOLD_SCHEMA, + version=SCHEMA.Optional(METADATAVERSION_SCHEMA), + expires=SCHEMA.Optional(SCHEMA.OneOf([EXPIRATION_SCHEMA, TIME_SCHEMA])), signatures=SCHEMA.Optional(SCHEMA.ListOf(SIGNATURE_SCHEMA)), paths=SCHEMA.Optional(RELPATHS_SCHEMA), path_hash_prefixes=SCHEMA.Optional(PATH_HASH_PREFIXES_SCHEMA), - delegations=SCHEMA.Optional(SCHEMA.Object( - keys=KEYDICT_SCHEMA, - roles=SCHEMA.ListOf(ROLENAME_SCHEMA)))) + delegations=SCHEMA.Optional(DELEGATIONS_SCHEMA)) # Root role: indicates root keys and top-level roles. ROOT_SCHEMA = SCHEMA.Object( diff --git a/tuf/keydb.py b/tuf/keydb.py index 6cae0272..023a3184 100755 --- a/tuf/keydb.py +++ b/tuf/keydb.py @@ -28,6 +28,7 @@ """ import logging +import copy import tuf import tuf.formats @@ -85,7 +86,7 @@ def create_keydb_from_root_metadata(root_metadata): # 'key_metadata' is stored in 'KEY_SCHEMA' format. Call # create_from_metadata_format() to get the key in 'RSAKEY_SCHEMA' # format, which is the format expected by 'add_key()'. - rsakey_dict = tuf.keys.create_from_metadata_format(key_metadata) + rsakey_dict = tuf.keys.format_metadata_to_key(key_metadata) try: add_key(rsakey_dict, keyid) # 'tuf.Error' raised if keyid does not match the keyid for 'rsakey_dict'. @@ -111,7 +112,7 @@ def add_key(key_dict, keyid=None): key_dict: - A dictionary conformant to 'tuf.formats.RSAKEY_SCHEMA'. + A dictionary conformant to 'tuf.formats.ANYKEY_SCHEMA'. It has the form: {'keytype': 'rsa', 'keyid': keyid, @@ -158,7 +159,7 @@ def add_key(key_dict, keyid=None): if keyid in _keydb_dict: raise tuf.KeyAlreadyExistsError('Key: '+keyid) - _keydb_dict[keyid] = key_dict + _keydb_dict[keyid] = copy.deepcopy(key_dict) @@ -195,7 +196,7 @@ def get_key(keyid): # Return the key belonging to 'keyid', if found in the key database. try: - return _keydb_dict[keyid] + return copy.deepcopy(_keydb_dict[keyid]) except KeyError: raise tuf.UnknownKeyError('Key: '+keyid) diff --git a/tuf/libtuf.py b/tuf/libtuf.py index fe0e7acc..2e5da77b 100755 --- a/tuf/libtuf.py +++ b/tuf/libtuf.py @@ -14,12 +14,21 @@ """ +# 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 + import os import errno import sys import time import getpass import logging +import tempfile +import shutil import json import tuf @@ -49,9 +58,12 @@ TIMESTAMP_FILENAME = 'timestamp.txt' # The targets and metadata directory names. -METADATA_DIRECTORY_NAME = 'metadata' +METADATA_DIRECTORY_NAME = 'metadata.staged' TARGETS_DIRECTORY_NAME = 'targets' +# The file extension of TUF metadata files. +METADATA_EXTENSION = '.txt' + # Expiration date delta, in seconds, of the top-level roles. A metadata # expiration date is set by taking the current time and adding the expiration # seconds listed below. @@ -71,7 +83,7 @@ # The suffix added to metadata filenames of partially written metadata. # Partial metadata may contain insufficient number of signatures and require # multiple repository maintainers to independently sign them. -PARTIAL_METADATA_SUFFIX = '.partial' +#PARTIAL_METADATA_SUFFIX = '.partial' class Repository(object): @@ -110,7 +122,7 @@ def __init__(self, repository_directory, metadata_directory, targets_directory): self.root = Root() self.release = Release() self.timestamp = Timestamp() - self.targets = Targets('targets', self._targets_directory) + self.targets = Targets(self._targets_directory, 'targets') @@ -126,105 +138,292 @@ def status(self): + None. """ + + root_roleinfo = tuf.roledb.get_roleinfo('root') + targets_roleinfo = tuf.roledb.get_roleinfo('targets') + release_roleinfo = tuf.roledb.get_roleinfo('release') + timestamp_roleinfo = tuf.roledb.get_roleinfo('timestamp') + temp_repository_directory = None + + try: + temp_repository_directory = tempfile.mkdtemp() + metadata_directory = os.path.join(temp_repository_directory, + METADATA_DIRECTORY_NAME) + os.mkdir(metadata_directory) + + filenames = get_metadata_filenames(metadata_directory) + root_filename = filenames[ROOT_FILENAME] + targets_filename = filenames[TARGETS_FILENAME] + release_filename = filenames[RELEASE_FILENAME] + timestamp_filename = filenames[TIMESTAMP_FILENAME] - # tuf.sig - - - def write(self): + # Delegated roles. + delegated_roles = tuf.roledb.get_delegated_rolenames('targets') + insufficient_keys = [] + insufficient_signatures = [] + for delegated_role in delegated_roles: + try: + _check_role_keys(delegated_role) + except tuf.InsufficientKeysError, e: + insufficient_keys.append(delegated_role) + continue + + roleinfo = tuf.roledb.get_roleinfo(delegated_role) + try: + write_delegated_metadata_file(temp_repository_directory, + self._targets_directory, + delegated_role, + roleinfo['version'], + roleinfo['expires'], + roleinfo['signing_keyids'], + roleinfo['paths'], + roleinfo['delegations'], + roleinfo['signatures'], + write_partial=False) + except tuf.Error, e: + insufficient_signatures.append(delegated_role) + if len(insufficient_keys): + message = 'Delegated roles with insufficient keys: '+ \ + repr(insufficient_keys) + print(message) + return + + if len(insufficient_signatures): + message = 'Delegated roles with insufficient signatures: '+ \ + repr(insufficient_signatures) + print(message) + return + + # Root role. + try: + _check_role_keys(self.root.rolename) + except tuf.InsufficientKeysError, e: + print(str(e)) + return + + root_metadata = generate_root_metadata(root_roleinfo['version'], + root_roleinfo['expires']) + signed_root = sign_metadata(root_metadata, root_roleinfo['signing_keyids'], + root_filename) + signed_root['signatures'].extend(root_roleinfo['signatures']) + root_status = tuf.sig.get_signature_status(signed_root, 'root') + message = repr(self.root.rolename)+' role contains '+ \ + repr(len(root_status['good_sigs']))+' / '+ \ + repr(root_status['threshold'])+' signatures.' + print(message) + + if tuf.sig.verify(signed_root, 'root'): + write_metadata_file(signed_root, root_filename, compression=None) + else: + return + + + # Targets role. + try: + _check_role_keys(self.targets.rolename) + except tuf.InsufficientKeysError, e: + print(str(e)) + return + + targets_metadata = generate_targets_metadata(self._targets_directory, + targets_roleinfo['paths'], + targets_roleinfo['version'], + targets_roleinfo['expires'], + targets_roleinfo['delegations']) + signed_targets = sign_metadata(targets_metadata, + targets_roleinfo['signing_keyids'], + targets_filename) + signed_targets['signatures'].extend(targets_roleinfo['signatures']) + targets_status = tuf.sig.get_signature_status(signed_targets, 'targets') + message = repr(self.targets.rolename)+' role contains '+ \ + repr(len(targets_status['good_sigs']))+' / '+ \ + repr(targets_status['threshold'])+' signatures.' + print(message) + + if tuf.sig.verify(signed_targets, 'targets'): + write_metadata_file(signed_targets, targets_filename, compression=None) + else: + return + + + # Release role. + try: + _check_role_keys(self.release.rolename) + except tuf.InsufficientKeysError, e: + print(str(e)) + return + + release_metadata = generate_release_metadata(metadata_directory, + release_roleinfo['version'], + release_roleinfo['expires']) + signed_release = sign_metadata(release_metadata, + release_roleinfo['signing_keyids'], + release_filename) + signed_release['signatures'].extend(release_roleinfo['signatures']) + release_status = tuf.sig.get_signature_status(signed_release, 'release') + + message = repr(self.release.rolename)+' role contains '+ \ + repr(len(release_status['good_sigs']))+' / '+ \ + repr(release_status['threshold'])+' signatures.' + print(message) + if tuf.sig.verify(signed_release, 'release'): + write_metadata_file(signed_release, release_filename, compression=None) + else: + return + + # Timestamp role. + try: + _check_role_keys(self.timestamp.rolename) + except tuf.InsufficientKeysError, e: + print(str(e)) + return + + timestamp_metadata = generate_timestamp_metadata(release_filename, + timestamp_roleinfo['version'], + timestamp_roleinfo['expires'], + compressions=()) + + signed_timestamp= sign_metadata(timestamp_metadata, + timestamp_roleinfo['signing_keyids'], + release_filename) + signed_timestamp['signatures'].extend(timestamp_roleinfo['signatures']) + timestamp_status = tuf.sig.get_signature_status(signed_timestamp, + 'timestamp') + + message = repr(self.timestamp.rolename)+' role contains '+ \ + repr(len(timestamp_status['good_sigs']))+' / '+ \ + repr(timestamp_status['threshold'])+' signatures.' + print(message) + if tuf.sig.verify(signed_timestamp, 'timestamp'): + write_metadata_file(signed_timestamp, timestamp_filename, + compression=None) + else: + return + + finally: + shutil.rmtree(temp_repository_directory, ignore_errors=True) + + + + def write(self, write_partial=False): """ Write all the JSON Metadata objects to their corresponding files. + None. + tuf.RepositoryError, if any of the top-level roles do not have a minimum + threshold of signatures. + Creates metadata files in the repository's metadata directory. + None. """ - # At this point the keystore is built and the 'role_info' dictionary - # looks something like this: - # {'keyids : [keyid1, keyid2] , 'threshold' : 2} + # Does 'partial' have the correct format? + # Raise 'tuf.FormatError' if 'partial' is improperly formatted. + tuf.formats.TOGGLE_SCHEMA.check_match(write_partial) + + # At this point the tuf.keydb and tuf.roledb stores must be fully + # populated, otherwise write() throwns a 'tuf.Repository' exception if + # any of the top-level roles are missing signatures, keys, etc. filenames = get_metadata_filenames(self._metadata_directory) root_filename = filenames[ROOT_FILENAME] targets_filename = filenames[TARGETS_FILENAME] release_filename = filenames[RELEASE_FILENAME] timestamp_filename = filenames[TIMESTAMP_FILENAME] + # Write the metadata files of all the delegated roles. + delegated_roles = tuf.roledb.get_delegated_rolenames('targets') + for delegated_role in delegated_roles: + roleinfo = tuf.roledb.get_roleinfo(delegated_role) + + write_delegated_metadata_file(self._repository_directory, + self._targets_directory, + delegated_role, + roleinfo['version'], roleinfo['expires'], + roleinfo['signing_keyids'], + roleinfo['paths'], + roleinfo['delegations'], + roleinfo['signatures'], + write_partial) + + # Generate the 'root.txt' metadata file. - # Newly created metadata start at version 1. The expiration date for the - # 'Root' role is extracted from the configuration file that was set, above, - # by the user. - root_keyids = tuf.roledb.get_role_keyids(self.root.rolename) - root_version = self.root.version - root_expiration = self.root.expiration - if root_expiration is None: - root_expiration = tuf.formats.format_time(time.time()+ROOT_EXPIRATION) - root_metadata = generate_root_metadata(root_version, root_expiration) - write_metadata_file(root_metadata, root_filename, compression=None) + roleinfo = tuf.roledb.get_roleinfo('root') + + root_metadata = generate_root_metadata(roleinfo['version'], + roleinfo['expires']) + signed_root = sign_metadata(root_metadata, roleinfo['signing_keyids'], + root_filename) + signed_root['signatures'].extend(roleinfo['signatures']) + if tuf.sig.verify(signed_root, 'root') or write_partial: + write_metadata_file(signed_root, root_filename, compression=None) + else: + message = 'Not enough signatures for '+repr(root_filename) + raise tuf.Error(message) + # Generate the 'targets.txt' metadata file. - targets_keyids = tuf.roledb.get_role_keyids(self.targets.rolename) - targets_version = self.targets.version - targets_expiration = self.targets.expiration - targets_files = self.targets.target_files - if targets_expiration is None: - targets_expiration = \ - tuf.formats.format_time(time.time()+TARGETS_EXPIRATION) - targets_metadata = generate_targets_metadata(self._repository_directory, - targets_files, targets_version, - targets_expiration) - write_metadata_file(targets_metadata, targets_filename, compression=None) + roleinfo = tuf.roledb.get_roleinfo('targets') + targets_metadata = generate_targets_metadata(self._targets_directory, + roleinfo['paths'], + roleinfo['version'], + roleinfo['expires'], + roleinfo['delegations']) + signed_targets = sign_metadata(targets_metadata, roleinfo['signing_keyids'], + targets_filename) + signed_targets['signatures'].extend(roleinfo['signatures']) + + if tuf.sig.verify(signed_targets, 'targets') or write_partial: + write_metadata_file(signed_targets, targets_filename, compression=None) + else: + message = 'Not enough signatures for '+repr(targets_filename) + raise tuf.Error(message) + # Generate the 'release.txt' metadata file. - release_keyids = tuf.roledb.get_role_keyids(self.release.rolename) - release_version = self.release.version - release_expiration = self.release.expiration - if release_expiration is None: - release_expiration = \ - tuf.formats.format_time(time.time()+RELEASE_EXPIRATION) + roleinfo = tuf.roledb.get_roleinfo('release') release_metadata = generate_release_metadata(self._metadata_directory, - release_version, - release_expiration) - write_metadata_file(release_metadata, release_filename, compression=None) + roleinfo['version'], + roleinfo['expires']) + signed_release = sign_metadata(release_metadata, roleinfo['signing_keyids'], + release_filename) + signed_release['signatures'].extend(roleinfo['signatures']) + + if tuf.sig.verify(signed_release, 'release') or write_partial: + write_metadata_file(signed_release, release_filename, compression=None) + else: + message = 'Not enough signatures for '+repr(release_filename) + raise tuf.Error(message) + # Generate the 'timestamp.txt' metadata file. - timestamp_keyids = tuf.roledb.get_role_keyids(self.timestamp.rolename) - timestamp_version = self.timestamp.version - timestamp_expiration = self.timestamp.expiration - if timestamp_expiration is None: - timestamp_expiration = \ - tuf.formats.format_time(time.time()+TIMESTAMP_EXPIRATION) + roleinfo = tuf.roledb.get_roleinfo('timestamp') timestamp_metadata = generate_timestamp_metadata(release_filename, - timestamp_version, - timestamp_expiration, + roleinfo['version'], + roleinfo['expires'], compressions=()) - write_metadata_file(timestamp_metadata, timestamp_filename, compression=None) - - - - def partial_write(): - """ - - - - - - - - - - None. - """ + signed_timestamp = sign_metadata(timestamp_metadata, + roleinfo['signing_keyids'], + timestamp_filename) + signed_timestamp['signatures'].extend(roleinfo['signatures']) - #PARTIAL_METADATA_SUFFIX + if tuf.sig.verify(signed_timestamp, 'timestamp') or write_partial: + write_metadata_file(signed_timestamp, timestamp_filename, compression=None) + else: + message = 'Not enough signatures for '+repr(timestamp_filename) + raise tuf.Error(message) - def get_filepaths_in_directory(files_directory, recursive_walk=False, + def get_filepaths_in_directory(self, files_directory, recursive_walk=False, followlinks=True): """ @@ -241,6 +440,9 @@ def get_filepaths_in_directory(files_directory, recursive_walk=False, To follow symbolic links, set followlinks=True. + tuf.FormatError, if the arguments are improperly formatted. + + tuf.Error, if Python IO exceptions. @@ -250,6 +452,16 @@ def get_filepaths_in_directory(files_directory, recursive_walk=False, A list of absolute paths to target files in the given files_directory. """ + # Do the arguments have the correct format? + # Raise 'tuf.FormatError' if there is a mismatch. + tuf.formats.PATH_SCHEMA.check_match(files_directory) + tuf.formats.TOGGLE_SCHEMA.check_match(recursive_walk) + tuf.formats.TOGGLE_SCHEMA.check_match(followlinks) + + if not os.path.isdir(files_directory): + message = repr(files_directory)+' is not a directory.' + raise tuf.Error(message) + targets = [] # FIXME: We need a way to tell Python 2, but not Python 3, to return @@ -293,11 +505,9 @@ def __init__(self): self._version = 1 self._threshold = 1 self._role_keys = [] - self._signatures = [] - self._expiration = None - - - + + + def add_key(self, key): """ @@ -317,7 +527,9 @@ def add_key(self, key): None. """ - + + # Does 'key' have the correct format? + # Raise 'tuf.FormatError' if there is a mismatch. tuf.formats.ANYKEY_SCHEMA.check_match(key) try: @@ -326,18 +538,289 @@ def add_key(self, key): pass keyid = key['keyid'] - roleinfo = tuf.roledb.get_roleinfo(self._rolename) - roleinfo['keyids'].append(keyid) - tuf.roledb.update_roleinfo(self._rolename, roleinfo) - - self._role_keys.append(keyid) + roleinfo = tuf.roledb.get_roleinfo(self.rolename) + if keyid not in roleinfo['keyids']: + roleinfo['keyids'].append(keyid) + tuf.roledb.update_roleinfo(self._rolename, roleinfo) + + if keyid not in self._role_keys: + self._role_keys.append(keyid) - + + def remove_key(self, key): + """ + + + >>> + >>> + >>> + + + key: + tuf.formats.ANYKEY_SCHEMA + + + tuf.FormatError, if 'key' is improperly formatted. + + + Updates 'tuf.keydb.py'. + + + None. + """ + + # Does 'key' have the correct format? + # Raise 'tuf.FormatError' if there is a mismatch. + tuf.formats.ANYKEY_SCHEMA.check_match(key) + + keyid = key['keyid'] + roleinfo = tuf.roledb.get_roleinfo(self.rolename) + if keyid in roleinfo['keyids']: + roleinfo['keyids'].remove(keyid) + tuf.roledb.update_roleinfo(self._rolename, roleinfo) + + if keyid in self._role_keys: + self._role_keys.remove(keyid) + + + + def load_signing_key(self, key): + """ + + + >>> + >>> + >>> + + + key: + tuf.formats.ANYKEY_SCHEMA + + + tuf.FormatError, if 'key' is improperly formatted. + + tuf.Error, if the private key is unavailable in 'key'. + + + Updates 'tuf.keydb.py'. + + + None. + """ + + # Does 'key' have the correct format? + # Raise 'tuf.FormatError' if there is a mismatch. + tuf.formats.ANYKEY_SCHEMA.check_match(key) + + if not len(key['keyval']['private']): + message = 'The private key is unavailable.' + raise tuf.Error(message) + + try: + tuf.keydb.add_key(key) + except tuf.KeyAlreadyExistsError, e: + tuf.keydb.remove_key(key['keyid']) + tuf.keydb.add_key(key) + + # Update 'signing_keys' in roledb. + roleinfo = tuf.roledb.get_roleinfo(self.rolename) + if key['keyid'] not in roleinfo['signing_keyids']: + roleinfo['signing_keyids'].append(key['keyid']) + tuf.roledb.update_roleinfo(self.rolename, roleinfo) + + + + def unload_signing_key(self, key): + """ + + + >>> + >>> + >>> + + + key: + tuf.formats.ANYKEY_SCHEMA + + + tuf.FormatError, if 'key' is improperly formatted. + + tuf.Error, if the private key is unavailable in 'key'. + + + Updates 'tuf.keydb.py'. + + + None. + """ + + # Does 'key' have the correct format? + # Raise 'tuf.FormatError' if there is a mismatch. + tuf.formats.ANYKEY_SCHEMA.check_match(key) + + # Update 'signing_keys' in roledb. + roleinfo = tuf.roledb.get_roleinfo(self.rolename) + if key['keyid'] in roleinfo['signing_keyids']: + roleinfo['signing_keyids'].remove(key['keyid']) + tuf.roledb.update_roleinfo(self.rolename, roleinfo) + + + + def add_signature(self, signature): + """ + + + >>> + >>> + >>> + + + key: + tuf.formats.ANYKEY_SCHEMA + + + tuf.FormatError, if 'key' is improperly formatted. + + tuf.Error, if the private key is unavailable in 'key'. + + + Updates 'tuf.keydb.py'. + + + None. + """ + + # Does 'key' have the correct format? + # Raise 'tuf.FormatError' if there is a mismatch. + tuf.formats.SIGNATURE_SCHEMA.check_match(signature) + + roleinfo = tuf.roledb.get_roleinfo(self.rolename) + if 'signatures' not in roleinfo: + roleinfo['signatures'] = [] + + if signature not in roleinfo['signatures']: + roleinfo['signatures'].append(signature) + tuf.roledb.update_roleinfo(self.rolename, roleinfo) + + + + def remove_signature(self, signature): + """ + + + >>> + >>> + >>> + + + key: + tuf.formats.ANYKEY_SCHEMA + + + tuf.FormatError, if 'key' is improperly formatted. + + tuf.Error, if the private key is unavailable in 'key'. + + + Updates 'tuf.keydb.py'. + + + None. + """ + + # Does 'key' have the correct format? + # Raise 'tuf.FormatError' if there is a mismatch. + tuf.formats.SIGNATURE_SCHEMA.check_match(signature) + + roleinfo = tuf.roledb.get_roleinfo(self.rolename) + if signature in roleinfo['signatures']: + roleinfo['signatures'].remove(signature) + tuf.roledb.update_roleinfo(self.rolename, roleinfo) + + + + @property + def signatures(self): + """ + """ + + roleinfo = tuf.roledb.get_roleinfo(self.rolename) + + return roleinfo['signatures'] + + + + @property + def role_keys(self): + """ + """ + + roleinfo = tuf.roledb.get_roleinfo(self.rolename) + + return roleinfo['keyids'] + + + + @property + def rolename(self): + """ + """ + + return self._rolename + + + + @property + def version(self): + """ + """ + + roleinfo = tuf.roledb.get_roleinfo(self.rolename) + version = roleinfo['version'] + + return version + + + + @version.setter + def version(self, version): + """ + + + >>> + >>> + >>> + + + threshold: + tuf.formats.THRESHOLD_SCHEMA + + + tuf.FormatError, if the argument is improperly formatted. + + + Modifies the threshold attribute of the Repository object. + + + None. + """ + + # Does 'version' have the correct format? + # Raise 'tuf.FormatError' if there is a mismatch. + tuf.formats.METADATAVERSION_SCHEMA.check_match(version) + + roleinfo = tuf.roledb.get_roleinfo(self.rolename) + roleinfo['version'] = version + + tuf.roledb.update_roleinfo(self._rolename, roleinfo) + self._version = version + + + @property def threshold(self): """ - """ return self._threshold @@ -366,7 +849,9 @@ def threshold(self, threshold): None. """ - + + # Does 'threshold' have the correct format? + # Raise 'tuf.FormatError' if there is a mismatch. tuf.formats.THRESHOLD_SCHEMA.check_match(threshold) roleinfo = tuf.roledb.get_roleinfo(self._rolename) @@ -397,8 +882,10 @@ def expiration(self): The role's expiration datetime, conformant to tuf.formats.DATETIME_SCHEMA. """ + + roleinfo = tuf.roledb.get_roleinfo(self.rolename) - return self._expiration + return roleinfo['expires'] @@ -440,13 +927,27 @@ def expiration(self, expiration_datetime_utc): message = 'The expiration date must occur after the current date.' raise tuf.FormatError(message) - self._expiration = expiration_datetime_utc + roleinfo = tuf.roledb.get_roleinfo(self.rolename) + roleinfo['expires'] = expiration_datetime_utc + tuf.roledb.update_roleinfo(self.rolename, roleinfo) + + + + @property + def signing_keys(self): + """ + """ + + roleinfo = tuf.roledb.get_roleinfo(self.rolename) + + return roleinfo['signing_keyids'] def write_partial(self, object): """ + #PARTIAL_METADATA_SUFFIX >>> >>> @@ -489,9 +990,16 @@ def __init__(self): super(Root, self).__init__() self._rolename = 'root' - - roleinfo = {'keyids': [], 'threshold': 1} - tuf.roledb.add_role(self._rolename, roleinfo) + + + expiration = tuf.formats.format_time(time.time()+ROOT_EXPIRATION) + + roleinfo = {'keyids': [], 'signing_keyids': [], 'threshold': 1, + 'signatures': [], 'version': 1, 'expires': expiration} + try: + tuf.roledb.add_role(self._rolename, roleinfo) + except tuf.RoleAlreadyExistsError, e: + pass def write_partial(self): @@ -522,11 +1030,17 @@ def __init__(self): super(Timestamp, self).__init__() - self._rolename = 'timestamp' - - roleinfo = {'keyids': [], 'threshold': 1} - tuf.roledb.add_role(self._rolename, roleinfo) + self._rolename = 'timestamp' + expiration = tuf.formats.format_time(time.time()+TIMESTAMP_EXPIRATION) + + roleinfo = {'keyids': [], 'signing_keyids': [], 'threshold': 1, + 'signatures': [], 'version': 1, 'expires': expiration} + + try: + tuf.roledb.add_role(self._rolename, roleinfo) + except tuf.RoleAlreadyExistsError, e: + pass def write_partial(self): @@ -559,9 +1073,15 @@ def __init__(self): self._rolename = 'release' - roleinfo = {'keyids': [], 'threshold': 1} - tuf.roledb.add_role(self._rolename, roleinfo) - + expiration = tuf.formats.format_time(time.time()+RELEASE_EXPIRATION) + + roleinfo = {'keyids': [], 'signing_keyids': [], 'threshold': 1, + 'signatures': [], 'version': 1, 'expires': expiration} + + try: + tuf.roledb.add_role(self._rolename, roleinfo) + except tuf.RoleAlreadyExistsError, e: + pass def write_partial(self): @@ -593,25 +1113,40 @@ class Targets(Metadata): None. """ - def __init__(self, rolename, targets_directory): + def __init__(self, targets_directory, rolename, roleinfo=None): # Do the arguments have the correct format? # Raise 'tuf.FormatError' if any are improperly formatted. - tuf.formats.ROLENAME_SCHEMA.check_match(rolename) tuf.formats.PATH_SCHEMA.check_match(targets_directory) + tuf.formats.ROLENAME_SCHEMA.check_match(rolename) + if roleinfo is not None: + tuf.formats.ROLEDB_SCHEMA.check_match(roleinfo) + super(Targets, self).__init__() self._targets_directory = targets_directory self._rolename = rolename self._target_files = [] self._delegations = {} + + expiration = tuf.formats.format_time(time.time()+TARGETS_EXPIRATION) - roleinfo = {'keyids': [], 'threshold': 1, 'paths': [], - 'path_hash_prefixes': [], - 'delegations': {'keys': {}, - 'roles': []}} - - tuf.roledb.add_role(self._rolename, roleinfo) + if roleinfo is None: + roleinfo = {'keyids': [], + 'signing_keyids': [], + 'threshold': 1, + 'version': 1, + 'expires': expiration, + 'signatures': [], + 'paths': [], + 'path_hash_prefixes': [], + 'delegations': {'keys': {}, + 'roles': []}} + + try: + tuf.roledb.add_role(self._rolename, roleinfo) + except tuf.RoleAlreadyExistsError, e: + pass @@ -638,7 +1173,9 @@ def target_files(self): None. """ - return self._target_files + target_files = tuf.roledb.get_roleinfo(self._rolename)['paths'] + + return target_files @@ -687,14 +1224,14 @@ def add_target(self, filepath): 'directory: '+repr(self._targets_directory) raise tuf.Error(message) - # TODO: Ensure is an allowed target path according to the parent's + # TODO: Ensure 'filepath' is an allowed target path according to the parent's # delegation. """ - for child_target in actual_child_targets: - for allowed_child_path in allowed_child_paths: - prefix = os.path.commonprefix([child_target, allowed_child_path]) - if prefix == allowed_child_path: - break + for child_target in actual_child_targets: + for allowed_child_path in allowed_child_paths: + prefix = os.path.commonprefix([child_target, allowed_child_path]) + if prefix == allowed_child_path: + break """ # Add 'filepath' (i.e., relative to the targets directory) to the role's @@ -743,13 +1280,13 @@ def add_targets(self, list_of_targets): # TODO: Ensure list of targets allowed paths according to the parent's # delegation. - # TODO: Update the tuf.roledb entry. + # Update the tuf.roledb entry. targets_directory_length = len(self._targets_directory) - absolute_paths_list_of_targets = [] + absolute_list_of_targets = [] relative_list_of_targets = [] for target in list_of_targets: - filepath = os.path.abspath(filepath) + filepath = os.path.abspath(target) if not os.path.commonprefix([self._targets_directory, filepath]) == \ self._targets_directory: @@ -757,7 +1294,7 @@ def add_targets(self, list_of_targets): 'directory: '+repr(self._targets_directory) raise tuf.Error(message) if os.path.isfile(filepath): - absolute_paths_list_of_targets.append(filepath) + absolute_list_of_targets.append(filepath) relative_list_of_targets.append(filepath[targets_directory_length+1:]) else: message = repr(filepath)+' is not a valid file.' @@ -783,21 +1320,46 @@ def remove_target(self, filepath): filepath: + Relative to the targets directory. tuf.FormatError, if 'filepath' is improperly formatted. + tuf.Error, if 'filepath' is not under the targets directory. + Modifies the target role's 'tuf.roledb.py' entry. + None. """ + + # Does 'filepath' have the correct format? + # Raise 'tuf.FormatError' if there is a mismatch. + tuf.formats.RELPATH_SCHEMA.check_match(filepath) + + filepath = os.path.abspath(filepath) + targets_directory_length = len(self._targets_directory) + + # Ensure 'filepath' is under the targets directory. + if not os.path.commonprefix([self._targets_directory, filepath]) == \ + self._targets_directory: + message = repr(filepath)+' is not under the Repository\'s targets '+\ + 'directory: '+repr(self._targets_directory) + raise tuf.Error(message) + + relative_filepath = filepath[targets_directory_length+1:] + + fileinfo = tuf.roledb.get_roleinfo(self.rolename) + if relative_filepath in fileinfo['paths']: + fileinfo['paths'].remove(relative_filepath) + + tuf.roledb.update_roleinfo(self.rolename, fileinfo) - - - def delegate(self, rolename, public_keys, list_of_targets, restricted_paths=None): + def delegate(self, rolename, public_keys, list_of_targets, + threshold=1, restricted_paths=None): """ 'targets' is a list of target filepaths, and can be empty. @@ -813,6 +1375,8 @@ def delegate(self, rolename, public_keys, list_of_targets, restricted_paths=None list_of_targets: + expiration: + restricted_paths: @@ -832,16 +1396,15 @@ def delegate(self, rolename, public_keys, list_of_targets, restricted_paths=None tuf.formats.ROLENAME_SCHEMA.check_match(rolename) tuf.formats.ANYKEYLIST_SCHEMA.check_match(public_keys) tuf.formats.RELPATHS_SCHEMA.check_match(list_of_targets) + tuf.formats.THRESHOLD_SCHEMA.check_match(threshold) - # Validate 'list_of_targets' - # Ensure 'restricted_paths' is allowed by current role according to the - # parent. - - # Update the 'delegations' field of the current role. - + if restricted_paths is not None: + tuf.formats.RELPATHS_SCHEMA.check_match(restricted_paths) + full_rolename = self._rolename+'/'+rolename keyids = [] - + keydict = {} + # Add public keys to tuf.keydb for key in public_keys: @@ -849,25 +1412,77 @@ def delegate(self, rolename, public_keys, list_of_targets, restricted_paths=None tuf.keydb.add_key(key) except tuf.KeyAlreadyExistsError, e: pass - + keyid = key['keyid'] + key_metadata_format = tuf.keys.format_keyval_to_metadata(key['keytype'], + key['keyval']) + keydict.update({keyid: key_metadata_format}) keyids.append(keyid) - # Add role to 'tuf.roledb.py' - roleinfo = {'keyids': keyids, - 'threshold': 1, + # Validate 'list_of_targets'. + relative_targetpaths = [] + targets_directory_length = len(self._targets_directory) + + for target in list_of_targets: + target = os.path.abspath(target) + if not os.path.commonprefix([self._targets_directory, target]) == \ + self._targets_directory: + message = repr(target)+' is not under the Repository\'s targets '+\ + 'directory: '+repr(self._targets_directory) + raise tuf.Error(message) + + relative_targetpaths.append(target[targets_directory_length+1:]) + + # Validate 'restricted_paths'. + relative_restricted_paths = [] + + if restricted_paths is not None: + for target in restricted_paths: + target = os.path.abspath(target) + if not os.path.commonprefix([self._targets_directory, target]) == \ + self._targets_directory: + message = repr(target)+' is not under the Repository\'s targets '+\ + 'directory: '+repr(self._targets_directory) + raise tuf.Error(message) + + relative_restricted_paths.append(target[targets_directory_length+1:]) + + # Add role to 'tuf.roledb.py'. + expiration = tuf.formats.format_time(time.time()+TARGETS_EXPIRATION) + roleinfo = {'name': full_rolename, + 'keyids': keyids, + 'signing_keyids': [], + 'threshold': threshold, + 'version': 1, + 'expires': expiration, 'signatures': [], - 'paths': list_of_targets, + 'paths': relative_targetpaths, 'delegations': {'keys': {}, 'roles': []}} - tuf.roledb.add_role(full_rolename, roleinfo) + #tuf.roledb.add_role(full_rolename, roleinfo) + new_targets_object = Targets(self._targets_directory, full_rolename, + roleinfo ) - new_targets_object = Targets(rolename, self._targets_directory) + # Update the 'delegations' field of the current role. + current_roleinfo = tuf.roledb.get_roleinfo(self.rolename) + current_roleinfo['delegations']['keys'].update(keydict) + + # A ROLE_SCHEMA object requires only 'keyids', 'threshold', and 'paths'. + roleinfo = {'name': full_rolename, + 'keyids': roleinfo['keyids'], + 'threshold': roleinfo['threshold'], + 'paths': roleinfo['paths']} + if restricted_paths is not None: + roleinfo['paths'] = relative_restricted_paths + + current_roleinfo['delegations']['roles'].append(roleinfo) + tuf.roledb.update_roleinfo(self.rolename, current_roleinfo) # Update 'new_targets_object' attributes. for key in public_keys: new_targets_object.add_key(key) + #self._delegations = self.__setattr__(rolename, new_targets_object) @@ -882,8 +1497,8 @@ def revoke(self, rolename): rolename: - Not the full rolename ('Django' in 'targets/unclaimed/Django') of the role the - parent role (this role) wants to revoke. + Not the full rolename ('Django' in 'targets/unclaimed/Django') of the + role the parent role (this role) wants to revoke. tuf.FormatError, if 'rolename' is improperly formatted. @@ -895,14 +1510,19 @@ def revoke(self, rolename): """ tuf.formats.ROLENAME_SCHEMA.check_match(rolename) - - self.__delattr__(rolename) # Remove from this Target's delegations dict. + full_rolename = self.rolename+'/'+rolename + roleinfo = tuf.roledb.get_roleinfo(self.rolename) + del roleinfo['delegations']['roles'][full_rolename] - # Remove from 'tuf.roledb.py' + # Remove from 'tuf.roledb.py'. The delegated roles of 'rolename' are also + # removed. + tuf.roledb.remove_role(full_rolename) + + # Remove the rolename attribute from the current role. + self.__delattr__(rolename) - # Remove def _prompt(message, result_type=str): @@ -936,7 +1556,7 @@ def _get_password(prompt='Password: ', confirm=False): if password == password2: return password else: - print 'Mismatch; try again.' + print('Mismatch; try again.') @@ -982,6 +1602,33 @@ def _check_directory(directory): +def _check_role_keys(rolename): + """ + rolename: + full rolename. + """ + + roleinfo = tuf.roledb.get_roleinfo(rolename) + total_keyids = len(roleinfo['keyids']) + threshold = roleinfo['threshold'] + total_signatures = len(roleinfo['signatures']) + total_signing_keys = len(roleinfo['signing_keyids']) + + if total_keyids < threshold: + message = repr(rolename)+' role contains '+repr(total_keyids)+' / '+ \ + repr(threshold)+' public keys.' + raise tuf.InsufficientKeysError(message) + + if total_signatures == 0 and total_signing_keys < threshold: + message = repr(rolename)+' role contains '+repr(total_signing_keys)+' / '+ \ + repr(threshold)+' signing keys.' + raise tuf.InsufficientKeysError(message) + + + + + + def create_new_repository(repository_directory): """ @@ -1052,23 +1699,205 @@ def create_new_repository(repository_directory): -def load_repository(repository_directory, partial_metadata_suffix=None): +def load_repository(repository_directory): """ - Return a repository object that represents an existing repository. + Return a repository object containing the contents of metadata files loaded + from the repository. repository_directory: - partial_metadata_suffix: - + tuf.FormatError, if 'repository_directory' or any of the metadata files + are improperly formatted. Also raised if, at a minimum, the Root role + cannot be found. + All the metadata files found in the repository are loaded and their contents + stored in a libtuf.Repository object. libtuf.Repository object. """ + + # Does 'repository_directory' have the correct format? + # Raise 'tuf.FormatError' if there is a mismatch. + tuf.formats.PATH_SCHEMA.check_match(repository_directory) + + # Load top-level metadata. + repository_directory = os.path.abspath(repository_directory) + metadata_directory = os.path.join(repository_directory, + METADATA_DIRECTORY_NAME) + targets_directory = os.path.join(repository_directory, + TARGETS_DIRECTORY_NAME) + + repository = None + + filenames = get_metadata_filenames(metadata_directory) + root_filename = filenames[ROOT_FILENAME] + targets_filename = filenames[TARGETS_FILENAME] + release_filename = filenames[RELEASE_FILENAME] + timestamp_filename = filenames[TIMESTAMP_FILENAME] + + root_metadata = None + targets_metadata = None + release_metadata = None + timestamp_metadata = None + + # ROOT.txt + if os.path.exists(root_filename): + + # Initialize the key and role metadata of the top-level roles. + signable = tuf.util.load_json_file(root_filename) + tuf.formats.check_signable_object_format(signable) + root_metadata = signable['signed'] + tuf.keydb.create_keydb_from_root_metadata(root_metadata) + tuf.roledb.create_roledb_from_root_metadata(root_metadata) + + roleinfo = tuf.roledb.get_roleinfo('root') + roleinfo['signatures'] = [] + for signature in signable['signatures']: + roleinfo['signatures'].append(signature) + tuf.roledb.update_roleinfo('root', roleinfo) + else: + message = 'Cannot load the required root file: '+repr(root_filename) + raise tuf.RepositoryError(message) + + repository = Repository(repository_directory, metadata_directory, + targets_directory) + + # TARGETS.txt + if os.path.exists(targets_filename): + signable = tuf.util.load_json_file(targets_filename) + tuf.formats.check_signable_object_format(signable) + targets_metadata = signable['signed'] + + for signature in signable['signatures']: + repository.targets.add_signature(signature) + + # Update 'targets.txt' in 'tuf.roledb.py' + roleinfo = tuf.roledb.get_roleinfo('targets') + roleinfo['paths'] = targets_metadata['targets'].keys() + roleinfo['version'] = targets_metadata['version'] + roleinfo['expires'] = targets_metadata['expires'] + roleinfo['delegations'] = targets_metadata['delegations'] + tuf.roledb.update_roleinfo('targets', roleinfo) + + # Add the keys specified in the delegations field of the Targets role. + # TODO: Delegated role's are only missing the threshold value, which the + # parent role sets. Remember to request threshold value from parent role. + for key_metadata in targets_metadata['delegations']['keys'].values(): + key_object = tuf.keys.format_metadata_to_key(key_metadata) + tuf.keydb.add_key(key_object) + + for role in targets_metadata['delegations']['roles']: + rolename = role['name'] + roleinfo = {'name': role['name'], + 'keyids': role['keyids'], + 'threshold': role['threshold'], + 'signing_keyids': [], + 'signatures': [], + 'delegations': {'keys': {}, + 'roles': []}} + tuf.roledb.add_role(rolename, roleinfo) + + else: + pass + + + # RELEASE.txt + if os.path.exists(release_filename): + signable = tuf.util.load_json_file(release_filename) + tuf.formats.check_signable_object_format(signable) + release_metadata = signable['signed'] + for signature in signable['signatures']: + repository.release.add_signature(signature) + + roleinfo = tuf.roledb.get_roleinfo('release') + roleinfo['expires'] = release_metadata['expires'] + roleinfo['version'] = release_metadata['version'] + tuf.roledb.update_roleinfo('release', roleinfo) + + else: + pass + + + # TIMESTAMP.txt + if os.path.exists(timestamp_filename): + signable = tuf.util.load_json_file(timestamp_filename) + timestamp_metadata = signable['signed'] + for signature in signable['signatures']: + repository.timestamp.add_signature(signature) + + roleinfo = tuf.roledb.get_roleinfo('timestamp') + roleinfo['expires'] = timestamp_metadata['expires'] + roleinfo['version'] = timestamp_metadata['version'] + tuf.roledb.update_roleinfo('timestamp', roleinfo) + + else: + pass + + # Load delegated targets metadata. + # Walk the 'targets/' directory and generate the file info for all + # the files listed there. This information is stored in the 'meta' + # field of the release metadata object. + targets_objects = {} + targets_objects['targets'] = repository.targets + targets_metadata_directory = os.path.join(metadata_directory, + TARGETS_DIRECTORY_NAME) + if os.path.exists(targets_metadata_directory) and \ + os.path.isdir(targets_metadata_directory): + for root, directories, files in os.walk(targets_metadata_directory): + # 'files' here is a list of target file names. + for basename in files: + metadata_path = os.path.join(root, basename) + metadata_name = metadata_path[len(metadata_directory):].lstrip(os.path.sep) + extension_length = len(METADATA_EXTENSION) + metadata_name = metadata_name[:-extension_length] + + signable = None + try: + signable = tuf.util.load_json_file(metadata_path) + except (ValueError, IOError), e: + continue + + metadata_object = signable['signed'] + + roleinfo = tuf.roledb.get_roleinfo(metadata_name) + roleinfo['signatures'].extend(signable['signatures']) + roleinfo['version'] = metadata_object['version'] + roleinfo['expires'] = metadata_object['expires'] + roleinfo['paths'] = metadata_object['targets'].keys() + + tuf.roledb.update_roleinfo(metadata_name, roleinfo) + + new_targets_object = Targets(targets_directory, metadata_name, roleinfo) + targets_object = targets_objects[tuf.roledb.get_parent_rolename(metadata_name)] + targets_object.__setattr__(os.path.basename(metadata_name), + new_targets_object) + + # Add the keys specified in the delegations field of the Targets role. + for key_metadata in metadata_object['delegations']['keys'].values(): + key_object = tuf.keys.format_metadata_to_key(key_metadata) + try: + tuf.keydb.add_key(key_object) + except tuf.KeyAlreadyExistsError, e: + pass + + for role in metadata_object['delegations']['roles']: + rolename = role['name'] + roleinfo = {'name': role['name'], + 'keyids': role['keyids'], + 'threshold': role['threshold'], + 'signing_keyids': [], + 'signatures': [], + 'delegations': {'keys': {}, + 'roles': []}} + tuf.roledb.update_roleinfo(rolename, roleinfo) + + return repository + @@ -1077,7 +1906,6 @@ def generate_and_write_rsa_keypair(filepath, bits=DEFAULT_RSA_KEY_BITS, password=None): """ - Return a repository object that represents an existing repository. filepath: @@ -1090,10 +1918,13 @@ def generate_and_write_rsa_keypair(filepath, bits=DEFAULT_RSA_KEY_BITS, password: + tuf.FormatError, if the arguments are improperly formatted. + Writes key files to '' and '.pub'. + None. """ # Does 'filepath' have the correct format? @@ -1117,7 +1948,9 @@ def generate_and_write_rsa_keypair(filepath, bits=DEFAULT_RSA_KEY_BITS, encrypted_pem = tuf.keys.create_rsa_encrypted_pem(private, password) # Write public key (i.e., 'public', which is in PEM format) to - # '.pub' + # '.pub'. + tuf.util.ensure_parent_dir(filepath) + with open(filepath+'.pub', 'w') as file_object: file_object.write(public) @@ -1175,16 +2008,19 @@ def import_rsa_privatekey_from_file(filepath, password=None): def import_rsa_publickey_from_file(filepath): """ + If the RSA PEM in 'filepath' contains a private key, it is discarded. filepath: .pub file, an RSA PEM file. + tuf.FormatError, if 'filepath' is improperly formatted. + An RSA key object conformant to 'tuf.formats.RSAKEY_SCHEMA'. """ # Does 'filepath' have the correct format? @@ -1431,8 +2267,8 @@ def generate_root_metadata(version, expiration_date): -def generate_targets_metadata(repository_directory, target_files, version, - expiration_date): +def generate_targets_metadata(targets_directory, target_files, version, + expiration_date, delegations=None): """ Generate the targets metadata object. The targets must exist at the same @@ -1441,18 +2277,18 @@ def generate_targets_metadata(repository_directory, target_files, version, provide keys. + targets_directory: + The directory (absolute path) containing the target files and directories. + target_files: The target files tracked by 'targets.txt'. 'target_files' is a list of - paths/directories of target files that are relative to the repository - (e.g., ['targets/file1.txt', ...]). If the target files are saved in + paths/directories of target files that are relative to the targets + directory (e.g., ['file1.txt', 'Django/module.py']). If the target files + are saved in the root folder 'targets' on the repository, then 'targets' must be included in the target paths. The repository does not have to name this folder 'targets'. - repository_directory: - The directory (absolute path) containing the metadata and target - directories. - version: The metadata version number. Clients use the version number to determine if the downloaded version is newer than the one currently @@ -1461,6 +2297,9 @@ def generate_targets_metadata(repository_directory, target_files, version, expiration_date: The expiration date, in UTC, of the metadata file. Conformant to 'tuf.formats.TIME_SCHEMA'. + + delegations: + tuf.FormatError, if an error occurred trying to generate the targets @@ -1477,31 +2316,38 @@ def generate_targets_metadata(repository_directory, target_files, version, # Do the arguments have the correct format. # Raise 'tuf.FormatError' if there is a mismatch. + tuf.formats.PATH_SCHEMA.check_match(targets_directory) tuf.formats.PATHS_SCHEMA.check_match(target_files) - tuf.formats.PATH_SCHEMA.check_match(repository_directory) tuf.formats.METADATAVERSION_SCHEMA.check_match(version) tuf.formats.TIME_SCHEMA.check_match(expiration_date) + if delegations is not None: + tuf.formats.DELEGATIONS_SCHEMA.check_match(delegations) + filedict = {} - - repository_directory = _check_directory(repository_directory) + targets_directory = _check_directory(targets_directory) # Generate the file info for all the target files listed in 'target_files'. for target in target_files: + # Strip 'targets/' from from 'target' and keep the rest (e.g., # 'targets/more_targets/somefile.txt' -> 'more_targets/somefile.txt' - relative_targetpath = os.path.sep.join(target.split(os.path.sep)[1:]) - target_path = os.path.join(repository_directory, target) + #relative_targetpath = os.path.sep.join(target.split(os.path.sep)[1:]) + relative_targetpath = target + target_path = os.path.join(targets_directory, target) + if not os.path.exists(target_path): message = repr(target_path)+' could not be read. Unable to generate '+\ 'targets metadata.' raise tuf.Error(message) + filedict[relative_targetpath] = get_metadata_file_info(target_path) # Generate the targets metadata object. targets_metadata = tuf.formats.TargetsFile.make_metadata(version, expiration_date, - filedict) + filedict, + delegations) return tuf.formats.make_signable(targets_metadata) @@ -1568,6 +2414,7 @@ def generate_release_metadata(metadata_directory, version, expiration_date): targets_metadata = os.path.join(metadata_directory, 'targets') if os.path.exists(targets_metadata) and os.path.isdir(targets_metadata): for directory_path, junk, files in os.walk(targets_metadata): + # 'files' here is a list of target file names. for basename in files: metadata_path = os.path.join(directory_path, basename) @@ -1634,11 +2481,14 @@ def generate_timestamp_metadata(release_filename, version, # Save the file info of the compressed versions of 'timestamp.txt'. for file_extension in compressions: + compressed_filename = release_filename + '.' + file_extension try: compressed_fileinfo = get_metadata_file_info(compressed_filename) + except: logger.warn('Could not get fileinfo about '+str(compressed_filename)) + else: logger.info('Including fileinfo about '+str(compressed_filename)) fileinfo[RELEASE_FILENAME+'.' + file_extension] = compressed_fileinfo @@ -1699,8 +2549,9 @@ def sign_metadata(metadata, keyids, filename): # Sign the metadata with each keyid in 'keyids'. for keyid in keyids: + # Load the signing key. - key = tuf.repo.keystore.get_key(keyid) + key = tuf.keydb.get_key(keyid) logger.info('Signing '+repr(filename)+' with '+key['keyid']) # Create a new signature list. If 'keyid' is encountered, @@ -1713,11 +2564,15 @@ def sign_metadata(metadata, keyids, filename): # Generate the signature using the appropriate signing method. if key['keytype'] == 'rsa': - signed = signable['signed'] - signature = tuf.sig.generate_rsa_signature(signed, key) - signable['signatures'].append(signature) + if len(key['keyval']['private']): + signed = signable['signed'] + signature = tuf.sig.generate_rsa_signature(signed, key) + signable['signatures'].append(signature) + else: + logger.warn('Private key unset. Skipping: '+repr(keyid)) + else: - raise tuf.Error('The keystore contains a key with an invalid key type') + raise tuf.Error('The keydb contains a key with an invalid key type.') # Raise 'tuf.FormatError' if the resulting 'signable' is not formatted # correctly. @@ -1770,6 +2625,7 @@ def write_metadata_file(metadata, filename, compression=None): # We choose a file-like object that depends on the compression algorithm. file_object = None + # We may modify the filename, depending on the compression algorithm, so we # store it separately. filename_with_compression = filename @@ -1778,10 +2634,12 @@ def write_metadata_file(metadata, filename, compression=None): if compression is None: logger.info('No compression for '+str(filename)) file_object = open(filename_with_compression, 'w') + elif compression == 'gz': logger.info('gzip compression for '+str(filename)) filename_with_compression += '.gz' file_object = gzip.open(filename_with_compression, 'w') + else: raise tuf.FormatError('Unknown compression algorithm: '+str(compression)) @@ -1797,9 +2655,11 @@ def write_metadata_file(metadata, filename, compression=None): except: # Raise any runtime exception. raise + else: # Otherwise, return the written filename. return filename_with_compression + finally: # Always close the file. file_object.close() @@ -1808,42 +2668,49 @@ def write_metadata_file(metadata, filename, compression=None): -def build_delegated_role_file(delegated_targets_directory, delegated_keyids, - metadata_directory, delegation_metadata_directory, - delegation_role_name, version, expiration_date): +def write_delegated_metadata_file(repository_directory, targets_directory, + rolename, version, expiration, keyids, + list_of_targets, delegations, signatures, + write_partial=False): """ Build the targets metadata file using the signing keys in 'delegated_keyids'. The generated metadata file is saved to 'metadata_directory'. The target files located in 'targets_directory' will - be tracked by the built targets metadata. + be tracked by the built targets metadata. - delegated_targets_directory: - The directory (absolute path) containing all the delegated target - files. - - delegated_keyids: - The list of keyids to be used as the signing keys for the delegated - role file. - - metadata_directory: - The metadata directory (absolute path) containing all the metadata files. - - delegation_metadata_directory: - The location of the delegated role's metadata. - - delegation_role_name: - The delegated role's file name ending in '.txt'. Ex: 'role1.txt'. + repository_directory: + The repository directory (absolute path) containing all the metadata + and target files. + + rolename: + The delegated role's full rolename (e.g., 'targets/unclaimed/django'). version: The metadata version number. Clients use the version number to determine if the downloaded version is newer than the one currently trusted. - expiration_date: + expiration: The expiration date, in UTC, of the metadata file. Conformant to 'tuf.formats.TIME_SCHEMA'. + + keyids: + The list of keyids to be used as the signing keys for the delegated + metadata file. + + list_of_targets: + The directory (absolute path) containing all the delegated target + files. The filepaths are not required to live under the targets + directory. The caller is reponsible for ensuring the correct location + of target files. + + delegations: + 'tuf.formats.DELEGATIONS_SCHEMA'. + + signatures: + 'tuf.formats.SIGNATURES_SCHEMA'. tuf.FormatError, if any of the arguments are improperly formatted. @@ -1859,37 +2726,102 @@ def build_delegated_role_file(delegated_targets_directory, delegated_keyids, # Do the arguments have the correct format? # Raise 'tuf.FormatError' if there is a mismatch. - tuf.formats.PATH_SCHEMA.check_match(delegated_targets_directory) - tuf.formats.KEYIDS_SCHEMA.check_match(delegated_keyids) - tuf.formats.PATH_SCHEMA.check_match(metadata_directory) - tuf.formats.PATH_SCHEMA.check_match(delegation_metadata_directory) - tuf.formats.NAME_SCHEMA.check_match(delegation_role_name) + tuf.formats.PATH_SCHEMA.check_match(repository_directory) + tuf.formats.PATH_SCHEMA.check_match(targets_directory) - # Check if 'targets_directory' and 'metadata_directory' are valid. - targets_directory = _check_directory(delegated_targets_directory) - metadata_directory = _check_directory(metadata_directory) + tuf.formats.ROLENAME_SCHEMA.check_match(rolename) + tuf.formats.METADATAVERSION_SCHEMA.check_match(version) + tuf.formats.TIME_SCHEMA.check_match(expiration) + tuf.formats.KEYIDS_SCHEMA.check_match(keyids) + tuf.formats.RELPATHS_SCHEMA.check_match(list_of_targets) + tuf.formats.DELEGATIONS_SCHEMA.check_match(delegations) + tuf.formats.SIGNATURES_SCHEMA.check_match(signatures) + tuf.formats.TOGGLE_SCHEMA.check_match(write_partial) + + # Check if 'repository_directory' is valid. + repository_directory = _check_directory(repository_directory) + metadata_directory = os.path.join(repository_directory, + METADATA_DIRECTORY_NAME) - repository_directory, junk = os.path.split(metadata_directory) - repository_directory_length = len(repository_directory) + # Create the metadata object. Delegated roles are of type + # 'tuf.formats.TARGETS_SCHEMA', same as the Targets role. - # Get the list of targets. - targets = [] - for root, directories, files in os.walk(targets_directory): - for target_file in files: - # Note: '+1' in the line below is there to remove '/'. - filename = os.path.join(root, target_file)[repository_directory_length+1:] - targets.append(filename) + metadata_object = generate_targets_metadata(targets_directory, + list_of_targets, version, + expiration, delegations) - # Create the targets metadata object. - targets_metadata = generate_targets_metadata(repository_directory, targets, - version, expiration_date) + # Delegated metadata are written to their respective directories on the + # repository. For example, the role 'targets/unclaimed/django' is written + # to '{repository_directory}/metadata/targets/unlaimed/django.txt'. + metadata_filepath = os.path.join(metadata_directory, rolename+'.txt') + + # Ensure the parent directories of metadata_filepath exist, otherwise an IO + # exception is raised. + tuf.util.ensure_parent_dir(metadata_filepath) # Sign it. - targets_filepath = os.path.join(delegation_metadata_directory, - delegation_role_name) - signable = sign_metadata(targets_metadata, delegated_keyids, targets_filepath) + signable = sign_metadata(metadata_object, keyids, metadata_filepath) + for signature in signatures: + signable['signatures'].append(signature) + if tuf.sig.verify(signable, rolename) or write_partial: + write_metadata_file(signable, metadata_filepath) + else: + raise tuf.Error('Not enough signatures for: '+repr(metadata_filepath)) - return write_metadata_file(signable, targets_filepath) + + + + +def create_tuf_client_directory(repository_directory, client_directory): + """ + + Create the file containing the metadata. + + + repository_directory: + + client_directory: + + + tuf.FormatError, if the arguments are improperly formatted. + + + + + None. + """ + + # Do the arguments have the correct format? + # Raise 'tuf.FormatError' if there is a mismatch. + tuf.formats.PATH_SCHEMA.check_match(repository_directory) + tuf.formats.PATH_SCHEMA.check_match(client_directory) + + repository_directory = os.path.abspath(repository_directory) + metadata_directory = os.path.join(repository_directory, + METADATA_DIRECTORY_NAME) + + # Generate the 'client' directory containing the metadata of the created + # repository. 'tuf.client.updater.py' expects the 'current' and 'previous' + # directories to exist under 'metadata'. + client_directory = os.path.abspath(client_directory) + client_metadata_directory = os.path.join(client_directory, + METADATA_DIRECTORY_NAME) + + try: + os.makedirs(client_metadata_directory) + except OSError, e: + if e.errno == errno.EEXIST: + message = 'Cannot create a fresh client metadata directory: '+ \ + repr(client_metadata_directory)+'. Already exists.' + raise tuf.RepositoryError(message) + else: + raise + + # Move the metadata to the client's 'current' and 'previous' directories. + client_current = os.path.join(client_metadata_directory, 'current') + client_previous = os.path.join(client_metadata_directory, 'previous') + shutil.copytree(metadata_directory, client_current) + shutil.copytree(metadata_directory, client_previous) diff --git a/tuf/roledb.py b/tuf/roledb.py index eabf50e9..6b222504 100755 --- a/tuf/roledb.py +++ b/tuf/roledb.py @@ -36,6 +36,7 @@ """ import logging +import copy import tuf import tuf.formats @@ -86,6 +87,16 @@ def create_roledb_from_root_metadata(root_metadata): # Iterate through the roles found in 'root_metadata' # and add them to '_roledb_dict'. Duplicates are avoided. for rolename, roleinfo in root_metadata['roles'].items(): + if rolename == 'root': + roleinfo['version'] = root_metadata['version'] + roleinfo['expires'] = root_metadata['expires'] + + roleinfo['signatures'] = [] + roleinfo['signing_keyids'] = [] + + if rolename.startswith('targets'): + roleinfo['delegations'] = {'keys': {}, 'roles': []} + try: add_role(rolename, roleinfo) # tuf.Error raised if the parent role of 'rolename' does not exist. @@ -168,7 +179,7 @@ def add_role(rolename, roleinfo, require_parent=True): if parent_role not in _roledb_dict: raise tuf.Error('Parent role does not exist: '+parent_role) - _roledb_dict[rolename] = roleinfo + _roledb_dict[rolename] = copy.deepcopy(roleinfo) @@ -226,7 +237,7 @@ def update_roleinfo(rolename, roleinfo): if rolename not in _roledb_dict: raise tuf.UnknownRoleError('Role does not exist: '+rolename) - _roledb_dict[rolename] = roleinfo + _roledb_dict[rolename] = copy.deepcopy(roleinfo) @@ -467,13 +478,16 @@ def get_roleinfo(rolename): """ Return the roleinfo of 'rolename'. - {'name': 'role_name', - 'keyids': ['34345df32093bd12...'], - 'threshold': 1, - 'paths': ['path/to/target1', 'path/to/target2', ...], - 'path_hash_prefixes': ['a324fcd...', ...]} - The 'name', 'paths', and 'path_hash_prefixes' dict keys are optional. + {'keyids': ['34345df32093bd12...'], + 'threshold': 1, + 'signatures': ['ab453bdf...', ...], + 'paths': ['path/to/target1', 'path/to/target2', ...], + 'path_hash_prefixes': ['a324fcd...', ...], + 'delegations': {'keys': {}, 'roles': []}} + + The 'signatures', 'paths', 'path_hash_prefixes', and 'delegations' dict keys + are optional. rolename: @@ -495,7 +509,7 @@ def get_roleinfo(rolename): # Raises tuf.FormatError, tuf.UnknownRoleError, or tuf.InvalidNameError. _check_rolename(rolename) - return _roledb_dict[rolename] + return copy.deepcopy(_roledb_dict[rolename]) @@ -639,7 +653,7 @@ def get_delegated_rolenames(rolename): A list of rolenames. Note that the rolenames are *NOT* sorted by order of - delegation! + delegation. """ # Raises tuf.FormatError, tuf.UnknownRoleError, or tuf.InvalidNameError. diff --git a/tuf/schema.py b/tuf/schema.py index 60ad5349..278e1899 100755 --- a/tuf/schema.py +++ b/tuf/schema.py @@ -496,7 +496,7 @@ def check_match(self, object): raise tuf.FormatError('Got '+repr(object)+' instead of an integer.') elif not (self._lo <= object <= self._hi): - int_range = '['+repr(self._lo)+','+repr(self._hi)+'].' + int_range = '['+repr(self._lo)+', '+repr(self._hi)+'].' raise tuf.FormatError(repr(object)+' not in range '+int_range) diff --git a/tuf/util.py b/tuf/util.py index 1460e838..249b9274 100755 --- a/tuf/util.py +++ b/tuf/util.py @@ -547,7 +547,7 @@ def load_json_file(filepath): Deserialize a JSON object from a file containing the object. - data: + filepath: Absolute path of JSON file. From d6b9e187b7a2991eb00309877a13645473e019b8 Mon Sep 17 00:00:00 2001 From: vladdd Date: Tue, 12 Nov 2013 16:55:51 -0500 Subject: [PATCH 66/95] Fix repository.targets.revoke() --- tuf/libtuf.py | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/tuf/libtuf.py b/tuf/libtuf.py index 2e5da77b..add76913 100755 --- a/tuf/libtuf.py +++ b/tuf/libtuf.py @@ -1514,8 +1514,11 @@ def revoke(self, rolename): # Remove from this Target's delegations dict. full_rolename = self.rolename+'/'+rolename roleinfo = tuf.roledb.get_roleinfo(self.rolename) - del roleinfo['delegations']['roles'][full_rolename] - + + for role in roleinfo['delegations']['roles']: + if role['name'] == full_rolename: + roleinfo['delegations']['roles'].remove(role) + # Remove from 'tuf.roledb.py'. The delegated roles of 'rolename' are also # removed. tuf.roledb.remove_role(full_rolename) @@ -2798,14 +2801,14 @@ def create_tuf_client_directory(repository_directory, client_directory): repository_directory = os.path.abspath(repository_directory) metadata_directory = os.path.join(repository_directory, - METADATA_DIRECTORY_NAME) + 'metadata') # Generate the 'client' directory containing the metadata of the created # repository. 'tuf.client.updater.py' expects the 'current' and 'previous' # directories to exist under 'metadata'. client_directory = os.path.abspath(client_directory) client_metadata_directory = os.path.join(client_directory, - METADATA_DIRECTORY_NAME) + 'metadata') try: os.makedirs(client_metadata_directory) From 973ed15a965897fa26fe6bdd20d5cc669c0154ed Mon Sep 17 00:00:00 2001 From: santiago Date: Tue, 12 Nov 2013 17:07:08 -0500 Subject: [PATCH 67/95] Added base markdown file --- tuf/README.md | 1 + 1 file changed, 1 insertion(+) create mode 100644 tuf/README.md diff --git a/tuf/README.md b/tuf/README.md new file mode 100644 index 00000000..23f761a0 --- /dev/null +++ b/tuf/README.md @@ -0,0 +1 @@ +markdown! From 7049c7359ed30444a0460d3ea663ba67d53cbd67 Mon Sep 17 00:00:00 2001 From: santiago Date: Tue, 12 Nov 2013 17:23:31 -0500 Subject: [PATCH 68/95] Added the first code block for RSA key creation --- tuf/README.md | 27 ++++++++++++++++++++++++++- 1 file changed, 26 insertions(+), 1 deletion(-) diff --git a/tuf/README.md b/tuf/README.md index 23f761a0..012413f6 100644 --- a/tuf/README.md +++ b/tuf/README.md @@ -1 +1,26 @@ -markdown! +## Create TUF Repository + +### Keys + +#### Create RSA Keys +```python +from libtuf import * + + +# Generate and write the first of two root keys for the repository. +# The following function creates an RSA key pair, where the private key is saved to +# “path/to/root_key” and the public key to “path/to/root_key.pub”. +generate_and_write_rsa_keypair("path/to/root_key",bits=2048,password="password") + +#if thhe key length is unspecified, it defaults to 3072 bits. A length of then +#than 2048 bits prints an error mesage. A password may be supplied as an +#argument, otherwise a user prompt is presented +generate_and_write_rsa_keypair("path/to/root_key2") +>>> Enter a password for the RSA key: +>>> Confirm: +``` +The following four files should now exist: +1. root_key +2. root_key.pub +3. root_key2 +4. root_key2.pub From ab09a28a12a02a883f549c04151bc9d6e6193aa6 Mon Sep 17 00:00:00 2001 From: SantiagoTorres Date: Tue, 12 Nov 2013 17:24:41 -0500 Subject: [PATCH 69/95] Update README.md --- tuf/README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/tuf/README.md b/tuf/README.md index 012413f6..985cf417 100644 --- a/tuf/README.md +++ b/tuf/README.md @@ -20,6 +20,7 @@ generate_and_write_rsa_keypair("path/to/root_key2") >>> Confirm: ``` The following four files should now exist: + 1. root_key 2. root_key.pub 3. root_key2 From 45c7ac3e01866a747760519445d3cfa07c39e865 Mon Sep 17 00:00:00 2001 From: santiago Date: Tue, 12 Nov 2013 17:38:33 -0500 Subject: [PATCH 70/95] Added the import key codeblock --- tuf/README.md | 23 +++++++++++++++++++++-- 1 file changed, 21 insertions(+), 2 deletions(-) diff --git a/tuf/README.md b/tuf/README.md index 012413f6..d4fe9ff5 100644 --- a/tuf/README.md +++ b/tuf/README.md @@ -16,11 +16,30 @@ generate_and_write_rsa_keypair("path/to/root_key",bits=2048,password="password") #than 2048 bits prints an error mesage. A password may be supplied as an #argument, otherwise a user prompt is presented generate_and_write_rsa_keypair("path/to/root_key2") ->>> Enter a password for the RSA key: ->>> Confirm: +Enter a password for the RSA key: +Confirm: ``` The following four files should now exist: + 1. root_key 2. root_key.pub 3. root_key2 4. root_key2.pub + +### Import RSA Keys +```python +from libtuf import * + +#import an existing public key +public_root_key = import_rsa_publickey_from_file("path/to/root_key.pub") + +#import an existing private key +private_root_key = import_rsa_privatekey_from_file("path/to/root_key) +Enter a password for the RSA key: +Confirm: +``` +At the time of importing the private RSA, a tuf.CryptoError can be thrown if +the key is invalid + +### Create a new Repository +``` From 8ab4f9c2da3acb11829173bd01e9303ec054361c Mon Sep 17 00:00:00 2001 From: santiago Date: Tue, 12 Nov 2013 17:52:11 -0500 Subject: [PATCH 71/95] Added the root metadata codeblock --- tuf/README.md | 43 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 43 insertions(+) diff --git a/tuf/README.md b/tuf/README.md index d4fe9ff5..cd151398 100644 --- a/tuf/README.md +++ b/tuf/README.md @@ -42,4 +42,47 @@ At the time of importing the private RSA, a tuf.CryptoError can be thrown if the key is invalid ### Create a new Repository + +#### Create Root +```python +# Continuing from the previous section... + +# Create a new Repository object that holds the file path to the repository and the four +# top-level role objects (Root, Targets, Release, Timestamp). Metadata files are created when +# repository.write() is called. The repository directory is created if it does not exist. +repository = create_new_repository("path/to/repository/") + +# The Repository instance, ‘repository’, initially contains top-level Metadata objects. +# Add one of the public keys, created in the previous section, to the root role. Metadata is +# considered valid if it is signed by the public key’s corresponding private key +repository.root.add_key(public_root_key) + +# Add a second public key to the root role. Although previously generated and saved to a file, +# the second public key must be imported before it can added to a role. +public_root_key2 = import_rsa_publickey_from_file("path/to/root_key2.pub") +repository.root.add_key(public_root_key2) + +# Threshold for each role defaults to 1. Users may change the threshold value, but libtuf.py +# validates thresholds and signatures and warns users. Set the threshold of the root role to 2, +# which means the root metadata file is considered valid if it contains at least 2 valid +# signatures. +repository.root.threshold = 2 +private_root_key2=import_rsa_privatekey_from_file("path/to/root_key2,password="pw") + +# Load the root signing keys to the repository, which write() uses to sign the root metadata. +# The load_signing_key() method SHOULD warn when the key is NOT explicitly allowed to +# sign for it. +repository.root.load_signing_key(private_root_key) +repository.root_load_signing_key(private_root_key2) + +try: + repository.write() +# An exception is raised here by write() because the other top-level roles (targets, release, +# and timestamp) have not been configured with keys. +except tuf.Error, e: + print e +Not enough signatures for '/home/santiago/Documents/o2013/NYU/TUF/repo-tools/repo-real/metadata.staged/root.txt' + +# In the next section, update the other top-level roles and create a repository with valid metadata ``` + From 0cc8841ead40e08c5a14b1aac3ceff8eb36c9eea Mon Sep 17 00:00:00 2001 From: santiago Date: Tue, 12 Nov 2013 18:02:40 -0500 Subject: [PATCH 72/95] Added the targets,root and timestamp metadata codeblock --- tuf/README.md | 51 ++++++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 50 insertions(+), 1 deletion(-) diff --git a/tuf/README.md b/tuf/README.md index cd151398..24ed7ffe 100644 --- a/tuf/README.md +++ b/tuf/README.md @@ -67,7 +67,7 @@ repository.root.add_key(public_root_key2) # which means the root metadata file is considered valid if it contains at least 2 valid # signatures. repository.root.threshold = 2 -private_root_key2=import_rsa_privatekey_from_file("path/to/root_key2,password="pw") +private_root_key2=import_rsa_privatekey_from_file("path/to/root_key2",password="pw") # Load the root signing keys to the repository, which write() uses to sign the root metadata. # The load_signing_key() method SHOULD warn when the key is NOT explicitly allowed to @@ -86,3 +86,52 @@ Not enough signatures for '/home/santiago/Documents/o2013/NYU/TUF/repo-tools/rep # In the next section, update the other top-level roles and create a repository with valid metadata ``` +#### Create Timestamp, Release, Targets + +```python +# Continuing from the previous section . . . + +# Generate keys for the remaining top-level roles. The root keys have been set above. +# The password argument may be omitted if a password prompt is needed. +generate_and_write_rsa_keypair("path/to/targets_key", password="pw") +generate_and_write_rsa_keypair("path/to/release_key", password="pw") +generate_and_write_rsa_keypair("path/to/timestamp_key", password="pw") + +# Add the public keys of the remaining top-level roles. +repository.targets.add_key(import_rsa_publickey_from_file("path/to/targets_key.pub")) +repository.release.add_key(import_rsa_publickey_from_file("path/to/release_key.pub")) +repository.timestamp.add_key(import_rsa_publickey_from_file("path/to/timestamp_key.pub")) + +# Import the signing keys of the remaining top-level roles. Prompt for passwords. +private_targets_key = import_rsa_privatekey_from_file("path/to/targets_key") +Enter a password for the RSA key: +Confirm: +private_release_key = import_rsa_privatekey_from_file("path/to/release_key") +Enter a password for the RSA key: +Confirm: +private_timestamp_key = import_rsa_privatekey_from_file("path/to/timestamp_key") +Enter a password for the RSA key: +Confirm: + +# Load the signing keys of the remaining roles so that valid signatures are generated when +# repository.write() is called. +repository.targets.load_signing_key(private_targets_key) +repository.release.load_signing_key(private_release_key) +repository.timestamp.load_signing_key(private_timestamp_key) + +# Optionally set the expiration date of the timestamp role. By default, roles are set to expire +# as follows: root(1 year), targets(3 months), release(1 week), timestamp(1 day). +repository.timestamp.expiration = "2014-10-28 12:08:00" + +# Write all metadata to “path/to/repository/metadata/” +# The common case is to crawl the filesystem for all roles in +# “path/to/repository/metadata/targets/”. +repository.write() +``` + +### Targets + +#### Add Target Files +```python + +``` From 7aeeeb884af5466135865ba0194d019129bf558f Mon Sep 17 00:00:00 2001 From: santiago Date: Tue, 12 Nov 2013 18:15:47 -0500 Subject: [PATCH 73/95] added the "add target" codeblock/section --- tuf/README.md | 45 ++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 44 insertions(+), 1 deletion(-) diff --git a/tuf/README.md b/tuf/README.md index 24ed7ffe..cd33effe 100644 --- a/tuf/README.md +++ b/tuf/README.md @@ -39,7 +39,7 @@ Enter a password for the RSA key: Confirm: ``` At the time of importing the private RSA, a tuf.CryptoError can be thrown if -the key is invalid +the key/password is invalid ### Create a new Repository @@ -133,5 +133,48 @@ repository.write() #### Add Target Files ```python +# Load the repository created in the previous section. This repository contains metadata for +# the top-level roles, but no targets. +repository = load_repository("path/to/repository/") +# Get a list of file paths in a directory, even those in sub-directories. +# This must be relative to an existing directory in the repository, otherwise throw an +# error. +list_of_targets = repository.get_filepaths_in_directory("path/to/repository/targets/", recursive_walk=True, followlinks=True) + +# Add the list of target paths to the metadata of the Targets role. +repository.targets.add_targets(list_of_targets) + +# Individual target files may also be added. +repository.targets.add_target("path/to/repository/targets/file.txt") + +# The private key of the updated targets metadata must be loaded before it can be signed and # written (Note the load_repository() call above). +private_targets_key = import_rsa_privatekey_from_file("path/to/targets_key") +Enter a password for the RSA key: +Confirm: +repository.targets.load_signing_key(private_targets_key) + +# Due to the load_repository(), we must also load the private keys of the other top-level roles +# to generate a valid set of metadata. +private_root_key = import_rsa_privatekey_from_file("path/to/root_key") +Enter a password for the RSA key: +Confirm: +private_root_key2 = import_rsa_privatekey_from_file("path/to/root_key2") +Enter a password for the RSA key: +Confirm: +private_release_key = import_rsa_privatekey_from_file("path/to/release_key") +Enter a password for the RSA key: +Confirm: +private_timestamp_key = import_rsa_privatekey_from_file("path/to/timestamp_key") +Enter a password for the RSA key: +Confirm: + +repository.root.load_signing_key(private_root_key) +repository.root.load_signing_key(private_root_key2) +repository.release.load_signing_key(private_release_key) +repository.timestamp.load_signing_key(private_timestamp_key) + +# Generate new versions of all the top-level metadata and increment version numbers. +repository.write() ``` + From 363d170b8632b0eb9ad6ea0ec36580de7cff6959 Mon Sep 17 00:00:00 2001 From: santiago Date: Tue, 12 Nov 2013 18:17:53 -0500 Subject: [PATCH 74/95] Added the "remove targets" section and codeblock --- tuf/README.md | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/tuf/README.md b/tuf/README.md index cd33effe..e3acb044 100644 --- a/tuf/README.md +++ b/tuf/README.md @@ -178,3 +178,14 @@ repository.timestamp.load_signing_key(private_timestamp_key) repository.write() ``` +#### Remove Target Files +```python +# Continuing from the previous section . . . + +# Remove a target file listed in the “targets” metadata. The target file is not actually deleted +# from the file system. +repository.targets.remove_target("path/to/repository/targets/file.txt") + +# repository.write() creates any new metadata files, updates those that have changed, and any that need updating to make a new “release” (new release.txt and timestamp.txt). +repository.write() +``` From 21d245bd460c40dd02ed34ea8f0ebf7ef763cfb2 Mon Sep 17 00:00:00 2001 From: santiago Date: Tue, 12 Nov 2013 18:20:51 -0500 Subject: [PATCH 75/95] added the "create a delegated role" codeblock --- tuf/README.md | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/tuf/README.md b/tuf/README.md index e3acb044..ecc7f511 100644 --- a/tuf/README.md +++ b/tuf/README.md @@ -189,3 +189,20 @@ repository.targets.remove_target("path/to/repository/targets/file.txt") # repository.write() creates any new metadata files, updates those that have changed, and any that need updating to make a new “release” (new release.txt and timestamp.txt). repository.write() ``` + +### Delegations +```python +# Continuing from the previous section . . . + +# Generate a key for a new delegated role named “unclaimed”. +generate_and_write_rsa_keypair("path/to/unclaimed_key", bits=2048, password="pw") +public_unclaimed_key = import_rsa_publickey_from_file("path/to/unclaimed_key.pub") + +# Make a delegation from “targets” to “targets/unclaimed”, for all targets in “list_of_targets”. +# The delegated role’s full name is not required. +# delegated(rolename, list_of_public_keys, list_of_file_paths, threshold, restricted_paths) +repository.targets.delegate(“unclaimed”, [public_unclaimed_key], list_of_targets) + +# Load the private key of “targets/unclaimed” so that signatures are added and valid metadata +# is created. +``` From dedf18d78f0f7d8e9f0ed75218471ae1b1c99bda Mon Sep 17 00:00:00 2001 From: santiago Date: Tue, 12 Nov 2013 18:25:23 -0500 Subject: [PATCH 76/95] Added the "Revoke Delegated Role" codeblock/section --- tuf/README.md | 25 +++++++++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/tuf/README.md b/tuf/README.md index ecc7f511..81235e36 100644 --- a/tuf/README.md +++ b/tuf/README.md @@ -205,4 +205,29 @@ repository.targets.delegate(“unclaimed”, [public_unclaimed_key], list_of_tar # Load the private key of “targets/unclaimed” so that signatures are added and valid metadata # is created. +private_unclaimed_key = import_rsa_privatekey_from_file(“path/to/unclaimed_key”) +Enter a password for the RSA key: +Confirm: +repository.targets.unclaimed.load_signing_key(private_unclaimed_key) + +# Update attributes of the unclaimed role and add a target file. +repository.targets.unclaimed.expiration = “2014-10-28 12:08:00” +repository.targets.unclaimed.add_target(“path/to/file.txt”) + +# Write the metadata of “targets/unclaimed”, targets, release, and timestamp. +repository.write() +``` + +#### Revoke Delegated Role +```python +# Continuing from the previous section . . . + +# Revoke “targets/unclaimed” and write the metadata of all remaining roles. +repository.targets.revoke(“targets/unclaimed”) + +repository.write() +``` + +```bash +$ mv “path/to/repository/metadata.staged” “path/to/repository/metadata” ``` From bab3e5c4366817639c1b04548071e519ba546838 Mon Sep 17 00:00:00 2001 From: santiago Date: Tue, 12 Nov 2013 18:49:54 -0500 Subject: [PATCH 77/95] Added the diagram in the beginning --- resources/images/TUF repository tools.png | Bin 0 -> 59446 bytes tuf/README.md | 13 +++++++------ 2 files changed, 7 insertions(+), 6 deletions(-) create mode 100644 resources/images/TUF repository tools.png diff --git a/resources/images/TUF repository tools.png b/resources/images/TUF repository tools.png new file mode 100644 index 0000000000000000000000000000000000000000..3fe9cc9dc0a24db7c715c1e891da40cfbf83a6f9 GIT binary patch literal 59446 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∨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 literal 0 HcmV?d00001 diff --git a/tuf/README.md b/tuf/README.md index 81235e36..4dae25aa 100644 --- a/tuf/README.md +++ b/tuf/README.md @@ -1,5 +1,6 @@ ## Create TUF Repository +![Repo Tools Diagram 1](https://raw.github.com/SantiagoTorres/tuf/master/TUF%20repository%20tools.png) ### Keys #### Create RSA Keys @@ -201,18 +202,18 @@ public_unclaimed_key = import_rsa_publickey_from_file("path/to/unclaimed_key.pub # Make a delegation from “targets” to “targets/unclaimed”, for all targets in “list_of_targets”. # The delegated role’s full name is not required. # delegated(rolename, list_of_public_keys, list_of_file_paths, threshold, restricted_paths) -repository.targets.delegate(“unclaimed”, [public_unclaimed_key], list_of_targets) +repository.targets.delegate("unclaimed", [public_unclaimed_key], list_of_targets) # Load the private key of “targets/unclaimed” so that signatures are added and valid metadata # is created. -private_unclaimed_key = import_rsa_privatekey_from_file(“path/to/unclaimed_key”) +private_unclaimed_key = import_rsa_privatekey_from_file("path/to/unclaimed_key") Enter a password for the RSA key: Confirm: repository.targets.unclaimed.load_signing_key(private_unclaimed_key) # Update attributes of the unclaimed role and add a target file. -repository.targets.unclaimed.expiration = “2014-10-28 12:08:00” -repository.targets.unclaimed.add_target(“path/to/file.txt”) +repository.targets.unclaimed.expiration = "2014-10-28 12:08:00" +repository.targets.unclaimed.add_target("path/to/file.txt") # Write the metadata of “targets/unclaimed”, targets, release, and timestamp. repository.write() @@ -223,11 +224,11 @@ repository.write() # Continuing from the previous section . . . # Revoke “targets/unclaimed” and write the metadata of all remaining roles. -repository.targets.revoke(“targets/unclaimed”) +repository.targets.revoke("targets/unclaimed") repository.write() ``` ```bash -$ mv “path/to/repository/metadata.staged” “path/to/repository/metadata” +$ mv "path/to/repository/metadata.staged" "path/to/repository/metadata" ``` From 306769d38d1c3f54a17c04be9d591ed7f38b9a8a Mon Sep 17 00:00:00 2001 From: SantiagoTorres Date: Tue, 12 Nov 2013 18:52:51 -0500 Subject: [PATCH 78/95] Update README.md fixed the broken link for the image --- tuf/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tuf/README.md b/tuf/README.md index 4dae25aa..49708cda 100644 --- a/tuf/README.md +++ b/tuf/README.md @@ -1,6 +1,6 @@ ## Create TUF Repository -![Repo Tools Diagram 1](https://raw.github.com/SantiagoTorres/tuf/master/TUF%20repository%20tools.png) +![Repo Tools Diagram 1](https://raw.github.com/SantiagoTorres/tuf/repository-tools/resources/images/TUF%20repository%20tools.png) ### Keys #### Create RSA Keys From 788f2ed587084b5590dcb8e0cf49630e59d6636b Mon Sep 17 00:00:00 2001 From: santiago Date: Tue, 12 Nov 2013 19:01:34 -0500 Subject: [PATCH 79/95] Added the client-side codeblocks and section --- tuf/README.md | 28 ++++++++++++++++++++++++++++ 1 file changed, 28 insertions(+) diff --git a/tuf/README.md b/tuf/README.md index 4dae25aa..daf04cfa 100644 --- a/tuf/README.md +++ b/tuf/README.md @@ -232,3 +232,31 @@ repository.write() ```bash $ mv "path/to/repository/metadata.staged" "path/to/repository/metadata" ``` + +## Client Setup and Repository TRIAL + +### Using TUF WIthin an Example Client Updater +```python +# The following function creates a directory structure that a client +# downloading new software using tuf (via tuf/client/updater.py) will expect. +# The root.txt metadata file must exist, and also the directories that hold the metadata files +# downloaded from a repository. Software updaters integrating with TUF may use this +# directory to store TUF updates saved on the client side. create_tuf_client_directory() +# moves metadata files “path/to/repository/” to “path/to/client/”. The repository in +# “path/to/repository/” is the repository created in the “Create TUF Repository” section. +create_tuf_client_directory(“path/to/repository/”, “path/to/client/”) +``` + +#### Test TUF Locally +```Bash +# Run the local TUF repository server. +$ cd “path/to/repository/”; python -m SimpleHTTPServer 8001 + +# Retrieve targets from the TUF repository and save them to “path/to/client/”. The +# basic_client.py module is available in “tuf/client/”. +# In a different command-line prompt . . . +$ cd “path/to/client/”; python basic_client.py --repo http://localhost:8001 +``` + + + From f490e68fa25d037b634757ef728c9ec1082bd9a5 Mon Sep 17 00:00:00 2001 From: dachshund Date: Wed, 13 Nov 2013 00:56:19 -0500 Subject: [PATCH 80/95] Add the bundled cryptographic library bindings. --- setup.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/setup.py b/setup.py index d035942f..ba140213 100755 --- a/setup.py +++ b/setup.py @@ -69,6 +69,8 @@ url='https://www.updateframework.com', install_requires=['pycrypto>=2.6'], packages=[ + 'ed25519', + 'evpy', 'tuf', 'tuf.client', 'tuf.compatibility', From 6c376a3442c59674eb3dedd6e87b3da27826b082 Mon Sep 17 00:00:00 2001 From: Trishank Karthik Kuppusamy Date: Wed, 13 Nov 2013 01:15:12 -0500 Subject: [PATCH 81/95] Update README.md with correct import statements --- tuf/README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tuf/README.md b/tuf/README.md index 2d59a828..109d2acc 100644 --- a/tuf/README.md +++ b/tuf/README.md @@ -5,7 +5,7 @@ #### Create RSA Keys ```python -from libtuf import * +from tuf.libtuf import * # Generate and write the first of two root keys for the repository. @@ -29,7 +29,7 @@ The following four files should now exist: ### Import RSA Keys ```python -from libtuf import * +from tuf.libtuf import * #import an existing public key public_root_key = import_rsa_publickey_from_file("path/to/root_key.pub") From cff05ad4ec350691f8ba576a6d1a6e95cb4b20a3 Mon Sep 17 00:00:00 2001 From: Vladimir Diaz Date: Wed, 13 Nov 2013 09:19:46 -0500 Subject: [PATCH 82/95] Update README.md 14: Add spacing to argument list. 16-18: Fix comment formatting and garbled text. 38: Add missing quotation mark. 71: Fix spacing. 75: Fix indentation. 233-234: Add comments and update command. 246-248, 256-259: Modify quotation marks. --- tuf/README.md | 25 +++++++++++++------------ 1 file changed, 13 insertions(+), 12 deletions(-) diff --git a/tuf/README.md b/tuf/README.md index 109d2acc..b22ccf44 100644 --- a/tuf/README.md +++ b/tuf/README.md @@ -11,11 +11,11 @@ from tuf.libtuf import * # Generate and write the first of two root keys for the repository. # The following function creates an RSA key pair, where the private key is saved to # “path/to/root_key” and the public key to “path/to/root_key.pub”. -generate_and_write_rsa_keypair("path/to/root_key",bits=2048,password="password") +generate_and_write_rsa_keypair("path/to/root_key", bits=2048, password="password") -#if thhe key length is unspecified, it defaults to 3072 bits. A length of then -#than 2048 bits prints an error mesage. A password may be supplied as an -#argument, otherwise a user prompt is presented +# If the key length is unspecified, it defaults to 3072 bits. A length of less +# than 2048 bits prints an error mesage. A password may be supplied as an +# argument, otherwise a user prompt is presented. generate_and_write_rsa_keypair("path/to/root_key2") Enter a password for the RSA key: Confirm: @@ -35,7 +35,7 @@ from tuf.libtuf import * public_root_key = import_rsa_publickey_from_file("path/to/root_key.pub") #import an existing private key -private_root_key = import_rsa_privatekey_from_file("path/to/root_key) +private_root_key = import_rsa_privatekey_from_file("path/to/root_key") Enter a password for the RSA key: Confirm: ``` @@ -68,11 +68,11 @@ repository.root.add_key(public_root_key2) # which means the root metadata file is considered valid if it contains at least 2 valid # signatures. repository.root.threshold = 2 -private_root_key2=import_rsa_privatekey_from_file("path/to/root_key2",password="pw") +private_root_key2=import_rsa_privatekey_from_file("path/to/root_key2", password="pw") # Load the root signing keys to the repository, which write() uses to sign the root metadata. # The load_signing_key() method SHOULD warn when the key is NOT explicitly allowed to -# sign for it. +# sign for it. repository.root.load_signing_key(private_root_key) repository.root_load_signing_key(private_root_key2) @@ -230,7 +230,8 @@ repository.write() ``` ```bash -$ mv "path/to/repository/metadata.staged" "path/to/repository/metadata" +# Copy the staged metadata directory changes to the live repository. +$ cp -r "path/to/repository/metadata.staged" "path/to/repository/metadata" ``` ## Client Setup and Repository TRIAL @@ -244,7 +245,7 @@ $ mv "path/to/repository/metadata.staged" "path/to/repository/metadata" # directory to store TUF updates saved on the client side. create_tuf_client_directory() # moves metadata files “path/to/repository/” to “path/to/client/”. The repository in # “path/to/repository/” is the repository created in the “Create TUF Repository” section. -create_tuf_client_directory(“path/to/repository/”, “path/to/client/”) +create_tuf_client_directory("path/to/repository/", "path/to/client/") ``` #### Test TUF Locally @@ -252,10 +253,10 @@ create_tuf_client_directory(“path/to/repository/”, “path/to/client/”) # Run the local TUF repository server. $ cd “path/to/repository/”; python -m SimpleHTTPServer 8001 -# Retrieve targets from the TUF repository and save them to “path/to/client/”. The -# basic_client.py module is available in “tuf/client/”. +# Retrieve targets from the TUF repository and save them to "path/to/client/". The +# basic_client.py module is available in "tuf/client/". # In a different command-line prompt . . . -$ cd “path/to/client/”; python basic_client.py --repo http://localhost:8001 +$ cd "path/to/client/"; python basic_client.py --repo http://localhost:8001 ``` From 8b16b00ab8fba74c3413f52b2557b9f5df138c59 Mon Sep 17 00:00:00 2001 From: Vladimir Diaz Date: Wed, 13 Nov 2013 09:27:00 -0500 Subject: [PATCH 83/95] Update README.md 79-82: Add repository.status() example. --- tuf/README.md | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/tuf/README.md b/tuf/README.md index b22ccf44..af03236e 100644 --- a/tuf/README.md +++ b/tuf/README.md @@ -76,6 +76,11 @@ private_root_key2=import_rsa_privatekey_from_file("path/to/root_key2", password= repository.root.load_signing_key(private_root_key) repository.root_load_signing_key(private_root_key2) +# Print the number of valid signatures and public & private keys of the repository's metadata. +repository.status() +'root' role contains 2 / 2 signatures. +'targets' role contains 0 / 1 public keys. + try: repository.write() # An exception is raised here by write() because the other top-level roles (targets, release, From 6bcaee3926c45da6101ed563b7dddeeb558691d6 Mon Sep 17 00:00:00 2001 From: Vladimir Diaz Date: Wed, 13 Nov 2013 09:46:42 -0500 Subject: [PATCH 84/95] Update README.md 34, 37-38: Fix indentation, add missing period, and expand comment. 93: Add missing period. 152-153: Expand comment to make it clear that target files are not created and previous target paths are not replaced. 245: Fix typo. --- tuf/README.md | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/tuf/README.md b/tuf/README.md index af03236e..167faf42 100644 --- a/tuf/README.md +++ b/tuf/README.md @@ -31,10 +31,11 @@ The following four files should now exist: ```python from tuf.libtuf import * -#import an existing public key +# Import an existing public key. public_root_key = import_rsa_publickey_from_file("path/to/root_key.pub") -#import an existing private key +# Import an existing private key. Importing a private key requires a password, whereas +# importing a public key does not. private_root_key = import_rsa_privatekey_from_file("path/to/root_key") Enter a password for the RSA key: Confirm: @@ -89,7 +90,7 @@ except tuf.Error, e: print e Not enough signatures for '/home/santiago/Documents/o2013/NYU/TUF/repo-tools/repo-real/metadata.staged/root.txt' -# In the next section, update the other top-level roles and create a repository with valid metadata +# In the next section, update the other top-level roles and create a repository with valid metadata. ``` #### Create Timestamp, Release, Targets @@ -148,7 +149,8 @@ repository = load_repository("path/to/repository/") # error. list_of_targets = repository.get_filepaths_in_directory("path/to/repository/targets/", recursive_walk=True, followlinks=True) -# Add the list of target paths to the metadata of the Targets role. +# Add the list of target paths to the metadata of the Targets role. Any target file paths that may already exist +# are NOT replaced. add_targets() does not create or move target files. repository.targets.add_targets(list_of_targets) # Individual target files may also be added. @@ -241,7 +243,7 @@ $ cp -r "path/to/repository/metadata.staged" "path/to/repository/metadata" ## Client Setup and Repository TRIAL -### Using TUF WIthin an Example Client Updater +### Using TUF Within an Example Client Updater ```python # The following function creates a directory structure that a client # downloading new software using tuf (via tuf/client/updater.py) will expect. From fff486d134931a5a5f0bb32a7c94296e0924bc96 Mon Sep 17 00:00:00 2001 From: Vladimir Diaz Date: Wed, 13 Nov 2013 09:58:51 -0500 Subject: [PATCH 85/95] Update README.md Add directory listing of a successful update test. --- tuf/README.md | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/tuf/README.md b/tuf/README.md index 167faf42..9dca48ac 100644 --- a/tuf/README.md +++ b/tuf/README.md @@ -263,8 +263,10 @@ $ cd “path/to/repository/”; python -m SimpleHTTPServer 8001 # Retrieve targets from the TUF repository and save them to "path/to/client/". The # basic_client.py module is available in "tuf/client/". # In a different command-line prompt . . . -$ cd "path/to/client/"; python basic_client.py --repo http://localhost:8001 +$ cd "path/to/client/" +$ ls +metadata/ +$ python basic_client.py --repo http://localhost:8001 +$ ls +metadata/ targets/ ``` - - - From 19c659f5c0dc5682718a81f417c91d3ce9b638d8 Mon Sep 17 00:00:00 2001 From: Vladimir Diaz Date: Wed, 13 Nov 2013 11:12:07 -0500 Subject: [PATCH 86/95] Update README.md 91-92: Move Metadata file output to new line to avoid wrapping. 153-154: Fix length of comment block to avoid wrapping. 159-160: Fix garbled comment block. 199-200: Shorten lengthy comment. --- tuf/README.md | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/tuf/README.md b/tuf/README.md index 9dca48ac..57342bcf 100644 --- a/tuf/README.md +++ b/tuf/README.md @@ -88,7 +88,8 @@ try: # and timestamp) have not been configured with keys. except tuf.Error, e: print e -Not enough signatures for '/home/santiago/Documents/o2013/NYU/TUF/repo-tools/repo-real/metadata.staged/root.txt' +Not enough signatures for +'/home/santiago/Documents/o2013/NYU/TUF/repo-tools/repo-real/metadata.staged/root.txt' # In the next section, update the other top-level roles and create a repository with valid metadata. ``` @@ -147,16 +148,18 @@ repository = load_repository("path/to/repository/") # Get a list of file paths in a directory, even those in sub-directories. # This must be relative to an existing directory in the repository, otherwise throw an # error. -list_of_targets = repository.get_filepaths_in_directory("path/to/repository/targets/", recursive_walk=True, followlinks=True) +list_of_targets = repository.get_filepaths_in_directory("path/to/repository/targets/", + recursive_walk=True, followlinks=True) -# Add the list of target paths to the metadata of the Targets role. Any target file paths that may already exist -# are NOT replaced. add_targets() does not create or move target files. +# Add the list of target paths to the metadata of the Targets role. Any target file paths +# that may already exist are NOT replaced. add_targets() does not create or move target files. repository.targets.add_targets(list_of_targets) # Individual target files may also be added. repository.targets.add_target("path/to/repository/targets/file.txt") -# The private key of the updated targets metadata must be loaded before it can be signed and # written (Note the load_repository() call above). +# The private key of the updated targets metadata must be loaded before it can be signed and +# written (Note the load_repository() call above). private_targets_key = import_rsa_privatekey_from_file("path/to/targets_key") Enter a password for the RSA key: Confirm: @@ -194,7 +197,8 @@ repository.write() # from the file system. repository.targets.remove_target("path/to/repository/targets/file.txt") -# repository.write() creates any new metadata files, updates those that have changed, and any that need updating to make a new “release” (new release.txt and timestamp.txt). +# repository.write() creates any new metadata files, updates those that have changed, and any that +# need updating to make a new “release” (new release.txt and timestamp.txt). repository.write() ``` From 8520c49cf330dd88c55910d22af348633abe4f4a Mon Sep 17 00:00:00 2001 From: Vladimir Diaz Date: Wed, 13 Nov 2013 11:21:59 -0500 Subject: [PATCH 87/95] Update README.md 238: Remove full rolename argument to revoke(). --- tuf/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tuf/README.md b/tuf/README.md index 57342bcf..c4580ddb 100644 --- a/tuf/README.md +++ b/tuf/README.md @@ -235,7 +235,7 @@ repository.write() # Continuing from the previous section . . . # Revoke “targets/unclaimed” and write the metadata of all remaining roles. -repository.targets.revoke("targets/unclaimed") +repository.targets.revoke("unclaimed") repository.write() ``` From 86e8f0b771bd1e57ec7de0598791da39614c800a Mon Sep 17 00:00:00 2001 From: vladdd Date: Wed, 13 Nov 2013 11:44:00 -0500 Subject: [PATCH 88/95] Switch default ed25519 cryptography library to 'ed25519' Modify so that testing the repository tools does not raise errors for users without pynacl installed. --- tuf/conf.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tuf/conf.py b/tuf/conf.py index f25d2075..a48f1b75 100755 --- a/tuf/conf.py +++ b/tuf/conf.py @@ -70,4 +70,4 @@ RSA_CRYPTO_LIBRARY = 'pycrypto' # Supported ed25519 cryptography libraries: ['pynacl', 'ed25519'] -ED25519_CRYPTO_LIBRARY = 'pynacl' +ED25519_CRYPTO_LIBRARY = 'ed25519' From b2e9fca1425ccba098819eb4ac8d185ab8bf0b79 Mon Sep 17 00:00:00 2001 From: dachshund Date: Wed, 13 Nov 2013 11:48:22 -0500 Subject: [PATCH 89/95] Remove evpy. --- setup.py | 1 - 1 file changed, 1 deletion(-) diff --git a/setup.py b/setup.py index ba140213..ccd855f4 100755 --- a/setup.py +++ b/setup.py @@ -70,7 +70,6 @@ install_requires=['pycrypto>=2.6'], packages=[ 'ed25519', - 'evpy', 'tuf', 'tuf.client', 'tuf.compatibility', From c860d277874bd3d4d4cdb37bdd88c42c1dab7e76 Mon Sep 17 00:00:00 2001 From: vladdd Date: Wed, 13 Nov 2013 11:52:15 -0500 Subject: [PATCH 90/95] Add 'tuf/client/basic_client.py' to setup.py --- setup.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/setup.py b/setup.py index ba140213..4e20638e 100755 --- a/setup.py +++ b/setup.py @@ -84,6 +84,7 @@ 'tuf/repo/quickstart.py', 'tuf/pushtools/push.py', 'tuf/pushtools/receivetools/receive.py', - 'tuf/repo/signercli.py' + 'tuf/repo/signercli.py', + 'tuf/client/basic_client.py' ] ) From 6cc0b9067290d81333960a617f9f1ab65ef51faa Mon Sep 17 00:00:00 2001 From: vladdd Date: Wed, 13 Nov 2013 14:28:34 -0500 Subject: [PATCH 91/95] Remove invalid signatures from signables prior to final repository.write() signables may contain signatures (now invalid) from previous versions of metadata loaded with load_repository(). --- tuf/libtuf.py | 40 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 40 insertions(+) diff --git a/tuf/libtuf.py b/tuf/libtuf.py index add76913..f862dee0 100755 --- a/tuf/libtuf.py +++ b/tuf/libtuf.py @@ -364,7 +364,10 @@ def write(self, write_partial=False): root_filename) signed_root['signatures'].extend(roleinfo['signatures']) if tuf.sig.verify(signed_root, 'root') or write_partial: + if not write_partial: + _remove_invalid_signatures(signed_root) write_metadata_file(signed_root, root_filename, compression=None) + else: message = 'Not enough signatures for '+repr(root_filename) raise tuf.Error(message) @@ -382,7 +385,10 @@ def write(self, write_partial=False): signed_targets['signatures'].extend(roleinfo['signatures']) if tuf.sig.verify(signed_targets, 'targets') or write_partial: + if not write_partial: + _remove_invalid_signatures(signed_targets) write_metadata_file(signed_targets, targets_filename, compression=None) + else: message = 'Not enough signatures for '+repr(targets_filename) raise tuf.Error(message) @@ -398,7 +404,10 @@ def write(self, write_partial=False): signed_release['signatures'].extend(roleinfo['signatures']) if tuf.sig.verify(signed_release, 'release') or write_partial: + if not write_partial: + _remove_invalid_signatures(signed_release) write_metadata_file(signed_release, release_filename, compression=None) + else: message = 'Not enough signatures for '+repr(release_filename) raise tuf.Error(message) @@ -416,7 +425,10 @@ def write(self, write_partial=False): signed_timestamp['signatures'].extend(roleinfo['signatures']) if tuf.sig.verify(signed_timestamp, 'timestamp') or write_partial: + if not write_partial: + _remove_invalid_signatures(signed_timestamp) write_metadata_file(signed_timestamp, timestamp_filename, compression=None) + else: message = 'Not enough signatures for '+repr(timestamp_filename) raise tuf.Error(message) @@ -1631,6 +1643,31 @@ def _check_role_keys(rolename): +def _remove_invalid_signatures(signable): + """ + Remove invalid signatures from 'signable'. + 'signable' may contain signatures (invalid) from previous versions + of the metadata and loaded with load_repository(). 'signable' is modified. + """ + + for signature in signable['signatures']: + data = tuf.formats.encode_canonical(signable['signed']) + keyid = signature['keyid'] + key = None + + # Remove 'signature' from 'signable' if the listed keyid does not exist. + try: + key = tuf.keydb.get_key(keyid) + except tuf.UnknownKeyError, e: + signable['signatures'].remove(signature) + + # Remove signature from 'signable' if it is invalid. + if not tuf.keys.verify_signature(key, signature, data): + signable['signatures'].remove(signature) + + + + def create_new_repository(repository_directory): """ @@ -2767,7 +2804,10 @@ def write_delegated_metadata_file(repository_directory, targets_directory, for signature in signatures: signable['signatures'].append(signature) if tuf.sig.verify(signable, rolename) or write_partial: + if not write_partial: + _remove_invalid_signatures(signable) write_metadata_file(signable, metadata_filepath) + else: raise tuf.Error('Not enough signatures for: '+repr(metadata_filepath)) From 9acebfa1bea493016183c9e977252c82dce0e50b Mon Sep 17 00:00:00 2001 From: Vladimir Diaz Date: Wed, 13 Nov 2013 14:46:28 -0500 Subject: [PATCH 92/95] Update README.md Add sections, comments, and command-line output related to target files. Fix garbled section heading. --- tuf/README.md | 26 ++++++++++++++++++-------- 1 file changed, 18 insertions(+), 8 deletions(-) diff --git a/tuf/README.md b/tuf/README.md index c4580ddb..3faa24a7 100644 --- a/tuf/README.md +++ b/tuf/README.md @@ -88,8 +88,7 @@ try: # and timestamp) have not been configured with keys. except tuf.Error, e: print e -Not enough signatures for -'/home/santiago/Documents/o2013/NYU/TUF/repo-tools/repo-real/metadata.staged/root.txt' +Not enough signatures for 'path/to/repository/metadata.staged/root.txt' # In the next section, update the other top-level roles and create a repository with valid metadata. ``` @@ -140,6 +139,13 @@ repository.write() ### Targets #### Add Target Files +```Bash +# Create and save target files to the repository. +$ cd path/to/repository/targets/ +$ echo 'file1' > file1.txt +$ echo 'file2' > file2.txt +``` + ```python # Load the repository created in the previous section. This repository contains metadata for # the top-level roles, but no targets. @@ -156,7 +162,7 @@ list_of_targets = repository.get_filepaths_in_directory("path/to/repository/targ repository.targets.add_targets(list_of_targets) # Individual target files may also be added. -repository.targets.add_target("path/to/repository/targets/file.txt") +repository.targets.add_target("path/to/repository/targets/file1.txt") # The private key of the updated targets metadata must be loaded before it can be signed and # written (Note the load_repository() call above). @@ -195,7 +201,7 @@ repository.write() # Remove a target file listed in the “targets” metadata. The target file is not actually deleted # from the file system. -repository.targets.remove_target("path/to/repository/targets/file.txt") +repository.targets.remove_target("path/to/repository/targets/file1.txt") # repository.write() creates any new metadata files, updates those that have changed, and any that # need updating to make a new “release” (new release.txt and timestamp.txt). @@ -224,7 +230,7 @@ repository.targets.unclaimed.load_signing_key(private_unclaimed_key) # Update attributes of the unclaimed role and add a target file. repository.targets.unclaimed.expiration = "2014-10-28 12:08:00" -repository.targets.unclaimed.add_target("path/to/file.txt") +repository.targets.unclaimed.add_target("path/to/repository/targets/file1.txt") # Write the metadata of “targets/unclaimed”, targets, release, and timestamp. repository.write() @@ -245,7 +251,7 @@ repository.write() $ cp -r "path/to/repository/metadata.staged" "path/to/repository/metadata" ``` -## Client Setup and Repository TRIAL +## Client Setup and Repository Trial ### Using TUF Within an Example Client Updater ```python @@ -271,6 +277,10 @@ $ cd "path/to/client/" $ ls metadata/ $ python basic_client.py --repo http://localhost:8001 -$ ls -metadata/ targets/ +$ ls . targets/ +.: +metadata targets tuf.log + +targets/: +file1.txt file2.txt ``` From 85d2b7fb199d2a8f3d1d88e2aa8001e83577d0f8 Mon Sep 17 00:00:00 2001 From: vladdd Date: Thu, 14 Nov 2013 14:24:07 -0500 Subject: [PATCH 93/95] Add updates and features following review Add support for compressed metadata, path hash prefixes, delegated role revocation fix, removal of obsolete metadata files, and other minor changes. --- tuf/client/updater.py | 5 +- tuf/formats.py | 8 ++ tuf/libtuf.py | 208 ++++++++++++++++++++++++++---------------- tuf/roledb.py | 2 +- 4 files changed, 141 insertions(+), 82 deletions(-) diff --git a/tuf/client/updater.py b/tuf/client/updater.py index 7d250b68..22c80262 100755 --- a/tuf/client/updater.py +++ b/tuf/client/updater.py @@ -1822,7 +1822,10 @@ def _refresh_targets_metadata(self, rolename='targets', include_delegations=Fals if metadata_path == rolename + '.txt': roles_to_update.append(metadata_path[:-len('.txt')]) elif include_delegations and metadata_path.startswith(role_prefix): - roles_to_update.append(metadata_path[:-len('.txt')]) + # Add delegated roles. Skip roles names containing compression + # extensions. + if metadata_path.endswith('.txt'): + roles_to_update.append(metadata_path[:-len('.txt')]) # Remove the 'targets' role because it gets updated when the targets.txt # file is updated in _update_metadata_if_changed('targets'). diff --git a/tuf/formats.py b/tuf/formats.py index 426bcd1b..ee92487a 100755 --- a/tuf/formats.py +++ b/tuf/formats.py @@ -348,6 +348,13 @@ # maintainers may later modify this value (TIME_SCHEMA). EXPIRATION_SCHEMA = SCHEMA.Integer(lo=86400) +# Supported compression extension (e.g., 'gz'). +COMPRESSION_SCHEMA = SCHEMA.OneOf([SCHEMA.String(''), SCHEMA.String('gz')]) + +# List of supported compression extensions. +COMPRESSIONS_SCHEMA = SCHEMA.ListOf( + SCHEMA.OneOf([SCHEMA.String(''), SCHEMA.String('gz')])) + # tuf.roledb ROLEDB_SCHEMA = SCHEMA.Object( object_name='ROLEDB_SCHEMA', @@ -357,6 +364,7 @@ version=SCHEMA.Optional(METADATAVERSION_SCHEMA), expires=SCHEMA.Optional(SCHEMA.OneOf([EXPIRATION_SCHEMA, TIME_SCHEMA])), signatures=SCHEMA.Optional(SCHEMA.ListOf(SIGNATURE_SCHEMA)), + compressions=SCHEMA.Optional(COMPRESSIONS_SCHEMA), paths=SCHEMA.Optional(RELPATHS_SCHEMA), path_hash_prefixes=SCHEMA.Optional(PATH_HASH_PREFIXES_SCHEMA), delegations=SCHEMA.Optional(DELEGATIONS_SCHEMA)) diff --git a/tuf/libtuf.py b/tuf/libtuf.py index f862dee0..c358b3cd 100755 --- a/tuf/libtuf.py +++ b/tuf/libtuf.py @@ -30,6 +30,7 @@ import tempfile import shutil import json +import gzip import tuf import tuf.formats @@ -62,7 +63,7 @@ TARGETS_DIRECTORY_NAME = 'targets' # The file extension of TUF metadata files. -METADATA_EXTENSION = '.txt' +METADATA_EXTENSIONS = ['.txt', '.txt.gz'] # Expiration date delta, in seconds, of the top-level roles. A metadata # expiration date is set by taking the current time and adding the expiration @@ -181,6 +182,7 @@ def status(self): roleinfo['paths'], roleinfo['delegations'], roleinfo['signatures'], + roleinfo['compressions'], write_partial=False) except tuf.Error, e: insufficient_signatures.append(delegated_role) @@ -215,7 +217,8 @@ def status(self): print(message) if tuf.sig.verify(signed_root, 'root'): - write_metadata_file(signed_root, root_filename, compression=None) + for compression in root_roleinfo['compressions']: + write_metadata_file(signed_root, root_filename, compression) else: return @@ -242,8 +245,9 @@ def status(self): repr(targets_status['threshold'])+' signatures.' print(message) - if tuf.sig.verify(signed_targets, 'targets'): - write_metadata_file(signed_targets, targets_filename, compression=None) + if tuf.sig.verify(signed_targets, 'targets'): + for compression in targets_roleinfo['compressions']: + write_metadata_file(signed_targets, targets_filename, compression) else: return @@ -268,8 +272,9 @@ def status(self): repr(len(release_status['good_sigs']))+' / '+ \ repr(release_status['threshold'])+' signatures.' print(message) - if tuf.sig.verify(signed_release, 'release'): - write_metadata_file(signed_release, release_filename, compression=None) + if tuf.sig.verify(signed_release, 'release'): + for compression in release_info['compressions']: + write_metadata_file(signed_release, release_filename, compression) else: return @@ -283,7 +288,7 @@ def status(self): timestamp_metadata = generate_timestamp_metadata(release_filename, timestamp_roleinfo['version'], timestamp_roleinfo['expires'], - compressions=()) + release_roleinfo['compressions']) signed_timestamp= sign_metadata(timestamp_metadata, timestamp_roleinfo['signing_keyids'], @@ -296,9 +301,9 @@ def status(self): repr(len(timestamp_status['good_sigs']))+' / '+ \ repr(timestamp_status['threshold'])+' signatures.' print(message) - if tuf.sig.verify(signed_timestamp, 'timestamp'): - write_metadata_file(signed_timestamp, timestamp_filename, - compression=None) + if tuf.sig.verify(signed_timestamp, 'timestamp'): + for compressions in timestamp_roleinfo['compressions']: + write_metadata_file(signed_timestamp, timestamp_filename, compression) else: return @@ -352,9 +357,9 @@ def write(self, write_partial=False): roleinfo['paths'], roleinfo['delegations'], roleinfo['signatures'], + roleinfo['compressions'], write_partial) - # Generate the 'root.txt' metadata file. roleinfo = tuf.roledb.get_roleinfo('root') @@ -366,7 +371,8 @@ def write(self, write_partial=False): if tuf.sig.verify(signed_root, 'root') or write_partial: if not write_partial: _remove_invalid_signatures(signed_root) - write_metadata_file(signed_root, root_filename, compression=None) + for compression in roleinfo['compressions']: + write_metadata_file(signed_root, root_filename, compression) else: message = 'Not enough signatures for '+repr(root_filename) @@ -387,7 +393,8 @@ def write(self, write_partial=False): if tuf.sig.verify(signed_targets, 'targets') or write_partial: if not write_partial: _remove_invalid_signatures(signed_targets) - write_metadata_file(signed_targets, targets_filename, compression=None) + for compression in roleinfo['compressions']: + write_metadata_file(signed_targets, targets_filename, compression) else: message = 'Not enough signatures for '+repr(targets_filename) @@ -396,6 +403,7 @@ def write(self, write_partial=False): # Generate the 'release.txt' metadata file. roleinfo = tuf.roledb.get_roleinfo('release') + release_compressions = roleinfo['compressions'] release_metadata = generate_release_metadata(self._metadata_directory, roleinfo['version'], roleinfo['expires']) @@ -406,7 +414,8 @@ def write(self, write_partial=False): if tuf.sig.verify(signed_release, 'release') or write_partial: if not write_partial: _remove_invalid_signatures(signed_release) - write_metadata_file(signed_release, release_filename, compression=None) + for compression in roleinfo['compressions']: + write_metadata_file(signed_release, release_filename, compression) else: message = 'Not enough signatures for '+repr(release_filename) @@ -418,7 +427,7 @@ def write(self, write_partial=False): timestamp_metadata = generate_timestamp_metadata(release_filename, roleinfo['version'], roleinfo['expires'], - compressions=()) + release_compressions) signed_timestamp = sign_metadata(timestamp_metadata, roleinfo['signing_keyids'], timestamp_filename) @@ -427,11 +436,14 @@ def write(self, write_partial=False): if tuf.sig.verify(signed_timestamp, 'timestamp') or write_partial: if not write_partial: _remove_invalid_signatures(signed_timestamp) - write_metadata_file(signed_timestamp, timestamp_filename, compression=None) + for compression in roleinfo['compressions']: + write_metadata_file(signed_timestamp, timestamp_filename, compression) else: message = 'Not enough signatures for '+repr(timestamp_filename) raise tuf.Error(message) + + _delete_obsolete_metadata(self._metadata_directory) @@ -512,12 +524,7 @@ class Metadata(object): def __init__(self): self._rolename = None - self._signing_keys = [] - self._version = 1 - self._threshold = 1 - self._role_keys = [] - def add_key(self, key): @@ -555,9 +562,6 @@ def add_key(self, key): roleinfo['keyids'].append(keyid) tuf.roledb.update_roleinfo(self._rolename, roleinfo) - if keyid not in self._role_keys: - self._role_keys.append(keyid) - def remove_key(self, key): @@ -592,9 +596,6 @@ def remove_key(self, key): roleinfo['keyids'].remove(keyid) tuf.roledb.update_roleinfo(self._rolename, roleinfo) - if keyid in self._role_keys: - self._role_keys.remove(keyid) - def load_signing_key(self, key): @@ -758,19 +759,21 @@ def signatures(self): """ roleinfo = tuf.roledb.get_roleinfo(self.rolename) - - return roleinfo['signatures'] + signature = roleinfo['signatures'] + + return signatures @property - def role_keys(self): + def keys(self): """ """ roleinfo = tuf.roledb.get_roleinfo(self.rolename) - - return roleinfo['keyids'] + keyids = roleinfo['keyids'] + + return keyids @@ -826,7 +829,6 @@ def version(self, version): roleinfo['version'] = version tuf.roledb.update_roleinfo(self._rolename, roleinfo) - self._version = version @@ -834,8 +836,11 @@ def version(self, version): def threshold(self): """ """ + + roleinfo = tuf.roledb.get_roleinfo(self._rolename) + threshold = roleinfo['threshold'] - return self._threshold + return threshold @@ -951,30 +956,36 @@ def signing_keys(self): """ roleinfo = tuf.roledb.get_roleinfo(self.rolename) - - return roleinfo['signing_keyids'] + signing_keyids = roleinfo['signing_keyids'] + + return signing_keyids - def write_partial(self, object): + @property + def compressions(self): """ - - #PARTIAL_METADATA_SUFFIX - - >>> - >>> - >>> - - - - - - - - """ - - raise NotImplementedError() + + tuf.roledb.get_roleinfo(self.rolename) + compressions = roleinfo['compressions'] + + return compressions + + + + @compressions.setter + def compressions(self, compression_list): + """ + """ + + # Does 'compression_name' have the correct format? + # Raise 'tuf.FormatError' if it is improperly formatted. + tuf.formats.COMPRESSIONS_SCHEMA.check_match(compression_list) + + roleinfo = tuf.roledb.get_roleinfo(self.rolename) + roleinfo['compressions'].extend(compression_list) + tuf.roledb.update_roleinfo(self.rolename, roleinfo) @@ -1003,21 +1014,17 @@ def __init__(self): self._rolename = 'root' - expiration = tuf.formats.format_time(time.time()+ROOT_EXPIRATION) roleinfo = {'keyids': [], 'signing_keyids': [], 'threshold': 1, - 'signatures': [], 'version': 1, 'expires': expiration} + 'signatures': [], 'version': 1, 'compressions': [''], + 'expires': expiration} try: tuf.roledb.add_role(self._rolename, roleinfo) except tuf.RoleAlreadyExistsError, e: pass - def write_partial(self): - pass - - @@ -1047,18 +1054,15 @@ def __init__(self): expiration = tuf.formats.format_time(time.time()+TIMESTAMP_EXPIRATION) roleinfo = {'keyids': [], 'signing_keyids': [], 'threshold': 1, - 'signatures': [], 'version': 1, 'expires': expiration} + 'signatures': [], 'version': 1, 'compressions': [''], + 'expires': expiration} try: - tuf.roledb.add_role(self._rolename, roleinfo) + tuf.roledb.add_role(self.rolename, roleinfo) except tuf.RoleAlreadyExistsError, e: pass - def write_partial(self): - pass - - @@ -1088,7 +1092,8 @@ def __init__(self): expiration = tuf.formats.format_time(time.time()+RELEASE_EXPIRATION) roleinfo = {'keyids': [], 'signing_keyids': [], 'threshold': 1, - 'signatures': [], 'version': 1, 'expires': expiration} + 'signatures': [], 'version': 1, 'compressions': [''], + 'expires': expiration} try: tuf.roledb.add_role(self._rolename, roleinfo) @@ -1148,6 +1153,7 @@ def __init__(self, targets_directory, rolename, roleinfo=None): 'signing_keyids': [], 'threshold': 1, 'version': 1, + 'compressions': [''], 'expires': expiration, 'signatures': [], 'paths': [], @@ -1191,12 +1197,6 @@ def target_files(self): - def write_partial(self): - pass - - - - def add_target(self, filepath): """ @@ -1371,7 +1371,7 @@ def remove_target(self, filepath): def delegate(self, rolename, public_keys, list_of_targets, - threshold=1, restricted_paths=None): + threshold=1, restricted_paths=None, path_hash_prefixes=None): """ 'targets' is a list of target filepaths, and can be empty. @@ -1409,9 +1409,10 @@ def delegate(self, rolename, public_keys, list_of_targets, tuf.formats.ANYKEYLIST_SCHEMA.check_match(public_keys) tuf.formats.RELPATHS_SCHEMA.check_match(list_of_targets) tuf.formats.THRESHOLD_SCHEMA.check_match(threshold) - if restricted_paths is not None: tuf.formats.RELPATHS_SCHEMA.check_match(restricted_paths) + if path_hash_prefixes is not None: + tuf.formats.PATH_HASH_PREFIXES_SCHEMA.check_match(path_hash_prefixes) full_rolename = self._rolename+'/'+rolename keyids = [] @@ -1466,6 +1467,7 @@ def delegate(self, rolename, public_keys, list_of_targets, 'signing_keyids': [], 'threshold': threshold, 'version': 1, + 'compressions': [''], 'expires': expiration, 'signatures': [], 'paths': relative_targetpaths, @@ -1486,6 +1488,8 @@ def delegate(self, rolename, public_keys, list_of_targets, 'paths': roleinfo['paths']} if restricted_paths is not None: roleinfo['paths'] = relative_restricted_paths + if path_hash_prefixes is not None: + roleinfo['path_hash_prefixes'] = path_hash_prefixes current_roleinfo['delegations']['roles'].append(roleinfo) tuf.roledb.update_roleinfo(self.rolename, current_roleinfo) @@ -1530,11 +1534,13 @@ def revoke(self, rolename): for role in roleinfo['delegations']['roles']: if role['name'] == full_rolename: roleinfo['delegations']['roles'].remove(role) + + tuf.roledb.update_roleinfo(self.rolename, roleinfo) # Remove from 'tuf.roledb.py'. The delegated roles of 'rolename' are also # removed. tuf.roledb.remove_role(full_rolename) - + # Remove the rolename attribute from the current role. self.__delattr__(rolename) @@ -1669,6 +1675,29 @@ def _remove_invalid_signatures(signable): +def _delete_obsolete_metadata(metadata_directory): + """ + """ + + targets_metadata = os.path.join(metadata_directory, 'targets') + + if os.path.exists(targets_metadata) and os.path.isdir(targets_metadata): + for directory_path, junk_directories, files in os.walk(targets_metadata): + + # 'files' here is a list of target file names. + for basename in files: + metadata_path = os.path.join(directory_path, basename) + metadata_name = metadata_path[len(metadata_directory):].lstrip(os.path.sep) + for metadata_extension in METADATA_EXTENSIONS: + if metadata_name.endswith(metadata_extension): + metadata_name = metadata_name[:-len(metadata_extension)] + if not tuf.roledb.role_exists(metadata_name): + os.remove(metadata_path) + + + + + def create_new_repository(repository_directory): """ @@ -1799,7 +1828,11 @@ def load_repository(repository_directory): roleinfo['signatures'] = [] for signature in signable['signatures']: roleinfo['signatures'].append(signature) + + if os.path.exists(root_filename+'.gz'): + roleinfo['compressions'].append('gz') tuf.roledb.update_roleinfo('root', roleinfo) + else: message = 'Cannot load the required root file: '+repr(root_filename) raise tuf.RepositoryError(message) @@ -1822,6 +1855,8 @@ def load_repository(repository_directory): roleinfo['version'] = targets_metadata['version'] roleinfo['expires'] = targets_metadata['expires'] roleinfo['delegations'] = targets_metadata['delegations'] + if os.path.exists(targets_filename+'.gz'): + roleinfo['compressions'].append('gz') tuf.roledb.update_roleinfo('targets', roleinfo) # Add the keys specified in the delegations field of the Targets role. @@ -1857,6 +1892,8 @@ def load_repository(repository_directory): roleinfo = tuf.roledb.get_roleinfo('release') roleinfo['expires'] = release_metadata['expires'] roleinfo['version'] = release_metadata['version'] + if os.path.exists(release_filename+'.gz'): + roleinfo['compressions'].append('gz') tuf.roledb.update_roleinfo('release', roleinfo) else: @@ -1873,6 +1910,8 @@ def load_repository(repository_directory): roleinfo = tuf.roledb.get_roleinfo('timestamp') roleinfo['expires'] = timestamp_metadata['expires'] roleinfo['version'] = timestamp_metadata['version'] + if os.path.exists(timestamp_filename+'.gz'): + roleinfo['compressions'].append('gz') tuf.roledb.update_roleinfo('timestamp', roleinfo) else: @@ -1910,6 +1949,8 @@ def load_repository(repository_directory): roleinfo['expires'] = metadata_object['expires'] roleinfo['paths'] = metadata_object['targets'].keys() + if os.path.exists(timestamp_filename+'.gz'): + roleinfo['compressions'].append('gz') tuf.roledb.update_roleinfo(metadata_name, roleinfo) new_targets_object = Targets(targets_directory, metadata_name, roleinfo) @@ -2447,6 +2488,7 @@ def generate_release_metadata(metadata_directory, version, expiration_date): filedict = {} filedict[ROOT_FILENAME] = get_metadata_file_info(root_filename) filedict[TARGETS_FILENAME] = get_metadata_file_info(targets_filename) + # Walk the 'targets/' directory and generate the file info for all # the files listed there. This information is stored in the 'meta' @@ -2459,7 +2501,11 @@ def generate_release_metadata(metadata_directory, version, expiration_date): for basename in files: metadata_path = os.path.join(directory_path, basename) metadata_name = metadata_path[len(metadata_directory):].lstrip(os.path.sep) - filedict[metadata_name] = get_metadata_file_info(metadata_path) + for metadata_extension in METADATA_EXTENSIONS: + if metadata_name.endswith(metadata_extension): + rolename = metadata_name[:-len(metadata_extension)] + if tuf.roledb.role_exists(rolename): + filedict[metadata_name] = get_metadata_file_info(metadata_path) # Generate the release metadata object. release_metadata = tuf.formats.ReleaseFile.make_metadata(version, @@ -2624,7 +2670,7 @@ def sign_metadata(metadata, keyids, filename): -def write_metadata_file(metadata, filename, compression=None): +def write_metadata_file(metadata, filename, compression=''): """ Create the file containing the metadata. @@ -2659,6 +2705,7 @@ def write_metadata_file(metadata, filename, compression=None): # Raise 'tuf.FormatError' if there is a mismatch. tuf.formats.SIGNABLE_SCHEMA.check_match(metadata) tuf.formats.PATH_SCHEMA.check_match(filename) + tuf.formats.COMPRESSION_SCHEMA.check_match(compression) # Verify 'filename' directory. _check_directory(os.path.dirname(filename)) @@ -2671,7 +2718,7 @@ def write_metadata_file(metadata, filename, compression=None): filename_with_compression = filename # Take care of compression. - if compression is None: + if not len(compression): logger.info('No compression for '+str(filename)) file_object = open(filename_with_compression, 'w') @@ -2711,7 +2758,7 @@ def write_metadata_file(metadata, filename, compression=None): def write_delegated_metadata_file(repository_directory, targets_directory, rolename, version, expiration, keyids, list_of_targets, delegations, signatures, - write_partial=False): + compressions, write_partial=False): """ Build the targets metadata file using the signing keys in @@ -2806,7 +2853,8 @@ def write_delegated_metadata_file(repository_directory, targets_directory, if tuf.sig.verify(signable, rolename) or write_partial: if not write_partial: _remove_invalid_signatures(signable) - write_metadata_file(signable, metadata_filepath) + for compression in compressions: + write_metadata_file(signable, metadata_filepath, compression) else: raise tuf.Error('Not enough signatures for: '+repr(metadata_filepath)) diff --git a/tuf/roledb.py b/tuf/roledb.py index 6b222504..7cba394c 100755 --- a/tuf/roledb.py +++ b/tuf/roledb.py @@ -93,7 +93,7 @@ def create_roledb_from_root_metadata(root_metadata): roleinfo['signatures'] = [] roleinfo['signing_keyids'] = [] - + roleinfo['compressions'] = [''] if rolename.startswith('targets'): roleinfo['delegations'] = {'keys': {}, 'roles': []} From d2d6794dad5e6168508de355e2f37a5e234c7498 Mon Sep 17 00:00:00 2001 From: vladdd Date: Thu, 14 Nov 2013 20:36:28 -0500 Subject: [PATCH 94/95] Support compressed metadata for all role types and minor edits --- tuf/libtuf.py | 51 +++++++++++++++++++++++++++++++++++++-------------- 1 file changed, 37 insertions(+), 14 deletions(-) diff --git a/tuf/libtuf.py b/tuf/libtuf.py index c358b3cd..cfc14edd 100755 --- a/tuf/libtuf.py +++ b/tuf/libtuf.py @@ -62,9 +62,12 @@ METADATA_DIRECTORY_NAME = 'metadata.staged' TARGETS_DIRECTORY_NAME = 'targets' -# The file extension of TUF metadata files. +# The supported file extensions of TUF metadata files. METADATA_EXTENSIONS = ['.txt', '.txt.gz'] +# The recognized compression extensions. +SUPPORTED_COMPRESSION_EXTENSIONS = ['.gz'] + # Expiration date delta, in seconds, of the top-level roles. A metadata # expiration date is set by taking the current time and adding the expiration # seconds listed below. @@ -273,7 +276,7 @@ def status(self): repr(release_status['threshold'])+' signatures.' print(message) if tuf.sig.verify(signed_release, 'release'): - for compression in release_info['compressions']: + for compression in release_roleinfo['compressions']: write_metadata_file(signed_release, release_filename, compression) else: return @@ -759,7 +762,7 @@ def signatures(self): """ roleinfo = tuf.roledb.get_roleinfo(self.rolename) - signature = roleinfo['signatures'] + signatures = roleinfo['signatures'] return signatures @@ -1144,7 +1147,6 @@ def __init__(self, targets_directory, rolename, roleinfo=None): self._targets_directory = targets_directory self._rolename = rolename self._target_files = [] - self._delegations = {} expiration = tuf.formats.format_time(time.time()+TARGETS_EXPIRATION) @@ -1253,9 +1255,10 @@ def add_target(self, filepath): # Update the role's 'tuf.roledb.py' entry and 'self._target_files'. targets_directory_length = len(self._targets_directory) roleinfo = tuf.roledb.get_roleinfo(self._rolename) - roleinfo['paths'].append(filepath[targets_directory_length+1:]) + relative_path = filepath[targets_directory_length+1:] + if relative_path not in roleinfo['paths']: + roleinfo['paths'].append(relative_path) tuf.roledb.update_roleinfo(self._rolename, roleinfo) - self._target_files.append(filepath) else: message = repr(filepath)+' is not a valid file.' @@ -1313,10 +1316,9 @@ def add_targets(self, list_of_targets): raise tuf.Error(message) # Update the role's target_files and its 'tuf.roledb.py' entry. - self._target_files.extend(absolute_list_of_targets) roleinfo = tuf.roledb.get_roleinfo(self._rolename) roleinfo['paths'].extend(relative_list_of_targets) - tuf.roledb.update_roleinfo(self._rolename, roleinfo) + tuf.roledb.update_roleinfo(self.rolename, roleinfo) @@ -1498,11 +1500,10 @@ def delegate(self, rolename, public_keys, list_of_targets, for key in public_keys: new_targets_object.add_key(key) - #self._delegations = self.__setattr__(rolename, new_targets_object) - - - + + + def revoke(self, rolename): """ @@ -1545,6 +1546,17 @@ def revoke(self, rolename): self.__delattr__(rolename) + @property + def delegations(self): + """ + """ + + roleinfo = tuf.roledb.get_roleinfo(self.rolename) + delegations = roleinfo['delegations'] + + return delegations + + def _prompt(message, result_type=str): """ @@ -1827,7 +1839,8 @@ def load_repository(repository_directory): roleinfo = tuf.roledb.get_roleinfo('root') roleinfo['signatures'] = [] for signature in signable['signatures']: - roleinfo['signatures'].append(signature) + if signature not in roleinfo['signatures']: + roleinfo['signatures'].append(signature) if os.path.exists(root_filename+'.gz'): roleinfo['compressions'].append('gz') @@ -2488,7 +2501,17 @@ def generate_release_metadata(metadata_directory, version, expiration_date): filedict = {} filedict[ROOT_FILENAME] = get_metadata_file_info(root_filename) filedict[TARGETS_FILENAME] = get_metadata_file_info(targets_filename) - + + # Add compressed versions of the 'targets.txt' and 'root.txt' metadata. + for extension in SUPPORTED_COMPRESSION_EXTENSIONS: + compressed_root_filename = root_filename+extension + compressed_targets_filename = targets_filename+extension + if os.path.exists(compressed_root_filename): + filedict[ROOT_FILENAME+extension] = \ + get_metadata_file_info(compressed_root_filename) + if os.path.exists(compressed_targets_filename): + filedict[TARGETS_FILENAME+extension] = \ + get_metadata_file_info(compressed_targets_filename) # Walk the 'targets/' directory and generate the file info for all # the files listed there. This information is stored in the 'meta' From f1d72f0b82cf69eeef1495c2a6192d7fde3d46ec Mon Sep 17 00:00:00 2001 From: Vladimir Diaz Date: Thu, 14 Nov 2013 20:43:34 -0500 Subject: [PATCH 95/95] Update README.md 9-10: Remove extra whitespace. 58: Add missing period. Update a few comments. Add examples for nested delegations, restricted paths, metadata compression, and selection of target paths. --- tuf/README.md | 56 +++++++++++++++++++++++++++++++++++++-------------- 1 file changed, 41 insertions(+), 15 deletions(-) diff --git a/tuf/README.md b/tuf/README.md index 3faa24a7..a1102983 100644 --- a/tuf/README.md +++ b/tuf/README.md @@ -7,7 +7,6 @@ ```python from tuf.libtuf import * - # Generate and write the first of two root keys for the repository. # The following function creates an RSA key pair, where the private key is saved to # “path/to/root_key” and the public key to “path/to/root_key.pub”. @@ -56,9 +55,14 @@ repository = create_new_repository("path/to/repository/") # The Repository instance, ‘repository’, initially contains top-level Metadata objects. # Add one of the public keys, created in the previous section, to the root role. Metadata is -# considered valid if it is signed by the public key’s corresponding private key +# considered valid if it is signed by the public key’s corresponding private key. repository.root.add_key(public_root_key) +# Role keys (i.e., keyid) may be queried. Other attributes include: signing_keys, version, +# signatures, expiration, threshold, and compressions. +repository.root.keys +[u'b23514431a53676595922e955c2d547293da4a7917e3ca243a175e72bbf718df'] + # Add a second public key to the root role. Although previously generated and saved to a file, # the second public key must be imported before it can added to a role. public_root_key2 = import_rsa_publickey_from_file("path/to/root_key2.pub") @@ -130,6 +134,10 @@ repository.timestamp.load_signing_key(private_timestamp_key) # as follows: root(1 year), targets(3 months), release(1 week), timestamp(1 day). repository.timestamp.expiration = "2014-10-28 12:08:00" +# Metadata files may also be compressed. Only "gz" is currently supported. +repository.targets.compressions = ["gz"] +repository.release.compressions = ["gz"] + # Write all metadata to “path/to/repository/metadata/” # The common case is to crawl the filesystem for all roles in # “path/to/repository/metadata/targets/”. @@ -140,10 +148,12 @@ repository.write() #### Add Target Files ```Bash -# Create and save target files to the repository. +# Create and save target files to the targets directory of the repository. $ cd path/to/repository/targets/ $ echo 'file1' > file1.txt $ echo 'file2' > file2.txt +$ echo 'file3' > file3.txt +$ mkdir django; echo 'file4' > django/file4.txt ``` ```python @@ -155,14 +165,14 @@ repository = load_repository("path/to/repository/") # This must be relative to an existing directory in the repository, otherwise throw an # error. list_of_targets = repository.get_filepaths_in_directory("path/to/repository/targets/", - recursive_walk=True, followlinks=True) + recursive_walk=False, followlinks=True) # Add the list of target paths to the metadata of the Targets role. Any target file paths # that may already exist are NOT replaced. add_targets() does not create or move target files. repository.targets.add_targets(list_of_targets) # Individual target files may also be added. -repository.targets.add_target("path/to/repository/targets/file1.txt") +repository.targets.add_target("path/to/repository/targets/file3.txt") # The private key of the updated targets metadata must be loaded before it can be signed and # written (Note the load_repository() call above). @@ -201,7 +211,7 @@ repository.write() # Remove a target file listed in the “targets” metadata. The target file is not actually deleted # from the file system. -repository.targets.remove_target("path/to/repository/targets/file1.txt") +repository.targets.remove_target("path/to/repository/targets/file3.txt") # repository.write() creates any new metadata files, updates those that have changed, and any that # need updating to make a new “release” (new release.txt and timestamp.txt). @@ -219,7 +229,7 @@ public_unclaimed_key = import_rsa_publickey_from_file("path/to/unclaimed_key.pub # Make a delegation from “targets” to “targets/unclaimed”, for all targets in “list_of_targets”. # The delegated role’s full name is not required. # delegated(rolename, list_of_public_keys, list_of_file_paths, threshold, restricted_paths) -repository.targets.delegate("unclaimed", [public_unclaimed_key], list_of_targets) +repository.targets.delegate("unclaimed", [public_unclaimed_key], []) # Load the private key of “targets/unclaimed” so that signatures are added and valid metadata # is created. @@ -228,11 +238,20 @@ Enter a password for the RSA key: Confirm: repository.targets.unclaimed.load_signing_key(private_unclaimed_key) -# Update attributes of the unclaimed role and add a target file. -repository.targets.unclaimed.expiration = "2014-10-28 12:08:00" -repository.targets.unclaimed.add_target("path/to/repository/targets/file1.txt") +# Update an attribute of the unclaimed role and add a target file. +repository.targets.unclaimed.version = 2 -# Write the metadata of “targets/unclaimed”, targets, release, and timestamp. +# Delegations may also be nested. Create the delegated role "targets/unclaimed/django", +# where it initially contains zero targets and future targets are restricted to a +# particular directory. +repository.targets.unclaimed.delegate("django", [public_unclaimed_key], [], + restricted_paths=["path/to/repository/targets/django/"]) +repository.targets.unclaimed.django.load_signing_key(private_unclaimed_key) +repository.targets.unclaimed.django.add_target("path/to/repository/targets/django/file4.txt") +repository.targets.unclaimed.django.compressions = ["gz"] + +# Write the metadata of "targets/unclaimed", targets/unclaimed/django", targets, release, +# and timestamp. repository.write() ``` @@ -240,9 +259,11 @@ repository.write() ```python # Continuing from the previous section . . . -# Revoke “targets/unclaimed” and write the metadata of all remaining roles. -repository.targets.revoke("unclaimed") +# Create a delegated role that will be revoked in the next step. +repository.targets.unclaimed.delegate("flask", [public_unclaimed_key], []) +# Revoke “targets/unclaimed/flask” and write the metadata of all remaining roles. +repository.targets.unclaimed.revoke("flask") repository.write() ``` @@ -276,11 +297,16 @@ $ cd “path/to/repository/”; python -m SimpleHTTPServer 8001 $ cd "path/to/client/" $ ls metadata/ + $ python basic_client.py --repo http://localhost:8001 -$ ls . targets/ + +$ ls . targets/ targets/django/ .: metadata targets tuf.log targets/: -file1.txt file2.txt +django file1.txt file2.txt + +targets/django/: +file4.txt ```