File: src\libraries\Common\src\Interop\Unix\System.Security.Cryptography.Native\Interop.OpenSsl.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.Collections.Concurrent;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Diagnostics;
using System.Globalization;
using System.IO;
using System.Net;
using System.Net.Security;
using System.Runtime.InteropServices;
using System.Security.Authentication;
using System.Security.Authentication.ExtendedProtection;
using System.Security.Cryptography;
using System.Security.Cryptography.X509Certificates;
using Microsoft.Win32.SafeHandles;
 
internal static partial class Interop
{
    internal static partial class OpenSsl
    {
        private const string TlsCacheSizeCtxName = "System.Net.Security.TlsCacheSize";
        private const string TlsCacheSizeEnvironmentVariable = "DOTNET_SYSTEM_NET_SECURITY_TLSCACHESIZE";
        private const SslProtocols FakeAlpnSslProtocol = (SslProtocols)1;   // used to distinguish server sessions with ALPN
 
        private sealed class SafeSslContextCache : SafeHandleCache<SslContextCacheKey, SafeSslContextHandle> { }
 
        private static readonly SafeSslContextCache s_clientSslContexts = new();
 
        internal readonly struct SslContextCacheKey : IEquatable<SslContextCacheKey>
        {
            public readonly byte[]? CertificateThumbprint;
            public readonly SslProtocols SslProtocols;
 
            public SslContextCacheKey(SslProtocols sslProtocols, byte[]? certificateThumbprint)
            {
                SslProtocols = sslProtocols;
                CertificateThumbprint = certificateThumbprint;
            }
 
            public override bool Equals(object? obj) => obj is SslContextCacheKey key && Equals(key);
 
            public bool Equals(SslContextCacheKey other) =>
                SslProtocols == other.SslProtocols &&
                (CertificateThumbprint == null && other.CertificateThumbprint == null ||
                 CertificateThumbprint != null && other.CertificateThumbprint != null && CertificateThumbprint.AsSpan().SequenceEqual(other.CertificateThumbprint));
 
            public override int GetHashCode()
            {
                HashCode hash = default;
 
                hash.Add(SslProtocols);
                if (CertificateThumbprint != null)
                {
                    hash.AddBytes(CertificateThumbprint);
                }
 
                return hash.ToHashCode();
            }
        }
 
        #region internal methods
        internal static SafeChannelBindingHandle? QueryChannelBinding(SafeSslHandle context, ChannelBindingKind bindingType)
        {
            Debug.Assert(
                bindingType != ChannelBindingKind.Endpoint,
                "Endpoint binding should be handled by EndpointChannelBindingToken");
 
            SafeChannelBindingHandle? bindingHandle;
            switch (bindingType)
            {
                case ChannelBindingKind.Unique:
                    bindingHandle = new SafeChannelBindingHandle(bindingType);
                    QueryUniqueChannelBinding(context, bindingHandle);
                    break;
 
                default:
                    // Keeping parity with windows, we should return null in this case.
                    bindingHandle = null;
                    break;
            }
 
            return bindingHandle;
        }
 
        private static readonly int s_cacheSize = GetCacheSize();
 
        private static int GetCacheSize()
        {
            string? value = AppContext.GetData(TlsCacheSizeCtxName) as string ?? Environment.GetEnvironmentVariable(TlsCacheSizeEnvironmentVariable);
            if (!int.TryParse(value, CultureInfo.InvariantCulture, out int cacheSize))
            {
                cacheSize = -1;
            }
 
            return cacheSize;
        }
 
        // This is helper function to adjust requested protocols based on CipherSuitePolicy and system capability.
        private static SslProtocols CalculateEffectiveProtocols(SslAuthenticationOptions sslAuthenticationOptions)
        {
            // make sure low bit is not set since we use it in context dictionary to distinguish use with ALPN
            Debug.Assert((sslAuthenticationOptions.EnabledSslProtocols & FakeAlpnSslProtocol) == 0);
            SslProtocols protocols = sslAuthenticationOptions.EnabledSslProtocols & ~((SslProtocols)1);
 
            if (!Interop.Ssl.Capabilities.Tls13Supported)
            {
                if (protocols != SslProtocols.None &&
                    CipherSuitesPolicyPal.WantsTls13(protocols))
                {
                    protocols &= ~SslProtocols.Tls13;
                }
            }
            else if (CipherSuitesPolicyPal.WantsTls13(protocols) &&
                CipherSuitesPolicyPal.ShouldOptOutOfTls13(sslAuthenticationOptions.CipherSuitesPolicy, sslAuthenticationOptions.EncryptionPolicy))
            {
                if (protocols == SslProtocols.None)
                {
                    // we are using default settings but cipher suites policy says that TLS 1.3
                    // is not compatible with our settings (i.e. we requested no encryption or disabled
                    // all TLS 1.3 cipher suites)
#pragma warning disable SYSLIB0039 // TLS 1.0 and 1.1 are obsolete
                    protocols = SslProtocols.Tls | SslProtocols.Tls11 | SslProtocols.Tls12;
#pragma warning restore SYSLIB0039
                }
                else
                {
                    // user explicitly asks for TLS 1.3 but their policy is not compatible with TLS 1.3
                    throw new SslException(
                        SR.Format(SR.net_ssl_encryptionpolicy_notsupported, sslAuthenticationOptions.EncryptionPolicy));
                }
            }
 
            if (CipherSuitesPolicyPal.ShouldOptOutOfLowerThanTls13(sslAuthenticationOptions.CipherSuitesPolicy))
            {
                if (!CipherSuitesPolicyPal.WantsTls13(protocols))
                {
                    // We cannot provide neither TLS 1.3 or non TLS 1.3, user disabled all cipher suites
                    throw new SslException(
                        SR.Format(SR.net_ssl_encryptionpolicy_notsupported, sslAuthenticationOptions.EncryptionPolicy));
                }
 
                protocols = SslProtocols.Tls13;
            }
 
            return protocols;
        }
 
        internal static SafeSslContextHandle GetOrCreateSslContextHandle(SslAuthenticationOptions sslAuthenticationOptions, bool allowCached)
        {
            SslProtocols protocols = CalculateEffectiveProtocols(sslAuthenticationOptions);
 
            if (!allowCached)
            {
                return AllocateSslContext(sslAuthenticationOptions, protocols, allowCached);
            }
 
            if (sslAuthenticationOptions.IsClient)
            {
                var key = new SslContextCacheKey(protocols, sslAuthenticationOptions.CertificateContext?.TargetCertificate.GetCertHash(HashAlgorithmName.SHA256));
                return s_clientSslContexts.GetOrCreate(key, static (args) =>
                {
                    var (sslAuthOptions, protocols, allowCached) = args;
                    return AllocateSslContext(sslAuthOptions, protocols, allowCached);
                }, (sslAuthenticationOptions, protocols, allowCached));
            }
 
            // cache in SslStreamCertificateContext is bounded and there is no eviction
            // so the handle should always be valid,
 
            bool hasAlpn = sslAuthenticationOptions.ApplicationProtocols != null && sslAuthenticationOptions.ApplicationProtocols.Count != 0;
 
            SslProtocols serverCacheKey = protocols | (hasAlpn ? FakeAlpnSslProtocol : SslProtocols.None);
            if (!sslAuthenticationOptions.CertificateContext!.SslContexts!.TryGetValue(serverCacheKey, out SafeSslContextHandle? handle))
            {
                // not found in cache, create and insert
                handle = AllocateSslContext(sslAuthenticationOptions, protocols, allowCached);
 
                SafeSslContextHandle cached = sslAuthenticationOptions.CertificateContext!.SslContexts!.GetOrAdd(serverCacheKey, handle);
 
                if (handle != cached)
                {
                    // lost the race, another thread created the SSL_CTX meanwhile, prefer the cached one
                    handle.Dispose();
                    Debug.Assert(handle.IsClosed);
                    handle = cached;
                }
            }
 
            Debug.Assert(!handle.IsClosed);
            handle.TryAddRentCount();
            return handle;
        }
 
        // This essentially wraps SSL_CTX* aka SSL_CTX_new + setting
        internal static unsafe SafeSslContextHandle AllocateSslContext(SslAuthenticationOptions sslAuthenticationOptions, SslProtocols protocols, bool enableResume)
        {
            // Always use SSLv23_method, regardless of protocols.  It supports negotiating to the highest
            // mutually supported version and can thus handle any of the set protocols, and we then use
            // SetProtocolOptions to ensure we only allow the ones requested.
            SafeSslContextHandle sslCtx = Ssl.SslCtxCreate(Ssl.SslMethods.SSLv23_method);
            try
            {
                if (sslCtx.IsInvalid)
                {
                    throw CreateSslException(SR.net_allocate_ssl_context_failed);
                }
 
                Ssl.SslCtxSetProtocolOptions(sslCtx, protocols);
 
                if (sslAuthenticationOptions.EncryptionPolicy != EncryptionPolicy.RequireEncryption)
                {
                    // Sets policy and security level
                    if (!Ssl.SetEncryptionPolicy(sslCtx, sslAuthenticationOptions.EncryptionPolicy))
                    {
                        throw new SslException(SR.Format(SR.net_ssl_encryptionpolicy_notsupported, sslAuthenticationOptions.EncryptionPolicy));
                    }
                }
 
                ReadOnlySpan<byte> cipherList = CipherSuitesPolicyPal.GetOpenSslCipherList(sslAuthenticationOptions.CipherSuitesPolicy, protocols, sslAuthenticationOptions.EncryptionPolicy);
                Debug.Assert(cipherList.IsEmpty || cipherList[^1] == 0);
 
                byte[]? cipherSuites = CipherSuitesPolicyPal.GetOpenSslCipherSuites(sslAuthenticationOptions.CipherSuitesPolicy, protocols, sslAuthenticationOptions.EncryptionPolicy);
                Debug.Assert(cipherSuites == null || (cipherSuites.Length >= 1 && cipherSuites[cipherSuites.Length - 1] == 0));
 
                fixed (byte* cipherListStr = cipherList)
                fixed (byte* cipherSuitesStr = cipherSuites)
                {
                    if (!Ssl.SslCtxSetCiphers(sslCtx, cipherListStr, cipherSuitesStr))
                    {
                        Crypto.ErrClearError();
                        throw new PlatformNotSupportedException(SR.Format(SR.net_ssl_encryptionpolicy_notsupported, sslAuthenticationOptions.EncryptionPolicy));
                    }
                }
 
                // The logic in SafeSslHandle.Disconnect is simple because we are doing a quiet
                // shutdown (we aren't negotiating for session close to enable later session
                // restoration).
                //
                // If you find yourself wanting to remove this line to enable bidirectional
                // close-notify, you'll probably need to rewrite SafeSslHandle.Disconnect().
                // https://www.openssl.org/docs/manmaster/ssl/SSL_shutdown.html
                Ssl.SslCtxSetQuietShutdown(sslCtx);
 
                if (enableResume)
                {
                    if (sslAuthenticationOptions.IsServer)
                    {
                        Span<byte> contextId = stackalloc byte[32];
                        RandomNumberGenerator.Fill(contextId);
                        Ssl.SslCtxSetCaching(sslCtx, 1, s_cacheSize, contextId.Length, contextId, null, null);
                    }
                    else
                    {
                        int result = Ssl.SslCtxSetCaching(sslCtx, 1, s_cacheSize, 0, null, &NewSessionCallback, &RemoveSessionCallback);
                        Debug.Assert(result == 1);
                        sslCtx.EnableSessionCache();
                    }
                }
                else
                {
                    Ssl.SslCtxSetCaching(sslCtx, 0, -1, 0, null, null, null);
                }
 
                if (sslAuthenticationOptions.IsServer && sslAuthenticationOptions.ApplicationProtocols != null && sslAuthenticationOptions.ApplicationProtocols.Count != 0)
                {
                    Interop.Ssl.SslCtxSetAlpnSelectCb(sslCtx, &AlpnServerSelectCallback, IntPtr.Zero);
                }
 
                if (sslAuthenticationOptions.CertificateContext != null && sslAuthenticationOptions.IsServer)
                {
                    SetSslCertificate(sslCtx, sslAuthenticationOptions.CertificateContext.CertificateHandle, sslAuthenticationOptions.CertificateContext.KeyHandle);
 
                    if (sslAuthenticationOptions.CertificateContext.IntermediateCertificates.Count > 0)
                    {
                        if (!Ssl.AddExtraChainCertificates(sslCtx, sslAuthenticationOptions.CertificateContext.IntermediateCertificates))
                        {
                            throw CreateSslException(SR.net_ssl_use_cert_failed);
                        }
                    }
 
                    if (sslAuthenticationOptions.CertificateContext.OcspStaplingAvailable)
                    {
                        Ssl.SslCtxSetDefaultOcspCallback(sslCtx);
                    }
                }
                if (SslKeyLogger.IsEnabled)
                {
                    Ssl.SslCtxSetKeylogCallback(sslCtx, &KeyLogCallback);
                }
            }
            catch
            {
                sslCtx.Dispose();
                throw;
            }
 
            return sslCtx;
        }
 
        internal static void UpdateClientCertificate(SafeSslHandle ssl, SslAuthenticationOptions sslAuthenticationOptions)
        {
            // Disable certificate selection callback. We either got certificate or we will try to proceed without it.
            Interop.Ssl.SslSetClientCertCallback(ssl, 0);
 
            if (sslAuthenticationOptions.CertificateContext == null)
            {
                return;
            }
 
            Debug.Assert(sslAuthenticationOptions.CertificateContext.CertificateHandle != null);
            Debug.Assert(sslAuthenticationOptions.CertificateContext.KeyHandle != null);
 
            int retVal = Ssl.SslUseCertificate(ssl, sslAuthenticationOptions.CertificateContext.CertificateHandle);
            if (1 != retVal)
            {
                throw CreateSslException(SR.net_ssl_use_cert_failed);
            }
 
            retVal = Ssl.SslUsePrivateKey(ssl, sslAuthenticationOptions.CertificateContext.KeyHandle);
            if (1 != retVal)
            {
                throw CreateSslException(SR.net_ssl_use_private_key_failed);
            }
 
            if (sslAuthenticationOptions.CertificateContext.IntermediateCertificates.Count > 0)
            {
                if (!Ssl.AddExtraChainCertificates(ssl, sslAuthenticationOptions.CertificateContext.IntermediateCertificates))
                {
                    throw CreateSslException(SR.net_ssl_use_cert_failed);
                }
            }
        }
 
        // This essentially wraps SSL* SSL_new()
        internal static SafeSslHandle AllocateSslHandle(SslAuthenticationOptions sslAuthenticationOptions)
        {
            SafeSslHandle? sslHandle = null;
            bool cacheSslContext = sslAuthenticationOptions.AllowTlsResume && !SslStream.DisableTlsResume && sslAuthenticationOptions.EncryptionPolicy == EncryptionPolicy.RequireEncryption && sslAuthenticationOptions.CipherSuitesPolicy == null;
 
            if (cacheSslContext)
            {
                if (sslAuthenticationOptions.IsClient)
                {
                    // We don't support client resume on old OpenSSL versions.
                    // We don't want to try on empty TargetName since that is our key.
                    // If we already have CertificateContext, then we know which cert the user wants to use and we can cache.
                    // The only client auth scenario where we can't cache is when user provides a cert callback and we don't know
                    // beforehand which cert will be used. and wan't to avoid resuming session created with different certificate.
                    if (!Interop.Ssl.Capabilities.Tls13Supported ||
                       string.IsNullOrEmpty(sslAuthenticationOptions.TargetHost) ||
                       (sslAuthenticationOptions.CertificateContext == null && sslAuthenticationOptions.CertSelectionDelegate != null))
                    {
                        cacheSslContext = false;
                    }
                }
                else
                {
                    // Server should always have certificate
                    Debug.Assert(sslAuthenticationOptions.CertificateContext != null);
                    if (sslAuthenticationOptions.CertificateContext == null ||
                       sslAuthenticationOptions.CertificateContext.SslContexts == null)
                    {
                        cacheSslContext = false;
                    }
                }
            }
 
            // We do not touch the SSL_CTX after we create and configure SSL
            // objects, and SSL object created later in this function will keep an
            // outstanding up-ref on SSL_CTX.
            //
            // For uncached SafeSslContextHandles, the handle will be disposed and closed.
            // Cached SafeSslContextHandles are returned with increaset rent count so that
            // Dispose() here will not close the handle.
            using SafeSslContextHandle sslCtxHandle = GetOrCreateSslContextHandle(sslAuthenticationOptions, cacheSslContext);
 
            GCHandle alpnHandle = default;
            try
            {
                sslHandle = SafeSslHandle.Create(sslCtxHandle, sslAuthenticationOptions.IsServer);
                Debug.Assert(sslHandle != null, "Expected non-null return value from SafeSslHandle.Create");
                if (sslHandle.IsInvalid)
                {
                    sslHandle.Dispose();
                    throw CreateSslException(SR.net_allocate_ssl_context_failed);
                }
 
                if (sslAuthenticationOptions.ApplicationProtocols != null && sslAuthenticationOptions.ApplicationProtocols.Count != 0)
                {
                    if (sslAuthenticationOptions.IsServer)
                    {
                        Debug.Assert(Interop.Ssl.SslGetData(sslHandle) == IntPtr.Zero);
                        alpnHandle = GCHandle.Alloc(sslAuthenticationOptions.ApplicationProtocols);
                        Interop.Ssl.SslSetData(sslHandle, GCHandle.ToIntPtr(alpnHandle));
                        sslHandle.AlpnHandle = alpnHandle;
                    }
                    else
                    {
                        if (Interop.Ssl.SslSetAlpnProtos(sslHandle, sslAuthenticationOptions.ApplicationProtocols) != 0)
                        {
                            throw CreateSslException(SR.net_alpn_config_failed);
                        }
                    }
                }
 
                if (sslAuthenticationOptions.IsClient)
                {
                    if (!string.IsNullOrEmpty(sslAuthenticationOptions.TargetHost) && !TargetHostNameHelper.IsValidAddress(sslAuthenticationOptions.TargetHost))
                    {
                        // Similar to windows behavior, set SNI on openssl by default for client context, ignore errors.
                        if (!Ssl.SslSetTlsExtHostName(sslHandle, sslAuthenticationOptions.TargetHost))
                        {
                            Crypto.ErrClearError();
                        }
 
                        if (cacheSslContext)
                        {
                            sslCtxHandle.TrySetSession(sslHandle, sslAuthenticationOptions.TargetHost);
 
                            // Maintain additional rent count for the context so
                            // that it is not evicted from the cache and future
                            // SSL objects can reuse it. This call should always
                            // succeed because already have increased rent count
                            // when getting the context from the cache
                            bool success = sslCtxHandle.TryAddRentCount();
                            Debug.Assert(success);
                            sslHandle.SslContextHandle = sslCtxHandle;
                        }
                    }
 
                    // relevant to TLS 1.3 only: if user supplied a client cert or cert callback,
                    // advertise that we are willing to send the certificate post-handshake.
                    if (sslAuthenticationOptions.CertificateContext != null ||
                        sslAuthenticationOptions.ClientCertificates?.Count > 0 ||
                        sslAuthenticationOptions.CertSelectionDelegate != null)
                    {
                        Ssl.SslSetPostHandshakeAuth(sslHandle, 1);
                    }
 
                    // Set client cert callback, this will interrupt the handshake with SecurityStatusPalErrorCode.CredentialsNeeded
                    // if server actually requests a certificate.
                    Ssl.SslSetClientCertCallback(sslHandle, 1);
                }
                else // sslAuthenticationOptions.IsServer
                {
                    if (sslAuthenticationOptions.RemoteCertRequired)
                    {
                        Ssl.SslSetVerifyPeer(sslHandle);
                    }
 
                    if (sslAuthenticationOptions.CertificateContext != null)
                    {
                        if (sslAuthenticationOptions.CertificateContext.Trust?._sendTrustInHandshake == true)
                        {
                            SslCertificateTrust trust = sslAuthenticationOptions.CertificateContext!.Trust!;
                            X509Certificate2Collection certList = (trust._trustList ?? trust._store!.Certificates);
 
                            Debug.Assert(certList != null);
                            Span<IntPtr> handles = certList.Count <= 256 ?
                                stackalloc IntPtr[256] :
                                new IntPtr[certList.Count];
 
                            for (int i = 0; i < certList.Count; i++)
                            {
                                handles[i] = certList[i].Handle;
                            }
 
                            if (!Ssl.SslAddClientCAs(sslHandle, handles.Slice(0, certList.Count)))
                            {
                                // The method can fail only when the number of cert names exceeds the maximum capacity
                                // supported by STACK_OF(X509_NAME) structure, which should not happen under normal
                                // operation.
                                Debug.Fail("Failed to add issuer to trusted CA list.");
                            }
                        }
 
                        byte[]? ocspResponse = sslAuthenticationOptions.CertificateContext.GetOcspResponseNoWaiting();
 
                        if (ocspResponse != null)
                        {
                            Ssl.SslStapleOcsp(sslHandle, ocspResponse);
                        }
                    }
                }
            }
            catch
            {
                if (alpnHandle.IsAllocated)
                {
                    alpnHandle.Free();
                }
 
                throw;
            }
 
            return sslHandle;
        }
 
        internal static SecurityStatusPal SslRenegotiate(SafeSslHandle sslContext, out byte[]? outputBuffer)
        {
            int ret = Interop.Ssl.SslRenegotiate(sslContext, out Ssl.SslErrorCode errorCode);
 
            outputBuffer = Array.Empty<byte>();
            if (ret != 1)
            {
                return new SecurityStatusPal(SecurityStatusPalErrorCode.InternalError, GetSslError(ret, errorCode));
            }
            return new SecurityStatusPal(SecurityStatusPalErrorCode.OK);
        }
 
        internal static SecurityStatusPalErrorCode DoSslHandshake(SafeSslHandle context, ReadOnlySpan<byte> input, ref ProtocolToken token)
        {
            token.Size = 0;
            Exception? handshakeException = null;
 
            if (input.Length > 0)
            {
                if (Ssl.BioWrite(context.InputBio!, ref MemoryMarshal.GetReference(input), input.Length) != input.Length)
                {
                    // Make sure we clear out the error that is stored in the queue
                    throw Crypto.CreateOpenSslCryptographicException();
                }
            }
 
            int retVal = Ssl.SslDoHandshake(context, out Ssl.SslErrorCode errorCode);
            if (retVal != 1)
            {
                if (errorCode == Ssl.SslErrorCode.SSL_ERROR_WANT_X509_LOOKUP)
                {
                    return SecurityStatusPalErrorCode.CredentialsNeeded;
                }
 
                if ((retVal != -1) || (errorCode != Ssl.SslErrorCode.SSL_ERROR_WANT_READ))
                {
                    Exception? innerError = GetSslError(retVal, errorCode);
 
                    // Handshake failed, but even if the handshake does not need to read, there may be an Alert going out.
                    // To handle that we will fall-through the block below to pull it out, and we will fail after.
                    handshakeException = new SslException(SR.Format(SR.net_ssl_handshake_failed_error, errorCode), innerError);
                }
            }
 
            int sendCount = Crypto.BioCtrlPending(context.OutputBio!);
            if (sendCount > 0)
            {
                token.EnsureAvailableSpace(sendCount);
                try
                {
                    sendCount = BioRead(context.OutputBio!, token.AvailableSpan, sendCount);
                }
                catch (Exception) when (handshakeException != null)
                {
                    // If we already have handshake exception, ignore any exception from BioRead().
                }
                finally
                {
                    if (sendCount <= 0)
                    {
                        // Make sure we clear out the error that is stored in the queue
                        Crypto.ErrClearError();
                        sendCount = 0;
                    }
                }
            }
 
            token.Size = sendCount;
 
            if (handshakeException != null)
            {
                throw handshakeException;
            }
 
            // in case of TLS 1.3 post-handshake authentication, SslDoHandhaske
            // may return SSL_ERROR_NONE while still expecting more data from
            // the client. Attempts to send app data in this state would result
            // in SSL_ERROR_WANT_READ from SslWrite, override the return status
            // to continue waiting for the rest of the TLS frames
            if (context.IsServer && token.Size == 0 && errorCode == Ssl.SslErrorCode.SSL_ERROR_NONE && Ssl.IsSslRenegotiatePending(context))
            {
                return SecurityStatusPalErrorCode.ContinueNeeded;
            }
 
            bool stateOk = Ssl.IsSslStateOK(context);
            if (stateOk)
            {
                context.MarkHandshakeCompleted();
            }
 
            return stateOk ? SecurityStatusPalErrorCode.OK : SecurityStatusPalErrorCode.ContinueNeeded;
        }
 
        internal static Ssl.SslErrorCode Encrypt(SafeSslHandle context, ReadOnlySpan<byte> input, ref ProtocolToken outToken)
        {
            int retVal = Ssl.SslWrite(context, ref MemoryMarshal.GetReference(input), input.Length, out Ssl.SslErrorCode errorCode);
 
            if (retVal != input.Length)
            {
                outToken.Size = 0;
                switch (errorCode)
                {
                    // indicate end-of-file
                    case Ssl.SslErrorCode.SSL_ERROR_ZERO_RETURN:
                    case Ssl.SslErrorCode.SSL_ERROR_WANT_READ:
                        break;
 
                    default:
                        throw new SslException(SR.Format(SR.net_ssl_encrypt_failed, errorCode), GetSslError(retVal, errorCode));
                }
            }
            else
            {
                int capacityNeeded = Crypto.BioCtrlPending(context.OutputBio!);
                outToken.EnsureAvailableSpace(capacityNeeded);
                retVal = BioRead(context.OutputBio!, outToken.AvailableSpan, capacityNeeded);
 
                if (retVal <= 0)
                {
                    // Make sure we clear out the error that is stored in the queue
                    Crypto.ErrClearError();
                    outToken.Size = 0;
                }
                else
                {
                    outToken.Size = retVal;
                }
            }
 
            return errorCode;
        }
 
        internal static int Decrypt(SafeSslHandle context, Span<byte> buffer, out Ssl.SslErrorCode errorCode)
        {
            BioWrite(context.InputBio!, buffer);
 
            int retVal = Ssl.SslRead(context, ref MemoryMarshal.GetReference(buffer), buffer.Length, out errorCode);
            if (retVal > 0)
            {
                return retVal;
            }
 
            switch (errorCode)
            {
                // indicate end-of-file
                case Ssl.SslErrorCode.SSL_ERROR_ZERO_RETURN:
                    break;
 
                case Ssl.SslErrorCode.SSL_ERROR_WANT_READ:
                    // update error code to renegotiate if renegotiate is pending, otherwise make it SSL_ERROR_WANT_READ
                    errorCode = Ssl.IsSslRenegotiatePending(context)
                        ? Ssl.SslErrorCode.SSL_ERROR_RENEGOTIATE
                        : Ssl.SslErrorCode.SSL_ERROR_WANT_READ;
                    break;
 
                case Ssl.SslErrorCode.SSL_ERROR_WANT_X509_LOOKUP:
                    // This happens in TLS 1.3 when server requests post-handshake authentication
                    // but no certificate is provided by client. We can process it the same way as
                    // renegotiation on older TLS versions
                    errorCode = Ssl.SslErrorCode.SSL_ERROR_RENEGOTIATE;
                    break;
 
                default:
                    throw new SslException(SR.Format(SR.net_ssl_decrypt_failed, errorCode), GetSslError(retVal, errorCode));
            }
 
            return 0;
        }
 
        internal static IntPtr GetPeerCertificate(SafeSslHandle context)
        {
            return Ssl.SslGetPeerCertificate(context);
        }
 
        internal static SafeSharedX509StackHandle GetPeerCertificateChain(SafeSslHandle context)
        {
            return Ssl.SslGetPeerCertChain(context);
        }
 
        #endregion
 
        #region private methods
 
        private static void QueryUniqueChannelBinding(SafeSslHandle context, SafeChannelBindingHandle bindingHandle)
        {
            bool sessionReused = Ssl.SslSessionReused(context);
            int certHashLength = context.IsServer ^ sessionReused ?
                                 Ssl.SslGetPeerFinished(context, bindingHandle.CertHashPtr, bindingHandle.Length) :
                                 Ssl.SslGetFinished(context, bindingHandle.CertHashPtr, bindingHandle.Length);
 
            if (0 == certHashLength)
            {
                throw CreateSslException(SR.net_ssl_get_channel_binding_token_failed);
            }
 
            bindingHandle.SetCertHashLength(certHashLength);
        }
 
#pragma warning disable IDE0060
        [UnmanagedCallersOnly]
        private static int VerifyClientCertificate(int preverify_ok, IntPtr x509_ctx_ptr)
        {
            // Full validation is handled after the handshake in VerifyCertificateProperties and the
            // user callback.  It's also up to those handlers to decide if a null certificate
            // is appropriate.  So just return success to tell OpenSSL that the cert is acceptable,
            // we'll process it after the handshake finishes.
            const int OpenSslSuccess = 1;
            return OpenSslSuccess;
        }
#pragma warning restore IDE0060
 
        [UnmanagedCallersOnly]
        private static unsafe int AlpnServerSelectCallback(IntPtr ssl, byte** outp, byte* outlen, byte* inp, uint inlen, IntPtr arg)
        {
            *outp = null;
            *outlen = 0;
            IntPtr sslData = Ssl.SslGetData(ssl);
 
            if (sslData == IntPtr.Zero)
            {
                return Ssl.SSL_TLSEXT_ERR_ALERT_FATAL;
            }
 
            GCHandle protocolHandle = GCHandle.FromIntPtr(sslData);
            if (!(protocolHandle.Target is List<SslApplicationProtocol> protocolList))
            {
                return Ssl.SSL_TLSEXT_ERR_ALERT_FATAL;
            }
 
            try
            {
                for (int i = 0; i < protocolList.Count; i++)
                {
                    var clientList = new Span<byte>(inp, (int)inlen);
                    while (clientList.Length > 0)
                    {
                        byte length = clientList[0];
                        Span<byte> clientProto = clientList.Slice(1, length);
                        if (clientProto.SequenceEqual(protocolList[i].Protocol.Span))
                        {
                            fixed (byte* p = &MemoryMarshal.GetReference(clientProto)) *outp = p;
                            *outlen = length;
                            return Ssl.SSL_TLSEXT_ERR_OK;
                        }
 
                        clientList = clientList.Slice(1 + length);
                    }
                }
            }
            catch
            {
                // No common application protocol was negotiated, set the target on the alpnHandle to null.
                // It is ok to clear the handle value here, this results in handshake failure, so the SslStream object is disposed.
                protocolHandle.Target = null;
 
                return Ssl.SSL_TLSEXT_ERR_ALERT_FATAL;
            }
 
            // No common application protocol was negotiated, set the target on the alpnHandle to null.
            // It is ok to clear the handle value here, this results in handshake failure, so the SslStream object is disposed.
            protocolHandle.Target = null;
 
            return Ssl.SSL_TLSEXT_ERR_ALERT_FATAL;
        }
 
        [UnmanagedCallersOnly]
        // Invoked from OpenSSL when new session is created.
        // We attached GCHandle to the SSL so we can find back SafeSslContextHandle holding the cache.
        // New session has refCount of 1.
        // If this function returns 0, OpenSSL will drop the refCount and discard the session.
        // If we return 1, the ownership is transferred to us and we will need to call SessionFree().
        private static unsafe int NewSessionCallback(IntPtr ssl, IntPtr session)
        {
            Debug.Assert(ssl != IntPtr.Zero);
            Debug.Assert(session != IntPtr.Zero);
 
            // remember if the session used a certificate, this information is used after
            // session resumption, the pointer is not being dereferenced and the refcount
            // is not going to be manipulated.
            IntPtr cert = Interop.Ssl.SslGetCertificate(ssl);
            Interop.Ssl.SslSessionSetData(session, cert);
 
            IntPtr ptr = Ssl.SslGetData(ssl);
            if (ptr != IntPtr.Zero)
            {
                GCHandle gch = GCHandle.FromIntPtr(ptr);
 
                SafeSslContextHandle? ctxHandle = gch.Target as SafeSslContextHandle;
                // There is no relation between SafeSslContextHandle and SafeSslHandle so the handle
                // may be released while the ssl session is still active.
                if (ctxHandle != null && ctxHandle.TryAddSession(Ssl.SslGetServerName(ssl), session))
                {
                    // offered session was stored in our cache.
                    return 1;
                }
            }
 
            // OpenSSL will destroy session.
            return 0;
        }
 
        [UnmanagedCallersOnly]
        private static unsafe void RemoveSessionCallback(IntPtr ctx, IntPtr session)
        {
            Debug.Assert(ctx != IntPtr.Zero && session != IntPtr.Zero);
 
            IntPtr ptr = Ssl.SslCtxGetData(ctx);
            if (ptr == IntPtr.Zero)
            {
                // Same as above, SafeSslContextHandle could be released while OpenSSL still holds reference.
                return;
            }
 
            GCHandle gch = GCHandle.FromIntPtr(ptr);
            SafeSslContextHandle? ctxHandle = gch.Target as SafeSslContextHandle;
            if (ctxHandle == null)
            {
                return;
            }
 
            IntPtr name = Ssl.SessionGetHostname(session);
            Debug.Assert(name != IntPtr.Zero);
            ctxHandle.RemoveSession(name, session);
        }
 
        [UnmanagedCallersOnly]
        private static unsafe void KeyLogCallback(IntPtr ssl, char* line)
        {
            ReadOnlySpan<byte> data = MemoryMarshal.CreateReadOnlySpanFromNullTerminated((byte*)line);
            SslKeyLogger.WriteLineRaw(data);
        }
 
        private static int BioRead(SafeBioHandle bio, Span<byte> buffer, int count)
        {
            Debug.Assert(count >= 0);
            Debug.Assert(buffer.Length >= count);
 
            int bytes = Crypto.BioRead(bio, buffer);
            if (bytes != count)
            {
                throw CreateSslException(SR.net_ssl_read_bio_failed_error);
            }
            return bytes;
        }
 
        private static void BioWrite(SafeBioHandle bio, ReadOnlySpan<byte> buffer)
        {
            int bytes = Ssl.BioWrite(bio, ref MemoryMarshal.GetReference(buffer), buffer.Length);
            if (bytes != buffer.Length)
            {
                throw CreateSslException(SR.net_ssl_write_bio_failed_error);
            }
        }
 
        private static Exception? GetSslError(int result, Ssl.SslErrorCode retVal)
        {
            Exception? innerError;
            switch (retVal)
            {
                case Ssl.SslErrorCode.SSL_ERROR_SYSCALL:
                    ErrorInfo lastErrno = Sys.GetLastErrorInfo();
                    // Some I/O error occurred
                    innerError =
                        Crypto.ErrPeekError() != 0 ? Crypto.CreateOpenSslCryptographicException() : // crypto error queue not empty
                        result == 0 ? new EndOfStreamException() : // end of file that violates protocol
                        result == -1 && lastErrno.Error != Error.SUCCESS ? new IOException(lastErrno.GetErrorMessage(), lastErrno.RawErrno) : // underlying I/O error
                        null; // no additional info available
                    break;
 
                case Ssl.SslErrorCode.SSL_ERROR_SSL:
                    // OpenSSL failure occurred.  The error queue contains more details, when building the exception the queue will be cleared.
                    innerError = Interop.Crypto.CreateOpenSslCryptographicException();
                    break;
 
                default:
                    // No additional info available.
                    innerError = null;
                    break;
            }
 
            return innerError;
        }
 
        private static void SetSslCertificate(SafeSslContextHandle contextPtr, SafeX509Handle certPtr, SafeEvpPKeyHandle keyPtr)
        {
            Debug.Assert(certPtr != null && !certPtr.IsInvalid);
            Debug.Assert(keyPtr != null && !keyPtr.IsInvalid);
 
            int retVal = Ssl.SslCtxUseCertificate(contextPtr, certPtr);
 
            if (1 != retVal)
            {
                throw CreateSslException(SR.net_ssl_use_cert_failed);
            }
 
            retVal = Ssl.SslCtxUsePrivateKey(contextPtr, keyPtr);
 
            if (1 != retVal)
            {
                throw CreateSslException(SR.net_ssl_use_private_key_failed);
            }
 
            //check private key
            retVal = Ssl.SslCtxCheckPrivateKey(contextPtr);
 
            if (1 != retVal)
            {
                throw CreateSslException(SR.net_ssl_check_private_key_failed);
            }
        }
 
        internal static SslException CreateSslException(string message)
        {
            // Capture last error to be consistent with CreateOpenSslCryptographicException
            ulong errorVal = Crypto.ErrPeekLastError();
            Crypto.ErrClearError();
            string msg = SR.Format(message, Marshal.PtrToStringUTF8(Crypto.ErrReasonErrorString(errorVal)));
            return new SslException(msg, (int)errorVal);
        }
 
        #endregion
 
        #region Internal class
 
        internal sealed class SslException : Exception
        {
            public SslException(string? inputMessage)
                : base(inputMessage)
            {
            }
 
            public SslException(string? inputMessage, Exception? ex)
                : base(inputMessage, ex)
            {
            }
 
            public SslException(string? inputMessage, int error)
                : this(inputMessage)
            {
                HResult = error;
            }
 
            public SslException(int error)
                : this(SR.Format(SR.net_generic_operation_failed, error))
            {
                HResult = error;
            }
        }
 
        #endregion
    }
}