@@ -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