URL: https://github.com/freeipa/freeipa/pull/568
Author: HonzaCholasta
 Title: #568: cert: include certificate chain in cert command output
Action: synchronized

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 2f08a1e0e6e8ee82d7fa67e8d5d26cdbabc4fc45 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.

Deprecate --out in cert-show in favor of --certificate-out.

https://pagure.io/freeipa/issue/6547
---
 ipaclient/plugins/cert.py | 66 +++++++++++++++++++++++++++++++++++++----------
 1 file changed, 52 insertions(+), 14 deletions(-)

diff --git a/ipaclient/plugins/cert.py b/ipaclient/plugins/cert.py
index 348529c..62171e9 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,10 +39,37 @@
 register = Registry()
 
 
-@register(override=True, no_fail=True)
-class cert_request(MethodOverride):
+class CertRetrieveOverride(MethodOverride):
     takes_options = (
         Str(
+            'certificate_out?',
+            doc=_('Write certificate (chain if --chain used) to file'),
+            include='cli',
+            cli_metavar='FILE',
+        ),
+    )
+
+    def forward(self, *args, **options):
+        certificate_out = options.pop('certificate_out', None)
+        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(CertRetrieveOverride):
+    takes_options = CertRetrieveOverride.takes_options + (
+        Str(
             'database?',
             label=_('Path to NSS database'),
             doc=_('Path to NSS database to use for private key'),
@@ -135,18 +163,28 @@ def forward(self, csr=None, **options):
 
 
 @register(override=True, no_fail=True)
-class cert_show(MethodOverride):
-    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)
+class cert_show(CertRetrieveOverride):
+    def get_options(self):
+        for option in super(cert_show, self).get_options():
+            if option.name == 'out':
+                # skip server-defined --out
+                continue
+            if option.name == 'certificate_out':
+                # add --out as a deprecated alias of --certificate-out
+                option = option.clone_rename(
+                    'out',
+                    cli_name='certificate_out',
+                    deprecated_cli_aliases={'out'},
+                )
+            yield option
+
+    def forward(self, *args, **options):
+        try:
+            options['certificate_out'] = options.pop('out')
+        except KeyError:
+            pass
+
+        return super(cert_show, self).forward(*args, **options)
 
 
 @register(override=True, no_fail=True)

From d3b3266018df4390b348ff253dae42b522511c34 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 62171e9..9ec6970 100644
--- a/ipaclient/plugins/cert.py
+++ b/ipaclient/plugins/cert.py
@@ -57,7 +57,10 @@ def forward(self, *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

Reply via email to