Skip to content

Add support to use certificates from string in ssl connection #2048

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 4 commits into from
Mar 14, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions redis/asyncio/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -163,6 +163,7 @@ def __init__(
ssl_certfile: Optional[str] = None,
ssl_cert_reqs: str = "required",
ssl_ca_certs: Optional[str] = None,
ssl_ca_data: Optional[str] = None,
ssl_check_hostname: bool = False,
max_connections: Optional[int] = None,
single_connection_client: bool = False,
Expand Down Expand Up @@ -228,6 +229,7 @@ def __init__(
"ssl_certfile": ssl_certfile,
"ssl_cert_reqs": ssl_cert_reqs,
"ssl_ca_certs": ssl_ca_certs,
"ssl_ca_data": ssl_ca_data,
"ssl_check_hostname": ssl_check_hostname,
}
)
Expand Down
13 changes: 11 additions & 2 deletions redis/asyncio/connection.py
Original file line number Diff line number Diff line change
Expand Up @@ -1026,6 +1026,7 @@ def __init__(
ssl_certfile: Optional[str] = None,
ssl_cert_reqs: str = "required",
ssl_ca_certs: Optional[str] = None,
ssl_ca_data: Optional[str] = None,
ssl_check_hostname: bool = False,
**kwargs,
):
Expand All @@ -1035,6 +1036,7 @@ def __init__(
certfile=ssl_certfile,
cert_reqs=ssl_cert_reqs,
ca_certs=ssl_ca_certs,
ca_data=ssl_ca_data,
check_hostname=ssl_check_hostname,
)

Expand All @@ -1054,6 +1056,10 @@ def cert_reqs(self):
def ca_certs(self):
return self.ssl_context.ca_certs

@property
def ca_data(self):
return self.ssl_context.ca_data

@property
def check_hostname(self):
return self.ssl_context.check_hostname
Expand All @@ -1065,6 +1071,7 @@ class RedisSSLContext:
"certfile",
"cert_reqs",
"ca_certs",
"ca_data",
"context",
"check_hostname",
)
Expand All @@ -1075,6 +1082,7 @@ def __init__(
certfile: Optional[str] = None,
cert_reqs: Optional[str] = None,
ca_certs: Optional[str] = None,
ca_data: Optional[str] = None,
check_hostname: bool = False,
):
self.keyfile = keyfile
Expand All @@ -1093,6 +1101,7 @@ def __init__(
)
self.cert_reqs = CERT_REQS[cert_reqs]
self.ca_certs = ca_certs
self.ca_data = ca_data
self.check_hostname = check_hostname
self.context: Optional[ssl.SSLContext] = None

Expand All @@ -1103,8 +1112,8 @@ def get(self) -> ssl.SSLContext:
context.verify_mode = self.cert_reqs
if self.certfile and self.keyfile:
context.load_cert_chain(certfile=self.certfile, keyfile=self.keyfile)
if self.ca_certs:
context.load_verify_locations(self.ca_certs)
if self.ca_certs or self.ca_data:
context.load_verify_locations(cafile=self.ca_certs, cadata=self.ca_data)
self.context = context
return self.context

Expand Down
2 changes: 2 additions & 0 deletions redis/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -883,6 +883,7 @@ def __init__(
ssl_cert_reqs="required",
ssl_ca_certs=None,
ssl_ca_path=None,
ssl_ca_data=None,
ssl_check_hostname=False,
ssl_password=None,
ssl_validate_ocsp=False,
Expand Down Expand Up @@ -964,6 +965,7 @@ def __init__(
"ssl_certfile": ssl_certfile,
"ssl_cert_reqs": ssl_cert_reqs,
"ssl_ca_certs": ssl_ca_certs,
"ssl_ca_data": ssl_ca_data,
"ssl_check_hostname": ssl_check_hostname,
"ssl_password": ssl_password,
"ssl_ca_path": ssl_ca_path,
Expand Down
1 change: 1 addition & 0 deletions redis/cluster.py
Original file line number Diff line number Diff line change
Expand Up @@ -116,6 +116,7 @@ def fix_server(*args):
"socket_timeout",
"ssl",
"ssl_ca_certs",
"ssl_ca_data",
"ssl_certfile",
"ssl_cert_reqs",
"ssl_keyfile",
Expand Down
13 changes: 11 additions & 2 deletions redis/connection.py
Original file line number Diff line number Diff line change
Expand Up @@ -923,6 +923,7 @@ def __init__(
ssl_certfile=None,
ssl_cert_reqs="required",
ssl_ca_certs=None,
ssl_ca_data=None,
ssl_check_hostname=False,
ssl_ca_path=None,
ssl_password=None,
Expand All @@ -939,6 +940,7 @@ def __init__(
ssl_certfile: Path to an ssl certificate. Defaults to None.
ssl_cert_reqs: The string value for the SSLContext.verify_mode (none, optional, required). Defaults to "required".
ssl_ca_certs: The path to a file of concatenated CA certificates in PEM format. Defaults to None.
ssl_ca_data: Either an ASCII string of one or more PEM-encoded certificates or a bytes-like object of DER-encoded certificates.
ssl_check_hostname: If set, match the hostname during the SSL handshake. Defaults to False.
ssl_ca_path: The path to a directory containing several CA certificates in PEM format. Defaults to None.
ssl_password: Password for unlocking an encrypted private key. Defaults to None.
Expand Down Expand Up @@ -973,6 +975,7 @@ def __init__(
ssl_cert_reqs = CERT_REQS[ssl_cert_reqs]
self.cert_reqs = ssl_cert_reqs
self.ca_certs = ssl_ca_certs
self.ca_data = ssl_ca_data
self.ca_path = ssl_ca_path
self.check_hostname = ssl_check_hostname
self.certificate_password = ssl_password
Expand All @@ -993,8 +996,14 @@ def _connect(self):
keyfile=self.keyfile,
password=self.certificate_password,
)
if self.ca_certs is not None or self.ca_path is not None:
context.load_verify_locations(cafile=self.ca_certs, capath=self.ca_path)
if (
self.ca_certs is not None
or self.ca_path is not None
or self.ca_data is not None
):
context.load_verify_locations(
cafile=self.ca_certs, capath=self.ca_path, cadata=self.ca_data
)
sslsock = context.wrap_socket(sock, server_hostname=self.host)
if self.ssl_validate_ocsp is True and CRYPTOGRAPHY_AVAILABLE is False:
raise RedisError("cryptography is not installed.")
Expand Down
16 changes: 16 additions & 0 deletions tests/test_ssl.py
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,22 @@ def test_validating_self_signed_certificate(self, request):
)
assert r.ping()

def test_validating_self_signed_string_certificate(self, request):
f = open(self.SERVER_CERT)
cert_data = f.read()
ssl_url = request.config.option.redis_ssl_url
p = urlparse(ssl_url)[1].split(":")
r = redis.Redis(
host=p[0],
port=p[1],
ssl=True,
ssl_certfile=self.SERVER_CERT,
ssl_keyfile=self.SERVER_KEY,
ssl_cert_reqs="required",
ssl_ca_data=cert_data,
)
assert r.ping()

def _create_oscp_conn(self, request):
ssl_url = request.config.option.redis_ssl_url
p = urlparse(ssl_url)[1].split(":")
Expand Down