00001 """
00002 One X.509 implementation to rule them all...
00003
00004 ...and in the darkness hide the twisty maze of partially overlapping
00005 X.509 support packages in Python.
00006
00007 There are several existing packages, none of which do quite what I
00008 need, due to age, lack of documentation, specialization, or lack of
00009 foresight on somebody's part (perhaps mine). This module attempts to
00010 bring together the functionality I need in a way that hides at least
00011 some of the nasty details. This involves a lot of format conversion.
00012
00013 $Id: x509.py 3107 2010-03-16 23:13:08Z sra $
00014
00015
00016 Copyright (C) 2009-2010 Internet Systems Consortium ("ISC")
00017
00018 Permission to use, copy, modify, and distribute this software for any
00019 purpose with or without fee is hereby granted, provided that the above
00020 copyright notice and this permission notice appear in all copies.
00021
00022 THE SOFTWARE IS PROVIDED "AS IS" AND ISC DISCLAIMS ALL WARRANTIES WITH
00023 REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
00024 AND FITNESS. IN NO EVENT SHALL ISC BE LIABLE FOR ANY SPECIAL, DIRECT,
00025 INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
00026 LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE
00027 OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
00028 PERFORMANCE OF THIS SOFTWARE.
00029
00030
00031 Portions copyright (C) 2007--2008 American Registry for Internet Numbers ("ARIN")
00032
00033 Permission to use, copy, modify, and distribute this software for any
00034 purpose with or without fee is hereby granted, provided that the above
00035 copyright notice and this permission notice appear in all copies.
00036
00037 THE SOFTWARE IS PROVIDED "AS IS" AND ARIN DISCLAIMS ALL WARRANTIES WITH
00038 REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
00039 AND FITNESS. IN NO EVENT SHALL ARIN BE LIABLE FOR ANY SPECIAL, DIRECT,
00040 INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
00041 LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE
00042 OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
00043 PERFORMANCE OF THIS SOFTWARE.
00044 """
00045
00046 import POW, POW.pkix, base64, lxml.etree, os, subprocess, sys
00047 import email.mime.application, email.utils, mailbox, time
00048 import rpki.exceptions, rpki.resource_set, rpki.oids, rpki.sundial
00049 import rpki.manifest, rpki.roa, rpki.log, rpki.async
00050
00051 def calculate_SKI(public_key_der):
00052 """
00053 Calculate the SKI value given the DER representation of a public
00054 key, which requires first peeling the ASN.1 wrapper off the key.
00055 """
00056 k = POW.pkix.SubjectPublicKeyInfo()
00057 k.fromString(public_key_der)
00058 d = POW.Digest(POW.SHA1_DIGEST)
00059 d.update(k.subjectPublicKey.get())
00060 return d.digest()
00061
00062 class PEM_converter(object):
00063 """
00064 Convert between DER and PEM encodings for various kinds of ASN.1 data.
00065 """
00066
00067 def __init__(self, kind):
00068 """
00069 Initialize PEM_converter.
00070 """
00071 self.b = "-----BEGIN %s-----" % kind
00072 self.e = "-----END %s-----" % kind
00073
00074 def looks_like_PEM(self, text):
00075 """
00076 Guess whether text looks like a PEM encoding.
00077 """
00078 b = text.find(self.b)
00079 return b >= 0 and text.find(self.e) > b + len(self.b)
00080
00081 def to_DER(self, pem):
00082 """
00083 Convert from PEM to DER.
00084 """
00085 lines = [line.strip() for line in pem.splitlines(0)]
00086 while lines and lines.pop(0) != self.b:
00087 pass
00088 while lines and lines.pop(-1) != self.e:
00089 pass
00090 if not lines:
00091 raise rpki.exceptions.EmptyPEM, "Could not find PEM in:\n%s" % pem
00092 return base64.b64decode("".join(lines))
00093
00094 def to_PEM(self, der):
00095 """
00096 Convert from DER to PEM.
00097 """
00098 b64 = base64.b64encode(der)
00099 pem = self.b + "\n"
00100 while len(b64) > 64:
00101 pem += b64[0:64] + "\n"
00102 b64 = b64[64:]
00103 return pem + b64 + "\n" + self.e + "\n"
00104
00105 def _find_xia_uri(extension, name):
00106 """
00107 Find a rsync URI in an SIA or AIA extension.
00108 Returns the URI if found, otherwise None.
00109 """
00110 oid = rpki.oids.name2oid[name]
00111
00112 for method, location in extension:
00113 if method == oid and location[0] == "uri" and location[1].startswith("rsync://"):
00114 return location[1]
00115 return None
00116
00117 class DER_object(object):
00118 """
00119 Virtual class to hold a generic DER object.
00120 """
00121
00122
00123 formats = ("DER",)
00124
00125
00126 pem_converter = None
00127
00128
00129 other_clear = ()
00130
00131
00132
00133
00134 def empty(self):
00135 """
00136 Test whether this object is empty.
00137 """
00138 for a in self.formats:
00139 if getattr(self, a, None) is not None:
00140 return False
00141 return True
00142
00143 def clear(self):
00144 """
00145 Make this object empty.
00146 """
00147 for a in self.formats + self.other_clear:
00148 setattr(self, a, None)
00149
00150 def __init__(self, **kw):
00151 """
00152 Initialize a DER_object.
00153 """
00154 self.clear()
00155 if len(kw):
00156 self.set(**kw)
00157
00158 def set(self, **kw):
00159 """
00160 Set this object by setting one of its known formats.
00161
00162 This method only allows one to set one format at a time.
00163 Subsequent calls will clear the object first. The point of all
00164 this is to let the object's internal converters handle mustering
00165 the object into whatever format you need at the moment.
00166 """
00167
00168 if len(kw) == 1:
00169 name = kw.keys()[0]
00170 if name in self.formats:
00171 self.clear()
00172 setattr(self, name, kw[name])
00173 return
00174 if name == "PEM":
00175 self.clear()
00176 self.DER = self.pem_converter.to_DER(kw[name])
00177 return
00178 if name == "Base64":
00179 self.clear()
00180 self.DER = base64.b64decode(kw[name])
00181 return
00182 if name in ("PEM_file", "DER_file", "Auto_file"):
00183 f = open(kw[name], "rb")
00184 value = f.read()
00185 f.close()
00186 if name == "PEM_file" or (name == "Auto_file" and self.pem_converter.looks_like_PEM(value)):
00187 value = self.pem_converter.to_DER(value)
00188 self.clear()
00189 self.DER = value
00190 return
00191 raise rpki.exceptions.DERObjectConversionError, "Can't honor conversion request %r" % (kw,)
00192
00193 def get_DER(self):
00194 """
00195 Get the DER value of this object.
00196
00197 Subclasses will almost certainly override this method.
00198 """
00199 assert not self.empty()
00200 if self.DER:
00201 return self.DER
00202 raise rpki.exceptions.DERObjectConversionError, "No conversion path to DER available"
00203
00204 def get_Base64(self):
00205 """Get the Base64 encoding of the DER value of this object."""
00206 return base64.b64encode(self.get_DER())
00207
00208 def get_PEM(self):
00209 """Get the PEM representation of this object."""
00210 return self.pem_converter.to_PEM(self.get_DER())
00211
00212 def __cmp__(self, other):
00213 """
00214 Compare two DER-encoded objects.
00215 """
00216 if self is None and other is None:
00217 return 0
00218 elif self is None:
00219 return -1
00220 elif other is None:
00221 return 1
00222 else:
00223 return cmp(self.get_DER(), other.get_DER())
00224
00225 def hSKI(self):
00226 """
00227 Return hexadecimal string representation of SKI for this object.
00228 Only work for subclasses that implement get_SKI().
00229 """
00230 ski = self.get_SKI()
00231 return ":".join(("%02X" % ord(i) for i in ski)) if ski else ""
00232
00233 def gSKI(self):
00234 """
00235 Calculate g(SKI) for this object. Only work for subclasses
00236 that implement get_SKI().
00237 """
00238 return base64.urlsafe_b64encode(self.get_SKI()).rstrip("=")
00239
00240 def hAKI(self):
00241 """
00242 Return hexadecimal string representation of AKI for this
00243 object. Only work for subclasses that implement get_AKI().
00244 """
00245 aki = self.get_AKI()
00246 return ":".join(("%02X" % ord(i) for i in aki)) if aki else ""
00247
00248 def gAKI(self):
00249 """
00250 Calculate g(AKI) for this object. Only work for subclasses
00251 that implement get_AKI().
00252 """
00253 return base64.urlsafe_b64encode(self.get_AKI()).rstrip("=")
00254
00255 def get_AKI(self):
00256 """
00257 Get the AKI extension from this object. Only works for subclasses
00258 that support getExtension().
00259 """
00260 aki = (self.get_POWpkix().getExtension(rpki.oids.name2oid["authorityKeyIdentifier"]) or ((), 0, None))[2]
00261 return aki[0] if isinstance(aki, tuple) else aki
00262
00263 def get_SKI(self):
00264 """
00265 Get the SKI extension from this object. Only works for subclasses
00266 that support getExtension().
00267 """
00268 return (self.get_POWpkix().getExtension(rpki.oids.name2oid["subjectKeyIdentifier"]) or ((), 0, None))[2]
00269
00270 def get_SIA(self):
00271 """
00272 Get the SIA extension from this object. Only works for subclasses
00273 that support getExtension().
00274 """
00275 return (self.get_POWpkix().getExtension(rpki.oids.name2oid["subjectInfoAccess"]) or ((), 0, None))[2]
00276
00277 def get_sia_directory_uri(self):
00278 """
00279 Get SIA directory (id-ad-caRepository) URI from this object.
00280 Only works for subclasses that support getExtension().
00281 """
00282 return _find_xia_uri(self.get_SIA(), "id-ad-caRepository")
00283
00284 def get_sia_manifest_uri(self):
00285 """
00286 Get SIA manifest (id-ad-rpkiManifest) URI from this object.
00287 Only works for subclasses that support getExtension().
00288 """
00289 return _find_xia_uri(self.get_SIA(), "id-ad-rpkiManifest")
00290
00291 def get_AIA(self):
00292 """
00293 Get the SIA extension from this object. Only works for subclasses
00294 that support getExtension().
00295 """
00296 return (self.get_POWpkix().getExtension(rpki.oids.name2oid["authorityInfoAccess"]) or ((), 0, None))[2]
00297
00298 def get_aia_uri(self):
00299 """
00300 Get AIA (id-ad-caIssuers) URI from this object.
00301 Only works for subclasses that support getExtension().
00302 """
00303 return _find_xia_uri(self.get_AIA(), "id-ad-caIssuers")
00304
00305 def get_basicConstraints(self):
00306 """
00307 Get the basicConstraints extension from this object. Only works
00308 for subclasses that support getExtension().
00309 """
00310 return (self.get_POWpkix().getExtension(rpki.oids.name2oid["basicConstraints"]) or ((), 0, None))[2]
00311
00312 def is_CA(self):
00313 """
00314 Return True if and only if object has the basicConstraints
00315 extension and its cA value is true.
00316 """
00317 basicConstraints = self.get_basicConstraints()
00318 return basicConstraints and basicConstraints[0] != 0
00319
00320 def get_3779resources(self):
00321 """
00322 Get RFC 3779 resources as rpki.resource_set objects. Only works
00323 for subclasses that support getExtensions().
00324 """
00325 resources = rpki.resource_set.resource_bag.from_rfc3779_tuples(self.get_POWpkix().getExtensions())
00326 try:
00327 resources.valid_until = self.getNotAfter()
00328 except AttributeError:
00329 pass
00330 return resources
00331
00332 @classmethod
00333 def from_sql(cls, x):
00334 """Convert from SQL storage format."""
00335 return cls(DER = x)
00336
00337 def to_sql(self):
00338 """Convert to SQL storage format."""
00339 return self.get_DER()
00340
00341 def dumpasn1(self):
00342 """
00343 Pretty print an ASN.1 DER object using cryptlib dumpasn1 tool.
00344 Use a temporary file rather than popen4() because dumpasn1 uses
00345 seek() when decoding ASN.1 content nested in OCTET STRING values.
00346 """
00347
00348 ret = None
00349 fn = "dumpasn1.%d.tmp" % os.getpid()
00350 try:
00351 f = open(fn, "wb")
00352 f.write(self.get_DER())
00353 f.close()
00354 p = subprocess.Popen(("dumpasn1", "-a", fn), stdout = subprocess.PIPE, stderr = subprocess.STDOUT)
00355 ret = "\n".join(x for x in p.communicate()[0].splitlines() if x.startswith(" "))
00356 finally:
00357 os.unlink(fn)
00358 return ret
00359
00360 class X509(DER_object):
00361 """
00362 X.509 certificates.
00363
00364 This class is designed to hold all the different representations of
00365 X.509 certs we're using and convert between them. X.509 support in
00366 Python a nasty maze of half-cooked stuff (except perhaps for
00367 cryptlib, which is just different). Users of this module should not
00368 have to care about this implementation nightmare.
00369 """
00370
00371 formats = ("DER", "POW", "POWpkix")
00372 pem_converter = PEM_converter("CERTIFICATE")
00373
00374 def get_DER(self):
00375 """
00376 Get the DER value of this certificate.
00377 """
00378 assert not self.empty()
00379 if self.DER:
00380 return self.DER
00381 if self.POW:
00382 self.DER = self.POW.derWrite()
00383 return self.get_DER()
00384 if self.POWpkix:
00385 self.DER = self.POWpkix.toString()
00386 return self.get_DER()
00387 raise rpki.exceptions.DERObjectConversionError, "No conversion path to DER available"
00388
00389 def get_POW(self):
00390 """
00391 Get the POW value of this certificate.
00392 """
00393 assert not self.empty()
00394 if not self.POW:
00395 self.POW = POW.derRead(POW.X509_CERTIFICATE, self.get_DER())
00396 return self.POW
00397
00398 def get_POWpkix(self):
00399 """
00400 Get the POW.pkix value of this certificate.
00401 """
00402 assert not self.empty()
00403 if not self.POWpkix:
00404 cert = POW.pkix.Certificate()
00405 cert.fromString(self.get_DER())
00406 self.POWpkix = cert
00407 return self.POWpkix
00408
00409 def getIssuer(self):
00410 """Get the issuer of this certificate."""
00411 return self.get_POW().getIssuer()
00412
00413 def getSubject(self):
00414 """Get the subject of this certificate."""
00415 return self.get_POW().getSubject()
00416
00417 def getNotBefore(self):
00418 """Get the inception time of this certificate."""
00419 return rpki.sundial.datetime.fromASN1tuple(self.get_POWpkix().tbs.validity.notBefore.get())
00420
00421 def getNotAfter(self):
00422 """Get the expiration time of this certificate."""
00423 return rpki.sundial.datetime.fromASN1tuple(self.get_POWpkix().tbs.validity.notAfter.get())
00424
00425 def getSerial(self):
00426 """Get the serial number of this certificate."""
00427 return self.get_POW().getSerial()
00428
00429 def getPublicKey(self):
00430 """Extract the public key from this certificate."""
00431 return RSApublic(DER = self.get_POWpkix().tbs.subjectPublicKeyInfo.toString())
00432
00433 def expired(self):
00434 """Test whether this certificate has expired."""
00435 return self.getNotAfter() <= rpki.sundial.now()
00436
00437 def issue(self, keypair, subject_key, serial, sia, aia, crldp, notAfter,
00438 cn = None, resources = None, is_ca = True):
00439 """
00440 Issue a certificate.
00441 """
00442
00443 now = rpki.sundial.now()
00444 aki = self.get_SKI()
00445 ski = subject_key.get_SKI()
00446
00447 if cn is None:
00448 cn = "".join(("%02X" % ord(i) for i in ski))
00449
00450
00451
00452 cert = POW.pkix.Certificate()
00453 cert.setVersion(2)
00454 cert.setSerial(serial)
00455 cert.setIssuer(self.get_POWpkix().getSubject())
00456 cert.setSubject((((rpki.oids.name2oid["commonName"], ("printableString", cn)),),))
00457 cert.setNotBefore(now.toASN1tuple())
00458 cert.setNotAfter(notAfter.toASN1tuple())
00459 cert.tbs.subjectPublicKeyInfo.fromString(subject_key.get_DER())
00460
00461 exts = [ ["subjectKeyIdentifier", False, ski],
00462 ["authorityKeyIdentifier", False, (aki, (), None)],
00463 ["cRLDistributionPoints", False, ((("fullName", (("uri", crldp),)), None, ()),)],
00464 ["authorityInfoAccess", False, ((rpki.oids.name2oid["id-ad-caIssuers"], ("uri", aia)),)],
00465 ["certificatePolicies", True, ((rpki.oids.name2oid["id-cp-ipAddr-asNumber"], ()),)] ]
00466
00467 if is_ca:
00468 exts.append(["basicConstraints", True, (1, None)])
00469 exts.append(["keyUsage", True, (0, 0, 0, 0, 0, 1, 1)])
00470 else:
00471 exts.append(["keyUsage", True, (1,)])
00472
00473 if sia is not None:
00474 exts.append(["subjectInfoAccess", False, sia])
00475 else:
00476 assert not is_ca
00477
00478 if resources is not None and resources.asn:
00479 exts.append(["sbgp-autonomousSysNum", True, (resources.asn.to_rfc3779_tuple(), None)])
00480
00481 if resources is not None and (resources.v4 or resources.v6):
00482 exts.append(["sbgp-ipAddrBlock", True, [x for x in (resources.v4.to_rfc3779_tuple(), resources.v6.to_rfc3779_tuple()) if x is not None]])
00483
00484 for x in exts:
00485 x[0] = rpki.oids.name2oid[x[0]]
00486 cert.setExtensions(exts)
00487
00488 cert.sign(keypair.get_POW(), POW.SHA256_DIGEST)
00489
00490 return X509(POWpkix = cert)
00491
00492 def cross_certify(self, keypair, source_cert, serial, notAfter, now = None, pathLenConstraint = 0):
00493 """
00494 Issue a certificate with values taking from an existing certificate.
00495 This is used to construct some kinds oF BPKI certificates.
00496 """
00497
00498 if now is None:
00499 now = rpki.sundial.now()
00500
00501 assert isinstance(pathLenConstraint, int) and pathLenConstraint >= 0
00502
00503 cert = POW.pkix.Certificate()
00504 cert.setVersion(2)
00505 cert.setSerial(serial)
00506 cert.setIssuer(self.get_POWpkix().getSubject())
00507 cert.setSubject(source_cert.get_POWpkix().getSubject())
00508 cert.setNotBefore(now.toASN1tuple())
00509 cert.setNotAfter(notAfter.toASN1tuple())
00510 cert.tbs.subjectPublicKeyInfo.set(
00511 source_cert.get_POWpkix().tbs.subjectPublicKeyInfo.get())
00512 cert.setExtensions((
00513 (rpki.oids.name2oid["subjectKeyIdentifier" ], False, source_cert.get_SKI()),
00514 (rpki.oids.name2oid["authorityKeyIdentifier"], False, (self.get_SKI(), (), None)),
00515 (rpki.oids.name2oid["basicConstraints" ], True, (1, 0))))
00516 cert.sign(keypair.get_POW(), POW.SHA256_DIGEST)
00517
00518 return X509(POWpkix = cert)
00519
00520 @classmethod
00521 def normalize_chain(cls, chain):
00522 """
00523 Normalize a chain of certificates into a tuple of X509 objects.
00524 Given all the glue certificates needed for BPKI cross
00525 certification, it's easiest to allow sloppy arguments to the HTTPS
00526 and CMS validation methods and provide a single method that
00527 normalizes the allowed cases. So this method allows X509, None,
00528 lists, and tuples, and returns a tuple of X509 objects.
00529 """
00530 if isinstance(chain, cls):
00531 chain = (chain,)
00532 return tuple(x for x in chain if x is not None)
00533
00534 class PKCS10(DER_object):
00535 """
00536 Class to hold a PKCS #10 request.
00537 """
00538
00539 formats = ("DER", "POWpkix")
00540 pem_converter = PEM_converter("CERTIFICATE REQUEST")
00541
00542 def get_DER(self):
00543 """
00544 Get the DER value of this certification request.
00545 """
00546 assert not self.empty()
00547 if self.DER:
00548 return self.DER
00549 if self.POWpkix:
00550 self.DER = self.POWpkix.toString()
00551 return self.get_DER()
00552 raise rpki.exceptions.DERObjectConversionError, "No conversion path to DER available"
00553
00554 def get_POWpkix(self):
00555 """
00556 Get the POW.pkix value of this certification request.
00557 """
00558 assert not self.empty()
00559 if not self.POWpkix:
00560 req = POW.pkix.CertificationRequest()
00561 req.fromString(self.get_DER())
00562 self.POWpkix = req
00563 return self.POWpkix
00564
00565 def getPublicKey(self):
00566 """Extract the public key from this certification request."""
00567 return RSApublic(DER = self.get_POWpkix().certificationRequestInfo.subjectPublicKeyInfo.toString())
00568
00569 def check_valid_rpki(self):
00570 """
00571 Check this certification request to see whether it's a valid
00572 request for an RPKI certificate. This is broken out of the
00573 up-down protocol code because it's somewhat involved and the
00574 up-down code doesn't need to know the details.
00575
00576 Throws an exception if the request isn't valid, so if this method
00577 returns at all, the request is ok.
00578 """
00579
00580 if not self.get_POWpkix().verify():
00581 raise rpki.exceptions.BadPKCS10, "Signature check failed"
00582
00583 if self.get_POWpkix().certificationRequestInfo.version.get() != 0:
00584 raise rpki.exceptions.BadPKCS10, \
00585 "Bad version number %s" % self.get_POWpkix().certificationRequestInfo.version
00586
00587 if rpki.oids.oid2name.get(self.get_POWpkix().signatureAlgorithm.algorithm.get()) \
00588 not in ("sha256WithRSAEncryption", "sha384WithRSAEncryption", "sha512WithRSAEncryption"):
00589 raise rpki.exceptions.BadPKCS10, "Bad signature algorithm %s" % self.get_POWpkix().signatureAlgorithm
00590
00591 exts = self.get_POWpkix().getExtensions()
00592 for oid, critical, value in exts:
00593 if rpki.oids.oid2name.get(oid) not in ("basicConstraints", "keyUsage", "subjectInfoAccess"):
00594 raise rpki.exceptions.BadExtension, "Forbidden extension %s" % oid
00595 req_exts = dict((rpki.oids.oid2name[oid], value) for (oid, critical, value) in exts)
00596
00597 if "basicConstraints" not in req_exts or not req_exts["basicConstraints"][0]:
00598 raise rpki.exceptions.BadPKCS10, "request for EE cert not allowed here"
00599
00600 if req_exts["basicConstraints"][1] is not None:
00601 raise rpki.exceptions.BadPKCS10, "basicConstraints must not specify Path Length"
00602
00603 if "keyUsage" in req_exts and (not req_exts["keyUsage"][5] or not req_exts["keyUsage"][6]):
00604 raise rpki.exceptions.BadPKCS10, "keyUsage doesn't match basicConstraints"
00605
00606 for method, location in req_exts.get("subjectInfoAccess", ()):
00607 if rpki.oids.oid2name.get(method) == "id-ad-caRepository" and \
00608 (location[0] != "uri" or (location[1].startswith("rsync://") and not location[1].endswith("/"))):
00609 raise rpki.exceptions.BadPKCS10, "Certificate request includes bad SIA component: %r" % location
00610
00611
00612
00613 assert "subjectInfoAccess" in req_exts, "Can't (yet) handle PKCS #10 without an SIA extension"
00614
00615 @classmethod
00616 def create_ca(cls, keypair, sia = None):
00617 """
00618 Create a new request for a given keypair, including given SIA value.
00619 """
00620 exts = [["basicConstraints", True, (1, None)],
00621 ["keyUsage", True, (0, 0, 0, 0, 0, 1, 1)]]
00622 if sia is not None:
00623 exts.append(["subjectInfoAccess", False, sia])
00624 for x in exts:
00625 x[0] = rpki.oids.name2oid[x[0]]
00626 return cls.create(keypair, exts)
00627
00628 @classmethod
00629 def create(cls, keypair, exts = None):
00630 """
00631 Create a new request for a given keypair, including given extensions.
00632 """
00633 cn = "".join(("%02X" % ord(i) for i in keypair.get_SKI()))
00634 req = POW.pkix.CertificationRequest()
00635 req.certificationRequestInfo.version.set(0)
00636 req.certificationRequestInfo.subject.set((((rpki.oids.name2oid["commonName"],
00637 ("printableString", cn)),),))
00638 if exts is not None:
00639 req.setExtensions(exts)
00640 req.sign(keypair.get_POW(), POW.SHA256_DIGEST)
00641 return cls(POWpkix = req)
00642
00643 class RSA(DER_object):
00644 """
00645 Class to hold an RSA key pair.
00646 """
00647
00648 formats = ("DER", "POW")
00649 pem_converter = PEM_converter("RSA PRIVATE KEY")
00650
00651 def get_DER(self):
00652 """
00653 Get the DER value of this keypair.
00654 """
00655 assert not self.empty()
00656 if self.DER:
00657 return self.DER
00658 if self.POW:
00659 self.DER = self.POW.derWrite(POW.RSA_PRIVATE_KEY)
00660 return self.get_DER()
00661 raise rpki.exceptions.DERObjectConversionError, "No conversion path to DER available"
00662
00663 def get_POW(self):
00664 """
00665 Get the POW value of this keypair.
00666 """
00667 assert not self.empty()
00668 if not self.POW:
00669 self.POW = POW.derRead(POW.RSA_PRIVATE_KEY, self.get_DER())
00670 return self.POW
00671
00672 @classmethod
00673 def generate(cls, keylength = 2048):
00674 """
00675 Generate a new keypair.
00676 """
00677 rpki.log.debug("Generating new %d-bit RSA key" % keylength)
00678 return cls(POW = POW.Asymmetric(POW.RSA_CIPHER, keylength))
00679
00680 def get_public_DER(self):
00681 """Get the DER encoding of the public key from this keypair."""
00682 return self.get_POW().derWrite(POW.RSA_PUBLIC_KEY)
00683
00684 def get_SKI(self):
00685 """Calculate the SKI of this keypair."""
00686 return calculate_SKI(self.get_public_DER())
00687
00688 def get_RSApublic(self):
00689 """Convert the public key of this keypair into a RSApublic object."""
00690 return RSApublic(DER = self.get_public_DER())
00691
00692 class RSApublic(DER_object):
00693 """
00694 Class to hold an RSA public key.
00695 """
00696
00697 formats = ("DER", "POW")
00698 pem_converter = PEM_converter("RSA PUBLIC KEY")
00699
00700 def get_DER(self):
00701 """
00702 Get the DER value of this public key.
00703 """
00704 assert not self.empty()
00705 if self.DER:
00706 return self.DER
00707 if self.POW:
00708 self.DER = self.POW.derWrite(POW.RSA_PUBLIC_KEY)
00709 return self.get_DER()
00710 raise rpki.exceptions.DERObjectConversionError, "No conversion path to DER available"
00711
00712 def get_POW(self):
00713 """
00714 Get the POW value of this public key.
00715 """
00716 assert not self.empty()
00717 if not self.POW:
00718 self.POW = POW.derRead(POW.RSA_PUBLIC_KEY, self.get_DER())
00719 return self.POW
00720
00721 def get_SKI(self):
00722 """Calculate the SKI of this public key."""
00723 return calculate_SKI(self.get_DER())
00724
00725 def POWify_OID(oid):
00726 """
00727 Utility function to convert tuple form of an OID to the
00728 dotted-decimal string form that POW uses.
00729 """
00730 if isinstance(oid, str):
00731 return POWify_OID(rpki.oids.name2oid[oid])
00732 else:
00733 return ".".join(str(i) for i in oid)
00734
00735 class CMS_object(DER_object):
00736 """
00737 Class to hold a CMS-wrapped object.
00738
00739 CMS-wrapped objects are a little different from the other DER_object
00740 types because the signed object is CMS wrapping inner content that's
00741 also ASN.1, and due to our current minimal support for CMS we can't
00742 just handle this as a pretty composite object. So, for now anyway,
00743 a CMS_object is the outer CMS wrapped object so that the usual DER
00744 and PEM operations do the obvious things, and the inner content is
00745 handle via separate methods.
00746 """
00747
00748 formats = ("DER", "POW")
00749 other_clear = ("content",)
00750 econtent_oid = POWify_OID("id-data")
00751 pem_converter = PEM_converter("CMS")
00752
00753
00754
00755
00756 dump_on_verify_failure = True
00757
00758
00759
00760
00761 debug_cms_certs = False
00762
00763
00764
00765
00766
00767
00768 require_crls = False
00769
00770
00771
00772
00773
00774 print_on_der_error = True
00775
00776 def get_DER(self):
00777 """
00778 Get the DER value of this CMS_object.
00779 """
00780 assert not self.empty()
00781 if self.DER:
00782 return self.DER
00783 if self.POW:
00784 self.DER = self.POW.derWrite()
00785 return self.get_DER()
00786 raise rpki.exceptions.DERObjectConversionError, "No conversion path to DER available"
00787
00788 def get_POW(self):
00789 """
00790 Get the POW value of this CMS_object.
00791 """
00792 assert not self.empty()
00793 if not self.POW:
00794 self.POW = POW.derRead(POW.CMS_MESSAGE, self.get_DER())
00795 return self.POW
00796
00797 def get_content(self):
00798 """
00799 Get the inner content of this CMS_object.
00800 """
00801 if self.content is None:
00802 raise rpki.exceptions.CMSContentNotSet, "Inner content of CMS object %r is not set" % self
00803 return self.content
00804
00805 def set_content(self, content):
00806 """
00807 Set the (inner) content of this CMS_object, clearing the wrapper.
00808 """
00809 self.clear()
00810 self.content = content
00811
00812 def verify(self, ta):
00813 """
00814 Verify CMS wrapper and store inner content.
00815 """
00816
00817 try:
00818 cms = self.get_POW()
00819 except (rpki.async.ExitNow, SystemExit):
00820 raise
00821 except:
00822 if self.print_on_der_error:
00823 rpki.log.debug("Problem parsing DER CMS message, might not really be DER: %r" % self.get_DER())
00824 raise rpki.exceptions.UnparsableCMSDER
00825
00826 if cms.eContentType() != self.econtent_oid:
00827 raise rpki.exceptions.WrongEContentType, "Got CMS eContentType %s, expected %s" % (cms.eContentType(), self.econtent_oid)
00828
00829 certs = [X509(POW = x) for x in cms.certs()]
00830 crls = [CRL(POW = c) for c in cms.crls()]
00831
00832 if self.debug_cms_certs:
00833 for x in certs:
00834 rpki.log.debug("Received CMS cert issuer %s subject %s SKI %s" % (x.getIssuer(), x.getSubject(), x.hSKI()))
00835 for c in crls:
00836 rpki.log.debug("Received CMS CRL issuer %r" % (c.getIssuer(),))
00837
00838 store = POW.X509Store()
00839
00840 trusted_ee = None
00841
00842 for x in X509.normalize_chain(ta):
00843 if self.debug_cms_certs:
00844 rpki.log.debug("CMS trusted cert issuer %s subject %s SKI %s" % (x.getIssuer(), x.getSubject(), x.hSKI()))
00845 if not x.is_CA():
00846 assert trusted_ee is None, "Can't have two EE certs in the same validation chain"
00847 trusted_ee = x
00848 store.addTrust(x.get_POW())
00849
00850 if trusted_ee:
00851 if self.debug_cms_certs:
00852 rpki.log.debug("Trusted CMS EE cert issuer %s subject %s SKI %s" % (trusted_ee.getIssuer(), trusted_ee.getSubject(), trusted_ee.hSKI()))
00853 if certs and (len(certs) > 1 or certs[0] != trusted_ee):
00854 raise rpki.exceptions.UnexpectedCMSCerts, certs
00855 if crls:
00856 raise rpki.exceptions.UnexpectedCMSCRLs, crls
00857 else:
00858 if not certs:
00859 raise rpki.exceptions.MissingCMSEEcert, certs
00860 if len(certs) > 1 or certs[0].is_CA():
00861 raise rpki.exceptions.UnexpectedCMSCerts, certs
00862 if not crls:
00863 if self.require_crls:
00864 raise rpki.exceptions.MissingCMSCRL, crls
00865 else:
00866 rpki.log.warn("MISSING CMS CRL! Ignoring per self.require_crls setting")
00867 if len(crls) > 1:
00868 raise rpki.exceptions.UnexpectedCMSCRLs, crls
00869
00870 try:
00871 content = cms.verify(store)
00872 except (rpki.async.ExitNow, SystemExit):
00873 raise
00874 except:
00875 if self.dump_on_verify_failure:
00876 if True:
00877 dbg = self.dumpasn1()
00878 else:
00879 dbg = cms.pprint()
00880 sys.stderr.write("CMS verification failed, dumping ASN.1 (%d octets):\n%s\n" % (len(self.get_DER()), dbg))
00881 raise rpki.exceptions.CMSVerificationFailed, "CMS verification failed"
00882
00883 self.decode(content)
00884 return self.get_content()
00885
00886 def extract(self):
00887 """
00888 Extract and store inner content from CMS wrapper without verifying
00889 the CMS.
00890
00891 DANGER WILL ROBINSON!!!
00892
00893 Do not use this method on unvalidated data. Use the verify()
00894 method instead.
00895
00896 If you don't understand this warning, don't use this method.
00897 """
00898
00899 try:
00900 cms = self.get_POW()
00901 except (rpki.async.ExitNow, SystemExit):
00902 raise
00903 except:
00904 raise rpki.exceptions.UnparsableCMSDER
00905
00906 if cms.eContentType() != self.econtent_oid:
00907 raise rpki.exceptions.WrongEContentType, "Got CMS eContentType %s, expected %s" % (cms.eContentType(), self.econtent_oid)
00908
00909 content = cms.verify(POW.X509Store(), None, POW.CMS_NOCRL | POW.CMS_NO_SIGNER_CERT_VERIFY | POW.CMS_NO_ATTR_VERIFY | POW.CMS_NO_CONTENT_VERIFY)
00910
00911 self.decode(content)
00912 return self.get_content()
00913
00914 def sign(self, keypair, certs, crls = None, no_certs = False):
00915 """
00916 Sign and wrap inner content.
00917 """
00918
00919 rpki.log.trace()
00920
00921 if isinstance(certs, X509):
00922 cert = certs
00923 certs = ()
00924 else:
00925 cert = certs[0]
00926 certs = certs[1:]
00927
00928 if crls is None:
00929 crls = ()
00930 elif isinstance(crls, CRL):
00931 crls = (crls,)
00932
00933 if self.debug_cms_certs:
00934 rpki.log.debug("Signing with cert issuer %s subject %s SKI %s" % (cert.getIssuer(), cert.getSubject(), cert.hSKI()))
00935 for i, c in enumerate(certs):
00936 rpki.log.debug("Additional cert %d issuer %s subject %s SKI %s" % (i, c.getIssuer(), c.getSubject(), c.hSKI()))
00937
00938 cms = POW.CMS()
00939
00940 cms.sign(cert.get_POW(),
00941 keypair.get_POW(),
00942 self.encode(),
00943 [x.get_POW() for x in certs],
00944 [c.get_POW() for c in crls],
00945 self.econtent_oid,
00946 POW.CMS_NOCERTS if no_certs else 0)
00947
00948 self.POW = cms
00949
00950 class DER_CMS_object(CMS_object):
00951 """
00952 Class to hold CMS objects with DER-based content.
00953 """
00954
00955 def encode(self):
00956 """Encode inner content for signing."""
00957 return self.get_content().toString()
00958
00959 def decode(self, der):
00960 """
00961 Decode DER and set inner content.
00962 """
00963 obj = self.content_class()
00964 obj.fromString(der)
00965 self.content = obj
00966
00967 class SignedManifest(DER_CMS_object):
00968 """
00969 Class to hold a signed manifest.
00970 """
00971
00972 pem_converter = PEM_converter("RPKI MANIFEST")
00973 content_class = rpki.manifest.Manifest
00974 econtent_oid = POWify_OID("id-ct-rpkiManifest")
00975
00976 def getThisUpdate(self):
00977 """Get thisUpdate value from this manifest."""
00978 return rpki.sundial.datetime.fromGeneralizedTime(self.get_content().thisUpdate.get())
00979
00980 def getNextUpdate(self):
00981 """Get nextUpdate value from this manifest."""
00982 return rpki.sundial.datetime.fromGeneralizedTime(self.get_content().nextUpdate.get())
00983
00984 @classmethod
00985 def build(cls, serial, thisUpdate, nextUpdate, names_and_objs, keypair, certs, version = 0):
00986 """
00987 Build a signed manifest.
00988 """
00989 self = cls()
00990 filelist = []
00991 for name, obj in names_and_objs:
00992 d = POW.Digest(POW.SHA256_DIGEST)
00993 d.update(obj.get_DER())
00994 filelist.append((name.rpartition("/")[2], d.digest()))
00995 filelist.sort(key = lambda x: x[0])
00996 m = rpki.manifest.Manifest()
00997 m.version.set(version)
00998 m.manifestNumber.set(serial)
00999 m.thisUpdate.set(thisUpdate.toGeneralizedTime())
01000 m.nextUpdate.set(nextUpdate.toGeneralizedTime())
01001 m.fileHashAlg.set(rpki.oids.name2oid["id-sha256"])
01002 m.fileList.set(filelist)
01003 self.set_content(m)
01004 self.sign(keypair, certs)
01005 return self
01006
01007 class ROA(DER_CMS_object):
01008 """
01009 Class to hold a signed ROA.
01010 """
01011
01012 pem_converter = PEM_converter("ROUTE ORIGIN ATTESTATION")
01013 content_class = rpki.roa.RouteOriginAttestation
01014 econtent_oid = POWify_OID("id-ct-routeOriginAttestation")
01015
01016 @classmethod
01017 def build(cls, asn, ipv4, ipv6, keypair, certs, version = 0):
01018 """
01019 Build a ROA.
01020 """
01021 try:
01022 self = cls()
01023 r = rpki.roa.RouteOriginAttestation()
01024 r.version.set(version)
01025 r.asID.set(asn)
01026 r.ipAddrBlocks.set((a.to_roa_tuple() for a in (ipv4, ipv6) if a))
01027 self.set_content(r)
01028 self.sign(keypair, certs)
01029 return self
01030 except POW.pkix.DerError, e:
01031 rpki.log.debug("Encoding error while generating ROA %r: %s" % (self, e))
01032 rpki.log.debug("ROA inner content: %r" % (r.get(),))
01033 raise
01034
01035 class DeadDrop(object):
01036 """
01037 Dead-drop utility for storing copies of CMS messages for debugging or
01038 audit. At the moment this uses Maildir mailbox format, as it has
01039 approximately the right properties and a number of useful tools for
01040 manipulating it already exist.
01041 """
01042
01043 def __init__(self, name):
01044 self.maildir = mailbox.Maildir(name, factory = None, create = True)
01045 self.pid = os.getpid()
01046
01047 def dump(self, obj):
01048 now = time.time()
01049 msg = email.mime.application.MIMEApplication(obj.get_DER(), "x-rpki")
01050 msg["Date"] = email.utils.formatdate(now)
01051 msg["Subject"] = "Process %s dump of %r" % (self.pid, obj)
01052 msg["Message-ID"] = email.utils.make_msgid()
01053 msg["X-RPKI-PID"] = str(self.pid)
01054 msg["X-RPKI-Object"] = repr(obj)
01055 msg["X-RPKI-Timestamp"] = "%f" % now
01056 self.maildir.add(msg)
01057
01058 class XML_CMS_object(CMS_object):
01059 """
01060 Class to hold CMS-wrapped XML protocol data.
01061 """
01062
01063 econtent_oid = POWify_OID("id-ct-xml")
01064
01065
01066
01067
01068
01069 dump_outbound_cms = None
01070
01071
01072
01073
01074
01075 dump_inbound_cms = None
01076
01077 def encode(self):
01078 """Encode inner content for signing."""
01079 return lxml.etree.tostring(self.get_content(), pretty_print = True, encoding = self.encoding, xml_declaration = True)
01080
01081 def decode(self, xml):
01082 """Decode XML and set inner content."""
01083 self.content = lxml.etree.fromstring(xml)
01084
01085 def pretty_print_content(self):
01086 """Pretty print XML content of this message."""
01087 return lxml.etree.tostring(self.get_content(), pretty_print = True, encoding = self.encoding, xml_declaration = True)
01088
01089 def schema_check(self):
01090 """
01091 Handle XML RelaxNG schema check.
01092 """
01093 try:
01094 self.schema.assertValid(self.get_content())
01095 except lxml.etree.DocumentInvalid:
01096 rpki.log.error("PDU failed schema check: " + self.pretty_print_content())
01097 raise
01098
01099 def dump_to_disk(self, prefix):
01100 """
01101 Write DER of current message to disk, for debugging.
01102 """
01103 f = open(prefix + rpki.sundial.now().isoformat() + "Z.cms", "wb")
01104 f.write(self.get_DER())
01105 f.close()
01106
01107 @classmethod
01108 def wrap(cls, msg, keypair, certs, crls = None, pretty_print = False):
01109 """
01110 Build a CMS-wrapped XML PDU and return its DER encoding.
01111 """
01112 rpki.log.trace()
01113 self = cls()
01114 self.set_content(msg.toXML())
01115 self.schema_check()
01116 self.sign(keypair, certs, crls)
01117 if self.dump_outbound_cms:
01118 self.dump_outbound_cms.dump(self)
01119 if pretty_print:
01120 return self.get_DER(), self.pretty_print_content()
01121 else:
01122 return self.get_DER()
01123
01124 @classmethod
01125 def unwrap(cls, der, ta, pretty_print = False):
01126 """
01127 Unwrap a CMS-wrapped XML PDU and return Python objects.
01128 """
01129 self = cls(DER = der)
01130 if self.dump_inbound_cms:
01131 self.dump_inbound_cms.dump(self)
01132 self.verify(ta)
01133 self.schema_check()
01134 msg = self.saxify(self.get_content())
01135 if pretty_print:
01136 return msg, self.pretty_print_content()
01137 else:
01138 return msg
01139
01140 class CRL(DER_object):
01141 """
01142 Class to hold a Certificate Revocation List.
01143 """
01144
01145 formats = ("DER", "POW", "POWpkix")
01146 pem_converter = PEM_converter("X509 CRL")
01147
01148 def get_DER(self):
01149 """
01150 Get the DER value of this CRL.
01151 """
01152 assert not self.empty()
01153 if self.DER:
01154 return self.DER
01155 if self.POW:
01156 self.DER = self.POW.derWrite()
01157 return self.get_DER()
01158 if self.POWpkix:
01159 self.DER = self.POWpkix.toString()
01160 return self.get_DER()
01161 raise rpki.exceptions.DERObjectConversionError, "No conversion path to DER available"
01162
01163 def get_POW(self):
01164 """
01165 Get the POW value of this CRL.
01166 """
01167 assert not self.empty()
01168 if not self.POW:
01169 self.POW = POW.derRead(POW.X509_CRL, self.get_DER())
01170 return self.POW
01171
01172 def get_POWpkix(self):
01173 """
01174 Get the POW.pkix value of this CRL.
01175 """
01176 assert not self.empty()
01177 if not self.POWpkix:
01178 crl = POW.pkix.CertificateList()
01179 crl.fromString(self.get_DER())
01180 self.POWpkix = crl
01181 return self.POWpkix
01182
01183 def getThisUpdate(self):
01184 """Get thisUpdate value from this CRL."""
01185 return rpki.sundial.datetime.fromASN1tuple(self.get_POWpkix().getThisUpdate())
01186
01187 def getNextUpdate(self):
01188 """Get nextUpdate value from this CRL."""
01189 return rpki.sundial.datetime.fromASN1tuple(self.get_POWpkix().getNextUpdate())
01190
01191 def getIssuer(self):
01192 """Get issuer value of this CRL."""
01193 return self.get_POW().getIssuer()
01194
01195 @classmethod
01196 def generate(cls, keypair, issuer, serial, thisUpdate, nextUpdate, revokedCertificates, version = 1, digestType = "sha256WithRSAEncryption"):
01197 """
01198 Generate a new CRL.
01199 """
01200 crl = POW.pkix.CertificateList()
01201 crl.setVersion(version)
01202 crl.setIssuer(issuer.get_POWpkix().getSubject())
01203 crl.setThisUpdate(thisUpdate.toASN1tuple())
01204 crl.setNextUpdate(nextUpdate.toASN1tuple())
01205 if revokedCertificates:
01206 crl.setRevokedCertificates(revokedCertificates)
01207 crl.setExtensions(
01208 ((rpki.oids.name2oid["authorityKeyIdentifier"], False, (issuer.get_SKI(), (), None)),
01209 (rpki.oids.name2oid["cRLNumber"], False, serial)))
01210 crl.sign(keypair.get_POW(), digestType)
01211 return cls(POWpkix = crl)