File: src\libraries\Common\src\Interop\Unix\System.Security.Cryptography.Native\Interop.SslCtx.cs
Web Access
Project: src\src\libraries\System.Net.Security\src\System.Net.Security.csproj (System.Net.Security)
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
 
using System;
using System.Net;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Diagnostics;
using System.Net.Security;
using System.Runtime.InteropServices;
using System.Security.Cryptography.X509Certificates;
using System.Text;
using System.Threading;
using Microsoft.Win32.SafeHandles;
 
internal static partial class Interop
{
    internal static partial class Ssl
    {
        [LibraryImport(Libraries.CryptoNative, EntryPoint = "CryptoNative_SslCtxCreate")]
        internal static partial SafeSslContextHandle SslCtxCreate(IntPtr method);
 
        [LibraryImport(Libraries.CryptoNative, EntryPoint = "CryptoNative_SslCtxDestroy")]
        internal static partial void SslCtxDestroy(IntPtr ctx);
 
        [LibraryImport(Libraries.CryptoNative, EntryPoint = "CryptoNative_SslCtxGetData")]
        internal static partial IntPtr SslCtxGetData(IntPtr ctx);
 
        [LibraryImport(Libraries.CryptoNative, EntryPoint = "CryptoNative_SslCtxSetData")]
        internal static partial int SslCtxSetData(SafeSslContextHandle ctx, IntPtr data);
 
        [LibraryImport(Libraries.CryptoNative, EntryPoint = "CryptoNative_SslCtxSetData")]
        internal static partial int SslCtxSetData(IntPtr ctx, IntPtr data);
 
        [LibraryImport(Libraries.CryptoNative, EntryPoint = "CryptoNative_SslCtxSetAlpnSelectCb")]
        internal static unsafe partial void SslCtxSetAlpnSelectCb(SafeSslContextHandle ctx, delegate* unmanaged<IntPtr, byte**, byte*, byte*, uint, IntPtr, int> callback, IntPtr arg);
 
        [LibraryImport(Libraries.CryptoNative, EntryPoint = "CryptoNative_SslCtxSetKeylogCallback")]
        internal static unsafe partial void SslCtxSetKeylogCallback(SafeSslContextHandle ctx, delegate* unmanaged<IntPtr, char*, void> callback);
 
        [LibraryImport(Libraries.CryptoNative, EntryPoint = "CryptoNative_SslCtxSetCaching")]
        internal static unsafe partial int SslCtxSetCaching(SafeSslContextHandle ctx, int mode, int cacheSize, int contextIdLength, Span<byte> contextId, delegate* unmanaged<IntPtr, IntPtr, int> neewSessionCallback, delegate* unmanaged<IntPtr, IntPtr, void> removeSessionCallback);
 
        [LibraryImport(Libraries.CryptoNative, EntryPoint = "CryptoNative_SslCtxRemoveSession")]
        internal static unsafe partial void SslCtxRemoveSession(SafeSslContextHandle ctx, IntPtr session);
 
        internal static bool AddExtraChainCertificates(SafeSslContextHandle ctx, ReadOnlyCollection<X509Certificate2> chain)
        {
            // send pre-computed list of intermediates.
            for (int i = 0; i < chain.Count; i++)
            {
                SafeX509Handle dupCertHandle = Crypto.X509UpRef(chain[i].Handle);
                Crypto.CheckValidOpenSslHandle(dupCertHandle);
                if (!SslCtxAddExtraChainCert(ctx, dupCertHandle))
                {
                    Crypto.ErrClearError();
                    dupCertHandle.Dispose(); // we still own the safe handle; clean it up
                    return false;
                }
                dupCertHandle.SetHandleAsInvalid(); // ownership has been transferred to sslHandle; do not free via this safe handle
            }
 
            return true;
        }
    }
}
 
namespace Microsoft.Win32.SafeHandles
{
    internal sealed class SafeSslContextHandle : SafeHandle, ISafeHandleCachable
    {
        // This is session cache keyed by SNI e.g. TargetHost
        private Dictionary<string, IntPtr>? _sslSessions;
        private GCHandle _gch;
 
        // SSL_CTX handles are cached, so we need to keep track of the
        // number of times a handle is being used. Once we decide to dispose the handle,
        // we set the _rentCount to -1.
        private volatile int _rentCount;
 
        public SafeSslContextHandle()
            : base(IntPtr.Zero, true)
        {
        }
 
        internal SafeSslContextHandle(IntPtr handle, bool ownsHandle)
            : base(handle, ownsHandle)
        {
        }
 
        public override bool IsInvalid
        {
            get { return handle == IntPtr.Zero; }
        }
 
        public bool TryAddRentCount()
        {
            int oldCount;
 
            do
            {
                oldCount = _rentCount;
                if (oldCount < 0)
                {
                    // The handle is already disposed.
                    return false;
                }
            } while (Interlocked.CompareExchange(ref _rentCount, oldCount + 1, oldCount) != oldCount);
 
            return true;
        }
 
        public bool TryMarkForDispose()
        {
            return Interlocked.CompareExchange(ref _rentCount, -1, 0) == 0;
        }
 
        protected override void Dispose(bool disposing)
        {
            if (Interlocked.Decrement(ref _rentCount) < 0)
            {
                // _rentCount is 0 if the handle was never rented (e.g. failure during creation),
                // and is -1 when evicted from cache.
                base.Dispose(disposing);
            }
        }
 
        protected override bool ReleaseHandle()
        {
            if (_sslSessions != null)
            {
                // The SSL_CTX is ref counted and may not immediately die when we call SslCtxDestroy()
                // Since there is no relation between SafeSslContextHandle and SafeSslHandle `this` can be release
                // while we still have SSL session using it.
                Interop.Ssl.SslCtxSetData(handle, IntPtr.Zero);
 
                lock (_sslSessions)
                {
                    foreach (IntPtr session in _sslSessions.Values)
                    {
                        Interop.Ssl.SessionFree(session);
                    }
 
                    _sslSessions.Clear();
                }
 
                Debug.Assert(_gch.IsAllocated);
                _gch.Free();
            }
 
            Interop.Ssl.SslCtxDestroy(handle);
            SetHandle(IntPtr.Zero);
 
            return true;
        }
 
        internal void EnableSessionCache()
        {
            Debug.Assert(_sslSessions == null);
 
            _sslSessions = new Dictionary<string, IntPtr>();
            _gch = GCHandle.Alloc(this);
            Debug.Assert(_gch.IsAllocated);
            // This is needed so we can find the handle from session in SessionRemove callback.
            Interop.Ssl.SslCtxSetData(this, (IntPtr)_gch);
        }
 
        internal bool TryAddSession(IntPtr namePtr, IntPtr session)
        {
            Debug.Assert(_sslSessions != null && session != IntPtr.Zero);
 
            if (_sslSessions == null || namePtr == IntPtr.Zero)
            {
                return false;
            }
 
            string? targetName = Marshal.PtrToStringUTF8(namePtr);
            Debug.Assert(targetName != null);
 
            if (!string.IsNullOrEmpty(targetName))
            {
                // We do this only for lookup in RemoveSession.
                // Since this is part of cache manipulation and no function impact it is done here.
                // This will use strdup() so it is safe to pass in raw pointer.
                Interop.Ssl.SessionSetHostname(session, namePtr);
 
                IntPtr oldSession = IntPtr.Zero;
 
                lock (_sslSessions)
                {
                    if (!_sslSessions.TryAdd(targetName, session))
                    {
                        // session to this target host exists, replace it
                        _sslSessions.Remove(targetName, out oldSession);
                        bool added = _sslSessions.TryAdd(targetName, session);
                        Debug.Assert(added);
                    }
                }
 
                if (oldSession != IntPtr.Zero)
                {
                    // remove old session also from the internal OpenSSL cache
                    // and drop reference count. Since SSL_CTX_remove_session
                    // will call session_remove_cb, we need to do this outside
                    // of _sslSessions lock to avoid deadlock with another thread
                    // which could be holding SSL_CTX lock and trying to acquire
                    // _sslSessions lock.
                    Interop.Ssl.SslCtxRemoveSession(this, oldSession);
                    Interop.Ssl.SessionFree(oldSession);
                }
 
                return true;
            }
 
            return false;
        }
 
        internal void RemoveSession(IntPtr namePtr, IntPtr session)
        {
            Debug.Assert(_sslSessions != null);
 
            string? targetName = Marshal.PtrToStringUTF8(namePtr);
            Debug.Assert(targetName != null);
 
            if (_sslSessions != null && targetName != null)
            {
                IntPtr oldSession = IntPtr.Zero;
                bool removed = false;
                lock (_sslSessions)
                {
                    if (_sslSessions.TryGetValue(targetName, out IntPtr existingSession) && existingSession == session)
                    {
                        removed = _sslSessions.Remove(targetName, out oldSession);
                    }
                }
 
                if (removed)
                {
                    // It seems like we may be called more than once. Since we grabbed only one refference
                    // when added to Dictionary, we will also drop exactly one when removed.
                    Interop.Ssl.SessionFree(oldSession);
                }
 
            }
        }
 
        internal bool TrySetSession(SafeSslHandle sslHandle, string name)
        {
            Debug.Assert(_sslSessions != null);
 
            if (_sslSessions == null || string.IsNullOrEmpty(name))
            {
                return false;
            }
 
            // even if we don't have matching session, we can get new one and we need
            // way how to link SSL back to `this`.
            Debug.Assert(Interop.Ssl.SslGetData(sslHandle) == IntPtr.Zero);
            Interop.Ssl.SslSetData(sslHandle, (IntPtr)_gch);
 
            lock (_sslSessions)
            {
                if (_sslSessions.TryGetValue(name, out IntPtr session))
                {
                    // This will increase reference count on the session as needed.
                    // We need to hold lock here to prevent session being deleted before the call is done.
                    Interop.Ssl.SslSetSession(sslHandle, session);
                    return true;
                }
            }
 
            return false;
        }
    }
}