File: System\Net\Security\TlsContext.cs
Web Access
Project: src\runtime\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.Diagnostics;
using System.Diagnostics.CodeAnalysis;

namespace System.Net.Security
{
    /// <summary>
    /// Long-lived TLS configuration. Wraps an <see cref="SslAuthenticationOptions"/>
    /// constructed from either <see cref="SslClientAuthenticationOptions"/> or
    /// <see cref="SslServerAuthenticationOptions"/>. Role (client vs. server) is
    /// determined by which factory is used.
    /// </summary>
    /// <remarks>
    /// <para>
    /// Holds the resolved options bag. Session caches are reused on supported
    /// platforms; each <see cref="TlsSession"/> gets its own native context
    /// allocated lazily on the first handshake call.
    /// </para>
    /// <para>
    /// Lifetime: it is safe to dispose the <see cref="TlsContext"/> while
    /// <see cref="TlsSession"/> instances created from it are still in use.
    /// The underlying native TLS context (e.g. OpenSSL <c>SSL_CTX</c>, SChannel
    /// credentials handle) is reference-counted at the native layer, so each
    /// live session keeps it alive until the session itself is disposed.
    /// After the context is disposed, however, attempts to create new sessions
    /// from it throw <see cref="ObjectDisposedException"/>.
    /// </para>
    /// </remarks>
    [Experimental(Experimentals.LowLevelTlsDiagId, UrlFormat = Experimentals.SharedUrlFormat)]
    public sealed partial class TlsContext : IDisposable
    {
        // Non-readonly so Dispose can null it out; keeping the reference alive would
        // otherwise pin the (potentially large) options bag until TlsContext itself
        // is collected. In wedge mode the bag is owned by SslStream and Dispose
        // only forgets it; in owned mode Dispose also disposes the bag itself.
        private SslAuthenticationOptions? _options;
        // True when this context wraps an SslStream-owned options bag (wedge mode):
        // sessions share the same bag by reference and TlsContext does not dispose it,
        // does not allocate its own native context, and defers cred-handle lifetime to
        // the wrapper. False for standalone contexts created via Create(...).
        private readonly bool _isWedge;
        private readonly bool _templateHasServerOptions;

        // SChannel credentials handle (an SSPI CredHandle from AcquireCredentialsHandle).
        // Owned by TlsContext so it can be shared across multiple TlsSession instances.
        // In wedge mode (WrapShared) SslStream owns the lifetime and we skip disposing
        // here to avoid double-free. Stays null on Unix — the OpenSSL SSL_CTX equivalent
        // lives in TlsContext.OpenSsl.cs.
        internal SafeFreeCredentials? CredentialsHandle;

        private TlsContext(SslAuthenticationOptions options, bool isWedge, bool templateHasServerOptions)
        {
            _options = options;
            _isWedge = isWedge;
            _templateHasServerOptions = templateHasServerOptions;
        }

        internal SslAuthenticationOptions Options => _options ?? throw new ObjectDisposedException(nameof(TlsContext));

        // Internal accessor for the obsolete EncryptionPolicy carried in options. Not exposed
        // publicly: a brand-new type should not re-publish a SYSLIB0040-obsolete concept. The
        // setting is honored at handshake time via the options bag; internal consumers that
        // need to introspect it (e.g. SslStream when re-platformed on TlsSession) read it here.
        internal EncryptionPolicy EncryptionPolicy => Options.EncryptionPolicy;

        // True when sessions should reuse the context's options bag directly (wedge mode).
        // False when each session must take a private clone before mutating any field.
        internal bool ShareOptions => _isWedge;

        // True if the template was constructed with non-null server options. Sessions seed
        // their own per-session HasServerOptions from this and flip it in SetContext.
        internal bool TemplateHasServerOptions => _templateHasServerOptions;

        // Returns a per-session options bag. For normal contexts each call returns a fresh
        // clone of the template so session-scoped mutations (TargetHost, SafeSslHandle,
        // resolved server cert, ...) don't leak between sessions. In wedge mode the bag is
        // owned by SslStream and we hand it out by reference. On platforms that own a
        // long-lived native context (e.g. OpenSSL SSL_CTX), the platform partial stamps it
        // onto the returned bag so the PAL can reuse it across sessions.
        internal SslAuthenticationOptions CreateSessionOptions()
        {
            SslAuthenticationOptions options = Options;
            SslAuthenticationOptions sessionOptions = _isWedge ? options : options.Clone();
            sessionOptions.ForceSyncPal = true;
            AttachSharedNativeContext(sessionOptions);
            return sessionOptions;
        }

        // Platform hook: lets the OpenSSL partial attach the TlsContext-owned SSL_CTX to
        // the per-session options bag. No-op on Windows (which uses CredentialsHandle) and
        // on macOS/iOS/Android (no reusable native context to share).
        partial void AttachSharedNativeContext(SslAuthenticationOptions sessionOptions);

        // Platform hook: lets the OpenSSL partial dispose the owned SSL_CTX. No-op elsewhere.
        partial void DisposeNativeContext();

        internal bool IsServer => Options.IsServer;

        /// <summary>
        /// Creates a server-side TLS context.
        /// </summary>
        /// <param name="options">
        /// The server authentication options. May be a default-constructed instance
        /// (no server certificate, no <see cref="SslServerAuthenticationOptions.ServerCertificateSelectionCallback"/>)
        /// to defer server configuration: the first <see cref="TlsBufferSession.Handshake"/>
        /// call on a session built from that context returns
        /// <see cref="TlsOperationStatus.NeedsTlsContext"/> with
        /// <see cref="TlsSession.ClientHelloInfo"/> populated; the caller must then
        /// invoke <see cref="TlsSession.SetContext"/> before continuing the
        /// handshake. Useful for SNI-based options selection that involves I/O.
        /// </param>
        /// <returns>A new server-side <see cref="TlsContext"/>.</returns>
        /// <exception cref="ArgumentNullException"><paramref name="options"/> is <see langword="null"/>.</exception>
        public static TlsContext CreateServer(SslServerAuthenticationOptions options)
        {
            ArgumentNullException.ThrowIfNull(options);
            SslAuthenticationOptions bag = new SslAuthenticationOptions();
            bool hasServerCredentials =
                options.ServerCertificate != null ||
                options.ServerCertificateContext != null ||
                options.ServerCertificateSelectionCallback != null;

            if (!hasServerCredentials)
            {
                // Deferred: caller will resolve the credential from SNI and hand back
                // a completed TlsContext via TlsSession.SetContext.
                bag.IsServer = true;
                return new TlsContext(bag, isWedge: false, templateHasServerOptions: false);
            }

            bag.UpdateOptions(options);
            return new TlsContext(bag, isWedge: false, templateHasServerOptions: true);
        }

        /// <summary>
        /// Creates a client-side TLS context.
        /// </summary>
        /// <param name="options">The client authentication options.</param>
        /// <returns>A new client-side <see cref="TlsContext"/>.</returns>
        /// <exception cref="ArgumentNullException"><paramref name="options"/> is <see langword="null"/>.</exception>
        /// <remarks>
        /// Peer certificate validation always runs outside the TLS state machine: after the
        /// handshake reaches the point at which the peer cert is available, <see cref="TlsBufferSession.Handshake"/>
        /// returns <see cref="TlsOperationStatus.NeedsCertificateValidation"/> and the caller
        /// must record a result via <see cref="TlsSession.SetRemoteCertificateValidationResult(System.Net.Security.SslPolicyErrors)"/>
        /// or <see cref="TlsSession.AcceptWithDefaultValidation"/>. Any
        /// <see cref="SslClientAuthenticationOptions.RemoteCertificateValidationCallback"/> set on
        /// <paramref name="options"/> is invoked only by <see cref="TlsSession.AcceptWithDefaultValidation"/>.
        /// </remarks>
        public static TlsContext CreateClient(SslClientAuthenticationOptions options)
        {
            ArgumentNullException.ThrowIfNull(options);
            SslAuthenticationOptions bag = new SslAuthenticationOptions();
            bag.UpdateOptions(options);
            return new TlsContext(bag, isWedge: false, templateHasServerOptions: false);
        }

        // Used by SslStream's TlsSession wedge: share the existing options bag so
        // SNI / client-cert selection results made by SslStream are visible to the
        // TlsSession-driven PAL calls, and to avoid double Dispose on the bag.
        internal static TlsContext WrapShared(SslAuthenticationOptions sharedOptions)
        {
            Debug.Assert(sharedOptions != null);
            return new TlsContext(sharedOptions, isWedge: true, templateHasServerOptions: sharedOptions.IsServer);
        }

        public void Dispose()
        {
            if (_options is null)
            {
                return;
            }

            if (!_isWedge)
            {
                CredentialsHandle?.Dispose();
                CredentialsHandle = null;
                DisposeNativeContext();
                _options.Dispose();
            }

            // In wedge mode the options bag is owned by SslStream and we must not
            // dispose it; but we do drop the reference so a disposed TlsContext no
            // longer keeps the (possibly large) bag alive.
            _options = null;
        }
    }
}