|
30 | 30 | import shutil |
31 | 31 | import json |
32 | 32 | import tempfile |
| 33 | +from pyblake2 import blake2b |
| 34 | +import random |
33 | 35 |
|
34 | 36 | import securesystemslib # pylint: disable=unused-import |
35 | 37 | from securesystemslib import exceptions as sslib_exceptions |
@@ -90,7 +92,7 @@ def _generate_and_write_metadata(rolename, metadata_filename, |
90 | 92 | increment_version_number=True, repository_name='default', |
91 | 93 | use_existing_fileinfo=False, use_timestamp_length=True, |
92 | 94 | use_timestamp_hashes=True, use_snapshot_length=False, |
93 | | - use_snapshot_hashes=False): |
| 95 | + use_snapshot_hashes=False, rsa_acc=False): |
94 | 96 | """ |
95 | 97 | Non-public function that can generate and write the metadata for the |
96 | 98 | specified 'rolename'. It also increments the version number of 'rolename' if |
@@ -122,6 +124,16 @@ def _generate_and_write_metadata(rolename, metadata_filename, |
122 | 124 | storage_backend, consistent_snapshot, repository_name, |
123 | 125 | use_length=use_snapshot_length, use_hashes=use_snapshot_hashes) |
124 | 126 |
|
| 127 | + if rsa_acc: |
| 128 | + root, leaves = _build_rsa_acc(fileinfodict) |
| 129 | + |
| 130 | + # Add the rsa accumulator to the timestamp roleinfo |
| 131 | + timestamp_roleinfo = tuf.roledb.get_roleinfo('timestamp', repository_name) |
| 132 | + timestamp_roleinfo['rsa_acc'] = root |
| 133 | + |
| 134 | + tuf.roledb.update_roleinfo('timestamp', timestamp_roleinfo, |
| 135 | + repository_name=repository_name) |
| 136 | + |
125 | 137 |
|
126 | 138 | _log_warning_if_expires_soon(SNAPSHOT_FILENAME, roleinfo['expires'], |
127 | 139 | SNAPSHOT_EXPIRES_WARN_SECONDS) |
@@ -180,6 +192,9 @@ def _generate_and_write_metadata(rolename, metadata_filename, |
180 | 192 | else: |
181 | 193 | logger.debug('Not incrementing ' + repr(rolename) + '\'s version number.') |
182 | 194 |
|
| 195 | + if rolename == 'snapshot' and rsa_acc: |
| 196 | + _write_rsa_proofs(root, leaves, storage_backend, metadata_directory, metadata['version']) |
| 197 | + |
183 | 198 | if rolename in roledb.TOP_LEVEL_ROLES and not allow_partially_signed: |
184 | 199 | # Verify that the top-level 'rolename' is fully signed. Only a delegated |
185 | 200 | # role should not be written to disk without full verification of its |
@@ -1541,6 +1556,169 @@ def _get_hashes_and_length_if_needed(use_length, use_hashes, full_file_path, |
1541 | 1556 |
|
1542 | 1557 |
|
1543 | 1558 |
|
| 1559 | + |
| 1560 | +# I couldn't find a currently maintained python library for this, so |
| 1561 | +# implementing it here. It would be better to implement this in c, |
| 1562 | +# even better to use an existing library |
| 1563 | +# This is inspired by: https://www.literateprograms.org/miller-rabin_primality_test__python_.html |
| 1564 | +def miller_rabin_round(a, s, d, n): |
| 1565 | + a_to_power = pow(a, d, n) |
| 1566 | + if a_to_power == 1: |
| 1567 | + return True |
| 1568 | + for i in range(s): |
| 1569 | + if a_to_power == n - 1: |
| 1570 | + return True |
| 1571 | + a_to_power = (a_to_power * a_to_power) % n |
| 1572 | + return False |
| 1573 | + |
| 1574 | + |
| 1575 | + |
| 1576 | + |
| 1577 | + |
| 1578 | +def miller_rabin(n, rounds): |
| 1579 | + if n == 1: |
| 1580 | + return false |
| 1581 | + if n == 2 or n == 3: |
| 1582 | + return true |
| 1583 | + |
| 1584 | + d = n -1 |
| 1585 | + s = 0 |
| 1586 | + while d % 2 == 0: |
| 1587 | + d = d >> 1 |
| 1588 | + s = s + 1 |
| 1589 | + |
| 1590 | + for i in range(rounds): |
| 1591 | + a = random.randrange(n) |
| 1592 | + if not miller_rabin_round(a, s, d, n): |
| 1593 | + return False |
| 1594 | + return True |
| 1595 | + |
| 1596 | + |
| 1597 | + |
| 1598 | + |
| 1599 | +# RSA Accumulator code insprired by https://github.com/ElrondNetwork/elrond-go/blob/v1.0.30/crypto/accumulator/rsa/rsaAcc.go |
| 1600 | +def hash_to_prime(data): |
| 1601 | + # TODO: move constant definitions |
| 1602 | + basesMillerRabin = 12 |
| 1603 | + |
| 1604 | + h = blake2b(str(data).encode('utf-8')) |
| 1605 | + p = int(h.hexdigest(), 16) |
| 1606 | + |
| 1607 | + # use Miller-Rabin primality test, if p is not prime, do more rounds of hashing |
| 1608 | + while (not miller_rabin(p, basesMillerRabin)): |
| 1609 | + h = blake2b(str(p).encode('utf-8')) |
| 1610 | + p = int(h.hexdigest(), 16) |
| 1611 | + |
| 1612 | + return p |
| 1613 | + |
| 1614 | + |
| 1615 | + |
| 1616 | +class acc_contents(object): |
| 1617 | + contents = None |
| 1618 | + name = None |
| 1619 | + proof = None |
| 1620 | + |
| 1621 | + def __init__(self, name, contents): |
| 1622 | + # Include the name to ensure the digest differs between elements and cannot be replayed |
| 1623 | + contents["name"] = name |
| 1624 | + self.contents = contents |
| 1625 | + self.name = name |
| 1626 | + |
| 1627 | + def set_proof(self, proof): |
| 1628 | + self.proof = proof |
| 1629 | + |
| 1630 | + |
| 1631 | + |
| 1632 | +def _build_rsa_acc(fileinfodict): |
| 1633 | + """ |
| 1634 | + Create an RSA accululator from the snapshot fileinfo and writes it to individual snapshot files |
| 1635 | +
|
| 1636 | + Returns the root and leaves |
| 1637 | + """ |
| 1638 | + |
| 1639 | + # RSA accululator contants |
| 1640 | + # TODO: move constant definitions |
| 1641 | + g = 3 |
| 1642 | + # Modulus from https://en.wikipedia.org/wiki/RSA_numbers#RSA-2048 |
| 1643 | + # We will want to generate a new one |
| 1644 | + Modulus = "2519590847565789349402718324004839857142928212620403202777713783604366202070759555626401852588078" + \ |
| 1645 | + "4406918290641249515082189298559149176184502808489120072844992687392807287776735971418347270261896375014971" + \ |
| 1646 | + "8246911650776133798590957000973304597488084284017974291006424586918171951187461215151726546322822168699875" + \ |
| 1647 | + "4918242243363725908514186546204357679842338718477444792073993423658482382428119816381501067481045166037730" + \ |
| 1648 | + "6056201619676256133844143603833904414952634432190114657544454178424020924616515723350778707749817125772467" + \ |
| 1649 | + "962926386356373289912154831438167899885040445364023527381951378636564391212010397122822120720357" |
| 1650 | + m = int(Modulus, 10) |
| 1651 | + |
| 1652 | + |
| 1653 | + # We will build the accumulator starting with the leaf nodes. Each |
| 1654 | + # leaf contains snapshot information for a single metadata file. |
| 1655 | + leaves = [] |
| 1656 | + primes = [] |
| 1657 | + acc_exp = 1 |
| 1658 | + for name, contents in sorted(fileinfodict.items()): |
| 1659 | + if name.endswith(".json"): |
| 1660 | + name = os.path.splitext(name)[0] |
| 1661 | + cont = acc_contents(name, contents) |
| 1662 | + leaves.append(cont) |
| 1663 | + |
| 1664 | + json_contents = securesystemslib.formats.encode_canonical(contents) |
| 1665 | + prime = hash_to_prime(json_contents) |
| 1666 | + primes.append(prime) |
| 1667 | + acc_exp = acc_exp * prime |
| 1668 | + |
| 1669 | + acc = pow(g, acc_exp, m) |
| 1670 | + |
| 1671 | + proofs = [] |
| 1672 | + for i in range(len(leaves)): |
| 1673 | + proof_exp = acc_exp/primes[i] |
| 1674 | + proof = pow(g, int(proof_exp), m) |
| 1675 | + proofs.append(proof) |
| 1676 | + leaves[i].set_proof(proof) |
| 1677 | + |
| 1678 | + |
| 1679 | + root = acc |
| 1680 | + |
| 1681 | + # Return the root (the total accumulator) and the leaves. The root must be used along with |
| 1682 | + # the proof. The root hash should be securely sent to |
| 1683 | + # each client. To do so, we will add it to the timestamp metadata. |
| 1684 | + # The leaves will be used for verification |
| 1685 | + return root, leaves |
| 1686 | + |
| 1687 | +def _write_rsa_proofs(root, leaves, storage_backend, rsa_acc_directory, version): |
| 1688 | + # The root and leaves must be part of the same fully constructed |
| 1689 | + # RSA accumulator. |
| 1690 | + # The contents and proof will be downloaded by |
| 1691 | + # the client and used for verification. |
| 1692 | + |
| 1693 | + # Before writing each leaf, make sure the storage_backend |
| 1694 | + # is instantiated |
| 1695 | + if storage_backend is None: |
| 1696 | + storage_backend = securesystemslib.storage.FilesystemBackend() |
| 1697 | + |
| 1698 | + for l in leaves: |
| 1699 | + # Write the leaf to the rsa_acc_directory |
| 1700 | + print(l) |
| 1701 | + file_contents = tuf.formats.build_dict_conforming_to_schema( |
| 1702 | + tuf.formats.SNAPSHOT_RSA_ACC_SCHEMA, |
| 1703 | + leaf_contents=l.contents, |
| 1704 | + rsa_acc_proof=str(l.proof)) |
| 1705 | + file_content = _get_written_metadata(file_contents) |
| 1706 | + file_object = tempfile.TemporaryFile() |
| 1707 | + file_object.write(file_content) |
| 1708 | + filename = os.path.join(rsa_acc_directory, l.name + '-snapshot.json') |
| 1709 | + |
| 1710 | + # Also write with consistent snapshots for auditing and client verification |
| 1711 | + consistent_filename = os.path.join(rsa_acc_directory, str(version) + '.' |
| 1712 | + + l.name + '-snapshot.json') |
| 1713 | + securesystemslib.util.persist_temp_file(file_object, consistent_filename, |
| 1714 | + should_close=False) |
| 1715 | + |
| 1716 | + storage_backend.put(file_object, filename) |
| 1717 | + file_object.close() |
| 1718 | + |
| 1719 | + |
| 1720 | + |
| 1721 | + |
1544 | 1722 | def generate_snapshot_metadata(metadata_directory, version, expiration_date, |
1545 | 1723 | storage_backend, consistent_snapshot=False, |
1546 | 1724 | repository_name='default', use_length=False, use_hashes=False): |
@@ -1733,6 +1911,10 @@ def generate_timestamp_metadata(snapshot_file_path, version, expiration_date, |
1733 | 1911 | metadata file in the timestamp metadata. |
1734 | 1912 | Default is True. |
1735 | 1913 |
|
| 1914 | + roleinfo: |
| 1915 | + The roleinfo for the timestamp role. This is used when an RSA |
| 1916 | + accumulator is used. |
| 1917 | +
|
1736 | 1918 | <Exceptions> |
1737 | 1919 | securesystemslib.exceptions.FormatError, if the generated timestamp metadata |
1738 | 1920 | object cannot be formatted correctly, or one of the arguments is improperly |
@@ -1768,6 +1950,15 @@ def generate_timestamp_metadata(snapshot_file_path, version, expiration_date, |
1768 | 1950 | formats.make_metadata_fileinfo(snapshot_version['version'], |
1769 | 1951 | length, hashes) |
1770 | 1952 |
|
| 1953 | + if roleinfo and 'rsa_acc' in roleinfo: |
| 1954 | + rsa_acc = roleinfo['rsa_acc'] |
| 1955 | + return tuf.formats.build_dict_conforming_to_schema( |
| 1956 | + tuf.formats.TIMESTAMP_SCHEMA, |
| 1957 | + version=version, |
| 1958 | + expires=expiration_date, |
| 1959 | + meta=snapshot_fileinfo, |
| 1960 | + rsa_acc=str(rsa_acc)) |
| 1961 | + |
1771 | 1962 | # Generate the timestamp metadata object. |
1772 | 1963 | # Use generalized build_dict_conforming_to_schema func to produce a dict that |
1773 | 1964 | # contains all the appropriate information for timestamp metadata, |
|
0 commit comments