From 75c6d07a8f87d7beb92e4304be8da24bf5046aa2 Mon Sep 17 00:00:00 2001 From: yucheng Date: Tue, 27 Oct 2026 14:17:56 -0700 Subject: [PATCH] feat(proxy): refuse FIPS mode when cryptography's OpenSSL is not FIPS The stock cryptography wheel bundles its own non-FIPS OpenSSL, which serves JWT, Fernet and AES-GCM even when Python's OpenSSL enforces FIPS. --- litellm/proxy/common_utils/fips.py | 17 ++++++- tests/unit/proxy/common_utils/test_fips.py | 46 ++++++++++++++----- ...test_proxy_server_endpoints_and_startup.py | 1 + 3 files changed, 52 insertions(+), 12 deletions(-) diff --git a/litellm/proxy/common_utils/fips.py b/litellm/proxy/common_utils/fips.py index 3a8df10b9e5..f57f56d3248 100644 --- a/litellm/proxy/common_utils/fips.py +++ b/litellm/proxy/common_utils/fips.py @@ -30,9 +30,19 @@ def openssl_enforces_fips() -> bool: return False +def cryptography_enforces_fips() -> bool: + """The cryptography library's own OpenSSL runs with FIPS-only defaults.""" + try: + from cryptography.hazmat.bindings._rust import openssl as rust_openssl + + return rust_openssl.is_fips_enabled() + except (ImportError, AttributeError): + return False + + def enforce_fips_mode() -> None: """Refuse to start when LITELLM_FIPS_MODE is not a boolean, or is on but TLS verification is off - or OpenSSL does not enforce FIPS.""" + or Python's or the cryptography library's OpenSSL does not enforce FIPS.""" value: Final = os.getenv(FIPS_MODE_ENV_VAR) if value and str_to_bool(value) is None: raise FipsModeError(f"{FIPS_MODE_ENV_VAR}={value!r} is not a boolean. Set it to true or false, or unset it.") @@ -51,3 +61,8 @@ def enforce_fips_mode() -> None: f"{FIPS_MODE_ENV_VAR} is on but OpenSSL does not enforce FIPS. Run the proxy on an OpenSSL with " f"its FIPS provider active and default_properties = fips=yes, or unset {FIPS_MODE_ENV_VAR}." ) + if not cryptography_enforces_fips(): + raise FipsModeError( + f"{FIPS_MODE_ENV_VAR} is on but the cryptography library's OpenSSL does not enforce FIPS. Build it against " + f"the system OpenSSL (pip install --no-binary cryptography cryptography), or unset {FIPS_MODE_ENV_VAR}." + ) diff --git a/tests/unit/proxy/common_utils/test_fips.py b/tests/unit/proxy/common_utils/test_fips.py index 392db0d9cb4..98e95263dcb 100644 --- a/tests/unit/proxy/common_utils/test_fips.py +++ b/tests/unit/proxy/common_utils/test_fips.py @@ -2,9 +2,16 @@ import _hashlib from contextlib import nullcontext import pytest +from cryptography.hazmat.bindings._rust import openssl as rust_openssl import litellm -from litellm.proxy.common_utils.fips import FipsModeError, enforce_fips_mode, is_fips_mode, openssl_enforces_fips +from litellm.proxy.common_utils.fips import ( + FipsModeError, + cryptography_enforces_fips, + enforce_fips_mode, + is_fips_mode, + openssl_enforces_fips, +) def _fake_openssl(monkeypatch, *, fips_flag: int, md5_refused: bool) -> None: @@ -43,23 +50,36 @@ def test_openssl_without_get_fips_mode_does_not_enforce_fips(monkeypatch): assert openssl_enforces_fips() is False +@pytest.mark.parametrize("fips_enabled", [True, False]) +def test_cryptography_enforces_fips_reads_its_openssl_fips_flag(monkeypatch, fips_enabled): + monkeypatch.setattr(rust_openssl, "is_fips_enabled", lambda: fips_enabled) + assert cryptography_enforces_fips() is fips_enabled + + +def test_cryptography_without_is_fips_enabled_does_not_enforce_fips(monkeypatch): + monkeypatch.delattr(rust_openssl, "is_fips_enabled") + assert cryptography_enforces_fips() is False + + def test_this_runners_openssl_does_not_enforce_fips(): if _hashlib.get_fips_mode() == 1: pytest.skip("this runner's OpenSSL is in FIPS mode") assert openssl_enforces_fips() is False + assert cryptography_enforces_fips() is False @pytest.mark.parametrize( - "fips_mode, ssl_verify_env, ssl_verify_setting, openssl_fips, refusal", + "fips_mode, ssl_verify_env, ssl_verify_setting, openssl_fips, cryptography_fips, refusal", [ - (None, "false", False, False, None), - ("", "false", False, False, None), - ("false", "false", False, False, None), - ("true", None, True, True, None), - ("true", "false", True, True, "TLS certificate verification is disabled"), - ("true", None, False, True, "TLS certificate verification is disabled"), - ("true", None, True, False, "OpenSSL does not enforce FIPS"), - ("enforced", None, True, True, "LITELLM_FIPS_MODE='enforced' is not a boolean"), + (None, "false", False, False, False, None), + ("", "false", False, False, False, None), + ("false", "false", False, False, False, None), + ("true", None, True, True, True, None), + ("true", "false", True, True, True, "TLS certificate verification is disabled"), + ("true", None, False, True, True, "TLS certificate verification is disabled"), + ("true", None, True, False, True, "is on but OpenSSL does not enforce FIPS"), + ("true", None, True, True, False, "cryptography library's OpenSSL does not enforce FIPS"), + ("enforced", None, True, True, True, "LITELLM_FIPS_MODE='enforced' is not a boolean"), ], ids=[ "unset", @@ -69,10 +89,13 @@ def test_this_runners_openssl_does_not_enforce_fips(): "ssl-verify-env-off", "ssl-verify-setting-off", "openssl-not-fips", + "cryptography-not-fips", "not-a-boolean", ], ) -def test_enforce_fips_mode(monkeypatch, fips_mode, ssl_verify_env, ssl_verify_setting, openssl_fips, refusal): +def test_enforce_fips_mode( + monkeypatch, fips_mode, ssl_verify_env, ssl_verify_setting, openssl_fips, cryptography_fips, refusal +): for name, value in (("LITELLM_FIPS_MODE", fips_mode), ("SSL_VERIFY", ssl_verify_env)): if value is None: monkeypatch.delenv(name, raising=False) @@ -80,6 +103,7 @@ def test_enforce_fips_mode(monkeypatch, fips_mode, ssl_verify_env, ssl_verify_se monkeypatch.setenv(name, value) monkeypatch.setattr(litellm, "ssl_verify", ssl_verify_setting) _fake_openssl(monkeypatch, fips_flag=int(openssl_fips), md5_refused=openssl_fips) + monkeypatch.setattr(rust_openssl, "is_fips_enabled", lambda: cryptography_fips) with pytest.raises(FipsModeError, match=refusal) if refusal else nullcontext(): enforce_fips_mode() diff --git a/tests/unit/proxy/test_proxy_server_endpoints_and_startup.py b/tests/unit/proxy/test_proxy_server_endpoints_and_startup.py index 12f0161ab0c..031c772dc44 100644 --- a/tests/unit/proxy/test_proxy_server_endpoints_and_startup.py +++ b/tests/unit/proxy/test_proxy_server_endpoints_and_startup.py @@ -1801,6 +1801,7 @@ async def test_proxy_startup_refuses_fips_mode_when_the_password_migration_fails monkeypatch.delenv("SSL_VERIFY", raising=False) monkeypatch.setenv("LITELLM_FIPS_MODE", "true") monkeypatch.setattr("litellm.proxy.common_utils.fips.openssl_enforces_fips", lambda: True) + monkeypatch.setattr("litellm.proxy.common_utils.fips.cryptography_enforces_fips", lambda: True) monkeypatch.setattr(ProxyStartupEvent, "setup_prisma_client", AsyncMock(return_value=MockPrisma())) scrypt_refused = ValueError("[digital envelope routines] unsupported") monkeypatch.setattr(