diff options
author | Fraser Tweedale <ftweedal@redhat.com> | 2016-10-13 17:12:31 +1000 |
---|---|---|
committer | David Kupka <dkupka@redhat.com> | 2016-11-10 10:21:47 +0100 |
commit | db116f73fe5fc199bb2e28103cf5e3e2a24eab4c (patch) | |
tree | ff1a043b376ec4d98b6399040a868e8b45725ee0 /ipalib/x509.py | |
parent | c57dc890b2bf447ab575f2e91249179bce3f05d5 (diff) | |
download | freeipa-db116f73fe5fc199bb2e28103cf5e3e2a24eab4c.tar.gz freeipa-db116f73fe5fc199bb2e28103cf5e3e2a24eab4c.tar.xz freeipa-db116f73fe5fc199bb2e28103cf5e3e2a24eab4c.zip |
x509: use python-cryptography to process certs
Update x509.load_certificate and related functions to return
python-cryptography ``Certificate`` objects. Update the call sites
accordingly, including removal of NSS initialisation code.
Also update GeneralName parsing code to return python-cryptography
GeneralName values, for consistency with other code that processes
GeneralNames. The new function, `get_san_general_names`, and
associated helper functions, can be removed when python-cryptography
provides a way to deal with unrecognised critical extensions.
Part of: https://fedorahosted.org/freeipa/ticket/6398
Reviewed-By: Jan Cholasta <jcholast@redhat.com>
Reviewed-By: Florence Blanc-Renaud <frenaud@redhat.com>
Diffstat (limited to 'ipalib/x509.py')
-rw-r--r-- | ipalib/x509.py | 327 |
1 files changed, 162 insertions, 165 deletions
diff --git a/ipalib/x509.py b/ipalib/x509.py index a807d1270..7f7a89c31 100644 --- a/ipalib/x509.py +++ b/ipalib/x509.py @@ -28,31 +28,27 @@ # # cert: the certificate is a PEM-encoded certificate # dercert: the certificate is DER-encoded -# nsscert: the certificate is an NSS Certificate object # rawcert: the cert is in an unknown format from __future__ import print_function import binascii -import collections -import os +import datetime +import ipaddress import sys import base64 import re +from cryptography.hazmat.backends import default_backend import cryptography.x509 -import nss.nss as nss -from nss.error import NSPRError from pyasn1.type import univ, char, namedtype, tag from pyasn1.codec.der import decoder, encoder from pyasn1_modules import rfc2459 import six from ipalib import api -from ipalib import _ from ipalib import util from ipalib import errors -from ipaplatform.paths import paths from ipapython.dn import DN if six.PY3: @@ -95,32 +91,16 @@ def strip_header(pem): return pem -def initialize_nss_database(dbdir=None): - """ - Initializes NSS database, if not initialized yet. Uses a proper database - directory (.ipa/alias or HTTPD_ALIAS_DIR), depending on the value of - api.env.in_tree. - """ - if not nss.nss_is_initialized(): - if dbdir is None: - if 'in_tree' in api.env: - if api.env.in_tree: - dbdir = api.env.dot_ipa + os.sep + 'alias' - else: - dbdir = paths.HTTPD_ALIAS_DIR - nss.nss_init(dbdir) - else: - nss.nss_init_nodb() - else: - nss.nss_init(dbdir) - -def load_certificate(data, datatype=PEM, dbdir=None): +def load_certificate(data, datatype=PEM): """ - Given a base64-encoded certificate, with or without the - header/footer, return a request object. + Load an X.509 certificate. + + :param datatype: PEM for base64-encoded data (with or without header), + or DER + :return: a python-cryptography ``CertificateSigningRequest`` object. + :raises: ``ValueError`` if unable to load the certificate. - Returns a nss.Certificate type """ if type(data) in (tuple, list): data = data[0] @@ -129,82 +109,50 @@ def load_certificate(data, datatype=PEM, dbdir=None): data = strip_header(data) data = base64.b64decode(data) - initialize_nss_database(dbdir=dbdir) + return cryptography.x509.load_der_x509_certificate(data, default_backend()) - if six.PY2: - return nss.Certificate(buffer(data)) # pylint: disable=buffer-builtin - else: - # In python 3 , `bytes` has the buffer interface - return nss.Certificate(data) def load_certificate_from_file(filename, dbdir=None): """ Load a certificate from a PEM file. - Returns a nss.Certificate type - """ - fd = open(filename, 'r') - data = fd.read() - fd.close() - - return load_certificate(data, PEM, dbdir) - -def load_certificate_list(data, dbdir=None): - certs = PEM_REGEX.findall(data) - certs = [load_certificate(cert, PEM, dbdir) for cert in certs] - return certs + Returns a python-cryptography ``Certificate`` object. -def load_certificate_list_from_file(filename, dbdir=None): """ - Load a certificate list from a PEM file. + with open(filename, mode='rb') as f: + return load_certificate(f.read(), PEM) + - Returns a list of nss.Certificate objects. +def load_certificate_list(data): """ - fd = open(filename, 'r') - data = fd.read() - fd.close() + Load a certificate list from a sequence of concatenated PEMs. - return load_certificate_list(data, dbdir) + Return a list of python-cryptography ``Certificate`` objects. -def get_subject(certificate, datatype=PEM, dbdir=None): - """ - Load an X509.3 certificate and get the subject. """ + certs = PEM_REGEX.findall(data) + certs = [load_certificate(cert, PEM) for cert in certs] + return certs - nsscert = load_certificate(certificate, datatype, dbdir) - subject = nsscert.subject - del(nsscert) - return subject -def get_issuer(certificate, datatype=PEM, dbdir=None): - """ - Load an X509.3 certificate and get the issuer. +def load_certificate_list_from_file(filename): """ + Load a certificate list from a PEM file. - nsscert = load_certificate(certificate, datatype, dbdir) - issuer = nsscert.issuer - del(nsscert) - return issuer + Return a list of python-cryptography ``Certificate`` objects. -def get_serial_number(certificate, datatype=PEM, dbdir=None): """ - Return the decimal value of the serial number. - """ - nsscert = load_certificate(certificate, datatype, dbdir) - serial_number = nsscert.serial_number - del(nsscert) - return serial_number + with open(filename) as f: + return load_certificate_list(f.read()) + -def is_self_signed(certificate, datatype=PEM, dbdir=None): - nsscert = load_certificate(certificate, datatype, dbdir) - self_signed = (nsscert.issuer == nsscert.subject) - del nsscert - return self_signed +def is_self_signed(certificate, datatype=PEM): + cert = load_certificate(certificate, datatype) + return cert.issuer == cert.subject def _get_der_field(cert, datatype, dbdir, field): - cert = load_certificate(cert, datatype, dbdir) - cert = cert.der_data + cert = normalize_certificate(cert) cert = decoder.decode(cert, rfc2459.Certificate())[0] field = cert['tbsCertificate'][field] field = encoder.encode(field) @@ -222,20 +170,17 @@ def get_der_serial_number(cert, datatype=PEM, dbdir=None): def get_der_public_key_info(cert, datatype=PEM, dbdir=None): return _get_der_field(cert, datatype, dbdir, 'subjectPublicKeyInfo') -def get_ext_key_usage(certificate, datatype=PEM, dbdir=None): - nsscert = load_certificate(certificate, datatype, dbdir) - if not nsscert.extensions: - return None - for ext in nsscert.extensions: - if ext.oid_tag == nss.SEC_OID_X509_EXT_KEY_USAGE: - break - else: +def get_ext_key_usage(certificate, datatype=PEM): + cert = load_certificate(certificate, datatype) + try: + eku = cert.extensions.get_extension_for_oid( + cryptography.x509.oid.ExtensionOID.EXTENDED_KEY_USAGE).value + except cryptography.x509.ExtensionNotFound: return None - eku = nss.x509_ext_key_usage(ext.value, nss.AsDottedDecimal) - eku = set(o[4:] for o in eku) - return eku + return set(oid.dotted_string for oid in eku) + def make_pem(data): """ @@ -270,27 +215,21 @@ def normalize_certificate(rawcert): else: dercert = rawcert - # At this point we should have a certificate, either because the data - # was base64-encoded and now its not or it came in as DER format. - # Let's decode it and see. Fetching the serial number will pass the - # certificate through the NSS DER parser. + # At this point we should have a DER certificate. + # Attempt to decode it. validate_certificate(dercert, datatype=DER) return dercert -def validate_certificate(cert, datatype=PEM, dbdir=None): +def validate_certificate(cert, datatype=PEM): """ - Perform certificate validation by trying to load it into NSS database + Perform cert validation by trying to load it via python-cryptography. """ try: - load_certificate(cert, datatype=datatype, dbdir=dbdir) - except NSPRError as nsprerr: - if nsprerr.errno == -8183: # SEC_ERROR_BAD_DER - raise errors.CertificateFormatError( - error=_('improperly formatted DER-encoded certificate')) - else: - raise errors.CertificateFormatError(error=str(nsprerr)) + load_certificate(cert, datatype=datatype) + except ValueError as e: + raise errors.CertificateFormatError(error=str(e)) def write_certificate(rawcert, filename): @@ -379,56 +318,6 @@ def _decode_krb5principalname(data): return name -GeneralNameInfo = collections.namedtuple( - 'GeneralNameInfo', ('type', 'desc', 'value', 'der_value')) - - -def decode_generalnames(secitem): - """ - Decode a GeneralNames object (this the data for the Subject - Alt Name and Issuer Alt Name extensions, among others). - - ``secitem`` - The input is the DER-encoded extension data, without the - OCTET STRING header, as an nss SecItem object. - - Return a list of ``GeneralNameInfo`` namedtuples. The - ``der_value`` field is set for otherNames, otherwise it is - ``None``. - - """ - nss_names = nss.x509_alt_name(secitem, repr_kind=nss.AsObject) - asn1_names = decoder.decode( - secitem.data, asn1Spec=rfc2459.SubjectAltName())[0] - names = [] - for nss_name, asn1_name in zip(nss_names, asn1_names): - # NOTE: we use the NSS enum to identify the name type. - # (For otherName we also tuple it up with the type-id OID). - # The enum does not correspond exactly to the ASN.1 tags. - # If we ever want to switch to using the true tag numbers, - # the expression to get the tag is: - # - # asn1_name.getComponent().getTagSet()[0].asTuple()[2] - # - if nss_name.type_enum == nss.certOtherName: - oid = str(asn1_name['otherName']['type-id']) - nametype = (nss_name.type_enum, oid) - der_value = asn1_name['otherName']['value'].asOctets() - else: - nametype = nss_name.type_enum - der_value = None - - if nametype == (nss.certOtherName, SAN_KRB5PRINCIPALNAME): - name = _decode_krb5principalname(asn1_name['otherName']['value']) - else: - name = nss_name.name - - gni = GeneralNameInfo(nametype, nss_name.type_string, name, der_value) - names.append(gni) - - return names - - class KRB5PrincipalName(cryptography.x509.general_name.OtherName): def __init__(self, type_id, value): super(KRB5PrincipalName, self).__init__(type_id, value) @@ -464,6 +353,100 @@ def process_othernames(gns): yield gn +def get_san_general_names(cert): + """ + Return SAN general names from a python-cryptography + certificate object. If the SAN extension is not present, + return an empty sequence. + + Because python-cryptography does not yet provide a way to + handle unrecognised critical extensions (which may occur), + we must parse the certificate and extract the General Names. + For uniformity with other code, we manually construct values + of python-crytography GeneralName subtypes. + + python-cryptography does not yet provide types for + ediPartyName or x400Address, so we drop these name types. + + otherNames are NOT instantiated to more specific types where + the type is known. Use ``process_othernames`` to do that. + + When python-cryptography can handle certs with unrecognised + critical extensions and implements ediPartyName and + x400Address, this function (and helpers) will be redundant + and should go away. + + """ + tbs = decoder.decode( + cert.tbs_certificate_bytes, + asn1Spec=rfc2459.TBSCertificate() + )[0] + OID_SAN = univ.ObjectIdentifier('2.5.29.17') + gns = [] + for ext in tbs['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 = { + 'rfc822Name': lambda x: cryptography.x509.RFC822Name(unicode(x)), + 'dNSName': lambda x: cryptography.x509.DNSName(unicode(x)), + 'directoryName': _pyasn1_to_cryptography_directoryname, + 'registeredID': _pyasn1_to_cryptography_registeredid, + 'iPAddress': _pyasn1_to_cryptography_ipaddress, + 'uniformResourceIdentifier': + lambda x: cryptography.x509.UniformResourceIdentifier(unicode(x)), + 'otherName': _pyasn1_to_cryptography_othername, + } + + result = [] + + for gn in gns: + gn_type = gn.getName() + if gn_type in GENERAL_NAME_CONSTRUCTORS: + result.append( + GENERAL_NAME_CONSTRUCTORS[gn_type](gn.getComponent())) + + return result + + +def _pyasn1_to_cryptography_directoryname(dn): + attrs = [] + + # Name is CHOICE { RDNSequence } (only one possibility) + for rdn in dn.getComponent(): + for ava in rdn: + attr = cryptography.x509.NameAttribute( + _pyasn1_to_cryptography_oid(ava['type']), + unicode(decoder.decode(ava['value'])[0]) + ) + attrs.append(attr) + + return cryptography.x509.DirectoryName(cryptography.x509.Name(attrs)) + + +def _pyasn1_to_cryptography_registeredid(oid): + return cryptography.x509.RegisteredID(_pyasn1_to_cryptography_oid(oid)) + + +def _pyasn1_to_cryptography_ipaddress(octet_string): + return cryptography.x509.IPAddress( + ipaddress.ip_address(bytes(octet_string))) + + +def _pyasn1_to_cryptography_othername(on): + return cryptography.x509.OtherName( + _pyasn1_to_cryptography_oid(on['type-id']), + bytes(on['value']) + ) + + +def _pyasn1_to_cryptography_oid(oid): + return cryptography.x509.ObjectIdentifier(str(oid)) + + def chunk(size, s): """Yield chunks of the specified size from the given string. @@ -486,20 +469,34 @@ def to_hex_with_colons(bs): return add_colons(binascii.hexlify(bs).decode('utf-8')) +class UTC(datetime.tzinfo): + ZERO = datetime.timedelta(0) + + def tzname(self, dt): + return "UTC" + + def utcoffset(self, dt): + return self.ZERO + + def dst(self, dt): + return self.ZERO + + +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")) + + if __name__ == '__main__': # this can be run with: # python ipalib/x509.py < /etc/ipa/ca.crt - api.bootstrap() - api.finalize() - - nss.nss_init_nodb() - - # Read PEM certs from stdin and print out its components + # Read PEM cert from stdin and print out its components certlines = sys.stdin.readlines() cert = ''.join(certlines) - nsscert = load_certificate(cert) + cert = load_certificate(cert) - print(nsscert) + print(cert) |