URL: https://github.com/freeipa/freeipa/pull/568 Author: HonzaCholasta Title: #568: cert: include certificate chain in cert command output Action: opened
PR body: """ **cert: add output file option to cert-request** The certificate returned by cert-request can now be saved to a file in the CLI using a new --certificate-out option. **cert: include certificate chain in cert command output** Include the full certificate chain in the output of cert-request, cert-show and cert-find if --chain or --all is specified. If output file is specified in the CLI together with --chain, the full certificate chain is written to the file. https://pagure.io/freeipa/issue/6547 """ To pull the PR as Git branch: git remote add ghfreeipa https://github.com/freeipa/freeipa git fetch ghfreeipa pull/568/head:pr568 git checkout pr568
From 852f3e76cf92d81fc61261e5c5233ed212948e8f Mon Sep 17 00:00:00 2001 From: Jan Cholasta <jchol...@redhat.com> Date: Fri, 10 Mar 2017 09:19:53 +0000 Subject: [PATCH 1/2] cert: add output file option to cert-request The certificate returned by cert-request can now be saved to a file in the CLI using a new --certificate-out option. https://pagure.io/freeipa/issue/6547 --- ipaclient/plugins/cert.py | 44 ++++++++++++++++++++++++++++++++------------ 1 file changed, 32 insertions(+), 12 deletions(-) diff --git a/ipaclient/plugins/cert.py b/ipaclient/plugins/cert.py index 348529c..0fbd04a 100644 --- a/ipaclient/plugins/cert.py +++ b/ipaclient/plugins/cert.py @@ -19,6 +19,7 @@ # You should have received a copy of the GNU General Public License # along with this program. If not, see <http://www.gnu.org/licenses/>. +import base64 import subprocess from tempfile import NamedTemporaryFile as NTF @@ -38,8 +39,25 @@ register = Registry() +class CertRetrieveOverride(MethodOverride): + def forward(self, certificate_out=None, *args, **options): + if certificate_out is not None: + util.check_writable_file(certificate_out) + + result = super(CertRetrieveOverride, self).forward(*args, **options) + + if certificate_out is not None: + certs = [result['result']['certificate']] + certs = (x509.normalize_certificate(cert) for cert in certs) + certs = (x509.make_pem(base64.b64encode(cert)) for cert in certs) + with open(certificate_out, 'w') as f: + f.write('\n'.join(certs)) + + return result + + @register(override=True, no_fail=True) -class cert_request(MethodOverride): +class cert_request(CertRetrieveOverride): takes_options = ( Str( 'database?', @@ -61,6 +79,12 @@ class cert_request(MethodOverride): label=_('Name of CSR generation profile (if not the same as' ' profile_id)'), ), + Str( + 'certificate_out?', + doc=_('Write certificate (chain if --chain used) to file'), + include='cli', + cli_metavar='FILE', + ), ) def get_args(self): @@ -135,18 +159,14 @@ def forward(self, csr=None, **options): @register(override=True, no_fail=True) -class cert_show(MethodOverride): +class cert_show(CertRetrieveOverride): def forward(self, *keys, **options): - if 'out' in options: - util.check_writable_file(options['out']) - result = super(cert_show, self).forward(*keys, **options) - if 'certificate' in result['result']: - x509.write_certificate(result['result']['certificate'], options['out']) - return result - else: - raise errors.NoCertificateError(entry=keys[-1]) - else: - return super(cert_show, self).forward(*keys, **options) + try: + options['certificate_out'] = options['out'] + except KeyError: + pass + + return super(cert_show, self).forward(*keys, **options) @register(override=True, no_fail=True) From 9eb38a0d21fce225fc5d7ddc71ac0d10b133da41 Mon Sep 17 00:00:00 2001 From: Jan Cholasta <jchol...@redhat.com> Date: Fri, 10 Mar 2017 09:22:42 +0000 Subject: [PATCH 2/2] cert: include certificate chain in cert command output Include the full certificate chain in the output of cert-request, cert-show and cert-find if --chain or --all is specified. If output file is specified in the CLI together with --chain, the full certificate chain is written to the file. https://pagure.io/freeipa/issue/6547 --- API.txt | 6 ++++-- VERSION.m4 | 4 ++-- ipaclient/plugins/cert.py | 5 ++++- ipaserver/plugins/cert.py | 53 ++++++++++++++++++++++++++++++++++++++++------- 4 files changed, 56 insertions(+), 12 deletions(-) diff --git a/API.txt b/API.txt index 90cda74..2d6b401 100644 --- a/API.txt +++ b/API.txt @@ -782,11 +782,12 @@ option: Str('cacn?', autofill=True, cli_name='ca', default=u'ipa') option: Str('version?') output: Output('result') command: cert_request/1 -args: 1,8,3 +args: 1,9,3 arg: Str('csr', cli_name='csr_file') option: Flag('add', autofill=True, default=False) option: Flag('all', autofill=True, cli_name='all', default=False) option: Str('cacn?', autofill=True, cli_name='ca', default=u'ipa') +option: Flag('chain', autofill=True, default=False) option: Principal('principal') option: Str('profile_id?') option: Flag('raw', autofill=True, cli_name='raw', default=False) @@ -803,10 +804,11 @@ option: Int('revocation_reason', autofill=True, default=0) option: Str('version?') output: Output('result') command: cert_show/1 -args: 1,6,3 +args: 1,7,3 arg: Int('serial_number') option: Flag('all', autofill=True, cli_name='all', default=False) option: Str('cacn?', autofill=True, cli_name='ca', default=u'ipa') +option: Flag('chain', autofill=True, default=False) option: Flag('no_members', autofill=True, default=False) option: Str('out?') option: Flag('raw', autofill=True, cli_name='raw', default=False) diff --git a/VERSION.m4 b/VERSION.m4 index f943566..246d6bb 100644 --- a/VERSION.m4 +++ b/VERSION.m4 @@ -73,8 +73,8 @@ define(IPA_DATA_VERSION, 20100614120000) # # ######################################################## define(IPA_API_VERSION_MAJOR, 2) -define(IPA_API_VERSION_MINOR, 220) -# Last change: Add whoami command +define(IPA_API_VERSION_MINOR, 221) +# Last change: cert: include certificate chain in cert command output ######################################################## diff --git a/ipaclient/plugins/cert.py b/ipaclient/plugins/cert.py index 0fbd04a..6a88339 100644 --- a/ipaclient/plugins/cert.py +++ b/ipaclient/plugins/cert.py @@ -47,7 +47,10 @@ def forward(self, certificate_out=None, *args, **options): result = super(CertRetrieveOverride, self).forward(*args, **options) if certificate_out is not None: - certs = [result['result']['certificate']] + if options.get('chain', False): + certs = result['result']['certificate_chain'] + else: + certs = [result['result']['certificate']] certs = (x509.normalize_certificate(cert) for cert in certs) certs = (x509.make_pem(base64.b64encode(cert)) for cert in certs) with open(certificate_out, 'w') as f: diff --git a/ipaserver/plugins/cert.py b/ipaserver/plugins/cert.py index fb16f5b..8b9b863 100644 --- a/ipaserver/plugins/cert.py +++ b/ipaserver/plugins/cert.py @@ -267,6 +267,12 @@ class BaseCertObject(Object): normalizer=x509.normalize_certificate, flags={'no_create', 'no_update', 'no_search'}, ), + Bytes( + 'certificate_chain*', + label=_("Certificate chain"), + doc=_("X.509 certificate chain"), + flags={'no_create', 'no_update', 'no_search'}, + ), DNParam( 'subject', label=_('Subject'), @@ -495,6 +501,13 @@ class certreq(BaseCertObject): ) +_chain_flag = Flag( + 'chain', + default=False, + doc=_('Include certificate chain in output'), +) + + @register() class cert_request(Create, BaseCertMethod, VirtualCommand): __doc__ = _('Submit a certificate signing request.') @@ -526,6 +539,7 @@ class cert_request(Create, BaseCertMethod, VirtualCommand): "automatically add the principal if it doesn't exist " "(service principals only)"), ), + _chain_flag, ) def get_args(self): @@ -535,7 +549,7 @@ def get_args(self): continue yield arg - def execute(self, csr, all=False, raw=False, **kw): + def execute(self, csr, all=False, raw=False, chain=False, **kw): ca_enabled_check(self.api) ldap = self.api.Backend.ldap2 @@ -549,7 +563,7 @@ def execute(self, csr, all=False, raw=False, **kw): # referencing nonexistant CA) and look up authority ID. # ca = kw['cacn'] - ca_obj = api.Command.ca_show(ca)['result'] + ca_obj = api.Command.ca_show(ca, all=all, chain=chain)['result'] ca_id = ca_obj['ipacaid'][0] """ @@ -823,6 +837,11 @@ def execute(self, csr, all=False, raw=False, **kw): self.log.error("Profiles used to store cert should't be " "used for krbtgt certificates") + if 'certificate_chain' in ca_obj: + cert = x509.load_certificate(result['certificate']) + cert = cert.public_bytes(serialization.Encoding.DER) + result['certificate_chain'] = [cert] + ca_obj['certificate_chain'] + return dict( result=result, value=pkey_to_value(int(result['request_id']), kw), @@ -999,12 +1018,13 @@ class cert_show(Retrieve, CertMethod, VirtualCommand): doc=_('File to store the certificate in.'), exclude='webui', ), + _chain_flag, ) operation="retrieve certificate" def execute(self, serial_number, all=False, raw=False, no_members=False, - **options): + chain=False, **options): ca_enabled_check(self.api) # Dogtag lightweight CAs have shared serial number domain, so @@ -1020,7 +1040,11 @@ def execute(self, serial_number, all=False, raw=False, no_members=False, if not bind_principal_can_manage_cert(cert): raise acierr # pylint: disable=E0702 - ca_obj = api.Command.ca_show(options['cacn'])['result'] + ca_obj = api.Command.ca_show( + options['cacn'], + all=all, + chain=chain, + )['result'] if DN(cert.issuer) != DN(ca_obj['ipacasubjectdn'][0]): # DN of cert differs from what we requested raise errors.NotFound( @@ -1028,10 +1052,11 @@ def execute(self, serial_number, all=False, raw=False, no_members=False, "issued by CA '%(ca)s' not found") % dict(serial=serial_number, ca=options['cacn'])) + der_cert = base64.b64decode(result['certificate']) + if all or not no_members: ldap = self.api.Backend.ldap2 - filter = ldap.make_filter_from_attr( - 'usercertificate', base64.b64decode(result['certificate'])) + filter = ldap.make_filter_from_attr('usercertificate', der_cert) try: entries = ldap.get_entries(base_dn=self.api.env.basedn, filter=filter, @@ -1048,6 +1073,10 @@ def execute(self, serial_number, all=False, raw=False, no_members=False, self.obj._fill_owners(result) result['cacn'] = ca_obj['cn'][0] + if 'certificate_chain' in ca_obj: + result['certificate_chain'] = ( + [der_cert] + ca_obj['certificate_chain']) + return dict(result=result, value=pkey_to_value(serial_number, options)) @@ -1319,7 +1348,11 @@ def _ca_search(self, all, raw, pkey_only, sizelimit, exactly, **options): raise return result, False, complete - ca_objs = self.api.Command.ca_find(timelimit=0, sizelimit=0)['result'] + ca_objs = self.api.Command.ca_find( + all=all, + timelimit=0, + sizelimit=0, + )['result'] ca_objs = {DN(ca['ipacasubjectdn'][0]): ca for ca in ca_objs} ra = self.api.Backend.ra @@ -1354,6 +1387,12 @@ def _ca_search(self, all, raw, pkey_only, sizelimit, exactly, **options): obj['certificate'].replace('\r\n', '')) self.obj._parse(obj) + if 'certificate_chain' in ca_obj: + cert = x509.load_certificate(obj['certificate']) + cert_der = cert.public_bytes(serialization.Encoding.DER) + obj['certificate_chain'] = ( + [cert_der] + ca_obj['certificate_chain']) + obj['cacn'] = ca_obj['cn'][0] result[issuer, serial_number] = obj
-- Manage your subscription for the Freeipa-devel mailing list: https://www.redhat.com/mailman/listinfo/freeipa-devel Contribute to FreeIPA: http://www.freeipa.org/page/Contribute/Code