Skip to content

Commit 2dc6e73

Browse files
committed
Initial poc for RSA Accumulator snapshot: client side
This is missing test data for the client Signed-off-by: Marina Moore <[email protected]>
1 parent 2149ba1 commit 2dc6e73

File tree

2 files changed

+335
-10
lines changed

2 files changed

+335
-10
lines changed

tests/test_updater.py

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1771,6 +1771,35 @@ def test_13__targets_of_role(self):
17711771

17721772

17731773

1774+
def test_snapshot_rsa_acc(self):
1775+
# replace timestamp with an RSA accumulator timestamp and create the updater
1776+
rsa_acc_timestamp = os.path.join(self.repository_directory, 'metadata', 'timestamp-rsa.json')
1777+
timestamp = os.path.join(self.repository_directory, 'metadata', 'timestamp.json')
1778+
1779+
shutil.move(rsa_acc_timestamp, timestamp)
1780+
1781+
repository_updater = updater.Updater(self.repository_name,
1782+
self.repository_mirrors)
1783+
repository_updater.refresh()
1784+
1785+
# Test verify RSA accumulator proof
1786+
snapshot_info = repository_updater.verify_rsa_acc_proof('targets')
1787+
self.assertEqual(snapshot_info['version'], 1)
1788+
1789+
snapshot_info = repository_updater.verify_rsa_acc_proof('role1')
1790+
self.assertEqual(snapshot_info['version'], 1)
1791+
1792+
# verify RSA accumulator with invalid role
1793+
self.assertRaises(tuf.exceptions.NoWorkingMirrorError,
1794+
repository_updater.verify_rsa_acc_proof, 'foo')
1795+
1796+
# Test get_one_valid_targetinfo with snapshot RSA accumulator
1797+
repository_updater.get_one_valid_targetinfo('file1.txt')
1798+
1799+
1800+
1801+
1802+
17741803
class TestMultiRepoUpdater(unittest_toolbox.Modified_TestCase):
17751804

17761805
def setUp(self):

tuf/client/updater.py

Lines changed: 306 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1087,8 +1087,12 @@ def refresh(self, unsafely_update_root_if_necessary=True):
10871087
# require strict checks on its required length.
10881088
self._update_metadata('timestamp', DEFAULT_TIMESTAMP_UPPERLENGTH)
10891089

1090-
self._update_metadata_if_changed('snapshot',
1091-
referenced_metadata='timestamp')
1090+
if 'rsa_acc' not in self.metadata['current']['timestamp']:
1091+
# If an RSA Accumulator is defined, do not update snapshot metadata. Instead,
1092+
# we will download the relevant proof files later when downloading
1093+
# a target.
1094+
self._update_metadata_if_changed('snapshot',
1095+
referenced_metadata='timestamp')
10921096
self._update_metadata_if_changed('targets')
10931097

10941098

@@ -1616,6 +1620,206 @@ def _get_metadata_file(self, metadata_role, remote_filename,
16161620

16171621

16181622

1623+
def signable_verification(self, metadata_role, file_object, expected_version):
1624+
# Verify 'file_object' according to the callable function.
1625+
# 'file_object' is also verified if decompressed above (i.e., the
1626+
# uncompressed version).
1627+
metadata_signable = \
1628+
securesystemslib.util.load_json_string(file_object.read().decode('utf-8'))
1629+
1630+
# Determine if the specification version number is supported. It is
1631+
# assumed that "spec_version" is in (major.minor.fix) format, (for
1632+
# example: "1.4.3") and that releases with the same major version
1633+
# number maintain backwards compatibility. Consequently, if the major
1634+
# version number of new metadata equals our expected major version
1635+
# number, the new metadata is safe to parse.
1636+
try:
1637+
metadata_spec_version = metadata_signable['signed']['spec_version']
1638+
metadata_spec_version_split = metadata_spec_version.split('.')
1639+
metadata_spec_major_version = int(metadata_spec_version_split[0])
1640+
metadata_spec_minor_version = int(metadata_spec_version_split[1])
1641+
1642+
code_spec_version_split = tuf.SPECIFICATION_VERSION.split('.')
1643+
code_spec_major_version = int(code_spec_version_split[0])
1644+
code_spec_minor_version = int(code_spec_version_split[1])
1645+
1646+
if metadata_spec_major_version != code_spec_major_version:
1647+
raise tuf.exceptions.UnsupportedSpecificationError(
1648+
'Downloaded metadata that specifies an unsupported '
1649+
'spec_version. This code supports major version number: ' +
1650+
repr(code_spec_major_version) + '; however, the obtained '
1651+
'metadata lists version number: ' + str(metadata_spec_version))
1652+
1653+
#report to user if minor versions do not match, continue with update
1654+
if metadata_spec_minor_version != code_spec_minor_version:
1655+
logger.info("Downloaded metadata that specifies a different minor " +
1656+
"spec_version. This code has version " +
1657+
str(tuf.SPECIFICATION_VERSION) +
1658+
" and the metadata lists version number " +
1659+
str(metadata_spec_version) +
1660+
". The update will continue as the major versions match.")
1661+
1662+
except (ValueError, TypeError) as error:
1663+
six.raise_from(securesystemslib.exceptions.FormatError('Improperly'
1664+
' formatted spec_version, which must be in major.minor.fix format'),
1665+
error)
1666+
1667+
# If the version number is unspecified, ensure that the version number
1668+
# downloaded is greater than the currently trusted version number for
1669+
# 'metadata_role'.
1670+
version_downloaded = metadata_signable['signed']['version']
1671+
1672+
if expected_version is not None:
1673+
# Verify that the downloaded version matches the version expected by
1674+
# the caller.
1675+
if version_downloaded != expected_version:
1676+
raise tuf.exceptions.BadVersionNumberError('Downloaded'
1677+
' version number: ' + repr(version_downloaded) + '. Version'
1678+
' number MUST be: ' + repr(expected_version))
1679+
1680+
# The caller does not know which version to download. Verify that the
1681+
# downloaded version is at least greater than the one locally
1682+
# available.
1683+
else:
1684+
# Verify that the version number of the locally stored
1685+
# 'timestamp.json', if available, is less than what was downloaded.
1686+
# Otherwise, accept the new timestamp with version number
1687+
# 'version_downloaded'.
1688+
1689+
try:
1690+
current_version = \
1691+
self.metadata['current'][metadata_role]['version']
1692+
1693+
if version_downloaded < current_version:
1694+
raise tuf.exceptions.ReplayedMetadataError(metadata_role,
1695+
version_downloaded, current_version)
1696+
1697+
except KeyError:
1698+
logger.info(metadata_role + ' not available locally.')
1699+
1700+
self._verify_metadata_file(file_object, metadata_role)
1701+
1702+
1703+
1704+
1705+
1706+
1707+
def _update_rsa_acc_metadata(self, proof_filename, upperbound_filelength,
1708+
version=None):
1709+
"""
1710+
<Purpose>
1711+
Non-public method that downloads, verifies, and 'installs' the proof
1712+
metadata belonging to 'proof_filename'. Calling this method implies
1713+
that the 'proof_filename' on the repository is newer than the client's,
1714+
and thus needs to be re-downloaded. The current and previous metadata
1715+
stores are updated if the newly downloaded metadata is successfully
1716+
downloaded and verified. This method also assumes that the store of
1717+
top-level metadata is the latest and exists.
1718+
1719+
<Arguments>
1720+
proof_filename:
1721+
The name of the metadata. This is an RSA accumulator proof file and should
1722+
not end in '.json'. Examples: 'role1-snapshot', 'targets-snapshot'
1723+
1724+
upperbound_filelength:
1725+
The expected length, or upper bound, of the metadata file to be
1726+
downloaded.
1727+
1728+
version:
1729+
The expected and required version number of the 'proof_filename' file
1730+
downloaded. 'version' is an integer.
1731+
1732+
<Exceptions>
1733+
tuf.exceptions.NoWorkingMirrorError:
1734+
The metadata cannot be updated. This is not specific to a single
1735+
failure but rather indicates that all possible ways to update the
1736+
metadata have been tried and failed.
1737+
1738+
<Side Effects>
1739+
The metadata file belonging to 'proof_filename' is downloaded from a
1740+
repository mirror. If the metadata is valid, it is stored in the
1741+
metadata store.
1742+
1743+
<Returns>
1744+
None.
1745+
"""
1746+
1747+
# Construct the metadata filename as expected by the download/mirror
1748+
# modules.
1749+
metadata_filename = proof_filename + '.json'
1750+
1751+
# Attempt a file download from each mirror until the file is downloaded and
1752+
# verified. If the signature of the downloaded file is valid, proceed,
1753+
# otherwise log a warning and try the next mirror. 'metadata_file_object'
1754+
# is the file-like object returned by 'download.py'. 'metadata_signable'
1755+
# is the object extracted from 'metadata_file_object'. Metadata saved to
1756+
# files are regarded as 'signable' objects, conformant to
1757+
# 'tuf.formats.SIGNABLE_SCHEMA'.
1758+
#
1759+
# Some metadata (presently timestamp) will be downloaded "unsafely", in the
1760+
# sense that we can only estimate its true length and know nothing about
1761+
# its version. This is because not all metadata will have other metadata
1762+
# for it; otherwise we will have an infinite regress of metadata signing
1763+
# for each other. In this case, we will download the metadata up to the
1764+
# best length we can get for it, not request a specific version, but
1765+
# perform the rest of the checks (e.g., signature verification).
1766+
1767+
remote_filename = metadata_filename
1768+
filename_version = ''
1769+
1770+
if self.consistent_snapshot and version:
1771+
filename_version = version
1772+
dirname, basename = os.path.split(remote_filename)
1773+
remote_filename = os.path.join(
1774+
dirname, str(filename_version) + '.' + basename)
1775+
1776+
verification_fn = None
1777+
1778+
metadata_file_object = \
1779+
self._get_metadata_file(proof_filename, remote_filename,
1780+
upperbound_filelength, version, verification_fn)
1781+
1782+
# The metadata has been verified. Move the metadata file into place.
1783+
# First, move the 'current' metadata file to the 'previous' directory
1784+
# if it exists.
1785+
current_filepath = os.path.join(self.metadata_directory['current'],
1786+
metadata_filename)
1787+
current_filepath = os.path.abspath(current_filepath)
1788+
securesystemslib.util.ensure_parent_dir(current_filepath)
1789+
1790+
previous_filepath = os.path.join(self.metadata_directory['previous'],
1791+
metadata_filename)
1792+
previous_filepath = os.path.abspath(previous_filepath)
1793+
1794+
if os.path.exists(current_filepath):
1795+
# Previous metadata might not exist, say when delegations are added.
1796+
securesystemslib.util.ensure_parent_dir(previous_filepath)
1797+
shutil.move(current_filepath, previous_filepath)
1798+
1799+
# Next, move the verified updated metadata file to the 'current' directory.
1800+
metadata_file_object.seek(0)
1801+
updated_metadata_object = \
1802+
securesystemslib.util.load_json_string(metadata_file_object.read().decode('utf-8'))
1803+
1804+
securesystemslib.util.persist_temp_file(metadata_file_object, current_filepath)
1805+
1806+
# Extract the metadata object so we can store it to the metadata store.
1807+
# 'current_metadata_object' set to 'None' if there is not an object
1808+
# stored for 'proof_filename'.
1809+
current_metadata_object = self.metadata['current'].get(proof_filename)
1810+
1811+
# Finally, update the metadata and fileinfo stores, and rebuild the
1812+
# key and role info for the top-level roles if 'proof_filename' is root.
1813+
# Rebuilding the key and role info is required if the newly-installed
1814+
# root metadata has revoked keys or updated any top-level role information.
1815+
logger.debug('Updated ' + repr(current_filepath) + '.')
1816+
self.metadata['previous'][proof_filename] = current_metadata_object
1817+
self.metadata['current'][proof_filename] = updated_metadata_object
1818+
1819+
1820+
1821+
1822+
16191823

16201824
def _update_metadata(self, metadata_role, upperbound_filelength, version=None):
16211825
"""
@@ -1732,6 +1936,87 @@ def _update_metadata(self, metadata_role, upperbound_filelength, version=None):
17321936

17331937

17341938

1939+
def verify_rsa_acc_proof(self, metadata_role, version=None, rsa_acc=None):
1940+
"""
1941+
<Purpose>
1942+
Download the RSA accumulator proof associated with metadata_role and verify the hashes.
1943+
<Arguments>
1944+
metadata_role:
1945+
The name of the metadata role. This should not include a file extension.
1946+
<Exceptions>
1947+
tuf.exceptions.RepositoryError:
1948+
If the snapshot rsa accumulator file is invalid or the verification fails
1949+
<Returns>
1950+
A dictionary containing the snapshot information about metadata role,
1951+
conforming to VERSIONINFO_SCHEMA or METADATA_FILEINFO_SCHEMA
1952+
"""
1953+
1954+
# Modulus from https://en.wikipedia.org/wiki/RSA_numbers#RSA-2048
1955+
# We will want to generate a new one
1956+
# This is duplicate code from repo lib, should live somewhere else
1957+
Modulus = "2519590847565789349402718324004839857142928212620403202777713783604366202070759555626401852588078" + \
1958+
"4406918290641249515082189298559149176184502808489120072844992687392807287776735971418347270261896375014971" + \
1959+
"8246911650776133798590957000973304597488084284017974291006424586918171951187461215151726546322822168699875" + \
1960+
"4918242243363725908514186546204357679842338718477444792073993423658482382428119816381501067481045166037730" + \
1961+
"6056201619676256133844143603833904414952634432190114657544454178424020924616515723350778707749817125772467" + \
1962+
"962926386356373289912154831438167899885040445364023527381951378636564391212010397122822120720357"
1963+
m = int(Modulus, 10)
1964+
1965+
1966+
if not rsa_acc:
1967+
rsa_acc = self.metadata['current']['timestamp']['rsa_acc']
1968+
1969+
metadata_rolename = metadata_role + '-snapshot'
1970+
1971+
# Download RSA accumulator proof
1972+
upperbound_filelength = tuf.settings.MERKLE_FILELENGTH
1973+
self._update_rsa_acc_metadata(metadata_rolename, upperbound_filelength, version)
1974+
metadata_directory = self.metadata_directory['current']
1975+
metadata_filename = metadata_rolename + '.json'
1976+
metadata_filepath = os.path.join(metadata_directory, metadata_filename)
1977+
1978+
# Ensure the metadata path is valid/exists, else ignore the call.
1979+
if not os.path.exists(metadata_filepath):
1980+
# No RSA accumulator proof found
1981+
raise tuf.exceptions.RepositoryError('No snapshot rsa accumulator proof file for ' +
1982+
metadata_role)
1983+
try:
1984+
snapshot_rsa_acc_proof = securesystemslib.util.load_json_file(
1985+
metadata_filepath)
1986+
1987+
# Although the metadata file may exist locally, it may not
1988+
# be a valid json file. On the next refresh cycle, it will be
1989+
# updated as required. If Root if cannot be loaded from disk
1990+
# successfully, an exception should be raised by the caller.
1991+
except securesystemslib.exceptions.Error:
1992+
return
1993+
1994+
# check the format
1995+
tuf.formats.SNAPSHOT_RSA_ACC_SCHEMA.check_match(snapshot_rsa_acc_proof)
1996+
1997+
# canonicalize the contents to determine the RSA accumulator prime
1998+
contents = snapshot_rsa_acc_proof['leaf_contents']
1999+
json_contents = securesystemslib.formats.encode_canonical(contents)
2000+
2001+
prime = repository_lib.hash_to_prime(json_contents)
2002+
2003+
# RSA accumulator proof
2004+
proof = snapshot_rsa_acc_proof['rsa_acc_proof']
2005+
rsa_acc_proof_test = pow(proof, prime, m)
2006+
2007+
# Does the result match the RSA accumulator?
2008+
if rsa_acc_proof_test != rsa_acc:
2009+
raise tuf.exceptions.RepositoryError('RSA accumulator ' + rsa_acc +
2010+
' does not match the proof ' + proof + ' for ' + metadata_role)
2011+
2012+
# return the verified snapshot contents
2013+
return contents
2014+
2015+
2016+
2017+
2018+
2019+
17352020
def _update_metadata_if_changed(self, metadata_role,
17362021
referenced_metadata='snapshot'):
17372022
"""
@@ -1801,7 +2086,9 @@ def _update_metadata_if_changed(self, metadata_role,
18012086

18022087
# Ensure the referenced metadata has been loaded. The 'root' role may be
18032088
# updated without having 'snapshot' available.
1804-
if referenced_metadata not in self.metadata['current']:
2089+
# When a snapshot rsa accumulator is used, there will not be a snapshot file.
2090+
# Instead, if the snapshot rsa proof is missing, this will error below.
2091+
if 'rsa_acc' not in self.metadata['current']['timestamp'] and referenced_metadata not in self.metadata['current']:
18052092
raise exceptions.RepositoryError('Cannot update'
18062093
' ' + repr(metadata_role) + ' because ' + referenced_metadata + ' is'
18072094
' missing.')
@@ -1813,12 +2100,18 @@ def _update_metadata_if_changed(self, metadata_role,
18132100
repr(referenced_metadata)+ '. ' + repr(metadata_role) +
18142101
' may be updated.')
18152102

1816-
# Simply return if the metadata for 'metadata_role' has not been updated,
1817-
# according to the uncompressed metadata provided by the referenced
1818-
# metadata. The metadata is considered updated if its version number is
1819-
# strictly greater than its currently trusted version number.
1820-
expected_versioninfo = self.metadata['current'][referenced_metadata] \
1821-
['meta'][metadata_filename]
2103+
if 'rsa_acc' in self.metadata['current']['timestamp']:
2104+
# Download version information from RSA accumulator proof
2105+
contents = self.verify_rsa_acc_proof(metadata_role)
2106+
expected_versioninfo = contents
2107+
2108+
else:
2109+
# Simply return if the metadata for 'metadata_role' has not been updated,
2110+
# according to the uncompressed metadata provided by the referenced
2111+
# metadata. The metadata is considered updated if its version number is
2112+
# strictly greater than its currently trusted version number.
2113+
expected_versioninfo = self.metadata['current'][referenced_metadata] \
2114+
['meta'][metadata_filename]
18222115

18232116
if not self._versioninfo_has_been_updated(metadata_filename,
18242117
expected_versioninfo):
@@ -2388,7 +2681,10 @@ def _refresh_targets_metadata(self, rolename='targets',
23882681

23892682
roles_to_update = []
23902683

2391-
if rolename + '.json' in self.metadata['current']['snapshot']['meta']:
2684+
# Add the role if it is listed in snapshot. If a snapshot rsa
2685+
# accumulator is used, the snapshot check will be done later when
2686+
# the proof is verified
2687+
if 'rsa_acc' in self.metadata['current']['timestamp'] or rolename + '.json' in self.metadata['current']['snapshot']['meta']:
23922688
roles_to_update.append(rolename)
23932689

23942690
if refresh_all_delegated_roles:

0 commit comments

Comments
 (0)