Skip to content

Commit 6732705

Browse files
committed
Add metadata model class and method docstrings
Signed-off-by: Lukas Puehringer <[email protected]>
1 parent 22e8ea9 commit 6732705

File tree

2 files changed

+163
-11
lines changed

2 files changed

+163
-11
lines changed

tests/test_api.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22

33
# Copyright 2020, New York University and the TUF contributors
44
# SPDX-License-Identifier: MIT OR Apache-2.0
5-
""" Unit tests for api/metdata.py
5+
""" Unit tests for api/metadata.py
66
77
Skipped on Python < 3.6.
88

tuf/api/metadata.py

Lines changed: 162 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,6 @@
66
77
TODO:
88
9-
* Add docstrings
10-
119
* Finalize/Document Verify/Sign functions (I am not fully sure about expected
1210
behavior). See
1311
https://github.com/theupdateframework/tuf/pull/1060#issuecomment-660056376
@@ -54,20 +52,42 @@
5452
# Classes.
5553

5654
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+
"""
5776
def __init__(
5877
self, signed: 'Signed' = None, signatures: list = None) -> None:
5978
# TODO: How much init magic do we want?
6079
self.signed = signed
6180
self.signatures = signatures
6281

6382
def as_dict(self) -> JsonDict:
83+
"""Returns the JSON-serializable dictionary representation of self. """
6484
return {
6585
'signatures': self.signatures,
6686
'signed': self.signed.as_dict()
6787
}
6888

6989
def as_json(self, compact: bool = False) -> None:
70-
"""Returns the optionally compacted JSON representation. """
90+
"""Returns the optionally compacted JSON representation of self. """
7191
return json.dumps(
7292
self.as_dict(),
7393
indent=(None if compact else 1),
@@ -124,7 +144,7 @@ def read_from_json(
124144
cls, filename: str,
125145
storage_backend: Optional[StorageBackendInterface] = None
126146
) -> 'Metadata':
127-
"""Loads JSON-formatted TUF metadata from a file storage.
147+
"""Loads JSON-formatted TUF metadata from file storage.
128148
129149
Arguments:
130150
filename: The path to read the file from.
@@ -166,7 +186,7 @@ def read_from_json(
166186
def write_to_json(
167187
self, filename: str, compact: bool = False,
168188
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.
170190
171191
Arguments:
172192
filename: The path to write the file to.
@@ -186,6 +206,21 @@ def write_to_json(
186206

187207

188208
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+
"""
189224
# NOTE: Signed is a stupid name, because this might not be signed yet, but
190225
# we keep it to match spec terminology (I often refer to this as "payload",
191226
# or "inner metadata")
@@ -220,19 +255,18 @@ def signed_bytes(self) -> bytes:
220255

221256
@property
222257
def expires(self) -> str:
223-
"""The expiration property in TUF metadata format."""
224258
return self.__expiration.isoformat() + 'Z'
225259

226260
def bump_expiration(self, delta: timedelta = timedelta(days=1)) -> None:
261+
"""Increments the expires attribute by the passed timedelta. """
227262
self.__expiration = self.__expiration + delta
228263

229264
def bump_version(self) -> None:
265+
"""Increments the metadata version number by 1."""
230266
self.version += 1
231267

232268
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. """
236270
return {
237271
'_type': self._type,
238272
'version': self.version,
@@ -246,7 +280,24 @@ def read_from_json(
246280
storage_backend: Optional[StorageBackendInterface] = None
247281
) -> Metadata:
248282
signable = load_json_file(filename, storage_backend)
283+
"""Loads corresponding JSON-formatted metadata from file storage.
249284
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+
"""
250301
# FIXME: It feels dirty to access signable["signed"]["version"] here in
251302
# order to do this check, and also a bit random (there are likely other
252303
# things to check), but later we don't have the filename anymore. If we
@@ -264,21 +315,40 @@ def read_from_json(
264315

265316

266317
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+
"""
267336
def __init__(self, meta: JsonDict = None, **kwargs) -> None:
268337
super().__init__(**kwargs)
269338
# TODO: How much init magic do we want?
270339
# TODO: Is there merit in creating classes for dict fields?
271340
self.meta = meta
272341

273342
def as_dict(self) -> JsonDict:
343+
"""Returns the JSON-serializable dictionary representation of self. """
274344
json_dict = super().as_dict()
275345
json_dict.update({
276346
'meta': self.meta
277347
})
278348
return json_dict
279349

280-
# Update metadata about the snapshot metadata.
281350
def update(self, version: int, length: int, hashes: JsonDict) -> None:
351+
"""Assigns passed info about snapshot metadata to meta dict. """
282352
self.meta['snapshot.json'] = {
283353
'version': version,
284354
'length': length,
@@ -287,13 +357,39 @@ def update(self, version: int, length: int, hashes: JsonDict) -> None:
287357

288358

289359
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+
"""
290385
def __init__(self, meta: JsonDict = None, **kwargs) -> None:
291386
# TODO: How much init magic do we want?
292387
# TODO: Is there merit in creating classes for dict fields?
293388
super().__init__(**kwargs)
294389
self.meta = meta
295390

296391
def as_dict(self) -> JsonDict:
392+
"""Returns the JSON-serializable dictionary representation of self. """
297393
json_dict = super().as_dict()
298394
json_dict.update({
299395
'meta': self.meta
@@ -304,6 +400,7 @@ def as_dict(self) -> JsonDict:
304400
def update(
305401
self, rolename: str, version: int, length: Optional[int] = None,
306402
hashes: Optional[JsonDict] = None) -> None:
403+
"""Assigns passed (delegated) targets role info to meta dict. """
307404
metadata_fn = f'{rolename}.json'
308405

309406
self.meta[metadata_fn] = {'version': version}
@@ -315,6 +412,59 @@ def update(
315412

316413

317414
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+
"""
318468
def __init__(
319469
self, targets: JsonDict = None, delegations: JsonDict = None,
320470
**kwargs) -> None:
@@ -325,6 +475,7 @@ def __init__(
325475
self.delegations = delegations
326476

327477
def as_dict(self) -> JsonDict:
478+
"""Returns the JSON-serializable dictionary representation of self. """
328479
json_dict = super().as_dict()
329480
json_dict.update({
330481
'targets': self.targets,
@@ -334,4 +485,5 @@ def as_dict(self) -> JsonDict:
334485

335486
# Add or update metadata about the target.
336487
def update(self, filename: str, fileinfo: JsonDict) -> None:
488+
"""Assigns passed target file info to meta dict. """
337489
self.targets[filename] = fileinfo

0 commit comments

Comments
 (0)