aboutsummaryrefslogtreecommitdiff
path: root/myrpki/myrpki.py
diff options
context:
space:
mode:
Diffstat (limited to 'myrpki/myrpki.py')
-rw-r--r--myrpki/myrpki.py644
1 files changed, 0 insertions, 644 deletions
diff --git a/myrpki/myrpki.py b/myrpki/myrpki.py
deleted file mode 100644
index 7937521d..00000000
--- a/myrpki/myrpki.py
+++ /dev/null
@@ -1,644 +0,0 @@
-"""
-Read an OpenSSL-style config file and a bunch of .csv files to find
-out about parents and children and resources and ROA requests, oh my.
-Run OpenSSL command line tool to construct BPKI certificates,
-including cross-certification of other entities' BPKI certificates.
-
-Package up all of the above as a single XML file which user can then
-ship off to the IRBE. If an XML file already exists, check it for
-data coming back from the IRBE (principally PKCS #10 requests for our
-BSC) and update it with current data.
-
-The general idea here is that this one XML file contains all of the
-data that needs to be exchanged as part of ordinary update operations;
-each party updates it as necessary, then ships it to the other via
-some secure channel: carrier pigeon, USB stick, gpg-protected email,
-we don't really care.
-
-This one program is written a little differently from all the other
-Python RPKI programs. This one program is intended to run as a
-stand-alone script, without the other programs present. It does
-require a reasonably up-to-date version of the OpenSSL command line
-tool (the one built as a side effect of building rcynic will do), but
-it does -not- require POW or any Python libraries beyond what ships
-with Python 2.5. So this script uses xml.etree from the Python
-standard libraries instead of lxml.etree, which sacrifices XML schema
-validation support in favor of portability, and so forth.
-
-To make things a little weirder, as a convenience to IRBE operators,
-this script can itself be loaded as a Python module and invoked as
-part of another program. This requires a few minor contortions, but
-avoids duplicating common code.
-
-$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.
-"""
-
-# Only standard Python libraries for this program, please.
-
-import subprocess, csv, re, os, getopt, sys, ConfigParser, base64
-
-from xml.etree.ElementTree import Element, SubElement, ElementTree
-
-# Our XML namespace.
-
-namespace = "http://www.hactrn.net/uris/rpki/myrpki/"
-
-# Dialect for our use of CSV files, here to make it easy to change if
-# your site needs to do something different. See doc for the csv
-# module in the Python standard libraries for details if you need to
-# customize this.
-
-csv_dialect = csv.get_dialect("excel-tab")
-
-# Whether to include incomplete entries when rendering to XML.
-
-allow_incomplete = False
-
-# Whether to whine about incomplete entries while rendering to XML.
-
-whine = False
-
-class comma_set(set):
- """
- Minor customization of set(), to provide a print syntax.
- """
-
- def __str__(self):
- return ",".join(self)
-
-class roa_request(object):
- """
- Representation of a ROA request.
- """
-
- v4re = re.compile("^([0-9]{1,3}\.){3}[0-9]{1,3}/[0-9]+(-[0-9]+)?$", re.I)
- v6re = re.compile("^([0-9a-f]{0,4}:){0,15}[0-9a-f]{0,4}/[0-9]+(-[0-9]+)?$", re.I)
-
- def __init__(self, asn, group):
- self.asn = asn
- self.group = group
- self.v4 = comma_set()
- self.v6 = comma_set()
-
- def __repr__(self):
- s = "<%s asn %s group %s" % (self.__class__.__name__, self.asn, self.group)
- if self.v4:
- s += " v4 %s" % self.v4
- if self.v6:
- s += " v6 %s" % self.v6
- return s + ">"
-
- def add(self, prefix):
- """
- Add one prefix to this ROA request.
- """
- if self.v4re.match(prefix):
- self.v4.add(prefix)
- elif self.v6re.match(prefix):
- self.v6.add(prefix)
- else:
- raise RuntimeError, "Bad prefix syntax: %r" % (prefix,)
-
- def xml(self, e):
- """
- Generate XML element represeting representing this ROA request.
- """
- SubElement(e, "roa_request",
- asn = self.asn,
- v4 = str(self.v4),
- v6 = str(self.v6))
-
-class roa_requests(dict):
- """
- Database of ROA requests.
- """
-
- def add(self, asn, group, prefix):
- """
- Add one <ASN, group, prefix> set to ROA request database.
- """
- key = (asn, group)
- if key not in self:
- self[key] = roa_request(asn, group)
- self[key].add(prefix)
-
- def xml(self, e):
- """
- Render ROA requests as XML elements.
- """
- for r in self.itervalues():
- r.xml(e)
-
- @classmethod
- def from_csv(cls, roa_csv_file):
- """
- Parse ROA requests from CSV file.
- """
- self = cls()
- # format: p/n-m asn group
- for pnm, asn, group in csv_open(roa_csv_file):
- self.add(asn = asn, group = group, prefix = pnm)
- return self
-
-class child(object):
- """
- Representation of one child entity.
- """
-
- v4re = re.compile("^(([0-9]{1,3}\.){3}[0-9]{1,3}/[0-9]+)|(([0-9]{1,3}\.){3}[0-9]{1,3}-([0-9]{1,3}\.){3}[0-9]{1,3})$", re.I)
- v6re = re.compile("^(([0-9a-f]{0,4}:){0,15}[0-9a-f]{0,4}/[0-9]+)|(([0-9a-f]{0,4}:){0,15}[0-9a-f]{0,4}-([0-9a-f]{0,4}:){0,15}[0-9a-f]{0,4})$", re.I)
-
- def __init__(self, handle):
- self.handle = handle
- self.asns = comma_set()
- self.v4 = comma_set()
- self.v6 = comma_set()
- self.validity = None
- self.bpki_certificate = None
-
- def __repr__(self):
- s = "<%s %s" % (self.__class__.__name__, self.handle)
- if self.asns:
- s += " asn %s" % self.asns
- if self.v4:
- s += " v4 %s" % self.v4
- if self.v6:
- s += " v6 %s" % self.v6
- if self.validity:
- s += " valid %s" % self.validity
- if self.bpki_certificate:
- s += " cert %s" % self.bpki_certificate
- return s + ">"
-
- def add(self, prefix = None, asn = None, validity = None, bpki_certificate = None):
- """
- Add prefix, autonomous system number, validity date, or BPKI
- certificate for this child.
- """
- if prefix is not None:
- if self.v4re.match(prefix):
- self.v4.add(prefix)
- elif self.v6re.match(prefix):
- self.v6.add(prefix)
- else:
- raise RuntimeError, "Bad prefix syntax: %r" % (prefix,)
- if asn is not None:
- self.asns.add(asn)
- if validity is not None:
- self.validity = validity
- if bpki_certificate is not None:
- self.bpki_certificate = bpki_certificate
-
- def xml(self, e):
- """
- Render this child as an XML element.
- """
- complete = self.bpki_certificate and self.validity
- if whine and not complete:
- print "Incomplete child entry %s" % self
- if complete or allow_incomplete:
- e = SubElement(e, "child",
- handle = self.handle,
- valid_until = self.validity,
- asns = str(self.asns),
- v4 = str(self.v4),
- v6 = str(self.v6))
- if self.bpki_certificate:
- PEMElement(e, "bpki_certificate", self.bpki_certificate)
-
-class children(dict):
- """
- Database of children.
- """
-
- def add(self, handle, prefix = None, asn = None, validity = None, bpki_certificate = None):
- """
- Add resources to a child, creating the child object if necessary.
- """
- if handle not in self:
- self[handle] = child(handle)
- self[handle].add(prefix = prefix, asn = asn, validity = validity, bpki_certificate = bpki_certificate)
-
- def xml(self, e):
- """
- Render children database to XML.
- """
- for c in self.itervalues():
- c.xml(e)
-
- @classmethod
- def from_csv(cls, children_csv_file, prefix_csv_file, asn_csv_file, xcert):
- """
- Parse child resources, certificates, and validity dates from CSV files.
- """
- self = cls()
- # childname date pemfile
- for handle, date, pemfile in csv_open(children_csv_file):
- self.add(handle = handle, validity = date, bpki_certificate = xcert(pemfile))
- # childname p/n
- for handle, pn in csv_open(prefix_csv_file):
- self.add(handle = handle, prefix = pn)
- # childname asn
- for handle, asn in csv_open(asn_csv_file):
- self.add(handle = handle, asn = asn)
- return self
-
-class parent(object):
- """
- Representation of one parent entity.
- """
-
- def __init__(self, handle):
- self.handle = handle
- self.service_uri = None
- self.bpki_cms_certificate = None
- self.bpki_https_certificate = None
- self.myhandle = None
- self.sia_base = None
-
- def __repr__(self):
- s = "<%s %s" % (self.__class__.__name__, self.handle)
- if self.myhandle:
- s += " myhandle %s" % self.myhandle
- if self.service_uri:
- s += " uri %s" % self.service_uri
- if self.sia_base:
- s += " sia %s" % self.sia_base
- if self.bpki_cms_certificate:
- s += " cms %s" % self.bpki_cms_certificate
- if self.bpki_https_certificate:
- s += " https %s" % self.bpki_https_certificate
- return s + ">"
-
- def add(self, service_uri = None,
- bpki_cms_certificate = None,
- bpki_https_certificate = None,
- myhandle = None,
- sia_base = None):
- """
- Add service URI or BPKI certificates to this parent object.
- """
- if service_uri is not None:
- self.service_uri = service_uri
- if bpki_cms_certificate is not None:
- self.bpki_cms_certificate = bpki_cms_certificate
- if bpki_https_certificate is not None:
- self.bpki_https_certificate = bpki_https_certificate
- if myhandle is not None:
- self.myhandle = myhandle
- if sia_base is not None:
- self.sia_base = sia_base
-
- def xml(self, e):
- """
- Render this parent object to XML.
- """
- complete = self.bpki_cms_certificate and self.bpki_https_certificate and self.myhandle and self.service_uri and self.sia_base
- if whine and not complete:
- print "Incomplete parent entry %s" % self
- if complete or allow_incomplete:
- e = SubElement(e, "parent",
- handle = self.handle,
- myhandle = self.myhandle,
- service_uri = self.service_uri,
- sia_base = self.sia_base)
- if self.bpki_cms_certificate:
- PEMElement(e, "bpki_cms_certificate", self.bpki_cms_certificate)
- if self.bpki_https_certificate:
- PEMElement(e, "bpki_https_certificate", self.bpki_https_certificate)
-
-class parents(dict):
- """
- Database of parent objects.
- """
-
- def add(self, handle,
- service_uri = None,
- bpki_cms_certificate = None,
- bpki_https_certificate = None,
- myhandle = None,
- sia_base = None):
- """
- Add service URI or certificates to parent object, creating it if necessary.
- """
- if handle not in self:
- self[handle] = parent(handle)
- self[handle].add(service_uri = service_uri,
- bpki_cms_certificate = bpki_cms_certificate,
- bpki_https_certificate = bpki_https_certificate,
- myhandle = myhandle,
- sia_base = sia_base)
-
- def xml(self, e):
- for c in self.itervalues():
- c.xml(e)
-
- @classmethod
- def from_csv(cls, parents_csv_file, xcert):
- """
- Parse parent data from CSV file.
- """
- self = cls()
- # parentname service_uri parent_bpki_cms_pemfile parent_bpki_https_pemfile myhandle sia_base
- for handle, service_uri, parent_cms_pemfile, parent_https_pemfile, myhandle, sia_base in csv_open(parents_csv_file):
- self.add(handle = handle,
- service_uri = service_uri,
- bpki_cms_certificate = xcert(parent_cms_pemfile),
- bpki_https_certificate = xcert(parent_https_pemfile),
- myhandle = myhandle,
- sia_base = sia_base)
- return self
-
-def csv_open(filename):
- """
- Open a CSV file, with settings that make it a tab-delimited file.
- You may need to tweak this function for your environment, see the
- csv module in the Python standard libraries for details.
- """
- return csv.reader(open(filename, "rb"), dialect = csv_dialect)
-
-def PEMElement(e, tag, filename):
- """
- Create an XML element containing Base64 encoded data taken from a
- PEM file.
- """
- lines = open(filename).readlines()
- while lines:
- if lines.pop(0).startswith("-----BEGIN "):
- break
- while lines:
- if lines.pop(-1).startswith("-----END "):
- break
- SubElement(e, tag).text = "".join(line.strip() for line in lines)
-
-class CA(object):
- """
- Representation of one certification authority.
- """
-
- # Mapping of path restriction values we use to OpenSSL config file
- # section names.
-
- path_restriction = { 0 : "ca_x509_ext_xcert0",
- 1 : "ca_x509_ext_xcert1" }
-
- def __init__(self, cfg, dir):
- self.cfg = cfg
- self.dir = dir
- self.cer = dir + "/ca.cer"
- self.key = dir + "/ca.key"
- self.req = dir + "/ca.req"
- self.crl = dir + "/ca.crl"
- self.index = dir + "/index"
- self.serial = dir + "/serial"
- self.crlnum = dir + "/crl_number"
-
- self.env = { "PATH" : os.environ["PATH"],
- "BPKI_DIRECTORY" : dir,
- "RANDFILE" : ".OpenSSL.whines.unless.I.set.this" }
-
- def run_ca(self, *args):
- """
- Run OpenSSL "ca" command with tailored environment variables and common initial
- arguments.
- """
- cmd = (openssl, "ca", "-batch", "-config", self.cfg) + args
- subprocess.check_call(cmd, env = self.env)
-
- def run_req(self, key_file, req_file):
- """
- Run OpenSSL "req" command with tailored environment variables and common arguments.
- """
- if not os.path.exists(key_file) or not os.path.exists(req_file):
- subprocess.check_call((openssl, "req", "-new", "-sha256", "-newkey", "rsa:2048",
- "-config", self.cfg, "-keyout", key_file, "-out", req_file),
- env = self.env)
-
- @staticmethod
- def touch_file(filename, content = None):
- """
- Create dumb little text files expected by OpenSSL "ca" utility.
- """
- if not os.path.exists(filename):
- f = open(filename, "w")
- if content is not None:
- f.write(content)
- f.close()
-
- def setup(self, ca_name):
- """
- Set up this CA. ca_name is an X.509 distinguished name in
- /tag=val/tag=val format.
- """
-
- modified = False
-
- if not os.path.exists(self.dir):
- os.makedirs(self.dir)
- self.touch_file(self.index)
- self.touch_file(self.serial, "01\n")
- self.touch_file(self.crlnum, "01\n")
-
- self.run_req(key_file = self.key, req_file = self.req)
-
- if not os.path.exists(self.cer):
- modified = True
- self.run_ca("-selfsign", "-extensions", "ca_x509_ext_ca", "-subj", ca_name, "-in", self.req, "-out", self.cer)
-
- if not os.path.exists(self.crl):
- modified = True
- self.run_ca("-gencrl", "-out", self.crl)
-
- return modified
-
- def ee(self, ee_name, base_name):
- """
- Issue an end-enity certificate.
- """
- key_file = "%s/%s.key" % (self.dir, base_name)
- req_file = "%s/%s.req" % (self.dir, base_name)
- cer_file = "%s/%s.cer" % (self.dir, base_name)
- self.run_req(key_file = key_file, req_file = req_file)
- if not os.path.exists(cer_file):
- self.run_ca("-extensions", "ca_x509_ext_ee", "-subj", ee_name, "-in", req_file, "-out", cer_file)
- return True
- else:
- return False
-
- def bsc(self, pkcs10):
- """
- Issue BSC certificiate, if we have a PKCS #10 request for it.
- """
-
- if pkcs10 is None:
- return None, None
-
- pkcs10 = base64.b64decode(pkcs10)
-
- assert pkcs10
-
- p = subprocess.Popen((openssl, "dgst", "-md5"), stdin = subprocess.PIPE, stdout = subprocess.PIPE)
- hash = p.communicate(pkcs10)[0].strip()
- if p.wait() != 0:
- raise RuntimeError, "Couldn't hash PKCS#10 request"
-
- req_file = "%s/bsc.%s.req" % (self.dir, hash)
- cer_file = "%s/bsc.%s.cer" % (self.dir, hash)
-
- if not os.path.exists(cer_file):
-
- p = subprocess.Popen((openssl, "req", "-inform", "DER", "-out", req_file), stdin = subprocess.PIPE)
- p.communicate(pkcs10)
- if p.wait() != 0:
- raise RuntimeError, "Couldn't store PKCS #10 request"
-
- self.run_ca("-extensions", "ca_x509_ext_ee", "-in", req_file, "-out", cer_file)
-
- return req_file, cer_file
-
- def fxcert(self, filename, cert, path_restriction = 0):
- """
- Write PEM certificate to file, then cross-certify.
- """
- fn = os.path.join(self.dir, filename)
- f = open(fn, "w")
- f.write(cert)
- f.close()
- return self.xcert(fn, path_restriction)
-
- def xcert(self, cert, path_restriction = 0):
- """
- Cross-certify a certificate represented as a PEM file.
- """
-
- if not cert:
- return None
-
- if not os.path.exists(cert):
- #print "Certificate %s doesn't exist, skipping" % cert
- return None
-
- # Extract public key and subject name from PEM file and hash it so
- # we can use the result as a tag for cross-certifying this cert.
-
- p1 = subprocess.Popen((openssl, "x509", "-noout", "-pubkey", "-subject", "-in", cert), stdout = subprocess.PIPE)
- p2 = subprocess.Popen((openssl, "dgst", "-md5"), stdin = p1.stdout, stdout = subprocess.PIPE)
-
- xcert = "%s/xcert.%s.cer" % (self.dir, p2.communicate()[0].strip())
-
- if p1.wait() != 0 or p2.wait() != 0:
- raise RuntimeError, "Couldn't generate cross-certification tag for %r" % cert
-
- # Cross-certify the cert we were given, if we haven't already.
- # This only works for self-signed certs, due to limitations of the
- # OpenSSL command line tool, but that suffices for our purposes.
-
- if not os.path.exists(xcert):
- self.run_ca("-ss_cert", cert, "-out", xcert, "-extensions", self.path_restriction[path_restriction])
-
- return xcert
-
-def extract_resources():
- """
- Extract RFC 3779 resources from a certificate. Not written yet.
-
- """
- raise NotImplementedError
-
-
-def main(argv = ()):
- """
- Main program. Must be callable from other programs as well as being
- invoked directly when this module is run as a script.
- """
-
- cfg_file = "myrpki.conf"
- section = "myrpki"
-
- opts, argv = getopt.getopt(argv, "c:h:?", ["config=", "help"])
- for o, a in opts:
- if o in ("-h", "--help", "-?"):
- print __doc__
- sys.exit(0)
- elif o in ("-c", "--config"):
- cfg_file = a
- if argv:
- raise RuntimeError, "Unexpected arguments %r" % (argv,)
-
- cfg = ConfigParser.RawConfigParser()
- cfg.readfp(open(cfg_file, "r"), cfg_file)
-
- my_handle = cfg.get(section, "handle")
- roa_csv_file = cfg.get(section, "roa_csv")
- children_csv_file = cfg.get(section, "children_csv")
- parents_csv_file = cfg.get(section, "parents_csv")
- prefix_csv_file = cfg.get(section, "prefix_csv")
- asn_csv_file = cfg.get(section, "asn_csv")
- bpki_dir = cfg.get(section, "bpki_directory")
- xml_filename = cfg.get(section, "xml_filename")
- repository_bpki_certificate = cfg.get(section, "repository_bpki_certificate")
- repository_handle = cfg.get(section, "repository_handle")
-
- global openssl
- openssl = cfg.get(section, "openssl") if cfg.has_option(section, "openssl") else "openssl"
-
- bpki = CA(cfg_file, bpki_dir)
- bpki.setup("/CN=%s TA" % my_handle)
-
- if os.path.exists(xml_filename):
- e = ElementTree(file = xml_filename).getroot()
- bsc_req, bsc_cer = bpki.bsc(e.findtext("{%s}%s" % (namespace, "bpki_bsc_pkcs10")))
- else:
- bsc_req, bsc_cer = None, None
-
- e = Element("myrpki", xmlns = namespace, version = "1", handle = my_handle, repository_handle = repository_handle)
-
- roa_requests.from_csv(roa_csv_file).xml(e)
-
- children.from_csv(
- children_csv_file = children_csv_file,
- prefix_csv_file = prefix_csv_file,
- asn_csv_file = asn_csv_file,
- xcert = bpki.xcert).xml(e)
-
- parents.from_csv(
- parents_csv_file = parents_csv_file,
- xcert = bpki.xcert).xml(e)
-
- PEMElement(e, "bpki_ca_certificate", bpki.cer)
- PEMElement(e, "bpki_crl", bpki.crl)
-
- if os.path.exists(repository_bpki_certificate):
- PEMElement(e, "bpki_repository_certificate", bpki.xcert(repository_bpki_certificate))
-
- if bsc_cer:
- PEMElement(e, "bpki_bsc_certificate", bsc_cer)
-
- if bsc_req:
- PEMElement(e, "bpki_bsc_pkcs10", bsc_req)
-
- # I still miss SYSCAL(RENMWO)
-
- ElementTree(e).write(xml_filename + ".tmp")
- os.rename(xml_filename + ".tmp", xml_filename)
-
-# When this file is run as a script, run main() with command line
-# arguments. main() can't use sys.argv directly as that might be the
-# command line for some other program that loads this module.
-
-if __name__ == "__main__":
- main(sys.argv[1:])