Generate client password-protected cert with trustme
DELETE
dummyserver/certs/client_intermediate.pem
------BEGIN CERTIFICATE-----
-MIIChzCCAfCgAwIBAgIUZgix95Zxzc+WryIWanrDezW1VjcwDQYJKoZIhvcNAQEL
-BQAwRDEbMBkGA1UECgwSdHJ1c3RtZSB2MC40LjArZGV2MSUwIwYDVQQLDBxUZXN0
-aW5nIENBICNwN2dEd0tMS3EydlJOajZmMCAXDTAwMDEwMTAwMDAwMFoYDzMwMDAw
-MTAxMDAwMDAwWjBNMRswGQYDVQQKDBJ0cnVzdG1lIHYwLjQuMCtkZXYxLjAsBgNV
-BAsMJVRlc3Rpbmcgc2VydmVyIGNlcnQgI0NPajVGVkxXWEVtcmFHNTQwgZ8wDQYJ
-KoZIhvcNAQEBBQADgY0AMIGJAoGBAKeE765+Ws1ZdC86tfZ5LvLTjWluQgmsTx2o
-7xhYAOFmTbZb6qNLCDS07R1VP74ve6UlFD55cV8VbxvEZd8Z3LOADF6nTN61XPbj
-dn2J6GfsSjaHE6+mJDXhCtVrD4EGdD4nXRem48mjsrAkrvJ8v4gQNzGzQ27D2dWT
-B7Ij6mWNAgMBAAGjazBpMB0GA1UdDgQWBBT66uW6I2OfZYacXgQkop4qlX+qJTAM
-BgNVHRMBAf8EAjAAMB8GA1UdIwQYMBaAFESoDYfzVyFP3QHyZG9cvxmlBIGsMBkG
-A1UdEQEB/wQPMA2CC2xvY2FsY2xpZW50MA0GCSqGSIb3DQEBCwUAA4GBAG8zoqW0
-w5ROSuNFE7fi5I4bdC6sbddiFRXX//TkP2vRD3cM11AKp52UjzK2nUrkoigrJ5p8
-xa/PGnPfOVCPiKIb1kzeI/7tyBet6n3q2L0wQo3PR/QCHeSiIpm8lAi1a+8ShXFM
-F2CG+z7IN0cQO4bzcwtkk8MhcCsMP14K5PK2
------END CERTIFICATE-----
------BEGIN CERTIFICATE-----
-MIICwjCCAiugAwIBAgIUWL7wOmK0BVMR8LM5UBewDZEEuH0wDQYJKoZIhvcNAQEL
-BQAwgYExCzAJBgNVBAYTAkZJMQ4wDAYDVQQIDAVkdW1teTEOMAwGA1UEBwwFZHVt
-bXkxDjAMBgNVBAoMBWR1bW15MQ4wDAYDVQQLDAVkdW1teTERMA8GA1UEAwwIU25h
-a2VPaWwxHzAdBgkqhkiG9w0BCQEWEGR1bW15QHRlc3QubG9jYWwwIBcNMDAwMTAx
-MDAwMDAwWhgPMzAwMDAxMDEwMDAwMDBaMEQxGzAZBgNVBAoMEnRydXN0bWUgdjAu
-NC4wK2RldjElMCMGA1UECwwcVGVzdGluZyBDQSAjcDdnRHdLTEtxMnZSTmo2ZjCB
-nzANBgkqhkiG9w0BAQEFAAOBjQAwgYkCgYEAr7134NKsqNQ44gIFElVC5KnGYIYv
-D96Kv+5UgXVAyNNK4NpQXHVFmCZpSuyvlz4UZzFBoykISjU+vcGqbFqwRrYciPwh
-45HVQgtoe0SSpze7sv0qsMJiGNRDK06nVI/aCHP9FRoD5iPq8E7lSNVYipai466G
-1lEvVLb0SGNihAUCAwEAAaNxMG8wHQYDVR0OBBYEFESoDYfzVyFP3QHyZG9cvxml
-BIGsMBIGA1UdEwEB/wQIMAYBAf8CAQgwDgYDVR0PAQH/BAQDAgEGMCoGA1UdJQEB
-/wQgMB4GCCsGAQUFBwMCBggrBgEFBQcDAQYIKwYBBQUHAwMwDQYJKoZIhvcNAQEL
-BQADgYEAEs9EAeL3300UxzmT4zyj2cHB2GQxisteEuz9VcWhrvyNDxQ3ko0BxG04
-4fye7dpElrrbSq8PYkygA1qiBCN2NL+v78XWb2OYd7PptpbPehzaEpCTK37O+Num
-sB4v1c63r2w1mH1lSjZDkJfd1hml+VwntSzuCmGERlroE6PQwf8=
------END CERTIFICATE-----

DELETE
dummyserver/certs/client_password.key
------BEGIN RSA PRIVATE KEY-----
-Proc-Type: 4,ENCRYPTED
-DEK-Info: AES-256-CBC,70C641602D5F366DC5DB70645351993D
-
-/Ijrtw+2Rjc1mQCXWoNCtzjbRoIhBHQu9ZbQoCnC4/lHru2megV0vDQju0yYjs2H
-7Y7tnMe0hlR9F21be6AkoKDF4B5Kg2X47fwG5V9SIbHBkz3KClfnPp/ojrhIWLTo
-grtZoXBFkivDnkuF9NO3qRlskP7u//r3kB5uXIG0ZpfUbRwgm13SqHj1oEB9RdYM
-bGhB3tL6dxdIXEgyc9numKBQ0lQu5yYlOH+1aiJSQQdN59ZunreIq//UM1Qc7Uj/
-ILJusFmnec40ArJ+aykENWkToHSKkpeL6no6ZRCnkAYqtUJ84B6zMv9zYhN5UF3O
-WHP/4FAu4AylJvNx9sYxXdGaBb+YcX46B7wQk2mkmCtK6cgkrNV3/bohUbYt3tSe
-K9dH2xe9orxsGQjoKxylwh7+h8o+BwHpk1naFSzliQV4gvi8yBEzXxM98vNU5B4L
-ex8Q2ARWvfNc7OBqboPP0yBMKP/cV9n+fNMwbP0koHxBt71527fVQLoemMiPRb5M
-+rcufc+80AUK4baAA5Nu2sZGRqoiFemQ2vgEAxOzRbt/pHzdheO6OHqLJ5W4IWaW
-Erojm7/ar6gDlIIGwM8IJdbcMG69s7r8u47lD45ONQMq41Io4Svvs0SCgdRhLt/3
-Nb6Smxy7vWFOcrHEJVsv27UD0FViaYHy37DIc6lVvX9s6+VKbdIYuiqxalbaCpKo
-VP8kdQZ4SFBAxV9cgPjFbQKVBXkLBdxJKGPzzK3Jc9khD1uHp5Um8OSM21Kh55N3
-jvDY5h8fQ0cPyJmlZJzRdYi1+8H5TSFvEXd6cqVkYWiJ1ac0gPOoVt7+YAZ6JB2J
------END RSA PRIVATE KEY-----

MODIFY
dummyserver/server.py
dummyserver/server.py
@@ -18,10 +18,13 @@ from datetime import datetime
from urllib3.exceptions import HTTPWarning
+from cryptography.hazmat.backends import default_backend
+from cryptography.hazmat.primitives import serialization
import tornado.httpserver
import tornado.ioloop
import tornado.netutil
import tornado.web
+import trustme
log = logging.getLogger(__name__)
@@ -33,11 +36,6 @@ DEFAULT_CERTS = {
"cert_reqs": ssl.CERT_OPTIONAL,
"ca_certs": os.path.join(CERTS_PATH, "cacert.pem"),
}
-CLIENT_INTERMEDIATE_PEM = "client_intermediate.pem"
-CLIENT_NO_INTERMEDIATE_PEM = "client_no_intermediate.pem"
-CLIENT_INTERMEDIATE_KEY = "client_intermediate.key"
-CLIENT_CERT = os.path.join(CERTS_PATH, CLIENT_INTERMEDIATE_PEM)
-PASSWORD_CLIENT_KEYFILE = os.path.join(CERTS_PATH, "client_password.key")
DEFAULT_CA = os.path.join(CERTS_PATH, "cacert.pem")
DEFAULT_CA_KEY = os.path.join(CERTS_PATH, "cacert.key")
DEFAULT_CA_BAD = os.path.join(CERTS_PATH, "client_bad.pem")
@@ -174,3 +172,15 @@ if __name__ == "__main__":
server_thread = run_loop_in_thread(io_loop)
print("Listening on http://{host}:{port}".format(host=host, port=port))
+
+
+def encrypt_key_pem(private_key_pem, password):
+ private_key = serialization.load_pem_private_key(
+ private_key_pem.bytes(), password=None, backend=default_backend()
+ )
+ encrypted_key = private_key.private_bytes(
+ serialization.Encoding.PEM,
+ serialization.PrivateFormat.TraditionalOpenSSL,
+ serialization.BestAvailableEncryption(password),
+ )
+ return trustme.Blob(encrypted_key)

MODIFY
test/conftest.py
test/conftest.py
@@ -10,14 +10,7 @@ from tornado import web, ioloop
from dummyserver.handlers import TestingApp
from dummyserver.server import run_tornado_app
-from dummyserver.server import (
- DEFAULT_CA,
- DEFAULT_CA_KEY,
- CLIENT_INTERMEDIATE_PEM,
- CLIENT_NO_INTERMEDIATE_PEM,
- CLIENT_INTERMEDIATE_KEY,
- HAS_IPV6,
-)
+from dummyserver.server import HAS_IPV6
# The Python 3.8+ default loop on Windows breaks Tornado
@@ -29,28 +22,6 @@ def configure_windows_event_loop():
asyncio.set_event_loop_policy(asyncio.WindowsSelectorEventLoopPolicy())
-@pytest.fixture(scope="session")
-def certs_dir(tmp_path_factory):
- tmpdir = tmp_path_factory.mktemp("certs")
- # Start from existing root CA as we don't want to change the server certificate yet
- with open(DEFAULT_CA, "rb") as crt, open(DEFAULT_CA_KEY, "rb") as key:
- root_ca = trustme.CA.from_pem(crt.read(), key.read())
-
- # client cert chain
- intermediate_ca = root_ca.create_child_ca()
- cert = intermediate_ca.issue_cert(u"example.com")
-
- cert.private_key_pem.write_to_path(str(tmpdir / CLIENT_INTERMEDIATE_KEY))
- # Write the client cert and the intermediate CA
- client_cert = str(tmpdir / CLIENT_INTERMEDIATE_PEM)
- cert.cert_chain_pems[0].write_to_path(client_cert)
- cert.cert_chain_pems[1].write_to_path(client_cert, append=True)
- # Write only the client cert
- cert.cert_chain_pems[0].write_to_path(str(tmpdir / CLIENT_NO_INTERMEDIATE_PEM))
-
- yield tmpdir
-
-
ServerConfig = collections.namedtuple("ServerConfig", ["host", "port", "ca_certs"])

MODIFY
test/with_dummyserver/test_https.py
test/with_dummyserver/test_https.py
import datetime
import json
import logging
+import os.path
import ssl
import sys
import shutil
+import tempfile
import warnings
import mock
import pytest
+import trustme
from dummyserver.testcase import HTTPSDummyServerTestCase
from dummyserver.server import (
- CLIENT_CERT,
- CLIENT_INTERMEDIATE_PEM,
- CLIENT_NO_INTERMEDIATE_PEM,
- CLIENT_INTERMEDIATE_KEY,
+ encrypt_key_pem,
DEFAULT_CA,
+ DEFAULT_CA_KEY,
DEFAULT_CA_BAD,
DEFAULT_CERTS,
- PASSWORD_CLIENT_KEYFILE,
)
from test import (
@@ -77,9 +77,51 @@ TLSv1_3_CERTS = DEFAULT_CERTS.copy()
TLSv1_3_CERTS["ssl_version"] = getattr(ssl, "PROTOCOL_TLS", None)
+CLIENT_INTERMEDIATE_PEM = "client_intermediate.pem"
+CLIENT_NO_INTERMEDIATE_PEM = "client_no_intermediate.pem"
+CLIENT_INTERMEDIATE_KEY = "client_intermediate.key"
+PASSWORD_CLIENT_KEYFILE = "client_password.key"
+CLIENT_CERT = CLIENT_INTERMEDIATE_PEM
+
+
class TestHTTPS(HTTPSDummyServerTestCase):
tls_protocol_name = None
+ @classmethod
+ def setup_class(cls):
+ super(TestHTTPS, cls).setup_class()
+
+ cls.certs_dir = tempfile.mkdtemp()
+ # Start from existing root CA as we don't want to change the server certificate yet
+ with open(DEFAULT_CA, "rb") as crt, open(DEFAULT_CA_KEY, "rb") as key:
+ root_ca = trustme.CA.from_pem(crt.read(), key.read())
+
+ # client cert chain
+ intermediate_ca = root_ca.create_child_ca()
+ cert = intermediate_ca.issue_cert(u"example.com")
+ encrypted_key = encrypt_key_pem(cert.private_key_pem, b"letmein")
+
+ cert.private_key_pem.write_to_path(
+ os.path.join(cls.certs_dir, CLIENT_INTERMEDIATE_KEY)
+ )
+ encrypted_key.write_to_path(
+ os.path.join(cls.certs_dir, PASSWORD_CLIENT_KEYFILE)
+ )
+ # Write the client cert and the intermediate CA
+ client_cert = os.path.join(cls.certs_dir, CLIENT_INTERMEDIATE_PEM)
+ cert.cert_chain_pems[0].write_to_path(client_cert)
+ cert.cert_chain_pems[1].write_to_path(client_cert, append=True)
+ # Write only the client cert
+ cert.cert_chain_pems[0].write_to_path(
+ os.path.join(cls.certs_dir, CLIENT_NO_INTERMEDIATE_PEM)
+ )
+
+ @classmethod
+ def teardown_class(cls):
+ super(TestHTTPS, cls).teardown_class()
+
+ shutil.rmtree(cls.certs_dir)
+
def test_simple(self):
with HTTPSConnectionPool(
self.host, self.port, ca_certs=DEFAULT_CA
@@ -95,7 +137,7 @@ class TestHTTPS(HTTPSDummyServerTestCase):
r = pool.request("GET", "/")
assert r.status == 200, r.data
- def test_client_intermediate(self, certs_dir):
+ def test_client_intermediate(self):
"""Check that certificate chains work well with client certs
We generate an intermediate CA from the root CA, and issue a client certificate
@@ -106,15 +148,15 @@ class TestHTTPS(HTTPSDummyServerTestCase):
with HTTPSConnectionPool(
self.host,
self.port,
- key_file=str(certs_dir / CLIENT_INTERMEDIATE_KEY),
- cert_file=str(certs_dir / CLIENT_INTERMEDIATE_PEM),
+ key_file=os.path.join(self.certs_dir, CLIENT_INTERMEDIATE_KEY),
+ cert_file=os.path.join(self.certs_dir, CLIENT_INTERMEDIATE_PEM),
ca_certs=DEFAULT_CA,
) as https_pool:
r = https_pool.request("GET", "/certificate")
subject = json.loads(r.data.decode("utf-8"))
assert subject["organizationalUnitName"].startswith("Testing cert")
- def test_client_no_intermediate(self, certs_dir):
+ def test_client_no_intermediate(self):
"""Check that missing links in certificate chains indeed break
The only difference with test_client_intermediate is that we don't send the
@@ -123,8 +165,8 @@ class TestHTTPS(HTTPSDummyServerTestCase):
with HTTPSConnectionPool(
self.host,
self.port,
- cert_file=str(certs_dir / CLIENT_NO_INTERMEDIATE_PEM),
- key_file=str(certs_dir / CLIENT_INTERMEDIATE_KEY),
+ cert_file=os.path.join(self.certs_dir, CLIENT_NO_INTERMEDIATE_PEM),
+ key_file=os.path.join(self.certs_dir, CLIENT_INTERMEDIATE_KEY),
ca_certs=DEFAULT_CA,
) as https_pool:
try:
@@ -157,21 +199,21 @@ class TestHTTPS(HTTPSDummyServerTestCase):
self.host,
self.port,
ca_certs=DEFAULT_CA,
- key_file=PASSWORD_CLIENT_KEYFILE,
- cert_file=CLIENT_CERT,
+ key_file=os.path.join(self.certs_dir, PASSWORD_CLIENT_KEYFILE),
+ cert_file=os.path.join(self.certs_dir, CLIENT_CERT),
key_password="letmein",
) as https_pool:
r = https_pool.request("GET", "/certificate")
subject = json.loads(r.data.decode("utf-8"))
- assert subject["organizationalUnitName"].startswith("Testing server cert")
+ assert subject["organizationalUnitName"].startswith("Testing cert")
@requires_ssl_context_keyfile_password
def test_client_encrypted_key_requires_password(self):
with HTTPSConnectionPool(
self.host,
self.port,
- key_file=PASSWORD_CLIENT_KEYFILE,
- cert_file=CLIENT_CERT,
+ key_file=os.path.join(self.certs_dir, PASSWORD_CLIENT_KEYFILE),
+ cert_file=os.path.join(self.certs_dir, CLIENT_CERT),
key_password=None,
) as https_pool:
with pytest.raises(MaxRetryError) as e:

MODIFY
test/with_dummyserver/test_socketlevel.py
test/with_dummyserver/test_socketlevel.py
@@ -17,7 +17,12 @@ from urllib3.util.retry import Retry
from urllib3._collections import HTTPHeaderDict
from dummyserver.testcase import SocketDummyServerTestCase, consume_socket
-from dummyserver.server import DEFAULT_CERTS, DEFAULT_CA, get_unreachable_address
+from dummyserver.server import (
+ DEFAULT_CERTS,
+ DEFAULT_CA,
+ get_unreachable_address,
+ encrypt_key_pem,
+)
from .. import onlyPy3, LogRecorder
@@ -39,8 +44,6 @@ import ssl
import tempfile
import mock
-from cryptography.hazmat.backends import default_backend
-from cryptography.hazmat.primitives import serialization
import pytest
import trustme
@@ -110,24 +113,12 @@ class TestClientCerts(SocketDummyServerTestCase):
Tests for client certificate support.
"""
- @classmethod
- def _encrypt_key(cls, private_key_pem, password):
- private_key = serialization.load_pem_private_key(
- private_key_pem.bytes(), password=None, backend=default_backend()
- )
- encrypted_key = private_key.private_bytes(
- serialization.Encoding.PEM,
- serialization.PrivateFormat.TraditionalOpenSSL,
- serialization.BestAvailableEncryption(password),
- )
- return trustme.Blob(encrypted_key)
-
@classmethod
def setup_class(cls):
cls.tmpdir = tempfile.mkdtemp()
ca = trustme.CA()
cert = ca.issue_cert(u"localhost")
- encrypted_key = cls._encrypt_key(cert.private_key_pem, b"letmein")
+ encrypted_key = encrypt_key_pem(cert.private_key_pem, b"letmein")
cls.ca_path = os.path.join(cls.tmpdir, "ca.pem")
cls.cert_combined_path = os.path.join(cls.tmpdir, "server.combined.pem")

Generate client password-protected cert with trustme  (#1793)