SslStream, disable session caching

前端 未结 2 585
伪装坚强ぢ
伪装坚强ぢ 2020-12-18 23:24

The MSDN documentation says

The Framework caches SSL sessions as they are created and attempts to reuse a cached session for a new request, if possibl

相关标签:
2条回答
  • 2020-12-19 00:01

    So I solved this problem a bit differently. I really didn't like the idea of reflecting out this private static method to dump the cache because you don't really know what you're getting into by doing so; you're basically circumventing encapsulation and that could cause unforeseen problems. But really, I was worried about race conditions where I dump the cache and before I send the request, some other thread comes in and establishes a new session so then my first thread inadvertently hijacks that session. Bad news... anyway, here's what I did.

    I stopped to think about whether or not there was a way to sort of isolate the process and then an Android co-worker of mine recalled the availability of AppDomains. We both agreed that spinning one up should allow the Tcp/Ssl call to run, isolated from everything else. This would allow the caching logic to remain intact without causing conflicts between SSL sessions.

    Basically, I had originally written my SSL client to be internal to a separate library. Then within that library, I had a public service act as a proxy/mediator to that client. In the application layer, I wanted the ability to switch between services (HSM services, in my case) based on the hardware type, so I wrapped that into an adapter and interfaced that to be used with a factory. Ok, so how is that relevant? Well it just made it easier to do this AppDomain thing cleanly, without forcing this behavior any other consumer of the public service (the proxy/mediator I spoke of). You don't have to follow this abstraction, I just like to share good examples of abstraction whenever I find them :)

    Now, in the adapter, instead of calling the service directly, I basically create the domain. Here is the ctor:

    public VCRklServiceAdapter(
        string hostname, 
        int port,  
        IHsmLogger logger)
    {
        Ensure.IsNotNullOrEmpty(hostname, nameof(hostname));
        Ensure.IsNotDefault(port, nameof(port), failureMessage: $"It does not appear that the port number was actually set (port: {port})");
        Ensure.IsNotNull(logger, nameof(logger));
    
        ClientId = Guid.NewGuid();
    
        _logger = logger;
        _hostname = hostname;
        _port = port;
    
        // configure the domain
        _instanceDomain = AppDomain.CreateDomain(
            $"vcrypt_rkl_instance_{ClientId}",
            null, 
            AppDomain.CurrentDomain.SetupInformation);
    
        // using the configured domain, grab a command instance from which we can
        // marshall in some data
        _rklServiceRuntime = (IRklServiceRuntime)_instanceDomain.CreateInstanceAndUnwrap(
            typeof(VCServiceRuntime).Assembly.FullName,
            typeof(VCServiceRuntime).FullName);
    }
    

    All this does is creates a named domain from which my actual service will run in isolation. Now, most articles that I came across on how to actually execute within the domain sort of over-simplify how it works. The examples typically involve calling myDomain.DoCallback(() => ...); which isn't wrong, but trying to get data in and out of that domain will likely become problematic as serialization will likely stop you dead in your tracks. Simply put, objects that are instantiated outside of DoCallback() are not the same objects when called from inside of DoCallback since they were created outside of this domain (see object marshalling). So you'll likely get all kinds of serialization errors. This isn't a problem if running the entire operation, input and output and all can occur from inside myDomain.DoCallback() but this is problematic if you need to use external parameters and return something across this AppDomain back to the originating domain.

    I came across a different pattern here on SO that worked out for me and solved this problem. Look at _rklServiceRuntime = in my sample ctor. What this is doing is actually asking the domain to instantiate an object for you to act as a proxy from that domain. This will allow you to marshall some objects in and out of it. Here is my implemenation of IRklServiceRuntime:

    public interface IRklServiceRuntime
    {       
        RklResponse Run(RklRequest request, string hostname, int port, Guid clientId, IHsmLogger logger);
    }
    
    public class VCServiceRuntime : MarshalByRefObject, IRklServiceRuntime
    {
        public RklResponse Run(
            RklRequest request, 
            string hostname, 
            int port, 
            Guid clientId,
            IHsmLogger logger)
        {
            Ensure.IsNotNull(request, nameof(request));
            Ensure.IsNotNullOrEmpty(hostname, nameof(hostname));
            Ensure.IsNotDefault(port, nameof(port), failureMessage: $"It does not appear that the port number was actually set (port: {port})");
            Ensure.IsNotNull(logger, nameof(logger));
    
            // these are set here instead of passed in because they are not
            // serializable
            var clientCert = ApplicationValues.VCClientCertificate;
            var clientCerts = new X509Certificate2Collection(clientCert);
    
            using (var client = new VCServiceClient(hostname, port, clientCerts, clientId, logger))
            {
                var response = client.RetrieveDeviceKeys(request);
                return response;
            }
        }
    }
    

    This inherits from MarshallByRefObject which allows it to cross AppDomain boundaries, and has a single method that takes your external parameters and executes your logic from within the domain that instantiated it.

    So now back to the service adapter: All the service adapters has to do now is call _rklServiceRuntime.Run(...) and feed in the necessary, serializable parameters. Now, I just create as many instances of the service adapter as I need and they all run in their own domain. This works for me because my SSL calls are small and brief and these requests are made inside of an internal web service where instancing requests like this is very important. Here is the complete adapter:

    public class VCRklServiceAdapter : IRklService
    {
        private readonly string _hostname;
        private readonly int _port;
        private readonly IHsmLogger _logger;
        private readonly AppDomain _instanceDomain;
        private readonly IRklServiceRuntime _rklServiceRuntime;
    
        public Guid ClientId { get; }
    
        public VCRklServiceAdapter(
            string hostname, 
            int port,  
            IHsmLogger logger)
        {
            Ensure.IsNotNullOrEmpty(hostname, nameof(hostname));
            Ensure.IsNotDefault(port, nameof(port), failureMessage: $"It does not appear that the port number was actually set (port: {port})");
            Ensure.IsNotNull(logger, nameof(logger));
    
            ClientId = Guid.NewGuid();
    
            _logger = logger;
            _hostname = hostname;
            _port = port;
    
            // configure the domain
            _instanceDomain = AppDomain.CreateDomain(
                $"vc_rkl_instance_{ClientId}",
                null, 
                AppDomain.CurrentDomain.SetupInformation);
    
            // using the configured domain, grab a command instance from which we can
            // marshall in some data
            _rklServiceRuntime = (IRklServiceRuntime)_instanceDomain.CreateInstanceAndUnwrap(
                typeof(VCServiceRuntime).Assembly.FullName,
                typeof(VCServiceRuntime).FullName);
        }
    
        public RklResponse GetKeys(RklRequest rklRequest)
        {
            Ensure.IsNotNull(rklRequest, nameof(rklRequest));
    
            var response = _rklServiceRuntime.Run(
                rklRequest, 
                _hostname, 
                _port, 
                ClientId, 
                _logger);
    
            return response;
        }
    
        /// <summary>
        /// Releases unmanaged and - optionally - managed resources.
        /// </summary>
        public void Dispose()
        {
            AppDomain.Unload(_instanceDomain);
        }
    }
    

    Notice the dispose method. Don't forget to unload the domain. This service implements IRklService which implements IDisposable, so when I use it, it used with a using statement.

    This seems a bit contrived, but it's really not and now the logic will be run on it's own domain, in isolation, and thus the caching logic remains intact but non-problematic. Much better than meddling with the SSLSessionCache!

    Please forgive any naming inconsistencies as I was sanitizing the actual names quickly after writing the post.. I hope this helps someone!

    0 讨论(0)
  • 2020-12-19 00:15

    Caching is handled inside SecureChannel - internal class that wraps SSPI and used by SslStream. I don't see any points inside that you can use to disable session caching for client connections.

    You can clear cache between connections using reflection:

    var sslAssembly = Assembly.GetAssembly(typeof(SslStream));
    
    var sslSessionCacheClass = sslAssembly.GetType("System.Net.Security.SslSessionsCache");
    
    var cachedCredsInfo = sslSessionCacheClass.GetField("s_CachedCreds", BindingFlags.NonPublic | BindingFlags.Static);
    var cachedCreds = (Hashtable)cachedCredsInfo.GetValue(null);
    
    cachedCreds.Clear();
    

    But it's very bad practice. Consider to fix server side.

    0 讨论(0)
提交回复
热议问题