From 0a49ee70dd99d88522761425cbdb9412d570aeb1 Mon Sep 17 00:00:00 2001 From: Ben Lipton Date: Tue, 5 Jul 2016 14:19:35 -0400 Subject: [PATCH 1/4] Add code to generate scripts that generate CSRs Adds a library that uses jinja2 to format a script that, when run, will build a CSR. Also adds a CLI command, 'cert-get-requestdata', that uses this library and builds the script for a given principal. The rules are read from json files in /usr/share/ipa/csr, but the rule provider is a separate class so that it can be replaced easily. https://fedorahosted.org/freeipa/ticket/4899 --- freeipa.spec.in | 8 + install/configure.ac | 1 + install/share/Makefile.am | 1 + install/share/csr/Makefile.am | 27 +++ install/share/csr/templates/certutil_base.tmpl | 14 ++ install/share/csr/templates/ipa_macros.tmpl | 42 ++++ install/share/csr/templates/openssl_base.tmpl | 35 +++ install/share/csr/templates/openssl_macros.tmpl | 29 +++ ipaclient/plugins/certmapping.py | 105 +++++++++ ipalib/certmapping.py | 285 ++++++++++++++++++++++++ ipalib/errors.py | 9 + ipapython/templating.py | 31 +++ 12 files changed, 587 insertions(+) create mode 100644 install/share/csr/Makefile.am create mode 100644 install/share/csr/templates/certutil_base.tmpl create mode 100644 install/share/csr/templates/ipa_macros.tmpl create mode 100644 install/share/csr/templates/openssl_base.tmpl create mode 100644 install/share/csr/templates/openssl_macros.tmpl create mode 100644 ipaclient/plugins/certmapping.py create mode 100644 ipalib/certmapping.py create mode 100644 ipapython/templating.py diff --git a/freeipa.spec.in b/freeipa.spec.in index d55ab4f..beec87a 100644 --- a/freeipa.spec.in +++ b/freeipa.spec.in @@ -503,6 +503,7 @@ Requires: python-custodia Requires: python-dns >= 1.11.1 Requires: python-netifaces >= 0.10.4 Requires: pyusb +Requires: python-jinja2 Conflicts: %{alt_name}-python < %{version} @@ -1174,6 +1175,13 @@ fi %{_usr}/share/ipa/advise/legacy/*.template %dir %{_usr}/share/ipa/profiles %{_usr}/share/ipa/profiles/*.cfg +%dir %{_usr}/share/ipa/csr +%dir %{_usr}/share/ipa/csr/templates +%{_usr}/share/ipa/csr/templates/*.tmpl +%dir %{_usr}/share/ipa/csr/profiles +%{_usr}/share/ipa/csr/profiles/*.json +%dir %{_usr}/share/ipa/csr/rules +%{_usr}/share/ipa/csr/rules/*.json %dir %{_usr}/share/ipa/ffextension %{_usr}/share/ipa/ffextension/bootstrap.js %{_usr}/share/ipa/ffextension/install.rdf diff --git a/install/configure.ac b/install/configure.ac index 81f17b9..365f0e9 100644 --- a/install/configure.ac +++ b/install/configure.ac @@ -87,6 +87,7 @@ AC_CONFIG_FILES([ share/Makefile share/advise/Makefile share/advise/legacy/Makefile + share/csr/Makefile share/profiles/Makefile share/schema.d/Makefile ui/Makefile diff --git a/install/share/Makefile.am b/install/share/Makefile.am index d8845ee..0a15635 100644 --- a/install/share/Makefile.am +++ b/install/share/Makefile.am @@ -2,6 +2,7 @@ NULL = SUBDIRS = \ advise \ + csr \ profiles \ schema.d \ $(NULL) diff --git a/install/share/csr/Makefile.am b/install/share/csr/Makefile.am new file mode 100644 index 0000000..5a8ef5c --- /dev/null +++ b/install/share/csr/Makefile.am @@ -0,0 +1,27 @@ +NULL = + +profiledir = $(IPA_DATA_DIR)/csr/profiles +profile_DATA = \ + $(NULL) + +ruledir = $(IPA_DATA_DIR)/csr/rules +rule_DATA = \ + $(NULL) + +templatedir = $(IPA_DATA_DIR)/csr/templates +template_DATA = \ + templates/certutil_base.tmpl \ + templates/openssl_base.tmpl \ + templates/openssl_macros.tmpl \ + templates/ipa_macros.tmpl \ + $(NULL) + +EXTRA_DIST = \ + $(profile_DATA) \ + $(rule_DATA) \ + $(template_DATA) \ + $(NULL) + +MAINTAINERCLEANFILES = \ + *~ \ + Makefile.in diff --git a/install/share/csr/templates/certutil_base.tmpl b/install/share/csr/templates/certutil_base.tmpl new file mode 100644 index 0000000..6c6425f --- /dev/null +++ b/install/share/csr/templates/certutil_base.tmpl @@ -0,0 +1,14 @@ +{% raw -%} +{% import "ipa_macros.tmpl" as ipa -%} +{%- endraw %} +#!/bin/bash -e + +if [[ $# -lt 1 ]]; then +echo "Usage: $0 [ ]" +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/install/share/csr/templates/ipa_macros.tmpl b/install/share/csr/templates/ipa_macros.tmpl new file mode 100644 index 0000000..e790d4e --- /dev/null +++ b/install/share/csr/templates/ipa_macros.tmpl @@ -0,0 +1,42 @@ +{% set rendersyntax = {} %} + +{% set renderdata = {} %} + +{# Wrapper for syntax rules. We render the contents of the rule into a +variable, so that if we find that none of the contained data rules rendered we +can suppress the whole syntax rule. That is, a syntax rule is rendered either +if no data rules are specified (unusual) or if at least one of the data rules +rendered successfully. #} +{% macro syntaxrule() -%} +{% do rendersyntax.update(none=true, any=false) -%} +{% set contents -%} +{{ caller() -}} +{% endset -%} +{% if rendersyntax['none'] or rendersyntax['any'] -%} +{{ contents -}} +{% endif -%} +{% endmacro %} + +{# Wrapper for data rules. A data rule is rendered only when all of the data +fields it contains have data available. #} +{% macro datarule() -%} +{% do rendersyntax.update(none=false) -%} +{% do renderdata.update(all=true) -%} +{% set contents -%} +{{ caller() -}} +{% endset -%} +{% if renderdata['all'] -%} +{% do rendersyntax.update(any=true) -%} +{{ contents -}} +{% endif -%} +{% endmacro %} + +{# Wrapper for fields in data rules. If any value wrapped by this macro +produces an empty string, the entire data rule will be suppressed. #} +{% macro datafield(value) -%} +{% if value -%} +{{ value -}} +{% else -%} +{% do renderdata.update(all=false) -%} +{% endif -%} +{% endmacro %} diff --git a/install/share/csr/templates/openssl_base.tmpl b/install/share/csr/templates/openssl_base.tmpl new file mode 100644 index 0000000..597577b --- /dev/null +++ b/install/share/csr/templates/openssl_base.tmpl @@ -0,0 +1,35 @@ +{% raw -%} +{% import "openssl_macros.tmpl" as openssl -%} +{% import "ipa_macros.tmpl" as ipa -%} +{%- endraw %} +#!/bin/bash -e + +if [[ $# -ne 2 ]]; then +echo "Usage: $0 " +echo "Called as: $0 $@" +exit 1 +fi + +CONFIG="$(mktemp)" +CSR="$1" +shift + +echo \ +{% raw %}{% filter quote %}{% endraw -%} +[ req ] +prompt = no +encrypt_key = no + +{{ parameters|join('\n') }} +{% raw %}{% set rendered_extensions -%}{% endraw %} +{{ extensions|join('\n') }} +{% raw -%} +{%- endset -%} +{% if rendered_extensions -%} +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 $1 +rm "$CONFIG" diff --git a/install/share/csr/templates/openssl_macros.tmpl b/install/share/csr/templates/openssl_macros.tmpl new file mode 100644 index 0000000..d31b8fe --- /dev/null +++ b/install/share/csr/templates/openssl_macros.tmpl @@ -0,0 +1,29 @@ +{# List containing rendered sections to be included at end #} +{% set openssl_sections = [] %} + +{# +List containing one entry for each section name allocated. Because of +scoping rules, we need to use a list so that it can be a "per-render global" +that gets updated in place. Real globals are shared by all templates with the +same environment, and variables defined in the macro don't persist after the +macro invocation ends. +#} +{% set openssl_section_num = [] %} + +{% macro section() -%} +{% set name -%} +sec{{ openssl_section_num|length -}} +{% endset -%} +{% do openssl_section_num.append('') -%} +{% set contents %}{{ caller() }}{% endset -%} +{% if contents -%} +{% set sectiondata = formatsection(name, contents) -%} +{% do openssl_sections.append(sectiondata) -%} +{% endif -%} +{{ name -}} +{% endmacro %} + +{% macro formatsection(name, contents) -%} +[ {{ name }} ] +{{ contents -}} +{% endmacro %} diff --git a/ipaclient/plugins/certmapping.py b/ipaclient/plugins/certmapping.py new file mode 100644 index 0000000..d48dd00 --- /dev/null +++ b/ipaclient/plugins/certmapping.py @@ -0,0 +1,105 @@ +# +# Copyright (C) 2016 FreeIPA Contributors see COPYING for license +# + +from ipalib import api +from ipalib import errors +from ipalib import output +from ipalib import util +from ipalib.certmapping import CSRGenerator, FileRuleProvider +from ipalib.frontend import Command, Str +from ipalib.parameters import Principal +from ipalib.plugable import Registry +from ipalib.text import _ +from ipaserver.plugins.certprofile import validate_profile_id + +register = Registry() + +import six + +if six.PY3: + unicode = str + +__doc__ = _(""" +Commands to build certificate requests automatically +""") + +@register() +class cert_get_requestdata(Command): + __doc__ = _('Gather data for a certificate signing request.') + + takes_options = ( + Principal('principal', + label=_('Principal'), + doc=_('Principal for this certificate (e.g.' + ' HTTP/test.example.com)'), + ), + Str('profile_id', + validate_profile_id, + label=_('Profile ID'), + doc=_('Certificate 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'), + ), + Str('out?', + doc=_('Write CSR generation script to file'), + ), + ) + + has_output = ( + output.Output( + 'result', + type=dict, + doc=_('Dictionary mapping variable name to value'), + ), + ) + + has_output_params = ( + Str( + 'script', + label=_('Generation script'), + ) + ) + + + def forward(self, *args, **options): + if 'out' in options: + util.check_writable_file(options['out']) + + principal = options.get('principal') + profile_id = options.get('profile_id') + helper = options.get('helper') + + try: + if principal.is_host: + principal_obj = api.Command.host_show( + principal.hostname, all=True) + elif principal.is_service: + principal_obj = api.Command.service_show( + unicode(principal), all=True) + elif principal.is_user: + principal_obj = api.Command.user_show( + principal.username, all=True) + except errors.NotFound: + raise errors.NotFound( + reason=_("The principal for this request doesn't exist.")) + principal_obj = principal_obj['result'] + + generator = CSRGenerator(FileRuleProvider()) + + script = generator.csr_script( + principal_obj, profile_id, helper) + + result = {} + if 'out' in options: + with open(options['out'], 'wb') as f: + f.write(script) + else: + result = dict(script=script) + + return dict( + result=result + ) diff --git a/ipalib/certmapping.py b/ipalib/certmapping.py new file mode 100644 index 0000000..2259685 --- /dev/null +++ b/ipalib/certmapping.py @@ -0,0 +1,285 @@ +# +# Copyright (C) 2016 FreeIPA Contributors see COPYING for license +# + +import collections +import jinja2 +import jinja2.ext +import jinja2.sandbox +import json +import os.path +import traceback + +from ipalib import api +from ipalib import errors +from ipalib.text import _ +from ipapython.ipa_log_manager import root_logger +from ipapython.templating import IPAExtension + +import six + +if six.PY3: + unicode = str + +__doc__ = _(""" +Routines for constructing certificate signing requests using IPA data and +stored mapping rules. +""") + +CSR_DATA_DIR = '/usr/share/ipa/csr' + + +class IndexableUndefined(jinja2.Undefined): + def __getitem__(self, key): + return jinja2.Undefined( + hint=self._undefined_hint, obj=self._undefined_obj, + name=self._undefined_name, exc=self._undefined_exception) + + +class Formatter(object): + """ + Class for processing a set of mapping rules into a template. + + The template can be rendered with user and database data to produce a + script, which generates a CSR when run. + + Subclasses of Formatter should set the value of base_template_name to the + filename of a base template with spaces for the processed rules. + Additionally, they should override the _get_template_params method to + produce the correct output for the base template. + """ + base_template_name = None + + def __init__(self): + self.jinja2 = jinja2.sandbox.SandboxedEnvironment( + loader=jinja2.FileSystemLoader( + os.path.join(CSR_DATA_DIR, 'templates')), + extensions=[jinja2.ext.ExprStmtExtension, IPAExtension], + keep_trailing_newline=True, undefined=IndexableUndefined) + + self.passthrough_globals = {} + self._define_passthrough('ipa.syntaxrule') + self._define_passthrough('ipa.datarule') + + def _define_passthrough(self, call): + + def passthrough(caller): + return u'{%% call %s() %%}%s{%% endcall %%}' % (call, caller()) + + parts = call.split('.') + current_level = self.passthrough_globals + for part in parts[:-1]: + if part not in current_level: + current_level[part] = {} + current_level = current_level[part] + current_level[parts[-1]] = passthrough + + def build_template(self, rules): + """ + Construct a template that can produce CSR generator strings. + + :param rules: list of MappingRuleset to use to populate the template. + + :returns: jinja2.Template that can be rendered to produce the CSR data. + """ + syntax_rules = [] + for description, syntax_rule, data_rules in rules: + data_rules_prepared = [ + self._prepare_data_rule(rule) for rule in data_rules] + syntax_rules.append(self._prepare_syntax_rule( + syntax_rule, data_rules_prepared, description)) + + template_params = self._get_template_params(syntax_rules) + base_template = self.jinja2.get_template( + self.base_template_name, globals=self.passthrough_globals) + + try: + combined_template_source = base_template.render(**template_params) + except jinja2.UndefinedError: + root_logger.debug(traceback.format_exc()) + raise errors.CertificateMappingError(reason=_( + 'Template error when formatting certificate data')) + + root_logger.debug( + 'Formatting with template: %s' % combined_template_source) + combined_template = self.jinja2.from_string(combined_template_source) + + return combined_template + + def _wrap_rule(self, rule, rule_type): + template = '{%% call ipa.%srule() %%}%s{%% endcall %%}' % ( + rule_type, rule) + + return template + + def _wrap_required(self, rule, name): + template = '{%% filter required("%s") %%}%s{%% endfilter %%}' % ( + name, rule) + + return template + + def _prepare_data_rule(self, data_rule): + return self._wrap_rule(data_rule.template, 'data') + + def _prepare_syntax_rule(self, syntax_rule, data_rules, name): + root_logger.debug('Syntax rule template: %s' % syntax_rule.template) + template = self.jinja2.from_string( + syntax_rule.template, globals=self.passthrough_globals) + is_required = syntax_rule.options.get('required', False) + try: + rendered = template.render(datarules=data_rules) + except jinja2.UndefinedError: + root_logger.debug(traceback.format_exc()) + raise errors.CertificateMappingError(reason=_( + 'Template error when formatting certificate data')) + + prepared_template = self._wrap_rule(rendered, 'syntax') + if is_required: + prepared_template = self._wrap_required(prepared_template, name) + + return prepared_template + + def _get_template_params(self, syntax_rules): + """ + Package the syntax rules into fields expected by the base template. + + :param syntax_rules: list of prepared syntax rules to be included in + the template. + + :returns: dict of values needed to render the base template. + """ + raise NotImplementedError('Formatter class must be subclassed') + + +class OpenSSLFormatter(Formatter): + """Formatter class supporting the openssl command-line tool.""" + + base_template_name = 'openssl_base.tmpl' + + # Syntax rules are wrapped in this data structure, to keep track of whether + # each goes in the extension or the root section + SyntaxRule = collections.namedtuple( + 'SyntaxRule', ['template', 'is_extension']) + + def __init__(self): + super(OpenSSLFormatter, self).__init__() + self._define_passthrough('openssl.section') + + def _get_template_params(self, syntax_rules): + parameters = [rule.template for rule in syntax_rules + if not rule.is_extension] + extensions = [rule.template for rule in syntax_rules + if rule.is_extension] + + return {'parameters': parameters, 'extensions': extensions} + + def _prepare_syntax_rule(self, syntax_rule, data_rules, name): + """Overrides method to pull out whether rule is an extension or not.""" + prepared_template = super(OpenSSLFormatter, self)._prepare_syntax_rule( + syntax_rule, data_rules, name) + is_extension = syntax_rule.options.get('extension', False) + 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} + + +# FieldMapping - representation of the rules needed to construct a complete +# certificate field. +# - description: str, a name or description of this field, to be used in +# messages +# - syntax_rule: Rule, the rule defining the syntax of this field +# - data_rules: list of Rule, the rules that produce data to be stored in this +# field +FieldMapping = collections.namedtuple( + 'FieldMapping', ['description', 'syntax_rule', 'data_rules']) +Rule = collections.namedtuple( + 'Rule', ['name', 'template', 'options']) + + +class RuleProvider(object): + def rules_for_profile(self, profile_id, helper): + """ + Return the rules needed to build a CSR for the given certificate + profile. + + :param profile_id: str, name of the certificate 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 + """ + raise NotImplementedError('RuleProvider class must be subclassed') + + +class FileRuleProvider(RuleProvider): + def __init__(self): + self.rules = {} + + def _rule(self, rule_name, helper): + if (rule_name, helper) not in self.rules: + rule_path = os.path.join(CSR_DATA_DIR, 'rules', + '%s.json' % rule_name) + with open(rule_path) as rule_file: + ruleset = json.load(rule_file) + try: + rule = [r for r in ruleset['rules'] + if r['helper'] == helper][0] + except IndexError: + raise errors.NotFound( + reason=_('No transformation in "%(ruleset)s" rule supports' + ' helper "%(helper)s"') % + {'ruleset': rule_name, 'helper': helper}) + + options = {} + if 'options' in ruleset: + options.update(ruleset['options']) + if 'options' in rule: + options.update(rule['options']) + self.rules[(rule_name, helper)] = Rule(rule_name, rule['template'], options) + return self.rules[(rule_name, helper)] + + def rules_for_profile(self, profile_id, helper): + profile_path = os.path.join(CSR_DATA_DIR, 'profiles', + '%s.json' % profile_id) + with open(profile_path) as profile_file: + profile = json.load(profile_file) + + field_mappings = [] + for field in profile: + syntax_rule = self._rule(field['syntax'], helper) + data_rules = [self._rule(name, helper) 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): + self.rule_provider = rule_provider + + def csr_script(self, principal, profile_id, helper): + config = api.Command.config_show()['result'] + 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) + + try: + script = template.render(render_data) + except jinja2.UndefinedError: + root_logger.debug(traceback.format_exc()) + raise errors.CertificateMappingError(reason=_( + 'Template error when formatting certificate data')) + + return script diff --git a/ipalib/errors.py b/ipalib/errors.py index 4cc4455..b1fb503 100644 --- a/ipalib/errors.py +++ b/ipalib/errors.py @@ -1697,6 +1697,15 @@ class CertificateFormatError(CertificateError): format = _('Certificate format error: %(error)s') +class CertificateMappingError(CertificateError): + """ + **4303** Raised when a valid cert request config can not be generated + """ + + errno = 4303 + format = _('%(reason)s') + + class MutuallyExclusiveError(ExecutionError): """ **4303** Raised when an operation would result in setting two attributes which are mutually exlusive. diff --git a/ipapython/templating.py b/ipapython/templating.py new file mode 100644 index 0000000..1302e0a --- /dev/null +++ b/ipapython/templating.py @@ -0,0 +1,31 @@ +# +# Copyright (C) 2016 FreeIPA Contributors see COPYING for license +# + +import pipes + +from jinja2.ext import Extension + +from ipalib import errors +from ipalib.text import _ + +class IPAExtension(Extension): + """Jinja2 extension providing useful features for cert mapping rules.""" + + def __init__(self, environment): + super(IPAExtension, self).__init__(environment) + + environment.filters.update( + quote=self.quote, + required=self.required, + ) + + def quote(self, data): + return pipes.quote(data) + + def required(self, data, name): + if not data: + raise errors.CertificateMappingError( + reason=_('Required mapping rule %(name)s is missing data') % + {'name': name}) + return data From e5f4cfe8e47195ba71031e97390301e4169ba3c5 Mon Sep 17 00:00:00 2001 From: Ben Lipton Date: Mon, 22 Aug 2016 10:43:49 -0400 Subject: [PATCH 2/4] Add certificate mappings for caIPAserviceCert https://fedorahosted.org/freeipa/ticket/4899 --- install/share/csr/Makefile.am | 5 +++++ install/share/csr/profiles/caIPAserviceCert.json | 14 ++++++++++++++ install/share/csr/rules/dataDNS.json | 12 ++++++++++++ install/share/csr/rules/dataHostCN.json | 12 ++++++++++++ install/share/csr/rules/syntaxSAN.json | 15 +++++++++++++++ install/share/csr/rules/syntaxSubject.json | 15 +++++++++++++++ 6 files changed, 73 insertions(+) create mode 100644 install/share/csr/profiles/caIPAserviceCert.json create mode 100644 install/share/csr/rules/dataDNS.json create mode 100644 install/share/csr/rules/dataHostCN.json create mode 100644 install/share/csr/rules/syntaxSAN.json create mode 100644 install/share/csr/rules/syntaxSubject.json diff --git a/install/share/csr/Makefile.am b/install/share/csr/Makefile.am index 5a8ef5c..2ae99ed 100644 --- a/install/share/csr/Makefile.am +++ b/install/share/csr/Makefile.am @@ -2,10 +2,15 @@ NULL = profiledir = $(IPA_DATA_DIR)/csr/profiles profile_DATA = \ + profiles/caIPAserviceCert.json \ $(NULL) ruledir = $(IPA_DATA_DIR)/csr/rules rule_DATA = \ + rules/dataDNS.json \ + rules/dataHostCN.json \ + rules/syntaxSAN.json \ + rules/syntaxSubject.json \ $(NULL) templatedir = $(IPA_DATA_DIR)/csr/templates diff --git a/install/share/csr/profiles/caIPAserviceCert.json b/install/share/csr/profiles/caIPAserviceCert.json new file mode 100644 index 0000000..0d1be5e --- /dev/null +++ b/install/share/csr/profiles/caIPAserviceCert.json @@ -0,0 +1,14 @@ +[ + { + "syntax": "syntaxSubject", + "data": [ + "dataHostCN" + ] + }, + { + "syntax": "syntaxSAN", + "data": [ + "dataDNS" + ] + } +] diff --git a/install/share/csr/rules/dataDNS.json b/install/share/csr/rules/dataDNS.json new file mode 100644 index 0000000..f0aadca --- /dev/null +++ b/install/share/csr/rules/dataDNS.json @@ -0,0 +1,12 @@ +{ + "rules": [ + { + "helper": "openssl", + "template": "DNS = {{ipa.datafield(subject.krbprincipalname.0.partition('/')[2].partition('@')[0])}}" + }, + { + "helper": "certutil", + "template": "dns:{{ipa.datafield(subject.krbprincipalname.0.partition('/')[2].partition('@')[0])|quote}}" + } + ] +} diff --git a/install/share/csr/rules/dataHostCN.json b/install/share/csr/rules/dataHostCN.json new file mode 100644 index 0000000..172c7ec --- /dev/null +++ b/install/share/csr/rules/dataHostCN.json @@ -0,0 +1,12 @@ +{ + "rules": [ + { + "helper": "openssl", + "template": "{{ipa.datafield(config.ipacertificatesubjectbase.0)}}\nCN={{ipa.datafield(subject.krbprincipalname.0.partition('/')[2].partition('@')[0])}}" + }, + { + "helper": "certutil", + "template": "CN={{ipa.datafield(subject.krbprincipalname.0.partition('/')[2].partition('@')[0])|quote}},{{ipa.datafield(config.ipacertificatesubjectbase.0)|quote}}" + } + ] +} diff --git a/install/share/csr/rules/syntaxSAN.json b/install/share/csr/rules/syntaxSAN.json new file mode 100644 index 0000000..122eb12 --- /dev/null +++ b/install/share/csr/rules/syntaxSAN.json @@ -0,0 +1,15 @@ +{ + "rules": [ + { + "helper": "openssl", + "template": "subjectAltName = @{% call openssl.section() %}{{ datarules|join('\n') }}{% endcall %}", + "options": { + "extension": true + } + }, + { + "helper": "certutil", + "template": "--extSAN {{ datarules|join(',') }}" + } + ] +} diff --git a/install/share/csr/rules/syntaxSubject.json b/install/share/csr/rules/syntaxSubject.json new file mode 100644 index 0000000..7dfa932 --- /dev/null +++ b/install/share/csr/rules/syntaxSubject.json @@ -0,0 +1,15 @@ +{ + "rules": [ + { + "helper": "openssl", + "template": "distinguished_name = {% call openssl.section() %}{{ datarules|first }}{% endcall %}" + }, + { + "helper": "certutil", + "template": "-s {{ datarules|first }}" + } + ], + "options": { + "required": true + } +} From 661de2b27ab6131538b1f34a70cdcb7da288e3a0 Mon Sep 17 00:00:00 2001 From: Ben Lipton Date: Mon, 22 Aug 2016 10:45:04 -0400 Subject: [PATCH 3/4] Add a new cert profile for users https://fedorahosted.org/freeipa/ticket/4899 --- install/share/csr/Makefile.am | 3 + install/share/csr/profiles/userCert.json | 14 +++++ install/share/csr/rules/dataEmail.json | 12 ++++ install/share/csr/rules/dataUsernameCN.json | 12 ++++ install/share/profiles/Makefile.am | 1 + install/share/profiles/userCert.cfg | 97 +++++++++++++++++++++++++++++ ipapython/dogtag.py | 1 + 7 files changed, 140 insertions(+) create mode 100644 install/share/csr/profiles/userCert.json create mode 100644 install/share/csr/rules/dataEmail.json create mode 100644 install/share/csr/rules/dataUsernameCN.json create mode 100644 install/share/profiles/userCert.cfg diff --git a/install/share/csr/Makefile.am b/install/share/csr/Makefile.am index 2ae99ed..76e684c 100644 --- a/install/share/csr/Makefile.am +++ b/install/share/csr/Makefile.am @@ -3,12 +3,15 @@ NULL = profiledir = $(IPA_DATA_DIR)/csr/profiles profile_DATA = \ profiles/caIPAserviceCert.json \ + profiles/userCert.json \ $(NULL) ruledir = $(IPA_DATA_DIR)/csr/rules rule_DATA = \ rules/dataDNS.json \ + rules/dataEmail.json \ rules/dataHostCN.json \ + rules/dataUsernameCN.json \ rules/syntaxSAN.json \ rules/syntaxSubject.json \ $(NULL) diff --git a/install/share/csr/profiles/userCert.json b/install/share/csr/profiles/userCert.json new file mode 100644 index 0000000..d5f822e --- /dev/null +++ b/install/share/csr/profiles/userCert.json @@ -0,0 +1,14 @@ +[ + { + "syntax": "syntaxSubject", + "data": [ + "dataUsernameCN" + ] + }, + { + "syntax": "syntaxSAN", + "data": [ + "dataEmail" + ] + } +] diff --git a/install/share/csr/rules/dataEmail.json b/install/share/csr/rules/dataEmail.json new file mode 100644 index 0000000..cfc1f60 --- /dev/null +++ b/install/share/csr/rules/dataEmail.json @@ -0,0 +1,12 @@ +{ + "rules": [ + { + "helper": "openssl", + "template": "email = {{ipa.datafield(subject.mail.0)}}" + }, + { + "helper": "certutil", + "template": "email:{{ipa.datafield(subject.mail.0)|quote}}" + } + ] +} diff --git a/install/share/csr/rules/dataUsernameCN.json b/install/share/csr/rules/dataUsernameCN.json new file mode 100644 index 0000000..c3e2409 --- /dev/null +++ b/install/share/csr/rules/dataUsernameCN.json @@ -0,0 +1,12 @@ +{ + "rules": [ + { + "helper": "openssl", + "template": "{{ipa.datafield(config.ipacertificatesubjectbase.0)}}\nCN={{ipa.datafield(subject.uid.0)}}" + }, + { + "helper": "certutil", + "template": "CN={{ipa.datafield(subject.uid.0)|quote}},{{ipa.datafield(config.ipacertificatesubjectbase.0)|quote}}" + } + ] +} diff --git a/install/share/profiles/Makefile.am b/install/share/profiles/Makefile.am index b5ccb6e..2e0166b 100644 --- a/install/share/profiles/Makefile.am +++ b/install/share/profiles/Makefile.am @@ -4,6 +4,7 @@ appdir = $(IPA_DATA_DIR)/profiles app_DATA = \ caIPAserviceCert.cfg \ IECUserRoles.cfg \ + userCert.cfg \ $(NULL) EXTRA_DIST = \ diff --git a/install/share/profiles/userCert.cfg b/install/share/profiles/userCert.cfg new file mode 100644 index 0000000..3c2eeb5 --- /dev/null +++ b/install/share/profiles/userCert.cfg @@ -0,0 +1,97 @@ +profileId=userCert +classId=caEnrollImpl +desc=This certificate profile is for enrolling user certificates with S/MIME key usage permitted +visible=false +enable=true +enableBy=admin +auth.instance_id=raCertAuth +name=Manual User Dual-Use S/MIME capabilities Certificate Enrollment +input.list=i1,i2 +input.i1.class_id=certReqInputImpl +input.i2.class_id=submitterInfoInputImpl +output.list=o1 +output.o1.class_id=certOutputImpl +policyset.list=userCertSet +policyset.userCertSet.list=1,2,3,4,5,6,7,8,9,10 +policyset.userCertSet.1.constraint.class_id=subjectNameConstraintImpl +policyset.userCertSet.1.constraint.name=Subject Name Constraint +policyset.userCertSet.1.constraint.params.pattern=CN=[^,]+,.+ +policyset.userCertSet.1.constraint.params.accept=true +policyset.userCertSet.1.default.class_id=subjectNameDefaultImpl +policyset.userCertSet.1.default.name=Subject Name Default +policyset.userCertSet.1.default.params.name=CN=$$request.req_subject_name.cn$$, $SUBJECT_DN_O +policyset.userCertSet.2.constraint.class_id=validityConstraintImpl +policyset.userCertSet.2.constraint.name=Validity Constraint +policyset.userCertSet.2.constraint.params.range=365 +policyset.userCertSet.2.constraint.params.notBeforeCheck=false +policyset.userCertSet.2.constraint.params.notAfterCheck=false +policyset.userCertSet.2.default.class_id=validityDefaultImpl +policyset.userCertSet.2.default.name=Validity Default +policyset.userCertSet.2.default.params.range=180 +policyset.userCertSet.2.default.params.startTime=0 +policyset.userCertSet.3.constraint.class_id=keyConstraintImpl +policyset.userCertSet.3.constraint.name=Key Constraint +policyset.userCertSet.3.constraint.params.keyType=RSA +policyset.userCertSet.3.constraint.params.keyParameters=1024,2048,3072,4096 +policyset.userCertSet.3.default.class_id=userKeyDefaultImpl +policyset.userCertSet.3.default.name=Key Default +policyset.userCertSet.4.constraint.class_id=noConstraintImpl +policyset.userCertSet.4.constraint.name=No Constraint +policyset.userCertSet.4.default.class_id=authorityKeyIdentifierExtDefaultImpl +policyset.userCertSet.4.default.name=Authority Key Identifier Default +policyset.userCertSet.5.constraint.class_id=noConstraintImpl +policyset.userCertSet.5.constraint.name=No Constraint +policyset.userCertSet.5.default.class_id=authInfoAccessExtDefaultImpl +policyset.userCertSet.5.default.name=AIA Extension Default +policyset.userCertSet.5.default.params.authInfoAccessADEnable_0=true +policyset.userCertSet.5.default.params.authInfoAccessADLocationType_0=URIName +policyset.userCertSet.5.default.params.authInfoAccessADLocation_0=http://$IPA_CA_RECORD.$DOMAIN/ca/ocsp +policyset.userCertSet.5.default.params.authInfoAccessADMethod_0=1.3.6.1.5.5.7.48.1 +policyset.userCertSet.5.default.params.authInfoAccessCritical=false +policyset.userCertSet.5.default.params.authInfoAccessNumADs=1 +policyset.userCertSet.6.constraint.class_id=keyUsageExtConstraintImpl +policyset.userCertSet.6.constraint.name=Key Usage Extension Constraint +policyset.userCertSet.6.constraint.params.keyUsageCritical=true +policyset.userCertSet.6.constraint.params.keyUsageDigitalSignature=true +policyset.userCertSet.6.constraint.params.keyUsageNonRepudiation=true +policyset.userCertSet.6.constraint.params.keyUsageDataEncipherment=false +policyset.userCertSet.6.constraint.params.keyUsageKeyEncipherment=true +policyset.userCertSet.6.constraint.params.keyUsageKeyAgreement=false +policyset.userCertSet.6.constraint.params.keyUsageKeyCertSign=false +policyset.userCertSet.6.constraint.params.keyUsageCrlSign=false +policyset.userCertSet.6.constraint.params.keyUsageEncipherOnly=false +policyset.userCertSet.6.constraint.params.keyUsageDecipherOnly=false +policyset.userCertSet.6.default.class_id=keyUsageExtDefaultImpl +policyset.userCertSet.6.default.name=Key Usage Default +policyset.userCertSet.6.default.params.keyUsageCritical=true +policyset.userCertSet.6.default.params.keyUsageDigitalSignature=true +policyset.userCertSet.6.default.params.keyUsageNonRepudiation=true +policyset.userCertSet.6.default.params.keyUsageDataEncipherment=false +policyset.userCertSet.6.default.params.keyUsageKeyEncipherment=true +policyset.userCertSet.6.default.params.keyUsageKeyAgreement=false +policyset.userCertSet.6.default.params.keyUsageKeyCertSign=false +policyset.userCertSet.6.default.params.keyUsageCrlSign=false +policyset.userCertSet.6.default.params.keyUsageEncipherOnly=false +policyset.userCertSet.6.default.params.keyUsageDecipherOnly=false +policyset.userCertSet.7.constraint.class_id=noConstraintImpl +policyset.userCertSet.7.constraint.name=No Constraint +policyset.userCertSet.7.default.class_id=extendedKeyUsageExtDefaultImpl +policyset.userCertSet.7.default.name=Extended Key Usage Extension Default +policyset.userCertSet.7.default.params.exKeyUsageCritical=false +policyset.userCertSet.7.default.params.exKeyUsageOIDs=1.3.6.1.5.5.7.3.2,1.3.6.1.5.5.7.3.4 +policyset.userCertSet.8.constraint.class_id=signingAlgConstraintImpl +policyset.userCertSet.8.constraint.name=No Constraint +policyset.userCertSet.8.constraint.params.signingAlgsAllowed=SHA1withRSA,SHA256withRSA,SHA512withRSA,MD5withRSA,MD2withRSA,SHA1withDSA,SHA1withEC,SHA256withEC,SHA384withEC,SHA512withEC +policyset.userCertSet.8.default.class_id=signingAlgDefaultImpl +policyset.userCertSet.8.default.name=Signing Alg +policyset.userCertSet.8.default.params.signingAlg=- +policyset.userCertSet.9.constraint.class_id=noConstraintImpl +policyset.userCertSet.9.constraint.name=No Constraint +policyset.userCertSet.9.default.class_id=subjectKeyIdentifierExtDefaultImpl +policyset.userCertSet.9.default.name=Subject Key Identifier Extension Default +policyset.userCertSet.9.default.params.critical=false +policyset.userCertSet.10.constraint.class_id=noConstraintImpl +policyset.userCertSet.10.constraint.name=No Constraint +policyset.userCertSet.10.default.class_id=userExtensionDefaultImpl +policyset.userCertSet.10.default.name=User Supplied Extension Default +policyset.userCertSet.10.default.params.userExtOID=2.5.29.17 diff --git a/ipapython/dogtag.py b/ipapython/dogtag.py index 6f13880..3b4e205 100644 --- a/ipapython/dogtag.py +++ b/ipapython/dogtag.py @@ -44,6 +44,7 @@ INCLUDED_PROFILES = { Profile(u'caIPAserviceCert', u'Standard profile for network services', True), + Profile(u'userCert', u'Standard profile for users', True), Profile(u'IECUserRoles', u'User profile that includes IECUserRoles extension from request', True), } From fa15aeb8dbbe7482d0f6ebc22930f08743efa2bc Mon Sep 17 00:00:00 2001 From: Ben Lipton Date: Fri, 26 Aug 2016 18:04:23 -0400 Subject: [PATCH 4/4] fixup! Add code to generate scripts that generate CSRs --- freeipa.spec.in | 1 + 1 file changed, 1 insertion(+) diff --git a/freeipa.spec.in b/freeipa.spec.in index beec87a..2d7c7ea 100644 --- a/freeipa.spec.in +++ b/freeipa.spec.in @@ -105,6 +105,7 @@ BuildRequires: custodia BuildRequires: libini_config-devel >= 1.2.0 BuildRequires: dbus-python BuildRequires: python-netifaces >= 0.10.4 +BuildRequires: python-jinja2 # Build dependencies for unit tests BuildRequires: libcmocka-devel