Merge pull request #14879 from santoshankr/python_ssl_session_cache_lru

TLS session resumption support for Python clients.
pull/15689/head
Nathaniel Manista 7 years ago committed by GitHub
commit 8872a312c3
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 5
      src/python/grpcio/grpc/_cython/_cygrpc/credentials.pxd.pxi
  2. 19
      src/python/grpcio/grpc/_cython/_cygrpc/credentials.pyx.pxi
  3. 9
      src/python/grpcio/grpc/_cython/_cygrpc/grpc.pxi
  4. 1
      src/python/grpcio/grpc/_cython/_cygrpc/records.pyx.pxi
  5. 45
      src/python/grpcio/grpc/experimental/session_cache.py
  6. 1
      src/python/grpcio_tests/tests/tests.json
  7. 45
      src/python/grpcio_tests/tests/unit/_auth_context_test.py
  8. 145
      src/python/grpcio_tests/tests/unit/_session_cache_test.py

@ -57,6 +57,11 @@ cdef class ChannelCredentials:
cdef grpc_channel_credentials *c_credentials
cdef class SSLSessionCacheLRU:
cdef grpc_ssl_session_cache *_cache
cdef class SSLChannelCredentials(ChannelCredentials):
cdef readonly object _pem_root_certificates

@ -17,6 +17,9 @@ cimport cpython
import grpc
import threading
from libc.stdint cimport uintptr_t
def _spawn_callback_in_thread(cb_func, args):
threading.Thread(target=cb_func, args=args).start()
@ -29,6 +32,7 @@ def set_async_callback_func(callback_func):
def _spawn_callback_async(callback, args):
async_callback_func(callback, args)
cdef class CallCredentials:
cdef grpc_call_credentials *c(self):
@ -107,6 +111,21 @@ cdef class ChannelCredentials:
raise NotImplementedError()
cdef class SSLSessionCacheLRU:
def __cinit__(self, capacity):
grpc_init()
self._cache = grpc_ssl_session_cache_create_lru(capacity)
def __int__(self):
return <uintptr_t>self._cache
def __dealloc__(self):
if self._cache != NULL:
grpc_ssl_session_cache_destroy(self._cache)
grpc_shutdown()
cdef class SSLChannelCredentials(ChannelCredentials):
def __cinit__(self, pem_root_certificates, private_key, certificate_chain):

@ -131,6 +131,7 @@ cdef extern from "grpc/grpc.h":
const char *GRPC_ARG_PRIMARY_USER_AGENT_STRING
const char *GRPC_ARG_SECONDARY_USER_AGENT_STRING
const char *GRPC_SSL_TARGET_NAME_OVERRIDE_ARG
const char *GRPC_SSL_SESSION_CACHE_ARG
const char *GRPC_COMPRESSION_CHANNEL_DEFAULT_ALGORITHM
const char *GRPC_COMPRESSION_CHANNEL_DEFAULT_LEVEL
const char *GRPC_COMPRESSION_CHANNEL_ENABLED_ALGORITHMS_BITSET
@ -452,8 +453,16 @@ cdef extern from "grpc/grpc_security.h":
# We don't care about the internals (and in fact don't know them)
pass
ctypedef struct grpc_ssl_session_cache:
# We don't care about the internals (and in fact don't know them)
pass
ctypedef void (*grpc_ssl_roots_override_callback)(char **pem_root_certs)
grpc_ssl_session_cache *grpc_ssl_session_cache_create_lru(size_t capacity)
void grpc_ssl_session_cache_destroy(grpc_ssl_session_cache* cache)
void grpc_set_ssl_roots_override_callback(
grpc_ssl_roots_override_callback cb) nogil

@ -51,6 +51,7 @@ class ChannelArgKey:
default_authority = GRPC_ARG_DEFAULT_AUTHORITY
primary_user_agent_string = GRPC_ARG_PRIMARY_USER_AGENT_STRING
secondary_user_agent_string = GRPC_ARG_SECONDARY_USER_AGENT_STRING
ssl_session_cache = GRPC_SSL_SESSION_CACHE_ARG
ssl_target_name_override = GRPC_SSL_TARGET_NAME_OVERRIDE_ARG

@ -0,0 +1,45 @@
# Copyright 2018 gRPC authors.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
"""gRPC's APIs for TLS Session Resumption support"""
from grpc._cython import cygrpc as _cygrpc
def ssl_session_cache_lru(capacity):
"""Creates an SSLSessionCache with LRU replacement policy
Args:
capacity: Size of the cache
Returns:
An SSLSessionCache with LRU replacement policy that can be passed as a value for
the grpc.ssl_session_cache option to a grpc.Channel. SSL session caches are used
to store session tickets, which clients can present to resume previous TLS sessions
with a server.
"""
return SSLSessionCache(_cygrpc.SSLSessionCacheLRU(capacity))
class SSLSessionCache(object):
"""An encapsulation of a session cache used for TLS session resumption.
Instances of this class can be passed to a Channel as values for the
grpc.ssl_session_cache option
"""
def __init__(self, cache):
self._cache = cache
def __int__(self):
return int(self._cache)

@ -53,6 +53,7 @@
"unit._server_ssl_cert_config_test.ServerSSLCertReloadTestCertConfigReuse",
"unit._server_ssl_cert_config_test.ServerSSLCertReloadTestWithClientAuth",
"unit._server_ssl_cert_config_test.ServerSSLCertReloadTestWithoutClientAuth",
"unit._session_cache_test.SSLSessionCacheTest",
"unit.beta._beta_features_test.BetaFeaturesTest",
"unit.beta._beta_features_test.ContextManagementAndLifecycleTest",
"unit.beta._connectivity_channel_test.ConnectivityStatesTest",

@ -18,6 +18,7 @@ import unittest
import grpc
from grpc import _channel
from grpc.experimental import session_cache
import six
from tests.unit import test_common
@ -140,6 +141,50 @@ class AuthContextTest(unittest.TestCase):
self.assertSequenceEqual([b'*.test.google.com'],
auth_ctx['x509_common_name'])
def _do_one_shot_client_rpc(self, channel_creds, channel_options, port,
expect_ssl_session_reused):
channel = grpc.secure_channel(
'localhost:{}'.format(port), channel_creds, options=channel_options)
response = channel.unary_unary(_UNARY_UNARY)(_REQUEST)
auth_data = pickle.loads(response)
self.assertEqual(expect_ssl_session_reused,
auth_data[_AUTH_CTX]['ssl_session_reused'])
channel.close()
def testSessionResumption(self):
# Set up a secure server
handler = grpc.method_handlers_generic_handler('test', {
'UnaryUnary':
grpc.unary_unary_rpc_method_handler(handle_unary_unary)
})
server = test_common.test_server()
server.add_generic_rpc_handlers((handler,))
server_cred = grpc.ssl_server_credentials(_SERVER_CERTS)
port = server.add_secure_port('[::]:0', server_cred)
server.start()
# Create a cache for TLS session tickets
cache = session_cache.ssl_session_cache_lru(1)
channel_creds = grpc.ssl_channel_credentials(
root_certificates=_TEST_ROOT_CERTIFICATES)
channel_options = _PROPERTY_OPTIONS + (
('grpc.ssl_session_cache', cache),)
# Initial connection has no session to resume
self._do_one_shot_client_rpc(
channel_creds,
channel_options,
port,
expect_ssl_session_reused=[b'false'])
# Subsequent connections resume sessions
self._do_one_shot_client_rpc(
channel_creds,
channel_options,
port,
expect_ssl_session_reused=[b'true'])
server.stop(None)
if __name__ == '__main__':
unittest.main(verbosity=2)

@ -0,0 +1,145 @@
# Copyright 2018 gRPC authors.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
"""Tests experimental TLS Session Resumption API"""
import pickle
import unittest
import grpc
from grpc import _channel
from grpc.experimental import session_cache
from tests.unit import test_common
from tests.unit import resources
_REQUEST = b'\x00\x00\x00'
_RESPONSE = b'\x00\x00\x00'
_UNARY_UNARY = '/test/UnaryUnary'
_SERVER_HOST_OVERRIDE = 'foo.test.google.fr'
_ID = 'id'
_ID_KEY = 'id_key'
_AUTH_CTX = 'auth_ctx'
_PRIVATE_KEY = resources.private_key()
_CERTIFICATE_CHAIN = resources.certificate_chain()
_TEST_ROOT_CERTIFICATES = resources.test_root_certificates()
_SERVER_CERTS = ((_PRIVATE_KEY, _CERTIFICATE_CHAIN),)
_PROPERTY_OPTIONS = ((
'grpc.ssl_target_name_override',
_SERVER_HOST_OVERRIDE,
),)
def handle_unary_unary(request, servicer_context):
return pickle.dumps({
_ID: servicer_context.peer_identities(),
_ID_KEY: servicer_context.peer_identity_key(),
_AUTH_CTX: servicer_context.auth_context()
})
def start_secure_server():
handler = grpc.method_handlers_generic_handler('test', {
'UnaryUnary':
grpc.unary_unary_rpc_method_handler(handle_unary_unary)
})
server = test_common.test_server()
server.add_generic_rpc_handlers((handler,))
server_cred = grpc.ssl_server_credentials(_SERVER_CERTS)
port = server.add_secure_port('[::]:0', server_cred)
server.start()
return server, port
class SSLSessionCacheTest(unittest.TestCase):
def _do_one_shot_client_rpc(self, channel_creds, channel_options, port,
expect_ssl_session_reused):
channel = grpc.secure_channel(
'localhost:{}'.format(port), channel_creds, options=channel_options)
response = channel.unary_unary(_UNARY_UNARY)(_REQUEST)
auth_data = pickle.loads(response)
self.assertEqual(expect_ssl_session_reused,
auth_data[_AUTH_CTX]['ssl_session_reused'])
channel.close()
def testSSLSessionCacheLRU(self):
server_1, port_1 = start_secure_server()
cache = session_cache.ssl_session_cache_lru(1)
channel_creds = grpc.ssl_channel_credentials(
root_certificates=_TEST_ROOT_CERTIFICATES)
channel_options = _PROPERTY_OPTIONS + (
('grpc.ssl_session_cache', cache),)
# Initial connection has no session to resume
self._do_one_shot_client_rpc(
channel_creds,
channel_options,
port_1,
expect_ssl_session_reused=[b'false'])
# Connection to server_1 resumes from initial session
self._do_one_shot_client_rpc(
channel_creds,
channel_options,
port_1,
expect_ssl_session_reused=[b'true'])
# Connection to a different server with the same name overwrites the cache entry
server_2, port_2 = start_secure_server()
self._do_one_shot_client_rpc(
channel_creds,
channel_options,
port_2,
expect_ssl_session_reused=[b'false'])
self._do_one_shot_client_rpc(
channel_creds,
channel_options,
port_2,
expect_ssl_session_reused=[b'true'])
server_2.stop(None)
# Connection to server_1 now falls back to full TLS handshake
self._do_one_shot_client_rpc(
channel_creds,
channel_options,
port_1,
expect_ssl_session_reused=[b'false'])
# Re-creating server_1 causes old sessions to become invalid
server_1.stop(None)
server_1, port_1 = start_secure_server()
# Old sessions should no longer be valid
self._do_one_shot_client_rpc(
channel_creds,
channel_options,
port_1,
expect_ssl_session_reused=[b'false'])
# Resumption should work for subsequent connections
self._do_one_shot_client_rpc(
channel_creds,
channel_options,
port_1,
expect_ssl_session_reused=[b'true'])
server_1.stop(None)
if __name__ == '__main__':
unittest.main(verbosity=2)
Loading…
Cancel
Save