-
Notifications
You must be signed in to change notification settings - Fork 267
/
basic_repo.py
358 lines (305 loc) · 14.4 KB
/
basic_repo.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
"""
A TUF repository example using the low-level TUF Metadata API.
The example code in this file demonstrates how to *manually* create and
maintain repository metadata using the low-level Metadata API. It implements
similar functionality to that of the deprecated legacy 'repository_tool' and
'repository_lib'. (see ADR-0010 for details about repository library design)
Contents:
* creation of top-level metadata
* target file handling
* consistent snapshots
* key management
* top-level delegation and signing thresholds
* target delegation
* in-band and out-of-band metadata signing
* writing and reading metadata files
* root key rotation
NOTE: Metadata files will be written to a 'tmp*'-directory in CWD.
"""
import os
import tempfile
from datetime import datetime, timedelta
from pathlib import Path
from typing import Any, Dict
from securesystemslib.keys import generate_ed25519_key
from securesystemslib.signer import SSlibKey, SSlibSigner
from tuf.api.metadata import (
SPECIFICATION_VERSION,
DelegatedRole,
Delegations,
Metadata,
MetaFile,
Root,
Snapshot,
TargetFile,
Targets,
Timestamp,
)
from tuf.api.serialization.json import JSONSerializer
def _in(days: float) -> datetime:
"""Adds 'days' to now and returns datetime object w/o microseconds."""
return datetime.utcnow().replace(microsecond=0) + timedelta(days=days)
# Create top-level metadata
# =========================
# Every TUF repository has at least four roles, i.e. the top-level roles
# 'targets', 'snapshot', 'timestamp' and 'root'. Below we will discuss their
# purpose, show how to create the corresponding metadata, and how to use them
# to provide integrity, consistency and freshness for the files TUF aims to
# protect, i.e. target files.
# Common fields
# -------------
# All roles have the same metadata container format, for which the metadata API
# provides a generic 'Metadata' class. This class has two fields, one for
# cryptographic signatures, i.e. 'signatures', and one for the payload over
# which signatures are generated, i.e. 'signed'. The payload must be an
# instance of either 'Targets', 'Snapshot', 'Timestamp' or 'Root' class. Common
# fields in all of these 'Signed' classes are:
#
# spec_version -- The supported TUF specification version number.
# version -- The metadata version number.
# expires -- The metadata expiry date.
#
# The 'version', which is incremented on each metadata change, is used to
# reference metadata from within other metadata, and thus allows for repository
# consistency in addition to protecting against rollback attacks.
#
# The date the metadata 'expires' protects against freeze attacks and allows
# for implicit key revocation. Choosing an appropriate expiration interval
# depends on the volatility of a role and how easy it is to re-sign them.
# Highly volatile roles (timestamp, snapshot, targets), usually have shorter
# expiration intervals, whereas roles that change less and might use offline
# keys (root, delegating targets) may have longer expiration intervals.
SPEC_VERSION = ".".join(SPECIFICATION_VERSION)
# Define containers for role objects and cryptographic keys created below. This
# allows us to sign and write metadata in a batch more easily.
roles: Dict[str, Metadata] = {}
keys: Dict[str, Dict[str, Any]] = {}
# Targets (integrity)
# -------------------
# The targets role guarantees integrity for the files that TUF aims to protect,
# i.e. target files. It does so by listing the relevant target files, along
# with their hash and length.
roles["targets"] = Metadata(Targets(expires=_in(7)))
# For the purpose of this example we use the top-level targets role to protect
# the integrity of this very example script. The metadata entry contains the
# hash and length of this file at the local path. In addition, it specifies the
# 'target path', which a client uses to locate the target file relative to a
# configured mirror base URL.
#
# |----base URL---||-------target path-------|
# e.g. tuf-examples.org/repo_example/basic_repo.py
local_path = Path(__file__).resolve()
target_path = f"{local_path.parts[-2]}/{local_path.parts[-1]}"
target_file_info = TargetFile.from_file(target_path, str(local_path))
roles["targets"].signed.targets[target_path] = target_file_info
# Snapshot (consistency)
# ----------------------
# The snapshot role guarantees consistency of the entire repository. It does so
# by listing all available targets metadata files at their latest version. This
# becomes relevant, when there are multiple targets metadata files in a
# repository and we want to protect the client against mix-and-match attacks.
roles["snapshot"] = Metadata(Snapshot(expires=_in(7)))
# Timestamp (freshness)
# ---------------------
# The timestamp role guarantees freshness of the repository metadata. It does
# so by listing the latest snapshot (which in turn lists all the latest
# targets) metadata. A short expiration interval requires the repository to
# regularly issue new timestamp metadata and thus protects the client against
# freeze attacks.
#
# Note that snapshot and timestamp use the same generic wireline metadata
# format. But given that timestamp metadata always has only one entry in its
# 'meta' field, i.e. for the latest snapshot file, the timestamp object
# provides the shortcut 'snapshot_meta'.
roles["timestamp"] = Metadata(Timestamp(expires=_in(1)))
# Root (root of trust)
# --------------------
# The root role serves as root of trust for all top-level roles, including
# itself. It does so by mapping cryptographic keys to roles, i.e. the keys that
# are authorized to sign any top-level role metadata, and signing thresholds,
# i.e. how many authorized keys are required for a given role (see 'roles'
# field). This is called top-level delegation.
#
# In addition, root provides all public keys to verify these signatures (see
# 'keys' field), and a configuration parameter that describes whether a
# repository uses consistent snapshots (see section 'Persist metadata' below
# for more details).
# Create root metadata object
roles["root"] = Metadata(Root(expires=_in(365)))
# For this example, we generate one 'ed25519' key pair for each top-level role
# using python-tuf's in-house crypto library.
# See https://github.com/secure-systems-lab/securesystemslib for more details
# about key handling, and don't forget to password-encrypt your private keys!
for name in ["targets", "snapshot", "timestamp", "root"]:
keys[name] = generate_ed25519_key()
roles["root"].signed.add_key(
SSlibKey.from_securesystemslib_key(keys[name]), name
)
# NOTE: We only need the public part to populate root, so it is possible to use
# out-of-band mechanisms to generate key pairs and only expose the public part
# to whoever maintains the root role. As a matter of fact, the very purpose of
# signature thresholds is to avoid having private keys all in one place.
# Signature thresholds
# --------------------
# Given the importance of the root role, it is highly recommended to require a
# threshold of multiple keys to sign root metadata. For this example we
# generate another root key (you can pretend it's out-of-band) and increase the
# required signature threshold.
another_root_key = generate_ed25519_key()
roles["root"].signed.add_key(
SSlibKey.from_securesystemslib_key(another_root_key), "root"
)
roles["root"].signed.roles["root"].threshold = 2
# Sign top-level metadata (in-band)
# =================================
# In this example we have access to all top-level signing keys, so we can use
# them to create and add a signature for each role metadata.
for name in ["targets", "snapshot", "timestamp", "root"]:
key = keys[roles[name].signed.type]
signer = SSlibSigner(key)
roles[name].sign(signer)
# Persist metadata (consistent snapshot)
# ======================================
# It is time to publish the first set of metadata for a client to safely
# download the target file that we have registered for this example repository.
#
# For the purpose of this example we will follow the consistent snapshot naming
# convention for all metadata. This means that each metadata file, must be
# prefixed with its version number, except for timestamp. The naming convention
# also affects the target files, but we don't cover this in the example. See
# the TUF specification for more details:
# https://theupdateframework.github.io/specification/latest/#writing-consistent-snapshots
#
# Also note that the TUF specification does not mandate a wireline format. In
# this demo we use a non-compact JSON format and store all metadata in
# temporary directory at CWD for review.
PRETTY = JSONSerializer(compact=False)
TMP_DIR = tempfile.mkdtemp(dir=os.getcwd())
for name in ["root", "targets", "snapshot"]:
filename = f"{roles[name].signed.version}.{roles[name].signed.type}.json"
path = os.path.join(TMP_DIR, filename)
roles[name].to_file(path, serializer=PRETTY)
roles["timestamp"].to_file(
os.path.join(TMP_DIR, "timestamp.json"), serializer=PRETTY
)
# Threshold signing (out-of-band)
# ===============================
# As mentioned above, using signature thresholds usually entails that not all
# signing keys for a given role are in the same place. Let's briefly pretend
# this is the case for the second root key we registered above, and we are now
# on that key owner's computer. All the owner has to do is read the metadata
# file, sign it, and write it back to the same file, and this can be repeated
# until the threshold is satisfied.
root_path = os.path.join(TMP_DIR, "1.root.json")
roles["root"].from_file(root_path)
roles["root"].sign(SSlibSigner(another_root_key), append=True)
roles["root"].to_file(root_path, serializer=PRETTY)
# Targets delegation
# ==================
# Similar to how the root role delegates responsibilities about integrity,
# consistency and freshness to the corresponding top-level roles, a targets
# role may further delegate its responsibility for target files (or a subset
# thereof) to other targets roles. This allows creation of a granular trust
# hierarchy, and further reduces the impact of a single role compromise.
#
# In this example the top-level targets role trusts a new "python-scripts"
# targets role to provide integrity for any target file that ends with ".py".
delegatee_name = "python-scripts"
keys[delegatee_name] = generate_ed25519_key()
# Delegatee
# ---------
# Create a new targets role, akin to how we created top-level targets above, and
# add target file info from above according to the delegatee's responsibility.
roles[delegatee_name] = Metadata[Targets](
signed=Targets(
version=1,
spec_version=SPEC_VERSION,
expires=_in(7),
targets={target_path: target_file_info},
),
signatures={},
)
# Delegator
# ---------
# Akin to top-level delegation, the delegator expresses its trust in the
# delegatee by authorizing a threshold of cryptographic keys to provide
# signatures for the delegatee metadata. It also provides the corresponding
# public key store.
# The delegation info defined by the delegator further requires the provision
# of a unique delegatee name and constraints about the target files the
# delegatee is responsible for, e.g. a list of path patterns. For details about
# all configuration parameters see
# https://theupdateframework.github.io/specification/latest/#delegations
roles["targets"].signed.delegations = Delegations(
keys={
keys[delegatee_name]["keyid"]: SSlibKey.from_securesystemslib_key(
keys[delegatee_name]
)
},
roles={
delegatee_name: DelegatedRole(
name=delegatee_name,
keyids=[keys[delegatee_name]["keyid"]],
threshold=1,
terminating=True,
paths=["*.py"],
),
},
)
# Remove target file info from top-level targets (delegatee is now responsible)
del roles["targets"].signed.targets[target_path]
# Increase expiry (delegators should be less volatile)
roles["targets"].signed.expires = _in(365)
# Snapshot + Timestamp + Sign + Persist
# -------------------------------------
# In order to publish a new consistent set of metadata, we need to update
# dependent roles (snapshot, timestamp) accordingly, bumping versions of all
# changed metadata.
# Bump targets version
roles["targets"].signed.version += 1
# Update snapshot to account for changed and new targets metadata
roles["snapshot"].signed.meta["targets.json"].version = roles[
"targets"
].signed.version
roles["snapshot"].signed.meta[f"{delegatee_name}.json"] = MetaFile(version=1)
roles["snapshot"].signed.version += 1
# Update timestamp to account for changed snapshot metadata
roles["timestamp"].signed.snapshot_meta.version = roles[
"snapshot"
].signed.version
roles["timestamp"].signed.version += 1
# Sign and write metadata for all changed roles, i.e. all but root
for role_name in ["targets", "python-scripts", "snapshot", "timestamp"]:
signer = SSlibSigner(keys[role_name])
roles[role_name].sign(signer)
# Prefix all but timestamp with version number (see consistent snapshot)
filename = f"{role_name}.json"
if role_name != "timestamp":
filename = f"{roles[role_name].signed.version}.{filename}"
roles[role_name].to_file(os.path.join(TMP_DIR, filename), serializer=PRETTY)
# Root key rotation (recover from a compromise / key loss)
# ========================================================
# TUF makes it easy to recover from a key compromise in-band. Given the trust
# hierarchy through top-level and targets delegation you can easily
# replace compromised or lost keys for any role using the delegating role, even
# for the root role.
# However, since root authorizes its own keys, it always has to be signed with
# both the threshold of keys from the previous version and the threshold of
# keys from the new version. This establishes a trusted line of continuity.
#
# In this example we will replace a root key, and sign a new version of root
# with the threshold of old and new keys. Since one of the previous root keys
# remains in place, it can be used to count towards the old and new threshold.
new_root_key = generate_ed25519_key()
roles["root"].signed.revoke_key(keys["root"]["keyid"], "root")
roles["root"].signed.add_key(
SSlibKey.from_securesystemslib_key(new_root_key), "root"
)
roles["root"].signed.version += 1
roles["root"].signatures.clear()
for key in [keys["root"], another_root_key, new_root_key]:
roles["root"].sign(SSlibSigner(key), append=True)
roles["root"].to_file(
os.path.join(TMP_DIR, f"{roles['root'].signed.version}.root.json"),
serializer=PRETTY,
)