Repository: benoitc/gunicorn

Issue:
Add support for named constants in the --ssl-version flag
I'm using gunicorn with ssl cert and key. It provides TLS connection 1.0. Is it possible to configure gunicorn to use TLS 1.2?

Add support for named constants in the --ssl-version flag
I'm using gunicorn with ssl cert and key. It provides TLS connection 1.0. Is it possible to configure gunicorn to use TLS 1.2?



Discussion:
Hi @rrajaravi, did you try setting http://docs.gunicorn.org/en/latest/settings.html#ciphers ?

bump

How to determine it work or not?

http://superuser.com/questions/109213/how-do-i-list-the-ssl-tls-cipher-suites-a-particular-website-offers

can we close this issue?

TL;DR
Gunicorn only appears to support:
TLSv1
SSLv2

And does not support:
SSLv3
SSLv23
TLSv1_1
TLSv1_2

@berkerpeksag The docs at http://docs.gunicorn.org/en/latest/settings.html#ciphers do not appear to be correct.
I found the protocols in ssl.py. None of them except, TLSv1 and SSLv2,   work when provided at a command line option on Ubuntu 14.04 LTS, Python 3.5.1, OpenSSL 1.0.1f 6 Jan 2014

The rest fail with error:
[2016-03-22 08:51:49 +0000] [2924] [ERROR] Exception in worker process:
Traceback (most recent call last):
  File "/home/me/Envs/myproject/lib/python3.5/site-packages/gunicorn/workers/sync.py", line 126, in handle
    **self.cfg.ssl_options)
  File "/usr/local/lib/python3.5/ssl.py", line 1064, in wrap_socket
    ciphers=ciphers)
  File "/usr/local/lib/python3.5/ssl.py", line 690, in **init**
    self._context.set_ciphers(ciphers)
ssl.SSLError: ('No cipher can be selected.',)

---

/usr/local/lib/python3.5/ssl.py
The following constants identify various SSL protocol variants:

PROTOCOL_SSLv2
PROTOCOL_SSLv3
PROTOCOL_SSLv23
PROTOCOL_TLSv1
PROTOCOL_TLSv1_1
PROTOCOL_TLSv1_2

Thanks for triaging this, @edwardotis. I'm reopening this to clarify documentation.

@berkerpeksag also are we using the good default?

`TLSv1` looks fine for older Python versions, but perhaps we can change the default value to `SSLv23` to use best available protocol (both for clients and servers).

Also, I think we should raise an exception if `SSLv2` or `SSLv3` is used in Gunicorn 20 (it would be great to deprecate them in the next 19.x release).

@berkerpeksag :+1: 

Has anyone implemented these changes? If not, i'd be happy to give it a shot & open a PR.. Being able to set the cipher to something > TLSv1.0 is important to us and from what we can see any version of TLS greater than 1 can't be set.. it just defaults back to 1.0 (We tried the ones listed in docs & the constants listed here to no avail) -- just outputs `No cipher can be selected` 

@AndrewJHart that would be great! :) Thanks!

It's possible to use other versions. You just need to specify a constant to `--ssl-version` on the command line. These constants come from the `ssl` stdlib module.

```
$ python -c "import ssl; print(ssl.PROTOCOL_SSLv23)"     
  2
$ python -c "import ssl; print(ssl.PROTOCOL_TLSv1_2)"
  5
```

Just specify `--ssl-version 2` or `--ssl-version 5` and you should have TLS 1.2 cipher support.

@AndrewJHart take a look at #1440 if you would like to contribute some improvements to SSL. We should switch to using SSLContext for better control over the context, such as setting server cipher order preference. I'll post a diff over on that issue momentarily with an example of starting to implement this for the sync worker. Any help with improving SSL support would be greatly appreciated.

Also, a simple pull request to change the default value in gunicorn/config.py to use SSLv23 constant instead of TLSv1 would be great.

Sorry, I meant to say #1140

I don't understand why this issue was re-opened. I have tested Gunicorn with `--ssl-version 2` and found that TLS 1.2 ciphers work fine.

I will close, but please re-open and explain if you think I have done so in error.

I opened #1249 to track changing the default.

I reopened it because:

a) Users should be able to pass `'SSLv23'` or `'TLSv1_2'` instead of `2`, or `5`.
b) We should document available options instead of redirecting them to Python docs. https://docs.python.org/3.5/library/ssl.html is one of the longest documents in the stdlib and users shouldn't spend too much time just to see what options are available.

Re-opened, adding the documentation labels.

let me know what's needed here, if we can close it before a 19.5 release (ie before wed) would be cool :)

This is open still to provide only support for named constants in the
--ssl-version flag.

On Mon, May 2, 2016, 05:43 Benoit Chesneau notifications@github.com wrote:

> let me know what's needed here, if we can close it before a 19.5 release
> (ie before wed) would be cool :)
> 
> —
> You are receiving this because you modified the open/close state.
> Reply to this email directly or view it on GitHub
> https://github.com/benoitc/gunicorn/issues/1114#issuecomment-216225302

let's forget the 3.x version for now. What are the constant needed to add to support it on all version of python? 
Here's a proof-of-concept: https://github.com/benoitc/gunicorn/pull/1454
Hi @rrajaravi, did you try setting http://docs.gunicorn.org/en/latest/settings.html#ciphers ?

bump

How to determine it work or not?

http://superuser.com/questions/109213/how-do-i-list-the-ssl-tls-cipher-suites-a-particular-website-offers

can we close this issue?

TL;DR
Gunicorn only appears to support:
TLSv1
SSLv2

And does not support:
SSLv3
SSLv23
TLSv1_1
TLSv1_2

@berkerpeksag The docs at http://docs.gunicorn.org/en/latest/settings.html#ciphers do not appear to be correct.
I found the protocols in ssl.py. None of them except, TLSv1 and SSLv2,   work when provided at a command line option on Ubuntu 14.04 LTS, Python 3.5.1, OpenSSL 1.0.1f 6 Jan 2014

The rest fail with error:
[2016-03-22 08:51:49 +0000] [2924] [ERROR] Exception in worker process:
Traceback (most recent call last):
  File "/home/me/Envs/myproject/lib/python3.5/site-packages/gunicorn/workers/sync.py", line 126, in handle
    **self.cfg.ssl_options)
  File "/usr/local/lib/python3.5/ssl.py", line 1064, in wrap_socket
    ciphers=ciphers)
  File "/usr/local/lib/python3.5/ssl.py", line 690, in **init**
    self._context.set_ciphers(ciphers)
ssl.SSLError: ('No cipher can be selected.',)

---

/usr/local/lib/python3.5/ssl.py
The following constants identify various SSL protocol variants:

PROTOCOL_SSLv2
PROTOCOL_SSLv3
PROTOCOL_SSLv23
PROTOCOL_TLSv1
PROTOCOL_TLSv1_1
PROTOCOL_TLSv1_2

Thanks for triaging this, @edwardotis. I'm reopening this to clarify documentation.

@berkerpeksag also are we using the good default?

`TLSv1` looks fine for older Python versions, but perhaps we can change the default value to `SSLv23` to use best available protocol (both for clients and servers).

Also, I think we should raise an exception if `SSLv2` or `SSLv3` is used in Gunicorn 20 (it would be great to deprecate them in the next 19.x release).

@berkerpeksag :+1: 

Has anyone implemented these changes? If not, i'd be happy to give it a shot & open a PR.. Being able to set the cipher to something > TLSv1.0 is important to us and from what we can see any version of TLS greater than 1 can't be set.. it just defaults back to 1.0 (We tried the ones listed in docs & the constants listed here to no avail) -- just outputs `No cipher can be selected` 

@AndrewJHart that would be great! :) Thanks!

It's possible to use other versions. You just need to specify a constant to `--ssl-version` on the command line. These constants come from the `ssl` stdlib module.

```
$ python -c "import ssl; print(ssl.PROTOCOL_SSLv23)"     
  2
$ python -c "import ssl; print(ssl.PROTOCOL_TLSv1_2)"
  5
```

Just specify `--ssl-version 2` or `--ssl-version 5` and you should have TLS 1.2 cipher support.

@AndrewJHart take a look at #1440 if you would like to contribute some improvements to SSL. We should switch to using SSLContext for better control over the context, such as setting server cipher order preference. I'll post a diff over on that issue momentarily with an example of starting to implement this for the sync worker. Any help with improving SSL support would be greatly appreciated.

Also, a simple pull request to change the default value in gunicorn/config.py to use SSLv23 constant instead of TLSv1 would be great.

Sorry, I meant to say #1140

I don't understand why this issue was re-opened. I have tested Gunicorn with `--ssl-version 2` and found that TLS 1.2 ciphers work fine.

I will close, but please re-open and explain if you think I have done so in error.

I opened #1249 to track changing the default.

I reopened it because:

a) Users should be able to pass `'SSLv23'` or `'TLSv1_2'` instead of `2`, or `5`.
b) We should document available options instead of redirecting them to Python docs. https://docs.python.org/3.5/library/ssl.html is one of the longest documents in the stdlib and users shouldn't spend too much time just to see what options are available.

Re-opened, adding the documentation labels.

let me know what's needed here, if we can close it before a 19.5 release (ie before wed) would be cool :)

This is open still to provide only support for named constants in the
--ssl-version flag.

On Mon, May 2, 2016, 05:43 Benoit Chesneau notifications@github.com wrote:

> let me know what's needed here, if we can close it before a 19.5 release
> (ie before wed) would be cool :)
> 
> —
> You are receiving this because you modified the open/close state.
> Reply to this email directly or view it on GitHub
> https://github.com/benoitc/gunicorn/issues/1114#issuecomment-216225302

let's forget the 3.x version for now. What are the constant needed to add to support it on all version of python? 
Here's a proof-of-concept: https://github.com/benoitc/gunicorn/pull/1454

Resolving patch:
diff --git a/gunicorn/config.py b/gunicorn/config.py
--- a/gunicorn/config.py
+++ b/gunicorn/config.py
@@ -344,6 +344,28 @@ def validate_pos_int(val):
     return val
 
 
+def validate_ssl_version(val):
+    ssl_versions = {}
+    for protocol in [p for p in dir(ssl) if p.startswith("PROTOCOL_")]:
+        ssl_versions[protocol[9:]] = getattr(ssl, protocol)
+    if val in ssl_versions:
+        # string matching PROTOCOL_...
+        return ssl_versions[val]
+
+    try:
+        intval = validate_pos_int(val)
+        if intval in ssl_versions.values():
+            # positive int matching a protocol int constant
+            return intval
+    except (ValueError, TypeError):
+        # negative integer or not an integer
+        # drop this in favour of the more descriptive ValueError below
+        pass
+
+    raise ValueError("Invalid ssl_version: %s. Valid options: %s"
+                     % (val, ', '.join(ssl_versions)))
+
+
 def validate_string(val):
     if val is None:
         return None
@@ -1861,14 +1883,31 @@ class SSLVersion(Setting):
     name = "ssl_version"
     section = "SSL"
     cli = ["--ssl-version"]
-    validator = validate_pos_int
+    validator = validate_ssl_version
     default = ssl.PROTOCOL_SSLv23
     desc = """\
-    SSL version to use (see stdlib ssl module's)
+    SSL version to use.
+
+    ============= ============
+    --ssl-version Description
+    ============= ============
+    SSLv3         SSLv3 is not-secure and is strongly discouraged.
+    SSLv23        Alias for TLS. Deprecated in Python 3.6, use TLS.
+    TLS           Negotiate highest possible version between client/server.
+                  Can yield SSL. (Python 3.6+)
+    TLSv1         TLS 1.0
+    TLSv1_1       TLS 1.1 (Python 3.4+)
+    TLSv2         TLS 1.2 (Python 3.4+)
+    TLS_SERVER    Auto-negotiate the highest protocol version like TLS,
+                  but only support server-side SSLSocket connections.
+                  (Python 3.6+)
 
     .. versionchanged:: 19.7
        The default value has been changed from ``ssl.PROTOCOL_TLSv1`` to
        ``ssl.PROTOCOL_SSLv23``.
+    .. versionchanged:: 20.0
+       This setting now accepts string names based on ``ssl.PROTOCOL_``
+       constants.
     """
 
 class CertReqs(Setting):


Test patch:
diff --git a/tests/test_config.py b/tests/test_config.py
--- a/tests/test_config.py
+++ b/tests/test_config.py
@@ -386,3 +386,46 @@ def test_umask_config(options, expected):
     with AltArgs(cmdline):
         app = NoConfigApp()
     assert app.cfg.umask == expected
+
+
+@pytest.mark.parametrize("options, expected", [
+    (["--ssl-version", "SSLv23"], 2),
+    (["--ssl-version", "TLSv1"], 3),
+    (["--ssl-version", "2"], 2),
+    (["--ssl-version", "3"], 3),
+])
+def test_ssl_version_named_constants_python3(options, expected):
+    _test_ssl_version(options, expected)
+
+
+@pytest.mark.skipif(sys.version_info < (3, 6),
+    reason="requires python3.6+")
+@pytest.mark.parametrize("options, expected", [
+    (["--ssl-version", "TLS"], 2),
+    (["--ssl-version", "TLSv1_1"], 4),
+    (["--ssl-version", "TLSv1_2"], 5),
+    (["--ssl-version", "TLS_SERVER"], 17),
+])
+def test_ssl_version_named_constants_python36(options, expected):
+    _test_ssl_version(options, expected)
+
+
+@pytest.mark.parametrize("ssl_version", [
+    "FOO",
+    "-99",
+    "99991234"
+])
+def test_ssl_version_bad(ssl_version):
+    c = config.Config()
+    with pytest.raises(ValueError) as exc:
+        c.set("ssl_version", ssl_version)
+    assert 'Valid options' in str(exc.value)
+    assert "TLSv" in str(exc.value)
+
+
+def _test_ssl_version(options, expected):
+    cmdline = ["prog_name"]
+    cmdline.extend(options)
+    with AltArgs(cmdline):
+        app = NoConfigApp()
+    assert app.cfg.ssl_version == expected