00001 """RPKI "publication" protocol.
00002
00003 $Id: publication.py 1873 2008-06-12 02:49:41Z 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
00024 class publication_namespace(object):
00025 """XML namespace parameters for publication protocol."""
00026
00027 xmlns = "http://www.hactrn.net/uris/rpki/publication-spec/"
00028 nsmap = { None : xmlns }
00029
00030 class control_elt(rpki.xml_utils.data_elt, rpki.sql.sql_persistant, publication_namespace):
00031 """Virtual class for control channel objects."""
00032
00033 def serve_dispatch(self, r_msg, client):
00034 """Action dispatch handler. This needs special handling because
00035 we need to make sure that this PDU arrived via the control channel.
00036 """
00037 if client is not None:
00038 raise rpki.exceptions.BadQuery, "Control query received on client channel"
00039 rpki.xml_utils.data_elt.serve_dispatch(self, r_msg)
00040
00041 class config_elt(control_elt):
00042 """<config/> element. This is a little weird because there should
00043 never be more than one row in the SQL config table, but we have to
00044 put the BPKI CRL somewhere and SQL is the least bad place available.
00045
00046 So we reuse a lot of the SQL machinery, but we nail config_id at 1,
00047 we don't expose it in the XML protocol, and we only support the get
00048 and set actions.
00049 """
00050
00051 attributes = ("action", "tag")
00052 element_name = "config"
00053 elements = ("bpki_crl",)
00054
00055 sql_template = rpki.sql.template("config", "config_id", ("bpki_crl", rpki.x509.CRL))
00056
00057 wired_in_config_id = 1
00058
00059 def startElement(self, stack, name, attrs):
00060 """StartElement() handler for config object. This requires
00061 special handling because of the weird way we treat config_id.
00062 """
00063 control_elt.startElement(self, stack, name, attrs)
00064 self.config_id = self.wired_in_config_id
00065
00066 @classmethod
00067 def fetch(cls, gctx):
00068 """Fetch the config object from SQL. This requires special
00069 handling because of the weird way we treat config_id.
00070 """
00071 return cls.sql_fetch(gctx, cls.wired_in_config_id)
00072
00073 def serve_set(self, r_msg):
00074 """Handle a set action. This requires special handling because
00075 config we don't support the create method.
00076 """
00077 if self.sql_fetch(self.gctx, self.config_id) is None:
00078 control_elt.serve_create(self, r_msg)
00079 else:
00080 control_elt.serve_set(self, r_msg)
00081
00082 def serve_fetch_one(self):
00083 """Find the config object on which a get or set method should
00084 operate.
00085 """
00086 r = self.sql_fetch(self.gctx, self.config_id)
00087 if r is None:
00088 raise rpki.exceptions.NotFound
00089 return r
00090
00091 class client_elt(control_elt):
00092 """<client/> element."""
00093
00094 element_name = "client"
00095 attributes = ("action", "tag", "client_id", "base_uri")
00096 elements = ("bpki_cert", "bpki_glue")
00097
00098 sql_template = rpki.sql.template("client", "client_id", "base_uri", ("bpki_cert", rpki.x509.X509), ("bpki_glue", rpki.x509.X509))
00099
00100 base_uri = None
00101 bpki_cert = None
00102 bpki_glue = None
00103
00104 clear_https_ta_cache = False
00105
00106 def endElement(self, stack, name, text):
00107 """Handle subelements of <client/> element. These require special
00108 handling because modifying them invalidates the HTTPS trust anchor
00109 cache.
00110 """
00111 control_elt.endElement(self, stack, name, text)
00112 if name in self.elements:
00113 self.clear_https_ta_cache = True
00114
00115 def serve_post_save_hook(self, q_pdu, r_pdu):
00116 """Extra server actions for client_elt."""
00117 if self.clear_https_ta_cache:
00118 self.gctx.clear_https_ta_cache()
00119 self.clear_https_ta_cache = False
00120
00121 def serve_fetch_one(self):
00122 """Find the client object on which a get, set, or destroy method
00123 should operate.
00124 """
00125 r = self.sql_fetch(self.gctx, self.client_id)
00126 if r is None:
00127 raise rpki.exceptions.NotFound
00128 return r
00129
00130 def serve_fetch_all(self):
00131 """Find client objects on which a list method should operate."""
00132 return self.sql_fetch_all(self.gctx)
00133
00134 def check_allowed_uri(self, uri):
00135 if not uri.startswith(self.base_uri):
00136 raise rpki.exceptions.ForbiddenURI
00137
00138 class publication_object_elt(rpki.xml_utils.base_elt, publication_namespace):
00139 """Virtual class for publishable objects. These have very similar
00140 syntax, differences lie in underlying datatype and methods. XML
00141 methods are a little different from the pattern used for objects
00142 that support the create/set/get/list/destroy actions, but
00143 publishable objects don't go in SQL either so these classes would be
00144 different in any case.
00145 """
00146
00147 attributes = ("action", "tag", "client_id", "uri")
00148 payload = None
00149
00150 def endElement(self, stack, name, text):
00151 """Handle a publishable element element."""
00152 assert name == self.element_name, "Unexpected name %s, stack %s" % (name, stack)
00153 if text:
00154 self.payload = self.payload_type(Base64 = text)
00155 stack.pop()
00156
00157 def toXML(self):
00158 """Generate XML element for publishable object."""
00159 elt = self.make_elt()
00160 if self.payload:
00161 elt.text = base64.b64encode(self.payload.get_DER())
00162 return elt
00163
00164 def serve_dispatch(self, r_msg, client):
00165 """Action dispatch handler."""
00166 if client is None:
00167 raise rpki.exceptions.BadQuery, "Client query received on control channel"
00168 dispatch = { "publish" : self.serve_publish,
00169 "withdraw" : self.serve_withdraw }
00170 if self.action not in dispatch:
00171 raise rpki.exceptions.BadQuery, "Unexpected query: action %s" % self.action
00172 client.check_allowed_uri(self.uri)
00173 dispatch[self.action]()
00174 r_pdu = self.__class__()
00175 r_pdu.action = self.action
00176 r_pdu.tag = self.tag
00177 r_pdu.uri = self.uri
00178 r_msg.append(r_pdu)
00179
00180 def serve_publish(self):
00181 """Publish an object."""
00182 rpki.log.info("Publishing %s as %s" % (repr(self.payload), repr(self.uri)))
00183 filename = self.uri_to_filename()
00184 dirname = os.path.dirname(filename)
00185 if not os.path.isdir(dirname):
00186 os.makedirs(dirname)
00187 f = open(filename, "wb")
00188 f.write(self.payload.get_DER())
00189 f.close()
00190
00191 def serve_withdraw(self):
00192 """Withdraw an object."""
00193 rpki.log.info("Withdrawing %s from at %s" % (repr(self.payload), repr(self.uri)))
00194 os.remove(self.uri_to_filename())
00195
00196 def uri_to_filename(self):
00197 """Convert a URI to a local filename."""
00198 if not self.uri.startswith("rsync://"):
00199 raise rpki.exceptions.BadURISyntax
00200 filename = self.gctx.publication_base + self.uri[len("rsync://"):]
00201 if filename.find("//") >= 0 or filename.find("/../") >= 0 or filename.endswith("/.."):
00202 raise rpki.exceptions.BadURISyntax
00203 return filename
00204
00205 class certificate_elt(publication_object_elt):
00206 """<certificate/> element."""
00207
00208 element_name = "certificate"
00209 payload_type = rpki.x509.X509
00210
00211 class crl_elt(publication_object_elt):
00212 """<crl/> element."""
00213
00214 element_name = "crl"
00215 payload_type = rpki.x509.CRL
00216
00217 class manifest_elt(publication_object_elt):
00218 """<manifest/> element."""
00219
00220 element_name = "manifest"
00221 payload_type = rpki.x509.SignedManifest
00222
00223 class roa_elt(publication_object_elt):
00224 """<roa/> element."""
00225
00226 element_name = "roa"
00227 payload_type = rpki.x509.ROA
00228
00229
00230
00231
00232 obj2elt = dict((e.payload_type, e) for e in (certificate_elt, crl_elt, manifest_elt, roa_elt))
00233
00234 class report_error_elt(rpki.xml_utils.base_elt, publication_namespace):
00235 """<report_error/> element."""
00236
00237 element_name = "report_error"
00238 attributes = ("tag", "error_code")
00239
00240 @classmethod
00241 def from_exception(cls, exc):
00242 """Generate a <report_error/> element from an exception."""
00243 self = cls()
00244 self.error_code = exc.__class__.__name__
00245 return self
00246
00247 class msg(rpki.xml_utils.msg, publication_namespace):
00248 """Publication PDU."""
00249
00250
00251
00252 version = 1
00253
00254
00255
00256 pdus = dict((x.element_name, x)
00257 for x in (config_elt, client_elt, certificate_elt, crl_elt, manifest_elt, roa_elt, report_error_elt))
00258
00259 def serve_top_level(self, gctx, client):
00260 """Serve one msg PDU."""
00261 if self.type != "query":
00262 raise rpki.exceptions.BadQuery, "Message type is not query"
00263 r_msg = self.__class__()
00264 r_msg.type = "reply"
00265 for q_pdu in self:
00266 q_pdu.gctx = gctx
00267 q_pdu.serve_dispatch(r_msg, client)
00268 return r_msg
00269
00270 class sax_handler(rpki.xml_utils.sax_handler):
00271 """SAX handler for publication protocol."""
00272
00273 pdu = msg
00274 name = "msg"
00275 version = "1"
00276
00277 class cms_msg(rpki.x509.XML_CMS_object):
00278 """Class to hold a CMS-signed publication PDU."""
00279
00280 encoding = "us-ascii"
00281 schema = rpki.relaxng.publication
00282 saxify = sax_handler.saxify