From 77acc3638722fb55744478c00efba07abd6d5a0b Mon Sep 17 00:00:00 2001 From: Alex Gaynor Date: Wed, 13 Aug 2014 14:46:15 -0700 Subject: Fixes #145 -- fixes loading pkcs7 data from ASN1 --- OpenSSL/crypto.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/OpenSSL/crypto.py b/OpenSSL/crypto.py index 54569ea..313a30a 100644 --- a/OpenSSL/crypto.py +++ b/OpenSSL/crypto.py @@ -2351,7 +2351,7 @@ def load_pkcs7_data(type, buffer): if type == FILETYPE_PEM: pkcs7 = _lib.PEM_read_bio_PKCS7(bio, _ffi.NULL, _ffi.NULL, _ffi.NULL) elif type == FILETYPE_ASN1: - pass + pkcs7 = _lib.d2i_PKCS7_bio(bio, _ffi.NULL) else: # TODO: This is untested. _raise_current_error() -- cgit v1.2.1 From 4b9c96ae3c794c9f5c9d9ad1323d1f8ba4b5db1e Mon Sep 17 00:00:00 2001 From: Alex Gaynor Date: Thu, 14 Aug 2014 09:51:48 -0700 Subject: Added a test for loading ASN1 data --- OpenSSL/test/test_crypto.py | 32 ++++++++++++++++++++++++++++++-- 1 file changed, 30 insertions(+), 2 deletions(-) diff --git a/OpenSSL/test/test_crypto.py b/OpenSSL/test/test_crypto.py index bbe5d05..2da29a5 100644 --- a/OpenSSL/test/test_crypto.py +++ b/OpenSSL/test/test_crypto.py @@ -7,7 +7,9 @@ Unit tests for :py:mod:`OpenSSL.crypto`. from unittest import main -import os, re +import base64 +import os +import re from subprocess import PIPE, Popen from datetime import datetime, timedelta @@ -248,6 +250,27 @@ Ho4EzbYCOaEAMQA= -----END PKCS7----- """) +pkcs7DataASN1 = b(base64.b64decode(b""" +MIIDNwYJKoZIhvcNAQcCoIIDKDCCAyQCAQExADALBgkqhkiG9w0BBwGgggMKMIID +BjCCAm+gAwIBAgIBATANBgkqhkiG9w0BAQQFADB7MQswCQYDVQQGEwJTRzERMA8G +A1UEChMITTJDcnlwdG8xFDASBgNVBAsTC00yQ3J5cHRvIENBMSQwIgYDVQQDExtN +MkNyeXB0byBDZXJ0aWZpY2F0ZSBNYXN0ZXIxHTAbBgkqhkiG9w0BCQEWDm5ncHNA +cG9zdDEuY29tMB4XDTAwMDkxMDA5NTEzMFoXDTAyMDkxMDA5NTEzMFowUzELMAkG +A1UEBhMCU0cxETAPBgNVBAoTCE0yQ3J5cHRvMRIwEAYDVQQDEwlsb2NhbGhvc3Qx +HTAbBgkqhkiG9w0BCQEWDm5ncHNAcG9zdDEuY29tMFwwDQYJKoZIhvcNAQEBBQAD +SwAwSAJBAKy+e3dulvXzV7zoTZWc5TzgApr8DmeQHTYC8ydfzH7EECe4R1Xh5kwI +zOuuFfn178FBiS84gngaNcrFi0Z5fAkCAwEAAaOCAQQwggEAMAkGA1UdEwQCMAAw +LAYJYIZIAYb4QgENBB8WHU9wZW5TU0wgR2VuZXJhdGVkIENlcnRpZmljYXRlMB0G +A1UdDgQWBBTPhIKSvnsmYsBVNWjj0m3M2z0qVTCBpQYDVR0jBIGdMIGagBT7hyNp +65w6kxXlxb8pUU/+7Sg4AaF/pH0wezELMAkGA1UEBhMCU0cxETAPBgNVBAoTCE0y +Q3J5cHRvMRQwEgYDVQQLEwtNMkNyeXB0byBDQTEkMCIGA1UEAxMbTTJDcnlwdG8g +Q2VydGlmaWNhdGUgTWFzdGVyMR0wGwYJKoZIhvcNAQkBFg5uZ3BzQHBvc3QxLmNv +bYIBADANBgkqhkiG9w0BAQQFAAOBgQA7/CqT6PoHycTdhEStWNZde7M/2Yc6BoJu +VwnW8YxGO8Sn6UJ4FeffZNcYZddSDKosw8LtPOeWoK3JINjAk5jiPQ2cww++7QGG +/g5NDjxFZNDJP1dGiLAxPW6JXwov4v0FmdzfLOZ01jDcgQQZqEpYlgpuI5JEWUQ9 +Ho4EzbYCOaEAMQA= +""")) + crlData = b("""\ -----BEGIN X509 CRL----- MIIBWzCBxTANBgkqhkiG9w0BAQQFADBYMQswCQYDVQQGEwJVUzELMAkGA1UECBMC @@ -2560,7 +2583,7 @@ class FunctionTests(TestCase): dump_privatekey, FILETYPE_PEM, key, GOOD_CIPHER, cb) - def test_load_pkcs7_data(self): + def test_load_pkcs7_data_pem(self): """ :py:obj:`load_pkcs7_data` accepts a PKCS#7 string and returns an instance of :py:obj:`PKCS7Type`. @@ -2569,6 +2592,11 @@ class FunctionTests(TestCase): self.assertTrue(isinstance(pkcs7, PKCS7Type)) + def test_load_pkcs7_data_asn1(self): + pkcs7 = load_pkcs7_data(FILETYPE_ASN1, pkcs7DataASN1) + self.assertTrue(isinstance(pkcs7, PKCS7Type)) + + def test_load_pkcs7_data_invalid(self): """ If the data passed to :py:obj:`load_pkcs7_data` is invalid, -- cgit v1.2.1 From 8fa1dd68a83eeceb4fef78122e1d5d33a53b0fab Mon Sep 17 00:00:00 2001 From: Alex Gaynor Date: Thu, 14 Aug 2014 09:57:51 -0700 Subject: Py3k fix --- OpenSSL/test/test_crypto.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/OpenSSL/test/test_crypto.py b/OpenSSL/test/test_crypto.py index 2da29a5..c6ed6c0 100644 --- a/OpenSSL/test/test_crypto.py +++ b/OpenSSL/test/test_crypto.py @@ -250,7 +250,7 @@ Ho4EzbYCOaEAMQA= -----END PKCS7----- """) -pkcs7DataASN1 = b(base64.b64decode(b""" +pkcs7DataASN1 = base64.b64decode(b""" MIIDNwYJKoZIhvcNAQcCoIIDKDCCAyQCAQExADALBgkqhkiG9w0BBwGgggMKMIID BjCCAm+gAwIBAgIBATANBgkqhkiG9w0BAQQFADB7MQswCQYDVQQGEwJTRzERMA8G A1UEChMITTJDcnlwdG8xFDASBgNVBAsTC00yQ3J5cHRvIENBMSQwIgYDVQQDExtN @@ -269,7 +269,7 @@ bYIBADANBgkqhkiG9w0BAQQFAAOBgQA7/CqT6PoHycTdhEStWNZde7M/2Yc6BoJu VwnW8YxGO8Sn6UJ4FeffZNcYZddSDKosw8LtPOeWoK3JINjAk5jiPQ2cww++7QGG /g5NDjxFZNDJP1dGiLAxPW6JXwov4v0FmdzfLOZ01jDcgQQZqEpYlgpuI5JEWUQ9 Ho4EzbYCOaEAMQA= -""")) +""") crlData = b("""\ -----BEGIN X509 CRL----- -- cgit v1.2.1 From 9875a9149dae1e64129d50690a455ce693f20ac9 Mon Sep 17 00:00:00 2001 From: Alex Gaynor Date: Thu, 14 Aug 2014 13:35:05 -0700 Subject: Added a docstring --- OpenSSL/test/test_crypto.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/OpenSSL/test/test_crypto.py b/OpenSSL/test/test_crypto.py index c6ed6c0..60f9e6a 100644 --- a/OpenSSL/test/test_crypto.py +++ b/OpenSSL/test/test_crypto.py @@ -2593,6 +2593,10 @@ class FunctionTests(TestCase): def test_load_pkcs7_data_asn1(self): + """ + :py:obj:`load_pkcs7_data` accepts a bytes containing ASN1 data + representing PKCS#7 and returns an instance of :py:obj`PKCS7Type`. + """ pkcs7 = load_pkcs7_data(FILETYPE_ASN1, pkcs7DataASN1) self.assertTrue(isinstance(pkcs7, PKCS7Type)) -- cgit v1.2.1 From bff5c43257d011af07bf212b16fa9c47ef43305d Mon Sep 17 00:00:00 2001 From: Alex Gaynor Date: Wed, 20 Aug 2014 23:10:55 -0700 Subject: This needs the latest cryptography --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index 3d3fe04..1450630 100755 --- a/setup.py +++ b/setup.py @@ -34,7 +34,7 @@ setup(name='pyOpenSSL', version=__version__, maintainer_email = 'exarkun@twistedmatrix.com', url = 'https://github.com/pyca/pyopenssl', license = 'APL2', - install_requires=["cryptography>=0.4", "six>=1.5.2"], + install_requires=["cryptography>=0.5.4", "six>=1.5.2"], long_description = """\ High-level wrapper around a subset of the OpenSSL library, includes * SSL.Connection objects, wrapping the methods of Python's portable -- cgit v1.2.1 From cf823d02e8986c06d65c2e669adf96f77b44ff4b Mon Sep 17 00:00:00 2001 From: Alex Gaynor Date: Thu, 21 Aug 2014 10:50:18 -0700 Subject: Added a changelog entry --- ChangeLog | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/ChangeLog b/ChangeLog index 9ad9317..291f977 100644 --- a/ChangeLog +++ b/ChangeLog @@ -1,3 +1,8 @@ +2014-08-21 Alex Gaynor + + * OpenSSL/crypto.py: Fixed a regression where calling ``load_pkcs7_data`` + with ``FILETYPE_ASN1`` would fail with a ``NameError. + 2014-05-05 Jean-Paul Calderone * OpenSSL/SSL.py: Fix a regression in which the first argument of -- cgit v1.2.1 From 3b0ee97f10decc3eeafa4039facdb1cbfd23d6b9 Mon Sep 17 00:00:00 2001 From: Alex Gaynor Date: Sat, 15 Nov 2014 09:17:33 -0800 Subject: Fixed several typos --- OpenSSL/RATIONALE | 2 +- OpenSSL/crypto.py | 2 +- OpenSSL/test/test_crypto.py | 4 ++-- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/OpenSSL/RATIONALE b/OpenSSL/RATIONALE index a0e389c..074422c 100644 --- a/OpenSSL/RATIONALE +++ b/OpenSSL/RATIONALE @@ -30,7 +30,7 @@ This is a list of things we need from an OpenSSL module: This could of course be done a lot better than the way it works now, so more transport layers than sockets are possible! + A well-organized error system that mimics OpenSSL's error system is - desireable. Specifically there has to be a way to find out wether the + desirable. Specifically there has to be a way to find out wether the operation was successful, or if it failed, why it failed, so some sort of interface to OpenSSL's error queue mechanism is needed. + Certificate objects (X509) and certificate name objects (X509_NAME) are diff --git a/OpenSSL/crypto.py b/OpenSSL/crypto.py index 313a30a..8d971f2 100644 --- a/OpenSSL/crypto.py +++ b/OpenSSL/crypto.py @@ -1897,7 +1897,7 @@ class PKCS12(object): def set_ca_certificates(self, cacerts): """ - Replace or set the CA certificates withing the PKCS12 object. + Replace or set the CA certificates within the PKCS12 object. :param cacerts: The new CA certificates. :type cacerts: :py:data:`None` or an iterable of :py:class:`X509` diff --git a/OpenSSL/test/test_crypto.py b/OpenSSL/test/test_crypto.py index 60f9e6a..f704ac0 100644 --- a/OpenSSL/test/test_crypto.py +++ b/OpenSSL/test/test_crypto.py @@ -536,7 +536,7 @@ class X509ExtTests(TestCase): def test_issuer(self): """ - If an extension requires a issuer, the :py:data:`issuer` parameter to + If an extension requires an issuer, the :py:data:`issuer` parameter to :py:class:`X509Extension` provides its value. """ ext2 = X509Extension( @@ -1212,7 +1212,7 @@ class X509ReqTests(TestCase, _PKeyInteractionTestsMixin): def test_verify_success(self): """ :py:obj:`X509Req.verify` returns :py:obj:`True` if called with a - :py:obj:`OpenSSL.crypto.PKey` which represents the public part ofthe key + :py:obj:`OpenSSL.crypto.PKey` which represents the public part of the key which signed the request. """ request = X509Req() -- cgit v1.2.1 From b586da331b82a4f50fe78138a40fe61a143cea8d Mon Sep 17 00:00:00 2001 From: Alex Gaynor Date: Sat, 15 Nov 2014 09:22:21 -0800 Subject: verisign.org turned off SSLv3 (Happy Days!) --- OpenSSL/test/test_ssl.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/OpenSSL/test/test_ssl.py b/OpenSSL/test/test_ssl.py index 6409b8e..44980d5 100644 --- a/OpenSSL/test/test_ssl.py +++ b/OpenSSL/test/test_ssl.py @@ -938,8 +938,8 @@ class ContextTests(TestCase, _LoopbackMixin): # in a unit test is bad, but it's the only way I can think of to # really test this. -exarkun - # Arg, verisign.com doesn't speak TLSv1 - context = Context(SSLv3_METHOD) + # Arg, verisign.com doesn't speak anything newer than TLS 1.0 + context = Context(TLSv1_METHOD) context.set_default_verify_paths() context.set_verify( VERIFY_PEER, -- cgit v1.2.1 From 932f5cc1d7dc5ef2c85ff0f38b9cf8879c733521 Mon Sep 17 00:00:00 2001 From: Jean-Paul Calderone Date: Thu, 11 Dec 2014 13:58:00 -0500 Subject: Add tests for Context.check_privatekey. --- OpenSSL/test/test_ssl.py | 37 +++++++++++++++++++++++++++++++++++++ 1 file changed, 37 insertions(+) diff --git a/OpenSSL/test/test_ssl.py b/OpenSSL/test/test_ssl.py index 44980d5..bbd97e6 100644 --- a/OpenSSL/test/test_ssl.py +++ b/OpenSSL/test/test_ssl.py @@ -507,6 +507,43 @@ class ContextTests(TestCase, _LoopbackMixin): ctx.use_certificate_file(pem_filename, long(FILETYPE_PEM)) + def test_check_privatekey_valid(self): + """ + :py:obj:`Context.check_privatekey` returns :py:obj:`None` if the + :py:obj:`Context` instance has been configured to use a matched key and + certificate pair. + """ + key = load_privatekey(FILETYPE_PEM, client_key_pem) + cert = load_certificate(FILETYPE_PEM, client_cert_pem) + context = Context(TLSv1_METHOD) + context.use_privatekey(key) + context.use_certificate(cert) + self.assertIs(None, context.check_privatekey()) + + + def test_check_privatekey_invalid(self): + """ + :py:obj:`Context.check_privatekey` raises :py:obj:`Error` if the + :py:obj:`Context` instance has been configured to use a key and + certificate pair which don't relate to each other. + """ + key = load_privatekey(FILETYPE_PEM, client_key_pem) + cert = load_certificate(FILETYPE_PEM, server_cert_pem) + context = Context(TLSv1_METHOD) + context.use_privatekey(key) + context.use_certificate(cert) + self.assertRaises(Error, context.check_privatekey) + + + def test_check_privatekey_wrong_args(self): + """ + :py:obj:`Context.check_privatekey` raises :py:obj:`TypeError` if called + with other than no arguments. + """ + context = Context(TLSv1_METHOD) + self.assertRaises(TypeError, context.check_privatekey, object()) + + def test_set_app_data_wrong_args(self): """ :py:obj:`Context.set_app_data` raises :py:obj:`TypeError` if called with other than -- cgit v1.2.1 From a03449287727239f58f1c319bff134ebc7f2013a Mon Sep 17 00:00:00 2001 From: Jean-Paul Calderone Date: Thu, 11 Dec 2014 14:02:31 -0500 Subject: Add the necessary SSL_CTX_check_private_key call and error handling. --- OpenSSL/SSL.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/OpenSSL/SSL.py b/OpenSSL/SSL.py index 7b1cbc1..751cc2a 100644 --- a/OpenSSL/SSL.py +++ b/OpenSSL/SSL.py @@ -492,6 +492,9 @@ class Context(object): :return: None (raises an exception if something's wrong) """ + if not _lib.SSL_CTX_check_private_key(self._context): + _raise_current_error() + def load_client_ca(self, cafile): """ -- cgit v1.2.1 From 01209075d4214b4e7104cd5c0ca5fb09b7724cbe Mon Sep 17 00:00:00 2001 From: Jean-Paul Calderone Date: Thu, 11 Dec 2014 14:03:02 -0500 Subject: Tentatively bump the required version of cryptography necessary for the new OpenSSL API. --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index 1450630..65a1b52 100755 --- a/setup.py +++ b/setup.py @@ -34,7 +34,7 @@ setup(name='pyOpenSSL', version=__version__, maintainer_email = 'exarkun@twistedmatrix.com', url = 'https://github.com/pyca/pyopenssl', license = 'APL2', - install_requires=["cryptography>=0.5.4", "six>=1.5.2"], + install_requires=["cryptography>=0.7", "six>=1.5.2"], long_description = """\ High-level wrapper around a subset of the OpenSSL library, includes * SSL.Connection objects, wrapping the methods of Python's portable -- cgit v1.2.1 From 5ab37af29fd602b49ce89fe54cbdf4c12cc81f5c Mon Sep 17 00:00:00 2001 From: Jean-Paul Calderone Date: Thu, 11 Dec 2014 14:05:24 -0500 Subject: ChangeLog --- ChangeLog | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/ChangeLog b/ChangeLog index 291f977..3226ca7 100644 --- a/ChangeLog +++ b/ChangeLog @@ -1,3 +1,8 @@ +2014-12-11 Jean-Paul Calderone + + * OpenSSL/SSL.py: Fixed a regression ``Context.check_privatekey`` + causing it to always succeed - even if it should fail. + 2014-08-21 Alex Gaynor * OpenSSL/crypto.py: Fixed a regression where calling ``load_pkcs7_data`` -- cgit v1.2.1 From a35801674c3cd91c45b41f552e1a2ed96b62cbad Mon Sep 17 00:00:00 2001 From: Jean-Paul Calderone Date: Fri, 12 Dec 2014 18:39:20 -0500 Subject: Perhaps this is a sensible way to declare a dependency on post-0.6.1 master? Perhaps not, I'm not sure. This might be nonsense. --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index 65a1b52..8d9d12a 100755 --- a/setup.py +++ b/setup.py @@ -34,7 +34,7 @@ setup(name='pyOpenSSL', version=__version__, maintainer_email = 'exarkun@twistedmatrix.com', url = 'https://github.com/pyca/pyopenssl', license = 'APL2', - install_requires=["cryptography>=0.7", "six>=1.5.2"], + install_requires=["cryptography>=0.7.dev0", "six>=1.5.2"], long_description = """\ High-level wrapper around a subset of the OpenSSL library, includes * SSL.Connection objects, wrapping the methods of Python's portable -- cgit v1.2.1 From 77b3d0888a38808f9f00440ada01001c55451356 Mon Sep 17 00:00:00 2001 From: Jean-Paul Calderone Date: Fri, 12 Dec 2014 20:04:35 -0500 Subject: Give assertIs and assertIsNot their stdlib-preferred names. --- OpenSSL/test/util.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/OpenSSL/test/util.py b/OpenSSL/test/util.py index 21bbdc4..4260eb0 100644 --- a/OpenSSL/test/util.py +++ b/OpenSSL/test/util.py @@ -227,7 +227,7 @@ class TestCase(TestCase): failIfIn = assertNotIn - def failUnlessIdentical(self, first, second, msg=None): + def assertIs(self, first, second, msg=None): """ Fail the test if :py:data:`first` is not :py:data:`second`. This is an obect-identity-equality test, not an object equality @@ -239,10 +239,10 @@ class TestCase(TestCase): if first is not second: raise self.failureException(msg or '%r is not %r' % (first, second)) return first - assertIdentical = failUnlessIdentical + assertIdentical = failUnlessIdentical = assertIs - def failIfIdentical(self, first, second, msg=None): + def assertIsNot(self, first, second, msg=None): """ Fail the test if :py:data:`first` is :py:data:`second`. This is an obect-identity-equality test, not an object equality @@ -254,7 +254,7 @@ class TestCase(TestCase): if first is second: raise self.failureException(msg or '%r is %r' % (first, second)) return first - assertNotIdentical = failIfIdentical + assertNotIdentical = failIfIdentical = assertIsNot def failUnlessRaises(self, exception, f, *args, **kwargs): -- cgit v1.2.1 From c85e08616dc7c18f4f24c808f39fcf22d2cce487 Mon Sep 17 00:00:00 2001 From: Paul Aurich Date: Thu, 8 Jan 2015 08:34:33 -0800 Subject: Add test for Connection.shutdown on closed socket --- OpenSSL/test/test_ssl.py | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/OpenSSL/test/test_ssl.py b/OpenSSL/test/test_ssl.py index 44980d5..79010fb 100644 --- a/OpenSSL/test/test_ssl.py +++ b/OpenSSL/test/test_ssl.py @@ -1709,6 +1709,20 @@ class ConnectionTests(TestCase, _LoopbackMixin): self.assertEquals(server.get_shutdown(), SENT_SHUTDOWN|RECEIVED_SHUTDOWN) + def test_shutdown_closed(self): + """ + If the underlying socket is closed, :py:obj:`Connection.shutdown` propagates the + write error from the low level write call. + """ + server, client = self._loopback() + server.sock_shutdown(2) + exc = self.assertRaises(SysCallError, server.shutdown) + if platform == "win32": + self.assertEqual(exc.args[0], ESHUTDOWN) + else: + self.assertEqual(exc.args[0], EPIPE) + + def test_set_shutdown(self): """ :py:obj:`Connection.set_shutdown` sets the state of the SSL connection shutdown -- cgit v1.2.1 From bff1d1ae72e743e41254bb31a5b50b5b63a37b85 Mon Sep 17 00:00:00 2001 From: Paul Aurich Date: Thu, 8 Jan 2015 08:36:53 -0800 Subject: Fixes #91 -- proper error handling in Connection.shutdown On error (return < 0), the OpenSSL documentation says to call SSL_get_error to discover the cause, as the act of trying to shutdown may raise lower-level errors (e.g. socket errors), or return SSL_ERR_WANT_WRITE or SSL_ERR_WANT_READ. --- OpenSSL/SSL.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/OpenSSL/SSL.py b/OpenSSL/SSL.py index 7b1cbc1..b6c8076 100644 --- a/OpenSSL/SSL.py +++ b/OpenSSL/SSL.py @@ -1183,8 +1183,7 @@ class Connection(object): """ result = _lib.SSL_shutdown(self._ssl) if result < 0: - # TODO: This is untested. - _raise_current_error() + self._raise_ssl_error(self._ssl, result) elif result > 0: return True else: -- cgit v1.2.1 From 306b75ab0d8959e8bfb21bdad831a5610609afaa Mon Sep 17 00:00:00 2001 From: Paul Aurich Date: Thu, 8 Jan 2015 18:35:03 -0800 Subject: ChangeLog entry --- ChangeLog | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/ChangeLog b/ChangeLog index 291f977..4019c27 100644 --- a/ChangeLog +++ b/ChangeLog @@ -1,3 +1,8 @@ +2015-01-08 Paul Aurich + + * OpenSSL/SSL.py: ``Connection.shutdown`` now propagates errors from the + underlying socket. + 2014-08-21 Alex Gaynor * OpenSSL/crypto.py: Fixed a regression where calling ``load_pkcs7_data`` -- cgit v1.2.1 From 7cf3b47ef61479820e5bf779dea84fc2d09fae07 Mon Sep 17 00:00:00 2001 From: Jean-Paul Calderone Date: Sun, 18 Jan 2015 18:35:40 -0500 Subject: 0.7 was released. --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index 8d9d12a..65a1b52 100755 --- a/setup.py +++ b/setup.py @@ -34,7 +34,7 @@ setup(name='pyOpenSSL', version=__version__, maintainer_email = 'exarkun@twistedmatrix.com', url = 'https://github.com/pyca/pyopenssl', license = 'APL2', - install_requires=["cryptography>=0.7.dev0", "six>=1.5.2"], + install_requires=["cryptography>=0.7", "six>=1.5.2"], long_description = """\ High-level wrapper around a subset of the OpenSSL library, includes * SSL.Connection objects, wrapping the methods of Python's portable -- cgit v1.2.1 From 0d9815fb0a1b24b59f0329e8d623f4a34239399a Mon Sep 17 00:00:00 2001 From: Stephen Holsapple Date: Wed, 27 Aug 2014 19:36:53 -0700 Subject: Add OpenSSL.crypto.verify_chain method. This change adds support for verifying a certificate or a certificate chain. This implementation uses OpenSSL's underlying X509_STORE_CTX_* class of functions to accomplish this. This change also adds an intermediate signing certificate/key and a service certificate/key signed with the intermediate signing certificate, to make testing the OpenSSL.crypto.verify_chain method easier to test. I figured I would add it to the top level module so other people can use an intermediate signing certificate in their own tests. Issue: https://github.com/pyca/pyopenssl/issues/154 --- ChangeLog | 2 +- OpenSSL/_util.py | 22 +++++-- OpenSSL/crypto.py | 81 +++++++++++++++++++++++ OpenSSL/test/test_crypto.py | 156 +++++++++++++++++++++++++++++++++++++++++++- doc/api/crypto.rst | 24 ++++++- 5 files changed, 277 insertions(+), 8 deletions(-) diff --git a/ChangeLog b/ChangeLog index 482bae4..e5b2ac4 100644 --- a/ChangeLog +++ b/ChangeLog @@ -11,7 +11,7 @@ 2014-08-21 Alex Gaynor * OpenSSL/crypto.py: Fixed a regression where calling ``load_pkcs7_data`` - with ``FILETYPE_ASN1`` would fail with a ``NameError. + with ``FILETYPE_ASN1`` would fail with a ``NameError``. 2014-05-05 Jean-Paul Calderone diff --git a/OpenSSL/_util.py b/OpenSSL/_util.py index baeecc6..cf13666 100644 --- a/OpenSSL/_util.py +++ b/OpenSSL/_util.py @@ -5,11 +5,25 @@ binding = Binding() ffi = binding.ffi lib = binding.lib -def exception_from_error_queue(exceptionType): - def text(charp): - return native(ffi.string(charp)) + + +def text(charp): + return native(ffi.string(charp)) + + + +def exception_from_error_queue(exception_type): + """ + Convert an OpenSSL library failure into a Python exception. + + When a call to the native OpenSSL library fails, this is usually signalled + by the return value, and an error code is stored in an error queue + associated with the current thread. The err library provides functions to + obtain these error codes and textual error messages. + """ errors = [] + while True: error = lib.ERR_get_error() if error == 0: @@ -19,7 +33,7 @@ def exception_from_error_queue(exceptionType): text(lib.ERR_func_error_string(error)), text(lib.ERR_reason_error_string(error)))) - raise exceptionType(errors) + raise exception_type(errors) diff --git a/OpenSSL/crypto.py b/OpenSSL/crypto.py index 8d971f2..6c07962 100644 --- a/OpenSSL/crypto.py +++ b/OpenSSL/crypto.py @@ -25,13 +25,43 @@ TYPE_RSA = _lib.EVP_PKEY_RSA TYPE_DSA = _lib.EVP_PKEY_DSA + class Error(Exception): """ An error occurred in an `OpenSSL.crypto` API. """ + +def _exception_from_context_error(exception_type, store_ctx): + """ + Convert a :py:func:`OpenSSL.crypto.verify_cert` failure into a Python + exception. + + When a call to native OpenSSL X509_verify_cert fails, additonal information + about the failure can be contained from the store context. + """ + + errors = [ + _lib.X509_STORE_CTX_get_error(store_ctx._store_ctx), + _lib.X509_STORE_CTX_get_error_depth(store_ctx._store_ctx), + _native(_ffi.string(_lib.X509_verify_cert_error_string(_lib.X509_STORE_CTX_get_error(store_ctx._store_ctx)))), + ] + _x509 = _lib.X509_STORE_CTX_get_current_cert(store_ctx._store_ctx) + if _x509 != _ffi.NULL: + _cert = _lib.X509_dup(_x509) + pycert = X509.__new__(X509) + pycert._x509 = _ffi.gc(_cert, _lib.X509_free) + e = exception_type(errors) + e.certificate = pycert + raise e + + + _raise_current_error = partial(_exception_from_error_queue, Error) +_raise_context_error = partial(_exception_from_context_error, Error) + + def _untested_error(where): """ @@ -1357,6 +1387,44 @@ X509StoreType = X509Store +class X509StoreContext(object): + """ + An X.509 store context. + + A :py:class:`X509StoreContext` is used to verify a certificate in some + context in conjunction with :py:func:`verify_cert`. The information + encapsulated in this object includes, but is not limited to, a set of + trusted certificates, verification parameters and revoked certificates. + + :param store: A :py:class:`X509Store` of trusted certificates. + :param cert: An :py:class:`X509` certificate to be validated during a + subsequent call to :py:func:`verify_cert`. + """ + + def __init__(self, store, cert): + store_ctx = _lib.X509_STORE_CTX_new() + self._store_ctx = _ffi.gc(store_ctx, _lib.X509_STORE_CTX_free) + self._store = store + self._cert = cert + + def _init(self): + """ + Set up the store context for a subsequent verification operation. + """ + ret = _lib.X509_STORE_CTX_init(self._store_ctx, self._store._store, self._cert._x509, _ffi.NULL) + if ret <= 0: + _raise_current_error() + + def _cleanup(self): + """ + Internally cleans up the store context. + + The store context can then be reused with a new call to + :py:meth:`init`. + """ + _lib.X509_STORE_CTX_cleanup(self._store_ctx) + + def load_certificate(type, buffer): """ Load a certificate from a buffer @@ -2304,6 +2372,19 @@ def verify(cert, signature, data, digest): _raise_current_error() +def verify_cert(store_ctx): + """ + Verify a certificate in a context. + + :param store_ctx: The :py:class:`X509StoreContext` to verify. + :raises: Error + """ + store_ctx._init() + ret = _lib.X509_verify_cert(store_ctx._store_ctx) + store_ctx._cleanup() + if ret <= 0: + _raise_context_error(store_ctx) + def load_crl(type, buffer): """ diff --git a/OpenSSL/test/test_crypto.py b/OpenSSL/test/test_crypto.py index f704ac0..8c04938 100644 --- a/OpenSSL/test/test_crypto.py +++ b/OpenSSL/test/test_crypto.py @@ -10,6 +10,7 @@ from unittest import main import base64 import os import re +import sys from subprocess import PIPE, Popen from datetime import datetime, timedelta @@ -17,7 +18,8 @@ from six import u, b, binary_type from OpenSSL.crypto import TYPE_RSA, TYPE_DSA, Error, PKey, PKeyType from OpenSSL.crypto import X509, X509Type, X509Name, X509NameType -from OpenSSL.crypto import X509Store, X509StoreType, X509Req, X509ReqType +from OpenSSL.crypto import X509Store, X509StoreType, X509StoreContext +from OpenSSL.crypto import X509Req, X509ReqType from OpenSSL.crypto import X509Extension, X509ExtensionType from OpenSSL.crypto import load_certificate, load_privatekey from OpenSSL.crypto import FILETYPE_PEM, FILETYPE_ASN1, FILETYPE_TEXT @@ -28,7 +30,7 @@ from OpenSSL.crypto import PKCS12, PKCS12Type, load_pkcs12 from OpenSSL.crypto import CRL, Revoked, load_crl from OpenSSL.crypto import NetscapeSPKI, NetscapeSPKIType from OpenSSL.crypto import ( - sign, verify, get_elliptic_curve, get_elliptic_curves) + sign, verify, verify_cert, get_elliptic_curve, get_elliptic_curves) from OpenSSL.test.util import EqualityTestsMixin, TestCase from OpenSSL._util import native, lib @@ -83,6 +85,40 @@ cbvAhow217X9V0dVerEOKxnNYspXRrh36h7k4mQA+sDq -----END RSA PRIVATE KEY----- """) +intermediate_cert_pem = b("""-----BEGIN CERTIFICATE----- +MIICVzCCAcCgAwIBAgIRAMPzhm6//0Y/g2pmnHR2C4cwDQYJKoZIhvcNAQENBQAw +WDELMAkGA1UEBhMCVVMxCzAJBgNVBAgTAklMMRAwDgYDVQQHEwdDaGljYWdvMRAw +DgYDVQQKEwdUZXN0aW5nMRgwFgYDVQQDEw9UZXN0aW5nIFJvb3QgQ0EwHhcNMTQw +ODI4MDIwNDA4WhcNMjQwODI1MDIwNDA4WjBmMRUwEwYDVQQDEwxpbnRlcm1lZGlh +dGUxDDAKBgNVBAoTA29yZzERMA8GA1UECxMIb3JnLXVuaXQxCzAJBgNVBAYTAlVT +MQswCQYDVQQIEwJDQTESMBAGA1UEBxMJU2FuIERpZWdvMIGfMA0GCSqGSIb3DQEB +AQUAA4GNADCBiQKBgQDYcEQw5lfbEQRjr5Yy4yxAHGV0b9Al+Lmu7wLHMkZ/ZMmK +FGIbljbviiD1Nz97Oh2cpB91YwOXOTN2vXHq26S+A5xe8z/QJbBsyghMur88CjdT +21H2qwMa+r5dCQwEhuGIiZ3KbzB/n4DTMYI5zy4IYPv0pjxShZn4aZTCCK2IUwID +AQABoxMwETAPBgNVHRMBAf8EBTADAQH/MA0GCSqGSIb3DQEBDQUAA4GBAPIWSkLX +QRMApOjjyC+tMxumT5e2pMqChHmxobQK4NMdrf2VCx+cRT6EmY8sK3/Xl/X8UBQ+ +9n5zXb1ZwhW/sTWgUvmOceJ4/XVs9FkdWOOn1J0XBch9ZIiFe/s5ASIgG7fUdcUF +9mAWS6FK2ca3xIh5kIupCXOFa0dPvlw/YUFT +-----END CERTIFICATE----- +""") + +intermediate_key_pem = b("""-----BEGIN RSA PRIVATE KEY----- +MIICWwIBAAKBgQDYcEQw5lfbEQRjr5Yy4yxAHGV0b9Al+Lmu7wLHMkZ/ZMmKFGIb +ljbviiD1Nz97Oh2cpB91YwOXOTN2vXHq26S+A5xe8z/QJbBsyghMur88CjdT21H2 +qwMa+r5dCQwEhuGIiZ3KbzB/n4DTMYI5zy4IYPv0pjxShZn4aZTCCK2IUwIDAQAB +AoGAfSZVV80pSeOKHTYfbGdNY/jHdU9eFUa/33YWriXU+77EhpIItJjkRRgivIfo +rhFJpBSGmDLblaqepm8emsXMeH4+2QzOYIf0QGGP6E6scjTt1PLqdqKfVJ1a2REN +147cujNcmFJb/5VQHHMpaPTgttEjlzuww4+BCDPsVRABWrkCQQD3loH36nLoQTtf ++kQq0T6Bs9/UWkTAGo0ND81ALj0F8Ie1oeZg6RNT96RxZ3aVuFTESTv6/TbjWywO +wdzlmV1vAkEA38rTJ6PTwaJlw5OttdDzAXGPB9tDmzh9oSi7cHwQQXizYd8MBYx4 +sjHUKD3dCQnb1dxJFhd3BT5HsnkRMbVZXQJAbXduH17ZTzcIOXc9jHDXYiFVZV5D +52vV0WCbLzVCZc3jMrtSUKa8lPN5EWrdU3UchWybyG0MR5mX8S5lrF4SoQJAIyUD +DBKaSqpqONCUUx1BTFS9FYrFjzbL4+c1qHCTTPTblt8kUCrDOZjBrKAqeiTmNSum +/qUot9YUBF8m6BuGsQJATHHmdFy/fG1VLkyBp49CAa8tN3Z5r/CgTznI4DfMTf4C +NbRHn2UmYlwQBa+L5lg9phewNe8aEwpPyPLoV85U8Q== +-----END RSA PRIVATE KEY----- +""") + server_cert_pem = b("""-----BEGIN CERTIFICATE----- MIICKDCCAZGgAwIBAgIJAJn/HpR21r/8MA0GCSqGSIb3DQEBBQUAMFgxCzAJBgNV BAYTAlVTMQswCQYDVQQIEwJJTDEQMA4GA1UEBxMHQ2hpY2FnbzEQMA4GA1UEChMH @@ -116,6 +152,40 @@ r50+LF74iLXFwqysVCebPKMOpDWp/qQ1BbJQIPs7/A== -----END RSA PRIVATE KEY----- """)) +intermediate_server_cert_pem = b("""-----BEGIN CERTIFICATE----- +MIICWDCCAcGgAwIBAgIRAPQFY9jfskSihdiNSNdt6GswDQYJKoZIhvcNAQENBQAw +ZjEVMBMGA1UEAxMMaW50ZXJtZWRpYXRlMQwwCgYDVQQKEwNvcmcxETAPBgNVBAsT +CG9yZy11bml0MQswCQYDVQQGEwJVUzELMAkGA1UECBMCQ0ExEjAQBgNVBAcTCVNh +biBEaWVnbzAeFw0xNDA4MjgwMjEwNDhaFw0yNDA4MjUwMjEwNDhaMG4xHTAbBgNV +BAMTFGludGVybWVkaWF0ZS1zZXJ2aWNlMQwwCgYDVQQKEwNvcmcxETAPBgNVBAsT +CG9yZy11bml0MQswCQYDVQQGEwJVUzELMAkGA1UECBMCQ0ExEjAQBgNVBAcTCVNh +biBEaWVnbzCBnzANBgkqhkiG9w0BAQEFAAOBjQAwgYkCgYEAqpJZygd+w1faLOr1 +iOAmbBhx5SZWcTCZ/ZjHQTJM7GuPT624QkqsixFghRKdDROwpwnAP7gMRukLqiy4 ++kRuGT5OfyGggL95i2xqA+zehjj08lSTlvGHpePJgCyTavIy5+Ljsj4DKnKyuhxm +biXTRrH83NDgixVkObTEmh/OVK0CAwEAATANBgkqhkiG9w0BAQ0FAAOBgQBa0Npw +UkzjaYEo1OUE1sTI6Mm4riTIHMak4/nswKh9hYup//WVOlr/RBSBtZ7Q/BwbjobN +3bfAtV7eSAqBsfxYXyof7G1ALANQERkq3+oyLP1iVt08W1WOUlIMPhdCF/QuCwy6 +x9MJLhUCGLJPM+O2rAPWVD9wCmvq10ALsiH3yA== +-----END CERTIFICATE----- +""") + +intermediate_server_key_pem = b("""-----BEGIN RSA PRIVATE KEY----- +MIICXAIBAAKBgQCqklnKB37DV9os6vWI4CZsGHHlJlZxMJn9mMdBMkzsa49PrbhC +SqyLEWCFEp0NE7CnCcA/uAxG6QuqLLj6RG4ZPk5/IaCAv3mLbGoD7N6GOPTyVJOW +8Yel48mALJNq8jLn4uOyPgMqcrK6HGZuJdNGsfzc0OCLFWQ5tMSaH85UrQIDAQAB +AoGAIQ594j5zna3/9WaPsTgnmhlesVctt4AAx/n827DA4ayyuHFlXUuVhtoWR5Pk +5ezj9mtYW8DyeCegABnsu2vZni/CdvU6uiS1Hv6qM1GyYDm9KWgovIP9rQCDSGaz +d57IWVGxx7ODFkm3gN5nxnSBOFVHytuW1J7FBRnEsehRroECQQDXHFOv82JuXDcz +z3+4c74IEURdOHcbycxlppmK9kFqm5lsUdydnnGW+mvwDk0APOB7Wg7vyFyr393e +dpmBDCzNAkEAyv6tVbTKUYhSjW+QhabJo896/EqQEYUmtMXxk4cQnKeR/Ao84Rkf +EqD5IykMUfUI0jJU4DGX+gWZ10a7kNbHYQJAVFCuHNFxS4Cpwo0aqtnzKoZaHY/8 +X9ABZfafSHCtw3Op92M+7ikkrOELXdS9KdKyyqbKJAKNEHF3LbOfB44WIQJAA2N4 +9UNNVUsXRbElEnYUS529CdUczo4QdVgQjkvk5RiPAUwSdBd9Q0xYnFOlFwEmIowg +ipWJWe0aAlP18ZcEQQJBAL+5lekZ/GUdQoZ4HAsN5a9syrzavJ9VvU1KOOPorPZK +nMRZbbQgP+aSB7yl6K0gaLaZ8XaK0pjxNBh6ASqg9f4= +-----END RSA PRIVATE KEY----- +""") + client_cert_pem = b("""-----BEGIN CERTIFICATE----- MIICJjCCAY+gAwIBAgIJAKxpFI5lODkjMA0GCSqGSIb3DQEBBQUAMFgxCzAJBgNV BAYTAlVTMQswCQYDVQQIEwJJTDEQMA4GA1UEBxMHQ2hpY2FnbzEQMA4GA1UEChMH @@ -3105,6 +3175,88 @@ class CRLTests(TestCase): self.assertRaises(Error, load_crl, FILETYPE_PEM, b"hello, world") +class VerifyCertTests(TestCase): + """ + Tests for :py:obj:`OpenSSL.crypto.verify_cert`. + """ + root_cert = load_certificate(FILETYPE_PEM, root_cert_pem) + intermediate_cert = load_certificate(FILETYPE_PEM, intermediate_cert_pem) + intermediate_server_cert = load_certificate(FILETYPE_PEM, intermediate_server_cert_pem) + + def test_valid(self): + """ + :py:obj:`verify_cert` does nothing when called with a certificate and + valid chain. + """ + store = X509Store() + store.add_cert(self.root_cert) + store.add_cert(self.intermediate_cert) + store_ctx = X509StoreContext(store, self.intermediate_server_cert) + self.assertEqual(verify_cert(store_ctx), None) + + def test_reuse(self): + """ + :py:obj:`verify_cert` can be called multiple times. + """ + store = X509Store() + store.add_cert(self.root_cert) + store.add_cert(self.intermediate_cert) + store_ctx = X509StoreContext(store, self.intermediate_server_cert) + self.assertEqual(verify_cert(store_ctx), None) + self.assertEqual(verify_cert(store_ctx), None) + + def test_trusted_self_signed(self): + """ + :py:obj:`verify_cert` does nothign when called with a self-signed + certificate and itself in the chain. + """ + store = X509Store() + store.add_cert(self.root_cert) + store_ctx = X509StoreContext(store, self.root_cert) + self.assertEqual(verify_cert(store_ctx), None) + + def test_untrusted_self_signed(self): + """ + :py:obj:`verify_cert` raises error when a self-signed certificate is + verified without itself in the chain. + """ + store = X509Store() + store_ctx = X509StoreContext(store, self.root_cert) + try: + verify_cert(store_ctx) + self.assertTrue(False) + except Error as e: + self.assertTrue('self signed certificate' in str(e)) + self.assertEqual(e.certificate.get_subject().CN, 'Testing Root CA') + + def test_invalid_chain_no_root(self): + """ + :py:obj:`verify_cert` raises error when a root certificate is missing + from the chain. + """ + store = X509Store() + store.add_cert(self.intermediate_cert) + store_ctx = X509StoreContext(store, self.intermediate_server_cert) + try: + verify_cert(store_ctx) + except Error as e: + self.assertTrue('unable to get issuer certificate' in str(e)) + self.assertEqual(e.certificate.get_subject().CN, 'intermediate') + + def test_invalid_chain_no_intermediate(self): + """ + :py:obj:`verify_cert` raises error when an intermediate certificate is + missing from the chain. + """ + store = X509Store() + store.add_cert(self.root_cert) + store_ctx = X509StoreContext(store, self.intermediate_server_cert) + try: + verify_cert(store_ctx) + except Error as e: + self.assertTrue('unable to get local issuer certificate' in str(e)) + self.assertEqual(e.certificate.get_subject().CN, 'intermediate-service') + class SignVerifyTests(TestCase): """ diff --git a/doc/api/crypto.rst b/doc/api/crypto.rst index b360e89..344fa40 100644 --- a/doc/api/crypto.rst +++ b/doc/api/crypto.rst @@ -42,7 +42,17 @@ .. py:data:: X509StoreType - A Python type object representing the X509Store object type. + See :py:class:`X509Store` + + +.. py:data X509Store + + A class representing the X.509 store. + + +.. py:data:: X509StoreContext + + A class representing the X.509 store context. .. py:data:: PKeyType @@ -230,6 +240,18 @@ .. versionadded:: 0.11 +.. py:function:: verify_cert(store_ctx) + + Verify a certificate in a context. + + A :py:class:`X509StoreContext` is used to verify a certificate in some + context in conjunction with :py:func:`verify_cert`. The information + encapsulated in this object includes, but is not limited to, a set of + trusted certificates, verification parameters and revoked certificates. + + .. versionadded:: 0.15 + + .. _openssl-x509: X509 objects -- cgit v1.2.1 From 41a4b727bd4240b2d9ff2be256704dc1f729b727 Mon Sep 17 00:00:00 2001 From: Jean-Paul Calderone Date: Sun, 18 Jan 2015 15:26:13 -0500 Subject: I think this word choice makes more sense. --- OpenSSL/crypto.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/OpenSSL/crypto.py b/OpenSSL/crypto.py index 6c07962..eeddbcb 100644 --- a/OpenSSL/crypto.py +++ b/OpenSSL/crypto.py @@ -39,7 +39,7 @@ def _exception_from_context_error(exception_type, store_ctx): exception. When a call to native OpenSSL X509_verify_cert fails, additonal information - about the failure can be contained from the store context. + about the failure can be obtained from the store context. """ errors = [ -- cgit v1.2.1 From 093d9fee1af2d0959271867dfa34cf123dad7401 Mon Sep 17 00:00:00 2001 From: Jean-Paul Calderone Date: Sun, 18 Jan 2015 15:26:20 -0500 Subject: wrap long line --- OpenSSL/crypto.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/OpenSSL/crypto.py b/OpenSSL/crypto.py index eeddbcb..0272dd6 100644 --- a/OpenSSL/crypto.py +++ b/OpenSSL/crypto.py @@ -45,7 +45,8 @@ def _exception_from_context_error(exception_type, store_ctx): errors = [ _lib.X509_STORE_CTX_get_error(store_ctx._store_ctx), _lib.X509_STORE_CTX_get_error_depth(store_ctx._store_ctx), - _native(_ffi.string(_lib.X509_verify_cert_error_string(_lib.X509_STORE_CTX_get_error(store_ctx._store_ctx)))), + _native(_ffi.string(_lib.X509_verify_cert_error_string( + _lib.X509_STORE_CTX_get_error(store_ctx._store_ctx)))), ] _x509 = _lib.X509_STORE_CTX_get_current_cert(store_ctx._store_ctx) if _x509 != _ffi.NULL: -- cgit v1.2.1 From c1a15a44e3fd64aa4a8608aab922f71aeb625e27 Mon Sep 17 00:00:00 2001 From: Jean-Paul Calderone Date: Sun, 18 Jan 2015 15:27:34 -0500 Subject: Consistent a/an for x509. --- OpenSSL/crypto.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/OpenSSL/crypto.py b/OpenSSL/crypto.py index 0272dd6..28a1589 100644 --- a/OpenSSL/crypto.py +++ b/OpenSSL/crypto.py @@ -1392,12 +1392,12 @@ class X509StoreContext(object): """ An X.509 store context. - A :py:class:`X509StoreContext` is used to verify a certificate in some + An :py:class:`X509StoreContext` is used to verify a certificate in some context in conjunction with :py:func:`verify_cert`. The information encapsulated in this object includes, but is not limited to, a set of trusted certificates, verification parameters and revoked certificates. - :param store: A :py:class:`X509Store` of trusted certificates. + :param store: An :py:class:`X509Store` of trusted certificates. :param cert: An :py:class:`X509` certificate to be validated during a subsequent call to :py:func:`verify_cert`. """ -- cgit v1.2.1 From 282d3c81ce78d05540bf18cfce105581ed890350 Mon Sep 17 00:00:00 2001 From: Jean-Paul Calderone Date: Sun, 18 Jan 2015 15:27:41 -0500 Subject: four space indent --- OpenSSL/crypto.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/OpenSSL/crypto.py b/OpenSSL/crypto.py index 28a1589..5a46983 100644 --- a/OpenSSL/crypto.py +++ b/OpenSSL/crypto.py @@ -1399,7 +1399,7 @@ class X509StoreContext(object): :param store: An :py:class:`X509Store` of trusted certificates. :param cert: An :py:class:`X509` certificate to be validated during a - subsequent call to :py:func:`verify_cert`. + subsequent call to :py:func:`verify_cert`. """ def __init__(self, store, cert): -- cgit v1.2.1 From b7b7fb9b223a039bef4013a60292cf4b6e583049 Mon Sep 17 00:00:00 2001 From: Jean-Paul Calderone Date: Sun, 18 Jan 2015 15:37:10 -0500 Subject: Whitespace and X509StoreContext documentation cleanups. --- OpenSSL/crypto.py | 23 ++++++++++++++++++----- 1 file changed, 18 insertions(+), 5 deletions(-) diff --git a/OpenSSL/crypto.py b/OpenSSL/crypto.py index 5a46983..fbccb6d 100644 --- a/OpenSSL/crypto.py +++ b/OpenSSL/crypto.py @@ -1397,16 +1397,27 @@ class X509StoreContext(object): encapsulated in this object includes, but is not limited to, a set of trusted certificates, verification parameters and revoked certificates. - :param store: An :py:class:`X509Store` of trusted certificates. - :param cert: An :py:class:`X509` certificate to be validated during a - subsequent call to :py:func:`verify_cert`. + :ivar _store_ctx: The underlying X509_STORE_CTX structure used by this + instance. It is dynamically allocated and automatically garbage + collected. + + :ivar _store: See the ``store`` `__init__`` parameter. + + :ivar _cert: See the ``certificate`` `__init__`` parameter. """ - def __init__(self, store, cert): + def __init__(self, store, certificate): + """ + :param X509Store store: The certificates which will be trusted for the + purposes of any verifications. + + :param X509 certificate: The certificate to be verified. + """ store_ctx = _lib.X509_STORE_CTX_new() self._store_ctx = _ffi.gc(store_ctx, _lib.X509_STORE_CTX_free) self._store = store - self._cert = cert + self._cert = certificate + def _init(self): """ @@ -1416,6 +1427,7 @@ class X509StoreContext(object): if ret <= 0: _raise_current_error() + def _cleanup(self): """ Internally cleans up the store context. @@ -1426,6 +1438,7 @@ class X509StoreContext(object): _lib.X509_STORE_CTX_cleanup(self._store_ctx) + def load_certificate(type, buffer): """ Load a certificate from a buffer -- cgit v1.2.1 From 517816ed103e428229508d5eb7ecc43c7a8556a7 Mon Sep 17 00:00:00 2001 From: Jean-Paul Calderone Date: Sun, 18 Jan 2015 15:39:26 -0500 Subject: Four space indentation and other whitespace fixes. --- OpenSSL/test/test_crypto.py | 27 +++++++++++++++++---------- 1 file changed, 17 insertions(+), 10 deletions(-) diff --git a/OpenSSL/test/test_crypto.py b/OpenSSL/test/test_crypto.py index 8c04938..51d44b2 100644 --- a/OpenSSL/test/test_crypto.py +++ b/OpenSSL/test/test_crypto.py @@ -3175,6 +3175,7 @@ class CRLTests(TestCase): self.assertRaises(Error, load_crl, FILETYPE_PEM, b"hello, world") + class VerifyCertTests(TestCase): """ Tests for :py:obj:`OpenSSL.crypto.verify_cert`. @@ -3194,6 +3195,7 @@ class VerifyCertTests(TestCase): store_ctx = X509StoreContext(store, self.intermediate_server_cert) self.assertEqual(verify_cert(store_ctx), None) + def test_reuse(self): """ :py:obj:`verify_cert` can be called multiple times. @@ -3205,6 +3207,7 @@ class VerifyCertTests(TestCase): self.assertEqual(verify_cert(store_ctx), None) self.assertEqual(verify_cert(store_ctx), None) + def test_trusted_self_signed(self): """ :py:obj:`verify_cert` does nothign when called with a self-signed @@ -3215,6 +3218,7 @@ class VerifyCertTests(TestCase): store_ctx = X509StoreContext(store, self.root_cert) self.assertEqual(verify_cert(store_ctx), None) + def test_untrusted_self_signed(self): """ :py:obj:`verify_cert` raises error when a self-signed certificate is @@ -3223,11 +3227,12 @@ class VerifyCertTests(TestCase): store = X509Store() store_ctx = X509StoreContext(store, self.root_cert) try: - verify_cert(store_ctx) - self.assertTrue(False) + verify_cert(store_ctx) + self.assertTrue(False) except Error as e: - self.assertTrue('self signed certificate' in str(e)) - self.assertEqual(e.certificate.get_subject().CN, 'Testing Root CA') + self.assertTrue('self signed certificate' in str(e)) + self.assertEqual(e.certificate.get_subject().CN, 'Testing Root CA') + def test_invalid_chain_no_root(self): """ @@ -3238,10 +3243,11 @@ class VerifyCertTests(TestCase): store.add_cert(self.intermediate_cert) store_ctx = X509StoreContext(store, self.intermediate_server_cert) try: - verify_cert(store_ctx) + verify_cert(store_ctx) except Error as e: - self.assertTrue('unable to get issuer certificate' in str(e)) - self.assertEqual(e.certificate.get_subject().CN, 'intermediate') + self.assertTrue('unable to get issuer certificate' in str(e)) + self.assertEqual(e.certificate.get_subject().CN, 'intermediate') + def test_invalid_chain_no_intermediate(self): """ @@ -3252,10 +3258,11 @@ class VerifyCertTests(TestCase): store.add_cert(self.root_cert) store_ctx = X509StoreContext(store, self.intermediate_server_cert) try: - verify_cert(store_ctx) + verify_cert(store_ctx) except Error as e: - self.assertTrue('unable to get local issuer certificate' in str(e)) - self.assertEqual(e.certificate.get_subject().CN, 'intermediate-service') + self.assertTrue('unable to get local issuer certificate' in str(e)) + self.assertEqual(e.certificate.get_subject().CN, 'intermediate-service') + class SignVerifyTests(TestCase): -- cgit v1.2.1 From 06e01b9d8f8e3ccbfd1820ff3f24fffe001e7b4f Mon Sep 17 00:00:00 2001 From: Jean-Paul Calderone Date: Sun, 18 Jan 2015 15:43:13 -0500 Subject: Small test method docstring fixes and fixes for tests of exceptional cases. --- OpenSSL/test/test_crypto.py | 34 ++++++++++++++-------------------- 1 file changed, 14 insertions(+), 20 deletions(-) diff --git a/OpenSSL/test/test_crypto.py b/OpenSSL/test/test_crypto.py index 51d44b2..c36a988 100644 --- a/OpenSSL/test/test_crypto.py +++ b/OpenSSL/test/test_crypto.py @@ -3186,8 +3186,8 @@ class VerifyCertTests(TestCase): def test_valid(self): """ - :py:obj:`verify_cert` does nothing when called with a certificate and - valid chain. + :py:obj:`verify_cert` returns ``None`` when called with a certificate + and valid chain. """ store = X509Store() store.add_cert(self.root_cert) @@ -3198,7 +3198,8 @@ class VerifyCertTests(TestCase): def test_reuse(self): """ - :py:obj:`verify_cert` can be called multiple times. + :py:obj:`verify_cert` can be called multiple times with the same + ``X509StoreContext`` instance to produce the same result. """ store = X509Store() store.add_cert(self.root_cert) @@ -3210,7 +3211,7 @@ class VerifyCertTests(TestCase): def test_trusted_self_signed(self): """ - :py:obj:`verify_cert` does nothign when called with a self-signed + :py:obj:`verify_cert` returns ``None`` when called with a self-signed certificate and itself in the chain. """ store = X509Store() @@ -3226,12 +3227,9 @@ class VerifyCertTests(TestCase): """ store = X509Store() store_ctx = X509StoreContext(store, self.root_cert) - try: - verify_cert(store_ctx) - self.assertTrue(False) - except Error as e: - self.assertTrue('self signed certificate' in str(e)) - self.assertEqual(e.certificate.get_subject().CN, 'Testing Root CA') + e = self.assertRaises(Error, verify_cert, store_ctx) + self.assertIn('self signed certificate', str(e)) + self.assertEqual(e.certificate.get_subject().CN, 'Testing Root CA') def test_invalid_chain_no_root(self): @@ -3242,11 +3240,9 @@ class VerifyCertTests(TestCase): store = X509Store() store.add_cert(self.intermediate_cert) store_ctx = X509StoreContext(store, self.intermediate_server_cert) - try: - verify_cert(store_ctx) - except Error as e: - self.assertTrue('unable to get issuer certificate' in str(e)) - self.assertEqual(e.certificate.get_subject().CN, 'intermediate') + e = self.assertRaises(Error, verify_cert, store_ctx) + self.assertIn('unable to get issuer certificate', str(e)) + self.assertEqual(e.certificate.get_subject().CN, 'intermediate') def test_invalid_chain_no_intermediate(self): @@ -3257,11 +3253,9 @@ class VerifyCertTests(TestCase): store = X509Store() store.add_cert(self.root_cert) store_ctx = X509StoreContext(store, self.intermediate_server_cert) - try: - verify_cert(store_ctx) - except Error as e: - self.assertTrue('unable to get local issuer certificate' in str(e)) - self.assertEqual(e.certificate.get_subject().CN, 'intermediate-service') + e = self.assertRaises(Error, verify_cert, store_ctx) + self.assertIn('unable to get local issuer certificate', str(e)) + self.assertEqual(e.certificate.get_subject().CN, 'intermediate-service') -- cgit v1.2.1 From 13a816887abfcf4e1b7ea4bf8478f625f428cb36 Mon Sep 17 00:00:00 2001 From: Jean-Paul Calderone Date: Sun, 18 Jan 2015 15:49:15 -0500 Subject: A little bit more explanation of what "context" means here. --- OpenSSL/crypto.py | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/OpenSSL/crypto.py b/OpenSSL/crypto.py index fbccb6d..b6951c0 100644 --- a/OpenSSL/crypto.py +++ b/OpenSSL/crypto.py @@ -1392,10 +1392,12 @@ class X509StoreContext(object): """ An X.509 store context. - An :py:class:`X509StoreContext` is used to verify a certificate in some - context in conjunction with :py:func:`verify_cert`. The information - encapsulated in this object includes, but is not limited to, a set of - trusted certificates, verification parameters and revoked certificates. + An :py:class:`X509StoreContext` is used to define some of the criteria for + certificate verification. The information encapsulated in this object + includes, but is not limited to, a set of trusted certificates, + verification parameters, and revoked certificates. + + Of these, only the set of trusted certificates is currently exposed. :ivar _store_ctx: The underlying X509_STORE_CTX structure used by this instance. It is dynamically allocated and automatically garbage -- cgit v1.2.1 From b4e40fb91c750419bcc29423d40072bb91257ecd Mon Sep 17 00:00:00 2001 From: Jean-Paul Calderone Date: Sun, 18 Jan 2015 16:28:44 -0500 Subject: remove unused import --- OpenSSL/test/test_crypto.py | 1 - 1 file changed, 1 deletion(-) diff --git a/OpenSSL/test/test_crypto.py b/OpenSSL/test/test_crypto.py index c36a988..82b3d7d 100644 --- a/OpenSSL/test/test_crypto.py +++ b/OpenSSL/test/test_crypto.py @@ -10,7 +10,6 @@ from unittest import main import base64 import os import re -import sys from subprocess import PIPE, Popen from datetime import datetime, timedelta -- cgit v1.2.1 From 4c10a1e73001851754be551f378bac657320698c Mon Sep 17 00:00:00 2001 From: Jean-Paul Calderone Date: Sun, 18 Jan 2015 16:31:27 -0500 Subject: Make a more specific assertion about where the failure reason shows up. --- OpenSSL/test/test_crypto.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/OpenSSL/test/test_crypto.py b/OpenSSL/test/test_crypto.py index 82b3d7d..fc6350c 100644 --- a/OpenSSL/test/test_crypto.py +++ b/OpenSSL/test/test_crypto.py @@ -3227,7 +3227,7 @@ class VerifyCertTests(TestCase): store = X509Store() store_ctx = X509StoreContext(store, self.root_cert) e = self.assertRaises(Error, verify_cert, store_ctx) - self.assertIn('self signed certificate', str(e)) + self.assertEqual(e.args[0][2], 'self signed certificate') self.assertEqual(e.certificate.get_subject().CN, 'Testing Root CA') @@ -3240,7 +3240,7 @@ class VerifyCertTests(TestCase): store.add_cert(self.intermediate_cert) store_ctx = X509StoreContext(store, self.intermediate_server_cert) e = self.assertRaises(Error, verify_cert, store_ctx) - self.assertIn('unable to get issuer certificate', str(e)) + self.assertEqual(e.args[0][2], 'unable to get issuer certificate') self.assertEqual(e.certificate.get_subject().CN, 'intermediate') @@ -3253,7 +3253,7 @@ class VerifyCertTests(TestCase): store.add_cert(self.root_cert) store_ctx = X509StoreContext(store, self.intermediate_server_cert) e = self.assertRaises(Error, verify_cert, store_ctx) - self.assertIn('unable to get local issuer certificate', str(e)) + self.assertEqual(e.args[0][2], 'unable to get local issuer certificate') self.assertEqual(e.certificate.get_subject().CN, 'intermediate-service') -- cgit v1.2.1 From bf1f81c666c2cde383a793de209b7b864fa58dd7 Mon Sep 17 00:00:00 2001 From: Jean-Paul Calderone Date: Thu, 11 Dec 2014 14:05:24 -0500 Subject: ChangeLog --- ChangeLog | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/ChangeLog b/ChangeLog index e5b2ac4..252f13c 100644 --- a/ChangeLog +++ b/ChangeLog @@ -1,13 +1,18 @@ -2014-12-11 Jean-Paul Calderone +2015-01-30 Stephen Holsapple - * OpenSSL/SSL.py: Fixed a regression ``Context.check_privatekey`` - causing it to always succeed - even if it should fail. + * OpenSSL/crypto.py: Expose ``X509StoreContext`` for verifying certificates. + * OpenSSL/test/test_crypto.py: Add intermediate certificates for 2015-01-08 Paul Aurich * OpenSSL/SSL.py: ``Connection.shutdown`` now propagates errors from the underlying socket. +2014-12-11 Jean-Paul Calderone + + * OpenSSL/SSL.py: Fixed a regression ``Context.check_privatekey`` + causing it to always succeed - even if it should fail. + 2014-08-21 Alex Gaynor * OpenSSL/crypto.py: Fixed a regression where calling ``load_pkcs7_data`` -- cgit v1.2.1 From 08ffaa641b8ac19ddaae6472688f5a65844c8473 Mon Sep 17 00:00:00 2001 From: Stephen Holsapple Date: Fri, 30 Jan 2015 17:18:40 -0800 Subject: Refactoring verify_cert Apply the changes that we've been talking about in https://github.com/pyca/pyopenssl/pull/155 regarding the placement of verify_cert, viz., moving verify_cert from top level of crypto into X509StoreContext. This makes the pyOpenSSL API slightly different than the OpenSSL API, but the plan will be to add back a verify_cert to the top level that is nice to use. --- OpenSSL/crypto.py | 91 ++++++++++++++++++++++++--------------------- OpenSSL/test/test_crypto.py | 34 ++++++++--------- doc/api/crypto.rst | 28 ++++++++------ 3 files changed, 82 insertions(+), 71 deletions(-) diff --git a/OpenSSL/crypto.py b/OpenSSL/crypto.py index b6951c0..395f273 100644 --- a/OpenSSL/crypto.py +++ b/OpenSSL/crypto.py @@ -32,35 +32,7 @@ class Error(Exception): """ - -def _exception_from_context_error(exception_type, store_ctx): - """ - Convert a :py:func:`OpenSSL.crypto.verify_cert` failure into a Python - exception. - - When a call to native OpenSSL X509_verify_cert fails, additonal information - about the failure can be obtained from the store context. - """ - - errors = [ - _lib.X509_STORE_CTX_get_error(store_ctx._store_ctx), - _lib.X509_STORE_CTX_get_error_depth(store_ctx._store_ctx), - _native(_ffi.string(_lib.X509_verify_cert_error_string( - _lib.X509_STORE_CTX_get_error(store_ctx._store_ctx)))), - ] - _x509 = _lib.X509_STORE_CTX_get_current_cert(store_ctx._store_ctx) - if _x509 != _ffi.NULL: - _cert = _lib.X509_dup(_x509) - pycert = X509.__new__(X509) - pycert._x509 = _ffi.gc(_cert, _lib.X509_free) - e = exception_type(errors) - e.certificate = pycert - raise e - - - _raise_current_error = partial(_exception_from_error_queue, Error) -_raise_context_error = partial(_exception_from_context_error, Error) @@ -1387,6 +1359,19 @@ class X509Store(object): X509StoreType = X509Store +class X509StoreContextError(Exception): + """ + An error occurred while verifying a certificate using + `OpenSSL.X509StoreContext.verify_certificate`. + + :param certificate: The certificate which caused verificate failure. + :type cert: :class:`X509` + + """ + def __init__(self, message, certificate): + super(X509StoreContextError, self).__init__(message) + self.certificate = certificate + class X509StoreContext(object): """ @@ -1440,6 +1425,42 @@ class X509StoreContext(object): _lib.X509_STORE_CTX_cleanup(self._store_ctx) + def _exception_from_context(self): + """ + Convert an OpenSSL native context error failure into a Python + exception. + + When a call to native OpenSSL X509_verify_cert fails, additonal information + about the failure can be obtained from the store context. + """ + errors = [ + _lib.X509_STORE_CTX_get_error(self._store_ctx), + _lib.X509_STORE_CTX_get_error_depth(self._store_ctx), + _native(_ffi.string(_lib.X509_verify_cert_error_string( + _lib.X509_STORE_CTX_get_error(self._store_ctx)))), + ] + _x509 = _lib.X509_STORE_CTX_get_current_cert(self._store_ctx) + if _x509 != _ffi.NULL: + _cert = _lib.X509_dup(_x509) + pycert = X509.__new__(X509) + pycert._x509 = _ffi.gc(_cert, _lib.X509_free) + return X509StoreContextError(errors, pycert) + + + def verify_certificate(self): + """ + Verify a certificate in a context. + + :param store_ctx: The :py:class:`X509StoreContext` to verify. + :raises: Error + """ + self._init() + ret = _lib.X509_verify_cert(self._store_ctx) + self._cleanup() + if ret <= 0: + raise self._exception_from_context() + + def load_certificate(type, buffer): """ @@ -2388,20 +2409,6 @@ def verify(cert, signature, data, digest): _raise_current_error() -def verify_cert(store_ctx): - """ - Verify a certificate in a context. - - :param store_ctx: The :py:class:`X509StoreContext` to verify. - :raises: Error - """ - store_ctx._init() - ret = _lib.X509_verify_cert(store_ctx._store_ctx) - store_ctx._cleanup() - if ret <= 0: - _raise_context_error(store_ctx) - - def load_crl(type, buffer): """ Load a certificate revocation list from a buffer diff --git a/OpenSSL/test/test_crypto.py b/OpenSSL/test/test_crypto.py index fc6350c..0aac1e5 100644 --- a/OpenSSL/test/test_crypto.py +++ b/OpenSSL/test/test_crypto.py @@ -17,7 +17,7 @@ from six import u, b, binary_type from OpenSSL.crypto import TYPE_RSA, TYPE_DSA, Error, PKey, PKeyType from OpenSSL.crypto import X509, X509Type, X509Name, X509NameType -from OpenSSL.crypto import X509Store, X509StoreType, X509StoreContext +from OpenSSL.crypto import X509Store, X509StoreType, X509StoreContext, X509StoreContextError from OpenSSL.crypto import X509Req, X509ReqType from OpenSSL.crypto import X509Extension, X509ExtensionType from OpenSSL.crypto import load_certificate, load_privatekey @@ -29,7 +29,7 @@ from OpenSSL.crypto import PKCS12, PKCS12Type, load_pkcs12 from OpenSSL.crypto import CRL, Revoked, load_crl from OpenSSL.crypto import NetscapeSPKI, NetscapeSPKIType from OpenSSL.crypto import ( - sign, verify, verify_cert, get_elliptic_curve, get_elliptic_curves) + sign, verify, get_elliptic_curve, get_elliptic_curves) from OpenSSL.test.util import EqualityTestsMixin, TestCase from OpenSSL._util import native, lib @@ -3175,9 +3175,9 @@ class CRLTests(TestCase): -class VerifyCertTests(TestCase): +class X509StoreContextTests(TestCase): """ - Tests for :py:obj:`OpenSSL.crypto.verify_cert`. + Tests for :py:obj:`OpenSSL.crypto.X509StoreContext`. """ root_cert = load_certificate(FILETYPE_PEM, root_cert_pem) intermediate_cert = load_certificate(FILETYPE_PEM, intermediate_cert_pem) @@ -3185,74 +3185,74 @@ class VerifyCertTests(TestCase): def test_valid(self): """ - :py:obj:`verify_cert` returns ``None`` when called with a certificate + :py:obj:`verify_certificate` returns ``None`` when called with a certificate and valid chain. """ store = X509Store() store.add_cert(self.root_cert) store.add_cert(self.intermediate_cert) store_ctx = X509StoreContext(store, self.intermediate_server_cert) - self.assertEqual(verify_cert(store_ctx), None) + self.assertEqual(store_ctx.verify_certificate(), None) def test_reuse(self): """ - :py:obj:`verify_cert` can be called multiple times with the same + :py:obj:`verify_certificate` can be called multiple times with the same ``X509StoreContext`` instance to produce the same result. """ store = X509Store() store.add_cert(self.root_cert) store.add_cert(self.intermediate_cert) store_ctx = X509StoreContext(store, self.intermediate_server_cert) - self.assertEqual(verify_cert(store_ctx), None) - self.assertEqual(verify_cert(store_ctx), None) + self.assertEqual(store_ctx.verify_certificate(), None) + self.assertEqual(store_ctx.verify_certificate(), None) def test_trusted_self_signed(self): """ - :py:obj:`verify_cert` returns ``None`` when called with a self-signed + :py:obj:`verify_certificate` returns ``None`` when called with a self-signed certificate and itself in the chain. """ store = X509Store() store.add_cert(self.root_cert) store_ctx = X509StoreContext(store, self.root_cert) - self.assertEqual(verify_cert(store_ctx), None) + self.assertEqual(store_ctx.verify_certificate(), None) def test_untrusted_self_signed(self): """ - :py:obj:`verify_cert` raises error when a self-signed certificate is + :py:obj:`verify_certificate` raises error when a self-signed certificate is verified without itself in the chain. """ store = X509Store() store_ctx = X509StoreContext(store, self.root_cert) - e = self.assertRaises(Error, verify_cert, store_ctx) + e = self.assertRaises(X509StoreContextError, store_ctx.verify_certificate) self.assertEqual(e.args[0][2], 'self signed certificate') self.assertEqual(e.certificate.get_subject().CN, 'Testing Root CA') def test_invalid_chain_no_root(self): """ - :py:obj:`verify_cert` raises error when a root certificate is missing + :py:obj:`verify_certificate` raises error when a root certificate is missing from the chain. """ store = X509Store() store.add_cert(self.intermediate_cert) store_ctx = X509StoreContext(store, self.intermediate_server_cert) - e = self.assertRaises(Error, verify_cert, store_ctx) + e = self.assertRaises(X509StoreContextError, store_ctx.verify_certificate) self.assertEqual(e.args[0][2], 'unable to get issuer certificate') self.assertEqual(e.certificate.get_subject().CN, 'intermediate') def test_invalid_chain_no_intermediate(self): """ - :py:obj:`verify_cert` raises error when an intermediate certificate is + :py:obj:`verify_certificate` raises error when an intermediate certificate is missing from the chain. """ store = X509Store() store.add_cert(self.root_cert) store_ctx = X509StoreContext(store, self.intermediate_server_cert) - e = self.assertRaises(Error, verify_cert, store_ctx) + e = self.assertRaises(X509StoreContextError, store_ctx.verify_certificate) self.assertEqual(e.args[0][2], 'unable to get local issuer certificate') self.assertEqual(e.certificate.get_subject().CN, 'intermediate-service') diff --git a/doc/api/crypto.rst b/doc/api/crypto.rst index 344fa40..5528870 100644 --- a/doc/api/crypto.rst +++ b/doc/api/crypto.rst @@ -240,18 +240,6 @@ .. versionadded:: 0.11 -.. py:function:: verify_cert(store_ctx) - - Verify a certificate in a context. - - A :py:class:`X509StoreContext` is used to verify a certificate in some - context in conjunction with :py:func:`verify_cert`. The information - encapsulated in this object includes, but is not limited to, a set of - trusted certificates, verification parameters and revoked certificates. - - .. versionadded:: 0.15 - - .. _openssl-x509: X509 objects @@ -548,6 +536,22 @@ The X509Store object has currently just one method: Add the certificate *cert* to the certificate store. +X509StoreContext objects +------------------------ + +The X509StoreContext object is used for verifying a certificate against a set +of trusted certificates. + + +.. py:method:: X509StoreContext.verify_certificate() + + Verify a certificate in the context of this initialized `X509StoreContext`. + On error, raises `X509StoreContextError`, otherwise does nothing. + + .. versionadded:: 0.15 + + + .. _openssl-pkey: PKey objects -- cgit v1.2.1 From 1f713eb84f76b55cfb7b1c21b23388eca9753ec1 Mon Sep 17 00:00:00 2001 From: Stephen Holsapple Date: Mon, 9 Feb 2015 19:19:44 -0800 Subject: Trust return value in context error The function X509_STORE_CTX_get_current_cert seems to always return a certificate. After reviewing upstream OpenSSL package, it seems they do no error checking on this function either, so I think this approach should be safe. Worst case scenario, for a case I think is impossible, we'll get a AttributeError or TypeError. --- OpenSSL/crypto.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/OpenSSL/crypto.py b/OpenSSL/crypto.py index 395f273..c3d4c9b 100644 --- a/OpenSSL/crypto.py +++ b/OpenSSL/crypto.py @@ -1439,11 +1439,12 @@ class X509StoreContext(object): _native(_ffi.string(_lib.X509_verify_cert_error_string( _lib.X509_STORE_CTX_get_error(self._store_ctx)))), ] + # A context error should always be associated with a certificate, so we + # expect this call to never return :class:`None`. _x509 = _lib.X509_STORE_CTX_get_current_cert(self._store_ctx) - if _x509 != _ffi.NULL: - _cert = _lib.X509_dup(_x509) - pycert = X509.__new__(X509) - pycert._x509 = _ffi.gc(_cert, _lib.X509_free) + _cert = _lib.X509_dup(_x509) + pycert = X509.__new__(X509) + pycert._x509 = _ffi.gc(_cert, _lib.X509_free) return X509StoreContextError(errors, pycert) -- cgit v1.2.1 From 95a46658350b229cfaa10d5a376942d4832d5032 Mon Sep 17 00:00:00 2001 From: Stephen Holsapple Date: Mon, 9 Feb 2015 19:34:25 -0800 Subject: Add documentation for X509StoreContextError I'm not sure if it is customary to provide documentation for exceptions or not, but because this exception provides an additional meaningful attribute on the exception base class, I figured it would be helpful to document. --- doc/api/crypto.rst | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/doc/api/crypto.rst b/doc/api/crypto.rst index 5528870..3955802 100644 --- a/doc/api/crypto.rst +++ b/doc/api/crypto.rst @@ -536,6 +536,18 @@ The X509Store object has currently just one method: Add the certificate *cert* to the certificate store. +X509StoreContextError objects +----------------------------- + +The X509StoreContextError is an exception raised from +`X509StoreContext.verify_certificate` in circumstances where a certificate +cannot be verified in a provided context. + +The exception objects have a :py:class:`basestring` ``message`` attribute which +contains error messages and :py:class:`X509` ``certificate`` attribute by which +they indicate where the verification error was detected. + + X509StoreContext objects ------------------------ -- cgit v1.2.1 From 46a092529bdd38313c64bd01d6866ef64477ec81 Mon Sep 17 00:00:00 2001 From: Stephen Holsapple Date: Thu, 12 Feb 2015 14:45:43 -0800 Subject: Initialize a context at instantiation time To maintain a Pythonic API, we need to initialize the store context object at object instantiation time so that it is possible to modify the trust store (a legitable use case) after the object is created. As the store context implementation becomes more featureful, this will become more important. E.g., when we add support for `X509_STORE_CTX_get0_param` and X509_STORE_CTX_set0_param` to change verification parameters, we'll want to do this. This change also adds a very simple `set_store` method mostly to make the initialization and modification changes easier to test. --- OpenSSL/crypto.py | 18 +++++++++++++++++- OpenSSL/test/test_crypto.py | 18 ++++++++++++++++++ 2 files changed, 35 insertions(+), 1 deletion(-) diff --git a/OpenSSL/crypto.py b/OpenSSL/crypto.py index c3d4c9b..5093745 100644 --- a/OpenSSL/crypto.py +++ b/OpenSSL/crypto.py @@ -1404,6 +1404,10 @@ class X509StoreContext(object): self._store_ctx = _ffi.gc(store_ctx, _lib.X509_STORE_CTX_free) self._store = store self._cert = certificate + # Make the store context available for use after instantiating this + # class by initializing it now. Per testing, subsequent calls to + # :py:meth:`_init` have no adverse affect. + self._init() def _init(self): @@ -1420,7 +1424,7 @@ class X509StoreContext(object): Internally cleans up the store context. The store context can then be reused with a new call to - :py:meth:`init`. + :py:meth:`_init`. """ _lib.X509_STORE_CTX_cleanup(self._store_ctx) @@ -1448,6 +1452,16 @@ class X509StoreContext(object): return X509StoreContextError(errors, pycert) + def set_store(self, store): + """ + Set the context's trust store. + + :param X509Store store: The certificates which will be trusted for the + purposes of any *future* verifications. + """ + self._store = store + + def verify_certificate(self): """ Verify a certificate in a context. @@ -1455,6 +1469,8 @@ class X509StoreContext(object): :param store_ctx: The :py:class:`X509StoreContext` to verify. :raises: Error """ + # Always re-initialize the store context in case + # :py:meth:`verify_certificate` is called multiple times. self._init() ret = _lib.X509_verify_cert(self._store_ctx) self._cleanup() diff --git a/OpenSSL/test/test_crypto.py b/OpenSSL/test/test_crypto.py index 0aac1e5..ca54176 100644 --- a/OpenSSL/test/test_crypto.py +++ b/OpenSSL/test/test_crypto.py @@ -3257,6 +3257,24 @@ class X509StoreContextTests(TestCase): self.assertEqual(e.certificate.get_subject().CN, 'intermediate-service') + def test_modification_pre_verify(self): + """ + :py:obj:`verify_certificate` can use a store context modified after + instantiation. + """ + store_bad = X509Store() + store_bad.add_cert(self.intermediate_cert) + store_good = X509Store() + store_good.add_cert(self.root_cert) + store_good.add_cert(self.intermediate_cert) + store_ctx = X509StoreContext(store_bad, self.intermediate_server_cert) + e = self.assertRaises(X509StoreContextError, store_ctx.verify_certificate) + self.assertEqual(e.args[0][2], 'unable to get issuer certificate') + self.assertEqual(e.certificate.get_subject().CN, 'intermediate') + store_ctx.set_store(store_good) + self.assertEqual(store_ctx.verify_certificate(), None) + + class SignVerifyTests(TestCase): """ -- cgit v1.2.1 From 130cd0ea0d9db22fbe44d830730bb00dc37d6185 Mon Sep 17 00:00:00 2001 From: Jean-Paul Calderone Date: Sun, 15 Mar 2015 15:49:33 -0400 Subject: Give this new top-level function a docstring. --- OpenSSL/_util.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/OpenSSL/_util.py b/OpenSSL/_util.py index cf13666..de292be 100644 --- a/OpenSSL/_util.py +++ b/OpenSSL/_util.py @@ -8,6 +8,13 @@ lib = binding.lib def text(charp): + """ + Get a native string type representing of the given CFFI ``char*`` object. + + :param charp: A C-style string represented using CFFI. + + :return: :class:`str` + """ return native(ffi.string(charp)) -- cgit v1.2.1 From feb1743114f6ef84680e28421f1a404dbc586403 Mon Sep 17 00:00:00 2001 From: Jean-Paul Calderone Date: Sun, 15 Mar 2015 15:49:45 -0400 Subject: Tweaks to the X509StoreContextError docstring. --- OpenSSL/crypto.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/OpenSSL/crypto.py b/OpenSSL/crypto.py index 5093745..2e58f82 100644 --- a/OpenSSL/crypto.py +++ b/OpenSSL/crypto.py @@ -1362,9 +1362,9 @@ X509StoreType = X509Store class X509StoreContextError(Exception): """ An error occurred while verifying a certificate using - `OpenSSL.X509StoreContext.verify_certificate`. + `OpenSSL.X509StoreContext.verify_certificate`. - :param certificate: The certificate which caused verificate failure. + :ivar certificate: The certificate which caused verificate failure. :type cert: :class:`X509` """ -- cgit v1.2.1 From 64b6b84b525d4b6c4af31066bb71ffe3a5cce2f0 Mon Sep 17 00:00:00 2001 From: Jean-Paul Calderone Date: Sun, 15 Mar 2015 16:08:02 -0400 Subject: Fix trivial rst syntax errors. --- OpenSSL/crypto.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/OpenSSL/crypto.py b/OpenSSL/crypto.py index 2e58f82..0e99576 100644 --- a/OpenSSL/crypto.py +++ b/OpenSSL/crypto.py @@ -1388,9 +1388,9 @@ class X509StoreContext(object): instance. It is dynamically allocated and automatically garbage collected. - :ivar _store: See the ``store`` `__init__`` parameter. + :ivar _store: See the ``store`` ``__init__`` parameter. - :ivar _cert: See the ``certificate`` `__init__`` parameter. + :ivar _cert: See the ``certificate`` ``__init__`` parameter. """ def __init__(self, store, certificate): -- cgit v1.2.1 From 876b2ac5929fd9edcf7c03d1f28ee5334bce60ea Mon Sep 17 00:00:00 2001 From: Jean-Paul Calderone Date: Sun, 15 Mar 2015 16:17:19 -0400 Subject: The message is not a basestring and basestring is an awful type anyway. --- doc/api/crypto.rst | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/doc/api/crypto.rst b/doc/api/crypto.rst index 3955802..f378e84 100644 --- a/doc/api/crypto.rst +++ b/doc/api/crypto.rst @@ -543,9 +543,11 @@ The X509StoreContextError is an exception raised from `X509StoreContext.verify_certificate` in circumstances where a certificate cannot be verified in a provided context. -The exception objects have a :py:class:`basestring` ``message`` attribute which -contains error messages and :py:class:`X509` ``certificate`` attribute by which -they indicate where the verification error was detected. +The certificate for which the verification error was detected is given by the +``certificate`` attribute of the exception instance as a :class:`X509` +instance. + +Details about the verification error are given in the exception's ``args`` attribute. X509StoreContext objects -- cgit v1.2.1