/tests/test_ssl.py

https://github.com/pombredanne/redis-py · Python · 225 lines · 183 code · 34 blank · 8 comment · 25 complexity · c148271a9b3a2ca70362b31a62b24c6f MD5 · raw file

  1. import os
  2. import socket
  3. import ssl
  4. from urllib.parse import urlparse
  5. import pytest
  6. import redis
  7. from redis.exceptions import ConnectionError, RedisError
  8. from .conftest import skip_if_cryptography, skip_if_nocryptography
  9. @pytest.mark.ssl
  10. class TestSSL:
  11. """Tests for SSL connections
  12. This relies on the --redis-ssl-url purely for rebuilding the client
  13. and connecting to the appropriate port.
  14. """
  15. ROOT = os.path.join(os.path.dirname(__file__), "..")
  16. CERT_DIR = os.path.abspath(os.path.join(ROOT, "docker", "stunnel", "keys"))
  17. if not os.path.isdir(CERT_DIR): # github actions package validation case
  18. CERT_DIR = os.path.abspath(
  19. os.path.join(ROOT, "..", "docker", "stunnel", "keys")
  20. )
  21. if not os.path.isdir(CERT_DIR):
  22. raise IOError(f"No SSL certificates found. They should be in {CERT_DIR}")
  23. SERVER_CERT = os.path.join(CERT_DIR, "server-cert.pem")
  24. SERVER_KEY = os.path.join(CERT_DIR, "server-key.pem")
  25. def test_ssl_with_invalid_cert(self, request):
  26. ssl_url = request.config.option.redis_ssl_url
  27. sslclient = redis.from_url(ssl_url)
  28. with pytest.raises(ConnectionError) as e:
  29. sslclient.ping()
  30. assert "SSL: CERTIFICATE_VERIFY_FAILED" in str(e)
  31. def test_ssl_connection(self, request):
  32. ssl_url = request.config.option.redis_ssl_url
  33. p = urlparse(ssl_url)[1].split(":")
  34. r = redis.Redis(host=p[0], port=p[1], ssl=True, ssl_cert_reqs="none")
  35. assert r.ping()
  36. def test_ssl_connection_without_ssl(self, request):
  37. ssl_url = request.config.option.redis_ssl_url
  38. p = urlparse(ssl_url)[1].split(":")
  39. r = redis.Redis(host=p[0], port=p[1], ssl=False)
  40. with pytest.raises(ConnectionError) as e:
  41. r.ping()
  42. assert "Connection closed by server" in str(e)
  43. def test_validating_self_signed_certificate(self, request):
  44. ssl_url = request.config.option.redis_ssl_url
  45. p = urlparse(ssl_url)[1].split(":")
  46. r = redis.Redis(
  47. host=p[0],
  48. port=p[1],
  49. ssl=True,
  50. ssl_certfile=self.SERVER_CERT,
  51. ssl_keyfile=self.SERVER_KEY,
  52. ssl_cert_reqs="required",
  53. ssl_ca_certs=self.SERVER_CERT,
  54. )
  55. assert r.ping()
  56. def _create_oscp_conn(self, request):
  57. ssl_url = request.config.option.redis_ssl_url
  58. p = urlparse(ssl_url)[1].split(":")
  59. r = redis.Redis(
  60. host=p[0],
  61. port=p[1],
  62. ssl=True,
  63. ssl_certfile=self.SERVER_CERT,
  64. ssl_keyfile=self.SERVER_KEY,
  65. ssl_cert_reqs="required",
  66. ssl_ca_certs=self.SERVER_CERT,
  67. ssl_validate_ocsp=True,
  68. )
  69. return r
  70. @skip_if_cryptography()
  71. def test_ssl_ocsp_called(self, request):
  72. r = self._create_oscp_conn(request)
  73. with pytest.raises(RedisError) as e:
  74. assert r.ping()
  75. assert "cryptography not installed" in str(e)
  76. @skip_if_nocryptography()
  77. def test_ssl_ocsp_called_withcrypto(self, request):
  78. r = self._create_oscp_conn(request)
  79. with pytest.raises(ConnectionError) as e:
  80. assert r.ping()
  81. assert "No AIA information present in ssl certificate" in str(e)
  82. # rediss://, url based
  83. ssl_url = request.config.option.redis_ssl_url
  84. sslclient = redis.from_url(ssl_url)
  85. with pytest.raises(ConnectionError) as e:
  86. sslclient.ping()
  87. assert "No AIA information present in ssl certificate" in str(e)
  88. @skip_if_nocryptography()
  89. def test_valid_ocsp_cert_http(self):
  90. from redis.ocsp import OCSPVerifier
  91. hostnames = ["github.com", "aws.amazon.com", "ynet.co.il"]
  92. for hostname in hostnames:
  93. context = ssl.create_default_context()
  94. with socket.create_connection((hostname, 443)) as sock:
  95. with context.wrap_socket(sock, server_hostname=hostname) as wrapped:
  96. ocsp = OCSPVerifier(wrapped, hostname, 443)
  97. assert ocsp.is_valid()
  98. @skip_if_nocryptography()
  99. def test_revoked_ocsp_certificate(self):
  100. from redis.ocsp import OCSPVerifier
  101. context = ssl.create_default_context()
  102. hostname = "revoked.badssl.com"
  103. with socket.create_connection((hostname, 443)) as sock:
  104. with context.wrap_socket(sock, server_hostname=hostname) as wrapped:
  105. ocsp = OCSPVerifier(wrapped, hostname, 443)
  106. with pytest.raises(ConnectionError) as e:
  107. assert ocsp.is_valid()
  108. assert "REVOKED" in str(e)
  109. @skip_if_nocryptography()
  110. def test_unauthorized_ocsp(self):
  111. from redis.ocsp import OCSPVerifier
  112. context = ssl.create_default_context()
  113. hostname = "stackoverflow.com"
  114. with socket.create_connection((hostname, 443)) as sock:
  115. with context.wrap_socket(sock, server_hostname=hostname) as wrapped:
  116. ocsp = OCSPVerifier(wrapped, hostname, 443)
  117. with pytest.raises(ConnectionError):
  118. ocsp.is_valid()
  119. @skip_if_nocryptography()
  120. def test_ocsp_not_present_in_response(self):
  121. from redis.ocsp import OCSPVerifier
  122. context = ssl.create_default_context()
  123. hostname = "google.co.il"
  124. with socket.create_connection((hostname, 443)) as sock:
  125. with context.wrap_socket(sock, server_hostname=hostname) as wrapped:
  126. ocsp = OCSPVerifier(wrapped, hostname, 443)
  127. with pytest.raises(ConnectionError) as e:
  128. assert ocsp.is_valid()
  129. assert "from the" in str(e)
  130. @skip_if_nocryptography()
  131. def test_unauthorized_then_direct(self):
  132. from redis.ocsp import OCSPVerifier
  133. # these certificates on the socket end return unauthorized
  134. # then the second call succeeds
  135. hostnames = ["wikipedia.org", "squarespace.com"]
  136. for hostname in hostnames:
  137. context = ssl.create_default_context()
  138. with socket.create_connection((hostname, 443)) as sock:
  139. with context.wrap_socket(sock, server_hostname=hostname) as wrapped:
  140. ocsp = OCSPVerifier(wrapped, hostname, 443)
  141. assert ocsp.is_valid()
  142. @skip_if_nocryptography()
  143. def test_mock_ocsp_staple(self, request):
  144. import OpenSSL
  145. ssl_url = request.config.option.redis_ssl_url
  146. p = urlparse(ssl_url)[1].split(":")
  147. r = redis.Redis(
  148. host=p[0],
  149. port=p[1],
  150. ssl=True,
  151. ssl_certfile=self.SERVER_CERT,
  152. ssl_keyfile=self.SERVER_KEY,
  153. ssl_cert_reqs="required",
  154. ssl_ca_certs=self.SERVER_CERT,
  155. ssl_validate_ocsp=True,
  156. ssl_ocsp_context=p, # just needs to not be none
  157. )
  158. with pytest.raises(RedisError):
  159. r.ping()
  160. ctx = OpenSSL.SSL.Context(OpenSSL.SSL.SSLv23_METHOD)
  161. ctx.use_certificate_file(self.SERVER_CERT)
  162. ctx.use_privatekey_file(self.SERVER_KEY)
  163. r = redis.Redis(
  164. host=p[0],
  165. port=p[1],
  166. ssl=True,
  167. ssl_certfile=self.SERVER_CERT,
  168. ssl_keyfile=self.SERVER_KEY,
  169. ssl_cert_reqs="required",
  170. ssl_ca_certs=self.SERVER_CERT,
  171. ssl_ocsp_context=ctx,
  172. ssl_ocsp_expected_cert=open(self.SERVER_KEY, "rb").read(),
  173. ssl_validate_ocsp_stapled=True,
  174. )
  175. with pytest.raises(ConnectionError) as e:
  176. r.ping()
  177. assert "no ocsp response present" in str(e)
  178. r = redis.Redis(
  179. host=p[0],
  180. port=p[1],
  181. ssl=True,
  182. ssl_certfile=self.SERVER_CERT,
  183. ssl_keyfile=self.SERVER_KEY,
  184. ssl_cert_reqs="required",
  185. ssl_ca_certs=self.SERVER_CERT,
  186. ssl_validate_ocsp_stapled=True,
  187. )
  188. with pytest.raises(ConnectionError) as e:
  189. r.ping()
  190. assert "no ocsp response present" in str(e)