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 81be8bb7632a51fb8d886d192019329f56e1bc8d 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
---
 ipaclient/plugins/cert.py   | 76 ++++++++++++++++++++++++++++++++++++++++++++-
 ipaclient/plugins/csrgen.py |  5 ++-
 2 files changed, 79 insertions(+), 2 deletions(-)

diff --git a/ipaclient/plugins/cert.py b/ipaclient/plugins/cert.py
index 1075972..5d712b5 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
+from tempfile import NamedTemporaryFile as NTF
+
+import six
+
 from ipaclient.frontend import MethodOverride
 from ipalib import errors
 from ipalib import x509
@@ -27,17 +32,86 @@
 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 NTF() as scriptfile, NTF() as csrfile:
+                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')))
+        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/ipaclient/plugins/csrgen.py b/ipaclient/plugins/csrgen.py
index 0669a47..0d6eca0 100644
--- a/ipaclient/plugins/csrgen.py
+++ b/ipaclient/plugins/csrgen.py
@@ -13,6 +13,7 @@
 from ipalib.parameters import Principal
 from ipalib.plugable import Registry
 from ipalib.text import _
+from ipapython import dogtag
 
 if six.PY3:
     unicode = str
@@ -36,7 +37,7 @@ class cert_get_requestdata(Local):
                   ' HTTP/test.example.com)'),
         ),
         Str(
-            'profile_id',
+            'profile_id?',
             label=_('Profile ID'),
             doc=_('CSR Generation Profile to use'),
         ),
@@ -73,6 +74,8 @@ def execute(self, *args, **options):
 
         principal = options.get('principal')
         profile_id = options.get('profile_id')
+        if profile_id is None:
+            profile_id = dogtag.DEFAULT_PROFILE
         helper = options.get('helper')
 
         if self.api.env.in_server:

From acedefa0e56454383917dd0b5f4608addf28786e 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 | 13 ++++++++++++-
 1 file changed, 12 insertions(+), 1 deletion(-)

diff --git a/ipaclient/plugins/cert.py b/ipaclient/plugins/cert.py
index 5d712b5..16244e1 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:
@@ -75,7 +81,12 @@ def forward(self, csr=None, **options):
                     message=u"One of 'database' or 'private_key' is required")
 
             with NTF() as scriptfile, NTF() as csrfile:
-                profile_id = options.get('profile_id')
+                # 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,

From 83f4e892e5c35df876fb3fb3d19e7673350dfd75 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 16244e1..348529c 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