From e59db1cdc1c704ee752c7070fe43c0034d7d5cf1 Mon Sep 17 00:00:00 2001 From: Peter Hamilton Date: Fri, 13 Nov 2015 10:27:25 -0500 Subject: [PATCH 1/6] Add certificate signature verification This change adds a CertificateVerificationContext to the x509 API. The context can be used to verify that arbitrary signed certificates were signed by the certificate associated with the context and that the signing certificate is a valid certificate capable of signing certificates. --- src/cryptography/x509/__init__.py | 7 + src/cryptography/x509/verification.py | 81 ++++++++ tests/test_x509_verification.py | 259 ++++++++++++++++++++++++++ 3 files changed, 347 insertions(+) create mode 100644 src/cryptography/x509/verification.py create mode 100644 tests/test_x509_verification.py diff --git a/src/cryptography/x509/__init__.py b/src/cryptography/x509/__init__.py index a1deb7f42cf8..281a84842fdd 100644 --- a/src/cryptography/x509/__init__.py +++ b/src/cryptography/x509/__init__.py @@ -34,6 +34,10 @@ CertificatePoliciesOID, ExtendedKeyUsageOID, ExtensionOID, NameOID, ObjectIdentifier, SignatureAlgorithmOID, _SIG_OIDS_TO_HASH ) +from cryptography.x509.verification import ( + CertificateVerificationContext, InvalidCertificate, + InvalidSigningCertificate +) OID_AUTHORITY_INFORMATION_ACCESS = ExtensionOID.AUTHORITY_INFORMATION_ACCESS @@ -117,6 +121,8 @@ "UnsupportedExtension", "ExtensionNotFound", "UnsupportedGeneralNameType", + "InvalidCertificate", + "InvalidSigningCertificate", "NameAttribute", "Name", "ObjectIdentifier", @@ -160,6 +166,7 @@ "RevokedCertificateBuilder", "CertificateSigningRequestBuilder", "CertificateBuilder", + "CertificateVerificationContext", "Version", "_SIG_OIDS_TO_HASH", "OID_CA_ISSUERS", diff --git a/src/cryptography/x509/verification.py b/src/cryptography/x509/verification.py new file mode 100644 index 000000000000..eaf836443ca3 --- /dev/null +++ b/src/cryptography/x509/verification.py @@ -0,0 +1,81 @@ +# This file is dual licensed under the terms of the Apache License, Version +# 2.0, and the BSD License. See the LICENSE file in the root of this repository +# for complete details. + +from __future__ import absolute_import, division, print_function + +from cryptography.hazmat.primitives.asymmetric import ec, padding, rsa +from cryptography.x509 import Certificate +from cryptography.x509.oid import ExtensionOID + + +class InvalidCertificate(Exception): + pass + + +class InvalidSigningCertificate(Exception): + pass + + +def _can_sign_certificates(certificate): + basic_constraints = certificate.extensions.get_extension_for_oid( + ExtensionOID.BASIC_CONSTRAINTS).value + key_usage = certificate.extensions.get_extension_for_oid( + ExtensionOID.KEY_USAGE).value + + if not basic_constraints.ca: + raise InvalidSigningCertificate( + "The certificate is not marked as a CA in its BasicConstraints " + "extension." + ) + elif not key_usage.key_cert_sign: + raise InvalidSigningCertificate( + "The certificate public key is not marked for verifying " + "certificates in its KeyUsage extension." + ) + else: + return True + + +class CertificateVerificationContext(object): + def __init__(self, signing_certificate): + if not isinstance(signing_certificate, Certificate): + raise InvalidCertificate( + "The signing certificate must be a Certificate." + ) + _can_sign_certificates(signing_certificate) + + self._signing_cert = signing_certificate + + def update(self, certificate): + """ + Processes the provided certificate and returns nothing. + """ + if not isinstance(certificate, Certificate): + raise InvalidCertificate( + "The signed certificate must be a Certificate." + ) + + self._signed_cert = certificate + + def verify(self): + """ + Raises an exception if the signature of the certificate provided to + update was not generated by the signing certificate. + """ + signature_hash_algorithm = self._signed_cert.signature_hash_algorithm + signature_bytes = self._signed_cert.signature + signer_public_key = self._signing_cert.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(self._signed_cert.tbs_certificate_bytes) + verifier.verify() diff --git a/tests/test_x509_verification.py b/tests/test_x509_verification.py new file mode 100644 index 000000000000..43c21254f42c --- /dev/null +++ b/tests/test_x509_verification.py @@ -0,0 +1,259 @@ +# This file is dual licensed under the terms of the Apache License, Version +# 2.0, and the BSD License. See the LICENSE file in the root of this repository +# for complete details. + +from __future__ import absolute_import, division, print_function + +import datetime +import os + +import pytest + +from cryptography import x509 +from cryptography.exceptions import InvalidSignature + +from cryptography.hazmat.backends.interfaces import X509Backend +from cryptography.hazmat.primitives import hashes +from cryptography.hazmat.primitives.asymmetric import dsa, ec + +from cryptography.x509 import oid +from cryptography.x509.extensions import ExtensionNotFound +from cryptography.x509.verification import ( + CertificateVerificationContext, InvalidCertificate, + InvalidSigningCertificate +) + +from .hazmat.primitives import fixtures_dsa, fixtures_ec, fixtures_rsa + +from .test_x509 import _load_cert + + +def build_subjects(common_names): + subjects = [] + for common_name in common_names: + subject = x509.Name([ + x509.NameAttribute(oid.NameOID.COUNTRY_NAME, u'US'), + x509.NameAttribute(oid.NameOID.STATE_OR_PROVINCE_NAME, u'State'), + x509.NameAttribute(oid.NameOID.LOCALITY_NAME, u'Locality'), + x509.NameAttribute(oid.NameOID.ORGANIZATION_NAME, u'Org'), + x509.NameAttribute(oid.NameOID.COMMON_NAME, common_name)] + ) + subjects.append(subject) + return subjects + + +def build_certificate_chain(names, not_valid_before, not_valid_after, + keys, extension_lists, hash_algorithm, backend, + serial_number): + subjects = build_subjects(names) + not_valid_befores = [not_valid_before] * len(subjects) + not_valid_afters = [not_valid_after] * len(subjects) + + certificate_chain = [] + private_key = keys[0].private_key(backend) + certificate_chain.append( + build_certificate( + subjects[0], subjects[0], not_valid_befores[0], + not_valid_afters[0], private_key, private_key.public_key(), + extension_lists[0], hash_algorithm, backend, serial_number + ) + ) + + for i in range(1, len(subjects)): + private_key = keys[i - 1].private_key(backend) + public_key = keys[i].private_key(backend).public_key() + certificate_chain.append( + build_certificate( + subjects[i], subjects[i - 1], not_valid_befores[i], + not_valid_afters[i], private_key, public_key, + extension_lists[i], hash_algorithm, backend, serial_number + ) + ) + + return certificate_chain + + +def build_certificate(subject, issuer, not_valid_before, not_valid_after, + signing_key, public_key, extensions, hash_alg, backend, + serial_number): + builder = x509.CertificateBuilder().serial_number( + serial_number + ).issuer_name( + issuer + ).subject_name( + subject + ).public_key( + public_key + ).not_valid_before( + not_valid_before + ).not_valid_after( + not_valid_after + ) + + for ext in extensions: + builder = builder.add_extension( + ext.get('extension'), ext.get('critical') + ) + return builder.sign(signing_key, hash_alg, backend) + + +def build_extensions(cas, path_lengths, key_cert_signs, num_extension_sets): + extension_lists = [] + for i in range(num_extension_sets): + extension_list = [ + { + 'extension': x509.BasicConstraints( + cas[i], path_lengths[i] + ), + 'critical': True + }, + { + 'extension': x509.KeyUsage( + digital_signature=False, + content_commitment=False, + key_encipherment=False, + data_encipherment=False, + key_agreement=False, + key_cert_sign=key_cert_signs[i], + crl_sign=False, + encipher_only=False, + decipher_only=False + ), + 'critical': True + } + ] + extension_lists.append(extension_list) + + return extension_lists + + +def _skip_backend_if_key_unsupported(key, backend): + private_key = key.private_key(backend) + if isinstance( + private_key, (dsa.DSAPrivateKey, ec.EllipticCurvePrivateKey) + ): + if backend._lib.OPENSSL_VERSION_NUMBER <= 0x10001000: + pytest.skip("Requires a newer OpenSSL. Must be > 1.0.1") + + +@pytest.mark.requires_backend_interface(interface=X509Backend) +class TestCertificateVerificationContext(object): + + def test_init(self, backend): + trusted_cert = _load_cert( + os.path.join( + "x509", "PKITS_data", "certs", "pathLenConstraint6CACert.crt" + ), + x509.load_der_x509_certificate, + backend + ) + CertificateVerificationContext(trusted_cert) + + def test_init_fail_not_a_cert(self, backend): + with pytest.raises(InvalidCertificate): + CertificateVerificationContext("invalid") + + def test_init_fail_cert_missing_extension(self, backend): + cert = _load_cert( + os.path.join("x509", "custom", "dsa_selfsigned_ca.pem"), + x509.load_pem_x509_certificate, + backend + ) + + with pytest.raises(ExtensionNotFound): + CertificateVerificationContext(cert) + + def test_init_fail_cert_with_ca_false(self, backend): + cert = _load_cert( + os.path.join( + "x509", "custom", "all_supported_names.pem" + ), + x509.load_pem_x509_certificate, + backend + ) + + with pytest.raises(InvalidSigningCertificate): + CertificateVerificationContext(cert) + + def test_init_fail_cert_with_key_cert_sign_false(self, backend): + cert = _load_cert( + os.path.join( + "x509", "PKITS_data", "certs", + "keyUsageNotCriticalkeyCertSignFalseCACert.crt" + ), + x509.load_der_x509_certificate, + backend + ) + + with pytest.raises(InvalidSigningCertificate): + CertificateVerificationContext(cert) + + def test_update(self, backend): + certificate = _load_cert( + os.path.join( + "x509", "PKITS_data", "certs", "pathLenConstraint6CACert.crt" + ), + x509.load_der_x509_certificate, + backend + ) + + # Update with a valid certificate. + verifier = CertificateVerificationContext(certificate) + verifier.update(certificate) + assert verifier._signed_cert == certificate + + # Update with an invalid certificate. + with pytest.raises(InvalidCertificate): + verifier.update("invalid") + + @pytest.mark.parametrize( + ("keys"), + [ + ( + [ + fixtures_rsa.RSA_KEY_512, + fixtures_rsa.RSA_KEY_1024, + ] + ), + ( + [ + fixtures_dsa.DSA_KEY_1024, + fixtures_dsa.DSA_KEY_2048, + ] + ), + ( + [ + fixtures_ec.EC_KEY_SECP192R1, + fixtures_ec.EC_KEY_SECT163K1, + ] + ), + ] + ) + def test_verify(self, keys, backend): + for key in keys: + _skip_backend_if_key_unsupported(key, backend) + + names = [u'a.com', u'b.com'] + not_valid_before = datetime.datetime(2002, 1, 1, 12, 1) + not_valid_after = datetime.datetime(2030, 12, 31, 8, 30) + serial_number = 77 + extension_lists = build_extensions( + [True, True], [0, 0], [True, True], len(names) + ) + + cert_chain = build_certificate_chain( + names, not_valid_before, not_valid_after, keys, extension_lists, + hashes.SHA1(), backend, serial_number + ) + + # Test a valid call. + verifier = CertificateVerificationContext(cert_chain[0]) + verifier.update(cert_chain[1]) + verifier.verify() + + # Test an invalid call. + verifier = CertificateVerificationContext(cert_chain[1]) + verifier.update(cert_chain[0]) + + with pytest.raises(InvalidSignature): + verifier.verify() From 0059d1553109e3f4fefc7cd582455ffdc456204f Mon Sep 17 00:00:00 2001 From: Peter Hamilton Date: Mon, 16 Nov 2015 13:27:44 -0500 Subject: [PATCH 2/6] Splitting out elliptic curve key tests This change moves the elliptic curve key tests into a separate method to allow for the specification of the EllipticCurveBackend. Omitting this caused various backend failures. --- tests/test_x509_verification.py | 54 ++++++++++++++++++--------------- 1 file changed, 29 insertions(+), 25 deletions(-) diff --git a/tests/test_x509_verification.py b/tests/test_x509_verification.py index 43c21254f42c..1563f0e53c5c 100644 --- a/tests/test_x509_verification.py +++ b/tests/test_x509_verification.py @@ -12,7 +12,9 @@ from cryptography import x509 from cryptography.exceptions import InvalidSignature -from cryptography.hazmat.backends.interfaces import X509Backend +from cryptography.hazmat.backends.interfaces import ( + EllipticCurveBackend, X509Backend +) from cryptography.hazmat.primitives import hashes from cryptography.hazmat.primitives.asymmetric import dsa, ec @@ -206,30 +208,7 @@ def test_update(self, backend): with pytest.raises(InvalidCertificate): verifier.update("invalid") - @pytest.mark.parametrize( - ("keys"), - [ - ( - [ - fixtures_rsa.RSA_KEY_512, - fixtures_rsa.RSA_KEY_1024, - ] - ), - ( - [ - fixtures_dsa.DSA_KEY_1024, - fixtures_dsa.DSA_KEY_2048, - ] - ), - ( - [ - fixtures_ec.EC_KEY_SECP192R1, - fixtures_ec.EC_KEY_SECT163K1, - ] - ), - ] - ) - def test_verify(self, keys, backend): + def _test_verify(self, keys, backend): for key in keys: _skip_backend_if_key_unsupported(key, backend) @@ -257,3 +236,28 @@ def test_verify(self, keys, backend): with pytest.raises(InvalidSignature): verifier.verify() + + @pytest.mark.parametrize( + ("keys"), + [ + ( + [ + fixtures_rsa.RSA_KEY_512, + fixtures_rsa.RSA_KEY_1024, + ] + ), + ( + [ + fixtures_dsa.DSA_KEY_1024, + fixtures_dsa.DSA_KEY_2048, + ] + ), + ] + ) + def test_verify(self, keys, backend): + self._test_verify(keys, backend) + + @pytest.mark.requires_backend_interface(interface=EllipticCurveBackend) + def test_verify_with_elliptic_curves(self, backend): + keys = [fixtures_ec.EC_KEY_SECP192R1, fixtures_ec.EC_KEY_SECT163K1] + self._test_verify(keys, backend) From b787c85483b5b66e2a1aaa244320c9141b4f30a5 Mon Sep 17 00:00:00 2001 From: Peter Hamilton Date: Mon, 16 Nov 2015 16:02:38 -0500 Subject: [PATCH 3/6] Adding backend check for elliptic curve support --- tests/test_x509_verification.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/tests/test_x509_verification.py b/tests/test_x509_verification.py index 1563f0e53c5c..7c9c72b512cd 100644 --- a/tests/test_x509_verification.py +++ b/tests/test_x509_verification.py @@ -27,6 +27,7 @@ from .hazmat.primitives import fixtures_dsa, fixtures_ec, fixtures_rsa +from .hazmat.primitives.test_ec import _skip_curve_unsupported from .test_x509 import _load_cert @@ -260,4 +261,6 @@ def test_verify(self, keys, backend): @pytest.mark.requires_backend_interface(interface=EllipticCurveBackend) def test_verify_with_elliptic_curves(self, backend): keys = [fixtures_ec.EC_KEY_SECP192R1, fixtures_ec.EC_KEY_SECT163K1] + for key in keys: + _skip_curve_unsupported(backend, key.public_numbers.curve) self._test_verify(keys, backend) From 63f8140ca4e761b72f0f83d17c017bffb7065fb3 Mon Sep 17 00:00:00 2001 From: Peter Hamilton Date: Tue, 17 Nov 2015 10:02:26 -0500 Subject: [PATCH 4/6] Adding subject/issuer name enforcement to verify This change adds a constraint to the signature verification process that ensures that the subject name of the signing certificate is checked against the issuer name of the signed certificate. --- src/cryptography/x509/verification.py | 26 +++++++++++++++++++------- tests/test_x509_verification.py | 12 ++++++++++++ 2 files changed, 31 insertions(+), 7 deletions(-) diff --git a/src/cryptography/x509/verification.py b/src/cryptography/x509/verification.py index eaf836443ca3..cca63965bd0f 100644 --- a/src/cryptography/x509/verification.py +++ b/src/cryptography/x509/verification.py @@ -37,19 +37,24 @@ def _can_sign_certificates(certificate): return True +def _is_issuing_certificate(issuing_certificate, issued_certificate): + return (issuing_certificate.subject == issued_certificate.issuer) + + class CertificateVerificationContext(object): - def __init__(self, signing_certificate): - if not isinstance(signing_certificate, Certificate): + def __init__(self, certificate): + if not isinstance(certificate, Certificate): raise InvalidCertificate( "The signing certificate must be a Certificate." ) - _can_sign_certificates(signing_certificate) + _can_sign_certificates(certificate) - self._signing_cert = signing_certificate + self._signing_cert = certificate def update(self, certificate): """ - Processes the provided certificate and returns nothing. + Processes the provided certificate. Raises an exception if the + certificate is invalid. """ if not isinstance(certificate, Certificate): raise InvalidCertificate( @@ -60,9 +65,16 @@ def update(self, certificate): def verify(self): """ - Raises an exception if the signature of the certificate provided to - update was not generated by the signing certificate. + Verifies the signature of the certificate provided to update against + the certificate associated with the context. Raises an exception if + the verification process fails. """ + if not _is_issuing_certificate(self._signing_cert, self._signed_cert): + raise InvalidCertificate( + "The certificate issuer does not match the subject name of " + "the context certificate." + ) + signature_hash_algorithm = self._signed_cert.signature_hash_algorithm signature_bytes = self._signed_cert.signature signer_public_key = self._signing_cert.public_key() diff --git a/tests/test_x509_verification.py b/tests/test_x509_verification.py index 7c9c72b512cd..9d2cac4fea26 100644 --- a/tests/test_x509_verification.py +++ b/tests/test_x509_verification.py @@ -235,6 +235,18 @@ def _test_verify(self, keys, backend): verifier = CertificateVerificationContext(cert_chain[1]) verifier.update(cert_chain[0]) + with pytest.raises(InvalidCertificate): + verifier.verify() + + # Test an invalid call with valid subject/issuer names. + keys.reverse() + alt_cert_chain = build_certificate_chain( + names, not_valid_before, not_valid_after, keys, extension_lists, + hashes.SHA1(), backend, serial_number + ) + verifier = CertificateVerificationContext(cert_chain[0]) + verifier.update(alt_cert_chain[1]) + with pytest.raises(InvalidSignature): verifier.verify() From da883932b71ae54252c82c3471d44bdca0406046 Mon Sep 17 00:00:00 2001 From: Peter Date: Mon, 4 Jan 2016 10:06:42 -0500 Subject: [PATCH 5/6] Adding CertificateVerificationContext documentation --- docs/x509/reference.rst | 35 +++++++++++++++++++++++++++++++++++ 1 file changed, 35 insertions(+) diff --git a/docs/x509/reference.rst b/docs/x509/reference.rst index 8bb3f40d6ded..8b31fe9f3e78 100644 --- a/docs/x509/reference.rst +++ b/docs/x509/reference.rst @@ -1116,6 +1116,34 @@ X.509 CSR (Certificate Signing Request) Builder Object The dotted string value of the OID (e.g. ``"2.5.4.3"``) +X.509 Certificate Verification +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +.. class:: CertificateVerificationContext + + .. versionadded:: 1.2 + + .. method:: update(certificate) + + Processes the provided certificate. Raises an exception if the + certificate is invalid. + + :param certificate: The certificate whose signature needs to be + verified. + + :raises cryptography.x509.verification.InvalidCertificate: If the + certificate is an invalid type. + + .. method:: verify() + + Verifies the signature of the certificate provided to update against + the certificate associated with the context. Raises an exception if the + verification process fails. + + :raises cryptography.x509.verification.InvalidCertificate: If the + certificate provided to update was not issued by the certificate + associated with the context. + .. _general_name_classes: General Name Classes @@ -2467,6 +2495,13 @@ Exceptions The integer value of the unsupported type. The complete list of types can be found in `RFC 5280 section 4.2.1.6`_. +.. currentmodule:: cryptography.x509.verification + +.. class:: InvalidCertificate + + This is raised by :class:`~cryptography.x509.CertificateVerificationContext` + when dealing with invalid certificate arguments and when certificate + signature verification fails. .. _`RFC 5280 section 4.2.1.1`: https://tools.ietf.org/html/rfc5280#section-4.2.1.1 .. _`RFC 5280 section 4.2.1.6`: https://tools.ietf.org/html/rfc5280#section-4.2.1.6 From 3497c1ce60ae590ae7b78ee9341c406662ca6180 Mon Sep 17 00:00:00 2001 From: Peter Date: Mon, 4 Jan 2016 11:09:45 -0500 Subject: [PATCH 6/6] Adding changelog update --- CHANGELOG.rst | 2 ++ 1 file changed, 2 insertions(+) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 717c9e71c781..502622dad490 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -51,6 +51,8 @@ Changelog CRLs. * Unrecognized non-critical X.509 extensions are now parsed into an :class:`~cryptography.x509.UnrecognizedExtension` object. +* Added :class: `~cryptography.x509.CertificationVerificationContext` to allow + verification of certificate signatures. 1.1.2 - 2015-12-10 ~~~~~~~~~~~~~~~~~~