certdb: use certutil and match_hostname for cert verification

Use certutil and ssl.match_hostname calls instead of python-nss for
certificate verification.

Reviewed-By: Christian Heimes <cheimes@redhat.com>
This commit is contained in:
Jan Cholasta
2017-01-02 13:53:18 +01:00
committed by Martin Basti
parent 274b0bcf5f
commit 9183cf2a75
4 changed files with 95 additions and 76 deletions

View File

@@ -160,8 +160,8 @@ BuildRequires: python3-wheel
# #
%if 0%{?with_lint} %if 0%{?with_lint}
BuildRequires: samba-python BuildRequires: samba-python
# 1.4: the version where Certificate.serial changed to .serial_number # 1.6: x509.Name.rdns (https://github.com/pyca/cryptography/issues/3199)
BuildRequires: python-cryptography >= 1.4 BuildRequires: python-cryptography >= 1.6
BuildRequires: python-gssapi >= 1.2.0 BuildRequires: python-gssapi >= 1.2.0
BuildRequires: pylint >= 1.6 BuildRequires: pylint >= 1.6
# workaround for https://bugzilla.redhat.com/show_bug.cgi?id=1096506 # workaround for https://bugzilla.redhat.com/show_bug.cgi?id=1096506
@@ -196,8 +196,8 @@ BuildRequires: python2-jinja2
%if 0%{?with_python3} %if 0%{?with_python3}
# FIXME: this depedency is missing - server will not work # FIXME: this depedency is missing - server will not work
#BuildRequires: python3-samba #BuildRequires: python3-samba
# 1.4: the version where Certificate.serial changed to .serial_number # 1.6: x509.Name.rdns (https://github.com/pyca/cryptography/issues/3199)
BuildRequires: python3-cryptography >= 1.4 BuildRequires: python3-cryptography >= 1.6
BuildRequires: python3-gssapi >= 1.2.0 BuildRequires: python3-gssapi >= 1.2.0
BuildRequires: python3-pylint >= 1.6 BuildRequires: python3-pylint >= 1.6
# workaround for https://bugzilla.redhat.com/show_bug.cgi?id=1096506 # workaround for https://bugzilla.redhat.com/show_bug.cgi?id=1096506
@@ -636,7 +636,7 @@ Requires: gnupg
Requires: keyutils Requires: keyutils
Requires: pyOpenSSL Requires: pyOpenSSL
Requires: python-nss >= 0.16 Requires: python-nss >= 0.16
Requires: python-cryptography >= 1.4 Requires: python-cryptography >= 1.6
Requires: python-netaddr Requires: python-netaddr
Requires: python-libipa_hbac Requires: python-libipa_hbac
Requires: python-qrcode-core >= 5.0.0 Requires: python-qrcode-core >= 5.0.0
@@ -685,7 +685,7 @@ Requires: gnupg
Requires: keyutils Requires: keyutils
Requires: python3-pyOpenSSL Requires: python3-pyOpenSSL
Requires: python3-nss >= 0.16 Requires: python3-nss >= 0.16
Requires: python3-cryptography >= 1.4 Requires: python3-cryptography >= 1.6
Requires: python3-netaddr Requires: python3-netaddr
Requires: python3-libipa_hbac Requires: python3-libipa_hbac
Requires: python3-qrcode-core >= 5.0.0 Requires: python3-qrcode-core >= 5.0.0
@@ -760,7 +760,7 @@ Requires: python-pytest-multihost >= 0.5
Requires: python-pytest-sourceorder Requires: python-pytest-sourceorder
Requires: ldns-utils Requires: ldns-utils
Requires: python-sssdconfig Requires: python-sssdconfig
Requires: python2-cryptography >= 1.4 Requires: python2-cryptography >= 1.6
Provides: %{alt_name}-tests = %{version} Provides: %{alt_name}-tests = %{version}
Conflicts: %{alt_name}-tests Conflicts: %{alt_name}-tests
@@ -794,7 +794,7 @@ Requires: python3-pytest-multihost >= 0.5
Requires: python3-pytest-sourceorder Requires: python3-pytest-sourceorder
Requires: ldns-utils Requires: ldns-utils
Requires: python3-sssdconfig Requires: python3-sssdconfig
Requires: python3-cryptography >= 1.4 Requires: python3-cryptography >= 1.6
%description -n python3-ipatests %description -n python3-ipatests
IPA is an integrated solution to provide centrally managed Identity (users, IPA is an integrated solution to provide centrally managed Identity (users,

View File

@@ -35,6 +35,7 @@ from __future__ import print_function
import binascii import binascii
import datetime import datetime
import ipaddress import ipaddress
import ssl
import base64 import base64
import re import re
@@ -49,6 +50,7 @@ from ipalib import api
from ipalib import util from ipalib import util
from ipalib import errors from ipalib import errors
from ipapython.dn import DN from ipapython.dn import DN
from ipapython.dnsutil import DNSName
if six.PY3: if six.PY3:
unicode = str unicode = str
@@ -406,6 +408,27 @@ def process_othernames(gns):
yield gn yield gn
def _pyasn1_get_san_general_names(cert):
tbs = decoder.decode(
cert.tbs_certificate_bytes,
asn1Spec=rfc2459.TBSCertificate()
)[0]
OID_SAN = univ.ObjectIdentifier('2.5.29.17')
# One would expect KeyError or empty iterable when the key ('extensions'
# in this particular case) is not pressent in the certificate but pyasn1
# returns None here
extensions = tbs['extensions'] or []
gns = []
for ext in extensions:
if ext['extnID'] == OID_SAN:
der = decoder.decode(
ext['extnValue'], asn1Spec=univ.OctetString())[0]
gns = decoder.decode(der, asn1Spec=rfc2459.SubjectAltName())[0]
break
return gns
def get_san_general_names(cert): def get_san_general_names(cert):
""" """
Return SAN general names from a python-cryptography Return SAN general names from a python-cryptography
@@ -430,22 +453,7 @@ def get_san_general_names(cert):
and should go away. and should go away.
""" """
tbs = decoder.decode( gns = _pyasn1_get_san_general_names(cert)
cert.tbs_certificate_bytes,
asn1Spec=rfc2459.TBSCertificate()
)[0]
OID_SAN = univ.ObjectIdentifier('2.5.29.17')
# One would expect KeyError or empty iterable when the key ('extensions'
# in this particular case) is not pressent in the certificate but pyasn1
# returns None here
extensions = tbs['extensions'] or []
gns = []
for ext in extensions:
if ext['extnID'] == OID_SAN:
der = decoder.decode(
ext['extnValue'], asn1Spec=univ.OctetString())[0]
gns = decoder.decode(der, asn1Spec=rfc2459.SubjectAltName())[0]
break
GENERAL_NAME_CONSTRUCTORS = { GENERAL_NAME_CONSTRUCTORS = {
'rfc822Name': lambda x: cryptography.x509.RFC822Name(unicode(x)), 'rfc822Name': lambda x: cryptography.x509.RFC822Name(unicode(x)),
@@ -504,6 +512,17 @@ def _pyasn1_to_cryptography_oid(oid):
return cryptography.x509.ObjectIdentifier(str(oid)) return cryptography.x509.ObjectIdentifier(str(oid))
def get_san_a_label_dns_names(cert):
gns = _pyasn1_get_san_general_names(cert)
result = []
for gn in gns:
if gn.getName() == 'dNSName':
result.append(unicode(gn.getComponent()))
return result
def chunk(size, s): def chunk(size, s):
"""Yield chunks of the specified size from the given string. """Yield chunks of the specified size from the given string.
@@ -543,3 +562,23 @@ def format_datetime(t):
if t.tzinfo is None: if t.tzinfo is None:
t = t.replace(tzinfo=UTC()) t = t.replace(tzinfo=UTC())
return unicode(t.strftime("%a %b %d %H:%M:%S %Y %Z")) return unicode(t.strftime("%a %b %d %H:%M:%S %Y %Z"))
def match_hostname(cert, hostname):
match_cert = {}
match_cert['subject'] = match_subject = []
for rdn in cert.subject.rdns:
match_rdn = []
for ava in rdn:
if ava.oid == cryptography.x509.oid.NameOID.COMMON_NAME:
match_rdn.append(('commonName', ava.value))
match_subject.append(match_rdn)
values = get_san_a_label_dns_names(cert)
if values:
match_cert['subjectAltName'] = match_san = []
for value in values:
match_san.append(('DNS', value))
ssl.match_hostname(match_cert, DNSName(hostname).ToASCII())

View File

@@ -25,9 +25,9 @@ import re
import tempfile import tempfile
import shutil import shutil
import base64 import base64
from cryptography.hazmat.primitives import serialization from cryptography.hazmat.primitives import serialization
from nss import nss import cryptography.x509
from nss.error import NSPRError
from ipapython.dn import DN from ipapython.dn import DN
from ipapython.ipa_log_manager import root_logger from ipapython.ipa_log_manager import root_logger
@@ -543,59 +543,39 @@ class NSSDatabase(object):
Raises a ValueError if the certificate is invalid. Raises a ValueError if the certificate is invalid.
""" """
certdb = cert = None cert = self.get_cert(nickname)
if nss.nss_is_initialized(): cert = x509.load_certificate(cert, x509.DER)
nss.nss_shutdown()
nss.nss_init(self.secdir)
try:
certdb = nss.get_default_certdb()
cert = nss.find_cert_from_nickname(nickname)
intended_usage = nss.certificateUsageSSLServer
try:
approved_usage = cert.verify_now(certdb, True, intended_usage)
except NSPRError as e:
if e.errno != -8102:
raise ValueError(e.strerror)
approved_usage = 0
if not approved_usage & intended_usage:
raise ValueError('invalid for a SSL server')
if not cert.verify_hostname(hostname):
raise ValueError('invalid for server %s' % hostname)
finally:
del certdb, cert
nss.nss_shutdown()
return None try:
self.run_certutil(['-V', '-n', nickname, '-u', 'V'])
except ipautil.CalledProcessError:
raise ValueError('invalid for a SSL server')
try:
x509.match_hostname(cert, hostname)
except ValueError:
raise ValueError('invalid for server %s' % hostname)
def verify_ca_cert_validity(self, nickname): def verify_ca_cert_validity(self, nickname):
certdb = cert = None cert = self.get_cert(nickname)
if nss.nss_is_initialized(): cert = x509.load_certificate(cert, x509.DER)
nss.nss_shutdown()
nss.nss_init(self.secdir) if not cert.subject:
raise ValueError("has empty subject")
try: try:
certdb = nss.get_default_certdb() bc = cert.extensions.get_extension_for_class(
cert = nss.find_cert_from_nickname(nickname) cryptography.x509.BasicConstraints)
if not cert.subject: except cryptography.x509.ExtensionNotFound:
raise ValueError("has empty subject") raise ValueError("missing basic constraints")
try:
bc = cert.get_extension(nss.SEC_OID_X509_BASIC_CONSTRAINTS) if not bc.ca:
except KeyError: raise ValueError("not a CA certificate")
raise ValueError("missing basic constraints")
bc = nss.BasicConstraints(bc.value) try:
if not bc.is_ca: self.run_certutil(['-V', '-n', nickname, '-u', 'L'])
raise ValueError("not a CA certificate") except ipautil.CalledProcessError:
intended_usage = nss.certificateUsageSSLCA raise ValueError('invalid for a CA')
try:
approved_usage = cert.verify_now(certdb, True, intended_usage)
except NSPRError as e:
if e.errno != -8102: # SEC_ERROR_INADEQUATE_KEY_USAGE
raise ValueError(e.strerror)
approved_usage = 0
if approved_usage & intended_usage != intended_usage:
raise ValueError('invalid for a CA')
finally:
del certdb, cert
nss.nss_shutdown()
def publish_ca_cert(self, canickname, location): def publish_ca_cert(self, canickname, location):
args = ["-L", "-n", canickname, "-a"] args = ["-L", "-n", canickname, "-a"]

View File

@@ -63,7 +63,7 @@ if SETUPTOOLS_VERSION < (8, 0, 0):
PACKAGE_VERSION = { PACKAGE_VERSION = {
'cryptography': 'cryptography >= 1.4', 'cryptography': 'cryptography >= 1.6',
'custodia': 'custodia >= 0.3.1', 'custodia': 'custodia >= 0.3.1',
'dnspython': 'dnspython >= 1.15', 'dnspython': 'dnspython >= 1.15',
'gssapi': 'gssapi >= 1.2.0', 'gssapi': 'gssapi >= 1.2.0',