aboutsummaryrefslogtreecommitdiff
path: root/rpkid.stable/rpki
diff options
context:
space:
mode:
Diffstat (limited to 'rpkid.stable/rpki')
-rw-r--r--rpkid.stable/rpki/__init__.py1992
-rw-r--r--rpkid.stable/rpki/config.py56
-rw-r--r--rpkid.stable/rpki/exceptions.py135
-rw-r--r--rpkid.stable/rpki/https.py291
-rw-r--r--rpkid.stable/rpki/ipaddrs.py103
-rw-r--r--rpkid.stable/rpki/left_right.py833
-rw-r--r--rpkid.stable/rpki/log.py58
-rw-r--r--rpkid.stable/rpki/manifest.py53
-rw-r--r--rpkid.stable/rpki/oids.py57
-rw-r--r--rpkid.stable/rpki/publication.py282
-rw-r--r--rpkid.stable/rpki/relaxng.py1699
-rw-r--r--rpkid.stable/rpki/resource_set.py795
-rw-r--r--rpkid.stable/rpki/roa.py75
-rw-r--r--rpkid.stable/rpki/rpki_engine.py819
-rw-r--r--rpkid.stable/rpki/sql.py295
-rw-r--r--rpkid.stable/rpki/sundial.py198
-rw-r--r--rpkid.stable/rpki/up_down.py535
-rw-r--r--rpkid.stable/rpki/x509.py995
-rw-r--r--rpkid.stable/rpki/xml_utils.py317
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 &lt;self/&gt; %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 &lt;self/&gt; %object, representing the engine
+# operator's organization, but in environments where the engine operator
+# hosts other entities, there will be one @c @c &lt;self/&gt; %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 &lt;self/&gt; %object.
+# Data which are shared by all hosted entities are referred to as
+# "per-engine" data, data which are specific to a particular @c &lt;self/&gt;
+# %object are "per-self" data.
+#
+# Since all other RPKI engine %objects refer to a @c &lt;self/&gt; %object via a
+# "self_id" value, one must create a @c &lt;self/&gt; %object before one can
+# usefully configure any other left-right protocol %objects.
+#
+# Every @c &lt;self/&gt; %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 &lt;self/&gt; %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 &lt;self/&gt;, 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 &lt;self/&gt;. 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 &lt;repository/&gt;, @c &lt;parent/&gt;, and @c &lt;child/&gt; %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 &lt;self/&gt;, 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 &lt;parent/&gt; %object associated with this @c &lt;self/&gt; %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 &lt;parent/&gt; %object associated with this
+# @c &lt;self/&gt; %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 &lt;self/&gt; %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 &lt;self/&gt;
+# %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 &lt;bsc/&gt; ("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 &lt;bsc/&gt; %object. Whether a particular
+# @c &lt;self/&gt; uses only one @c &lt;bsc/&gt; 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 &lt;bsc/&gt; %object that it can use to
+# produce that signature.
+#
+# Every @c &lt;bsc/&gt; %object has a bsc_id, which must be specified for the
+# "get", "set", and "destroy" actions. Every @c &lt;bsc/&gt; also has a self_id
+# attribute which indicates the @c &lt;self/&gt; %object with which this @c &lt;bsc/&gt;
+# %object is associated.
+#
+# Payload data which can be configured in a @c &lt;isc/&gt; %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 &lt;bsc/&gt; %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 &lt;bsc_pkcs10/> element, as do replies to "get" and "list"
+# actions for a @c &lt;bsc/&gt; %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 &lt;parent/&gt; %object represents the RPKI engine's view of a particular
+# parent of the current @c &lt;self/&gt; %object in the up-down protocol. Due to
+# the way that the resource hierarchy works, a given @c &lt;self/&gt; 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 &lt;parent/&gt; %object has a parent_id, which must be specified for
+# the "get", "set", and "destroy" actions. Every @c &lt;parent/&gt; also has a
+# self_id attribute which indicates the @c &lt;self/&gt; %object with which this
+# @c &lt;parent/&gt; %object is associated, a bsc_id attribute indicating the @c &lt;bsc/&gt;
+# %object to be used when signing messages sent to this parent, and a
+# repository_id indicating the @c &lt;repository/&gt; %object to be used when
+# publishing issued by the certificate issued by this parent.
+#
+# Payload data which can be configured in a @c &lt;parent/&gt; %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 &lt;parent/&gt;. 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 &lt;self/&gt; %object.
+#
+# @li @c bpki_cms_glue (element):
+# Another BPKI CMS CA certificate for this @c &lt;parent/&gt;, 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 &lt;self/&gt; %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 &lt;parent/&gt;. 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 &lt;parent/&gt;, 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 &lt;self/&gt; %object, but limited
+# to RPKI CAs under this parent.
+#
+# @li @c reissue:
+# This is like the reissue command in the @c &lt;self/&gt; %object, but limited
+# to RPKI CAs under this parent.
+#
+# @li @c revoke:
+# This is like the revoke command in the @c &lt;self/&gt; %object, but limited
+# to RPKI CAs under this parent.
+#
+# @subsection child_obj <child/> object
+#
+# The @c &lt;child/&gt; %object represents the RPKI engine's view of particular
+# child of the current @c &lt;self/&gt; in the up-down protocol.
+#
+# Every @c &lt;child/&gt; %object has a parent_id, which must be specified for the
+# "get", "set", and "destroy" actions. Every @c &lt;child/&gt; also has a
+# self_id attribute which indicates the @c &lt;self/&gt; %object with which this
+# @c &lt;child/&gt; %object is associated.
+#
+# Payload data which can be configured in a @c &lt;child/&gt; %object:
+#
+# @li @c bpki_cert (element):
+# BPKI CA certificate for this @c &lt;child/&gt;. 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 &lt;self/&gt; %object.
+#
+# @li @c bpki_glue (element):
+# Another BPKI CA certificate for this @c &lt;child/&gt;, 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 &lt;self/&gt; %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 &lt;repository/&gt; %object represents the RPKI engine's view of a
+# particular publication repository used by the current @c &lt;self/&gt; %object.
+#
+# Every @c &lt;repository/&gt; %object has a repository_id, which must be
+# specified for the "get", "set", and "destroy" actions. Every
+# @c &lt;repository/&gt; also has a self_id attribute which indicates the @c &lt;self/&gt;
+# %object with which this @c &lt;repository/&gt; %object is associated.
+#
+# Payload data which can be configured in a @c &lt;repository/&gt; %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 &lt;repository/&gt;. 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 &lt;self/&gt; %object.
+#
+# @li @c bpki_cms_glue (element):
+# Another BPKI CMS CA certificate for this @c &lt;repository/&gt;, 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 &lt;self/&gt; %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 &lt;repository/&gt;. 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 &lt;repository/&gt;, 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 &lt;repository/&gt; %objects.
+#
+# @subsection route_origin_obj <route_origin/> object
+#
+# The @c &lt;route_origin/&gt; %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 &lt;route_origin/&gt; %object represents a ROA to be generated on
+# behalf of @c &lt;self/&gt;, not on behalf of a @c &lt;child/&gt;. Thus, a hosted entity
+# that has no children but which does need to generate ROAs would be
+# represented by a hosted @c &lt;self/&gt; with no @c &lt;child/&gt; %objects but one or
+# more @c &lt;route_origin/&gt; %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 &lt;route_origin/&gt; 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 &lt;route_origin/&gt; %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 &lt;route_origin/&gt; %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 &lt;list_resources/&gt; 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 &lt;self/&gt; 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 &lt;list_resources/&gt; 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 &lt;route_origin/&gt;
+# %object, but the semantics differ: note in particular that
+# @c &lt;route_origin/&gt; %objects don't allow ranges, while @c &lt;list_resources/&gt;
+# 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 &lt;report_error/&gt; message which
+# takes the place of the expected protocol response message.
+# @c &lt;report_error/&gt; messages are CMS-signed XML messages like the rest of
+# this protocol, and thus can be archived to provide an audit trail.
+#
+# @c &lt;report_error/&gt; messages only appear in replies, never in queries.
+# The @c &lt;report_error/&gt; 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 &lt;report_error/&gt; 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 &lt;self/&gt; 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 &lt;report_error/&gt; 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 &lt;config/&gt; %object allows configuration of data that apply to the
+# entire %publication server rather than a particular client.
+#
+# There is exactly one &lt;config/&gt; %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 &lt;config/&gt; %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 &lt;client/&gt; %object represents one client authorized to use the
+# %publication server.
+#
+# The &lt;client/&gt; %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 &lt;client/&gt; %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 &lt;client/&gt;. 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 &lt;client/&gt;, 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 &lt;certificate/&gt; %object represents an RPKI certificate to be
+# published or withdrawn.
+#
+# @subsection crl_obj <crl/> object
+#
+# The &lt;crl/&gt; %object represents an RPKI CRL to be published or withdrawn.
+#
+# @subsection manifest_obj <manifest/> object
+#
+# The &lt;manifest/&gt; %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 &lt;roa/&gt; %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 &lt;report_error/&gt; message which
+# takes the place of the expected protocol response message.
+# &lt;report_error/&gt; messages are CMS-signed XML messages like the rest of
+# this protocol, and thus can be archived to provide an audit trail.
+#
+# &lt;report_error/&gt; messages only appear in replies, never in
+# queries. The &lt;report_error/&gt; message can appear in both the
+# control and publication subprotocols.
+#
+# The &lt;report_error/&gt; 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 &lt;report_error/&gt; 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
+# &lt;certificate/&gt; %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