mirror of
https://salsa.debian.org/freeipa-team/freeipa.git
synced 2025-02-25 18:55:28 -06:00
Add options to write lightweight CA cert or chain to file
Administrators need a way to retrieve the certificate or certificate chain of an IPA-managed lightweight CA. Add params to the `ca' object for carrying the CA certificate and chain (as multiple DER values). Add the `--chain' flag for including the chain in the result (chain is also included with `--all'). Add the `--certificate-out' option for writing the certificate to a file (or the chain, if `--chain' was given). Fixes: https://fedorahosted.org/freeipa/ticket/6178 Reviewed-By: Jan Cholasta <jcholast@redhat.com> Reviewed-By: Tomas Krizek <tkrizek@redhat.com>
This commit is contained in:
parent
cc5b88e5d4
commit
32b1743e5f
6
API.txt
6
API.txt
@ -445,10 +445,11 @@ option: Str('version?')
|
|||||||
output: Output('count', type=[<type 'int'>])
|
output: Output('count', type=[<type 'int'>])
|
||||||
output: Output('results', type=[<type 'list'>, <type 'tuple'>])
|
output: Output('results', type=[<type 'list'>, <type 'tuple'>])
|
||||||
command: ca_add/1
|
command: ca_add/1
|
||||||
args: 1,7,3
|
args: 1,8,3
|
||||||
arg: Str('cn', cli_name='name')
|
arg: Str('cn', cli_name='name')
|
||||||
option: Str('addattr*', cli_name='addattr')
|
option: Str('addattr*', cli_name='addattr')
|
||||||
option: Flag('all', autofill=True, cli_name='all', default=False)
|
option: Flag('all', autofill=True, cli_name='all', default=False)
|
||||||
|
option: Flag('chain', autofill=True, default=False)
|
||||||
option: Str('description?', cli_name='desc')
|
option: Str('description?', cli_name='desc')
|
||||||
option: DNParam('ipacasubjectdn', cli_name='subject')
|
option: DNParam('ipacasubjectdn', cli_name='subject')
|
||||||
option: Flag('raw', autofill=True, cli_name='raw', default=False)
|
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: Output('summary', type=[<type 'unicode'>, <type 'NoneType'>])
|
||||||
output: PrimaryKey('value')
|
output: PrimaryKey('value')
|
||||||
command: ca_show/1
|
command: ca_show/1
|
||||||
args: 1,4,3
|
args: 1,5,3
|
||||||
arg: Str('cn', cli_name='name')
|
arg: Str('cn', cli_name='name')
|
||||||
option: Flag('all', autofill=True, cli_name='all', default=False)
|
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('raw', autofill=True, cli_name='raw', default=False)
|
||||||
option: Flag('rights', autofill=True, default=False)
|
option: Flag('rights', autofill=True, default=False)
|
||||||
option: Str('version?')
|
option: Str('version?')
|
||||||
|
@ -73,8 +73,8 @@ define(IPA_DATA_VERSION, 20100614120000)
|
|||||||
# #
|
# #
|
||||||
########################################################
|
########################################################
|
||||||
define(IPA_API_VERSION_MAJOR, 2)
|
define(IPA_API_VERSION_MAJOR, 2)
|
||||||
define(IPA_API_VERSION_MINOR, 216)
|
define(IPA_API_VERSION_MINOR, 217)
|
||||||
# Last change: DNS: Support URI resource record type
|
# Last change: Add options to write lightweight CA cert or chain to file
|
||||||
|
|
||||||
|
|
||||||
########################################################
|
########################################################
|
||||||
|
53
ipaclient/plugins/ca.py
Normal file
53
ipaclient/plugins/ca.py
Normal file
@ -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
|
@ -2,14 +2,18 @@
|
|||||||
# Copyright (C) 2016 FreeIPA Contributors see COPYING for license
|
# 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.constants import IPA_CA_CN
|
||||||
from ipalib.plugable import Registry
|
from ipalib.plugable import Registry
|
||||||
from ipaserver.plugins.baseldap import (
|
from ipaserver.plugins.baseldap import (
|
||||||
LDAPObject, LDAPSearch, LDAPCreate, LDAPDelete,
|
LDAPObject, LDAPSearch, LDAPCreate, LDAPDelete,
|
||||||
LDAPUpdate, LDAPRetrieve, LDAPQuery, pkey_to_value)
|
LDAPUpdate, LDAPRetrieve, LDAPQuery, pkey_to_value)
|
||||||
from ipaserver.plugins.cert import ca_enabled_check
|
from ipaserver.plugins.cert import ca_enabled_check
|
||||||
from ipalib import _, ngettext
|
from ipalib import _, ngettext, x509
|
||||||
|
|
||||||
|
|
||||||
__doc__ = _("""
|
__doc__ = _("""
|
||||||
@ -100,6 +104,18 @@ class ca(LDAPObject):
|
|||||||
doc=_('Issuer Distinguished Name'),
|
doc=_('Issuer Distinguished Name'),
|
||||||
flags=['no_create', 'no_update'],
|
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']
|
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()
|
@register()
|
||||||
class ca_find(LDAPSearch):
|
class ca_find(LDAPSearch):
|
||||||
__doc__ = _("Search for CAs.")
|
__doc__ = _("Search for CAs.")
|
||||||
@ -154,16 +185,32 @@ class ca_find(LDAPSearch):
|
|||||||
|
|
||||||
def execute(self, *keys, **options):
|
def execute(self, *keys, **options):
|
||||||
ca_enabled_check()
|
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()
|
@register()
|
||||||
class ca_show(LDAPRetrieve):
|
class ca_show(LDAPRetrieve):
|
||||||
__doc__ = _("Display the properties of a CA.")
|
__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()
|
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()
|
@register()
|
||||||
@ -171,6 +218,10 @@ class ca_add(LDAPCreate):
|
|||||||
__doc__ = _("Create a CA.")
|
__doc__ = _("Create a CA.")
|
||||||
msg_summary = _('Created CA "%(value)s"')
|
msg_summary = _('Created CA "%(value)s"')
|
||||||
|
|
||||||
|
takes_options = LDAPCreate.takes_options + (
|
||||||
|
_chain_flag,
|
||||||
|
)
|
||||||
|
|
||||||
def pre_callback(self, ldap, dn, entry, entry_attrs, *keys, **options):
|
def pre_callback(self, ldap, dn, entry, entry_attrs, *keys, **options):
|
||||||
ca_enabled_check()
|
ca_enabled_check()
|
||||||
if not ldap.can_add(dn[1:]):
|
if not ldap.can_add(dn[1:]):
|
||||||
@ -203,6 +254,10 @@ class ca_add(LDAPCreate):
|
|||||||
entry['ipacasubjectdn'] = [resp['dn']]
|
entry['ipacasubjectdn'] = [resp['dn']]
|
||||||
return dn
|
return dn
|
||||||
|
|
||||||
|
def post_callback(self, ldap, dn, entry_attrs, *keys, **options):
|
||||||
|
set_certificate_attrs(entry_attrs, options)
|
||||||
|
return dn
|
||||||
|
|
||||||
|
|
||||||
@register()
|
@register()
|
||||||
class ca_del(LDAPDelete):
|
class ca_del(LDAPDelete):
|
||||||
|
@ -2125,6 +2125,18 @@ class ra_lightweight_ca(RestClient):
|
|||||||
except:
|
except:
|
||||||
raise errors.RemoteRetrieveError(reason=_("Response from CA was not valid JSON"))
|
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):
|
def disable_ca(self, ca_id):
|
||||||
self._ssldo(
|
self._ssldo(
|
||||||
'POST', ca_id + '/disable',
|
'POST', ca_id + '/disable',
|
||||||
|
@ -8,7 +8,13 @@ import six
|
|||||||
from ipapython.dn import DN
|
from ipapython.dn import DN
|
||||||
from ipatests.test_xmlrpc.tracker.base import Tracker
|
from ipatests.test_xmlrpc.tracker.base import Tracker
|
||||||
from ipatests.util import assert_deepequal
|
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
|
from ipatests.test_xmlrpc import objectclasses
|
||||||
|
|
||||||
|
|
||||||
@ -19,12 +25,21 @@ if six.PY3:
|
|||||||
class CATracker(Tracker):
|
class CATracker(Tracker):
|
||||||
"""Implementation of a Tracker class for CA plugin."""
|
"""Implementation of a Tracker class for CA plugin."""
|
||||||
|
|
||||||
retrieve_keys = {
|
ldap_keys = {
|
||||||
'dn', 'cn', 'ipacaid', 'ipacasubjectdn', 'ipacaissuerdn', 'description'
|
'dn', 'cn', 'ipacaid', 'ipacasubjectdn', 'ipacaissuerdn', 'description'
|
||||||
}
|
}
|
||||||
retrieve_all_keys = {'objectclass'} | retrieve_keys
|
cert_keys = {
|
||||||
create_keys = retrieve_all_keys
|
'certificate',
|
||||||
update_keys = retrieve_keys - {'dn'}
|
}
|
||||||
|
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",
|
def __init__(self, name, subject, desc=u"Test generated CA",
|
||||||
default_version=None):
|
default_version=None):
|
||||||
@ -59,6 +74,8 @@ class CATracker(Tracker):
|
|||||||
ipacasubjectdn=[self.ipasubjectdn],
|
ipacasubjectdn=[self.ipasubjectdn],
|
||||||
ipacaissuerdn=[fuzzy_issuer],
|
ipacaissuerdn=[fuzzy_issuer],
|
||||||
ipacaid=[fuzzy_caid],
|
ipacaid=[fuzzy_caid],
|
||||||
|
certificate=fuzzy_base64,
|
||||||
|
certificate_chain=fuzzy_sequence_of(fuzzy_bytes),
|
||||||
objectclass=objectclasses.ca
|
objectclass=objectclasses.ca
|
||||||
)
|
)
|
||||||
self.exists = True
|
self.exists = True
|
||||||
@ -102,9 +119,9 @@ class CATracker(Tracker):
|
|||||||
def check_find(self, result, all=False, raw=False):
|
def check_find(self, result, all=False, raw=False):
|
||||||
"""Check the plugin's `find` command result"""
|
"""Check the plugin's `find` command result"""
|
||||||
if all:
|
if all:
|
||||||
expected = self.filter_attrs(self.retrieve_all_keys)
|
expected = self.filter_attrs(self.find_all_keys)
|
||||||
else:
|
else:
|
||||||
expected = self.filter_attrs(self.retrieve_keys)
|
expected = self.filter_attrs(self.find_keys)
|
||||||
|
|
||||||
assert_deepequal(dict(
|
assert_deepequal(dict(
|
||||||
count=1,
|
count=1,
|
||||||
|
@ -22,6 +22,7 @@ Base class for all XML-RPC tests
|
|||||||
"""
|
"""
|
||||||
from __future__ import print_function
|
from __future__ import print_function
|
||||||
|
|
||||||
|
import collections
|
||||||
import datetime
|
import datetime
|
||||||
import inspect
|
import inspect
|
||||||
|
|
||||||
@ -49,6 +50,20 @@ fuzzy_automember_dn = Fuzzy(
|
|||||||
'^cn=%s,cn=automember rebuild membership,cn=tasks,cn=config$' % uuid_re
|
'^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
|
# Matches an automember task finish message
|
||||||
fuzzy_automember_message = Fuzzy(
|
fuzzy_automember_message = Fuzzy(
|
||||||
'^Automember rebuild task finished\. Processed \(\d+\) entries\.$'
|
'^Automember rebuild task finished\. Processed \(\d+\) entries\.$'
|
||||||
@ -109,6 +124,8 @@ fuzzy_dergeneralizedtime = Fuzzy(type=datetime.datetime)
|
|||||||
# match any string
|
# match any string
|
||||||
fuzzy_string = Fuzzy(type=six.string_types)
|
fuzzy_string = Fuzzy(type=six.string_types)
|
||||||
|
|
||||||
|
fuzzy_bytes = Fuzzy(type=bytes)
|
||||||
|
|
||||||
# case insensitive match of sets
|
# case insensitive match of sets
|
||||||
def fuzzy_set_ci(s):
|
def fuzzy_set_ci(s):
|
||||||
return Fuzzy(test=lambda other: set(x.lower() for x in other) == set(y.lower() for y in s))
|
return Fuzzy(test=lambda other: set(x.lower() for x in other) == set(y.lower() for y in s))
|
||||||
|
Loading…
Reference in New Issue
Block a user