summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--API.txt6
-rw-r--r--VERSION.m44
-rw-r--r--ipaclient/plugins/ca.py53
-rw-r--r--ipaserver/plugins/ca.py65
-rw-r--r--ipaserver/plugins/dogtag.py12
-rw-r--r--ipatests/test_xmlrpc/tracker/ca_plugin.py31
-rw-r--r--ipatests/test_xmlrpc/xmlrpc_test.py17
7 files changed, 172 insertions, 16 deletions
diff --git a/API.txt b/API.txt
index bad3b9284..543cec530 100644
--- a/API.txt
+++ b/API.txt
@@ -445,10 +445,11 @@ option: Str('version?')
output: Output('count', type=[<type 'int'>])
output: Output('results', type=[<type 'list'>, <type 'tuple'>])
command: ca_add/1
-args: 1,7,3
+args: 1,8,3
arg: Str('cn', cli_name='name')
option: Str('addattr*', cli_name='addattr')
option: Flag('all', autofill=True, cli_name='all', default=False)
+option: Flag('chain', autofill=True, default=False)
option: Str('description?', cli_name='desc')
option: DNParam('ipacasubjectdn', cli_name='subject')
option: Flag('raw', autofill=True, cli_name='raw', default=False)
@@ -519,9 +520,10 @@ output: Entry('result')
output: Output('summary', type=[<type 'unicode'>, <type 'NoneType'>])
output: PrimaryKey('value')
command: ca_show/1
-args: 1,4,3
+args: 1,5,3
arg: Str('cn', cli_name='name')
option: Flag('all', autofill=True, cli_name='all', default=False)
+option: Flag('chain', autofill=True, default=False)
option: Flag('raw', autofill=True, cli_name='raw', default=False)
option: Flag('rights', autofill=True, default=False)
option: Str('version?')
diff --git a/VERSION.m4 b/VERSION.m4
index 7d9e1079c..36929eeca 100644
--- a/VERSION.m4
+++ b/VERSION.m4
@@ -73,8 +73,8 @@ define(IPA_DATA_VERSION, 20100614120000)
# #
########################################################
define(IPA_API_VERSION_MAJOR, 2)
-define(IPA_API_VERSION_MINOR, 216)
-# Last change: DNS: Support URI resource record type
+define(IPA_API_VERSION_MINOR, 217)
+# Last change: Add options to write lightweight CA cert or chain to file
########################################################
diff --git a/ipaclient/plugins/ca.py b/ipaclient/plugins/ca.py
new file mode 100644
index 000000000..fcdf48463
--- /dev/null
+++ b/ipaclient/plugins/ca.py
@@ -0,0 +1,53 @@
+#
+# Copyright (C) 2016 FreeIPA Contributors see COPYING for license
+#
+
+import base64
+from ipaclient.frontend import MethodOverride
+from ipalib import util, x509, Str
+from ipalib.plugable import Registry
+from ipalib.text import _
+
+register = Registry()
+
+
+class WithCertOutArgs(MethodOverride):
+
+ takes_options = (
+ Str(
+ 'certificate_out?',
+ doc=_('Write certificate (chain if --chain used) to file'),
+ include='cli',
+ cli_metavar='FILE',
+ ),
+ )
+
+ def forward(self, *keys, **options):
+ filename = None
+ if 'certificate_out' in options:
+ filename = options.pop('certificate_out')
+ util.check_writable_file(filename)
+
+ result = super(WithCertOutArgs, self).forward(*keys, **options)
+ if filename:
+ def to_pem(x):
+ return x509.make_pem(x)
+ if options.get('chain', False):
+ ders = result['result']['certificate_chain']
+ data = '\n'.join(to_pem(base64.b64encode(der)) for der in ders)
+ else:
+ data = to_pem(result['result']['certificate'])
+ with open(filename, 'wb') as f:
+ f.write(data)
+
+ return result
+
+
+@register(override=True, no_fail=True)
+class ca_add(WithCertOutArgs):
+ pass
+
+
+@register(override=True, no_fail=True)
+class ca_show(WithCertOutArgs):
+ pass
diff --git a/ipaserver/plugins/ca.py b/ipaserver/plugins/ca.py
index d9ae8c81f..ef1d68c3a 100644
--- a/ipaserver/plugins/ca.py
+++ b/ipaserver/plugins/ca.py
@@ -2,14 +2,18 @@
# Copyright (C) 2016 FreeIPA Contributors see COPYING for license
#
-from ipalib import api, errors, output, DNParam, Str
+import base64
+
+import six
+
+from ipalib import api, errors, output, Bytes, DNParam, Flag, Str
from ipalib.constants import IPA_CA_CN
from ipalib.plugable import Registry
from ipaserver.plugins.baseldap import (
LDAPObject, LDAPSearch, LDAPCreate, LDAPDelete,
LDAPUpdate, LDAPRetrieve, LDAPQuery, pkey_to_value)
from ipaserver.plugins.cert import ca_enabled_check
-from ipalib import _, ngettext
+from ipalib import _, ngettext, x509
__doc__ = _("""
@@ -100,6 +104,18 @@ class ca(LDAPObject):
doc=_('Issuer Distinguished Name'),
flags=['no_create', 'no_update'],
),
+ Bytes(
+ 'certificate',
+ label=_("Certificate"),
+ doc=_("Base-64 encoded certificate."),
+ flags={'no_create', 'no_update', 'no_search'},
+ ),
+ Bytes(
+ 'certificate_chain*',
+ label=_("Certificate chain"),
+ doc=_("X.509 certificate chain"),
+ flags={'no_create', 'no_update', 'no_search'},
+ ),
)
permission_filter_objectclasses = ['ipaca']
@@ -145,6 +161,21 @@ class ca(LDAPObject):
}
+def set_certificate_attrs(entry, options, always_include_cert=True):
+ ca_id = entry['ipacaid'][0]
+ full = options.get('all', False)
+ with api.Backend.ra_lightweight_ca as ca_api:
+ if always_include_cert or full:
+ der = ca_api.read_ca_cert(ca_id)
+ entry['certificate'] = six.text_type(base64.b64encode(der))
+
+ if options.get('chain', False) or full:
+ pkcs7_der = ca_api.read_ca_chain(ca_id)
+ pems = x509.pkcs7_to_pems(pkcs7_der, x509.DER)
+ ders = [x509.normalize_certificate(pem) for pem in pems]
+ entry['certificate_chain'] = ders
+
+
@register()
class ca_find(LDAPSearch):
__doc__ = _("Search for CAs.")
@@ -154,16 +185,32 @@ class ca_find(LDAPSearch):
def execute(self, *keys, **options):
ca_enabled_check()
- return super(ca_find, self).execute(*keys, **options)
+ result = super(ca_find, self).execute(*keys, **options)
+ for entry in result['result']:
+ set_certificate_attrs(entry, options, always_include_cert=False)
+ return result
+
+
+_chain_flag = Flag(
+ 'chain',
+ default=False,
+ doc=_('Include certificate chain in output'),
+)
@register()
class ca_show(LDAPRetrieve):
__doc__ = _("Display the properties of a CA.")
- def execute(self, *args, **kwargs):
+ takes_options = LDAPRetrieve.takes_options + (
+ _chain_flag,
+ )
+
+ def execute(self, *keys, **options):
ca_enabled_check()
- return super(ca_show, self).execute(*args, **kwargs)
+ result = super(ca_show, self).execute(*keys, **options)
+ set_certificate_attrs(result['result'], options)
+ return result
@register()
@@ -171,6 +218,10 @@ class ca_add(LDAPCreate):
__doc__ = _("Create a CA.")
msg_summary = _('Created CA "%(value)s"')
+ takes_options = LDAPCreate.takes_options + (
+ _chain_flag,
+ )
+
def pre_callback(self, ldap, dn, entry, entry_attrs, *keys, **options):
ca_enabled_check()
if not ldap.can_add(dn[1:]):
@@ -203,6 +254,10 @@ class ca_add(LDAPCreate):
entry['ipacasubjectdn'] = [resp['dn']]
return dn
+ def post_callback(self, ldap, dn, entry_attrs, *keys, **options):
+ set_certificate_attrs(entry_attrs, options)
+ return dn
+
@register()
class ca_del(LDAPDelete):
diff --git a/ipaserver/plugins/dogtag.py b/ipaserver/plugins/dogtag.py
index 0bdb4daf2..b77b21aa2 100644
--- a/ipaserver/plugins/dogtag.py
+++ b/ipaserver/plugins/dogtag.py
@@ -2125,6 +2125,18 @@ class ra_lightweight_ca(RestClient):
except:
raise errors.RemoteRetrieveError(reason=_("Response from CA was not valid JSON"))
+ def read_ca_cert(self, ca_id):
+ _status, _resp_headers, resp_body = self._ssldo(
+ 'GET', '{}/cert'.format(ca_id),
+ headers={'Accept': 'application/pkix-cert'})
+ return resp_body
+
+ def read_ca_chain(self, ca_id):
+ _status, _resp_headers, resp_body = self._ssldo(
+ 'GET', '{}/chain'.format(ca_id),
+ headers={'Accept': 'application/pkcs7-mime'})
+ return resp_body
+
def disable_ca(self, ca_id):
self._ssldo(
'POST', ca_id + '/disable',
diff --git a/ipatests/test_xmlrpc/tracker/ca_plugin.py b/ipatests/test_xmlrpc/tracker/ca_plugin.py
index ec58c28ab..e18b1c178 100644
--- a/ipatests/test_xmlrpc/tracker/ca_plugin.py
+++ b/ipatests/test_xmlrpc/tracker/ca_plugin.py
@@ -8,7 +8,13 @@ import six
from ipapython.dn import DN
from ipatests.test_xmlrpc.tracker.base import Tracker
from ipatests.util import assert_deepequal
-from ipatests.test_xmlrpc.xmlrpc_test import fuzzy_issuer, fuzzy_caid
+from ipatests.test_xmlrpc.xmlrpc_test import (
+ fuzzy_issuer,
+ fuzzy_caid,
+ fuzzy_base64,
+ fuzzy_sequence_of,
+ fuzzy_bytes,
+)
from ipatests.test_xmlrpc import objectclasses
@@ -19,12 +25,21 @@ if six.PY3:
class CATracker(Tracker):
"""Implementation of a Tracker class for CA plugin."""
- retrieve_keys = {
+ ldap_keys = {
'dn', 'cn', 'ipacaid', 'ipacasubjectdn', 'ipacaissuerdn', 'description'
}
- retrieve_all_keys = {'objectclass'} | retrieve_keys
- create_keys = retrieve_all_keys
- update_keys = retrieve_keys - {'dn'}
+ cert_keys = {
+ 'certificate',
+ }
+ cert_all_keys = {
+ 'certificate_chain',
+ }
+ find_keys = ldap_keys
+ find_all_keys = {'objectclass'} | ldap_keys
+ retrieve_keys = ldap_keys | cert_keys
+ retrieve_all_keys = {'objectclass'} | retrieve_keys | cert_all_keys
+ create_keys = {'objectclass'} | retrieve_keys
+ update_keys = ldap_keys - {'dn'}
def __init__(self, name, subject, desc=u"Test generated CA",
default_version=None):
@@ -59,6 +74,8 @@ class CATracker(Tracker):
ipacasubjectdn=[self.ipasubjectdn],
ipacaissuerdn=[fuzzy_issuer],
ipacaid=[fuzzy_caid],
+ certificate=fuzzy_base64,
+ certificate_chain=fuzzy_sequence_of(fuzzy_bytes),
objectclass=objectclasses.ca
)
self.exists = True
@@ -102,9 +119,9 @@ class CATracker(Tracker):
def check_find(self, result, all=False, raw=False):
"""Check the plugin's `find` command result"""
if all:
- expected = self.filter_attrs(self.retrieve_all_keys)
+ expected = self.filter_attrs(self.find_all_keys)
else:
- expected = self.filter_attrs(self.retrieve_keys)
+ expected = self.filter_attrs(self.find_keys)
assert_deepequal(dict(
count=1,
diff --git a/ipatests/test_xmlrpc/xmlrpc_test.py b/ipatests/test_xmlrpc/xmlrpc_test.py
index 0ce1245f4..67565b0d4 100644
--- a/ipatests/test_xmlrpc/xmlrpc_test.py
+++ b/ipatests/test_xmlrpc/xmlrpc_test.py
@@ -22,6 +22,7 @@ Base class for all XML-RPC tests
"""
from __future__ import print_function
+import collections
import datetime
import inspect
@@ -49,6 +50,20 @@ fuzzy_automember_dn = Fuzzy(
'^cn=%s,cn=automember rebuild membership,cn=tasks,cn=config$' % uuid_re
)
+# base64-encoded value
+fuzzy_base64 = Fuzzy('^[0-9A-Za-z/+]+={0,2}$')
+
+
+def fuzzy_sequence_of(fuzzy):
+ """Construct a Fuzzy for a Sequence of values matching the given Fuzzy."""
+ def test(xs):
+ if not isinstance(xs, collections.Sequence):
+ return False
+ else:
+ return all(fuzzy == x for x in xs)
+
+ return Fuzzy(test=test)
+
# Matches an automember task finish message
fuzzy_automember_message = Fuzzy(
'^Automember rebuild task finished\. Processed \(\d+\) entries\.$'
@@ -109,6 +124,8 @@ fuzzy_dergeneralizedtime = Fuzzy(type=datetime.datetime)
# match any string
fuzzy_string = Fuzzy(type=six.string_types)
+fuzzy_bytes = Fuzzy(type=bytes)
+
# case insensitive match of sets
def fuzzy_set_ci(s):
return Fuzzy(test=lambda other: set(x.lower() for x in other) == set(y.lower() for y in s))