libtls で TLS 通信プログラミング（クライアント編）

[libtls](http://man.openbsd.org/?query=tls_init) は OpenBSD プロジェクトが LibreSSL に新規追加した TLS 通信用のプログラミング API です。
libtls はシンプルな API なので見通しの良い TLS 通信プログラムを書くことができます。
以下、libtls を使ったクライアント側のプログラミング例を紹介します。

---

# はじめに
libtls は、[OpenSSL](https://www.openssl.org/) と API 互換を基本方針としている [LibreSSL](http://www.libressl.org/) に、全く別の API 体系として追加されています。libtls は内部では OpenSSL の API である `SSL_xxx()` をコールしているので、OpenSSL API のラッパーとも言えますが、煩雑な部分をうまく隠蔽しているので見通しの良い API 体系になっています。

[OpenBSD プロジェクト](https://www.openbsd.org/)は TLS 通信を必要とするプログラムで libtls を使い始めています。httpd は libtls を使ったサーバ側のリファレンス実装ですし、nc(netcat) コマンドも libtls で書き直されて LibreSSL ポータブル版にも含まれています。

libtls の API は `tls_` で始まる関数名で統一されており、ライブラリ `libtls.*` をリンクすることで利用できます。libtls は LibreSSL に同梱されています。

## libtls の情報
libtls の API は `man tls_init` でマニュアルを参照できます。

## 環境
今回の確認は CentOS7(x86_64) で行いました。
LibreSSL は以下のコマンドにより /usr/local/ 配下にインストールしました。

```shell-session
# wget http://ftp.openbsd.org/pub/OpenBSD/LibreSSL/libressl-2.4.1.tar.gz
# tar xf libressl-2.4.1.tar.gz
# cd libressl-2.4.1/
# ./configure
# make check
# sudo make install
# export LD_LIBRARY_PATH=/usr/local/lib
```

# libtls クライアント プログラム の例
最初に、HTTP サーバに TLS 通信で GET リクエストを投げてレスポンスを受けるだけのクライアント プログラム を書いてみました。

```c:libtls_client1.c
#include <stdio.h>
#include <string.h>
#include <err.h>
#include <tls.h>

int
main(int argc, char *argv[])
{
	struct tls *ctx = NULL;
	ssize_t len;
	unsigned char buf[BUFSIZ];

	if (argc < 3) {
		fprintf(stderr, "usage: %s host port\n", argv[0]);
		return(1);
	}

	/*
	** (1) libtls の初期化
	*/

	if (tls_init() != 0)
		err(1, "tls_init:");

	/*
	** (2) TLS 通信のコンテキスト生成
	*/

	if ((ctx = tls_client()) == NULL)
		err(1, "tls_client:");

	/*
	** (3) HTTP サーバに TLS 接続
	*/

	if (tls_connect(ctx, argv[1], argv[2]) != 0)
		err(1, "tls_connect: %s", tls_error(ctx));

	/*
	** (4) HTTP 要求を TLS で送信
	*/

	sprintf(buf, "GET / HTTP/1.0\r\nHost: %s\r\n\r\n", argv[1]);

	if((len = tls_write(ctx, buf, strlen(buf))) < 0)
		err(1, "tls_write: %s", tls_error(ctx));

	/*
	** (5) HTTP 応答を TLS で受信
	*/

	while ((len = tls_read(ctx, buf, sizeof(buf))) > 0) {
		if (len == TLS_WANT_POLLIN || len == TLS_WANT_POLLOUT)
			continue;
		if (len < 0)
			err(1, "tls_read: %s", tls_error(ctx));
		else
			printf("%*.*s", (int)len, (int)len, buf);
	}

	/*
	** (6) TLS 接続をクローズ、コンテキストを解放
	*/

	if (tls_close(ctx) != 0)
		err(1, "tls_close: %s", tls_error(ctx));
	tls_free(ctx);

	return(0);
}
```

このソースコードを以下の Makefile で `make -f make1.mk` としてコンパイルします。

```bash:make1.mk
all :: libtls_client1

libtls_client1 : libtls_client1.o
	$(CC) -g -o $@ $^ -L/usr/local/lib -ltls -lssl -lcrypto

.c.o :
	$(CC) -g -c -o $@ $< -I/usr/local/include
```

このプログラムを実行すると、以下のように HTTP サーバからトップページの HTML を https で取って来ます。

```shell-session
# ./libtls_client1 www.openbsd.org 443
HTTP/1.0 200 OK
Connection: close
Content-Length: 4642
Content-Type: text/html
Date: Thu, 16 Jun 2016 11:12:10 GMT
Last-Modified: Wed, 18 May 2016 22:08:02 GMT
Server: OpenBSD httpd

<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN">
<html>
<head>
    <title>OpenBSD</title>
... (略) ...
</html>
libtls_client: tls_close: EOF without close notify: Success
# 
```

libtls はシンプルなので、API の使い方に頭を悩ませることがありません。
libtls を使うと TLS 通信プログラムを書くのが簡単になると思います。

# もう少し高度な要求への対応
上の例では、libtls の基礎を確認するために最小限の API だけを使いました。
次に、もう少し高度な要求への対応方法を見てみます。

## プロトコルや暗号化方式を指定する方法
上の例は、プロトコルや暗号化方式を全て libtls のデフォルトにお任せでした。
libtls でこれらを明示的に指定するにはどうすればよいかを見てみます。

以下コード例ですが、これらの設定を行うには `tls_config_xxx()` シリーズの API を使います。

```c
...
    /*
	** プロトコルと暗号化方式の指定
	*/

	if ((cfg = tls_config_new()) == NULL)
		err(1, "tls_config_new:");

	if (tls_config_parse_protocols(&protocols, "all") != 0)
		err(1, "tls_config_parse_protocols: %s", tls_error(ctx));

	tls_config_set_protocols(cfg, protocols);

	if (tls_config_set_ciphers(cfg, "CAMELLIA:AESGCM") != 0)
		err(1, "tls_config_set_ciphers: %s", tls_error(ctx));

	if (tls_configure(ctx, cfg) != 0)
		err(1, "tls_configure: %s", tls_error(ctx));
...
```

`tls_config_new()` で cfg を作っておき、`tls_config_xxx()` シリーズの API で種々の設定を行い、`tls_configure()` で設定をコンテキストに結びつけるという手順になります。
上の例では、プロトコルを "all" としており、これは "tlsv1.0,tlsv1.1,tlsv1.2" と同じ意味になります。
暗号化方式は "CAMELLIA:AESGCM" を指定しています。

コード例にはありませんが、CA証明書の設定、クライアント証明書の設定、秘密鍵の設定、等も `tls_config_xxx()` シリーズの API で行うことができます。

## TLS セッション情報を得る方法
TLS 通信は実際の送受信が始まる前にハンドシェイクによってプロトコルや暗号化方式をサーバとクライアントの間で取り決めています。
取り決めにより決定したのが、どんなプロトコルでどんな暗号化方式なのか等のセッション情報を得る libtls API を見てみます。

以下コード例です。libtls はこれらの情報を得る API を用意しており、使用中の有効な ctx に対して実行することができます。

```c
...
	if ((p = tls_peer_cert_issuer(ctx)) == NULL)
		err(1, "tls_peer_cert_issuer: %s", tls_error(ctx));
	printf("tls_peer_cert_issuer: %s\n", p);

	if ((p = tls_peer_cert_subject(ctx)) == NULL)
		err(1, "tls_peer_cert_subject: %s", tls_error(ctx));
	printf("tls_peer_cert_subject: %s\n", p);
...
	if ((p = tls_conn_version(ctx)) == NULL)
		err(1, "tls_conn_version: %s", tls_error(ctx));
	printf("tls_conn_version: %s\n", p);

	if ((p = tls_conn_cipher(ctx)) == NULL)
		err(1, "tls_conn_cipher: %s", tls_error(ctx));
	printf("tls_conn_cipher: %s\n", p);
...
```

これらの API は TLS のハンドシェイクが行われた後でないと実行できません。
libtls においてはハンドシェイクは `tls_read()` や `tls_write()` が実行されると自動的に背後で行われますが、明示的に行いたい場合は `tls_handshake()` を使うことができます。

## もう少し高度な要求に対応したプログラム例
プロトコルと暗号化方式を指定し、TLS セッション情報を表示するプログラムが以下になります。

```c:libtls_client2.c
#include <stdio.h>
#include <string.h>
#include <err.h>
#include <tls.h>

int dump_session_info(struct tls *ctx, const char *name);

int
main(int argc, char *argv[])
{
	struct tls *ctx = NULL;
	struct tls_config *cfg = NULL;
	uint32_t protocols;
	ssize_t len;
	unsigned char buf[BUFSIZ];

	if (argc < 3) {
		fprintf(stderr, "usage: %s host port\n", argv[0]);
		return(1);
	}

	/*
	** (1) libtls の初期化
	*/

	if (tls_init() != 0)
		err(1, "tls_init:");

	/*
	** (2) TLS 通信のコンテキスト生成
	*/

	if ((ctx = tls_client()) == NULL)
		err(1, "tls_client:");

	/*
	** プロトコルと暗号化方式の指定
	*/

	if ((cfg = tls_config_new()) == NULL)
		err(1, "tls_config_new:");

	if (tls_config_parse_protocols(&protocols, "all") != 0)
		err(1, "tls_config_parse_protocols: %s", tls_error(ctx));

	tls_config_set_protocols(cfg, protocols);

	if (tls_config_set_ciphers(cfg, "CAMELLIA:AESGCM") != 0)
		err(1, "tls_config_set_ciphers: %s", tls_error(ctx));

	if (tls_configure(ctx, cfg) != 0)
		err(1, "tls_configure: %s", tls_error(ctx));

	/*
	** (3) HTTP サーバに TLS 接続
	*/

	if (tls_connect(ctx, argv[1], argv[2]) != 0)
		err(1, "tls_connect: %s", tls_error(ctx));

	/*
	** (4) HTTP 要求を TLS で送信
	*/

	sprintf(buf, "GET / HTTP/1.0\r\nHost: %s\r\n\r\n", argv[1]);

	if((len = tls_write(ctx, buf, strlen(buf))) < 0)
		err(1, "tls_write: %s", tls_error(ctx));

	/*
	** セッション情報のダンプ
	*/

	dump_session_info(ctx, argv[1]);

	/*
	** (5) HTTP 応答を TLS で受信
	*/

	while ((len = tls_read(ctx, buf, sizeof(buf))) > 0) {
		if (len == TLS_WANT_POLLIN || len == TLS_WANT_POLLOUT)
			continue;
		if (len < 0)
			err(1, "tls_read: %s", tls_error(ctx));
		else
			printf("%*.*s", (int)len, (int)len, buf);
	}

	/*
	** (6) TLS 接続をクローズ、cfgとコンテキストを解放
	*/

	if (tls_close(ctx) != 0)
		err(1, "tls_close: %s", tls_error(ctx));
	tls_config_free(cfg);
	tls_free(ctx);

	return(0);
}
```

以下は TLS セッション情報をダンプする関数のソースコードです。

```c:libtls_util.c
#include <stdio.h>
#include <time.h>
#include <err.h>
#include <tls.h>

int
dump_session_info(struct tls *ctx, const char *name)
{
	const char *p;
	time_t time;
	struct tm *tm;

	if (tls_peer_cert_provided(ctx) == 1)
		printf("tls_peer_cert_provided: YES\n");
	else {
		printf("tls_peer_cert_provided: NO\n");
		return(-1);
	}
	
	if (tls_peer_cert_contains_name(ctx, name) == 1)
		printf("tls_peer_cert_contains_name: %s\n", name);
	else
		printf("tls_peer_cert_contains_name: invalid\n");

	if ((p = tls_peer_cert_issuer(ctx)) == NULL)
		err(1, "tls_peer_cert_issuer: %s", tls_error(ctx));
	printf("tls_peer_cert_issuer: %s\n", p);

	if ((p = tls_peer_cert_subject(ctx)) == NULL)
		err(1, "tls_peer_cert_subject: %s", tls_error(ctx));
	printf("tls_peer_cert_subject: %s\n", p);

	if ((p = tls_peer_cert_hash(ctx)) == NULL)
		err(1, "tls_peer_cert_hash: %s", tls_error(ctx));
	printf("tls_peer_cert_hash: %s\n", p);

	if ((time = tls_peer_cert_notbefore(ctx)) < 0)
		err(1, "tls_peer_cert_notbefore: %s", tls_error(ctx));
	tm = localtime(&time);
	printf("tls_peer_cert_notbefore: %04d/%02d/%02d %02d:%02d:%02d\n",
		1900 + tm->tm_year, tm->tm_mon + 1, tm->tm_mday,
		tm->tm_hour, tm->tm_min, tm->tm_sec);

	if ((time = tls_peer_cert_notafter(ctx)) < 0)
		err(1, "tls_peer_cert_notafter: %s", tls_error(ctx));
	tm = localtime(&time);
	printf("tls_peer_cert_notafter: %04d/%02d/%02d %02d:%02d:%02d\n",
		1900 + tm->tm_year, tm->tm_mon + 1, tm->tm_mday,
		tm->tm_hour, tm->tm_min, tm->tm_sec);

	if ((p = tls_conn_version(ctx)) == NULL)
		err(1, "tls_conn_version: %s", tls_error(ctx));
	printf("tls_conn_version: %s\n", p);

	if ((p = tls_conn_cipher(ctx)) == NULL)
		err(1, "tls_conn_cipher: %s", tls_error(ctx));
	printf("tls_conn_cipher: %s\n", p);

	return(0);
}
```

これらのソースコードを以下の Makefile で `make -f make2.mk` としてコンパイルします。

```bash:make2.mk
all :: libtls_client2

libtls_client2 : libtls_client2.o libtls_util.o
	$(CC) -g -o $@ $^ -L/usr/local/lib -ltls -lssl -lcrypto

.c.o :
	$(CC) -g -c -o $@ $< -I/usr/local/include
```

このプログラムを実行すると、以下のように HTTP サーバとのハンドシェイクにより入手した証明書の情報や、取り決められたプロトコル(TLSv1.2)と暗号化方式(ECDHE-RSA-AES256-GCM-SHA384)の情報が得られるのが分かります。

```shell-session
# ./libtls_client2 www.openbsd.org 443
tls_peer_cert_provided: YES
tls_peer_cert_contains_name: www.openbsd.org
tls_peer_cert_issuer: /C=US/O=Let's Encrypt/CN=Let's Encrypt Authority X3
tls_peer_cert_subject: /CN=www.openbsd.org
tls_peer_cert_hash: SHA256:b8d17ff36a985d8cd511281e96a2365d20d5e3bf72e6e2abba583f0ccf70fbb4
tls_peer_cert_notbefore: 2016/05/09 13:50:00
tls_peer_cert_notafter: 2016/08/07 13:50:00
tls_conn_version: TLSv1.2
tls_conn_cipher: ECDHE-RSA-AES256-GCM-SHA384
HTTP/1.0 200 OK
Connection: close
Content-Length: 4642
Content-Type: text/html
Date: Thu, 16 Jun 2016 14:27:31 GMT
Last-Modified: Wed, 18 May 2016 22:08:02 GMT
Server: OpenBSD httpd

<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN">
<html>
<head>
    <title>OpenBSD</title>
... (略) ...
</html>
libtls_client2: tls_close: EOF without close notify: Success
#
```

# まとめ
LibreSSL の libtls API を使った TLS 通信のプログラミングを確認しました。
TLS 通信処理を libtls に任せることで見通しの良いソースコードが書けると思います。
今回はクライアント側に話を絞ってプログラム例を書きましたが、サーバ側でも同様にプログラミングができます。