Home > ブログ > OpenSSLでX.509証明書のホスト名検証を行う際のフラグ指定

ブログ

OpenSSLでX.509証明書のホスト名検証を行う際のフラグ指定

httpsなどでTLS接続する際、サーバー証明書の検証が行われ、その中で証明書のホスト名が接続先のホスト名と一致するかどうかがチェックされます。 OpenSSLではX509_check_host(), X509_VERIFY_PARAM_set_hostflags()の関数に与えるフラグで、このホスト名の検証動作を変更することができます。

X509_check_host(), X509_VERIFY_PARAM_set_hostflags()で指定できるフラグには以下のものがあります。

  • X509_CHECK_FLAG_ALWAYS_CHECK_SUBJECT
  • X509_CHECK_FLAG_NEVER_CHECK_SUBJECT
  • X509_CHECK_FLAG_NO_WILDCARDS
  • X509_CHECK_FLAG_NO_PARTIAL_WILDCARDS
  • X509_CHECK_FLAG_MULTI_LABEL_WILDCARDS
  • X509_CHECK_FLAG_SINGLE_LABEL_SUBDOMAINS

様々なフラグがありますが、今回、各々のフラグがどのような動作をするのかを説明したいと思います。 各フラグの動作自体は man X509_check_host をみればいいので、関連するRFCなどのドキュメントを参照しながらもう少し詳しく説明することを試みたいと思います。

ちなみに関連するRFCは以下になります。

  • RFC2818 - HTTP Over TLS
  • RFC6125 - Representation and Verification of Domain-Based Application Service Identity within Internet Public Key Infrastructure Using X.509 (PKIX) Certificates in the Context of Transport Layer Security (TLS)
  • RFC9110 - HTTP Semantics

各フラグの動作

それでは順番に見ていきましょう。

(1) X509_CHECK_FLAG_ALWAYS_CHECK_SUBJECT

このフラグの説明の前提知識として、X.509証明書にはホスト名が設定される場所としてSubjectフィールドのコモンネームとSAN(Subject Alt Name)拡張の2箇所があります。

証明書の内容をopensslコマンドで確認すると以下のようになっています。SubjectとSANにそれぞれFQDNが確認できます(青色箇所)。

Certificate:
    Data:
        Version: 3 (0x2)
        Serial Number:
            78:65:5b:44:d4:c4:46:48:b7:f4:4e:db:ed:14:62:cf:02:ff:ff:be
        Signature Algorithm: sha256WithRSAEncryption
略
        Subject: C = JP, ST = Tokyo, L = Ohtaku, O = Example, OU = Example, CN = example.com
略
        X509v3 extensions:
            X509v3 Subject Alternative Name: 
                DNS:www.example.com, DNS:example.com

ホスト名の検証では、証明書内のこれらのホスト名と接続先のホスト名が一致するかどうかをチェックします。

デフォルト(X509_CHECK_FLAG_ALWAYS_CHECK_SUBJECTが未指定の状態)ではSAN(Subject Alternative Name)拡張にDNS名が存在する場合は、SANのDNS名のみがマッチングに使用され、Subjectのコモンネームは無視されます。SANにDNS名がない時のみコモンネームが参照されます。

つまり、以下のような証明書では、

CN example.com
SAN DNS:www.example.com

www.example.comはマッチしますが、example.comはマッチしません(証明書エラー)。

このため、最近の証明書では、コモンネームに指定されているドメイン名は、SANにも指定するようになっています。 TLS証明書を購入する際、コモンネームにexample.comを指定したCSRを送ると、SANにもexample.comが登録された証明書を発行してくれます(そして、www.付きのFQDNもSANに登録されていることが多いはずです)。

CN example.com
SAN DNS:www.example.com, DNS:example.com

この証明書であれば、www.example.comもexample.comもマッチします。

これがデフォルトの動作ですが、X509_CHECK_FLAG_ALWAYS_CHECK_SUBJECTフラグを指定した場合は、SANにDNS名があった場合でも、コモンネームがチェックされるようになります。

元々ホスト名検証時はSANもSubjectのコモンネームもチェックされていたのですが、現在では、SANのDNS名が存在する場合はコモンネームは使われないようになっています。このあたりの動作は、RFC2818に記述されています。

RFC2818 - HTTP Over TLS

3.1. Server Identity

If a subjectAltName extension of type dNSName is present, that MUST be used as the identity. Otherwise, the (most specific) Common Name field in the Subject field of the certificate MUST be used. Although the use of the Common Name is existing practice, it is deprecated and Certification Authorities are encouraged to use the dNSName instead.

dNSName型のSAN拡張が存在するならIDとして使用しなければならない[MUST]。 そうでなければ、Subjectフィールドのコモンネームが使われなければならない[MUST]。 コモンネームの使用は既存の慣行だが、非推奨であり、認証局は dNSName を使用することが推奨される。

その後、RFC2818はRFC9110で更新されています。

RFC9110 - HTTP Semantics

4.3.4. https Certificate Verification

A reference identity of type CN-ID MUST NOT be used by clients. As noted in Section 6.2.1 of [RFC6125], a reference identity of type CN-ID might be used by older clients.

CN-ID型の参照ID(証明書のコモンネームのこと)は、クライアントが使用してはならない[MUST NOT]。RFC6125の6.2.1節で記述されているように、CN-ID型の参照IDは、 古いクライアントによって使用される可能性がある。

より強い、「クライアントは使用してはいけない」という記述になっています。

RFC6125にも以下のような記述があります。

RFC6125

6.4.4. Checking of Common Names

As noted, a client MUST NOT seek a match for a reference identifier of CN-ID if the presented identifiers include a DNS-ID, SRV-ID, URI-ID, or any application-specific identifier types supported by the client.

前述のように、提示された識別子がDNS-ID、SRV-ID、URI-ID、またはクライアント がサポートするアプリケーション固有の識別子タイプを含む場合、クライアントはCN-IDの参照識別子の一致を求めてはならない。[MUST NOT]

というわけで、現在では証明書のホスト名検証においてコモンネームは使わないようになっています。そもそも、なぜコモンネームが使われなくなったかというと以下のドキュメントが参考になります。

RFC6125

2.3. Subject Naming in PKIX Certificates

However, the Common Name is not strongly typed because a Common Name might contain a human-friendly string for the service, rather than a string whose form matches that of a fully qualified DNS domain name (a certificate with such a single Common Name will typically have at least one subjectAltName entry containing the fully qualified DNS domain name):

    CN=A Free Chat Service,O=Example Org,C=GB

しかし、コモンネームはDNSドメイン名の形式ではなく、人間に優しい文字列を含めることができるので強く型付けされていない。 (このような単一コモンネームの証明書は、通常、完全修飾ドメイン名(FQDN)を含む少なくとも1つのsubjectAltNameエントリを持つ)

https://developer.chrome.com/blog/chrome-58-deprecations/#remove_support_for_commonname_matching_in_certificates

The use of the subjectAlternativeName fields leaves it unambiguous whether a certificate is expressing a binding to an IP address or a domain name, and is fully defined in terms of its interaction with Name Constraints. However, the commonName is ambiguous, and because of this, support for it has been a source of security bugs in Chrome, the libraries it uses, and within the TLS ecosystem at large.

subjectAlternativeNameフィールドの使用は、証明書がIPアドレスとドメイン名のどちらにバインドされているかが明確になり、Name Constraintsとの相互作用が完全に定義される。しかし、コモンネームは曖昧であるため、Chromeやライブラリ、そしてTLSエコシステム全体でセキュリティバグの原因となっている。

まとめると、コモンネームでは任意の文字列を入れられるので、明確にDNS名だと型付けできるSANを使おうということになったようです。

Chromeなどは早い段階(March 2017)でコモンネームのチェックをやめており(*1)、当時少し話題になったような記憶があります。

コモンネームを使用しなくなりはじめた頃は、互換性のために本フラグを設定することもあったかもしれませんが、現在ではあまり出番はないのではないでしょうか。

(2) X509_CHECK_FLAG_NEVER_CHECK_SUBJECT

(1)で説明したとおり、デフォルトではSANにDNS名エントリがない場合は、Subjectのコモンネームを参照しますが、本フラグを設定すると、SANにDNSエントリがなくてもSubjectを参照しないようになります。コモンネームを完全に無視し、RFC9110により厳密に従う形になります。

(3) X509_CHECK_FLAG_NO_WILDCARDS

証明書に設定されるFQDNにはワイルドカードを使うことができます(いわゆるワイルドカード証明書)。これによりドメイン内のサブドメインをまとめてひとつの証明書でカバーすることができます。

OpenSSLでもワイルドカード証明書の検証に対応していますが、本フラグを設定するとワイルドカード(*)のマッチングを行わないようになります。例えばwww.example.comは*.example.comの証明書にマッチしなくなります。

ワイルドカード証明書は実際に使用もされていますが、RFC6125 7.2節によると、そもそもワイルドカード(*)は使うべきではないとあります。

RFC6125

7.2. Wildcard Certificates

This document states that the wildcard character '*' SHOULD NOT be included in presented identifiers but MAY be checked by application clients (mainly for the sake of backward compatibility with deployed infrastructure). As a result, the rules provided in this document are more restrictive than the rules for many existing application technologies (such as those excerpted under Appendix B). Several security considerations justify tightening the rules:

この文書は、ワイルドカード文字 '*' が識別子に含まれるべきではないが[SHOULD NOT]、アプリケーションクライアントによってチェックしてもいい[MAY]ことを述べる(主にデプロイされているインフラストラクチャとの後方互換性のため)。結果として、この文書で提供されるルールは、多くの既存のアプリケーション技術のルール(付録Bに抜粋されたものなど)よりも制約が多い。いくつかのセキュリティ上の考慮事項が、ルールを厳格化する正当な理由となる。

7.2節の他の記述も読むと、FQDN内でワイルドカード文字を使える場所の仕様が明確でないせいでワイルドカードの処理が曖昧になり、本来マッチすべきでないホスト名が検証に通ってしまったりと、セキュリティバグが発生しそうなので、ワイルドカードの使用は推奨しないということのようです。実際、EV証明書ではEV SSL Certificate Guidelines(*2)により、ワイルドカード証明書の使用は禁止されています。

本フラグを設定して、ワイルドカードのマッチングを拒否することで、誤ったホスト名マッチングをするリスクを排除できます。 ただ現状、実際にワイルドカード証明書は存在するので、一般的な環境では本フラグを設定することはないでしょう。

高いセキュリティが求められ(接続先も決まっている)、ネットバンキングアプリなどでは使えるのかもしれません。

(4) X509_CHECK_FLAG_NO_PARTIAL_WILDCARDS

本フラグを設定すると、prefixやsuffixがあるワイルドカード(www*, *www)のマッチングを行わないようになります。例えば、www1.example.com は www*.example.comの証明書にマッチしなくなります。

partial wildcardの扱いについては、https://en.wikipedia.org/wiki/Wildcard_certificateに簡潔にまとめられています。

However, use of "partial-wildcard" certs is not recommended. As of 2011, partial wildcard support is optional, and is explicitly disallowed in SubjectAltName headers that are required for multi-name certificates.[11] All major browsers have deliberately removed support for partial-wildcard certificates;[12][13]

部分的なワイルドカードの使用は推奨されていない。2011年時点では、部分的なワイルドカードのサポートはオプションであり、マルチネーム証明書に必要なSubjectAltNameヘッダーでは明示的に禁じられている。主要なブラウザは、部分的なワイルドカード証明書のサポートを意図的に削除している。

ということで、partial wildcard証明書は使われていないようです。私も見たことはありません。

本オプションは設定してもしなくてもどちらでもいいとは思いますが、私は https://wiki.openssl.org/index.php/Hostname_validation のサンプルコードを参考にして、設定していたりします。

(5) X509_CHECK_FLAG_MULTI_LABEL_WILDCARDS

このフラグもワイルドカードの扱いを指定するフラグです。

デフォルトではhost.www.example.comは*.example.comの証明書にはマッチしませんが(*は一つのラベルにしかマッチしない)、本フラグを設定するとマッチするようになります。

ワイルドカードのマッチングルールは、RFC6125の6.4.3節に記述があります

RFC6125

6.4.3. Checking of Wildcard Certificates

2. If the wildcard character is the only character of the left-most label in the presented identifier, the client SHOULD NOT compare against anything but the left-most label of the reference dentifier (e.g., *.example.com would match foo.example.com but not bar.foo.example.com or example.com).

ワイルドカード文字が識別子の最も左側のラベルの唯一の文字である場合、クライアントは参照識別子の最も左側のラベル以外とは比較すべきではない(例えば、*.example.comはfoo.example.comには一致するが、bar.foo.example.comやexample.comには一致しない)。

上記から、本来ワイルドカード文字(*)は一つのラベルにしかマッチすべきではありませんが、本フラグを設定すると複数のラベルにもマッチするようになります。

(6) X509_CHECK_FLAG_SINGLE_LABEL_SUBDOMAINS

OpenSSLではドットで始まるホスト名は証明書の任意のサブドメインにマッチします。 例えば.exampleはwww.example.comにもwww.sub.example.comにもマッチします(example.comにはマッチしない)。

本フラグが設定されていると直下のサブドメインにしかマッチしなくなります。 例えば、.exampleはwww.example.comにはマッチするが、www.sub.example.comにはマッチしなくなります。

このドットで始まるホスト名が証明書の任意のサブドメインにマッチする機能はOpenSSL独自の機能で関連するRFCはありません。ワイルドカード証明書がサーバー(証明書)サイドのワイルドカードとすれば、クライアントサイドのワイルドカードと考えればよいかもしれません。

この機能について参考になる記述は以下くらいでしょうか。

https://www.spinics.net/lists/openssl-users/msg15053.html

「ドットで始まるホスト名が特別であるという考えは、私が見てきたどのRFCにもなかった。だれかこれについて教えてくれ。」という質問に対し、

「これらはローカルな問題(APIの詳細)であり、どのRFCにも関係しない。 特定のホスト名を要求する検証者は、この機能に影響を受けない。 しかし、OpenSSLに".example.com"を証明書と照合するように要求する検証者は、「曖昧な」一致を指定している。」とOpenSSL独自の機能であることを回答しています。

https://github.com/openssl/openssl

commit a09e4d24ada871ed0e6f5e37fadd52a76b29542a
Author: Viktor Dukhovni 
Date:   Thu Jun 12 01:56:31 2014 -0400

    Client-side namecheck wildcards.
    
    A client reference identity of ".example.com" matches a server
    certificate presented identity that is any sub-domain of "example.com"
    (e.g. "www.sub.example.com).
    
    With the X509_CHECK_FLAG_SINGLE_LABEL_SUBDOMAINS flag, it matches
    only direct child sub-domains (e.g. "www.sub.example.com").

この"Client-side namecheck wildcards"機能がcommitされた時ログ。 X509_CHECK_FLAG_SINGLE_LABEL_SUBDOMAINS フラグもこの時に追加されています。

正直、この"Client-side namecheck wildcards"機能は使ったことがないですし、使いどころもわかりません。

まとめ

以上でフラグの説明は終わりです。

ホスト名検証の際、結局フラグは何を指定すればいいかというと、基本はデフォルトの0でいいと思います。上で説明したように私はX509_CHECK_FLAG_NO_PARTIAL_WILDCARDSのみ指定していたりしますが。特定の接続にしか接続せず、より強固なセキュリティが求められるアプリケーションにはX509_CHECK_FLAG_NEVER_CHECK_SUBJECTやX509_CHECK_FLAG_NO_WILDCARDSを指定してもいいかもしれません。

各フラグの動作は man X509_check_host を確認すればわかりますが、関連するドキュメント類を引用して、なぜそれらが存在するのかを解説したつもりです。

おまけ

各フラグの動作を実際に試してみたい方のための簡単なツールをここに貼っておきます。小さいのでgithubにアップするのはやめておきます。

host_check.c

#include <stdio.h>
#include <openssl/err.h>
#include <openssl/pem.h>
#include <openssl/x509v3.h>

void usage()
{
	fprintf(stderr, "Usage:\n");
	fprintf(stderr, "host_check <hostname> <cert file>\n");
	exit(0);
}

struct check_flag {
	unsigned int flag;
	char        *name;
};
static struct check_flag check_flags[] = {
	{X509_CHECK_FLAG_ALWAYS_CHECK_SUBJECT,
	 "X509_CHECK_FLAG_ALWAYS_CHECK_SUBJECT"},
	{X509_CHECK_FLAG_NEVER_CHECK_SUBJECT,
	 "X509_CHECK_FLAG_NEVER_CHECK_SUBJECT"},
	{X509_CHECK_FLAG_NO_WILDCARDS,
	 "X509_CHECK_FLAG_NO_WILDCARDS"},
	{X509_CHECK_FLAG_NO_PARTIAL_WILDCARDS,
	 "X509_CHECK_FLAG_NO_PARTIAL_WILDCARDS"},
	{X509_CHECK_FLAG_MULTI_LABEL_WILDCARDS,
	 "X509_CHECK_FLAG_MULTI_LABEL_WILDCARDS"},
	{X509_CHECK_FLAG_SINGLE_LABEL_SUBDOMAINS,
	 "X509_CHECK_FLAG_SINGLE_LABEL_SUBDOMAINS"},
};

void out_check_flags(unsigned int flags)
{
	int i;
	int count = 0;

	printf("flags: ");
	if (flags == 0) {
		printf("None\n");
		return;
	}
	for (i = 0; i < sizeof(check_flags) / sizeof(struct check_flag); i++) {
		if (flags & check_flags[i].flag) {
			if (count > 0) {
				printf("       ");
			}
			printf("%s\n", check_flags[i].name);
			count++;
		}
	}
}

int check_host(X509 *x509, const char *hostname, unsigned int flags)
{
	int result;
	char *peer;

	out_check_flags(flags);

	result =X509_check_host(x509, hostname, strlen(hostname), flags, &peer);
	switch (result) {
	case 1:
		printf("Matched %s\n", peer);
		OPENSSL_free(peer);
		break;
	case 0:
		printf("Not matched\n");
		break;
	case -2:
		printf("Input is malformed\n");
		break;
	case -1:
	default:
		printf("Internal error\n");
		break;
	}
	printf("\n");

	return result;
}

int main(int argc, char *argv[])
{
	const char* hostname;
	const char* cert_file;
	FILE *fp;
	X509 *x509;
	int i;

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

	hostname = argv[1];
	cert_file = argv[2];

	fp = fopen(cert_file, "r");
	if (fp == NULL) {
		perror("fopen");
		return 1;
	}

	x509 = PEM_read_X509(fp, NULL, 0, NULL);
	if (x509 == NULL) {
		fclose(fp);
		ERR_print_errors_fp(stderr);
		return 1;
	}

	fclose(fp);

	check_host(x509, hostname, 0);

	for (i = 0; i < sizeof(check_flags) / sizeof(struct check_flag); i++) {
		check_host(x509, hostname, check_flags[i].flag);
	}

	X509_free(x509);

	return 0;

}

ビルド例(Fedora37)

gcc -Wall `pkg-config --cflags openssl`  `pkg-config --libs openssl` host_check.c  -o host_check

実行にあたってはテスト対象の証明書が必要なので、自己署名証明書を作成します。

openssl genrsa -out server.key 2048
openssl req -nodes -newkey rsa:2048 -key server.key -subj "/CN=example.com" | openssl x509 -req -days 365 -signkey server.key > test.crt

コモンネームがexample.comの証明書を作成しています(SANは省略)。

以下のようにホスト名と証明書ファイルを指定して実行すると、証明書に対してホスト名の検証を行い結果を表示します。フラグ指定なしと6種のフラグをそれぞれ設定したケースで結果を表示しています。

$ ./host_check example.com test.crt 
flags: None
Matched example.com

flags: X509_CHECK_FLAG_ALWAYS_CHECK_SUBJECT
Matched example.com

flags: X509_CHECK_FLAG_NEVER_CHECK_SUBJECT
Not matched

flags: X509_CHECK_FLAG_NO_WILDCARDS
Matched example.com

flags: X509_CHECK_FLAG_NO_PARTIAL_WILDCARDS
Matched example.com

flags: X509_CHECK_FLAG_MULTI_LABEL_WILDCARDS
Matched example.com

flags: X509_CHECK_FLAG_SINGLE_LABEL_SUBDOMAINS
Matched example.com

ご参考まで。

(*1) https://en.wikipedia.org/wiki/Subject_Alternative_Name

(*2) https://cabforum.org/wp-content/uploads/CA-Browser-Forum-EV-Guidelines-1.8.0.pdf

投稿日:2023/09/15 15:38

タグ: ネットワーク 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)