Skip to content
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.

Commit 588f417

Browse files
committedJan 29, 2021
Add auditor implementation
The auditor verifies all nodes in the snapshot merkle tree to check for rollback attacks. This PoC implementation re-uses a lot of functionality from the updater. It may be better to move some of the re-used functionality to a separate place (ie separating file downloading from update logic). The auditor implementation currently requires there to be snapshot metadata on the repository in order to iterate over all of the nodes. In the future, if the verification succeeds, the auditor should add a signature to timestamp metadata. Signed-off-by: Marina Moore <[email protected]>
1 parent d869cc4 commit 588f417

File tree

4 files changed

+503
-9
lines changed

4 files changed

+503
-9
lines changed
 

‎tests/test_auditor.py

+317
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,317 @@
1+
#!/usr/bin/env python
2+
3+
"""
4+
<Program Name>
5+
test_auditor.py
6+
7+
<Author>
8+
Marina Moore
9+
10+
<Started>
11+
January 29, 2021
12+
13+
<Copyright>
14+
See LICENSE-MIT OR LICENSE for licensing information.
15+
16+
<Purpose>
17+
'test-auditor.py' provides a collection of methods that test the public /
18+
non-public methods and functions of 'tuf.client.auditor.py'.
19+
20+
"""
21+
22+
import unittest
23+
import tempfile
24+
import os
25+
import logging
26+
import shutil
27+
28+
import tuf
29+
import tuf.exceptions
30+
import tuf.log
31+
import tuf.keydb
32+
import tuf.roledb
33+
import tuf.repository_tool as repo_tool
34+
import tuf.repository_lib as repo_lib
35+
import tuf.unittest_toolbox as unittest_toolbox
36+
import tuf.client.auditor as auditor
37+
38+
from tests import utils
39+
40+
import securesystemslib
41+
42+
logger = logging.getLogger(__name__)
43+
repo_tool.disable_console_log_messages()
44+
45+
46+
class TestAuditor(unittest_toolbox.Modified_TestCase):
47+
48+
@classmethod
49+
def setUpClass(cls):
50+
# setUpClass is called before tests in an individual class are executed.
51+
52+
# Create a temporary directory to store the repository, metadata, and target
53+
# files. 'temporary_directory' must be deleted in TearDownModule() so that
54+
# temporary files are always removed, even when exceptions occur.
55+
cls.temporary_directory = tempfile.mkdtemp(dir=os.getcwd())
56+
57+
# Needed because in some tests simple_server.py cannot be found.
58+
# The reason is that the current working directory
59+
# has been changed when executing a subprocess.
60+
cls.SIMPLE_SERVER_PATH = os.path.join(os.getcwd(), 'simple_server.py')
61+
62+
# Launch a SimpleHTTPServer (serves files in the current directory).
63+
# Test cases will request metadata and target files that have been
64+
# pre-generated in 'tuf/tests/repository_data', which will be served
65+
# by the SimpleHTTPServer launched here. The test cases of 'test_updater.py'
66+
# assume the pre-generated metadata files have a specific structure, such
67+
# as a delegated role 'targets/role1', three target files, five key files,
68+
# etc.
69+
cls.server_process_handler = utils.TestServerProcess(log=logger,
70+
server=cls.SIMPLE_SERVER_PATH)
71+
72+
73+
@classmethod
74+
def tearDownClass(cls):
75+
# Cleans the resources and flush the logged lines (if any).
76+
cls.server_process_handler.clean()
77+
78+
# Remove the temporary repository directory, which should contain all the
79+
# metadata, targets, and key files generated for the test cases
80+
shutil.rmtree(cls.temporary_directory)
81+
82+
83+
def setUp(self):
84+
# We are inheriting from custom class.
85+
unittest_toolbox.Modified_TestCase.setUp(self)
86+
87+
tuf.roledb.clear_roledb(clear_all=True)
88+
tuf.keydb.clear_keydb(clear_all=True)
89+
90+
self.repository_name = 'test_repository1'
91+
92+
# Copy the original repository files provided in the test folder so that
93+
# any modifications made to repository files are restricted to the copies.
94+
# The 'repository_data' directory is expected to exist in 'tuf.tests/'.
95+
original_repository_files = os.path.join(os.getcwd(), 'repository_data')
96+
temporary_repository_root = \
97+
self.make_temp_directory(directory=self.temporary_directory)
98+
99+
# The original repository, keystore, and client directories will be copied
100+
# for each test case.
101+
original_repository = os.path.join(original_repository_files, 'repository')
102+
original_keystore = os.path.join(original_repository_files, 'keystore')
103+
original_client = os.path.join(original_repository_files, 'client')
104+
105+
# Save references to the often-needed client repository directories.
106+
# Test cases need these references to access metadata and target files.
107+
self.repository_directory = \
108+
os.path.join(temporary_repository_root, 'repository')
109+
self.keystore_directory = \
110+
os.path.join(temporary_repository_root, 'keystore')
111+
112+
self.client_directory = os.path.join(temporary_repository_root,
113+
'client')
114+
self.client_metadata = os.path.join(self.client_directory,
115+
self.repository_name, 'metadata')
116+
self.client_metadata_current = os.path.join(self.client_metadata,
117+
'current')
118+
self.client_metadata_previous = os.path.join(self.client_metadata,
119+
'previous')
120+
121+
# Copy the original 'repository', 'client', and 'keystore' directories
122+
# to the temporary repository the test cases can use.
123+
shutil.copytree(original_repository, self.repository_directory)
124+
shutil.copytree(original_client, self.client_directory)
125+
shutil.copytree(original_keystore, self.keystore_directory)
126+
127+
# 'path/to/tmp/repository' -> 'localhost:8001/tmp/repository'.
128+
repository_basepath = self.repository_directory[len(os.getcwd()):]
129+
url_prefix = 'http://localhost:' \
130+
+ str(self.server_process_handler.port) + repository_basepath
131+
132+
# Setting 'tuf.settings.repository_directory' with the temporary client
133+
# directory copied from the original repository files.
134+
tuf.settings.repositories_directory = self.client_directory
135+
136+
# replace timestamp with a merkle timestamp
137+
merkle_timestamp = os.path.join(self.repository_directory, 'metadata', 'timestamp-merkle.json')
138+
timestamp = os.path.join(self.repository_directory, 'metadata', 'timestamp.json')
139+
shutil.move(merkle_timestamp, timestamp)
140+
141+
# Metadata role keys are needed by the test cases to make changes to the
142+
# repository (e.g., adding a new target file to 'targets.json' and then
143+
# requesting a refresh()).
144+
self.role_keys = _load_role_keys(self.keystore_directory)
145+
146+
# The repository must be rewritten with 'consistent_snapshot' set.
147+
repository = repo_tool.load_repository(self.repository_directory)
148+
149+
# Write metadata for all the top-level roles , since consistent snapshot
150+
# is now being set to true (i.e., the pre-generated repository isn't set
151+
# to support consistent snapshots. A new version of targets.json is needed
152+
# to ensure <digest>.filename target files are written to disk.
153+
repository.targets.load_signing_key(self.role_keys['targets']['private'])
154+
repository.root.load_signing_key(self.role_keys['root']['private'])
155+
repository.snapshot.load_signing_key(self.role_keys['snapshot']['private'])
156+
repository.timestamp.load_signing_key(self.role_keys['timestamp']['private'])
157+
158+
repository.mark_dirty(['targets', 'root', 'snapshot', 'timestamp'])
159+
repository.writeall(snapshot_merkle=True, consistent_snapshot=True)
160+
161+
# Move the staged metadata to the "live" metadata.
162+
shutil.rmtree(os.path.join(self.repository_directory, 'metadata'))
163+
shutil.copytree(os.path.join(self.repository_directory, 'metadata.staged'),
164+
os.path.join(self.repository_directory, 'metadata'))
165+
166+
self.repository_mirrors = {'mirror1': {'url_prefix': url_prefix,
167+
'metadata_path': 'metadata',
168+
'targets_path': 'targets'}}
169+
170+
171+
172+
173+
def tearDown(self):
174+
# We are inheriting from custom class.
175+
unittest_toolbox.Modified_TestCase.tearDown(self)
176+
tuf.roledb.clear_roledb(clear_all=True)
177+
tuf.keydb.clear_keydb(clear_all=True)
178+
179+
# Logs stdout and stderr from the sever subprocess.
180+
self.server_process_handler.flush_log()
181+
182+
183+
# UNIT TESTS.
184+
185+
def test_1__init_exceptions(self):
186+
# Invalid arguments
187+
self.assertRaises(securesystemslib.exceptions.FormatError, auditor.Auditor,
188+
5, self.repository_mirrors)
189+
self.assertRaises(securesystemslib.exceptions.FormatError, auditor.Auditor,
190+
self.repository_name, 5)
191+
192+
193+
194+
def test_2__verify_merkle_tree(self):
195+
repository_auditor = auditor.Auditor(self.repository_name, self.repository_mirrors)
196+
# skip version 1 as it was written without consistent snapshots
197+
repository_auditor.last_version_verified = 1
198+
199+
# The repository must be rewritten with 'consistent_snapshot' set.
200+
repository = repo_tool.load_repository(self.repository_directory)
201+
202+
# Write metadata for all the top-level roles , since consistent snapshot
203+
# is now being set to true (i.e., the pre-generated repository isn't set
204+
# to support consistent snapshots. A new version of targets.json is needed
205+
# to ensure <digest>.filename target files are written to disk.
206+
repository.targets.load_signing_key(self.role_keys['targets']['private'])
207+
repository.root.load_signing_key(self.role_keys['root']['private'])
208+
repository.snapshot.load_signing_key(self.role_keys['snapshot']['private'])
209+
repository.timestamp.load_signing_key(self.role_keys['timestamp']['private'])
210+
211+
repository.targets.add_target('file1.txt')
212+
213+
repository.mark_dirty(['targets', 'root', 'snapshot', 'timestamp'])
214+
repository.writeall(snapshot_merkle=True, consistent_snapshot=True)
215+
216+
# Move the staged metadata to the "live" metadata.
217+
shutil.rmtree(os.path.join(self.repository_directory, 'metadata'))
218+
shutil.copytree(os.path.join(self.repository_directory, 'metadata.staged'),
219+
os.path.join(self.repository_directory, 'metadata'))
220+
221+
222+
# Normal case, should not error
223+
repository_auditor.verify()
224+
225+
self.assertEqual(repository_auditor.version_info['role1.json'], 1)
226+
self.assertEqual(repository_auditor.version_info['targets.json'], 3)
227+
self.assertEqual(repository_auditor.last_version_verified, 3)
228+
229+
# modify targets
230+
repository.targets.add_target('file2.txt')
231+
232+
repository.targets.load_signing_key(self.role_keys['targets']['private'])
233+
repository.root.load_signing_key(self.role_keys['root']['private'])
234+
repository.snapshot.load_signing_key(self.role_keys['snapshot']['private'])
235+
repository.timestamp.load_signing_key(self.role_keys['timestamp']['private'])
236+
237+
238+
repository.mark_dirty(['targets', 'root', 'snapshot', 'timestamp'])
239+
repository.writeall(snapshot_merkle=True, consistent_snapshot=True)
240+
241+
# Move the staged metadata to the "live" metadata.
242+
shutil.rmtree(os.path.join(self.repository_directory, 'metadata'))
243+
shutil.copytree(os.path.join(self.repository_directory, 'metadata.staged'),
244+
os.path.join(self.repository_directory, 'metadata'))
245+
246+
repository_auditor.verify()
247+
248+
# Ensure the auditor checked the latest targets
249+
self.assertEqual(repository_auditor.version_info['targets.json'], 4)
250+
251+
# Test rollback attack detection
252+
repository_auditor.version_info['targets.json'] = 5
253+
repository_auditor.last_version_verified = 3
254+
255+
self.assertRaises(tuf.exceptions.RepositoryError, repository_auditor.verify)
256+
257+
258+
259+
260+
def _load_role_keys(keystore_directory):
261+
262+
# Populating 'self.role_keys' by importing the required public and private
263+
# keys of 'tuf/tests/repository_data/'. The role keys are needed when
264+
# modifying the remote repository used by the test cases in this unit test.
265+
266+
# The pre-generated key files in 'repository_data/keystore' are all encrypted with
267+
# a 'password' passphrase.
268+
EXPECTED_KEYFILE_PASSWORD = 'password'
269+
270+
# Store and return the cryptography keys of the top-level roles, including 1
271+
# delegated role.
272+
role_keys = {}
273+
274+
root_key_file = os.path.join(keystore_directory, 'root_key')
275+
targets_key_file = os.path.join(keystore_directory, 'targets_key')
276+
snapshot_key_file = os.path.join(keystore_directory, 'snapshot_key')
277+
timestamp_key_file = os.path.join(keystore_directory, 'timestamp_key')
278+
delegation_key_file = os.path.join(keystore_directory, 'delegation_key')
279+
280+
role_keys = {'root': {}, 'targets': {}, 'snapshot': {}, 'timestamp': {},
281+
'role1': {}}
282+
283+
# Import the top-level and delegated role public keys.
284+
role_keys['root']['public'] = \
285+
repo_tool.import_rsa_publickey_from_file(root_key_file+'.pub')
286+
role_keys['targets']['public'] = \
287+
repo_tool.import_ed25519_publickey_from_file(targets_key_file+'.pub')
288+
role_keys['snapshot']['public'] = \
289+
repo_tool.import_ed25519_publickey_from_file(snapshot_key_file+'.pub')
290+
role_keys['timestamp']['public'] = \
291+
repo_tool.import_ed25519_publickey_from_file(timestamp_key_file+'.pub')
292+
role_keys['role1']['public'] = \
293+
repo_tool.import_ed25519_publickey_from_file(delegation_key_file+'.pub')
294+
295+
# Import the private keys of the top-level and delegated roles.
296+
role_keys['root']['private'] = \
297+
repo_tool.import_rsa_privatekey_from_file(root_key_file,
298+
EXPECTED_KEYFILE_PASSWORD)
299+
role_keys['targets']['private'] = \
300+
repo_tool.import_ed25519_privatekey_from_file(targets_key_file,
301+
EXPECTED_KEYFILE_PASSWORD)
302+
role_keys['snapshot']['private'] = \
303+
repo_tool.import_ed25519_privatekey_from_file(snapshot_key_file,
304+
EXPECTED_KEYFILE_PASSWORD)
305+
role_keys['timestamp']['private'] = \
306+
repo_tool.import_ed25519_privatekey_from_file(timestamp_key_file,
307+
EXPECTED_KEYFILE_PASSWORD)
308+
role_keys['role1']['private'] = \
309+
repo_tool.import_ed25519_privatekey_from_file(delegation_key_file,
310+
EXPECTED_KEYFILE_PASSWORD)
311+
312+
return role_keys
313+
314+
315+
316+
if __name__ == '__main__':
317+
unittest.main()

‎tests/test_updater.py

+3-3
Original file line numberDiff line numberDiff line change
@@ -1791,15 +1791,15 @@ def test_snapshot_merkle(self):
17911791
repository_updater.refresh()
17921792

17931793
# Test verify merkle path
1794-
snapshot_info = repository_updater._verify_merkle_path('targets')
1794+
snapshot_info = repository_updater.verify_merkle_path('targets')
17951795
self.assertEqual(snapshot_info['version'], 1)
17961796

1797-
snapshot_info = repository_updater._verify_merkle_path('role1')
1797+
snapshot_info = repository_updater.verify_merkle_path('role1')
17981798
self.assertEqual(snapshot_info['version'], 1)
17991799

18001800
# verify merkle path with invalid role
18011801
self.assertRaises(tuf.exceptions.NoWorkingMirrorError,
1802-
repository_updater._verify_merkle_path, 'foo')
1802+
repository_updater.verify_merkle_path, 'foo')
18031803

18041804
# Test get_one_valid_targetinfo with snapshot merkle
18051805
repository_updater.get_one_valid_targetinfo('file1.txt')

‎tuf/client/auditor.py

+137
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,137 @@
1+
#!/usr/bin/env python
2+
3+
# Copyright 2012 - 2017, New York University and the TUF contributors
4+
# SPDX-License-Identifier: MIT OR Apache-2.0
5+
6+
"""
7+
<Program Name>
8+
auditor.py
9+
10+
<Author>
11+
Marina Moore <mnm678@gmail.com>
12+
<Started>
13+
January 28, 2021
14+
<Copyright>
15+
See LICENSE-MIT OR LICENSE for licensing information.
16+
<Purpose>
17+
'auditor.py' provides an implementation of an auditor for
18+
snapshot merkle metadata.
19+
20+
"""
21+
22+
import tuf
23+
import tuf.download
24+
import tuf.formats
25+
import tuf.client.updater
26+
27+
import securesystemslib.hash
28+
29+
30+
31+
class Auditor(object):
32+
"""
33+
<Purpose>
34+
Provide a class that downloads and verifies snapshot merkle metadata
35+
from a repository.
36+
37+
<Arguments>
38+
repository_name:
39+
Name of the repository to be audited
40+
41+
repository_mirrors:
42+
Dictionary holding repository mirror information, conformant to
43+
`tuf.formats.MIRRORDICT_SCHEMA`.
44+
45+
<Exceptions>
46+
securesystemslib.exceptions.FormatError:
47+
If the arguments are improperly formatted.
48+
49+
<Side Effects>
50+
None.
51+
52+
<Returns>
53+
None.
54+
"""
55+
56+
def __init__(self, repository_name, repository_mirrors):
57+
securesystemslib.formats.NAME_SCHEMA.check_match(repository_name)
58+
tuf.formats.MIRRORDICT_SCHEMA.check_match(repository_mirrors)
59+
60+
self.repository_name = repository_name
61+
self.mirrors = repository_mirrors
62+
63+
# Create a dictionary to store current version information
64+
# for all targets metadata
65+
self.version_info = {}
66+
67+
# Keep track of the last timestamp version number checked
68+
self.last_version_verified = 0
69+
70+
# Updater will be used to update top-level metadata
71+
self.updater = tuf.client.updater.Updater(repository_name, repository_mirrors)
72+
73+
74+
def verify(self):
75+
# download most recent top-level metadata, determine current timestamp key
76+
self.updater.refresh()
77+
78+
cur_timestamp_keys = self.updater.metadata['current']['root']['roles']['timestamp']['keyids']
79+
80+
# Download all trees since last_version_verified that use cur_timestamp_key
81+
82+
next_version = self.last_version_verified + 1
83+
version_exists = True
84+
85+
while(version_exists):
86+
verification_fn = self.updater.signable_verification
87+
88+
# Attempt to download this version of timestamp. If it does not exist,
89+
# break out of the loop
90+
timestamp = self.updater.download_metadata_version_if_exists("timestamp",
91+
next_version, verification_fn,
92+
tuf.settings.DEFAULT_TIMESTAMP_REQUIRED_LENGTH)
93+
94+
if not timestamp:
95+
version_exists = False
96+
break
97+
98+
99+
# Compare with the current timestamp keys. We only verify any trees
100+
# that use the current keys for fast forward attack recovery
101+
# TODO support more than one timestamp key
102+
if timestamp['signatures'][0]['keyid'] != cur_timestamp_keys[0]:
103+
# TODO: Should the auditor also verify older trees?
104+
break
105+
106+
merkle_root = timestamp['signed']['merkle_root']
107+
108+
# Download and verify Merkle trees
109+
110+
# First, download snapshot to get a list of nodes
111+
snapshot = self.updater.download_metadata_version_if_exists("snapshot",
112+
next_version, verification_fn,
113+
tuf.settings.DEFAULT_SNAPSHOT_REQUIRED_LENGTH)
114+
115+
for metadata_filename in snapshot['signed']['meta']:
116+
# Download the node and verify its path
117+
versioninfo = self.updater.verify_merkle_path(
118+
metadata_filename[:-len('.json')], next_version, merkle_root)
119+
120+
# Have we seen this metadata file before?
121+
# If yes, compare the version info
122+
if metadata_filename in self.version_info:
123+
if self.version_info[metadata_filename] > versioninfo['version']:
124+
raise tuf.exceptions.RepositoryError('Rollback attack detected' +
125+
'for ' + metadata_filename + '. Version ' +
126+
str(versioninfo['version']) + ' is less than ' +
127+
str(self.version_info[metadata_filename]))
128+
129+
# Update `version_info` with the latest seen version
130+
self.version_info[metadata_filename] = versioninfo['version']
131+
132+
133+
self.last_version_verified = next_version
134+
next_version = next_version + 1
135+
136+
137+

‎tuf/client/updater.py

+46-6
Original file line numberDiff line numberDiff line change
@@ -1492,7 +1492,7 @@ def _get_metadata_file(self, metadata_role, remote_filename,
14921492
snapshot_merkle:
14931493
Is the metadata file a snapshot merkle file? Snapshot merkle files
14941494
are not signed and so should skip some of the verification steps here.
1495-
Instead, they must be verified using _verify_merkle_path.
1495+
Instead, they must be verified using verify_merkle_path.
14961496
14971497
<Exceptions>
14981498
tuf.exceptions.NoWorkingMirrorError:
@@ -1654,7 +1654,7 @@ def _update_merkle_metadata(self, merkle_filename, upperbound_filelength,
16541654
16551655
version:
16561656
The expected and required version number of the 'merkle_filename' file
1657-
downloaded. 'expected_version' is an integer.
1657+
downloaded. 'version' is an integer.
16581658
16591659
<Exceptions>
16601660
tuf.exceptions.NoWorkingMirrorError:
@@ -1865,7 +1865,7 @@ def _update_metadata(self, metadata_role, upperbound_filelength, version=None):
18651865

18661866

18671867

1868-
def _verify_merkle_path(self, metadata_role):
1868+
def verify_merkle_path(self, metadata_role, version=None, merkle_root=None):
18691869
"""
18701870
<Purpose>
18711871
Download the merkle path associated with metadata_role and verify the hashes.
@@ -1879,13 +1879,14 @@ def _verify_merkle_path(self, metadata_role):
18791879
A dictionary containing the snapshot information about metadata role,
18801880
conforming to VERSIONINFO_SCHEMA or METADATA_FILEINFO_SCHEMA
18811881
"""
1882-
merkle_root = self.metadata['current']['timestamp']['merkle_root']
1882+
if not merkle_root:
1883+
merkle_root = self.metadata['current']['timestamp']['merkle_root']
18831884

18841885
metadata_rolename = metadata_role + '-snapshot'
18851886

18861887
# Download Merkle path
18871888
upperbound_filelength = tuf.settings.MERKLE_FILELENGTH
1888-
self._update_merkle_metadata(metadata_rolename, upperbound_filelength)
1889+
self._update_merkle_metadata(metadata_rolename, upperbound_filelength, version)
18891890
metadata_directory = self.metadata_directory['current']
18901891
metadata_filename = metadata_rolename + '.json'
18911892
metadata_filepath = os.path.join(metadata_directory, metadata_filename)
@@ -2040,7 +2041,7 @@ def _update_metadata_if_changed(self, metadata_role,
20402041

20412042
if 'merkle_root' in self.metadata['current']['timestamp']:
20422043
# Download version information from merkle tree
2043-
contents = self._verify_merkle_path(metadata_role)
2044+
contents = self.verify_merkle_path(metadata_role)
20442045
expected_versioninfo = contents
20452046

20462047
else:
@@ -3419,3 +3420,42 @@ def download_target(self, target, destination_directory,
34193420
trusted_hashes, prefix_filename_with_hash)
34203421

34213422
securesystemslib.util.persist_temp_file(target_file_object, destination)
3423+
3424+
def download_metadata_version_if_exists(self, role_name, version, verification_fn, upperbound_filelength):
3425+
3426+
filename = role_name + ".json"
3427+
dirname, basename = os.path.split(filename)
3428+
remote_filename = os.path.join(dirname, str(version) + '.' + basename)
3429+
3430+
3431+
def neither_403_nor_404(mirror_error):
3432+
if isinstance(mirror_error, requests.exceptions.HTTPError):
3433+
if mirror_error.response.status_code in {403, 404}:
3434+
return False
3435+
return True
3436+
3437+
updated_metadata_object = None
3438+
3439+
try:
3440+
# Thoroughly verify it.
3441+
metadata_file_object = \
3442+
self._get_metadata_file(role_name, remote_filename,
3443+
upperbound_filelength, version, verification_fn)
3444+
metadata_file_object.seek(0)
3445+
updated_metadata_object = \
3446+
securesystemslib.util.load_json_string(metadata_file_object.read().decode('utf-8'))
3447+
# When we run into HTTP 403/404 error from ALL mirrors,
3448+
# metadata file is most likely missing.
3449+
except tuf.exceptions.NoWorkingMirrorError as exception:
3450+
for mirror_error in exception.mirror_errors.values():
3451+
# Otherwise, reraise the error, because it is not a simple HTTP
3452+
# error.
3453+
if neither_403_nor_404(mirror_error):
3454+
logger.exception('Misc error for root version '+str(version))
3455+
raise
3456+
else:
3457+
# Calling this function should give us a detailed stack trace
3458+
# including an HTTP error code, if any.
3459+
logger.exception('HTTP error for root version '+str(version))
3460+
3461+
return updated_metadata_object

0 commit comments

Comments
 (0)
Please sign in to comment.