はじめに
RFC 8705 の Section 2 で定義されている『TLS クライアント認証』では、クライアントが提示した X.509 証明書に含まれる値が事前に登録された値と一致するかどうかを確認します。
クライアント認証の詳細については下記をご参照ください。
本記事では、その確認方法を Java コードで見ていきます。
比較対象のフィールド
クライアント認証に TLS クライアント認証 (tls_client_auth) を用いるクライアントは、下記のクライアントメタデータのいずれか一つを事前登録しておかなければなりません。
| クライアントメタデータ名 | 対応する X.509 証明書のフィールド |
|---|---|
tls_client_auth_subject_dn |
サブジェクト識別名 |
tls_client_auth_san_dns |
サブジェクト別名 / DNS |
tls_client_auth_san_uri |
サブジェクト別名 / URI |
tls_client_auth_san_ip |
サブジェクト別名 / IP アドレス |
tls_client_auth_san_email |
サブジェクト別名 / メールアドレス |
認可サーバは、登録されているクライアントメタデータの値と、クライアントが提示した X.509 証明書内の対応するフィールドの値が一致するかどうかを確認します。
X.509 証明書の構造については『図解 X.509 証明書』をご参照ください。
以下、サブジェクト識別名 (tls_client_auth_subject_dn) とサブジェクト別名 (tls_client_auth_san_*) の、それぞれの比較方法を見ていきます。
サブジェクト識別名の比較
まず、クライアント証明書からサブジェクト識別名を取り出します。
// クライアント証明書
X509Certificate certificate = ...;
// X.509 証明書内のサブジェクト識別名
String actualSubjectDN =
certificate.getSubjectX500Principal().getName();
次に、事前登録されたサブジェクト識別名をデータベースなどから取り出します。
// 事前登録されたサブジェクト識別名
String registeredSubjectDN = ...;
サブジェクト識別名は「属性=値」の組み合わせで表現されています (RFC 4514)。そのため、単純な文字列比較ではサブジェクト識別名の一致を正確に判定できません。そこで、String 型で表現されているサブジェクト識別名を X500Name に変換してから比較を行います。
// X.509 証明書内のサブジェクト識別名を X500Name で表す
X500Name actualName =
new X500Name(RFC4519Style.INSTANCE, actualSubjectDN);
// 事前登録されたサブジェクト識別名を X500Name で表す
X500Name registeredName =
new X500Name(RFC4519Style.INSTANCE, registeredSubjectDN);
// 比較する
boolean matched = actualName.equals(registeredName);
商用実装は、Java プラットフォームがデフォルトではサポートしていない属性への対応が求められる場合があります。例えば、ブラジルのオープンファイナンスで使われる X.509 証明書への対応などです。本記事では商用実装の詳細については割愛します。
サブジェクト別名の比較
X.509 証明書からサブジェクト別名を取り出します。サブジェクト識別名は X.509 証明書内に一つしかありませんが、サブジェクト別名は複数ありうることに注意してください。
// サブジェクト別名群を取得する
Collection<List<?>> sans =
certificate.getSubjectAlternativeNames();
個々のサブジェクト別名は List<?> 型で表現されています。この List の一番目の要素がサブジェクト別名のタイプ (DNS や URI など)、二番目の要素が値となっています。次のコードは、タイプと値を取り出す方法を示しています。
// 個々のサブジェクト別名毎に
for (List<?> san : sans)
{
// サブジェクト別名のタイプと値
Integer type = (Integer)san.get(0);
String value = (String) san.get(1);
switch (type)
{
// サブジェクト別名 / DNS
case GeneralName.dNSName:
...
// サブジェクト別名 / URI
case GeneralName.uniformResourceIdentifier:
...
// サブジェクト別名 / IP アドレス
case GeneralName.iPAddress:
...
// サブジェクト別名 / メールアドレス
case GeneralName.rfc822Name:
...
}
}
技術的には同じタイプのサブジェクト別名が一つの X.509 証明書に含まれている可能性があります。この点を考慮に入れ、それらのうちのいずれかが事前登録されたものと一致するかどうかを確認します。
おわりに
RFC 8705 がまだ IETF ワーキンググループドラフトだった頃、「サブジェクト識別名や個々のサブジェクト別名毎にクライアントメタデータを設けるのではなく、比較対象とするフィールドの名前とその値、の二つのクライアントメタデータだけ定義すればよいのでは?」、と提案したことがありました。しかし、仕様策定作業が終盤に差し掛かっていたため、「その方がスマートだが、この段階で仕様を変更するのは難しい」と言われました。
標準仕様策定作業は、何年もかけて多くの人々の合意を得ながら進められます。そのため、作業完了目前により良い案が見つかっても、それが合意済みの仕様を大きく変更することになる場合、その案は仕様に取り込まれません。
標準仕様だからといって設計がベストとは限りません。経験を積み、自分の視点で標準仕様の良し悪しを評価できるようになっていきましょう!(参考:id_token_hintをサポートしない理由)