diff options
author | Fraser Tweedale <ftweedal@redhat.com> | 2015-05-14 01:46:06 -0400 |
---|---|---|
committer | Jan Cholasta <jcholast@redhat.com> | 2016-06-15 07:13:38 +0200 |
commit | 3d4db834caa0688bcefc0092b7978402b783eaf3 (patch) | |
tree | e9fddc70bdff260ce738dc9458938851453d86c0 /ipaserver | |
parent | 8135651abb857fbe489a1de8aacad3747d7d5cc9 (diff) | |
download | freeipa-3d4db834caa0688bcefc0092b7978402b783eaf3.tar.gz freeipa-3d4db834caa0688bcefc0092b7978402b783eaf3.tar.xz freeipa-3d4db834caa0688bcefc0092b7978402b783eaf3.zip |
Add 'ca' plugin
This commit adds the 'ca' plugin for creating and managing
lightweight CAs. The initial implementation supports a single level
of sub-CAs underneath the IPA CA.
This commit also:
- adds the container for FreeIPA CA objects
- adds schema for the FreeIPA CA objects
- updates ipa-pki-proxy.conf to allow access to the Dogtag
lightweight CAs REST API.
Part of: https://fedorahosted.org/freeipa/ticket/4559
Reviewed-By: Jan Cholasta <jcholast@redhat.com>
Reviewed-By: Martin Babinsky <mbabinsk@redhat.com>
Diffstat (limited to 'ipaserver')
-rw-r--r-- | ipaserver/install/cainstance.py | 7 | ||||
-rw-r--r-- | ipaserver/install/server/upgrade.py | 16 | ||||
-rw-r--r-- | ipaserver/plugins/ca.py | 217 | ||||
-rw-r--r-- | ipaserver/plugins/dogtag.py | 54 |
4 files changed, 292 insertions, 2 deletions
diff --git a/ipaserver/install/cainstance.py b/ipaserver/install/cainstance.py index becb0b172..3e2576d05 100644 --- a/ipaserver/install/cainstance.py +++ b/ipaserver/install/cainstance.py @@ -1629,6 +1629,13 @@ def ensure_ldap_profiles_container(): ou=['certificateProfiles'], ) +def ensure_lightweight_cas_container(): + ensure_entry( + DN(('ou', 'authorities'), ('ou', 'ca'), ('o', 'ipaca')), + objectclass=['top', 'organizationalUnit'], + ou=['authorities'], + ) + def ensure_entry(dn, **attrs): server_id = installutils.realm_to_serverid(api.env.realm) diff --git a/ipaserver/install/server/upgrade.py b/ipaserver/install/server/upgrade.py index cd2ad2e11..81a49e8af 100644 --- a/ipaserver/install/server/upgrade.py +++ b/ipaserver/install/server/upgrade.py @@ -345,6 +345,16 @@ def ca_import_included_profiles(ca): return cainstance.import_included_profiles() +def ca_ensure_lightweight_cas_container(ca): + root_logger.info('[Ensuring Lightweight CAs container exists in Dogtag database]') + + if not ca.is_configured(): + root_logger.info('CA is not configured') + return False + + return cainstance.ensure_lightweight_cas_container() + + def upgrade_ca_audit_cert_validity(ca): """ Update the Dogtag audit signing certificate. @@ -1438,7 +1448,10 @@ def ca_upgrade_schema(ca): root_logger.info('CA is not configured') return False - schema_files=['/usr/share/pki/server/conf/schema-certProfile.ldif'] + schema_files=[ + '/usr/share/pki/server/conf/schema-certProfile.ldif', + '/usr/share/pki/server/conf/schema-authority.ldif', + ] try: modified = schemaupdate.update_schema(schema_files, ldapi=True) except Exception as e: @@ -1698,6 +1711,7 @@ def upgrade_configuration(): except ipautil.CalledProcessError as e: root_logger.error("Failed to restart %s: %s", ca.service_name, e) + ca_ensure_lightweight_cas_container(ca) ca_enable_ldap_profile_subsystem(ca) # This step MUST be done after ca_enable_ldap_profile_subsystem and diff --git a/ipaserver/plugins/ca.py b/ipaserver/plugins/ca.py new file mode 100644 index 000000000..ee98f0a2a --- /dev/null +++ b/ipaserver/plugins/ca.py @@ -0,0 +1,217 @@ +# +# Copyright (C) 2016 FreeIPA Contributors see COPYING for license +# + +from ipalib import api, errors, DNParam, Str +from ipalib.constants import IPA_CA_CN +from ipalib.plugable import Registry +from ipaserver.plugins.baseldap import ( + LDAPObject, LDAPSearch, LDAPCreate, LDAPDelete, + LDAPUpdate, LDAPRetrieve) +from ipaserver.plugins.cert import ca_enabled_check +from ipalib import _, ngettext + + +__doc__ = _(""" +Manage Certificate Authorities + +Subordinate Certificate Authorities (Sub-CAs) can be added for scoped issuance +of X.509 certificates. + +EXAMPLES: + + Create new CA, subordinate to the IPA CA. + + ipa ca-add puppet --desc "Puppet" \\ + --subject "CN=Puppet CA,O=EXAMPLE.COM" + +""") + + +register = Registry() + + +@register() +class ca(LDAPObject): + """ + Lightweight CA Object + """ + container_dn = api.env.container_ca + object_name = _('Certificate Authority') + object_name_plural = _('Certificate Authorities') + object_class = ['ipaca'] + permission_filter_objectclasses = ['ipaca'] + default_attributes = [ + 'cn', 'description', 'ipacaid', 'ipacaissuerdn', 'ipacasubjectdn', + ] + rdn_attribute = 'cn' + rdn_is_primary_key = True + label = _('Certificate Authorities') + label_singular = _('Certificate Authority') + + takes_params = ( + Str('cn', + primary_key=True, + cli_name='name', + label=_('Name'), + doc=_('Name for referencing the CA'), + ), + Str('description?', + cli_name='desc', + label=_('Description'), + doc=_('Description of the purpose of the CA'), + ), + Str('ipacaid', + cli_name='id', + label=_('Authority ID'), + doc=_('Dogtag Authority ID'), + flags=['no_create', 'no_update'], + ), + DNParam('ipacasubjectdn', + cli_name='subject', + label=_('Subject DN'), + doc=_('Subject Distinguished Name'), + flags=['no_update'], + ), + DNParam('ipacaissuerdn', + cli_name='issuer', + label=_('Issuer DN'), + doc=_('Issuer Distinguished Name'), + flags=['no_create', 'no_update'], + ), + ) + + permission_filter_objectclasses = ['ipaca'] + managed_permissions = { + 'System: Read CAs': { + 'replaces_global_anonymous_aci': True, + 'ipapermbindruletype': 'all', + 'ipapermright': {'read', 'search', 'compare'}, + 'ipapermdefaultattr': { + 'cn', + 'description', + 'ipacaid', + 'ipacaissuerdn', + 'ipacasubjectdn', + 'objectclass', + }, + }, + 'System: Add CA': { + 'ipapermright': {'add'}, + 'replaces': [ + '(target = "ldap:///cn=*,cn=cas,cn=ca,$SUFFIX")(version 3.0;acl "permission:Add CA";allow (add) groupdn = "ldap:///cn=Add CA,cn=permissions,cn=pbac,$SUFFIX";)', + ], + 'default_privileges': {'CA Administrator'}, + }, + 'System: Delete CA': { + 'ipapermright': {'delete'}, + 'replaces': [ + '(target = "ldap:///cn=*,cn=cas,cn=ca,$SUFFIX")(version 3.0;acl "permission:Delete CA";allow (delete) groupdn = "ldap:///cn=Delete CA,cn=permissions,cn=pbac,$SUFFIX";)', + ], + 'default_privileges': {'CA Administrator'}, + }, + 'System: Modify CA': { + 'ipapermright': {'write'}, + 'ipapermdefaultattr': { + 'cn', + 'description', + }, + 'replaces': [ + '(targetattr = "cn || description")(target = "ldap:///cn=*,cn=cas,cn=ca,$SUFFIX")(version 3.0;acl "permission:Modify CA";allow (write) groupdn = "ldap:///cn=Modify CA,cn=permissions,cn=pbac,$SUFFIX";)', + ], + 'default_privileges': {'CA Administrator'}, + }, + } + + +@register() +class ca_find(LDAPSearch): + __doc__ = _("Search for CAs.") + msg_summary = ngettext( + '%(count)d CA matched', '%(count)d CAs matched', 0 + ) + + def execute(self, *keys, **options): + ca_enabled_check() + return super(ca_find, self).execute(*keys, **options) + + +@register() +class ca_show(LDAPRetrieve): + __doc__ = _("Display the properties of a CA.") + + def execute(self, *args, **kwargs): + ca_enabled_check() + return super(ca_show, self).execute(*args, **kwargs) + + +@register() +class ca_add(LDAPCreate): + __doc__ = _("Create a CA.") + msg_summary = _('Created CA "%(value)s"') + + def pre_callback(self, ldap, dn, entry, entry_attrs, *keys, **options): + ca_enabled_check() + if not ldap.can_add(dn[1:]): + raise errors.ACIError( + info=_("Insufficient 'add' privilege for entry '%s'.") % dn) + + # check for name collision before creating CA in Dogtag + try: + api.Object.ca.get_dn_if_exists(keys[-1]) + self.obj.handle_duplicate_entry(*keys) + except errors.NotFound: + pass + + # Create the CA in Dogtag. + with self.api.Backend.ra_lightweight_ca as ca_api: + resp = ca_api.create_ca(options['ipacasubjectdn']) + entry['ipacaid'] = [resp['id']] + entry['ipacaissuerdn'] = [resp['issuerDN']] + + # In the event that the issued certificate's subject DN + # differs from what was requested, record the actual DN. + # + entry['ipacasubjectdn'] = [resp['dn']] + return dn + + +@register() +class ca_del(LDAPDelete): + __doc__ = _('Delete a CA.') + + msg_summary = _('Deleted CA "%(value)s"') + + def pre_callback(self, ldap, dn, *keys, **options): + ca_enabled_check() + + if keys[0] == IPA_CA_CN: + raise errors.ProtectedEntryError( + label=_("CA"), + key=keys[0], + reason=_("IPA CA cannot be deleted")) + + ca_id = self.api.Command.ca_show(keys[0])['result']['ipacaid'][0] + with self.api.Backend.ra_lightweight_ca as ca_api: + ca_api.disable_ca(ca_id) + ca_api.delete_ca(ca_id) + + return dn + + +@register() +class ca_mod(LDAPUpdate): + __doc__ = _("Modify CA configuration.") + msg_summary = _('Modified CA "%(value)s"') + + def pre_callback(self, ldap, dn, entry_attrs, attrs_list, *keys, **options): + ca_enabled_check() + + if 'rename' in options or 'cn' in entry_attrs: + if keys[0] == IPA_CA_CN: + raise errors.ProtectedEntryError( + label=_("CA"), + key=keys[0], + reason=u'IPA CA cannot be renamed') + + return dn diff --git a/ipaserver/plugins/dogtag.py b/ipaserver/plugins/dogtag.py index 197814c4d..20349b05f 100644 --- a/ipaserver/plugins/dogtag.py +++ b/ipaserver/plugins/dogtag.py @@ -2073,7 +2073,10 @@ class RestClient(Backend): headers = headers or {} headers['Cookie'] = self.cookie - resource = os.path.join('/ca/rest', self.path, path) + if path is not None: + resource = os.path.join('/ca/rest', self.path, path) + else: + resource = os.path.join('/ca/rest', self.path) # perform main request status, resp_headers, resp_body = dogtag.https_request( @@ -2147,3 +2150,52 @@ class ra_certprofile(RestClient): Delete the profile from Dogtag """ self._ssldo('DELETE', profile_id, headers={'Accept': 'application/json'}) + + +@register() +class ra_lightweight_ca(RestClient): + """ + Lightweight CA management backend plugin. + """ + path = 'authorities' + + def create_ca(self, dn): + """Create CA with the given DN. + + New CA is issued by IPA CA. Nested sub-CAs and unrelated + root CAs are not yet supported. + + Return the (parsed) JSON response from server. + + """ + + assert isinstance(dn, DN) + status, resp_headers, resp_body = self._ssldo( + 'POST', None, + headers={ + 'Content-type': 'application/json', + 'Accept': 'application/json', + }, + body=json.dumps({"parentID": "host-authority", "dn": unicode(dn)}), + ) + try: + return json.loads(resp_body) + except: + raise errors.RemoteRetrieveError(reason=_("Response from CA was not valid JSON")) + + def read_ca(self, ca_id): + status, resp_headers, resp_body = self._ssldo( + 'GET', ca_id, headers={'Accept': 'application/json'}) + try: + return json.loads(resp_body) + except: + raise errors.RemoteRetrieveError(reason=_("Response from CA was not valid JSON")) + + def disable_ca(self, ca_id): + self._ssldo( + 'POST', ca_id + '/disable', + headers={'Accept': 'application/json'}, + ) + + def delete_ca(self, ca_id): + self._ssldo('DELETE', ca_id) |