Skip to content

Commit b1a1ba7

Browse files
committed
Introduce HashManager to work around FIPS
1 parent 2416f5c commit b1a1ba7

File tree

2 files changed

+144
-12
lines changed

2 files changed

+144
-12
lines changed

src/poetry/publishing/uploader.py

Lines changed: 61 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,11 @@
33
import hashlib
44
import io
55

6+
from contextlib import suppress
67
from pathlib import Path
78
from typing import TYPE_CHECKING
89
from typing import Any
10+
from typing import NamedTuple
911

1012
import requests
1113

@@ -48,6 +50,59 @@ def __init__(self, error: ConnectionError | HTTPError | str) -> None:
4850
super().__init__(message)
4951

5052

53+
class Hexdigest(NamedTuple):
54+
md5: str | None
55+
sha256: str | None
56+
blake2_256: str | None
57+
58+
59+
class HashManager:
60+
def __init__(self) -> None:
61+
self._sha2_hasher = hashlib.sha256()
62+
63+
self._md5_hasher = None
64+
with suppress(ValueError):
65+
# FIPS mode disables MD5
66+
self._md5_hasher = hashlib.md5()
67+
68+
self._blake_hasher = None
69+
with suppress(ValueError, TypeError):
70+
# FIPS mode disables blake2
71+
self._blake_hasher = hashlib.blake2b(digest_size=256 // 8)
72+
73+
def _md5_update(self, content: bytes) -> None:
74+
if self._md5_hasher is not None:
75+
self._md5_hasher.update(content)
76+
77+
def _md5_hexdigest(self) -> str | None:
78+
if self._md5_hasher is not None:
79+
return self._md5_hasher.hexdigest()
80+
return None
81+
82+
def _blake_update(self, content: bytes) -> None:
83+
if self._blake_hasher is not None:
84+
self._blake_hasher.update(content)
85+
86+
def _blake_hexdigest(self) -> str | None:
87+
if self._blake_hasher is not None:
88+
return self._blake_hasher.hexdigest()
89+
return None
90+
91+
def hash(self, file: Path) -> None:
92+
with file.open("rb") as fp:
93+
for content in iter(lambda: fp.read(io.DEFAULT_BUFFER_SIZE), b""):
94+
self._md5_update(content)
95+
self._sha2_hasher.update(content)
96+
self._blake_update(content)
97+
98+
def hexdigest(self) -> Hexdigest:
99+
return Hexdigest(
100+
self._md5_hexdigest(),
101+
self._sha2_hasher.hexdigest(),
102+
self._blake_hexdigest(),
103+
)
104+
105+
51106
class Uploader:
52107
def __init__(self, poetry: Poetry, io: IO, dist_dir: Path | None = None) -> None:
53108
self._poetry = poetry
@@ -126,19 +181,13 @@ def post_data(self, file: Path) -> dict[str, Any]:
126181

127182
file_type = self._get_type(file)
128183

129-
blake2_256_hash = hashlib.blake2b(digest_size=256 // 8)
130-
131-
md5_hash = hashlib.md5()
132-
sha256_hash = hashlib.sha256()
133-
with file.open("rb") as fp:
134-
for content in iter(lambda: fp.read(io.DEFAULT_BUFFER_SIZE), b""):
135-
md5_hash.update(content)
136-
sha256_hash.update(content)
137-
blake2_256_hash.update(content)
184+
hash_manager = HashManager()
185+
hash_manager.hash(file)
186+
file_hashes = hash_manager.hexdigest()
138187

139-
md5_digest = md5_hash.hexdigest()
140-
sha2_digest = sha256_hash.hexdigest()
141-
blake2_256_digest = blake2_256_hash.hexdigest()
188+
md5_digest = file_hashes.md5
189+
sha2_digest = file_hashes.sha256
190+
blake2_256_digest = file_hashes.blake2_256
142191

143192
py_version: str | None = None
144193
if file_type == "bdist_wheel":

tests/publishing/test_file_hashes.py

Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
from __future__ import annotations
2+
3+
from typing import TYPE_CHECKING
4+
from typing import Any
5+
6+
import pytest
7+
8+
from poetry.publishing.uploader import HashManager
9+
10+
11+
if TYPE_CHECKING:
12+
from pathlib import Path
13+
14+
from pytest_mock import MockerFixture
15+
16+
from tests.types import FixtureDirGetter
17+
18+
19+
@pytest.fixture
20+
def distributions_dir(fixture_dir: FixtureDirGetter) -> Path:
21+
return fixture_dir("distributions")
22+
23+
24+
@pytest.mark.parametrize(
25+
"file, hashes",
26+
(
27+
(
28+
"demo-0.1.0.tar.gz",
29+
(
30+
"d1912c917363a64e127318655f7d1fe7",
31+
"9fa123ad707a5c6c944743bf3e11a0e80d86cb518d3cf25320866ca3ef43e2ad",
32+
"cb638093d63df647e70b03e963bedc31e021cb088695e29101b69f525e3d5fef",
33+
),
34+
),
35+
(
36+
"demo-0.1.2-py2.py3-none-any.whl",
37+
(
38+
"53b4e10d2bfa81a4206221c4b87843d9",
39+
"55dde4e6828081de7a1e429f33180459c333d9da593db62a3d75a8f5e505dde1",
40+
"b35b9aab064e88fffe42309550ebe425907fb42ccb3b1d173b7d6b7509f38eac",
41+
),
42+
),
43+
),
44+
)
45+
def test_file_hashes_returns_proper_hashes_for_file(
46+
file: str, hashes: tuple[str, ...], distributions_dir: Path
47+
) -> None:
48+
manager = HashManager()
49+
manager.hash(distributions_dir / file)
50+
file_hashes = manager.hexdigest()
51+
assert file_hashes == hashes
52+
53+
54+
def test_file_hashes_returns_none_for_md5_with_fips(
55+
mocker: MockerFixture, distributions_dir: Path
56+
) -> None:
57+
# disable md5
58+
def fips_md5(*args: Any, **kwargs: Any) -> Any:
59+
raise ValueError("Disabled by FIPS")
60+
61+
mocker.patch("hashlib.md5", new=fips_md5)
62+
63+
manager = HashManager()
64+
manager.hash(distributions_dir / "demo-0.1.0.tar.gz")
65+
file_hashes = manager.hexdigest()
66+
67+
assert file_hashes.md5 is None
68+
69+
70+
def test_file_hashes_returns_none_for_blake2_with_fips(
71+
mocker: MockerFixture, distributions_dir: Path
72+
) -> None:
73+
# disable md5
74+
def fips_blake2b(*args: Any, **kwargs: Any) -> Any:
75+
raise ValueError("Disabled by FIPS")
76+
77+
mocker.patch("hashlib.blake2b", new=fips_blake2b)
78+
79+
manager = HashManager()
80+
manager.hash(distributions_dir / "demo-0.1.0.tar.gz")
81+
file_hashes = manager.hexdigest()
82+
83+
assert file_hashes.blake2_256 is None

0 commit comments

Comments
 (0)