Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

try loading trusted certs from a list of fallbacks #633

Merged
merged 17 commits into from
Jun 29, 2017
Merged
1 change: 1 addition & 0 deletions CHANGELOG.rst
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ Changes:

- Added ``OpenSSL.debug`` that allows to get an overview of used library versions (including linked OpenSSL) and other useful runtime information using ``python -m OpenSSL.debug``.
`#620 <https://github.com/pyca/pyopenssl/pull/620>`_
- Added a fallback path to `Context.set_default_verify_paths` to accommodate the upcoming release of ``cryptography`` ``manylinux1`` wheels. `#633 <https://github.com/pyca/pyopenssl/pull/633>`_


----
Expand Down
78 changes: 78 additions & 0 deletions src/OpenSSL/SSL.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import os
import socket
from sys import platform
from functools import wraps, partial
Expand Down Expand Up @@ -130,6 +131,22 @@ class _buffer(object):
SSL_CB_HANDSHAKE_START = _lib.SSL_CB_HANDSHAKE_START
SSL_CB_HANDSHAKE_DONE = _lib.SSL_CB_HANDSHAKE_DONE

# Taken from https://golang.org/src/crypto/x509/root_linux.go
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we care about the BSD, Plan9 (lol), or Solaris values?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We won't ship a precompiled binary for those platforms so we shouldn't need to care.

_CERTIFICATE_FILE_LOCATIONS = [
"/etc/ssl/certs/ca-certificates.crt", # Debian/Ubuntu/Gentoo etc.
"/etc/pki/tls/certs/ca-bundle.crt", # Fedora/RHEL 6
"/etc/ssl/ca-bundle.pem", # OpenSUSE
"/etc/pki/tls/cacert.pem", # OpenELEC
"/etc/pki/ca-trust/extracted/pem/tls-ca-bundle.pem", # CentOS/RHEL 7
]

_CERTIFICATE_PATH_LOCATIONS = [
"/etc/ssl/certs", # SLES10/SLES11
]

_CRYPTOGRAPHY_MANYLINUX1_CA_DIR = "/opt/pyca/cryptography/openssl/certs"
_CRYPTOGRAPHY_MANYLINUX1_CA_FILE = "/opt/pyca/cryptography/openssl/cert.pem"


class Error(Exception):
"""
Expand Down Expand Up @@ -699,8 +716,69 @@ def set_default_verify_paths(self):

:return: None
"""
# SSL_CTX_set_default_verify_paths will attempt to load certs from
# both a cafile and capath that are set at compile time. However,
# it will first check environment variables and, if present, load
# those paths instead
set_result = _lib.SSL_CTX_set_default_verify_paths(self._context)
_openssl_assert(set_result == 1)
# After attempting to set default_verify_paths we need to know whether
# to go down the fallback path.
# First we'll check to see if any env vars have been set. If so,
# we won't try to do anything else because the user has set the path
# themselves.
dir_env_var = _ffi.string(
_lib.X509_get_default_cert_dir_env()
).decode("ascii")
file_env_var = _ffi.string(
_lib.X509_get_default_cert_file_env()
).decode("ascii")
if not self._check_env_vars_set(dir_env_var, file_env_var):
default_dir = _ffi.string(_lib.X509_get_default_cert_dir())
default_file = _ffi.string(_lib.X509_get_default_cert_file())
# Now we check to see if the default_dir and default_file are set
# to the exact values we use in our manylinux1 builds. If they are
# then we know to load the fallbacks
if (
default_dir == _CRYPTOGRAPHY_MANYLINUX1_CA_DIR and
default_file == _CRYPTOGRAPHY_MANYLINUX1_CA_FILE
):
# This is manylinux1, let's load our fallback paths
self._fallback_default_verify_paths(
_CERTIFICATE_FILE_LOCATIONS,
_CERTIFICATE_PATH_LOCATIONS
)

def _check_env_vars_set(self, dir_env_var, file_env_var):
"""
Check to see if the default cert dir/file environment vars are present.

:return: bool
"""
return (
os.environ.get(file_env_var) is not None or
os.environ.get(dir_env_var) is not None
)

def _fallback_default_verify_paths(self, file_path, dir_path):
"""
Default verify paths are based on the compiled version of OpenSSL.
However, when pyca/cryptography is compiled as a manylinux1 wheel
that compiled location can potentially be wrong. So, like Go, we
will try a predefined set of paths and attempt to load roots
from there.

:return: None
"""
for cafile in file_path:
if os.path.isfile(cafile):
self.load_verify_locations(cafile)
break

for capath in dir_path:
if os.path.isdir(capath):
self.load_verify_locations(None, capath)
break

def use_certificate_chain_file(self, certfile):
"""
Expand Down
89 changes: 88 additions & 1 deletion tests/test_ssl.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,8 @@

import pytest

from pretend import raiser

from six import PY3, text_type

from cryptography import x509
Expand All @@ -46,6 +48,7 @@
from OpenSSL.SSL import (
VERIFY_PEER, VERIFY_FAIL_IF_NO_PEER_CERT, VERIFY_CLIENT_ONCE, VERIFY_NONE)

from OpenSSL import SSL
from OpenSSL.SSL import (
SESS_CACHE_OFF, SESS_CACHE_CLIENT, SESS_CACHE_SERVER, SESS_CACHE_BOTH,
SESS_CACHE_NO_AUTO_CLEAR, SESS_CACHE_NO_INTERNAL_LOOKUP,
Expand All @@ -57,7 +60,7 @@
Context, ContextType, Session, Connection, ConnectionType, SSLeay_version)
from OpenSSL.SSL import _make_requires

from OpenSSL._util import lib as _lib
from OpenSSL._util import ffi as _ffi, lib as _lib

from OpenSSL.SSL import (
OP_NO_QUERY_MTU, OP_COOKIE_EXCHANGE, OP_NO_TICKET, OP_NO_COMPRESSION,
Expand Down Expand Up @@ -1108,6 +1111,79 @@ def test_load_verify_locations_wrong_args(self):
with pytest.raises(TypeError):
context.load_verify_locations(object(), object())

@pytest.mark.skipif(
not platform.startswith("linux"),
reason="Loading fallback paths is a linux-specific behavior to "
"accommodate pyca/cryptography manylinux1 wheels"
)
def test_fallback_default_verify_paths(self, monkeypatch):
"""
Test that we load certificates successfully on linux from the fallback
path. To do this we set the _CRYPTOGRAPHY_MANYLINUX1_CA_FILE and
_CRYPTOGRAPHY_MANYLINUX1_CA_DIR vars to be equal to whatever the
current OpenSSL default is and we disable
SSL_CTX_SET_default_verify_paths so that it can't find certs unless
it loads via fallback.
"""
context = Context(TLSv1_METHOD)
monkeypatch.setattr(
_lib, "SSL_CTX_set_default_verify_paths", lambda x: 1
)
monkeypatch.setattr(
SSL,
"_CRYPTOGRAPHY_MANYLINUX1_CA_FILE",
_ffi.string(_lib.X509_get_default_cert_file())
)
monkeypatch.setattr(
SSL,
"_CRYPTOGRAPHY_MANYLINUX1_CA_DIR",
_ffi.string(_lib.X509_get_default_cert_dir())
)
context.set_default_verify_paths()
store = context.get_cert_store()
sk_obj = _lib.X509_STORE_get0_objects(store._store)
assert sk_obj != _ffi.NULL
num = _lib.sk_X509_OBJECT_num(sk_obj)
assert num != 0

def test_check_env_vars(self, monkeypatch):
"""
Test that we return True/False appropriately if the env vars are set.
"""
context = Context(TLSv1_METHOD)
dir_var = "CUSTOM_DIR_VAR"
file_var = "CUSTOM_FILE_VAR"
assert context._check_env_vars_set(dir_var, file_var) is False
monkeypatch.setenv(dir_var, "value")
monkeypatch.setenv(file_var, "value")
assert context._check_env_vars_set(dir_var, file_var) is True
assert context._check_env_vars_set(dir_var, file_var) is True

def test_verify_no_fallback_if_env_vars_set(self, monkeypatch):
"""
Test that we don't use the fallback path if env vars are set.
"""
context = Context(TLSv1_METHOD)
monkeypatch.setattr(
_lib, "SSL_CTX_set_default_verify_paths", lambda x: 1
)
dir_env_var = _ffi.string(
_lib.X509_get_default_cert_dir_env()
).decode("ascii")
file_env_var = _ffi.string(
_lib.X509_get_default_cert_file_env()
).decode("ascii")
monkeypatch.setenv(dir_env_var, "value")
monkeypatch.setenv(file_env_var, "value")
context.set_default_verify_paths()

monkeypatch.setattr(
context,
"_fallback_default_verify_paths",
raiser(SystemError)
)
context.set_default_verify_paths()

@pytest.mark.skipif(
platform == "win32",
reason="set_default_verify_paths appears not to work on Windows. "
Expand Down Expand Up @@ -1141,6 +1217,17 @@ def test_set_default_verify_paths(self):
clientSSL.send(b"GET / HTTP/1.0\r\n\r\n")
assert clientSSL.recv(1024)

def test_fallback_path_is_not_file_or_dir(self):
"""
Test that when passed empty arrays or paths that do not exist no
errors are raised.
"""
context = Context(TLSv1_METHOD)
context._fallback_default_verify_paths([], [])
context._fallback_default_verify_paths(
["/not/a/file"], ["/not/a/dir"]
)

def test_add_extra_chain_cert_invalid_cert(self):
"""
`Context.add_extra_chain_cert` raises `TypeError` if called with an
Expand Down
1 change: 1 addition & 0 deletions tox.ini
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ passenv = ARCHFLAGS CFLAGS LC_ALL LDFLAGS PATH LD_LIBRARY_PATH TERM
deps =
coverage>=4.2
pytest>=3.0.1
pretend
cryptographyMaster: git+https://github.com/pyca/cryptography.git
cryptographyMinimum: cryptography<1.8
setenv =
Expand Down