WPFアプリでhttp/httpsの通信をしてデータを取得するには、通常はSystem.Net名前空間のクラスを使います。http通信(非暗号通信)は普通に通信できます。しかし、https通信(暗号通信)は使用するプロトコルや.NET frameworkのバージョンによっては、そのままでは通信に失敗します。.NET frameworkアプリにおけるhttps通信に関する実装についての投稿です。
http通信をする
WPFアプリなどの.NET frameworkアプリでhttp/https通信する場合は、System.Net名前空間にある
- WebClientクラス
- WebRequestクラス/WebResponseクラス
- 上記のクラスの派生クラス
を使います。
たとえば、WebClientクラスを使って、UTF8の文字列データを取得する場合は、以下のような関数で実現できます。
public static async Task<string> ReadFromUrlAsync(Uri url) { using (var webClient = new WebClient()) { using (var stream = await webClient.OpenReadTaskAsync(url)) { using (var textReader = new StreamReader(stream, Encoding.UTF8, true)) { var body = await textReader.ReadToEndAsync(); return body; } } } }
この関数を使うと、
var data = await ReadFromUrlAsync("http://example.com/utf8.txt");
のように文字列データを取得できます。
https通信をすると例外が発生する
ところが、”https://example.com/utf8.txt” のようにプロトコルをhttp(非暗号化通信)からhttps(暗号化通信)に変更するとサーバーの設定によっては、例外が発生します。
その例外の内容は、
System.Net.WebException
Message: “要求は中止されました: SSL/TLS のセキュリティで保護されているチャネルを作成できませんでした”
HResult: -2146233079 (0x80131509)
StackTrace:
場所 System.Net.HttpWebRequest.EndGetResponse(IAsyncResult asyncResult)
場所 System.Net.WebClient.GetWebResponse(WebRequest request, IAsyncResult result)
場所 System.Net.WebClient.OpenReadAsyncCallback(IAsyncResult result)
のような内容です。通信チャネルの作成に失敗しています。
これは、サーバーがSSLまたはTLS1.0のプロトコルを受け付けず、TLS1.1 / TLS1.2のプロトコルのみを受け付けるようになっていると発生します。
.NET frameworkでのTLS1.1/1.2プロトコルのサポート状況
ところで、WPFアプリ(.NET frameworkアプリ)の場合にTLS1.1/TLS1.2のプロトコルは、どのような実行環境で利用できるのでしょうか?それはWindowsのバージョンと.NET frameworkのバージョンに依存します。
暗号化通信に使用するプロトコルとして、TLS1.1/1.2のプロトコルを指定するための値が利用できる.NET frameworkのバージョンは、
- .NET framework 3.5以前: 条件付きで、値を利用可能
各Windowsに対応するKBがインストールされていることが条件
(KB3154517 / KB3154517 / KB3154518 / KB3154519 / KB3154520) - .NET framework 4.0: 値を利用不可
- .NET framework 4.5以降: 値を利用可能
となっています。注意してほしいのは、これは、実際に暗号化通信できるかどうかではなく、設定としてプロパティにTLS1.1/1.2プロトコルを指定できるかどうかということです。
通信時に使う暗号化プロトコルとしてTLS1.1/1.2を指定した時に、実際にTLS1.1/1.2で暗号化通信ができるかどうかは、Windowsのバージョンに依存します。
- TLS1.1/1.2の通信不可: Windows XP, Windows Vista
- TLS1.1/1.2の通信可能: Windows 7 / 8.0 / 8.1 / 10
のようになっています。
もちろん、アプリ側の実行環境だけでなく、通信相手のサーバーがTLS1.1/1.2を使うことができる設定になっている必要があります。
.NET frameworkでの暗号化通信プロトコルのデフォルト値
各.NET frameworkにおける利用する暗号化通信のプロトコルのデフォルト値は
- 2.0 / 3.0 / 3.5 / 4.0: SSL3 / TLS1.0
- 4.5 / 4.5.1 / 4.5.2: SSL3 / TLS1.0
- 4.6 / 4.6.1 / 4.6.2: TLS1.0 / TLS1.1 / TLS1.2
- 4.7: SystemDefault
となっています。これらのデフォルト値は2018年9月時点でのWindows 10上での値です。他のWindows上や、今後のWindows 10のWindows Updateの内容によっては変わる可能性があります。
値として複数のプロトコルが設定されています。複数のプロトコルが指定されている場合は、サーバー側で利用できるプロトコルを考慮して、暗号化強度が一番高いものが利用されます。
.NET framework 4.7では、個別のプロトコル値ではなく、SystemDefaultという値がデフォルト値となっています。このSystemDefaultが設定されている場合は、実際のプロトコルの値は、Windowsのシステム設定に依存します。特別な設定変更をしていない場合は、以下のようになっています。
- Windows 7: SSL3 / TLS1.0
- Windows 8.1: SSL3 / TLS1.0 / TLS1.1 / TLS1.2
- Windows 10 (1511まで): SSL3 / TLS1.0 / TLS1.1 / TLS1.2
- Windows 10 (1607から): TLS1.0 / TLS1.1 / TLS1.2
詳細は、英語になりますがマイクロソフトの以下のサイトにあります。
.NET framework 4.5におけるTLS1.1/1.2での暗号化通信
ここで、.NET framework 4.5のデフォルト値に注目すると、TLS 1.1/1.2が含まれていません。
そうなんです、.NET framework 4.5で動作するように設定されたWPFアプリ(.NET frameworkアプリ)は、そのままでは、TLS1.1/1.2を使った暗号化通信ができないのです。そのため、SSL3 / TLS1.0を無効化して、TLS1.1/1.2しか有効化していないサーバーと暗号化通信をしようとすると失敗します。このとき、冒頭に記したSystem.Net.WebExceptionの例外が発生します。
さて、これを解決するにはどうすればよいのでしょうか?
.NET frameworkでの暗号化通信プロトコルの明示的な指定
答えは簡単です。暗号化通信に利用するプロトコルを設定すればよいのです。具体的には、System.Net名前空間にあるServicePointManager.SecurityProtocolのプロパティに設定します。このプロパティは静的(staticな)プロパティであるので、設定するとそのプロセス内のすべてのhttps通信に反映されます。
.NET framework 4.5以降のみを利用する場合
.NET framework 4.5以降の場合は、Tls11/Tls12がSecurityProtocolTypeの列挙値として定義されているので、以下のように設定できます。
ServicePointManager.SecurityProtocol = SecurityProtocolType.Tls | SecurityProtocolType.Tls11 | SecurityProtocolType.Tls12;
この設定をした後のhttps通信(暗号化通信)では、TLS1.0/1.1/1.2のみが利用可能となります。
.NET framework 4.5以前でも利用する場合
.NET framework 4.0以前の場合は、Tls11/Tls12がSecurityProtocolTypeの列挙値として定義されていません。そのためそのままでは設定できません。以下のように拡張した列挙値(SecurityProtocolTypeExtensions)を用意します。
namespace System.Net { public static class SecurityProtocolTypeExtensions { public const SecurityProtocolType Tls12 = (SecurityProtocolType)0x00000C00; public const SecurityProtocolType Tls11 = (SecurityProtocolType)0x00000300; public const SecurityProtocolType SystemDefault = (SecurityProtocolType)0; } }
そして、定義した値を使用してプロパティを設定します。プロパティ値を設定するところでは、必要なKBが当たっていないときに例外が発生することを考慮して、以下のようにして、設定します。
try { ServicePointManager.SecurityProtocol = SecurityProtocolType.Tls | SecurityProtocolTypeExtensions.Tls11 | SecurityProtocolTypeExtensions.Tls12; } catch { ServicePointManager.SecurityProtocol = SecurityProtocolType.Ssl3 | SecurityProtocolType.Tls; }
この例では、TLS1.0/1.1/1.2を設定しています。KBが当たていない環境などで例外が発生した時は、SSL3/TLS1.0を設定しています。
これで、.NET framework 4.5および以前を利用するアプリでもTLS1.1/1.2を使った暗号化通信ができます。