Skip to content

Commit 7ac9f23

Browse files
committed
Port securesystemslib.hash module
securesystemslib.hash is a small wrapper around hashlib, which serves two main purposes: * provide helper function to hash a file * translate custom hash algorithm name "blake2b-256" to "blake2b" with (digest_size=32). In preparation for the removal of securesystemslib.hash, this patch ports above behavior to tuf and uses the builtin hashlib directly where possible. related secure-systems-lab/securesystemslib#943 Signed-off-by: Lukas Puehringer <[email protected]>
1 parent 9f873cb commit 7ac9f23

File tree

3 files changed

+45
-27
lines changed

3 files changed

+45
-27
lines changed

tests/repository_simulator.py

+5-3
Original file line numberDiff line numberDiff line change
@@ -45,14 +45,14 @@
4545
from __future__ import annotations
4646

4747
import datetime
48+
import hashlib
4849
import logging
4950
import os
5051
import tempfile
5152
from dataclasses import dataclass, field
5253
from typing import TYPE_CHECKING
5354
from urllib import parse
5455

55-
import securesystemslib.hash as sslib_hash
5656
from securesystemslib.signer import CryptoSigner, Signer
5757

5858
from tuf.api.exceptions import DownloadHTTPError
@@ -80,6 +80,8 @@
8080

8181
SPEC_VER = ".".join(SPECIFICATION_VERSION)
8282

83+
_DEFAULT_HASH_ALGORITHM = "sha256"
84+
8385

8486
@dataclass
8587
class FetchTracker:
@@ -292,9 +294,9 @@ def _compute_hashes_and_length(
292294
self, role: str
293295
) -> tuple[dict[str, str], int]:
294296
data = self.fetch_metadata(role)
295-
digest_object = sslib_hash.digest(sslib_hash.DEFAULT_HASH_ALGORITHM)
297+
digest_object = hashlib.new(_DEFAULT_HASH_ALGORITHM)
296298
digest_object.update(data)
297-
hashes = {sslib_hash.DEFAULT_HASH_ALGORITHM: digest_object.hexdigest()}
299+
hashes = {_DEFAULT_HASH_ALGORITHM: digest_object.hexdigest()}
298300
return hashes, len(data)
299301

300302
def update_timestamp(self) -> None:

tests/test_api.py

+1-4
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,6 @@
1717
from typing import ClassVar
1818

1919
from securesystemslib import exceptions as sslib_exceptions
20-
from securesystemslib import hash as sslib_hash
2120
from securesystemslib.signer import (
2221
CryptoSigner,
2322
Key,
@@ -958,9 +957,7 @@ def test_targetfile_from_file(self) -> None:
958957
# Test with a non-existing file
959958
file_path = os.path.join(self.repo_dir, Targets.type, "file123.txt")
960959
with self.assertRaises(FileNotFoundError):
961-
TargetFile.from_file(
962-
file_path, file_path, [sslib_hash.DEFAULT_HASH_ALGORITHM]
963-
)
960+
TargetFile.from_file(file_path, file_path, ["sha256"])
964961

965962
# Test with an unsupported algorithm
966963
file_path = os.path.join(self.repo_dir, Targets.type, "file1.txt")

tuf/api/_payload.py

+39-20
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88

99
import abc
1010
import fnmatch
11+
import hashlib
1112
import io
1213
import logging
1314
from dataclasses import dataclass
@@ -21,7 +22,6 @@
2122
)
2223

2324
from securesystemslib import exceptions as sslib_exceptions
24-
from securesystemslib import hash as sslib_hash
2525
from securesystemslib.signer import Key, Signature
2626

2727
from tuf.api.exceptions import LengthOrHashMismatchError, UnsignedMetadataError
@@ -34,6 +34,9 @@
3434
_TARGETS = "targets"
3535
_TIMESTAMP = "timestamp"
3636

37+
_DEFAULT_HASH_ALGORITHM = "sha256"
38+
_BLAKE_HASH_ALGORITHM = "blake2b-256"
39+
3740
# We aim to support SPECIFICATION_VERSION and require the input metadata
3841
# files to have the same major version (the first number) as ours.
3942
SPECIFICATION_VERSION = ["1", "0", "31"]
@@ -45,6 +48,31 @@
4548
T = TypeVar("T", "Root", "Timestamp", "Snapshot", "Targets")
4649

4750

51+
def _hash(algo: str) -> Any: # noqa: ANN401
52+
"""Returns new hash object, supporting custom "blake2b-256" algo name."""
53+
if algo == _BLAKE_HASH_ALGORITHM:
54+
return hashlib.blake2b(digest_size=32)
55+
56+
return hashlib.new(algo)
57+
58+
59+
def _file_hash(f: IO[bytes], algo: str) -> Any: # noqa: ANN401
60+
"""Returns hashed file."""
61+
f.seek(0)
62+
try:
63+
# Only availble on Pythons 3.11+
64+
digest = hashlib.file_digest(f, lambda: _hash(algo)) # type: ignore[arg-type]
65+
66+
except AttributeError:
67+
# Ports `securesystemslib.hash.digest_fileobject` (v1.0), as fallback
68+
# on older Pythons (chunk size is taken from there).
69+
digest = _hash(algo)
70+
for chunk in iter(lambda: f.read(4096), b""):
71+
digest.update(chunk)
72+
73+
return digest
74+
75+
4876
class Signed(metaclass=abc.ABCMeta):
4977
"""A base class for the signed part of TUF metadata.
5078
@@ -664,19 +692,15 @@ def _verify_hashes(
664692
data: bytes | IO[bytes], expected_hashes: dict[str, str]
665693
) -> None:
666694
"""Verify that the hash of ``data`` matches ``expected_hashes``."""
667-
is_bytes = isinstance(data, bytes)
668695
for algo, exp_hash in expected_hashes.items():
669696
try:
670-
if is_bytes:
671-
digest_object = sslib_hash.digest(algo)
697+
if isinstance(data, bytes):
698+
digest_object = _hash(algo)
672699
digest_object.update(data)
673700
else:
674701
# if data is not bytes, assume it is a file object
675-
digest_object = sslib_hash.digest_fileobject(data, algo)
676-
except (
677-
sslib_exceptions.UnsupportedAlgorithmError,
678-
sslib_exceptions.FormatError,
679-
) as e:
702+
digest_object = _file_hash(data, algo)
703+
except (ValueError, TypeError) as e:
680704
raise LengthOrHashMismatchError(
681705
f"Unsupported algorithm '{algo}'"
682706
) from e
@@ -731,21 +755,16 @@ def _get_length_and_hashes(
731755
hashes = {}
732756

733757
if hash_algorithms is None:
734-
hash_algorithms = [sslib_hash.DEFAULT_HASH_ALGORITHM]
758+
hash_algorithms = [_DEFAULT_HASH_ALGORITHM]
735759

736760
for algorithm in hash_algorithms:
737761
try:
738762
if isinstance(data, bytes):
739-
digest_object = sslib_hash.digest(algorithm)
763+
digest_object = _hash(algorithm)
740764
digest_object.update(data)
741765
else:
742-
digest_object = sslib_hash.digest_fileobject(
743-
data, algorithm
744-
)
745-
except (
746-
sslib_exceptions.UnsupportedAlgorithmError,
747-
sslib_exceptions.FormatError,
748-
) as e:
766+
digest_object = _file_hash(data, algorithm)
767+
except (ValueError, TypeError) as e:
749768
raise ValueError(f"Unsupported algorithm '{algorithm}'") from e
750769

751770
hashes[algorithm] = digest_object.hexdigest()
@@ -1150,7 +1169,7 @@ def is_delegated_path(self, target_filepath: str) -> bool:
11501169
if self.path_hash_prefixes is not None:
11511170
# Calculate the hash of the filepath
11521171
# to determine in which bin to find the target.
1153-
digest_object = sslib_hash.digest(algorithm="sha256")
1172+
digest_object = hashlib.new(name="sha256")
11541173
digest_object.update(target_filepath.encode("utf-8"))
11551174
target_filepath_hash = digest_object.hexdigest()
11561175

@@ -1269,7 +1288,7 @@ def get_role_for_target(self, target_filepath: str) -> str:
12691288
target_filepath: URL path to a target file, relative to a base
12701289
targets URL.
12711290
"""
1272-
hasher = sslib_hash.digest(algorithm="sha256")
1291+
hasher = hashlib.new(name="sha256")
12731292
hasher.update(target_filepath.encode("utf-8"))
12741293

12751294
# We can't ever need more than 4 bytes (32 bits).

0 commit comments

Comments
 (0)