diff options
Diffstat (limited to 'rpkid/rpki/sql.py')
-rw-r--r-- | rpkid/rpki/sql.py | 801 |
1 files changed, 801 insertions, 0 deletions
diff --git a/rpkid/rpki/sql.py b/rpkid/rpki/sql.py new file mode 100644 index 00000000..022e4dd5 --- /dev/null +++ b/rpkid/rpki/sql.py @@ -0,0 +1,801 @@ +# $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 +import rpki.x509, rpki.resource_set, rpki.sundial + +def connect(cfg): + """Connect to a MySQL database using connection parameters from an + rpki.config.parser object. + """ + return MySQLdb.connect(user = cfg.get("sql-username"), + db = cfg.get("sql-database"), + passwd = cfg.get("sql-password")) + +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) + +## @var sql_cache +# Cache of objects pulled from SQL. + +sql_cache = {} + +## @var sql_dirty +# Set of objects that need to be written back to SQL. + +sql_dirty = set() + +def sql_cache_clear(): + """Clear the object cache.""" + sql_cache.clear() + +def sql_assert_pristine(): + """Assert that there are no dirty objects in the cache.""" + assert not sql_dirty, "Dirty objects in SQL cache: %s" % sql_dirty + +def sql_sweep(gctx): + """Write any dirty objects out to SQL.""" + for s in sql_dirty.copy(): + rpki.log.debug("Sweeping %s" % repr(s)) + s.sql_store(gctx) + sql_assert_pristine() + +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 + + @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. + """ + key = (cls, id) + if key in sql_cache: + return 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: + gctx.cur.execute(cls.sql_template.select) + else: + gctx.cur.execute(cls.sql_template.select + " WHERE " + where, args) + results = [] + for row in gctx.cur.fetchall(): + key = (cls, row[0]) + if key in sql_cache: + results.append(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.sql_decode(dict(zip(cls.sql_template.columns, row))) + sql_cache[key] = self + self.sql_in_db = True + self.sql_fetch_hook(gctx) + return self + + def sql_mark_dirty(self): + """Mark this object as needing to be written back to SQL.""" + sql_dirty.add(self) + + def sql_mark_clean(self): + """Mark this object as not needing to be written back to SQL.""" + sql_dirty.discard(self) + + def sql_is_dirty(self): + """Query whether this object needs to be written back to SQL.""" + return self in sql_dirty + + def sql_store(self, gctx): + """Store this object to SQL.""" + if not self.sql_in_db: + gctx.cur.execute(self.sql_template.insert, self.sql_encode()) + setattr(self, self.sql_template.index, gctx.cur.lastrowid) + sql_cache[(self.__class__, gctx.cur.lastrowid)] = self + self.sql_insert_hook(gctx) + else: + gctx.cur.execute(self.sql_template.update, self.sql_encode()) + self.sql_update_hook(gctx) + key = (self.__class__, getattr(self, self.sql_template.index)) + assert key in sql_cache and sql_cache[key] == self + self.sql_mark_clean() + self.sql_in_db = True + + def sql_delete(self, gctx): + """Delete this object from SQL.""" + if self.sql_in_db: + id = getattr(self, self.sql_template.index) + gctx.cur.execute(self.sql_template.delete, id) + self.sql_delete_hook(gctx) + key = (self.__class__, id) + if sql_cache.get(key) == self: + del 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, gctx): + """Customization hook.""" + pass + + def sql_insert_hook(self, gctx): + """Customization hook.""" + pass + + def sql_update_hook(self, gctx): + """Customization hook.""" + self.sql_delete_hook(gctx) + self.sql_insert_hook(gctx) + + def sql_delete_hook(self, gctx): + """Customization hook.""" + pass + +# Some persistant objects are defined in rpki.left_right, since +# they're also left-right PDUs. The rest are defined below, for now. + +class ca_obj(sql_persistant): + """Internal CA object.""" + + sql_template = 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, gctx): + """Fetch parent object to which this CA object links.""" + return rpki.left_right.parent_elt.sql_fetch(gctx, self.parent_id) + + def ca_details(self, gctx): + """Fetch all ca_detail objects that link to this CA object.""" + return ca_detail_obj.sql_fetch_where(gctx, "ca_id = %s", (self.ca_id,)) + + def fetch_pending(self, gctx): + """Fetch the pending ca_details for this CA, if any.""" + return ca_detail_obj.sql_fetch_where(gctx, "ca_id = %s AND state = 'pending'", (self.ca_id,)) + + def fetch_active(self, gctx): + """Fetch the active ca_detail for this CA, if any.""" + return ca_detail_obj.sql_fetch_where1(gctx, "ca_id = %s AND state = 'active'", (self.ca_id,)) + + def fetch_deprecated(self, gctx): + """Fetch deprecated ca_details for this CA, if any.""" + return ca_detail_obj.sql_fetch_where(gctx, "ca_id = %s AND state = 'deprecated'", (self.ca_id,)) + + def fetch_revoked(self, gctx): + """Fetch revoked ca_details for this CA, if any.""" + return ca_detail_obj.sql_fetch_where(gctx, "ca_id = %s AND state = 'revoked'", (self.ca_id,)) + + def construct_sia_uri(self, gctx, 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(gctx) + 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, gctx, 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(gctx, 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(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 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( + gctx = gctx, + parent = parent, + ca = self, + rc = rc, + sia_uri_changed = sia_uri_changed, + old_resources = current_resources) + del cert_map[ski] + assert not cert_map, "Certificates in list_response missing from our database, SKIs %s" % ", ".join(c.cert.hSKI() for c in cert_map.values()) + + @classmethod + def create(cls, gctx, 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.parent_id = parent.parent_id + self.parent_resource_class = rc.class_name + self.sql_store(gctx) + self.sia_uri = self.construct_sia_uri(gctx, parent, rc) + ca_detail = ca_detail_obj.create(gctx, self) + + # This will need a callback when we go event-driven + issue_response = rpki.up_down.issue_pdu.query(gctx, parent, self, ca_detail) + + ca_detail.activate( + gctx = gctx, + ca = self, + cert = issue_response.payload.classes[0].certs[0].cert, + uri = issue_response.payload.classes[0].certs[0].cert_url) + + def delete(self, gctx, 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(gctx) + for ca_detail in self.ca_details(gctx): + ca_detail.delete(gctx, ca, repository) + self.sql_delete(gctx) + + 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, gctx): + """Initiate a rekey operation for this ca. + + Tasks: + + - 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(gctx) + old_detail = self.fetch_active(gctx) + new_detail = ca_detail_obj.create(gctx, self) + + # This will need a callback when we go event-driven + issue_response = rpki.up_down.issue_pdu.query(gctx, parent, self, new_detail) + + new_detail.activate( + gctx = gctx, + 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, gctx): + """Revoke deprecated ca_detail objects associated with this ca.""" + + rpki.log.trace() + + for ca_detail in self.fetch_deprecated(gctx): + ca_detail.revoke(gctx) + +class ca_detail_obj(sql_persistant): + """Internal CA detail object.""" + + sql_template = 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.""" + 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, gctx): + """Fetch CA object to which this ca_detail links.""" + return ca_obj.sql_fetch(gctx, self.ca_id) + + def child_certs(self, gctx, child = None, ski = None, revoked = False, unique = False): + """Fetch all child_cert objects that link to this ca_detail.""" + return rpki.sql.child_cert_obj.fetch(gctx, child, self, ski, revoked, unique) + + def route_origins(self, gctx): + """Fetch all route_origin objects that link to this ca_detail.""" + return rpki.left_right.route_origin_elt.sql_fetch_where(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.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, gctx, 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(gctx) + self.generate_manifest(gctx) + 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(gctx): + child_cert.reissue(gctx, self) + + def delete(self, gctx, ca, repository): + """Delete this ca_detail and all of its associated child_cert objects.""" + + for child_cert in self.child_certs(gctx): + repository.withdraw(gctx, child_cert.cert, child_cert.uri(ca)) + child_cert.sql_delete(gctx) + for child_cert in self.child_certs(gctx, revoked = True): + child_cert.sql_delete(gctx) + repository.withdraw(gctx, self.latest_manifest, self.manifest_uri(ca)) + repository.withdraw(gctx, self.latest_crl, self.crl_uri()) + self.sql_delete(gctx) + + def revoke(self, gctx): + """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(gctx, self) + + if r_msg.payload.ski != self.latest_ca_cert.gSKI(): + raise rpki.exceptions.SKIMismatch + + ca = self.ca(gctx) + parent = ca.parent(gctx) + crl_interval = rpki.sundial.timedelta(seconds = parent.self(gctx).crl_interval) + + nextUpdate = rpki.sundial.datetime.utcnow() + + 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(gctx): + nextUpdate = nextUpdate.later(child_cert.cert.getNotAfter()) + child_cert.revoke(gctx) + + nextUpdate += crl_interval + + self.generate_crl(gctx, nextUpdate) + self.generate_manifest(gctx, 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, gctx, 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(gctx, 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(gctx): + child_resources = child_cert.cert.get_3779resources() + if sia_uri_changed or child_resources.oversized(new_resources): + child_cert.reissue( + gctx = gctx, + ca_detail = self, + resources = child_resources.intersection(new_resources)) + + @classmethod + def create(cls, gctx, ca): + """Create a new ca_detail object for a specified CA.""" + self = cls() + self.ca_id = ca.ca_id + self.state = "pending" + + self.private_key_id = rpki.x509.RSA() + self.private_key_id.generate() + self.public_key = self.private_key_id.get_RSApublic() + + self.manifest_private_key_id = rpki.x509.RSA() + self.manifest_private_key_id.generate() + self.manifest_public_key = self.manifest_private_key_id.get_RSApublic() + + self.sql_store(gctx) + return self + + def generate_manifest_cert(self, ca): + """Generate a new manifest certificate for this ca_detail.""" + + resources = rpki.resource_set.resource_bag( + as = 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.latest_ca_cert.issue( + keypair = self.private_key_id, + subject_key = self.manifest_public_key, + serial = ca.next_manifest_number(), + sia = None, + aia = self.ca_cert_uri, + crldp = self.crl_uri(ca), + resources = resources, + notAfter = self.latest_ca_cert.getNotAfter(), + is_ca = False) + + def issue(self, gctx, 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.sql.child_cert_obj( + 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(gctx) + + ca.parent(gctx).repository(gctx).publish(gctx, child_cert.cert, child_cert.uri(ca)) + + self.generate_manifest(gctx) + + return child_cert + + def generate_crl(self, gctx, 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(gctx) + parent = ca.parent(gctx) + repository = parent.repository(gctx) + crl_interval = rpki.sundial.timedelta(seconds = parent.self(gctx).crl_interval) + now = rpki.sundial.datetime.utcnow() + + if nextUpdate is None: + nextUpdate = now + crl_interval + + certlist = [] + for child_cert in self.child_certs(gctx, revoked = True): + if now > child_cert.cert.getNotAfter() + crl_interval: + child_cert.sql_delete() + else: + certlist.append((child_cert.cert.getSerial(), child_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(gctx, self.latest_crl, self.crl_uri(ca)) + + def generate_manifest(self, gctx, nextUpdate = None): + """Generate a new manifest for this ca_detail.""" + + ca = self.ca(gctx) + parent = ca.parent(gctx) + repository = parent.repository(gctx) + crl_interval = rpki.sundial.timedelta(seconds = parent.self(gctx).crl_interval) + now = rpki.sundial.datetime.utcnow() + + if nextUpdate is None: + nextUpdate = now + crl_interval + + certs = self.child_certs(gctx) + + m = rpki.x509.SignedManifest() + m.build( + serial = ca.next_manifest_number(), + thisUpdate = now, + nextUpdate = nextUpdate, + names_and_objs = [(c.uri_tail(), c.cert) for c in certs], + keypair = self.manifest_private_key_id, + certs = rpki.x509.X509_chain(self.latest_manifest_cert)) + self.latest_manifest = m + + repository.publish(gctx, self.latest_manifest, self.manifest_uri(ca)) + +class child_cert_obj(sql_persistant): + """Certificate that has been issued to a child.""" + + sql_template = template("child_cert", "child_cert_id", ("cert", rpki.x509.X509), "child_id", "ca_detail_id", "ski", ("revoked", rpki.sundial.datetime)) + + def __init__(self, child_id = None, ca_detail_id = None, cert = None): + """Initialize a child_cert_obj.""" + self.child_id = child_id + self.ca_detail_id = ca_detail_id + self.cert = cert + self.revoked = None + if child_id or ca_detail_id or cert: + self.sql_mark_dirty() + + def child(self, gctx): + """Fetch child object to which this child_cert object links.""" + return rpki.left_right.child_elt.sql_fetch(gctx, self.child_id) + + def ca_detail(self, gctx): + """Fetch ca_detail object to which this child_cert object links.""" + return ca_detail_obj.sql_fetch(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, gctx): + """Mark a child cert as revoked.""" + if self.revoked is None: + rpki.log.debug("Revoking %s" % repr(self)) + self.revoked = rpki.sundial.datetime.utcnow() + ca = self.ca_detail(gctx).ca(gctx) + repository = ca.parent(gctx).repository(gctx) + repository.withdraw(gctx, self.cert, self.uri(ca)) + self.sql_mark_dirty() + + def reissue(self, gctx, 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(gctx) + child = self.child(gctx) + + old_resources = self.cert.get_3779resources() + old_sia = self.cert.get_SIA() + old_ca_detail = self.ca_detail(gctx) + + 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( + gctx = gctx, + 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(gctx = gctx, ca_detail = ca_detail, ski = self.ski): + if cert is not child_cert: + cert.revoke(gctx) + + return child_cert + + @classmethod + def fetch(cls, gctx, child = None, ca_detail = None, ski = None, revoked = False, 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 = "revoked IS" + if revoked: + where += " NOT" + where += " NULL" + if child: + where += " AND child_id = %s" + args.append(child.child_id) + if ca_detail: + where += " AND ca_detail_id = %s" + args.append(ca_detail.ca_detail_id) + if ski: + where += " AND ski = %s" + args.append(ski) + if unique: + return cls.sql_fetch_where1(gctx, where, args) + else: + return cls.sql_fetch_where(gctx, where, args) |