00001 """
00002 RPKI "publication" protocol.
00003
00004 $Id: publication.py 3107 2010-03-16 23:13:08Z sra $
00005
00006 Copyright (C) 2009-2010 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 path = self.uri.split("/")[3:]
00257 if not self.gctx.publication_multimodule:
00258 del path[0]
00259 path.insert(0, self.gctx.publication_base.rstrip("/"))
00260 filename = "/".join(path)
00261 if "/../" in filename or filename.endswith("/.."):
00262 raise rpki.exceptions.BadURISyntax, filename
00263 return filename
00264
00265 @classmethod
00266 def make_publish(cls, uri, obj, tag = None):
00267 """
00268 Construct a publication PDU.
00269 """
00270 assert cls.payload_type is not None and type(obj) is cls.payload_type
00271 return cls.make_pdu(action = "publish", uri = uri, payload = obj, tag = tag)
00272
00273 @classmethod
00274 def make_withdraw(cls, uri, obj, tag = None):
00275 """
00276 Construct a withdrawal PDU.
00277 """
00278 assert cls.payload_type is not None and type(obj) is cls.payload_type
00279 return cls.make_pdu(action = "withdraw", uri = uri, tag = tag)
00280
00281 def raise_if_error(self):
00282 """
00283 No-op, since this is not a <report_error/> PDU.
00284 """
00285 pass
00286
00287 class certificate_elt(publication_object_elt):
00288 """
00289 <certificate/> element.
00290 """
00291
00292 element_name = "certificate"
00293 payload_type = rpki.x509.X509
00294
00295 class crl_elt(publication_object_elt):
00296 """
00297 <crl/> element.
00298 """
00299
00300 element_name = "crl"
00301 payload_type = rpki.x509.CRL
00302
00303 class manifest_elt(publication_object_elt):
00304 """
00305 <manifest/> element.
00306 """
00307
00308 element_name = "manifest"
00309 payload_type = rpki.x509.SignedManifest
00310
00311 class roa_elt(publication_object_elt):
00312 """
00313 <roa/> element.
00314 """
00315
00316 element_name = "roa"
00317 payload_type = rpki.x509.ROA
00318
00319 publication_object_elt.obj2elt = dict((e.payload_type, e) for e in (certificate_elt, crl_elt, manifest_elt, roa_elt))
00320
00321 class report_error_elt(rpki.xml_utils.text_elt, publication_namespace):
00322 """
00323 <report_error/> element.
00324 """
00325
00326 element_name = "report_error"
00327 attributes = ("tag", "error_code")
00328 text_attribute = "error_text"
00329
00330 error_text = None
00331
00332 @classmethod
00333 def from_exception(cls, e, tag = None):
00334 """
00335 Generate a <report_error/> element from an exception.
00336 """
00337 self = cls()
00338 self.tag = tag
00339 self.error_code = e.__class__.__name__
00340 self.error_text = str(e)
00341 return self
00342
00343 def __str__(self):
00344 s = ""
00345 if getattr(self, "tag", None) is not None:
00346 s += "[%s] " % self.tag
00347 s += self.error_code
00348 if getattr(self, "error_text", None) is not None:
00349 s += ": " + self.error_text
00350 return s
00351
00352 def raise_if_error(self):
00353 """
00354 Raise exception associated with this <report_error/> PDU.
00355 """
00356 t = rpki.exceptions.__dict__.get(self.error_code)
00357 if isinstance(t, type) and issubclass(t, rpki.exceptions.RPKI_Exception):
00358 raise t, getattr(self, "text", None)
00359 else:
00360 raise rpki.exceptions.BadPublicationReply, "Unexpected response from pubd: %s" % self
00361
00362 class msg(rpki.xml_utils.msg, publication_namespace):
00363 """
00364 Publication PDU.
00365 """
00366
00367
00368
00369 version = 1
00370
00371
00372
00373 pdus = dict((x.element_name, x)
00374 for x in (config_elt, client_elt, certificate_elt, crl_elt, manifest_elt, roa_elt, report_error_elt))
00375
00376 def serve_top_level(self, gctx, client, cb):
00377 """
00378 Serve one msg PDU.
00379 """
00380 if not self.is_query():
00381 raise rpki.exceptions.BadQuery, "Message type is not query"
00382 r_msg = self.__class__.reply()
00383
00384 def loop(iterator, q_pdu):
00385
00386 def fail(e):
00387 if not isinstance(e, rpki.exceptions.NotFound):
00388 rpki.log.traceback()
00389 r_msg.append(report_error_elt.from_exception(e, q_pdu.tag))
00390 cb(r_msg)
00391
00392 try:
00393 q_pdu.gctx = gctx
00394 q_pdu.client = client
00395 q_pdu.serve_dispatch(r_msg, iterator, fail)
00396 except (rpki.async.ExitNow, SystemExit):
00397 raise
00398 except Exception, e:
00399 fail(e)
00400
00401 def done():
00402 cb(r_msg)
00403
00404 rpki.async.iterator(self, loop, done)
00405
00406 class sax_handler(rpki.xml_utils.sax_handler):
00407 """
00408 SAX handler for publication protocol.
00409 """
00410
00411 pdu = msg
00412 name = "msg"
00413 version = "1"
00414
00415 class cms_msg(rpki.x509.XML_CMS_object):
00416 """
00417 Class to hold a CMS-signed publication PDU.
00418 """
00419
00420 encoding = "us-ascii"
00421 schema = rpki.relaxng.publication
00422 saxify = sax_handler.saxify