はじめに
クライアントとサーバの間の接続が相互 TLS 接続の場合、サーバからだけではなく、クライアントからも X.509 証明書 (解説記事) を通信相手に送信します。このクライアント証明書をアプリケーションレイヤーで利用したいときがあります。
例えば、Certificate-Bound アクセストークン (RFC 8705) のバリデーションをおこなう場合、サーバ側のアプリケーションレイヤーでクライアント証明書の SHA-256 ハッシュ値を計算する必要があります。
Certificate-Bound アクセストークンの概要については、Authlete 社のウェブサイトで公開されている『標準仕様による徹底的な API 保護』という文書をご参照ください。
本記事の主目的は Jakarta EE のサーバ側コードでクライアント証明書にアクセスする方法を紹介することですが、加えて、Certificate-Bound アクセストークンによる API 保護を実現する際に必要となる X.509 Certificate SHA-256 Thumbprint の算出方法も紹介します。
ServletRequest アトリビュートから
Jakarta Servlet 6.0 仕様の Section 3.11. SSL Attributes には次の記載があります。
If there is an SSL certificate associated with the request, it must be exposed by the servlet container to the servlet programmer as an array of objects of type
java.security.cert.X509Certificateand accessible via aServletRequestattribute ofjakarta.servlet.request.X509Certificate.The order of this array is defined as being in ascending order of trust. The first certificate in the chain is the one set by the client, the next is the one used to authenticate the first, and so on.
この情報を踏まえ、ServletRequest インターフェースのインスタンス (たいていの場合は ServletRequest を拡張する HttpServletRequest インターフェースのインスタンス) を request という変数名で参照できると想定すると、クライアント証明書を取り出す処理は次のように書けます (分かりやすくするためエラー処理は省いています)。
// 証明書チェーンを取り出す
X509Certificate[] certs = (X509Certificate[])
request.getAttribute("jakarta.servlet.request.X509Certificate");
// 先頭にあるのがクライアント証明書
X509Certifcate x509cert = certs[0];
ContainerRequestContext プロパティから
Jakarta RESTful Web Services 3.1 仕様の ContainerRequestContext クラスの getProperty(String) メソッドの JavaDoc には次のように書かれています。
In a Servlet container, the properties are synchronized with the
ServletRequestand expose all the attributes available in theServletRequest. Any modifications of the properties are also reflected in the set of properties of the associatedServletRequest.
この情報を踏まえ、Jakarta RESTful Web Services の ContainerRequestContext を context という変数名で参照できると想定すると、クライアント証明書を取り出す処理は次のように書けます (分かりやすくするためエラー処理は省いています)。
// 証明書チェーンを取り出す
X509Certificate[] certs = (X509Certificate[])
context.getProperty("jakarta.servlet.request.X509Certificate");
// 先頭にあるのがクライアント証明書
X509Certifcate x509cert = certs[0];
Client-Cert HTTP フィールドから
TLS で保護されたアプリケーションサーバを配備する際、クライアントとアプリケーションサーバの間にリバースプロキシを置くという構成は一般的です。リバースプロキシは、クライアントとの TLS 接続を担当し、そこで TLS 接続を終端させてから、後方に控えるアプリケーションサーバにクライアントからのリクエストを転送します。
このような構成では、アプリケーションサーバはクライアントと直接 TLS 接続をおこないません。そのため、たとえクライアントがリバースプロキシとの間に相互 TLS 接続を確立していたとしても、その接続内でクライアントが提示したクライアント証明書をアプリケーションサーバは参照できません。もしもアプリケーションサーバのロジックでクライアント証明書を参照したければ、何らかの方法でリバースプロキシからクライアント証明書を転送してもらわなければなりません。
この目的のため、「クライアント証明書を値として持つカスタム HTTP ヘッダ (例: X-Ssl-Cert) を転送するリクエストに追加する」という方法が昔からよく行われてきました。RFC 9440 は、この HTTP ヘッダの名前として Client-Cert を定義し、標準化しました。
RFC 9440 は、クライアント証明書を HTTP ヘッダの値として埋め込む際のフォーマットも標準化しました。それまでは、PEM フォーマット (RFC 7468) を使っている実装がよく見られましたが、改行の有無や BEGIN / END バウンダリーの有無などの揺れがあり、互換性は低い状態でした。RFC 9440 は、フォーマットを「DER エンコード (X.690) されたクライアント証明書をあらわすバイトシーケンス (RFC 8941 Section 3.3.5)」と定めました。
RFC 8941 のバイトシーケンスのフォーマットは「:{base64}:」です。元データを Base64 (RFC 4648) でエンコードし、両脇にコロン (:) を置きます。
ここまでの情報を踏まえ、Jakarta RESTful Web Services の ContainerRequestContext を context という変数名で参照できると想定すると、Client-Cert HTTP フィールドからクライアント証明書を取り出す処理は次のように書けます (分かりやすくするためエラー処理は省いています)。
// Client-Cert HTTP フィールドの値を取り出す
String cert = context.getHeaders().getFirst("Client-Cert");
// 両端のコロンを取り除く
cert = cert.substring(1, cert.length() - 1);
// PEM フォーマットにする
String pem = new StringBuilder()
.append("-----BEGIN CERTIFICATE-----\n")
.append(cert)
.append("\n-----END CERTIFICATE-----")
.toString();
// InputStream にする
InputStream in =
new ByteArrayInputStream(pem.getBytes(StandardCharsets.UTF_8));
// X509Certificate にする
X509Certificate x509cert =
CertificateFactory.getInstance("X.509").generateCertificate(in);
X.509 Certificate SHA-256 Thumbprint
クライアント証明書を X509Certificate インスタンスの形で用意できれば、その証明書の X.509 Certificate SHA-256 Thumbprint、すなわち「DER 表現の SHA-256 ハッシュ値の base64url 表現」は次のように書けます (分かりやすくするためエラー処理は省いています)。
// DER 表現
byte[] der = x509cert.getEncoded();
// SHA-256 ハッシュ
byte[] sha256 =
MessageDigest.getInstance("SHA-256").digest(der);
// Thumbprint (Base64URL)
String thumbprint =
Base64.getUrlEncoder().withoutPadding().encodeToString(sha256);
このようにして求めた Thumbprint を、アクセストークンに紐付いている cnf.x5t#S256 (RFC 8705, Section 3.1) と比較し、一致する時のみアクセスを許可するようにすれば、Certificate-Bound アクセストークンによる API 保護を実現できます。
おわりに
以前、クライアント証明書は TLS レイヤーでのみ参照されていたので、リバースプロキシや API ゲートウェイで処理され、それらの後方に控えるアプリケーションサーバにクライアント証明書が渡されることはありませんでした。
しかし、RFC 8705 による Certificate-Bound アクセストークンの登場で状況が変化してきました。各国のオープンバンキングエコシステムが FAPI という高セキュリティプロファイルを採用し、その FAPI で Certificate-Bound アクセストークンが必須とされたため、アプリケーションレイヤーでクライアント証明書を扱う必要が出てきたのです。
この状況変化に対し、リバースプロキシや API ゲートウェイ製品も、後方にクライアント証明書を渡すように進化していきました。『金融グレード Amazon API Gateway』という文書は、この状況変化について触れています。
これらの状況を踏まえ、本記事では Jakarta EE でクライアント証明書にアクセスする方法を紹介させていただきました。
2025 年 10 月 29 日に開催したオンライン勉強会『OAuth・OpenID 標準仕様による徹底的な API 保護』で Certificate-Bound アクセストークンについて説明していますので、ご興味があれば録画をご視聴ください。
OAuth・OpenID 標準仕様による徹底的な API 保護 | 送信者限定 / MTLS
