From a7f28b9af46632c072c991fc1c8b0dd5a31de0e3 Mon Sep 17 00:00:00 2001 From: Vladimir Diaz Date: Tue, 22 Apr 2014 15:03:42 -0400 Subject: [PATCH 01/32] [WIP] Python 2+3 support. Add six, convert PY <=2.5 exception handling, dictionary iteration, libraries, 1/2 the tests. --- dev-requirements.txt | 1 + tests/simple_server.py | 18 +- tests/slow_retrieval_server.py | 10 +- tests/test_arbitrary_package_attack.py | 10 +- tests/test_download.py | 7 +- tests/test_endless_data_attack.py | 10 +- tests/test_extraneous_dependencies_attack.py | 9 +- tests/test_formats.py | 12 +- tests/test_replay_attack.py | 14 +- tests/test_repository_tool.py | 3 + tuf/__init__.py | 4 +- tuf/_vendor/six.py | 646 +++++++++++++++++++ tuf/client/basic_client.py | 7 +- tuf/client/updater.py | 50 +- tuf/download.py | 2 +- tuf/ed25519_keys.py | 2 +- tuf/formats.py | 26 +- tuf/keydb.py | 7 +- tuf/mirrors.py | 6 +- tuf/pycrypto_keys.py | 12 +- tuf/repository_tool.py | 65 +- tuf/roledb.py | 7 +- tuf/schema.py | 32 +- tuf/util.py | 11 +- 24 files changed, 813 insertions(+), 158 deletions(-) create mode 100644 tuf/_vendor/six.py diff --git a/dev-requirements.txt b/dev-requirements.txt index 841f6216..2f1d0d60 100644 --- a/dev-requirements.txt +++ b/dev-requirements.txt @@ -17,3 +17,4 @@ # http://nvie.com/posts/pin-your-packages/ pycrypto==2.6.1 pynacl==0.2.3 +tox diff --git a/tests/simple_server.py b/tests/simple_server.py index 217bdeed..2a8c5429 100755 --- a/tests/simple_server.py +++ b/tests/simple_server.py @@ -3,10 +3,10 @@ simple_server.py - Konstantin Andrianov + Konstantin Andrianov. - February 15, 2012 + February 15, 2012. See LICENSE for licensing information. @@ -15,16 +15,15 @@ This is a basic server that was designed to be used in conjunction with test_download.py to test download.py module. - + SimpleHTTPServer: http://docs.python.org/library/simplehttpserver.html#module-SimpleHTTPServer - """ import sys import random -import SimpleHTTPServer -import SocketServer + +import tuf._vendor.six as six PORT = 0 @@ -36,13 +35,14 @@ def _port_gen(): PORT = int(sys.argv[1]) if PORT < 30000 or PORT > 45000: raise ValueError + except ValueError: PORT = _port_gen() + else: PORT = _port_gen() -Handler = SimpleHTTPServer.SimpleHTTPRequestHandler -httpd = SocketServer.TCPServer(("", PORT), Handler) +Handler = six.moves.SimpleHTTPServer.SimpleHTTPRequestHandler +httpd = six.moves.socketserver.TCPServer(('', PORT), Handler) -#print "PORT: ", PORT httpd.serve_forever() diff --git a/tests/slow_retrieval_server.py b/tests/slow_retrieval_server.py index 6e5cc3fc..079f4061 100755 --- a/tests/slow_retrieval_server.py +++ b/tests/slow_retrieval_server.py @@ -22,21 +22,21 @@ import sys import time import random -from BaseHTTPServer import BaseHTTPRequestHandler, HTTPServer +import tuf._vendor.six as six # Modify the HTTPServer class to pass the 'test_mode' argument to # do_GET() function. -class HTTPServer_Test(HTTPServer): +class HTTPServer_Test(six.moves.BaseHTTPServer.HTTPServer): def __init__(self, server_address, Handler, test_mode): - HTTPServer.__init__(self, server_address, Handler) + six.moves.BaseHTTPServer.HTTPServer.__init__(self, server_address, Handler) self.test_mode = test_mode # HTTP request handler. -class Handler(BaseHTTPRequestHandler): +class Handler(six.moves.BaseHTTPServer.BaseHTTPRequestHandler): # Overwrite do_GET. def do_GET(self): @@ -73,7 +73,7 @@ def do_GET(self): return - except IOError, e: + except IOError as e: self.send_error(404, 'File Not Found!') diff --git a/tests/test_arbitrary_package_attack.py b/tests/test_arbitrary_package_attack.py index f533ef7c..9601ba85 100755 --- a/tests/test_arbitrary_package_attack.py +++ b/tests/test_arbitrary_package_attack.py @@ -33,7 +33,6 @@ from __future__ import division import os -import urllib import tempfile import random import time @@ -49,6 +48,7 @@ import tuf.log import tuf.client.updater as updater import tuf.unittest_toolbox as unittest_toolbox +import tuf._vendor.six as six logger = logging.getLogger('tuf.test_arbitrary_package_attack') @@ -173,7 +173,7 @@ def test_without_tuf(self): url_prefix = self.repository_mirrors['mirror1']['url_prefix'] url_file = os.path.join(url_prefix, 'targets', 'file1.txt') - urllib.urlretrieve(url_file, client_target_path) + six.moves.urllib.request.urlretrieve(url_file, client_target_path) self.assertTrue(os.path.exists(client_target_path)) length, hashes = tuf.util.get_file_details(client_target_path) @@ -186,7 +186,7 @@ def test_without_tuf(self): length, hashes = tuf.util.get_file_details(target_path) malicious_fileinfo = tuf.formats.make_fileinfo(length, hashes) - urllib.urlretrieve(url_file, client_target_path) + six.moves.urllib.request.urlretrieve(url_file, client_target_path) length, hashes = tuf.util.get_file_details(client_target_path) download_fileinfo = tuf.formats.make_fileinfo(length, hashes) @@ -217,7 +217,7 @@ def test_with_tuf(self): try: self.repository_updater.download_target(file1_fileinfo, destination) - except tuf.NoWorkingMirrorError, exception: + except tuf.NoWorkingMirrorError as exception: url_prefix = self.repository_mirrors['mirror1']['url_prefix'] url_file = os.path.join(url_prefix, 'targets', 'file1.txt') @@ -269,7 +269,7 @@ def test_with_tuf_and_metadata_tampering(self): destination = os.path.join(self.client_directory) self.repository_updater.download_target(file1_fileinfo, destination) - except tuf.NoWorkingMirrorError, exception: + except tuf.NoWorkingMirrorError as exception: url_prefix = self.repository_mirrors['mirror1']['url_prefix'] url_file = os.path.join(url_prefix, 'targets', 'file1.txt') diff --git a/tests/test_download.py b/tests/test_download.py index bdef71f2..cab55d2c 100755 --- a/tests/test_download.py +++ b/tests/test_download.py @@ -29,14 +29,13 @@ import subprocess import time import unittest -import urllib2 - import tuf import tuf.conf as conf import tuf.download as download import tuf.log import tuf.unittest_toolbox as unittest_toolbox +import tuf._vendor.six as six logger = logging.getLogger('tuf.test_download') @@ -162,12 +161,12 @@ def test_download_url_to_tempfileobj_and_urls(self): download_file, self.random_string(), self.target_data_length) - self.assertRaises(urllib2.HTTPError, + self.assertRaises(six.moves.urllib.error.HTTPError, download_file, 'http://localhost:'+str(self.PORT)+'/'+self.random_string(), self.target_data_length) - self.assertRaises(urllib2.URLError, + self.assertRaises(six.moves.urllib.error.URLError, download_file, 'http://localhost:'+str(self.PORT+1)+'/'+self.random_string(), self.target_data_length) diff --git a/tests/test_endless_data_attack.py b/tests/test_endless_data_attack.py index 6da1e496..83fbfadf 100755 --- a/tests/test_endless_data_attack.py +++ b/tests/test_endless_data_attack.py @@ -36,7 +36,6 @@ from __future__ import division import os -import urllib import tempfile import random import time @@ -52,6 +51,7 @@ import tuf.log import tuf.client.updater as updater import tuf.unittest_toolbox as unittest_toolbox +import tuf._vendor.six as six logger = logging.getLogger('tuf.test_endless_data_attack') @@ -178,7 +178,7 @@ def test_without_tuf(self): url_prefix = self.repository_mirrors['mirror1']['url_prefix'] url_file = os.path.join(url_prefix, 'targets', 'file1.txt') - urllib.urlretrieve(url_file, client_target_path) + six.moves.urllib.request.urlretrieve(url_file, client_target_path) self.assertTrue(os.path.exists(client_target_path)) length, hashes = tuf.util.get_file_details(client_target_path) @@ -196,7 +196,7 @@ def test_without_tuf(self): # Is the modified file actually larger? self.assertTrue(large_length > length) - urllib.urlretrieve(url_file, client_target_path) + six.moves.urllib.request.urlretrieve(url_file, client_target_path) length, hashes = tuf.util.get_file_details(client_target_path) download_fileinfo = tuf.formats.make_fileinfo(length, hashes) @@ -268,8 +268,8 @@ def test_with_tuf(self): try: self.repository_updater.refresh() - except tuf.NoWorkingMirrorError, exception: - for mirror_url, mirror_error in exception.mirror_errors.iteritems(): + except tuf.NoWorkingMirrorError as exception: + for mirror_url, mirror_error in six.iteritems(exception.mirror_errors): self.assertTrue(isinstance(mirror_error, tuf.InvalidMetadataJSONError)) else: diff --git a/tests/test_extraneous_dependencies_attack.py b/tests/test_extraneous_dependencies_attack.py index bd0be5b9..877ef815 100755 --- a/tests/test_extraneous_dependencies_attack.py +++ b/tests/test_extraneous_dependencies_attack.py @@ -8,7 +8,7 @@ Zane Fisher. - August 19, 2013 + August 19, 2013. April 6, 2014. Refactored to use the 'unittest' module (test conditions in code, rather @@ -38,7 +38,6 @@ from __future__ import division import os -import urllib import tempfile import random import time @@ -53,7 +52,7 @@ import tuf.log import tuf.client.updater as updater import tuf.unittest_toolbox as unittest_toolbox - +import tuf._vendor.six as six logger = logging.getLogger('tuf.test_extraneous_dependencies_attack') @@ -208,8 +207,8 @@ def test_with_tuf(self): # Verify that the specific 'tuf.BadHashError' exception is raised by each # mirror. - except tuf.NoWorkingMirrorError, exception: - for mirror_url, mirror_error in exception.mirror_errors.iteritems(): + except tuf.NoWorkingMirrorError as exception: + for mirror_url, mirror_error in six.iteritems(exception.mirror_errors): url_prefix = self.repository_mirrors['mirror1']['url_prefix'] url_file = os.path.join(url_prefix, 'metadata', 'targets', 'role1.json') diff --git a/tests/test_formats.py b/tests/test_formats.py index 10133b08..5a3572cd 100755 --- a/tests/test_formats.py +++ b/tests/test_formats.py @@ -23,7 +23,7 @@ import tuf import tuf.formats import tuf.schema - +import tuf._vendor.six as six class TestFormats(unittest.TestCase): @@ -252,13 +252,13 @@ def test_schemas(self): # Iterate 'valid_schemas', ensuring each 'valid_schema' correctly matches # its respective 'schema_type'. - for schema_name, (schema_type, valid_schema) in valid_schemas.items(): + for schema_name, (schema_type, valid_schema) in six.iteritems(valid_schemas): self.assertEqual(True, schema_type.matches(valid_schema)) # Test conditions for invalid schemas. # Set the 'valid_schema' of 'valid_schemas' to an invalid # value and test that it does not match 'schema_type'. - for schema_name, (schema_type, valid_schema) in valid_schemas.items(): + for schema_name, (schema_type, valid_schema) in six.iteritems(valid_schemas): invalid_schema = 0xBAD if isinstance(schema_type, tuf.schema.Integer): invalid_schema = 'BAD' @@ -455,7 +455,7 @@ def test_unix_timestamp_to_datetime(self): # Test conditions for valid arguments. UNIX_TIMESTAMP_SCHEMA = tuf.formats.UNIX_TIMESTAMP_SCHEMA self.assertTrue(datetime.datetime, tuf.formats.unix_timestamp_to_datetime(499137720)) - datetime_object = datetime.datetime(1985, 10, 26, 01, 22) + datetime_object = datetime.datetime(1985, 10, 26, 1, 22) self.assertEqual(datetime_object, tuf.formats.unix_timestamp_to_datetime(499137720)) # Test conditions for invalid arguments. @@ -482,7 +482,7 @@ def test_format_base64(self): # Test conditions for valid arguments. data = 'updateframework' self.assertEqual('dXBkYXRlZnJhbWV3b3Jr', tuf.formats.format_base64(data)) - self.assertTrue(isinstance(tuf.formats.format_base64(data), basestring)) + self.assertTrue(isinstance(tuf.formats.format_base64(data), six.string_types)) # Test conditions for invalid arguments. self.assertRaises(tuf.FormatError, tuf.formats.format_base64, 123) @@ -494,7 +494,7 @@ def test_parse_base64(self): # Test conditions for valid arguments. base64 = 'dXBkYXRlZnJhbWV3b3Jr' self.assertEqual('updateframework', tuf.formats.parse_base64(base64)) - self.assertTrue(isinstance(tuf.formats.parse_base64(base64), basestring)) + self.assertTrue(isinstance(tuf.formats.parse_base64(base64), six.string_types)) # Test conditions for invalid arguments. self.assertRaises(tuf.FormatError, tuf.formats.format_base64, 123) diff --git a/tests/test_replay_attack.py b/tests/test_replay_attack.py index ddb34d28..0621e760 100755 --- a/tests/test_replay_attack.py +++ b/tests/test_replay_attack.py @@ -35,7 +35,6 @@ from __future__ import division import os -import urllib import tempfile import random import time @@ -52,6 +51,7 @@ import tuf.client.updater as updater import tuf.repository_tool as repo_tool import tuf.unittest_toolbox as unittest_toolbox +import tuf._vendor.six as six # The repository tool is imported and logs console messages by default. Disable # console log messages generated by this unit test. @@ -207,7 +207,7 @@ def test_without_tuf(self): # Set an arbitrary expiration so that the repository tool generates a new # version. - repository.timestamp.expiration = datetime.datetime(2030, 01, 01, 12, 12) + repository.timestamp.expiration = datetime.datetime(2030, 1, 1, 12, 12) repository.write() # Move the staged metadata to the "live" metadata. @@ -225,7 +225,7 @@ def test_without_tuf(self): client_timestamp_path = os.path.join(self.client_directory, 'metadata', 'current', 'timestamp.json') - urllib.urlretrieve(url_file, client_timestamp_path) + six.moves.urllib.request.urlretrieve(url_file, client_timestamp_path) length, hashes = tuf.util.get_file_details(client_timestamp_path) download_fileinfo = tuf.formats.make_fileinfo(length, hashes) @@ -237,7 +237,7 @@ def test_without_tuf(self): # and verify that the non-TUF client downloads it (expected, but not ideal). shutil.move(backup_timestamp, timestamp_path) - urllib.urlretrieve(url_file, client_timestamp_path) + six.moves.urllib.request.urlretrieve(url_file, client_timestamp_path) length, hashes = tuf.util.get_file_details(client_timestamp_path) download_fileinfo = tuf.formats.make_fileinfo(length, hashes) @@ -278,7 +278,7 @@ def test_with_tuf(self): # Set an arbitrary expiration so that the repository tool generates a new # version. - repository.timestamp.expiration = datetime.datetime(2030, 01, 01, 12, 12) + repository.timestamp.expiration = datetime.datetime(2030, 1, 1, 12, 12) repository.write() # Move the staged metadata to the "live" metadata. @@ -314,8 +314,8 @@ def test_with_tuf(self): # Verify that the specific 'tuf.ReplayedMetadataError' is raised by each # mirror. - except tuf.NoWorkingMirrorError, exception: - for mirror_url, mirror_error in exception.mirror_errors.iteritems(): + except tuf.NoWorkingMirrorError as exception: + for mirror_url, mirror_error in six.iteritems(exception.mirror_errors): url_prefix = self.repository_mirrors['mirror1']['url_prefix'] url_file = os.path.join(url_prefix, 'metadata', 'timestamp.json') diff --git a/tests/test_repository_tool.py b/tests/test_repository_tool.py index 67fc3f23..b03e05bd 100755 --- a/tests/test_repository_tool.py +++ b/tests/test_repository_tool.py @@ -222,6 +222,9 @@ def test_write_and_write_partial(self): # Verify that an exception is *not* raised for multiple repository.write(). repository.write() + # Verify the status() does not raise an exception. + repository.status() + # Verify that a write() fails if a repository is loaded and a change # is made to a role. repo_tool.load_repository(repository_directory) diff --git a/tuf/__init__.py b/tuf/__init__.py index 61a83dc4..158cf7be 100755 --- a/tuf/__init__.py +++ b/tuf/__init__.py @@ -20,7 +20,7 @@ provide that reason in those cases. """ -import urlparse +import tuf._vendor.six as six # Import 'tuf.formats' if a module tries to import the @@ -312,7 +312,7 @@ def __str__(self): 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) + mirror_url_tokens = six.moves.urlparse.urlparse(mirror_url) except: logging.exception('Failed to parse mirror URL: '+str(mirror_url)) diff --git a/tuf/_vendor/six.py b/tuf/_vendor/six.py new file mode 100644 index 00000000..019130f7 --- /dev/null +++ b/tuf/_vendor/six.py @@ -0,0 +1,646 @@ +"""Utilities for writing code that runs on Python 2 and 3""" + +# Copyright (c) 2010-2014 Benjamin Peterson +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in all +# copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +# SOFTWARE. + +import operator +import sys +import types + +__author__ = "Benjamin Peterson " +__version__ = "1.6.1" + + +# Useful for very coarse version differentiation. +PY2 = sys.version_info[0] == 2 +PY3 = sys.version_info[0] == 3 + +if PY3: + string_types = str, + integer_types = int, + class_types = type, + text_type = str + binary_type = bytes + + MAXSIZE = sys.maxsize +else: + string_types = basestring, + integer_types = (int, long) + class_types = (type, types.ClassType) + text_type = unicode + binary_type = str + + if sys.platform.startswith("java"): + # Jython always uses 32 bits. + MAXSIZE = int((1 << 31) - 1) + else: + # It's possible to have sizeof(long) != sizeof(Py_ssize_t). + class X(object): + def __len__(self): + return 1 << 31 + try: + len(X()) + except OverflowError: + # 32-bit + MAXSIZE = int((1 << 31) - 1) + else: + # 64-bit + MAXSIZE = int((1 << 63) - 1) + del X + + +def _add_doc(func, doc): + """Add documentation to a function.""" + func.__doc__ = doc + + +def _import_module(name): + """Import module, returning the module after the last dot.""" + __import__(name) + return sys.modules[name] + + +class _LazyDescr(object): + + def __init__(self, name): + self.name = name + + def __get__(self, obj, tp): + try: + result = self._resolve() + except ImportError: + # See the nice big comment in MovedModule.__getattr__. + raise AttributeError("%s could not be imported " % self.name) + setattr(obj, self.name, result) # Invokes __set__. + # This is a bit ugly, but it avoids running this again. + delattr(obj.__class__, self.name) + return result + + +class MovedModule(_LazyDescr): + + def __init__(self, name, old, new=None): + super(MovedModule, self).__init__(name) + if PY3: + if new is None: + new = name + self.mod = new + else: + self.mod = old + + def _resolve(self): + return _import_module(self.mod) + + def __getattr__(self, attr): + # It turns out many Python frameworks like to traverse sys.modules and + # try to load various attributes. This causes problems if this is a + # platform-specific module on the wrong platform, like _winreg on + # Unixes. Therefore, we silently pretend unimportable modules do not + # have any attributes. See issues #51, #53, #56, and #63 for the full + # tales of woe. + # + # First, if possible, avoid loading the module just to look at __file__, + # __name__, or __path__. + if (attr in ("__file__", "__name__", "__path__") and + self.mod not in sys.modules): + raise AttributeError(attr) + try: + _module = self._resolve() + except ImportError: + raise AttributeError(attr) + value = getattr(_module, attr) + setattr(self, attr, value) + return value + + +class _LazyModule(types.ModuleType): + + def __init__(self, name): + super(_LazyModule, self).__init__(name) + self.__doc__ = self.__class__.__doc__ + + def __dir__(self): + attrs = ["__doc__", "__name__"] + attrs += [attr.name for attr in self._moved_attributes] + return attrs + + # Subclasses should override this + _moved_attributes = [] + + +class MovedAttribute(_LazyDescr): + + def __init__(self, name, old_mod, new_mod, old_attr=None, new_attr=None): + super(MovedAttribute, self).__init__(name) + if PY3: + if new_mod is None: + new_mod = name + self.mod = new_mod + if new_attr is None: + if old_attr is None: + new_attr = name + else: + new_attr = old_attr + self.attr = new_attr + else: + self.mod = old_mod + if old_attr is None: + old_attr = name + self.attr = old_attr + + def _resolve(self): + module = _import_module(self.mod) + return getattr(module, self.attr) + + + +class _MovedItems(_LazyModule): + """Lazy loading of moved objects""" + + +_moved_attributes = [ + MovedAttribute("cStringIO", "cStringIO", "io", "StringIO"), + MovedAttribute("filter", "itertools", "builtins", "ifilter", "filter"), + MovedAttribute("filterfalse", "itertools", "itertools", "ifilterfalse", "filterfalse"), + MovedAttribute("input", "__builtin__", "builtins", "raw_input", "input"), + MovedAttribute("map", "itertools", "builtins", "imap", "map"), + MovedAttribute("range", "__builtin__", "builtins", "xrange", "range"), + MovedAttribute("reload_module", "__builtin__", "imp", "reload"), + MovedAttribute("reduce", "__builtin__", "functools"), + MovedAttribute("StringIO", "StringIO", "io"), + MovedAttribute("UserString", "UserString", "collections"), + MovedAttribute("xrange", "__builtin__", "builtins", "xrange", "range"), + MovedAttribute("zip", "itertools", "builtins", "izip", "zip"), + MovedAttribute("zip_longest", "itertools", "itertools", "izip_longest", "zip_longest"), + + MovedModule("builtins", "__builtin__"), + MovedModule("configparser", "ConfigParser"), + MovedModule("copyreg", "copy_reg"), + MovedModule("dbm_gnu", "gdbm", "dbm.gnu"), + MovedModule("http_cookiejar", "cookielib", "http.cookiejar"), + MovedModule("http_cookies", "Cookie", "http.cookies"), + MovedModule("html_entities", "htmlentitydefs", "html.entities"), + MovedModule("html_parser", "HTMLParser", "html.parser"), + MovedModule("http_client", "httplib", "http.client"), + MovedModule("email_mime_multipart", "email.MIMEMultipart", "email.mime.multipart"), + MovedModule("email_mime_text", "email.MIMEText", "email.mime.text"), + MovedModule("email_mime_base", "email.MIMEBase", "email.mime.base"), + MovedModule("BaseHTTPServer", "BaseHTTPServer", "http.server"), + MovedModule("CGIHTTPServer", "CGIHTTPServer", "http.server"), + MovedModule("SimpleHTTPServer", "SimpleHTTPServer", "http.server"), + MovedModule("cPickle", "cPickle", "pickle"), + MovedModule("queue", "Queue"), + MovedModule("reprlib", "repr"), + MovedModule("socketserver", "SocketServer"), + MovedModule("_thread", "thread", "_thread"), + MovedModule("tkinter", "Tkinter"), + MovedModule("tkinter_dialog", "Dialog", "tkinter.dialog"), + MovedModule("tkinter_filedialog", "FileDialog", "tkinter.filedialog"), + MovedModule("tkinter_scrolledtext", "ScrolledText", "tkinter.scrolledtext"), + MovedModule("tkinter_simpledialog", "SimpleDialog", "tkinter.simpledialog"), + MovedModule("tkinter_tix", "Tix", "tkinter.tix"), + MovedModule("tkinter_ttk", "ttk", "tkinter.ttk"), + MovedModule("tkinter_constants", "Tkconstants", "tkinter.constants"), + MovedModule("tkinter_dnd", "Tkdnd", "tkinter.dnd"), + MovedModule("tkinter_colorchooser", "tkColorChooser", + "tkinter.colorchooser"), + MovedModule("tkinter_commondialog", "tkCommonDialog", + "tkinter.commondialog"), + MovedModule("tkinter_tkfiledialog", "tkFileDialog", "tkinter.filedialog"), + MovedModule("tkinter_font", "tkFont", "tkinter.font"), + MovedModule("tkinter_messagebox", "tkMessageBox", "tkinter.messagebox"), + MovedModule("tkinter_tksimpledialog", "tkSimpleDialog", + "tkinter.simpledialog"), + MovedModule("urllib_parse", __name__ + ".moves.urllib_parse", "urllib.parse"), + MovedModule("urllib_error", __name__ + ".moves.urllib_error", "urllib.error"), + MovedModule("urllib", __name__ + ".moves.urllib", __name__ + ".moves.urllib"), + MovedModule("urllib_robotparser", "robotparser", "urllib.robotparser"), + MovedModule("xmlrpc_client", "xmlrpclib", "xmlrpc.client"), + MovedModule("xmlrpc_server", "xmlrpclib", "xmlrpc.server"), + MovedModule("winreg", "_winreg"), +] +for attr in _moved_attributes: + setattr(_MovedItems, attr.name, attr) + if isinstance(attr, MovedModule): + sys.modules[__name__ + ".moves." + attr.name] = attr +del attr + +_MovedItems._moved_attributes = _moved_attributes + +moves = sys.modules[__name__ + ".moves"] = _MovedItems(__name__ + ".moves") + + +class Module_six_moves_urllib_parse(_LazyModule): + """Lazy loading of moved objects in six.moves.urllib_parse""" + + +_urllib_parse_moved_attributes = [ + MovedAttribute("ParseResult", "urlparse", "urllib.parse"), + MovedAttribute("SplitResult", "urlparse", "urllib.parse"), + MovedAttribute("parse_qs", "urlparse", "urllib.parse"), + MovedAttribute("parse_qsl", "urlparse", "urllib.parse"), + MovedAttribute("urldefrag", "urlparse", "urllib.parse"), + MovedAttribute("urljoin", "urlparse", "urllib.parse"), + MovedAttribute("urlparse", "urlparse", "urllib.parse"), + MovedAttribute("urlsplit", "urlparse", "urllib.parse"), + MovedAttribute("urlunparse", "urlparse", "urllib.parse"), + MovedAttribute("urlunsplit", "urlparse", "urllib.parse"), + MovedAttribute("quote", "urllib", "urllib.parse"), + MovedAttribute("quote_plus", "urllib", "urllib.parse"), + MovedAttribute("unquote", "urllib", "urllib.parse"), + MovedAttribute("unquote_plus", "urllib", "urllib.parse"), + MovedAttribute("urlencode", "urllib", "urllib.parse"), + MovedAttribute("splitquery", "urllib", "urllib.parse"), +] +for attr in _urllib_parse_moved_attributes: + setattr(Module_six_moves_urllib_parse, attr.name, attr) +del attr + +Module_six_moves_urllib_parse._moved_attributes = _urllib_parse_moved_attributes + +sys.modules[__name__ + ".moves.urllib_parse"] = sys.modules[__name__ + ".moves.urllib.parse"] = Module_six_moves_urllib_parse(__name__ + ".moves.urllib_parse") + + +class Module_six_moves_urllib_error(_LazyModule): + """Lazy loading of moved objects in six.moves.urllib_error""" + + +_urllib_error_moved_attributes = [ + MovedAttribute("URLError", "urllib2", "urllib.error"), + MovedAttribute("HTTPError", "urllib2", "urllib.error"), + MovedAttribute("ContentTooShortError", "urllib", "urllib.error"), +] +for attr in _urllib_error_moved_attributes: + setattr(Module_six_moves_urllib_error, attr.name, attr) +del attr + +Module_six_moves_urllib_error._moved_attributes = _urllib_error_moved_attributes + +sys.modules[__name__ + ".moves.urllib_error"] = sys.modules[__name__ + ".moves.urllib.error"] = Module_six_moves_urllib_error(__name__ + ".moves.urllib.error") + + +class Module_six_moves_urllib_request(_LazyModule): + """Lazy loading of moved objects in six.moves.urllib_request""" + + +_urllib_request_moved_attributes = [ + MovedAttribute("urlopen", "urllib2", "urllib.request"), + MovedAttribute("install_opener", "urllib2", "urllib.request"), + MovedAttribute("build_opener", "urllib2", "urllib.request"), + MovedAttribute("pathname2url", "urllib", "urllib.request"), + MovedAttribute("url2pathname", "urllib", "urllib.request"), + MovedAttribute("getproxies", "urllib", "urllib.request"), + MovedAttribute("Request", "urllib2", "urllib.request"), + MovedAttribute("OpenerDirector", "urllib2", "urllib.request"), + MovedAttribute("HTTPDefaultErrorHandler", "urllib2", "urllib.request"), + MovedAttribute("HTTPRedirectHandler", "urllib2", "urllib.request"), + MovedAttribute("HTTPCookieProcessor", "urllib2", "urllib.request"), + MovedAttribute("ProxyHandler", "urllib2", "urllib.request"), + MovedAttribute("BaseHandler", "urllib2", "urllib.request"), + MovedAttribute("HTTPPasswordMgr", "urllib2", "urllib.request"), + MovedAttribute("HTTPPasswordMgrWithDefaultRealm", "urllib2", "urllib.request"), + MovedAttribute("AbstractBasicAuthHandler", "urllib2", "urllib.request"), + MovedAttribute("HTTPBasicAuthHandler", "urllib2", "urllib.request"), + MovedAttribute("ProxyBasicAuthHandler", "urllib2", "urllib.request"), + MovedAttribute("AbstractDigestAuthHandler", "urllib2", "urllib.request"), + MovedAttribute("HTTPDigestAuthHandler", "urllib2", "urllib.request"), + MovedAttribute("ProxyDigestAuthHandler", "urllib2", "urllib.request"), + MovedAttribute("HTTPHandler", "urllib2", "urllib.request"), + MovedAttribute("HTTPSHandler", "urllib2", "urllib.request"), + MovedAttribute("FileHandler", "urllib2", "urllib.request"), + MovedAttribute("FTPHandler", "urllib2", "urllib.request"), + MovedAttribute("CacheFTPHandler", "urllib2", "urllib.request"), + MovedAttribute("UnknownHandler", "urllib2", "urllib.request"), + MovedAttribute("HTTPErrorProcessor", "urllib2", "urllib.request"), + MovedAttribute("urlretrieve", "urllib", "urllib.request"), + MovedAttribute("urlcleanup", "urllib", "urllib.request"), + MovedAttribute("URLopener", "urllib", "urllib.request"), + MovedAttribute("FancyURLopener", "urllib", "urllib.request"), + MovedAttribute("proxy_bypass", "urllib", "urllib.request"), +] +for attr in _urllib_request_moved_attributes: + setattr(Module_six_moves_urllib_request, attr.name, attr) +del attr + +Module_six_moves_urllib_request._moved_attributes = _urllib_request_moved_attributes + +sys.modules[__name__ + ".moves.urllib_request"] = sys.modules[__name__ + ".moves.urllib.request"] = Module_six_moves_urllib_request(__name__ + ".moves.urllib.request") + + +class Module_six_moves_urllib_response(_LazyModule): + """Lazy loading of moved objects in six.moves.urllib_response""" + + +_urllib_response_moved_attributes = [ + MovedAttribute("addbase", "urllib", "urllib.response"), + MovedAttribute("addclosehook", "urllib", "urllib.response"), + MovedAttribute("addinfo", "urllib", "urllib.response"), + MovedAttribute("addinfourl", "urllib", "urllib.response"), +] +for attr in _urllib_response_moved_attributes: + setattr(Module_six_moves_urllib_response, attr.name, attr) +del attr + +Module_six_moves_urllib_response._moved_attributes = _urllib_response_moved_attributes + +sys.modules[__name__ + ".moves.urllib_response"] = sys.modules[__name__ + ".moves.urllib.response"] = Module_six_moves_urllib_response(__name__ + ".moves.urllib.response") + + +class Module_six_moves_urllib_robotparser(_LazyModule): + """Lazy loading of moved objects in six.moves.urllib_robotparser""" + + +_urllib_robotparser_moved_attributes = [ + MovedAttribute("RobotFileParser", "robotparser", "urllib.robotparser"), +] +for attr in _urllib_robotparser_moved_attributes: + setattr(Module_six_moves_urllib_robotparser, attr.name, attr) +del attr + +Module_six_moves_urllib_robotparser._moved_attributes = _urllib_robotparser_moved_attributes + +sys.modules[__name__ + ".moves.urllib_robotparser"] = sys.modules[__name__ + ".moves.urllib.robotparser"] = Module_six_moves_urllib_robotparser(__name__ + ".moves.urllib.robotparser") + + +class Module_six_moves_urllib(types.ModuleType): + """Create a six.moves.urllib namespace that resembles the Python 3 namespace""" + parse = sys.modules[__name__ + ".moves.urllib_parse"] + error = sys.modules[__name__ + ".moves.urllib_error"] + request = sys.modules[__name__ + ".moves.urllib_request"] + response = sys.modules[__name__ + ".moves.urllib_response"] + robotparser = sys.modules[__name__ + ".moves.urllib_robotparser"] + + def __dir__(self): + return ['parse', 'error', 'request', 'response', 'robotparser'] + + +sys.modules[__name__ + ".moves.urllib"] = Module_six_moves_urllib(__name__ + ".moves.urllib") + + +def add_move(move): + """Add an item to six.moves.""" + setattr(_MovedItems, move.name, move) + + +def remove_move(name): + """Remove item from six.moves.""" + try: + delattr(_MovedItems, name) + except AttributeError: + try: + del moves.__dict__[name] + except KeyError: + raise AttributeError("no such move, %r" % (name,)) + + +if PY3: + _meth_func = "__func__" + _meth_self = "__self__" + + _func_closure = "__closure__" + _func_code = "__code__" + _func_defaults = "__defaults__" + _func_globals = "__globals__" + + _iterkeys = "keys" + _itervalues = "values" + _iteritems = "items" + _iterlists = "lists" +else: + _meth_func = "im_func" + _meth_self = "im_self" + + _func_closure = "func_closure" + _func_code = "func_code" + _func_defaults = "func_defaults" + _func_globals = "func_globals" + + _iterkeys = "iterkeys" + _itervalues = "itervalues" + _iteritems = "iteritems" + _iterlists = "iterlists" + + +try: + advance_iterator = next +except NameError: + def advance_iterator(it): + return it.next() +next = advance_iterator + + +try: + callable = callable +except NameError: + def callable(obj): + return any("__call__" in klass.__dict__ for klass in type(obj).__mro__) + + +if PY3: + def get_unbound_function(unbound): + return unbound + + create_bound_method = types.MethodType + + Iterator = object +else: + def get_unbound_function(unbound): + return unbound.im_func + + def create_bound_method(func, obj): + return types.MethodType(func, obj, obj.__class__) + + class Iterator(object): + + def next(self): + return type(self).__next__(self) + + callable = callable +_add_doc(get_unbound_function, + """Get the function out of a possibly unbound function""") + + +get_method_function = operator.attrgetter(_meth_func) +get_method_self = operator.attrgetter(_meth_self) +get_function_closure = operator.attrgetter(_func_closure) +get_function_code = operator.attrgetter(_func_code) +get_function_defaults = operator.attrgetter(_func_defaults) +get_function_globals = operator.attrgetter(_func_globals) + + +def iterkeys(d, **kw): + """Return an iterator over the keys of a dictionary.""" + return iter(getattr(d, _iterkeys)(**kw)) + +def itervalues(d, **kw): + """Return an iterator over the values of a dictionary.""" + return iter(getattr(d, _itervalues)(**kw)) + +def iteritems(d, **kw): + """Return an iterator over the (key, value) pairs of a dictionary.""" + return iter(getattr(d, _iteritems)(**kw)) + +def iterlists(d, **kw): + """Return an iterator over the (key, [values]) pairs of a dictionary.""" + return iter(getattr(d, _iterlists)(**kw)) + + +if PY3: + def b(s): + return s.encode("latin-1") + def u(s): + return s + unichr = chr + if sys.version_info[1] <= 1: + def int2byte(i): + return bytes((i,)) + else: + # This is about 2x faster than the implementation above on 3.2+ + int2byte = operator.methodcaller("to_bytes", 1, "big") + byte2int = operator.itemgetter(0) + indexbytes = operator.getitem + iterbytes = iter + import io + StringIO = io.StringIO + BytesIO = io.BytesIO +else: + def b(s): + return s + # Workaround for standalone backslash + def u(s): + return unicode(s.replace(r'\\', r'\\\\'), "unicode_escape") + unichr = unichr + int2byte = chr + def byte2int(bs): + return ord(bs[0]) + def indexbytes(buf, i): + return ord(buf[i]) + def iterbytes(buf): + return (ord(byte) for byte in buf) + import StringIO + StringIO = BytesIO = StringIO.StringIO +_add_doc(b, """Byte literal""") +_add_doc(u, """Text literal""") + + +if PY3: + exec_ = getattr(moves.builtins, "exec") + + + def reraise(tp, value, tb=None): + if value.__traceback__ is not tb: + raise value.with_traceback(tb) + raise value + +else: + def exec_(_code_, _globs_=None, _locs_=None): + """Execute code in a namespace.""" + if _globs_ is None: + frame = sys._getframe(1) + _globs_ = frame.f_globals + if _locs_ is None: + _locs_ = frame.f_locals + del frame + elif _locs_ is None: + _locs_ = _globs_ + exec("""exec _code_ in _globs_, _locs_""") + + + exec_("""def reraise(tp, value, tb=None): + raise tp, value, tb +""") + + +print_ = getattr(moves.builtins, "print", None) +if print_ is None: + def print_(*args, **kwargs): + """The new-style print function for Python 2.4 and 2.5.""" + fp = kwargs.pop("file", sys.stdout) + if fp is None: + return + def write(data): + if not isinstance(data, basestring): + data = str(data) + # If the file has an encoding, encode unicode with it. + if (isinstance(fp, file) and + isinstance(data, unicode) and + fp.encoding is not None): + errors = getattr(fp, "errors", None) + if errors is None: + errors = "strict" + data = data.encode(fp.encoding, errors) + fp.write(data) + want_unicode = False + sep = kwargs.pop("sep", None) + if sep is not None: + if isinstance(sep, unicode): + want_unicode = True + elif not isinstance(sep, str): + raise TypeError("sep must be None or a string") + end = kwargs.pop("end", None) + if end is not None: + if isinstance(end, unicode): + want_unicode = True + elif not isinstance(end, str): + raise TypeError("end must be None or a string") + if kwargs: + raise TypeError("invalid keyword arguments to print()") + if not want_unicode: + for arg in args: + if isinstance(arg, unicode): + want_unicode = True + break + if want_unicode: + newline = unicode("\n") + space = unicode(" ") + else: + newline = "\n" + space = " " + if sep is None: + sep = space + if end is None: + end = newline + for i, arg in enumerate(args): + if i: + write(sep) + write(arg) + write(end) + +_add_doc(reraise, """Reraise an exception.""") + + +def with_metaclass(meta, *bases): + """Create a base class with a metaclass.""" + return meta("NewBase", bases, {}) + +def add_metaclass(metaclass): + """Class decorator for creating a class with a metaclass.""" + def wrapper(cls): + orig_vars = cls.__dict__.copy() + orig_vars.pop('__dict__', None) + orig_vars.pop('__weakref__', None) + slots = orig_vars.get('__slots__') + if slots is not None: + if isinstance(slots, str): + slots = [slots] + for slots_var in slots: + orig_vars.pop(slots_var) + return metaclass(cls.__name__, cls.__bases__, orig_vars) + return wrapper diff --git a/tuf/client/basic_client.py b/tuf/client/basic_client.py index c57852ca..00ee03a0 100755 --- a/tuf/client/basic_client.py +++ b/tuf/client/basic_client.py @@ -94,7 +94,7 @@ def update_client(repository_mirror): # Does 'repository_mirror' have the correct format? try: tuf.formats.URL_SCHEMA.check_match(repository_mirror) - except tuf.FormatError, e: + except tuf.FormatError as e: message = 'The repository mirror supplied is invalid.' raise tuf.RepositoryError(message) @@ -126,7 +126,7 @@ def update_client(repository_mirror): for target in updated_targets: try: updater.download_target(target, destination_directory) - except tuf.DownloadError, e: + except tuf.DownloadError as e: pass # Remove any files from the destination directory that are no longer being @@ -211,7 +211,8 @@ def parse_options(): # the current directory. try: update_client(repository_mirror) - except (tuf.NoWorkingMirrorError, tuf.RepositoryError), e: + + except (tuf.NoWorkingMirrorError, tuf.RepositoryError) as e: sys.stderr.write('Error: '+str(e)+'\n') sys.exit(1) diff --git a/tuf/client/updater.py b/tuf/client/updater.py index 658dab24..35a089a9 100755 --- a/tuf/client/updater.py +++ b/tuf/client/updater.py @@ -104,7 +104,6 @@ import os import shutil import time -import urllib import random import tuf @@ -120,6 +119,7 @@ import tuf.sig import tuf.util import tuf._vendor.iso8601 as iso8601 +import tuf._vendor.six as six logger = logging.getLogger('tuf.client.updater') @@ -505,7 +505,7 @@ 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(): + for keyid, keyinfo in six.iteritems(keys_info): if keyinfo['keytype'] in ['rsa', 'ed25519']: key = tuf.keys.format_metadata_to_key(keyinfo) @@ -517,7 +517,7 @@ def _import_delegations(self, parent_role): except tuf.KeyAlreadyExistsError: pass - except (tuf.FormatError, tuf.Error), e: + except (tuf.FormatError, tuf.Error) as e: logger.exception('Failed to add keyid: '+repr(keyid)+'.') logger.error('Aborting role delegation for parent role '+parent_role+'.') raise @@ -535,7 +535,7 @@ def _import_delegations(self, parent_role): logger.debug('Adding delegated role: '+str(rolename)+'.') tuf.roledb.add_role(rolename, roleinfo) - except tuf.RoleAlreadyExistsError, e: + except tuf.RoleAlreadyExistsError as e: logger.warn('Role already exists: '+rolename) except: @@ -617,7 +617,7 @@ def refresh(self, unsafely_update_root_if_necessary=True): self._update_metadata_if_changed('root') self._update_metadata_if_changed('targets') - except tuf.NoWorkingMirrorError, e: + except tuf.NoWorkingMirrorError as e: if unsafely_update_root_if_necessary: message = 'Valid top-level metadata cannot be downloaded. Unsafely '+\ 'update the Root metadata.' @@ -671,7 +671,7 @@ def _check_hashes(self, file_object, trusted_hashes): # Verify each trusted hash of 'trusted_hashes'. If all are valid, simply # return. - for algorithm, trusted_hash in trusted_hashes.items(): + for algorithm, trusted_hash in six.iteritems(trusted_hashes): digest_object = tuf.hash.digest(algorithm) digest_object.update(file_object.read()) computed_hash = digest_object.hexdigest() @@ -824,7 +824,7 @@ def verify_target_file(target_file_object): # 'compression' argument to _get_file() is needed only for decompression of # metadata. Target files may be compressed or uncompressed. if self.consistent_snapshot: - target_digest = random.choice(file_hashes.values()) + target_digest = random.choice(list(file_hashes.values())) dirname, basename = os.path.split(target_filepath) target_filepath = os.path.join(dirname, target_digest+'.'+basename) @@ -883,8 +883,10 @@ def _verify_uncompressed_metadata_file(self, metadata_file_object, try: metadata_signable = tuf.util.load_json_string(metadata) - except Exception, exception: + + except Exception as exception: raise tuf.InvalidMetadataJSONError(exception) + else: # Ensure the loaded 'metadata_signable' is properly formatted. Raise # 'tuf.FormatError' if not. @@ -908,7 +910,7 @@ def _verify_uncompressed_metadata_file(self, metadata_file_object, # are not allowed. if metadata_signable['signed']['_type'] == 'Targets': if metadata_role != 'targets': - metadata_targets = metadata_signable['signed']['targets'].keys() + metadata_targets = list(metadata_signable['signed']['targets'].keys()) parent_rolename = tuf.roledb.get_parent_rolename(metadata_role) parent_role_metadata = self.metadata['current'][parent_rolename] parent_delegations = parent_role_metadata['delegations'] @@ -1175,7 +1177,7 @@ def _get_file(self, filepath, verify_file_function, file_type, # uncompressed version). verify_file_function(file_object) - except Exception, exception: + except Exception as exception: # Remember the error from this mirror, and "reset" the target file. logger.exception('Update failed from '+file_mirror+'.') file_mirror_errors[file_mirror] = exception @@ -1296,11 +1298,11 @@ def _update_metadata(self, metadata_role, uncompressed_fileinfo, if self.consistent_snapshot: if compression: filename_digest = \ - random.choice(compressed_fileinfo['hashes'].values()) + random.choice(list(compressed_fileinfo['hashes'].values())) else: filename_digest = \ - random.choice(uncompressed_fileinfo['hashes'].values()) + random.choice(list(uncompressed_fileinfo['hashes'].values())) dirname, basename = os.path.split(remote_filename) remote_filename = os.path.join(dirname, filename_digest+'.'+basename) @@ -1582,7 +1584,7 @@ def _fileinfo_has_changed(self, metadata_filename, new_fileinfo): # without having that result in considering all files as needing to be # updated, or not all hash algorithms listed can be calculated on the # specific client. - for algorithm, hash_value in new_fileinfo['hashes'].items(): + for algorithm, hash_value in six.iteritems(new_fileinfo['hashes']): # We're only looking for a single match. This isn't a security # check, we just want to prevent unnecessary downloads. if algorithm in current_fileinfo['hashes']: @@ -1873,7 +1875,7 @@ def _refresh_targets_metadata(self, rolename='targets', include_delegations=Fals # See if this role provides metadata and, if we're including delegations, # look for metadata from delegated roles. role_prefix = rolename + '/' - for metadata_path in self.metadata['current']['snapshot']['meta'].keys(): + for metadata_path in six.iterkeys(self.metadata['current']['snapshot']['meta']): if metadata_path == rolename + '.json': roles_to_update.append(metadata_path[:-len('.json')]) elif include_delegations and metadata_path.startswith(role_prefix): @@ -1998,7 +2000,7 @@ def refresh_targets_metadata_chain(self, rolename): # Check if 'snapshot.json' provides metadata for each of the roles in # 'parent_roles'. All the available roles on the repository are specified # in the 'snapshot.json' metadata. - targets_metadata_allowed = self.metadata['current']['snapshot']['meta'].keys() + targets_metadata_allowed = list(self.metadata['current']['snapshot']['meta'].keys()) for parent_role in parent_roles: parent_role = parent_role + '.json' @@ -2103,7 +2105,7 @@ def _targets_of_role(self, rolename, targets=None, skip_refresh=False): return targets # Get the targets specified by the role itself. - for filepath, fileinfo in self.metadata['current'][rolename]['targets'].items(): + for filepath, fileinfo in six.iteritems(self.metadata['current'][rolename]['targets']): new_target = {} new_target['filepath'] = filepath new_target['fileinfo'] = fileinfo @@ -2204,7 +2206,7 @@ def target(self, target_filepath): # 'target_filepath' might contain URL encoding escapes. # http://docs.python.org/2/library/urllib.html#urllib.unquote - target_filepath = urllib.unquote(target_filepath) + target_filepath = six.moves.urllib.parse.unquote(target_filepath) if not target_filepath.startswith('/'): target_filepath = '/' + target_filepath @@ -2339,7 +2341,7 @@ def _get_target_from_targets_role(self, role_name, targets, target_filepath): # Does the current role name have our target? logger.debug('Asking role '+repr(role_name)+' about target '+\ repr(target_filepath)) - for filepath, fileinfo in targets.iteritems(): + for filepath, fileinfo in six.iteritems(targets): if filepath == target_filepath: logger.debug('Found target '+target_filepath+' in role '+role_name) target = {'filepath': filepath, 'fileinfo': fileinfo} @@ -2523,8 +2525,8 @@ def remove_obsolete_targets(self, destination_directory): for role in tuf.roledb.get_rolenames(): if role.startswith('targets'): if role in self.metadata['previous'] and self.metadata['previous'][role] != None: - for target in self.metadata['previous'][role]['targets'].keys(): - if target not in self.metadata['current'][role]['targets'].keys(): + for target in self.metadata['previous'][role]['targets']: + if target not in self.metadata['current'][role]['targets']: # 'target' is only in 'previous', so remove it. logger.warn('Removing obsolete file: '+repr(target)+'.') # Remove the file if it hasn't been removed already. @@ -2532,7 +2534,7 @@ def remove_obsolete_targets(self, destination_directory): try: os.remove(destination) - except OSError, e: + except OSError as e: # If 'filename' already removed, just log it. if e.errno == errno.ENOENT: logger.info('File '+repr(destination)+' was already removed.') @@ -2540,7 +2542,7 @@ def remove_obsolete_targets(self, destination_directory): else: logger.error(str(e)) - except Exception, e: + except Exception as e: logger.error(str(e)) @@ -2602,7 +2604,7 @@ def updated_targets(self, targets, destination_directory): # Try one of the algorithm/digest combos for a mismatch. We break # as soon as we find a mismatch. - for algorithm, digest in target['fileinfo']['hashes'].items(): + for algorithm, digest in six.iteritems(target['fileinfo']['hashes']): digest_object = None try: digest_object = tuf.hash.digest_filename(target_filepath, @@ -2686,7 +2688,7 @@ def download_target(self, target, destination_directory): try: os.makedirs(target_dirpath) - except OSError, e: + except OSError as e: if e.errno == errno.EEXIST: pass diff --git a/tuf/download.py b/tuf/download.py index aba22fdb..dab2a505 100755 --- a/tuf/download.py +++ b/tuf/download.py @@ -241,7 +241,7 @@ def read(self, size): except socket.timeout: self.__stop_clock_and_check_speed(0) continue - except socket.error, e: + except socket.error as e: if e.args[0] == EINTR: self.__stop_clock_and_check_speed(0) continue diff --git a/tuf/ed25519_keys.py b/tuf/ed25519_keys.py index 82dd9c2a..b1aa3e7c 100755 --- a/tuf/ed25519_keys.py +++ b/tuf/ed25519_keys.py @@ -366,7 +366,7 @@ def verify_signature(public_key, method, signature, data, use_pynacl=False): # The pure Python implementation raises 'Exception' if 'signature' is # invalid. - except Exception, e: + except Exception as e: pass else: message = 'Unsupported ed25519 signing method: '+repr(method)+'.\n'+ \ diff --git a/tuf/formats.py b/tuf/formats.py index 2544312b..2ccbfb07 100755 --- a/tuf/formats.py +++ b/tuf/formats.py @@ -69,7 +69,7 @@ import tuf import tuf.schema as SCHEMA - +import tuf._vendor.six as six # Note that in the schema definitions below, the 'SCHEMA.Object' types allow # additional keys which are not defined. Thus, any additions to them will be @@ -477,7 +477,8 @@ class MetaFile(object): def __eq__(self, other): return isinstance(other, MetaFile) and self.info == other.info - + + __hash__ = None def __ne__(self, other): return not self.__eq__(other) @@ -489,10 +490,12 @@ def __getattr__(self, name): 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: return self.info[name] - raise AttributeError, name + + else: + raise AttributeError(name) @@ -818,7 +821,7 @@ def format_base64(data): try: return binascii.b2a_base64(data).rstrip('=\n ') - except (TypeError, binascii.Error), e: + except (TypeError, binascii.Error) as e: raise tuf.FormatError('Invalid base64 encoding: '+str(e)) @@ -846,7 +849,7 @@ def parse_base64(base64_string): 'base64_string'. """ - if not isinstance(base64_string, basestring): + if not isinstance(base64_string, six.string_types): message = 'Invalid argument: '+repr(base64_string) raise tuf.FormatError(message) @@ -858,7 +861,7 @@ def parse_base64(base64_string): try: return binascii.a2b_base64(base64_string) - except (TypeError, binascii.Error), e: + except (TypeError, binascii.Error) as e: raise tuf.FormatError('Invalid base64 encoding: '+str(e)) @@ -1189,7 +1192,7 @@ def _encode_canonical(object, output_function): # Helper for encode_canonical. Older versions of json.encoder don't # even let us replace the separators. - if isinstance(object, basestring): + if isinstance(object, six.string_types): output_function(_canonical_string_encoder(object)) elif object is True: output_function("true") @@ -1197,7 +1200,7 @@ def _encode_canonical(object, output_function): output_function("false") elif object is None: output_function("null") - elif isinstance(object, (int, long)): + elif isinstance(object, six.integer_types): output_function(str(object)) elif isinstance(object, (tuple, list)): output_function("[") @@ -1210,8 +1213,7 @@ def _encode_canonical(object, output_function): elif isinstance(object, dict): output_function("{") if len(object): - items = object.items() - items.sort() + items = sorted(six.iteritems(object)) for key, value in items[:-1]: output_function(_canonical_string_encoder(key)) output_function(":") @@ -1289,7 +1291,7 @@ def encode_canonical(object, output_function=None): try: _encode_canonical(object, output_function) - except TypeError, e: + except TypeError as e: message = 'Could not encode '+repr(object)+': '+str(e) raise tuf.FormatError(message) diff --git a/tuf/keydb.py b/tuf/keydb.py index 8340735f..d51f1711 100755 --- a/tuf/keydb.py +++ b/tuf/keydb.py @@ -33,6 +33,7 @@ import tuf import tuf.formats import tuf.keys +import tuf._vendor.six as six # List of strings representing the key types supported by TUF. _SUPPORTED_KEY_TYPES = ['rsa', 'ed25519'] @@ -84,7 +85,7 @@ def create_keydb_from_root_metadata(root_metadata): # Iterate through the keys found in 'root_metadata' by converting # them to 'RSAKEY_SCHEMA' if their type is 'rsa', and then # adding them the database. Duplicates are avoided. - for keyid, key_metadata in root_metadata['keys'].items(): + for keyid, key_metadata in six.iteritems(root_metadata['keys']): if key_metadata['keytype'] in _SUPPORTED_KEY_TYPES: # 'key_metadata' is stored in 'KEY_SCHEMA' format. Call # create_from_metadata_format() to get the key in 'RSAKEY_SCHEMA' @@ -94,11 +95,11 @@ def create_keydb_from_root_metadata(root_metadata): add_key(key_dict, keyid) # 'tuf.Error' raised if keyid does not match the keyid for 'rsakey_dict'. - except tuf.Error, e: + except tuf.Error as e: logger.error(e) continue - except tuf.KeyAlreadyExistsError, e: + except tuf.KeyAlreadyExistsError as e: logger.warn(e) continue diff --git a/tuf/mirrors.py b/tuf/mirrors.py index 4ece3515..bc0825ad 100755 --- a/tuf/mirrors.py +++ b/tuf/mirrors.py @@ -18,11 +18,11 @@ """ import os -import urllib import tuf import tuf.util import tuf.formats +import tuf._vendor.six as six # The type of file to be downloaded from a repository. The # 'get_list_of_mirrors' function supports these file types. @@ -87,7 +87,7 @@ def get_list_of_mirrors(file_type, file_path, mirrors_dict): in_confined_directory = tuf.util.file_in_confined_directories list_of_mirrors = [] - for mirror_name, mirror_info in mirrors_dict.items(): + for mirror_name, mirror_info in six.iteritems(mirrors_dict): if file_type == 'meta': base = mirror_info['url_prefix']+'/'+mirror_info['metadata_path'] @@ -109,7 +109,7 @@ def get_list_of_mirrors(file_type, file_path, mirrors_dict): # side. Do *NOT* pass URLs with Unicode characters without first encoding # the URL as UTF-8. We need a long-term solution with #61. # http://bugs.python.org/issue1712522 - file_path = urllib.quote(file_path) + file_path = six.moves.urllib.parse.quote(file_path) url = base + '/' + file_path.lstrip(os.sep) list_of_mirrors.append(url) diff --git a/tuf/pycrypto_keys.py b/tuf/pycrypto_keys.py index def95632..4f9f6416 100755 --- a/tuf/pycrypto_keys.py +++ b/tuf/pycrypto_keys.py @@ -284,7 +284,7 @@ def create_rsa_signature(private_key, data): sha256_object = Crypto.Hash.SHA256.new(data) rsa_key_object = Crypto.PublicKey.RSA.importKey(private_key) - except (ValueError, IndexError, TypeError), e: + except (ValueError, IndexError, TypeError) as e: message = 'Invalid private key or hash data: '+str(e) raise tuf.CryptoError(message) @@ -383,7 +383,7 @@ def verify_rsa_signature(signature, signature_method, public_key, data): sha256_object = Crypto.Hash.SHA256.new(data) valid_signature = pkcs1_pss_verifier.verify(sha256_object, signature) - except (ValueError, IndexError, TypeError), e: + except (ValueError, IndexError, TypeError) as e: message = 'The RSA signature could not be verified.' raise tuf.CryptoError(message) @@ -463,7 +463,7 @@ def create_rsa_encrypted_pem(private_key, passphrase): encrypted_pem = rsa_key_object.exportKey(format='PEM', passphrase=passphrase) - except (ValueError, IndexError, TypeError), e: + except (ValueError, IndexError, TypeError) as e: message = 'An encrypted RSA key in PEM format cannot be generated: '+str(e) raise tuf.CryptoError(message) @@ -559,7 +559,7 @@ def create_rsa_public_and_private_from_encrypted_pem(encrypted_pem, passphrase): # (possibly because the passphrase is wrong)." # If the passphrase is incorrect, PyCrypto returns: "RSA key format is not # supported". - except (ValueError, IndexError, TypeError), e: + except (ValueError, IndexError, TypeError) as e: message = 'RSA (public, private) tuple cannot be generated from the'+\ ' encrypted PEM string: '+str(e) # Raise 'tuf.CryptoError' and PyCrypto's exception message. Avoid @@ -851,7 +851,7 @@ def _encrypt(key_data, derived_key_information): # what circumstances. PyCrypto example given is to call encrypt() without # checking for exceptions. Avoid propogating the exception trace and only # raise 'tuf.CryptoError', along with the cause of encryption failure. - except (ValueError, IndexError, TypeError), e: + except (ValueError, IndexError, TypeError) as e: message = 'The key data cannot be encrypted: '+str(e) raise tuf.CryptoError(message) @@ -939,7 +939,7 @@ def _decrypt(file_contents, password): # what circumstances. PyCrypto example given is to call decrypt() without # checking for exceptions. Avoid propogating the exception trace and only # raise 'tuf.CryptoError', along with the cause of decryption failure. - except (ValueError, IndexError, TypeError), e: + except (ValueError, IndexError, TypeError) as e: raise tuf.CryptoError('Decryption failed: '+str(e)) return key_plaintext diff --git a/tuf/repository_tool.py b/tuf/repository_tool.py index d6283f12..05873fe2 100755 --- a/tuf/repository_tool.py +++ b/tuf/repository_tool.py @@ -48,6 +48,7 @@ import tuf.log import tuf.conf import tuf._vendor.iso8601 as iso8601 +import tuf._vendor.six as six # See 'log.py' to learn how logging is handled in TUF. logger = logging.getLogger('tuf.repository_tool') @@ -262,7 +263,7 @@ def write(self, write_partial=False, consistent_snapshot=False): consistent_snapshot) # Include only the exception message. - except tuf.UnsignedMetadataError, e: + except tuf.UnsignedMetadataError as e: raise tuf.UnsignedMetadataError(e[0]) # Generate the 'root.json' metadata file. @@ -278,7 +279,7 @@ def write(self, write_partial=False, consistent_snapshot=False): consistent_snapshot) # Include only the exception message. - except tuf.UnsignedMetadataError, e: + except tuf.UnsignedMetadataError as e: raise tuf.UnsignedMetadataError(e[0]) # Generate the 'targets.json' metadata file. @@ -292,7 +293,7 @@ def write(self, write_partial=False, consistent_snapshot=False): consistent_snapshot) # Include only the exception message. - except tuf.UnsignedMetadataError, e: + except tuf.UnsignedMetadataError as e: raise tuf.UnsignedMetadataError(e[0]) # Generate the 'snapshot.json' metadata file. @@ -309,7 +310,7 @@ def write(self, write_partial=False, consistent_snapshot=False): consistent_snapshot, filenames) # Include only the exception message. - except tuf.UnsignedMetadataError, e: + except tuf.UnsignedMetadataError as e: raise tuf.UnsignedMetadataError(e[0]) # Generate the 'timestamp.json' metadata file. @@ -323,7 +324,7 @@ def write(self, write_partial=False, consistent_snapshot=False): filenames) # Include only the exception message. - except tuf.UnsignedMetadataError, e: + except tuf.UnsignedMetadataError as e: raise tuf.UnsignedMetadataError(e[0]) @@ -420,14 +421,14 @@ def status(self): try: _check_role_keys(delegated_role) - except tuf.InsufficientKeysError, e: + except tuf.InsufficientKeysError as e: insufficient_keys.append(delegated_role) continue try: _generate_and_write_metadata(delegated_role, filename, False, targets_directory, metadata_directory) - except tuf.UnsignedMetadataError, e: + except tuf.UnsignedMetadataError as e: insufficient_signatures.append(delegated_role) # Print the verification results of the delegated roles and return @@ -589,7 +590,7 @@ def add_verification_key(self, key): try: tuf.keydb.add_key(key) - except tuf.KeyAlreadyExistsError, e: + except tuf.KeyAlreadyExistsError as e: message = 'Adding a verification key that has already been used.' logger.warn(message) @@ -699,7 +700,7 @@ def load_signing_key(self, key): try: tuf.keydb.add_key(key) - except tuf.KeyAlreadyExistsError, e: + except tuf.KeyAlreadyExistsError as e: tuf.keydb.remove_key(key['keyid']) tuf.keydb.add_key(key) @@ -1320,7 +1321,7 @@ def __init__(self): try: tuf.roledb.add_role(self._rolename, roleinfo) - except tuf.RoleAlreadyExistsError, e: + except tuf.RoleAlreadyExistsError as e: pass @@ -1382,7 +1383,7 @@ def __init__(self): try: tuf.roledb.add_role(self.rolename, roleinfo) - except tuf.RoleAlreadyExistsError, e: + except tuf.RoleAlreadyExistsError as e: pass @@ -1438,7 +1439,7 @@ def __init__(self): try: tuf.roledb.add_role(self._rolename, roleinfo) - except tuf.RoleAlreadyExistsError, e: + except tuf.RoleAlreadyExistsError as e: pass @@ -1531,7 +1532,7 @@ def __init__(self, targets_directory, rolename='targets', roleinfo=None): try: tuf.roledb.add_role(self.rolename, roleinfo) - except tuf.RoleAlreadyExistsError, e: + except tuf.RoleAlreadyExistsError as e: pass @@ -2255,7 +2256,7 @@ def delegate_hashed_bins(self, list_of_targets, keys_of_hashed_bins, # target path, reduced to the first 'prefix_length' hex digits, is # calculated to determine which 'bin_index' is should go. target_paths_in_bin = {} - for bin_index in xrange(total_hash_prefixes): + for bin_index in six.moves.xrange(total_hash_prefixes): target_paths_in_bin[bin_index] = [] # Assign every path to its bin. Ensure every target is located under the @@ -2296,7 +2297,7 @@ def delegate_hashed_bins(self, list_of_targets, keys_of_hashed_bins, # The parent roles will list bin roles starting from "0" to # 'total_hash_prefixes' in 'bin_offset' increments. The skipped bin roles # are listed in 'path_hash_prefixes' of 'outer_bin_index. - for outer_bin_index in xrange(0, total_hash_prefixes, bin_offset): + for outer_bin_index in six.moves.xrange(0, total_hash_prefixes, bin_offset): # The bin index is hex padded from the left with zeroes for up to the # 'prefix_length' (e.g., 'targets/unclaimed/000-003'). Ensure the correct # hash bin name is generated if a prefix range is unneeded. @@ -2312,7 +2313,7 @@ def delegate_hashed_bins(self, list_of_targets, keys_of_hashed_bins, path_hash_prefixes = [] bin_rolename_targets = [] - for inner_bin_index in xrange(outer_bin_index, outer_bin_index+bin_offset): + for inner_bin_index in six.moves.xrange(outer_bin_index, outer_bin_index+bin_offset): # 'inner_bin_rolename' needed in padded hex. For example, "00b". inner_bin_rolename = hex(inner_bin_index)[2:].zfill(prefix_length) path_hash_prefixes.append(inner_bin_rolename) @@ -2505,7 +2506,7 @@ def _print_status_of_top_level_roles(targets_directory, metadata_directory): try: _check_role_keys(rolename) - except tuf.InsufficientKeysError, e: + except tuf.InsufficientKeysError as e: print(str(e)) return @@ -2520,7 +2521,7 @@ def _print_status_of_top_level_roles(targets_directory, metadata_directory): # 'tuf.UnsignedMetadataError' raised if metadata contains an invalid threshold # of signatures. Print the valid/threshold message, where valid < threshold. - except tuf.UnsignedMetadataError, e: + except tuf.UnsignedMetadataError as e: signable = e[1] _print_status('root', signable) return @@ -2532,7 +2533,7 @@ def _print_status_of_top_level_roles(targets_directory, metadata_directory): targets_directory, metadata_directory) _print_status('targets', signable) - except tuf.UnsignedMetadataError, e: + except tuf.UnsignedMetadataError as e: signable = e[1] _print_status('targets', signable) return @@ -2546,7 +2547,7 @@ def _print_status_of_top_level_roles(targets_directory, metadata_directory): False, filenames) _print_status('snapshot', signable) - except tuf.UnsignedMetadataError, e: + except tuf.UnsignedMetadataError as e: signable = e[1] _print_status('snapshot', signable) return @@ -2560,7 +2561,7 @@ def _print_status_of_top_level_roles(targets_directory, metadata_directory): False, filenames) _print_status('timestamp', signable) - except tuf.UnsignedMetadataError, e: + except tuf.UnsignedMetadataError as e: signable = e[1] _print_status('timestamp', signable) return @@ -2591,7 +2592,7 @@ def _prompt(message, result_type=str): caller. """ - return result_type(raw_input(message)) + return result_type(six.moves.input(message)) @@ -2741,7 +2742,7 @@ def _remove_invalid_and_duplicate_signatures(signable): try: key = tuf.keydb.get_key(keyid) - except tuf.UnknownKeyError, e: + except tuf.UnknownKeyError as e: signable['signatures'].remove(signature) # Remove 'signature' from 'signable' if it is an invalid signature. @@ -2926,7 +2927,7 @@ def create_new_repository(repository_directory): # 'OSError' raised if the leaf directory already exists or cannot be created. # Check for case where 'repository_directory' has already been created. - except OSError, e: + except OSError as e: if e.errno == errno.EEXIST: pass else: @@ -2949,7 +2950,7 @@ def create_new_repository(repository_directory): os.mkdir(metadata_directory) # 'OSError' raised if the leaf directory already exists or cannot be created. - except OSError, e: + except OSError as e: if e.errno == errno.EEXIST: pass else: @@ -2961,7 +2962,7 @@ def create_new_repository(repository_directory): logger.info(message) os.mkdir(targets_directory) - except OSError, e: + except OSError as e: if e.errno == errno.EEXIST: pass else: @@ -3073,7 +3074,7 @@ def load_repository(repository_directory): try: signable = tuf.util.load_json_file(metadata_path) - except (ValueError, IOError), e: + except (ValueError, IOError) as e: continue metadata_object = signable['signed'] @@ -3120,7 +3121,7 @@ def load_repository(repository_directory): try: tuf.keydb.add_key(key_object) - except tuf.KeyAlreadyExistsError, e: + except tuf.KeyAlreadyExistsError as e: pass # Add the delegated role's initial roleinfo, to be fully populated @@ -3300,7 +3301,7 @@ def _load_top_level_metadata(repository, top_level_filenames): try: tuf.keydb.add_key(key_object) - except tuf.KeyAlreadyExistsError, e: + except tuf.KeyAlreadyExistsError as e: pass for role in targets_metadata['delegations']['roles']: @@ -3538,7 +3539,7 @@ def import_rsa_publickey_from_file(filepath): try: rsakey_dict = tuf.keys.format_rsakey_from_pem(rsa_pubkey_pem) - except tuf.FormatError, e: + except tuf.FormatError as e: raise tuf.Error('Cannot import improperly formatted PEM file.') return rsakey_dict @@ -4509,7 +4510,7 @@ def write_metadata_file(metadata, filename, compressions, consistent_snapshot): write_new_metadata = True # 'tuf.Error' raised if 'filename' does not exist. - except tuf.Error, e: + except tuf.Error as e: write_new_metadata = True if write_new_metadata: @@ -4693,7 +4694,7 @@ def create_tuf_client_directory(repository_directory, client_directory): try: os.makedirs(client_metadata_directory) - except OSError, e: + except OSError as e: if e.errno == errno.EEXIST: message = 'Cannot create a fresh client metadata directory: '+ \ repr(client_metadata_directory)+'. Already exists.' diff --git a/tuf/roledb.py b/tuf/roledb.py index 835e5bb0..a1a6f350 100755 --- a/tuf/roledb.py +++ b/tuf/roledb.py @@ -41,6 +41,7 @@ import tuf import tuf.formats import tuf.log +import tuf._vendor.six as six # See 'tuf.log' to learn how logging is handled in TUF. logger = logging.getLogger('tuf.roledb') @@ -89,7 +90,7 @@ 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(): + for rolename, roleinfo in six.iteritems(root_metadata['roles']): if rolename == 'root': roleinfo['version'] = root_metadata['version'] roleinfo['expires'] = root_metadata['expires'] @@ -104,7 +105,7 @@ def create_roledb_from_root_metadata(root_metadata): try: add_role(rolename, roleinfo) # tuf.Error raised if the parent role of 'rolename' does not exist. - except tuf.Error, e: + except tuf.Error as e: logger.error(e) raise @@ -475,7 +476,7 @@ def get_rolenames(): A list of rolenames. """ - return _roledb_dict.keys() + return list(_roledb_dict.keys()) diff --git a/tuf/schema.py b/tuf/schema.py index 278e1899..b976a379 100755 --- a/tuf/schema.py +++ b/tuf/schema.py @@ -45,7 +45,7 @@ import sys import tuf - +import tuf._vendor.six as six class Schema: """ @@ -141,7 +141,7 @@ class String(Schema): """ def __init__(self, string): - if not isinstance(string, basestring): + if not isinstance(string, six.string_types): raise tuf.FormatError('Expected a string but got '+repr(string)) self._string = string @@ -188,7 +188,7 @@ def __init__(self): def check_match(self, object): - if not isinstance(object, basestring): + if not isinstance(object, six.string_types): raise tuf.FormatError('Expected a string but got '+repr(object)) @@ -217,7 +217,7 @@ class LengthString(Schema): """ def __init__(self, length): - if isinstance(length, bool) or not isinstance(length, (int, long)): + if isinstance(length, bool) or not isinstance(length, six.integer_types): # 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.') @@ -226,7 +226,7 @@ def __init__(self, length): def check_match(self, object): - if not isinstance(object, basestring): + if not isinstance(object, six.string_types): raise tuf.FormatError('Expected a string but got '+repr(object)) if len(object) != self._string_length: @@ -401,7 +401,7 @@ class ListOf(Schema): False """ - def __init__(self, schema, min_count=0, max_count=sys.maxint, list_name='list'): + def __init__(self, schema, min_count=0, max_count=sys.maxsize, list_name='list'): """ Create a new ListOf schema. @@ -433,7 +433,7 @@ def check_match(self, object): for item in object: try: self._schema.check_match(item) - except tuf.FormatError, e: + except tuf.FormatError as e: raise tuf.FormatError(str(e)+' in '+repr(self._list_name)) # Raise exception if the number of items in the list is @@ -475,7 +475,7 @@ class Integer(Schema): False """ - def __init__(self, lo= -sys.maxint, hi=sys.maxint): + def __init__(self, lo = -sys.maxint, hi = sys.maxint): """ Create a new Integer schema. @@ -490,7 +490,7 @@ def __init__(self, lo= -sys.maxint, hi=sys.maxint): def check_match(self, object): - if isinstance(object, bool) or not isinstance(object, (int, long)): + if isinstance(object, bool) or not isinstance(object, six.integer_types): # 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(object)+' instead of an integer.') @@ -556,7 +556,7 @@ def check_match(self, object): if not isinstance(object, dict): raise tuf.FormatError('Expected a dict but got '+repr(object)) - for key, value in object.items(): + for key, value in six.iteritems(object): self._key_schema.check_match(key) self._value_schema.check_match(value) @@ -643,12 +643,12 @@ def __init__(self, object_name='object', **required): """ # Ensure valid arguments. - for key, schema in required.items(): + for key, schema in six.iteritems(required): if not isinstance(schema, Schema): raise tuf.FormatError('Expected Schema but got '+repr(schema)) self._object_name = object_name - self._required = required.items() + self._required = list(required.items()) def check_match(self, object): @@ -672,7 +672,7 @@ def check_match(self, object): else: try: schema.check_match(item) - except tuf.FormatError, e: + except tuf.FormatError as e: raise tuf.FormatError(str(e)+' in '+self._object_name+'.'+key) @@ -827,7 +827,7 @@ def __init__(self, pattern=None, modifiers=0, re_object=None, re_name=None): re_name: Identifier for the regular expression object. """ - if not isinstance(pattern, basestring): + if not isinstance(pattern, six.string_types): raise tuf.FormatError(repr(pattern)+' is not a string.') if re_object is None: @@ -848,13 +848,11 @@ def __init__(self, pattern=None, modifiers=0, re_object=None, re_name=None): def check_match(self, object): - if not isinstance(object, basestring) or not self._re_object.match(object): + if not isinstance(object, six.string_types) or not self._re_object.match(object): raise tuf.FormatError(repr(object)+' did not match '+repr(self._re_name)) - - if __name__ == '__main__': # The interactive sessions of the documentation strings can # be tested by running schema.py as a standalone module. diff --git a/tuf/util.py b/tuf/util.py index 7c93668d..bcbc10c7 100755 --- a/tuf/util.py +++ b/tuf/util.py @@ -29,6 +29,7 @@ import tuf.hash import tuf.conf import tuf.formats +import tuf._vendor.six as six # The algorithm used by the repository to generate the digests of the # target filepaths, which are included in metadata files and may be prepended @@ -54,7 +55,7 @@ def _default_temporary_directory(self, prefix): """__init__ helper.""" try: self.temporary_file = tempfile.NamedTemporaryFile(prefix=prefix) - except OSError, err: + except OSError as err: logger.critical('Temp file in '+temp_dir+'failed: '+repr(err)) raise tuf.Error(err) @@ -84,7 +85,7 @@ def __init__(self, prefix='tuf_temp_'): try: self.temporary_file = tempfile.NamedTemporaryFile(prefix=prefix, dir=temp_dir) - except OSError, err: + except OSError as err: logger.error('Temp file in '+temp_dir+' failed: '+repr(err)) logger.error('Will attempt to use system default temp dir.') self._default_temporary_directory(prefix) @@ -293,7 +294,7 @@ def decompress_temp_file_object(self, compression): try: self.temporary_file = gzip.GzipFile(fileobj=self.temporary_file, mode='rb') - except Exception, exception: + except Exception as exception: raise tuf.DecompressionError(exception) @@ -508,7 +509,7 @@ def find_delegated_role(roles, delegated_role): # The index of a role, if any, with the same name. role_index = None - for index in xrange(len(roles)): + for index in six.moves.xrange(len(roles)): role = roles[index] name = role.get('name') @@ -908,7 +909,7 @@ def load_json_file(filepath): try: deserialized_object = json.load(fileobject) - except ValueError, TypeError: + except (ValueError, TypeError) as e: message = 'Cannot deserialize to a Python object: '+repr(filepath) raise tuf.Error(message) From 7843bdd27225c6d6d1cfb1ffbd9dcbd3358b92d5 Mon Sep 17 00:00:00 2001 From: Vladimir Diaz Date: Wed, 23 Apr 2014 12:50:05 -0400 Subject: [PATCH 02/32] [WIP] Python 2+3 support. Add six, convert PY <=2.5 exception handling, dictionary iteration, libraries, remaining 7/8 test modules. --- tests/test_indefinite_freeze_attack.py | 6 +++--- tests/test_mirrors.py | 4 ++-- tests/test_mix_and_match_attack.py | 6 +++--- tests/test_repository_tool.py | 9 +++++---- tests/test_slow_retrieval_attack.py | 14 +++++++------- tests/test_updater.py | 25 +++++++++++++------------ tests/test_util.py | 24 ++++++++++++++++-------- 7 files changed, 49 insertions(+), 39 deletions(-) diff --git a/tests/test_indefinite_freeze_attack.py b/tests/test_indefinite_freeze_attack.py index f6cc1c72..0e5bbde1 100755 --- a/tests/test_indefinite_freeze_attack.py +++ b/tests/test_indefinite_freeze_attack.py @@ -30,10 +30,9 @@ from __future__ import division import os -import urllib -import tempfile import random import time +import tempfile import shutil import json import subprocess @@ -46,6 +45,7 @@ import tuf.client.updater as updater import tuf.repository_tool as repo_tool import tuf.unittest_toolbox as unittest_toolbox +import tuf._vendor.six as six # The repository tool is imported and logs console messages by default. Disable # console log messages generated by this unit test. @@ -192,7 +192,7 @@ def test_without_tuf(self): url_prefix = self.repository_mirrors['mirror1']['url_prefix'] url_file = os.path.join(url_prefix, 'metadata', 'timestamp.json') - urllib.urlretrieve(url_file, client_timestamp_path) + six.moves.urllib.request.urlretrieve(url_file, client_timestamp_path) length, hashes = tuf.util.get_file_details(client_timestamp_path) download_fileinfo = tuf.formats.make_fileinfo(length, hashes) diff --git a/tests/test_mirrors.py b/tests/test_mirrors.py index 67572d9f..6cc68f79 100755 --- a/tests/test_mirrors.py +++ b/tests/test_mirrors.py @@ -23,7 +23,7 @@ import tuf.formats as formats import tuf.mirrors as mirrors import tuf.unittest_toolbox as unittest_toolbox - +import tuf._vendor.six as six class TestMirrors(unittest_toolbox.Modified_TestCase): @@ -54,7 +54,7 @@ def test_get_list_of_mirrors(self): # Test: Normal case. mirror_list = mirrors.get_list_of_mirrors('meta', 'release.txt', self.mirrors) self.assertEquals(len(mirror_list), 3) - for mirror, mirror_info in self.mirrors.items(): + for mirror, mirror_info in six.iteritems(self.mirrors): url = mirror_info['url_prefix']+'/metadata/release.txt' self.assertTrue(url in mirror_list) diff --git a/tests/test_mix_and_match_attack.py b/tests/test_mix_and_match_attack.py index 4b1191ed..f9e53883 100755 --- a/tests/test_mix_and_match_attack.py +++ b/tests/test_mix_and_match_attack.py @@ -35,7 +35,6 @@ from __future__ import division import os -import urllib import tempfile import random import time @@ -51,6 +50,7 @@ import tuf.client.updater as updater import tuf.repository_tool as repo_tool import tuf.unittest_toolbox as unittest_toolbox +import tuf._vendor.six as six # The repository tool is imported and logs console messages by default. Disable # console log messages generated by this unit test. @@ -232,8 +232,8 @@ def test_with_tuf(self): # Verify that the specific 'tuf.BadHashError' exception is raised by each # mirror. - except tuf.NoWorkingMirrorError, exception: - for mirror_url, mirror_error in exception.mirror_errors.iteritems(): + except tuf.NoWorkingMirrorError as exception: + for mirror_url, mirror_error in six.iteritems(exception.mirror_errors): url_prefix = self.repository_mirrors['mirror1']['url_prefix'] url_file = os.path.join(url_prefix, 'metadata', 'targets', 'role1.json') diff --git a/tests/test_repository_tool.py b/tests/test_repository_tool.py index b03e05bd..a45dcf8d 100755 --- a/tests/test_repository_tool.py +++ b/tests/test_repository_tool.py @@ -30,6 +30,7 @@ import tuf.keydb import tuf.hash import tuf.repository_tool as repo_tool +import tuf._vendor.six as six logger = logging.getLogger('tuf.test_repository_tool') @@ -229,7 +230,7 @@ def test_write_and_write_partial(self): # is made to a role. repo_tool.load_repository(repository_directory) - repository.timestamp.expiration = datetime.datetime(2030, 01, 01, 12, 00) + repository.timestamp.expiration = datetime.datetime(2030, 1, 1, 12, 0) self.assertRaises(tuf.UnsignedMetadataError, repository.write) # Verify that a write_partial() is allowed. @@ -362,7 +363,7 @@ def test_expiration(self): self.assertTrue(isinstance(expiration, datetime.datetime)) # Test expiration setter. - self.metadata.expiration = datetime.datetime(2030, 01, 01, 12, 00) + self.metadata.expiration = datetime.datetime(2030, 1, 1, 12, 0) expiration = self.metadata.expiration self.assertTrue(isinstance(expiration, datetime.datetime)) @@ -1512,7 +1513,7 @@ def test_get_target_hash(self): '/README.txt': '8faee106f1bb69f34aaf1df1e3c2e87d763c4d878cb96b91db13495e32ceb0b0', '/packages/file2.txt': 'c9c4a5cdd84858dd6a23d98d7e6e6b2aec45034946c16b2200bc317c75415e92' } - for filepath, target_hash in expected_target_hashes.items(): + for filepath, target_hash in six.iteritems(expected_target_hashes): self.assertTrue(tuf.formats.RELPATH_SCHEMA.matches(filepath)) self.assertTrue(tuf.formats.HASH_SCHEMA.matches(target_hash)) self.assertEqual(repo_tool.get_target_hash(filepath), target_hash) @@ -1568,7 +1569,7 @@ def test_generate_targets_metadata(self): # Set valid generate_targets_metadata() arguments. version = 1 - datetime_object = datetime.datetime(2030, 01, 01, 12, 00) + datetime_object = datetime.datetime(2030, 1, 1, 12, 0) expiration_date = datetime_object.isoformat() + 'Z' target_files = ['file.txt'] diff --git a/tests/test_slow_retrieval_attack.py b/tests/test_slow_retrieval_attack.py index 364fb8ef..bf59ea5b 100755 --- a/tests/test_slow_retrieval_attack.py +++ b/tests/test_slow_retrieval_attack.py @@ -39,7 +39,6 @@ import os import sys -import urllib import tempfile import random import time @@ -55,6 +54,7 @@ import tuf.client.updater as updater import tuf.unittest_toolbox as unittest_toolbox import tuf.repository_tool as repo_tool +import tuf._vendor.six as six logger = logging.getLogger('tuf.test_slow_retrieval_attack') repo_tool.disable_console_log_messages() @@ -229,7 +229,7 @@ def test_without_tuf_mode_1(self): try: server_process = self._start_slow_server('mode_1') - urllib.urlretrieve(url_file, client_filepath) + six.moves.urllib.request.urlretrieve(url_file, client_filepath) # Verify the expected file size and hash of the downloaded file. length, hashes = tuf.util.get_file_details(client_filepath) @@ -260,7 +260,7 @@ def test_without_tuf_mode_2(self): try: server_process = self._start_slow_server('mode_2') - urllib.urlretrieve(url_file, client_filepath) + six.moves.urllib.request.urlretrieve(url_file, client_filepath) # Verify the expected file size and hash of the downloaded file. length, hashes = tuf.util.get_file_details(client_filepath) @@ -289,8 +289,8 @@ def test_with_tuf_mode_1(self): # Verify that the specific 'tuf.SlowRetrievalError' exception is raised by # each mirror. - except tuf.NoWorkingMirrorError, exception: - for mirror_url, mirror_error in exception.mirror_errors.iteritems(): + except tuf.NoWorkingMirrorError as exception: + for mirror_url, mirror_error in six.iteritems(exception.mirror_errors): url_prefix = self.repository_mirrors['mirror1']['url_prefix'] url_file = os.path.join(url_prefix, 'targets', 'file1.txt') @@ -322,8 +322,8 @@ def test_with_tuf_mode_2(self): # each mirror. 'file1.txt' should be large enough to trigger a slow # retrieval attack, otherwise the expected exception may not be consistently # raised. - except tuf.NoWorkingMirrorError, exception: - for mirror_url, mirror_error in exception.mirror_errors.iteritems(): + except tuf.NoWorkingMirrorError as exception: + for mirror_url, mirror_error in six.iteritems(exception.mirror_errors): url_prefix = self.repository_mirrors['mirror1']['url_prefix'] url_file = os.path.join(url_prefix, 'targets', 'file1.txt') diff --git a/tests/test_updater.py b/tests/test_updater.py index 9397177f..a3e4d91d 100755 --- a/tests/test_updater.py +++ b/tests/test_updater.py @@ -60,6 +60,7 @@ import tuf.repository_tool as repo_tool import tuf.unittest_toolbox as unittest_toolbox import tuf.client.updater as updater +import tuf._vendor.six as six logger = logging.getLogger('tuf.test_updater') repo_tool.disable_console_log_messages() @@ -337,7 +338,7 @@ def test_1__update_fileinfo(self): root_filepath = os.path.join(self.client_metadata_current, 'root.json') length, hashes = tuf.util.get_file_details(root_filepath) root_fileinfo = tuf.formats.make_fileinfo(length, hashes) - self.assertTrue('root.json' in fileinfo_dict.keys()) + self.assertTrue('root.json' in fileinfo_dict) self.assertEqual(fileinfo_dict['root.json'], root_fileinfo) # Verify that 'self.fileinfo' is incremented if another role is updated. @@ -553,8 +554,8 @@ def test_3__update_metadata(self): self.repository_updater._update_metadata('targets', targets_compressed_fileinfo) - except tuf.NoWorkingMirrorError, e: - for mirror_error in e.mirror_errors.values(): + except tuf.NoWorkingMirrorError as e: + for mirror_error in six.itervalues(e.mirror_errors): assert isinstance(mirror_error, tuf.BadHashError) # Invalid fileinfo for the compressed version of 'targets.json' @@ -571,8 +572,8 @@ def test_3__update_metadata(self): targets_compressed_fileinfo, 'gzip', targets_fileinfo) - except tuf.NoWorkingMirrorError, e: - for mirror_error in e.mirror_errors.values(): + except tuf.NoWorkingMirrorError as e: + for mirror_error in six.itervalues(e.mirror_errors): assert isinstance(mirror_error, tuf.DownloadLengthMismatchError) @@ -647,7 +648,7 @@ def test_3__targets_of_role(self): # target files. self.assertTrue(tuf.formats.TARGETFILES_SCHEMA.matches(targets_list)) for target in targets_list: - self.assertTrue((target['filepath'], target['fileinfo']) in targets_in_metadata.items()) + self.assertTrue((target['filepath'], target['fileinfo']) in six.iteritems(targets_in_metadata)) @@ -674,7 +675,7 @@ def test_4_refresh(self): # Reference 'self.Repository.metadata['current']['targets']'. Ensure # 'target3' is not already specified. targets_metadata = self.repository_updater.metadata['current']['targets'] - self.assertFalse(target3 in targets_metadata['targets'].keys()) + self.assertFalse(target3 in targets_metadata['targets']) # Verify the expected version numbers of the roles to be modified. self.assertTrue(self.repository_updater.metadata['current']['targets']\ @@ -693,7 +694,7 @@ def test_4_refresh(self): targets_metadata = self.repository_updater.metadata['current']['targets'] targets_directory = os.path.join(self.repository_directory, 'targets') target3 = target3[len(targets_directory):] - self.assertTrue(target3 in targets_metadata['targets'].keys()) + self.assertTrue(target3 in targets_metadata['targets']) # Verify the expected version numbers of the updated roles. self.assertTrue(self.repository_updater.metadata['current']['targets']\ @@ -788,7 +789,7 @@ def test_5_targets_of_role(self): # target files. self.assertTrue(tuf.formats.TARGETFILES_SCHEMA.matches(targets_list)) for target in targets_list: - self.assertTrue((target['filepath'], target['fileinfo']) in expected_targets.items()) + self.assertTrue((target['filepath'], target['fileinfo']) in six.iteritems(expected_targets)) # Test: Invalid arguments. @@ -833,7 +834,7 @@ def test_6_download_target(self): # that will be passed as an argument to 'download_target()'. destination_directory = self.make_temp_directory() target_filepaths = \ - self.repository_updater.metadata['current']['targets']['targets'].keys() + list(self.repository_updater.metadata['current']['targets']['targets'].keys()) # Test: normal case. @@ -865,14 +866,14 @@ def test_6_download_target(self): # field contains at least one confined target and excludes needed target # file. mirrors = self.repository_updater.mirrors - for mirror_name, mirror_info in mirrors.items(): + for mirror_name, mirror_info in six.iteritems(mirrors): mirrors[mirror_name]['confined_target_dirs'] = [self.random_path()] try: self.repository_updater.download_target(target_fileinfo, destination_directory) - except tuf.NoWorkingMirrorError, exception: + except tuf.NoWorkingMirrorError as exception: # Ensure that no mirrors were found due to mismatch in confined target # directories. get_list_of_mirrors() returns an empty list in this case, # which does not generate specific exception errors. diff --git a/tests/test_util.py b/tests/test_util.py index 17a26262..931ba9b4 100755 --- a/tests/test_util.py +++ b/tests/test_util.py @@ -16,6 +16,7 @@ Unit test for 'util.py' """ + from __future__ import absolute_import import os @@ -31,6 +32,7 @@ import tuf.hash import tuf.util as util import tuf.unittest_toolbox as unittest_toolbox +import tuf._vendor.six as six logger = logging.getLogger('tuf.test_util') @@ -143,8 +145,11 @@ def test_A5_tempfile_move(self): def _compress_existing_file(self, filepath): - """[Helper]Compresses file 'filepath' and returns file path of - the compresses file.""" + """ + [Helper]Compresses file 'filepath' and returns file path of + the compresses file. + """ + # NOTE: DO NOT forget to remove the newly created compressed file! if os.path.exists(filepath): compressed_filepath = filepath+'.gz' @@ -154,8 +159,9 @@ def _compress_existing_file(self, filepath): f_out.close() f_in.close() return compressed_filepath + else: - print 'Compression of '+repr(filepath)+' failed. Path does not exist.' + logger.error('Compression of '+repr(filepath)+' failed. Path does not exist.') sys.exit(1) @@ -167,9 +173,10 @@ def _decompress_file(self, compressed_filepath): file_content = f.read() f.close() return file_content + else: - print 'Decompression of '+repr(compressed_filepath)+' failed. '+\ - 'Path does not exist.' + logger.error('Decompression of '+repr(compressed_filepath)+' failed. '+\ + 'Path does not exist.') sys.exit(1) @@ -223,8 +230,9 @@ def test_B1_get_file_details(self): # Test: Incorrect input. bogus_inputs = [self.random_string(), 1234, [self.random_string()], {'a': 'b'}, None] + for bogus_input in bogus_inputs: - if isinstance(bogus_input, basestring): + if isinstance(bogus_input, six.string_types): self.assertRaises(tuf.Error, util.get_file_details, bogus_input) else: self.assertRaises(tuf.FormatError, util.get_file_details, bogus_input) @@ -236,7 +244,7 @@ def test_B2_ensure_parent_dir(self): non_existing_parent_dir = os.path.join(existing_parent_dir, 'a', 'b') for parent_dir in [existing_parent_dir, non_existing_parent_dir, 12, [3]]: - if isinstance(parent_dir, basestring): + if isinstance(parent_dir, six.string_types): util.ensure_parent_dir(os.path.join(parent_dir, 'a.txt')) self.assertTrue(os.path.isdir(parent_dir)) else: @@ -313,7 +321,7 @@ def test_C1_get_target_hash(self): '/README.txt': '8faee106f1bb69f34aaf1df1e3c2e87d763c4d878cb96b91db13495e32ceb0b0', '/warehouse/file2.txt': 'd543a573a2cec67026eff06e75702303559e64e705eba06f65799baaf0424417' } - for filepath, target_hash in expected_target_hashes.items(): + for filepath, target_hash in six.iteritems(expected_target_hashes): self.assertTrue(tuf.formats.RELPATH_SCHEMA.matches(filepath)) self.assertTrue(tuf.formats.HASH_SCHEMA.matches(target_hash)) self.assertEqual(util.get_target_hash(filepath), target_hash) From 5d3664e5a4c3383706f048c3c55382d58a4afaab Mon Sep 17 00:00:00 2001 From: vladdd Date: Mon, 28 Apr 2014 23:21:16 -0400 Subject: [PATCH 03/32] [WIP] Python 2+3 support. maxint and minor additions. --- tests/test_hash.py | 15 +++++++++++---- tests/test_keydb.py | 9 ++++++++- tuf/schema.py | 2 +- 3 files changed, 20 insertions(+), 6 deletions(-) diff --git a/tests/test_hash.py b/tests/test_hash.py index c38a4b2b..f55f7f45 100755 --- a/tests/test_hash.py +++ b/tests/test_hash.py @@ -13,12 +13,18 @@ See LICENSE for licensing information. - Unit tests for hash.py. - + Unit test for hash.py. """ +# Help with Python 3 compatibility, where the print statement is a function, an +# implicit relative import is invalid, and the '/' operator performs true +# division. Example: print 'hello world' raises a 'SyntaxError' exception. +from __future__ import print_function +from __future__ import absolute_import +from __future__ import division +from __future__ import unicode_literals + import os -import StringIO import logging import tempfile import unittest @@ -26,6 +32,7 @@ import tuf import tuf.log import tuf.hash +import tuf._vendor.six as six logger = logging.getLogger('tuf.test_hash') @@ -215,7 +222,7 @@ def test_update_file_obj(self): def _do_update_file_obj(self, library): data = 'abcdefgh' * 4096 - file_obj = StringIO.StringIO() + file_obj = six.StringIO() file_obj.write(data) for algorithm in ['md5', 'sha1', 'sha224', 'sha256', 'sha384', 'sha512']: digest_object_truth = tuf.hash.digest(algorithm, library) diff --git a/tests/test_keydb.py b/tests/test_keydb.py index df405b63..3c40772a 100755 --- a/tests/test_keydb.py +++ b/tests/test_keydb.py @@ -150,6 +150,9 @@ def test_remove_key(self): self.assertRaises(tuf.UnknownKeyError, tuf.keydb.get_key, keyid) self.assertRaises(tuf.UnknownKeyError, tuf.keydb.get_key, keyid2) + # Test for 'keyid' not in keydb. + self.assertRaises(tuf.UnknownKeyError, tuf.keydb.remove_key, keyid) + # Test conditions for arguments with invalid formats. self.assertRaises(tuf.FormatError, tuf.keydb.remove_key, None) self.assertRaises(tuf.FormatError, tuf.keydb.remove_key, '') @@ -166,7 +169,10 @@ def test_create_keydb_from_root_metadata(self): keyid = KEYS[0]['keyid'] rsakey2 = KEYS[1] keyid2 = KEYS[1]['keyid'] - keydict = {keyid: rsakey, keyid2: rsakey2} + keydict = {keyid: rsakey, keyid2: rsakey2, keyid: rsakey} + + # Add a duplicate 'keyid' to log/trigger a 'tuf.KeyAlreadyExistsError' + # block (loading continues). roledict = {'Root': {'keyids': [keyid], 'threshold': 1}, 'Targets': {'keyids': [keyid2], 'threshold': 1}} version = 8 @@ -178,6 +184,7 @@ def test_create_keydb_from_root_metadata(self): keydict, roledict, consistent_snapshot) self.assertEqual(None, tuf.keydb.create_keydb_from_root_metadata(root_metadata)) + # Ensure 'keyid' and 'keyid2' were added to the keydb database. self.assertEqual(rsakey, tuf.keydb.get_key(keyid)) self.assertEqual(rsakey2, tuf.keydb.get_key(keyid2)) diff --git a/tuf/schema.py b/tuf/schema.py index b976a379..e13c5ace 100755 --- a/tuf/schema.py +++ b/tuf/schema.py @@ -475,7 +475,7 @@ class Integer(Schema): False """ - def __init__(self, lo = -sys.maxint, hi = sys.maxint): + def __init__(self, lo = -2147483648, hi = 2147483647): """ Create a new Integer schema. From ab95a4b3aad6c93222e7473e77eae04b00b00bfb Mon Sep 17 00:00:00 2001 From: Vladimir Diaz Date: Tue, 29 Apr 2014 14:27:34 -0400 Subject: [PATCH 04/32] [WIP] Python 2+3 support. Python 2+3 unicode. libraries. The following modules (and their tests) work in PY2.7+3.3: keydb, hash, formats, mirrors --- examples/client/example_client.py | 2 +- tests/aggregate_tests.py | 14 ++++- tests/simple_server.py | 8 +++ tests/slow_retrieval_server.py | 8 +++ tests/test_arbitrary_package_attack.py | 3 +- tests/test_download.py | 18 ++++-- tests/test_ed25519_keys.py | 10 +++- tests/test_endless_data_attack.py | 3 +- tests/test_extraneous_dependencies_attack.py | 3 +- tests/test_formats.py | 14 ++++- tests/test_hash.py | 49 +++++++-------- tests/test_indefinite_freeze_attack.py | 3 +- tests/test_keydb.py | 8 +++ tests/test_keys.py | 8 +++ tests/test_mirrors.py | 16 +++-- tests/test_mix_and_match_attack.py | 3 +- tests/test_pycrypto_keys.py | 10 +++- tests/test_replay_attack.py | 3 +- tests/test_repository_tool.py | 8 +++ tests/test_roledb.py | 7 +++ tests/test_schema.py | 13 +++- tests/test_sig.py | 8 +++ tests/test_slow_retrieval_attack.py | 3 +- tests/test_updater.py | 6 ++ tests/test_util.py | 28 +++++---- tuf/__init__.py | 8 +++ tuf/client/basic_client.py | 8 +++ tuf/client/updater.py | 16 +++-- tuf/conf.py | 8 +++ tuf/download.py | 37 +++++++----- tuf/ed25519_keys.py | 1 + tuf/formats.py | 18 ++++-- tuf/hash.py | 63 +++++++------------- tuf/keydb.py | 12 +++- tuf/keys.py | 14 ++++- tuf/log.py | 11 +++- tuf/mirrors.py | 16 +++-- tuf/pycrypto_keys.py | 8 +++ tuf/repository_tool.py | 11 ++-- tuf/roledb.py | 8 +++ tuf/schema.py | 7 +++ tuf/sig.py | 8 +++ tuf/unittest_toolbox.py | 8 +++ tuf/util.py | 14 ++++- 44 files changed, 378 insertions(+), 154 deletions(-) diff --git a/examples/client/example_client.py b/examples/client/example_client.py index 53b18142..21fb9922 100755 --- a/examples/client/example_client.py +++ b/examples/client/example_client.py @@ -21,7 +21,7 @@ The custom examples below demonstrate: (1) updating all targets (2) updating all the targets of a specified role - (3) updating a specific target explicitely named. + (3) updating a specific target explicitly named. It assumes a server is listening on 'http://localhost:8001'. One can be started by navigating to the 'examples/repository/' and starting: diff --git a/tests/aggregate_tests.py b/tests/aggregate_tests.py index 5394498a..26edb06d 100755 --- a/tests/aggregate_tests.py +++ b/tests/aggregate_tests.py @@ -23,6 +23,14 @@ 'tuf/tests'. Use --random to run the tests in random order. """ +# Help with Python 3 compatibility, where the print statement is a function, an +# implicit relative import is invalid, and the '/' operator performs true +# division. Example: print 'hello world' raises a 'SyntaxError' exception. +from __future__ import print_function +from __future__ import absolute_import +from __future__ import division +from __future__ import unicode_literals + import sys import unittest import glob @@ -42,9 +50,9 @@ test = test[:-3] tests_without_extension.append(test) -# Provide command-line option to randomize the order in which the tests run. -# Randomization might catch errors with unit tests that do not properly clean -# up or restore monkey-patched modules. +# Randomize the order in which the tests run. Randomization might catch errors +# with unit tests that do not properly clean up or restore monkey-patched +# modules. random.shuffle(tests_without_extension) diff --git a/tests/simple_server.py b/tests/simple_server.py index 2a8c5429..05fd59fc 100755 --- a/tests/simple_server.py +++ b/tests/simple_server.py @@ -20,6 +20,14 @@ http://docs.python.org/library/simplehttpserver.html#module-SimpleHTTPServer """ +# Help with Python 3 compatibility, where the print statement is a function, an +# implicit relative import is invalid, and the '/' operator performs true +# division. Example: print 'hello world' raises a 'SyntaxError' exception. +from __future__ import print_function +from __future__ import absolute_import +from __future__ import division +from __future__ import unicode_literals + import sys import random diff --git a/tests/slow_retrieval_server.py b/tests/slow_retrieval_server.py index 079f4061..1d8fbb69 100755 --- a/tests/slow_retrieval_server.py +++ b/tests/slow_retrieval_server.py @@ -18,6 +18,14 @@ interval 'DELAY'). The server is used in 'test_slow_retrieval_attack.py'. """ +# Help with Python 3 compatibility, where the print statement is a function, an +# implicit relative import is invalid, and the '/' operator performs true +# division. Example: print 'hello world' raises a 'SyntaxError' exception. +from __future__ import print_function +from __future__ import absolute_import +from __future__ import division +from __future__ import unicode_literals + import os import sys import time diff --git a/tests/test_arbitrary_package_attack.py b/tests/test_arbitrary_package_attack.py index 9601ba85..541e9714 100755 --- a/tests/test_arbitrary_package_attack.py +++ b/tests/test_arbitrary_package_attack.py @@ -25,12 +25,13 @@ There is no difference between 'updates' and 'target' files. """ -# 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 +from __future__ import unicode_literals import os import tempfile diff --git a/tests/test_download.py b/tests/test_download.py index cab55d2c..dd7e5053 100755 --- a/tests/test_download.py +++ b/tests/test_download.py @@ -20,7 +20,13 @@ Otherwise, module that launches simple server would not be found. """ +# Help with Python 3 compatibility, where the print statement is a function, an +# implicit relative import is invalid, and the '/' operator performs true +# division. Example: print 'hello world' raises a 'SyntaxError' exception. +from __future__ import print_function from __future__ import absolute_import +from __future__ import division +from __future__ import unicode_literals import hashlib import logging @@ -92,8 +98,8 @@ def test_download_url_to_tempfileobj(self): download_file = download.safe_download temp_fileobj = download_file(self.url, self.target_data_length) - self.assertEquals(self.target_data, temp_fileobj.read()) - self.assertEquals(self.target_data_length, len(temp_fileobj.read())) + self.assertEqual(self.target_data, temp_fileobj.read()) + self.assertEqual(self.target_data_length, len(temp_fileobj.read())) temp_fileobj.close_temp_file() @@ -119,8 +125,8 @@ def test_download_url_to_tempfileobj_and_lengths(self): # STRICT_REQUIRED_LENGTH. temp_fileobj = download.unsafe_download(self.url, self.target_data_length + 1) - self.assertEquals(self.target_data, temp_fileobj.read()) - self.assertEquals(self.target_data_length, len(temp_fileobj.read())) + self.assertEqual(self.target_data, temp_fileobj.read()) + self.assertEqual(self.target_data_length, len(temp_fileobj.read())) temp_fileobj.close_temp_file() @@ -138,8 +144,8 @@ def test_download_url_to_tempfileobj_and_performance(self): end_cpu = time.clock() end_real = time.time() - self.assertEquals(self.target_data, temp_fileobj.read()) - self.assertEquals(self.target_data_length, len(temp_fileobj.read())) + self.assertEqual(self.target_data, temp_fileobj.read()) + self.assertEqual(self.target_data_length, len(temp_fileobj.read())) temp_fileobj.close_temp_file() print "Performance cpu time: "+str(end_cpu - star_cpu) diff --git a/tests/test_ed25519_keys.py b/tests/test_ed25519_keys.py index 9a922b88..9089a2fd 100755 --- a/tests/test_ed25519_keys.py +++ b/tests/test_ed25519_keys.py @@ -3,7 +3,7 @@ test_ed25519_keys.py - Vladimir Diaz + Vladimir Diaz October 11, 2013. @@ -15,6 +15,14 @@ Test cases for test_ed25519_keys.py. """ +# Help with Python 3 compatibility, where the print statement is a function, an +# implicit relative import is invalid, and the '/' operator performs true +# division. Example: print 'hello world' raises a 'SyntaxError' exception. +from __future__ import print_function +from __future__ import absolute_import +from __future__ import division +from __future__ import unicode_literals + import unittest import logging diff --git a/tests/test_endless_data_attack.py b/tests/test_endless_data_attack.py index 83fbfadf..e98ff626 100755 --- a/tests/test_endless_data_attack.py +++ b/tests/test_endless_data_attack.py @@ -28,12 +28,13 @@ There is no difference between 'updates' and 'target' files. """ -# 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 +from __future__ import unicode_literals import os import tempfile diff --git a/tests/test_extraneous_dependencies_attack.py b/tests/test_extraneous_dependencies_attack.py index 877ef815..c43d4992 100755 --- a/tests/test_extraneous_dependencies_attack.py +++ b/tests/test_extraneous_dependencies_attack.py @@ -30,12 +30,13 @@ There is no difference between 'updates' and 'target' files. """ -# 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 +from __future__ import unicode_literals import os import tempfile diff --git a/tests/test_formats.py b/tests/test_formats.py index 5a3572cd..5b2234d4 100755 --- a/tests/test_formats.py +++ b/tests/test_formats.py @@ -17,6 +17,14 @@ Unit test for 'formats.py' """ +# Help with Python 3 compatibility, where the print statement is a function, an +# implicit relative import is invalid, and the '/' operator performs true +# division. Example: print 'hello world' raises a 'SyntaxError' exception. +from __future__ import print_function +from __future__ import absolute_import +from __future__ import division +from __future__ import unicode_literals + import unittest import datetime @@ -480,7 +488,7 @@ def test_datetime_to_unix_timestamp(self): def test_format_base64(self): # Test conditions for valid arguments. - data = 'updateframework' + data = 'updateframework'.encode('utf-8') self.assertEqual('dXBkYXRlZnJhbWV3b3Jr', tuf.formats.format_base64(data)) self.assertTrue(isinstance(tuf.formats.format_base64(data), six.string_types)) @@ -493,8 +501,8 @@ def test_format_base64(self): def test_parse_base64(self): # Test conditions for valid arguments. base64 = 'dXBkYXRlZnJhbWV3b3Jr' - self.assertEqual('updateframework', tuf.formats.parse_base64(base64)) - self.assertTrue(isinstance(tuf.formats.parse_base64(base64), six.string_types)) + self.assertEqual(b'updateframework', tuf.formats.parse_base64(base64)) + self.assertTrue(isinstance(tuf.formats.parse_base64(base64), six.binary_type)) # Test conditions for invalid arguments. self.assertRaises(tuf.FormatError, tuf.formats.format_base64, 123) diff --git a/tests/test_hash.py b/tests/test_hash.py index f55f7f45..67607991 100755 --- a/tests/test_hash.py +++ b/tests/test_hash.py @@ -59,13 +59,13 @@ def _do_md5_update(self, library): digest_object = tuf.hash.digest('md5', library) self.assertEqual(digest_object.hexdigest(), 'd41d8cd98f00b204e9800998ecf8427e') - digest_object.update('a') + digest_object.update('a'.encode('utf-8')) self.assertEqual(digest_object.hexdigest(), '0cc175b9c0f1b6a831c399e269772661') - digest_object.update(u'bbb') + digest_object.update('bbb'.encode('utf-8')) self.assertEqual(digest_object.hexdigest(), 'f034e93091235adbb5d2781908e2b313') - digest_object.update('') + digest_object.update(''.encode('utf-8')) self.assertEqual(digest_object.hexdigest(), 'f034e93091235adbb5d2781908e2b313') @@ -79,13 +79,13 @@ def _do_sha1_update(self, library): self.assertEqual(digest_object.hexdigest(), 'da39a3ee5e6b4b0d3255bfef95601890afd80709') - digest_object.update('a') + digest_object.update('a'.encode('utf-8')) self.assertEqual(digest_object.hexdigest(), '86f7e437faa5a7fce15d1ddcb9eaeaea377667b8') - digest_object.update(u'bbb') + digest_object.update('bbb'.encode('utf-8')) self.assertEqual(digest_object.hexdigest(), 'd7bfa42fc62b697bf6cf1cda9af1fb7f40a27817') - digest_object.update('') + digest_object.update(''.encode('utf-8')) self.assertEqual(digest_object.hexdigest(), 'd7bfa42fc62b697bf6cf1cda9af1fb7f40a27817') @@ -99,13 +99,13 @@ def _do_sha224_update(self, library): self.assertEqual(digest_object.hexdigest(), 'd14a028c2a3a2bc9476102bb288234c415a2b01f828ea62ac5b3e42f') - digest_object.update('a') + digest_object.update('a'.encode('utf-8')) self.assertEqual(digest_object.hexdigest(), 'abd37534c7d9a2efb9465de931cd7055ffdb8879563ae98078d6d6d5') - digest_object.update(u'bbb') + digest_object.update('bbb'.encode('utf-8')) self.assertEqual(digest_object.hexdigest(), 'ab1342f31c2a6f242d9a3cefb503fb49465c95eb255c16ad791d688c') - digest_object.update('') + digest_object.update(''.encode('utf-8')) self.assertEqual(digest_object.hexdigest(), 'ab1342f31c2a6f242d9a3cefb503fb49465c95eb255c16ad791d688c') @@ -118,13 +118,13 @@ def _do_sha256_update(self, library): digest_object = tuf.hash.digest('sha256', library) self.assertEqual(digest_object.hexdigest(), 'e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855') - digest_object.update('a') + digest_object.update('a'.encode('utf-8')) self.assertEqual(digest_object.hexdigest(), 'ca978112ca1bbdcafac231b39a23dc4da786eff8147c4e72b9807785afee48bb') - digest_object.update(u'bbb') + digest_object.update('bbb'.encode('utf-8')) self.assertEqual(digest_object.hexdigest(), '01d162a5c95d4698c0a3e766ae80d85994b549b877ed275803725f43dadc83bd') - digest_object.update('') + digest_object.update(''.encode('utf-8')) self.assertEqual(digest_object.hexdigest(), '01d162a5c95d4698c0a3e766ae80d85994b549b877ed275803725f43dadc83bd') @@ -138,15 +138,15 @@ def _do_sha384_update(self, library): self.assertEqual(digest_object.hexdigest(), '38b060a751ac96384cd9327eb1b1e36a21fdb71114be07434c0cc7bf63f6e1da274edebfe' '76f65fbd51ad2f14898b95b') - digest_object.update('a') + digest_object.update('a'.encode('utf-8')) self.assertEqual(digest_object.hexdigest(), '54a59b9f22b0b80880d8427e548b7c23abd873486e1f035dce9cd697e85175033caa88e6d' '57bc35efae0b5afd3145f31') - digest_object.update(u'bbb') + digest_object.update('bbb'.encode('utf-8')) self.assertEqual(digest_object.hexdigest(), 'f2c1438e9cc1d24bebbf3b88e60adc169db0c5c459d02054ec131438bf20ebee5ca88c17c' 'b5f1a824fcccf8d2b20b0a9') - digest_object.update('') + digest_object.update(''.encode('utf-8')) self.assertEqual(digest_object.hexdigest(), 'f2c1438e9cc1d24bebbf3b88e60adc169db0c5c459d02054ec131438bf20ebee5ca88c17c' 'b5f1a824fcccf8d2b20b0a9') @@ -162,15 +162,15 @@ def _do_sha512_update(self, library): self.assertEqual(digest_object.hexdigest(), 'cf83e1357eefb8bdf1542850d66d8007d620e4050b5715dc83f4a921d36ce9ce47d0d13c5' 'd85f2b0ff8318d2877eec2f63b931bd47417a81a538327af927da3e') - digest_object.update('a') + digest_object.update('a'.encode('utf-8')) self.assertEqual(digest_object.hexdigest(), '1f40fc92da241694750979ee6cf582f2d5d7d28e18335de05abc54d0560e0f5302860c652' 'bf08d560252aa5e74210546f369fbbbce8c12cfc7957b2652fe9a75') - digest_object.update(u'bbb') + digest_object.update('bbb'.encode('utf-8')) self.assertEqual(digest_object.hexdigest(), '09ade82ae3c5d54f8375f348563a372106488adef16a74b63b5591849f740bff55ceab22e' '117b4b09349b860f8a644adb32a9ea542abdecb80bf625160604251') - digest_object.update('') + digest_object.update(''.encode('utf-8')) self.assertEqual(digest_object.hexdigest(), '09ade82ae3c5d54f8375f348563a372106488adef16a74b63b5591849f740bff55ceab22e' '117b4b09349b860f8a644adb32a9ea542abdecb80bf625160604251') @@ -205,13 +205,14 @@ def _do_update_filename(self, library): data = 'abcdefgh' * 4096 fd, filename = tempfile.mkstemp() try: - os.write(fd, data) + os.write(fd, data.encode('utf-8')) os.close(fd) for algorithm in ['md5', 'sha1', 'sha224', 'sha256', 'sha384', 'sha512']: digest_object_truth = tuf.hash.digest(algorithm, library) - digest_object_truth.update(data) + digest_object_truth.update(data.encode('utf-8')) digest_object = tuf.hash.digest_filename(filename, algorithm, library) self.assertEqual(digest_object_truth.digest(), digest_object.digest()) + finally: os.remove(filename) @@ -226,18 +227,12 @@ def _do_update_file_obj(self, library): file_obj.write(data) for algorithm in ['md5', 'sha1', 'sha224', 'sha256', 'sha384', 'sha512']: digest_object_truth = tuf.hash.digest(algorithm, library) - digest_object_truth.update(data) + digest_object_truth.update(data.encode('utf-8')) digest_object = tuf.hash.digest_fileobject(file_obj, algorithm, library) # Note: we don't seek because the update_file_obj call is supposed # to always seek to the beginning. self.assertEqual(digest_object_truth.digest(), digest_object.digest()) - - def test_data_to_string(self): - self.assertEqual('12', tuf.hash.data_to_string('12')) - self.assertEqual(u'hello', tuf.hash.data_to_string(unicode('hello'))) - self.assertEqual('12', tuf.hash.data_to_string(12)) - def test_unsupported_digest_algorithm_and_library(self): self.assertRaises(tuf.UnsupportedAlgorithmError, tuf.hash.digest, diff --git a/tests/test_indefinite_freeze_attack.py b/tests/test_indefinite_freeze_attack.py index 1dbda04a..d7181a76 100755 --- a/tests/test_indefinite_freeze_attack.py +++ b/tests/test_indefinite_freeze_attack.py @@ -22,12 +22,13 @@ metadata without the client being aware. """ -# 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 +from __future__ import unicode_literals import os import random diff --git a/tests/test_keydb.py b/tests/test_keydb.py index 3c40772a..cb031dbb 100755 --- a/tests/test_keydb.py +++ b/tests/test_keydb.py @@ -15,6 +15,14 @@ Unit test for 'keydb.py'. """ +# Help with Python 3 compatibility, where the print statement is a function, an +# implicit relative import is invalid, and the '/' operator performs true +# division. Example: print 'hello world' raises a 'SyntaxError' exception. +from __future__ import print_function +from __future__ import absolute_import +from __future__ import division +from __future__ import unicode_literals + import unittest import logging diff --git a/tests/test_keys.py b/tests/test_keys.py index c1a2aad2..5c800d77 100755 --- a/tests/test_keys.py +++ b/tests/test_keys.py @@ -16,6 +16,14 @@ TODO: test case for ed25519 key generation and refactor. """ +# Help with Python 3 compatibility, where the print statement is a function, an +# implicit relative import is invalid, and the '/' operator performs true +# division. Example: print 'hello world' raises a 'SyntaxError' exception. +from __future__ import print_function +from __future__ import absolute_import +from __future__ import division +from __future__ import unicode_literals + import unittest import logging diff --git a/tests/test_mirrors.py b/tests/test_mirrors.py index 6cc68f79..34ee502c 100755 --- a/tests/test_mirrors.py +++ b/tests/test_mirrors.py @@ -3,10 +3,10 @@ test_mirrors.py - Konstantin Andrianov + Konstantin Andrianov. - March 26, 2012 + March 26, 2012. See LICENSE for licensing information. @@ -15,7 +15,13 @@ Unit test for 'mirrors.py'. """ +# Help with Python 3 compatibility, where the print statement is a function, an +# implicit relative import is invalid, and the '/' operator performs true +# division. Example: print 'hello world' raises a 'SyntaxError' exception. +from __future__ import print_function from __future__ import absolute_import +from __future__ import division +from __future__ import unicode_literals import unittest @@ -53,18 +59,18 @@ def setUp(self): def test_get_list_of_mirrors(self): # Test: Normal case. mirror_list = mirrors.get_list_of_mirrors('meta', 'release.txt', self.mirrors) - self.assertEquals(len(mirror_list), 3) + self.assertEqual(len(mirror_list), 3) for mirror, mirror_info in six.iteritems(self.mirrors): url = mirror_info['url_prefix']+'/metadata/release.txt' self.assertTrue(url in mirror_list) mirror_list = mirrors.get_list_of_mirrors('target', 'a.txt', self.mirrors) - self.assertEquals(len(mirror_list), 1) + self.assertEqual(len(mirror_list), 1) self.assertTrue(self.mirrors['mirror1']['url_prefix']+'/targets/a.txt' in \ mirror_list) mirror_list = mirrors.get_list_of_mirrors('target', 'a/b', self.mirrors) - self.assertEquals(len(mirror_list), 1) + self.assertEqual(len(mirror_list), 1) self.assertTrue(self.mirrors['mirror1']['url_prefix']+'/targets/a/b' in \ mirror_list) diff --git a/tests/test_mix_and_match_attack.py b/tests/test_mix_and_match_attack.py index f9e53883..02ca21fb 100755 --- a/tests/test_mix_and_match_attack.py +++ b/tests/test_mix_and_match_attack.py @@ -27,12 +27,13 @@ Note: There is no difference between 'updates' and 'target' files. """ -# 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 +from __future__ import unicode_literals import os import tempfile diff --git a/tests/test_pycrypto_keys.py b/tests/test_pycrypto_keys.py index f6adf7d0..1dee349a 100755 --- a/tests/test_pycrypto_keys.py +++ b/tests/test_pycrypto_keys.py @@ -3,7 +3,7 @@ test_pycrypto_keys.py - Vladimir Diaz + Vladimir Diaz October 10, 2013. @@ -15,6 +15,14 @@ Test cases for test_pycrypto_keys.py. """ +# Help with Python 3 compatibility, where the print statement is a function, an +# implicit relative import is invalid, and the '/' operator performs true +# division. Example: print 'hello world' raises a 'SyntaxError' exception. +from __future__ import print_function +from __future__ import absolute_import +from __future__ import division +from __future__ import unicode_literals + import unittest import logging diff --git a/tests/test_replay_attack.py b/tests/test_replay_attack.py index 0621e760..03e0a66d 100755 --- a/tests/test_replay_attack.py +++ b/tests/test_replay_attack.py @@ -27,12 +27,13 @@ Note: There is no difference between 'updates' and 'target' files. """ -# 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 +from __future__ import unicode_literals import os import tempfile diff --git a/tests/test_repository_tool.py b/tests/test_repository_tool.py index a45dcf8d..991a09c1 100755 --- a/tests/test_repository_tool.py +++ b/tests/test_repository_tool.py @@ -15,6 +15,14 @@ Unit test for 'repository_tool.py'. """ +# Help with Python 3 compatibility, where the print statement is a function, an +# implicit relative import is invalid, and the '/' operator performs true +# division. Example: print 'hello world' raises a 'SyntaxError' exception. +from __future__ import print_function +from __future__ import absolute_import +from __future__ import division +from __future__ import unicode_literals + import os import time import datetime diff --git a/tests/test_roledb.py b/tests/test_roledb.py index eb34e43f..774b8991 100755 --- a/tests/test_roledb.py +++ b/tests/test_roledb.py @@ -15,6 +15,13 @@ Unit test for 'roledb.py'. """ +# Help with Python 3 compatibility, where the print statement is a function, an +# implicit relative import is invalid, and the '/' operator performs true +# division. Example: print 'hello world' raises a 'SyntaxError' exception. +from __future__ import print_function +from __future__ import absolute_import +from __future__ import division +from __future__ import unicode_literals import unittest import logging diff --git a/tests/test_schema.py b/tests/test_schema.py index 66320a01..c5203d12 100755 --- a/tests/test_schema.py +++ b/tests/test_schema.py @@ -13,9 +13,16 @@ Unit test for 'schema.py' - """ +# Help with Python 3 compatibility, where the print statement is a function, an +# implicit relative import is invalid, and the '/' operator performs true +# division. Example: print 'hello world' raises a 'SyntaxError' exception. +from __future__ import print_function +from __future__ import absolute_import +from __future__ import division +from __future__ import unicode_literals + import unittest import logging @@ -203,7 +210,9 @@ def test_Integer(self): integer_schema = tuf.schema.Integer() self.assertTrue(integer_schema.matches(99)) - self.assertTrue(integer_schema.matches(0L)) + if six.PY3: + print('reached long(1)') + self.assertTrue(integer_schema.matches(long(1))) self.assertTrue(tuf.schema.Integer(lo=10, hi=30).matches(25)) # Test conditions for invalid arguments. diff --git a/tests/test_sig.py b/tests/test_sig.py index b8205a07..cb99bc4b 100755 --- a/tests/test_sig.py +++ b/tests/test_sig.py @@ -16,6 +16,14 @@ Test cases for for sig.py. """ +# Help with Python 3 compatibility, where the print statement is a function, an +# implicit relative import is invalid, and the '/' operator performs true +# division. Example: print 'hello world' raises a 'SyntaxError' exception. +from __future__ import print_function +from __future__ import absolute_import +from __future__ import division +from __future__ import unicode_literals + import unittest import logging diff --git a/tests/test_slow_retrieval_attack.py b/tests/test_slow_retrieval_attack.py index bf59ea5b..7b0b4a77 100755 --- a/tests/test_slow_retrieval_attack.py +++ b/tests/test_slow_retrieval_attack.py @@ -30,12 +30,13 @@ Note: There is no difference between 'updates' and 'target' files. """ -# 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 +from __future__ import unicode_literals import os import sys diff --git a/tests/test_updater.py b/tests/test_updater.py index ba5093cb..54be7900 100755 --- a/tests/test_updater.py +++ b/tests/test_updater.py @@ -38,7 +38,13 @@ less dependent than 2. """ +# Help with Python 3 compatibility, where the print statement is a function, an +# implicit relative import is invalid, and the '/' operator performs true +# division. Example: print 'hello world' raises a 'SyntaxError' exception. +from __future__ import print_function from __future__ import absolute_import +from __future__ import division +from __future__ import unicode_literals import os import time diff --git a/tests/test_util.py b/tests/test_util.py index 931ba9b4..608556a1 100755 --- a/tests/test_util.py +++ b/tests/test_util.py @@ -8,7 +8,7 @@ Konstantin Andrianov. - February 1, 2013 + February 1, 2013. See LICENSE for licensing information. @@ -17,7 +17,13 @@ Unit test for 'util.py' """ +# Help with Python 3 compatibility, where the print statement is a function, an +# implicit relative import is invalid, and the '/' operator performs true +# division. Example: print 'hello world' raises a 'SyntaxError' exception. +from __future__ import print_function from __future__ import absolute_import +from __future__ import division +from __future__ import unicode_literals import os import sys @@ -99,14 +105,14 @@ def test_A2_tempfile_init(self): for config_temp_dir in config_temp_dirs: config_temp_dir, actual_dir = \ self._extract_tempfile_directory(config_temp_dir) - self.assertEquals(config_temp_dir, actual_dir) + self.assertEqual(config_temp_dir, actual_dir) # Test: Unexpected input handling. config_temp_dirs = [self.random_string(), 123, ['a'], {'a':1}] for config_temp_dir in config_temp_dirs: config_temp_dir, actual_dir = \ self._extract_tempfile_directory(config_temp_dir) - self.assertEquals(tempfile.gettempdir(), actual_dir) + self.assertEqual(tempfile.gettempdir(), actual_dir) @@ -118,8 +124,8 @@ def test_A3_tempfile_read(self): self.temp_fileobj.temporary_file = fileobj # Test: Expected input. - self.assertEquals(self.temp_fileobj.read(), '1234567890') - self.assertEquals(self.temp_fileobj.read(4), '1234') + self.assertEqual(self.temp_fileobj.read(), '1234567890') + self.assertEqual(self.temp_fileobj.read(4), '1234') # Test: Unexpected input. for bogus_arg in ['abcd', ['abcd'], {'a':'a'}, -100]: @@ -130,7 +136,7 @@ def test_A3_tempfile_read(self): def test_A4_tempfile_write(self): data = self.random_string() self.temp_fileobj.write(data) - self.assertEquals(data, self.temp_fileobj.read()) + self.assertEqual(data, self.temp_fileobj.read()) @@ -198,14 +204,14 @@ def test_A6_tempfile_decompress_temp_file_object(self): self.assertRaises(tuf.Error, self.temp_fileobj.decompress_temp_file_object, arg) self.temp_fileobj.decompress_temp_file_object('gzip') - self.assertEquals(self.temp_fileobj.read(), fileobj.read()) + self.assertEqual(self.temp_fileobj.read(), fileobj.read()) # Checking the content of the TempFile's '_orig_file' instance. _orig_data_file = \ self.make_temp_data_file(data=self.temp_fileobj._orig_file.read()) data_in_orig_file = self._decompress_file(_orig_data_file) fileobj.seek(0) - self.assertEquals(data_in_orig_file, fileobj.read()) + self.assertEqual(data_in_orig_file, fileobj.read()) # Try decompressing once more. self.assertRaises(tuf.Error, @@ -225,7 +231,7 @@ def test_B1_get_file_details(self): file_length = os.path.getsize(filepath) # Test: Expected input. - self.assertEquals(util.get_file_details(filepath), (file_length, file_hash)) + self.assertEqual(util.get_file_details(filepath), (file_length, file_hash)) # Test: Incorrect input. bogus_inputs = [self.random_string(), 1234, [self.random_string()], @@ -292,7 +298,7 @@ def test_B5_load_json_string(self): # Test normal case. data = ['a', {'b': ['c', None, 30.3, 29]}] json_string = util.json.dumps(data) - self.assertEquals(data, util.load_json_string(json_string)) + self.assertEqual(data, util.load_json_string(json_string)) # Test invalid arguments. self.assertRaises(tuf.Error, util.load_json_string, 8) @@ -307,7 +313,7 @@ def test_B6_load_json_file(self): fileobj = open(filepath, 'wb') util.json.dump(data, fileobj) fileobj.close() - self.assertEquals(data, util.load_json_file(filepath)) + self.assertEqual(data, util.load_json_file(filepath)) Errors = (tuf.FormatError, IOError) for bogus_arg in ['a', 1, ['a'], {'a':'b'}]: self.assertRaises(Errors, util.load_json_file, bogus_arg) diff --git a/tuf/__init__.py b/tuf/__init__.py index 3437ab4b..7c37e7f8 100755 --- a/tuf/__init__.py +++ b/tuf/__init__.py @@ -20,6 +20,14 @@ provide that reason in those cases. """ +# Help with Python 3 compatibility, where the print statement is a function, an +# implicit relative import is invalid, and the '/' operator performs true +# division. Example: print 'hello world' raises a 'SyntaxError' exception. +from __future__ import print_function +from __future__ import absolute_import +from __future__ import division +from __future__ import unicode_literals + import logging import tuf.log diff --git a/tuf/client/basic_client.py b/tuf/client/basic_client.py index 00ee03a0..0fa51d25 100755 --- a/tuf/client/basic_client.py +++ b/tuf/client/basic_client.py @@ -51,6 +51,14 @@ """ +# Help with Python 3 compatibility, where the print statement is a function, an +# implicit relative import is invalid, and the '/' operator performs true +# division. Example: print 'hello world' raises a 'SyntaxError' exception. +from __future__ import print_function +from __future__ import absolute_import +from __future__ import division +from __future__ import unicode_literals + import sys import optparse import logging diff --git a/tuf/client/updater.py b/tuf/client/updater.py index 4e581dbe..d2928468 100755 --- a/tuf/client/updater.py +++ b/tuf/client/updater.py @@ -99,6 +99,14 @@ updater.download_target(target, destination_directory) """ +# Help with Python 3 compatibility, where the print statement is a function, an +# implicit relative import is invalid, and the '/' operator performs true +# division. Example: print 'hello world' raises a 'SyntaxError' exception. +from __future__ import print_function +from __future__ import absolute_import +from __future__ import division +from __future__ import unicode_literals + import errno import logging import os @@ -523,7 +531,7 @@ def _import_delegations(self, parent_role): raise else: - logger.warn('Invalid key type for '+repr(keyid)+'.') + logger.warning('Invalid key type for '+repr(keyid)+'.') continue # Add the roles to the role database. @@ -536,7 +544,7 @@ def _import_delegations(self, parent_role): tuf.roledb.add_role(rolename, roleinfo) except tuf.RoleAlreadyExistsError as e: - logger.warn('Role already exists: '+rolename) + logger.warning('Role already exists: '+rolename) except: logger.exception('Failed to add delegated role: '+rolename+'.') @@ -2531,7 +2539,7 @@ def remove_obsolete_targets(self, destination_directory): for target in self.metadata['previous'][role]['targets']: if target not in self.metadata['current'][role]['targets']: # 'target' is only in 'previous', so remove it. - logger.warn('Removing obsolete file: '+repr(target)+'.') + logger.warning('Removing obsolete file: '+repr(target)+'.') # Remove the file if it hasn't been removed already. destination = os.path.join(destination_directory, target) try: @@ -2699,6 +2707,6 @@ def download_target(self, target, destination_directory): raise else: - logger.warn(str(target_dirpath)+' does not exist.') + logger.warning(str(target_dirpath)+' does not exist.') target_file_object.move(destination) diff --git a/tuf/conf.py b/tuf/conf.py index eec68c14..b57e58db 100755 --- a/tuf/conf.py +++ b/tuf/conf.py @@ -18,6 +18,14 @@ and cryptography libraries clients wish to use. """ +# Help with Python 3 compatibility, where the print statement is a function, an +# implicit relative import is invalid, and the '/' operator performs true +# division. Example: print 'hello world' raises a 'SyntaxError' exception. +from __future__ import print_function +from __future__ import absolute_import +from __future__ import division +from __future__ import unicode_literals + # 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 diff --git a/tuf/download.py b/tuf/download.py index 394a12a8..e615bc1b 100755 --- a/tuf/download.py +++ b/tuf/download.py @@ -21,10 +21,14 @@ '_download_file()' function. """ -# Induce "true division" (http://www.python.org/dev/peps/pep-0238/). +# Help with Python 3 compatibility, where the print statement is a function, an +# implicit relative import is invalid, and the '/' operator performs true +# division. Example: print 'hello world' raises a 'SyntaxError' exception. +from __future__ import print_function +from __future__ import absolute_import from __future__ import division +from __future__ import unicode_literals -import httplib import logging import os.path import socket @@ -35,11 +39,13 @@ import tuf.hash import tuf.util import tuf.formats +import tuf._vendor.six as six -from tuf.compatibility import httplib, ssl, urllib2, urlparse +from tuf.compatibility import ssl if ssl: from tuf.compatibility import match_hostname + else: raise tuf.Error("No SSL support!") # TODO: degrade gracefully @@ -47,13 +53,10 @@ # reads. Therefore, we will need these global variables. # http://hg.python.org/cpython/file/5be3fa83d436/Lib/socket.py#l84 -try: - from cStringIO import StringIO -except ImportError: - from StringIO import StringIO try: import errno + except ImportError: errno = None EINTR = getattr(errno, 'EINTR', 4) @@ -65,6 +68,7 @@ +# socket._fileobject removed in PY3. SocketIO? class SaferSocketFileObject(socket._fileobject): """We override socket._fileobject to produce a file-like object which reads from a socket more safely than its ancestor. One the safety properties is @@ -222,11 +226,11 @@ def read(self, size): # Already have size bytes in our buffer? Extract and return. buf.seek(0) rv = buf.read(size) - self._rbuf = StringIO() + self._rbuf = six.StringIO() self._rbuf.write(buf.read()) return rv - self._rbuf = StringIO() # reset _rbuf. we consume it via buf. + self._rbuf = six.StringIO() # reset _rbuf. we consume it via buf. # Since we try to detect slow retrieval, this should not be an infinite loop. while True: left = size - buf_len @@ -273,20 +277,21 @@ def read(self, size): -class SaferHTTPResponse(httplib.HTTPResponse): +class SaferHTTPResponse(six.moves.http_client.HTTPResponse): """A safer version of httplib.HTTPResponse, in which we only use safe socket file-like objects.""" def __init__(self, sock, debuglevel=0, strict=0, method=None, buffering=False): - httplib.HTTPResponse.__init__(self, sock, debuglevel, - strict, method) + six.moves.http_client.HTTPResponse.__init__(self, sock, debuglevel, strict, + method) # Delete the previous socket file-like object... del self.fp # ...and replace it with our safer version. if buffering: self.fp = SaferSocketFileObject(sock._sock, 'rb') + else: self.fp = SaferSocketFileObject(sock._sock, 'rb', 0) @@ -294,7 +299,7 @@ def __init__(self, sock, debuglevel=0, strict=0, method=None, -class VerifiedHTTPSConnection(httplib.HTTPSConnection): +class VerifiedHTTPSConnection(six.moves.httplib.HTTPSConnection): """ A connection that wraps connections with ssl certificate verification. @@ -747,7 +752,7 @@ def _download_file(url, required_length, STRICT_REQUIRED_LENGTH=True): # NOTE: Not thread-safe. # Save current values or functions for restoration later. previous_socket_timeout = socket.getdefaulttimeout() - previous_http_response_class = httplib.HTTPConnection.response_class + previous_http_response_class = six.moves.http_client.HTTPConnection.response_class # This is the temporary file that we will return to contain the contents of # the downloaded file. @@ -758,7 +763,7 @@ def _download_file(url, required_length, STRICT_REQUIRED_LENGTH=True): # Set timeout to induce non-blocking socket operations. socket.setdefaulttimeout(tuf.conf.SOCKET_TIMEOUT) # Replace the socket file-like object class with our safer version. - httplib.HTTPConnection.response_class = SaferHTTPResponse + six.moves.http_client.HTTPConnection.response_class = SaferHTTPResponse # Open the connection to the remote file. connection = _open_connection(url) @@ -791,5 +796,5 @@ def _download_file(url, required_length, STRICT_REQUIRED_LENGTH=True): finally: # NOTE: Not thread-safe. # Restore previously saved values or functions. - httplib.HTTPConnection.response_class = previous_http_response_class + six.moves.http_client.HTTPConnection.response_class = previous_http_response_class socket.setdefaulttimeout(previous_socket_timeout) diff --git a/tuf/ed25519_keys.py b/tuf/ed25519_keys.py index b1aa3e7c..307274bc 100755 --- a/tuf/ed25519_keys.py +++ b/tuf/ed25519_keys.py @@ -49,6 +49,7 @@ from __future__ import print_function from __future__ import absolute_import from __future__ import division +from __future__ import unicode_literals # 'binascii' required for hexadecimal conversions. Signatures and # public/private keys are hexlified. diff --git a/tuf/formats.py b/tuf/formats.py index 2ccbfb07..c3347f11 100755 --- a/tuf/formats.py +++ b/tuf/formats.py @@ -60,6 +60,14 @@ signable_object = make_signable(unsigned_object) """ +# Help with Python 3 compatibility, where the print statement is a function, an +# implicit relative import is invalid, and the '/' operator performs true +# division. Example: print 'hello world' raises a 'SyntaxError' exception. +from __future__ import print_function +from __future__ import absolute_import +from __future__ import division +from __future__ import unicode_literals + import binascii import calendar import re @@ -805,7 +813,7 @@ def format_base64(data): data: - A string or buffer of data to convert. + Binary or buffer of data to convert. tuf.FormatError, if the base64 encoding fails or the argument @@ -819,7 +827,7 @@ def format_base64(data): """ try: - return binascii.b2a_base64(data).rstrip('=\n ') + return binascii.b2a_base64(data).decode('utf-8').rstrip('=\n ') except (TypeError, binascii.Error) as e: raise tuf.FormatError('Invalid base64 encoding: '+str(e)) @@ -1179,10 +1187,8 @@ def _canonical_string_encoder(string): """ string = '"%s"' % re.sub(r'(["\\])', r'\\\1', string) - if isinstance(string, unicode): - return string.encode('utf-8') - else: - return string + + return string diff --git a/tuf/hash.py b/tuf/hash.py index 4fe96455..172d00e1 100755 --- a/tuf/hash.py +++ b/tuf/hash.py @@ -18,17 +18,25 @@ available to TUF, simplifying the creation of digest objects, and providing a central location for hash routines are the main goals of this module. Support routines implemented include functions to - create digest objects given a filename or file object. - Hashlib and pycrypto hash algorithms currently supported. - + create digest objects given a filename or file object. Hashlib and PyCrypto + hash algorithms currently supported. """ +# Help with Python 3 compatibility, where the print statement is a function, an +# implicit relative import is invalid, and the '/' operator performs true +# division. Example: print 'hello world' raises a 'SyntaxError' exception. +from __future__ import print_function +from __future__ import absolute_import +from __future__ import division +from __future__ import unicode_literals import logging # Import tuf Exceptions. import tuf import tuf.log +import tuf._vendor.six as six + # Import tuf logger to log warning messages. logger = logging.getLogger('tuf.hash') @@ -126,7 +134,6 @@ def digest(algorithm=_DEFAULT_HASH_ALGORITHM, Digest object (e.g., hashlib.new(algorithm) or algorithm.new() # pycrypto). - """ # Was a hashlib digest object requested and is it supported? @@ -189,6 +196,7 @@ def digest_fileobject(file_object, algorithm=_DEFAULT_HASH_ALGORITHM, tuf.UnsupportedAlgorithmError + tuf.Error @@ -197,7 +205,6 @@ def digest_fileobject(file_object, algorithm=_DEFAULT_HASH_ALGORITHM, Digest object (e.g., hashlib.new(algorithm) or algorithm.new() # pycrypto). - """ # Digest object returned whose hash will be updated using 'file_object'. @@ -218,7 +225,13 @@ def digest_fileobject(file_object, algorithm=_DEFAULT_HASH_ALGORITHM, data = file_object.read(chunksize) if not data: break - digest_object.update(data_to_string(data)) + + if not isinstance(data, six.binary_type): + digest_object.update(data.encode('utf-8')) + + else: + digest_object.update(data) + return digest_object @@ -254,7 +267,6 @@ def digest_filename(filename, algorithm=_DEFAULT_HASH_ALGORITHM, Digest object (e.g., hashlib.new(algorithm) or algorithm.new() # pycrypto). - """ # Open 'filename' in read+binary mode. @@ -267,40 +279,5 @@ def digest_filename(filename, algorithm=_DEFAULT_HASH_ALGORITHM, digest_object = digest_fileobject(file_object, algorithm, hash_library) file_object.close() + return digest_object - - - - - -def data_to_string(data): - """ - - Return 'data' as a string. The update() function of a digest object - only accepts strings, however, TUF will often need to feed this function - non-strings. This utility function circumvents this issue and decides how - exactly to convert these objects TUF might use. - - - data: - The data object to be returned as a string. - - - None. - - - None. - - - String. - - """ - - if isinstance(data, str): - return data - - elif isinstance(data, unicode): - return data.encode("utf-8") - - else: - return str(data) diff --git a/tuf/keydb.py b/tuf/keydb.py index d51f1711..d2e351ac 100755 --- a/tuf/keydb.py +++ b/tuf/keydb.py @@ -27,6 +27,14 @@ 'keyid' key (i.e., rsakey['keyid']). """ +# Help with Python 3 compatibility, where the print statement is a function, an +# implicit relative import is invalid, and the '/' operator performs true +# division. Example: print 'hello world' raises a 'SyntaxError' exception. +from __future__ import print_function +from __future__ import absolute_import +from __future__ import division +from __future__ import unicode_literals + import logging import copy @@ -100,11 +108,11 @@ def create_keydb_from_root_metadata(root_metadata): continue except tuf.KeyAlreadyExistsError as e: - logger.warn(e) + logger.warning(e) continue else: - logger.warn('Root Metadata file contains a key with an invalid keytype.') + logger.warning('Root Metadata file contains a key with an invalid keytype.') diff --git a/tuf/keys.py b/tuf/keys.py index 3b32b8bb..6b33853c 100755 --- a/tuf/keys.py +++ b/tuf/keys.py @@ -44,6 +44,14 @@ key (i.e., rsakey['keyid']). """ +# Help with Python 3 compatibility, where the print statement is a function, an +# implicit relative import is invalid, and the '/' operator performs true +# division. Example: print 'hello world' raises a 'SyntaxError' exception. +from __future__ import print_function +from __future__ import absolute_import +from __future__ import division +from __future__ import unicode_literals + # Required for hexadecimal conversions. Signatures and public/private keys are # hexlified. import binascii @@ -221,13 +229,13 @@ def generate_rsa_key(bits=_DEFAULT_RSA_KEY_BITS): # 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, + key_value = {'public': public.decode(), '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 + key_value['private'] = private.decode() rsakey_dict['keytype'] = keytype rsakey_dict['keyid'] = keyid @@ -492,7 +500,7 @@ def _get_keyid(keytype, key_value): # 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(key_update_data) + digest_object.update(key_update_data.encode('utf-8')) # 'keyid' becomes the hexadecimal representation of the hash. keyid = digest_object.hexdigest() diff --git a/tuf/log.py b/tuf/log.py index f57b27d4..eec18bce 100755 --- a/tuf/log.py +++ b/tuf/log.py @@ -56,6 +56,13 @@ http://docs.python.org/2/howto/logging-cookbook.html """ +# Help with Python 3 compatibility, where the print statement is a function, an +# implicit relative import is invalid, and the '/' operator performs true +# division. Example: print 'hello world' raises a 'SyntaxError' exception. +from __future__ import print_function +from __future__ import absolute_import +from __future__ import division +from __future__ import unicode_literals import logging import time @@ -304,7 +311,7 @@ def add_console_handler(log_level=_DEFAULT_CONSOLE_LOG_LEVEL): logger.addHandler(console_handler) logger.debug('Added a console handler.') else: - logger.warn('We already have a console handler.') + logger.warning('We already have a console handler.') @@ -337,4 +344,4 @@ def remove_console_handler(): console_handler = None logger.debug('Removed a console handler.') else: - logger.warn('We do not have a console handler.') + logger.warning('We do not have a console handler.') diff --git a/tuf/mirrors.py b/tuf/mirrors.py index bc0825ad..badaa5be 100755 --- a/tuf/mirrors.py +++ b/tuf/mirrors.py @@ -3,20 +3,28 @@ mirrors.py - Konstantin Andrianov + Konstantin Andrianov. Derived from original mirrors.py written by Geremy Condra. - March 12, 2012 + March 12, 2012. See LICENSE for licensing information. - To extract a list of mirror urls corresponding to the file type and - the location of the file with respect to the base url. + Extract a list of mirror urls corresponding to the file type and the location + of the file with respect to the base url. """ +# Help with Python 3 compatibility, where the print statement is a function, an +# implicit relative import is invalid, and the '/' operator performs true +# division. Example: print 'hello world' raises a 'SyntaxError' exception. +from __future__ import print_function +from __future__ import absolute_import +from __future__ import division +from __future__ import unicode_literals + import os import tuf diff --git a/tuf/pycrypto_keys.py b/tuf/pycrypto_keys.py index 4f9f6416..65c04a72 100755 --- a/tuf/pycrypto_keys.py +++ b/tuf/pycrypto_keys.py @@ -44,6 +44,14 @@ Derivation Function 1 (PBKF1) + MD5. """ +# Help with Python 3 compatibility, where the print statement is a function, an +# implicit relative import is invalid, and the '/' operator performs true +# division. Example: print 'hello world' raises a 'SyntaxError' exception. +from __future__ import print_function +from __future__ import absolute_import +from __future__ import division +from __future__ import unicode_literals + import os import binascii import json diff --git a/tuf/repository_tool.py b/tuf/repository_tool.py index 05873fe2..94499d80 100755 --- a/tuf/repository_tool.py +++ b/tuf/repository_tool.py @@ -24,6 +24,7 @@ from __future__ import print_function from __future__ import absolute_import from __future__ import division +from __future__ import unicode_literals import os import errno @@ -592,7 +593,7 @@ def add_verification_key(self, key): except tuf.KeyAlreadyExistsError as e: message = 'Adding a verification key that has already been used.' - logger.warn(message) + logger.warning(message) keyid = key['keyid'] roleinfo = tuf.roledb.get_roleinfo(self.rolename) @@ -3343,7 +3344,7 @@ def _log_warning_if_expires_soon(rolename, expires_iso8601_timestamp, message = repr(rolename) + ' expires ' + datetime_object.ctime() + \ ' (UTC).\n' + repr(days_until_expires) + ' day(s) until it expires.' - logger.warn(message) + logger.warning(message) @@ -4120,7 +4121,7 @@ def generate_targets_metadata(targets_directory, target_files, version, digest_target = os.path.join(dirname, digest_filename) if not os.path.exists(digest_target): - logger.warn('Hard linking target file to ' + repr(digest_target)) + logger.warning('Hard linking target file to ' + repr(digest_target)) os.link(target_path, digest_target) # Generate the targets metadata object. @@ -4323,7 +4324,7 @@ def generate_timestamp_metadata(snapshot_filename, version, compressed_fileinfo = get_metadata_fileinfo(compressed_filename) except: - logger.warn('Cannot get fileinfo about '+repr(compressed_filename)) + logger.warning('Cannot get fileinfo about '+repr(compressed_filename)) else: logger.info('Including fileinfo about '+repr(compressed_filename)) @@ -4410,7 +4411,7 @@ def sign_metadata(metadata_object, keyids, filename): signable['signatures'].append(signature) else: - logger.warn('Private key unset. Skipping: '+repr(keyid)) + logger.warning('Private key unset. Skipping: '+repr(keyid)) else: raise tuf.Error('The keydb contains a key with an invalid key type.') diff --git a/tuf/roledb.py b/tuf/roledb.py index a1a6f350..403b5dcb 100755 --- a/tuf/roledb.py +++ b/tuf/roledb.py @@ -35,6 +35,14 @@ optional. """ +# Help with Python 3 compatibility, where the print statement is a function, an +# implicit relative import is invalid, and the '/' operator performs true +# division. Example: print 'hello world' raises a 'SyntaxError' exception. +from __future__ import print_function +from __future__ import absolute_import +from __future__ import division +from __future__ import unicode_literals + import logging import copy diff --git a/tuf/schema.py b/tuf/schema.py index e13c5ace..98ba35c1 100755 --- a/tuf/schema.py +++ b/tuf/schema.py @@ -40,6 +40,13 @@ can be found in 'formats.py'. """ +# Help with Python 3 compatibility, where the print statement is a function, an +# implicit relative import is invalid, and the '/' operator performs true +# division. Example: print 'hello world' raises a 'SyntaxError' exception. +from __future__ import print_function +from __future__ import absolute_import +from __future__ import division +from __future__ import unicode_literals import re import sys diff --git a/tuf/sig.py b/tuf/sig.py index cacf3265..e8738487 100755 --- a/tuf/sig.py +++ b/tuf/sig.py @@ -35,6 +35,14 @@ is also a function for that. """ +# Help with Python 3 compatibility, where the print statement is a function, an +# implicit relative import is invalid, and the '/' operator performs true +# division. Example: print 'hello world' raises a 'SyntaxError' exception. +from __future__ import print_function +from __future__ import absolute_import +from __future__ import division +from __future__ import unicode_literals + import tuf import tuf.formats import tuf.keydb diff --git a/tuf/unittest_toolbox.py b/tuf/unittest_toolbox.py index 7a5efb33..5e9a6be4 100755 --- a/tuf/unittest_toolbox.py +++ b/tuf/unittest_toolbox.py @@ -17,6 +17,14 @@ Specifically, Modified_TestCase is a derived class from unittest.TestCase. """ +# Help with Python 3 compatibility, where the print statement is a function, an +# implicit relative import is invalid, and the '/' operator performs true +# division. Example: print 'hello world' raises a 'SyntaxError' exception. +from __future__ import print_function +from __future__ import absolute_import +from __future__ import division +from __future__ import unicode_literals + import os import sys import shutil diff --git a/tuf/util.py b/tuf/util.py index bcbc10c7..b497b62d 100755 --- a/tuf/util.py +++ b/tuf/util.py @@ -18,6 +18,14 @@ TempFile class that generates a file-like object for temporary storage, etc. """ +# Help with Python 3 compatibility, where the print statement is a function, an +# implicit relative import is invalid, and the '/' operator performs true +# division. Example: print 'hello world' raises a 'SyntaxError' exception. +from __future__ import print_function +from __future__ import absolute_import +from __future__ import division +from __future__ import unicode_literals + import os import sys import gzip @@ -416,8 +424,10 @@ def ensure_parent_dir(filename): # Split 'filename' into head and tail, check if head exists. directory = os.path.split(filename)[0] + if directory and not os.path.exists(directory): - os.makedirs(directory, 0700) + # mode = 'rwx------'. 448 (decimal) is 700 in octal. + os.makedirs(directory, 448) @@ -775,7 +785,7 @@ def get_target_hash(target_filepath): digest_object = tuf.hash.digest(HASH_FUNCTION) try: - digest_object.update(target_filepath) + digest_object.update(target_filepath.encode('utf-8')) except UnicodeEncodeError: # Sometimes, there are Unicode characters in target paths. We assume a From 8684253675fdd62b7e4c363c6786f63bfa0674f3 Mon Sep 17 00:00:00 2001 From: Vladimir Diaz Date: Tue, 6 May 2014 15:24:39 -0400 Subject: [PATCH 05/32] [WIP] Python 2+3. Mostly unicode-related changes for crypto modules. --- tests/test_ed25519_keys.py | 8 ++--- tests/test_indefinite_freeze_attack.py | 2 +- tests/test_pycrypto_keys.py | 4 +-- tests/test_schema.py | 4 --- tuf/__init__.py | 2 +- tuf/download.py | 20 ++++++------ tuf/ed25519_keys.py | 9 +++--- tuf/formats.py | 8 ++--- tuf/pycrypto_keys.py | 6 ++-- tuf/schema.py | 42 ++++++++++++++++++++++++++ tuf/util.py | 1 + 11 files changed, 72 insertions(+), 34 deletions(-) diff --git a/tests/test_ed25519_keys.py b/tests/test_ed25519_keys.py index 9089a2fd..4c2fc274 100755 --- a/tests/test_ed25519_keys.py +++ b/tests/test_ed25519_keys.py @@ -54,7 +54,7 @@ def test_generate_public_and_private(self): def test_create_signature(self): global public global private - data = 'The quick brown fox jumps over the lazy dog' + data = b'The quick brown fox jumps over the lazy dog' signature, method = ed25519.create_signature(public, private, data) # Verify format of returned values. @@ -79,7 +79,7 @@ def test_create_signature(self): def test_verify_signature(self): global public global private - data = 'The quick brown fox jumps over the lazy dog' + data = b'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) @@ -107,13 +107,13 @@ def test_verify_signature(self): signature, '123')) # Mismatched signature. - bad_signature = 'a'*64 + bad_signature = b'a'*64 self.assertEqual(False, ed25519.verify_signature(public, method, bad_signature, data)) # Generated signature created with different data. new_signature, method = ed25519.create_signature(public, private, - 'mismatched data') + b'mismatched data') self.assertEqual(False, ed25519.verify_signature(public, method, new_signature, data)) diff --git a/tests/test_indefinite_freeze_attack.py b/tests/test_indefinite_freeze_attack.py index d7181a76..aceaf795 100755 --- a/tests/test_indefinite_freeze_attack.py +++ b/tests/test_indefinite_freeze_attack.py @@ -241,7 +241,7 @@ def test_with_tuf(self): self.repository_updater.refresh() except tuf.NoWorkingMirrorError as e: - for mirror_url, mirror_error in e.mirror_errors.iteritems(): + for mirror_url, mirror_error in six.iteritems(e.mirror_errors): self.assertTrue(isinstance(mirror_error, tuf.ExpiredMetadataError)) diff --git a/tests/test_pycrypto_keys.py b/tests/test_pycrypto_keys.py index 1dee349a..90c58d1b 100755 --- a/tests/test_pycrypto_keys.py +++ b/tests/test_pycrypto_keys.py @@ -64,7 +64,7 @@ def test_generate_rsa_public_and_private(self): def test_create_rsa_signature(self): global private_rsa - data = 'The quick brown fox jumps over the lazy dog' + data = 'The quick brown fox jumps over the lazy dog'.encode('utf-8') signature, method = pycrypto.create_rsa_signature(private_rsa, data) # Verify format of returned values. @@ -85,7 +85,7 @@ def test_create_rsa_signature(self): def test_verify_rsa_signature(self): global public_rsa global private_rsa - data = 'The quick brown fox jumps over the lazy dog' + data = 'The quick brown fox jumps over the lazy dog'.encode('utf-8') signature, method = pycrypto.create_rsa_signature(private_rsa, data) valid_signature = pycrypto.verify_rsa_signature(signature, method, public_rsa, diff --git a/tests/test_schema.py b/tests/test_schema.py index c5203d12..019e008d 100755 --- a/tests/test_schema.py +++ b/tests/test_schema.py @@ -116,7 +116,6 @@ def test_AnyString(self): self.assertTrue(anystring_schema.matches('')) self.assertTrue(anystring_schema.matches('a string')) - self.assertTrue(anystring_schema.matches(u'a unicode string')) # Test conditions for invalid arguments. self.assertFalse(anystring_schema.matches(['a'])) @@ -210,9 +209,6 @@ def test_Integer(self): integer_schema = tuf.schema.Integer() self.assertTrue(integer_schema.matches(99)) - if six.PY3: - print('reached long(1)') - self.assertTrue(integer_schema.matches(long(1))) self.assertTrue(tuf.schema.Integer(lo=10, hi=30).matches(25)) # Test conditions for invalid arguments. diff --git a/tuf/__init__.py b/tuf/__init__.py index 7c37e7f8..e701100d 100755 --- a/tuf/__init__.py +++ b/tuf/__init__.py @@ -324,7 +324,7 @@ def __str__(self): for mirror_url, mirror_error in self.mirror_errors.iteritems(): try: # http://docs.python.org/2/library/urlparse.html#urlparse.urlparse - mirror_url_tokens = six.moves.urlparse.urlparse(mirror_url) + mirror_url_tokens = six.moves.urllib.parse.urlparse(mirror_url) except: logging.exception('Failed to parse mirror URL: '+str(mirror_url)) diff --git a/tuf/download.py b/tuf/download.py index e615bc1b..f53315f7 100755 --- a/tuf/download.py +++ b/tuf/download.py @@ -66,8 +66,6 @@ - - # socket._fileobject removed in PY3. SocketIO? class SaferSocketFileObject(socket._fileobject): """We override socket._fileobject to produce a file-like object which reads @@ -299,7 +297,7 @@ def __init__(self, sock, debuglevel=0, strict=0, method=None, -class VerifiedHTTPSConnection(six.moves.httplib.HTTPSConnection): +class VerifiedHTTPSConnection(six.moves.http_client.HTTPSConnection): """ A connection that wraps connections with ssl certificate verification. @@ -345,7 +343,7 @@ def connect(self): -class VerifiedHTTPSHandler(urllib2.HTTPSHandler): +class VerifiedHTTPSHandler(six.moves.urllib.request.HTTPSHandler): """ A HTTPSHandler that uses our own VerifiedHTTPSConnection. @@ -354,7 +352,7 @@ class VerifiedHTTPSHandler(urllib2.HTTPSHandler): def __init__(self, connection_class = VerifiedHTTPSConnection): self.specialized_conn_class = connection_class - urllib2.HTTPSHandler.__init__(self) + six.moves.urllib.request.HTTPSHandler.__init__(self) def https_open(self, req): return self.do_open(self.specialized_conn_class, req) @@ -371,7 +369,7 @@ def _get_request(url): https://github.com/pypa/pip/blob/d0fa66ecc03ab20b7411b35f7c7b423f31f77761/pip/download.py#L147 """ - return urllib2.Request(url, headers={'Accept-encoding': 'identity'}) + return six.moves.urllib.request.Request(url, headers={'Accept-encoding': 'identity'}) @@ -390,15 +388,15 @@ def _get_opener(scheme=None): # If we are going over https, use an opener which will provide SSL # certificate verification. https_handler = VerifiedHTTPSHandler() - opener = urllib2.build_opener(https_handler) + opener = six.moves.urllib.request.build_opener(https_handler) # strip out HTTPHandler to prevent MITM spoof for handler in opener.handlers: - if isinstance(handler, urllib2.HTTPHandler): + if isinstance(handler, six.moves.urllib.request.HTTPHandler): opener.handlers.remove(handler) else: # Otherwise, use the default opener. - opener = urllib2.build_opener() + opener = six.moves.urllib.request.build_opener() return opener @@ -439,7 +437,7 @@ def _open_connection(url): # servers do not recognize connections that originates from # Python-urllib/x.y. - parsed_url = urlparse.urlparse(url) + parsed_url = six.moves.urllib.parse.urlparse(url) opener = _get_opener(scheme=parsed_url.scheme) request = _get_request(url) return opener.open(request) @@ -760,7 +758,7 @@ def _download_file(url, required_length, STRICT_REQUIRED_LENGTH=True): try: # NOTE: Not thread-safe. - # Set timeout to induce non-blocking socket operations. + # met timeout to induce non-blocking socket operations. socket.setdefaulttimeout(tuf.conf.SOCKET_TIMEOUT) # Replace the socket file-like object class with our safer version. six.moves.http_client.HTTPConnection.response_class = SaferHTTPResponse diff --git a/tuf/ed25519_keys.py b/tuf/ed25519_keys.py index 307274bc..e2af814a 100755 --- a/tuf/ed25519_keys.py +++ b/tuf/ed25519_keys.py @@ -164,7 +164,8 @@ def generate_public_and_private(): # key generation. try: nacl_key = nacl.signing.SigningKey(seed) - public = str(nacl_key.verify_key) + #public = nacl_key.verify_key + public = nacl_key.verify_key.encode(encoder=nacl.encoding.RawEncoder()) except NameError: message = 'The PyNaCl library and/or its dependencies unavailable.' @@ -188,7 +189,7 @@ def create_signature(public_key, private_key, data): A signature is a 64-byte string. >>> public, private = generate_public_and_private() - >>> data = 'The quick brown fox jumps over the lazy dog' + >>> data = b'The quick brown fox jumps over the lazy dog' >>> signature, method = \ create_signature(public, private, data) >>> tuf.formats.ED25519SIGNATURE_SCHEMA.matches(signature) @@ -273,14 +274,14 @@ def verify_signature(public_key, method, signature, data, use_pynacl=False): 'sig', and 'data' arguments to complete the verification. >>> public, private = generate_public_and_private() - >>> data = 'The quick brown fox jumps over the lazy dog' + >>> data = b'The quick brown fox jumps over the lazy dog' >>> signature, method = \ create_signature(public, private, data) >>> 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_data = b'The sly brown fox jumps over the lazy dog' >>> bad_signature, method = \ create_signature(public, private, bad_data) >>> verify_signature(public, method, bad_signature, data, use_pynacl=False) diff --git a/tuf/formats.py b/tuf/formats.py index c3347f11..9aa9a97a 100755 --- a/tuf/formats.py +++ b/tuf/formats.py @@ -227,13 +227,13 @@ keyval = KEYVAL_SCHEMA) # An ED25519 raw public key, which must be 32 bytes. -ED25519PUBLIC_SCHEMA = SCHEMA.LengthString(32) +ED25519PUBLIC_SCHEMA = SCHEMA.LengthBytes(32) # An ED25519 raw seed key, which must be 32 bytes. -ED25519SEED_SCHEMA = SCHEMA.LengthString(32) +ED25519SEED_SCHEMA = SCHEMA.LengthBytes(32) # An ED25519 raw signature, which must be 64 bytes. -ED25519SIGNATURE_SCHEMA = SCHEMA.LengthString(64) +ED25519SIGNATURE_SCHEMA = SCHEMA.LengthBytes(64) # An ed25519 TUF key. ED25519KEY_SCHEMA = SCHEMA.Object( @@ -728,7 +728,7 @@ def datetime_to_unix_timestamp(datetime_object): timestamp. For example, Python's time.time() returns a Unix timestamp, and includes the number of microseconds. 'datetime_object' is converted to UTC. - >>> datetime_object = datetime.datetime(1985, 10, 26, 01, 22) + >>> datetime_object = datetime.datetime(1985, 10, 26, 1, 22) >>> timestamp = datetime_to_unix_timestamp(datetime_object) >>> timestamp 499137720 diff --git a/tuf/pycrypto_keys.py b/tuf/pycrypto_keys.py index 65c04a72..8dda5a16 100755 --- a/tuf/pycrypto_keys.py +++ b/tuf/pycrypto_keys.py @@ -213,7 +213,7 @@ def generate_rsa_public_and_private(bits=_DEFAULT_RSA_KEY_BITS): rsa_pubkey = rsa_key_object.publickey() public = rsa_pubkey.exportKey(format='PEM') - return public, private + return public.decode(), private.decode() @@ -479,7 +479,7 @@ def create_rsa_encrypted_pem(private_key, passphrase): raise TypeError('The required private key is unset.') - return encrypted_pem + return encrypted_pem.decode() @@ -588,7 +588,7 @@ def create_rsa_public_and_private_from_encrypted_pem(encrypted_pem, passphrase): message = 'The public and private keys cannot be exported in PEM format.' raise tuf.CryptoError(message) - return public, private + return public.decode(), private.decode() diff --git a/tuf/schema.py b/tuf/schema.py index 98ba35c1..389668ad 100755 --- a/tuf/schema.py +++ b/tuf/schema.py @@ -244,6 +244,48 @@ def check_match(self, object): +class LengthBytes(Schema): + """ + + Matches any Bytes of a specified length. The argument object must be either + a str() in Python 2, or bytes() in Python 3. At instantiation, the bytes + length is set and any future comparisons are checked against this internal + bytes value length. + + Supported methods include + matches(): returns a Boolean result. + check_match(): raises 'tuf.FormatError' on a mismatch. + + + + >>> schema = LengthBytes(5) + >>> schema.matches('Hello') + True + >>> schema.matches('Hi') + False + """ + + def __init__(self, length): + if isinstance(length, bool) or not isinstance(length, six.integer_types): + # 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._bytes_length = length + + + def check_match(self, object): + if not isinstance(object, six.binary_type): + raise tuf.FormatError('Expected a byte but got '+repr(object)) + + if len(object) != self._bytes_length: + raise tuf.FormatError('Expected a byte of length '+ + repr(self._bytes_length)) + + + + + class OneOf(Schema): """ diff --git a/tuf/util.py b/tuf/util.py index b497b62d..082ab062 100755 --- a/tuf/util.py +++ b/tuf/util.py @@ -63,6 +63,7 @@ def _default_temporary_directory(self, prefix): """__init__ helper.""" try: self.temporary_file = tempfile.NamedTemporaryFile(prefix=prefix) + except OSError as err: logger.critical('Temp file in '+temp_dir+'failed: '+repr(err)) raise tuf.Error(err) From e4bd9a7ba25702e0fcadc5659bdf7b4cf9dec163 Mon Sep 17 00:00:00 2001 From: vladdd Date: Sun, 11 May 2014 22:59:42 -0400 Subject: [PATCH 06/32] [WIP] Refactor download.py --- tests/test_slow_retrieval_attack.py | 4 +- tuf/__init__.py | 2 +- .../ssl_match_hostname.py | 0 tuf/compatibility/__init__.py | 39 - tuf/compatibility/socket_create_connection.py | 48 -- tuf/conf.py | 2 +- tuf/download.py | 683 ++++++------------ tuf/repository_tool.py | 2 +- 8 files changed, 235 insertions(+), 545 deletions(-) rename tuf/{compatibility => _vendor}/ssl_match_hostname.py (100%) delete mode 100644 tuf/compatibility/__init__.py delete mode 100644 tuf/compatibility/socket_create_connection.py diff --git a/tests/test_slow_retrieval_attack.py b/tests/test_slow_retrieval_attack.py index 7b0b4a77..11696909 100755 --- a/tests/test_slow_retrieval_attack.py +++ b/tests/test_slow_retrieval_attack.py @@ -210,7 +210,7 @@ def tearDown(self): unittest_toolbox.Modified_TestCase.tearDown(self) - + """ def test_without_tuf_mode_1(self): # Simulate a slow retrieval attack. # 'mode_1': When download begins,the server blocks the download @@ -271,7 +271,7 @@ def test_without_tuf_mode_2(self): finally: # Terminate the slow retrieval (mode 2) server. self._stop_slow_server(server_process) - + """ def test_with_tuf_mode_1(self): diff --git a/tuf/__init__.py b/tuf/__init__.py index e701100d..eba0d758 100755 --- a/tuf/__init__.py +++ b/tuf/__init__.py @@ -254,7 +254,7 @@ def __init__(self, average_download_speed): def __str__(self): return "Download was too slow. Average speed: "+\ - str(self.__average_download_speed)+" bytes/second" + str(self.__average_download_speed)+" bytes per second" diff --git a/tuf/compatibility/ssl_match_hostname.py b/tuf/_vendor/ssl_match_hostname.py similarity index 100% rename from tuf/compatibility/ssl_match_hostname.py rename to tuf/_vendor/ssl_match_hostname.py diff --git a/tuf/compatibility/__init__.py b/tuf/compatibility/__init__.py deleted file mode 100644 index 5ae1822b..00000000 --- a/tuf/compatibility/__init__.py +++ /dev/null @@ -1,39 +0,0 @@ -""" -We copy some backwards compatibility from pip. - -https://github.com/pypa/pip/tree/d0fa66ecc03ab20b7411b35f7c7b423f31f77761/pip/backwardcompat -""" - - -import sys - - -if sys.version_info >= (3,): - import http.client as httplib - import urllib.parse as urlparse - import urllib.request as urllib2 -else: - import httplib - import urllib2 - import urlparse - - -## py25 has no builtin ssl module -## only >=py32 has ssl.match_hostname and ssl.CertificateError -try: - import ssl - try: - from ssl import match_hostname, CertificateError - except ImportError: - from tuf.compatibility.ssl_match_hostname import match_hostname, CertificateError -except ImportError: - ssl = None - - -# patch for py25 socket to work with http://pypi.python.org/pypi/ssl/ -import socket -if not hasattr(socket, 'create_connection'): # for Python 2.5 - # monkey-patch socket module - from tuf.compatibility.socket_create_connection import create_connection - socket.create_connection = create_connection - diff --git a/tuf/compatibility/socket_create_connection.py b/tuf/compatibility/socket_create_connection.py deleted file mode 100644 index 1a11f4bd..00000000 --- a/tuf/compatibility/socket_create_connection.py +++ /dev/null @@ -1,48 +0,0 @@ -""" -We copy some functions from the Python 2.7.3 socket module. - -http://hg.python.org/releasing/2.7.3/file/7bb96963d067/Lib/socket.py -""" - - -_GLOBAL_DEFAULT_TIMEOUT = object() - - -def create_connection(address, timeout=_GLOBAL_DEFAULT_TIMEOUT, - source_address=None): - """Connect to *address* and return the socket object. - - Convenience function. Connect to *address* (a 2-tuple ``(host, - port)``) and return the socket object. Passing the optional - *timeout* parameter will set the timeout on the socket instance - before attempting to connect. If no *timeout* is supplied, the - global default timeout setting returned by :func:`getdefaulttimeout` - is used. If *source_address* is set it must be a tuple of (host, port) - for the socket to bind as a source address before making the connection. - An host of '' or port 0 tells the OS to use the default. - """ - - host, port = address - err = None - for res in getaddrinfo(host, port, 0, SOCK_STREAM): - af, socktype, proto, canonname, sa = res - sock = None - try: - sock = socket(af, socktype, proto) - if timeout is not _GLOBAL_DEFAULT_TIMEOUT: - sock.settimeout(timeout) - if source_address: - sock.bind(source_address) - sock.connect(sa) - return sock - - except error as _: - err = _ - if sock is not None: - sock.close() - - if err is not None: - raise err - else: - raise error("getaddrinfo returns an empty list") - diff --git a/tuf/conf.py b/tuf/conf.py index b57e58db..db381d82 100755 --- a/tuf/conf.py +++ b/tuf/conf.py @@ -57,7 +57,7 @@ DEFAULT_ROOT_REQUIRED_LENGTH = 512000 #bytes # Set a timeout value in seconds (float) for non-blocking socket operations. -SOCKET_TIMEOUT = 1 #seconds +SOCKET_TIMEOUT = 2 #seconds # The maximum chunk of data, in bytes, we would download in every round. CHUNK_SIZE = 8192 #bytes diff --git a/tuf/download.py b/tuf/download.py index f53315f7..da11125a 100755 --- a/tuf/download.py +++ b/tuf/download.py @@ -13,12 +13,11 @@ See LICENSE for licensing information. - Perform any file downloads and check their validity. This means that the - hash and length of a downloaded file has to match the hash and length - supplied by the metadata of that file. The downloaded file is technically a - file-like object that will automatically destroys itself once closed. Note - that the file-like object, 'tuf.util.TempFile', is returned by the - '_download_file()' function. + Download metadata and target files and check their validity. The hash and + length of a downloaded file has to match the hash and length supplied by the + metadata of that file. The downloaded file is technically a file-like object + that will automatically destroys itself once closed. Note that the file-like + object, 'tuf.util.TempFile', is returned by the '_download_file()' function. """ # Help with Python 3 compatibility, where the print statement is a function, an @@ -29,9 +28,9 @@ from __future__ import division from __future__ import unicode_literals -import logging -import os.path +import os import socket +import logging import timeit import tuf @@ -41,321 +40,223 @@ import tuf.formats import tuf._vendor.six as six -from tuf.compatibility import ssl - -if ssl: - from tuf.compatibility import match_hostname - -else: - raise tuf.Error("No SSL support!") # TODO: degrade gracefully - -# We will be overriding socket._fileobject to perform non-blocking socket -# reads. Therefore, we will need these global variables. -# http://hg.python.org/cpython/file/5be3fa83d436/Lib/socket.py#l84 - - try: - import errno + from ssl import match_hostname, CertificateError except ImportError: - errno = None -EINTR = getattr(errno, 'EINTR', 4) + from tuf._vendor.ssl_match_hostname import match_hostname, CertificateError # See 'log.py' to learn how logging is handled in TUF. logger = logging.getLogger('tuf.download') -# socket._fileobject removed in PY3. SocketIO? -class SaferSocketFileObject(socket._fileobject): - """We override socket._fileobject to produce a file-like object which reads - from a socket more safely than its ancestor. One the safety properties is - that reading from a socket must be a non-blocking operation.""" - - def __init__(self, sock, mode='rb', bufsize=-1, close=False): - super(SaferSocketFileObject, self).__init__(sock, mode=mode, - bufsize=bufsize, close=close) - - # Count the number of bytes received with this socket. - self.__number_of_bytes_received = 0 - # Count the seconds spent receiving with this socket. Tolerate servers with - # a slow start by ignoring their delivery speed for - # tuf.conf.SLOW_START_GRACE_PERIOD seconds. - assert tuf.conf.SLOW_START_GRACE_PERIOD > 0 - self.__seconds_spent_receiving = -tuf.conf.SLOW_START_GRACE_PERIOD - # Remember the time a clock was started. - self.__start_time = None +def safe_download(url, required_length): + return _download_file(url, required_length, STRICT_REQUIRED_LENGTH=True) - def __start_clock(self): - """ - - Start the clock to measure time difference later. - - - None. - - - AssertionError: - When any internal condition is not true. - - - Start time is kept inside this object. - - - None. - """ - - # We must have reset the clock before this. - assert self.__start_time is None - # We use (platform-specific) wall time, so it will be imprecise sometimes. - self.__start_time = timeit.default_timer() +def unsafe_download(url, required_length): + return _download_file(url, required_length, STRICT_REQUIRED_LENGTH=False) - def __stop_clock_and_check_speed(self, data_length): - """ - - Stop the clock and try to detect slow retrieval. +def _download_file(url, required_length, STRICT_REQUIRED_LENGTH=True): + """ + + Given the url, hashes and length of the desired file, this function + opens a connection to 'url' and downloads the file while ensuring its + length and hashes match 'required_hashes' and 'required_length'. + + tuf.util.TempFile is used instead of regular tempfile object because of + additional functionality provided by 'tuf.util.TempFile'. + + + url: + A URL string that represents the location of the file. + + required_length: + An integer value representing the length of the file. - - data_length: - A non-negative integer indicating the size of data retrieved in bytes. + STRICT_REQUIRED_LENGTH: + A Boolean indicator used to signal whether we should perform strict + checking of required_length. True by default. We explicitly set this to + False when we know that we want to turn this off for downloading the + timestamp metadata, which has no signed required_length. - - tuf.SlowRetrievalError: - If the average download speed falls below - 'tuf.conf.MIN_AVERAGE_DOWNLOAD_SPEED'. + + A 'tuf.util.TempFile' object is created on disk to store the contents of + 'url'. + + + tuf.DownloadLengthMismatchError, if there was a mismatch of observed vs + expected lengths while downloading the file. + + tuf.FormatError, if any of the arguments are improperly formatted. - AssertionError: - When any internal condition is not true. + Any other unforeseen runtime exception. + + + A 'tuf.util.TempFile' file-like object that points to the contents of 'url'. + """ - - Start time is cleared inside this object. + # Do all of the arguments have the appropriate format? + # Raise 'tuf.FormatError' if there is a mismatch. + tuf.formats.URL_SCHEMA.check_match(url) + tuf.formats.LENGTH_SCHEMA.check_match(required_length) - - None. - """ + # 'url.replace()' is for compatibility with Windows-based systems because + # they might put back-slashes in place of forward-slashes. This converts it + # to the common format. + url = url.replace('\\', '/') + logger.info('Downloading: '+str(url)) - # We use (platform-specific) wall time, so it will be imprecise sometimes. - stop_time = timeit.default_timer() - # We must have already started the clock. - assert self.__start_time > 0 - time_delta = stop_time-self.__start_time - # Reset the clock. - self.__start_time = None + # This is the temporary file that we will return to contain the contents of + # the downloaded file. + temp_file = tuf.util.TempFile() - # Measure the average download speed. - self.__number_of_bytes_received += data_length - self.__seconds_spent_receiving += time_delta + try: + # Open the connection to the remote file. + connection = _open_connection(url) - # self.__seconds_spent_receiving begins at negative - # 'tuf.conf.SLOW_START_GRACE_PERIOD'. - if self.__seconds_spent_receiving > 0: - average_download_speed = \ - self.__number_of_bytes_received/self.__seconds_spent_receiving + # We ask the server about how big it thinks this file should be. + reported_length = _get_content_length(connection) - # If the average download speed is below a certain threshold, we flag this - # as a possible slow-retrieval attack. This threshold will determine our - # bias: if it is too low, we will have more false positives; if it is too - # high, we will have more false negatives. - if average_download_speed < tuf.conf.MIN_AVERAGE_DOWNLOAD_SPEED: - raise tuf.SlowRetrievalError(average_download_speed) - else: - logger.debug('Good average download speed: '+\ - str(average_download_speed)+' bytes/second') - else: - logger.debug('Ignoring average download speed for another: '+\ - str(-self.__seconds_spent_receiving)+' seconds') + # Then, we check whether the required length matches the reported length. + _check_content_length(reported_length, required_length, + STRICT_REQUIRED_LENGTH) + + # Download the contents of the URL, up to the required length, to a + # temporary file, and get the total number of downloaded bytes. + total_downloaded = _download_fixed_amount_of_data(connection, temp_file, + required_length) + + # Does the total number of downloaded bytes match the required length? + _check_downloaded_length(total_downloaded, required_length, + STRICT_REQUIRED_LENGTH=STRICT_REQUIRED_LENGTH) + + except: + # Close 'temp_file'; any written data is lost. + temp_file.close_temp_file() + logger.exception('Could not download URL: '+str(url)) + raise + + else: + return temp_file +def _download_fixed_amount_of_data(connection, temp_file, required_length): + """ + + This is a helper function, where the download really happens. While-block + reads data from connection a fixed chunk of data at a time, or less, until + 'required_length' is reached. + + + connection: + The object that the _open_connection returns for communicating with the + server about the contents of a URL. - def read(self, size): - """ - - We override the ancestor read (socket._fileobject.read) operation to be a - non-blocking operation. + temp_file: + A temporary file where the contents at the URL specified by the + 'connection' object will be stored. - Original code is at: - http://hg.python.org/cpython/file/5be3fa83d436/Lib/socket.py#l336 + required_length: + The number of bytes that we must download for the file. This is almost + always specified by the TUF metadata for the data file in question + (except in the case of timestamp metadata, in which case we would fix a + reasonable upper bound). + + + Data from the server will be written to 'temp_file'. + + + Runtime or network exceptions will be raised without question. + + + total_downloaded: + The total number of bytes downloaded for the desired file. + """ + + # Tolerate servers with a slow start by ignoring their delivery speed for + # 'tuf.conf.SLOW_START_GRACE_PERIOD' seconds. Set 'seconds_spent_receiving' + # to negative SLOW_START_GRACE_PERIOD seconds, and begin checking the average + # download speed once it is positive. + seconds_spent_receiving = -tuf.conf.SLOW_START_GRACE_PERIOD - - size: - The length of the data chunk that we would like to download. We assume - that the size of the expected data chunk is accurate; otherwise, we are - liable to miscount the number of truly slowly-retrieved chunks. + # Keep track of total bytes downloaded. + number_of_bytes_received = 0 - - tuf.SlowRetrievalError, in case we detect a slow-retrieval attack. - - Any other exception thrown by socket._fileobject.read. - - - None. - - - Received data up to 'size' bytes. - """ - - # We should never try to specify a negative size. - assert size >= 0 - - # Use max, disallow tiny reads in a loop as they are very inefficient. - # We never leave read() with any leftover data from a new recv() call - # in our internal buffer. - rbufsize = max(self._rbufsize, self.default_bufsize) - # Our use of StringIO rather than lists of string objects returned by - # recv() minimizes memory usage and fragmentation that occurs when - # rbufsize is large compared to the typical return value of recv(). - buf = self._rbuf - buf.seek(0, 2) # seek end - - # Read until size bytes or EOF seen, whichever comes first - buf_len = buf.tell() - if buf_len >= size: - # Already have size bytes in our buffer? Extract and return. - buf.seek(0) - rv = buf.read(size) - self._rbuf = six.StringIO() - self._rbuf.write(buf.read()) - return rv - - self._rbuf = six.StringIO() # reset _rbuf. we consume it via buf. - # Since we try to detect slow retrieval, this should not be an infinite loop. + try: while True: - left = size - buf_len - # recv() will malloc the amount of memory given as its - # parameter even though it often returns much less data - # than that. The returned data string is short lived - # as we copy it into a StringIO and free it. This avoids - # fragmentation issues on many platforms. - try: - self.__start_clock() - data = self._sock.recv(left) - except socket.timeout: - self.__stop_clock_and_check_speed(0) - continue - except socket.error as e: - if e.args[0] == EINTR: - self.__stop_clock_and_check_speed(0) - continue - raise + # We download a fixed chunk of data in every round. This is so that we + # can defend against slow retrieval attacks. Furthermore, we do not wish + # to download an extremely large file in one shot. + data = '' + read_amount = min(tuf.conf.CHUNK_SIZE, + required_length - number_of_bytes_received) + logger.debug('Reading next chunk...') + + start_time = timeit.default_timer() + + try: + data = connection.read(read_amount) + if len(data): + print('len data: ' + str(len(data))) + except socket.error: + pass + + stop_time = timeit.default_timer() + time_delta = stop_time - start_time + + # Measure the average download speed. + number_of_bytes_received = number_of_bytes_received + len(data) + seconds_spent_receiving = seconds_spent_receiving + time_delta + + if seconds_spent_receiving > 0: + average_download_speed = number_of_bytes_received / seconds_spent_receiving + + # If the average download speed is below a certain threshold, we flag + # this as a possible slow-retrieval attack. + if average_download_speed < tuf.conf.MIN_AVERAGE_DOWNLOAD_SPEED: + raise tuf.SlowRetrievalError(average_download_speed) + + else: + logger.debug('Good average download speed: '+\ + str(average_download_speed) + ' bytes per second') + else: - self.__stop_clock_and_check_speed(len(data)) - if not data: + logger.debug('Ignoring average download speed for another: '+\ + str(-seconds_spent_receiving) + ' seconds') + + # We might have no more data to read. Check number of bytes downloaded. + if not data and number_of_bytes_received == required_length: + message = 'Downloaded '+str(number_of_bytes_received)+'/'+ \ + str(required_length)+' bytes.' + logger.debug(message) + + # Finally, we signal that the download is complete. break - n = len(data) - if n == size and not buf_len: - # Shortcut. Avoid buffer data copies when: - # - We have no data in our buffer. - # AND - # - Our call to recv returned exactly the - # number of bytes we were asked to read. - return data - if n == left: - buf.write(data) - del data # explicit free - break - assert n <= left, "recv(%d) returned %d bytes" % (left, n) - buf.write(data) - buf_len += n - del data # explicit free - #assert buf_len == buf.tell() - return buf.getvalue() - - - - -class SaferHTTPResponse(six.moves.http_client.HTTPResponse): - """A safer version of httplib.HTTPResponse, in which we only use safe socket - file-like objects.""" - - def __init__(self, sock, debuglevel=0, strict=0, method=None, - buffering=False): - six.moves.http_client.HTTPResponse.__init__(self, sock, debuglevel, strict, - method) - - # Delete the previous socket file-like object... - del self.fp - # ...and replace it with our safer version. - if buffering: - self.fp = SaferSocketFileObject(sock._sock, 'rb') - - else: - self.fp = SaferSocketFileObject(sock._sock, 'rb', 0) - - - - - -class VerifiedHTTPSConnection(six.moves.http_client.HTTPSConnection): - """ - A connection that wraps connections with ssl certificate verification. - - https://github.com/pypa/pip/blob/d0fa66ecc03ab20b7411b35f7c7b423f31f77761/pip/download.py#L72 - """ - - def connect(self): - - self.connection_kwargs = {} - - #TODO: refactor compatibility logic into tuf.compatibility? - - # for > py2.5 - if hasattr(self, 'timeout'): - self.connection_kwargs.update(timeout = self.timeout) - - # for >= py2.7 - if hasattr(self, 'source_address'): - self.connection_kwargs.update(source_address = self.source_address) - - sock = socket.create_connection((self.host, self.port), **self.connection_kwargs) - - # for >= py2.7 - if getattr(self, '_tunnel_host', None): - self.sock = sock - self._tunnel() - - # set location of certificate authorities - assert os.path.isfile( tuf.conf.ssl_certificates ) - cert_path = tuf.conf.ssl_certificates - - # TODO: Disallow SSLv2. - # http://docs.python.org/dev/library/ssl.html#protocol-versions - # TODO: Select the right ciphers. - # http://docs.python.org/dev/library/ssl.html#cipher-selection - self.sock = ssl.wrap_socket(sock, self.key_file, self.cert_file, - cert_reqs=ssl.CERT_REQUIRED, - ca_certs=cert_path) - - match_hostname(self.sock.getpeercert(), self.host) - - - - - -class VerifiedHTTPSHandler(six.moves.urllib.request.HTTPSHandler): - """ - A HTTPSHandler that uses our own VerifiedHTTPSConnection. - - https://github.com/pypa/pip/blob/d0fa66ecc03ab20b7411b35f7c7b423f31f77761/pip/download.py#L109 - """ - - def __init__(self, connection_class = VerifiedHTTPSConnection): - self.specialized_conn_class = connection_class - six.moves.urllib.request.HTTPSHandler.__init__(self) - - def https_open(self, req): - return self.do_open(self.specialized_conn_class, req) + # Data successfully read from the connection. Store it. + temp_file.write(data) + + except: + raise + + else: + return number_of_bytes_received + + finally: + # Whatever happens, make sure that we always close the connection. + connection.close() @@ -394,6 +295,7 @@ def _get_opener(scheme=None): for handler in opener.handlers: if isinstance(handler, six.moves.urllib.request.HTTPHandler): opener.handlers.remove(handler) + else: # Otherwise, use the default opener. opener = six.moves.urllib.request.build_opener() @@ -440,81 +342,8 @@ def _open_connection(url): parsed_url = six.moves.urllib.parse.urlparse(url) opener = _get_opener(scheme=parsed_url.scheme) request = _get_request(url) - return opener.open(request) - - - - - -def _download_fixed_amount_of_data(connection, temp_file, required_length): - """ - - This is a helper function, where the download really happens. While-block - reads data from connection a fixed chunk of data at a time, or less, until - 'required_length' is reached. - - connection: - The object that the _open_connection returns for communicating with the - server about the contents of a URL. - - temp_file: - A temporary file where the contents at the URL specified by the - 'connection' object will be stored. - - required_length: - The number of bytes that we must download for the file. This is almost - always specified by the TUF metadata for the data file in question - (except in the case of timestamp metadata, in which case we would fix a - reasonable upper bound). - - - Data from the server will be written to 'temp_file'. - - - Runtime or network exceptions will be raised without question. - - - total_downloaded: - The total number of bytes we have downloaded for the desired file and - which should be equal to 'required_length'. - """ - - # Keep track of total bytes downloaded. - total_downloaded = 0 - - try: - while True: - # We download a fixed chunk of data in every round. This is so that we - # can defend against slow retrieval attacks. Furthermore, we do not wish - # to download an extremely large file in one shot. - amount_to_read = min(tuf.conf.CHUNK_SIZE, - required_length-total_downloaded) - logger.debug('Reading next chunk...') - data = connection.read(amount_to_read) - - # We might have no more data to read. Check number of bytes downloaded. - if not data: - message = 'Downloaded '+str(total_downloaded)+'/'+ \ - str(required_length)+' bytes.' - logger.debug(message) - - # Finally, we signal that the download is complete. - break - - # Data successfully read from the connection. Store it. - temp_file.write(data) - total_downloaded = total_downloaded + len(data) - - except: - raise - - else: - return total_downloaded - - finally: - # Whatever happens, make sure that we always close the connection. - connection.close() + return opener.open(request, timeout = tuf.conf.SOCKET_TIMEOUT) @@ -545,14 +374,18 @@ def _get_content_length(connection): try: # What is the length of this document according to the HTTP spec? reported_length = connection.info().get('Content-Length') + # Try casting it as a decimal number. reported_length = int(reported_length, 10) + # Make sure that it is a nonnegative integer. assert reported_length > -1 + except: logger.exception('Could not get content length about '+str(connection)+ ' from server!') reported_length = None + finally: return reported_length @@ -682,117 +515,61 @@ def _check_downloaded_length(total_downloaded, required_length, -def safe_download(url, required_length): - return _download_file(url, required_length, STRICT_REQUIRED_LENGTH=True) - - - - -def unsafe_download(url, required_length): - return _download_file(url, required_length, STRICT_REQUIRED_LENGTH=False) - - - - - -def _download_file(url, required_length, STRICT_REQUIRED_LENGTH=True): +class VerifiedHTTPSConnection(six.moves.http_client.HTTPSConnection): """ - - Given the url, hashes and length of the desired file, this function - opens a connection to 'url' and downloads the file while ensuring its - length and hashes match 'required_hashes' and 'required_length'. - - tuf.util.TempFile is used instead of regular tempfile object because of - additional functionality provided by 'tuf.util.TempFile'. - - - url: - A URL string that represents the location of the file. - - required_length: - An integer value representing the length of the file. + A connection that wraps connections with ssl certificate verification. - STRICT_REQUIRED_LENGTH: - A Boolean indicator used to signal whether we should perform strict - checking of required_length. True by default. We explicitly set this to - False when we know that we want to turn this off for downloading the - timestamp metadata, which has no signed required_length. - - - A 'tuf.util.TempFile' object is created on disk to store the contents of - 'url'. - - - tuf.DownloadLengthMismatchError, if there was a mismatch of observed vs - expected lengths while downloading the file. - - tuf.FormatError, if any of the arguments are improperly formatted. - - Any other unforeseen runtime exception. - - - A 'tuf.util.TempFile' file-like object which points to the contents of - 'url'. + https://github.com/pypa/pip/blob/d0fa66ecc03ab20b7411b35f7c7b423f31f77761/pip/download.py#L72 """ - # Do all of the arguments have the appropriate format? - # Raise 'tuf.FormatError' if there is a mismatch. - tuf.formats.URL_SCHEMA.check_match(url) - tuf.formats.LENGTH_SCHEMA.check_match(required_length) + def connect(self): - # 'url.replace()' is for compatibility with Windows-based systems because - # they might put back-slashes in place of forward-slashes. This converts it - # to the common format. - url = url.replace('\\', '/') - logger.info('Downloading: '+str(url)) + self.connection_kwargs = {} - # NOTE: Not thread-safe. - # Save current values or functions for restoration later. - previous_socket_timeout = socket.getdefaulttimeout() - previous_http_response_class = six.moves.http_client.HTTPConnection.response_class + # for > py2.5 + if hasattr(self, 'timeout'): + self.connection_kwargs.update(timeout = self.timeout) - # This is the temporary file that we will return to contain the contents of - # the downloaded file. - temp_file = tuf.util.TempFile() + # for >= py2.7 + if hasattr(self, 'source_address'): + self.connection_kwargs.update(source_address = self.source_address) - try: - # NOTE: Not thread-safe. - # met timeout to induce non-blocking socket operations. - socket.setdefaulttimeout(tuf.conf.SOCKET_TIMEOUT) - # Replace the socket file-like object class with our safer version. - six.moves.http_client.HTTPConnection.response_class = SaferHTTPResponse + sock = socket.create_connection((self.host, self.port), **self.connection_kwargs) - # Open the connection to the remote file. - connection = _open_connection(url) + # for >= py2.7 + if getattr(self, '_tunnel_host', None): + self.sock = sock + self._tunnel() - # We ask the server about how big it thinks this file should be. - reported_length = _get_content_length(connection) + # set location of certificate authorities + assert os.path.isfile( tuf.conf.ssl_certificates ) + cert_path = tuf.conf.ssl_certificates - # Then, we check whether the required length matches the reported length. - _check_content_length(reported_length, required_length, - STRICT_REQUIRED_LENGTH) + # TODO: Disallow SSLv2. + # http://docs.python.org/dev/library/ssl.html#protocol-versions + # TODO: Select the right ciphers. + # http://docs.python.org/dev/library/ssl.html#cipher-selection + self.sock = ssl.wrap_socket(sock, self.key_file, self.cert_file, + cert_reqs=ssl.CERT_REQUIRED, + ca_certs=cert_path) - # Download the contents of the URL, up to the required length, to a - # temporary file, and get the total number of downloaded bytes. - total_downloaded = _download_fixed_amount_of_data(connection, temp_file, - required_length) + match_hostname(self.sock.getpeercert(), self.host) - # Does the total number of downloaded bytes match the required length? - _check_downloaded_length(total_downloaded, required_length, - STRICT_REQUIRED_LENGTH=STRICT_REQUIRED_LENGTH) - except: - # Close 'temp_file'; any written data is lost. - temp_file.close_temp_file() - logger.exception('Could not download URL: '+str(url)) - raise - else: - return temp_file - finally: - # NOTE: Not thread-safe. - # Restore previously saved values or functions. - six.moves.http_client.HTTPConnection.response_class = previous_http_response_class - socket.setdefaulttimeout(previous_socket_timeout) + +class VerifiedHTTPSHandler(six.moves.urllib.request.HTTPSHandler): + """ + A HTTPSHandler that uses our own VerifiedHTTPSConnection. + + https://github.com/pypa/pip/blob/d0fa66ecc03ab20b7411b35f7c7b423f31f77761/pip/download.py#L109 + """ + + def __init__(self, connection_class = VerifiedHTTPSConnection): + self.specialized_conn_class = connection_class + six.moves.urllib.request.HTTPSHandler.__init__(self) + + def https_open(self, req): + return self.do_open(self.specialized_conn_class, req) diff --git a/tuf/repository_tool.py b/tuf/repository_tool.py index ee14bd5e..c3d48fc1 100755 --- a/tuf/repository_tool.py +++ b/tuf/repository_tool.py @@ -2629,7 +2629,7 @@ def _log_status_of_top_level_roles(targets_directory, metadata_directory): try: _check_role_keys(rolename) - except tuf.InsufficientKeysError, e: + except tuf.InsufficientKeysError as e: logger.info(str(e)) return From bc99524e2b08f79d4918ed4aa12b98b7d62e04e0 Mon Sep 17 00:00:00 2001 From: vladdd Date: Tue, 13 May 2014 12:53:50 -0400 Subject: [PATCH 07/32] Finish initial refactor of slow retrieval attack. --- tests/test_download.py | 1 + tests/test_keys.py | 2 +- tests/test_slow_retrieval_attack.py | 4 +-- tuf/conf.py | 2 +- tuf/download.py | 56 +++++++++++++++-------------- tuf/ed25519_keys.py | 4 +-- tuf/formats.py | 4 +-- tuf/keys.py | 4 +-- 8 files changed, 40 insertions(+), 37 deletions(-) diff --git a/tests/test_download.py b/tests/test_download.py index dd7e5053..0f00fbbe 100755 --- a/tests/test_download.py +++ b/tests/test_download.py @@ -118,6 +118,7 @@ def test_download_url_to_tempfileobj_and_lengths(self): # STRICT_REQUIRED_LENGTH, which is True by default, mandates that we must # download exactly what is required. self.assertRaises(tuf.DownloadLengthMismatchError, download.safe_download, + #self.assertRaises(tuf.SlowRetrievalError, download.safe_download, self.url, self.target_data_length + 1) # NOTE: However, we do not catch a tuf.DownloadLengthMismatchError here for diff --git a/tests/test_keys.py b/tests/test_keys.py index 5c800d77..dfcca83a 100755 --- a/tests/test_keys.py +++ b/tests/test_keys.py @@ -191,7 +191,7 @@ def test_verify_signature(self): # in creating the 'rsa_signature'. Function should return 'False'. # Modifying 'DATA'. - _DATA = '1111'+DATA+'1111' + _DATA = '1111' + DATA + '1111' # Verifying the 'signature' of modified '_DATA'. verified = KEYS.verify_signature(self.rsakey_dict, rsa_signature, _DATA) diff --git a/tests/test_slow_retrieval_attack.py b/tests/test_slow_retrieval_attack.py index 11696909..cb211261 100755 --- a/tests/test_slow_retrieval_attack.py +++ b/tests/test_slow_retrieval_attack.py @@ -297,7 +297,7 @@ def test_with_tuf_mode_1(self): # Verify that 'file1.txt' is the culprit. self.assertEqual(url_file, mirror_url) - self.assertTrue(isinstance(mirror_error, tuf.SlowRetrievalError)) + self.assertTrue(isinstance(mirror_error, tuf.Error)) else: self.fail('TUF did not prevent a slow retrieval attack.') @@ -330,7 +330,7 @@ def test_with_tuf_mode_2(self): # Verify that 'file1.txt' is the culprit. self.assertEqual(url_file, mirror_url) - self.assertTrue(isinstance(mirror_error, tuf.SlowRetrievalError)) + self.assertTrue(isinstance(mirror_error, tuf.DownloadLengthMismatchError)) else: # Another possibility is to check for a successfully downloaded diff --git a/tuf/conf.py b/tuf/conf.py index db381d82..0d6e2a02 100755 --- a/tuf/conf.py +++ b/tuf/conf.py @@ -67,7 +67,7 @@ MIN_AVERAGE_DOWNLOAD_SPEED = CHUNK_SIZE #bytes/second # The time (in seconds) we ignore a server with a slow initial retrieval speed. -SLOW_START_GRACE_PERIOD = 30 #seconds +SLOW_START_GRACE_PERIOD = 3 #seconds # The current "good enough" number of PBKDF2 passphrase iterations. # We recommend that important keys, such as root, be kept offline. diff --git a/tuf/download.py b/tuf/download.py index da11125a..3997d869 100755 --- a/tuf/download.py +++ b/tuf/download.py @@ -189,10 +189,12 @@ def _download_fixed_amount_of_data(connection, temp_file, required_length): # 'tuf.conf.SLOW_START_GRACE_PERIOD' seconds. Set 'seconds_spent_receiving' # to negative SLOW_START_GRACE_PERIOD seconds, and begin checking the average # download speed once it is positive. - seconds_spent_receiving = -tuf.conf.SLOW_START_GRACE_PERIOD + grace_period = -tuf.conf.SLOW_START_GRACE_PERIOD # Keep track of total bytes downloaded. number_of_bytes_received = 0 + + start_time = timeit.default_timer() try: while True: @@ -204,49 +206,49 @@ def _download_fixed_amount_of_data(connection, temp_file, required_length): required_length - number_of_bytes_received) logger.debug('Reading next chunk...') - start_time = timeit.default_timer() - try: data = connection.read(read_amount) - if len(data): - print('len data: ' + str(len(data))) + except socket.error: pass + + number_of_bytes_received = number_of_bytes_received + len(data) + + # Data successfully read from the connection. Store it. + temp_file.write(data) + + if number_of_bytes_received == required_length: + break stop_time = timeit.default_timer() - time_delta = stop_time - start_time + seconds_spent_receiving = stop_time - start_time - # Measure the average download speed. - number_of_bytes_received = number_of_bytes_received + len(data) - seconds_spent_receiving = seconds_spent_receiving + time_delta - - if seconds_spent_receiving > 0: - average_download_speed = number_of_bytes_received / seconds_spent_receiving - - # If the average download speed is below a certain threshold, we flag - # this as a possible slow-retrieval attack. - if average_download_speed < tuf.conf.MIN_AVERAGE_DOWNLOAD_SPEED: - raise tuf.SlowRetrievalError(average_download_speed) - - else: - logger.debug('Good average download speed: '+\ - str(average_download_speed) + ' bytes per second') - - else: + if (seconds_spent_receiving + grace_period) < 0: logger.debug('Ignoring average download speed for another: '+\ str(-seconds_spent_receiving) + ' seconds') + continue + + # Measure the average download speed. + average_download_speed = number_of_bytes_received / seconds_spent_receiving + + # If the average download speed is below a certain threshold, we flag + # this as a possible slow-retrieval attack. + if average_download_speed < tuf.conf.MIN_AVERAGE_DOWNLOAD_SPEED: + #raise tuf.SlowRetrievalError(average_download_speed) + break + + else: + logger.debug('Good average download speed: '+\ + str(average_download_speed) + ' bytes per second') # We might have no more data to read. Check number of bytes downloaded. - if not data and number_of_bytes_received == required_length: + if not data: message = 'Downloaded '+str(number_of_bytes_received)+'/'+ \ str(required_length)+' bytes.' logger.debug(message) # Finally, we signal that the download is complete. break - - # Data successfully read from the connection. Store it. - temp_file.write(data) except: raise diff --git a/tuf/ed25519_keys.py b/tuf/ed25519_keys.py index e2af814a..af5d275b 100755 --- a/tuf/ed25519_keys.py +++ b/tuf/ed25519_keys.py @@ -256,9 +256,9 @@ def create_signature(public_key, private_key, data): message = 'The PyNaCl library and/or its dependencies unavailable.' raise tuf.UnsupportedLibraryError(message) - except (ValueError, TypeError, nacl.exceptions.CryptoError): + except (ValueError, TypeError, nacl.exceptions.CryptoError) as e: message = 'An "ed25519" signature could not be created with PyNaCl.' - raise tuf.CryptoError(message) + raise tuf.CryptoError(message + str(e)) return signature, method diff --git a/tuf/formats.py b/tuf/formats.py index bec3477b..b33fc107 100755 --- a/tuf/formats.py +++ b/tuf/formats.py @@ -1194,7 +1194,7 @@ def _canonical_string_encoder(string): string = '"%s"' % re.sub(r'(["\\])', r'\\\1', string) - return string + return string @@ -1311,7 +1311,7 @@ def encode_canonical(object, output_function=None): # Note: Implies 'output_function' is None, # otherwise results are sent to 'output_function'. if result is not None: - return ''.join(result) + return ''.join(result).encode('utf-8') diff --git a/tuf/keys.py b/tuf/keys.py index cd70c494..02e583ec 100755 --- a/tuf/keys.py +++ b/tuf/keys.py @@ -228,13 +228,13 @@ def generate_rsa_key(bits=_DEFAULT_RSA_KEY_BITS): # 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.decode(), + 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.decode() + key_value['private'] = private rsakey_dict['keytype'] = keytype rsakey_dict['keyid'] = keyid From 6b8b2399a2938dc9d9ac55e933fa9ed202202adf Mon Sep 17 00:00:00 2001 From: vladdd Date: Tue, 27 May 2014 13:55:48 -0400 Subject: [PATCH 08/32] Finish unit tests for Python2 + 3 support. All unit tests updated / running for Python 2 + 3. TODO: Fix non-Python 3 issue with util.py. --- tests/test_arbitrary_package_attack.py | 8 +- tests/test_download.py | 6 +- tests/test_endless_data_attack.py | 6 +- tests/test_extraneous_dependencies_attack.py | 2 +- tests/test_indefinite_freeze_attack.py | 2 +- tests/test_mix_and_match_attack.py | 2 +- tests/test_pycrypto_keys.py | 4 +- tests/test_repository_tool.py | 12 +- tests/test_slow_retrieval_attack.py | 10 +- tests/test_util.py | 121 ++++++++--------- tox.ini | 2 +- tuf/__init__.py | 7 + tuf/client/updater.py | 4 +- tuf/download.py | 3 +- tuf/formats.py | 8 +- tuf/keys.py | 14 +- tuf/pycrypto_keys.py | 25 ++-- tuf/repository_tool.py | 132 ++++++++----------- tuf/schema.py | 38 ++++++ tuf/unittest_toolbox.py | 2 +- 20 files changed, 213 insertions(+), 195 deletions(-) diff --git a/tests/test_arbitrary_package_attack.py b/tests/test_arbitrary_package_attack.py index 541e9714..e65582e2 100755 --- a/tests/test_arbitrary_package_attack.py +++ b/tests/test_arbitrary_package_attack.py @@ -182,7 +182,7 @@ def test_without_tuf(self): self.assertEqual(fileinfo, download_fileinfo) # Test: Download a target file that has been modified by an attacker. - with open(target_path, 'wb') as file_object: + with open(target_path, 'wt') as file_object: file_object.write('add malicious content.') length, hashes = tuf.util.get_file_details(target_path) malicious_fileinfo = tuf.formats.make_fileinfo(length, hashes) @@ -212,7 +212,7 @@ def test_with_tuf(self): # Modify 'file1.txt' and confirm that the TUF client rejects it. target_path = os.path.join(self.repository_directory, 'targets', 'file1.txt') - with open(target_path, 'wb') as file_object: + with open(target_path, 'wt') as file_object: file_object.write('add malicious content.') try: @@ -242,7 +242,7 @@ def test_with_tuf_and_metadata_tampering(self): # An attacker modifies 'file1.txt'. target_path = os.path.join(self.repository_directory, 'targets', 'file1.txt') - with open(target_path, 'wb') as file_object: + with open(target_path, 'wt') as file_object: file_object.write('add malicious content.') # An attacker also tries to add the malicious target's length and digest @@ -259,7 +259,7 @@ def test_with_tuf_and_metadata_tampering(self): tuf.formats.check_signable_object_format(metadata) with open(metadata_path, 'wb') as file_object: - json.dump(metadata, file_object, indent=1, sort_keys=True) + json.dumps(metadata, file_object, indent=1, sort_keys=True).encode('utf-8') # Verify that the malicious 'targets.json' is not downloaded. Perform # a refresh of top-level metadata to demonstrate that the malicious diff --git a/tests/test_download.py b/tests/test_download.py index 0f00fbbe..f3fbaa1f 100755 --- a/tests/test_download.py +++ b/tests/test_download.py @@ -78,7 +78,7 @@ def setUp(self): # Computing hash of target file data. m = hashlib.md5() - m.update(self.target_data) + m.update(self.target_data.encode('utf-8')) digest = m.hexdigest() self.target_hash = {'md5':digest} @@ -98,7 +98,7 @@ def test_download_url_to_tempfileobj(self): download_file = download.safe_download temp_fileobj = download_file(self.url, self.target_data_length) - self.assertEqual(self.target_data, temp_fileobj.read()) + self.assertEqual(self.target_data, temp_fileobj.read().decode('utf-8')) self.assertEqual(self.target_data_length, len(temp_fileobj.read())) temp_fileobj.close_temp_file() @@ -126,7 +126,7 @@ def test_download_url_to_tempfileobj_and_lengths(self): # STRICT_REQUIRED_LENGTH. temp_fileobj = download.unsafe_download(self.url, self.target_data_length + 1) - self.assertEqual(self.target_data, temp_fileobj.read()) + self.assertEqual(self.target_data, temp_fileobj.read().decode('utf-8')) self.assertEqual(self.target_data_length, len(temp_fileobj.read())) temp_fileobj.close_temp_file() diff --git a/tests/test_endless_data_attack.py b/tests/test_endless_data_attack.py index e98ff626..f8175657 100755 --- a/tests/test_endless_data_attack.py +++ b/tests/test_endless_data_attack.py @@ -188,7 +188,7 @@ def test_without_tuf(self): # Test: Download a target file that has been modified by an attacker with # extra data. - with open(target_path, 'r+b') as file_object: + with open(target_path, 'r+t') as file_object: original_content = file_object.read() file_object.write(original_content+('append large amount of data' * 100000)) large_length, hashes = tuf.util.get_file_details(target_path) @@ -232,7 +232,7 @@ def test_with_tuf(self): # Modify 'file1.txt' and confirm that the TUF client only downloads up to # the expected file length. - with open(target_path, 'r+b') as file_object: + with open(target_path, 'r+t') as file_object: original_content = file_object.read() file_object.write(original_content+('append large amount of data' * 10000)) @@ -256,7 +256,7 @@ def test_with_tuf(self): original_length, hashes = tuf.util.get_file_details(timestamp_path) - with open(timestamp_path, 'r+b') as file_object: + with open(timestamp_path, 'r+t') as file_object: original_content = file_object.read() file_object.write(original_content+('append large amount of data' * 10000)) diff --git a/tests/test_extraneous_dependencies_attack.py b/tests/test_extraneous_dependencies_attack.py index c43d4992..642dd405 100755 --- a/tests/test_extraneous_dependencies_attack.py +++ b/tests/test_extraneous_dependencies_attack.py @@ -185,7 +185,7 @@ def test_with_tuf(self): tuf.formats.check_signable_object_format(role1_metadata) - with open(role1_filepath, 'wb') as file_object: + with open(role1_filepath, 'wt') as file_object: json.dump(role1_metadata, file_object, indent=1, sort_keys=True) # Un-install the metadata of the top-level roles so that the client can diff --git a/tests/test_indefinite_freeze_attack.py b/tests/test_indefinite_freeze_attack.py index aceaf795..825fd093 100755 --- a/tests/test_indefinite_freeze_attack.py +++ b/tests/test_indefinite_freeze_attack.py @@ -181,7 +181,7 @@ def test_without_tuf(self): tuf.formats.check_signable_object_format(timestamp_metadata) with open(timestamp_path, 'wb') as file_object: - json.dump(timestamp_metadata, file_object, indent=1, sort_keys=True) + json.dumps(timestamp_metadata, file_object, indent=1, sort_keys=True).encode('utf-8') client_timestamp_path = os.path.join(self.client_directory, 'timestamp.json') diff --git a/tests/test_mix_and_match_attack.py b/tests/test_mix_and_match_attack.py index 02ca21fb..5435d104 100755 --- a/tests/test_mix_and_match_attack.py +++ b/tests/test_mix_and_match_attack.py @@ -207,7 +207,7 @@ def test_with_tuf(self): # Modify a 'role1.json' target file, and add it to its metadata so that a # new version is generated. - with open(file3_path, 'wb') as file_object: + with open(file3_path, 'wt') as file_object: file_object.write('update file3') repository.targets('role1').add_target(file3_path) diff --git a/tests/test_pycrypto_keys.py b/tests/test_pycrypto_keys.py index 90c58d1b..6c11bf1f 100755 --- a/tests/test_pycrypto_keys.py +++ b/tests/test_pycrypto_keys.py @@ -107,10 +107,10 @@ def test_verify_rsa_signature(self): method, public_rsa, 123) self.assertEqual(False, pycrypto.verify_rsa_signature(signature, method, - public_rsa, 'mismatched data')) + public_rsa, b'mismatched data')) mismatched_signature, method = pycrypto.create_rsa_signature(private_rsa, - 'mismatched data') + b'mismatched data') self.assertEqual(False, pycrypto.verify_rsa_signature(mismatched_signature, method, public_rsa, data)) diff --git a/tests/test_repository_tool.py b/tests/test_repository_tool.py index 799f4238..d497ee94 100755 --- a/tests/test_repository_tool.py +++ b/tests/test_repository_tool.py @@ -1441,7 +1441,7 @@ def test_import_rsa_privatekey_from_file(self): # Invalid key file argument. invalid_keyfile = os.path.join(temporary_directory, 'invalid_keyfile') with open(invalid_keyfile, 'wb') as file_object: - file_object.write('bad keyfile') + file_object.write(b'bad keyfile') self.assertRaises(tuf.CryptoError, repo_tool.import_rsa_privatekey_from_file, invalid_keyfile, 'pw') @@ -1475,7 +1475,7 @@ def test_import_rsa_publickey_from_file(self): # Invalid key file argument. invalid_keyfile = os.path.join(temporary_directory, 'invalid_keyfile') with open(invalid_keyfile, 'wb') as file_object: - file_object.write('bad keyfile') + file_object.write(b'bad keyfile') self.assertRaises(tuf.Error, repo_tool.import_rsa_publickey_from_file, invalid_keyfile) @@ -1537,7 +1537,7 @@ def test_import_ed25519_publickey_from_file(self): # Invalid key file argument. invalid_keyfile = os.path.join(temporary_directory, 'invalid_keyfile') with open(invalid_keyfile, 'wb') as file_object: - file_object.write('bad keyfile') + file_object.write(b'bad keyfile') self.assertRaises(tuf.Error, repo_tool.import_ed25519_publickey_from_file, invalid_keyfile) @@ -1571,7 +1571,7 @@ def test_import_ed25519_privatekey_from_file(self): # Invalid key file argument. invalid_keyfile = os.path.join(temporary_directory, 'invalid_keyfile') with open(invalid_keyfile, 'wb') as file_object: - file_object.write('bad keyfile') + file_object.write(b'bad keyfile') self.assertRaises(tuf.Error, repo_tool.import_ed25519_privatekey_from_file, invalid_keyfile, 'pw') @@ -1609,7 +1609,7 @@ def test_get_metadata_fileinfo(self): temporary_directory = tempfile.mkdtemp(dir=self.temporary_directory) test_filepath = os.path.join(temporary_directory, 'file.txt') - with open(test_filepath, 'wb') as file_object: + with open(test_filepath, 'wt') as file_object: file_object.write('test file') # Generate test fileinfo object. It is assumed SHA256 hashes are computed @@ -1692,7 +1692,7 @@ def test_generate_targets_metadata(self): file1_path = os.path.join(targets_directory, 'file.txt') tuf.util.ensure_parent_dir(file1_path) - with open(file1_path, 'wb') as file_object: + with open(file1_path, 'wt') as file_object: file_object.write('test file.') # Set valid generate_targets_metadata() arguments. diff --git a/tests/test_slow_retrieval_attack.py b/tests/test_slow_retrieval_attack.py index cb211261..cedb32c3 100755 --- a/tests/test_slow_retrieval_attack.py +++ b/tests/test_slow_retrieval_attack.py @@ -158,9 +158,9 @@ def setUp(self): repository = repo_tool.load_repository(self.repository_directory) file1_filepath = os.path.join(self.repository_directory, 'targets', 'file1.txt') - with open(file1_filepath, 'wb') as file_object: - file_object.write('a' * total_bytes) + data = b'a' * total_bytes + file_object.write(data) key_file = os.path.join(self.keystore_directory, 'timestamp_key') timestamp_private = repo_tool.import_rsa_privatekey_from_file(key_file, @@ -286,7 +286,7 @@ def test_with_tuf_mode_1(self): client_filepath = os.path.join(self.client_directory, 'file1.txt') try: file1_target = self.repository_updater.target('file1.txt') - self.repository_updater.download_target(file1_target, client_filepath) + self.repository_updater.download_target(file1_target, self.client_directory) # Verify that the specific 'tuf.SlowRetrievalError' exception is raised by # each mirror. @@ -297,7 +297,7 @@ def test_with_tuf_mode_1(self): # Verify that 'file1.txt' is the culprit. self.assertEqual(url_file, mirror_url) - self.assertTrue(isinstance(mirror_error, tuf.Error)) + self.assertTrue(isinstance(mirror_error, tuf.DownloadLengthMismatchError)) else: self.fail('TUF did not prevent a slow retrieval attack.') @@ -317,7 +317,7 @@ def test_with_tuf_mode_2(self): client_filepath = os.path.join(self.client_directory, 'file1.txt') try: file1_target = self.repository_updater.target('file1.txt') - self.repository_updater.download_target(file1_target, client_filepath) + self.repository_updater.download_target(file1_target, self.client_directory) # Verify that the specific 'tuf.SlowRetrievalError' exception is raised by # each mirror. 'file1.txt' should be large enough to trigger a slow diff --git a/tests/test_util.py b/tests/test_util.py index 608556a1..323e56b9 100755 --- a/tests/test_util.py +++ b/tests/test_util.py @@ -36,7 +36,7 @@ import tuf import tuf.log import tuf.hash -import tuf.util as util +import tuf.util import tuf.unittest_toolbox as unittest_toolbox import tuf._vendor.six as six @@ -47,7 +47,7 @@ class TestUtil(unittest_toolbox.Modified_TestCase): def setUp(self): unittest_toolbox.Modified_TestCase.setUp(self) - self.temp_fileobj = util.TempFile() + self.temp_fileobj = tuf.util.TempFile() @@ -71,7 +71,7 @@ def _extract_tempfile_directory(self, config_temp_dir=None): tempfile. Returns the config's temporary directory (or default temp directory) and actual directory.""" # Patching 'tuf.conf.temporary_directory'. - util.tuf.conf.temporary_directory = config_temp_dir + tuf.conf.temporary_directory = config_temp_dir if config_temp_dir is None: # 'config_temp_dir' needs to be set to default. @@ -80,10 +80,10 @@ def _extract_tempfile_directory(self, config_temp_dir=None): # Patching 'tempfile.TemporaryFile()' (by substituting # temfile.TemporaryFile() with tempfile.mkstemp()) in order to get the # directory of the stored tempfile object. - saved_tempfile_TemporaryFile = util.tempfile.NamedTemporaryFile - util.tempfile.NamedTemporaryFile = tempfile.mkstemp - _temp_fileobj = util.TempFile() - util.tempfile.NamedTemporaryFile = saved_tempfile_TemporaryFile + saved_tempfile_TemporaryFile = tuf.util.tempfile.NamedTemporaryFile + tuf.util.tempfile.NamedTemporaryFile = tempfile.mkstemp + _temp_fileobj = tuf.util.TempFile() + tuf.util.tempfile.NamedTemporaryFile = saved_tempfile_TemporaryFile junk, _tempfilepath = _temp_fileobj.temporary_file _tempfile_dir = os.path.dirname(_tempfilepath) @@ -124,8 +124,8 @@ def test_A3_tempfile_read(self): self.temp_fileobj.temporary_file = fileobj # Test: Expected input. - self.assertEqual(self.temp_fileobj.read(), '1234567890') - self.assertEqual(self.temp_fileobj.read(4), '1234') + self.assertEqual(self.temp_fileobj.read().decode('utf-8'), '1234567890') + self.assertEqual(self.temp_fileobj.read(4).decode('utf-8'), '1234') # Test: Unexpected input. for bogus_arg in ['abcd', ['abcd'], {'a':'a'}, -100]: @@ -135,8 +135,8 @@ def test_A3_tempfile_read(self): def test_A4_tempfile_write(self): data = self.random_string() - self.temp_fileobj.write(data) - self.assertEqual(data, self.temp_fileobj.read()) + self.temp_fileobj.write(data.encode('utf-8')) + self.assertEqual(data, self.temp_fileobj.read().decode('utf-8')) @@ -144,7 +144,7 @@ def test_A5_tempfile_move(self): # Destination directory to save the temporary file in. dest_temp_dir = self.make_temp_directory() dest_path = os.path.join(dest_temp_dir, self.random_string()) - self.temp_fileobj.write(self.random_string()) + self.temp_fileobj.write(self.random_string().encode('utf-8')) self.temp_fileobj.move(dest_path) self.assertTrue(dest_path) @@ -207,12 +207,13 @@ def test_A6_tempfile_decompress_temp_file_object(self): self.assertEqual(self.temp_fileobj.read(), fileobj.read()) # Checking the content of the TempFile's '_orig_file' instance. - _orig_data_file = \ - self.make_temp_data_file(data=self.temp_fileobj._orig_file.read()) - data_in_orig_file = self._decompress_file(_orig_data_file) + check_compressed_original = self.make_temp_file() + with open(check_compressed_original, 'wb') as file_object: + file_object.write(self.temp_fileobj._orig_file.read()) + data_in_orig_file = self._decompress_file(check_compressed_original) fileobj.seek(0) self.assertEqual(data_in_orig_file, fileobj.read()) - + # Try decompressing once more. self.assertRaises(tuf.Error, self.temp_fileobj.decompress_temp_file_object,'gzip') @@ -231,7 +232,7 @@ def test_B1_get_file_details(self): file_length = os.path.getsize(filepath) # Test: Expected input. - self.assertEqual(util.get_file_details(filepath), (file_length, file_hash)) + self.assertEqual(tuf.util.get_file_details(filepath), (file_length, file_hash)) # Test: Incorrect input. bogus_inputs = [self.random_string(), 1234, [self.random_string()], @@ -239,9 +240,9 @@ def test_B1_get_file_details(self): for bogus_input in bogus_inputs: if isinstance(bogus_input, six.string_types): - self.assertRaises(tuf.Error, util.get_file_details, bogus_input) + self.assertRaises(tuf.Error, tuf.util.get_file_details, bogus_input) else: - self.assertRaises(tuf.FormatError, util.get_file_details, bogus_input) + self.assertRaises(tuf.FormatError, tuf.util.get_file_details, bogus_input) @@ -251,10 +252,10 @@ def test_B2_ensure_parent_dir(self): for parent_dir in [existing_parent_dir, non_existing_parent_dir, 12, [3]]: if isinstance(parent_dir, six.string_types): - util.ensure_parent_dir(os.path.join(parent_dir, 'a.txt')) + tuf.util.ensure_parent_dir(os.path.join(parent_dir, 'a.txt')) self.assertTrue(os.path.isdir(parent_dir)) else: - self.assertRaises(tuf.FormatError, util.ensure_parent_dir, parent_dir) + self.assertRaises(tuf.FormatError, tuf.util.ensure_parent_dir, parent_dir) @@ -297,26 +298,26 @@ def test_B4_import_json(self): def test_B5_load_json_string(self): # Test normal case. data = ['a', {'b': ['c', None, 30.3, 29]}] - json_string = util.json.dumps(data) - self.assertEqual(data, util.load_json_string(json_string)) + json_string = tuf.util.json.dumps(data) + self.assertEqual(data, tuf.util.load_json_string(json_string)) # Test invalid arguments. - self.assertRaises(tuf.Error, util.load_json_string, 8) + self.assertRaises(tuf.Error, tuf.util.load_json_string, 8) invalid_json_string = {'a': tuf.FormatError} - self.assertRaises(tuf.Error, util.load_json_string, invalid_json_string) + self.assertRaises(tuf.Error, tuf.util.load_json_string, invalid_json_string) def test_B6_load_json_file(self): data = ['a', {'b': ['c', None, 30.3, 29]}] filepath = self.make_temp_file() - fileobj = open(filepath, 'wb') - util.json.dump(data, fileobj) + fileobj = open(filepath, 'wt') + tuf.util.json.dump(data, fileobj) fileobj.close() - self.assertEqual(data, util.load_json_file(filepath)) + self.assertEqual(data, tuf.util.load_json_file(filepath)) Errors = (tuf.FormatError, IOError) - for bogus_arg in ['a', 1, ['a'], {'a':'b'}]: - self.assertRaises(Errors, util.load_json_file, bogus_arg) + for bogus_arg in [b'a', 1, [b'a'], {'a':b'b'}]: + self.assertRaises(Errors, tuf.util.load_json_file, bogus_arg) @@ -330,10 +331,10 @@ def test_C1_get_target_hash(self): for filepath, target_hash in six.iteritems(expected_target_hashes): self.assertTrue(tuf.formats.RELPATH_SCHEMA.matches(filepath)) self.assertTrue(tuf.formats.HASH_SCHEMA.matches(target_hash)) - self.assertEqual(util.get_target_hash(filepath), target_hash) + self.assertEqual(tuf.util.get_target_hash(filepath), target_hash) # Test for improperly formatted argument. - self.assertRaises(tuf.FormatError, util.get_target_hash, 8) + self.assertRaises(tuf.FormatError, tuf.util.get_target_hash, 8) @@ -364,20 +365,20 @@ def test_C2_find_delegated_role(self): ] self.assertTrue(tuf.formats.ROLELIST_SCHEMA.matches(role_list)) - self.assertEqual(util.find_delegated_role(role_list, 'targets/tuf'), 1) - self.assertEqual(util.find_delegated_role(role_list, 'targets/warehouse'), 0) + self.assertEqual(tuf.util.find_delegated_role(role_list, 'targets/tuf'), 1) + self.assertEqual(tuf.util.find_delegated_role(role_list, 'targets/warehouse'), 0) # Test for non-existent role. 'find_delegated_role()' returns 'None' # if the role is not found. - self.assertEqual(util.find_delegated_role(role_list, 'targets/non-existent'), + self.assertEqual(tuf.util.find_delegated_role(role_list, 'targets/non-existent'), None) # Test improperly formatted arguments. - self.assertRaises(tuf.FormatError, util.find_delegated_role, 8, role_list) - self.assertRaises(tuf.FormatError, util.find_delegated_role, 8, 'targets/tuf') + self.assertRaises(tuf.FormatError, tuf.util.find_delegated_role, 8, role_list) + self.assertRaises(tuf.FormatError, tuf.util.find_delegated_role, 8, 'targets/tuf') # Test duplicate roles. role_list.append(role_list[1]) - self.assertRaises(tuf.RepositoryError, util.find_delegated_role, role_list, + self.assertRaises(tuf.RepositoryError, tuf.util.find_delegated_role, role_list, 'targets/tuf') # Test missing 'name' attribute (optional, but required by @@ -385,7 +386,7 @@ def test_C2_find_delegated_role(self): # Delete the duplicate role, and the remaining role's 'name' attribute. del role_list[2] del role_list[0]['name'] - self.assertRaises(tuf.RepositoryError, util.find_delegated_role, role_list, + self.assertRaises(tuf.RepositoryError, tuf.util.find_delegated_role, role_list, 'targets/warehouse') @@ -398,38 +399,38 @@ def test_C3_paths_are_consistent_with_hash_prefixes(self): # Ensure the paths of 'list_of_targets' each have the epected path hash # prefix listed in 'path_hash_prefixes'. for filepath in list_of_targets: - self.assertTrue(util.get_target_hash(filepath)[0:4] in path_hash_prefixes) + self.assertTrue(tuf.util.get_target_hash(filepath)[0:4] in path_hash_prefixes) - self.assertTrue(util.paths_are_consistent_with_hash_prefixes(list_of_targets, + self.assertTrue(tuf.util.paths_are_consistent_with_hash_prefixes(list_of_targets, path_hash_prefixes)) extra_invalid_prefix = ['e3a3', '8fae', 'd543', '0000'] - self.assertTrue(util.paths_are_consistent_with_hash_prefixes(list_of_targets, + self.assertTrue(tuf.util.paths_are_consistent_with_hash_prefixes(list_of_targets, extra_invalid_prefix)) # Test improperly formatted arguments. self.assertRaises(tuf.FormatError, - util.paths_are_consistent_with_hash_prefixes, 8, + tuf.util.paths_are_consistent_with_hash_prefixes, 8, path_hash_prefixes) self.assertRaises(tuf.FormatError, - util.paths_are_consistent_with_hash_prefixes, + tuf.util.paths_are_consistent_with_hash_prefixes, list_of_targets, 8) self.assertRaises(tuf.FormatError, - util.paths_are_consistent_with_hash_prefixes, + tuf.util.paths_are_consistent_with_hash_prefixes, list_of_targets, ['zza1']) # Test invalid list of targets. bad_target_path = '/file5.txt' - self.assertTrue(util.get_target_hash(bad_target_path)[0:4] not in + self.assertTrue(tuf.util.get_target_hash(bad_target_path)[0:4] not in path_hash_prefixes) - self.assertFalse(util.paths_are_consistent_with_hash_prefixes([bad_target_path], + self.assertFalse(tuf.util.paths_are_consistent_with_hash_prefixes([bad_target_path], path_hash_prefixes)) # Add invalid target path to 'list_of_targets'. list_of_targets.append(bad_target_path) - self.assertFalse(util.paths_are_consistent_with_hash_prefixes(list_of_targets, + self.assertFalse(tuf.util.paths_are_consistent_with_hash_prefixes(list_of_targets, path_hash_prefixes)) @@ -463,42 +464,42 @@ def test_C4_ensure_all_targets_allowed(self): } self.assertTrue(tuf.formats.DELEGATIONS_SCHEMA.matches(parent_delegations)) - util.ensure_all_targets_allowed(rolename, list_of_targets, + tuf.util.ensure_all_targets_allowed(rolename, list_of_targets, parent_delegations) # The target files of 'targets' are always allowed. 'list_of_targets' and # 'parent_delegations' are not checked in this case. - util.ensure_all_targets_allowed('targets', list_of_targets, + tuf.util.ensure_all_targets_allowed('targets', list_of_targets, parent_delegations) # Test improperly formatted arguments. - self.assertRaises(tuf.FormatError, util.ensure_all_targets_allowed, + self.assertRaises(tuf.FormatError, tuf.util.ensure_all_targets_allowed, 8, list_of_targets, parent_delegations) - self.assertRaises(tuf.FormatError, util.ensure_all_targets_allowed, + self.assertRaises(tuf.FormatError, tuf.util.ensure_all_targets_allowed, rolename, 8, parent_delegations) - self.assertRaises(tuf.FormatError, util.ensure_all_targets_allowed, + self.assertRaises(tuf.FormatError, tuf.util.ensure_all_targets_allowed, rolename, list_of_targets, 8) # Test for invalid 'rolename', which has not been delegated by its parent, # 'targets'. - self.assertRaises(tuf.RepositoryError, util.ensure_all_targets_allowed, + self.assertRaises(tuf.RepositoryError, tuf.util.ensure_all_targets_allowed, 'targets/non-delegated_rolename', list_of_targets, parent_delegations) # Test for target file that is not allowed by the parent role. - self.assertRaises(tuf.ForbiddenTargetError, util.ensure_all_targets_allowed, + self.assertRaises(tuf.ForbiddenTargetError, tuf.util.ensure_all_targets_allowed, 'targets/warehouse', ['file8.txt'], parent_delegations) - self.assertRaises(tuf.ForbiddenTargetError, util.ensure_all_targets_allowed, + self.assertRaises(tuf.ForbiddenTargetError, tuf.util.ensure_all_targets_allowed, 'targets/warehouse', ['file1.txt', 'bad-README.txt'], parent_delegations) # Test for required attributes. # Missing 'paths' attribute. del parent_delegations['roles'][0]['paths'] - self.assertRaises(tuf.FormatError, util.ensure_all_targets_allowed, + self.assertRaises(tuf.FormatError, tuf.util.ensure_all_targets_allowed, 'targets/warehouse', list_of_targets, parent_delegations) # Test 'path_hash_prefixes' attribute. @@ -506,15 +507,15 @@ def test_C4_ensure_all_targets_allowed(self): parent_delegations['roles'][0]['path_hash_prefixes'] = path_hash_prefixes # Test normal case for 'path_hash_prefixes'. - util.ensure_all_targets_allowed('targets/warehouse', list_of_targets, + tuf.util.ensure_all_targets_allowed('targets/warehouse', list_of_targets, parent_delegations) # Test target file with a path_hash_prefix that is not allowed in its # parent role. - path_hash_prefix = util.get_target_hash('file5.txt')[0:4] + path_hash_prefix = tuf.util.get_target_hash('file5.txt')[0:4] self.assertTrue(path_hash_prefix not in parent_delegations['roles'][0] ['path_hash_prefixes']) - self.assertRaises(tuf.ForbiddenTargetError, util.ensure_all_targets_allowed, + self.assertRaises(tuf.ForbiddenTargetError, tuf.util.ensure_all_targets_allowed, 'targets/warehouse', ['file5.txt'], parent_delegations) diff --git a/tox.ini b/tox.ini index 9a3935f8..ccc991bc 100644 --- a/tox.ini +++ b/tox.ini @@ -4,7 +4,7 @@ # and then run "tox" from this directory. [tox] -envlist = py27 +envlist = py27, py33 [testenv] diff --git a/tuf/__init__.py b/tuf/__init__.py index eba0d758..a422a418 100755 --- a/tuf/__init__.py +++ b/tuf/__init__.py @@ -302,6 +302,13 @@ class InvalidNameError(Error): class UnsignedMetadataError(Error): """Indicate metadata object with insufficient threshold of signatures.""" + + def __init__(self, message, signable): + self.exception_message = message + self.signable = signable + + def __str__(self): + return self.exception_message diff --git a/tuf/client/updater.py b/tuf/client/updater.py index d2928468..0fb06c0e 100755 --- a/tuf/client/updater.py +++ b/tuf/client/updater.py @@ -905,7 +905,7 @@ def _verify_uncompressed_metadata_file(self, metadata_file_object, None. """ - metadata = metadata_file_object.read() + metadata = metadata_file_object.read().decode('utf-8') try: metadata_signable = tuf.util.load_json_string(metadata) @@ -1360,7 +1360,7 @@ def _update_metadata(self, metadata_role, uncompressed_fileinfo, # Next, move the verified updated metadata file to the 'current' directory. # Note that the 'move' method comes from tuf.util's TempFile class. # 'metadata_file_object' is an instance of tuf.util.TempFile. - metadata_signable = tuf.util.load_json_string(metadata_file_object.read()) + metadata_signable = tuf.util.load_json_string(metadata_file_object.read().decode('utf-8')) if compression == 'gzip': current_uncompressed_filepath = \ os.path.join(self.metadata_directory['current'], diff --git a/tuf/download.py b/tuf/download.py index 3997d869..57fdec91 100755 --- a/tuf/download.py +++ b/tuf/download.py @@ -201,7 +201,7 @@ def _download_fixed_amount_of_data(connection, temp_file, required_length): # We download a fixed chunk of data in every round. This is so that we # can defend against slow retrieval attacks. Furthermore, we do not wish # to download an extremely large file in one shot. - data = '' + data = b'' read_amount = min(tuf.conf.CHUNK_SIZE, required_length - number_of_bytes_received) logger.debug('Reading next chunk...') @@ -234,7 +234,6 @@ def _download_fixed_amount_of_data(connection, temp_file, required_length): # If the average download speed is below a certain threshold, we flag # this as a possible slow-retrieval attack. if average_download_speed < tuf.conf.MIN_AVERAGE_DOWNLOAD_SPEED: - #raise tuf.SlowRetrievalError(average_download_speed) break else: diff --git a/tuf/formats.py b/tuf/formats.py index b33fc107..9d5540a4 100755 --- a/tuf/formats.py +++ b/tuf/formats.py @@ -154,7 +154,7 @@ # The contents of an encrypted TUF key. Encrypted TUF keys are saved to files # in this format. -ENCRYPTEDKEY_SCHEMA = SCHEMA.AnyString() +ENCRYPTEDKEY_SCHEMA = SCHEMA.AnyBytes() # A value that is either True or False, on or off, etc. BOOLEAN_SCHEMA = SCHEMA.Boolean() @@ -177,7 +177,7 @@ NUMBINS_SCHEMA = SCHEMA.Integer(lo=1) # A PyCrypto signature. -PYCRYPTOSIGNATURE_SCHEMA = SCHEMA.AnyString() +PYCRYPTOSIGNATURE_SCHEMA = SCHEMA.AnyBytes() # An RSA key in PEM format. PEMRSA_SCHEMA = SCHEMA.AnyString() @@ -1193,7 +1193,7 @@ def _canonical_string_encoder(string): """ string = '"%s"' % re.sub(r'(["\\])', r'\\\1', string) - + return string @@ -1311,7 +1311,7 @@ def encode_canonical(object, output_function=None): # Note: Implies 'output_function' is None, # otherwise results are sent to 'output_function'. if result is not None: - return ''.join(result).encode('utf-8') + return ''.join(result) diff --git a/tuf/keys.py b/tuf/keys.py index 02e583ec..f29f2e5b 100755 --- a/tuf/keys.py +++ b/tuf/keys.py @@ -311,13 +311,13 @@ def generate_ed25519_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), + key_value = {'public': binascii.hexlify(public).decode(), 'private': ''} keyid = _get_keyid(keytype, key_value) # 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) + key_value['private'] = binascii.hexlify(private).decode() ed25519_key['keytype'] = keytype ed25519_key['keyid'] = keyid @@ -692,7 +692,7 @@ def create_signature(key_dict, data): # otherwise raise an exception. if keytype == 'rsa': if _RSA_CRYPTO_LIBRARY == 'pycrypto': - sig, method = tuf.pycrypto_keys.create_rsa_signature(private, data) + sig, method = tuf.pycrypto_keys.create_rsa_signature(private, data.encode('utf-8')) else: # pragma: no cover message = 'Unsupported "tuf.conf.RSA_CRYPTO_LIBRARY": '+\ @@ -703,7 +703,7 @@ def create_signature(key_dict, data): public = binascii.unhexlify(public) private = binascii.unhexlify(private) if 'pynacl' in _available_crypto_libraries: - sig, method = tuf.ed25519_keys.create_signature(public, private, data) + sig, method = tuf.ed25519_keys.create_signature(public, private, data.encode('utf-8')) else: # pragma: no cover message = 'The required PyNaCl library is unavailable.' @@ -717,7 +717,7 @@ def create_signature(key_dict, data): # The hexadecimal representation of 'sig' is stored in the signature. signature['keyid'] = keyid signature['method'] = method - signature['sig'] = binascii.hexlify(sig) + signature['sig'] = binascii.hexlify(sig).decode() return signature @@ -815,7 +815,7 @@ def verify_signature(key_dict, signature, data): # generated across different platforms and Python key dictionaries. The # resulting 'data' is a string encoded in UTF-8 and compatible with the input # expected by the cryptography functions called below. - data = tuf.formats.encode_canonical(data) + data = tuf.formats.encode_canonical(data).encode('utf-8') # Call the appropriate cryptography libraries for the supported key types, # otherwise raise an exception. @@ -1015,7 +1015,7 @@ def format_rsakey_from_pem(pem): # Ensure the PEM string starts with the required number of dashes. Although # a simple validation of 'pem' is performed here, a fully valid PEM string is # needed to successfully verify signatures. - if not pem.startswith(b'-----'): + if not pem.startswith('-----'): raise tuf.FormatError('The PEM string argument is improperly formatted.') # Begin building the RSA key dictionary. diff --git a/tuf/pycrypto_keys.py b/tuf/pycrypto_keys.py index 8dda5a16..780e19de 100755 --- a/tuf/pycrypto_keys.py +++ b/tuf/pycrypto_keys.py @@ -329,7 +329,7 @@ def verify_rsa_signature(signature, signature_method, public_key, data): and 'data' to complete the verification. >>> public, private = generate_rsa_public_and_private(2048) - >>> data = 'The quick brown fox jumps over the lazy dog' + >>> data = b'The quick brown fox jumps over the lazy dog' >>> signature, method = create_rsa_signature(private, data) >>> verify_rsa_signature(signature, method, public, data) True @@ -759,11 +759,11 @@ def decrypt_key(encrypted_key, password): # Decrypt 'encrypted_key', using 'password' (and additional key derivation # data like salts and password iterations) to re-derive the decryption key. - json_data = _decrypt(encrypted_key, password) + json_data = _decrypt(encrypted_key.decode('utf-8'), password) # Raise 'tuf.Error' if 'json_data' cannot be deserialized to a valid # 'tuf.formats.ANYKEY_SCHEMA' key object. - key_object = tuf.util.load_json_string(json_data) + key_object = tuf.util.load_json_string(json_data.decode()) return key_object @@ -844,7 +844,7 @@ def _encrypt(key_data, derived_key_information): # encryption. iv = Crypto.Random.new().read(16) stateful_counter_128bit_blocks = Crypto.Util.Counter.new(128, - initial_value=long(iv.encode('hex'), 16)) + initial_value=int(binascii.hexlify(iv), 16)) symmetric_key = derived_key_information['derived_key'] aes_cipher = Crypto.Cipher.AES.new(symmetric_key, Crypto.Cipher.AES.MODE_CTR, @@ -870,7 +870,7 @@ def _encrypt(key_data, derived_key_information): hmac_object = Crypto.Hash.HMAC.new(symmetric_key, ciphertext, Crypto.Hash.SHA256) hmac = hmac_object.hexdigest() - + # Store the number of PBKDF2 iterations used to derive the symmetric key so # that the decryption routine can regenerate the symmetric key successfully. # The pbkdf2 iterations are allowed to vary for the keys loaded and saved. @@ -881,11 +881,11 @@ def _encrypt(key_data, derived_key_information): # '_ENCRYPTION_DELIMITER' to make extraction easier. This delimiter is # arbitrarily chosen and should not occur in the hexadecimal representations # of the fields it is separating. - return binascii.hexlify(salt) + _ENCRYPTION_DELIMITER + \ - binascii.hexlify(str(iterations)) + _ENCRYPTION_DELIMITER + \ - binascii.hexlify(hmac) + _ENCRYPTION_DELIMITER + \ - binascii.hexlify(iv) + _ENCRYPTION_DELIMITER + \ - binascii.hexlify(ciphertext) + return binascii.hexlify(salt).decode() + _ENCRYPTION_DELIMITER + \ + str(iterations) + _ENCRYPTION_DELIMITER + \ + hmac + _ENCRYPTION_DELIMITER + \ + binascii.hexlify(iv).decode() + _ENCRYPTION_DELIMITER + \ + binascii.hexlify(ciphertext).decode() @@ -913,8 +913,7 @@ def _decrypt(file_contents, password): # Ensure we have the expected raw data for the delimited cryptographic data. salt = binascii.unhexlify(salt) - iterations = int(binascii.unhexlify(iterations)) - hmac = binascii.unhexlify(hmac) + iterations = int(iterations) iv = binascii.unhexlify(iv) ciphertext = binascii.unhexlify(ciphertext) @@ -936,7 +935,7 @@ def _decrypt(file_contents, password): # The following decryption routine assumes 'ciphertext' was encrypted with # AES-256. stateful_counter_128bit_blocks = Crypto.Util.Counter.new(128, - initial_value=long(iv.encode('hex'), 16)) + initial_value=int(binascii.hexlify(iv), 16)) aes_cipher = Crypto.Cipher.AES.new(derived_key, Crypto.Cipher.AES.MODE_CTR, counter=stateful_counter_128bit_blocks) diff --git a/tuf/repository_tool.py b/tuf/repository_tool.py index c3d48fc1..6ee8ccf5 100755 --- a/tuf/repository_tool.py +++ b/tuf/repository_tool.py @@ -266,45 +266,32 @@ def write(self, write_partial=False, consistent_snapshot=False): # sub-directory. tuf.util.ensure_parent_dir(delegated_filename) - try: - _generate_and_write_metadata(delegated_rolename, delegated_filename, - write_partial, self._targets_directory, - self._metadata_directory, - consistent_snapshot) - - # Include only the exception message. - except tuf.UnsignedMetadataError as e: - raise tuf.UnsignedMetadataError(e[0]) - + _generate_and_write_metadata(delegated_rolename, delegated_filename, + write_partial, self._targets_directory, + self._metadata_directory, + consistent_snapshot) + # Generate the 'root.json' metadata file. # _generate_and_write_metadata() raises a 'tuf.Error' exception if the # metadata cannot be written. root_filename = 'root' + METADATA_EXTENSION root_filename = os.path.join(self._metadata_directory, root_filename) - try: - signable_junk, root_filename = \ - _generate_and_write_metadata('root', root_filename, write_partial, - self._targets_directory, - self._metadata_directory, - consistent_snapshot) - - # Include only the exception message. - except tuf.UnsignedMetadataError as e: - raise tuf.UnsignedMetadataError(e[0]) + signable_junk, root_filename = \ + _generate_and_write_metadata('root', root_filename, write_partial, + self._targets_directory, + self._metadata_directory, + consistent_snapshot) + # Generate the 'targets.json' metadata file. targets_filename = 'targets' + METADATA_EXTENSION targets_filename = os.path.join(self._metadata_directory, targets_filename) - try: - signable_junk, targets_filename = \ - _generate_and_write_metadata('targets', targets_filename, write_partial, - self._targets_directory, - self._metadata_directory, - consistent_snapshot) - # Include only the exception message. - except tuf.UnsignedMetadataError as e: - raise tuf.UnsignedMetadataError(e[0]) + signable_junk, targets_filename = \ + _generate_and_write_metadata('targets', targets_filename, write_partial, + self._targets_directory, + self._metadata_directory, + consistent_snapshot) # Generate the 'snapshot.json' metadata file. snapshot_filename = os.path.join(self._metadata_directory, 'snapshot') @@ -312,31 +299,22 @@ def write(self, write_partial=False, consistent_snapshot=False): snapshot_filename = os.path.join(self._metadata_directory, snapshot_filename) filenames = {'root': root_filename, 'targets': targets_filename} snapshot_signable = None - try: - snapshot_signable, snapshot_filename = \ - _generate_and_write_metadata('snapshot', snapshot_filename, write_partial, - self._targets_directory, - self._metadata_directory, - consistent_snapshot, filenames) - - # Include only the exception message. - except tuf.UnsignedMetadataError as e: - raise tuf.UnsignedMetadataError(e[0]) + snapshot_signable, snapshot_filename = \ + _generate_and_write_metadata('snapshot', snapshot_filename, write_partial, + self._targets_directory, + self._metadata_directory, + consistent_snapshot, filenames) + # Generate the 'timestamp.json' metadata file. timestamp_filename = 'timestamp' + METADATA_EXTENSION timestamp_filename = os.path.join(self._metadata_directory, timestamp_filename) filenames = {'snapshot': snapshot_filename} - try: - _generate_and_write_metadata('timestamp', timestamp_filename, write_partial, - self._targets_directory, - self._metadata_directory, consistent_snapshot, - filenames) - # Include only the exception message. - except tuf.UnsignedMetadataError as e: - raise tuf.UnsignedMetadataError(e[0]) - + _generate_and_write_metadata('timestamp', timestamp_filename, write_partial, + self._targets_directory, + self._metadata_directory, consistent_snapshot, + filenames) # Delete the metadata of roles no longer in 'tuf.roledb'. Obsolete roles # may have been revoked and should no longer have their metadata files @@ -2293,7 +2271,7 @@ def delegate_hashed_bins(self, list_of_targets, keys_of_hashed_bins, # '{repository_root}/targets/file1.txt' -> 'file1.txt'. relative_path = target_path[len(self._targets_directory):] digest_object = tuf.hash.digest(algorithm=HASH_FUNCTION) - digest_object.update(relative_path) + digest_object.update(relative_path.encode('utf-8')) relative_path_hash = digest_object.hexdigest() relative_path_hash_prefix = relative_path_hash[:prefix_length] @@ -2426,7 +2404,7 @@ def add_target_to_bin(self, target_filepath): # '{repository_root}/targets/file1.txt' -> '/file1.txt'. relative_path = filepath[len(self._targets_directory):] digest_object = tuf.hash.digest(algorithm=HASH_FUNCTION) - digest_object.update(relative_path) + digest_object.update(relative_path.encode('utf-8')) path_hash = digest_object.hexdigest() path_hash_prefix = path_hash[:prefix_length] @@ -2477,7 +2455,7 @@ def delegations(self): A list containing the Targets objects of this Targets' delegations. """ - return self._delegated_roles.values() + return list(self._delegated_roles.values()) @@ -2645,8 +2623,7 @@ def _log_status_of_top_level_roles(targets_directory, metadata_directory): # 'tuf.UnsignedMetadataError' raised if metadata contains an invalid threshold # of signatures. log the valid/threshold message, where valid < threshold. except tuf.UnsignedMetadataError as e: - signable = e[1] - _log_status('root', signable) + _log_status('root', e.signable) return # Verify the metadata of the Targets role. @@ -2657,8 +2634,7 @@ def _log_status_of_top_level_roles(targets_directory, metadata_directory): _log_status('targets', signable) except tuf.UnsignedMetadataError as e: - signable = e[1] - _log_status('targets', signable) + _log_status('targets', e.signable) return # Verify the metadata of the snapshot role. @@ -2671,8 +2647,7 @@ def _log_status_of_top_level_roles(targets_directory, metadata_directory): _log_status('snapshot', signable) except tuf.UnsignedMetadataError as e: - signable = e[1] - _log_status('snapshot', signable) + _log_status('snapshot', e.signable) return # Verify the metadata of the Timestamp role. @@ -2685,8 +2660,7 @@ def _log_status_of_top_level_roles(targets_directory, metadata_directory): _log_status('timestamp', signable) except tuf.UnsignedMetadataError as e: - signable = e[1] - _log_status('timestamp', signable) + _log_status('timestamp', e.signable) return @@ -2945,8 +2919,8 @@ def _delete_obsolete_metadata(metadata_directory, snapshot_metadata, # Delete outdated consistent snapshots. snapshot metadata includes # the file extension of roles. if consistent_snapshot and embeded_digest is not None: - file_hashes = snapshot_metadata['meta'][metadata_name_extension] \ - ['hashes'].values() + file_hashes = list(snapshot_metadata['meta'][metadata_name_extension] \ + ['hashes'].values()) if embeded_digest not in file_hashes: logger.info('Removing outdated metadata: ' + repr(metadata_path)) os.remove(metadata_path) @@ -2961,8 +2935,8 @@ def _get_written_metadata_and_digests(metadata_signable): its digest. """ - written_metadata_content = unicode(json.dumps(metadata_signable, indent=1, - sort_keys=True)) + written_metadata_content = json.dumps(metadata_signable, indent=1, + sort_keys=True).encode('utf-8') written_metadata_digests = {} for hash_algorithm in tuf.conf.REPOSITORY_HASH_ALGORITHMS: @@ -3208,7 +3182,7 @@ def load_repository(repository_directory): roleinfo['signatures'].extend(signable['signatures']) roleinfo['version'] = metadata_object['version'] roleinfo['expires'] = metadata_object['expires'] - roleinfo['paths'] = metadata_object['targets'].keys() + roleinfo['paths'] = list(metadata_object['targets'].keys()) roleinfo['delegations'] = metadata_object['delegations'] if os.path.exists(metadata_path+'.gz'): @@ -3239,7 +3213,7 @@ def load_repository(repository_directory): # log a warning here as there may be many such duplicate key warnings. # The repository maintainer should have also been made aware of the # duplicate key when it was added. - for key_metadata in metadata_object['delegations']['keys'].values(): + for key_metadata in six.itervalues(metadata_object['delegations']['keys']): key_object = tuf.keys.format_metadata_to_key(key_metadata) try: tuf.keydb.add_key(key_object) @@ -3349,7 +3323,7 @@ def _load_top_level_metadata(repository, top_level_filenames): # if 'consistent_snapshot' is True. if consistent_snapshot: snapshot_hashes = timestamp_metadata['meta'][SNAPSHOT_FILENAME]['hashes'] - snapshot_digest = random.choice(snapshot_hashes.values()) + snapshot_digest = random.choice(list(snapshot_hashes.values())) dirname, basename = os.path.split(snapshot_filename) snapshot_filename = os.path.join(dirname, snapshot_digest + '.' + basename) @@ -3382,7 +3356,7 @@ def _load_top_level_metadata(repository, top_level_filenames): # 'consistent_snapshot' is True. if consistent_snapshot: targets_hashes = snapshot_metadata['meta'][TARGETS_FILENAME]['hashes'] - targets_digest = random.choice(targets_hashes.values()) + targets_digest = random.choice(list(targets_hashes.values())) dirname, basename = os.path.split(targets_filename) targets_filename = os.path.join(dirname, targets_digest + '.' + basename) @@ -3396,7 +3370,7 @@ def _load_top_level_metadata(repository, top_level_filenames): # Update 'targets.json' in 'tuf.roledb.py' roleinfo = tuf.roledb.get_roleinfo('targets') - roleinfo['paths'] = targets_metadata['targets'].keys() + roleinfo['paths'] = list(targets_metadata['targets'].keys()) roleinfo['version'] = targets_metadata['version'] roleinfo['expires'] = targets_metadata['expires'] roleinfo['delegations'] = targets_metadata['delegations'] @@ -3408,11 +3382,11 @@ def _load_top_level_metadata(repository, top_level_filenames): _log_warning_if_expires_soon(TARGETS_FILENAME, roleinfo['expires'], TARGETS_EXPIRES_WARN_SECONDS) - + tuf.roledb.update_roleinfo('targets', roleinfo) # Add the keys specified in the delegations field of the Targets role. - for key_metadata in targets_metadata['delegations']['keys'].values(): + for key_metadata in six.itervalues(targets_metadata['delegations']['keys']): key_object = tuf.keys.format_metadata_to_key(key_metadata) # Add 'key_object' to the list of recognized keys. Keys may be shared, @@ -3537,7 +3511,7 @@ def generate_and_write_rsa_keypair(filepath, bits=DEFAULT_RSA_KEY_BITS, # Create a tempororary file, write the contents of the public key, and move # to final destination. file_object = tuf.util.TempFile() - file_object.write(public) + file_object.write(public.encode('utf-8')) # The temporary file is closed after the final move. file_object.move(filepath+'.pub') @@ -3546,7 +3520,7 @@ def generate_and_write_rsa_keypair(filepath, bits=DEFAULT_RSA_KEY_BITS, # Unlike the public key file, the private key does not have a file # extension. file_object = tuf.util.TempFile() - file_object.write(encrypted_pem) + file_object.write(encrypted_pem.encode('utf-8')) file_object.move(filepath) @@ -3606,7 +3580,7 @@ def import_rsa_privatekey_from_file(filepath, password=None): # Read the contents of 'filepath' that should be an encrypted PEM. with open(filepath, 'rb') as file_object: - encrypted_pem = file_object.read() + encrypted_pem = file_object.read().decode('utf-8') # Convert 'encrypted_pem' to 'tuf.formats.RSAKEY_SCHEMA' format. Raise # 'tuf.CryptoError' if 'encrypted_pem' is invalid. @@ -3656,10 +3630,10 @@ def import_rsa_publickey_from_file(filepath): # Read the contents of the key file that should be in PEM format and contains # the public portion of the RSA key. with open(filepath, 'rb') as file_object: - rsa_pubkey_pem = file_object.read() + rsa_pubkey_pem = file_object.read().decode('utf-8') # Convert 'rsa_pubkey_pem' to 'tuf.formats.RSAKEY_SCHEMA' format. - try: + try: rsakey_dict = tuf.keys.format_rsakey_from_pem(rsa_pubkey_pem) except tuf.FormatError as e: @@ -3744,7 +3718,7 @@ def generate_and_write_ed25519_keypair(filepath, password=None): # Create a tempororary file, write the contents of the public key, and move # to final destination. file_object = tuf.util.TempFile() - file_object.write(json.dumps(ed25519key_metadata_format)) + file_object.write(json.dumps(ed25519key_metadata_format).encode('utf-8')) # The temporary file is closed after the final move. file_object.move(filepath+'.pub') @@ -3752,7 +3726,7 @@ def generate_and_write_ed25519_keypair(filepath, password=None): # Write the encrypted key string, conformant to # 'tuf.formats.ENCRYPTEDKEY_SCHEMA', to ''. file_object = tuf.util.TempFile() - file_object.write(encrypted_key) + file_object.write(encrypted_key.encode('utf-8')) file_object.move(filepath) @@ -4237,7 +4211,7 @@ def generate_targets_metadata(targets_directory, target_files, version, filedict[relative_targetpath] = get_metadata_fileinfo(target_path) if write_consistent_targets: - for target_digest in filedict[relative_targetpath]['hashes'].values(): + for target_digest in filedict[relative_targetpath]['hashes']: dirname, basename = os.path.split(target_path) digest_filename = target_digest + '.' + basename digest_target = os.path.join(dirname, digest_filename) @@ -4613,7 +4587,7 @@ def write_metadata_file(metadata, filename, compressions, consistent_snapshot): file_content, new_digests = _get_written_metadata_and_digests(metadata) if consistent_snapshot: - for new_digest in new_digests.values(): + for new_digest in six.itervalues(new_digests): dirname, basename = os.path.split(filename) digest_and_filename = new_digest + '.' + basename consistent_filenames.append(os.path.join(dirname, digest_and_filename)) diff --git a/tuf/schema.py b/tuf/schema.py index 389668ad..50fdc1f6 100755 --- a/tuf/schema.py +++ b/tuf/schema.py @@ -202,6 +202,44 @@ def check_match(self, object): +class AnyBytes(Schema): + """ + + Matches any byte string, but not a non-byte object. This schema + can be viewed as the Any() schema applied to byte strings, but an + additional check is performed to ensure only strings are considered. + + Supported methods include + matches(): returns a Boolean result. + check_match(): raises 'tuf.FormatError' on a mismatch. + + + + >>> schema = AnyString() + >>> schema.matches(b'') + True + >>> schema.matches(b'a string') + True + >>> schema.matches(['a']) + False + >>> schema.matches(3) + False + >>> schema.matches({}) + False + """ + + def __init__(self): + pass + + + def check_match(self, object): + if not isinstance(object, six.binary_type): + raise tuf.FormatError('Expected a byte string but got '+repr(object)) + + + + + class LengthString(Schema): """ diff --git a/tuf/unittest_toolbox.py b/tuf/unittest_toolbox.py index 5e9a6be4..67ef7286 100755 --- a/tuf/unittest_toolbox.py +++ b/tuf/unittest_toolbox.py @@ -116,7 +116,7 @@ def _destroy_temp_file(): def make_temp_data_file(self, suffix='', directory=None, data = 'junk data'): """Returns an absolute path of a temp file containing data.""" temp_file_path = self.make_temp_file(suffix=suffix, directory=directory) - temp_file = open(temp_file_path, 'wb') + temp_file = open(temp_file_path, 'wt') temp_file.write(data) temp_file.close() return temp_file_path From 71b1d38c1b40fe3f36e38b8692220a272f30571d Mon Sep 17 00:00:00 2001 From: Vladimir Diaz Date: Wed, 28 May 2014 12:11:31 -0400 Subject: [PATCH 09/32] Update slow retrieval server and fix integration tests. Update the slow retrieval server, integration tests, and the download module to address issues with unexpected exceptions and bytes. --- tests/slow_retrieval_server.py | 4 ++-- tests/test_endless_data_attack.py | 11 +++++++---- tests/test_slow_retrieval_attack.py | 4 ++-- tuf/download.py | 7 +++++-- tuf/repository_tool.py | 2 +- 5 files changed, 17 insertions(+), 11 deletions(-) diff --git a/tests/slow_retrieval_server.py b/tests/slow_retrieval_server.py index 1d8fbb69..c8dfcaba 100755 --- a/tests/slow_retrieval_server.py +++ b/tests/slow_retrieval_server.py @@ -52,7 +52,7 @@ def do_GET(self): try: filepath = os.path.join(current_dir, self.path.lstrip('/')) data = None - with open(filepath, 'rb') as fileobj: + with open(filepath, 'r') as fileobj: data = fileobj.read() self.send_response(200) @@ -76,7 +76,7 @@ def do_GET(self): # 'tuf.conf.SLOW_START_GRACE_PERIOD' seconds before triggering a # potential slow retrieval error. for i in range(len(data)): - self.wfile.write(data[i]) + self.wfile.write(data[i].encode('utf-8')) time.sleep(DELAY) return diff --git a/tests/test_endless_data_attack.py b/tests/test_endless_data_attack.py index f8175657..816855ee 100755 --- a/tests/test_endless_data_attack.py +++ b/tests/test_endless_data_attack.py @@ -256,13 +256,16 @@ def test_with_tuf(self): original_length, hashes = tuf.util.get_file_details(timestamp_path) - with open(timestamp_path, 'r+t') as file_object: - original_content = file_object.read() - file_object.write(original_content+('append large amount of data' * 10000)) + with open(timestamp_path, 'r+') as file_object: + timestamp_content = tuf.util.load_json_file(timestamp_path) + large_data = 'LargeTimestamp' * 10000 + timestamp_content['signed']['_type'] = large_data + json.dump(timestamp_content, file_object, indent=1, sort_keys=True) + modified_length, hashes = tuf.util.get_file_details(timestamp_path) self.assertTrue(modified_length > original_length) - + # Does the TUF client download the upper limit of an unsafely fetched # 'timestamp.json'? 'timestamp.json' must not be greater than # 'tuf.conf.DEFAULT_TIMESTAMP_REQUIRED_LENGTH'. diff --git a/tests/test_slow_retrieval_attack.py b/tests/test_slow_retrieval_attack.py index cedb32c3..9162062d 100755 --- a/tests/test_slow_retrieval_attack.py +++ b/tests/test_slow_retrieval_attack.py @@ -159,8 +159,8 @@ def setUp(self): file1_filepath = os.path.join(self.repository_directory, 'targets', 'file1.txt') with open(file1_filepath, 'wb') as file_object: - data = b'a' * total_bytes - file_object.write(data) + data = 'a' * total_bytes + file_object.write(data.encode('utf-8')) key_file = os.path.join(self.keystore_directory, 'timestamp_key') timestamp_private = repo_tool.import_rsa_privatekey_from_file(key_file, diff --git a/tuf/download.py b/tuf/download.py index 57fdec91..26990daf 100755 --- a/tuf/download.py +++ b/tuf/download.py @@ -211,7 +211,7 @@ def _download_fixed_amount_of_data(connection, temp_file, required_length): except socket.error: pass - + number_of_bytes_received = number_of_bytes_received + len(data) # Data successfully read from the connection. Store it. @@ -253,6 +253,9 @@ def _download_fixed_amount_of_data(connection, temp_file, required_length): raise else: + # This else block returns and skips closing the connection in the finally + # block, so close the connection here. + connection.close() return number_of_bytes_received finally: @@ -490,7 +493,7 @@ def _check_downloaded_length(total_downloaded, required_length, logger.info('Downloaded '+str(total_downloaded)+' bytes out of the '+\ 'expected '+str(required_length)+ ' bytes.') else: - difference_in_bytes = abs(total_downloaded-required_length) + difference_in_bytes = abs(total_downloaded - required_length) # What we downloaded is not equal to the required length, but did we ask # for strict checking of required length? diff --git a/tuf/repository_tool.py b/tuf/repository_tool.py index 6ee8ccf5..f9461ba2 100755 --- a/tuf/repository_tool.py +++ b/tuf/repository_tool.py @@ -2936,7 +2936,7 @@ def _get_written_metadata_and_digests(metadata_signable): """ written_metadata_content = json.dumps(metadata_signable, indent=1, - sort_keys=True).encode('utf-8') + sort_keys=True).encode('utf-8') written_metadata_digests = {} for hash_algorithm in tuf.conf.REPOSITORY_HASH_ALGORITHMS: From dc167e4a273d0e3167b1f92e0098f99e41d54ff0 Mon Sep 17 00:00:00 2001 From: Vladimir Diaz Date: Thu, 29 May 2014 12:59:36 -0400 Subject: [PATCH 10/32] Address Python 3.2 byte-string compatibility issues. --- tests/test_mix_and_match_attack.py | 4 ++-- tests/test_util.py | 5 +++-- tox.ini | 2 +- tuf/download.py | 5 +++-- tuf/formats.py | 2 +- tuf/keys.py | 8 ++++---- tuf/pycrypto_keys.py | 6 +++--- tuf/repository_tool.py | 2 +- 8 files changed, 18 insertions(+), 16 deletions(-) diff --git a/tests/test_mix_and_match_attack.py b/tests/test_mix_and_match_attack.py index 5435d104..209fe6e7 100755 --- a/tests/test_mix_and_match_attack.py +++ b/tests/test_mix_and_match_attack.py @@ -208,7 +208,7 @@ def test_with_tuf(self): # Modify a 'role1.json' target file, and add it to its metadata so that a # new version is generated. with open(file3_path, 'wt') as file_object: - file_object.write('update file3') + file_object.write('This is role2\'s target file.') repository.targets('role1').add_target(file3_path) repository.write() @@ -222,7 +222,7 @@ def test_with_tuf(self): shutil.move(backup_role1, role1_path) # Verify that the TUF client detects unexpected metadata (previously valid, - # but not up-to-date with the latest snashot of the repository) and refuses + # but not up-to-date with the latest snapshot of the repository) and refuses # to continue the update process. # Refresh top-level metadata so that the client is aware of the latest # snapshot of the repository. diff --git a/tests/test_util.py b/tests/test_util.py index 323e56b9..660ef98b 100755 --- a/tests/test_util.py +++ b/tests/test_util.py @@ -65,7 +65,7 @@ def test_A1_tempfile_close_temp_file(self): def _extract_tempfile_directory(self, config_temp_dir=None): - """[Helper] Takes a directory (essentially specified in the config.py as + """[Helper] Takes a directory (essentially specified in the conf.py as 'temporary_directory') and substitutes tempfile.TemporaryFile() with tempfile.mkstemp() in order to extract actual directory of the stored tempfile. Returns the config's temporary directory (or default temp @@ -98,7 +98,8 @@ def _extract_tempfile_directory(self, config_temp_dir=None): def test_A2_tempfile_init(self): - # Goal: Verify that tempfile is stored in an appropriate temp directory. + # Goal: Verify that temporary files are stored in the appropriate temp + # directory. The location of the temporary files is set in 'tuf.conf.py'. # Test: Expected input verification. config_temp_dirs = [None, self.make_temp_directory()] diff --git a/tox.ini b/tox.ini index ccc991bc..2be4244d 100644 --- a/tox.ini +++ b/tox.ini @@ -4,7 +4,7 @@ # and then run "tox" from this directory. [tox] -envlist = py27, py33 +envlist = py27, py32, py33, py34 [testenv] diff --git a/tuf/download.py b/tuf/download.py index 26990daf..43471737 100755 --- a/tuf/download.py +++ b/tuf/download.py @@ -208,8 +208,9 @@ def _download_fixed_amount_of_data(connection, temp_file, required_length): try: data = connection.read(read_amount) - - except socket.error: + + # Python 3.2 returns 'IOError' if the remote file object has timed out. + except (socket.error, IOError): pass number_of_bytes_received = number_of_bytes_received + len(data) diff --git a/tuf/formats.py b/tuf/formats.py index 9d5540a4..8e4d868b 100755 --- a/tuf/formats.py +++ b/tuf/formats.py @@ -873,7 +873,7 @@ def parse_base64(base64_string): base64_string = base64_string + padding try: - return binascii.a2b_base64(base64_string) + return binascii.a2b_base64(base64_string.encode('utf-8')) except (TypeError, binascii.Error) as e: raise tuf.FormatError('Invalid base64 encoding: '+str(e)) diff --git a/tuf/keys.py b/tuf/keys.py index f29f2e5b..b8ba317d 100755 --- a/tuf/keys.py +++ b/tuf/keys.py @@ -700,8 +700,8 @@ def create_signature(key_dict, data): raise tuf.UnsupportedLibraryError(message) elif keytype == 'ed25519': - public = binascii.unhexlify(public) - private = binascii.unhexlify(private) + public = binascii.unhexlify(public.encode('utf-8')) + private = binascii.unhexlify(private.encode('utf-8')) if 'pynacl' in _available_crypto_libraries: sig, method = tuf.ed25519_keys.create_signature(public, private, data.encode('utf-8')) @@ -806,7 +806,7 @@ def verify_signature(key_dict, signature, data): # key_dict['keyval']['private']. method = signature['method'] sig = signature['sig'] - sig = binascii.unhexlify(sig) + sig = binascii.unhexlify(sig.encode('utf-8')) public = key_dict['keyval']['public'] keytype = key_dict['keytype'] valid_signature = False @@ -836,7 +836,7 @@ def verify_signature(key_dict, signature, data): raise tuf.UnsupportedLibraryError(message) elif keytype == 'ed25519': - public = binascii.unhexlify(public) + public = binascii.unhexlify(public.encode('utf-8')) if _ED25519_CRYPTO_LIBRARY == 'pynacl' or \ 'pynacl' in _available_crypto_libraries: valid_signature = tuf.ed25519_keys.verify_signature(public, diff --git a/tuf/pycrypto_keys.py b/tuf/pycrypto_keys.py index 780e19de..10cef481 100755 --- a/tuf/pycrypto_keys.py +++ b/tuf/pycrypto_keys.py @@ -912,10 +912,10 @@ def _decrypt(file_contents, password): raise tuf.CryptoError('Invalid encrypted file.') # Ensure we have the expected raw data for the delimited cryptographic data. - salt = binascii.unhexlify(salt) + salt = binascii.unhexlify(salt.encode('utf-8')) iterations = int(iterations) - iv = binascii.unhexlify(iv) - ciphertext = binascii.unhexlify(ciphertext) + iv = binascii.unhexlify(iv.encode('utf-8')) + ciphertext = binascii.unhexlify(ciphertext.encode('utf-8')) # Generate derived key from 'password'. The salt and iterations are specified # so that the expected derived key is regenerated correctly. Discard the old diff --git a/tuf/repository_tool.py b/tuf/repository_tool.py index f9461ba2..0fb486da 100755 --- a/tuf/repository_tool.py +++ b/tuf/repository_tool.py @@ -2564,7 +2564,7 @@ def _generate_and_write_metadata(rolename, metadata_filename, write_partial, # 'signable' contains an invalid threshold of signatures. else: - message = 'Not enough signatures for '+repr(metadata_filename) + message = 'Not enough signatures for ' + repr(metadata_filename) raise tuf.UnsignedMetadataError(message, signable) return signable, filename From 65f30a7bd86513014c3969f24b22f1670523fa1a Mon Sep 17 00:00:00 2001 From: Vladimir Diaz Date: Fri, 30 May 2014 12:47:33 -0400 Subject: [PATCH 11/32] Improve test coverage. Update unit tests for pycrypto_keys, schema, ed25519_keys, and affected modules. --- tests/test_ed25519_keys.py | 18 ++++++++ tests/test_pycrypto_keys.py | 60 ++++++++++++++++++++++--- tests/test_schema.py | 87 ++++++++++++++++++++++++++++++++++++- tuf/ed25519_keys.py | 17 +++++--- tuf/keys.py | 2 +- tuf/mirrors.py | 12 +++-- tuf/pycrypto_keys.py | 4 +- tuf/repository_tool.py | 2 +- tuf/schema.py | 5 ++- 9 files changed, 181 insertions(+), 26 deletions(-) diff --git a/tests/test_ed25519_keys.py b/tests/test_ed25519_keys.py index 4c2fc274..de4d3b17 100755 --- a/tests/test_ed25519_keys.py +++ b/tests/test_ed25519_keys.py @@ -24,6 +24,7 @@ from __future__ import unicode_literals import unittest +import os import logging import tuf @@ -84,6 +85,19 @@ def test_verify_signature(self): valid_signature = ed25519.verify_signature(public, method, signature, data) self.assertEqual(True, valid_signature) + + # Test with 'pynacl'. + valid_signature = ed25519.verify_signature(public, method, signature, data, + use_pynacl=True) + self.assertEqual(True, valid_signature) + + # Test with 'pynacl', but a bad signature is provided. + bad_signature = os.urandom(64) + valid_signature = ed25519.verify_signature(public, method, bad_signature, + data, use_pynacl=True) + self.assertEqual(False, valid_signature) + + # Check for improperly formatted arguments. self.assertRaises(tuf.FormatError, ed25519.verify_signature, 123, method, @@ -93,6 +107,10 @@ def test_verify_signature(self): self.assertRaises(tuf.FormatError, ed25519.verify_signature, public, 123, signature, data) + # Invalid signature method. + self.assertRaises(tuf.UnknownMethodError, ed25519.verify_signature, public, + 'unsupported_method', signature, data) + # Signature not a string. self.assertRaises(tuf.FormatError, ed25519.verify_signature, public, method, 123, data) diff --git a/tests/test_pycrypto_keys.py b/tests/test_pycrypto_keys.py index 6c11bf1f..a165abe2 100755 --- a/tests/test_pycrypto_keys.py +++ b/tests/test_pycrypto_keys.py @@ -73,9 +73,12 @@ def test_create_rsa_signature(self): FORMAT_ERROR_MSG) self.assertEqual('RSASSA-PSS', method) - # Check for improperly formatted argument. + # Check for improperly formatted arguments. self.assertRaises(tuf.FormatError, pycrypto.create_rsa_signature, 123, data) + + self.assertRaises(TypeError, + pycrypto.create_rsa_signature, '', data) # Check for invalid 'data'. self.assertRaises(tuf.CryptoError, @@ -102,6 +105,11 @@ def test_verify_rsa_signature(self): self.assertRaises(tuf.FormatError, pycrypto.verify_rsa_signature, 123, method, public_rsa, data) + self.assertRaises(tuf.UnknownMethodError, pycrypto.verify_rsa_signature, + signature, + 'invalid_method', + public_rsa, data) + # Check for invalid signature and data. self.assertRaises(tuf.CryptoError, pycrypto.verify_rsa_signature, signature, method, public_rsa, 123) @@ -116,7 +124,6 @@ def test_verify_rsa_signature(self): method, public_rsa, data)) - def test_create_rsa_encrypted_pem(self): global public_rsa global private_rsa @@ -142,8 +149,13 @@ def test_create_rsa_encrypted_pem(self): pycrypto.create_rsa_encrypted_pem, 1, passphrase) self.assertRaises(tuf.FormatError, pycrypto.create_rsa_encrypted_pem, private_rsa, ['pw']) - - + + self.assertRaises(tuf.CryptoError, pycrypto.create_rsa_encrypted_pem, + 'abc', passphrase) + self.assertRaises(TypeError, pycrypto.create_rsa_encrypted_pem, '', passphrase) + + + def test_create_rsa_public_and_private_from_encrypted_pem(self): global private_rsa passphrase = 'pw' @@ -198,7 +210,45 @@ def test_create_rsa_public_and_private_from_encrypted_pem(self): self.assertRaises(tuf.CryptoError, pycrypto.create_rsa_public_and_private_from_encrypted_pem, 'invalid_pem', passphrase) - + + + + def test_encrypt_key(self): + # Test for valid arguments. + global public_rsa + global private_rsa + passphrase = 'pw' + + rsa_key = {'keytype': 'rsa', + 'keyid': 'd62247f817883f593cf6c66a5a55292488d457bcf638ae03207dbbba9dbe457d', + 'keyval': {'public': public_rsa, 'private': private_rsa}} + + encrypted_rsa_key = tuf.pycrypto_keys.encrypt_key(rsa_key, passphrase) + + # Test for invalid arguments. + rsa_key['keyval']['private'] = '' + self.assertRaises(tuf.FormatError, tuf.pycrypto_keys.encrypt_key, rsa_key, + 'passphrase') + + + def test_decrypt_key(self): + # Test for valid arguments. + global public_rsa + global private_rsa + passphrase = 'pw' + + rsa_key = {'keytype': 'rsa', + 'keyid': 'd62247f817883f593cf6c66a5a55292488d457bcf638ae03207dbbba9dbe457d', + 'keyval': {'public': public_rsa, 'private': private_rsa}} + + encrypted_rsa_key = tuf.pycrypto_keys.encrypt_key(rsa_key, passphrase).encode('utf-8') + + decrypted_rsa_key = tuf.pycrypto_keys.decrypt_key(encrypted_rsa_key, passphrase) + + + # Test for invalid arguments. + self.assertRaises(tuf.CryptoError, tuf.pycrypto_keys.decrypt_key, b'bad', + passphrase) # Run the unit tests. diff --git a/tests/test_schema.py b/tests/test_schema.py index 019e008d..11c978a5 100755 --- a/tests/test_schema.py +++ b/tests/test_schema.py @@ -24,6 +24,7 @@ from __future__ import unicode_literals import unittest +import re import logging import tuf @@ -278,6 +279,11 @@ def test_Object(self): self.assertRaises(tuf.FormatError, tuf.schema.Object, a=tuf.schema.AnyString(), b=1) + # Test condition for invalid non-dict arguments. + self.assertFalse(object_schema.matches([{'a':'XYZ'}])) + self.assertFalse(object_schema.matches(8)) + + def test_Struct(self): # Test conditions for valid arguments. @@ -323,16 +329,95 @@ def test_Struct(self): def test_RegularExpression(self): - # Test conditions for valid arguments. + # Test conditions for valid arguments. + # RegularExpression(pattern, modifiers, re_object, re_name). re_schema = tuf.schema.RegularExpression('h.*d') self.assertTrue(re_schema.matches('hello world')) + # Provide a pattern that contains the trailing '$' + re_schema_2 = tuf.schema.RegularExpression(pattern='abc$', + modifiers=0, + re_object=None, + re_name='my_re') + + self.assertTrue(re_schema_2.matches('abc')) + + # Test for valid optional arguments. + compiled_re = re.compile('^[a-z].*') + re_schema_optional = tuf.schema.RegularExpression(pattern='abc', + modifiers=0, + re_object=compiled_re, + re_name='my_re') + self.assertTrue(re_schema_optional.matches('abc')) + + # Valid arguments, but the 'pattern' argument is unset (required if the + # 're_object' is 'None'.) + self.assertRaises(tuf.FormatError, tuf.schema.RegularExpression, None, 0, + None, None) + + # Valid arguments, 're_name' is unset, and 'pattern' is None. An exception + # is not raised, but 're_name' is set to 'pattern'. + re_schema_optional = tuf.schema.RegularExpression(pattern=None, + modifiers=0, + re_object=compiled_re, + re_name=None) + + self.assertTrue(re_schema_optional.matches('abc')) + self.assertTrue(re_schema_optional._re_name == 'pattern') + # Test conditions for invalid arguments. self.assertFalse(re_schema.matches('Hello World')) self.assertFalse(re_schema.matches('hello world!')) self.assertFalse(re_schema.matches([33, 'Hello'])) + self.assertRaises(tuf.FormatError, tuf.schema.RegularExpression, 8) + + + + def test_LengthString(self): + # Test conditions for valid arguments. + length_string = tuf.schema.LengthString(11) + + self.assertTrue(length_string.matches('Hello World')) + self.assertTrue(length_string.matches('Hello Marty')) + + # Test conditions for invalid arguments. + self.assertRaises(tuf.FormatError, tuf.schema.LengthString, 'hello') + + self.assertFalse(length_string.matches('hello')) + self.assertFalse(length_string.matches(8)) + + + + def test_LengthBytes(self): + # Test conditions for valid arguments. + length_bytes = tuf.schema.LengthBytes(11) + + self.assertTrue(length_bytes.matches(b'Hello World')) + self.assertTrue(length_bytes.matches(b'Hello Marty')) + + # Test conditions for invalid arguments. + self.assertRaises(tuf.FormatError, tuf.schema.LengthBytes, 'hello') + self.assertRaises(tuf.FormatError, tuf.schema.LengthBytes, True) + + self.assertFalse(length_bytes.matches(b'hello')) + self.assertFalse(length_bytes.matches(8)) + + + + def test_AnyBytes(self): + # Test conditions for valid arguments. + anybytes_schema = tuf.schema.AnyBytes() + + self.assertTrue(anybytes_schema.matches(b'')) + self.assertTrue(anybytes_schema.matches(b'a string')) + + # Test conditions for invalid arguments. + self.assertFalse(anybytes_schema.matches('a string')) + self.assertFalse(anybytes_schema.matches(['a'])) + self.assertFalse(anybytes_schema.matches(3)) + self.assertFalse(anybytes_schema.matches({'a': 'string'})) # Run the unit tests. diff --git a/tuf/ed25519_keys.py b/tuf/ed25519_keys.py index af5d275b..381dfbdf 100755 --- a/tuf/ed25519_keys.py +++ b/tuf/ed25519_keys.py @@ -85,6 +85,10 @@ # TODO: Version 0.2.3 of 'pynacl' prints: "UserWarning: reimporting '...' might # overwrite older definitions." when importing 'nacl.signing'. Suppress user # warnings temporarily (at least until this issue is fixed by PyNaCl). +# +# Note: A 'pragma: no cover' comment is intended for test 'coverage'. Lines +# or code blocks with this comment should not be flagged as uncovered. +# pynacl will always be install prior to running the unit tests. with warnings.catch_warnings(): warnings.simplefilter('ignore') try: @@ -93,7 +97,7 @@ # PyNaCl's 'cffi' dependency may raise an 'IOError' exception when importing # 'nacl.signing'. - except (ImportError, IOError): + except (ImportError, IOError): # pragma: no cover pass # The optimized pure Python implementation of ed25519 provided by TUF. If @@ -164,10 +168,9 @@ def generate_public_and_private(): # key generation. try: nacl_key = nacl.signing.SigningKey(seed) - #public = nacl_key.verify_key public = nacl_key.verify_key.encode(encoder=nacl.encoding.RawEncoder()) - except NameError: + except NameError: # pragma: no cover message = 'The PyNaCl library and/or its dependencies unavailable.' raise tuf.UnsupportedLibraryError(message) @@ -252,7 +255,7 @@ def create_signature(public_key, private_key, data): nacl_sig = nacl_key.sign(data) signature = nacl_sig.signature - except NameError: + except NameError: # pragma: no cover message = 'The PyNaCl library and/or its dependencies unavailable.' raise tuf.UnsupportedLibraryError(message) @@ -350,10 +353,9 @@ def verify_signature(public_key, method, signature, data, use_pynacl=False): try: nacl_verify_key = nacl.signing.VerifyKey(public) nacl_message = nacl_verify_key.verify(data, signature) - if nacl_message == data: - valid_signature = True + valid_signature = True - except NameError: + except NameError: # pragma: no cover message = 'The PyNaCl library and/or its dependencies unavailable.' raise tuf.UnsupportedLibraryError(message) @@ -370,6 +372,7 @@ def verify_signature(public_key, method, signature, data, use_pynacl=False): # invalid. except Exception as e: pass + else: message = 'Unsupported ed25519 signing method: '+repr(method)+'.\n'+ \ 'Supported methods: '+repr(_SUPPORTED_ED25519_SIGNING_METHODS)+'.' diff --git a/tuf/keys.py b/tuf/keys.py index b8ba317d..1b547bc3 100755 --- a/tuf/keys.py +++ b/tuf/keys.py @@ -1124,7 +1124,7 @@ def encrypt_key(key_object, password): message = 'Invalid crypto library: '+repr(_GENERAL_CRYPTO_LIBRARY)+'.' raise tuf.UnsupportedLibraryError(message) - return encrypted_key + return encrypted_key.encode('utf-8') diff --git a/tuf/mirrors.py b/tuf/mirrors.py index badaa5be..2fad086d 100755 --- a/tuf/mirrors.py +++ b/tuf/mirrors.py @@ -82,6 +82,7 @@ def get_list_of_mirrors(file_type, file_path, mirrors_dict): tuf.formats.MIRRORDICT_SCHEMA.check_match(mirrors_dict) tuf.formats.NAME_SCHEMA.check_match(file_type) + # Verify 'file_type' is supported. if file_type not in _SUPPORTED_FILE_TYPES: message = 'Invalid file_type argument. '+ \ 'Supported file types: '+repr(_SUPPORTED_FILE_TYPES) @@ -98,19 +99,16 @@ def get_list_of_mirrors(file_type, file_path, mirrors_dict): for mirror_name, mirror_info in six.iteritems(mirrors_dict): if file_type == 'meta': base = mirror_info['url_prefix']+'/'+mirror_info['metadata_path'] - - elif file_type == 'target': + + # 'file_type' == 'target'. 'file_type' should have been verified to contain + # a supported string value above (either 'meta' or 'target'). + else: targets_path = mirror_info['targets_path'] full_filepath = os.path.join(targets_path, file_path) if not in_confined_directory(full_filepath, mirror_info['confined_target_dirs']): continue base = mirror_info['url_prefix']+'/'+mirror_info['targets_path'] - - else: - message = repr(file_type)+' is not a supported file type. '+ \ - 'Supported file types: '+repr(_SUPPORTED_FILE_TYPES) - raise tuf.Error(message) # urllib.quote(string) replaces special characters in string using the %xx # escape. This is done to avoid parsing issues of the URL on the server diff --git a/tuf/pycrypto_keys.py b/tuf/pycrypto_keys.py index 10cef481..0cd75e44 100755 --- a/tuf/pycrypto_keys.py +++ b/tuf/pycrypto_keys.py @@ -275,8 +275,8 @@ def create_rsa_signature(private_key, data): 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 + # key is a NULL string if unset. Although it may be clearer to explicitly + # check that 'private_key' is not '', we can/should check for a value and not # compare identities with the 'is' keyword. Up to this point 'private_key' # has variable size and can be an empty string. if len(private_key): diff --git a/tuf/repository_tool.py b/tuf/repository_tool.py index 0fb486da..639ecbb7 100755 --- a/tuf/repository_tool.py +++ b/tuf/repository_tool.py @@ -3726,7 +3726,7 @@ def generate_and_write_ed25519_keypair(filepath, password=None): # Write the encrypted key string, conformant to # 'tuf.formats.ENCRYPTEDKEY_SCHEMA', to ''. file_object = tuf.util.TempFile() - file_object.write(encrypted_key.encode('utf-8')) + file_object.write(encrypted_key) file_object.move(filepath) diff --git a/tuf/schema.py b/tuf/schema.py index 50fdc1f6..6ae23528 100755 --- a/tuf/schema.py +++ b/tuf/schema.py @@ -215,7 +215,7 @@ class AnyBytes(Schema): - >>> schema = AnyString() + >>> schema = AnyBytes() >>> schema.matches(b'') True >>> schema.matches(b'a string') @@ -915,7 +915,8 @@ def __init__(self, pattern=None, modifiers=0, re_object=None, re_name=None): """ if not isinstance(pattern, six.string_types): - raise tuf.FormatError(repr(pattern)+' is not a string.') + if pattern is not None: + raise tuf.FormatError(repr(pattern)+' is not a string.') if re_object is None: if pattern is None: From 80ad012bc342f7936ef70e2ebe06ba9c497ecb96 Mon Sep 17 00:00:00 2001 From: Vladimir Diaz Date: Tue, 3 Jun 2014 14:28:46 -0400 Subject: [PATCH 12/32] Re-generate repository data. --- .../client/metadata/current/root.json | Bin 3778 -> 3756 bytes .../client/metadata/current/snapshot.json | Bin 1393 -> 1380 bytes .../client/metadata/current/targets.json | Bin 1951 -> 1936 bytes .../client/metadata/current/targets.json.gz | Bin 1211 -> 0 bytes .../metadata/current/targets/role1.json | Bin 983 -> 974 bytes .../client/metadata/current/timestamp.json | Bin 931 -> 924 bytes .../client/metadata/previous/root.json | Bin 3778 -> 3756 bytes .../client/metadata/previous/snapshot.json | Bin 1393 -> 1380 bytes .../client/metadata/previous/targets.json | Bin 1951 -> 1936 bytes .../client/metadata/previous/targets.json.gz | Bin 1211 -> 1202 bytes .../metadata/previous/targets/role1.json | Bin 983 -> 974 bytes .../client/metadata/previous/timestamp.json | Bin 931 -> 924 bytes tests/repository_data/generate.py | 18 +++--- tests/repository_data/keystore/delegation_key | 52 +++++++++--------- .../keystore/delegation_key.pub | 14 ++--- tests/repository_data/keystore/root_key | 52 +++++++++--------- tests/repository_data/keystore/root_key.pub | 14 ++--- tests/repository_data/keystore/snapshot_key | 52 +++++++++--------- .../repository_data/keystore/snapshot_key.pub | 14 ++--- tests/repository_data/keystore/targets_key | 52 +++++++++--------- .../repository_data/keystore/targets_key.pub | 14 ++--- tests/repository_data/keystore/timestamp_key | 52 +++++++++--------- .../keystore/timestamp_key.pub | 14 ++--- .../repository/metadata.staged/root.json | Bin 3778 -> 3756 bytes .../repository/metadata.staged/snapshot.json | Bin 1393 -> 1380 bytes .../repository/metadata.staged/targets.json | Bin 1951 -> 1936 bytes .../metadata.staged/targets.json.gz | Bin 1211 -> 1202 bytes .../metadata.staged/targets/role1.json | Bin 983 -> 974 bytes .../repository/metadata.staged/timestamp.json | Bin 931 -> 924 bytes .../repository/metadata/root.json | Bin 3778 -> 3756 bytes .../repository/metadata/snapshot.json | Bin 1393 -> 1380 bytes .../repository/metadata/targets.json | Bin 1951 -> 1936 bytes .../repository/metadata/targets.json.gz | Bin 1211 -> 1202 bytes .../repository/metadata/targets/role1.json | Bin 983 -> 974 bytes .../repository/metadata/timestamp.json | Bin 931 -> 924 bytes 35 files changed, 175 insertions(+), 173 deletions(-) delete mode 100644 tests/repository_data/client/metadata/current/targets.json.gz diff --git a/tests/repository_data/client/metadata/current/root.json b/tests/repository_data/client/metadata/current/root.json index 37993bf1f278ddb52705a2df93ea8feb3d186b57..9174375e06a09dee6d59f428d3f2fbb7d386ed54 100644 GIT binary patch literal 3756 zcmd5RP29{Y#4_;^XN(w< za8Fu*5U`1a){~xLN@9(frwLCmkU|(nC6w5c2w{Su(64{{w0AJ&CjT@TO&XI)2? z@@>g^@Z5+fA{0hMQH_Pikp_}5>0w0?Q-UW%VP!Cs8d)hc2PC0_0%<($aRL*9EXTlO zo~O8mfK%qYR00Z2s5S{k6cEn}%o9YoGSXV4oUfQL_gDeBq5^s}@jOpTsv(4oQY{iH zl>m}jPXM48NMItc&oQLrL@c8~C)}OdL54`_ z!mkM9L@VG~KrIJMxuYoQk^+&zuMQ`_e0lkY!^2ItlSsoiMLmCKc-(H{R#mBKLF3YQviPad{#fmv4;o|Mc&-N@JvVMOZhaLE?%Q%p(J8vM8!^w#I-|b2 zfnsx=|i@wXU*Z6)>xo_ua33oUyEx^fnxv8Rcc39X- zgtR0Q4v3N;^M3gzW`p{5z{ainB(#W_x@b(Y-1 zW@FrNwmy^78aDqmUr!8wqS*a>fqaKDJkpyz<>X1=QJnbqMC$6562MJ}=b8NyL z;n)OOfuvB(7%5Sregn*tk^euistz5GA5z__g@;Kq80X^+T4!2KJ6EvHJUQ*1s29P` z^4xw~T}5<0Q*zIu{%kp-%cG=`UyY;L?RLT?zSbu*6)_w(*IBL4^G>ot{n6PD@5po) zWjoEP>sZUG!~q%KPTRx%W6GPXFWR-~d$Ya4P!CnS|G13P`Z}}CbFG{Nptfp7i`#M8 z+*O@<_wWv8sC4{k5qBFgO1V-J?90&K<&AFHjKp9#suqnx?%24BcHugERNIwpcHS2iN{3M= zmi_o~tKs7k#D_;U7?OtXy9)BAAwPcfPt(J&Ke>08w0~GU!t|cF;u!yVSP6ugbe+bb z$MuaT`wC8#>pTWys-@D7lRc6c$J8Q0)H1DvBt%=!HEUzgUjSD3x4dk(vm)AGYNv(V zD_D+QLW*oy7GZtgyfRU^l9bzFdruc0+|*7;7M%{nT%LVpl+9Bj%I=}rAM8=9`{=i( z;ccBw-#&Wx$GLaV!Rowktw!-82o}|B;S@wX#Jy|OocqHr2KOC`ucP*S>m@l2+PzLK zu7#(}ac;C2p=y4gY{XE)1)PfRy*C%D<;kS-{&qaN(Qr}=?lXPBwY)w$l(x&4@*~=) zIP9I)*%(IN;aK>Bgtt9f*~xMf1L)QEx9nr~IN|70jwh!a-#hRscgH637gz{!UHjUC{Me;4Q_aF^n$@Q(dPwQJa%- zTAF2Voh|+cwsLZaTEabL0f24<2y|-dsQ|dsc@jG#nH(yQx8=6?NbPpNmp6T9WaoVUdBgZBw$4u>_j|#VCcj=ko|ljB z)?%M+_^BkH*2;}jf}4E*)RW$G>RW66opAj0>f5E~_l|sbJ^Ud@N_qY;B}rQuEz<=D&F2mcIh#YR*NPXJ2#n%a>pN2@5MgEdT%j literal 3778 zcmd5YOTQs5;EFpsPIT0U;DRCuYDfR~DZTf0bWBXl^z?n` zF~HDD$U2#MD(loQKYV-^=`LgC?W&5;pFaNn!^g)j`u_Mls7EUG$R|KC!5m3Wg;JOk zg)C+gX*$ROK#xfqNk$n^6mURz3>+(#F{czU(^Qmw{_W%K@TAJ*{PsAVxwD!3V>p|A zvuOG@oK zq0Of_XGL0+Dl303GIl8vZTaaVX2(LkIrpjVm-Gv-KYhBIW=04(Te+aU8nTl<8>}Be7=Jjm3Qm z{jm1?4RUC-8?EM@th+(?fJZAYnsnBgtA?A6ypsyb=E=wBuA6N$>#{Ni8FK^d73_K- z1>c?ObzaXUI1haPbZNxnja7|Wt9@P#2X$(#7suli)#0k|Rw3ACB*i)Idh^Dx>9-ym zLTSgF+A;R4qqlO}>rrXc`@1}0Iya2qj?=s<>~xV-$72!8^0CdtGF%CxPLP@Bp2xGR z)j8Khg1rqGIr-|LDpsP(V) zdJ*|eW&QQF{aL_NKz9$t1+{IxNFd~nG^GL(LEQ-iXTm|c!vaB&iWO8c)~l2V%yd7X z;5Wv+I`dydtebkQp5WLDpN~g89WFa@~xB zCyb7r1{`OO^t`;^8ONFOPmVLYKfLF8!nfJfEw7F-JDsDrkOkhj*}bw!+8vY!%r@=H z7fI6Y8J1u4A5Jn<`6VjH?l9~{c{9#;^KG_yMri7bH4 z8?)pdtL3zeX0yexB@H<=hP)QXusG{HsJT+Ri90{J_T^^F7Hg{YBAYdmqFiid&Lmvi zs%4G(<^=To#i~Ny3>=>6Vjo7`z_$HuHuy8d`tJStYwP5oxEy3`+M(UP~+B)2b*GgW$0PmNw22c zZ2o769@ba?uvT{vSG4 zVcy)ku%dbqu*WhSm2}b2#2O5pWZ!Kdw?|Nv`pY0k&D(QPrnmlhw25*!>(y&Vxp%pL z97kotg`I1!ejF!!IouqQt=>uZrdn9RFahwswCZv9hTw#{)5EFhvO_U65WAd#=kLo2 z8%5`9(u0jvt4u*Mn>U2sj{SW|ETdQVj5#_+-G0?=@dPHwV^>OttK-mB>v2nt@@mSK zheJ96roS6Cjp;0XbT?tT&G<7v4waF6lSQLCl&8~!ZfeN)Yc0~doo0GBaOgif-X*e} zd%telGMI;_A{~J7_}r*<9;{`4gLR0#(!%DsZ*ZTg;DG9xJBeU$-1n2+^N_AP(}(9l z82Qu7>NMLAcW$?R^~4eB?f8Ep)=S=fQD)ck@qOO-BD$_GP51jYl*(_1k=Ie=yF1)_ zH-4+im%B32nlL#ZU)s`onZ9k&zY~#fTwf1CzlZYOYvK<=DcN(l3(!*;&Ed$7x&PxHLJJN$BUIDF3cu>C$i-AAr$hMiWI;tQbWAN1-nhKq>d zFf`W~5xf9fSa-xa#w>%W7DUGFyFK0?^Kv}x0e`%Ge|!7>%@4P?U$;`hH4)bq(ilZU zQEd&70u(j8b&zOSno%s3=GwJ$r7F!zkeDp)(gkODC=Xr}3_=XNVFW2xi4@Zu7qG+R zfezle!;DHehgKa>TUJ1&%tE7q57I1>2w6>g0|cvPJb*FWKw2$JX_}ij&DNunMMYY5 z%~rs&2G3JgF})eutk=3Sv}9&ka|sujrOoAmO$Tu&W+~iwP#@iAnpa~^S8Z!lsEbxx zM4CR7A*(f+S^^Z)9&KextSEVtBje^_=c!SK6((w3p?h^$E#0Z0V~tW{Il3cdTTO*I z7HAQG2w=$O#$a`=rQ{No^g>z&vr=bRaNh5fwS$R_RuwXOeyvl}wJIZE;7YkkOHCeT zYDy)%44cl-16^P%LmsPUef{^1z8X^@fH9;cKx!LNNrUw8`jOJ~QJ~*X|c_X4RqP!rh zXnRT4+q-{WW!Um=-V2KN(7AG;XmSGHbmq7PtgEkS~w~YgmGqW7)?n5faKw~p3McgoEN?Qphb@eqEF^{RTKyzQqc09i|?LXaA X#$8)}_J(eM&8PGIPFT4~p1=GHZOLY= literal 1393 zcma)*-EJf`5QXpa6r;XomvY%!42Wfwcc*5OMO+XmlDk`K zcbCsO_0^Y~-FCXWyQcOC zk>ypqR2^(lmX8Swv~Um(YpS*3tdTMxN()0UmxfVmWvx@zf)Ty2M@P>u7fEMI;Z`Qv zvZ%~cWRzxARjo63Y$&Ctc+va;d8Q+56_^4M^C4N0X7^@9g;|GD87?@TT)-!@b(pg@ zThPYrt49EeFR81{7QzaFy1W3aV#5TQ%;M?R!qrFXrI83%i_Em8zFu^wXr{ijO``Ny z1`I4kn85~lZc}4Z8ktU3SVep1w4Oc?Qw>!Ls^Hu!gRZ&8fZ9e$S$S%iYD0Q~bT@2* z)EcZ5)`%R{Q}4u(*+DZNK_JT(#8?A(!Cc&F;RqWgW9PBr+QG;uG*+wGA60756=6z+ zY(e@)_M}L;VhLL{J0fm{w*=+sX7}@jm@Ail;pM{P_RIP6gI^fg->-+$;dp*VXY;=v z?w--z5GuTblIsV657+hM%U8+Xda1PA9*@WK`(IDT`>$_y+hLs!SN^Z;=W(aQLh6_E znAS9sDU`4enrx&l23iMaV$oz(pUJ%Y)GAQYG^L*cPAy*v$kg*haRU5CuIh8aa9^n?1uSu$jVC5Bb;Rc*=r z|7iR6@%YKh+oOwhnJF|iZ^`{a^ctFBHfz1*WX`DBNYY#onz42+z15O|W{_7-|Lxp= adOEpY)#dHEt=pgec)Giimb#yAp8f&iFK$Qx diff --git a/tests/repository_data/client/metadata/current/targets.json b/tests/repository_data/client/metadata/current/targets.json index 0a7b4a8bf43e2d63977e1804a83f4fae07449b15..e9da47bb59db2882c5dffd99a8ddc120e14b5fad 100644 GIT binary patch literal 1936 zcmbtV+iv1W6nxKDAirj3)A!4~ncyUlAwWzBWHVaz6-XF!4}nqseUD9&(XO=8N^9Ac zX*Z{C&asff~9Z8&!kGl@f>$!}G?A7L|@R|RkLG|$s7-8_GP1qjN847M6W z9g2yNgy_JnbB2WwoQV-jYzYpMaN}aYj!`EpkHnHFG1DRf1dY66l;TKS5RN({0I)Rh zNLqdU@#tam)${7gd-WL?w38dJCFaP9o!& zVc~%@Ayha}Tt%sv1(_@*PIAkbLlILFwYCIEa|{ru<%)r{KnFHi#}Ik+TuXzblL`3b z?WFb{{8E`Rlx``_nEbifFG6XNzFCdKW?lIv@nIImW;2}^rE@=B`}w1mNa39>>AR_lFBKGu2nuLnDlhlYV;hJSOfdUV_Sg;|AXaO0H12Wu#ms)qt0jxA zolRW{_Dgxxv&-g9t$SDh);zCWXQOKHv1m7&_2yH0Qy)JsC(qMMy+if1mtCZ3=REDH zw0wU)zJ}*4oedstR6%-Y_fN|~wwmVkn+;T8EsOA>Wr}U{s(#VVZ`o;s*o*x2=7~1u z%|jdAO1Jr=TixZgK2V0?4I2XM`GK`Uoy2n>$oTv^`;wacNceIKJR2tn}_~maXCWgEZ=lSuVKEsnvClG z#;`rr!})sLA4YB_`!Mh_7x&B4r7E7*ZaTr~@G{rBc`?Zhe=J75W+UyT^~a*xob0CY z&;2T$2?v<-%BK-Et#10q|&NtRM2ESF@%eB-E$SM>Wq#=Y6 z#nLOzG(wgd#GsL(Gs^&EO2Rlp-W*9Cx1^TLLNVSN-rLC?Y;St5eVh=qVcU(%u$!V)=RC&=&$N!?Q4yU&35jq@}!(7@tL2SYEk61dVeYR*Plk@ zgp-(KgMR$E+jp2RkORwFy>OLMPXZdxn*Xk|fL2LfBNuL&%`hZ&T zsQWpNZ`$!l#r5;{{0he1xa>5Wsk-`lm2Ug%?~l*>>9{+~<>hAFxw@vic=APUF5M{K zr?Ne;n_D(nO}FjQ5`X`!(;I#hKQyz;hfcHqeE)UQe7PAOnzzfrEMLgg@WEhmy=m^- zcXF~T%A#Eq#g}#dnBLw#JYLV&d209h@=iR*O}TpM<5ppta&q6DPP6tbFD6O3T876< zMao!?Z^k=2d_tvOZ$??x8(eqG7VD446&=z}M>lti(Egkjarer{y~@qwS-xh&{(!L z>a7RQ%av%w-Ax#V#V)SIb*$Vfxm|a%W1q z9>uL$ek@LR(D`S*YH`8^`h5F-Jv-aB$bOmU**@P4^vzT2cmBT=PY90faSWK7)v+h4 zQ%`)Xf#7#tu1%Hx4Zz2+$UObMdall7-a}O>8~|yN)eVQ=DSm_DiDu-V*Nf935fMn_ z2ZZ4sBlPKbPtOf`hVs@7(Q!+B-16L?jVLu`dQ8}{Um-DNY6ur~y9jQT6X2%UX-yNQ z72)8$$QTZX3YOp}Oe18eK}=~liNKR7xVwaTh7xn)buLUJ%6vYrQV2|soX+znP~<;@ zVw^ayC{i4O=TjdTBTOaA3j`-2*92p&!P69#9{iia*}w(%!D7Rl(fVIO={nK1pw21Y O`)vtUb}GT^#p_SQkq*58 diff --git a/tests/repository_data/client/metadata/current/targets.json.gz b/tests/repository_data/client/metadata/current/targets.json.gz deleted file mode 100644 index 757cff5064add4a5eb85863404672b95fe28df7c..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1211 zcmV;s1VsBEiwFR;o>L&dPnn z%d6z-DtR}$y88YMG3*`~9v%10811d*E(WWNGE^9$IpstdVjTb>5P%w$DkrQK#Dewc zWIeS3cS*Uijjy35S%h44QTH{6CkN^RBA%~j=Jt_0WmCN z)CQ*r#cxlei`-(+ukBpqyq3Im}!= zFN^BvxmWJnk47`7psA#eD2r<0MsX5RMV3a1`cSAX2@sHu@dk@yi47bSO{Hg&a&F!t z?E`;D@l?5^b`SQA%YQgFnya-)WrnZuiFf;=JE)_ZP{q zJ$rb}9_FWZOWMh}_c2K>I?321yXT$R8Q%Ajs@rO1=IrxX_B2?1y?x%?O?vl*K7E{Y z&(8TenSL^lr(s-dGTj;a$4fC?-aU233;Ok=%`W6ca?{FBZ@R6)^Y!Ox>*Zp!ZCx&g z_r+WmeL^T*bvbET*2vMM`eS$Al}Yam zl71a#$$haBqrsN-m&5H~zJ^+ECnH)_laP-_)3z$(!_s{HTn?^Zwv+y}m4$IOG-fg0 zh~Dg(=J^66Oz(Uzedw=-&x@sMC%wlwit}|+tMkN!WqP^l=B+_NI7#S3Uk?XmRjoIZ z5P#clF5&LtzL;+N-DI4!?~8qLdc*GD>(x9I_x0rM^Yu8rp33}fcs71k%RK#I{y(I6 z2=wJmc!gW;)mR^T;{6T?{h`a1tFylVcpv9E@L%ia`X8a{3>QFF=DUWYKPY}V`A)M5 z&#U=iNi-QI@*SagO$hn0zwV9=d4%%TjQb<${>ZnM%v_a0^;2V2nPXDzA}i@lP&iF# zu$HIB8YXe}66Yi?6{>Nb2uq0PjtFCM6X8teINdec1xek3*Re2-W)8)y&M+{iKNoBd ziv4F$Lb3pc6C(-Ed=60vCQNET5!{5*GD@w*nP#{KoZlRGgH#lvr%nWC?Z1MuC&xS= ZQ@nwv3P;(Y1g|Hre*$kK5ka2^000K=T{i## diff --git a/tests/repository_data/client/metadata/current/targets/role1.json b/tests/repository_data/client/metadata/current/targets/role1.json index d4d3c5603b127a4fa96c5ece27f9377b1073d62f..c5e3bc866133420448769ab01acbf4cb56d45b84 100644 GIT binary patch literal 974 zcmX|=-EJH=42AFg6pOiTT9K6ale@e@kvLST_lH044u>Ck4%^@H{XDa>3D03ZrifZYV_`SYfCGn*gF2g=?#tHpT#mRf(&^!eJz4*jQ|=VK7_Ax)g! zL|M~xM3zh|G{SDJa=5`7t#lie`qV*@W4Bp{#KbYVOr5xk78+Y}@^W|hoV?tijGdMp#_g}`_b;&{^2wj?<9fMqv(0#jd%vEa9(Qv8 zr}p;p@(SJa)59y)pLgxOw#BzE=U3J@EL7eV$mdfj?{Ci^uhQ4IK0jrDTjz(MU$5V; zKRX?^lV46Z@muFqzMMSC*vEtlOKMq-M0070UG3_v_(B{#uOj~{2%elSM^9LryAK?t lB(vQ2B;Mvt-G_L*zn)STb+2bcZ&v<|=S#ZJ8@S8z%YUrl{-FQ> literal 983 zcmX|<&u$bk494$$iq)Jc*!gF>H{JlTJ%JFi6Q{ePw5U5E5bC?**>=%n4sqho&gb9X ztKD)s-aY90^T|)k&F=HnZud9--SW-<97jZ!-a27Lg(gKPbIUMAU2s-WZG}s9?@)=2 z$*X|$>-t5kT-+etio)tgt-|zSDuRrYfm$T?$E5(By8;GL!thrZ_rU|gJ z&5 zi{_Nc`xtY$bIIAYz%)o9YR*Ok%~d3Y12byKLc5(+FS}$tu@kGXJoWA zCOHsk9VA#8^3Fc!IIDR#J6kLCEodS#krdER#$CH1<%S zQ7@6KBudE|iX~GHYX}n2MAo4(9!rk37d7j{pmL0+V6Kj!@lFHmkkL~%LaIHcUK}xO zV#~^geT0(@K(ch7$rZuSv}MCcj<1&9O;ZoWoP@)JX{aT%N-<%Ukz!gCCA{e1L6BRK z){)%Zpd|1mKzY8}ec6aPbLkr|8;{G^_0JFA7`fG_J6}&P8 z=U4Ea9`9e+{<6uRgNy(EaeN_uNn!0ZV60n!oAdhgs=mI~x+uS!$9vCj*Wc@<*ls!K z>2Q`m7mrR)2TiqYtzyARflj@`?sy2e~i zlbS9Z5@VT{*acPLDQ$GkXDXFe60f?rgJX*1j+0j*FO!{|Z5VUw3r3Mv)JA6wPng!y z4~|6ThNi1DrRK`OTAPlts#C)-BvP9+(L-zjVhI_t0yB~sjVTz3bxeUK)x?NFX@+91 zCk>(ewh0I|V)Ia=St$jZX@&5G0wL;Yvem|JMPkoW3k6JLL!gnIeK(;*2=2k)YuFMf z?h;lNRU`AO;LVyUBT{Mv>M4c?GId3CHw~3$Lx5XM4q^#*QQ$rxw0m0>g;TV)=xq>Y z{1HAAu8z{13&3hZ#SJ=yG0aqNTq?7+6=YTl#_SnvGBJ%>xel~0*rq9G!k8LpG@jwn zLx*nJlkX3wKeI1KDd*mny^Pym_pdK=ugHh%!2P~i<=ULOG7 z9oO%#FJ3@~8>iMWc%}Kwj7KI|0|cSxDKJ>uJG#3{U&l{y+WwwTAMcmM#cHr`((vnY`*(i1 MT@O+CkmK+F0~zn~cmMzZ literal 931 zcmXw2%Z?m347~4GH2NF|d`P6|oBtrl_>>?BM2Yf@lYPK531S%j_poQzfEo=@-Nh=h z>f6J%UoW4Y`t6@DaoyiNe%jxvvE;$^nXj;)Wm%27E?ZrUa zv_@*(IA5a#R>#pNGw4PiN(8;ML3gus%ll5bdR!b@b-iHyF=$^jQ?UAZZlPMVs? zn7{Vi5f$yC8VF5uPHQCPx^OnuiWnr3ga)bLbkVWwAm3d;ds8Os(U}chMPjC=?hHMP zI3%b9_wZPWT$iz68^!7|9nZ~kb6YES!JU1qwW3mLtIFEJrZFr8aw({Op9?10sHKgu zFie)GhZ=L+(RP29{Y#4_;^XN(w< za8Fu*5U`1a){~xLN@9(frwLCmkU|(nC6w5c2w{Su(64{{w0AJ&CjT@TO&XI)2? z@@>g^@Z5+fA{0hMQH_Pikp_}5>0w0?Q-UW%VP!Cs8d)hc2PC0_0%<($aRL*9EXTlO zo~O8mfK%qYR00Z2s5S{k6cEn}%o9YoGSXV4oUfQL_gDeBq5^s}@jOpTsv(4oQY{iH zl>m}jPXM48NMItc&oQLrL@c8~C)}OdL54`_ z!mkM9L@VG~KrIJMxuYoQk^+&zuMQ`_e0lkY!^2ItlSsoiMLmCKc-(H{R#mBKLF3YQviPad{#fmv4;o|Mc&-N@JvVMOZhaLE?%Q%p(J8vM8!^w#I-|b2 zfnsx=|i@wXU*Z6)>xo_ua33oUyEx^fnxv8Rcc39X- zgtR0Q4v3N;^M3gzW`p{5z{ainB(#W_x@b(Y-1 zW@FrNwmy^78aDqmUr!8wqS*a>fqaKDJkpyz<>X1=QJnbqMC$6562MJ}=b8NyL z;n)OOfuvB(7%5Sregn*tk^euistz5GA5z__g@;Kq80X^+T4!2KJ6EvHJUQ*1s29P` z^4xw~T}5<0Q*zIu{%kp-%cG=`UyY;L?RLT?zSbu*6)_w(*IBL4^G>ot{n6PD@5po) zWjoEP>sZUG!~q%KPTRx%W6GPXFWR-~d$Ya4P!CnS|G13P`Z}}CbFG{Nptfp7i`#M8 z+*O@<_wWv8sC4{k5qBFgO1V-J?90&K<&AFHjKp9#suqnx?%24BcHugERNIwpcHS2iN{3M= zmi_o~tKs7k#D_;U7?OtXy9)BAAwPcfPt(J&Ke>08w0~GU!t|cF;u!yVSP6ugbe+bb z$MuaT`wC8#>pTWys-@D7lRc6c$J8Q0)H1DvBt%=!HEUzgUjSD3x4dk(vm)AGYNv(V zD_D+QLW*oy7GZtgyfRU^l9bzFdruc0+|*7;7M%{nT%LVpl+9Bj%I=}rAM8=9`{=i( z;ccBw-#&Wx$GLaV!Rowktw!-82o}|B;S@wX#Jy|OocqHr2KOC`ucP*S>m@l2+PzLK zu7#(}ac;C2p=y4gY{XE)1)PfRy*C%D<;kS-{&qaN(Qr}=?lXPBwY)w$l(x&4@*~=) zIP9I)*%(IN;aK>Bgtt9f*~xMf1L)QEx9nr~IN|70jwh!a-#hRscgH637gz{!UHjUC{Me;4Q_aF^n$@Q(dPwQJa%- zTAF2Voh|+cwsLZaTEabL0f24<2y|-dsQ|dsc@jG#nH(yQx8=6?NbPpNmp6T9WaoVUdBgZBw$4u>_j|#VCcj=ko|ljB z)?%M+_^BkH*2;}jf}4E*)RW$G>RW66opAj0>f5E~_l|sbJ^Ud@N_qY;B}rQuEz<=D&F2mcIh#YR*NPXJ2#n%a>pN2@5MgEdT%j literal 3778 zcmd5YOTQs5;EFpsPIT0U;DRCuYDfR~DZTf0bWBXl^z?n` zF~HDD$U2#MD(loQKYV-^=`LgC?W&5;pFaNn!^g)j`u_Mls7EUG$R|KC!5m3Wg;JOk zg)C+gX*$ROK#xfqNk$n^6mURz3>+(#F{czU(^Qmw{_W%K@TAJ*{PsAVxwD!3V>p|A zvuOG@oK zq0Of_XGL0+Dl303GIl8vZTaaVX2(LkIrpjVm-Gv-KYhBIW=04(Te+aU8nTl<8>}Be7=Jjm3Qm z{jm1?4RUC-8?EM@th+(?fJZAYnsnBgtA?A6ypsyb=E=wBuA6N$>#{Ni8FK^d73_K- z1>c?ObzaXUI1haPbZNxnja7|Wt9@P#2X$(#7suli)#0k|Rw3ACB*i)Idh^Dx>9-ym zLTSgF+A;R4qqlO}>rrXc`@1}0Iya2qj?=s<>~xV-$72!8^0CdtGF%CxPLP@Bp2xGR z)j8Khg1rqGIr-|LDpsP(V) zdJ*|eW&QQF{aL_NKz9$t1+{IxNFd~nG^GL(LEQ-iXTm|c!vaB&iWO8c)~l2V%yd7X z;5Wv+I`dydtebkQp5WLDpN~g89WFa@~xB zCyb7r1{`OO^t`;^8ONFOPmVLYKfLF8!nfJfEw7F-JDsDrkOkhj*}bw!+8vY!%r@=H z7fI6Y8J1u4A5Jn<`6VjH?l9~{c{9#;^KG_yMri7bH4 z8?)pdtL3zeX0yexB@H<=hP)QXusG{HsJT+Ri90{J_T^^F7Hg{YBAYdmqFiid&Lmvi zs%4G(<^=To#i~Ny3>=>6Vjo7`z_$HuHuy8d`tJStYwP5oxEy3`+M(UP~+B)2b*GgW$0PmNw22c zZ2o769@ba?uvT{vSG4 zVcy)ku%dbqu*WhSm2}b2#2O5pWZ!Kdw?|Nv`pY0k&D(QPrnmlhw25*!>(y&Vxp%pL z97kotg`I1!ejF!!IouqQt=>uZrdn9RFahwswCZv9hTw#{)5EFhvO_U65WAd#=kLo2 z8%5`9(u0jvt4u*Mn>U2sj{SW|ETdQVj5#_+-G0?=@dPHwV^>OttK-mB>v2nt@@mSK zheJ96roS6Cjp;0XbT?tT&G<7v4waF6lSQLCl&8~!ZfeN)Yc0~doo0GBaOgif-X*e} zd%telGMI;_A{~J7_}r*<9;{`4gLR0#(!%DsZ*ZTg;DG9xJBeU$-1n2+^N_AP(}(9l z82Qu7>NMLAcW$?R^~4eB?f8Ep)=S=fQD)ck@qOO-BD$_GP51jYl*(_1k=Ie=yF1)_ zH-4+im%B32nlL#ZU)s`onZ9k&zY~#fTwf1CzlZYOYvK<=DcN(l3(!*;&Ed$7x&PxHLJJN$BUIDF3cu>C$i-AAr$hMiWI;tQbWAN1-nhKq>d zFf`W~5xf9fSa-xa#w>%W7DUGFyFK0?^Kv}x0e`%Ge|!7>%@4P?U$;`hH4)bq(ilZU zQEd&70u(j8b&zOSno%s3=GwJ$r7F!zkeDp)(gkODC=Xr}3_=XNVFW2xi4@Zu7qG+R zfezle!;DHehgKa>TUJ1&%tE7q57I1>2w6>g0|cvPJb*FWKw2$JX_}ij&DNunMMYY5 z%~rs&2G3JgF})eutk=3Sv}9&ka|sujrOoAmO$Tu&W+~iwP#@iAnpa~^S8Z!lsEbxx zM4CR7A*(f+S^^Z)9&KextSEVtBje^_=c!SK6((w3p?h^$E#0Z0V~tW{Il3cdTTO*I z7HAQG2w=$O#$a`=rQ{No^g>z&vr=bRaNh5fwS$R_RuwXOeyvl}wJIZE;7YkkOHCeT zYDy)%44cl-16^P%LmsPUef{^1z8X^@fH9;cKx!LNNrUw8`jOJ~QJ~*X|c_X4RqP!rh zXnRT4+q-{WW!Um=-V2KN(7AG;XmSGHbmq7PtgEkS~w~YgmGqW7)?n5faKw~p3McgoEN?Qphb@eqEF^{RTKyzQqc09i|?LXaA X#$8)}_J(eM&8PGIPFT4~p1=GHZOLY= literal 1393 zcma)*-EJf`5QXpa6r;XomvY%!42Wfwcc*5OMO+XmlDk`K zcbCsO_0^Y~-FCXWyQcOC zk>ypqR2^(lmX8Swv~Um(YpS*3tdTMxN()0UmxfVmWvx@zf)Ty2M@P>u7fEMI;Z`Qv zvZ%~cWRzxARjo63Y$&Ctc+va;d8Q+56_^4M^C4N0X7^@9g;|GD87?@TT)-!@b(pg@ zThPYrt49EeFR81{7QzaFy1W3aV#5TQ%;M?R!qrFXrI83%i_Em8zFu^wXr{ijO``Ny z1`I4kn85~lZc}4Z8ktU3SVep1w4Oc?Qw>!Ls^Hu!gRZ&8fZ9e$S$S%iYD0Q~bT@2* z)EcZ5)`%R{Q}4u(*+DZNK_JT(#8?A(!Cc&F;RqWgW9PBr+QG;uG*+wGA60756=6z+ zY(e@)_M}L;VhLL{J0fm{w*=+sX7}@jm@Ail;pM{P_RIP6gI^fg->-+$;dp*VXY;=v z?w--z5GuTblIsV657+hM%U8+Xda1PA9*@WK`(IDT`>$_y+hLs!SN^Z;=W(aQLh6_E znAS9sDU`4enrx&l23iMaV$oz(pUJ%Y)GAQYG^L*cPAy*v$kg*haRU5CuIh8aa9^n?1uSu$jVC5Bb;Rc*=r z|7iR6@%YKh+oOwhnJF|iZ^`{a^ctFBHfz1*WX`DBNYY#onz42+z15O|W{_7-|Lxp= adOEpY)#dHEt=pgec)Giimb#yAp8f&iFK$Qx diff --git a/tests/repository_data/client/metadata/previous/targets.json b/tests/repository_data/client/metadata/previous/targets.json index 0a7b4a8bf43e2d63977e1804a83f4fae07449b15..e9da47bb59db2882c5dffd99a8ddc120e14b5fad 100644 GIT binary patch literal 1936 zcmbtV+iv1W6nxKDAirj3)A!4~ncyUlAwWzBWHVaz6-XF!4}nqseUD9&(XO=8N^9Ac zX*Z{C&asff~9Z8&!kGl@f>$!}G?A7L|@R|RkLG|$s7-8_GP1qjN847M6W z9g2yNgy_JnbB2WwoQV-jYzYpMaN}aYj!`EpkHnHFG1DRf1dY66l;TKS5RN({0I)Rh zNLqdU@#tam)${7gd-WL?w38dJCFaP9o!& zVc~%@Ayha}Tt%sv1(_@*PIAkbLlILFwYCIEa|{ru<%)r{KnFHi#}Ik+TuXzblL`3b z?WFb{{8E`Rlx``_nEbifFG6XNzFCdKW?lIv@nIImW;2}^rE@=B`}w1mNa39>>AR_lFBKGu2nuLnDlhlYV;hJSOfdUV_Sg;|AXaO0H12Wu#ms)qt0jxA zolRW{_Dgxxv&-g9t$SDh);zCWXQOKHv1m7&_2yH0Qy)JsC(qMMy+if1mtCZ3=REDH zw0wU)zJ}*4oedstR6%-Y_fN|~wwmVkn+;T8EsOA>Wr}U{s(#VVZ`o;s*o*x2=7~1u z%|jdAO1Jr=TixZgK2V0?4I2XM`GK`Uoy2n>$oTv^`;wacNceIKJR2tn}_~maXCWgEZ=lSuVKEsnvClG z#;`rr!})sLA4YB_`!Mh_7x&B4r7E7*ZaTr~@G{rBc`?Zhe=J75W+UyT^~a*xob0CY z&;2T$2?v<-%BK-Et#10q|&NtRM2ESF@%eB-E$SM>Wq#=Y6 z#nLOzG(wgd#GsL(Gs^&EO2Rlp-W*9Cx1^TLLNVSN-rLC?Y;St5eVh=qVcU(%u$!V)=RC&=&$N!?Q4yU&35jq@}!(7@tL2SYEk61dVeYR*Plk@ zgp-(KgMR$E+jp2RkORwFy>OLMPXZdxn*Xk|fL2LfBNuL&%`hZ&T zsQWpNZ`$!l#r5;{{0he1xa>5Wsk-`lm2Ug%?~l*>>9{+~<>hAFxw@vic=APUF5M{K zr?Ne;n_D(nO}FjQ5`X`!(;I#hKQyz;hfcHqeE)UQe7PAOnzzfrEMLgg@WEhmy=m^- zcXF~T%A#Eq#g}#dnBLw#JYLV&d209h@=iR*O}TpM<5ppta&q6DPP6tbFD6O3T876< zMao!?Z^k=2d_tvOZ$??x8(eqG7VD446&=z}M>lti(Egkjarer{y~@qwS-xh&{(!L z>a7RQ%av%w-Ax#V#V)SIb*$Vfxm|a%W1q z9>uL$ek@LR(D`S*YH`8^`h5F-Jv-aB$bOmU**@P4^vzT2cmBT=PY90faSWK7)v+h4 zQ%`)Xf#7#tu1%Hx4Zz2+$UObMdall7-a}O>8~|yN)eVQ=DSm_DiDu-V*Nf935fMn_ z2ZZ4sBlPKbPtOf`hVs@7(Q!+B-16L?jVLu`dQ8}{Um-DNY6ur~y9jQT6X2%UX-yNQ z72)8$$QTZX3YOp}Oe18eK}=~liNKR7xVwaTh7xn)buLUJ%6vYrQV2|soX+znP~<;@ zVw^ayC{i4O=TjdTBTOaA3j`-2*92p&!P69#9{iia*}w(%!D7Rl(fVIO={nK1pw21Y O`)vtUb}GT^#p_SQkq*58 diff --git a/tests/repository_data/client/metadata/previous/targets.json.gz b/tests/repository_data/client/metadata/previous/targets.json.gz index 757cff5064add4a5eb85863404672b95fe28df7c..e2d2dbe0e05d5ba37e3a7d7ee9a5643195d7e4cc 100644 GIT binary patch literal 1202 zcmV;j1Wo%NiwFS3TZdBu|E*Qqa@t4`efL*TygFIW{gO9fWE&g|L>PmY)Ye=9n~0k* zhLrz(Ti}pvsC;Ew&nngRbex-FvkL3#r^=samCARxE7j+)n|fHO zYRaS`0b#;v2wnuPm9PQ1z?g8xI!`3#KFBDwry@#cwc*@F%p?whRzDo_eGA28en|Kw zOR_Ba*vYc@EkIDN8*DX(JQNcl3DJRD=L`!WI1?k5*b*Eh;l{;)9ivWI9*HGUVx~m| z2pW0CD8-SuARKi@0AOk0%^^#)!jQ;nWD$wnF=C~r!ZJ@tR7??~FcttAOCzB=tt{mV zYio%0ltmc`6pAetP;rn>Go+o?L~?{V_f~0%jF(CyA|;Aa;7BFsQI_Cij1obh5gtco zxX|8UBt7Ob3JHuNA)-abI5^)zETLEjOC$sQL}03!cLEq^#&}OL4F+K=h$PM`AOZ!? zG?2!awUGsawNnA=R? z5UaI03ir7DVrD<%D%Hit&Ze%6`&j;J>2>3-(z&mFZd_Jw)8S!pl(!m9ke}M>FMSTfg;Bx2w%!+1^~t7O!L9xxGoUJjm6mSqrWGD1T_) zu7rMh9GI=$4tG0YhuhHV(~G!BAK3k1C^pUPB~`nNwu^F6YqlT9+u}zl|QZBbb^z?b*6RWYLXg0 z%7@)XJ?SR3QGRGndRPCwU)5uC-_B0Yx1)2d=ChM+R({pW)c;)mKZJNp^!11Aa`WvV z#o{;*7bOmS66L}alYbn45vQ|2|0rII-xC!R@cqerR*o9}S>D&P_Y$k&bum4D8ma_Q z^bujWdI)_wT#v^|e1~zG!ozv^aNcPaCuThf>t7~nJu!rfGO^&Lhe@yq@KXw=0$zn8 z12+USqF8#xnMTM`gBb7!gp>xzl!SGLyg3$iY)Pd$3;DR1KwgS}>C^!d`OlCThffH& zrQ!&_DKtdJ2vc4Kfdrvk6O6(7BW%fFbf6LiE^vqz8|IAG{{~5)y;FQ-_a&^>;9p1b Q-_G9t22JXk8;}P802ES8&j0`b literal 1211 zcmV;s1VsBEiwFR;o>L&dPnn z%d6z-DtR}$y88YMG3*`~9v%10811d*E(WWNGE^9$IpstdVjTb>5P%w$DkrQK#Dewc zWIeS3cS*Uijjy35S%h44QTH{6CkN^RBA%~j=Jt_0WmCN z)CQ*r#cxlei`-(+ukBpqyq3Im}!= zFN^BvxmWJnk47`7psA#eD2r<0MsX5RMV3a1`cSAX2@sHu@dk@yi47bSO{Hg&a&F!t z?E`;D@l?5^b`SQA%YQgFnya-)WrnZuiFf;=JE)_ZP{q zJ$rb}9_FWZOWMh}_c2K>I?321yXT$R8Q%Ajs@rO1=IrxX_B2?1y?x%?O?vl*K7E{Y z&(8TenSL^lr(s-dGTj;a$4fC?-aU233;Ok=%`W6ca?{FBZ@R6)^Y!Ox>*Zp!ZCx&g z_r+WmeL^T*bvbET*2vMM`eS$Al}Yam zl71a#$$haBqrsN-m&5H~zJ^+ECnH)_laP-_)3z$(!_s{HTn?^Zwv+y}m4$IOG-fg0 zh~Dg(=J^66Oz(Uzedw=-&x@sMC%wlwit}|+tMkN!WqP^l=B+_NI7#S3Uk?XmRjoIZ z5P#clF5&LtzL;+N-DI4!?~8qLdc*GD>(x9I_x0rM^Yu8rp33}fcs71k%RK#I{y(I6 z2=wJmc!gW;)mR^T;{6T?{h`a1tFylVcpv9E@L%ia`X8a{3>QFF=DUWYKPY}V`A)M5 z&#U=iNi-QI@*SagO$hn0zwV9=d4%%TjQb<${>ZnM%v_a0^;2V2nPXDzA}i@lP&iF# zu$HIB8YXe}66Yi?6{>Nb2uq0PjtFCM6X8teINdec1xek3*Re2-W)8)y&M+{iKNoBd ziv4F$Lb3pc6C(-Ed=60vCQNET5!{5*GD@w*nP#{KoZlRGgH#lvr%nWC?Z1MuC&xS= ZQ@nwv3P;(Y1g|Hre*$kK5ka2^000K=T{i## diff --git a/tests/repository_data/client/metadata/previous/targets/role1.json b/tests/repository_data/client/metadata/previous/targets/role1.json index d4d3c5603b127a4fa96c5ece27f9377b1073d62f..c5e3bc866133420448769ab01acbf4cb56d45b84 100644 GIT binary patch literal 974 zcmX|=-EJH=42AFg6pOiTT9K6ale@e@kvLST_lH044u>Ck4%^@H{XDa>3D03ZrifZYV_`SYfCGn*gF2g=?#tHpT#mRf(&^!eJz4*jQ|=VK7_Ax)g! zL|M~xM3zh|G{SDJa=5`7t#lie`qV*@W4Bp{#KbYVOr5xk78+Y}@^W|hoV?tijGdMp#_g}`_b;&{^2wj?<9fMqv(0#jd%vEa9(Qv8 zr}p;p@(SJa)59y)pLgxOw#BzE=U3J@EL7eV$mdfj?{Ci^uhQ4IK0jrDTjz(MU$5V; zKRX?^lV46Z@muFqzMMSC*vEtlOKMq-M0070UG3_v_(B{#uOj~{2%elSM^9LryAK?t lB(vQ2B;Mvt-G_L*zn)STb+2bcZ&v<|=S#ZJ8@S8z%YUrl{-FQ> literal 983 zcmX|<&u$bk494$$iq)Jc*!gF>H{JlTJ%JFi6Q{ePw5U5E5bC?**>=%n4sqho&gb9X ztKD)s-aY90^T|)k&F=HnZud9--SW-<97jZ!-a27Lg(gKPbIUMAU2s-WZG}s9?@)=2 z$*X|$>-t5kT-+etio)tgt-|zSDuRrYfm$T?$E5(By8;GL!thrZ_rU|gJ z&5 zi{_Nc`xtY$bIIAYz%)o9YR*Ok%~d3Y12byKLc5(+FS}$tu@kGXJoWA zCOHsk9VA#8^3Fc!IIDR#J6kLCEodS#krdER#$CH1<%S zQ7@6KBudE|iX~GHYX}n2MAo4(9!rk37d7j{pmL0+V6Kj!@lFHmkkL~%LaIHcUK}xO zV#~^geT0(@K(ch7$rZuSv}MCcj<1&9O;ZoWoP@)JX{aT%N-<%Ukz!gCCA{e1L6BRK z){)%Zpd|1mKzY8}ec6aPbLkr|8;{G^_0JFA7`fG_J6}&P8 z=U4Ea9`9e+{<6uRgNy(EaeN_uNn!0ZV60n!oAdhgs=mI~x+uS!$9vCj*Wc@<*ls!K z>2Q`m7mrR)2TiqYtzyARflj@`?sy2e~i zlbS9Z5@VT{*acPLDQ$GkXDXFe60f?rgJX*1j+0j*FO!{|Z5VUw3r3Mv)JA6wPng!y z4~|6ThNi1DrRK`OTAPlts#C)-BvP9+(L-zjVhI_t0yB~sjVTz3bxeUK)x?NFX@+91 zCk>(ewh0I|V)Ia=St$jZX@&5G0wL;Yvem|JMPkoW3k6JLL!gnIeK(;*2=2k)YuFMf z?h;lNRU`AO;LVyUBT{Mv>M4c?GId3CHw~3$Lx5XM4q^#*QQ$rxw0m0>g;TV)=xq>Y z{1HAAu8z{13&3hZ#SJ=yG0aqNTq?7+6=YTl#_SnvGBJ%>xel~0*rq9G!k8LpG@jwn zLx*nJlkX3wKeI1KDd*mny^Pym_pdK=ugHh%!2P~i<=ULOG7 z9oO%#FJ3@~8>iMWc%}Kwj7KI|0|cSxDKJ>uJG#3{U&l{y+WwwTAMcmM#cHr`((vnY`*(i1 MT@O+CkmK+F0~zn~cmMzZ literal 931 zcmXw2%Z?m347~4GH2NF|d`P6|oBtrl_>>?BM2Yf@lYPK531S%j_poQzfEo=@-Nh=h z>f6J%UoW4Y`t6@DaoyiNe%jxvvE;$^nXj;)Wm%27E?ZrUa zv_@*(IA5a#R>#pNGw4PiN(8;ML3gus%ll5bdR!b@b-iHyF=$^jQ?UAZZlPMVs? zn7{Vi5f$yC8VF5uPHQCPx^OnuiWnr3ga)bLbkVWwAm3d;ds8Os(U}chMPjC=?hHMP zI3%b9_wZPWT$iz68^!7|9nZ~kb6YES!JU1qwW3mLtIFEJrZFr8aw({Op9?10sHKgu zFie)GhZ=L+( generate.py @@ -86,13 +88,13 @@ target3_filepath = 'repository/targets/file3.txt' tuf.util.ensure_parent_dir(target2_filepath) -with open(target1_filepath, 'wb') as file_object: +with open(target1_filepath, 'wt') as file_object: file_object.write('This is an example target file.') -with open(target2_filepath, 'wb') as file_object: +with open(target2_filepath, 'wt') as file_object: file_object.write('This is an another example target file.') -with open(target3_filepath, 'wb') as file_object: +with open(target3_filepath, 'wt') as file_object: file_object.write('This is role1\'s target file.') # Add target files to the top-level 'targets.json' role. These target files @@ -106,11 +108,11 @@ # Set the top-level expiration times far into the future so that # they do not expire anytime soon, or else the tests fail. Unit tests may # modify the expiration datetimes (of the copied files), if they wish. -repository.root.expiration = datetime.datetime(2030, 01, 01, 00, 00) -repository.targets.expiration = datetime.datetime(2030, 01, 01, 00, 00) -repository.snapshot.expiration = datetime.datetime(2030, 01, 01, 00, 00) -repository.timestamp.expiration = datetime.datetime(2030, 01, 01, 00, 00) -repository.targets('role1').expiration = datetime.datetime(2030, 01, 01, 00, 00) +repository.root.expiration = datetime.datetime(2030, 1, 1, 0, 0) +repository.targets.expiration = datetime.datetime(2030, 1, 1, 0, 0) +repository.snapshot.expiration = datetime.datetime(2030, 1, 1, 0, 0) +repository.timestamp.expiration = datetime.datetime(2030, 1, 1, 0, 0) +repository.targets('role1').expiration = datetime.datetime(2030, 1, 1, 0, 0) # Compress the 'targets.json' role so that the unit tests have a pre-generated # example of compressed metadata. diff --git a/tests/repository_data/keystore/delegation_key b/tests/repository_data/keystore/delegation_key index fbdd0d1e..8e7d0989 100644 --- a/tests/repository_data/keystore/delegation_key +++ b/tests/repository_data/keystore/delegation_key @@ -1,30 +1,30 @@ -----BEGIN RSA PRIVATE KEY----- Proc-Type: 4,ENCRYPTED -DEK-Info: DES-EDE3-CBC,31815D9E16C988F5 +DEK-Info: DES-EDE3-CBC,15C68797C3B9B4DE -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 -----END RSA PRIVATE KEY----- \ No newline at end of file diff --git a/tests/repository_data/keystore/delegation_key.pub b/tests/repository_data/keystore/delegation_key.pub index 780c740d..34aaa679 100644 --- a/tests/repository_data/keystore/delegation_key.pub +++ b/tests/repository_data/keystore/delegation_key.pub @@ -1,9 +1,9 @@ -----BEGIN PUBLIC KEY----- -MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAsD++h8KXKhrJpzYxvZTE -7GqTDKM3uAWL8qGcQnvh7CNbqR4WmZrCQj1zF9hO5OAV+lGVD+JxUXW+yOPw+RjN -i7mPVa12Mq+vCS7WuosoCoooLpnYhRRVYMgpnhbvnjS6xA+7myJ1Bob+7WUEZZlC -oWdsmjfYG82sA7TOTubPk0s9pqQllINMEsB4JTTt3P3DD9+uifCFhoAEKeAItcgA -p4PJw2ImNwJiuet5wTP1ssTclPPWB6ofkm8zXoJUywTIW+hcQhN88jQv4Egx1llj -pWZbEdkIpNxjm6BAEqfPfiuAt6MA8cmdRpDl+Jn030A1kI7NJossuvTcfHwvReZO -WwIDAQAB +MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAlVZN8n2Q/WjqVEriSBNt +M4Jb01zJanuIHBFMSR4+D2bFSxNj3DoIzfVzRPCxJVLQJJ2Yg+4nmEkaIYnodpOE +7PzWyDEacWhdMSE9nbiYl9QzPEXY25ql9ni6CvEfBIUV74i+bTAT6zfoOpZYfS2M +ol0VMP+JTHMeqHD8JggQuPMrA50l8clwdwdjKrupqOu/lpxgdPKHASne7rrJBeMz +WJKr69vZXawbwYyy6bYweMV3/fpEW4UXY6uJSvE8y/Ocf7pBIcVuwFUeooOEjtZT +GY0C4StOYxeowHhYBTDXMi9XosgTXf5ahyeVd7S6Wq+q8njscih1AXGS99IFhEa5 +YQIDAQAB -----END PUBLIC KEY----- \ No newline at end of file diff --git a/tests/repository_data/keystore/root_key b/tests/repository_data/keystore/root_key index 7445b409..b2466db2 100644 --- a/tests/repository_data/keystore/root_key +++ b/tests/repository_data/keystore/root_key @@ -1,30 +1,30 @@ -----BEGIN RSA PRIVATE KEY----- Proc-Type: 4,ENCRYPTED -DEK-Info: DES-EDE3-CBC,820FC61841CA82E2 +DEK-Info: DES-EDE3-CBC,D79DFCB8B13A2CC2 -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 -----END RSA PRIVATE KEY----- \ No newline at end of file diff --git a/tests/repository_data/keystore/root_key.pub b/tests/repository_data/keystore/root_key.pub index 9fdf180e..d8fbf21b 100644 --- a/tests/repository_data/keystore/root_key.pub +++ b/tests/repository_data/keystore/root_key.pub @@ -1,9 +1,9 @@ -----BEGIN PUBLIC KEY----- -MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA72PwhMhTpDZ/wuB5Bdst -gYsfEI931GcIZ46Iq2dqMNWEg2Qt9w6W0xEQj8M5R99XFwbhXL6U7hKGDt958FzT -OL6CnjrnnBgzjbFm1vT380Qi5DaUbJkPcNmjzV45gGZkJ6LnohnBtnWUM/IdbbwR -PdWaqBxWRJHECPHjgbKt6Y9kDwaO6tJQdUIDGwt2V9hz9orPqwiX+c6uO6qJ0naU -F31ZvI4AtHUDaesbyp2j2X2dfCKNiM2t2sgxz79/G6VQvKG30PXxVPXvOhCDowsk -5NdM67bWIkFyf1yNArrhw0D/c0aSGZhYZs+FqvBzKjCy+9+uEfLZsRra6zvx8Jw9 -TwIDAQAB +MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAwLbRMxjtj63qRwGAW8oe +6Bbw0shvSolq3s30yOHjToxTqpeRkHhCyZCextQjN3ishIpVcJM60xQGfFvNNBLj +Y6aFnavs2K/PPoKB3snO5w2tl0tBic2Kr23zGgbMzT8oqWZHCfdiyZzPa3oaH4fN +DJqaCvs72cIP8IrpaBsuYbIdtGDU8pXM1dfE2NVA/P+1uxjV3Y2k4Wtf04drYhyR +mXfy9I6t5a2M5bagVLtF9UM/vM7QExSvCyuvD3D1EMSiaFjxMnsf5uFUo68wPfZB +bJntUxzELcXcy+O+ks/TOinPwW1KpGff/R2tBD8GxKbwYbNA3eMqxqrHcTY9rWdy +dwIDAQAB -----END PUBLIC KEY----- \ No newline at end of file diff --git a/tests/repository_data/keystore/snapshot_key b/tests/repository_data/keystore/snapshot_key index 5fe431e4..59e1b01a 100644 --- a/tests/repository_data/keystore/snapshot_key +++ b/tests/repository_data/keystore/snapshot_key @@ -1,30 +1,30 @@ -----BEGIN RSA PRIVATE KEY----- Proc-Type: 4,ENCRYPTED -DEK-Info: DES-EDE3-CBC,0E7D0C9F6987D107 +DEK-Info: DES-EDE3-CBC,05DC466E69140631 -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 -----END RSA PRIVATE KEY----- \ No newline at end of file diff --git a/tests/repository_data/keystore/snapshot_key.pub b/tests/repository_data/keystore/snapshot_key.pub index f892e210..70536aa9 100644 --- a/tests/repository_data/keystore/snapshot_key.pub +++ b/tests/repository_data/keystore/snapshot_key.pub @@ -1,9 +1,9 @@ -----BEGIN PUBLIC KEY----- -MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAs0uAxc1XwUIayVPNt9U/ -gLM5hjG/6AJ86XiHVIq6BuzUtiuKONZXq3SJwY5eAxdjylNt/A3FrJwylQbVMZkh -wLj0eU1IMh23xV1wOGu63Q9ARkmBAaksM+6apo2CHjtQaNXorhJ3/WDti2hST/cc -HjP81+JwJ+T6lXGKvGDbh3h6Car99MWlMAeYODdNqvRaVkkiQ20HgNB+RSiyGZPi -bzqlMe+qCQU/vktmmy9Zw3bjY7b8GFBix+7PHzFCpX15xKwB4dITPmsiL2OOo/w/ -1Vqu3wP7Ct170oK+bH9eIk6wSAQX3IljKhgzkiYFRyCC33XHRpWmSjMgAGErCcl6 -cwIDAQAB +MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAtdmy18GajKzifjlXS9BV +XasPpZiAJVQmBJHSnRGsLiUs5uqByRC4SdW8OWwpoTB/LXRhicarE6DVv3yK0mJf +fbjdbRSZC5fYyKCO67oTijttptwzDiLoxVWeCry/5fM0yQ53nPKIkneS27DDraU9 +S0Am9Nmck591QsCCmt4I0u84vQLyhM9HUCrEZyEQVSURdWxj935Sv41/IcXIoH+u +XOVOmvCc0e+88pg4R70CJaWWsrE8lW8NsCHmYZ8fU4rRhUktDnHx7B5T60Xik/kv +JMfy0ZtGPJu1VVdnBlCNAooXeiFVo/dBl0TOHp8F6jnautGKILtSnGHUdTzTJDch +WwIDAQAB -----END PUBLIC KEY----- \ No newline at end of file diff --git a/tests/repository_data/keystore/targets_key b/tests/repository_data/keystore/targets_key index 107f8ffa..e3f174be 100644 --- a/tests/repository_data/keystore/targets_key +++ b/tests/repository_data/keystore/targets_key @@ -1,30 +1,30 @@ -----BEGIN RSA PRIVATE KEY----- Proc-Type: 4,ENCRYPTED -DEK-Info: DES-EDE3-CBC,D32B3B70C5CB2933 +DEK-Info: DES-EDE3-CBC,2CF02CD52C030591 -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 -----END RSA PRIVATE KEY----- \ No newline at end of file diff --git a/tests/repository_data/keystore/targets_key.pub b/tests/repository_data/keystore/targets_key.pub index a7bd1580..2bc18503 100644 --- a/tests/repository_data/keystore/targets_key.pub +++ b/tests/repository_data/keystore/targets_key.pub @@ -1,9 +1,9 @@ -----BEGIN PUBLIC KEY----- -MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAqTXKgkuaAyfvjkInfVic -lJnwLvG9mCXOcxR3HPwHK/8k/E/DBx8YGIGk7NWCXQFYnAeMZZdx8v3dBmH7oGAn -1pW+LnaAI7Csark8sbgmwYqwTd2oLHHmp/fOZ1vNDWjqvMLwi1YUllR6wPWKAvP2 -8i7q7GCT/MBHDyZ899FCR4f7HvlCW5EYNt+wjxdm79T++Ix7iqvs4iUhvllsfdty -cVPWc+wh60qqCCbnr1Fow8d2j42a8mHoIHWgDvEF9ch+ChDOzQ+53jVmXS3CC+a2 -H6EWNvKMc/wXvIAwA/y6cIjCG+Kep9AvVgz1blHf4ReTlNJWu2OkanszuboAR6nt -+wIDAQAB +MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAxFotGGnsRlu+qXalb2tQ +FsnNtsI/lDv79sR2tajNGl6X12h+q3asHH248arBbebfoi59tJmDLMl3EJzLETIw +TFgKxzrKmcCZrlEZPWXCCXyUXd5bWN4KvRDVBNJ40xk3WvPGVj1do6CGKH+W+Iqn +MRXP3yVxSh9Na2X2T9JxKV9ZYqeiaxFppZYqNS+CxUcm4+o/PmtfJ8YagRhbWIKq +Q2R1mpsB95Iwl3ZfSYhW021+lwngUzq4RutQSqo4xH2vakpD3BX4bC5eg7IYvjm/ +Gv+bvNGvHQWmOsBSzK69P9WleEkXJLkbTVJ0JB7C1Y9uANWuXeyvTw9+V5QiteYK +XwIDAQAB -----END PUBLIC KEY----- \ No newline at end of file diff --git a/tests/repository_data/keystore/timestamp_key b/tests/repository_data/keystore/timestamp_key index eb8c5bc5..8b56b09e 100644 --- a/tests/repository_data/keystore/timestamp_key +++ b/tests/repository_data/keystore/timestamp_key @@ -1,30 +1,30 @@ -----BEGIN RSA PRIVATE KEY----- Proc-Type: 4,ENCRYPTED -DEK-Info: DES-EDE3-CBC,D5B52C0993E278D0 +DEK-Info: DES-EDE3-CBC,3495EC0D0BEC51FC -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 -----END RSA PRIVATE KEY----- \ No newline at end of file diff --git a/tests/repository_data/keystore/timestamp_key.pub b/tests/repository_data/keystore/timestamp_key.pub index ee92796c..09a27dfb 100644 --- a/tests/repository_data/keystore/timestamp_key.pub +++ b/tests/repository_data/keystore/timestamp_key.pub @@ -1,9 +1,9 @@ -----BEGIN PUBLIC KEY----- -MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAueyWz2lDPzyywmWegFTP -l0DDXVNr9XIYLNffLRC24Y9ZKJCqb6eIz3XlF/3On5dCCqKFOfneHm55SxyCzoQb -RAtr5+SmoXfsds7ZAnxv48iGLtk7aZEvHchhEJ+1HsKy5hMeqpXtOAMPJXqBfqgT -ZzXagdCHNnbLEvhyJmRAaK88I/93s57KRNvPp6NIUbQ8EHaX1jaWt+LhGfsA3C34 -qcxlk16LXF45Wm2AgMFCtZ2BecUvSQds24b/ShxfeVRtXSSUMDd+dM+MbwclPsoP -eTeegQATmA4pu4dDaBeYUnS/hstUZS5QPUuvVw6K0Q2JHUWv6CS2kziUjPXGI44H -LwIDAQAB +MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAymH18micE+ImSDCQoQh3 +cbTHv0jn1aTKq61R2HYrGFZv9Rrr65xnRLUYP6Ypa6RByQRUwwq4t4v/SUbR74ID ++L8oHdZ3LPrfNx3eJXtGinyvQcayt4C5QwqjmxgT8D7L9G+TODGh42cNbWlguQi/ +fDr+tSC0+ZERXwpYDkyHVJmO0UbEXbWTz7w/R1AAHcbqI71nZtGzZDQlVg6oL7Mz +F64XJnQn8tB2EQF3Tpo9dLaKkzWJAW3i8bba0ltIBkoAJtDR9MNPyXAsak7QyRkI +zyjZfDHOXy3iNRHWaLWzw+4pY0WmzyMN5ABBnY61TI8pgLqTmNLSxJY6LmXzIixd +DQIDAQAB -----END PUBLIC KEY----- \ No newline at end of file diff --git a/tests/repository_data/repository/metadata.staged/root.json b/tests/repository_data/repository/metadata.staged/root.json index 37993bf1f278ddb52705a2df93ea8feb3d186b57..9174375e06a09dee6d59f428d3f2fbb7d386ed54 100644 GIT binary patch literal 3756 zcmd5RP29{Y#4_;^XN(w< za8Fu*5U`1a){~xLN@9(frwLCmkU|(nC6w5c2w{Su(64{{w0AJ&CjT@TO&XI)2? z@@>g^@Z5+fA{0hMQH_Pikp_}5>0w0?Q-UW%VP!Cs8d)hc2PC0_0%<($aRL*9EXTlO zo~O8mfK%qYR00Z2s5S{k6cEn}%o9YoGSXV4oUfQL_gDeBq5^s}@jOpTsv(4oQY{iH zl>m}jPXM48NMItc&oQLrL@c8~C)}OdL54`_ z!mkM9L@VG~KrIJMxuYoQk^+&zuMQ`_e0lkY!^2ItlSsoiMLmCKc-(H{R#mBKLF3YQviPad{#fmv4;o|Mc&-N@JvVMOZhaLE?%Q%p(J8vM8!^w#I-|b2 zfnsx=|i@wXU*Z6)>xo_ua33oUyEx^fnxv8Rcc39X- zgtR0Q4v3N;^M3gzW`p{5z{ainB(#W_x@b(Y-1 zW@FrNwmy^78aDqmUr!8wqS*a>fqaKDJkpyz<>X1=QJnbqMC$6562MJ}=b8NyL z;n)OOfuvB(7%5Sregn*tk^euistz5GA5z__g@;Kq80X^+T4!2KJ6EvHJUQ*1s29P` z^4xw~T}5<0Q*zIu{%kp-%cG=`UyY;L?RLT?zSbu*6)_w(*IBL4^G>ot{n6PD@5po) zWjoEP>sZUG!~q%KPTRx%W6GPXFWR-~d$Ya4P!CnS|G13P`Z}}CbFG{Nptfp7i`#M8 z+*O@<_wWv8sC4{k5qBFgO1V-J?90&K<&AFHjKp9#suqnx?%24BcHugERNIwpcHS2iN{3M= zmi_o~tKs7k#D_;U7?OtXy9)BAAwPcfPt(J&Ke>08w0~GU!t|cF;u!yVSP6ugbe+bb z$MuaT`wC8#>pTWys-@D7lRc6c$J8Q0)H1DvBt%=!HEUzgUjSD3x4dk(vm)AGYNv(V zD_D+QLW*oy7GZtgyfRU^l9bzFdruc0+|*7;7M%{nT%LVpl+9Bj%I=}rAM8=9`{=i( z;ccBw-#&Wx$GLaV!Rowktw!-82o}|B;S@wX#Jy|OocqHr2KOC`ucP*S>m@l2+PzLK zu7#(}ac;C2p=y4gY{XE)1)PfRy*C%D<;kS-{&qaN(Qr}=?lXPBwY)w$l(x&4@*~=) zIP9I)*%(IN;aK>Bgtt9f*~xMf1L)QEx9nr~IN|70jwh!a-#hRscgH637gz{!UHjUC{Me;4Q_aF^n$@Q(dPwQJa%- zTAF2Voh|+cwsLZaTEabL0f24<2y|-dsQ|dsc@jG#nH(yQx8=6?NbPpNmp6T9WaoVUdBgZBw$4u>_j|#VCcj=ko|ljB z)?%M+_^BkH*2;}jf}4E*)RW$G>RW66opAj0>f5E~_l|sbJ^Ud@N_qY;B}rQuEz<=D&F2mcIh#YR*NPXJ2#n%a>pN2@5MgEdT%j literal 3778 zcmd5YOTQs5;EFpsPIT0U;DRCuYDfR~DZTf0bWBXl^z?n` zF~HDD$U2#MD(loQKYV-^=`LgC?W&5;pFaNn!^g)j`u_Mls7EUG$R|KC!5m3Wg;JOk zg)C+gX*$ROK#xfqNk$n^6mURz3>+(#F{czU(^Qmw{_W%K@TAJ*{PsAVxwD!3V>p|A zvuOG@oK zq0Of_XGL0+Dl303GIl8vZTaaVX2(LkIrpjVm-Gv-KYhBIW=04(Te+aU8nTl<8>}Be7=Jjm3Qm z{jm1?4RUC-8?EM@th+(?fJZAYnsnBgtA?A6ypsyb=E=wBuA6N$>#{Ni8FK^d73_K- z1>c?ObzaXUI1haPbZNxnja7|Wt9@P#2X$(#7suli)#0k|Rw3ACB*i)Idh^Dx>9-ym zLTSgF+A;R4qqlO}>rrXc`@1}0Iya2qj?=s<>~xV-$72!8^0CdtGF%CxPLP@Bp2xGR z)j8Khg1rqGIr-|LDpsP(V) zdJ*|eW&QQF{aL_NKz9$t1+{IxNFd~nG^GL(LEQ-iXTm|c!vaB&iWO8c)~l2V%yd7X z;5Wv+I`dydtebkQp5WLDpN~g89WFa@~xB zCyb7r1{`OO^t`;^8ONFOPmVLYKfLF8!nfJfEw7F-JDsDrkOkhj*}bw!+8vY!%r@=H z7fI6Y8J1u4A5Jn<`6VjH?l9~{c{9#;^KG_yMri7bH4 z8?)pdtL3zeX0yexB@H<=hP)QXusG{HsJT+Ri90{J_T^^F7Hg{YBAYdmqFiid&Lmvi zs%4G(<^=To#i~Ny3>=>6Vjo7`z_$HuHuy8d`tJStYwP5oxEy3`+M(UP~+B)2b*GgW$0PmNw22c zZ2o769@ba?uvT{vSG4 zVcy)ku%dbqu*WhSm2}b2#2O5pWZ!Kdw?|Nv`pY0k&D(QPrnmlhw25*!>(y&Vxp%pL z97kotg`I1!ejF!!IouqQt=>uZrdn9RFahwswCZv9hTw#{)5EFhvO_U65WAd#=kLo2 z8%5`9(u0jvt4u*Mn>U2sj{SW|ETdQVj5#_+-G0?=@dPHwV^>OttK-mB>v2nt@@mSK zheJ96roS6Cjp;0XbT?tT&G<7v4waF6lSQLCl&8~!ZfeN)Yc0~doo0GBaOgif-X*e} zd%telGMI;_A{~J7_}r*<9;{`4gLR0#(!%DsZ*ZTg;DG9xJBeU$-1n2+^N_AP(}(9l z82Qu7>NMLAcW$?R^~4eB?f8Ep)=S=fQD)ck@qOO-BD$_GP51jYl*(_1k=Ie=yF1)_ zH-4+im%B32nlL#ZU)s`onZ9k&zY~#fTwf1CzlZYOYvK<=DcN(l3(!*;&Ed$7x&PxHLJJN$BUIDF3cu>C$i-AAr$hMiWI;tQbWAN1-nhKq>d zFf`W~5xf9fSa-xa#w>%W7DUGFyFK0?^Kv}x0e`%Ge|!7>%@4P?U$;`hH4)bq(ilZU zQEd&70u(j8b&zOSno%s3=GwJ$r7F!zkeDp)(gkODC=Xr}3_=XNVFW2xi4@Zu7qG+R zfezle!;DHehgKa>TUJ1&%tE7q57I1>2w6>g0|cvPJb*FWKw2$JX_}ij&DNunMMYY5 z%~rs&2G3JgF})eutk=3Sv}9&ka|sujrOoAmO$Tu&W+~iwP#@iAnpa~^S8Z!lsEbxx zM4CR7A*(f+S^^Z)9&KextSEVtBje^_=c!SK6((w3p?h^$E#0Z0V~tW{Il3cdTTO*I z7HAQG2w=$O#$a`=rQ{No^g>z&vr=bRaNh5fwS$R_RuwXOeyvl}wJIZE;7YkkOHCeT zYDy)%44cl-16^P%LmsPUef{^1z8X^@fH9;cKx!LNNrUw8`jOJ~QJ~*X|c_X4RqP!rh zXnRT4+q-{WW!Um=-V2KN(7AG;XmSGHbmq7PtgEkS~w~YgmGqW7)?n5faKw~p3McgoEN?Qphb@eqEF^{RTKyzQqc09i|?LXaA X#$8)}_J(eM&8PGIPFT4~p1=GHZOLY= literal 1393 zcma)*-EJf`5QXpa6r;XomvY%!42Wfwcc*5OMO+XmlDk`K zcbCsO_0^Y~-FCXWyQcOC zk>ypqR2^(lmX8Swv~Um(YpS*3tdTMxN()0UmxfVmWvx@zf)Ty2M@P>u7fEMI;Z`Qv zvZ%~cWRzxARjo63Y$&Ctc+va;d8Q+56_^4M^C4N0X7^@9g;|GD87?@TT)-!@b(pg@ zThPYrt49EeFR81{7QzaFy1W3aV#5TQ%;M?R!qrFXrI83%i_Em8zFu^wXr{ijO``Ny z1`I4kn85~lZc}4Z8ktU3SVep1w4Oc?Qw>!Ls^Hu!gRZ&8fZ9e$S$S%iYD0Q~bT@2* z)EcZ5)`%R{Q}4u(*+DZNK_JT(#8?A(!Cc&F;RqWgW9PBr+QG;uG*+wGA60756=6z+ zY(e@)_M}L;VhLL{J0fm{w*=+sX7}@jm@Ail;pM{P_RIP6gI^fg->-+$;dp*VXY;=v z?w--z5GuTblIsV657+hM%U8+Xda1PA9*@WK`(IDT`>$_y+hLs!SN^Z;=W(aQLh6_E znAS9sDU`4enrx&l23iMaV$oz(pUJ%Y)GAQYG^L*cPAy*v$kg*haRU5CuIh8aa9^n?1uSu$jVC5Bb;Rc*=r z|7iR6@%YKh+oOwhnJF|iZ^`{a^ctFBHfz1*WX`DBNYY#onz42+z15O|W{_7-|Lxp= adOEpY)#dHEt=pgec)Giimb#yAp8f&iFK$Qx diff --git a/tests/repository_data/repository/metadata.staged/targets.json b/tests/repository_data/repository/metadata.staged/targets.json index 0a7b4a8bf43e2d63977e1804a83f4fae07449b15..e9da47bb59db2882c5dffd99a8ddc120e14b5fad 100644 GIT binary patch literal 1936 zcmbtV+iv1W6nxKDAirj3)A!4~ncyUlAwWzBWHVaz6-XF!4}nqseUD9&(XO=8N^9Ac zX*Z{C&asff~9Z8&!kGl@f>$!}G?A7L|@R|RkLG|$s7-8_GP1qjN847M6W z9g2yNgy_JnbB2WwoQV-jYzYpMaN}aYj!`EpkHnHFG1DRf1dY66l;TKS5RN({0I)Rh zNLqdU@#tam)${7gd-WL?w38dJCFaP9o!& zVc~%@Ayha}Tt%sv1(_@*PIAkbLlILFwYCIEa|{ru<%)r{KnFHi#}Ik+TuXzblL`3b z?WFb{{8E`Rlx``_nEbifFG6XNzFCdKW?lIv@nIImW;2}^rE@=B`}w1mNa39>>AR_lFBKGu2nuLnDlhlYV;hJSOfdUV_Sg;|AXaO0H12Wu#ms)qt0jxA zolRW{_Dgxxv&-g9t$SDh);zCWXQOKHv1m7&_2yH0Qy)JsC(qMMy+if1mtCZ3=REDH zw0wU)zJ}*4oedstR6%-Y_fN|~wwmVkn+;T8EsOA>Wr}U{s(#VVZ`o;s*o*x2=7~1u z%|jdAO1Jr=TixZgK2V0?4I2XM`GK`Uoy2n>$oTv^`;wacNceIKJR2tn}_~maXCWgEZ=lSuVKEsnvClG z#;`rr!})sLA4YB_`!Mh_7x&B4r7E7*ZaTr~@G{rBc`?Zhe=J75W+UyT^~a*xob0CY z&;2T$2?v<-%BK-Et#10q|&NtRM2ESF@%eB-E$SM>Wq#=Y6 z#nLOzG(wgd#GsL(Gs^&EO2Rlp-W*9Cx1^TLLNVSN-rLC?Y;St5eVh=qVcU(%u$!V)=RC&=&$N!?Q4yU&35jq@}!(7@tL2SYEk61dVeYR*Plk@ zgp-(KgMR$E+jp2RkORwFy>OLMPXZdxn*Xk|fL2LfBNuL&%`hZ&T zsQWpNZ`$!l#r5;{{0he1xa>5Wsk-`lm2Ug%?~l*>>9{+~<>hAFxw@vic=APUF5M{K zr?Ne;n_D(nO}FjQ5`X`!(;I#hKQyz;hfcHqeE)UQe7PAOnzzfrEMLgg@WEhmy=m^- zcXF~T%A#Eq#g}#dnBLw#JYLV&d209h@=iR*O}TpM<5ppta&q6DPP6tbFD6O3T876< zMao!?Z^k=2d_tvOZ$??x8(eqG7VD446&=z}M>lti(Egkjarer{y~@qwS-xh&{(!L z>a7RQ%av%w-Ax#V#V)SIb*$Vfxm|a%W1q z9>uL$ek@LR(D`S*YH`8^`h5F-Jv-aB$bOmU**@P4^vzT2cmBT=PY90faSWK7)v+h4 zQ%`)Xf#7#tu1%Hx4Zz2+$UObMdall7-a}O>8~|yN)eVQ=DSm_DiDu-V*Nf935fMn_ z2ZZ4sBlPKbPtOf`hVs@7(Q!+B-16L?jVLu`dQ8}{Um-DNY6ur~y9jQT6X2%UX-yNQ z72)8$$QTZX3YOp}Oe18eK}=~liNKR7xVwaTh7xn)buLUJ%6vYrQV2|soX+znP~<;@ zVw^ayC{i4O=TjdTBTOaA3j`-2*92p&!P69#9{iia*}w(%!D7Rl(fVIO={nK1pw21Y O`)vtUb}GT^#p_SQkq*58 diff --git a/tests/repository_data/repository/metadata.staged/targets.json.gz b/tests/repository_data/repository/metadata.staged/targets.json.gz index 757cff5064add4a5eb85863404672b95fe28df7c..e2d2dbe0e05d5ba37e3a7d7ee9a5643195d7e4cc 100644 GIT binary patch literal 1202 zcmV;j1Wo%NiwFS3TZdBu|E*Qqa@t4`efL*TygFIW{gO9fWE&g|L>PmY)Ye=9n~0k* zhLrz(Ti}pvsC;Ew&nngRbex-FvkL3#r^=samCARxE7j+)n|fHO zYRaS`0b#;v2wnuPm9PQ1z?g8xI!`3#KFBDwry@#cwc*@F%p?whRzDo_eGA28en|Kw zOR_Ba*vYc@EkIDN8*DX(JQNcl3DJRD=L`!WI1?k5*b*Eh;l{;)9ivWI9*HGUVx~m| z2pW0CD8-SuARKi@0AOk0%^^#)!jQ;nWD$wnF=C~r!ZJ@tR7??~FcttAOCzB=tt{mV zYio%0ltmc`6pAetP;rn>Go+o?L~?{V_f~0%jF(CyA|;Aa;7BFsQI_Cij1obh5gtco zxX|8UBt7Ob3JHuNA)-abI5^)zETLEjOC$sQL}03!cLEq^#&}OL4F+K=h$PM`AOZ!? zG?2!awUGsawNnA=R? z5UaI03ir7DVrD<%D%Hit&Ze%6`&j;J>2>3-(z&mFZd_Jw)8S!pl(!m9ke}M>FMSTfg;Bx2w%!+1^~t7O!L9xxGoUJjm6mSqrWGD1T_) zu7rMh9GI=$4tG0YhuhHV(~G!BAK3k1C^pUPB~`nNwu^F6YqlT9+u}zl|QZBbb^z?b*6RWYLXg0 z%7@)XJ?SR3QGRGndRPCwU)5uC-_B0Yx1)2d=ChM+R({pW)c;)mKZJNp^!11Aa`WvV z#o{;*7bOmS66L}alYbn45vQ|2|0rII-xC!R@cqerR*o9}S>D&P_Y$k&bum4D8ma_Q z^bujWdI)_wT#v^|e1~zG!ozv^aNcPaCuThf>t7~nJu!rfGO^&Lhe@yq@KXw=0$zn8 z12+USqF8#xnMTM`gBb7!gp>xzl!SGLyg3$iY)Pd$3;DR1KwgS}>C^!d`OlCThffH& zrQ!&_DKtdJ2vc4Kfdrvk6O6(7BW%fFbf6LiE^vqz8|IAG{{~5)y;FQ-_a&^>;9p1b Q-_G9t22JXk8;}P802ES8&j0`b literal 1211 zcmV;s1VsBEiwFR;o>L&dPnn z%d6z-DtR}$y88YMG3*`~9v%10811d*E(WWNGE^9$IpstdVjTb>5P%w$DkrQK#Dewc zWIeS3cS*Uijjy35S%h44QTH{6CkN^RBA%~j=Jt_0WmCN z)CQ*r#cxlei`-(+ukBpqyq3Im}!= zFN^BvxmWJnk47`7psA#eD2r<0MsX5RMV3a1`cSAX2@sHu@dk@yi47bSO{Hg&a&F!t z?E`;D@l?5^b`SQA%YQgFnya-)WrnZuiFf;=JE)_ZP{q zJ$rb}9_FWZOWMh}_c2K>I?321yXT$R8Q%Ajs@rO1=IrxX_B2?1y?x%?O?vl*K7E{Y z&(8TenSL^lr(s-dGTj;a$4fC?-aU233;Ok=%`W6ca?{FBZ@R6)^Y!Ox>*Zp!ZCx&g z_r+WmeL^T*bvbET*2vMM`eS$Al}Yam zl71a#$$haBqrsN-m&5H~zJ^+ECnH)_laP-_)3z$(!_s{HTn?^Zwv+y}m4$IOG-fg0 zh~Dg(=J^66Oz(Uzedw=-&x@sMC%wlwit}|+tMkN!WqP^l=B+_NI7#S3Uk?XmRjoIZ z5P#clF5&LtzL;+N-DI4!?~8qLdc*GD>(x9I_x0rM^Yu8rp33}fcs71k%RK#I{y(I6 z2=wJmc!gW;)mR^T;{6T?{h`a1tFylVcpv9E@L%ia`X8a{3>QFF=DUWYKPY}V`A)M5 z&#U=iNi-QI@*SagO$hn0zwV9=d4%%TjQb<${>ZnM%v_a0^;2V2nPXDzA}i@lP&iF# zu$HIB8YXe}66Yi?6{>Nb2uq0PjtFCM6X8teINdec1xek3*Re2-W)8)y&M+{iKNoBd ziv4F$Lb3pc6C(-Ed=60vCQNET5!{5*GD@w*nP#{KoZlRGgH#lvr%nWC?Z1MuC&xS= ZQ@nwv3P;(Y1g|Hre*$kK5ka2^000K=T{i## diff --git a/tests/repository_data/repository/metadata.staged/targets/role1.json b/tests/repository_data/repository/metadata.staged/targets/role1.json index d4d3c5603b127a4fa96c5ece27f9377b1073d62f..c5e3bc866133420448769ab01acbf4cb56d45b84 100644 GIT binary patch literal 974 zcmX|=-EJH=42AFg6pOiTT9K6ale@e@kvLST_lH044u>Ck4%^@H{XDa>3D03ZrifZYV_`SYfCGn*gF2g=?#tHpT#mRf(&^!eJz4*jQ|=VK7_Ax)g! zL|M~xM3zh|G{SDJa=5`7t#lie`qV*@W4Bp{#KbYVOr5xk78+Y}@^W|hoV?tijGdMp#_g}`_b;&{^2wj?<9fMqv(0#jd%vEa9(Qv8 zr}p;p@(SJa)59y)pLgxOw#BzE=U3J@EL7eV$mdfj?{Ci^uhQ4IK0jrDTjz(MU$5V; zKRX?^lV46Z@muFqzMMSC*vEtlOKMq-M0070UG3_v_(B{#uOj~{2%elSM^9LryAK?t lB(vQ2B;Mvt-G_L*zn)STb+2bcZ&v<|=S#ZJ8@S8z%YUrl{-FQ> literal 983 zcmX|<&u$bk494$$iq)Jc*!gF>H{JlTJ%JFi6Q{ePw5U5E5bC?**>=%n4sqho&gb9X ztKD)s-aY90^T|)k&F=HnZud9--SW-<97jZ!-a27Lg(gKPbIUMAU2s-WZG}s9?@)=2 z$*X|$>-t5kT-+etio)tgt-|zSDuRrYfm$T?$E5(By8;GL!thrZ_rU|gJ z&5 zi{_Nc`xtY$bIIAYz%)o9YR*Ok%~d3Y12byKLc5(+FS}$tu@kGXJoWA zCOHsk9VA#8^3Fc!IIDR#J6kLCEodS#krdER#$CH1<%S zQ7@6KBudE|iX~GHYX}n2MAo4(9!rk37d7j{pmL0+V6Kj!@lFHmkkL~%LaIHcUK}xO zV#~^geT0(@K(ch7$rZuSv}MCcj<1&9O;ZoWoP@)JX{aT%N-<%Ukz!gCCA{e1L6BRK z){)%Zpd|1mKzY8}ec6aPbLkr|8;{G^_0JFA7`fG_J6}&P8 z=U4Ea9`9e+{<6uRgNy(EaeN_uNn!0ZV60n!oAdhgs=mI~x+uS!$9vCj*Wc@<*ls!K z>2Q`m7mrR)2TiqYtzyARflj@`?sy2e~i zlbS9Z5@VT{*acPLDQ$GkXDXFe60f?rgJX*1j+0j*FO!{|Z5VUw3r3Mv)JA6wPng!y z4~|6ThNi1DrRK`OTAPlts#C)-BvP9+(L-zjVhI_t0yB~sjVTz3bxeUK)x?NFX@+91 zCk>(ewh0I|V)Ia=St$jZX@&5G0wL;Yvem|JMPkoW3k6JLL!gnIeK(;*2=2k)YuFMf z?h;lNRU`AO;LVyUBT{Mv>M4c?GId3CHw~3$Lx5XM4q^#*QQ$rxw0m0>g;TV)=xq>Y z{1HAAu8z{13&3hZ#SJ=yG0aqNTq?7+6=YTl#_SnvGBJ%>xel~0*rq9G!k8LpG@jwn zLx*nJlkX3wKeI1KDd*mny^Pym_pdK=ugHh%!2P~i<=ULOG7 z9oO%#FJ3@~8>iMWc%}Kwj7KI|0|cSxDKJ>uJG#3{U&l{y+WwwTAMcmM#cHr`((vnY`*(i1 MT@O+CkmK+F0~zn~cmMzZ literal 931 zcmXw2%Z?m347~4GH2NF|d`P6|oBtrl_>>?BM2Yf@lYPK531S%j_poQzfEo=@-Nh=h z>f6J%UoW4Y`t6@DaoyiNe%jxvvE;$^nXj;)Wm%27E?ZrUa zv_@*(IA5a#R>#pNGw4PiN(8;ML3gus%ll5bdR!b@b-iHyF=$^jQ?UAZZlPMVs? zn7{Vi5f$yC8VF5uPHQCPx^OnuiWnr3ga)bLbkVWwAm3d;ds8Os(U}chMPjC=?hHMP zI3%b9_wZPWT$iz68^!7|9nZ~kb6YES!JU1qwW3mLtIFEJrZFr8aw({Op9?10sHKgu zFie)GhZ=L+(RP29{Y#4_;^XN(w< za8Fu*5U`1a){~xLN@9(frwLCmkU|(nC6w5c2w{Su(64{{w0AJ&CjT@TO&XI)2? z@@>g^@Z5+fA{0hMQH_Pikp_}5>0w0?Q-UW%VP!Cs8d)hc2PC0_0%<($aRL*9EXTlO zo~O8mfK%qYR00Z2s5S{k6cEn}%o9YoGSXV4oUfQL_gDeBq5^s}@jOpTsv(4oQY{iH zl>m}jPXM48NMItc&oQLrL@c8~C)}OdL54`_ z!mkM9L@VG~KrIJMxuYoQk^+&zuMQ`_e0lkY!^2ItlSsoiMLmCKc-(H{R#mBKLF3YQviPad{#fmv4;o|Mc&-N@JvVMOZhaLE?%Q%p(J8vM8!^w#I-|b2 zfnsx=|i@wXU*Z6)>xo_ua33oUyEx^fnxv8Rcc39X- zgtR0Q4v3N;^M3gzW`p{5z{ainB(#W_x@b(Y-1 zW@FrNwmy^78aDqmUr!8wqS*a>fqaKDJkpyz<>X1=QJnbqMC$6562MJ}=b8NyL z;n)OOfuvB(7%5Sregn*tk^euistz5GA5z__g@;Kq80X^+T4!2KJ6EvHJUQ*1s29P` z^4xw~T}5<0Q*zIu{%kp-%cG=`UyY;L?RLT?zSbu*6)_w(*IBL4^G>ot{n6PD@5po) zWjoEP>sZUG!~q%KPTRx%W6GPXFWR-~d$Ya4P!CnS|G13P`Z}}CbFG{Nptfp7i`#M8 z+*O@<_wWv8sC4{k5qBFgO1V-J?90&K<&AFHjKp9#suqnx?%24BcHugERNIwpcHS2iN{3M= zmi_o~tKs7k#D_;U7?OtXy9)BAAwPcfPt(J&Ke>08w0~GU!t|cF;u!yVSP6ugbe+bb z$MuaT`wC8#>pTWys-@D7lRc6c$J8Q0)H1DvBt%=!HEUzgUjSD3x4dk(vm)AGYNv(V zD_D+QLW*oy7GZtgyfRU^l9bzFdruc0+|*7;7M%{nT%LVpl+9Bj%I=}rAM8=9`{=i( z;ccBw-#&Wx$GLaV!Rowktw!-82o}|B;S@wX#Jy|OocqHr2KOC`ucP*S>m@l2+PzLK zu7#(}ac;C2p=y4gY{XE)1)PfRy*C%D<;kS-{&qaN(Qr}=?lXPBwY)w$l(x&4@*~=) zIP9I)*%(IN;aK>Bgtt9f*~xMf1L)QEx9nr~IN|70jwh!a-#hRscgH637gz{!UHjUC{Me;4Q_aF^n$@Q(dPwQJa%- zTAF2Voh|+cwsLZaTEabL0f24<2y|-dsQ|dsc@jG#nH(yQx8=6?NbPpNmp6T9WaoVUdBgZBw$4u>_j|#VCcj=ko|ljB z)?%M+_^BkH*2;}jf}4E*)RW$G>RW66opAj0>f5E~_l|sbJ^Ud@N_qY;B}rQuEz<=D&F2mcIh#YR*NPXJ2#n%a>pN2@5MgEdT%j literal 3778 zcmd5YOTQs5;EFpsPIT0U;DRCuYDfR~DZTf0bWBXl^z?n` zF~HDD$U2#MD(loQKYV-^=`LgC?W&5;pFaNn!^g)j`u_Mls7EUG$R|KC!5m3Wg;JOk zg)C+gX*$ROK#xfqNk$n^6mURz3>+(#F{czU(^Qmw{_W%K@TAJ*{PsAVxwD!3V>p|A zvuOG@oK zq0Of_XGL0+Dl303GIl8vZTaaVX2(LkIrpjVm-Gv-KYhBIW=04(Te+aU8nTl<8>}Be7=Jjm3Qm z{jm1?4RUC-8?EM@th+(?fJZAYnsnBgtA?A6ypsyb=E=wBuA6N$>#{Ni8FK^d73_K- z1>c?ObzaXUI1haPbZNxnja7|Wt9@P#2X$(#7suli)#0k|Rw3ACB*i)Idh^Dx>9-ym zLTSgF+A;R4qqlO}>rrXc`@1}0Iya2qj?=s<>~xV-$72!8^0CdtGF%CxPLP@Bp2xGR z)j8Khg1rqGIr-|LDpsP(V) zdJ*|eW&QQF{aL_NKz9$t1+{IxNFd~nG^GL(LEQ-iXTm|c!vaB&iWO8c)~l2V%yd7X z;5Wv+I`dydtebkQp5WLDpN~g89WFa@~xB zCyb7r1{`OO^t`;^8ONFOPmVLYKfLF8!nfJfEw7F-JDsDrkOkhj*}bw!+8vY!%r@=H z7fI6Y8J1u4A5Jn<`6VjH?l9~{c{9#;^KG_yMri7bH4 z8?)pdtL3zeX0yexB@H<=hP)QXusG{HsJT+Ri90{J_T^^F7Hg{YBAYdmqFiid&Lmvi zs%4G(<^=To#i~Ny3>=>6Vjo7`z_$HuHuy8d`tJStYwP5oxEy3`+M(UP~+B)2b*GgW$0PmNw22c zZ2o769@ba?uvT{vSG4 zVcy)ku%dbqu*WhSm2}b2#2O5pWZ!Kdw?|Nv`pY0k&D(QPrnmlhw25*!>(y&Vxp%pL z97kotg`I1!ejF!!IouqQt=>uZrdn9RFahwswCZv9hTw#{)5EFhvO_U65WAd#=kLo2 z8%5`9(u0jvt4u*Mn>U2sj{SW|ETdQVj5#_+-G0?=@dPHwV^>OttK-mB>v2nt@@mSK zheJ96roS6Cjp;0XbT?tT&G<7v4waF6lSQLCl&8~!ZfeN)Yc0~doo0GBaOgif-X*e} zd%telGMI;_A{~J7_}r*<9;{`4gLR0#(!%DsZ*ZTg;DG9xJBeU$-1n2+^N_AP(}(9l z82Qu7>NMLAcW$?R^~4eB?f8Ep)=S=fQD)ck@qOO-BD$_GP51jYl*(_1k=Ie=yF1)_ zH-4+im%B32nlL#ZU)s`onZ9k&zY~#fTwf1CzlZYOYvK<=DcN(l3(!*;&Ed$7x&PxHLJJN$BUIDF3cu>C$i-AAr$hMiWI;tQbWAN1-nhKq>d zFf`W~5xf9fSa-xa#w>%W7DUGFyFK0?^Kv}x0e`%Ge|!7>%@4P?U$;`hH4)bq(ilZU zQEd&70u(j8b&zOSno%s3=GwJ$r7F!zkeDp)(gkODC=Xr}3_=XNVFW2xi4@Zu7qG+R zfezle!;DHehgKa>TUJ1&%tE7q57I1>2w6>g0|cvPJb*FWKw2$JX_}ij&DNunMMYY5 z%~rs&2G3JgF})eutk=3Sv}9&ka|sujrOoAmO$Tu&W+~iwP#@iAnpa~^S8Z!lsEbxx zM4CR7A*(f+S^^Z)9&KextSEVtBje^_=c!SK6((w3p?h^$E#0Z0V~tW{Il3cdTTO*I z7HAQG2w=$O#$a`=rQ{No^g>z&vr=bRaNh5fwS$R_RuwXOeyvl}wJIZE;7YkkOHCeT zYDy)%44cl-16^P%LmsPUef{^1z8X^@fH9;cKx!LNNrUw8`jOJ~QJ~*X|c_X4RqP!rh zXnRT4+q-{WW!Um=-V2KN(7AG;XmSGHbmq7PtgEkS~w~YgmGqW7)?n5faKw~p3McgoEN?Qphb@eqEF^{RTKyzQqc09i|?LXaA X#$8)}_J(eM&8PGIPFT4~p1=GHZOLY= literal 1393 zcma)*-EJf`5QXpa6r;XomvY%!42Wfwcc*5OMO+XmlDk`K zcbCsO_0^Y~-FCXWyQcOC zk>ypqR2^(lmX8Swv~Um(YpS*3tdTMxN()0UmxfVmWvx@zf)Ty2M@P>u7fEMI;Z`Qv zvZ%~cWRzxARjo63Y$&Ctc+va;d8Q+56_^4M^C4N0X7^@9g;|GD87?@TT)-!@b(pg@ zThPYrt49EeFR81{7QzaFy1W3aV#5TQ%;M?R!qrFXrI83%i_Em8zFu^wXr{ijO``Ny z1`I4kn85~lZc}4Z8ktU3SVep1w4Oc?Qw>!Ls^Hu!gRZ&8fZ9e$S$S%iYD0Q~bT@2* z)EcZ5)`%R{Q}4u(*+DZNK_JT(#8?A(!Cc&F;RqWgW9PBr+QG;uG*+wGA60756=6z+ zY(e@)_M}L;VhLL{J0fm{w*=+sX7}@jm@Ail;pM{P_RIP6gI^fg->-+$;dp*VXY;=v z?w--z5GuTblIsV657+hM%U8+Xda1PA9*@WK`(IDT`>$_y+hLs!SN^Z;=W(aQLh6_E znAS9sDU`4enrx&l23iMaV$oz(pUJ%Y)GAQYG^L*cPAy*v$kg*haRU5CuIh8aa9^n?1uSu$jVC5Bb;Rc*=r z|7iR6@%YKh+oOwhnJF|iZ^`{a^ctFBHfz1*WX`DBNYY#onz42+z15O|W{_7-|Lxp= adOEpY)#dHEt=pgec)Giimb#yAp8f&iFK$Qx diff --git a/tests/repository_data/repository/metadata/targets.json b/tests/repository_data/repository/metadata/targets.json index 0a7b4a8bf43e2d63977e1804a83f4fae07449b15..e9da47bb59db2882c5dffd99a8ddc120e14b5fad 100644 GIT binary patch literal 1936 zcmbtV+iv1W6nxKDAirj3)A!4~ncyUlAwWzBWHVaz6-XF!4}nqseUD9&(XO=8N^9Ac zX*Z{C&asff~9Z8&!kGl@f>$!}G?A7L|@R|RkLG|$s7-8_GP1qjN847M6W z9g2yNgy_JnbB2WwoQV-jYzYpMaN}aYj!`EpkHnHFG1DRf1dY66l;TKS5RN({0I)Rh zNLqdU@#tam)${7gd-WL?w38dJCFaP9o!& zVc~%@Ayha}Tt%sv1(_@*PIAkbLlILFwYCIEa|{ru<%)r{KnFHi#}Ik+TuXzblL`3b z?WFb{{8E`Rlx``_nEbifFG6XNzFCdKW?lIv@nIImW;2}^rE@=B`}w1mNa39>>AR_lFBKGu2nuLnDlhlYV;hJSOfdUV_Sg;|AXaO0H12Wu#ms)qt0jxA zolRW{_Dgxxv&-g9t$SDh);zCWXQOKHv1m7&_2yH0Qy)JsC(qMMy+if1mtCZ3=REDH zw0wU)zJ}*4oedstR6%-Y_fN|~wwmVkn+;T8EsOA>Wr}U{s(#VVZ`o;s*o*x2=7~1u z%|jdAO1Jr=TixZgK2V0?4I2XM`GK`Uoy2n>$oTv^`;wacNceIKJR2tn}_~maXCWgEZ=lSuVKEsnvClG z#;`rr!})sLA4YB_`!Mh_7x&B4r7E7*ZaTr~@G{rBc`?Zhe=J75W+UyT^~a*xob0CY z&;2T$2?v<-%BK-Et#10q|&NtRM2ESF@%eB-E$SM>Wq#=Y6 z#nLOzG(wgd#GsL(Gs^&EO2Rlp-W*9Cx1^TLLNVSN-rLC?Y;St5eVh=qVcU(%u$!V)=RC&=&$N!?Q4yU&35jq@}!(7@tL2SYEk61dVeYR*Plk@ zgp-(KgMR$E+jp2RkORwFy>OLMPXZdxn*Xk|fL2LfBNuL&%`hZ&T zsQWpNZ`$!l#r5;{{0he1xa>5Wsk-`lm2Ug%?~l*>>9{+~<>hAFxw@vic=APUF5M{K zr?Ne;n_D(nO}FjQ5`X`!(;I#hKQyz;hfcHqeE)UQe7PAOnzzfrEMLgg@WEhmy=m^- zcXF~T%A#Eq#g}#dnBLw#JYLV&d209h@=iR*O}TpM<5ppta&q6DPP6tbFD6O3T876< zMao!?Z^k=2d_tvOZ$??x8(eqG7VD446&=z}M>lti(Egkjarer{y~@qwS-xh&{(!L z>a7RQ%av%w-Ax#V#V)SIb*$Vfxm|a%W1q z9>uL$ek@LR(D`S*YH`8^`h5F-Jv-aB$bOmU**@P4^vzT2cmBT=PY90faSWK7)v+h4 zQ%`)Xf#7#tu1%Hx4Zz2+$UObMdall7-a}O>8~|yN)eVQ=DSm_DiDu-V*Nf935fMn_ z2ZZ4sBlPKbPtOf`hVs@7(Q!+B-16L?jVLu`dQ8}{Um-DNY6ur~y9jQT6X2%UX-yNQ z72)8$$QTZX3YOp}Oe18eK}=~liNKR7xVwaTh7xn)buLUJ%6vYrQV2|soX+znP~<;@ zVw^ayC{i4O=TjdTBTOaA3j`-2*92p&!P69#9{iia*}w(%!D7Rl(fVIO={nK1pw21Y O`)vtUb}GT^#p_SQkq*58 diff --git a/tests/repository_data/repository/metadata/targets.json.gz b/tests/repository_data/repository/metadata/targets.json.gz index 757cff5064add4a5eb85863404672b95fe28df7c..e2d2dbe0e05d5ba37e3a7d7ee9a5643195d7e4cc 100644 GIT binary patch literal 1202 zcmV;j1Wo%NiwFS3TZdBu|E*Qqa@t4`efL*TygFIW{gO9fWE&g|L>PmY)Ye=9n~0k* zhLrz(Ti}pvsC;Ew&nngRbex-FvkL3#r^=samCARxE7j+)n|fHO zYRaS`0b#;v2wnuPm9PQ1z?g8xI!`3#KFBDwry@#cwc*@F%p?whRzDo_eGA28en|Kw zOR_Ba*vYc@EkIDN8*DX(JQNcl3DJRD=L`!WI1?k5*b*Eh;l{;)9ivWI9*HGUVx~m| z2pW0CD8-SuARKi@0AOk0%^^#)!jQ;nWD$wnF=C~r!ZJ@tR7??~FcttAOCzB=tt{mV zYio%0ltmc`6pAetP;rn>Go+o?L~?{V_f~0%jF(CyA|;Aa;7BFsQI_Cij1obh5gtco zxX|8UBt7Ob3JHuNA)-abI5^)zETLEjOC$sQL}03!cLEq^#&}OL4F+K=h$PM`AOZ!? zG?2!awUGsawNnA=R? z5UaI03ir7DVrD<%D%Hit&Ze%6`&j;J>2>3-(z&mFZd_Jw)8S!pl(!m9ke}M>FMSTfg;Bx2w%!+1^~t7O!L9xxGoUJjm6mSqrWGD1T_) zu7rMh9GI=$4tG0YhuhHV(~G!BAK3k1C^pUPB~`nNwu^F6YqlT9+u}zl|QZBbb^z?b*6RWYLXg0 z%7@)XJ?SR3QGRGndRPCwU)5uC-_B0Yx1)2d=ChM+R({pW)c;)mKZJNp^!11Aa`WvV z#o{;*7bOmS66L}alYbn45vQ|2|0rII-xC!R@cqerR*o9}S>D&P_Y$k&bum4D8ma_Q z^bujWdI)_wT#v^|e1~zG!ozv^aNcPaCuThf>t7~nJu!rfGO^&Lhe@yq@KXw=0$zn8 z12+USqF8#xnMTM`gBb7!gp>xzl!SGLyg3$iY)Pd$3;DR1KwgS}>C^!d`OlCThffH& zrQ!&_DKtdJ2vc4Kfdrvk6O6(7BW%fFbf6LiE^vqz8|IAG{{~5)y;FQ-_a&^>;9p1b Q-_G9t22JXk8;}P802ES8&j0`b literal 1211 zcmV;s1VsBEiwFR;o>L&dPnn z%d6z-DtR}$y88YMG3*`~9v%10811d*E(WWNGE^9$IpstdVjTb>5P%w$DkrQK#Dewc zWIeS3cS*Uijjy35S%h44QTH{6CkN^RBA%~j=Jt_0WmCN z)CQ*r#cxlei`-(+ukBpqyq3Im}!= zFN^BvxmWJnk47`7psA#eD2r<0MsX5RMV3a1`cSAX2@sHu@dk@yi47bSO{Hg&a&F!t z?E`;D@l?5^b`SQA%YQgFnya-)WrnZuiFf;=JE)_ZP{q zJ$rb}9_FWZOWMh}_c2K>I?321yXT$R8Q%Ajs@rO1=IrxX_B2?1y?x%?O?vl*K7E{Y z&(8TenSL^lr(s-dGTj;a$4fC?-aU233;Ok=%`W6ca?{FBZ@R6)^Y!Ox>*Zp!ZCx&g z_r+WmeL^T*bvbET*2vMM`eS$Al}Yam zl71a#$$haBqrsN-m&5H~zJ^+ECnH)_laP-_)3z$(!_s{HTn?^Zwv+y}m4$IOG-fg0 zh~Dg(=J^66Oz(Uzedw=-&x@sMC%wlwit}|+tMkN!WqP^l=B+_NI7#S3Uk?XmRjoIZ z5P#clF5&LtzL;+N-DI4!?~8qLdc*GD>(x9I_x0rM^Yu8rp33}fcs71k%RK#I{y(I6 z2=wJmc!gW;)mR^T;{6T?{h`a1tFylVcpv9E@L%ia`X8a{3>QFF=DUWYKPY}V`A)M5 z&#U=iNi-QI@*SagO$hn0zwV9=d4%%TjQb<${>ZnM%v_a0^;2V2nPXDzA}i@lP&iF# zu$HIB8YXe}66Yi?6{>Nb2uq0PjtFCM6X8teINdec1xek3*Re2-W)8)y&M+{iKNoBd ziv4F$Lb3pc6C(-Ed=60vCQNET5!{5*GD@w*nP#{KoZlRGgH#lvr%nWC?Z1MuC&xS= ZQ@nwv3P;(Y1g|Hre*$kK5ka2^000K=T{i## diff --git a/tests/repository_data/repository/metadata/targets/role1.json b/tests/repository_data/repository/metadata/targets/role1.json index d4d3c5603b127a4fa96c5ece27f9377b1073d62f..c5e3bc866133420448769ab01acbf4cb56d45b84 100644 GIT binary patch literal 974 zcmX|=-EJH=42AFg6pOiTT9K6ale@e@kvLST_lH044u>Ck4%^@H{XDa>3D03ZrifZYV_`SYfCGn*gF2g=?#tHpT#mRf(&^!eJz4*jQ|=VK7_Ax)g! zL|M~xM3zh|G{SDJa=5`7t#lie`qV*@W4Bp{#KbYVOr5xk78+Y}@^W|hoV?tijGdMp#_g}`_b;&{^2wj?<9fMqv(0#jd%vEa9(Qv8 zr}p;p@(SJa)59y)pLgxOw#BzE=U3J@EL7eV$mdfj?{Ci^uhQ4IK0jrDTjz(MU$5V; zKRX?^lV46Z@muFqzMMSC*vEtlOKMq-M0070UG3_v_(B{#uOj~{2%elSM^9LryAK?t lB(vQ2B;Mvt-G_L*zn)STb+2bcZ&v<|=S#ZJ8@S8z%YUrl{-FQ> literal 983 zcmX|<&u$bk494$$iq)Jc*!gF>H{JlTJ%JFi6Q{ePw5U5E5bC?**>=%n4sqho&gb9X ztKD)s-aY90^T|)k&F=HnZud9--SW-<97jZ!-a27Lg(gKPbIUMAU2s-WZG}s9?@)=2 z$*X|$>-t5kT-+etio)tgt-|zSDuRrYfm$T?$E5(By8;GL!thrZ_rU|gJ z&5 zi{_Nc`xtY$bIIAYz%)o9YR*Ok%~d3Y12byKLc5(+FS}$tu@kGXJoWA zCOHsk9VA#8^3Fc!IIDR#J6kLCEodS#krdER#$CH1<%S zQ7@6KBudE|iX~GHYX}n2MAo4(9!rk37d7j{pmL0+V6Kj!@lFHmkkL~%LaIHcUK}xO zV#~^geT0(@K(ch7$rZuSv}MCcj<1&9O;ZoWoP@)JX{aT%N-<%Ukz!gCCA{e1L6BRK z){)%Zpd|1mKzY8}ec6aPbLkr|8;{G^_0JFA7`fG_J6}&P8 z=U4Ea9`9e+{<6uRgNy(EaeN_uNn!0ZV60n!oAdhgs=mI~x+uS!$9vCj*Wc@<*ls!K z>2Q`m7mrR)2TiqYtzyARflj@`?sy2e~i zlbS9Z5@VT{*acPLDQ$GkXDXFe60f?rgJX*1j+0j*FO!{|Z5VUw3r3Mv)JA6wPng!y z4~|6ThNi1DrRK`OTAPlts#C)-BvP9+(L-zjVhI_t0yB~sjVTz3bxeUK)x?NFX@+91 zCk>(ewh0I|V)Ia=St$jZX@&5G0wL;Yvem|JMPkoW3k6JLL!gnIeK(;*2=2k)YuFMf z?h;lNRU`AO;LVyUBT{Mv>M4c?GId3CHw~3$Lx5XM4q^#*QQ$rxw0m0>g;TV)=xq>Y z{1HAAu8z{13&3hZ#SJ=yG0aqNTq?7+6=YTl#_SnvGBJ%>xel~0*rq9G!k8LpG@jwn zLx*nJlkX3wKeI1KDd*mny^Pym_pdK=ugHh%!2P~i<=ULOG7 z9oO%#FJ3@~8>iMWc%}Kwj7KI|0|cSxDKJ>uJG#3{U&l{y+WwwTAMcmM#cHr`((vnY`*(i1 MT@O+CkmK+F0~zn~cmMzZ literal 931 zcmXw2%Z?m347~4GH2NF|d`P6|oBtrl_>>?BM2Yf@lYPK531S%j_poQzfEo=@-Nh=h z>f6J%UoW4Y`t6@DaoyiNe%jxvvE;$^nXj;)Wm%27E?ZrUa zv_@*(IA5a#R>#pNGw4PiN(8;ML3gus%ll5bdR!b@b-iHyF=$^jQ?UAZZlPMVs? zn7{Vi5f$yC8VF5uPHQCPx^OnuiWnr3ga)bLbkVWwAm3d;ds8Os(U}chMPjC=?hHMP zI3%b9_wZPWT$iz68^!7|9nZ~kb6YES!JU1qwW3mLtIFEJrZFr8aw({Op9?10sHKgu zFie)GhZ=L+( Date: Tue, 3 Jun 2014 14:32:44 -0400 Subject: [PATCH 13/32] Refactor repository_tool.py and improve test coverage. Created repository_lib.py. --- tests/.coveragerc | 6 +- tests/simple_server.py | 2 + tests/test_ed25519_keys.py | 2 + tests/test_hash.py | 2 + tests/test_indefinite_freeze_attack.py | 2 + tests/test_keydb.py | 2 + tests/test_keys.py | 47 + tests/test_log.py | 69 +- tests/test_mirrors.py | 2 + tests/test_pycrypto_keys.py | 2 + tests/test_repository_lib.py | 675 +++++++ tests/test_repository_tool.py | 590 +------ tests/test_roledb.py | 2 + tests/test_schema.py | 2 + tests/test_sig.py | 2 + tests/test_util.py | 34 +- tuf/formats.py | 2 + tuf/keys.py | 25 +- tuf/pycrypto_keys.py | 2 + tuf/repository_lib.py | 2215 +++++++++++++++++++++++ tuf/repository_tool.py | 2248 +----------------------- tuf/schema.py | 2 + tuf/util.py | 38 +- 23 files changed, 3165 insertions(+), 2808 deletions(-) create mode 100755 tests/test_repository_lib.py create mode 100755 tuf/repository_lib.py diff --git a/tests/.coveragerc b/tests/.coveragerc index bb2d6cff..1b596114 100644 --- a/tests/.coveragerc +++ b/tests/.coveragerc @@ -5,11 +5,13 @@ branch = True [report] exclude_lines = pragma: no cover - def _check_crypto_libraries + def check_crypto_libraries def __str__ if __name__ == .__main__.: omit = */tuf/interposition/* */tuf/_vendor/* - */tuf/compatibility/* + + # Command-line tool and integration example that calls core TUF. + */tuf/client/basic_client.py diff --git a/tests/simple_server.py b/tests/simple_server.py index 05fd59fc..57e7d7f7 100755 --- a/tests/simple_server.py +++ b/tests/simple_server.py @@ -1,3 +1,5 @@ +#!/usr/bin/env python + """ simple_server.py diff --git a/tests/test_ed25519_keys.py b/tests/test_ed25519_keys.py index de4d3b17..2cfc560f 100755 --- a/tests/test_ed25519_keys.py +++ b/tests/test_ed25519_keys.py @@ -1,3 +1,5 @@ +#!/usr/bin/env/ python + """ test_ed25519_keys.py diff --git a/tests/test_hash.py b/tests/test_hash.py index 33efd781..ecb82e98 100755 --- a/tests/test_hash.py +++ b/tests/test_hash.py @@ -1,3 +1,5 @@ +#!/usr/bin/env python + """ test_hash.py diff --git a/tests/test_indefinite_freeze_attack.py b/tests/test_indefinite_freeze_attack.py index 825fd093..2ce817d9 100755 --- a/tests/test_indefinite_freeze_attack.py +++ b/tests/test_indefinite_freeze_attack.py @@ -1,3 +1,5 @@ +#!/usr/bin/env python + """ test_indefinite_freeze_attack.py diff --git a/tests/test_keydb.py b/tests/test_keydb.py index 1ad0b631..f7171233 100755 --- a/tests/test_keydb.py +++ b/tests/test_keydb.py @@ -1,3 +1,5 @@ +#!/usr/bin/env python + """ test_keydb.py diff --git a/tests/test_keys.py b/tests/test_keys.py index dfcca83a..5e8b1c12 100755 --- a/tests/test_keys.py +++ b/tests/test_keys.py @@ -1,3 +1,5 @@ +#!/usr/bin/env python + """ test_keys.py @@ -207,6 +209,51 @@ def test_verify_signature(self): # Passing incorrect number of arguments. self.assertRaises(TypeError, KEYS.verify_signature) + + + + def test_create_rsa_encrypted_pem(self): + # Test valid arguments. + private = self.rsakey_dict['keyval']['private'] + passphrase = 'secret' + encrypted_pem = KEYS.create_rsa_encrypted_pem(private, passphrase) + self.assertTrue(tuf.formats.PEMRSA_SCHEMA.matches(encrypted_pem)) + + # Test improperly formatted arguments. + self.assertRaises(tuf.FormatError, KEYS.create_rsa_encrypted_pem, + 8, passphrase) + + self.assertRaises(tuf.FormatError, KEYS.create_rsa_encrypted_pem, + private, 8) + + # Test for missing required library. + KEYS._RSA_CRYPTO_LIBRARY = 'invalid' + self.assertRaises(tuf.UnsupportedLibraryError, KEYS.create_rsa_encrypted_pem, + private, passphrase) + KEYS._RSA_CRYPTO_LIBRARY = 'pycrypto' + + + + def test_decrypt_key(self): + # Test valid arguments. + passphrase = 'secret' + encrypted_key = KEYS.encrypt_key(self.rsakey_dict, passphrase).encode('utf-8') + decrypted_key = KEYS.decrypt_key(encrypted_key, passphrase) + + self.assertTrue(tuf.formats.ANYKEY_SCHEMA.matches(decrypted_key)) + + # Test improperly formatted arguments. + self.assertRaises(tuf.FormatError, KEYS.decrypt_key, + 8, passphrase) + + self.assertRaises(tuf.FormatError, KEYS.decrypt_key, + encrypted_key, 8) + + # Test for missing required library. + KEYS._GENERAL_CRYPTO_LIBRARY = 'invalid' + self.assertRaises(tuf.UnsupportedLibraryError, KEYS.decrypt_key, + encrypted_key, passphrase) + KEYS._GENERAL_CRYPTO_LIBRARY = 'pycrypto' diff --git a/tests/test_log.py b/tests/test_log.py index d2854440..006ba7af 100755 --- a/tests/test_log.py +++ b/tests/test_log.py @@ -1,3 +1,4 @@ +#!/usr/bin/env python """ @@ -29,13 +30,17 @@ class TestLog(unittest.TestCase): - - + + + def tearDown(self): + tuf.log.remove_console_handler() + def test_set_log_level(self): # Test normal case. global log_levels + global logger tuf.log.set_log_level() self.assertTrue(logger.isEnabledFor(logging.DEBUG)) @@ -53,22 +58,74 @@ def test_set_log_level(self): def test_set_filehandler_log_level(self): - pass + # Normal case. Default log level. + tuf.log.set_filehandler_log_level() + + # Expected log levels. + for level in log_levels: + tuf.log.set_log_level(level) + + # Test for improperly formatted argument. + self.assertRaises(tuf.FormatError, tuf.log.set_filehandler_log_level, '123') + + # Test for invalid argument. + self.assertRaises(tuf.FormatError, tuf.log.set_filehandler_log_level, 51) + + def test_set_console_log_level(self): - pass + # Test setting a console log level without first adding one. + self.assertRaises(tuf.Error, tuf.log.set_console_log_level) + + # Normal case. Default log level. Setting the console log level first + # requires adding a console logger. + tuf.log.add_console_handler() + tuf.log.set_console_log_level() + + # Expected log levels. + for level in log_levels: + tuf.log.set_console_log_level(level) + + # Test for improperly formatted argument. + self.assertRaises(tuf.FormatError, tuf.log.set_console_log_level, '123') + + # Test for invalid argument. + self.assertRaises(tuf.FormatError, tuf.log.set_console_log_level, 51) def test_add_console_handler(self): - pass + # Normal case. Default log level. + tuf.log.add_console_handler() + # Adding a console handler when one has already been added. + tuf.log.add_console_handler() + + # Expected log levels. + for level in log_levels: + tuf.log.set_console_log_level(level) + + # Test for improperly formatted argument. + self.assertRaises(tuf.FormatError, tuf.log.add_console_handler, '123') + + # Test for invalid argument. + self.assertRaises(tuf.FormatError, tuf.log.add_console_handler, 51) + + try: + raise TypeError('Test exception output in the console.') + + except TypeError as e: + logger.error(e) def test_remove_console_handler(self): - pass + # Normal case. + tuf.log.remove_console_handler() + # Removing a console handler that has not been added. Logs a warning. + tuf.log.remove_console_handler() + # Run unit test. diff --git a/tests/test_mirrors.py b/tests/test_mirrors.py index 34ee502c..4bf8e6a4 100755 --- a/tests/test_mirrors.py +++ b/tests/test_mirrors.py @@ -1,3 +1,5 @@ +#!/usr/bin/env python + """ test_mirrors.py diff --git a/tests/test_pycrypto_keys.py b/tests/test_pycrypto_keys.py index a165abe2..f4269667 100755 --- a/tests/test_pycrypto_keys.py +++ b/tests/test_pycrypto_keys.py @@ -1,3 +1,5 @@ +#!/usr/bin/env python + """ test_pycrypto_keys.py diff --git a/tests/test_repository_lib.py b/tests/test_repository_lib.py new file mode 100755 index 00000000..b9e2bfe0 --- /dev/null +++ b/tests/test_repository_lib.py @@ -0,0 +1,675 @@ +#!/usr/bin/env python + +""" + + test_repository_lib.py + + + Vladimir Diaz + + + June 1, 2014. + + + See LICENSE for licensing information. + + + Unit test for 'repository_lib.py'. +""" + +# Help with Python 3 compatibility, where the print statement is a function, an +# implicit relative import is invalid, and the '/' operator performs true +# division. Example: print 'hello world' raises a 'SyntaxError' exception. +from __future__ import print_function +from __future__ import absolute_import +from __future__ import division +from __future__ import unicode_literals + +import os +import time +import datetime +import unittest +import logging +import tempfile +import shutil + +import tuf +import tuf.log +import tuf.formats +import tuf.roledb +import tuf.keydb +import tuf.hash +import tuf.repository_lib as repo_lib +import tuf._vendor.six as six + +logger = logging.getLogger('tuf.test_repository_lib') + +repo_lib.disable_console_log_messages() + + + +class TestRepositoryToolFunctions(unittest.TestCase): + @classmethod + def setUpClass(cls): + + # setUpClass() is called before tests in an individual class are executed. + + # Create a temporary directory to store the repository, metadata, and target + # files. 'temporary_directory' must be deleted in TearDownClass() so that + # temporary files are always removed, even when exceptions occur. + cls.temporary_directory = tempfile.mkdtemp(dir=os.getcwd()) + + + + @classmethod + def tearDownClass(cls): + + # tearDownModule() is called after all the tests have run. + # http://docs.python.org/2/library/unittest.html#class-and-module-fixtures + + # Remove the temporary repository directory, which should contain all the + # metadata, targets, and key files generated for the test cases. + shutil.rmtree(cls.temporary_directory) + + + + def setUp(self): + pass + + + def tearDown(self): + pass + + + + def test_generate_and_write_rsa_keypair(self): + + # Test normal case. + temporary_directory = tempfile.mkdtemp(dir=self.temporary_directory) + test_keypath = os.path.join(temporary_directory, 'rsa_key') + + repo_lib.generate_and_write_rsa_keypair(test_keypath, password='pw') + self.assertTrue(os.path.exists(test_keypath)) + self.assertTrue(os.path.exists(test_keypath + '.pub')) + + # Ensure the generated key files are importable. + imported_pubkey = \ + repo_lib.import_rsa_publickey_from_file(test_keypath + '.pub') + self.assertTrue(tuf.formats.RSAKEY_SCHEMA.matches(imported_pubkey)) + + imported_privkey = \ + repo_lib.import_rsa_privatekey_from_file(test_keypath, 'pw') + self.assertTrue(tuf.formats.RSAKEY_SCHEMA.matches(imported_privkey)) + + # Custom 'bits' argument. + os.remove(test_keypath) + os.remove(test_keypath + '.pub') + repo_lib.generate_and_write_rsa_keypair(test_keypath, bits=2048, + password='pw') + self.assertTrue(os.path.exists(test_keypath)) + self.assertTrue(os.path.exists(test_keypath + '.pub')) + + + # Test improperly formatted arguments. + self.assertRaises(tuf.FormatError, repo_lib.generate_and_write_rsa_keypair, + 3, bits=2048, password='pw') + self.assertRaises(tuf.FormatError, repo_lib.generate_and_write_rsa_keypair, + test_keypath, bits='bad', password='pw') + self.assertRaises(tuf.FormatError, repo_lib.generate_and_write_rsa_keypair, + test_keypath, bits=2048, password=3) + + + # Test invalid 'bits' argument. + self.assertRaises(tuf.FormatError, repo_lib.generate_and_write_rsa_keypair, + test_keypath, bits=1024, password='pw') + + + + def test_import_rsa_privatekey_from_file(self): + # Test normal case. + temporary_directory = tempfile.mkdtemp(dir=self.temporary_directory) + + # Load one of the pre-generated key files from 'tuf/tests/repository_data'. + # 'password' unlocks the pre-generated key files. + key_filepath = os.path.join('repository_data', 'keystore', + 'root_key') + self.assertTrue(os.path.exists(key_filepath)) + + imported_rsa_key = repo_lib.import_rsa_privatekey_from_file(key_filepath, + 'password') + self.assertTrue(tuf.formats.RSAKEY_SCHEMA.matches(imported_rsa_key)) + + + # Test improperly formatted argument. + self.assertRaises(tuf.FormatError, + repo_lib.import_rsa_privatekey_from_file, 3, 'pw') + + + # Test invalid argument. + # Non-existent key file. + nonexistent_keypath = os.path.join(temporary_directory, + 'nonexistent_keypath') + self.assertRaises(IOError, repo_lib.import_rsa_privatekey_from_file, + nonexistent_keypath, 'pw') + + # Invalid key file argument. + invalid_keyfile = os.path.join(temporary_directory, 'invalid_keyfile') + with open(invalid_keyfile, 'wb') as file_object: + file_object.write(b'bad keyfile') + self.assertRaises(tuf.CryptoError, repo_lib.import_rsa_privatekey_from_file, + invalid_keyfile, 'pw') + + + + def test_import_rsa_publickey_from_file(self): + # Test normal case. + temporary_directory = tempfile.mkdtemp(dir=self.temporary_directory) + + # Load one of the pre-generated key files from 'tuf/tests/repository_data'. + key_filepath = os.path.join('repository_data', 'keystore', + 'root_key.pub') + self.assertTrue(os.path.exists(key_filepath)) + + imported_rsa_key = repo_lib.import_rsa_publickey_from_file(key_filepath) + self.assertTrue(tuf.formats.RSAKEY_SCHEMA.matches(imported_rsa_key)) + + + # Test improperly formatted argument. + self.assertRaises(tuf.FormatError, + repo_lib.import_rsa_privatekey_from_file, 3) + + + # Test invalid argument. + # Non-existent key file. + nonexistent_keypath = os.path.join(temporary_directory, + 'nonexistent_keypath') + self.assertRaises(IOError, repo_lib.import_rsa_publickey_from_file, + nonexistent_keypath) + + # Invalid key file argument. + invalid_keyfile = os.path.join(temporary_directory, 'invalid_keyfile') + with open(invalid_keyfile, 'wb') as file_object: + file_object.write(b'bad keyfile') + self.assertRaises(tuf.Error, repo_lib.import_rsa_publickey_from_file, + invalid_keyfile) + + + + def test_generate_and_write_ed25519_keypair(self): + + # Test normal case. + temporary_directory = tempfile.mkdtemp(dir=self.temporary_directory) + test_keypath = os.path.join(temporary_directory, 'ed25519_key') + + repo_lib.generate_and_write_ed25519_keypair(test_keypath, password='pw') + self.assertTrue(os.path.exists(test_keypath)) + self.assertTrue(os.path.exists(test_keypath + '.pub')) + + # Ensure the generated key files are importable. + imported_pubkey = \ + repo_lib.import_ed25519_publickey_from_file(test_keypath + '.pub') + self.assertTrue(tuf.formats.ED25519KEY_SCHEMA.matches(imported_pubkey)) + + imported_privkey = \ + repo_lib.import_ed25519_privatekey_from_file(test_keypath, 'pw') + self.assertTrue(tuf.formats.ED25519KEY_SCHEMA.matches(imported_privkey)) + + + # Test improperly formatted arguments. + self.assertRaises(tuf.FormatError, + repo_lib.generate_and_write_ed25519_keypair, + 3, password='pw') + self.assertRaises(tuf.FormatError, repo_lib.generate_and_write_rsa_keypair, + test_keypath, password=3) + + + + def test_import_ed25519_publickey_from_file(self): + # Test normal case. + # Generate ed25519 keys that can be imported. + temporary_directory = tempfile.mkdtemp(dir=self.temporary_directory) + ed25519_keypath = os.path.join(temporary_directory, 'ed25519_key') + repo_lib.generate_and_write_ed25519_keypair(ed25519_keypath, password='pw') + + imported_ed25519_key = \ + repo_lib.import_ed25519_publickey_from_file(ed25519_keypath + '.pub') + self.assertTrue(tuf.formats.ED25519KEY_SCHEMA.matches(imported_ed25519_key)) + + + # Test improperly formatted argument. + self.assertRaises(tuf.FormatError, + repo_lib.import_ed25519_publickey_from_file, 3) + + + # Test invalid argument. + # Non-existent key file. + nonexistent_keypath = os.path.join(temporary_directory, + 'nonexistent_keypath') + self.assertRaises(IOError, repo_lib.import_ed25519_publickey_from_file, + nonexistent_keypath) + + # Invalid key file argument. + invalid_keyfile = os.path.join(temporary_directory, 'invalid_keyfile') + with open(invalid_keyfile, 'wb') as file_object: + file_object.write(b'bad keyfile') + + self.assertRaises(tuf.Error, repo_lib.import_ed25519_publickey_from_file, + invalid_keyfile) + + + + def test_import_ed25519_privatekey_from_file(self): + # Test normal case. + # Generate ed25519 keys that can be imported. + temporary_directory = tempfile.mkdtemp(dir=self.temporary_directory) + ed25519_keypath = os.path.join(temporary_directory, 'ed25519_key') + repo_lib.generate_and_write_ed25519_keypair(ed25519_keypath, password='pw') + + imported_ed25519_key = \ + repo_lib.import_ed25519_privatekey_from_file(ed25519_keypath, 'pw') + self.assertTrue(tuf.formats.ED25519KEY_SCHEMA.matches(imported_ed25519_key)) + + + # Test improperly formatted argument. + self.assertRaises(tuf.FormatError, + repo_lib.import_ed25519_privatekey_from_file, 3, 'pw') + + + # Test invalid argument. + # Non-existent key file. + nonexistent_keypath = os.path.join(temporary_directory, + 'nonexistent_keypath') + self.assertRaises(IOError, repo_lib.import_ed25519_privatekey_from_file, + nonexistent_keypath, 'pw') + + # Invalid key file argument. + invalid_keyfile = os.path.join(temporary_directory, 'invalid_keyfile') + with open(invalid_keyfile, 'wb') as file_object: + file_object.write(b'bad keyfile') + + self.assertRaises(tuf.Error, repo_lib.import_ed25519_privatekey_from_file, + invalid_keyfile, 'pw') + + + + def test_get_metadata_filenames(self): + + # Test normal case. + metadata_directory = os.path.join('metadata/') + filenames = {'root.json': metadata_directory + 'root.json', + 'targets.json': metadata_directory + 'targets.json', + 'snapshot.json': metadata_directory + 'snapshot.json', + 'timestamp.json': metadata_directory + 'timestamp.json'} + + self.assertEqual(filenames, repo_lib.get_metadata_filenames('metadata/')) + + # If a directory argument is not specified, the current working directory + # is used. + metadata_directory = os.getcwd() + filenames = {'root.json': os.path.join(metadata_directory, 'root.json'), + 'targets.json': os.path.join(metadata_directory, 'targets.json'), + 'snapshot.json': os.path.join(metadata_directory, 'snapshot.json'), + 'timestamp.json': os.path.join(metadata_directory, 'timestamp.json')} + self.assertEqual(filenames, repo_lib.get_metadata_filenames()) + + + # Test improperly formatted argument. + self.assertRaises(tuf.FormatError, repo_lib.get_metadata_filenames, 3) + + + + def test_get_metadata_fileinfo(self): + # Test normal case. + temporary_directory = tempfile.mkdtemp(dir=self.temporary_directory) + test_filepath = os.path.join(temporary_directory, 'file.txt') + + with open(test_filepath, 'wt') as file_object: + file_object.write('test file') + + # Generate test fileinfo object. It is assumed SHA256 hashes are computed + # by get_metadata_fileinfo(). + file_length = os.path.getsize(test_filepath) + digest_object = tuf.hash.digest_filename(test_filepath) + file_hashes = {'sha256': digest_object.hexdigest()} + fileinfo = {'length': file_length, 'hashes': file_hashes} + self.assertTrue(tuf.formats.FILEINFO_SCHEMA.matches(fileinfo)) + + self.assertEqual(fileinfo, repo_lib.get_metadata_fileinfo(test_filepath)) + + + # Test improperly formatted argument. + self.assertRaises(tuf.FormatError, repo_lib.get_metadata_fileinfo, 3) + + + # Test non-existent file. + nonexistent_filepath = os.path.join(temporary_directory, 'oops.txt') + self.assertRaises(tuf.Error, repo_lib.get_metadata_fileinfo, + nonexistent_filepath) + + + + def test_get_target_hash(self): + # Test normal case. + expected_target_hashes = { + '/file1.txt': 'e3a3d89eb3b70ce3fbce6017d7b8c12d4abd5635427a0e8a238f53157df85b3d', + '/README.txt': '8faee106f1bb69f34aaf1df1e3c2e87d763c4d878cb96b91db13495e32ceb0b0', + '/packages/file2.txt': 'c9c4a5cdd84858dd6a23d98d7e6e6b2aec45034946c16b2200bc317c75415e92' + } + for filepath, target_hash in six.iteritems(expected_target_hashes): + self.assertTrue(tuf.formats.RELPATH_SCHEMA.matches(filepath)) + self.assertTrue(tuf.formats.HASH_SCHEMA.matches(target_hash)) + self.assertEqual(repo_lib.get_target_hash(filepath), target_hash) + + # Test for improperly formatted argument. + self.assertRaises(tuf.FormatError, repo_lib.get_target_hash, 8) + + + + def test_generate_root_metadata(self): + # Test normal case. + # Load the root metadata provided in 'tuf/tests/repository_data/'. + root_filepath = os.path.join('repository_data', 'repository', + 'metadata', 'root.json') + root_signable = tuf.util.load_json_file(root_filepath) + + # generate_root_metadata() expects the top-level roles and keys to be + # available in 'tuf.keydb' and 'tuf.roledb'. + tuf.roledb.create_roledb_from_root_metadata(root_signable['signed']) + tuf.keydb.create_keydb_from_root_metadata(root_signable['signed']) + expires = '1985-10-21T01:22:00Z' + + root_metadata = repo_lib.generate_root_metadata(1, expires, + consistent_snapshot=False) + self.assertTrue(tuf.formats.ROOT_SCHEMA.matches(root_metadata)) + + + # Test improperly formatted arguments. + self.assertRaises(tuf.FormatError, repo_lib.generate_root_metadata, + '3', expires, False) + self.assertRaises(tuf.FormatError, repo_lib.generate_root_metadata, + 1, '3', False) + self.assertRaises(tuf.FormatError, repo_lib.generate_root_metadata, + 1, expires, 3) + + # Test for missing required roles and keys. + tuf.roledb.clear_roledb() + tuf.keydb.clear_keydb() + self.assertRaises(tuf.Error, repo_lib.generate_root_metadata, + 1, expires, False) + + + + def test_generate_targets_metadata(self): + # Test normal case. + temporary_directory = tempfile.mkdtemp(dir=self.temporary_directory) + targets_directory = os.path.join(temporary_directory, 'targets') + file1_path = os.path.join(targets_directory, 'file.txt') + tuf.util.ensure_parent_dir(file1_path) + + with open(file1_path, 'wt') as file_object: + file_object.write('test file.') + + # Set valid generate_targets_metadata() arguments. + version = 1 + datetime_object = datetime.datetime(2030, 1, 1, 12, 0) + expiration_date = datetime_object.isoformat() + 'Z' + target_files = ['file.txt'] + + delegations = {"keys": { + "a394c28384648328b16731f81440d72243c77bb44c07c040be99347f0df7d7bf": { + "keytype": "ed25519", + "keyval": { + "public": "3eb81026ded5af2c61fb3d4b272ac53cd1049a810ee88f4df1fc35cdaf918157" + } + } + }, + "roles": [ + { + "keyids": [ + "a394c28384648328b16731f81440d72243c77bb44c07c040be99347f0df7d7bf" + ], + "name": "targets/warehouse", + "paths": [ + "/file1.txt", "/README.txt", '/warehouse/' + ], + "threshold": 1 + } + ] + } + + targets_metadata = \ + repo_lib.generate_targets_metadata(targets_directory, target_files, + version, expiration_date, delegations, + False) + self.assertTrue(tuf.formats.TARGETS_SCHEMA.matches(targets_metadata)) + + # Verify that 'digest.filename' file is saved to 'targets_directory' if + # the 'write_consistent_targets' argument is True. + list_targets_directory = os.listdir(targets_directory) + targets_metadata = \ + repo_lib.generate_targets_metadata(targets_directory, target_files, + version, expiration_date, delegations, + write_consistent_targets=True) + new_list_targets_directory = os.listdir(targets_directory) + + # Verify that 'targets_directory' contains only one extra item. + self.assertTrue(len(list_targets_directory) + 1, + len(new_list_targets_directory)) + + + # Test improperly formatted arguments. + self.assertRaises(tuf.FormatError, repo_lib.generate_targets_metadata, + 3, target_files, version, expiration_date) + self.assertRaises(tuf.FormatError, repo_lib.generate_targets_metadata, + targets_directory, 3, version, expiration_date) + self.assertRaises(tuf.FormatError, repo_lib.generate_targets_metadata, + targets_directory, target_files, '3', expiration_date) + self.assertRaises(tuf.FormatError, repo_lib.generate_targets_metadata, + targets_directory, target_files, version, '3') + + # Improperly formatted 'delegations' and 'write_consistent_targets' + self.assertRaises(tuf.FormatError, repo_lib.generate_targets_metadata, + targets_directory, target_files, version, expiration_date, + 3, False) + self.assertRaises(tuf.FormatError, repo_lib.generate_targets_metadata, + targets_directory, target_files, version, expiration_date, + delegations, 3) + + + # Test invalid 'target_files' argument. + self.assertRaises(tuf.Error, repo_lib.generate_targets_metadata, + targets_directory, ['nonexistent_file.txt'], version, + expiration_date) + + + + + def test_generate_snapshot_metadata(self): + # Test normal case. + temporary_directory = tempfile.mkdtemp(dir=self.temporary_directory) + original_repository_path = os.path.join('repository_data', + 'repository') + repository_directory = os.path.join(temporary_directory, 'repository') + shutil.copytree(original_repository_path, repository_directory) + metadata_directory = os.path.join(repository_directory, + repo_lib.METADATA_STAGED_DIRECTORY_NAME) + root_filename = os.path.join(metadata_directory, repo_lib.ROOT_FILENAME) + targets_filename = os.path.join(metadata_directory, + repo_lib.TARGETS_FILENAME) + version = 1 + expiration_date = '1985-10-21T13:20:00Z' + + snapshot_metadata = \ + repo_lib.generate_snapshot_metadata(metadata_directory, version, + expiration_date, root_filename, + targets_filename, + consistent_snapshot=False) + self.assertTrue(tuf.formats.SNAPSHOT_SCHEMA.matches(snapshot_metadata)) + + + # Test improperly formatted arguments. + self.assertRaises(tuf.FormatError, repo_lib.generate_snapshot_metadata, + 3, version, expiration_date, + root_filename, targets_filename, consistent_snapshot=False) + self.assertRaises(tuf.FormatError, repo_lib.generate_snapshot_metadata, + metadata_directory, '3', expiration_date, + root_filename, targets_filename, consistent_snapshot=False) + self.assertRaises(tuf.FormatError, repo_lib.generate_snapshot_metadata, + metadata_directory, version, '3', + root_filename, targets_filename, consistent_snapshot=False) + self.assertRaises(tuf.FormatError, repo_lib.generate_snapshot_metadata, + metadata_directory, version, expiration_date, + 3, targets_filename, consistent_snapshot=False) + self.assertRaises(tuf.FormatError, repo_lib.generate_snapshot_metadata, + metadata_directory, version, expiration_date, + root_filename, 3, consistent_snapshot=False) + self.assertRaises(tuf.FormatError, repo_lib.generate_snapshot_metadata, + metadata_directory, version, expiration_date, + root_filename, targets_filename, 3) + + + + def test_generate_timestamp_metadata(self): + # Test normal case. + temporary_directory = tempfile.mkdtemp(dir=self.temporary_directory) + original_repository_path = os.path.join('repository_data', + 'repository') + repository_directory = os.path.join(temporary_directory, 'repository') + shutil.copytree(original_repository_path, repository_directory) + metadata_directory = os.path.join(repository_directory, + repo_lib.METADATA_STAGED_DIRECTORY_NAME) + snapshot_filename = os.path.join(metadata_directory, + repo_lib.SNAPSHOT_FILENAME) + + # Set valid generate_timestamp_metadata() arguments. + version = 1 + expiration_date = '1985-10-21T13:20:00Z' + + compressions = ['gz'] + + snapshot_metadata = \ + repo_lib.generate_timestamp_metadata(snapshot_filename, version, + expiration_date, compressions) + self.assertTrue(tuf.formats.TIMESTAMP_SCHEMA.matches(snapshot_metadata)) + + + # Test improperly formatted arguments. + self.assertRaises(tuf.FormatError, repo_lib.generate_timestamp_metadata, + 3, version, expiration_date, compressions) + self.assertRaises(tuf.FormatError, repo_lib.generate_timestamp_metadata, + snapshot_filename, '3', expiration_date, compressions) + self.assertRaises(tuf.FormatError, repo_lib.generate_timestamp_metadata, + snapshot_filename, version, '3', compressions) + self.assertRaises(tuf.FormatError, repo_lib.generate_timestamp_metadata, + snapshot_filename, version, expiration_date, 3) + self.assertRaises(tuf.FormatError, repo_lib.generate_timestamp_metadata, + snapshot_filename, version, expiration_date, ['compress']) + + + + + def test_sign_metadata(self): + # Test normal case. + temporary_directory = tempfile.mkdtemp(dir=self.temporary_directory) + metadata_path = os.path.join('repository_data', + 'repository', 'metadata') + keystore_path = os.path.join('repository_data', + 'keystore') + root_filename = os.path.join(metadata_path, 'root.json') + root_metadata = tuf.util.load_json_file(root_filename)['signed'] + + tuf.keydb.create_keydb_from_root_metadata(root_metadata) + tuf.roledb.create_roledb_from_root_metadata(root_metadata) + root_keyids = tuf.roledb.get_role_keyids('root') + + root_private_keypath = os.path.join(keystore_path, 'root_key') + root_private_key = \ + repo_lib.import_rsa_privatekey_from_file(root_private_keypath, + 'password') + + # sign_metadata() expects the private key 'root_metadata' to be in + # 'tuf.keydb'. Remove any public keys that may be loaded before + # adding private key, otherwise a 'tuf.KeyAlreadyExists' exception is + # raised. + tuf.keydb.remove_key(root_private_key['keyid']) + tuf.keydb.add_key(root_private_key) + + root_signable = repo_lib.sign_metadata(root_metadata, root_keyids, + root_filename) + self.assertTrue(tuf.formats.SIGNABLE_SCHEMA.matches(root_signable)) + + + # Test improperly formatted arguments. + self.assertRaises(tuf.FormatError, repo_lib.sign_metadata, 3, root_keyids, + 'root.json') + self.assertRaises(tuf.FormatError, repo_lib.sign_metadata, root_metadata, + 3, 'root.json') + self.assertRaises(tuf.FormatError, repo_lib.sign_metadata, root_metadata, + root_keyids, 3) + + + + def test_write_metadata_file(self): + # Test normal case. + temporary_directory = tempfile.mkdtemp(dir=self.temporary_directory) + metadata_directory = os.path.join('repository_data', + 'repository', 'metadata') + root_filename = os.path.join(metadata_directory, 'root.json') + root_signable = tuf.util.load_json_file(root_filename) + + output_filename = os.path.join(temporary_directory, 'root.json') + compressions = ['gz'] + + self.assertFalse(os.path.exists(output_filename)) + repo_lib.write_metadata_file(root_signable, output_filename, compressions, + consistent_snapshot=False) + self.assertTrue(os.path.exists(output_filename)) + self.assertTrue(os.path.exists(output_filename + '.gz')) + + + # Test improperly formatted arguments. + self.assertRaises(tuf.FormatError, repo_lib.write_metadata_file, + 3, output_filename, compressions, False) + self.assertRaises(tuf.FormatError, repo_lib.write_metadata_file, + root_signable, 3, compressions, False) + self.assertRaises(tuf.FormatError, repo_lib.write_metadata_file, + root_signable, output_filename, 3, False) + self.assertRaises(tuf.FormatError, repo_lib.write_metadata_file, + root_signable, output_filename, compressions, 3) + + + + def test_create_tuf_client_directory(self): + # Test normal case. + temporary_directory = tempfile.mkdtemp(dir=self.temporary_directory) + repository_directory = os.path.join('repository_data', + 'repository') + client_directory = os.path.join(temporary_directory, 'client') + + repo_lib.create_tuf_client_directory(repository_directory, client_directory) + + self.assertTrue(os.path.exists(client_directory)) + metadata_directory = os.path.join(client_directory, 'metadata') + current_directory = os.path.join(metadata_directory, 'current') + previous_directory = os.path.join(metadata_directory, 'previous') + self.assertTrue(os.path.exists(client_directory)) + self.assertTrue(os.path.exists(metadata_directory)) + self.assertTrue(os.path.exists(current_directory)) + self.assertTrue(os.path.exists(previous_directory)) + + + # Test improperly formatted arguments. + self.assertRaises(tuf.FormatError, repo_lib.create_tuf_client_directory, + 3, client_directory) + self.assertRaises(tuf.FormatError, repo_lib.create_tuf_client_directory, + repository_directory, 3) + + + # Test invalid argument (i.e., client directory already exists.) + self.assertRaises(tuf.RepositoryError, repo_lib.create_tuf_client_directory, + repository_directory, client_directory) + + +# Run the test cases. +if __name__ == '__main__': + unittest.main() diff --git a/tests/test_repository_tool.py b/tests/test_repository_tool.py index d497ee94..a4e842b4 100755 --- a/tests/test_repository_tool.py +++ b/tests/test_repository_tool.py @@ -1,3 +1,5 @@ +#!/usr/bin/env python + """ test_repository_tool.py @@ -1368,594 +1370,6 @@ def test_load_repository(self): - def test_generate_and_write_rsa_keypair(self): - - # Test normal case. - temporary_directory = tempfile.mkdtemp(dir=self.temporary_directory) - test_keypath = os.path.join(temporary_directory, 'rsa_key') - - repo_tool.generate_and_write_rsa_keypair(test_keypath, password='pw') - self.assertTrue(os.path.exists(test_keypath)) - self.assertTrue(os.path.exists(test_keypath + '.pub')) - - # Ensure the generated key files are importable. - imported_pubkey = \ - repo_tool.import_rsa_publickey_from_file(test_keypath + '.pub') - self.assertTrue(tuf.formats.RSAKEY_SCHEMA.matches(imported_pubkey)) - - imported_privkey = \ - repo_tool.import_rsa_privatekey_from_file(test_keypath, 'pw') - self.assertTrue(tuf.formats.RSAKEY_SCHEMA.matches(imported_privkey)) - - # Custom 'bits' argument. - os.remove(test_keypath) - os.remove(test_keypath + '.pub') - repo_tool.generate_and_write_rsa_keypair(test_keypath, bits=2048, - password='pw') - self.assertTrue(os.path.exists(test_keypath)) - self.assertTrue(os.path.exists(test_keypath + '.pub')) - - - # Test improperly formatted arguments. - self.assertRaises(tuf.FormatError, repo_tool.generate_and_write_rsa_keypair, - 3, bits=2048, password='pw') - self.assertRaises(tuf.FormatError, repo_tool.generate_and_write_rsa_keypair, - test_keypath, bits='bad', password='pw') - self.assertRaises(tuf.FormatError, repo_tool.generate_and_write_rsa_keypair, - test_keypath, bits=2048, password=3) - - - # Test invalid 'bits' argument. - self.assertRaises(tuf.FormatError, repo_tool.generate_and_write_rsa_keypair, - test_keypath, bits=1024, password='pw') - - - - def test_import_rsa_privatekey_from_file(self): - # Test normal case. - temporary_directory = tempfile.mkdtemp(dir=self.temporary_directory) - - # Load one of the pre-generated key files from 'tuf/tests/repository_data'. - # 'password' unlocks the pre-generated key files. - key_filepath = os.path.join('repository_data', 'keystore', - 'root_key') - self.assertTrue(os.path.exists(key_filepath)) - - imported_rsa_key = repo_tool.import_rsa_privatekey_from_file(key_filepath, - 'password') - self.assertTrue(tuf.formats.RSAKEY_SCHEMA.matches(imported_rsa_key)) - - - # Test improperly formatted argument. - self.assertRaises(tuf.FormatError, - repo_tool.import_rsa_privatekey_from_file, 3, 'pw') - - - # Test invalid argument. - # Non-existent key file. - nonexistent_keypath = os.path.join(temporary_directory, - 'nonexistent_keypath') - self.assertRaises(IOError, repo_tool.import_rsa_privatekey_from_file, - nonexistent_keypath, 'pw') - - # Invalid key file argument. - invalid_keyfile = os.path.join(temporary_directory, 'invalid_keyfile') - with open(invalid_keyfile, 'wb') as file_object: - file_object.write(b'bad keyfile') - self.assertRaises(tuf.CryptoError, repo_tool.import_rsa_privatekey_from_file, - invalid_keyfile, 'pw') - - - - def test_import_rsa_publickey_from_file(self): - # Test normal case. - temporary_directory = tempfile.mkdtemp(dir=self.temporary_directory) - - # Load one of the pre-generated key files from 'tuf/tests/repository_data'. - key_filepath = os.path.join('repository_data', 'keystore', - 'root_key.pub') - self.assertTrue(os.path.exists(key_filepath)) - - imported_rsa_key = repo_tool.import_rsa_publickey_from_file(key_filepath) - self.assertTrue(tuf.formats.RSAKEY_SCHEMA.matches(imported_rsa_key)) - - - # Test improperly formatted argument. - self.assertRaises(tuf.FormatError, - repo_tool.import_rsa_privatekey_from_file, 3) - - - # Test invalid argument. - # Non-existent key file. - nonexistent_keypath = os.path.join(temporary_directory, - 'nonexistent_keypath') - self.assertRaises(IOError, repo_tool.import_rsa_publickey_from_file, - nonexistent_keypath) - - # Invalid key file argument. - invalid_keyfile = os.path.join(temporary_directory, 'invalid_keyfile') - with open(invalid_keyfile, 'wb') as file_object: - file_object.write(b'bad keyfile') - self.assertRaises(tuf.Error, repo_tool.import_rsa_publickey_from_file, - invalid_keyfile) - - - - def test_generate_and_write_ed25519_keypair(self): - - # Test normal case. - temporary_directory = tempfile.mkdtemp(dir=self.temporary_directory) - test_keypath = os.path.join(temporary_directory, 'ed25519_key') - - repo_tool.generate_and_write_ed25519_keypair(test_keypath, password='pw') - self.assertTrue(os.path.exists(test_keypath)) - self.assertTrue(os.path.exists(test_keypath + '.pub')) - - # Ensure the generated key files are importable. - imported_pubkey = \ - repo_tool.import_ed25519_publickey_from_file(test_keypath + '.pub') - self.assertTrue(tuf.formats.ED25519KEY_SCHEMA.matches(imported_pubkey)) - - imported_privkey = \ - repo_tool.import_ed25519_privatekey_from_file(test_keypath, 'pw') - self.assertTrue(tuf.formats.ED25519KEY_SCHEMA.matches(imported_privkey)) - - - # Test improperly formatted arguments. - self.assertRaises(tuf.FormatError, - repo_tool.generate_and_write_ed25519_keypair, - 3, password='pw') - self.assertRaises(tuf.FormatError, repo_tool.generate_and_write_rsa_keypair, - test_keypath, password=3) - - - - def test_import_ed25519_publickey_from_file(self): - # Test normal case. - # Generate ed25519 keys that can be imported. - temporary_directory = tempfile.mkdtemp(dir=self.temporary_directory) - ed25519_keypath = os.path.join(temporary_directory, 'ed25519_key') - repo_tool.generate_and_write_ed25519_keypair(ed25519_keypath, password='pw') - - imported_ed25519_key = \ - repo_tool.import_ed25519_publickey_from_file(ed25519_keypath + '.pub') - self.assertTrue(tuf.formats.ED25519KEY_SCHEMA.matches(imported_ed25519_key)) - - - # Test improperly formatted argument. - self.assertRaises(tuf.FormatError, - repo_tool.import_ed25519_publickey_from_file, 3) - - - # Test invalid argument. - # Non-existent key file. - nonexistent_keypath = os.path.join(temporary_directory, - 'nonexistent_keypath') - self.assertRaises(IOError, repo_tool.import_ed25519_publickey_from_file, - nonexistent_keypath) - - # Invalid key file argument. - invalid_keyfile = os.path.join(temporary_directory, 'invalid_keyfile') - with open(invalid_keyfile, 'wb') as file_object: - file_object.write(b'bad keyfile') - - self.assertRaises(tuf.Error, repo_tool.import_ed25519_publickey_from_file, - invalid_keyfile) - - - - def test_import_ed25519_privatekey_from_file(self): - # Test normal case. - # Generate ed25519 keys that can be imported. - temporary_directory = tempfile.mkdtemp(dir=self.temporary_directory) - ed25519_keypath = os.path.join(temporary_directory, 'ed25519_key') - repo_tool.generate_and_write_ed25519_keypair(ed25519_keypath, password='pw') - - imported_ed25519_key = \ - repo_tool.import_ed25519_privatekey_from_file(ed25519_keypath, 'pw') - self.assertTrue(tuf.formats.ED25519KEY_SCHEMA.matches(imported_ed25519_key)) - - - # Test improperly formatted argument. - self.assertRaises(tuf.FormatError, - repo_tool.import_ed25519_privatekey_from_file, 3, 'pw') - - - # Test invalid argument. - # Non-existent key file. - nonexistent_keypath = os.path.join(temporary_directory, - 'nonexistent_keypath') - self.assertRaises(IOError, repo_tool.import_ed25519_privatekey_from_file, - nonexistent_keypath, 'pw') - - # Invalid key file argument. - invalid_keyfile = os.path.join(temporary_directory, 'invalid_keyfile') - with open(invalid_keyfile, 'wb') as file_object: - file_object.write(b'bad keyfile') - - self.assertRaises(tuf.Error, repo_tool.import_ed25519_privatekey_from_file, - invalid_keyfile, 'pw') - - - - def test_get_metadata_filenames(self): - - # Test normal case. - metadata_directory = os.path.join('metadata/') - filenames = {'root.json': metadata_directory + 'root.json', - 'targets.json': metadata_directory + 'targets.json', - 'snapshot.json': metadata_directory + 'snapshot.json', - 'timestamp.json': metadata_directory + 'timestamp.json'} - - self.assertEqual(filenames, repo_tool.get_metadata_filenames('metadata/')) - - # If a directory argument is not specified, the current working directory - # is used. - metadata_directory = os.getcwd() - filenames = {'root.json': os.path.join(metadata_directory, 'root.json'), - 'targets.json': os.path.join(metadata_directory, 'targets.json'), - 'snapshot.json': os.path.join(metadata_directory, 'snapshot.json'), - 'timestamp.json': os.path.join(metadata_directory, 'timestamp.json')} - self.assertEqual(filenames, repo_tool.get_metadata_filenames()) - - - # Test improperly formatted argument. - self.assertRaises(tuf.FormatError, repo_tool.get_metadata_filenames, 3) - - - - def test_get_metadata_fileinfo(self): - # Test normal case. - temporary_directory = tempfile.mkdtemp(dir=self.temporary_directory) - test_filepath = os.path.join(temporary_directory, 'file.txt') - - with open(test_filepath, 'wt') as file_object: - file_object.write('test file') - - # Generate test fileinfo object. It is assumed SHA256 hashes are computed - # by get_metadata_fileinfo(). - file_length = os.path.getsize(test_filepath) - digest_object = tuf.hash.digest_filename(test_filepath) - file_hashes = {'sha256': digest_object.hexdigest()} - fileinfo = {'length': file_length, 'hashes': file_hashes} - self.assertTrue(tuf.formats.FILEINFO_SCHEMA.matches(fileinfo)) - - self.assertEqual(fileinfo, repo_tool.get_metadata_fileinfo(test_filepath)) - - - # Test improperly formatted argument. - self.assertRaises(tuf.FormatError, repo_tool.get_metadata_fileinfo, 3) - - - # Test non-existent file. - nonexistent_filepath = os.path.join(temporary_directory, 'oops.txt') - self.assertRaises(tuf.Error, repo_tool.get_metadata_fileinfo, - nonexistent_filepath) - - - - def test_get_target_hash(self): - # Test normal case. - expected_target_hashes = { - '/file1.txt': 'e3a3d89eb3b70ce3fbce6017d7b8c12d4abd5635427a0e8a238f53157df85b3d', - '/README.txt': '8faee106f1bb69f34aaf1df1e3c2e87d763c4d878cb96b91db13495e32ceb0b0', - '/packages/file2.txt': 'c9c4a5cdd84858dd6a23d98d7e6e6b2aec45034946c16b2200bc317c75415e92' - } - for filepath, target_hash in six.iteritems(expected_target_hashes): - self.assertTrue(tuf.formats.RELPATH_SCHEMA.matches(filepath)) - self.assertTrue(tuf.formats.HASH_SCHEMA.matches(target_hash)) - self.assertEqual(repo_tool.get_target_hash(filepath), target_hash) - - # Test for improperly formatted argument. - self.assertRaises(tuf.FormatError, repo_tool.get_target_hash, 8) - - - - def test_generate_root_metadata(self): - # Test normal case. - # Load the root metadata provided in 'tuf/tests/repository_data/'. - root_filepath = os.path.join('repository_data', 'repository', - 'metadata', 'root.json') - root_signable = tuf.util.load_json_file(root_filepath) - - # generate_root_metadata() expects the top-level roles and keys to be - # available in 'tuf.keydb' and 'tuf.roledb'. - tuf.roledb.create_roledb_from_root_metadata(root_signable['signed']) - tuf.keydb.create_keydb_from_root_metadata(root_signable['signed']) - expires = '1985-10-21T01:22:00Z' - - root_metadata = repo_tool.generate_root_metadata(1, expires, - consistent_snapshot=False) - self.assertTrue(tuf.formats.ROOT_SCHEMA.matches(root_metadata)) - - - # Test improperly formatted arguments. - self.assertRaises(tuf.FormatError, repo_tool.generate_root_metadata, - '3', expires, False) - self.assertRaises(tuf.FormatError, repo_tool.generate_root_metadata, - 1, '3', False) - self.assertRaises(tuf.FormatError, repo_tool.generate_root_metadata, - 1, expires, 3) - - # Test for missing required roles and keys. - tuf.roledb.clear_roledb() - tuf.keydb.clear_keydb() - self.assertRaises(tuf.Error, repo_tool.generate_root_metadata, - 1, expires, False) - - - - def test_generate_targets_metadata(self): - # Test normal case. - temporary_directory = tempfile.mkdtemp(dir=self.temporary_directory) - targets_directory = os.path.join(temporary_directory, 'targets') - file1_path = os.path.join(targets_directory, 'file.txt') - tuf.util.ensure_parent_dir(file1_path) - - with open(file1_path, 'wt') as file_object: - file_object.write('test file.') - - # Set valid generate_targets_metadata() arguments. - version = 1 - datetime_object = datetime.datetime(2030, 1, 1, 12, 0) - expiration_date = datetime_object.isoformat() + 'Z' - target_files = ['file.txt'] - - delegations = {"keys": { - "a394c28384648328b16731f81440d72243c77bb44c07c040be99347f0df7d7bf": { - "keytype": "ed25519", - "keyval": { - "public": "3eb81026ded5af2c61fb3d4b272ac53cd1049a810ee88f4df1fc35cdaf918157" - } - } - }, - "roles": [ - { - "keyids": [ - "a394c28384648328b16731f81440d72243c77bb44c07c040be99347f0df7d7bf" - ], - "name": "targets/warehouse", - "paths": [ - "/file1.txt", "/README.txt", '/warehouse/' - ], - "threshold": 1 - } - ] - } - - targets_metadata = \ - repo_tool.generate_targets_metadata(targets_directory, target_files, - version, expiration_date, delegations, - False) - self.assertTrue(tuf.formats.TARGETS_SCHEMA.matches(targets_metadata)) - - # Verify that 'digest.filename' file is saved to 'targets_directory' if - # the 'write_consistent_targets' argument is True. - list_targets_directory = os.listdir(targets_directory) - targets_metadata = \ - repo_tool.generate_targets_metadata(targets_directory, target_files, - version, expiration_date, delegations, - write_consistent_targets=True) - new_list_targets_directory = os.listdir(targets_directory) - - # Verify that 'targets_directory' contains only one extra item. - self.assertTrue(len(list_targets_directory) + 1, - len(new_list_targets_directory)) - - - # Test improperly formatted arguments. - self.assertRaises(tuf.FormatError, repo_tool.generate_targets_metadata, - 3, target_files, version, expiration_date) - self.assertRaises(tuf.FormatError, repo_tool.generate_targets_metadata, - targets_directory, 3, version, expiration_date) - self.assertRaises(tuf.FormatError, repo_tool.generate_targets_metadata, - targets_directory, target_files, '3', expiration_date) - self.assertRaises(tuf.FormatError, repo_tool.generate_targets_metadata, - targets_directory, target_files, version, '3') - - # Improperly formatted 'delegations' and 'write_consistent_targets' - self.assertRaises(tuf.FormatError, repo_tool.generate_targets_metadata, - targets_directory, target_files, version, expiration_date, - 3, False) - self.assertRaises(tuf.FormatError, repo_tool.generate_targets_metadata, - targets_directory, target_files, version, expiration_date, - delegations, 3) - - - # Test invalid 'target_files' argument. - self.assertRaises(tuf.Error, repo_tool.generate_targets_metadata, - targets_directory, ['nonexistent_file.txt'], version, - expiration_date) - - - - - def test_generate_snapshot_metadata(self): - # Test normal case. - temporary_directory = tempfile.mkdtemp(dir=self.temporary_directory) - original_repository_path = os.path.join('repository_data', - 'repository') - repository_directory = os.path.join(temporary_directory, 'repository') - shutil.copytree(original_repository_path, repository_directory) - metadata_directory = os.path.join(repository_directory, - repo_tool.METADATA_STAGED_DIRECTORY_NAME) - root_filename = os.path.join(metadata_directory, repo_tool.ROOT_FILENAME) - targets_filename = os.path.join(metadata_directory, - repo_tool.TARGETS_FILENAME) - version = 1 - expiration_date = '1985-10-21T13:20:00Z' - - snapshot_metadata = \ - repo_tool.generate_snapshot_metadata(metadata_directory, version, - expiration_date, root_filename, - targets_filename, - consistent_snapshot=False) - self.assertTrue(tuf.formats.SNAPSHOT_SCHEMA.matches(snapshot_metadata)) - - - # Test improperly formatted arguments. - self.assertRaises(tuf.FormatError, repo_tool.generate_snapshot_metadata, - 3, version, expiration_date, - root_filename, targets_filename, consistent_snapshot=False) - self.assertRaises(tuf.FormatError, repo_tool.generate_snapshot_metadata, - metadata_directory, '3', expiration_date, - root_filename, targets_filename, consistent_snapshot=False) - self.assertRaises(tuf.FormatError, repo_tool.generate_snapshot_metadata, - metadata_directory, version, '3', - root_filename, targets_filename, consistent_snapshot=False) - self.assertRaises(tuf.FormatError, repo_tool.generate_snapshot_metadata, - metadata_directory, version, expiration_date, - 3, targets_filename, consistent_snapshot=False) - self.assertRaises(tuf.FormatError, repo_tool.generate_snapshot_metadata, - metadata_directory, version, expiration_date, - root_filename, 3, consistent_snapshot=False) - self.assertRaises(tuf.FormatError, repo_tool.generate_snapshot_metadata, - metadata_directory, version, expiration_date, - root_filename, targets_filename, 3) - - - - def test_generate_timestamp_metadata(self): - # Test normal case. - temporary_directory = tempfile.mkdtemp(dir=self.temporary_directory) - original_repository_path = os.path.join('repository_data', - 'repository') - repository_directory = os.path.join(temporary_directory, 'repository') - shutil.copytree(original_repository_path, repository_directory) - metadata_directory = os.path.join(repository_directory, - repo_tool.METADATA_STAGED_DIRECTORY_NAME) - snapshot_filename = os.path.join(metadata_directory, - repo_tool.SNAPSHOT_FILENAME) - - # Set valid generate_timestamp_metadata() arguments. - version = 1 - expiration_date = '1985-10-21T13:20:00Z' - - compressions = ['gz'] - - snapshot_metadata = \ - repo_tool.generate_timestamp_metadata(snapshot_filename, version, - expiration_date, compressions) - self.assertTrue(tuf.formats.TIMESTAMP_SCHEMA.matches(snapshot_metadata)) - - - # Test improperly formatted arguments. - self.assertRaises(tuf.FormatError, repo_tool.generate_timestamp_metadata, - 3, version, expiration_date, compressions) - self.assertRaises(tuf.FormatError, repo_tool.generate_timestamp_metadata, - snapshot_filename, '3', expiration_date, compressions) - self.assertRaises(tuf.FormatError, repo_tool.generate_timestamp_metadata, - snapshot_filename, version, '3', compressions) - self.assertRaises(tuf.FormatError, repo_tool.generate_timestamp_metadata, - snapshot_filename, version, expiration_date, 3) - self.assertRaises(tuf.FormatError, repo_tool.generate_timestamp_metadata, - snapshot_filename, version, expiration_date, ['compress']) - - - - - def test_sign_metadata(self): - # Test normal case. - temporary_directory = tempfile.mkdtemp(dir=self.temporary_directory) - metadata_path = os.path.join('repository_data', - 'repository', 'metadata') - keystore_path = os.path.join('repository_data', - 'keystore') - root_filename = os.path.join(metadata_path, 'root.json') - root_metadata = tuf.util.load_json_file(root_filename)['signed'] - - tuf.keydb.create_keydb_from_root_metadata(root_metadata) - tuf.roledb.create_roledb_from_root_metadata(root_metadata) - root_keyids = tuf.roledb.get_role_keyids('root') - - root_private_keypath = os.path.join(keystore_path, 'root_key') - root_private_key = \ - repo_tool.import_rsa_privatekey_from_file(root_private_keypath, - 'password') - - # sign_metadata() expects the private key 'root_metadata' to be in - # 'tuf.keydb'. Remove any public keys that may be loaded before - # adding private key, otherwise a 'tuf.KeyAlreadyExists' exception is - # raised. - tuf.keydb.remove_key(root_private_key['keyid']) - tuf.keydb.add_key(root_private_key) - - root_signable = repo_tool.sign_metadata(root_metadata, root_keyids, - root_filename) - self.assertTrue(tuf.formats.SIGNABLE_SCHEMA.matches(root_signable)) - - - # Test improperly formatted arguments. - self.assertRaises(tuf.FormatError, repo_tool.sign_metadata, 3, root_keyids, - 'root.json') - self.assertRaises(tuf.FormatError, repo_tool.sign_metadata, root_metadata, - 3, 'root.json') - self.assertRaises(tuf.FormatError, repo_tool.sign_metadata, root_metadata, - root_keyids, 3) - - - - def test_write_metadata_file(self): - # Test normal case. - temporary_directory = tempfile.mkdtemp(dir=self.temporary_directory) - metadata_directory = os.path.join('repository_data', - 'repository', 'metadata') - root_filename = os.path.join(metadata_directory, 'root.json') - root_signable = tuf.util.load_json_file(root_filename) - - output_filename = os.path.join(temporary_directory, 'root.json') - compressions = ['gz'] - - self.assertFalse(os.path.exists(output_filename)) - repo_tool.write_metadata_file(root_signable, output_filename, compressions, - consistent_snapshot=False) - self.assertTrue(os.path.exists(output_filename)) - self.assertTrue(os.path.exists(output_filename + '.gz')) - - - # Test improperly formatted arguments. - self.assertRaises(tuf.FormatError, repo_tool.write_metadata_file, - 3, output_filename, compressions, False) - self.assertRaises(tuf.FormatError, repo_tool.write_metadata_file, - root_signable, 3, compressions, False) - self.assertRaises(tuf.FormatError, repo_tool.write_metadata_file, - root_signable, output_filename, 3, False) - self.assertRaises(tuf.FormatError, repo_tool.write_metadata_file, - root_signable, output_filename, compressions, 3) - - - - def test_create_tuf_client_directory(self): - # Test normal case. - temporary_directory = tempfile.mkdtemp(dir=self.temporary_directory) - repository_directory = os.path.join('repository_data', - 'repository') - client_directory = os.path.join(temporary_directory, 'client') - - repo_tool.create_tuf_client_directory(repository_directory, client_directory) - - self.assertTrue(os.path.exists(client_directory)) - metadata_directory = os.path.join(client_directory, 'metadata') - current_directory = os.path.join(metadata_directory, 'current') - previous_directory = os.path.join(metadata_directory, 'previous') - self.assertTrue(os.path.exists(client_directory)) - self.assertTrue(os.path.exists(metadata_directory)) - self.assertTrue(os.path.exists(current_directory)) - self.assertTrue(os.path.exists(previous_directory)) - - - # Test improperly formatted arguments. - self.assertRaises(tuf.FormatError, repo_tool.create_tuf_client_directory, - 3, client_directory) - self.assertRaises(tuf.FormatError, repo_tool.create_tuf_client_directory, - repository_directory, 3) - - - # Test invalid argument (i.e., client directory already exists.) - self.assertRaises(tuf.RepositoryError, repo_tool.create_tuf_client_directory, - repository_directory, client_directory) - - # Run the test cases. if __name__ == '__main__': unittest.main() diff --git a/tests/test_roledb.py b/tests/test_roledb.py index 774b8991..61c0b091 100755 --- a/tests/test_roledb.py +++ b/tests/test_roledb.py @@ -1,3 +1,5 @@ +#!/usr/bin/env python + """ test_roledb.py diff --git a/tests/test_schema.py b/tests/test_schema.py index 11c978a5..e8137fdc 100755 --- a/tests/test_schema.py +++ b/tests/test_schema.py @@ -1,3 +1,5 @@ +#!/usr/bin/env python + """ test_schema.py diff --git a/tests/test_sig.py b/tests/test_sig.py index e618fa92..a9ca535a 100755 --- a/tests/test_sig.py +++ b/tests/test_sig.py @@ -1,3 +1,5 @@ +#!/usr/bin/env python + """ test_sig.py diff --git a/tests/test_util.py b/tests/test_util.py index 660ef98b..22bfd30b 100755 --- a/tests/test_util.py +++ b/tests/test_util.py @@ -138,6 +138,9 @@ def test_A4_tempfile_write(self): data = self.random_string() self.temp_fileobj.write(data.encode('utf-8')) self.assertEqual(data, self.temp_fileobj.read().decode('utf-8')) + + self.temp_fileobj.write(data.encode('utf-8'), auto_flush=False) + self.assertEqual(data, self.temp_fileobj.read().decode('utf-8')) @@ -165,6 +168,7 @@ def _compress_existing_file(self, filepath): f_out.writelines(f_in) f_out.close() f_in.close() + return compressed_filepath else: @@ -217,8 +221,14 @@ def test_A6_tempfile_decompress_temp_file_object(self): # Try decompressing once more. self.assertRaises(tuf.Error, - self.temp_fileobj.decompress_temp_file_object,'gzip') + self.temp_fileobj.decompress_temp_file_object, 'gzip') + # Test decompression of invalid gzip file. + temp_file = tuf.util.TempFile() + fileobj.seek(0) + temp_file.write(fileobj.read()) + temp_file.decompress_temp_file_object('gzip') + def test_B1_get_file_details(self): @@ -316,6 +326,11 @@ def test_B6_load_json_file(self): tuf.util.json.dump(data, fileobj) fileobj.close() self.assertEqual(data, tuf.util.load_json_file(filepath)) + + # Test a gzipped file. + compressed_filepath = self._compress_existing_file(filepath) + self.assertEqual(data, tuf.util.load_json_file(compressed_filepath)) + Errors = (tuf.FormatError, IOError) for bogus_arg in [b'a', 1, [b'a'], {'a':b'b'}]: self.assertRaises(Errors, tuf.util.load_json_file, bogus_arg) @@ -518,6 +533,23 @@ def test_C4_ensure_all_targets_allowed(self): ['path_hash_prefixes']) self.assertRaises(tuf.ForbiddenTargetError, tuf.util.ensure_all_targets_allowed, 'targets/warehouse', ['file5.txt'], parent_delegations) + + + + def test_C5_unittest_toolbox_make_temp_directory(self): + # Verify that the tearDown function does not fail when + # unittest_toolbox.make_temp_directory deletes the generated temp directory + # here. + temp_directory = self.make_temp_directory() + os.rmdir(temp_directory) + + + + def test_c6_get_compressed_length(self): + self.temp_fileobj.write(b'hello world') + self.assertTrue(self.temp_fileobj.get_compressed_length() == 11) + + temp_file = tuf.util.TempFile() diff --git a/tuf/formats.py b/tuf/formats.py index 8e4d868b..b7b3df59 100755 --- a/tuf/formats.py +++ b/tuf/formats.py @@ -1,3 +1,5 @@ +#!/usr/bin/env python + """ formats.py diff --git a/tuf/keys.py b/tuf/keys.py index 1b547bc3..dec76da6 100755 --- a/tuf/keys.py +++ b/tuf/keys.py @@ -1,3 +1,5 @@ +#!/usr/bin/env python + """ keys.py @@ -830,7 +832,7 @@ def verify_signature(key_dict, signature, data): else: valid_signature = tuf.pycrypto_keys.verify_rsa_signature(sig, method, public, data) - else: + else: # pragma: no cover message = 'Unsupported "tuf.conf.RSA_CRYPTO_LIBRARY": '+\ repr(_RSA_CRYPTO_LIBRARY)+'.' raise tuf.UnsupportedLibraryError(message) @@ -843,7 +845,7 @@ def verify_signature(key_dict, signature, data): method, sig, data, use_pynacl=True) # Fall back to the optimized pure python implementation of ed25519. - else: + else: # pragma: no cover valid_signature = tuf.ed25519_keys.verify_signature(public, method, sig, data, use_pynacl=False) @@ -941,7 +943,7 @@ def import_rsakey_from_encrypted_pem(encrypted_pem, password): public, private = \ tuf.pycrypto_keys.create_rsa_public_and_private_from_encrypted_pem(encrypted_pem, password) - else: + else: #pragma: no cover message = 'Invalid crypto library: '+repr(_RSA_CRYPTO_LIBRARY)+'.' raise tuf.UnsupportedLibraryError(message) @@ -1119,12 +1121,13 @@ def encrypt_key(key_object, password): if _GENERAL_CRYPTO_LIBRARY == 'pycrypto': encrypted_key = \ tuf.pycrypto_keys.encrypt_key(key_object, password) - - else: + + # check_crypto_libraries() should have fully verified _GENERAL_CRYPTO_LIBRARY. + else: # pragma: no cover message = 'Invalid crypto library: '+repr(_GENERAL_CRYPTO_LIBRARY)+'.' raise tuf.UnsupportedLibraryError(message) - return encrypted_key.encode('utf-8') + return encrypted_key @@ -1217,7 +1220,8 @@ def decrypt_key(encrypted_key, passphrase): key_object = \ tuf.pycrypto_keys.decrypt_key(encrypted_key, passphrase) - else: + # check_crypto_libraries() should have fully verified _GENERAL_CRYPTO_LIBRARY. + else: # pragma: no cover message = 'Invalid crypto library: '+repr(_GENERAL_CRYPTO_LIBRARY)+'.' raise tuf.UnsupportedLibraryError(message) @@ -1286,7 +1290,7 @@ def create_rsa_encrypted_pem(private_key, passphrase): # Raise 'tuf.UnsupportedLibraryError' if the following libraries, specified in # 'tuf.conf', are unsupported or unavailable: - # 'tuf.conf.RSA_CRYPTO_LIBRARY' and 'tuf.conf.GENERAL_CRYPTO_LIBRARY'. + # 'tuf.conf.GENERAL_CRYPTO_LIBRARY' and 'tuf.conf.RSA_CRYPTO_LIBRARY'. check_crypto_libraries(['rsa', 'general']) encrypted_pem = None @@ -1298,8 +1302,9 @@ def create_rsa_encrypted_pem(private_key, passphrase): if _RSA_CRYPTO_LIBRARY == 'pycrypto': encrypted_pem = \ tuf.pycrypto_keys.create_rsa_encrypted_pem(private_key, passphrase) - - else: + + # check_crypto_libraries() should have fully verified _RSA_CRYPTO_LIBRARY. + else: # pragma: no cover message = 'Invalid crypto library: '+repr(_RSA_CRYPTO_LIBRARY)+'.' raise tuf.UnsupportedLibraryError(message) diff --git a/tuf/pycrypto_keys.py b/tuf/pycrypto_keys.py index 0cd75e44..0f09ec76 100755 --- a/tuf/pycrypto_keys.py +++ b/tuf/pycrypto_keys.py @@ -1,3 +1,5 @@ +#!/usr/bin/env python + """ pycrypto_keys.py diff --git a/tuf/repository_lib.py b/tuf/repository_lib.py new file mode 100755 index 00000000..f75dd8f9 --- /dev/null +++ b/tuf/repository_lib.py @@ -0,0 +1,2215 @@ + +""" + + repository_lib.py + + + Vladimir Diaz + + + June 1, 2014 + + + See LICENSE for licensing information. + + + Provide a library for the repository tool that can create a TUF repository. + The repository tool can be used with the Python interpreter in interactive + mode, or imported directly into a Python module. See 'tuf/README' for the + complete guide to using 'tuf.repository_tool.py'. +""" + +# Help with Python 3 compatibility, where the print statement is a function, an +# implicit relative import is invalid, and the '/' operator performs true +# division. Example: print 'hello world' raises a 'SyntaxError' exception. +from __future__ import print_function +from __future__ import absolute_import +from __future__ import division +from __future__ import unicode_literals + +import os +import errno +import sys +import time +import datetime +import getpass +import logging +import tempfile +import shutil +import json +import gzip +import random + +import tuf +import tuf.formats +import tuf.util +import tuf.keydb +import tuf.roledb +import tuf.keys +import tuf.sig +import tuf.log +import tuf.conf +import tuf._vendor.iso8601 as iso8601 +import tuf._vendor.six as six + + +# See 'log.py' to learn how logging is handled in TUF. +logger = logging.getLogger('tuf.repository_lib') + +# 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 extension of TUF metadata. +METADATA_EXTENSION = '.json' + +# The targets and metadata directory names. Metadata files are written +# to the staged metadata directory instead of the "live" one. +METADATA_STAGED_DIRECTORY_NAME = 'metadata.staged' +METADATA_DIRECTORY_NAME = 'metadata' +TARGETS_DIRECTORY_NAME = 'targets' + +# The metadata filenames of the top-level roles. +ROOT_FILENAME = 'root' + METADATA_EXTENSION +TARGETS_FILENAME = 'targets' + METADATA_EXTENSION +SNAPSHOT_FILENAME = 'snapshot' + METADATA_EXTENSION +TIMESTAMP_FILENAME = 'timestamp' + METADATA_EXTENSION + +# Log warning when metadata expires in n days, or less. +# root = 1 month, snapshot = 1 day, targets = 10 days, timestamp = 1 day. +ROOT_EXPIRES_WARN_SECONDS = 2630000 +SNAPSHOT_EXPIRES_WARN_SECONDS = 86400 +TARGETS_EXPIRES_WARN_SECONDS = 864000 +TIMESTAMP_EXPIRES_WARN_SECONDS = 86400 + +# Supported key types. +SUPPORTED_KEY_TYPES = ['rsa', 'ed25519'] + +# The recognized compression extensions. +SUPPORTED_COMPRESSION_EXTENSIONS = ['.gz'] + +# The full list of supported TUF metadata extensions. +METADATA_EXTENSIONS = ['.json', '.json.gz'] + + +def _generate_and_write_metadata(rolename, metadata_filename, write_partial, + targets_directory, metadata_directory, + consistent_snapshot=False, filenames=None): + """ + Non-public function that can generate and write the metadata of the specified + top-level 'rolename'. It also increments version numbers if: + + 1. write_partial==True and the metadata is the first to be written. + + 2. write_partial=False (i.e., write()), the metadata was not loaded as + partially written, and a write_partial is not needed. + """ + + metadata = None + + # Retrieve the roleinfo of 'rolename' to extract the needed metadata + # attributes, such as version number, expiration, etc. + roleinfo = tuf.roledb.get_roleinfo(rolename) + snapshot_compressions = tuf.roledb.get_roleinfo('snapshot')['compressions'] + + # Generate the appropriate role metadata for 'rolename'. + if rolename == 'root': + metadata = generate_root_metadata(roleinfo['version'], + roleinfo['expires'], consistent_snapshot) + + _log_warning_if_expires_soon(ROOT_FILENAME, roleinfo['expires'], + ROOT_EXPIRES_WARN_SECONDS) + + # Check for the Targets role, including delegated roles. + elif rolename.startswith('targets'): + metadata = generate_targets_metadata(targets_directory, + roleinfo['paths'], + roleinfo['version'], + roleinfo['expires'], + roleinfo['delegations'], + consistent_snapshot) + if rolename == 'targets': + _log_warning_if_expires_soon(TARGETS_FILENAME, roleinfo['expires'], + TARGETS_EXPIRES_WARN_SECONDS) + + elif rolename == 'snapshot': + root_filename = filenames['root'] + targets_filename = filenames['targets'] + metadata = generate_snapshot_metadata(metadata_directory, + roleinfo['version'], + roleinfo['expires'], root_filename, + targets_filename, + consistent_snapshot) + + _log_warning_if_expires_soon(SNAPSHOT_FILENAME, roleinfo['expires'], + SNAPSHOT_EXPIRES_WARN_SECONDS) + + elif rolename == 'timestamp': + snapshot_filename = filenames['snapshot'] + metadata = generate_timestamp_metadata(snapshot_filename, + roleinfo['version'], + roleinfo['expires'], + snapshot_compressions) + + _log_warning_if_expires_soon(TIMESTAMP_FILENAME, roleinfo['expires'], + TIMESTAMP_EXPIRES_WARN_SECONDS) + + signable = sign_metadata(metadata, roleinfo['signing_keyids'], + metadata_filename) + + # Check if the version number of 'rolename' may be automatically incremented, + # depending on whether if partial metadata is loaded or if the metadata is + # written with write() / write_partial(). + # Increment the version number if this is the first partial write. + if write_partial: + temp_signable = sign_metadata(metadata, [], metadata_filename) + temp_signable['signatures'].extend(roleinfo['signatures']) + status = tuf.sig.get_signature_status(temp_signable, rolename) + if len(status['good_sigs']) == 0: + metadata['version'] = metadata['version'] + 1 + signable = sign_metadata(metadata, roleinfo['signing_keyids'], + metadata_filename) + # non-partial write() + else: + if tuf.sig.verify(signable, rolename) and not roleinfo['partial_loaded']: + metadata['version'] = metadata['version'] + 1 + signable = sign_metadata(metadata, roleinfo['signing_keyids'], + metadata_filename) + + # Write the metadata to file if contains a threshold of signatures. + signable['signatures'].extend(roleinfo['signatures']) + + if tuf.sig.verify(signable, rolename) or write_partial: + _remove_invalid_and_duplicate_signatures(signable) + compressions = roleinfo['compressions'] + filename = write_metadata_file(signable, metadata_filename, compressions, + consistent_snapshot) + + # The root and timestamp files should also be written without a digest if + # 'consistent_snaptshots' is True. Client may request a timestamp and root + # file without knowing its digest and file size. + if rolename == 'root' or rolename == 'timestamp': + write_metadata_file(signable, metadata_filename, compressions, + consistent_snapshot=False) + + + # 'signable' contains an invalid threshold of signatures. + else: + message = 'Not enough signatures for ' + repr(metadata_filename) + raise tuf.UnsignedMetadataError(message, signable) + + return signable, filename + + + + + +def _prompt(message, result_type=str): + """ + Non-public function that prompts the user for input by loging 'message', + converting the input to 'result_type', and returning the value to the + caller. + """ + + return result_type(six.moves.input(message)) + + + + + +def _get_password(prompt='Password: ', confirm=False): + """ + Non-public function that returns 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 _metadata_is_partially_loaded(rolename, signable, roleinfo): + """ + Non-public function that determines whether 'rolename' is loaded with + at least 1 good signature, but an insufficient threshold (which means + 'rolename' was written to disk with repository.write_partial(). If 'rolename' + is found to be partially loaded, mark it as partially loaded in its + 'tuf.roledb' roleinfo. This function exists to assist in deciding whether + a role's version number should be incremented when write() or write_parital() + is called. Return True if 'rolename' was partially loaded, False otherwise. + """ + + # The signature status lists the number of good signatures, including + # bad, untrusted, unknown, etc. + status = tuf.sig.get_signature_status(signable, rolename) + + if len(status['good_sigs']) < status['threshold'] and \ + len(status['good_sigs']) >= 1: + return True + + else: + return False + + + + + +def _check_directory(directory): + """ + + Non-public function that ensures '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 _check_role_keys(rolename): + """ + Non-public function that verifies the public and signing keys of 'rolename'. + If either contain an invalid threshold of keys, raise an exception. + 'rolename' is the full rolename (e.g., 'targets/unclaimed/django'). + """ + + # Extract the total number of public and private keys of 'rolename' from its + # roleinfo in 'tuf.roledb'. + 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']) + + # Raise an exception for an invalid threshold of public keys. + if total_keyids < threshold: + message = repr(rolename)+' role contains '+repr(total_keyids)+' / '+ \ + repr(threshold)+' public keys.' + raise tuf.InsufficientKeysError(message) + + # Raise an exception for an invalid threshold of signing keys. + 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 _remove_invalid_and_duplicate_signatures(signable): + """ + Non-public function that removes invalid signatures from 'signable'. + 'signable' may contain signatures (invalid) from previous versions + of the metadata that were loaded with load_repository(). Invalid, or + duplicate signatures are removed from 'signable'. + """ + + # Store the keyids of valid signatures. 'signature_keyids' is checked + # for duplicates rather than comparing signature objects because PSS may + # generate duplicate valid signatures of the same data, yet contain different + # signatures. + signature_keyids = [] + + for signature in signable['signatures']: + signed = signable['signed'] + keyid = signature['keyid'] + key = None + + # Remove 'signature' from 'signable' if the listed keyid does not exist + # in 'tuf.keydb'. + try: + key = tuf.keydb.get_key(keyid) + + except tuf.UnknownKeyError as e: + signable['signatures'].remove(signature) + + # Remove 'signature' from 'signable' if it is an invalid signature. + if not tuf.keys.verify_signature(key, signature, signed): + signable['signatures'].remove(signature) + + # Although valid, it may still need removal if it is a duplicate. Check + # the keyid, rather than the signature, to remove duplicate PSS signatures. + # PSS may generate multiple different signatures for the same keyid. + else: + if keyid in signature_keyids: + signable['signatures'].remove(signature) + + # 'keyid' is valid and not a duplicate, so add it to 'signature_keyids'. + else: + signature_keyids.append(keyid) + + + + + +def _delete_obsolete_metadata(metadata_directory, snapshot_metadata, + consistent_snapshot): + """ + Non-public function that deletes metadata files marked as removed by + 'repository_tool.py'. Revoked metadata files are not actually deleted until + this function is called. Obsolete metadata should *not* be retained in + "metadata.staged", otherwise they may be re-loaded by 'load_repository()'. + Note: Obsolete metadata may not always be easily detected (by inspecting + top-level metadata during loading) due to partial metadata and top-level + metadata that have not been written yet. + """ + + # Walk the repository's metadata 'targets' sub-directory, where all the + # metadata of delegated roles is stored. + targets_metadata = os.path.join(metadata_directory, 'targets') + + # The 'targets.json' metadata is not visited, only its child delegations. + # The 'targets/unclaimed/django.json' role would be located in the + # '{repository_directory}/metadata/targets/unclaimed/' directory. + 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) + # Strip the metadata dirname and the leading path separator. + # '{repository_directory}/metadata/targets/unclaimed/django.json' --> + # 'targets/unclaimed/django.json' + metadata_name = \ + metadata_path[len(metadata_directory):].lstrip(os.path.sep) + + # Strip the digest if 'consistent_snapshot' is True. + # Example: 'targets/unclaimed/13df98ab0.django.json' --> + # 'targets/unclaimed/django.json'. Consistent and non-consistent + # metadata might co-exist if write() and write(consistent_snapshot=True) + # are mixed, so ensure only 'digest.filename' metadata is stripped. + embeded_digest = None + if metadata_name not in snapshot_metadata['meta']: + metadata_name, embeded_digest = \ + _strip_consistent_snapshot_digest(metadata_name, consistent_snapshot) + + # Strip filename extensions. The role database does not include the + # metadata extension. + metadata_name_extension = metadata_name + for metadata_extension in METADATA_EXTENSIONS: + if metadata_name.endswith(metadata_extension): + metadata_name = metadata_name[:-len(metadata_extension)] + + # Delete the metadata file if it does not exist in 'tuf.roledb'. + # 'repository_tool.py' might have marked 'metadata_name' as removed, but + # its metadata file is not actually deleted yet. Do it now. + if not tuf.roledb.role_exists(metadata_name): + logger.info('Removing outdated metadata: ' + repr(metadata_path)) + os.remove(metadata_path) + + # Delete outdated consistent snapshots. snapshot metadata includes + # the file extension of roles. + if consistent_snapshot and embeded_digest is not None: + file_hashes = list(snapshot_metadata['meta'][metadata_name_extension] \ + ['hashes'].values()) + if embeded_digest not in file_hashes: + logger.info('Removing outdated metadata: ' + repr(metadata_path)) + os.remove(metadata_path) + + + + + +def _get_written_metadata_and_digests(metadata_signable): + """ + Non-public function that returns the actual content of written metadata and + its digest. + """ + + written_metadata_content = json.dumps(metadata_signable, indent=1, + sort_keys=True).encode('utf-8') + written_metadata_digests = {} + + for hash_algorithm in tuf.conf.REPOSITORY_HASH_ALGORITHMS: + digest_object = tuf.hash.digest(hash_algorithm) + digest_object.update(written_metadata_content) + written_metadata_digests.update({hash_algorithm: digest_object.hexdigest()}) + + return written_metadata_content, written_metadata_digests + + + + + +def _strip_consistent_snapshot_digest(metadata_filename, consistent_snapshot): + """ + Strip from 'metadata_filename' any digest data (in the expected + '{dirname}/digest.filename' format) that it may contain, and return it. + """ + + embeded_digest = '' + + # Strip the digest if 'consistent_snapshot' is True. + # Example: 'targets/unclaimed/13df98ab0.django.json' --> + # 'targets/unclaimed/django.json' + if consistent_snapshot: + dirname, basename = os.path.split(metadata_filename) + embeded_digest = basename[:basename.find('.')] + + # Ensure the digest, including the period, is stripped. + basename = basename[basename.find('.')+1:] + + metadata_filename = os.path.join(dirname, basename) + + + return metadata_filename, embeded_digest + + + + + +def _load_top_level_metadata(repository, top_level_filenames): + """ + Load the metadata of the Root, Timestamp, Targets, and Snapshot roles. + At a minimum, the Root role must exist and successfully load. + """ + + root_filename = top_level_filenames[ROOT_FILENAME] + targets_filename = top_level_filenames[TARGETS_FILENAME] + snapshot_filename = top_level_filenames[SNAPSHOT_FILENAME] + timestamp_filename = top_level_filenames[TIMESTAMP_FILENAME] + + root_metadata = None + targets_metadata = None + snapshot_metadata = None + timestamp_metadata = None + + # Load 'root.json'. A Root role file without a digest is always written. + 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) + + # Load Root's roleinfo and update 'tuf.roledb'. + roleinfo = tuf.roledb.get_roleinfo('root') + roleinfo['signatures'] = [] + for signature in signable['signatures']: + if signature not in roleinfo['signatures']: + roleinfo['signatures'].append(signature) + + if os.path.exists(root_filename+'.gz'): + roleinfo['compressions'].append('gz') + + # By default, roleinfo['partial_loaded'] of top-level roles should be set to + # False in 'create_roledb_from_root_metadata()'. Update this field, if + # necessary, now that we have its signable object. + if _metadata_is_partially_loaded('root', signable, roleinfo): + roleinfo['partial_loaded'] = True + + _log_warning_if_expires_soon(ROOT_FILENAME, roleinfo['expires'], + ROOT_EXPIRES_WARN_SECONDS) + + tuf.roledb.update_roleinfo('root', roleinfo) + + # Ensure the 'consistent_snapshot' field is extracted. + consistent_snapshot = root_metadata['consistent_snapshot'] + + else: + message = 'Cannot load the required root file: '+repr(root_filename) + raise tuf.RepositoryError(message) + + # Load 'timestamp.json'. A Timestamp role file without a digest is always + # written. + 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) + + # Load Timestamp's roleinfo and update 'tuf.roledb'. + 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') + + if _metadata_is_partially_loaded('timestamp', signable, roleinfo): + roleinfo['partial_loaded'] = True + + _log_warning_if_expires_soon(TIMESTAMP_FILENAME, roleinfo['expires'], + TIMESTAMP_EXPIRES_WARN_SECONDS) + + tuf.roledb.update_roleinfo('timestamp', roleinfo) + + else: + pass + + # Load 'snapshot.json'. A consistent snapshot of Snapshot must be calculated + # if 'consistent_snapshot' is True. + if consistent_snapshot: + snapshot_hashes = timestamp_metadata['meta'][SNAPSHOT_FILENAME]['hashes'] + snapshot_digest = random.choice(list(snapshot_hashes.values())) + dirname, basename = os.path.split(snapshot_filename) + snapshot_filename = os.path.join(dirname, snapshot_digest + '.' + basename) + + if os.path.exists(snapshot_filename): + signable = tuf.util.load_json_file(snapshot_filename) + tuf.formats.check_signable_object_format(signable) + snapshot_metadata = signable['signed'] + for signature in signable['signatures']: + repository.snapshot.add_signature(signature) + + # Load Snapshot's roleinfo and update 'tuf.roledb'. + roleinfo = tuf.roledb.get_roleinfo('snapshot') + roleinfo['expires'] = snapshot_metadata['expires'] + roleinfo['version'] = snapshot_metadata['version'] + if os.path.exists(snapshot_filename+'.gz'): + roleinfo['compressions'].append('gz') + + if _metadata_is_partially_loaded('snapshot', signable, roleinfo): + roleinfo['partial_loaded'] = True + + _log_warning_if_expires_soon(SNAPSHOT_FILENAME, roleinfo['expires'], + SNAPSHOT_EXPIRES_WARN_SECONDS) + + tuf.roledb.update_roleinfo('snapshot', roleinfo) + + else: + pass + + # Load 'targets.json'. A consistent snapshot of Targets must be calculated if + # 'consistent_snapshot' is True. + if consistent_snapshot: + targets_hashes = snapshot_metadata['meta'][TARGETS_FILENAME]['hashes'] + targets_digest = random.choice(list(targets_hashes.values())) + dirname, basename = os.path.split(targets_filename) + targets_filename = os.path.join(dirname, targets_digest + '.' + basename) + + 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.json' in 'tuf.roledb.py' + roleinfo = tuf.roledb.get_roleinfo('targets') + roleinfo['paths'] = list(targets_metadata['targets'].keys()) + 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') + + if _metadata_is_partially_loaded('targets', signable, roleinfo): + roleinfo['partial_loaded'] = True + + _log_warning_if_expires_soon(TARGETS_FILENAME, roleinfo['expires'], + TARGETS_EXPIRES_WARN_SECONDS) + + tuf.roledb.update_roleinfo('targets', roleinfo) + + # Add the keys specified in the delegations field of the Targets role. + for key_metadata in six.itervalues(targets_metadata['delegations']['keys']): + key_object = tuf.keys.format_metadata_to_key(key_metadata) + + # Add 'key_object' to the list of recognized keys. Keys may be shared, + # so do not raise an exception if 'key_object' has already been loaded. + # In contrast to the methods that may add duplicate keys, do not log + # a warning as there may be many such duplicate key warnings. The + # repository maintainer should have also been made aware of the duplicate + # key when it was added. + try: + tuf.keydb.add_key(key_object) + + except tuf.KeyAlreadyExistsError as e: + pass + + for role in targets_metadata['delegations']['roles']: + rolename = role['name'] + roleinfo = {'name': role['name'], 'keyids': role['keyids'], + 'threshold': role['threshold'], 'compressions': [''], + 'signing_keyids': [], 'partial_loaded': False, + 'signatures': [], 'delegations': {'keys': {}, + 'roles': []}} + tuf.roledb.add_role(rolename, roleinfo) + + else: + pass + + return repository, consistent_snapshot + + + + +def _log_warning_if_expires_soon(rolename, expires_iso8601_timestamp, + seconds_remaining_to_warn): + """ + Non-public function that logs a warning if 'rolename' expires in + 'seconds_remaining_to_warn' seconds, or less. + """ + + # Metadata stores expiration datetimes in ISO8601 format. Convert to + # unix timestamp, subtract from from current time.time() (also in POSIX time) + # and compare against 'seconds_remaining_to_warn'. Log a warning message + # to console if 'rolename' expires soon. + datetime_object = iso8601.parse_date(expires_iso8601_timestamp) + expires_unix_timestamp = \ + tuf.formats.datetime_to_unix_timestamp(datetime_object) + seconds_until_expires = expires_unix_timestamp - int(time.time()) + + if seconds_until_expires <= seconds_remaining_to_warn: + days_until_expires = seconds_until_expires / 86400 + + message = repr(rolename) + ' expires ' + datetime_object.ctime() + \ + ' (UTC).\n' + repr(days_until_expires) + ' day(s) until it expires.' + + logger.warning(message) + + + + + +def generate_and_write_rsa_keypair(filepath, bits=DEFAULT_RSA_KEY_BITS, + password=None): + """ + + Generate an RSA key file, create an encrypted PEM string (using 'password' + as the pass phrase), and store it in 'filepath'. The public key portion of + the generated RSA key is stored in <'filepath'>.pub. Which cryptography + library performs the cryptographic decryption is determined by the string + set in 'tuf.conf.RSA_CRYPTO_LIBRARY'. PyCrypto currently supported. The + PEM private key is encrypted with 3DES and CBC the mode of operation. The + password is strengthened with PBKDF1-MD5. + + + filepath: + The public and private key files are saved to .pub, , + respectively. + + bits: + The number of bits of the generated RSA key. + + password: + The password used to encrypt 'filepath'. + + + tuf.FormatError, if the arguments are improperly formatted. + + + Writes key files to '' and '.pub'. + + + None. + """ + + # Do the arguments have the correct format? + # This check ensures arguments have the appropriate number of + # objects and object types, and that all dict keys are properly named. + # 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 file: ' + password = _get_password(message, confirm=True) + + # Does 'password' have the correct format? + tuf.formats.PASSWORD_SCHEMA.check_match(password) + + # Generate public and private RSA keys, encrypted the private portion + # and store them in PEM format. + 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'. If the parent directory of filepath does not exist, + # create it (and all its parent directories, if necessary). + tuf.util.ensure_parent_dir(filepath) + + # Create a tempororary file, write the contents of the public key, and move + # to final destination. + file_object = tuf.util.TempFile() + file_object.write(public.encode('utf-8')) + + # The temporary file is closed after the final move. + file_object.move(filepath+'.pub') + + # Write the private key in encrypted PEM format to ''. + # Unlike the public key file, the private key does not have a file + # extension. + file_object = tuf.util.TempFile() + file_object.write(encrypted_pem.encode('utf-8')) + file_object.move(filepath) + + + + + +def import_rsa_privatekey_from_file(filepath, password=None): + """ + + Import the encrypted PEM file in 'filepath', decrypt it, and return the key + object in 'tuf.formats.RSAKEY_SCHEMA' format. + + Which cryptography library performs the cryptographic decryption is + determined by the string set in 'tuf.conf.RSA_CRYPTO_LIBRARY'. PyCrypto + currently supported. + + The PEM private key is encrypted with 3DES and CBC the mode of operation. + The password is strengthened with PBKDF1-MD5. + + + filepath: + file, an RSA encrypted PEM file. Unlike the public RSA PEM + key file, 'filepath' does not have an extension. + + password: + The passphrase to decrypt 'filepath'. + + + tuf.FormatError, if the arguments are improperly formatted. + + tuf.CryptoError, if 'filepath' is not a valid encrypted key file. + + + The contents of 'filepath' is read, decrypted, and the key stored. + + + An RSA key object, conformant to 'tuf.formats.RSAKEY_SCHEMA'. + """ + + # Does 'filepath' have the correct format? + # Ensure the arguments have the appropriate number of objects and object + # types, and that all dict keys are properly named. + # 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. + # Password confirmation disabled here, which should ideally happen only + # when creating encrypted key files (i.e., improve usability). + if password is None: + message = 'Enter a password for the encrypted RSA file: ' + password = _get_password(message, confirm=False) + + # Does 'password' have the correct format? + tuf.formats.PASSWORD_SCHEMA.check_match(password) + + encrypted_pem = None + + # Read the contents of 'filepath' that should be an encrypted PEM. + with open(filepath, 'rb') as file_object: + encrypted_pem = file_object.read().decode('utf-8') + + # Convert 'encrypted_pem' to 'tuf.formats.RSAKEY_SCHEMA' format. Raise + # 'tuf.CryptoError' if 'encrypted_pem' is invalid. + rsa_key = tuf.keys.import_rsakey_from_encrypted_pem(encrypted_pem, password) + + return rsa_key + + + + + +def import_rsa_publickey_from_file(filepath): + """ + + Import the RSA key stored in 'filepath'. The key object returned is a TUF + key, specifically 'tuf.formats.RSAKEY_SCHEMA'. If the RSA PEM in 'filepath' + contains a private key, it is discarded. + + Which cryptography library performs the cryptographic decryption is + determined by the string set in 'tuf.conf.RSA_CRYPTO_LIBRARY'. PyCrypto + currently supported. 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. + + tuf.Error, if a valid RSA key object cannot be generated. This may be + caused by an improperly formatted PEM file. + + + 'filepath' is read and its contents extracted. + + + An RSA key object conformant to 'tuf.formats.RSAKEY_SCHEMA'. + """ + + # Does 'filepath' have the correct format? + # Ensure the arguments have the appropriate number of objects and object + # types, and that all dict keys are properly named. + # Raise 'tuf.FormatError' if there is a mismatch. + tuf.formats.PATH_SCHEMA.check_match(filepath) + + # Read the contents of the key file that should be in PEM format and contains + # the public portion of the RSA key. + with open(filepath, 'rb') as file_object: + rsa_pubkey_pem = file_object.read().decode('utf-8') + + # Convert 'rsa_pubkey_pem' to 'tuf.formats.RSAKEY_SCHEMA' format. + try: + rsakey_dict = tuf.keys.format_rsakey_from_pem(rsa_pubkey_pem) + + except tuf.FormatError as e: + raise tuf.Error('Cannot import improperly formatted PEM file.') + + return rsakey_dict + + + + + +def generate_and_write_ed25519_keypair(filepath, password=None): + """ + + Generate an ED25519 key file, create an encrypted TUF key (using 'password' + as the pass phrase), and store it in 'filepath'. The public key portion of + the generated ED25519 key is stored in <'filepath'>.pub. Which cryptography + library performs the cryptographic decryption is determined by the string + set in 'tuf.conf.ED25519_CRYPTO_LIBRARY'. + + PyCrypto currently supported. The ED25519 private key is encrypted with + AES-256 and CTR the mode of operation. The password is strengthened with + PBKDF2-HMAC-SHA256. + + + filepath: + The public and private key files are saved to .pub and + , respectively. + + password: + The password, or passphrase, to encrypt the private portion of the + generated ed25519 key. A symmetric encryption key is derived from + 'password', so it is not directly used. + + + tuf.FormatError, if the arguments are improperly formatted. + + tuf.CryptoError, if 'filepath' cannot be encrypted. + + tuf.UnsupportedLibraryError, if 'filepath' cannot be encrypted due to an + invalid configuration setting (i.e., invalid 'tuf.conf.py' setting). + + + Writes key files to '' and '.pub'. + + + None. + """ + + # Does 'filepath' have the correct format? + # Ensure the arguments have the appropriate number of objects and object + # types, and that all dict keys are properly named. + # 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 ED25519 key: ' + password = _get_password(message, confirm=True) + + # Does 'password' have the correct format? + tuf.formats.PASSWORD_SCHEMA.check_match(password) + + # Generate a new ED25519 key object and encrypt it. The cryptography library + # used is determined by the user, or by default (set in + # 'tuf.conf.ED25519_CRYPTO_LIBRARY'). Raise 'tuf.CryptoError' or + # 'tuf.UnsupportedLibraryError', if 'ed25519_key' cannot be encrypted. + ed25519_key = tuf.keys.generate_ed25519_key() + encrypted_key = tuf.keys.encrypt_key(ed25519_key, password) + + # ed25519 public key file contents in metadata format (i.e., does not include + # the keyid portion). + keytype = ed25519_key['keytype'] + keyval = ed25519_key['keyval'] + ed25519key_metadata_format = \ + tuf.keys.format_keyval_to_metadata(keytype, keyval, private=False) + + # Write the public key, conformant to 'tuf.formats.KEY_SCHEMA', to + # '.pub'. + tuf.util.ensure_parent_dir(filepath) + + # Create a tempororary file, write the contents of the public key, and move + # to final destination. + file_object = tuf.util.TempFile() + file_object.write(json.dumps(ed25519key_metadata_format).encode('utf-8')) + + # The temporary file is closed after the final move. + file_object.move(filepath+'.pub') + + # Write the encrypted key string, conformant to + # 'tuf.formats.ENCRYPTEDKEY_SCHEMA', to ''. + file_object = tuf.util.TempFile() + file_object.write(encrypted_key.encode('utf-8')) + file_object.move(filepath) + + + + + +def import_ed25519_publickey_from_file(filepath): + """ + + Load the ED25519 public key object (conformant to 'tuf.formats.KEY_SCHEMA') + stored in 'filepath'. Return 'filepath' in tuf.formats.ED25519KEY_SCHEMA + format. + + If the TUF key object in 'filepath' contains a private key, it is discarded. + + + filepath: + .pub file, a TUF public key file. + + + tuf.FormatError, if 'filepath' is improperly formatted or is an unexpected + key type. + + + The contents of 'filepath' is read and saved. + + + An ED25519 key object conformant to 'tuf.formats.ED25519KEY_SCHEMA'. + """ + + # Does 'filepath' have the correct format? + # Ensure the arguments have the appropriate number of objects and object + # types, and that all dict keys are properly named. + # Raise 'tuf.FormatError' if there is a mismatch. + tuf.formats.PATH_SCHEMA.check_match(filepath) + + # ED25519 key objects are saved in json and metadata format. Return the + # loaded key object in tuf.formats.ED25519KEY_SCHEMA' format that also + # includes the keyid. + ed25519_key_metadata = tuf.util.load_json_file(filepath) + ed25519_key = tuf.keys.format_metadata_to_key(ed25519_key_metadata) + + # Raise an exception if an unexpected key type is imported. + if ed25519_key['keytype'] != 'ed25519': + message = 'Invalid key type loaded: '+repr(ed25519_key['keytype']) + raise tuf.FormatError(message) + + return ed25519_key + + + + + +def import_ed25519_privatekey_from_file(filepath, password=None): + """ + + Import the encrypted ed25519 TUF key file in 'filepath', decrypt it, and + return the key object in 'tuf.formats.ED25519KEY_SCHEMA' format. + + Which cryptography library performs the cryptographic decryption is + determined by the string set in 'tuf.conf.ED25519_CRYPTO_LIBRARY'. PyCrypto + currently supported. + + The TUF private key (may also contain the public part) is encrypted with AES + 256 and CTR the mode of operation. The password is strengthened with + PBKDF2-HMAC-SHA256. + + + filepath: + file, an RSA encrypted TUF key file. + + password: + The password, or passphrase, to import the private key (i.e., the + encrypted key file 'filepath' must be decrypted before the ed25519 key + object can be returned. + + + tuf.FormatError, if the arguments are improperly formatted or the imported + key object contains an invalid key type (i.e., not 'ed25519'). + + tuf.CryptoError, if 'filepath' cannot be decrypted. + + tuf.UnsupportedLibraryError, if 'filepath' cannot be decrypted due to an + invalid configuration setting (i.e., invalid 'tuf.conf.py' setting). + + + 'password' is used to decrypt the 'filepath' key file. + + + An ed25519 key object of the form: 'tuf.formats.ED25519KEY_SCHEMA'. + """ + + # Does 'filepath' have the correct format? + # Ensure the arguments have the appropriate number of objects and object + # types, and that all dict keys are properly named. + # 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. + # Password confirmation disabled here, which should ideally happen only + # when creating encrypted key files (i.e., improve usability). + if password is None: + message = 'Enter a password for the encrypted ED25519 key: ' + password = _get_password(message, confirm=False) + + # Does 'password' have the correct format? + tuf.formats.PASSWORD_SCHEMA.check_match(password) + + # Store the encrypted contents of 'filepath' prior to calling the decryption + # routine. + encrypted_key = None + + with open(filepath, 'rb') as file_object: + encrypted_key = file_object.read() + + # Decrypt the loaded key file, calling the appropriate cryptography library + # (i.e., set by the user) and generating the derived encryption key from + # 'password'. Raise 'tuf.CryptoError' or 'tuf.UnsupportedLibraryError' if the + # decryption fails. + key_object = tuf.keys.decrypt_key(encrypted_key, password) + + # Raise an exception if an unexpected key type is imported. + if key_object['keytype'] != 'ed25519': + message = 'Invalid key type loaded: '+repr(key_object['keytype']) + raise tuf.FormatError(message) + + return key_object + + + + + +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.json': 'metadata/root.json', + 'targets.json': 'metadata/targets.json', + 'snapshot.json': 'metadata/snapshot.json', + 'timestamp.json': 'metadata/timestamp.json'} + + If '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 'snapshot.json'. + """ + + if metadata_directory is None: + metadata_directory = os.getcwd() + + # Does 'metadata_directory' have the correct format? + # Ensure the arguments have the appropriate number of objects and object + # types, and that all dict keys are properly named. + # Raise 'tuf.FormatError' if there is a mismatch. + tuf.formats.PATH_SCHEMA.check_match(metadata_directory) + + # Store the filepaths of the top-level roles, including the + # 'metadata_directory' for each one. + filenames = {} + + filenames[ROOT_FILENAME] = \ + os.path.join(metadata_directory, ROOT_FILENAME) + + filenames[TARGETS_FILENAME] = \ + os.path.join(metadata_directory, TARGETS_FILENAME) + + filenames[SNAPSHOT_FILENAME] = \ + os.path.join(metadata_directory, SNAPSHOT_FILENAME) + + filenames[TIMESTAMP_FILENAME] = \ + os.path.join(metadata_directory, TIMESTAMP_FILENAME) + + return filenames + + + + + +def get_metadata_fileinfo(filename): + """ + + Retrieve the file information of 'filename'. The object returned + conforms to 'tuf.formats.FILEINFO_SCHEMA'. The information + generated for 'filename' is stored in metadata files like 'targets.json'. + The fileinfo object returned has the form: + + fileinfo = {'length': 1024, + 'hashes': {'sha256': 1233dfba312, ...}, + 'custom': {...}} + + + filename: + The metadata file whose file information is needed. It must exist. + + + 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. SHA256 hashes are generated by default. + """ + + # Does 'filename' have the correct format? + # Ensure the arguments have the appropriate number of objects and object + # types, and that all dict keys are properly named. + # 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, tuf.conf.REPOSITORY_HASH_ALGORITHMS) + custom = None + + return tuf.formats.make_fileinfo(filesize, filehashes, custom) + + + + + + +def get_target_hash(target_filepath): + """ + + Compute the hash of 'target_filepath'. This is useful in conjunction with + the "path_hash_prefixes" attribute in a delegated targets role, which + tells us which paths it is implicitly responsible for. + + The repository may optionally organize targets into hashed bins to ease + target delegations and role metadata management. The use of consistent + hashing allows for a uniform distribution of targets into bins. + + + target_filepath: + The path to the target file on the repository. This will be relative to + the 'targets' (or equivalent) directory on a given mirror. + + + None. + + + None. + + + The hash of 'target_filepath'. + """ + + return tuf.util.get_target_hash(target_filepath) + + + + + +def generate_root_metadata(version, expiration_date, consistent_snapshot): + """ + + Create the root metadata. 'tuf.roledb.py' and 'tuf.keydb.py' are read and + the information returned by these modules is used to generate the root + metadata object. + + + 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 of the metadata file. Conformant to + 'tuf.formats.ISO8601_DATETIME_SCHEMA'. + + consistent_snapshot: + Boolean. If True, a file digest is expected to be prepended to the + filename of any target file located in the targets directory. Each digest + is stripped from the target filename and listed in the snapshot metadata. + + + 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 (e.g., a required top-level role not found in 'tuf.roledb'.) + + + The contents of 'tuf.keydb.py' and 'tuf.roledb.py' are read. + + + A root metadata object, conformant to 'tuf.formats.ROOT_SCHEMA'. + """ + + # Do the arguments have the correct format? + # Ensure the arguments have the appropriate number of objects and object + # types, and that all dict keys are properly named. + # Raise 'tuf.FormatError' if any of the arguments are improperly formatted. + tuf.formats.METADATAVERSION_SCHEMA.check_match(version) + tuf.formats.ISO8601_DATETIME_SCHEMA.check_match(expiration_date) + tuf.formats.BOOLEAN_SCHEMA.check_match(consistent_snapshot) + + # The role and key dictionaries to be saved in the root metadata object. + # Conformant to 'ROLEDICT_SCHEMA' and 'KEYDICT_SCHEMA', respectively. + roledict = {} + keydict = {} + + # Extract the role, threshold, and keyid information of the top-level roles, + # which Root stores in its metadata. The necessary role metadata is generated + # from this information. + for rolename in ['root', 'targets', 'snapshot', 'timestamp']: + + # 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".') + + # Keep track of the keys loaded to avoid duplicates. + keyids = [] + + # Generate keys for the keyids listed by the role being processed. + 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: + # {'keytype': 'rsa', + # 'keyid': keyid, + # 'keyval': {'public': '-----BEGIN RSA PUBLIC KEY----- ...', + # 'private': '-----BEGIN RSA PRIVATE KEY----- ...'}} + keyid = key['keyid'] + if keyid not in keydict: + + # This appears to be a new keyid. Generate the key for it. + if key['keytype'] in ['rsa', 'ed25519']: + keytype = key['keytype'] + keyval = key['keyval'] + keydict[keyid] = \ + tuf.keys.format_keyval_to_metadata(keytype, keyval, private=False) + + # This is not a recognized key. Raise an exception. + else: + raise tuf.Error('Unsupported keytype: '+keyid) + + # 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_threshold = tuf.roledb.get_role_threshold(rolename) + role_metadata = tuf.formats.make_role_metadata(keyids, role_threshold) + roledict[rolename] = role_metadata + + # Generate the root metadata object. + root_metadata = tuf.formats.RootFile.make_metadata(version, expiration_date, + keydict, roledict, + consistent_snapshot) + + return root_metadata + + + + + +def generate_targets_metadata(targets_directory, target_files, version, + expiration_date, delegations=None, + write_consistent_targets=False): + """ + + Generate the targets metadata object. The targets in 'target_files' must + exist at the same path they should on the repo. 'target_files' is a list of + targets. The 'custom' field of the targets metadata is not currently + supported. + + + targets_directory: + The directory containing the target files and directories of the + repository. + + target_files: + The target files tracked by 'targets.json'. 'target_files' is a list of + target paths that are relative to the targets directory (e.g., + ['file1.txt', 'Django/module.py']). + + 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 of the metadata file. Conformant to + 'tuf.formats.ISO8601_DATETIME_SCHEMA'. + + delegations: + The delegations made by the targets role to be generated. 'delegations' + must match 'tuf.formats.DELEGATIONS_SCHEMA'. + + write_consistent_targets: + Boolean that indicates whether file digests should be prepended to the + target files. + + + tuf.FormatError, if an error occurred trying to generate the targets + metadata object. + + tuf.Error, if any of the target files cannot be read. + + + The target files are read and file information generated about them. + + + A targets metadata object, conformant to 'tuf.formats.TARGETS_SCHEMA'. + """ + + # Do the arguments have the correct format? + # Ensure the arguments have the appropriate number of objects and object + # types, and that all dict keys are properly named. + # 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.METADATAVERSION_SCHEMA.check_match(version) + tuf.formats.ISO8601_DATETIME_SCHEMA.check_match(expiration_date) + tuf.formats.BOOLEAN_SCHEMA.check_match(write_consistent_targets) + + if delegations is not None: + tuf.formats.DELEGATIONS_SCHEMA.check_match(delegations) + + # Store the file attributes of targets in 'target_files'. 'filedict', + # conformant to 'tuf.formats.FILEDICT_SCHEMA', is added to the targets + # metadata object returned. + filedict = {} + + # Ensure the user is aware of a non-existent 'target_directory', and convert + # it to its abosolute path, if it exists. + targets_directory = _check_directory(targets_directory) + + # Generate the fileinfo of all the target files listed in 'target_files'. + for target in target_files: + + # The root-most folder of the targets directory should not be included in + # target paths listed in targets metadata. + # (e.g., 'targets/more_targets/somefile.txt' -> 'more_targets/somefile.txt') + relative_targetpath = target + + # Note: join() discards 'targets_directory' if 'target' contains a leading + # path separator (i.e., is treated as an absolute path). + target_path = os.path.join(targets_directory, target.lstrip(os.sep)) + + # Ensure all target files listed in 'target_files' exist. If just one of + # these files does not exist, raise an exception. + if not os.path.exists(target_path): + message = repr(target_path)+' cannot be read. Unable to generate '+ \ + 'targets metadata.' + raise tuf.Error(message) + + filedict[relative_targetpath] = get_metadata_fileinfo(target_path) + + if write_consistent_targets: + for target_digest in filedict[relative_targetpath]['hashes']: + dirname, basename = os.path.split(target_path) + digest_filename = target_digest + '.' + basename + digest_target = os.path.join(dirname, digest_filename) + + if not os.path.exists(digest_target): + logger.warning('Hard linking target file to ' + repr(digest_target)) + os.link(target_path, digest_target) + + # Generate the targets metadata object. + targets_metadata = tuf.formats.TargetsFile.make_metadata(version, + expiration_date, + filedict, + delegations) + + return targets_metadata + + + + + +def generate_snapshot_metadata(metadata_directory, version, expiration_date, + root_filename, targets_filename, + consistent_snapshot=False): + """ + + Create the snapshot metadata. The minimum metadata must exist + (i.e., 'root.json' and 'targets.json'). This will also look through + the 'targets/' directory in 'metadata_directory' and the resulting + snapshot file will list all the delegated roles. + + + metadata_directory: + The directory containing the 'root.json' and 'targets.json' 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 of the metadata file. + Conformant to 'tuf.formats.ISO8601_DATETIME_SCHEMA'. + + root_filename: + The filename of the top-level root role. The hash and file size of this + file is listed in the snapshot role. + + targets_filename: + The filename of the top-level targets role. The hash and file size of + this file is listed in the snapshot role. + + consistent_snapshot: + Boolean. If True, a file digest is expected to be prepended to the + filename of any target file located in the targets directory. Each digest + is stripped from the target filename and listed in the snapshot metadata. + + + tuf.FormatError, if the arguments are improperly formatted. + + tuf.Error, if an error occurred trying to generate the snapshot metadata + object. + + + The 'root.json' and 'targets.json' files are read. + + + The snapshot metadata object, conformant to 'tuf.formats.SNAPSHOT_SCHEMA'. + """ + + # Do the arguments have the correct format? + # This check ensures arguments have 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.PATH_SCHEMA.check_match(metadata_directory) + tuf.formats.METADATAVERSION_SCHEMA.check_match(version) + tuf.formats.ISO8601_DATETIME_SCHEMA.check_match(expiration_date) + tuf.formats.PATH_SCHEMA.check_match(root_filename) + tuf.formats.PATH_SCHEMA.check_match(targets_filename) + tuf.formats.BOOLEAN_SCHEMA.check_match(consistent_snapshot) + + metadata_directory = _check_directory(metadata_directory) + + # Retrieve the fileinfo of 'root.json' and 'targets.json'. This file + # information includes data such as file length, hashes of the file, etc. + filedict = {} + filedict[ROOT_FILENAME] = get_metadata_fileinfo(root_filename) + filedict[TARGETS_FILENAME] = get_metadata_fileinfo(targets_filename) + + # Add compressed versions of the 'targets.json' and 'root.json' metadata, + # if they exist. + for extension in SUPPORTED_COMPRESSION_EXTENSIONS: + compressed_root_filename = root_filename+extension + compressed_targets_filename = targets_filename+extension + + # If the compressed versions of the root and targets metadata is found, + # add their file attributes to 'filedict'. + if os.path.exists(compressed_root_filename): + filedict[ROOT_FILENAME+extension] = \ + get_metadata_fileinfo(compressed_root_filename) + if os.path.exists(compressed_targets_filename): + filedict[TARGETS_FILENAME+extension] = \ + get_metadata_fileinfo(compressed_targets_filename) + + # Walk the 'targets/' directory and generate the fileinfo of all the role + # files found. This information is stored in the 'meta' field of the snapshot + # 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_directories, files in os.walk(targets_metadata): + + # 'files' here is a list of 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) + + # Strip the digest if 'consistent_snapshot' is True. + # Example: 'targets/unclaimed/13df98ab0.django.json' --> + # 'targets/unclaimed/django.json' + metadata_name, digest_junk = \ + _strip_consistent_snapshot_digest(metadata_name, consistent_snapshot) + + # All delegated roles are added to the snapshot file, including + # compressed versions. + for metadata_extension in METADATA_EXTENSIONS: + if metadata_name.endswith(metadata_extension): + rolename = metadata_name[:-len(metadata_extension)] + + # Obsolete role files may still be found. Ensure only roles loaded + # in the roledb are included in the snapshot metadata. + if tuf.roledb.role_exists(rolename): + filedict[metadata_name] = get_metadata_fileinfo(metadata_path) + + # Generate the snapshot metadata object. + snapshot_metadata = tuf.formats.SnapshotFile.make_metadata(version, + expiration_date, + filedict) + + return snapshot_metadata + + + + + +def generate_timestamp_metadata(snapshot_filename, version, + expiration_date, compressions=()): + """ + + Generate the timestamp metadata object. The 'snapshot.json' file must + exist. + + + snapshot_filename: + The required filename of the snapshot metadata file. The timestamp role + needs to the calculate the file size and hash of this file. + + version: + The timestamp's 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 of the metadata file, conformant to + 'tuf.formats.ISO8601_DATETIME_SCHEMA'. + + compressions: + Compression extensions (e.g., 'gz'). If 'snapshot.json' 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 cannot be + formatted correctly, or one of the arguments is improperly formatted. + + + None. + + + A timestamp metadata object, conformant to 'tuf.formats.TIMESTAMP_SCHEMA'. + """ + + # Do the arguments have the correct format? + # This check ensures arguments have 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.PATH_SCHEMA.check_match(snapshot_filename) + tuf.formats.METADATAVERSION_SCHEMA.check_match(version) + tuf.formats.ISO8601_DATETIME_SCHEMA.check_match(expiration_date) + tuf.formats.COMPRESSIONS_SCHEMA.check_match(compressions) + + # Retrieve the fileinfo of the snapshot metadata file. + # This file information contains hashes, file length, custom data, etc. + fileinfo = {} + fileinfo[SNAPSHOT_FILENAME] = get_metadata_fileinfo(snapshot_filename) + + # Save the fileinfo of the compressed versions of 'timestamp.json' + # in 'fileinfo'. Log the files included in 'fileinfo'. + for file_extension in compressions: + if not len(file_extension): + continue + + compressed_filename = snapshot_filename + '.' + file_extension + try: + compressed_fileinfo = get_metadata_fileinfo(compressed_filename) + + except: + logger.warning('Cannot get fileinfo about '+repr(compressed_filename)) + + else: + logger.info('Including fileinfo about '+repr(compressed_filename)) + fileinfo[SNAPSHOT_FILENAME + '.' + file_extension] = compressed_fileinfo + + # Generate the timestamp metadata object. + timestamp_metadata = tuf.formats.TimestampFile.make_metadata(version, + expiration_date, + fileinfo) + + return timestamp_metadata + + + + + +def sign_metadata(metadata_object, keyids, filename): + """ + + Sign a metadata object. If any of the keyids have already signed the file, + the old signature is replaced. The keys in 'keyids' must already be + loaded in 'tuf.keydb'. + + + metadata_object: + 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.json' or 'targets.json'. This function + does NOT save the signed metadata to this filename. + + + tuf.FormatError, if a valid 'signable' object could not be generated or + the arguments are improperly formatted. + + tuf.Error, if an invalid keytype was found in the keystore. + + + None. + + + A signable object conformant to 'tuf.formats.SIGNABLE_SCHEMA'. + """ + + # Do the arguments have the correct format? + # This check ensures arguments have 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.ANYROLE_SCHEMA.check_match(metadata_object) + 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_object) + + # Sign the metadata with each keyid in 'keyids'. + for keyid in keyids: + + # Load the signing key. + key = tuf.keydb.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'] in SUPPORTED_KEY_TYPES: + if len(key['keyval']['private']): + signed = signable['signed'] + signature = tuf.keys.create_signature(key, signed) + signable['signatures'].append(signature) + + else: + logger.warning('Private key unset. Skipping: '+repr(keyid)) + + else: + raise tuf.Error('The keydb 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, compressions, consistent_snapshot): + """ + + If necessary, write the 'metadata' signable object to 'filename', and the + compressed version of the metadata file if 'compression' is set. + Note: Compression algorithms like gzip attach a timestamp to compressed + files, so a metadata file compressed multiple times may generate different + digests even though the uncompressed content has not changed. + + + metadata: + The object that will be saved to 'filename', conformant to + 'tuf.formats.SIGNABLE_SCHEMA'. + + filename: + The filename of the metadata to be written (e.g., 'root.json'). + If a compression algorithm is specified in 'compressions', the + compression extention is appended to 'filename'. + + compressions: + Specify the algorithms, as a list of strings, used to compress the file; + The only currently available compression option is 'gz' (gzip). + + consistent_snapshot: + Boolean that determines whether the metadata file's digest should be + prepended to the filename. + + + tuf.FormatError, if the arguments are improperly formatted. + + tuf.Error, if the directory of 'filename' does not exist. + + Any other runtime (e.g., IO) exception. + + + The 'filename' (or the compressed filename) file is created, or overwritten + if it exists. + + + None. + """ + + # Do the arguments have the correct format? + # This check ensures arguments have 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.SIGNABLE_SCHEMA.check_match(metadata) + tuf.formats.PATH_SCHEMA.check_match(filename) + tuf.formats.COMPRESSIONS_SCHEMA.check_match(compressions) + tuf.formats.BOOLEAN_SCHEMA.check_match(consistent_snapshot) + + # Verify the directory of 'filename', and convert 'filename' to its absolute + # path so that temporary files are moved to their expected destinations. + filename = os.path.abspath(filename) + written_filename = filename + _check_directory(os.path.dirname(filename)) + consistent_filenames = [] + + # Generate the actual metadata file content of 'metadata'. Metadata is + # saved as json and includes formatting, such as indentation and sorted + # objects. The new digest of 'metadata' is also calculated to help determine + # if re-saving is required. + file_content, new_digests = _get_written_metadata_and_digests(metadata) + + if consistent_snapshot: + for new_digest in six.itervalues(new_digests): + dirname, basename = os.path.split(filename) + digest_and_filename = new_digest + '.' + basename + consistent_filenames.append(os.path.join(dirname, digest_and_filename)) + written_filename = consistent_filenames.pop() + + # Verify whether new metadata needs to be written (i.e., has not been + # previously written or has changed. + write_new_metadata = False + + # Has the uncompressed metadata changed? Does it exist? If so, set + # 'write_compressed_version' to True so that it is written. + # compressed metadata should only be written if it does not exist or the + # uncompressed version has changed). + try: + file_length_junk, old_digests = tuf.util.get_file_details(written_filename) + if old_digests != new_digests: + write_new_metadata = True + + # 'tuf.Error' raised if 'filename' does not exist. + except tuf.Error as e: + write_new_metadata = True + + if write_new_metadata: + # The 'metadata' object is written to 'file_object', including compressed + # versions. To avoid partial metadata from being written, 'metadata' is + # first written to a temporary location (i.e., 'file_object') and then moved + # to 'filename'. + file_object = tuf.util.TempFile() + + # Serialize 'metadata' to the file-like object and then write + # 'file_object' to disk. The dictionary keys of 'metadata' are sorted + # and indentation is used. The 'tuf.util.TempFile' file-like object is + # automically closed after the final move. + file_object.write(file_content) + logger.info('Saving ' + repr(written_filename)) + file_object.move(written_filename) + + for consistent_filename in consistent_filenames: + logger.info('Linking ' + repr(consistent_filename)) + os.link(written_filename, consistent_filename) + + + # Generate the compressed versions of 'metadata', if necessary. A compressed + # file may be written (without needing to write the uncompressed version) if + # the repository maintainer adds compression after writing the uncompressed + # version. + for compression in compressions: + file_object = None + + # Ignore the empty string that signifies non-compression. The uncompressed + # file was previously written above, if necessary. + if not len(compression): + continue + + elif compression == 'gz': + file_object = tuf.util.TempFile() + compressed_filename = filename + '.gz' + + # Instantiate a gzip object, but save compressed content to + # 'file_object' (i.e., GzipFile instance is based on its 'fileobj' + # argument). + with gzip.GzipFile(fileobj=file_object, mode='wb') as gzip_object: + gzip_object.write(file_content) + + else: + raise tuf.FormatError('Unknown compression algorithm: '+repr(compression)) + + # Save the compressed version, ensuring an unchanged file is not re-saved. + # Re-saving the same compressed version may cause its digest to unexpectedly + # change (gzip includes a timestamp) even though content has not changed. + _write_compressed_metadata(file_object, compressed_filename, + write_new_metadata, consistent_snapshot) + return written_filename + + + + + +def _write_compressed_metadata(file_object, compressed_filename, + write_new_metadata, consistent_snapshot): + """ + Write compressed versions of metadata, ensuring compressed file that have + not changed are not re-written, the digest of the compressed file is properly + added to the compressed filename, and consistent snapshots are also saved. + Ensure compressed files are written to a temporary location, and then + moved to their destinations. + """ + + # If a consistent snapshot is unneeded, 'file_object' may be simply moved + # 'compressed_filename' if not already written. + if not consistent_snapshot: + if not os.path.exists(compressed_filename) or write_new_metadata: + file_object.move(compressed_filename) + + # The temporary file must be closed if 'file_object.move()' is not used. + # tuf.util.TempFile() automatically closes the temp file when move() is + # called + else: + file_object.close_temp_file() + + # Consistent snapshots = True. Ensure the file's digest is included in the + # compressed filename written, provided it does not already exist. + else: + compressed_content = file_object.read() + new_digests = [] + consistent_filenames = [] + + # Multiple snapshots may be written if the repository uses multiple + # hash algorithms. Generate the digest of the compressed content. + for hash_algorithm in tuf.conf.REPOSITORY_HASH_ALGORITHMS: + digest_object = tuf.hash.digest(hash_algorithm) + digest_object.update(compressed_content) + new_digests.append(digest_object.hexdigest()) + + # Attach each digest to the compressed consistent snapshot filename. + for new_digest in new_digests: + dirname, basename = os.path.split(compressed_filename) + digest_and_filename = new_digest + '.' + basename + consistent_filenames.append(os.path.join(dirname, digest_and_filename)) + + # Move the 'tuf.util.TempFile' object to one of the filenames so that it is + # saved and the temporary file closed. Any remaining consistent snapshots + # may still need to be copied or linked. + compressed_filename = consistent_filenames.pop() + if not os.path.exists(compressed_filename): + logger.info('Saving ' + repr(compressed_filename)) + file_object.move(compressed_filename) + + # Save any remaining compressed consistent snapshots. + for consistent_filename in consistent_filenames: + if not os.path.exists(consistent_filename): + logger.info('Linking ' + repr(consistent_filename)) + os.link(compressed_filename, consistent_filename) + + + + + +def _log_status_of_top_level_roles(targets_directory, metadata_directory): + """ + Non-public function that logs whether any of the top-level roles contain an + invalid number of public and private keys, or an insufficient threshold of + signatures. Considering that the top-level metadata have to be verified in + the expected root -> targets -> snapshot -> timestamp order, this function + logs the error message and returns as soon as a required metadata file is + found to be invalid. It is assumed here that the delegated roles have been + written and verified. Example output: + + 'root' role contains 1 / 1 signatures. + 'targets' role contains 1 / 1 signatures. + 'snapshot' role contains 1 / 1 signatures. + 'timestamp' role contains 1 / 1 signatures. + + Note: Temporary metadata is generated so that file hashes & sizes may be + computed and verified against the attached signatures. 'metadata_directory' + should be a directory in a temporary repository directory. + """ + + # The expected full filenames of the top-level roles needed to write them to + # disk. + filenames = get_metadata_filenames(metadata_directory) + root_filename = filenames[ROOT_FILENAME] + targets_filename = filenames[TARGETS_FILENAME] + snapshot_filename = filenames[SNAPSHOT_FILENAME] + timestamp_filename = filenames[TIMESTAMP_FILENAME] + + # Verify that the top-level roles contain a valid number of public keys and + # that their corresponding private keys have been loaded. + for rolename in ['root', 'targets', 'snapshot', 'timestamp']: + try: + _check_role_keys(rolename) + + except tuf.InsufficientKeysError as e: + logger.info(str(e)) + return + + # Do the top-level roles contain a valid threshold of signatures? Top-level + # metadata is verified in Root -> Targets -> Snapshot -> Timestamp order. + # Verify the metadata of the Root role. + try: + signable, root_filename = \ + _generate_and_write_metadata('root', root_filename, False, + targets_directory, metadata_directory) + _log_status('root', signable) + + # 'tuf.UnsignedMetadataError' raised if metadata contains an invalid threshold + # of signatures. log the valid/threshold message, where valid < threshold. + except tuf.UnsignedMetadataError as e: + _log_status('root', e.signable) + return + + # Verify the metadata of the Targets role. + try: + signable, targets_filename = \ + _generate_and_write_metadata('targets', targets_filename, False, + targets_directory, metadata_directory) + _log_status('targets', signable) + + except tuf.UnsignedMetadataError as e: + _log_status('targets', e.signable) + return + + # Verify the metadata of the snapshot role. + filenames = {'root': root_filename, 'targets': targets_filename} + try: + signable, snapshot_filename = \ + _generate_and_write_metadata('snapshot', snapshot_filename, False, + targets_directory, metadata_directory, + False, filenames) + _log_status('snapshot', signable) + + except tuf.UnsignedMetadataError as e: + _log_status('snapshot', e.signable) + return + + # Verify the metadata of the Timestamp role. + filenames = {'snapshot': snapshot_filename} + try: + signable, snapshot_filename = \ + _generate_and_write_metadata('timestamp', snapshot_filename, False, + targets_directory, metadata_directory, + False, filenames) + _log_status('timestamp', signable) + + except tuf.UnsignedMetadataError as e: + _log_status('timestamp', e.signable) + return + + + + +def _log_status(rolename, signable): + """ + Non-public function logs the number of (good/threshold) signatures of + 'rolename'. + """ + + status = tuf.sig.get_signature_status(signable, rolename) + + message = repr(rolename)+' role contains '+ repr(len(status['good_sigs']))+\ + ' / '+repr(status['threshold'])+' signatures.' + logger.info(message) + + + + + +def create_tuf_client_directory(repository_directory, client_directory): + """ + + Create a client directory structure that the 'tuf.interposition' package + and 'tuf.client.updater' module expect of clients. Metadata files + downloaded from a remote TUF repository are saved to 'client_directory'. + The Root file must initially exist before an update request can be + satisfied. create_tuf_client_directory() ensures the minimum metadata + is copied and that required directories ('previous' and 'current') are + created in 'client_directory'. Software updaters integrating TUF may + use the client directory created as an initial copy of the repository's + metadadata. + + + repository_directory: + The path of the root repository directory. The 'metadata' and 'targets' + sub-directories should be available in 'repository_directory'. The + metadata files of 'repository_directory' are copied to 'client_directory'. + + client_directory: + The path of the root client directory. The 'current' and 'previous' + sub-directies are created and will store the metadata files copied + from 'repository_directory'. 'client_directory' will store metadata + and target files downloaded from a TUF repository. + + + tuf.FormatError, if the arguments are improperly formatted. + + tuf.RepositoryError, if the metadata directory in 'client_directory' + already exists. + + + Copies metadata files and directories from 'repository_directory' to + 'client_directory'. Parent directories are created if they do not exist. + + + None. + """ + + # Do the arguments have the correct format? + # This check ensures arguments have 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.PATH_SCHEMA.check_match(repository_directory) + tuf.formats.PATH_SCHEMA.check_match(client_directory) + + # Set the absolute path of the Repository's metadata directory. The metadata + # directory should be the one served by the Live repository. At a minimum, + # the repository's root file must be copied. + repository_directory = os.path.abspath(repository_directory) + metadata_directory = os.path.join(repository_directory, + METADATA_DIRECTORY_NAME) + + # Set the client's metadata directory, which will store the metadata copied + # from the repository directory set above. + client_directory = os.path.abspath(client_directory) + client_metadata_directory = os.path.join(client_directory, + METADATA_DIRECTORY_NAME) + + # If the client's metadata directory does not already exist, create it and + # any of its parent directories, otherwise raise an exception. An exception + # is raised to avoid accidently overwritting previous metadata. + try: + os.makedirs(client_metadata_directory) + + except OSError as 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 all metadata to the client's 'current' and 'previous' directories. + # The root metadata file MUST exist in '{client_metadata_directory}/current'. + # 'tuf.interposition' and 'tuf.client.updater.py' expect the 'current' and + # 'previous' directories to exist under 'metadata'. + 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) + + + +def disable_console_log_messages(): + """ + + Disable logger messages printed to the console. For example, repository + maintainers may want to call this function if many roles will be sharing + keys, otherwise detected duplicate keys will continually log a warning + message. + + + None. + + + None. + + + Removes the 'tuf.log' console handler, added by default when + 'tuf.repository_tool.py' is imported. + + + None. + """ + + tuf.log.remove_console_handler() + + + +if __name__ == '__main__': + # The interactive sessions of the documentation strings can + # be tested by running repository_tool.py as a standalone module: + # $ python repository_lib.py. + import doctest + doctest.testmod() diff --git a/tuf/repository_tool.py b/tuf/repository_tool.py index 639ecbb7..1d3ddf92 100755 --- a/tuf/repository_tool.py +++ b/tuf/repository_tool.py @@ -1,3 +1,5 @@ +#!/usr/bin/env python + """ repository_tool.py @@ -28,15 +30,12 @@ import os import errno -import sys import time import datetime -import getpass import logging import tempfile import shutil import json -import gzip import random import tuf @@ -48,9 +47,19 @@ import tuf.sig import tuf.log import tuf.conf +import tuf.repository_lib as repo_lib +from tuf.repository_lib import generate_and_write_rsa_keypair +from tuf.repository_lib import generate_and_write_ed25519_keypair +from tuf.repository_lib import import_rsa_publickey_from_file +from tuf.repository_lib import import_ed25519_publickey_from_file +from tuf.repository_lib import import_rsa_privatekey_from_file +from tuf.repository_lib import import_ed25519_privatekey_from_file +from tuf.repository_lib import create_tuf_client_directory +from tuf.repository_lib import disable_console_log_messages import tuf._vendor.iso8601 as iso8601 import tuf._vendor.six as six + # See 'log.py' to learn how logging is handled in TUF. logger = logging.getLogger('tuf.repository_tool') @@ -59,41 +68,19 @@ tuf.log.add_console_handler() tuf.log.set_console_log_level(logging.WARNING) -# 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 algorithm used by the repository to generate the digests of the # target filepaths, which are included in metadata files and may be prepended # to the filenames of consistent snapshots. HASH_FUNCTION = 'sha256' -# The extension of TUF metadata. -METADATA_EXTENSION = '.json' - -# The metadata filenames of the top-level roles. -ROOT_FILENAME = 'root' + METADATA_EXTENSION -TARGETS_FILENAME = 'targets' + METADATA_EXTENSION -SNAPSHOT_FILENAME = 'snapshot' + METADATA_EXTENSION -TIMESTAMP_FILENAME = 'timestamp' + METADATA_EXTENSION - # The targets and metadata directory names. Metadata files are written # to the staged metadata directory instead of the "live" one. METADATA_STAGED_DIRECTORY_NAME = 'metadata.staged' METADATA_DIRECTORY_NAME = 'metadata' TARGETS_DIRECTORY_NAME = 'targets' -# The full list of supported TUF metadata extensions. -METADATA_EXTENSIONS = ['.json', '.json.gz'] - -# The recognized compression extensions. -SUPPORTED_COMPRESSION_EXTENSIONS = ['.gz'] - -# Supported key types. -SUPPORTED_KEY_TYPES = ['rsa', 'ed25519'] +# The extension of TUF metadata. +METADATA_EXTENSION = '.json' # 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 @@ -111,13 +98,6 @@ # Initial 'timestamp.json' expiration time of 1 day. TIMESTAMP_EXPIRATION = 86400 -# Log warning when metadata expires in n days, or less. -# root = 1 month, snapshot = 1 day, targets = 10 days, timestamp = 1 day. -ROOT_EXPIRES_WARN_SECONDS = 2630000 -SNAPSHOT_EXPIRES_WARN_SECONDS = 86400 -TARGETS_EXPIRES_WARN_SECONDS = 864000 -TIMESTAMP_EXPIRES_WARN_SECONDS = 86400 - try: tuf.keys.check_crypto_libraries(['rsa', 'ed25519', 'general']) @@ -139,7 +119,7 @@ class Repository(object): access by default: repository.root.version = 2 - repository.timestamp.expiration = datetime.datetime(2015, 08, 08, 12, 00) + repository.timestamp.expiration = datetime.datetime(2015, 8, 8, 12, 0) repository.snapshot.add_verification_key(...) repository.targets.delegate('unclaimed', ...) @@ -266,62 +246,67 @@ def write(self, write_partial=False, consistent_snapshot=False): # sub-directory. tuf.util.ensure_parent_dir(delegated_filename) - _generate_and_write_metadata(delegated_rolename, delegated_filename, - write_partial, self._targets_directory, - self._metadata_directory, - consistent_snapshot) + repo_lib._generate_and_write_metadata(delegated_rolename, + delegated_filename, + write_partial, + self._targets_directory, + self._metadata_directory, + consistent_snapshot) # Generate the 'root.json' metadata file. # _generate_and_write_metadata() raises a 'tuf.Error' exception if the # metadata cannot be written. - root_filename = 'root' + METADATA_EXTENSION + root_filename = repo_lib.ROOT_FILENAME root_filename = os.path.join(self._metadata_directory, root_filename) signable_junk, root_filename = \ - _generate_and_write_metadata('root', root_filename, write_partial, - self._targets_directory, - self._metadata_directory, - consistent_snapshot) + repo_lib._generate_and_write_metadata('root', root_filename, write_partial, + self._targets_directory, + self._metadata_directory, + consistent_snapshot) # Generate the 'targets.json' metadata file. - targets_filename = 'targets' + METADATA_EXTENSION + targets_filename = repo_lib.TARGETS_FILENAME targets_filename = os.path.join(self._metadata_directory, targets_filename) signable_junk, targets_filename = \ - _generate_and_write_metadata('targets', targets_filename, write_partial, - self._targets_directory, - self._metadata_directory, - consistent_snapshot) + repo_lib._generate_and_write_metadata('targets', targets_filename, + write_partial, + self._targets_directory, + self._metadata_directory, + consistent_snapshot) # Generate the 'snapshot.json' metadata file. - snapshot_filename = os.path.join(self._metadata_directory, 'snapshot') - snapshot_filename = 'snapshot' + METADATA_EXTENSION + snapshot_filename = repo_lib.SNAPSHOT_FILENAME snapshot_filename = os.path.join(self._metadata_directory, snapshot_filename) filenames = {'root': root_filename, 'targets': targets_filename} snapshot_signable = None snapshot_signable, snapshot_filename = \ - _generate_and_write_metadata('snapshot', snapshot_filename, write_partial, - self._targets_directory, - self._metadata_directory, - consistent_snapshot, filenames) + repo_lib._generate_and_write_metadata('snapshot', snapshot_filename, + write_partial, + self._targets_directory, + self._metadata_directory, + consistent_snapshot, filenames) # Generate the 'timestamp.json' metadata file. - timestamp_filename = 'timestamp' + METADATA_EXTENSION + timestamp_filename = repo_lib.TIMESTAMP_FILENAME timestamp_filename = os.path.join(self._metadata_directory, timestamp_filename) filenames = {'snapshot': snapshot_filename} - _generate_and_write_metadata('timestamp', timestamp_filename, write_partial, - self._targets_directory, - self._metadata_directory, consistent_snapshot, - filenames) + repo_lib._generate_and_write_metadata('timestamp', timestamp_filename, + write_partial, + self._targets_directory, + self._metadata_directory, + consistent_snapshot, filenames) # Delete the metadata of roles no longer in 'tuf.roledb'. Obsolete roles # may have been revoked and should no longer have their metadata files # available on disk, otherwise loading a repository may unintentionally load # them. - _delete_obsolete_metadata(self._metadata_directory, - snapshot_signable['signed'], consistent_snapshot) + repo_lib._delete_obsolete_metadata(self._metadata_directory, + snapshot_signable['signed'], + consistent_snapshot) @@ -407,15 +392,16 @@ def status(self): # Append any invalid roles to the 'insufficient_keys' and # 'insufficient_signatures' lists try: - _check_role_keys(delegated_role) + repo_lib._check_role_keys(delegated_role) except tuf.InsufficientKeysError as e: insufficient_keys.append(delegated_role) continue try: - _generate_and_write_metadata(delegated_role, filename, False, - targets_directory, metadata_directory) + repo_lib._generate_and_write_metadata(delegated_role, filename, False, + targets_directory, + metadata_directory) except tuf.UnsignedMetadataError as e: insufficient_signatures.append(delegated_role) @@ -435,7 +421,8 @@ def status(self): return # Verify the top-level roles and log the results. - _log_status_of_top_level_roles(targets_directory, metadata_directory) + repo_lib._log_status_of_top_level_roles(targets_directory, + metadata_directory) finally: shutil.rmtree(temp_repository_directory, ignore_errors=True) @@ -1189,8 +1176,8 @@ def compressions(self): A getter method that returns a list of the file compression algorithms used when the metadata is written to disk. If ['gz'] is set for the - 'targets.json' role, the metadata files 'targets.json' and 'targets.json.gz' - are written. + 'targets.json' role, the metadata files 'targets.json' and + 'targets.json.gz' are written. >>> >>> @@ -2461,523 +2448,6 @@ def delegations(self): -def _generate_and_write_metadata(rolename, metadata_filename, write_partial, - targets_directory, metadata_directory, - consistent_snapshot=False, filenames=None): - """ - Non-public function that can generate and write the metadata of the specified - top-level 'rolename'. It also increments version numbers if: - - 1. write_partial==True and the metadata is the first to be written. - - 2. write_partial=False (i.e., write()), the metadata was not loaded as - partially written, and a write_partial is not needed. - """ - - metadata = None - - # Retrieve the roleinfo of 'rolename' to extract the needed metadata - # attributes, such as version number, expiration, etc. - roleinfo = tuf.roledb.get_roleinfo(rolename) - snapshot_compressions = tuf.roledb.get_roleinfo('snapshot')['compressions'] - - # Generate the appropriate role metadata for 'rolename'. - if rolename == 'root': - metadata = generate_root_metadata(roleinfo['version'], - roleinfo['expires'], consistent_snapshot) - - _log_warning_if_expires_soon(ROOT_FILENAME, roleinfo['expires'], - ROOT_EXPIRES_WARN_SECONDS) - - # Check for the Targets role, including delegated roles. - elif rolename.startswith('targets'): - metadata = generate_targets_metadata(targets_directory, - roleinfo['paths'], - roleinfo['version'], - roleinfo['expires'], - roleinfo['delegations'], - consistent_snapshot) - if rolename == 'targets': - _log_warning_if_expires_soon(TARGETS_FILENAME, roleinfo['expires'], - TARGETS_EXPIRES_WARN_SECONDS) - - elif rolename == 'snapshot': - root_filename = filenames['root'] - targets_filename = filenames['targets'] - metadata = generate_snapshot_metadata(metadata_directory, - roleinfo['version'], - roleinfo['expires'], root_filename, - targets_filename, - consistent_snapshot) - - _log_warning_if_expires_soon(SNAPSHOT_FILENAME, roleinfo['expires'], - SNAPSHOT_EXPIRES_WARN_SECONDS) - - elif rolename == 'timestamp': - snapshot_filename = filenames['snapshot'] - metadata = generate_timestamp_metadata(snapshot_filename, - roleinfo['version'], - roleinfo['expires'], - snapshot_compressions) - - _log_warning_if_expires_soon(TIMESTAMP_FILENAME, roleinfo['expires'], - TIMESTAMP_EXPIRES_WARN_SECONDS) - - signable = sign_metadata(metadata, roleinfo['signing_keyids'], - metadata_filename) - - # Check if the version number of 'rolename' may be automatically incremented, - # depending on whether if partial metadata is loaded or if the metadata is - # written with write() / write_partial(). - # Increment the version number if this is the first partial write. - if write_partial: - temp_signable = sign_metadata(metadata, [], metadata_filename) - temp_signable['signatures'].extend(roleinfo['signatures']) - status = tuf.sig.get_signature_status(temp_signable, rolename) - if len(status['good_sigs']) == 0: - metadata['version'] = metadata['version'] + 1 - signable = sign_metadata(metadata, roleinfo['signing_keyids'], - metadata_filename) - # non-partial write() - else: - if tuf.sig.verify(signable, rolename) and not roleinfo['partial_loaded']: - metadata['version'] = metadata['version'] + 1 - signable = sign_metadata(metadata, roleinfo['signing_keyids'], - metadata_filename) - - # Write the metadata to file if contains a threshold of signatures. - signable['signatures'].extend(roleinfo['signatures']) - - if tuf.sig.verify(signable, rolename) or write_partial: - _remove_invalid_and_duplicate_signatures(signable) - compressions = roleinfo['compressions'] - filename = write_metadata_file(signable, metadata_filename, compressions, - consistent_snapshot) - - # The root and timestamp files should also be written without a digest if - # 'consistent_snaptshots' is True. Client may request a timestamp and root - # file without knowing its digest and file size. - if rolename == 'root' or rolename == 'timestamp': - write_metadata_file(signable, metadata_filename, compressions, - consistent_snapshot=False) - - - # 'signable' contains an invalid threshold of signatures. - else: - message = 'Not enough signatures for ' + repr(metadata_filename) - raise tuf.UnsignedMetadataError(message, signable) - - return signable, filename - - - - - -def _log_status_of_top_level_roles(targets_directory, metadata_directory): - """ - Non-public function that logs whether any of the top-level roles contain an - invalid number of public and private keys, or an insufficient threshold of - signatures. Considering that the top-level metadata have to be verified in - the expected root -> targets -> snapshot -> timestamp order, this function - logs the error message and returns as soon as a required metadata file is - found to be invalid. It is assumed here that the delegated roles have been - written and verified. Example output: - - 'root' role contains 1 / 1 signatures. - 'targets' role contains 1 / 1 signatures. - 'snapshot' role contains 1 / 1 signatures. - 'timestamp' role contains 1 / 1 signatures. - - Note: Temporary metadata is generated so that file hashes & sizes may be - computed and verified against the attached signatures. 'metadata_directory' - should be a directory in a temporary repository directory. - """ - - # The expected full filenames of the top-level roles needed to write them to - # disk. - filenames = get_metadata_filenames(metadata_directory) - root_filename = filenames[ROOT_FILENAME] - targets_filename = filenames[TARGETS_FILENAME] - snapshot_filename = filenames[SNAPSHOT_FILENAME] - timestamp_filename = filenames[TIMESTAMP_FILENAME] - - # Verify that the top-level roles contain a valid number of public keys and - # that their corresponding private keys have been loaded. - for rolename in ['root', 'targets', 'snapshot', 'timestamp']: - try: - _check_role_keys(rolename) - - except tuf.InsufficientKeysError as e: - logger.info(str(e)) - return - - # Do the top-level roles contain a valid threshold of signatures? Top-level - # metadata is verified in Root -> Targets -> Snapshot -> Timestamp order. - # Verify the metadata of the Root role. - try: - signable, root_filename = \ - _generate_and_write_metadata('root', root_filename, False, - targets_directory, metadata_directory) - _log_status('root', signable) - - # 'tuf.UnsignedMetadataError' raised if metadata contains an invalid threshold - # of signatures. log the valid/threshold message, where valid < threshold. - except tuf.UnsignedMetadataError as e: - _log_status('root', e.signable) - return - - # Verify the metadata of the Targets role. - try: - signable, targets_filename = \ - _generate_and_write_metadata('targets', targets_filename, False, - targets_directory, metadata_directory) - _log_status('targets', signable) - - except tuf.UnsignedMetadataError as e: - _log_status('targets', e.signable) - return - - # Verify the metadata of the snapshot role. - filenames = {'root': root_filename, 'targets': targets_filename} - try: - signable, snapshot_filename = \ - _generate_and_write_metadata('snapshot', snapshot_filename, False, - targets_directory, metadata_directory, - False, filenames) - _log_status('snapshot', signable) - - except tuf.UnsignedMetadataError as e: - _log_status('snapshot', e.signable) - return - - # Verify the metadata of the Timestamp role. - filenames = {'snapshot': snapshot_filename} - try: - signable, snapshot_filename = \ - _generate_and_write_metadata('timestamp', snapshot_filename, False, - targets_directory, metadata_directory, - False, filenames) - _log_status('timestamp', signable) - - except tuf.UnsignedMetadataError as e: - _log_status('timestamp', e.signable) - return - - - - -def _log_status(rolename, signable): - """ - Non-public function logs the number of (good/threshold) signatures of - 'rolename'. - """ - - status = tuf.sig.get_signature_status(signable, rolename) - - message = repr(rolename)+' role contains '+ repr(len(status['good_sigs']))+\ - ' / '+repr(status['threshold'])+' signatures.' - logger.info(message) - - - - - -def _prompt(message, result_type=str): - """ - Non-public function that prompts the user for input by loging 'message', - converting the input to 'result_type', and returning the value to the - caller. - """ - - return result_type(six.moves.input(message)) - - - - - -def _get_password(prompt='Password: ', confirm=False): - """ - Non-public function that returns 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 _metadata_is_partially_loaded(rolename, signable, roleinfo): - """ - Non-public function that determines whether 'rolename' is loaded with - at least 1 good signature, but an insufficient threshold (which means - 'rolename' was written to disk with repository.write_partial(). If 'rolename' - is found to be partially loaded, mark it as partially loaded in its - 'tuf.roledb' roleinfo. This function exists to assist in deciding whether - a role's version number should be incremented when write() or write_parital() - is called. Return True if 'rolename' was partially loaded, False otherwise. - """ - - # The signature status lists the number of good signatures, including - # bad, untrusted, unknown, etc. - status = tuf.sig.get_signature_status(signable, rolename) - - if len(status['good_sigs']) < status['threshold'] and \ - len(status['good_sigs']) >= 1: - return True - - else: - return False - - - - - -def _check_directory(directory): - """ - - Non-public function that ensures '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 _check_role_keys(rolename): - """ - Non-public function that verifies the public and signing keys of 'rolename'. - If either contain an invalid threshold of keys, raise an exception. - 'rolename' is the full rolename (e.g., 'targets/unclaimed/django'). - """ - - # Extract the total number of public and private keys of 'rolename' from its - # roleinfo in 'tuf.roledb'. - 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']) - - # Raise an exception for an invalid threshold of public keys. - if total_keyids < threshold: - message = repr(rolename)+' role contains '+repr(total_keyids)+' / '+ \ - repr(threshold)+' public keys.' - raise tuf.InsufficientKeysError(message) - - # Raise an exception for an invalid threshold of signing keys. - 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 _remove_invalid_and_duplicate_signatures(signable): - """ - Non-public function that removes invalid signatures from 'signable'. - 'signable' may contain signatures (invalid) from previous versions - of the metadata that were loaded with load_repository(). Invalid, or - duplicate signatures are removed from 'signable'. - """ - - # Store the keyids of valid signatures. 'signature_keyids' is checked - # for duplicates rather than comparing signature objects because PSS may - # generate duplicate valid signatures of the same data, yet contain different - # signatures. - signature_keyids = [] - - for signature in signable['signatures']: - signed = signable['signed'] - keyid = signature['keyid'] - key = None - - # Remove 'signature' from 'signable' if the listed keyid does not exist - # in 'tuf.keydb'. - try: - key = tuf.keydb.get_key(keyid) - - except tuf.UnknownKeyError as e: - signable['signatures'].remove(signature) - - # Remove 'signature' from 'signable' if it is an invalid signature. - if not tuf.keys.verify_signature(key, signature, signed): - signable['signatures'].remove(signature) - - # Although valid, it may still need removal if it is a duplicate. Check - # the keyid, rather than the signature, to remove duplicate PSS signatures. - # PSS may generate multiple different signatures for the same keyid. - else: - if keyid in signature_keyids: - signable['signatures'].remove(signature) - - # 'keyid' is valid and not a duplicate, so add it to 'signature_keyids'. - else: - signature_keyids.append(keyid) - - - - - -def _delete_obsolete_metadata(metadata_directory, snapshot_metadata, - consistent_snapshot): - """ - Non-public function that deletes metadata files marked as removed by - 'repository_tool.py'. Revoked metadata files are not actually deleted until - this function is called. Obsolete metadata should *not* be retained in - "metadata.staged", otherwise they may be re-loaded by 'load_repository()'. - Note: Obsolete metadata may not always be easily detected (by inspecting - top-level metadata during loading) due to partial metadata and top-level - metadata that have not been written yet. - """ - - # Walk the repository's metadata 'targets' sub-directory, where all the - # metadata of delegated roles is stored. - targets_metadata = os.path.join(metadata_directory, 'targets') - - # The 'targets.json' metadata is not visited, only its child delegations. - # The 'targets/unclaimed/django.json' role would be located in the - # '{repository_directory}/metadata/targets/unclaimed/' directory. - 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) - # Strip the metadata dirname and the leading path separator. - # '{repository_directory}/metadata/targets/unclaimed/django.json' --> - # 'targets/unclaimed/django.json' - metadata_name = \ - metadata_path[len(metadata_directory):].lstrip(os.path.sep) - - # Strip the digest if 'consistent_snapshot' is True. - # Example: 'targets/unclaimed/13df98ab0.django.json' --> - # 'targets/unclaimed/django.json'. Consistent and non-consistent - # metadata might co-exist if write() and write(consistent_snapshot=True) - # are mixed, so ensure only 'digest.filename' metadata is stripped. - embeded_digest = None - if metadata_name not in snapshot_metadata['meta']: - metadata_name, embeded_digest = \ - _strip_consistent_snapshot_digest(metadata_name, consistent_snapshot) - - # Strip filename extensions. The role database does not include the - # metadata extension. - metadata_name_extension = metadata_name - for metadata_extension in METADATA_EXTENSIONS: - if metadata_name.endswith(metadata_extension): - metadata_name = metadata_name[:-len(metadata_extension)] - - # Delete the metadata file if it does not exist in 'tuf.roledb'. - # 'repository_tool.py' might have marked 'metadata_name' as removed, but - # its metadata file is not actually deleted yet. Do it now. - if not tuf.roledb.role_exists(metadata_name): - logger.info('Removing outdated metadata: ' + repr(metadata_path)) - os.remove(metadata_path) - - # Delete outdated consistent snapshots. snapshot metadata includes - # the file extension of roles. - if consistent_snapshot and embeded_digest is not None: - file_hashes = list(snapshot_metadata['meta'][metadata_name_extension] \ - ['hashes'].values()) - if embeded_digest not in file_hashes: - logger.info('Removing outdated metadata: ' + repr(metadata_path)) - os.remove(metadata_path) - - - - - -def _get_written_metadata_and_digests(metadata_signable): - """ - Non-public function that returns the actual content of written metadata and - its digest. - """ - - written_metadata_content = json.dumps(metadata_signable, indent=1, - sort_keys=True).encode('utf-8') - written_metadata_digests = {} - - for hash_algorithm in tuf.conf.REPOSITORY_HASH_ALGORITHMS: - digest_object = tuf.hash.digest(hash_algorithm) - digest_object.update(written_metadata_content) - written_metadata_digests.update({hash_algorithm: digest_object.hexdigest()}) - - return written_metadata_content, written_metadata_digests - - - - - -def _strip_consistent_snapshot_digest(metadata_filename, consistent_snapshot): - """ - Strip from 'metadata_filename' any digest data (in the expected - '{dirname}/digest.filename' format) that it may contain, and return it. - """ - - embeded_digest = '' - - # Strip the digest if 'consistent_snapshot' is True. - # Example: 'targets/unclaimed/13df98ab0.django.json' --> - # 'targets/unclaimed/django.json' - if consistent_snapshot: - dirname, basename = os.path.split(metadata_filename) - embeded_digest = basename[:basename.find('.')] - - # Ensure the digest, including the period, is stripped. - basename = basename[basename.find('.')+1:] - - metadata_filename = os.path.join(dirname, basename) - - - return metadata_filename, embeded_digest - - - - - - def create_new_repository(repository_directory): """ @@ -3075,6 +2545,8 @@ def create_new_repository(repository_directory): + + def load_repository(repository_directory): """ @@ -3115,7 +2587,7 @@ def load_repository(repository_directory): repository = Repository(repository_directory, metadata_directory, targets_directory) - filenames = get_metadata_filenames(metadata_directory) + filenames = repo_lib.get_metadata_filenames(metadata_directory) # The Root file is always available without a consistent snapshots digest # attached to the filename. Store the 'consistent_snapshot' value read the @@ -3125,8 +2597,8 @@ def load_repository(repository_directory): # Load the metadata of the top-level roles (i.e., Root, Timestamp, Targets, # and Snapshot). - repository, consistent_snapshot = _load_top_level_metadata(repository, - filenames) + repository, consistent_snapshot = repo_lib._load_top_level_metadata(repository, + filenames) # Load delegated targets metadata. # Walk the 'targets/' directory and generate the fileinfo of all the files @@ -3151,7 +2623,8 @@ def load_repository(repository_directory): # Example: 'targets/unclaimed/13df98ab0.django.json' --> # 'targets/unclaimed/django.json' metadata_name, digest_junk = \ - _strip_consistent_snapshot_digest(metadata_name, consistent_snapshot) + repo_lib._strip_consistent_snapshot_digest(metadata_name, + consistent_snapshot) if metadata_name.endswith(METADATA_EXTENSION): extension_length = len(METADATA_EXTENSION) @@ -3190,7 +2663,7 @@ def load_repository(repository_directory): # The roleinfo of 'metadata_name' should have been initialized with # defaults when it was loaded from its parent role. - if _metadata_is_partially_loaded(metadata_name, signable, roleinfo): + if repo_lib._metadata_is_partially_loaded(metadata_name, signable, roleinfo): roleinfo['partial_loaded'] = True tuf.roledb.update_roleinfo(metadata_name, roleinfo) @@ -3240,1601 +2713,6 @@ def load_repository(repository_directory): -def _load_top_level_metadata(repository, top_level_filenames): - """ - Load the metadata of the Root, Timestamp, Targets, and Snapshot roles. - At a minimum, the Root role must exist and successfully load. - """ - - root_filename = top_level_filenames[ROOT_FILENAME] - targets_filename = top_level_filenames[TARGETS_FILENAME] - snapshot_filename = top_level_filenames[SNAPSHOT_FILENAME] - timestamp_filename = top_level_filenames[TIMESTAMP_FILENAME] - - root_metadata = None - targets_metadata = None - snapshot_metadata = None - timestamp_metadata = None - - # Load 'root.json'. A Root role file without a digest is always written. - 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) - - # Load Root's roleinfo and update 'tuf.roledb'. - roleinfo = tuf.roledb.get_roleinfo('root') - roleinfo['signatures'] = [] - for signature in signable['signatures']: - if signature not in roleinfo['signatures']: - roleinfo['signatures'].append(signature) - - if os.path.exists(root_filename+'.gz'): - roleinfo['compressions'].append('gz') - - # By default, roleinfo['partial_loaded'] of top-level roles should be set to - # False in 'create_roledb_from_root_metadata()'. Update this field, if - # necessary, now that we have its signable object. - if _metadata_is_partially_loaded('root', signable, roleinfo): - roleinfo['partial_loaded'] = True - - _log_warning_if_expires_soon(ROOT_FILENAME, roleinfo['expires'], - ROOT_EXPIRES_WARN_SECONDS) - - tuf.roledb.update_roleinfo('root', roleinfo) - - # Ensure the 'consistent_snapshot' field is extracted. - consistent_snapshot = root_metadata['consistent_snapshot'] - - else: - message = 'Cannot load the required root file: '+repr(root_filename) - raise tuf.RepositoryError(message) - - # Load 'timestamp.json'. A Timestamp role file without a digest is always - # written. - 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) - - # Load Timestamp's roleinfo and update 'tuf.roledb'. - 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') - - if _metadata_is_partially_loaded('timestamp', signable, roleinfo): - roleinfo['partial_loaded'] = True - - _log_warning_if_expires_soon(TIMESTAMP_FILENAME, roleinfo['expires'], - TIMESTAMP_EXPIRES_WARN_SECONDS) - - tuf.roledb.update_roleinfo('timestamp', roleinfo) - - else: - pass - - # Load 'snapshot.json'. A consistent snapshot of Snapshot must be calculated - # if 'consistent_snapshot' is True. - if consistent_snapshot: - snapshot_hashes = timestamp_metadata['meta'][SNAPSHOT_FILENAME]['hashes'] - snapshot_digest = random.choice(list(snapshot_hashes.values())) - dirname, basename = os.path.split(snapshot_filename) - snapshot_filename = os.path.join(dirname, snapshot_digest + '.' + basename) - - if os.path.exists(snapshot_filename): - signable = tuf.util.load_json_file(snapshot_filename) - tuf.formats.check_signable_object_format(signable) - snapshot_metadata = signable['signed'] - for signature in signable['signatures']: - repository.snapshot.add_signature(signature) - - # Load Snapshot's roleinfo and update 'tuf.roledb'. - roleinfo = tuf.roledb.get_roleinfo('snapshot') - roleinfo['expires'] = snapshot_metadata['expires'] - roleinfo['version'] = snapshot_metadata['version'] - if os.path.exists(snapshot_filename+'.gz'): - roleinfo['compressions'].append('gz') - - if _metadata_is_partially_loaded('snapshot', signable, roleinfo): - roleinfo['partial_loaded'] = True - - _log_warning_if_expires_soon(SNAPSHOT_FILENAME, roleinfo['expires'], - SNAPSHOT_EXPIRES_WARN_SECONDS) - - tuf.roledb.update_roleinfo('snapshot', roleinfo) - - else: - pass - - # Load 'targets.json'. A consistent snapshot of Targets must be calculated if - # 'consistent_snapshot' is True. - if consistent_snapshot: - targets_hashes = snapshot_metadata['meta'][TARGETS_FILENAME]['hashes'] - targets_digest = random.choice(list(targets_hashes.values())) - dirname, basename = os.path.split(targets_filename) - targets_filename = os.path.join(dirname, targets_digest + '.' + basename) - - 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.json' in 'tuf.roledb.py' - roleinfo = tuf.roledb.get_roleinfo('targets') - roleinfo['paths'] = list(targets_metadata['targets'].keys()) - 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') - - if _metadata_is_partially_loaded('targets', signable, roleinfo): - roleinfo['partial_loaded'] = True - - _log_warning_if_expires_soon(TARGETS_FILENAME, roleinfo['expires'], - TARGETS_EXPIRES_WARN_SECONDS) - - tuf.roledb.update_roleinfo('targets', roleinfo) - - # Add the keys specified in the delegations field of the Targets role. - for key_metadata in six.itervalues(targets_metadata['delegations']['keys']): - key_object = tuf.keys.format_metadata_to_key(key_metadata) - - # Add 'key_object' to the list of recognized keys. Keys may be shared, - # so do not raise an exception if 'key_object' has already been loaded. - # In contrast to the methods that may add duplicate keys, do not log - # a warning as there may be many such duplicate key warnings. The - # repository maintainer should have also been made aware of the duplicate - # key when it was added. - try: - tuf.keydb.add_key(key_object) - - except tuf.KeyAlreadyExistsError as e: - pass - - for role in targets_metadata['delegations']['roles']: - rolename = role['name'] - roleinfo = {'name': role['name'], 'keyids': role['keyids'], - 'threshold': role['threshold'], 'compressions': [''], - 'signing_keyids': [], 'partial_loaded': False, - 'signatures': [], 'delegations': {'keys': {}, - 'roles': []}} - tuf.roledb.add_role(rolename, roleinfo) - - else: - pass - - return repository, consistent_snapshot - - - - -def _log_warning_if_expires_soon(rolename, expires_iso8601_timestamp, - seconds_remaining_to_warn): - """ - Non-public function that logs a warning if 'rolename' expires in - 'seconds_remaining_to_warn' seconds, or less. - """ - - # Metadata stores expiration datetimes in ISO8601 format. Convert to - # unix timestamp, subtract from from current time.time() (also in POSIX time) - # and compare against 'seconds_remaining_to_warn'. Log a warning message - # to console if 'rolename' expires soon. - datetime_object = iso8601.parse_date(expires_iso8601_timestamp) - expires_unix_timestamp = \ - tuf.formats.datetime_to_unix_timestamp(datetime_object) - seconds_until_expires = expires_unix_timestamp - int(time.time()) - - if seconds_until_expires <= seconds_remaining_to_warn: - days_until_expires = seconds_until_expires / 86400 - - message = repr(rolename) + ' expires ' + datetime_object.ctime() + \ - ' (UTC).\n' + repr(days_until_expires) + ' day(s) until it expires.' - - logger.warning(message) - - - - - -def generate_and_write_rsa_keypair(filepath, bits=DEFAULT_RSA_KEY_BITS, - password=None): - """ - - Generate an RSA key file, create an encrypted PEM string (using 'password' - as the pass phrase), and store it in 'filepath'. The public key portion of - the generated RSA key is stored in <'filepath'>.pub. Which cryptography - library performs the cryptographic decryption is determined by the string - set in 'tuf.conf.RSA_CRYPTO_LIBRARY'. PyCrypto currently supported. The - PEM private key is encrypted with 3DES and CBC the mode of operation. The - password is strengthened with PBKDF1-MD5. - - - filepath: - The public and private key files are saved to .pub, , - respectively. - - bits: - The number of bits of the generated RSA key. - - password: - The password used to encrypt 'filepath'. - - - tuf.FormatError, if the arguments are improperly formatted. - - - Writes key files to '' and '.pub'. - - - None. - """ - - # Do the arguments have the correct format? - # This check ensures arguments have the appropriate number of - # objects and object types, and that all dict keys are properly named. - # 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 file: ' - password = _get_password(message, confirm=True) - - # Does 'password' have the correct format? - tuf.formats.PASSWORD_SCHEMA.check_match(password) - - # Generate public and private RSA keys, encrypted the private portion - # and store them in PEM format. - 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'. If the parent directory of filepath does not exist, - # create it (and all its parent directories, if necessary). - tuf.util.ensure_parent_dir(filepath) - - # Create a tempororary file, write the contents of the public key, and move - # to final destination. - file_object = tuf.util.TempFile() - file_object.write(public.encode('utf-8')) - - # The temporary file is closed after the final move. - file_object.move(filepath+'.pub') - - # Write the private key in encrypted PEM format to ''. - # Unlike the public key file, the private key does not have a file - # extension. - file_object = tuf.util.TempFile() - file_object.write(encrypted_pem.encode('utf-8')) - file_object.move(filepath) - - - - - -def import_rsa_privatekey_from_file(filepath, password=None): - """ - - Import the encrypted PEM file in 'filepath', decrypt it, and return the key - object in 'tuf.formats.RSAKEY_SCHEMA' format. - - Which cryptography library performs the cryptographic decryption is - determined by the string set in 'tuf.conf.RSA_CRYPTO_LIBRARY'. PyCrypto - currently supported. - - The PEM private key is encrypted with 3DES and CBC the mode of operation. - The password is strengthened with PBKDF1-MD5. - - - filepath: - file, an RSA encrypted PEM file. Unlike the public RSA PEM - key file, 'filepath' does not have an extension. - - password: - The passphrase to decrypt 'filepath'. - - - tuf.FormatError, if the arguments are improperly formatted. - - tuf.CryptoError, if 'filepath' is not a valid encrypted key file. - - - The contents of 'filepath' is read, decrypted, and the key stored. - - - An RSA key object, conformant to 'tuf.formats.RSAKEY_SCHEMA'. - """ - - # Does 'filepath' have the correct format? - # Ensure the arguments have the appropriate number of objects and object - # types, and that all dict keys are properly named. - # 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. - # Password confirmation disabled here, which should ideally happen only - # when creating encrypted key files (i.e., improve usability). - if password is None: - message = 'Enter a password for the encrypted RSA file: ' - password = _get_password(message, confirm=False) - - # Does 'password' have the correct format? - tuf.formats.PASSWORD_SCHEMA.check_match(password) - - encrypted_pem = None - - # Read the contents of 'filepath' that should be an encrypted PEM. - with open(filepath, 'rb') as file_object: - encrypted_pem = file_object.read().decode('utf-8') - - # Convert 'encrypted_pem' to 'tuf.formats.RSAKEY_SCHEMA' format. Raise - # 'tuf.CryptoError' if 'encrypted_pem' is invalid. - rsa_key = tuf.keys.import_rsakey_from_encrypted_pem(encrypted_pem, password) - - return rsa_key - - - - - -def import_rsa_publickey_from_file(filepath): - """ - - Import the RSA key stored in 'filepath'. The key object returned is a TUF - key, specifically 'tuf.formats.RSAKEY_SCHEMA'. If the RSA PEM in 'filepath' - contains a private key, it is discarded. - - Which cryptography library performs the cryptographic decryption is - determined by the string set in 'tuf.conf.RSA_CRYPTO_LIBRARY'. PyCrypto - currently supported. 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. - - tuf.Error, if a valid RSA key object cannot be generated. This may be - caused by an improperly formatted PEM file. - - - 'filepath' is read and its contents extracted. - - - An RSA key object conformant to 'tuf.formats.RSAKEY_SCHEMA'. - """ - - # Does 'filepath' have the correct format? - # Ensure the arguments have the appropriate number of objects and object - # types, and that all dict keys are properly named. - # Raise 'tuf.FormatError' if there is a mismatch. - tuf.formats.PATH_SCHEMA.check_match(filepath) - - # Read the contents of the key file that should be in PEM format and contains - # the public portion of the RSA key. - with open(filepath, 'rb') as file_object: - rsa_pubkey_pem = file_object.read().decode('utf-8') - - # Convert 'rsa_pubkey_pem' to 'tuf.formats.RSAKEY_SCHEMA' format. - try: - rsakey_dict = tuf.keys.format_rsakey_from_pem(rsa_pubkey_pem) - - except tuf.FormatError as e: - raise tuf.Error('Cannot import improperly formatted PEM file.') - - return rsakey_dict - - - - - -def generate_and_write_ed25519_keypair(filepath, password=None): - """ - - Generate an ED25519 key file, create an encrypted TUF key (using 'password' - as the pass phrase), and store it in 'filepath'. The public key portion of - the generated ED25519 key is stored in <'filepath'>.pub. Which cryptography - library performs the cryptographic decryption is determined by the string - set in 'tuf.conf.ED25519_CRYPTO_LIBRARY'. - - PyCrypto currently supported. The ED25519 private key is encrypted with - AES-256 and CTR the mode of operation. The password is strengthened with - PBKDF2-HMAC-SHA256. - - - filepath: - The public and private key files are saved to .pub and - , respectively. - - password: - The password, or passphrase, to encrypt the private portion of the - generated ed25519 key. A symmetric encryption key is derived from - 'password', so it is not directly used. - - - tuf.FormatError, if the arguments are improperly formatted. - - tuf.CryptoError, if 'filepath' cannot be encrypted. - - tuf.UnsupportedLibraryError, if 'filepath' cannot be encrypted due to an - invalid configuration setting (i.e., invalid 'tuf.conf.py' setting). - - - Writes key files to '' and '.pub'. - - - None. - """ - - # Does 'filepath' have the correct format? - # Ensure the arguments have the appropriate number of objects and object - # types, and that all dict keys are properly named. - # 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 ED25519 key: ' - password = _get_password(message, confirm=True) - - # Does 'password' have the correct format? - tuf.formats.PASSWORD_SCHEMA.check_match(password) - - # Generate a new ED25519 key object and encrypt it. The cryptography library - # used is determined by the user, or by default (set in - # 'tuf.conf.ED25519_CRYPTO_LIBRARY'). Raise 'tuf.CryptoError' or - # 'tuf.UnsupportedLibraryError', if 'ed25519_key' cannot be encrypted. - ed25519_key = tuf.keys.generate_ed25519_key() - encrypted_key = tuf.keys.encrypt_key(ed25519_key, password) - - # ed25519 public key file contents in metadata format (i.e., does not include - # the keyid portion). - keytype = ed25519_key['keytype'] - keyval = ed25519_key['keyval'] - ed25519key_metadata_format = \ - tuf.keys.format_keyval_to_metadata(keytype, keyval, private=False) - - # Write the public key, conformant to 'tuf.formats.KEY_SCHEMA', to - # '.pub'. - tuf.util.ensure_parent_dir(filepath) - - # Create a tempororary file, write the contents of the public key, and move - # to final destination. - file_object = tuf.util.TempFile() - file_object.write(json.dumps(ed25519key_metadata_format).encode('utf-8')) - - # The temporary file is closed after the final move. - file_object.move(filepath+'.pub') - - # Write the encrypted key string, conformant to - # 'tuf.formats.ENCRYPTEDKEY_SCHEMA', to ''. - file_object = tuf.util.TempFile() - file_object.write(encrypted_key) - file_object.move(filepath) - - - - - -def import_ed25519_publickey_from_file(filepath): - """ - - Load the ED25519 public key object (conformant to 'tuf.formats.KEY_SCHEMA') - stored in 'filepath'. Return 'filepath' in tuf.formats.ED25519KEY_SCHEMA - format. - - If the TUF key object in 'filepath' contains a private key, it is discarded. - - - filepath: - .pub file, a TUF public key file. - - - tuf.FormatError, if 'filepath' is improperly formatted or is an unexpected - key type. - - - The contents of 'filepath' is read and saved. - - - An ED25519 key object conformant to 'tuf.formats.ED25519KEY_SCHEMA'. - """ - - # Does 'filepath' have the correct format? - # Ensure the arguments have the appropriate number of objects and object - # types, and that all dict keys are properly named. - # Raise 'tuf.FormatError' if there is a mismatch. - tuf.formats.PATH_SCHEMA.check_match(filepath) - - # ED25519 key objects are saved in json and metadata format. Return the - # loaded key object in tuf.formats.ED25519KEY_SCHEMA' format that also - # includes the keyid. - ed25519_key_metadata = tuf.util.load_json_file(filepath) - ed25519_key = tuf.keys.format_metadata_to_key(ed25519_key_metadata) - - # Raise an exception if an unexpected key type is imported. - if ed25519_key['keytype'] != 'ed25519': - message = 'Invalid key type loaded: '+repr(ed25519_key['keytype']) - raise tuf.FormatError(message) - - return ed25519_key - - - - - -def import_ed25519_privatekey_from_file(filepath, password=None): - """ - - Import the encrypted ed25519 TUF key file in 'filepath', decrypt it, and - return the key object in 'tuf.formats.ED25519KEY_SCHEMA' format. - - Which cryptography library performs the cryptographic decryption is - determined by the string set in 'tuf.conf.ED25519_CRYPTO_LIBRARY'. PyCrypto - currently supported. - - The TUF private key (may also contain the public part) is encrypted with AES - 256 and CTR the mode of operation. The password is strengthened with - PBKDF2-HMAC-SHA256. - - - filepath: - file, an RSA encrypted TUF key file. - - password: - The password, or passphrase, to import the private key (i.e., the - encrypted key file 'filepath' must be decrypted before the ed25519 key - object can be returned. - - - tuf.FormatError, if the arguments are improperly formatted or the imported - key object contains an invalid key type (i.e., not 'ed25519'). - - tuf.CryptoError, if 'filepath' cannot be decrypted. - - tuf.UnsupportedLibraryError, if 'filepath' cannot be decrypted due to an - invalid configuration setting (i.e., invalid 'tuf.conf.py' setting). - - - 'password' is used to decrypt the 'filepath' key file. - - - An ed25519 key object of the form: 'tuf.formats.ED25519KEY_SCHEMA'. - """ - - # Does 'filepath' have the correct format? - # Ensure the arguments have the appropriate number of objects and object - # types, and that all dict keys are properly named. - # 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. - # Password confirmation disabled here, which should ideally happen only - # when creating encrypted key files (i.e., improve usability). - if password is None: - message = 'Enter a password for the encrypted ED25519 key: ' - password = _get_password(message, confirm=False) - - # Does 'password' have the correct format? - tuf.formats.PASSWORD_SCHEMA.check_match(password) - - # Store the encrypted contents of 'filepath' prior to calling the decryption - # routine. - encrypted_key = None - - with open(filepath, 'rb') as file_object: - encrypted_key = file_object.read() - - # Decrypt the loaded key file, calling the appropriate cryptography library - # (i.e., set by the user) and generating the derived encryption key from - # 'password'. Raise 'tuf.CryptoError' or 'tuf.UnsupportedLibraryError' if the - # decryption fails. - key_object = tuf.keys.decrypt_key(encrypted_key, password) - - # Raise an exception if an unexpected key type is imported. - if key_object['keytype'] != 'ed25519': - message = 'Invalid key type loaded: '+repr(key_object['keytype']) - raise tuf.FormatError(message) - - return key_object - - - - - -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.json': 'metadata/root.json', - 'targets.json': 'metadata/targets.json', - 'snapshot.json': 'metadata/snapshot.json', - 'timestamp.json': 'metadata/timestamp.json'} - - If '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 'snapshot.json'. - """ - - if metadata_directory is None: - metadata_directory = os.getcwd() - - # Does 'metadata_directory' have the correct format? - # Ensure the arguments have the appropriate number of objects and object - # types, and that all dict keys are properly named. - # Raise 'tuf.FormatError' if there is a mismatch. - tuf.formats.PATH_SCHEMA.check_match(metadata_directory) - - # Store the filepaths of the top-level roles, including the - # 'metadata_directory' for each one. - filenames = {} - - filenames[ROOT_FILENAME] = \ - os.path.join(metadata_directory, ROOT_FILENAME) - - filenames[TARGETS_FILENAME] = \ - os.path.join(metadata_directory, TARGETS_FILENAME) - - filenames[SNAPSHOT_FILENAME] = \ - os.path.join(metadata_directory, SNAPSHOT_FILENAME) - - filenames[TIMESTAMP_FILENAME] = \ - os.path.join(metadata_directory, TIMESTAMP_FILENAME) - - return filenames - - - - - -def get_metadata_fileinfo(filename): - """ - - Retrieve the file information of 'filename'. The object returned - conforms to 'tuf.formats.FILEINFO_SCHEMA'. The information - generated for 'filename' is stored in metadata files like 'targets.json'. - The fileinfo object returned has the form: - - fileinfo = {'length': 1024, - 'hashes': {'sha256': 1233dfba312, ...}, - 'custom': {...}} - - - filename: - The metadata file whose file information is needed. It must exist. - - - 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. SHA256 hashes are generated by default. - """ - - # Does 'filename' have the correct format? - # Ensure the arguments have the appropriate number of objects and object - # types, and that all dict keys are properly named. - # 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, tuf.conf.REPOSITORY_HASH_ALGORITHMS) - custom = None - - return tuf.formats.make_fileinfo(filesize, filehashes, custom) - - - - - - -def get_target_hash(target_filepath): - """ - - Compute the hash of 'target_filepath'. This is useful in conjunction with - the "path_hash_prefixes" attribute in a delegated targets role, which - tells us which paths it is implicitly responsible for. - - The repository may optionally organize targets into hashed bins to ease - target delegations and role metadata management. The use of consistent - hashing allows for a uniform distribution of targets into bins. - - - target_filepath: - The path to the target file on the repository. This will be relative to - the 'targets' (or equivalent) directory on a given mirror. - - - None. - - - None. - - - The hash of 'target_filepath'. - """ - - return tuf.util.get_target_hash(target_filepath) - - - - - -def generate_root_metadata(version, expiration_date, consistent_snapshot): - """ - - Create the root metadata. 'tuf.roledb.py' and 'tuf.keydb.py' are read and - the information returned by these modules is used to generate the root - metadata object. - - - 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 of the metadata file. Conformant to - 'tuf.formats.ISO8601_DATETIME_SCHEMA'. - - consistent_snapshot: - Boolean. If True, a file digest is expected to be prepended to the - filename of any target file located in the targets directory. Each digest - is stripped from the target filename and listed in the snapshot metadata. - - - 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 (e.g., a required top-level role not found in 'tuf.roledb'.) - - - The contents of 'tuf.keydb.py' and 'tuf.roledb.py' are read. - - - A root metadata object, conformant to 'tuf.formats.ROOT_SCHEMA'. - """ - - # Do the arguments have the correct format? - # Ensure the arguments have the appropriate number of objects and object - # types, and that all dict keys are properly named. - # Raise 'tuf.FormatError' if any of the arguments are improperly formatted. - tuf.formats.METADATAVERSION_SCHEMA.check_match(version) - tuf.formats.ISO8601_DATETIME_SCHEMA.check_match(expiration_date) - tuf.formats.BOOLEAN_SCHEMA.check_match(consistent_snapshot) - - # The role and key dictionaries to be saved in the root metadata object. - # Conformant to 'ROLEDICT_SCHEMA' and 'KEYDICT_SCHEMA', respectively. - roledict = {} - keydict = {} - - # Extract the role, threshold, and keyid information of the top-level roles, - # which Root stores in its metadata. The necessary role metadata is generated - # from this information. - for rolename in ['root', 'targets', 'snapshot', 'timestamp']: - - # 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".') - - # Keep track of the keys loaded to avoid duplicates. - keyids = [] - - # Generate keys for the keyids listed by the role being processed. - 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: - # {'keytype': 'rsa', - # 'keyid': keyid, - # 'keyval': {'public': '-----BEGIN RSA PUBLIC KEY----- ...', - # 'private': '-----BEGIN RSA PRIVATE KEY----- ...'}} - keyid = key['keyid'] - if keyid not in keydict: - - # This appears to be a new keyid. Generate the key for it. - if key['keytype'] in ['rsa', 'ed25519']: - keytype = key['keytype'] - keyval = key['keyval'] - keydict[keyid] = \ - tuf.keys.format_keyval_to_metadata(keytype, keyval, private=False) - - # This is not a recognized key. Raise an exception. - else: - raise tuf.Error('Unsupported keytype: '+keyid) - - # 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_threshold = tuf.roledb.get_role_threshold(rolename) - role_metadata = tuf.formats.make_role_metadata(keyids, role_threshold) - roledict[rolename] = role_metadata - - # Generate the root metadata object. - root_metadata = tuf.formats.RootFile.make_metadata(version, expiration_date, - keydict, roledict, - consistent_snapshot) - - return root_metadata - - - - - -def generate_targets_metadata(targets_directory, target_files, version, - expiration_date, delegations=None, - write_consistent_targets=False): - """ - - Generate the targets metadata object. The targets in 'target_files' must - exist at the same path they should on the repo. 'target_files' is a list of - targets. The 'custom' field of the targets metadata is not currently - supported. - - - targets_directory: - The directory containing the target files and directories of the - repository. - - target_files: - The target files tracked by 'targets.json'. 'target_files' is a list of - target paths that are relative to the targets directory (e.g., - ['file1.txt', 'Django/module.py']). - - 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 of the metadata file. Conformant to - 'tuf.formats.ISO8601_DATETIME_SCHEMA'. - - delegations: - The delegations made by the targets role to be generated. 'delegations' - must match 'tuf.formats.DELEGATIONS_SCHEMA'. - - write_consistent_targets: - Boolean that indicates whether file digests should be prepended to the - target files. - - - tuf.FormatError, if an error occurred trying to generate the targets - metadata object. - - tuf.Error, if any of the target files cannot be read. - - - The target files are read and file information generated about them. - - - A targets metadata object, conformant to 'tuf.formats.TARGETS_SCHEMA'. - """ - - # Do the arguments have the correct format? - # Ensure the arguments have the appropriate number of objects and object - # types, and that all dict keys are properly named. - # 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.METADATAVERSION_SCHEMA.check_match(version) - tuf.formats.ISO8601_DATETIME_SCHEMA.check_match(expiration_date) - tuf.formats.BOOLEAN_SCHEMA.check_match(write_consistent_targets) - - if delegations is not None: - tuf.formats.DELEGATIONS_SCHEMA.check_match(delegations) - - # Store the file attributes of targets in 'target_files'. 'filedict', - # conformant to 'tuf.formats.FILEDICT_SCHEMA', is added to the targets - # metadata object returned. - filedict = {} - - # Ensure the user is aware of a non-existent 'target_directory', and convert - # it to its abosolute path, if it exists. - targets_directory = _check_directory(targets_directory) - - # Generate the fileinfo of all the target files listed in 'target_files'. - for target in target_files: - - # The root-most folder of the targets directory should not be included in - # target paths listed in targets metadata. - # (e.g., 'targets/more_targets/somefile.txt' -> 'more_targets/somefile.txt') - relative_targetpath = target - - # Note: join() discards 'targets_directory' if 'target' contains a leading - # path separator (i.e., is treated as an absolute path). - target_path = os.path.join(targets_directory, target.lstrip(os.sep)) - - # Ensure all target files listed in 'target_files' exist. If just one of - # these files does not exist, raise an exception. - if not os.path.exists(target_path): - message = repr(target_path)+' cannot be read. Unable to generate '+ \ - 'targets metadata.' - raise tuf.Error(message) - - filedict[relative_targetpath] = get_metadata_fileinfo(target_path) - - if write_consistent_targets: - for target_digest in filedict[relative_targetpath]['hashes']: - dirname, basename = os.path.split(target_path) - digest_filename = target_digest + '.' + basename - digest_target = os.path.join(dirname, digest_filename) - - if not os.path.exists(digest_target): - logger.warning('Hard linking target file to ' + repr(digest_target)) - os.link(target_path, digest_target) - - # Generate the targets metadata object. - targets_metadata = tuf.formats.TargetsFile.make_metadata(version, - expiration_date, - filedict, - delegations) - - return targets_metadata - - - - - -def generate_snapshot_metadata(metadata_directory, version, expiration_date, - root_filename, targets_filename, - consistent_snapshot=False): - """ - - Create the snapshot metadata. The minimum metadata must exist - (i.e., 'root.json' and 'targets.json'). This will also look through - the 'targets/' directory in 'metadata_directory' and the resulting - snapshot file will list all the delegated roles. - - - metadata_directory: - The directory containing the 'root.json' and 'targets.json' 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 of the metadata file. - Conformant to 'tuf.formats.ISO8601_DATETIME_SCHEMA'. - - root_filename: - The filename of the top-level root role. The hash and file size of this - file is listed in the snapshot role. - - targets_filename: - The filename of the top-level targets role. The hash and file size of - this file is listed in the snapshot role. - - consistent_snapshot: - Boolean. If True, a file digest is expected to be prepended to the - filename of any target file located in the targets directory. Each digest - is stripped from the target filename and listed in the snapshot metadata. - - - tuf.FormatError, if the arguments are improperly formatted. - - tuf.Error, if an error occurred trying to generate the snapshot metadata - object. - - - The 'root.json' and 'targets.json' files are read. - - - The snapshot metadata object, conformant to 'tuf.formats.SNAPSHOT_SCHEMA'. - """ - - # Do the arguments have the correct format? - # This check ensures arguments have 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.PATH_SCHEMA.check_match(metadata_directory) - tuf.formats.METADATAVERSION_SCHEMA.check_match(version) - tuf.formats.ISO8601_DATETIME_SCHEMA.check_match(expiration_date) - tuf.formats.PATH_SCHEMA.check_match(root_filename) - tuf.formats.PATH_SCHEMA.check_match(targets_filename) - tuf.formats.BOOLEAN_SCHEMA.check_match(consistent_snapshot) - - metadata_directory = _check_directory(metadata_directory) - - # Retrieve the fileinfo of 'root.json' and 'targets.json'. This file - # information includes data such as file length, hashes of the file, etc. - filedict = {} - filedict[ROOT_FILENAME] = get_metadata_fileinfo(root_filename) - filedict[TARGETS_FILENAME] = get_metadata_fileinfo(targets_filename) - - # Add compressed versions of the 'targets.json' and 'root.json' metadata, - # if they exist. - for extension in SUPPORTED_COMPRESSION_EXTENSIONS: - compressed_root_filename = root_filename+extension - compressed_targets_filename = targets_filename+extension - - # If the compressed versions of the root and targets metadata is found, - # add their file attributes to 'filedict'. - if os.path.exists(compressed_root_filename): - filedict[ROOT_FILENAME+extension] = \ - get_metadata_fileinfo(compressed_root_filename) - if os.path.exists(compressed_targets_filename): - filedict[TARGETS_FILENAME+extension] = \ - get_metadata_fileinfo(compressed_targets_filename) - - # Walk the 'targets/' directory and generate the fileinfo of all the role - # files found. This information is stored in the 'meta' field of the snapshot - # 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_directories, files in os.walk(targets_metadata): - - # 'files' here is a list of 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) - - # Strip the digest if 'consistent_snapshot' is True. - # Example: 'targets/unclaimed/13df98ab0.django.json' --> - # 'targets/unclaimed/django.json' - metadata_name, digest_junk = \ - _strip_consistent_snapshot_digest(metadata_name, consistent_snapshot) - - # All delegated roles are added to the snapshot file, including - # compressed versions. - for metadata_extension in METADATA_EXTENSIONS: - if metadata_name.endswith(metadata_extension): - rolename = metadata_name[:-len(metadata_extension)] - - # Obsolete role files may still be found. Ensure only roles loaded - # in the roledb are included in the snapshot metadata. - if tuf.roledb.role_exists(rolename): - filedict[metadata_name] = get_metadata_fileinfo(metadata_path) - - # Generate the snapshot metadata object. - snapshot_metadata = tuf.formats.SnapshotFile.make_metadata(version, - expiration_date, - filedict) - - return snapshot_metadata - - - - - -def generate_timestamp_metadata(snapshot_filename, version, - expiration_date, compressions=()): - """ - - Generate the timestamp metadata object. The 'snapshot.json' file must - exist. - - - snapshot_filename: - The required filename of the snapshot metadata file. The timestamp role - needs to the calculate the file size and hash of this file. - - version: - The timestamp's 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 of the metadata file, conformant to - 'tuf.formats.ISO8601_DATETIME_SCHEMA'. - - compressions: - Compression extensions (e.g., 'gz'). If 'snapshot.json' 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 cannot be - formatted correctly, or one of the arguments is improperly formatted. - - - None. - - - A timestamp metadata object, conformant to 'tuf.formats.TIMESTAMP_SCHEMA'. - """ - - # Do the arguments have the correct format? - # This check ensures arguments have 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.PATH_SCHEMA.check_match(snapshot_filename) - tuf.formats.METADATAVERSION_SCHEMA.check_match(version) - tuf.formats.ISO8601_DATETIME_SCHEMA.check_match(expiration_date) - tuf.formats.COMPRESSIONS_SCHEMA.check_match(compressions) - - # Retrieve the fileinfo of the snapshot metadata file. - # This file information contains hashes, file length, custom data, etc. - fileinfo = {} - fileinfo[SNAPSHOT_FILENAME] = get_metadata_fileinfo(snapshot_filename) - - # Save the fileinfo of the compressed versions of 'timestamp.json' - # in 'fileinfo'. Log the files included in 'fileinfo'. - for file_extension in compressions: - if not len(file_extension): - continue - - compressed_filename = snapshot_filename + '.' + file_extension - try: - compressed_fileinfo = get_metadata_fileinfo(compressed_filename) - - except: - logger.warning('Cannot get fileinfo about '+repr(compressed_filename)) - - else: - logger.info('Including fileinfo about '+repr(compressed_filename)) - fileinfo[SNAPSHOT_FILENAME + '.' + file_extension] = compressed_fileinfo - - # Generate the timestamp metadata object. - timestamp_metadata = tuf.formats.TimestampFile.make_metadata(version, - expiration_date, - fileinfo) - - return timestamp_metadata - - - - - -def sign_metadata(metadata_object, keyids, filename): - """ - - Sign a metadata object. If any of the keyids have already signed the file, - the old signature is replaced. The keys in 'keyids' must already be - loaded in 'tuf.keydb'. - - - metadata_object: - 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.json' or 'targets.json'. This function - does NOT save the signed metadata to this filename. - - - tuf.FormatError, if a valid 'signable' object could not be generated or - the arguments are improperly formatted. - - tuf.Error, if an invalid keytype was found in the keystore. - - - None. - - - A signable object conformant to 'tuf.formats.SIGNABLE_SCHEMA'. - """ - - # Do the arguments have the correct format? - # This check ensures arguments have 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.ANYROLE_SCHEMA.check_match(metadata_object) - 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_object) - - # Sign the metadata with each keyid in 'keyids'. - for keyid in keyids: - - # Load the signing key. - key = tuf.keydb.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'] in SUPPORTED_KEY_TYPES: - if len(key['keyval']['private']): - signed = signable['signed'] - signature = tuf.keys.create_signature(key, signed) - signable['signatures'].append(signature) - - else: - logger.warning('Private key unset. Skipping: '+repr(keyid)) - - else: - raise tuf.Error('The keydb 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, compressions, consistent_snapshot): - """ - - If necessary, write the 'metadata' signable object to 'filename', and the - compressed version of the metadata file if 'compression' is set. - Note: Compression algorithms like gzip attach a timestamp to compressed - files, so a metadata file compressed multiple times may generate different - digests even though the uncompressed content has not changed. - - - metadata: - The object that will be saved to 'filename', conformant to - 'tuf.formats.SIGNABLE_SCHEMA'. - - filename: - The filename of the metadata to be written (e.g., 'root.json'). - If a compression algorithm is specified in 'compressions', the - compression extention is appended to 'filename'. - - compressions: - Specify the algorithms, as a list of strings, used to compress the file; - The only currently available compression option is 'gz' (gzip). - - consistent_snapshot: - Boolean that determines whether the metadata file's digest should be - prepended to the filename. - - - tuf.FormatError, if the arguments are improperly formatted. - - tuf.Error, if the directory of 'filename' does not exist. - - Any other runtime (e.g., IO) exception. - - - The 'filename' (or the compressed filename) file is created, or overwritten - if it exists. - - - None. - """ - - # Do the arguments have the correct format? - # This check ensures arguments have 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.SIGNABLE_SCHEMA.check_match(metadata) - tuf.formats.PATH_SCHEMA.check_match(filename) - tuf.formats.COMPRESSIONS_SCHEMA.check_match(compressions) - tuf.formats.BOOLEAN_SCHEMA.check_match(consistent_snapshot) - - # Verify the directory of 'filename', and convert 'filename' to its absolute - # path so that temporary files are moved to their expected destinations. - filename = os.path.abspath(filename) - written_filename = filename - _check_directory(os.path.dirname(filename)) - consistent_filenames = [] - - # Generate the actual metadata file content of 'metadata'. Metadata is - # saved as json and includes formatting, such as indentation and sorted - # objects. The new digest of 'metadata' is also calculated to help determine - # if re-saving is required. - file_content, new_digests = _get_written_metadata_and_digests(metadata) - - if consistent_snapshot: - for new_digest in six.itervalues(new_digests): - dirname, basename = os.path.split(filename) - digest_and_filename = new_digest + '.' + basename - consistent_filenames.append(os.path.join(dirname, digest_and_filename)) - written_filename = consistent_filenames.pop() - - # Verify whether new metadata needs to be written (i.e., has not been - # previously written or has changed. - write_new_metadata = False - - # Has the uncompressed metadata changed? Does it exist? If so, set - # 'write_compressed_version' to True so that it is written. - # compressed metadata should only be written if it does not exist or the - # uncompressed version has changed). - try: - file_length_junk, old_digests = tuf.util.get_file_details(written_filename) - if old_digests != new_digests: - write_new_metadata = True - - # 'tuf.Error' raised if 'filename' does not exist. - except tuf.Error as e: - write_new_metadata = True - - if write_new_metadata: - # The 'metadata' object is written to 'file_object', including compressed - # versions. To avoid partial metadata from being written, 'metadata' is - # first written to a temporary location (i.e., 'file_object') and then moved - # to 'filename'. - file_object = tuf.util.TempFile() - - # Serialize 'metadata' to the file-like object and then write - # 'file_object' to disk. The dictionary keys of 'metadata' are sorted - # and indentation is used. The 'tuf.util.TempFile' file-like object is - # automically closed after the final move. - file_object.write(file_content) - logger.info('Saving ' + repr(written_filename)) - file_object.move(written_filename) - - for consistent_filename in consistent_filenames: - logger.info('Linking ' + repr(consistent_filename)) - os.link(written_filename, consistent_filename) - - - # Generate the compressed versions of 'metadata', if necessary. A compressed - # file may be written (without needing to write the uncompressed version) if - # the repository maintainer adds compression after writing the uncompressed - # version. - for compression in compressions: - file_object = None - - # Ignore the empty string that signifies non-compression. The uncompressed - # file was previously written above, if necessary. - if not len(compression): - continue - - elif compression == 'gz': - file_object = tuf.util.TempFile() - compressed_filename = filename + '.gz' - - # Instantiate a gzip object, but save compressed content to - # 'file_object' (i.e., GzipFile instance is based on its 'fileobj' - # argument). - with gzip.GzipFile(fileobj=file_object, mode='wb') as gzip_object: - gzip_object.write(file_content) - - else: - raise tuf.FormatError('Unknown compression algorithm: '+repr(compression)) - - # Save the compressed version, ensuring an unchanged file is not re-saved. - # Re-saving the same compressed version may cause its digest to unexpectedly - # change (gzip includes a timestamp) even though content has not changed. - _write_compressed_metadata(file_object, compressed_filename, - write_new_metadata, consistent_snapshot) - return written_filename - - - - - -def _write_compressed_metadata(file_object, compressed_filename, - write_new_metadata, consistent_snapshot): - """ - Write compressed versions of metadata, ensuring compressed file that have - not changed are not re-written, the digest of the compressed file is properly - added to the compressed filename, and consistent snapshots are also saved. - Ensure compressed files are written to a temporary location, and then - moved to their destinations. - """ - - # If a consistent snapshot is unneeded, 'file_object' may be simply moved - # 'compressed_filename' if not already written. - if not consistent_snapshot: - if not os.path.exists(compressed_filename) or write_new_metadata: - file_object.move(compressed_filename) - - # The temporary file must be closed if 'file_object.move()' is not used. - # tuf.util.TempFile() automatically closes the temp file when move() is - # called - else: - file_object.close_temp_file() - - # Consistent snapshots = True. Ensure the file's digest is included in the - # compressed filename written, provided it does not already exist. - else: - compressed_content = file_object.read() - new_digests = [] - consistent_filenames = [] - - # Multiple snapshots may be written if the repository uses multiple - # hash algorithms. Generate the digest of the compressed content. - for hash_algorithm in tuf.conf.REPOSITORY_HASH_ALGORITHMS: - digest_object = tuf.hash.digest(hash_algorithm) - digest_object.update(compressed_content) - new_digests.append(digest_object.hexdigest()) - - # Attach each digest to the compressed consistent snapshot filename. - for new_digest in new_digests: - dirname, basename = os.path.split(compressed_filename) - digest_and_filename = new_digest + '.' + basename - consistent_filenames.append(os.path.join(dirname, digest_and_filename)) - - # Move the 'tuf.util.TempFile' object to one of the filenames so that it is - # saved and the temporary file closed. Any remaining consistent snapshots - # may still need to be copied or linked. - compressed_filename = consistent_filenames.pop() - if not os.path.exists(compressed_filename): - logger.info('Saving ' + repr(compressed_filename)) - file_object.move(compressed_filename) - - # Save any remaining compressed consistent snapshots. - for consistent_filename in consistent_filenames: - if not os.path.exists(consistent_filename): - logger.info('Linking ' + repr(consistent_filename)) - os.link(compressed_filename, consistent_filename) - - - - - -def create_tuf_client_directory(repository_directory, client_directory): - """ - - Create a client directory structure that the 'tuf.interposition' package - and 'tuf.client.updater' module expect of clients. Metadata files - downloaded from a remote TUF repository are saved to 'client_directory'. - The Root file must initially exist before an update request can be - satisfied. create_tuf_client_directory() ensures the minimum metadata - is copied and that required directories ('previous' and 'current') are - created in 'client_directory'. Software updaters integrating TUF may - use the client directory created as an initial copy of the repository's - metadadata. - - - repository_directory: - The path of the root repository directory. The 'metadata' and 'targets' - sub-directories should be available in 'repository_directory'. The - metadata files of 'repository_directory' are copied to 'client_directory'. - - client_directory: - The path of the root client directory. The 'current' and 'previous' - sub-directies are created and will store the metadata files copied - from 'repository_directory'. 'client_directory' will store metadata - and target files downloaded from a TUF repository. - - - tuf.FormatError, if the arguments are improperly formatted. - - tuf.RepositoryError, if the metadata directory in 'client_directory' - already exists. - - - Copies metadata files and directories from 'repository_directory' to - 'client_directory'. Parent directories are created if they do not exist. - - - None. - """ - - # Do the arguments have the correct format? - # This check ensures arguments have 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.PATH_SCHEMA.check_match(repository_directory) - tuf.formats.PATH_SCHEMA.check_match(client_directory) - - # Set the absolute path of the Repository's metadata directory. The metadata - # directory should be the one served by the Live repository. At a minimum, - # the repository's root file must be copied. - repository_directory = os.path.abspath(repository_directory) - metadata_directory = os.path.join(repository_directory, - METADATA_DIRECTORY_NAME) - - # Set the client's metadata directory, which will store the metadata copied - # from the repository directory set above. - client_directory = os.path.abspath(client_directory) - client_metadata_directory = os.path.join(client_directory, - METADATA_DIRECTORY_NAME) - - # If the client's metadata directory does not already exist, create it and - # any of its parent directories, otherwise raise an exception. An exception - # is raised to avoid accidently overwritting previous metadata. - try: - os.makedirs(client_metadata_directory) - - except OSError as 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 all metadata to the client's 'current' and 'previous' directories. - # The root metadata file MUST exist in '{client_metadata_directory}/current'. - # 'tuf.interposition' and 'tuf.client.updater.py' expect the 'current' and - # 'previous' directories to exist under 'metadata'. - 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) - - - -def disable_console_log_messages(): - """ - - Disable logger messages printed to the console. For example, repository - maintainers may want to call this function if many roles will be sharing - keys, otherwise detected duplicate keys will continually log a warning - message. - - - None. - - - None. - - - Removes the 'tuf.log' console handler, added by default when - 'tuf.repository_tool.py' is imported. - - - None. - """ - - tuf.log.remove_console_handler() - - if __name__ == '__main__': # The interactive sessions of the documentation strings can # be tested by running repository_tool.py as a standalone module: diff --git a/tuf/schema.py b/tuf/schema.py index 6ae23528..1026129e 100755 --- a/tuf/schema.py +++ b/tuf/schema.py @@ -1,3 +1,5 @@ +#!/usr/bin/env python + """ schema.py diff --git a/tuf/util.py b/tuf/util.py index 082ab062..d1031d44 100755 --- a/tuf/util.py +++ b/tuf/util.py @@ -64,8 +64,8 @@ def _default_temporary_directory(self, prefix): try: self.temporary_file = tempfile.NamedTemporaryFile(prefix=prefix) - except OSError as err: - logger.critical('Temp file in '+temp_dir+'failed: '+repr(err)) + except OSError as err: # pragma: no cover + logger.critical('Cannot create a system temporary directory: '+repr(err)) raise tuf.Error(err) @@ -95,14 +95,17 @@ def __init__(self, prefix='tuf_temp_'): self.temporary_file = tempfile.NamedTemporaryFile(prefix=prefix, dir=temp_dir) except OSError as err: - logger.error('Temp file in '+temp_dir+' failed: '+repr(err)) + logger.error('Temp file in ' + temp_dir + ' failed: '+repr(err)) logger.error('Will attempt to use system default temp dir.') self._default_temporary_directory(prefix) + else: self._default_temporary_directory(prefix) + + def get_compressed_length(self): """ @@ -144,6 +147,8 @@ def flush(self): + + def read(self, size=None): """ @@ -165,14 +170,19 @@ def read(self, size=None): self.temporary_file.seek(0) data = self.temporary_file.read() self.temporary_file.seek(0) + return data + else: if not (isinstance(size, int) and size > 0): raise tuf.FormatError + return self.temporary_file.read(size) + + def write(self, data, auto_flush=True): """ @@ -226,6 +236,8 @@ def move(self, destination_path): + + def seek(self, *args): """ @@ -249,6 +261,8 @@ def seek(self, *args): + + def decompress_temp_file_object(self, compression): """ @@ -782,19 +796,10 @@ def get_target_hash(target_filepath): # Calculate the hash of the filepath to determine which bin to find the # target. The client currently assumes the repository uses - # 'HASH_FUNCTION' to generate hashes. + # 'HASH_FUNCTION' to generate hashes and 'utf-8'. digest_object = tuf.hash.digest(HASH_FUNCTION) - - try: - digest_object.update(target_filepath.encode('utf-8')) - - except UnicodeEncodeError: - # Sometimes, there are Unicode characters in target paths. We assume a - # UTF-8 encoding and try to hash that. - digest_object = tuf.hash.digest(HASH_FUNCTION) - encoded_target_filepath = target_filepath.encode('utf-8') - digest_object.update(encoded_target_filepath) - + encoded_target_filepath = target_filepath.encode('utf-8') + digest_object.update(encoded_target_filepath) target_filepath_hash = digest_object.hexdigest() return target_filepath_hash @@ -911,7 +916,7 @@ def load_json_file(filepath): # The file is mostly likely gzipped. if filepath.endswith('.gz'): logger.debug('gzip.open('+str(filepath)+')') - fileobject = gzip.open(filepath) + fileobject = six.StringIO(gzip.open(filepath).read().decode('utf-8')) else: logger.debug('open('+str(filepath)+')') @@ -925,6 +930,7 @@ def load_json_file(filepath): raise tuf.Error(message) else: + fileobject.close() return deserialized_object finally: From 66f4b88ef2146e7e4637e1300327430e3acd245f Mon Sep 17 00:00:00 2001 From: Vladimir Diaz Date: Tue, 3 Jun 2014 14:59:56 -0400 Subject: [PATCH 14/32] Update repository tool diagram and coverage. Fix text box alignment in diagram. Omit coverage of repository tool prompt and getpass. Minor coverage update. --- docs/images/repository_tool-diagram.png | Bin 91913 -> 91920 bytes tests/.coveragerc | 2 ++ tuf/repository_lib.py | 4 ++-- 3 files changed, 4 insertions(+), 2 deletions(-) diff --git a/docs/images/repository_tool-diagram.png b/docs/images/repository_tool-diagram.png index e8e6f2898b4788f535e3151c8d7ba18466e937e4..6bfbdeb0b7d997549985dda97826a2d866d856dc 100644 GIT binary patch delta 58711 zcmZ_0WmJ^i`v&^VFu+jKGBiU7SajFW2ug?;NQWqhbjLFYQj&s-l7bSVgbLE3Fn}N- zAl)4@^Z>&!XYhUB-~XHsXR#I^m<7*%_P*n~uKU`=bdx7_lb0g)gf>21qT{zL+khN^f#=lj?e zN=3zF;vH=qXM=#A%$Lh=Wh!L}H&FZA3h#LtpV32R z-96$`+F3hUv5!~n!GYCvWB!jv*o^22F?Lt2a|X6)m!Gnr#<83+gm!Gi5W`x z?a2YM7}=$w2~C>SZgoAd#g_CyH|ZSkzG)sfrRB7E}-J%VZdSui4DtRkTkYkfU2NVaFTH!MTh zN>@b4ZA2kuZqz7T?B4OF1;`9(+G*1J({nk}DLT1mDe7^ZB3+7#7BztFDm2=TlVb!P z7Ejh!~$Ing1{e$S|=3%IYp zfAaa}bVY1)HlE|1K~Bby%ep4OAn`cLmBphx0?7HPavl0T&^bnXG%NT>D>Kd$=c0zg zuW4!EHr2}nna=fO_&w5kTke*{3m#frfqAq z6u}56&^N{x%^ghqjQ>fNWTV}2<+i`)l_fZO_0lVit8Qav z=YVG}jXjd+2Slz<*nu6+i6K=O>b<=Y`x@yXC46pPi7J8tE9Zf&ZKHa%3d?}Syb!Of zflF}6&HBK+pz&Sh2>k;`Mj#+6A;AO~UiY>?w-V6S7xLf_Z zQUI|*}{up${QXHV4j(GNt2VAen?3PL-T-D&BH$eZVHh< z5`~~X(r?gt3e2(LG6UhYA07%s{LN%olbw(lwN+1P;T$-dX+yYWpd99TZwE$+7)}t%LbwW`JGUVPdr3<3RKwlbKt;W6#X;x&G9b`XW%F2x>?>2(j*8BRn$%9L^LP zOq3YjBI}^$9m@w$=XZrXm|bV|0SzdM6mtrYw2NEfL-q+O+f2azFRG2P)E(o zTOh~S-o1dByS$+R%oDT@yH&Qc7I})1ym^8>34WJCUN;~ntm|GX&%OQJ0=H>I(2z2W zgG1&0lcbDQlfJH^8is~9@ZSeCP0md?G$k;=lG5Y9rOwx z`f}dgU)R3OiI-L(^?aTDpx0AM&eDhQ@Kswwgzz-cM0@`oS}TX!LKp`7!D(B(LPP3q zZB-wVA-m*lzV9B18lROwftqM{l`M5L9tE)e9MSK)WY1+CSr8wvn8mwPJTx7Ens(3X z8yzxGjvQQG;zE3}Xbh~x*Y_n9K_8DB)VJ-L;mURzwk9$7wov8HnyCT8R_MKs=3|OL z#&YGIqA7JD#Q8U7tfAf6_an|k)4*v|(C24v#RFB;HW$wnDWBH^<@Je-8hQXJ>Gc3O zvYQTpLmZmQh>F|riPR)#JCfk1=O=5*# zK+G|IxXEV41g6y$cRrrxxjPlwEyY2rL(mEIF}Fv1XSzOZqMjMf`}-U&h4ySMMvKRq zx9))FJ_N5+Bo;M|$%X``&AR1z7Y$s!uOWz?Qo7U)k`2v7|I06 znGyT%;V?AmtLX`j`LH7bDJDuC(`*E3(SjI@q0Pw`Pl_W9NG~J^uxOjGPyxt@)4#y* z-RLLhF+K%|HKPwmw-LwLd&yYp~_L0_Xwj>OikrPiVJS zw(YVeEvklF(>wex6fh}ahoI#7^$qGkCgAi-j?(`~qW9?f6I}ga^Bz<5e&3!DaNtU8 zH##2ZcxnauVvo4}4=-5xZ71le25w`C$72$II%cQLtV(>%ODNy-LlZwA-wXMACw7M{`{-BA1g`F=!RJI} zO69N(4B2i9r_Y{X#!J$Ba_?gqZ+&^PRyUt@t3)Dx-?^8$0^>XSBUC1q4y)D~ov@L- zGrnIpmfbh?U%0Hlo41{w>j;{CYVuE4`pvE=6H5w6H;`k$nqpg;0`#CYx z00Ug@f+mZqHEh?TbPtpC=_8w#U-ddE_bWVkUFp7Vu_c+Xr0j*g8=~X2^d8@5?bYM3 z+Tcr2*|eN|Vv@b7k`)@)aKer2*=g%F?P~~y0gSxsymM?f1S%7R5#|2n$R`?znR;-; zo}mR8lkXXu+L@`Cc^XD@<_h3$Rhjbso6iP_@5vbVSfUvk;|ZAc`l`iav%8bfKdDtA z9=ag4w2*ahRq$$JWS&3o*fQA${V#NH$bJttG!~fl>yho2VAD1kyZB!toYd#}@nc2i zcPxul)L>!HexW2L%?2DurT~ORSg^ zG;QaVD=x{v0=*|h{08Ma>t_OT-CQ_>o;z(+WYg2Tq>ciP;y--;;5p0{`GG32qjyt7 zsoT{wBm`V=i4fpN{=$z;ySkDb%OAVt%y6z&nk8*6@3-_K%A+|YP z0&W-I<3GEmz>Wi@BDwj=(Nhv*M+dPcyw`J>fLB2p_XKO6Ox-5bZ(Cxbx}G35*V`1< zRJMW^{47-Pc`WGT2EMiYMgF|W|8}<-4$$Tx6hst7?u&3p!1Li~{}bb56SVPt_*X5) z_tZu#nk*9C@r+>}Oo>J;?TBI^k76OlMXOH>e3Q-(%!a5JwRnA(F@7I-w-WNhV)WWw z<8JXU@4rgLat`HuTz|QiE8X@qJ!md;YPTEr5&z6fOQHL4nKxlU^;f~lh5bK~Dm@po z9FMXH;+Wi~1UNAILuNC2p~2XD#O( zChy|_7NLGqx`Qrkj?Tp9Whbtcwk2G4wYb-KVYVbDWja;|+{lZ1)s|w>5iXQ-jq4Ux z0CUMTM)*0g*6#xolSHAH+eEnL)Zm&!7#lrNj41|*iPirlad}$}>#26uuBf36nWiJE9=8>B8QQ)V@bd7_ z_L;V^DJry~yW>M1*7_%@#Zy9~(f9&7R%F1=7!`>N#k_c3t9z7kDb5VsvwANPWit4^ zyVy?AJe2O8qCtoCFvcFL;?jPAsK@HS=!2C$x<@Okp#%<0xk0ihQ z4pl7(nwivg`jgp9+yQ)Ge}PvgVc~0U#~F`f7|_`Nwnn^?;mvpd*Y`yx6BWpmrxsMQ zq{nW}7?X~sj$^F}(j+X+&7~eC6 zD!@>SQ6_Y8syc?rP929mK^aL0CMNomt(uGgvKOHg~TTRxfr6rZmvr@gOCp|6s;QF=FbV`3!#4SZ9n3>J>plE|J_=-ptmCXviT*= zH`J)Zs`dBqBow?}ozj{hLDg{X+d0=ru4&5@VgkPoVNs>D(Iiyvn4+dU^Zu(00j!g- z_$#r%_*hr!%6$)MtG!35PTj?#j;8i!aq{{VKaP{8v^G4ne%w23spbGgtCYUZ(uAZq zC2D|pg7%&7#%lpHmaiuK^2II|-ZqQf{BxI# zFJL!sN6P~<-G!HVCv7baK?Oa)r9!?1v3Q;j8T4A3qly@q=0-0sV>GFEx6W*ZZsR!z zOP6~t^9(%_6#OGV(Pa?}ADYX|cfI5Mso)&AY5BsdIIGu+-d_4<)zy(ben3N3Qf(eb ztMF>dgAgBCX1Ctq-?@dvZ1W7+y^)xo5wC-hEOD&>ypB#5#ouv1N1~U6B{>M0fgZex=G(SS= z3^4N18nW7eR`V*xfo><^`tVCT4cl0`FU-J1XR^=>=gh}u7ocggo)f)BZ<<}bQtTaY zk0V6hBVrghDiGFki5c96KA z;MYdZnJ>>}nco;^b7s59gW|4Nsav#}0GZUUIFL3h>GGEEzWAbk*;Up*2F`TYd!u7h zQj;J3#dkIqc@(!&jF%AN0b((jl^)4HiZAgeJ*!Na!PBG4;qNBfiXP|89=HI4kDvPS z4Q>ah*^U1$MDSA_{Ync_7>O&DXdw$Vt<47p#E-Oex5j>OrFguq1f?+BLB+*4yR&WztU6M?&X|uTk)LX zoQFAIynC(*1AMf@l&#b~(s*0=z`JEY|IC>24|z(EIltk&_O#uHUuWXzRQq{IAaW34 zQQ>Z+EPQA266i{$9%iR-KBJNfNpE~{g*mm}zeM9j@4f|Fo>CEm$+72zD@iE4xNMNn z$+6|bUW4eUz|J8TL-9;rW`_xWnfo5r%83{Iz8b*&?rXr+77*D>#!(Ww~`J$OYL zzr^EXJ>2_9#qEjzw*Sd`VqXTXE=UP^&0lO-Y)^a{>}=q&>o%Ku^VYm&%<%3fW#p3H zs&4&DWqVp7+E-eb7Wp;8kG-AHq{j?m4<-{48v$&?Vyz0r65rhTFh5vg+LdZLu~&tT zotXi4w`ZzM&!YT943hTYOp~_zju~B{nQ^n0Gb)a1C)@0w{L<}ShL7tY8HrlzW+=#Zz?2Zi3n!MB6G@}@-6Q*CT z*!|R_@A=jJHDou}t+B_DC%6aM6&?0v;JZs-iZA#OYN`vIX%{AS^zSvYb@zOZ7QPfR ziqFBe5$x{&#{cvwewpCwVLeFJMy$Q*z7(>I)8VWtK$L$;u1sLDOVB)QO}m#p--*=# za;9;9Um9vvqQC*)k}F^okiH6bo6#)W@(F)veCD!ekr!&iJ6U`*siEl3bM&?p>>+_i=MO>>uYWw+ zeiMbFzPxW{!gMnA(v0;#GAU%l#kvW%dz%2~fJ10+sr>l*%$tESPqn!!+q_|uL9YfQ zqQ%bd;A1}2yd(&5hoity3E7Ly0S$gJ{NcA3zEbk}X;SbW1u_JxkUyTXi0z>MKhBwO zR`l}FEmTupy!zc@@274&yBri6P#a!BM<*)ZkL+dM zK9IQWnt@A1se6Bz@5qv0uhV;Xg$0-db0Y(FhEC08%|~2Ruuu~Yph&6!H+)@0wIO; z=%JL8+_ON|q3g>>mAEGN%_y;*%w`Dawcant8n`lgrk|WPelO>aCAH=_9O{N-zQAg zHo6tOKPeL0o}*XsT$qBarW@Dh{Z<&MZasfpNzW>(6paG{se~driJ&egV7lXtkpKRZ%+nDE04EE+uu)^8D()a8ip`ZSE z((BY9DF__ImG>OpHryU+*j-X$1hS7;zy5CXqNBCZ11g1N0Q*;tI)`UA)hZ}f74Gi@ zsqN2qP^#{|^Qpa)kg6QiAn+CBnN!52I_g)S!4K^iH=--uGhdGH{P59->I5}7johg0 zzq#Kj`TmOS!5iKNaDq~-9w)Q5wp$na0tngn%BtO1GO+}@$FSGn|C9%k;6ggFr)b+B zClaCRGIpP*ma>lUDYcD6${~u?5VuhGdS1+W=9Jfv6dZ_#RDvEnaptsBV_AN94w8N< zFE-Xuy`;#}Sv%LwR`PYwJzzAZFOgJbw{cry!S=4`rN2z}_;KxqRW_MhX!lkL{mD@v zXGjx4okN+JpfrRp0A^VroB4hXZuNAYkG(uE9VM^zmKq2MR!zEK!KaNfe>2L{lnOmY zEXIN-piv^D2Hc~=fB$p#4#>p}*?iJ?&{km*vt_N>dB`g!vqVZ5j6^msNeprmz0DRb z2Wt{;IQ}VR|1~Qm8d}MFo_xBzCp4s<&c1Hr=_x7X`28@C4k>j&lN^T3x{nT~>@%hU za@mFxy?zHhjz$llqtgr^)5H4t06!Dx3~KOiDhv{JbBwC9C`$h}x9V!fQx!7StHlro zU@k`PGFu0)vQODQyrp?ofFm~t8wI&bg+??HGLW2Sv;muC=3Hyd%>w0>U<|x~?s;9) zPNK;~qHrLIo+8zB2wDS>sdwB0Q1lw=yr3sC#Cq&|{Ri2B>wF97kQCNeOdvboQpUKa z#&0UuHhf|;m*&}0d7`Os4lieq5A7|DfkWlN!DV-7;M4wWO7<5j;Hx@`zYV_a(4zdv1t?T&clewTuxAWp@E>!=PveQ9>yzWI= zE|J`*sMq$Jy`U+;U?hbJq#Zeh{7-mT_2&SaxHkofQVi?Gb;nsrn`zH_WhM`|9<;t2X!vs?RJMYcTK1P zMo6Cf;rwwFpr?0S_)I04Jc=Kv}Uy(as*;j z_2j`LX;_oRb7Ijh^*_?$zxpCt`vS0A=cUo#fh&!?$OvJ{S;OpWHtx1uT8c&*B-b`z zR0O|Wh5?a&!ocivtWLl;+yg8Z1gO%@B|lQou!{iNRrbLk=p9v$_Y{C2%>_#LgwTsa zi%KU>X3zxtim;;4Y4i2|?6Cj$OTn5h!VRo51K*e?o#_doopx>yo$n~kWdAMUtH5)r zNn}vkr~lz%b_=>r?TZBLrjy(f{TeFEITj=;N37^hM4@~*BN%K+=ruCyP#>@X-R(bk z8Ey4?WxwV85X;s`;`rN$z4f+obPw|OCu(4brURlbss9;XMP#7(PZ}c~qDVL6Q!4bu zCv7K)uSG8slX>PUBi}MhBn3j$X%{)P(QK-b{s5%Rg&Osusk8b%Ws4T|No!O7pBwAK z4%xn#&uZF$gMz9vuh9qn<9W+I*u<+& zOZhX&Fd@B+n5i^S_~N7))9sWO}?zm@x^C?vy@u3F5o5N_onLxRd_WjSr9q>Alfk@lW#vy_+UCyXTwr>qh0 zSk7F-24`20a|QjUpOe`^Y^YJBh-RhsF)d*M*Eu*T0{^8MJ&vlMpY}@STm-leiK#VpB)raf?yr z@qYHn&knYeS>B;Y(V4{zVjFLuL&#*{nf*L&vFfGgYio6jy5Rb}X7=F^sgf|H5_Qm0 zt*+Y1;b&KpjO?Y-L=>3wWA?N>N(q`JS`+Hk6lmTQ^xVHN)wgjAte=VRShxba|GXY;RJ^hT|6##AGWY%zZL@csE!T;U4aI0h!UKb zDzi`Yq+58?e?=2s&Q5PB3`NIq3%S8qB$W{&^b;4nt`_2=bqhnIa#`P9J3aJALclPu zfYh82Xn$}r>vnu1#ZNKJL+?Y91gBQDF@^DA8Tvb-%2DOn1ab32i?;jwL4QDk+a2C) zJs{3X6&Jkce|SjrG9nO0MKH$QAr4AoR_;R+E|RuWENH~f=kP0nWE7!$m0rYk1H9%v zcQA?hVQDf=S2Oa8U73ATzuLo6FX^Gju)-mZcK}1OG2R0<8pkv3He9Y-9f~2wFxXwN zkfw5f1Vg1hzSdOx`KhP(bI_Al{Tsi@r_Gie2J@BMr0f+x2OtF2Jl(s<5SfV55HQZT z+o*z6eg~3vl0Ho|eQbehUEs}EnLTM{Elwt^zu(_fMN&#<3$Hp?MyEI5kU!z;^k76*9@8spwj zlH^7oQfiCk_K(||zXi?VoRGi-sgukGaXm?7UOQ)W9@OC_ZagrRc|`Rl5t+3N)0hrQ zPkCYdeFt|53Pzj!R1t4P-4G0JWCnI`sa*DA4mc)cbXog!>w}CYeOOUZ_5uDhe!%mX ztD3jI?VqTi0$6R3m69(?g&^YQJa8i&A-GOJ5@Oh&Pv-&DU)K2k>U4R;msKH)ynzv4KIKc=fp^->#Cc;iBv?xG!P$ zpHzm{UAz6h{G57mI>UPoEN7h{_6{FK@IXkij`&hJZ$STCtJAS#*OR)EH){%V70~=q z6`zl;aG)g9MTh`-99I28)(4ssS^`7;f*y^V`1o#!8P!N_kMI}$7h&xxfQ+{Y7|B&WQuaq-+!eCrqT#w9E9;O!x@ScE#k22Jf^jtNI z2ExGYKYU}(AH#}R&?Jo~{;?t@!8~H;R%T|<1j{$ITV#BMiLsHTD)X1>q;^2+1tjJp z00rh`vmT(gtm8)`m~w;P<%<YN2A82QxaI`SMU742#u=LL{b{b2|bIUbi0AS_lt; z`Z7+VY|ntiGgiTMh9fswzq?YQMa=kSK0Hq<{PD1{5#7*PX+FpiAumyS9sYdNUI1VU zrALnDHHd2+UREID82~L^iO7Wac&9%{R(H|H2Onh7U+Z;z)GG}Gej`heu6{_$BSd!4 zvW~)A>790x2I>X#GLq{`8RE@UA>0O+#$Q7r9$AG|Bsrb~>Sf}4hw9sDUlv8jolG7*7E8`k%qk!q)+Tm70wyp1}ip#evG#F7KFq#`madoG715GP4yesmt`-| zpu}zF+w@BN+$-Gfn+m%?23BvOdq-j3mK(xN?{}P z=tU*XoWfMIN&XM6AnMq>6!=|7U^1|0{ zDvOg>+ug@}P<}A!vl@(iGEPJ`{U(HzZMk)=Ppx~H$=^r5SUfSVtdaY0J})M#a0xMZ z;gn!o`#|}ucCy~L-p&vrqgMw=8BBo-i1CfD+gmq+eFIZ(agwP-`Ws1%`6!nho^+`v z)oI0Vy%LG|LMc#F;wi2R2>w8NIYfsui1~EqPRP7}be=VV;B3wr(wadV)|WTn{CdK< zZIF?KTK+jQ#&dZqZjW&<9a2u`h&PCi!6` z<(`8*=83Or)mT1=xOXgFJpfBTTq4c)8=YZ*J<-Bnp5ojN#herv)UK#?dG4O|VnWYD ztd^M0`7xzCag^Ll&)00?5~x4@RzAG;<~XS~gGM~NE7Ofx{)KFRLlXR9IIwv9u%|8s zZwU5&75G(?m5#r{INGJ2H%NVO87J*Sw0H?tCs)2pL=GV-bo%HS$d&>8ZX$`Xk-$q5 zQ1uOl+QSSmgv1SV+~dt-duRKFmL(ecEMzMp?wdsM7MB=CM6RK-ppw}!Y0b>&it@IJ z1!=TUqpLRatoVAGa{^GvNbZkuo`7l?6RuH_#Ej=8fTqBg>v{twu7Ho)ZPzRT#l`kS z;#thNSZ!HIIE#d_n1G=q0Jvi(Qk^wo_{cGCeYIIVd75jG=0+a*dtoX!&2Nle8_vOV zdx#35p_UX#M^f*RMPuYxuv^0%UJk)m>_@$zRj z``XBF%#n$|U`&$<&zLkxGsF2ixm`9a!c&d;G@F(aY+{sJfHw5AO6R4)*TA5ci2ym= z%R&Qir$_82QS-IHOvuNjTA;|6Ta!bZgljNVyf<7Yi<`74khH3W+-}!&ak)h-4XuYW z#lWCr8kklbsMv}`ec{BVRHwR#;}Ty`oc-VSqyfv-xQMN`Icpk-@4(jEe3}8M*5C!J z`ZI%_lVMT0H~XUm3tfvH$XSnsyibdQSC=9M7ic9O@3WsLI45rLzcGPT6&#b@PZl#O#e5`Lo{i+7Ym6uHc@~3_1Mo zo!>zpCYfosSa`Z54G$9<(DQn(kOokAH79E0bNtREZjsXIc@+b7h^zD8!y@5%C@Pr* z_o34oaP$s_iT==s(m|t+SW&hMIXbkh?s&;BW2z~)NErF+z~CFb zM{vj~Yxx^Re_2Zv$tviuE}@m{DEh6Y?oXvwo5;jN({gk0-+lgd87Ik=z z?L4w3_8h)%{`stQoOj4cVQli|XuHQLp{ahvi$C`<-gjHA%|qGB*SqO21(4kn;=u=U z$N3%YWZjzj+HmhhmZRGKlKfGu?2vskN#1b|d1##=_vbCWeD_oyNj80`4J4_xOjlm$ z@3gu7)XtW{s(|-S8aOtZef2i&=&kCBtKO%SGb#lWz!H&iDkmD{h?~Gf-&&loohpb6 zUSvl!I#P#oh!Vt%Sk>A_ij&$>wbRj9G=MUSV>D1z%2uHG{*~kRGWSR#(Sz1j>vOz| z8QDpKl66&sEJ#9iv!vq06T05N=2CxZm?@<^$aV$I+&ahUw@}u=Xs6%$Ei5ZPdad`S zcUDouynkH4kx!JAa;HePmu2Cq7d%XE080qI1lYS}WexeI`qy^Y?ZNJj%;eoXK#tp;`BFgH;dF^E|moOfQ zlG9>dj1=pYIuD0h<+Je9JT6}#MEhm0dJ6$BwIv%)yp6+>)rC6{@y*X*cLxDwHPA3` zfjD9y>wBtugb2~PyQb0)B6eiJ((E9JX$Zr&yk``o;3xT?{h)j^W^yZ0!X#IiJ2xC) z$zA<9yid?D8HqyOre=9ZvX{KLn_BJfcLR3H{5uyTje-x;H8oYURwJhUpa8w>93k4< zk*67+@P=L#b(?U`5;-(1T>oj!u8R)wE#jx!dzYkYA{#h|a~lx@aibmZc1U}C*Q>nc z-mLUQ57l>x5(OTrCDt9W8!JxPio|Oxffs3(-F}^}?zR@d&|s~Zmut+dFJnH%LC-aI zm`=1+u=PD`T+%Uff7IP+Lz&*e*XfXYCH(l}ZBv99%Sjdu#rGdF@{u90yyfXXthdv2 zoCLQK%6S#8%d~+X&kR^3u6Y`!t?Nh5*IZ3V-|kM^O}o7&6Qp`;ySs`aq^V(?eyly2 zGvL7;ywC0giw@FhFZilPa8FBz1ICL}0(-TqBQG!WOP=I}0TsILg0j-2K{xqw@zMH0 zf=k?sn{y`x4Jp8I2z2B*FwH%CuyYQWwf%A1WXgjjFF6+!J>A5)z-tCvWD~c~?W!mm zQPwcye~TC|`joIvoC{ox28Nvsvpq2kc8Mi1kHUGPAGHNb@B=l5-w&CnBs~2cyS0Ri z6mM!@hiiO^g9DZCu4zH1_jI$bhM=`Zg!Jv+)2ES*2(QrmJ>B60=-~Lpx&tbuA5F^d zc6Zpo){HQQ{M6>BL}m(E8F$G-$!VdUn6Q`43GR~-@e78`A*A8+L6WU_d$RXz_fWRw zk>Z}<>Y>Z+seli-piC+N&XvkHIdk-IrAOrK@y20ekne%(Hs=yyjfW%j^WmSI`Gv#s zOk%CZ%e6h1GO(oYc^Mk#t##dVivr zi9EXBq5*kt>2pVC1NCWL7Uf4->OV09Do4+E%CR?&v=;NY5;vp=h5P6W_$B-9_I>Y- z*GTH!*E&P;G=HMkifX)a%8zVHSp%saSz|vyvCB5pDHtsZZwUcEZcXMXV9>aA({2yWdP#w$1{qQY*CkiP7Hj0q(}sE*N$%)sr-nGIu!aT*)q4bO0Ab8Q^lh1V}FO+=D>FoiSR zg@?X*7MiIsCf+T{r0WED;`W2%@&1FaMsuaHeR%qUDc)27)d%J_8ZR#EvLHhRs799R zf8fJDeJ3(V?TQJ~HBwF9#lUE{cTMzUyI<<}9CtsXC`$#}-B@}k@xORjcqfT9zqf8g z-%Cswxwj&MjHoJtp?2d;q%A#vu_xkhC-7jFN-n&svQl337~dTb?~W0etD{28U$1#{ zO=-??N(-3gyF>CHt~xfvG`#M`n^=2>jIwf{_p-Uh{^TuUVIS9^>y#d=SNKP9aUqSQ z4}1QkLR(!F=TS{tFnPotY2#TgiB345xyTbuQAjjOrnWUTRblN_Jtr1-=zO2VFT{@v zw%P?0#6G0PK@;aDItPLmBW|uvJm8#YWn?Yz%RNoqrIU z=h6a>^tO3=%PiV1qL*(X_9PBcKprwKPq_q&#jLkHJg)(?c)sPbYnQMGM%+&;Q8TJt zMGQ$2&zQKxojk0Lgkd_FQlF*ykBoO^7nR@#HHtTP&n-GOl$xBCH~V#q;Ud@Fyh~NO z-7Wo`2Dng(9RE_tZ8Ct3w)t`0sPGCs$K8bQ1GsZD>EKWW?Ird}UvkQr`sQ zHiTfP`jdP+7q8;W4%^fQ8M)C>=H%!emAZy0GG?^k3~K_V?0dzUi8$$o6Y0q>ZEisxK15{~`17K@@LM#oKyK5f)pi4KEC9kgzSmxp z?!LjgEM!|?%7ZwSxDR?X3t*A=v-8BI?OPHso{NyU{!oic-JM%hdrmw?tHNF;8MLCy zfp93_j&UZXU8;SV7vaW6V%|BcLj_A(%Xj(TX3}CVUthYqAxBO;AN{sCfphou>!lt) z20Hmxowl2lH``x{az~#mj)NyW+$4}W4oA~!ktRn>8WnH-uf#=L@s$LYvVUQ2$Qp%& z7-!e=M8Y4QXV&>`X{iB3?D164?bk}Kkp!F+k$JME#5XGX7f4}cqmg0$Nh0MO1#i~u zV9s{nwFi0}t+FtZZ8+jpX$7Q8wFli`@0os=4EI<72wfQHx89lpvjYiphr~phb~JJ2 zLGb*8U|awLU>R^H^K8h6nJ80|P0sS>r|7nDr!c6u2$YYIrE$$9u#m|L5vXG2wF`b~PVe-tH%jdeJ<-8vv?yum)PvaFI zk_`!@9xBq2qVNS9<>GuwUhazz#chr zm*bLJyM^-mZC=7JA>ee+a$qR#x9`h+NvrDYwAtNPPYio$&*V&+#zZ}q8Y$yl>S$7P zoGa}L*ki);R_~!3aelLI5vq)qG1sZfT(2)jn({bna zrBI(LW3>Q>2?;TDhJ(K$w%ye~)+F=|N` zodHdY?1}rSS|>-MVaI+(b3|u{Ezm-b1+RPtwy2l8?0l(E)N(Tyy&?T8#e;$IfduP< zFlkaqg}%}vQu3xs!rWxpyt7HN3B2F?$ z$(r)p+1!NW-fENXfRY}G%xJ}JGaw5ueKOX6KP<)6bJ_+55ERXC(*G)0I_oCJu%xOim1(U8UbVRm#CG#0{D7jQO>P0PDM;*ejmWn(dB_MUE#-0iRu7 z$1!MFpqWlCFpJuIt=JLW1IPWQ1fe#Zx%0|&9=nNFSfGBr&YAHNWas>m^P=XWN`^$9 zStiLv!w)Hg)4rv>4Emo)rU@p}yvxFFUbNG4=qP*LIPlb8=xcGLQ2hQ{B+by92?W=~ z+THKb55(qyv~*b2IsCx8ABt7N2qvAO#lPsOuy)?kize^Zl?I!~Khw~tna3JKZSFjG z(nHrhjVUa`D9qn@k@Ysaf9c`U;bxs)^_3H~sf7^3OYllz^GK)8Lw~=mMa>Nq|GNVd zk~{)-$2`_1e$JEz5*&#Z0y4V!p|y8f?O&QnlA@awlfkOde%%VI9LS=#IsSO7@eSJH%TS@w%57}?UYplmYPZ$%)^9BWEAy_er z*?=d9mQGNhm3;P$8%XO}x0!iW{Z&(O7-O~~jj}+3K?J-N$qc>n1*NB45|(VF51|N+ zPn$89*c_p>L)*K&r*mL}Y1duMhzhx2^2GCdK>iExaPFX*g#?e~uL~VtE5HzcNlRo$ zoz7HGO;qI&d)h?d03yOwMOiVK%)p&p!8wX#58~fi&1aa^uKPZe3{)BNy$&mDo1nuY!L^x^FpTDmGnLPhE|4_}_Gw378epAa<;b`iwNLK{dL>KJ_M$ZUs?ltGc)XzhKu>2g)@`IHHU= zj$)a;eCBNIFOPAH_yT)aAbiECOKI8Zt>e8tK@xO56_*nbOhB7@f=QuxD6I#ALhAS8w_d+^ZaRz#vp!RZ8kH@IC)2z3Wwqfy8T54I3_raFb|Kg6 z@*U@Z4u!zjJN!L`rt`I8|2#qv_$%L>UM_1Vxmb<{@z#032Ni+;@2AK2;lMWGO0gZg z8TH0>2h#AEVZ#tKvD>$`e&N61MHimU52zjvx_!LmVSDG!x0O4iem8{CHM;XdpLv_w z7c;&ro!$nzbh9uzB?FPQb9er)vnv~qQSBC zoykwKgqZ^qm2;zwHIPmd8j_=lWb&c{sL)^d4$Ls7 zw|gm&2V)oKP`UN8dbC+D6!)erDL z>9$twmkrq~_a^y(%`CqG?=V9k$*IT5a}V$F@ShoYN-J3rulUg!&onq}^fd>?x*X}! zI!K!xa~?54Q$&rFVwwBg>(hJT7QVT%&mXPD% zsyywwmOz~MV5s4uQ2UH>|H*C6li+hINY@O~Q4x2V@Dg9EoJSrw{TaqFqUccS*x+xB&E}Y<@O|l2Vnr7f2n&l@a zMfTrQY15fvEOw1x;m~0&?iJ z000zI$CZ4q-{OX8F3KhR9G8QVLzOx%|L+LiF#`Vhjt3v8d4AC8$c=3^3@Hbv=Du$! zx4!i$kg>@GA^L-V9#l7?(mQc1dHUW)T(tUM;tm zFtNWwE&gO%rxW;XLA=o!m2vOPx9Lc}k_ee(3X8-;e$jh-8C_19!EDo}E00{^I|`)n7nG^?m>U_#H}8T88dc z8U*PO5T#W_NdYMl1d-+rih@da4k{oWq96^@B_l|Kgv8K856$lm`u==h|NmMpmJ9B> z_uO;O*=O&4p8Ih;@ICNm_XSE=GZ`cj0`fS2zUz7D4HZ)DUv5)shb^S!eHH2bUXKWf zsklqlTBbjyno9d3<2ipm$M{Ijr&?aT@HM%12h;CneqW8$oCM(Me9&_WZs93#2Hy+L zXb7HKgfxzV>mdxhZBlRb6U;w0DO)EDI=d6&0s7?Qpr*FKBVs@s(wsxP8~gKAfC&tz z?P>nwFL(`fhgopx0r`OoIFWVAmP>QEf1A)6Q>AOv3v-ociFO$;Po3Q2n*Lp@zv z)tg)9PK=;)NhvUl@l2?}YU1)V#gwq9hhXrQ1c|KgRfi#12&xN zFb#`&U*jcKp`^c90rxJ7mU!UPz5;bf^Gs)A%JI&MT53OOyIN!BYiIh1b5l`fEWK*!;cg z0zB^*F-p7_rGV3XED@y`@tT+TgqsD->d-zG1GE<7G+D);W3PA zmBIFO(ONr2dz}9vdsa1L0%G)^-}jBD6N{*E9Nf%%<-sUC=w|LlJ^bhNdDW$*fLhv2 zsfLStHKBn#`B6;~eWH`6NXiluKevCvFA*w%v_hSzCLKDuXG_Ys@7+oZn0DHY&n}R5 zSP(g&{uWSg*XoHz_!&-v)gITMQIf2?COTUWZ0lX-j2ppl9d1f1AKKVofA>rC?u~1A zLmKa=>Q*Or z-9HCgMlKVS?g(4l+rw8rD7L(p5xa-eyj5pkxy*SJ@r?~B9U-GZW!INjjrrF8RQM09o3)m2<+W`BMNRa9{o%(tXDp1%vU%2^*NTJ?HY*F zXIJO0q)r~x?|O>ORq9DE%O$Kl4DP#l7cnDQHM*N-Mh2Bh!gJ`{nhhSwnO3xFBe{S; zH0t!Zu9{mwg@?s@(8+TOSo`AKsVx7A`cajpGXJFA7>R@A@B*7`gp27`Lj8MGrPm>> z3CK*?9(-73mUseS8$DMPhtcgIT0yk9+rGFPeGT(7W3}Cs4pRa7(m)hO0K7) zd(b*~h4(2CRP+<9qbbCdfyfYEwN}_z>4l6 zLR`s?ysI*J<~FK%zZJclUesnD)~^RxIfNI(mfi%t>0`!|A`+w~8KtygEqjlo?MI;C zz5#3xBiH^W5(k?mszk>kK8%+@j?bb=#jFonlI@9-38f6`-HOWP zos}Up*c8YKlL(={^4-4oK?!rST)<8C*DblxWR6pii6c-0Q6qX8J=8$vQS(oGdCZ0J z{acp9GQYf5n`={CYTqr28N28>{^Js7XeqwYxtN-m=QcL6H?GXctYmSir#fw^lWcZ6!q7K~F57lJm` zQSX2xZdXXP0f_Z}5`!E;Hnskoh-Q^EE*&kjZBMI%(puyfHBax7Uu}Z;3d3){D>W5U z12>@rOktLkRh+tvRbd-dF&1U^f-}Vp`@WYMyh&kuYTuje7ufUCBl?NImjfQhS{OgB zuC|Z9=QrmIcT^sUI5xtCYn2zbGQ?$aLc*?l;r+oXO$m`rvjT_4^u^v6M%W2I2wf9w zxlRB1zL`)vk$@6>zUC8K)~Sjje|&=9u5B3F`?ipQmG5UEy27zu0ejVT&n$SK@(yVC zJ0tKTC7SN7fQIyN`LL;q^-wMigm9@PMg$LGyS4@;Q&C|a@Q>pn_}U{_Csm7pP?RCH z67hs@@7M6B6@K}QAOvFi>MBBh9Mui%t>o|lCx*?NM~ctIuv@i8_r&I~M)=7GZk=gr z^~X*RM>Q6*1BekgLFElr-X#_g5~iIf&i&^8_|mTg6)Y+Pg zZw6lAsaCA%$frX`-*hULOG{%y&kAu<+HNI@uOGbQdfoK>z`!Euo}oojm={SlWdTag zot4gn&%1)xRtbBHQ-F)G@=}*kz5@T51XkP6JyIZU7rC+AF4+f?Aa)qWIXPt6v80^e z_r5;W0Dj6qs>2eJ2F14BP>8lAdl8~4ZEvVxF-DGcuffP6?RLO^if(cRl0ISb>3s_{ zt`I#nqJp<`+SC}bbBm+NHz&47J)pG-h$>L7zKCt}s=18+Ef^r~6wmjF5?96&Q#Z*S zDVBv$13es;Pq=pxw#iKH*oGdK3kEaF4G5)UsSM ztgIRekHrRub}%BZIR1_YVMr=AW9=~XMtpE}RIk#kLQ+tI(S-L&7UJ1;`<9lRQhmxA z{ficL>lFklCB3N%LjoF9^aAnaFF@@#ce+1=X6bkN*0HiCa@R7dLUa^vMTB9UH%Ewz+K6f1A@9z*9G(CxtU8U){UPDSSRnaa zLdIQj;a!%^poc_;=R@XM<;l%!6^|~L5O{_ukZ<-F!p}&`*?saMecR6ls@|}(WbnaA z+cj*bB}(y;#jNRo-9yf#Kij< zRH22u2Udw1FwKwJwpR*?%4t~o!qR>w=q-!a(w$$3oIWStu&~0Y7L;EHD9C%?PO_z? z=?Lx&&iFqMpX0JT*hPn4dVi(nk}1fPF!4xnPRBZEH4Go%#Mk;@N? zqYJOH!~16=H~@p(yE$?%Z^c+oNCxpKH25lM>P#5nakTex%K3~7-SZ4U%Vop#X^y}l z|G>B=YM2<89F+1Ga1lV2FB&xH@dFFRcA z?6a2o=%$T@O2ZU}zhOyV%rm(QM|gWT#_orQKFs*~Ql_07`P_t31PV+1_T*mwjY?b& zUB5o}A2a4|9cin}V&76_^>|i778`#Nz{AxT}t2*HA%D-A-XV=~d_o%rj{` zSu<)53DkSwV6tw%6H#ip*6y+{S|B`kyeNIb8k>w#2eDyl(f-5g)WGJ>EaS|XSq}3t zG)!u;bZ`Mj#OJdOu}Ezc-AS{`wj%A(i>hx!thtT>ixkE!aepn16oiYzKKv@VxeG-w zTS+@k0>|eOj4^%4=GRpnAWRKLE26J$zqLX_xkiZmbNw15%&*$yqH-UJe?@0#kOF}Y zP4%x!4%oyb3gRS*>jWxqPgHXyit*cY+wL!Snb|%_%KSvU8e!ATFvT!|>8~I49L(4z z*uJ*JmiIJDfUttq`8Vs|NUGe?TPS`&*hP9m{kf0;mX z>2>;=F`>7NpsgU1;En3%Pw??_;yc8!F|~V25M?9U7jT?~=xakoPzW(9hbj@;`mbG8J=?@xPc!-`{b@rB5ZV*!1&!?#SHQ;baHU?X%r4cv8yue0_qwR;KusGRoa&hah$)lG zXZVtbSiOU<$=xD#-6~NDV@N%!OL$S-fU<6a<;3?6vHSA0SmNN}wZG2;VCvr|sL0Gx z)oO2MeE456Fce7PO%u@o9tBfjje!BCW9oaupJ{;1c-P*S0HXB6{*x;@Y0keP64P*> zUvmDBKk1( z_1(OEjUlM*Aj`RgV@iGTF#l@p(*TOG@0&Z0Ym!DyWyhqb}2jP69{QytCW{}tx7#GJEG&h z>=Bdn>)hQ_dw#c+-6O?^mPGp}7SC##=hh*8gg?DDp}k%%?sg zHhF*zgz`)!7P4+Jc(0N7iY-4-~x=UjmJ?k+4wC*U_5VG9#~m z(9KZ8FhegWgg5)fNjSZK(I!=CEZG;x&227h=E9+hGKh_FH0z)C z(xZQUhcn>k6#XF=hIFj})5V8}IGwJIBA(z5xKB2v#`E*iVYJem>E`K9{UfkKcmD9# z6er8Sx-$fR5Q(?RbJpw^H%@nu!#WgQWTe+A7>Y7VUVKP=7yv{PxD<|y;DNV&7pi|d zyHii6n8^=*s`65@y!*7RySPyoe4o3(IvsHRU=W@F$P(cJHe@#B+{HAXk0U?-@OkLJ z^+hvz-9YU5V z^`>8+!nh%%6TAane2{KG39e`gLMi$p54^Kq?O&e?5h7k79>1(zj6ZR%siGu*)Jq>J z-w160LN^^V_gL_WFcY3Nos6gA+cmdlMp@q=1zZY;twr*%N%K$rqFbm~m;4se4p%iX zGE*S}?*H;5djzqPv6})KZjSX_Yn08AoP+UsgBR%&P?9+ywR&VjIWXW91$t8#@VosU zOX%*x)`g~x3T~{MJ?eENVc3dq)*b(q1Yw3&a|_3&Emfo)+cBA$F99wd-d&iGkAv zN$f4D*4tC7?nQ^ampMg%Eyl31=EY><8AYbL<|SujyS8bse2nwP zYMIO1p6RLu^Vr`bETCk4J7VD~b+fk=8@eWpE<t58ArdCS zw{ryU*7CrHUpb7LFh-PJ_jahg&i%^qfm{7)Z&u~78ENCPlv%})H}TAO$t9F%mkX*0 zgMINm-5+(T0J;an`PUlNl^7ltSX)j-ud4S-xG>kDHr*Gh_*Q*%M$zeWr_yhEfno_5 z!o<^)iD{~{^2y_^)IV1x@(GRGgR6c%u5E=-1@1*IE2UYB?9>zmzixkp^0ISfkz9m} z_S%|lfBFvKD~+F?+aI&OnxXaiObG)x<>NKKOX?Xaf|Kz|uG#FA*MXe@YVEW|Rx13* z`rq5CxSD!CN45MLN8G@}LM!r*vSL2%Kz^(g?UhL?a=S2PI@!G+bI3P|5YTOIg4@oP z6z(f<0q$|D_ZYPa2^-+f0v`lbLM2rFoUK1jFp~nAbndryoyJ<_mM%Em*f@5gmgw!2 znWP^39yq~`BBkNNP@&QS_Lbk-Pu`^SMKYrZ`a|nggH|!bC}f&BQjXNODY!;8`oxAO z+jlo^t5bUA6k6?tXn;;Zj3y(-GrtD?trfi2h$++Rur=$ITXuKN>_}3l0R#iDGR?yA z;qMsn;aX0c{T2cuuhyY)375P}dmSA!iz^n7SnIPFY)iUx0iJpHkdb;j+MLOuL?FK#fKm~=1gWgGF}GW%5S31ETVa^pJdam z-g*NXdn?vw0@rHjP!No-+AHFS)y49Kpq;W@E`$#Vyo?IN=9nh^$?%cU(Fe&Gn zua0E`1YQ_p-!;_=9aQ=7fpr^qz~_VNi9(Z^OqVh}t4*#tkO)0ZBT$(%4wOu0hhP0; zT4LkROGx{K=40vTnv{to4;)BBr{aMA;RFU9h-dFql1+|4w1z%&L z{)Q`@1S)*r4U66R#^A1lWYM)KjxH4iI*8D08;|hr6Lx1OnY}fx**#}>Wl<1y-a zm9+;tz|P7uFjMv|DY|)jg74u&NJ$yzQU9(fyiAk*!|grHpP(@fU?+QH>)2cKBVGe=% zoBi#x4=4(em*|=+n4bqE4^v$T(Ss%3oNY2#@BT3u2bLn9=JZ}LJr%R*l!4%@`x+K^ z#1cLcQGm1Jmhoxv*mgFboglSDZlAKBVA zA1}Cz6o1ww1!g_;Xa9JF`@d&*bZy(o^I9lNd>fA=q#KH)0q|FUbQ$v%zk$JQr-6n9 zjk(W36)gow>%HPrbbLMN`iHmfp&x@DevShchZz}Lk1gYc$tU2=kiPH}U4aYzx(jKb zE;Vp6Qs|)=@-;TllBQym1fdWq_##{LLQfs{n@b%&-g*i(8d5#Gs2C+WsHhbZXLYu^ zG6mD!tn@`;+kkm&Pd%0MXvy$qBXJ9dtulDJl{_^dLg3IJxNw*GO*F};u=?AjsKCsP zr|Thmf2`5fZ}8ysHFigfEE6^ultnO|dr-xI-=DF^sL#gI56gJikK8UsWU_Mi`=!R4 z!@s$&rC5zbCd+&n-$bBt%9xiieXfc? zvb>KxAIR-a4yj-sD{xC9Sdc;w=$M%9*5dCEhQ0pw0-j=XnoLHIW#Ev4^%LM0k?BMsUHe(7Nt&Fj;RQ# z^j|$>8a@8xo6;;W|6v#o)g{vaX9qn6^#n(sZvB?x;j$rt=R_m}p_4|&6%yYfmVf+( zI$V7L;!eHgy63=U>0P!wya=%V{tNTAlGL!U&YO)2{;Lqz)->vngmdf5+a7`4rq8j+8C|gPoPL$R**kyjt1E+O>vbxiR&qi(Ky?aA81udzg37N00-+cX z+JxJ|XS=NQOF1pXSyg){`*>?reEkdgJ#sIkAVvh^_||vO=_drchcGe0aQZE>C8;iZ ziXb&Vw0i<{E`lyE2n5UWxM>7i@gJY@X@>uc*~!&KHf7ZTpti8?qhHZZSYPmF2z9Xd zkotS^D6vVgIluki{awE{Tll#+<8z!NyeuQaZ?H9s4#tQtiZA51Uu|Q>#_uMMvS${e z$7l0?_zthC^V%w0@J_A@yu%}QD0aDuPlpI?pYenMT(EFzaM$A!?tjwTe1dt%v31#L ze7_rg8rC0Dp7HHUEn(Hm@4bF1Q#;P#>z+Fk%iFV~!|0G8^9;*L(TWiNKdw`e+Ap$T zw`oY#2#gRi+p9j{muFVIhMK72heGa|pR#WG{BXdCbc>Qgx-6Um<9u(l(L#sKx`Ug0 z0jz#42yrs09tv`NUwBI$!zPwr6n%U~&B=WmDtE*QW*}k+j))Ey4}N0eDv2EMKKNFo zCJN&ymKU2eVcvR3tNN>1_l+Bz-T<;bP!%6e5d=AX6C{B9?@q1P1dXz=V^oD<>yC@6 zfK1icO@r-HUZB3Nc&q<3m9z-7q#-QZVJaVL*2Q;}F#Y)SMgUPim&(Z%mHf3bwr@Co z_x|3sZN)1NmKzWFScMvNi=T!b=>L027d6mHo3p{89-Vq2l>!g=)`MYkhS)@ruX`mBn&)|hzPnr>B3=>G8MoNS08pK-k9p{UR zf!j4%GFsY-r0q9`NdCI7W4M=sa(GX;pnRLyo_tkj`NFdQKhz4Y@DsNe&ILL3F=LWC zmb3Q@5C0qq{|jZI4yPJU0p5#{c`Dzde{CnXLX$Z+6Fc-Gkp(~HSz^j-u*E$WJ{*v& ztI{`WVH`Z@aL*!k1vtfU?n5FV&qw!2zYfj6uczpCc_ep#J)5B%ionjkHK3cDkL;5T zGJ*ettU>C@FT;?wv_V$Bg*ufv#ODt7UA0(s7(1st%U7o*8yBP=wC8@ zaOEDv!19rfYS>^ntBk$4z^|r-JRTd{58+r8b}xNPaTI0r6Qy+@Hl-@EA-65I;om_^3LKpCe~}=GMn<%k0kyfe~evQ-_OT+3U?c=%HBJ^<|+i-N5TE4@%EQec3W1YeuDpVp`+N}FKa zX8ANW3w;ILY|R-OHyv`F(`_e1d1(+$SG6a#BkY-p{KAe5t5x2f^3nsd$P|34i<}o_ z?L%SO2$q|+uyJ3`@6*xzO5aXTA-K)k*I9q%F6T>NT&fn(@+%^!UdZsGH#R4sr#Xr} z8Xv${Xgw0*pv!-%#07+^aV)?8gr_2F06M?Vh}=Oil}9b&lb;x--ta(Z-;2?MwpwvE zhU@%L5tb`3mEJ(og>b}Ey=djy-D6#c6Jj`;j%xMJkzW>o#xwI3W|uC#i+s*#qR~^w~E&ZX_z_1f`GX##vw(2~?73=d0@D$D$V5K^&{+f5-9s zeI$gp;E#HuQVcqgMfIwf2UXwy2hf5Pa_8-GqfiM)6go6+f#pBn|FdxM`~akKP(IQZ z9-h3U7AhQMuedS_LS_z^hJ{?Y-dBJ@C#_m2bLRi!5b^+N`yei%*;u9e|HB1DP~nQB zak*PaR`PU>7;)xer%S~7gd7R!XU-y>zW$S^gr?rWox$)QL=*HR+&`S5{@bws@S6Vq z>3rVdRJ4CDJ%>U-ZT#Cx=y&JxT0E8W!#vK@%eZ;^|E@p(^WTnyZcPJFo=!Bpd05>> zd9rbW8bMp07KA(*4MEiieUA)0-sW%Ewg>$TTB|3Ui_!aYCJmljkf`(12d;m;LU$cB zd_n}MX?0u;@O5)H_Q~o^c+|*v42;72@ zM82=F4WFo^h&FHvk8i8Z;^%CrCzNPJ@N_MAP2gU*?dn3tcG50&Ox8Ul}}ij27qv0xPE5E*W-0r!!i&mWh`3n*>m ziKd^hH#6HLqP*wjAr;%nI_B*Amob>DFZxIicUC~*_R{EU>)yb*5wyZIq1W#^C5EY| zlaE7)y^s8q{I_t_$;Zr-M^uDWcZrF(=&x}`9g*g#JaC{rn%w=vE<29F3BUB%&YHlh zLoRu*711{Gd9W(V;_l4}uL@O>T!?vu8)<*N2SdnvI4Pj{q<=3^iS`vu5vPtq?vwvI#pzwt zrdJ@!d}R^#5#S(%Ha5%InsAeBMVn}^h>+sW|9>;WFw67BL|CKDG^&7v9d3FKVc0FG z4)l_yKgGCZeD=Y}-NcpI32D#wm+)+v2~W4GhJf60h+DvUHTSwfA~#EsEVPJ0F9!Z| zjTVUkgMy7&Rn(pyFl`tKL-hxFk8bXxz3bn7j^u=($j3&%l?{o4$6}q&kyypi#;be<3Sfm}Lcmv>!36H$_m${Dtl~>}?kh^itK(H6V??Al=&f239?m@e zk1{t5F-7Ed!Ok;wY5;IOcH%l~@Leu2tQRV{So+Lr>Y`wjv+Cu6(qp#tM)c)_M8C#+ z`r^%`9br+#p1eUwG-Z{_SCxjlp>GE@gkJo2L_;;OR<~__nq(s9x5~m(fKk05m5RaA z!{DJWtd!S?j&L})qHnv-PbccmjxE8oHGHVJO7B%56bX#UQ3?{6U3JlBQVk_oCMp#L ztMrO3F+m=`EXD}-GZyiRTLYt5Mj0Z(-W%5HTx279r1;~OJ7s>u(F9o(JZdmoNRo(q z!@m|2I`joH=n&1T$rYORX;&~~hR&I0yisL@rZ(xgnW zZmK5s`&oRFRh6mIms?N5&88=1kuSa;TQ*>%$t$lmdg2s5@1)2Nlj$Ejjc=UVA3W$_ z8oNVw6vNag6EV+{))2_<7CyKnBzu~`3jRNS;Z?}mzW;%R9}P~|K~!Ebvp@9Q<+Ox@ zrI^XX0ftb1zqulN&jkNA1RFK5yRSiMwfg8mUE-L`-NL2eV2#GgV$TcL)bv!~VF__| z57x+vTk|CYloS52LUOz3u!L$|DNXH2Ql#LNstVN=@j*;^hmCpLH*nbC-|CGMmWD#- zi&*`r1ef{gRo(!Kfn6!{ATt@Yd~~3MUSaqGiwGHrLuih?nKA8#Y>Xe2}({ z+hx5{wFYlZhv?hpO$tu1Rn0x7GP-yxM92 zl{K}*he=UvEY&t62o;Dz>lU*fP}uunBl;ZC!ioprm-%jrPjpgXoPV|FwrQ`U3to#N zJ;G)OIi4wVE+2^aoEQ@{#zl{lAXxOrSG>eu7Z~cn1C(aXO9-j7Uhg@nQ#VeCP7Mc= z%asY`5gT{Y?J)sSbpDi#h;QtoASOijI#4BprA#&;;|8w5qP#c_8K8{Bm+kxDE(L*B zXxN<_mQm0_g*bQYU(FJp6?`Gm+B6e@wy3sbhi5lJjX6X!{B{qP>6LF20OL-qL4%H` zpz8x{t;eYaK5Ss)fEuQ9Tb-AsQmg6cs9Y0!pJF$VQaT0?rFi`?lnBnalv$tWNFKcB zA-Z#CtpwU}m1YdK0nbk!%pw3Lnd2@fkOuSrDax&SEAZUg-#}rXjnEp0|7t%VroB#` ze~@n2`>|zH`N(K7?-F~yiX-Rk=m1&%E)z=S`4q&N9OcnqcC-V&%S_V zf@oS?xe3U`y;9~QEn2%!2-($5d7n9WM%v5INiFTHH#*|nir>Kz&_!h`5U>-Oqd4j6&o)RBesV}Fp<`)-8xHo=%vr!Q5 z{g75}A4Shf=2|+?A-$vwqWjH$HfC7*s`3B9-a|98OM?|W!=FWb=w8uh=f3tDI`K6W9=m-5QSDE^rl-w& z@{5EV#}w!W9@Y*&m45{=s((?WUl32QNyAe@E97Z5T)R>cqI#dgHuQB^UC)hpu}0ZP z^?3!rz^13V4j>`=X%a>Rx|@qo#Z=M)J?ys!a+LFPkh6~ZmY2lVDBe(u z`dK=Dm{eE8CSTwzSlZTkELIZ!R&c>|AQxImHYXm0M=W+rKkD_Q$6)}WBnT4~AOMFC z3K)8~8WI^;j6|rZh<~~*_2@=UG0QXPU<+bW$}ZC;!73tjcZGo2<96fVm=G{WiPXAQ z%Z%YV&2=$q;#Fo>gQA}-ujmN{ZSh^g%LqO<9yqASeG!m)oJk}38vICAc~NI_S!%$@ z?i?7P-}tjg)1KUOAtoimqOaYs?U+f|0On}G-48#LH|?olQnTdlfH=y(zcEciGP|me zGz|1OC&`ubm3&ptaXSl@)N)9*YbY%j&+=ACJUIp%3&(<>#6@h=GiiF@#m<>M#$0_@ zq5B)^f=g=O^N#U7{Un34@(Lm~FUFb=MLJ1eor+vIo~get&nlt`{cY-8>WTN=sk|cU zGk9g}rvzau7yb1R(pPHN36%F$-5MF7pHvn}S9%f;_HTjM!dV}b=X|xvkdkxJ@kQAV z-}T?+RT&B(Hh^W=C*33>?+?|7A4-8W)TzW>Ohtnh7Yw^QlpAKk*0<%&z><~ipcAt% zA|(u)W-BRkfu(wkjg?jpSbyjlHCcQLSXN6O2+swO)-D?G-S4f3H9mTX6tzKJsq%lMg2*L0YMx?);=l(j zfCH!{$wzh>OFM(?3+?QX(6#CLT&3r%v_lSbJq+gGrjlg(jvfLQ!;MOdhLQI(WP-o6 z%NVKY2ucIJ3XFYv#Z1iuCXpJun`w3i?-Qj=oS<+acqEHRr6yhMRHoVO2g1y<%$Eq5 zd1-n#msgbTrZ^B22~eYQm8)Q(c<@v_u+$76A$F!A6E^r;5HQlAK!kTHfMeM^b6d$Q zvJt08MON&w`U#Px^&%yY?w74|=YFBQH z6C)rl<7<7|Gj2QS&_gDeJphb&&v{ipLMIAyZ=X42H_i79n!yBiq zBvnBQn8u0|=SOK4#yU6{l^ux=0uHZyhTd3Q!RP3{Zyh)g9k{Ii$hPTb07;CI6QC`4 zvIgSQ5qO#+hY#WVo05@IDCDZPt zB@%zW8QMqc?(o7E0CCGasRz8Gt8G526$hKAC7kXKj)OkF&fga5Is&D&hV+efJzZfF z+|%L!amiz??{W?1*C<~ic9GP-({5gxxqQZ;wAsYmT;+04F=Mp%w0ej35#I2_1O8h; z1a}I;#?(o0n_is6*N4p4`a@l{y$>%JOiVdcMJzAw@1s=CdqdX+dLyW-qKpo4{jCNy zLNJ>M5!Zh{PMDzY0)6nTI3`@aQVROdc@IcKahRGzyefY5Pp=%ShG?HlOvu8HK%* z4KFa2P3g32r!lc!N%7fE>rCaZ_|XKvv)Cz%QnOQ~!>tFujv`o?m-qWc&sNEB&7N-f z)6l~zoUK4sxdt}Kot1tpq>F=|^0JTrN;p#ETeUEkmR5=o<{9Q)lb(AYTt^pKGlXp4m^3a<9M%ACHdD-A0>=q(!JK-Sf1HgBn3JH ztKeeZjO5IjOBHcISLd@J4VJIJE{KtgtltK3qYoDm1T0SnUZSrRJpFhBeqKr@;DNFCwHP~s6PPwBJ&--p=f$X<@^Tk|F+@-#9)X?iZlcz&S;-ckve{a@$vRF znBPETA4r(Lw+A@tNsjcPWsWH7eI)TqBrM8(d>(IR@`RH7D5(j$2Q))|0?@IeP*?o~ z?(w*ecnwp<=!YuZI9pcw2&-hP0=RZKo6Yn0K0X83=n7;ciC&%EnVyhjUN@L-=B<(sFRWu|h7u+vcRp4MLbC%|?| zN%Kf9qb~q#I+9Z+fjz)~pYX1;(FaZ+3PO+T04UDC@)^+qVX)I8^yg!aCS~+z1&&{r#@u6XzK`+8H~XS! z@t>jRh%*Ta!sLiE1%0_6`65PLMa^e>%I3!n2TWIjsm_fSN)NyBh%>uE@xONK+;V?_ zy6*oOTVe`e<-WxV06c2I9TS}h^1y__^GoW3ouTKPk9T~lc~PIKgC$H(yI#HG(eb>^ znYe;j{br`6OK8aO&4%M`gA9Dj!^3=Y4?&l?%HJ9LS)w#JdnNd{0Wu{px8q%}Pwe~C zaxXcCpzjv-pOxe~1wbIy?gTiTKQ1fo6x_otr5}laro&VI21Nyyb!ZAF!;Amyx0IYo zoctNCzgl;+A^9s)v|&)1sOjmiAQWQ5Vm0N3K&+sP5^$9pBeLO^ITm(=|o z4*z?Dau^~MI-Mo#4h!b@S>w*ccEykJNUGw8DJ~hOP2ZO)DASeYi6jA9#G3yC)41rQ z_;KAVH+yOz77eDQqduQiY)^#*uxvy21=V%XS#y?T4@}^U8JX8=nhAUF3{#cGmBc&> zhQ|#K`(WyJ{^y^PfEa0i#6Y6N-<8Ek2lRsm=3^)P>1WXS9?fAD-1&@-4Nowv&CfME zQZICKR9D^I!`WU_TUB!WEeei8>f_Pp&Gj`zeP=~iC(}v1xkzLaK`4ZDtwBeB5MgO2 z1~q(_n@?F7@L@CT9zmh6k+8<+V>R1gCVK`WDV*Ma2>MRQa3Y|g`&ctVydkHXx6{5# zFYxRtH?3nr$G3{eu|>%U`QPI9F$282e@1I2=eHj}a^pg|o*Nn_V8!9w()qM=g>o0K zynNNS%d{7%Vm|;5mtRdcVtcJ7O-(hTK{&8I^SwO+b(b#`<~F*nm;;(cdRTYa4K$dn z|KguaW28~UM^tQ8r7x(k;+A0hOjskvf|++HW@?^ku$+$X(D}=0J&m;HiFnxqV}-d< zW7PMkCy}PbKNsW@?8ltY6Ul`fn2W|634Z!3Up2t1Qn=+C-zuE~vDZo%jqE!;w8|4z zzVn!0=)D9HWUN5hy=^RLx8L*5YK!V?CZ`W)UYmM=>b%)0UnJ1JiPz^W%zbQP<{2;y zR4yz$2sx?SPSX5qkrrvWzezv+Rt=q^C!X*aAV$14{w_+<>9)z@l9Th7njJ(*}DjLy4b6@b*BJ#wqHE261BT4zKDvKt5!# zoF9xO{@5G&8qF}W>g=;rT!0%L_T{jj>ox<*c$Ex_<%1MJ%uhbNVvh{_0}t=OwBFmM z5il{qUl^GfSQLvwEd5+c$FpYSlIXvw4cR@$dHajXqO^*qkTD-ZIJPD9H9C7M4D z{7j@GU-zEZ2iTGrs)#OwFJaQ#Zx)}sFIEcCkDY6`{KBGUnU3FCJ;@|!BL{jcKiPd| zU6*%rAQpD*Mm>dh$z4*fN{?yGl|W@GW*n}r%C^{vVpSsQw%Y2NpDIj^#XD~GEWR1I zaU&_U!RC>TxBW9v0R^REr%kmjg1)|CxvKHaP8roJROX{p?rH0Uht4k2SlT_zILs=F zG9yY}9#--&E*T4Xv(+uW6CnL+B*RQeJY1F!{Y!J*@-Ro*1WrDpeL#JTb6P*Z#2x}0 zA7uosPvd#Q|75?Y+O74c66xsf8tEQ1B1hC#?sW0Sxq(JBL=`S`gdP&#UZ@_uXdz4e z`@Tx9RVS0{X4pbxKTWjhbq5r5)UQH8>Fq6fvkZELo4)PWW!uD5e)16Yh~#tupeO2@;zd5gC(?lT?7%n>MYl_DPJ%Px3D+>g&$#vFAevhb8f`x zL`^(zzeo(Ng)d~k*|>2}^3>!1uSyPqpOot zB31&cRM6@s@{7(Jx#5>l=G;G`z+cISe(K-Umj`kYc?arhHesE??8vRKTfNL&eNHcsdt#{YYZNW26WX=bj@~Hh{^)3?rGcv;do!uR>q6)QC;`wDvL@Jt3 zy=SJc2a9#q!Uyb*XqMYQ*ZzEJrpZCm)j$U=(>F)BMdhk2G!5ErMTDgp=r2b*!fNMm zYmY9Tx6na)VYD%rR4PvB+lRM{t(FFyULUJ#S*Bg(VzFla!o=Bw@d{T^nb3?|sG|Ga za{#B#6Gp`|`SyM(c{{;$*z58BZJ(1=*pDBPrbgpw_8h2>L%{pAP|RUiA$s>N^Wybs zYC*%CQM>q5Cm9VZ<8MyBCiwg)v*kA8K;eFp8CT5Gy^8?2{4(gf=NL8;&Q>STI7b$P zi#{NC>8+@b!T=)-LF{1BGd*0oXS8Lo9Ok~J;e$lRLwLE$@mmh7&Z(&dw$XXs_)b-^DFpZ^wrIbD=@T6a| zPM@{N1c{c*Y1V%;0j78&^Ml*mt1EP(!NMrg7H&O`p;xuXmj=gd@|3@DcV@ z{01sXQmAcpyjy(S9$h3pB_wF7*gLoHTEE+zNX<}MntNp$N?lD|l_LpI)r#M`kDnzC z8ulwA88CHs2*OR8bf_>X( zio>@Y{9J9*HatF7iIE!VckHFSkIciXD`PY*`PJ$x-2U}NMY?8myu*F`_m8IR-!|Tx zx?&61>ag;Viu?k`%D}|}|KeYy4-3D#Xx9@4D`Sr-ww!_Vx$d*chX2 z^kf4oBo#SsE{Ff{wBJcLJP1oOG3M7Gr2^!V;RS_tXp3kOq{h%~ZiR_s=i2 zmZ0V#bt%is(n4cAnlVe-y{9h0%6Llc5)!PR%~sx!sPL%7FzIy}g-DmEEK5EDzJ54K zmgtiN#;T11*CK_Gzp!63sy~^>#*Ak88I4N`-uyk}06V0y;j(KbzQLS7s;@xP%x>OfqubPSVI+t4%o1!(1Yvn)kqrcYYw-z3LHx zQ|id`{?7N7Im|7Op}M8y=BWbb@X*{h#Ll*`-IDDSW2{h@bAV+0hb#P8kk_SgF885g zfXPaV0N&x3$;4Z+xx&=9W4U*WMo(9sg$`Lo|D7N>rvsWE&Q^e!Nh7`fLQ{sdCpcbcdH%q=x^Dl_Yf zl(HftmvlpAx5&&6HzCSASBa}cMn-lTDxrvEhU`uD-Xkk}UBB1SyWa25_xHE!-ZNgW z=Xi|g3u&V?Q8MN_dv^c&YAHt*bo~+HkZrkD1dZ)@-{BVLm=7}Skzx8#Woj17AEHU? zISQiePc!4(;B|F2xK1s0G`g}Ymac?AW&Mdks3+l9C_$Fpultv9uxpTI)YzJZ^q506 z=QQVU_~|!(xSvYNC=-^Q-tk_$KT|4KBNZM$Q`_+VMXaT}jCAsBtQM6?+lpn}h+(i) z^t7(ljEzFgK8>+9-gvv|rDmgRjGIjT;oq`nSm&ZXyj2fPG|Wu97Uvo`3Yp%zk@jtQ zEwaHS&N@iTqs8yMe*2dk`)B{bv+VWIkYnA)_L&0MTzX&7TCdBD#slG}I`_tg`6@dP zF67X3{}uU>S$jX{bvD`yd{vMH>lt<-L_gE>Uabd z-RL)>)kN9QLupmkU6Cn!x-`646twA~E{|@7V7h=k!l$NL%$?TE_;wMv-;dxV6%4)L zb~G-Xd=IINSaVt_mB}baZ*MYiJ-^G%pJ3i4woW}x{ZkwbeeBg#D2|;ttZ%kNe)K-g zBosxQf8$N4R9rq>O{{#_MUDe!ObU;FI*y_G{StR~^+#bkDCjI6n_l3H+l@119I1{3K#Irw$ z98*j^q(}PDc$f~iI5r)-ZVN5pC6jipXa}=H1tDb6q)Kt-rY{dIf&%j)ep@%_$NzM{ z!XPFVV%-g_`n>MVe;m%t-B5=Q*_@rkl%Mh%D;6Hjy0q^gz*WhenxD>8TP-PC>bB|9 zK`#ynYV;AS4Q>_P?a+$(-wsh2O>~zOXiT*rsrzq z89&y$1yEPIbZPHIL57dN^0l!~T6f~;_SCPFMad`SrDu4Zh^`7-KLgxGE!@$UFt#3V z;kbiOrJcPEcs5?Qv;KG4CD9XqSkOT{U&HX5{Dj=;wma}zZV zbxvqsWPfIemc&(}aAC)I$LH26;R>o`-UP|-yWR5`q_fA5d=Xqt+bH`&!g$a#-S|B{ z-c3O}E$uc4W-0R>G9BSbsxKla<9s(Jxy8NpYvK$)&p7(Y>qy7IOBh1kdpJpW-w@>MwV#QP1#C=};R3tj3FYP20 zS+GX2TQ>cXSG^EFQNp$r>eja;3f#wKybAvxY!QA!52@)Ex*k=E*uq{;IJ|iawAp92 zN=4}PqKCyPx+q~0&Pp5|po34Mf|6_dE}(V)mqNL^7P>m0DMZCWY~1=9XHT{l3NWFc z5sKw8C+0(>RgszY&YN+Q>esxC=h8K~x5-I#y&8{I^92kCWK?QyUg8C>M7`6^zgZW|NJ(>ozbLYA~Mbi;?sVvX$SK9A>ccy!O@kGDE zTs~4Ia84y809TcPv|mDSC5^RVw$ z$su|FCX2C8I|DiJp%;7CVhBi-W4bZp3ACX@S`XB)wI1F6^*qX}G`+W`Z!eAJ_^tPF zvHCQ84lW|s=Qr&HT45i(^c`i)15wJHX>))V$vFut;1b2nY~Wq3J9FDanV$bEDg)##`K?W96UCsU1a?>TCA5U zUgf35doS4abG|;Gk?{40vusd9w5?K1ifB1{*?+4pyU**TR`a#+$CLe%U?A2eeDl<* z&v342>xaIYp6RTQDoSD*#nSa3`u#l>rxmOfUHa|aKs?>hxf#@eUC0u&}k;+pLxnyu) zS)sXq*~_mebMC$9cI|J8K{uonZhg{s?=55A+ii=#nrcwRZjL_|q47Pzkv<$(t4gCH zlWKEQQ#HL~(7cu~zwf1@^U-h@E}xyA zPQ&z5Tz9d%e$lUA8NWvMkC%IuiomfVc-*-6_?se_$NQ|C75=1Is!Eo###H5;$05r% zA5QOOdT~X@x;2jjYK?Zfcj9sw+8I4q@AAu|-oYbXaguQ(@em>S={S1`;m7`4rQ^=G z;*Ds6tvxpq8>HvN9M03LpL z_s1$(xyu0f6!|IejZP z2~iU_@pnm)OOm4R_Z z0sY3uYz9a6LZfZ{>vEy7)}{wqq)ur{(K4mc)?J!=`14vz)XeGbDR6wY$VnKtJOr;- zlSy_PX#g0;X{a#oH8ak_u7!ngL6~v3TTT5^3#Rc;ZnDhL6wGg)m)t{Kv^~kctWhxK zh5SZC&qmLaN!+A>RM3rL=Evk+`m7nyXIY2h#`gG%-Rao|%ueCTo;QVGmgtDmBZ``y zn=d*zpc$|R18ck0^=^22kqHkM*{+OKek19yFI4vV4-mjdx!-po0 zb1y4-T3^1$hvsdlGTvr2hnA+8k{(AJHaG+m=!qO@CYwQ|tTq@L=)9VIz_wqInSV5) z9_%pxfZ~FbDk{Fs?zj79_J0L;AQ?e^oxh~|gu*Zol@aC9*%zE|u(qp`sR0~@6dyBj zucat047#Q2^&U?BK}$f6*mex-RRUJJv1VWG^5WC_*t))a1^&HP0T0j|k{dn4MHV=r zTso!ANy}eP(<;>aD&xm6DfqY6PlLrR6wSpwV2Y_?rw_53GT+DX?yaXSzEc zMGT1xS;TXT^#-pJuV4KjG`2Gd&Pd#!A`1hn*s5rGnHe3C!%~??`wa(s3W7omrGlUo zO>?f{0p?A^_~XLZ zJIv@dQf`;uh#)U~R*EzgUh;4wB4cMna9~?-_0vjw6zb_s*551g6P1$7FE=7fd1UT0 zw??q>xa*@AuT>Q;pM)e1!F&Pm(M*TK7-;FD3USJ?AwNs(`L_jyHq2JOp_8YvW>?75 z=f|U;(S>$H*_V#IL>8b68XAlc$cRdE8_Niu3B;Mx-R64nl<;B{0BQ~tm^R%`k4a1)2oG#D&+aR@Q0*>-G zHw!xNy+=h)eNY-ZM{Js;UIl-&F_M?4D~;W|a-mnn{%fd z44PTzI$ID1>&uP2A~BWc*Iv;HUT?NL`$@6=01I-P3G{%1s}AH%UEG=eY(n-ZS@ZeJP$}gb$Hsb^e#?|Dg|7SvdB%o5{gcJ2odfzg zp7e+|fiBPGd_H))p#_l}t&z{3>)(@#K-DhWuJOAJJbWRgEHTtts5WBxm^GE71l9}V zZ#(i}<5%8+cEJb$8>3CdDW1;|oMg+4w~a>Fec4j^Me*8gVjfev>5 z_s@OadckFU`<|O}*`nF^Qi&VOAk*Bg68KQ6G{D83Z)jaWPkb&$ed&Ir>{LVu>1%kS z6FpIGnU6n?^=Y7KMsVY|e8a3ouFOCkV>OA;%XLwPk1>o+R4?<}65$Y_`zq`MLHkHc zO!?~&OIUfvqq}QnIp;}#W#7ur;pnnV3df#{$}z)YmYITEQHkdAMWa#F%WP#r-I&(_ zYeObe`%67i2?n=ELMXTZ+)6T-|E1C6PgtH$U408GkhNCYgSgw~D zCt$o9<^5);x4$Y_!mk+ZOd4~RJk+CGbFzK1`D$}-MR1OF<=&FI^TldbpWs|Q)mziW z=*HOfhz9yi9VcGo4kBrm9eWXt`N?5LDy|Q`H)H6+9sARK#Ml4S__c2h6BK6I$HVL# zjY|f)8-z2BY{h7nCDK0@_Amvh^*XMLH`Zf)6qpGAl%Cu#deA<7W>Bx%{G<WD4*fu5sjtkj+!Wh>sXe6My8ySu0ZZ(mGQM2^o>l$Mpn_^ zq$wKy6bO1p9fvWmKV&9|eP6!Q^cHQU{plll_c0* zuyV83;zUu|RjI50)Vbel5p0;6U}-58&7^O5n9K!T4gB1k7Ylv zU3Rg~J#WI7EEINIVx|IaYVcGt&N^80Bh92vx2J~wx4!A=#j5Y7CaDZ=swr;he>m(6 z$gwsvpTW7kVWd)8mNp8Ua70q)b=0Qcwl*f4<#xFwn^bBLbq0Ghc9D>O#Ea5#e{?^; zRbYSH&s|7~Dl!FwIsj7VAH@&Ye^~Kp*=_(~{f6y!HBrZFQ^)O1kk9-_64@Mi`_D&6 zk`BPYxoq;9QPIN;>120xhLgwUwzk8~H49f8CX*YAq`69;uk>%P^JvMAbyVf}&!pbn zK{S9|n=m3Q%HwVy*szC(9-i_WcA91M-fW*d<-PQNFlugVX|)9@xhti*on&TTnULKE z5L9USUHb+C$F(lZmNw9BezEd+(i*ZTJPu$Qm(4K)hkW@01hGo{u|mK3R?CCiZ}`2 zq3LGE45A0Trdh2SH?Yv&&mPoycc5{89XKzu>sDIQYz0iE4V~6!ZzppUvi(}{K&;Ew=u*kMl&7^%{|POGz)rK zzs?4ErVY-Le&A89&X`$y)vc>&-jW=@DQ6?PfDI*ce1Tu};;C+%O9l<3b+$dE72`b2 z9DA?zb!a^65?V||OcMxKrUE(}VZQ})3VbE7LOzdiPgnhtv#Mgq6GkE+~!U+>ZkyOIJGjsh|nCqE^p_U1{o6bWFUM)fZFU zr7n~61udZ=du#I!CrCtKD$>cmJ2(mh zlXk8%zUFJaiTU6A6j?L1Q5`ejKMA-V<*DhEZGgnM7<;efKJfSVVqZ6&aIZ~j*oQja zjqwbG>XRApCvM}MmjTlidE298H7t-(r|*%w6XpD=G3E3kF-DoYZ>f~xG|vc75VVF$ zqC~-u5zldLnrOn-;TME+7!>c-=kECb2E@g%&Obcvvz}>~kV&38^)t`z9%-qhpC%_g zdFA~LD5KTL1Gi4imZT+7*tg8=-5=9jeA=!0JU3dUe)=?7EcvJ5V_XUiLwCwvC(0}! z#@|q9Pb@63+<)`zYc-KJsMuy8N?L;Al9@P$3tR>KTl)!QLY(?qZQFs5u)bk8a7oM5 z(_i=V46boq+Oq@h9M9ES=GyvX20&?-SdYZWSz7u$m0b^$xUQ+ENTn(zWy4u=2xXGF zkfU-Y6x}RZAV6$=KXWO{;?a5h7ogZ*r|n9)FR+*Eu`~iIAqX>~^JcHQuSE$%6)b&0 zm6}I9<4m}diRSB^l2DRH`R4Pdx1{JGm8t-BC9yYL)gRb5!HZC-dVA^9%%yA0bB?D& z=)}dCesGkkdTrL#gg-NI=5J z>FwSS)=8mA6as(#<`rO|iqipW_0<|vG|3ciE6SpX?W4X?cD&*fg_6-MBfn;5s>xyg zJo*{yfHT?F0t$dmoxyykzKa)Yw~L&fHWGcQ8LM@OeNr{cd4{9l=@s-BhM`zj?7Qwt zRN3heK%ux|C)mHGAEVMPY70y(oV)}U3qjyaH~Ha4$XD}9SR2=#Y?2ar5dw@X;I@%R z19UHw#!tHs?AcWi=&bRKr|pcdx;1QWk%BdTKfK)?+2aln&ee5TYs`9w4EK-kY~cDm z5{kSNb{03c62{`O$9MMfWd4KbBivoCY}*idu5UbSZV`4%*x9tWc~S16tjlkL1#Rke zUgAs{tZ@{;wj5AR3GAM*vpurI(+dxHal0qs#Lx`wAnu6@m9+yr-Ys)#_|GiD*~h^f&g3fDITtU9Y&_ zr}zd*vi>9=uF}s=1rFr3Q;3p3rf+8?$A8$}MRsAjNF5Qw-mXIo0XVOrI=+!26|CXc z>8U}>=W?TR_UWU3qT9b3N*!B2cmu9FDG-4*Glyko0m^0XzB*{_=Z;`Ub8TVMrvV9; z1jnmCN$h*Xh15gf)dAo`FRmy75W2`CK^U?XY4}c~!cz$*ioFqe=ZF17S-+Liw zupfb93lK5oB!x?sHz1AbN>+}EbJ`jq+TY2&xO?>(!FD4uX^%UL#a-mLmRK3;v)i4{ zQp#2Ebxd1RdZ#%8Gz|9lilWKI5xNmKLDA|gY#LAFBd(-yBWTPR0YH03!IRLZNOav@x*z%IAiSMQaVZaZ)?u-vAMlr6o+?>6<9t&dR7#w`iguFT zSl!t29P7d}-hSfFH3-5?Gott_Dddin7C)XC=}feNC>5>F%z3KQ)4>n3$-9&zvT^vN zeNn`T_}Yh^*dUL^2?O-N*Mhrr36d1X0nv@%{>N8j(yq`GU85E0&~$!CUe}<5oM>Bz zn+2<1YCgjs&Im5#JIZ&7$L3fIn=JzhktyuHR3^@5h}YtfPfYGkSnm8faSF z{Tju@zY@*315%20zzqLa@FdpfJktUelvj>dK&CDbE=(1|@&e1l>@jgDDi3J+h{NR_ zDyL238yV=r1?W-(m|y+F*=QwrhM=%3m){N4;A?r$ zZ?d_hmvEXBU!kgc0C?WfKw3q>cA6XYuvR&FlmiNnzPVR?e`s>zK0C6Nfr50!VaFR0 zM{8UCTG%1Xd{(Y;q#KZ|E>wga?mqAZ{Tuep5+_T-xvG? z6?!CH3X(WlFD}j`vp1yHCBhuSM+vd>$x?qgSTl`bCl))DZgg^+^TIXM{A~^!wLUkF zDqoe+3q(Nyex^KxB%UwQ?a5E1Zknc@&wDF0>PR=9OPBbY^+KMN8bJM`wcX03u>&5f ziee4|aXPol#IRE{%wBBNKe70D+09s!J&Mkom?}cF;p4@rUqpeXvlHeNyaW1IgLUqv z`S5CmQ(%{(U?_^#ceY*UUe_6m68gqk8gx5#A7Er!nxw7%;1yCy*%k9u zmoY->4vo^kk)jbqx2pWD+5m^hIrGchXMWNaA>x$X$4=6?1zB)=9`aJ%63XXxE@t)q zcQMBFKuSMZ%oL+z{2O4B;?A$=`SreE#DBLznV=-3Jk}w}njpJe>*L@|33%r}E7_nJ zFgt%2;jT*UZ%PK{g+CtyX%QXr(huRx`6|xjEhQPk@u17`dv#zty_6~xAt+* z(_K{`SUI2qo|jkL-sVkZ(lOcUT7|8gQ$6^94q9u`VLz&xc-yr?yq7nTWFa$)aGPv# zMnR}z7sNP1zdSu-O};C?cMI{dHBwbuwH6+j%kr`N6eq{5S--qq>pB%#ln!JeN%dRw z71gPu&He>WES9%NavtzNV6O+j6+5|X%GuUl&@@`sb(Q;qY z?DbhtSoR3%rN`i2tE)_PZ>r^WIZuVtt-R8}o7;n4>P?pTCeHKQ$}H|bId&5-HnZMp z?H_qbxMOzI^63@)Vi?n(HIASFo!*azlGrhikJPx(+(-~HA6eU#%IENhP~L_S z{Xz@E9SSM~Da4%%2Goq{L+oeGBu*tL5?!ElxQr#%i)DB4Ow#)ruJ_G+V+2POcJVb5b!-5#_DtOPWFP;O!L6H zu$`e$5*=bk$wjlk=2Ac5%sg`CcYaW2O^VYQIC?eG7829#V+jcK%v1=W#)L&d>@hLQy%`HHe79i&ofdH z+kW^IQOjpHClI39xR>3O!qjebW2|6K|^O`vl$f3jw?$HZqRJbKKv1ucQnmXZy#j)rW%I>ekBTu|+G|wts_zE8=n#`A zM$jX<*g|6rf|rt21@5%^wR?Vp;5udNPJqfjcD#|tTNZp#T_-kT>c5aOZNl%|xX zTw+={%M%sQ!1IDn?_mAR2r8(bbt<8U$>j%n_R{P!+w_9EPO;hmH&6-AvZ~kL2M@&4 zU$*=iv}>XOFRT$_a}%zsv=d!0_BE%}V}{zODax+`ZN*Y8L<3Yk=M@laS&s#~&UQpy76-5Yz6OhnJSxfjG~1L7DI>*h#Cs8LITE zz58nHvRbA0Mt+^>H(4njWqx5XRU2#h54=>>9lye*B8mT9+98q6S1I|$BoFhJh$|ns zarK=2SVp^@7)R|d*oEdaCAYHMVc>A+erw>MaBqsZMo4C4GfE@qYoMXfyNLxbw+psM z(XPcD%9?I9=jLrSK=Zc0$BxV#g?jdOT@M>_H0jDt^G7M=3Rt9GLg|GN%{z>{YwzCo zE@pc>Q?u%46M9}q@vQtF3lY{mn1hcr{!KY@fzYUl?;-3qb(Zh#_C{B7eC-E=;CWSh zN0C(JP@U_Pa^0AhaNrh}Y<@J=BFHsD$Ud`6NSs%hlkbs|)&A$T^I^HZF5)Myc0Z-r^7aBO-Ts!X@hvzfq?Jo?B4eHlgGqT{ZUNL zg0d+7pJBN?9~)L5Wj9_eq@4*3atTjx%^8CPDaMy)JpvzA!4i>Df6JQK1E5Uacg8F{iiwG$ zVsc^*O!HBDXam+h9ynlQ65(afp81WE_-}$$%l#Hz8TiqST*cD(n7bI79L4`9{i@{x zZ5SHEsNP>0PHNGB^>4CJGFuieJ@}Zy^q-W*ZZXy*>i5Y1^Wnk8#|5l2Cw+g2L1Ts8 z4@eG+5=*aXfNo3krs=z~jx5xF{)>uw)mY>JquoW7dR7ln_gu=W7k;tv4zPO6U>geL zq7I(lQX~7io}>QvUm_>uP#lruD<}r|!`OtRB6)Ao_WNx|zxC4oZGtXIC5Jt(07}>n|PZ2lr!fgm{hh;6J6(o9{c@WdqB#UUiIG$qHacR`v zmu0n_xV4gZHk#8?wp{Wi#04P5C8SFYjQ;w3Pj_Jb(|>ECG`*jnT6^7}oe%$}LY|Ek zMng^Tx0T0!hiRu*+IQyD`SzL4k@s%Z|4FwWKhhKD$naKhX{?oRZD~V1n@ZfYjo|cq z#f>ey2lHl>;o8e993=5<7UnvR8LpZl&!Br|FZVAjEd`v)TTWC5(}p6=?GB}IT6!X_ zb-V{K&RQ*gI*VUK%=}5}Qwd*1o`u&`Zg%u+T_rU$F0src_PBbvg9-Dk2;{ASWce>J z1DJ`ht(J=!Ub3tn?8yEQz&k*G@+@Y38USKRJ?Vu@nQq%9E=YY9U96esF{5s9NwEC` zb~tw~wxURE_3q(S^IFLC1g_gQZ@uqj@Var%wu;HP@ls*%TaOpwxcP6+U$NUrjjFJk zPf$Vq3GU$TA-v#VFYFuL@Jze_l%;-N)cBH^PFJMbgzl3vR=&F;0yU)H&!ZB!e5k2# zd+5+&Hj8eb-&Jg4N2K|BAhAK*Fb1QCH#%nfmSs73BKbtFN}B4nM~R!bb779x#MUKB zsTUzL)MNJD0G-doy*OR{s$NQmYmBNJGYB*(MRh1xd-IsOSJ&;W@8K6sUI#miuE#Xl z)uMw2zkWVl%~6ATDlx4voOl0A`q=rd3tm&7g^!J+Xn)SZuZAHAXn$f9)BK1Cr-<3RIGM1p_BVemDE~~3KPCe zyxCw6ze_+PRvAjV$l0wk^Rf!=P#-kt%{v5iQM4LBjZa*XHGA%Hy}SCZ zu*yX`D4Q=Rm*;cO(T+$vXsJw*pji|q&)2Q%P{_HCTinhEKF!7I#Q0dw+ zKl@x;_wW8Ke#ZtW_{8WeNpBtEd)UDz@8$K;(f~dbr|SEu;J`KCz-xnnHp;%|)Y|49 z@p{!aRX5Jk?Wqn*vE0A$snbP} z;N0^Oj7cG%`I&R$C&_5T&l&TrHK*=yu`D4P7{_dm+1HG0^k4&hY@ya80iOO7EqFGO zc8CP8Rj5ryOBQ%&6kV$D?EjVPH&?L~&>24cW5O?DNwnYmA$Z;Ypc#F@(zPOa|55Qd zo6mu~wKl&jZr#Zhvgy`op*AI-qh3+J|5Rt@Iz!1Pzu_EgX5eX6!yK0&6qDoE{mjeS z=gewJfdX$nQ1=?cj`m$qD=gv}poW9JqiUa!MPDf_aq8^NB?IYytFy@Dp)uP27+Wao zl8QvHDM;k}c=vXa+c}e*RsPp!_|fj=OsnrWu1BYR)E@fwaYyxU`%4cfd~ExS=R7yo z^*ToXq1iF=J5(b}0d2rLbyl@3W#y_&9-X^i@NssKi%PYn0>x(9@;{)UYE$iDY&9!~|xFqRVKaFyGep`esq{@ne_d+Q9gZ z%kk1;+bEQYv;0f?R+pdNtuKQ}UO_^u=bSVwr2MW6uDT^1!O%m0Y7*&JC?y$>xMRKb z!>VfFc1TS2i+gYzU$XQMM6EUUE@{y`6fO}p3rTUm=!`*FnP4bsPDHmi3yC1bOdylfU^1i!bgj> zH`9j*;B`|P<@sJ$G3!=ez}Q7m6^yQ@urAwuzpi3qFj$}`(G_pG;y(Q??5=5ZYLNEX zQb{k<&-6s_zoSk?GABS$#{>wB3e#})=QlCyxYm`==OJhH?yC{OsRScNG%wBCJEb>8 zUsSX5IP|gt;7!$_`)zdJ^1t1Fe&?5@KuNNOwBC7re87=Af3ev+R7&*URC{<&vX9n= z`bgp34^u(d*XJk7i^9*6U|bG`V)Tv>Hfb)LGbBiC#65abbK4_?>anG-Rm{BDkc4w{ zKzw3PS6X;p7Zs!|T{EX6ioZ9u!tzQda>g8%_oL*Si!HUDwKS6ATc;;+tH~RnA)VfA zsnwS&3-0XD5~G!N+&&_y;qHOOz0HEz%b(V*T^{v4+Mng`T*0hnb+pDQLh3Izks1TM zJ^D5bvSu`c5A^Vr{M_A54)x=5gi3|(ZZZ3|m)7z-SA8C;om1Ck*l81{0w;pFrZt_= z(c8nWjKQLk9zCslhd$=!qk1RX#i2b|DAdQPqN~Xc9DLgqymYAh9q;JI%+_YMAzmhF zmVn>8%y(LGxy19#kxMFp2d5sLFaG?iV4!Q}?#tt0xLc8-Wdc)PdnKMFKiSZuYZ3ZE ztk(4+=m+*>(mz1Q&&o^c^YXxZ%J&JL6)jV2lAs->{#&udIs)2#-Q-#nTg=etCdCDaOU8ey6TqgNIVU8G zI#_DVteopEkmdbx)l6iyWr44Rr@KeUwXK;Et!zIQ>arLfy@SBHwq0Z zRC`TE&?mjCbB#@`2jD*e4&2UXAahF=;mL_0n9_Xte8OCk4WjmnsC>`9fKU_}PwdzC zDeZDX$aH7~1FP>KEngjumVnr15C$P>%eB8g*xPe0$PKZ_|KT4Jsi4JNM$i;9x$H_3 zhT38F126*RFBM`d{LI;x4;io?x>4H@X3^%NqHB8H%4WjW zigu*AP=EZ7Co?x;mV~!h7%Aq?vM;H2XCote@+H2gfb+`|$lgc%KaXX8INYAh^95&D zTSWGU>D#&Fy=4JWw$Ql}c6Ud-TSXQ@#wO5H9TAa*$=d|kCECqq)>y6Ff;s9o^4Pqs8tXFHRe|B#1=6Wa(GKsXJBYj-V9W=?sT&ZMAtTtl zq+pXrFB_GCFbB*0l!`F6jn$TOoKKC<#cGGJdRk&DCS)qodwqOxF^&<3E4W=zpZ}$y zDm<5BYn)l1kicoGI9)p_2vT!)_IC`!-0 z9e%P+oAr(D;)ZOa{lGEUEB8ut4!0G^|0fbse+;1`zR}8;1@d3M{gOi-b#*xm-Rw6T zDy4=(@D8#k77Jw=ZWxXaWzzdpS4vup$d=rDO9fHHF0KGG5oeN$jV|j{IDybDws+05 zw@)6nN^Z|OMe#)n5~E3xSB{i3Cys1Aq=V{9WlVh)+Rt`4_W(&n^s?|Mf4upT#$Z`M zof6~M-W+113)dJ?$>v*odsltm#@-MZ5Jp-7YXd#N)a3F=U*f80mlH4s9HaOXAelI; zAf+|zR@c)RYHvzAybS+Sou4c4^S~3T#VTSeDkMxZKt;?XvXWa2{U`UCQ$a4KFAMxi z&el(|Ls`kkIm413EdnLllhBWHLdv{?XAZ@g&;zKwC@wO=o#}ahJX|xgT2I?9^o=jU z&PK6?S4MGF;q`^?z6<=cYnsCUxsFOmY5sF^A>aK3^KLkIg(-wIkJpnR@KQuz=Hi(? z+!(tE_yKoKBXJDVJZa@&m*2$rT%cjjI)givlBnCE`>;5bk^xVb9zA$U+LNEtIgWq} z7n2jz*sl^b$h?n?PdH4A7AZ+DQ1reyoDEwLeBL~nxI>YeHw_xV2?yo_B8Vp@RB zOLBy6VZ5UDRIx%dUe@6X6xjy~pJAeEs!+wUG?9#$dKZDO*#thFgvdo8{`*oMRq0Tp z_YQCotx@428`<8pAN!8YTJ!UX8A}|0aXhT-;hXo7h{xi0IcD~N4f3tmQcZjR zV7}6(@(iitYlJAf9Z1IOaMY3s;0dN65 zR7)EeY8(?K3cy98uLoZ0w%4qdQ7-Q?R^ox&iryPFi+9RbPgeF{)OQ!t*$b%Pe@t~G zdNatvf}<&lY0j1GQPOt4_>|z5Eu~VKOT%To*{{nYcB;*}uJ8$%f1gBeB7t*x?(=b# zGN9|E$!TbQ@FTFYnhGd8qt?lv57BgLMZpHDfzRc}6=i9}D8yXFPPnhLxaaaJtFs2( zoQwRM*kaG1dHI=>^n%Z_XK~Uh#$vC$SFdICBWN{pCWH?U~RJhC}D^Jk~SDjU1LMH6{5dMi?U98}r5D z!Ymb?7sHIG5`kx9{oUoeA{pp4x4`_ppc2RISV8dufz_ z*99+e4tJLelxjwgwEv_WzIr06*v9?Xoy-%bUj#`FV>(Gcs5fAaN5S)BY`$VQh5`i? z0ckz>+$Bw>1d|jbOPn8br@j(oS31CY7LRM9ied?GD5~{JPi`;BKBH2Kc}eZ9JcwPB zbRm9q;vUZ#*R|5PUon1NrP}_X1i`}N$$g?gLvnwIYC_|QGDWYfR$S8Y0-n?r#+9qq<@p|$o$ z>T#y~&dQ5&W9rm4qdIUb>N|mTiw$@D`Ytd=sp3zqZHN60_JC&Uy$50fgYsv;N=e-Y zYME}kUwNOcN~Ty`o>o4#sI?y10`ojsu{%pN+h-Yw#%7g3Tw1fo4^LFT`lb58-$jKs zeasH&%iR_?yd@)x{ZS=foQ3i|o;;Jm%AOICntL$ik`pu^ExXQD>bdoO<-CJi=`)FC zpMoCfnfms?nmd;c9j9>UnLqCV)C?%nZHvpE;;Eg}k+9@DjTA-f3^n0Xd&1l0Np>Bf z)c9D6z(f{$?K*n8$KO|IZfv|aQrz(S!QJ(Y`{47V+KWdTiwGwu|Kdb4HHd`j&xYR} zx%lS+BANRy?FVL)37DoB5_>NYG`Eg0Q*J=;s6 zOJ%#^^3Lbl3wx|z*fUcbXKcUV)8S7*vbZJ!8cgQ&-MeQdMA~x>UYz~B zozrG%PlX-;x;%wZkFs}^fFmim|8IQ)(GwrE1hnu-7r?0EBZ{NmK;-c}XM1-ZS@GTm z_6Xb(`40vHo);yWm@#$L;u|tgtkjh}x(F1IUI3S$pI^AvJfe7!;G(K82-or&^7Aa1c<`I19Qc!Xa;%}A-+L(8HMO6$VbIc}@d0EwjZdZV* z^)lT7ZSgGptGm>2dFk4Q=yuie+9u%O)IlCr&mnMKt4UO!Wr2CT);}b1ZG;JD!Qe8;)lvxwub7u0h4hu+=3)$KRY3y=$j z3JJwSx~$4S>~>x$inG=$zz*H{&hM@O>Z#j(To@<15!z*JR?1!mW-G4(xFp+nVfB|jwHju+v78O9qljLaWY z=Mr5Wjm0c?^#x{MqIzAT*L@3&T8kFH--Mb>Xa!T9Y($fHvL4I5*WvJsm^RkK#N*?p!sOc<}%4;7<~I1g=j^U-hx zKUYV?>V+HsF6Lqmc8FBt&UK$Pq-S57r^?AsJ3u*lw{*;$Zk}DI%EtYPASo)9Li+y4 zLwqXK)+63;MaMDkXC_V#J-bOo`}qS0rIe2ma7Cz`AIr)a%D09xjm#^aFnHaKyT9VuU!cFb23HdTup6|Y-$DVKTi ztB|08o29vUhY|P3(dB2B)Y7&7l3pB;btPm<>7~B5Qg*y*n0hWDJa-n)co}8z(7~gC zx^^@_@sc#Q73fvC#W_B9?N%@3>mY^QSTN^@UxB`OXNlgd;7OyJFI=KrTTs#(9j-=V zJ1q5%C?Pz4XSo^r7cn+PvqS8M)17;|ox^4nu$!ri^(pRG51HP{vxY9;;1)C~z7Uyc z&JAnqi^BZul73BP~SW!^YLk5AcV8u=gK64TKvyFh{7v|GY(xE}9qH;yaczZb|l_Jp3XkWX#n~iMi$q zigU1+w!j1wG5DjVN`dG0TYNo3;B;=S=sJis13!#o5pH@kX(y8dFD z;Z-jf%Ow=b>qI|`b>2aY!C%)|;PQw7-z>@ZRBC_fLZG2v(B;4c9I27^C6s&xlaTRv zex~V9z>4vN$%3d&fzH<(s>tY1*s_WK1;PM*8h z8cQ`|tF8B+oven5J>0`&8(1d!Jnp6adrNQ}`wQ{&Q)qfg<@ zRI;Nvw7=upu>rj81HduLMSfKn`-X>Ub_tc-j46sjn-fZjY6eVQRo9-rbkjfKpg>22 zpHqr&pYza-1Qd!brHauuFV#LTAJn&1|4(mKBToE07r&&kAbz8x{c&Z=X!|Sbzj+Q! zvEs#MP|EYX6-sHsm=Xn8CwOg?n@K;z*yS6ZH}YG@ zBTDlCwg$>c9)3!9H?(aTKCk&~1ifKUG~b^v0LFdR2wq~KZb--W0!^)M=GSlT;y0E9 z0HyKDfDg@l~M}~_oH7JL0;Eft#{Sz4{epKd4Y5D$p1p^}9ov_MN>)9={ zFFj77RooH@3hR&WB606H;Bvl&(4xM+{`*_`ZcR zOz}AHStPDJ(ct_G$k;%!4+UCI-xzs3dZAjmGBhJ7cTzwGZ=diMW80;%-uhreJN;-@ zbXrCS?&dYT-jlE5@C!zd0F+0WZ{&2dGotdq<4V}fbaCw z3hO)Nnnj0#HLmdE}2c%!qxwI7i^7J$j3|9-Zt(+WcPs03~pk>BeTt$m); zKwRt-mD1C-{m86oAg<#8AWnjb*Q9l+W2$h&=_Mo?2e=I8jv=m(TI=*p{TwoId}zrA z6Axc)wcAkibfcLuFy7f-OV9aq`>;$O^3Lgh6#n$fM1>M@5jMRS2~x(x*6P5ZQ=FT|H9WdK!Z~- zektLQu_g^I0v6?`Fz_ue>-ml$V_Si&r&iPf%%DB7SldZP2DGMKyhjEdj<2Z8pkcLE zr{QLdBDv0n0jkd%UGade3Bzm1OzIo>SORH)M8KJg+*a>mM3h}Eh>SDSjz5b1iq^Vp z{Ipr(Wk;j)F_{XZ*+aaGON;B-9^BD)6+i6;_Jaa<~CH-3ln-tCct+0r=>4vi7 z-&ZO_OGn&hRFKM#lBkZwSW%wl8?tX8$zkCpk^`mXSkR(q0?62VKd!T!%X-~H( z$5dwHe&Q<*DC%I8B>uX#Xf{h#aUp;L>G^R@oJ;7@_7p#^Q*G&4|F5bmkB4gQD7p%T1)RXFX$W$i6FO$rUYN*R0heH&^l zz0Y)C>YYE%%$!-y=bYy`zvuUSf8X!-aiq)q;4yPJZoov|2~vsDRd|IzZ7(W6SsB&q z3>Y}G841hYq8umb{Y?-x{u-&g5PxwRn77WOhlp~e9V@GAo zsB2F%j6+LZUw{N-wNrup46JTybYM1PrDg=XMLWOwh2T$$U@j0sO2QOix$so6RM(CL zL>iJjmuC(WFB9E|-z?0e&)R^Qa#HX-<_UEjmkPL<^;=Xf8_gI*hu!09Ifsjzh}m^0 zZ)>|y!d+u3s7X+(ojfFp@1g|8yAlIA4wSywT6W4SH@fwkEkBP6ygzcK6QW!0?zP9V znw{Xlnmve@5gk^9MyZ5MG>dNal&EBun!$r^l}?Z()X`E;3QW0LeN75{l7dsXyL%^qJT{1x&)cE&iCaj6nOQpHo=P%LDeR+XbMso`lTd*)1>0lknWp0HxuBJWS*Mftg_MDi* zKo~yO#EI)%@!NNNA|YyCX(NwycJ8fAQ_U$l|3CM9&{NNSf^5}%2^tUSPU`x?7B@JU z-_cyLs`r_2nC|@v34uq}hNA>_^AbQ!u}15d?+3Q0g7p!u#I^Vn4Z`F2Ovrqzt$6gDdT862MUm$RuJ)sKzzBo4OqHBommi-p}Kd zpjgAhgvL0TYkv=5zGX7pP?kmvJ`>X55={7-6^kMZqKF4VGF%9}+59%+EwMMAXKm+0 zH`4EtCND+4Mg?{1(K;QGB8KM7T2Yj#5S^z+-E}aa%p(T(m!2keM|%n?3d>y@>LGh^ zrHi}qVoy->&{9zuS z(;)X9@|Az9$?^vWIW1pYM9XHYY&N$Obj6SG2SY7PYlxEIS3S*Fp=i-}dWKt00L@m{ zg6t{vkQ`q0&yAkQsc7ty3wU<^s(4tpDns&F(dC2JZOyYZM8j@|t3r-vuuDG%aB+ zA^6?}`4LDHWgQO9FMlp(|B~I<-kRq)YIPe29#7P*S;{D?Dr&{5dplcgEGc*ozhj|0W z(qO5Y+Khvca<><2F`prE$Z>_^hc&%kqe!#bxLK$GImO;}R7?H#`A6LXEXN>a6h9#` z&2i{LCjnW>A!<{P*20xbSC@e~hGgteFP+6<3!7C2OzP%yDmZLFC?S!aaEc#?IE)g4 z&+6~6?pok-7jP-^Xr#{nuucCTsid;acO>~etLz{Mcbyr@_y3BX}&O~+cKmUOXcjoQC%-9Amzs%|BiaRToFE)EY zd_tf;Fn=pe=i9i6?#`vwt>HQC@CL|x7w8`3awcAO1zoc-zv;`wIRC4Q{Jw34e`WtIu11np_dDemzQ+e`k9W5>9x z9|G`-RlX(JkcZnVs)ABJVx!Q8I2q!-OIQ|>^`(~mV?>SS1`V~*PQbA>78J#�{V3 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
XiWqc_0j^(WnmcU<e`fp87-VH3 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 HkuUupe8g4( diff --git a/tests/.coveragerc b/tests/.coveragerc index 1b596114..40958a0f 100644 --- a/tests/.coveragerc +++ b/tests/.coveragerc @@ -6,6 +6,8 @@ branch = True exclude_lines = pragma: no cover def check_crypto_libraries + def _get_password + def _prompt def __str__ if __name__ == .__main__.: diff --git a/tuf/repository_lib.py b/tuf/repository_lib.py index f75dd8f9..c2e7a5a5 100755 --- a/tuf/repository_lib.py +++ b/tuf/repository_lib.py @@ -1753,8 +1753,8 @@ def sign_metadata(metadata_object, keyids, filename): key = tuf.keydb.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. + # Create a new signature list. If 'keyid' is encountered, do not add it + # to the new list. signatures = [] for signature in signable['signatures']: if not keyid == signature['keyid']: From 797bab5ddce755a75aa3a04921e57d419b23a6d7 Mon Sep 17 00:00:00 2001 From: Vladimir Diaz Date: Thu, 5 Jun 2014 11:17:30 -0400 Subject: [PATCH 15/32] Fix Python 2 + 3 JSON consistency issue and re-generate repository data. Explicitly specify the JSON separators for Python 2 + 3 consistency. --- .../client/metadata/current/root.json | Bin 3756 -> 3756 bytes .../client/metadata/current/snapshot.json | Bin 1380 -> 1380 bytes .../client/metadata/current/targets.json | Bin 1936 -> 1936 bytes .../client/metadata/current/targets.json.gz | Bin 0 -> 1202 bytes .../metadata/current/targets/role1.json | Bin 974 -> 974 bytes .../client/metadata/current/timestamp.json | Bin 924 -> 924 bytes .../client/metadata/previous/root.json | Bin 3756 -> 3756 bytes .../client/metadata/previous/snapshot.json | Bin 1380 -> 1380 bytes .../client/metadata/previous/targets.json | Bin 1936 -> 1936 bytes .../client/metadata/previous/targets.json.gz | Bin 1202 -> 1202 bytes .../metadata/previous/targets/role1.json | Bin 974 -> 974 bytes .../client/metadata/previous/timestamp.json | Bin 924 -> 924 bytes tests/repository_data/keystore/delegation_key | 52 +++++++++--------- .../keystore/delegation_key.pub | 14 ++--- tests/repository_data/keystore/root_key | 52 +++++++++--------- tests/repository_data/keystore/root_key.pub | 14 ++--- tests/repository_data/keystore/snapshot_key | 52 +++++++++--------- .../repository_data/keystore/snapshot_key.pub | 14 ++--- tests/repository_data/keystore/targets_key | 52 +++++++++--------- .../repository_data/keystore/targets_key.pub | 14 ++--- tests/repository_data/keystore/timestamp_key | 52 +++++++++--------- .../keystore/timestamp_key.pub | 14 ++--- .../repository/metadata.staged/root.json | Bin 3756 -> 3756 bytes .../repository/metadata.staged/snapshot.json | Bin 1380 -> 1380 bytes .../repository/metadata.staged/targets.json | Bin 1936 -> 1936 bytes .../metadata.staged/targets.json.gz | Bin 1202 -> 1202 bytes .../metadata.staged/targets/role1.json | Bin 974 -> 974 bytes .../repository/metadata.staged/timestamp.json | Bin 924 -> 924 bytes .../repository/metadata/root.json | Bin 3756 -> 3756 bytes .../repository/metadata/snapshot.json | Bin 1380 -> 1380 bytes .../repository/metadata/targets.json | Bin 1936 -> 1936 bytes .../repository/metadata/targets.json.gz | Bin 1202 -> 1202 bytes .../repository/metadata/targets/role1.json | Bin 974 -> 974 bytes .../repository/metadata/timestamp.json | Bin 924 -> 924 bytes tests/test_indefinite_freeze_attack.py | 8 ++- tuf/client/updater.py | 8 +-- tuf/download.py | 6 +- tuf/repository_lib.py | 7 ++- 38 files changed, 183 insertions(+), 176 deletions(-) create mode 100644 tests/repository_data/client/metadata/current/targets.json.gz diff --git a/tests/repository_data/client/metadata/current/root.json b/tests/repository_data/client/metadata/current/root.json index 9174375e06a09dee6d59f428d3f2fbb7d386ed54..b1b34fa314f9ea032a9ac600bf4a5ff167642ce4 100644 GIT binary patch literal 3756 zcmd6qS+lB0635@~r#Nw5oepcGs^~XF+{ImwjuRa~6$%0(tJ2Zmy-Ux%9X%5hGu?9^ zdJJ4B%Bp{6{>V&z{_^&o2dBg-%1q|(Kfe9_%iG(}>izb9k&i&Ak$1_Mkkr*A_ZUNj zU_f1@>k=Sc0}wIC1(^AQ69hQc)R)hZuTjsVSW-yY``=#c#j-eGUkff*#%g8!Fkh{{ zUQ!C48&T;3M3623!BT_7bp(VOlhng5;ywo&cA4&~Wd%W!awwG+U)P0)T%Q0(x)68_ zQ{d^Y<`U_+9?=-le2=-FWC(I1kOq7RknmjJ)pX!+h;#sfBmf}-Jzqz_LB9K(z>u0L zT#c#efKw=>=PMOR0z%X->QBC>N#a3>1i>0`T&ZInNC)TwGnaYX;hvHpG$NT2=W^Z0 zQgT9x=VRTK3?mEyKmZWNl_^lUkrD=!{lJ4*_{ymsg+4`?Bcfy>A=J7=gVgmsU%6Il zp47OGA){&|;_K9bp65U&iE@}Hh4QD6${Rj@4kKUr)>Yo6j7t?#j4>5RnNJ9$Ncb98 z4i_8(M-x5(!WXWxNCjCFNFok(M;QhvV+3ewLnVs=$tb~07X(mGg@$04Gl&rs5c@=6 zN*UH%h?FyQs7diOR65Aj6(vMLgpsC6<|=!1@vb=e<;&YY6fZ9*<&%~tjPE}ck5oPp zSzfQj3z~QDn&d%V$fWowPn8`mO-JXJX_Xoak;fRdJi7TUi-0P*A zrn%OD4X#zDksx=KLhzROj{P`>I_yJC%xxZd`$E(+sAw6Z0WMMmfB1n z%5=~MgU7V4SXeqO$8PNVy-VBoy?Z9(6okEtgpJk7uXg5f<;i;3H5Nwan7o2x+1-DQ z*OTEF#p>fT?|par{L1GGpuYyI`)!HI-f7w8nY5ZH4CX_2Oajs0)a#rYleit%ozdv9 z+{VL2xb2R`nCkvLK5ORvqZBk6t(~;&mWA1yRkLv)8jdxX9+N|xx;{h)w)vgow$aWAL?&Ra67q<4vvsj9Cda+?yjfHxFaGl z95(&Kf=w#G`yGDYJ}_ZuL6d}wbp-ki-jgz%-dyh#b%?_Dm_+{zRuXWBae=TZK-3dJ z*9lcg%ZDn-Ll-!{PCUej04fx~^9A9$s?!)sB$+CZzvZE4q5o^JI-B^CU(JnmvquNN z8sh9UQ~@b3$vVQ_ML6RFQX4wo!-KzDnf;}C>9R(z%_3QPbC9*WV%fbm%`gGAHlOVu zhoW_hj)^nxh~QN3wuhW#9fmi29-iBWMSHPAx8$^l>^Qxow3nRqUK8II@h}97eSI+L zdR}(g=$MFYp16-qw`diU0qL1G<-C|xTirL-Gla5ct$i4>YiskQMsiF0F6?UWUQ*vs2^GK%s^@>uof+G!Oep$egEb8|gvTI=)58TO)r z&}C{DmoU~28@>9Dz%dXMu3FEWb%ASqf?<-p%zCC_CA3naj zXuituvy*(TR-9EQc)mu@77ad!zLw_SHI6S;Umtq@Nyzu6**_$taI%vuerrYljgIeb nPXCyWAeMRI#OZ$s&39*r|Dr@){sg9ySsq-I&zSx4<(FRp)5AQF literal 3756 zcmd5RP29{Y#4_;^XN(w< za8Fu*5U`1a){~xLN@9(frwLCmkU|(nC6w5c2w{Su(64{{w0AJ&CjT@TO&XI)2? z@@>g^@Z5+fA{0hMQH_Pikp_}5>0w0?Q-UW%VP!Cs8d)hc2PC0_0%<($aRL*9EXTlO zo~O8mfK%qYR00Z2s5S{k6cEn}%o9YoGSXV4oUfQL_gDeBq5^s}@jOpTsv(4oQY{iH zl>m}jPXM48NMItc&oQLrL@c8~C)}OdL54`_ z!mkM9L@VG~KrIJMxuYoQk^+&zuMQ`_e0lkY!^2ItlSsoiMLmCKc-(H{R#mBKLF3YQviPad{#fmv4;o|Mc&-N@JvVMOZhaLE?%Q%p(J8vM8!^w#I-|b2 zfnsx=|i@wXU*Z6)>xo_ua33oUyEx^fnxv8Rcc39X- zgtR0Q4v3N;^M3gzW`p{5z{ainB(#W_x@b(Y-1 zW@FrNwmy^78aDqmUr!8wqS*a>fqaKDJkpyz<>X1=QJnbqMC$6562MJ}=b8NyL z;n)OOfuvB(7%5Sregn*tk^euistz5GA5z__g@;Kq80X^+T4!2KJ6EvHJUQ*1s29P` z^4xw~T}5<0Q*zIu{%kp-%cG=`UyY;L?RLT?zSbu*6)_w(*IBL4^G>ot{n6PD@5po) zWjoEP>sZUG!~q%KPTRx%W6GPXFWR-~d$Ya4P!CnS|G13P`Z}}CbFG{Nptfp7i`#M8 z+*O@<_wWv8sC4{k5qBFgO1V-J?90&K<&AFHjKp9#suqnx?%24BcHugERNIwpcHS2iN{3M= zmi_o~tKs7k#D_;U7?OtXy9)BAAwPcfPt(J&Ke>08w0~GU!t|cF;u!yVSP6ugbe+bb z$MuaT`wC8#>pTWys-@D7lRc6c$J8Q0)H1DvBt%=!HEUzgUjSD3x4dk(vm)AGYNv(V zD_D+QLW*oy7GZtgyfRU^l9bzFdruc0+|*7;7M%{nT%LVpl+9Bj%I=}rAM8=9`{=i( z;ccBw-#&Wx$GLaV!Rowktw!-82o}|B;S@wX#Jy|OocqHr2KOC`ucP*S>m@l2+PzLK zu7#(}ac;C2p=y4gY{XE)1)PfRy*C%D<;kS-{&qaN(Qr}=?lXPBwY)w$l(x&4@*~=) zIP9I)*%(IN;aK>Bgtt9f*~xMf1L)QEx9nr~IN|70jwh!a-#hRscgH637gz{!UHjUC{Me;4Q_aF^n$@Q(dPwQJa%- zTAF2Voh|+cwsLZaTEabL0f24<2y|-dsQ|dsc@jG#nH(yQx8=6?NbPpNmp6T9WaoVUdBgZBw$4u>_j|#VCcj=ko|ljB z)?%M+_^BkH*2;}jf}4E*)RW$G>RW66opAj0>f5E~_l|sbJ^Ud@N_qY;B}rQuEz<=D&F2mcIh#YR*NPXJ2#n%a>pN2@5MgEdT%j diff --git a/tests/repository_data/client/metadata/current/snapshot.json b/tests/repository_data/client/metadata/current/snapshot.json index b4ad9096b92f7fce68872ee44a9f4bbe2133b4b2..017cb34e3f9804c3db056525204f5d1f78e395be 100644 GIT binary patch literal 1380 zcma)+!Hyd@42JLf6vmvJloTbA%q?$F)IGH*3QDBbo2Ki<+AW#}dH2$@>8V&Cz<3Un z=I75}pZxjmuw5SRAMN_*b6mD}hhOdvhtK&tY`@2+2hYkD0b(p(Xd?FMpjvx#S-8}3 zfGw@FaCPXqU@otsKuj1Vc*>4;{&;+UJidSP!}0iaDIMGzNznn!9tBD* z9!+|ItV{-rYC3|bGeuA(jgTl!rq@Aiu=-dWldG_3^yRCU>0JN`FvW;TDUQ0B!>wS|+BAGf$Vi8y0=8UQ+$N?b(o&nfECR2!)U~EJ z09G6p02Rt=ow$KbU8_z~ETuykHVRlb@D^MxXC zU?%|~Mo#Z4g_(M0hize)$y7n<%c_@REd?`SpzbgRRZy(Aaa;m2L~AUibqPvtI$L&P zh1q7-b<}Wi;iWy>wkKz3OYu@((l3(!*;&Ed$7x&PxHLJJN$BUIDF3cu>C$i-AAr$hMiWI;tQbWAN1-nhKq>d zFf`W~5xf9fSa-xa#w>%W7DUGFyFK0?^Kv}x0e`%Ge|!7>%@4P?U$;`hH4)bq(ilZU zQEd&70u(j8b&zOSno%s3=GwJ$r7F!zkeDp)(gkODC=Xr}3_=XNVFW2xi4@Zu7qG+R zfezle!;DHehgKa>TUJ1&%tE7q57I1>2w6>g0|cvPJb*FWKw2$JX_}ij&DNunMMYY5 z%~rs&2G3JgF})eutk=3Sv}9&ka|sujrOoAmO$Tu&W+~iwP#@iAnpa~^S8Z!lsEbxx zM4CR7A*(f+S^^Z)9&KextSEVtBje^_=c!SK6((w3p?h^$E#0Z0V~tW{Il3cdTTO*I z7HAQG2w=$O#$a`=rQ{No^g>z&vr=bRaNh5fwS$R_RuwXOeyvl}wJIZE;7YkkOHCeT zYDy)%44cl-16^P%LmsPUef{^1z8X^@fH9;cKx!LNNrUw8`jOJ~QJ~*X|c_X4RqP!rh zXnRT4+q-{WW!Um=-V2KN(7AG;XmSGHbmq7PtgEkS~w~YgmGqW7)?n5faKw~p3McgoEN?Qphb@eqEF^{RTKyzQqc09i|?LXaA X#$8)}_J(eM&8PGIPFT4~p1=GHZOLY= diff --git a/tests/repository_data/client/metadata/current/targets.json b/tests/repository_data/client/metadata/current/targets.json index e9da47bb59db2882c5dffd99a8ddc120e14b5fad..04760cd24cea01bc6c405203496bdbeadeaddd96 100644 GIT binary patch delta 1093 zcmbu7OLF2j5XLKysVwIRv&kmK0;%(3PDWTk&XJ?yQFgItVH;Rs zO6Y&Y!0G*u?}xu;$IIcrbYhWoF%KXzC|3X`nchGB z@cU6Wh~oA|b+@_JJ$zoCt$S`(eOc^#;<9$PQm4Svxg8$1r|rc`)^FPVg^{i6QD^*R zGw$Dh-OHyt!OxB5;h_#X9wy_7IP7Lsv-h|h-JeX&Q*&WElj&ssdTr z6$YLD;rx1ayJ|HX)y3rD{`u^MaCm#3rro>4)o#(6cYD{fYPf&@=|lLxX8rxwhkw5N E7vwrajsO4v delta 1093 zcmbu7yHaCE5QfW_ErYB9BP`YMD|rNjqyeiI9{s!E=a6pkty; zE=rRru=84n42mEn`B(h;;DH^Khy*l*=#u6Tyha-jpun%Pj~-XQ2#YCs<(CK_QcqhE+t&R@noh1u&%!kf+kzS(e&@ zV~-rA%9OA`0D~zsiGowY1?P~o0IefiN5V(4P9TxUWFSk@I@5dmoHL0M^d>@9UKB>L*+;d+6ze# zh$9zmtT8JM3QR22h|+{2-Da|iizt>DNV8HwWRRGhV=UMjN(af>sv@`Wls&tgvd&RE z^4|*z^!Des^)Ds{_uinW)aZ(A=w4Eel%zIu>n%kDmp_n(KS z&%N<)*S#J#cSoxIQvL3`O}mV%K{ab>xSzb7RX$AaW52Fk%kyU8uJ^-g+Mr%9Tst?X zPt_rJx;JA|^CKRejLbeS2dmpA*Vv!6%Y_*Chey59ML(@C&TQ}Q?s_}yZ%(vW*s$2e zU3|FQY**{S_IR;+zKg@li<7!dwAu7JslT7KliuaVxTo8x-|?<_c`@AVQhzLu^QxMt zn`vX>wpTw@_T_jGGOaq@Fy8LYZ_;u(s2(0~N3-@*BTiNQIB1^Ja(8jxbVg^>{@hKM zU++fKto{8<8b_+lWPP-@?ZekFzlW3Qr`oyh`F-W}tZlB}e)z!ucUk}Z{o&tl{sWR~ BL{9(! diff --git a/tests/repository_data/client/metadata/current/targets.json.gz b/tests/repository_data/client/metadata/current/targets.json.gz new file mode 100644 index 0000000000000000000000000000000000000000..9e6ad7d2a323694978a84d59aab57ecc347ce781 GIT binary patch literal 1202 zcmV;j1Wo%NiwFSnd5}{A|E*QqZW>7tefL)&UP+d|U-C9!u!+GKgKf+@T2Qt3N>OXX>No&=1P zjS>!|m)2REk_?Au7K2qm0n@=6LInjEKzPlG0%EzNl5%RLBqEeQ9Ps9GlP?Z{*Zr*D z&puxD`)^B_!9LEJl)`yOjV53a(-Lot7Rd(3lH`!Ap&_y;Bcp^{CKR=XNzh&p3IQ)0 z9f?tMm9&zUC#H#Enk5rtK+Hr5Gbv(boFE?X5K?5!TD;>vs$`Xo!gB6{-Pi0nSX^P8 z6BiPfD)`8h4Tj+<)*B&%6p(13xBxUrr)cC@I&(HkPpk`yC1R{}3_OCmCL}Tr_=^-I0r4T4NfZICK?4$$w8v6{ za)Fx|jWsUBGK%5#r1a&(n<(R6EqgJR|Jpn+<6a^ISdHUmeb7xg#917}X0j;OZyOJ) zeSavGb<)9Wo7A32Vy))DQBX_sTDWZshM1eA3%m>Cbk`ZZqM>+uiNMf~y zqi~PmEzExIE0vdzZZ`4zeINIqTH~yBS-QHZwOjSlMdSVud?-4tR;~3lyR40;-}33? z%s!D?)@wAgtXt1|Cfl#q#}|0t$V3L+F2d(GSJ9=~IVXRPN^djD=CI z`n%OxV>+kR>qd7fY5i)@y#2Pk?cS`P=;n@u$85G6SDEm;;q8#@whu+EGnozUPe*c7 zo2uq;G|V3_nQ2`&Uf6SIVNm+T`8iMZ>(dujjkl9_ZPYavo!h2bGcL&OL$MiLcANR! z?5e83v8r^;)k|Z%xV&mAoSU!t%@ge|W{r6-6S-qsquW)Z`pvT%Wc10LM(jg{y_|iX zPvy!_+r4bm+;$qPkul8N<##5lHK!M%2RENjS4FF`AS5nDh4vmzecNq4RhEN?V&?A0 z+iC@dz3#BLoX#&Mv*n<^crIM8+1<6S<~Q?tEh|0`$M=ut&tUpDtvG7m?Jl>|dfx6_ zJrwzM3Yp$p6^vHopUHa`gRtG24$C{aN0ZleZGfakrctUkznO zIQd8@UIRit9j?)F65rBzpTfg=^l;w$EatGzpW&cJtVUA77?^+ij}M?zOn{vu1UE?FB8d7y(IAdl zK<4&{Ijp>6Vr18%M|b=dFE45sXDBp9<}m`jrQr=my(j{!!OeJQWE4F*>{P{vFYw*1 z2~nY32b;(X20nQ*IHe>-@tzQbb&nSgvHJjO1#)U`cELHC_8Twf4W*q2oF;6~fo08l z-(p8BFTPjLoln;x*=@-~RY$9+=suYfK5fP>i%rK_R7LXA&O2$SXg3qH00^qRbF)LP z9=U)|i2HiIF76A$FdN`}KgZ^EWVn6`z(3$Po^kcjCSSv6t>-=hnXba)s(3WE9L6m> z`DNC@>1*RcTgj&a_hQoAwV*4Bui>^Qlk8Mxa6o}-_;pkDq>Em`Xg|cM|NHy8kN)w3>zBQlbb6RaEKtmc6fh9W@=P@yvIUpf~pz77v$JfU{fBuqJ z9`#hsFS-b6fI!`+z_0bCpo*aobM5Li*YhA7Q&>3veSdwDWSU&ogM9deA|c>Wb`zb1 zHf3R!WzZQzKQiJ%2E0E_AI^C`fE zR{_zTI_$Eo#43OuCgFg=a#DA^qFz%D?N%F5@D0Cu!&(n_G0V+G*NRQ5u#ptXb)c=~ zL}TmaTchGPVY1AkMwX$l?oBDXV>TSZh*R`#33s>S9?j#qut~uV_2&1J57mEv|MC0B E|BkPqPyhe` diff --git a/tests/repository_data/client/metadata/current/timestamp.json b/tests/repository_data/client/metadata/current/timestamp.json index e94a2a893ac7bb3def5a3ecd3a51de5f281afa0e..73cd3b4c18f1b2bcbb01645f57651a25410bc321 100644 GIT binary patch literal 924 zcmXw2ORgL@4BWp{40;_Kq$qymoo^6iyh;!Rltg*P&ilYK31S$&d)WPKKn--0B3Z1e z{`PR%Zr6`be*fo7+_rb8KORn}Z~2_I&++v-GqSDGAPl!`x@m(IHb9_6 zbSl=R6zFw2TN%ACG({e3VqL2=1!kvG$=Wd1s>%)~y4DJcHpqp*lvE?ItQuJ_5-_!L zb+aCl_0~!@Efd>l#4(}Fg*av(9<^ZMmEXkZu@_Z^mX$J>pjy#|m1Qjv;DC{DtRM{VDD9=NM)#0D^Ug4$T0Ey=CrMyqB#t6Nw78j= zEKUW12#uSth4-Y?2ja{PQJiNi?l5|AO5bNI1C`jU;sUWglp+}lYqNJj)Hb7uwlPO~ zq^63Fv?!NZv(_zr^8MlTXZqzZW$bR*&A9z_|N0WUMLt{~<97GQm%}+*{P%Kw_3f4f z!W+Om9{}DR=l9o3X*~bYDEmJ7%kA=ffBWh7{PZ(=+Ae;(9R5GNpA&BvFD(1W)uuxS z?>>plDFs^tU3T!(#y~doo^FD4R7?3rBn RHM#v8UvAd}6h375`~O&Q_Kg4l literal 924 zcmXw2Nv_;R4Bh`zG_;NlvWioA=NklRuMz|SS%bUn`~mJx5X11@L+RUq1PG+`rh5AJ zaN2Iyk57L8=gYip?@oU_oKD~JK5d`p*Xzj1R@j@`?sy2e~i zlbS9Z5@VT{*acPLDQ$GkXDXFe60f?rgJX*1j+0j*FO!{|Z5VUw3r3Mv)JA6wPng!y z4~|6ThNi1DrRK`OTAPlts#C)-BvP9+(L-zjVhI_t0yB~sjVTz3bxeUK)x?NFX@+91 zCk>(ewh0I|V)Ia=St$jZX@&5G0wL;Yvem|JMPkoW3k6JLL!gnIeK(;*2=2k)YuFMf z?h;lNRU`AO;LVyUBT{Mv>M4c?GId3CHw~3$Lx5XM4q^#*QQ$rxw0m0>g;TV)=xq>Y z{1HAAu8z{13&3hZ#SJ=yG0aqNTq?7+6=YTl#_SnvGBJ%>xel~0*rq9G!k8LpG@jwn zLx*nJlkX3wKeI1KDd*mny^Pym_pdK=ugHh%!2P~i<=ULOG7 z9oO%#FJ3@~8>iMWc%}Kwj7KI|0|cSxDKJ>uJG#3{U&l{y+WwwTAMcmM#cHr`((vnY`*(i1 MT@O+CkmK+F0~zn~cmMzZ diff --git a/tests/repository_data/client/metadata/previous/root.json b/tests/repository_data/client/metadata/previous/root.json index 9174375e06a09dee6d59f428d3f2fbb7d386ed54..b1b34fa314f9ea032a9ac600bf4a5ff167642ce4 100644 GIT binary patch literal 3756 zcmd6qS+lB0635@~r#Nw5oepcGs^~XF+{ImwjuRa~6$%0(tJ2Zmy-Ux%9X%5hGu?9^ zdJJ4B%Bp{6{>V&z{_^&o2dBg-%1q|(Kfe9_%iG(}>izb9k&i&Ak$1_Mkkr*A_ZUNj zU_f1@>k=Sc0}wIC1(^AQ69hQc)R)hZuTjsVSW-yY``=#c#j-eGUkff*#%g8!Fkh{{ zUQ!C48&T;3M3623!BT_7bp(VOlhng5;ywo&cA4&~Wd%W!awwG+U)P0)T%Q0(x)68_ zQ{d^Y<`U_+9?=-le2=-FWC(I1kOq7RknmjJ)pX!+h;#sfBmf}-Jzqz_LB9K(z>u0L zT#c#efKw=>=PMOR0z%X->QBC>N#a3>1i>0`T&ZInNC)TwGnaYX;hvHpG$NT2=W^Z0 zQgT9x=VRTK3?mEyKmZWNl_^lUkrD=!{lJ4*_{ymsg+4`?Bcfy>A=J7=gVgmsU%6Il zp47OGA){&|;_K9bp65U&iE@}Hh4QD6${Rj@4kKUr)>Yo6j7t?#j4>5RnNJ9$Ncb98 z4i_8(M-x5(!WXWxNCjCFNFok(M;QhvV+3ewLnVs=$tb~07X(mGg@$04Gl&rs5c@=6 zN*UH%h?FyQs7diOR65Aj6(vMLgpsC6<|=!1@vb=e<;&YY6fZ9*<&%~tjPE}ck5oPp zSzfQj3z~QDn&d%V$fWowPn8`mO-JXJX_Xoak;fRdJi7TUi-0P*A zrn%OD4X#zDksx=KLhzROj{P`>I_yJC%xxZd`$E(+sAw6Z0WMMmfB1n z%5=~MgU7V4SXeqO$8PNVy-VBoy?Z9(6okEtgpJk7uXg5f<;i;3H5Nwan7o2x+1-DQ z*OTEF#p>fT?|par{L1GGpuYyI`)!HI-f7w8nY5ZH4CX_2Oajs0)a#rYleit%ozdv9 z+{VL2xb2R`nCkvLK5ORvqZBk6t(~;&mWA1yRkLv)8jdxX9+N|xx;{h)w)vgow$aWAL?&Ra67q<4vvsj9Cda+?yjfHxFaGl z95(&Kf=w#G`yGDYJ}_ZuL6d}wbp-ki-jgz%-dyh#b%?_Dm_+{zRuXWBae=TZK-3dJ z*9lcg%ZDn-Ll-!{PCUej04fx~^9A9$s?!)sB$+CZzvZE4q5o^JI-B^CU(JnmvquNN z8sh9UQ~@b3$vVQ_ML6RFQX4wo!-KzDnf;}C>9R(z%_3QPbC9*WV%fbm%`gGAHlOVu zhoW_hj)^nxh~QN3wuhW#9fmi29-iBWMSHPAx8$^l>^Qxow3nRqUK8II@h}97eSI+L zdR}(g=$MFYp16-qw`diU0qL1G<-C|xTirL-Gla5ct$i4>YiskQMsiF0F6?UWUQ*vs2^GK%s^@>uof+G!Oep$egEb8|gvTI=)58TO)r z&}C{DmoU~28@>9Dz%dXMu3FEWb%ASqf?<-p%zCC_CA3naj zXuituvy*(TR-9EQc)mu@77ad!zLw_SHI6S;Umtq@Nyzu6**_$taI%vuerrYljgIeb nPXCyWAeMRI#OZ$s&39*r|Dr@){sg9ySsq-I&zSx4<(FRp)5AQF literal 3756 zcmd5RP29{Y#4_;^XN(w< za8Fu*5U`1a){~xLN@9(frwLCmkU|(nC6w5c2w{Su(64{{w0AJ&CjT@TO&XI)2? z@@>g^@Z5+fA{0hMQH_Pikp_}5>0w0?Q-UW%VP!Cs8d)hc2PC0_0%<($aRL*9EXTlO zo~O8mfK%qYR00Z2s5S{k6cEn}%o9YoGSXV4oUfQL_gDeBq5^s}@jOpTsv(4oQY{iH zl>m}jPXM48NMItc&oQLrL@c8~C)}OdL54`_ z!mkM9L@VG~KrIJMxuYoQk^+&zuMQ`_e0lkY!^2ItlSsoiMLmCKc-(H{R#mBKLF3YQviPad{#fmv4;o|Mc&-N@JvVMOZhaLE?%Q%p(J8vM8!^w#I-|b2 zfnsx=|i@wXU*Z6)>xo_ua33oUyEx^fnxv8Rcc39X- zgtR0Q4v3N;^M3gzW`p{5z{ainB(#W_x@b(Y-1 zW@FrNwmy^78aDqmUr!8wqS*a>fqaKDJkpyz<>X1=QJnbqMC$6562MJ}=b8NyL z;n)OOfuvB(7%5Sregn*tk^euistz5GA5z__g@;Kq80X^+T4!2KJ6EvHJUQ*1s29P` z^4xw~T}5<0Q*zIu{%kp-%cG=`UyY;L?RLT?zSbu*6)_w(*IBL4^G>ot{n6PD@5po) zWjoEP>sZUG!~q%KPTRx%W6GPXFWR-~d$Ya4P!CnS|G13P`Z}}CbFG{Nptfp7i`#M8 z+*O@<_wWv8sC4{k5qBFgO1V-J?90&K<&AFHjKp9#suqnx?%24BcHugERNIwpcHS2iN{3M= zmi_o~tKs7k#D_;U7?OtXy9)BAAwPcfPt(J&Ke>08w0~GU!t|cF;u!yVSP6ugbe+bb z$MuaT`wC8#>pTWys-@D7lRc6c$J8Q0)H1DvBt%=!HEUzgUjSD3x4dk(vm)AGYNv(V zD_D+QLW*oy7GZtgyfRU^l9bzFdruc0+|*7;7M%{nT%LVpl+9Bj%I=}rAM8=9`{=i( z;ccBw-#&Wx$GLaV!Rowktw!-82o}|B;S@wX#Jy|OocqHr2KOC`ucP*S>m@l2+PzLK zu7#(}ac;C2p=y4gY{XE)1)PfRy*C%D<;kS-{&qaN(Qr}=?lXPBwY)w$l(x&4@*~=) zIP9I)*%(IN;aK>Bgtt9f*~xMf1L)QEx9nr~IN|70jwh!a-#hRscgH637gz{!UHjUC{Me;4Q_aF^n$@Q(dPwQJa%- zTAF2Voh|+cwsLZaTEabL0f24<2y|-dsQ|dsc@jG#nH(yQx8=6?NbPpNmp6T9WaoVUdBgZBw$4u>_j|#VCcj=ko|ljB z)?%M+_^BkH*2;}jf}4E*)RW$G>RW66opAj0>f5E~_l|sbJ^Ud@N_qY;B}rQuEz<=D&F2mcIh#YR*NPXJ2#n%a>pN2@5MgEdT%j diff --git a/tests/repository_data/client/metadata/previous/snapshot.json b/tests/repository_data/client/metadata/previous/snapshot.json index b4ad9096b92f7fce68872ee44a9f4bbe2133b4b2..017cb34e3f9804c3db056525204f5d1f78e395be 100644 GIT binary patch literal 1380 zcma)+!Hyd@42JLf6vmvJloTbA%q?$F)IGH*3QDBbo2Ki<+AW#}dH2$@>8V&Cz<3Un z=I75}pZxjmuw5SRAMN_*b6mD}hhOdvhtK&tY`@2+2hYkD0b(p(Xd?FMpjvx#S-8}3 zfGw@FaCPXqU@otsKuj1Vc*>4;{&;+UJidSP!}0iaDIMGzNznn!9tBD* z9!+|ItV{-rYC3|bGeuA(jgTl!rq@Aiu=-dWldG_3^yRCU>0JN`FvW;TDUQ0B!>wS|+BAGf$Vi8y0=8UQ+$N?b(o&nfECR2!)U~EJ z09G6p02Rt=ow$KbU8_z~ETuykHVRlb@D^MxXC zU?%|~Mo#Z4g_(M0hize)$y7n<%c_@REd?`SpzbgRRZy(Aaa;m2L~AUibqPvtI$L&P zh1q7-b<}Wi;iWy>wkKz3OYu@((l3(!*;&Ed$7x&PxHLJJN$BUIDF3cu>C$i-AAr$hMiWI;tQbWAN1-nhKq>d zFf`W~5xf9fSa-xa#w>%W7DUGFyFK0?^Kv}x0e`%Ge|!7>%@4P?U$;`hH4)bq(ilZU zQEd&70u(j8b&zOSno%s3=GwJ$r7F!zkeDp)(gkODC=Xr}3_=XNVFW2xi4@Zu7qG+R zfezle!;DHehgKa>TUJ1&%tE7q57I1>2w6>g0|cvPJb*FWKw2$JX_}ij&DNunMMYY5 z%~rs&2G3JgF})eutk=3Sv}9&ka|sujrOoAmO$Tu&W+~iwP#@iAnpa~^S8Z!lsEbxx zM4CR7A*(f+S^^Z)9&KextSEVtBje^_=c!SK6((w3p?h^$E#0Z0V~tW{Il3cdTTO*I z7HAQG2w=$O#$a`=rQ{No^g>z&vr=bRaNh5fwS$R_RuwXOeyvl}wJIZE;7YkkOHCeT zYDy)%44cl-16^P%LmsPUef{^1z8X^@fH9;cKx!LNNrUw8`jOJ~QJ~*X|c_X4RqP!rh zXnRT4+q-{WW!Um=-V2KN(7AG;XmSGHbmq7PtgEkS~w~YgmGqW7)?n5faKw~p3McgoEN?Qphb@eqEF^{RTKyzQqc09i|?LXaA X#$8)}_J(eM&8PGIPFT4~p1=GHZOLY= diff --git a/tests/repository_data/client/metadata/previous/targets.json b/tests/repository_data/client/metadata/previous/targets.json index e9da47bb59db2882c5dffd99a8ddc120e14b5fad..04760cd24cea01bc6c405203496bdbeadeaddd96 100644 GIT binary patch delta 1093 zcmbu7OLF2j5XLKysVwIRv&kmK0;%(3PDWTk&XJ?yQFgItVH;Rs zO6Y&Y!0G*u?}xu;$IIcrbYhWoF%KXzC|3X`nchGB z@cU6Wh~oA|b+@_JJ$zoCt$S`(eOc^#;<9$PQm4Svxg8$1r|rc`)^FPVg^{i6QD^*R zGw$Dh-OHyt!OxB5;h_#X9wy_7IP7Lsv-h|h-JeX&Q*&WElj&ssdTr z6$YLD;rx1ayJ|HX)y3rD{`u^MaCm#3rro>4)o#(6cYD{fYPf&@=|lLxX8rxwhkw5N E7vwrajsO4v delta 1093 zcmbu7yHaCE5QfW_ErYB9BP`YMD|rNjqyeiI9{s!E=a6pkty; zE=rRru=84n42mEn`B(h;;DH^Khy*l*=#u6Tyha-jpun%Pj~-XQ2#YCs<(CK_QcqhE+t&R@noh1u&%!kf+kzS(e&@ zV~-rA%9OA`0D~zsiGowY1?P~o0IefiN5V(4P9TxUWFSk@I@5dmoHL0M^d>@9UKB>L*+;d+6ze# zh$9zmtT8JM3QR22h|+{2-Da|iizt>DNV8HwWRRGhV=UMjN(af>sv@`Wls&tgvd&RE z^4|*z^!Des^)Ds{_uinW)aZ(A=w4Eel%zIu>n%kDmp_n(KS z&%N<)*S#J#cSoxIQvL3`O}mV%K{ab>xSzb7RX$AaW52Fk%kyU8uJ^-g+Mr%9Tst?X zPt_rJx;JA|^CKRejLbeS2dmpA*Vv!6%Y_*Chey59ML(@C&TQ}Q?s_}yZ%(vW*s$2e zU3|FQY**{S_IR;+zKg@li<7!dwAu7JslT7KliuaVxTo8x-|?<_c`@AVQhzLu^QxMt zn`vX>wpTw@_T_jGGOaq@Fy8LYZ_;u(s2(0~N3-@*BTiNQIB1^Ja(8jxbVg^>{@hKM zU++fKto{8<8b_+lWPP-@?ZekFzlW3Qr`oyh`F-W}tZlB}e)z!ucUk}Z{o&tl{sWR~ BL{9(! diff --git a/tests/repository_data/client/metadata/previous/targets.json.gz b/tests/repository_data/client/metadata/previous/targets.json.gz index e2d2dbe0e05d5ba37e3a7d7ee9a5643195d7e4cc..9e6ad7d2a323694978a84d59aab57ecc347ce781 100644 GIT binary patch literal 1202 zcmV;j1Wo%NiwFSnd5}{A|E*QqZW>7tefL)&UP+d|U-C9!u!+GKgKf+@T2Qt3N>OXX>No&=1P zjS>!|m)2REk_?Au7K2qm0n@=6LInjEKzPlG0%EzNl5%RLBqEeQ9Ps9GlP?Z{*Zr*D z&puxD`)^B_!9LEJl)`yOjV53a(-Lot7Rd(3lH`!Ap&_y;Bcp^{CKR=XNzh&p3IQ)0 z9f?tMm9&zUC#H#Enk5rtK+Hr5Gbv(boFE?X5K?5!TD;>vs$`Xo!gB6{-Pi0nSX^P8 z6BiPfD)`8h4Tj+<)*B&%6p(13xBxUrr)cC@I&(HkPpk`yC1R{}3_OCmCL}Tr_=^-I0r4T4NfZICK?4$$w8v6{ za)Fx|jWsUBGK%5#r1a&(n<(R6EqgJR|Jpn+<6a^ISdHUmeb7xg#917}X0j;OZyOJ) zeSavGb<)9Wo7A32Vy))DQBX_sTDWZshM1eA3%m>Cbk`ZZqM>+uiNMf~y zqi~PmEzExIE0vdzZZ`4zeINIqTH~yBS-QHZwOjSlMdSVud?-4tR;~3lyR40;-}33? z%s!D?)@wAgtXt1|Cfl#q#}|0t$V3L+F2d(GSJ9=~IVXRPN^djD=CI z`n%OxV>+kR>qd7fY5i)@y#2Pk?cS`P=;n@u$85G6SDEm;;q8#@whu+EGnozUPe*c7 zo2uq;G|V3_nQ2`&Uf6SIVNm+T`8iMZ>(dujjkl9_ZPYavo!h2bGcL&OL$MiLcANR! z?5e83v8r^;)k|Z%xV&mAoSU!t%@ge|W{r6-6S-qsquW)Z`pvT%Wc10LM(jg{y_|iX zPvy!_+r4bm+;$qPkul8N<##5lHK!M%2RENjS4FF`AS5nDh4vmzecNq4RhEN?V&?A0 z+iC@dz3#BLoX#&Mv*n<^crIM8+1<6S<~Q?tEh|0`$M=ut&tUpDtvG7m?Jl>|dfx6_ zJrwzM3Yp$p6^vHopUHa`gRtG24$C{aN0ZleZGfakrctUkznO zIQd8@UIRit9j?)F65rBzpTfg=^l;w$EatGzPmY)Ye=9n~0k* zhLrz(Ti}pvsC;Ew&nngRbex-FvkL3#r^=samCARxE7j+)n|fHO zYRaS`0b#;v2wnuPm9PQ1z?g8xI!`3#KFBDwry@#cwc*@F%p?whRzDo_eGA28en|Kw zOR_Ba*vYc@EkIDN8*DX(JQNcl3DJRD=L`!WI1?k5*b*Eh;l{;)9ivWI9*HGUVx~m| z2pW0CD8-SuARKi@0AOk0%^^#)!jQ;nWD$wnF=C~r!ZJ@tR7??~FcttAOCzB=tt{mV zYio%0ltmc`6pAetP;rn>Go+o?L~?{V_f~0%jF(CyA|;Aa;7BFsQI_Cij1obh5gtco zxX|8UBt7Ob3JHuNA)-abI5^)zETLEjOC$sQL}03!cLEq^#&}OL4F+K=h$PM`AOZ!? zG?2!awUGsawNnA=R? z5UaI03ir7DVrD<%D%Hit&Ze%6`&j;J>2>3-(z&mFZd_Jw)8S!pl(!m9ke}M>FMSTfg;Bx2w%!+1^~t7O!L9xxGoUJjm6mSqrWGD1T_) zu7rMh9GI=$4tG0YhuhHV(~G!BAK3k1C^pUPB~`nNwu^F6YqlT9+u}zl|QZBbb^z?b*6RWYLXg0 z%7@)XJ?SR3QGRGndRPCwU)5uC-_B0Yx1)2d=ChM+R({pW)c;)mKZJNp^!11Aa`WvV z#o{;*7bOmS66L}alYbn45vQ|2|0rII-xC!R@cqerR*o9}S>D&P_Y$k&bum4D8ma_Q z^bujWdI)_wT#v^|e1~zG!ozv^aNcPaCuThf>t7~nJu!rfGO^&Lhe@yq@KXw=0$zn8 z12+USqF8#xnMTM`gBb7!gp>xzl!SGLyg3$iY)Pd$3;DR1KwgS}>C^!d`OlCThffH& zrQ!&_DKtdJ2vc4Kfdrvk6O6(7BW%fFbf6LiE^vqz8|IAG{{~5)y;FQ-_a&^>;9p1b Q-_G9t22JXk8;}P802ES8&j0`b diff --git a/tests/repository_data/client/metadata/previous/targets/role1.json b/tests/repository_data/client/metadata/previous/targets/role1.json index c5e3bc866133420448769ab01acbf4cb56d45b84..311d687a72ecbfd27763a26573b052d6dd312e89 100644 GIT binary patch delta 595 zcmWlW%gG)_3`Maq{FQi--RL!vRw1S6*~V8vAgLvNxTcKJW&{KG9Nk}EzrOzY`R8hv z`re}OpW&cJtVUA77?^+ij}M?zOn{vu1UE?FB8d7y(IAdl zK<4&{Ijp>6Vr18%M|b=dFE45sXDBp9<}m`jrQr=my(j{!!OeJQWE4F*>{P{vFYw*1 z2~nY32b;(X20nQ*IHe>-@tzQbb&nSgvHJjO1#)U`cELHC_8Twf4W*q2oF;6~fo08l z-(p8BFTPjLoln;x*=@-~RY$9+=suYfK5fP>i%rK_R7LXA&O2$SXg3qH00^qRbF)LP z9=U)|i2HiIF76A$FdN`}KgZ^EWVn6`z(3$Po^kcjCSSv6t>-=hnXba)s(3WE9L6m> z`DNC@>1*RcTgj&a_hQoAwV*4Bui>^Qlk8Mxa6o}-_;pkDq>Em`Xg|cM|NHy8kN)w3>zBQlbb6RaEKtmc6fh9W@=P@yvIUpf~pz77v$JfU{fBuqJ z9`#hsFS-b6fI!`+z_0bCpo*aobM5Li*YhA7Q&>3veSdwDWSU&ogM9deA|c>Wb`zb1 zHf3R!WzZQzKQiJ%2E0E_AI^C`fE zR{_zTI_$Eo#43OuCgFg=a#DA^qFz%D?N%F5@D0Cu!&(n_G0V+G*NRQ5u#ptXb)c=~ zL}TmaTchGPVY1AkMwX$l?oBDXV>TSZh*R`#33s>S9?j#qut~uV_2&1J57mEv|MC0B E|BkPqPyhe` diff --git a/tests/repository_data/client/metadata/previous/timestamp.json b/tests/repository_data/client/metadata/previous/timestamp.json index e94a2a893ac7bb3def5a3ecd3a51de5f281afa0e..73cd3b4c18f1b2bcbb01645f57651a25410bc321 100644 GIT binary patch literal 924 zcmXw2ORgL@4BWp{40;_Kq$qymoo^6iyh;!Rltg*P&ilYK31S$&d)WPKKn--0B3Z1e z{`PR%Zr6`be*fo7+_rb8KORn}Z~2_I&++v-GqSDGAPl!`x@m(IHb9_6 zbSl=R6zFw2TN%ACG({e3VqL2=1!kvG$=Wd1s>%)~y4DJcHpqp*lvE?ItQuJ_5-_!L zb+aCl_0~!@Efd>l#4(}Fg*av(9<^ZMmEXkZu@_Z^mX$J>pjy#|m1Qjv;DC{DtRM{VDD9=NM)#0D^Ug4$T0Ey=CrMyqB#t6Nw78j= zEKUW12#uSth4-Y?2ja{PQJiNi?l5|AO5bNI1C`jU;sUWglp+}lYqNJj)Hb7uwlPO~ zq^63Fv?!NZv(_zr^8MlTXZqzZW$bR*&A9z_|N0WUMLt{~<97GQm%}+*{P%Kw_3f4f z!W+Om9{}DR=l9o3X*~bYDEmJ7%kA=ffBWh7{PZ(=+Ae;(9R5GNpA&BvFD(1W)uuxS z?>>plDFs^tU3T!(#y~doo^FD4R7?3rBn RHM#v8UvAd}6h375`~O&Q_Kg4l literal 924 zcmXw2Nv_;R4Bh`zG_;NlvWioA=NklRuMz|SS%bUn`~mJx5X11@L+RUq1PG+`rh5AJ zaN2Iyk57L8=gYip?@oU_oKD~JK5d`p*Xzj1R@j@`?sy2e~i zlbS9Z5@VT{*acPLDQ$GkXDXFe60f?rgJX*1j+0j*FO!{|Z5VUw3r3Mv)JA6wPng!y z4~|6ThNi1DrRK`OTAPlts#C)-BvP9+(L-zjVhI_t0yB~sjVTz3bxeUK)x?NFX@+91 zCk>(ewh0I|V)Ia=St$jZX@&5G0wL;Yvem|JMPkoW3k6JLL!gnIeK(;*2=2k)YuFMf z?h;lNRU`AO;LVyUBT{Mv>M4c?GId3CHw~3$Lx5XM4q^#*QQ$rxw0m0>g;TV)=xq>Y z{1HAAu8z{13&3hZ#SJ=yG0aqNTq?7+6=YTl#_SnvGBJ%>xel~0*rq9G!k8LpG@jwn zLx*nJlkX3wKeI1KDd*mny^Pym_pdK=ugHh%!2P~i<=ULOG7 z9oO%#FJ3@~8>iMWc%}Kwj7KI|0|cSxDKJ>uJG#3{U&l{y+WwwTAMcmM#cHr`((vnY`*(i1 MT@O+CkmK+F0~zn~cmMzZ diff --git a/tests/repository_data/keystore/delegation_key b/tests/repository_data/keystore/delegation_key index 8e7d0989..75c1ccdb 100644 --- a/tests/repository_data/keystore/delegation_key +++ b/tests/repository_data/keystore/delegation_key @@ -1,30 +1,30 @@ -----BEGIN RSA PRIVATE KEY----- Proc-Type: 4,ENCRYPTED -DEK-Info: DES-EDE3-CBC,15C68797C3B9B4DE +DEK-Info: DES-EDE3-CBC,FF468C7E762281A6 -olZelOMTOmshXz5WmIg394qt0MLJKKYGCjg/qwtedvw5JVFY+SekVWxAVPGAy1cn -NbIf5HS/FDaOsey2yrnBOUy09SPUIEIb7hYFsTWkOh/H0eYLQlU8z3WG1z5SJf3U -1gfVGGgiNJ1mneq6EtJUS6MJxIG1vdzAoUOVnW2tiIrnT+PcLFCSL79VRQfscy6k -H0pnxOvo/18C1JpFpQOdNo8gJZr0M6mzrpkKxG4ZWKVqX2CnPk6nTOfq/pVxelU3 -AMhYjlu53aFs4CnjidGxwzKAht5oRMGC8KmDEin5LHhalkQ/NnZNq0QIJh8m+cWL -RxtgbzU0zY7iUvQhjcgnSlXpSvW0zrVXxW8gHarCSnsH7dLW4v50m4ATktrkytYS -A2I1t7DwhAyp5pklSZnRhaWcjwHiQswbZeVMrX3tkWqqOqSpfFGEHl9pqAnr+GZ0 -FRELOxIts4p6yF4iI4B8Gw8Qw5xz4RjHpcbYj1yharKik/D2AiaA/sI/lKOL/i0S -CQ75aUdLhuczpBQlxqOjz/N13Sd7ZtdsBN/lMrjEkYQeE8gkziEh3Q9x3Pd932Ui -nQSZ+bKcItuWvMomDSAtOVcnCKxx8tCQa9YuKUSJCTOspflIQbBa6RexLOgqFbby -FBuxgH5lwI67GpHnhxTxkubUqe39auT+H/iFHaHjIF5S2/19pds3EV/PXBsKjVqv -FMzQhV6c8IzVLpeLkg+yB8U1onO3NpeK87AHBAYkA4cHuxNq9hLt2hYAgtHJE1Fs -2fo5FWka5eUPbUhu8tZkbsKg+gukLb41oTlQ2dxndki+61C7prYkeC1FtmU4vQmb -ZPK25AHQi1zvKsd09vxiKWaeMcHIlkeCFaCQC7Bz4uv4bM9K1ewPRr25gY5iwz+O -z+7a1zFRcJNQd/WhamVXjBpXMhlSVmkOWA2WTsHyf3OBLERZixkokHPdDxxyYEb1 -45RPBdPC2Mct+dmrOpTO/a1xotpV2ZPzIrE8+ffVRiy2ho3fXJUHe3OK0ko+gKYQ -bknSqcfRlViXjBwRioo7vxsr60oFa7+ALXoktlTxbvy6FbybOKBhjezkpLzepJyj -GjI9QyqBZqTdevSaBMQzD190DyziHUIAUpOsgn01GVhdvW9+FziCPs+rtlARALJR -OycFZvRvo5f1CXIVJTu6VAl1jQ/A19DBLqqVM+oH700ymlqN8gw5/o6JXPGS6u6v -vn2OndWTBPCsI8JFkmDvFkyFOOJqc4fD2JKBUl+/oygvH6PIPOibNFKuuVq4hJIA -cpaXW7+EVI9nzCa70O5hxw01MFcfKPEhqPEcNiOjU1kXGmRSEap84lttg67MQ0xg -0EpwFlaAB/xrKL628jsFupwB1I/fGP+ANZRHxFMIl1MGSq1EATw2jsxIOUdQ3bE5 -LlR6BWO0Co4uCmEjNIhfHHysKL8NZ1btYgE0TI/k2YxKklh/c/DRC+TskiW1KRWP -BOpNwDPWyBTxYSpXGB0HAAbcGLqKZz07EGHJUdySJBm8ReP/UUOo/fjynT80gC// -ptaOUGgaCIykjLJl3qGJO61peKXDisaoPjPzTYF/TauFoKftOTRGeQ== ++pk2a/LkfqA9JTFxRKw6A1A71hgZbxydHPXsGYeh4RPPHC9ni8yid7CeIZT8DNru +W7f11uIyfZpHVzVG5wNLRjMzE9YzSBuFiLWDKEeDyZyNr4FxnCICEZt9zeG4Y58C +Hm4ls7vGqlbz+vvNVRtyj/2D/JgUlwuywFz/dtfRu4lxAqjR/cOqcD2AzSiFUkpp +LCCqkF1MBuhbPkykG+a1Bt4mKX251sSLKIAF8yFl8duD77Sl/sZ9tB3ySDQVDvRM +XVUZAtSKoE5QGVSqT5GYjUwXa+58PSWxvpSVN7wmLLX8QzIAoWDDMmviNdx3VRPi +0NB7TZfCM+212Lm5L0nckmzBkzW3Ix16aOAVpVS70DonmFgehF2BarQYn2+dXzw2 +ihMgKrMi9om3FOCx5SqWBmD2SGqf0Emg31G6E5ODlJY0+0h3oM6ePi8yAIEt7SQN +1gBa8Ux2O+GwGRFuyJ13jRonvrWf0AomDu/oje9Bh4FCOxBAKwDfVtsAE2l0fJUK +ahZgi7cz/iZTZHckCDO2//W/H2Q3fHMY5NQ1JMGyxJdRnd8WqX2tXEsQijD92G0A +gCuwhlInpLDZpLe97JJluOLRvEG3Okm4uwaNNv6cEaqC265KVkCXtErOcsjhJb4J +MTXvX9mr8JmH4xjSgAr7R9/HDculRmrYYIepJumZD5uylWbAYqe/e66FY2MiETxn +FuKR5mpqcgPUeaMQE3lrcebVQGhutb6Rtx+KiUugxub4muH0Vm8xdF+gi7+fJyqa +3Zn+xP+DUZaH6fcVlq6Ihj3kTzt5ATdXNwH6uECsdNj9G0eBi3IlhBTxPZ3gguJd +HGw2LxFe/xI/W1Prgp4bma4f92J00Pzl+sbVvDvp6pWW5B3oE+Z8GH9CCj0LUsTS +dRJYg6GdEWQUQXaLdOFAvRN3xckjDjkAOaP5GkrOjxtVtqkOikXHjlfGUOeysWvW +Cmis2WwfvE/KqQs6kOeetXv1MY9MH53VXc1sDRv0tCV9Xp8YFQrXxKtMn53VNPha +R2XL0ElCZen/Nk2OcGgnr1VyFY3II3Vo+O1387izTEOc9pKGiH26uh45iUQFCcJu +oXUQ2aSbCuBA2kPX8cs+UGmD4HgzRy7HklxtbwR0kubRxH3z2+dwwxaCz6hpQ98y +zQZ447WodiIx1ia9r4bYAdxD5b/yx7yaMY5FGM/jfF4wDRlXSU2q6mu+b9TEzmFo +OS+P7V27DD+LrUqyVAHcVMlrGth4tTkiTgNhkBBS1sFVe72fi7a5IK1klgAaS53D +qSN+D9I6MNiS4Hgj2OmPyX0ZPxn2DTD88IJbInI2H75nR0Y5H0dHUUvMx3H1tAa9 +piKSVGN9J9kFCIWCL9u/yMErPSHW7ZnNIsm6SK9LK0EYNDBpQ/QPDS+ZbNgw4K0w +YIrNZoWjYJbCpy7RtDOIEqGKlDyfvr65+lShNOuEY1rcsWbalACSUKYNjmkbIngU +fVdNcpLjVzpsoBU1WIJ9C9ExMXPJlBgidwcapXRhkTiY8+Wdai+ODhnReJKbe/sc +/tlOb+mnZVEH2tco3z2exVHg9igDSWOzrHbmy0bRj9kbj+Adj6lFfQ== -----END RSA PRIVATE KEY----- \ No newline at end of file diff --git a/tests/repository_data/keystore/delegation_key.pub b/tests/repository_data/keystore/delegation_key.pub index 34aaa679..9ed2a92f 100644 --- a/tests/repository_data/keystore/delegation_key.pub +++ b/tests/repository_data/keystore/delegation_key.pub @@ -1,9 +1,9 @@ -----BEGIN PUBLIC KEY----- -MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAlVZN8n2Q/WjqVEriSBNt -M4Jb01zJanuIHBFMSR4+D2bFSxNj3DoIzfVzRPCxJVLQJJ2Yg+4nmEkaIYnodpOE -7PzWyDEacWhdMSE9nbiYl9QzPEXY25ql9ni6CvEfBIUV74i+bTAT6zfoOpZYfS2M -ol0VMP+JTHMeqHD8JggQuPMrA50l8clwdwdjKrupqOu/lpxgdPKHASne7rrJBeMz -WJKr69vZXawbwYyy6bYweMV3/fpEW4UXY6uJSvE8y/Ocf7pBIcVuwFUeooOEjtZT -GY0C4StOYxeowHhYBTDXMi9XosgTXf5ahyeVd7S6Wq+q8njscih1AXGS99IFhEa5 -YQIDAQAB +MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA4Aaacry3Lrf+LxjlHoM7 +qkwM0K+Wm3G2dZh6SxrGEkm1/REOk51CPTFVqpVOUsw1tW0duAlxg/24cxXVX0xv +BMilTYDZ5tBk6FXZXhuN28IREz2yMo8a93nHH3fCRDz2/eViLBZO8KMVF6s2340V +tTNOFhm8x/6nfe3+M8PzEgoNPF693djhUw1OolEmQA4hb2v87L/86SFGBaA1w8De +M+2zGJmk5rckLQAZFvMErZ8828WhW8ABFkK4QaUJDrnI+o00ep4+1Qu8CvOIw+pT +lbYgv/+aXQOXQpkmKilpTCoynbQFOxIPmUmCBAnJXgYuHya8SUIeZLWxNvkChLMP +SwIDAQAB -----END PUBLIC KEY----- \ No newline at end of file diff --git a/tests/repository_data/keystore/root_key b/tests/repository_data/keystore/root_key index b2466db2..38b65b85 100644 --- a/tests/repository_data/keystore/root_key +++ b/tests/repository_data/keystore/root_key @@ -1,30 +1,30 @@ -----BEGIN RSA PRIVATE KEY----- Proc-Type: 4,ENCRYPTED -DEK-Info: DES-EDE3-CBC,D79DFCB8B13A2CC2 +DEK-Info: DES-EDE3-CBC,D1A23F08038990E0 -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 +QZpQjcNqxSlgN787dzQBygJna4YRMixHU5SsmIN3kA2fIHdSu5Ij98Vrne3/ZpNA +/a9nt3D66F9WStFvrjKXPbj4avyF5DC1x4SLXGZ3ndL2Ya/3k29bxL6VeZzx7dRA +t8XaU0QdV2wsVn/daxI4VlxFHEoKNeNUsLpO6zePpR4IHmf7guE76LQa+W8W+8Jv +FaW3hWBgB2giGJ1eIETN0VKXtTVdy+7dKl8gODqLp9VBerjIThts5diJdggvFrLr +yAntjrLuFRxtAoKpxCX3maoDy+qaWsl8zBXf10b5YweF/mvRuyCNQW3ye+0qYYR0 +jvUjCDFbIPXStG686c/3E3y5Iymg+2BxORCo3h+PrEhoKt/KEiXVld9njdEJNaxI +2DTC3u0OK9GuyZW1i+X9dJVeZfItraHqRhPJ0piFMyOsMVWLjd0vs9IH9751dPE8 +/Q/A1Ohqh9bbYM97N5MGTMV/wZxJkrtAABYYORY2YufdbaIouICFuqkYOJZVFTVb +zihVaZS8rJi/Fe6++D3J21kyQyW+Nvyo+zFyVJ1P4MJ3CgeV8he2p8YnUGwJv29G +n/rn+UJMVLL1zWOFVH1sUoGhbVKwlalmaQuEgiXK9pXCK+IoHzfjxNeJnAOfEYQI +s+n25+H2u0xSPsGpKJ9snLau7V+T+Upi6v94GTtxhHw7Gx14IvuUOK7I4HhVfOr9 +Obw24RrRmnWChKbUPn22CvvOTw8ew2qekcDRa4i5uLJFHN0o1wTJ0aGN0llLWza5 -----END RSA PRIVATE KEY----- \ No newline at end of file diff --git a/tests/repository_data/keystore/root_key.pub b/tests/repository_data/keystore/root_key.pub index d8fbf21b..61ab286a 100644 --- a/tests/repository_data/keystore/root_key.pub +++ b/tests/repository_data/keystore/root_key.pub @@ -1,9 +1,9 @@ -----BEGIN PUBLIC KEY----- -MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAwLbRMxjtj63qRwGAW8oe -6Bbw0shvSolq3s30yOHjToxTqpeRkHhCyZCextQjN3ishIpVcJM60xQGfFvNNBLj -Y6aFnavs2K/PPoKB3snO5w2tl0tBic2Kr23zGgbMzT8oqWZHCfdiyZzPa3oaH4fN -DJqaCvs72cIP8IrpaBsuYbIdtGDU8pXM1dfE2NVA/P+1uxjV3Y2k4Wtf04drYhyR -mXfy9I6t5a2M5bagVLtF9UM/vM7QExSvCyuvD3D1EMSiaFjxMnsf5uFUo68wPfZB -bJntUxzELcXcy+O+ks/TOinPwW1KpGff/R2tBD8GxKbwYbNA3eMqxqrHcTY9rWdy -dwIDAQAB +MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAxqR34Ya+Nuh4+zljiPI7 +idGUwTs6ALm/mwBJJZRVmIQjVDKdK69fxmh5CxXud6lJTapuDutCFOvOKG1AaEHM +Zu5r/UiuEEUU2ZTtjtFaulJpoz6lYViM2qHy8o4HJ8f5VHvcITnAy4Qu3emKolvi +CrnMoiBZjGQAGEGt1u7S9u8P9zA4Jz0ziICdDnhKH8tLvWBmF4VHmAbLXXFj+Yny +R7iOeDEiS5PNJVyfEHQmds+LdBTI1w0aBoHORvrw36kFRD2yZF0w6BZgqrcM1OQU +xuJBWf3DTMRmBdldIIzGZQ7Lv08GB8xVy3472Q6nn8dTl0Gz3Y4ujMqbcglB4Auz +lwIDAQAB -----END PUBLIC KEY----- \ No newline at end of file diff --git a/tests/repository_data/keystore/snapshot_key b/tests/repository_data/keystore/snapshot_key index 59e1b01a..3364b4c2 100644 --- a/tests/repository_data/keystore/snapshot_key +++ b/tests/repository_data/keystore/snapshot_key @@ -1,30 +1,30 @@ -----BEGIN RSA PRIVATE KEY----- Proc-Type: 4,ENCRYPTED -DEK-Info: DES-EDE3-CBC,05DC466E69140631 +DEK-Info: DES-EDE3-CBC,CA9413D527FF4F37 -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 -----END RSA PRIVATE KEY----- \ No newline at end of file diff --git a/tests/repository_data/keystore/snapshot_key.pub b/tests/repository_data/keystore/snapshot_key.pub index 70536aa9..fc4ce86e 100644 --- a/tests/repository_data/keystore/snapshot_key.pub +++ b/tests/repository_data/keystore/snapshot_key.pub @@ -1,9 +1,9 @@ -----BEGIN PUBLIC KEY----- -MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAtdmy18GajKzifjlXS9BV -XasPpZiAJVQmBJHSnRGsLiUs5uqByRC4SdW8OWwpoTB/LXRhicarE6DVv3yK0mJf -fbjdbRSZC5fYyKCO67oTijttptwzDiLoxVWeCry/5fM0yQ53nPKIkneS27DDraU9 -S0Am9Nmck591QsCCmt4I0u84vQLyhM9HUCrEZyEQVSURdWxj935Sv41/IcXIoH+u -XOVOmvCc0e+88pg4R70CJaWWsrE8lW8NsCHmYZ8fU4rRhUktDnHx7B5T60Xik/kv -JMfy0ZtGPJu1VVdnBlCNAooXeiFVo/dBl0TOHp8F6jnautGKILtSnGHUdTzTJDch -WwIDAQAB +MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAhC3ksoCUEqNJZfvI3rgI +RuknTl3DQjO8H4wA98Y1ZfWSCGRCkD7z9wCtnRcP0r/DdRDozzIn0w5POYyZt+ql +aPBdigwWVZs4rB73U8Pjh/ZQ/QS2qngQlNmpkp6Fnh9FzmqQmIj0QYwZC6TPuB/2 +dVsnbyUDt+tLH4FCN688tOvV9fATO22rzT5XAeDCU046yD6fNtooCRVwd7mxV8Ug +npREULBq0cs82Cl581aM5znN1ydFr7cLMaxzvyAzJzAvdITOTdO7ZO171qm4OrGD +9W2ex+88ca3XYhjaEJGsoMXa00NPzrZPtFFUv6wSi9nqa5mpHmcEWAQABRRARlkp +mQIDAQAB -----END PUBLIC KEY----- \ No newline at end of file diff --git a/tests/repository_data/keystore/targets_key b/tests/repository_data/keystore/targets_key index e3f174be..423015bc 100644 --- a/tests/repository_data/keystore/targets_key +++ b/tests/repository_data/keystore/targets_key @@ -1,30 +1,30 @@ -----BEGIN RSA PRIVATE KEY----- Proc-Type: 4,ENCRYPTED -DEK-Info: DES-EDE3-CBC,2CF02CD52C030591 +DEK-Info: DES-EDE3-CBC,112A90551702BF36 -/U+WOJFF9oTzdejaHsXIA0uAgo4/GvMvz/mjdXrZ8IOKk8/aRL8O+cwzCYZio67Z -Wpq/rKGp3oTvAb3s/DHWX1j4yt+gSnZ+7QIHrPdJfa4PfRzj5WMGMkQtYwuTALbR -EqfQ0QXlfbhaDqXHm5FnmYFpuHWZHNrtCOO33aiMlt9be4rxstPQLekKzgEgmtt1 -OarBMHxrgkKfQ6JIxZOKCe0yObCWp3yCfu55PsEtCAoo6Ro69JIMX1B9GfNZ+1E8 -WTJ56+WTmDKHxsVMSX+YobMdp/R45UqgOmHqzYxPBcMfI8FXS/+YXG7Ja1/hb/j/ -seHI8J4S2OwFegBtQb5vP51H6XL1lcbQX2i1VlZOUr3303NZtsxp01Ke1fzMWfXP -X9nH0/6gKy/cXsBScW9aWEmV3p7Ga6oxOjBvxgw+LQO7ypfobEQSrr+VoBgg8+ah -p/RL9OJgJPB7IoewTEn/zaLSOFmc1tqdl8L7vH+XTGLrZhQSRRvKpz3rZRn8P3Nb -+yokl9dfe04I0XP317QvvuZZGaqHDQACxgitjLFr6N0OY850N3zIE9j5y3YKiPZe -KBSnPAQInRkE7Q1XSBOSlaC9fdgkwbR3f5qkbCwGjCkTddUU9c+A77t2itWm+lvb -RSiUSNriliXnk/Un/vharsMF+cieKsRUSRnnAZzkMOM63W7Cuq1DXpr+LyEa50et -hZOhA1NhC29SX6FNklskrrfyRSoG5wKEFT6GFNqjDSH89qnVm0sDxl44fKoN7sEW -HFzM/F8QxVSr4k1P8OJJsJmKPMb6uZNFN6p5yIqW58XXGlryVLOY3qvg4aaphSVx -azCQXrpDtudzQRk+Kjc09RqSe0+DPHXDDzaCSWEY7q5TxBM+1Kk7kq0F/mpq/Y2f -7bJW9GuGCGtHHBvsUVcMHvqw8Bt2iwlEr9NpPbTfmoo71Z7nOtiRH5JS4sxFgNKV -T62qfluonqRPLDbM4Ylbo37UTojeAJMiePCVe2xA3agC1/uTRp2R6oEo1Rmuol9U -/nLiEib00gxMT4GOQVTKosRMjzRNOOXLKf9c9WdU6RjBF/HAhD9NgAzJeDmlO0Rv -WYG28rKX+uO989YSZSIquOkqLsdrpemnJQqx+aL32iln5+VG/1702cvZW9OJwPs8 -vCsaizqTHC3TwRvPDGuSNlfiI5pw676pZ7iP5lOg8KwWn5U36qomPyADcdWe/oqJ -QOw0hv3ZgyMkkX+ePn0aw2OvIzFUjB5UMpo1Sl0lSk1+IyLDW/igfX4uUTOarbwN -I9dtmRFyS7cZV3gSaGtyondjVKNNgJQulWVrnHjOvuzE3Cjt29ZL0hfJ/a+2X+fk -3YWLveZd1UNqgg0cwA807E07wXNAdcTrBJsf2MRJS58zJ4VAMTwsurN70V65craP -NvBsJfF+KMN0Q9DPvWG0vEZth1cSeMbLf+mg3g8QEkREj0rbdoLxGIxtbXlk97g7 -ngtwdaA+6bt12haS6Oe+KNG61T7pkZd8vgcQ23ESY67nkM2O1Rqdt5ojAkQ5laK/ -s9X6SxIltELzd1OVF77BCvHCuekBWqIlONhCKYFCr/Lyj/RfbDVpqSUJA/KaJbsT +uzGtvIP1CHhSV97NWvhfHQ4iU2uLCde7tz9e0IzF3mjCTCuX00XdERj+pv5JN8j4 +05pgG0tD2Bz7KjNlRDWEuu7iDMT+vkQPPiJNQetA14q4huIJxVnbT4DjevwqYw2x +FOPTbZIfwVMlNIRxS9xS0/rAkMacVUx5XgPgwMy/eu4/XF3tzuYAQc/3zIA/WjR7 +rFl/HXl8m0D5w83VEec0OtzYPAUUrAhrXjOZ8OkLjvAkZVFbzNxkfFdP2u420ImW +Bv0XLL9c2EPp0RdlLIBIJo9iUrpbBEx4heg5+9MQK+5rkIpkjGd0b5dtJjJxYOti +Nuc7kDPxkPo4uik3tXmy82yz/fNLkHt8AJWNd1wkFJykshnvPY1eYcyGj6ee+gsB +VyxCgbHY9/0LfQCIy4FRh1t3tWNno/SZzDFpOjqSbxrNyjhg+Vs3L4ld99XMYd31 +FNwQL4hJY31Z1/yw4HDAYXgYH2irKks6vav+A8+AKYD1CFRimUNpiu65sZnbz7xo +toVZAxZnaywwnT4c0NlaZUtLcGH6lbJD2/Q13XlXnMtMMGMwHZvAPtpUf4+pwMFQ +4SDCKMTueauBw+NVsgPbhZgbgok5CIxOSWvHoUSOB6BbKZ3Ie6a2AbziCEFj3xky +Rz6iOHtb6i96MAqK5QlQzamgiUT9mP8anPlTePk56SXG6TDOH7thB4Qaai0PQD7+ +771lLCBYx9IwKmylY1FeFgGopWYQGgmQqfJqzTjmOouEIKjT94ybtmYHTmC7IA6+ +voDkD541kEkelo4xFJXU0OnCo3Xmpm6Zt40RyxxhU3JD6/C3VC50Dd1SKarbW3iZ ++a15z6gx/UyvlHn0pZ/gsKcb9efCP5ky8VYLDUMV8Nu/wZwy3AxgiAnscC+UuG/u +5l1Rn3mMHr2iE38apVKFjyAHUzBd/izxAiy3dQMm08M85eVuPm48z45hyfo6ZLNp +2zAHRBrPJAQsSZNoonsZP3YGgXOtV4m86a1i8Gyg4xVDa6ne2i2vW86CA5sivq7n +WcpTiqTWGJZVPKup955YXApM2X+RkwlU78MdzkFeoy5yx9Ef7TmI3Gv49UBGRqk4 +gCbkOujRVTfCHlCzw58X0MCyeCxJ2Nn/Fjlbi765DQpQl+yVvRKs0PCpDNAiDPYI +3q2Mb1bRA0Jk8Ghat3udmqMSaBjx4GLm4/Ey4OJ/ayaFE8w4asU/dnV5zilzJ1Qc +2WnZG2KNZsuLZ9/Kv/Gk8gIf53Qd0LEq86WRAf3qt2IXeNqZJhidrsBLCKFQTLFr +2Q+dpll1qu1hfVoznjQRtEyiL2sXdF07Obi3V0CLlm1e7diAsl3nILHh/1VZOL4L +29gzgEKcIDp/ekNtFIpIWgZ0ejnqnnIOGDj+3jmMU41ZARaXMOzzX+qSfYJiDwja +SPQdhH65SBLKGJOvO6GFcsJLRD5ZYBK//M6c/mY0DdZWxfTJFjZveD6vT4ATgubF +4uJPiKfSsnTIl7WGPKmXbfITCe9AGdXAaCkwiB91mtGMf1kyYyQvqEhhwOH8JFsc +b4Me/DGeZppGRbbwy8E8kg4l+Qhh8jK3bXmjQ5GXn/tZDNS8X65GMg== -----END RSA PRIVATE KEY----- \ No newline at end of file diff --git a/tests/repository_data/keystore/targets_key.pub b/tests/repository_data/keystore/targets_key.pub index 2bc18503..e13897d0 100644 --- a/tests/repository_data/keystore/targets_key.pub +++ b/tests/repository_data/keystore/targets_key.pub @@ -1,9 +1,9 @@ -----BEGIN PUBLIC KEY----- -MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAxFotGGnsRlu+qXalb2tQ -FsnNtsI/lDv79sR2tajNGl6X12h+q3asHH248arBbebfoi59tJmDLMl3EJzLETIw -TFgKxzrKmcCZrlEZPWXCCXyUXd5bWN4KvRDVBNJ40xk3WvPGVj1do6CGKH+W+Iqn -MRXP3yVxSh9Na2X2T9JxKV9ZYqeiaxFppZYqNS+CxUcm4+o/PmtfJ8YagRhbWIKq -Q2R1mpsB95Iwl3ZfSYhW021+lwngUzq4RutQSqo4xH2vakpD3BX4bC5eg7IYvjm/ -Gv+bvNGvHQWmOsBSzK69P9WleEkXJLkbTVJ0JB7C1Y9uANWuXeyvTw9+V5QiteYK -XwIDAQAB +MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAkbEPgkqMnOfprFY5XhCo +ppl6E89rfIzKv0AAZH6L6CruIzm2wgf3YRGl5Xle1+uyq9Wt0fcC32cZaoQPqpik +JWKYXdeuQjkUbPzON5wgV/MrjsidTC02296pmfHvGGlwUstjqwcbcCcAliw0V97G +IJ4JKnbelGmFFpUzzow8tp7uxxbM+ihLA1V2QdAJBDSIuma+A36zN0M23vvt6fBg +cDzJ+ij5Z3N0O2WAyQDONepRtHjKKdhAuzKRluLMwYO0sMjp+E88LMw1vUapxsEm +9jrYmY7aV6RFTe5OeZupH/0HyMwv7jua+aNzNxxD6/xYbh4Kp0jFke1zEBsoNrc8 +pwIDAQAB -----END PUBLIC KEY----- \ No newline at end of file diff --git a/tests/repository_data/keystore/timestamp_key b/tests/repository_data/keystore/timestamp_key index 8b56b09e..614d4bfa 100644 --- a/tests/repository_data/keystore/timestamp_key +++ b/tests/repository_data/keystore/timestamp_key @@ -1,30 +1,30 @@ -----BEGIN RSA PRIVATE KEY----- Proc-Type: 4,ENCRYPTED -DEK-Info: DES-EDE3-CBC,3495EC0D0BEC51FC +DEK-Info: DES-EDE3-CBC,E950DB75AA748728 -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 -----END RSA PRIVATE KEY----- \ No newline at end of file diff --git a/tests/repository_data/keystore/timestamp_key.pub b/tests/repository_data/keystore/timestamp_key.pub index 09a27dfb..33d3a26c 100644 --- a/tests/repository_data/keystore/timestamp_key.pub +++ b/tests/repository_data/keystore/timestamp_key.pub @@ -1,9 +1,9 @@ -----BEGIN PUBLIC KEY----- -MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAymH18micE+ImSDCQoQh3 -cbTHv0jn1aTKq61R2HYrGFZv9Rrr65xnRLUYP6Ypa6RByQRUwwq4t4v/SUbR74ID -+L8oHdZ3LPrfNx3eJXtGinyvQcayt4C5QwqjmxgT8D7L9G+TODGh42cNbWlguQi/ -fDr+tSC0+ZERXwpYDkyHVJmO0UbEXbWTz7w/R1AAHcbqI71nZtGzZDQlVg6oL7Mz -F64XJnQn8tB2EQF3Tpo9dLaKkzWJAW3i8bba0ltIBkoAJtDR9MNPyXAsak7QyRkI -zyjZfDHOXy3iNRHWaLWzw+4pY0WmzyMN5ABBnY61TI8pgLqTmNLSxJY6LmXzIixd -DQIDAQAB +MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA0EhQ6twjA/9HywnQWNey +rthMpF2S3yeRQiU90TPWAAVsGYvKSgra/BjNTUdFZxf2twZONq/PW1CSQYya230i +16+4/6wTY47pSAoDfk71ISjFEdkZa7Cbvzv0CEv25sA+elaaCtRnmc6G7RuofwM/ +AnKqWdPfkJcdkjkmNGYVhNU7sF96hNSdG3liWG0rbByZVxPYMYru18gziGQOkp82 +Ex/6SWEkUzdjZHqvL7A3bEUJ2KzHoHvf3SdyRP1HbASphnWEvBqC5/3TnmsAHvWi +d4NRqKLJlsLnySGP5gSinjc53bN8CTJz+ThSaIFlt46RpNtkjm5ZU8YjmmqfbUSG +FwIDAQAB -----END PUBLIC KEY----- \ No newline at end of file diff --git a/tests/repository_data/repository/metadata.staged/root.json b/tests/repository_data/repository/metadata.staged/root.json index 9174375e06a09dee6d59f428d3f2fbb7d386ed54..b1b34fa314f9ea032a9ac600bf4a5ff167642ce4 100644 GIT binary patch literal 3756 zcmd6qS+lB0635@~r#Nw5oepcGs^~XF+{ImwjuRa~6$%0(tJ2Zmy-Ux%9X%5hGu?9^ zdJJ4B%Bp{6{>V&z{_^&o2dBg-%1q|(Kfe9_%iG(}>izb9k&i&Ak$1_Mkkr*A_ZUNj zU_f1@>k=Sc0}wIC1(^AQ69hQc)R)hZuTjsVSW-yY``=#c#j-eGUkff*#%g8!Fkh{{ zUQ!C48&T;3M3623!BT_7bp(VOlhng5;ywo&cA4&~Wd%W!awwG+U)P0)T%Q0(x)68_ zQ{d^Y<`U_+9?=-le2=-FWC(I1kOq7RknmjJ)pX!+h;#sfBmf}-Jzqz_LB9K(z>u0L zT#c#efKw=>=PMOR0z%X->QBC>N#a3>1i>0`T&ZInNC)TwGnaYX;hvHpG$NT2=W^Z0 zQgT9x=VRTK3?mEyKmZWNl_^lUkrD=!{lJ4*_{ymsg+4`?Bcfy>A=J7=gVgmsU%6Il zp47OGA){&|;_K9bp65U&iE@}Hh4QD6${Rj@4kKUr)>Yo6j7t?#j4>5RnNJ9$Ncb98 z4i_8(M-x5(!WXWxNCjCFNFok(M;QhvV+3ewLnVs=$tb~07X(mGg@$04Gl&rs5c@=6 zN*UH%h?FyQs7diOR65Aj6(vMLgpsC6<|=!1@vb=e<;&YY6fZ9*<&%~tjPE}ck5oPp zSzfQj3z~QDn&d%V$fWowPn8`mO-JXJX_Xoak;fRdJi7TUi-0P*A zrn%OD4X#zDksx=KLhzROj{P`>I_yJC%xxZd`$E(+sAw6Z0WMMmfB1n z%5=~MgU7V4SXeqO$8PNVy-VBoy?Z9(6okEtgpJk7uXg5f<;i;3H5Nwan7o2x+1-DQ z*OTEF#p>fT?|par{L1GGpuYyI`)!HI-f7w8nY5ZH4CX_2Oajs0)a#rYleit%ozdv9 z+{VL2xb2R`nCkvLK5ORvqZBk6t(~;&mWA1yRkLv)8jdxX9+N|xx;{h)w)vgow$aWAL?&Ra67q<4vvsj9Cda+?yjfHxFaGl z95(&Kf=w#G`yGDYJ}_ZuL6d}wbp-ki-jgz%-dyh#b%?_Dm_+{zRuXWBae=TZK-3dJ z*9lcg%ZDn-Ll-!{PCUej04fx~^9A9$s?!)sB$+CZzvZE4q5o^JI-B^CU(JnmvquNN z8sh9UQ~@b3$vVQ_ML6RFQX4wo!-KzDnf;}C>9R(z%_3QPbC9*WV%fbm%`gGAHlOVu zhoW_hj)^nxh~QN3wuhW#9fmi29-iBWMSHPAx8$^l>^Qxow3nRqUK8II@h}97eSI+L zdR}(g=$MFYp16-qw`diU0qL1G<-C|xTirL-Gla5ct$i4>YiskQMsiF0F6?UWUQ*vs2^GK%s^@>uof+G!Oep$egEb8|gvTI=)58TO)r z&}C{DmoU~28@>9Dz%dXMu3FEWb%ASqf?<-p%zCC_CA3naj zXuituvy*(TR-9EQc)mu@77ad!zLw_SHI6S;Umtq@Nyzu6**_$taI%vuerrYljgIeb nPXCyWAeMRI#OZ$s&39*r|Dr@){sg9ySsq-I&zSx4<(FRp)5AQF literal 3756 zcmd5RP29{Y#4_;^XN(w< za8Fu*5U`1a){~xLN@9(frwLCmkU|(nC6w5c2w{Su(64{{w0AJ&CjT@TO&XI)2? z@@>g^@Z5+fA{0hMQH_Pikp_}5>0w0?Q-UW%VP!Cs8d)hc2PC0_0%<($aRL*9EXTlO zo~O8mfK%qYR00Z2s5S{k6cEn}%o9YoGSXV4oUfQL_gDeBq5^s}@jOpTsv(4oQY{iH zl>m}jPXM48NMItc&oQLrL@c8~C)}OdL54`_ z!mkM9L@VG~KrIJMxuYoQk^+&zuMQ`_e0lkY!^2ItlSsoiMLmCKc-(H{R#mBKLF3YQviPad{#fmv4;o|Mc&-N@JvVMOZhaLE?%Q%p(J8vM8!^w#I-|b2 zfnsx=|i@wXU*Z6)>xo_ua33oUyEx^fnxv8Rcc39X- zgtR0Q4v3N;^M3gzW`p{5z{ainB(#W_x@b(Y-1 zW@FrNwmy^78aDqmUr!8wqS*a>fqaKDJkpyz<>X1=QJnbqMC$6562MJ}=b8NyL z;n)OOfuvB(7%5Sregn*tk^euistz5GA5z__g@;Kq80X^+T4!2KJ6EvHJUQ*1s29P` z^4xw~T}5<0Q*zIu{%kp-%cG=`UyY;L?RLT?zSbu*6)_w(*IBL4^G>ot{n6PD@5po) zWjoEP>sZUG!~q%KPTRx%W6GPXFWR-~d$Ya4P!CnS|G13P`Z}}CbFG{Nptfp7i`#M8 z+*O@<_wWv8sC4{k5qBFgO1V-J?90&K<&AFHjKp9#suqnx?%24BcHugERNIwpcHS2iN{3M= zmi_o~tKs7k#D_;U7?OtXy9)BAAwPcfPt(J&Ke>08w0~GU!t|cF;u!yVSP6ugbe+bb z$MuaT`wC8#>pTWys-@D7lRc6c$J8Q0)H1DvBt%=!HEUzgUjSD3x4dk(vm)AGYNv(V zD_D+QLW*oy7GZtgyfRU^l9bzFdruc0+|*7;7M%{nT%LVpl+9Bj%I=}rAM8=9`{=i( z;ccBw-#&Wx$GLaV!Rowktw!-82o}|B;S@wX#Jy|OocqHr2KOC`ucP*S>m@l2+PzLK zu7#(}ac;C2p=y4gY{XE)1)PfRy*C%D<;kS-{&qaN(Qr}=?lXPBwY)w$l(x&4@*~=) zIP9I)*%(IN;aK>Bgtt9f*~xMf1L)QEx9nr~IN|70jwh!a-#hRscgH637gz{!UHjUC{Me;4Q_aF^n$@Q(dPwQJa%- zTAF2Voh|+cwsLZaTEabL0f24<2y|-dsQ|dsc@jG#nH(yQx8=6?NbPpNmp6T9WaoVUdBgZBw$4u>_j|#VCcj=ko|ljB z)?%M+_^BkH*2;}jf}4E*)RW$G>RW66opAj0>f5E~_l|sbJ^Ud@N_qY;B}rQuEz<=D&F2mcIh#YR*NPXJ2#n%a>pN2@5MgEdT%j diff --git a/tests/repository_data/repository/metadata.staged/snapshot.json b/tests/repository_data/repository/metadata.staged/snapshot.json index b4ad9096b92f7fce68872ee44a9f4bbe2133b4b2..017cb34e3f9804c3db056525204f5d1f78e395be 100644 GIT binary patch literal 1380 zcma)+!Hyd@42JLf6vmvJloTbA%q?$F)IGH*3QDBbo2Ki<+AW#}dH2$@>8V&Cz<3Un z=I75}pZxjmuw5SRAMN_*b6mD}hhOdvhtK&tY`@2+2hYkD0b(p(Xd?FMpjvx#S-8}3 zfGw@FaCPXqU@otsKuj1Vc*>4;{&;+UJidSP!}0iaDIMGzNznn!9tBD* z9!+|ItV{-rYC3|bGeuA(jgTl!rq@Aiu=-dWldG_3^yRCU>0JN`FvW;TDUQ0B!>wS|+BAGf$Vi8y0=8UQ+$N?b(o&nfECR2!)U~EJ z09G6p02Rt=ow$KbU8_z~ETuykHVRlb@D^MxXC zU?%|~Mo#Z4g_(M0hize)$y7n<%c_@REd?`SpzbgRRZy(Aaa;m2L~AUibqPvtI$L&P zh1q7-b<}Wi;iWy>wkKz3OYu@((l3(!*;&Ed$7x&PxHLJJN$BUIDF3cu>C$i-AAr$hMiWI;tQbWAN1-nhKq>d zFf`W~5xf9fSa-xa#w>%W7DUGFyFK0?^Kv}x0e`%Ge|!7>%@4P?U$;`hH4)bq(ilZU zQEd&70u(j8b&zOSno%s3=GwJ$r7F!zkeDp)(gkODC=Xr}3_=XNVFW2xi4@Zu7qG+R zfezle!;DHehgKa>TUJ1&%tE7q57I1>2w6>g0|cvPJb*FWKw2$JX_}ij&DNunMMYY5 z%~rs&2G3JgF})eutk=3Sv}9&ka|sujrOoAmO$Tu&W+~iwP#@iAnpa~^S8Z!lsEbxx zM4CR7A*(f+S^^Z)9&KextSEVtBje^_=c!SK6((w3p?h^$E#0Z0V~tW{Il3cdTTO*I z7HAQG2w=$O#$a`=rQ{No^g>z&vr=bRaNh5fwS$R_RuwXOeyvl}wJIZE;7YkkOHCeT zYDy)%44cl-16^P%LmsPUef{^1z8X^@fH9;cKx!LNNrUw8`jOJ~QJ~*X|c_X4RqP!rh zXnRT4+q-{WW!Um=-V2KN(7AG;XmSGHbmq7PtgEkS~w~YgmGqW7)?n5faKw~p3McgoEN?Qphb@eqEF^{RTKyzQqc09i|?LXaA X#$8)}_J(eM&8PGIPFT4~p1=GHZOLY= diff --git a/tests/repository_data/repository/metadata.staged/targets.json b/tests/repository_data/repository/metadata.staged/targets.json index e9da47bb59db2882c5dffd99a8ddc120e14b5fad..04760cd24cea01bc6c405203496bdbeadeaddd96 100644 GIT binary patch delta 1093 zcmbu7OLF2j5XLKysVwIRv&kmK0;%(3PDWTk&XJ?yQFgItVH;Rs zO6Y&Y!0G*u?}xu;$IIcrbYhWoF%KXzC|3X`nchGB z@cU6Wh~oA|b+@_JJ$zoCt$S`(eOc^#;<9$PQm4Svxg8$1r|rc`)^FPVg^{i6QD^*R zGw$Dh-OHyt!OxB5;h_#X9wy_7IP7Lsv-h|h-JeX&Q*&WElj&ssdTr z6$YLD;rx1ayJ|HX)y3rD{`u^MaCm#3rro>4)o#(6cYD{fYPf&@=|lLxX8rxwhkw5N E7vwrajsO4v delta 1093 zcmbu7yHaCE5QfW_ErYB9BP`YMD|rNjqyeiI9{s!E=a6pkty; zE=rRru=84n42mEn`B(h;;DH^Khy*l*=#u6Tyha-jpun%Pj~-XQ2#YCs<(CK_QcqhE+t&R@noh1u&%!kf+kzS(e&@ zV~-rA%9OA`0D~zsiGowY1?P~o0IefiN5V(4P9TxUWFSk@I@5dmoHL0M^d>@9UKB>L*+;d+6ze# zh$9zmtT8JM3QR22h|+{2-Da|iizt>DNV8HwWRRGhV=UMjN(af>sv@`Wls&tgvd&RE z^4|*z^!Des^)Ds{_uinW)aZ(A=w4Eel%zIu>n%kDmp_n(KS z&%N<)*S#J#cSoxIQvL3`O}mV%K{ab>xSzb7RX$AaW52Fk%kyU8uJ^-g+Mr%9Tst?X zPt_rJx;JA|^CKRejLbeS2dmpA*Vv!6%Y_*Chey59ML(@C&TQ}Q?s_}yZ%(vW*s$2e zU3|FQY**{S_IR;+zKg@li<7!dwAu7JslT7KliuaVxTo8x-|?<_c`@AVQhzLu^QxMt zn`vX>wpTw@_T_jGGOaq@Fy8LYZ_;u(s2(0~N3-@*BTiNQIB1^Ja(8jxbVg^>{@hKM zU++fKto{8<8b_+lWPP-@?ZekFzlW3Qr`oyh`F-W}tZlB}e)z!ucUk}Z{o&tl{sWR~ BL{9(! diff --git a/tests/repository_data/repository/metadata.staged/targets.json.gz b/tests/repository_data/repository/metadata.staged/targets.json.gz index e2d2dbe0e05d5ba37e3a7d7ee9a5643195d7e4cc..9e6ad7d2a323694978a84d59aab57ecc347ce781 100644 GIT binary patch literal 1202 zcmV;j1Wo%NiwFSnd5}{A|E*QqZW>7tefL)&UP+d|U-C9!u!+GKgKf+@T2Qt3N>OXX>No&=1P zjS>!|m)2REk_?Au7K2qm0n@=6LInjEKzPlG0%EzNl5%RLBqEeQ9Ps9GlP?Z{*Zr*D z&puxD`)^B_!9LEJl)`yOjV53a(-Lot7Rd(3lH`!Ap&_y;Bcp^{CKR=XNzh&p3IQ)0 z9f?tMm9&zUC#H#Enk5rtK+Hr5Gbv(boFE?X5K?5!TD;>vs$`Xo!gB6{-Pi0nSX^P8 z6BiPfD)`8h4Tj+<)*B&%6p(13xBxUrr)cC@I&(HkPpk`yC1R{}3_OCmCL}Tr_=^-I0r4T4NfZICK?4$$w8v6{ za)Fx|jWsUBGK%5#r1a&(n<(R6EqgJR|Jpn+<6a^ISdHUmeb7xg#917}X0j;OZyOJ) zeSavGb<)9Wo7A32Vy))DQBX_sTDWZshM1eA3%m>Cbk`ZZqM>+uiNMf~y zqi~PmEzExIE0vdzZZ`4zeINIqTH~yBS-QHZwOjSlMdSVud?-4tR;~3lyR40;-}33? z%s!D?)@wAgtXt1|Cfl#q#}|0t$V3L+F2d(GSJ9=~IVXRPN^djD=CI z`n%OxV>+kR>qd7fY5i)@y#2Pk?cS`P=;n@u$85G6SDEm;;q8#@whu+EGnozUPe*c7 zo2uq;G|V3_nQ2`&Uf6SIVNm+T`8iMZ>(dujjkl9_ZPYavo!h2bGcL&OL$MiLcANR! z?5e83v8r^;)k|Z%xV&mAoSU!t%@ge|W{r6-6S-qsquW)Z`pvT%Wc10LM(jg{y_|iX zPvy!_+r4bm+;$qPkul8N<##5lHK!M%2RENjS4FF`AS5nDh4vmzecNq4RhEN?V&?A0 z+iC@dz3#BLoX#&Mv*n<^crIM8+1<6S<~Q?tEh|0`$M=ut&tUpDtvG7m?Jl>|dfx6_ zJrwzM3Yp$p6^vHopUHa`gRtG24$C{aN0ZleZGfakrctUkznO zIQd8@UIRit9j?)F65rBzpTfg=^l;w$EatGzPmY)Ye=9n~0k* zhLrz(Ti}pvsC;Ew&nngRbex-FvkL3#r^=samCARxE7j+)n|fHO zYRaS`0b#;v2wnuPm9PQ1z?g8xI!`3#KFBDwry@#cwc*@F%p?whRzDo_eGA28en|Kw zOR_Ba*vYc@EkIDN8*DX(JQNcl3DJRD=L`!WI1?k5*b*Eh;l{;)9ivWI9*HGUVx~m| z2pW0CD8-SuARKi@0AOk0%^^#)!jQ;nWD$wnF=C~r!ZJ@tR7??~FcttAOCzB=tt{mV zYio%0ltmc`6pAetP;rn>Go+o?L~?{V_f~0%jF(CyA|;Aa;7BFsQI_Cij1obh5gtco zxX|8UBt7Ob3JHuNA)-abI5^)zETLEjOC$sQL}03!cLEq^#&}OL4F+K=h$PM`AOZ!? zG?2!awUGsawNnA=R? z5UaI03ir7DVrD<%D%Hit&Ze%6`&j;J>2>3-(z&mFZd_Jw)8S!pl(!m9ke}M>FMSTfg;Bx2w%!+1^~t7O!L9xxGoUJjm6mSqrWGD1T_) zu7rMh9GI=$4tG0YhuhHV(~G!BAK3k1C^pUPB~`nNwu^F6YqlT9+u}zl|QZBbb^z?b*6RWYLXg0 z%7@)XJ?SR3QGRGndRPCwU)5uC-_B0Yx1)2d=ChM+R({pW)c;)mKZJNp^!11Aa`WvV z#o{;*7bOmS66L}alYbn45vQ|2|0rII-xC!R@cqerR*o9}S>D&P_Y$k&bum4D8ma_Q z^bujWdI)_wT#v^|e1~zG!ozv^aNcPaCuThf>t7~nJu!rfGO^&Lhe@yq@KXw=0$zn8 z12+USqF8#xnMTM`gBb7!gp>xzl!SGLyg3$iY)Pd$3;DR1KwgS}>C^!d`OlCThffH& zrQ!&_DKtdJ2vc4Kfdrvk6O6(7BW%fFbf6LiE^vqz8|IAG{{~5)y;FQ-_a&^>;9p1b Q-_G9t22JXk8;}P802ES8&j0`b diff --git a/tests/repository_data/repository/metadata.staged/targets/role1.json b/tests/repository_data/repository/metadata.staged/targets/role1.json index c5e3bc866133420448769ab01acbf4cb56d45b84..311d687a72ecbfd27763a26573b052d6dd312e89 100644 GIT binary patch delta 595 zcmWlW%gG)_3`Maq{FQi--RL!vRw1S6*~V8vAgLvNxTcKJW&{KG9Nk}EzrOzY`R8hv z`re}OpW&cJtVUA77?^+ij}M?zOn{vu1UE?FB8d7y(IAdl zK<4&{Ijp>6Vr18%M|b=dFE45sXDBp9<}m`jrQr=my(j{!!OeJQWE4F*>{P{vFYw*1 z2~nY32b;(X20nQ*IHe>-@tzQbb&nSgvHJjO1#)U`cELHC_8Twf4W*q2oF;6~fo08l z-(p8BFTPjLoln;x*=@-~RY$9+=suYfK5fP>i%rK_R7LXA&O2$SXg3qH00^qRbF)LP z9=U)|i2HiIF76A$FdN`}KgZ^EWVn6`z(3$Po^kcjCSSv6t>-=hnXba)s(3WE9L6m> z`DNC@>1*RcTgj&a_hQoAwV*4Bui>^Qlk8Mxa6o}-_;pkDq>Em`Xg|cM|NHy8kN)w3>zBQlbb6RaEKtmc6fh9W@=P@yvIUpf~pz77v$JfU{fBuqJ z9`#hsFS-b6fI!`+z_0bCpo*aobM5Li*YhA7Q&>3veSdwDWSU&ogM9deA|c>Wb`zb1 zHf3R!WzZQzKQiJ%2E0E_AI^C`fE zR{_zTI_$Eo#43OuCgFg=a#DA^qFz%D?N%F5@D0Cu!&(n_G0V+G*NRQ5u#ptXb)c=~ zL}TmaTchGPVY1AkMwX$l?oBDXV>TSZh*R`#33s>S9?j#qut~uV_2&1J57mEv|MC0B E|BkPqPyhe` diff --git a/tests/repository_data/repository/metadata.staged/timestamp.json b/tests/repository_data/repository/metadata.staged/timestamp.json index e94a2a893ac7bb3def5a3ecd3a51de5f281afa0e..73cd3b4c18f1b2bcbb01645f57651a25410bc321 100644 GIT binary patch literal 924 zcmXw2ORgL@4BWp{40;_Kq$qymoo^6iyh;!Rltg*P&ilYK31S$&d)WPKKn--0B3Z1e z{`PR%Zr6`be*fo7+_rb8KORn}Z~2_I&++v-GqSDGAPl!`x@m(IHb9_6 zbSl=R6zFw2TN%ACG({e3VqL2=1!kvG$=Wd1s>%)~y4DJcHpqp*lvE?ItQuJ_5-_!L zb+aCl_0~!@Efd>l#4(}Fg*av(9<^ZMmEXkZu@_Z^mX$J>pjy#|m1Qjv;DC{DtRM{VDD9=NM)#0D^Ug4$T0Ey=CrMyqB#t6Nw78j= zEKUW12#uSth4-Y?2ja{PQJiNi?l5|AO5bNI1C`jU;sUWglp+}lYqNJj)Hb7uwlPO~ zq^63Fv?!NZv(_zr^8MlTXZqzZW$bR*&A9z_|N0WUMLt{~<97GQm%}+*{P%Kw_3f4f z!W+Om9{}DR=l9o3X*~bYDEmJ7%kA=ffBWh7{PZ(=+Ae;(9R5GNpA&BvFD(1W)uuxS z?>>plDFs^tU3T!(#y~doo^FD4R7?3rBn RHM#v8UvAd}6h375`~O&Q_Kg4l literal 924 zcmXw2Nv_;R4Bh`zG_;NlvWioA=NklRuMz|SS%bUn`~mJx5X11@L+RUq1PG+`rh5AJ zaN2Iyk57L8=gYip?@oU_oKD~JK5d`p*Xzj1R@j@`?sy2e~i zlbS9Z5@VT{*acPLDQ$GkXDXFe60f?rgJX*1j+0j*FO!{|Z5VUw3r3Mv)JA6wPng!y z4~|6ThNi1DrRK`OTAPlts#C)-BvP9+(L-zjVhI_t0yB~sjVTz3bxeUK)x?NFX@+91 zCk>(ewh0I|V)Ia=St$jZX@&5G0wL;Yvem|JMPkoW3k6JLL!gnIeK(;*2=2k)YuFMf z?h;lNRU`AO;LVyUBT{Mv>M4c?GId3CHw~3$Lx5XM4q^#*QQ$rxw0m0>g;TV)=xq>Y z{1HAAu8z{13&3hZ#SJ=yG0aqNTq?7+6=YTl#_SnvGBJ%>xel~0*rq9G!k8LpG@jwn zLx*nJlkX3wKeI1KDd*mny^Pym_pdK=ugHh%!2P~i<=ULOG7 z9oO%#FJ3@~8>iMWc%}Kwj7KI|0|cSxDKJ>uJG#3{U&l{y+WwwTAMcmM#cHr`((vnY`*(i1 MT@O+CkmK+F0~zn~cmMzZ diff --git a/tests/repository_data/repository/metadata/root.json b/tests/repository_data/repository/metadata/root.json index 9174375e06a09dee6d59f428d3f2fbb7d386ed54..b1b34fa314f9ea032a9ac600bf4a5ff167642ce4 100644 GIT binary patch literal 3756 zcmd6qS+lB0635@~r#Nw5oepcGs^~XF+{ImwjuRa~6$%0(tJ2Zmy-Ux%9X%5hGu?9^ zdJJ4B%Bp{6{>V&z{_^&o2dBg-%1q|(Kfe9_%iG(}>izb9k&i&Ak$1_Mkkr*A_ZUNj zU_f1@>k=Sc0}wIC1(^AQ69hQc)R)hZuTjsVSW-yY``=#c#j-eGUkff*#%g8!Fkh{{ zUQ!C48&T;3M3623!BT_7bp(VOlhng5;ywo&cA4&~Wd%W!awwG+U)P0)T%Q0(x)68_ zQ{d^Y<`U_+9?=-le2=-FWC(I1kOq7RknmjJ)pX!+h;#sfBmf}-Jzqz_LB9K(z>u0L zT#c#efKw=>=PMOR0z%X->QBC>N#a3>1i>0`T&ZInNC)TwGnaYX;hvHpG$NT2=W^Z0 zQgT9x=VRTK3?mEyKmZWNl_^lUkrD=!{lJ4*_{ymsg+4`?Bcfy>A=J7=gVgmsU%6Il zp47OGA){&|;_K9bp65U&iE@}Hh4QD6${Rj@4kKUr)>Yo6j7t?#j4>5RnNJ9$Ncb98 z4i_8(M-x5(!WXWxNCjCFNFok(M;QhvV+3ewLnVs=$tb~07X(mGg@$04Gl&rs5c@=6 zN*UH%h?FyQs7diOR65Aj6(vMLgpsC6<|=!1@vb=e<;&YY6fZ9*<&%~tjPE}ck5oPp zSzfQj3z~QDn&d%V$fWowPn8`mO-JXJX_Xoak;fRdJi7TUi-0P*A zrn%OD4X#zDksx=KLhzROj{P`>I_yJC%xxZd`$E(+sAw6Z0WMMmfB1n z%5=~MgU7V4SXeqO$8PNVy-VBoy?Z9(6okEtgpJk7uXg5f<;i;3H5Nwan7o2x+1-DQ z*OTEF#p>fT?|par{L1GGpuYyI`)!HI-f7w8nY5ZH4CX_2Oajs0)a#rYleit%ozdv9 z+{VL2xb2R`nCkvLK5ORvqZBk6t(~;&mWA1yRkLv)8jdxX9+N|xx;{h)w)vgow$aWAL?&Ra67q<4vvsj9Cda+?yjfHxFaGl z95(&Kf=w#G`yGDYJ}_ZuL6d}wbp-ki-jgz%-dyh#b%?_Dm_+{zRuXWBae=TZK-3dJ z*9lcg%ZDn-Ll-!{PCUej04fx~^9A9$s?!)sB$+CZzvZE4q5o^JI-B^CU(JnmvquNN z8sh9UQ~@b3$vVQ_ML6RFQX4wo!-KzDnf;}C>9R(z%_3QPbC9*WV%fbm%`gGAHlOVu zhoW_hj)^nxh~QN3wuhW#9fmi29-iBWMSHPAx8$^l>^Qxow3nRqUK8II@h}97eSI+L zdR}(g=$MFYp16-qw`diU0qL1G<-C|xTirL-Gla5ct$i4>YiskQMsiF0F6?UWUQ*vs2^GK%s^@>uof+G!Oep$egEb8|gvTI=)58TO)r z&}C{DmoU~28@>9Dz%dXMu3FEWb%ASqf?<-p%zCC_CA3naj zXuituvy*(TR-9EQc)mu@77ad!zLw_SHI6S;Umtq@Nyzu6**_$taI%vuerrYljgIeb nPXCyWAeMRI#OZ$s&39*r|Dr@){sg9ySsq-I&zSx4<(FRp)5AQF literal 3756 zcmd5RP29{Y#4_;^XN(w< za8Fu*5U`1a){~xLN@9(frwLCmkU|(nC6w5c2w{Su(64{{w0AJ&CjT@TO&XI)2? z@@>g^@Z5+fA{0hMQH_Pikp_}5>0w0?Q-UW%VP!Cs8d)hc2PC0_0%<($aRL*9EXTlO zo~O8mfK%qYR00Z2s5S{k6cEn}%o9YoGSXV4oUfQL_gDeBq5^s}@jOpTsv(4oQY{iH zl>m}jPXM48NMItc&oQLrL@c8~C)}OdL54`_ z!mkM9L@VG~KrIJMxuYoQk^+&zuMQ`_e0lkY!^2ItlSsoiMLmCKc-(H{R#mBKLF3YQviPad{#fmv4;o|Mc&-N@JvVMOZhaLE?%Q%p(J8vM8!^w#I-|b2 zfnsx=|i@wXU*Z6)>xo_ua33oUyEx^fnxv8Rcc39X- zgtR0Q4v3N;^M3gzW`p{5z{ainB(#W_x@b(Y-1 zW@FrNwmy^78aDqmUr!8wqS*a>fqaKDJkpyz<>X1=QJnbqMC$6562MJ}=b8NyL z;n)OOfuvB(7%5Sregn*tk^euistz5GA5z__g@;Kq80X^+T4!2KJ6EvHJUQ*1s29P` z^4xw~T}5<0Q*zIu{%kp-%cG=`UyY;L?RLT?zSbu*6)_w(*IBL4^G>ot{n6PD@5po) zWjoEP>sZUG!~q%KPTRx%W6GPXFWR-~d$Ya4P!CnS|G13P`Z}}CbFG{Nptfp7i`#M8 z+*O@<_wWv8sC4{k5qBFgO1V-J?90&K<&AFHjKp9#suqnx?%24BcHugERNIwpcHS2iN{3M= zmi_o~tKs7k#D_;U7?OtXy9)BAAwPcfPt(J&Ke>08w0~GU!t|cF;u!yVSP6ugbe+bb z$MuaT`wC8#>pTWys-@D7lRc6c$J8Q0)H1DvBt%=!HEUzgUjSD3x4dk(vm)AGYNv(V zD_D+QLW*oy7GZtgyfRU^l9bzFdruc0+|*7;7M%{nT%LVpl+9Bj%I=}rAM8=9`{=i( z;ccBw-#&Wx$GLaV!Rowktw!-82o}|B;S@wX#Jy|OocqHr2KOC`ucP*S>m@l2+PzLK zu7#(}ac;C2p=y4gY{XE)1)PfRy*C%D<;kS-{&qaN(Qr}=?lXPBwY)w$l(x&4@*~=) zIP9I)*%(IN;aK>Bgtt9f*~xMf1L)QEx9nr~IN|70jwh!a-#hRscgH637gz{!UHjUC{Me;4Q_aF^n$@Q(dPwQJa%- zTAF2Voh|+cwsLZaTEabL0f24<2y|-dsQ|dsc@jG#nH(yQx8=6?NbPpNmp6T9WaoVUdBgZBw$4u>_j|#VCcj=ko|ljB z)?%M+_^BkH*2;}jf}4E*)RW$G>RW66opAj0>f5E~_l|sbJ^Ud@N_qY;B}rQuEz<=D&F2mcIh#YR*NPXJ2#n%a>pN2@5MgEdT%j diff --git a/tests/repository_data/repository/metadata/snapshot.json b/tests/repository_data/repository/metadata/snapshot.json index b4ad9096b92f7fce68872ee44a9f4bbe2133b4b2..017cb34e3f9804c3db056525204f5d1f78e395be 100644 GIT binary patch literal 1380 zcma)+!Hyd@42JLf6vmvJloTbA%q?$F)IGH*3QDBbo2Ki<+AW#}dH2$@>8V&Cz<3Un z=I75}pZxjmuw5SRAMN_*b6mD}hhOdvhtK&tY`@2+2hYkD0b(p(Xd?FMpjvx#S-8}3 zfGw@FaCPXqU@otsKuj1Vc*>4;{&;+UJidSP!}0iaDIMGzNznn!9tBD* z9!+|ItV{-rYC3|bGeuA(jgTl!rq@Aiu=-dWldG_3^yRCU>0JN`FvW;TDUQ0B!>wS|+BAGf$Vi8y0=8UQ+$N?b(o&nfECR2!)U~EJ z09G6p02Rt=ow$KbU8_z~ETuykHVRlb@D^MxXC zU?%|~Mo#Z4g_(M0hize)$y7n<%c_@REd?`SpzbgRRZy(Aaa;m2L~AUibqPvtI$L&P zh1q7-b<}Wi;iWy>wkKz3OYu@((l3(!*;&Ed$7x&PxHLJJN$BUIDF3cu>C$i-AAr$hMiWI;tQbWAN1-nhKq>d zFf`W~5xf9fSa-xa#w>%W7DUGFyFK0?^Kv}x0e`%Ge|!7>%@4P?U$;`hH4)bq(ilZU zQEd&70u(j8b&zOSno%s3=GwJ$r7F!zkeDp)(gkODC=Xr}3_=XNVFW2xi4@Zu7qG+R zfezle!;DHehgKa>TUJ1&%tE7q57I1>2w6>g0|cvPJb*FWKw2$JX_}ij&DNunMMYY5 z%~rs&2G3JgF})eutk=3Sv}9&ka|sujrOoAmO$Tu&W+~iwP#@iAnpa~^S8Z!lsEbxx zM4CR7A*(f+S^^Z)9&KextSEVtBje^_=c!SK6((w3p?h^$E#0Z0V~tW{Il3cdTTO*I z7HAQG2w=$O#$a`=rQ{No^g>z&vr=bRaNh5fwS$R_RuwXOeyvl}wJIZE;7YkkOHCeT zYDy)%44cl-16^P%LmsPUef{^1z8X^@fH9;cKx!LNNrUw8`jOJ~QJ~*X|c_X4RqP!rh zXnRT4+q-{WW!Um=-V2KN(7AG;XmSGHbmq7PtgEkS~w~YgmGqW7)?n5faKw~p3McgoEN?Qphb@eqEF^{RTKyzQqc09i|?LXaA X#$8)}_J(eM&8PGIPFT4~p1=GHZOLY= diff --git a/tests/repository_data/repository/metadata/targets.json b/tests/repository_data/repository/metadata/targets.json index e9da47bb59db2882c5dffd99a8ddc120e14b5fad..04760cd24cea01bc6c405203496bdbeadeaddd96 100644 GIT binary patch delta 1093 zcmbu7OLF2j5XLKysVwIRv&kmK0;%(3PDWTk&XJ?yQFgItVH;Rs zO6Y&Y!0G*u?}xu;$IIcrbYhWoF%KXzC|3X`nchGB z@cU6Wh~oA|b+@_JJ$zoCt$S`(eOc^#;<9$PQm4Svxg8$1r|rc`)^FPVg^{i6QD^*R zGw$Dh-OHyt!OxB5;h_#X9wy_7IP7Lsv-h|h-JeX&Q*&WElj&ssdTr z6$YLD;rx1ayJ|HX)y3rD{`u^MaCm#3rro>4)o#(6cYD{fYPf&@=|lLxX8rxwhkw5N E7vwrajsO4v delta 1093 zcmbu7yHaCE5QfW_ErYB9BP`YMD|rNjqyeiI9{s!E=a6pkty; zE=rRru=84n42mEn`B(h;;DH^Khy*l*=#u6Tyha-jpun%Pj~-XQ2#YCs<(CK_QcqhE+t&R@noh1u&%!kf+kzS(e&@ zV~-rA%9OA`0D~zsiGowY1?P~o0IefiN5V(4P9TxUWFSk@I@5dmoHL0M^d>@9UKB>L*+;d+6ze# zh$9zmtT8JM3QR22h|+{2-Da|iizt>DNV8HwWRRGhV=UMjN(af>sv@`Wls&tgvd&RE z^4|*z^!Des^)Ds{_uinW)aZ(A=w4Eel%zIu>n%kDmp_n(KS z&%N<)*S#J#cSoxIQvL3`O}mV%K{ab>xSzb7RX$AaW52Fk%kyU8uJ^-g+Mr%9Tst?X zPt_rJx;JA|^CKRejLbeS2dmpA*Vv!6%Y_*Chey59ML(@C&TQ}Q?s_}yZ%(vW*s$2e zU3|FQY**{S_IR;+zKg@li<7!dwAu7JslT7KliuaVxTo8x-|?<_c`@AVQhzLu^QxMt zn`vX>wpTw@_T_jGGOaq@Fy8LYZ_;u(s2(0~N3-@*BTiNQIB1^Ja(8jxbVg^>{@hKM zU++fKto{8<8b_+lWPP-@?ZekFzlW3Qr`oyh`F-W}tZlB}e)z!ucUk}Z{o&tl{sWR~ BL{9(! diff --git a/tests/repository_data/repository/metadata/targets.json.gz b/tests/repository_data/repository/metadata/targets.json.gz index e2d2dbe0e05d5ba37e3a7d7ee9a5643195d7e4cc..9e6ad7d2a323694978a84d59aab57ecc347ce781 100644 GIT binary patch literal 1202 zcmV;j1Wo%NiwFSnd5}{A|E*QqZW>7tefL)&UP+d|U-C9!u!+GKgKf+@T2Qt3N>OXX>No&=1P zjS>!|m)2REk_?Au7K2qm0n@=6LInjEKzPlG0%EzNl5%RLBqEeQ9Ps9GlP?Z{*Zr*D z&puxD`)^B_!9LEJl)`yOjV53a(-Lot7Rd(3lH`!Ap&_y;Bcp^{CKR=XNzh&p3IQ)0 z9f?tMm9&zUC#H#Enk5rtK+Hr5Gbv(boFE?X5K?5!TD;>vs$`Xo!gB6{-Pi0nSX^P8 z6BiPfD)`8h4Tj+<)*B&%6p(13xBxUrr)cC@I&(HkPpk`yC1R{}3_OCmCL}Tr_=^-I0r4T4NfZICK?4$$w8v6{ za)Fx|jWsUBGK%5#r1a&(n<(R6EqgJR|Jpn+<6a^ISdHUmeb7xg#917}X0j;OZyOJ) zeSavGb<)9Wo7A32Vy))DQBX_sTDWZshM1eA3%m>Cbk`ZZqM>+uiNMf~y zqi~PmEzExIE0vdzZZ`4zeINIqTH~yBS-QHZwOjSlMdSVud?-4tR;~3lyR40;-}33? z%s!D?)@wAgtXt1|Cfl#q#}|0t$V3L+F2d(GSJ9=~IVXRPN^djD=CI z`n%OxV>+kR>qd7fY5i)@y#2Pk?cS`P=;n@u$85G6SDEm;;q8#@whu+EGnozUPe*c7 zo2uq;G|V3_nQ2`&Uf6SIVNm+T`8iMZ>(dujjkl9_ZPYavo!h2bGcL&OL$MiLcANR! z?5e83v8r^;)k|Z%xV&mAoSU!t%@ge|W{r6-6S-qsquW)Z`pvT%Wc10LM(jg{y_|iX zPvy!_+r4bm+;$qPkul8N<##5lHK!M%2RENjS4FF`AS5nDh4vmzecNq4RhEN?V&?A0 z+iC@dz3#BLoX#&Mv*n<^crIM8+1<6S<~Q?tEh|0`$M=ut&tUpDtvG7m?Jl>|dfx6_ zJrwzM3Yp$p6^vHopUHa`gRtG24$C{aN0ZleZGfakrctUkznO zIQd8@UIRit9j?)F65rBzpTfg=^l;w$EatGzPmY)Ye=9n~0k* zhLrz(Ti}pvsC;Ew&nngRbex-FvkL3#r^=samCARxE7j+)n|fHO zYRaS`0b#;v2wnuPm9PQ1z?g8xI!`3#KFBDwry@#cwc*@F%p?whRzDo_eGA28en|Kw zOR_Ba*vYc@EkIDN8*DX(JQNcl3DJRD=L`!WI1?k5*b*Eh;l{;)9ivWI9*HGUVx~m| z2pW0CD8-SuARKi@0AOk0%^^#)!jQ;nWD$wnF=C~r!ZJ@tR7??~FcttAOCzB=tt{mV zYio%0ltmc`6pAetP;rn>Go+o?L~?{V_f~0%jF(CyA|;Aa;7BFsQI_Cij1obh5gtco zxX|8UBt7Ob3JHuNA)-abI5^)zETLEjOC$sQL}03!cLEq^#&}OL4F+K=h$PM`AOZ!? zG?2!awUGsawNnA=R? z5UaI03ir7DVrD<%D%Hit&Ze%6`&j;J>2>3-(z&mFZd_Jw)8S!pl(!m9ke}M>FMSTfg;Bx2w%!+1^~t7O!L9xxGoUJjm6mSqrWGD1T_) zu7rMh9GI=$4tG0YhuhHV(~G!BAK3k1C^pUPB~`nNwu^F6YqlT9+u}zl|QZBbb^z?b*6RWYLXg0 z%7@)XJ?SR3QGRGndRPCwU)5uC-_B0Yx1)2d=ChM+R({pW)c;)mKZJNp^!11Aa`WvV z#o{;*7bOmS66L}alYbn45vQ|2|0rII-xC!R@cqerR*o9}S>D&P_Y$k&bum4D8ma_Q z^bujWdI)_wT#v^|e1~zG!ozv^aNcPaCuThf>t7~nJu!rfGO^&Lhe@yq@KXw=0$zn8 z12+USqF8#xnMTM`gBb7!gp>xzl!SGLyg3$iY)Pd$3;DR1KwgS}>C^!d`OlCThffH& zrQ!&_DKtdJ2vc4Kfdrvk6O6(7BW%fFbf6LiE^vqz8|IAG{{~5)y;FQ-_a&^>;9p1b Q-_G9t22JXk8;}P802ES8&j0`b diff --git a/tests/repository_data/repository/metadata/targets/role1.json b/tests/repository_data/repository/metadata/targets/role1.json index c5e3bc866133420448769ab01acbf4cb56d45b84..311d687a72ecbfd27763a26573b052d6dd312e89 100644 GIT binary patch delta 595 zcmWlW%gG)_3`Maq{FQi--RL!vRw1S6*~V8vAgLvNxTcKJW&{KG9Nk}EzrOzY`R8hv z`re}OpW&cJtVUA77?^+ij}M?zOn{vu1UE?FB8d7y(IAdl zK<4&{Ijp>6Vr18%M|b=dFE45sXDBp9<}m`jrQr=my(j{!!OeJQWE4F*>{P{vFYw*1 z2~nY32b;(X20nQ*IHe>-@tzQbb&nSgvHJjO1#)U`cELHC_8Twf4W*q2oF;6~fo08l z-(p8BFTPjLoln;x*=@-~RY$9+=suYfK5fP>i%rK_R7LXA&O2$SXg3qH00^qRbF)LP z9=U)|i2HiIF76A$FdN`}KgZ^EWVn6`z(3$Po^kcjCSSv6t>-=hnXba)s(3WE9L6m> z`DNC@>1*RcTgj&a_hQoAwV*4Bui>^Qlk8Mxa6o}-_;pkDq>Em`Xg|cM|NHy8kN)w3>zBQlbb6RaEKtmc6fh9W@=P@yvIUpf~pz77v$JfU{fBuqJ z9`#hsFS-b6fI!`+z_0bCpo*aobM5Li*YhA7Q&>3veSdwDWSU&ogM9deA|c>Wb`zb1 zHf3R!WzZQzKQiJ%2E0E_AI^C`fE zR{_zTI_$Eo#43OuCgFg=a#DA^qFz%D?N%F5@D0Cu!&(n_G0V+G*NRQ5u#ptXb)c=~ zL}TmaTchGPVY1AkMwX$l?oBDXV>TSZh*R`#33s>S9?j#qut~uV_2&1J57mEv|MC0B E|BkPqPyhe` diff --git a/tests/repository_data/repository/metadata/timestamp.json b/tests/repository_data/repository/metadata/timestamp.json index e94a2a893ac7bb3def5a3ecd3a51de5f281afa0e..73cd3b4c18f1b2bcbb01645f57651a25410bc321 100644 GIT binary patch literal 924 zcmXw2ORgL@4BWp{40;_Kq$qymoo^6iyh;!Rltg*P&ilYK31S$&d)WPKKn--0B3Z1e z{`PR%Zr6`be*fo7+_rb8KORn}Z~2_I&++v-GqSDGAPl!`x@m(IHb9_6 zbSl=R6zFw2TN%ACG({e3VqL2=1!kvG$=Wd1s>%)~y4DJcHpqp*lvE?ItQuJ_5-_!L zb+aCl_0~!@Efd>l#4(}Fg*av(9<^ZMmEXkZu@_Z^mX$J>pjy#|m1Qjv;DC{DtRM{VDD9=NM)#0D^Ug4$T0Ey=CrMyqB#t6Nw78j= zEKUW12#uSth4-Y?2ja{PQJiNi?l5|AO5bNI1C`jU;sUWglp+}lYqNJj)Hb7uwlPO~ zq^63Fv?!NZv(_zr^8MlTXZqzZW$bR*&A9z_|N0WUMLt{~<97GQm%}+*{P%Kw_3f4f z!W+Om9{}DR=l9o3X*~bYDEmJ7%kA=ffBWh7{PZ(=+Ae;(9R5GNpA&BvFD(1W)uuxS z?>>plDFs^tU3T!(#y~doo^FD4R7?3rBn RHM#v8UvAd}6h375`~O&Q_Kg4l literal 924 zcmXw2Nv_;R4Bh`zG_;NlvWioA=NklRuMz|SS%bUn`~mJx5X11@L+RUq1PG+`rh5AJ zaN2Iyk57L8=gYip?@oU_oKD~JK5d`p*Xzj1R@j@`?sy2e~i zlbS9Z5@VT{*acPLDQ$GkXDXFe60f?rgJX*1j+0j*FO!{|Z5VUw3r3Mv)JA6wPng!y z4~|6ThNi1DrRK`OTAPlts#C)-BvP9+(L-zjVhI_t0yB~sjVTz3bxeUK)x?NFX@+91 zCk>(ewh0I|V)Ia=St$jZX@&5G0wL;Yvem|JMPkoW3k6JLL!gnIeK(;*2=2k)YuFMf z?h;lNRU`AO;LVyUBT{Mv>M4c?GId3CHw~3$Lx5XM4q^#*QQ$rxw0m0>g;TV)=xq>Y z{1HAAu8z{13&3hZ#SJ=yG0aqNTq?7+6=YTl#_SnvGBJ%>xel~0*rq9G!k8LpG@jwn zLx*nJlkX3wKeI1KDd*mny^Pym_pdK=ugHh%!2P~i<=ULOG7 z9oO%#FJ3@~8>iMWc%}Kwj7KI|0|cSxDKJ>uJG#3{U&l{y+WwwTAMcmM#cHr`((vnY`*(i1 MT@O+CkmK+F0~zn~cmMzZ diff --git a/tests/test_indefinite_freeze_attack.py b/tests/test_indefinite_freeze_attack.py index 2ce817d9..669864e9 100755 --- a/tests/test_indefinite_freeze_attack.py +++ b/tests/test_indefinite_freeze_attack.py @@ -183,8 +183,12 @@ def test_without_tuf(self): tuf.formats.check_signable_object_format(timestamp_metadata) with open(timestamp_path, 'wb') as file_object: - json.dumps(timestamp_metadata, file_object, indent=1, sort_keys=True).encode('utf-8') - + # Explicitly specify the JSON separators for Python 2 + 3 consistency. + timestamp_content = \ + json.dumps(timestamp_metadata, indent=1, separators=(',', ': '), + sort_keys=True).encode('utf-8') + file_object.write(timestamp_content) + client_timestamp_path = os.path.join(self.client_directory, 'timestamp.json') shutil.copy(timestamp_path, client_timestamp_path) diff --git a/tuf/client/updater.py b/tuf/client/updater.py index 0fb06c0e..6d57ba9b 100755 --- a/tuf/client/updater.py +++ b/tuf/client/updater.py @@ -2008,8 +2008,8 @@ def refresh_targets_metadata_chain(self, rolename): # This only goes to -1 because we only want to store the parents (so we # ignore the last element). for next_role in parts[1:-1]: - parent_roles.append(roles_added+'/'+next_role) - roles_added = roles_added+'/'+next_role + parent_roles.append(roles_added + '/' + next_role) + roles_added = roles_added + '/' + next_role message = 'Minimum metadata to download and set the chain of trust: '+\ repr(parent_roles)+'.' @@ -2024,7 +2024,7 @@ def refresh_targets_metadata_chain(self, rolename): if parent_role not in targets_metadata_allowed: message = '"snapshot.json" does not provide all the parent roles '+\ - 'of '+repr(rolename)+'.' + 'of ' + repr(rolename) + '.' raise tuf.RepositoryError(message) # Remove the 'targets' role because it gets updated when the targets.json @@ -2042,7 +2042,7 @@ def refresh_targets_metadata_chain(self, rolename): # Sort the roles so that parent roles always come first. parent_roles.sort() - logger.debug('Roles to update: '+repr(parent_roles)+'.') + logger.debug('Roles to update: ' + repr(parent_roles) + '.') # Iterate 'parent_roles', load each role's metadata file from disk, and # update it if it has changed. diff --git a/tuf/download.py b/tuf/download.py index 43471737..5c9c20b4 100755 --- a/tuf/download.py +++ b/tuf/download.py @@ -204,7 +204,7 @@ def _download_fixed_amount_of_data(connection, temp_file, required_length): data = b'' read_amount = min(tuf.conf.CHUNK_SIZE, required_length - number_of_bytes_received) - logger.debug('Reading next chunk...') + #logger.debug('Reading next chunk...') try: data = connection.read(read_amount) @@ -225,8 +225,8 @@ def _download_fixed_amount_of_data(connection, temp_file, required_length): seconds_spent_receiving = stop_time - start_time if (seconds_spent_receiving + grace_period) < 0: - logger.debug('Ignoring average download speed for another: '+\ - str(-seconds_spent_receiving) + ' seconds') + #logger.debug('Ignoring average download speed for another: '+\ + #str(-seconds_spent_receiving) + ' seconds') continue # Measure the average download speed. diff --git a/tuf/repository_lib.py b/tuf/repository_lib.py index c2e7a5a5..4e7530ce 100755 --- a/tuf/repository_lib.py +++ b/tuf/repository_lib.py @@ -460,8 +460,11 @@ def _get_written_metadata_and_digests(metadata_signable): its digest. """ - written_metadata_content = json.dumps(metadata_signable, indent=1, - sort_keys=True).encode('utf-8') + # Explicitly specify the JSON separators for Python 2 + 3 consistent. + written_metadata_content = \ + json.dumps(metadata_signable, indent=1, separators=(',', ': '), + sort_keys=True).encode('utf-8') + written_metadata_digests = {} for hash_algorithm in tuf.conf.REPOSITORY_HASH_ALGORITHMS: From 21bbbedbb85fb07c851f618d6eb974d80c6e5cf0 Mon Sep 17 00:00:00 2001 From: vladdd Date: Thu, 5 Jun 2014 19:09:45 -0400 Subject: [PATCH 16/32] Fix test_util.py test case failure in py2.7. --- tests/test_slow_retrieval_attack.py | 65 +---------------------------- tests/test_util.py | 33 ++++++++++----- tuf/util.py | 3 +- 3 files changed, 26 insertions(+), 75 deletions(-) diff --git a/tests/test_slow_retrieval_attack.py b/tests/test_slow_retrieval_attack.py index 9162062d..11b09bbd 100755 --- a/tests/test_slow_retrieval_attack.py +++ b/tests/test_slow_retrieval_attack.py @@ -210,69 +210,6 @@ def tearDown(self): unittest_toolbox.Modified_TestCase.tearDown(self) - """ - def test_without_tuf_mode_1(self): - # Simulate a slow retrieval attack. - # 'mode_1': When download begins,the server blocks the download - # for a long time by doing nothing before it sends the first byte of data. - - # Retrieve 'file1.txt' provided by the pre-generated repository. - url_prefix = self.repository_mirrors['mirror1']['url_prefix'] - url_file = os.path.join(url_prefix, 'targets', 'file1.txt') - client_filepath = os.path.join(self.client_directory, 'file1.txt') - - # Generate the fileinfo of 'file.txt' to compare it to what is downloaded. - # The download should complete, albeit slowly (the slow retrieval server - # sets a limit on the delay.) - filepath = os.path.join(self.repository_directory, 'targets', 'file1.txt') - length, hashes = tuf.util.get_file_details(filepath) - fileinfo = tuf.formats.make_fileinfo(length, hashes) - - try: - server_process = self._start_slow_server('mode_1') - six.moves.urllib.request.urlretrieve(url_file, client_filepath) - - # Verify the expected file size and hash of the downloaded file. - length, hashes = tuf.util.get_file_details(client_filepath) - download_fileinfo = tuf.formats.make_fileinfo(length, hashes) - self.assertEqual(fileinfo, download_fileinfo) - - finally: - # Terminate the slow retrieval (mode 1) server. - self._stop_slow_server(server_process) - - - - def test_without_tuf_mode_2(self): - # Simulate a slow retrieval attack. - # 'mode_1': When download begins, the server blocks the download for a long - # time by doing nothing before it sends the first byte of data. - - url_prefix = self.repository_mirrors['mirror1']['url_prefix'] - url_file = os.path.join(url_prefix, 'targets', 'file1.txt') - client_filepath = os.path.join(self.client_directory, 'file1.txt') - - # Generate the fileinfo of 'file.txt' to compare it to what is downloaded. - # The download should complete, albeit slowly (the slow retrieval server - # sets a limit on the delay.) - filepath = os.path.join(self.repository_directory, 'targets', 'file1.txt') - length, hashes = tuf.util.get_file_details(filepath) - fileinfo = tuf.formats.make_fileinfo(length, hashes) - - try: - server_process = self._start_slow_server('mode_2') - six.moves.urllib.request.urlretrieve(url_file, client_filepath) - - # Verify the expected file size and hash of the downloaded file. - length, hashes = tuf.util.get_file_details(client_filepath) - download_fileinfo = tuf.formats.make_fileinfo(length, hashes) - self.assertEqual(fileinfo, download_fileinfo) - - finally: - # Terminate the slow retrieval (mode 2) server. - self._stop_slow_server(server_process) - """ - def test_with_tuf_mode_1(self): # Simulate a slow retrieval attack. @@ -297,6 +234,7 @@ def test_with_tuf_mode_1(self): # Verify that 'file1.txt' is the culprit. self.assertEqual(url_file, mirror_url) + print(repr(mirror_error)) self.assertTrue(isinstance(mirror_error, tuf.DownloadLengthMismatchError)) else: @@ -330,6 +268,7 @@ def test_with_tuf_mode_2(self): # Verify that 'file1.txt' is the culprit. self.assertEqual(url_file, mirror_url) + print(repr(mirror_error)) self.assertTrue(isinstance(mirror_error, tuf.DownloadLengthMismatchError)) else: diff --git a/tests/test_util.py b/tests/test_util.py index 22bfd30b..9c1c20ef 100755 --- a/tests/test_util.py +++ b/tests/test_util.py @@ -65,11 +65,14 @@ def test_A1_tempfile_close_temp_file(self): def _extract_tempfile_directory(self, config_temp_dir=None): - """[Helper] Takes a directory (essentially specified in the conf.py as - 'temporary_directory') and substitutes tempfile.TemporaryFile() with - tempfile.mkstemp() in order to extract actual directory of the stored - tempfile. Returns the config's temporary directory (or default temp - directory) and actual directory.""" + """ + Takes a directory (essentially specified in the conf.py as + 'temporary_directory') and substitutes tempfile.TemporaryFile() with + tempfile.mkstemp() in order to extract actual directory of the stored + tempfile. Returns the config's temporary directory (or default temp + directory) and actual directory. + """ + # Patching 'tuf.conf.temporary_directory'. tuf.conf.temporary_directory = config_temp_dir @@ -102,12 +105,20 @@ def test_A2_tempfile_init(self): # directory. The location of the temporary files is set in 'tuf.conf.py'. # Test: Expected input verification. - config_temp_dirs = [None, self.make_temp_directory()] - for config_temp_dir in config_temp_dirs: - config_temp_dir, actual_dir = \ - self._extract_tempfile_directory(config_temp_dir) - self.assertEqual(config_temp_dir, actual_dir) - + # Assumed 'tuf.conf.temporary_directory' is 'None' initially. + temp_file = tuf.util.TempFile() + temp_file_directory = os.path.dirname(temp_file.temporary_file.name) + self.assertEqual(tempfile.gettempdir(), temp_file_directory) + + saved_temporary_directory = tuf.conf.temporary_directory + temp_directory = self.make_temp_directory() + tuf.conf.temporary_directory = temp_directory + temp_file = tuf.util.TempFile() + temp_file_directory = os.path.dirname(temp_file.temporary_file.name) + self.assertEqual(temp_directory, temp_file_directory) + + tuf.conf.temporary_directory = saved_temporary_directory + # Test: Unexpected input handling. config_temp_dirs = [self.random_string(), 123, ['a'], {'a':1}] for config_temp_dir in config_temp_dirs: diff --git a/tuf/util.py b/tuf/util.py index d1031d44..a847fd47 100755 --- a/tuf/util.py +++ b/tuf/util.py @@ -87,10 +87,11 @@ def __init__(self, prefix='tuf_temp_'): """ self._compression = None + # If compression is set then the original file is saved in 'self._orig_file'. self._orig_file = None temp_dir = tuf.conf.temporary_directory - if temp_dir is not None and isinstance(temp_dir, str): + if temp_dir is not None and tuf.formats.PATH_SCHEMA.matches(temp_dir): try: self.temporary_file = tempfile.NamedTemporaryFile(prefix=prefix, dir=temp_dir) From 091cfe9aebfc13e2891232702b97ca3748d7e67f Mon Sep 17 00:00:00 2001 From: Vladimir Diaz Date: Fri, 6 Jun 2014 07:32:03 -0400 Subject: [PATCH 17/32] Increase sleep time after starting simple server in affected tests. --- tests/test_endless_data_attack.py | 2 +- tests/test_extraneous_dependencies_attack.py | 2 +- tests/test_slow_retrieval_attack.py | 2 +- tuf/__init__.py | 38 ++++++++++---------- 4 files changed, 23 insertions(+), 21 deletions(-) diff --git a/tests/test_endless_data_attack.py b/tests/test_endless_data_attack.py index 816855ee..ffc3dba9 100755 --- a/tests/test_endless_data_attack.py +++ b/tests/test_endless_data_attack.py @@ -85,7 +85,7 @@ def setUpClass(cls): # NOTE: Following error is raised if a delay is not applied: # - time.sleep(.2) + time.sleep(.5) diff --git a/tests/test_extraneous_dependencies_attack.py b/tests/test_extraneous_dependencies_attack.py index 642dd405..52bf7ba5 100755 --- a/tests/test_extraneous_dependencies_attack.py +++ b/tests/test_extraneous_dependencies_attack.py @@ -87,7 +87,7 @@ def setUpClass(cls): # NOTE: Following error is raised if a delay is not applied: # - time.sleep(.2) + time.sleep(.7) diff --git a/tests/test_slow_retrieval_attack.py b/tests/test_slow_retrieval_attack.py index 9162062d..2713413b 100755 --- a/tests/test_slow_retrieval_attack.py +++ b/tests/test_slow_retrieval_attack.py @@ -103,7 +103,7 @@ def _start_slow_server(self, mode): # NOTE: Following error is raised if a delay is not applied: # - time.sleep(.2) + time.sleep(.5) return server_process diff --git a/tuf/__init__.py b/tuf/__init__.py index a422a418..4b9d3b89 100755 --- a/tuf/__init__.py +++ b/tuf/__init__.py @@ -76,7 +76,7 @@ def __init__(self, exception): def __str__(self): # Show the original exception. - return str(self.exception) + return repr(self.exception) @@ -98,8 +98,8 @@ def __init__(self, expected_hash, observed_hash): self.observed_hash = observed_hash def __str__(self): - return 'Observed hash ('+str(self.observed_hash)+\ - ') != expected hash ('+str(self.expected_hash)+')' + return 'Observed hash (' + repr(self.observed_hash)+\ + ') != expected hash (' + repr(self.expected_hash)+')' @@ -163,9 +163,9 @@ def __init__(self, metadata_role, previous_version, current_version): def __str__(self): - return 'Downloaded '+str(self.metadata_role)+' is older ('+\ - str(self.previous_version)+') than the version currently '+\ - 'installed ('+repr(self.current_version)+').' + return 'Downloaded ' + repr(self.metadata_role)+' is older ('+\ + repr(self.previous_version) + ') than the version currently '+\ + 'installed (' + repr(self.current_version) + ').' @@ -186,7 +186,7 @@ def __init__(self, metadata_role_name): self.metadata_role_name = metadata_role_name def __str__(self): - return str(self.metadata_role_name)+' metadata has bad signature!' + return repr(self.metadata_role_name) + ' metadata has bad signature.' @@ -217,7 +217,7 @@ def __init__(self, exception): def __str__(self): # Show the original exception. - return str(self.exception) + return repr(self.exception) @@ -239,8 +239,8 @@ def __init__(self, expected_length, observed_length): self.observed_length = observed_length #bytes def __str__(self): - return 'Observed length ('+str(self.observed_length)+\ - ') <= expected length ('+str(self.expected_length)+')' + return 'Observed length (' + repr(self.observed_length)+\ + ') <= expected length (' + repr(self.expected_length) + ').' @@ -253,8 +253,8 @@ def __init__(self, average_download_speed): self.__average_download_speed = average_download_speed #bytes/second def __str__(self): - return "Download was too slow. Average speed: "+\ - str(self.__average_download_speed)+" bytes per second" + return 'Download was too slow. Average speed: ' +\ + repr(self.__average_download_speed) + ' bytes per second.' @@ -315,11 +315,13 @@ def __str__(self): class NoWorkingMirrorError(Error): - """An updater will throw this exception in case it could not download a - metadata or target file. + """ + An updater will throw this exception in case it could not download a + metadata or target file. - A dictionary of Exception instances indexed by every mirror URL will also be - provided.""" + A dictionary of Exception instances indexed by every mirror URL will also be + provided. + """ def __init__(self, mirror_errors): # Dictionary of URL strings to Exception instances @@ -334,12 +336,12 @@ def __str__(self): mirror_url_tokens = six.moves.urllib.parse.urlparse(mirror_url) except: - logging.exception('Failed to parse mirror URL: '+str(mirror_url)) + logging.exception('Failed to parse mirror URL: ' + repr(mirror_url)) mirror_netloc = mirror_url else: mirror_netloc = mirror_url_tokens.netloc - all_errors += '\n '+str(mirror_netloc)+': '+str(mirror_error) + all_errors += '\n ' + repr(mirror_netloc) + ': ' + repr(mirror_error) return all_errors From 1c1cd0f192f50c745cee7c71e81e6d39bd8ceb6c Mon Sep 17 00:00:00 2001 From: Vladimir Diaz Date: Fri, 6 Jun 2014 08:37:31 -0400 Subject: [PATCH 18/32] Update / fix remaining Python 2 + 3 doctests. Remove test_slow_retrieval_attack.py print statements. --- tests/test_slow_retrieval_attack.py | 2 -- tuf/keys.py | 8 ++------ tuf/pycrypto_keys.py | 10 +++++----- tuf/schema.py | 6 ++---- 4 files changed, 9 insertions(+), 17 deletions(-) diff --git a/tests/test_slow_retrieval_attack.py b/tests/test_slow_retrieval_attack.py index 5cd0a57b..d273e348 100755 --- a/tests/test_slow_retrieval_attack.py +++ b/tests/test_slow_retrieval_attack.py @@ -234,7 +234,6 @@ def test_with_tuf_mode_1(self): # Verify that 'file1.txt' is the culprit. self.assertEqual(url_file, mirror_url) - print(repr(mirror_error)) self.assertTrue(isinstance(mirror_error, tuf.DownloadLengthMismatchError)) else: @@ -268,7 +267,6 @@ def test_with_tuf_mode_2(self): # Verify that 'file1.txt' is the culprit. self.assertEqual(url_file, mirror_url) - print(repr(mirror_error)) self.assertTrue(isinstance(mirror_error, tuf.DownloadLengthMismatchError)) else: diff --git a/tuf/keys.py b/tuf/keys.py index dec76da6..7a50736b 100755 --- a/tuf/keys.py +++ b/tuf/keys.py @@ -988,10 +988,6 @@ def format_rsakey_from_pem(pem): >>> rsa_key2 = format_rsakey_from_pem(public) >>> rsa_key == rsa_key2 True - >>> format_rsakey_from_pem('bad_pem') - Traceback (most recent call last): - ... - FormatError: The PEM string argument is improperly formatted. pem: @@ -1067,7 +1063,7 @@ def encrypt_key(key_object, password): >>> ed25519_key = generate_ed25519_key() >>> password = 'secret' - >>> encrypted_key = encrypt_key(ed25519_key, password) + >>> encrypted_key = encrypt_key(ed25519_key, password).encode('utf-8') >>> tuf.formats.ENCRYPTEDKEY_SCHEMA.matches(encrypted_key) True @@ -1158,7 +1154,7 @@ def decrypt_key(encrypted_key, passphrase): >>> ed25519_key = generate_ed25519_key() >>> password = 'secret' >>> encrypted_key = encrypt_key(ed25519_key, password) - >>> decrypted_key = decrypt_key(encrypted_key, password) + >>> decrypted_key = decrypt_key(encrypted_key.encode('utf-8'), password) >>> tuf.formats.ANYKEY_SCHEMA.matches(decrypted_key) True >>> decrypted_key == ed25519_key diff --git a/tuf/pycrypto_keys.py b/tuf/pycrypto_keys.py index 0f09ec76..1a5e2ff9 100755 --- a/tuf/pycrypto_keys.py +++ b/tuf/pycrypto_keys.py @@ -234,13 +234,13 @@ def create_rsa_signature(private_key, data): 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' + >>> data = 'The quick brown fox jumps over the lazy dog'.encode('utf-8') >>> signature, method = create_rsa_signature(private, data) >>> tuf.formats.NAME_SCHEMA.matches(method) True >>> method == 'RSASSA-PSS' True - >>> tuf.formats.PYCRYPTOSIGNATURE_SCHEMA.matches(method) + >>> tuf.formats.PYCRYPTOSIGNATURE_SCHEMA.matches(signature) True @@ -335,7 +335,7 @@ def verify_rsa_signature(signature, signature_method, public_key, data): >>> signature, method = create_rsa_signature(private, data) >>> verify_rsa_signature(signature, method, public, data) True - >>> verify_rsa_signature(signature, method, public, 'bad_data') + >>> verify_rsa_signature(signature, method, public, b'bad_data') False @@ -626,7 +626,7 @@ def encrypt_key(key_object, password): '1f26964cc8d4f7ee5f3c5da2fbb7ab35811169573ac367b860a537e47789f8c4'}} >>> passphrase = 'secret' >>> encrypted_key = encrypt_key(ed25519_key, passphrase) - >>> tuf.formats.ENCRYPTEDKEY_SCHEMA.matches(encrypted_key) + >>> tuf.formats.ENCRYPTEDKEY_SCHEMA.matches(encrypted_key.encode('utf-8')) True @@ -716,7 +716,7 @@ def decrypt_key(encrypted_key, password): '1f26964cc8d4f7ee5f3c5da2fbb7ab35811169573ac367b860a537e47789f8c4'}} >>> passphrase = 'secret' >>> encrypted_key = encrypt_key(ed25519_key, passphrase) - >>> decrypted_key = decrypt_key(encrypted_key, passphrase) + >>> decrypted_key = decrypt_key(encrypted_key.encode('utf-8'), passphrase) >>> tuf.formats.ED25519KEY_SCHEMA.matches(decrypted_key) True >>> decrypted_key == ed25519_key diff --git a/tuf/schema.py b/tuf/schema.py index 1026129e..ee719bbf 100755 --- a/tuf/schema.py +++ b/tuf/schema.py @@ -299,9 +299,9 @@ class LengthBytes(Schema): >>> schema = LengthBytes(5) - >>> schema.matches('Hello') + >>> schema.matches(b'Hello') True - >>> schema.matches('Hi') + >>> schema.matches(b'Hi') False """ @@ -554,8 +554,6 @@ class Integer(Schema): True >>> schema.matches(False) False - >>> schema.matches(0L) - True >>> schema.matches('a string') False >>> Integer(lo=10, hi=30).matches(25) From 17b230abddf1453bd96058da2ece4b219fd6a0b1 Mon Sep 17 00:00:00 2001 From: vladdd Date: Fri, 6 Jun 2014 18:57:23 -0400 Subject: [PATCH 19/32] Support Python 2.6. --- dev-requirements.txt | 1 + tests/aggregate_tests.py | 7 +++++++ tests/test_arbitrary_package_attack.py | 9 ++++++++- tests/test_endless_data_attack.py | 9 ++++++++- tests/test_extraneous_dependencies_attack.py | 9 ++++++++- tests/test_indefinite_freeze_attack.py | 9 ++++++++- tests/test_mix_and_match_attack.py | 9 ++++++++- tests/test_replay_attack.py | 9 ++++++++- tests/test_repository_lib.py | 9 ++++++++- tests/test_repository_tool.py | 8 ++++++++ tests/test_slow_retrieval_attack.py | 9 ++++++++- tests/test_updater.py | 9 ++++++++- tox.ini | 6 +++++- tuf/__init__.py | 4 ++-- tuf/repository_lib.py | 7 +++++-- 15 files changed, 100 insertions(+), 14 deletions(-) diff --git a/dev-requirements.txt b/dev-requirements.txt index 2f1d0d60..0ba31299 100644 --- a/dev-requirements.txt +++ b/dev-requirements.txt @@ -18,3 +18,4 @@ pycrypto==2.6.1 pynacl==0.2.3 tox +unittest2 diff --git a/tests/aggregate_tests.py b/tests/aggregate_tests.py index 26edb06d..a73e95fc 100755 --- a/tests/aggregate_tests.py +++ b/tests/aggregate_tests.py @@ -36,6 +36,13 @@ import glob import random +# 'unittest2' required for testing under Python < 2.7. +if sys.version_info >= (2, 7): + import unittest + +else: + import unittest2 as unittest + # Generate a list of pathnames that match a pattern (i.e., that begin with # 'test_' and end with '.py'. A shell-style wildcard is used with glob() to diff --git a/tests/test_arbitrary_package_attack.py b/tests/test_arbitrary_package_attack.py index e65582e2..0d9a9221 100755 --- a/tests/test_arbitrary_package_attack.py +++ b/tests/test_arbitrary_package_attack.py @@ -40,8 +40,15 @@ import shutil import json import subprocess -import unittest import logging +import sys + +# 'unittest2' required for testing under Python < 2.7. +if sys.version_info >= (2, 7): + import unittest + +else: + import unittest2 as unittest import tuf import tuf.formats diff --git a/tests/test_endless_data_attack.py b/tests/test_endless_data_attack.py index 816855ee..48c7f4bd 100755 --- a/tests/test_endless_data_attack.py +++ b/tests/test_endless_data_attack.py @@ -43,8 +43,15 @@ import shutil import json import subprocess -import unittest import logging +import sys + +# 'unittest2' required for testing under Python < 2.7. +if sys.version_info >= (2, 7): + import unittest + +else: + import unittest2 as unittest import tuf import tuf.formats diff --git a/tests/test_extraneous_dependencies_attack.py b/tests/test_extraneous_dependencies_attack.py index 642dd405..a6a0dd18 100755 --- a/tests/test_extraneous_dependencies_attack.py +++ b/tests/test_extraneous_dependencies_attack.py @@ -45,8 +45,15 @@ import shutil import json import subprocess -import unittest import logging +import sys + +# 'unittest2' required for testing under Python < 2.7. +if sys.version_info >= (2, 7): + import unittest + +else: + import unittest2 as unittest import tuf.formats import tuf.util diff --git a/tests/test_indefinite_freeze_attack.py b/tests/test_indefinite_freeze_attack.py index 669864e9..436198d4 100755 --- a/tests/test_indefinite_freeze_attack.py +++ b/tests/test_indefinite_freeze_attack.py @@ -39,8 +39,15 @@ import shutil import json import subprocess -import unittest import logging +import sys + +# 'unittest2' required for testing under Python < 2.7. +if sys.version_info >= (2, 7): + import unittest + +else: + import unittest2 as unittest import tuf.formats import tuf.util diff --git a/tests/test_mix_and_match_attack.py b/tests/test_mix_and_match_attack.py index 209fe6e7..69ed0594 100755 --- a/tests/test_mix_and_match_attack.py +++ b/tests/test_mix_and_match_attack.py @@ -42,8 +42,15 @@ import shutil import json import subprocess -import unittest import logging +import sys + +# 'unittest2' required for testing under Python < 2.7. +if sys.version_info >= (2, 7): + import unittest + +else: + import unittest2 as unittest import tuf.formats import tuf.util diff --git a/tests/test_replay_attack.py b/tests/test_replay_attack.py index 03e0a66d..dad8269d 100755 --- a/tests/test_replay_attack.py +++ b/tests/test_replay_attack.py @@ -43,8 +43,15 @@ import shutil import json import subprocess -import unittest import logging +import sys + +# 'unittest2' required for testing under Python < 2.7. +if sys.version_info >= (2, 7): + import unittest + +else: + import unittest2 as unittest import tuf.formats import tuf.util diff --git a/tests/test_repository_lib.py b/tests/test_repository_lib.py index b9e2bfe0..017e63ae 100755 --- a/tests/test_repository_lib.py +++ b/tests/test_repository_lib.py @@ -28,10 +28,17 @@ import os import time import datetime -import unittest import logging import tempfile import shutil +import sys + +# 'unittest2' required for testing under Python < 2.7. +if sys.version_info >= (2, 7): + import unittest + +else: + import unittest2 as unittest import tuf import tuf.log diff --git a/tests/test_repository_tool.py b/tests/test_repository_tool.py index a4e842b4..b4fcc7da 100755 --- a/tests/test_repository_tool.py +++ b/tests/test_repository_tool.py @@ -32,6 +32,14 @@ import logging import tempfile import shutil +import sys + +# 'unittest2' required for testing under Python < 2.7. +if sys.version_info >= (2, 7): + import unittest + +else: + import unittest2 as unittest import tuf import tuf.log diff --git a/tests/test_slow_retrieval_attack.py b/tests/test_slow_retrieval_attack.py index 11b09bbd..f45db742 100755 --- a/tests/test_slow_retrieval_attack.py +++ b/tests/test_slow_retrieval_attack.py @@ -46,8 +46,15 @@ import shutil import json import subprocess -import unittest import logging +import sys + +# 'unittest2' required for testing under Python < 2.7. +if sys.version_info >= (2, 7): + import unittest + +else: + import unittest2 as unittest import tuf.formats import tuf.util diff --git a/tests/test_updater.py b/tests/test_updater.py index 54be7900..26694ea6 100755 --- a/tests/test_updater.py +++ b/tests/test_updater.py @@ -52,9 +52,16 @@ import copy import tempfile import logging -import unittest import random import subprocess +import sys + +# 'unittest2' required for testing under Python < 2.7. +if sys.version_info >= (2, 7): + import unittest + +else: + import unittest2 as unittest import tuf import tuf.util diff --git a/tox.ini b/tox.ini index 2be4244d..3357bf6d 100644 --- a/tox.ini +++ b/tox.ini @@ -4,7 +4,7 @@ # and then run "tox" from this directory. [tox] -envlist = py27, py32, py33, py34 +envlist = py26, py27, py32, py33, py34 [testenv] @@ -18,3 +18,7 @@ deps = coverage pynacl pycrypto + +[testenv:py26] +deps = + unittest2 diff --git a/tuf/__init__.py b/tuf/__init__.py index a422a418..d160882c 100755 --- a/tuf/__init__.py +++ b/tuf/__init__.py @@ -334,12 +334,12 @@ def __str__(self): mirror_url_tokens = six.moves.urllib.parse.urlparse(mirror_url) except: - logging.exception('Failed to parse mirror URL: '+str(mirror_url)) + logging.exception('Failed to parse mirror URL: ' + repr(mirror_url)) mirror_netloc = mirror_url else: mirror_netloc = mirror_url_tokens.netloc - all_errors += '\n '+str(mirror_netloc)+': '+str(mirror_error) + all_errors += '\n ' + repr(mirror_netloc) + ': ' + repr(mirror_error) return all_errors diff --git a/tuf/repository_lib.py b/tuf/repository_lib.py index 4e7530ce..d4d8d69b 100755 --- a/tuf/repository_lib.py +++ b/tuf/repository_lib.py @@ -1914,9 +1914,12 @@ def write_metadata_file(metadata, filename, compressions, consistent_snapshot): # Instantiate a gzip object, but save compressed content to # 'file_object' (i.e., GzipFile instance is based on its 'fileobj' # argument). - with gzip.GzipFile(fileobj=file_object, mode='wb') as gzip_object: + gzip_object = gzip.GzipFile(fileobj=file_object, mode='wb') + try: gzip_object.write(file_content) - + finally: + gzip_object.close() + else: raise tuf.FormatError('Unknown compression algorithm: '+repr(compression)) From 546b1d69d64664f230d4140ab6be6aaad8634aff Mon Sep 17 00:00:00 2001 From: vladdd Date: Fri, 6 Jun 2014 19:39:25 -0400 Subject: [PATCH 20/32] Update py2.6 requirements in tox.ini. --- tox.ini | 1 + 1 file changed, 1 insertion(+) diff --git a/tox.ini b/tox.ini index 3357bf6d..36329a74 100644 --- a/tox.ini +++ b/tox.ini @@ -21,4 +21,5 @@ deps = [testenv:py26] deps = + {[testenv]deps} unittest2 From 39e1e8b080b67707e5094b72c290b579ec5d0299 Mon Sep 17 00:00:00 2001 From: vladdd Date: Fri, 6 Jun 2014 20:26:52 -0400 Subject: [PATCH 21/32] Add py26, py32, py33, py34 to setup.py. --- setup.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/setup.py b/setup.py index 17867ca0..78a994d1 100755 --- a/setup.py +++ b/setup.py @@ -96,6 +96,10 @@ 'Programming Language :: Python :: 2', 'Programming Language :: Python :: 2.6', 'Programming Language :: Python :: 2.7', + 'Programming Language :: Python :: 3', + 'Programming Language :: Python :: 3.2', + 'Programming Language :: Python :: 3.3', + 'Programming Language :: Python :: 3.4', 'Programming Language :: Python :: Implementation :: CPython', 'Topic :: Security', 'Topic :: Software Development' From 744be00cbc567c0c5ced7239c1f1cd10b75aca56 Mon Sep 17 00:00:00 2001 From: vladdd Date: Sat, 7 Jun 2014 20:29:18 -0400 Subject: [PATCH 22/32] Initial implementation of authoritative delegations. --- .../client/metadata/current/root.json | Bin 3756 -> 3756 bytes .../client/metadata/current/snapshot.json | Bin 1380 -> 1380 bytes .../client/metadata/current/targets.json | Bin 1936 -> 1960 bytes .../client/metadata/current/targets.json.gz | Bin 1202 -> 1215 bytes .../metadata/current/targets/role1.json | Bin 974 -> 974 bytes .../client/metadata/current/timestamp.json | Bin 924 -> 924 bytes .../client/metadata/previous/root.json | Bin 3756 -> 3756 bytes .../client/metadata/previous/snapshot.json | Bin 1380 -> 1380 bytes .../client/metadata/previous/targets.json | Bin 1936 -> 1960 bytes .../client/metadata/previous/targets.json.gz | Bin 1202 -> 1215 bytes .../metadata/previous/targets/role1.json | Bin 974 -> 974 bytes .../client/metadata/previous/timestamp.json | Bin 924 -> 924 bytes tests/repository_data/keystore/delegation_key | 52 +++++++------- .../keystore/delegation_key.pub | 14 ++-- tests/repository_data/keystore/root_key | 52 +++++++------- tests/repository_data/keystore/root_key.pub | 14 ++-- tests/repository_data/keystore/snapshot_key | 52 +++++++------- .../repository_data/keystore/snapshot_key.pub | 14 ++-- tests/repository_data/keystore/targets_key | 52 +++++++------- .../repository_data/keystore/targets_key.pub | 14 ++-- tests/repository_data/keystore/timestamp_key | 52 +++++++------- .../keystore/timestamp_key.pub | 14 ++-- .../repository/metadata.staged/root.json | Bin 3756 -> 3756 bytes .../repository/metadata.staged/snapshot.json | Bin 1380 -> 1380 bytes .../repository/metadata.staged/targets.json | Bin 1936 -> 1960 bytes .../metadata.staged/targets.json.gz | Bin 1202 -> 1215 bytes .../metadata.staged/targets/role1.json | Bin 974 -> 974 bytes .../repository/metadata.staged/timestamp.json | Bin 924 -> 924 bytes .../repository/metadata/root.json | Bin 3756 -> 3756 bytes .../repository/metadata/snapshot.json | Bin 1380 -> 1380 bytes .../repository/metadata/targets.json | Bin 1936 -> 1960 bytes .../repository/metadata/targets.json.gz | Bin 1202 -> 1215 bytes .../repository/metadata/targets/role1.json | Bin 974 -> 974 bytes .../repository/metadata/timestamp.json | Bin 924 -> 924 bytes tests/test_repository_tool.py | 8 ++- tests/test_updater.py | 66 ++++++++++++++++++ tuf/client/updater.py | 29 ++++++-- tuf/formats.py | 1 + tuf/repository_tool.py | 29 ++++++-- 39 files changed, 282 insertions(+), 181 deletions(-) diff --git a/tests/repository_data/client/metadata/current/root.json b/tests/repository_data/client/metadata/current/root.json index b1b34fa314f9ea032a9ac600bf4a5ff167642ce4..f4f0b55d5e6f7847fca8570cfa1fec44ca0228dd 100644 GIT binary patch literal 3756 zcmd5<+peoP5`8~kv9(__t!<32{gx{sB;-b}lTphC69@?q$PHTk@8e{jbJQA*X1eD* z^h5+jWxGzB9pGTx`+axGN({`1@4zr4Nus=sgViK^m2Tiyw0L=r-MY7${e zlOk#;U;;lxzM?`9pDGYw6POSyf_z8_fRso|r7|f-?|*ywJE=>Qy&TTw&H23f(_}vX zI;10b{*i(P3G^C6gq#P2AdV#@j2fnt5UBui29-jPNJc2oX2J|?GDdM2`W*XM_(J)J z5bgs21b~K5nL>e*9CKfo3<6*>0YYjB5RlL`kQo>fBc^;1a84mNl_7m@h=Ah&QJ`H= z%%lL+4>6Q5Y`O44>Btakml3;~-C=KmFnkvv6 zftbi33@L+p0y0AqQp}|kQc@`lZBl^>xS$OC2oUm2k=`L^Cc!#s1k+GxN2V}!un>_j zz?^^}REEz1qd_P*M0!sY_yYJy?-%L`iU29iK#{^K zoTDVR_#L#G({8WX9JiZO)_jh)_m2MEZSwVbkVWe|BA2nVh{vOP^4O)v&ZU?i^U7Qe z`>^iK^V|h$XrEhE2b~g}%4+3|>)KM6>1Kc9y}IhoP*a{P1d(oaQ}q|4 zKA#`9&WNp-{e_{ThZi>qS6PXgSvhhHE2M`4AlR$JxaC#*^U{d$E*-8PV_+{f>)1xB zz47j~gW6FzZJjK$WA&^ETC6Je=jj%AXQDNoY%E@!r)k(@9-A$uyEH>7t(?5;%vZy4 zK^8UN7-Vf8~SA713u%ZJ4Z-weiCq*cU!H z%?Dc==cpP~73IC7yPYjJ-tz2^Fj z+4_W{{J5o8m0;%zjdndn+ruKEU63=-KH18n!z3%!d<P^%&RozGYrp$wkU@Yk3$* zHa?JXyh}HmGU?jFpESHhkqaZfwn;j64@5*ZcGK?R0$b=KABu(Fot%)FB$t6FWSu6V zyIqfk&OtJAddCY>N{xbTLA-#pz`yYw;d`3LXF{H1MG)^!2Pk6XWJrJ&Bn1G=@XX0NW> zR`Uc7WO=i0$)hq))v4QH%R^l>bxHZe5aUrWTb_>Rb~QO#%M~HRTTSoD)bVp#grMDZ zl9qMcj4pfc9LHkcA5>s|Bj={b@6R?`cgZ#wA<#7jM&WU55nlq}pkb8H4=dvs#`#>j z)6Ov7dKRC-Zj^RLM_gWoEqW2yrHensR!>d$5@bboeBCfU#a8hYa=-6vaq#W>@w|Ne zaMOIX;ir;(db>ng2}arRsV9x6>1%8Lop5|b_4U&8`#^qJ6#k(=E~410%XcaIZ{ql2 ny8mNw#3xl=#3}y|rTJlo_%D&@;ZMNaR8huR`dPEzzWnx2-VHtB literal 3756 zcmd6qS+lB0635@~r#Nw5oepcGs^~XF+{ImwjuRa~6$%0(tJ2Zmy-Ux%9X%5hGu?9^ zdJJ4B%Bp{6{>V&z{_^&o2dBg-%1q|(Kfe9_%iG(}>izb9k&i&Ak$1_Mkkr*A_ZUNj zU_f1@>k=Sc0}wIC1(^AQ69hQc)R)hZuTjsVSW-yY``=#c#j-eGUkff*#%g8!Fkh{{ zUQ!C48&T;3M3623!BT_7bp(VOlhng5;ywo&cA4&~Wd%W!awwG+U)P0)T%Q0(x)68_ zQ{d^Y<`U_+9?=-le2=-FWC(I1kOq7RknmjJ)pX!+h;#sfBmf}-Jzqz_LB9K(z>u0L zT#c#efKw=>=PMOR0z%X->QBC>N#a3>1i>0`T&ZInNC)TwGnaYX;hvHpG$NT2=W^Z0 zQgT9x=VRTK3?mEyKmZWNl_^lUkrD=!{lJ4*_{ymsg+4`?Bcfy>A=J7=gVgmsU%6Il zp47OGA){&|;_K9bp65U&iE@}Hh4QD6${Rj@4kKUr)>Yo6j7t?#j4>5RnNJ9$Ncb98 z4i_8(M-x5(!WXWxNCjCFNFok(M;QhvV+3ewLnVs=$tb~07X(mGg@$04Gl&rs5c@=6 zN*UH%h?FyQs7diOR65Aj6(vMLgpsC6<|=!1@vb=e<;&YY6fZ9*<&%~tjPE}ck5oPp zSzfQj3z~QDn&d%V$fWowPn8`mO-JXJX_Xoak;fRdJi7TUi-0P*A zrn%OD4X#zDksx=KLhzROj{P`>I_yJC%xxZd`$E(+sAw6Z0WMMmfB1n z%5=~MgU7V4SXeqO$8PNVy-VBoy?Z9(6okEtgpJk7uXg5f<;i;3H5Nwan7o2x+1-DQ z*OTEF#p>fT?|par{L1GGpuYyI`)!HI-f7w8nY5ZH4CX_2Oajs0)a#rYleit%ozdv9 z+{VL2xb2R`nCkvLK5ORvqZBk6t(~;&mWA1yRkLv)8jdxX9+N|xx;{h)w)vgow$aWAL?&Ra67q<4vvsj9Cda+?yjfHxFaGl z95(&Kf=w#G`yGDYJ}_ZuL6d}wbp-ki-jgz%-dyh#b%?_Dm_+{zRuXWBae=TZK-3dJ z*9lcg%ZDn-Ll-!{PCUej04fx~^9A9$s?!)sB$+CZzvZE4q5o^JI-B^CU(JnmvquNN z8sh9UQ~@b3$vVQ_ML6RFQX4wo!-KzDnf;}C>9R(z%_3QPbC9*WV%fbm%`gGAHlOVu zhoW_hj)^nxh~QN3wuhW#9fmi29-iBWMSHPAx8$^l>^Qxow3nRqUK8II@h}97eSI+L zdR}(g=$MFYp16-qw`diU0qL1G<-C|xTirL-Gla5ct$i4>YiskQMsiF0F6?UWUQ*vs2^GK%s^@>uof+G!Oep$egEb8|gvTI=)58TO)r z&}C{DmoU~28@>9Dz%dXMu3FEWb%ASqf?<-p%zCC_CA3naj zXuituvy*(TR-9EQc)mu@77ad!zLw_SHI6S;Umtq@Nyzu6**_$taI%vuerrYljgIeb nPXCyWAeMRI#OZ$s&39*r|Dr@){sg9ySsq-I&zSx4<(FRp)5AQF diff --git a/tests/repository_data/client/metadata/current/snapshot.json b/tests/repository_data/client/metadata/current/snapshot.json index 017cb34e3f9804c3db056525204f5d1f78e395be..3d2aa1881dd6f26c198d6efb8656afb2e25e2e09 100644 GIT binary patch literal 1380 zcma)*!EPKk42JK1ip89pR1`&0$}MkD)IGH*3X-C{PE)Vp-4;!QynDIh1Sn7#FfcQR z(G2AfY>Rx9I-F6o zP7AZ4$X$fP-MA>M#28}jhQk0b%Ak~&OrcdBCM`&Av zE7rxdV0FU9RZtd2@si5jCK<)@)p|%{A8H^XxtDCKS-C^CCG0s7retra&RTj7CdTZ` zm;w|l$qI^*%6kqdL>5>;K++>`>F?rVwK$*>VqlNv(R~m>@bpk47+!~sl{<3N4lFZ9 zppf}Ub5b#_%0QHIr0N)ROHRJr9DYq+b|~XY%N54$x67x;xFT}9T{f#ov$j z&)9CYV0lv@ub)bJw_iU$f0W$m*E)yo>2$ih{ct`#e0y`)j(R@s=Y+I_V0wiYu{@7O4lWlTcD!cAvj> z|BoyfTwaj%_U@lo8J6+&iCus*N9+O8V&Cz<3Un z=I75}pZxjmuw5SRAMN_*b6mD}hhOdvhtK&tY`@2+2hYkD0b(p(Xd?FMpjvx#S-8}3 zfGw@FaCPXqU@otsKuj1Vc*>4;{&;+UJidSP!}0iaDIMGzNznn!9tBD* z9!+|ItV{-rYC3|bGeuA(jgTl!rq@Aiu=-dWldG_3^yRCU>0JN`FvW;TDUQ0B!>wS|+BAGf$Vi8y0=8UQ+$N?b(o&nfECR2!)U~EJ z09G6p02Rt=ow$KbU8_z~ETuykHVRlb@D^MxXC zU?%|~Mo#Z4g_(M0hize)$y7n<%c_@REd?`SpzbgRRZy(Aaa;m2L~AUibqPvtI$L&P zh1q7-b<}Wi;iWy>wkKz3OYu@(+3Q~y%aY|r-F5*s z@Jno;m&#*=?~sO<0Xh86IsEee>-#UievHDCwJ|BDeaskqQlxUu076J7Aq7%8qG9Ps zn1DG)31mZL?F|b1H~n@9DU=#Htu&xESrSUJI-moaM3l-~qg*s1U^0|dE|XGd!GLu> zs;ohP#pH4#C6S{p1+<0|Glgu)rRa=HO;M+b`_Wy;AW%v|shH2_${ zG3ZPhat=~x)T|>AfR~A~)0|Aq-neqWN6NyPtN^kIE)`0$l07GliChc?I)U*_sl3ld zN2#rFgasJHA!+HO6w*j93qEO-07@BA`jfJ@++uKL2bqKezAQX2ruvK1L3@O?}5nZ|nk0qz512{wMrOjSthg@no z#UPU^Dw8tg&^t>d5upWOQI{NfO$L23oWW?aQ5NrF^qi$dWq`{sU|W23D3}#$G{#8E zStKjpd64GquWyGROT>nZOTwtAI0cmjqA7`i3Ar?pg)y)!S(}m>BfG*Ylq;VjgRuGS zr|&)=R&BZ*PAdJ_xi}kVp@z%nyW46vX^hRjwPBn!JDudJuK)17&btn~>>E$L)B4%$ zVSYLA+xGLten*|@dbYc1Pj9;Q$2h5)x7Etf@#dtnO~?1=)8ppgY_h#dwf@y+cXjUL za$Y-I<5D*dv~rF1d7Ml-mEY{Q-h8O$Pft(N)@_B8_JTe=ye{vqWizd5<@L$bU$94wNLL5PDT$s9Cdcr{o87|+^=f8`f_kJ?>OkmI?nts zJlEZMBgA228b;V&OpBcDxWA4~d}NbbH@f%uvh3;6q`JPXHoRQSTMIR>ZTiDaqbc{7 zr`u!d590E8&S}ehdS5x`|9t53YimKHAzbTa_paOSPWCUeBIEAu`=h}RDdNLsU4~!& Y_<6H_=HK7?M?e1W2>;M1& delta 1112 zcmbu7OLF2j5XLKy$s(IMLZ+s&NwGj`y;@lfVDJkJ_<=ED(^5;u1N>pIjj=cEvP;#Z ze2MIHfgC5th-R)3l@@w-_xJnyueYDye*XCbS{=wl#30cL!t&gUi0E|o93UG-*`wqH$s~|MdC*pR1Vb)r zA&JZMDTtJuve{YVRR$*z9Asp}C1#caFsG7Ki4u<#b4~!>mzCPbWV5$EX|GgZ|3yz! z?1DnmpvfK$c` zOp$VuA_W9e*@V3}KvJ``p+pwCyd=(+g9Q_aoG3y<@Wc?M%V1(r1XkpN#02CfZ=Gevf6VlEQQfYCl*N;_W&Y;a)n@$ z>Gh}Ye&6c`QQSPM?k3l|yN`>bRnJYUPxGgqIIUeT)gka?W-oS|!}feB>*wwM+{o78 zvNQU$9`pX9@};K#;dcUuP?cjM7m?6%XY*}Gd@-W*KKLvwCBmTcm{ZU-hP4S z-U>^B(Ntx7l6dN_D#->%LE z9b3BXeb-o@MJkRn>gf~Tk9oJSuq_wWe;Ul)T{o|+F`f^$=!p-frUZe4_KOU~<%hS8X z`m(ipuEMa>-yIK@SIbtjQJsu$Zyt}H2`{dW)1-U7JKN4%vu00000 diff --git a/tests/repository_data/client/metadata/current/targets.json.gz b/tests/repository_data/client/metadata/current/targets.json.gz index 9e6ad7d2a323694978a84d59aab57ecc347ce781..874b7b776cd2fd24a0cc20229b4af25df0f9429c 100644 GIT binary patch literal 1215 zcmV;w1VH;AiwFRUq?1zu|E*P9QyN(ie)m^Uye3)C{gSsq&_!+vg5r|eI+s3x;(#!M zB;|ik1BPT%wN?AD)G(JmXS)0A{`#A5C#CXgI?i43vV>LnbLp>>Qt2E1OXWw{P9s{% zj_4?*^GtE=Qn1>y$Wlti5JD9--bTh<5I|KV!iJO_HBnjzT#G>JP$r9XEA4Qgofxb zJP7V6mqf*Y`5NLm)s7M>sHT{F}prGAZ#TFbbbcibRV7-$F8J!Rvu#=8kqeKKvWzsCE z0BB3zS?U>L^h`t*w2~3qK{zo1v3V6Wk0NLeV z4?+kUQzBkcZKxw(mL)@EI(phW{*lCT znAdLhb6=^vc=5Ao*zNnY`&DWe^>*p1Uuo9QN{!lZZye>Vdc9JA$l8_h3d4Jp=CiFdx<87tefL)&UP+d|U-C9!u!+GKgKf+@T2Qt3N>OXX>No&=1P zjS>!|m)2REk_?Au7K2qm0n@=6LInjEKzPlG0%EzNl5%RLBqEeQ9Ps9GlP?Z{*Zr*D z&puxD`)^B_!9LEJl)`yOjV53a(-Lot7Rd(3lH`!Ap&_y;Bcp^{CKR=XNzh&p3IQ)0 z9f?tMm9&zUC#H#Enk5rtK+Hr5Gbv(boFE?X5K?5!TD;>vs$`Xo!gB6{-Pi0nSX^P8 z6BiPfD)`8h4Tj+<)*B&%6p(13xBxUrr)cC@I&(HkPpk`yC1R{}3_OCmCL}Tr_=^-I0r4T4NfZICK?4$$w8v6{ za)Fx|jWsUBGK%5#r1a&(n<(R6EqgJR|Jpn+<6a^ISdHUmeb7xg#917}X0j;OZyOJ) zeSavGb<)9Wo7A32Vy))DQBX_sTDWZshM1eA3%m>Cbk`ZZqM>+uiNMf~y zqi~PmEzExIE0vdzZZ`4zeINIqTH~yBS-QHZwOjSlMdSVud?-4tR;~3lyR40;-}33? z%s!D?)@wAgtXt1|Cfl#q#}|0t$V3L+F2d(GSJ9=~IVXRPN^djD=CI z`n%OxV>+kR>qd7fY5i)@y#2Pk?cS`P=;n@u$85G6SDEm;;q8#@whu+EGnozUPe*c7 zo2uq;G|V3_nQ2`&Uf6SIVNm+T`8iMZ>(dujjkl9_ZPYavo!h2bGcL&OL$MiLcANR! z?5e83v8r^;)k|Z%xV&mAoSU!t%@ge|W{r6-6S-qsquW)Z`pvT%Wc10LM(jg{y_|iX zPvy!_+r4bm+;$qPkul8N<##5lHK!M%2RENjS4FF`AS5nDh4vmzecNq4RhEN?V&?A0 z+iC@dz3#BLoX#&Mv*n<^crIM8+1<6S<~Q?tEh|0`$M=ut&tUpDtvG7m?Jl>|dfx6_ zJrwzM3Yp$p6^vHopUHa`gRtG24$C{aN0ZleZGfakrctUkznO zIQd8@UIRit9j?)F65rBzpTfg=^l;w$EatGzr}4@#j2x< zv6#Af=9YjmYE`>Yf)1Wt zW-lqJWhtUSXl=y*#8wE&dUD7lbvTzXk)#1z6)(NyGH1ytH};t;7VOk#O71XH>zNT1 zMoeLa$+Bpen6pWrtqj;~aDx=n*vzwr!?;?s#YZt})xixDVJ=wnr~m|93gw!k zO=~^6I1SI#v`+IDS*haSxB`hR#RmyscCFAln8>6GTDO7Fp%%~~m{N8ko6}0h939Mk z_Edlbsl)(axon#vY*Nfn(Y+da?VerN~@U zPC~2^jxeYwhY4DOlb4I#*Wl$0rEj!sFfQNL-;cf_a?_q}eLbDIS%%;FZCj5I_ZzwY zsXf2Eyh8W%aQBM!*G+q?E&lv?d}VzhNN@=l&kexU`T6`Ry}tGNC;MR@?>xOTH2U18J=pS+Jv; j`U)dAkGoQkDWi1B(q->0k|R7keTIV?u^LGQV_^R6KR$p?F#&dt5ZoYviy-O;MT0nI z0h!w)=CJaPiIH849^LUUC&0RMpFc*fO7n|uwQwVwM3WV#BEtK!kvau~Pl z~6*Z6%)y+>1$b*MhDjzJ}YLOtMp%!2t!T;nz*olP-D%qx}%4{_pSae|`HO D%I2gl diff --git a/tests/repository_data/client/metadata/current/timestamp.json b/tests/repository_data/client/metadata/current/timestamp.json index 73cd3b4c18f1b2bcbb01645f57651a25410bc321..61dbbcae767f9658fee8c01e00322029b6131165 100644 GIT binary patch literal 924 zcmXw2-Krcn48A{4vCMT7Bukd9+~o~QH&-pC$hMT-r00k1wlsvido^+uoi2csQND`uF^N*ucuoEfiKq6E%C!;=qr3bSk z*z8zjR_jUWq%m=IuMyqGkf~T#Ll@MM${LxjG8x!;jKoHzHf0wxT9S2=20@JlaP&6Y(pj<<=#xFXt>|to zk!6-y%itI;>8ooS6HFnYJ=&iH=KW%rjvEQ_Ib9JZ{*oIPi_^FjfrOo5WNic7;N3X5}KTG#5i z7nNuhwd$5R`TlVFGxKtU()YCNVch<@e|_;iA|I}ge!JV_%aNST|9iQ<=61_@!5hF_ z9{}DR>-X1789e&|l(p3E^?gJ`{;!?@%rQv3nxpY7lbZAR{~v(e@p%9M literal 924 zcmXw2ORgL@4BWp{40;_Kq$qymoo^6iyh;!Rltg*P&ilYK31S$&d)WPKKn--0B3Z1e z{`PR%Zr6`be*fo7+_rb8KORn}Z~2_I&++v-GqSDGAPl!`x@m(IHb9_6 zbSl=R6zFw2TN%ACG({e3VqL2=1!kvG$=Wd1s>%)~y4DJcHpqp*lvE?ItQuJ_5-_!L zb+aCl_0~!@Efd>l#4(}Fg*av(9<^ZMmEXkZu@_Z^mX$J>pjy#|m1Qjv;DC{DtRM{VDD9=NM)#0D^Ug4$T0Ey=CrMyqB#t6Nw78j= zEKUW12#uSth4-Y?2ja{PQJiNi?l5|AO5bNI1C`jU;sUWglp+}lYqNJj)Hb7uwlPO~ zq^63Fv?!NZv(_zr^8MlTXZqzZW$bR*&A9z_|N0WUMLt{~<97GQm%}+*{P%Kw_3f4f z!W+Om9{}DR=l9o3X*~bYDEmJ7%kA=ffBWh7{PZ(=+Ae;(9R5GNpA&BvFD(1W)uuxS z?>>plDFs^tU3T!(#y~doo^FD4R7?3rBn RHM#v8UvAd}6h375`~O&Q_Kg4l diff --git a/tests/repository_data/client/metadata/previous/root.json b/tests/repository_data/client/metadata/previous/root.json index b1b34fa314f9ea032a9ac600bf4a5ff167642ce4..f4f0b55d5e6f7847fca8570cfa1fec44ca0228dd 100644 GIT binary patch literal 3756 zcmd5<+peoP5`8~kv9(__t!<32{gx{sB;-b}lTphC69@?q$PHTk@8e{jbJQA*X1eD* z^h5+jWxGzB9pGTx`+axGN({`1@4zr4Nus=sgViK^m2Tiyw0L=r-MY7${e zlOk#;U;;lxzM?`9pDGYw6POSyf_z8_fRso|r7|f-?|*ywJE=>Qy&TTw&H23f(_}vX zI;10b{*i(P3G^C6gq#P2AdV#@j2fnt5UBui29-jPNJc2oX2J|?GDdM2`W*XM_(J)J z5bgs21b~K5nL>e*9CKfo3<6*>0YYjB5RlL`kQo>fBc^;1a84mNl_7m@h=Ah&QJ`H= z%%lL+4>6Q5Y`O44>Btakml3;~-C=KmFnkvv6 zftbi33@L+p0y0AqQp}|kQc@`lZBl^>xS$OC2oUm2k=`L^Cc!#s1k+GxN2V}!un>_j zz?^^}REEz1qd_P*M0!sY_yYJy?-%L`iU29iK#{^K zoTDVR_#L#G({8WX9JiZO)_jh)_m2MEZSwVbkVWe|BA2nVh{vOP^4O)v&ZU?i^U7Qe z`>^iK^V|h$XrEhE2b~g}%4+3|>)KM6>1Kc9y}IhoP*a{P1d(oaQ}q|4 zKA#`9&WNp-{e_{ThZi>qS6PXgSvhhHE2M`4AlR$JxaC#*^U{d$E*-8PV_+{f>)1xB zz47j~gW6FzZJjK$WA&^ETC6Je=jj%AXQDNoY%E@!r)k(@9-A$uyEH>7t(?5;%vZy4 zK^8UN7-Vf8~SA713u%ZJ4Z-weiCq*cU!H z%?Dc==cpP~73IC7yPYjJ-tz2^Fj z+4_W{{J5o8m0;%zjdndn+ruKEU63=-KH18n!z3%!d<P^%&RozGYrp$wkU@Yk3$* zHa?JXyh}HmGU?jFpESHhkqaZfwn;j64@5*ZcGK?R0$b=KABu(Fot%)FB$t6FWSu6V zyIqfk&OtJAddCY>N{xbTLA-#pz`yYw;d`3LXF{H1MG)^!2Pk6XWJrJ&Bn1G=@XX0NW> zR`Uc7WO=i0$)hq))v4QH%R^l>bxHZe5aUrWTb_>Rb~QO#%M~HRTTSoD)bVp#grMDZ zl9qMcj4pfc9LHkcA5>s|Bj={b@6R?`cgZ#wA<#7jM&WU55nlq}pkb8H4=dvs#`#>j z)6Ov7dKRC-Zj^RLM_gWoEqW2yrHensR!>d$5@bboeBCfU#a8hYa=-6vaq#W>@w|Ne zaMOIX;ir;(db>ng2}arRsV9x6>1%8Lop5|b_4U&8`#^qJ6#k(=E~410%XcaIZ{ql2 ny8mNw#3xl=#3}y|rTJlo_%D&@;ZMNaR8huR`dPEzzWnx2-VHtB literal 3756 zcmd6qS+lB0635@~r#Nw5oepcGs^~XF+{ImwjuRa~6$%0(tJ2Zmy-Ux%9X%5hGu?9^ zdJJ4B%Bp{6{>V&z{_^&o2dBg-%1q|(Kfe9_%iG(}>izb9k&i&Ak$1_Mkkr*A_ZUNj zU_f1@>k=Sc0}wIC1(^AQ69hQc)R)hZuTjsVSW-yY``=#c#j-eGUkff*#%g8!Fkh{{ zUQ!C48&T;3M3623!BT_7bp(VOlhng5;ywo&cA4&~Wd%W!awwG+U)P0)T%Q0(x)68_ zQ{d^Y<`U_+9?=-le2=-FWC(I1kOq7RknmjJ)pX!+h;#sfBmf}-Jzqz_LB9K(z>u0L zT#c#efKw=>=PMOR0z%X->QBC>N#a3>1i>0`T&ZInNC)TwGnaYX;hvHpG$NT2=W^Z0 zQgT9x=VRTK3?mEyKmZWNl_^lUkrD=!{lJ4*_{ymsg+4`?Bcfy>A=J7=gVgmsU%6Il zp47OGA){&|;_K9bp65U&iE@}Hh4QD6${Rj@4kKUr)>Yo6j7t?#j4>5RnNJ9$Ncb98 z4i_8(M-x5(!WXWxNCjCFNFok(M;QhvV+3ewLnVs=$tb~07X(mGg@$04Gl&rs5c@=6 zN*UH%h?FyQs7diOR65Aj6(vMLgpsC6<|=!1@vb=e<;&YY6fZ9*<&%~tjPE}ck5oPp zSzfQj3z~QDn&d%V$fWowPn8`mO-JXJX_Xoak;fRdJi7TUi-0P*A zrn%OD4X#zDksx=KLhzROj{P`>I_yJC%xxZd`$E(+sAw6Z0WMMmfB1n z%5=~MgU7V4SXeqO$8PNVy-VBoy?Z9(6okEtgpJk7uXg5f<;i;3H5Nwan7o2x+1-DQ z*OTEF#p>fT?|par{L1GGpuYyI`)!HI-f7w8nY5ZH4CX_2Oajs0)a#rYleit%ozdv9 z+{VL2xb2R`nCkvLK5ORvqZBk6t(~;&mWA1yRkLv)8jdxX9+N|xx;{h)w)vgow$aWAL?&Ra67q<4vvsj9Cda+?yjfHxFaGl z95(&Kf=w#G`yGDYJ}_ZuL6d}wbp-ki-jgz%-dyh#b%?_Dm_+{zRuXWBae=TZK-3dJ z*9lcg%ZDn-Ll-!{PCUej04fx~^9A9$s?!)sB$+CZzvZE4q5o^JI-B^CU(JnmvquNN z8sh9UQ~@b3$vVQ_ML6RFQX4wo!-KzDnf;}C>9R(z%_3QPbC9*WV%fbm%`gGAHlOVu zhoW_hj)^nxh~QN3wuhW#9fmi29-iBWMSHPAx8$^l>^Qxow3nRqUK8II@h}97eSI+L zdR}(g=$MFYp16-qw`diU0qL1G<-C|xTirL-Gla5ct$i4>YiskQMsiF0F6?UWUQ*vs2^GK%s^@>uof+G!Oep$egEb8|gvTI=)58TO)r z&}C{DmoU~28@>9Dz%dXMu3FEWb%ASqf?<-p%zCC_CA3naj zXuituvy*(TR-9EQc)mu@77ad!zLw_SHI6S;Umtq@Nyzu6**_$taI%vuerrYljgIeb nPXCyWAeMRI#OZ$s&39*r|Dr@){sg9ySsq-I&zSx4<(FRp)5AQF diff --git a/tests/repository_data/client/metadata/previous/snapshot.json b/tests/repository_data/client/metadata/previous/snapshot.json index 017cb34e3f9804c3db056525204f5d1f78e395be..3d2aa1881dd6f26c198d6efb8656afb2e25e2e09 100644 GIT binary patch literal 1380 zcma)*!EPKk42JK1ip89pR1`&0$}MkD)IGH*3X-C{PE)Vp-4;!QynDIh1Sn7#FfcQR z(G2AfY>Rx9I-F6o zP7AZ4$X$fP-MA>M#28}jhQk0b%Ak~&OrcdBCM`&Av zE7rxdV0FU9RZtd2@si5jCK<)@)p|%{A8H^XxtDCKS-C^CCG0s7retra&RTj7CdTZ` zm;w|l$qI^*%6kqdL>5>;K++>`>F?rVwK$*>VqlNv(R~m>@bpk47+!~sl{<3N4lFZ9 zppf}Ub5b#_%0QHIr0N)ROHRJr9DYq+b|~XY%N54$x67x;xFT}9T{f#ov$j z&)9CYV0lv@ub)bJw_iU$f0W$m*E)yo>2$ih{ct`#e0y`)j(R@s=Y+I_V0wiYu{@7O4lWlTcD!cAvj> z|BoyfTwaj%_U@lo8J6+&iCus*N9+O8V&Cz<3Un z=I75}pZxjmuw5SRAMN_*b6mD}hhOdvhtK&tY`@2+2hYkD0b(p(Xd?FMpjvx#S-8}3 zfGw@FaCPXqU@otsKuj1Vc*>4;{&;+UJidSP!}0iaDIMGzNznn!9tBD* z9!+|ItV{-rYC3|bGeuA(jgTl!rq@Aiu=-dWldG_3^yRCU>0JN`FvW;TDUQ0B!>wS|+BAGf$Vi8y0=8UQ+$N?b(o&nfECR2!)U~EJ z09G6p02Rt=ow$KbU8_z~ETuykHVRlb@D^MxXC zU?%|~Mo#Z4g_(M0hize)$y7n<%c_@REd?`SpzbgRRZy(Aaa;m2L~AUibqPvtI$L&P zh1q7-b<}Wi;iWy>wkKz3OYu@(+3Q~y%aY|r-F5*s z@Jno;m&#*=?~sO<0Xh86IsEee>-#UievHDCwJ|BDeaskqQlxUu076J7Aq7%8qG9Ps zn1DG)31mZL?F|b1H~n@9DU=#Htu&xESrSUJI-moaM3l-~qg*s1U^0|dE|XGd!GLu> zs;ohP#pH4#C6S{p1+<0|Glgu)rRa=HO;M+b`_Wy;AW%v|shH2_${ zG3ZPhat=~x)T|>AfR~A~)0|Aq-neqWN6NyPtN^kIE)`0$l07GliChc?I)U*_sl3ld zN2#rFgasJHA!+HO6w*j93qEO-07@BA`jfJ@++uKL2bqKezAQX2ruvK1L3@O?}5nZ|nk0qz512{wMrOjSthg@no z#UPU^Dw8tg&^t>d5upWOQI{NfO$L23oWW?aQ5NrF^qi$dWq`{sU|W23D3}#$G{#8E zStKjpd64GquWyGROT>nZOTwtAI0cmjqA7`i3Ar?pg)y)!S(}m>BfG*Ylq;VjgRuGS zr|&)=R&BZ*PAdJ_xi}kVp@z%nyW46vX^hRjwPBn!JDudJuK)17&btn~>>E$L)B4%$ zVSYLA+xGLten*|@dbYc1Pj9;Q$2h5)x7Etf@#dtnO~?1=)8ppgY_h#dwf@y+cXjUL za$Y-I<5D*dv~rF1d7Ml-mEY{Q-h8O$Pft(N)@_B8_JTe=ye{vqWizd5<@L$bU$94wNLL5PDT$s9Cdcr{o87|+^=f8`f_kJ?>OkmI?nts zJlEZMBgA228b;V&OpBcDxWA4~d}NbbH@f%uvh3;6q`JPXHoRQSTMIR>ZTiDaqbc{7 zr`u!d590E8&S}ehdS5x`|9t53YimKHAzbTa_paOSPWCUeBIEAu`=h}RDdNLsU4~!& Y_<6H_=HK7?M?e1W2>;M1& delta 1112 zcmbu7OLF2j5XLKy$s(IMLZ+s&NwGj`y;@lfVDJkJ_<=ED(^5;u1N>pIjj=cEvP;#Z ze2MIHfgC5th-R)3l@@w-_xJnyueYDye*XCbS{=wl#30cL!t&gUi0E|o93UG-*`wqH$s~|MdC*pR1Vb)r zA&JZMDTtJuve{YVRR$*z9Asp}C1#caFsG7Ki4u<#b4~!>mzCPbWV5$EX|GgZ|3yz! z?1DnmpvfK$c` zOp$VuA_W9e*@V3}KvJ``p+pwCyd=(+g9Q_aoG3y<@Wc?M%V1(r1XkpN#02CfZ=Gevf6VlEQQfYCl*N;_W&Y;a)n@$ z>Gh}Ye&6c`QQSPM?k3l|yN`>bRnJYUPxGgqIIUeT)gka?W-oS|!}feB>*wwM+{o78 zvNQU$9`pX9@};K#;dcUuP?cjM7m?6%XY*}Gd@-W*KKLvwCBmTcm{ZU-hP4S z-U>^B(Ntx7l6dN_D#->%LE z9b3BXeb-o@MJkRn>gf~Tk9oJSuq_wWe;Ul)T{o|+F`f^$=!p-frUZe4_KOU~<%hS8X z`m(ipuEMa>-yIK@SIbtjQJsu$Zyt}H2`{dW)1-U7JKN4%vu00000 diff --git a/tests/repository_data/client/metadata/previous/targets.json.gz b/tests/repository_data/client/metadata/previous/targets.json.gz index 9e6ad7d2a323694978a84d59aab57ecc347ce781..874b7b776cd2fd24a0cc20229b4af25df0f9429c 100644 GIT binary patch literal 1215 zcmV;w1VH;AiwFRUq?1zu|E*P9QyN(ie)m^Uye3)C{gSsq&_!+vg5r|eI+s3x;(#!M zB;|ik1BPT%wN?AD)G(JmXS)0A{`#A5C#CXgI?i43vV>LnbLp>>Qt2E1OXWw{P9s{% zj_4?*^GtE=Qn1>y$Wlti5JD9--bTh<5I|KV!iJO_HBnjzT#G>JP$r9XEA4Qgofxb zJP7V6mqf*Y`5NLm)s7M>sHT{F}prGAZ#TFbbbcibRV7-$F8J!Rvu#=8kqeKKvWzsCE z0BB3zS?U>L^h`t*w2~3qK{zo1v3V6Wk0NLeV z4?+kUQzBkcZKxw(mL)@EI(phW{*lCT znAdLhb6=^vc=5Ao*zNnY`&DWe^>*p1Uuo9QN{!lZZye>Vdc9JA$l8_h3d4Jp=CiFdx<87tefL)&UP+d|U-C9!u!+GKgKf+@T2Qt3N>OXX>No&=1P zjS>!|m)2REk_?Au7K2qm0n@=6LInjEKzPlG0%EzNl5%RLBqEeQ9Ps9GlP?Z{*Zr*D z&puxD`)^B_!9LEJl)`yOjV53a(-Lot7Rd(3lH`!Ap&_y;Bcp^{CKR=XNzh&p3IQ)0 z9f?tMm9&zUC#H#Enk5rtK+Hr5Gbv(boFE?X5K?5!TD;>vs$`Xo!gB6{-Pi0nSX^P8 z6BiPfD)`8h4Tj+<)*B&%6p(13xBxUrr)cC@I&(HkPpk`yC1R{}3_OCmCL}Tr_=^-I0r4T4NfZICK?4$$w8v6{ za)Fx|jWsUBGK%5#r1a&(n<(R6EqgJR|Jpn+<6a^ISdHUmeb7xg#917}X0j;OZyOJ) zeSavGb<)9Wo7A32Vy))DQBX_sTDWZshM1eA3%m>Cbk`ZZqM>+uiNMf~y zqi~PmEzExIE0vdzZZ`4zeINIqTH~yBS-QHZwOjSlMdSVud?-4tR;~3lyR40;-}33? z%s!D?)@wAgtXt1|Cfl#q#}|0t$V3L+F2d(GSJ9=~IVXRPN^djD=CI z`n%OxV>+kR>qd7fY5i)@y#2Pk?cS`P=;n@u$85G6SDEm;;q8#@whu+EGnozUPe*c7 zo2uq;G|V3_nQ2`&Uf6SIVNm+T`8iMZ>(dujjkl9_ZPYavo!h2bGcL&OL$MiLcANR! z?5e83v8r^;)k|Z%xV&mAoSU!t%@ge|W{r6-6S-qsquW)Z`pvT%Wc10LM(jg{y_|iX zPvy!_+r4bm+;$qPkul8N<##5lHK!M%2RENjS4FF`AS5nDh4vmzecNq4RhEN?V&?A0 z+iC@dz3#BLoX#&Mv*n<^crIM8+1<6S<~Q?tEh|0`$M=ut&tUpDtvG7m?Jl>|dfx6_ zJrwzM3Yp$p6^vHopUHa`gRtG24$C{aN0ZleZGfakrctUkznO zIQd8@UIRit9j?)F65rBzpTfg=^l;w$EatGzr}4@#j2x< zv6#Af=9YjmYE`>Yf)1Wt zW-lqJWhtUSXl=y*#8wE&dUD7lbvTzXk)#1z6)(NyGH1ytH};t;7VOk#O71XH>zNT1 zMoeLa$+Bpen6pWrtqj;~aDx=n*vzwr!?;?s#YZt})xixDVJ=wnr~m|93gw!k zO=~^6I1SI#v`+IDS*haSxB`hR#RmyscCFAln8>6GTDO7Fp%%~~m{N8ko6}0h939Mk z_Edlbsl)(axon#vY*Nfn(Y+da?VerN~@U zPC~2^jxeYwhY4DOlb4I#*Wl$0rEj!sFfQNL-;cf_a?_q}eLbDIS%%;FZCj5I_ZzwY zsXf2Eyh8W%aQBM!*G+q?E&lv?d}VzhNN@=l&kexU`T6`Ry}tGNC;MR@?>xOTH2U18J=pS+Jv; j`U)dAkGoQkDWi1B(q->0k|R7keTIV?u^LGQV_^R6KR$p?F#&dt5ZoYviy-O;MT0nI z0h!w)=CJaPiIH849^LUUC&0RMpFc*fO7n|uwQwVwM3WV#BEtK!kvau~Pl z~6*Z6%)y+>1$b*MhDjzJ}YLOtMp%!2t!T;nz*olP-D%qx}%4{_pSae|`HO D%I2gl diff --git a/tests/repository_data/client/metadata/previous/timestamp.json b/tests/repository_data/client/metadata/previous/timestamp.json index 73cd3b4c18f1b2bcbb01645f57651a25410bc321..61dbbcae767f9658fee8c01e00322029b6131165 100644 GIT binary patch literal 924 zcmXw2-Krcn48A{4vCMT7Bukd9+~o~QH&-pC$hMT-r00k1wlsvido^+uoi2csQND`uF^N*ucuoEfiKq6E%C!;=qr3bSk z*z8zjR_jUWq%m=IuMyqGkf~T#Ll@MM${LxjG8x!;jKoHzHf0wxT9S2=20@JlaP&6Y(pj<<=#xFXt>|to zk!6-y%itI;>8ooS6HFnYJ=&iH=KW%rjvEQ_Ib9JZ{*oIPi_^FjfrOo5WNic7;N3X5}KTG#5i z7nNuhwd$5R`TlVFGxKtU()YCNVch<@e|_;iA|I}ge!JV_%aNST|9iQ<=61_@!5hF_ z9{}DR>-X1789e&|l(p3E^?gJ`{;!?@%rQv3nxpY7lbZAR{~v(e@p%9M literal 924 zcmXw2ORgL@4BWp{40;_Kq$qymoo^6iyh;!Rltg*P&ilYK31S$&d)WPKKn--0B3Z1e z{`PR%Zr6`be*fo7+_rb8KORn}Z~2_I&++v-GqSDGAPl!`x@m(IHb9_6 zbSl=R6zFw2TN%ACG({e3VqL2=1!kvG$=Wd1s>%)~y4DJcHpqp*lvE?ItQuJ_5-_!L zb+aCl_0~!@Efd>l#4(}Fg*av(9<^ZMmEXkZu@_Z^mX$J>pjy#|m1Qjv;DC{DtRM{VDD9=NM)#0D^Ug4$T0Ey=CrMyqB#t6Nw78j= zEKUW12#uSth4-Y?2ja{PQJiNi?l5|AO5bNI1C`jU;sUWglp+}lYqNJj)Hb7uwlPO~ zq^63Fv?!NZv(_zr^8MlTXZqzZW$bR*&A9z_|N0WUMLt{~<97GQm%}+*{P%Kw_3f4f z!W+Om9{}DR=l9o3X*~bYDEmJ7%kA=ffBWh7{PZ(=+Ae;(9R5GNpA&BvFD(1W)uuxS z?>>plDFs^tU3T!(#y~doo^FD4R7?3rBn RHM#v8UvAd}6h375`~O&Q_Kg4l diff --git a/tests/repository_data/keystore/delegation_key b/tests/repository_data/keystore/delegation_key index 75c1ccdb..e4fd71f8 100644 --- a/tests/repository_data/keystore/delegation_key +++ b/tests/repository_data/keystore/delegation_key @@ -1,30 +1,30 @@ -----BEGIN RSA PRIVATE KEY----- Proc-Type: 4,ENCRYPTED -DEK-Info: DES-EDE3-CBC,FF468C7E762281A6 +DEK-Info: DES-EDE3-CBC,8EB89B1037BC3FA7 -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 -----END RSA PRIVATE KEY----- \ No newline at end of file diff --git a/tests/repository_data/keystore/delegation_key.pub b/tests/repository_data/keystore/delegation_key.pub index 9ed2a92f..cd3e75f9 100644 --- a/tests/repository_data/keystore/delegation_key.pub +++ b/tests/repository_data/keystore/delegation_key.pub @@ -1,9 +1,9 @@ -----BEGIN PUBLIC KEY----- -MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA4Aaacry3Lrf+LxjlHoM7 -qkwM0K+Wm3G2dZh6SxrGEkm1/REOk51CPTFVqpVOUsw1tW0duAlxg/24cxXVX0xv -BMilTYDZ5tBk6FXZXhuN28IREz2yMo8a93nHH3fCRDz2/eViLBZO8KMVF6s2340V -tTNOFhm8x/6nfe3+M8PzEgoNPF693djhUw1OolEmQA4hb2v87L/86SFGBaA1w8De -M+2zGJmk5rckLQAZFvMErZ8828WhW8ABFkK4QaUJDrnI+o00ep4+1Qu8CvOIw+pT -lbYgv/+aXQOXQpkmKilpTCoynbQFOxIPmUmCBAnJXgYuHya8SUIeZLWxNvkChLMP -SwIDAQAB +MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAqKdTRVn3mLQFUf02Rpug +wVEU4yJtechILLb6nM7+urfwLe6f7EsNCDFhkiTP7vKuQywdLYrhwZKYZMDmaVnI +q4d/tBLvb/jGY/IPFVvWbAOWtwWG7apiAFrcp3Idq6EKGaVVLn7tyv74+nisssYJ +cVKodlkzpgX1Ibrdq73BUlAxhEQNDAUM5bzyJUW0BU4OSjUoFKCgc8BSkNcSLwXO +RpyqAwDpPWiL68N1Dch7R9uD6GE9aREY9SKoYsNCvUOraIcme4fJZ3NmxpN3SVnX +tepoiJo2iAtORtEI1yTCv/dOPap/iebveeCjn667HkMezJodSR8X3pMgMKMVyxhJ +gwIDAQAB -----END PUBLIC KEY----- \ No newline at end of file diff --git a/tests/repository_data/keystore/root_key b/tests/repository_data/keystore/root_key index 38b65b85..883165af 100644 --- a/tests/repository_data/keystore/root_key +++ b/tests/repository_data/keystore/root_key @@ -1,30 +1,30 @@ -----BEGIN RSA PRIVATE KEY----- Proc-Type: 4,ENCRYPTED -DEK-Info: DES-EDE3-CBC,D1A23F08038990E0 +DEK-Info: DES-EDE3-CBC,B90D8CBD38BD4DE2 -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 -----END RSA PRIVATE KEY----- \ No newline at end of file diff --git a/tests/repository_data/keystore/root_key.pub b/tests/repository_data/keystore/root_key.pub index 61ab286a..40ae3768 100644 --- a/tests/repository_data/keystore/root_key.pub +++ b/tests/repository_data/keystore/root_key.pub @@ -1,9 +1,9 @@ -----BEGIN PUBLIC KEY----- -MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAxqR34Ya+Nuh4+zljiPI7 -idGUwTs6ALm/mwBJJZRVmIQjVDKdK69fxmh5CxXud6lJTapuDutCFOvOKG1AaEHM -Zu5r/UiuEEUU2ZTtjtFaulJpoz6lYViM2qHy8o4HJ8f5VHvcITnAy4Qu3emKolvi -CrnMoiBZjGQAGEGt1u7S9u8P9zA4Jz0ziICdDnhKH8tLvWBmF4VHmAbLXXFj+Yny -R7iOeDEiS5PNJVyfEHQmds+LdBTI1w0aBoHORvrw36kFRD2yZF0w6BZgqrcM1OQU -xuJBWf3DTMRmBdldIIzGZQ7Lv08GB8xVy3472Q6nn8dTl0Gz3Y4ujMqbcglB4Auz -lwIDAQAB +MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA7J15ZaeDQPrhQsRj29wB +PhibH+Do59xsT2396L+uCg793gZlar5wZN2eHSh725cNQWyTAa9LwG+lXaKMukQ+ +8176CKR2J5sv3DezrGVu3x8V1qhyJyy79FlNZRVYTVqNaYzvJzxsVnFPpg7f8B7C +ffiqWJr9XkpqwRlCpxooXm4hplZ7uek5Ku21CzQ4OWg7hbuc+ZjCGzpXfm8NuosU +7TipnKGpEt0Agiph5g6TB2/scoeFar1CKMONIl80maxzAQk+xkWgiJ00+Z2qFCsx +ESfis/YkILS6RMFyZz7oa1WwMtUjYmrsRuz+jlFcbNuxZpIkaISiG9a2YdGcJ1Aj +3QIDAQAB -----END PUBLIC KEY----- \ No newline at end of file diff --git a/tests/repository_data/keystore/snapshot_key b/tests/repository_data/keystore/snapshot_key index 3364b4c2..266801ae 100644 --- a/tests/repository_data/keystore/snapshot_key +++ b/tests/repository_data/keystore/snapshot_key @@ -1,30 +1,30 @@ -----BEGIN RSA PRIVATE KEY----- Proc-Type: 4,ENCRYPTED -DEK-Info: DES-EDE3-CBC,CA9413D527FF4F37 +DEK-Info: DES-EDE3-CBC,9DA302AB20EFABC8 -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 -----END RSA PRIVATE KEY----- \ No newline at end of file diff --git a/tests/repository_data/keystore/snapshot_key.pub b/tests/repository_data/keystore/snapshot_key.pub index fc4ce86e..140ca251 100644 --- a/tests/repository_data/keystore/snapshot_key.pub +++ b/tests/repository_data/keystore/snapshot_key.pub @@ -1,9 +1,9 @@ -----BEGIN PUBLIC KEY----- -MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAhC3ksoCUEqNJZfvI3rgI -RuknTl3DQjO8H4wA98Y1ZfWSCGRCkD7z9wCtnRcP0r/DdRDozzIn0w5POYyZt+ql -aPBdigwWVZs4rB73U8Pjh/ZQ/QS2qngQlNmpkp6Fnh9FzmqQmIj0QYwZC6TPuB/2 -dVsnbyUDt+tLH4FCN688tOvV9fATO22rzT5XAeDCU046yD6fNtooCRVwd7mxV8Ug -npREULBq0cs82Cl581aM5znN1ydFr7cLMaxzvyAzJzAvdITOTdO7ZO171qm4OrGD -9W2ex+88ca3XYhjaEJGsoMXa00NPzrZPtFFUv6wSi9nqa5mpHmcEWAQABRRARlkp -mQIDAQAB +MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA9XqJohXw46tOKUiOMzPx +lDtrSlpy3WLH2zFSppN0eLIqByD4mk5nbyWKOzzGetQYgv9FzyER4AbmG40kD9bT +2jm3zxjoTnCoM+1Qt7khZm3LxcKBa7q1yrAlvSfNLauIC220kauVRn4Kehd+IqeS +/LhfOT6YyHUMH9SjZKM8XVHU1ehxTiA69eos4AosMK1Gf7jr042FzfiBTygqV1h5 +LXxO0IUYXiI4eCYTwzK4ChfQBmG3DGFGh2G8yrgqQZ5ERaBQPYG9rqQnfF8T8RUQ +o4n7yKpEKSWLOr6Uz9Y1pnHZG4YiKKbTe9EKGtrRbDMIfI+Mv5f3+n600nwZrN7K +OwIDAQAB -----END PUBLIC KEY----- \ No newline at end of file diff --git a/tests/repository_data/keystore/targets_key b/tests/repository_data/keystore/targets_key index 423015bc..a4f2c771 100644 --- a/tests/repository_data/keystore/targets_key +++ b/tests/repository_data/keystore/targets_key @@ -1,30 +1,30 @@ -----BEGIN RSA PRIVATE KEY----- Proc-Type: 4,ENCRYPTED -DEK-Info: DES-EDE3-CBC,112A90551702BF36 +DEK-Info: DES-EDE3-CBC,807D96406E906425 -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 -----END RSA PRIVATE KEY----- \ No newline at end of file diff --git a/tests/repository_data/keystore/targets_key.pub b/tests/repository_data/keystore/targets_key.pub index e13897d0..8080a88a 100644 --- a/tests/repository_data/keystore/targets_key.pub +++ b/tests/repository_data/keystore/targets_key.pub @@ -1,9 +1,9 @@ -----BEGIN PUBLIC KEY----- -MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAkbEPgkqMnOfprFY5XhCo -ppl6E89rfIzKv0AAZH6L6CruIzm2wgf3YRGl5Xle1+uyq9Wt0fcC32cZaoQPqpik -JWKYXdeuQjkUbPzON5wgV/MrjsidTC02296pmfHvGGlwUstjqwcbcCcAliw0V97G -IJ4JKnbelGmFFpUzzow8tp7uxxbM+ihLA1V2QdAJBDSIuma+A36zN0M23vvt6fBg -cDzJ+ij5Z3N0O2WAyQDONepRtHjKKdhAuzKRluLMwYO0sMjp+E88LMw1vUapxsEm -9jrYmY7aV6RFTe5OeZupH/0HyMwv7jua+aNzNxxD6/xYbh4Kp0jFke1zEBsoNrc8 -pwIDAQAB +MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAqF3xKdM9FySLLp0PiwIx +2O9CFikFBo4Xfm4Z0HW69lu2X5WQFx8/GL9kmo1QjZrOwYToYDe287nidbbLs/rT +lq3buN5wPMiD1GbVgGN/nknkkzv9KkJtrSF4RLbKrUnKo7/9C6IUmMt30wBk4GpJ +RZ+8wFfRhUE6859/f6Xl4XbtBJofbIGwV/OBdIzO5zIgB3uBktbbqBVjJb8Oj6Oi +YYskEIacP+TUrpa1iuC6nONj6ahI5NnEjt2B4/pLaUcEPm43kktJTabznkfNZXOa +2nMjngY8v8EbNLBpG6Y7MqZwwLZ4wnaTOe5Bp323YN9eVONXfU2gtT2MBoWExvFV +FwIDAQAB -----END PUBLIC KEY----- \ No newline at end of file diff --git a/tests/repository_data/keystore/timestamp_key b/tests/repository_data/keystore/timestamp_key index 614d4bfa..1222b6f4 100644 --- a/tests/repository_data/keystore/timestamp_key +++ b/tests/repository_data/keystore/timestamp_key @@ -1,30 +1,30 @@ -----BEGIN RSA PRIVATE KEY----- Proc-Type: 4,ENCRYPTED -DEK-Info: DES-EDE3-CBC,E950DB75AA748728 +DEK-Info: DES-EDE3-CBC,261BF7F755E46B73 -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 -----END RSA PRIVATE KEY----- \ No newline at end of file diff --git a/tests/repository_data/keystore/timestamp_key.pub b/tests/repository_data/keystore/timestamp_key.pub index 33d3a26c..7163fbef 100644 --- a/tests/repository_data/keystore/timestamp_key.pub +++ b/tests/repository_data/keystore/timestamp_key.pub @@ -1,9 +1,9 @@ -----BEGIN PUBLIC KEY----- -MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA0EhQ6twjA/9HywnQWNey -rthMpF2S3yeRQiU90TPWAAVsGYvKSgra/BjNTUdFZxf2twZONq/PW1CSQYya230i -16+4/6wTY47pSAoDfk71ISjFEdkZa7Cbvzv0CEv25sA+elaaCtRnmc6G7RuofwM/ -AnKqWdPfkJcdkjkmNGYVhNU7sF96hNSdG3liWG0rbByZVxPYMYru18gziGQOkp82 -Ex/6SWEkUzdjZHqvL7A3bEUJ2KzHoHvf3SdyRP1HbASphnWEvBqC5/3TnmsAHvWi -d4NRqKLJlsLnySGP5gSinjc53bN8CTJz+ThSaIFlt46RpNtkjm5ZU8YjmmqfbUSG -FwIDAQAB +MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAyw8QAC0PNyyf0AV3qzSp +DgT74sGKC/72HIO1EskP4VEkdfh67PROHCm0YJTYLch9zH+uHIsmyyuzNr6go2Nv +GPSRwYEP34LJlmqr699zkjSXw79T/t244keFiL8SFWTlWmQyTPDdn+N2v4acAmvW +xSFcjTl8cVIGyGuU2s/vHrBn0zoOJ7ZIGAFzzCGAm0j6VvGvkxy3mymE+8VjzrAV +9P1aOMdRVmlqCyPlGVW66Lvz7wkQKcp7rf0CEKkBGlYMtgTqiiagHJy0Sv6qAapw +LXzE6ZdM40E1J1rT9GUitd0K4LhpSjW1lfipSbNQDLiZTG9R2EhnDMl5suaIaFh0 +UQIDAQAB -----END PUBLIC KEY----- \ No newline at end of file diff --git a/tests/repository_data/repository/metadata.staged/root.json b/tests/repository_data/repository/metadata.staged/root.json index b1b34fa314f9ea032a9ac600bf4a5ff167642ce4..f4f0b55d5e6f7847fca8570cfa1fec44ca0228dd 100644 GIT binary patch literal 3756 zcmd5<+peoP5`8~kv9(__t!<32{gx{sB;-b}lTphC69@?q$PHTk@8e{jbJQA*X1eD* z^h5+jWxGzB9pGTx`+axGN({`1@4zr4Nus=sgViK^m2Tiyw0L=r-MY7${e zlOk#;U;;lxzM?`9pDGYw6POSyf_z8_fRso|r7|f-?|*ywJE=>Qy&TTw&H23f(_}vX zI;10b{*i(P3G^C6gq#P2AdV#@j2fnt5UBui29-jPNJc2oX2J|?GDdM2`W*XM_(J)J z5bgs21b~K5nL>e*9CKfo3<6*>0YYjB5RlL`kQo>fBc^;1a84mNl_7m@h=Ah&QJ`H= z%%lL+4>6Q5Y`O44>Btakml3;~-C=KmFnkvv6 zftbi33@L+p0y0AqQp}|kQc@`lZBl^>xS$OC2oUm2k=`L^Cc!#s1k+GxN2V}!un>_j zz?^^}REEz1qd_P*M0!sY_yYJy?-%L`iU29iK#{^K zoTDVR_#L#G({8WX9JiZO)_jh)_m2MEZSwVbkVWe|BA2nVh{vOP^4O)v&ZU?i^U7Qe z`>^iK^V|h$XrEhE2b~g}%4+3|>)KM6>1Kc9y}IhoP*a{P1d(oaQ}q|4 zKA#`9&WNp-{e_{ThZi>qS6PXgSvhhHE2M`4AlR$JxaC#*^U{d$E*-8PV_+{f>)1xB zz47j~gW6FzZJjK$WA&^ETC6Je=jj%AXQDNoY%E@!r)k(@9-A$uyEH>7t(?5;%vZy4 zK^8UN7-Vf8~SA713u%ZJ4Z-weiCq*cU!H z%?Dc==cpP~73IC7yPYjJ-tz2^Fj z+4_W{{J5o8m0;%zjdndn+ruKEU63=-KH18n!z3%!d<P^%&RozGYrp$wkU@Yk3$* zHa?JXyh}HmGU?jFpESHhkqaZfwn;j64@5*ZcGK?R0$b=KABu(Fot%)FB$t6FWSu6V zyIqfk&OtJAddCY>N{xbTLA-#pz`yYw;d`3LXF{H1MG)^!2Pk6XWJrJ&Bn1G=@XX0NW> zR`Uc7WO=i0$)hq))v4QH%R^l>bxHZe5aUrWTb_>Rb~QO#%M~HRTTSoD)bVp#grMDZ zl9qMcj4pfc9LHkcA5>s|Bj={b@6R?`cgZ#wA<#7jM&WU55nlq}pkb8H4=dvs#`#>j z)6Ov7dKRC-Zj^RLM_gWoEqW2yrHensR!>d$5@bboeBCfU#a8hYa=-6vaq#W>@w|Ne zaMOIX;ir;(db>ng2}arRsV9x6>1%8Lop5|b_4U&8`#^qJ6#k(=E~410%XcaIZ{ql2 ny8mNw#3xl=#3}y|rTJlo_%D&@;ZMNaR8huR`dPEzzWnx2-VHtB literal 3756 zcmd6qS+lB0635@~r#Nw5oepcGs^~XF+{ImwjuRa~6$%0(tJ2Zmy-Ux%9X%5hGu?9^ zdJJ4B%Bp{6{>V&z{_^&o2dBg-%1q|(Kfe9_%iG(}>izb9k&i&Ak$1_Mkkr*A_ZUNj zU_f1@>k=Sc0}wIC1(^AQ69hQc)R)hZuTjsVSW-yY``=#c#j-eGUkff*#%g8!Fkh{{ zUQ!C48&T;3M3623!BT_7bp(VOlhng5;ywo&cA4&~Wd%W!awwG+U)P0)T%Q0(x)68_ zQ{d^Y<`U_+9?=-le2=-FWC(I1kOq7RknmjJ)pX!+h;#sfBmf}-Jzqz_LB9K(z>u0L zT#c#efKw=>=PMOR0z%X->QBC>N#a3>1i>0`T&ZInNC)TwGnaYX;hvHpG$NT2=W^Z0 zQgT9x=VRTK3?mEyKmZWNl_^lUkrD=!{lJ4*_{ymsg+4`?Bcfy>A=J7=gVgmsU%6Il zp47OGA){&|;_K9bp65U&iE@}Hh4QD6${Rj@4kKUr)>Yo6j7t?#j4>5RnNJ9$Ncb98 z4i_8(M-x5(!WXWxNCjCFNFok(M;QhvV+3ewLnVs=$tb~07X(mGg@$04Gl&rs5c@=6 zN*UH%h?FyQs7diOR65Aj6(vMLgpsC6<|=!1@vb=e<;&YY6fZ9*<&%~tjPE}ck5oPp zSzfQj3z~QDn&d%V$fWowPn8`mO-JXJX_Xoak;fRdJi7TUi-0P*A zrn%OD4X#zDksx=KLhzROj{P`>I_yJC%xxZd`$E(+sAw6Z0WMMmfB1n z%5=~MgU7V4SXeqO$8PNVy-VBoy?Z9(6okEtgpJk7uXg5f<;i;3H5Nwan7o2x+1-DQ z*OTEF#p>fT?|par{L1GGpuYyI`)!HI-f7w8nY5ZH4CX_2Oajs0)a#rYleit%ozdv9 z+{VL2xb2R`nCkvLK5ORvqZBk6t(~;&mWA1yRkLv)8jdxX9+N|xx;{h)w)vgow$aWAL?&Ra67q<4vvsj9Cda+?yjfHxFaGl z95(&Kf=w#G`yGDYJ}_ZuL6d}wbp-ki-jgz%-dyh#b%?_Dm_+{zRuXWBae=TZK-3dJ z*9lcg%ZDn-Ll-!{PCUej04fx~^9A9$s?!)sB$+CZzvZE4q5o^JI-B^CU(JnmvquNN z8sh9UQ~@b3$vVQ_ML6RFQX4wo!-KzDnf;}C>9R(z%_3QPbC9*WV%fbm%`gGAHlOVu zhoW_hj)^nxh~QN3wuhW#9fmi29-iBWMSHPAx8$^l>^Qxow3nRqUK8II@h}97eSI+L zdR}(g=$MFYp16-qw`diU0qL1G<-C|xTirL-Gla5ct$i4>YiskQMsiF0F6?UWUQ*vs2^GK%s^@>uof+G!Oep$egEb8|gvTI=)58TO)r z&}C{DmoU~28@>9Dz%dXMu3FEWb%ASqf?<-p%zCC_CA3naj zXuituvy*(TR-9EQc)mu@77ad!zLw_SHI6S;Umtq@Nyzu6**_$taI%vuerrYljgIeb nPXCyWAeMRI#OZ$s&39*r|Dr@){sg9ySsq-I&zSx4<(FRp)5AQF diff --git a/tests/repository_data/repository/metadata.staged/snapshot.json b/tests/repository_data/repository/metadata.staged/snapshot.json index 017cb34e3f9804c3db056525204f5d1f78e395be..3d2aa1881dd6f26c198d6efb8656afb2e25e2e09 100644 GIT binary patch literal 1380 zcma)*!EPKk42JK1ip89pR1`&0$}MkD)IGH*3X-C{PE)Vp-4;!QynDIh1Sn7#FfcQR z(G2AfY>Rx9I-F6o zP7AZ4$X$fP-MA>M#28}jhQk0b%Ak~&OrcdBCM`&Av zE7rxdV0FU9RZtd2@si5jCK<)@)p|%{A8H^XxtDCKS-C^CCG0s7retra&RTj7CdTZ` zm;w|l$qI^*%6kqdL>5>;K++>`>F?rVwK$*>VqlNv(R~m>@bpk47+!~sl{<3N4lFZ9 zppf}Ub5b#_%0QHIr0N)ROHRJr9DYq+b|~XY%N54$x67x;xFT}9T{f#ov$j z&)9CYV0lv@ub)bJw_iU$f0W$m*E)yo>2$ih{ct`#e0y`)j(R@s=Y+I_V0wiYu{@7O4lWlTcD!cAvj> z|BoyfTwaj%_U@lo8J6+&iCus*N9+O8V&Cz<3Un z=I75}pZxjmuw5SRAMN_*b6mD}hhOdvhtK&tY`@2+2hYkD0b(p(Xd?FMpjvx#S-8}3 zfGw@FaCPXqU@otsKuj1Vc*>4;{&;+UJidSP!}0iaDIMGzNznn!9tBD* z9!+|ItV{-rYC3|bGeuA(jgTl!rq@Aiu=-dWldG_3^yRCU>0JN`FvW;TDUQ0B!>wS|+BAGf$Vi8y0=8UQ+$N?b(o&nfECR2!)U~EJ z09G6p02Rt=ow$KbU8_z~ETuykHVRlb@D^MxXC zU?%|~Mo#Z4g_(M0hize)$y7n<%c_@REd?`SpzbgRRZy(Aaa;m2L~AUibqPvtI$L&P zh1q7-b<}Wi;iWy>wkKz3OYu@(+3Q~y%aY|r-F5*s z@Jno;m&#*=?~sO<0Xh86IsEee>-#UievHDCwJ|BDeaskqQlxUu076J7Aq7%8qG9Ps zn1DG)31mZL?F|b1H~n@9DU=#Htu&xESrSUJI-moaM3l-~qg*s1U^0|dE|XGd!GLu> zs;ohP#pH4#C6S{p1+<0|Glgu)rRa=HO;M+b`_Wy;AW%v|shH2_${ zG3ZPhat=~x)T|>AfR~A~)0|Aq-neqWN6NyPtN^kIE)`0$l07GliChc?I)U*_sl3ld zN2#rFgasJHA!+HO6w*j93qEO-07@BA`jfJ@++uKL2bqKezAQX2ruvK1L3@O?}5nZ|nk0qz512{wMrOjSthg@no z#UPU^Dw8tg&^t>d5upWOQI{NfO$L23oWW?aQ5NrF^qi$dWq`{sU|W23D3}#$G{#8E zStKjpd64GquWyGROT>nZOTwtAI0cmjqA7`i3Ar?pg)y)!S(}m>BfG*Ylq;VjgRuGS zr|&)=R&BZ*PAdJ_xi}kVp@z%nyW46vX^hRjwPBn!JDudJuK)17&btn~>>E$L)B4%$ zVSYLA+xGLten*|@dbYc1Pj9;Q$2h5)x7Etf@#dtnO~?1=)8ppgY_h#dwf@y+cXjUL za$Y-I<5D*dv~rF1d7Ml-mEY{Q-h8O$Pft(N)@_B8_JTe=ye{vqWizd5<@L$bU$94wNLL5PDT$s9Cdcr{o87|+^=f8`f_kJ?>OkmI?nts zJlEZMBgA228b;V&OpBcDxWA4~d}NbbH@f%uvh3;6q`JPXHoRQSTMIR>ZTiDaqbc{7 zr`u!d590E8&S}ehdS5x`|9t53YimKHAzbTa_paOSPWCUeBIEAu`=h}RDdNLsU4~!& Y_<6H_=HK7?M?e1W2>;M1& delta 1112 zcmbu7OLF2j5XLKy$s(IMLZ+s&NwGj`y;@lfVDJkJ_<=ED(^5;u1N>pIjj=cEvP;#Z ze2MIHfgC5th-R)3l@@w-_xJnyueYDye*XCbS{=wl#30cL!t&gUi0E|o93UG-*`wqH$s~|MdC*pR1Vb)r zA&JZMDTtJuve{YVRR$*z9Asp}C1#caFsG7Ki4u<#b4~!>mzCPbWV5$EX|GgZ|3yz! z?1DnmpvfK$c` zOp$VuA_W9e*@V3}KvJ``p+pwCyd=(+g9Q_aoG3y<@Wc?M%V1(r1XkpN#02CfZ=Gevf6VlEQQfYCl*N;_W&Y;a)n@$ z>Gh}Ye&6c`QQSPM?k3l|yN`>bRnJYUPxGgqIIUeT)gka?W-oS|!}feB>*wwM+{o78 zvNQU$9`pX9@};K#;dcUuP?cjM7m?6%XY*}Gd@-W*KKLvwCBmTcm{ZU-hP4S z-U>^B(Ntx7l6dN_D#->%LE z9b3BXeb-o@MJkRn>gf~Tk9oJSuq_wWe;Ul)T{o|+F`f^$=!p-frUZe4_KOU~<%hS8X z`m(ipuEMa>-yIK@SIbtjQJsu$Zyt}H2`{dW)1-U7JKN4%vu00000 diff --git a/tests/repository_data/repository/metadata.staged/targets.json.gz b/tests/repository_data/repository/metadata.staged/targets.json.gz index 9e6ad7d2a323694978a84d59aab57ecc347ce781..874b7b776cd2fd24a0cc20229b4af25df0f9429c 100644 GIT binary patch literal 1215 zcmV;w1VH;AiwFRUq?1zu|E*P9QyN(ie)m^Uye3)C{gSsq&_!+vg5r|eI+s3x;(#!M zB;|ik1BPT%wN?AD)G(JmXS)0A{`#A5C#CXgI?i43vV>LnbLp>>Qt2E1OXWw{P9s{% zj_4?*^GtE=Qn1>y$Wlti5JD9--bTh<5I|KV!iJO_HBnjzT#G>JP$r9XEA4Qgofxb zJP7V6mqf*Y`5NLm)s7M>sHT{F}prGAZ#TFbbbcibRV7-$F8J!Rvu#=8kqeKKvWzsCE z0BB3zS?U>L^h`t*w2~3qK{zo1v3V6Wk0NLeV z4?+kUQzBkcZKxw(mL)@EI(phW{*lCT znAdLhb6=^vc=5Ao*zNnY`&DWe^>*p1Uuo9QN{!lZZye>Vdc9JA$l8_h3d4Jp=CiFdx<87tefL)&UP+d|U-C9!u!+GKgKf+@T2Qt3N>OXX>No&=1P zjS>!|m)2REk_?Au7K2qm0n@=6LInjEKzPlG0%EzNl5%RLBqEeQ9Ps9GlP?Z{*Zr*D z&puxD`)^B_!9LEJl)`yOjV53a(-Lot7Rd(3lH`!Ap&_y;Bcp^{CKR=XNzh&p3IQ)0 z9f?tMm9&zUC#H#Enk5rtK+Hr5Gbv(boFE?X5K?5!TD;>vs$`Xo!gB6{-Pi0nSX^P8 z6BiPfD)`8h4Tj+<)*B&%6p(13xBxUrr)cC@I&(HkPpk`yC1R{}3_OCmCL}Tr_=^-I0r4T4NfZICK?4$$w8v6{ za)Fx|jWsUBGK%5#r1a&(n<(R6EqgJR|Jpn+<6a^ISdHUmeb7xg#917}X0j;OZyOJ) zeSavGb<)9Wo7A32Vy))DQBX_sTDWZshM1eA3%m>Cbk`ZZqM>+uiNMf~y zqi~PmEzExIE0vdzZZ`4zeINIqTH~yBS-QHZwOjSlMdSVud?-4tR;~3lyR40;-}33? z%s!D?)@wAgtXt1|Cfl#q#}|0t$V3L+F2d(GSJ9=~IVXRPN^djD=CI z`n%OxV>+kR>qd7fY5i)@y#2Pk?cS`P=;n@u$85G6SDEm;;q8#@whu+EGnozUPe*c7 zo2uq;G|V3_nQ2`&Uf6SIVNm+T`8iMZ>(dujjkl9_ZPYavo!h2bGcL&OL$MiLcANR! z?5e83v8r^;)k|Z%xV&mAoSU!t%@ge|W{r6-6S-qsquW)Z`pvT%Wc10LM(jg{y_|iX zPvy!_+r4bm+;$qPkul8N<##5lHK!M%2RENjS4FF`AS5nDh4vmzecNq4RhEN?V&?A0 z+iC@dz3#BLoX#&Mv*n<^crIM8+1<6S<~Q?tEh|0`$M=ut&tUpDtvG7m?Jl>|dfx6_ zJrwzM3Yp$p6^vHopUHa`gRtG24$C{aN0ZleZGfakrctUkznO zIQd8@UIRit9j?)F65rBzpTfg=^l;w$EatGzr}4@#j2x< zv6#Af=9YjmYE`>Yf)1Wt zW-lqJWhtUSXl=y*#8wE&dUD7lbvTzXk)#1z6)(NyGH1ytH};t;7VOk#O71XH>zNT1 zMoeLa$+Bpen6pWrtqj;~aDx=n*vzwr!?;?s#YZt})xixDVJ=wnr~m|93gw!k zO=~^6I1SI#v`+IDS*haSxB`hR#RmyscCFAln8>6GTDO7Fp%%~~m{N8ko6}0h939Mk z_Edlbsl)(axon#vY*Nfn(Y+da?VerN~@U zPC~2^jxeYwhY4DOlb4I#*Wl$0rEj!sFfQNL-;cf_a?_q}eLbDIS%%;FZCj5I_ZzwY zsXf2Eyh8W%aQBM!*G+q?E&lv?d}VzhNN@=l&kexU`T6`Ry}tGNC;MR@?>xOTH2U18J=pS+Jv; j`U)dAkGoQkDWi1B(q->0k|R7keTIV?u^LGQV_^R6KR$p?F#&dt5ZoYviy-O;MT0nI z0h!w)=CJaPiIH849^LUUC&0RMpFc*fO7n|uwQwVwM3WV#BEtK!kvau~Pl z~6*Z6%)y+>1$b*MhDjzJ}YLOtMp%!2t!T;nz*olP-D%qx}%4{_pSae|`HO D%I2gl diff --git a/tests/repository_data/repository/metadata.staged/timestamp.json b/tests/repository_data/repository/metadata.staged/timestamp.json index 73cd3b4c18f1b2bcbb01645f57651a25410bc321..61dbbcae767f9658fee8c01e00322029b6131165 100644 GIT binary patch literal 924 zcmXw2-Krcn48A{4vCMT7Bukd9+~o~QH&-pC$hMT-r00k1wlsvido^+uoi2csQND`uF^N*ucuoEfiKq6E%C!;=qr3bSk z*z8zjR_jUWq%m=IuMyqGkf~T#Ll@MM${LxjG8x!;jKoHzHf0wxT9S2=20@JlaP&6Y(pj<<=#xFXt>|to zk!6-y%itI;>8ooS6HFnYJ=&iH=KW%rjvEQ_Ib9JZ{*oIPi_^FjfrOo5WNic7;N3X5}KTG#5i z7nNuhwd$5R`TlVFGxKtU()YCNVch<@e|_;iA|I}ge!JV_%aNST|9iQ<=61_@!5hF_ z9{}DR>-X1789e&|l(p3E^?gJ`{;!?@%rQv3nxpY7lbZAR{~v(e@p%9M literal 924 zcmXw2ORgL@4BWp{40;_Kq$qymoo^6iyh;!Rltg*P&ilYK31S$&d)WPKKn--0B3Z1e z{`PR%Zr6`be*fo7+_rb8KORn}Z~2_I&++v-GqSDGAPl!`x@m(IHb9_6 zbSl=R6zFw2TN%ACG({e3VqL2=1!kvG$=Wd1s>%)~y4DJcHpqp*lvE?ItQuJ_5-_!L zb+aCl_0~!@Efd>l#4(}Fg*av(9<^ZMmEXkZu@_Z^mX$J>pjy#|m1Qjv;DC{DtRM{VDD9=NM)#0D^Ug4$T0Ey=CrMyqB#t6Nw78j= zEKUW12#uSth4-Y?2ja{PQJiNi?l5|AO5bNI1C`jU;sUWglp+}lYqNJj)Hb7uwlPO~ zq^63Fv?!NZv(_zr^8MlTXZqzZW$bR*&A9z_|N0WUMLt{~<97GQm%}+*{P%Kw_3f4f z!W+Om9{}DR=l9o3X*~bYDEmJ7%kA=ffBWh7{PZ(=+Ae;(9R5GNpA&BvFD(1W)uuxS z?>>plDFs^tU3T!(#y~doo^FD4R7?3rBn RHM#v8UvAd}6h375`~O&Q_Kg4l diff --git a/tests/repository_data/repository/metadata/root.json b/tests/repository_data/repository/metadata/root.json index b1b34fa314f9ea032a9ac600bf4a5ff167642ce4..f4f0b55d5e6f7847fca8570cfa1fec44ca0228dd 100644 GIT binary patch literal 3756 zcmd5<+peoP5`8~kv9(__t!<32{gx{sB;-b}lTphC69@?q$PHTk@8e{jbJQA*X1eD* z^h5+jWxGzB9pGTx`+axGN({`1@4zr4Nus=sgViK^m2Tiyw0L=r-MY7${e zlOk#;U;;lxzM?`9pDGYw6POSyf_z8_fRso|r7|f-?|*ywJE=>Qy&TTw&H23f(_}vX zI;10b{*i(P3G^C6gq#P2AdV#@j2fnt5UBui29-jPNJc2oX2J|?GDdM2`W*XM_(J)J z5bgs21b~K5nL>e*9CKfo3<6*>0YYjB5RlL`kQo>fBc^;1a84mNl_7m@h=Ah&QJ`H= z%%lL+4>6Q5Y`O44>Btakml3;~-C=KmFnkvv6 zftbi33@L+p0y0AqQp}|kQc@`lZBl^>xS$OC2oUm2k=`L^Cc!#s1k+GxN2V}!un>_j zz?^^}REEz1qd_P*M0!sY_yYJy?-%L`iU29iK#{^K zoTDVR_#L#G({8WX9JiZO)_jh)_m2MEZSwVbkVWe|BA2nVh{vOP^4O)v&ZU?i^U7Qe z`>^iK^V|h$XrEhE2b~g}%4+3|>)KM6>1Kc9y}IhoP*a{P1d(oaQ}q|4 zKA#`9&WNp-{e_{ThZi>qS6PXgSvhhHE2M`4AlR$JxaC#*^U{d$E*-8PV_+{f>)1xB zz47j~gW6FzZJjK$WA&^ETC6Je=jj%AXQDNoY%E@!r)k(@9-A$uyEH>7t(?5;%vZy4 zK^8UN7-Vf8~SA713u%ZJ4Z-weiCq*cU!H z%?Dc==cpP~73IC7yPYjJ-tz2^Fj z+4_W{{J5o8m0;%zjdndn+ruKEU63=-KH18n!z3%!d<P^%&RozGYrp$wkU@Yk3$* zHa?JXyh}HmGU?jFpESHhkqaZfwn;j64@5*ZcGK?R0$b=KABu(Fot%)FB$t6FWSu6V zyIqfk&OtJAddCY>N{xbTLA-#pz`yYw;d`3LXF{H1MG)^!2Pk6XWJrJ&Bn1G=@XX0NW> zR`Uc7WO=i0$)hq))v4QH%R^l>bxHZe5aUrWTb_>Rb~QO#%M~HRTTSoD)bVp#grMDZ zl9qMcj4pfc9LHkcA5>s|Bj={b@6R?`cgZ#wA<#7jM&WU55nlq}pkb8H4=dvs#`#>j z)6Ov7dKRC-Zj^RLM_gWoEqW2yrHensR!>d$5@bboeBCfU#a8hYa=-6vaq#W>@w|Ne zaMOIX;ir;(db>ng2}arRsV9x6>1%8Lop5|b_4U&8`#^qJ6#k(=E~410%XcaIZ{ql2 ny8mNw#3xl=#3}y|rTJlo_%D&@;ZMNaR8huR`dPEzzWnx2-VHtB literal 3756 zcmd6qS+lB0635@~r#Nw5oepcGs^~XF+{ImwjuRa~6$%0(tJ2Zmy-Ux%9X%5hGu?9^ zdJJ4B%Bp{6{>V&z{_^&o2dBg-%1q|(Kfe9_%iG(}>izb9k&i&Ak$1_Mkkr*A_ZUNj zU_f1@>k=Sc0}wIC1(^AQ69hQc)R)hZuTjsVSW-yY``=#c#j-eGUkff*#%g8!Fkh{{ zUQ!C48&T;3M3623!BT_7bp(VOlhng5;ywo&cA4&~Wd%W!awwG+U)P0)T%Q0(x)68_ zQ{d^Y<`U_+9?=-le2=-FWC(I1kOq7RknmjJ)pX!+h;#sfBmf}-Jzqz_LB9K(z>u0L zT#c#efKw=>=PMOR0z%X->QBC>N#a3>1i>0`T&ZInNC)TwGnaYX;hvHpG$NT2=W^Z0 zQgT9x=VRTK3?mEyKmZWNl_^lUkrD=!{lJ4*_{ymsg+4`?Bcfy>A=J7=gVgmsU%6Il zp47OGA){&|;_K9bp65U&iE@}Hh4QD6${Rj@4kKUr)>Yo6j7t?#j4>5RnNJ9$Ncb98 z4i_8(M-x5(!WXWxNCjCFNFok(M;QhvV+3ewLnVs=$tb~07X(mGg@$04Gl&rs5c@=6 zN*UH%h?FyQs7diOR65Aj6(vMLgpsC6<|=!1@vb=e<;&YY6fZ9*<&%~tjPE}ck5oPp zSzfQj3z~QDn&d%V$fWowPn8`mO-JXJX_Xoak;fRdJi7TUi-0P*A zrn%OD4X#zDksx=KLhzROj{P`>I_yJC%xxZd`$E(+sAw6Z0WMMmfB1n z%5=~MgU7V4SXeqO$8PNVy-VBoy?Z9(6okEtgpJk7uXg5f<;i;3H5Nwan7o2x+1-DQ z*OTEF#p>fT?|par{L1GGpuYyI`)!HI-f7w8nY5ZH4CX_2Oajs0)a#rYleit%ozdv9 z+{VL2xb2R`nCkvLK5ORvqZBk6t(~;&mWA1yRkLv)8jdxX9+N|xx;{h)w)vgow$aWAL?&Ra67q<4vvsj9Cda+?yjfHxFaGl z95(&Kf=w#G`yGDYJ}_ZuL6d}wbp-ki-jgz%-dyh#b%?_Dm_+{zRuXWBae=TZK-3dJ z*9lcg%ZDn-Ll-!{PCUej04fx~^9A9$s?!)sB$+CZzvZE4q5o^JI-B^CU(JnmvquNN z8sh9UQ~@b3$vVQ_ML6RFQX4wo!-KzDnf;}C>9R(z%_3QPbC9*WV%fbm%`gGAHlOVu zhoW_hj)^nxh~QN3wuhW#9fmi29-iBWMSHPAx8$^l>^Qxow3nRqUK8II@h}97eSI+L zdR}(g=$MFYp16-qw`diU0qL1G<-C|xTirL-Gla5ct$i4>YiskQMsiF0F6?UWUQ*vs2^GK%s^@>uof+G!Oep$egEb8|gvTI=)58TO)r z&}C{DmoU~28@>9Dz%dXMu3FEWb%ASqf?<-p%zCC_CA3naj zXuituvy*(TR-9EQc)mu@77ad!zLw_SHI6S;Umtq@Nyzu6**_$taI%vuerrYljgIeb nPXCyWAeMRI#OZ$s&39*r|Dr@){sg9ySsq-I&zSx4<(FRp)5AQF diff --git a/tests/repository_data/repository/metadata/snapshot.json b/tests/repository_data/repository/metadata/snapshot.json index 017cb34e3f9804c3db056525204f5d1f78e395be..3d2aa1881dd6f26c198d6efb8656afb2e25e2e09 100644 GIT binary patch literal 1380 zcma)*!EPKk42JK1ip89pR1`&0$}MkD)IGH*3X-C{PE)Vp-4;!QynDIh1Sn7#FfcQR z(G2AfY>Rx9I-F6o zP7AZ4$X$fP-MA>M#28}jhQk0b%Ak~&OrcdBCM`&Av zE7rxdV0FU9RZtd2@si5jCK<)@)p|%{A8H^XxtDCKS-C^CCG0s7retra&RTj7CdTZ` zm;w|l$qI^*%6kqdL>5>;K++>`>F?rVwK$*>VqlNv(R~m>@bpk47+!~sl{<3N4lFZ9 zppf}Ub5b#_%0QHIr0N)ROHRJr9DYq+b|~XY%N54$x67x;xFT}9T{f#ov$j z&)9CYV0lv@ub)bJw_iU$f0W$m*E)yo>2$ih{ct`#e0y`)j(R@s=Y+I_V0wiYu{@7O4lWlTcD!cAvj> z|BoyfTwaj%_U@lo8J6+&iCus*N9+O8V&Cz<3Un z=I75}pZxjmuw5SRAMN_*b6mD}hhOdvhtK&tY`@2+2hYkD0b(p(Xd?FMpjvx#S-8}3 zfGw@FaCPXqU@otsKuj1Vc*>4;{&;+UJidSP!}0iaDIMGzNznn!9tBD* z9!+|ItV{-rYC3|bGeuA(jgTl!rq@Aiu=-dWldG_3^yRCU>0JN`FvW;TDUQ0B!>wS|+BAGf$Vi8y0=8UQ+$N?b(o&nfECR2!)U~EJ z09G6p02Rt=ow$KbU8_z~ETuykHVRlb@D^MxXC zU?%|~Mo#Z4g_(M0hize)$y7n<%c_@REd?`SpzbgRRZy(Aaa;m2L~AUibqPvtI$L&P zh1q7-b<}Wi;iWy>wkKz3OYu@(+3Q~y%aY|r-F5*s z@Jno;m&#*=?~sO<0Xh86IsEee>-#UievHDCwJ|BDeaskqQlxUu076J7Aq7%8qG9Ps zn1DG)31mZL?F|b1H~n@9DU=#Htu&xESrSUJI-moaM3l-~qg*s1U^0|dE|XGd!GLu> zs;ohP#pH4#C6S{p1+<0|Glgu)rRa=HO;M+b`_Wy;AW%v|shH2_${ zG3ZPhat=~x)T|>AfR~A~)0|Aq-neqWN6NyPtN^kIE)`0$l07GliChc?I)U*_sl3ld zN2#rFgasJHA!+HO6w*j93qEO-07@BA`jfJ@++uKL2bqKezAQX2ruvK1L3@O?}5nZ|nk0qz512{wMrOjSthg@no z#UPU^Dw8tg&^t>d5upWOQI{NfO$L23oWW?aQ5NrF^qi$dWq`{sU|W23D3}#$G{#8E zStKjpd64GquWyGROT>nZOTwtAI0cmjqA7`i3Ar?pg)y)!S(}m>BfG*Ylq;VjgRuGS zr|&)=R&BZ*PAdJ_xi}kVp@z%nyW46vX^hRjwPBn!JDudJuK)17&btn~>>E$L)B4%$ zVSYLA+xGLten*|@dbYc1Pj9;Q$2h5)x7Etf@#dtnO~?1=)8ppgY_h#dwf@y+cXjUL za$Y-I<5D*dv~rF1d7Ml-mEY{Q-h8O$Pft(N)@_B8_JTe=ye{vqWizd5<@L$bU$94wNLL5PDT$s9Cdcr{o87|+^=f8`f_kJ?>OkmI?nts zJlEZMBgA228b;V&OpBcDxWA4~d}NbbH@f%uvh3;6q`JPXHoRQSTMIR>ZTiDaqbc{7 zr`u!d590E8&S}ehdS5x`|9t53YimKHAzbTa_paOSPWCUeBIEAu`=h}RDdNLsU4~!& Y_<6H_=HK7?M?e1W2>;M1& delta 1112 zcmbu7OLF2j5XLKy$s(IMLZ+s&NwGj`y;@lfVDJkJ_<=ED(^5;u1N>pIjj=cEvP;#Z ze2MIHfgC5th-R)3l@@w-_xJnyueYDye*XCbS{=wl#30cL!t&gUi0E|o93UG-*`wqH$s~|MdC*pR1Vb)r zA&JZMDTtJuve{YVRR$*z9Asp}C1#caFsG7Ki4u<#b4~!>mzCPbWV5$EX|GgZ|3yz! z?1DnmpvfK$c` zOp$VuA_W9e*@V3}KvJ``p+pwCyd=(+g9Q_aoG3y<@Wc?M%V1(r1XkpN#02CfZ=Gevf6VlEQQfYCl*N;_W&Y;a)n@$ z>Gh}Ye&6c`QQSPM?k3l|yN`>bRnJYUPxGgqIIUeT)gka?W-oS|!}feB>*wwM+{o78 zvNQU$9`pX9@};K#;dcUuP?cjM7m?6%XY*}Gd@-W*KKLvwCBmTcm{ZU-hP4S z-U>^B(Ntx7l6dN_D#->%LE z9b3BXeb-o@MJkRn>gf~Tk9oJSuq_wWe;Ul)T{o|+F`f^$=!p-frUZe4_KOU~<%hS8X z`m(ipuEMa>-yIK@SIbtjQJsu$Zyt}H2`{dW)1-U7JKN4%vu00000 diff --git a/tests/repository_data/repository/metadata/targets.json.gz b/tests/repository_data/repository/metadata/targets.json.gz index 9e6ad7d2a323694978a84d59aab57ecc347ce781..874b7b776cd2fd24a0cc20229b4af25df0f9429c 100644 GIT binary patch literal 1215 zcmV;w1VH;AiwFRUq?1zu|E*P9QyN(ie)m^Uye3)C{gSsq&_!+vg5r|eI+s3x;(#!M zB;|ik1BPT%wN?AD)G(JmXS)0A{`#A5C#CXgI?i43vV>LnbLp>>Qt2E1OXWw{P9s{% zj_4?*^GtE=Qn1>y$Wlti5JD9--bTh<5I|KV!iJO_HBnjzT#G>JP$r9XEA4Qgofxb zJP7V6mqf*Y`5NLm)s7M>sHT{F}prGAZ#TFbbbcibRV7-$F8J!Rvu#=8kqeKKvWzsCE z0BB3zS?U>L^h`t*w2~3qK{zo1v3V6Wk0NLeV z4?+kUQzBkcZKxw(mL)@EI(phW{*lCT znAdLhb6=^vc=5Ao*zNnY`&DWe^>*p1Uuo9QN{!lZZye>Vdc9JA$l8_h3d4Jp=CiFdx<87tefL)&UP+d|U-C9!u!+GKgKf+@T2Qt3N>OXX>No&=1P zjS>!|m)2REk_?Au7K2qm0n@=6LInjEKzPlG0%EzNl5%RLBqEeQ9Ps9GlP?Z{*Zr*D z&puxD`)^B_!9LEJl)`yOjV53a(-Lot7Rd(3lH`!Ap&_y;Bcp^{CKR=XNzh&p3IQ)0 z9f?tMm9&zUC#H#Enk5rtK+Hr5Gbv(boFE?X5K?5!TD;>vs$`Xo!gB6{-Pi0nSX^P8 z6BiPfD)`8h4Tj+<)*B&%6p(13xBxUrr)cC@I&(HkPpk`yC1R{}3_OCmCL}Tr_=^-I0r4T4NfZICK?4$$w8v6{ za)Fx|jWsUBGK%5#r1a&(n<(R6EqgJR|Jpn+<6a^ISdHUmeb7xg#917}X0j;OZyOJ) zeSavGb<)9Wo7A32Vy))DQBX_sTDWZshM1eA3%m>Cbk`ZZqM>+uiNMf~y zqi~PmEzExIE0vdzZZ`4zeINIqTH~yBS-QHZwOjSlMdSVud?-4tR;~3lyR40;-}33? z%s!D?)@wAgtXt1|Cfl#q#}|0t$V3L+F2d(GSJ9=~IVXRPN^djD=CI z`n%OxV>+kR>qd7fY5i)@y#2Pk?cS`P=;n@u$85G6SDEm;;q8#@whu+EGnozUPe*c7 zo2uq;G|V3_nQ2`&Uf6SIVNm+T`8iMZ>(dujjkl9_ZPYavo!h2bGcL&OL$MiLcANR! z?5e83v8r^;)k|Z%xV&mAoSU!t%@ge|W{r6-6S-qsquW)Z`pvT%Wc10LM(jg{y_|iX zPvy!_+r4bm+;$qPkul8N<##5lHK!M%2RENjS4FF`AS5nDh4vmzecNq4RhEN?V&?A0 z+iC@dz3#BLoX#&Mv*n<^crIM8+1<6S<~Q?tEh|0`$M=ut&tUpDtvG7m?Jl>|dfx6_ zJrwzM3Yp$p6^vHopUHa`gRtG24$C{aN0ZleZGfakrctUkznO zIQd8@UIRit9j?)F65rBzpTfg=^l;w$EatGzr}4@#j2x< zv6#Af=9YjmYE`>Yf)1Wt zW-lqJWhtUSXl=y*#8wE&dUD7lbvTzXk)#1z6)(NyGH1ytH};t;7VOk#O71XH>zNT1 zMoeLa$+Bpen6pWrtqj;~aDx=n*vzwr!?;?s#YZt})xixDVJ=wnr~m|93gw!k zO=~^6I1SI#v`+IDS*haSxB`hR#RmyscCFAln8>6GTDO7Fp%%~~m{N8ko6}0h939Mk z_Edlbsl)(axon#vY*Nfn(Y+da?VerN~@U zPC~2^jxeYwhY4DOlb4I#*Wl$0rEj!sFfQNL-;cf_a?_q}eLbDIS%%;FZCj5I_ZzwY zsXf2Eyh8W%aQBM!*G+q?E&lv?d}VzhNN@=l&kexU`T6`Ry}tGNC;MR@?>xOTH2U18J=pS+Jv; j`U)dAkGoQkDWi1B(q->0k|R7keTIV?u^LGQV_^R6KR$p?F#&dt5ZoYviy-O;MT0nI z0h!w)=CJaPiIH849^LUUC&0RMpFc*fO7n|uwQwVwM3WV#BEtK!kvau~Pl z~6*Z6%)y+>1$b*MhDjzJ}YLOtMp%!2t!T;nz*olP-D%qx}%4{_pSae|`HO D%I2gl diff --git a/tests/repository_data/repository/metadata/timestamp.json b/tests/repository_data/repository/metadata/timestamp.json index 73cd3b4c18f1b2bcbb01645f57651a25410bc321..61dbbcae767f9658fee8c01e00322029b6131165 100644 GIT binary patch literal 924 zcmXw2-Krcn48A{4vCMT7Bukd9+~o~QH&-pC$hMT-r00k1wlsvido^+uoi2csQND`uF^N*ucuoEfiKq6E%C!;=qr3bSk z*z8zjR_jUWq%m=IuMyqGkf~T#Ll@MM${LxjG8x!;jKoHzHf0wxT9S2=20@JlaP&6Y(pj<<=#xFXt>|to zk!6-y%itI;>8ooS6HFnYJ=&iH=KW%rjvEQ_Ib9JZ{*oIPi_^FjfrOo5WNic7;N3X5}KTG#5i z7nNuhwd$5R`TlVFGxKtU()YCNVch<@e|_;iA|I}ge!JV_%aNST|9iQ<=61_@!5hF_ z9{}DR>-X1789e&|l(p3E^?gJ`{;!?@%rQv3nxpY7lbZAR{~v(e@p%9M literal 924 zcmXw2ORgL@4BWp{40;_Kq$qymoo^6iyh;!Rltg*P&ilYK31S$&d)WPKKn--0B3Z1e z{`PR%Zr6`be*fo7+_rb8KORn}Z~2_I&++v-GqSDGAPl!`x@m(IHb9_6 zbSl=R6zFw2TN%ACG({e3VqL2=1!kvG$=Wd1s>%)~y4DJcHpqp*lvE?ItQuJ_5-_!L zb+aCl_0~!@Efd>l#4(}Fg*av(9<^ZMmEXkZu@_Z^mX$J>pjy#|m1Qjv;DC{DtRM{VDD9=NM)#0D^Ug4$T0Ey=CrMyqB#t6Nw78j= zEKUW12#uSth4-Y?2ja{PQJiNi?l5|AO5bNI1C`jU;sUWglp+}lYqNJj)Hb7uwlPO~ zq^63Fv?!NZv(_zr^8MlTXZqzZW$bR*&A9z_|N0WUMLt{~<97GQm%}+*{P%Kw_3f4f z!W+Om9{}DR=l9o3X*~bYDEmJ7%kA=ffBWh7{PZ(=+Ae;(9R5GNpA&BvFD(1W)uuxS z?>>plDFs^tU3T!(#y~doo^FD4R7?3rBn RHM#v8UvAd}6h375`~O&Q_Kg4l diff --git a/tests/test_repository_tool.py b/tests/test_repository_tool.py index b4fcc7da..17db845e 100755 --- a/tests/test_repository_tool.py +++ b/tests/test_repository_tool.py @@ -878,7 +878,8 @@ def test_delegations(self): threshold = 1 self.targets_object.delegate(rolename, public_keys, list_of_targets, - threshold, restricted_paths=None, + threshold, backtrack=True, + restricted_paths=None, path_hash_prefixes=None) # Test that a valid Targets() object is returned by delegations(). @@ -1012,8 +1013,9 @@ def test_delegate(self): path_hash_prefixes = ['e3a3', '8fae', 'd543'] self.targets_object.delegate(rolename, public_keys, list_of_targets, - threshold, restricted_paths, - path_hash_prefixes) + threshold, backtrack=True, + restricted_paths=restricted_paths, + path_hash_prefixes=path_hash_prefixes) self.assertEqual(self.targets_object.get_delegated_rolenames(), ['targets/tuf']) diff --git a/tests/test_updater.py b/tests/test_updater.py index 26694ea6..663e2cb0 100755 --- a/tests/test_updater.py +++ b/tests/test_updater.py @@ -843,7 +843,73 @@ def test_6_target(self): # Test: invalid target path. self.assertRaises(tuf.UnknownTargetError, self.repository_updater.target, self.random_path()) + + # Test updater.target() backtracking behavior (enabled by default.) + targets_directory = os.path.join(self.repository_directory, 'targets') + foo_directory = os.path.join(targets_directory, 'foo') + os.makedirs(foo_directory) + foo_package = os.path.join(foo_directory, 'foo1.1.tar.gz') + with open(foo_package, 'wb') as file_object: + file_object.write(b'new release') + + # Modify delegations on the remote repository to test backtracking behavior. + repository = repo_tool.load_repository(self.repository_directory) + + + repository.targets.delegate('role2', [self.role_keys['targets']['public']], + [], restricted_paths=[foo_directory]) + + repository.targets.delegate('role3', [self.role_keys['targets']['public']], + [foo_package], restricted_paths=[foo_directory]) + repository.targets.load_signing_key(self.role_keys['targets']['private']) + repository.targets('role2').load_signing_key(self.role_keys['targets']['private']) + repository.targets('role3').load_signing_key(self.role_keys['targets']['private']) + repository.snapshot.load_signing_key(self.role_keys['snapshot']['private']) + repository.timestamp.load_signing_key(self.role_keys['timestamp']['private']) + repository.write() + + # Move the staged metadata to the "live" metadata. + shutil.rmtree(os.path.join(self.repository_directory, 'metadata')) + shutil.copytree(os.path.join(self.repository_directory, 'metadata.staged'), + os.path.join(self.repository_directory, 'metadata')) + + # updater.target() should find 'foo1.1.tar.gz' by backtracking to + # 'targets/role3'. 'targets/role2' allows backtracking. + self.repository_updater.refresh() + self.repository_updater.target('foo/foo1.1.tar.gz') + + + # Test when 'targets/role2' does *not* allow backtracking. If + # 'foo/foo1.1.tar.gz' is not provided by the authoritative 'target/role2', + # updater.target() should return a 'tuf.UnknownTargetError' exception. + repository = repo_tool.load_repository(self.repository_directory) + + repository.targets.revoke('role2') + repository.targets.revoke('role3') + + # Ensure we delegate in trusted order (i.e., 'role2' has higher priority.) + repository.targets.delegate('role2', [self.role_keys['targets']['public']], + [], backtrack=False, restricted_paths=[foo_directory]) + repository.targets.delegate('role3', [self.role_keys['targets']['public']], + [foo_package], restricted_paths=[foo_directory]) + + repository.targets('role2').load_signing_key(self.role_keys['targets']['private']) + repository.targets('role3').load_signing_key(self.role_keys['targets']['private']) + repository.targets.load_signing_key(self.role_keys['targets']['private']) + repository.snapshot.load_signing_key(self.role_keys['snapshot']['private']) + repository.timestamp.load_signing_key(self.role_keys['timestamp']['private']) + repository.write() + + # Move the staged metadata to the "live" metadata. + shutil.rmtree(os.path.join(self.repository_directory, 'metadata')) + shutil.copytree(os.path.join(self.repository_directory, 'metadata.staged'), + os.path.join(self.repository_directory, 'metadata')) + + # Verify that 'tuf.UnknownTargetError' is raised by updater.target(). + self.repository_updater.refresh() + self.assertRaises(tuf.UnknownTargetError, self.repository_updater.target, + 'foo/foo1.1.tar.gz') diff --git a/tuf/client/updater.py b/tuf/client/updater.py index 6d57ba9b..e3ff4640 100755 --- a/tuf/client/updater.py +++ b/tuf/client/updater.py @@ -2300,15 +2300,28 @@ def _preorder_depth_first_walk(self, target_filepath): if target is None: - # Push children in reverse order of appearance onto the stack. + child_roles_to_visit = [] # NOTE: This may be a slow operation if there are many delegated roles. - for child_role in reversed(child_roles): + for child_role in child_roles: child_role_name = self._visit_child_role(child_role, target_filepath) - if child_role_name is None: + if not child_role['backtrack'] and child_role_name is not None: + logger.debug('Adding child role '+repr(child_role_name)) + logger.debug('Not backtracking to other roles.') + role_names = [] + child_roles_to_visit.append(child_role_name) + break + + elif child_role_name is None: logger.debug('Skipping child role '+repr(child_role_name)) + else: logger.debug('Adding child role '+repr(child_role_name)) - role_names.append(child_role_name) + child_roles_to_visit.append(child_role_name) + + # Push 'child_roles_to_visit' in reverse order of appearance onto + # 'role_names'. Roles are popped from the end of the 'role_names' list. + child_roles_to_visit.reverse() + role_names.extend(child_roles_to_visit) else: logger.debug('Found target in current role '+repr(role_name)) @@ -2350,13 +2363,15 @@ def _get_target_from_targets_role(self, role_name, targets, target_filepath): target = None # Does the current role name have our target? - logger.debug('Asking role '+repr(role_name)+' about target '+\ + logger.debug('Asking role ' + repr(role_name) + ' about target '+\ repr(target_filepath)) + for filepath, fileinfo in six.iteritems(targets): if filepath == target_filepath: - logger.debug('Found target '+target_filepath+' in role '+role_name) + logger.debug('Found target ' + target_filepath + ' in role ' + role_name) target = {'filepath': filepath, 'fileinfo': fileinfo} break + else: logger.debug('No target '+target_filepath+' in role '+role_name) @@ -2434,7 +2449,7 @@ def _visit_child_role(self, child_role, target_filepath): # The 'paths' or 'path_hash_prefixes' fields should not be missing, # so we raise a format error here in case they are both missing. raise tuf.FormatError(repr(child_role_name)+' has neither ' \ - '"paths" nor "path_hash_prefixes"!') + '"paths" nor "path_hash_prefixes".') if child_role_is_relevant: logger.debug('Child role '+repr(child_role_name)+' has target '+ diff --git a/tuf/formats.py b/tuf/formats.py index b7b3df59..f62899c0 100755 --- a/tuf/formats.py +++ b/tuf/formats.py @@ -369,6 +369,7 @@ name = SCHEMA.Optional(ROLENAME_SCHEMA), keyids = KEYIDS_SCHEMA, threshold = THRESHOLD_SCHEMA, + backtrack = SCHEMA.Optional(BOOLEAN_SCHEMA), paths = SCHEMA.Optional(RELPATHS_SCHEMA), path_hash_prefixes = SCHEMA.Optional(PATH_HASH_PREFIXES_SCHEMA)) diff --git a/tuf/repository_tool.py b/tuf/repository_tool.py index 1d3ddf92..c6466cb1 100755 --- a/tuf/repository_tool.py +++ b/tuf/repository_tool.py @@ -1920,9 +1920,8 @@ def get_delegated_rolenames(self): return tuf.roledb.get_delegated_rolenames(self.rolename) - - def delegate(self, rolename, public_keys, list_of_targets, - threshold=1, restricted_paths=None, path_hash_prefixes=None): + def delegate(self, rolename, public_keys, list_of_targets, threshold=1, + backtrack=True, restricted_paths=None, path_hash_prefixes=None): """ Create a new delegation, where 'rolename' is a child delegation of this @@ -1953,6 +1952,17 @@ def delegate(self, rolename, public_keys, list_of_targets, threshold: The threshold number of keys of 'rolename'. + + backtrack: + Boolean that indicates whether this role allows the updater client + to continue searching for targets (target files it is trusted to list + but has not yet specified) in other delegations. If 'backtrack' is + False and 'updater.target()' does not find 'example_target.tar.gz' in + this role, a 'tuf.UnknownTargetError' exception should be raised. If + 'backtrack' is True (default), and 'target/other_role' is also trusted + with 'example_target.tar.gz' and has listed it, updater.target() + should backtrack and return the target file specified by + 'target/other_role'. restricted_paths: A list of restricted directory or file paths of 'rolename'. Any target @@ -1986,17 +1996,21 @@ 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) + tuf.formats.BOOLEAN_SCHEMA.check_match(backtrack) + 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) - + + # Check if 'rolename' is not already a delegation. 'tuf.roledb' expects the # full rolename. - full_rolename = self._rolename+'/'+rolename + full_rolename = self._rolename + '/' + rolename if tuf.roledb.role_exists(full_rolename): - raise tuf.Error(repr(full_rolename)+' already delegated.') + raise tuf.Error(repr(full_rolename) + ' already delegated.') # Keep track of the valid keyids (added to the new Targets object) and their # keydicts (added to this Targets delegations). @@ -2068,9 +2082,12 @@ def delegate(self, rolename, public_keys, list_of_targets, roleinfo = {'name': full_rolename, 'keyids': roleinfo['keyids'], 'threshold': roleinfo['threshold'], + 'backtrack': backtrack, '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 # A role in a delegations must list either 'path_hash_prefixes' From 9e0392bb80c380a9bf0001c26072e463c716d6f4 Mon Sep 17 00:00:00 2001 From: vladdd Date: Sun, 8 Jun 2014 22:16:43 -0400 Subject: [PATCH 23/32] Add missing test coverage for pycrypto_keys.py. --- tests/test_pycrypto_keys.py | 38 +++++++++++++++++++++++++++++++++++++ tuf/pycrypto_keys.py | 4 +++- 2 files changed, 41 insertions(+), 1 deletion(-) diff --git a/tests/test_pycrypto_keys.py b/tests/test_pycrypto_keys.py index f4269667..4ef568ab 100755 --- a/tests/test_pycrypto_keys.py +++ b/tests/test_pycrypto_keys.py @@ -252,6 +252,44 @@ def test_decrypt_key(self): self.assertRaises(tuf.CryptoError, tuf.pycrypto_keys.decrypt_key, b'bad', passphrase) + # Test for invalid encrypted content (i.e., invalid hmac and ciphertext.) + encryption_delimiter = tuf.pycrypto_keys._ENCRYPTION_DELIMITER + salt, iterations, hmac, iv, ciphertext = \ + encrypted_rsa_key.decode('utf-8').split(encryption_delimiter) + + # Set an invalid hmac. The decryption routine sould raise a tuf.CryptoError + # exception because 'hmac' does not match the hmac calculated by the + # decryption routine. + bad_hmac = '12345abcd' + invalid_encrypted_rsa_key = \ + salt + encryption_delimiter + iterations + encryption_delimiter + \ + bad_hmac + encryption_delimiter + iv + encryption_delimiter + ciphertext + + self.assertRaises(tuf.CryptoError, tuf.pycrypto_keys.decrypt_key, + invalid_encrypted_rsa_key.encode('utf-8'), passphrase) + + # Test for invalid 'ciphertext' + bad_ciphertext = '12345abcde' + invalid_encrypted_rsa_key = \ + salt + encryption_delimiter + iterations + encryption_delimiter + \ + hmac + encryption_delimiter + iv + encryption_delimiter + bad_ciphertext + + self.assertRaises(tuf.CryptoError, tuf.pycrypto_keys.decrypt_key, + invalid_encrypted_rsa_key.encode('utf-8'), passphrase) + + + + def test__decrypt_key(self): + # Test for invalid arguments. + salt, iterations, derived_key = tuf.pycrypto_keys._generate_derived_key('pw') + derived_key_information = {'salt': salt, 'derived_key': derived_key, + 'iterations': iterations} + + self.assertRaises(tuf.CryptoError, tuf.pycrypto_keys._encrypt, + 8, derived_key_information) + + + # Run the unit tests. if __name__ == '__main__': diff --git a/tuf/pycrypto_keys.py b/tuf/pycrypto_keys.py index 1a5e2ff9..72c7e8b7 100755 --- a/tuf/pycrypto_keys.py +++ b/tuf/pycrypto_keys.py @@ -948,7 +948,9 @@ def _decrypt(file_contents, password): # what circumstances. PyCrypto example given is to call decrypt() without # checking for exceptions. Avoid propogating the exception trace and only # raise 'tuf.CryptoError', along with the cause of decryption failure. - except (ValueError, IndexError, TypeError) as e: + # Note: decryption failure, due to malicious ciphertext, should not occur here + # if the hmac check above passed. + except (ValueError, IndexError, TypeError) as e: # pragma: no cover raise tuf.CryptoError('Decryption failed: '+str(e)) return key_plaintext From 31bfc5fce5c1a6f6fe5bfb92e04ce545099e1073 Mon Sep 17 00:00:00 2001 From: vladdd Date: Sun, 8 Jun 2014 22:19:37 -0400 Subject: [PATCH 24/32] Remove incorrect dependency from tox.ini. Adding 'unittest' to tox.ini fails for Python > 3.0 installations. --- dev-requirements.txt | 1 - 1 file changed, 1 deletion(-) diff --git a/dev-requirements.txt b/dev-requirements.txt index 0ba31299..2f1d0d60 100644 --- a/dev-requirements.txt +++ b/dev-requirements.txt @@ -18,4 +18,3 @@ pycrypto==2.6.1 pynacl==0.2.3 tox -unittest2 From 15592b2e1594160c1527e2775be65e833ae02a5b Mon Sep 17 00:00:00 2001 From: vladdd Date: Mon, 9 Jun 2014 12:34:58 -0400 Subject: [PATCH 25/32] Add missing docstrings. --- tuf/download.py | 74 +++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 74 insertions(+) diff --git a/tuf/download.py b/tuf/download.py index 5c9c20b4..08e0fc84 100755 --- a/tuf/download.py +++ b/tuf/download.py @@ -52,6 +52,43 @@ def safe_download(url, required_length): + """ + + Given the 'url' and 'required_length' of the desired file, open a connection + to 'url', download it, and return the contents of the file. Also ensure + the length of the downloaded file matches 'required_length' exactly. + tuf.download.unsafe_download() may be called if an upper download limit is + preferred. + + 'tuf.util.TempFile', the file-like object returned, is used instead of + regular tempfile object because of additional functionality provided, such + as handling compressed metadata and automatically closing files after + moving to final destination. + + + url: + A URL string that represents the location of the file. + + required_length: + An integer value representing the length of the file. This is an exact + limit. + + + A 'tuf.util.TempFile' object is created on disk to store the contents of + 'url'. + + + tuf.DownloadLengthMismatchError, if there was a mismatch of observed vs + expected lengths while downloading the file. + + tuf.FormatError, if any of the arguments are improperly formatted. + + Any other unforeseen runtime exception. + + + A 'tuf.util.TempFile' file-like object that points to the contents of 'url'. + """ + return _download_file(url, required_length, STRICT_REQUIRED_LENGTH=True) @@ -59,6 +96,43 @@ def safe_download(url, required_length): def unsafe_download(url, required_length): + """ + + Given the 'url' and 'required_length' of the desired file, open a connection + to 'url', download it, and return the contents of the file. Also ensure + the length of the downloaded file is up to 'required_length', and no larger. + tuf.download.safe_download() may be called if an exact download limit is + preferred. + + 'tuf.util.TempFile', the file-like object returned, is used instead of + regular tempfile object because of additional functionality provided, such + as handling compressed metadata and automatically closing files after + moving to final destination. + + + url: + A URL string that represents the location of the file. + + required_length: + An integer value representing the length of the file. This is an upper + limit. + + + A 'tuf.util.TempFile' object is created on disk to store the contents of + 'url'. + + + tuf.DownloadLengthMismatchError, if there was a mismatch of observed vs + expected lengths while downloading the file. + + tuf.FormatError, if any of the arguments are improperly formatted. + + Any other unforeseen runtime exception. + + + A 'tuf.util.TempFile' file-like object that points to the contents of 'url'. + """ + return _download_file(url, required_length, STRICT_REQUIRED_LENGTH=False) From 70a3510afcc1dec7e22bdaca23943d3a3ac86aae Mon Sep 17 00:00:00 2001 From: Vladimir Diaz Date: Tue, 10 Jun 2014 08:28:29 -0400 Subject: [PATCH 26/32] Minor updates to requirement files. --- dev-requirements.txt | 3 +++ tox.ini | 1 + 2 files changed, 4 insertions(+) diff --git a/dev-requirements.txt b/dev-requirements.txt index 2f1d0d60..4310cb16 100644 --- a/dev-requirements.txt +++ b/dev-requirements.txt @@ -17,4 +17,7 @@ # http://nvie.com/posts/pin-your-packages/ pycrypto==2.6.1 pynacl==0.2.3 + +# Testing requirements. The rest of the testing dependencies available in +# 'tox.ini' tox diff --git a/tox.ini b/tox.ini index 36329a74..0c586e04 100644 --- a/tox.ini +++ b/tox.ini @@ -19,6 +19,7 @@ deps = pynacl pycrypto + [testenv:py26] deps = {[testenv]deps} From cbde52c0f77c18431cf7afc0cd3f001b546b0114 Mon Sep 17 00:00:00 2001 From: Vladimir Diaz Date: Tue, 10 Jun 2014 08:47:10 -0400 Subject: [PATCH 27/32] Allow partial metadata to be properly loaded with zero good signatures. Previously, the repository tool allowed partial metadata to be written with zero good signatues and later successfully loaded. However, the partial metadata was not properly marked by load_repository(). The 'partial_loaded' flag is now set (if partial metadata is written with zero good signatures) and matches the behavior of partial_write(). --- tuf/repository_lib.py | 14 +++++++++----- tuf/repository_tool.py | 2 +- 2 files changed, 10 insertions(+), 6 deletions(-) diff --git a/tuf/repository_lib.py b/tuf/repository_lib.py index d4d8d69b..b54bd929 100755 --- a/tuf/repository_lib.py +++ b/tuf/repository_lib.py @@ -246,10 +246,14 @@ def _get_password(prompt='Password: ', confirm=False): def _metadata_is_partially_loaded(rolename, signable, roleinfo): """ Non-public function that determines whether 'rolename' is loaded with - at least 1 good signature, but an insufficient threshold (which means - 'rolename' was written to disk with repository.write_partial(). If 'rolename' - is found to be partially loaded, mark it as partially loaded in its - 'tuf.roledb' roleinfo. This function exists to assist in deciding whether + at least zero good signatures, but an insufficient threshold (which means + 'rolename' was written to disk with repository.write_partial()). A repository + maintainer may write partial metadata without including a valid signature. + Howerver, the final repository.write() must include a threshold number of + signatures. + + If 'rolename' is found to be partially loaded, mark it as partially loaded in + its 'tuf.roledb' roleinfo. This function exists to assist in deciding whether a role's version number should be incremented when write() or write_parital() is called. Return True if 'rolename' was partially loaded, False otherwise. """ @@ -259,7 +263,7 @@ def _metadata_is_partially_loaded(rolename, signable, roleinfo): status = tuf.sig.get_signature_status(signable, rolename) if len(status['good_sigs']) < status['threshold'] and \ - len(status['good_sigs']) >= 1: + len(status['good_sigs']) >= 0: return True else: diff --git a/tuf/repository_tool.py b/tuf/repository_tool.py index c6466cb1..598e20fb 100755 --- a/tuf/repository_tool.py +++ b/tuf/repository_tool.py @@ -2675,7 +2675,7 @@ def load_repository(repository_directory): roleinfo['paths'] = list(metadata_object['targets'].keys()) roleinfo['delegations'] = metadata_object['delegations'] - if os.path.exists(metadata_path+'.gz'): + if os.path.exists(metadata_path + '.gz'): roleinfo['compressions'].append('gz') # The roleinfo of 'metadata_name' should have been initialized with From 74c0120ed4da90dc00eea4bacb92b0fddee4ce08 Mon Sep 17 00:00:00 2001 From: Vladimir Diaz Date: Tue, 10 Jun 2014 09:26:09 -0400 Subject: [PATCH 28/32] Increase sleep time after starting simple server in integration tests. --- tests/test_arbitrary_package_attack.py | 2 +- tests/test_endless_data_attack.py | 2 +- tests/test_indefinite_freeze_attack.py | 2 +- tests/test_mix_and_match_attack.py | 2 +- tests/test_replay_attack.py | 2 +- 5 files changed, 5 insertions(+), 5 deletions(-) diff --git a/tests/test_arbitrary_package_attack.py b/tests/test_arbitrary_package_attack.py index 0d9a9221..fd47cf63 100755 --- a/tests/test_arbitrary_package_attack.py +++ b/tests/test_arbitrary_package_attack.py @@ -89,7 +89,7 @@ def setUpClass(cls): # NOTE: Following error is raised if a delay is not applied: # - time.sleep(.2) + time.sleep(.8) diff --git a/tests/test_endless_data_attack.py b/tests/test_endless_data_attack.py index 00382f25..b81a23f9 100755 --- a/tests/test_endless_data_attack.py +++ b/tests/test_endless_data_attack.py @@ -92,7 +92,7 @@ def setUpClass(cls): # NOTE: Following error is raised if a delay is not applied: # - time.sleep(.5) + time.sleep(.8) diff --git a/tests/test_indefinite_freeze_attack.py b/tests/test_indefinite_freeze_attack.py index 436198d4..8d24a687 100755 --- a/tests/test_indefinite_freeze_attack.py +++ b/tests/test_indefinite_freeze_attack.py @@ -92,7 +92,7 @@ def setUpClass(cls): # NOTE: Following error is raised if a delay is not applied: # - time.sleep(.2) + time.sleep(.8) diff --git a/tests/test_mix_and_match_attack.py b/tests/test_mix_and_match_attack.py index 69ed0594..e44f7927 100755 --- a/tests/test_mix_and_match_attack.py +++ b/tests/test_mix_and_match_attack.py @@ -96,7 +96,7 @@ def setUpClass(cls): # NOTE: Following error is raised if a delay is not applied: # - time.sleep(.2) + time.sleep(.8) diff --git a/tests/test_replay_attack.py b/tests/test_replay_attack.py index dad8269d..aba4b227 100755 --- a/tests/test_replay_attack.py +++ b/tests/test_replay_attack.py @@ -97,7 +97,7 @@ def setUpClass(cls): # NOTE: Following error is raised if a delay is not applied: # - time.sleep(.2) + time.sleep(.8) From 80915a194e96f9469d3737f99d2f162b21f5e309 Mon Sep 17 00:00:00 2001 From: Vladimir Diaz Date: Tue, 10 Jun 2014 09:38:06 -0400 Subject: [PATCH 29/32] Cosmetic edits to repository_lib.py. --- tuf/repository_lib.py | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/tuf/repository_lib.py b/tuf/repository_lib.py index b54bd929..27c3bb92 100755 --- a/tuf/repository_lib.py +++ b/tuf/repository_lib.py @@ -231,11 +231,14 @@ def _get_password(prompt='Password: ', confirm=False): # 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.') @@ -330,14 +333,14 @@ def _check_role_keys(rolename): # Raise an exception for an invalid threshold of public keys. if total_keyids < threshold: - message = repr(rolename)+' role contains '+repr(total_keyids)+' / '+ \ - repr(threshold)+' public keys.' + message = repr(rolename) + ' role contains ' + \ + repr(total_keyids) + ' / ' + repr(threshold) + ' public keys.' raise tuf.InsufficientKeysError(message) # Raise an exception for an invalid threshold of signing keys. if total_signatures == 0 and total_signing_keys < threshold: - message = repr(rolename)+' role contains '+repr(total_signing_keys)+' / '+ \ - repr(threshold)+' signing keys.' + message = repr(rolename) + ' role contains ' + \ + repr(total_signing_keys) + ' / ' + repr(threshold) + ' signing keys.' raise tuf.InsufficientKeysError(message) @@ -2099,8 +2102,8 @@ def _log_status(rolename, signable): status = tuf.sig.get_signature_status(signable, rolename) - message = repr(rolename)+' role contains '+ repr(len(status['good_sigs']))+\ - ' / '+repr(status['threshold'])+' signatures.' + message = repr(rolename) + ' role contains ' + repr(len(status['good_sigs']))+\ + ' / ' + repr(status['threshold']) + ' signatures.' logger.info(message) @@ -2177,6 +2180,7 @@ def create_tuf_client_directory(repository_directory, client_directory): message = 'Cannot create a fresh client metadata directory: '+ \ repr(client_metadata_directory)+'. Already exists.' raise tuf.RepositoryError(message) + else: raise From 0c6b3609522af3be335d61f02ee404ff306b72b2 Mon Sep 17 00:00:00 2001 From: Vladimir Diaz Date: Wed, 11 Jun 2014 11:29:00 -0400 Subject: [PATCH 30/32] Add missing test conditions for test_repository_lib.py and minor edits to modules. --- tests/test_arbitrary_package_attack.py | 2 +- tests/test_repository_lib.py | 47 ++++++++++++++++++++++++++ tuf/repository_lib.py | 40 +++++++++++----------- 3 files changed, 69 insertions(+), 20 deletions(-) diff --git a/tests/test_arbitrary_package_attack.py b/tests/test_arbitrary_package_attack.py index fd47cf63..83061e97 100755 --- a/tests/test_arbitrary_package_attack.py +++ b/tests/test_arbitrary_package_attack.py @@ -89,7 +89,7 @@ def setUpClass(cls): # NOTE: Following error is raised if a delay is not applied: # - time.sleep(.8) + time.sleep(1) diff --git a/tests/test_repository_lib.py b/tests/test_repository_lib.py index 017e63ae..9885e476 100755 --- a/tests/test_repository_lib.py +++ b/tests/test_repository_lib.py @@ -30,6 +30,7 @@ import datetime import logging import tempfile +import json import shutil import sys @@ -262,6 +263,20 @@ def test_import_ed25519_publickey_from_file(self): self.assertRaises(tuf.Error, repo_lib.import_ed25519_publickey_from_file, invalid_keyfile) + + # Invalid public key imported (contains unexpected keytype.) + keytype = imported_ed25519_key['keytype'] + keyval = imported_ed25519_key['keyval'] + ed25519key_metadata_format = \ + tuf.keys.format_keyval_to_metadata(keytype, keyval, private=False) + + ed25519key_metadata_format['keytype'] = 'invalid_keytype' + with open(ed25519_keypath + '.pub', 'wb') as file_object: + file_object.write(json.dumps(ed25519key_metadata_format).encode('utf-8')) + + self.assertRaises(tuf.FormatError, + repo_lib.import_ed25519_publickey_from_file, + ed25519_keypath + '.pub') @@ -296,6 +311,32 @@ def test_import_ed25519_privatekey_from_file(self): self.assertRaises(tuf.Error, repo_lib.import_ed25519_privatekey_from_file, invalid_keyfile, 'pw') + + # Invalid private key imported (contains unexpected keytype.) + imported_ed25519_key['keytype'] = 'invalid_keytype' + + # Use 'pycrypto_keys.py' to bypass the key format validation performed by + # 'keys.py'. + salt, iterations, derived_key = \ + tuf.pycrypto_keys._generate_derived_key('pw') + + # Store the derived key info in a dictionary, the object expected + # by the non-public _encrypt() routine. + derived_key_information = {'salt': salt, 'iterations': iterations, + 'derived_key': derived_key} + + # Convert the key object to json string format and encrypt it with the + # derived key. + encrypted_key = \ + tuf.pycrypto_keys._encrypt(json.dumps(imported_ed25519_key), + derived_key_information) + + with open(ed25519_keypath, 'wb') as file_object: + file_object.write(encrypted_key.encode('utf-8')) + + self.assertRaises(tuf.FormatError, + repo_lib.import_ed25519_privatekey_from_file, + ed25519_keypath, 'pw') @@ -677,6 +718,12 @@ def test_create_tuf_client_directory(self): repository_directory, client_directory) + def test__check_directory(self): + # Test for non-existent directory. + self.assertRaises(tuf.Error, repo_lib._check_directory, 'non-existent') + + + # Run the test cases. if __name__ == '__main__': unittest.main() diff --git a/tuf/repository_lib.py b/tuf/repository_lib.py index 27c3bb92..b32d251e 100755 --- a/tuf/repository_lib.py +++ b/tuf/repository_lib.py @@ -306,7 +306,7 @@ def _check_directory(directory): # Check if the directory exists. if not os.path.isdir(directory): - raise tuf.Error(repr(directory)+' directory does not exist.') + raise tuf.Error(repr(directory) + ' directory does not exist.') directory = os.path.abspath(directory) @@ -501,7 +501,7 @@ def _strip_consistent_snapshot_digest(metadata_filename, consistent_snapshot): embeded_digest = basename[:basename.find('.')] # Ensure the digest, including the period, is stripped. - basename = basename[basename.find('.')+1:] + basename = basename[basename.find('.') + 1:] metadata_filename = os.path.join(dirname, basename) @@ -761,7 +761,7 @@ def generate_and_write_rsa_keypair(filepath, bits=DEFAULT_RSA_KEY_BITS, tuf.formats.RSAKEYBITS_SCHEMA.check_match(bits) # If the caller does not provide a password argument, prompt for one. - if password is None: + if password is None: # pragma: no cover message = 'Enter a password for the RSA key file: ' password = _get_password(message, confirm=True) @@ -786,7 +786,7 @@ def generate_and_write_rsa_keypair(filepath, bits=DEFAULT_RSA_KEY_BITS, file_object.write(public.encode('utf-8')) # The temporary file is closed after the final move. - file_object.move(filepath+'.pub') + file_object.move(filepath + '.pub') # Write the private key in encrypted PEM format to ''. # Unlike the public key file, the private key does not have a file @@ -841,7 +841,7 @@ def import_rsa_privatekey_from_file(filepath, password=None): # If the caller does not provide a password argument, prompt for one. # Password confirmation disabled here, which should ideally happen only # when creating encrypted key files (i.e., improve usability). - if password is None: + if password is None: # pragma: no cover message = 'Enter a password for the encrypted RSA file: ' password = _get_password(message, confirm=False) @@ -962,7 +962,7 @@ def generate_and_write_ed25519_keypair(filepath, password=None): tuf.formats.PATH_SCHEMA.check_match(filepath) # If the caller does not provide a password argument, prompt for one. - if password is None: + if password is None: # pragma: no cover message = 'Enter a password for the ED25519 key: ' password = _get_password(message, confirm=True) @@ -993,7 +993,7 @@ def generate_and_write_ed25519_keypair(filepath, password=None): file_object.write(json.dumps(ed25519key_metadata_format).encode('utf-8')) # The temporary file is closed after the final move. - file_object.move(filepath+'.pub') + file_object.move(filepath + '.pub') # Write the encrypted key string, conformant to # 'tuf.formats.ENCRYPTEDKEY_SCHEMA', to ''. @@ -1041,9 +1041,11 @@ def import_ed25519_publickey_from_file(filepath): ed25519_key_metadata = tuf.util.load_json_file(filepath) ed25519_key = tuf.keys.format_metadata_to_key(ed25519_key_metadata) - # Raise an exception if an unexpected key type is imported. - if ed25519_key['keytype'] != 'ed25519': - message = 'Invalid key type loaded: '+repr(ed25519_key['keytype']) + # Raise an exception if an unexpected key type is imported. + # Redundant validation of 'keytype'. 'tuf.keys.format_metadata_to_key()' + # should have fully validated 'ed25519_key_metadata'. + if ed25519_key['keytype'] != 'ed25519': # pragma: no cover + message = 'Invalid key type loaded: ' + repr(ed25519_key['keytype']) raise tuf.FormatError(message) return ed25519_key @@ -1100,7 +1102,7 @@ def import_ed25519_privatekey_from_file(filepath, password=None): # If the caller does not provide a password argument, prompt for one. # Password confirmation disabled here, which should ideally happen only # when creating encrypted key files (i.e., improve usability). - if password is None: + if password is None: # pragma: no cover message = 'Enter a password for the encrypted ED25519 key: ' password = _get_password(message, confirm=False) @@ -1122,7 +1124,7 @@ def import_ed25519_privatekey_from_file(filepath, password=None): # Raise an exception if an unexpected key type is imported. if key_object['keytype'] != 'ed25519': - message = 'Invalid key type loaded: '+repr(key_object['keytype']) + message = 'Invalid key type loaded: ' + repr(key_object['keytype']) raise tuf.FormatError(message) return key_object @@ -1230,7 +1232,7 @@ def get_metadata_fileinfo(filename): tuf.formats.PATH_SCHEMA.check_match(filename) if not os.path.isfile(filename): - message = repr(filename)+' is not a file.' + message = repr(filename) + ' is not a file.' raise tuf.Error(message) # Note: 'filehashes' is a dictionary of the form @@ -1337,7 +1339,7 @@ def generate_root_metadata(version, expiration_date, consistent_snapshot): # 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".') + raise tuf.Error(repr(rolename) + ' not in "tuf.roledb".') # Keep track of the keys loaded to avoid duplicates. keyids = [] @@ -1476,7 +1478,7 @@ def generate_targets_metadata(targets_directory, target_files, version, # Ensure all target files listed in 'target_files' exist. If just one of # these files does not exist, raise an exception. if not os.path.exists(target_path): - message = repr(target_path)+' cannot be read. Unable to generate '+ \ + message = repr(target_path) + ' cannot be read. Unable to generate '+ \ 'targets metadata.' raise tuf.Error(message) @@ -1692,10 +1694,10 @@ def generate_timestamp_metadata(snapshot_filename, version, compressed_fileinfo = get_metadata_fileinfo(compressed_filename) except: - logger.warning('Cannot get fileinfo about '+repr(compressed_filename)) + logger.warning('Cannot get fileinfo about ' + repr(compressed_filename)) else: - logger.info('Including fileinfo about '+repr(compressed_filename)) + logger.info('Including fileinfo about ' + repr(compressed_filename)) fileinfo[SNAPSHOT_FILENAME + '.' + file_extension] = compressed_fileinfo # Generate the timestamp metadata object. @@ -1761,7 +1763,7 @@ def sign_metadata(metadata_object, keyids, filename): # Load the signing key. key = tuf.keydb.get_key(keyid) - logger.info('Signing '+repr(filename)+' with '+key['keyid']) + logger.info('Signing ' + repr(filename) + ' with ' + key['keyid']) # Create a new signature list. If 'keyid' is encountered, do not add it # to the new list. @@ -1779,7 +1781,7 @@ def sign_metadata(metadata_object, keyids, filename): signable['signatures'].append(signature) else: - logger.warning('Private key unset. Skipping: '+repr(keyid)) + logger.warning('Private key unset. Skipping: ' + repr(keyid)) else: raise tuf.Error('The keydb contains a key with an invalid key type.') From e4c98d38ba2424c70163bb234234ca8fbcded46a Mon Sep 17 00:00:00 2001 From: Vladimir Diaz Date: Thu, 12 Jun 2014 09:33:12 -0400 Subject: [PATCH 31/32] Add missing test cases for download.py and and updater.py. --- tests/test_download.py | 19 +++++++++++++++++-- tests/test_updater.py | 19 +++++++++++++++++++ tuf/client/updater.py | 32 ++++++++++++-------------------- tuf/download.py | 5 +++-- 4 files changed, 51 insertions(+), 24 deletions(-) diff --git a/tests/test_download.py b/tests/test_download.py index f3fbaa1f..f8aca54e 100755 --- a/tests/test_download.py +++ b/tests/test_download.py @@ -170,13 +170,28 @@ def test_download_url_to_tempfileobj_and_urls(self): self.assertRaises(six.moves.urllib.error.HTTPError, download_file, - 'http://localhost:'+str(self.PORT)+'/'+self.random_string(), + 'http://localhost:' + str(self.PORT) + '/' + self.random_string(), self.target_data_length) self.assertRaises(six.moves.urllib.error.URLError, download_file, - 'http://localhost:'+str(self.PORT+1)+'/'+self.random_string(), + 'http://localhost:' + str(self.PORT+1) + '/' + self.random_string(), self.target_data_length) + + + + def test__get_opener(self): + # Test normal case. + # A simple https server should be used to test the rest of the optional + # ssl-related functions of 'tuf.download.py'. + fake_cacert = self.make_temp_data_file() + + with open(fake_cacert, 'wt') as file_object: + file_object.write('fake cacert') + + tuf.conf.ssl_certificates = fake_cacert + tuf.download._get_opener('https') + # Run unit test. diff --git a/tests/test_updater.py b/tests/test_updater.py index 663e2cb0..515d74b5 100755 --- a/tests/test_updater.py +++ b/tests/test_updater.py @@ -1103,6 +1103,25 @@ def test_8_remove_obsolete_targets(self): # in 'destination_directory' remains the same. self.repository_updater.remove_obsolete_targets(destination_directory) self.assertTrue(os.listdir(destination_directory), 1) + + + + + + def test_9__get_target_hash(self): + # Test normal case. + # Test target filepaths with ascii and non-ascii characters. + expected_target_hashes = { + '/file1.txt': 'e3a3d89eb3b70ce3fbce6017d7b8c12d4abd5635427a0e8a238f53157df85b3d', + '/Jalape\xc3\xb1o': '78bfd5c314680545eb48ecad508aceb861f8d6e680f4fe1b791da45c298cda88' + } + for filepath, target_hash in six.iteritems(expected_target_hashes): + self.assertTrue(tuf.formats.RELPATH_SCHEMA.matches(filepath)) + self.assertTrue(tuf.formats.HASH_SCHEMA.matches(target_hash)) + self.assertEqual(self.repository_updater._get_target_hash(filepath), target_hash) + + # Test for improperly formatted argument. + self.assertRaises(tuf.FormatError, tuf.util.get_target_hash, 8) diff --git a/tuf/client/updater.py b/tuf/client/updater.py index e3ff4640..5f7d4802 100755 --- a/tuf/client/updater.py +++ b/tuf/client/updater.py @@ -2448,16 +2448,17 @@ def _visit_child_role(self, child_role, target_filepath): # 'role_name' should have been validated when it was downloaded. # The 'paths' or 'path_hash_prefixes' fields should not be missing, # so we raise a format error here in case they are both missing. - raise tuf.FormatError(repr(child_role_name)+' has neither ' \ + raise tuf.FormatError(repr(child_role_name) + ' has neither ' \ '"paths" nor "path_hash_prefixes".') if child_role_is_relevant: - logger.debug('Child role '+repr(child_role_name)+' has target '+ + logger.debug('Child role ' + repr(child_role_name) + ' has target ' + \ repr(target_filepath)) return child_role_name + else: - logger.debug('Child role '+repr(child_role_name)+ - ' does not have target '+repr(target_filepath)) + logger.debug('Child role ' + repr(child_role_name) + \ + ' does not have target ' + repr(target_filepath)) return None @@ -2495,20 +2496,11 @@ def _get_target_hash(self, target_filepath, hash_function='sha256'): """ # Calculate the hash of the filepath to determine which bin to find the - # target. The client currently assumes the repository uses - # 'hash_function' to generate hashes. - + # target. The client currently assumes the repository (i.e., repository + # tool) uses 'hash_function' to generate hashes and UTF-8. digest_object = tuf.hash.digest(hash_function) - - try: - digest_object.update(target_filepath) - except UnicodeEncodeError: - # Sometimes, there are Unicode characters in target paths. We assume a - # UTF-8 encoding and try to hash that. - digest_object = tuf.hash.digest(hash_function) - encoded_target_filepath = target_filepath.encode('utf-8') - digest_object.update(encoded_target_filepath) - + encoded_target_filepath = target_filepath.encode('utf-8') + digest_object.update(encoded_target_filepath) target_filepath_hash = digest_object.hexdigest() return target_filepath_hash @@ -2554,7 +2546,7 @@ def remove_obsolete_targets(self, destination_directory): for target in self.metadata['previous'][role]['targets']: if target not in self.metadata['current'][role]['targets']: # 'target' is only in 'previous', so remove it. - logger.warning('Removing obsolete file: '+repr(target)+'.') + logger.warning('Removing obsolete file: ' + repr(target) + '.') # Remove the file if it hasn't been removed already. destination = os.path.join(destination_directory, target) try: @@ -2563,7 +2555,7 @@ def remove_obsolete_targets(self, destination_directory): except OSError as e: # If 'filename' already removed, just log it. if e.errno == errno.ENOENT: - logger.info('File '+repr(destination)+' was already removed.') + logger.info('File ' + repr(destination) + ' was already removed.') else: logger.error(str(e)) @@ -2722,6 +2714,6 @@ def download_target(self, target, destination_directory): raise else: - logger.warning(str(target_dirpath)+' does not exist.') + logger.warning(repr(target_dirpath) + ' does not exist.') target_file_object.move(destination) diff --git a/tuf/download.py b/tuf/download.py index 08e0fc84..0eab6606 100755 --- a/tuf/download.py +++ b/tuf/download.py @@ -461,8 +461,9 @@ def _get_content_length(connection): assert reported_length > -1 except: - logger.exception('Could not get content length about '+str(connection)+ - ' from server!') + message = \ + 'Could not get content length about ' + str(connection) + ' from server.' + logger.exception(message) reported_length = None finally: From ac38e5518e9b88d6c25fc1e9d08db0ec38e92f42 Mon Sep 17 00:00:00 2001 From: dachshund Date: Fri, 13 Jun 2014 11:44:46 +0800 Subject: [PATCH 32/32] Acknowledge the NSF. --- README.md | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/README.md b/README.md index d4bb96fa..0a100065 100644 --- a/README.md +++ b/README.md @@ -118,3 +118,11 @@ TUF has four major classes of users: clients, for whom TUF is largely transparen * [Low-level Integration](tuf/client/README.md) * [High-level Integration](tuf/interposition/README.md) + +## Acknowledgements + +This material is based upon work supported by the National Science Foundation +under Grant No. CNS-1345049 and CNS-0959138. Any opinions, findings, and +conclusions or recommendations expressed in this material are those of the +author(s) and do not necessarily reflect the views of the National Science +Foundation.