freeipa

FreeIPA is an integrated Identity and Authentication solution for Linux/UNIX networked environments.  |  http://www.freeipa.org/

Commit e7588ab csrgen: Modify cert_get_requestdata to return a CertificationRequestInfo

4 files Authored by benlipton a year ago , Committed by jcholast a year ago ,
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 <jcholast@redhat.com>

    
  1 @@ -7,13 +7,22 @@
  2   import json
  3   import os.path
  4   import pipes
  5 + import subprocess
  6   import traceback
  7   
  8   import pkg_resources
  9   
 10 + from cryptography.hazmat.backends import default_backend
 11 + from cryptography.hazmat.primitives.asymmetric import padding
 12 + from cryptography.hazmat.primitives import hashes
 13 + from cryptography.hazmat.primitives.serialization import (
 14 +     load_pem_private_key, Encoding, PublicFormat)
 15   import jinja2
 16   import jinja2.ext
 17   import jinja2.sandbox
 18 + from pyasn1.codec.der import decoder, encoder
 19 + from pyasn1.type import univ
 20 + from pyasn1_modules import rfc2314
 21   import six
 22   
 23   from ipalib import api
 24 @@ -56,7 +65,8 @@
 25       def required(self, data, name):
 26           if not data:
 27               raise errors.CSRTemplateError(
 28 -                 reason=_('Required CSR generation rule %(name)s is missing data') %
 29 +                 reason=_(
 30 +                     'Required CSR generation rule %(name)s is missing data') %
 31                   {'name': name})
 32           return data
 33   
 34 @@ -373,3 +383,66 @@
 35                   'Template error when formatting certificate data'))
 36   
 37           return config
 38 + 
 39 + 
 40 + class CSRLibraryAdaptor(object):
 41 +     def get_subject_public_key_info(self):
 42 +         raise NotImplementedError('Use a subclass of CSRLibraryAdaptor')
 43 + 
 44 +     def sign_csr(self, certification_request_info):
 45 +         """Sign a CertificationRequestInfo.
 46 + 
 47 +         Returns: str, a DER-encoded signed CSR.
 48 +         """
 49 +         raise NotImplementedError('Use a subclass of CSRLibraryAdaptor')
 50 + 
 51 + 
 52 + class OpenSSLAdaptor(object):
 53 +     def __init__(self, key_filename, password_filename):
 54 +         self.key_filename = key_filename
 55 +         self.password_filename = password_filename
 56 + 
 57 +     def key(self):
 58 +         with open(self.key_filename, 'r') as key_file:
 59 +             key_bytes = key_file.read()
 60 +         password = None
 61 +         if self.password_filename is not None:
 62 +             with open(self.password_filename, 'r') as password_file:
 63 +                 password = password_file.read().strip()
 64 + 
 65 +         key = load_pem_private_key(key_bytes, password, default_backend())
 66 +         return key
 67 + 
 68 +     def get_subject_public_key_info(self):
 69 +         pubkey_info = self.key().public_key().public_bytes(
 70 +             Encoding.DER, PublicFormat.SubjectPublicKeyInfo)
 71 +         return pubkey_info
 72 + 
 73 +     def sign_csr(self, certification_request_info):
 74 +         reqinfo = decoder.decode(
 75 +             certification_request_info, rfc2314.CertificationRequestInfo())[0]
 76 +         csr = rfc2314.CertificationRequest()
 77 +         csr.setComponentByName('certificationRequestInfo', reqinfo)
 78 + 
 79 +         algorithm = rfc2314.SignatureAlgorithmIdentifier()
 80 +         algorithm.setComponentByName(
 81 +             'algorithm', univ.ObjectIdentifier(
 82 +                 '1.2.840.113549.1.1.11'))  # sha256WithRSAEncryption
 83 +         csr.setComponentByName('signatureAlgorithm', algorithm)
 84 + 
 85 +         signature = self.key().sign(
 86 +             certification_request_info,
 87 +             padding.PKCS1v15(),
 88 +             hashes.SHA256()
 89 +         )
 90 +         asn1sig = univ.BitString("'%s'H" % signature.encode('hex'))
 91 +         csr.setComponentByName('signature', asn1sig)
 92 +         return encoder.encode(csr)
 93 + 
 94 + 
 95 + class NSSAdaptor(object):
 96 +     def get_subject_public_key_info(self):
 97 +         raise NotImplementedError('NSS is not yet supported')
 98 + 
 99 +     def sign_csr(self, certification_request_info):
100 +         raise NotImplementedError('NSS is not yet supported')
  1 @@ -0,0 +1,291 @@
  2 + #!/usr/bin/python
  3 + 
  4 + from cffi import FFI
  5 + import ctypes.util
  6 + 
  7 + from ipalib import errors
  8 + 
  9 + _ffi = FFI()
 10 + 
 11 + _ffi.cdef('''
 12 + typedef ... CONF;
 13 + typedef ... CONF_METHOD;
 14 + typedef ... BIO;
 15 + typedef ... ipa_STACK_OF_CONF_VALUE;
 16 + 
 17 + /* openssl/conf.h */
 18 + typedef struct {
 19 +     char *section;
 20 +     char *name;
 21 +     char *value;
 22 + } CONF_VALUE;
 23 + 
 24 + CONF *NCONF_new(CONF_METHOD *meth);
 25 + void NCONF_free(CONF *conf);
 26 + int NCONF_load_bio(CONF *conf, BIO *bp, long *eline);
 27 + ipa_STACK_OF_CONF_VALUE *NCONF_get_section(const CONF *conf,
 28 +                                         const char *section);
 29 + char *NCONF_get_string(const CONF *conf, const char *group, const char *name);
 30 + 
 31 + /* openssl/safestack.h */
 32 + // int sk_CONF_VALUE_num(ipa_STACK_OF_CONF_VALUE *);
 33 + // CONF_VALUE *sk_CONF_VALUE_value(ipa_STACK_OF_CONF_VALUE *, int);
 34 + 
 35 + /* openssl/stack.h */
 36 + typedef ... _STACK;
 37 + 
 38 + int sk_num(const _STACK *);
 39 + void *sk_value(const _STACK *, int);
 40 + 
 41 + /* openssl/bio.h */
 42 + BIO *BIO_new_mem_buf(const void *buf, int len);
 43 + int BIO_free(BIO *a);
 44 + 
 45 + /* openssl/asn1.h */
 46 + typedef struct ASN1_ENCODING_st {
 47 +     unsigned char *enc;         /* DER encoding */
 48 +     long len;                   /* Length of encoding */
 49 +     int modified;               /* set to 1 if 'enc' is invalid */
 50 + } ASN1_ENCODING;
 51 + 
 52 + /* openssl/evp.h */
 53 + typedef ... EVP_PKEY;
 54 + 
 55 + void EVP_PKEY_free(EVP_PKEY *pkey);
 56 + 
 57 + /* openssl/x509.h */
 58 + typedef ... ASN1_INTEGER;
 59 + typedef ... ASN1_BIT_STRING;
 60 + typedef ... X509;
 61 + typedef ... X509_ALGOR;
 62 + typedef ... X509_CRL;
 63 + typedef ... X509_NAME;
 64 + typedef ... X509_PUBKEY;
 65 + typedef ... ipa_STACK_OF_X509_ATTRIBUTE;
 66 + 
 67 + typedef struct X509_req_info_st {
 68 +     ASN1_ENCODING enc;
 69 +     ASN1_INTEGER *version;
 70 +     X509_NAME *subject;
 71 +     X509_PUBKEY *pubkey;
 72 +     /*  d=2 hl=2 l=  0 cons: cont: 00 */
 73 +     ipa_STACK_OF_X509_ATTRIBUTE *attributes; /* [ 0 ] */
 74 + } X509_REQ_INFO;
 75 + 
 76 + typedef struct X509_req_st {
 77 +     X509_REQ_INFO *req_info;
 78 +     X509_ALGOR *sig_alg;
 79 +     ASN1_BIT_STRING *signature;
 80 +     int references;
 81 + } X509_REQ;
 82 + 
 83 + X509_REQ *X509_REQ_new(void);
 84 + void X509_REQ_free(X509_REQ *);
 85 + EVP_PKEY *d2i_PUBKEY_bio(BIO *bp, EVP_PKEY **a);
 86 + int X509_REQ_set_pubkey(X509_REQ *x, EVP_PKEY *pkey);
 87 + int X509_NAME_add_entry_by_txt(X509_NAME *name, const char *field, int type,
 88 +                                const unsigned char *bytes, int len, int loc,
 89 +                                int set);
 90 + int X509_NAME_entry_count(X509_NAME *name);
 91 + int i2d_X509_REQ_INFO(X509_REQ_INFO *a, unsigned char **out); \
 92 + 
 93 + /* openssl/x509v3.h */
 94 + typedef ... X509V3_CONF_METHOD;
 95 + 
 96 + typedef struct v3_ext_ctx {
 97 +     int flags;
 98 +     X509 *issuer_cert;
 99 +     X509 *subject_cert;
100 +     X509_REQ *subject_req;
101 +     X509_CRL *crl;
102 +     X509V3_CONF_METHOD *db_meth;
103 +     void *db;
104 + } X509V3_CTX;
105 + 
106 + void X509V3_set_ctx(X509V3_CTX *ctx, X509 *issuer, X509 *subject,
107 +                     X509_REQ *req, X509_CRL *crl, int flags);
108 + void X509V3_set_nconf(X509V3_CTX *ctx, CONF *conf);
109 + int X509V3_EXT_REQ_add_nconf(CONF *conf, X509V3_CTX *ctx, char *section,
110 +                              X509_REQ *req);
111 + 
112 + /* openssl/x509v3.h */
113 + unsigned long ERR_get_error(void);
114 + char *ERR_error_string(unsigned long e, char *buf);
115 + ''')
116 + 
117 + _libcrypto = _ffi.dlopen(ctypes.util.find_library('crypto'))
118 + 
119 + NULL = _ffi.NULL
120 + 
121 + # openssl/conf.h
122 + NCONF_new = _libcrypto.NCONF_new
123 + NCONF_free = _libcrypto.NCONF_free
124 + NCONF_load_bio = _libcrypto.NCONF_load_bio
125 + NCONF_get_section = _libcrypto.NCONF_get_section
126 + NCONF_get_string = _libcrypto.NCONF_get_string
127 + 
128 + # openssl/stack.h
129 + sk_num = _libcrypto.sk_num
130 + sk_value = _libcrypto.sk_value
131 + 
132 + 
133 + def sk_CONF_VALUE_num(sk):
134 +     return sk_num(_ffi.cast("_STACK *", sk))
135 + 
136 + 
137 + def sk_CONF_VALUE_value(sk, i):
138 +     return _ffi.cast("CONF_VALUE *", sk_value(_ffi.cast("_STACK *", sk), i))
139 + 
140 + 
141 + # openssl/bio.h
142 + BIO_new_mem_buf = _libcrypto.BIO_new_mem_buf
143 + BIO_free = _libcrypto.BIO_free
144 + 
145 + # openssl/x509.h
146 + X509_REQ_new = _libcrypto.X509_REQ_new
147 + X509_REQ_free = _libcrypto.X509_REQ_free
148 + X509_REQ_set_pubkey = _libcrypto.X509_REQ_set_pubkey
149 + d2i_PUBKEY_bio = _libcrypto.d2i_PUBKEY_bio
150 + i2d_X509_REQ_INFO = _libcrypto.i2d_X509_REQ_INFO
151 + X509_NAME_add_entry_by_txt = _libcrypto.X509_NAME_add_entry_by_txt
152 + X509_NAME_entry_count = _libcrypto.X509_NAME_entry_count
153 + 
154 + 
155 + def X509_REQ_get_subject_name(req):
156 +     return req.req_info.subject
157 + 
158 + # openssl/evp.h
159 + EVP_PKEY_free = _libcrypto.EVP_PKEY_free
160 + 
161 + # openssl/asn1.h
162 + MBSTRING_UTF8 = 0x1000
163 + 
164 + # openssl/x509v3.h
165 + X509V3_set_ctx = _libcrypto.X509V3_set_ctx
166 + X509V3_set_nconf = _libcrypto.X509V3_set_nconf
167 + X509V3_EXT_REQ_add_nconf = _libcrypto.X509V3_EXT_REQ_add_nconf
168 + 
169 + # openssl/err.h
170 + ERR_get_error = _libcrypto.ERR_get_error
171 + ERR_error_string = _libcrypto.ERR_error_string
172 + 
173 + 
174 + def _raise_openssl_errors():
175 +     msgs = []
176 + 
177 +     code = ERR_get_error()
178 +     while code != 0:
179 +         msg = ERR_error_string(code, NULL)
180 +         msgs.append(_ffi.string(msg))
181 +         code = ERR_get_error()
182 + 
183 +     raise errors.CSRTemplateError(reason='\n'.join(msgs))
184 + 
185 + 
186 + def _parse_dn_section(subj, dn_sk):
187 +     for i in range(sk_CONF_VALUE_num(dn_sk)):
188 +         v = sk_CONF_VALUE_value(dn_sk, i)
189 +         rdn_type = _ffi.string(v.name)
190 + 
191 +         # Skip past any leading X. X: X, etc to allow for multiple instances
192 +         for idx, c in enumerate(rdn_type):
193 +             if c in ':,.':
194 +                 if idx+1 < len(rdn_type):
195 +                     rdn_type = rdn_type[idx+1:]
196 +                 break
197 +         if rdn_type.startswith('+'):
198 +             rdn_type = rdn_type[1:]
199 +             mval = -1
200 +         else:
201 +             mval = 0
202 +         if not X509_NAME_add_entry_by_txt(
203 +                 subj, rdn_type, MBSTRING_UTF8, v.value, -1, -1, mval):
204 +             _raise_openssl_errors()
205 + 
206 +     if not X509_NAME_entry_count(subj):
207 +         raise errors.CSRTemplateError(
208 +             reason='error, subject in config file is empty')
209 + 
210 + 
211 + def build_requestinfo(config, public_key_info):
212 +     reqdata = NULL
213 +     req = NULL
214 +     nconf_bio = NULL
215 +     pubkey_bio = NULL
216 +     pubkey = NULL
217 + 
218 +     try:
219 +         reqdata = NCONF_new(NULL)
220 +         if reqdata == NULL:
221 +             _raise_openssl_errors()
222 + 
223 +         nconf_bio = BIO_new_mem_buf(config, len(config))
224 +         errorline = _ffi.new('long[1]', [-1])
225 +         i = NCONF_load_bio(reqdata, nconf_bio, errorline)
226 +         if i < 0:
227 +             if errorline[0] < 0:
228 +                 raise errors.CSRTemplateError(reason="Can't load config file")
229 +             else:
230 +                 raise errors.CSRTemplateError(
231 +                     reason='Error on line %d of config file' % errorline[0])
232 + 
233 +         dn_sect = NCONF_get_string(reqdata, 'req', 'distinguished_name')
234 +         if dn_sect == NULL:
235 +             raise errors.CSRTemplateError(
236 +                 reason='Unable to find "distinguished_name" key in config')
237 + 
238 +         dn_sk = NCONF_get_section(reqdata, dn_sect)
239 +         if dn_sk == NULL:
240 +             raise errors.CSRTemplateError(
241 +                 reason='Unable to find "%s" section in config' %
242 +                 _ffi.string(dn_sect))
243 + 
244 +         pubkey_bio = BIO_new_mem_buf(public_key_info, len(public_key_info))
245 +         pubkey = d2i_PUBKEY_bio(pubkey_bio, NULL)
246 +         if pubkey == NULL:
247 +             _raise_openssl_errors()
248 + 
249 +         req = X509_REQ_new()
250 +         if req == NULL:
251 +             _raise_openssl_errors()
252 + 
253 +         subject = X509_REQ_get_subject_name(req)
254 + 
255 +         _parse_dn_section(subject, dn_sk)
256 + 
257 +         if not X509_REQ_set_pubkey(req, pubkey):
258 +             _raise_openssl_errors()
259 + 
260 +         ext_ctx = _ffi.new("X509V3_CTX[1]")
261 +         X509V3_set_ctx(ext_ctx, NULL, NULL, req, NULL, 0)
262 +         X509V3_set_nconf(ext_ctx, reqdata)
263 + 
264 +         extn_section = NCONF_get_string(reqdata, "req", "req_extensions")
265 +         if extn_section != NULL:
266 +             if not X509V3_EXT_REQ_add_nconf(
267 +                     reqdata, ext_ctx, extn_section, req):
268 +                 _raise_openssl_errors()
269 + 
270 +         der_len = i2d_X509_REQ_INFO(req.req_info, NULL)
271 +         if der_len < 0:
272 +             _raise_openssl_errors()
273 + 
274 +         der_buf = _ffi.new("unsigned char[%d]" % der_len)
275 +         der_out = _ffi.new("unsigned char **", der_buf)
276 +         der_len = i2d_X509_REQ_INFO(req.req_info, der_out)
277 +         if der_len < 0:
278 +             _raise_openssl_errors()
279 + 
280 +         return _ffi.buffer(der_buf, der_len)
281 + 
282 +     finally:
283 +         if reqdata != NULL:
284 +             NCONF_free(reqdata)
285 +         if req != NULL:
286 +             X509_REQ_free(req)
287 +         if nconf_bio != NULL:
288 +             BIO_free(nconf_bio)
289 +         if pubkey_bio != NULL:
290 +             BIO_free(pubkey_bio)
291 +         if pubkey != NULL:
292 +             EVP_PKEY_free(pubkey)
 1 @@ -20,11 +20,10 @@
 2   # along with this program.  If not, see <http://www.gnu.org/licenses/>.
 3   
 4   import base64
 5 - import subprocess
 6 - from tempfile import NamedTemporaryFile as NTF
 7   
 8   import six
 9   
10 + from ipaclient import csrgen
11   from ipaclient.frontend import MethodOverride
12   from ipalib import errors
13   from ipalib import x509
14 @@ -108,54 +107,40 @@
15   
16           if csr is None:
17               if database:
18 -                 helper = u'certutil'
19 -                 helper_args = ['-d', database]
20 -                 if password_file:
21 -                     helper_args += ['-f', password_file]
22 +                 adaptor = csrgen.NSSAdaptor(database, password_file)
23               elif private_key:
24 -                 helper = u'openssl'
25 -                 helper_args = [private_key]
26 -                 if password_file:
27 -                     helper_args += ['-passin', 'file:%s' % password_file]
28 +                 adaptor = csrgen.OpenSSLAdaptor(private_key, password_file)
29               else:
30                   raise errors.InvocationError(
31                       message=u"One of 'database' or 'private_key' is required")
32   
33 -             with NTF() as scriptfile, NTF() as csrfile:
34 -                 # If csr_profile_id is passed, that takes precedence.
35 -                 # Otherwise, use profile_id. If neither are passed, the default
36 -                 # in cert_get_requestdata will be used.
37 -                 profile_id = csr_profile_id
38 -                 if profile_id is None:
39 -                     profile_id = options.get('profile_id')
40 - 
41 -                 self.api.Command.cert_get_requestdata(
42 -                     profile_id=profile_id,
43 -                     principal=options.get('principal'),
44 -                     out=unicode(scriptfile.name),
45 -                     helper=helper)
46 - 
47 -                 helper_cmd = [
48 -                     'bash', '-e', scriptfile.name, csrfile.name] + helper_args
49 - 
50 -                 try:
51 -                     subprocess.check_output(helper_cmd)
52 -                 except subprocess.CalledProcessError as e:
53 -                     raise errors.CertificateOperationError(
54 -                         error=(
55 -                             _('Error running "%(cmd)s" to generate CSR:'
56 -                               ' %(err)s') %
57 -                             {'cmd': ' '.join(helper_cmd), 'err': e.output}))
58 - 
59 -                 try:
60 -                     csr = unicode(csrfile.read())
61 -                 except IOError as e:
62 -                     raise errors.CertificateOperationError(
63 -                         error=(_('Unable to read generated CSR file: %(err)s')
64 -                                % {'err': e}))
65 -                 if not csr:
66 -                     raise errors.CertificateOperationError(
67 -                         error=(_('Generated CSR was empty')))
68 +             pubkey_info = adaptor.get_subject_public_key_info()
69 +             pubkey_info_b64 = base64.b64encode(pubkey_info)
70 + 
71 +             # If csr_profile_id is passed, that takes precedence.
72 +             # Otherwise, use profile_id. If neither are passed, the default
73 +             # in cert_get_requestdata will be used.
74 +             profile_id = csr_profile_id
75 +             if profile_id is None:
76 +                 profile_id = options.get('profile_id')
77 + 
78 +             response = self.api.Command.cert_get_requestdata(
79 +                 profile_id=profile_id,
80 +                 principal=options.get('principal'),
81 +                 public_key_info=unicode(pubkey_info_b64))
82 + 
83 +             req_info_b64 = response['result']['request_info']
84 +             req_info = base64.b64decode(req_info_b64)
85 + 
86 +             csr = adaptor.sign_csr(req_info)
87 + 
88 +             if not csr:
89 +                 raise errors.CertificateOperationError(
90 +                     error=(_('Generated CSR was empty')))
91 + 
92 +             # cert_request requires the CSR to be base64-encoded (but PEM
93 +             # header and footer are not required)
94 +             csr = unicode(base64.b64encode(csr))
95           else:
96               if database is not None or private_key is not None:
97                   raise errors.MutuallyExclusiveError(reason=_(
 1 @@ -2,15 +2,18 @@
 2   # Copyright (C) 2016  FreeIPA Contributors see COPYING for license
 3   #
 4   
 5 + import base64
 6 + 
 7   import six
 8   
 9 - from ipaclient.csrgen import CSRGenerator, FileRuleProvider
10 + from ipaclient import csrgen
11 + from ipaclient import csrgen_ffi
12   from ipalib import api
13   from ipalib import errors
14   from ipalib import output
15   from ipalib import util
16   from ipalib.frontend import Local, Str
17 - from ipalib.parameters import Principal
18 + from ipalib.parameters import File, Principal
19   from ipalib.plugable import Registry
20   from ipalib.text import _
21   from ipapython import dogtag
22 @@ -43,15 +46,14 @@
23               label=_('Profile ID'),
24               doc=_('CSR Generation Profile to use'),
25           ),
26 -         Str(
27 -             'helper',
28 -             label=_('Name of CSR generation tool'),
29 -             doc=_('Name of tool (e.g. openssl, certutil) that will be used to'
30 -                   ' create CSR'),
31 +         File(
32 +             'public_key_info',
33 +             label=_('Subject Public Key Info'),
34 +             doc=_('DER-encoded SubjectPublicKeyInfo structure'),
35           ),
36           Str(
37               'out?',
38 -             doc=_('Write CSR generation script to file'),
39 +             doc=_('Write CertificationRequestInfo to file'),
40           ),
41       )
42   
43 @@ -65,8 +67,8 @@
44   
45       has_output_params = (
46           Str(
47 -             'script',
48 -             label=_('Generation script'),
49 +             'request_info',
50 +             label=_('CertificationRequestInfo structure'),
51           )
52       )
53   
54 @@ -78,7 +80,8 @@
55           profile_id = options.get('profile_id')
56           if profile_id is None:
57               profile_id = dogtag.DEFAULT_PROFILE
58 -         helper = options.get('helper')
59 +         public_key_info = options.get('public_key_info')
60 +         public_key_info = base64.b64decode(public_key_info)
61   
62           if self.api.env.in_server:
63               backend = self.api.Backend.ldap2
64 @@ -103,16 +106,18 @@
65           principal_obj = principal_obj['result']
66           config = api.Command.config_show()['result']
67   
68 -         generator = CSRGenerator(FileRuleProvider())
69 +         generator = csrgen.CSRGenerator(csrgen.FileRuleProvider())
70   
71 -         script = generator.csr_config(principal_obj, config, profile_id)
72 +         csr_config = generator.csr_config(principal_obj, config, profile_id)
73 +         request_info = base64.b64encode(csrgen_ffi.build_requestinfo(
74 +             csr_config.encode('utf8'), public_key_info))
75   
76           result = {}
77           if 'out' in options:
78               with open(options['out'], 'wb') as f:
79 -                 f.write(script)
80 +                 f.write(request_info)
81           else:
82 -             result = dict(script=script)
83 +             result = dict(request_info=request_info)
84   
85           return dict(
86               result=result