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}
BuildRequires: samba-python
# 1.4: the version where Certificate.serial changed to .serial_number
BuildRequires: python-cryptography >= 1.4
# 1.6: x509.Name.rdns (https://github.com/pyca/cryptography/issues/3199)
BuildRequires: python-cryptography >= 1.6
BuildRequires: python-gssapi >= 1.2.0
BuildRequires: pylint >= 1.6
# workaround for https://bugzilla.redhat.com/show_bug.cgi?id=1096506
@@ -196,8 +196,8 @@ BuildRequires: python2-jinja2
%if 0%{?with_python3}
# FIXME: this depedency is missing - server will not work
#BuildRequires: python3-samba
# 1.4: the version where Certificate.serial changed to .serial_number
BuildRequires: python3-cryptography >= 1.4
# 1.6: x509.Name.rdns (https://github.com/pyca/cryptography/issues/3199)
BuildRequires: python3-cryptography >= 1.6
BuildRequires: python3-gssapi >= 1.2.0
BuildRequires: python3-pylint >= 1.6
# workaround for https://bugzilla.redhat.com/show_bug.cgi?id=1096506
@@ -636,7 +636,7 @@ Requires: gnupg
Requires: keyutils
Requires: pyOpenSSL
Requires: python-nss >= 0.16
Requires: python-cryptography >= 1.4
Requires: python-cryptography >= 1.6
Requires: python-netaddr
Requires: python-libipa_hbac
Requires: python-qrcode-core >= 5.0.0
@@ -685,7 +685,7 @@ Requires: gnupg
Requires: keyutils
Requires: python3-pyOpenSSL
Requires: python3-nss >= 0.16
Requires: python3-cryptography >= 1.4
Requires: python3-cryptography >= 1.6
Requires: python3-netaddr
Requires: python3-libipa_hbac
Requires: python3-qrcode-core >= 5.0.0
@@ -760,7 +760,7 @@ Requires: python-pytest-multihost >= 0.5
Requires: python-pytest-sourceorder
Requires: ldns-utils
Requires: python-sssdconfig
Requires: python2-cryptography >= 1.4
Requires: python2-cryptography >= 1.6
Provides: %{alt_name}-tests = %{version}
Conflicts: %{alt_name}-tests
@@ -794,7 +794,7 @@ Requires: python3-pytest-multihost >= 0.5
Requires: python3-pytest-sourceorder
Requires: ldns-utils
Requires: python3-sssdconfig
Requires: python3-cryptography >= 1.4
Requires: python3-cryptography >= 1.6
%description -n python3-ipatests
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 datetime
import ipaddress
import ssl
import base64
import re
@@ -49,6 +50,7 @@ from ipalib import api
from ipalib import util
from ipalib import errors
from ipapython.dn import DN
from ipapython.dnsutil import DNSName
if six.PY3:
unicode = str
@@ -406,6 +408,27 @@ def process_othernames(gns):
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):
"""
Return SAN general names from a python-cryptography
@@ -430,22 +453,7 @@ def get_san_general_names(cert):
and should go away.
"""
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
gns = _pyasn1_get_san_general_names(cert)
GENERAL_NAME_CONSTRUCTORS = {
'rfc822Name': lambda x: cryptography.x509.RFC822Name(unicode(x)),
@@ -504,6 +512,17 @@ def _pyasn1_to_cryptography_oid(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):
"""Yield chunks of the specified size from the given string.
@@ -543,3 +562,23 @@ def format_datetime(t):
if t.tzinfo is None:
t = t.replace(tzinfo=UTC())
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 shutil
import base64
from cryptography.hazmat.primitives import serialization
from nss import nss
from nss.error import NSPRError
import cryptography.x509
from ipapython.dn import DN
from ipapython.ipa_log_manager import root_logger
@@ -543,59 +543,39 @@ class NSSDatabase(object):
Raises a ValueError if the certificate is invalid.
"""
certdb = cert = None
if nss.nss_is_initialized():
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()
cert = self.get_cert(nickname)
cert = x509.load_certificate(cert, x509.DER)
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):
certdb = cert = None
if nss.nss_is_initialized():
nss.nss_shutdown()
nss.nss_init(self.secdir)
cert = self.get_cert(nickname)
cert = x509.load_certificate(cert, x509.DER)
if not cert.subject:
raise ValueError("has empty subject")
try:
certdb = nss.get_default_certdb()
cert = nss.find_cert_from_nickname(nickname)
if not cert.subject:
raise ValueError("has empty subject")
try:
bc = cert.get_extension(nss.SEC_OID_X509_BASIC_CONSTRAINTS)
except KeyError:
raise ValueError("missing basic constraints")
bc = nss.BasicConstraints(bc.value)
if not bc.is_ca:
raise ValueError("not a CA certificate")
intended_usage = nss.certificateUsageSSLCA
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()
bc = cert.extensions.get_extension_for_class(
cryptography.x509.BasicConstraints)
except cryptography.x509.ExtensionNotFound:
raise ValueError("missing basic constraints")
if not bc.ca:
raise ValueError("not a CA certificate")
try:
self.run_certutil(['-V', '-n', nickname, '-u', 'L'])
except ipautil.CalledProcessError:
raise ValueError('invalid for a CA')
def publish_ca_cert(self, canickname, location):
args = ["-L", "-n", canickname, "-a"]

View File

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