summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rwxr-xr-xchecks/check-ra.py133
-rw-r--r--ipalib/plugins/cert.py36
-rw-r--r--ipalib/plugins/service.py5
-rw-r--r--ipalib/x509.py7
-rw-r--r--ipaserver/plugins/dogtag.py1751
-rw-r--r--ipaserver/plugins/selfsign.py102
6 files changed, 1742 insertions, 292 deletions
diff --git a/checks/check-ra.py b/checks/check-ra.py
index c9831461..6b5e7608 100755
--- a/checks/check-ra.py
+++ b/checks/check-ra.py
@@ -14,87 +14,98 @@ server. I don't exactly remember the steps, so ping him for help.
from os import path
import sys
parent = path.dirname(path.dirname(path.abspath(__file__)))
-sys.path.append(parent)
+sys.path.insert(0, parent)
+verbose = True
from base64 import b64encode, b64decode
from ipalib import api
-# certificate with serial number 17
-cert = b64decode("""
-MIIC3zCCAcegAwIBAgIBETANBgkqhkiG9w0BAQUFADA7MRkwFwYDVQQKExBTamNSZWRoYXQgRG9tYW
-luMR4wHAYDVQQDExVDZXJ0aWZpY2F0ZSBBdXRob3JpdHkwHhcNMDkwMTIyMjMzODA2WhcNMDkwNzIx
-MjMzODA2WjAUMRIwEAYKCZImiZPyLGQBARMCbGwwgZ8wDQYJKoZIhvcNAQEBBQADgY0AMIGJAoGBAM
-id6i9ri9ldyAXaH4MJSPdUDjdc9+E10hwxw7crFE1K0uvr8YT2e1YotNqv7Q+Bk7KVRrLH6Y5UPlWY
-uSAP8G9t8yjn5Uo3iXU5AqsrRek+pxerD/WocwedF6yjJ/zlQyYyg93h0njJr1lStyVLTyp+VVqtk3
-FSDIwLCWQHOTejAgMBAAGjgZgwgZUwHwYDVR0jBBgwFoAUlz9JZxqVabh4QQOEkxyWt80pIQkwQwYI
-KwYBBQUHAQEENzA1MDMGCCsGAQUFBzABhidodHRwOi8vYS1mOC5zamMucmVkaGF0LmNvbTo5MTgwL2
-NhL29jc3AwDgYDVR0PAQH/BAQDAgXgMB0GA1UdJQQWMBQGCCsGAQUFBwMCBggrBgEFBQcDBDANBgkq
-hkiG9w0BAQUFAAOCAQEAhU+oqPh+rlYFPm0D8HAJ0RIWw9gkNctHUfVGi+NeYTaUAEGWUOpXjLSQgP
-gq1fNBHd+IRLhycwp4uUsFCPE1n3eStmn/D6o9u1eNnTFPj74MLZVQQTXPE8+LBYeHgTUwFuKp2WyW
-9J/BDZ3pDWKYWWMawhD7ext7UhZkpIJODFEaDxiXCfB8GsAEbmfoYFk21znuGQQu3Wu1s6licyunLh
-/W3sxCFGIT9DHxS0GZKimm7M02IPGxK/0TZr0kVcLQx6XGKqiK1464rvl4u60mQjwJwfhawshs84YT
-xFnXZKkvsT3GjfIe/k687TMG3paTFtKkis+u7z0v6355uJzLpQ==
-""")
-
-csr = 'MIIBlDCB/gIBADBVMR0wGwYDVQQKExRVc2Vyc3lzUmVkaGF0LURvbWFpbjEQMA4GA1UECxMHcGtpLWlwYTEiMCAGA1UEAxMZSVBBLVN1YnN5c3RlbS1DZXJ0aWZpY2F0ZTCBnzANBgkqhkiG9w0BAQEFAAOBjQAwgYkCgYEA3Qmpr81WxbnISmyyhc2ShiPzUvWIrCg5FgJ1QrBl7CRe62Wl/YYiV/DbuMoex1ec7zKfgfSFVFU9/2iwj7Du0sZdXYJNQPdj9yLdPk2tyxdgJuHLdxI0SNgaEFyvmIMP/X9vQN9H5w0/PyrJQscOxc6tbTcYL0ZSSylLQ+diaQECAwEAA'
+subject = u'CN=vm-070.idm.lab.bos.redhat.com'
+csr = '\
+MIIBZzCB0QIBADAoMSYwJAYDVQQDEx12bS0wNzAuaWRtLmxhYi5ib3MucmVkaGF0\n\
+LmNvbTCBnzANBgkqhkiG9w0BAQEFAAOBjQAwgYkCgYEAriTSlAG+/xkvtxliWMeO\n\
+Qu+vFQTz+/fgy7xWIg6WR2At6j/9eJ7LUYhqguqevOAQpuePxY4/FEfpmQ6PTgs/\n\
+LXKa0vhIkXzkmMjKynUIWHYeaZekcXxye1dV/PdNB6H801xs60YjbScOJj3Hexvm\n\
+hOKsdmwO1ukqTTEKDXrr3c8CAwEAAaAAMA0GCSqGSIb3DQEBBQUAA4GBAG4pTLrE\n\
+cvrkQXVdMOjgAVJ6KZYl/caIOYhIlcJ3jhf95Bv/Zs3lpfHjXnM8jj4EWfyd0lZx\n\
+2EUytXXubKJUpjUCeBp4oaQ2Ahvdxo++oUcbXkKxtCOUB6Mw8XEIVYaldZlcHDHM\n\
+dysLdrZ3K9HOzoeSq2e0m+trQaWnBQG47O7F\n\
+'
+
+reference_decode = {
+ 'certificate' : b64decode
+}
+
+trial_decode = {
+ 'certificate' : b64decode
+}
api.bootstrap(
in_server=True,
enable_ra=True,
- ca_host='a-f8.sjc.redhat.com',
+ ra_plugin='dogtag',
+ ca_host='vm-070.idm.lab.bos.redhat.com',
debug=True,
in_tree=True,
)
api.finalize()
ra = api.Backend.ra
-def assert_equal(*vals):
- val0 = vals[0]
- for val in vals[1:]:
- assert val == val0, '%r != %r' % (val, val0)
+def assert_equal(trial, reference):
+ keys = reference.keys()
+ keys.sort()
+ for key in keys:
+ reference_val = reference[key]
+ trial_val = trial[key]
+ if reference_decode.has_key(key):
+ reference_val = reference_decode[key](reference_val)
-api.log.info('******** Testing ra.check_request_status() ********')
-assert_equal(
- ra.check_request_status('35'),
- dict(
- status='0',
- serial_number='17',
- request_status='complete',
- request_id='35',
- )
-)
+ if trial_decode.has_key(key):
+ trial_val = trial_decode[key](trial_val)
+
+ assert reference_val == trial_val, \
+ '%s: not equal\n\nreference_val:\n%r\ntrial_val:\n%r' % \
+ (key, reference[key], trial[key])
-api.log.info('******** Testing ra.get_certificate() ********')
-assert_equal(
- ra.get_certificate('17'),
- dict(
- status='0',
- certificate=b64encode(cert),
- )
-)
api.log.info('******** Testing ra.request_certificate() ********')
-assert_equal(
- ra.request_certificate(csr),
- dict(
- status='1',
- )
-)
+request_result = ra.request_certificate(csr)
+if verbose: print "request_result=\n%s" % request_result
+assert_equal(request_result,
+ {'subject' : subject,
+ })
+
+api.log.info('******** Testing ra.check_request_status() ********')
+status_result = ra.check_request_status(request_result['request_id'])
+if verbose: print "status_result=\n%s" % status_result
+assert_equal(status_result,
+ {'serial_number' : request_result['serial_number'],
+ 'request_id' : request_result['request_id'],
+ 'cert_request_status' : u'complete',
+ })
+
+api.log.info('******** Testing ra.get_certificate() ********')
+get_result = ra.get_certificate(request_result['serial_number'])
+if verbose: print "get_result=\n%s" % get_result
+assert_equal(get_result,
+ {'serial_number' : request_result['serial_number'],
+ 'certificate' : request_result['certificate'],
+ })
api.log.info('******** Testing ra.revoke_certificate() ********')
-assert_equal(
- ra.revoke_certificate('17', revocation_reason=6), # Put on hold
- dict(
- status='0',
- revoked=True,
- )
-)
+revoke_result = ra.revoke_certificate(request_result['serial_number'],
+ revocation_reason=6) # Put on hold
+if verbose: print "revoke_result=\n%s" % revoke_result
+assert_equal(revoke_result,
+ {'revoked' : True
+ })
+
api.log.info('******** Testing ra.take_certificate_off_hold() ********')
-assert_equal(
- ra.take_certificate_off_hold('17'),
- dict(
- taken_off_hold=True,
- )
-)
+unrevoke_result = ra.take_certificate_off_hold(request_result['serial_number'])
+if verbose: print "unrevoke_result=\n%s" % unrevoke_result
+assert_equal(unrevoke_result,
+ {'unrevoked' : True
+ })
+
diff --git a/ipalib/plugins/cert.py b/ipalib/plugins/cert.py
index ba088dd9..5540e6ec 100644
--- a/ipalib/plugins/cert.py
+++ b/ipalib/plugins/cert.py
@@ -1,6 +1,7 @@
# Authors:
# Andrew Wnuk <awnuk@redhat.com>
# Jason Gerard DeRose <jderose@redhat.com>
+# John Dennis <jdennis@redhat.com>
#
# Copyright (C) 2009 Red Hat
# see file 'COPYING' for use and warranty information
@@ -38,10 +39,12 @@ from ipapython import dnsclient
from pyasn1.error import PyAsn1Error
import logging
import traceback
+from ipalib.request import ugettext as _
def get_serial(certificate):
"""
Given a certificate, return the serial number in that cert
+ as a Python long object.
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.
@@ -49,9 +52,9 @@ def get_serial(certificate):
if type(certificate) in (list, tuple):
certificate = certificate[0]
try:
- serial = str(x509.get_serial_number(certificate))
+ serial = x509.get_serial_number(certificate)
except PyAsn1Error:
- raise errors.GenericError(format='Unable to decode certificate in entry')
+ raise errors.CertificateOperationError(error=_('Unable to decode certificate in entry'))
return serial
@@ -69,7 +72,7 @@ def get_csr_hostname(csr):
# 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())
- raise errors.GenericError(format='Failure decoding Certificate Signing Request')
+ raise errors.CertificateOperationError(error=_('Failure decoding Certificate Signing Request'))
return None
@@ -83,7 +86,7 @@ def get_subjectaltname(csr):
# 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())
- raise errors.GenericError(format='Failure decoding Certificate Signing Request')
+ raise errors.CertificateOperationError(error=_('Failure decoding Certificate Signing Request'))
return request.get_subjectaltname()
def validate_csr(ugettext, csr):
@@ -100,9 +103,9 @@ def validate_csr(ugettext, csr):
except TypeError, e:
raise errors.Base64DecodeError(reason=str(e))
except PyAsn1Error:
- raise errors.GenericError(format='Failure decoding Certificate Signing Request')
+ raise errors.CertificateOperationError(error=_('Failure decoding Certificate Signing Request'))
except Exception, e:
- raise errors.GenericError(format='Failure decoding Certificate Signing Request: %s' % str(e))
+ raise errors.CertificateOperationError(error=_('Failure decoding Certificate Signing Request: %s') % str(e))
class cert_request(VirtualCommand):
@@ -170,7 +173,7 @@ class cert_request(VirtualCommand):
(dn, service) = api.Command['service_show'](principal, all=True, raw=True)
if 'usercertificate' in service:
# FIXME, what to do here? Do we revoke the old cert?
- raise errors.GenericError(format='entry already has a certificate, serial number %s' % get_serial(base64.b64encode(service['usercertificate'][0])))
+ raise errors.CertificateOperationError(error=_('entry already has a certificate, serial number %s') % get_serial(base64.b64encode(service['usercertificate'][0])))
except errors.NotFound, e:
if not add:
raise errors.NotFound(reason="The service principal for this request doesn't exist.")
@@ -213,7 +216,7 @@ class cert_request(VirtualCommand):
if isinstance(result, dict) and len(result) > 0:
textui.print_entry(result, 0)
else:
- textui.print_plain('Failed to submit a certificate request.')
+ textui.print_plain(_('Failed to submit a certificate request.'))
api.register(cert_request)
@@ -235,7 +238,7 @@ class cert_status(VirtualCommand):
if isinstance(result, dict) and len(result) > 0:
textui.print_entry(result, 0)
else:
- textui.print_plain('Failed to retrieve a request status.')
+ textui.print_plain(_('Failed to retrieve a request status.'))
api.register(cert_status)
@@ -245,7 +248,8 @@ class cert_get(VirtualCommand):
Retrieve an existing certificate.
"""
- takes_args = ('serial_number')
+ takes_args = (Str('serial_number',
+ doc='serial number in decimal or if prefixed with 0x in hexadecimal'))
operation="retrieve certificate"
def execute(self, serial_number):
@@ -256,7 +260,7 @@ class cert_get(VirtualCommand):
if isinstance(result, dict) and len(result) > 0:
textui.print_entry(result, 0)
else:
- textui.print_plain('Failed to obtain a certificate.')
+ textui.print_plain(_('Failed to obtain a certificate.'))
api.register(cert_get)
@@ -266,7 +270,8 @@ class cert_revoke(VirtualCommand):
Revoke a certificate.
"""
- takes_args = ('serial_number')
+ takes_args = (Str('serial_number',
+ doc='serial number in decimal or if prefixed with 0x in hexadecimal'))
operation = "revoke certificate"
# FIXME: The default is 0. Is this really an Int param?
@@ -288,7 +293,7 @@ class cert_revoke(VirtualCommand):
if isinstance(result, dict) and len(result) > 0:
textui.print_entry(result, 0)
else:
- textui.print_plain('Failed to revoke a certificate.')
+ textui.print_plain(_('Failed to revoke a certificate.'))
api.register(cert_revoke)
@@ -298,7 +303,8 @@ class cert_remove_hold(VirtualCommand):
Take a revoked certificate off hold.
"""
- takes_args = ('serial_number')
+ takes_args = (Str('serial_number',
+ doc='serial number in decimal or if prefixed with 0x in hexadecimal'))
operation = "certificate remove hold"
def execute(self, serial_number, **kw):
@@ -309,6 +315,6 @@ class cert_remove_hold(VirtualCommand):
if isinstance(result, dict) and len(result) > 0:
textui.print_entry(result, 0)
else:
- textui.print_plain('Failed to take a revoked certificate off hold.')
+ textui.print_plain(_('Failed to take a revoked certificate off hold.'))
api.register(cert_remove_hold)
diff --git a/ipalib/plugins/service.py b/ipalib/plugins/service.py
index 93b9e2b7..5b011915 100644
--- a/ipalib/plugins/service.py
+++ b/ipalib/plugins/service.py
@@ -32,12 +32,13 @@ from pyasn1.error import PyAsn1Error
def get_serial(certificate):
"""
- Given a certificate, return the serial number in that cert.
+ Given a certificate, return the serial number in that
+ cert as a Python long object.
"""
if type(certificate) in (list, tuple):
certificate = certificate[0]
try:
- serial = str(x509.get_serial_number(certificate, type=x509.DER))
+ serial = x509.get_serial_number(certificate, type=x509.DER)
except PyAsn1Error:
raise errors.GenericError(
format='Unable to decode certificate in entry'
diff --git a/ipalib/x509.py b/ipalib/x509.py
index 1db25d06..b570d075 100644
--- a/ipalib/x509.py
+++ b/ipalib/x509.py
@@ -188,8 +188,9 @@ class Certificate(univ.Sequence):
return info.getComponentByName('subject')
def get_serial_number(self):
+ 'return the serial number as a Python long object'
info = self.getComponentByName('tbsCertificate')
- return info.getComponentByName('serialNumber')
+ return long(info.getComponentByName('serialNumber'))
# end of ASN.1 data structures
@@ -230,9 +231,7 @@ def get_subject_components(certificate, type=PEM):
def get_serial_number(certificate, type=PEM):
"""
- Return the serial number of a certificate.
-
- Returns an integer
+ Return the serial number of a certificate as a Python long object.
"""
x509cert = load_certificate(certificate, type)
return x509cert.get_serial_number()
diff --git a/ipaserver/plugins/dogtag.py b/ipaserver/plugins/dogtag.py
index 1bbabbf7..710a50a3 100644
--- a/ipaserver/plugins/dogtag.py
+++ b/ipaserver/plugins/dogtag.py
@@ -2,6 +2,7 @@
# Andrew Wnuk <awnuk@redhat.com>
# Jason Gerard DeRose <jderose@redhat.com>
# Rob Crittenden <rcritten@@redhat.com>
+# John Dennis <jdennis@redhat.com>
#
# Copyright (C) 2009 Red Hat
# see file 'COPYING' for use and warranty information
@@ -19,9 +20,1177 @@
# along with this program; if not, write to the Free Software
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
-"""
-Backend plugin for RA using Dogtag.
-"""
+'''
+
+==============================================
+Backend plugin for RA using Dogtag (e.g. CMS)
+==============================================
+
+Overview of interacting with CMS:
+---------------------------------
+
+CMS stands for "Certificate Management System". It has been released under a
+variety of names, the open source version is called "dogtag".
+
+CMS consists of a number of servlets which in rough terms can be thought of as
+RPC commands. A servlet is invoked by making an HTTP request to a specific URL
+and passing URL arguments. Normally CMS responds with an HTTP reponse consisting
+of HTML to be rendered by a web browser. This HTTP HTML response has both
+Javascript SCRIPT components and HTML rendering code. One of the Javascript
+SCRIPT blocks holds the data for the result. The rest of the response is derived
+from templates associated with the servlet which may be customized. The
+templates pull the result data from Javascript variables.
+
+One way to get the result data is to parse the HTML looking for the Javascript
+varible initializations. Simple string searchs are not a robust method. First of
+all one must be sure the string is only found in a Javascript SCRIPT block and
+not somewhere else in the HTML document. Some of the Javascript variable
+initializations are rather complex (e.g. lists of structures). It would be hard
+to correctly parse such complex and diverse Javascript. Existing Javascript
+parsers are not generally available. Finally, it's important to know the
+character encoding for strings. There is a somewhat complex set of precident
+rules for determining the current character encoding from the HTTP header,
+meta-equiv tags, mime Content-Type and charset attributes on HTML elements. All
+of this means trying to read the result data from a CMS HTML response is
+difficult to do robustly.
+
+However, CMS also supports returning the result data as a XML document
+(distinct from an XHTML document which would be essentially the same as
+described above). There are a wide variety of tools to robustly parse
+XML. Because XML is so well defined things like escapes, character encodings,
+etc. are automatically handled by the tools.
+
+Thus we never try to parse Javascript, instead we always ask CMS to return us an
+XML document by passing the URL argument xml="true". The body of the HTTP
+response is an XML document rather than HTML with embedded Javascript.
+
+To parse the XML documents we use the Python lxml package which is a Python
+binding around the libxml2 implementation. libxml2 is a very fast, standard
+compliant, feature full XML implementation. libxml2 is the XML library of choice
+for many projects. One of the features in lxml and libxml2 that is particularly
+valuable to us is the XPath implementation. We make heavy use of XPath to find
+data in the XML documents we're parsing.
+
+Parse Results vs. IPA command results:
+--------------------------------------
+
+CMS results can be parsed from either HTML or XML. CMS unfortunately is not
+consistent with how it names items or how it utilizes data types. IPA has strict
+rules about data types. Also IPA would like to see a more consistent view CMS
+data. Therefore we split the task of parsing CMS results out from the IPA
+command code. The parse functions normalize the result data by using a
+consistent set of names and data types. The IPA command only deals with the
+normalized parse results. This also allow us to use different parsers if need be
+(i.e. if we had to parse Javascript for some reason). The parse functions
+attempt to parse as must information from the CMS result as is possible. It puts
+the parse result into a dict whose normalized key/value pairs are easy to
+access. IPA commands do not need to return all the parsed results, it can pick
+and choose what it wants to return in the IPA command result from the parse
+result. It also rest assured the values in the parse result will be the correct
+data type. Thus the general sequence of steps for an IPA command talking to CMS
+are:
+
+#. Receive IPA arguments from IPA command
+#. Formulate URL with arguments for CMS
+#. Make request to CMS server
+#. Extract XML document from HTML body returned by CMS
+#. Parse XML document using matching parse routine which returns response dict
+#. Extract relevant items from parse result and insert into command result
+#. Return command result
+
+Serial Numbers:
+---------------
+
+Serial numbers are integral values of any magnitude because they are based on
+ASN.1 integers. CMS uses the Java BigInteger to represent these. Fortunately
+Python also has support for big integers via the Python long() object. Any
+BigIntegers we receive from CMS as a string can be parsed into a Python long
+without loss of information.
+
+However Python has a neat trick. It normally represents integers via the int
+object which internally uses the native C long type. If you create an int
+object by passing the int constructor a string it will check the magnitude of
+the value. If it would fit in a C long then it returns you an int
+object. However if the value is too big for a C long type then it returns you
+a Python long object instead. This is a very nice property because it's much
+more efficient to use C long types when possible (e.g. Python int), but when
+necessary you'll get a Python long() object to handle large magnitude
+values. Python also nicely handles type promotion transparently between int
+and long objects. For example if you multiply two int objects you may get back
+a long object if necessary. In general Python int and long objects may be
+freely mixed without the programmer needing to be aware of which type of
+intergral object is being operated on.
+
+The leads to the following rule, always parse a string representing an
+integral value using the int() constructor even if it might have large
+magnitude because Python will return either an int or a long automatically. By
+the same token don't test for type of an object being int exclusively because
+it could either be an int or a long object.
+
+Internally we should always being using int or long object to hold integral
+values. This is because we should be able to compare them correctly, be free
+from concerns about having the know the radix of the string, perform
+arithmetic operations, and convert to string representation (with correct
+radix) when necessary. In other words internally we should never handle
+integral values as strings.
+
+However, the XMLRPC transport cannot properly handle a Python long object. The
+XMLRPC encoder upon seeing a Python long will test to see if the value fits
+within the range of an 32-bit integer, if so it passes the integer parameter
+otherwise it raises an Overflow exception. The XMLRPC specification does
+permit 64-bit integers (e.g. i8) and the Python XMLRPC module could allow long
+values within the 64-bit range to be passed if it were patched, however this
+only moves the problem, it does not solve passing big integers through
+XMLRPC. Thus we must always pass big integers as a strings through the XMLRPC
+interface. But upon receiving that value from XMLRPC we should convert it back
+into an int or long object. Recall also that Python will automatically perform
+a conversion to string if you output the int or long object in a string context.
+
+Radix Issues:
+-------------
+
+CMS uses the following conventions: Serial numbers are always returned as
+hexadecimal strings without a radix prefix. When CMS takes a serial number as
+input it accepts the value in either decimal or hexadecimal utilizing the radix
+prefix (e.g. 0x) to determine how to parse the value.
+
+IPA has adopted the convention that all integral values in the user interface
+will use base 10 decimal radix.
+
+Basic rules on handling these values
+
+1. Reading a serial number from CMS requires conversion from hexadecimal
+ by converting it into a Python int or long object, use the int constructor:
+
+ >>> serial_number = int(serial_number, 16)
+
+2. Big integers passed to XMLRPC must be decimal unicode strings
+
+ >>> unicode(serial_number)
+
+3. Big integers received from XMLRPC must be converted back to int or long
+ objects from the decimal string representation.
+
+ >>> serial_number = int(serial_number)
+
+Xpath pattern matching on node names:
+-------------------------------------
+
+There are many excellent tutorial on how to use xpath to find items in an XML
+document, as such there is no need to repeat this information here. However,
+most xpath tutorials make the assumption the node names you're searching for are
+fixed. For example:
+
+ doc.xpath('//book/chapter[*]/section[2]')
+
+Selects the second section of every chapter of the book. In this example the
+node names 'book', 'chapter', 'section' are fixed. But what if the XML document
+embedded the chapter number in the node name, for example 'chapter1',
+'chapter2', etc.? (If you're thinking this would be incredibly lame, you're
+right, but sadly people do things like this). Thus in this case you can't use
+the node name 'chapter' in the xpath location step because it's not fixed and
+hence won't match 'chapter1', 'chapter2', etc. The solution to this seems
+obvious, use some type of pattern matching on the node name. Unfortunately this
+advanced use of xpath is seldom discussed in tutorials and it's not obvious how
+to do it. Here are some hints.
+
+Use the built-in xpath string functions. Most of the examples illustrate the
+string function being passed the text *contents* of the node via '.' or
+string(.). However we don't want to pass the contents of the node, instead we
+want to pass the node name. To do this use the name() function. One way we could
+solve the chapter problem above is by using a predicate which says if the node
+name begins with 'chapter' it's a match. Here is how you can do that.
+
+ >>> doc.xpath("//book/*[starts-with(name(), 'chapter')]/section[2]")
+
+The built-in starts-with() returns true if it's first argument starts with it's
+second argument. Thus the example above says if the node name of the second
+location step begins with 'chapter' consider it a match and the search
+proceeds to the next location step, which in this example is any node named
+'section'.
+
+But what if we would like to utilize the power of regular expressions to perform
+the test against the node name? In this case we can use the EXSLT regular
+expression extension. EXSLT extensions are accessed by using XML
+namespaces. The regular expression name space identifier is 're:' In lxml we
+need to pass a set of namespaces to XPath object constructor in order to allow
+it to bind to those namespaces during it's evaluation. Then we just use the
+EXSLT regular expression match() function on the node name. Here is how this is
+done:
+
+ >>> regexpNS = "http://exslt.org/regular-expressions"
+ >>> find = etree.XPath("//book/*[re:match(name(), '^chapter(_\d+)$')]/section[2]",
+ ... namespaces={'re':regexpNS}
+ >>> find(doc)
+
+What is happening here is that etree.XPath() has returned us an evaluator
+function which we bind to the name 'find'. We've passed it a set of namespaces
+as a dict via the 'namespaces' keyword parameter of etree.XPath(). The predicate
+for the second location step uses the 're:' namespace to find the function name
+'match'. The re:match() takes a string to search as it's first argument and a
+regular expression pattern as it's second argument. In this example the string
+to seach is the node name of the location step because we called the built-in
+node() function of XPath. The regular expression pattern we've passed says it's
+a match if the string begins with 'chapter' is followed by any number of
+digits and nothing else follows.
+
+'''
+
+from lxml import etree
+import datetime
+
+# These are general status return values used when
+# CMSServlet.outputError() is invoked.
+CMS_SUCCESS = 0
+CMS_FAILURE = 1
+CMS_AUTH_FAILURE = 2
+
+# CMS (Certificate Management System) status return values
+# These are requestStatus return values used with templates
+CMS_STATUS_UNAUTHORIZED = 1
+CMS_STATUS_SUCCESS = 2
+CMS_STATUS_PENDING = 3
+CMS_STATUS_SVC_PENDING = 4
+CMS_STATUS_REJECTED = 5
+CMS_STATUS_ERROR = 6
+CMS_STATUS_EXCEPTION = 7
+
+def cms_request_status_to_string(request_status):
+ '''
+ :param request_status: The integral request status value
+ :return: String name of request status
+ '''
+ return {
+ 1 : 'UNAUTHORIZED',
+ 2 : 'SUCCESS',
+ 3 : 'PENDING',
+ 4 : 'SVC_PENDING',
+ 5 : 'REJECTED',
+ 6 : 'ERROR',
+ 7 : 'EXCEPTION',
+ }.get(request_status, "unknown(%d)" % request_status)
+
+def cms_error_code_to_string(error_code):
+ '''
+ :param error_code: The integral error code value
+ :return: String name of the error code
+ '''
+ return {
+ 0 : 'SUCCESS',
+ 1 : 'FAILURE',
+ 2 : 'AUTH_FAILURE',
+ }.get(error_code, "unknown(%d)" % error_code)
+
+def parse_and_set_boolean_xml(node, response, response_name):
+ '''
+ :param node: xml node object containing value to parse for boolean result
+ :param response: response dict to set boolean result in
+ :param response_name: name of the respone value to set
+ :except ValueError:
+
+ Read the value out of a xml text node and interpret it as a boolean value.
+ The text values are stripped of whitespace and converted to lower case
+ prior to interpretation.
+
+ If the value is recognized the response dict is updated using the
+ request_name as the key and the value is set to the bool value of either
+ True or False depending on the interpretation of the text value. If the text
+ value is not recognized a ValueError exception is thrown.
+
+ Text values which result in True:
+
+ - true
+ - yes
+ - on
+
+ Text values which result in False:
+
+ - false
+ - no
+ - off
+ '''
+ value = node.text.strip().lower()
+ if value == 'true' or value == 'yes':
+ value = True
+ elif value == 'false' or value == 'no':
+ value = False
+ else:
+ raise ValueError('expected true|false|yes|no|on|off for "%s", but got "%s"' % \
+ (response_name, value))
+ response[response_name] = value
+
+def get_error_code_xml(doc):
+ '''
+ :param doc: The root node of the xml document to parse
+ :returns: error code as an integer or None if not found
+
+ Returns the error code when the servlet replied with
+ CMSServlet.outputError()
+
+ The possible error code values are:
+
+ - CMS_SUCCESS = 0
+ - CMS_FAILURE = 1
+ - CMS_AUTH_FAILURE = 2
+
+ However, profileSubmit sometimes also returns these values:
+
+ - EXCEPTION = 1
+ - DEFERRED = 2
+ - REJECTED = 3
+
+ '''
+
+ error_code = doc.xpath('//XMLResponse/Status[1]')
+ if len(error_code) == 1:
+ error_code = int(error_code[0].text)
+ else:
+ # If error code wasn't present, but error string was
+ # then it's an error.
+ error_string = doc.xpath('//XMLResponse/Error[1]')
+ if len(error_string) == 1:
+ error_code = CMS_FAILURE
+ else:
+ # no status and no error string, assume success
+ error_code = CMS_SUCCESS
+
+ return error_code
+
+def get_request_status_xml(doc):
+ '''
+ :param doc: The root node of the xml document to parse
+ :returns: request status as an integer
+
+ Returns the request status from a CMS operation. May be one of:
+
+ - CMS_STATUS_UNAUTHORIZED = 1
+ - CMS_STATUS_SUCCESS = 2
+ - CMS_STATUS_PENDING = 3
+ - CMS_STATUS_SVC_PENDING = 4
+ - CMS_STATUS_REJECTED = 5
+ - CMS_STATUS_ERROR = 6
+ - CMS_STATUS_EXCEPTION = 7
+
+ CMS will often fail to return requestStatus when the status is
+ SUCCESS. Therefore if we fail to find a requestStatus field we default the
+ result to CMS_STATUS_SUCCESS.
+ '''
+
+ request_status = doc.xpath('//xml/fixed/requestStatus[1]')
+ if len(request_status) == 1:
+ request_status = int(request_status[0].text)
+ else:
+ # When a request is successful CMS often omits the requestStatus
+ request_status = CMS_STATUS_SUCCESS
+
+ # However, if an error string was returned it's an error no
+ # matter what CMS returned as requestStatus.
+ # Just to make life interesting CMS sometimes returns an empty error string
+ # when nothing wrong occurred.
+ error_detail = doc.xpath('//xml/fixed/errorDetails[1]')
+ if len(error_detail) == 1 and len(error_detail[0].text.strip()) > 0:
+ # There was a non-empty error string, if the status was something
+ # other than error or exception then force it to be an error.
+ if not (request_status in (CMS_STATUS_ERROR, CMS_STATUS_EXCEPTION)):
+ request_status = CMS_STATUS_ERROR
+
+ return request_status
+
+
+def parse_error_template_xml(doc):
+ '''
+ :param doc: The root node of the xml document to parse
+ :returns: result dict
+
+ CMS currently returns errors via XML as either a "template" document
+ (generated by CMSServlet.outputXML() or a "response" document (generated by
+ CMSServlet.outputError()).
+
+ This routine is used to parse a "template" style error or exception
+ document.
+
+ This routine should be use when the CMS requestStatus is ERROR or
+ EXCEPTION. It is capable of parsing both. A CMS ERROR occurs when a known
+ anticipated error condition occurs (e.g. asking for an item which does not
+ exist). A CMS EXCEPTION occurs when an exception is thrown in the CMS server
+ and it's not caught and converted into an ERROR. Think of EXCEPTIONS as the
+ "catch all" error situation.
+
+ ERROR's and EXCEPTIONS's both have error message strings associated with
+ them. For an ERROR it's errorDetails, for an EXCEPTION it's
+ unexpectedError. In addition an EXCEPTION may include an array of additional
+ error strings in it's errorDescription field.
+
+ After parsing the results are returned in a result dict. The following
+ table illustrates the mapping from the CMS data item to what may be found in
+ the result dict. If a CMS data item is absent it will also be absent in the
+ result dict.
+
+ +----------------+---------------+------------------+---------------+
+ |cms name |cms type |result name |result type |
+ +================+===============+==================+===============+
+ |requestStatus |int |request_status |int |
+ +----------------+---------------+------------------+---------------+
+ |errorDetails |string |error_string [1]_ |unicode |
+ +----------------+---------------+------------------+---------------+
+ |unexpectedError |string |error_string [1]_ |unicode |
+ +----------------+---------------+------------------+---------------+
+ |errorDescription|[string] |error_descriptions|[unicode] |
+ +----------------+---------------+------------------+---------------+
+ |authority |string |authority |unicode |
+ +----------------+---------------+------------------+---------------+
+
+ .. [1] errorDetails is the error message string when the requestStatus
+ is ERROR. unexpectedError is the error message string when
+ the requestStatus is EXCEPTION. This routine recognizes both
+ ERROR's and EXCEPTION's and depending on which is found folds
+ the error message into the error_string result value.
+ '''
+
+ response = {}
+ response['request_status'] = CMS_STATUS_ERROR # assume error
+
+
+ request_status = doc.xpath('//xml/fixed/requestStatus[1]')
+ if len(request_status) == 1:
+ request_status = int(request_status[0].text)
+ response['request_status'] = request_status
+
+ error_descriptions = []
+ for description in doc.xpath('//xml/records[*]/record/errorDescription'):
+ error_descriptions.append(etree.tostring(description, method='text',
+ encoding=unicode).strip())
+ if len(error_descriptions) > 0:
+ response['error_descriptions'] = error_descriptions
+
+ authority = doc.xpath('//xml/fixed/authorityName[1]')
+ if len(authority) == 1:
+ authority = etree.tostring(authority[0], method='text',
+ encoding=unicode).strip()
+ response['authority'] = authority
+
+ # Should never get both errorDetail and unexpectedError
+ error_detail = doc.xpath('//xml/fixed/errorDetails[1]')
+ if len(error_detail) == 1:
+ error_detail = etree.tostring(error_detail[0], method='text',
+ encoding=unicode).strip()
+ response['error_string'] = error_detail
+
+ unexpected_error = doc.xpath('//xml/fixed/unexpectedError[1]')
+ if len(unexpected_error) == 1:
+ unexpected_error = etree.tostring(unexpected_error[0], method='text',
+ encoding=unicode).strip()
+ response['error_string'] = unexpected_error
+
+ return response
+
+
+def parse_error_response_xml(doc):
+ '''
+ :param doc: The root node of the xml document to parse
+ :returns: result dict
+
+ CMS currently returns errors via XML as either a "template" document
+ (generated by CMSServlet.outputXML() or a "response" document (generated by
+ CMSServlet.outputError()).
+
+ This routine is used to parse a "response" style error document.
+
+ +---------------+---------------+---------------+---------------+
+ |cms name |cms type |result name |result type |
+ +===============+===============+===============+===============+
+ |Status |int |error_code |int [1]_ |
+ +---------------+---------------+---------------+---------------+
+ |Error |string |error_string |unicode |
+ +---------------+---------------+---------------+---------------+
+ |RequestID |string |request_id |string |
+ +---------------+---------------+---------------+---------------+
+
+ .. [1] error code may be one of:
+
+ - CMS_SUCCESS = 0
+ - CMS_FAILURE = 1
+ - CMS_AUTH_FAILURE = 2
+
+ However, profileSubmit sometimes also returns these values:
+
+ - EXCEPTION = 1
+ - DEFERRED = 2
+ - REJECTED = 3
+
+ '''
+
+ response = {}
+ response['error_code'] = CMS_FAILURE # assume error
+
+ error_code = doc.xpath('//XMLResponse/Status[1]')
+ if len(error_code) == 1:
+ error_code = int(error_code[0].text)
+ response['error_code'] = error_code
+
+ error_string = doc.xpath('//XMLResponse/Error[1]')
+ if len(error_string) == 1:
+ error_string = etree.tostring(error_string[0], method='text',
+ encoding=unicode).strip()
+ response['error_string'] = error_string
+
+ request_id = doc.xpath('//XMLResponse/RequestId[1]')
+ if len(request_id) == 1:
+ request_id = etree.tostring(request_id[0], method='text',
+ encoding=unicode).strip()
+ response['request_id'] = request_id
+
+ return response
+
+def parse_profile_submit_result_xml(doc):
+ '''
+ :param doc: The root node of the xml document to parse
+ :returns: result dict
+ :except ValueError:
+
+ CMS returns an error code and an array of request records.
+
+ This function returns a response dict with the following format:
+ {'error_code' : int, 'requests' : [{}]}
+
+ The mapping of fields and data types is illustrated in the following table.
+
+ If the error_code is not SUCCESS then the response dict will have the
+ contents described in `parse_error_response_xml`.
+
+ +--------------------+----------------+------------------------+---------------+
+ |cms name |cms type |result name |result type |
+ +====================+================+========================+===============+
+ |Status |int |error_code |int |
+ +--------------------+----------------+------------------------+---------------+
+ |Requests[].Id |string |requests[].request_id |unicode |
+ +--------------------+----------------+------------------------+---------------+
+ |Requests[].SubjectDN|string |requests[].subject |unicode |
+ +--------------------+----------------+------------------------+---------------+
+ |Requests[].serialno |BigInteger |requests[].serial_number|int|long |
+ +--------------------+----------------+------------------------+---------------+
+ |Requests[].b64 |string |requests[].certificate |unicode [1]_ |
+ +--------------------+----------------+------------------------+---------------+
+ |Requests[].pkcs7 |string | | |
+ +--------------------+----------------+------------------------+---------------+
+
+ .. [1] Base64 encoded
+
+ '''
+
+ error_code = get_error_code_xml(doc)
+ if error_code != CMS_SUCCESS:
+ response = parse_error_response_xml(doc)
+ return response
+
+ response = {}
+ response['error_code'] = error_code
+
+ requests = []
+ response['requests'] = requests
+
+ for request in doc.xpath('//XMLResponse/Requests[*]/Request'):
+ response_request = {}
+ requests.append(response_request)
+
+ request_id = request.xpath('Id[1]')
+ if len(request_id) == 1:
+ request_id = etree.tostring(request_id[0], method='text',
+ encoding=unicode).strip()
+ response_request['request_id'] = request_id
+
+ subject_dn = request.xpath('SubjectDN[1]')
+ if len(subject_dn) == 1:
+ subject_dn = etree.tostring(subject_dn[0], method='text',
+ encoding=unicode).strip()
+ response_request['subject'] = subject_dn
+
+ serial_number = request.xpath('serialno[1]')
+ if len(serial_number) == 1:
+ serial_number = int(serial_number[0].text, 16) # parse as hex
+ response_request['serial_number'] = serial_number
+
+ certificate = request.xpath('b64[1]')
+ if len(certificate) == 1:
+ certificate = etree.tostring(certificate[0], method='text',
+ encoding=unicode).strip()
+ response_request['certificate'] = certificate
+
+ return response
+
+
+def parse_check_request_result_xml(doc):
+ '''
+ :param doc: The root node of the xml document to parse
+ :returns: result dict
+ :except ValueError:
+
+ After parsing the results are returned in a result dict. The following
+ table illustrates the mapping from the CMS data item to what may be found in
+ the result dict. If a CMS data item is absent it will also be absent in the
+ result dict.
+
+ If the requestStatus is not SUCCESS then the response dict will have the
+ contents described in `parse_error_template_xml`.
+
+ +-------------------------+---------------+-------------------+-----------------+
+ |cms name |cms type |result name |result type |
+ +=========================+===============+===================+=================+
+ |authority |string |authority |unicode |
+ +-------------------------+---------------+-------------------+-----------------+
+ |requestId |string |request_id |string |
+ +-------------------------+---------------+-------------------+-----------------+
+ |staus |string |cert_request_status|unicode [1]_ |
+ +-------------------------+---------------+-------------------+-----------------+
+ |createdOn |long, timestamp|created_on |datetime.datetime|
+ +-------------------------+---------------+-------------------+-----------------+
+ |updatedOn |long, timestamp|updated_on |datetime.datetime|
+ +-------------------------+---------------+-------------------+-----------------+
+ |requestNotes |string |request_notes |unicode |
+ +-------------------------+---------------+-------------------+-----------------+
+ |pkcs7ChainBase64 |string |pkcs7_chain |unicode [2]_ |
+ +-------------------------+---------------+-------------------+-----------------+
+ |cmcFullEnrollmentResponse|string |full_response |unicode [2]_ |
+ +-------------------------+---------------+-------------------+-----------------+
+ |records[].serialNumber |BigInteger |serial_numbers |[int|long] |
+ +-------------------------+---------------+-------------------+-----------------+
+
+ .. [1] cert_request_status may be one of:
+
+ - "begin"
+ - "pending"
+ - "approved"
+ - "svc_pending"
+ - "canceled"
+ - "rejected"
+ - "complete"
+
+ .. [2] Base64 encoded
+
+ '''
+ request_status = get_request_status_xml(doc)
+
+ if request_status != CMS_STATUS_SUCCESS:
+ response = parse_error_template_xml(doc)
+ return response
+
+ response = {}
+ response['request_status'] = request_status
+
+ cert_request_status = doc.xpath('//xml/header/status[1]')
+ if len(cert_request_status) == 1:
+ cert_request_status = etree.tostring(cert_request_status[0], method='text',
+ encoding=unicode).strip()
+ response['cert_request_status'] = cert_request_status
+
+ request_id = doc.xpath('//xml/header/requestId[1]')
+ if len(request_id) == 1:
+ request_id = etree.tostring(request_id[0], method='text',
+ encoding=unicode).strip()
+ response['request_id'] = request_id
+
+ authority = doc.xpath('//xml/header/authority[1]')
+ if len(authority) == 1:
+ authority = etree.tostring(authority[0], method='text',
+ encoding=unicode).strip()
+ response['authority'] = authority
+
+ updated_on = doc.xpath('//xml/header/updatedOn[1]')
+ if len(updated_on) == 1:
+ updated_on = datetime.datetime.utcfromtimestamp(int(updated_on[0].text))
+ response['updated_on'] = updated_on
+
+ created_on = doc.xpath('//xml/header/createdOn[1]')
+ if len(created_on) == 1:
+ created_on = datetime.datetime.utcfromtimestamp(int(created_on[0].text))
+ response['created_on'] = created_on
+
+ request_notes = doc.xpath('//xml/header/requestNotes[1]')
+ if len(request_notes) == 1:
+ request_notes = etree.tostring(request_notes[0], method='text',
+ encoding=unicode).strip()
+ response['request_notes'] = request_notes
+
+ pkcs7_chain = doc.xpath('//xml/header/pkcs7ChainBase64[1]')
+ if len(pkcs7_chain) == 1:
+ pkcs7_chain = etree.tostring(pkcs7_chain[0], method='text',
+ encoding=unicode).strip()
+ response['pkcs7_chain'] = pkcs7_chain
+
+ full_response = doc.xpath('//xml/header/cmcFullEnrollmentResponse[1]')
+ if len(full_response) == 1:
+ full_response = etree.tostring(full_response[0], method='text',
+ encoding=unicode).strip()
+ response['full_response'] = full_response
+
+ serial_numbers = []
+ response['serial_numbers'] = serial_numbers
+ for serial_number in doc.xpath('//xml/records[*]/record/serialNumber'):
+ serial_number = int(serial_number.text, 16) # parse as hex
+ serial_numbers.append(serial_number)
+
+ return response
+
+def parse_display_cert_xml(doc):
+ '''
+ :param doc: The root node of the xml document to parse
+ :returns: result dict
+ :except ValueError:
+
+ After parsing the results are returned in a result dict. The following
+ table illustrates the mapping from the CMS data item to what may be found in
+ the result dict. If a CMS data item is absent it will also be absent in the
+ result dict.
+
+ If the requestStatus is not SUCCESS then the response dict will have the
+ contents described in `parse_error_template_xml`.
+
+ +----------------+---------------+-----------------+---------------+
+ |cms name |cms type |result name |result type |
+ +================+===============+=================+===============+
+ |emailCert |Boolean |email_cert |bool |
+ +----------------+---------------+-----------------+---------------+
+ |noCertImport |Boolean |no_cert_import |bool |
+ +----------------+---------------+-----------------+---------------+
+ |revocationReason|int |revocation_reason|int [1]_ |
+ +----------------+---------------+-----------------+---------------+
+ |certPrettyPrint |string |cert_pretty |unicode |
+ +----------------+---------------+-----------------+---------------+
+ |authorityid |string |authority |unicode |
+ +----------------+---------------+-----------------+---------------+
+ |certFingerprint |string |fingerprint |unicode |
+ +----------------+---------------+-----------------+---------------+
+ |certChainBase64 |string |certificate |unicode [2]_ |
+ +----------------+---------------+-----------------+---------------+
+ |serialNumber |string |serial_number |int|long |
+ +----------------+---------------+-----------------+---------------+
+ |pkcs7ChainBase64|string |pkcs7_chain |unicode [2]_ |
+ +----------------+---------------+-----------------+---------------+
+
+ .. [1] revocation reason may be one of:
+
+ - 0 = UNSPECIFIED
+ - 1 = KEY_COMPROMISE
+ - 2 = CA_COMPROMISE
+ - 3 = AFFILIATION_CHANGED
+ - 4 = SUPERSEDED
+ - 5 = CESSATION_OF_OPERATION
+ - 6 = CERTIFICATE_HOLD
+ - 8 = REMOVE_FROM_CRL
+ - 9 = PRIVILEGE_WITHDRAWN
+ - 10 = AA_COMPROMISE
+
+ .. [2] Base64 encoded
+
+ '''
+
+ request_status = get_request_status_xml(doc)
+
+ if request_status != CMS_STATUS_SUCCESS:
+ response = parse_error_template_xml(doc)
+ return response
+
+ response = {}
+ response['request_status'] = request_status
+
+ email_cert = doc.xpath('//xml/header/emailCert[1]')
+ if len(email_cert) == 1:
+ parse_and_set_boolean_xml(email_cert[0], response, 'email_cert')
+
+ no_cert_import = doc.xpath('//xml/header/noCertImport[1]')
+ if len(no_cert_import) == 1:
+ parse_and_set_boolean_xml(no_cert_import[0], response, 'no_cert_import')
+
+ revocation_reason = doc.xpath('//xml/header/revocationReason[1]')
+ if len(revocation_reason) == 1:
+ revocation_reason = int(revocation_reason[0].text)
+ response['revocation_reason'] = revocation_reason
+
+ cert_pretty = doc.xpath('//xml/header/certPrettyPrint[1]')
+ if len(cert_pretty) == 1:
+ cert_pretty = etree.tostring(cert_pretty[0], method='text',
+ encoding=unicode).strip()
+ response['cert_pretty'] = cert_pretty
+
+ authority = doc.xpath('//xml/header/authorityid[1]')
+ if len(authority) == 1:
+ authority = etree.tostring(authority[0], method='text',
+ encoding=unicode).strip()
+ response['authority'] = authority
+
+ fingerprint = doc.xpath('//xml/header/certFingerprint[1]')
+ if len(fingerprint) == 1:
+ fingerprint = etree.tostring(fingerprint[0], method='text',
+ encoding=unicode).strip()
+ response['fingerprint'] = fingerprint
+
+ certificate = doc.xpath('//xml/header/certChainBase64[1]')
+ if len(certificate) == 1:
+ certificate = etree.tostring(certificate[0], method='text',
+ encoding=unicode).strip()
+ response['certificate'] = certificate
+
+ serial_number = doc.xpath('//xml/header/serialNumber[1]')
+ if len(serial_number) == 1:
+ serial_number = int(serial_number[0].text, 16) # parse as hex
+ response['serial_number'] = serial_number
+
+ pkcs7_chain = doc.xpath('//xml/header/pkcs7ChainBase64[1]')
+ if len(pkcs7_chain) == 1:
+ pkcs7_chain = etree.tostring(pkcs7_chain[0], method='text',
+ encoding=unicode).strip()
+ response['pkcs7_chain'] = pkcs7_chain
+
+ return response
+
+def parse_revoke_cert_xml(doc):
+ '''
+ :param doc: The root node of the xml document to parse
+ :returns: result dict
+ :except ValueError:
+
+ After parsing the results are returned in a result dict. The following
+ table illustrates the mapping from the CMS data item to what may be found in
+ the result dict. If a CMS data item is absent it will also be absent in the
+ result dict.
+
+ If the requestStatus is not SUCCESS then the response dict will have the
+ contents described in `parse_error_template_xml`.
+
+ +----------------------+----------------+-----------------------+---------------+
+ |cms name |cms type |result name |result type |
+ +======================+================+=======================+===============+
+ |dirEnabled |string [1]_ |dir_enabled |bool |
+ +----------------------+----------------+-----------------------+---------------+
+ |certsUpdated |int |certs_updated |int |
+ +----------------------+----------------+-----------------------+---------------+
+ |certsToUpdate |int |certs_to_update |int |
+ +----------------------+----------------+-----------------------+---------------+
+ |error |string [2]_ |error_string |unicode |
+ +----------------------+----------------+-----------------------+---------------+
+ |revoked |string [3]_ |revoked |unicode |
+ +----------------------+----------------+-----------------------+---------------+
+ |totalRecordCount |int |total_record_count |int |
+ +----------------------+----------------+-----------------------+---------------+
+ |updateCRL |string [1]_ [4]_|update_crl |bool |
+ +----------------------+----------------+-----------------------+---------------+
+ |updateCRLSuccess |string [1]_ [4]_|update_crl_success |bool |
+ +----------------------+----------------+-----------------------+---------------+
+ |updateCRLError |string [4]_ |update_crl_error |unicode |
+ +----------------------+----------------+-----------------------+---------------+
+ |publishCRLSuccess |string [1]_[4]_ |publish_crl_success |bool |
+ +----------------------+----------------+-----------------------+---------------+
+ |publishCRLError |string [4]_ |publish_crl_error |unicode |
+ +----------------------+----------------+-----------------------+---------------+
+ |crlUpdateStatus |string [1]_ [5]_|crl_update_status |bool |
+ +----------------------+----------------+-----------------------+---------------+
+ |crlUpdateError |string [5]_ |crl_update_error |unicode |
+ +----------------------+----------------+-----------------------+---------------+
+ |crlPublishStatus |string [1]_ [5]_|crl_publish_status |bool |
+ +----------------------+----------------+-----------------------+---------------+
+ |crlPublishError |string [5]_ |crl_publish_error |unicode |
+ +----------------------+----------------+-----------------------+---------------+
+ |records[].serialNumber|BigInteger |records[].serial_number|int|long |
+ +----------------------+----------------+-----------------------+---------------+
+ |records[].error |string [2]_ |records[].error_string |unicode |
+ +----------------------+----------------+-----------------------+---------------+
+
+ .. [1] String value is either "yes" or "no"
+ .. [2] Sometimes the error string is empty (null)
+ .. [3] revoked may be one of:
+
+ - "yes"
+ - "no"
+ - "begin"
+ - "pending"
+ - "approved"
+ - "svc_pending"
+ - "canceled"
+ - "rejected"
+ - "complete"
+
+ .. [4] Only sent if CRL update information is available.
+ If sent it's only value is "yes".
+ If sent then the following values may also be sent,
+ otherwise they will be absent:
+
+ - updateCRLSuccess
+ - updateCRLError
+ - publishCRLSuccess
+ - publishCRLError
+
+ .. [5] The cms name varies depending on whether the issuing point is MasterCRL
+ or not. If the issuing point is not the MasterCRL then the cms name
+ will be appended with an underscore and the issuing point name.
+ Thus for example the cms name crlUpdateStatus will be crlUpdateStatus
+ if the issuing point is the MasterCRL. However if the issuing point
+ is "foobar" then crlUpdateStatus will be crlUpdateStatus_foobar.
+ When we return the response dict the key will always be the "base"
+ name without the _issuing_point suffix. Thus crlUpdateStatus_foobar
+ will appear in the response dict under the key 'crl_update_status'
+
+ '''
+
+ request_status = get_request_status_xml(doc)
+
+ if request_status != CMS_STATUS_SUCCESS:
+ response = parse_error_template_xml(doc)
+ return response
+
+ response = {}
+ response['request_status'] = request_status
+
+ records = []
+ response['records'] = records
+
+ dir_enabled = doc.xpath('//xml/header/dirEnabled[1]')
+ if len(dir_enabled) == 1:
+ parse_and_set_boolean_xml(dir_enabled[0], response, 'dir_enabled')
+
+ certs_updated = doc.xpath('//xml/header/certsUpdated[1]')
+ if len(certs_updated) == 1:
+ certs_updated = int(certs_updated[0].text)
+ response['certs_updated'] = certs_updated
+
+ certs_to_update = doc.xpath('//xml/header/certsToUpdate[1]')
+ if len(certs_to_update) == 1:
+ certs_to_update = int(certs_to_update[0].text)
+ response['certs_to_update'] = certs_to_update
+
+ error_string = doc.xpath('//xml/header/error[1]')
+ if len(error_string) == 1:
+ error_string = etree.tostring(error_string[0], method='text',
+ encoding=unicode).strip()
+ response['error_string'] = error_string
+
+ revoked = doc.xpath('//xml/header/revoked[1]')
+ if len(revoked) == 1:
+ revoked = etree.tostring(revoked[0], method='text',
+ encoding=unicode).strip()
+ response['revoked'] = revoked
+
+ total_record_count = doc.xpath('//xml/header/totalRecordCount[1]')
+ if len(total_record_count) == 1:
+ total_record_count = int(total_record_count[0].text)
+ response['total_record_count'] = total_record_count
+
+ update_crl = doc.xpath('//xml/header/updateCRL[1]')
+ if len(update_crl) == 1:
+ parse_and_set_boolean_xml(update_crl[0], response, 'update_crl')
+
+ update_crl_success = doc.xpath('//xml/header/updateCRLSuccess[1]')
+ if len(update_crl_success) == 1:
+ parse_and_set_boolean_xml(update_crl_success[0], response, 'update_crl_success')
+
+ update_crl_error = doc.xpath('//xml/header/updateCRLError[1]')
+ if len(update_crl_error) == 1:
+ update_crl_error = etree.tostring(update_crl_error[0], method='text',
+ encoding=unicode).strip()
+ response['update_crl_error'] = update_crl_error
+
+ publish_crl_success = doc.xpath('//xml/header/publishCRLSuccess[1]')
+ if len(publish_crl_success) == 1:
+ parse_and_set_boolean_xml(publish_crl_success[0], response, 'publish_crl_success')
+
+ publish_crl_error = doc.xpath('//xml/header/publishCRLError[1]')
+ if len(publish_crl_error) == 1:
+ publish_crl_error = etree.tostring(publish_crl_error[0], method='text',
+ encoding=unicode).strip()
+ response['publish_crl_error'] = publish_crl_error
+
+ crl_update_status = doc.xpath("//xml/header/*[starts-with(name(), 'crlUpdateStatus')][1]")
+ if len(crl_update_status) == 1:
+ parse_and_set_boolean_xml(crl_update_status[0], response, 'crl_update_status')
+
+ crl_update_error = doc.xpath("//xml/header/*[starts-with(name(), 'crlUpdateError')][1]")
+ if len(crl_update_error) == 1:
+ crl_update_error = etree.tostring(crl_update_error[0], method='text',
+ encoding=unicode).strip()
+ response['crl_update_error'] = crl_update_error
+
+ crl_publish_status = doc.xpath("//xml/header/*[starts-with(name(), 'crlPublishStatus')][1]")
+ if len(crl_publish_status) == 1:
+ parse_and_set_boolean_xml(crl_publish_status[0], response, 'crl_publish_status')
+
+ crl_publish_error = doc.xpath("//xml/header/*[starts-with(name(), 'crlPublishError')][1]")
+ if len(crl_publish_error) == 1:
+ crl_publish_error = etree.tostring(crl_publish_error[0], method='text',
+ encoding=unicode).strip()
+ response['crl_publish_error'] = crl_publish_error
+
+ for record in doc.xpath('//xml/records[*]/record'):
+ response_record = {}
+ records.append(response_record)
+
+ serial_number = record.xpath('serialNumber[1]')
+ if len(serial_number) == 1:
+ serial_number = int(serial_number[0].text, 16) # parse as hex
+ response_record['serial_number'] = serial_number
+
+ error_string = record.xpath('error[1]')
+ if len(error_string) == 1:
+ error_string = etree.tostring(error_string[0], method='text',
+ encoding=unicode).strip()
+ response_record['error_string'] = error_string
+
+ return response
+
+def parse_unrevoke_cert_xml(doc):
+ '''
+ :param doc: The root node of the xml document to parse
+ :returns: result dict
+ :except ValueError:
+
+ After parsing the results are returned in a result dict. The following
+ table illustrates the mapping from the CMS data item to what may be found in
+ the result dict. If a CMS data item is absent it will also be absent in the
+ result dict.
+
+ If the requestStatus is not SUCCESS then the response dict will have the
+ contents described in `parse_error_template_xml`.
+
+ +----------------------+----------------+-----------------------+---------------+
+ |cms name |cms type |result name |result type |
+ +======================+================+=======================+===============+
+ |dirEnabled |string [1]_ |dir_enabled |bool |
+ +----------------------+----------------+-----------------------+---------------+
+ |dirUpdated |string [1]_ |dir_updated |bool |
+ +----------------------+----------------+-----------------------+---------------+
+ |error |string |error_string |unicode |
+ +----------------------+----------------+-----------------------+---------------+
+ |unrevoked |string [3]_ |unrevoked |unicode |
+ +----------------------+----------------+-----------------------+---------------+
+ |updateCRL |string [1]_ [4]_|update_crl |bool |
+ +----------------------+----------------+-----------------------+---------------+
+ |updateCRLSuccess |string [1]_ [4]_|update_crl_success |bool |
+ +----------------------+----------------+-----------------------+---------------+
+ |updateCRLError |string [4]_ |update_crl_error |unicode |
+ +----------------------+----------------+-----------------------+---------------+
+ |publishCRLSuccess |string [1]_ [4]_|publish_crl_success |bool |
+ +----------------------+----------------+-----------------------+---------------+
+ |publishCRLError |string [4]_ |publish_crl_error |unicode |
+ +----------------------+----------------+-----------------------+---------------+
+ |crlUpdateStatus |string [1]_ [5]_|crl_update_status |bool |
+ +----------------------+----------------+-----------------------+---------------+
+ |crlUpdateError |string [5]_ |crl_update_error |unicode |
+ +----------------------+----------------+-----------------------+---------------+
+ |crlPublishStatus |string [1]_ [5]_|crl_publish_status |bool |
+ +----------------------+----------------+-----------------------+---------------+
+ |crlPublishError |string [5]_ |crl_publish_error |unicode |
+ +----------------------+----------------+-----------------------+---------------+
+ |serialNumber |BigInteger |serial_number |int|long |
+ +----------------------+----------------+-----------------------+---------------+
+
+ .. [1] String value is either "yes" or "no"
+ .. [3] unrevoked may be one of:
+
+ - "yes"
+ - "no"
+ - "pending"
+
+ .. [4] Only sent if CRL update information is available.
+ If sent it's only value is "yes".
+ If sent then the following values may also be sent,
+ otherwise they will be absent:
+
+ - updateCRLSuccess
+ - updateCRLError
+ - publishCRLSuccess
+ - publishCRLError
+
+ .. [5] The cms name varies depending on whether the issuing point is MasterCRL
+ or not. If the issuing point is not the MasterCRL then the cms name
+ will be appended with an underscore and the issuing point name.
+ Thus for example the cms name crlUpdateStatus will be crlUpdateStatus
+ if the issuing point is the MasterCRL. However if the issuing point
+ is "foobar" then crlUpdateStatus will be crlUpdateStatus_foobar.
+ When we return the response dict the key will always be the "base"
+ name without the _issuing_point suffix. Thus crlUpdateStatus_foobar
+ will appear in the response dict under the key 'crl_update_status'
+
+ '''
+
+ request_status = get_request_status_xml(doc)
+
+ if request_status != CMS_STATUS_SUCCESS:
+ response = parse_error_template_xml(doc)
+ return response
+
+ response = {}
+ response['request_status'] = request_status
+
+ dir_enabled = doc.xpath('//xml/header/dirEnabled[1]')
+ if len(dir_enabled) == 1:
+ parse_and_set_boolean_xml(dir_enabled[0], response, 'dir_enabled')
+
+ dir_updated = doc.xpath('//xml/header/dirUpdated[1]')
+ if len(dir_updated) == 1:
+ parse_and_set_boolean_xml(dir_updated[0], response, 'dir_updated')
+
+ error_string = doc.xpath('//xml/header/error[1]')
+ if len(error_string) == 1:
+ error_string = etree.tostring(error_string[0], method='text',
+ encoding=unicode).strip()
+ response['error_string'] = error_string
+
+ unrevoked = doc.xpath('//xml/header/unrevoked[1]')
+ if len(unrevoked) == 1:
+ unrevoked = etree.tostring(unrevoked[0], method='text',
+ encoding=unicode).strip()
+ response['unrevoked'] = unrevoked
+
+ update_crl = doc.xpath('//xml/header/updateCRL[1]')
+ if len(update_crl) == 1:
+ parse_and_set_boolean_xml(update_crl[0], response, 'update_crl')
+
+ update_crl_success = doc.xpath('//xml/header/updateCRLSuccess[1]')
+ if len(update_crl_success) == 1:
+ parse_and_set_boolean_xml(update_crl_success[0], response, 'update_crl_success')
+
+ update_crl_error = doc.xpath('//xml/header/updateCRLError[1]')
+ if len(update_crl_error) == 1:
+ update_crl_error = etree.tostring(update_crl_error[0], method='text',
+ encoding=unicode).strip()
+ response['update_crl_error'] = update_crl_error
+
+ publish_crl_success = doc.xpath('//xml/header/publishCRLSuccess[1]')
+ if len(publish_crl_success) == 1:
+ parse_and_set_boolean_xml(publish_crl_success[0], response, 'publish_crl_success')
+
+ publish_crl_error = doc.xpath('//xml/header/publishCRLError[1]')
+ if len(publish_crl_error) == 1:
+ publish_crl_error = etree.tostring(publish_crl_error[0], method='text',
+ encoding=unicode).strip()
+ response['publish_crl_error'] = publish_crl_error
+
+ crl_update_status = doc.xpath("//xml/header/*[starts-with(name(), 'crlUpdateStatus')][1]")
+ if len(crl_update_status) == 1:
+ parse_and_set_boolean_xml(crl_update_status[0], response, 'crl_update_status')
+
+ crl_update_error = doc.xpath("//xml/header/*[starts-with(name(), 'crlUpdateError')][1]")
+ if len(crl_update_error) == 1:
+ crl_update_error = etree.tostring(crl_update_error[0], method='text',
+ encoding=unicode).strip()
+ response['crl_update_error'] = crl_update_error
+
+ crl_publish_status = doc.xpath("//xml/header/*[starts-with(name(), 'crlPublishStatus')][1]")
+ if len(crl_publish_status) == 1:
+ parse_and_set_boolean_xml(crl_publish_status[0], response, 'crl_publish_status')
+
+ crl_publish_error = doc.xpath("//xml/header/*[starts-with(name(), 'crlPublishError')][1]")
+ if len(crl_publish_error) == 1:
+ crl_publish_error = etree.tostring(crl_publish_error[0], method='text',
+ encoding=unicode).strip()
+ response['crl_publish_error'] = crl_publish_error
+
+ serial_number = doc.xpath('//xml/header/serialNumber[1]')
+ if len(serial_number) == 1:
+ serial_number = int(serial_number[0].text, 16) # parse as hex
+ response['serial_number'] = serial_number
+
+ return response
+
+#-------------------------------------------------------------------------------
from ipalib import api, SkipPluginModule
if api.env.ra_plugin != 'dogtag':
@@ -32,18 +1201,11 @@ from httplib import HTTPConnection
from urllib import urlencode
from ipaserver.plugins import rabase
import socket
-from ipalib.errors import NetworkError
+from ipalib.errors import NetworkError, CertificateOperationError
from ipalib.constants import TYPE_ERROR
from ipapython import nsslib
import nss.nss as nss
-import xml.dom.minidom
-
-def get_xml_value(doc, tagname):
- try:
- item_node = doc.getElementsByTagName(tagname)
- return item_node[0].childNodes[0].data
- except IndexError:
- return None
+from ipalib.request import ugettext as _
class ra(rabase.rabase):
"""
@@ -68,257 +1230,452 @@ class ra(rabase.rabase):
self.password = ''
super(ra, self).__init__()
- def _request(self, url, **kw):
+ def _request(self, url, port, **kw):
"""
- Perform an HTTP request.
-
:param url: The URL to post to.
:param kw: Keyword arguments to encode into POST body.
+ :return: (http_status, http_reason_phrase, http_headers, http_body)
+ as (integer, unicode, dict, str)
+
+ Perform an HTTP request.
"""
- uri = 'http://%s:%s%s' % (self.env.ca_host, self.env.ca_port, url)
+ uri = 'http://%s:%s%s' % (self.env.ca_host, port, url)
post = urlencode(kw)
self.info('request %r', uri)
self.debug('request post %r', post)
- conn = HTTPConnection(self.env.ca_host, self.env.ca_port)
+ conn = HTTPConnection(self.env.ca_host, port)
try:
conn.request('POST', url,
body=post,
headers={'Content-type': 'application/x-www-form-urlencoded'},
)
+ res = conn.getresponse()
+
+ http_status = res.status
+ http_reason_phrase = unicode(res.reason, 'utf-8')
+ http_headers = res.msg.dict
+ http_body = res.read()
+ conn.close()
except socket.error, e:
raise NetworkError(uri=uri, error=e.args[1])
- response = conn.getresponse()
- (status, reason) = (response.status, response.reason)
- data = response.read()
- conn.close()
- self.debug('request status %r', status)
- self.debug('request reason %s', reason)
- self.debug('request data %s', data)
- return (status, reason, data)
+
+ self.debug('request status %d', http_status)
+ self.debug('request reason_phrase %r', http_reason_phrase)
+ self.debug('request headers %s', http_headers)
+ self.debug('request body %r', http_body)
+
+ return http_status, http_reason_phrase, http_headers, http_body
def _sslget(self, url, port, **kw):
"""
- Perform an HTTPS request
-
:param url: The URL to post to.
- :param kw: Keyword arguments to encode into POST body.
+ :param kw: Keyword arguments to encode into POST body.
+ :return: (http_status, http_reason_phrase, http_headers, http_body)
+ as (integer, unicode, dict, str)
+
+ Perform an HTTPS request
"""
uri = 'https://%s:%d%s' % (self.env.ca_host, port, url)
post = urlencode(kw)
self.info('sslget %r', uri)
self.debug('sslget post %r', post)
- headers = {"Content-type": "application/x-www-form-urlencoded",
- "Accept": "text/plain"}
- conn = nsslib.NSSConnection(self.env.ca_host, port, dbdir=self.sec_dir)
- conn.sslsock.set_client_auth_data_callback(nsslib.client_auth_data_callback, self.ipa_certificate_nickname, self.password, nss.get_default_certdb())
- conn.set_debuglevel(10)
- conn.request("POST", url, post, headers)
- res = conn.getresponse()
- (status, reason) = (res.status, res.reason)
- data = res.read()
- conn.close()
-
- self.debug('sslget status %r', status)
- self.debug('sslget reason %s', reason)
- self.debug('sslget output %s', data)
- return (status, reason, data)
+ request_headers = {"Content-type": "application/x-www-form-urlencoded",
+ "Accept": "text/plain"}
+ try:
+ conn = nsslib.NSSConnection(self.env.ca_host, port, dbdir=self.sec_dir)
+ conn.sslsock.set_client_auth_data_callback(nsslib.client_auth_data_callback,
+ self.ipa_certificate_nickname,
+ self.password, nss.get_default_certdb())
+ conn.set_debuglevel(10)
+ conn.request("POST", url, post, request_headers)
+
+ res = conn.getresponse()
+
+ http_status = res.status
+ http_reason_phrase = unicode(res.reason, 'utf-8')
+ http_headers = res.msg.dict
+ http_body = res.read()
+ conn.close()
+ except Exception, e:
+ raise NetworkError(uri=uri, error=str(e))
+
+ self.debug('sslget status %d', http_status)
+ self.debug('sslget reason_phrase %r', http_reason_phrase)
+ self.debug('sslget headers %s', http_headers)
+ self.debug('sslget body %r', http_body)
+
+ return http_status, http_reason_phrase, http_headers, http_body
+
+ def get_parse_result_xml(self, xml_text, parse_func):
+ '''
+ :param xml_text: The XML text to parse
+ :param parse_func: The XML parsing function to apply to the parsed DOM tree.
+ :return: parsed result dict
+
+ Utility routine which parses the input text into an XML DOM tree
+ and then invokes the parsing function on the DOM tree in order
+ to get the parsing result as a dict of key/value pairs.
+ '''
+ parser = etree.XMLParser()
+ doc = etree.fromstring(xml_text, parser)
+ result = parse_func(doc)
+ self.debug("%s() xml_text:\n%s\nparse_result:\n%s" % (parse_func.__name__, xml_text, result))
+ return result
def check_request_status(self, request_id):
"""
+ :param request_id: request ID
+
Check status of a certificate signing request.
- :param request_id: request ID
+ The command returns a dict with these possible key/value pairs.
+ Some key/value pairs may be absent.
+
+ +-------------------+---------------+---------------+
+ |result name |result type |comments |
+ +===================+===============+===============+
+ |serial_number |unicode [1]_ | |
+ +-------------------+---------------+---------------+
+ |request_id |unicode | |
+ +-------------------+---------------+---------------+
+ |cert_request_status|unicode [2]_ | |
+ +-------------------+---------------+---------------+
+
+ .. [1] Passed through XMLRPC as decimal string. Can convert to
+ optimal integer type (int or long) via int(serial_number)
+
+ .. [2] cert_request_status may be one of:
+
+ - "begin"
+ - "pending"
+ - "approved"
+ - "svc_pending"
+ - "canceled"
+ - "rejected"
+ - "complete"
+
+
"""
self.debug('%s.check_request_status()', self.fullname)
- (s, r, data) = self._request('/ca/ee/ca/checkRequest',
- requestId=request_id,
- xmlOutput='true',
- )
- response = {'status': '2'}
- if data is not None:
- request_status = self.__find_substring(
- data, 'header.status = "', '"'
- )
- if request_status is not None:
- response['status'] = '0'
- response['request_status'] = request_status
- serial_number = self.__find_substring(
- data, 'record.serialNumber="', '"'
- )
- if serial_number is not None:
- # This was "0x"+serial_number, but we should return it in
- # the same form used as arg to get_certificate(), etc.
- response['serial_number'] = serial_number
- request_id = self.__find_substring(
- data, 'header.requestId = "', '"'
- )
- if request_id is not None:
- response['request_id'] = request_id
- error = self.__find_substring(
- data, 'fixed.unexpectedError = "', '"'
- )
- if error is not None:
- response['error'] = error
- return response
+
+ # Call CMS
+ http_status, http_reason_phrase, http_headers, http_body = \
+ self._request('/ca/ee/ca/checkRequest',
+ self.env.ca_port,
+ requestId=request_id,
+ xml='true')
+
+ # Parse and handle errors
+ if (http_status != 200):
+ raise CertificateOperationError(error=_('Unable to communicate with CMS (%s)') % \
+ http_reason_phrase)
+
+ parse_result = self.get_parse_result_xml(http_body, parse_check_request_result_xml)
+ request_status = parse_result['request_status']
+ if request_status != CMS_STATUS_SUCCESS:
+ raise CertificateOperationError(error='%s (%s)' % \
+ (cms_request_status_to_string(request_status), parse_result.get('error_string')))
+
+ # Return command result
+ cmd_result = {}
+ if parse_result.has_key('serial_numbers') and len(parse_result['serial_numbers']) > 0:
+ # see module documentation concerning serial numbers and XMLRPC
+ cmd_result['serial_number'] = unicode(parse_result['serial_numbers'][0])
+
+ if parse_result.has_key('request_id'):
+ cmd_result['request_id'] = parse_result['request_id']
+
+ if parse_result.has_key('cert_request_status'):
+ cmd_result['cert_request_status'] = parse_result['cert_request_status']
+
+ return cmd_result
def get_certificate(self, serial_number=None):
"""
Retrieve an existing certificate.
- :param serial_number: certificate serial number
+ :param serial_number: Certificate serial number. Must be a string value
+ because serial numbers may be of any magnitue and
+ XMLRPC cannot handle integers larger than 64-bit.
+ The string value should be decimal, but may optionally
+ be prefixed with a hex radix prefix if the integal value
+ is represented as hexadecimal. If no radix prefix is
+ supplied the string will be interpreted as decimal.
+
+ The command returns a dict with these possible key/value pairs.
+ Some key/value pairs may be absent.
+
+ +-----------------+---------------+---------------+
+ |result name |result type |comments |
+ +=================+===============+===============+
+ |certificate |unicode [1]_ | |
+ +-----------------+---------------+---------------+
+ |serial_number |unicode [2]_ | |
+ +-----------------+---------------+---------------+
+ |revocation_reason|int [3]_ | |
+ +-----------------+---------------+---------------+
+
+ .. [1] Base64 encoded
+
+ .. [2] Passed through XMLRPC as decimal string. Can convert to
+ optimal integer type (int or long) via int(serial_number)
+
+ .. [3] revocation reason may be one of:
+
+ - 0 = UNSPECIFIED
+ - 1 = KEY_COMPROMISE
+ - 2 = CA_COMPROMISE
+ - 3 = AFFILIATION_CHANGED
+ - 4 = SUPERSEDED
+ - 5 = CESSATION_OF_OPERATION
+ - 6 = CERTIFICATE_HOLD
+ - 8 = REMOVE_FROM_CRL
+ - 9 = PRIVILEGE_WITHDRAWN
+ - 10 = AA_COMPROMISE
+
+
"""
self.debug('%s.get_certificate()', self.fullname)
- issued_certificate = None
- (status, reason, stdout) = self._sslget(
- '/ca/agent/ca/displayBySerial',
- self.env.ca_agent_port,
- serialNumber=serial_number,
- xmlOutput='true',
- )
- response = {}
- if (status == 200):
- issued_certificate = self.__find_substring(
- stdout, 'header.certChainBase64 = "', '"'
- )
- if issued_certificate is not None:
- response['status'] = '0'
- issued_certificate = issued_certificate.replace('\\r', '')
- issued_certificate = issued_certificate.replace('\\n', '')
- self.debug('IPA-RA: issued_certificate: %s', issued_certificate)
- response['certificate'] = issued_certificate
- else:
- response['status'] = '1'
- revocation_reason = self.__find_substring(
- stdout, 'header.revocationReason = ', ';'
- )
- if revocation_reason is not None:
- response['revocation_reason'] = revocation_reason
- else:
- response['status'] = str(status)
- return response
+
+ # Convert serial number to integral type from string to properly handle
+ # radix issues. Note: the int object constructor will properly handle large
+ # magnitude integral values by returning a Python long type when necessary.
+ serial_number = int(serial_number, 0)
+
+ # Call CMS
+ http_status, http_reason_phrase, http_headers, http_body = \
+ self._sslget('/ca/agent/ca/displayBySerial',
+ self.env.ca_agent_port,
+ serialNumber=str(serial_number),
+ xml='true')
+
+
+ # Parse and handle errors
+ if (http_status != 200):
+ raise CertificateOperationError(error=_('Unable to communicate with CMS (%s)') % \
+ http_reason_phrase)
+
+ parse_result = self.get_parse_result_xml(http_body, parse_display_cert_xml)
+ request_status = parse_result['request_status']
+ if request_status != CMS_STATUS_SUCCESS:
+ raise CertificateOperationError(error='%s (%s)' % \
+ (cms_request_status_to_string(request_status), parse_result.get('error_string')))
+
+ # Return command result
+ cmd_result = {}
+
+ if parse_result.has_key('certificate'):
+ cmd_result['certificate'] = parse_result['certificate']
+
+ if parse_result.has_key('serial_number'):
+ # see module documentation concerning serial numbers and XMLRPC
+ cmd_result['serial_number'] = unicode(parse_result['serial_number'])
+
+ if parse_result.has_key('revocation_reason'):
+ cmd_result['revocation_reason'] = parse_result['revocation_reason']
+
+ return cmd_result
+
def request_certificate(self, csr, request_type='pkcs10'):
"""
- Submit certificate signing request.
-
:param csr: The certificate signing request.
:param request_type: The request type (defaults to ``'pkcs10'``).
+
+ Submit certificate signing request.
+
+ The command returns a dict with these possible key/value pairs.
+ Some key/value pairs may be absent.
+
+ +---------------+---------------+---------------+
+ |result name |result type |comments |
+ +===============+===============+===============+
+ |serial_number |unicode [1]_ | |
+ +---------------+---------------+---------------+
+ |certificate |unicode [2]_ | |
+ +---------------+---------------+---------------+
+ |request_id |unicode | |
+ +---------------+---------------+---------------+
+ |subject |unicode | |
+ +---------------+---------------+---------------+
+
+ .. [1] Passed through XMLRPC as decimal string. Can convert to
+ optimal integer type (int or long) via int(serial_number)
+
+ .. [2] Base64 encoded
+
"""
self.debug('%s.request_certificate()', self.fullname)
- certificate = None
- (status, reason, stdout) = self._sslget('/ca/ee/ca/profileSubmit',
- self.env.ca_ee_port,
- profileId='caRAserverCert',
- cert_request_type=request_type,
- cert_request=csr,
- xmlOutput='true',
- )
- response = {}
- if (status == 200):
- doc = xml.dom.minidom.parseString(stdout)
-
- status = get_xml_value(doc, "Status")
- if status is not None:
- response["status"] = status
- request_id = get_xml_value(doc, "Id")
- if request_id is not None:
- response["request_id"] = request_id
- serial_number = get_xml_value(doc, "serialno")
- if serial_number is not None:
- response["serial_number"] = ("0x%s" % serial_number)
- subject = get_xml_value(doc, "SubjectDN")
- if subject is not None:
- response["subject"] = subject
- certificate = get_xml_value(doc, "b64")
- if certificate is not None:
- response["certificate"] = certificate
- if response.has_key("status") is False:
- response["status"] = "2"
-
- doc.unlink()
- else:
- response["status"] = str(status)
- return response
- def revoke_certificate(self, serial_number, revocation_reason=0):
- """
- Revoke a certificate.
+ # Call CMS
+ http_status, http_reason_phrase, http_headers, http_body = \
+ self._sslget('/ca/ee/ca/profileSubmit',
+ self.env.ca_ee_port,
+ profileId='caRAserverCert',
+ cert_request_type=request_type,
+ cert_request=csr,
+ xml='true')
+ # Parse and handle errors
+ if (http_status != 200):
+ raise CertificateOperationError(error=_('Unable to communicate with CMS (%s)') % \
+ http_reason_phrase)
+
+ parse_result = self.get_parse_result_xml(http_body, parse_profile_submit_result_xml)
+ # Note different status return, it's not request_status, it's error_code
+ error_code = parse_result['error_code']
+ if error_code != CMS_SUCCESS:
+ raise CertificateOperationError(error='%s (%s)' % \
+ (cms_error_code_to_string(error_code), parse_result.get('error_string')))
- The integer ``revocation_reason`` code must have one of these values:
+ # Return command result
+ cmd_result = {}
- * ``0`` - unspecified
- * ``1`` - keyCompromise
- * ``2`` - cACompromise
- * ``3`` - affiliationChanged
- * ``4`` - superseded
- * ``5`` - cessationOfOperation
- * ``6`` - certificateHold
- * ``8`` - removeFromCRL
- * ``9`` - privilegeWithdrawn
- * ``10`` - aACompromise
+ # FIXME: should we return all the requests instead of just the first one?
+ if len(parse_result['requests']) < 1:
+ return cmd_result
+ request = parse_result['requests'][0]
- Note that reason code ``7`` is not used. See RFC 5280 for more details:
+ if request.has_key('serial_number'):
+ # see module documentation concerning serial numbers and XMLRPC
+ cmd_result['serial_number'] = unicode(request['serial_number'])
- http://www.ietf.org/rfc/rfc5280.txt
+ if request.has_key('certificate'):
+ cmd_result['certificate'] = request['certificate']
- :param serial_number: Certificate serial number.
+ if request.has_key('request_id'):
+ cmd_result['request_id'] = request['request_id']
+
+ if request.has_key('subject'):
+ cmd_result['subject'] = request['subject']
+
+ return cmd_result
+
+
+ def revoke_certificate(self, serial_number, revocation_reason=0):
+ """
+ :param serial_number: Certificate serial number. Must be a string value
+ because serial numbers may be of any magnitue and
+ XMLRPC cannot handle integers larger than 64-bit.
+ The string value should be decimal, but may optionally
+ be prefixed with a hex radix prefix if the integal value
+ is represented as hexadecimal. If no radix prefix is
+ supplied the string will be interpreted as decimal.
:param revocation_reason: Integer code of revocation reason.
+
+ Revoke a certificate.
+
+ The command returns a dict with these possible key/value pairs.
+ Some key/value pairs may be absent.
+
+ +---------------+---------------+---------------+
+ |result name |result type |comments |
+ +===============+===============+===============+
+ |revoked |bool | |
+ +---------------+---------------+---------------+
+
"""
self.debug('%s.revoke_certificate()', self.fullname)
if type(revocation_reason) is not int:
raise TYPE_ERROR('revocation_reason', int, revocation_reason,
type(revocation_reason)
)
- response = {}
- (status, reason, stdout) = self._sslget('/ca/agent/ca/doRevoke',
- self.env.ca_agent_port,
- op='revoke',
- revocationReason=revocation_reason,
- revokeAll='(certRecordId=%s)' % serial_number,
- totalRecordCount=1,
- )
- if status == 200:
- response['status'] = '0'
- if (stdout.find('revoked = "yes"') > -1):
- response['revoked'] = True
- else:
- response['revoked'] = False
+
+ # Convert serial number to integral type from string to properly handle
+ # radix issues. Note: the int object constructor will properly handle large
+ # magnitude integral values by returning a Python long type when necessary.
+ serial_number = int(serial_number, 0)
+
+ # Call CMS
+ http_status, http_reason_phrase, http_headers, http_body = \
+ self._sslget('/ca/agent/ca/doRevoke',
+ self.env.ca_agent_port,
+ op='revoke',
+ revocationReason=revocation_reason,
+ revokeAll='(certRecordId=%s)' % str(serial_number),
+ totalRecordCount=1,
+ xml='true')
+
+ # Parse and handle errors
+ if (http_status != 200):
+ raise CertificateOperationError(error=_('Unable to communicate with CMS (%s)') % \
+ http_reason_phrase)
+
+ parse_result = self.get_parse_result_xml(http_body, parse_revoke_cert_xml)
+ request_status = parse_result['request_status']
+ if request_status != CMS_STATUS_SUCCESS:
+ raise CertificateOperationError(error='%s (%s)' % \
+ (cms_request_status_to_string(request_status), parse_result.get('error_string')))
+
+ # Return command result
+ cmd_result = {}
+
+ if parse_result.get('revoked') == 'yes':
+ cmd_result['revoked'] = True
else:
- response['status'] = str(status)
- return response
+ cmd_result['revoked'] = False
+
+ return cmd_result
def take_certificate_off_hold(self, serial_number):
"""
+ :param serial_number: Certificate serial number. Must be a string value
+ because serial numbers may be of any magnitue and
+ XMLRPC cannot handle integers larger than 64-bit.
+ The string value should be decimal, but may optionally
+ be prefixed with a hex radix prefix if the integal value
+ is represented as hexadecimal. If no radix prefix is
+ supplied the string will be interpreted as decimal.
+
Take revoked certificate off hold.
- :param serial_number: Certificate serial number.
+ The command returns a dict with these possible key/value pairs.
+ Some key/value pairs may be absent.
+
+ +---------------+---------------+---------------+
+ |result name |result type |comments |
+ +===============+===============+===============+
+ |unrevoked |bool | |
+ +---------------+---------------+---------------+
+ |error_string |unicode | |
+ +---------------+---------------+---------------+
"""
- response = {}
+
self.debug('%s.take_certificate_off_hold()', self.fullname)
- (status, reason, stdout) = self._sslget('/ca/agent/ca/doUnrevoke',
- self.env.ca_agent_port,
- serialNumber=serial_number,
- )
- if (status == 0):
- if (stdout.find('unrevoked = "yes"') > -1):
- response['taken_off_hold'] = True
- else:
- response['taken_off_hold'] = False
+
+ # Convert serial number to integral type from string to properly handle
+ # radix issues. Note: the int object constructor will properly handle large
+ # magnitude integral values by returning a Python long type when necessary.
+ serial_number = int(serial_number, 0)
+
+ # Call CMS
+ http_status, http_reason_phrase, http_headers, http_body = \
+ self._sslget('/ca/agent/ca/doUnrevoke',
+ self.env.ca_agent_port,
+ serialNumber=str(serial_number),
+ xml='true')
+
+ # Parse and handle errors
+ if (http_status != 200):
+ raise CertificateOperationError(error=_('Unable to communicate with CMS (%s)') % \
+ http_reason_phrase)
+
+ parse_result = self.get_parse_result_xml(http_body, parse_unrevoke_cert_xml)
+ request_status = parse_result['request_status']
+ if request_status != CMS_STATUS_SUCCESS:
+ raise CertificateOperationError(error='%s (%s)' % \
+ (cms_request_status_to_string(request_status), parse_result.get('error_string')))
+
+ # Return command result
+ cmd_result = {}
+
+ if parse_result.has_key('error_string'):
+ cmd_result['error_string'] = parse_result['error_string']
+
+ if parse_result.get('unrevoked') == 'yes':
+ cmd_result['unrevoked'] = True
else:
- response['status'] = str(status)
- return response
+ cmd_result['unrevoked'] = False
- def __find_substring(self, str, str1, str2):
- sub_str = None
- k0 = len(str)
- k1 = str.find(str1)
- k2 = len(str1)
- if (k0 > 0 and k1 > -1 and k2 > 0 and k0 > k1 + k2):
- sub_str = str[k1+k2:]
- k3 = len(sub_str)
- k4 = sub_str.find(str2)
- if (k3 > 0 and k4 > -1 and k3 > k4):
- sub_str = sub_str[:k4]
- return sub_str
+ return cmd_result
api.register(ra)
diff --git a/ipaserver/plugins/selfsign.py b/ipaserver/plugins/selfsign.py
index d4b2efcf..7d5dafa7 100644
--- a/ipaserver/plugins/selfsign.py
+++ b/ipaserver/plugins/selfsign.py
@@ -1,5 +1,6 @@
# Authors:
# Rob Crittenden <rcritten@@redhat.com>
+# John Dennis <jdennis@redhat.com>
#
# Copyright (C) 2009 Red Hat
# see file 'COPYING' for use and warranty information
@@ -43,6 +44,7 @@ from ipaserver.plugins import rabase
from ipaserver.install import certs
import tempfile
from pyasn1 import error
+from ipalib.request import ugettext as _
class ra(rabase.rabase):
"""
@@ -51,13 +53,32 @@ class ra(rabase.rabase):
def request_certificate(self, csr, request_type='pkcs10'):
"""
- Submit certificate signing request.
-
:param csr: The certificate signing request.
:param request_type: The request type (defaults to ``'pkcs10'``).
- """
- (csr_fd, csr_name) = tempfile.mkstemp()
+ Submit certificate signing request.
+
+ The command returns a dict with these possible key/value pairs.
+ Some key/value pairs may be absent.
+
+ +---------------+---------------+---------------+
+ |result name |result type |comments |
+ +===============+===============+===============+
+ |serial_number |unicode [1]_ | |
+ +---------------+---------------+---------------+
+ |certificate |unicode [2]_ | |
+ +---------------+---------------+---------------+
+ |request_id |unicode | |
+ +---------------+---------------+---------------+
+ |subject |unicode | |
+ +---------------+---------------+---------------+
+
+ .. [1] Passed through XMLRPC as decimal string. Can convert to
+ optimal integer type (int or long) via int(serial_number)
+
+ .. [2] Base64 encoded
+
+ """
# certutil wants the CSR to have have a header and footer. Add one
# if it isn't there.
s = csr.find('-----BEGIN NEW CERTIFICATE REQUEST-----')
@@ -66,12 +87,47 @@ class ra(rabase.rabase):
if s == -1:
csr = '-----BEGIN NEW CERTIFICATE REQUEST-----\n' + csr + \
'-----END NEW CERTIFICATE REQUEST-----\n'
- os.write(csr_fd, csr)
- os.close(csr_fd)
- (cert_fd, cert_name) = tempfile.mkstemp()
- os.close(cert_fd)
- serialno = certs.next_serial(self.serial_file)
+ try:
+ (csr_fd, csr_name) = tempfile.mkstemp()
+ os.write(csr_fd, csr)
+ os.close(csr_fd)
+ except Exception, e:
+ try:
+ os.remove(csr_name)
+ except:
+ pass
+ self.log.error('unable to create temporary csr file: %s' % e)
+ raise errors.CertificateOperationError(error=_('file operation'))
+
+ try:
+ (cert_fd, cert_name) = tempfile.mkstemp()
+ os.close(cert_fd)
+ except Exception, e:
+ try:
+ os.remove(csr_name)
+ except:
+ pass
+ try:
+ os.remove(cert_name)
+ except:
+ pass
+ self.log.error('unable to create temporary certificate file: %s' % e)
+ raise errors.CertificateOperationError(error=_('file operation'))
+
+ try:
+ serialno = certs.next_serial(self.serial_file)
+ except Exception, e:
+ try:
+ os.remove(csr_name)
+ except:
+ pass
+ try:
+ os.remove(cert_name)
+ except:
+ pass
+ self.log.error('next_serial() failed: %s' % e)
+ raise errors.CertificateOperationError(error=_('cannot obtain next serial number'))
try:
args = [
@@ -97,17 +153,31 @@ class ra(rabase.rabase):
p.stdin.write("0\n9\nn\n")
p.stdin.write("1\n9\nn\n")
(stdout, stderr) = p.communicate()
+ status = p.returncode
self.log.debug("stdout = %s" % stdout)
self.log.debug("stderr = %s" % stderr)
+ if status != 0:
+ try:
+ os.remove(cert_name)
+ except:
+ pass
+ self.log.error('certutil failed: %s' % stderr)
+ raise errors.CertificateOperationError(error=_('certutil failure'))
finally:
- os.remove(csr_name)
+ try:
+ os.remove(csr_name)
+ except:
+ pass
try:
cert_fd = open(cert_name)
cert = cert_fd.read()
cert_fd.close()
finally:
- os.remove(cert_name)
+ try:
+ os.remove(cert_name)
+ except:
+ pass
try:
# Grab the subject, reverse it, combine it and return it
@@ -120,7 +190,8 @@ class ra(rabase.rabase):
serial = x509.get_serial_number(cert)
except error.PyAsn1Error, e:
- raise errors.GenericError(format='Unable to decode certificate in entry: %s' % str(e))
+ self.log.error('Unable to decode certificate in entry: %s' % str(e))
+ raise errors.CertificateOperationError(error='Unable to decode certificate in entry: %s' % str(e))
# To make it look like dogtag return just the base64 data.
cert = cert.replace('\n','')
@@ -130,6 +201,11 @@ class ra(rabase.rabase):
s = s + 27
cert = cert[s:e]
- return {'status':0, 'subject': subject, 'certificate':cert, 'serial_number': "0x%x" % serial}
+ cmd_result = {}
+ cmd_result['serial_number'] = unicode(serial) # convert long to decimal unicode string
+ cmd_result['certificate'] = unicode(cert)
+ cmd_result['subject'] = unicode(subject)
+
+ return cmd_result
api.register(ra)