aboutsummaryrefslogtreecommitdiff
path: root/rpkid/rpki/x509.py
diff options
context:
space:
mode:
Diffstat (limited to 'rpkid/rpki/x509.py')
-rw-r--r--rpkid/rpki/x509.py260
1 files changed, 236 insertions, 24 deletions
diff --git a/rpkid/rpki/x509.py b/rpkid/rpki/x509.py
index 58129363..7bbb47bc 100644
--- a/rpkid/rpki/x509.py
+++ b/rpkid/rpki/x509.py
@@ -47,6 +47,7 @@ import rpki.POW, rpki.POW.pkix, base64, lxml.etree, os, subprocess, sys
import email.mime.application, email.utils, mailbox, time
import rpki.exceptions, rpki.resource_set, rpki.oids, rpki.sundial
import rpki.manifest, rpki.roa, rpki.log, rpki.async, rpki.ghostbuster
+import rpki.relaxng
def base64_with_linebreaks(der):
"""
@@ -120,6 +121,74 @@ def _find_xia_uri(extension, name):
return location[1]
return None
+class X501DN(object):
+ """
+ Class to hold an X.501 Distinguished Name.
+
+ This is nothing like a complete implementation, just enough for our
+ purposes. POW has one interface to this, POW.pkix has another. In
+ terms of completeness in the Python representation, the POW.pkix
+ representation is much closer to right, but the whole thing is a
+ horrible mess.
+
+ See RFC 5280 4.1.2.4 for the ASN.1 details. In brief:
+
+ - A DN is a SEQUENCE of RDNs.
+
+ - A RDN is a set of AttributeAndValues; in practice, multi-value
+ RDNs are rare, so an RDN is almost always a set with a single
+ element.
+
+ - An AttributeAndValue is an OID and a value, where a whole bunch
+ of things including both syntax and semantics of the value are
+ determined by the OID.
+
+ - The value is some kind of ASN.1 string; there are far too many
+ encoding options options, most of which are either strongly
+ discouraged or outright forbidden by the PKIX profile, but which
+ persist for historical reasons. The only ones PKIX actually
+ likes are PrintableString and UTF8String, but there are nuances
+ and special cases where some of the others are required.
+
+ The RPKI profile further restricts DNs to a single mandatory
+ CommonName attribute with a single optional SerialNumber attribute
+ (not to be confused with the certificate serial number).
+
+ BPKI certificates should (we hope) follow the general PKIX guideline
+ but the ones we construct ourselves are likely to be relatively
+ simple.
+
+ The main purpose of this class is to hide as much as possible of
+ this mess from code that has to work with these wretched things.
+ """
+
+ def __init__(self, ini = None, **kwargs):
+ assert ini is None or not kwargs
+ if len(kwargs) == 1 and "CN" in kwargs:
+ ini = kwargs.pop("CN")
+ if isinstance(ini, (str, unicode)):
+ self.dn = (((rpki.oids.name2oid["commonName"], ("printableString", ini)),),)
+ elif isinstance(ini, tuple):
+ self.dn = ini
+ elif kwargs:
+ raise NotImplementedError("Sorry, I haven't implemented keyword arguments yet")
+ elif ini is not None:
+ raise TypeError("Don't know how to interpret %r as an X.501 DN" % (ini,), ini)
+
+ def __str__(self):
+ return "".join("/" + "+".join("%s=%s" % (rpki.oids.oid2name[a[0]], a[1][1])
+ for a in rdn)
+ for rdn in self.dn)
+
+ def __cmp__(self, other):
+ return cmp(self.dn, other.dn)
+
+ def get_POWpkix(self):
+ return self.dn
+
+ def get_POW(self):
+ raise NotImplementedError("Sorry, I haven't written the conversion to POW format yet")
+
class DER_object(object):
"""
Virtual class to hold a generic DER object.
@@ -259,6 +328,8 @@ class DER_object(object):
return -1
elif other is None:
return 1
+ elif isinstance(other, str):
+ return cmp(self.get_DER(), other)
else:
return cmp(self.get_DER(), other.get_DER())
@@ -456,13 +527,13 @@ class X509(DER_object):
"""
Get the issuer of this certificate.
"""
- return "".join("/%s=%s" % rdn for rdn in self.get_POW().getIssuer())
+ return X501DN(self.get_POWpkix().getIssuer())
def getSubject(self):
"""
Get the subject of this certificate.
"""
- return "".join("/%s=%s" % rdn for rdn in self.get_POW().getSubject())
+ return X501DN(self.get_POWpkix().getSubject())
def getNotBefore(self):
"""
@@ -497,11 +568,60 @@ class X509(DER_object):
def issue(self, keypair, subject_key, serial, sia, aia, crldp, notAfter,
cn = None, resources = None, is_ca = True):
"""
- Issue a certificate.
+ Issue an RPKI certificate.
+ """
+
+ assert aia is not None and crldp is not None
+
+ return self._issue(
+ keypair = keypair,
+ subject_key = subject_key,
+ serial = serial,
+ sia = sia,
+ aia = aia,
+ crldp = crldp,
+ notAfter = notAfter,
+ cn = cn,
+ resources = resources,
+ is_ca = is_ca,
+ aki = self.get_SKI(),
+ issuer_name = self.get_POWpkix().getSubject())
+
+
+ @classmethod
+ def self_certify(cls, keypair, subject_key, serial, sia, notAfter,
+ cn = None, resources = None):
+ """
+ Generate a self-certified RPKI certificate.
+ """
+
+ ski = subject_key.get_SKI()
+ if cn is None:
+ cn = "".join(("%02X" % ord(i) for i in ski))
+
+ return cls._issue(
+ keypair = keypair,
+ subject_key = subject_key,
+ serial = serial,
+ sia = sia,
+ aia = None,
+ crldp = None,
+ notAfter = notAfter,
+ cn = cn,
+ resources = resources,
+ is_ca = True,
+ aki = ski,
+ issuer_name = (((rpki.oids.name2oid["commonName"], ("printableString", cn)),),))
+
+
+ @staticmethod
+ def _issue(keypair, subject_key, serial, sia, aia, crldp, notAfter,
+ cn, resources, is_ca, aki, issuer_name):
+ """
+ Common code to issue an RPKI certificate.
"""
now = rpki.sundial.now()
- aki = self.get_SKI()
ski = subject_key.get_SKI()
if cn is None:
@@ -512,7 +632,7 @@ class X509(DER_object):
cert = rpki.POW.pkix.Certificate()
cert.setVersion(2)
cert.setSerial(serial)
- cert.setIssuer(self.get_POWpkix().getSubject())
+ cert.setIssuer(issuer_name)
cert.setSubject((((rpki.oids.name2oid["commonName"], ("printableString", cn)),),))
cert.setNotBefore(now.toASN1tuple())
cert.setNotAfter(notAfter.toASN1tuple())
@@ -520,10 +640,15 @@ class X509(DER_object):
exts = [ ["subjectKeyIdentifier", False, ski],
["authorityKeyIdentifier", False, (aki, (), None)],
- ["cRLDistributionPoints", False, ((("fullName", (("uri", crldp),)), None, ()),)],
- ["authorityInfoAccess", False, ((rpki.oids.name2oid["id-ad-caIssuers"], ("uri", aia)),)],
["certificatePolicies", True, ((rpki.oids.name2oid["id-cp-ipAddr-asNumber"], ()),)] ]
+
+ if crldp is not None:
+ exts.append(["cRLDistributionPoints", False, ((("fullName", (("uri", crldp),)), None, ()),)])
+
+ if aia is not None:
+ exts.append(["authorityInfoAccess", False, ((rpki.oids.name2oid["id-ad-caIssuers"], ("uri", aia)),)])
+
if is_ca:
exts.append(["basicConstraints", True, (1, None)])
exts.append(["keyUsage", True, (0, 0, 0, 0, 0, 1, 1)])
@@ -555,33 +680,96 @@ class X509(DER_object):
return X509(POWpkix = cert)
- def cross_certify(self, keypair, source_cert, serial, notAfter, now = None, pathLenConstraint = 0):
+ def bpki_cross_certify(self, keypair, source_cert, serial, notAfter,
+ now = None, pathLenConstraint = 0):
"""
- Issue a certificate with values taking from an existing certificate.
- This is used to construct some kinds oF BPKI certificates.
+ Issue a BPKI certificate with values taking from an existing certificate.
+ """
+ return self.bpki_certify(
+ keypair = keypair,
+ subject_name = source_cert.getSubject(),
+ subject_key = source_cert.getPublicKey(),
+ serial = serial,
+ notAfter = notAfter,
+ now = now,
+ pathLenConstraint = pathLenConstraint,
+ is_ca = True)
+
+ @classmethod
+ def bpki_self_certify(cls, keypair, subject_name, serial, notAfter,
+ now = None, pathLenConstraint = None):
+ """
+ Issue a self-signed BPKI CA certificate.
+ """
+ return cls._bpki_certify(
+ keypair = keypair,
+ issuer_name = subject_name,
+ subject_name = subject_name,
+ subject_key = keypair.get_RSApublic(),
+ serial = serial,
+ now = now,
+ notAfter = notAfter,
+ pathLenConstraint = pathLenConstraint,
+ is_ca = True)
+
+ def bpki_certify(self, keypair, subject_name, subject_key, serial, notAfter, is_ca,
+ now = None, pathLenConstraint = None):
+ """
+ Issue a normal BPKI certificate.
+ """
+ assert keypair.get_RSApublic() == self.getPublicKey()
+ return self._bpki_certify(
+ keypair = keypair,
+ issuer_name = self.getSubject(),
+ subject_name = subject_name,
+ subject_key = subject_key,
+ serial = serial,
+ now = now,
+ notAfter = notAfter,
+ pathLenConstraint = pathLenConstraint,
+ is_ca = is_ca)
+
+ @classmethod
+ def _bpki_certify(cls, keypair, issuer_name, subject_name, subject_key,
+ serial, now, notAfter, pathLenConstraint, is_ca):
+ """
+ Issue a BPKI certificate. This internal method does the real
+ work, after one of the wrapper methods has extracted the relevant
+ fields.
"""
if now is None:
now = rpki.sundial.now()
- assert isinstance(pathLenConstraint, int) and pathLenConstraint >= 0
+ issuer_key = keypair.get_RSApublic()
+
+ assert (issuer_key == subject_key) == (issuer_name == subject_name)
+ assert is_ca or issuer_name != subject_name
+ assert is_ca or pathLenConstraint is None
+ assert pathLenConstraint is None or (isinstance(pathLenConstraint, (int, long)) and
+ pathLenConstraint >= 0)
+
+ extensions = [
+ (rpki.oids.name2oid["subjectKeyIdentifier" ], False, subject_key.get_SKI())]
+ if issuer_key != subject_key:
+ extensions.append(
+ (rpki.oids.name2oid["authorityKeyIdentifier"], False, (issuer_key.get_SKI(), (), None)))
+ if is_ca:
+ extensions.append(
+ (rpki.oids.name2oid["basicConstraints" ], True, (1, pathLenConstraint)))
cert = rpki.POW.pkix.Certificate()
cert.setVersion(2)
cert.setSerial(serial)
- cert.setIssuer(self.get_POWpkix().getSubject())
- cert.setSubject(source_cert.get_POWpkix().getSubject())
+ cert.setIssuer(issuer_name.get_POWpkix())
+ cert.setSubject(subject_name.get_POWpkix())
cert.setNotBefore(now.toASN1tuple())
cert.setNotAfter(notAfter.toASN1tuple())
- cert.tbs.subjectPublicKeyInfo.set(
- source_cert.get_POWpkix().tbs.subjectPublicKeyInfo.get())
- cert.setExtensions((
- (rpki.oids.name2oid["subjectKeyIdentifier" ], False, source_cert.get_SKI()),
- (rpki.oids.name2oid["authorityKeyIdentifier"], False, (self.get_SKI(), (), None)),
- (rpki.oids.name2oid["basicConstraints" ], True, (1, 0))))
+ cert.tbs.subjectPublicKeyInfo.fromString(subject_key.get_DER())
+ cert.setExtensions(extensions)
cert.sign(keypair.get_POW(), rpki.POW.SHA256_DIGEST)
- return X509(POWpkix = cert)
+ return cls(POWpkix = cert)
@classmethod
def normalize_chain(cls, chain):
@@ -628,6 +816,12 @@ class PKCS10(DER_object):
self.POWpkix = req
return self.POWpkix
+ def getSubject(self):
+ """
+ Extract the subject name from this certification request.
+ """
+ return X501DN(self.get_POWpkix().certificationRequestInfo.subject.get())
+
def getPublicKey(self):
"""
Extract the public key from this certification request.
@@ -955,7 +1149,7 @@ class CMS_object(DER_object):
if certs and (len(certs) > 1 or certs[0].getSubject() != trusted_ee.getSubject() or certs[0].getPublicKey() != trusted_ee.getPublicKey()):
raise rpki.exceptions.UnexpectedCMSCerts # , certs
if crls:
- raise rpki.exceptions.UnexpectedCMSCRLs # , crls
+ rpki.log.warn("Ignoring unexpected CMS CRL%s from trusted peer" % ("" if len(crls) == 1 else "s"))
else:
if not certs:
raise rpki.exceptions.MissingCMSEEcert # , certs
@@ -1246,7 +1440,10 @@ class XML_CMS_object(CMS_object):
Wrap an XML PDU in CMS and return its DER encoding.
"""
rpki.log.trace()
- self.set_content(msg.toXML())
+ if self.saxify is None:
+ self.set_content(msg)
+ else:
+ self.set_content(msg.toXML())
self.schema_check()
self.sign(keypair, certs, crls)
if self.dump_outbound_cms:
@@ -1261,7 +1458,22 @@ class XML_CMS_object(CMS_object):
self.dump_inbound_cms.dump(self)
self.verify(ta)
self.schema_check()
- return self.saxify(self.get_content())
+ if self.saxify is None:
+ return self.get_content()
+ else:
+ return self.saxify(self.get_content())
+
+ ## @var saxify
+ # SAX handler hook. Subclasses can set this to a SAX handler, in
+ # which case .unwrap() will call it and return the result.
+ # Otherwise, .unwrap() just returns a verified element tree.
+
+ saxify = None
+
+class SignedReferral(XML_CMS_object):
+ encoding = "us-ascii"
+ schema = rpki.relaxng.myrpki
+ saxify = None
class Ghostbuster(CMS_object):
"""
@@ -1357,7 +1569,7 @@ class CRL(DER_object):
"""
Get issuer value of this CRL.
"""
- return "".join("/%s=%s" % rdn for rdn in self.get_POW().getIssuer())
+ return X501DN(self.get_POWpkix().getIssuer())
@classmethod
def generate(cls, keypair, issuer, serial, thisUpdate, nextUpdate, revokedCertificates, version = 1, digestType = "sha256WithRSAEncryption"):