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