00001 """RPKI "left-right" protocol.
00002
00003 $Id: left_right.py 1912 2008-06-21 07:55:01Z sra $
00004
00005 Copyright (C) 2007--2008 American Registry for Internet Numbers ("ARIN")
00006
00007 Permission to use, copy, modify, and distribute this software for any
00008 purpose with or without fee is hereby granted, provided that the above
00009 copyright notice and this permission notice appear in all copies.
00010
00011 THE SOFTWARE IS PROVIDED "AS IS" AND ARIN DISCLAIMS ALL WARRANTIES WITH
00012 REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
00013 AND FITNESS. IN NO EVENT SHALL ARIN BE LIABLE FOR ANY SPECIAL, DIRECT,
00014 INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
00015 LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE
00016 OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
00017 PERFORMANCE OF THIS SOFTWARE.
00018 """
00019
00020 import base64, lxml.etree, time, traceback, os
00021 import rpki.resource_set, rpki.x509, rpki.sql, rpki.exceptions, rpki.xml_utils
00022 import rpki.https, rpki.up_down, rpki.relaxng, rpki.sundial, rpki.log, rpki.roa
00023 import rpki.publication
00024
00025
00026 enforce_strict_up_down_xml_sender = False
00027
00028 class left_right_namespace(object):
00029 """XML namespace parameters for left-right protocol."""
00030
00031 xmlns = "http://www.hactrn.net/uris/rpki/left-right-spec/"
00032 nsmap = { None : xmlns }
00033
00034 class data_elt(rpki.xml_utils.data_elt, rpki.sql.sql_persistant, left_right_namespace):
00035 """Virtual class for top-level left-right protocol data elements."""
00036
00037 def self(this):
00038 """Fetch self object to which this object links."""
00039 return self_elt.sql_fetch(this.gctx, this.self_id)
00040
00041 def bsc(self):
00042 """Return BSC object to which this object links."""
00043 return bsc_elt.sql_fetch(self.gctx, self.bsc_id)
00044
00045 def make_reply_clone_hook(self, r_pdu):
00046 """Set self_id when cloning."""
00047 r_pdu.self_id = self.self_id
00048
00049 def serve_fetch_one(self):
00050 """Find the object on which a get, set, or destroy method should
00051 operate.
00052 """
00053 where = self.sql_template.index + " = %s AND self_id = %s"
00054 args = (getattr(self, self.sql_template.index), self.self_id)
00055 r = self.sql_fetch_where1(self.gctx, where, args)
00056 if r is None:
00057 raise rpki.exceptions.NotFound, "Lookup failed where %s" + (where % args)
00058 return r
00059
00060 def serve_fetch_all(self):
00061 """Find the objects on which a list method should operate."""
00062 return self.sql_fetch_where(self.gctx, "self_id = %s", (self.self_id,))
00063
00064 def unimplemented_control(self, *controls):
00065 """Uniform handling for unimplemented control operations."""
00066 unimplemented = [x for x in controls if getattr(self, x, False)]
00067 if unimplemented:
00068 raise rpki.exceptions.NotImplementedYet, "Unimplemented control %s" % ", ".join(unimplemented)
00069
00070 class self_elt(data_elt):
00071 """<self/> element."""
00072
00073 element_name = "self"
00074 attributes = ("action", "tag", "self_id", "crl_interval", "regen_margin")
00075 elements = ("bpki_cert", "bpki_glue")
00076 booleans = ("rekey", "reissue", "revoke", "run_now", "publish_world_now")
00077
00078 sql_template = rpki.sql.template("self", "self_id", "use_hsm", "crl_interval", "regen_margin",
00079 ("bpki_cert", rpki.x509.X509), ("bpki_glue", rpki.x509.X509))
00080
00081 self_id = None
00082 use_hsm = False
00083 crl_interval = None
00084 regen_margin = None
00085 bpki_cert = None
00086 bpki_glue = None
00087
00088 def bscs(self):
00089 """Fetch all BSC objects that link to this self object."""
00090 return bsc_elt.sql_fetch_where(self.gctx, "self_id = %s", (self.self_id,))
00091
00092 def repositories(self):
00093 """Fetch all repository objects that link to this self object."""
00094 return repository_elt.sql_fetch_where(self.gctx, "self_id = %s", (self.self_id,))
00095
00096 def parents(self):
00097 """Fetch all parent objects that link to this self object."""
00098 return parent_elt.sql_fetch_where(self.gctx, "self_id = %s", (self.self_id,))
00099
00100 def children(self):
00101 """Fetch all child objects that link to this self object."""
00102 return child_elt.sql_fetch_where(self.gctx, "self_id = %s", (self.self_id,))
00103
00104 def route_origins(self):
00105 """Fetch all route_origin objects that link to this self object."""
00106 return route_origin_elt.sql_fetch_where(self.gctx, "self_id = %s", (self.self_id,))
00107
00108 def serve_post_save_hook(self, q_pdu, r_pdu):
00109 """Extra server actions for self_elt."""
00110 rpki.log.trace()
00111 if q_pdu.rekey:
00112 self.serve_rekey()
00113 if q_pdu.revoke:
00114 self.serve_revoke()
00115 self.unimplemented_control("reissue", "run_now", "publish_world_now")
00116
00117 def serve_rekey(self):
00118 """Handle a left-right rekey action for this self."""
00119 rpki.log.trace()
00120 for parent in self.parents():
00121 parent.serve_rekey()
00122
00123 def serve_revoke(self):
00124 """Handle a left-right revoke action for this self."""
00125 rpki.log.trace()
00126 for parent in self.parents():
00127 parent.serve_revoke()
00128
00129 def serve_fetch_one(self):
00130 """Find the self object upon which a get, set, or destroy action
00131 should operate.
00132 """
00133 r = self.sql_fetch(self.gctx, self.self_id)
00134 if r is None:
00135 raise rpki.exceptions.NotFound
00136 return r
00137
00138 def serve_fetch_all(self):
00139 """Find the self objects upon which a list action should operate.
00140 This is different from the list action for all other objects,
00141 where list only works within a given self_id context.
00142 """
00143 return self.sql_fetch_all(self.gctx)
00144
00145 def client_poll(self):
00146 """Run the regular client poll cycle with each of this self's parents in turn."""
00147
00148 rpki.log.trace()
00149
00150 for parent in self.parents():
00151
00152
00153 r_msg = rpki.up_down.list_pdu.query(parent)
00154
00155 ca_map = dict((ca.parent_resource_class, ca) for ca in parent.cas())
00156 for rc in r_msg.payload.classes:
00157 if rc.class_name in ca_map:
00158 ca = ca_map[rc.class_name]
00159 del ca_map[rc.class_name]
00160 ca.check_for_updates(parent, rc)
00161 else:
00162 rpki.rpki_engine.ca_obj.create(parent, rc)
00163 for ca in ca_map.values():
00164 ca.delete(parent)
00165 self.gctx.sql.sweep()
00166
00167 def update_children(self):
00168 """Check for updated IRDB data for all of this self's children and
00169 issue new certs as necessary. Must handle changes both in
00170 resources and in expiration date.
00171 """
00172
00173 rpki.log.trace()
00174
00175 now = rpki.sundial.now()
00176
00177 for child in self.children():
00178 child_certs = child.child_certs()
00179 if not child_certs:
00180 continue
00181
00182
00183 irdb_resources = self.gctx.irdb_query(child.self_id, child.child_id)
00184
00185 for child_cert in child_certs:
00186 ca_detail = child_cert.ca_detail()
00187 if ca_detail.state != "active":
00188 continue
00189 old_resources = child_cert.cert.get_3779resources()
00190 new_resources = irdb_resources.intersection(old_resources)
00191 if old_resources != new_resources:
00192 rpki.log.debug("Need to reissue %s" % repr(child_cert))
00193 child_cert.reissue(
00194 ca_detail = ca_detail,
00195 resources = new_resources)
00196 elif old_resources.valid_until < now:
00197 ca = ca_detail.ca()
00198 parent = ca.parent()
00199 repository = parent.repository()
00200 child_cert.sql_delete()
00201 ca_detail.generate_manifest()
00202 repository.withdraw(child_cert.cert, child_cert.uri(ca))
00203
00204 def regenerate_crls_and_manifests(self):
00205 """Generate new CRLs and manifests as necessary for all of this
00206 self's CAs. Extracting nextUpdate from a manifest is hard at the
00207 moment due to implementation silliness, so for now we generate a
00208 new manifest whenever we generate a new CRL
00209
00210 This method also cleans up tombstones left behind by revoked
00211 ca_detail objects, since we're walking through the relevant
00212 portions of the database anyway.
00213 """
00214
00215 rpki.log.trace()
00216
00217 now = rpki.sundial.now()
00218 for parent in self.parents():
00219 repository = parent.repository()
00220 for ca in parent.cas():
00221 for ca_detail in ca.fetch_revoked():
00222 if now > ca_detail.latest_crl.getNextUpdate():
00223 ca_detail.delete(ca, repository)
00224 ca_detail = ca.fetch_active()
00225 if now > ca_detail.latest_crl.getNextUpdate():
00226 ca_detail.generate_crl()
00227 ca_detail.generate_manifest()
00228
00229 def update_roas(self):
00230 """Generate or update ROAs for this self's route_origin objects."""
00231
00232 for route_origin in self.route_origins():
00233 route_origin.update_roa()
00234
00235 class bsc_elt(data_elt):
00236 """<bsc/> (Business Signing Context) element."""
00237
00238 element_name = "bsc"
00239 attributes = ("action", "tag", "self_id", "bsc_id", "key_type", "hash_alg", "key_length")
00240 elements = ("signing_cert", "signing_cert_crl", "pkcs10_request")
00241 booleans = ("generate_keypair",)
00242
00243 sql_template = rpki.sql.template("bsc", "bsc_id", "self_id", "hash_alg",
00244 ("private_key_id", rpki.x509.RSA),
00245 ("pkcs10_request", rpki.x509.PKCS10),
00246 ("signing_cert", rpki.x509.X509),
00247 ("signing_cert_crl", rpki.x509.CRL))
00248
00249 private_key_id = None
00250 pkcs10_request = None
00251 signing_cert = None
00252 signing_cert_crl = None
00253
00254 def repositories(self):
00255 """Fetch all repository objects that link to this BSC object."""
00256 return repository_elt.sql_fetch_where(self.gctx, "bsc_id = %s", (self.bsc_id,))
00257
00258 def parents(self):
00259 """Fetch all parent objects that link to this BSC object."""
00260 return parent_elt.sql_fetch_where(self.gctx, "bsc_id = %s", (self.bsc_id,))
00261
00262 def children(self):
00263 """Fetch all child objects that link to this BSC object."""
00264 return child_elt.sql_fetch_where(self.gctx, "bsc_id = %s", (self.bsc_id,))
00265
00266 def serve_pre_save_hook(self, q_pdu, r_pdu):
00267 """Extra server actions for bsc_elt -- handle key generation.
00268 For now this only allows RSA with SHA-256.
00269 """
00270 if q_pdu.generate_keypair:
00271 assert q_pdu.key_type in (None, "rsa") and q_pdu.hash_alg in (None, "sha256")
00272 self.private_key_id = rpki.x509.RSA.generate(keylength = q_pdu.key_length or 2048)
00273 self.pkcs10_request = rpki.x509.PKCS10.create(self.private_key_id)
00274 r_pdu.pkcs10_request = self.pkcs10_request
00275
00276 class parent_elt(data_elt):
00277 """<parent/> element."""
00278
00279 element_name = "parent"
00280 attributes = ("action", "tag", "self_id", "parent_id", "bsc_id", "repository_id",
00281 "peer_contact_uri", "sia_base", "sender_name", "recipient_name")
00282 elements = ("bpki_cms_cert", "bpki_cms_glue", "bpki_https_cert", "bpki_https_glue")
00283 booleans = ("rekey", "reissue", "revoke")
00284
00285 sql_template = rpki.sql.template("parent", "parent_id", "self_id", "bsc_id", "repository_id",
00286 ("bpki_cms_cert", rpki.x509.X509), ("bpki_cms_glue", rpki.x509.X509),
00287 ("bpki_https_cert", rpki.x509.X509), ("bpki_https_glue", rpki.x509.X509),
00288 "peer_contact_uri", "sia_base", "sender_name", "recipient_name")
00289
00290 bpki_cms_cert = None
00291 bpki_cms_glue = None
00292 bpki_https_cert = None
00293 bpki_https_glue = None
00294
00295 def repository(self):
00296 """Fetch repository object to which this parent object links."""
00297 return repository_elt.sql_fetch(self.gctx, self.repository_id)
00298
00299 def cas(self):
00300 """Fetch all CA objects that link to this parent object."""
00301 return rpki.rpki_engine.ca_obj.sql_fetch_where(self.gctx, "parent_id = %s", (self.parent_id,))
00302
00303 def serve_post_save_hook(self, q_pdu, r_pdu):
00304 """Extra server actions for parent_elt."""
00305 if q_pdu.rekey:
00306 self.serve_rekey()
00307 if q_pdu.revoke:
00308 self.serve_revoke()
00309 self.unimplemented_control("reissue")
00310
00311 def serve_rekey(self):
00312 """Handle a left-right rekey action for this parent."""
00313 for ca in self.cas():
00314 ca.rekey()
00315
00316 def serve_revoke(self):
00317 """Handle a left-right revoke action for this parent."""
00318 for ca in self.cas():
00319 ca.revoke()
00320
00321 def query_up_down(self, q_pdu):
00322 """Client code for sending one up-down query PDU to this parent.
00323
00324 I haven't figured out yet whether this method should do something
00325 clever like dispatching via a method in the response PDU payload,
00326 or just hand back the whole response to the caller. In the long
00327 run this will have to become event driven with a context object
00328 that has methods of its own, but as this method is common code for
00329 several different queries and I don't yet know what the response
00330 processing looks like, it's too soon to tell what will make sense.
00331
00332 For now, keep this dead simple lock step, rewrite it later.
00333 """
00334
00335 rpki.log.trace()
00336
00337 bsc = self.bsc()
00338 if bsc is None:
00339 raise rpki.exceptions.BSCNotFound, "Could not find BSC %s" % self.bsc_id
00340
00341 q_msg = rpki.up_down.message_pdu.make_query(
00342 payload = q_pdu,
00343 sender = self.sender_name,
00344 recipient = self.recipient_name)
00345
00346 q_cms = rpki.up_down.cms_msg.wrap(q_msg, bsc.private_key_id,
00347 bsc.signing_cert,
00348 bsc.signing_cert_crl)
00349
00350 der = rpki.https.client(server_ta = (self.gctx.bpki_ta,
00351 self.self().bpki_cert, self.self().bpki_glue,
00352 self.bpki_https_cert, self.bpki_https_glue),
00353 client_key = bsc.private_key_id,
00354 client_cert = bsc.signing_cert,
00355 msg = q_cms,
00356 url = self.peer_contact_uri)
00357
00358 r_msg = rpki.up_down.cms_msg.unwrap(der, (self.gctx.bpki_ta,
00359 self.self().bpki_cert, self.self().bpki_glue,
00360 self.bpki_cms_cert, self.bpki_cms_glue))
00361
00362 r_msg.payload.check_response()
00363 return r_msg
00364
00365
00366 class child_elt(data_elt):
00367 """<child/> element."""
00368
00369 element_name = "child"
00370 attributes = ("action", "tag", "self_id", "child_id", "bsc_id")
00371 elements = ("bpki_cert", "bpki_glue")
00372 booleans = ("reissue", )
00373
00374 sql_template = rpki.sql.template("child", "child_id", "self_id", "bsc_id",
00375 ("bpki_cert", rpki.x509.X509),
00376 ("bpki_glue", rpki.x509.X509))
00377
00378 bpki_cert = None
00379 bpki_glue = None
00380 clear_https_ta_cache = False
00381
00382 def child_certs(self, ca_detail = None, ski = None, unique = False):
00383 """Fetch all child_cert objects that link to this child object."""
00384 return rpki.rpki_engine.child_cert_obj.fetch(self.gctx, self, ca_detail, ski, unique)
00385
00386 def parents(self):
00387 """Fetch all parent objects that link to self object to which this child object links."""
00388 return parent_elt.sql_fetch_where(self.gctx, "self_id = %s", (self.self_id,))
00389
00390 def ca_from_class_name(self, class_name):
00391 """Fetch the CA corresponding to an up-down class_name."""
00392 if not class_name.isdigit():
00393 raise rpki.exceptions.BadClassNameSyntax, "Bad class name %s" % class_name
00394 ca = rpki.rpki_engine.ca_obj.sql_fetch(self.gctx, long(class_name))
00395 parent = ca.parent()
00396 if self.self_id != parent.self_id:
00397 raise rpki.exceptions.ClassNameMismatch, "child.self_id = %d, parent.self_id = %d" % (self.self_id, parent.self_id)
00398 return ca
00399
00400 def serve_post_save_hook(self, q_pdu, r_pdu):
00401 """Extra server actions for child_elt."""
00402 self.unimplemented_control("reissue")
00403 if self.clear_https_ta_cache:
00404 self.gctx.clear_https_ta_cache()
00405 self.clear_https_ta_cache = False
00406
00407 def endElement(self, stack, name, text):
00408 """Handle subelements of <child/> element. These require special
00409 handling because modifying them invalidates the HTTPS trust anchor
00410 cache.
00411 """
00412 rpki.xml_utils.data_elt.endElement(self, stack, name, text)
00413 if name in self.elements:
00414 self.clear_https_ta_cache = True
00415
00416 def serve_up_down(self, query):
00417 """Outer layer of server handling for one up-down PDU from this child."""
00418
00419 rpki.log.trace()
00420
00421 bsc = self.bsc()
00422 if bsc is None:
00423 raise rpki.exceptions.BSCNotFound, "Could not find BSC %s" % self.bsc_id
00424 q_msg = rpki.up_down.cms_msg.unwrap(query, (self.gctx.bpki_ta,
00425 self.self().bpki_cert, self.self().bpki_glue,
00426 self.bpki_cert, self.bpki_glue))
00427 q_msg.payload.gctx = self.gctx
00428 if enforce_strict_up_down_xml_sender and q_msg.sender != str(self.child_id):
00429 raise rpki.exceptions.BadSender, "Unexpected XML sender %s" % q_msg.sender
00430 try:
00431 r_msg = q_msg.serve_top_level(self)
00432 except Exception, data:
00433 rpki.log.error(traceback.format_exc())
00434 r_msg = q_msg.serve_error(data)
00435
00436
00437
00438
00439
00440 r_cms = rpki.up_down.cms_msg.wrap(r_msg, bsc.private_key_id,
00441 bsc.signing_cert, bsc.signing_cert_crl)
00442 return r_cms
00443
00444 class repository_elt(data_elt):
00445 """<repository/> element."""
00446
00447 element_name = "repository"
00448 attributes = ("action", "tag", "self_id", "repository_id", "bsc_id", "peer_contact_uri")
00449 elements = ("bpki_cms_cert", "bpki_cms_glue", "bpki_https_cert", "bpki_https_glue")
00450
00451 sql_template = rpki.sql.template("repository", "repository_id", "self_id", "bsc_id", "peer_contact_uri",
00452 ("bpki_cms_cert", rpki.x509.X509), ("bpki_cms_glue", rpki.x509.X509),
00453 ("bpki_https_cert", rpki.x509.X509), ("bpki_https_glue", rpki.x509.X509))
00454
00455 bpki_cms_cert = None
00456 bpki_cms_glue = None
00457 bpki_https_cert = None
00458 bpki_https_glue = None
00459
00460 use_pubd = True
00461
00462 def parents(self):
00463 """Fetch all parent objects that link to this repository object."""
00464 return parent_elt.sql_fetch_where(self.gctx, "repository_id = %s", (self.repository_id,))
00465
00466 @staticmethod
00467 def uri_to_filename(base, uri):
00468 """Convert a URI to a filename. [TEMPORARY]"""
00469 if not uri.startswith("rsync://"):
00470 raise rpki.exceptions.BadURISyntax
00471 filename = base + uri[len("rsync://"):]
00472 if filename.find("//") >= 0 or filename.find("/../") >= 0 or filename.endswith("/.."):
00473 raise rpki.exceptions.BadURISyntax
00474 return filename
00475
00476 @classmethod
00477 def object_write(cls, base, uri, obj):
00478 """Write an object to disk. [TEMPORARY]"""
00479 rpki.log.trace()
00480 filename = cls.uri_to_filename(base, uri)
00481 dirname = os.path.dirname(filename)
00482 if not os.path.isdir(dirname):
00483 os.makedirs(dirname)
00484 f = open(filename, "wb")
00485 f.write(obj.get_DER())
00486 f.close()
00487
00488 @classmethod
00489 def object_delete(cls, base, uri):
00490 """Delete an object from disk. [TEMPORARY]"""
00491 rpki.log.trace()
00492 os.remove(cls.uri_to_filename(base, uri))
00493
00494 def call_pubd(self, *pdus):
00495 """Send a message to publication daemon and return the response."""
00496 rpki.log.trace()
00497 bsc = self.bsc()
00498 q_msg = rpki.publication.msg(pdus)
00499 q_msg.type = "query"
00500 q_cms = rpki.publication.cms_msg.wrap(q_msg, bsc.private_key_id, bsc.signing_cert, bsc.signing_cert_crl)
00501 bpki_ta_path = (self.gctx.bpki_ta, self.self().bpki_cert, self.self().bpki_glue, self.bpki_https_cert, self.bpki_https_glue)
00502 r_cms = rpki.https.client(
00503 client_key = bsc.private_key_id,
00504 client_cert = bsc.signing_cert,
00505 server_ta = bpki_ta_path,
00506 url = self.peer_contact_uri,
00507 msg = q_cms)
00508 r_msg = rpki.publication.cms_msg.unwrap(r_cms, bpki_ta_path)
00509 assert len(r_msg) == 1
00510 return r_msg[0]
00511
00512 def publish(self, obj, uri):
00513 """Placeholder for publication operation. [TEMPORARY]"""
00514 rpki.log.trace()
00515 rpki.log.info("Publishing %s as %s" % (repr(obj), repr(uri)))
00516 if self.use_pubd:
00517 self.call_pubd(rpki.publication.obj2elt[type(obj)].make_pdu(action = "publish", uri = uri, payload = obj))
00518 else:
00519 self.object_write(self.gctx.publication_kludge_base, uri, obj)
00520
00521 def withdraw(self, obj, uri):
00522 """Placeholder for publication withdrawal operation. [TEMPORARY]"""
00523 rpki.log.trace()
00524 rpki.log.info("Withdrawing %s from at %s" % (repr(obj), repr(uri)))
00525 if self.use_pubd:
00526 self.call_pubd(rpki.publication.obj2elt[type(obj)].make_pdu(action = "withdraw", uri = uri))
00527 else:
00528 self.object_delete(self.gctx.publication_kludge_base, uri)
00529
00530 class route_origin_elt(data_elt):
00531 """<route_origin/> element."""
00532
00533 element_name = "route_origin"
00534 attributes = ("action", "tag", "self_id", "route_origin_id", "as_number", "ipv4", "ipv6")
00535 booleans = ("suppress_publication",)
00536
00537 sql_template = rpki.sql.template("route_origin", "route_origin_id", "ca_detail_id",
00538 "self_id", "as_number",
00539 ("roa", rpki.x509.ROA),
00540 ("cert", rpki.x509.X509))
00541
00542 ca_detail_id = None
00543 cert = None
00544 roa = None
00545
00546 def sql_fetch_hook(self):
00547 """Extra SQL fetch actions for route_origin_elt -- handle prefix list."""
00548 self.ipv4 = rpki.resource_set.roa_prefix_set_ipv4.from_sql(
00549 self.gctx.sql,
00550 """
00551 SELECT address, prefixlen, max_prefixlen FROM route_origin_prefix
00552 WHERE route_origin_id = %s AND address NOT LIKE '%:%'
00553 """, (self.route_origin_id,))
00554 self.ipv6 = rpki.resource_set.roa_prefix_set_ipv6.from_sql(
00555 self.gctx.sql,
00556 """
00557 SELECT address, prefixlen, max_prefixlen FROM route_origin_prefix
00558 WHERE route_origin_id = %s AND address LIKE '%:%'
00559 """, (self.route_origin_id,))
00560
00561 def sql_insert_hook(self):
00562 """Extra SQL insert actions for route_origin_elt -- handle address ranges."""
00563 if self.ipv4 or self.ipv6:
00564 self.gctx.sql.executemany("""
00565 INSERT route_origin_prefix (route_origin_id, address, prefixlen, max_prefixlen)
00566 VALUES (%s, %s, %s, %s)""",
00567 ((self.route_origin_id, x.address, x.prefixlen, x.max_prefixlen)
00568 for x in (self.ipv4 or []) + (self.ipv6 or [])))
00569
00570 def sql_delete_hook(self):
00571 """Extra SQL delete actions for route_origin_elt -- handle address ranges."""
00572 self.gctx.sql.execute("DELETE FROM route_origin_prefix WHERE route_origin_id = %s", (self.route_origin_id,))
00573
00574 def ca_detail(self):
00575 """Fetch all ca_detail objects that link to this route_origin object."""
00576 return rpki.rpki_engine.ca_detail_obj.sql_fetch(self.gctx, self.ca_detail_id)
00577
00578 def serve_post_save_hook(self, q_pdu, r_pdu):
00579 """Extra server actions for route_origin_elt."""
00580 self.unimplemented_control("suppress_publication")
00581
00582 def startElement(self, stack, name, attrs):
00583 """Handle <route_origin/> element. This requires special
00584 processing due to the data types of some of the attributes.
00585 """
00586 assert name == "route_origin", "Unexpected name %s, stack %s" % (name, stack)
00587 self.read_attrs(attrs)
00588 if self.as_number is not None:
00589 self.as_number = long(self.as_number)
00590 if self.ipv4 is not None:
00591 self.ipv4 = rpki.resource_set.roa_prefix_set_ipv4(self.ipv4)
00592 if self.ipv6 is not None:
00593 self.ipv6 = rpki.resource_set.roa_prefix_set_ipv6(self.ipv6)
00594
00595 def update_roa(self):
00596 """Bring this route_origin's ROA up to date if necesssary."""
00597
00598 if self.roa is None:
00599 return self.generate_roa()
00600
00601 ca_detail = self.ca_detail()
00602
00603 if ca_detail.state != "active":
00604 return self.regenerate_roa()
00605
00606 regen_margin = rpki.sundial.timedelta(seconds = self.self().regen_margin)
00607
00608 if rpki.sundial.now() + regen_margin > self.cert.getNotAfter():
00609 return self.regenerate_roa()
00610
00611 ca_resources = ca_detail.latest_ca_cert.get_3779resources()
00612 ee_resources = self.cert.get_3779resources()
00613
00614 if ee_resources.oversized(ca_resources):
00615 return self.regenerate_roa()
00616
00617 v4 = self.ipv4.to_resource_set() if self.ipv4 is not None else rpki.resource_set.resource_set_ipv4()
00618 v6 = self.ipv6.to_resource_set() if self.ipv6 is not None else rpki.resource_set.resource_set_ipv6()
00619
00620 if ee_resources.v4 != v4 or ee_resources.v6 != v6:
00621 return self.regenerate_roa()
00622
00623 def generate_roa(self):
00624 """Generate a ROA based on this <route_origin/> object.
00625
00626 At present this does not support ROAs with multiple signatures
00627 (neither does the current CMS code).
00628
00629 At present we have no way of performing a direct lookup from a
00630 desired set of resources to a covering certificate, so we have to
00631 search. This could be quite slow if we have a lot of active
00632 ca_detail objects. Punt on the issue for now, revisit if
00633 profiling shows this as a hotspot.
00634
00635 Once we have the right covering certificate, we generate the ROA
00636 payload, generate a new EE certificate, use the EE certificate to
00637 sign the ROA payload, publish the result, then throw away the
00638 private key for the EE cert, all per the ROA specification. This
00639 implies that generating a lot of ROAs will tend to thrash
00640 /dev/random, but there is not much we can do about that.
00641 """
00642
00643 if self.ipv4 is None and self.ipv6 is None:
00644 rpki.log.warn("Can't generate ROA for empty prefix list")
00645 return
00646
00647
00648
00649
00650
00651
00652
00653
00654 v4 = self.ipv4.to_resource_set() if self.ipv4 is not None else rpki.resource_set.resource_set_ipv4()
00655 v6 = self.ipv6.to_resource_set() if self.ipv6 is not None else rpki.resource_set.resource_set_ipv6()
00656
00657 ca_detail = self.ca_detail()
00658 if ca_detail is None or ca_detail.state != "active":
00659 ca_detail = None
00660 for parent in self.self().parents():
00661 for ca in parent.cas():
00662 ca_detail = ca.fetch_active()
00663 if ca_detail is not None:
00664 resources = ca_detail.latest_ca_cert.get_3779resources()
00665 if v4.issubset(resources.v4) and v6.issubset(resources.v6):
00666 break
00667 ca_detail = None
00668 if ca_detail is not None:
00669 break
00670
00671 if ca_detail is None:
00672 rpki.log.warn("generate_roa() could not find a covering certificate")
00673 return
00674
00675 resources = rpki.resource_set.resource_bag(v4 = v4, v6 = v6)
00676
00677 keypair = rpki.x509.RSA.generate()
00678
00679 sia = ((rpki.oids.name2oid["id-ad-signedObject"], ("uri", self.roa_uri(ca, keypair))),)
00680
00681 self.cert = ca_detail.issue_ee(ca, resources, keypair.get_RSApublic(), sia = sia)
00682 self.roa = rpki.x509.ROA.build(self.as_number, self.ipv4, self.ipv6, keypair, (self.cert,))
00683 self.ca_detail_id = ca_detail.ca_detail_id
00684 self.sql_store()
00685
00686 repository = parent.repository()
00687 repository.publish(self.roa, self.roa_uri(ca))
00688 repository.publish(self.cert, self.ee_uri(ca))
00689 ca_detail.generate_manifest()
00690
00691 def withdraw_roa(self, regenerate = False):
00692 """Withdraw ROA associated with this route_origin.
00693
00694 In order to preserve make-before-break properties without
00695 duplicating code, this method also handles generating a
00696 replacement ROA when requested.
00697 """
00698
00699 ca_detail = self.ca_detail()
00700 ca = ca_detail.ca()
00701 repository = ca.parent().repository()
00702 cert = self.cert
00703 roa = self.roa
00704 roa_uri = self.roa_uri(ca)
00705 ee_uri = self.ee_uri(ca)
00706
00707 if ca_detail.state != 'active':
00708 self.ca_detail_id = None
00709 if regenerate:
00710 self.generate_roa()
00711
00712 rpki.log.debug("Withdrawing ROA and revoking its EE cert")
00713 rpki.rpki_engine.revoked_cert_obj.revoke(cert = cert, ca_detail = ca_detail)
00714 repository.withdraw(roa, roa_uri)
00715 repository.withdraw(cert, ee_uri)
00716 self.gctx.sql.sweep()
00717 ca_detail.generate_crl()
00718 ca_detail.generate_manifest()
00719
00720 def regenerate_roa(self):
00721 """Reissue ROA associated with this route_origin."""
00722 self.withdraw_roa(regenerate = True)
00723
00724 def roa_uri(self, ca, key = None):
00725 """Return the publication URI for this route_origin's ROA."""
00726 return ca.sia_uri + (key or self.cert).gSKI() + ".roa"
00727
00728 def ee_uri_tail(self):
00729 """Return the tail (filename) portion of the URI for this route_origin's ROA's EE certificate."""
00730 return self.cert.gSKI() + ".cer"
00731
00732 def ee_uri(self, ca):
00733 """Return the publication URI for this route_origin's ROA's EE certificate."""
00734 return ca.sia_uri + self.ee_uri_tail()
00735
00736 class list_resources_elt(rpki.xml_utils.base_elt, left_right_namespace):
00737 """<list_resources/> element."""
00738
00739 element_name = "list_resources"
00740 attributes = ("self_id", "tag", "child_id", "valid_until", "asn", "ipv4", "ipv6", "subject_name")
00741 valid_until = None
00742
00743 def startElement(self, stack, name, attrs):
00744 """Handle <list_resources/> element. This requires special
00745 handling due to the data types of some of the attributes.
00746 """
00747 assert name == "list_resources", "Unexpected name %s, stack %s" % (name, stack)
00748 self.read_attrs(attrs)
00749 if isinstance(self.valid_until, str):
00750 self.valid_until = rpki.sundial.datetime.fromXMLtime(self.valid_until)
00751 if self.asn is not None:
00752 self.asn = rpki.resource_set.resource_set_as(self.asn)
00753 if self.ipv4 is not None:
00754 self.ipv4 = rpki.resource_set.resource_set_ipv4(self.ipv4)
00755 if self.ipv6 is not None:
00756 self.ipv6 = rpki.resource_set.resource_set_ipv6(self.ipv6)
00757
00758 def toXML(self):
00759 """Generate <list_resources/> element. This requires special
00760 handling due to the data types of some of the attributes.
00761 """
00762 elt = self.make_elt()
00763 if isinstance(self.valid_until, int):
00764 elt.set("valid_until", self.valid_until.toXMLtime())
00765 return elt
00766
00767 class report_error_elt(rpki.xml_utils.base_elt, left_right_namespace):
00768 """<report_error/> element."""
00769
00770 element_name = "report_error"
00771 attributes = ("tag", "self_id", "error_code")
00772
00773 @classmethod
00774 def from_exception(cls, exc, self_id = None):
00775 """Generate a <report_error/> element from an exception."""
00776 self = cls()
00777 self.self_id = self_id
00778 self.error_code = exc.__class__.__name__
00779 return self
00780
00781 class msg(rpki.xml_utils.msg, left_right_namespace):
00782 """Left-right PDU."""
00783
00784
00785
00786 version = 1
00787
00788
00789
00790 pdus = dict((x.element_name, x)
00791 for x in (self_elt, child_elt, parent_elt, bsc_elt, repository_elt,
00792 route_origin_elt, list_resources_elt, report_error_elt))
00793
00794 def serve_top_level(self, gctx):
00795 """Serve one msg PDU."""
00796 r_msg = self.__class__()
00797 r_msg.type = "reply"
00798 for q_pdu in self:
00799 q_pdu.gctx = gctx
00800 q_pdu.serve_dispatch(r_msg)
00801 return r_msg
00802
00803 class sax_handler(rpki.xml_utils.sax_handler):
00804 """SAX handler for Left-Right protocol."""
00805
00806 pdu = msg
00807 name = "msg"
00808 version = "1"
00809
00810 class cms_msg(rpki.x509.XML_CMS_object):
00811 """Class to hold a CMS-signed left-right PDU."""
00812
00813 encoding = "us-ascii"
00814 schema = rpki.relaxng.left_right
00815 saxify = sax_handler.saxify