From e64a0c49ef43110db91ce35ed9d5f311df97c75b Mon Sep 17 00:00:00 2001 From: Vladimir Magamedov Date: Tue, 23 Jun 2020 23:03:03 +0300 Subject: [PATCH] Added certifi support, documented secure channels --- docs/client.rst | 25 +++++++++++++++++++++++++ grpclib/client.py | 12 +++++++++++- requirements/runtime.in | 1 + requirements/runtime.txt | 1 + requirements/test.txt | 1 + tests/test_client_channel.py | 13 +++++++++++++ 6 files changed, 52 insertions(+), 1 deletion(-) diff --git a/docs/client.rst b/docs/client.rst index b3790cb..8faef2b 100644 --- a/docs/client.rst +++ b/docs/client.rst @@ -45,9 +45,34 @@ There are two ways to call RPC methods: See reference docs for all method types and for the :py:class:`~grpclib.client.Stream` methods and attributes. +Secure Channels +~~~~~~~~~~~~~~~ + +Here is how to establish a secure connection to a public gRPC server: + +.. code-block:: python3 + + channel = Channel(host, port, ssl=True) + ^^^^^^^^ + +In this case ``grpclib`` uses system CA certificates. But ``grpclib`` has also +a built-in support for a certifi_ package which contains actual Mozilla's +collection of CA certificates. All you need is to install it and keep it +updated -- this is a more favorable way than relying on system CA certificates: + +.. code-block:: console + + $ pip3 install certifi + +``grpclib`` also allows you to use a custom SSL configuration by providing a +:py:class:`~python:ssl.SSLContext` object. We have a simple mTLS auth example +in our code repository to illustrate how this works. + Reference ~~~~~~~~~ .. automodule:: grpclib.client :members: Channel, Stream, UnaryUnaryMethod, UnaryStreamMethod, StreamUnaryMethod, StreamStreamMethod + +.. _certifi: https://github.com/certifi/python-certifi diff --git a/grpclib/client.py b/grpclib/client.py index 92581a3..ec02088 100644 --- a/grpclib/client.py +++ b/grpclib/client.py @@ -706,7 +706,17 @@ class Channel: if _ssl is None: raise RuntimeError('SSL is not supported.') - ctx = _ssl.create_default_context(purpose=_ssl.Purpose.SERVER_AUTH) + try: + import certifi + except ImportError: + cafile = None + else: + cafile = certifi.where() + + ctx = _ssl.create_default_context( + purpose=_ssl.Purpose.SERVER_AUTH, + cafile=cafile, + ) ctx.options |= (_ssl.OP_NO_TLSv1 | _ssl.OP_NO_TLSv1_1) ctx.set_ciphers('ECDHE+AESGCM:ECDHE+CHACHA20:DHE+AESGCM:DHE+CHACHA20') ctx.set_alpn_protocols(['h2']) diff --git a/requirements/runtime.in b/requirements/runtime.in index b99ce8d..8fe7d74 100644 --- a/requirements/runtime.in +++ b/requirements/runtime.in @@ -1,3 +1,4 @@ -r ../setup.txt protobuf googleapis-common-protos +certifi diff --git a/requirements/runtime.txt b/requirements/runtime.txt index f9318d3..25dc201 100644 --- a/requirements/runtime.txt +++ b/requirements/runtime.txt @@ -4,6 +4,7 @@ # # pip-compile requirements/runtime.in # +certifi==2020.6.20 googleapis-common-protos==1.51.0 h2==3.1.1 hpack==3.0.0 diff --git a/requirements/test.txt b/requirements/test.txt index 55f1d64..b5b039e 100644 --- a/requirements/test.txt +++ b/requirements/test.txt @@ -6,6 +6,7 @@ # async-timeout==3.0.1 attrs==19.3.0 # via pytest +certifi==2020.6.20 coverage==5.0.3 # via pytest-cov faker==4.0.0 googleapis-common-protos==1.51.0 diff --git a/tests/test_client_channel.py b/tests/test_client_channel.py index c0a9254..78ece9a 100644 --- a/tests/test_client_channel.py +++ b/tests/test_client_channel.py @@ -36,3 +36,16 @@ async def test_concurrent_connect(loop): replies = await asyncio.gather(*tasks) assert replies == reps po.assert_called_once_with(ANY, '127.0.0.1', 50051, ssl=None) + + +def test_default_ssl_context(): + certifi_channel = Channel(ssl=True) + with patch.dict('sys.modules', {'certifi': None}): + system_channel = Channel(ssl=True) + + certifi_certs = certifi_channel._ssl.get_ca_certs(binary_form=True) + system_certs = system_channel._ssl.get_ca_certs(binary_form=True) + + assert certifi_certs + assert system_certs + assert certifi_certs != system_certs