def update_delegated_targets(self, data: bytes, role_name: str, delegator_name: str) -> None: """Verifies and loads 'data' as new metadata for target 'role_name'. Args: data: unverified new metadata as bytes role_name: The role name of the new metadata delegator_name: The name of the role delegating to the new metadata Raises: RepositoryError: Metadata failed to load or verify. The actual error type and content will contain more details. """ if self.snapshot is None: raise RuntimeError("Cannot load targets before snapshot") # Targets cannot be loaded if final snapshot is expired or its version # does not match meta version in timestamp self._check_final_snapshot() delegator: Optional[Metadata] = self.get(delegator_name) if delegator is None: raise RuntimeError("Cannot load targets before delegator") logger.debug("Updating %s delegated by %s", role_name, delegator_name) # Verify against the hashes in snapshot, if any meta = self.snapshot.signed.meta.get(f"{role_name}.json") if meta is None: raise exceptions.RepositoryError( f"Snapshot does not contain information for '{role_name}'") try: meta.verify_length_and_hashes(data) except exceptions.LengthOrHashMismatchError as e: raise exceptions.RepositoryError( f"{role_name} length or hashes do not match") from e try: new_delegate = Metadata[Targets].from_bytes(data) except DeserializationError as e: raise exceptions.RepositoryError("Failed to load snapshot") from e if new_delegate.signed.type != "targets": raise exceptions.RepositoryError( f"Expected 'targets', got '{new_delegate.signed.type}'") delegator.verify_delegate(role_name, new_delegate) version = new_delegate.signed.version if version != meta.version: raise exceptions.BadVersionNumberError( f"Expected {role_name} v{meta.version}, got v{version}.") if new_delegate.signed.is_expired(self.reference_time): raise exceptions.ExpiredMetadataError( f"New {role_name} is expired") self._trusted_set[role_name] = new_delegate logger.info("Updated %s v%d", role_name, version)
def update_timestamp(self, data: bytes): """Verifies and loads 'data' as new timestamp metadata. Args: data: unverified new timestamp metadata as bytes Raises: RepositoryError: Metadata failed to load or verify. The actual error type and content will contain more details. """ if not self._root_update_finished: raise RuntimeError("Cannot update timestamp before root") if self.snapshot is not None: raise RuntimeError("Cannot update timestamp after snapshot") try: new_timestamp = Metadata.from_bytes(data) except DeserializationError as e: raise exceptions.RepositoryError("Failed to load timestamp") from e if new_timestamp.signed.type != "timestamp": raise exceptions.RepositoryError( f"Expected 'timestamp', got '{new_timestamp.signed.type}'") self.root.verify_delegate("timestamp", new_timestamp) # If an existing trusted timestamp is updated, # check for a rollback attack if self.timestamp is not None: # Prevent rolling back timestamp version if new_timestamp.signed.version < self.timestamp.signed.version: raise exceptions.ReplayedMetadataError( "timestamp", new_timestamp.signed.version, self.timestamp.signed.version, ) # Prevent rolling back snapshot version if (new_timestamp.signed.meta["snapshot.json"].version < self.timestamp.signed.meta["snapshot.json"].version): raise exceptions.ReplayedMetadataError( "snapshot", new_timestamp.signed.meta["snapshot.json"].version, self.timestamp.signed.meta["snapshot.json"].version, ) if new_timestamp.signed.is_expired(self.reference_time): raise exceptions.ExpiredMetadataError("New timestamp is expired") self._trusted_set["timestamp"] = new_timestamp logger.debug("Updated timestamp")
def _check_final_snapshot(self) -> None: """Raise if snapshot is expired or meta version does not match""" assert self.snapshot is not None # nosec assert self.timestamp is not None # nosec if self.snapshot.signed.is_expired(self.reference_time): raise exceptions.ExpiredMetadataError("snapshot.json is expired") if (self.snapshot.signed.version != self.timestamp.signed.meta["snapshot.json"].version): raise exceptions.BadVersionNumberError( f"Expected snapshot version " f"{self.timestamp.signed.meta['snapshot.json'].version}, " f"got {self.snapshot.signed.version}")
def root_update_finished(self): """Marks root metadata as final and verifies it is not expired Raises: ExpiredMetadataError: The final root metadata is expired. """ if self._root_update_finished: raise RuntimeError("Root update is already finished") if self.root.signed.is_expired(self.reference_time): raise exceptions.ExpiredMetadataError("New root.json is expired") # No need to delete timestamp/snapshot here as specification instructs # for fast-forward attack recovery: timestamp/snapshot can not be # loaded at this point and when loaded later they will be verified # with current root keys. self._root_update_finished = True logger.debug("Verified final root.json")
def update_snapshot(self, data: bytes): """Verifies and loads 'data' as new snapshot metadata. Args: data: unverified new snapshot metadata as bytes Raises: RepositoryError: Metadata failed to load or verify. The actual error type and content will contain more details. """ if self.timestamp is None: raise RuntimeError("Cannot update snapshot before timestamp") if self.targets is not None: raise RuntimeError("Cannot update snapshot after targets") logger.debug("Updating snapshot") meta = self.timestamp.signed.meta["snapshot.json"] # Verify against the hashes in timestamp, if any try: meta.verify_length_and_hashes(data) except exceptions.LengthOrHashMismatchError as e: raise exceptions.RepositoryError( "Snapshot length or hashes do not match") from e try: new_snapshot = Metadata.from_bytes(data) except DeserializationError as e: raise exceptions.RepositoryError("Failed to load snapshot") from e if new_snapshot.signed.type != "snapshot": raise exceptions.RepositoryError( f"Expected 'snapshot', got '{new_snapshot.signed.type}'") self.root.verify_delegate("snapshot", new_snapshot) if (new_snapshot.signed.version != self.timestamp.signed.meta["snapshot.json"].version): raise exceptions.BadVersionNumberError( f"Expected snapshot version " f"{self.timestamp.signed.meta['snapshot.json'].version}, " f"got {new_snapshot.signed.version}") # If an existing trusted snapshot is updated, # check for a rollback attack if self.snapshot is not None: for filename, fileinfo in self.snapshot.signed.meta.items(): new_fileinfo = new_snapshot.signed.meta.get(filename) # Prevent removal of any metadata in meta if new_fileinfo is None: raise exceptions.RepositoryError( f"New snapshot is missing info for '{filename}'") # Prevent rollback of any metadata versions if new_fileinfo.version < fileinfo.version: raise exceptions.BadVersionNumberError( f"Expected {filename} version " f"{new_fileinfo.version}, got {fileinfo.version}.") if new_snapshot.signed.is_expired(self.reference_time): raise exceptions.ExpiredMetadataError("New snapshot is expired") self._trusted_set["snapshot"] = new_snapshot logger.debug("Updated snapshot")
def _check_final_timestamp(self) -> None: """Raise if timestamp is expired""" assert self.timestamp is not None # nosec if self.timestamp.signed.is_expired(self.reference_time): raise exceptions.ExpiredMetadataError("timestamp.json is expired")
def update_timestamp(self, data: bytes) -> None: """Verifies and loads 'data' as new timestamp metadata. Note that an intermediate timestamp is allowed to be expired: TrustedMetadataSet will throw an ExpiredMetadataError in this case but the intermediate timestamp will be loaded. This way a newer timestamp can still be loaded (and the intermediate timestamp will be used for rollback protection). Expired timestamp will prevent loading snapshot metadata. Args: data: unverified new timestamp metadata as bytes Raises: RepositoryError: Metadata failed to load or verify as final timestamp. The actual error type and content will contain more details. """ if self.snapshot is not None: raise RuntimeError("Cannot update timestamp after snapshot") # client workflow 5.3.10: Make sure final root is not expired. if self.root.signed.is_expired(self.reference_time): raise exceptions.ExpiredMetadataError("Final root.json is expired") # No need to check for 5.3.11 (fast forward attack recovery): # timestamp/snapshot can not yet be loaded at this point try: new_timestamp = Metadata[Timestamp].from_bytes(data) except DeserializationError as e: raise exceptions.RepositoryError("Failed to load timestamp") from e if new_timestamp.signed.type != "timestamp": raise exceptions.RepositoryError( f"Expected 'timestamp', got '{new_timestamp.signed.type}'") self.root.verify_delegate("timestamp", new_timestamp) # If an existing trusted timestamp is updated, # check for a rollback attack if self.timestamp is not None: # Prevent rolling back timestamp version if new_timestamp.signed.version < self.timestamp.signed.version: raise exceptions.ReplayedMetadataError( "timestamp", new_timestamp.signed.version, self.timestamp.signed.version, ) # Prevent rolling back snapshot version if (new_timestamp.signed.meta["snapshot.json"].version < self.timestamp.signed.meta["snapshot.json"].version): raise exceptions.ReplayedMetadataError( "snapshot", new_timestamp.signed.meta["snapshot.json"].version, self.timestamp.signed.meta["snapshot.json"].version, ) # expiry not checked to allow old timestamp to be used for rollback # protection of new timestamp: expiry is checked in update_snapshot() self._trusted_set["timestamp"] = new_timestamp logger.info("Updated timestamp v%d", new_timestamp.signed.version) # timestamp is loaded: raise if it is not valid _final_ timestamp self._check_final_timestamp()