SslStream su Windows: perché non puoi usare OpenSSL (e cosa fare davvero)

Molti sviluppatori .NET chiedono se sia possibile obbligare la classe SslStream a usare OpenSSL anche su Windows. La risposta breve è “no”: su Windows SslStream si appoggia sempre a Schannel, il provider TLS di sistema. Qui trovi ciò che è supportato, le alternative praticabili e le conseguenze architetturali.

Indice

Problema posto

L’utente desidera sapere se, in ambiente Windows, sia possibile far sì che la classe .NET SslStream utilizzi esplicitamente OpenSSL anziché il provider TLS di sistema (Schannel).

Risposta sintetica

Cosa si può fare?Spiegazione
Non è possibile cambiare provider da SslStreamSslStream è un wrapper sullo stack TLS del sistema operativo: su Windows usa sempre Schannel; su Linux usa OpenSSL; su macOS usa l’implementazione Apple (Secure Transport/Network.framework). L’API non offre alcun interruttore per forzare OpenSSL.
Soluzione alternativa (custom layer)Se serve OpenSSL a tutti i costi, occorre scrivere o adottare un wrapper nativo su OpenSSL (libssl) e richiamarlo da .NET via P/Invoke o C++/CLI, esponendo un’interfaccia simile a SslStream.
Conseguenze• Maggior complessità di sviluppo e manutenzione (codice nativo, aggiornamenti di OpenSSL).
• Perdita dell’integrazione con lo store certificati e le policy di sicurezza di Windows.
• Occorre gestire manualmente patch, dipendenze e compatibilità binaria di OpenSSL.

Come funziona davvero SslStream per piattaforma

Per progettare correttamente è utile capire che SslStream non implementa TLS in managed code, ma demanda tutto a librerie native del sistema:

PiattaformaProvider TLS usato da SslStreamNote chiave
WindowsSchannel (SChannel / Secur32)Integrazione nativa con lo store certificati di Windows, smart‑card/CSP/KSP, policy di gruppo e Windows Update per le patch di sicurezza.
LinuxOpenSSLVersione e funzionalità dipendono da libssl presente a runtime (es. 1.1.1 o 3.x). Permette API come CipherSuitesPolicy.
macOSApple TLS (Secure Transport / Network.framework)Gestione certificati tramite Keychain; il set di feature segue le API Apple, non OpenSSL.
iOS/tvOSApple TLSStesso discorso di macOS.
AndroidProvider TLS di piattaforma (BoringSSL/OpenJDK)Dipende dallo stack TLS del dispositivo/SDK.

Punto chiave: non esiste alcuna opzione in SslClientAuthenticationOptions o SslServerAuthenticationOptions che consenta di “forzare OpenSSL” quando l’app gira su Windows. È una scelta di runtime legata alla piattaforma.

Perché di solito Schannel è la scelta consigliata su Windows

  • Patch tramite Windows Update: lo stack TLS di sistema viene aggiornato centralmente senza che l’applicazione debba distribuire librerie native.
  • Supporto TLS 1.3 su versioni recenti: Windows 11 e Windows Server recenti includono TLS 1.3 in Schannel.
  • Integrazione enterprise: accesso allo store certificati macchina/utente (My, Root, CA), smart‑card, TPM, criteri di gruppo, enforcement FIPS, audit centralizzati.
  • Hardening coerente: le stesse policy (cifrari, protocolli, curve) si applicano a tutte le app che usano Schannel, semplificando la compliance.

Quando OpenSSL può essere un requisito

  • Uniformità cross‑platform: si vuole lo stesso comportamento di TLS in tutti gli ambienti, compreso Windows, ad esempio per controllare puntualmente le suite di cifratura o per sfruttare feature specifiche di OpenSSL.
  • Compatibilità con sistemi legacy o speciali: il peer richiede estensioni o suite non esposte da Schannel su determinate versioni di Windows.
  • Controllo granulare della cipher list da codice: su Linux SslStream + OpenSSL consente l’uso di CipherSuitesPolicy (laddove supportato); su Windows la selezione dei cifrari è gestita a livello OS.
  • Distribuzione contenuta in container: nei container Linux spesso si preferisce portare con sé una versione specifica di OpenSSL; per coerenza si potrebbe volere lo stesso approccio su Windows (accettando i costi correlati).

Alternative tecniche per usare OpenSSL da .NET su Windows

Dal momento che non si può “convincere” SslStream a usare OpenSSL su Windows, l’unica via è creare un layer personalizzato che esponga un flusso TLS compatibile con le tue esigenze e internamente chiami OpenSSL.

Opzione A — Wrapper nativo con C API + P/Invoke

È l’approccio più portabile. Si realizza una piccola DLL C/C++ che incapsula OpenSSL e presenta poche funzioni C stabili. In C# si usa DllImport per richiamarle.

/ tls_shim.h — header della tua DLL /
#ifdef _WIN32
#define TLSAPI _declspec(dllexport)
#else
#define TLS_API
#endif

#include <stdint.h>
#include <stddef.h>

typedef void* tlsctxt;
typedef void* tlsconnt;

typedef enum { TLSOK = 0, TLSERR = -1 } tls_status;

TLSAPI tlsctxt tlsctxcreateclient(const char* capem, sizet ca_len);
TLSAPI tlsctxt tlsctxcreateserver(const char* certpem, sizet cert_len,
                                        const char* keypem, sizet key_len);
TLSAPI void      tlsctxfree(tlsctx_t ctx);

TLSAPI tlsconnt tlsconnwrapfd(tlsctxt ctx, intptrt socketfd, int is_server);
TLSAPI tlsstatus tlsconnhandshake(tlsconnt c);
TLSAPI int        tlsconnread(tlsconnt c, uint8t* buf, int len);
TLSAPI int        tlsconnwrite(tlsconnt c, const uint8t* buf, int len);
TLSAPI void       tlsconnclose(tlsconn_t c);
// P/Invoke minimale (C#)
internal static class TlsShim
{
    [DllImport("tls_shim", CallingConvention = CallingConvention.Cdecl)]
    public static extern IntPtr tlsctxcreate_client(byte[] caPem, UIntPtr caLen);

    [DllImport("tls_shim", CallingConvention = CallingConvention.Cdecl)]
    public static extern IntPtr tlsctxcreate_server(byte[] certPem, UIntPtr certLen,
                                                      byte[] keyPem, UIntPtr keyLen);

    [DllImport("tls_shim", CallingConvention = CallingConvention.Cdecl)]
    public static extern void tlsctxfree(IntPtr ctx);

    [DllImport("tls_shim", CallingConvention = CallingConvention.Cdecl)]
    public static extern IntPtr tlsconnwrap_fd(IntPtr ctx, IntPtr socketFd, int isServer);

    [DllImport("tls_shim", CallingConvention = CallingConvention.Cdecl)]
    public static extern int tlsconnhandshake(IntPtr conn);

    [DllImport("tls_shim", CallingConvention = CallingConvention.Cdecl)]
    public static extern int tlsconnread(IntPtr conn, byte[] buf, int len);

    [DllImport("tls_shim", CallingConvention = CallingConvention.Cdecl)]
    public static extern int tlsconnwrite(IntPtr conn, byte[] buf, int len);

    [DllImport("tls_shim", CallingConvention = CallingConvention.Cdecl)]
    public static extern void tlsconnclose(IntPtr conn);
}

Si può poi incapsulare tutto in una classe managed che espone un Stream compatibile:

public sealed class OpenSslStream : Stream
{
    private readonly Socket _socket;
    private readonly IntPtr _ctx;
    private readonly IntPtr _conn;

    public OpenSslStream(Socket socket, ReadOnlySpan<byte> caPem)
    {
        _socket = socket;
        ctx = TlsShim.tlsctxcreateclient(caPem.ToArray(), (UIntPtr)caPem.Length);
        conn = TlsShim.tlsconnwrapfd(_ctx, socket.Handle, 0);
        if (TlsShim.tlsconnhandshake(_conn) != 0)
            throw new IOException("Handshake TLS fallito");
    }

    public override int Read(byte[] buffer, int offset, int count)
    {
        var tmp = offset == 0 ? buffer : buffer.Skip(offset).Take(count).ToArray();
        int n = TlsShim.tlsconnread(_conn, tmp, count);
        if (n < 0) throw new IOException("Read TLS fallita");
        if (offset != 0) Array.Copy(tmp, 0, buffer, offset, n);
        return n;
    }

    public override void Write(byte[] buffer, int offset, int count)
    {
        var tmp = offset == 0 ? buffer : buffer.Skip(offset).Take(count).ToArray();
        int n = TlsShim.tlsconnwrite(_conn, tmp, count);
        if (n <= 0) throw new IOException("Write TLS fallita");
    }

    protected override void Dispose(bool disposing)
    {
        TlsShim.tlsconnclose(_conn);
        TlsShim.tlsctxfree(_ctx);
        _socket?.Dispose();
        base.Dispose(disposing);
    }

    #region Membri Stream non mostrati per brevità
    public override bool CanRead => true;
    public override bool CanSeek => false;
    public override bool CanWrite => true;
    public override long Length => throw new NotSupportedException();
    public override long Position { get => throw new NotSupportedException(); set => throw new NotSupportedException(); }
    public override void Flush() { }
    public override long Seek(long offset, SeekOrigin origin) => throw new NotSupportedException();
    public override void SetLength(long value) => throw new NotSupportedException();
    #endregion
}

Vantaggi: pieno controllo su OpenSSL e versioni. Svantaggi: occorre confezionare e firmare una DLL nativa; gestire ABI, threading, errori, e memory‑safety.

Opzione B — C++/CLI come ponte

Se il tuo progetto è Windows‑only, C++/CLI semplifica l’integrazione: puoi chiamare direttamente libssl e offrire una classe ref consumabile da C#. Il rovescio della medaglia è il vincolo al runtime .NET su Windows e una toolchain più complessa nei build server.

Opzione C — Wrapper già pronti

  • OpenSSL.NET / ManagedOpenSsl e simili: mettono a disposizione binding a libssl. Non sono drop‑in replacement di SslStream; richiedono comunque refactoring.
  • Alternative pure C# (es. BouncyCastle): implementano TLS in managed code, evitando DLL native; utile in ambienti con forti vincoli di distribuzione. Anche in questo caso non sostituiscono SslStream in modo trasparente.

Confronto pratico: restare su Schannel vs. forzare OpenSSL

AspettoSchannel (con SslStream)OpenSSL (wrapper custom)
DistribuzioneNessuna DLL extra; patch via OSDistribuire libssl/libcrypto e la tua DLL
Controllo cifrari da codiceLimitato/OS‑driven; CipherSuitesPolicy non applicabile su WindowsCompleto tramite API OpenSSL
CertificatiUsa lo store Windows, smart‑card, policy GPOFile PEM/PKCS#12 gestiti dall’app; integrazione GPO assente
Compliance/FIPSAllineata allo stato del sistemaRichiede modulo FIPS OpenSSL + configurazione corretta
PortabilitàOttima in ambiente WindowsBuona cross‑platform, ma con più manutenzione
Tempo di sviluppoMinimoSignificativo (interop, error handling, test)

Domande frequenti (FAQ)

Posso impostare OpenSSL tramite SslClientAuthenticationOptions?

No. Le opzioni controllano ALPN, certificati, callback di validazione, ecc., ma non possono sostituire il provider TLS della piattaforma.

La proprietà CipherSuitesPolicy funziona su Windows?

Su Windows viene ignorata (Schannel non offre quel controllo dal processo). È supportata quando SslStream usa OpenSSL (Linux) o piattaforme che consentono la scelta dei cifrari dall’applicazione.

Come seleziono i certificati client su Windows?

Tramite LocalCertificateSelectionCallback e gli store di Windows; con OpenSSL dovrai invece caricare/gestire i certificati da file (PEM/PFX) o da HSM via engine specifici.

E se mi serve TLS 1.3?

Su Windows è disponibile con Schannel nelle versioni recenti del sistema operativo. Su Linux con OpenSSL 1.1.1+ o 3.x è ugualmente disponibile. La scelta non dipende da SslStream ma dallo stack nativo sottostante.

Esempio: client TLS con SslStream su Windows

Questo esempio mostra le opzioni più comuni. Notare che non c’è alcun parametro per scegliere OpenSSL.

using System.Net.Security;
using System.Net.Sockets;
using System.Security.Authentication;
using System.Security.Cryptography.X509Certificates;

static async Task ConnectWithSslStreamAsync(string host, int port)
{
    using var tcp = new TcpClient();
    await tcp.ConnectAsync(host, port);

    using var ssl = new SslStream(
        tcp.GetStream(),
        leaveInnerStreamOpen: false,
        userCertificateValidationCallback: ValidateServerCertificate);

    var options = new SslClientAuthenticationOptions
    {
        TargetHost = host,
        EnabledSslProtocols = SslProtocols.Tls12 | SslProtocols.Tls13,
        CertificateRevocationCheckMode = X509RevocationMode.Online,
        ApplicationProtocols = new() { SslApplicationProtocol.Http2 }
        // Nessuna opzione per "OpenSSL vs Schannel": è scelto dalla piattaforma
    };

    await ssl.AuthenticateAsClientAsync(options);

    // Usa ssl.Read/Write come su uno stream qualunque...
}

static bool ValidateServerCertificate(object sender, X509Certificate? cert,
                                      X509Chain? chain, SslPolicyErrors errors)
{
    if (errors == SslPolicyErrors.None) return true;
    // Esempio di pinning o log degli errori:
    Console.Error.WriteLine($"Cert errors: {errors}");
    return false;
}

Esempio: creare un “quasi‑SslStream” su OpenSSL

Se devi a tutti i costi usare OpenSSL su Windows, puoi incapsulare la tua DLL in una classe che espone un’API a flusso. L’idea è fornire metodi compatibili con quelli di Stream e utilizzarla al posto di SslStream dove necessario.

public sealed class TlsClient : IDisposable
{
    private readonly Socket _socket;
    private readonly OpenSslStream _tls;

    public TlsClient(string host, int port, ReadOnlySpan<byte> caPem)
    {
        _socket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
        _socket.Connect(host, port);
        tls = new OpenSslStream(socket, caPem);
        // qui potresti esporre proprietà come PeerCertificate, Protocol, Cipher ecc.
    }

    public int Read(Span<byte> buffer) => _tls.Read(buffer.ToArray(), 0, buffer.Length);
    public void Write(ReadOnlySpan<byte> data) => _tls.Write(data.ToArray(), 0, data.Length);
    public void Dispose() { tls.Dispose(); socket.Dispose(); }
}

Nota: la classe sopra è volutamente semplificata. In produzione serviranno: gestione timeouts, half‑close, shutdown corretto, traduzione errori OpenSSL in eccezioni .NET, sincronizzazione I/O, cancellazione, logging strutturato, test di resilienza e fuzzing.

Considerazioni di sicurezza e manutenzione

  • Patch di sicurezza: con Schannel è l’OS ad aggiornare. Con OpenSSL devi backportare e ridistribuire. Prevedi un piano di aggiornamento e un canale di rilascio rapido.
  • FIPS/Compliance: verifica in anticipo le politiche interne. Se l’ambiente richiede FIPS, su Windows potresti dover seguire le impostazioni di sistema; con OpenSSL è necessario abilitare il provider/modulo FIPS certificato e configurarlo correttamente.
  • Gestione certificati: su Schannel puoi utilizzare lo store macchina/utente e smart‑card. Con OpenSSL dovrai gestire PEM/PFX, concatenazioni di chain, CRL/OCSP (stapling), trust store dedicati.
  • ALPN, SNI, 0‑RTT: verifica feature‑by‑feature. Alcune combinazioni dipendono dalla versione dello stack sottostante (Schannel/OpenSSL).
  • Prestazioni: i costi di contesto/handshake possono variare. Misura con il tuo carico reale (dimensione record, cipher, riutilizzo sessione, hardware con AES‑NI/ARMv8 Crypto).

Checklist decisionale

  1. Requisiti hard: esistono requisiti normativi o funzionali che Schannel non può soddisfare? Se no, resta su SslStream con Schannel.
  2. Distribuzione: sei pronto a distribuire e firmare una DLL nativa e a mantenerla nel tempo?
  3. Compatibilità: devi supportare piattaforme Windows eterogenee (client/server, versioni diverse)? In tal caso preferisci affidarti allo stack OS.
  4. Test: hai in place test di interoperabilità con i peer (OpenSSL, Schannel, Apple TLS), incluse varianti di cipher e protocolli?
  5. Observability: prevedi metriche e logging sufficienti per diagnosticare handshake, renegotiation vietata, resumption, errori di chain building.

Perché spesso conviene restare con SslStream su Windows

Per la maggior parte delle applicazioni line‑of‑business e dei servizi backend su Windows, Schannel offre tutto il necessario: protocolli moderni, supporto enterprise agli store di certificati, criteri centralizzati e un modello di manutenzione a basso attrito. Scegli un wrapper OpenSSL soltanto quando c’è un requisito non negoziabile che lo impone (compatibilità con dispositivi specifici, controllo fine della cipher list in runtime, necessità di test identici a Linux a byte‑level, ecc.).

Esempi di scenari concreti

  • Mutual TLS (mTLS) con smart‑card aziendali: Schannel è ideale perché sfrutta CSP/KSP e le policy di Windows. Con OpenSSL dovresti integrare engine PKCS#11 e gestire PIN e slot.
  • Forzare CHACHA20‑POLY1305 su host Windows datati: se lo stack OS non espone la suite desiderata, OpenSSL può offrirla, ma valuta l’impatto operativo e i rischi di compliance.
  • Controllo dei cifrari in A/B testing: su Linux puoi farlo via CipherSuitesPolicy. Per avere comportamento identico su Windows, serve un layer OpenSSL custom.

Bozza di piano di adozione di un wrapper OpenSSL

  1. Definizione dei requisiti: protocolli, suite, ALPN, mTLS, OCSP, 0‑RTT, FIPS.
  2. Scelta tecnica: C API + P/Invoke (più portabile) oppure C++/CLI (più semplice ma Windows‑bound).
  3. Packaging: versionare libssl/libcrypto, firma della DLL, criteri di caricamento sicuri (SafeDllSearchMode, percorsi).
  4. API managed: esporre un Stream o un’interfaccia simile a SslStream per minimizzare l’impatto sul codice esistente.
  5. Test di interoperabilità: OpenSSL <-> Schannel <-> Apple TLS; casi positivi/negativi, revoche, chain intermedie mancanti.
  6. Osservabilità: metriche su handshake time, errori, protocolli/cifrari negoziati, resumption rate.
  7. Piano di patch: come allineare rapidamente OpenSSL a fronte di CVE; canale di rilascio e rollback.

In sintesi

SslStream non può essere configurato per usare OpenSSL su Windows. La classe è un involucro che demanda allo stack TLS della piattaforma: Schannel su Windows, OpenSSL su Linux, Apple TLS su macOS/iOS. Se il requisito di usare OpenSSL è inderogabile, occorre creare o adottare un wrapper nativo e consumarlo via interop (P/Invoke o C++/CLI). Questa strada offre massimo controllo ma introduce complessità tecnica, oneri di manutenzione e perdita dell’integrazione “gratuita” con lo stack di sicurezza di Windows. Per la maggior parte degli scenari enterprise, rimanere su Schannel attraverso SslStream resta la scelta più semplice, sicura e conveniente.


Informazioni aggiuntive utili

Binding esistenti

OpenSSL.NET/ManagedOpenSsl e librerie simili offrono wrapper già pronti, ma non sono drop‑in replacement di SslStream; vanno integrati nel codice applicativo.

Alternative pure C#

BouncyCastle implementa TLS interamente in managed code; può essere usato quando si vuole evitare DLL native, ma anch’esso non sostituisce SslStream in modo trasparente.

Perché di solito Schannel è la scelta consigliata

  • Riceve aggiornamenti di sicurezza tramite Windows Update.
  • Supporta già TLS 1.3 sulle versioni recenti di Windows (11, Server 2022).
  • Integra automaticamente lo store certificati, le smart‑card, le policy di gruppo e i provider hardware.

Conclusione operativa: se oggi il tuo codice gira su Windows e usa SslStream, non cercare un interruttore per OpenSSL perché non c’è. Valuta invece se i motivi che ti spingono verso OpenSSL giustificano l’introduzione di un layer dedicato con le responsabilità che ne derivano. In caso contrario, Schannel rimane la soluzione più robusta e manutenibile.

Se ti serve un aiuto a trasformare i requisiti in una checklist tecnica o vuoi una revisione del design del wrapper, prendi spunto dal “Bozza di piano di adozione” qui sopra per impostare backlog e risk assessment.

Indice