Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 3 additions & 4 deletions OpenSSL/SSL.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,13 +16,12 @@
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:
Expand Down Expand Up @@ -629,7 +628,7 @@ def _raise_passphrase_exception(self):
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

Expand All @@ -640,7 +639,7 @@ def use_privatekey_file(self, keyfile, filetype=_unspecified):
"""
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")
Expand Down
5 changes: 5 additions & 0 deletions OpenSSL/_util.py
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,11 @@ def byte_string(s):
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"
)
Expand Down
31 changes: 25 additions & 6 deletions OpenSSL/crypto.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
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,
Expand All @@ -14,6 +15,7 @@
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,
)

Expand Down Expand Up @@ -1831,7 +1833,8 @@ def add_revoked(self, revoked):
_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

Expand All @@ -1841,12 +1844,15 @@ def export(self, cert, key, type=FILETYPE_PEM, days=100):
: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")
Expand All @@ -1855,6 +1861,19 @@ def export(self, cert, key, type=FILETYPE_PEM, days=100):
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.
Expand All @@ -1874,7 +1893,7 @@ def export(self, cert, key, type=FILETYPE_PEM, days=100):

_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()

Expand Down
112 changes: 104 additions & 8 deletions OpenSSL/test/test_crypto.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
"""

from unittest import main
from warnings import catch_warnings, simplefilter

import base64
import os
Expand Down Expand Up @@ -3043,11 +3044,9 @@ def test_construction_wrong_args(self):
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()
Expand All @@ -3056,26 +3055,110 @@ def test_export(self):
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`
Expand Down Expand Up @@ -3106,7 +3189,7 @@ def test_export_wrong_args(self):
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)
Expand All @@ -3124,6 +3207,19 @@ def test_export_unknown_filetype(self):
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.
Expand Down
3 changes: 2 additions & 1 deletion doc/api/crypto.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down