diff options
Diffstat (limited to 'rpkid.stable/rpki')
-rw-r--r-- | rpkid.stable/rpki/__init__.py | 1992 | ||||
-rw-r--r-- | rpkid.stable/rpki/config.py | 56 | ||||
-rw-r--r-- | rpkid.stable/rpki/exceptions.py | 135 | ||||
-rw-r--r-- | rpkid.stable/rpki/https.py | 291 | ||||
-rw-r--r-- | rpkid.stable/rpki/ipaddrs.py | 103 | ||||
-rw-r--r-- | rpkid.stable/rpki/left_right.py | 833 | ||||
-rw-r--r-- | rpkid.stable/rpki/log.py | 58 | ||||
-rw-r--r-- | rpkid.stable/rpki/manifest.py | 53 | ||||
-rw-r--r-- | rpkid.stable/rpki/oids.py | 57 | ||||
-rw-r--r-- | rpkid.stable/rpki/publication.py | 282 | ||||
-rw-r--r-- | rpkid.stable/rpki/relaxng.py | 1699 | ||||
-rw-r--r-- | rpkid.stable/rpki/resource_set.py | 795 | ||||
-rw-r--r-- | rpkid.stable/rpki/roa.py | 75 | ||||
-rw-r--r-- | rpkid.stable/rpki/rpki_engine.py | 819 | ||||
-rw-r--r-- | rpkid.stable/rpki/sql.py | 295 | ||||
-rw-r--r-- | rpkid.stable/rpki/sundial.py | 198 | ||||
-rw-r--r-- | rpkid.stable/rpki/up_down.py | 535 | ||||
-rw-r--r-- | rpkid.stable/rpki/x509.py | 995 | ||||
-rw-r--r-- | rpkid.stable/rpki/xml_utils.py | 317 |
19 files changed, 9588 insertions, 0 deletions
diff --git a/rpkid.stable/rpki/__init__.py b/rpkid.stable/rpki/__init__.py new file mode 100644 index 00000000..1ac55ca3 --- /dev/null +++ b/rpkid.stable/rpki/__init__.py @@ -0,0 +1,1992 @@ +# $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. + +# This file exists to tell Python that this the content of this +# directory constitute a Python package. Since we're not doing +# anything exotic, this file doesn't need to contain any code, but +# since its existance defines the package, it's as sensible a place as +# any to put the Doxygen mainpage. + +# The "usage" text for irbe_cli in the OPERATIONS section is generated +# automatically by running the program with its --help command. +# Should do the same with the other programs. Don't yet have a sane +# way to automate options in config files, though. Would be nice. + +## @mainpage RPKI Engine Reference Manual +# +# This collection of Python modules implements a prototype of the +# RPKI Engine. This is a work in progress. +# +# See http://viewvc.hactrn.net/subvert-rpki.hactrn.net/ for code, +# design documents, a text mirror of portions of APNIC's Wiki, etc. +# +# The documentation you're reading is generated automatically by +# Doxygen from comments and documentation in +# <a href="http://viewvc.hactrn.net/subvert-rpki.hactrn.net/rpkid/rpki/">the code</a>. +# +# Besides the automatically-generated code documentation, this manual +# also includes documentation of the overall package: +# +# @li The @subpage Installation "installation instructions" +# @li The @subpage Operation "operation instructions" +# @li A description of the @subpage Left-right "left-right protocol" +# @li A description of the @subpage Publication "publication protocol" +# @li A description of the @subpage bpki-model "BPKI model" +# used to secure the up-down, left-right, and %publication protocols +# @li A description of the several @subpage sql-schemas "SQL database schemas" +# @li Some suggestions for @subpage further-reading "further reading" +# +# This work has been funded by <a +# href="http://www.arin.net/">ARIN</a>, in collaboration with the +# other Regional Internet Registries. + +## @page further-reading Further Reading +# +# If you're interested in this package you might also be interested +# in: +# +# @li <a href="http://viewvc.hactrn.net/subvert-rpki.hactrn.net/rcynic/">The rcynic validation tool</a> +# @li <a href="http://www.hactrn.net/opaque/rcynic.html">A live sample of rcynic's summary output</a> +# @li <a href="http://mirin.apnic.net/resourcecerts/wiki/">APNIC's Wiki</a> +# @li <a href="http://mirin.apnic.net/trac/">APNIC's project Trac instance</a> + +## @page Installation Installation Guide +# +# Preliminary installation instructions for rpkid et al. These are the +# production-side RPKI tools, for Internet Registries (RIRs, LIRs, etc). +# See the "rcynic" program for relying party tools. +# +# rpkid is a set of Python modules supporting generation and maintenance +# of resource certificates. Most of the code is in the rpkid/rpki/ +# directory. rpkid itself is a relatively small program that calls the +# library modules. There are several other programs that make use of +# the same libraries, as well as a collection of test programs. +# +# At present the package is intended to be run out of its build +# directory. Setting up proper installation in a system area using the +# Python distutils package would likely not be very hard but has not yet +# been done. +# +# Note that initial development of this code has been on FreeBSD, so +# installation will probably be easiest on FreeBSD. +# +# Before attempting to build the package, you need to install any +# missing prerequisites. Note that the Python code requires Python +# version 2.5. rpkid et al are mostly self-contained, but do require +# a small number of external packages to run. +# +# <ul> +# <li> +# <a href="http://codespeak.net/lxml/">http://codespeak.net/lxml/</a>. +# lxml in turn requires the Gnome LibXML2 C libraries. +# <ul> +# <li>FreeBSD: /usr/ports/devel/py-lxml</li> +# <li>Fedora: python-lxml.i386</li> +# </ul> +# </li> +# <li> +# <a href="http://sourceforge.net/projects/mysql-python/">http://sourceforge.net/projects/mysql-python/</a>. +# MySQLdb in turn requires MySQL client and server. rpkid et al have +# been tested with MySQL 5.0 and 5.1. +# <ul> +# <li>FreeBSD: /usr/ports/databases/py-MySQLdb</li> +# <li>Fedora: MySQL-python.i386</li> +# </ul> +# </li> +# <li> +# <a href="http://trevp.net/tlslite/">http://trevp.net/tlslite/</a>. +# TLSLite pulls in other crypto packages. +# <ul> +# <li>FreeBSD: /usr/ports/security/py-tlslite</li> +# </ul> +# </li> +# </ul> +# +# rpkid et al also make heavy use of a modified copy of the Python +# OpenSSL Wrappers (POW) package, but this copy has enough modifications +# and additions that it's included in the subversion tree. +# +# The next step is to build the OpenSSL and POW binaries. At present +# the OpenSSL code is just a copy of the stock OpenSSL 0.9.8g release, +# compiled with special options to enable RFC 3779 support that ISC +# wrote under previous contract to ARIN. The POW (Python OpenSSL +# Wrapper) library is an extended copy of the stock POW release. +# +# To build these, cd to the top-level directory in the distribution and +# type "make". +# +# @verbatim +# $ cd $top +# $ make +# @endverbatim +# +# This should automatically build everything, in the right order, +# including staticly linking the POW extension module with the OpenSSL +# library to provide RFC 3779 support. +# +# You will also need a MySQL installation. This code was developed +# using MySQL 5.1 and has been tested with MySQL 5.0 and 5.1. +# +# The architecture is intended to support hardware signing modules +# (HSMs), but the code to support them has not been written. +# +# At this point, you should have all the necessary software installed. +# You will probably want to test it. All tests should be run from the +# rpkid/ directory. The test suite requires a few more external +# packages, only one of which is Python code. +# +# <ul> +# <li> +# <a href="http://pyyaml.org/">http://pyyaml.org/</a>. +# testpoke.py (an up-down protocol command line test client) and +# testbed.py (a test harness) use PyYAML. +# <ul> +# <li>FreeBSD: /usr/ports/devel/py-yaml</li> +# </ul> +# </li> +# <li> +# <a href="http://xmlsoft.org/XSLT/">http://xmlsoft.org/XSLT/</a>. +# Some of the test code uses xsltproc, from the Gnome LibXSLT +# package. +# <ul> +# <li>FreeBSD: /usr/ports/textproc/libxslt</li> +# </ul> +# </li> +# <li> +# <a href="http://w3m.sourceforge.net/">http://w3m.sourceforge.net/</a>. +# testbed.py uses w3m to display the summary output from rcynic. +# Nothing terrible will happen if w3m isn't available, testbed.py +# will just complain about it being missing and won't display +# rcynic's output. +# <ul> +# <li>FreeBSD: /usr/ports/www/w3m</li> +# </ul> +# </li> +# </ul> +# +# Some of the tests require MySQL databases to store their data. To set +# up all the databases that the tests will need, run the SQL commands in +# rpkid/testbed.sql. The MySQL command line client is usually the +# easiest way to do this, eg: +# +# @verbatim +# $ cd $top/rpkid +# $ mysql -u root -p <testbed.sql +# @endverbatim +# +# To run the tests, run "make all-tests": +# +# @verbatim +# $ cd $top/rpkid +# $ make all-tests +# @endverbatim +# +# If nothing explodes, your installation is probably ok. Any Python +# backtraces in the output indicate a problem. +# +# There's a last set of tools that only developers should need, as +# they're only used when modifying schemas or regenerating the +# documentation. These tools are listed here for completeness. +# +# <ul> +# <li> +# <a href="http://www.doxygen.org/">http://www.doxygen.org/</a>. +# Doxygen in turn pulls in several other tools, notably Graphviz, +# pdfLaTeX, and Ghostscript. +# <ul> +# <li>FreeBSD: /usr/ports/devel/doxygen</li> +# </ul> +# </li> +# <li> +# <a href="http://lynx.isc.org/current/">http://lynx.isc.org/current/</a>. +# The documentation build process uses xsltproc and Lynx to dump +# flat text versions of a few critical documentation pages. +# <ul> +# <li>FreeBSD: /usr/ports/www/lynx</li> +# </ul> +# </li> +# <li> +# <a href="http://www.thaiopensource.com/relaxng/trang.html">http://www.thaiopensource.com/relaxng/trang.html</a>. +# Trang is used to convert RelaxNG schemas from the human-readable +# "compact" form to the XML form that LibXML2 understands. Trang in +# turn requires Java. +# <ul> +# <li>FreeBSD: /usr/ports/textproc/trang</li> +# </ul> +# </li> +# <li> +# <a href="http://search.cpan.org/dist/SQL-Translator/">http://search.cpan.org/dist/SQL-Translator/</a>. +# SQL-Translator, also known as "SQL Fairy", includes code to parse +# an SQL schema and dump a description of it as Graphviz input. +# SQL Fairy in turn requires Perl. +# </li> +# </ul> + +## @page Operation Operation Guide +# +# Preliminary operation instructions for rpkid et al. These are the +# production-side RPKI tools, for Internet Registries (RIRs, LIRs, etc). +# See rcynic/README for relying party tools. +# +# @warning +# rpkid is still in development, and the code changes more often than +# the hand-maintained portions of this documentation. The following +# text was reasonably accurate at the time it was written but may be +# obsolete by the time you read it. +# +# At present the package is intended to be run out of the @c rpkid/ +# directory. +# +# In addition to the library routines in the @c rpkid/rpki/ directory, +# the package includes the following programs: +# +# @li @c rpkid.py: +# The main RPKI engine daemon. +# +# @li @c pubd.py: +# The publication engine daemon. +# +# @li @c rootd.py: +# A separate daemon for handling the root of an RPKI +# certificate tree. This is essentially a stripped down +# version of rpkid with no SQL database, no left-right +# protocol implementation, and only the parent side of +# the up-down protocol. It's separate because the root +# is a special case in several ways and it was simpler +# to keep the special cases out of the main daemon. +# +# @li @c irdbd.py: +# A sample implementation of an IR database daemon. +# rpkid calls into this to perform lookups via the +# left-right protocol. +# +# @li @c irbe_cli.py: +# A command-line client for the left-right control +# protocol. +# +# @li @c cross_certify.py: +# A BPKI cross-certification tool. +# +# @li @c irbe-setup.py: +# An example of a script to set up the mappings between +# the IRDB and rpkid's own database, using the +# left-right control protocol. +# +# @li @c cronjob.py: +# A trivial HTTP client used to drive rpkid cron events. +# +# @li @c testbed.py: +# A test tool for running a collection of rpkid and irdb +# instances under common control, driven by a unified +# test script. +# +# @li @c testpoke.py: +# A simple client for the up-down protocol, mostly +# compatable with APNIC's rpki_poke.pl tool. +# +# Most of these programs take configuration files in a common format +# similar to that used by the OpenSSL command line tool. The test +# programs also take input in YAML format to drive the tests. Runs of +# the testbed.py test tool will generate a fairly complete set +# configuration files which may be useful as examples. +# +# Basic operation consists of creating the appropriate MySQL databases, +# starting rpkid, pubd, rootd, and irdbd, using the left-right control +# protocol to set up rpkid's internal state, and setting up a cron job +# to invoke rpkid's cron action at regular intervals. All other +# operations should occur either as a result of cron events or as a +# result of incoming left-right and up-down protocol requests. +# +# Note that the full event-driven model for rpkid hasn't yet been +# implemented. The design is intended to allow an arbitrary number of +# hosted RPKI engines to run in a single rpkid instance, but without the +# event-driven tasking model one must set up a separate rpkid instance +# for each hosted RPKI engine. +# +# At present the daemon programs all run in foreground, that is, if one +# wants them to run in background one must do so manually, eg, using +# Bourne shell syntax: +# +# @verbatim +# $ python whatever.py & +# $ echo >whatever.pid "$!" +# @endverbatim +# +# All of the daemons use syslog. At present they all set LOG_PERROR, so +# all logging also goes to stderr. +# +# +# @section rpkid rpkid.py +# +# rpkid is the main RPKI engine daemon. Configuration of rpkid is a +# two step process: a %config file to bootstrap rpkid to the point +# where it can speak using the @link Left-right left-right protocol, +# @endlink followed by dynamic configuration via the left-right +# protocol. In production use the latter stage would be handled by +# the IRBE stub; for test and develoment purposes it's handled by the +# irbe_cli.py command line interface or by the testbed.py test +# framework. +# +# rpkid stores dynamic data in an SQL database, which must have been +# created for it, as explained in the @link Installation installation +# guide. @endlink +# +# The default %config file is rpkid.conf, start rpkid with "-c filename" +# to choose a different %config file. All options are in the section +# "[rpkid]". Certificates, keys, and trust anchors may be in either DER +# or PEM format. +# +# %Config file options: +# +# @li @c startup-message: +# String to %log on startup, useful when +# debugging a collection of rpkid instances at +# once. +# +# @li @c sql-username: +# Username to hand to MySQL when connecting to +# rpkid's database. +# +# @li @c sql-database: +# MySQL's database name for rpkid's database. +# +# @li @c sql-password: +# Password to hand to MySQL when connecting to +# rpkid's database. +# +# @li @c bpki-ta: +# Name of file containing BPKI trust anchor. +# All BPKI certificate verification within rpkid +# traces back to this trust anchor. +# +# @li @c rpkid-cert: +# Name of file containing rpkid's own BPKI EE +# certificate. +# +# @li @c rpkid-key: +# Name of file containing RSA key corresponding +# to rpkid-cert. +# +# @li @c irbe-cert: +# Name of file containing BPKI certificate used +# by IRBE when talking to rpkid. +# +# @li @c irdb-cert: +# Name of file containing BPKI certificate used +# by irdbd. +# +# @li @c irdb-url: +# Service URL for irdbd. Must be a %https:// URL. +# +# @li @c server-host: +# Hostname or IP address on which to listen for +# HTTPS connections. Current default is +# INADDR_ANY (IPv4 0.0.0.0); this will need to +# be hacked to support IPv6 for production. +# +# @li @c server-port: +# TCP port on which to listen for HTTPS +# connections. +# +# +# @section pubd pubd.py +# +# pubd is the publication daemon. It implements the server side of +# the publication protocol, and is used by rpkid to publish the +# certificates and other objects that rpkid generates. +# +# pubd is separate from rpkid for two reasons: +# +# @li The hosting model allows entities which choose to run their own +# copies of rpkid to publish their output under a common +# publication point. In general, encouraging shared publication +# services where practical is a good thing for relying parties, +# as it will speed up rcynic synchronization time. +# +# @li The publication server has to run on (or at least close to) the +# publication point itself, which in turn must be on a publically +# reachable server to be useful. rpkid, on the other hand, need +# only be reachable by the IRBE and its children in the RPKI tree. +# rpkid is a much more complex piece of software than pubd, so in +# some situations it might make sense to wrap tighter firewall +# constraints around rpkid than would be practical if rpkid and +# pubd were a single program. +# +# pubd stores dynamic data in an SQL database, which must have been +# created for it, as explained in the installation guide. pubd also +# stores the published objects themselves as disk files in a +# configurable location which should correspond to an appropriate +# module definition in rsync.conf. +# +# The default %config file is pubd.conf, start pubd with "-c +# filename" to choose a different %config file. ALl options are in +# the section "[pubd]". Certifiates, keys, and trust anchors may be +# either DER or PEM format. +# +# %Config file options: +# +# @li @c sql-username: +# Username to hand to MySQL when connecting to +# pubd's database. +# +# @li @c sql-database: +# MySQL's database name for pubd's database. +# +# @li @c sql-password: +# Password to hand to MySQL when connecting to +# pubd's database. +# +# @li @c bpki-ta: +# Name of file containing master BPKI trust +# anchor for pubd. All BPKI validation in pubd +# traces back to this trust anchor. +# +# @li @c irbe-cert: +# Name of file containing BPKI certificate used +# by IRBE when talking to pubd. +# +# @li @c pubd-cert: +# Name of file containing BPKI certificate used +# by pubd. +# +# @li @c pubd-key: +# Name of file containing RSA key corresponding +# to @c pubd-cert. +# +# @li @c server-host: +# Hostname or IP address on which to listen for +# HTTPS connections. Current default is +# INADDR_ANY (IPv4 0.0.0.0); this will need to +# be hacked to support IPv6 for production. +# +# @li @c server-port: +# TCP port on which to listen for HTTPS +# connections. +# +# @li @c publication-base: +# Path to base of filesystem tree where pubd +# should store publishable objects. Default is +# "publication/". +# +# +# @section rootd rootd.py +# +# rootd is a stripped down implmenetation of (only) the server side of +# the up-down protocol. It's a separate program because the root +# certificate of an RPKI certificate tree requires special handling and +# may also require a special handling policy. rootd is a simple +# implementation intended for test use, it's not suitable for use in a +# production system. All configuration comes via the %config file. +# +# The default %config file is rootd.conf, start rootd with "-c filename" +# to choose a different %config file. All options are in the section +# "[rootd]". Certificates, keys, and trust anchors may be in either DER +# or PEM format. +# +# %Config file options: +# +# @li @c bpki-ta: +# Name of file containing BPKI trust anchor. All +# BPKI certificate validation in rootd traces +# back to this trust anchor. +# +# @li @c rootd-bpki-cert: +# Name of file containing rootd's own BPKI +# certificate. +# +# @li @c rootd-bpki-key: +# Name of file containing RSA key corresponding to +# rootd-bpki-cert. +# +# @li @c rootd-bpki-crl: +# Name of file containing BPKI CRL that would +# cover rootd-bpki-cert had it been revoked. +# +# @li @c child-bpki-cert: +# Name of file containing BPKI certificate for +# rootd's one and only child (RPKI engine to +# which rootd issues an RPKI certificate). +# +# @li @c server-host: +# Hostname or IP address on which to listen for +# HTTPS connections. Default is localhost. +# +# @li @c server-port: +# TCP port on which to listen for HTTPS +# connections. +# +# @li @c rpki-root-key: +# Name of file containing RSA key to use in +# signing resource certificates. +# +# @li @c rpki-root-cert: +# Name of file containing self-signed root +# resource certificate corresponding to +# rpki-root-key. +# +# @li @c rpki-root-dir: +# Name of directory where rootd should write +# RPKI subject certificate, manifest, and CRL. +# +# @li @c rpki-subject-cert: +# Name of file that rootd should use to save the +# one and only certificate it issues. +# Default is "Subroot.cer". +# +# @li @c rpki-root-crl: +# Name of file to which rootd should save its +# RPKI CRL. Default is "Root.crl". +# +# @li @c rpki-root-manifest: +# Name of file to which rootd should save its +# RPKI manifest. Default is "Root.mnf". +# +# @li @c rpki-subject-pkcs10: +# Name of file that rootd should use when saving +# a copy of the received PKCS #10 request for a +# resource certificate. This is only used for +# debugging. Default is not to save the PKCS +# #10 request. +# +# +# @section irdbd irdbd.py +# +# irdbd is a sample implemntation of the server side of the IRDB +# callback subset of the left-right protocol. In production use this +# service is a function of the IRBE stub; irdbd may be suitable for +# production use in simple cases, but an IR with a complex IRDB may need +# to extend or rewrite irdbd. +# +# irdbd requires a pre-populated database to represent the IR's +# customers. irdbd expects this database to use the SQL schema defined +# in rpkid/irdbd.sql. Once this database has been populated, the +# IRBE stub needs to create the appropriate objects in rpkid's database +# via the control subset of the left-right protocol, and store the +# linkage IDs (foreign keys into rpkid's database, basicly) in the +# IRDB. The irbe-setup.py program shows an example of how to do this. +# +# irdbd's default %config file is irdbd.conf, start irdbd with "-c +# filename" to choose a different %config file. All options are in the +# section "[irdbd]". Certificates, keys, and trust anchors may be in +# either DER or PEM format. +# +# %Config file options: +# +# @li @c startup-message: +# String to %log on startup, useful when +# debugging a collection of irdbd instances at +# once. +# +# @li @c sql-username: +# Username to hand to MySQL when connecting to +# irdbd's database. +# +# @li @c sql-database: +# MySQL's database name for irdbd's database. +# +# @li @c sql-password: +# Password to hand to MySQL when connecting to +# irdbd's database. +# +# @li @c bpki-ta: +# Name of file containing BPKI trust anchor. All +# BPKI certificate validation in irdbd traces +# back to this trust anchor. +# +# @li @c irdbd-cert: +# Name of file containing irdbd's own BPKI +# certificate. +# +# @li @c irdbd-key: +# Name of file containing RSA key corresponding +# to irdbd-cert. +# +# @li @c rpkid-cert: +# Name of file containing certificate used the +# one and only by rpkid instance authorized to +# contact this irdbd instance. +# +# @li @c https-url: +# Service URL for irdbd. Must be a %https:// URL. +# +# +# @section irdbd_cli irbe_cli.py +# +# irbe_cli is a simple command line client for the control subsets of +# the @link Left-right left-right @endlink and @link Publication +# publication @endlink protocols. In production use this +# functionality would be part of the IRBE stub. +# +# Basic configuration of irbe_cli is handled via a %config file. The +# specific action or actions to be performed are specified on the +# command line, and map closely to the protocols themselves. +# +# At present the user is assumed to be able to read the (XML) +# left-right and publication protocol messages, and with one +# exception, irdbd-cli makes no attempt to interpret the responses +# other than to check for signature and syntax errors. The one +# exception is that, if the @c --pem_out option is specified on the +# command line, any PKCS \#10 requests received from rpkid will be +# written in PEM format to that file; this makes it easier to hand +# these requests off to the business PKI (BPKI in order to issue signing +# certs corresponding to newly generated business keys. +# +# @verbinclude irbe_cli.usage +# +# Global options (@c --config, @c --help, @c --pem_out) come first, +# then zero or more commands (@c parent, @c repository, @c self, @c +# child, @c route_origin, @c bsc, @c config, @c client), each followed +# by its own set of options. The commands map to elements in the +# protocols, and the command-specific options map to attributes or +# subelements for those commands. +# +# @c --tag is an optional arbitrary tag (think IMAP) to simplify +# matching up replies with batched queries. +# +# @c --*_id options refer to the primary keys of previously created +# objects. +# +# The remaining options are specific to the particular commands, and +# follow directly from the protocol specifications. +# +# A trailing "=" in the above option summary indicates that an option +# takes a value, eg, "--action create" or "--action=create". Options +# without a trailing "=" correspond to boolean control attributes. +# +# The default %config file for irbe_cli is irbe_cli.conf, start +# irbe_cli with "-c filename" (or "--config filename") to choose a +# different %config file. All options are in the section +# "[irbe_cli]". Certificates, keys, and trust anchors may be in +# either DER or PEM format. +# +# %Config file options: +# +# @li @c rpkid-bpki-ta: +# Name of file containing BPKI trust anchor to +# use when authenticating messages from rpkid. +# +# @li @c rpkid-irbe-cert: +# Name of file containing BPKI certificate +# irbe_cli should use when talking to rpkid. +# +# @li @c rpkid-irbe-key: +# Name of file containing RSA key corresponding to +# rpkid-irbe-cert. +# +# @li @c rpkid-cert: +# Name of file containing rpkid's BPKI certificate. +# +# @li @c rpkid-url: +# Service URL for rpkid. Must be a %https:// URL. +# +# @li @c pubd-bpki-ta: +# Name of file containing BPKI trust anchor to +# use when authenticating messages from pubd. +# +# @li @c pubd-irbe-cert: +# Name of file containing BPKI certificate +# irbe_cli should use when talking to pubd. +# +# @li @c pubd-irbe-key: +# Name of file containing RSA key corresponding to +# pubd-irbe-cert. +# +# @li @c pubd-cert: +# Name of file containing pubd's BPKI certificate. +# +# @li @c pubd-url: +# Service URL for pubd. Must be a %https:// URL. +# +# +# +# @section cross_certify cross_certify.py +# +# cross_certify.py is a small tool to extract certain fields from an +# existing X.509 certificate and generate issue a new certificate that +# can be used as part of a cross-certification chain. cross_certify +# doesn't take a config file, all of its arguments are specified on +# the command line. +# +# @verbatim +# python cross_certify.py { -i | --in } input_cert +# { -c | --ca } issuing_cert +# { -k | --key } issuing_cert_key +# { -s | --serial } serial_filename +# [ { -h | --help } ] +# [ { -o | --out } filename ] +# [ { -l | --lifetime } timedelta ] +# @endverbatim +# +# +# @section irbe_setup irbe-setup.py config file +# +# @warning +# irbe-setup is old code, not currently used, kept in case it is +# useful at some later date. It may not work properly or at all. If +# you don't understand what it does, you don't need it. You have been +# warned. +# +# The default %config file is irbe.conf, start rpkid with "-c filename" +# to choose a different %config file. Most options are in the section +# "[irbe_cli]", but a few are in the section "[irdbd]". Certificates, +# keys, and trust anchors may be in either DER or PEM format. +# +# Options in the "[irbe_cli]" section: +# +# @li @c bpki-ta: +# Name of file containing BPKI trust anchor. +# +# @li @c irbe-cert: +# Name of file containing BPKI certificate +# irbe-setup should use. +# +# @li @c irbe-key: +# Name of file containing RSA key corresponding +# to irbe-cert. +# +# @li @c rpkid-cert: +# Name of file containing rpkid's BPKI +# certificate. +# +# @li @c https-url: +# Service URL for rpkid. Must be a %https:// URL. +# +# Options in the "[irdbd]" section: +# +# @li @c sql-username: +# Username to hand to MySQL when connecting to +# irdbd's database. +# +# @li @c sql-database: +# MySQL's database name for irdbd's database. +# +# @li @c sql-password: +# Password to hand to MySQL when connecting to +# irdbd's database. +# +# +# @section cronjob cronjob.py +# +# This is a trivial program to trigger a cron run within rpkid. Once +# rpkid has been converted to the planned event-driven model, this +# function will be handled internally, but for now it has to be +# triggered by an external program. For pseudo-production use one would +# run this program under the system cron daemon. For scripted testing +# it happens to be useful to be able to control when cron cycles occur, +# so at the current stage of code development use of an external trigger +# is a useful feature. +# +# The default %config file is cronjob.conf, start cronjob with "-c +# filename" to choose a different %config file. All options are in the +# section "[cronjob]". Certificates, keys, and trust anchors may be in +# either DER or PEM format. +# +# %Config file options: +# +# @li @c bpki-ta: +# Name of file containing BPKI trust anchor. +# +# @li @c irbe-cert: +# Name of file containing cronjob.py's BPKI +# certificate. +# +# @li @c https-key: +# Name of file containing RSA key corresponding +# to irbe-cert. +# +# @li @c rpkid-cert: +# Name of file containing rpkid's BPKI certificate. +# +# @li @c https-url: +# Service URL for rpkid. Must be a %https:// URL. +# +# +# @section testbed testbed.py: +# +# testbed is a test harness to set up and run a collection of rpkid and +# irdbd instances under scripted control. testbed is a very recent +# addition to the toolset and is still evolving rapidly. +# +# Unlike the programs described above, testbed takes two configuration +# files in different languages. The first configuration file uses the +# same syntax as the above configuration files but is completely +# optional. The second configuration file is the test script, which is +# encoded using the YAML serialization language (see +# http://www.yaml.org/ for more information on YAML). The YAML script +# is not optional, as it describes the test layout. testbed is designed +# to support running a fairly wide set of test configurations as canned +# scripts without writing any new control code. The intent is to make +# it possible to write meaningful regression tests. +# +# All of the options in in the first (optional) configuration file are +# just overrides for wired-in default values. In most cases the +# defaults will suffice, and the set of options is still in flux, so +# only a few of the options are described here. The default name for +# this configuration file is testbed.conf, run testbed with "-c +# filename" to change it. +# +# testbed.conf options: +# +# @li @c testbed_dir: +# Working directory into which testbed should write the +# (many) files it generates. Default is "testbed.dir". +# +# @li @c irdb_db_pass: +# MySQL password for the "irdb" user. Default is +# "fnord". You may want to override this. +# +# @li @c rpki_db_pass: +# MySQL password for the "rpki" user. Default is +# "fnord". You may want to override this. +# +# @li @c rootd_sia: +# rsync URI naming a (perhaps fictious) directory to use +# as the id-ad-caRepository SIA value in the generated +# root resource certificate. Default is +# "rsync://wombat.invalid/". You may want to override +# this if you intend to run an rsync server and test +# against the generated results using rcynic. This +# default will likely change if and when testbed learns +# how to run rcynic itself as part of the test suite. +# +# The second configuration file is named testbed.yaml by default, run +# testbed with "-y filename" to change it. The YAML file contains +# multiple YAML "documents". The first document describes the initial +# test layout and resource allocations, subsequent documents describe +# modifications to the initial allocations and other parameters. +# Resources listed in the initial layout are aggregated automatically, +# so that a node in the resource hierarchy automatically receives the +# resources it needs to issue whatever its children are listed as +# holding. Actions in the subsequent documents are modifications to the +# current resource set, modifications to validity dates or other +# non-resource parameters, or special commands like "sleep". The +# details are still evolving, but here's an example of current usage: +# +# @verbatim +# name: RIR +# valid_for: 2d +# sia_base: "rsync://wombat.invalid/" +# kids: +# - name: LIR0 +# kids: +# - name: Alice +# ipv4: 192.0.2.1-192.0.2.33 +# asn: 64533 +# --- +# - name: Alice +# valid_add: 10 +# --- +# - name: Alice +# add_as: 33 +# valid_add: 2d +# --- +# - name: Alice +# valid_sub: 2d +# --- +# - name: Alice +# valid_for: 10d +# @endverbatim +# +# This specifies an initial layout consisting of an RPKI engine named +# "RIR", with one child "LIR0", which in turn has one child "Alice". +# Alice has a set of assigned resources, and all resources in the system +# are initially set to be valid for two days from the time at which the +# test is started. The first subsequent document adds ten seconds to +# the validity interval for Alice's resources and makes no other +# modifications. The second subsequent document grants Alice additional +# resources and adds another two days to the validity interval for +# Alice's resources. The next document subtracts two days from the +# validity interval for Alice's resources. The final document sets the +# validity interval for Alice's resources to ten days. +# +# Operators in subsequent (update) documents: +# +# @li @c add_as, @c add_v4, @c add_v6: +# These add ASN, IPv4, or IPv6 resources, respectively. +# +# @li @c sub_as, @c sub_v4, @c sub_v6: +# These subtract resources. +# +# @li @c valid_until: +# Set an absolute expiration date. +# +# @li @c valid_for: +# Set a relative expiration date. +# +# @li @c valid_add, @c valid_sub: +# Add to or subtract from validity interval. +# +# @li @c sleep [interval]: +# Sleep for specified interval, or until testbed receives a SIGALRM signal. +# +# Absolute timestamps should be in the form shown (UTC timestamp format +# as used in XML). +# +# Intervals (@c valid_add, @c valid_sub, @c valid_for, @c sleep) are either +# integers, in which case they're interpreted as seconds, or are a +# string of the form "wD xH yM zS" where w, x, y, and z are integers and +# D, H, M, and S indicate days, hours, minutes, and seconds. In the +# latter case all of the fields are optional, but at least one must be +# specified. For example, "3D4H" means "three days plus four hours". +# +# +# @section testpoke testpoke.py +# +# This is a command-line client for the up-down protocol. Unlike all of +# the above programs, testpoke does not accept a %config file in +# OpenSSL-compatable format at all. Instead, it is configured +# exclusively by a YAML script. testpoke's design was constrained by a +# desire to have it be compatable with APNIC's rpki_poke.pl tool, so +# that the two tools could use a common configuration language to +# simplify scripted testing. There are minor variations due to slightly +# different feature sets, but YAML files intended for one program will +# usually work with the other. +# +# README for APNIC's tool describing the input language can be found at +# <a href="http://mirin.apnic.net/svn/rpki_engine/branches/gary-poker/client/poke/README"> +# http://mirin.apnic.net/svn/rpki_engine/branches/gary-poker/client/poke/README</a>. +# +# testpoke.py takes a simplified command line and uses only one YAML +# input file. +# +# @verbatim +# Usage: python testpoke.py [ { -y | --yaml } configfile ] +# [ { -r | --request } requestname ] +# [ { -h | --help } ] +# @endverbatim +# +# Default configuration file is testpoke.yaml, override with --yaml +# option. +# +# The --request option specifies the specific command within the YAML +# file to execute. +# +# Sample configuration file: +# +# @verbatim +# --- +# # Sample YAML configuration file for testpoke.py +# +# version: 1 +# posturl: https://localhost:4433/up-down/1 +# recipient-id: wombat +# sender-id: "1" +# +# cms-cert-file: biz-certs/Frank-EE.cer +# cms-key-file: biz-certs/Frank-EE.key +# cms-ca-cert-file: biz-certs/Bob-Root.cer +# cms-cert-chain-file: [ biz-certs/Frank-CA.cer ] +# +# ssl-cert-file: biz-certs/Frank-EE.cer +# ssl-key-file: biz-certs/Frank-EE.key +# ssl-ca-cert-file: biz-certs/Bob-Root.cer +# +# requests: +# list: +# type: list +# issue: +# type: issue +# class: 1 +# sia: [ "rsync://bandicoot.invalid/some/where/" ] +# revoke: +# type: revoke +# class: 1 +# ski: "CB5K6APY-4KcGAW9jaK_cVPXKX0" +# @endverbatim +# +# testpoke adds one extension to the language described in APNIC's +# README: the cms-cert-chain-* and ssl-cert-chain-* options, which allow +# one to specify a chain of intermediate certificates to be presented in +# the CMS or TLS protocol. APNIC's initial implementation required +# direct knowledge of the issuing certificate (ie, it supported a +# maximum chain length of one); subsequent APNIC code changes have +# probably relaxed this restriction, and with luck APNIC has copied +# testpoke's syntax to express chains of intermediate certificates. + +## @page Left-right Left-right protocol +# +# The left-right protocol is really two separate client/server +# protocols over separate channels between the RPKI engine and the IR +# back end (IRBE). The IRBE is the client for one of the +# subprotocols, the RPKI engine is the client for the other. +# +# @section Terminology +# +# @li @em IRBE: Internet Registry Back End +# +# @li @em IRDB: Internet Registry Data Base +# +# @li @em BPKI: Business PKI +# +# @li @em RPKI: Resource PKI +# +# @section Operations initiated by the IRBE +# +# This part of the protcol uses a kind of message-passing. Each %object +# that the RPKI engine knows about takes five messages: "create", "set", +# "get", "list", and "destroy". Actions which are not just data +# operations on %objects are handled via an SNMP-like mechanism, as if +# they were fields to be set. For example, to generate a keypair one +# "sets" the "generate-keypair" field of a BSC %object, even though there +# is no such field in the %object itself as stored in SQL. This is a bit +# of a kludge, but the reason for doing it as if these were variables +# being set is to allow composite operations such as creating a BSC, +# populating all of its data fields, and generating a keypair, all as a +# single operation. With this model, that's trivial, otherwise it's at +# least two round trips. +# +# Fields can be set in either "create" or "set" operations, the +# difference just being whether the %object already exists. A "get" +# operation returns all visible fields of the %object. A "list" +# operation returns a %list containing what "get" would have returned on +# each of those %objects. +# +# Left-right protocol %objects are encoded as signed CMS messages +# containing XML as eContent and using an eContentType OID of @c id-ct-xml +# (1.2.840.113549.1.9.16.1.28). These CMS messages are in turn passed +# as the data for HTTPS POST operations, with an HTTP content type of +# "application/x-rpki" for both the POST data and the response data. +# +# All operations allow an optional "tag" attribute which can be any +# alphanumeric token. The main purpose of the tag attribute is to allow +# batching of multiple requests into a single PDU. +# +# @subsection self_obj <self/> object +# +# A @c <self/> %object represents one virtual RPKI engine. In simple cases +# where the RPKI engine operator operates the engine only on their own +# behalf, there will only be one @c <self/> %object, representing the engine +# operator's organization, but in environments where the engine operator +# hosts other entities, there will be one @c @c <self/> %object per hosted +# entity (probably including the engine operator's own organization, +# considered as a hosted customer of itself). +# +# Some of the RPKI engine's configured parameters and data are shared by +# all hosted entities, but most are tied to a specific @c <self/> %object. +# Data which are shared by all hosted entities are referred to as +# "per-engine" data, data which are specific to a particular @c <self/> +# %object are "per-self" data. +# +# Since all other RPKI engine %objects refer to a @c <self/> %object via a +# "self_id" value, one must create a @c <self/> %object before one can +# usefully configure any other left-right protocol %objects. +# +# Every @c <self/> %object has a self_id attribute, which must be specified +# for the "set", "get", and "destroy" actions. +# +# Payload data which can be configured in a @c <self/> %object: +# +# @li @c use_hsm (attribute): +# Whether to use a Hardware Signing Module. At present this option +# has no effect, as the implementation does not yet support HSMs. +# +# @li @c crl_interval (attribute): +# Positive integer representing the planned lifetime of an RPKI CRL +# for this @c <self/>, measured in seconds. +# +# @li @c regen_margin (attribute): +# Positive integer representing how long before expiration of an +# RPKI certificiate a new one should be generated, measured in +# seconds. At present this only affects the one-off EE certificates +# associated with ROAs. +# +# @li @c bpki_cert (element): +# BPKI CA certificate for this @c <self/>. This is used as part of the +# certificate chain when validating incoming TLS and CMS messages, +# and should be the issuer of cross-certification BPKI certificates +# used in @c <repository/>, @c <parent/>, and @c <child/> %objects. If the +# bpki_glue certificate is in use (below), the bpki_cert certificate +# should be issued by the bpki_glue certificate; otherwise, the +# bpki_cert certificate should be issued by the per-engine bpki_ta +# certificate. +# +# @li @c bpki_glue (element): +# Another BPKI CA certificate for this @c <self/>, usually not needed. +# Certain pathological cross-certification cases require a +# two-certificate chain due to issuer name conflicts. If used, the +# bpki_glue certificate should be the issuer of the bpki_cert +# certificate and should be issued by the per-engine bpki_ta +# certificate; if not needed, the bpki_glue certificate should be +# left unset. +# +# Control attributes that can be set to "yes" to force actions: +# +# @li @c rekey: +# Start a key rollover for every RPKI CA associated with every +# @c <parent/> %object associated with this @c <self/> %object. This is the +# first phase of a key rollover operation. +# +# @li @c revoke: +# Revoke any remaining certificates for any expired key associated +# with any RPKI CA for any @c <parent/> %object associated with this +# @c <self/> %object. This is the second (cleanup) phase for a key +# rollover operation; it's separate from the first phase to leave +# time for new RPKI certificates to propegate and be installed. +# +# @li @c reissue: +# Not implemented, may be removed from protocol. Original theory +# was that this operation would force reissuance of any %object with +# a changed key, but as that happens automatically as part of the +# key rollover mechanism this operation seems unnecessary. +# +# @li @c run_now: +# Force immediate processing for all tasks associated with this +# @c <self/> %object that would ordinarily be performed under cron. Not +# currently implemented. +# +# @li @c publish_world_now: +# Force (re)publication of every publishable %object for this @c <self/> +# %object. Not currently implemented. Intended to aid in recovery +# if RPKI engine and publication engine somehow get out of sync. +# +# +# @subsection bsc_obj <bsc/> object +# +# The @c <bsc/> ("business signing context") %object represents all the BPKI +# data needed to sign outgoing CMS or HTTPS messages. Various other +# %objects include pointers to a @c <bsc/> %object. Whether a particular +# @c <self/> uses only one @c <bsc/> or multiple is a configuration decision +# based on external requirements: the RPKI engine code doesn't care, it +# just cares that, for any %object representing a relationship for which +# it must sign messages, there be a @c <bsc/> %object that it can use to +# produce that signature. +# +# Every @c <bsc/> %object has a bsc_id, which must be specified for the +# "get", "set", and "destroy" actions. Every @c <bsc/> also has a self_id +# attribute which indicates the @c <self/> %object with which this @c <bsc/> +# %object is associated. +# +# Payload data which can be configured in a @c <isc/> %object: +# +# @li @c signing_cert (element): +# BPKI certificate to use when generating a signature. +# +# @li @c signing_cert_crl (element): +# CRL which would %list signing_cert if it had been revoked. +# +# Control attributes that can be set to "yes" to force actions: +# +# @li @c generate_keypair: +# Generate a new BPKI keypair and return a PKCS #10 certificate +# request. The resulting certificate, once issued, should be +# configured as this @c <bsc/> %object's signing_cert. +# +# Additional attributes which may be specified when specifying +# "generate_keypair": +# +# @li @c key_type: +# Type of BPKI keypair to generate. "rsa" is both the default and, +# at the moment, the only allowed value. +# +# @li @c hash_alg: +# Cryptographic hash algorithm to use with this keypair. "sha256" +# is both the default and, at the moment, the only allowed value. +# +# @li @c key_length: +# Length in bits of the keypair to be generated. "2048" is both the +# default and, at the moment, the only allowed value. +# +# Replies to "create" and "set" actions that specify "generate-keypair" +# include a <bsc_pkcs10/> element, as do replies to "get" and "list" +# actions for a @c <bsc/> %object for which a "generate-keypair" command has +# been issued. The RPKI engine stores the PKCS #10 request, which +# allows the IRBE to reuse the request if and when it needs to reissue +# the corresponding BPKI signing certificate. +# +# @subsection parent_obj <parent/> object +# +# The @c <parent/> %object represents the RPKI engine's view of a particular +# parent of the current @c <self/> %object in the up-down protocol. Due to +# the way that the resource hierarchy works, a given @c <self/> may obtain +# resources from multiple parents, but it will always have at least one; +# in the case of IANA or an RIR, the parent RPKI engine may be a trivial +# stub. +# +# Every @c <parent/> %object has a parent_id, which must be specified for +# the "get", "set", and "destroy" actions. Every @c <parent/> also has a +# self_id attribute which indicates the @c <self/> %object with which this +# @c <parent/> %object is associated, a bsc_id attribute indicating the @c <bsc/> +# %object to be used when signing messages sent to this parent, and a +# repository_id indicating the @c <repository/> %object to be used when +# publishing issued by the certificate issued by this parent. +# +# Payload data which can be configured in a @c <parent/> %object: +# +# @li @c peer_contact_uri (attribute): +# HTTPS URI used to contact this parent. +# +# @li @c sia_base (attribute): +# The leading portion of an rsync URI that the RPKI engine should +# use when composing the publication URI for %objects issued by the +# RPKI certificate issued by this parent. +# +# @li @c sender_name (attribute): +# Sender name to use in the up-down protocol when talking to this +# parent. The RPKI engine doesn't really care what this value is, +# but other implementations of the up-down protocol do care. +# +# @li @c recipient_name (attribute): +# Recipient name to use in the up-down protocol when talking to this +# parent. The RPKI engine doesn't really care what this value is, +# but other implementations of the up-down protocol do care. +# +# @li @c bpki_cms_cert (element): +# BPKI CMS CA certificate for this @c <parent/>. This is used as part +# of the certificate chain when validating incoming CMS messages If +# the bpki_cms_glue certificate is in use (below), the bpki_cms_cert +# certificate should be issued by the bpki_cms_glue certificate; +# otherwise, the bpki_cms_cert certificate should be issued by the +# bpki_cert certificate in the @c <self/> %object. +# +# @li @c bpki_cms_glue (element): +# Another BPKI CMS CA certificate for this @c <parent/>, usually not +# needed. Certain pathological cross-certification cases require a +# two-certificate chain due to issuer name conflicts. If used, the +# bpki_cms_glue certificate should be the issuer of the +# bpki_cms_cert certificate and should be issued by the bpki_cert +# certificate in the @c <self/> %object; if not needed, the +# bpki_cms_glue certificate should be left unset. +# +# @li @c bpki_https_cert (element): +# BPKI HTTPS CA certificate for this @c <parent/>. This is like the +# bpki_cms_cert %object, only used for validating incoming TLS +# messages rather than CMS. +# +# @li @c bpki_cms_glue (element): +# Another BPKI HTTPS CA certificate for this @c <parent/>, usually not +# needed. This is like the bpki_cms_glue certificate, only used for +# validating incoming TLS messages rather than CMS. +# +# Control attributes that can be set to "yes" to force actions: +# +# @li @c rekey: +# This is like the rekey command in the @c <self/> %object, but limited +# to RPKI CAs under this parent. +# +# @li @c reissue: +# This is like the reissue command in the @c <self/> %object, but limited +# to RPKI CAs under this parent. +# +# @li @c revoke: +# This is like the revoke command in the @c <self/> %object, but limited +# to RPKI CAs under this parent. +# +# @subsection child_obj <child/> object +# +# The @c <child/> %object represents the RPKI engine's view of particular +# child of the current @c <self/> in the up-down protocol. +# +# Every @c <child/> %object has a parent_id, which must be specified for the +# "get", "set", and "destroy" actions. Every @c <child/> also has a +# self_id attribute which indicates the @c <self/> %object with which this +# @c <child/> %object is associated. +# +# Payload data which can be configured in a @c <child/> %object: +# +# @li @c bpki_cert (element): +# BPKI CA certificate for this @c <child/>. This is used as part of +# the certificate chain when validating incoming TLS and CMS +# messages. If the bpki_glue certificate is in use (below), the +# bpki_cert certificate should be issued by the bpki_glue +# certificate; otherwise, the bpki_cert certificate should be issued +# by the bpki_cert certificate in the @c <self/> %object. +# +# @li @c bpki_glue (element): +# Another BPKI CA certificate for this @c <child/>, usually not needed. +# Certain pathological cross-certification cases require a +# two-certificate chain due to issuer name conflicts. If used, the +# bpki_glue certificate should be the issuer of the bpki_cert +# certificate and should be issued by the bpki_cert certificate in +# the @c <self/> %object; if not needed, the bpki_glue certificate +# should be left unset. +# +# Control attributes that can be set to "yes" to force actions: +# +# @li @c reissue: +# Not implemented, may be removed from protocol. +# +# @subsection repository_obj <repository/> object +# +# The @c <repository/> %object represents the RPKI engine's view of a +# particular publication repository used by the current @c <self/> %object. +# +# Every @c <repository/> %object has a repository_id, which must be +# specified for the "get", "set", and "destroy" actions. Every +# @c <repository/> also has a self_id attribute which indicates the @c <self/> +# %object with which this @c <repository/> %object is associated. +# +# Payload data which can be configured in a @c <repository/> %object: +# +# @li @c peer_contact_uri (attribute): +# HTTPS URI used to contact this repository. +# +# @li @c bpki_cms_cert (element): +# BPKI CMS CA certificate for this @c <repository/>. This is used as part +# of the certificate chain when validating incoming CMS messages If +# the bpki_cms_glue certificate is in use (below), the bpki_cms_cert +# certificate should be issued by the bpki_cms_glue certificate; +# otherwise, the bpki_cms_cert certificate should be issued by the +# bpki_cert certificate in the @c <self/> %object. +# +# @li @c bpki_cms_glue (element): +# Another BPKI CMS CA certificate for this @c <repository/>, usually not +# needed. Certain pathological cross-certification cases require a +# two-certificate chain due to issuer name conflicts. If used, the +# bpki_cms_glue certificate should be the issuer of the +# bpki_cms_cert certificate and should be issued by the bpki_cert +# certificate in the @c <self/> %object; if not needed, the +# bpki_cms_glue certificate should be left unset. +# +# @li @c bpki_https_cert (element): +# BPKI HTTPS CA certificate for this @c <repository/>. This is like the +# bpki_cms_cert %object, only used for validating incoming TLS +# messages rather than CMS. +# +# @li @c bpki_cms_glue (element): +# Another BPKI HTTPS CA certificate for this @c <repository/>, usually not +# needed. This is like the bpki_cms_glue certificate, only used for +# validating incoming TLS messages rather than CMS. +# +# At present there are no control attributes for @c <repository/> %objects. +# +# @subsection route_origin_obj <route_origin/> object +# +# The @c <route_origin/> %object is a kind of prototype for a ROA. It +# contains all the information needed to generate a ROA once the RPKI +# engine obtains the appropriate RPKI certificates from its parent(s). +# +# Note that a @c <route_origin/> %object represents a ROA to be generated on +# behalf of @c <self/>, not on behalf of a @c <child/>. Thus, a hosted entity +# that has no children but which does need to generate ROAs would be +# represented by a hosted @c <self/> with no @c <child/> %objects but one or +# more @c <route_origin/> %objects. While lumping ROA generation in with +# the other RPKI engine activities may seem a little odd at first, it's +# a natural consequence of the design requirement that the RPKI daemon +# never transmit private keys across the network in any form; given this +# requirement, the RPKI engine that holds the private keys for an RPKI +# certificate must also be the engine which generates any ROAs that +# derive from that RPKI certificate. +# +# The precise content of the @c <route_origin/> has changed over time as +# the underlying ROA specification has changed. The current +# implementation as of this writing matches what we expect to see in +# draft-ietf-sidr-roa-format-03, once it is issued. In particular, note +# that the exactMatch boolean from the -02 draft has been replaced by +# the prefix and maxLength encoding used in the -03 draft. +# +# Payload data which can be configured in a @c <route_origin/> %object: +# +# @li @c as_number (attribute): +# Autonomous System Number (ASN) to place in the generated ROA. A +# single ROA can only grant authorization to a single ASN; multiple +# ASNs require multiple ROAs, thus multiple @c <route_origin/> %objects. +# +# @li @c ipv4 (attribute): +# %List of IPv4 prefix and maxLength values, see below for format. +# +# @li @c ipv6 (attribute): +# %List of IPv6 prefix and maxLength values, see below for format. +# +# Control attributes that can be set to "yes" to force actions: +# +# @li @c suppress_publication: +# Not implemented, may be removed from protocol. +# +# The lists of IPv4 and IPv6 prefix and maxLength values are represented +# as comma-separated text strings, with no whitespace permitted. Each +# entry in such a string represents a single prefix/maxLength pair. +# +# ABNF for these address lists: +# +# @verbatim +# +# <ROAIPAddress> ::= <address> "/" <prefixlen> [ "-" <max_prefixlen> ] +# ; Where <max_prefixlen> defaults to the same +# ; value as <prefixlen>. +# +# <ROAIPAddressList> ::= <ROAIPAddress> *( "," <ROAIPAddress> ) +# +# @endverbatim +# +# For example, @c "10.0.1.0/24-32,10.0.2.0/24", which is a shorthand +# form of @c "10.0.1.0/24-32,10.0.2.0/24-24". +# +# @section irdb_queries Operations initiated by the RPKI engine +# +# The left-right protocol also includes queries from the RPKI engine +# back to the IRDB. These queries do not follow the message-passing +# pattern used in the IRBE-initiated part of the protocol. Instead, +# there's a single query back to the IRDB, with a corresponding +# response. The CMS and HTTPS encoding are the same as in the rest of +# the protocol, but the BPKI certificates will be different as the +# back-queries and responses form a separate communication channel. +# +# @subsection list_resources_msg <list_resources/> messages +# +# The @c <list_resources/> query and response allow the RPKI engine to ask +# the IRDB for information about resources assigned to a particular +# child. The query must include both a @c "self_id" attribute naming +# the @c <self/> that is making the request and also a @c "child_id" +# attribute naming the child that is the subject of the query. The +# query and response also allow an optional @c "tag" attribute of the +# same form used elsewhere in this protocol, to allow batching. +# +# A @c <list_resources/> response includes the following attributes, along +# with the @c tag (if specified), @c self_id, and @c child_id copied +# from the request: +# +# @li @c valid_until: +# A timestamp indicating the date and time at which certificates +# generated by the RPKI engine for these data should expire. The +# timestamp is expressed as an XML @c xsd:dateTime, must be +# expressed in UTC, and must carry the "Z" suffix indicating UTC. +# +# @li @c subject_name: +# An optional text string naming the child. Not currently used. +# +# @li @c asn: +# A %list of autonomous sequence numbers, expressed as a +# comma-separated sequence of decimal integers with no whitespace. +# +# @li @c ipv4: +# A %list of IPv4 address prefixes and ranges, expressed as a +# comma-separated %list of prefixes and ranges with no whitespace. +# See below for format details. +# +# @li @c ipv6: +# A %list of IPv6 address prefixes and ranges, expressed as a +# comma-separated %list of prefixes and ranges with no whitespace. +# See below for format details. +# +# Entries in a %list of address prefixes and ranges can be either +# prefixes, which are written in the usual address/prefixlen notation, +# or ranges, which are expressed as a pair of addresses denoting the +# beginning and end of the range, written in ascending order separated +# by a single "-" character. This format is superficially similar to +# the format used for prefix and maxLength values in the @c <route_origin/> +# %object, but the semantics differ: note in particular that +# @c <route_origin/> %objects don't allow ranges, while @c <list_resources/> +# messages don't allow a maxLength specification. +# +# @section left_right_error_handling Error handling +# +# Error in this protocol are handled at two levels. +# +# Since all messages in this protocol are conveyed over HTTPS +# connections, basic errors are indicated via the HTTP response code. +# 4xx and 5xx responses indicate that something bad happened. Errors +# that make it impossible to decode a query or encode a response are +# handled in this way. +# +# Where possible, errors will result in a @c <report_error/> message which +# takes the place of the expected protocol response message. +# @c <report_error/> messages are CMS-signed XML messages like the rest of +# this protocol, and thus can be archived to provide an audit trail. +# +# @c <report_error/> messages only appear in replies, never in queries. +# The @c <report_error/> message can appear on either the "forward" (IRBE +# as client of RPKI engine) or "back" (RPKI engine as client of IRDB) +# communication channel. +# +# The @c <report_error/> message includes an optional @c "tag" attribute to +# assist in matching the error with a particular query when using +# batching, and also includes a @c "self_id" attribute indicating the +# @c <self/> that issued the error. +# +# The error itself is conveyed in the @c error_code (attribute). The +# value of this attribute is a token indicating the specific error that +# occurred. At present this will be the name of a Python exception; the +# production version of this protocol will nail down the allowed error +# tokens here, probably in the RelaxNG schema. +# +# The body of the @c <report_error/> element itself is an optional text +# string; if present, this is debugging information. At present this +# capabilty is not used, debugging information goes to syslog. + +## @page Publication Publication protocol +# +# The %publication protocol is really two separate client/server +# protocols, between different parties. The first is a configuration +# protocol for an IRBE to use to configure a %publication engine, +# the second is the interface by which authorized clients request +# %publication of specific objects. +# +# Much of the architecture of the %publication protocol is borrowed +# from the @link Left-right left-right protocol: @endlink like the +# left-right protocol, the %publication protocol uses CMS-wrapped XML +# over HTTPS with the same eContentType OID and the same HTTPS +# content-type, and the overall style of the XML messages is very +# similar to the left-right protocol. All operations allow an +# optional "tag" attribute to allow batching. +# +# The %publication engine operates a single HTTPS server which serves +# both of these subprotocols. The two subprotocols share a single +# server port, but use distinct URLs to allow demultiplexing. +# +# @section Terminology +# +# @li @em IRBE: Internet Registry Back End +# +# @li @em IRDB: Internet Registry Data Base +# +# @li @em BPKI: Business PKI +# +# @li @em RPKI: Resource PKI +# +# @section Publication-control Publication control subprotocol +# +# The control subprotocol reuses the message-passing design of the +# left-right protocol. Configured objects support the "create", "set", +# "get", "list", and "destroy" actions, or a subset thereof when the +# full set of actions doesn't make sense. +# +# @subsection config_obj <config/> object +# +# The <config/> %object allows configuration of data that apply to the +# entire %publication server rather than a particular client. +# +# There is exactly one <config/> %object in the %publication server, and +# it only supports the "set" and "get" actions -- it cannot be created +# or destroyed. +# +# Payload data which can be configured in a <config/> %object: +# +# @li @c bpki_crl (element): +# This is the BPKI CRL used by the %publication server when +# signing the CMS wrapper on responses in the %publication +# subprotocol. As the CRL must be updated at regular intervals, +# it's not practical to restart the %publication server when the +# BPKI CRL needs to be updated. The BPKI model doesn't require +# use of a BPKI CRL between the IRBE and the %publication server, +# so we can use the %publication control subprotocol to update the +# BPKI CRL. +# +# @subsection client_obj <client/> object +# +# The <client/> %object represents one client authorized to use the +# %publication server. +# +# The <client/> %object supports the full set of "create", "set", "get", +# "list", and "destroy" actions. Each client has a "client_id" +# attribute, which is used in responses and must be specified in "set", +# "get", or "destroy" actions. +# +# Payload data which can be configured in a <client/> %object: +# +# @li @c base_uri (attribute): +# This is the base URI below which this client is allowed to publish +# data. The %publication server may impose additional constraints in +# the case of a child publishing beneath its parent. +# +# @li @c bpki_cert (element): +# BPKI CA certificate for this <client/>. This is used as part of +# the certificate chain when validating incoming TLS and CMS +# messages. If the bpki_glue certificate is in use (below), the +# bpki_cert certificate should be issued by the bpki_glue +# certificate; otherwise, the bpki_cert certificate should be issued +# by the %publication engine's bpki_ta certificate. +# +# @li @c bpki_glue (element): +# Another BPKI CA certificate for this <client/>, usually not +# needed. Certain pathological cross-certification cases require a +# two-certificate chain due to issuer name conflicts. If used, the +# bpki_glue certificate should be the issuer of the bpki_cert +# certificate and should be issued by the %publication engine's +# bpki_ta certificate; if not needed, the bpki_glue certificate +# should be left unset. +# +# @section Publication-publication Publication subprotocol +# +# The %publication subprotocol is structured somewhat differently from +# the %publication control protocol. Objects in the %publication +# subprotocol represent objects to be published or objects to be +# withdrawn from %publication. Each kind of %object supports two actions: +# "publish" and "withdraw". In each case the XML element representing +# hte %object to be published or withdrawn has a "uri" attribute which +# contains the %publication URI. For "publish" actions, the XML element +# body contains the DER %object to be published, encoded in Base64; for +# "withdraw" actions, the XML element body is empty. +# +# In theory, the detailed access control for each kind of %object might +# be different. In practice, as of this writing, access control for all +# objects is a simple check that the client's @c "base_uri" is a leading +# substring of the %publication URI. Details of why access control might +# need to become more complicated are discussed in a later section. +# +# @subsection certificate_obj <certificate/> object +# +# The <certificate/> %object represents an RPKI certificate to be +# published or withdrawn. +# +# @subsection crl_obj <crl/> object +# +# The <crl/> %object represents an RPKI CRL to be published or withdrawn. +# +# @subsection manifest_obj <manifest/> object +# +# The <manifest/> %object represents an RPKI %publication %manifest to be +# published or withdrawn. +# +# Note that part of the reason for the batching support in the +# %publication protocol is because @em every %publication or withdrawal +# action requires a new %manifest, thus every %publication or withdrawal +# action will involve at least two objects. +# +# @subsection roa_obj <roa/> object +# +# The <roa/> %object represents a ROA to be published or withdrawn. +# +# @section publication_error_handling Error handling +# +# Error in this protocol are handled at two levels. +# +# Since all messages in this protocol are conveyed over HTTPS +# connections, basic errors are indicated via the HTTP response code. +# 4xx and 5xx responses indicate that something bad happened. Errors +# that make it impossible to decode a query or encode a response are +# handled in this way. +# +# Where possible, errors will result in a <report_error/> message which +# takes the place of the expected protocol response message. +# <report_error/> messages are CMS-signed XML messages like the rest of +# this protocol, and thus can be archived to provide an audit trail. +# +# <report_error/> messages only appear in replies, never in +# queries. The <report_error/> message can appear in both the +# control and publication subprotocols. +# +# The <report_error/> message includes an optional @c "tag" attribute to +# assist in matching the error with a particular query when using +# batching. +# +# The error itself is conveyed in the @c error_code (attribute). The +# value of this attribute is a token indicating the specific error that +# occurred. At present this will be the name of a Python exception; the +# production version of this protocol will nail down the allowed error +# tokens here, probably in the RelaxNG schema. +# +# The body of the <report_error/> element itself is an optional text +# string; if present, this is debugging information. At present this +# capabilty is not used, debugging information goes to syslog. +# +# @section publication_access_control Additional access control considerations. +# +# As detailed above, the %publication protocol is trivially simple. This +# glosses over two bits of potential complexity: +# +# @li In the case where parent and child are sharing a repository, we'd +# like to nest child under parent, because testing has demonstrated +# that even on relatively slow hardware the delays involved in +# setting up separate rsync connections tend to dominate +# synchronization time for relying parties. +# +# @li The repository operator might also want to do some checks to +# assure itself that what it's about to allow the RPKI engine to +# publish is not dangerous toxic waste. +# +# The up-down protocol includes a mechanism by which a parent can +# suggest a %publication URI to each of its children. The children are +# not required to accept this hint, and the children must make separate +# arrangements with the repository operator (who might or might not be +# the same as the entity that hosts the children's RPKI engine +# operations) to use the suggested %publication point, but if everything +# works out, this allows children to nest cleanly under their parents +# %publication points, which helps reduce synchronization time for +# relying parties. +# +# In this case, one could argue that the %publication server is +# responsible for preventing one of its clients (the child in the above +# description) from stomping on data published by another of its clients +# (the parent in the above description). This goes beyond the basic +# access check and requires the %publication server to determine whether +# the parent has given its consent for the child to publish under the +# parent. Since the RPKI certificate profile requires the child's +# %publication point to be indicated in an SIA extension in a certificate +# issued by the parent to the child, the %publication engine can infer +# this permission from the parent's issuance of a certificate to the +# child. Since, by definition, the parent also uses this %publication +# server, this is an easy check, as the %publication server should +# already have the parent's certificate available by the time it needs +# to check the child's certificate. +# +# The previous paragraph only covers a "publish" action for a +# <certificate/> %object. For "publish" actions on other +# objects, the %publication server would need to trace permission back +# to the certificate issued by the parent; for "withdraw" actions, +# the %publication server would have to perform the same checks it +# would perform for a "publish" action, using the current published +# data before withdrawing it. The latter in turn implies an ordering +# constraint on "withdraw" actions in order to preserve the data +# necessary for these access control decisions; as this may prove +# impractical, the %publication server may probably need to make +# periodic sweeps over its published data looking for orphaned +# objects, but that's probably a good idea anyway. +# +# Note that, in this %publication model, any agreement that the +# repository makes to publish the RPKI engine's output is conditional +# upon the %object to be published passing whatever access control checks +# the %publication server imposes. + +## @page sql-schemas SQL database schemas +# +# @li @subpage rpkid-sql "rpkid database schema" +# @li @subpage pubd-sql "pubd database schema" +# @li @subpage irdbd-sql "irdbd database schema" + +## @page rpkid-sql rpkid SQL schema +# +# @dotfile rpkid.dot "Diagram of rpkid.sql" +# +# @verbinclude rpkid.sql + +## @page pubd-sql pubd SQL Schema +# +# @dotfile pubd.dot "Diagram of pubd.sql" +# +# @verbinclude pubd.sql + +## @page irdbd-sql irdbd SQL Schema +# +# @dotfile irdbd.dot "Diagram of irdbd.sql" +# +# @verbinclude irdbd.sql + +## @page bpki-model BPKI model +# +# The "business PKI" (BPKI) is the PKI used to authenticate +# communication on the up-down, left-right, and %publication protocols. +# BPKI certificates are @em not resource PKI (RPKI) certificates. The +# BPKI is a separate PKI that represents relationships between the +# various entities involved in the production side of the RPKI system. +# In most cases the BPKI tree will follow existing business +# relationships, hence the name "BPKI". +# +# Setup of the BPKI is handled by the back end; for the most part, +# rpkid and pubd just use the result. The one place where the engines +# are directly involved in creation of new BPKI certificates is in the +# production of end-entity certificates for use by the engines. +# +# There are a few design principals that underly the chosen BPKI model: +# @li Each engine should rely on a single BPKI trust anchor which is +# controlled by the back end entity that runs the engine; all +# other trust material should be cross-certified into the engine's +# BPKI tree. +# @li Private keys must never transit the network. +# @li Except for end entity certificates, the engine should only have +# access to the BPKI certificates; in particular, the private key +# for the BPKI trust anchor should not be accessible to the engine. +# @li The number of BPKI keys and certificates that the engine has to +# manage should be no larger than is necessary. +# +# rpkid's hosting model adds an additional constraint: rpkid's BPKI +# trust anchor belongs to the entity operating rpkid, but the entities +# hosted by rpkid should have control of their own BPKI private keys. +# This implies the need for an additional layer of BPKI certificate +# hierarchy within rpkid. +# +# Here is a simplified picture of what the BPKI might look like for an +# rpkid operator that hosts two entities, "Alice" and "Ellen": +# +# @dot +# // Color code: +# // Black: Hosting entity +# // Blue: Hosted entity +# // Red: Cross-certified peer +# // +# // Shape code: +# // Octagon: TA +# // Diamond: CA +# // Record: EE +# +# digraph bpki_rpkid { +# splines = true; +# size = "14,14"; +# node [ fontname = Times, fontsize = 9 ]; +# +# // Hosting entity +# node [ color = black, shape = record ]; +# TA [ shape = octagon, label = "BPKI TA" ]; +# rpkid [ label = "rpkid|{HTTPS server|HTTPS left-right client|CMS left-right}" ]; +# irdbd [ label = "irdbd|{HTTPS left-right server|CMS left-right}" ]; +# irbe [ label = "IRBE|{HTTPS left-right client|CMS left-right}" ]; +# +# // Hosted entities +# node [ color = blue, fontcolor = blue ]; +# Alice_CA [ shape = diamond ]; +# Alice_EE [ label = "Alice\nBSC EE|{HTTPS up-down client|CMS up-down}" ]; +# Ellen_CA [ shape = diamond ]; +# Ellen_EE [ label = "Ellen\nBSC EE|{HTTPS up-down client|CMS up-down}" ]; +# +# // Peers +# node [ color = red, fontcolor = red, shape = diamond ]; +# Bob_CA; +# Carol_CA; +# Dave_CA; +# Frank_CA; +# Ginny_CA; +# Harry_CA; +# node [ shape = record ]; +# Bob_EE [ label = "Bob\nEE|{HTTPS up-down|CMS up-down}" ]; +# Carol_EE [ label = "Carol\nEE|{HTTPS up-down|CMS up-down}" ]; +# Dave_EE [ label = "Dave\nEE|{HTTPS up-down|CMS up-down}" ]; +# Frank_EE [ label = "Frank\nEE|{HTTPS up-down|CMS up-down}" ]; +# Ginny_EE [ label = "Ginny\nEE|{HTTPS up-down|CMS up-down}" ]; +# Harry_EE [ label = "Bob\nEE|{HTTPS up-down|CMS up-down}" ]; +# +# edge [ color = black, style = solid ]; +# TA -> Alice_CA; +# TA -> Ellen_CA; +# +# edge [ color = black, style = dotted ]; +# TA -> rpkid; +# TA -> irdbd; +# TA -> irbe; +# +# edge [ color = blue, style = solid ]; +# Alice_CA -> Bob_CA; +# Alice_CA -> Carol_CA; +# Alice_CA -> Dave_CA; +# Ellen_CA -> Frank_CA; +# Ellen_CA -> Ginny_CA; +# Ellen_CA -> Harry_CA; +# +# edge [ color = blue, style = dotted ]; +# Alice_CA -> Alice_EE; +# Ellen_CA -> Ellen_EE; +# +# edge [ color = red, style = solid ]; +# Bob_CA -> Bob_EE; +# Carol_CA -> Carol_EE; +# Dave_CA -> Dave_EE; +# Frank_CA -> Frank_EE; +# Ginny_CA -> Ginny_EE; +# Harry_CA -> Harry_EE; +# } +# @enddot +# +# Black objects belong to the hosting entity, blue objects belong to +# the hosted entities, red objects are cross-certified objects from +# the hosted entities' peers. The arrows indicate certificate +# issuance: solid arrows are the ones that rpkid will care about +# during certificate validation, dotted arrows show the origin of the +# EE certificates that rpkid uses to sign CMS and TLS messages. +# +# There's one nasty bit where the model had to bend to fit the current +# state of the underlying protocols: it's not possible to use exactly +# the same BPKI keys and certificates for HTTPS and CMS. The reason +# for this is simple: each hosted entity has its own BPKI, as does the +# hosting entity, but the HTTPS listener is shared. The only ways to +# avoid sharing the HTTPS server certificate would be to use separate +# listeners for each hosted entity, which scales poorly, or to rely on +# the TLS "Server Name Indication" extension (RFC 4366 3.1) which is +# not yet widely implemented. +# +# The certificate tree looks complicated, but the set of certificates +# needed to build any particular validation chain is obvious, again +# excepting the HTTPS server case, where the client certificate is the +# first hint that the engine has of the client's identity, so the +# server must be prepared to accept any current client certificate. +# +# Detailed instructions on how to build a BPKI are beyond the scope of +# this document, but one can handle simple cases using the OpenSSL +# command line tool and cross_certify.py; the latter is a tool +# designed specifically for the purpose of generating the +# cross-certification certificates needed to splice foreign trust +# material into a BPKI tree. +# +# The BPKI tree for a pubd instance is similar to to the BPKI tree for +# an rpkid instance, but is a bit simpler, as pubd does not provide +# hosting in the same sense that rpkid does: pubd is a relatively +# simple server that publishes objects as instructed by its clients. +# +# Here's a simplified picture of what the BPKI might look like for a +# pubd operator that serves two clients, "Alice" and "Bob": +# +# @dot +# // Color code: +# // Black: Operating entity +# // Red: Cross-certified client +# // +# // Shape code: +# // Octagon: TA +# // Diamond: CA +# // Record: EE +# +# digraph bpki_pubd { +# splines = true; +# size = "14,14"; +# node [ fontname = Times, fontsize = 9 ]; +# +# // Operating entity +# node [ color = black, fontcolor = black, shape = record ]; +# TA [ shape = octagon, label = "BPKI TA" ]; +# pubd [ label = "pubd|{HTTPS server|CMS}" ]; +# ctl [ label = "Control|{HTTPS client|CMS}" ]; +# +# // Clients +# node [ color = red, fontcolor = red, shape = diamond ]; +# Alice_CA; +# Bob_CA; +# node [ color = red, fontcolor = red, shape = record ]; +# Alice_EE [ label = "Alice\nEE|{HTTPS client|CMS}" ]; +# Bob_EE [ label = "Bob\nEE|{HTTPS client|CMS}" ]; +# +# edge [ color = black, style = dotted ]; +# TA -> pubd; +# TA -> ctl; +# +# edge [ color = black, style = solid ]; +# TA -> Alice_CA; +# TA -> Bob_CA; +# +# edge [ color = red, style = solid ]; +# Alice_CA -> Alice_EE; +# Bob_CA -> Bob_EE; +# } +# @enddot +# +# While it is likely that RIRs (at least) will operate both rpkid and +# pubd instances, the two functions are conceptually separate. As far +# as pubd is concerned, it doesn't matter who operates the rpkid +# instance: pubd just has clients, each of which has trust material +# that has been cross-certified into pubd's BPKI. Similarly, rpkid +# doesn't really care who operates a pubd instance that it's been +# configured to use, it just treats that pubd as a foreign BPKI whose +# trust material has to be cross-certified into its own BPKI. Cross +# certification itself is done by the back end operator, using +# cross_certify or some equivalent tool; the resulting BPKI +# certificates are configured into rpkid and pubd via the left-right +# protocol and the control subprotocol of the publication protocol, +# respectively. +# +# Because the BPKI tree is almost entirely controlled by the operating +# entity, CRLs are not necessary for most of the BPKI. The one +# exception to this is the EE certificates issued under the +# cross-certification points. These EE certificates are generated by +# the peer, not the local operator, and thus require CRLs. Because of +# this, both rpkid and pubd require regular updates of certain BPKI +# CRLs, again via the left-right and publication control protocols. +# +# Because the left-right protocol and the publication control +# subprotocol are used to configure BPKI certificates and CRLs, they +# cannot themselves use certificates and CRLs configured in this way. +# This is why the configuration files for rpkid and pubd require +# static configuration of the left-right and publication control +# certificates. + +# Local Variables: +# compile-command: "cd .. && make doc" +# End: diff --git a/rpkid.stable/rpki/config.py b/rpkid.stable/rpki/config.py new file mode 100644 index 00000000..df856311 --- /dev/null +++ b/rpkid.stable/rpki/config.py @@ -0,0 +1,56 @@ +"""Configuration file parsing utilities, layered on top of stock +Python ConfigParser module. + +$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 ConfigParser + +class parser(ConfigParser.RawConfigParser): + + def __init__(self, file = None, section = None): + """Initialize this parser.""" + ConfigParser.RawConfigParser.__init__(self) + if file: + self.read(file) + self.default_section = section + + def multiget(self, option, section = None): + """Parse OpenSSL-style foo.0, foo.1, ... subscripted options. + + Returns a list of values matching the specified option name. + """ + matches = [] + if section is None: + section = self.default_section + if self.has_option(section, option): + matches.append((-1, self.get(option, section = section))) + for key, value in self.items(section): + s = key.rsplit(".", 1) + if len(s) == 2 and s[0] == option and s[1].isdigit(): + matches.append((int(s[1]), value)) + matches.sort() + return [match[1] for match in matches] + + def get(self, option, default = None, section = None): + """Get an option, perhaps with a default value.""" + if section is None: + section = self.default_section + if default is None or self.has_option(section, option): + return ConfigParser.RawConfigParser.get(self, section, option) + else: + return default diff --git a/rpkid.stable/rpki/exceptions.py b/rpkid.stable/rpki/exceptions.py new file mode 100644 index 00000000..393700e6 --- /dev/null +++ b/rpkid.stable/rpki/exceptions.py @@ -0,0 +1,135 @@ +"""Exception definitions for RPKI modules. + +$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. +""" + +class RPKI_Exception(Exception): + """Base class for RPKI exceptions.""" + +class NotInDatabase(RPKI_Exception): + """Lookup failed for an object expected to be in the database.""" + +class BadURISyntax(RPKI_Exception): + """Illegal syntax for a URI.""" + +class BadStatusCode(RPKI_Exception): + """Unrecognized protocol status code.""" + +class BadQuery(RPKI_Exception): + """Unexpected protocol query.""" + +class DBConsistancyError(RPKI_Exception): + """Found multiple matches for a database query that shouldn't ever return that.""" + +class CMSVerificationFailed(RPKI_Exception): + """Verification of a CMS message failed.""" + +class HTTPRequestFailed(RPKI_Exception): + """HTTP request failed.""" + +class DERObjectConversionError(RPKI_Exception): + """Error trying to convert a DER-based object from one representation to another.""" + +class NotACertificateChain(RPKI_Exception): + """Certificates don't form a proper chain.""" + +class BadContactURL(RPKI_Exception): + """Error trying to parse up-down protocol contact URL.""" + +class BadClassNameSyntax(RPKI_Exception): + """Illegal syntax for a class_name.""" + +class BadIssueResponse(RPKI_Exception): + """issue_response PDU with wrong number of classes or certificates.""" + +class NotImplementedYet(RPKI_Exception): + """Internal error -- not implemented yet.""" + +class BadPKCS10(RPKI_Exception): + """Bad PKCS #10 object.""" + +class UpstreamError(RPKI_Exception): + """Received an error from upstream.""" + +class ChildNotFound(RPKI_Exception): + """Could not find specified child in database.""" + +class BSCNotFound(RPKI_Exception): + """Could not find specified BSC in database.""" + +class BadSender(RPKI_Exception): + """Unexpected XML sender value.""" + +class ClassNameMismatch(RPKI_Exception): + """class_name does not match child context.""" + +class ClassNameUnknown(RPKI_Exception): + """Unknown class_name.""" + +class SKIMismatch(RPKI_Exception): + """SKI value in response does not match request.""" + +class SubprocessError(RPKI_Exception): + """Subprocess returned unexpected error.""" + +class BadIRDBReply(RPKI_Exception): + """Unexpected reply to IRDB query.""" + +class NotFound(RPKI_Exception): + """Object not found in database.""" + +class MustBePrefix(RPKI_Exception): + """Resource range cannot be expressed as a prefix.""" + +class TLSValidationError(RPKI_Exception): + """TLS certificate validation error.""" + +class MultipleTLSEECert(TLSValidationError): + """Received more than one TLS EE certificate.""" + +class ReceivedTLSCACert(TLSValidationError): + """Received CA certificate via TLS.""" + +class WrongEContentType(RPKI_Exception): + """Received wrong CMS eContentType.""" + +class EmptyPEM(RPKI_Exception): + """Couldn't find PEM block to convert.""" + +class UnexpectedCMSCerts(RPKI_Exception): + """Received CMS certs when not expecting any.""" + +class UnexpectedCMSCRLs(RPKI_Exception): + """Received CMS CRLs when not expecting any.""" + +class MissingCMSEEcert(RPKI_Exception): + """Didn't receive CMS EE cert when expecting one.""" + +class MissingCMSCRL(RPKI_Exception): + """Didn't receive CMS CRL when expecting one.""" + +class UnparsableCMSDER(RPKI_Exception): + """Alleged CMS DER wasn't parsable.""" + +class CMSCRLNotSet(RPKI_Exception): + """CMS CRL has not been configured.""" + +class ServerShuttingDown(RPKI_Exception): + """Server is shutting down.""" + +class NoActiveCA(RPKI_Exception): + """No active ca_detail for specified class.""" diff --git a/rpkid.stable/rpki/https.py b/rpkid.stable/rpki/https.py new file mode 100644 index 00000000..a0443c01 --- /dev/null +++ b/rpkid.stable/rpki/https.py @@ -0,0 +1,291 @@ +"""HTTPS utilities, both client and server. + +At the moment this only knows how to use the PEM certs in my +subversion repository; generalizing it would not be hard, but the more +general version should use SQL anyway. + +$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 httplib, BaseHTTPServer, tlslite.api, glob, traceback, urlparse, socket, signal +import rpki.x509, rpki.exceptions, rpki.log + +# This should be wrapped somewhere in rpki.x509 eventually +import POW + +# Do not set this to True for production use! +disable_tls_certificate_validation_exceptions = False + +# Chatter about TLS certificates +debug_tls_certs = False + +rpki_content_type = "application/x-rpki" + +def tlslite_certChain(x509): + """Utility function to construct tlslite certChains.""" + if isinstance(x509, rpki.x509.X509): + return tlslite.api.X509CertChain([x509.get_tlslite()]) + else: + return tlslite.api.X509CertChain([x.get_tlslite() for x in x509]) + +def build_https_ta_cache(certs): + """Build a dynamic TLS trust anchor cache.""" + + store = POW.X509Store() + for x in certs: + if rpki.https.debug_tls_certs: + rpki.log.debug("HTTPS dynamic trusted cert issuer %s [%s] subject %s [%s]" % (x.getIssuer(), x.hAKI(), x.getSubject(), x.hSKI())) + store.addTrust(x.get_POW()) + return store + +class Checker(tlslite.api.Checker): + """Derived class to handle X.509 client certificate checking.""" + + ## @var refuse_tls_ca_certs + # Raise an exception upon receiving CA certificates via TLS rather + # than just quietly ignoring them. + + refuse_tls_ca_certs = False + + ## @var pem_dump_tls_certs + # Vile debugging hack + + pem_dump_tls_certs = False + + def __init__(self, trust_anchor = None, dynamic_https_trust_anchor = None): + """Initialize our modified certificate checker.""" + + self.dynamic_https_trust_anchor = dynamic_https_trust_anchor + + if dynamic_https_trust_anchor is not None: + return + + self.x509store = POW.X509Store() + + trust_anchor = rpki.x509.X509.normalize_chain(trust_anchor) + assert trust_anchor + + for x in trust_anchor: + if debug_tls_certs: + rpki.log.debug("HTTPS trusted cert issuer %s [%s] subject %s [%s]" % (x.getIssuer(), x.hAKI(), x.getSubject(), x.hSKI())) + self.x509store.addTrust(x.get_POW()) + if self.pem_dump_tls_certs: + print x.get_PEM() + + def x509store_thunk(self): + if self.dynamic_https_trust_anchor is not None: + return self.dynamic_https_trust_anchor() + else: + return self.x509store + + def __call__(self, tlsConnection): + """POW/OpenSSL-based certificate checker. + + Given our BPKI model, we're only interested in the TLS EE + certificates. + """ + + if tlsConnection._client: + chain = tlsConnection.session.serverCertChain + peer = "server" + else: + chain = tlsConnection.session.clientCertChain + peer = "client" + + chain = [rpki.x509.X509(tlslite = chain.x509List[i]) for i in range(chain.getNumCerts())] + + ee = None + + for x in chain: + + if debug_tls_certs: + rpki.log.debug("Received %s TLS %s cert issuer %s [%s] subject %s [%s]" + % (peer, "CA" if x.is_CA() else "EE", x.getIssuer(), x.hAKI(), x.getSubject(), x.hSKI())) + if self.pem_dump_tls_certs: + print x.get_PEM() + + if x.is_CA(): + if self.refuse_tls_ca_certs: + raise rpki.exceptions.ReceivedTLSCACert + continue + + if ee is not None: + raise rpki.exceptions.MultipleTLSEECert, chain + ee = x + + result = self.x509store_thunk().verifyDetailed(ee.get_POW()) + if not result[0]: + rpki.log.debug("TLS certificate validation result %s" % repr(result)) + if disable_tls_certificate_validation_exceptions: + rpki.log.warn("DANGER WILL ROBINSON! IGNORING TLS VALIDATION FAILURE!") + else: + raise rpki.exceptions.TLSValidationError + +class httpsClient(tlslite.api.HTTPTLSConnection): + """Derived class to let us replace the default Checker.""" + + def __init__(self, host, port = None, + client_cert = None, client_key = None, + server_ta = None, settings = None): + """Create a new httpsClient.""" + + tlslite.api.HTTPTLSConnection.__init__( + self, host = host, port = port, settings = settings, + certChain = client_cert, privateKey = client_key) + + self.checker = Checker(trust_anchor = server_ta) + +def client(msg, client_key, client_cert, server_ta, url, timeout = 300): + """Open client HTTPS connection, send a message, wait for response. + + This function wraps most of what one needs to do to send a message + over HTTPS and get a response. The certificate checking isn't quite + up to snuff; it's better than with the other packages I've found, + but doesn't appear to handle subjectAltName extensions (sigh). + """ + + u = urlparse.urlparse(url) + + assert u.scheme in ("", "https") and \ + u.username is None and \ + u.password is None and \ + u.params == "" and \ + u.query == "" and \ + u.fragment == "" + + rpki.log.debug("Contacting %s" % url) + + if debug_tls_certs: + for cert in (client_cert,) if isinstance(client_cert, rpki.x509.X509) else client_cert: + rpki.log.debug("Sending client TLS cert issuer %s subject %s" % (cert.getIssuer(), cert.getSubject())) + + # We could add a "settings = foo" argument to the following call to + # pass in a tlslite.HandshakeSettings object that would let us + # insist on, eg, particular SSL/TLS versions. + + httpc = httpsClient(host = u.hostname or "localhost", + port = u.port or 443, + client_key = client_key.get_tlslite(), + client_cert = tlslite_certChain(client_cert), + server_ta = server_ta) + httpc.connect() + httpc.sock.settimeout(timeout) + httpc.request("POST", u.path, msg, {"Content-Type" : rpki_content_type}) + response = httpc.getresponse() + if response.status == httplib.OK: + return response.read() + else: + r = response.read() + raise rpki.exceptions.HTTPRequestFailed, \ + "HTTP request failed with status %s, response %s" % (response.status, r) + +class requestHandler(BaseHTTPServer.BaseHTTPRequestHandler): + """Derived type to supply POST handler and override logging.""" + + rpki_handlers = None # Subclass must bind + + def rpki_find_handler(self): + """Helper method to search self.rpki_handlers.""" + for s,h in self.rpki_handlers: + if self.path.startswith(s): + return h + return None + + def do_POST(self): + """POST handler.""" + try: + handler = self.rpki_find_handler() + if self.headers["Content-Type"] != rpki_content_type: + rcode, rtext = 415, "Received Content-Type %s, expected %s" \ + % (self.headers["Content-Type"], rpki_content_type) + elif handler is None: + rcode, rtext = 404, "No handler found for URL " + self.path + else: + rcode, rtext = handler(query = self.rfile.read(int(self.headers["Content-Length"])), + path = self.path) + except Exception, edata: + rpki.log.error(traceback.format_exc()) + rcode, rtext = 500, "Unhandled exception %s" % edata + self.send_response(rcode) + self.send_header("Content-Type", rpki_content_type) + self.end_headers() + self.wfile.write(rtext) + + def log_message(self, format, *args): + """Redirect HTTP server logging into our own logging system.""" + if args: + rpki.log.info(format % args) + else: + rpki.log.info(format) + +class httpsServer(tlslite.api.TLSSocketServerMixIn, BaseHTTPServer.HTTPServer): + """Derived type to handle TLS aspects of HTTPS.""" + + rpki_sessionCache = None + rpki_server_key = None + rpki_server_cert = None + rpki_checker = None + + def handshake(self, tlsConnection): + """TLS handshake handler.""" + assert self.rpki_server_cert is not None + assert self.rpki_server_key is not None + assert self.rpki_sessionCache is not None + + try: + # + # We could add a "settings = foo" argument to the following call + # to pass in a tlslite.HandshakeSettings object that would let + # us insist on, eg, particular SSL/TLS versions. + # + tlsConnection.handshakeServer(certChain = self.rpki_server_cert, + privateKey = self.rpki_server_key, + sessionCache = self.rpki_sessionCache, + checker = self.rpki_checker, + reqCert = True) + tlsConnection.ignoreAbruptClose = True + return True + except (tlslite.api.TLSError, rpki.exceptions.TLSValidationError), error: + rpki.log.warn("TLS handshake failure: " + str(error)) + return False + +def server(handlers, server_key, server_cert, port = 4433, host ="", client_ta = None, dynamic_https_trust_anchor = None, catch_signals = (signal.SIGINT, signal.SIGTERM)): + """Run an HTTPS server and wait (forever) for connections.""" + + if not isinstance(handlers, (tuple, list)): + handlers = (("/", handlers),) + + class boundRequestHandler(requestHandler): + rpki_handlers = handlers + + httpd = httpsServer((host, port), boundRequestHandler) + + httpd.rpki_server_key = server_key.get_tlslite() + httpd.rpki_server_cert = tlslite_certChain(server_cert) + httpd.rpki_sessionCache = tlslite.api.SessionCache() + httpd.rpki_checker = Checker(trust_anchor = client_ta, dynamic_https_trust_anchor = dynamic_https_trust_anchor) + + try: + def raiseServerShuttingDown(signum, frame): + raise rpki.exceptions.ServerShuttingDown + old_signal_handlers = tuple((sig, signal.signal(sig, raiseServerShuttingDown)) for sig in catch_signals) + httpd.serve_forever() + except rpki.exceptions.ServerShuttingDown: + pass + finally: + for sig,handler in old_signal_handlers: + signal.signal(sig, handler) diff --git a/rpkid.stable/rpki/ipaddrs.py b/rpkid.stable/rpki/ipaddrs.py new file mode 100644 index 00000000..db6a5891 --- /dev/null +++ b/rpkid.stable/rpki/ipaddrs.py @@ -0,0 +1,103 @@ +"""Classes to represent IP addresses. + +Given some of the other operations we need to perform on them, it's +most convenient to represent IP addresses as Python "long" values. +The classes in this module just wrap suitable read/write syntax around +the underlying "long" type. + +These classes also supply a "bits" attribute for use by other code +built on these classes; for the most part, IPv6 addresses really are +just IPv4 addresses with more bits, so we supply the number of bits +once, here, thus avoiding a lot of duplicate code elsewhere. + +$Id$ + + +Copyright (C) 2009 Internet Systems Consortium ("ISC") + +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 ISC DISCLAIMS ALL WARRANTIES WITH +REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY +AND FITNESS. IN NO EVENT SHALL ISC 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. + + +Portions 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 socket, struct + +class v4addr(long): + """IPv4 address. + + Derived from long, but supports IPv4 print syntax. + """ + + bits = 32 + + def __new__(cls, x): + """Construct a v4addr object.""" + if isinstance(x, str): + return cls.from_bytes(socket.inet_pton(socket.AF_INET, ".".join(str(int(i)) for i in x.split(".")))) + else: + return long.__new__(cls, x) + + def to_bytes(self): + """Convert a v4addr object to a raw byte string.""" + return struct.pack("!I", long(self)) + + @classmethod + def from_bytes(cls, x): + """Convert from a raw byte string to a v4addr object.""" + return cls(struct.unpack("!I", x)[0]) + + def __str__(self): + """Convert a v4addr object to string format.""" + return socket.inet_ntop(socket.AF_INET, self.to_bytes()) + +class v6addr(long): + """IPv6 address. + + Derived from long, but supports IPv6 print syntax. + """ + + bits = 128 + + def __new__(cls, x): + """Construct a v6addr object.""" + if isinstance(x, str): + return cls.from_bytes(socket.inet_pton(socket.AF_INET6, x)) + else: + return long.__new__(cls, x) + + def to_bytes(self): + """Convert a v6addr object to a raw byte string.""" + return struct.pack("!QQ", long(self) >> 64, long(self) & 0xFFFFFFFFFFFFFFFF) + + @classmethod + def from_bytes(cls, x): + """Convert from a raw byte string to a v6addr object.""" + x = struct.unpack("!QQ", x) + return cls((x[0] << 64) | x[1]) + + def __str__(self): + """Convert a v6addr object to string format.""" + return socket.inet_ntop(socket.AF_INET6, self.to_bytes()) 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 diff --git a/rpkid.stable/rpki/log.py b/rpkid.stable/rpki/log.py new file mode 100644 index 00000000..efaba10c --- /dev/null +++ b/rpkid.stable/rpki/log.py @@ -0,0 +1,58 @@ +"""Logging facilities for RPKI libraries. + +$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 syslog, traceback + +## @var enable_trace +# Whether call tracing is enabled. + +enable_trace = False + +def init(ident = "rpki", flags = syslog.LOG_PID | syslog.LOG_PERROR, facility = syslog.LOG_DAEMON): + """Initialize logging system.""" + + return syslog.openlog(ident, flags, facility) + +def set_trace(trace): + """Enable or disable call tracing.""" + + global enable_trace + enable_trace = trace + +class logger(object): + """Closure for logging.""" + + def __init__(self, priority): + self.priority = priority + + def __call__(self, message): + return syslog.syslog(self.priority, message) + +error = logger(syslog.LOG_ERR) +warn = logger(syslog.LOG_WARNING) +note = logger(syslog.LOG_NOTICE) +info = logger(syslog.LOG_INFO) +debug = logger(syslog.LOG_DEBUG) + +def trace(): + """Execution trace -- where are we now, and whence came we here?""" + + if enable_trace: + bt = traceback.extract_stack(limit = 3) + return debug("[%s() at %s:%d from %s:%d]" % (bt[1][2], bt[1][0], bt[1][1], bt[0][0], bt[0][1])) diff --git a/rpkid.stable/rpki/manifest.py b/rpkid.stable/rpki/manifest.py new file mode 100644 index 00000000..97b1cc71 --- /dev/null +++ b/rpkid.stable/rpki/manifest.py @@ -0,0 +1,53 @@ +"""Signed manifests. This is just the ASN.1 encoder, the rest is in +rpki.x509 with the rest of the DER_object code. + +Note that rpki.x509.SignedManifest implements the signed manifest; +the structures here are just the payload of the CMS eContent field. + +$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. +""" + +from POW._der import * + +class FileAndHash(Sequence): + def __init__(self, optional=0, default=''): + self.file = IA5String() + self.hash = AltBitString() + contents = [ self.file, self.hash ] + Sequence.__init__(self, contents, optional, default) + +class FilesAndHashes(SequenceOf): + def __init__(self, optional=0, default=''): + SequenceOf.__init__(self, FileAndHash, optional, default) + +class Manifest(Sequence): + def __init__(self, optional=0, default=''): + self.version = Integer() + self.explicitVersion = Explicit(CLASS_CONTEXT, FORM_CONSTRUCTED, 0, self.version, 0, 'oAMCAQA=') + self.manifestNumber = Integer() + self.thisUpdate = GeneralizedTime() + self.nextUpdate = GeneralizedTime() + self.fileHashAlg = Oid() + self.fileList = FilesAndHashes() + + contents = [ self.explicitVersion, + self.manifestNumber, + self.thisUpdate, + self.nextUpdate, + self.fileHashAlg, + self.fileList ] + Sequence.__init__(self, contents, optional, default) diff --git a/rpkid.stable/rpki/oids.py b/rpkid.stable/rpki/oids.py new file mode 100644 index 00000000..5824ad17 --- /dev/null +++ b/rpkid.stable/rpki/oids.py @@ -0,0 +1,57 @@ +"""OID database. + +$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. +""" + +## @var oid2name +# Mapping table of OIDs to conventional string names. + +oid2name = { + (1, 2, 840, 113549, 1, 1, 11) : "sha256WithRSAEncryption", + (1, 2, 840, 113549, 1, 1, 12) : "sha384WithRSAEncryption", + (1, 2, 840, 113549, 1, 1, 13) : "sha512WithRSAEncryption", + (1, 2, 840, 113549, 1, 7, 1) : "id-data", + (1, 2, 840, 113549, 1, 9, 16) : "id-smime", + (1, 2, 840, 113549, 1, 9, 16, 1) : "id-ct", + (1, 2, 840, 113549, 1, 9, 16, 1, 24) : "id-ct-routeOriginAttestation", + (1, 2, 840, 113549, 1, 9, 16, 1, 26) : "id-ct-rpkiManifest", + (1, 2, 840, 113549, 1, 9, 16, 1, 28) : "id-ct-xml", + (1, 3, 6, 1, 5, 5, 7, 1, 1) : "authorityInfoAccess", + (1, 3, 6, 1, 5, 5, 7, 1, 11) : "subjectInfoAccess", + (1, 3, 6, 1, 5, 5, 7, 1, 7) : "sbgp-ipAddrBlock", + (1, 3, 6, 1, 5, 5, 7, 1, 8) : "sbgp-autonomousSysNum", + (1, 3, 6, 1, 5, 5, 7, 14, 2) : "id-cp-ipAddr-asNumber", + (1, 3, 6, 1, 5, 5, 7, 48, 2) : "id-ad-caIssuers", + (1, 3, 6, 1, 5, 5, 7, 48, 5) : "id-ad-caRepository", + (1, 3, 6, 1, 5, 5, 7, 48, 9) : "id-ad-signedObjectRepository", + (1, 3, 6, 1, 5, 5, 7, 48, 10) : "id-ad-rpkiManifest", + (1, 3, 6, 1, 5, 5, 7, 48, 11) : "id-ad-signedObject", + (2, 16, 840, 1, 101, 3, 4, 2, 1) : "id-sha256", + (2, 5, 29, 14) : "subjectKeyIdentifier", + (2, 5, 29, 15) : "keyUsage", + (2, 5, 29, 19) : "basicConstraints", + (2, 5, 29, 20) : "cRLNumber", + (2, 5, 29, 31) : "cRLDistributionPoints", + (2, 5, 29, 32) : "certificatePolicies", + (2, 5, 29, 35) : "authorityKeyIdentifier", + (2, 5, 4, 3) : "commonName", +} + +## @var name2oid +# Mapping table of string names to OIDs + +name2oid = dict((v,k) for k,v in oid2name.items()) diff --git a/rpkid.stable/rpki/publication.py b/rpkid.stable/rpki/publication.py new file mode 100644 index 00000000..fe52b631 --- /dev/null +++ b/rpkid.stable/rpki/publication.py @@ -0,0 +1,282 @@ +"""RPKI "publication" 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 + +class publication_namespace(object): + """XML namespace parameters for publication protocol.""" + + xmlns = "http://www.hactrn.net/uris/rpki/publication-spec/" + nsmap = { None : xmlns } + +class control_elt(rpki.xml_utils.data_elt, rpki.sql.sql_persistant, publication_namespace): + """Virtual class for control channel objects.""" + + def serve_dispatch(self, r_msg, client): + """Action dispatch handler. This needs special handling because + we need to make sure that this PDU arrived via the control channel. + """ + if client is not None: + raise rpki.exceptions.BadQuery, "Control query received on client channel" + rpki.xml_utils.data_elt.serve_dispatch(self, r_msg) + +class config_elt(control_elt): + """<config/> element. This is a little weird because there should + never be more than one row in the SQL config table, but we have to + put the BPKI CRL somewhere and SQL is the least bad place available. + + So we reuse a lot of the SQL machinery, but we nail config_id at 1, + we don't expose it in the XML protocol, and we only support the get + and set actions. + """ + + attributes = ("action", "tag") + element_name = "config" + elements = ("bpki_crl",) + + sql_template = rpki.sql.template("config", "config_id", ("bpki_crl", rpki.x509.CRL)) + + wired_in_config_id = 1 + + def startElement(self, stack, name, attrs): + """StartElement() handler for config object. This requires + special handling because of the weird way we treat config_id. + """ + control_elt.startElement(self, stack, name, attrs) + self.config_id = self.wired_in_config_id + + @classmethod + def fetch(cls, gctx): + """Fetch the config object from SQL. This requires special + handling because of the weird way we treat config_id. + """ + return cls.sql_fetch(gctx, cls.wired_in_config_id) + + def serve_set(self, r_msg): + """Handle a set action. This requires special handling because + config we don't support the create method. + """ + if self.sql_fetch(self.gctx, self.config_id) is None: + control_elt.serve_create(self, r_msg) + else: + control_elt.serve_set(self, r_msg) + + def serve_fetch_one(self): + """Find the config object on which a get or set method should + operate. + """ + r = self.sql_fetch(self.gctx, self.config_id) + if r is None: + raise rpki.exceptions.NotFound + return r + +class client_elt(control_elt): + """<client/> element.""" + + element_name = "client" + attributes = ("action", "tag", "client_id", "base_uri") + elements = ("bpki_cert", "bpki_glue") + + sql_template = rpki.sql.template("client", "client_id", "base_uri", ("bpki_cert", rpki.x509.X509), ("bpki_glue", rpki.x509.X509)) + + base_uri = None + bpki_cert = None + bpki_glue = None + + clear_https_ta_cache = False + + def endElement(self, stack, name, text): + """Handle subelements of <client/> element. These require special + handling because modifying them invalidates the HTTPS trust anchor + cache. + """ + control_elt.endElement(self, stack, name, text) + if name in self.elements: + self.clear_https_ta_cache = True + + def serve_post_save_hook(self, q_pdu, r_pdu): + """Extra server actions for client_elt.""" + if self.clear_https_ta_cache: + self.gctx.clear_https_ta_cache() + self.clear_https_ta_cache = False + + def serve_fetch_one(self): + """Find the client object on which a get, set, or destroy method + should operate. + """ + r = self.sql_fetch(self.gctx, self.client_id) + if r is None: + raise rpki.exceptions.NotFound + return r + + def serve_fetch_all(self): + """Find client objects on which a list method should operate.""" + return self.sql_fetch_all(self.gctx) + + def check_allowed_uri(self, uri): + if not uri.startswith(self.base_uri): + raise rpki.exceptions.ForbiddenURI + +class publication_object_elt(rpki.xml_utils.base_elt, publication_namespace): + """Virtual class for publishable objects. These have very similar + syntax, differences lie in underlying datatype and methods. XML + methods are a little different from the pattern used for objects + that support the create/set/get/list/destroy actions, but + publishable objects don't go in SQL either so these classes would be + different in any case. + """ + + attributes = ("action", "tag", "client_id", "uri") + payload = None + + def endElement(self, stack, name, text): + """Handle a publishable element element.""" + assert name == self.element_name, "Unexpected name %s, stack %s" % (name, stack) + if text: + self.payload = self.payload_type(Base64 = text) + stack.pop() + + def toXML(self): + """Generate XML element for publishable object.""" + elt = self.make_elt() + if self.payload: + elt.text = base64.b64encode(self.payload.get_DER()) + return elt + + def serve_dispatch(self, r_msg, client): + """Action dispatch handler.""" + if client is None: + raise rpki.exceptions.BadQuery, "Client query received on control channel" + dispatch = { "publish" : self.serve_publish, + "withdraw" : self.serve_withdraw } + if self.action not in dispatch: + raise rpki.exceptions.BadQuery, "Unexpected query: action %s" % self.action + client.check_allowed_uri(self.uri) + dispatch[self.action]() + r_pdu = self.__class__() + r_pdu.action = self.action + r_pdu.tag = self.tag + r_pdu.uri = self.uri + r_msg.append(r_pdu) + + def serve_publish(self): + """Publish an object.""" + rpki.log.info("Publishing %s as %s" % (repr(self.payload), repr(self.uri))) + filename = self.uri_to_filename() + dirname = os.path.dirname(filename) + if not os.path.isdir(dirname): + os.makedirs(dirname) + f = open(filename, "wb") + f.write(self.payload.get_DER()) + f.close() + + def serve_withdraw(self): + """Withdraw an object.""" + rpki.log.info("Withdrawing %s" % repr(self.uri)) + os.remove(self.uri_to_filename()) + + def uri_to_filename(self): + """Convert a URI to a local filename.""" + if not self.uri.startswith("rsync://"): + raise rpki.exceptions.BadURISyntax + filename = self.gctx.publication_base + self.uri[len("rsync://"):] + if filename.find("//") >= 0 or filename.find("/../") >= 0 or filename.endswith("/.."): + raise rpki.exceptions.BadURISyntax + return filename + +class certificate_elt(publication_object_elt): + """<certificate/> element.""" + + element_name = "certificate" + payload_type = rpki.x509.X509 + +class crl_elt(publication_object_elt): + """<crl/> element.""" + + element_name = "crl" + payload_type = rpki.x509.CRL + +class manifest_elt(publication_object_elt): + """<manifest/> element.""" + + element_name = "manifest" + payload_type = rpki.x509.SignedManifest + +class roa_elt(publication_object_elt): + """<roa/> element.""" + + element_name = "roa" + payload_type = rpki.x509.ROA + +## @var obj2elt +# Map of data types to publication element wrapper types + +obj2elt = dict((e.payload_type, e) for e in (certificate_elt, crl_elt, manifest_elt, roa_elt)) + +class report_error_elt(rpki.xml_utils.base_elt, publication_namespace): + """<report_error/> element.""" + + element_name = "report_error" + attributes = ("tag", "error_code") + + @classmethod + def from_exception(cls, exc): + """Generate a <report_error/> element from an exception.""" + self = cls() + self.error_code = exc.__class__.__name__ + return self + +class msg(rpki.xml_utils.msg, publication_namespace): + """Publication 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 (config_elt, client_elt, certificate_elt, crl_elt, manifest_elt, roa_elt, report_error_elt)) + + def serve_top_level(self, gctx, client): + """Serve one msg PDU.""" + if self.type != "query": + raise rpki.exceptions.BadQuery, "Message type is not query" + r_msg = self.__class__() + r_msg.type = "reply" + for q_pdu in self: + q_pdu.gctx = gctx + q_pdu.serve_dispatch(r_msg, client) + return r_msg + +class sax_handler(rpki.xml_utils.sax_handler): + """SAX handler for publication protocol.""" + + pdu = msg + name = "msg" + version = "1" + +class cms_msg(rpki.x509.XML_CMS_object): + """Class to hold a CMS-signed publication PDU.""" + + encoding = "us-ascii" + schema = rpki.relaxng.publication + saxify = sax_handler.saxify diff --git a/rpkid.stable/rpki/relaxng.py b/rpkid.stable/rpki/relaxng.py new file mode 100644 index 00000000..c66d965a --- /dev/null +++ b/rpkid.stable/rpki/relaxng.py @@ -0,0 +1,1699 @@ +# Automatically generated, do not edit. + +import lxml.etree + +## @var left_right +## Parsed RelaxNG left_right schema +left_right = lxml.etree.RelaxNG(lxml.etree.fromstring('''<?xml version="1.0" encoding="UTF-8"?> +<!-- + $Id: left-right-schema.rnc 1835 2008-06-02 23:43:01Z sra $ + + RelaxNG Schema for RPKI left-right protocol. + + libxml2 (including xmllint) only groks the XML syntax of RelaxNG, so + run the compact syntax through trang to get XML syntax. +--> +<grammar ns="http://www.hactrn.net/uris/rpki/left-right-spec/" xmlns="http://relaxng.org/ns/structure/1.0" datatypeLibrary="http://www.w3.org/2001/XMLSchema-datatypes"> + <!-- Top level PDU --> + <start> + <element name="msg"> + <attribute name="version"> + <data type="positiveInteger"> + <param name="maxInclusive">1</param> + </data> + </attribute> + <choice> + <group> + <attribute name="type"> + <value>query</value> + </attribute> + <zeroOrMore> + <ref name="query_elt"/> + </zeroOrMore> + </group> + <group> + <attribute name="type"> + <value>reply</value> + </attribute> + <zeroOrMore> + <ref name="reply_elt"/> + </zeroOrMore> + </group> + </choice> + </element> + </start> + <!-- PDUs allowed in a query --> + <define name="query_elt" combine="choice"> + <ref name="self_query"/> + </define> + <define name="query_elt" combine="choice"> + <ref name="bsc_query"/> + </define> + <define name="query_elt" combine="choice"> + <ref name="parent_query"/> + </define> + <define name="query_elt" combine="choice"> + <ref name="child_query"/> + </define> + <define name="query_elt" combine="choice"> + <ref name="repository_query"/> + </define> + <define name="query_elt" combine="choice"> + <ref name="route_origin_query"/> + </define> + <define name="query_elt" combine="choice"> + <ref name="list_resources_query"/> + </define> + <!-- PDUs allowed in a reply --> + <define name="reply_elt" combine="choice"> + <ref name="self_reply"/> + </define> + <define name="reply_elt" combine="choice"> + <ref name="bsc_reply"/> + </define> + <define name="reply_elt" combine="choice"> + <ref name="parent_reply"/> + </define> + <define name="reply_elt" combine="choice"> + <ref name="child_reply"/> + </define> + <define name="reply_elt" combine="choice"> + <ref name="repository_reply"/> + </define> + <define name="reply_elt" combine="choice"> + <ref name="route_origin_reply"/> + </define> + <define name="reply_elt" combine="choice"> + <ref name="list_resources_reply"/> + </define> + <define name="reply_elt" combine="choice"> + <ref name="report_error_reply"/> + </define> + <!-- Tag attributes for bulk operations --> + <define name="tag"> + <optional> + <attribute name="tag"> + <data type="token"> + <param name="maxLength">1024</param> + </data> + </attribute> + </optional> + </define> + <!-- + Combinations of action and type attributes used in later definitions. + The same patterns repeat in most of the elements in this protocol. + --> + <define name="ctl_create"> + <attribute name="action"> + <value>create</value> + </attribute> + <ref name="tag"/> + </define> + <define name="ctl_set"> + <attribute name="action"> + <value>set</value> + </attribute> + <ref name="tag"/> + </define> + <define name="ctl_get"> + <attribute name="action"> + <value>get</value> + </attribute> + <ref name="tag"/> + </define> + <define name="ctl_list"> + <attribute name="action"> + <value>list</value> + </attribute> + <ref name="tag"/> + </define> + <define name="ctl_destroy"> + <attribute name="action"> + <value>destroy</value> + </attribute> + <ref name="tag"/> + </define> + <!-- Base64 encoded DER stuff --> + <define name="base64"> + <data type="base64Binary"> + <param name="maxLength">512000</param> + </data> + </define> + <!-- Base definition for all fields that are really just SQL primary indices --> + <define name="sql_id"> + <data type="nonNegativeInteger"/> + </define> + <!-- URIs --> + <define name="uri"> + <data type="anyURI"> + <param name="maxLength">4096</param> + </data> + </define> + <!-- Name fields imported from up-down protocol --> + <define name="up_down_name"> + <data type="token"> + <param name="maxLength">1024</param> + </data> + </define> + <!-- Resource lists --> + <define name="asn_list"> + <data type="string"> + <param name="maxLength">512000</param> + <param name="pattern">[\-,0-9]*</param> + </data> + </define> + <define name="ipv4_list"> + <data type="string"> + <param name="maxLength">512000</param> + <param name="pattern">[\-,0-9/.]*</param> + </data> + </define> + <define name="ipv6_list"> + <data type="string"> + <param name="maxLength">512000</param> + <param name="pattern">[\-,0-9/:a-fA-F]*</param> + </data> + </define> + <!-- <self/> element --> + <define name="self_bool"> + <optional> + <attribute name="rekey"> + <value>yes</value> + </attribute> + </optional> + <optional> + <attribute name="reissue"> + <value>yes</value> + </attribute> + </optional> + <optional> + <attribute name="revoke"> + <value>yes</value> + </attribute> + </optional> + <optional> + <attribute name="run_now"> + <value>yes</value> + </attribute> + </optional> + <optional> + <attribute name="publish_world_now"> + <value>yes</value> + </attribute> + </optional> + </define> + <define name="self_payload"> + <optional> + <attribute name="use_hsm"> + <choice> + <value>yes</value> + <value>no</value> + </choice> + </attribute> + </optional> + <optional> + <attribute name="crl_interval"> + <data type="positiveInteger"/> + </attribute> + </optional> + <optional> + <attribute name="regen_margin"> + <data type="positiveInteger"/> + </attribute> + </optional> + <optional> + <element name="bpki_cert"> + <ref name="base64"/> + </element> + </optional> + <optional> + <element name="bpki_glue"> + <ref name="base64"/> + </element> + </optional> + </define> + <define name="self_id"> + <attribute name="self_id"> + <ref name="sql_id"/> + </attribute> + </define> + <define name="self_query" combine="choice"> + <element name="self"> + <ref name="ctl_create"/> + <ref name="self_bool"/> + <ref name="self_payload"/> + </element> + </define> + <define name="self_reply" combine="choice"> + <element name="self"> + <ref name="ctl_create"/> + <ref name="self_id"/> + </element> + </define> + <define name="self_query" combine="choice"> + <element name="self"> + <ref name="ctl_set"/> + <ref name="self_id"/> + <ref name="self_bool"/> + <ref name="self_payload"/> + </element> + </define> + <define name="self_reply" combine="choice"> + <element name="self"> + <ref name="ctl_set"/> + <ref name="self_id"/> + </element> + </define> + <define name="self_query" combine="choice"> + <element name="self"> + <ref name="ctl_get"/> + <ref name="self_id"/> + </element> + </define> + <define name="self_reply" combine="choice"> + <element name="self"> + <ref name="ctl_get"/> + <ref name="self_id"/> + <ref name="self_payload"/> + </element> + </define> + <define name="self_query" combine="choice"> + <element name="self"> + <ref name="ctl_list"/> + </element> + </define> + <define name="self_reply" combine="choice"> + <element name="self"> + <ref name="ctl_list"/> + <ref name="self_id"/> + <ref name="self_payload"/> + </element> + </define> + <define name="self_query" combine="choice"> + <element name="self"> + <ref name="ctl_destroy"/> + <ref name="self_id"/> + </element> + </define> + <define name="self_reply" combine="choice"> + <element name="self"> + <ref name="ctl_destroy"/> + <ref name="self_id"/> + </element> + </define> + <!-- <bsc/> element. Key parameters hardwired for now. --> + <define name="bsc_bool"> + <optional> + <attribute name="generate_keypair"> + <value>yes</value> + </attribute> + <optional> + <attribute name="key_type"> + <value>rsa</value> + </attribute> + </optional> + <optional> + <attribute name="hash_alg"> + <value>sha256</value> + </attribute> + </optional> + <optional> + <attribute name="key_length"> + <value>2048</value> + </attribute> + </optional> + </optional> + </define> + <define name="bsc_id"> + <attribute name="bsc_id"> + <ref name="sql_id"/> + </attribute> + </define> + <define name="bsc_payload"> + <optional> + <element name="signing_cert"> + <ref name="base64"/> + </element> + </optional> + <optional> + <element name="signing_cert_crl"> + <ref name="base64"/> + </element> + </optional> + </define> + <define name="bsc_pkcs10"> + <optional> + <element name="pkcs10_request"> + <ref name="base64"/> + </element> + </optional> + </define> + <define name="bsc_query" combine="choice"> + <element name="bsc"> + <ref name="ctl_create"/> + <ref name="self_id"/> + <ref name="bsc_bool"/> + <ref name="bsc_payload"/> + </element> + </define> + <define name="bsc_reply" combine="choice"> + <element name="bsc"> + <ref name="ctl_create"/> + <ref name="self_id"/> + <ref name="bsc_id"/> + <ref name="bsc_pkcs10"/> + </element> + </define> + <define name="bsc_query" combine="choice"> + <element name="bsc"> + <ref name="ctl_set"/> + <ref name="self_id"/> + <ref name="bsc_id"/> + <ref name="bsc_bool"/> + <ref name="bsc_payload"/> + </element> + </define> + <define name="bsc_reply" combine="choice"> + <element name="bsc"> + <ref name="ctl_set"/> + <ref name="self_id"/> + <ref name="bsc_id"/> + <ref name="bsc_pkcs10"/> + </element> + </define> + <define name="bsc_query" combine="choice"> + <element name="bsc"> + <ref name="ctl_get"/> + <ref name="self_id"/> + <ref name="bsc_id"/> + </element> + </define> + <define name="bsc_reply" combine="choice"> + <element name="bsc"> + <ref name="ctl_get"/> + <ref name="self_id"/> + <ref name="bsc_id"/> + <ref name="bsc_payload"/> + <ref name="bsc_pkcs10"/> + </element> + </define> + <define name="bsc_query" combine="choice"> + <element name="bsc"> + <ref name="ctl_list"/> + <ref name="self_id"/> + </element> + </define> + <define name="bsc_reply" combine="choice"> + <element name="bsc"> + <ref name="ctl_list"/> + <ref name="self_id"/> + <ref name="bsc_id"/> + <ref name="bsc_payload"/> + <ref name="bsc_pkcs10"/> + </element> + </define> + <define name="bsc_query" combine="choice"> + <element name="bsc"> + <ref name="ctl_destroy"/> + <ref name="self_id"/> + <ref name="bsc_id"/> + </element> + </define> + <define name="bsc_reply" combine="choice"> + <element name="bsc"> + <ref name="ctl_destroy"/> + <ref name="self_id"/> + <ref name="bsc_id"/> + </element> + </define> + <!-- <parent/> element --> + <define name="parent_id"> + <attribute name="parent_id"> + <ref name="sql_id"/> + </attribute> + </define> + <define name="parent_bool"> + <optional> + <attribute name="rekey"> + <value>yes</value> + </attribute> + </optional> + <optional> + <attribute name="reissue"> + <value>yes</value> + </attribute> + </optional> + <optional> + <attribute name="revoke"> + <value>yes</value> + </attribute> + </optional> + </define> + <define name="parent_payload"> + <optional> + <attribute name="peer_contact_uri"> + <ref name="uri"/> + </attribute> + </optional> + <optional> + <attribute name="sia_base"> + <ref name="uri"/> + </attribute> + </optional> + <optional> + <ref name="bsc_id"/> + </optional> + <optional> + <ref name="repository_id"/> + </optional> + <optional> + <attribute name="sender_name"> + <ref name="up_down_name"/> + </attribute> + </optional> + <optional> + <attribute name="recipient_name"> + <ref name="up_down_name"/> + </attribute> + </optional> + <optional> + <element name="bpki_cms_cert"> + <ref name="base64"/> + </element> + </optional> + <optional> + <element name="bpki_cms_glue"> + <ref name="base64"/> + </element> + </optional> + <optional> + <element name="bpki_https_cert"> + <ref name="base64"/> + </element> + </optional> + <optional> + <element name="bpki_https_glue"> + <ref name="base64"/> + </element> + </optional> + </define> + <define name="parent_query" combine="choice"> + <element name="parent"> + <ref name="ctl_create"/> + <ref name="self_id"/> + <ref name="parent_bool"/> + <ref name="parent_payload"/> + </element> + </define> + <define name="parent_reply" combine="choice"> + <element name="parent"> + <ref name="ctl_create"/> + <ref name="self_id"/> + <ref name="parent_id"/> + </element> + </define> + <define name="parent_query" combine="choice"> + <element name="parent"> + <ref name="ctl_set"/> + <ref name="self_id"/> + <ref name="parent_id"/> + <ref name="parent_bool"/> + <ref name="parent_payload"/> + </element> + </define> + <define name="parent_reply" combine="choice"> + <element name="parent"> + <ref name="ctl_set"/> + <ref name="self_id"/> + <ref name="parent_id"/> + </element> + </define> + <define name="parent_query" combine="choice"> + <element name="parent"> + <ref name="ctl_get"/> + <ref name="self_id"/> + <ref name="parent_id"/> + </element> + </define> + <define name="parent_reply" combine="choice"> + <element name="parent"> + <ref name="ctl_get"/> + <ref name="self_id"/> + <ref name="parent_id"/> + <ref name="parent_payload"/> + </element> + </define> + <define name="parent_query" combine="choice"> + <element name="parent"> + <ref name="ctl_list"/> + <ref name="self_id"/> + </element> + </define> + <define name="parent_reply" combine="choice"> + <element name="parent"> + <ref name="ctl_list"/> + <ref name="self_id"/> + <ref name="parent_id"/> + <ref name="parent_payload"/> + </element> + </define> + <define name="parent_query" combine="choice"> + <element name="parent"> + <ref name="ctl_destroy"/> + <ref name="self_id"/> + <ref name="parent_id"/> + </element> + </define> + <define name="parent_reply" combine="choice"> + <element name="parent"> + <ref name="ctl_destroy"/> + <ref name="self_id"/> + <ref name="parent_id"/> + </element> + </define> + <!-- <child/> element --> + <define name="child_id"> + <attribute name="child_id"> + <ref name="sql_id"/> + </attribute> + </define> + <define name="child_bool"> + <optional> + <attribute name="reissue"> + <value>yes</value> + </attribute> + </optional> + </define> + <define name="child_payload"> + <optional> + <ref name="bsc_id"/> + </optional> + <optional> + <element name="bpki_cert"> + <ref name="base64"/> + </element> + </optional> + <optional> + <element name="bpki_glue"> + <ref name="base64"/> + </element> + </optional> + </define> + <define name="child_query" combine="choice"> + <element name="child"> + <ref name="ctl_create"/> + <ref name="self_id"/> + <ref name="child_bool"/> + <ref name="child_payload"/> + </element> + </define> + <define name="child_reply" combine="choice"> + <element name="child"> + <ref name="ctl_create"/> + <ref name="self_id"/> + <ref name="child_id"/> + </element> + </define> + <define name="child_query" combine="choice"> + <element name="child"> + <ref name="ctl_set"/> + <ref name="self_id"/> + <ref name="child_id"/> + <ref name="child_bool"/> + <ref name="child_payload"/> + </element> + </define> + <define name="child_reply" combine="choice"> + <element name="child"> + <ref name="ctl_set"/> + <ref name="self_id"/> + <ref name="child_id"/> + </element> + </define> + <define name="child_query" combine="choice"> + <element name="child"> + <ref name="ctl_get"/> + <ref name="self_id"/> + <ref name="child_id"/> + </element> + </define> + <define name="child_reply" combine="choice"> + <element name="child"> + <ref name="ctl_get"/> + <ref name="self_id"/> + <ref name="child_id"/> + <ref name="child_payload"/> + </element> + </define> + <define name="child_query" combine="choice"> + <element name="child"> + <ref name="ctl_list"/> + <ref name="self_id"/> + </element> + </define> + <define name="child_reply" combine="choice"> + <element name="child"> + <ref name="ctl_list"/> + <ref name="self_id"/> + <ref name="child_id"/> + <ref name="child_payload"/> + </element> + </define> + <define name="child_query" combine="choice"> + <element name="child"> + <ref name="ctl_destroy"/> + <ref name="self_id"/> + <ref name="child_id"/> + </element> + </define> + <define name="child_reply" combine="choice"> + <element name="child"> + <ref name="ctl_destroy"/> + <ref name="self_id"/> + <ref name="child_id"/> + </element> + </define> + <!-- <repository/> element --> + <define name="repository_id"> + <attribute name="repository_id"> + <ref name="sql_id"/> + </attribute> + </define> + <define name="repository_payload"> + <optional> + <attribute name="peer_contact_uri"> + <ref name="uri"/> + </attribute> + </optional> + <optional> + <ref name="bsc_id"/> + </optional> + <optional> + <element name="bpki_cms_cert"> + <ref name="base64"/> + </element> + </optional> + <optional> + <element name="bpki_cms_glue"> + <ref name="base64"/> + </element> + </optional> + <optional> + <element name="bpki_https_cert"> + <ref name="base64"/> + </element> + </optional> + <optional> + <element name="bpki_https_glue"> + <ref name="base64"/> + </element> + </optional> + </define> + <define name="repository_query" combine="choice"> + <element name="repository"> + <ref name="ctl_create"/> + <ref name="self_id"/> + <ref name="repository_payload"/> + </element> + </define> + <define name="repository_reply" combine="choice"> + <element name="repository"> + <ref name="ctl_create"/> + <ref name="self_id"/> + <ref name="repository_id"/> + </element> + </define> + <define name="repository_query" combine="choice"> + <element name="repository"> + <ref name="ctl_set"/> + <ref name="self_id"/> + <ref name="repository_id"/> + <ref name="repository_payload"/> + </element> + </define> + <define name="repository_reply" combine="choice"> + <element name="repository"> + <ref name="ctl_set"/> + <ref name="self_id"/> + <ref name="repository_id"/> + </element> + </define> + <define name="repository_query" combine="choice"> + <element name="repository"> + <ref name="ctl_get"/> + <ref name="self_id"/> + <ref name="repository_id"/> + </element> + </define> + <define name="repository_reply" combine="choice"> + <element name="repository"> + <ref name="ctl_get"/> + <ref name="self_id"/> + <ref name="repository_id"/> + <ref name="repository_payload"/> + </element> + </define> + <define name="repository_query" combine="choice"> + <element name="repository"> + <ref name="ctl_list"/> + <ref name="self_id"/> + </element> + </define> + <define name="repository_reply" combine="choice"> + <element name="repository"> + <ref name="ctl_list"/> + <ref name="self_id"/> + <ref name="repository_id"/> + <ref name="repository_payload"/> + </element> + </define> + <define name="repository_query" combine="choice"> + <element name="repository"> + <ref name="ctl_destroy"/> + <ref name="self_id"/> + <ref name="repository_id"/> + </element> + </define> + <define name="repository_reply" combine="choice"> + <element name="repository"> + <ref name="ctl_destroy"/> + <ref name="self_id"/> + <ref name="repository_id"/> + </element> + </define> + <!-- <route_origin/> element --> + <define name="route_origin_id"> + <attribute name="route_origin_id"> + <ref name="sql_id"/> + </attribute> + </define> + <define name="route_origin_bool"> + <optional> + <attribute name="suppress_publication"> + <value>yes</value> + </attribute> + </optional> + </define> + <define name="route_origin_payload"> + <optional> + <attribute name="as_number"> + <data type="positiveInteger"/> + </attribute> + </optional> + <optional> + <attribute name="ipv4"> + <ref name="ipv4_list"/> + </attribute> + </optional> + <optional> + <attribute name="ipv6"> + <ref name="ipv6_list"/> + </attribute> + </optional> + </define> + <define name="route_origin_query" combine="choice"> + <element name="route_origin"> + <ref name="ctl_create"/> + <ref name="self_id"/> + <ref name="route_origin_bool"/> + <ref name="route_origin_payload"/> + </element> + </define> + <define name="route_origin_reply" combine="choice"> + <element name="route_origin"> + <ref name="ctl_create"/> + <ref name="self_id"/> + <ref name="route_origin_id"/> + </element> + </define> + <define name="route_origin_query" combine="choice"> + <element name="route_origin"> + <ref name="ctl_set"/> + <ref name="self_id"/> + <ref name="route_origin_id"/> + <ref name="route_origin_bool"/> + <ref name="route_origin_payload"/> + </element> + </define> + <define name="route_origin_reply" combine="choice"> + <element name="route_origin"> + <ref name="ctl_set"/> + <ref name="self_id"/> + <ref name="route_origin_id"/> + </element> + </define> + <define name="route_origin_query" combine="choice"> + <element name="route_origin"> + <ref name="ctl_get"/> + <ref name="self_id"/> + <ref name="route_origin_id"/> + </element> + </define> + <define name="route_origin_reply" combine="choice"> + <element name="route_origin"> + <ref name="ctl_get"/> + <ref name="self_id"/> + <ref name="route_origin_id"/> + <ref name="route_origin_payload"/> + </element> + </define> + <define name="route_origin_query" combine="choice"> + <element name="route_origin"> + <ref name="ctl_list"/> + <ref name="self_id"/> + </element> + </define> + <define name="route_origin_reply" combine="choice"> + <element name="route_origin"> + <ref name="ctl_list"/> + <ref name="self_id"/> + <ref name="route_origin_id"/> + <ref name="route_origin_payload"/> + </element> + </define> + <define name="route_origin_query" combine="choice"> + <element name="route_origin"> + <ref name="ctl_destroy"/> + <ref name="self_id"/> + <ref name="route_origin_id"/> + </element> + </define> + <define name="route_origin_reply" combine="choice"> + <element name="route_origin"> + <ref name="ctl_destroy"/> + <ref name="self_id"/> + <ref name="route_origin_id"/> + </element> + </define> + <!-- <list_resources/> element --> + <define name="list_resources_query"> + <element name="list_resources"> + <ref name="tag"/> + <ref name="self_id"/> + <ref name="child_id"/> + </element> + </define> + <define name="list_resources_reply"> + <element name="list_resources"> + <ref name="tag"/> + <ref name="self_id"/> + <ref name="child_id"/> + <attribute name="valid_until"> + <data type="dateTime"> + <param name="pattern">.*Z</param> + </data> + </attribute> + <optional> + <attribute name="subject_name"> + <data type="token"> + <param name="maxLength">1024</param> + </data> + </attribute> + </optional> + <optional> + <attribute name="asn"> + <ref name="asn_list"/> + </attribute> + </optional> + <optional> + <attribute name="ipv4"> + <ref name="ipv4_list"/> + </attribute> + </optional> + <optional> + <attribute name="ipv6"> + <ref name="ipv6_list"/> + </attribute> + </optional> + </element> + </define> + <!-- <report_error/> element --> + <define name="error"> + <data type="token"> + <param name="maxLength">1024</param> + </data> + </define> + <define name="report_error_reply"> + <element name="report_error"> + <ref name="tag"/> + <ref name="self_id"/> + <attribute name="error_code"> + <ref name="error"/> + </attribute> + <optional> + <data type="string"> + <param name="maxLength">512000</param> + </data> + </optional> + </element> + </define> +</grammar> +<!-- + Local Variables: + indent-tabs-mode: nil + End: +--> +''')) + +## @var up_down +## Parsed RelaxNG up_down schema +up_down = lxml.etree.RelaxNG(lxml.etree.fromstring('''<?xml version="1.0" encoding="UTF-8"?> +<!-- + $Id: up-down-schema.rnc 1798 2008-05-17 08:21:50Z sra $ + + RelaxNG Scheme for up-down protocol, extracted from APNIC Wiki. + + libxml2 (including xmllint) only groks the XML syntax of RelaxNG, so + run the compact syntax through trang to get XML syntax. +--> +<grammar ns="http://www.apnic.net/specs/rescerts/up-down/" xmlns="http://relaxng.org/ns/structure/1.0" datatypeLibrary="http://www.w3.org/2001/XMLSchema-datatypes"> + <start> + <element name="message"> + <attribute name="version"> + <data type="positiveInteger"> + <param name="maxInclusive">1</param> + </data> + </attribute> + <attribute name="sender"> + <data type="token"> + <param name="maxLength">1024</param> + </data> + </attribute> + <attribute name="recipient"> + <data type="token"> + <param name="maxLength">1024</param> + </data> + </attribute> + <ref name="payload"/> + </element> + </start> + <define name="payload" combine="choice"> + <attribute name="type"> + <value>list</value> + </attribute> + <ref name="list_request"/> + </define> + <define name="payload" combine="choice"> + <attribute name="type"> + <value>list_response</value> + </attribute> + <ref name="list_response"/> + </define> + <define name="payload" combine="choice"> + <attribute name="type"> + <value>issue</value> + </attribute> + <ref name="issue_request"/> + </define> + <define name="payload" combine="choice"> + <attribute name="type"> + <value>issue_response</value> + </attribute> + <ref name="issue_response"/> + </define> + <define name="payload" combine="choice"> + <attribute name="type"> + <value>revoke</value> + </attribute> + <ref name="revoke_request"/> + </define> + <define name="payload" combine="choice"> + <attribute name="type"> + <value>revoke_response</value> + </attribute> + <ref name="revoke_response"/> + </define> + <define name="payload" combine="choice"> + <attribute name="type"> + <value>error_response</value> + </attribute> + <ref name="error_response"/> + </define> + <define name="list_request"> + <empty/> + </define> + <define name="list_response"> + <zeroOrMore> + <ref name="class"/> + </zeroOrMore> + </define> + <define name="class"> + <element name="class"> + <attribute name="class_name"> + <data type="token"> + <param name="maxLength">1024</param> + </data> + </attribute> + <attribute name="cert_url"> + <data type="string"> + <param name="maxLength">4096</param> + </data> + </attribute> + <attribute name="resource_set_as"> + <data type="string"> + <param name="maxLength">512000</param> + <param name="pattern">[\-,0-9]*</param> + </data> + </attribute> + <attribute name="resource_set_ipv4"> + <data type="string"> + <param name="maxLength">512000</param> + <param name="pattern">[\-,/.0-9]*</param> + </data> + </attribute> + <attribute name="resource_set_ipv6"> + <data type="string"> + <param name="maxLength">512000</param> + <param name="pattern">[\-,/:0-9a-fA-F]*</param> + </data> + </attribute> + <optional> + <attribute name="resource_set_notafter"> + <data type="dateTime"> + <param name="pattern">.*Z</param> + </data> + </attribute> + </optional> + <optional> + <attribute name="suggested_sia_head"> + <data type="anyURI"> + <param name="maxLength">1024</param> + <param name="pattern">rsync://.+</param> + </data> + </attribute> + </optional> + <zeroOrMore> + <element name="certificate"> + <attribute name="cert_url"> + <data type="string"> + <param name="maxLength">4096</param> + </data> + </attribute> + <optional> + <attribute name="req_resource_set_as"> + <data type="string"> + <param name="maxLength">512000</param> + <param name="pattern">[\-,0-9]*</param> + </data> + </attribute> + </optional> + <optional> + <attribute name="req_resource_set_ipv4"> + <data type="string"> + <param name="maxLength">512000</param> + <param name="pattern">[\-,/.0-9]*</param> + </data> + </attribute> + </optional> + <optional> + <attribute name="req_resource_set_ipv6"> + <data type="string"> + <param name="maxLength">512000</param> + <param name="pattern">[\-,/:0-9a-fA-F]*</param> + </data> + </attribute> + </optional> + <data type="base64Binary"> + <param name="maxLength">512000</param> + </data> + </element> + </zeroOrMore> + <element name="issuer"> + <data type="base64Binary"> + <param name="maxLength">512000</param> + </data> + </element> + </element> + </define> + <define name="issue_request"> + <element name="request"> + <attribute name="class_name"> + <data type="token"> + <param name="maxLength">1024</param> + </data> + </attribute> + <optional> + <attribute name="req_resource_set_as"> + <data type="string"> + <param name="maxLength">512000</param> + <param name="pattern">[\-,0-9]*</param> + </data> + </attribute> + </optional> + <optional> + <attribute name="req_resource_set_ipv4"> + <data type="string"> + <param name="maxLength">512000</param> + <param name="pattern">[\-,/.0-9]*</param> + </data> + </attribute> + </optional> + <optional> + <attribute name="req_resource_set_ipv6"> + <data type="string"> + <param name="maxLength">512000</param> + <param name="pattern">[\-,/:0-9a-fA-F]*</param> + </data> + </attribute> + </optional> + <data type="base64Binary"> + <param name="maxLength">512000</param> + </data> + </element> + </define> + <define name="issue_response"> + <ref name="class"/> + </define> + <define name="revoke_request"> + <ref name="revocation"/> + </define> + <define name="revoke_response"> + <ref name="revocation"/> + </define> + <define name="revocation"> + <element name="key"> + <attribute name="class_name"> + <data type="token"> + <param name="maxLength">1024</param> + </data> + </attribute> + <attribute name="ski"> + <data type="token"> + <param name="maxLength">1024</param> + </data> + </attribute> + </element> + </define> + <define name="error_response"> + <element name="status"> + <data type="positiveInteger"> + <param name="maxInclusive">999999999999999</param> + </data> + </element> + <optional> + <element name="description"> + <attribute name="xml:lang"> + <data type="language"/> + </attribute> + <data type="string"> + <param name="maxLength">1024</param> + </data> + </element> + </optional> + </define> +</grammar> +<!-- + Local Variables: + indent-tabs-mode: nil + End: +--> +''')) + +## @var publication +## Parsed RelaxNG publication schema +publication = lxml.etree.RelaxNG(lxml.etree.fromstring('''<?xml version="1.0" encoding="UTF-8"?> +<!-- + $Id: publication-schema.rnc 1837 2008-06-02 23:47:19Z sra $ + + RelaxNG Schema for RPKI publication protocol. + + libxml2 (including xmllint) only groks the XML syntax of RelaxNG, so + run the compact syntax through trang to get XML syntax. +--> +<grammar ns="http://www.hactrn.net/uris/rpki/publication-spec/" xmlns="http://relaxng.org/ns/structure/1.0" datatypeLibrary="http://www.w3.org/2001/XMLSchema-datatypes"> + <!-- Top level PDU --> + <start> + <element name="msg"> + <attribute name="version"> + <data type="positiveInteger"> + <param name="maxInclusive">1</param> + </data> + </attribute> + <choice> + <group> + <attribute name="type"> + <value>query</value> + </attribute> + <zeroOrMore> + <ref name="query_elt"/> + </zeroOrMore> + </group> + <group> + <attribute name="type"> + <value>reply</value> + </attribute> + <zeroOrMore> + <ref name="reply_elt"/> + </zeroOrMore> + </group> + </choice> + </element> + </start> + <!-- PDUs allowed in a query --> + <define name="query_elt"> + <choice> + <ref name="config_query"/> + <ref name="client_query"/> + <ref name="certificate_query"/> + <ref name="crl_query"/> + <ref name="manifest_query"/> + <ref name="roa_query"/> + </choice> + </define> + <!-- PDUs allowed in a reply --> + <define name="reply_elt"> + <choice> + <ref name="config_reply"/> + <ref name="client_reply"/> + <ref name="certificate_reply"/> + <ref name="crl_reply"/> + <ref name="manifest_reply"/> + <ref name="roa_reply"/> + <ref name="report_error_reply"/> + </choice> + </define> + <!-- Tag attributes for bulk operations --> + <define name="tag"> + <attribute name="tag"> + <data type="token"> + <param name="maxLength">1024</param> + </data> + </attribute> + </define> + <!-- Base64 encoded DER stuff --> + <define name="base64"> + <data type="base64Binary"> + <param name="maxLength">512000</param> + </data> + </define> + <!-- Publication URLs --> + <define name="uri_t"> + <data type="anyURI"> + <param name="maxLength">4096</param> + </data> + </define> + <define name="uri"> + <attribute name="uri"> + <ref name="uri_t"/> + </attribute> + </define> + <!-- + <config/> element (use restricted to repository operator) + config_id attribute, create, list, and destroy commands omitted deliberately, see code for details + --> + <define name="config_payload"> + <optional> + <element name="bpki_crl"> + <ref name="base64"/> + </element> + </optional> + </define> + <define name="config_query" combine="choice"> + <element name="config"> + <attribute name="action"> + <value>set</value> + </attribute> + <optional> + <ref name="tag"/> + </optional> + <ref name="config_payload"/> + </element> + </define> + <define name="config_reply" combine="choice"> + <element name="config"> + <attribute name="action"> + <value>set</value> + </attribute> + <optional> + <ref name="tag"/> + </optional> + </element> + </define> + <define name="config_query" combine="choice"> + <element name="config"> + <attribute name="action"> + <value>get</value> + </attribute> + <optional> + <ref name="tag"/> + </optional> + </element> + </define> + <define name="config_reply" combine="choice"> + <element name="config"> + <attribute name="action"> + <value>get</value> + </attribute> + <optional> + <ref name="tag"/> + </optional> + <ref name="config_payload"/> + </element> + </define> + <!-- <client/> element (use restricted to repository operator) --> + <define name="client_id"> + <attribute name="client_id"> + <data type="nonNegativeInteger"/> + </attribute> + </define> + <define name="client_payload"> + <optional> + <attribute name="base_uri"> + <ref name="uri_t"/> + </attribute> + </optional> + <optional> + <element name="bpki_cert"> + <ref name="base64"/> + </element> + </optional> + <optional> + <element name="bpki_glue"> + <ref name="base64"/> + </element> + </optional> + </define> + <define name="client_query" combine="choice"> + <element name="client"> + <attribute name="action"> + <value>create</value> + </attribute> + <optional> + <ref name="tag"/> + </optional> + <ref name="client_payload"/> + </element> + </define> + <define name="client_reply" combine="choice"> + <element name="client"> + <attribute name="action"> + <value>create</value> + </attribute> + <optional> + <ref name="tag"/> + </optional> + <ref name="client_id"/> + </element> + </define> + <define name="client_query" combine="choice"> + <element name="client"> + <attribute name="action"> + <value>set</value> + </attribute> + <optional> + <ref name="tag"/> + </optional> + <ref name="client_id"/> + <ref name="client_payload"/> + </element> + </define> + <define name="client_reply" combine="choice"> + <element name="client"> + <attribute name="action"> + <value>set</value> + </attribute> + <optional> + <ref name="tag"/> + </optional> + <ref name="client_id"/> + </element> + </define> + <define name="client_query" combine="choice"> + <element name="client"> + <attribute name="action"> + <value>get</value> + </attribute> + <optional> + <ref name="tag"/> + </optional> + <ref name="client_id"/> + </element> + </define> + <define name="client_reply" combine="choice"> + <element name="client"> + <attribute name="action"> + <value>get</value> + </attribute> + <optional> + <ref name="tag"/> + </optional> + <ref name="client_id"/> + <ref name="client_payload"/> + </element> + </define> + <define name="client_query" combine="choice"> + <element name="client"> + <attribute name="action"> + <value>list</value> + </attribute> + <optional> + <ref name="tag"/> + </optional> + </element> + </define> + <define name="client_reply" combine="choice"> + <element name="client"> + <attribute name="action"> + <value>list</value> + </attribute> + <optional> + <ref name="tag"/> + </optional> + <ref name="client_id"/> + <ref name="client_payload"/> + </element> + </define> + <define name="client_query" combine="choice"> + <element name="client"> + <attribute name="action"> + <value>destroy</value> + </attribute> + <optional> + <ref name="tag"/> + </optional> + <ref name="client_id"/> + </element> + </define> + <define name="client_reply" combine="choice"> + <element name="client"> + <attribute name="action"> + <value>destroy</value> + </attribute> + <optional> + <ref name="tag"/> + </optional> + <ref name="client_id"/> + </element> + </define> + <!-- <certificate/> element --> + <define name="certificate_query" combine="choice"> + <element name="certificate"> + <attribute name="action"> + <value>publish</value> + </attribute> + <optional> + <ref name="tag"/> + </optional> + <ref name="uri"/> + <ref name="base64"/> + </element> + </define> + <define name="certificate_reply" combine="choice"> + <element name="certificate"> + <attribute name="action"> + <value>publish</value> + </attribute> + <optional> + <ref name="tag"/> + </optional> + <ref name="uri"/> + </element> + </define> + <define name="certificate_query" combine="choice"> + <element name="certificate"> + <attribute name="action"> + <value>withdraw</value> + </attribute> + <optional> + <ref name="tag"/> + </optional> + <ref name="uri"/> + </element> + </define> + <define name="certificate_reply" combine="choice"> + <element name="certificate"> + <attribute name="action"> + <value>withdraw</value> + </attribute> + <optional> + <ref name="tag"/> + </optional> + <ref name="uri"/> + </element> + </define> + <!-- <crl/> element --> + <define name="crl_query" combine="choice"> + <element name="crl"> + <attribute name="action"> + <value>publish</value> + </attribute> + <optional> + <ref name="tag"/> + </optional> + <ref name="uri"/> + <ref name="base64"/> + </element> + </define> + <define name="crl_reply" combine="choice"> + <element name="crl"> + <attribute name="action"> + <value>publish</value> + </attribute> + <optional> + <ref name="tag"/> + </optional> + <ref name="uri"/> + </element> + </define> + <define name="crl_query" combine="choice"> + <element name="crl"> + <attribute name="action"> + <value>withdraw</value> + </attribute> + <optional> + <ref name="tag"/> + </optional> + <ref name="uri"/> + </element> + </define> + <define name="crl_reply" combine="choice"> + <element name="crl"> + <attribute name="action"> + <value>withdraw</value> + </attribute> + <optional> + <ref name="tag"/> + </optional> + <ref name="uri"/> + </element> + </define> + <!-- <manifest/> element --> + <define name="manifest_query" combine="choice"> + <element name="manifest"> + <attribute name="action"> + <value>publish</value> + </attribute> + <optional> + <ref name="tag"/> + </optional> + <ref name="uri"/> + <ref name="base64"/> + </element> + </define> + <define name="manifest_reply" combine="choice"> + <element name="manifest"> + <attribute name="action"> + <value>publish</value> + </attribute> + <optional> + <ref name="tag"/> + </optional> + <ref name="uri"/> + </element> + </define> + <define name="manifest_query" combine="choice"> + <element name="manifest"> + <attribute name="action"> + <value>withdraw</value> + </attribute> + <optional> + <ref name="tag"/> + </optional> + <ref name="uri"/> + </element> + </define> + <define name="manifest_reply" combine="choice"> + <element name="manifest"> + <attribute name="action"> + <value>withdraw</value> + </attribute> + <optional> + <ref name="tag"/> + </optional> + <ref name="uri"/> + </element> + </define> + <!-- <roa/> element --> + <define name="roa_query" combine="choice"> + <element name="roa"> + <attribute name="action"> + <value>publish</value> + </attribute> + <optional> + <ref name="tag"/> + </optional> + <ref name="uri"/> + <ref name="base64"/> + </element> + </define> + <define name="roa_reply" combine="choice"> + <element name="roa"> + <attribute name="action"> + <value>publish</value> + </attribute> + <optional> + <ref name="tag"/> + </optional> + <ref name="uri"/> + </element> + </define> + <define name="roa_query" combine="choice"> + <element name="roa"> + <attribute name="action"> + <value>withdraw</value> + </attribute> + <optional> + <ref name="tag"/> + </optional> + <ref name="uri"/> + </element> + </define> + <define name="roa_reply" combine="choice"> + <element name="roa"> + <attribute name="action"> + <value>withdraw</value> + </attribute> + <optional> + <ref name="tag"/> + </optional> + <ref name="uri"/> + </element> + </define> + <!-- <report_error/> element --> + <define name="error"> + <data type="token"> + <param name="maxLength">1024</param> + </data> + </define> + <define name="report_error_reply"> + <element name="report_error"> + <optional> + <ref name="tag"/> + </optional> + <attribute name="error_code"> + <ref name="error"/> + </attribute> + <optional> + <data type="string"> + <param name="maxLength">512000</param> + </data> + </optional> + </element> + </define> +</grammar> +<!-- + Local Variables: + indent-tabs-mode: nil + End: +--> +''')) diff --git a/rpkid.stable/rpki/resource_set.py b/rpkid.stable/rpki/resource_set.py new file mode 100644 index 00000000..baaccfc4 --- /dev/null +++ b/rpkid.stable/rpki/resource_set.py @@ -0,0 +1,795 @@ +"""Classes dealing with sets of resources. + +The basic mechanics of a resource set are the same for any of the +resources we handle (ASNs, IPv4 addresses, or IPv6 addresses), so we +can provide the same operations on any of them, even though the +underlying details vary. + +We also provide some basic set operations (union, intersection, etc). + +$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 re +import rpki.ipaddrs, rpki.oids, rpki.exceptions + +## @var inherit_token +# Token used to indicate inheritance in read and print syntax. + +inherit_token = "<inherit>" + +class resource_range(object): + """Generic resource range type. Assumes underlying type is some + kind of integer. + + This is a virtual class. You probably don't want to use this type + directly. + """ + + def __init__(self, min, max): + """Initialize and sanity check a resource_range.""" + assert min <= max, "Mis-ordered range: %s before %s" % (str(min), str(max)) + self.min = min + self.max = max + + def __cmp__(self, other): + """Compare two resource_range objects.""" + assert self.__class__ is other.__class__ + c = self.min - other.min + if c == 0: c = self.max - other.max + if c < 0: c = -1 + if c > 0: c = 1 + return c + +class resource_range_as(resource_range): + """Range of Autonomous System Numbers. + + Denotes a single ASN by a range whose min and max values are identical. + """ + + ## @var datum_type + # Type of underlying data (min and max). + + datum_type = long + + def __str__(self): + """Convert a resource_range_as to string format.""" + if self.min == self.max: + return str(self.min) + else: + return str(self.min) + "-" + str(self.max) + + def to_rfc3779_tuple(self): + """Convert a resource_range_as to tuple format for RFC 3779 ASN.1 encoding.""" + if self.min == self.max: + return ("id", self.min) + else: + return ("range", (self.min, self.max)) + +class resource_range_ip(resource_range): + """Range of (generic) IP addresses. + + Prefixes are converted to ranges on input, and ranges that can be + represented as prefixes are written as prefixes on output. + + This is a virtual class. You probably don't want to use it directly. + """ + + def _prefixlen(self): + """Determine whether a resource_range_ip can be expressed as a prefix.""" + mask = self.min ^ self.max + if self.min & mask != 0: + return -1 + prefixlen = self.datum_type.bits + while mask & 1: + prefixlen -= 1 + mask >>= 1 + if mask: + return -1 + else: + return prefixlen + + def __str__(self): + """Convert a resource_range_ip to string format.""" + prefixlen = self._prefixlen() + if prefixlen < 0: + return str(self.min) + "-" + str(self.max) + else: + return str(self.min) + "/" + str(prefixlen) + + def to_rfc3779_tuple(self): + """Convert a resource_range_ip to tuple format for RFC 3779 ASN.1 encoding.""" + prefixlen = self._prefixlen() + if prefixlen < 0: + return ("addressRange", (_long2bs(self.min, self.datum_type.bits, strip = 0), + _long2bs(self.max, self.datum_type.bits, strip = 1))) + else: + return ("addressPrefix", _long2bs(self.min, self.datum_type.bits, prefixlen = prefixlen)) + + @classmethod + def make_prefix(cls, address, prefixlen): + """Construct a resource range corresponding to a prefix.""" + assert isinstance(address, cls.datum_type) and isinstance(prefixlen, (int, long)) + assert prefixlen >= 0 and prefixlen <= cls.datum_type.bits, "Nonsensical prefix length: %s" % prefixlen + mask = (1 << (cls.datum_type.bits - prefixlen)) - 1 + assert (address & mask) == 0, "Resource not in canonical form: %s/%s" % (address, prefixlen) + return cls(cls.datum_type(address), cls.datum_type(address | mask)) + +class resource_range_ipv4(resource_range_ip): + """Range of IPv4 addresses.""" + + ## @var datum_type + # Type of underlying data (min and max). + + datum_type = rpki.ipaddrs.v4addr + +class resource_range_ipv6(resource_range_ip): + """Range of IPv6 addresses.""" + + ## @var datum_type + # Type of underlying data (min and max). + + datum_type = rpki.ipaddrs.v6addr + +def _rsplit(rset, that): + """Utility function to split a resource range into two resource ranges.""" + this = rset.pop(0) + cell_type = type(this.min) + assert type(this) is type(that) and type(this.max) is cell_type and \ + type(that.min) is cell_type and type(that.max) is cell_type + if this.min < that.min: + rset.insert(0, type(this)(this.min, cell_type(that.min - 1))) + rset.insert(1, type(this)(that.min, this.max)) + else: + assert this.max > that.max + rset.insert(0, type(this)(this.min, that.max)) + rset.insert(1, type(this)(cell_type(that.max + 1), this.max)) + +class resource_set(list): + """Generic resource set. + This is a list subclass containing resource ranges. + + This is a virtual class. You probably don't want to use it + directly. + """ + + ## @var inherit + # Boolean indicating whether this resource_set uses RFC 3779 inheritance. + + inherit = False + + def __init__(self, ini = None): + """Initialize a resource_set.""" + if isinstance(ini, (int, long)): + ini = str(ini) + if ini == inherit_token: + self.inherit = True + elif isinstance(ini, str) and len(ini): + self.extend(self.parse_str(s) for s in ini.split(",")) + elif isinstance(ini, tuple): + self.parse_rfc3779_tuple(ini) + elif isinstance(ini, list): + self.extend(ini) + else: + assert ini is None or ini == "", "Unexpected initializer: %s" % str(ini) + assert not self.inherit or not self + self.sort() + for i in xrange(len(self) - 2, -1, -1): + if self[i].max + 1 == self[i+1].min: + self[i] = type(self[i])(self[i].min, self[i+1].max) + self.pop(i + 1) + if __debug__: + for i in xrange(0, len(self) - 1): + assert self[i].max < self[i+1].min, "Resource overlap: %s %s" % (self[i], self[i+1]) + + def __str__(self): + """Convert a resource_set to string format.""" + if self.inherit: + return inherit_token + else: + return ",".join(str(x) for x in self) + + def _comm(self, other): + """Like comm(1), sort of. + + Returns a tuple of three resource sets: resources only in self, + resources only in other, and resources in both. Used (not very + efficiently) as the basis for most set operations on resource + sets. + """ + assert not self.inherit + assert type(self) is type(other), "Type mismatch %s %s" % (repr(type(self)), repr(type(other))) + set1 = self[:] + set2 = other[:] + only1, only2, both = [], [], [] + while set1 or set2: + if set1 and (not set2 or set1[0].max < set2[0].min): + only1.append(set1.pop(0)) + elif set2 and (not set1 or set2[0].max < set1[0].min): + only2.append(set2.pop(0)) + elif set1[0].min < set2[0].min: + _rsplit(set1, set2[0]) + elif set2[0].min < set1[0].min: + _rsplit(set2, set1[0]) + elif set1[0].max < set2[0].max: + _rsplit(set2, set1[0]) + elif set2[0].max < set1[0].max: + _rsplit(set1, set2[0]) + else: + assert set1[0].min == set2[0].min and set1[0].max == set2[0].max + both.append(set1.pop(0)) + set2.pop(0) + return type(self)(only1), type(self)(only2), type(self)(both) + + def union(self, other): + """Set union for resource sets.""" + assert not self.inherit + assert type(self) is type(other), "Type mismatch: %s %s" % (repr(type(self)), repr(type(other))) + set1 = self[:] + set2 = other[:] + result = [] + while set1 or set2: + if set1 and (not set2 or set1[0].max < set2[0].min): + result.append(set1.pop(0)) + elif set2 and (not set1 or set2[0].max < set1[0].min): + result.append(set2.pop(0)) + else: + this = set1.pop(0) + that = set2.pop(0) + assert type(this) is type(that) + if this.min < that.min: min = this.min + else: min = that.min + if this.max > that.max: max = this.max + else: max = that.max + result.append(type(this)(min, max)) + return type(self)(result) + + def intersection(self, other): + """Set intersection for resource sets.""" + return self._comm(other)[2] + + def difference(self, other): + """Set difference for resource sets.""" + return self._comm(other)[0] + + def symmetric_difference(self, other): + """Set symmetric difference (XOR) for resource sets.""" + com = self._comm(other) + return com[0].union(com[1]) + + def contains(self, item): + """Set membership test for resource sets.""" + assert not self.inherit + for i in self: + if isinstance(item, type(i)) and i.min <= item.min and i.max >= item.max: + return True + elif isinstance(item, type(i.min)) and i.min <= item and i.max >= item: + return True + else: + assert isinstance(item, (type(i), type(i.min))) + return False + + def issubset(self, other): + """Test whether self is a subset (possibly improper) of other.""" + for i in self: + if not other.contains(i): + return False + return True + + def issuperset(self, other): + """Test whether self is a superset (possibly improper) of other.""" + return other.issubset(self) + + @classmethod + def from_sql(cls, sql, query, args = None): + """Create resource set from an SQL query. + + sql is an object that supports execute() and fetchall() methods + like a DB API 2.0 cursor object. + + query is an SQL query that returns a sequence of (min, max) pairs. + """ + + sql.execute(query, args) + return cls(ini = [cls.range_type(cls.range_type.datum_type(b), + cls.range_type.datum_type(e)) + for (b,e) in sql.fetchall()]) + +class resource_set_as(resource_set): + """ASN resource set.""" + + ## @var range_type + # Type of range underlying this type of resource_set. + + range_type = resource_range_as + + def parse_str(self, x): + """Parse ASN resource sets from text (eg, XML attributes).""" + r = re.match("^([0-9]+)-([0-9]+)$", x) + if r: + return resource_range_as(long(r.group(1)), long(r.group(2))) + else: + return resource_range_as(long(x), long(x)) + + def parse_rfc3779_tuple(self, x): + """Parse ASN resource from tuple format generated by RFC 3779 ASN.1 decoder.""" + if x[0] == "asIdsOrRanges": + for aor in x[1]: + if aor[0] == "range": + min = aor[1][0] + max = aor[1][1] + else: + min = aor[1] + max = min + self.append(resource_range_as(min, max)) + else: + assert x[0] == "inherit" + self.inherit = True + + def to_rfc3779_tuple(self): + """Convert ASN resource set into tuple format used for RFC 3779 ASN.1 encoding.""" + if self: + return ("asIdsOrRanges", tuple(a.to_rfc3779_tuple() for a in self)) + elif self.inherit: + return ("inherit", "") + else: + return None + +class resource_set_ip(resource_set): + """(Generic) IP address resource set. + + This is a virtual class. You probably don't want to use it + directly. + """ + + def parse_str(self, x): + """Parse IP address resource sets from text (eg, XML attributes).""" + r = re.match("^([0-9:.a-fA-F]+)-([0-9:.a-fA-F]+)$", x) + if r: + return self.range_type(self.range_type.datum_type(r.group(1)), self.range_type.datum_type(r.group(2))) + r = re.match("^([0-9:.a-fA-F]+)/([0-9]+)$", x) + if r: + return self.range_type.make_prefix(self.range_type.datum_type(r.group(1)), int(r.group(2))) + raise RuntimeError, 'Bad IP resource "%s"' % (x) + + def parse_rfc3779_tuple(self, x): + """Parse IP address resource sets from tuple format generated by RFC 3779 ASN.1 decoder.""" + if x[0] == "addressesOrRanges": + for aor in x[1]: + if aor[0] == "addressRange": + min = _bs2long(aor[1][0], self.range_type.datum_type.bits, 0) + max = _bs2long(aor[1][1], self.range_type.datum_type.bits, 1) + else: + min = _bs2long(aor[1], self.range_type.datum_type.bits, 0) + max = _bs2long(aor[1], self.range_type.datum_type.bits, 1) + self.append(self.range_type(self.range_type.datum_type(min), self.range_type.datum_type(max))) + else: + assert x[0] == "inherit" + self.inherit = True + + def to_rfc3779_tuple(self): + """Convert IP resource set into tuple format used by RFC 3779 ASN.1 encoder.""" + if self: + return (self.afi, ("addressesOrRanges", tuple(a.to_rfc3779_tuple() for a in self))) + elif self.inherit: + return (self.afi, ("inherit", "")) + else: + return None + +class resource_set_ipv4(resource_set_ip): + """IPv4 address resource set.""" + + ## @var range_type + # Type of range underlying this type of resource_set. + + range_type = resource_range_ipv4 + + ## @var afi + # Address Family Identifier value for IPv4. + + afi = "\x00\x01" + +class resource_set_ipv6(resource_set_ip): + """IPv6 address resource set.""" + + ## @var range_type + # Type of range underlying this type of resource_set. + + range_type = resource_range_ipv6 + + ## @var afi + # Address Family Identifier value for IPv6. + + afi = "\x00\x02" + +def _bs2long(bs, addrlen, fill): + """Utility function to convert a bitstring (POW.pkix tuple + representation) into a Python long. + """ + x = 0L + for y in bs: + x = (x << 1) | y + for y in xrange(addrlen - len(bs)): + x = (x << 1) | fill + return x + +def _long2bs(number, addrlen, prefixlen = None, strip = None): + """Utility function to convert a Python long into a POW.pkix tuple + bitstring. This is a bit complicated because it supports the + fiendishly compact encoding used in RFC 3779. + """ + assert prefixlen is None or strip is None + bs = [] + while number: + bs.append(int(number & 1)) + number >>= 1 + if addrlen > len(bs): + bs.extend((0 for i in xrange(addrlen - len(bs)))) + bs.reverse() + if prefixlen is not None: + return tuple(bs[0:prefixlen]) + if strip is not None: + while bs and bs[-1] == strip: + bs.pop() + return tuple(bs) + +class resource_bag(object): + """Container to simplify passing around the usual triple of ASN, + IPv4, and IPv6 resource sets. + """ + + ## @var asn + # Set of Autonomous System Number resources. + + ## @var v4 + # Set of IPv4 resources. + + ## @var v6 + # Set of IPv6 resources. + + ## @var valid_until + # Expiration date of resources, for setting certificate notAfter field. + + def __init__(self, asn = None, v4 = None, v6 = None, valid_until = None): + self.asn = asn or resource_set_as() + self.v4 = v4 or resource_set_ipv4() + self.v6 = v6 or resource_set_ipv6() + self.valid_until = valid_until + + def oversized(self, other): + """True iff self is oversized with respect to other.""" + return not self.asn.issubset(other.asn) or \ + not self.v4.issubset(other.v4) or \ + not self.v6.issubset(other.v6) + + def undersized(self, other): + """True iff self is undersized with respect to other.""" + return not other.asn.issubset(self.asn) or \ + not other.v4.issubset(self.v4) or \ + not other.v6.issubset(self.v6) + + @classmethod + def from_rfc3779_tuples(cls, exts): + """Build a resource_bag from intermediate form generated by RFC 3779 ASN.1 decoder.""" + asn = None + v4 = None + v6 = None + for x in exts: + if x[0] == rpki.oids.name2oid["sbgp-autonomousSysNum"]: # + assert len(x[2]) == 1 or x[2][1] is None, "RDI not implemented: %s" % (str(x)) + assert asn is None + asn = resource_set_as(x[2][0]) + if x[0] == rpki.oids.name2oid["sbgp-ipAddrBlock"]: + for fam in x[2]: + if fam[0] == resource_set_ipv4.afi: + assert v4 is None + v4 = resource_set_ipv4(fam[1]) + if fam[0] == resource_set_ipv6.afi: + assert v6 is None + v6 = resource_set_ipv6(fam[1]) + return cls(asn, v4, v6) + + def empty(self): + """Return True iff all resource sets in this bag are empty.""" + return not self.asn and not self.v4 and not self.v6 + + def __eq__(self, other): + return self.asn == other.asn and \ + self.v4 == other.v4 and \ + self.v6 == other.v6 and \ + self.valid_until == other.valid_until + + def __ne__(self, other): + return not (self == other) + + def intersection(self, other): + """Compute intersection with another resource_bag. + valid_until attribute (if any) inherits from self. + """ + return self.__class__(self.asn.intersection(other.asn), + self.v4.intersection(other.v4), + self.v6.intersection(other.v6), + self.valid_until) + + def union(self, other): + """Compute union with another resource_bag. + valid_until attribute (if any) inherits from self. + """ + return self.__class__(self.asn.union(other.asn), + self.v4.union(other.v4), + self.v6.union(other.v6), + self.valid_until) + + def __str__(self): + s = "" + if self.asn: + s += "ASN: %s" % self.asn + if self.v4: + if s: + s += ", " + s += "V4: %s" % self.v4 + if self.v6: + if s: + s += ", " + s += "V6: %s" % self.v6 + return s + +# Sadly, there are enough differences between RFC 3779 and the data +# structures in the latest proposed ROA format that we can't just use +# the RFC 3779 code for ROAs. So we need a separate set of classes +# that are similar in concept but different in detail, with conversion +# functions. Such is life. I suppose it might be possible to do this +# with multiple inheritance, but that's probably more bother than it's +# worth. + +class roa_prefix(object): + """ROA prefix. This is similar to the resource_range_ip class, but + differs in that it only represents prefixes, never ranges, and + includes the maximum prefix length as an additional value. + + This is a virtual class, you probably don't want to use it directly. + """ + + ## @var address + # Address portion of prefix. + + ## @var prefixlen + # (Minimum) prefix length. + + ## @var max_prefixlen + # Maxmimum prefix length. + + def __init__(self, address, prefixlen, max_prefixlen = None): + """Initialize a ROA prefix. max_prefixlen is optional and + defaults to prefixlen. max_prefixlen must not be smaller than + prefixlen. + """ + if max_prefixlen is None: + max_prefixlen = prefixlen + assert max_prefixlen >= prefixlen, "Bad max_prefixlen: %d must not be shorter than %d" % (max_prefixlen, prefixlen) + self.address = address + self.prefixlen = prefixlen + self.max_prefixlen = max_prefixlen + + def __cmp__(self, other): + """Compare two ROA prefix objects. Comparision is based on + address, prefixlen, and max_prefixlen, in that order. + """ + assert self.__class__ is other.__class__ + c = self.address - other.address + if c == 0: c = self.prefixlen - other.prefixlen + if c == 0: c = self.max_prefixlen - other.max_prefixlen + if c < 0: c = -1 + if c > 0: c = 1 + return c + + def __str__(self): + """Convert a ROA prefix to string format.""" + if self.prefixlen == self.max_prefixlen: + return str(self.address) + "/" + str(self.prefixlen) + else: + return str(self.address) + "/" + str(self.prefixlen) + "-" + str(self.max_prefixlen) + + def to_resource_range(self): + """Convert this ROA prefix to the equivilent resource_range_ip + object. This is an irreversable transformation because it loses + the max_prefixlen attribute, nothing we can do about that. + """ + return self.range_type.make_prefix(self.address, self.prefixlen) + + def min(self): + """Return lowest address covered by prefix.""" + return self.address + + def max(self): + """Return highest address covered by prefix.""" + t = self.range_type.datum_type + return t(self.address | ((1 << (t.bits - self.prefixlen)) - 1)) + + def to_roa_tuple(self): + """Convert a resource_range_ip to tuple format for ROA ASN.1 encoding.""" + return (_long2bs(self.address, self.range_type.datum_type.bits, prefixlen = self.prefixlen), + None if self.prefixlen == self.max_prefixlen else self.max_prefixlen) + +class roa_prefix_ipv4(roa_prefix): + """IPv4 ROA prefix.""" + + ## @var range_type + # Type of corresponding resource_range_ip. + + range_type = resource_range_ipv4 + +class roa_prefix_ipv6(roa_prefix): + """IPv6 ROA prefix.""" + + ## @var range_type + # Type of corresponding resource_range_ip. + + range_type = resource_range_ipv6 + +class roa_prefix_set(list): + """Set of ROA prefixes, analogous to the resource_set_ip class.""" + + def __init__(self, ini = None): + """Initialize a ROA prefix set.""" + if isinstance(ini, str) and len(ini): + self.extend(self.parse_str(s) for s in ini.split(",")) + elif isinstance(ini, (list, tuple)): + self.extend(ini) + else: + assert ini is None or ini == "", "Unexpected initializer: %s" % str(ini) + self.sort() + if __debug__: + for i in xrange(0, len(self) - 1): + assert self[i].max() < self[i+1].min(), "Prefix overlap: %s %s" % (self[i], self[i+1]) + + def __str__(self): + """Convert a ROA prefix set to string format.""" + return ",".join(str(x) for x in self) + + def parse_str(self, x): + """Parse ROA prefix from text (eg, an XML attribute).""" + r = re.match("^([0-9:.a-fA-F]+)/([0-9]+)-([0-9]+)$", x) + if r: + return self.prefix_type(self.prefix_type.range_type.datum_type(r.group(1)), int(r.group(2)), int(r.group(3))) + r = re.match("^([0-9:.a-fA-F]+)/([0-9]+)$", x) + if r: + return self.prefix_type(self.prefix_type.range_type.datum_type(r.group(1)), int(r.group(2))) + raise RuntimeError, 'Bad ROA prefix "%s"' % (x) + + def to_resource_set(self): + """Convert a ROA prefix set to a resource set. This is an + irreversable transformation. + """ + return self.resource_set_type([p.to_resource_range() for p in self]) + + @classmethod + def from_sql(cls, sql, query, args = None): + """Create ROA prefix set from an SQL query. + + sql is an object that supports execute() and fetchall() methods + like a DB API 2.0 cursor object. + + query is an SQL query that returns a sequence of (address, + prefixlen, max_prefixlen) triples. + """ + + sql.execute(query, args) + return cls([cls.prefix_type(cls.prefix_type.range_type.datum_type(x), int(y), int(z)) + for (x,y,z) in sql.fetchall()]) + + def to_roa_tuple(self): + """Convert ROA prefix set into tuple format used by ROA ASN.1 encoder. + This is a variation on the format used in RFC 3779.""" + if self: + return (self.resource_set_type.afi, tuple(a.to_roa_tuple() for a in self)) + else: + return None + +class roa_prefix_set_ipv4(roa_prefix_set): + """Set of IPv4 ROA prefixes.""" + + ## @var prefix_type + # Type of underlying roa_prefix. + + prefix_type = roa_prefix_ipv4 + + ## @var resource_set_type + # Type of corresponding resource_set_ip class. + + resource_set_type = resource_set_ipv4 + +class roa_prefix_set_ipv6(roa_prefix_set): + """Set of IPv6 ROA prefixes.""" + + ## @var prefix_type + # Type of underlying roa_prefix. + + prefix_type = roa_prefix_ipv6 + + ## @var resource_set_type + # Type of corresponding resource_set_ip class. + + resource_set_type = resource_set_ipv6 + +# Test suite for set operations. + +if __name__ == "__main__": + + def test1(t, s1, s2): + if isinstance(s1, str) and isinstance(s2, str): + print "x: ", s1 + print "y: ", s2 + r1 = t(s1) + r2 = t(s2) + print "x: ", r1 + print "y: ", r2 + v1 = r1._comm(r2) + v2 = r2._comm(r1) + assert v1[0] == v2[1] and v1[1] == v2[0] and v1[2] == v2[2] + for i in r1: assert r1.contains(i) and r1.contains(i.min) and r1.contains(i.max) + for i in r2: assert r2.contains(i) and r2.contains(i.min) and r2.contains(i.max) + for i in v1[0]: assert r1.contains(i) and not r2.contains(i) + for i in v1[1]: assert not r1.contains(i) and r2.contains(i) + for i in v1[2]: assert r1.contains(i) and r2.contains(i) + v1 = r1.union(r2) + v2 = r2.union(r1) + assert v1 == v2 + print "x|y:", v1 + v1 = r1.difference(r2) + v2 = r2.difference(r1) + print "x-y:", v1 + print "y-x:", v2 + v1 = r1.symmetric_difference(r2) + v2 = r2.symmetric_difference(r1) + assert v1 == v2 + print "x^y:", v1 + v1 = r1.intersection(r2) + v2 = r2.intersection(r1) + assert v1 == v2 + print "x&y:", v1 + + def test2(t, s1, s2): + print "x: ", s1 + print "y: ", s2 + r1 = t(s1) + r2 = t(s2) + print "x: ", r1 + print "y: ", r2 + print "x>y:", (r1 > r2) + print "x<y:", (r1 < r2) + test1(t.resource_set_type, r1.to_resource_set(), r2.to_resource_set()) + + print + print "Testing set operations on resource sets" + print + test1(resource_set_as, "1,2,3,4,5,6,11,12,13,14,15", "1,2,3,4,5,6,111,121,131,141,151") + print + test1(resource_set_ipv4, "10.0.0.44/32,10.6.0.2/32", "10.3.0.0/24,10.0.0.77/32") + print + test1(resource_set_ipv4, "10.0.0.44/32,10.6.0.2/32", "10.0.0.0/24") + print + test1(resource_set_ipv4, "10.0.0.0/24", "10.3.0.0/24,10.0.0.77/32") + print + print "Testing set operations on ROA prefixes" + print + test2(roa_prefix_set_ipv4, "10.0.0.44/32,10.6.0.2/32", "10.3.0.0/24,10.0.0.77/32") + print + test2(roa_prefix_set_ipv4, "10.0.0.0/24-32,10.6.0.0/24-32", "10.3.0.0/24,10.0.0.0/16-32") + print + test2(roa_prefix_set_ipv4, "10.3.0.0/24-24,10.0.0.0/16-32", "10.3.0.0/24,10.0.0.0/16-32") + print diff --git a/rpkid.stable/rpki/roa.py b/rpkid.stable/rpki/roa.py new file mode 100644 index 00000000..ab178db0 --- /dev/null +++ b/rpkid.stable/rpki/roa.py @@ -0,0 +1,75 @@ +"""ROA (Route Origin Authorization). + +At the moment this is just the ASN.1 encoder. + +This corresponds to draft-ietf-sidr-roa-format, which is a work in +progress, so this may need updating later. + +$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. + +draft-ietf-sidr-roa-format-03 2.1.3.2 specifies: + + RouteOriginAttestation ::= SEQUENCE { + version [0] INTEGER DEFAULT 0, + asID ASID, + ipAddrBlocks SEQUENCE OF ROAIPAddressFamily } + + ASID ::= INTEGER + + ROAIPAddressFamily ::= SEQUENCE { + addressFamily OCTET STRING (SIZE (2..3)), + addresses SEQUENCE OF ROAIPAddress } + + ROAIPAddress ::= SEQUENCE { + address IPAddress, + maxLength INTEGER OPTIONAL } + + IPAddress ::= BIT STRING +""" + +from POW._der import * + +class ROAIPAddress(Sequence): + def __init__(self, optional=0, default=''): + self.address = BitString() + self.maxLength = Integer(1) + contents = [ self.address, self.maxLength ] + Sequence.__init__(self, contents, optional, default) + +class ROAIPAddresses(SequenceOf): + def __init__(self, optional=0, default=''): + SequenceOf.__init__(self, ROAIPAddress, optional, default) + +class ROAIPAddressFamily(Sequence): + def __init__(self, optional=0, default=''): + self.addressFamily = OctetString() + self.addresses = ROAIPAddresses() + contents = [ self.addressFamily, self.addresses ] + Sequence.__init__(self, contents, optional, default) + +class ROAIPAddressFamilies(SequenceOf): + def __init__(self, optional=0, default=''): + SequenceOf.__init__(self, ROAIPAddressFamily, optional, default) + +class RouteOriginAttestation(Sequence): + def __init__(self, optional=0, default=''): + self.version = Integer() + self.explicitVersion = Explicit(CLASS_CONTEXT, FORM_CONSTRUCTED, 0, self.version, 0, 'oAMCAQA=') + self.asID = Integer() + self.ipAddrBlocks = ROAIPAddressFamilies() + contents = [ self.explicitVersion, self.asID, self.ipAddrBlocks ] + Sequence.__init__(self, contents, optional, default) diff --git a/rpkid.stable/rpki/rpki_engine.py b/rpkid.stable/rpki/rpki_engine.py new file mode 100644 index 00000000..a49121c1 --- /dev/null +++ b/rpkid.stable/rpki/rpki_engine.py @@ -0,0 +1,819 @@ +"""Global context for rpkid. + +$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 traceback, os, time, getopt, sys, MySQLdb, lxml.etree +import rpki.resource_set, rpki.up_down, rpki.left_right, rpki.x509, rpki.sql +import rpki.https, rpki.config, rpki.exceptions, rpki.relaxng, rpki.log + +class rpkid_context(object): + """A container for various global rpkid parameters.""" + + def __init__(self, cfg): + + self.sql = rpki.sql.session(cfg) + + self.bpki_ta = rpki.x509.X509(Auto_file = cfg.get("bpki-ta")) + self.irdb_cert = rpki.x509.X509(Auto_file = cfg.get("irdb-cert")) + self.irbe_cert = rpki.x509.X509(Auto_file = cfg.get("irbe-cert")) + self.rpkid_cert = rpki.x509.X509(Auto_file = cfg.get("rpkid-cert")) + self.rpkid_key = rpki.x509.RSA( Auto_file = cfg.get("rpkid-key")) + + self.irdb_url = cfg.get("irdb-url") + + self.https_server_host = cfg.get("server-host", "") + self.https_server_port = int(cfg.get("server-port", "4433")) + + self.publication_kludge_base = cfg.get("publication-kludge-base", "publication/") + + def irdb_query(self, self_id, child_id = None): + """Perform an IRDB callback query. In the long run this should not + be a blocking routine, it should instead issue a query and set up a + handler to receive the response. For the moment, though, we are + doing simple lock step and damn the torpedos. Not yet doing + anything useful with subject name. Most likely this function should + really be wrapped up in a class that carries both the query result + and also the intermediate state needed for the event-driven code + that this function will need to become. + """ + + rpki.log.trace() + + q_msg = rpki.left_right.msg() + q_msg.type = "query" + q_msg.append(rpki.left_right.list_resources_elt()) + q_msg[0].self_id = self_id + q_msg[0].child_id = child_id + q_cms = rpki.left_right.cms_msg.wrap(q_msg, self.rpkid_key, self.rpkid_cert) + der = rpki.https.client( + server_ta = (self.bpki_ta, self.irdb_cert), + client_key = self.rpkid_key, + client_cert = self.rpkid_cert, + url = self.irdb_url, + msg = q_cms) + r_msg = rpki.left_right.cms_msg.unwrap(der, (self.bpki_ta, self.irdb_cert)) + if len(r_msg) == 0 or not isinstance(r_msg[0], rpki.left_right.list_resources_elt) or r_msg.type != "reply": + raise rpki.exceptions.BadIRDBReply, "Unexpected response to IRDB query: %s" % lxml.etree.tostring(r_msg.toXML(), pretty_print = True, encoding = "us-ascii") + return rpki.resource_set.resource_bag( + asn = r_msg[0].asn, + v4 = r_msg[0].ipv4, + v6 = r_msg[0].ipv6, + valid_until = r_msg[0].valid_until) + + def left_right_handler(self, query, path): + """Process one left-right PDU.""" + rpki.log.trace() + try: + self.sql.ping() + q_msg = rpki.left_right.cms_msg.unwrap(query, (self.bpki_ta, self.irbe_cert)) + if q_msg.type != "query": + raise rpki.exceptions.BadQuery, "Message type is not query" + r_msg = q_msg.serve_top_level(self) + reply = rpki.left_right.cms_msg.wrap(r_msg, self.rpkid_key, self.rpkid_cert) + self.sql.sweep() + return 200, reply + except Exception, data: + rpki.log.error(traceback.format_exc()) + return 500, "Unhandled exception %s" % data + + def up_down_handler(self, query, path): + """Process one up-down PDU.""" + rpki.log.trace() + try: + self.sql.ping() + child_id = path.partition("/up-down/")[2] + if not child_id.isdigit(): + raise rpki.exceptions.BadContactURL, "Bad path: %s" % path + child = rpki.left_right.child_elt.sql_fetch(self, long(child_id)) + if child is None: + raise rpki.exceptions.ChildNotFound, "Could not find child %s" % child_id + reply = child.serve_up_down(query) + self.sql.sweep() + return 200, reply + except Exception, data: + rpki.log.error(traceback.format_exc()) + return 400, "Could not process PDU: %s" % data + + def cronjob_handler(self, query, path): + """Periodic tasks. As simple as possible for now, may need to break + this up into separate handlers later. + """ + + rpki.log.trace() + try: + self.sql.ping() + for s in rpki.left_right.self_elt.sql_fetch_all(self): + rpki.log.debug("Self %s polling parents" % s.self_id) + s.client_poll() + rpki.log.debug("Self %s updating children" % s.self_id) + s.update_children() + rpki.log.debug("Self %s updating ROAs" % s.self_id) + s.update_roas() + rpki.log.debug("Self %s regenerating CRLs and manifests" % s.self_id) + s.regenerate_crls_and_manifests() + self.sql.sweep() + return 200, "OK" + except Exception, data: + rpki.log.error(traceback.format_exc()) + return 500, "Unhandled exception %s" % data + + ## @var https_ta_cache + # HTTPS trust anchor cache, to avoid regenerating it for every TLS connection. + https_ta_cache = None + + def clear_https_ta_cache(self): + """Clear dynamic TLS trust anchors.""" + + if self.https_ta_cache is not None: + rpki.log.debug("Clearing HTTPS trusted cert cache") + self.https_ta_cache = None + + def build_https_ta_cache(self): + """Build dynamic TLS trust anchors.""" + + if self.https_ta_cache is None: + + selves = rpki.left_right.self_elt.sql_fetch_all(self) + children = rpki.left_right.child_elt.sql_fetch_all(self) + + self.https_ta_cache = rpki.https.build_https_ta_cache( + [c.bpki_cert for c in children if c.bpki_cert is not None] + + [c.bpki_glue for c in children if c.bpki_glue is not None] + + [s.bpki_cert for s in selves if s.bpki_cert is not None] + + [s.bpki_glue for s in selves if s.bpki_glue is not None] + + [self.irbe_cert, self.irdb_cert, self.bpki_ta]) + + return self.https_ta_cache + + +class ca_obj(rpki.sql.sql_persistant): + """Internal CA object.""" + + sql_template = rpki.sql.template( + "ca", + "ca_id", + "last_crl_sn", + ("next_crl_update", rpki.sundial.datetime), + "last_issued_sn", "last_manifest_sn", + ("next_manifest_update", rpki.sundial.datetime), + "sia_uri", "parent_id", "parent_resource_class") + + last_crl_sn = 0 + last_issued_sn = 0 + last_manifest_sn = 0 + + def parent(self): + """Fetch parent object to which this CA object links.""" + return rpki.left_right.parent_elt.sql_fetch(self.gctx, self.parent_id) + + def ca_details(self): + """Fetch all ca_detail objects that link to this CA object.""" + return ca_detail_obj.sql_fetch_where(self.gctx, "ca_id = %s", (self.ca_id,)) + + def fetch_pending(self): + """Fetch the pending ca_details for this CA, if any.""" + return ca_detail_obj.sql_fetch_where(self.gctx, "ca_id = %s AND state = 'pending'", (self.ca_id,)) + + def fetch_active(self): + """Fetch the active ca_detail for this CA, if any.""" + return ca_detail_obj.sql_fetch_where1(self.gctx, "ca_id = %s AND state = 'active'", (self.ca_id,)) + + def fetch_deprecated(self): + """Fetch deprecated ca_details for this CA, if any.""" + return ca_detail_obj.sql_fetch_where(self.gctx, "ca_id = %s AND state = 'deprecated'", (self.ca_id,)) + + def fetch_revoked(self): + """Fetch revoked ca_details for this CA, if any.""" + return ca_detail_obj.sql_fetch_where(self.gctx, "ca_id = %s AND state = 'revoked'", (self.ca_id,)) + + def construct_sia_uri(self, parent, rc): + """Construct the sia_uri value for this CA given configured + information and the parent's up-down protocol list_response PDU. + """ + + repository = parent.repository() + sia_uri = rc.suggested_sia_head and rc.suggested_sia_head.rsync() + if not sia_uri or not sia_uri.startswith(parent.sia_base): + sia_uri = parent.sia_base + elif not sia_uri.endswith("/"): + raise rpki.exceptions.BadURISyntax, "SIA URI must end with a slash: %s" % sia_uri + return sia_uri + str(self.ca_id) + "/" + + def check_for_updates(self, parent, rc): + """Parent has signaled continued existance of a resource class we + already knew about, so we need to check for an updated + certificate, changes in resource coverage, revocation and reissue + with the same key, etc. + """ + + sia_uri = self.construct_sia_uri(parent, rc) + sia_uri_changed = self.sia_uri != sia_uri + if sia_uri_changed: + self.sia_uri = sia_uri + self.sql_mark_dirty() + + rc_resources = rc.to_resource_bag() + cert_map = dict((c.cert.get_SKI(), c) for c in rc.certs) + + for ca_detail in ca_detail_obj.sql_fetch_where(self.gctx, "ca_id = %s AND latest_ca_cert IS NOT NULL AND state != 'revoked'", (self.ca_id,)): + + ski = ca_detail.latest_ca_cert.get_SKI() + + if ski not in cert_map: + rpki.log.warn("Certificate in database missing from list_response, class %s, SKI %s, maybe parent certificate went away?" + % (repr(rc.class_name), ca_detail.latest_ca_cert.gSKI())) + ca_detail.delete(self, parent.repository()) + continue + + if ca_detail.state in ("pending", "active"): + current_resources = ca_detail.latest_ca_cert.get_3779resources() + if sia_uri_changed or \ + ca_detail.latest_ca_cert != cert_map[ski].cert or \ + current_resources.undersized(rc_resources) or \ + current_resources.oversized(rc_resources): + ca_detail.update( + parent = parent, + ca = self, + rc = rc, + sia_uri_changed = sia_uri_changed, + old_resources = current_resources) + + del cert_map[ski] + + if cert_map: + rpki.log.warn("Certificates in list_response missing from our database, class %s, SKIs %s" + % (repr(rc.class_name), ", ".join(c.cert.gSKI() for c in cert_map.values()))) + + @classmethod + def create(cls, parent, rc): + """Parent has signaled existance of a new resource class, so we + need to create and set up a corresponding CA object. + """ + + self = cls() + self.gctx = parent.gctx + self.parent_id = parent.parent_id + self.parent_resource_class = rc.class_name + self.sql_store() + self.sia_uri = self.construct_sia_uri(parent, rc) + ca_detail = ca_detail_obj.create(self) + + # This will need a callback when we go event-driven + issue_response = rpki.up_down.issue_pdu.query(parent, self, ca_detail) + + ca_detail.activate( + ca = self, + cert = issue_response.payload.classes[0].certs[0].cert, + uri = issue_response.payload.classes[0].certs[0].cert_url) + + def delete(self, parent): + """The list of current resource classes received from parent does + not include the class corresponding to this CA, so we need to + delete it (and its little dog too...). + + All certs published by this CA are now invalid, so need to + withdraw them, the CRL, and the manifest from the repository, + delete all child_cert and ca_detail records associated with this + CA, then finally delete this CA itself. + """ + + repository = parent.repository() + for ca_detail in self.ca_details(): + ca_detail.delete(self, repository) + self.sql_delete() + + def next_serial_number(self): + """Allocate a certificate serial number.""" + self.last_issued_sn += 1 + self.sql_mark_dirty() + return self.last_issued_sn + + def next_manifest_number(self): + """Allocate a manifest serial number.""" + self.last_manifest_sn += 1 + self.sql_mark_dirty() + return self.last_manifest_sn + + def next_crl_number(self): + """Allocate a CRL serial number.""" + self.last_crl_sn += 1 + self.sql_mark_dirty() + return self.last_crl_sn + + def rekey(self): + """Initiate a rekey operation for this ca. Generate a new + keypair. Request cert from parent using new keypair. Mark result + as our active ca_detail. Reissue all child certs issued by this + ca using the new ca_detail. + """ + + rpki.log.trace() + + parent = self.parent() + old_detail = self.fetch_active() + new_detail = ca_detail_obj.create(self) + + # This will need a callback when we go event-driven + issue_response = rpki.up_down.issue_pdu.query(parent, self, new_detail) + + new_detail.activate( + ca = self, + cert = issue_response.payload.classes[0].certs[0].cert, + uri = issue_response.payload.classes[0].certs[0].cert_url, + predecessor = old_detail) + + def revoke(self): + """Revoke deprecated ca_detail objects associated with this ca.""" + + rpki.log.trace() + + for ca_detail in self.fetch_deprecated(): + ca_detail.revoke() + +class ca_detail_obj(rpki.sql.sql_persistant): + """Internal CA detail object.""" + + sql_template = rpki.sql.template( + "ca_detail", + "ca_detail_id", + ("private_key_id", rpki.x509.RSA), + ("public_key", rpki.x509.RSApublic), + ("latest_ca_cert", rpki.x509.X509), + ("manifest_private_key_id", rpki.x509.RSA), + ("manifest_public_key", rpki.x509.RSApublic), + ("latest_manifest_cert", rpki.x509.X509), + ("latest_manifest", rpki.x509.SignedManifest), + ("latest_crl", rpki.x509.CRL), + "state", + "ca_cert_uri", + "ca_id") + + def sql_decode(self, vals): + """Extra assertions for SQL decode of a ca_detail_obj.""" + rpki.sql.sql_persistant.sql_decode(self, vals) + assert (self.public_key is None and self.private_key_id is None) or \ + self.public_key.get_DER() == self.private_key_id.get_public_DER() + assert (self.manifest_public_key is None and self.manifest_private_key_id is None) or \ + self.manifest_public_key.get_DER() == self.manifest_private_key_id.get_public_DER() + + def ca(self): + """Fetch CA object to which this ca_detail links.""" + return ca_obj.sql_fetch(self.gctx, self.ca_id) + + def child_certs(self, child = None, ski = None, unique = False): + """Fetch all child_cert objects that link to this ca_detail.""" + return rpki.rpki_engine.child_cert_obj.fetch(self.gctx, child, self, ski, unique) + + def revoked_certs(self): + """Fetch all revoked_cert objects that link to this ca_detail.""" + return revoked_cert_obj.sql_fetch_where(self.gctx, "ca_detail_id = %s", (self.ca_detail_id,)) + + def route_origins(self): + """Fetch all route_origin objects that link to this ca_detail.""" + return rpki.left_right.route_origin_elt.sql_fetch_where(self.gctx, "ca_detail_id = %s", (self.ca_detail_id,)) + + def crl_uri(self, ca): + """Return publication URI for this ca_detail's CRL.""" + return ca.sia_uri + self.crl_uri_tail() + + def crl_uri_tail(self): + """Return tail (filename portion) of publication URI for this ca_detail's CRL.""" + return self.public_key.gSKI() + ".crl" + + def manifest_uri(self, ca): + """Return publication URI for this ca_detail's manifest.""" + return ca.sia_uri + self.public_key.gSKI() + ".mnf" + + def activate(self, ca, cert, uri, predecessor = None): + """Activate this ca_detail.""" + + self.latest_ca_cert = cert + self.ca_cert_uri = uri.rsync() + self.generate_manifest_cert(ca) + self.generate_crl() + self.generate_manifest() + self.state = "active" + self.sql_mark_dirty() + + if predecessor is not None: + predecessor.state = "deprecated" + predecessor.sql_mark_dirty() + for child_cert in predecessor.child_certs(): + child_cert.reissue(self) + for route_origin in predecessor.route_origins(): + route_origin.regenerate_roa() + + def delete(self, ca, repository): + """Delete this ca_detail and all of the certs it issued.""" + + for child_cert in self.child_certs(): + repository.withdraw(child_cert.cert, child_cert.uri(ca)) + child_cert.sql_delete() + for revoked_cert in self.revoked_certs(): + revoked_cert.sql_delete() + for route_origin in self.route_origins(): + route_origin.withdraw_roa() + repository.withdraw(self.latest_manifest, self.manifest_uri(ca)) + repository.withdraw(self.latest_crl, self.crl_uri(ca)) + self.sql_delete() + + def revoke(self): + """Request revocation of all certificates whose SKI matches the key for this ca_detail. + + Tasks: + + - Request revocation of old keypair by parent. + + - Revoke all child certs issued by the old keypair. + + - Generate a final CRL, signed with the old keypair, listing all + the revoked certs, with a next CRL time after the last cert or + CRL signed by the old keypair will have expired. + + - Destroy old keypair (and manifest keypair). + + - Leave final CRL in place until its next CRL time has passed. + """ + + # This will need a callback when we go event-driven + r_msg = rpki.up_down.revoke_pdu.query(self) + + if r_msg.payload.ski != self.latest_ca_cert.gSKI(): + raise rpki.exceptions.SKIMismatch + + ca = self.ca() + parent = ca.parent() + crl_interval = rpki.sundial.timedelta(seconds = parent.self().crl_interval) + + nextUpdate = rpki.sundial.now() + + if self.latest_manifest is not None: + nextUpdate = nextUpdate.later(self.latest_manifest.getNextUpdate()) + + if self.latest_crl is not None: + nextUpdate = nextUpdate.later(self.latest_crl.getNextUpdate()) + + for child_cert in self.child_certs(): + nextUpdate = nextUpdate.later(child_cert.cert.getNotAfter()) + child_cert.revoke() + + nextUpdate += crl_interval + + self.generate_crl(nextUpdate) + self.generate_manifest(nextUpdate) + + self.private_key_id = None + self.manifest_private_key_id = None + self.manifest_public_key = None + self.latest_manifest_cert = None + self.state = "revoked" + self.sql_mark_dirty() + + def update(self, parent, ca, rc, sia_uri_changed, old_resources): + """Need to get a new certificate for this ca_detail and perhaps + frob children of this ca_detail. + """ + + # This will need a callback when we go event-driven + issue_response = rpki.up_down.issue_pdu.query(parent, ca, self) + + self.latest_ca_cert = issue_response.payload.classes[0].certs[0].cert + new_resources = self.latest_ca_cert.get_3779resources() + + if sia_uri_changed or old_resources.oversized(new_resources): + for child_cert in self.child_certs(): + child_resources = child_cert.cert.get_3779resources() + if sia_uri_changed or child_resources.oversized(new_resources): + child_cert.reissue( + ca_detail = self, + resources = child_resources.intersection(new_resources)) + + @classmethod + def create(cls, ca): + """Create a new ca_detail object for a specified CA.""" + self = cls() + self.gctx = ca.gctx + self.ca_id = ca.ca_id + self.state = "pending" + + self.private_key_id = rpki.x509.RSA.generate() + self.public_key = self.private_key_id.get_RSApublic() + + self.manifest_private_key_id = rpki.x509.RSA.generate() + self.manifest_public_key = self.manifest_private_key_id.get_RSApublic() + + self.sql_store() + return self + + def issue_ee(self, ca, resources, subject_key, sia = None): + """Issue a new EE certificate.""" + + return self.latest_ca_cert.issue( + keypair = self.private_key_id, + subject_key = subject_key, + serial = ca.next_serial_number(), + sia = sia, + aia = self.ca_cert_uri, + crldp = self.crl_uri(ca), + resources = resources, + notAfter = self.latest_ca_cert.getNotAfter(), + is_ca = False) + + + def generate_manifest_cert(self, ca): + """Generate a new manifest certificate for this ca_detail.""" + + resources = rpki.resource_set.resource_bag( + asn = rpki.resource_set.resource_set_as("<inherit>"), + v4 = rpki.resource_set.resource_set_ipv4("<inherit>"), + v6 = rpki.resource_set.resource_set_ipv6("<inherit>")) + + self.latest_manifest_cert = self.issue_ee(ca, resources, self.manifest_public_key) + + def issue(self, ca, child, subject_key, sia, resources, child_cert = None): + """Issue a new certificate to a child. Optional child_cert + argument specifies an existing child_cert object to update in + place; if not specified, we create a new one. Returns the + child_cert object containing the newly issued cert. + """ + + assert child_cert is None or (child_cert.child_id == child.child_id and + child_cert.ca_detail_id == self.ca_detail_id) + + cert = self.latest_ca_cert.issue( + keypair = self.private_key_id, + subject_key = subject_key, + serial = ca.next_serial_number(), + aia = self.ca_cert_uri, + crldp = self.crl_uri(ca), + sia = sia, + resources = resources, + notAfter = resources.valid_until) + + if child_cert is None: + child_cert = rpki.rpki_engine.child_cert_obj( + gctx = child.gctx, + child_id = child.child_id, + ca_detail_id = self.ca_detail_id, + cert = cert) + rpki.log.debug("Created new child_cert %s" % repr(child_cert)) + else: + child_cert.cert = cert + rpki.log.debug("Reusing existing child_cert %s" % repr(child_cert)) + + child_cert.ski = cert.get_SKI() + + child_cert.sql_store() + + ca.parent().repository().publish(child_cert.cert, child_cert.uri(ca)) + + self.generate_manifest() + + return child_cert + + def generate_crl(self, nextUpdate = None): + """Generate a new CRL for this ca_detail. At the moment this is + unconditional, that is, it is up to the caller to decide whether a + new CRL is needed. + """ + + ca = self.ca() + parent = ca.parent() + repository = parent.repository() + crl_interval = rpki.sundial.timedelta(seconds = parent.self().crl_interval) + now = rpki.sundial.now() + + if nextUpdate is None: + nextUpdate = now + crl_interval + + certlist = [] + for revoked_cert in self.revoked_certs(): + if now > revoked_cert.expires + crl_interval: + revoked_cert.sql_delete() + else: + certlist.append((revoked_cert.serial, revoked_cert.revoked.toASN1tuple(), ())) + certlist.sort() + + self.latest_crl = rpki.x509.CRL.generate( + keypair = self.private_key_id, + issuer = self.latest_ca_cert, + serial = ca.next_crl_number(), + thisUpdate = now, + nextUpdate = nextUpdate, + revokedCertificates = certlist) + + repository.publish(self.latest_crl, self.crl_uri(ca)) + + def generate_manifest(self, nextUpdate = None): + """Generate a new manifest for this ca_detail.""" + + ca = self.ca() + parent = ca.parent() + repository = parent.repository() + crl_interval = rpki.sundial.timedelta(seconds = parent.self().crl_interval) + now = rpki.sundial.now() + + if nextUpdate is None: + nextUpdate = now + crl_interval + + route_origins = [r for r in self.route_origins() if r.cert is not None and r.roa is not None] + + if self.latest_manifest_cert is None or self.latest_manifest_cert.getNotAfter() < nextUpdate: + self.generate_manifest_cert(ca) + + certs = [(c.uri_tail(), c.cert) for c in self.child_certs()] + \ + [(r.roa_uri_tail(), r.roa) for r in route_origins] + \ + [(r.ee_uri_tail(), r.cert) for r in route_origins] + \ + [(self.crl_uri_tail(), self.latest_crl)] + + self.latest_manifest = rpki.x509.SignedManifest.build( + serial = ca.next_manifest_number(), + thisUpdate = now, + nextUpdate = nextUpdate, + names_and_objs = certs, + keypair = self.manifest_private_key_id, + certs = self.latest_manifest_cert) + + repository.publish(self.latest_manifest, self.manifest_uri(ca)) + +class child_cert_obj(rpki.sql.sql_persistant): + """Certificate that has been issued to a child.""" + + sql_template = rpki.sql.template( + "child_cert", + "child_cert_id", + ("cert", rpki.x509.X509), + "child_id", + "ca_detail_id", + "ski") + + def __init__(self, gctx = None, child_id = None, ca_detail_id = None, cert = None): + """Initialize a child_cert_obj.""" + self.gctx = gctx + self.child_id = child_id + self.ca_detail_id = ca_detail_id + self.cert = cert + if child_id or ca_detail_id or cert: + self.sql_mark_dirty() + + def child(self): + """Fetch child object to which this child_cert object links.""" + return rpki.left_right.child_elt.sql_fetch(self.gctx, self.child_id) + + def ca_detail(self): + """Fetch ca_detail object to which this child_cert object links.""" + return ca_detail_obj.sql_fetch(self.gctx, self.ca_detail_id) + + def uri_tail(self): + """Return the tail (filename) portion of the URI for this child_cert.""" + return self.cert.gSKI() + ".cer" + + def uri(self, ca): + """Return the publication URI for this child_cert.""" + return ca.sia_uri + self.uri_tail() + + def revoke(self): + """Revoke a child cert.""" + rpki.log.debug("Revoking %s" % repr(self)) + ca_detail = self.ca_detail() + ca = ca_detail.ca() + revoked_cert_obj.revoke(cert = self.cert, ca_detail = ca_detail) + repository = ca.parent().repository() + repository.withdraw(self.cert, self.uri(ca)) + self.gctx.sql.sweep() + self.sql_delete() + + def reissue(self, ca_detail, resources = None, sia = None): + """Reissue an existing cert, reusing the public key. If the cert + we would generate is identical to the one we already have, we just + return the one we already have. If we have to revoke the old + certificate when generating the new one, we have to generate a new + child_cert_obj, so calling code that needs the updated + child_cert_obj must use the return value from this method. + """ + + ca = ca_detail.ca() + child = self.child() + + old_resources = self.cert.get_3779resources() + old_sia = self.cert.get_SIA() + old_ca_detail = self.ca_detail() + + if resources is None: + resources = old_resources + + if sia is None: + sia = old_sia + + assert resources.valid_until is not None and old_resources.valid_until is not None + + if resources == old_resources and sia == old_sia and ca_detail == old_ca_detail: + return self + + must_revoke = old_resources.oversized(resources) or old_resources.valid_until > resources.valid_until + new_issuer = ca_detail != old_ca_detail + + if resources.valid_until != old_resources.valid_until: + rpki.log.debug("Validity changed: %s %s" % ( old_resources.valid_until, resources.valid_until)) + + if must_revoke or new_issuer: + child_cert = None + else: + child_cert = self + + child_cert = ca_detail.issue( + ca = ca, + child = child, + subject_key = self.cert.getPublicKey(), + sia = sia, + resources = resources, + child_cert = child_cert) + + if must_revoke: + for cert in child.child_certs(ca_detail = ca_detail, ski = self.ski): + if cert is not child_cert: + cert.revoke() + + return child_cert + + @classmethod + def fetch(cls, gctx = None, child = None, ca_detail = None, ski = None, unique = False): + """Fetch all child_cert objects matching a particular set of + parameters. This is a wrapper to consolidate various queries that + would otherwise be inline SQL WHERE expressions. In most cases + code calls this indirectly, through methods in other classes. + """ + + args = [] + where = [] + + if child: + where.append("child_id = %s") + args.append(child.child_id) + + if ca_detail: + where.append("ca_detail_id = %s") + args.append(ca_detail.ca_detail_id) + + if ski: + where.append("ski = %s") + args.append(ski) + + where = " AND ".join(where) + + gctx = gctx or (child and child.gctx) or (ca_detail and ca_detail.gctx) or None + + if unique: + return cls.sql_fetch_where1(gctx, where, args) + else: + return cls.sql_fetch_where(gctx, where, args) + +class revoked_cert_obj(rpki.sql.sql_persistant): + """Tombstone for a revoked certificate.""" + + sql_template = rpki.sql.template( + "revoked_cert", + "revoked_cert_id", + "serial", + "ca_detail_id", + ("revoked", rpki.sundial.datetime), + ("expires", rpki.sundial.datetime)) + + def __init__(self, gctx = None, serial = None, revoked = None, expires = None, ca_detail_id = None): + """Initialize a revoked_cert_obj.""" + self.gctx = gctx + self.serial = serial + self.revoked = revoked + self.expires = expires + self.ca_detail_id = ca_detail_id + if serial or revoked or expires or ca_detail_id: + self.sql_mark_dirty() + + def ca_detail(self): + """Fetch ca_detail object to which this revoked_cert_obj links.""" + return ca_detail_obj.sql_fetch(self.gctx, self.ca_detail_id) + + @classmethod + def revoke(cls, cert, ca_detail): + """Revoke a certificate.""" + return cls( + serial = cert.getSerial(), + expires = cert.getNotAfter(), + revoked = rpki.sundial.now(), + gctx = ca_detail.gctx, + ca_detail_id = ca_detail.ca_detail_id) diff --git a/rpkid.stable/rpki/sql.py b/rpkid.stable/rpki/sql.py new file mode 100644 index 00000000..e9284539 --- /dev/null +++ b/rpkid.stable/rpki/sql.py @@ -0,0 +1,295 @@ +"""SQL interface code. + +$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 MySQLdb, time, warnings, _mysql_exceptions +import rpki.x509, rpki.resource_set, rpki.sundial, rpki.log + +class session(object): + """SQL session layer.""" + + _exceptions_enabled = False + + def __init__(self, cfg): + + if not self._exceptions_enabled: + warnings.simplefilter("error", _mysql_exceptions.Warning) + self.__class__._exceptions_enabled = True + + self.username = cfg.get("sql-username") + self.database = cfg.get("sql-database") + self.password = cfg.get("sql-password") + + self.cache = {} + self.dirty = set() + + self.connect() + + def connect(self): + self.db = MySQLdb.connect(user = self.username, db = self.database, passwd = self.password) + self.cur = self.db.cursor() + + def close(self): + if self.cur: + self.cur.close() + self.cur = None + if self.db: + self.db.close() + self.db = None + + def ping(self): + return self.db.ping(True) + + def _wrap_execute(self, func, query, args): + try: + return func(query, args) + except _mysql_exceptions.MySQLError: + if self.dirty: + rpki.log.warn("MySQL exception with dirty objects in SQL cache!") + raise + + def execute(self, query, args = None): + return self._wrap_execute(self.cur.execute, query, args) + + def executemany(self, query, args): + return self._wrap_execute(self.cur.executemany, query, args) + + def fetchall(self): + return self.cur.fetchall() + + def lastrowid(self): + return self.cur.lastrowid + + def cache_clear(self): + """Clear the object cache.""" + self.cache.clear() + + def assert_pristine(self): + """Assert that there are no dirty objects in the cache.""" + assert not self.dirty, "Dirty objects in SQL cache: %s" % self.dirty + + def sweep(self): + """Write any dirty objects out to SQL.""" + for s in self.dirty.copy(): + rpki.log.debug("Sweeping %s" % repr(s)) + if s.sql_deleted: + s.sql_delete() + else: + s.sql_store() + self.assert_pristine() + +class template(object): + """SQL template generator.""" + def __init__(self, table_name, index_column, *data_columns): + """Build a SQL template.""" + type_map = dict((x[0],x[1]) for x in data_columns if isinstance(x, tuple)) + data_columns = tuple(isinstance(x, tuple) and x[0] or x for x in data_columns) + columns = (index_column,) + data_columns + self.table = table_name + self.index = index_column + self.columns = columns + self.map = type_map + self.select = "SELECT %s FROM %s" % (", ".join(columns), table_name) + self.insert = "INSERT %s (%s) VALUES (%s)" % (table_name, ", ".join(data_columns), + ", ".join("%(" + s + ")s" for s in data_columns)) + self.update = "UPDATE %s SET %s WHERE %s = %%(%s)s" % \ + (table_name, ", ".join(s + " = %(" + s + ")s" for s in data_columns), + index_column, index_column) + self.delete = "DELETE FROM %s WHERE %s = %%s" % (table_name, index_column) + +class sql_persistant(object): + """Mixin for persistant class that needs to be stored in SQL. + """ + + ## @var sql_in_db + # Whether this object is already in SQL or not. + + sql_in_db = False + + ## @var sql_deleted + # Whether our cached copy of this object has been deleted. + + sql_deleted = False + + ## @var sql_debug + # Enable logging of SQL actions + + sql_debug = False + + @classmethod + def sql_fetch(cls, gctx, id): + """Fetch one object from SQL, based on its primary key. + + Since in this one case we know that the primary index is also the + cache key, we check for a cache hit directly in the hope of + bypassing the SQL lookup entirely. + + This method is usually called via a one-line class-specific + wrapper. As a convenience, we also accept an id of None, and just + return None in this case. + """ + + if id is None: + return None + assert isinstance(id, (int, long)), "id should be an integer, was %s" % repr(type(id)) + key = (cls, id) + if key in gctx.sql.cache: + return gctx.sql.cache[key] + else: + return cls.sql_fetch_where1(gctx, "%s = %%s" % cls.sql_template.index, (id,)) + + @classmethod + def sql_fetch_where1(cls, gctx, where, args = None): + """Fetch one object from SQL, based on an arbitrary SQL WHERE expression.""" + results = cls.sql_fetch_where(gctx, where, args) + if len(results) == 0: + return None + elif len(results) == 1: + return results[0] + else: + raise rpki.exceptions.DBConsistancyError, \ + "Database contained multiple matches for %s where %s" % \ + (cls.__name__, where % tuple(repr(a) for a in args)) + + @classmethod + def sql_fetch_all(cls, gctx): + """Fetch all objects of this type from SQL.""" + return cls.sql_fetch_where(gctx, None) + + @classmethod + def sql_fetch_where(cls, gctx, where, args = None): + """Fetch objects of this type matching an arbitrary SQL WHERE expression.""" + if where is None: + assert args is None + if cls.sql_debug: + rpki.log.debug("sql_fetch_where(%s)" % repr(cls.sql_template.select)) + gctx.sql.execute(cls.sql_template.select) + else: + query = cls.sql_template.select + " WHERE " + where + if cls.sql_debug: + rpki.log.debug("sql_fetch_where(%s, %s)" % (repr(query), repr(args))) + gctx.sql.execute(query, args) + results = [] + for row in gctx.sql.fetchall(): + key = (cls, row[0]) + if key in gctx.sql.cache: + results.append(gctx.sql.cache[key]) + else: + results.append(cls.sql_init(gctx, row, key)) + return results + + @classmethod + def sql_init(cls, gctx, row, key): + """Initialize one Python object from the result of a SQL query.""" + self = cls() + self.gctx = gctx + self.sql_decode(dict(zip(cls.sql_template.columns, row))) + gctx.sql.cache[key] = self + self.sql_in_db = True + self.sql_fetch_hook() + return self + + def sql_mark_dirty(self): + """Mark this object as needing to be written back to SQL.""" + self.gctx.sql.dirty.add(self) + + def sql_mark_clean(self): + """Mark this object as not needing to be written back to SQL.""" + self.gctx.sql.dirty.discard(self) + + def sql_is_dirty(self): + """Query whether this object needs to be written back to SQL.""" + return self in self.gctx.sql.dirty + + def sql_mark_deleted(self): + """Mark this object as needing to be deleted in SQL.""" + self.sql_deleted = True + + def sql_store(self): + """Store this object to SQL.""" + args = self.sql_encode() + if not self.sql_in_db: + if self.sql_debug: + rpki.log.debug("sql_fetch_store(%s, %s)" % (repr(self.sql_template.insert), repr(args))) + self.gctx.sql.execute(self.sql_template.insert, args) + setattr(self, self.sql_template.index, self.gctx.sql.lastrowid()) + self.gctx.sql.cache[(self.__class__, self.gctx.sql.lastrowid())] = self + self.sql_insert_hook() + else: + if self.sql_debug: + rpki.log.debug("sql_fetch_store(%s, %s)" % (repr(self.sql_template.update), repr(args))) + self.gctx.sql.execute(self.sql_template.update, args) + self.sql_update_hook() + key = (self.__class__, getattr(self, self.sql_template.index)) + assert key in self.gctx.sql.cache and self.gctx.sql.cache[key] == self + self.sql_mark_clean() + self.sql_in_db = True + + def sql_delete(self): + """Delete this object from SQL.""" + if self.sql_in_db: + id = getattr(self, self.sql_template.index) + self.gctx.sql.execute(self.sql_template.delete, id) + self.sql_delete_hook() + key = (self.__class__, id) + if self.gctx.sql.cache.get(key) == self: + del self.gctx.sql.cache[key] + self.sql_in_db = False + self.sql_mark_clean() + + def sql_encode(self): + """Convert object attributes into a dict for use with canned SQL + queries. This is a default version that assumes a one-to-one + mapping between column names in SQL and attribute names in Python. + If you need something fancier, override this. + """ + d = dict((a, getattr(self, a, None)) for a in self.sql_template.columns) + for i in self.sql_template.map: + if d.get(i) is not None: + d[i] = self.sql_template.map[i].to_sql(d[i]) + return d + + def sql_decode(self, vals): + """Initialize an object with values returned by self.sql_fetch(). + This is a default version that assumes a one-to-one mapping + between column names in SQL and attribute names in Python. If you + need something fancier, override this. + """ + for a in self.sql_template.columns: + if vals.get(a) is not None and a in self.sql_template.map: + setattr(self, a, self.sql_template.map[a].from_sql(vals[a])) + else: + setattr(self, a, vals[a]) + + def sql_fetch_hook(self): + """Customization hook.""" + pass + + def sql_insert_hook(self): + """Customization hook.""" + pass + + def sql_update_hook(self): + """Customization hook.""" + self.sql_delete_hook() + self.sql_insert_hook() + + def sql_delete_hook(self): + """Customization hook.""" + pass + diff --git a/rpkid.stable/rpki/sundial.py b/rpkid.stable/rpki/sundial.py new file mode 100644 index 00000000..1d7ff8bf --- /dev/null +++ b/rpkid.stable/rpki/sundial.py @@ -0,0 +1,198 @@ +"""Unified RPKI date/time handling, based on the standard Python datetime module. + +Module name chosen to sidestep a nightmare of import-related errors +that occur with the more obvious module names. + +$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 datetime as pydatetime +import re + +def now(): + """Get current timestamp.""" + return datetime.utcnow() + +class datetime(pydatetime.datetime): + """RPKI extensions to standard datetime.datetime class. All work + here is in UTC, so we use naive datetime objects. + """ + + def totimestamp(self): + """Convert to seconds from epoch (like time.time()). Conversion + method is a bit silly, but avoids time module timezone whackiness. + """ + return int(self.strftime("%s")) + + @classmethod + def fromUTCTime(cls, x): + """Convert from ASN.1 UTCTime.""" + return cls.strptime(x, "%y%m%d%H%M%SZ") + + def toUTCTime(self): + """Convert to ASN.1 UTCTime.""" + return self.strftime("%y%m%d%H%M%SZ") + + @classmethod + def fromGeneralizedTime(cls, x): + """Convert from ASN.1 GeneralizedTime.""" + return cls.strptime(x, "%Y%m%d%H%M%SZ") + + def toGeneralizedTime(self): + """Convert to ASN.1 GeneralizedTime.""" + return self.strftime("%Y%m%d%H%M%SZ") + + @classmethod + def fromASN1tuple(cls, x): + """Convert from ASN.1 tuple representation.""" + assert isinstance(x, tuple) and len(x) == 2 and x[0] in ("utcTime", "generalTime") + if x[0] == "utcTime": + return cls.fromUTCTime(x[1]) + else: + return cls.fromGeneralizedTime(x[1]) + + ## @var PKIX_threshhold + # Threshold specified in RFC 3280 for switchover from UTCTime to GeneralizedTime. + + PKIX_threshhold = pydatetime.datetime(2050, 1, 1) + + def toASN1tuple(self): + """Convert to ASN.1 tuple representation.""" + if self < self.PKIX_threshhold: + return "utcTime", self.toUTCTime() + else: + return "generalTime", self.toGeneralizedTime() + + @classmethod + def fromXMLtime(cls, x): + """Convert from XML time representation.""" + if x is None: + return None + else: + return cls.strptime(x, "%Y-%m-%dT%H:%M:%SZ") + + def toXMLtime(self): + """Convert to XML time representation.""" + return self.strftime("%Y-%m-%dT%H:%M:%SZ") + + def __str__(self): + return self.toXMLtime() + + @classmethod + def fromdatetime(cls, x): + """Convert a datetime.datetime object into this subclass. + This is whacky due to the weird constructors for datetime. + """ + return cls.combine(x.date(), x.time()) + + def __add__(self, other): + """Force correct class for timedelta results.""" + return self.fromdatetime(pydatetime.datetime.__add__(self, other)) + + def __sub__(self, other): + """Force correct class for timedelta results.""" + return self.fromdatetime(pydatetime.datetime.__sub__(self, other)) + + @classmethod + def from_sql(cls, x): + """Convert from SQL storage format.""" + return cls.fromdatetime(x) + + def to_sql(self): + """Convert to SQL storage format. + + There's something whacky going on in the MySQLdb module, it throws + range errors when storing a derived type into a DATETIME column. + Investigate some day, but for now brute force this by copying the + relevant fields into a datetime.datetime for MySQLdb's + consumption. + + """ + return pydatetime.datetime(year = self.year, month = self.month, day = self.day, + hour = self.hour, minute = self.minute, second = self.second, + microsecond = 0, tzinfo = None) + + def later(self, other): + """Return the later of two timestamps.""" + return other if other > self else self + + def earlier(self, other): + """Return the earlier of two timestamps.""" + return other if other < self else self + +class timedelta(pydatetime.timedelta): + """Timedelta with text parsing. This accepts two input formats: + + - A simple integer, indicating a number of seconds. + + - A string of the form "wD xH yM zS" where w, x, y, and z are integers + and D, H, M, and S indicate days, hours, minutes, and seconds. + All of the fields are optional, but at least one must be specified. + Eg, "3D4H" means "three days plus four hours". + """ + + ## @var regexp + # Hideously ugly regular expression to parse the complex text form. + # Tags are intended for use with re.MatchObject.groupdict() and map + # directly to the keywords expected by the timedelta constructor. + + regexp = re.compile("\\s*".join(("^", + "(?:(?P<days>\\d+)D)?", + "(?:(?P<hours>\\d+)H)?", + "(?:(?P<minutes>\\d+)M)?", + "(?:(?P<seconds>\\d+)S)?", + "$")), + re.I) + + @classmethod + def parse(cls, arg): + """Parse text into a timedelta object.""" + if not isinstance(arg, str): + return cls(seconds = arg) + elif arg.isdigit(): + return cls(seconds = int(arg)) + else: + match = cls.regexp.match(arg) + if match: + return cls(**dict((k, int(v)) for (k, v) in match.groupdict().items() if v is not None)) + else: + raise RuntimeError, "Couldn't parse timedelta %s" % repr(arg) + + + def convert_to_seconds(self): + """Convert a timedelta interval to seconds.""" + return self.days * 24 * 60 * 60 + self.seconds + +if __name__ == "__main__": + + def test(t): + print + print "str: ", t + print "repr: ", repr(t) + print "seconds since epoch:", t.strftime("%s") + print "UTCTime: ", t.toUTCTime() + print "GeneralizedTime: ", t.toGeneralizedTime() + print "ASN1tuple: ", t.toASN1tuple() + print "XMLtime: ", t.toXMLtime() + print + + print + print "Testing time conversion routines" + test(now()) + test(now() + timedelta(days = 30)) + test(now() + timedelta.parse("3d5s")) + timedelta.parse(" 3d 5s ") diff --git a/rpkid.stable/rpki/up_down.py b/rpkid.stable/rpki/up_down.py new file mode 100644 index 00000000..30085390 --- /dev/null +++ b/rpkid.stable/rpki/up_down.py @@ -0,0 +1,535 @@ +"""RPKI "up-down" 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 +import rpki.resource_set, rpki.x509, rpki.exceptions +import rpki.xml_utils, rpki.relaxng + +xmlns="http://www.apnic.net/specs/rescerts/up-down/" + +nsmap = { None : xmlns } + +class base_elt(object): + """Generic PDU object. + + Virtual class, just provides some default methods. + """ + + def startElement(self, stack, name, attrs): + """Ignore startElement() if there's no specific handler. + + Some elements have no attributes and we only care about their + text content. + """ + pass + + def endElement(self, stack, name, text): + """Ignore endElement() if there's no specific handler. + + If we don't need to do anything else, just pop the stack. + """ + stack.pop() + + def make_elt(self, name, *attrs): + """Construct a element, copying over a set of attributes.""" + elt = lxml.etree.Element("{%s}%s" % (xmlns, name), nsmap=nsmap) + for key in attrs: + val = getattr(self, key, None) + if val is not None: + elt.set(key, str(val)) + return elt + + def make_b64elt(self, elt, name, value=None): + """Construct a sub-element with Base64 text content.""" + if value is None: + value = getattr(self, name, None) + if value is not None: + lxml.etree.SubElement(elt, "{%s}%s" % (xmlns, name), nsmap=nsmap).text = base64.b64encode(value) + + def serve_pdu(self, q_msg, r_msg, child): + """Default PDU handler to catch unexpected types.""" + raise rpki.exceptions.BadQuery, "Unexpected query type %s" % q_msg.type + + def check_response(self): + """Placeholder for response checking.""" + pass + +class multi_uri(list): + """Container for a set of URIs.""" + + def __init__(self, ini): + """Initialize a set of URIs, which includes basic some syntax checking.""" + if isinstance(ini, (list, tuple)): + self[:] = ini + elif isinstance(ini, str): + self[:] = ini.split(",") + for s in self: + if s.strip() != s or s.find("://") < 0: + raise rpki.exceptions.BadURISyntax, "Bad URI \"%s\"" % s + else: + raise TypeError + + def __str__(self): + """Convert a multi_uri back to a string representation.""" + return ",".join(self) + + def rsync(self): + """Find first rsync://... URI in self.""" + for s in self: + if s.startswith("rsync://"): + return s + return None + +class certificate_elt(base_elt): + """Up-Down protocol representation of an issued certificate.""" + + def startElement(self, stack, name, attrs): + """Handle attributes of <certificate/> element.""" + assert name == "certificate", "Unexpected name %s, stack %s" % (name, stack) + self.cert_url = multi_uri(attrs["cert_url"]) + self.req_resource_set_as = rpki.resource_set.resource_set_as(attrs.get("req_resource_set_as")) + self.req_resource_set_ipv4 = rpki.resource_set.resource_set_ipv4(attrs.get("req_resource_set_ipv4")) + self.req_resource_set_ipv6 = rpki.resource_set.resource_set_ipv6(attrs.get("req_resource_set_ipv6")) + + def endElement(self, stack, name, text): + """Handle text content of a <certificate/> element.""" + assert name == "certificate", "Unexpected name %s, stack %s" % (name, stack) + self.cert = rpki.x509.X509(Base64=text) + stack.pop() + + def toXML(self): + """Generate a <certificate/> element.""" + elt = self.make_elt("certificate", "cert_url", + "req_resource_set_as", "req_resource_set_ipv4", "req_resource_set_ipv6") + elt.text = self.cert.get_Base64() + return elt + +class class_elt(base_elt): + """Up-Down protocol representation of a resource class.""" + + issuer = None + + def __init__(self): + """Initialize class_elt.""" + self.certs = [] + + def startElement(self, stack, name, attrs): + """Handle <class/> elements and their children.""" + if name == "certificate": + cert = certificate_elt() + self.certs.append(cert) + stack.append(cert) + cert.startElement(stack, name, attrs) + elif name != "issuer": + assert name == "class", "Unexpected name %s, stack %s" % (name, stack) + self.class_name = attrs["class_name"] + self.cert_url = multi_uri(attrs["cert_url"]) + self.suggested_sia_head = attrs.get("suggested_sia_head") + self.resource_set_as = rpki.resource_set.resource_set_as(attrs["resource_set_as"]) + self.resource_set_ipv4 = rpki.resource_set.resource_set_ipv4(attrs["resource_set_ipv4"]) + self.resource_set_ipv6 = rpki.resource_set.resource_set_ipv6(attrs["resource_set_ipv6"]) + self.resource_set_notafter = rpki.sundial.datetime.fromXMLtime(attrs.get("resource_set_notafter")) + + def endElement(self, stack, name, text): + """Handle <class/> elements and their children.""" + if name == "issuer": + self.issuer = rpki.x509.X509(Base64=text) + else: + assert name == "class", "Unexpected name %s, stack %s" % (name, stack) + stack.pop() + + def toXML(self): + """Generate a <class/> element.""" + elt = self.make_elt("class", "class_name", "cert_url", "resource_set_as", + "resource_set_ipv4", "resource_set_ipv6", + "resource_set_notafter", "suggested_sia_head") + elt.extend([i.toXML() for i in self.certs]) + if self.issuer is not None: + self.make_b64elt(elt, "issuer", self.issuer.get_DER()) + return elt + + def to_resource_bag(self): + """Build a resource_bag from from this <class/> element.""" + return rpki.resource_set.resource_bag(self.resource_set_as, + self.resource_set_ipv4, + self.resource_set_ipv6, + self.resource_set_notafter) + + def from_resource_bag(self, bag): + """Set resources of this class element from a resource_bag.""" + self.resource_set_as = bag.asn + self.resource_set_ipv4 = bag.v4 + self.resource_set_ipv6 = bag.v6 + self.resource_set_notafter = bag.valid_until + +class list_pdu(base_elt): + """Up-Down protocol "list" PDU.""" + + def toXML(self): + """Generate (empty) payload of "list" PDU.""" + return [] + + def serve_pdu(self, q_msg, r_msg, child): + """Serve one "list" PDU.""" + r_msg.payload = list_response_pdu() + + # This will require a callback when we go event-driven + irdb_resources = self.gctx.irdb_query(child.self_id, child.child_id) + + for parent in child.parents(): + for ca in parent.cas(): + ca_detail = ca.fetch_active() + if not ca_detail: + continue + resources = ca_detail.latest_ca_cert.get_3779resources().intersection(irdb_resources) + if resources.empty(): + continue + rc = class_elt() + rc.class_name = str(ca.ca_id) + rc.cert_url = multi_uri(ca_detail.ca_cert_uri) + rc.from_resource_bag(resources) + for child_cert in child.child_certs(ca_detail = ca_detail): + c = certificate_elt() + c.cert_url = multi_uri(child_cert.uri(ca)) + c.cert = child_cert.cert + rc.certs.append(c) + rc.issuer = ca_detail.latest_ca_cert + r_msg.payload.classes.append(rc) + + @classmethod + def query(cls, parent): + """Send a "list" query to parent.""" + return parent.query_up_down(cls()) + +class class_response_syntax(base_elt): + """Syntax for Up-Down protocol "list_response" and "issue_response" PDUs.""" + + def __init__(self): + """Initialize class_response_syntax.""" + self.classes = [] + + def startElement(self, stack, name, attrs): + """Handle "list_response" and "issue_response" PDUs.""" + assert name == "class", "Unexpected name %s, stack %s" % (name, stack) + c = class_elt() + self.classes.append(c) + stack.append(c) + c.startElement(stack, name, attrs) + + def toXML(self): + """Generate payload of "list_response" and "issue_response" PDUs.""" + return [c.toXML() for c in self.classes] + +class list_response_pdu(class_response_syntax): + """Up-Down protocol "list_response" PDU.""" + + pass + +class issue_pdu(base_elt): + """Up-Down protocol "issue" PDU.""" + + def startElement(self, stack, name, attrs): + """Handle "issue" PDU.""" + assert name == "request", "Unexpected name %s, stack %s" % (name, stack) + self.class_name = attrs["class_name"] + self.req_resource_set_as = rpki.resource_set.resource_set_as(attrs.get("req_resource_set_as")) + self.req_resource_set_ipv4 = rpki.resource_set.resource_set_ipv4(attrs.get("req_resource_set_ipv4")) + self.req_resource_set_ipv6 = rpki.resource_set.resource_set_ipv6(attrs.get("req_resource_set_ipv6")) + + def endElement(self, stack, name, text): + """Handle "issue" PDU.""" + assert name == "request", "Unexpected name %s, stack %s" % (name, stack) + self.pkcs10 = rpki.x509.PKCS10(Base64=text) + stack.pop() + + def toXML(self): + """Generate payload of "issue" PDU.""" + elt = self.make_elt("request", "class_name", "req_resource_set_as", + "req_resource_set_ipv4", "req_resource_set_ipv6") + elt.text = self.pkcs10.get_Base64() + return [elt] + + def serve_pdu(self, q_msg, r_msg, child): + """Serve one issue request PDU.""" + + # Subsetting not yet implemented, this is the one place where we + # have to handle it, by reporting that we're lame. + + if self.req_resource_set_as or \ + self.req_resource_set_ipv4 or \ + self.req_resource_set_ipv6: + raise rpki.exceptions.NotImplementedYet, "req_* attributes not implemented yet, sorry" + + # Check the request + self.pkcs10.check_valid_rpki() + ca = child.ca_from_class_name(self.class_name) + ca_detail = ca.fetch_active() + if ca_detail is None: + raise rpki.exceptions.NoActiveCA, "No active CA for class %s" % repr(self.class_name) + + # Check current cert, if any + + # This will require a callback when we go event-driven + irdb_resources = self.gctx.irdb_query(child.self_id, child.child_id) + + resources = irdb_resources.intersection(ca_detail.latest_ca_cert.get_3779resources()) + req_key = self.pkcs10.getPublicKey() + req_sia = self.pkcs10.get_SIA() + child_cert = child.child_certs(ca_detail = ca_detail, ski = req_key.get_SKI(), unique = True) + + # Generate new cert or regenerate old one if necessary + + if child_cert is None: + child_cert = ca_detail.issue( + ca = ca, + child = child, + subject_key = req_key, + sia = req_sia, + resources = resources) + else: + child_cert = child_cert.reissue( + ca_detail = ca_detail, + sia = req_sia, + resources = resources) + + # Save anything we modified and generate response + self.gctx.sql.sweep() + assert child_cert and child_cert.sql_in_db + c = certificate_elt() + c.cert_url = multi_uri(child_cert.uri(ca)) + c.cert = child_cert.cert + rc = class_elt() + rc.class_name = self.class_name + rc.cert_url = multi_uri(ca_detail.ca_cert_uri) + rc.from_resource_bag(resources) + rc.certs.append(c) + rc.issuer = ca_detail.latest_ca_cert + r_msg.payload = issue_response_pdu() + r_msg.payload.classes.append(rc) + + @classmethod + def query(cls, parent, ca, ca_detail): + """Send an "issue" request to parent associated with ca.""" + assert ca_detail is not None and ca_detail.state in ("pending", "active") + sia = ((rpki.oids.name2oid["id-ad-caRepository"], ("uri", ca.sia_uri)), + (rpki.oids.name2oid["id-ad-rpkiManifest"], ("uri", ca_detail.manifest_uri(ca)))) + self = cls() + self.class_name = ca.parent_resource_class + self.pkcs10 = rpki.x509.PKCS10.create_ca(ca_detail.private_key_id, sia) + return parent.query_up_down(self) + +class issue_response_pdu(class_response_syntax): + """Up-Down protocol "issue_response" PDU.""" + + def check_response(self): + """Check whether this looks like a reasonable issue_response PDU. + XML schema should be tighter for this response. + """ + if len(self.classes) != 1 or len(self.classes[0].certs) != 1: + raise rpki.exceptions.BadIssueResponse + +class revoke_syntax(base_elt): + """Syntax for Up-Down protocol "revoke" and "revoke_response" PDUs.""" + + def startElement(self, stack, name, attrs): + """Handle "revoke" PDU.""" + self.class_name = attrs["class_name"] + self.ski = attrs["ski"] + + def toXML(self): + """Generate payload of "revoke" PDU.""" + return [self.make_elt("key", "class_name", "ski")] + +class revoke_pdu(revoke_syntax): + """Up-Down protocol "revoke" PDU.""" + + def get_SKI(self): + """Convert g(SKI) encoding from PDU back to raw SKI.""" + return base64.urlsafe_b64decode(self.ski + "=") + + def serve_pdu(self, q_msg, r_msg, child): + """Serve one revoke request PDU.""" + for ca_detail in child.ca_from_class_name(self.class_name).ca_details(): + for child_cert in child.child_certs(ca_detail = ca_detail, ski = self.get_SKI()): + child_cert.revoke() + self.gctx.sql.sweep() + r_msg.payload = revoke_response_pdu() + r_msg.payload.class_name = self.class_name + r_msg.payload.ski = self.ski + + @classmethod + def query(cls, ca_detail): + """Send a "revoke" request to parent associated with ca_detail.""" + ca = ca_detail.ca() + parent = ca.parent() + self = cls() + self.class_name = ca.parent_resource_class + self.ski = ca_detail.latest_ca_cert.gSKI() + return parent.query_up_down(self) + +class revoke_response_pdu(revoke_syntax): + """Up-Down protocol "revoke_response" PDU.""" + + pass + +class error_response_pdu(base_elt): + """Up-Down protocol "error_response" PDU.""" + + codes = { + 1101 : "Already processing request", + 1102 : "Version number error", + 1103 : "Unrecognised request type", + 1201 : "Request - no such resource class", + 1202 : "Request - no resources allocated in resource class", + 1203 : "Request - badly formed certificate request", + 1301 : "Revoke - no such resource class", + 1302 : "Revoke - no such key", + 2001 : "Internal Server Error - Request not performed" } + + exceptions = {} + + def __init__(self, exception = None): + """Initialize an error_response PDU from an exception object.""" + if exception is not None: + if exception in self.exceptions: + self.status = exceptions[exception] + else: + self.status = 2001 + self.description = str(exception) + + def endElement(self, stack, name, text): + """Handle "error_response" PDU.""" + if name == "status": + code = int(text) + if code not in self.codes: + raise rpki.exceptions.BadStatusCode, "%s is not a known status code" % code + self.status = code + elif name == "description": + self.description = text + else: + assert name == "message", "Unexpected name %s, stack %s" % (name, stack) + stack.pop() + stack[-1].endElement(stack, name, text) + + def toXML(self): + """Generate payload of "error_response" PDU.""" + assert self.status in self.codes + elt = self.make_elt("status") + elt.text = str(self.status) + payload = [elt] + if self.description: + elt = self.make_elt("description") + elt.text = str(self.description) + elt.set("{http://www.w3.org/XML/1998/namespace}lang", "en-US") + payload.append(elt) + return payload + + def check_response(self): + """Handle an error response. For now, just raise an exception, + perhaps figure out something more clever to do later. + """ + raise rpki.exceptions.UpstreamError, self.codes[self.status] + +class message_pdu(base_elt): + """Up-Down protocol message wrapper PDU.""" + + version = 1 + + name2type = { + "list" : list_pdu, + "list_response" : list_response_pdu, + "issue" : issue_pdu, + "issue_response" : issue_response_pdu, + "revoke" : revoke_pdu, + "revoke_response" : revoke_response_pdu, + "error_response" : error_response_pdu } + + type2name = dict((v,k) for k,v in name2type.items()) + + def toXML(self): + """Generate payload of message PDU.""" + elt = self.make_elt("message", "version", "sender", "recipient", "type") + elt.extend(self.payload.toXML()) + return elt + + def startElement(self, stack, name, attrs): + """Handle message PDU. + + Payload of the <message/> element varies depending on the "type" + attribute, so after some basic checks we have to instantiate the + right class object to handle whatever kind of PDU this is. + """ + assert name == "message", "Unexpected name %s, stack %s" % (name, stack) + assert self.version == int(attrs["version"]) + self.sender = attrs["sender"] + self.recipient = attrs["recipient"] + self.type = attrs["type"] + self.payload = self.name2type[attrs["type"]]() + stack.append(self.payload) + + def __str__(self): + """Convert a message PDU to a string.""" + lxml.etree.tostring(self.toXML(), pretty_print = True, encoding = "UTF-8") + + def serve_top_level(self, child): + """Serve one message request PDU.""" + r_msg = message_pdu() + r_msg.sender = self.recipient + r_msg.recipient = self.sender + self.payload.serve_pdu(self, r_msg, child) + r_msg.type = self.type2name[type(r_msg.payload)] + return r_msg + + def serve_error(self, exception): + """Generate an error_response message PDU.""" + r_msg = message_pdu() + r_msg.sender = self.recipient + r_msg.recipient = self.sender + r_msg.payload = error_response_pdu(exception) + r_msg.type = self.type2name[type(r_msg.payload)] + return r_msg + + @classmethod + def make_query(cls, payload, sender, recipient): + """Construct one message PDU.""" + assert not cls.type2name[type(payload)].endswith("_response") + if sender is None: + sender = "tweedledee" + if recipient is None: + recipient = "tweedledum" + self = cls() + self.sender = sender + self.recipient = recipient + self.payload = payload + self.type = self.type2name[type(payload)] + return self + +class sax_handler(rpki.xml_utils.sax_handler): + """SAX handler for Up-Down protocol.""" + + pdu = message_pdu + name = "message" + version = "1" + +class cms_msg(rpki.x509.XML_CMS_object): + """Class to hold a CMS-signed up-down PDU.""" + + encoding = "UTF-8" + schema = rpki.relaxng.up_down + saxify = sax_handler.saxify diff --git a/rpkid.stable/rpki/x509.py b/rpkid.stable/rpki/x509.py new file mode 100644 index 00000000..b167560c --- /dev/null +++ b/rpkid.stable/rpki/x509.py @@ -0,0 +1,995 @@ +"""One X.509 implementation to rule them all... + +...and in the darkness hide the twisty maze of partially overlapping +X.509 support packages in Python. + +There are several existing packages, none of which do quite what I +need, due to age, lack of documentation, specialization, or lack of +foresight on somebody's part (perhaps mine). This module attempts to +bring together the functionality I need in a way that hides at least +some of the nasty details. This involves a lot of format conversion. + +$Id$ + + +Copyright (C) 2009 Internet Systems Consortium ("ISC") + +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 ISC DISCLAIMS ALL WARRANTIES WITH +REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY +AND FITNESS. IN NO EVENT SHALL ISC 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. + + +Portions 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 POW, tlslite.api, POW.pkix, base64, lxml.etree, os +import rpki.exceptions, rpki.resource_set, rpki.oids, rpki.sundial +import rpki.manifest, rpki.roa, rpki.log + +def calculate_SKI(public_key_der): + """Calculate the SKI value given the DER representation of a public + key, which requires first peeling the ASN.1 wrapper off the key. + """ + k = POW.pkix.SubjectPublicKeyInfo() + k.fromString(public_key_der) + d = POW.Digest(POW.SHA1_DIGEST) + d.update(k.subjectPublicKey.get()) + return d.digest() + +class PEM_converter(object): + """Convert between DER and PEM encodings for various kinds of ASN.1 data.""" + + def __init__(self, kind): # "CERTIFICATE", "RSA PRIVATE KEY", ... + """Initialize PEM_converter.""" + self.b = "-----BEGIN %s-----" % kind + self.e = "-----END %s-----" % kind + + def looks_like_PEM(self, text): + """Guess whether text looks like a PEM encoding.""" + b = text.find(self.b) + return b >= 0 and text.find(self.e) > b + len(self.b) + + def to_DER(self, pem): + """Convert from PEM to DER.""" + lines = [line.strip() for line in pem.splitlines(0)] + while lines and lines.pop(0) != self.b: + pass + while lines and lines.pop(-1) != self.e: + pass + if not lines: + raise rpki.exceptions.EmptyPEM, "Could not find PEM in:\n%s" % pem + return base64.b64decode("".join(lines)) + + def to_PEM(self, der): + """Convert from DER to PEM.""" + b64 = base64.b64encode(der) + pem = self.b + "\n" + while len(b64) > 64: + pem += b64[0:64] + "\n" + b64 = b64[64:] + return pem + b64 + "\n" + self.e + "\n" + +class DER_object(object): + """Virtual class to hold a generic DER object.""" + + ## Formats supported in this object + formats = ("DER",) + + ## PEM converter for this object + pem_converter = None + + ## Other attributes that self.clear() should whack + other_clear = () + + ## @var DER + ## DER value of this object + + def empty(self): + """Test whether this object is empty.""" + for a in self.formats: + if getattr(self, a, None) is not None: + return False + return True + + def clear(self): + """Make this object empty.""" + for a in self.formats + self.other_clear: + setattr(self, a, None) + + def __init__(self, **kw): + """Initialize a DER_object.""" + self.clear() + if len(kw): + self.set(**kw) + + def set(self, **kw): + """Set this object by setting one of its known formats. + + This method only allows one to set one format at a time. + Subsequent calls will clear the object first. The point of all + this is to let the object's internal converters handle mustering + the object into whatever format you need at the moment. + """ + if len(kw) == 1: + name = kw.keys()[0] + if name in self.formats: + self.clear() + setattr(self, name, kw[name]) + return + if name == "PEM": + self.clear() + self.DER = self.pem_converter.to_DER(kw[name]) + return + if name == "Base64": + self.clear() + self.DER = base64.b64decode(kw[name]) + return + if name in ("PEM_file", "DER_file", "Auto_file"): + f = open(kw[name], "rb") + value = f.read() + f.close() + if name == "PEM_file" or (name == "Auto_file" and self.pem_converter.looks_like_PEM(value)): + value = self.pem_converter.to_DER(value) + self.clear() + self.DER = value + return + raise rpki.exceptions.DERObjectConversionError, "Can't honor conversion request %s" % repr(kw) + + def get_DER(self): + """Get the DER value of this object. + + Subclasses will almost certainly override this method. + """ + assert not self.empty() + if self.DER: + return self.DER + raise rpki.exceptions.DERObjectConversionError, "No conversion path to DER available" + + def get_Base64(self): + """Get the Base64 encoding of the DER value of this object.""" + return base64.b64encode(self.get_DER()) + + def get_PEM(self): + """Get the PEM representation of this object.""" + return self.pem_converter.to_PEM(self.get_DER()) + + def __cmp__(self, other): + """Compare two DER-encoded objects.""" + return cmp(self.get_DER(), other.get_DER()) + + def hSKI(self): + """Return hexadecimal string representation of SKI for this + object. Only work for subclasses that implement get_SKI(). + """ + ski = self.get_SKI() + return ":".join(("%02X" % ord(i) for i in ski)) if ski else "" + + def gSKI(self): + """Calculate g(SKI) for this object. Only work for subclasses + that implement get_SKI(). + """ + return base64.urlsafe_b64encode(self.get_SKI()).rstrip("=") + + def hAKI(self): + """Return hexadecimal string representation of AKI for this + object. Only work for subclasses that implement get_AKI(). + """ + aki = self.get_AKI() + return ":".join(("%02X" % ord(i) for i in aki)) if aki else "" + + def gAKI(self): + """Calculate g(AKI) for this object. Only work for subclasses + that implement get_AKI(). + """ + return base64.urlsafe_b64encode(self.get_AKI()).rstrip("=") + + def get_AKI(self): + """Get the AKI extension from this object. Only works for subclasses that support getExtension().""" + aki = (self.get_POWpkix().getExtension(rpki.oids.name2oid["authorityKeyIdentifier"]) or ((), 0, None))[2] + return aki[0] if isinstance(aki, tuple) else aki + + def get_SKI(self): + """Get the SKI extension from this object. Only works for subclasses that support getExtension().""" + return (self.get_POWpkix().getExtension(rpki.oids.name2oid["subjectKeyIdentifier"]) or ((), 0, None))[2] + + def get_SIA(self): + """Get the SIA extension from this object. Only works for subclasses that support getExtension().""" + return (self.get_POWpkix().getExtension(rpki.oids.name2oid["subjectInfoAccess"]) or ((), 0, None))[2] + + def get_AIA(self): + """Get the SIA extension from this object. Only works for subclasses that support getExtension().""" + return (self.get_POWpkix().getExtension(rpki.oids.name2oid["subjectInfoAccess"]) or ((), 0, None))[2] + + def get_basicConstraints(self): + """Get the basicConstraints extension from this object. Only works for subclasses that support getExtension().""" + return (self.get_POWpkix().getExtension(rpki.oids.name2oid["basicConstraints"]) or ((), 0, None))[2] + + def is_CA(self): + """Return True if and only if object has the basicConstraints extension and its cA value is true.""" + basicConstraints = self.get_basicConstraints() + return basicConstraints and basicConstraints[0] != 0 + + def get_3779resources(self): + """Get RFC 3779 resources as rpki.resource_set objects. + Only works for subclasses that support getExtensions(). + """ + resources = rpki.resource_set.resource_bag.from_rfc3779_tuples(self.get_POWpkix().getExtensions()) + try: + resources.valid_until = self.getNotAfter() + except AttributeError: + pass + return resources + + @classmethod + def from_sql(cls, x): + """Convert from SQL storage format.""" + return cls(DER = x) + + def to_sql(self): + """Convert to SQL storage format.""" + return self.get_DER() + + def dumpasn1(self): + """Pretty print an ASN.1 DER object using cryptlib dumpasn1 tool. + Use a temporary file rather than popen4() because dumpasn1 uses + seek() when decoding ASN.1 content nested in OCTET STRING values. + """ + + ret = None + fn = "dumpasn1.tmp" + try: + f = open(fn, "wb") + f.write(self.get_DER()) + f.close() + f = os.popen("dumpasn1 2>&1 -a " + fn) + ret = "\n".join(x for x in f.read().splitlines() if x.startswith(" ")) + f.close() + finally: + os.unlink(fn) + return ret + +class X509(DER_object): + """X.509 certificates. + + This class is designed to hold all the different representations of + X.509 certs we're using and convert between them. X.509 support in + Python a nasty maze of half-cooked stuff (except perhaps for + cryptlib, which is just different). Users of this module should not + have to care about this implementation nightmare. + """ + + formats = ("DER", "POW", "POWpkix", "tlslite") + pem_converter = PEM_converter("CERTIFICATE") + + def get_DER(self): + """Get the DER value of this certificate.""" + assert not self.empty() + if self.DER: + return self.DER + if self.POW: + self.DER = self.POW.derWrite() + return self.get_DER() + if self.POWpkix: + self.DER = self.POWpkix.toString() + return self.get_DER() + if self.tlslite: + der = self.tlslite.writeBytes() + if not isinstance(der, str): # Apparently sometimes tlslite strings aren't strings, + der = der.tostring() # then again somtimes they are. Isn't that special? + self.DER = der + return self.get_DER() + raise rpki.exceptions.DERObjectConversionError, "No conversion path to DER available" + + def get_POW(self): + """Get the POW value of this certificate.""" + assert not self.empty() + if not self.POW: + self.POW = POW.derRead(POW.X509_CERTIFICATE, self.get_DER()) + return self.POW + + def get_POWpkix(self): + """Get the POW.pkix value of this certificate.""" + assert not self.empty() + if not self.POWpkix: + cert = POW.pkix.Certificate() + cert.fromString(self.get_DER()) + self.POWpkix = cert + return self.POWpkix + + def get_tlslite(self): + """Get the tlslite value of this certificate.""" + assert not self.empty() + if not self.tlslite: + cert = tlslite.api.X509() + cert.parseBinary(self.get_DER()) + self.tlslite = cert + return self.tlslite + + def getIssuer(self): + """Get the issuer of this certificate.""" + return self.get_POW().getIssuer() + + def getSubject(self): + """Get the subject of this certificate.""" + return self.get_POW().getSubject() + + def getNotBefore(self): + """Get the inception time of this certificate.""" + return rpki.sundial.datetime.fromASN1tuple(self.get_POWpkix().tbs.validity.notBefore.get()) + + def getNotAfter(self): + """Get the expiration time of this certificate.""" + return rpki.sundial.datetime.fromASN1tuple(self.get_POWpkix().tbs.validity.notAfter.get()) + + def getSerial(self): + """Get the serial number of this certificate.""" + return self.get_POW().getSerial() + + def getPublicKey(self): + """Extract the public key from this certificate.""" + return RSApublic(DER = self.get_POWpkix().tbs.subjectPublicKeyInfo.toString()) + + def expired(self): + """Test whether this certificate has expired.""" + return self.getNotAfter() <= rpki.sundial.now() + + def issue(self, keypair, subject_key, serial, sia, aia, crldp, notAfter, + cn = None, resources = None, is_ca = True): + """Issue a certificate.""" + + now = rpki.sundial.now() + aki = self.get_SKI() + ski = subject_key.get_SKI() + + if cn is None: + cn = "".join(("%02X" % ord(i) for i in ski)) + + # if notAfter is None: notAfter = now + rpki.sundial.timedelta(days = 30) + + cert = POW.pkix.Certificate() + cert.setVersion(2) + cert.setSerial(serial) + cert.setIssuer(self.get_POWpkix().getSubject()) + cert.setSubject((((rpki.oids.name2oid["commonName"], ("printableString", cn)),),)) + cert.setNotBefore(now.toASN1tuple()) + cert.setNotAfter(notAfter.toASN1tuple()) + cert.tbs.subjectPublicKeyInfo.fromString(subject_key.get_DER()) + + exts = [ ["subjectKeyIdentifier", False, ski], + ["authorityKeyIdentifier", False, (aki, (), None)], + ["cRLDistributionPoints", False, ((("fullName", (("uri", crldp),)), None, ()),)], + ["authorityInfoAccess", False, ((rpki.oids.name2oid["id-ad-caIssuers"], ("uri", aia)),)], + ["certificatePolicies", True, ((rpki.oids.name2oid["id-cp-ipAddr-asNumber"], ()),)] ] + + if is_ca: + exts.append(["basicConstraints", True, (1, None)]) + exts.append(["keyUsage", True, (0, 0, 0, 0, 0, 1, 1)]) + else: + exts.append(["keyUsage", True, (1,)]) + + if sia is not None: + exts.append(["subjectInfoAccess", False, sia]) + else: + assert not is_ca + + if resources is not None and resources.asn: + exts.append(["sbgp-autonomousSysNum", True, (resources.asn.to_rfc3779_tuple(), None)]) + + if resources is not None and (resources.v4 or resources.v6): + exts.append(["sbgp-ipAddrBlock", True, [x for x in (resources.v4.to_rfc3779_tuple(), resources.v6.to_rfc3779_tuple()) if x is not None]]) + + for x in exts: + x[0] = rpki.oids.name2oid[x[0]] + cert.setExtensions(exts) + + cert.sign(keypair.get_POW(), POW.SHA256_DIGEST) + + return X509(POWpkix = cert) + + @classmethod + def normalize_chain(cls, chain): + """Normalize a chain of certificates into a tuple of X509 objects. + Given all the glue certificates needed for BPKI cross + certification, it's easiest to allow sloppy arguments to the HTTPS + and CMS validation methods and provide a single method that + normalizes the allowed cases. So this method allows X509, None, + lists, and tuples, and returns a tuple of X509 objects. + """ + if isinstance(chain, cls): + chain = (chain,) + return tuple(x for x in chain if x is not None) + +class PKCS10(DER_object): + """Class to hold a PKCS #10 request.""" + + formats = ("DER", "POWpkix") + pem_converter = PEM_converter("CERTIFICATE REQUEST") + + def get_DER(self): + """Get the DER value of this certification request.""" + assert not self.empty() + if self.DER: + return self.DER + if self.POWpkix: + self.DER = self.POWpkix.toString() + return self.get_DER() + raise rpki.exceptions.DERObjectConversionError, "No conversion path to DER available" + + def get_POWpkix(self): + """Get the POW.pkix value of this certification request.""" + assert not self.empty() + if not self.POWpkix: + req = POW.pkix.CertificationRequest() + req.fromString(self.get_DER()) + self.POWpkix = req + return self.POWpkix + + def getPublicKey(self): + """Extract the public key from this certification request.""" + return RSApublic(DER = self.get_POWpkix().certificationRequestInfo.subjectPublicKeyInfo.toString()) + + def check_valid_rpki(self): + """Check this certification request to see whether it's a valid + request for an RPKI certificate. This is broken out of the + up-down protocol code because it's somewhat involved and the + up-down code doesn't need to know the details. + + Throws an exception if the request isn't valid, so if this method + returns at all, the request is ok. + """ + + if not self.get_POWpkix().verify(): + raise rpki.exceptions.BadPKCS10, "Signature check failed" + + if self.get_POWpkix().certificationRequestInfo.version.get() != 0: + raise rpki.exceptions.BadPKCS10, \ + "Bad version number %s" % self.get_POWpkix().certificationRequestInfo.version + + if rpki.oids.oid2name.get(self.get_POWpkix().signatureAlgorithm.algorithm.get()) \ + not in ("sha256WithRSAEncryption", "sha384WithRSAEncryption", "sha512WithRSAEncryption"): + raise rpki.exceptions.BadPKCS10, "Bad signature algorithm %s" % self.get_POWpkix().signatureAlgorithm + + exts = self.get_POWpkix().getExtensions() + for oid, critical, value in exts: + if rpki.oids.oid2name.get(oid) not in ("basicConstraints", "keyUsage", "subjectInfoAccess"): + raise rpki.exceptions.BadExtension, "Forbidden extension %s" % oid + req_exts = dict((rpki.oids.oid2name[oid], value) for (oid, critical, value) in exts) + + if "basicConstraints" not in req_exts or not req_exts["basicConstraints"][0]: + raise rpki.exceptions.BadPKCS10, "request for EE cert not allowed here" + + if req_exts["basicConstraints"][1] is not None: + raise rpki.exceptions.BadPKCS10, "basicConstraints must not specify Path Length" + + if "keyUsage" in req_exts and (not req_exts["keyUsage"][5] or not req_exts["keyUsage"][6]): + raise rpki.exceptions.BadPKCS10, "keyUsage doesn't match basicConstraints" + + for method, location in req_exts.get("subjectInfoAccess", ()): + if rpki.oids.oid2name.get(method) == "id-ad-caRepository" and \ + (location[0] != "uri" or (location[1].startswith("rsync://") and not location[1].endswith("/"))): + raise rpki.exceptions.BadPKCS10, "Certificate request includes bad SIA component: %s" % repr(location) + + # This one is an implementation restriction. I don't yet + # understand what the spec is telling me to do in this case. + assert "subjectInfoAccess" in req_exts, "Can't (yet) handle PKCS #10 without an SIA extension" + + @classmethod + def create_ca(cls, keypair, sia = None): + """Create a new request for a given keypair, including given SIA value.""" + exts = [["basicConstraints", True, (1, None)], + ["keyUsage", True, (0, 0, 0, 0, 0, 1, 1)]] + if sia is not None: + exts.append(["subjectInfoAccess", False, sia]) + for x in exts: + x[0] = rpki.oids.name2oid[x[0]] + return cls.create(keypair, exts) + + @classmethod + def create(cls, keypair, exts = None): + """Create a new request for a given keypair, including given extensions.""" + cn = "".join(("%02X" % ord(i) for i in keypair.get_SKI())) + req = POW.pkix.CertificationRequest() + req.certificationRequestInfo.version.set(0) + req.certificationRequestInfo.subject.set((((rpki.oids.name2oid["commonName"], + ("printableString", cn)),),)) + if exts is not None: + req.setExtensions(exts) + req.sign(keypair.get_POW(), POW.SHA256_DIGEST) + return cls(POWpkix = req) + +class RSA(DER_object): + """Class to hold an RSA key pair.""" + + formats = ("DER", "POW", "tlslite") + pem_converter = PEM_converter("RSA PRIVATE KEY") + + def get_DER(self): + """Get the DER value of this keypair.""" + assert not self.empty() + if self.DER: + return self.DER + if self.POW: + self.DER = self.POW.derWrite(POW.RSA_PRIVATE_KEY) + return self.get_DER() + raise rpki.exceptions.DERObjectConversionError, "No conversion path to DER available" + + def get_POW(self): + """Get the POW value of this keypair.""" + assert not self.empty() + if not self.POW: + self.POW = POW.derRead(POW.RSA_PRIVATE_KEY, self.get_DER()) + return self.POW + + def get_tlslite(self): + """Get the tlslite value of this keypair.""" + assert not self.empty() + if not self.tlslite: + self.tlslite = tlslite.api.parsePEMKey(self.get_PEM(), private=True) + return self.tlslite + + @classmethod + def generate(cls, keylength = 2048): + """Generate a new keypair.""" + return cls(POW = POW.Asymmetric(POW.RSA_CIPHER, keylength)) + + def get_public_DER(self): + """Get the DER encoding of the public key from this keypair.""" + return self.get_POW().derWrite(POW.RSA_PUBLIC_KEY) + + def get_SKI(self): + """Calculate the SKI of this keypair.""" + return calculate_SKI(self.get_public_DER()) + + def get_RSApublic(self): + """Convert the public key of this keypair into a RSApublic object.""" + return RSApublic(DER = self.get_public_DER()) + +class RSApublic(DER_object): + """Class to hold an RSA public key.""" + + formats = ("DER", "POW") + pem_converter = PEM_converter("RSA PUBLIC KEY") + + def get_DER(self): + """Get the DER value of this public key.""" + assert not self.empty() + if self.DER: + return self.DER + if self.POW: + self.DER = self.POW.derWrite(POW.RSA_PUBLIC_KEY) + return self.get_DER() + raise rpki.exceptions.DERObjectConversionError, "No conversion path to DER available" + + def get_POW(self): + """Get the POW value of this public key.""" + assert not self.empty() + if not self.POW: + self.POW = POW.derRead(POW.RSA_PUBLIC_KEY, self.get_DER()) + return self.POW + + def get_SKI(self): + """Calculate the SKI of this public key.""" + return calculate_SKI(self.get_DER()) + +def POWify_OID(oid): + """Utility function to convert tuple form of an OID to + the dotted-decimal string form that POW uses. + """ + if isinstance(oid, str): + return POWify_OID(rpki.oids.name2oid[oid]) + else: + return ".".join(str(i) for i in oid) + +class CMS_object(DER_object): + """Class to hold a CMS-wrapped object. + + CMS-wrapped objects are a little different from the other DER_object + types because the signed object is CMS wrapping inner content that's + also ASN.1, and due to our current minimal support for CMS we can't + just handle this as a pretty composite object. So, for now anyway, + a CMS_object is the outer CMS wrapped object so that the usual DER + and PEM operations do the obvious things, and the inner content is + handle via separate methods. + """ + + formats = ("DER", "POW") + other_clear = ("content",) + econtent_oid = POWify_OID("id-data") + pem_converter = PEM_converter("CMS") + + ## @var dump_on_verify_failure + # Set this to True to get dumpasn1 dumps of ASN.1 on CMS verify failures. + + dump_on_verify_failure = True + + ## @var debug_cms_certs + # Set this to True to log a lot of chatter about CMS certificates. + + debug_cms_certs = False + + ## @var require_crls + # Set this to False to make CMS CRLs optional in the cases where we + # would otherwise require them. Some day this option should go away + # and CRLs should be uncondtionally mandatory in such cases. + + require_crls = False + + ## @var print_on_der_error + # Set this to True to log alleged DER when we have trouble parsing + # it, in case it's really a Perl backtrace or something. + + print_on_der_error = True + + def get_DER(self): + """Get the DER value of this CMS_object.""" + assert not self.empty() + if self.DER: + return self.DER + if self.POW: + self.DER = self.POW.derWrite() + return self.get_DER() + raise rpki.exceptions.DERObjectConversionError, "No conversion path to DER available" + + def get_POW(self): + """Get the POW value of this CMS_object.""" + assert not self.empty() + if not self.POW: + self.POW = POW.derRead(POW.CMS_MESSAGE, self.get_DER()) + return self.POW + + def get_content(self): + """Get the inner content of this CMS_object.""" + assert self.content is not None + return self.content + + def set_content(self, content): + """Set the (inner) content of this CMS_object, clearing the wrapper.""" + self.clear() + self.content = content + + def verify(self, ta): + """Verify CMS wrapper and store inner content.""" + + try: + cms = self.get_POW() + except: + if self.print_on_der_error: + rpki.log.debug("Problem parsing DER CMS message, might not really be DER: %s" + % repr(self.get_DER())) + raise rpki.exceptions.UnparsableCMSDER + + if cms.eContentType() != self.econtent_oid: + raise rpki.exceptions.WrongEContentType, "Got CMS eContentType %s, expected %s" % (cms.eContentType(), self.econtent_oid) + + certs = [X509(POW = x) for x in cms.certs()] + crls = [CRL(POW = c) for c in cms.crls()] + + if self.debug_cms_certs: + for x in certs: + rpki.log.debug("Received CMS cert issuer %s subject %s" % (x.getIssuer(), x.getSubject())) + for c in crls: + rpki.log.debug("Received CMS CRL issuer %s" % repr(c.getIssuer())) + + store = POW.X509Store() + + trusted_ee = None + + for x in X509.normalize_chain(ta): + if self.debug_cms_certs: + rpki.log.debug("CMS trusted cert issuer %s subject %s" % (x.getIssuer(), x.getSubject())) + if not x.is_CA(): + assert trusted_ee is None, "Can't have two EE certs in the same validation chain" + trusted_ee = x + store.addTrust(x.get_POW()) + + if trusted_ee: + if self.debug_cms_certs: + rpki.log.debug("Trusted CMS EE cert issuer %s subject %s" % (trusted_ee.getIssuer(), trusted_ee.getSubject())) + if certs and (len(certs) > 1 or certs[0] != trusted_ee): + raise rpki.exceptions.UnexpectedCMSCerts, certs + if crls: + raise rpki.exceptions.UnexpectedCMSCRLs, crls + else: + if not certs: + raise rpki.exceptions.MissingCMSEEcert, certs + if len(certs) > 1 or certs[0].is_CA(): + raise rpki.exceptions.UnexpectedCMSCerts, certs + if not crls: + if self.require_crls: + raise rpki.exceptions.MissingCMSCRL, crls + else: + rpki.log.warn("MISSING CMS CRL! Ignoring per self.require_crls setting") + if len(crls) > 1: + raise rpki.exceptions.UnexpectedCMSCRLs, crls + + try: + content = cms.verify(store) + except: + if self.dump_on_verify_failure: + if True: + dbg = self.dumpasn1() + else: + dbg = cms.pprint() + print "CMS verification failed, dumping ASN.1 (%d octets):\n%s" % (len(self.get_DER()), dbg) + raise rpki.exceptions.CMSVerificationFailed, "CMS verification failed" + + self.decode(content) + return self.get_content() + + def extract(self): + """Extract and store inner content from CMS wrapper without + verifying the CMS. + + DANGER WILL ROBINSON!!! + + Do not use this method on unvalidated data. Use the verify() + method instead. + + If you don't understand this warning, don't use this method. + """ + + try: + cms = self.get_POW() + except: + raise rpki.exceptions.UnparsableCMSDER + + if cms.eContentType() != self.econtent_oid: + raise rpki.exceptions.WrongEContentType, "Got CMS eContentType %s, expected %s" % (cms.eContentType(), self.econtent_oid) + + content = cms.verify(POW.X509Store(), None, POW.CMS_NOCRL | POW.CMS_NO_SIGNER_CERT_VERIFY | POW.CMS_NO_ATTR_VERIFY | POW.CMS_NO_CONTENT_VERIFY) + + self.decode(content) + return self.get_content() + + def sign(self, keypair, certs, crls = None, no_certs = False): + """Sign and wrap inner content.""" + + rpki.log.trace() + + if isinstance(certs, X509): + cert = certs + certs = () + else: + cert = certs[0] + certs = certs[1:] + + if crls is None: + crls = () + elif isinstance(crls, CRL): + crls = (crls,) + + cms = POW.CMS() + + cms.sign(cert.get_POW(), + keypair.get_POW(), + self.encode(), + [x.get_POW() for x in certs], + [c.get_POW() for c in crls], + self.econtent_oid, + POW.CMS_NOCERTS if no_certs else 0) + + self.POW = cms + +class DER_CMS_object(CMS_object): + """Class to hold CMS objects with DER-based content.""" + + def encode(self): + """Encode inner content for signing.""" + return self.get_content().toString() + + def decode(self, der): + """Decode DER and set inner content.""" + obj = self.content_class() + obj.fromString(der) + self.content = obj + +class SignedManifest(DER_CMS_object): + """Class to hold a signed manifest.""" + + pem_converter = PEM_converter("RPKI MANIFEST") + content_class = rpki.manifest.Manifest + econtent_oid = POWify_OID("id-ct-rpkiManifest") + + def getThisUpdate(self): + """Get thisUpdate value from this manifest.""" + return rpki.sundial.datetime.fromGeneralizedTime(self.get_content().thisUpdate.get()) + + def getNextUpdate(self): + """Get nextUpdate value from this manifest.""" + return rpki.sundial.datetime.fromGeneralizedTime(self.get_content().nextUpdate.get()) + + @classmethod + def build(cls, serial, thisUpdate, nextUpdate, names_and_objs, keypair, certs, version = 0): + """Build a signed manifest.""" + self = cls() + filelist = [] + for name, obj in names_and_objs: + d = POW.Digest(POW.SHA256_DIGEST) + d.update(obj.get_DER()) + filelist.append((name.rpartition("/")[2], d.digest())) + filelist.sort(key = lambda x: x[0]) + m = rpki.manifest.Manifest() + m.version.set(version) + m.manifestNumber.set(serial) + m.thisUpdate.set(thisUpdate.toGeneralizedTime()) + m.nextUpdate.set(nextUpdate.toGeneralizedTime()) + m.fileHashAlg.set(rpki.oids.name2oid["id-sha256"]) + m.fileList.set(filelist) + self.set_content(m) + self.sign(keypair, certs) + return self + +class ROA(DER_CMS_object): + """Class to hold a signed ROA.""" + + pem_converter = PEM_converter("ROUTE ORIGIN ATTESTATION") + content_class = rpki.roa.RouteOriginAttestation + econtent_oid = POWify_OID("id-ct-routeOriginAttestation") + + @classmethod + def build(cls, as_number, ipv4, ipv6, keypair, certs, version = 0): + """Build a ROA.""" + self = cls() + r = rpki.roa.RouteOriginAttestation() + r.version.set(version) + r.asID.set(as_number) + r.ipAddrBlocks.set((a.to_roa_tuple() for a in (ipv4, ipv6) if a)) + self.set_content(r) + self.sign(keypair, certs) + return self + +class XML_CMS_object(CMS_object): + """Class to hold CMS-wrapped XML protocol data.""" + + econtent_oid = POWify_OID("id-ct-xml") + + ## @var dump_outbound_cms + # If set, we write all outbound XML-CMS PDUs to disk, for debugging. + # Value of this variable is prefix portion of filename, tail will + # be a timestamp. + + dump_outbound_cms = None + + ## @var dump_outbound_cms + # If set, we write all inbound XML-CMS PDUs to disk, for debugging. + # Value of this variable is prefix portion of filename, tail will + # be a timestamp. + + dump_inbound_cms = None + + def encode(self): + """Encode inner content for signing.""" + return lxml.etree.tostring(self.get_content(), pretty_print = True, encoding = self.encoding, xml_declaration = True) + + def decode(self, xml): + """Decode XML and set inner content.""" + self.content = lxml.etree.fromstring(xml) + + def pretty_print_content(self): + """Pretty print XML content of this message.""" + return lxml.etree.tostring(self.get_content(), pretty_print = True, encoding = self.encoding, xml_declaration = True) + + def schema_check(self): + """Handle XML RelaxNG schema check.""" + try: + self.schema.assertValid(self.get_content()) + except lxml.etree.DocumentInvalid: + rpki.log.error("PDU failed schema check: " + self.pretty_print_content()) + raise + + def dump_to_disk(self, prefix): + """Write DER of current message to disk, for debugging.""" + f = open(prefix + rpki.sundial.now().isoformat() + "Z.cms", "wb") + f.write(self.get_DER()) + f.close() + + @classmethod + def wrap(cls, msg, keypair, certs, crls = None, pretty_print = False): + """Build a CMS-wrapped XML PDU and return its DER encoding.""" + rpki.log.trace() + self = cls() + self.set_content(msg.toXML()) + self.schema_check() + self.sign(keypair, certs, crls) + if self.dump_outbound_cms: + self.dump_to_disk(self.dump_outbound_cms) + if pretty_print: + return self.get_DER(), self.pretty_print_content() + else: + return self.get_DER() + + @classmethod + def unwrap(cls, der, ta, pretty_print = False): + """Unwrap a CMS-wrapped XML PDU and return Python objects.""" + self = cls(DER = der) + if self.dump_inbound_cms: + self.dump_to_disk(self.dump_inbound_cms) + self.verify(ta) + self.schema_check() + msg = self.saxify(self.get_content()) + if pretty_print: + return msg, self.pretty_print_content() + else: + return msg + +class CRL(DER_object): + """Class to hold a Certificate Revocation List.""" + + formats = ("DER", "POW", "POWpkix") + pem_converter = PEM_converter("X509 CRL") + + def get_DER(self): + """Get the DER value of this CRL.""" + assert not self.empty() + if self.DER: + return self.DER + if self.POW: + self.DER = self.POW.derWrite() + return self.get_DER() + if self.POWpkix: + self.DER = self.POWpkix.toString() + return self.get_DER() + raise rpki.exceptions.DERObjectConversionError, "No conversion path to DER available" + + def get_POW(self): + """Get the POW value of this CRL.""" + assert not self.empty() + if not self.POW: + self.POW = POW.derRead(POW.X509_CRL, self.get_DER()) + return self.POW + + def get_POWpkix(self): + """Get the POW.pkix value of this CRL.""" + assert not self.empty() + if not self.POWpkix: + crl = POW.pkix.CertificateList() + crl.fromString(self.get_DER()) + self.POWpkix = crl + return self.POWpkix + + def getThisUpdate(self): + """Get thisUpdate value from this CRL.""" + return rpki.sundial.datetime.fromASN1tuple(self.get_POWpkix().getThisUpdate()) + + def getNextUpdate(self): + """Get nextUpdate value from this CRL.""" + return rpki.sundial.datetime.fromASN1tuple(self.get_POWpkix().getNextUpdate()) + + def getIssuer(self): + """Get issuer value of this CRL.""" + return self.get_POW().getIssuer() + + @classmethod + def generate(cls, keypair, issuer, serial, thisUpdate, nextUpdate, revokedCertificates, version = 1, digestType = "sha256WithRSAEncryption"): + crl = POW.pkix.CertificateList() + crl.setVersion(version) + crl.setIssuer(issuer.get_POWpkix().getSubject()) + crl.setThisUpdate(thisUpdate.toASN1tuple()) + crl.setNextUpdate(nextUpdate.toASN1tuple()) + if revokedCertificates: + crl.setRevokedCertificates(revokedCertificates) + crl.setExtensions( + ((rpki.oids.name2oid["authorityKeyIdentifier"], False, (issuer.get_SKI(), (), None)), + (rpki.oids.name2oid["cRLNumber"], False, serial))) + crl.sign(keypair.get_POW(), digestType) + return cls(POWpkix = crl) diff --git a/rpkid.stable/rpki/xml_utils.py b/rpkid.stable/rpki/xml_utils.py new file mode 100644 index 00000000..eda8aa85 --- /dev/null +++ b/rpkid.stable/rpki/xml_utils.py @@ -0,0 +1,317 @@ +"""XML utilities. + +$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 xml.sax, lxml.sax, lxml.etree, base64 + +class sax_handler(xml.sax.handler.ContentHandler): + """SAX handler for RPKI protocols. + + This class provides some basic amenities for parsing protocol XML of + the kind we use in the RPKI protocols, including whacking all the + protocol element text into US-ASCII, simplifying accumulation of + text fields, and hiding some of the fun relating to XML namespaces. + + General assumption: by the time this parsing code gets invoked, the + XML has already passed RelaxNG validation, so we only have to check + for errors that the schema can't catch, and we don't have to play as + many XML namespace games. + """ + + def __init__(self): + """Initialize SAX handler.""" + self.text = "" + self.stack = [] + + def startElementNS(self, name, qname, attrs): + """Redirect startElementNS() events to startElement().""" + return self.startElement(name[1], attrs) + + def endElementNS(self, name, qname): + """Redirect endElementNS() events to endElement().""" + return self.endElement(name[1]) + + def characters(self, content): + """Accumulate a chuck of element content (text).""" + self.text += content + + def startElement(self, name, attrs): + """Handle startElement() events. + + We maintain a stack of nested elements under construction so that + we can feed events directly to the current element rather than + having to pass them through all the nesting elements. + + If the stack is empty, this event is for the outermost element, so + we call a virtual method to create the corresponding object and + that's the object we'll be returning as our final result. + """ + a = dict() + for k,v in attrs.items(): + if isinstance(k, tuple): + if k == ("http://www.w3.org/XML/1998/namespace", "lang"): + k = "xml:lang" + else: + assert k[0] is None + k = k[1] + a[k.encode("ascii")] = v.encode("ascii") + if len(self.stack) == 0: + assert not hasattr(self, "result") + self.result = self.create_top_level(name, a) + self.stack.append(self.result) + self.stack[-1].startElement(self.stack, name, a) + + def endElement(self, name): + """Handle endElement() events. + + Mostly this means handling any accumulated element text. + """ + text = self.text.encode("ascii").strip() + self.text = "" + self.stack[-1].endElement(self.stack, name, text) + + @classmethod + def saxify(cls, elt): + """Create a one-off SAX parser, parse an ETree, return the result. + """ + self = cls() + lxml.sax.saxify(elt, self) + return self.result + + def create_top_level(self, name, attrs): + """Handle top-level PDU for this protocol.""" + assert name == self.name and attrs["version"] == self.version + return self.pdu() + +class base_elt(object): + """Virtual base class for XML message elements. The left-right and + publication protocols use this. At least for now, the up-down + protocol does not, due to different design assumptions. + """ + + ## @var attributes + # XML attributes for this element. + attributes = () + + ## @var elements + # XML elements contained by this element. + elements = () + + ## @var booleans + # Boolean attributes (value "yes" or "no") for this element. + booleans = () + + def startElement(self, stack, name, attrs): + """Default startElement() handler: just process attributes.""" + if name not in self.elements: + assert name == self.element_name, "Unexpected name %s, stack %s" % (name, stack) + self.read_attrs(attrs) + + def endElement(self, stack, name, text): + """Default endElement() handler: just pop the stack.""" + assert name == self.element_name, "Unexpected name %s, stack %s" % (name, stack) + stack.pop() + + def toXML(self): + """Default toXML() element generator.""" + return self.make_elt() + + def read_attrs(self, attrs): + """Template-driven attribute reader.""" + for key in self.attributes: + val = attrs.get(key, None) + if isinstance(val, str) and val.isdigit(): + val = long(val) + setattr(self, key, val) + for key in self.booleans: + setattr(self, key, attrs.get(key, False)) + + def make_elt(self): + """XML element constructor.""" + elt = lxml.etree.Element("{%s}%s" % (self.xmlns, self.element_name), nsmap = self.nsmap) + for key in self.attributes: + val = getattr(self, key, None) + if val is not None: + elt.set(key, str(val)) + for key in self.booleans: + if getattr(self, key, False): + elt.set(key, "yes") + return elt + + def make_b64elt(self, elt, name, value = None): + """Constructor for Base64-encoded subelement.""" + if value is None: + value = getattr(self, name, None) + if value is not None: + lxml.etree.SubElement(elt, "{%s}%s" % (self.xmlns, name), nsmap = self.nsmap).text = base64.b64encode(value) + + def __str__(self): + """Convert a base_elt object to string format.""" + lxml.etree.tostring(self.toXML(), pretty_print = True, encoding = "us-ascii") + + @classmethod + def make_pdu(cls, **kargs): + """Generic PDU constructor.""" + self = cls() + for k,v in kargs.items(): + if isinstance(v, bool): + v = 1 if v else 0 + setattr(self, k, v) + return self + +class data_elt(base_elt): + """Virtual base class for PDUs that map to SQL objects. These + objects all implement the create/set/get/list/destroy action + attribute. + """ + + def endElement(self, stack, name, text): + """Default endElement handler for SQL-based objects. This assumes + that sub-elements are Base64-encoded using the sql_template mechanism. + """ + if name in self.elements: + elt_type = self.sql_template.map.get(name) + assert elt_type is not None, "Couldn't find element type for %s, stack %s" % (name, stack) + setattr(self, name, elt_type(Base64 = text)) + else: + assert name == self.element_name, "Unexpected name %s, stack %s" % (name, stack) + stack.pop() + + def toXML(self): + """Default element generator for SQL-based objects. This assumes + that sub-elements are Base64-encoded DER objects. + """ + elt = self.make_elt() + for i in self.elements: + x = getattr(self, i, None) + if x and not x.empty(): + self.make_b64elt(elt, i, x.get_DER()) + return elt + + def make_reply(self, r_pdu = None): + """Construct a reply PDU.""" + if r_pdu is None: + r_pdu = self.__class__() + self.make_reply_clone_hook(r_pdu) + setattr(r_pdu, self.sql_template.index, getattr(self, self.sql_template.index)) + else: + for b in r_pdu.booleans: + setattr(r_pdu, b, False) + r_pdu.action = self.action + r_pdu.tag = self.tag + return r_pdu + + def make_reply_clone_hook(self, r_pdu): + """Overridable hook.""" + pass + + def serve_pre_save_hook(self, q_pdu, r_pdu): + """Overridable hook.""" + pass + + def serve_post_save_hook(self, q_pdu, r_pdu): + """Overridable hook.""" + pass + + def serve_create(self, r_msg): + """Handle a create action.""" + r_pdu = self.make_reply() + self.serve_pre_save_hook(self, r_pdu) + self.sql_store() + setattr(r_pdu, self.sql_template.index, getattr(self, self.sql_template.index)) + self.serve_post_save_hook(self, r_pdu) + r_msg.append(r_pdu) + + def serve_set(self, r_msg): + """Handle a set action.""" + db_pdu = self.serve_fetch_one() + r_pdu = self.make_reply() + for a in db_pdu.sql_template.columns[1:]: + v = getattr(self, a) + if v is not None: + setattr(db_pdu, a, v) + db_pdu.sql_mark_dirty() + db_pdu.serve_pre_save_hook(self, r_pdu) + db_pdu.sql_store() + db_pdu.serve_post_save_hook(self, r_pdu) + r_msg.append(r_pdu) + + def serve_get(self, r_msg): + """Handle a get action.""" + r_pdu = self.serve_fetch_one() + self.make_reply(r_pdu) + r_msg.append(r_pdu) + + def serve_list(self, r_msg): + """Handle a list action for non-self objects.""" + for r_pdu in self.serve_fetch_all(): + self.make_reply(r_pdu) + r_msg.append(r_pdu) + + def serve_destroy(self, r_msg): + """Handle a destroy action.""" + db_pdu = self.serve_fetch_one() + db_pdu.sql_delete() + r_msg.append(self.make_reply()) + + def serve_dispatch(self, r_msg): + """Action dispatch handler.""" + dispatch = { "create" : self.serve_create, + "set" : self.serve_set, + "get" : self.serve_get, + "list" : self.serve_list, + "destroy" : self.serve_destroy } + if self.action not in dispatch: + raise rpki.exceptions.BadQuery, "Unexpected query: action %s" % self.action + dispatch[self.action](r_msg) + + 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 msg(list): + """Generic top-level PDU.""" + + def startElement(self, stack, name, attrs): + """Handle top-level PDU.""" + if name == "msg": + assert self.version == int(attrs["version"]) + self.type = attrs["type"] + else: + elt = self.pdus[name]() + self.append(elt) + stack.append(elt) + elt.startElement(stack, name, attrs) + + def endElement(self, stack, name, text): + """Handle top-level PDU.""" + assert name == "msg", "Unexpected name %s, stack %s" % (name, stack) + assert len(stack) == 1 + stack.pop() + + def __str__(self): + """Convert msg object to string.""" + lxml.etree.tostring(self.toXML(), pretty_print = True, encoding = "us-ascii") + + def toXML(self): + """Generate top-level PDU.""" + elt = lxml.etree.Element("{%s}msg" % (self.xmlns), nsmap = self.nsmap, version = str(self.version), type = self.type) + elt.extend([i.toXML() for i in self]) + return elt |