.NET の System.Net.Http.HttpClient で TLS バージョンを指定してみたところ、OS ごとに TLS 1.3 のサポート状況が違った

2024.03.04

いわさです。

最近 TLS 1.2 や TLS 1.3 の評価を行うことが多いのですが、そんな中でレガシークライアントと呼ばれるやつを自分で構築する機会がありました。

最近登場した .NET 8 のクロスプラットフォーム環境で動作するコンソールアプリケーションで作成することにしました。
TLS バージョンについては評価が出来たのですが、その過程で .NET の HttpClient で、特定プラットフォームで TLS 1.3 が有効化出来ないことを知りました。
試した内容などをまとめておきたいと思います。

System.Net.Http.HttpClient クラスで TLS バージョンを指定する

まずは適当なコンソールアプリを作成します。

% dotnet --version
8.0.101
% dotnet new console
The template "Console App" was created successfully.

Processing post-creation actions...
Restoring /Users/iwasa.takahito/work/hoge0304tls/hoge0304tls.csproj:
  Determining projects to restore...
  Restored /Users/iwasa.takahito/work/hoge0304tls/hoge0304tls.csproj (in 634 ms).
Restore succeeded.

HttpClient を使って GET リクエストを送信する部分を実装します。
送信先エンドポイントは先日のこちらの記事で使った CloudFront ディストリビューションを使ってみます。

Program.cs

var client = new HttpClient();
var response = await client.GetAsync("https://d656rohqh9cat.cloudfront.net/");
var content = await response.Content.ReadAsStringAsync();
Console.WriteLine(content);

特に TLS に関する指定はしていませんが、このまま実行してみましょう。

% dotnet run
hoge.html

% curl https://d656rohqh9cat.cloudfront.net/
hoge.html

レスポンスを受信出来ていますね。

SocketsHttpHandler を使って TLS バージョンを指定する

HttpClient はハンドラーを使って挙動をカスタマイズさせることが出来ます。
標準で SocketsHttpHandler というものが提供されており、SslOptions というプロパティの中でターゲット SSL/TLS バージョンを指定することが出来るみたいなので試してみました。

次のように SSL/TLS バージョンを指定した SocketsHttpHandler インスタンスを HttpClient に渡すだけです。
EnabledSslProtocolsはビットフラグとなっており、複数指定することが可能です。

Program.cs

using System.Net.Security;
using System.Security.Authentication;

var client = new HttpClient(new SocketsHttpHandler 
                            {
                                SslOptions = new SslClientAuthenticationOptions
                                {
                                    EnabledSslProtocols = SslProtocols.Tls12 | SslProtocols.Tls13 
                                }
                            });
var response = await client.GetAsync("https://d656rohqh9cat.cloudfront.net/");
var content = await response.Content.ReadAsStringAsync();
Console.WriteLine(content);

実行してみると、とりあえず動いていそうです。

% dotnet run
hoge.html

ここで、TLS 1.1 のみを指定してみます。
指定した時点でエディターで警告表示されていますね。こいつぁ良いや。

また、今回使っているエンドポイントは TLS 1.2 と TLS 1.3 のみが有効化されており、TLS 1.1 以下は使えません。
実行してみましょう。

% dotnet run
/Users/iwasa.takahito/work/hoge0304tls/Program.cs(9,59): warning SYSLIB0039: 'SslProtocols.Tls11' is obsolete: 'TLS versions 1.0 and 1.1 have known vulnerabilities and are not recommended. Use a newer TLS version instead, or use SslProtocols.None to defer to OS defaults.' (https://aka.ms/dotnet-warnings/SYSLIB0039) [/Users/iwasa.takahito/work/hoge0304tls/hoge0304tls.csproj]
Unhandled exception. System.Net.Http.HttpRequestException: The SSL connection could not be established, see inner exception.
 ---> System.Security.Authentication.AuthenticationException: Authentication failed, see inner exception.
 ---> Interop+AppleCrypto+SslException: bad protocol version
   --- End of inner exception stack trace ---
   at System.Net.Security.SslStream.ForceAuthenticationAsync[TIOAdapter](Boolean receiveFirst, Byte[] reAuthenticationData, CancellationToken cancellationToken)
   at System.Net.Http.ConnectHelper.EstablishSslConnectionAsync(SslClientAuthenticationOptions sslOptions, HttpRequestMessage request, Boolean async, Stream stream, CancellationToken cancellationToken)
   --- End of inner exception stack trace ---
   at System.Net.Http.ConnectHelper.EstablishSslConnectionAsync(SslClientAuthenticationOptions sslOptions, HttpRequestMessage request, Boolean async, Stream stream, CancellationToken cancellationToken)
   at System.Net.Http.HttpConnectionPool.ConnectAsync(HttpRequestMessage request, Boolean async, CancellationToken cancellationToken)
   at System.Net.Http.HttpConnectionPool.CreateHttp11ConnectionAsync(HttpRequestMessage request, Boolean async, CancellationToken cancellationToken)
   at System.Net.Http.HttpConnectionPool.AddHttp11ConnectionAsync(QueueItem queueItem)
   at System.Threading.Tasks.TaskCompletionSourceWithCancellation`1.WaitWithCancellationAsync(CancellationToken cancellationToken)
   at System.Net.Http.HttpConnectionPool.SendWithVersionDetectionAndRetryAsync(HttpRequestMessage request, Boolean async, Boolean doRequestAuth, CancellationToken cancellationToken)
   at System.Net.Http.RedirectHandler.SendAsync(HttpRequestMessage request, Boolean async, CancellationToken cancellationToken)
   at System.Net.Http.HttpClient.<SendAsync>g__Core|83_0(HttpRequestMessage request, HttpCompletionOption completionOption, CancellationTokenSource cts, Boolean disposeCts, CancellationTokenSource pendingRequestsCts, CancellationToken originalCancellationToken)
   at Program.<Main>$(String[] args) in /Users/iwasa.takahito/work/hoge0304tls/Program.cs:line 12
   at Program.<Main>(String[] args)

InnerException をたどっていくとbad protocol versionを確認することが出来ますね。

macOS だと TLS 1.3 が使えない...!

ここで想定外のことが起きたのですが、TLS 1.3 のみを指定して同じように試してみたところ次のように失敗しました。

% dotnet run
Unhandled exception. System.Net.Http.HttpRequestException: The SSL connection could not be established, see inner exception.
 ---> System.Security.Authentication.AuthenticationException: Authentication failed, see inner exception.
 ---> System.PlatformNotSupportedException: The requested security protocol is not supported.
   at System.Net.SslProtocolsValidation.ValidateContiguous(SslProtocols protocols, SslProtocols[] orderedSslProtocols)
   at System.Net.SafeDeleteSslContext.SetProtocols(SafeSslHandle sslContext, SslProtocols protocols)
   at System.Net.SafeDeleteSslContext.CreateSslContext(SslAuthenticationOptions sslAuthenticationOptions)
   at System.Net.SafeDeleteSslContext..ctor(SslAuthenticationOptions sslAuthenticationOptions)
   at System.Net.Security.SslStreamPal.HandshakeInternal(SafeDeleteSslContext& context, ReadOnlySpan`1 inputBuffer, Byte[]& outputBuffer, SslAuthenticationOptions sslAuthenticationOptions)
   --- End of inner exception stack trace ---
   at System.Net.Security.SslStream.ForceAuthenticationAsync[TIOAdapter](Boolean receiveFirst, Byte[] reAuthenticationData, CancellationToken cancellationToken)
   at System.Net.Http.ConnectHelper.EstablishSslConnectionAsync(SslClientAuthenticationOptions sslOptions, HttpRequestMessage request, Boolean async, Stream stream, CancellationToken cancellationToken)
   --- End of inner exception stack trace ---
   at System.Net.Http.ConnectHelper.EstablishSslConnectionAsync(SslClientAuthenticationOptions sslOptions, HttpRequestMessage request, Boolean async, Stream stream, CancellationToken cancellationToken)
   at System.Net.Http.HttpConnectionPool.ConnectAsync(HttpRequestMessage request, Boolean async, CancellationToken cancellationToken)
   at System.Net.Http.HttpConnectionPool.CreateHttp11ConnectionAsync(HttpRequestMessage request, Boolean async, CancellationToken cancellationToken)
   at System.Net.Http.HttpConnectionPool.AddHttp11ConnectionAsync(QueueItem queueItem)
   at System.Threading.Tasks.TaskCompletionSourceWithCancellation`1.WaitWithCancellationAsync(CancellationToken cancellationToken)
   at System.Net.Http.HttpConnectionPool.SendWithVersionDetectionAndRetryAsync(HttpRequestMessage request, Boolean async, Boolean doRequestAuth, CancellationToken cancellationToken)
   at System.Net.Http.RedirectHandler.SendAsync(HttpRequestMessage request, Boolean async, CancellationToken cancellationToken)
   at System.Net.Http.HttpClient.<SendAsync>g__Core|83_0(HttpRequestMessage request, HttpCompletionOption completionOption, CancellationTokenSource cts, Boolean disposeCts, CancellationTokenSource pendingRequestsCts, CancellationToken originalCancellationToken)

あれー、サーバー側は TLS 1.3 許可されてるんだけどなと思ったのですがよく見てみると先ほどとエラーメッセージが異なっています。 System.PlatformNotSupportedException: The requested security protocol is not supported.ということで、TLS 1.3 がサポートされていなさそうです。

エラーメッセージで調べてみると .NET ランタイム公式リポジトリの次の Issue に辿り着きました。

結論としては、macOS で HttpClient を使う場合は .NET 8 時点では TLS 1.3 がサポートされていないようです。
なんてこったい。

試しに同じコードを Windows 11 上で実行したところ、次のように動作しました。

さいごに

本日は .NET の System.Net.Http.HttpClient で TLS バージョンを指定してみたところ、OS ごとに TLS 1.3 のサポート状況が違ったので検証内容を紹介しました。

まず、実は最初にもともと確認したかったのは、レガシークライアント(ここでいうのは非推奨の TLS バージョンを使うクライアント)は実装次第で作れるものなのかという点を再確認したかったのですが、当然ですが普通に作れますね。
そのため、TLS 1.2 以上のみをサポートする場合にレガシークライアントを切り捨てる選択をする場合は、OS やブラウザ以外にもアプリケーションの実装を少し気にする必要があります。

また、今回 macOS + HttpClient クラスで TLS 1.3 が未サポートであることに気が付きました。
なるほど、プラットフォームによっては対応したくても対応できないケースも有り得るのか...。
今回の場合だと代替案としてはサードパーティの HTTP クライアントライブラリを使う必要があるようですね。.NET 9 が登場した時にこの Issue がどうなるのか少し注目してみようかなと思いました。