From c170b00b406b8a528dea119fdf01c6bbe86b4ad9 Mon Sep 17 00:00:00 2001 From: tommycbird Date: Tue, 26 Aug 2025 12:04:29 -0600 Subject: [PATCH 1/4] Avoid checksum generation when not desired --- pyartifactory/objects/artifact.py | 17 ++++++++++------- 1 file changed, 10 insertions(+), 7 deletions(-) diff --git a/pyartifactory/objects/artifact.py b/pyartifactory/objects/artifact.py index 6a0ede4..3b52711 100644 --- a/pyartifactory/objects/artifact.py +++ b/pyartifactory/objects/artifact.py @@ -102,14 +102,17 @@ def deploy( if properties is not None: properties_param_str = ";".join(f"{k}={value}" for k, values in properties.items() for value in values) route = ";".join(s for s in [artifact_folder.as_posix(), properties_param_str] if s) - artifact_check_sums = Checksums.generate(local_file) - headers = { - "X-Checksum-Sha1": artifact_check_sums.sha1, - "X-Checksum-Sha256": artifact_check_sums.sha256, - "X-Checksum": artifact_check_sums.md5, - } + headers: Dict[str, str] = {} if checksum_enabled: - headers["X-Checksum-Deploy"] = "true" + artifact_check_sums = Checksums.generate(local_file) + headers.update( + { + "X-Checksum-Sha1": artifact_check_sums.sha1, + "X-Checksum-Sha256": artifact_check_sums.sha256, + "X-Checksum": artifact_check_sums.md5, + "X-Checksum-Deploy": "true", + }, + ) try: self._put( route=route, From ac23195106e53b9561ea78bb95c8578c929dbeae Mon Sep 17 00:00:00 2001 From: tommycbird Date: Wed, 10 Sep 2025 15:12:36 -0600 Subject: [PATCH 2/4] Revert "Avoid checksum generation when not desired" This reverts commit 1c4a3131fd46d777d24238c5545c730287bec938. --- pyartifactory/objects/artifact.py | 17 +++++++---------- 1 file changed, 7 insertions(+), 10 deletions(-) diff --git a/pyartifactory/objects/artifact.py b/pyartifactory/objects/artifact.py index 3b52711..6a0ede4 100644 --- a/pyartifactory/objects/artifact.py +++ b/pyartifactory/objects/artifact.py @@ -102,17 +102,14 @@ def deploy( if properties is not None: properties_param_str = ";".join(f"{k}={value}" for k, values in properties.items() for value in values) route = ";".join(s for s in [artifact_folder.as_posix(), properties_param_str] if s) - headers: Dict[str, str] = {} + artifact_check_sums = Checksums.generate(local_file) + headers = { + "X-Checksum-Sha1": artifact_check_sums.sha1, + "X-Checksum-Sha256": artifact_check_sums.sha256, + "X-Checksum": artifact_check_sums.md5, + } if checksum_enabled: - artifact_check_sums = Checksums.generate(local_file) - headers.update( - { - "X-Checksum-Sha1": artifact_check_sums.sha1, - "X-Checksum-Sha256": artifact_check_sums.sha256, - "X-Checksum": artifact_check_sums.md5, - "X-Checksum-Deploy": "true", - }, - ) + headers["X-Checksum-Deploy"] = "true" try: self._put( route=route, From ab37ba58b511732282edb99d7e6b766a01320b60 Mon Sep 17 00:00:00 2001 From: tommycbird Date: Wed, 10 Sep 2025 15:20:03 -0600 Subject: [PATCH 3/4] Updated mapping to pass in usedforsecurity=False --- pyartifactory/models/artifact.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/pyartifactory/models/artifact.py b/pyartifactory/models/artifact.py index effe21b..5509ad3 100644 --- a/pyartifactory/models/artifact.py +++ b/pyartifactory/models/artifact.py @@ -21,7 +21,11 @@ class Checksums(BaseModel): @classmethod def generate(cls, file_: Path) -> Checksums: block_size: int = 65536 - mapping: dict[str, Callable[[], Any]] = {"md5": hashlib.md5, "sha1": hashlib.sha1, "sha256": hashlib.sha256} + mapping: dict[str, Callable[[], Any]] = { + "md5": lambda: hashlib.md5(usedforsecurity=False), + "sha1": lambda: hashlib.sha1(usedforsecurity=False), + "sha256": lambda: hashlib.sha256(usedforsecurity=False), + } results = {} for algorithm, hashing_function in mapping.items(): From 20c6d3824492dcd8c137c1e0cbe1dc5dd35e46cd Mon Sep 17 00:00:00 2001 From: tommycbird Date: Thu, 11 Sep 2025 09:39:03 -0600 Subject: [PATCH 4/4] try catch for security flag, unit tests added --- pyartifactory/models/artifact.py | 22 +++++++++++---- tests/test_artifacts.py | 48 ++++++++++++++++++++++++++++++++ 2 files changed, 64 insertions(+), 6 deletions(-) diff --git a/pyartifactory/models/artifact.py b/pyartifactory/models/artifact.py index 5509ad3..2207af6 100644 --- a/pyartifactory/models/artifact.py +++ b/pyartifactory/models/artifact.py @@ -6,7 +6,7 @@ import hashlib from datetime import datetime from pathlib import Path -from typing import Any, Callable, Dict, List, Literal, Optional, Union +from typing import Callable, Dict, List, Literal, Optional, Union from pydantic import BaseModel @@ -18,18 +18,28 @@ class Checksums(BaseModel): md5: str sha256: str + @staticmethod + def get_hasher(func: Callable[..., hashlib._Hash]) -> hashlib._Hash: + # In Python 3.9+, some hash algorithms (like md5, sha1, sha256) may be disabled in FIPS-compliant systems + # unless 'usedforsecurity=False' is specified. This flag allows the hash function to be used for non-security + # purposes such as checksums, even in FIPS environments. + try: + return func(usedforsecurity=False) + except TypeError: + return func() + @classmethod def generate(cls, file_: Path) -> Checksums: block_size: int = 65536 - mapping: dict[str, Callable[[], Any]] = { - "md5": lambda: hashlib.md5(usedforsecurity=False), - "sha1": lambda: hashlib.sha1(usedforsecurity=False), - "sha256": lambda: hashlib.sha256(usedforsecurity=False), + mapping = { + "md5": hashlib.md5, + "sha1": hashlib.sha1, + "sha256": hashlib.sha256, } results = {} for algorithm, hashing_function in mapping.items(): - hasher = hashing_function() + hasher = cls.get_hasher(hashing_function) with file_.absolute().open("rb") as fd: buf = fd.read(block_size) while len(buf) > 0: diff --git a/tests/test_artifacts.py b/tests/test_artifacts.py index 8143012..6174519 100644 --- a/tests/test_artifacts.py +++ b/tests/test_artifacts.py @@ -612,6 +612,54 @@ def test_checksum_defined_file(file_path: Path, expected_sha1: str, expected_md5 assert result == expected +def test_get_hasher_security_flag(): + calls = {"kwargs": None} + + class Dummy: + def __init__(self): + self._buf = b"" + + def update(self, b): + self._buf += b + + def hexdigest(self): + return "ok" + + def func(**kwargs): + calls["kwargs"] = kwargs + return Dummy() + + hasher = Checksums.get_hasher(func) + assert isinstance(hasher, Dummy) + assert calls["kwargs"] == {"usedforsecurity": False} + + +def test_get_hasher_type_error_thrown(): + calls = {"with_kwargs": 0, "without_kwargs": 0} + + class Dummy: + def __init__(self): + self._buf = b"" + + def update(self, b): + self._buf += b + + def hexdigest(self): + return "ok" + + def func(**kwargs): + if kwargs: + calls["with_kwargs"] += 1 + raise TypeError("unexpected kwarg") + calls["without_kwargs"] += 1 + return Dummy() + + hasher = Checksums.get_hasher(func) + assert isinstance(hasher, Dummy) + assert calls["with_kwargs"] == 1 + assert calls["without_kwargs"] == 1 + + @responses.activate def test_deploy_artifact_with_checksum_success(mocker): responses.add(responses.PUT, f"{URL}/{ARTIFACT_PATH}", status=200)