Home > ブログ > OpenSSLでのサーバー証明書の検証

ブログ

OpenSSLでのサーバー証明書の検証

今回はOpenSSLでのサーバー証明書の検証に関する話です。といってもopensslコマンドではなく、OpenSSLライブラリのAPIの方の話になります。

ブラウザからhttpsアクセスした場合など、TLS/SSLでサーバーと接続する際、通常はサーバー証明書の検証を行います。C言語などからOpenSSLのAPIを使ってTLS接続する場合でも、検証モードをSSL_VERIFY_PEERに設定しておけばOpenSSLは証明書の検証を行います。ただし、署名の内容と有効期限はチェックしますが、デフォルトではホスト名の検証(*1)は行いません。

私は仕事でお客様のソフトウェアに機能追加したり不具合を調査することがありますが、OpenSSLを使って自前でTLS接続処理を書いている場合に、ホスト名の検証が抜けてしまっているケースを何度かみかけました。

これは私の周りでたまたまよく見かけたというわけではなく、OpenSSLのwikiにもよくある間違いとして説明されています。

One common mistake made by users of OpenSSL is to assume that OpenSSL will validate the hostname in the server's certificate. Versions prior to 1.0.2 did not perform hostname validation. Version 1.0.2 and up contain support for hostname validation, but they still require the user to call a few functions to set it up.

よくある間違いの一つは、OpenSSLがサーバー証明書のホスト名検証を行うと仮定しているものだ。バージョン1.0.2まではホスト名検証は行っていなかった。1.0.2以降はホスト名検証をサポートしたが、それを使うにはユーザはいくつかの関数を呼び出す必要がある。

https://wiki.openssl.org/index.php/Hostname_validation より引用。

ホスト名検証を行うには上記リンク先にも記載されていますが、X509_VERIFY_PARAMを設定してやる必要があります。

	X509_VERIFY_PARAM *param;

	param = SSL_get0_param(ssl);  /* SSL *ssl */

	X509_VERIFY_PARAM_set_hostflags(param, X509_CHECK_FLAG_NO_PARTIAL_WILDCARDS);
	if (!X509_VERIFY_PARAM_set1_host(param, hostname, 0)) {
		ERR_print_errors_fp(stderr);
	}

上記コードだけでは全体像がつかみづらいので、TLS接続をしてHTTP Requestを行う全体のコードを記載しておきます(*2)。プラットフォームはLinux、OpenSSLはVersion. 1.1.1を使っています。

https接続時にホスト名検証も行う例

#include <stdio.h>
#include <netdb.h>
#include <netinet/in.h>
#include <string.h>
#include <sys/socket.h>
#include <unistd.h>
#include <openssl/err.h>
#include <openssl/ssl.h>
#include <openssl/x509v3.h>

int connect_to_remote(const char* hostname, unsigned short port)
{
	struct hostent *ent;
	struct sockaddr_in remote;
	int sock;

	ent = gethostbyname(hostname);
	if (ent == NULL) {
		herror("gethostbyname()");
		return -1;
	}
	if (ent->h_addrtype != AF_INET) {
		fprintf(stderr, "Can' get ipv4 address.\n");
		return -1;
	}

	memset(&remote, 0, sizeof(remote));
	remote.sin_family = AF_INET;
	remote.sin_port = htons(port);
	memcpy(&remote.sin_addr, ent->h_addr, sizeof(remote.sin_addr));

	sock = socket(AF_INET, SOCK_STREAM, 0); 
	if (sock == -1) {
		perror("socket()");
		return -1;
	}

	if (connect(sock, (struct sockaddr*) &remote, sizeof(remote)) == -1) {
		perror("connect()");
		close(sock);
		return -1;
	}

	return sock;
}

void https_get(SSL *ssl, const char *hostname)
{
	char buff[1024];
	int size;

	size = snprintf(buff, sizeof(buff), "GET / HTTP/1.1\r\nHost: %s\r\nConnection: Close\r\n\r\n", hostname);
	SSL_write(ssl, buff, size);

	while ((size = SSL_read(ssl, buff, sizeof(buff) - 1)) > 0) {
		buff[size] = '\0';
		printf(buff);
	}
}

/* for Debug */
int verify_callback(int preverified, X509_STORE_CTX *ctx)
{
	X509* cert;
	char subject[1024];

	cert = X509_STORE_CTX_get_current_cert(ctx);
	if (cert == NULL) {
		return 0;
	}
	X509_NAME_oneline(X509_get_subject_name(cert), &subject[0], sizeof(subject));
	printf("%d %s\n", preverified, subject);

	return preverified;
}

/* ホスト名検証を行うようにする */
void enable_hostname_validation(SSL *ssl, const char *hostname)
{
	X509_VERIFY_PARAM *param;

	param = SSL_get0_param(ssl);

	X509_VERIFY_PARAM_set_hostflags(param, X509_CHECK_FLAG_NO_PARTIAL_WILDCARDS);
	if (!X509_VERIFY_PARAM_set1_host(param, hostname, 0)) {
		ERR_print_errors_fp(stderr);
	}
}

int main()
{
	const char *hostname ="www.example.com";
	int sock = -1;
	SSL_CTX *ctx = NULL;
	SSL *ssl = NULL;

	sock = connect_to_remote(hostname, 443);
	if (sock == -1) {
		return 1;
	}

	/*
	 * OpenSSL1.1.0以降では以下の初期化関数はdeprecatedになった。
	 * 代わりに OPENSSL_init_ssl() を使う。
	 * ただし、初期化処理は自動で行われるので、デフォルト設定以外の
	 * 初期化をしたい時を除けば明示的に呼び出す必要はない。
	 */
#if 0
	SSL_load_error_strings();
	SSL_library_init();
#endif

	ctx = SSL_CTX_new(TLS_client_method());
	if (ctx == NULL) {
		ERR_print_errors_fp(stderr);
		goto error;
	}

	SSL_CTX_set_mode(ctx, SSL_MODE_AUTO_RETRY);

	if (SSL_CTX_set_min_proto_version(ctx, TLS1_2_VERSION) == 0) {
		ERR_print_errors_fp(stderr);
		goto error;
	}

	if (SSL_CTX_set_default_verify_paths(ctx) == 0) {
		ERR_print_errors_fp(stderr);
		goto error;
	}

	/* 証明書の検証設定 */
	SSL_CTX_set_verify(ctx, SSL_VERIFY_PEER, verify_callback);

	ssl = SSL_new(ctx);
	if (ssl == NULL) {
		ERR_print_errors_fp(stderr);
		goto error;
	}

	/* ホスト名検証を行うようにする */
	enable_hostname_validation(ssl, hostname);

	/* For SNI */
	SSL_set_tlsext_host_name(ssl, hostname);

	if (SSL_set_fd(ssl, sock) == 0) {
		ERR_print_errors_fp(stderr);
		goto error;
	}

	/* TLS handshake開始 */
	if (SSL_connect(ssl) <= 0) {
		ERR_print_errors_fp(stderr);
		goto error;
	}

	https_get(ssl, hostname);

	SSL_shutdown(ssl);

	SSL_free(ssl); 
	SSL_CTX_free(ctx);
	close(sock);
	/*
	 * 初期化関数同様、この関数もdeprecatedとなった。
	 * 現在は単なるdefineで何もしていない。
	 */
#if 0
	ERR_free_strings();
#endif

	return 0;

 error:
	if (ssl) {
		SSL_free(ssl); 
	}
	if (ctx) {
		SSL_CTX_free(ctx);
	}
	if (sock != -1) {
		close(sock);
	}
#if 0
	ERR_free_strings();
#endif

	return 1;
}

最近またホスト名検証をしていないケースを見かけたのでまとめてみました。

脱線しますが経験上、ホスト名検証の他にSSL_set_tlsext_host_name()を忘れているケースもよくあります。この場合、SNIが動作しないため、サーバー側の仮想ホストを増やした時などに証明書エラーで接続できなくなり問題が発覚するパターンです。

単純にRESTへのアクセス等でhttpsアクセスしたいだけなら、curl等のライブラリを使った方がよいと思います。

おまけでC++(Boost.asio)からTLS接続する例も記載しておきます。

Boost.asioではホスト名検証用にasio::ssl::rfc2818_verificationファンクタが用意されています。これをverify callbackに登録して呼び出すことでホスト名検証を行っています(OpenSSLの実装は使っていない)。

Boost.asioを使ってhttpsアクセスする例

#include <iostream>
#include <stdexcept>
#include <sstream>
#include <string>
#include <boost/asio.hpp>
#include <boost/asio/ssl.hpp>

void usage(const char* message = nullptr) {
  if (message) {
    std::cerr << message << std::endl << std::endl;
  }
  std::cerr << "Usage:" << std::endl;
  std::cerr << "tls-connect <hostname>" << std::endl << std::endl;

  exit(1);
}

int main(int argc, char *argv[]) {
  namespace asio = boost::asio;

  if (argc < 2) {
    usage();
  }

  std::string host(argv[argc - 1]);
  uint16_t port = 443;

  asio::io_context context;
  asio::ip::tcp::resolver resolver(context);
  auto endpoints = resolver.resolve(asio::ip::tcp::v4(), host, "");
  boost::asio::ip::address remote_address(endpoints.begin()->endpoint().address());

  asio::io_context io_context;

  asio::ssl::context ctx(asio::ssl::context::tlsv12_client);
  ctx.set_default_verify_paths();

  asio::ssl::stream<asio::ip::tcp::socket> stream(io_context, ctx);

  stream.lowest_layer().connect(asio::ip::tcp::endpoint(remote_address, port));

  // for SNI
  SSL_set_tlsext_host_name(stream.native_handle(), host.c_str());

  stream.set_verify_mode(asio::ssl::verify_peer);

  // ホスト名検証を行わせる
  stream.set_verify_callback(asio::ssl::rfc2818_verification(host));

  stream.handshake(boost::asio::ssl::stream_base::client);

  std::ostringstream request_stream;
  request_stream << "GET / HTTP/1.1\r\n"
                 << "Host: " << host << "\r\n"
                 << "Connection: Close\r\n"
                 << "\r\n";
  asio::write(stream, asio::buffer(request_stream.str()));

  boost::system::error_code ec;
  asio::streambuf buffer;
  asio::read(stream, buffer, asio::transfer_all(), ec);
  if (ec && ec != asio::error::eof) {
    throw std::runtime_error(ec.message());
  }
  std::cout << asio::buffer_cast<const char*>(buffer.data()) << std::endl;

  stream.lowest_layer().close();

  std::cout << "done" << std::endl;

  return 0;
}

ビルド方法

g++ -Wall -O2 -std=c++17 -lboost_system -lpthread `pkg-config --libs openssl` tls-connect.cc

[関連記事]

[追記]

OpenSSL 1.1.0からDeprecatedになった初期化関数 SSL_library_init() 等の呼び出しを削除しました。(2022.3.4追記)

証明書の検証とは関係ありませんが、SSL_MODE_AUTO_RETRYの設定を追加しました。(2022.3.22追記)

(*1) アクセスしたサーバーのホスト名と証明書内のCommon NameやSANがマッチしているか確認する作業。

(*2) 証明書の失効チェックは行っていないので、プロダクトレベルではさらにCRL(Certificate Revocation List)やOCSP(Online Certificate Status Protocol)による失効チェックが必要になる場合もあるでしょう。

投稿日:2020/04/07 10:44(最終更新:2022/03/22 15:43)

タグ: C++ ネットワーク プログラミング C OpenSSL

ネットワーク関連のソフトウェア開発でしたらおまかせください。
詳細はこちら

Top

アーカイブ

タグ

Server (28) 作業実績 (21) PHP (19) ネットワーク (17) プログラミング (15) OpenSSL (10) C (8) C++ (8) PHP関連更新作業 (8) EC-CUBE (7) Webアプリ (7) laravel (6) 書籍 (5) Nginx (5) Linux (5) AWS (4) Vue.js (4) JavaScript (4) 与太話 (4) Rust (3) Symfony (2) お知らせ (2) Golang (2) OSS (1) MySQL (1) デモ (1) CreateJS (1) Apache (1)