Skip to content

Commit 412625d

Browse files
committed
Separate _update_merkle_metadata from _update_metadata
This creates a separate function for updating merkle metadata to avoid all of the merkle tree special cases in _update_metadata. In addition, this cleans up the calling of _update_merkle_metadata by creating a MERKLE_FILELENGTH in tuf.settings Signed-off-by: marinamoore <[email protected]>
1 parent 7d81f70 commit 412625d

2 files changed

Lines changed: 188 additions & 76 deletions

File tree

tuf/client/updater.py

Lines changed: 186 additions & 76 deletions
Original file line numberDiff line numberDiff line change
@@ -1085,7 +1085,8 @@ def refresh(self, unsafely_update_root_if_necessary=True):
10851085

10861086
if 'merkle_root' not in self.metadata['current']['timestamp']:
10871087
# If merkle root is set, do not update snapshot metadata. Instead,
1088-
# download the relevant merkle path when downloading a target.
1088+
# we will download the relevant merkle path later when downloading
1089+
# a target.
10891090
self._update_metadata_if_changed('snapshot',
10901091
referenced_metadata='timestamp')
10911092
self._update_metadata_if_changed('targets')
@@ -1753,8 +1754,124 @@ def _get_file(self, filepath, verify_file_function, file_type, file_length,
17531754

17541755

17551756

1756-
def _update_metadata(self, metadata_role, upperbound_filelength, version=None,
1757-
snapshot_merkle=False):
1757+
def _update_merkle_metadata(self, merkle_filename, upperbound_filelength,
1758+
version=None):
1759+
"""
1760+
<Purpose>
1761+
Non-public method that downloads, verifies, and 'installs' the merkle
1762+
metadata belonging to 'merkle_filename'. Calling this method implies
1763+
that the 'merkle_filename' on the repository is newer than the client's,
1764+
and thus needs to be re-downloaded. The current and previous metadata
1765+
stores are updated if the newly downloaded metadata is successfully
1766+
downloaded and verified. This method also assumes that the store of
1767+
top-level metadata is the latest and exists.
1768+
1769+
<Arguments>
1770+
merkle_filename:
1771+
The name of the metadata. This is a merkle tree file and should
1772+
not end in '.json'. Examples: 'role1-merkle', 'targets-merkle'
1773+
1774+
upperbound_filelength:
1775+
The expected length, or upper bound, of the metadata file to be
1776+
downloaded.
1777+
1778+
version:
1779+
The expected and required version number of the 'merkle_filename' file
1780+
downloaded. 'expected_version' is an integer.
1781+
1782+
<Exceptions>
1783+
tuf.exceptions.NoWorkingMirrorError:
1784+
The metadata cannot be updated. This is not specific to a single
1785+
failure but rather indicates that all possible ways to update the
1786+
metadata have been tried and failed.
1787+
1788+
<Side Effects>
1789+
The metadata file belonging to 'merkle_filenaem' is downloaded from a
1790+
repository mirror. If the metadata is valid, it is stored in the
1791+
metadata store.
1792+
1793+
<Returns>
1794+
None.
1795+
"""
1796+
1797+
# Construct the metadata filename as expected by the download/mirror
1798+
# modules.
1799+
metadata_filename = merkle_filename + '.json'
1800+
1801+
# Attempt a file download from each mirror until the file is downloaded and
1802+
# verified. If the signature of the downloaded file is valid, proceed,
1803+
# otherwise log a warning and try the next mirror. 'metadata_file_object'
1804+
# is the file-like object returned by 'download.py'. 'metadata_signable'
1805+
# is the object extracted from 'metadata_file_object'. Metadata saved to
1806+
# files are regarded as 'signable' objects, conformant to
1807+
# 'tuf.formats.SIGNABLE_SCHEMA'.
1808+
#
1809+
# Some metadata (presently timestamp) will be downloaded "unsafely", in the
1810+
# sense that we can only estimate its true length and know nothing about
1811+
# its version. This is because not all metadata will have other metadata
1812+
# for it; otherwise we will have an infinite regress of metadata signing
1813+
# for each other. In this case, we will download the metadata up to the
1814+
# best length we can get for it, not request a specific version, but
1815+
# perform the rest of the checks (e.g., signature verification).
1816+
1817+
remote_filename = metadata_filename
1818+
filename_version = ''
1819+
1820+
if self.consistent_snapshot and version:
1821+
filename_version = version
1822+
dirname, basename = os.path.split(remote_filename)
1823+
remote_filename = os.path.join(
1824+
dirname, str(filename_version) + '.' + basename)
1825+
1826+
verification_fn = None
1827+
1828+
metadata_file_object = \
1829+
self._get_metadata_file(merkle_filename, remote_filename,
1830+
upperbound_filelength, version, verification_fn)
1831+
1832+
# The metadata has been verified. Move the metadata file into place.
1833+
# First, move the 'current' metadata file to the 'previous' directory
1834+
# if it exists.
1835+
current_filepath = os.path.join(self.metadata_directory['current'],
1836+
metadata_filename)
1837+
current_filepath = os.path.abspath(current_filepath)
1838+
securesystemslib.util.ensure_parent_dir(current_filepath)
1839+
1840+
previous_filepath = os.path.join(self.metadata_directory['previous'],
1841+
metadata_filename)
1842+
previous_filepath = os.path.abspath(previous_filepath)
1843+
1844+
if os.path.exists(current_filepath):
1845+
# Previous metadata might not exist, say when delegations are added.
1846+
securesystemslib.util.ensure_parent_dir(previous_filepath)
1847+
shutil.move(current_filepath, previous_filepath)
1848+
1849+
# Next, move the verified updated metadata file to the 'current' directory.
1850+
metadata_file_object.seek(0)
1851+
updated_metadata_object = \
1852+
securesystemslib.util.load_json_string(metadata_file_object.read().decode('utf-8'))
1853+
1854+
securesystemslib.util.persist_temp_file(metadata_file_object, current_filepath)
1855+
1856+
# Extract the metadata object so we can store it to the metadata store.
1857+
# 'current_metadata_object' set to 'None' if there is not an object
1858+
# stored for 'merkle_filename'.
1859+
current_metadata_object = self.metadata['current'].get(merkle_filename)
1860+
1861+
# Finally, update the metadata and fileinfo stores, and rebuild the
1862+
# key and role info for the top-level roles if 'merkle_filename' is root.
1863+
# Rebuilding the key and role info is required if the newly-installed
1864+
# root metadata has revoked keys or updated any top-level role information.
1865+
logger.debug('Updated ' + repr(current_filepath) + '.')
1866+
self.metadata['previous'][merkle_filename] = current_metadata_object
1867+
self.metadata['current'][merkle_filename] = updated_metadata_object
1868+
1869+
1870+
1871+
1872+
1873+
1874+
def _update_metadata(self, metadata_role, upperbound_filelength, version=None):
17581875
"""
17591876
<Purpose>
17601877
Non-public method that downloads, verifies, and 'installs' the metadata
@@ -1778,11 +1895,6 @@ def _update_metadata(self, metadata_role, upperbound_filelength, version=None,
17781895
The expected and required version number of the 'metadata_role' file
17791896
downloaded. 'expected_version' is an integer.
17801897
1781-
snapshot_merkle:
1782-
Is the metadata to be updated for a snapshot merkle file?
1783-
Snapshot merkle metadata does not contain a signature, but must
1784-
instead be verified using _verify_merkle_path.
1785-
17861898
<Exceptions>
17871899
tuf.exceptions.NoWorkingMirrorError:
17881900
The metadata cannot be updated. This is not specific to a single
@@ -1827,9 +1939,7 @@ def _update_metadata(self, metadata_role, upperbound_filelength, version=None,
18271939
remote_filename = os.path.join(
18281940
dirname, str(filename_version) + '.' + basename)
18291941

1830-
verification_fn = None
1831-
if not snapshot_merkle:
1832-
verification_fn = self.signable_verification
1942+
verification_fn = self.signable_verification
18331943

18341944
metadata_file_object = \
18351945
self._get_metadata_file(metadata_role, remote_filename,
@@ -1862,11 +1972,7 @@ def _update_metadata(self, metadata_role, upperbound_filelength, version=None,
18621972
# Extract the metadata object so we can store it to the metadata store.
18631973
# 'current_metadata_object' set to 'None' if there is not an object
18641974
# stored for 'metadata_role'.
1865-
if snapshot_merkle:
1866-
# Snaphot merkle files are not signed
1867-
updated_metadata_object = metadata_signable
1868-
else:
1869-
updated_metadata_object = metadata_signable['signed']
1975+
updated_metadata_object = metadata_signable['signed']
18701976
current_metadata_object = self.metadata['current'].get(metadata_role)
18711977

18721978
# Finally, update the metadata and fileinfo stores, and rebuild the
@@ -1876,8 +1982,7 @@ def _update_metadata(self, metadata_role, upperbound_filelength, version=None,
18761982
logger.debug('Updated ' + repr(current_filepath) + '.')
18771983
self.metadata['previous'][metadata_role] = current_metadata_object
18781984
self.metadata['current'][metadata_role] = updated_metadata_object
1879-
if not snapshot_merkle:
1880-
self._update_versioninfo(metadata_filename)
1985+
self._update_versioninfo(metadata_filename)
18811986

18821987

18831988

@@ -1893,76 +1998,81 @@ def _verify_merkle_path(self, metadata_role):
18931998
<Exceptions>
18941999
tuf.exceptions.RepositoryError:
18952000
If the snapshot merkle file is invalid or the verification fails
1896-
Returns the snapshot information about metadata role.
2001+
<Returns>
2002+
A dictionary containing the snapshot information about metadata role,
2003+
conforming to VERSIONINFO_SCHEMA or METADATA_FILEINFO_SCHEMA
18972004
"""
18982005
merkle_root = self.metadata['current']['timestamp']['merkle_root']
18992006

2007+
metadata_rolename = metadata_role + '-snapshot'
2008+
19002009
# Download Merkle path
1901-
self._update_metadata(metadata_role + '-snapshot', 1000, snapshot_merkle=True)
2010+
upperbound_filelength = tuf.settings.MERKLE_FILELENGTH
2011+
self._update_merkle_metadata(metadata_rolename, upperbound_filelength)
19022012
metadata_directory = self.metadata_directory['current']
1903-
metadata_filename = metadata_role + '-snapshot.json'
2013+
metadata_filename = metadata_rolename + '.json'
19042014
metadata_filepath = os.path.join(metadata_directory, metadata_filename)
1905-
# Ensure the metadata path is valid/exists, else ignore the call.
1906-
if os.path.exists(metadata_filepath):
1907-
try:
1908-
snapshot_merkle = securesystemslib.util.load_json_file(
1909-
metadata_filepath)
19102015

1911-
# Although the metadata file may exist locally, it may not
1912-
# be a valid json file. On the next refresh cycle, it will be
1913-
# updated as required. If Root if cannot be loaded from disk
1914-
# successfully, an exception should be raised by the caller.
1915-
except securesystemslib.exceptions.Error:
1916-
return
2016+
# Ensure the metadata path is valid/exists, else ignore the call.
2017+
if not os.path.exists(metadata_filepath):
2018+
# No merkle path found
2019+
raise tuf.exceptions.RepositoryError('No snapshot merkle file for ' +
2020+
metadata_role)
2021+
try:
2022+
snapshot_merkle = securesystemslib.util.load_json_file(
2023+
metadata_filepath)
2024+
2025+
# Although the metadata file may exist locally, it may not
2026+
# be a valid json file. On the next refresh cycle, it will be
2027+
# updated as required. If Root if cannot be loaded from disk
2028+
# successfully, an exception should be raised by the caller.
2029+
except securesystemslib.exceptions.Error:
2030+
return
19172031

1918-
# verify the Merkle path
1919-
tuf.formats.SNAPSHOT_MERKLE_SCHEMA.check_match(snapshot_merkle)
2032+
# verify the Merkle path
2033+
tuf.formats.SNAPSHOT_MERKLE_SCHEMA.check_match(snapshot_merkle)
2034+
2035+
# hash the contents to determine the leaf hash in the merkle tree
2036+
contents = snapshot_merkle['leaf_contents']
2037+
json_contents = securesystemslib.formats.encode_canonical(contents)
2038+
digest_object = securesystemslib.hash.digest()
2039+
digest_object.update((json_contents).encode('utf-8'))
2040+
node_hash = digest_object.hexdigest()
2041+
2042+
# For each hash in the merkle_path, determine if the current node is
2043+
# a left of a right node using the path_directions, then combine
2044+
# the hash from merkle_path with the current node_hash to determine
2045+
# the next node_hash. At the end, the node_hash should match the hash
2046+
# in merkle_root
2047+
merkle_path = snapshot_merkle['merkle_path']
2048+
path_directions = snapshot_merkle['path_directions']
2049+
2050+
# If merkle_path and path_directions have different lengths,
2051+
# the verification will not be possible
2052+
if len(merkle_path) != len(path_directions):
2053+
raise tuf.exceptions.RepositoryError('Invalid merkle path for ' +
2054+
metadata_role)
19202055

1921-
# hash the contents to determine the leaf hash in the merkle tree
1922-
contents = snapshot_merkle['leaf_contents']
1923-
json_contents = securesystemslib.formats.encode_canonical(contents)
1924-
digest_object = securesystemslib.hash.digest()
1925-
digest_object.update((json_contents).encode('utf-8'))
2056+
for index in range(len(merkle_path)):
2057+
i = str(index)
2058+
if path_directions[i] < 0:
2059+
# The current node is a left node
2060+
digest_object = securesystemslib.hash.digest()
2061+
digest_object.update((node_hash + merkle_path[i]).encode('utf-8'))
2062+
else:
2063+
# The current node is a right node
2064+
digest_object = securesystemslib.hash.digest()
2065+
digest_object.update((merkle_path[i] + node_hash).encode('utf-8'))
19262066
node_hash = digest_object.hexdigest()
19272067

1928-
# For each hash in the merkle_path, determine if the current node is
1929-
# a left of a right node using the path_directions, then combine
1930-
# the hash from merkle_path with the current node_hash to determine
1931-
# the next node_hash. At the end, the node_hash should match the hash
1932-
# in merkle_root
1933-
merkle_path = snapshot_merkle['merkle_path']
1934-
path_directions = snapshot_merkle['path_directions']
1935-
1936-
# If merkle_path and path_directions have different lengths,
1937-
# the verification will not be possible
1938-
if len(merkle_path) != len(path_directions):
1939-
raise tuf.exceptions.RepositoryError('Invalid merkle path for ' +
1940-
metadata_role)
1941-
1942-
for index in range(len(merkle_path)):
1943-
i = str(index)
1944-
if path_directions[i] < 0:
1945-
# The current node is a left node
1946-
digest_object = securesystemslib.hash.digest()
1947-
digest_object.update((node_hash + merkle_path[i]).encode('utf-8'))
1948-
else:
1949-
# The current node is a right node
1950-
digest_object = securesystemslib.hash.digest()
1951-
digest_object.update((merkle_path[i] + node_hash).encode('utf-8'))
1952-
node_hash = digest_object.hexdigest()
1953-
1954-
# Does the result match the merkle root?
1955-
if node_hash != merkle_root:
1956-
raise tuf.exceptions.RepositoryError('The merkle root ' + merkle_root +
1957-
' does not match the hash ' + node_hash + ' for ' + metadata_role)
2068+
# Does the result match the merkle root?
2069+
if node_hash != merkle_root:
2070+
raise tuf.exceptions.RepositoryError('The merkle root ' + merkle_root +
2071+
' does not match the hash ' + node_hash + ' for ' + metadata_role)
19582072

1959-
# return the verified snapshot contents
1960-
return contents
2073+
# return the verified snapshot contents
2074+
return contents
19612075

1962-
else:
1963-
# No merkle path found
1964-
raise tuf.exceptions.RepositoryError('No snapshot merkle file for ' +
1965-
metadata_role)
19662076

19672077

19682078

@@ -2637,7 +2747,7 @@ def _refresh_targets_metadata(self, rolename='targets',
26372747
roles_to_update = []
26382748

26392749
# Add the role if it is listed in snapshot. If snapshot merkle
2640-
# trees are used, the snaphot check will be done later when
2750+
# trees are used, the snapshot check will be done later when
26412751
# the merkle tree is verified
26422752
if 'merkle_root' in self.metadata['current']['timestamp'] or rolename + '.json' in self.metadata['current']['snapshot']['meta']:
26432753
roles_to_update.append(rolename)

tuf/settings.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,8 @@
7474
# download Targets metadata.
7575
DEFAULT_TARGETS_REQUIRED_LENGTH = 5000000 #bytes
7676

77+
MERKLE_FILELENGTH = 10000
78+
7779
# Set a timeout value in seconds (float) for non-blocking socket operations.
7880
SOCKET_TIMEOUT = 4 #seconds
7981

0 commit comments

Comments
 (0)