diff --git a/examples/repo_example/basic_repo.py b/examples/repo_example/basic_repo.py index 7d0c404e60..8d61ba1a81 100644 --- a/examples/repo_example/basic_repo.py +++ b/examples/repo_example/basic_repo.py @@ -36,7 +36,6 @@ Key, Metadata, MetaFile, - Role, Root, Snapshot, TargetFile, @@ -96,12 +95,7 @@ def _in(days: float) -> datetime: # 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]( - signed=Targets( - version=1, spec_version=SPEC_VERSION, expires=_in(7), targets={} - ), - signatures={}, -) +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 @@ -124,15 +118,7 @@ def _in(days: float) -> datetime: # 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]( - Snapshot( - version=1, - spec_version=SPEC_VERSION, - expires=_in(7), - meta={"targets.json": MetaFile(version=1)}, - ), - {}, -) +roles["snapshot"] = Metadata(Snapshot(expires=_in(7))) # Timestamp (freshness) # --------------------- @@ -146,15 +132,7 @@ def _in(days: float) -> datetime: # 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]( - Timestamp( - version=1, - spec_version=SPEC_VERSION, - expires=_in(1), - snapshot_meta=MetaFile(version=1), - ), - {}, -) +roles["timestamp"] = Metadata(Timestamp(expires=_in(1))) # Root (root of trust) # -------------------- @@ -168,32 +146,19 @@ def _in(days: float) -> datetime: # '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() - -# Create root metadata object -roles["root"] = Metadata[Root]( - signed=Root( - version=1, - spec_version=SPEC_VERSION, - expires=_in(365), - keys={ - key["keyid"]: Key.from_securesystemslib_key(key) - for key in keys.values() - }, - roles={ - role: Role([key["keyid"]], threshold=1) - for role, key in keys.items() - }, - consistent_snapshot=True, - ), - signatures={}, -) + roles["root"].signed.add_key( + name, Key.from_securesystemslib_key(keys[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 diff --git a/examples/repo_example/hashed_bin_delegation.py b/examples/repo_example/hashed_bin_delegation.py index e9ae3e87d0..c8bc3b34b2 100644 --- a/examples/repo_example/hashed_bin_delegation.py +++ b/examples/repo_example/hashed_bin_delegation.py @@ -147,22 +147,11 @@ def find_hash_bin(path: str) -> str: # Create preliminary delegating targets role (bins) and add public key for # delegated targets (bin_n) to key store. Delegation details are update below. -roles["bins"] = Metadata[Targets]( - signed=Targets( - version=1, - spec_version=SPEC_VERSION, - expires=_in(365), - targets={}, - delegations=Delegations( - keys={ - keys["bin-n"]["keyid"]: Key.from_securesystemslib_key( - keys["bin-n"] - ) - }, - roles={}, - ), - ), - signatures={}, +roles["bins"] = Metadata(Targets(expires=_in(365))) +bin_n_key = Key.from_securesystemslib_key(keys["bin-n"]) +roles["bins"].signed.delegations = Delegations( + keys={bin_n_key.keyid: bin_n_key}, + roles={}, ) # The hash bin generator yields an ordered list of incremental hash bin names @@ -185,12 +174,7 @@ def find_hash_bin(path: str) -> str: ) # Create delegated targets roles (bin_n) - roles[bin_n_name] = Metadata[Targets]( - signed=Targets( - version=1, spec_version=SPEC_VERSION, expires=_in(7), targets={} - ), - signatures={}, - ) + roles[bin_n_name] = Metadata(Targets(expires=_in(7))) # Add target file # --------------- diff --git a/tests/generated_data/generate_md.py b/tests/generated_data/generate_md.py index 9cbbe506cf..649e2bab74 100644 --- a/tests/generated_data/generate_md.py +++ b/tests/generated_data/generate_md.py @@ -11,18 +11,7 @@ from securesystemslib.signer import SSlibSigner from tests import utils -from tuf.api.metadata import ( - SPECIFICATION_VERSION, - TOP_LEVEL_ROLE_NAMES, - Key, - Metadata, - MetaFile, - Role, - Root, - Snapshot, - Targets, - Timestamp, -) +from tuf.api.metadata import Key, Metadata, Root, Snapshot, Targets, Timestamp from tuf.api.serialization.json import JSONSerializer # Hardcode keys and expiry time to achieve reproducibility. @@ -61,13 +50,11 @@ expires_str = "2050-01-01T00:00:00Z" EXPIRY = datetime.strptime(expires_str, "%Y-%m-%dT%H:%M:%SZ") -SPEC_VERSION = ".".join(SPECIFICATION_VERSION) OUT_DIR = "generated_data/ed25519_metadata" if not os.path.exists(OUT_DIR): os.mkdir(OUT_DIR) SERIALIZER = JSONSerializer() -ROLES = {role_name: Role([], 1) for role_name in TOP_LEVEL_ROLE_NAMES} def verify_generation(md: Metadata, path: str) -> None: @@ -97,23 +84,15 @@ def generate_all_files( verify: Whether to verify the newly generated files with the local staored. """ - root = Root(1, SPEC_VERSION, EXPIRY, {}, ROLES, True) - root.add_key("root", keys["ed25519_0"]) - root.add_key("timestamp", keys["ed25519_1"]) - root.add_key("snapshot", keys["ed25519_2"]) - root.add_key("targets", keys["ed25519_3"]) - - md_root: Metadata[Root] = Metadata(root, {}) - - timestamp = Timestamp(1, SPEC_VERSION, EXPIRY, MetaFile(1)) - md_timestamp: Metadata[Timestamp] = Metadata(timestamp, {}) - - meta: Dict[str, MetaFile] = {"targets.json": MetaFile(1)} - snapshot = Snapshot(1, SPEC_VERSION, EXPIRY, meta) - md_snapshot: Metadata[Snapshot] = Metadata(snapshot, {}) - - targets = Targets(1, SPEC_VERSION, EXPIRY, {}) - md_targets: Metadata[Targets] = Metadata(targets, {}) + md_root = Metadata(Root(expires=EXPIRY)) + md_timestamp = Metadata(Timestamp(expires=EXPIRY)) + md_snapshot = Metadata(Snapshot(expires=EXPIRY)) + md_targets = Metadata(Targets(expires=EXPIRY)) + + md_root.signed.add_key("root", keys["ed25519_0"]) + md_root.signed.add_key("timestamp", keys["ed25519_1"]) + md_root.signed.add_key("snapshot", keys["ed25519_2"]) + md_root.signed.add_key("targets", keys["ed25519_3"]) for i, md in enumerate([md_root, md_timestamp, md_snapshot, md_targets]): assert isinstance(md, Metadata) diff --git a/tests/repository_simulator.py b/tests/repository_simulator.py index f7d315781f..ae1ad3e6ca 100644 --- a/tests/repository_simulator.py +++ b/tests/repository_simulator.py @@ -65,7 +65,6 @@ Key, Metadata, MetaFile, - Role, Root, Snapshot, TargetFile, @@ -176,26 +175,16 @@ def rotate_keys(self, role: str) -> None: def _initialize(self) -> None: """Setup a minimal valid repository.""" - targets = Targets(1, SPEC_VER, self.safe_expiry, {}, None) - self.md_targets = Metadata(targets, {}) - - meta = {"targets.json": MetaFile(targets.version)} - snapshot = Snapshot(1, SPEC_VER, self.safe_expiry, meta) - self.md_snapshot = Metadata(snapshot, {}) - - snapshot_meta = MetaFile(snapshot.version) - timestamp = Timestamp(1, SPEC_VER, self.safe_expiry, snapshot_meta) - self.md_timestamp = Metadata(timestamp, {}) - - roles = {role_name: Role([], 1) for role_name in TOP_LEVEL_ROLE_NAMES} - root = Root(1, SPEC_VER, self.safe_expiry, {}, roles, True) + self.md_targets = Metadata(Targets(expires=self.safe_expiry)) + self.md_snapshot = Metadata(Snapshot(expires=self.safe_expiry)) + self.md_timestamp = Metadata(Timestamp(expires=self.safe_expiry)) + self.md_root = Metadata(Root(expires=self.safe_expiry)) for role in TOP_LEVEL_ROLE_NAMES: key, signer = self.create_key() - root.add_key(role, key) + self.md_root.signed.add_key(role, key) self.add_signer(role, signer) - self.md_root = Metadata(root, {}) self.publish_root() def publish_root(self) -> None: diff --git a/tuf/api/metadata.py b/tuf/api/metadata.py index 873ab85f22..4e217982ed 100644 --- a/tuf/api/metadata.py +++ b/tuf/api/metadata.py @@ -104,6 +104,14 @@ class Metadata(Generic[T]): ``[Root]`` is not validated at runtime (as pure annotations are not available then). + New Metadata instances can be created from scratch with:: + + one_day = datetime.utcnow() + timedelta(days=1) + timestamp = Metadata(Timestamp(expires=one_day)) + + Apart from ``expires`` all of the arguments to the inner constructors have + reasonable default values for new metadata. + *All parameters named below are not just constructor arguments but also instance attributes.* @@ -112,6 +120,7 @@ class Metadata(Generic[T]): ``Snapshot``, ``Timestamp`` or ``Root``. signatures: Ordered dictionary of keyids to ``Signature`` objects, each signing the canonical serialized representation of ``signed``. + Default is an empty dictionary. unrecognized_fields: Dictionary of all attributes that are not managed by TUF Metadata API. These fields are NOT signed and it's preferable if unrecognized fields are added to the Signed derivative classes. @@ -120,11 +129,11 @@ class Metadata(Generic[T]): def __init__( self, signed: T, - signatures: Dict[str, Signature], + signatures: Optional[Dict[str, Signature]] = None, unrecognized_fields: Optional[Mapping[str, Any]] = None, ): self.signed: T = signed - self.signatures = signatures + self.signatures = signatures if signatures is not None else {} self.unrecognized_fields: Mapping[str, Any] = unrecognized_fields or {} def __eq__(self, other: Any) -> bool: @@ -444,9 +453,11 @@ class Signed(metaclass=abc.ABCMeta): instance attributes.* Args: - version: Metadata version number. - spec_version: Supported TUF specification version number. - expires: Metadata expiry date. + version: Metadata version number. If None, then 1 is assigned. + spec_version: Supported TUF specification version. If None, then the + version currently supported by the library is assigned. + expires: Metadata expiry date. If None, then current date and time is + assigned. unrecognized_fields: Dictionary of all attributes that are not managed by TUF Metadata API @@ -480,11 +491,13 @@ def expires(self, value: datetime) -> None: # or "inner metadata") def __init__( self, - version: int, - spec_version: str, - expires: datetime, - unrecognized_fields: Optional[Mapping[str, Any]] = None, + version: Optional[int], + spec_version: Optional[str], + expires: Optional[datetime], + unrecognized_fields: Optional[Mapping[str, Any]], ): + if spec_version is None: + spec_version = ".".join(SPECIFICATION_VERSION) # Accept semver (X.Y.Z) but also X.Y for legacy compatibility spec_list = spec_version.split(".") if len(spec_list) not in [2, 3] or not all( @@ -497,11 +510,15 @@ def __init__( raise ValueError(f"Unsupported spec_version {spec_version}") self.spec_version = spec_version - self.expires = expires - if version <= 0: + self.expires = expires or datetime.utcnow() + + if version is None: + version = 1 + elif version <= 0: raise ValueError(f"version must be > 0, got {version}") self.version = version + self.unrecognized_fields: Mapping[str, Any] = unrecognized_fields or {} def __eq__(self, other: Any) -> bool: @@ -819,13 +836,17 @@ class Root(Signed): Parameters listed below are also instance attributes. Args: - version: Metadata version number. - spec_version: Supported TUF specification version number. - expires: Metadata expiry date. + version: Metadata version number. Default is 1. + spec_version: Supported TUF specification version. Default is the + version currently supported by the library. + expires: Metadata expiry date. Default is current date and time. keys: Dictionary of keyids to Keys. Defines the keys used in ``roles``. + Default is empty dictionary. roles: Dictionary of role names to Roles. Defines which keys are - required to sign the metadata for a specific role. + required to sign the metadata for a specific role. Default is + a dictionary of top level roles without keys and threshold of 1. consistent_snapshot: ``True`` if repository supports consistent snapshots. + Default is True. unrecognized_fields: Dictionary of all attributes that are not managed by TUF Metadata API @@ -838,20 +859,22 @@ class Root(Signed): # pylint: disable=too-many-arguments def __init__( self, - version: int, - spec_version: str, - expires: datetime, - keys: Dict[str, Key], - roles: Mapping[str, Role], - consistent_snapshot: Optional[bool] = None, + version: Optional[int] = None, + spec_version: Optional[str] = None, + expires: Optional[datetime] = None, + keys: Optional[Dict[str, Key]] = None, + roles: Optional[Mapping[str, Role]] = None, + consistent_snapshot: Optional[bool] = True, unrecognized_fields: Optional[Mapping[str, Any]] = None, ): super().__init__(version, spec_version, expires, unrecognized_fields) self.consistent_snapshot = consistent_snapshot - self.keys = keys - if set(roles) != TOP_LEVEL_ROLE_NAMES: - raise ValueError("Role names must be the top-level metadata roles") + self.keys = keys if keys is not None else {} + if roles is None: + roles = {r: Role([], 1) for r in TOP_LEVEL_ROLE_NAMES} + elif set(roles) != TOP_LEVEL_ROLE_NAMES: + raise ValueError("Role names must be the top-level metadata roles") self.roles = roles def __eq__(self, other: Any) -> bool: @@ -1114,12 +1137,14 @@ class Timestamp(Signed): instance attributes.* Args: - version: Metadata version number. - spec_version: Supported TUF specification version number. - expires: Metadata expiry date. + version: Metadata version number. Default is 1. + spec_version: Supported TUF specification version. Default is the + version currently supported by the library. + expires: Metadata expiry date. Default is current date and time. unrecognized_fields: Dictionary of all attributes that are not managed by TUF Metadata API - snapshot_meta: Meta information for snapshot metadata. + snapshot_meta: Meta information for snapshot metadata. Default is a + MetaFile with version 1. Raises: ValueError: Invalid arguments. @@ -1129,14 +1154,14 @@ class Timestamp(Signed): def __init__( self, - version: int, - spec_version: str, - expires: datetime, - snapshot_meta: MetaFile, + version: Optional[int] = None, + spec_version: Optional[str] = None, + expires: Optional[datetime] = None, + snapshot_meta: Optional[MetaFile] = None, unrecognized_fields: Optional[Mapping[str, Any]] = None, ): super().__init__(version, spec_version, expires, unrecognized_fields) - self.snapshot_meta = snapshot_meta + self.snapshot_meta = snapshot_meta or MetaFile(1) def __eq__(self, other: Any) -> bool: if not isinstance(other, Timestamp): @@ -1175,12 +1200,14 @@ class Snapshot(Signed): instance attributes.* Args: - version: Metadata version number. - spec_version: Supported TUF specification version number. - expires: Metadata expiry date. + version: Metadata version number. Default is 1. + spec_version: Supported TUF specification version. Default is the + version currently supported by the library. + expires: Metadata expiry date. Default is current date and time. unrecognized_fields: Dictionary of all attributes that are not managed by TUF Metadata API - meta: Dictionary of target metadata filenames to ``MetaFile`` objects. + meta: Dictionary of targets filenames to ``MetaFile`` objects. Default + is a dictionary with a Metafile for "snapshot.json" version 1. Raises: ValueError: Invalid arguments. @@ -1190,14 +1217,14 @@ class Snapshot(Signed): def __init__( self, - version: int, - spec_version: str, - expires: datetime, - meta: Dict[str, MetaFile], + version: Optional[int] = None, + spec_version: Optional[str] = None, + expires: Optional[datetime] = None, + meta: Optional[Dict[str, MetaFile]] = None, unrecognized_fields: Optional[Mapping[str, Any]] = None, ): super().__init__(version, spec_version, expires, unrecognized_fields) - self.meta = meta + self.meta = meta if meta is not None else {"targets.json": MetaFile(1)} def __eq__(self, other: Any) -> bool: if not isinstance(other, Snapshot): @@ -1642,12 +1669,14 @@ class Targets(Signed): instance attributes.* Args: - version: Metadata version number. - spec_version: Supported TUF specification version number. - expires: Metadata expiry date. - targets: Dictionary of target filenames to TargetFiles + version: Metadata version number. Default is 1. + spec_version: Supported TUF specification version. Default is the + version currently supported by the library. + expires: Metadata expiry date. Default is current date and time. + targets: Dictionary of target filenames to TargetFiles. Default is an + empty dictionary. delegations: Defines how this Targets delegates responsibility to other - Targets Metadata files. + Targets Metadata files. Default is None. unrecognized_fields: Dictionary of all attributes that are not managed by TUF Metadata API @@ -1660,15 +1689,15 @@ class Targets(Signed): # pylint: disable=too-many-arguments def __init__( self, - version: int, - spec_version: str, - expires: datetime, - targets: Dict[str, TargetFile], + version: Optional[int] = None, + spec_version: Optional[str] = None, + expires: Optional[datetime] = None, + targets: Optional[Dict[str, TargetFile]] = None, delegations: Optional[Delegations] = None, unrecognized_fields: Optional[Mapping[str, Any]] = None, ) -> None: super().__init__(version, spec_version, expires, unrecognized_fields) - self.targets = targets + self.targets = targets if targets is not None else {} self.delegations = delegations def __eq__(self, other: Any) -> bool: