Skip to content

Commit 013dc46

Browse files
committed
replace pyopenssl with cryptography
1 parent 9e597e1 commit 013dc46

File tree

3 files changed

+117
-89
lines changed

3 files changed

+117
-89
lines changed

pyproject.toml

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -20,10 +20,8 @@ classifiers = [
2020
]
2121
requires-python = ">= 3.9"
2222
dependencies = [
23-
"cryptography >=3.1",
23+
"cryptography >=40.0",
2424
"defusedxml",
25-
"pyopenssl <24.3.0",
26-
"python-dateutil",
2725
"requests >=2.0.0,<3.0.0", # ^2 means compatible with 2.x
2826
"xmlschema >=2.0.0,<3.0.0"
2927
]

src/saml2/cert.py

Lines changed: 98 additions & 80 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,11 @@
66
from datetime import datetime
77
from datetime import timezone
88

9-
from OpenSSL import crypto
9+
from cryptography import x509
10+
from cryptography.exceptions import InvalidSignature
11+
from cryptography.hazmat.primitives import hashes, serialization
12+
from cryptography.hazmat.primitives.asymmetric import rsa
13+
from cryptography.x509.oid import NameOID
1014
import dateutil.parser
1115

1216
import saml2.cryptography.pki
@@ -36,7 +40,6 @@ def create_certificate(
3640
valid_to=315360000,
3741
sn=1,
3842
key_length=1024,
39-
hash_alg="sha256",
4043
write_to_file=False,
4144
cert_dir="",
4245
cipher_passphrase=None,
@@ -87,8 +90,6 @@ def create_certificate(
8790
is 1.
8891
:param key_length: Length of the key to be generated. Defaults
8992
to 1024.
90-
:param hash_alg: Hash algorithm to use for the key. Default
91-
is sha256.
9293
:param write_to_file: True if you want to write the certificate
9394
to a file. The method will then return
9495
a tuple with path to certificate file and
@@ -131,49 +132,68 @@ def create_certificate(
131132
k_f = join(cert_dir, key_file)
132133

133134
# create a key pair
134-
k = crypto.PKey()
135-
k.generate_key(crypto.TYPE_RSA, key_length)
135+
k = rsa.generate_private_key(
136+
public_exponent=65537,
137+
key_size=key_length,
138+
)
136139

137140
# create a self-signed cert
138-
cert = crypto.X509()
141+
builder = x509.CertificateBuilder()
139142

140143
if request:
141-
cert = crypto.X509Req()
144+
builder = x509.CertificateSigningRequestBuilder()
142145

143146
if len(cert_info["country_code"]) != 2:
144147
raise WrongInput("Country code must be two letters!")
145-
cert.get_subject().C = cert_info["country_code"]
146-
cert.get_subject().ST = cert_info["state"]
147-
cert.get_subject().L = cert_info["city"]
148-
cert.get_subject().O = cert_info["organization"] # noqa: E741
149-
cert.get_subject().OU = cert_info["organization_unit"]
150-
cert.get_subject().CN = cn
148+
subject_name = x509.Name([
149+
x509.NameAttribute(NameOID.COUNTRY_NAME,
150+
cert_info["country_code"]),
151+
x509.NameAttribute(NameOID.STATE_OR_PROVINCE_NAME,
152+
cert_info["state"]),
153+
x509.NameAttribute(NameOID.LOCALITY_NAME,
154+
cert_info["city"]),
155+
x509.NameAttribute(NameOID.ORGANIZATION_NAME,
156+
cert_info["organization"]),
157+
x509.NameAttribute(NameOID.ORGANIZATIONAL_UNIT_NAME,
158+
cert_info["organization_unit"]),
159+
x509.NameAttribute(NameOID.COMMON_NAME, cn),
160+
])
161+
builder = builder.subject_name(subject_name)
151162
if not request:
152-
cert.set_serial_number(sn)
153-
cert.gmtime_adj_notBefore(valid_from) # Valid before present time
154-
cert.gmtime_adj_notAfter(valid_to) # 3 650 days
155-
cert.set_issuer(cert.get_subject())
156-
cert.set_pubkey(k)
157-
cert.sign(k, hash_alg)
163+
now = datetime.datetime.now(datetime.UTC)
164+
builder = builder.serial_number(
165+
sn,
166+
).not_valid_before(
167+
now + datetime.timedelta(seconds=valid_from),
168+
).not_valid_after(
169+
now + datetime.timedelta(seconds=valid_to),
170+
).issuer_name(
171+
subject_name,
172+
).public_key(
173+
k.public_key(),
174+
)
175+
cert = builder.sign(k, hashes.SHA256())
158176

159177
try:
160-
if request:
161-
tmp_cert = crypto.dump_certificate_request(crypto.FILETYPE_PEM, cert)
162-
else:
163-
tmp_cert = crypto.dump_certificate(crypto.FILETYPE_PEM, cert)
164-
tmp_key = None
178+
tmp_cert = cert.public_bytes(serialization.Encoding.PEM)
179+
key_encryption = None
165180
if cipher_passphrase is not None:
166181
passphrase = cipher_passphrase["passphrase"]
167182
if isinstance(cipher_passphrase["passphrase"], str):
168183
passphrase = passphrase.encode("utf-8")
169-
tmp_key = crypto.dump_privatekey(crypto.FILETYPE_PEM, k, cipher_passphrase["cipher"], passphrase)
184+
key_encryption = serialization.BestAvailableEncryption(passphrase)
170185
else:
171-
tmp_key = crypto.dump_privatekey(crypto.FILETYPE_PEM, k)
186+
key_encryption = serialization.NoEncryption()
187+
tmp_key = k.private_bytes(
188+
encoding=serialization.Encoding.PEM,
189+
format=serialization.PrivateFormat.TraditionalOpenSSL,
190+
encryption_algorithm=key_encryption,
191+
)
172192
if write_to_file:
173-
with open(c_f, "w") as fc:
174-
fc.write(tmp_cert.decode("utf-8"))
175-
with open(k_f, "w") as fk:
176-
fk.write(tmp_key.decode("utf-8"))
193+
with open(c_f, "wb") as fc:
194+
fc.write(tmp_cert)
195+
with open(k_f, "wb") as fk:
196+
fk.write(tmp_key)
177197
return c_f, k_f
178198
return tmp_cert, tmp_key
179199
except Exception as ex:
@@ -198,7 +218,6 @@ def create_cert_signed_certificate(
198218
sign_cert_str,
199219
sign_key_str,
200220
request_cert_str,
201-
hash_alg="sha256",
202221
valid_from=0,
203222
valid_to=315360000,
204223
sn=1,
@@ -222,8 +241,6 @@ def create_cert_signed_certificate(
222241
the requested certificate. If you only have
223242
a file use the method read_str_from_file
224243
to get a string representation.
225-
:param hash_alg: Hash algorithm to use for the key. Default
226-
is sha256.
227244
:param valid_from: When the certificate starts to be valid.
228245
Amount of seconds from when the
229246
certificate is generated.
@@ -237,27 +254,29 @@ def create_cert_signed_certificate(
237254
:return: String representation of the signed
238255
certificate.
239256
"""
240-
ca_cert = crypto.load_certificate(crypto.FILETYPE_PEM, sign_cert_str)
241-
ca_key = None
242-
if passphrase is not None:
243-
ca_key = crypto.load_privatekey(crypto.FILETYPE_PEM, sign_key_str, passphrase)
244-
else:
245-
ca_key = crypto.load_privatekey(crypto.FILETYPE_PEM, sign_key_str)
246-
req_cert = crypto.load_certificate_request(crypto.FILETYPE_PEM, request_cert_str)
247-
248-
cert = crypto.X509()
249-
cert.set_subject(req_cert.get_subject())
250-
cert.set_serial_number(sn)
251-
cert.gmtime_adj_notBefore(valid_from)
252-
cert.gmtime_adj_notAfter(valid_to)
253-
cert.set_issuer(ca_cert.get_subject())
254-
cert.set_pubkey(req_cert.get_pubkey())
255-
cert.sign(ca_key, hash_alg)
256-
257-
cert_dump = crypto.dump_certificate(crypto.FILETYPE_PEM, cert)
258-
if isinstance(cert_dump, str):
259-
return cert_dump
260-
return cert_dump.decode("utf-8")
257+
if isinstance(sign_cert_str, str):
258+
sign_cert_str = sign_cert_str.encode("utf-8")
259+
ca_cert = x509.load_pem_x509_certificate(sign_cert_str)
260+
ca_key = serialization.load_pem_private_key(
261+
sign_key_str, password=passphrase)
262+
req_cert = x509.load_pem_x509_csr(request_cert_str)
263+
264+
now = datetime.datetime.now(datetime.UTC)
265+
cert = x509.CertificateBuilder().subject_name(
266+
req_cert.subject,
267+
).serial_number(
268+
sn,
269+
).not_valid_before(
270+
now + datetime.timedelta(seconds=valid_from),
271+
).not_valid_after(
272+
now + datetime.timedelta(seconds=valid_to),
273+
).issuer_name(
274+
ca_cert.subject,
275+
).public_key(
276+
req_cert.public_key(),
277+
).sign(ca_key, hashes.SHA256())
278+
279+
return cert.public_bytes(serialization.Encoding.PEM).decode("utf-8")
261280

262281
def verify_chain(self, cert_chain_str_list, cert_str):
263282
"""
@@ -276,13 +295,6 @@ def verify_chain(self, cert_chain_str_list, cert_str):
276295
cert_str = tmp_cert_str
277296
return (True, "Signed certificate is valid and correctly signed by CA " "certificate.")
278297

279-
def certificate_not_valid_yet(self, cert):
280-
starts_to_be_valid = dateutil.parser.parse(cert.get_notBefore())
281-
now = datetime.now(timezone.utc)
282-
if starts_to_be_valid < now:
283-
return False
284-
return True
285-
286298
def verify(self, signing_cert_str, cert_str):
287299
"""
288300
Verifies if a certificate is valid and signed by a given certificate.
@@ -303,34 +315,34 @@ def verify(self, signing_cert_str, cert_str):
303315
Message = Why the validation failed.
304316
"""
305317
try:
306-
ca_cert = crypto.load_certificate(crypto.FILETYPE_PEM, signing_cert_str)
307-
cert = crypto.load_certificate(crypto.FILETYPE_PEM, cert_str)
308-
309-
if self.certificate_not_valid_yet(ca_cert):
318+
if isinstance(signing_cert_str, str):
319+
signing_cert_str = signing_cert_str.encode("utf-8")
320+
if isinstance(cert_str, str):
321+
cert_str = cert_str.encode("utf-8")
322+
ca_cert = x509.load_pem_x509_certificate(signing_cert_str)
323+
cert = x509.load_pem_x509_certificate(cert_str)
324+
now = datetime.datetime.now(datetime.UTC)
325+
326+
if ca_cert.not_valid_before_utc >= now:
310327
return False, "CA certificate is not valid yet."
311328

312-
if ca_cert.has_expired() == 1:
329+
if ca_cert.not_valid_after_utc < now:
313330
return False, "CA certificate is expired."
314331

315-
if cert.has_expired() == 1:
332+
if cert.not_valid_after_utc < now:
316333
return False, "The signed certificate is expired."
317334

318-
if self.certificate_not_valid_yet(cert):
335+
if cert.not_valid_before_utc >= now:
319336
return False, "The signed certificate is not valid yet."
320337

321-
if ca_cert.get_subject().CN == cert.get_subject().CN:
338+
if ca_cert.subject.get_attributes_for_oid(NameOID.COMMON_NAME) == \
339+
cert.subject.get_attributes_for_oid(NameOID.COMMON_NAME):
322340
return False, ("CN may not be equal for CA certificate and the " "signed certificate.")
323341

324-
cert_algorithm = cert.get_signature_algorithm()
325-
cert_algorithm = cert_algorithm.decode("ascii")
326-
cert_str = cert_str.encode("ascii")
327-
328-
cert_crypto = saml2.cryptography.pki.load_pem_x509_certificate(cert_str)
329-
330342
try:
331-
crypto.verify(ca_cert, cert_crypto.signature, cert_crypto.tbs_certificate_bytes, cert_algorithm)
343+
cert.verify_directly_issued_by(ca_cert)
332344
return True, "Signed certificate is valid and correctly signed by CA certificate."
333-
except crypto.Error as e:
345+
except (ValueError, TypeError, InvalidSignature) as e:
334346
return False, f"Certificate is incorrectly signed: {str(e)}"
335347
except Exception as e:
336348
return False, f"Certificate is not valid for an unknown reason. {str(e)}"
@@ -352,8 +364,14 @@ def read_cert_from_file(cert_file, cert_type="pem"):
352364
data = fp.read()
353365

354366
try:
355-
cert = saml2.cryptography.pki.load_x509_certificate(data, cert_type)
356-
pem_data = saml2.cryptography.pki.get_public_bytes_from_cert(cert)
367+
cert = None
368+
if cert_type == "pem":
369+
cert = x509.load_pem_x509_certificate(data)
370+
elif cert_type == "der":
371+
cert = x509.load_der_x509_certificate(data)
372+
else:
373+
raise ValueError(f"cert-type {cert_type} not supported")
374+
pem_data = cert.public_bytes(serialization.Encoding.PEM).decode("utf-8")
357375
except Exception as e:
358376
raise CertificateError(e)
359377

src/saml2/sigver.py

Lines changed: 18 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -18,9 +18,21 @@
1818
from urllib import parse
1919
from uuid import uuid4 as gen_random_key
2020

21-
from OpenSSL import crypto
2221
import dateutil
2322

23+
24+
# importlib.resources was introduced in python 3.7
25+
# files API from importlib.resources introduced in python 3.9
26+
if sys.version_info[:2] >= (3, 9):
27+
from importlib.resources import files as _resource_files
28+
else:
29+
from importlib_resources import files as _resource_files
30+
31+
from urllib import parse
32+
33+
from cryptography import x509
34+
import pytz
35+
2436
from saml2 import ExtensionElement
2537
from saml2 import SamlBase
2638
from saml2 import SAMLError
@@ -373,14 +385,14 @@ def active_cert(key):
373385
"""
374386
try:
375387
cert_str = pem_format(key)
376-
cert = crypto.load_certificate(crypto.FILETYPE_PEM, cert_str)
388+
cert = x509.load_pem_x509_certificate(cert_str)
377389
except AttributeError:
378390
return False
379391

380-
now = datetime.now(timezone.utc)
381-
valid_from = dateutil.parser.parse(cert.get_notBefore())
382-
valid_to = dateutil.parser.parse(cert.get_notAfter())
383-
active = not cert.has_expired() and valid_from <= now < valid_to
392+
now = datetime.datetime.now(datetime.UTC)
393+
valid_from = cert.not_valid_before_utc
394+
valid_to = cert.not_valid_after_utc
395+
active = valid_from <= now < valid_to
384396
return active
385397

386398

0 commit comments

Comments
 (0)