00001 """
00002 RPKI "publication" protocol.
00003
00004 $Id: publication.py 2935 2010-01-07 17:23:05Z sra $
00005
00006 Copyright (C) 2009 Internet Systems Consortium ("ISC")
00007
00008 Permission to use, copy, modify, and distribute this software for any
00009 purpose with or without fee is hereby granted, provided that the above
00010 copyright notice and this permission notice appear in all copies.
00011
00012 THE SOFTWARE IS PROVIDED "AS IS" AND ISC DISCLAIMS ALL WARRANTIES WITH
00013 REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
00014 AND FITNESS. IN NO EVENT SHALL ISC BE LIABLE FOR ANY SPECIAL, DIRECT,
00015 INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
00016 LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE
00017 OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
00018 PERFORMANCE OF THIS SOFTWARE.
00019
00020 Portions copyright (C) 2007--2008 American Registry for Internet Numbers ("ARIN")
00021
00022 Permission to use, copy, modify, and distribute this software for any
00023 purpose with or without fee is hereby granted, provided that the above
00024 copyright notice and this permission notice appear in all copies.
00025
00026 THE SOFTWARE IS PROVIDED "AS IS" AND ARIN DISCLAIMS ALL WARRANTIES WITH
00027 REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
00028 AND FITNESS. IN NO EVENT SHALL ARIN BE LIABLE FOR ANY SPECIAL, DIRECT,
00029 INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
00030 LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE
00031 OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
00032 PERFORMANCE OF THIS SOFTWARE.
00033 """
00034
00035 import base64, os, errno
00036 import rpki.resource_set, rpki.x509, rpki.sql, rpki.exceptions, rpki.xml_utils
00037 import rpki.https, rpki.up_down, rpki.relaxng, rpki.sundial, rpki.log, rpki.roa
00038
00039 class publication_namespace(object):
00040 """
00041 XML namespace parameters for publication protocol.
00042 """
00043
00044 xmlns = "http://www.hactrn.net/uris/rpki/publication-spec/"
00045 nsmap = { None : xmlns }
00046
00047 class control_elt(rpki.xml_utils.data_elt, rpki.sql.sql_persistent, publication_namespace):
00048 """
00049 Virtual class for control channel objects.
00050 """
00051
00052 def serve_dispatch(self, r_msg, cb, eb):
00053 """
00054 Action dispatch handler. This needs special handling because we
00055 need to make sure that this PDU arrived via the control channel.
00056 """
00057 if self.client is not None:
00058 raise rpki.exceptions.BadQuery, "Control query received on client channel"
00059 rpki.xml_utils.data_elt.serve_dispatch(self, r_msg, cb, eb)
00060
00061 class config_elt(control_elt):
00062 """
00063 <config/> element. This is a little weird because there should
00064 never be more than one row in the SQL config table, but we have to
00065 put the BPKI CRL somewhere and SQL is the least bad place available.
00066
00067 So we reuse a lot of the SQL machinery, but we nail config_id at 1,
00068 we don't expose it in the XML protocol, and we only support the get
00069 and set actions.
00070 """
00071
00072 attributes = ("action", "tag")
00073 element_name = "config"
00074 elements = ("bpki_crl",)
00075
00076 sql_template = rpki.sql.template("config", "config_id", ("bpki_crl", rpki.x509.CRL))
00077
00078 wired_in_config_id = 1
00079
00080 def startElement(self, stack, name, attrs):
00081 """
00082 StartElement() handler for config object. This requires special
00083 handling because of the weird way we treat config_id.
00084 """
00085 control_elt.startElement(self, stack, name, attrs)
00086 self.config_id = self.wired_in_config_id
00087
00088 @classmethod
00089 def fetch(cls, gctx):
00090 """
00091 Fetch the config object from SQL. This requires special handling
00092 because of the weird way we treat config_id.
00093 """
00094 return cls.sql_fetch(gctx, cls.wired_in_config_id)
00095
00096 def serve_set(self, r_msg, cb, eb):
00097 """
00098 Handle a set action. This requires special handling because
00099 config doesn't support the create method.
00100 """
00101 if self.sql_fetch(self.gctx, self.config_id) is None:
00102 control_elt.serve_create(self, r_msg, cb, eb)
00103 else:
00104 control_elt.serve_set(self, r_msg, cb, eb)
00105
00106 def serve_fetch_one_maybe(self):
00107 """
00108 Find the config object on which a get or set method should
00109 operate.
00110 """
00111 return self.sql_fetch(self.gctx, self.config_id)
00112
00113 class client_elt(control_elt):
00114 """
00115 <client/> element.
00116 """
00117
00118 element_name = "client"
00119 attributes = ("action", "tag", "client_handle", "base_uri")
00120 elements = ("bpki_cert", "bpki_glue")
00121
00122 sql_template = rpki.sql.template("client", "client_id", "client_handle", "base_uri", ("bpki_cert", rpki.x509.X509), ("bpki_glue", rpki.x509.X509))
00123
00124 base_uri = None
00125 bpki_cert = None
00126 bpki_glue = None
00127
00128 clear_https_ta_cache = False
00129
00130 def endElement(self, stack, name, text):
00131 """
00132 Handle subelements of <client/> element. These require special
00133 handling because modifying them invalidates the HTTPS trust anchor
00134 cache.
00135 """
00136 control_elt.endElement(self, stack, name, text)
00137 if name in self.elements:
00138 self.clear_https_ta_cache = True
00139
00140 def serve_post_save_hook(self, q_pdu, r_pdu, cb, eb):
00141 """
00142 Extra server actions for client_elt.
00143 """
00144 if self.clear_https_ta_cache:
00145 self.gctx.clear_https_ta_cache()
00146 self.clear_https_ta_cache = False
00147 cb()
00148
00149 def serve_fetch_one_maybe(self):
00150 """
00151 Find the client object on which a get, set, or destroy method
00152 should operate, or which would conflict with a create method.
00153 """
00154 return self.sql_fetch_where1(self.gctx, "client_handle = %s", self.client_handle)
00155
00156 def serve_fetch_all(self):
00157 """Find client objects on which a list method should operate."""
00158 return self.sql_fetch_all(self.gctx)
00159
00160 def check_allowed_uri(self, uri):
00161 if not uri.startswith(self.base_uri):
00162 raise rpki.exceptions.ForbiddenURI
00163
00164 class publication_object_elt(rpki.xml_utils.base_elt, publication_namespace):
00165 """
00166 Virtual class for publishable objects. These have very similar
00167 syntax, differences lie in underlying datatype and methods. XML
00168 methods are a little different from the pattern used for objects
00169 that support the create/set/get/list/destroy actions, but
00170 publishable objects don't go in SQL either so these classes would be
00171 different in any case.
00172 """
00173
00174 attributes = ("action", "tag", "client_handle", "uri")
00175 payload_type = None
00176 payload = None
00177
00178 def endElement(self, stack, name, text):
00179 """
00180 Handle a publishable element element.
00181 """
00182 assert name == self.element_name, "Unexpected name %s, stack %s" % (name, stack)
00183 if text:
00184 self.payload = self.payload_type(Base64 = text)
00185 stack.pop()
00186
00187 def toXML(self):
00188 """
00189 Generate XML element for publishable object.
00190 """
00191 elt = self.make_elt()
00192 if self.payload:
00193 elt.text = base64.b64encode(self.payload.get_DER())
00194 return elt
00195
00196 def serve_dispatch(self, r_msg, cb, eb):
00197 """
00198 Action dispatch handler.
00199 """
00200 try:
00201 if self.client is None:
00202 raise rpki.exceptions.BadQuery, "Client query received on control channel"
00203 dispatch = { "publish" : self.serve_publish,
00204 "withdraw" : self.serve_withdraw }
00205 if self.action not in dispatch:
00206 raise rpki.exceptions.BadQuery, "Unexpected query: action %s" % self.action
00207 self.client.check_allowed_uri(self.uri)
00208 dispatch[self.action]()
00209 r_pdu = self.__class__()
00210 r_pdu.action = self.action
00211 r_pdu.tag = self.tag
00212 r_pdu.uri = self.uri
00213 r_msg.append(r_pdu)
00214 cb()
00215 except rpki.exceptions.NoObjectAtURI, e:
00216
00217
00218 r_msg.append(report_error_elt.from_exception(e, self.tag))
00219 cb()
00220
00221 def serve_publish(self):
00222 """
00223 Publish an object.
00224 """
00225 rpki.log.info("Publishing %r as %r" % (self.payload, self.uri))
00226 filename = self.uri_to_filename()
00227 filename_tmp = filename + ".tmp"
00228 dirname = os.path.dirname(filename)
00229 if not os.path.isdir(dirname):
00230 os.makedirs(dirname)
00231 f = open(filename_tmp, "wb")
00232 f.write(self.payload.get_DER())
00233 f.close()
00234 os.rename(filename_tmp, filename)
00235
00236 def serve_withdraw(self):
00237 """
00238 Withdraw an object.
00239 """
00240 rpki.log.info("Withdrawing %r" % (self.uri,))
00241 filename = self.uri_to_filename()
00242 try:
00243 os.remove(filename)
00244 except OSError, e:
00245 if e.errno == errno.ENOENT:
00246 raise rpki.exceptions.NoObjectAtURI, "No object published at %r" % self.uri
00247 else:
00248 raise
00249
00250 def uri_to_filename(self):
00251 """
00252 Convert a URI to a local filename.
00253 """
00254 if not self.uri.startswith("rsync://"):
00255 raise rpki.exceptions.BadURISyntax, self.uri
00256 u = 0
00257 for i in xrange(4):
00258 u = self.uri.index("/", u + 1)
00259 filename = self.gctx.publication_base.rstrip("/") + self.uri[u:]
00260 if "//" in filename or "/../" in filename or filename.endswith("/.."):
00261 raise rpki.exceptions.BadURISyntax, filename
00262 return filename
00263
00264 @classmethod
00265 def make_publish(cls, uri, obj, tag = None):
00266 """
00267 Construct a publication PDU.
00268 """
00269 assert cls.payload_type is not None and type(obj) is cls.payload_type
00270 return cls.make_pdu(action = "publish", uri = uri, payload = obj, tag = tag)
00271
00272 @classmethod
00273 def make_withdraw(cls, uri, obj, tag = None):
00274 """
00275 Construct a withdrawal PDU.
00276 """
00277 assert cls.payload_type is not None and type(obj) is cls.payload_type
00278 return cls.make_pdu(action = "withdraw", uri = uri, tag = tag)
00279
00280 def raise_if_error(self):
00281 """
00282 No-op, since this is not a <report_error/> PDU.
00283 """
00284 pass
00285
00286 class certificate_elt(publication_object_elt):
00287 """
00288 <certificate/> element.
00289 """
00290
00291 element_name = "certificate"
00292 payload_type = rpki.x509.X509
00293
00294 class crl_elt(publication_object_elt):
00295 """
00296 <crl/> element.
00297 """
00298
00299 element_name = "crl"
00300 payload_type = rpki.x509.CRL
00301
00302 class manifest_elt(publication_object_elt):
00303 """
00304 <manifest/> element.
00305 """
00306
00307 element_name = "manifest"
00308 payload_type = rpki.x509.SignedManifest
00309
00310 class roa_elt(publication_object_elt):
00311 """
00312 <roa/> element.
00313 """
00314
00315 element_name = "roa"
00316 payload_type = rpki.x509.ROA
00317
00318 publication_object_elt.obj2elt = dict((e.payload_type, e) for e in (certificate_elt, crl_elt, manifest_elt, roa_elt))
00319
00320 class report_error_elt(rpki.xml_utils.text_elt, publication_namespace):
00321 """
00322 <report_error/> element.
00323 """
00324
00325 element_name = "report_error"
00326 attributes = ("tag", "error_code")
00327 text_attribute = "error_text"
00328
00329 error_text = None
00330
00331 @classmethod
00332 def from_exception(cls, e, tag = None):
00333 """
00334 Generate a <report_error/> element from an exception.
00335 """
00336 self = cls()
00337 self.tag = tag
00338 self.error_code = e.__class__.__name__
00339 self.error_text = str(e)
00340 return self
00341
00342 def __str__(self):
00343 s = ""
00344 if getattr(self, "tag", None) is not None:
00345 s += "[%s] " % self.tag
00346 s += self.error_code
00347 if getattr(self, "error_text", None) is not None:
00348 s += ": " + self.error_text
00349 return s
00350
00351 def raise_if_error(self):
00352 """
00353 Raise exception associated with this <report_error/> PDU.
00354 """
00355 t = rpki.exceptions.__dict__.get(self.error_code)
00356 if isinstance(t, type) and issubclass(t, rpki.exceptions.RPKI_Exception):
00357 raise t, getattr(self, "text", None)
00358 else:
00359 raise rpki.exceptions.BadPublicationReply, "Unexpected response from pubd: %s" % self
00360
00361 class msg(rpki.xml_utils.msg, publication_namespace):
00362 """
00363 Publication PDU.
00364 """
00365
00366
00367
00368 version = 1
00369
00370
00371
00372 pdus = dict((x.element_name, x)
00373 for x in (config_elt, client_elt, certificate_elt, crl_elt, manifest_elt, roa_elt, report_error_elt))
00374
00375 def serve_top_level(self, gctx, client, cb):
00376 """
00377 Serve one msg PDU.
00378 """
00379 if not self.is_query():
00380 raise rpki.exceptions.BadQuery, "Message type is not query"
00381 r_msg = self.__class__.reply()
00382
00383 def loop(iterator, q_pdu):
00384
00385 def fail(e):
00386 if not isinstance(e, rpki.exceptions.NotFound):
00387 rpki.log.traceback()
00388 r_msg.append(report_error_elt.from_exception(e, q_pdu.tag))
00389 cb(r_msg)
00390
00391 try:
00392 q_pdu.gctx = gctx
00393 q_pdu.client = client
00394 q_pdu.serve_dispatch(r_msg, iterator, fail)
00395 except (rpki.async.ExitNow, SystemExit):
00396 raise
00397 except Exception, e:
00398 fail(e)
00399
00400 def done():
00401 cb(r_msg)
00402
00403 rpki.async.iterator(self, loop, done)
00404
00405 class sax_handler(rpki.xml_utils.sax_handler):
00406 """
00407 SAX handler for publication protocol.
00408 """
00409
00410 pdu = msg
00411 name = "msg"
00412 version = "1"
00413
00414 class cms_msg(rpki.x509.XML_CMS_object):
00415 """
00416 Class to hold a CMS-signed publication PDU.
00417 """
00418
00419 encoding = "us-ascii"
00420 schema = rpki.relaxng.publication
00421 saxify = sax_handler.saxify