diff options
author | Jean-Paul Calderone <jean-paul@clusterhq.com> | 2015-04-13 22:34:17 -0400 |
---|---|---|
committer | Jean-Paul Calderone <jean-paul@clusterhq.com> | 2015-04-13 22:34:17 -0400 |
commit | 773b063c7290d5c4e700b7a3ba2c4fed97551f8d (patch) | |
tree | 5a50fecdfb6de9be7026ff8661cb1acda5fcaade | |
parent | 5c3b748846ad1f9597d51b24d04ac394980c2480 (diff) | |
parent | e011b9577fb390d59689f0c4e8430dd5070e5bb7 (diff) | |
download | pyopenssl-773b063c7290d5c4e700b7a3ba2c4fed97551f8d.tar.gz |
Merge pull request #212 from exarkun/159-crl-export-digest
Add a parameter to control the message digest used in CRL export.
-rw-r--r-- | OpenSSL/SSL.py | 7 | ||||
-rw-r--r-- | OpenSSL/_util.py | 5 | ||||
-rw-r--r-- | OpenSSL/crypto.py | 31 | ||||
-rw-r--r-- | OpenSSL/test/test_crypto.py | 112 | ||||
-rw-r--r-- | doc/api/crypto.rst | 3 |
5 files changed, 139 insertions, 19 deletions
diff --git a/OpenSSL/SSL.py b/OpenSSL/SSL.py index 39a2bca..cfe705a 100644 --- a/OpenSSL/SSL.py +++ b/OpenSSL/SSL.py @@ -16,13 +16,12 @@ from OpenSSL._util import ( native as _native, text_to_bytes_and_warn as _text_to_bytes_and_warn, path_string as _path_string, + UNSPECIFIED as _UNSPECIFIED, ) from OpenSSL.crypto import ( FILETYPE_PEM, _PassphraseHelper, PKey, X509Name, X509, X509Store) -_unspecified = object() - try: _memoryview = memoryview except NameError: @@ -629,7 +628,7 @@ class Context(object): raise exception - def use_privatekey_file(self, keyfile, filetype=_unspecified): + def use_privatekey_file(self, keyfile, filetype=_UNSPECIFIED): """ Load a private key from a file @@ -640,7 +639,7 @@ class Context(object): """ keyfile = _path_string(keyfile) - if filetype is _unspecified: + if filetype is _UNSPECIFIED: filetype = FILETYPE_PEM elif not isinstance(filetype, integer_types): raise TypeError("filetype must be an integer") diff --git a/OpenSSL/_util.py b/OpenSSL/_util.py index 8d59252..0cc34d8 100644 --- a/OpenSSL/_util.py +++ b/OpenSSL/_util.py @@ -95,6 +95,11 @@ else: def byte_string(s): return s + +# A marker object to observe whether some optional arguments are passed any +# value or not. +UNSPECIFIED = object() + _TEXT_WARNING = ( text_type.__name__ + " for {0} is no longer accepted, use bytes" ) diff --git a/OpenSSL/crypto.py b/OpenSSL/crypto.py index f9e189c..50ff74f 100644 --- a/OpenSSL/crypto.py +++ b/OpenSSL/crypto.py @@ -2,6 +2,7 @@ from time import time from base64 import b16encode from functools import partial from operator import __eq__, __ne__, __lt__, __le__, __gt__, __ge__ +from warnings import warn as _warn from six import ( integer_types as _integer_types, @@ -14,6 +15,7 @@ from OpenSSL._util import ( exception_from_error_queue as _exception_from_error_queue, byte_string as _byte_string, native as _native, + UNSPECIFIED as _UNSPECIFIED, text_to_bytes_and_warn as _text_to_bytes_and_warn, ) @@ -1831,7 +1833,8 @@ class CRL(object): _raise_current_error() - def export(self, cert, key, type=FILETYPE_PEM, days=100): + def export(self, cert, key, type=FILETYPE_PEM, days=100, + digest=_UNSPECIFIED): """ export a CRL as a string @@ -1841,12 +1844,15 @@ class CRL(object): :param key: Used to sign CRL. :type key: :class:`PKey` - :param type: The export format, either :py:data:`FILETYPE_PEM`, :py:data:`FILETYPE_ASN1`, or :py:data:`FILETYPE_TEXT`. + :param type: The export format, either :py:data:`FILETYPE_PEM`, + :py:data:`FILETYPE_ASN1`, or :py:data:`FILETYPE_TEXT`. - :param days: The number of days until the next update of this CRL. - :type days: :py:data:`int` + :param int days: The number of days until the next update of this CRL. - :return: :py:data:`str` + :param bytes digest: The name of the message digest to use (eg + ``b"sha1"``). + + :return: :py:data:`bytes` """ if not isinstance(cert, X509): raise TypeError("cert must be an X509 instance") @@ -1855,6 +1861,19 @@ class CRL(object): if not isinstance(type, int): raise TypeError("type must be an integer") + if digest is _UNSPECIFIED: + _warn( + "The default message digest (md5) is deprecated. " + "Pass the name of a message digest explicitly.", + category=DeprecationWarning, + stacklevel=2, + ) + digest = b"md5" + + digest_obj = _lib.EVP_get_digestbyname(digest) + if digest_obj == _ffi.NULL: + raise ValueError("No such digest method") + bio = _lib.BIO_new(_lib.BIO_s_mem()) if bio == _ffi.NULL: # TODO: This is untested. @@ -1874,7 +1893,7 @@ class CRL(object): _lib.X509_CRL_set_issuer_name(self._crl, _lib.X509_get_subject_name(cert._x509)) - sign_result = _lib.X509_CRL_sign(self._crl, key._pkey, _lib.EVP_md5()) + sign_result = _lib.X509_CRL_sign(self._crl, key._pkey, digest_obj) if not sign_result: _raise_current_error() diff --git a/OpenSSL/test/test_crypto.py b/OpenSSL/test/test_crypto.py index dea5858..f6f0751 100644 --- a/OpenSSL/test/test_crypto.py +++ b/OpenSSL/test/test_crypto.py @@ -6,6 +6,7 @@ Unit tests for :py:mod:`OpenSSL.crypto`. """ from unittest import main +from warnings import catch_warnings, simplefilter import base64 import os @@ -3043,11 +3044,9 @@ class CRLTests(TestCase): self.assertRaises(TypeError, CRL, None) - def test_export(self): + def _get_crl(self): """ - Use python to create a simple CRL with a revocation, and export - the CRL in formats of PEM, DER and text. Those outputs are verified - with the openssl program. + Get a new ``CRL`` with a revocation. """ crl = CRL() revoked = Revoked() @@ -3056,26 +3055,110 @@ class CRLTests(TestCase): revoked.set_serial(b('3ab')) revoked.set_reason(b('sUpErSeDEd')) crl.add_revoked(revoked) + return crl + + def test_export_pem(self): + """ + If not passed a format, ``CRL.export`` returns a "PEM" format string + representing a serial number, a revoked reason, and certificate issuer + information. + """ + crl = self._get_crl() # PEM format dumped_crl = crl.export(self.cert, self.pkey, days=20) text = _runopenssl(dumped_crl, b"crl", b"-noout", b"-text") + + # These magic values are based on the way the CRL above was constructed + # and with what certificate it was exported. text.index(b('Serial Number: 03AB')) text.index(b('Superseded')) - text.index(b('Issuer: /C=US/ST=IL/L=Chicago/O=Testing/CN=Testing Root CA')) + text.index( + b('Issuer: /C=US/ST=IL/L=Chicago/O=Testing/CN=Testing Root CA') + ) + + + def test_export_der(self): + """ + If passed ``FILETYPE_ASN1`` for the format, ``CRL.export`` returns a + "DER" format string representing a serial number, a revoked reason, and + certificate issuer information. + """ + crl = self._get_crl() # DER format dumped_crl = crl.export(self.cert, self.pkey, FILETYPE_ASN1) - text = _runopenssl(dumped_crl, b"crl", b"-noout", b"-text", b"-inform", b"DER") + text = _runopenssl( + dumped_crl, b"crl", b"-noout", b"-text", b"-inform", b"DER" + ) text.index(b('Serial Number: 03AB')) text.index(b('Superseded')) - text.index(b('Issuer: /C=US/ST=IL/L=Chicago/O=Testing/CN=Testing Root CA')) + text.index( + b('Issuer: /C=US/ST=IL/L=Chicago/O=Testing/CN=Testing Root CA') + ) + + + def test_export_text(self): + """ + If passed ``FILETYPE_TEXT`` for the format, ``CRL.export`` returns a + text format string like the one produced by the openssl command line + tool. + """ + crl = self._get_crl() + + dumped_crl = crl.export(self.cert, self.pkey, FILETYPE_ASN1) + text = _runopenssl( + dumped_crl, b"crl", b"-noout", b"-text", b"-inform", b"DER" + ) # text format dumped_text = crl.export(self.cert, self.pkey, type=FILETYPE_TEXT) self.assertEqual(text, dumped_text) + def test_export_custom_digest(self): + """ + If passed the name of a digest function, ``CRL.export`` uses a + signature algorithm based on that digest function. + """ + crl = self._get_crl() + dumped_crl = crl.export(self.cert, self.pkey, digest=b"sha1") + text = _runopenssl(dumped_crl, b"crl", b"-noout", b"-text") + text.index(b('Signature Algorithm: sha1')) + + + def test_export_md5_digest(self): + """ + If passed md5 as the digest function, ``CRL.export`` uses md5 and does + not emit a deprecation warning. + """ + crl = self._get_crl() + with catch_warnings(record=True) as catcher: + simplefilter("always") + self.assertEqual(0, len(catcher)) + dumped_crl = crl.export(self.cert, self.pkey, digest=b"md5") + text = _runopenssl(dumped_crl, b"crl", b"-noout", b"-text") + text.index(b('Signature Algorithm: md5')) + + + def test_export_default_digest(self): + """ + If not passed the name of a digest function, ``CRL.export`` uses a + signature algorithm based on MD5 and emits a deprecation warning. + """ + crl = self._get_crl() + with catch_warnings(record=True) as catcher: + simplefilter("always") + dumped_crl = crl.export(self.cert, self.pkey) + self.assertEqual( + "The default message digest (md5) is deprecated. " + "Pass the name of a message digest explicitly.", + str(catcher[0].message), + ) + text = _runopenssl(dumped_crl, b"crl", b"-noout", b"-text") + text.index(b('Signature Algorithm: md5')) + + def test_export_invalid(self): """ If :py:obj:`CRL.export` is used with an uninitialized :py:obj:`X509` @@ -3106,7 +3189,7 @@ class CRLTests(TestCase): crl = CRL() self.assertRaises(TypeError, crl.export) self.assertRaises(TypeError, crl.export, self.cert) - self.assertRaises(TypeError, crl.export, self.cert, self.pkey, FILETYPE_PEM, 10, "foo") + self.assertRaises(TypeError, crl.export, self.cert, self.pkey, FILETYPE_PEM, 10, "md5", "foo") self.assertRaises(TypeError, crl.export, None, self.pkey, FILETYPE_PEM, 10) self.assertRaises(TypeError, crl.export, self.cert, None, FILETYPE_PEM, 10) @@ -3124,6 +3207,19 @@ class CRLTests(TestCase): self.assertRaises(ValueError, crl.export, self.cert, self.pkey, 100, 10) + def test_export_unknown_digest(self): + """ + Calling :py:obj:`OpenSSL.CRL.export` with a unsupported digest results + in a :py:obj:`ValueError` being raised. + """ + crl = CRL() + self.assertRaises( + ValueError, + crl.export, + self.cert, self.pkey, FILETYPE_PEM, 10, b"strange-digest" + ) + + def test_get_revoked(self): """ Use python to create a simple CRL with two revocations. diff --git a/doc/api/crypto.rst b/doc/api/crypto.rst index ee261c5..57a60f3 100644 --- a/doc/api/crypto.rst +++ b/doc/api/crypto.rst @@ -764,10 +764,11 @@ CRL objects have the following methods: Add a Revoked object to the CRL, by value not reference. -.. py:method:: CRL.export(cert, key[, type=FILETYPE_PEM][, days=100]) +.. py:method:: CRL.export(cert, key[, type=FILETYPE_PEM][, days=100][, digest=b'md5']) Use *cert* and *key* to sign the CRL and return the CRL as a string. *days* is the number of days before the next CRL is due. + *digest* is the algorithm that will be used to sign CRL. .. py:method:: CRL.get_revoked() |