Use the OpenSSL certificate parser in cert-find

cert-find is a rather complex beast because it not only
looks for certificates in the optional CA but within the
IPA LDAP database as well. It has a process to deduplicate
the certificates since any PKI issued certificates will
also be associated with an IPA record.

In order to obtain the data to deduplicate the certificates
the cert from LDAP must be parser for issuer and serial number.
ipaldap has automation to determine the datatype of an
attribute and will use the python-cryptography engine to
decode a certificate automatically if you access
entry['usercertificate'].

The downside is that this is comparatively slow. Here is the
parse time in microseconds:

OpenSSL.crypto 175
pyasn1 1010
python-cryptography 3136

The python-cryptography time is fine if you're parsing one
certificate but if the LDAP search returns a lot of certificates,
say in the thousands, then those microseconds add up quickly.
In testing it took ~17 seconds to parse 5k certificates.

It's hard to overstate just how much better the cryptography
Python interface is. In the case of OpenSSL really the only
certificate fields easily available are serial number, subject
and issuer. And the subject/issuer are in the OpenSSL reverse
format which doesn't compare nicely to the cryptography format.
The DN module can correct this.

Fortunately for cert-find we only need serial number and issuer,
so the OpenSSL module fine. It takes ~2 seconds.

pyasn1 is also relatively faster but switch to it would require
subtantially more effort for less payback.

cert-find when there are a lot of certificates has been
historically slow. It isn't related to the CA which returns
large sets (well, 5k anyway) in a second or two. It was the
LDAP comparision adding tens of seconds to the runtime.

CLI times from before and after:

original:

-------------------------------
Number of entries returned 5011
-------------------------------
real    0m21.155s
user    0m0.835s
sys     0m0.159s

using OpenSSL:

real    0m5.747s
user    0m0.864s
sys     0m0.148s

OpenSSL is forcibly lazy-loaded so it doesn't conflict with
python-requests.  See ipaserver/wsgi.py for the gory details.

Fixes: https://pagure.io/freeipa/issue/9331

Signed-off-by: Rob Crittenden <rcritten@redhat.com>
Reviewed-By: Florence Blanc-Renaud <frenaud@redhat.com>
Reviewed-By: Antonio Torres <antorres@redhat.com>
This commit is contained in:
Rob Crittenden
2023-02-17 13:15:51 -05:00
parent 2b2f10c2eb
commit 191880bc9f
2 changed files with 25 additions and 3 deletions

View File

@@ -407,6 +407,7 @@ BuildRequires: python3-pylint
BuildRequires: python3-pytest-multihost
BuildRequires: python3-pytest-sourceorder
BuildRequires: python3-qrcode-core >= 5.0.0
BuildRequires: python3-pyOpenSSL
BuildRequires: python3-samba
BuildRequires: python3-six
BuildRequires: python3-sss
@@ -877,6 +878,7 @@ Requires: python3-netifaces >= 0.10.4
Requires: python3-pyasn1 >= 0.3.2-2
Requires: python3-pyasn1-modules >= 0.3.2-2
Requires: python3-pyusb
Requires: python3-pyOpenSSL
Requires: python3-qrcode-core >= 5.0.0
Requires: python3-requests
Requires: python3-six

View File

@@ -30,6 +30,7 @@ import cryptography.x509
from cryptography.hazmat.primitives import hashes, serialization
from dns import resolver, reversename
import six
import sys
from ipalib import Command, Str, Int, Flag, StrEnum, SerialNumber
from ipalib import api
@@ -1617,7 +1618,19 @@ class cert_find(Search, CertMethod):
)
def _get_cert_key(self, cert):
return (DN(cert.issuer), cert.serial_number)
# for cert-find with a certificate value
if isinstance(cert, x509.IPACertificate):
return (DN(cert.issuer), cert.serial_number)
issuer = []
for oid, value in cert.get_issuer().get_components():
issuer.append(
'{}={}'.format(oid.decode('utf-8'), value.decode('utf-8'))
)
issuer = ','.join(issuer)
# Use this to flip from OpenSSL reverse to X500 ordering
issuer = DN(issuer).x500_text()
return (DN(issuer), cert.get_serial_number())
def _cert_search(self, pkey_only, **options):
result = collections.OrderedDict()
@@ -1737,6 +1750,11 @@ class cert_find(Search, CertMethod):
return result, False, complete
def _ldap_search(self, all, pkey_only, no_members, **options):
# defer import of the OpenSSL module to not affect the requests
# module which will use pyopenssl if this is available.
if sys.modules.get('OpenSSL.SSL', False) is None:
del sys.modules["OpenSSL.SSL"]
import OpenSSL.crypto
ldap = self.api.Backend.ldap2
filters = []
@@ -1795,12 +1813,14 @@ class cert_find(Search, CertMethod):
ca_enabled = getattr(context, 'ca_enabled')
for entry in entries:
for attr in ('usercertificate', 'usercertificate;binary'):
for cert in entry.get(attr, []):
for der in entry.raw.get(attr, []):
cert = OpenSSL.crypto.load_certificate(
OpenSSL.crypto.FILETYPE_ASN1, der)
cert_key = self._get_cert_key(cert)
try:
obj = result[cert_key]
except KeyError:
obj = {'serial_number': cert.serial_number}
obj = {'serial_number': cert.get_serial_number()}
if not pkey_only and (all or not ca_enabled):
# Retrieving certificate details is now deferred
# until after all certificates are collected.