Skip to content

Commit

Permalink
use cryptography instead of pyOpenSSL
Browse files Browse the repository at this point in the history
pyOpenSSL is only a wrapper around cryptography now. It recommends
using cryptography directly for our use case.
  • Loading branch information
devkral authored and davidism committed Jul 12, 2019
1 parent 08d4062 commit 84c98f2
Show file tree
Hide file tree
Showing 5 changed files with 66 additions and 53 deletions.
2 changes: 2 additions & 0 deletions CHANGES.rst
Expand Up @@ -36,6 +36,8 @@ Unreleased
``samesite``. :issue:`1549`
- Optional request log highlighting with the development server is
handled by Click instead of termcolor. :issue:`1235`
- Optional ad-hoc TLS support for the development server is handled
by cryptography instead of pyOpenSSL. :pr:`1555`


Version 0.15.5
Expand Down
2 changes: 1 addition & 1 deletion docs/serving.rst
Expand Up @@ -229,7 +229,7 @@ certificate each time the server is reloaded. Adhoc certificates are
discouraged because modern browsers do a bad job at supporting them for
security reasons.

This feature requires the pyOpenSSL library to be installed.
This feature requires the cryptography library to be installed.


Unix Sockets
Expand Down
101 changes: 55 additions & 46 deletions src/werkzeug/serving.py
Expand Up @@ -40,6 +40,8 @@
import signal
import socket
import sys
from datetime import datetime as dt
from datetime import timedelta

import werkzeug
from ._compat import PY2
Expand Down Expand Up @@ -77,15 +79,6 @@ def __getattr__(self, name):
click = None


def _get_openssl_crypto_module():
try:
from OpenSSL import crypto
except ImportError:
raise TypeError("Using ad-hoc certificates requires the pyOpenSSL library.")
else:
return crypto


ThreadingMixIn = socketserver.ThreadingMixIn
can_fork = hasattr(os, "fork")

Expand Down Expand Up @@ -481,32 +474,39 @@ def get_header_items(self):


def generate_adhoc_ssl_pair(cn=None):
from random import random

crypto = _get_openssl_crypto_module()
try:
from cryptography import x509
from cryptography.x509.oid import NameOID
from cryptography.hazmat.backends import default_backend
from cryptography.hazmat.primitives import hashes
from cryptography.hazmat.primitives.asymmetric import rsa
except ImportError:
raise TypeError("Using ad-hoc certificates requires the cryptography library.")
pkey = rsa.generate_private_key(
public_exponent=65537, key_size=2048, backend=default_backend()
)

# pretty damn sure that this is not actually accepted by anyone
if cn is None:
cn = "*"

cert = crypto.X509()
cert.set_serial_number(int(random() * sys.maxsize))
cert.gmtime_adj_notBefore(0)
cert.gmtime_adj_notAfter(60 * 60 * 24 * 365)
cn = u"*"

subject = cert.get_subject()
subject.CN = cn
subject.O = "Dummy Certificate" # noqa: E741

issuer = cert.get_issuer()
issuer.CN = subject.CN
issuer.O = subject.O # noqa: E741

pkey = crypto.PKey()
pkey.generate_key(crypto.TYPE_RSA, 2048)
cert.set_pubkey(pkey)
cert.sign(pkey, "sha256")
subject = x509.Name(
[
x509.NameAttribute(NameOID.ORGANIZATION_NAME, u"Dummy Certificate"),
x509.NameAttribute(NameOID.COMMON_NAME, cn),
]
)

cert = (
x509.CertificateBuilder()
.subject_name(subject)
.issuer_name(subject)
.public_key(pkey.public_key())
.serial_number(x509.random_serial_number())
.not_valid_before(dt.utcnow())
.not_valid_after(dt.utcnow() + timedelta(days=365))
.sign(pkey, hashes.SHA256(), default_backend())
)
return cert, pkey


Expand All @@ -528,37 +528,54 @@ def make_ssl_devcert(base_path, host=None, cn=None):
for the `cn`.
:param cn: the `CN` to use.
"""
from OpenSSL import crypto

if host is not None:
cn = "*.%s/CN=%s" % (host, host)
cn = u"*.%s/CN=%s" % (host, host)
cert, pkey = generate_adhoc_ssl_pair(cn=cn)

from cryptography.hazmat.primitives import serialization

cert_file = base_path + ".crt"
pkey_file = base_path + ".key"

with open(cert_file, "wb") as f:
f.write(crypto.dump_certificate(crypto.FILETYPE_PEM, cert))
f.write(cert.public_bytes(serialization.Encoding.PEM))
with open(pkey_file, "wb") as f:
f.write(crypto.dump_privatekey(crypto.FILETYPE_PEM, pkey))
f.write(
pkey.private_bytes(
encoding=serialization.Encoding.PEM,
format=serialization.PrivateFormat.TraditionalOpenSSL,
encryption_algorithm=serialization.NoEncryption(),
)
)

return cert_file, pkey_file


def generate_adhoc_ssl_context():
"""Generates an adhoc SSL context for the development server."""
crypto = _get_openssl_crypto_module()
import tempfile
import atexit

cert, pkey = generate_adhoc_ssl_pair()

from cryptography.hazmat.primitives import serialization

cert_handle, cert_file = tempfile.mkstemp()
pkey_handle, pkey_file = tempfile.mkstemp()
atexit.register(os.remove, pkey_file)
atexit.register(os.remove, cert_file)

os.write(cert_handle, crypto.dump_certificate(crypto.FILETYPE_PEM, cert))
os.write(pkey_handle, crypto.dump_privatekey(crypto.FILETYPE_PEM, pkey))
os.write(cert_handle, cert.public_bytes(serialization.Encoding.PEM))
os.write(
pkey_handle,
pkey.private_bytes(
encoding=serialization.Encoding.PEM,
format=serialization.PrivateFormat.TraditionalOpenSSL,
encryption_algorithm=serialization.NoEncryption(),
),
)

os.close(cert_handle)
os.close(pkey_handle)
ctx = load_ssl_context(cert_file, pkey_file)
Expand Down Expand Up @@ -611,17 +628,9 @@ def wrap_socket(self, sock, **kwargs):

def is_ssl_error(error=None):
"""Checks if the given error (or the current one) is an SSL error."""
exc_types = (ssl.SSLError,)
try:
from OpenSSL.SSL import Error

exc_types += (Error,)
except ImportError:
pass

if error is None:
error = sys.exc_info()[1]
return isinstance(error, exc_types)
return isinstance(error, ssl.SSLError)


def select_address_family(host, port):
Expand Down
12 changes: 7 additions & 5 deletions tests/test_serving.py
Expand Up @@ -24,9 +24,9 @@
from werkzeug import serving

try:
import OpenSSL
import cryptography
except ImportError:
OpenSSL = None
cryptography = None

try:
import watchdog
Expand Down Expand Up @@ -101,7 +101,9 @@ def app(environ, start_response):
not hasattr(ssl, "SSLContext"),
reason="Missing PEP 466 (Python 2.7.9+) or Python 3.",
)
@pytest.mark.skipif(OpenSSL is None, reason="OpenSSL is required for cert generation.")
@pytest.mark.skipif(
cryptography is None, reason="cryptography is required for cert generation."
)
def test_stdlib_ssl_contexts(dev_server, tmpdir):
certificate, private_key = serving.make_ssl_devcert(str(tmpdir.mkdir("certs")))

Expand All @@ -124,7 +126,7 @@ def app(environ, start_response):
assert r.content == b"hello"


@pytest.mark.skipif(OpenSSL is None, reason="OpenSSL is not installed.")
@pytest.mark.skipif(cryptography is None, reason="cryptography is not installed.")
def test_ssl_context_adhoc(dev_server):
server = dev_server(
"""
Expand All @@ -139,7 +141,7 @@ def app(environ, start_response):
assert r.content == b"hello"


@pytest.mark.skipif(OpenSSL is None, reason="OpenSSL is not installed.")
@pytest.mark.skipif(cryptography is None, reason="cryptography is not installed.")
def test_make_ssl_devcert(tmpdir):
certificate, private_key = serving.make_ssl_devcert(str(tmpdir))
assert os.path.isfile(certificate)
Expand Down
2 changes: 1 addition & 1 deletion tox.ini
Expand Up @@ -14,7 +14,7 @@ deps =
pytest-xprocess
requests
requests_unixsocket
pyopenssl
cryptography
greenlet
watchdog
commands = coverage run -p -m pytest --tb=short --basetemp={envtmpdir} {posargs}
Expand Down

0 comments on commit 84c98f2

Please sign in to comment.