Module dogtag
[hide private]
[frames] | no frames]

Source Code for Module dogtag

   1  # Authors: 
   2  #   Andrew Wnuk <awnuk@redhat.com> 
   3  #   Jason Gerard DeRose <jderose@redhat.com> 
   4  #   Rob Crittenden <rcritten@@redhat.com> 
   5  #   John Dennis <jdennis@redhat.com> 
   6  # 
   7  # Copyright (C) 2009  Red Hat 
   8  # see file 'COPYING' for use and warranty information 
   9  # 
  10  # This program is free software; you can redistribute it and/or modify 
  11  # it under the terms of the GNU General Public License as published by 
  12  # the Free Software Foundation, either version 3 of the License, or 
  13  # (at your option) any later version. 
  14  # 
  15  # This program is distributed in the hope that it will be useful, 
  16  # but WITHOUT ANY WARRANTY; without even the implied warranty of 
  17  # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the 
  18  # GNU General Public License for more details. 
  19  # 
  20  # You should have received a copy of the GNU General Public License 
  21  # along with this program.  If not, see <http://www.gnu.org/licenses/>. 
  22   
  23  ''' 
  24   
  25  ============================================== 
  26  Backend plugin for RA using Dogtag (e.g. CMS) 
  27  ============================================== 
  28   
  29  Overview of interacting with CMS: 
  30  --------------------------------- 
  31   
  32  CMS stands for "Certificate Management System". It has been released under a 
  33  variety of names, the open source version is called "dogtag". 
  34   
  35  CMS consists of a number of servlets which in rough terms can be thought of as 
  36  RPC commands. A servlet is invoked by making an HTTP request to a specific URL 
  37  and passing URL arguments. Normally CMS responds with an HTTP reponse consisting 
  38  of HTML to be rendered by a web browser. This HTTP HTML response has both 
  39  Javascript SCRIPT components and HTML rendering code. One of the Javascript 
  40  SCRIPT blocks holds the data for the result. The rest of the response is derived 
  41  from templates associated with the servlet which may be customized. The 
  42  templates pull the result data from Javascript variables. 
  43   
  44  One way to get the result data is to parse the HTML looking for the Javascript 
  45  varible initializations. Simple string searchs are not a robust method. First of 
  46  all one must be sure the string is only found in a Javascript SCRIPT block and 
  47  not somewhere else in the HTML document. Some of the Javascript variable 
  48  initializations are rather complex (e.g. lists of structures). It would be hard 
  49  to correctly parse such complex and diverse Javascript. Existing Javascript 
  50  parsers are not generally available. Finally, it's important to know the 
  51  character encoding for strings. There is a somewhat complex set of precident 
  52  rules for determining the current character encoding from the HTTP header, 
  53  meta-equiv tags, mime Content-Type and charset attributes on HTML elements. All 
  54  of this means trying to read the result data from a CMS HTML response is 
  55  difficult to do robustly. 
  56   
  57  However, CMS also supports returning the result data as a XML document 
  58  (distinct from an XHTML document which would be essentially the same as 
  59  described above). There are a wide variety of tools to robustly parse 
  60  XML. Because XML is so well defined things like escapes, character encodings, 
  61  etc. are automatically handled by the tools. 
  62   
  63  Thus we never try to parse Javascript, instead we always ask CMS to return us an 
  64  XML document by passing the URL argument xml="true". The body of the HTTP 
  65  response is an XML document rather than HTML with embedded Javascript. 
  66   
  67  To parse the XML documents we use the Python lxml package which is a Python 
  68  binding around the libxml2 implementation. libxml2 is a very fast, standard 
  69  compliant, feature full XML implementation. libxml2 is the XML library of choice 
  70  for many projects. One of the features in lxml and libxml2 that is particularly 
  71  valuable to us is the XPath implementation. We make heavy use of XPath to find 
  72  data in the XML documents we're parsing. 
  73   
  74  Parse Results vs. IPA command results: 
  75  -------------------------------------- 
  76   
  77  CMS results can be parsed from either HTML or XML. CMS unfortunately is not 
  78  consistent with how it names items or how it utilizes data types. IPA has strict 
  79  rules about data types. Also IPA would like to see a more consistent view CMS 
  80  data. Therefore we split the task of parsing CMS results out from the IPA 
  81  command code. The parse functions normalize the result data by using a 
  82  consistent set of names and data types. The IPA command only deals with the 
  83  normalized parse results. This also allow us to use different parsers if need be 
  84  (i.e. if we had to parse Javascript for some reason). The parse functions 
  85  attempt to parse as must information from the CMS result as is possible. It puts 
  86  the parse result into a dict whose normalized key/value pairs are easy to 
  87  access. IPA commands do not need to return all the parsed results, it can pick 
  88  and choose what it wants to return in the IPA command result from the parse 
  89  result. It also rest assured the values in the parse result will be the correct 
  90  data type. Thus the general sequence of steps for an IPA command talking to CMS 
  91  are: 
  92   
  93  #. Receive IPA arguments from IPA command 
  94  #. Formulate URL with arguments for CMS 
  95  #. Make request to CMS server 
  96  #. Extract XML document from HTML body returned by CMS 
  97  #. Parse XML document using matching parse routine which returns response dict 
  98  #. Extract relevant items from parse result and insert into command result 
  99  #. Return command result 
 100   
 101  Serial Numbers: 
 102  --------------- 
 103   
 104  Serial numbers are integral values of any magnitude because they are based on 
 105  ASN.1 integers. CMS uses the Java BigInteger to represent these. Fortunately 
 106  Python also has support for big integers via the Python long() object. Any 
 107  BigIntegers we receive from CMS as a string can be parsed into a Python long 
 108  without loss of information. 
 109   
 110  However Python has a neat trick. It normally represents integers via the int 
 111  object which internally uses the native C long type. If you create an int 
 112  object by passing the int constructor a string it will check the magnitude of 
 113  the value. If it would fit in a C long then it returns you an int 
 114  object. However if the value is too big for a C long type then it returns you 
 115  a Python long object instead. This is a very nice property because it's much 
 116  more efficient to use C long types when possible (e.g. Python int), but when 
 117  necessary you'll get a Python long() object to handle large magnitude 
 118  values. Python also nicely handles type promotion transparently between int 
 119  and long objects. For example if you multiply two int objects you may get back 
 120  a long object if necessary. In general Python int and long objects may be 
 121  freely mixed without the programmer needing to be aware of which type of 
 122  intergral object is being operated on. 
 123   
 124  The leads to the following rule, always parse a string representing an 
 125  integral value using the int() constructor even if it might have large 
 126  magnitude because Python will return either an int or a long automatically. By 
 127  the same token don't test for type of an object being int exclusively because 
 128  it could either be an int or a long object. 
 129   
 130  Internally we should always being using int or long object to hold integral 
 131  values. This is because we should be able to compare them correctly, be free 
 132  from concerns about having the know the radix of the string, perform 
 133  arithmetic operations, and convert to string representation (with correct 
 134  radix) when necessary. In other words internally we should never handle 
 135  integral values as strings. 
 136   
 137  However, the XMLRPC transport cannot properly handle a Python long object. The 
 138  XMLRPC encoder upon seeing a Python long will test to see if the value fits 
 139  within the range of an 32-bit integer, if so it passes the integer parameter 
 140  otherwise it raises an Overflow exception. The XMLRPC specification does 
 141  permit 64-bit integers (e.g. i8) and the Python XMLRPC module could allow long 
 142  values within the 64-bit range to be passed if it were patched, however this 
 143  only moves the problem, it does not solve passing big integers through 
 144  XMLRPC. Thus we must always pass big integers as a strings through the XMLRPC 
 145  interface. But upon receiving that value from XMLRPC we should convert it back 
 146  into an int or long object. Recall also that Python will automatically perform 
 147  a conversion to string if you output the int or long object in a string context. 
 148   
 149  Radix Issues: 
 150  ------------- 
 151   
 152  CMS uses the following conventions: Serial numbers are always returned as 
 153  hexadecimal strings without a radix prefix. When CMS takes a serial number as 
 154  input it accepts the value in either decimal or hexadecimal utilizing the radix 
 155  prefix (e.g. 0x) to determine how to parse the value. 
 156   
 157  IPA has adopted the convention that all integral values in the user interface 
 158  will use base 10 decimal radix. 
 159   
 160  Basic rules on handling these values 
 161   
 162  1. Reading a serial number from CMS requires conversion from hexadecimal 
 163     by converting it into a Python int or long object, use the int constructor: 
 164   
 165     >>> serial_number = int(serial_number, 16) 
 166   
 167  2. Big integers passed to XMLRPC must be decimal unicode strings 
 168   
 169     >>> unicode(serial_number) 
 170   
 171  3. Big integers received from XMLRPC must be converted back to int or long 
 172     objects from the decimal string representation. 
 173   
 174     >>> serial_number = int(serial_number) 
 175   
 176  Xpath pattern matching on node names: 
 177  ------------------------------------- 
 178   
 179  There are many excellent tutorial on how to use xpath to find items in an XML 
 180  document, as such there is no need to repeat this information here. However, 
 181  most xpath tutorials make the assumption the node names you're searching for are 
 182  fixed. For example: 
 183   
 184      doc.xpath('//book/chapter[*]/section[2]') 
 185   
 186  Selects the second section of every chapter of the book. In this example the 
 187  node names 'book', 'chapter', 'section' are fixed. But what if the XML document 
 188  embedded the chapter number in the node name, for example 'chapter1', 
 189  'chapter2', etc.? (If you're thinking this would be incredibly lame, you're 
 190  right, but sadly people do things like this). Thus in this case you can't use 
 191  the node name 'chapter' in the xpath location step because it's not fixed and 
 192  hence won't match 'chapter1', 'chapter2', etc. The solution to this seems 
 193  obvious, use some type of pattern matching on the node name. Unfortunately this 
 194  advanced use of xpath is seldom discussed in tutorials and it's not obvious how 
 195  to do it. Here are some hints. 
 196   
 197  Use the built-in xpath string functions. Most of the examples illustrate the 
 198  string function being passed the text *contents* of the node via '.' or 
 199  string(.). However we don't want to pass the contents of the node, instead we 
 200  want to pass the node name. To do this use the name() function. One way we could 
 201  solve the chapter problem above is by using a predicate which says if the node 
 202  name begins with 'chapter' it's a match. Here is how you can do that. 
 203   
 204      >>> doc.xpath("//book/*[starts-with(name(), 'chapter')]/section[2]") 
 205   
 206  The built-in starts-with() returns true if it's first argument starts with it's 
 207  second argument. Thus the example above says if the node name of the second 
 208  location step begins with 'chapter' consider it a match and the search 
 209  proceeds to the next location step, which in this example is any node named 
 210  'section'. 
 211   
 212  But what if we would like to utilize the power of regular expressions to perform 
 213  the test against the node name? In this case we can use the EXSLT regular 
 214  expression extension. EXSLT extensions are accessed by using XML 
 215  namespaces. The regular expression name space identifier is 're:' In lxml we 
 216  need to pass a set of namespaces to XPath object constructor in order to allow 
 217  it to bind to those namespaces during it's evaluation. Then we just use the 
 218  EXSLT regular expression match() function on the node name. Here is how this is 
 219  done: 
 220   
 221      >>> regexpNS = "http://exslt.org/regular-expressions" 
 222      >>> find = etree.XPath("//book/*[re:match(name(), '^chapter(_\d+)$')]/section[2]", 
 223      ...                    namespaces={'re':regexpNS} 
 224      >>> find(doc) 
 225   
 226  What is happening here is that etree.XPath() has returned us an evaluator 
 227  function which we bind to the name 'find'. We've passed it a set of namespaces 
 228  as a dict via the 'namespaces' keyword parameter of etree.XPath(). The predicate 
 229  for the second location step uses the 're:' namespace to find the function name 
 230  'match'. The re:match() takes a string to search as it's first argument and a 
 231  regular expression pattern as it's second argument. In this example the string 
 232  to seach is the node name of the location step because we called the built-in 
 233  node() function of XPath. The regular expression pattern we've passed says it's 
 234  a match if the string begins with 'chapter' is followed by any number of 
 235  digits and nothing else follows. 
 236   
 237  ''' 
 238   
 239  from lxml import etree 
 240  import datetime 
 241   
 242  # These are general status return values used when 
 243  # CMSServlet.outputError() is invoked. 
 244  CMS_SUCCESS      = 0 
 245  CMS_FAILURE      = 1 
 246  CMS_AUTH_FAILURE = 2 
 247   
 248  # CMS (Certificate Management System) status return values 
 249  # These are requestStatus return values used with templates 
 250  CMS_STATUS_UNAUTHORIZED = 1 
 251  CMS_STATUS_SUCCESS      = 2 
 252  CMS_STATUS_PENDING      = 3 
 253  CMS_STATUS_SVC_PENDING  = 4 
 254  CMS_STATUS_REJECTED     = 5 
 255  CMS_STATUS_ERROR        = 6 
 256  CMS_STATUS_EXCEPTION    = 7 
 257   
258 -def cms_request_status_to_string(request_status):
259 ''' 260 :param request_status: The integral request status value 261 :return: String name of request status 262 ''' 263 return { 264 1 : 'UNAUTHORIZED', 265 2 : 'SUCCESS', 266 3 : 'PENDING', 267 4 : 'SVC_PENDING', 268 5 : 'REJECTED', 269 6 : 'ERROR', 270 7 : 'EXCEPTION', 271 }.get(request_status, "unknown(%d)" % request_status)
272
273 -def cms_error_code_to_string(error_code):
274 ''' 275 :param error_code: The integral error code value 276 :return: String name of the error code 277 ''' 278 return { 279 0 : 'SUCCESS', 280 1 : 'FAILURE', 281 2 : 'AUTH_FAILURE', 282 }.get(error_code, "unknown(%d)" % error_code)
283
284 -def parse_and_set_boolean_xml(node, response, response_name):
285 ''' 286 :param node: xml node object containing value to parse for boolean result 287 :param response: response dict to set boolean result in 288 :param response_name: name of the respone value to set 289 :except ValueError: 290 291 Read the value out of a xml text node and interpret it as a boolean value. 292 The text values are stripped of whitespace and converted to lower case 293 prior to interpretation. 294 295 If the value is recognized the response dict is updated using the 296 request_name as the key and the value is set to the bool value of either 297 True or False depending on the interpretation of the text value. If the text 298 value is not recognized a ValueError exception is thrown. 299 300 Text values which result in True: 301 302 - true 303 - yes 304 - on 305 306 Text values which result in False: 307 308 - false 309 - no 310 - off 311 ''' 312 value = node.text.strip().lower() 313 if value == 'true' or value == 'yes': 314 value = True 315 elif value == 'false' or value == 'no': 316 value = False 317 else: 318 raise ValueError('expected true|false|yes|no|on|off for "%s", but got "%s"' % \ 319 (response_name, value)) 320 response[response_name] = value
321
322 -def get_error_code_xml(doc):
323 ''' 324 :param doc: The root node of the xml document to parse 325 :returns: error code as an integer or None if not found 326 327 Returns the error code when the servlet replied with 328 CMSServlet.outputError() 329 330 The possible error code values are: 331 332 - CMS_SUCCESS = 0 333 - CMS_FAILURE = 1 334 - CMS_AUTH_FAILURE = 2 335 336 However, profileSubmit sometimes also returns these values: 337 338 - EXCEPTION = 1 339 - DEFERRED = 2 340 - REJECTED = 3 341 342 ''' 343 344 error_code = doc.xpath('//XMLResponse/Status[1]') 345 if len(error_code) == 1: 346 error_code = int(error_code[0].text) 347 else: 348 # If error code wasn't present, but error string was 349 # then it's an error. 350 error_string = doc.xpath('//XMLResponse/Error[1]') 351 if len(error_string) == 1: 352 error_code = CMS_FAILURE 353 else: 354 # no status and no error string, assume success 355 error_code = CMS_SUCCESS 356 357 return error_code
358
359 -def get_request_status_xml(doc):
360 ''' 361 :param doc: The root node of the xml document to parse 362 :returns: request status as an integer 363 364 Returns the request status from a CMS operation. May be one of: 365 366 - CMS_STATUS_UNAUTHORIZED = 1 367 - CMS_STATUS_SUCCESS = 2 368 - CMS_STATUS_PENDING = 3 369 - CMS_STATUS_SVC_PENDING = 4 370 - CMS_STATUS_REJECTED = 5 371 - CMS_STATUS_ERROR = 6 372 - CMS_STATUS_EXCEPTION = 7 373 374 CMS will often fail to return requestStatus when the status is 375 SUCCESS. Therefore if we fail to find a requestStatus field we default the 376 result to CMS_STATUS_SUCCESS. 377 ''' 378 379 request_status = doc.xpath('//xml/fixed/requestStatus[1]') 380 if len(request_status) == 1: 381 request_status = int(request_status[0].text) 382 else: 383 # When a request is successful CMS often omits the requestStatus 384 request_status = CMS_STATUS_SUCCESS 385 386 # However, if an error string was returned it's an error no 387 # matter what CMS returned as requestStatus. 388 # Just to make life interesting CMS sometimes returns an empty error string 389 # when nothing wrong occurred. 390 error_detail = doc.xpath('//xml/fixed/errorDetails[1]') 391 if len(error_detail) == 1 and len(error_detail[0].text.strip()) > 0: 392 # There was a non-empty error string, if the status was something 393 # other than error or exception then force it to be an error. 394 if not (request_status in (CMS_STATUS_ERROR, CMS_STATUS_EXCEPTION)): 395 request_status = CMS_STATUS_ERROR 396 397 return request_status
398 399
400 -def parse_error_template_xml(doc):
401 ''' 402 :param doc: The root node of the xml document to parse 403 :returns: result dict 404 405 CMS currently returns errors via XML as either a "template" document 406 (generated by CMSServlet.outputXML() or a "response" document (generated by 407 CMSServlet.outputError()). 408 409 This routine is used to parse a "template" style error or exception 410 document. 411 412 This routine should be use when the CMS requestStatus is ERROR or 413 EXCEPTION. It is capable of parsing both. A CMS ERROR occurs when a known 414 anticipated error condition occurs (e.g. asking for an item which does not 415 exist). A CMS EXCEPTION occurs when an exception is thrown in the CMS server 416 and it's not caught and converted into an ERROR. Think of EXCEPTIONS as the 417 "catch all" error situation. 418 419 ERROR's and EXCEPTIONS's both have error message strings associated with 420 them. For an ERROR it's errorDetails, for an EXCEPTION it's 421 unexpectedError. In addition an EXCEPTION may include an array of additional 422 error strings in it's errorDescription field. 423 424 After parsing the results are returned in a result dict. The following 425 table illustrates the mapping from the CMS data item to what may be found in 426 the result dict. If a CMS data item is absent it will also be absent in the 427 result dict. 428 429 +----------------+---------------+------------------+---------------+ 430 |cms name |cms type |result name |result type | 431 +================+===============+==================+===============+ 432 |requestStatus |int |request_status |int | 433 +----------------+---------------+------------------+---------------+ 434 |errorDetails |string |error_string [1]_ |unicode | 435 +----------------+---------------+------------------+---------------+ 436 |unexpectedError |string |error_string [1]_ |unicode | 437 +----------------+---------------+------------------+---------------+ 438 |errorDescription|[string] |error_descriptions|[unicode] | 439 +----------------+---------------+------------------+---------------+ 440 |authority |string |authority |unicode | 441 +----------------+---------------+------------------+---------------+ 442 443 .. [1] errorDetails is the error message string when the requestStatus 444 is ERROR. unexpectedError is the error message string when 445 the requestStatus is EXCEPTION. This routine recognizes both 446 ERROR's and EXCEPTION's and depending on which is found folds 447 the error message into the error_string result value. 448 ''' 449 450 response = {} 451 response['request_status'] = CMS_STATUS_ERROR # assume error 452 453 454 request_status = doc.xpath('//xml/fixed/requestStatus[1]') 455 if len(request_status) == 1: 456 request_status = int(request_status[0].text) 457 response['request_status'] = request_status 458 459 error_descriptions = [] 460 for description in doc.xpath('//xml/records[*]/record/errorDescription'): 461 error_descriptions.append(etree.tostring(description, method='text', 462 encoding=unicode).strip()) 463 if len(error_descriptions) > 0: 464 response['error_descriptions'] = error_descriptions 465 466 authority = doc.xpath('//xml/fixed/authorityName[1]') 467 if len(authority) == 1: 468 authority = etree.tostring(authority[0], method='text', 469 encoding=unicode).strip() 470 response['authority'] = authority 471 472 # Should never get both errorDetail and unexpectedError 473 error_detail = doc.xpath('//xml/fixed/errorDetails[1]') 474 if len(error_detail) == 1: 475 error_detail = etree.tostring(error_detail[0], method='text', 476 encoding=unicode).strip() 477 response['error_string'] = error_detail 478 479 unexpected_error = doc.xpath('//xml/fixed/unexpectedError[1]') 480 if len(unexpected_error) == 1: 481 unexpected_error = etree.tostring(unexpected_error[0], method='text', 482 encoding=unicode).strip() 483 response['error_string'] = unexpected_error 484 485 return response
486 487
488 -def parse_error_response_xml(doc):
489 ''' 490 :param doc: The root node of the xml document to parse 491 :returns: result dict 492 493 CMS currently returns errors via XML as either a "template" document 494 (generated by CMSServlet.outputXML() or a "response" document (generated by 495 CMSServlet.outputError()). 496 497 This routine is used to parse a "response" style error document. 498 499 +---------------+---------------+---------------+---------------+ 500 |cms name |cms type |result name |result type | 501 +===============+===============+===============+===============+ 502 |Status |int |error_code |int [1]_ | 503 +---------------+---------------+---------------+---------------+ 504 |Error |string |error_string |unicode | 505 +---------------+---------------+---------------+---------------+ 506 |RequestID |string |request_id |string | 507 +---------------+---------------+---------------+---------------+ 508 509 .. [1] error code may be one of: 510 511 - CMS_SUCCESS = 0 512 - CMS_FAILURE = 1 513 - CMS_AUTH_FAILURE = 2 514 515 However, profileSubmit sometimes also returns these values: 516 517 - EXCEPTION = 1 518 - DEFERRED = 2 519 - REJECTED = 3 520 521 ''' 522 523 response = {} 524 response['error_code'] = CMS_FAILURE # assume error 525 526 error_code = doc.xpath('//XMLResponse/Status[1]') 527 if len(error_code) == 1: 528 error_code = int(error_code[0].text) 529 response['error_code'] = error_code 530 531 error_string = doc.xpath('//XMLResponse/Error[1]') 532 if len(error_string) == 1: 533 error_string = etree.tostring(error_string[0], method='text', 534 encoding=unicode).strip() 535 response['error_string'] = error_string 536 537 request_id = doc.xpath('//XMLResponse/RequestId[1]') 538 if len(request_id) == 1: 539 request_id = etree.tostring(request_id[0], method='text', 540 encoding=unicode).strip() 541 response['request_id'] = request_id 542 543 return response
544
545 -def parse_profile_submit_result_xml(doc):
546 ''' 547 :param doc: The root node of the xml document to parse 548 :returns: result dict 549 :except ValueError: 550 551 CMS returns an error code and an array of request records. 552 553 This function returns a response dict with the following format: 554 {'error_code' : int, 'requests' : [{}]} 555 556 The mapping of fields and data types is illustrated in the following table. 557 558 If the error_code is not SUCCESS then the response dict will have the 559 contents described in `parse_error_response_xml`. 560 561 +--------------------+----------------+------------------------+---------------+ 562 |cms name |cms type |result name |result type | 563 +====================+================+========================+===============+ 564 |Status |int |error_code |int | 565 +--------------------+----------------+------------------------+---------------+ 566 |Requests[].Id |string |requests[].request_id |unicode | 567 +--------------------+----------------+------------------------+---------------+ 568 |Requests[].SubjectDN|string |requests[].subject |unicode | 569 +--------------------+----------------+------------------------+---------------+ 570 |Requests[].serialno |BigInteger |requests[].serial_number|int|long | 571 +--------------------+----------------+------------------------+---------------+ 572 |Requests[].b64 |string |requests[].certificate |unicode [1]_ | 573 +--------------------+----------------+------------------------+---------------+ 574 |Requests[].pkcs7 |string | | | 575 +--------------------+----------------+------------------------+---------------+ 576 577 .. [1] Base64 encoded 578 579 ''' 580 581 error_code = get_error_code_xml(doc) 582 if error_code != CMS_SUCCESS: 583 response = parse_error_response_xml(doc) 584 return response 585 586 response = {} 587 response['error_code'] = error_code 588 589 requests = [] 590 response['requests'] = requests 591 592 for request in doc.xpath('//XMLResponse/Requests[*]/Request'): 593 response_request = {} 594 requests.append(response_request) 595 596 request_id = request.xpath('Id[1]') 597 if len(request_id) == 1: 598 request_id = etree.tostring(request_id[0], method='text', 599 encoding=unicode).strip() 600 response_request['request_id'] = request_id 601 602 subject_dn = request.xpath('SubjectDN[1]') 603 if len(subject_dn) == 1: 604 subject_dn = etree.tostring(subject_dn[0], method='text', 605 encoding=unicode).strip() 606 response_request['subject'] = subject_dn 607 608 serial_number = request.xpath('serialno[1]') 609 if len(serial_number) == 1: 610 serial_number = int(serial_number[0].text, 16) # parse as hex 611 response_request['serial_number'] = serial_number 612 613 certificate = request.xpath('b64[1]') 614 if len(certificate) == 1: 615 certificate = etree.tostring(certificate[0], method='text', 616 encoding=unicode).strip() 617 response_request['certificate'] = certificate 618 619 return response
620 621
622 -def parse_check_request_result_xml(doc):
623 ''' 624 :param doc: The root node of the xml document to parse 625 :returns: result dict 626 :except ValueError: 627 628 After parsing the results are returned in a result dict. The following 629 table illustrates the mapping from the CMS data item to what may be found in 630 the result dict. If a CMS data item is absent it will also be absent in the 631 result dict. 632 633 If the requestStatus is not SUCCESS then the response dict will have the 634 contents described in `parse_error_template_xml`. 635 636 +-------------------------+---------------+-------------------+-----------------+ 637 |cms name |cms type |result name |result type | 638 +=========================+===============+===================+=================+ 639 |authority |string |authority |unicode | 640 +-------------------------+---------------+-------------------+-----------------+ 641 |requestId |string |request_id |string | 642 +-------------------------+---------------+-------------------+-----------------+ 643 |staus |string |cert_request_status|unicode [1]_ | 644 +-------------------------+---------------+-------------------+-----------------+ 645 |createdOn |long, timestamp|created_on |datetime.datetime| 646 +-------------------------+---------------+-------------------+-----------------+ 647 |updatedOn |long, timestamp|updated_on |datetime.datetime| 648 +-------------------------+---------------+-------------------+-----------------+ 649 |requestNotes |string |request_notes |unicode | 650 +-------------------------+---------------+-------------------+-----------------+ 651 |pkcs7ChainBase64 |string |pkcs7_chain |unicode [2]_ | 652 +-------------------------+---------------+-------------------+-----------------+ 653 |cmcFullEnrollmentResponse|string |full_response |unicode [2]_ | 654 +-------------------------+---------------+-------------------+-----------------+ 655 |records[].serialNumber |BigInteger |serial_numbers |[int|long] | 656 +-------------------------+---------------+-------------------+-----------------+ 657 658 .. [1] cert_request_status may be one of: 659 660 - "begin" 661 - "pending" 662 - "approved" 663 - "svc_pending" 664 - "canceled" 665 - "rejected" 666 - "complete" 667 668 .. [2] Base64 encoded 669 670 ''' 671 request_status = get_request_status_xml(doc) 672 673 if request_status != CMS_STATUS_SUCCESS: 674 response = parse_error_template_xml(doc) 675 return response 676 677 response = {} 678 response['request_status'] = request_status 679 680 cert_request_status = doc.xpath('//xml/header/status[1]') 681 if len(cert_request_status) == 1: 682 cert_request_status = etree.tostring(cert_request_status[0], method='text', 683 encoding=unicode).strip() 684 response['cert_request_status'] = cert_request_status 685 686 request_id = doc.xpath('//xml/header/requestId[1]') 687 if len(request_id) == 1: 688 request_id = etree.tostring(request_id[0], method='text', 689 encoding=unicode).strip() 690 response['request_id'] = request_id 691 692 authority = doc.xpath('//xml/header/authority[1]') 693 if len(authority) == 1: 694 authority = etree.tostring(authority[0], method='text', 695 encoding=unicode).strip() 696 response['authority'] = authority 697 698 updated_on = doc.xpath('//xml/header/updatedOn[1]') 699 if len(updated_on) == 1: 700 updated_on = datetime.datetime.utcfromtimestamp(int(updated_on[0].text)) 701 response['updated_on'] = updated_on 702 703 created_on = doc.xpath('//xml/header/createdOn[1]') 704 if len(created_on) == 1: 705 created_on = datetime.datetime.utcfromtimestamp(int(created_on[0].text)) 706 response['created_on'] = created_on 707 708 request_notes = doc.xpath('//xml/header/requestNotes[1]') 709 if len(request_notes) == 1: 710 request_notes = etree.tostring(request_notes[0], method='text', 711 encoding=unicode).strip() 712 response['request_notes'] = request_notes 713 714 pkcs7_chain = doc.xpath('//xml/header/pkcs7ChainBase64[1]') 715 if len(pkcs7_chain) == 1: 716 pkcs7_chain = etree.tostring(pkcs7_chain[0], method='text', 717 encoding=unicode).strip() 718 response['pkcs7_chain'] = pkcs7_chain 719 720 full_response = doc.xpath('//xml/header/cmcFullEnrollmentResponse[1]') 721 if len(full_response) == 1: 722 full_response = etree.tostring(full_response[0], method='text', 723 encoding=unicode).strip() 724 response['full_response'] = full_response 725 726 serial_numbers = [] 727 response['serial_numbers'] = serial_numbers 728 for serial_number in doc.xpath('//xml/records[*]/record/serialNumber'): 729 serial_number = int(serial_number.text, 16) # parse as hex 730 serial_numbers.append(serial_number) 731 732 return response
733
734 -def parse_display_cert_xml(doc):
735 ''' 736 :param doc: The root node of the xml document to parse 737 :returns: result dict 738 :except ValueError: 739 740 After parsing the results are returned in a result dict. The following 741 table illustrates the mapping from the CMS data item to what may be found in 742 the result dict. If a CMS data item is absent it will also be absent in the 743 result dict. 744 745 If the requestStatus is not SUCCESS then the response dict will have the 746 contents described in `parse_error_template_xml`. 747 748 +----------------+---------------+-----------------+---------------+ 749 |cms name |cms type |result name |result type | 750 +================+===============+=================+===============+ 751 |emailCert |Boolean |email_cert |bool | 752 +----------------+---------------+-----------------+---------------+ 753 |noCertImport |Boolean |no_cert_import |bool | 754 +----------------+---------------+-----------------+---------------+ 755 |revocationReason|int |revocation_reason|int [1]_ | 756 +----------------+---------------+-----------------+---------------+ 757 |certPrettyPrint |string |cert_pretty |unicode | 758 +----------------+---------------+-----------------+---------------+ 759 |authorityid |string |authority |unicode | 760 +----------------+---------------+-----------------+---------------+ 761 |certFingerprint |string |fingerprint |unicode | 762 +----------------+---------------+-----------------+---------------+ 763 |certChainBase64 |string |certificate |unicode [2]_ | 764 +----------------+---------------+-----------------+---------------+ 765 |serialNumber |string |serial_number |int|long | 766 +----------------+---------------+-----------------+---------------+ 767 |pkcs7ChainBase64|string |pkcs7_chain |unicode [2]_ | 768 +----------------+---------------+-----------------+---------------+ 769 770 .. [1] revocation reason may be one of: 771 772 - 0 = UNSPECIFIED 773 - 1 = KEY_COMPROMISE 774 - 2 = CA_COMPROMISE 775 - 3 = AFFILIATION_CHANGED 776 - 4 = SUPERSEDED 777 - 5 = CESSATION_OF_OPERATION 778 - 6 = CERTIFICATE_HOLD 779 - 8 = REMOVE_FROM_CRL 780 - 9 = PRIVILEGE_WITHDRAWN 781 - 10 = AA_COMPROMISE 782 783 .. [2] Base64 encoded 784 785 ''' 786 787 request_status = get_request_status_xml(doc) 788 789 if request_status != CMS_STATUS_SUCCESS: 790 response = parse_error_template_xml(doc) 791 return response 792 793 response = {} 794 response['request_status'] = request_status 795 796 email_cert = doc.xpath('//xml/header/emailCert[1]') 797 if len(email_cert) == 1: 798 parse_and_set_boolean_xml(email_cert[0], response, 'email_cert') 799 800 no_cert_import = doc.xpath('//xml/header/noCertImport[1]') 801 if len(no_cert_import) == 1: 802 parse_and_set_boolean_xml(no_cert_import[0], response, 'no_cert_import') 803 804 revocation_reason = doc.xpath('//xml/header/revocationReason[1]') 805 if len(revocation_reason) == 1: 806 revocation_reason = int(revocation_reason[0].text) 807 response['revocation_reason'] = revocation_reason 808 809 cert_pretty = doc.xpath('//xml/header/certPrettyPrint[1]') 810 if len(cert_pretty) == 1: 811 cert_pretty = etree.tostring(cert_pretty[0], method='text', 812 encoding=unicode).strip() 813 response['cert_pretty'] = cert_pretty 814 815 authority = doc.xpath('//xml/header/authorityid[1]') 816 if len(authority) == 1: 817 authority = etree.tostring(authority[0], method='text', 818 encoding=unicode).strip() 819 response['authority'] = authority 820 821 fingerprint = doc.xpath('//xml/header/certFingerprint[1]') 822 if len(fingerprint) == 1: 823 fingerprint = etree.tostring(fingerprint[0], method='text', 824 encoding=unicode).strip() 825 response['fingerprint'] = fingerprint 826 827 certificate = doc.xpath('//xml/header/certChainBase64[1]') 828 if len(certificate) == 1: 829 certificate = etree.tostring(certificate[0], method='text', 830 encoding=unicode).strip() 831 response['certificate'] = certificate 832 833 serial_number = doc.xpath('//xml/header/serialNumber[1]') 834 if len(serial_number) == 1: 835 serial_number = int(serial_number[0].text, 16) # parse as hex 836 response['serial_number'] = serial_number 837 838 pkcs7_chain = doc.xpath('//xml/header/pkcs7ChainBase64[1]') 839 if len(pkcs7_chain) == 1: 840 pkcs7_chain = etree.tostring(pkcs7_chain[0], method='text', 841 encoding=unicode).strip() 842 response['pkcs7_chain'] = pkcs7_chain 843 844 return response
845
846 -def parse_revoke_cert_xml(doc):
847 ''' 848 :param doc: The root node of the xml document to parse 849 :returns: result dict 850 :except ValueError: 851 852 After parsing the results are returned in a result dict. The following 853 table illustrates the mapping from the CMS data item to what may be found in 854 the result dict. If a CMS data item is absent it will also be absent in the 855 result dict. 856 857 If the requestStatus is not SUCCESS then the response dict will have the 858 contents described in `parse_error_template_xml`. 859 860 +----------------------+----------------+-----------------------+---------------+ 861 |cms name |cms type |result name |result type | 862 +======================+================+=======================+===============+ 863 |dirEnabled |string [1]_ |dir_enabled |bool | 864 +----------------------+----------------+-----------------------+---------------+ 865 |certsUpdated |int |certs_updated |int | 866 +----------------------+----------------+-----------------------+---------------+ 867 |certsToUpdate |int |certs_to_update |int | 868 +----------------------+----------------+-----------------------+---------------+ 869 |error |string [2]_ |error_string |unicode | 870 +----------------------+----------------+-----------------------+---------------+ 871 |revoked |string [3]_ |revoked |unicode | 872 +----------------------+----------------+-----------------------+---------------+ 873 |totalRecordCount |int |total_record_count |int | 874 +----------------------+----------------+-----------------------+---------------+ 875 |updateCRL |string [1]_ [4]_|update_crl |bool | 876 +----------------------+----------------+-----------------------+---------------+ 877 |updateCRLSuccess |string [1]_ [4]_|update_crl_success |bool | 878 +----------------------+----------------+-----------------------+---------------+ 879 |updateCRLError |string [4]_ |update_crl_error |unicode | 880 +----------------------+----------------+-----------------------+---------------+ 881 |publishCRLSuccess |string [1]_[4]_ |publish_crl_success |bool | 882 +----------------------+----------------+-----------------------+---------------+ 883 |publishCRLError |string [4]_ |publish_crl_error |unicode | 884 +----------------------+----------------+-----------------------+---------------+ 885 |crlUpdateStatus |string [1]_ [5]_|crl_update_status |bool | 886 +----------------------+----------------+-----------------------+---------------+ 887 |crlUpdateError |string [5]_ |crl_update_error |unicode | 888 +----------------------+----------------+-----------------------+---------------+ 889 |crlPublishStatus |string [1]_ [5]_|crl_publish_status |bool | 890 +----------------------+----------------+-----------------------+---------------+ 891 |crlPublishError |string [5]_ |crl_publish_error |unicode | 892 +----------------------+----------------+-----------------------+---------------+ 893 |records[].serialNumber|BigInteger |records[].serial_number|int|long | 894 +----------------------+----------------+-----------------------+---------------+ 895 |records[].error |string [2]_ |records[].error_string |unicode | 896 +----------------------+----------------+-----------------------+---------------+ 897 898 .. [1] String value is either "yes" or "no" 899 .. [2] Sometimes the error string is empty (null) 900 .. [3] revoked may be one of: 901 902 - "yes" 903 - "no" 904 - "begin" 905 - "pending" 906 - "approved" 907 - "svc_pending" 908 - "canceled" 909 - "rejected" 910 - "complete" 911 912 .. [4] Only sent if CRL update information is available. 913 If sent it's only value is "yes". 914 If sent then the following values may also be sent, 915 otherwise they will be absent: 916 917 - updateCRLSuccess 918 - updateCRLError 919 - publishCRLSuccess 920 - publishCRLError 921 922 .. [5] The cms name varies depending on whether the issuing point is MasterCRL 923 or not. If the issuing point is not the MasterCRL then the cms name 924 will be appended with an underscore and the issuing point name. 925 Thus for example the cms name crlUpdateStatus will be crlUpdateStatus 926 if the issuing point is the MasterCRL. However if the issuing point 927 is "foobar" then crlUpdateStatus will be crlUpdateStatus_foobar. 928 When we return the response dict the key will always be the "base" 929 name without the _issuing_point suffix. Thus crlUpdateStatus_foobar 930 will appear in the response dict under the key 'crl_update_status' 931 932 ''' 933 934 request_status = get_request_status_xml(doc) 935 936 if request_status != CMS_STATUS_SUCCESS: 937 response = parse_error_template_xml(doc) 938 return response 939 940 response = {} 941 response['request_status'] = request_status 942 943 records = [] 944 response['records'] = records 945 946 dir_enabled = doc.xpath('//xml/header/dirEnabled[1]') 947 if len(dir_enabled) == 1: 948 parse_and_set_boolean_xml(dir_enabled[0], response, 'dir_enabled') 949 950 certs_updated = doc.xpath('//xml/header/certsUpdated[1]') 951 if len(certs_updated) == 1: 952 certs_updated = int(certs_updated[0].text) 953 response['certs_updated'] = certs_updated 954 955 certs_to_update = doc.xpath('//xml/header/certsToUpdate[1]') 956 if len(certs_to_update) == 1: 957 certs_to_update = int(certs_to_update[0].text) 958 response['certs_to_update'] = certs_to_update 959 960 error_string = doc.xpath('//xml/header/error[1]') 961 if len(error_string) == 1: 962 error_string = etree.tostring(error_string[0], method='text', 963 encoding=unicode).strip() 964 response['error_string'] = error_string 965 966 revoked = doc.xpath('//xml/header/revoked[1]') 967 if len(revoked) == 1: 968 revoked = etree.tostring(revoked[0], method='text', 969 encoding=unicode).strip() 970 response['revoked'] = revoked 971 972 total_record_count = doc.xpath('//xml/header/totalRecordCount[1]') 973 if len(total_record_count) == 1: 974 total_record_count = int(total_record_count[0].text) 975 response['total_record_count'] = total_record_count 976 977 update_crl = doc.xpath('//xml/header/updateCRL[1]') 978 if len(update_crl) == 1: 979 parse_and_set_boolean_xml(update_crl[0], response, 'update_crl') 980 981 update_crl_success = doc.xpath('//xml/header/updateCRLSuccess[1]') 982 if len(update_crl_success) == 1: 983 parse_and_set_boolean_xml(update_crl_success[0], response, 'update_crl_success') 984 985 update_crl_error = doc.xpath('//xml/header/updateCRLError[1]') 986 if len(update_crl_error) == 1: 987 update_crl_error = etree.tostring(update_crl_error[0], method='text', 988 encoding=unicode).strip() 989 response['update_crl_error'] = update_crl_error 990 991 publish_crl_success = doc.xpath('//xml/header/publishCRLSuccess[1]') 992 if len(publish_crl_success) == 1: 993 parse_and_set_boolean_xml(publish_crl_success[0], response, 'publish_crl_success') 994 995 publish_crl_error = doc.xpath('//xml/header/publishCRLError[1]') 996 if len(publish_crl_error) == 1: 997 publish_crl_error = etree.tostring(publish_crl_error[0], method='text', 998 encoding=unicode).strip() 999 response['publish_crl_error'] = publish_crl_error 1000 1001 crl_update_status = doc.xpath("//xml/header/*[starts-with(name(), 'crlUpdateStatus')][1]") 1002 if len(crl_update_status) == 1: 1003 parse_and_set_boolean_xml(crl_update_status[0], response, 'crl_update_status') 1004 1005 crl_update_error = doc.xpath("//xml/header/*[starts-with(name(), 'crlUpdateError')][1]") 1006 if len(crl_update_error) == 1: 1007 crl_update_error = etree.tostring(crl_update_error[0], method='text', 1008 encoding=unicode).strip() 1009 response['crl_update_error'] = crl_update_error 1010 1011 crl_publish_status = doc.xpath("//xml/header/*[starts-with(name(), 'crlPublishStatus')][1]") 1012 if len(crl_publish_status) == 1: 1013 parse_and_set_boolean_xml(crl_publish_status[0], response, 'crl_publish_status') 1014 1015 crl_publish_error = doc.xpath("//xml/header/*[starts-with(name(), 'crlPublishError')][1]") 1016 if len(crl_publish_error) == 1: 1017 crl_publish_error = etree.tostring(crl_publish_error[0], method='text', 1018 encoding=unicode).strip() 1019 response['crl_publish_error'] = crl_publish_error 1020 1021 for record in doc.xpath('//xml/records[*]/record'): 1022 response_record = {} 1023 records.append(response_record) 1024 1025 serial_number = record.xpath('serialNumber[1]') 1026 if len(serial_number) == 1: 1027 serial_number = int(serial_number[0].text, 16) # parse as hex 1028 response_record['serial_number'] = serial_number 1029 1030 error_string = record.xpath('error[1]') 1031 if len(error_string) == 1: 1032 error_string = etree.tostring(error_string[0], method='text', 1033 encoding=unicode).strip() 1034 response_record['error_string'] = error_string 1035 1036 return response
1037
1038 -def parse_unrevoke_cert_xml(doc):
1039 ''' 1040 :param doc: The root node of the xml document to parse 1041 :returns: result dict 1042 :except ValueError: 1043 1044 After parsing the results are returned in a result dict. The following 1045 table illustrates the mapping from the CMS data item to what may be found in 1046 the result dict. If a CMS data item is absent it will also be absent in the 1047 result dict. 1048 1049 If the requestStatus is not SUCCESS then the response dict will have the 1050 contents described in `parse_error_template_xml`. 1051 1052 +----------------------+----------------+-----------------------+---------------+ 1053 |cms name |cms type |result name |result type | 1054 +======================+================+=======================+===============+ 1055 |dirEnabled |string [1]_ |dir_enabled |bool | 1056 +----------------------+----------------+-----------------------+---------------+ 1057 |dirUpdated |string [1]_ |dir_updated |bool | 1058 +----------------------+----------------+-----------------------+---------------+ 1059 |error |string |error_string |unicode | 1060 +----------------------+----------------+-----------------------+---------------+ 1061 |unrevoked |string [3]_ |unrevoked |unicode | 1062 +----------------------+----------------+-----------------------+---------------+ 1063 |updateCRL |string [1]_ [4]_|update_crl |bool | 1064 +----------------------+----------------+-----------------------+---------------+ 1065 |updateCRLSuccess |string [1]_ [4]_|update_crl_success |bool | 1066 +----------------------+----------------+-----------------------+---------------+ 1067 |updateCRLError |string [4]_ |update_crl_error |unicode | 1068 +----------------------+----------------+-----------------------+---------------+ 1069 |publishCRLSuccess |string [1]_ [4]_|publish_crl_success |bool | 1070 +----------------------+----------------+-----------------------+---------------+ 1071 |publishCRLError |string [4]_ |publish_crl_error |unicode | 1072 +----------------------+----------------+-----------------------+---------------+ 1073 |crlUpdateStatus |string [1]_ [5]_|crl_update_status |bool | 1074 +----------------------+----------------+-----------------------+---------------+ 1075 |crlUpdateError |string [5]_ |crl_update_error |unicode | 1076 +----------------------+----------------+-----------------------+---------------+ 1077 |crlPublishStatus |string [1]_ [5]_|crl_publish_status |bool | 1078 +----------------------+----------------+-----------------------+---------------+ 1079 |crlPublishError |string [5]_ |crl_publish_error |unicode | 1080 +----------------------+----------------+-----------------------+---------------+ 1081 |serialNumber |BigInteger |serial_number |int|long | 1082 +----------------------+----------------+-----------------------+---------------+ 1083 1084 .. [1] String value is either "yes" or "no" 1085 .. [3] unrevoked may be one of: 1086 1087 - "yes" 1088 - "no" 1089 - "pending" 1090 1091 .. [4] Only sent if CRL update information is available. 1092 If sent it's only value is "yes". 1093 If sent then the following values may also be sent, 1094 otherwise they will be absent: 1095 1096 - updateCRLSuccess 1097 - updateCRLError 1098 - publishCRLSuccess 1099 - publishCRLError 1100 1101 .. [5] The cms name varies depending on whether the issuing point is MasterCRL 1102 or not. If the issuing point is not the MasterCRL then the cms name 1103 will be appended with an underscore and the issuing point name. 1104 Thus for example the cms name crlUpdateStatus will be crlUpdateStatus 1105 if the issuing point is the MasterCRL. However if the issuing point 1106 is "foobar" then crlUpdateStatus will be crlUpdateStatus_foobar. 1107 When we return the response dict the key will always be the "base" 1108 name without the _issuing_point suffix. Thus crlUpdateStatus_foobar 1109 will appear in the response dict under the key 'crl_update_status' 1110 1111 ''' 1112 1113 request_status = get_request_status_xml(doc) 1114 1115 if request_status != CMS_STATUS_SUCCESS: 1116 response = parse_error_template_xml(doc) 1117 return response 1118 1119 response = {} 1120 response['request_status'] = request_status 1121 1122 dir_enabled = doc.xpath('//xml/header/dirEnabled[1]') 1123 if len(dir_enabled) == 1: 1124 parse_and_set_boolean_xml(dir_enabled[0], response, 'dir_enabled') 1125 1126 dir_updated = doc.xpath('//xml/header/dirUpdated[1]') 1127 if len(dir_updated) == 1: 1128 parse_and_set_boolean_xml(dir_updated[0], response, 'dir_updated') 1129 1130 error_string = doc.xpath('//xml/header/error[1]') 1131 if len(error_string) == 1: 1132 error_string = etree.tostring(error_string[0], method='text', 1133 encoding=unicode).strip() 1134 response['error_string'] = error_string 1135 1136 unrevoked = doc.xpath('//xml/header/unrevoked[1]') 1137 if len(unrevoked) == 1: 1138 unrevoked = etree.tostring(unrevoked[0], method='text', 1139 encoding=unicode).strip() 1140 response['unrevoked'] = unrevoked 1141 1142 update_crl = doc.xpath('//xml/header/updateCRL[1]') 1143 if len(update_crl) == 1: 1144 parse_and_set_boolean_xml(update_crl[0], response, 'update_crl') 1145 1146 update_crl_success = doc.xpath('//xml/header/updateCRLSuccess[1]') 1147 if len(update_crl_success) == 1: 1148 parse_and_set_boolean_xml(update_crl_success[0], response, 'update_crl_success') 1149 1150 update_crl_error = doc.xpath('//xml/header/updateCRLError[1]') 1151 if len(update_crl_error) == 1: 1152 update_crl_error = etree.tostring(update_crl_error[0], method='text', 1153 encoding=unicode).strip() 1154 response['update_crl_error'] = update_crl_error 1155 1156 publish_crl_success = doc.xpath('//xml/header/publishCRLSuccess[1]') 1157 if len(publish_crl_success) == 1: 1158 parse_and_set_boolean_xml(publish_crl_success[0], response, 'publish_crl_success') 1159 1160 publish_crl_error = doc.xpath('//xml/header/publishCRLError[1]') 1161 if len(publish_crl_error) == 1: 1162 publish_crl_error = etree.tostring(publish_crl_error[0], method='text', 1163 encoding=unicode).strip() 1164 response['publish_crl_error'] = publish_crl_error 1165 1166 crl_update_status = doc.xpath("//xml/header/*[starts-with(name(), 'crlUpdateStatus')][1]") 1167 if len(crl_update_status) == 1: 1168 parse_and_set_boolean_xml(crl_update_status[0], response, 'crl_update_status') 1169 1170 crl_update_error = doc.xpath("//xml/header/*[starts-with(name(), 'crlUpdateError')][1]") 1171 if len(crl_update_error) == 1: 1172 crl_update_error = etree.tostring(crl_update_error[0], method='text', 1173 encoding=unicode).strip() 1174 response['crl_update_error'] = crl_update_error 1175 1176 crl_publish_status = doc.xpath("//xml/header/*[starts-with(name(), 'crlPublishStatus')][1]") 1177 if len(crl_publish_status) == 1: 1178 parse_and_set_boolean_xml(crl_publish_status[0], response, 'crl_publish_status') 1179 1180 crl_publish_error = doc.xpath("//xml/header/*[starts-with(name(), 'crlPublishError')][1]") 1181 if len(crl_publish_error) == 1: 1182 crl_publish_error = etree.tostring(crl_publish_error[0], method='text', 1183 encoding=unicode).strip() 1184 response['crl_publish_error'] = crl_publish_error 1185 1186 serial_number = doc.xpath('//xml/header/serialNumber[1]') 1187 if len(serial_number) == 1: 1188 serial_number = int(serial_number[0].text, 16) # parse as hex 1189 response['serial_number'] = serial_number 1190 1191 return response
1192 1193 #------------------------------------------------------------------------------- 1194 1195 from ipalib import api, SkipPluginModule 1196 if api.env.ra_plugin != 'dogtag': 1197 # In this case, abort loading this plugin module... 1198 raise SkipPluginModule(reason='dogtag not selected as RA plugin') 1199 import os, random, ldap 1200 from ipaserver.plugins import rabase 1201 from ipalib.errors import NetworkError, CertificateOperationError 1202 from ipalib.constants import TYPE_ERROR 1203 from ipapython import dogtag 1204 from ipalib import _ 1205
1206 -class ra(rabase.rabase):
1207 """ 1208 Request Authority backend plugin. 1209 """
1210 - def __init__(self):
1211 if api.env.in_tree: 1212 self.sec_dir = api.env.dot_ipa + os.sep + 'alias' 1213 self.pwd_file = self.sec_dir + os.sep + '.pwd' 1214 else: 1215 self.sec_dir = "/etc/httpd/alias" 1216 self.pwd_file = "/etc/httpd/alias/pwdfile.txt" 1217 self.noise_file = self.sec_dir + os.sep + '.noise' 1218 self.ipa_key_size = "2048" 1219 self.ipa_certificate_nickname = "ipaCert" 1220 self.ca_certificate_nickname = "caCert" 1221 self.ca_host = None 1222 try: 1223 f = open(self.pwd_file, "r") 1224 self.password = f.readline().strip() 1225 f.close() 1226 except IOError: 1227 self.password = '' 1228 super(ra, self).__init__()
1229
1230 - def _host_has_service(self, host, service='CA'):
1231 """ 1232 :param host: A host which might be a master for a service. 1233 :param service: The service for which the host might be a master. 1234 :return: (true, false) 1235 1236 Check if a specified host is a master for a specified service. 1237 """ 1238 base_dn = 'cn=%s,cn=masters,cn=ipa,cn=etc,%s' % (host, api.env.basedn) 1239 filter = '(&(objectClass=ipaConfigObject)(cn=%s)(ipaConfigString=enabledService))' % service 1240 try: 1241 ldap2 = self.api.Backend.ldap2 1242 ent,trunc = ldap2.find_entries(filter=filter, base_dn=base_dn) 1243 if len(ent): 1244 return True 1245 except Exception, e: 1246 pass 1247 return False
1248
1249 - def _select_any_master(self, service='CA'):
1250 """ 1251 :param service: The service for which we're looking for a master. 1252 :return: host 1253 as str 1254 1255 Select any host which is a master for a specified service. 1256 """ 1257 base_dn = 'cn=masters,cn=ipa,cn=etc,%s' % api.env.basedn 1258 filter = '(&(objectClass=ipaConfigObject)(cn=%s)(ipaConfigString=enabledService))' % service 1259 try: 1260 ldap2 = self.api.Backend.ldap2 1261 ent,trunc = ldap2.find_entries(filter=filter, base_dn=base_dn) 1262 if len(ent): 1263 entry = random.choice(ent) 1264 return ldap.explode_dn(dn=entry[0],notypes=True)[1] 1265 except Exception, e: 1266 pass 1267 return None
1268
1269 - def _select_ca(self):
1270 """ 1271 :return: host 1272 as str 1273 1274 Select our CA host. 1275 """ 1276 if self._host_has_service(host=api.env.ca_host): 1277 return api.env.ca_host 1278 if api.env.host != api.env.ca_host: 1279 if self._host_has_service(host=api.env.host): 1280 return api.env.host 1281 host = self._select_any_master() 1282 if host: 1283 return host 1284 else: 1285 return api.env.ca_host
1286
1287 - def _request(self, url, port, **kw):
1288 """ 1289 :param url: The URL to post to. 1290 :param kw: Keyword arguments to encode into POST body. 1291 :return: (http_status, http_reason_phrase, http_headers, http_body) 1292 as (integer, unicode, dict, str) 1293 1294 Perform an HTTP request. 1295 """ 1296 if self.ca_host == None: 1297 self.ca_host = self._select_ca() 1298 return dogtag.http_request(self.ca_host, port, url, **kw)
1299
1300 - def _sslget(self, url, port, **kw):
1301 """ 1302 :param url: The URL to post to. 1303 :param kw: Keyword arguments to encode into POST body. 1304 :return: (http_status, http_reason_phrase, http_headers, http_body) 1305 as (integer, unicode, dict, str) 1306 1307 Perform an HTTPS request 1308 """ 1309 1310 if self.ca_host == None: 1311 self.ca_host = self._select_ca() 1312 return dogtag.https_request(self.ca_host, port, url, self.sec_dir, self.password, self.ipa_certificate_nickname, **kw)
1313
1314 - def get_parse_result_xml(self, xml_text, parse_func):
1315 ''' 1316 :param xml_text: The XML text to parse 1317 :param parse_func: The XML parsing function to apply to the parsed DOM tree. 1318 :return: parsed result dict 1319 1320 Utility routine which parses the input text into an XML DOM tree 1321 and then invokes the parsing function on the DOM tree in order 1322 to get the parsing result as a dict of key/value pairs. 1323 ''' 1324 parser = etree.XMLParser() 1325 doc = etree.fromstring(xml_text, parser) 1326 result = parse_func(doc) 1327 self.debug("%s() xml_text:\n%s\nparse_result:\n%s" % (parse_func.__name__, xml_text, result)) 1328 return result
1329
1330 - def check_request_status(self, request_id):
1331 """ 1332 :param request_id: request ID 1333 1334 Check status of a certificate signing request. 1335 1336 The command returns a dict with these possible key/value pairs. 1337 Some key/value pairs may be absent. 1338 1339 +-------------------+---------------+---------------+ 1340 |result name |result type |comments | 1341 +===================+===============+===============+ 1342 |serial_number |unicode [1]_ | | 1343 +-------------------+---------------+---------------+ 1344 |request_id |unicode | | 1345 +-------------------+---------------+---------------+ 1346 |cert_request_status|unicode [2]_ | | 1347 +-------------------+---------------+---------------+ 1348 1349 .. [1] Passed through XMLRPC as decimal string. Can convert to 1350 optimal integer type (int or long) via int(serial_number) 1351 1352 .. [2] cert_request_status may be one of: 1353 1354 - "begin" 1355 - "pending" 1356 - "approved" 1357 - "svc_pending" 1358 - "canceled" 1359 - "rejected" 1360 - "complete" 1361 1362 1363 """ 1364 self.debug('%s.check_request_status()', self.fullname) 1365 1366 # Call CMS 1367 http_status, http_reason_phrase, http_headers, http_body = \ 1368 self._request('/ca/ee/ca/checkRequest', 1369 self.env.ca_port, 1370 requestId=request_id, 1371 xml='true') 1372 1373 # Parse and handle errors 1374 if (http_status != 200): 1375 raise CertificateOperationError(error=_('Unable to communicate with CMS (%s)') % \ 1376 http_reason_phrase) 1377 1378 parse_result = self.get_parse_result_xml(http_body, parse_check_request_result_xml) 1379 request_status = parse_result['request_status'] 1380 if request_status != CMS_STATUS_SUCCESS: 1381 raise CertificateOperationError(error='%s (%s)' % \ 1382 (cms_request_status_to_string(request_status), parse_result.get('error_string'))) 1383 1384 # Return command result 1385 cmd_result = {} 1386 if parse_result.has_key('serial_numbers') and len(parse_result['serial_numbers']) > 0: 1387 # see module documentation concerning serial numbers and XMLRPC 1388 cmd_result['serial_number'] = unicode(parse_result['serial_numbers'][0]) 1389 1390 if parse_result.has_key('request_id'): 1391 cmd_result['request_id'] = parse_result['request_id'] 1392 1393 if parse_result.has_key('cert_request_status'): 1394 cmd_result['cert_request_status'] = parse_result['cert_request_status'] 1395 1396 return cmd_result
1397
1398 - def get_certificate(self, serial_number=None):
1399 """ 1400 Retrieve an existing certificate. 1401 1402 :param serial_number: Certificate serial number. Must be a string value 1403 because serial numbers may be of any magnitue and 1404 XMLRPC cannot handle integers larger than 64-bit. 1405 The string value should be decimal, but may optionally 1406 be prefixed with a hex radix prefix if the integal value 1407 is represented as hexadecimal. If no radix prefix is 1408 supplied the string will be interpreted as decimal. 1409 1410 The command returns a dict with these possible key/value pairs. 1411 Some key/value pairs may be absent. 1412 1413 +-----------------+---------------+---------------+ 1414 |result name |result type |comments | 1415 +=================+===============+===============+ 1416 |certificate |unicode [1]_ | | 1417 +-----------------+---------------+---------------+ 1418 |serial_number |unicode [2]_ | | 1419 +-----------------+---------------+---------------+ 1420 |revocation_reason|int [3]_ | | 1421 +-----------------+---------------+---------------+ 1422 1423 .. [1] Base64 encoded 1424 1425 .. [2] Passed through XMLRPC as decimal string. Can convert to 1426 optimal integer type (int or long) via int(serial_number) 1427 1428 .. [3] revocation reason may be one of: 1429 1430 - 0 = UNSPECIFIED 1431 - 1 = KEY_COMPROMISE 1432 - 2 = CA_COMPROMISE 1433 - 3 = AFFILIATION_CHANGED 1434 - 4 = SUPERSEDED 1435 - 5 = CESSATION_OF_OPERATION 1436 - 6 = CERTIFICATE_HOLD 1437 - 8 = REMOVE_FROM_CRL 1438 - 9 = PRIVILEGE_WITHDRAWN 1439 - 10 = AA_COMPROMISE 1440 1441 1442 """ 1443 self.debug('%s.get_certificate()', self.fullname) 1444 1445 # Convert serial number to integral type from string to properly handle 1446 # radix issues. Note: the int object constructor will properly handle large 1447 # magnitude integral values by returning a Python long type when necessary. 1448 serial_number = int(serial_number, 0) 1449 1450 # Call CMS 1451 http_status, http_reason_phrase, http_headers, http_body = \ 1452 self._sslget('/ca/agent/ca/displayBySerial', 1453 self.env.ca_agent_port, 1454 serialNumber=str(serial_number), 1455 xml='true') 1456 1457 1458 # Parse and handle errors 1459 if (http_status != 200): 1460 raise CertificateOperationError(error=_('Unable to communicate with CMS (%s)') % \ 1461 http_reason_phrase) 1462 1463 parse_result = self.get_parse_result_xml(http_body, parse_display_cert_xml) 1464 request_status = parse_result['request_status'] 1465 if request_status != CMS_STATUS_SUCCESS: 1466 raise CertificateOperationError(error='%s (%s)' % \ 1467 (cms_request_status_to_string(request_status), parse_result.get('error_string'))) 1468 1469 # Return command result 1470 cmd_result = {} 1471 1472 if parse_result.has_key('certificate'): 1473 cmd_result['certificate'] = parse_result['certificate'] 1474 1475 if parse_result.has_key('serial_number'): 1476 # see module documentation concerning serial numbers and XMLRPC 1477 cmd_result['serial_number'] = unicode(parse_result['serial_number']) 1478 1479 if parse_result.has_key('revocation_reason'): 1480 cmd_result['revocation_reason'] = parse_result['revocation_reason'] 1481 1482 return cmd_result
1483 1484
1485 - def request_certificate(self, csr, request_type='pkcs10'):
1486 """ 1487 :param csr: The certificate signing request. 1488 :param request_type: The request type (defaults to ``'pkcs10'``). 1489 1490 Submit certificate signing request. 1491 1492 The command returns a dict with these possible key/value pairs. 1493 Some key/value pairs may be absent. 1494 1495 +---------------+---------------+---------------+ 1496 |result name |result type |comments | 1497 +===============+===============+===============+ 1498 |serial_number |unicode [1]_ | | 1499 +---------------+---------------+---------------+ 1500 |certificate |unicode [2]_ | | 1501 +---------------+---------------+---------------+ 1502 |request_id |unicode | | 1503 +---------------+---------------+---------------+ 1504 |subject |unicode | | 1505 +---------------+---------------+---------------+ 1506 1507 .. [1] Passed through XMLRPC as decimal string. Can convert to 1508 optimal integer type (int or long) via int(serial_number) 1509 1510 .. [2] Base64 encoded 1511 1512 """ 1513 self.debug('%s.request_certificate()', self.fullname) 1514 1515 # Call CMS 1516 http_status, http_reason_phrase, http_headers, http_body = \ 1517 self._sslget('/ca/ee/ca/profileSubmitSSLClient', 1518 self.env.ca_ee_port, 1519 profileId='caIPAserviceCert', 1520 cert_request_type=request_type, 1521 cert_request=csr, 1522 xml='true') 1523 # Parse and handle errors 1524 if (http_status != 200): 1525 raise CertificateOperationError(error=_('Unable to communicate with CMS (%s)') % \ 1526 http_reason_phrase) 1527 1528 parse_result = self.get_parse_result_xml(http_body, parse_profile_submit_result_xml) 1529 # Note different status return, it's not request_status, it's error_code 1530 error_code = parse_result['error_code'] 1531 if error_code != CMS_SUCCESS: 1532 raise CertificateOperationError(error='%s (%s)' % \ 1533 (cms_error_code_to_string(error_code), parse_result.get('error_string'))) 1534 1535 # Return command result 1536 cmd_result = {} 1537 1538 # FIXME: should we return all the requests instead of just the first one? 1539 if len(parse_result['requests']) < 1: 1540 return cmd_result 1541 request = parse_result['requests'][0] 1542 1543 if request.has_key('serial_number'): 1544 # see module documentation concerning serial numbers and XMLRPC 1545 cmd_result['serial_number'] = unicode(request['serial_number']) 1546 1547 if request.has_key('certificate'): 1548 cmd_result['certificate'] = request['certificate'] 1549 1550 if request.has_key('request_id'): 1551 cmd_result['request_id'] = request['request_id'] 1552 1553 if request.has_key('subject'): 1554 cmd_result['subject'] = request['subject'] 1555 1556 return cmd_result
1557 1558
1559 - def revoke_certificate(self, serial_number, revocation_reason=0):
1560 """ 1561 :param serial_number: Certificate serial number. Must be a string value 1562 because serial numbers may be of any magnitue and 1563 XMLRPC cannot handle integers larger than 64-bit. 1564 The string value should be decimal, but may optionally 1565 be prefixed with a hex radix prefix if the integal value 1566 is represented as hexadecimal. If no radix prefix is 1567 supplied the string will be interpreted as decimal. 1568 :param revocation_reason: Integer code of revocation reason. 1569 1570 Revoke a certificate. 1571 1572 The command returns a dict with these possible key/value pairs. 1573 Some key/value pairs may be absent. 1574 1575 +---------------+---------------+---------------+ 1576 |result name |result type |comments | 1577 +===============+===============+===============+ 1578 |revoked |bool | | 1579 +---------------+---------------+---------------+ 1580 1581 """ 1582 self.debug('%s.revoke_certificate()', self.fullname) 1583 if type(revocation_reason) is not int: 1584 raise TypeError(TYPE_ERROR % ('revocation_reason', int, revocation_reason, type(revocation_reason))) 1585 1586 # Convert serial number to integral type from string to properly handle 1587 # radix issues. Note: the int object constructor will properly handle large 1588 # magnitude integral values by returning a Python long type when necessary. 1589 serial_number = int(serial_number, 0) 1590 1591 # Call CMS 1592 http_status, http_reason_phrase, http_headers, http_body = \ 1593 self._sslget('/ca/agent/ca/doRevoke', 1594 self.env.ca_agent_port, 1595 op='revoke', 1596 revocationReason=revocation_reason, 1597 revokeAll='(certRecordId=%s)' % str(serial_number), 1598 totalRecordCount=1, 1599 xml='true') 1600 1601 # Parse and handle errors 1602 if (http_status != 200): 1603 raise CertificateOperationError(error=_('Unable to communicate with CMS (%s)') % \ 1604 http_reason_phrase) 1605 1606 parse_result = self.get_parse_result_xml(http_body, parse_revoke_cert_xml) 1607 request_status = parse_result['request_status'] 1608 if request_status != CMS_STATUS_SUCCESS: 1609 raise CertificateOperationError(error='%s (%s)' % \ 1610 (cms_request_status_to_string(request_status), parse_result.get('error_string'))) 1611 1612 # Return command result 1613 cmd_result = {} 1614 1615 if parse_result.get('revoked') == 'yes': 1616 cmd_result['revoked'] = True 1617 else: 1618 cmd_result['revoked'] = False 1619 1620 return cmd_result
1621
1622 - def take_certificate_off_hold(self, serial_number):
1623 """ 1624 :param serial_number: Certificate serial number. Must be a string value 1625 because serial numbers may be of any magnitue and 1626 XMLRPC cannot handle integers larger than 64-bit. 1627 The string value should be decimal, but may optionally 1628 be prefixed with a hex radix prefix if the integal value 1629 is represented as hexadecimal. If no radix prefix is 1630 supplied the string will be interpreted as decimal. 1631 1632 Take revoked certificate off hold. 1633 1634 The command returns a dict with these possible key/value pairs. 1635 Some key/value pairs may be absent. 1636 1637 +---------------+---------------+---------------+ 1638 |result name |result type |comments | 1639 +===============+===============+===============+ 1640 |unrevoked |bool | | 1641 +---------------+---------------+---------------+ 1642 |error_string |unicode | | 1643 +---------------+---------------+---------------+ 1644 """ 1645 1646 self.debug('%s.take_certificate_off_hold()', self.fullname) 1647 1648 # Convert serial number to integral type from string to properly handle 1649 # radix issues. Note: the int object constructor will properly handle large 1650 # magnitude integral values by returning a Python long type when necessary. 1651 serial_number = int(serial_number, 0) 1652 1653 # Call CMS 1654 http_status, http_reason_phrase, http_headers, http_body = \ 1655 self._sslget('/ca/agent/ca/doUnrevoke', 1656 self.env.ca_agent_port, 1657 serialNumber=str(serial_number), 1658 xml='true') 1659 1660 # Parse and handle errors 1661 if (http_status != 200): 1662 raise CertificateOperationError(error=_('Unable to communicate with CMS (%s)') % \ 1663 http_reason_phrase) 1664 1665 parse_result = self.get_parse_result_xml(http_body, parse_unrevoke_cert_xml) 1666 request_status = parse_result['request_status'] 1667 if request_status != CMS_STATUS_SUCCESS: 1668 raise CertificateOperationError(error='%s (%s)' % \ 1669 (cms_request_status_to_string(request_status), parse_result.get('error_string'))) 1670 1671 # Return command result 1672 cmd_result = {} 1673 1674 if parse_result.has_key('error_string'): 1675 cmd_result['error_string'] = parse_result['error_string'] 1676 1677 if parse_result.get('unrevoked') == 'yes': 1678 cmd_result['unrevoked'] = True 1679 else: 1680 cmd_result['unrevoked'] = False 1681 1682 return cmd_result
1683 1684 api.register(ra) 1685