freeipa/ipalib/plugins/cert.py

440 lines
14 KiB
Python
Raw Normal View History

2008-12-21 15:15:53 -06:00
# Authors:
# Andrew Wnuk <awnuk@redhat.com>
# Jason Gerard DeRose <jderose@redhat.com>
2009-12-08 15:57:07 -06:00
# John Dennis <jdennis@redhat.com>
2008-12-21 15:15:53 -06:00
#
# Copyright (C) 2009 Red Hat
# see file 'COPYING' for use and warranty information
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License as
# published by the Free Software Foundation; version 2 only
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
"""
Command plugins for IPA-RA certificate operations.
2008-12-21 15:15:53 -06:00
"""
from ipalib import api, SkipPluginModule
if api.env.enable_ra is not True:
# In this case, abort loading this plugin module...
raise SkipPluginModule(reason='env.enable_ra is not True')
from ipalib import Command, Str, Int, Bytes, Flag, File
from ipalib import errors
from ipalib import pkcs10
from ipalib import x509
from ipalib.plugins.virtual import *
from ipalib.plugins.service import split_principal
import base64
from ipalib.request import context
from pyasn1.error import PyAsn1Error
import logging
import traceback
2009-12-08 15:57:07 -06:00
from ipalib.request import ugettext as _
from ipalib.request import context
from ipalib.output import Output
Add external CA signing and abstract out the RA backend External CA signing is a 2-step process. You first have to run the IPA installer which will generate a CSR. You pass this CSR to your external CA and get back a cert. You then pass this cert and the CA cert and re-run the installer. The CSR is always written to /root/ipa.csr. A run would look like: # ipa-server-install --ca --external-ca -p password -a password -r EXAMPLE.COM -u dirsrv -n example.com --hostname=ipa.example.com -U [ sign cert request ] # ipa-server-install --ca --external-ca -p password -a password --external_cert_file=/tmp/rob.crt --external_ca_file=/tmp/cacert.crt -U -p password -a password -r EXAMPLE.COM -u dirsrv -n example.com --hostname=ipa.example.com This also abstracts out the RA backend plugin so the self-signed CA we create can be used in a running server. This means that the cert plugin can request certs (and nothing else). This should let us do online replica creation. To handle the self-signed CA the simple ca_serialno file now contains additional data so we don't have overlapping serial numbers in replicas. This isn't used yet. Currently the cert plugin will not work on self-signed replicas. One very important change for self-signed CAs is that the CA is no longer held in the DS database. It is now in the Apache database. Lots of general fixes were also made in ipaserver.install.certs including: - better handling when multiple CA certificates are in a single file - A temporary directory for request certs is not always created when the class is instantiated (you have to call setup_cert_request())
2009-09-10 15:15:14 -05:00
def get_serial(certificate):
"""
Given a certificate, return the serial number in that cert
2009-12-08 15:57:07 -06:00
as a Python long object.
Add external CA signing and abstract out the RA backend External CA signing is a 2-step process. You first have to run the IPA installer which will generate a CSR. You pass this CSR to your external CA and get back a cert. You then pass this cert and the CA cert and re-run the installer. The CSR is always written to /root/ipa.csr. A run would look like: # ipa-server-install --ca --external-ca -p password -a password -r EXAMPLE.COM -u dirsrv -n example.com --hostname=ipa.example.com -U [ sign cert request ] # ipa-server-install --ca --external-ca -p password -a password --external_cert_file=/tmp/rob.crt --external_ca_file=/tmp/cacert.crt -U -p password -a password -r EXAMPLE.COM -u dirsrv -n example.com --hostname=ipa.example.com This also abstracts out the RA backend plugin so the self-signed CA we create can be used in a running server. This means that the cert plugin can request certs (and nothing else). This should let us do online replica creation. To handle the self-signed CA the simple ca_serialno file now contains additional data so we don't have overlapping serial numbers in replicas. This isn't used yet. Currently the cert plugin will not work on self-signed replicas. One very important change for self-signed CAs is that the CA is no longer held in the DS database. It is now in the Apache database. Lots of general fixes were also made in ipaserver.install.certs including: - better handling when multiple CA certificates are in a single file - A temporary directory for request certs is not always created when the class is instantiated (you have to call setup_cert_request())
2009-09-10 15:15:14 -05:00
In theory there should be only one cert per object so even if we get
passed in a list/tuple only return the first one.
"""
if type(certificate) in (list, tuple):
certificate = certificate[0]
try:
certificate = base64.b64decode(certificate)
except Exception, e:
pass
try:
serial = x509.get_serial_number(certificate, x509.DER)
except PyAsn1Error:
2009-12-08 15:57:07 -06:00
raise errors.CertificateOperationError(error=_('Unable to decode certificate in entry'))
Add external CA signing and abstract out the RA backend External CA signing is a 2-step process. You first have to run the IPA installer which will generate a CSR. You pass this CSR to your external CA and get back a cert. You then pass this cert and the CA cert and re-run the installer. The CSR is always written to /root/ipa.csr. A run would look like: # ipa-server-install --ca --external-ca -p password -a password -r EXAMPLE.COM -u dirsrv -n example.com --hostname=ipa.example.com -U [ sign cert request ] # ipa-server-install --ca --external-ca -p password -a password --external_cert_file=/tmp/rob.crt --external_ca_file=/tmp/cacert.crt -U -p password -a password -r EXAMPLE.COM -u dirsrv -n example.com --hostname=ipa.example.com This also abstracts out the RA backend plugin so the self-signed CA we create can be used in a running server. This means that the cert plugin can request certs (and nothing else). This should let us do online replica creation. To handle the self-signed CA the simple ca_serialno file now contains additional data so we don't have overlapping serial numbers in replicas. This isn't used yet. Currently the cert plugin will not work on self-signed replicas. One very important change for self-signed CAs is that the CA is no longer held in the DS database. It is now in the Apache database. Lots of general fixes were also made in ipaserver.install.certs including: - better handling when multiple CA certificates are in a single file - A temporary directory for request certs is not always created when the class is instantiated (you have to call setup_cert_request())
2009-09-10 15:15:14 -05:00
return serial
def get_subject(certificate):
"""
Given a certificate, return the subject
In theory there should be only one cert per object so even if we get
passed in a list/tuple only return the first one.
"""
if type(certificate) in (list, tuple):
certificate = certificate[0]
try:
certificate = base64.b64decode(certificate)
except Exception, e:
pass
try:
sub = list(x509.get_subject_components(certificate, type=x509.DER))
sub.reverse()
except PyAsn1Error:
raise errors.CertificateOperationError(error=_('Unable to decode certificate in entry'))
subject = ""
for s in sub:
subject = subject + "%s=%s," % (s[0], s[1])
return subject[:-1]
def get_csr_hostname(csr):
"""
Return the value of CN in the subject of the request
"""
try:
request = pkcs10.load_certificate_request(csr)
sub = request.get_subject().get_components()
for s in sub:
if s[0].lower() == "cn":
return s[1]
except PyAsn1Error:
# The ASN.1 decoding errors tend to be long and involved and the
# last bit is generally not interesting. We need the whole traceback.
logging.error('Unable to decode CSR\n%s', traceback.format_exc())
2009-12-08 15:57:07 -06:00
raise errors.CertificateOperationError(error=_('Failure decoding Certificate Signing Request'))
return None
def get_subjectaltname(csr):
"""
Return the value of the subject alt name, if any
"""
try:
request = pkcs10.load_certificate_request(csr)
except PyAsn1Error:
# The ASN.1 decoding errors tend to be long and involved and the
# last bit is generally not interesting. We need the whole traceback.
logging.error('Unable to decode CSR\n%s', traceback.format_exc())
2009-12-08 15:57:07 -06:00
raise errors.CertificateOperationError(error=_('Failure decoding Certificate Signing Request'))
return request.get_subjectaltname()
def validate_csr(ugettext, csr):
"""
Ensure the CSR is base64-encoded and can be decoded by our PKCS#10
parser.
"""
try:
request = pkcs10.load_certificate_request(csr)
# Explicitly request the attributes. This fires off additional
# decoding to get things like the subjectAltName.
attrs = request.get_attributes()
except TypeError, e:
raise errors.Base64DecodeError(reason=str(e))
except PyAsn1Error:
2009-12-08 15:57:07 -06:00
raise errors.CertificateOperationError(error=_('Failure decoding Certificate Signing Request'))
except Exception, e:
2009-12-08 15:57:07 -06:00
raise errors.CertificateOperationError(error=_('Failure decoding Certificate Signing Request: %s') % str(e))
2008-12-21 15:15:53 -06:00
def normalize_csr(csr):
"""
Strip any leading and trailing cruft around the BEGIN/END block
"""
end_len = 37
s = csr.find('-----BEGIN NEW CERTIFICATE REQUEST-----')
if s == -1:
s = csr.find('-----BEGIN CERTIFICATE REQUEST-----')
e = csr.find('-----END NEW CERTIFICATE REQUEST-----')
if e == -1:
e = csr.find('-----END CERTIFICATE REQUEST-----')
if e != -1:
end_len = 33
if s > -1 and e > -1:
# We're normalizing here, not validating
csr = csr[s:e+end_len]
return csr
2008-12-21 15:15:53 -06:00
class cert_request(VirtualCommand):
"""
Submit a certificate signing request.
"""
2008-12-21 15:15:53 -06:00
takes_args = (
File('csr', validate_csr,
cli_name='csr_file',
normalizer=normalize_csr,
),
)
operation="request certificate"
2008-12-21 15:15:53 -06:00
takes_options = (
Str('principal',
doc="service principal for this certificate (e.g. HTTP/test.example.com)",
),
Str('request_type',
default=u'pkcs10',
autofill=True,
),
Flag('add',
doc="automatically add the principal if it doesn't exist",
default=False,
autofill=True
),
Str('certificate?',
label='Certificate',
flags=['no_create', 'no_update', 'no_search'],
),
Str('subject?',
label='Subject',
flags=['no_create', 'no_update', 'no_search'],
),
Str('serial_number?',
label='Serial number',
flags=['no_create', 'no_update', 'no_search'],
),
)
has_output = (
Output('result',
type=dict,
doc='Dictionary mapping variable name to value',
),
)
2008-12-21 15:15:53 -06:00
def execute(self, csr, **kw):
ldap = self.api.Backend.ldap2
principal = kw.get('principal')
add = kw.get('add')
del kw['principal']
del kw['add']
service = None
"""
Access control is partially handled by the ACI titled
'Hosts can modify service userCertificate'. This is for the case
where a machine binds using a host/ prinicpal. It can only do the
request if the target hostname is in the managedBy attribute which
is managed using the add/del member commands.
Binding with a user principal one needs to be in the request_certs
taskgroup (directly or indirectly via role membership).
"""
bind_principal = getattr(context, 'principal')
# Can this user request certs?
if not bind_principal.startswith('host/'):
self.check_access()
# FIXME: add support for subject alt name
# Ensure that the hostname in the CSR matches the principal
subject_host = get_csr_hostname(csr)
(servicename, hostname, realm) = split_principal(principal)
if subject_host.lower() != hostname.lower():
raise errors.ACIError(info="hostname in subject of request '%s' does not match principal hostname '%s'" % (subject_host, hostname))
dn = None
service = None
# See if the service exists and punt if it doesn't and we aren't
# going to add it
try:
if not principal.startswith('host/'):
service = api.Command['service_show'](principal, all=True, raw=True)['result']
dn = service['dn']
else:
realm = principal.find('@')
if realm == -1:
realm = len(principal)
hostname = principal[5:realm]
service = api.Command['host_show'](hostname, all=True, raw=True)['result']
dn = service['dn']
except errors.NotFound, e:
if not add:
Add external CA signing and abstract out the RA backend External CA signing is a 2-step process. You first have to run the IPA installer which will generate a CSR. You pass this CSR to your external CA and get back a cert. You then pass this cert and the CA cert and re-run the installer. The CSR is always written to /root/ipa.csr. A run would look like: # ipa-server-install --ca --external-ca -p password -a password -r EXAMPLE.COM -u dirsrv -n example.com --hostname=ipa.example.com -U [ sign cert request ] # ipa-server-install --ca --external-ca -p password -a password --external_cert_file=/tmp/rob.crt --external_ca_file=/tmp/cacert.crt -U -p password -a password -r EXAMPLE.COM -u dirsrv -n example.com --hostname=ipa.example.com This also abstracts out the RA backend plugin so the self-signed CA we create can be used in a running server. This means that the cert plugin can request certs (and nothing else). This should let us do online replica creation. To handle the self-signed CA the simple ca_serialno file now contains additional data so we don't have overlapping serial numbers in replicas. This isn't used yet. Currently the cert plugin will not work on self-signed replicas. One very important change for self-signed CAs is that the CA is no longer held in the DS database. It is now in the Apache database. Lots of general fixes were also made in ipaserver.install.certs including: - better handling when multiple CA certificates are in a single file - A temporary directory for request certs is not always created when the class is instantiated (you have to call setup_cert_request())
2009-09-10 15:15:14 -05:00
raise errors.NotFound(reason="The service principal for this request doesn't exist.")
try:
service = api.Command['service_add'](principal, **{})['result']
dn = service['dn']
except errors.ACIError:
raise errors.ACIError(info='You need to be a member of the serviceadmin role to add services')
# We got this far so the service entry exists, can we write it?
if not ldap.can_write(dn, "usercertificate"):
raise errors.ACIError(info="Insufficient 'write' privilege to the 'userCertificate' attribute of entry '%s'." % dn)
# Validate the subject alt name, if any
subjectaltname = get_subjectaltname(csr)
if subjectaltname is not None:
for name in subjectaltname:
try:
hostentry = api.Command['host_show'](name, all=True, raw=True)['result']
hostdn = hostentry['dn']
except errors.NotFound:
# We don't want to issue any certificates referencing
# machines we don't know about. Nothing is stored in this
# host record related to this certificate.
raise errors.NotFound(reason='no host record for subject alt name %s in certificate request' % name)
authprincipal = getattr(context, 'principal')
if authprincipal.startswith("host/"):
if not hostdn in service.get('managedby', []):
raise errors.ACIError(info="Insufficient privilege to create a certificate with subject alt name '%s'." % name)
if 'usercertificate' in service:
serial = get_serial(base64.b64encode(service['usercertificate'][0]))
# revoke the certificate and remove it from the service
# entry before proceeding
try:
api.Command['cert_revoke'](unicode(serial), revocation_reason=4)
except errors.NotImplementedError:
# some CA's might not implement revoke
pass
api.Command['service_mod'](principal, usercertificate=None)
# Request the certificate
result = self.Backend.ra.request_certificate(csr, **kw)
# Success? Then add it to the service entry.
if 'certificate' in result:
if not principal.startswith('host/'):
skw = {"usercertificate": str(result.get('certificate'))}
api.Command['service_mod'](principal, **skw)
else:
realm = principal.find('@')
if realm == -1:
realm = len(principal)
hostname = principal[5:realm]
skw = {"usercertificate": str(result.get('certificate'))}
api.Command['host_mod'](hostname, **skw)
return dict(
result=result
)
api.register(cert_request)
2008-12-21 15:15:53 -06:00
class cert_status(VirtualCommand):
"""
Check status of a certificate signing request.
"""
takes_args = (
Str('request_id',
label='Request id',
flags=['no_create', 'no_update', 'no_search'],
),
)
takes_options = (
Str('cert_request_status?',
label='Request status',
flags=['no_create', 'no_update', 'no_search'],
),
)
operation = "certificate status"
2008-12-21 15:15:53 -06:00
def execute(self, request_id, **kw):
self.check_access()
return dict(
result=self.Backend.ra.check_request_status(request_id)
)
2008-12-21 15:15:53 -06:00
api.register(cert_status)
2008-12-21 15:15:53 -06:00
class cert_get(VirtualCommand):
"""
Retrieve an existing certificate.
"""
2008-12-21 15:15:53 -06:00
2009-12-08 15:57:07 -06:00
takes_args = (Str('serial_number',
label='Serial number',
2009-12-08 15:57:07 -06:00
doc='serial number in decimal or if prefixed with 0x in hexadecimal'))
takes_options = (
Str('certificate?',
label='Certificate',
flags=['no_create', 'no_update', 'no_search'],
),
Str('subject?',
label='Subject',
flags=['no_create', 'no_update', 'no_search'],
),
)
operation="retrieve certificate"
2008-12-21 15:15:53 -06:00
def execute(self, serial_number):
self.check_access()
result=self.Backend.ra.get_certificate(serial_number)
result['subject'] = get_subject(result['certificate'])
return dict(result=result)
2008-12-21 15:15:53 -06:00
api.register(cert_get)
2008-12-21 15:15:53 -06:00
class cert_revoke(VirtualCommand):
"""
Revoke a certificate.
"""
2008-12-21 15:15:53 -06:00
2009-12-08 15:57:07 -06:00
takes_args = (Str('serial_number',
doc='serial number in decimal or if prefixed with 0x in hexadecimal'))
takes_options = (
Flag('revoked?',
label='Revoked',
flags=['no_create', 'no_update', 'no_search'],
),
)
operation = "revoke certificate"
2008-12-21 15:15:53 -06:00
# FIXME: The default is 0. Is this really an Int param?
2009-05-08 13:10:53 -05:00
takes_options = (
Int('revocation_reason?',
doc='Reason for revoking the certificate (0-10)',
minvalue=0,
maxvalue=10,
default=0,
),
)
2008-12-21 15:15:53 -06:00
def execute(self, serial_number, **kw):
self.check_access()
return dict(
result=self.Backend.ra.revoke_certificate(serial_number, **kw)
)
2008-12-21 15:15:53 -06:00
api.register(cert_revoke)
2008-12-21 15:15:53 -06:00
class cert_remove_hold(VirtualCommand):
"""
Take a revoked certificate off hold.
"""
2008-12-21 15:15:53 -06:00
2009-12-08 15:57:07 -06:00
takes_args = (Str('serial_number',
doc='serial number in decimal or if prefixed with 0x in hexadecimal'))
takes_options = (
Flag('unrevoked?',
label='Unrevoked',
flags=['no_create', 'no_update', 'no_search'],
),
Str('error_string?',
label='Error',
flags=['no_create', 'no_update', 'no_search'],
),
)
operation = "certificate remove hold"
2008-12-21 15:15:53 -06:00
def execute(self, serial_number, **kw):
self.check_access()
return dict(
result=self.Backend.ra.take_certificate_off_hold(serial_number)
)
api.register(cert_remove_hold)