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を行う全体のコードを記載しておきます。プラットフォームはLinux、OpenSSLはVersion. 1.1.1を使っています。

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

#include <stdio.h>
#include <netdb.h>
#include <netinet/in.h>
#include <strings.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;
	}

	bzero(&remote, sizeof(remote));
	remote.sin_family = AF_INET;
	remote.sin_port = htons(port);
	bcopy(ent->h_addr, &remote.sin_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()");
		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.bit-hive.com";
	int sock = -1;
	SSL_CTX *ctx = NULL;
	SSL *ssl = NULL;

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

	SSL_load_error_strings();
	SSL_library_init();

	ctx = SSL_CTX_new(TLS_client_method());
	if (ctx == NULL) {
		ERR_print_errors_fp(stderr);
		goto error;
	}
	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);
	ERR_free_strings();

	return 0;

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

	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_service io_service;

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

  asio::ssl::stream<asio::ip::tcp::socket> stream(io_service, 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

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

投稿日:2020/04/07 10:44

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

Top

アーカイブ

タグ

作業実績 (10) Server (9) C++ (5) Webアプリ (5) Linux (4) PHP (4) JavaScript (3) laravel (3) 書籍 (2) プログラミング (2) ネットワーク (2) Nginx (2) Vue.js (2) Golang (2) EC-CUBE (2) C (1) デモ (1) CreateJS (1) AWS (1)

技術的な情報は以下にもあります。