6
6
7
7
TODO:
8
8
9
- * Add docstrings
10
-
11
9
* Finalize/Document Verify/Sign functions (I am not fully sure about expected
12
10
behavior). See
13
11
https://github.com/theupdateframework/tuf/pull/1060#issuecomment-660056376
54
52
# Classes.
55
53
56
54
class Metadata ():
55
+ """A container for signed TUF metadata.
56
+
57
+ Provides methods to (de-)serialize JSON metadata from and to file
58
+ storage, and to create and verify signatures.
59
+
60
+ Attributes:
61
+ signed: A subclass of Signed, which has the actual metadata payload,
62
+ i.e. one of Targets, Snapshot, Timestamp or Root.
63
+
64
+ signatures: A list of signatures over the canonical JSON representation
65
+ of the value of the signed attribute::
66
+
67
+ [
68
+ {
69
+ 'keyid': '<SIGNING KEY KEYID>',
70
+ 'sig':' '<SIGNATURE HEX REPRESENTATION>'
71
+ },
72
+ ...
73
+ ]
74
+
75
+ """
57
76
def __init__ (
58
77
self , signed : 'Signed' = None , signatures : list = None ) -> None :
59
78
# TODO: How much init magic do we want?
60
79
self .signed = signed
61
80
self .signatures = signatures
62
81
63
82
def as_dict (self ) -> JsonDict :
83
+ """Returns the JSON-serializable dictionary representation of self. """
64
84
return {
65
85
'signatures' : self .signatures ,
66
86
'signed' : self .signed .as_dict ()
67
87
}
68
88
69
89
def as_json (self , compact : bool = False ) -> None :
70
- """Returns the optionally compacted JSON representation. """
90
+ """Returns the optionally compacted JSON representation of self . """
71
91
return json .dumps (
72
92
self .as_dict (),
73
93
indent = (None if compact else 1 ),
@@ -124,7 +144,7 @@ def read_from_json(
124
144
cls , filename : str ,
125
145
storage_backend : Optional [StorageBackendInterface ] = None
126
146
) -> 'Metadata' :
127
- """Loads JSON-formatted TUF metadata from a file storage.
147
+ """Loads JSON-formatted TUF metadata from file storage.
128
148
129
149
Arguments:
130
150
filename: The path to read the file from.
@@ -166,7 +186,7 @@ def read_from_json(
166
186
def write_to_json (
167
187
self , filename : str , compact : bool = False ,
168
188
storage_backend : StorageBackendInterface = None ) -> None :
169
- """Writes the JSON representation of the instance to file storage.
189
+ """Writes the JSON representation of self to file storage.
170
190
171
191
Arguments:
172
192
filename: The path to write the file to.
@@ -186,6 +206,21 @@ def write_to_json(
186
206
187
207
188
208
class Signed :
209
+ """A base class for the signed part of TUF metadata.
210
+
211
+ Objects with base class Signed are usually included in a Metablock object
212
+ on the signed attribute. This class provides attributes and methods that
213
+ are common for all TUF metadata types (roles).
214
+
215
+ Attributes:
216
+ _type: The metadata type string.
217
+ version: The metadata version number.
218
+ spec_version: The TUF specification version number (semver) the
219
+ metadata format adheres to.
220
+ expires: The metadata expiration date in 'YYYY-MM-DDTHH:MM:SSZ' format.
221
+ signed_bytes: The UTF-8 encoded canonical JSON representation of self.
222
+
223
+ """
189
224
# NOTE: Signed is a stupid name, because this might not be signed yet, but
190
225
# we keep it to match spec terminology (I often refer to this as "payload",
191
226
# or "inner metadata")
@@ -220,19 +255,18 @@ def signed_bytes(self) -> bytes:
220
255
221
256
@property
222
257
def expires (self ) -> str :
223
- """The expiration property in TUF metadata format."""
224
258
return self .__expiration .isoformat () + 'Z'
225
259
226
260
def bump_expiration (self , delta : timedelta = timedelta (days = 1 )) -> None :
261
+ """Increments the expires attribute by the passed timedelta. """
227
262
self .__expiration = self .__expiration + delta
228
263
229
264
def bump_version (self ) -> None :
265
+ """Increments the metadata version number by 1."""
230
266
self .version += 1
231
267
232
268
def as_dict (self ) -> JsonDict :
233
- # NOTE: The classes should be the single source of truth about metadata
234
- # let's define the dict representation here and not in some dubious
235
- # build_dict_conforming_to_schema
269
+ """Returns the JSON-serializable dictionary representation of self. """
236
270
return {
237
271
'_type' : self ._type ,
238
272
'version' : self .version ,
@@ -246,7 +280,24 @@ def read_from_json(
246
280
storage_backend : Optional [StorageBackendInterface ] = None
247
281
) -> Metadata :
248
282
signable = load_json_file (filename , storage_backend )
283
+ """Loads corresponding JSON-formatted metadata from file storage.
249
284
285
+ Arguments:
286
+ filename: The path to read the file from.
287
+ storage_backend: An object that implements
288
+ securesystemslib.storage.StorageBackendInterface. Per default
289
+ a (local) FilesystemBackend is used.
290
+
291
+ Raises:
292
+ securesystemslib.exceptions.StorageError: The file cannot be read.
293
+ securesystemslib.exceptions.Error, ValueError: The metadata cannot
294
+ be parsed.
295
+
296
+ Returns:
297
+ A TUF Metadata object whose signed attribute contains an object
298
+ of this class.
299
+
300
+ """
250
301
# FIXME: It feels dirty to access signable["signed"]["version"] here in
251
302
# order to do this check, and also a bit random (there are likely other
252
303
# things to check), but later we don't have the filename anymore. If we
@@ -264,21 +315,40 @@ def read_from_json(
264
315
265
316
266
317
class Timestamp (Signed ):
318
+ """A container for the signed part of timestamp metadata.
319
+
320
+ Attributes:
321
+ meta: A dictionary that contains information about snapshot metadata::
322
+
323
+ {
324
+ 'snapshot.json': {
325
+ 'version': <SNAPSHOT METADATA VERSION NUMBER>,
326
+ 'length': <SNAPSHOT METADATA FILE SIZE>, // optional
327
+ 'hashes': {
328
+ '<HASH ALGO 1>': '<SNAPSHOT METADATA FILE HASH 1>',
329
+ '<HASH ALGO 2>': '<SNAPSHOT METADATA FILE HASH 2>',
330
+ ...
331
+ }
332
+ }
333
+ }
334
+
335
+ """
267
336
def __init__ (self , meta : JsonDict = None , ** kwargs ) -> None :
268
337
super ().__init__ (** kwargs )
269
338
# TODO: How much init magic do we want?
270
339
# TODO: Is there merit in creating classes for dict fields?
271
340
self .meta = meta
272
341
273
342
def as_dict (self ) -> JsonDict :
343
+ """Returns the JSON-serializable dictionary representation of self. """
274
344
json_dict = super ().as_dict ()
275
345
json_dict .update ({
276
346
'meta' : self .meta
277
347
})
278
348
return json_dict
279
349
280
- # Update metadata about the snapshot metadata.
281
350
def update (self , version : int , length : int , hashes : JsonDict ) -> None :
351
+ """Assigns passed info about snapshot metadata to meta dict. """
282
352
self .meta ['snapshot.json' ] = {
283
353
'version' : version ,
284
354
'length' : length ,
@@ -287,13 +357,39 @@ def update(self, version: int, length: int, hashes: JsonDict) -> None:
287
357
288
358
289
359
class Snapshot (Signed ):
360
+ """A container for the signed part of snapshot metadata.
361
+
362
+ Attributes:
363
+ meta: A dictionary that contains information about targets metadata::
364
+
365
+ {
366
+ 'targets.json': {
367
+ 'version': <TARGETS METADATA VERSION NUMBER>,
368
+ 'length': <TARGETS METADATA FILE SIZE>, // optional
369
+ 'hashes': {
370
+ '<HASH ALGO 1>': '<TARGETS METADATA FILE HASH 1>',
371
+ '<HASH ALGO 2>': '<TARGETS METADATA FILE HASH 2>',
372
+ ...
373
+ } // optional
374
+ },
375
+ '<DELEGATED TARGETS ROLE 1>.json': {
376
+ ...
377
+ },
378
+ '<DELEGATED TARGETS ROLE 2>.json': {
379
+ ...
380
+ },
381
+ ...
382
+ }
383
+
384
+ """
290
385
def __init__ (self , meta : JsonDict = None , ** kwargs ) -> None :
291
386
# TODO: How much init magic do we want?
292
387
# TODO: Is there merit in creating classes for dict fields?
293
388
super ().__init__ (** kwargs )
294
389
self .meta = meta
295
390
296
391
def as_dict (self ) -> JsonDict :
392
+ """Returns the JSON-serializable dictionary representation of self. """
297
393
json_dict = super ().as_dict ()
298
394
json_dict .update ({
299
395
'meta' : self .meta
@@ -304,6 +400,7 @@ def as_dict(self) -> JsonDict:
304
400
def update (
305
401
self , rolename : str , version : int , length : Optional [int ] = None ,
306
402
hashes : Optional [JsonDict ] = None ) -> None :
403
+ """Assigns passed (delegated) targets role info to meta dict. """
307
404
metadata_fn = f'{ rolename } .json'
308
405
309
406
self .meta [metadata_fn ] = {'version' : version }
@@ -315,6 +412,59 @@ def update(
315
412
316
413
317
414
class Targets (Signed ):
415
+ """A container for the signed part of targets metadata.
416
+
417
+ Attributes:
418
+ targets: A dictionary that contains information about target files::
419
+
420
+ {
421
+ '<TARGET FILE NAME>': {
422
+ 'length': <TARGET FILE SIZE>,
423
+ 'hashes': {
424
+ '<HASH ALGO 1>': '<TARGET FILE HASH 1>',
425
+ '<HASH ALGO 2>': '<TARGETS FILE HASH 2>',
426
+ ...
427
+ },
428
+ 'custom': <CUSTOM OPAQUE DICT> // optional
429
+ },
430
+ ...
431
+ }
432
+
433
+ delegations: A dictionary that contains a list of delegated target
434
+ roles and public key store used to verify their metadata
435
+ signatures::
436
+
437
+ {
438
+ 'keys' : {
439
+ '<KEYID>': {
440
+ 'keytype': '<KEY TYPE>',
441
+ 'scheme': '<KEY SCHEME>',
442
+ 'keyid_hash_algorithms': [
443
+ '<HASH ALGO 1>',
444
+ '<HASH ALGO 2>'
445
+ ...
446
+ ],
447
+ 'keyval': {
448
+ 'public': '<PUBLIC KEY HEX REPRESENTATION>'
449
+ }
450
+ },
451
+ ...
452
+ },
453
+ 'roles': [
454
+ {
455
+ 'name': '<ROLENAME>',
456
+ 'keyids': ['<SIGNING KEY KEYID>', ...],
457
+ 'threshold': <SIGNATURE THRESHOLD>,
458
+ 'terminating': <TERMINATING BOOLEAN>,
459
+ 'path_hash_prefixes': ['<HEX DIGEST>', ... ], // or
460
+ 'paths' : ['PATHPATTERN', ... ],
461
+ },
462
+ ...
463
+ ]
464
+ }
465
+
466
+
467
+ """
318
468
def __init__ (
319
469
self , targets : JsonDict = None , delegations : JsonDict = None ,
320
470
** kwargs ) -> None :
@@ -325,6 +475,7 @@ def __init__(
325
475
self .delegations = delegations
326
476
327
477
def as_dict (self ) -> JsonDict :
478
+ """Returns the JSON-serializable dictionary representation of self. """
328
479
json_dict = super ().as_dict ()
329
480
json_dict .update ({
330
481
'targets' : self .targets ,
@@ -334,4 +485,5 @@ def as_dict(self) -> JsonDict:
334
485
335
486
# Add or update metadata about the target.
336
487
def update (self , filename : str , fileinfo : JsonDict ) -> None :
488
+ """Assigns passed target file info to meta dict. """
337
489
self .targets [filename ] = fileinfo
0 commit comments