OpenSSL の ALPN/NPN API の使い方

  • 32
    いいね
  • 0
    コメント
この記事は最終更新日から1年以上が経過しています。

この記事では OpenSSL での ALPN/NPN API の使い方を紹介します. ALPNNPN は TLS 拡張であり, TLS ハンドシェークの後アプリケーションレイヤーのプロトコルをサーバーとクライアントでネゴシエーションするために使います. https URI で SPDY 対応のサービスにアクセスするとき, 必ずこれら何れかの拡張を使って SPDY プロトコルをネゴシエーションしていますのでみなさん毎日使っているはずです. この記事を執筆時点では策定中である HTTP/2 では https URI を使う場合, ALPN を使って HTTP/2 をネゴシエーションするようになる予定です.

ALPN と NPN の大きな違いは, ALPN ではクライアントが送信するアプリケーションプロトコルのリストの中から, サーバーがアプリケーションプロトコルを選択するのに対し, NPN ではサーバーが送信するリストの中から, クライアントがアプリケーションプロトコルを決定します.

OpenSSL は 1.0.1g では NPN をサポートしており, ALPN は 1.0.2 からサポートされる予定です (この記事の執筆時点では 1.0.2 は未リリース). 似たような機能なのですが OpenSSL では違う API となっていてアプローチも微妙に異なっています. 以下それぞれ基本的な使用方法を説明します.

ALPN

ALPN ではクライアントがアプリケーションプロトコルのリストをサーバーに送信し, サーバーがプロトコルを選択するという仕組みです. まずはクライアントでアプリケーションプロトコルのリストを指定する API を見てみます. 送信するアプリケーションプロトコルを OpenSSL に伝える API は以下の関数です.

int SSL_CTX_set_alpn_protos(SSL_CTX *ctx, const unsigned char* protos,
                            unsigned protos_len);
int SSL_set_alpn_protos(SSL *ssl, const unsigned char* protos,
                        unsigned protos_len);

一つ目は SSL_CTX にセットする場合, 二つ目は SSL にセットする場合に使います. protos にはサポートするアプリケーションプロトコルをエンコードしたバッファーへのポインターを渡します. protos_lenprotos で渡したバッファーの長さを渡します. protos は NULL 終端文字列ではなくバイト列になります. アプリケーションプロトコル名は ALPN 識別子という名前が与えられていて, それを使ってエンコードすることになります. アプリケーションプロトコルは, 以下のようにエンコードされます.

  1. ALPN 識別子の長さを 1 バイトでエンコードする.
  2. 次のバイト位置から ALPN 識別子をそのままコピーする.

複数のアプリケーションプロトコルを指定する場合は, ALPN 識別子毎にこの手順を繰り返し適用しバッファーを埋めます. 長さのフィールドが 1 バイトなので ALPN 識別子は最大 255 バイトという制限があります. 例えば, spdy/3, http/1.1 をエンコードした場合, 以下のようになります.

0                                       1
0   1   2   3   4   5   6   7   8   9   0   1   2   3   4   5   6
+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+
|0x6|'s'|'p'|'d'|'y'|'/'|'3'|0x8|'h'|'t'|'t'|'p'|'/'|'1'|'.'|'1'|
+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+

この場合, protos_len は 16 になります.

次にサーバー側の API を見てみます.

void SSL_CTX_set_alpn_select_cb(SSL_CTX* ctx,
                                int (*cb) (SSL *ssl,
                                           const unsigned char **out,
                                           unsigned char *outlen,
                                           const unsigned char *in,
                                           unsigned int inlen,
                                           void *arg),
                                void *arg);

サーバーでは, クライアントが ALPN でアプリケーションプロトコルのリストを送信してくるとコールバックを呼び出す仕組みになっています. アプリケーションはコールバックを実装してアプリケーションプロトコルを選択する, という流れになります. arg はコールバック cb のパラメーター arg として受け取ることができる任意のポインターです. コールバック関数だけ取り出してみます.

int (*cb) (SSL *ssl,
           const unsigned char **out,
           unsigned char *outlen,
           const unsigned char *in,
           unsigned int inlen,
           void *arg)

ininlen はクライアントがエンコードしたアプリケーションプロトコルのリストがそのまま渡されます. エンコードの仕方は上述の通りですのでデコードしてアプリケーションプロトコルを選択します. 選択したアプリケーションプロトコルの ALPN 識別子へのポインターを *out へ代入し, その長さを *outlen へ代入します. 例えば spdy/3.1 を選択する場合,

*out = "spdy/3.1";
*outlen = 8;

とすればよいということになります. 何も選択しないということもできますし, リストにないものを選択することもできますが, その場合どのアプリケーションプロトコルが使われるのかはその後の動作はクライアント次第であり未定義になります. コールバック関数からは通常 SSL_TLSEXT_ERR_OK を返します. SSL_TLSEXT_ERR_NOACK を返すと TLS ハンドシェークが失敗します. outoutlen は初期化されないでコールバックに引き渡されており, SSL_TLSEXT_ERR_OK を返すとこれらの値を使って処理が続行するので, out, outlen に何も代入しない場合は必ず SSL_TLSEXT_ERR_NOACK を返さなければなりません.

サーバーは選択したアプリケーションプロトコルを知っていますがクライアントからはどうやって知るのかというと次の API を使います.

void SSL_get0_alpn_selected(const SSL *ssl, const unsigned char **data,
                            unsigned *len);

この関数をコールすると *data に選択したアプリケーションプロトコルの ALPN 識別子が代入され, *len にはその長さが代入されます. この関数はサーバー側でもコールすることができます.

NPN

NPN ではサーバーがアプリケーションプロトコルのリストをクライアントに送信し, クライアントがプロトコルを選択するという仕組みです. まずはサーバーでアプリケーションプロトコルのリストを指定する API を見てみます. 送信するアプリケーションプロトコルを OpenSSL に伝える API は以下の関数です.

void SSL_CTX_set_next_protos_advertised_cb(SSL_CTX *s,
                                           int (*cb) (SSL *ssl,
                                                      const unsigned char **out,
                                                      unsigned int *outlen,
                                                      void *arg), void *arg);

ALPN とは違いコールバックを使った API になっています. NPN のほうが古くからあるので ALPN ではコールバックを使わない方法に変更されたということでしょう. この関数に渡す arg はコールバック cb のパラメーター arg として受け取ることができる任意のポインターです. コールバック関数の部分だけ取り出してみます.

int (*cb) (SSL *ssl,
           const unsigned char **out,
           unsigned int *outlen,
           void *arg)

アプリケーションはこのコールバックを実装し, *out にサポートするアプリケーションプロトコルの ALPN 識別子をエンコードしたバッファーへのポインターを代入し, *outlen にはバッファーの長さを代入することになります. ALPN 識別子のエンコードの仕方は ALPN の場合と同じです. out, outlen が未初期化の状態でコールバックに引き渡されるのは ALPN のコールバックと同様ですので注意してください. out, outlen に何も代入しない場合は必ず SSL_TLSEXT_ERR_NOACK を返さなければなりません.

次にクライアント側の API を見てみます.

void SSL_CTX_set_next_proto_select_cb(SSL_CTX *s,
                                      int (*cb) (SSL *ssl, unsigned char **out,
                                                 unsigned char *outlen,
                                                 const unsigned char *in,
                                                 unsigned int inlen, void *arg),
                                      void *arg);

SSL_CTX_set_alpn_select_cb とほとんど同じですが, コールバックの *out*inSSL_CTX_set_alpn_select_cb では const 修飾されていましたが, SSL_CTX_set_next_proto_select_cb のコールバックでは const がなくなっています. ALPN とコードを共有する場合はこのあたりは気をつける必要があります. この点以外は ALPN の場合と変わりません.

クライアントが選択したアプリケーションプロトコルをサーバーが知るには次の API を使います.

void SSL_get0_next_proto_negotiated(const SSL *s,
                                    const unsigned char **data, unsigned *len);

SSL_get0_alpn_selected と同様に, この関数をコールすると *data に選択したアプリケーションプロトコルの ALPN 識別子が代入され, *len にはその長さが代入されます. この関数はクライアント側でもコールすることができます.