29
29
import tempfile
30
30
31
31
from securesystemslib .formats import encode_canonical
32
- from securesystemslib .util import load_json_file , persist_temp_file
32
+ from securesystemslib .util import (
33
+ load_json_file ,
34
+ load_json_string ,
35
+ persist_temp_file
36
+ )
33
37
from securesystemslib .storage import StorageBackendInterface
34
38
from securesystemslib .keys import create_signature , verify_signature
35
39
from tuf .repository_lib import (
@@ -69,12 +73,34 @@ class Metadata():
69
73
]
70
74
71
75
"""
72
- def __init__ (
73
- self , signed : 'Signed' = None , signatures : list = None ) -> None :
74
- # TODO: How much init magic do we want?
76
+ def __init__ (self , signed : 'Signed' , signatures : list ) -> None :
75
77
self .signed = signed
76
78
self .signatures = signatures
77
79
80
+ @classmethod
81
+ def from_dict (cls , metadata : JsonDict ) -> 'Metadata' :
82
+ _type = metadata ['signed' ]['_type' ]
83
+
84
+ if _type == 'targets' :
85
+ signed_cls = Targets
86
+ elif _type == 'snapshot' :
87
+ signed_cls = Snapshot
88
+ elif _type == 'timestamp' :
89
+ signed_cls = Timestamp
90
+ elif _type == 'root' :
91
+ # TODO: implement Root class
92
+ raise NotImplementedError ('Root not yet implemented' )
93
+ else :
94
+ raise ValueError (f'unrecognized metadata type "{ _type } "' )
95
+
96
+ return Metadata (
97
+ signed = signed_cls .from_dict (metadata ['signed' ]),
98
+ signatures = metadata ['signatures' ])
99
+
100
+ def from_json (metadata_json : str ) -> 'Metadata' :
101
+ """Deserialize a json string and pass to from_dict(). """
102
+ return Metadata .from_dict (load_json_string (metadata_json ))
103
+
78
104
79
105
def to_json (self , compact : bool = False ) -> None :
80
106
"""Returns the optionally compacted JSON representation of self. """
@@ -174,27 +200,7 @@ def from_json_file(
174
200
A TUF Metadata object.
175
201
176
202
"""
177
- signable = load_json_file (filename , storage_backend )
178
-
179
- # TODO: Should we use constants?
180
- # And/or maybe a dispatch table? (<-- maybe too much magic)
181
- _type = signable ['signed' ]['_type' ]
182
-
183
- if _type == 'targets' :
184
- inner_cls = Targets
185
- elif _type == 'snapshot' :
186
- inner_cls = Snapshot
187
- elif _type == 'timestamp' :
188
- inner_cls = Timestamp
189
- elif _type == 'root' :
190
- # TODO: implement Root class
191
- raise NotImplementedError ('Root not yet implemented' )
192
- else :
193
- raise ValueError (f'unrecognized metadata type "{ _type } "' )
194
-
195
- return Metadata (
196
- signed = inner_cls (** signable ['signed' ]),
197
- signatures = signable ['signatures' ])
203
+ return Metadata .from_dict (load_json_file (filename , storage_backend ))
198
204
199
205
def to_json_file (
200
206
self , filename : str , compact : bool = False ,
@@ -237,41 +243,42 @@ class Signed:
237
243
# we keep it to match spec terminology (I often refer to this as "payload",
238
244
# or "inner metadata")
239
245
240
- # TODO: Re-think default values. It might be better to pass some things
241
- # as args and not es kwargs. Then we'd need to pop those from
242
- # signable["signed"] in read_from_json and pass them explicitly, which
243
- # some say is better than implicit. :)
244
246
def __init__ (
245
- self , _type : str = None , version : int = 0 ,
246
- spec_version : str = None , expires : datetime = None
247
- ) -> None :
248
- # TODO: How much init magic do we want?
247
+ self , _type : str , version : int , spec_version : str ,
248
+ expires : datetime ) -> None :
249
249
250
250
self ._type = _type
251
+ self .version = version
251
252
self .spec_version = spec_version
253
+ self .expires = expires
252
254
253
- # We always intend times to be UTC
254
- # NOTE: we could do this with datetime.fromisoformat() but that is not
255
- # available in Python 2.7's datetime
256
- # NOTE: Store as datetime object for convenient handling, use 'expires'
257
- # property to get the TUF metadata format representation
258
- self .__expiration = iso8601 .parse_date (expires ).replace (tzinfo = None )
259
-
255
+ # TODO: Should we separate data validation from constructor?
260
256
if version < 0 :
261
257
raise ValueError (f'version must be < 0, got { version } ' )
258
+
262
259
self .version = version
263
260
264
- @property
265
- def signed_bytes (self ) -> bytes :
266
- return encode_canonical (self .as_dict ()).encode ('UTF-8' )
261
+ @classmethod
262
+ def from_dict (cls , signed_dict ) -> 'Signed' :
263
+ # NOTE: Convert 'expires' TUF metadata string representation
264
+ # to datetime object, as the constructor expects it. See 'to_dict' for
265
+ # the inverse operation.
266
+ signed_dict ['expires' ] = iso8601 .parse_date (
267
+ signed_dict ['expires' ]).replace (tzinfo = None )
268
+
269
+ # NOTE: Any additional conversion of dict metadata should happen here
270
+ # or in the corresponding derived class of 'Signed'. E.g. if the
271
+ # delegations field
272
+
273
+ return cls (** signed_dict )
267
274
268
275
@property
269
- def expires (self ) -> str :
270
- return self .__expiration . isoformat () + 'Z'
276
+ def signed_bytes (self ) -> bytes :
277
+ return encode_canonical ( self .to_dict ()). encode ( 'UTF-8' )
271
278
272
279
def bump_expiration (self , delta : timedelta = timedelta (days = 1 )) -> None :
273
280
"""Increments the expires attribute by the passed timedelta. """
274
- self .__expiration = self . __expiration + delta
281
+ self .expires += delta
275
282
276
283
def bump_version (self ) -> None :
277
284
"""Increments the metadata version number by 1."""
@@ -283,7 +290,7 @@ def to_dict(self) -> JsonDict:
283
290
'_type' : self ._type ,
284
291
'version' : self .version ,
285
292
'spec_version' : self .spec_version ,
286
- 'expires' : self .expires
293
+ 'expires' : self .expires . isoformat () + 'Z'
287
294
}
288
295
289
296
class Timestamp (Signed ):
@@ -305,12 +312,17 @@ class Timestamp(Signed):
305
312
}
306
313
307
314
"""
308
- def __init__ (self , meta : JsonDict = None , ** kwargs ) -> None :
309
- super ().__init__ (** kwargs )
310
- # TODO: How much init magic do we want?
311
- # TODO: Is there merit in creating classes for dict fields?
315
+ def __init__ (
316
+ self , _type : str , version : int , spec_version : str ,
317
+ expires : datetime , meta : JsonDict ) -> None :
318
+ super ().__init__ (_type , version , spec_version , expires )
319
+ # TODO: Add class for meta
312
320
self .meta = meta
313
321
322
+ @classmethod
323
+ def from_dict (cls , timestamp_dict ):
324
+ return super ().from_dict (timestamp_dict )
325
+
314
326
def to_dict (self ) -> JsonDict :
315
327
"""Returns the JSON-serializable dictionary representation of self. """
316
328
json_dict = super ().to_dict ()
@@ -354,12 +366,17 @@ class Snapshot(Signed):
354
366
}
355
367
356
368
"""
357
- def __init__ (self , meta : JsonDict = None , ** kwargs ) -> None :
358
- # TODO: How much init magic do we want?
359
- # TODO: Is there merit in creating classes for dict fields?
360
- super ().__init__ (** kwargs )
369
+ def __init__ (
370
+ self , _type : str , version : int , spec_version : str ,
371
+ expires : datetime , meta : JsonDict ) -> None :
372
+ super ().__init__ (_type , version , spec_version , expires )
373
+ # TODO: Add class for meta
361
374
self .meta = meta
362
375
376
+ @classmethod
377
+ def from_dict (cls , snapshot_dict ):
378
+ return super ().from_dict (snapshot_dict )
379
+
363
380
def to_dict (self ) -> JsonDict :
364
381
"""Returns the JSON-serializable dictionary representation of self. """
365
382
json_dict = super ().to_dict ()
@@ -437,14 +454,18 @@ class Targets(Signed):
437
454
438
455
"""
439
456
def __init__ (
440
- self , targets : JsonDict = None , delegations : JsonDict = None ,
441
- ** kwargs ) -> None :
442
- # TODO: How much init magic do we want?
443
- # TODO: Is there merit in creating classes for dict fields?
444
- super (). __init__ ( ** kwargs )
457
+ self , _type : str , version : int , spec_version : str ,
458
+ expires : datetime , targets : JsonDict , delegations : JsonDict
459
+ ) -> None :
460
+ super (). __init__ ( _type , version , spec_version , expires )
461
+ # TODO: Add class for meta
445
462
self .targets = targets
446
463
self .delegations = delegations
447
464
465
+ @classmethod
466
+ def from_dict (cls , targets_dict ):
467
+ return super ().from_dict (targets_dict )
468
+
448
469
def to_dict (self ) -> JsonDict :
449
470
"""Returns the JSON-serializable dictionary representation of self. """
450
471
json_dict = super ().to_dict ()
0 commit comments