URL: https://github.com/freeipa/freeipa/pull/434
Author: LiptonB
 Title: #434: csrgen: Automate full cert request flow
Action: synchronized

To pull the PR as Git branch:
git remote add ghfreeipa https://github.com/freeipa/freeipa
git fetch ghfreeipa pull/434/head:pr434
git checkout pr434
From 5b4d2410d960084af766d44c112452604d0816c2 Mon Sep 17 00:00:00 2001
From: Ben Lipton <blip...@redhat.com>
Date: Mon, 22 Aug 2016 10:46:02 -0400
Subject: [PATCH 1/3] csrgen: Automate full cert request flow

Allows the `ipa cert-request` command to generate its own CSR. It no
longer requires a CSR passed on the command line, instead it creates a
config (bash script) with `cert-get-requestdata`, then runs it to build
a CSR, and submits that CSR.

Example usage (NSS database):
$ ipa cert-request --principal host/test.example.com --profile-id caIPAserviceCert --database /tmp/certs

Example usage (PEM private key file):
$ ipa cert-request --principal host/test.example.com --profile-id caIPAserviceCert --private-key /tmp/key.pem

https://fedorahosted.org/freeipa/ticket/4899
---
 API.txt                   |  2 +-
 ipaclient/plugins/cert.py | 83 ++++++++++++++++++++++++++++++++++++++++++++++-
 ipaserver/plugins/cert.py |  7 ++--
 3 files changed, 88 insertions(+), 4 deletions(-)

diff --git a/API.txt b/API.txt
index 543cec5..ac38514 100644
--- a/API.txt
+++ b/API.txt
@@ -788,7 +788,7 @@ 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: Principal('principal')
-option: Str('profile_id?')
+option: Str('profile_id', autofill=True, default=u'caIPAserviceCert')
 option: Flag('raw', autofill=True, cli_name='raw', default=False)
 option: Str('request_type', autofill=True, default=u'pkcs10')
 option: Str('version?')
diff --git a/ipaclient/plugins/cert.py b/ipaclient/plugins/cert.py
index 1075972..339b1d0 100644
--- a/ipaclient/plugins/cert.py
+++ b/ipaclient/plugins/cert.py
@@ -19,6 +19,11 @@
 # 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 subprocess
+import tempfile
+
+import six
+
 from ipaclient.frontend import MethodOverride
 from ipalib import errors
 from ipalib import x509
@@ -27,17 +32,93 @@
 from ipalib.plugable import Registry
 from ipalib.text import _
 
+if six.PY3:
+    unicode = str
+
 register = Registry()
 
 
 @register(override=True, no_fail=True)
 class cert_request(MethodOverride):
+    takes_options = (
+        Str(
+            'database?',
+            label=_('Path to NSS database'),
+            doc=_('Path to NSS database to use for private key'),
+        ),
+        Str(
+            'private_key?',
+            label=_('Path to private key file'),
+            doc=_('Path to PEM file containing a private key'),
+        ),
+    )
+
     def get_args(self):
         for arg in super(cert_request, self).get_args():
             if arg.name == 'csr':
-                arg = arg.clone_retype(arg.name, File)
+                arg = arg.clone_retype(arg.name, File, required=False)
             yield arg
 
+    def forward(self, csr=None, **options):
+        database = options.pop('database', None)
+        private_key = options.pop('private_key', None)
+
+        if csr is None:
+            if database:
+                helper = u'certutil'
+                helper_args = ['-d', database]
+            elif private_key:
+                helper = u'openssl'
+                helper_args = [private_key]
+            else:
+                raise errors.InvocationError(
+                    message=u"One of 'database' or 'private_key' is required")
+
+            with tempfile.NamedTemporaryFile(
+                    ) as scriptfile, tempfile.NamedTemporaryFile() as csrfile:
+                # profile_id is optional for cert_request, but not for
+                # cert_get_requestdata, so pass the default explicitly when
+                # necessary
+                profile_id = options.get('profile_id')
+                if profile_id is None:
+                    profile_id = self.get_default_of('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')))
+
+                return super(cert_request, self).forward(csr, **options)
+        else:
+            if database is not None or private_key is not None:
+                raise errors.MutuallyExclusiveError(reason=_(
+                    "Options 'database' and 'private_key' are not compatible"
+                    " with 'csr'"))
+            return super(cert_request, self).forward(csr, **options)
+
 
 @register(override=True, no_fail=True)
 class cert_show(MethodOverride):
diff --git a/ipaserver/plugins/cert.py b/ipaserver/plugins/cert.py
index 5bf4cfb..a6ee18b 100644
--- a/ipaserver/plugins/cert.py
+++ b/ipaserver/plugins/cert.py
@@ -47,6 +47,7 @@
 from ipalib.text import _
 from ipalib.request import context
 from ipalib import output
+from ipapython import dogtag
 from ipapython import kerberos
 from ipapython.dn import DN
 from ipapython.ipa_log_manager import root_logger
@@ -478,7 +479,9 @@ class certreq(BaseCertObject):
             flags={'no_update', 'no_update', 'no_search'},
         ),
         Str(
-            'profile_id?', validate_profile_id,
+            'profile_id', validate_profile_id,
+            default=dogtag.DEFAULT_PROFILE,
+            autofill=True,
             label=_("Profile ID"),
             doc=_("Certificate Profile to use"),
             flags={'no_update', 'no_update', 'no_search'},
@@ -544,7 +547,7 @@ def execute(self, csr, all=False, raw=False, **kw):
         realm = unicode(self.api.env.realm)
         add = kw.get('add')
         request_type = kw.get('request_type')
-        profile_id = kw.get('profile_id', self.Backend.ra.DEFAULT_PROFILE)
+        profile_id = kw.get('profile_id')
 
         # Check that requested authority exists (done before CA ACL
         # enforcement so that user gets better error message if

From 6e67bd41d8697159e7f474c306083e2a8102b576 Mon Sep 17 00:00:00 2001
From: Ben Lipton <blip...@redhat.com>
Date: Sat, 4 Feb 2017 10:25:42 -0500
Subject: [PATCH 2/3] csrgen: Allow overriding the CSR generation profile

In case users want multiple CSR generation profiles that work with the
same dogtag profile, or in case the profiles are not named the same,
this flag allows specifying an alternative CSR generation profile.

https://fedorahosted.org/freeipa/ticket/4899
---
 ipaclient/plugins/cert.py | 17 +++++++++++++----
 1 file changed, 13 insertions(+), 4 deletions(-)

diff --git a/ipaclient/plugins/cert.py b/ipaclient/plugins/cert.py
index 339b1d0..ba03a37 100644
--- a/ipaclient/plugins/cert.py
+++ b/ipaclient/plugins/cert.py
@@ -51,6 +51,11 @@ class cert_request(MethodOverride):
             label=_('Path to private key file'),
             doc=_('Path to PEM file containing a private key'),
         ),
+        Str(
+            'csr_profile_id?',
+            label=_('Name of CSR generation profile (if not the same as'
+                    ' profile_id)'),
+        ),
     )
 
     def get_args(self):
@@ -62,6 +67,7 @@ def get_args(self):
     def forward(self, csr=None, **options):
         database = options.pop('database', None)
         private_key = options.pop('private_key', None)
+        csr_profile_id = options.pop('csr_profile_id', None)
 
         if csr is None:
             if database:
@@ -76,10 +82,13 @@ def forward(self, csr=None, **options):
 
             with tempfile.NamedTemporaryFile(
                     ) as scriptfile, tempfile.NamedTemporaryFile() as csrfile:
-                # profile_id is optional for cert_request, but not for
-                # cert_get_requestdata, so pass the default explicitly when
-                # necessary
-                profile_id = options.get('profile_id')
+                # If csr_profile_id is passed, that takes precedence.
+                # Otherwise, use profile_id. And if neither is passed, we need
+                # the default profile_id so we can pass it explicitly to
+                # cert_get_requestdata which has no default profile.
+                profile_id = csr_profile_id
+                if profile_id is None:
+                    profile_id = options.get('profile_id')
                 if profile_id is None:
                     profile_id = self.get_default_of('profile_id')
 

From 79786f203f09fc18b388c59797713df4f15f2cf0 Mon Sep 17 00:00:00 2001
From: Ben Lipton <blip...@redhat.com>
Date: Wed, 8 Feb 2017 20:56:37 -0500
Subject: [PATCH 3/3] csrgen: Support encrypted private keys

https://fedorahosted.org/freeipa/ticket/4899
---
 install/share/csrgen/templates/openssl_base.tmpl |  9 +++++----
 ipaclient/plugins/cert.py                        | 10 ++++++++++
 2 files changed, 15 insertions(+), 4 deletions(-)

diff --git a/install/share/csrgen/templates/openssl_base.tmpl b/install/share/csrgen/templates/openssl_base.tmpl
index 2d6c070..22b1686 100644
--- a/install/share/csrgen/templates/openssl_base.tmpl
+++ b/install/share/csrgen/templates/openssl_base.tmpl
@@ -3,15 +3,16 @@
 {%- endraw %}
 #!/bin/bash -e
 
-if [[ $# -ne 2 ]]; then
-echo "Usage: $0 <outfile> <keyfile>"
+if [[ $# -lt 2 ]]; then
+echo "Usage: $0 <outfile> <keyfile> <other openssl arguments>"
 echo "Called as: $0 $@"
 exit 1
 fi
 
 CONFIG="$(mktemp)"
 CSR="$1"
-shift
+KEYFILE="$2"
+shift; shift
 
 echo \
 {% raw %}{% filter quote %}{% endraw -%}
@@ -30,5 +31,5 @@ req_extensions = {% call openssl.section() %}{{ rendered_extensions }}{% endcall
 {{ openssl.openssl_sections|join('\n\n') }}
 {% endfilter %}{%- endraw %} > "$CONFIG"
 
-openssl req -new -config "$CONFIG" -out "$CSR" -key $1
+openssl req -new -config "$CONFIG" -out "$CSR" -key "$KEYFILE" "$@"
 rm "$CONFIG"
diff --git a/ipaclient/plugins/cert.py b/ipaclient/plugins/cert.py
index ba03a37..04f7d69 100644
--- a/ipaclient/plugins/cert.py
+++ b/ipaclient/plugins/cert.py
@@ -52,6 +52,11 @@ class cert_request(MethodOverride):
             doc=_('Path to PEM file containing a private key'),
         ),
         Str(
+            'password_file?',
+            label=_(
+                'File containing a password for the private key or database'),
+        ),
+        Str(
             'csr_profile_id?',
             label=_('Name of CSR generation profile (if not the same as'
                     ' profile_id)'),
@@ -68,14 +73,19 @@ def forward(self, csr=None, **options):
         database = options.pop('database', None)
         private_key = options.pop('private_key', None)
         csr_profile_id = options.pop('csr_profile_id', None)
+        password_file = options.pop('password_file', None)
 
         if csr is None:
             if database:
                 helper = u'certutil'
                 helper_args = ['-d', database]
+                if password_file:
+                    helper_args += ['-f', password_file]
             elif private_key:
                 helper = u'openssl'
                 helper_args = [private_key]
+                if password_file:
+                    helper_args += ['-passin', 'file:%s' % password_file]
             else:
                 raise errors.InvocationError(
                     message=u"One of 'database' or 'private_key' is required")
-- 
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