diff --git a/cursive/certificate_utils.py b/cursive/certificate_utils.py new file mode 100644 index 0000000..046f695 --- /dev/null +++ b/cursive/certificate_utils.py @@ -0,0 +1,350 @@ +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +"""Support certificate validation.""" + +from cryptography.hazmat.primitives.asymmetric import ec +from cryptography.hazmat.primitives.asymmetric import padding +from cryptography.hazmat.primitives.asymmetric import rsa +from cryptography import x509, exceptions as cryptography_exceptions +from oslo_log import log as logging +from oslo_utils import timeutils + +from cursive import exception +from cursive import signature_utils + +LOG = logging.getLogger(__name__) + + +def is_within_valid_dates(certificate): + """Determine if the certificate is outside its valid date range. + + :param certificate: the cryptography certificate object + :return: False if the certificate valid time range does not include + now, True otherwise. + """ + # Get now in UTC, since certificate returns times in UTC + now = timeutils.utcnow() + + # Confirm the certificate valid time range includes now + if now < certificate.not_valid_before: + return False + elif now > certificate.not_valid_after: + return False + return True + + +def is_issuer(issuing_certificate, issued_certificate): + """Determine if the issuing cert is the parent of the issued cert. + + Determine if the issuing certificate is the parent of the issued + certificate by: + * conducting subject and issuer name matching, and + * verifying the signature of the issued certificate with the issuing + certificate's public key + + :param issuing_certificate: the cryptography certificate object that + is the potential parent of the issued certificate + :param issued_certificate: the cryptography certificate object that + is the potential child of the issuing certificate + :return: True if the issuing certificate is the parent of the issued + certificate, False otherwise. + """ + if (issuing_certificate is None) or (issued_certificate is None): + return False + elif issuing_certificate.subject != issued_certificate.issuer: + return False + else: + try: + verify_certificate_signature( + issuing_certificate, + issued_certificate + ) + except cryptography_exceptions.InvalidSignature: + # If verification fails, an exception is expected. + return False + return True + + +def can_sign_certificates(certificate, certificate_uuid=''): + """Determine if the certificate can sign other certificates. + + :param certificate: the cryptography certificate object + :param certificate_uuid: the uuid of the certificate + :return: False if the certificate cannot sign other certificates, + True otherwise. + """ + try: + basic_constraints = certificate.extensions.get_extension_for_oid( + x509.oid.ExtensionOID.BASIC_CONSTRAINTS + ).value + except x509.extensions.ExtensionNotFound: + LOG.debug( + "Certificate '%s' does not have a basic constraints extension.", + certificate_uuid) + return False + + try: + key_usage = certificate.extensions.get_extension_for_oid( + x509.oid.ExtensionOID.KEY_USAGE + ).value + except x509.extensions.ExtensionNotFound: + LOG.debug( + "Certificate '%s' does not have a key usage extension.", + certificate_uuid) + return False + + if basic_constraints.ca and key_usage.key_cert_sign: + return True + + if not basic_constraints.ca: + LOG.debug( + "Certificate '%s' is not marked as a CA in its basic constraints " + "extension.", + certificate_uuid) + if not key_usage.key_cert_sign: + LOG.debug( + "Certificate '%s' is not marked for verifying certificate " + "signatures in its key usage extension.", + certificate_uuid) + + return False + + +def verify_certificate_signature(signing_certificate, certificate): + """Verify that the certificate was signed correctly. + + :param signing_certificate: the cryptography certificate object used to + sign the certificate + :param certificate: the cryptography certificate object that was signed + by the signing certificate + :raises: cryptography.exceptions.InvalidSignature if certificate signature + verification fails. + """ + signature_hash_algorithm = certificate.signature_hash_algorithm + signature_bytes = certificate.signature + signer_public_key = signing_certificate.public_key() + + if isinstance(signer_public_key, rsa.RSAPublicKey): + verifier = signer_public_key.verifier( + signature_bytes, padding.PKCS1v15(), signature_hash_algorithm + ) + elif isinstance(signer_public_key, ec.EllipticCurvePublicKey): + verifier = signer_public_key.verifier( + signature_bytes, ec.ECDSA(signature_hash_algorithm) + ) + else: + verifier = signer_public_key.verifier( + signature_bytes, signature_hash_algorithm + ) + + verifier.update(certificate.tbs_certificate_bytes) + verifier.verify() + + +def verify_certificate(context, certificate_uuid, + trusted_certificate_uuids, + enforce_valid_dates=True, + enforce_signing_extensions=True, + enforce_path_length=True): + """Validate a certificate against a set of trusted certificates. + + From the key manager, load the set of trusted certificates and the + certificate to validate. Store the trusted certificates in a certificate + verification context. Use the context to verify that the certificate is + cryptographically linked to at least one of the trusted certificates. + + :param context: the user context for authentication + :param certificate_uuid: the uuid of a certificate to validate, stored in + the key manager + :param trusted_certificate_uuids: a list containing the uuids of trusted + certificates stored in the key manager + :param enforce_valid_dates: a boolean indicating whether date checking + should be enforced during certificate verification, defaults to + True + :param enforce_signing_extensions: a boolean indicating whether extension + checking should be enforced during certificate verification, + defaults to True + :param enforce_path_length: a boolean indicating whether path length + constraints should be enforced during certificate verification, + defaults to True + :raises: SignatureVerificationError if the certificate verification fails + for any reason. + """ + trusted_certificates = list() + for uuid in trusted_certificate_uuids: + try: + trusted_certificates.append( + (uuid, signature_utils.get_certificate(context, uuid)) + ) + except exception.SignatureVerificationError: + LOG.warning("Skipping trusted certificate: %(id)s" % {'id': uuid}) + + certificate = signature_utils.get_certificate(context, certificate_uuid) + certificate_context = CertificateVerificationContext( + trusted_certificates, + enforce_valid_dates=enforce_valid_dates, + enforce_signing_extensions=enforce_signing_extensions, + enforce_path_length=enforce_path_length + ) + certificate_context.update(certificate) + certificate_context.verify() + + +class CertificateVerificationContext(object): + """A collection of signing certificates. + + A collection of signing certificates that may be used to verify the + signatures of other certificates. + """ + + def __init__(self, certificate_tuples, enforce_valid_dates=True, + enforce_signing_extensions=True, + enforce_path_length=True): + self._signing_certificates = [] + for certificate_tuple in certificate_tuples: + certificate_uuid, certificate = certificate_tuple + if not isinstance(certificate, x509.Certificate): + LOG.error( + "A signing certificate must be an x509.Certificate object." + ) + continue + + if enforce_valid_dates: + if not is_within_valid_dates(certificate): + LOG.warning( + "Certificate '%s' is outside its valid date range and " + "cannot be used as a signing certificate.", + certificate_uuid) + continue + + if enforce_signing_extensions: + if not can_sign_certificates(certificate, certificate_uuid): + LOG.warning( + "Certificate '%s' is not configured to act as a " + "signing certificate. It will not be used as a " + "signing certificate.", + certificate_uuid) + continue + self._signing_certificates.append(certificate_tuple) + + self._signed_certificate = None + self._enforce_valid_dates = enforce_valid_dates + self._enforce_path_length = enforce_path_length + + def update(self, certificate): + """Process the certificate to be verified. + + Raises an exception if the certificate is invalid. Stores it + otherwise. + + :param certificate: the cryptography certificate to be verified + :raises: SignatureVerificationError if the certificate is not of the + right type or if it is outside its valid date range. + """ + if not isinstance(certificate, x509.Certificate): + raise exception.SignatureVerificationError( + "The certificate must be an x509.Certificate object." + ) + + if self._enforce_valid_dates: + if not is_within_valid_dates(certificate): + raise exception.SignatureVerificationError( + "The certificate is outside its valid date range." + ) + + self._signed_certificate = certificate + + def verify(self): + """Locate the certificate's signing certificate and verify it. + + Locate the certificate's signing certificate in the context + certificate cache, using both subject/issuer name matching and + signature verification. If the certificate is self-signed, verify that + it is also located in the context's certificate cache. Construct the + certificate chain from certificates in the context certificate cache. + Verify that the signing certificate can have a sufficient number of + child certificates to support the chain. + + :raises: SignatureVerificationError if certificate validation fails + for any reason, including mismatched signatures or a failure + to find the required signing certificate. + """ + signed_certificate = self._signed_certificate + certificate_chain = [('base', signed_certificate)] + + # Build the certificate chain. + while True: + signing_certificate_tuple = None + + # Search for the signing certificate + for certificate_tuple in self._signing_certificates: + _, candidate = certificate_tuple + if is_issuer(candidate, signed_certificate): + signing_certificate_tuple = certificate_tuple + break + + # If a valid signing certificate is found, prepare to find the + # next link in the certificate chain. Otherwise, raise an error. + if signing_certificate_tuple: + # If the certificate is self-signed, the root of the + # certificate chain has been found. Otherwise, repeat the + # verification process using the newly found signing + # certificate. + if signed_certificate == signing_certificate_tuple[1]: + break + else: + certificate_chain.insert(0, signing_certificate_tuple) + signed_certificate = signing_certificate_tuple[1] + else: + uuid = certificate_chain[0][0] + raise exception.SignatureVerificationError( + "Certificate chain building failed. Could not locate the " + "signing certificate for %s in the set of trusted " + "certificates." % + "the base certificate" if uuid == 'base' + else "certificate '%s'" % uuid + ) + + if self._enforce_path_length: + # Verify that each certificate's path length constraint allows + # for it to support the rest of the certificate chain. + for i in range(len(certificate_chain)): + certificate = certificate_chain[i][1] + + # No need to check the last certificate in the chain. + if certificate == certificate_chain[-1][1]: + break + + try: + constraints = certificate.extensions.get_extension_for_oid( + x509.oid.ExtensionOID.BASIC_CONSTRAINTS + ).value + except x509.extensions.ExtensionNotFound: + raise exception.SignatureVerificationError( + "Certificate validation failed. The signing " + "certificate '%s' does not have a basic constraints " + "extension." % certificate_chain[i][0] + ) + + # Path length only applies to non-self-issued intermediate + # certificates. Do not include the current or end certificates + # when computing path length. + chain_length = len(certificate_chain[i:]) + chain_length = (chain_length - 2) if chain_length > 2 else 0 + if constraints.path_length < chain_length: + raise exception.SignatureVerificationError( + "Certificate validation failed. The signing " + "certificate '%s' is not configured to support " + "certificate chains of sufficient " + "length." % certificate_chain[i][0] + ) diff --git a/cursive/signature_utils.py b/cursive/signature_utils.py index d676193..26cc674 100644 --- a/cursive/signature_utils.py +++ b/cursive/signature_utils.py @@ -26,7 +26,6 @@ from cryptography import x509 from oslo_log import log as logging from oslo_serialization import base64 from oslo_utils import encodeutils -from oslo_utils import timeutils from cursive import exception from cursive.i18n import _, _LE @@ -70,6 +69,7 @@ MASK_GEN_ALGORITHMS = { 'MGF1': padding.MGF1, } + # Required image property names (SIGNATURE, HASH_METHOD, KEY_TYPE, CERT_UUID) = ( 'img_signature', @@ -336,28 +336,4 @@ def get_certificate(context, signature_certificate_uuid): certificate = x509.load_der_x509_certificate(cert_data, default_backend()) - # verify the certificate - verify_certificate(certificate) - return certificate - - -def verify_certificate(certificate): - """Verify that the certificate has not expired. - - :param certificate: the cryptography certificate object - :raises: SignatureVerificationError if the certificate valid time range - does not include now - """ - # Get now in UTC, since certificate returns times in UTC - now = timeutils.utcnow() - - # Confirm the certificate valid time range includes now - if now < certificate.not_valid_before: - raise exception.SignatureVerificationError( - reason=_('Certificate is not valid before: %s UTC') - % certificate.not_valid_before) - elif now > certificate.not_valid_after: - raise exception.SignatureVerificationError( - reason=_('Certificate is not valid after: %s UTC') - % certificate.not_valid_after) diff --git a/cursive/tests/unit/data/child_cert.pem b/cursive/tests/unit/data/child_cert.pem new file mode 100644 index 0000000..bfe807b --- /dev/null +++ b/cursive/tests/unit/data/child_cert.pem @@ -0,0 +1,87 @@ +Certificate: + Data: + Version: 3 (0x2) + Serial Number: 10 (0xa) + Signature Algorithm: sha256WithRSAEncryption + Issuer: C=US, ST=Test, L=Test, O=Test, CN=Test Parent + Validity + Not Before: Oct 3 18:02:45 2017 GMT + Not After : Oct 1 18:02:45 2027 GMT + Subject: C=US, ST=Test, L=Test, O=Test, CN=Test Child + Subject Public Key Info: + Public Key Algorithm: rsaEncryption + Public-Key: (2048 bit) + Modulus: + 00:b2:45:31:e4:99:12:1f:0f:c3:5b:78:98:39:a5: + d0:da:c6:9f:38:23:09:df:fd:35:b6:95:b6:37:5d: + b6:49:f2:a5:f1:62:75:62:41:09:9d:36:e5:53:c8: + 82:1a:5c:9d:2a:fd:03:9c:a9:00:6d:28:b3:29:bb: + cf:f3:eb:0f:5c:c9:81:8d:69:e1:04:f7:9a:1c:09: + 33:ab:54:c1:ac:0c:d7:d1:11:79:6c:6f:c0:2b:54: + 9e:c2:86:85:05:a3:e4:70:06:84:42:eb:8b:c0:0e: + 3a:73:16:cd:13:79:a5:43:e6:89:8b:c3:7f:6b:04: + cd:7f:34:6b:4a:47:65:c3:4a:6a:d3:ea:8e:57:34: + 5d:39:18:fc:d0:8e:e4:f6:ff:74:86:a0:98:06:67: + 40:0c:8f:a6:5e:46:9d:ed:b9:25:99:7c:4c:62:b8: + 19:ae:12:1e:33:0b:d3:43:b9:3c:bc:5a:f3:6b:c6: + a9:1c:c1:ce:99:1f:64:b7:a3:8d:ed:c8:3e:95:75: + 19:e5:ce:51:f1:11:f1:c0:58:76:87:ee:42:12:a4: + ff:8e:c6:e8:42:3d:b4:df:c7:be:a6:c7:ea:6c:88: + 04:4b:d3:f3:9b:7f:d4:db:87:21:55:36:2e:3c:1c: + c9:21:4a:2f:7f:51:f0:08:d7:21:ea:75:c4:e2:78: + 91:9d + Exponent: 65537 (0x10001) + X509v3 extensions: + X509v3 Subject Key Identifier: + C2:03:EA:FC:7E:70:7F:34:21:C1:BE:33:0E:8A:E0:7E:C6:A2:21:1B + X509v3 Authority Key Identifier: + keyid:7A:BE:7D:09:5A:5F:5C:DE:CC:82:1A:3B:FE:A8:ED:CA:BA:16:58:49 + + X509v3 Basic Constraints: + CA:TRUE, pathlen:0 + X509v3 Key Usage: + Certificate Sign, CRL Sign + X509v3 Subject Alternative Name: + DNS:example.com, DNS:www.example.com, DNS:mail.example.com, DNS:ftp.example.com + Netscape Comment: + OpenSSL Generated Certificate + Signature Algorithm: sha256WithRSAEncryption + 10:46:2e:1e:37:b8:10:4a:8c:e3:76:7c:05:57:76:34:05:0b: + e2:ed:b3:1b:28:20:2b:56:9b:2d:59:70:e5:4e:5e:ce:a8:11: + d5:c1:9b:e7:c8:0e:61:2b:63:ae:d2:1b:ec:cf:75:31:d0:4f: + 35:86:c2:51:22:64:c3:07:a7:c4:6b:13:57:cc:e5:d9:86:8d: + b4:73:45:c5:ca:48:b7:b6:02:1e:c7:de:71:c6:5f:2a:64:7d: + b5:5b:16:9a:27:7d:5f:3c:8a:5e:95:38:7f:c0:7e:d4:39:3f: + 36:60:7d:7d:8e:9f:72:06:d4:69:7a:e5:45:3f:e2:c9:eb:7f: + 5f:74:1a:6b:6c:b8:a1:08:05:d9:25:ee:d4:97:db:5a:72:1f: + 4a:06:a9:86:76:41:58:34:0b:5a:39:be:65:ec:26:b1:13:41: + 6b:86:58:fa:2e:cd:ab:06:d2:59:0e:bb:e4:44:2c:de:21:d1: + 8c:9c:93:a5:d5:ae:fc:af:37:b0:91:1f:46:61:28:b9:a5:c8: + b4:3c:28:33:b1:d9:ca:49:53:fe:14:80:82:de:06:c1:ab:21: + e7:44:76:04:d8:85:b4:60:72:30:7a:28:b7:6f:4d:9e:52:70: + 21:df:4e:71:aa:01:d6:ba:fa:4b:4a:61:75:9c:57:67:a6:b2: + e7:ab:24:6c +-----BEGIN CERTIFICATE----- +MIID9jCCAt6gAwIBAgIBCjANBgkqhkiG9w0BAQsFADBQMQswCQYDVQQGEwJVUzEN +MAsGA1UECAwEVGVzdDENMAsGA1UEBwwEVGVzdDENMAsGA1UECgwEVGVzdDEUMBIG +A1UEAwwLVGVzdCBQYXJlbnQwHhcNMTcxMDAzMTgwMjQ1WhcNMjcxMDAxMTgwMjQ1 +WjBPMQswCQYDVQQGEwJVUzENMAsGA1UECAwEVGVzdDENMAsGA1UEBwwEVGVzdDEN +MAsGA1UECgwEVGVzdDETMBEGA1UEAwwKVGVzdCBDaGlsZDCCASIwDQYJKoZIhvcN +AQEBBQADggEPADCCAQoCggEBALJFMeSZEh8Pw1t4mDml0NrGnzgjCd/9NbaVtjdd +tknypfFidWJBCZ025VPIghpcnSr9A5ypAG0osym7z/PrD1zJgY1p4QT3mhwJM6tU +wawM19EReWxvwCtUnsKGhQWj5HAGhELri8AOOnMWzRN5pUPmiYvDf2sEzX80a0pH +ZcNKatPqjlc0XTkY/NCO5Pb/dIagmAZnQAyPpl5Gne25JZl8TGK4Ga4SHjML00O5 +PLxa82vGqRzBzpkfZLejje3IPpV1GeXOUfER8cBYdofuQhKk/47G6EI9tN/HvqbH +6myIBEvT85t/1NuHIVU2LjwcySFKL39R8AjXIep1xOJ4kZ0CAwEAAaOB2zCB2DAd +BgNVHQ4EFgQUwgPq/H5wfzQhwb4zDorgfsaiIRswHwYDVR0jBBgwFoAUer59CVpf +XN7Mgho7/qjtyroWWEkwDwYDVR0TBAgwBgEB/wIBADALBgNVHQ8EBAMCAQYwSgYD +VR0RBEMwQYILZXhhbXBsZS5jb22CD3d3dy5leGFtcGxlLmNvbYIQbWFpbC5leGFt +cGxlLmNvbYIPZnRwLmV4YW1wbGUuY29tMCwGCWCGSAGG+EIBDQQfFh1PcGVuU1NM +IEdlbmVyYXRlZCBDZXJ0aWZpY2F0ZTANBgkqhkiG9w0BAQsFAAOCAQEAEEYuHje4 +EEqM43Z8BVd2NAUL4u2zGyggK1abLVlw5U5ezqgR1cGb58gOYStjrtIb7M91MdBP +NYbCUSJkwwenxGsTV8zl2YaNtHNFxcpIt7YCHsfeccZfKmR9tVsWmid9XzyKXpU4 +f8B+1Dk/NmB9fY6fcgbUaXrlRT/iyet/X3Qaa2y4oQgF2SXu1JfbWnIfSgaphnZB +WDQLWjm+ZewmsRNBa4ZY+i7NqwbSWQ675EQs3iHRjJyTpdWu/K83sJEfRmEouaXI +tDwoM7HZyklT/hSAgt4Gwash50R2BNiFtGByMHoot29NnlJwId9OcaoB1rr6S0ph +dZxXZ6ay56skbA== +-----END CERTIFICATE----- diff --git a/cursive/tests/unit/data/grandchild_cert.pem b/cursive/tests/unit/data/grandchild_cert.pem new file mode 100644 index 0000000..f120c3f --- /dev/null +++ b/cursive/tests/unit/data/grandchild_cert.pem @@ -0,0 +1,87 @@ +Certificate: + Data: + Version: 3 (0x2) + Serial Number: 11 (0xb) + Signature Algorithm: sha256WithRSAEncryption + Issuer: C=US, ST=Test, L=Test, O=Test, CN=Test Child + Validity + Not Before: Oct 3 18:09:07 2017 GMT + Not After : Oct 1 18:09:07 2027 GMT + Subject: C=US, ST=Test, L=Test, O=Test, CN=Test Grandchild + Subject Public Key Info: + Public Key Algorithm: rsaEncryption + Public-Key: (2048 bit) + Modulus: + 00:bd:3c:4b:2a:e8:03:5d:07:ae:94:f4:19:ed:00: + 21:20:dd:1c:54:f1:dc:44:d8:bf:66:b4:bf:ce:21: + 7b:bf:b4:15:7b:b3:4f:0e:d5:ef:fa:f1:31:ab:2a: + 22:78:72:20:7d:ce:58:c3:45:0d:2f:5c:23:7c:87: + 07:bf:ee:8c:8c:9f:ae:31:70:19:61:dc:92:b5:8f: + fb:36:16:1c:08:d4:2c:c0:0c:86:e0:ee:a8:31:20: + 21:16:41:b2:78:bc:88:a8:ef:4c:3a:34:4f:a0:08: + 25:e7:35:e8:bc:66:d3:c3:b5:2a:05:34:91:b0:d0: + ae:02:f2:a1:58:22:af:43:42:d8:40:82:0c:e3:26: + 72:22:06:d2:b1:13:87:04:83:70:f6:b0:99:39:bf: + 79:26:f6:e2:ff:24:c3:72:48:9f:68:0a:c1:c9:aa: + b1:a8:b4:f7:cf:44:38:4a:77:bf:56:20:fa:7e:08: + 75:26:04:fb:5e:d5:4f:ff:b8:45:1f:80:12:fb:7e: + 61:7e:52:f0:dc:71:ee:72:91:27:fa:60:93:96:e5: + 78:1d:d9:fd:5a:b8:00:b9:97:46:12:b5:2a:93:0e: + c3:1b:30:6e:b2:67:5d:c5:ca:40:3f:36:0c:7c:4f: + d4:48:e0:1f:32:a9:28:0c:37:35:7c:5d:42:f5:cb: + 54:b9 + Exponent: 65537 (0x10001) + X509v3 extensions: + X509v3 Subject Key Identifier: + 4F:6C:A8:1F:80:F0:A6:EE:41:85:B9:A2:3F:EC:3A:B2:93:B4:0E:86 + X509v3 Authority Key Identifier: + keyid:C2:03:EA:FC:7E:70:7F:34:21:C1:BE:33:0E:8A:E0:7E:C6:A2:21:1B + + X509v3 Basic Constraints: + CA:FALSE + X509v3 Key Usage: + Digital Signature, Key Encipherment + X509v3 Subject Alternative Name: + DNS:example.com, DNS:www.example.com, DNS:mail.example.com, DNS:ftp.example.com + Netscape Comment: + OpenSSL Generated Certificate + Signature Algorithm: sha256WithRSAEncryption + ae:a7:62:9e:f6:b7:e3:02:84:0f:fe:c6:7c:c1:0b:74:8e:95: + c3:2e:e9:5f:c0:8b:fc:79:45:53:5c:34:9d:b0:de:e6:cf:ed: + 52:4c:3f:6a:3f:e9:8d:a3:58:d4:ae:4d:31:30:57:d5:31:f9: + a2:ed:82:e2:ae:1a:65:a5:ab:de:64:35:c9:0b:d1:86:b0:83: + 57:8a:e4:ca:21:d5:9a:79:5b:44:42:ff:52:9a:51:b6:f4:6e: + f1:da:dd:3b:ca:12:cb:4c:e5:9f:a5:12:4f:13:99:85:79:c8: + 00:3b:2c:25:7f:02:07:a3:4e:59:0b:4d:8e:f8:43:08:a9:91: + 30:0a:17:1c:ff:91:c0:16:d5:c0:1e:ec:a5:24:c8:cc:f0:2c: + 0e:30:b9:bb:34:11:83:e7:4d:02:e4:2d:2a:90:98:eb:d8:ae: + 7b:2f:19:31:db:63:fc:0c:0b:47:f5:8f:7b:cf:99:0b:30:91: + a6:44:19:51:7f:15:4f:ab:8c:08:e2:bd:91:42:e4:e7:88:8e: + c0:ea:fd:09:ac:96:c6:14:ef:0e:7d:75:6a:05:b0:b5:4d:43: + 60:62:31:85:61:cb:c3:0f:81:24:d6:de:10:42:54:ff:c0:63: + 95:40:3d:89:52:f9:00:2a:a5:74:1c:b1:42:be:a1:2f:de:90: + cb:d5:a7:3d +-----BEGIN CERTIFICATE----- +MIID9DCCAtygAwIBAgIBCzANBgkqhkiG9w0BAQsFADBPMQswCQYDVQQGEwJVUzEN +MAsGA1UECAwEVGVzdDENMAsGA1UEBwwEVGVzdDENMAsGA1UECgwEVGVzdDETMBEG +A1UEAwwKVGVzdCBDaGlsZDAeFw0xNzEwMDMxODA5MDdaFw0yNzEwMDExODA5MDda +MFQxCzAJBgNVBAYTAlVTMQ0wCwYDVQQIDARUZXN0MQ0wCwYDVQQHDARUZXN0MQ0w +CwYDVQQKDARUZXN0MRgwFgYDVQQDDA9UZXN0IEdyYW5kY2hpbGQwggEiMA0GCSqG +SIb3DQEBAQUAA4IBDwAwggEKAoIBAQC9PEsq6ANdB66U9BntACEg3RxU8dxE2L9m +tL/OIXu/tBV7s08O1e/68TGrKiJ4ciB9zljDRQ0vXCN8hwe/7oyMn64xcBlh3JK1 +j/s2FhwI1CzADIbg7qgxICEWQbJ4vIio70w6NE+gCCXnNei8ZtPDtSoFNJGw0K4C +8qFYIq9DQthAggzjJnIiBtKxE4cEg3D2sJk5v3km9uL/JMNySJ9oCsHJqrGotPfP +RDhKd79WIPp+CHUmBPte1U//uEUfgBL7fmF+UvDcce5ykSf6YJOW5Xgd2f1auAC5 +l0YStSqTDsMbMG6yZ13FykA/Ngx8T9RI4B8yqSgMNzV8XUL1y1S5AgMBAAGjgdUw +gdIwHQYDVR0OBBYEFE9sqB+A8KbuQYW5oj/sOrKTtA6GMB8GA1UdIwQYMBaAFMID +6vx+cH80IcG+Mw6K4H7GoiEbMAkGA1UdEwQCMAAwCwYDVR0PBAQDAgWgMEoGA1Ud +EQRDMEGCC2V4YW1wbGUuY29tgg93d3cuZXhhbXBsZS5jb22CEG1haWwuZXhhbXBs +ZS5jb22CD2Z0cC5leGFtcGxlLmNvbTAsBglghkgBhvhCAQ0EHxYdT3BlblNTTCBH +ZW5lcmF0ZWQgQ2VydGlmaWNhdGUwDQYJKoZIhvcNAQELBQADggEBAK6nYp72t+MC +hA/+xnzBC3SOlcMu6V/Ai/x5RVNcNJ2w3ubP7VJMP2o/6Y2jWNSuTTEwV9Ux+aLt +guKuGmWlq95kNckL0Yawg1eK5Moh1Zp5W0RC/1KaUbb0bvHa3TvKEstM5Z+lEk8T +mYV5yAA7LCV/AgejTlkLTY74QwipkTAKFxz/kcAW1cAe7KUkyMzwLA4wubs0EYPn +TQLkLSqQmOvYrnsvGTHbY/wMC0f1j3vPmQswkaZEGVF/FU+rjAjivZFC5OeIjsDq +/QmslsYU7w59dWoFsLVNQ2BiMYVhy8MPgSTW3hBCVP/AY5VAPYlS+QAqpXQcsUK+ +oS/ekMvVpz0= +-----END CERTIFICATE----- diff --git a/cursive/tests/unit/data/grandparent_cert.pem b/cursive/tests/unit/data/grandparent_cert.pem new file mode 100644 index 0000000..8c4c180 --- /dev/null +++ b/cursive/tests/unit/data/grandparent_cert.pem @@ -0,0 +1,34 @@ +-----BEGIN CERTIFICATE----- +MIIF7jCCA9agAwIBAgIJANHiL5B0pUVmMA0GCSqGSIb3DQEBCwUAMIGDMQswCQYD +VQQGEwJVUzENMAsGA1UECAwEVGVzdDENMAsGA1UEBwwEVGVzdDENMAsGA1UECgwE +VGVzdDENMAsGA1UECwwEVGVzdDEZMBcGA1UEAwwQVGVzdCBHcmFuZHBhcmVudDEd +MBsGCSqGSIb3DQEJARYOZ3BAZXhhbXBsZS5jb20wHhcNMTcxMDAzMTc0NzMyWhcN +MTcxMTAyMTc0NzMyWjCBgzELMAkGA1UEBhMCVVMxDTALBgNVBAgMBFRlc3QxDTAL +BgNVBAcMBFRlc3QxDTALBgNVBAoMBFRlc3QxDTALBgNVBAsMBFRlc3QxGTAXBgNV +BAMMEFRlc3QgR3JhbmRwYXJlbnQxHTAbBgkqhkiG9w0BCQEWDmdwQGV4YW1wbGUu +Y29tMIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEAn4m1O+fffNTSnGE5 +MPac07jjMNrKHEjARS4aM222C8wCPiXXrs1diTQlvtxrLFOzc0gtCH6xVl59Zcis +H7kBf8oV9wNIHcfy2BmGkP7Wv68p3UsIF1IzGSHPyKJWG+l/xNexuXFaVG9y+siu +5Z3bx9DMBPFfXalxwRGoS0fyBOG/tXlqicf/aojF1U3UolML58URQqQ7IvGjEq22 +iqAfduEwLlLb99iJ8uiFgO6Rl/hwxvy9gmrGWJGHpHQKJ2Dx37Zc8MMcAJ5yos7c +GAs3e31TvRJgyEBcPKtl+xmh36wNC+V1KKRYKAfENqB6v7b1GDZrVtH7uHvSPCDQ +ccKklF2thomO4cm6UpbCF7/5i50OLwtr1TcUI/YT+nR/YsCuc/PKdyXITpP0CNR9 +Wcw6pb5LsWgLisFh4my4PzTbwTcPGFUJHq0CUUsRP1YijMNRtiZsoMJwspMZyg9d +9Ufxf7HjbugazUfvfIKMfX/s/pIZLiDLx5lV9RbHmjvlCt4OfH3FhqBveguqDLq7 +LbBC6tYm1E21izUqXy4Zh+oogvZeZGUBQL/JUJ6XOkUnffv1nf2HDsCmlW64NLce +9gc25BtXAAf5/tMQL2J3t5SZ+Ladk+nklQXz6eClFcLEbRcd15bZ3QFXCajeLWcP +Y5TZFgDQFFr7/FjDhr2bByMOJEkCAwEAAaNjMGEwHQYDVR0OBBYEFKfrobRNKmLV +OJDtRPp3f/m66pkEMB8GA1UdIwQYMBaAFKfrobRNKmLVOJDtRPp3f/m66pkEMBIG +A1UdEwEB/wQIMAYBAf8CAQEwCwYDVR0PBAQDAgEGMA0GCSqGSIb3DQEBCwUAA4IC +AQByRvLd0xO238AwgDep4hBshcFH9tcSg5hDPFFeJWRnC4c63vNGXMVV7iEAoPSH +LPbuELCIRqiZFYW5A8Olv2MGGZ3kjiFrWYfbLFU0/z1y82/E2NtM65cvOQKYxMk1 +HBmaGF8s43LdDiGUZ0yFMTwe+da+zWcfDPgSYml36ReCsn2dGFmfkPFhqSf810kI +yl2EKQnjEf51AGDfrA6fmEafsQy8eFf0uH6cR8nrsa+0aXIkTHZ9erXrXujD30iL +9M4T3uW/0Qk/kqSN3wUgHYWDBRyTKxCDPMiEixXDq21Jm1VzSKJAFE+xEuFHtqXl +nfZQCzihdx3ckZnH3qfrJt0V0cu6qSNr6sbyrb7FVO8aCNyumdCDM9VdJ64UFAme +Xd/1/195PcoFOVEokoH15EH0mPr+/DDWA39c+FaRHH0A3LmuzX/P5rTRLO3wldpL +XiZkLrfG43UNq3PIdh3YZEabpFcQYTmab7N8nZnmoMRM6YoEnHjdqPcDv3xs4gJS +U24bVnFqzgSW3V1GfZGnlQyGXFrlrU+wWJ55eJ59ucQn8PDYlrSz7+x9RiqoFZcQ +c9L8j7dFMBx6zI4dI5Ddx5q9KNtxPJb4Hk9HEd4C5OQ0qqdBR/hSD9mDjBpqEyc8 +aXIzmrTpGm7A9lbyXCEaOzN+2Jvdq5KtWh/halEgqgToqQ== +-----END CERTIFICATE----- diff --git a/cursive/tests/unit/data/not_a_cert.txt b/cursive/tests/unit/data/not_a_cert.txt new file mode 100644 index 0000000..240c7f0 --- /dev/null +++ b/cursive/tests/unit/data/not_a_cert.txt @@ -0,0 +1 @@ +This is not a certificate. diff --git a/cursive/tests/unit/data/orphaned_cert.pem b/cursive/tests/unit/data/orphaned_cert.pem new file mode 100644 index 0000000..d944bbc --- /dev/null +++ b/cursive/tests/unit/data/orphaned_cert.pem @@ -0,0 +1,24 @@ +-----BEGIN CERTIFICATE----- +MIID8zCCAtugAwIBAgIJAMDwfYBIkXEGMA0GCSqGSIb3DQEBCwUAMIGPMQswCQYD +VQQGEwJVUzERMA8GA1UECAwIVGVzdG9uaWExDTALBgNVBAcMBFRlc3QxJDAiBgNV +BAoMG0NvbXByZWhlbnNpdmUgVGVzdGluZywgSW5jLjEaMBgGA1UECwwRVGVzdGlu +ZyBPdmVyc2lnaHQxHDAaBgNVBAMME1Rlc3RpbmcgQ2VydGlmaWNhdGUwHhcNMTYw +NjMwMTcyOTUwWhcNMTcwNjMwMTcyOTUwWjCBjzELMAkGA1UEBhMCVVMxETAPBgNV +BAgMCFRlc3RvbmlhMQ0wCwYDVQQHDARUZXN0MSQwIgYDVQQKDBtDb21wcmVoZW5z +aXZlIFRlc3RpbmcsIEluYy4xGjAYBgNVBAsMEVRlc3RpbmcgT3ZlcnNpZ2h0MRww +GgYDVQQDDBNUZXN0aW5nIENlcnRpZmljYXRlMIIBIjANBgkqhkiG9w0BAQEFAAOC +AQ8AMIIBCgKCAQEAm+W78acV27U11m7E3iUUUGb+JXMW0okP8epD9OsLtVHxR+oq +iOt19rgNIH/wJzaT+CnJ1jUerjzjFu2RwGhEr8Ph2KrWWQ7vxkhJzuXmKmGBZJm3 +FJcADrxcmZ8V3Yqxf3zO36Rg27jqDgxSy3uzxgO7ZXrkrJjrgrg+x8wVQ/pkhd8Y +gQ/YQ2r1DF1GcpS/tSkCSc3lbIpCCHhORaRmHZXURML5q7vibLmc55Ad90WxtS1d +WI8RAsWnQMvP1OmZcRcPKrUlRc/w+nIrxNF9HdeOweQv2tcnNlxBOcr6MwIL+Gle +N4TmmthyVYCXxNWhW1VFA3atfEfmyEpiKIcQGwIDAQABo1AwTjAdBgNVHQ4EFgQU +IkPrrGyB6+XUlWbd287uFbfCvkkwHwYDVR0jBBgwFoAUIkPrrGyB6+XUlWbd287u +FbfCvkkwDAYDVR0TBAUwAwEB/zANBgkqhkiG9w0BAQsFAAOCAQEAMWi3C19pjWs9 +9hg1rTC0D/5C9K/0nmQ1pstVMXOKn9Z3ndUqRvLzxhHZHhQ/ATQwHKeSM2vpCmKa +eV7PGivF6W+CAXJImvgNrsP2fMBnTsg2Q3hBHSIkTgwJxAHlYZ3NXSxWoDSuozvU ++qjRY3hMpYLSXpfGFKh73GHBNWXyjlo7pn+I4gAEoHOqDKTelOONz6PiKKi4Un2g +j5FqmLZEq9VvzqSEC5VuFLZs4BpGmsKBM16+q+8JWMa025wNcdq4DxuNAkb3Zsty +QZkgVYJgIeuEKOCubCQfDOya5W7ik3mtZZm9dFD5dZ3+CDB53a/AlKdi9YUAJOUW +xBJzlRBlLg== +-----END CERTIFICATE----- diff --git a/cursive/tests/unit/data/parent_cert.pem b/cursive/tests/unit/data/parent_cert.pem new file mode 100644 index 0000000..7e325dc --- /dev/null +++ b/cursive/tests/unit/data/parent_cert.pem @@ -0,0 +1,107 @@ +Certificate: + Data: + Version: 3 (0x2) + Serial Number: 9 (0x9) + Signature Algorithm: sha256WithRSAEncryption + Issuer: C=US, ST=Test, L=Test, O=Test, OU=Test, CN=Test Grandparent/emailAddress=gp@example.com + Validity + Not Before: Oct 3 17:58:30 2017 GMT + Not After : Oct 1 17:58:30 2027 GMT + Subject: C=US, ST=Test, L=Test, O=Test, CN=Test Parent + Subject Public Key Info: + Public Key Algorithm: rsaEncryption + Public-Key: (2048 bit) + Modulus: + 00:9f:9d:95:c4:a3:2f:37:52:e4:7c:cf:0b:0e:7f: + 14:69:63:1e:7a:cc:a8:19:a7:88:59:c8:17:f2:21: + 13:1b:45:21:fa:cc:93:40:71:cf:77:52:5a:1e:2e: + 5a:91:16:a9:67:3a:a3:6a:ea:cb:a2:bf:24:9b:8c: + 08:96:33:19:46:f9:7a:04:f9:c2:ee:87:f3:c3:23: + 73:37:59:0e:c0:71:f4:cd:0b:ad:23:63:51:0a:4f: + dc:d2:9b:ab:ab:8a:99:07:d4:c8:c8:70:fd:18:73: + 25:0a:48:82:32:0d:64:46:b1:63:84:24:03:0b:3c: + b8:17:92:78:6c:2b:4d:21:1b:46:3e:c1:cf:98:0b: + a8:43:91:c0:39:48:f5:4e:71:77:c5:43:0e:68:8f: + 01:c6:fb:59:77:d5:b3:f3:fe:95:27:ea:6e:ae:fc: + 8e:59:ad:06:97:0c:f7:a6:e7:61:df:23:91:26:d0: + bc:80:c6:2b:02:9b:fa:0f:e6:32:69:5a:90:29:c9: + 9c:34:eb:50:ed:1d:e3:eb:0f:67:88:e3:ec:2b:1a: + ab:41:c3:fa:d6:e8:aa:e3:7b:6a:16:3d:d8:da:6b: + af:92:81:32:98:2f:f7:c0:bd:c4:25:bb:02:60:43: + d5:e6:0c:29:7f:31:5d:09:4b:6a:a9:31:9b:92:24: + 09:8f + Exponent: 65537 (0x10001) + X509v3 extensions: + X509v3 Subject Key Identifier: + 7A:BE:7D:09:5A:5F:5C:DE:CC:82:1A:3B:FE:A8:ED:CA:BA:16:58:49 + X509v3 Authority Key Identifier: + keyid:A7:EB:A1:B4:4D:2A:62:D5:38:90:ED:44:FA:77:7F:F9:BA:EA:99:04 + + X509v3 Basic Constraints: + CA:TRUE, pathlen:0 + X509v3 Key Usage: + Certificate Sign, CRL Sign + X509v3 Subject Alternative Name: + DNS:example.com, DNS:www.example.com, DNS:mail.example.com, DNS:ftp.example.com + Netscape Comment: + OpenSSL Generated Certificate + Signature Algorithm: sha256WithRSAEncryption + 81:55:0d:1d:73:54:1d:72:73:72:dc:cf:ed:c1:47:c8:38:2a: + 78:33:5e:55:6f:02:cc:c0:6a:6f:7e:c9:fa:4c:3d:a0:5b:25: + 37:5e:87:69:7f:d8:66:73:4f:58:7d:c7:3e:6d:be:2a:85:43: + 6a:cb:ff:68:59:1d:72:d2:68:ad:e9:5b:2f:8d:f6:95:31:ba: + 1d:de:16:45:d9:12:51:85:12:bb:fb:89:fc:3a:7c:f5:e4:75: + 64:b4:7d:ff:9f:f6:15:fa:1e:cb:18:4a:9d:e8:d8:5e:5a:d7: + dd:78:c7:df:3d:21:2d:99:ef:b4:2c:78:2f:fb:fa:a0:7e:f3: + cb:3b:05:5a:65:7d:9b:0f:9b:a3:9b:a9:ad:25:f8:32:cb:08: + fd:c2:68:d3:92:15:09:59:5f:8b:c4:84:01:5f:75:7b:f0:55: + 5f:20:39:f1:26:65:3d:d8:a2:19:de:fb:79:a0:27:2a:24:ae: + 95:02:84:61:72:7a:47:37:4e:9f:af:20:5b:21:ec:c4:bf:ee: + 80:5b:35:4e:ee:20:46:e6:cb:a6:e2:2f:c6:3e:5a:fa:f9:97: + c3:97:09:1d:ce:08:a3:e9:09:cb:c3:59:3f:98:f3:b6:bf:00: + 8b:a7:40:de:0a:1c:09:88:f7:74:fa:b1:1c:05:44:ff:ba:73: + 84:3b:93:8d:a8:51:d0:d8:59:e6:cd:a8:79:d3:db:0a:1d:99: + 3f:7c:a0:f9:d5:9e:dd:13:58:ee:ef:0d:3d:e2:4a:8b:85:18: + 0c:86:f8:97:4d:18:54:c0:52:b8:10:38:1a:b8:8a:06:71:a5: + e7:78:11:00:5b:9f:19:92:34:28:0f:19:3f:b0:57:ea:11:69: + 29:ca:ed:05:36:08:f6:8d:ec:5d:34:79:92:8e:4c:e0:1c:a4: + ad:1a:31:90:b7:16:60:da:e3:8f:ee:ea:66:df:13:e8:46:8d: + a3:e2:3b:0a:f5:87:14:3d:4b:14:ea:da:89:c7:ae:e0:60:e3: + a0:4c:04:2f:a1:0f:a9:84:5a:5a:f7:3d:4f:7b:d4:7c:e1:cd: + ef:8b:28:45:19:ea:a9:4c:9e:59:f8:41:43:10:77:89:09:3e: + 30:d0:e9:58:96:45:07:50:0e:4d:cc:6a:53:9e:64:c4:8a:e0: + 51:96:3a:c6:8a:e2:94:af:9c:26:9a:fe:e3:7a:cd:cc:55:60: + f0:dc:bf:f3:0d:e8:69:e4:cf:49:e1:f4:d2:87:91:31:cf:42: + f7:2c:a7:f7:7b:88:90:e4:17:96:f6:34:d2:bf:a1:66:3c:03: + db:aa:07:fa:a6:c3:4b:d3:29:d1:d1:40:6f:a7:88:a5:7f:bd: + 5f:f5:00:94:db:53:5d:24 +-----BEGIN CERTIFICATE----- +MIIFKzCCAxOgAwIBAgIBCTANBgkqhkiG9w0BAQsFADCBgzELMAkGA1UEBhMCVVMx +DTALBgNVBAgMBFRlc3QxDTALBgNVBAcMBFRlc3QxDTALBgNVBAoMBFRlc3QxDTAL +BgNVBAsMBFRlc3QxGTAXBgNVBAMMEFRlc3QgR3JhbmRwYXJlbnQxHTAbBgkqhkiG +9w0BCQEWDmdwQGV4YW1wbGUuY29tMB4XDTE3MTAwMzE3NTgzMFoXDTI3MTAwMTE3 +NTgzMFowUDELMAkGA1UEBhMCVVMxDTALBgNVBAgMBFRlc3QxDTALBgNVBAcMBFRl +c3QxDTALBgNVBAoMBFRlc3QxFDASBgNVBAMMC1Rlc3QgUGFyZW50MIIBIjANBgkq +hkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAn52VxKMvN1LkfM8LDn8UaWMeesyoGaeI +WcgX8iETG0Uh+syTQHHPd1JaHi5akRapZzqjaurLor8km4wIljMZRvl6BPnC7ofz +wyNzN1kOwHH0zQutI2NRCk/c0purq4qZB9TIyHD9GHMlCkiCMg1kRrFjhCQDCzy4 +F5J4bCtNIRtGPsHPmAuoQ5HAOUj1TnF3xUMOaI8BxvtZd9Wz8/6VJ+purvyOWa0G +lwz3pudh3yORJtC8gMYrApv6D+YyaVqQKcmcNOtQ7R3j6w9niOPsKxqrQcP61uiq +43tqFj3Y2muvkoEymC/3wL3EJbsCYEPV5gwpfzFdCUtqqTGbkiQJjwIDAQABo4Hb +MIHYMB0GA1UdDgQWBBR6vn0JWl9c3syCGjv+qO3KuhZYSTAfBgNVHSMEGDAWgBSn +66G0TSpi1TiQ7UT6d3/5uuqZBDAPBgNVHRMECDAGAQH/AgEAMAsGA1UdDwQEAwIB +BjBKBgNVHREEQzBBggtleGFtcGxlLmNvbYIPd3d3LmV4YW1wbGUuY29tghBtYWls +LmV4YW1wbGUuY29tgg9mdHAuZXhhbXBsZS5jb20wLAYJYIZIAYb4QgENBB8WHU9w +ZW5TU0wgR2VuZXJhdGVkIENlcnRpZmljYXRlMA0GCSqGSIb3DQEBCwUAA4ICAQCB +VQ0dc1QdcnNy3M/twUfIOCp4M15VbwLMwGpvfsn6TD2gWyU3Xodpf9hmc09Yfcc+ +bb4qhUNqy/9oWR1y0mit6VsvjfaVMbod3hZF2RJRhRK7+4n8Onz15HVktH3/n/YV ++h7LGEqd6NheWtfdeMffPSEtme+0LHgv+/qgfvPLOwVaZX2bD5ujm6mtJfgyywj9 +wmjTkhUJWV+LxIQBX3V78FVfIDnxJmU92KIZ3vt5oCcqJK6VAoRhcnpHN06fryBb +IezEv+6AWzVO7iBG5sum4i/GPlr6+ZfDlwkdzgij6QnLw1k/mPO2vwCLp0DeChwJ +iPd0+rEcBUT/unOEO5ONqFHQ2Fnmzah509sKHZk/fKD51Z7dE1ju7w094kqLhRgM +hviXTRhUwFK4EDgauIoGcaXneBEAW58ZkjQoDxk/sFfqEWkpyu0FNgj2jexdNHmS +jkzgHKStGjGQtxZg2uOP7upm3xPoRo2j4jsK9YcUPUsU6tqJx67gYOOgTAQvoQ+p +hFpa9z1Pe9R84c3viyhFGeqpTJ5Z+EFDEHeJCT4w0OlYlkUHUA5NzGpTnmTEiuBR +ljrGiuKUr5wmmv7jes3MVWDw3L/zDehp5M9J4fTSh5Exz0L3LKf3e4iQ5BeW9jTS +v6FmPAPbqgf6psNL0ynR0UBvp4ilf71f9QCU21NdJA== +-----END CERTIFICATE----- diff --git a/cursive/tests/unit/data/self_signed_cert.der b/cursive/tests/unit/data/self_signed_cert.der new file mode 100644 index 0000000..33091c8 Binary files /dev/null and b/cursive/tests/unit/data/self_signed_cert.der differ diff --git a/cursive/tests/unit/data/self_signed_cert.pem b/cursive/tests/unit/data/self_signed_cert.pem new file mode 100644 index 0000000..48f23c3 --- /dev/null +++ b/cursive/tests/unit/data/self_signed_cert.pem @@ -0,0 +1,22 @@ +-----BEGIN CERTIFICATE----- +MIIDmTCCAoGgAwIBAgIJAKmiuSJghxIGMA0GCSqGSIb3DQEBCwUAMHsxCzAJBgNV +BAYTAlVTMQ0wCwYDVQQIEwRUZXN0MQ0wCwYDVQQHEwRUZXN0MQ0wCwYDVQQKEwRU +ZXN0MQ0wCwYDVQQLEwRUZXN0MQ0wCwYDVQQDEwRUZXN0MSEwHwYJKoZIhvcNAQkB +FhJ0ZXN0QGVtYWlsLmFkZHJlc3MwHhcNMTYwNjMwMTc1NzU1WhcNMTcwNjMwMTc1 +NzU1WjB7MQswCQYDVQQGEwJVUzENMAsGA1UECBMEVGVzdDENMAsGA1UEBxMEVGVz +dDENMAsGA1UEChMEVGVzdDENMAsGA1UECxMEVGVzdDENMAsGA1UEAxMEVGVzdDEh +MB8GCSqGSIb3DQEJARYSdGVzdEBlbWFpbC5hZGRyZXNzMIIBIjANBgkqhkiG9w0B +AQEFAAOCAQ8AMIIBCgKCAQEA4KHdygQZ2/posNLLE/9lR0U57/iQKyFAfzO70RA5 +9mYZWSQTk8yI4wsDQi75xBjyuhYExsl+9lX6dgV0uEElhkycRemTN9pHmdGLi6of +OIpVd5drZGcK19ndPeG5IzFvCpWXfsKuZ2kJf8p9i5XDNhigtYNq5rfLZBOIE3FY +HPKBbx9cBaPOL8kjyX8LPwG7tpmNRLAF4XgQZu/AbfWx0jg8UqqJhOKwPQz+YOPY +1eJ55BDyDYYiRj70qhQ1jIbfmYbWjg1VOv7LKzzwQWI8gTnKND26+L0D1tAy4joO +cV/XM9lWheBCvzWTULqKpy95hyUMTz9mdG3xb5yFEccYwwIDAQABoyAwHjAPBgNV +HRMECDAGAQH/AgECMAsGA1UdDwQEAwICBDANBgkqhkiG9w0BAQsFAAOCAQEAPwKA +rC4S7P//VbffPNYOdmlu1dOjSSoNgtXC2lwpCy2iuvakasIAaDtseEjHgvcqJ/ty +mHeOQu23qAP584ss+GoR7JUjlaTRoXRt/5PQ66HyrJXl/2jWLtT+7yU2+UtOxWSa +fE3xUnzZlW4ES0hi+pWCpK+WaEya8q1+ak+i5oF8kQ3nRcT1f0IcOgyYnhvu8GGI +Zd7BA8boUhR+L+X52zk6loaOEIwsmsfero9i2pn+JGZKyQfFKI8+bsnYuc7elIbY +PER8fGWHid/DoIgQety153LLKtfR/20rYBrlnNtatg3ePTRdFZ1p7lnLPfA+AiV5 +e1Y6hSfZYXpZRV+Qtw== +-----END CERTIFICATE----- diff --git a/cursive/tests/unit/data/self_signed_cert_invalid_ca_constraint.pem b/cursive/tests/unit/data/self_signed_cert_invalid_ca_constraint.pem new file mode 100644 index 0000000..6cc487c --- /dev/null +++ b/cursive/tests/unit/data/self_signed_cert_invalid_ca_constraint.pem @@ -0,0 +1,22 @@ +-----BEGIN CERTIFICATE----- +MIIDkzCCAnugAwIBAgIJAKnOrxg9gSXNMA0GCSqGSIb3DQEBCwUAMHsxCzAJBgNV +BAYTAlVTMQ0wCwYDVQQIEwRUZXN0MQ0wCwYDVQQHEwRUZXN0MQ0wCwYDVQQKEwRU +ZXN0MQ0wCwYDVQQLEwRUZXN0MQ0wCwYDVQQDEwRUZXN0MSEwHwYJKoZIhvcNAQkB +FhJ0ZXN0QGVtYWlsLmFkZHJlc3MwHhcNMTYwNjMwMTgxOTQyWhcNMTcwNjMwMTgx +OTQyWjB7MQswCQYDVQQGEwJVUzENMAsGA1UECBMEVGVzdDENMAsGA1UEBxMEVGVz +dDENMAsGA1UEChMEVGVzdDENMAsGA1UECxMEVGVzdDENMAsGA1UEAxMEVGVzdDEh +MB8GCSqGSIb3DQEJARYSdGVzdEBlbWFpbC5hZGRyZXNzMIIBIjANBgkqhkiG9w0B +AQEFAAOCAQ8AMIIBCgKCAQEAnhmtSiir3aB5igvVEQlHqIw2K98q336cyjipFq9b +Pt1YslTwLfUAagr7224i0tny45PIZ3o1YlBxhEwd/i1tMnCz2+DQyat+p+vVbbiI +ceN1ZzRFE4zJV0QjG+H+TOWqzjtdtq04jkdrKMOsp3Lv4NHIuEuLocQLPLuT79wP +VUO+BCHlU/0bUQHhAU/Jx9B81GQ1/4lYS400AYtANSEccMR1djUTjFha4wiwSDH2 +QZQBgmiqmDhf22uoioFgay9+yhOJ3SJx/lIiMavM2LMgNbns1DcbAD8oKGS69Mmo +TsQlgOVMQTIDbwsm3WaxIcY8BipUACSe3E+RdqDrP/MTDwIDAQABoxowGDAJBgNV +HRMEAjAAMAsGA1UdDwQEAwIDCDANBgkqhkiG9w0BAQsFAAOCAQEACP8ZAbyAtffS +sWZ6WbLcm+BT7FFdUm62gGqC2knaIZQud3AV5/wtj2CAw1lXgGbaCqfWNQrTDY9z +PpGXyQ5tvNUtBZG23K7nMvid+U1WDh1fhlRC0kxCK2MsPwv9T5BM3tj/YF0MGWuQ +3GlFL8wU8UoAP3alUhxQl5qXqfXc6qfMW2ec4Jb3j6nbezL7ttn15LiBCsvoJ6/V +Go9bbox81UrbtxnVin5+cYczUdB+Q9+fe23B/6MG99hzWU9arkkU7ZOlnI/bW9Zb +fx4+atZfpi18nyd2ljgiTapB6Ex6uxVPrzwuKxpGMt9wU1++ZYc3YDke4EOT+OIX +nKc7UFfjyg== +-----END CERTIFICATE----- diff --git a/cursive/tests/unit/data/self_signed_cert_invalid_key_usage.pem b/cursive/tests/unit/data/self_signed_cert_invalid_key_usage.pem new file mode 100644 index 0000000..2de8808 --- /dev/null +++ b/cursive/tests/unit/data/self_signed_cert_invalid_key_usage.pem @@ -0,0 +1,22 @@ +-----BEGIN CERTIFICATE----- +MIIDmTCCAoGgAwIBAgIJAKOf0EhCGUdQMA0GCSqGSIb3DQEBCwUAMHsxCzAJBgNV +BAYTAlVTMQ0wCwYDVQQIEwRUZXN0MQ0wCwYDVQQHEwRUZXN0MQ0wCwYDVQQKEwRU +ZXN0MQ0wCwYDVQQLEwRUZXN0MQ0wCwYDVQQDEwRUZXN0MSEwHwYJKoZIhvcNAQkB +FhJ0ZXN0QGVtYWlsLmFkZHJlc3MwHhcNMTYwNjMwMTgxNDMwWhcNMTcwNjMwMTgx +NDMwWjB7MQswCQYDVQQGEwJVUzENMAsGA1UECBMEVGVzdDENMAsGA1UEBxMEVGVz +dDENMAsGA1UEChMEVGVzdDENMAsGA1UECxMEVGVzdDENMAsGA1UEAxMEVGVzdDEh +MB8GCSqGSIb3DQEJARYSdGVzdEBlbWFpbC5hZGRyZXNzMIIBIjANBgkqhkiG9w0B +AQEFAAOCAQ8AMIIBCgKCAQEA575xHdxVuT5CYPom/PbFdDLt0PgG3CZFkiRNxPAG +rDN8cG5ouTw0R9RMBFA+nYcTx/4GnPJmSBEBVqMoPSzsB6Rx9k21KymNlaEs2O1W +jSMsYd9gW4NlHdyoomYw0nXQjkstmtdJxDNWg0zSZrHMPnkOVh+JNV58i4FXOx5O +bxWo4sSyAvNjAEH9GwDwy+Jz0X4RdFGQrGjm+/v+ohvy8JqU5ZKpz2oP2oQURjDj ++AH3ghmgNAVAk0syjtSqydEJd9aeMLTmTaUtP+gPnXdj/ZBj+TQH01RNlSECH2l4 +WrymS3g4+X5xsA7DeLIbiXB4K1xjbJFCSfDYrV52H/fE6QIDAQABoyAwHjAPBgNV +HRMECDAGAQH/AgECMAsGA1UdDwQEAwIDCDANBgkqhkiG9w0BAQsFAAOCAQEARNRE +XC8y/RoPLUVAVKJ/RwcH4cwaHoSSu6HnygIpg9Qs7Xc7u1aKCL0dRF1NqfmaqHZ6 +ZjllxDi5t0CFIXPfDQIYchfSzOafhJGEH3gilBwfmN43N4/eCSvdRKfhRbRFOD9j +0JHRAHkn2JRcwSTTjJEcJUJJETAIIbX1ovobJZuJOY0faI1O/Z2KILYrwdmcfnZ5 +3i0kUps5BWrBrcs70gBsDBugeM24ANa7hJzFk+9TztfLWF1AUfjpZ4Bj/rb21+Gp +08FRjvn80Y5bGlNh0Q7Qbu8NS8VbAHHF3t3PUVRymJhIycpvBBBS7dcQqHf+v1gs +Z/UpzuobJvnhq+7mcQ== +-----END CERTIFICATE----- diff --git a/cursive/tests/unit/data/self_signed_cert_missing_ca_constraint.pem b/cursive/tests/unit/data/self_signed_cert_missing_ca_constraint.pem new file mode 100644 index 0000000..7c61d6d --- /dev/null +++ b/cursive/tests/unit/data/self_signed_cert_missing_ca_constraint.pem @@ -0,0 +1,21 @@ +-----BEGIN CERTIFICATE----- +MIIDiDCCAnCgAwIBAgIJAOYNT7MuoypuMA0GCSqGSIb3DQEBCwUAMHsxCzAJBgNV +BAYTAlVTMQ0wCwYDVQQIEwRUZXN0MQ0wCwYDVQQHEwRUZXN0MQ0wCwYDVQQKEwRU +ZXN0MQ0wCwYDVQQLEwRUZXN0MQ0wCwYDVQQDEwRUZXN0MSEwHwYJKoZIhvcNAQkB +FhJ0ZXN0QGVtYWlsLmFkZHJlc3MwHhcNMTYwNjMwMTgyMTMyWhcNMTcwNjMwMTgy +MTMyWjB7MQswCQYDVQQGEwJVUzENMAsGA1UECBMEVGVzdDENMAsGA1UEBxMEVGVz +dDENMAsGA1UEChMEVGVzdDENMAsGA1UECxMEVGVzdDENMAsGA1UEAxMEVGVzdDEh +MB8GCSqGSIb3DQEJARYSdGVzdEBlbWFpbC5hZGRyZXNzMIIBIjANBgkqhkiG9w0B +AQEFAAOCAQ8AMIIBCgKCAQEA3qypxOJ2X0k47SIEkRGvA/ECqzVAX3nsC0yPnbF3 +14SGe7xFBzi0VMAXOcpVj0BL+G5TL95O5loVN/UnU5/xtjSa712HOOlJfvnqmv63 +BLq9cMS2strwufeOK3YUtQExtJdxMjcEYuCMt+NlQ3Hl+xNfBc0LXWNBdlusP4fs +6sLEgyD4ywSLC9oHzyzgDxi0pr52itu+KnZv2iET/Wotg/8Aiw5Q5fiTc8DoysdZ +MF1ix56oGo1SFFGFf+n2iwYbImtNGt6//jKEDP8P4iLdLAxHxmfsXXnl+Zs/6VoA +RrnS3Xt7F5xj4CWuoZy4CLo8YXhXdznRQZ2r5Qha65cFNwIDAQABow8wDTALBgNV +HQ8EBAMCAwgwDQYJKoZIhvcNAQELBQADggEBALOBnrWz3xMg4Yh92zsXfrSm2uAL +P8jgXQtQdsYWSEWcfYNYOSlmLnICweDnAn0V5Kzp0E5wZSHP8Ut4IYEKwe+IF7HA +pv3mpg1CYtwVbVsN0dhlLHDVuF27i0r8LuOv9yh0wxY3hPYrd2WvQ/qTP8NO0EoD +fM8w2fjNeTu2jB+lYWhGWOHfzEvltosxMZIPWBJxrh3PbYdbuJJZlm/NPqj5Urxx +nRB89AEBHKEKHlmNIMMOM3mQ+ShssgGrbRV6U6iJ5qv4H5RdaZtmXMGhjt8dFJrD +A2YZYW15QLQDEyud4flSztaw6UMHJi+4FChBAnuJuOcNRZA83v3szuM9t64= +-----END CERTIFICATE----- diff --git a/cursive/tests/unit/data/self_signed_cert_missing_key_usage.pem b/cursive/tests/unit/data/self_signed_cert_missing_key_usage.pem new file mode 100644 index 0000000..7885a15 --- /dev/null +++ b/cursive/tests/unit/data/self_signed_cert_missing_key_usage.pem @@ -0,0 +1,21 @@ +-----BEGIN CERTIFICATE----- +MIIDXTCCAkWgAwIBAgIJAI20mCsVbBjpMA0GCSqGSIb3DQEBCwUAMEUxCzAJBgNV +BAYTAkFVMRMwEQYDVQQIDApTb21lLVN0YXRlMSEwHwYDVQQKDBhJbnRlcm5ldCBX +aWRnaXRzIFB0eSBMdGQwHhcNMTYwNjMwMTM0NTU5WhcNMTcwNjMwMTM0NTU5WjBF +MQswCQYDVQQGEwJBVTETMBEGA1UECAwKU29tZS1TdGF0ZTEhMB8GA1UECgwYSW50 +ZXJuZXQgV2lkZ2l0cyBQdHkgTHRkMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIB +CgKCAQEA5NjoTq0D6LM6z7V16E1+Eul25L6X1BSLcppsHhVMB/NM1e7hjoUtAODv +71L/bAAgV4ky4aphOfwYQLfeAP8nkq4CU30LaAyAQwrT4RyW1NG7AA00xylauebt +sc2GUUy06gQ0Z6OjjEfA5HA4W+HYfeyuNzQpWXHWz/6K8xcKb9w10qAjDhNilHbj +3RVtL6u6rsZgQi0DlMxpHsp6gLezIRMN72B/AOKrzaobw4nY4hOVkqbRlOHB/tsk +4BJLUuW5WM30TVpfsKe07jCBgqUwb9XD9lZa1alkFRsSTZoWijeQBM6kiLD/VFNC +YlIKBrN7HZtfOlqhftMknCsoyrWsjQIDAQABo1AwTjAdBgNVHQ4EFgQUnDu25deh +bIOsCaj3LAcE9r2gx0QwHwYDVR0jBBgwFoAUnDu25dehbIOsCaj3LAcE9r2gx0Qw +DAYDVR0TBAUwAwEB/zANBgkqhkiG9w0BAQsFAAOCAQEAowntOCmRXKgz7M6hnbQc +sEyIt5tN3QILHSeprVO7/ONVIiPJMCfB+S8gJKJ0d5R0xXDYN/6+HyYlgfaL33Gt +HY75y8MRnfqpgbEWhXWsBkxgeuqWiM/OFMTqLtgkVbxsVzoUl6V+tHsZaaM9yuyb +iUBM9McAPGIgodpMGG86BV6qg07VjqWjl5pBUU4B2zvvzZjwrC8jqUYksVESHB9U +WBzwfPLXoj0PUfAog34ZtT33UXX8M3oXTw+yb/hx0rContYMc78Lnlk6mV9gGG+X ++3gSwAHn0SMZNNkKc3gdb1CLfluvHw2Od2jat0yfHHawh1JBtnfHrAU0px3Kzw5U +0A== +-----END CERTIFICATE----- diff --git a/cursive/tests/unit/data/signed_cert.pem b/cursive/tests/unit/data/signed_cert.pem new file mode 100644 index 0000000..8232778 --- /dev/null +++ b/cursive/tests/unit/data/signed_cert.pem @@ -0,0 +1,26 @@ +-----BEGIN CERTIFICATE----- +MIIEWzCCA0OgAwIBAgIBATANBgkqhkiG9w0BAQsFADB7MQswCQYDVQQGEwJVUzEN +MAsGA1UECBMEVGVzdDENMAsGA1UEBxMEVGVzdDENMAsGA1UEChMEVGVzdDENMAsG +A1UECxMEVGVzdDENMAsGA1UEAxMEVGVzdDEhMB8GCSqGSIb3DQEJARYSdGVzdEBl +bWFpbC5hZGRyZXNzMB4XDTE2MDYzMDE5NDA1N1oXDTE3MDYzMDE5NDA1N1owbzEL +MAkGA1UEBhMCVVMxDTALBgNVBAgMBFRlc3QxDTALBgNVBAoMBFRlc3QxDTALBgNV +BAsMBFRlc3QxDzANBgNVBAMMBkNsaWVudDEiMCAGCSqGSIb3DQEJARYTdGVzdEBj +bGllbnQuYWRkcmVzczCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBALOK +dKWvu8NLDee4KvHf5WmmJsNeqrfZt+5EBXlp9wEJ3i//6vRpZe9Gr/k3xfbQPVng +PS8LUBancZ/zPos6ZibUuJi+ZjgVXUm61S18536wq4S1LH4Hkb4RgJW+IKqlqi0z +RVC3xeNcUhGprcH9JtjinOusQ1HLWy4mSr5aaCfCVshj3YEN5uCrfDOXPkS5B1kd +kpnEZJt2tAUPLlIKD4Ytjq9A84bL6wHTrUg5NmZ8j+yfXfD0qE1rN69AZFQ2V72R +uKUbxvvx2T8ObJh1VpiSlMLLEeoEY1OUCZWB+Xz6GX9uL9B9zeD0+f9WcswI3UCw +9WEFgDHYWxgFciu7IYsCAwEAAaOB9TCB8jAJBgNVHRMEAjAAMCwGCWCGSAGG+EIB +DQQfFh1PcGVuU1NMIEdlbmVyYXRlZCBDZXJ0aWZpY2F0ZTAdBgNVHQ4EFgQUDs4X +d7VArujtnCJ3Ppht7hSfdL0wgZcGA1UdIwSBjzCBjKF/pH0wezELMAkGA1UEBhMC +VVMxDTALBgNVBAgTBFRlc3QxDTALBgNVBAcTBFRlc3QxDTALBgNVBAoTBFRlc3Qx +DTALBgNVBAsTBFRlc3QxDTALBgNVBAMTBFRlc3QxITAfBgkqhkiG9w0BCQEWEnRl +c3RAZW1haWwuYWRkcmVzc4IJAKmiuSJghxIGMA0GCSqGSIb3DQEBCwUAA4IBAQBm +f2VVj4Eqb+5pAgimkejDrYRzDgDQ4Eyr45vdUtu7JoGovGmkxg5z3izW/UKKj8GC +04aXIJiIu8d7mn5ZxuaIS0/mtVN167tVVI0wBlkQRK5dJNjn47fTixymEy4lwdUl +0iSb1JP6beVmSMIywD5lFxGPiW/MEJSvDCdlOT2Ojiv/Sbn9Q09PsXei0fAmNGZn +FEUSnlqgWkeGIIv3+//kY8pHlZ1RyYSShQ+3Vb8Qifx0lbiFQWDP82EgETu7JKWn +fKCoogSDybcLqB/WeGOQ0myXgEth5Lhkdo0n08J/FYL/bA1thADVnV66ZpERgb3h +38P4rEcobzZdVPcS4zwP +-----END CERTIFICATE----- diff --git a/cursive/tests/unit/test_certificate_utils.py b/cursive/tests/unit/test_certificate_utils.py new file mode 100644 index 0000000..a376528 --- /dev/null +++ b/cursive/tests/unit/test_certificate_utils.py @@ -0,0 +1,403 @@ +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +import datetime +import mock +import os + +from cryptography.hazmat.backends import default_backend +from cryptography import x509 + +from cursive import certificate_utils +from cursive import exception +from cursive.tests import base + + +class TestCertificateUtils(base.TestCase): + """Test methods for the certificate verification context and utilities""" + + def setUp(self): + super(TestCertificateUtils, self).setUp() + + self.cert_path = os.path.join( + os.path.dirname(os.path.realpath(__file__)), + 'data' + ) + + def tearDown(self): + super(TestCertificateUtils, self).tearDown() + + def load_certificate(self, cert_name): + # Load the raw certificate file data. + path = os.path.join(self.cert_path, cert_name) + with open(path, 'rb') as cert_file: + data = cert_file.read() + + # Convert the raw certificate data into a certificate object, first + # as a PEM-encoded certificate and, if that fails, then as a + # DER-encoded certificate. If both fail, the certificate cannot be + # loaded. + try: + return x509.load_pem_x509_certificate(data, default_backend()) + except Exception: + try: + return x509.load_der_x509_certificate(data, default_backend()) + except Exception: + raise exception.SignatureVerificationError( + "Failed to load certificate: %s" % path + ) + + def load_certificates(self, cert_names): + certs = list() + for cert_name in cert_names: + cert = self.load_certificate(cert_name) + certs.append(cert) + return certs + + @mock.patch('oslo_utils.timeutils.utcnow') + def test_is_within_valid_dates(self, mock_utcnow): + # Verify a certificate is valid at a time within its valid date range + cert = self.load_certificate('self_signed_cert.pem') + mock_utcnow.return_value = datetime.datetime(2017, 1, 1) + result = certificate_utils.is_within_valid_dates(cert) + self.assertEqual(True, result) + + @mock.patch('oslo_utils.timeutils.utcnow') + def test_is_before_valid_dates(self, mock_utcnow): + # Verify a certificate is invalid at a time before its valid date range + cert = self.load_certificate('self_signed_cert.pem') + mock_utcnow.return_value = datetime.datetime(2000, 1, 1) + result = certificate_utils.is_within_valid_dates(cert) + self.assertEqual(False, result) + + @mock.patch('oslo_utils.timeutils.utcnow') + def test_is_after_valid_dates(self, mock_utcnow): + # Verify a certificate is invalid at a time after its valid date range + cert = self.load_certificate('self_signed_cert.pem') + mock_utcnow.return_value = datetime.datetime(2100, 1, 1) + result = certificate_utils.is_within_valid_dates(cert) + self.assertEqual(False, result) + + def test_is_issuer(self): + # Test issuer and subject name matching for a self-signed certificate. + cert = self.load_certificate('self_signed_cert.pem') + result = certificate_utils.is_issuer(cert, cert) + self.assertEqual(True, result) + + def test_is_not_issuer(self): + # Test issuer and subject name mismatching. + cert = self.load_certificate('self_signed_cert.pem') + alt = self.load_certificate('orphaned_cert.pem') + result = certificate_utils.is_issuer(cert, alt) + self.assertEqual(False, result) + + def test_is_issuer_with_invalid_certs(self): + # Test issuer check with invalid certificates + cert = self.load_certificate('self_signed_cert.pem') + result = certificate_utils.is_issuer(cert, None) + self.assertEqual(False, result) + result = certificate_utils.is_issuer(None, cert) + self.assertEqual(False, result) + + def test_can_sign_certificates(self): + # Test that a well-formatted certificate can sign + cert = self.load_certificate('self_signed_cert.pem') + result = certificate_utils.can_sign_certificates(cert, 'test-ID') + self.assertEqual(True, result) + + def test_cannot_sign_certificates_without_basic_constraints(self): + # Verify a certificate without basic constraints cannot sign + cert = self.load_certificate( + 'self_signed_cert_missing_ca_constraint.pem' + ) + result = certificate_utils.can_sign_certificates(cert, 'test-ID') + self.assertEqual(False, result) + + def test_cannot_sign_certificates_with_invalid_basic_constraints(self): + # Verify a certificate with invalid basic constraints cannot sign + cert = self.load_certificate( + 'self_signed_cert_invalid_ca_constraint.pem' + ) + result = certificate_utils.can_sign_certificates(cert, 'test-ID') + self.assertEqual(False, result) + + def test_cannot_sign_certificates_without_key_usage(self): + # Verify a certificate without key usage cannot sign + cert = self.load_certificate('self_signed_cert_missing_key_usage.pem') + result = certificate_utils.can_sign_certificates(cert, 'test-ID') + self.assertEqual(False, result) + + def test_cannot_sign_certificates_with_invalid_key_usage(self): + # Verify a certificate with invalid key usage cannot sign + cert = self.load_certificate('self_signed_cert_invalid_key_usage.pem') + result = certificate_utils.can_sign_certificates(cert, 'test-ID') + self.assertEqual(False, result) + + def test_verify_signing_certificate(self): + signing_certificate = self.load_certificate('self_signed_cert.pem') + signed_certificate = self.load_certificate('signed_cert.pem') + + certificate_utils.verify_certificate_signature( + signing_certificate, + signed_certificate + ) + + @mock.patch('cursive.signature_utils.get_certificate') + @mock.patch('oslo_utils.timeutils.utcnow') + def test_verify_valid_certificate(self, mock_utcnow, mock_get_cert): + mock_utcnow.return_value = datetime.datetime(2017, 1, 1) + certs = self.load_certificates( + ['self_signed_cert.pem', 'self_signed_cert.der', + 'signed_cert.pem'] + ) + mock_get_cert.side_effect = certs + cert_uuid = '3' + trusted_cert_uuids = ['1', '2'] + certificate_utils.verify_certificate( + None, cert_uuid, trusted_cert_uuids + ) + + @mock.patch('cursive.signature_utils.get_certificate') + @mock.patch('oslo_utils.timeutils.utcnow') + def test_verify_invalid_certificate(self, mock_utcnow, mock_get_cert): + mock_utcnow.return_value = datetime.datetime(2017, 1, 1) + certs = self.load_certificates( + ['self_signed_cert.pem', 'self_signed_cert.der', + 'orphaned_cert.pem'] + ) + mock_get_cert.side_effect = certs + cert_uuid = '3' + trusted_cert_uuids = ['1', '2'] + self.assertRaisesRegex( + exception.SignatureVerificationError, + "Certificate chain building failed. Could not locate the " + "signing certificate for the base certificate in the set of " + "trusted certificates.", + certificate_utils.verify_certificate, + None, + cert_uuid, + trusted_cert_uuids + ) + + @mock.patch('cursive.signature_utils.get_certificate') + @mock.patch('oslo_utils.timeutils.utcnow') + def test_verify_valid_certificate_with_no_root(self, mock_utcnow, + mock_get_cert): + mock_utcnow.return_value = datetime.datetime(2017, 1, 1) + + # Test verifying a valid certificate against an empty list of trusted + # certificates. + certs = self.load_certificates(['signed_cert.pem']) + mock_get_cert.side_effect = certs + cert_uuid = '3' + trusted_cert_uuids = [] + self.assertRaisesRegex( + exception.SignatureVerificationError, + "Certificate chain building failed. Could not locate the " + "signing certificate for the base certificate in the set of " + "trusted certificates.", + certificate_utils.verify_certificate, + None, + cert_uuid, + trusted_cert_uuids + ) + + @mock.patch('oslo_utils.timeutils.utcnow') + def test_context_init(self, mock_utcnow): + # Test constructing a context object with a valid set of certificates + mock_utcnow.return_value = datetime.datetime(2017, 1, 1) + certs = self.load_certificates( + ['self_signed_cert.pem', 'self_signed_cert.der'] + ) + cert_tuples = [('1', certs[0]), ('2', certs[1])] + context = certificate_utils.CertificateVerificationContext( + cert_tuples + ) + self.assertEqual(2, len(context._signing_certificates)) + for t in cert_tuples: + path, cert = t + self.assertIn(cert, [x[1] for x in context._signing_certificates]) + + @mock.patch('cursive.certificate_utils.LOG') + @mock.patch('oslo_utils.timeutils.utcnow') + def test_context_init_with_invalid_certificate(self, mock_utcnow, + mock_log): + # Test constructing a context object with an invalid certificate + mock_utcnow.return_value = datetime.datetime(2017, 1, 1) + alt_cert_tuples = [('path', None)] + context = certificate_utils.CertificateVerificationContext( + alt_cert_tuples + ) + self.assertEqual(0, len(context._signing_certificates)) + self.assertEqual(1, mock_log.error.call_count) + + @mock.patch('cursive.certificate_utils.LOG') + @mock.patch('oslo_utils.timeutils.utcnow') + def test_context_init_with_non_signing_certificate(self, mock_utcnow, + mock_log): + # Test constructing a context object with an non-signing certificate + mock_utcnow.return_value = datetime.datetime(2017, 1, 1) + non_signing_cert = self.load_certificate( + 'self_signed_cert_missing_key_usage.pem' + ) + alt_cert_tuples = [('path', non_signing_cert)] + context = certificate_utils.CertificateVerificationContext( + alt_cert_tuples + ) + self.assertEqual(0, len(context._signing_certificates)) + self.assertEqual(1, mock_log.warning.call_count) + + @mock.patch('cursive.certificate_utils.LOG') + @mock.patch('oslo_utils.timeutils.utcnow') + def test_context_init_with_out_of_date_certificate(self, mock_utcnow, + mock_log): + # Test constructing a context object with out-of-date certificates + mock_utcnow.return_value = datetime.datetime(2100, 1, 1) + certs = self.load_certificates( + ['self_signed_cert.pem', 'self_signed_cert.der'] + ) + cert_tuples = [('1', certs[0]), ('2', certs[1])] + context = certificate_utils.CertificateVerificationContext(cert_tuples) + self.assertEqual(0, len(context._signing_certificates)) + self.assertEqual(2, mock_log.warning.call_count) + + @mock.patch('oslo_utils.timeutils.utcnow') + def test_context_update_with_valid_certificate(self, mock_utcnow): + # Test updating the context with a valid certificate + mock_utcnow.return_value = datetime.datetime(2017, 1, 1) + certs = self.load_certificates( + ['self_signed_cert.pem', 'self_signed_cert.der'] + ) + cert_tuples = [('1', certs[0]), ('2', certs[1])] + context = certificate_utils.CertificateVerificationContext(cert_tuples) + cert = self.load_certificate('orphaned_cert.pem') + context.update(cert) + self.assertEqual(cert, context._signed_certificate) + + @mock.patch('oslo_utils.timeutils.utcnow') + def test_context_update_with_date_invalid_certificate(self, mock_utcnow): + # Test updating the context with an out-of-date certificate + mock_utcnow.return_value = datetime.datetime(2017, 1, 1) + certs = self.load_certificates( + ['self_signed_cert.pem', 'self_signed_cert.der'] + ) + cert_tuples = [('1', certs[0]), ('2', certs[1])] + context = certificate_utils.CertificateVerificationContext(cert_tuples) + cert = self.load_certificate('orphaned_cert.pem') + mock_utcnow.return_value = datetime.datetime(2100, 1, 1) + self.assertRaisesRegex( + exception.SignatureVerificationError, + "The certificate is outside its valid date range.", + context.update, + cert + ) + + def test_context_update_with_invalid_certificate(self): + # Test updating the context with an invalid certificate + certs = self.load_certificates( + ['self_signed_cert.pem', 'self_signed_cert.der'] + ) + cert_tuples = [('1', certs[0]), ('2', certs[1])] + context = certificate_utils.CertificateVerificationContext( + cert_tuples + ) + + self.assertRaisesRegex( + exception.SignatureVerificationError, + "The certificate must be an x509.Certificate object.", + context.update, + None + ) + + @mock.patch('oslo_utils.timeutils.utcnow') + def test_context_verify(self, mock_utcnow): + mock_utcnow.return_value = datetime.datetime(2017, 1, 1) + certs = self.load_certificates( + ['self_signed_cert.pem', 'self_signed_cert.der'] + ) + cert_tuples = [('1', certs[0]), ('2', certs[1])] + + # Test verification with a two-link certificate chain. + context = certificate_utils.CertificateVerificationContext( + cert_tuples + ) + cert = self.load_certificate('signed_cert.pem') + context.update(cert) + context.verify() + + # Test verification with a single-link certificate chain. + context = certificate_utils.CertificateVerificationContext( + cert_tuples + ) + context.update(certs[0]) + context.verify() + + @mock.patch('oslo_utils.timeutils.utcnow') + def test_context_verify_disable_checks(self, mock_utcnow): + mock_utcnow.return_value = datetime.datetime(2017, 1, 1) + certs = self.load_certificates( + ['self_signed_cert.pem', 'self_signed_cert.der'] + ) + cert_tuples = [('1', certs[0]), ('2', certs[1])] + + # Test verification with a two-link certificate chain. + context = certificate_utils.CertificateVerificationContext( + cert_tuples, + enforce_valid_dates=False, + enforce_signing_extensions=False, + enforce_path_length=False + ) + cert = self.load_certificate('signed_cert.pem') + context.update(cert) + context.verify() + + # Test verification with a single-link certificate chain. + context = certificate_utils.CertificateVerificationContext( + cert_tuples, + enforce_valid_dates=False, + enforce_signing_extensions=False, + enforce_path_length=False + ) + context.update(certs[0]) + context.verify() + + @mock.patch('oslo_utils.timeutils.utcnow') + def test_context_verify_invalid_chain_length(self, mock_utcnow): + mock_utcnow.return_value = datetime.datetime(2017, 11, 1) + certs = self.load_certificates( + ['grandparent_cert.pem', 'parent_cert.pem', 'child_cert.pem'] + ) + cert_tuples = [ + ('1', certs[0]), + ('2', certs[1]), + ('3', certs[2]) + ] + cert = self.load_certificate('grandchild_cert.pem') + + context = certificate_utils.CertificateVerificationContext( + cert_tuples + ) + context.update(cert) + self.assertRaisesRegex( + exception.SignatureVerificationError, + "Certificate validation failed. The signing certificate '1' is " + "not configured to support certificate chains of sufficient " + "length.", + context.verify + ) + + context = certificate_utils.CertificateVerificationContext( + cert_tuples, + enforce_path_length=False + ) + context.update(cert) + context.verify() diff --git a/cursive/tests/unit/test_signature_utils.py b/cursive/tests/unit/test_signature_utils.py index 2cf2b76..4a50f62 100644 --- a/cursive/tests/unit/test_signature_utils.py +++ b/cursive/tests/unit/test_signature_utils.py @@ -12,6 +12,7 @@ import base64 import datetime +import mock from castellan.common.exception import KeyManagerError import cryptography.exceptions as crypto_exceptions @@ -20,7 +21,6 @@ from cryptography.hazmat.primitives.asymmetric import dsa from cryptography.hazmat.primitives.asymmetric import ec from cryptography.hazmat.primitives.asymmetric import padding from cryptography.hazmat.primitives.asymmetric import rsa -import mock from oslo_utils import timeutils from cursive import exception @@ -110,6 +110,12 @@ class BadPublicKey(object): class TestSignatureUtils(base.TestCase): """Test methods of signature_utils""" + def setUp(self): + super(TestSignatureUtils, self).setUp() + + def tearDown(self): + super(TestSignatureUtils, self).tearDown() + def test_should_create_verifier(self): image_props = {CERT_UUID: 'CERT_UUID', HASH_METHOD: 'HASH_METHOD', @@ -283,7 +289,8 @@ class TestSignatureUtils(base.TestCase): 'RSB-PSS') @mock.patch('cursive.signature_utils.get_certificate') - def test_get_public_key_rsa(self, mock_get_cert): + @mock.patch('cursive.certificate_utils.verify_certificate') + def test_get_public_key_rsa(self, mock_verify_cert, mock_get_cert): fake_cert = FakeCryptoCertificate() mock_get_cert.return_value = fake_cert sig_key_type = signature_utils.SignatureKeyType.lookup( @@ -294,7 +301,8 @@ class TestSignatureUtils(base.TestCase): self.assertEqual(fake_cert.public_key(), result_pub_key) @mock.patch('cursive.signature_utils.get_certificate') - def test_get_public_key_ecc(self, mock_get_cert): + @mock.patch('cursive.certificate_utils.verify_certificate') + def test_get_public_key_ecc(self, mock_verify_cert, mock_get_cert): fake_cert = FakeCryptoCertificate(TEST_ECC_PRIVATE_KEY.public_key()) mock_get_cert.return_value = fake_cert sig_key_type = signature_utils.SignatureKeyType.lookup('ECC_SECP521R1') @@ -303,7 +311,8 @@ class TestSignatureUtils(base.TestCase): self.assertEqual(fake_cert.public_key(), result_pub_key) @mock.patch('cursive.signature_utils.get_certificate') - def test_get_public_key_dsa(self, mock_get_cert): + @mock.patch('cursive.certificate_utils.verify_certificate') + def test_get_public_key_dsa(self, mock_verify_cert, mock_get_cert): fake_cert = FakeCryptoCertificate(TEST_DSA_PRIVATE_KEY.public_key()) mock_get_cert.return_value = fake_cert sig_key_type = signature_utils.SignatureKeyType.lookup( @@ -314,7 +323,9 @@ class TestSignatureUtils(base.TestCase): self.assertEqual(fake_cert.public_key(), result_pub_key) @mock.patch('cursive.signature_utils.get_certificate') - def test_get_public_key_invalid_key(self, mock_get_certificate): + @mock.patch('cursive.certificate_utils.verify_certificate') + def test_get_public_key_invalid_key(self, mock_verify_certificate, + mock_get_certificate): bad_pub_key = 'A' * 256 mock_get_certificate.return_value = FakeCryptoCertificate(bad_pub_key) sig_key_type = signature_utils.SignatureKeyType.lookup( @@ -335,34 +346,6 @@ class TestSignatureUtils(base.TestCase): self.assertEqual(x509_cert, signature_utils.get_certificate(None, cert_uuid)) - @mock.patch('cryptography.x509.load_der_x509_certificate') - @mock.patch('castellan.key_manager.API', return_value=FakeKeyManager()) - def test_get_expired_certificate(self, mock_key_manager_API, - mock_load_cert): - cert_uuid = 'valid_format_cert' - x509_cert = FakeCryptoCertificate( - not_valid_after=timeutils.utcnow() - - datetime.timedelta(hours=1)) - mock_load_cert.return_value = x509_cert - self.assertRaisesRegex(exception.SignatureVerificationError, - 'Certificate is not valid after: .*', - signature_utils.get_certificate, None, - cert_uuid) - - @mock.patch('cryptography.x509.load_der_x509_certificate') - @mock.patch('castellan.key_manager.API', return_value=FakeKeyManager()) - def test_get_not_yet_valid_certificate(self, mock_key_manager_API, - mock_load_cert): - cert_uuid = 'valid_format_cert' - x509_cert = FakeCryptoCertificate( - not_valid_before=timeutils.utcnow() + - datetime.timedelta(hours=1)) - mock_load_cert.return_value = x509_cert - self.assertRaisesRegex(exception.SignatureVerificationError, - 'Certificate is not valid before: .*', - signature_utils.get_certificate, None, - cert_uuid) - @mock.patch('castellan.key_manager.API', return_value=FakeKeyManager()) def test_get_certificate_key_manager_fail(self, mock_key_manager_API): bad_cert_uuid = 'fea14bc2-d75f-4ba5-bccc-b5c924ad0695' diff --git a/releasenotes/notes/add-certificate-validation-68a1ffbd5369a8d1.yaml b/releasenotes/notes/add-certificate-validation-68a1ffbd5369a8d1.yaml new file mode 100644 index 0000000..d279d92 --- /dev/null +++ b/releasenotes/notes/add-certificate-validation-68a1ffbd5369a8d1.yaml @@ -0,0 +1,37 @@ +--- +prelude: > + The cursive library supports the verification of digital signatures. + However, there is no way currently to validate the certificate used to + generate a given signature. Adding certificate validation improves the + security of signature verification when each is used together. +features: + - Adds a variety of certificate utility functions that inspect certificate + attributes and extensions for different settings. + - Adds the CertificateVerificationContext class which uses a set of + trusted certificates to conduct certificate validation, verifying that a + given certificate is part of a certificate chain rooted with a trusted + certificate. + - Adds a verify_certificate method that loads all certificates needed for + certificate validation from the key manager and uses them to create a + CertificateVerificationContext object. The context is then used to + determine if a certificate is valid. +upgrade: + - The addition of certificate validation as a separate operation from the + signature verification process preserves backwards compatibility. + Signatures previously verifiable with cursive will still be verifiable. + However, their signing certificates may not be valid. Each signing + certificate should be checked for validity before it is used to conduct + signature verification. +security: + - The usage of certificate validation with the signature verification + process improves the security of signature verification. A signature + should not be considered valid unless its corresponding certificate is + also valid. +other: + - The CertificateVerificationContext is built using a set of trusted + certificates. However, to conduct certificate verification the context + builds the full certificate chain, starting with the certificate to + validate and ending with the self-signed root certificate. If this + self-signed root certificate is not present in the context, or if one + of the intermediate certificates is not present in the context, the + certificate chain cannot be built and certificate validation will fail.