Skip to content

Commit e56cd12

Browse files
committed
Add initial poc for RSA Accumulator snapshots: repo side
This commit uses a custom python implementation of Miller-Rabin that we will want to replace with a well-maintained library. It does not include efficient updates to the RSA Accumulator Signed-off-by: Marina Moore <mnm678@gmail.com>
1 parent e242a75 commit e56cd12

5 files changed

Lines changed: 263 additions & 5 deletions

File tree

tests/test_repository_lib.py

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -458,6 +458,40 @@ def test_generate_targets_metadata(self):
458458
False, use_existing_fileinfo=True)
459459

460460

461+
def test_build_rsa_acc(self):
462+
temporary_directory = tempfile.mkdtemp(dir=self.temporary_directory)
463+
storage_backend = securesystemslib.storage.FilesystemBackend()
464+
version = 1
465+
466+
# Test an rsa accumulator with a few nodes to verify the output
467+
468+
test_nodes = {}
469+
test_nodes['file1'] = tuf.formats.make_metadata_fileinfo(5, None, None)
470+
471+
472+
root_1, leaves = repo_lib._build_rsa_acc(test_nodes)
473+
repo_lib._write_rsa_leaves(root_1, leaves, storage_backend,
474+
temporary_directory, version)
475+
476+
# Ensure that the paths are written to the directory
477+
file_path = os.path.join(temporary_directory, 'file1-snapshot.json')
478+
self.assertTrue(os.path.exists(file_path))
479+
480+
file_path = os.path.join(temporary_directory, '1.file1-snapshot.json')
481+
self.assertTrue(os.path.exists(file_path))
482+
483+
self.assertEqual(root_1, 5)
484+
485+
test_nodes = {}
486+
test_nodes['targets'] = tuf.formats.make_metadata_fileinfo(1, None, None)
487+
test_nodes['role1'] = tuf.formats.make_metadata_fileinfo(1, None, None)
488+
test_nodes['role2'] = tuf.formats.make_metadata_fileinfo(1, None, None)
489+
490+
root, leaves = repo_lib._build_rsa_acc(test_nodes)
491+
492+
493+
494+
461495
def _setup_generate_snapshot_metadata_test(self):
462496
# Test normal case.
463497
temporary_directory = tempfile.mkdtemp(dir=self.temporary_directory)

tests/test_repository_tool.py

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -247,6 +247,21 @@ def test_writeall(self):
247247
# Verify that status() does not raise an exception.
248248
repository.status()
249249

250+
# Test writeall with generating a snapshot RSA accumulator
251+
repository.mark_dirty(['role1', 'targets', 'root', 'snapshot', 'timestamp'])
252+
repository.writeall(rsa_acc=True)
253+
254+
# Were the RSA proof snapshots written?
255+
targets_snapshot_filepath = os.path.join(metadata_directory,
256+
'targets-snapshot.json')
257+
targets_snapshot = securesystemslib.util.load_json_file(targets_snapshot_filepath)
258+
tuf.formats.SNAPSHOT_RSA_ACC_SCHEMA.check_match(targets_snapshot)
259+
260+
# Does timestamp have the root hash?
261+
timestamp_filepath = os.path.join(metadata_directory, 'timestamp.json')
262+
timestamp = securesystemslib.util.load_json_file(timestamp_filepath)
263+
timestamp['signed']['rsa_acc']
264+
250265
# Verify that status() does not raise
251266
# 'tuf.exceptions.InsufficientKeysError' if a top-level role
252267
# does not contain a threshold of keys.
@@ -488,7 +503,13 @@ def test_get_filepaths_in_directory(self):
488503
# Construct list of file paths expected, determining absolute paths.
489504
expected_files = []
490505
for filepath in ['1.root.json', 'root.json', 'targets.json',
506+
<<<<<<< HEAD
491507
'snapshot.json', 'timestamp.json', 'role1.json', 'role2.json']:
508+
=======
509+
'snapshot.json', 'timestamp.json', 'role1.json', 'role2.json',
510+
'targets-snapshot.json', 'timestamp-rsa.json',
511+
'role1-snapshot.json', 'role2-snapshot.json']:
512+
>>>>>>> a71659bb (Add initial poc for RSA Accumulator snapshots: repo side)
492513
expected_files.append(os.path.abspath(os.path.join(
493514
'repository_data', 'repository', 'metadata', filepath)))
494515

tuf/formats.py

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -358,6 +358,11 @@
358358
targets = FILEDICT_SCHEMA,
359359
delegations = SCHEMA.Optional(DELEGATIONS_SCHEMA))
360360

361+
SNAPSHOT_RSA_ACC_SCHEMA = SCHEMA.Object(
362+
leaf_contents = SCHEMA.OneOf([VERSIONINFO_SCHEMA,
363+
METADATA_FILEINFO_SCHEMA]),
364+
rsa_acc_proof = SCHEMA.AnyString())
365+
361366
# Snapshot role: indicates the latest versions of all metadata (except
362367
# timestamp).
363368
SNAPSHOT_SCHEMA = SCHEMA.Object(
@@ -375,7 +380,8 @@
375380
spec_version = SPECIFICATION_VERSION_SCHEMA,
376381
version = METADATAVERSION_SCHEMA,
377382
expires = sslib_formats.ISO8601_DATETIME_SCHEMA,
378-
meta = FILEINFODICT_SCHEMA)
383+
meta = FILEINFODICT_SCHEMA,
384+
rsa_acc = SCHEMA.Optional(HASH_SCHEMA))
379385

380386

381387
# project.cfg file: stores information about the project in a json dictionary

tuf/repository_lib.py

Lines changed: 192 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,8 @@
3030
import shutil
3131
import json
3232
import tempfile
33+
from pyblake2 import blake2b
34+
import random
3335

3436
import securesystemslib # pylint: disable=unused-import
3537
from securesystemslib import exceptions as sslib_exceptions
@@ -90,7 +92,7 @@ def _generate_and_write_metadata(rolename, metadata_filename,
9092
increment_version_number=True, repository_name='default',
9193
use_existing_fileinfo=False, use_timestamp_length=True,
9294
use_timestamp_hashes=True, use_snapshot_length=False,
93-
use_snapshot_hashes=False):
95+
use_snapshot_hashes=False, rsa_acc=False):
9496
"""
9597
Non-public function that can generate and write the metadata for the
9698
specified 'rolename'. It also increments the version number of 'rolename' if
@@ -122,6 +124,16 @@ def _generate_and_write_metadata(rolename, metadata_filename,
122124
storage_backend, consistent_snapshot, repository_name,
123125
use_length=use_snapshot_length, use_hashes=use_snapshot_hashes)
124126

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+
125137

126138
_log_warning_if_expires_soon(SNAPSHOT_FILENAME, roleinfo['expires'],
127139
SNAPSHOT_EXPIRES_WARN_SECONDS)
@@ -180,6 +192,9 @@ def _generate_and_write_metadata(rolename, metadata_filename,
180192
else:
181193
logger.debug('Not incrementing ' + repr(rolename) + '\'s version number.')
182194

195+
if rolename == 'snapshot' and rsa_acc:
196+
_write_rsa_proofs(root, leaves, storage_backend, metadata_directory, metadata['version'])
197+
183198
if rolename in roledb.TOP_LEVEL_ROLES and not allow_partially_signed:
184199
# Verify that the top-level 'rolename' is fully signed. Only a delegated
185200
# 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,
15411556

15421557

15431558

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+
15441722
def generate_snapshot_metadata(metadata_directory, version, expiration_date,
15451723
storage_backend, consistent_snapshot=False,
15461724
repository_name='default', use_length=False, use_hashes=False):
@@ -1733,6 +1911,10 @@ def generate_timestamp_metadata(snapshot_file_path, version, expiration_date,
17331911
metadata file in the timestamp metadata.
17341912
Default is True.
17351913
1914+
roleinfo:
1915+
The roleinfo for the timestamp role. This is used when an RSA
1916+
accumulator is used.
1917+
17361918
<Exceptions>
17371919
securesystemslib.exceptions.FormatError, if the generated timestamp metadata
17381920
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,
17681950
formats.make_metadata_fileinfo(snapshot_version['version'],
17691951
length, hashes)
17701952

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+
17711962
# Generate the timestamp metadata object.
17721963
# Use generalized build_dict_conforming_to_schema func to produce a dict that
17731964
# contains all the appropriate information for timestamp metadata,

tuf/repository_tool.py

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -256,7 +256,7 @@ def __init__(self, repository_directory, metadata_directory,
256256

257257

258258

259-
def writeall(self, consistent_snapshot=False, use_existing_fileinfo=False):
259+
def writeall(self, consistent_snapshot=False, use_existing_fileinfo=False, rsa_acc=False):
260260
"""
261261
<Purpose>
262262
Write all the JSON Metadata objects to their corresponding files for
@@ -286,6 +286,10 @@ def writeall(self, consistent_snapshot=False, use_existing_fileinfo=False):
286286
written as-is (True) or whether hashes should be generated (False,
287287
requires access to the targets files on-disk).
288288
289+
rsa_acc:
290+
Whether to generate snapshot rsa accululator metadata in addition to snapshot
291+
metadata.
292+
289293
<Exceptions>
290294
tuf.exceptions.UnsignedMetadataError, if any of the top-level
291295
and delegated roles do not have the minimum threshold of signatures.
@@ -363,7 +367,8 @@ def writeall(self, consistent_snapshot=False, use_existing_fileinfo=False):
363367
consistent_snapshot, filenames,
364368
repository_name=self._repository_name,
365369
use_snapshot_length=self._use_snapshot_length,
366-
use_snapshot_hashes=self._use_snapshot_hashes)
370+
use_snapshot_hashes=self._use_snapshot_hashes,
371+
rsa_acc=rsa_acc)
367372

368373
# Generate the 'timestamp.json' metadata file.
369374
if 'timestamp' in dirty_rolenames:
@@ -372,7 +377,8 @@ def writeall(self, consistent_snapshot=False, use_existing_fileinfo=False):
372377
self._storage_backend, consistent_snapshot,
373378
filenames, repository_name=self._repository_name,
374379
use_timestamp_length=self._use_timestamp_length,
375-
use_timestamp_hashes=self._use_timestamp_hashes)
380+
use_timestamp_hashes=self._use_timestamp_hashes,
381+
rsa_acc=rsa_acc)
376382

377383
roledb.unmark_dirty(dirty_rolenames, self._repository_name)
378384

0 commit comments

Comments
 (0)