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

Reply via email to