Skip to content

Commit 500e8b9

Browse files
author
Lukas Pühringer
authored
Merge pull request #2815 from lukpueh/port-sslib-hash
Port securesystemslib.hash module
2 parents c69af99 + 75e83b3 commit 500e8b9

File tree

3 files changed

+72
-35
lines changed

3 files changed

+72
-35
lines changed

tests/repository_simulator.py

Lines changed: 5 additions & 3 deletions
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+
_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(_HASH_ALGORITHM)
296298
digest_object.update(data)
297-
hashes = {sslib_hash.DEFAULT_HASH_ALGORITHM: digest_object.hexdigest()}
299+
hashes = {_HASH_ALGORITHM: digest_object.hexdigest()}
298300
return hashes, len(data)
299301

300302
def update_timestamp(self) -> None:

tests/test_api.py

Lines changed: 17 additions & 4 deletions
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,
@@ -896,6 +895,12 @@ def test_length_and_hash_validation(self) -> None:
896895
# test with data as bytes
897896
snapshot_metafile.verify_length_and_hashes(data)
898897

898+
# test with custom blake algorithm
899+
snapshot_metafile.hashes = {
900+
"blake2b-256": "963a3c31aad8e2a91cfc603fdba12555e48dd0312674ac48cce2c19c243236a1"
901+
}
902+
snapshot_metafile.verify_length_and_hashes(data)
903+
899904
# test exceptions
900905
expected_length = snapshot_metafile.length
901906
snapshot_metafile.length = 2345
@@ -958,9 +963,7 @@ def test_targetfile_from_file(self) -> None:
958963
# Test with a non-existing file
959964
file_path = os.path.join(self.repo_dir, Targets.type, "file123.txt")
960965
with self.assertRaises(FileNotFoundError):
961-
TargetFile.from_file(
962-
file_path, file_path, [sslib_hash.DEFAULT_HASH_ALGORITHM]
963-
)
966+
TargetFile.from_file(file_path, file_path, ["sha256"])
964967

965968
# Test with an unsupported algorithm
966969
file_path = os.path.join(self.repo_dir, Targets.type, "file1.txt")
@@ -990,6 +993,12 @@ def test_targetfile_from_data(self) -> None:
990993
targetfile_from_data = TargetFile.from_data(target_file_path, data)
991994
targetfile_from_data.verify_length_and_hashes(data)
992995

996+
# Test with custom blake hash algorithm
997+
targetfile_from_data = TargetFile.from_data(
998+
target_file_path, data, ["blake2b-256"]
999+
)
1000+
targetfile_from_data.verify_length_and_hashes(data)
1001+
9931002
def test_metafile_from_data(self) -> None:
9941003
data = b"Inline test content"
9951004

@@ -1013,6 +1022,10 @@ def test_metafile_from_data(self) -> None:
10131022
),
10141023
)
10151024

1025+
# Test with custom blake hash algorithm
1026+
metafile = MetaFile.from_data(1, data, ["blake2b-256"])
1027+
metafile.verify_length_and_hashes(data)
1028+
10161029
def test_targetfile_get_prefixed_paths(self) -> None:
10171030
target = TargetFile(100, {"sha256": "abc", "md5": "def"}, "a/b/f.ext")
10181031
self.assertEqual(

tuf/api/_payload.py

Lines changed: 50 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -8,8 +8,10 @@
88

99
import abc
1010
import fnmatch
11+
import hashlib
1112
import io
1213
import logging
14+
import sys
1315
from dataclasses import dataclass
1416
from datetime import datetime, timezone
1517
from typing import (
@@ -21,7 +23,6 @@
2123
)
2224

2325
from securesystemslib import exceptions as sslib_exceptions
24-
from securesystemslib import hash as sslib_hash
2526
from securesystemslib.signer import Key, Signature
2627

2728
from tuf.api.exceptions import LengthOrHashMismatchError, UnsignedMetadataError
@@ -34,6 +35,9 @@
3435
_TARGETS = "targets"
3536
_TIMESTAMP = "timestamp"
3637

38+
_DEFAULT_HASH_ALGORITHM = "sha256"
39+
_BLAKE_HASH_ALGORITHM = "blake2b-256"
40+
3741
# We aim to support SPECIFICATION_VERSION and require the input metadata
3842
# files to have the same major version (the first number) as ours.
3943
SPECIFICATION_VERSION = ["1", "0", "31"]
@@ -45,6 +49,38 @@
4549
T = TypeVar("T", "Root", "Timestamp", "Snapshot", "Targets")
4650

4751

52+
def _get_digest(algo: str) -> Any: # noqa: ANN401
53+
"""New digest helper to support custom "blake2b-256" algo name."""
54+
if algo == _BLAKE_HASH_ALGORITHM:
55+
return hashlib.blake2b(digest_size=32)
56+
57+
return hashlib.new(algo)
58+
59+
60+
def _hash_bytes(data: bytes, algo: str) -> str:
61+
"""Returns hexdigest for data using algo."""
62+
digest = _get_digest(algo)
63+
digest.update(data)
64+
65+
return digest.hexdigest()
66+
67+
68+
def _hash_file(f: IO[bytes], algo: str) -> str:
69+
"""Returns hexdigest for file using algo."""
70+
f.seek(0)
71+
if sys.version_info >= (3, 11):
72+
digest = hashlib.file_digest(f, lambda: _get_digest(algo)) # type: ignore[arg-type]
73+
74+
else:
75+
# Fallback for older Pythons. Chunk size is taken from the previously
76+
# used and now deprecated `securesystemslib.hash.digest_fileobject`.
77+
digest = _get_digest(algo)
78+
for chunk in iter(lambda: f.read(4096), b""):
79+
digest.update(chunk)
80+
81+
return digest.hexdigest()
82+
83+
4884
class Signed(metaclass=abc.ABCMeta):
4985
"""A base class for the signed part of TUF metadata.
5086
@@ -664,24 +700,18 @@ def _verify_hashes(
664700
data: bytes | IO[bytes], expected_hashes: dict[str, str]
665701
) -> None:
666702
"""Verify that the hash of ``data`` matches ``expected_hashes``."""
667-
is_bytes = isinstance(data, bytes)
668703
for algo, exp_hash in expected_hashes.items():
669704
try:
670-
if is_bytes:
671-
digest_object = sslib_hash.digest(algo)
672-
digest_object.update(data)
705+
if isinstance(data, bytes):
706+
observed_hash = _hash_bytes(data, algo)
673707
else:
674708
# 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:
709+
observed_hash = _hash_file(data, algo)
710+
except (ValueError, TypeError) as e:
680711
raise LengthOrHashMismatchError(
681712
f"Unsupported algorithm '{algo}'"
682713
) from e
683714

684-
observed_hash = digest_object.hexdigest()
685715
if observed_hash != exp_hash:
686716
raise LengthOrHashMismatchError(
687717
f"Observed hash {observed_hash} does not match "
@@ -731,25 +761,17 @@ def _get_length_and_hashes(
731761
hashes = {}
732762

733763
if hash_algorithms is None:
734-
hash_algorithms = [sslib_hash.DEFAULT_HASH_ALGORITHM]
764+
hash_algorithms = [_DEFAULT_HASH_ALGORITHM]
735765

736766
for algorithm in hash_algorithms:
737767
try:
738768
if isinstance(data, bytes):
739-
digest_object = sslib_hash.digest(algorithm)
740-
digest_object.update(data)
769+
hashes[algorithm] = _hash_bytes(data, algorithm)
741770
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:
771+
hashes[algorithm] = _hash_file(data, algorithm)
772+
except (ValueError, TypeError) as e:
749773
raise ValueError(f"Unsupported algorithm '{algorithm}'") from e
750774

751-
hashes[algorithm] = digest_object.hexdigest()
752-
753775
return (length, hashes)
754776

755777

@@ -832,7 +854,7 @@ def from_data(
832854
version: Version of the metadata file.
833855
data: Metadata bytes that the metafile represents.
834856
hash_algorithms: Hash algorithms to create the hashes with. If not
835-
specified, the securesystemslib default hash algorithm is used.
857+
specified, "sha256" is used.
836858
837859
Raises:
838860
ValueError: The hash algorithms list contains an unsupported
@@ -1150,7 +1172,7 @@ def is_delegated_path(self, target_filepath: str) -> bool:
11501172
if self.path_hash_prefixes is not None:
11511173
# Calculate the hash of the filepath
11521174
# to determine in which bin to find the target.
1153-
digest_object = sslib_hash.digest(algorithm="sha256")
1175+
digest_object = hashlib.new(name="sha256")
11541176
digest_object.update(target_filepath.encode("utf-8"))
11551177
target_filepath_hash = digest_object.hexdigest()
11561178

@@ -1269,7 +1291,7 @@ def get_role_for_target(self, target_filepath: str) -> str:
12691291
target_filepath: URL path to a target file, relative to a base
12701292
targets URL.
12711293
"""
1272-
hasher = sslib_hash.digest(algorithm="sha256")
1294+
hasher = hashlib.new(name="sha256")
12731295
hasher.update(target_filepath.encode("utf-8"))
12741296

12751297
# We can't ever need more than 4 bytes (32 bits).
@@ -1542,7 +1564,7 @@ def from_file(
15421564
targets URL.
15431565
local_path: Local path to target file content.
15441566
hash_algorithms: Hash algorithms to calculate hashes with. If not
1545-
specified the securesystemslib default hash algorithm is used.
1567+
specified, "sha256" is used.
15461568
15471569
Raises:
15481570
FileNotFoundError: The file doesn't exist.
@@ -1566,7 +1588,7 @@ def from_data(
15661588
targets URL.
15671589
data: Target file content.
15681590
hash_algorithms: Hash algorithms to create the hashes with. If not
1569-
specified the securesystemslib default hash algorithm is used.
1591+
specified, "sha256" is used.
15701592
15711593
Raises:
15721594
ValueError: The hash algorithms list contains an unsupported

0 commit comments

Comments
 (0)