diff --git a/litellm/proxy/common_utils/fips.py b/litellm/proxy/common_utils/fips.py index 1f8404fc37c..1a4d7ddeadb 100644 --- a/litellm/proxy/common_utils/fips.py +++ b/litellm/proxy/common_utils/fips.py @@ -23,7 +23,7 @@ def _fips_mode_setting(general_settings: Mapping[str, object]) -> bool | None: def openssl_enforces_fips() -> bool: - """Python's OpenSSL runs with FIPS-only defaults and itself refuses MD5.""" + """Python's OpenSSL runs with FIPS-only defaults and itself refuses scrypt.""" try: import _hashlib @@ -32,7 +32,7 @@ def openssl_enforces_fips() -> bool: except (ImportError, AttributeError, ValueError): return False try: - _hashlib.new("md5", b"", usedforsecurity=True) + _hashlib.scrypt(b"", salt=b"\0" * 16, n=2, r=1, p=1, dklen=16) except ValueError: return True return False diff --git a/tests/unit/proxy/common_utils/test_fips.py b/tests/unit/proxy/common_utils/test_fips.py index a72772df492..0884d0bbbed 100644 --- a/tests/unit/proxy/common_utils/test_fips.py +++ b/tests/unit/proxy/common_utils/test_fips.py @@ -1,7 +1,6 @@ import _hashlib from collections.abc import Mapping from contextlib import nullcontext -from typing import Final from unittest.mock import Mock import pytest @@ -17,30 +16,32 @@ from litellm.proxy.common_utils.fips import ( ) -def _fake_openssl(monkeypatch: pytest.MonkeyPatch, *, fips_flag: int, md5_refused: bool) -> None: - real_new: Final = _hashlib.new - - def new(name: str, data: bytes = b"", *, usedforsecurity: bool = True) -> _hashlib.HASH: - if name == "md5" and md5_refused: - raise _hashlib.UnsupportedDigestmodError("unsupported hash type") - return real_new(name, data, usedforsecurity=usedforsecurity) - +def _fake_openssl(monkeypatch: pytest.MonkeyPatch, *, fips_flag: int, scrypt_refused: bool) -> None: monkeypatch.setattr(_hashlib, "get_fips_mode", lambda: fips_flag) - monkeypatch.setattr(_hashlib, "new", new) + if scrypt_refused: + monkeypatch.setattr(_hashlib, "scrypt", Mock(side_effect=ValueError("[digital envelope routines] unsupported"))) + else: + monkeypatch.setattr(_hashlib, "scrypt", Mock(return_value=b"\0" * 16)) @pytest.mark.parametrize( - "fips_flag, md5_refused, enforces", + "fips_flag, scrypt_refused, enforces", [(1, True, True), (0, True, False), (1, False, False)], - ids=["fips", "fips-flag-off", "md5-allowed"], + ids=["fips", "fips-flag-off", "scrypt-allowed"], ) -def test_openssl_enforces_fips_needs_the_fips_flag_and_a_refused_md5( - monkeypatch: pytest.MonkeyPatch, fips_flag: int, md5_refused: bool, enforces: bool +def test_openssl_enforces_fips_needs_the_fips_flag_and_a_refused_scrypt( + monkeypatch: pytest.MonkeyPatch, fips_flag: int, scrypt_refused: bool, enforces: bool ) -> None: - _fake_openssl(monkeypatch, fips_flag=fips_flag, md5_refused=md5_refused) + _fake_openssl(monkeypatch, fips_flag=fips_flag, scrypt_refused=scrypt_refused) assert openssl_enforces_fips() is enforces +def test_openssl_enforces_fips_while_md5_is_still_served_for_non_security_use(monkeypatch: pytest.MonkeyPatch) -> None: + _fake_openssl(monkeypatch, fips_flag=1, scrypt_refused=True) + monkeypatch.setattr(_hashlib, "new", Mock(return_value=Mock())) + assert openssl_enforces_fips() is True + + def test_openssl_without_get_fips_mode_does_not_enforce_fips(monkeypatch: pytest.MonkeyPatch) -> None: monkeypatch.delattr(_hashlib, "get_fips_mode") assert openssl_enforces_fips() is False @@ -113,7 +114,7 @@ def test_enforce_fips_mode( cryptography_fips: bool, refusal: str | None, ) -> None: - _fake_openssl(monkeypatch, fips_flag=int(openssl_fips), md5_refused=openssl_fips) + _fake_openssl(monkeypatch, fips_flag=int(openssl_fips), scrypt_refused=openssl_fips) monkeypatch.setattr(rust_openssl, "is_fips_enabled", lambda: cryptography_fips) with pytest.raises(FipsModeError, match=refusal) if refusal else nullcontext():