diff options
Diffstat (limited to 'rpki/rootd.py')
-rw-r--r-- | rpki/rootd.py | 403 |
1 files changed, 236 insertions, 167 deletions
diff --git a/rpki/rootd.py b/rpki/rootd.py index fb445213..987d8356 100644 --- a/rpki/rootd.py +++ b/rpki/rootd.py @@ -18,20 +18,22 @@ # WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. """ -Trivial RPKI up-down protocol root server. Not recommended for -production use. Overrides a bunch of method definitions from the -rpki.* classes in order to reuse as much code as possible. +Trivial RPKI up-down protocol root server. """ import os +import sys import time import logging +import httplib import argparse +import urlparse import rpki.resource_set import rpki.up_down import rpki.left_right import rpki.x509 import rpki.http +import rpki.http_simple import rpki.config import rpki.exceptions import rpki.relaxng @@ -39,106 +41,47 @@ import rpki.sundial import rpki.log import rpki.daemonize +from lxml.etree import Element, SubElement + logger = logging.getLogger(__name__) -rootd = None - -class list_pdu(rpki.up_down.list_pdu): - def serve_pdu(self, q_msg, r_msg, ignored, callback, errback): - r_msg.payload = rpki.up_down.list_response_pdu() - rootd.compose_response(r_msg) - callback() - -class issue_pdu(rpki.up_down.issue_pdu): - def serve_pdu(self, q_msg, r_msg, ignored, callback, errback): - self.pkcs10.check_valid_request_ca() - r_msg.payload = rpki.up_down.issue_response_pdu() - rootd.compose_response(r_msg, self.pkcs10) - callback() - -class revoke_pdu(rpki.up_down.revoke_pdu): - def serve_pdu(self, q_msg, r_msg, ignored, callback, errback): - logger.debug("Revocation requested for SKI %s", self.ski) - subject_cert = rootd.get_subject_cert() - if subject_cert is None: - logger.debug("No subject certificate, nothing to revoke") - raise rpki.exceptions.NotInDatabase - if subject_cert.gSKI() != self.ski: - logger.debug("Subject certificate has different SKI %s, not revoking", subject_cert.gSKI()) - raise rpki.exceptions.NotInDatabase - logger.debug("Revoking certificate %s", self.ski) - now = rpki.sundial.now() - rootd.revoke_subject_cert(now) - rootd.del_subject_cert() - rootd.del_subject_pkcs10() - rootd.generate_crl_and_manifest(now) - r_msg.payload = rpki.up_down.revoke_response_pdu() - r_msg.payload.class_name = self.class_name - r_msg.payload.ski = self.ski - callback() - -class error_response_pdu(rpki.up_down.error_response_pdu): - exceptions = rpki.up_down.error_response_pdu.exceptions.copy() - exceptions[rpki.exceptions.ClassNameUnknown, revoke_pdu] = 1301 - exceptions[rpki.exceptions.NotInDatabase, revoke_pdu] = 1302 - -class message_pdu(rpki.up_down.message_pdu): - - name2type = { - "list" : list_pdu, - "list_response" : rpki.up_down.list_response_pdu, - "issue" : issue_pdu, - "issue_response" : rpki.up_down.issue_response_pdu, - "revoke" : revoke_pdu, - "revoke_response" : rpki.up_down.revoke_response_pdu, - "error_response" : error_response_pdu } - - type2name = dict((v, k) for k, v in name2type.items()) - - error_pdu_type = error_response_pdu - - def log_query(self, child): - """ - Log query we're handling. - """ - logger.info("Serving %s query", self.type) - -class sax_handler(rpki.up_down.sax_handler): - pdu = message_pdu - -class cms_msg(rpki.up_down.cms_msg): - saxify = sax_handler.saxify + +class ReplayTracker(object): + """ + Stash for replay protection timestamps. + """ + + def __init__(self): + self.cms_timestamp = None + + class main(object): - def get_root_cert(self): - logger.debug("Read root cert %s", self.rpki_root_cert_file) - self.rpki_root_cert = rpki.x509.X509(Auto_file = self.rpki_root_cert_file) def root_newer_than_subject(self): - return os.stat(self.rpki_root_cert_file).st_mtime > \ - os.stat(os.path.join(self.rpki_root_dir, self.rpki_subject_cert)).st_mtime + return self.rpki_root_cert.mtime > os.stat(self.rpki_subject_cert_file).st_mtime + def get_subject_cert(self): - filename = os.path.join(self.rpki_root_dir, self.rpki_subject_cert) try: - x = rpki.x509.X509(Auto_file = filename) - logger.debug("Read subject cert %s", filename) + x = rpki.x509.X509(Auto_file = self.rpki_subject_cert_file) + logger.debug("Read subject cert %s", self.rpki_subject_cert_file) return x except IOError: return None + def set_subject_cert(self, cert): - filename = os.path.join(self.rpki_root_dir, self.rpki_subject_cert) - logger.debug("Writing subject cert %s, SKI %s", filename, cert.hSKI()) - f = open(filename, "wb") - f.write(cert.get_DER()) - f.close() + logger.debug("Writing subject cert %s, SKI %s", self.rpki_subject_cert_file, cert.hSKI()) + with open(self.rpki_subject_cert_file, "wb") as f: + f.write(cert.get_DER()) + def del_subject_cert(self): - filename = os.path.join(self.rpki_root_dir, self.rpki_subject_cert) - logger.debug("Deleting subject cert %s", filename) - os.remove(filename) + logger.debug("Deleting subject cert %s", self.rpki_subject_cert_file) + os.remove(self.rpki_subject_cert_file) + def get_subject_pkcs10(self): try: @@ -148,11 +91,12 @@ class main(object): except IOError: return None + def set_subject_pkcs10(self, pkcs10): logger.debug("Writing subject PKCS #10 %s", self.rpki_subject_pkcs10) - f = open(self.rpki_subject_pkcs10, "wb") - f.write(pkcs10.get_DER()) - f.close() + with open(self.rpki_subject_pkcs10, "wb") as f: + f.write(pkcs10.get_DER()) + def del_subject_pkcs10(self): logger.debug("Deleting subject PKCS #10 %s", self.rpki_subject_pkcs10) @@ -161,9 +105,11 @@ class main(object): except OSError: pass + def issue_subject_cert_maybe(self, new_pkcs10): now = rpki.sundial.now() subject_cert = self.get_subject_cert() + hash = None if subject_cert is None else rpki.x509.sha256(subject_cert.get_DER()).encode("hex") old_pkcs10 = self.get_subject_pkcs10() if new_pkcs10 is not None and new_pkcs10 != old_pkcs10: self.set_subject_pkcs10(new_pkcs10) @@ -179,17 +125,16 @@ class main(object): logger.debug("Root certificate has changed, regenerating subject") self.revoke_subject_cert(now) subject_cert = None - self.get_root_cert() if subject_cert is not None: - return subject_cert + return subject_cert, None pkcs10 = old_pkcs10 if new_pkcs10 is None else new_pkcs10 if pkcs10 is None: logger.debug("No PKCS #10 request, can't generate subject certificate yet") - return None + return None, None resources = self.rpki_root_cert.get_3779resources() notAfter = now + self.rpki_subject_lifetime logger.info("Generating subject cert %s with resources %s, expires %s", - self.rpki_base_uri + self.rpki_subject_cert, resources, notAfter) + self.rpki_subject_cert_uri, resources, notAfter) req_key = pkcs10.getPublicKey() req_sia = pkcs10.get_SIA() self.next_serial_number() @@ -199,15 +144,22 @@ class main(object): serial = self.serial_number, sia = req_sia, aia = self.rpki_root_cert_uri, - crldp = self.rpki_base_uri + self.rpki_root_crl, + crldp = self.rpki_root_crl_uri, resources = resources, notBefore = now, notAfter = notAfter) self.set_subject_cert(subject_cert) - self.generate_crl_and_manifest(now) - return subject_cert + pubd_msg = Element(rpki.publication.tag_msg, nsmap = rpki.publication.nsmap, + type = "query", version = rpki.publication.version) + pdu = SubElement(pubd_msg, rpki.publication.tag_publish, uri = self.rpki_subject_cert_uri) + pdu.text = subject_cert.get_Base64() + if hash is not None: + pdu.set("hash", hash) + self.generate_crl_and_manifest(now, pubd_msg) + return subject_cert, pubd_msg + - def generate_crl_and_manifest(self, now): + def generate_crl_and_manifest(self, now, pubd_msg): subject_cert = self.get_subject_cert() self.next_serial_number() self.next_crl_number() @@ -220,23 +172,26 @@ class main(object): thisUpdate = now, nextUpdate = now + self.rpki_subject_regen, revokedCertificates = self.revoked) - fn = os.path.join(self.rpki_root_dir, self.rpki_root_crl) - logger.debug("Writing CRL %s", fn) - f = open(fn, "wb") - f.write(crl.get_DER()) - f.close() - manifest_content = [(self.rpki_root_crl, crl)] + hash = self.read_hash_maybe(self.rpki_root_crl_file) + logger.debug("Writing CRL %s", self.rpki_root_crl_file) + with open(self.rpki_root_crl_file, "wb") as f: + f.write(crl.get_DER()) + pdu = SubElement(pubd_msg, rpki.publication.tag_publish, uri = self.rpki_root_crl_uri) + pdu.text = crl.get_Base64() + if hash is not None: + pdu.set("hash", hash) + manifest_content = [(os.path.basename(self.rpki_root_crl_uri), crl)] if subject_cert is not None: - manifest_content.append((self.rpki_subject_cert, subject_cert)) + manifest_content.append((os.path.basename(self.rpki_subject_cert_uri), subject_cert)) manifest_resources = rpki.resource_set.resource_bag.from_inheritance() manifest_keypair = rpki.x509.RSA.generate() manifest_cert = self.rpki_root_cert.issue( keypair = self.rpki_root_key, subject_key = manifest_keypair.get_public(), serial = self.serial_number, - sia = (None, None, self.rpki_base_uri + self.rpki_root_manifest), + sia = (None, None, self.rpki_root_manifest_uri), aia = self.rpki_root_cert_uri, - crldp = self.rpki_base_uri + self.rpki_root_crl, + crldp = self.rpki_root_crl_uri, resources = manifest_resources, notBefore = now, notAfter = now + self.rpki_subject_lifetime, @@ -248,63 +203,171 @@ class main(object): names_and_objs = manifest_content, keypair = manifest_keypair, certs = manifest_cert) - fn = os.path.join(self.rpki_root_dir, self.rpki_root_manifest) - logger.debug("Writing manifest %s", fn) - f = open(fn, "wb") - f.write(manifest.get_DER()) - f.close() + hash = self.read_hash_maybe(self.rpki_root_manifest_file) + logger.debug("Writing manifest %s", self.rpki_root_manifest_file) + with open(self.rpki_root_manifest_file, "wb") as f: + f.write(manifest.get_DER()) + pdu = SubElement(pubd_msg, rpki.publication.tag_publish, uri = self.rpki_root_manifest_uri) + pdu.text = manifest.get_Base64() + if hash is not None: + pdu.set("hash", hash) + hash = rpki.x509.sha256(self.rpki_root_cert.get_DER()).encode("hex") + if hash != self.rpki_root_cert_hash: + pdu = SubElement(pubd_msg, rpki.publication.tag_publish, uri = self.rpki_root_cert_uri) + pdu.text = self.rpki_root_cert.get_Base64() + if self.rpki_root_cert_hash is not None: + pdu.set("hash", self.rpki_root_cert_hash) + self.rpki_root_cert_hash = hash + + + @staticmethod + def read_hash_maybe(fn): + try: + with open(fn, "rb") as f: + return rpki.x509.sha256(f.read()).encode("hex") + except IOError: + return None + def revoke_subject_cert(self, now): self.revoked.append((self.get_subject_cert().getSerial(), now)) + + def publish(self, q_msg): + if q_msg is None: + return + assert len(q_msg) > 0 + + if not all(q_pdu.get("hash") is not None for q_pdu in q_msg): + logger.debug("Some publication PDUs are missing hashes, checking published data...") + q = Element(rpki.publication.tag_msg, nsmap = rpki.publication.nsmap, + type = "query", version = rpki.publication.version) + SubElement(q, rpki.publication.tag_list) + published_hash = dict((r.get("uri"), r.get("hash")) for r in self.call_pubd(q)) + for q_pdu in q_msg: + q_uri = q_pdu.get("uri") + if q_pdu.get("hash") is None and published_hash.get(q_uri) is not None: + logger.debug("Updating hash of %s to %s from previously published data", q_uri, published_hash[q_uri]) + q_pdu.set("hash", published_hash[q_uri]) + + r_msg = self.call_pubd(q_msg) + if len(q_msg) != len(r_msg): + raise rpki.exceptions.BadPublicationReply("Wrong number of response PDUs from pubd: sent %s, got %s" % (len(q_msg), len(r_msg))) + + + def call_pubd(self, q_msg): + for q_pdu in q_msg: + logger.info("Sending %s to pubd", q_pdu.get("uri")) + r_msg = rpki.http_simple.client( + proto_cms_msg = rpki.publication.cms_msg_no_sax, + client_key = self.rootd_bpki_key, + client_cert = self.rootd_bpki_cert, + client_crl = self.rootd_bpki_crl, + server_ta = self.bpki_ta, + server_cert = self.pubd_bpki_cert, + url = self.pubd_url, + q_msg = q_msg, + replay_track = self.pubd_replay_tracker) + rpki.publication.raise_if_error(r_msg) + return r_msg + + def compose_response(self, r_msg, pkcs10 = None): - subject_cert = self.issue_subject_cert_maybe(pkcs10) - rc = rpki.up_down.class_elt() - rc.class_name = self.rpki_class_name - rc.cert_url = rpki.up_down.multi_uri(self.rpki_root_cert_uri) - rc.from_resource_bag(self.rpki_root_cert.get_3779resources()) - rc.issuer = self.rpki_root_cert - r_msg.payload.classes.append(rc) + subject_cert, pubd_msg = self.issue_subject_cert_maybe(pkcs10) + bag = self.rpki_root_cert.get_3779resources() + rc = SubElement(r_msg, rpki.up_down.tag_class, + class_name = self.rpki_class_name, + cert_url = str(rpki.up_down.multi_uri(self.rpki_root_cert_uri)), + resource_set_as = str(bag.asn), + resource_set_ipv4 = str(bag.v4), + resource_set_ipv6 = str(bag.v6), + resource_set_notafter = str(bag.valid_until)) if subject_cert is not None: - rc.certs.append(rpki.up_down.certificate_elt()) - rc.certs[0].cert_url = rpki.up_down.multi_uri(self.rpki_base_uri + self.rpki_subject_cert) - rc.certs[0].cert = subject_cert + c = SubElement(rc, rpki.up_down.tag_certificate, + cert_url = str(rpki.up_down.multi_uri(self.rpki_subject_cert_uri))) + c.text = subject_cert.get_Base64() + SubElement(rc, rpki.up_down.tag_issuer).text = self.rpki_root_cert.get_Base64() + self.publish(pubd_msg) - def up_down_handler(self, query, path, cb): - try: - q_cms = cms_msg(DER = query) - q_msg = q_cms.unwrap((self.bpki_ta, self.child_bpki_cert)) - self.cms_timestamp = q_cms.check_replay(self.cms_timestamp, path) - except (rpki.async.ExitNow, SystemExit): - raise - except Exception, e: - logger.exception("Problem decoding PDU") - return cb(400, reason = "Could not decode PDU: %s" % e) - def done(r_msg): - cb(200, body = cms_msg().wrap( - r_msg, self.rootd_bpki_key, self.rootd_bpki_cert, - self.rootd_bpki_crl if self.include_bpki_crl else None)) + def handle_list(self, q_msg, r_msg): + self.compose_response(r_msg) + + def handle_issue(self, q_msg, r_msg): + # This is where we'd check q_msg[0].get("class_name") if this weren't rootd. + self.compose_response(r_msg, rpki.x509.PKCS10(Base64 = q_msg[0].text)) + + + def handle_revoke(self, q_msg, r_msg): + class_name = q_msg[0].get("class_name") + ski = q_msg[0].get("ski") + logger.debug("Revocation requested for class %s SKI %s", class_name, ski) + subject_cert = self.get_subject_cert() + if subject_cert is None: + logger.debug("No subject certificate, nothing to revoke") + raise rpki.exceptions.NotInDatabase + if subject_cert.gSKI() != ski: + logger.debug("Subject certificate has different SKI %s, not revoking", subject_cert.gSKI()) + raise rpki.exceptions.NotInDatabase + logger.debug("Revoking certificate %s", ski) + now = rpki.sundial.now() + pubd_msg = Element(rpki.publication.tag_msg, nsmap = rpki.publication.nsmap, + type = "query", version = rpki.publication.version) + self.revoke_subject_cert(now) + self.del_subject_cert() + self.del_subject_pkcs10() + SubElement(r_msg, q_msg[0].tag, class_name = class_name, ski = ski) + self.generate_crl_and_manifest(now, pubd_msg) + self.publish(pubd_msg) + + + # Need to do something about mapping exceptions to up-down error + # codes, right now everything shows up as "internal error". + # + #exceptions = { + # rpki.exceptions.ClassNameUnknown : 1201, + # rpki.exceptions.NoActiveCA : 1202, + # (rpki.exceptions.ClassNameUnknown, revoke_pdu) : 1301, + # (rpki.exceptions.NotInDatabase, revoke_pdu) : 1302 } + # + # Might be that what we want here is a subclass of + # rpki.exceptions.RPKI_Exception which carries an extra data field + # for the up-down error code, so that we can add the correct code + # when we instantiate it. + # + # There are also a few that are also schema violations, which means + # we'd have to catch them before validating or pick them out of a + # message that failed validation or otherwise break current + # modularity. Maybe an optional pre-validation check method hook in + # rpki.x509.XML_CMS_object which we can use to intercept such things? + + + def handler(self, request, q_der): try: - q_msg.serve_top_level(None, done) - except (rpki.async.ExitNow, SystemExit): - raise - except Exception, e: + q_cms = rpki.up_down.cms_msg_no_sax(DER = q_der) + q_msg = q_cms.unwrap((self.bpki_ta, self.child_bpki_cert)) + q_type = q_msg.get("type") + logger.info("Serving %s query", q_type) + r_msg = Element(rpki.up_down.tag_message, nsmap = rpki.up_down.nsmap, version = rpki.up_down.version, + sender = q_msg.get("recipient"), recipient = q_msg.get("sender"), type = q_type + "_response") try: - logger.exception("Exception serving up-down request %r", q_msg) - done(q_msg.serve_error(e)) - except (rpki.async.ExitNow, SystemExit): - raise + self.rpkid_cms_timestamp = q_cms.check_replay(self.rpkid_cms_timestamp, request.path) + getattr(self, "handle_" + q_type)(q_msg, r_msg) except Exception, e: - logger.exception("Exception while generating error report") - cb(500, reason = "Could not process PDU: %s" % e) + logger.exception("Exception processing up-down %s message", q_type) + rpki.up_down.generate_error_response_from_exception(r_msg, e, q_type) + request.send_cms_response(rpki.up_down.cms_msg_no_sax().wrap(r_msg, self.rootd_bpki_key, self.rootd_bpki_cert, + self.rootd_bpki_crl if self.include_bpki_crl else None)) + except Exception, e: + logger.exception("Unhandled exception processing up-down message") + request.send_error(500, "Unhandled exception %s: %s" % (e.__class__.__name__, e)) def next_crl_number(self): if self.crl_number is None: try: - crl = rpki.x509.CRL(DER_file = os.path.join(self.rpki_root_dir, self.rpki_root_crl)) + crl = rpki.x509.CRL(DER_file = self.rpki_root_crl_file) self.crl_number = crl.getCRLNumber() except: # pylint: disable=W0702 self.crl_number = 0 @@ -324,15 +387,11 @@ class main(object): def __init__(self): - - global rootd - rootd = self # Gross, but simpler than what we'd have to do otherwise - - self.rpki_root_cert = None self.serial_number = None self.crl_number = None self.revoked = [] - self.cms_timestamp = None + self.rpkid_cms_timestamp = None + self.pubd_replay_tracker = ReplayTracker() os.environ["TZ"] = "UTC" time.tzset() @@ -349,7 +408,7 @@ class main(object): rpki.log.init("rootd", args) - self.cfg = rpki.config.parser(args.config, "rootd") + self.cfg = rpki.config.parser(set_filename = args.config, section = "rootd") self.cfg.set_global_flags() if not args.foreground: @@ -361,28 +420,38 @@ class main(object): self.rootd_bpki_crl = rpki.x509.CRL( Auto_update = self.cfg.get("rootd-bpki-crl")) self.child_bpki_cert = rpki.x509.X509(Auto_update = self.cfg.get("child-bpki-cert")) + if self.cfg.has_option("pubd-bpki-cert"): + self.pubd_bpki_cert = rpki.x509.X509(Auto_update = self.cfg.get("pubd-bpki-cert")) + else: + self.pubd_bpki_cert = None + self.http_server_host = self.cfg.get("server-host", "") self.http_server_port = self.cfg.getint("server-port") - self.rpki_class_name = self.cfg.get("rpki-class-name", "wombat") + self.rpki_class_name = self.cfg.get("rpki-class-name") - self.rpki_root_dir = self.cfg.get("rpki-root-dir") - self.rpki_base_uri = self.cfg.get("rpki-base-uri", "rsync://" + self.rpki_class_name + ".invalid/") + self.rpki_root_key = rpki.x509.RSA( Auto_update = self.cfg.get("rpki-root-key-file")) + self.rpki_root_cert = rpki.x509.X509(Auto_update = self.cfg.get("rpki-root-cert-file")) + self.rpki_root_cert_uri = self.cfg.get("rpki-root-cert-uri") + self.rpki_root_cert_hash = None - self.rpki_root_key = rpki.x509.RSA(Auto_update = self.cfg.get("rpki-root-key")) - self.rpki_root_cert_file = self.cfg.get("rpki-root-cert") - self.rpki_root_cert_uri = self.cfg.get("rpki-root-cert-uri", self.rpki_base_uri + "root.cer") + self.rpki_root_manifest_file = self.cfg.get("rpki-root-manifest-file") + self.rpki_root_manifest_uri = self.cfg.get("rpki-root-manifest-uri") - self.rpki_root_manifest = self.cfg.get("rpki-root-manifest", "root.mft") - self.rpki_root_crl = self.cfg.get("rpki-root-crl", "root.crl") - self.rpki_subject_cert = self.cfg.get("rpki-subject-cert", "child.cer") - self.rpki_subject_pkcs10 = self.cfg.get("rpki-subject-pkcs10", "child.pkcs10") + self.rpki_root_crl_file = self.cfg.get("rpki-root-crl-file") + self.rpki_root_crl_uri = self.cfg.get("rpki-root-crl-uri") + self.rpki_subject_cert_file = self.cfg.get("rpki-subject-cert-file") + self.rpki_subject_cert_uri = self.cfg.get("rpki-subject-cert-uri") + self.rpki_subject_pkcs10 = self.cfg.get("rpki-subject-pkcs10-file") self.rpki_subject_lifetime = rpki.sundial.timedelta.parse(self.cfg.get("rpki-subject-lifetime", "8w")) - self.rpki_subject_regen = rpki.sundial.timedelta.parse(self.cfg.get("rpki-subject-regen", self.rpki_subject_lifetime.convert_to_seconds() / 2)) + self.rpki_subject_regen = rpki.sundial.timedelta.parse(self.cfg.get("rpki-subject-regen", + self.rpki_subject_lifetime.convert_to_seconds() / 2)) self.include_bpki_crl = self.cfg.getboolean("include-bpki-crl", False) - rpki.http.server(host = self.http_server_host, - port = self.http_server_port, - handlers = self.up_down_handler) + self.pubd_url = self.cfg.get("pubd-contact-uri") + + rpki.http_simple.server(host = self.http_server_host, + port = self.http_server_port, + handlers = self.handler) |