mirror of
https://salsa.debian.org/freeipa-team/freeipa.git
synced 2025-02-25 18:55:28 -06:00
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:
committed by
Martin Basti
parent
274b0bcf5f
commit
9183cf2a75
@@ -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,
|
||||||
|
|||||||
@@ -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())
|
||||||
|
|||||||
@@ -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"]
|
||||||
|
|||||||
@@ -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',
|
||||||
|
|||||||
Reference in New Issue
Block a user