Vous développez en .NET sous Windows et souhaitez imposer OpenSSL plutôt que Schannel pour le trafic TLS ? Voici un tour d’horizon clair, des solutions concrètes (avec exemples de code) et les compromis à connaître avant de choisir votre architecture.
Peut‑on obliger .SslStream
à utiliser OpenSSL sous Windows ?
Vue d’ensemble
Sous Windows, la pile TLS utilisée par System.Net.Security.SslStream
est Schannel (via SSPI/CNG). Il n’existe pas de sélecteur ni d’option d’exécution permettant de remplacer ce fournisseur par OpenSSL. Sur Linux, le runtime .NET s’appuie généralement sur OpenSSL. Sur macOS, l’implémentation s’appuie sur la pile TLS d’Apple (Apple TLS), pas sur OpenSSL.
À retenir : sur Windows,
.SslStream
→ Schannel, sans possibilité officielle de basculer vers OpenSSL. Pour utiliser OpenSSL, il faut sortir de.SslStream
(wrapper natif ou lib tierce), ou exécuter l’appli sur un OS où OpenSSL est le backend par défaut.
Réponse & solutions possibles
Piste | Description | Points clés |
---|---|---|
API .NET standard (impossible de changer de backend) | Sous Windows, .SslStream appelle obligatoirement Schannel. Aucun commutateur, variable d’environnement, AppContext switch ou option de projet ne permet de lui substituer OpenSSL. | Simplicité d’usage, intégration au magasin de certificats Windows. Respect des politiques de sécurité de l’OS (FIPS, stratégies groupe, suites autorisées). Pas de contrôle fin sur les suites/courbes hors configuration système. |
Écrire votre propre wrapper OpenSSL | Implémenter votre flux TLS managé au‑dessus de libssl /libcrypto via P/Invoke (ou via un fin shim C). Vous exposez une surface type Stream compatible (Read/Write/Authenticate). | Contrôle total : versions OpenSSL, suites, ALPN/SNI, paramètres ECDH. Coût élevé : gestion mémoire non managée, erreurs natives, mises à jour de sécurité. Responsabilité de la conformité (FIPS, durcissement, supply chain). |
Bibliothèques tierces .NET | Adopter une lib qui intègre OpenSSL/BoringSSL : wrappers OpenSSL pour .NET, bindings BoringSSL, ou piles natives embarquées (ex. anciens runtimes gRPC « Grpc.Core » basés sur une lib native). | Gain de temps, mutualisation des efforts communautaires. API parfois bas niveau, maintenance et cadence des patchs variables. Attention aux licences, tailles de déploiement, et à l’alignement avec vos exigences sécurité. |
Migrer l’exécutable sur Linux/WSL/conteneur | Exécuter votre application .NET sur un OS où la pile TLS s’appuie nativement sur OpenSSL (Linux), sans changer votre code métier. | Peu ou pas de changements de code côté TLS. Nécessite une chaîne de déploiement conteneur/WSL et des validations infra. |
Pourquoi .SslStream
ne peut pas charger OpenSSL sous Windows
La conception de .SslStream
délègue le protocole TLS au fournisseur natif de l’OS. Ce choix garantit :
- La cohérence avec les politiques de sécurité Windows (FIPS, suites autorisées, certificats racine).
- La mutualisation des correctifs via Windows Update.
- La compatibilité avec le magasin de certificats (
Cert:\LocalMachine\My
,CurrentUser\My
) et les modules matériels (CSP/KSP, HSM).
Exposer un « sélecteur de backend TLS » impliquerait de supporter deux piles natives aux sémantiques différentes, avec des comportements d’erreurs, de validation, d’ALPN/SNI et de gestion de clés parfois divergents. À ce jour, l’équipe .NET n’offre pas cette option, et aucun commutateur documenté ne contourne cette décision.
Quand (et comment) utiliser un wrapper OpenSSL
Architecture cible
L’idée consiste à créer un type managé (OpenSslStream
) qui encapsule un SSL*
/SSL_CTX*
et dialogue avec votre transport (socket, pipe, QUIC) via des memory BIO. Schéma :
- Le réseau alimente un tampon managé.
- Vous « feed » ce tampon dans un BIO de lecture (
BIO_write
). - Vous appelez
SSL_read
/SSL_write
; OpenSSL renvoie des octets clairs/cryptés. - Vous poussez les octets cryptés sur la socket.
Un shim C recommandé
Pour simplifier le P/Invoke (ABI, callbacks), exposez un petit « shim » C qui encapsule OpenSSL. Exemple minimaliste (illustratif) :
/* openssl_shim.h */
#ifdef _WIN32
#define DLL_EXPORT __declspec(dllexport)
#else
#define DLL_EXPORT
#endif
#ifdef __cplusplus
extern "C" {
#endif
typedef void* ossl_ctx;
typedef void* ossl_ssl;
DLL_EXPORT int ossl_init();
DLL_EXPORT void ossl_cleanup();
DLL_EXPORT ossl_ctx ossl_ctx_new_client(const char* alpn_csv);
DLL_EXPORT void ossl_ctx_free(ossl_ctx ctx);
DLL_EXPORT ossl_ssl ossl_ssl_new(ossl_ctx ctx);
DLL_EXPORT void ossl_ssl_free(ossl_ssl s);
DLL_EXPORT int ossl_set_sni(ossl_ssl s, const char* host);
DLL_EXPORT int ossl_set_verify_pem(ossl_ctx ctx, const char* ca_pem_path);
DLL_EXPORT int ossl_handshake(ossl_ssl s, const uint8_t* in, int in_len, uint8_t* out, int* out_len);
DLL_EXPORT int ossl_read(ossl_ssl s, uint8_t* clear, int clear_cap);
DLL_EXPORT int ossl_write(ossl_ssl s, const uint8_t* clear, int clear_len, uint8_t* out, int out_cap);
#ifdef __cplusplus
}
#endif
Côté C# : P/Invoke sur ce shim, puis enveloppez dans un Stream
.
public sealed class OpenSslStream : Stream
{
private SafeOsslCtx _ctx;
private SafeOsslSsl _ssl;
private readonly Stream _transport;
// ... états, buffers
public OpenSslStream(Stream transport, string host, string[] alpn)
{
_transport = transport;
Native.ossl_init();
_ctx = Native.ossl_ctx_new_client(string.Join(",", alpn));
_ssl = Native.ossl_ssl_new(_ctx);
Native.ossl_set_sni(_ssl, host);
}
public void AuthenticateAsClient(string host) => HandshakeLoop();
private void HandshakeLoop()
{
Span<byte> netIn = stackalloc byte[8192];
byte[] netOut = new byte[8192];
for (;;)
{
int read = _transport.Read(netIn);
int outLen = netOut.Length;
int hs = Native.ossl_handshake(_ssl, netIn[..read], netOut, ref outLen);
if (hs < 0) throw new IOException("TLS handshake failed.");
if (outLen > 0) _transport.Write(netOut, 0, outLen);
if (hs == 1) break; // handshake terminé
}
}
public override int Read(byte[] buffer, int offset, int count)
{
// lire du réseau si nécessaire, alimenter le BIO, puis SSL_read...
// (illustratif)
return 0;
}
public override void Write(byte[] buffer, int offset, int count)
{
// SSL_write vers buffer chiffré puis Write sur _transport
}
// ... Dispose/Close/Flush/CanRead/CanWrite etc.
}
Points d’attention :
- ALPN : exposez un sélecteur (ex. « h2,http/1.1 »). Prévoyez la négociation côté client/serveur.
- SNI :
SSL_set_tlsext_host_name
pour le nom de serveur. - Certificats : chargez vos
.pfx
/.pem
, validez la chaîne (date, EKU, CRL/OCSP si requis). - Erreurs : transformez les
SSL_get_error
en exceptions .NET explicites (timeouts, fermeture ordonnée, alertes). - Mises à jour : cadencer les patchs OpenSSL et surveiller les CVE (processus de sécurité obligatoire).
Validation des certificats côté client (pinning facultatif)
Si vous restez avec .SslStream
(Schannel), vous pouvez déjà contrôler finement la validation grâce au callback. Exemple utile même si vous n’utilisez pas OpenSSL :
using var client = new TcpClient();
await client.ConnectAsync(host, 443);
using var net = client.GetStream();
using var ssl = new SslStream(net, leaveInnerStreamOpen: false,
userCertificateValidationCallback: (sender, cert, chain, errors) =>
{
// Exemple: pinning sur l’empreinte
var expected = "AA:BB:CC:..."; // SHA-256 au format hex
var thumb = cert?.GetCertHashString(HashAlgorithmName.SHA256);
return string.Equals(expected, thumb, StringComparison.OrdinalIgnoreCase);
});
var options = new SslClientAuthenticationOptions
{
TargetHost = host,
EnabledSslProtocols = SslProtocols.Tls12 | SslProtocols.Tls13,
CertificateRevocationCheckMode = X509RevocationMode.Online
// Pas de choix direct des suites : dépend de Schannel/config OS.
};
await ssl.AuthenticateAsClientAsync(options);
Bibliothèques tierces : que regarder
- Wrappers OpenSSL .NET : bindings C# exposant
libssl
. Vérifiez l’activité des mainteneurs, l’API (SNI, ALPN, TLS 1.3), et la prise en charge des plateformes. - BoringSSL : forks/ports C# existent. Avantage : surface plus épurée, mais pas de promesse de stabilité d’API ; l’intégration demande une veille serrée.
- Stacks embarquées : certains clients/serveurs (ex. implémentations gRPC historiques via lib native) embarquent déjà une pile TLS non Schannel. Assurez‑vous du support Windows x64/ARM64, FIPS si nécessaire, et de la compatibilité avec vos politiques internes.
Alternative pragmatique : exécuter l’app .NET sur Linux
Si votre objectif est d’utiliser OpenSSL sans écrire de code natif, conteneuriser l’application peut être le chemin le plus direct. Exemple de Dockerfile (build & run) :
# Build
FROM mcr.microsoft.com/dotnet/sdk:8.0 AS build
WORKDIR /src
COPY . .
RUN dotnet publish -c Release -o /app
# Runtime (Debian/Ubuntu avec OpenSSL)
FROM mcr.microsoft.com/dotnet/runtime:8.0
WORKDIR /app
COPY --from=build /app .
ENV DOTNET_EnableDiagnostics=0
ENTRYPOINT ["dotnet", "VotreAppli.dll"]
Déployée ainsi sur Linux, votre app .NET utilisera la pile TLS du runtime (OpenSSL), sans modifications majeures du code.
Schannel vs OpenSSL : comparatif opérationnel
Critère | Schannel (Windows) | OpenSSL | Impact |
---|---|---|---|
Gouvernance et patching | Mises à jour via Windows Update, alignées sur les politiques d’entreprise. | Patching géré par vous (ou l’image conteneur). Réactivité nécessaire. | Charge d’exploitation : faible vs modérée/élevée. |
Contrôle des suites/courbes | Piloté par stratégies système (GP, registres). | Contrôle fin par code/config OpenSSL. | OpenSSL favorise l’expérimentation, Schannel favorise la conformité. |
Intégration HSM/TPM | Intégration native via CNG/KSP. | Possible mais au prix d’un développement spécifique. | Schannel simplifie l’usage des clés matérielles. |
Conformité FIPS | Couverture via modules Windows validés. | Requiert une build/chaîne validée FIPS. | Décisif pour environnements réglementés. |
ALPN/SNI | Supportés ; exposés via options SslStream . | Supportés ; entièrement pilotables par API OpenSSL. | OpenSSL permet une négociation très fine. |
CI/CD & taille de déploiement | Pas de binaires natifs additionnels. | Distribution de libssl /libcrypto , ou image conteneur. | Anticiper la logistique binaire et la SBOM. |
Gestion des certificats : différences pratiques
Avec Schannel
- Accès au magasin Windows (autorités racines, intermédiaires, certificats personnels).
- Usage direct des clés marquées « non exportables » (ex. clés protégées par TPM/HSM).
- Révocation configurable via stratégies (CRL, OCSP).
Avec OpenSSL
- Vous chargez explicitement vos
.pem
/.pfx
et votre bundle CA. - Vous implémentez la validation de chaîne (y compris EKU, contraintes de nom, usage OCSP stapling si nécessaire).
- Vous décidez du stockage des clés (fichiers, HSM via moteurs/Providers OpenSSL).
Exemples ciblés à explorer
- Un wrapper « OpenSslStream » issu d’exemples IoT/MQTT en C# qui encapsulent
libssl
. - Des projets démontrant un canal OpenSSL côté Java via JNA (analogue conceptuel au P/Invoke C#).
- Des bindings .NET pour OpenSSL/BoringSSL, exposant des API de bas niveau (gestion de
SSL_CTX
, BIO, ALPN).
Arbre de décision
Contrainte dominante | Choix recommandé | Raison |
---|---|---|
Conformité Windows/FIPS, maintenance minimale | Rester sur .SslStream + Schannel | Conforme aux politiques, patching via OS, intégration magasin cert. |
Contrôle total des suites et comportements TLS | Wrapper OpenSSL | Paramétrage fin (ALPN/SNI/courbes), expérimentation. |
Time‑to‑market, pas de code natif | Exécuter sur Linux (conteneur/WSL) | OpenSSL « out‑of‑the‑box » côté runtime .NET. |
Réutiliser un écosystème existant | Librairie tierce | Capitaliser sur une communauté/stack existante. |
Bonnes pratiques et pièges courants
- Ne mélangez pas des couches : évitez d’utiliser
.SslStream
au‑dessus d’un flux déjà chiffré par OpenSSL, et inversement. - Mesurez : la performance dépend du profil de charge, des cœurs CPU, et des accélérations (AES‑NI, PCLMULQDQ). Faites des benchs comparatifs Schannel vs OpenSSL avant toute décision.
- Gestion des erreurs TLS : mappez explicitement les alertes (ex. bad_record_mac, unknown_ca, handshake_failure) à des exceptions parlant métier/ops.
- Durcissement : désactivez TLS 1.0/1.1, privilégiez des courbes sûres (X25519, P‑256), activez les vérifications de révocation adaptées à votre contexte.
- SBOM & compliance : si vous embarquez OpenSSL, générez une SBOM, suivez les CVE, documentez vos options de compilation.
FAQ rapide
Existe‑t‑il une variable d’environnement pour basculer .SslStream
vers OpenSSL ?
Non. Vous pouvez activer/désactiver TLS 1.3 et quelques comportements via des switches .NET, mais pas changer de fournisseur TLS sous Windows.
Et si j’utilise HttpClient
?
Sous Windows, HttpClient
repose in fine sur la même pile TLS que .SslStream
(Schannel via SocketsHttpHandler
). Pour forcer OpenSSL, appliquez les mêmes stratégies : wrapper natif, lib tierce, ou exécuter l’application sur Linux.
Quid de QUIC/HTTP‑3 ?
Les piles QUIC (ex. MSQUIC) s’appuient elles aussi sur le fournisseur TLS de la plateforme. Sur Windows, cela reste Schannel ; sur Linux, OpenSSL/BoringSSL selon la compilation du provider. On ne peut pas « forcer » OpenSSL sous Windows sans changer de pile.
Exemples de code utiles (Schannel)
Serveur TLS minimal avec .SslStream
var cert = new X509Certificate2("server.pfx", "motdepasse",
X509KeyStorageFlags.MachineKeySet | X509KeyStorageFlags.EphemeralKeySet);
var listener = new TcpListener(IPAddress.Any, 8443);
listener.Start();
for (;;)
{
var client = await listener.AcceptTcpClientAsync();
_ = Task.Run(async () =>
{
using var net = client.GetStream();
using var ssl = new SslStream(net, false);
var opts = new SslServerAuthenticationOptions
{
ServerCertificate = cert,
ClientCertificateRequired = false,
EnabledSslProtocols = SslProtocols.Tls12 | SslProtocols.Tls13,
ApplicationProtocols = new() { SslApplicationProtocol.Http2 }
};
await ssl.AuthenticateAsServerAsync(opts);
await ssl.WriteAsync(Encoding.ASCII.GetBytes("hello over TLS"));
});
}
Client TLS avec sélection ALPN
using var tcp = new TcpClient();
await tcp.ConnectAsync("example.org", 443);
using var net = tcp.GetStream();
using var ssl = new SslStream(net, false);
var clientOpts = new SslClientAuthenticationOptions
{
TargetHost = "example.org",
ApplicationProtocols = new() {
SslApplicationProtocol.Http2,
SslApplicationProtocol.Http11
}
};
await ssl.AuthenticateAsClientAsync(clientOpts);
// L’ALPN négocié est disponible via ssl.NegotiatedApplicationProtocol
Cas d’usage typiques et recommandations
- Produit Windows‑only en entreprise régulée : restez sur Schannel. Appuyez‑vous sur les stratégies de sécurité et le magasin de certificats. Utilisez les callbacks de validation pour le pinning ou des règles spécifiques.
- SDK cross‑platform ou moteur réseau custom : wrapper OpenSSL/BoringSSL peut se justifier (contrôle fin, parité de comportement entre OS), mais exige une capacité « C/C++ » en interne.
- Microservices conteneurisés : exécuter sous Linux maximise l’homogénéité (OpenSSL partout), simplifie parfois le support, et évite d’embarquer des binaires natifs supplémentaires.
Synthèse
Non, on ne peut pas forcer .SslStream
à utiliser OpenSSL sous Windows. Trois routes existent pour bénéficier d’OpenSSL : (1) écrire/adapter un wrapper natif et exposer un stream managé ; (2) recourir à une bibliothèque tierce qui intègre déjà une pile TLS non Schannel ; (3) exécuter l’application sur Linux/WSL/conteneur pour profiter d’OpenSSL fourni par le runtime .NET. Le bon choix dépend de vos exigences de conformité, de votre tolérance au risque de sécurité (patching), de vos contraintes de déploiement, et du niveau de contrôle attendu (ALPN/SNI/suites).
En résumé : si vous ciblez une exécution Windows native et les piles .NET « officielles », .SslStream
restera adossé à Schannel. Pour OpenSSL, sortez de .SslStream
ou changez d’environnement d’exécution.