Skip to content

Commit 41496f9

Browse files
authored
fix python crypto deprecations (#426)
Use the modern crypto API instead of OpenSSL things.
1 parent e820482 commit 41496f9

5 files changed

Lines changed: 56 additions & 104 deletions

File tree

test/modules/md/md_cert_util.py

Lines changed: 46 additions & 97 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,12 @@
1111
from http.client import HTTPConnection
1212
from urllib.parse import urlparse
1313

14+
from cryptography.hazmat._oid import ExtensionOID
15+
from cryptography.hazmat.bindings._rust import ObjectIdentifier
16+
from cryptography.hazmat.primitives.serialization import Encoding, PrivateFormat, NoEncryption, load_pem_private_key
17+
1418
from cryptography import x509
19+
from cryptography.x509 import DNSName, ExtensionNotFound
1520

1621
SEC_PER_DAY = 24 * 60 * 60
1722

@@ -21,7 +26,6 @@
2126

2227
class MDCertUtil(object):
2328
# Utility class for inspecting certificates in test cases
24-
# Uses PyOpenSSL: https://pyopenssl.org/en/stable/index.html
2529

2630
@classmethod
2731
def load_server_cert(cls, host_ip, host_port, host_name, tls=None, ciphers=None):
@@ -42,12 +46,12 @@ def load_server_cert(cls, host_ip, host_port, host_name, tls=None, ciphers=None)
4246
connection.setblocking(1)
4347
connection.set_tlsext_host_name(host_name.encode('utf-8'))
4448
connection.do_handshake()
45-
peer_cert = connection.get_peer_certificate()
46-
return MDCertUtil(None, cert=peer_cert)
49+
ossl_cert = connection.get_peer_certificate()
50+
return MDCertUtil(None, cert=ossl_cert.to_cryptography())
4751

4852
@classmethod
4953
def parse_pem_cert(cls, text):
50-
cert = OpenSSL.crypto.load_certificate(OpenSSL.crypto.FILETYPE_PEM, text.encode('utf-8'))
54+
cert = x509.load_pem_x509_certificate(text.encode('utf-8'))
5155
return MDCertUtil(None, cert=cert)
5256

5357
@classmethod
@@ -72,46 +76,47 @@ def get_plain(cls, url, timeout):
7276
return None
7377

7478
def __init__(self, cert_path, cert=None):
79+
self.cert = cert
80+
self.privkey = None
7581
if cert_path is not None:
7682
self.cert_path = cert_path
7783
# load certificate and private key
7884
if cert_path.startswith("http"):
79-
cert_data = self.get_plain(cert_path, 1)
80-
else:
81-
cert_data = MDCertUtil._load_binary_file(cert_path)
82-
83-
for file_type in (OpenSSL.crypto.FILETYPE_PEM, OpenSSL.crypto.FILETYPE_ASN1):
84-
try:
85-
self.cert = OpenSSL.crypto.load_certificate(file_type, cert_data)
86-
except Exception as error:
87-
self.error = error
88-
if cert is not None:
89-
self.cert = cert
90-
91-
if self.cert is None:
92-
raise self.error
85+
assert False
86+
try:
87+
with open(cert_path) as fd:
88+
cert = x509.load_pem_x509_certificate("".join(fd.readlines()).encode())
89+
except Exception as error:
90+
self.error = error
91+
if cert is not None:
92+
self.cert = cert
93+
if self.cert is None:
94+
raise self.error
95+
96+
def add_privkey(self, path, password=None):
97+
with open(path) as fd:
98+
self.privkey = load_pem_private_key("".join(fd.readlines()).encode(), password=password)
9399

94100
def get_issuer(self):
95101
return self.cert.get_issuer()
96102

97103
def get_serial(self):
98104
# the string representation of a serial number is not unique. Some
99105
# add leading 0s to align with word boundaries.
100-
return ("%lx" % (self.cert.get_serial_number())).upper()
106+
return ("%lx" % (self.cert.serial_number)).upper()
101107

102108
@staticmethod
103109
def _get_serial(cert) -> int:
104110
if isinstance(cert, x509.Certificate):
105111
return cert.serial_number
106112
if isinstance(cert, MDCertUtil):
107-
return cert.get_serial_number()
108-
elif isinstance(cert, OpenSSL.crypto.X509):
109-
return cert.get_serial_number()
113+
return cert.cert.serial_number
110114
elif isinstance(cert, str):
111115
# assume a hex number
112116
return int(cert, 16)
113117
elif isinstance(cert, int):
114118
return cert
119+
assert False, f'{cert}'
115120
return 0
116121

117122
def get_serial_number(self):
@@ -121,89 +126,33 @@ def same_serial_as(self, other):
121126
return self._get_serial(self.cert) == self._get_serial(other)
122127

123128
def get_not_before(self):
124-
tsp = self.cert.get_notBefore()
125-
return self._parse_tsp(tsp)
129+
try:
130+
return self.cert.not_valid_before_utc
131+
except AttributeError:
132+
return self.cert.not_valid_before
126133

127134
def get_not_after(self):
128-
tsp = self.cert.get_notAfter()
129-
return self._parse_tsp(tsp)
130-
131-
def get_cn(self):
132-
return self.cert.get_subject().CN
135+
try:
136+
return self.cert.not_valid_after_utc
137+
except AttributeError:
138+
return self.cert.not_valid_after
133139

134140
def get_key_length(self):
135-
return self.cert.get_pubkey().bits()
141+
return self.cert.public_key().key_size
136142

137143
def get_san_list(self):
138-
text = OpenSSL.crypto.dump_certificate(OpenSSL.crypto.FILETYPE_TEXT, self.cert).decode("utf-8")
139-
m = re.search(r"X509v3 Subject Alternative Name:(\s+critical)?\s*(.*)", text)
140-
sans_list = []
141-
if m:
142-
sans_list = m.group(2).split(",")
143-
144-
def _strip_prefix(s):
145-
return s.split(":")[1] if s.strip().startswith("DNS:") else s.strip()
146-
return list(map(_strip_prefix, sans_list))
144+
sans = self.cert.extensions.get_extension_for_class(x509.SubjectAlternativeName)
145+
return sans.value.get_values_for_type(DNSName)
147146

148147
def get_must_staple(self):
149-
text = OpenSSL.crypto.dump_certificate(OpenSSL.crypto.FILETYPE_TEXT, self.cert).decode("utf-8")
150-
m = re.search(r"1.3.6.1.5.5.7.1.24:\s*\n\s*0....", text)
151-
if not m:
152-
# Newer openssl versions print this differently
153-
m = re.search(r"TLS Feature:\s*\n\s*status_request\s*\n", text)
154-
return m is not None
148+
try:
149+
self.cert.extensions.get_extension_for_oid(ExtensionOID.TLS_FEATURE)
150+
return True
151+
except ExtensionNotFound:
152+
return False
155153

156154
@classmethod
157155
def validate_privkey(cls, privkey_path, passphrase=None):
158-
privkey_data = cls._load_binary_file(privkey_path)
159-
if passphrase:
160-
privkey = OpenSSL.crypto.load_privatekey(OpenSSL.crypto.FILETYPE_PEM, privkey_data, passphrase)
161-
else:
162-
privkey = OpenSSL.crypto.load_privatekey(OpenSSL.crypto.FILETYPE_PEM, privkey_data)
163-
return privkey.check()
164-
165-
def validate_cert_matches_priv_key(self, privkey_path):
166-
# Verifies that the private key and cert match.
167-
privkey_data = MDCertUtil._load_binary_file(privkey_path)
168-
privkey = OpenSSL.crypto.load_privatekey(OpenSSL.crypto.FILETYPE_PEM, privkey_data)
169-
context = OpenSSL.SSL.Context(OpenSSL.SSL.SSLv23_METHOD)
170-
context.use_privatekey(privkey)
171-
context.use_certificate(self.cert)
172-
context.check_privatekey()
173-
174-
# --------- _utils_ ---------
175-
176-
def astr(self, s):
177-
return s.decode('utf-8')
178-
179-
def _parse_tsp(self, tsp):
180-
# timestampss returned by PyOpenSSL are bytes
181-
# parse date and time part
182-
s = ("%s-%s-%s %s:%s:%s" % (self.astr(tsp[0:4]), self.astr(tsp[4:6]), self.astr(tsp[6:8]),
183-
self.astr(tsp[8:10]), self.astr(tsp[10:12]), self.astr(tsp[12:14])))
184-
timestamp = datetime.strptime(s, '%Y-%m-%d %H:%M:%S')
185-
# adjust timezone
186-
tz_h, tz_m = 0, 0
187-
m = re.match(r"([+\-]\d{2})(\d{2})", self.astr(tsp[14:]))
188-
if m:
189-
tz_h, tz_m = int(m.group(1)), int(m.group(2)) if tz_h > 0 else -1 * int(m.group(2))
190-
return timestamp.replace(tzinfo=self.FixedOffset(60 * tz_h + tz_m))
191-
192-
@classmethod
193-
def _load_binary_file(cls, path):
194-
with open(path, mode="rb") as file:
195-
return file.read()
196-
197-
class FixedOffset(tzinfo):
198-
199-
def __init__(self, offset):
200-
self.__offset = timedelta(minutes=offset)
201-
202-
def utcoffset(self, dt):
203-
return self.__offset
204-
205-
def tzname(self, dt):
206-
return None
207-
208-
def dst(self, dt):
209-
return timedelta(0)
156+
with open(privkey_path) as fd:
157+
privkey = load_pem_private_key("".join(fd.readlines()).encode(), password=passphrase)
158+
return privkey is not None

test/modules/md/md_env.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -345,7 +345,7 @@ def check_md_complete(self, domain, pkey=None):
345345
md = self.get_md_status(domain)
346346
assert md
347347
assert 'state' in md, "md is unexpected: {0}".format(md)
348-
assert md['state'] is MDTestEnv.MD_S_COMPLETE, f"unexpected state: {md['state']}"
348+
assert md['state'] is MDTestEnv.MD_S_COMPLETE, f"unexpected state: {md}"
349349
pkey_file = self.store_domain_file(domain, self.pkey_fname(pkey))
350350
cert_file = self.store_domain_file(domain, self.cert_fname(pkey))
351351
r = self.run(['ls', os.path.dirname(pkey_file)])
@@ -364,7 +364,7 @@ def check_md_credentials(self, domain):
364364
# check private key, validate certificate, etc
365365
MDCertUtil.validate_privkey(self.store_domain_file(domain, 'privkey.pem'))
366366
cert = MDCertUtil(self.store_domain_file(domain, 'pubcert.pem'))
367-
cert.validate_cert_matches_priv_key(self.store_domain_file(domain, 'privkey.pem'))
367+
cert.add_privkey(self.store_domain_file(domain, 'privkey.pem'))
368368
# No longer check CN, it may not be set or is not trusted anyway
369369
# assert cert.get_cn() == domain, f'CN: expected "{domain}", got {cert.get_cn()}'
370370
# check SANs

test/modules/md/test_502_acmev2_drive.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -395,7 +395,7 @@ def test_md_502_200(self, env):
395395
# check new cert
396396
env.check_md_credentials([name, "test." + domain])
397397
new_cert = MDCertUtil(env.store_domain_file(name, 'pubcert.pem'))
398-
assert not old_cert.same_serial_as(new_cert.get_serial)
398+
assert not old_cert.same_serial_as(new_cert.get_serial())
399399

400400
@pytest.mark.parametrize("renew_window,test_data_list", [
401401
("14d", [
@@ -550,4 +550,4 @@ def _check_account_key(self, env, name):
550550
# check: key file is encrypted PEM
551551
md = env.a2md(["list", name]).json['output'][0]
552552
acc = md['ca']['account']
553-
MDCertUtil.validate_privkey(env.path_account_key(acc), lambda *args: encrypt_key)
553+
MDCertUtil.validate_privkey(env.path_account_key(acc), encrypt_key)

test/modules/md/test_702_auto.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -210,8 +210,8 @@ def test_md_702_005(self, env):
210210
# check temporary cert from server
211211
cert2 = MDCertUtil(env.path_fallback_cert(domain))
212212
assert cert1.same_serial_as(cert2), \
213-
"Unexpected temporary certificate on vhost %s. Expected cn: %s , "\
214-
"but found cn: %s" % (name_a, cert2.get_cn(), cert1.get_cn())
213+
f"Unexpected temporary certificate on vhost {name_a}." \
214+
f" Expected cn: {cert2}, but found cn: {cert1}"
215215

216216
# test case: drive MD with only invalid challenges, domains should stay 503'd
217217
def test_md_702_006(self, env):

test/modules/md/test_800_must_staple.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
# test mod_md must-staple support
2+
import time
23
import pytest
34

45
from .md_conf import MDConf
@@ -21,7 +22,8 @@ def _class_scope(self, env, acme):
2122

2223
@pytest.fixture(autouse=True, scope='function')
2324
def _method_scope(self, env, request):
24-
self.domain = env.get_class_domain(self.__class__)
25+
env.clear_store()
26+
self.domain = env.get_request_domain(request)
2527

2628
def configure_httpd(self, env, domain, add_lines=""):
2729
conf = MDConf(env, admin="admin@" + domain)
@@ -43,6 +45,7 @@ def test_md_800_001(self, env):
4345
def test_md_800_002(self, env):
4446
self.configure_httpd(env, self.domain, "MDMustStaple off")
4547
assert env.apache_restart() == 0, f'{env.apachectl_stderr}'
48+
assert env.await_completion([self.domain])
4649
env.check_md_complete(self.domain)
4750
cert1 = MDCertUtil(env.store_domain_file(self.domain, 'pubcert.pem'))
4851
assert not cert1.get_must_staple()

0 commit comments

Comments
 (0)