Skip to content

Commit 7dfca80

Browse files
[AAP-50642] memoize pem private key to avoid openssl slowdown (#791)
For 2.6, we've moved to RHEL9 for the base container, which has openssl 3.2 in it. Apparently , its is a known issue that the RSA key validation is more secure and more slow in this version vs. Openssl 1 that RHEL8 uses. We were loading and validating the same jwt private key on every request, at a cost of ~ 38 ms, vs previously this took ~ 3 ms. We noticed this slowdown in the benchmarks and after deep dive/profiling we found this was the difference and memoizing the validated private key saves us from this broad slowdown. See https://issues.redhat.com/browse/AAP-50642 for more details [pyca/cryptography#7236 (comment)](pyca/cryptography#7236 (comment)) <--- report that details Openssl 3 slowdown
1 parent 28ef3a6 commit 7dfca80

File tree

2 files changed

+34
-2
lines changed

2 files changed

+34
-2
lines changed

ansible_base/jwt_consumer/common/util.py

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import logging
22
import time
33
from base64 import b64encode
4+
from functools import lru_cache
45

56
from cryptography.exceptions import InvalidSignature
67
from cryptography.hazmat.primitives import hashes, serialization
@@ -14,8 +15,16 @@
1415
_SHARED_SECRET = 'trusted_proxy'
1516

1617

18+
@lru_cache
19+
def _load_pem_private_key(key: str):
20+
# Loading and validating the private key is more expensive in OpenSSL 3.2 (from RHEL9) than in Openssl 1.1 (from RHEL8)
21+
# For that reason, we will memoize the result of this function, and only re-execute it if the key changes
22+
# This is stored in memory local to the process
23+
return serialization.load_pem_private_key(bytes(key, 'utf-8'), password=None)
24+
25+
1726
def generate_x_trusted_proxy_header(key: str) -> str:
18-
private_key = serialization.load_pem_private_key(bytes(key, 'utf-8'), password=None)
27+
private_key = _load_pem_private_key(key)
1928
timestamp = time.time_ns()
2029
message = f'{_SHARED_SECRET}-{timestamp}'
2130
signature = private_key.sign(bytes(message, 'utf-8'), padding.PSS(mgf=padding.MGF1(hashes.SHA256()), salt_length=padding.PSS.MAX_LENGTH), hashes.SHA256())

test_app/tests/jwt_consumer/common/test_util.py

Lines changed: 24 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33

44
from django.test.utils import override_settings
55

6-
from ansible_base.jwt_consumer.common.util import generate_x_trusted_proxy_header, validate_x_trusted_proxy_header
6+
from ansible_base.jwt_consumer.common.util import _load_pem_private_key, generate_x_trusted_proxy_header, validate_x_trusted_proxy_header
77

88

99
class TestValidateTrustedProxy:
@@ -71,3 +71,26 @@ def test_validate_x_trusted_proxy_header_invalid_signature(self, random_public_k
7171
# 0 is invalid bytes
7272
timestamp, junk = header.split('-')
7373
assert validate_x_trusted_proxy_header(f"{timestamp}-0") is False
74+
75+
def test_generate_x_trusted_proxy_header(self, rsa_keypair, rsa_keypair_factory):
76+
"""
77+
This test ensures that, for the same key, the function is called only once.
78+
Otherwise, the function is not called and the return value is returned from the cache.
79+
"""
80+
_load_pem_private_key.cache_clear()
81+
new_rsa_keypair = rsa_keypair_factory()
82+
83+
# Create a mock private key that has the sign method
84+
mock_private_key = mock.Mock()
85+
mock_private_key.sign.return_value = b'fake_signature'
86+
87+
for keypair in [rsa_keypair, new_rsa_keypair]:
88+
with mock.patch("cryptography.hazmat.primitives.serialization.load_pem_private_key", return_value=mock_private_key) as mock_load_pem:
89+
# Call the function multiple times
90+
generate_x_trusted_proxy_header(keypair.private)
91+
generate_x_trusted_proxy_header(keypair.private)
92+
generate_x_trusted_proxy_header(keypair.private)
93+
94+
# Verify the function is called only once due to caching
95+
assert mock_load_pem.call_count == 1
96+
mock_load_pem.assert_called_with(bytes(keypair.private, 'utf-8'), password=None)

0 commit comments

Comments
 (0)