WinHttp APIでの通信でTLS1.1/1.2を有効化する#1

.NET FramworkでTLS1.1/1.2通信をする方法の投稿のアクセス数が多いことがわかりました。もしかしたら、C/C++などの実装でも需要があるかと思いネイティブの実装でTLS1.1/1.2通信をする方法の投稿です。

ネイティブの実装でのhttps通信のためのAPI

Windowsのデスクトップアプリでは、C/C++などのネイティブの実装でhttps通信するときは、多くの場合、Windows APIもしくはOpenSSLライブラリを使います。この投稿ではWindows APIを扱います。

Windows APIでhttps通信するには、socket, WinInet, WinHttpのいずれかのAPIセットを使います。socketでは、TCP通信の機能のみが提供されるだけなので、httpsの通信をすべて自前で実装することになります。WinInetとWinHttpでは、httpsの通信が機能として提供されています。

そのため、多くの場合、WinInet API、または、WinHttp APIを使うことになります。これらのAPIを使うと、容易にhttps通信ができます。しかし、何も考えずに使うと、近年のセキュリティ基準に適合しなくなります。

これらのAPIをデフォルト設定のまま使用してはいけないということです。デフォルト設定のままでは、現在において安全とされている暗号プロトコル(TLS1.2)が使われなかったり、古い暗号化プロトコルが使われることがあります。

今回は、WinHttp APIを使ったhttps通信について記載します。

WinHttp APIを使ったhttp/https通信

通常、WinHttp APIを使ってhttp通信する場合は、以下の手順で行います。

  1. URLの解析
    WinHttpCrackUrl関数を利用し、http通信するときのパラメーターを抽出します。
  2. セッションオブジェクトの作成
    WinHttpOpen関数を利用しセッションオブジェクトを作成します。
  3. コネクションオブジェクトの作成
    WinHttpConnect関数を利用し、コネクションオブジェクトを作成します。
  4. リクエストオブジェクトの作成
    WinHttpOpenRequest関数を利用し、リクエストオブジェクトを作成します。
    dwFlags引数は、http通信の場合には0を、https通信の場合にはWINHTTP_FLAG_SECURE を、指定します。
  5. リクエストの送信
    WinHttpSendRequest関数を利用し、httpリクエストを送信します。
  6. レスポンスの到着の待機
    WinHttpReceiveResponse関数を利用し、httpレスポンスの到着を待ちます。
  7. ステータスコードの取得
    WinHttpQueryHeaders関数を利用し、ステータスコードを取得します。
  8. レスポンスデータの取得
    WinHttpQueryDataAvailable / WinHttpReadData関数を利用し、レスポンスデータを取得します。
  9. オブジェクトの解放
    WinHttpCloseHandle関数を利用し、リクエストオブジェクト、コネクションオブジェクト、セッションオブジェクトを解放します。

手順4でWINHTTP_FLAG_SECUREを指定することで、https通信を行うことができます。

しかし、このままでは、https通信においてどの暗号化プロトコル(ssl2 / ssl3 / tls1.0 / tls1.1 / tls1.2)が使われるかはOSのバージョンや設定に依存することになります。

もし、最近の時代の流れに合わせてhttps通信でtls1.2を使いたいなら、上記の実装では実現できないことになります。

Windowsがサポートしているプロトコル

WinHttp APIがサポートしているプロトコルは、それを実行しているWindowsのバージョンによって異なります。

マイクロソフトのこの英語のサイト(Protocols in TLS/SSL (Schannel SSP))に一部の情報があります。

このサイトの情報と他の情報も合わせて情報をまとめると以下の表のようになります。

SSL 2SSL 3TLS 1.0TLS 1.1TLS 1.2DTLS 1.0DTLS 1.2
Windows XP
Windows Vista
Windows 7
Windows 8
Windows 8.1
Windows 10 1507-1511
Windows 10 1607-1703

SSL/TLSは、httpsで使われるTCP用のプロトコルです。DTLSは、UDP相当のTLSプロトコルです。

このように、Windowsのバージョンによって、サポートしているプロトコルが異なります。また、Windows 10 1607以降では、SSL2のサポートが打ち切られました。

WinHttp APIのデフォルトプロトコル

前述した手順のWinHttp APIを利用した実装では、https通信に使われる暗号化プロトコルはWindowsのバージョンや設定によって異なると述べました。実際どのようなプロトコルが使われるのでしょうか?

使用するプロトコルを明示的に指定しない場合、利用できるプロトコルは以下の表のようになります。色付きのチェックマーク()のところが、デフォルトプロトコルとなります。

SSL2SSL3TLS1.0TLS1.1TLS1.2
Windows XP
Windows Vista
Windows 7
Windows 8
Windows 8.1
Windows 10 1507-1511
Windows 10 1607-1703

この表はクライアントOSの値です。この表にはサーバーOSの値は記載していません。サーバーOSの値はクライアントOSの値と異なるところがあります。

デフォルトプロトコルの中でどれが使われるかは、実際に通信するときにサーバーとのネゴシエーションで決まります。通常であれば、クライアント側とサーバー側の両方で使用できるプロトコルの中で一番強度が高いものが使用されます。

この表を見てわかるように、Windows 7やWindows 8では、OSとしてはTLS 1.1やTLS1.2をサポートしていますが、WinHttp APIとしては、デフォルトでは利用されません。Windows 8.1以降であれば、デフォルトのままでもTLS1.2が使われます。

推奨の暗号化プロトコルはTLS1.2以降

現時点(2019年6月)で安全とされる推奨の暗号化プロトコルは、TLS1.2以降です。

TLS1.3は IETFは2018年3月23日に、ドラフト28を標準規格として承認しました。ただ標準規格となったばかりです。OSやWEBブラウザでは、対応が始まりつつある段階です。

したがって、現時点では、多くのプラットフォームで使えるのはTLS1.2のみです。

Windows 8.1以降であればデフォルトのままでのTLS1.2が利用されると述べました。しかし、デフォルトのままでは、TLS1.0やTLS1.1も利用してしまう可能性があります。推奨を満たすためには、明示的に暗号化プロトコルを指定する必要があります。

WinHttp APIでTLS1.2を使う

Windows 8.0以前のOSでは、デフォルト設定のままでは、httpsの暗号通信にTLS 1.2やTLS1.1やTLS1.0が使うことができないことは分かりました。

では、デフォルト設定のプロトコルよりも、より新しいプロトコルを使うようにするにはどうしたらよいのでしょうか?

結論から述べると、WinHttpSetOption() APIにて、使用可能とするプロトコルを明示的に指定します。

WinHttpSetOption() API

WinHttpSetOption() APIの説明はマイクロソフトのサイト(英語)にあります。

このAPIでは、WinHttpのAPIセットを使ってhttp/https通信するときのインターネットのオプションを設定できます。

構文

BOOLAPI WinHttpSetOption(
  HINTERNET hInternet,
  DWORD     dwOption,
  LPVOID    lpBuffer,
  DWORD     dwBufferLength
);

hInternetには、セッションオブジェクトハンドルまたはリクエストオブジェクトハンドルを指定します。dwOptionの値によって必要なオブジェクトハンドルの種類は異なります。どのオブジェクトハンドルを指定すればよいのかは、Option FlagsのRemarks(英語)に記載があります。httpsの暗号化プロトコルを設定するときは、WinHttpOpen の戻り値のセッションオブジェクトハンドルを指定します。

dwOptionには、設定したいオプションの種別を指定します。指定できるオプションの種類の説明は、Option Flags(英語)にあります。httpsの暗号化プロトコルを指定するときはWINHTTP_OPTION_SECURE_PROTOCOLSを指定します。

lpBufferdwBufferLengthには、設定値をもつ領域へのポインターとその設定値をもつ領域のバイトサイズを指定します。 オプションの種別ごとに設定すべきサイズなどの情報もOption Flags(英語)にあります。 httpsの暗号化プロトコルを指定するときは、DWORD型変数へのポインターとそのサイズsizeof(DWORD)を指定します。

WINHTTP_OPTION_SECURE_PROTOCOLSオプション

https接続時に使用する暗号化プロトコルを明示的に指定するときは、dwOptionWINHTTP_OPTION_SECURE_PROTOCOLSを指定することになるのですが、lpBufferには、何を設定すればよいのでしょうか?

これも、マイクロソフトのサイトのAPIリファレンスのOption Flags(英語)から確認できます。抜粋して、日本語訳にすると以下の通りです。

WINHTTP_OPTION_SECURE_PROTOCOLS

どの安全なプロトコルを受け入れるかを示すunsigned long integer(DWORD)の値を設定します。Windows 7/8でのデフォルト値はSSL3とTLS1.0だけが有効化されます。Windows 8.1/10のデフォルト値は、SSL3/TLS1/TLS1.1/TLS1.2だけが有効化されます。以下の値の一つ以上の組み合わせを設定することができます。

WINHTTP_FLAG_SECURE_PROTOCOL_ALL

セキュアソケットレイヤー(Secure Sockets Layer: SSL) 2.0、SSL 3.0、トランスポートレイヤーセキュリティ(Transport Layer Security: TLS) 1.0 プロトコルを使うことができます。
注) 定義名の接尾名は_ALLとなっていますが、実際にはALLでなく、古いプロトコルの組み合わせとなっています。

WINHTTP_FLAG_SECURE_PROTOCOL_SSL2

SSL 2.0プロトコルを使うことができます。

WINHTTP_FLAG_SECURE_PROTOCOL_SSL3

SSL 3.0プロトコルを使うことができます。

WINHTTP_FLAG_SECURE_PROTOCOL_TLS1

TLS 1.0プロトコルを使うことができます。

WINHTTP_FLAG_SECURE_PROTOCOL_TLS1_1

TLS 1.1プロトコルを使うことができます。

WINHTTP_FLAG_SECURE_PROTOCOL_TLS1_2

TLS 1.2プロトコルを使うことができます。

セキュアプロトコルの設定例

では、TLS 1.1/TLS1.2の二つのみを使用できるようにするにはどう設定すればよいのでしょうか?

一部のパラメーターは省略していますが、以下のようになります。

HINTERNET hSession = WinHttpOpen(...);
DWORD dwValue = WINHTTP_FLAG_SECURE_PROTOCOL_TLS1_1 | WINHTTP_FLAG_SECURE_PROTOCOL_TLS1_2;
WinHttpSetOption(hSession, WINHTTP_OPTION_SECURE_PROTOCOLS, &dwValue, sizeof(dwValue));
HINTERNET hConnect = WinHttpConnect(hSession, ...);
HINTERNET hRequest = WinHttpOpenRequest(hConnect, ... , WINHTTP_FLAG_SECURE);

WinHttpSetOption()に渡すHINTERNETハンドルは、セッションオブジェクトハンドルのため、WinHttpOpen()の戻り値を渡します。オプションの種類にはWINHTTP_OPTION_SECURE_PROTOCOLSを指定し、オプションの値にはWINHTTP_FLAG_SECURE_PROTOCOL_TLS1_1 | WINHTTP_FLAG_SECURE_PROTOCOL_TLS1_2を指定します。そして、実際の通信のリクエストであるWinHttpOpenRequest()の最後の引数には、WINHTTP_FLAG_SECUREを指定して、暗号化プロトコルの使用を指定します。

このようにすることで、各Windowsのバージョンごとのデフォルト値に依存することなく、希望の暗号化プロトコルで通信することができます。

クライアントOSごとの推奨値

現在の推奨の暗号化プロトコルはTLS1.2だと述べました。しかし、OSによってはTLS1.2をサポートしていないことがあります。各OSがサポートしているプロトコルを考慮したとき、WinHttp APIセットを使うときに、使用すべき推奨値は以下のようになります。

Windowsデフォルト値推奨値オプション値
Windows XPSSL3/TLS 1.0 TLS 1.0WINHTTP_FLAG_SECURE_PROTOCOL_TLS1
Windows VistaSSL3/TLS 1.0 TLS 1.0WINHTTP_FLAG_SECURE_PROTOCOL_TLS1
Windows 7SSL 3/TLS 1.0TLS 1.2WINHTTP_FLAG_SECURE_PROTOCOL_TLS1_2
Windows 8.0SSL 3/TLS 1.0TLS 1.2WINHTTP_FLAG_SECURE_PROTOCOL_TLS1_2
Windows 8.1SSL 3/TLS 1.0/1.1/1.2TLS 1.2WINHTTP_FLAG_SECURE_PROTOCOL_TLS1_2
Windows 10
1507-1511
SSL 3/TLS 1.0/1.1/1.2TLS 1.2WINHTTP_FLAG_SECURE_PROTOCOL_TLS1_2
Windows 10
1607-
TLS 1.0/1.1/1.2 TLS 1.2WINHTTP_FLAG_SECURE_PROTOCOL_TLS1_2

すべてのOSでデフォルト値と設定すべき推奨値が異なります。これは、すべてのOSで明示的に指定することが必要であることを意味します。

Windows Vista以前をサポートする

Windows 7以降をサポートするアプリの場合は、TLS1.2のみの設定で問題ありません。しかし、Windows XP/Vistaをサポートしたい場合は、OSがTLS1.2をサポートしていないので、TLS 1.2の設定は使えません。

ではどうすればよいのでしょうか?

安易に、Windows Vista以前をサポートするアプリケーションでは、すべてのOSに対応するために、静的にTLS1.0とTLS1.2の組み合わせの値(WINHTTP_FLAG_SECURE_PROTOCOL_TLS1 | WINHTTP_FLAG_SECURE_PROTOCOL_TLS1_2)を使用してはいけません。このような実装をすると、たとえばWindows 10でTLS1.0での通信をしてしまうことがあります。

したがって、Windows Vista以前もサポートしたい場合は、実行しているOSのバージョンを判定して、そのバージョンによって、動的に設定するオプション値を切り替える必要があります。

クライアント側としては、上記の実装でよいです。サーバー側はTLS1.0とTLS1.2を受け入れるように設定する必要があります。

しかし、サーバー側は、TLS1.0とTLS 1.2を無条件に受け入れるようにするだけでは問題があります。Windows 7以降のOSからTLS1.0で接続してきた場合は、接続を拒否する必要があります。そうしないと推奨のプロトコルが使われないことになります。

このような接続は、正規のクライアントアプリ以外からの接続で発生する可能性があります。そのため暗号化通信の確立後にクライアント認証をするなどして正しいクライアントからの接続のみ接続の継続を許可するようにします。なお、User Agentで判断することもできますが、User Agentは偽装が用意であるため、他の偽装できない方法で検証することが望まれます。


以上、WinHttp APIセットを使った場合のhttps通信の方法でした。次回は、テストツールを使って、実際の挙動を確認したいと思います。

2019年8月4日追記

Windows XPとWindows Vistaのデフォルト値が間違っていたので修正。

:SSL3
:SSL3/TLS1.0

コメントを残す