diff options
Diffstat (limited to 'rpki/x509.py')
-rw-r--r-- | rpki/x509.py | 137 |
1 files changed, 129 insertions, 8 deletions
diff --git a/rpki/x509.py b/rpki/x509.py index a7e4d17a..89b598d4 100644 --- a/rpki/x509.py +++ b/rpki/x509.py @@ -57,6 +57,7 @@ def base64_with_linebreaks(der): Encode DER (really, anything) as Base64 text, with linebreaks to keep the result (sort of) readable. """ + b = base64.b64encode(der) n = len(b) return "\n" + "\n".join(b[i : min(i + 64, n)] for i in xrange(0, n, 64)) + "\n" @@ -81,6 +82,27 @@ def first_rsync_uri(xia): return uri return None +def sha1(data): + """ + Calculate SHA-1 digest of some data. + Convenience wrapper around rpki.POW.Digest class. + """ + + d = rpki.POW.Digest(rpki.POW.SHA1_DIGEST) + d.update(data) + return d.digest() + +def sha256(data): + """ + Calculate SHA-256 digest of some data. + Convenience wrapper around rpki.POW.Digest class. + """ + + d = rpki.POW.Digest(rpki.POW.SHA256_DIGEST) + d.update(data) + return d.digest() + + class X501DN(object): """ Class to hold an X.501 Distinguished Name. @@ -207,12 +229,14 @@ class DER_object(object): """ Test whether this object is empty. """ + return all(getattr(self, a, None) is None for a in self.formats) def clear(self): """ Make this object empty. """ + for a in self.formats + self.other_clear: setattr(self, a, None) self.filename = None @@ -223,6 +247,7 @@ class DER_object(object): """ Initialize a DER_object. """ + self.clear() if len(kw): self.set(**kw) @@ -271,6 +296,7 @@ class DER_object(object): """ Check for updates to a DER object that auto-updates from a file. """ + if self.filename is None: return try: @@ -297,10 +323,19 @@ class DER_object(object): else: self.lastfail = None + @property + def mtime(self): + """ + Retrieve os.stat().st_mtime for auto-update files. + """ + + return os.stat(self.filename).st_mtime + def check(self): """ Perform basic checks on a DER object. """ + self.check_auto_update() assert not self.empty() @@ -309,6 +344,7 @@ class DER_object(object): Set the POW value of this object based on a PEM input value. Subclasses may need to override this. """ + assert self.empty() self.POW = self.POW_class.pemRead(pem) @@ -317,6 +353,7 @@ class DER_object(object): Get the DER value of this object. Subclasses may need to override this method. """ + self.check() if self.DER: return self.DER @@ -330,6 +367,7 @@ class DER_object(object): Get the rpki.POW value of this object. Subclasses may need to override this method. """ + self.check() if not self.POW: # pylint: disable=E0203 self.POW = self.POW_class.derRead(self.get_DER()) @@ -339,18 +377,21 @@ class DER_object(object): """ Get the Base64 encoding of the DER value of this object. """ + return base64_with_linebreaks(self.get_DER()) def get_PEM(self): """ Get the PEM representation of this object. """ + return self.get_POW().pemWrite() def __cmp__(self, other): """ Compare two DER-encoded objects. """ + if self is None and other is None: return 0 elif self is None: @@ -367,6 +408,7 @@ class DER_object(object): 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 "" @@ -375,6 +417,7 @@ class DER_object(object): 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): @@ -382,6 +425,7 @@ class DER_object(object): 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 "" @@ -390,24 +434,28 @@ class DER_object(object): 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, if supported. """ + return self.get_POW().getAKI() def get_SKI(self): """ Get the SKI extension from this object, if supported. """ + return self.get_POW().getSKI() def get_EKU(self): """ Get the Extended Key Usage extension from this object, if supported. """ + return self.get_POW().getEKU() def get_SIA(self): @@ -415,6 +463,7 @@ class DER_object(object): Get the SIA extension from this object. Only works for subclasses that support getSIA(). """ + return self.get_POW().getSIA() def get_sia_directory_uri(self): @@ -422,6 +471,7 @@ class DER_object(object): Get SIA directory (id-ad-caRepository) URI from this object. Only works for subclasses that support getSIA(). """ + sia = self.get_POW().getSIA() return None if sia is None else first_rsync_uri(sia[0]) @@ -430,6 +480,7 @@ class DER_object(object): Get SIA manifest (id-ad-rpkiManifest) URI from this object. Only works for subclasses that support getSIA(). """ + sia = self.get_POW().getSIA() return None if sia is None else first_rsync_uri(sia[1]) @@ -438,6 +489,7 @@ class DER_object(object): Get SIA object (id-ad-signedObject) URI from this object. Only works for subclasses that support getSIA(). """ + sia = self.get_POW().getSIA() return None if sia is None else first_rsync_uri(sia[2]) @@ -446,6 +498,7 @@ class DER_object(object): Get the SIA extension from this object. Only works for subclasses that support getAIA(). """ + return self.get_POW().getAIA() def get_aia_uri(self): @@ -453,6 +506,7 @@ class DER_object(object): Get AIA (id-ad-caIssuers) URI from this object. Only works for subclasses that support getAIA(). """ + return first_rsync_uri(self.get_POW().getAIA()) def get_basicConstraints(self): @@ -460,6 +514,7 @@ class DER_object(object): Get the basicConstraints extension from this object. Only works for subclasses that support getExtension(). """ + return self.get_POW().getBasicConstraints() def is_CA(self): @@ -467,6 +522,7 @@ class DER_object(object): Return True if and only if object has the basicConstraints extension and its cA value is true. """ + basicConstraints = self.get_basicConstraints() return basicConstraints is not None and basicConstraints[0] @@ -474,6 +530,7 @@ class DER_object(object): """ Get RFC 3779 resources as rpki.resource_set objects. """ + resources = rpki.resource_set.resource_bag.from_POW_rfc3779(self.get_POW().getRFC3779()) try: resources.valid_until = self.getNotAfter() @@ -486,12 +543,14 @@ class DER_object(object): """ 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): @@ -522,11 +581,11 @@ class DER_object(object): provide more information, but should make sure to include at least this information at the start of the tracking line. """ + try: - d = rpki.POW.Digest(rpki.POW.SHA1_DIGEST) - d.update(self.get_DER()) - return "%s %s %s" % (uri, self.creation_timestamp, - "".join(("%02X" % ord(b) for b in d.digest()))) + return "%s %s %s" % (uri, + self.creation_timestamp, + "".join(("%02X" % ord(b) for b in sha1(self.get_DER())))) except: # pylint: disable=W0702 return uri @@ -534,12 +593,14 @@ class DER_object(object): """ Pickling protocol -- pickle the DER encoding. """ + return self.get_DER() def __setstate__(self, state): """ Pickling protocol -- unpickle the DER encoding. """ + self.set(DER = state) class X509(DER_object): @@ -559,48 +620,56 @@ class X509(DER_object): """ Get the issuer of this certificate. """ + return X501DN.from_POW(self.get_POW().getIssuer()) def getSubject(self): """ Get the subject of this certificate. """ + return X501DN.from_POW(self.get_POW().getSubject()) def getNotBefore(self): """ Get the inception time of this certificate. """ + return self.get_POW().getNotBefore() def getNotAfter(self): """ Get the expiration time of this certificate. """ + return self.get_POW().getNotAfter() 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 PublicKey(POW = self.get_POW().getPublicKey()) def get_SKI(self): """ Get the SKI extension from this object. """ + return self.get_POW().getSKI() 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, @@ -743,6 +812,7 @@ class X509(DER_object): """ Issue a BPKI certificate with values taking from an existing certificate. """ + return self.bpki_certify( keypair = keypair, subject_name = source_cert.getSubject(), @@ -759,6 +829,7 @@ class X509(DER_object): """ Issue a self-signed BPKI CA certificate. """ + return cls._bpki_certify( keypair = keypair, issuer_name = subject_name, @@ -775,6 +846,7 @@ class X509(DER_object): """ Issue a normal BPKI certificate. """ + assert keypair.get_public() == self.getPublicKey() return self._bpki_certify( keypair = keypair, @@ -833,6 +905,7 @@ class X509(DER_object): 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) @@ -842,6 +915,7 @@ class X509(DER_object): """ Time at which this object was created. """ + return self.getNotBefore() class PKCS10(DER_object): @@ -869,6 +943,7 @@ class PKCS10(DER_object): """ Get the DER value of this certification request. """ + self.check() if self.DER: return self.DER @@ -881,6 +956,7 @@ class PKCS10(DER_object): """ Get the rpki.POW value of this certification request. """ + self.check() if not self.POW: # pylint: disable=E0203 self.POW = rpki.POW.PKCS10.derRead(self.get_DER()) @@ -890,18 +966,21 @@ class PKCS10(DER_object): """ Extract the subject name from this certification request. """ + return X501DN.from_POW(self.get_POW().getSubject()) def getPublicKey(self): """ Extract the public key from this certification request. """ + return PublicKey(POW = self.get_POW().getPublicKey()) def get_SKI(self): """ Compute SKI for public key from this certification request. """ + return self.getPublicKey().get_SKI() @@ -1150,6 +1229,7 @@ class PrivateKey(DER_object): """ Get the DER value of this keypair. """ + self.check() if self.DER: return self.DER @@ -1162,6 +1242,7 @@ class PrivateKey(DER_object): """ Get the rpki.POW value of this keypair. """ + self.check() if not self.POW: # pylint: disable=E0203 self.POW = rpki.POW.Asymmetric.derReadPrivate(self.get_DER()) @@ -1171,12 +1252,14 @@ class PrivateKey(DER_object): """ Get the PEM representation of this keypair. """ + return self.get_POW().pemWritePrivate() def _set_PEM(self, pem): """ Set the POW value of this keypair from a PEM string. """ + assert self.empty() self.POW = self.POW_class.pemReadPrivate(pem) @@ -1184,18 +1267,21 @@ class PrivateKey(DER_object): """ Get the DER encoding of the public key from this keypair. """ + return self.get_POW().derWritePublic() def get_SKI(self): """ Calculate the SKI of this keypair. """ + return self.get_POW().calculateSKI() def get_public(self): """ Convert the public key of this keypair into a PublicKey object. """ + return PublicKey(DER = self.get_public_DER()) class PublicKey(DER_object): @@ -1209,6 +1295,7 @@ class PublicKey(DER_object): """ Get the DER value of this public key. """ + self.check() if self.DER: return self.DER @@ -1221,6 +1308,7 @@ class PublicKey(DER_object): """ Get the rpki.POW value of this public key. """ + self.check() if not self.POW: # pylint: disable=E0203 self.POW = rpki.POW.Asymmetric.derReadPublic(self.get_DER()) @@ -1230,12 +1318,14 @@ class PublicKey(DER_object): """ Get the PEM representation of this public key. """ + return self.get_POW().pemWritePublic() def _set_PEM(self, pem): """ Set the POW value of this public key from a PEM string. """ + assert self.empty() self.POW = self.POW_class.pemReadPublic(pem) @@ -1243,6 +1333,7 @@ class PublicKey(DER_object): """ Calculate the SKI of this public key. """ + return self.get_POW().calculateSKI() class KeyParams(DER_object): @@ -1266,6 +1357,7 @@ class RSA(PrivateKey): """ Generate a new keypair. """ + if not quiet: logger.debug("Generating new %d-bit RSA key", keylength) if generate_insecure_debug_only_rsa_key is not None: @@ -1348,6 +1440,7 @@ class CMS_object(DER_object): """ Get the DER value of this CMS_object. """ + self.check() if self.DER: return self.DER @@ -1360,6 +1453,7 @@ class CMS_object(DER_object): """ Get the rpki.POW value of this CMS_object. """ + self.check() if not self.POW: # pylint: disable=E0203 self.POW = self.POW_class.derRead(self.get_DER()) @@ -1369,6 +1463,7 @@ class CMS_object(DER_object): """ Extract signingTime from CMS signed attributes. """ + return self.get_POW().signingTime() def verify(self, ta): @@ -1540,6 +1635,7 @@ class CMS_object(DER_object): """ Time at which this object was created. """ + return self.get_signingTime() @@ -1561,6 +1657,7 @@ class Wrapped_CMS_object(CMS_object): """ Get the inner content of this Wrapped_CMS_object. """ + if self.content is None: raise rpki.exceptions.CMSContentNotSet("Inner content of CMS object %r is not set" % self) return self.content @@ -1569,6 +1666,7 @@ class Wrapped_CMS_object(CMS_object): """ Set the (inner) content of this Wrapped_CMS_object, clearing the wrapper. """ + self.clear() self.content = content @@ -1651,12 +1749,14 @@ class SignedManifest(DER_CMS_object): """ Get thisUpdate value from this manifest. """ + return self.get_POW().getThisUpdate() def getNextUpdate(self): """ Get nextUpdate value from this manifest. """ + return self.get_POW().getNextUpdate() @classmethod @@ -1667,9 +1767,7 @@ class SignedManifest(DER_CMS_object): filelist = [] for name, obj in names_and_objs: - d = rpki.POW.Digest(rpki.POW.SHA256_DIGEST) - d.update(obj.get_DER()) - filelist.append((name.rpartition("/")[2], d.digest())) + filelist.append((name.rpartition("/")[2], sha256(obj.get_DER()))) filelist.sort(key = lambda x: x[0]) obj = cls.POW_class() @@ -1697,6 +1795,7 @@ class ROA(DER_CMS_object): """ Build a ROA. """ + ipv4 = ipv4.to_POW_roa_tuple() if ipv4 else None ipv6 = ipv6.to_POW_roa_tuple() if ipv6 else None obj = cls.POW_class() @@ -1712,6 +1811,7 @@ class ROA(DER_CMS_object): Return a string containing data we want to log when tracking how objects move through the RPKI system. """ + msg = DER_CMS_object.tracking_data(self, uri) try: self.extract_if_needed() @@ -1788,12 +1888,13 @@ class XML_CMS_object(Wrapped_CMS_object): ## @var check_outbound_schema # If set, perform RelaxNG schema check on outbound messages. - check_outbound_schema = False + check_outbound_schema = True def encode(self): """ Encode inner content for signing. """ + return lxml.etree.tostring(self.get_content(), pretty_print = True, encoding = self.encoding, @@ -1803,12 +1904,14 @@ class XML_CMS_object(Wrapped_CMS_object): """ 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, @@ -1818,6 +1921,7 @@ class XML_CMS_object(Wrapped_CMS_object): """ Handle XML RelaxNG schema check. """ + try: self.schema.assertValid(self.get_content()) except lxml.etree.DocumentInvalid: @@ -1830,6 +1934,7 @@ class XML_CMS_object(Wrapped_CMS_object): """ 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() @@ -1838,6 +1943,7 @@ class XML_CMS_object(Wrapped_CMS_object): """ Wrap an XML PDU in CMS and return its DER encoding. """ + if self.saxify is None: self.set_content(msg) else: @@ -1853,6 +1959,7 @@ class XML_CMS_object(Wrapped_CMS_object): """ Unwrap a CMS-wrapped XML PDU and return Python objects. """ + if self.dump_inbound_cms: self.dump_inbound_cms.dump(self) self.verify(ta) @@ -1869,6 +1976,7 @@ class XML_CMS_object(Wrapped_CMS_object): timestamp. Raises an exception if the recorded timestamp is more recent, otherwise returns the new timestamp. """ + new_timestamp = self.get_signingTime() if timestamp is not None and timestamp > new_timestamp: if context: @@ -1884,6 +1992,7 @@ class XML_CMS_object(Wrapped_CMS_object): "last_cms_timestamp" field of an SQL object and stores the new timestamp back in that same field. """ + obj.last_cms_timestamp = self.check_replay(obj.last_cms_timestamp, *context) obj.sql_mark_dirty() @@ -1913,6 +2022,7 @@ class Ghostbuster(Wrapped_CMS_object): Encode inner content for signing. At the moment we're treating the VCard as an opaque byte string, so no encoding needed here. """ + return self.get_content() def decode(self, vcard): @@ -1920,6 +2030,7 @@ class Ghostbuster(Wrapped_CMS_object): Decode XML and set inner content. At the moment we're treating the VCard as an opaque byte string, so no encoding needed here. """ + self.content = vcard @classmethod @@ -1927,6 +2038,7 @@ class Ghostbuster(Wrapped_CMS_object): """ Build a Ghostbuster record. """ + self = cls() self.set_content(vcard) self.sign(keypair, certs) @@ -1944,6 +2056,7 @@ class CRL(DER_object): """ Get the DER value of this CRL. """ + self.check() if self.DER: return self.DER @@ -1956,6 +2069,7 @@ class CRL(DER_object): """ Get the rpki.POW value of this CRL. """ + self.check() if not self.POW: # pylint: disable=E0203 self.POW = rpki.POW.CRL.derRead(self.get_DER()) @@ -1965,24 +2079,28 @@ class CRL(DER_object): """ Get thisUpdate value from this CRL. """ + return self.get_POW().getThisUpdate() def getNextUpdate(self): """ Get nextUpdate value from this CRL. """ + return self.get_POW().getNextUpdate() def getIssuer(self): """ Get issuer value of this CRL. """ + return X501DN.from_POW(self.get_POW().getIssuer()) def getCRLNumber(self): """ Get CRL Number value for this CRL. """ + return self.get_POW().getCRLNumber() @classmethod @@ -1990,6 +2108,7 @@ class CRL(DER_object): """ Generate a new CRL. """ + crl = rpki.POW.CRL() crl.setVersion(version) crl.setIssuer(issuer.getSubject().get_POW()) @@ -2006,6 +2125,7 @@ class CRL(DER_object): """ Time at which this object was created. """ + return self.getThisUpdate() ## @var uri_dispatch_map @@ -2024,4 +2144,5 @@ def uri_dispatch(uri): """ Return the Python class object corresponding to a given URI. """ + return uri_dispatch_map[os.path.splitext(uri)[1]] |