Skip to content

Commit 8710f8d

Browse files
committed
Add hash and length verification
Extend MetaFile and TargetFile classes with methods for length and hash verification. The common functionality is implemented as static methods of the base class while MetaFile and TargetFile implement the user API based on it. Define LengthOrHasheMismathError. Signed-off-by: Teodora Sechkova <[email protected]>
1 parent de78251 commit 8710f8d

File tree

2 files changed

+98
-12
lines changed

2 files changed

+98
-12
lines changed

tuf/api/metadata.py

+96-9
Original file line numberDiff line numberDiff line change
@@ -16,10 +16,23 @@
1616
1717
"""
1818
import abc
19+
import io
1920
import tempfile
2021
from datetime import datetime, timedelta
21-
from typing import Any, ClassVar, Dict, List, Mapping, Optional, Tuple, Type
22+
from typing import (
23+
Any,
24+
BinaryIO,
25+
ClassVar,
26+
Dict,
27+
List,
28+
Mapping,
29+
Optional,
30+
Tuple,
31+
Type,
32+
Union,
33+
)
2234

35+
from securesystemslib import hash as sslib_hash
2336
from securesystemslib import keys as sslib_keys
2437
from securesystemslib.signer import Signature, Signer
2538
from securesystemslib.storage import FilesystemBackend, StorageBackendInterface
@@ -622,7 +635,53 @@ def remove_key(self, role: str, keyid: str) -> None:
622635
del self.keys[keyid]
623636

624637

625-
class MetaFile:
638+
class BaseFile:
639+
"""A base class of MetaFile and TargetFile.
640+
641+
Encapsulates common static methods for length and hash verification.
642+
"""
643+
644+
@staticmethod
645+
def _verify_hashes(
646+
data: Union[bytes, BinaryIO], expected_hashes: Dict[str, str]
647+
) -> None:
648+
"""Verifies that the hash of 'data' matches 'expected_hashes'"""
649+
is_bytes = isinstance(data, bytes)
650+
for algo, exp_hash in expected_hashes.items():
651+
if is_bytes:
652+
digest_object = sslib_hash.digest(algo)
653+
digest_object.update(data)
654+
else:
655+
# if data is not bytes, assume it is a file object
656+
digest_object = sslib_hash.digest_fileobject(data, algo)
657+
658+
observed_hash = digest_object.hexdigest()
659+
if observed_hash != exp_hash:
660+
raise exceptions.LengthOrHashMismatchError(
661+
f"Observed hash {observed_hash} does not match"
662+
f"expected hash {exp_hash}"
663+
)
664+
665+
@staticmethod
666+
def _verify_length(
667+
data: Union[bytes, BinaryIO], expected_length: int
668+
) -> None:
669+
"""Verifies that the length of 'data' matches 'expected_length'"""
670+
if isinstance(data, bytes):
671+
observed_length = len(data)
672+
else:
673+
# if data is not bytes, assume it is a file object
674+
data.seek(0, io.SEEK_END)
675+
observed_length = data.tell()
676+
677+
if observed_length != expected_length:
678+
raise exceptions.LengthOrHashMismatchError(
679+
f"Observed length {observed_length} does not match"
680+
f"expected length {expected_length}"
681+
)
682+
683+
684+
class MetaFile(BaseFile):
626685
"""A container with information about a particular metadata file.
627686
628687
Attributes:
@@ -678,6 +737,22 @@ def to_dict(self) -> Dict[str, Any]:
678737

679738
return res_dict
680739

740+
def verify_length_and_hashes(self, data: Union[bytes, BinaryIO]):
741+
"""Verifies that the length and hashes of "data" match expected
742+
values.
743+
Args:
744+
data: File object or its content in bytes.
745+
Raises:
746+
LengthOrHashMismatchError: Calculated length or hashes do not
747+
match expected values.
748+
"""
749+
if self.length is not None:
750+
self._verify_length(data, self.length)
751+
752+
# Skip the check in case of an empty dictionary too
753+
if self.hashes:
754+
self._verify_hashes(data, self.hashes)
755+
681756

682757
class Timestamp(Signed):
683758
"""A container for the signed part of timestamp metadata.
@@ -905,7 +980,7 @@ def to_dict(self) -> Dict[str, Any]:
905980
}
906981

907982

908-
class TargetFile:
983+
class TargetFile(BaseFile):
909984
"""A container with information about a particular target file.
910985
911986
Attributes:
@@ -923,12 +998,6 @@ class TargetFile:
923998
924999
"""
9251000

926-
@property
927-
def custom(self):
928-
if self.unrecognized_fields is None:
929-
return None
930-
return self.unrecognized_fields.get("custom", None)
931-
9321001
def __init__(
9331002
self,
9341003
length: int,
@@ -939,6 +1008,12 @@ def __init__(
9391008
self.hashes = hashes
9401009
self.unrecognized_fields = unrecognized_fields or {}
9411010

1011+
@property
1012+
def custom(self):
1013+
if self.unrecognized_fields is None:
1014+
return None
1015+
return self.unrecognized_fields.get("custom", None)
1016+
9421017
@classmethod
9431018
def from_dict(cls, target_dict: Dict[str, Any]) -> "TargetFile":
9441019
"""Creates TargetFile object from its dict representation."""
@@ -955,6 +1030,18 @@ def to_dict(self) -> Dict[str, Any]:
9551030
**self.unrecognized_fields,
9561031
}
9571032

1033+
def verify_length_and_hashes(self, data: Union[bytes, BinaryIO]):
1034+
"""Verifies that the length and hashes of "data" match expected
1035+
values.
1036+
Args:
1037+
data: File object or its content in bytes.
1038+
Raises:
1039+
LengthOrHashMismatchError: Calculated length or hashes do not
1040+
match expected values.
1041+
"""
1042+
self._verify_length(data, self.length)
1043+
self._verify_hashes(data, self.hashes)
1044+
9581045

9591046
class Targets(Signed):
9601047
"""A container for the signed part of targets metadata.

tuf/exceptions.py

+2-3
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,8 @@ def __repr__(self):
6565
class UnsupportedAlgorithmError(Error):
6666
"""Indicate an error while trying to identify a user-specified algorithm."""
6767

68+
class LengthOrHashMismatchError(Error):
69+
"""Indicate an error while checking the length and hash values of an object"""
6870

6971
class BadHashError(Error):
7072
"""Indicate an error while checking the value of a hash object."""
@@ -88,9 +90,6 @@ def __repr__(self):
8890
# self.__class__.__name__ + '(' + repr(self.expected_hash) + ', ' +
8991
# repr(self.observed_hash) + ')')
9092

91-
92-
93-
9493
class BadVersionNumberError(Error):
9594
"""Indicate an error for metadata that contains an invalid version number."""
9695

0 commit comments

Comments
 (0)