summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorJean-Paul Calderone <jean-paul@clusterhq.com>2015-04-13 22:34:17 -0400
committerJean-Paul Calderone <jean-paul@clusterhq.com>2015-04-13 22:34:17 -0400
commit773b063c7290d5c4e700b7a3ba2c4fed97551f8d (patch)
tree5a50fecdfb6de9be7026ff8661cb1acda5fcaade
parent5c3b748846ad1f9597d51b24d04ac394980c2480 (diff)
parente011b9577fb390d59689f0c4e8430dd5070e5bb7 (diff)
downloadpyopenssl-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.py7
-rw-r--r--OpenSSL/_util.py5
-rw-r--r--OpenSSL/crypto.py31
-rw-r--r--OpenSSL/test/test_crypto.py112
-rw-r--r--doc/api/crypto.rst3
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()