aboutsummaryrefslogtreecommitdiff
path: root/rpkid.stable/rpki/left_right.py
diff options
context:
space:
mode:
Diffstat (limited to 'rpkid.stable/rpki/left_right.py')
-rw-r--r--rpkid.stable/rpki/left_right.py833
1 files changed, 833 insertions, 0 deletions
diff --git a/rpkid.stable/rpki/left_right.py b/rpkid.stable/rpki/left_right.py
new file mode 100644
index 00000000..82bf93f4
--- /dev/null
+++ b/rpkid.stable/rpki/left_right.py
@@ -0,0 +1,833 @@
+"""RPKI "left-right" protocol.
+
+$Id$
+
+Copyright (C) 2007--2008 American Registry for Internet Numbers ("ARIN")
+
+Permission to use, copy, modify, and distribute this software for any
+purpose with or without fee is hereby granted, provided that the above
+copyright notice and this permission notice appear in all copies.
+
+THE SOFTWARE IS PROVIDED "AS IS" AND ARIN DISCLAIMS ALL WARRANTIES WITH
+REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
+AND FITNESS. IN NO EVENT SHALL ARIN BE LIABLE FOR ANY SPECIAL, DIRECT,
+INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
+LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE
+OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
+PERFORMANCE OF THIS SOFTWARE.
+"""
+
+import base64, lxml.etree, time, traceback, os
+import rpki.resource_set, rpki.x509, rpki.sql, rpki.exceptions, rpki.xml_utils
+import rpki.https, rpki.up_down, rpki.relaxng, rpki.sundial, rpki.log, rpki.roa
+import rpki.publication
+
+# Enforce strict checking of XML "sender" field in up-down protocol
+enforce_strict_up_down_xml_sender = False
+
+class left_right_namespace(object):
+ """XML namespace parameters for left-right protocol."""
+
+ xmlns = "http://www.hactrn.net/uris/rpki/left-right-spec/"
+ nsmap = { None : xmlns }
+
+class data_elt(rpki.xml_utils.data_elt, rpki.sql.sql_persistant, left_right_namespace):
+ """Virtual class for top-level left-right protocol data elements."""
+
+ def self(this):
+ """Fetch self object to which this object links."""
+ return self_elt.sql_fetch(this.gctx, this.self_id)
+
+ def bsc(self):
+ """Return BSC object to which this object links."""
+ return bsc_elt.sql_fetch(self.gctx, self.bsc_id)
+
+ def make_reply_clone_hook(self, r_pdu):
+ """Set self_id when cloning."""
+ r_pdu.self_id = self.self_id
+
+ def serve_fetch_one(self):
+ """Find the object on which a get, set, or destroy method should
+ operate.
+ """
+ where = self.sql_template.index + " = %s AND self_id = %s"
+ args = (getattr(self, self.sql_template.index), self.self_id)
+ r = self.sql_fetch_where1(self.gctx, where, args)
+ if r is None:
+ raise rpki.exceptions.NotFound, "Lookup failed where " + (where % args)
+ return r
+
+ def serve_fetch_all(self):
+ """Find the objects on which a list method should operate."""
+ return self.sql_fetch_where(self.gctx, "self_id = %s", (self.self_id,))
+
+ def unimplemented_control(self, *controls):
+ """Uniform handling for unimplemented control operations."""
+ unimplemented = [x for x in controls if getattr(self, x, False)]
+ if unimplemented:
+ raise rpki.exceptions.NotImplementedYet, "Unimplemented control %s" % ", ".join(unimplemented)
+
+class self_elt(data_elt):
+ """<self/> element."""
+
+ element_name = "self"
+ attributes = ("action", "tag", "self_id", "crl_interval", "regen_margin")
+ elements = ("bpki_cert", "bpki_glue")
+ booleans = ("rekey", "reissue", "revoke", "run_now", "publish_world_now")
+
+ sql_template = rpki.sql.template("self", "self_id", "use_hsm", "crl_interval", "regen_margin",
+ ("bpki_cert", rpki.x509.X509), ("bpki_glue", rpki.x509.X509))
+
+ self_id = None
+ use_hsm = False
+ crl_interval = None
+ regen_margin = None
+ bpki_cert = None
+ bpki_glue = None
+
+ def bscs(self):
+ """Fetch all BSC objects that link to this self object."""
+ return bsc_elt.sql_fetch_where(self.gctx, "self_id = %s", (self.self_id,))
+
+ def repositories(self):
+ """Fetch all repository objects that link to this self object."""
+ return repository_elt.sql_fetch_where(self.gctx, "self_id = %s", (self.self_id,))
+
+ def parents(self):
+ """Fetch all parent objects that link to this self object."""
+ return parent_elt.sql_fetch_where(self.gctx, "self_id = %s", (self.self_id,))
+
+ def children(self):
+ """Fetch all child objects that link to this self object."""
+ return child_elt.sql_fetch_where(self.gctx, "self_id = %s", (self.self_id,))
+
+ def route_origins(self):
+ """Fetch all route_origin objects that link to this self object."""
+ return route_origin_elt.sql_fetch_where(self.gctx, "self_id = %s", (self.self_id,))
+
+ def serve_post_save_hook(self, q_pdu, r_pdu):
+ """Extra server actions for self_elt."""
+ rpki.log.trace()
+ if q_pdu.rekey:
+ self.serve_rekey()
+ if q_pdu.revoke:
+ self.serve_revoke()
+ self.unimplemented_control("reissue", "run_now", "publish_world_now")
+
+ def serve_rekey(self):
+ """Handle a left-right rekey action for this self."""
+ rpki.log.trace()
+ for parent in self.parents():
+ parent.serve_rekey()
+
+ def serve_revoke(self):
+ """Handle a left-right revoke action for this self."""
+ rpki.log.trace()
+ for parent in self.parents():
+ parent.serve_revoke()
+
+ def serve_fetch_one(self):
+ """Find the self object upon which a get, set, or destroy action
+ should operate.
+ """
+ r = self.sql_fetch(self.gctx, self.self_id)
+ if r is None:
+ raise rpki.exceptions.NotFound
+ return r
+
+ def serve_fetch_all(self):
+ """Find the self objects upon which a list action should operate.
+ This is different from the list action for all other objects,
+ where list only works within a given self_id context.
+ """
+ return self.sql_fetch_all(self.gctx)
+
+ def client_poll(self):
+ """Run the regular client poll cycle with each of this self's parents in turn."""
+
+ rpki.log.trace()
+
+ for parent in self.parents():
+
+ # This will need a callback when we go event-driven
+ r_msg = rpki.up_down.list_pdu.query(parent)
+
+ ca_map = dict((ca.parent_resource_class, ca) for ca in parent.cas())
+ for rc in r_msg.payload.classes:
+ if rc.class_name in ca_map:
+ ca = ca_map[rc.class_name]
+ del ca_map[rc.class_name]
+ ca.check_for_updates(parent, rc)
+ else:
+ rpki.rpki_engine.ca_obj.create(parent, rc)
+ for ca in ca_map.values():
+ ca.delete(parent) # CA not listed by parent
+ self.gctx.sql.sweep()
+
+ def update_children(self):
+ """Check for updated IRDB data for all of this self's children and
+ issue new certs as necessary. Must handle changes both in
+ resources and in expiration date.
+ """
+
+ rpki.log.trace()
+
+ now = rpki.sundial.now()
+
+ rsn = now + rpki.sundial.timedelta(seconds = self.regen_margin)
+
+ for child in self.children():
+ child_certs = child.child_certs()
+ if not child_certs:
+ continue
+
+ # This will require a callback when we go event-driven
+ irdb_resources = self.gctx.irdb_query(child.self_id, child.child_id)
+
+ for child_cert in child_certs:
+ ca_detail = child_cert.ca_detail()
+ if ca_detail.state != "active":
+ continue
+ old_resources = child_cert.cert.get_3779resources()
+ new_resources = irdb_resources.intersection(old_resources)
+ if old_resources != new_resources or (old_resources.valid_until < rsn and irdb_resources.valid_until > now):
+ rpki.log.debug("Need to reissue child certificate SKI %s" % child_cert.cert.gSKI())
+ child_cert.reissue(
+ ca_detail = ca_detail,
+ resources = new_resources)
+ elif old_resources.valid_until < now:
+ rpki.log.debug("Child certificate SKI %s has expired: cert.valid_until %s, irdb.valid_until %s"
+ % (child_cert.cert.gSKI(), old_resources.valid_until, irdb_resources.valid_until))
+ ca = ca_detail.ca()
+ parent = ca.parent()
+ repository = parent.repository()
+ child_cert.sql_delete()
+ ca_detail.generate_manifest()
+ repository.withdraw(child_cert.cert, child_cert.uri(ca))
+
+ def regenerate_crls_and_manifests(self):
+ """Generate new CRLs and manifests as necessary for all of this
+ self's CAs. Extracting nextUpdate from a manifest is hard at the
+ moment due to implementation silliness, so for now we generate a
+ new manifest whenever we generate a new CRL
+
+ This method also cleans up tombstones left behind by revoked
+ ca_detail objects, since we're walking through the relevant
+ portions of the database anyway.
+ """
+
+ rpki.log.trace()
+
+ now = rpki.sundial.now()
+ for parent in self.parents():
+ repository = parent.repository()
+ for ca in parent.cas():
+ for ca_detail in ca.fetch_revoked():
+ if now > ca_detail.latest_crl.getNextUpdate():
+ ca_detail.delete(ca, repository)
+ ca_detail = ca.fetch_active()
+ if ca_detail is not None and now > ca_detail.latest_crl.getNextUpdate():
+ ca_detail.generate_crl()
+ ca_detail.generate_manifest()
+
+ def update_roas(self):
+ """Generate or update ROAs for this self's route_origin objects."""
+
+ for route_origin in self.route_origins():
+ route_origin.update_roa()
+
+class bsc_elt(data_elt):
+ """<bsc/> (Business Signing Context) element."""
+
+ element_name = "bsc"
+ attributes = ("action", "tag", "self_id", "bsc_id", "key_type", "hash_alg", "key_length")
+ elements = ("signing_cert", "signing_cert_crl", "pkcs10_request")
+ booleans = ("generate_keypair",)
+
+ sql_template = rpki.sql.template("bsc", "bsc_id", "self_id", "hash_alg",
+ ("private_key_id", rpki.x509.RSA),
+ ("pkcs10_request", rpki.x509.PKCS10),
+ ("signing_cert", rpki.x509.X509),
+ ("signing_cert_crl", rpki.x509.CRL))
+
+ private_key_id = None
+ pkcs10_request = None
+ signing_cert = None
+ signing_cert_crl = None
+
+ def repositories(self):
+ """Fetch all repository objects that link to this BSC object."""
+ return repository_elt.sql_fetch_where(self.gctx, "bsc_id = %s", (self.bsc_id,))
+
+ def parents(self):
+ """Fetch all parent objects that link to this BSC object."""
+ return parent_elt.sql_fetch_where(self.gctx, "bsc_id = %s", (self.bsc_id,))
+
+ def children(self):
+ """Fetch all child objects that link to this BSC object."""
+ return child_elt.sql_fetch_where(self.gctx, "bsc_id = %s", (self.bsc_id,))
+
+ def serve_pre_save_hook(self, q_pdu, r_pdu):
+ """Extra server actions for bsc_elt -- handle key generation.
+ For now this only allows RSA with SHA-256.
+ """
+ if q_pdu.generate_keypair:
+ assert q_pdu.key_type in (None, "rsa") and q_pdu.hash_alg in (None, "sha256")
+ self.private_key_id = rpki.x509.RSA.generate(keylength = q_pdu.key_length or 2048)
+ self.pkcs10_request = rpki.x509.PKCS10.create(self.private_key_id)
+ r_pdu.pkcs10_request = self.pkcs10_request
+
+class parent_elt(data_elt):
+ """<parent/> element."""
+
+ element_name = "parent"
+ attributes = ("action", "tag", "self_id", "parent_id", "bsc_id", "repository_id",
+ "peer_contact_uri", "sia_base", "sender_name", "recipient_name")
+ elements = ("bpki_cms_cert", "bpki_cms_glue", "bpki_https_cert", "bpki_https_glue")
+ booleans = ("rekey", "reissue", "revoke")
+
+ sql_template = rpki.sql.template("parent", "parent_id", "self_id", "bsc_id", "repository_id",
+ ("bpki_cms_cert", rpki.x509.X509), ("bpki_cms_glue", rpki.x509.X509),
+ ("bpki_https_cert", rpki.x509.X509), ("bpki_https_glue", rpki.x509.X509),
+ "peer_contact_uri", "sia_base", "sender_name", "recipient_name")
+
+ bpki_cms_cert = None
+ bpki_cms_glue = None
+ bpki_https_cert = None
+ bpki_https_glue = None
+
+ def repository(self):
+ """Fetch repository object to which this parent object links."""
+ return repository_elt.sql_fetch(self.gctx, self.repository_id)
+
+ def cas(self):
+ """Fetch all CA objects that link to this parent object."""
+ return rpki.rpki_engine.ca_obj.sql_fetch_where(self.gctx, "parent_id = %s", (self.parent_id,))
+
+ def serve_post_save_hook(self, q_pdu, r_pdu):
+ """Extra server actions for parent_elt."""
+ if q_pdu.rekey:
+ self.serve_rekey()
+ if q_pdu.revoke:
+ self.serve_revoke()
+ self.unimplemented_control("reissue")
+
+ def serve_rekey(self):
+ """Handle a left-right rekey action for this parent."""
+ for ca in self.cas():
+ ca.rekey()
+
+ def serve_revoke(self):
+ """Handle a left-right revoke action for this parent."""
+ for ca in self.cas():
+ ca.revoke()
+
+ def query_up_down(self, q_pdu):
+ """Client code for sending one up-down query PDU to this parent.
+
+ I haven't figured out yet whether this method should do something
+ clever like dispatching via a method in the response PDU payload,
+ or just hand back the whole response to the caller. In the long
+ run this will have to become event driven with a context object
+ that has methods of its own, but as this method is common code for
+ several different queries and I don't yet know what the response
+ processing looks like, it's too soon to tell what will make sense.
+
+ For now, keep this dead simple lock step, rewrite it later.
+ """
+
+ rpki.log.trace()
+
+ bsc = self.bsc()
+ if bsc is None:
+ raise rpki.exceptions.BSCNotFound, "Could not find BSC %s" % self.bsc_id
+
+ q_msg = rpki.up_down.message_pdu.make_query(
+ payload = q_pdu,
+ sender = self.sender_name,
+ recipient = self.recipient_name)
+
+ q_cms = rpki.up_down.cms_msg.wrap(q_msg, bsc.private_key_id,
+ bsc.signing_cert,
+ bsc.signing_cert_crl)
+
+ der = rpki.https.client(server_ta = (self.gctx.bpki_ta,
+ self.self().bpki_cert, self.self().bpki_glue,
+ self.bpki_https_cert, self.bpki_https_glue),
+ client_key = bsc.private_key_id,
+ client_cert = bsc.signing_cert,
+ msg = q_cms,
+ url = self.peer_contact_uri)
+
+ r_msg = rpki.up_down.cms_msg.unwrap(der, (self.gctx.bpki_ta,
+ self.self().bpki_cert, self.self().bpki_glue,
+ self.bpki_cms_cert, self.bpki_cms_glue))
+
+ r_msg.payload.check_response()
+ return r_msg
+
+
+class child_elt(data_elt):
+ """<child/> element."""
+
+ element_name = "child"
+ attributes = ("action", "tag", "self_id", "child_id", "bsc_id")
+ elements = ("bpki_cert", "bpki_glue")
+ booleans = ("reissue", )
+
+ sql_template = rpki.sql.template("child", "child_id", "self_id", "bsc_id",
+ ("bpki_cert", rpki.x509.X509),
+ ("bpki_glue", rpki.x509.X509))
+
+ bpki_cert = None
+ bpki_glue = None
+ clear_https_ta_cache = False
+
+ def child_certs(self, ca_detail = None, ski = None, unique = False):
+ """Fetch all child_cert objects that link to this child object."""
+ return rpki.rpki_engine.child_cert_obj.fetch(self.gctx, self, ca_detail, ski, unique)
+
+ def parents(self):
+ """Fetch all parent objects that link to self object to which this child object links."""
+ return parent_elt.sql_fetch_where(self.gctx, "self_id = %s", (self.self_id,))
+
+ def ca_from_class_name(self, class_name):
+ """Fetch the CA corresponding to an up-down class_name."""
+ if not class_name.isdigit():
+ raise rpki.exceptions.BadClassNameSyntax, "Bad class name %s" % class_name
+ ca = rpki.rpki_engine.ca_obj.sql_fetch(self.gctx, long(class_name))
+ if ca is None:
+ raise rpki.exceptions.ClassNameUnknown, "Unknown class name %s" % class_name
+ parent = ca.parent()
+ if self.self_id != parent.self_id:
+ raise rpki.exceptions.ClassNameMismatch, "Class name mismatch: child.self_id = %d, parent.self_id = %d" % (self.self_id, parent.self_id)
+ return ca
+
+ def serve_post_save_hook(self, q_pdu, r_pdu):
+ """Extra server actions for child_elt."""
+ self.unimplemented_control("reissue")
+ if self.clear_https_ta_cache:
+ self.gctx.clear_https_ta_cache()
+ self.clear_https_ta_cache = False
+
+ def endElement(self, stack, name, text):
+ """Handle subelements of <child/> element. These require special
+ handling because modifying them invalidates the HTTPS trust anchor
+ cache.
+ """
+ rpki.xml_utils.data_elt.endElement(self, stack, name, text)
+ if name in self.elements:
+ self.clear_https_ta_cache = True
+
+ def serve_up_down(self, query):
+ """Outer layer of server handling for one up-down PDU from this child."""
+
+ rpki.log.trace()
+
+ bsc = self.bsc()
+ if bsc is None:
+ raise rpki.exceptions.BSCNotFound, "Could not find BSC %s" % self.bsc_id
+ q_msg = rpki.up_down.cms_msg.unwrap(query, (self.gctx.bpki_ta,
+ self.self().bpki_cert, self.self().bpki_glue,
+ self.bpki_cert, self.bpki_glue))
+ q_msg.payload.gctx = self.gctx
+ if enforce_strict_up_down_xml_sender and q_msg.sender != str(self.child_id):
+ raise rpki.exceptions.BadSender, "Unexpected XML sender %s" % q_msg.sender
+ try:
+ r_msg = q_msg.serve_top_level(self)
+ except Exception, data:
+ rpki.log.error(traceback.format_exc())
+ r_msg = q_msg.serve_error(data)
+ #
+ # Exceptions from this point on are problematic, as we have no
+ # sane way of reporting errors in the error reporting mechanism.
+ # May require refactoring, ignore the issue for now.
+ #
+ r_cms = rpki.up_down.cms_msg.wrap(r_msg, bsc.private_key_id,
+ bsc.signing_cert, bsc.signing_cert_crl)
+ return r_cms
+
+class repository_elt(data_elt):
+ """<repository/> element."""
+
+ element_name = "repository"
+ attributes = ("action", "tag", "self_id", "repository_id", "bsc_id", "peer_contact_uri")
+ elements = ("bpki_cms_cert", "bpki_cms_glue", "bpki_https_cert", "bpki_https_glue")
+
+ sql_template = rpki.sql.template("repository", "repository_id", "self_id", "bsc_id", "peer_contact_uri",
+ ("bpki_cms_cert", rpki.x509.X509), ("bpki_cms_glue", rpki.x509.X509),
+ ("bpki_https_cert", rpki.x509.X509), ("bpki_https_glue", rpki.x509.X509))
+
+ bpki_cms_cert = None
+ bpki_cms_glue = None
+ bpki_https_cert = None
+ bpki_https_glue = None
+
+ use_pubd = True
+
+ def parents(self):
+ """Fetch all parent objects that link to this repository object."""
+ return parent_elt.sql_fetch_where(self.gctx, "repository_id = %s", (self.repository_id,))
+
+ @staticmethod
+ def uri_to_filename(base, uri):
+ """Convert a URI to a filename. [TEMPORARY]"""
+ if not uri.startswith("rsync://"):
+ raise rpki.exceptions.BadURISyntax
+ filename = base + uri[len("rsync://"):]
+ if filename.find("//") >= 0 or filename.find("/../") >= 0 or filename.endswith("/.."):
+ raise rpki.exceptions.BadURISyntax
+ return filename
+
+ @classmethod
+ def object_write(cls, base, uri, obj):
+ """Write an object to disk. [TEMPORARY]"""
+ rpki.log.trace()
+ filename = cls.uri_to_filename(base, uri)
+ dirname = os.path.dirname(filename)
+ if not os.path.isdir(dirname):
+ os.makedirs(dirname)
+ f = open(filename, "wb")
+ f.write(obj.get_DER())
+ f.close()
+
+ @classmethod
+ def object_delete(cls, base, uri):
+ """Delete an object from disk. [TEMPORARY]"""
+ rpki.log.trace()
+ os.remove(cls.uri_to_filename(base, uri))
+
+ def call_pubd(self, *pdus):
+ """Send a message to publication daemon and return the response."""
+ rpki.log.trace()
+ bsc = self.bsc()
+ q_msg = rpki.publication.msg(pdus)
+ q_msg.type = "query"
+ q_cms = rpki.publication.cms_msg.wrap(q_msg, bsc.private_key_id, bsc.signing_cert, bsc.signing_cert_crl)
+ bpki_ta_path = (self.gctx.bpki_ta, self.self().bpki_cert, self.self().bpki_glue, self.bpki_https_cert, self.bpki_https_glue)
+ r_cms = rpki.https.client(
+ client_key = bsc.private_key_id,
+ client_cert = bsc.signing_cert,
+ server_ta = bpki_ta_path,
+ url = self.peer_contact_uri,
+ msg = q_cms)
+ r_msg = rpki.publication.cms_msg.unwrap(r_cms, bpki_ta_path)
+ assert len(r_msg) == 1
+ return r_msg[0]
+
+ def publish(self, obj, uri):
+ """Placeholder for publication operation. [TEMPORARY]"""
+ rpki.log.trace()
+ rpki.log.info("Publishing %s as %s" % (repr(obj), repr(uri)))
+ if self.use_pubd:
+ self.call_pubd(rpki.publication.obj2elt[type(obj)].make_pdu(action = "publish", uri = uri, payload = obj))
+ else:
+ self.object_write(self.gctx.publication_kludge_base, uri, obj)
+
+ def withdraw(self, obj, uri):
+ """Placeholder for publication withdrawal operation. [TEMPORARY]"""
+ rpki.log.trace()
+ rpki.log.info("Withdrawing %s from at %s" % (repr(obj), repr(uri)))
+ if self.use_pubd:
+ self.call_pubd(rpki.publication.obj2elt[type(obj)].make_pdu(action = "withdraw", uri = uri))
+ else:
+ self.object_delete(self.gctx.publication_kludge_base, uri)
+
+class route_origin_elt(data_elt):
+ """<route_origin/> element."""
+
+ element_name = "route_origin"
+ attributes = ("action", "tag", "self_id", "route_origin_id", "as_number", "ipv4", "ipv6")
+ booleans = ("suppress_publication",)
+
+ sql_template = rpki.sql.template("route_origin", "route_origin_id", "ca_detail_id",
+ "self_id", "as_number",
+ ("roa", rpki.x509.ROA),
+ ("cert", rpki.x509.X509))
+
+ ca_detail_id = None
+ cert = None
+ roa = None
+
+ ## @var publish_ee_separately
+ # Whether to publish the ROA EE certificate separately from the ROA.
+ publish_ee_separately = False
+
+ def sql_fetch_hook(self):
+ """Extra SQL fetch actions for route_origin_elt -- handle prefix list."""
+ self.ipv4 = rpki.resource_set.roa_prefix_set_ipv4.from_sql(
+ self.gctx.sql,
+ """
+ SELECT address, prefixlen, max_prefixlen FROM route_origin_prefix
+ WHERE route_origin_id = %s AND address NOT LIKE '%:%'
+ """, (self.route_origin_id,))
+ self.ipv6 = rpki.resource_set.roa_prefix_set_ipv6.from_sql(
+ self.gctx.sql,
+ """
+ SELECT address, prefixlen, max_prefixlen FROM route_origin_prefix
+ WHERE route_origin_id = %s AND address LIKE '%:%'
+ """, (self.route_origin_id,))
+
+ def sql_insert_hook(self):
+ """Extra SQL insert actions for route_origin_elt -- handle address ranges."""
+ if self.ipv4 or self.ipv6:
+ self.gctx.sql.executemany("""
+ INSERT route_origin_prefix (route_origin_id, address, prefixlen, max_prefixlen)
+ VALUES (%s, %s, %s, %s)""",
+ ((self.route_origin_id, x.address, x.prefixlen, x.max_prefixlen)
+ for x in (self.ipv4 or []) + (self.ipv6 or [])))
+
+ def sql_delete_hook(self):
+ """Extra SQL delete actions for route_origin_elt -- handle address ranges."""
+ self.gctx.sql.execute("DELETE FROM route_origin_prefix WHERE route_origin_id = %s", (self.route_origin_id,))
+
+ def ca_detail(self):
+ """Fetch all ca_detail objects that link to this route_origin object."""
+ return rpki.rpki_engine.ca_detail_obj.sql_fetch(self.gctx, self.ca_detail_id)
+
+ def serve_post_save_hook(self, q_pdu, r_pdu):
+ """Extra server actions for route_origin_elt."""
+ self.unimplemented_control("suppress_publication")
+
+ def startElement(self, stack, name, attrs):
+ """Handle <route_origin/> element. This requires special
+ processing due to the data types of some of the attributes.
+ """
+ assert name == "route_origin", "Unexpected name %s, stack %s" % (name, stack)
+ self.read_attrs(attrs)
+ if self.as_number is not None:
+ self.as_number = long(self.as_number)
+ if self.ipv4 is not None:
+ self.ipv4 = rpki.resource_set.roa_prefix_set_ipv4(self.ipv4)
+ if self.ipv6 is not None:
+ self.ipv6 = rpki.resource_set.roa_prefix_set_ipv6(self.ipv6)
+
+ def update_roa(self):
+ """Bring this route_origin's ROA up to date if necesssary."""
+
+ if self.roa is None:
+ return self.generate_roa()
+
+ ca_detail = self.ca_detail()
+
+ if ca_detail is None or ca_detail.state != "active":
+ return self.regenerate_roa()
+
+ regen_margin = rpki.sundial.timedelta(seconds = self.self().regen_margin)
+
+ if rpki.sundial.now() + regen_margin > self.cert.getNotAfter():
+ return self.regenerate_roa()
+
+ ca_resources = ca_detail.latest_ca_cert.get_3779resources()
+ ee_resources = self.cert.get_3779resources()
+
+ if ee_resources.oversized(ca_resources):
+ return self.regenerate_roa()
+
+ v4 = self.ipv4.to_resource_set() if self.ipv4 is not None else rpki.resource_set.resource_set_ipv4()
+ v6 = self.ipv6.to_resource_set() if self.ipv6 is not None else rpki.resource_set.resource_set_ipv6()
+
+ if ee_resources.v4 != v4 or ee_resources.v6 != v6:
+ return self.regenerate_roa()
+
+ def generate_roa(self):
+ """Generate a ROA based on this <route_origin/> object.
+
+ At present this does not support ROAs with multiple signatures
+ (neither does the current CMS code).
+
+ At present we have no way of performing a direct lookup from a
+ desired set of resources to a covering certificate, so we have to
+ search. This could be quite slow if we have a lot of active
+ ca_detail objects. Punt on the issue for now, revisit if
+ profiling shows this as a hotspot.
+
+ Once we have the right covering certificate, we generate the ROA
+ payload, generate a new EE certificate, use the EE certificate to
+ sign the ROA payload, publish the result, then throw away the
+ private key for the EE cert, all per the ROA specification. This
+ implies that generating a lot of ROAs will tend to thrash
+ /dev/random, but there is not much we can do about that.
+ """
+
+ if self.ipv4 is None and self.ipv6 is None:
+ rpki.log.warn("Can't generate ROA for empty prefix list")
+ return
+
+ # Ugly and expensive search for covering ca_detail, there has to
+ # be a better way, but it would require the ability to test for
+ # resource subsets in SQL.
+
+ v4 = self.ipv4.to_resource_set() if self.ipv4 is not None else rpki.resource_set.resource_set_ipv4()
+ v6 = self.ipv6.to_resource_set() if self.ipv6 is not None else rpki.resource_set.resource_set_ipv6()
+
+ ca_detail = self.ca_detail()
+ if ca_detail is None or ca_detail.state != "active":
+ ca_detail = None
+ for parent in self.self().parents():
+ for ca in parent.cas():
+ ca_detail = ca.fetch_active()
+ if ca_detail is not None:
+ resources = ca_detail.latest_ca_cert.get_3779resources()
+ if v4.issubset(resources.v4) and v6.issubset(resources.v6):
+ break
+ ca_detail = None
+ if ca_detail is not None:
+ break
+
+ if ca_detail is None:
+ rpki.log.warn("generate_roa() could not find a certificate covering %s %s" % (v4, v6))
+ return
+
+ ca = ca_detail.ca()
+
+ resources = rpki.resource_set.resource_bag(v4 = v4, v6 = v6)
+
+ keypair = rpki.x509.RSA.generate()
+
+ sia = ((rpki.oids.name2oid["id-ad-signedObject"], ("uri", self.roa_uri(ca, keypair))),)
+
+ self.cert = ca_detail.issue_ee(ca, resources, keypair.get_RSApublic(), sia = sia)
+ self.roa = rpki.x509.ROA.build(self.as_number, self.ipv4, self.ipv6, keypair, (self.cert,))
+ self.ca_detail_id = ca_detail.ca_detail_id
+ self.sql_store()
+
+ repository = ca.parent().repository()
+ repository.publish(self.roa, self.roa_uri(ca))
+ if self.publish_ee_separately:
+ repository.publish(self.cert, self.ee_uri(ca))
+ ca_detail.generate_manifest()
+
+ def withdraw_roa(self, regenerate = False):
+ """Withdraw ROA associated with this route_origin.
+
+ In order to preserve make-before-break properties without
+ duplicating code, this method also handles generating a
+ replacement ROA when requested.
+ """
+
+ ca_detail = self.ca_detail()
+ ca = ca_detail.ca()
+ repository = ca.parent().repository()
+ cert = self.cert
+ roa = self.roa
+ roa_uri = self.roa_uri(ca)
+ ee_uri = self.ee_uri(ca)
+
+ if ca_detail.state != 'active':
+ self.ca_detail_id = None
+ if regenerate:
+ self.generate_roa()
+
+ rpki.log.debug("Withdrawing ROA and revoking its EE cert")
+ rpki.rpki_engine.revoked_cert_obj.revoke(cert = cert, ca_detail = ca_detail)
+ repository.withdraw(roa, roa_uri)
+ if self.publish_ee_separately:
+ repository.withdraw(cert, ee_uri)
+ self.gctx.sql.sweep()
+ ca_detail.generate_crl()
+ ca_detail.generate_manifest()
+
+ def regenerate_roa(self):
+ """Reissue ROA associated with this route_origin."""
+ if self.ca_detail() is None:
+ self.generate_roa()
+ else:
+ self.withdraw_roa(regenerate = True)
+
+ def roa_uri(self, ca, key = None):
+ """Return the publication URI for this route_origin's ROA."""
+ return ca.sia_uri + self.roa_uri_tail(key)
+
+ def roa_uri_tail(self, key = None):
+ """Return the tail (filename portion) of the publication URI for this route_origin's ROA."""
+ return (key or self.cert).gSKI() + ".roa"
+
+ def ee_uri_tail(self):
+ """Return the tail (filename) portion of the URI for this route_origin's ROA's EE certificate."""
+ return self.cert.gSKI() + ".cer"
+
+ def ee_uri(self, ca):
+ """Return the publication URI for this route_origin's ROA's EE certificate."""
+ return ca.sia_uri + self.ee_uri_tail()
+
+class list_resources_elt(rpki.xml_utils.base_elt, left_right_namespace):
+ """<list_resources/> element."""
+
+ element_name = "list_resources"
+ attributes = ("self_id", "tag", "child_id", "valid_until", "asn", "ipv4", "ipv6", "subject_name")
+ valid_until = None
+
+ def startElement(self, stack, name, attrs):
+ """Handle <list_resources/> element. This requires special
+ handling due to the data types of some of the attributes.
+ """
+ assert name == "list_resources", "Unexpected name %s, stack %s" % (name, stack)
+ self.read_attrs(attrs)
+ if isinstance(self.valid_until, str):
+ self.valid_until = rpki.sundial.datetime.fromXMLtime(self.valid_until)
+ if self.asn is not None:
+ self.asn = rpki.resource_set.resource_set_as(self.asn)
+ if self.ipv4 is not None:
+ self.ipv4 = rpki.resource_set.resource_set_ipv4(self.ipv4)
+ if self.ipv6 is not None:
+ self.ipv6 = rpki.resource_set.resource_set_ipv6(self.ipv6)
+
+ def toXML(self):
+ """Generate <list_resources/> element. This requires special
+ handling due to the data types of some of the attributes.
+ """
+ elt = self.make_elt()
+ if isinstance(self.valid_until, int):
+ elt.set("valid_until", self.valid_until.toXMLtime())
+ return elt
+
+class report_error_elt(rpki.xml_utils.base_elt, left_right_namespace):
+ """<report_error/> element."""
+
+ element_name = "report_error"
+ attributes = ("tag", "self_id", "error_code")
+
+ @classmethod
+ def from_exception(cls, exc, self_id = None):
+ """Generate a <report_error/> element from an exception."""
+ self = cls()
+ self.self_id = self_id
+ self.error_code = exc.__class__.__name__
+ return self
+
+class msg(rpki.xml_utils.msg, left_right_namespace):
+ """Left-right PDU."""
+
+ ## @var version
+ # Protocol version
+ version = 1
+
+ ## @var pdus
+ # Dispatch table of PDUs for this protocol.
+ pdus = dict((x.element_name, x)
+ for x in (self_elt, child_elt, parent_elt, bsc_elt, repository_elt,
+ route_origin_elt, list_resources_elt, report_error_elt))
+
+ def serve_top_level(self, gctx):
+ """Serve one msg PDU."""
+ r_msg = self.__class__()
+ r_msg.type = "reply"
+ for q_pdu in self:
+ q_pdu.gctx = gctx
+ q_pdu.serve_dispatch(r_msg)
+ return r_msg
+
+class sax_handler(rpki.xml_utils.sax_handler):
+ """SAX handler for Left-Right protocol."""
+
+ pdu = msg
+ name = "msg"
+ version = "1"
+
+class cms_msg(rpki.x509.XML_CMS_object):
+ """Class to hold a CMS-signed left-right PDU."""
+
+ encoding = "us-ascii"
+ schema = rpki.relaxng.left_right
+ saxify = sax_handler.saxify