From e7588ab2dc73e7f66ebc6cdcfb99470540e37731 Mon Sep 17 00:00:00 2001 From: Ben Lipton Date: Apr 03 2017 07:46:30 +0000 Subject: csrgen: Modify cert_get_requestdata to return a CertificationRequestInfo Also modify cert_request to use this new format. Note, only PEM private keys are supported for now. NSS databases are not. https://pagure.io/freeipa/issue/4899 Reviewed-By: Jan Cholasta --- diff --git a/ipaclient/csrgen.py b/ipaclient/csrgen.py index eca99a1..04d2821 100644 --- a/ipaclient/csrgen.py +++ b/ipaclient/csrgen.py @@ -7,13 +7,22 @@ import errno import json import os.path import pipes +import subprocess import traceback import pkg_resources +from cryptography.hazmat.backends import default_backend +from cryptography.hazmat.primitives.asymmetric import padding +from cryptography.hazmat.primitives import hashes +from cryptography.hazmat.primitives.serialization import ( + load_pem_private_key, Encoding, PublicFormat) import jinja2 import jinja2.ext import jinja2.sandbox +from pyasn1.codec.der import decoder, encoder +from pyasn1.type import univ +from pyasn1_modules import rfc2314 import six from ipalib import api @@ -56,7 +65,8 @@ class IPAExtension(jinja2.ext.Extension): def required(self, data, name): if not data: raise errors.CSRTemplateError( - reason=_('Required CSR generation rule %(name)s is missing data') % + reason=_( + 'Required CSR generation rule %(name)s is missing data') % {'name': name}) return data @@ -373,3 +383,66 @@ class CSRGenerator(object): 'Template error when formatting certificate data')) return config + + +class CSRLibraryAdaptor(object): + def get_subject_public_key_info(self): + raise NotImplementedError('Use a subclass of CSRLibraryAdaptor') + + def sign_csr(self, certification_request_info): + """Sign a CertificationRequestInfo. + + Returns: str, a DER-encoded signed CSR. + """ + raise NotImplementedError('Use a subclass of CSRLibraryAdaptor') + + +class OpenSSLAdaptor(object): + def __init__(self, key_filename, password_filename): + self.key_filename = key_filename + self.password_filename = password_filename + + def key(self): + with open(self.key_filename, 'r') as key_file: + key_bytes = key_file.read() + password = None + if self.password_filename is not None: + with open(self.password_filename, 'r') as password_file: + password = password_file.read().strip() + + key = load_pem_private_key(key_bytes, password, default_backend()) + return key + + def get_subject_public_key_info(self): + pubkey_info = self.key().public_key().public_bytes( + Encoding.DER, PublicFormat.SubjectPublicKeyInfo) + return pubkey_info + + def sign_csr(self, certification_request_info): + reqinfo = decoder.decode( + certification_request_info, rfc2314.CertificationRequestInfo())[0] + csr = rfc2314.CertificationRequest() + csr.setComponentByName('certificationRequestInfo', reqinfo) + + algorithm = rfc2314.SignatureAlgorithmIdentifier() + algorithm.setComponentByName( + 'algorithm', univ.ObjectIdentifier( + '1.2.840.113549.1.1.11')) # sha256WithRSAEncryption + csr.setComponentByName('signatureAlgorithm', algorithm) + + signature = self.key().sign( + certification_request_info, + padding.PKCS1v15(), + hashes.SHA256() + ) + asn1sig = univ.BitString("'%s'H" % signature.encode('hex')) + csr.setComponentByName('signature', asn1sig) + return encoder.encode(csr) + + +class NSSAdaptor(object): + def get_subject_public_key_info(self): + raise NotImplementedError('NSS is not yet supported') + + def sign_csr(self, certification_request_info): + raise NotImplementedError('NSS is not yet supported') diff --git a/ipaclient/csrgen_ffi.py b/ipaclient/csrgen_ffi.py new file mode 100644 index 0000000..c45db5f --- /dev/null +++ b/ipaclient/csrgen_ffi.py @@ -0,0 +1,291 @@ +#!/usr/bin/python + +from cffi import FFI +import ctypes.util + +from ipalib import errors + +_ffi = FFI() + +_ffi.cdef(''' +typedef ... CONF; +typedef ... CONF_METHOD; +typedef ... BIO; +typedef ... ipa_STACK_OF_CONF_VALUE; + +/* openssl/conf.h */ +typedef struct { + char *section; + char *name; + char *value; +} CONF_VALUE; + +CONF *NCONF_new(CONF_METHOD *meth); +void NCONF_free(CONF *conf); +int NCONF_load_bio(CONF *conf, BIO *bp, long *eline); +ipa_STACK_OF_CONF_VALUE *NCONF_get_section(const CONF *conf, + const char *section); +char *NCONF_get_string(const CONF *conf, const char *group, const char *name); + +/* openssl/safestack.h */ +// int sk_CONF_VALUE_num(ipa_STACK_OF_CONF_VALUE *); +// CONF_VALUE *sk_CONF_VALUE_value(ipa_STACK_OF_CONF_VALUE *, int); + +/* openssl/stack.h */ +typedef ... _STACK; + +int sk_num(const _STACK *); +void *sk_value(const _STACK *, int); + +/* openssl/bio.h */ +BIO *BIO_new_mem_buf(const void *buf, int len); +int BIO_free(BIO *a); + +/* openssl/asn1.h */ +typedef struct ASN1_ENCODING_st { + unsigned char *enc; /* DER encoding */ + long len; /* Length of encoding */ + int modified; /* set to 1 if 'enc' is invalid */ +} ASN1_ENCODING; + +/* openssl/evp.h */ +typedef ... EVP_PKEY; + +void EVP_PKEY_free(EVP_PKEY *pkey); + +/* openssl/x509.h */ +typedef ... ASN1_INTEGER; +typedef ... ASN1_BIT_STRING; +typedef ... X509; +typedef ... X509_ALGOR; +typedef ... X509_CRL; +typedef ... X509_NAME; +typedef ... X509_PUBKEY; +typedef ... ipa_STACK_OF_X509_ATTRIBUTE; + +typedef struct X509_req_info_st { + ASN1_ENCODING enc; + ASN1_INTEGER *version; + X509_NAME *subject; + X509_PUBKEY *pubkey; + /* d=2 hl=2 l= 0 cons: cont: 00 */ + ipa_STACK_OF_X509_ATTRIBUTE *attributes; /* [ 0 ] */ +} X509_REQ_INFO; + +typedef struct X509_req_st { + X509_REQ_INFO *req_info; + X509_ALGOR *sig_alg; + ASN1_BIT_STRING *signature; + int references; +} X509_REQ; + +X509_REQ *X509_REQ_new(void); +void X509_REQ_free(X509_REQ *); +EVP_PKEY *d2i_PUBKEY_bio(BIO *bp, EVP_PKEY **a); +int X509_REQ_set_pubkey(X509_REQ *x, EVP_PKEY *pkey); +int X509_NAME_add_entry_by_txt(X509_NAME *name, const char *field, int type, + const unsigned char *bytes, int len, int loc, + int set); +int X509_NAME_entry_count(X509_NAME *name); +int i2d_X509_REQ_INFO(X509_REQ_INFO *a, unsigned char **out); \ + +/* openssl/x509v3.h */ +typedef ... X509V3_CONF_METHOD; + +typedef struct v3_ext_ctx { + int flags; + X509 *issuer_cert; + X509 *subject_cert; + X509_REQ *subject_req; + X509_CRL *crl; + X509V3_CONF_METHOD *db_meth; + void *db; +} X509V3_CTX; + +void X509V3_set_ctx(X509V3_CTX *ctx, X509 *issuer, X509 *subject, + X509_REQ *req, X509_CRL *crl, int flags); +void X509V3_set_nconf(X509V3_CTX *ctx, CONF *conf); +int X509V3_EXT_REQ_add_nconf(CONF *conf, X509V3_CTX *ctx, char *section, + X509_REQ *req); + +/* openssl/x509v3.h */ +unsigned long ERR_get_error(void); +char *ERR_error_string(unsigned long e, char *buf); +''') + +_libcrypto = _ffi.dlopen(ctypes.util.find_library('crypto')) + +NULL = _ffi.NULL + +# openssl/conf.h +NCONF_new = _libcrypto.NCONF_new +NCONF_free = _libcrypto.NCONF_free +NCONF_load_bio = _libcrypto.NCONF_load_bio +NCONF_get_section = _libcrypto.NCONF_get_section +NCONF_get_string = _libcrypto.NCONF_get_string + +# openssl/stack.h +sk_num = _libcrypto.sk_num +sk_value = _libcrypto.sk_value + + +def sk_CONF_VALUE_num(sk): + return sk_num(_ffi.cast("_STACK *", sk)) + + +def sk_CONF_VALUE_value(sk, i): + return _ffi.cast("CONF_VALUE *", sk_value(_ffi.cast("_STACK *", sk), i)) + + +# openssl/bio.h +BIO_new_mem_buf = _libcrypto.BIO_new_mem_buf +BIO_free = _libcrypto.BIO_free + +# openssl/x509.h +X509_REQ_new = _libcrypto.X509_REQ_new +X509_REQ_free = _libcrypto.X509_REQ_free +X509_REQ_set_pubkey = _libcrypto.X509_REQ_set_pubkey +d2i_PUBKEY_bio = _libcrypto.d2i_PUBKEY_bio +i2d_X509_REQ_INFO = _libcrypto.i2d_X509_REQ_INFO +X509_NAME_add_entry_by_txt = _libcrypto.X509_NAME_add_entry_by_txt +X509_NAME_entry_count = _libcrypto.X509_NAME_entry_count + + +def X509_REQ_get_subject_name(req): + return req.req_info.subject + +# openssl/evp.h +EVP_PKEY_free = _libcrypto.EVP_PKEY_free + +# openssl/asn1.h +MBSTRING_UTF8 = 0x1000 + +# openssl/x509v3.h +X509V3_set_ctx = _libcrypto.X509V3_set_ctx +X509V3_set_nconf = _libcrypto.X509V3_set_nconf +X509V3_EXT_REQ_add_nconf = _libcrypto.X509V3_EXT_REQ_add_nconf + +# openssl/err.h +ERR_get_error = _libcrypto.ERR_get_error +ERR_error_string = _libcrypto.ERR_error_string + + +def _raise_openssl_errors(): + msgs = [] + + code = ERR_get_error() + while code != 0: + msg = ERR_error_string(code, NULL) + msgs.append(_ffi.string(msg)) + code = ERR_get_error() + + raise errors.CSRTemplateError(reason='\n'.join(msgs)) + + +def _parse_dn_section(subj, dn_sk): + for i in range(sk_CONF_VALUE_num(dn_sk)): + v = sk_CONF_VALUE_value(dn_sk, i) + rdn_type = _ffi.string(v.name) + + # Skip past any leading X. X: X, etc to allow for multiple instances + for idx, c in enumerate(rdn_type): + if c in ':,.': + if idx+1 < len(rdn_type): + rdn_type = rdn_type[idx+1:] + break + if rdn_type.startswith('+'): + rdn_type = rdn_type[1:] + mval = -1 + else: + mval = 0 + if not X509_NAME_add_entry_by_txt( + subj, rdn_type, MBSTRING_UTF8, v.value, -1, -1, mval): + _raise_openssl_errors() + + if not X509_NAME_entry_count(subj): + raise errors.CSRTemplateError( + reason='error, subject in config file is empty') + + +def build_requestinfo(config, public_key_info): + reqdata = NULL + req = NULL + nconf_bio = NULL + pubkey_bio = NULL + pubkey = NULL + + try: + reqdata = NCONF_new(NULL) + if reqdata == NULL: + _raise_openssl_errors() + + nconf_bio = BIO_new_mem_buf(config, len(config)) + errorline = _ffi.new('long[1]', [-1]) + i = NCONF_load_bio(reqdata, nconf_bio, errorline) + if i < 0: + if errorline[0] < 0: + raise errors.CSRTemplateError(reason="Can't load config file") + else: + raise errors.CSRTemplateError( + reason='Error on line %d of config file' % errorline[0]) + + dn_sect = NCONF_get_string(reqdata, 'req', 'distinguished_name') + if dn_sect == NULL: + raise errors.CSRTemplateError( + reason='Unable to find "distinguished_name" key in config') + + dn_sk = NCONF_get_section(reqdata, dn_sect) + if dn_sk == NULL: + raise errors.CSRTemplateError( + reason='Unable to find "%s" section in config' % + _ffi.string(dn_sect)) + + pubkey_bio = BIO_new_mem_buf(public_key_info, len(public_key_info)) + pubkey = d2i_PUBKEY_bio(pubkey_bio, NULL) + if pubkey == NULL: + _raise_openssl_errors() + + req = X509_REQ_new() + if req == NULL: + _raise_openssl_errors() + + subject = X509_REQ_get_subject_name(req) + + _parse_dn_section(subject, dn_sk) + + if not X509_REQ_set_pubkey(req, pubkey): + _raise_openssl_errors() + + ext_ctx = _ffi.new("X509V3_CTX[1]") + X509V3_set_ctx(ext_ctx, NULL, NULL, req, NULL, 0) + X509V3_set_nconf(ext_ctx, reqdata) + + extn_section = NCONF_get_string(reqdata, "req", "req_extensions") + if extn_section != NULL: + if not X509V3_EXT_REQ_add_nconf( + reqdata, ext_ctx, extn_section, req): + _raise_openssl_errors() + + der_len = i2d_X509_REQ_INFO(req.req_info, NULL) + if der_len < 0: + _raise_openssl_errors() + + der_buf = _ffi.new("unsigned char[%d]" % der_len) + der_out = _ffi.new("unsigned char **", der_buf) + der_len = i2d_X509_REQ_INFO(req.req_info, der_out) + if der_len < 0: + _raise_openssl_errors() + + return _ffi.buffer(der_buf, der_len) + + finally: + if reqdata != NULL: + NCONF_free(reqdata) + if req != NULL: + X509_REQ_free(req) + if nconf_bio != NULL: + BIO_free(nconf_bio) + if pubkey_bio != NULL: + BIO_free(pubkey_bio) + if pubkey != NULL: + EVP_PKEY_free(pubkey) diff --git a/ipaclient/plugins/cert.py b/ipaclient/plugins/cert.py index 9ec6970..a4ee9a9 100644 --- a/ipaclient/plugins/cert.py +++ b/ipaclient/plugins/cert.py @@ -20,11 +20,10 @@ # along with this program. If not, see . import base64 -import subprocess -from tempfile import NamedTemporaryFile as NTF import six +from ipaclient import csrgen from ipaclient.frontend import MethodOverride from ipalib import errors from ipalib import x509 @@ -108,54 +107,40 @@ class cert_request(CertRetrieveOverride): if csr is None: if database: - helper = u'certutil' - helper_args = ['-d', database] - if password_file: - helper_args += ['-f', password_file] + adaptor = csrgen.NSSAdaptor(database, password_file) elif private_key: - helper = u'openssl' - helper_args = [private_key] - if password_file: - helper_args += ['-passin', 'file:%s' % password_file] + adaptor = csrgen.OpenSSLAdaptor(private_key, password_file) else: raise errors.InvocationError( message=u"One of 'database' or 'private_key' is required") - with NTF() as scriptfile, NTF() as csrfile: - # If csr_profile_id is passed, that takes precedence. - # Otherwise, use profile_id. If neither are passed, the default - # in cert_get_requestdata will be used. - profile_id = csr_profile_id - if profile_id is None: - profile_id = options.get('profile_id') - - self.api.Command.cert_get_requestdata( - profile_id=profile_id, - principal=options.get('principal'), - out=unicode(scriptfile.name), - helper=helper) - - helper_cmd = [ - 'bash', '-e', scriptfile.name, csrfile.name] + helper_args - - try: - subprocess.check_output(helper_cmd) - except subprocess.CalledProcessError as e: - raise errors.CertificateOperationError( - error=( - _('Error running "%(cmd)s" to generate CSR:' - ' %(err)s') % - {'cmd': ' '.join(helper_cmd), 'err': e.output})) - - try: - csr = unicode(csrfile.read()) - except IOError as e: - raise errors.CertificateOperationError( - error=(_('Unable to read generated CSR file: %(err)s') - % {'err': e})) - if not csr: - raise errors.CertificateOperationError( - error=(_('Generated CSR was empty'))) + pubkey_info = adaptor.get_subject_public_key_info() + pubkey_info_b64 = base64.b64encode(pubkey_info) + + # If csr_profile_id is passed, that takes precedence. + # Otherwise, use profile_id. If neither are passed, the default + # in cert_get_requestdata will be used. + profile_id = csr_profile_id + if profile_id is None: + profile_id = options.get('profile_id') + + response = self.api.Command.cert_get_requestdata( + profile_id=profile_id, + principal=options.get('principal'), + public_key_info=unicode(pubkey_info_b64)) + + req_info_b64 = response['result']['request_info'] + req_info = base64.b64decode(req_info_b64) + + csr = adaptor.sign_csr(req_info) + + if not csr: + raise errors.CertificateOperationError( + error=(_('Generated CSR was empty'))) + + # cert_request requires the CSR to be base64-encoded (but PEM + # header and footer are not required) + csr = unicode(base64.b64encode(csr)) else: if database is not None or private_key is not None: raise errors.MutuallyExclusiveError(reason=_( diff --git a/ipaclient/plugins/csrgen.py b/ipaclient/plugins/csrgen.py index 15ed791..568a79f 100644 --- a/ipaclient/plugins/csrgen.py +++ b/ipaclient/plugins/csrgen.py @@ -2,15 +2,18 @@ # Copyright (C) 2016 FreeIPA Contributors see COPYING for license # +import base64 + import six -from ipaclient.csrgen import CSRGenerator, FileRuleProvider +from ipaclient import csrgen +from ipaclient import csrgen_ffi from ipalib import api from ipalib import errors from ipalib import output from ipalib import util from ipalib.frontend import Local, Str -from ipalib.parameters import Principal +from ipalib.parameters import File, Principal from ipalib.plugable import Registry from ipalib.text import _ from ipapython import dogtag @@ -43,15 +46,14 @@ class cert_get_requestdata(Local): label=_('Profile ID'), doc=_('CSR Generation Profile to use'), ), - Str( - 'helper', - label=_('Name of CSR generation tool'), - doc=_('Name of tool (e.g. openssl, certutil) that will be used to' - ' create CSR'), + File( + 'public_key_info', + label=_('Subject Public Key Info'), + doc=_('DER-encoded SubjectPublicKeyInfo structure'), ), Str( 'out?', - doc=_('Write CSR generation script to file'), + doc=_('Write CertificationRequestInfo to file'), ), ) @@ -65,8 +67,8 @@ class cert_get_requestdata(Local): has_output_params = ( Str( - 'script', - label=_('Generation script'), + 'request_info', + label=_('CertificationRequestInfo structure'), ) ) @@ -78,7 +80,8 @@ class cert_get_requestdata(Local): profile_id = options.get('profile_id') if profile_id is None: profile_id = dogtag.DEFAULT_PROFILE - helper = options.get('helper') + public_key_info = options.get('public_key_info') + public_key_info = base64.b64decode(public_key_info) if self.api.env.in_server: backend = self.api.Backend.ldap2 @@ -103,16 +106,18 @@ class cert_get_requestdata(Local): principal_obj = principal_obj['result'] config = api.Command.config_show()['result'] - generator = CSRGenerator(FileRuleProvider()) + generator = csrgen.CSRGenerator(csrgen.FileRuleProvider()) - script = generator.csr_config(principal_obj, config, profile_id) + csr_config = generator.csr_config(principal_obj, config, profile_id) + request_info = base64.b64encode(csrgen_ffi.build_requestinfo( + csr_config.encode('utf8'), public_key_info)) result = {} if 'out' in options: with open(options['out'], 'wb') as f: - f.write(script) + f.write(request_info) else: - result = dict(script=script) + result = dict(request_info=request_info) return dict( result=result