URL: https://github.com/freeipa/freeipa/pull/542 Author: LiptonB Title: #542: Implementation independent interface for CSR generation Action: synchronized
To pull the PR as Git branch: git remote add ghfreeipa https://github.com/freeipa/freeipa git fetch ghfreeipa pull/542/head:pr542 git checkout pr542
From 3aab3e2cb40bde4d4bed5c01ce04d028706ec210 Mon Sep 17 00:00:00 2001 From: Ben Lipton <blip...@redhat.com> Date: Tue, 21 Mar 2017 12:21:30 -0400 Subject: [PATCH 1/4] csrgen: Remove helper abstraction All requests now use the OpenSSL formatter. However, we keep Formatter a separate class so that it can be changed out for tests. https://pagure.io/freeipa/issue/4899 --- ipaclient/csrgen.py | 71 ++++++---------- ipaclient/csrgen/rules/dataDNS.json | 13 +-- ipaclient/csrgen/rules/dataEmail.json | 13 +-- ipaclient/csrgen/rules/dataHostCN.json | 13 +-- ipaclient/csrgen/rules/dataSubjectBase.json | 13 +-- ipaclient/csrgen/rules/dataUsernameCN.json | 13 +-- ipaclient/csrgen/rules/syntaxSAN.json | 19 ++--- ipaclient/csrgen/rules/syntaxSubject.json | 13 +-- ipaclient/csrgen/templates/certutil_base.tmpl | 11 --- ipaclient/plugins/csrgen.py | 2 +- .../data/test_csrgen/configs/caIPAserviceCert.conf | 34 ++++++++ .../data/test_csrgen/configs/userCert.conf | 34 ++++++++ .../data/test_csrgen/rules/basic.json | 13 +-- .../data/test_csrgen/rules/options.json | 18 +--- .../scripts/caIPAserviceCert_certutil.sh | 11 --- .../scripts/caIPAserviceCert_openssl.sh | 34 -------- .../data/test_csrgen/scripts/userCert_certutil.sh | 11 --- .../data/test_csrgen/scripts/userCert_openssl.sh | 34 -------- ipatests/test_ipaclient/test_csrgen.py | 98 +++++----------------- 19 files changed, 145 insertions(+), 323 deletions(-) delete mode 100644 ipaclient/csrgen/templates/certutil_base.tmpl create mode 100644 ipatests/test_ipaclient/data/test_csrgen/configs/caIPAserviceCert.conf create mode 100644 ipatests/test_ipaclient/data/test_csrgen/configs/userCert.conf delete mode 100644 ipatests/test_ipaclient/data/test_csrgen/scripts/caIPAserviceCert_certutil.sh delete mode 100644 ipatests/test_ipaclient/data/test_csrgen/scripts/caIPAserviceCert_openssl.sh delete mode 100644 ipatests/test_ipaclient/data/test_csrgen/scripts/userCert_certutil.sh delete mode 100644 ipatests/test_ipaclient/data/test_csrgen/scripts/userCert_openssl.sh diff --git a/ipaclient/csrgen.py b/ipaclient/csrgen.py index 8fb0b32..8ca0722 100644 --- a/ipaclient/csrgen.py +++ b/ipaclient/csrgen.py @@ -244,13 +244,6 @@ def _prepare_syntax_rule( return self.SyntaxRule(prepared_template, is_extension) -class CertutilFormatter(Formatter): - base_template_name = 'certutil_base.tmpl' - - def _get_template_params(self, syntax_rules): - return {'options': syntax_rules} - - class FieldMapping(object): """Representation of the rules needed to construct a complete cert field. @@ -279,13 +272,11 @@ def __init__(self, name, template, options): class RuleProvider(object): - def rules_for_profile(self, profile_id, helper): + def rules_for_profile(self, profile_id): """ Return the rules needed to build a CSR using the given profile. :param profile_id: str, name of the CSR generation profile to use - :param helper: str, name of tool (e.g. openssl, certutil) that will be - used to create CSR :returns: list of FieldMapping, filled out with the appropriate rules """ @@ -321,40 +312,31 @@ def _open(self, subdir, filename): ) ) - def _rule(self, rule_name, helper): - if (rule_name, helper) not in self.rules: + def _rule(self, rule_name): + if rule_name not in self.rules: try: with self._open('rules', '%s.json' % rule_name) as f: - ruleset = json.load(f) + ruleconf = json.load(f) except IOError: raise errors.NotFound( - reason=_('Ruleset %(ruleset)s does not exist.') % - {'ruleset': rule_name}) + reason=_('No generation rule %(rulename)s found.') % + {'rulename': rule_name}) - matching_rules = [r for r in ruleset['rules'] - if r['helper'] == helper] - if len(matching_rules) == 0: + try: + rule = ruleconf['rule'] + except KeyError: raise errors.EmptyResult( - reason=_('No transformation in "%(ruleset)s" rule supports' - ' helper "%(helper)s"') % - {'ruleset': rule_name, 'helper': helper}) - elif len(matching_rules) > 1: - raise errors.RedundantMappingRule( - ruleset=rule_name, helper=helper) - rule = matching_rules[0] - - options = {} - if 'options' in ruleset: - options.update(ruleset['options']) - if 'options' in rule: - options.update(rule['options']) - - self.rules[(rule_name, helper)] = Rule( + reason=_('Generation rule "%(rulename)s" is missing the' + ' "rule" key') % {'rulename': rule_name}) + + options = ruleconf.get('options', {}) + + self.rules[rule_name] = Rule( rule_name, rule['template'], options) - return self.rules[(rule_name, helper)] + return self.rules[rule_name] - def rules_for_profile(self, profile_id, helper): + def rules_for_profile(self, profile_id): try: with self._open('profiles', '%s.json' % profile_id) as f: profile = json.load(f) @@ -365,28 +347,23 @@ def rules_for_profile(self, profile_id, helper): field_mappings = [] for field in profile: - syntax_rule = self._rule(field['syntax'], helper) - data_rules = [self._rule(name, helper) for name in field['data']] + syntax_rule = self._rule(field['syntax']) + data_rules = [self._rule(name) for name in field['data']] field_mappings.append(FieldMapping( syntax_rule.name, syntax_rule, data_rules)) return field_mappings class CSRGenerator(object): - FORMATTERS = { - 'openssl': OpenSSLFormatter, - 'certutil': CertutilFormatter, - } - - def __init__(self, rule_provider): + def __init__(self, rule_provider, formatter_class=OpenSSLFormatter): self.rule_provider = rule_provider + self.formatter = formatter_class() - def csr_script(self, principal, config, profile_id, helper): + def csr_script(self, principal, config, profile_id): render_data = {'subject': principal, 'config': config} - formatter = self.FORMATTERS[helper]() - rules = self.rule_provider.rules_for_profile(profile_id, helper) - template = formatter.build_template(rules) + rules = self.rule_provider.rules_for_profile(profile_id) + template = self.formatter.build_template(rules) try: script = template.render(render_data) diff --git a/ipaclient/csrgen/rules/dataDNS.json b/ipaclient/csrgen/rules/dataDNS.json index 2663f11..a79a3d7 100644 --- a/ipaclient/csrgen/rules/dataDNS.json +++ b/ipaclient/csrgen/rules/dataDNS.json @@ -1,14 +1,7 @@ { - "rules": [ - { - "helper": "openssl", - "template": "DNS = {{subject.krbprincipalname.0.partition('/')[2].partition('@')[0]}}" - }, - { - "helper": "certutil", - "template": "dns:{{subject.krbprincipalname.0.partition('/')[2].partition('@')[0]|quote}}" - } - ], + "rule": { + "template": "DNS = {{subject.krbprincipalname.0.partition('/')[2].partition('@')[0]}}" + }, "options": { "data_source": "subject.krbprincipalname.0.partition('/')[2].partition('@')[0]" } diff --git a/ipaclient/csrgen/rules/dataEmail.json b/ipaclient/csrgen/rules/dataEmail.json index 2eae9fb..4be6cec 100644 --- a/ipaclient/csrgen/rules/dataEmail.json +++ b/ipaclient/csrgen/rules/dataEmail.json @@ -1,14 +1,7 @@ { - "rules": [ - { - "helper": "openssl", - "template": "email = {{subject.mail.0}}" - }, - { - "helper": "certutil", - "template": "email:{{subject.mail.0|quote}}" - } - ], + "rule": { + "template": "email = {{subject.mail.0}}" + }, "options": { "data_source": "subject.mail.0" } diff --git a/ipaclient/csrgen/rules/dataHostCN.json b/ipaclient/csrgen/rules/dataHostCN.json index 5c415bb..f30c50f 100644 --- a/ipaclient/csrgen/rules/dataHostCN.json +++ b/ipaclient/csrgen/rules/dataHostCN.json @@ -1,14 +1,7 @@ { - "rules": [ - { - "helper": "openssl", - "template": "CN={{subject.krbprincipalname.0.partition('/')[2].partition('@')[0]}}" - }, - { - "helper": "certutil", - "template": "CN={{subject.krbprincipalname.0.partition('/')[2].partition('@')[0]|quote}}" - } - ], + "rule": { + "template": "CN={{subject.krbprincipalname.0.partition('/')[2].partition('@')[0]}}" + }, "options": { "data_source": "subject.krbprincipalname.0.partition('/')[2].partition('@')[0]" } diff --git a/ipaclient/csrgen/rules/dataSubjectBase.json b/ipaclient/csrgen/rules/dataSubjectBase.json index 309dfb1..31a38b4 100644 --- a/ipaclient/csrgen/rules/dataSubjectBase.json +++ b/ipaclient/csrgen/rules/dataSubjectBase.json @@ -1,14 +1,7 @@ { - "rules": [ - { - "helper": "openssl", - "template": "{{config.ipacertificatesubjectbase.0}}" - }, - { - "helper": "certutil", - "template": "{{config.ipacertificatesubjectbase.0|quote}}" - } - ], + "rule": { + "template": "{{config.ipacertificatesubjectbase.0}}" + }, "options": { "data_source": "config.ipacertificatesubjectbase.0" } diff --git a/ipaclient/csrgen/rules/dataUsernameCN.json b/ipaclient/csrgen/rules/dataUsernameCN.json index 37e7e01..acbb524 100644 --- a/ipaclient/csrgen/rules/dataUsernameCN.json +++ b/ipaclient/csrgen/rules/dataUsernameCN.json @@ -1,14 +1,7 @@ { - "rules": [ - { - "helper": "openssl", - "template": "CN={{subject.uid.0}}" - }, - { - "helper": "certutil", - "template": "CN={{subject.uid.0|quote}}" - } - ], + "rule": { + "template": "CN={{subject.uid.0}}" + }, "options": { "data_source": "subject.uid.0" } diff --git a/ipaclient/csrgen/rules/syntaxSAN.json b/ipaclient/csrgen/rules/syntaxSAN.json index 122eb12..c6943ed 100644 --- a/ipaclient/csrgen/rules/syntaxSAN.json +++ b/ipaclient/csrgen/rules/syntaxSAN.json @@ -1,15 +1,8 @@ { - "rules": [ - { - "helper": "openssl", - "template": "subjectAltName = @{% call openssl.section() %}{{ datarules|join('\n') }}{% endcall %}", - "options": { - "extension": true - } - }, - { - "helper": "certutil", - "template": "--extSAN {{ datarules|join(',') }}" - } - ] + "rule": { + "template": "subjectAltName = @{% call openssl.section() %}{{ datarules|join('\n') }}{% endcall %}" + }, + "options": { + "extension": true + } } diff --git a/ipaclient/csrgen/rules/syntaxSubject.json b/ipaclient/csrgen/rules/syntaxSubject.json index af6ec03..c609e01 100644 --- a/ipaclient/csrgen/rules/syntaxSubject.json +++ b/ipaclient/csrgen/rules/syntaxSubject.json @@ -1,14 +1,7 @@ { - "rules": [ - { - "helper": "openssl", - "template": "distinguished_name = {% call openssl.section() %}{{ datarules|reverse|join('\n') }}{% endcall %}" - }, - { - "helper": "certutil", - "template": "-s {{ datarules|join(',') }}" - } - ], + "rule": { + "template": "distinguished_name = {% call openssl.section() %}{{ datarules|reverse|join('\n') }}{% endcall %}" + }, "options": { "required": true, "data_source_combinator": "and" diff --git a/ipaclient/csrgen/templates/certutil_base.tmpl b/ipaclient/csrgen/templates/certutil_base.tmpl deleted file mode 100644 index a5556fd..0000000 --- a/ipaclient/csrgen/templates/certutil_base.tmpl +++ /dev/null @@ -1,11 +0,0 @@ -#!/bin/bash -e - -if [[ $# -lt 1 ]]; then -echo "Usage: $0 <outfile> [<any> <certutil> <args>]" -echo "Called as: $0 $@" -exit 1 -fi - -CSR="$1" -shift -certutil -R -a -z <(head -c 4096 /dev/urandom) -o "$CSR" {{ options|join(' ') }} "$@" diff --git a/ipaclient/plugins/csrgen.py b/ipaclient/plugins/csrgen.py index a0d99ef..c10ef2d 100644 --- a/ipaclient/plugins/csrgen.py +++ b/ipaclient/plugins/csrgen.py @@ -106,7 +106,7 @@ def execute(self, *args, **options): generator = CSRGenerator(FileRuleProvider()) script = generator.csr_script( - principal_obj, config, profile_id, helper) + principal_obj, config, profile_id) result = {} if 'out' in options: diff --git a/ipatests/test_ipaclient/data/test_csrgen/configs/caIPAserviceCert.conf b/ipatests/test_ipaclient/data/test_csrgen/configs/caIPAserviceCert.conf new file mode 100644 index 0000000..811bfd7 --- /dev/null +++ b/ipatests/test_ipaclient/data/test_csrgen/configs/caIPAserviceCert.conf @@ -0,0 +1,34 @@ +#!/bin/bash -e + +if [[ $# -lt 2 ]]; then +echo "Usage: $0 <outfile> <keyfile> <other openssl arguments>" +echo "Called as: $0 $@" +exit 1 +fi + +CONFIG="$(mktemp)" +CSR="$1" +KEYFILE="$2" +shift; shift + +echo \ +'[ req ] +prompt = no +encrypt_key = no + +distinguished_name = sec0 +req_extensions = sec2 + +[ sec0 ] +O=DOMAIN.EXAMPLE.COM +CN=machine.example.com + +[ sec1 ] +DNS = machine.example.com + +[ sec2 ] +subjectAltName = @sec1 +' > "$CONFIG" + +openssl req -new -config "$CONFIG" -out "$CSR" -key "$KEYFILE" "$@" +rm "$CONFIG" diff --git a/ipatests/test_ipaclient/data/test_csrgen/configs/userCert.conf b/ipatests/test_ipaclient/data/test_csrgen/configs/userCert.conf new file mode 100644 index 0000000..2edf067 --- /dev/null +++ b/ipatests/test_ipaclient/data/test_csrgen/configs/userCert.conf @@ -0,0 +1,34 @@ +#!/bin/bash -e + +if [[ $# -lt 2 ]]; then +echo "Usage: $0 <outfile> <keyfile> <other openssl arguments>" +echo "Called as: $0 $@" +exit 1 +fi + +CONFIG="$(mktemp)" +CSR="$1" +KEYFILE="$2" +shift; shift + +echo \ +'[ req ] +prompt = no +encrypt_key = no + +distinguished_name = sec0 +req_extensions = sec2 + +[ sec0 ] +O=DOMAIN.EXAMPLE.COM +CN=testuser + +[ sec1 ] +email = testu...@example.com + +[ sec2 ] +subjectAltName = @sec1 +' > "$CONFIG" + +openssl req -new -config "$CONFIG" -out "$CSR" -key "$KEYFILE" "$@" +rm "$CONFIG" diff --git a/ipatests/test_ipaclient/data/test_csrgen/rules/basic.json b/ipatests/test_ipaclient/data/test_csrgen/rules/basic.json index feba3e9..094ef71 100644 --- a/ipatests/test_ipaclient/data/test_csrgen/rules/basic.json +++ b/ipatests/test_ipaclient/data/test_csrgen/rules/basic.json @@ -1,12 +1,5 @@ { - "rules": [ - { - "helper": "openssl", - "template": "openssl_rule" - }, - { - "helper": "certutil", - "template": "certutil_rule" - } - ] + "rule": { + "template": "openssl_rule" + } } diff --git a/ipatests/test_ipaclient/data/test_csrgen/rules/options.json b/ipatests/test_ipaclient/data/test_csrgen/rules/options.json index 111a6d8..393ed8c 100644 --- a/ipatests/test_ipaclient/data/test_csrgen/rules/options.json +++ b/ipatests/test_ipaclient/data/test_csrgen/rules/options.json @@ -1,18 +1,8 @@ { - "rules": [ - { - "helper": "openssl", - "template": "openssl_rule", - "options": { - "helper_option": true - } - }, - { - "helper": "certutil", - "template": "certutil_rule" - } - ], + "rule": { + "template": "openssl_rule" + }, "options": { - "global_option": true + "rule_option": true } } diff --git a/ipatests/test_ipaclient/data/test_csrgen/scripts/caIPAserviceCert_certutil.sh b/ipatests/test_ipaclient/data/test_csrgen/scripts/caIPAserviceCert_certutil.sh deleted file mode 100644 index 74a704c..0000000 --- a/ipatests/test_ipaclient/data/test_csrgen/scripts/caIPAserviceCert_certutil.sh +++ /dev/null @@ -1,11 +0,0 @@ -#!/bin/bash -e - -if [[ $# -lt 1 ]]; then -echo "Usage: $0 <outfile> [<any> <certutil> <args>]" -echo "Called as: $0 $@" -exit 1 -fi - -CSR="$1" -shift -certutil -R -a -z <(head -c 4096 /dev/urandom) -o "$CSR" -s CN=machine.example.com,O=DOMAIN.EXAMPLE.COM --extSAN dns:machine.example.com "$@" diff --git a/ipatests/test_ipaclient/data/test_csrgen/scripts/caIPAserviceCert_openssl.sh b/ipatests/test_ipaclient/data/test_csrgen/scripts/caIPAserviceCert_openssl.sh deleted file mode 100644 index 811bfd7..0000000 --- a/ipatests/test_ipaclient/data/test_csrgen/scripts/caIPAserviceCert_openssl.sh +++ /dev/null @@ -1,34 +0,0 @@ -#!/bin/bash -e - -if [[ $# -lt 2 ]]; then -echo "Usage: $0 <outfile> <keyfile> <other openssl arguments>" -echo "Called as: $0 $@" -exit 1 -fi - -CONFIG="$(mktemp)" -CSR="$1" -KEYFILE="$2" -shift; shift - -echo \ -'[ req ] -prompt = no -encrypt_key = no - -distinguished_name = sec0 -req_extensions = sec2 - -[ sec0 ] -O=DOMAIN.EXAMPLE.COM -CN=machine.example.com - -[ sec1 ] -DNS = machine.example.com - -[ sec2 ] -subjectAltName = @sec1 -' > "$CONFIG" - -openssl req -new -config "$CONFIG" -out "$CSR" -key "$KEYFILE" "$@" -rm "$CONFIG" diff --git a/ipatests/test_ipaclient/data/test_csrgen/scripts/userCert_certutil.sh b/ipatests/test_ipaclient/data/test_csrgen/scripts/userCert_certutil.sh deleted file mode 100644 index 4aaeda0..0000000 --- a/ipatests/test_ipaclient/data/test_csrgen/scripts/userCert_certutil.sh +++ /dev/null @@ -1,11 +0,0 @@ -#!/bin/bash -e - -if [[ $# -lt 1 ]]; then -echo "Usage: $0 <outfile> [<any> <certutil> <args>]" -echo "Called as: $0 $@" -exit 1 -fi - -CSR="$1" -shift -certutil -R -a -z <(head -c 4096 /dev/urandom) -o "$CSR" -s CN=testuser,O=DOMAIN.EXAMPLE.COM --extSAN email:testu...@example.com "$@" diff --git a/ipatests/test_ipaclient/data/test_csrgen/scripts/userCert_openssl.sh b/ipatests/test_ipaclient/data/test_csrgen/scripts/userCert_openssl.sh deleted file mode 100644 index 2edf067..0000000 --- a/ipatests/test_ipaclient/data/test_csrgen/scripts/userCert_openssl.sh +++ /dev/null @@ -1,34 +0,0 @@ -#!/bin/bash -e - -if [[ $# -lt 2 ]]; then -echo "Usage: $0 <outfile> <keyfile> <other openssl arguments>" -echo "Called as: $0 $@" -exit 1 -fi - -CONFIG="$(mktemp)" -CSR="$1" -KEYFILE="$2" -shift; shift - -echo \ -'[ req ] -prompt = no -encrypt_key = no - -distinguished_name = sec0 -req_extensions = sec2 - -[ sec0 ] -O=DOMAIN.EXAMPLE.COM -CN=testuser - -[ sec1 ] -email = testu...@example.com - -[ sec2 ] -subjectAltName = @sec1 -' > "$CONFIG" - -openssl req -new -config "$CONFIG" -out "$CSR" -key "$KEYFILE" "$@" -rm "$CONFIG" diff --git a/ipatests/test_ipaclient/test_csrgen.py b/ipatests/test_ipaclient/test_csrgen.py index 556f8e0..ae127c9 100644 --- a/ipatests/test_ipaclient/test_csrgen.py +++ b/ipatests/test_ipaclient/test_csrgen.py @@ -36,7 +36,7 @@ def __init__(self): 'example', self.syntax_rule, [self.data_rule]) self.rules = [self.field_mapping] - def rules_for_profile(self, profile_id, helper): + def rules_for_profile(self, profile_id): return self.rules @@ -50,10 +50,6 @@ def _get_template_params(self, syntax_rules): return {'options': syntax_rules} -class IdentityCSRGenerator(csrgen.CSRGenerator): - FORMATTERS = {'identity': IdentityFormatter} - - class test_Formatter(object): def test_prepare_data_rule_with_data_source(self, formatter): data_rule = csrgen.Rule('uid', '{{subject.uid.0}}', @@ -139,40 +135,23 @@ class test_FileRuleProvider(object): def test_rule_basic(self, rule_provider): rule_name = 'basic' - rule1 = rule_provider._rule(rule_name, 'openssl') - rule2 = rule_provider._rule(rule_name, 'certutil') + rule = rule_provider._rule(rule_name) - assert rule1.template == 'openssl_rule' - assert rule2.template == 'certutil_rule' + assert rule.template == 'openssl_rule' def test_rule_global_options(self, rule_provider): rule_name = 'options' - rule1 = rule_provider._rule(rule_name, 'openssl') - rule2 = rule_provider._rule(rule_name, 'certutil') - - assert rule1.options['global_option'] is True - assert rule2.options['global_option'] is True - - def test_rule_helper_options(self, rule_provider): - rule_name = 'options' - - rule1 = rule_provider._rule(rule_name, 'openssl') - rule2 = rule_provider._rule(rule_name, 'certutil') + rule = rule_provider._rule(rule_name) - assert rule1.options['helper_option'] is True - assert 'helper_option' not in rule2.options + assert rule.options['rule_option'] is True def test_rule_nosuchrule(self, rule_provider): with pytest.raises(errors.NotFound): - rule_provider._rule('nosuchrule', 'openssl') - - def test_rule_nosuchhelper(self, rule_provider): - with pytest.raises(errors.EmptyResult): - rule_provider._rule('basic', 'nosuchhelper') + rule_provider._rule('nosuchrule') def test_rules_for_profile_success(self, rule_provider): - rules = rule_provider.rules_for_profile('profile', 'certutil') + rules = rule_provider.rules_for_profile('profile') assert len(rules) == 1 field_mapping = rules[0] @@ -182,7 +161,7 @@ def test_rules_for_profile_success(self, rule_provider): def test_rules_for_profile_nosuchprofile(self, rule_provider): with pytest.raises(errors.NotFound): - rule_provider.rules_for_profile('nosuchprofile', 'certutil') + rule_provider.rules_for_profile('nosuchprofile') class test_CSRGenerator(object): @@ -197,28 +176,9 @@ def test_userCert_OpenSSL(self, generator): ], } - script = generator.csr_script(principal, config, 'userCert', 'openssl') + script = generator.csr_script(principal, config, 'userCert') with open(os.path.join( - CSR_DATA_DIR, 'scripts', 'userCert_openssl.sh')) as f: - expected_script = f.read() - assert script == expected_script - - def test_userCert_Certutil(self, generator): - principal = { - 'uid': ['testuser'], - 'mail': ['testu...@example.com'], - } - config = { - 'ipacertificatesubjectbase': [ - 'O=DOMAIN.EXAMPLE.COM' - ], - } - - script = generator.csr_script( - principal, config, 'userCert', 'certutil') - - with open(os.path.join( - CSR_DATA_DIR, 'scripts', 'userCert_certutil.sh')) as f: + CSR_DATA_DIR, 'configs', 'userCert.conf')) as f: expected_script = f.read() assert script == expected_script @@ -235,28 +195,9 @@ def test_caIPAserviceCert_OpenSSL(self, generator): } script = generator.csr_script( - principal, config, 'caIPAserviceCert', 'openssl') - with open(os.path.join( - CSR_DATA_DIR, 'scripts', 'caIPAserviceCert_openssl.sh')) as f: - expected_script = f.read() - assert script == expected_script - - def test_caIPAserviceCert_Certutil(self, generator): - principal = { - 'krbprincipalname': [ - 'HTTP/machine.example....@domain.example.com' - ], - } - config = { - 'ipacertificatesubjectbase': [ - 'O=DOMAIN.EXAMPLE.COM' - ], - } - - script = generator.csr_script( - principal, config, 'caIPAserviceCert', 'certutil') + principal, config, 'caIPAserviceCert') with open(os.path.join( - CSR_DATA_DIR, 'scripts', 'caIPAserviceCert_certutil.sh')) as f: + CSR_DATA_DIR, 'configs', 'caIPAserviceCert.conf')) as f: expected_script = f.read() assert script == expected_script @@ -267,10 +208,11 @@ def test_optionalAttributeMissing(self, generator): rule_provider = StubRuleProvider() rule_provider.data_rule.template = '{{subject.mail}}' rule_provider.data_rule.options = {'data_source': 'subject.mail'} - generator = IdentityCSRGenerator(rule_provider) + generator = csrgen.CSRGenerator( + rule_provider, formatter_class=IdentityFormatter) script = generator.csr_script( - principal, {}, 'example', 'identity') + principal, {}, 'example') assert script == '\n' def test_twoDataRulesOneMissing(self, generator): @@ -280,9 +222,10 @@ def test_twoDataRulesOneMissing(self, generator): rule_provider.data_rule.options = {'data_source': 'subject.mail'} rule_provider.field_mapping.data_rules.append(csrgen.Rule( 'data2', '{{subject.uid}}', {'data_source': 'subject.uid'})) - generator = IdentityCSRGenerator(rule_provider) + generator = csrgen.CSRGenerator( + rule_provider, formatter_class=IdentityFormatter) - script = generator.csr_script(principal, {}, 'example', 'identity') + script = generator.csr_script(principal, {}, 'example') assert script == ',testuser\n' def test_requiredAttributeMissing(self): @@ -291,8 +234,9 @@ def test_requiredAttributeMissing(self): rule_provider.data_rule.template = '{{subject.mail}}' rule_provider.data_rule.options = {'data_source': 'subject.mail'} rule_provider.syntax_rule.options = {'required': True} - generator = IdentityCSRGenerator(rule_provider) + generator = csrgen.CSRGenerator( + rule_provider, formatter_class=IdentityFormatter) with pytest.raises(errors.CSRTemplateError): _script = generator.csr_script( - principal, {}, 'example', 'identity') + principal, {}, 'example') From 3a6b0a520ef395dcc578d892e38906af1fd75a6d Mon Sep 17 00:00:00 2001 From: Ben Lipton <blip...@redhat.com> Date: Tue, 21 Mar 2017 17:23:46 -0400 Subject: [PATCH 2/4] csrgen: Change to pure openssl config format (no script) https://pagure.io/freeipa/issue/4899 --- ipaclient/csrgen.py | 10 +++++----- ipaclient/csrgen/templates/openssl_base.tmpl | 22 ++-------------------- ipaclient/plugins/csrgen.py | 3 +-- .../data/test_csrgen/configs/caIPAserviceCert.conf | 20 +------------------- .../data/test_csrgen/configs/userCert.conf | 20 +------------------- ipatests/test_ipaclient/test_csrgen.py | 10 +++++----- 6 files changed, 15 insertions(+), 70 deletions(-) diff --git a/ipaclient/csrgen.py b/ipaclient/csrgen.py index 8ca0722..eca99a1 100644 --- a/ipaclient/csrgen.py +++ b/ipaclient/csrgen.py @@ -66,7 +66,7 @@ class Formatter(object): Class for processing a set of CSR generation rules into a template. The template can be rendered with user and database data to produce a - script, which generates a CSR when run. + config, which specifies how to build a CSR. Subclasses of Formatter should set the value of base_template_name to the filename of a base template with spaces for the processed rules. @@ -214,7 +214,7 @@ def _get_template_params(self, syntax_rules): class OpenSSLFormatter(Formatter): - """Formatter class supporting the openssl command-line tool.""" + """Formatter class generating the openssl config-file format.""" base_template_name = 'openssl_base.tmpl' @@ -359,17 +359,17 @@ def __init__(self, rule_provider, formatter_class=OpenSSLFormatter): self.rule_provider = rule_provider self.formatter = formatter_class() - def csr_script(self, principal, config, profile_id): + def csr_config(self, principal, config, profile_id): render_data = {'subject': principal, 'config': config} rules = self.rule_provider.rules_for_profile(profile_id) template = self.formatter.build_template(rules) try: - script = template.render(render_data) + config = template.render(render_data) except jinja2.UndefinedError: logger.debug(traceback.format_exc()) raise errors.CSRTemplateError(reason=_( 'Template error when formatting certificate data')) - return script + return config diff --git a/ipaclient/csrgen/templates/openssl_base.tmpl b/ipaclient/csrgen/templates/openssl_base.tmpl index 22b1686..8d37994 100644 --- a/ipaclient/csrgen/templates/openssl_base.tmpl +++ b/ipaclient/csrgen/templates/openssl_base.tmpl @@ -1,21 +1,6 @@ {% raw -%} {% import "openssl_macros.tmpl" as openssl -%} -{%- endraw %} -#!/bin/bash -e - -if [[ $# -lt 2 ]]; then -echo "Usage: $0 <outfile> <keyfile> <other openssl arguments>" -echo "Called as: $0 $@" -exit 1 -fi - -CONFIG="$(mktemp)" -CSR="$1" -KEYFILE="$2" -shift; shift - -echo \ -{% raw %}{% filter quote %}{% endraw -%} +{% endraw -%} [ req ] prompt = no encrypt_key = no @@ -29,7 +14,4 @@ encrypt_key = no req_extensions = {% call openssl.section() %}{{ rendered_extensions }}{% endcall %} {% endif %} {{ openssl.openssl_sections|join('\n\n') }} -{% endfilter %}{%- endraw %} > "$CONFIG" - -openssl req -new -config "$CONFIG" -out "$CSR" -key "$KEYFILE" "$@" -rm "$CONFIG" +{%- endraw %} diff --git a/ipaclient/plugins/csrgen.py b/ipaclient/plugins/csrgen.py index c10ef2d..15ed791 100644 --- a/ipaclient/plugins/csrgen.py +++ b/ipaclient/plugins/csrgen.py @@ -105,8 +105,7 @@ def execute(self, *args, **options): generator = CSRGenerator(FileRuleProvider()) - script = generator.csr_script( - principal_obj, config, profile_id) + script = generator.csr_config(principal_obj, config, profile_id) result = {} if 'out' in options: diff --git a/ipatests/test_ipaclient/data/test_csrgen/configs/caIPAserviceCert.conf b/ipatests/test_ipaclient/data/test_csrgen/configs/caIPAserviceCert.conf index 811bfd7..3724bdc 100644 --- a/ipatests/test_ipaclient/data/test_csrgen/configs/caIPAserviceCert.conf +++ b/ipatests/test_ipaclient/data/test_csrgen/configs/caIPAserviceCert.conf @@ -1,18 +1,4 @@ -#!/bin/bash -e - -if [[ $# -lt 2 ]]; then -echo "Usage: $0 <outfile> <keyfile> <other openssl arguments>" -echo "Called as: $0 $@" -exit 1 -fi - -CONFIG="$(mktemp)" -CSR="$1" -KEYFILE="$2" -shift; shift - -echo \ -'[ req ] +[ req ] prompt = no encrypt_key = no @@ -28,7 +14,3 @@ DNS = machine.example.com [ sec2 ] subjectAltName = @sec1 -' > "$CONFIG" - -openssl req -new -config "$CONFIG" -out "$CSR" -key "$KEYFILE" "$@" -rm "$CONFIG" diff --git a/ipatests/test_ipaclient/data/test_csrgen/configs/userCert.conf b/ipatests/test_ipaclient/data/test_csrgen/configs/userCert.conf index 2edf067..00d63de 100644 --- a/ipatests/test_ipaclient/data/test_csrgen/configs/userCert.conf +++ b/ipatests/test_ipaclient/data/test_csrgen/configs/userCert.conf @@ -1,18 +1,4 @@ -#!/bin/bash -e - -if [[ $# -lt 2 ]]; then -echo "Usage: $0 <outfile> <keyfile> <other openssl arguments>" -echo "Called as: $0 $@" -exit 1 -fi - -CONFIG="$(mktemp)" -CSR="$1" -KEYFILE="$2" -shift; shift - -echo \ -'[ req ] +[ req ] prompt = no encrypt_key = no @@ -28,7 +14,3 @@ email = testu...@example.com [ sec2 ] subjectAltName = @sec1 -' > "$CONFIG" - -openssl req -new -config "$CONFIG" -out "$CSR" -key "$KEYFILE" "$@" -rm "$CONFIG" diff --git a/ipatests/test_ipaclient/test_csrgen.py b/ipatests/test_ipaclient/test_csrgen.py index ae127c9..d0798f8 100644 --- a/ipatests/test_ipaclient/test_csrgen.py +++ b/ipatests/test_ipaclient/test_csrgen.py @@ -176,7 +176,7 @@ def test_userCert_OpenSSL(self, generator): ], } - script = generator.csr_script(principal, config, 'userCert') + script = generator.csr_config(principal, config, 'userCert') with open(os.path.join( CSR_DATA_DIR, 'configs', 'userCert.conf')) as f: expected_script = f.read() @@ -194,7 +194,7 @@ def test_caIPAserviceCert_OpenSSL(self, generator): ], } - script = generator.csr_script( + script = generator.csr_config( principal, config, 'caIPAserviceCert') with open(os.path.join( CSR_DATA_DIR, 'configs', 'caIPAserviceCert.conf')) as f: @@ -211,7 +211,7 @@ def test_optionalAttributeMissing(self, generator): generator = csrgen.CSRGenerator( rule_provider, formatter_class=IdentityFormatter) - script = generator.csr_script( + script = generator.csr_config( principal, {}, 'example') assert script == '\n' @@ -225,7 +225,7 @@ def test_twoDataRulesOneMissing(self, generator): generator = csrgen.CSRGenerator( rule_provider, formatter_class=IdentityFormatter) - script = generator.csr_script(principal, {}, 'example') + script = generator.csr_config(principal, {}, 'example') assert script == ',testuser\n' def test_requiredAttributeMissing(self): @@ -238,5 +238,5 @@ def test_requiredAttributeMissing(self): rule_provider, formatter_class=IdentityFormatter) with pytest.raises(errors.CSRTemplateError): - _script = generator.csr_script( + _script = generator.csr_config( principal, {}, 'example') From 51237f7b4f9549d512f47fdb09f29a61912994ac Mon Sep 17 00:00:00 2001 From: Ben Lipton <blip...@redhat.com> Date: Fri, 6 Jan 2017 11:19:19 -0500 Subject: [PATCH 3/4] csrgen: Modify cert_get_requestdata to return a CertificationRequestInfo Also modify cert_request to use this new format. Note, only PEM private keys are supported for now. NSS databases are not. https://pagure.io/freeipa/issue/4899 --- ipaclient/csrgen.py | 75 +++++++++++- ipaclient/csrgen_ffi.py | 291 ++++++++++++++++++++++++++++++++++++++++++++ ipaclient/plugins/cert.py | 75 +++++------- ipaclient/plugins/csrgen.py | 35 +++--- 4 files changed, 415 insertions(+), 61 deletions(-) create mode 100644 ipaclient/csrgen_ffi.py diff --git a/ipaclient/csrgen.py b/ipaclient/csrgen.py index eca99a1..04d2821 100644 --- a/ipaclient/csrgen.py +++ b/ipaclient/csrgen.py @@ -7,13 +7,22 @@ import json import os.path import pipes +import subprocess import traceback import pkg_resources +from cryptography.hazmat.backends import default_backend +from cryptography.hazmat.primitives.asymmetric import padding +from cryptography.hazmat.primitives import hashes +from cryptography.hazmat.primitives.serialization import ( + load_pem_private_key, Encoding, PublicFormat) import jinja2 import jinja2.ext import jinja2.sandbox +from pyasn1.codec.der import decoder, encoder +from pyasn1.type import univ +from pyasn1_modules import rfc2314 import six from ipalib import api @@ -56,7 +65,8 @@ def quote(self, data): def required(self, data, name): if not data: raise errors.CSRTemplateError( - reason=_('Required CSR generation rule %(name)s is missing data') % + reason=_( + 'Required CSR generation rule %(name)s is missing data') % {'name': name}) return data @@ -373,3 +383,66 @@ def csr_config(self, principal, config, profile_id): 'Template error when formatting certificate data')) return config + + +class CSRLibraryAdaptor(object): + def get_subject_public_key_info(self): + raise NotImplementedError('Use a subclass of CSRLibraryAdaptor') + + def sign_csr(self, certification_request_info): + """Sign a CertificationRequestInfo. + + Returns: str, a DER-encoded signed CSR. + """ + raise NotImplementedError('Use a subclass of CSRLibraryAdaptor') + + +class OpenSSLAdaptor(object): + def __init__(self, key_filename, password_filename): + self.key_filename = key_filename + self.password_filename = password_filename + + def key(self): + with open(self.key_filename, 'r') as key_file: + key_bytes = key_file.read() + password = None + if self.password_filename is not None: + with open(self.password_filename, 'r') as password_file: + password = password_file.read().strip() + + key = load_pem_private_key(key_bytes, password, default_backend()) + return key + + def get_subject_public_key_info(self): + pubkey_info = self.key().public_key().public_bytes( + Encoding.DER, PublicFormat.SubjectPublicKeyInfo) + return pubkey_info + + def sign_csr(self, certification_request_info): + reqinfo = decoder.decode( + certification_request_info, rfc2314.CertificationRequestInfo())[0] + csr = rfc2314.CertificationRequest() + csr.setComponentByName('certificationRequestInfo', reqinfo) + + algorithm = rfc2314.SignatureAlgorithmIdentifier() + algorithm.setComponentByName( + 'algorithm', univ.ObjectIdentifier( + '1.2.840.113549.1.1.11')) # sha256WithRSAEncryption + csr.setComponentByName('signatureAlgorithm', algorithm) + + signature = self.key().sign( + certification_request_info, + padding.PKCS1v15(), + hashes.SHA256() + ) + asn1sig = univ.BitString("'%s'H" % signature.encode('hex')) + csr.setComponentByName('signature', asn1sig) + return encoder.encode(csr) + + +class NSSAdaptor(object): + def get_subject_public_key_info(self): + raise NotImplementedError('NSS is not yet supported') + + def sign_csr(self, certification_request_info): + raise NotImplementedError('NSS is not yet supported') diff --git a/ipaclient/csrgen_ffi.py b/ipaclient/csrgen_ffi.py new file mode 100644 index 0000000..c45db5f --- /dev/null +++ b/ipaclient/csrgen_ffi.py @@ -0,0 +1,291 @@ +#!/usr/bin/python + +from cffi import FFI +import ctypes.util + +from ipalib import errors + +_ffi = FFI() + +_ffi.cdef(''' +typedef ... CONF; +typedef ... CONF_METHOD; +typedef ... BIO; +typedef ... ipa_STACK_OF_CONF_VALUE; + +/* openssl/conf.h */ +typedef struct { + char *section; + char *name; + char *value; +} CONF_VALUE; + +CONF *NCONF_new(CONF_METHOD *meth); +void NCONF_free(CONF *conf); +int NCONF_load_bio(CONF *conf, BIO *bp, long *eline); +ipa_STACK_OF_CONF_VALUE *NCONF_get_section(const CONF *conf, + const char *section); +char *NCONF_get_string(const CONF *conf, const char *group, const char *name); + +/* openssl/safestack.h */ +// int sk_CONF_VALUE_num(ipa_STACK_OF_CONF_VALUE *); +// CONF_VALUE *sk_CONF_VALUE_value(ipa_STACK_OF_CONF_VALUE *, int); + +/* openssl/stack.h */ +typedef ... _STACK; + +int sk_num(const _STACK *); +void *sk_value(const _STACK *, int); + +/* openssl/bio.h */ +BIO *BIO_new_mem_buf(const void *buf, int len); +int BIO_free(BIO *a); + +/* openssl/asn1.h */ +typedef struct ASN1_ENCODING_st { + unsigned char *enc; /* DER encoding */ + long len; /* Length of encoding */ + int modified; /* set to 1 if 'enc' is invalid */ +} ASN1_ENCODING; + +/* openssl/evp.h */ +typedef ... EVP_PKEY; + +void EVP_PKEY_free(EVP_PKEY *pkey); + +/* openssl/x509.h */ +typedef ... ASN1_INTEGER; +typedef ... ASN1_BIT_STRING; +typedef ... X509; +typedef ... X509_ALGOR; +typedef ... X509_CRL; +typedef ... X509_NAME; +typedef ... X509_PUBKEY; +typedef ... ipa_STACK_OF_X509_ATTRIBUTE; + +typedef struct X509_req_info_st { + ASN1_ENCODING enc; + ASN1_INTEGER *version; + X509_NAME *subject; + X509_PUBKEY *pubkey; + /* d=2 hl=2 l= 0 cons: cont: 00 */ + ipa_STACK_OF_X509_ATTRIBUTE *attributes; /* [ 0 ] */ +} X509_REQ_INFO; + +typedef struct X509_req_st { + X509_REQ_INFO *req_info; + X509_ALGOR *sig_alg; + ASN1_BIT_STRING *signature; + int references; +} X509_REQ; + +X509_REQ *X509_REQ_new(void); +void X509_REQ_free(X509_REQ *); +EVP_PKEY *d2i_PUBKEY_bio(BIO *bp, EVP_PKEY **a); +int X509_REQ_set_pubkey(X509_REQ *x, EVP_PKEY *pkey); +int X509_NAME_add_entry_by_txt(X509_NAME *name, const char *field, int type, + const unsigned char *bytes, int len, int loc, + int set); +int X509_NAME_entry_count(X509_NAME *name); +int i2d_X509_REQ_INFO(X509_REQ_INFO *a, unsigned char **out); \ + +/* openssl/x509v3.h */ +typedef ... X509V3_CONF_METHOD; + +typedef struct v3_ext_ctx { + int flags; + X509 *issuer_cert; + X509 *subject_cert; + X509_REQ *subject_req; + X509_CRL *crl; + X509V3_CONF_METHOD *db_meth; + void *db; +} X509V3_CTX; + +void X509V3_set_ctx(X509V3_CTX *ctx, X509 *issuer, X509 *subject, + X509_REQ *req, X509_CRL *crl, int flags); +void X509V3_set_nconf(X509V3_CTX *ctx, CONF *conf); +int X509V3_EXT_REQ_add_nconf(CONF *conf, X509V3_CTX *ctx, char *section, + X509_REQ *req); + +/* openssl/x509v3.h */ +unsigned long ERR_get_error(void); +char *ERR_error_string(unsigned long e, char *buf); +''') + +_libcrypto = _ffi.dlopen(ctypes.util.find_library('crypto')) + +NULL = _ffi.NULL + +# openssl/conf.h +NCONF_new = _libcrypto.NCONF_new +NCONF_free = _libcrypto.NCONF_free +NCONF_load_bio = _libcrypto.NCONF_load_bio +NCONF_get_section = _libcrypto.NCONF_get_section +NCONF_get_string = _libcrypto.NCONF_get_string + +# openssl/stack.h +sk_num = _libcrypto.sk_num +sk_value = _libcrypto.sk_value + + +def sk_CONF_VALUE_num(sk): + return sk_num(_ffi.cast("_STACK *", sk)) + + +def sk_CONF_VALUE_value(sk, i): + return _ffi.cast("CONF_VALUE *", sk_value(_ffi.cast("_STACK *", sk), i)) + + +# openssl/bio.h +BIO_new_mem_buf = _libcrypto.BIO_new_mem_buf +BIO_free = _libcrypto.BIO_free + +# openssl/x509.h +X509_REQ_new = _libcrypto.X509_REQ_new +X509_REQ_free = _libcrypto.X509_REQ_free +X509_REQ_set_pubkey = _libcrypto.X509_REQ_set_pubkey +d2i_PUBKEY_bio = _libcrypto.d2i_PUBKEY_bio +i2d_X509_REQ_INFO = _libcrypto.i2d_X509_REQ_INFO +X509_NAME_add_entry_by_txt = _libcrypto.X509_NAME_add_entry_by_txt +X509_NAME_entry_count = _libcrypto.X509_NAME_entry_count + + +def X509_REQ_get_subject_name(req): + return req.req_info.subject + +# openssl/evp.h +EVP_PKEY_free = _libcrypto.EVP_PKEY_free + +# openssl/asn1.h +MBSTRING_UTF8 = 0x1000 + +# openssl/x509v3.h +X509V3_set_ctx = _libcrypto.X509V3_set_ctx +X509V3_set_nconf = _libcrypto.X509V3_set_nconf +X509V3_EXT_REQ_add_nconf = _libcrypto.X509V3_EXT_REQ_add_nconf + +# openssl/err.h +ERR_get_error = _libcrypto.ERR_get_error +ERR_error_string = _libcrypto.ERR_error_string + + +def _raise_openssl_errors(): + msgs = [] + + code = ERR_get_error() + while code != 0: + msg = ERR_error_string(code, NULL) + msgs.append(_ffi.string(msg)) + code = ERR_get_error() + + raise errors.CSRTemplateError(reason='\n'.join(msgs)) + + +def _parse_dn_section(subj, dn_sk): + for i in range(sk_CONF_VALUE_num(dn_sk)): + v = sk_CONF_VALUE_value(dn_sk, i) + rdn_type = _ffi.string(v.name) + + # Skip past any leading X. X: X, etc to allow for multiple instances + for idx, c in enumerate(rdn_type): + if c in ':,.': + if idx+1 < len(rdn_type): + rdn_type = rdn_type[idx+1:] + break + if rdn_type.startswith('+'): + rdn_type = rdn_type[1:] + mval = -1 + else: + mval = 0 + if not X509_NAME_add_entry_by_txt( + subj, rdn_type, MBSTRING_UTF8, v.value, -1, -1, mval): + _raise_openssl_errors() + + if not X509_NAME_entry_count(subj): + raise errors.CSRTemplateError( + reason='error, subject in config file is empty') + + +def build_requestinfo(config, public_key_info): + reqdata = NULL + req = NULL + nconf_bio = NULL + pubkey_bio = NULL + pubkey = NULL + + try: + reqdata = NCONF_new(NULL) + if reqdata == NULL: + _raise_openssl_errors() + + nconf_bio = BIO_new_mem_buf(config, len(config)) + errorline = _ffi.new('long[1]', [-1]) + i = NCONF_load_bio(reqdata, nconf_bio, errorline) + if i < 0: + if errorline[0] < 0: + raise errors.CSRTemplateError(reason="Can't load config file") + else: + raise errors.CSRTemplateError( + reason='Error on line %d of config file' % errorline[0]) + + dn_sect = NCONF_get_string(reqdata, 'req', 'distinguished_name') + if dn_sect == NULL: + raise errors.CSRTemplateError( + reason='Unable to find "distinguished_name" key in config') + + dn_sk = NCONF_get_section(reqdata, dn_sect) + if dn_sk == NULL: + raise errors.CSRTemplateError( + reason='Unable to find "%s" section in config' % + _ffi.string(dn_sect)) + + pubkey_bio = BIO_new_mem_buf(public_key_info, len(public_key_info)) + pubkey = d2i_PUBKEY_bio(pubkey_bio, NULL) + if pubkey == NULL: + _raise_openssl_errors() + + req = X509_REQ_new() + if req == NULL: + _raise_openssl_errors() + + subject = X509_REQ_get_subject_name(req) + + _parse_dn_section(subject, dn_sk) + + if not X509_REQ_set_pubkey(req, pubkey): + _raise_openssl_errors() + + ext_ctx = _ffi.new("X509V3_CTX[1]") + X509V3_set_ctx(ext_ctx, NULL, NULL, req, NULL, 0) + X509V3_set_nconf(ext_ctx, reqdata) + + extn_section = NCONF_get_string(reqdata, "req", "req_extensions") + if extn_section != NULL: + if not X509V3_EXT_REQ_add_nconf( + reqdata, ext_ctx, extn_section, req): + _raise_openssl_errors() + + der_len = i2d_X509_REQ_INFO(req.req_info, NULL) + if der_len < 0: + _raise_openssl_errors() + + der_buf = _ffi.new("unsigned char[%d]" % der_len) + der_out = _ffi.new("unsigned char **", der_buf) + der_len = i2d_X509_REQ_INFO(req.req_info, der_out) + if der_len < 0: + _raise_openssl_errors() + + return _ffi.buffer(der_buf, der_len) + + finally: + if reqdata != NULL: + NCONF_free(reqdata) + if req != NULL: + X509_REQ_free(req) + if nconf_bio != NULL: + BIO_free(nconf_bio) + if pubkey_bio != NULL: + BIO_free(pubkey_bio) + if pubkey != NULL: + EVP_PKEY_free(pubkey) diff --git a/ipaclient/plugins/cert.py b/ipaclient/plugins/cert.py index 9ec6970..a4ee9a9 100644 --- a/ipaclient/plugins/cert.py +++ b/ipaclient/plugins/cert.py @@ -20,11 +20,10 @@ # along with this program. If not, see <http://www.gnu.org/licenses/>. import base64 -import subprocess -from tempfile import NamedTemporaryFile as NTF import six +from ipaclient import csrgen from ipaclient.frontend import MethodOverride from ipalib import errors from ipalib import x509 @@ -108,54 +107,40 @@ def forward(self, csr=None, **options): if csr is None: if database: - helper = u'certutil' - helper_args = ['-d', database] - if password_file: - helper_args += ['-f', password_file] + adaptor = csrgen.NSSAdaptor(database, password_file) elif private_key: - helper = u'openssl' - helper_args = [private_key] - if password_file: - helper_args += ['-passin', 'file:%s' % password_file] + adaptor = csrgen.OpenSSLAdaptor(private_key, password_file) else: raise errors.InvocationError( message=u"One of 'database' or 'private_key' is required") - with NTF() as scriptfile, NTF() as csrfile: - # 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, - 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'))) + pubkey_info = adaptor.get_subject_public_key_info() + pubkey_info_b64 = base64.b64encode(pubkey_info) + + # 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') + + response = self.api.Command.cert_get_requestdata( + profile_id=profile_id, + principal=options.get('principal'), + public_key_info=unicode(pubkey_info_b64)) + + req_info_b64 = response['result']['request_info'] + req_info = base64.b64decode(req_info_b64) + + csr = adaptor.sign_csr(req_info) + + if not csr: + raise errors.CertificateOperationError( + error=(_('Generated CSR was empty'))) + + # cert_request requires the CSR to be base64-encoded (but PEM + # header and footer are not required) + csr = unicode(base64.b64encode(csr)) else: if database is not None or private_key is not None: raise errors.MutuallyExclusiveError(reason=_( diff --git a/ipaclient/plugins/csrgen.py b/ipaclient/plugins/csrgen.py index 15ed791..568a79f 100644 --- a/ipaclient/plugins/csrgen.py +++ b/ipaclient/plugins/csrgen.py @@ -2,15 +2,18 @@ # Copyright (C) 2016 FreeIPA Contributors see COPYING for license # +import base64 + import six -from ipaclient.csrgen import CSRGenerator, FileRuleProvider +from ipaclient import csrgen +from ipaclient import csrgen_ffi from ipalib import api from ipalib import errors from ipalib import output from ipalib import util from ipalib.frontend import Local, Str -from ipalib.parameters import Principal +from ipalib.parameters import File, Principal from ipalib.plugable import Registry from ipalib.text import _ from ipapython import dogtag @@ -43,15 +46,14 @@ class cert_get_requestdata(Local): label=_('Profile ID'), doc=_('CSR Generation Profile to use'), ), - Str( - 'helper', - label=_('Name of CSR generation tool'), - doc=_('Name of tool (e.g. openssl, certutil) that will be used to' - ' create CSR'), + File( + 'public_key_info', + label=_('Subject Public Key Info'), + doc=_('DER-encoded SubjectPublicKeyInfo structure'), ), Str( 'out?', - doc=_('Write CSR generation script to file'), + doc=_('Write CertificationRequestInfo to file'), ), ) @@ -65,8 +67,8 @@ class cert_get_requestdata(Local): has_output_params = ( Str( - 'script', - label=_('Generation script'), + 'request_info', + label=_('CertificationRequestInfo structure'), ) ) @@ -78,7 +80,8 @@ def execute(self, *args, **options): profile_id = options.get('profile_id') if profile_id is None: profile_id = dogtag.DEFAULT_PROFILE - helper = options.get('helper') + public_key_info = options.get('public_key_info') + public_key_info = base64.b64decode(public_key_info) if self.api.env.in_server: backend = self.api.Backend.ldap2 @@ -103,16 +106,18 @@ def execute(self, *args, **options): principal_obj = principal_obj['result'] config = api.Command.config_show()['result'] - generator = CSRGenerator(FileRuleProvider()) + generator = csrgen.CSRGenerator(csrgen.FileRuleProvider()) - script = generator.csr_config(principal_obj, config, profile_id) + csr_config = generator.csr_config(principal_obj, config, profile_id) + request_info = base64.b64encode(csrgen_ffi.build_requestinfo( + csr_config.encode('utf8'), public_key_info)) result = {} if 'out' in options: with open(options['out'], 'wb') as f: - f.write(script) + f.write(request_info) else: - result = dict(script=script) + result = dict(request_info=request_info) return dict( result=result From f6f687de60d1a279a16c795dc87405ccc189d9b9 Mon Sep 17 00:00:00 2001 From: Ben Lipton <blip...@redhat.com> Date: Mon, 30 Jan 2017 10:51:11 -0500 Subject: [PATCH 4/4] csrgen: Beginnings of NSS database support https://pagure.io/freeipa/issue/4899 --- ipaclient/csrgen.py | 27 ++++++++++++++++++++++++++- 1 file changed, 26 insertions(+), 1 deletion(-) diff --git a/ipaclient/csrgen.py b/ipaclient/csrgen.py index 04d2821..0f52a8b 100644 --- a/ipaclient/csrgen.py +++ b/ipaclient/csrgen.py @@ -2,9 +2,11 @@ # Copyright (C) 2016 FreeIPA Contributors see COPYING for license # +import base64 import collections import errno import json +import os import os.path import pipes import subprocess @@ -17,6 +19,7 @@ from cryptography.hazmat.primitives import hashes from cryptography.hazmat.primitives.serialization import ( load_pem_private_key, Encoding, PublicFormat) +from cryptography.x509 import load_pem_x509_certificate import jinja2 import jinja2.ext import jinja2.sandbox @@ -441,8 +444,30 @@ def sign_csr(self, certification_request_info): class NSSAdaptor(object): + def __init__(self, database, password_filename): + self.database = database + self.password_filename = password_filename + self.nickname = base64.b32encode(os.urandom(40)) + def get_subject_public_key_info(self): - raise NotImplementedError('NSS is not yet supported') + temp_cn = base64.b32encode(os.urandom(40)) + + password_args = [] + if self.password_filename is not None: + password_args = ['-f', self.password_filename] + + subprocess.check_call( + ['certutil', '-S', '-n', self.nickname, '-s', 'CN=%s' % temp_cn, + '-x', '-t', ',,', '-d', self.database] + password_args) + cert_pem = subprocess.check_output( + ['certutil', '-L', '-n', self.nickname, '-a', + '-d', self.database] + password_args) + + cert = load_pem_x509_certificate(cert_pem, default_backend()) + pubkey_info = cert.public_key().public_bytes( + Encoding.DER, PublicFormat.SubjectPublicKeyInfo) + + return pubkey_info def sign_csr(self, certification_request_info): raise NotImplementedError('NSS is not yet supported')
-- 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