【図解】mTLS(相互TLS)とは?クライアント証明書でAPIの相手を確かめるしくみと設定を初心者向けに解説

TLS の記事では、クライアントがサーバの証明書を確かめるところまでを見ました。でも API の現場では、「つないできた相手は本当に取引先のシステムか」「社内のどのサービスか」を、サーバの側でも確かめたい場面があります。この記事では、クライアントも証明書を出す mTLS(相互 TLS)を、RFC と Microsoft のドキュメントで確かめながら、テスト用の環境で OpenSSL と curl を使って実際に動かした結果と一緒に、一歩ずつ図にしました。

この記事でわかること
  1. mTLS は、TLS のハンドシェイクの中で、クライアントも証明書と秘密鍵で自分を示すしくみ
  2. API キーやトークンとの違いと、組み合わせ方(証明書に結びついたトークン)
  3. サーバが確かめること(つながり・期限・用途・持ち主)と、「誰か」を決める仕事
  4. OpenSSL と curl で mTLS を動かし、失敗したときのエラーを読む方法
  5. Azure App Service と API Management で mTLS を使うときの注意点

まずはたとえで:入館証

取引先のビルを訪ねると、受付の窓口は会社の名札を下げています。ふつうの TLS は、この「窓口の身分証」だけを見せるしくみです。クライアント(来訪者)は窓口が本物かどうかを確かめますが、来訪者が誰かは TLS では確かめず、そのあとの ID やパスワード、トークンで確かめます。

TLS:窓口だけが身分証を見せる来訪者窓口窓口の身分証(サーバ証明書)来訪者が誰かは、TLS では確かめない(あとで ID やトークンで)mTLS:来訪者も入館証を見せる来訪者窓口窓口の身分証(サーバ証明書)来訪者の入館証(クライアント証明書)入館証は会社(CA)が出したもの。窓口は、自分が信じる会社の入館証だけ通すmTLS は、TLS のハンドシェイクの中で、クライアントも証明書で確かめる
図1 たとえで見る mTLS

セキュリティの厳しいビルでは、来訪者も入館証を見せます。入館証は会社が出したもので、窓口は「自分が信じる会社の入館証か」を見て通します。mTLS(mutual TLS、相互 TLS)はこれと同じで、サーバ証明書に加えて、クライアントもクライアント証明書を出します。入館証を出したのが CA(認証局)、入館証がクライアント証明書です。証明書と CA のしくみは、PKI の記事で扱いました。

使う場面

mTLS は、相手が「人」ではなく「プログラムや機器」で、あらかじめ証明書を配っておける場面で使われます。

サービスどうしの API社内のマイクロサービスが互いを確かめる取引先との B2B 連携相手のシステムを証明書で限定するIoT・機器からの接続機器ごとの証明書で接続を許すOAuth のクライアント認証シークレットの代わりに証明書(RFC 8705)トークンの盗用対策トークンを証明書に結びつける(RFC 8705)ゼロトラストの考え方社内でも、つなぐ相手を毎回確かめる共通点:人ではなく「プログラムや機器」が相手で、あらかじめ証明書を配っておけるブラウザを使う一般の利用者には、証明書を配って管理するのが難しいので、あまり使わないmTLS は「決まった相手のプログラムや機器」とつなぐ API で力を発揮する
図2 mTLS を使う場面

OAuth 2.0 でも、クライアントの認証に mTLS を使う方法が RFC 8705 で決まっています。クライアントシークレットの代わりに証明書でトークンを取り、さらにそのトークンを証明書に結びつけて、盗まれても使えないようにできます。後半の「証明書に結びついたトークン」で図にします。

mTLS の基本の形

API キー・トークンとの違い

最初に、よく比べられる3つの形を並べます。API キーや OAuth のトークンは、HTTP のヘッダに入れて送る値です。値そのものを持っていれば誰でも使えるので、漏れると困ります。mTLS では、サーバに見せるのは証明書(公開鍵)ですが、証明書だけでは通りません。クライアントの秘密鍵で署名できることを、ハンドシェイクの中で示す必要があります。

API キー・トークンだけヘッダの値を持っていれば通る盗まれると、だれでも使える確かめるのは API(アプリ)mTLS秘密鍵で署名できる相手だけ通る証明書だけ盗んでも使えない確かめるのは TLS の段階mTLS + トークン相手(証明書)と権限(トークン)トークンを証明書に結びつけられる両方を確かめるmTLS は「持っているもの(秘密鍵)」で相手を確かめる。トークンと組み合わせると強い
図3 API キー・トークン、mTLS、両方を組み合わせた形
比べることAPI キー・トークンだけmTLSmTLS + トークン
何で確かめるかヘッダの値証明書と、秘密鍵での署名両方
どこで確かめるかAPI(アプリ)TLS の段階(サーバや前段)TLS の段階と API
漏れたら値を持つ人なら使える証明書だけでは使えない。秘密鍵が漏れたら失効させるトークンだけでは使えない
権限(何をしてよいか)トークンのスコープなど証明書からは分からない。サーバが対応表で決めるトークンのスコープなど
向いている相手人・アプリ全般決まったプログラムや機器決まったプログラム(厳しい API)

mTLS が確かめるのは「相手が誰か」(認証)までです。その相手に何を許すか(認可)は、サーバやアプリが別に決めます。認証と認可の違いは、ここでも同じです。

サーバが確かめること

入館証のたとえで続けます。窓口は、来訪者が差し出した入館証を、上から順に5つ確かめます。1〜4 は「本物の入館証か」、5 は「今日ここに通してよい人か」の確認です。mTLS のサーバも、クライアント証明書を同じ順で確かめます。

確かめることたとえ(入館証)実際に見るもの1つながり信じる会社のハンコが押してあるか信じる CA まで署名をたどれる2期限・失効期限内で、紛失届が出ていないか有効期間の中・失効していない3用途来訪者用の入館証か用途(EKU)に clientAuth がある4持ち主その場でサインして本人と示せるかCertificateVerify の署名が合う1〜4 は TLS のハンドシェイクで確かめる(サーバや前段の設定)5 は「誰か」を決める確認(アプリや API ゲートウェイの設定)5誰か来訪者の名簿に載っているかSAN や指紋が登録した値と合う1〜4 で「本物の入館証」と分かり、5 で「通してよい相手」と分かる
図4 サーバがクライアント証明書で確かめる5つのこと
確認たとえ(入館証)実際に見るものだめなとき
(この記事の試験)
1 つながり信じる会社のハンコが押してあるかサーバが信じる CA まで、証明書の署名をたどれるかunknown ca(サーバ側は error 20)
2 期限・失効期限内で、紛失届が出ていないか有効期間の中か。CRL などで失効していないかPKI の記事の error 10・23 と同じ
3 用途来訪者用の入館証か用途(EKU)に clientAuth があるかunsupported certificate(サーバ側は error 26)
4 持ち主その場でサインして、本人と示せるかCertificateVerify の署名を、証明書の公開鍵で確かめて合うか署名が合わなければ断る
5 誰か来訪者の名簿に載っているかSAN・サブジェクト・指紋などが、登録した値と合うか断り方は作りによる(接続を切る、403 を返すなど)

1〜3 は、TLS の記事でクライアントがサーバ証明書に対して行ったのと同じ確認で、向きが逆になっただけです。2 の失効の確認は、製品や設定によっては行いません。たとえば OpenSSL では -crl_check などを付けたときだけ CRL を見ます。API Management の validate-client-certificate ポリシーは、既定で失効も確かめます。

持ち主の確認(CertificateVerify)が要る理由

証明書は、名前と公開鍵に CA がハンコを押したもので、相手に見せるためのものです。ログに残ったり、別の担当者に渡ったりすることもあるので、「証明書を持っている」だけでは本人とは言えません。そこでクライアントは、ハンドシェイクのその場のやり取りに、自分の秘密鍵で署名して送ります。これが CertificateVerify です。

本物の app01client.crtclient.key証明書を見せるその場のやり取りに秘密鍵で署名サーバ証明書の公開鍵で署名を確かめる合う → 本人証明書だけを手に入れた人client.crt秘密鍵がない証明書は見せられる合う署名を作れないサーバ公開鍵で確かめると合わない断る証明書は「見せるもの」。本人だと示すのは、秘密鍵での署名やり取りの中身は毎回違うので、前の署名を使い回すこともできない
図5 持ち主の確認:証明書だけでは本人と示せない

サーバは、証明書に入っている公開鍵でこの署名を確かめます。公開鍵と合う署名を作れるのは、ペアの秘密鍵を持つ app01 だけです。署名するやり取りには毎回変わる乱数が入っているので、前に見た署名をコピーして使い回すこともできません。RFC 8446 も、CertificateVerify を「証明書に対応する秘密鍵を持っていることの明示的な証明」と説明しています。

「本物の証明書」と「通してよい相手」は別

1〜4 が通っても、分かるのは「信じる CA が出した、本物の証明書を、本人が使っている」ことまでです。社内の CA は、ほかのシステムや社員のパソコンにも証明書を出しているかもしれません。それらもすべて 1〜4 を通ります。

社内の CAいろいろ発行発行された証明書確認 1〜4確認 5(この API の名簿)app01SAN:app01.example.test本物載っている → 通すapp02SAN:app02.example.test本物載っていない → 断る社員のパソコンSAN:pc-1234.example.test本物載っていない → 断る同じ CA の証明書は、どれも 1〜4 を通る。通す相手を決めるのは名簿
図6 同じ CA の証明書は、どれも 1〜4 を通る

そこで 5 として、「この API に通してよい相手の名簿」を持ち、SAN や指紋などを照らし合わせます。名簿を持つのは、サーバの設定、アプリのコード、前段の API ゲートウェイなどです。STEP 2 で使う openssl s_server は名簿を持たないので、Demo Root CA が出した clientAuth の証明書なら誰でも通します。本番では、この 5 を必ずどこかで行います。

誰が何を持つか

持ち物は、「自分を示すもの」と「相手を確かめるもの」の2種類に分けると整理しやすくなります。ふつうの TLS では、自分を示すのはサーバだけでした。mTLS では、クライアントも自分を示すものを持ち、サーバも相手を確かめるものを持ちます。図の網掛けが、mTLS で増える持ち物です。

クライアント(app01)CAサーバ(API)自分を示すもの相手を確かめるもの自分を示すもの相手を確かめるものクライアントの秘密鍵証明書の公開鍵とペア・外に出さないクライアント証明書(入館証)公開鍵が入っている・見せてよい信じる CA 証明書サーバ証明書を確かめる用サーバの秘密鍵証明書の公開鍵とペア・外に出さないサーバ証明書(窓口の名札)公開鍵が入っている・見せてよい信じる CA 証明書クライアント証明書を確かめる用名簿(対応表)証明書 → 誰か・何を許すかCA の秘密鍵証明書に署名CA 証明書両側に配る網掛け:mTLS で増えるもの(ふつうの TLS なら要らない)両側とも「自分の鍵と証明書」と「相手を確かめる CA 証明書」を持つ。サーバは名簿も持つ
図7 誰が何を持つか
持ち物持つ人何に使うか見せてよいかたとえ
クライアントの秘密鍵クライアントCertificateVerify の署名を作る(確認 4)見せない本人しか書けないサイン
クライアント証明書クライアントサーバに見せて、名前と公開鍵を伝える見せてよい入館証
サーバの秘密鍵サーバサーバの CertificateVerify の署名を作る見せない窓口のサイン
サーバ証明書サーバクライアントに見せる(TLS と同じ)見せてよい窓口の名札
信じる CA 証明書両方相手の証明書の署名をたどる起点(確認 1)見せてよい「この会社のハンコなら信じる」という見本
名簿(対応表)サーバ証明書から誰か・何を許すかを決める(確認 5)社内の設定来訪者名簿
CA の秘密鍵CA証明書に署名する見せない会社のハンコ

押さえておきたいのは次の3つです。

  • 秘密鍵と証明書はペアです。証明書の中の公開鍵と対になる秘密鍵があって、初めて「自分」を示せます。証明書は見せてよく、秘密鍵は持ち主の外に出しません。
  • 「信じる CA 証明書」は向きが逆です。クライアントが持つのはサーバ証明書を確かめるため、サーバが持つのはクライアント証明書を確かめるためのものです。この記事の試験では1つの CA で両方を出していますが、本番ではサーバ用とクライアント用で CA が違うことも多く、その場合は持つ CA 証明書も別になります。
  • クライアントの秘密鍵が漏れたら、証明書を失効させます。秘密鍵があれば、ほかの誰かが app01 として確認 1〜4 を通れてしまいます。失効させて、新しい鍵で証明書を出し直します(失効のしくみは PKI の記事で扱いました)。

全体の流れ

準備:クライアント証明書を発行して、サーバに信じる CA を設定する

サーバ証明書は、TLS の記事と PKI の記事で用意した形と同じです。ここでは、クライアント(app01)の証明書を発行し、サーバに設定するところまでを見ます。

クライアント(app01)CAサーバ(API)① 鍵ペアと CSRクライアントで操作② CSR を CA へ秘密鍵は送らない③ 確かめて署名clientAuth・SAN を付ける④ クライアント証明書⑤ 証明書と鍵を置く秘密鍵は自分だけ⑥ CA 証明書をサーバへクライアント証明書の確認用⑦ サーバで操作証明書を必須にする⑧ サーバで操作証明書 → 誰・権限クライアントは証明書と鍵を、サーバは「信じる CA」と「誰か」の対応を持つ
図8 準備の流れ(①〜⑧)
手順誰が・どこで何をするか鍵(物)はどうなるか・使う鍵
①クライアントで操作鍵ペアと CSR(証明書の申請書)を作るクライアントの秘密鍵は、クライアントの中だけ
②クライアント → CACSR を渡すCSR に入るのは名前と公開鍵。秘密鍵は送らない
③CA申請を確かめて署名するCA の秘密鍵で署名。用途に clientAuth、名前に SAN を付ける
④CA → クライアントクライアント証明書を渡す証明書は公開してよいもの
⑤クライアントで操作証明書と秘密鍵を、接続に使う場所に置く秘密鍵は自分だけが読めるようにする
⑥CA → サーバCA 証明書を渡すサーバが「この CA のクライアント証明書を信じる」ための起点
⑦サーバで操作クライアント証明書を必須にする証明書を出さない相手は、ハンドシェイクで断る
⑧サーバで操作証明書と、誰か・権限の対応を決めるSAN・サブジェクト・指紋などを、app01 に結びつける

本番では、クライアント証明書は社内の CA(AD CS など)や、クラウドのプライベート CA で出すのが一般的です。⑧の対応は、サーバやアプリ、前段の API ゲートウェイの設定で持ちます。

公的なサーバ証明書は clientAuth を含まなくなる

Microsoft のドキュメントによると、Chrome のルートプログラムの方針の更新で、2026年6月15日以降に発行される公的なサーバ証明書には、クライアント認証の用途(clientAuth)を入れられなくなりました。Azure App Service の証明書(App Service マネージド証明書と App Service 証明書)も clientAuth を外していて、これらの証明書を mTLS のクライアント証明書として使っている場合は、別の方法に移す必要があります。それより前に発行された clientAuth 付きの証明書は、期限が切れるか失効するまで有効です。

接続:mTLS のハンドシェイク

準備ができたら、クライアントは API につなぎます。TLS 1.3 のハンドシェイクに、サーバが証明書を求めるメッセージと、クライアントが証明書と署名を返すメッセージが加わります。

クライアント(app01)サーバ(API)平文(⑨⑩)暗号化されたハンドシェイク(⑪〜⑲)アプリのデータ(⑳〜㉒)⑨ ClientHellokey_share・SNI など(TLS と同じ)⑩ ServerHellokey_share⑪ EncryptedExtensions⑫ CertificateRequest証明書を出してください(受け付ける CA など)⑬ Certificate・CertificateVerify・Finishedサーバ証明書と、サーバの秘密鍵での署名⑭ サーバを確かめるTLS の記事と同じ⑮ Certificateクライアント証明書(app01)⑯ CertificateVerifyここまでのやり取りに、クライアントの秘密鍵で署名⑰ Finished⑱ クライアントを確かめる信じる CA まで・期限・clientAuth⑯の署名を証明書の公開鍵で⑲ 誰かを決めるSAN や指紋 → app01⑳ HTTP リクエストトークンを付けることも多い㉑ app01 の権限で処理認可はアプリの仕事㉒ HTTP レスポンスサーバが⑫で求め、クライアントが⑮⑯で証明書と署名を返す。それ以外は TLS と同じ
図9 mTLS のハンドシェイク(TLS 1.3、⑨〜㉒)
手順誰が・どこで何をするか鍵(物)はどうなるか・使う鍵
⑨クライアント → サーバClientHello を送る鍵交換の材料(key_share)。TLS と同じ
⑩サーバ → クライアントServerHello を返すここで共通の鍵ができ、この先は暗号化される
⑪サーバ → クライアントEncryptedExtensions を送るTLS と同じ
⑫サーバ → クライアントCertificateRequest で証明書を求める受け付ける署名の方式や、受け付ける CA の一覧を付けられる
⑬サーバ → クライアントサーバの証明書・署名・Finished を送るサーバの秘密鍵で署名(TLS と同じ)
⑭クライアントサーバを確かめるクライアントが信じる CA でたどる(TLS と同じ)
⑮クライアント → サーバCertificate でクライアント証明書を送る暗号化された中で送られる
⑯クライアント → サーバCertificateVerify を送るここまでのやり取りに、クライアントの秘密鍵で署名する
⑰クライアント → サーバFinished を送るやり取りが改ざんされていないことの確認
⑱サーバクライアントを確かめるサーバが信じる CA でたどり、⑯の署名を証明書の公開鍵で確かめる
⑲サーバ(アプリや設定)誰かを決めるSAN や指紋から app01 と決める
⑳クライアント → サーバHTTP リクエストを送るトークンを一緒に送ることも多い
㉑サーバ(アプリ)app01 の権限で処理する認可はアプリが決める
㉒サーバ → クライアントHTTP レスポンスを返す同じ暗号化された通信で返す

RFC 8446(TLS 1.3)では、サーバが CertificateRequest を送ったときだけ、クライアントは Certificate を送ります。証明書を送ったときは、続けて CertificateVerify を送ります。⑫に付ける「受け付ける CA の一覧」(certificate_authorities)は、クライアントが複数の証明書を持っているときに、どれを出すかを選ぶための手がかりです。

TLS 1.3 では、クライアント証明書も暗号化された中で送られるので、通信を見ている人には app01 の証明書は見えません。

証明書に結びついたトークン(RFC 8705)

mTLS とトークンを組み合わせると、トークンを盗まれたときの被害を小さくできます。RFC 8705 は、認可サーバが、トークンを取りに来たクライアントの証明書の「指紋」をトークンに入れる方法を決めています。API は、TLS でつないできた証明書の指紋と、トークンに入った指紋を比べます。

クライアント(app01)認可サーバAPI(リソースサーバ)トークンだけが盗まれたら(❼〜❾)❶ mTLS でトークンを要求クライアント証明書で認証❷ 証明書の指紋を入れるcnf の x5t#S256❸ アクセストークン証明書に結びついている❹ mTLS + トークン同じ証明書で接続する❺ 指紋を比べるTLS の証明書の指紋= トークンの cnf❻ 200 OK❼ 盗んだ人がトークンを送るapp01 の秘密鍵がないので、別の証明書か証明書なし❽ 指紋が合わないTLS の証明書の指紋≠ トークンの cnf❾ 401 invalid_tokenトークンだけでは使えないトークンに「この証明書の持ち主だけが使える」という印を入れる(RFC 8705)
図10 証明書に結びついたアクセストークン(❶〜❾)
項目最初の図(図9)この図(証明書に結びついたトークン)
登場するものクライアントとサーバクライアント・認可サーバ・API の3つ
トークン使っても使わなくてもよい(⑳)認可サーバが、証明書の指紋(cnf の x5t#S256)を入れて出す(❷❸)
API が確かめることクライアント証明書(⑱)クライアント証明書と、トークンの指紋が一致すること(❺)
トークンが漏れたらトークンだけの API なら使えてしまう指紋が合わないので 401 invalid_token(❽❾)

指紋は、証明書を DER(バイナリの形)にして SHA-256 でハッシュし、base64url で文字列にしたものです。JWT のアクセストークンなら、cnf(confirmation)というクレームの中の x5t#S256 に入ります。RFC 8705 では、API(リソースサーバ)は TLS で受け取った証明書とこの値が合うかを確かめ、合わなければ 401 と invalid_token のエラーで断ります。

RFC 8705 には、クライアントの認証のしかたも2つ決まっています。CA が出した証明書を使い、サブジェクトや SAN の値を登録しておく tls_client_auth と、自己署名の証明書をそのまま登録しておく self_signed_tls_client_auth です。tls_client_auth では、照合に使う値(サブジェクトの DN、SAN の DNS 名など)を、クライアントごとに1つだけ登録します。

試してみる(OpenSSL と curl)

ここからは、テスト用の環境で OpenSSL 3.0.13 と curl を使い、mTLS を動かした結果を見ていきます。名前は試験用のドメイン(example.test)にし、CA は1つで、サーバ証明書とクライアント証明書の両方を出しています。

CA とサーバ・クライアントの証明書を作る(図8の①〜⑤)

まず、テスト用の CA(Demo Root CA)を作り、その CA でサーバ証明書とクライアント証明書を発行します。違いは用途(extendedKeyUsage)と名前(subjectAltName)だけです。

CA の側ca.keyca.crt両方の証明書に署名ca.crt は両側に配るサーバ(API)server.keyserver.crtca.crtserverAuth・api.example.testクライアント(app01)client.keyclient.crtca.crtclientAuth・app01.example.testサーバの ca.crt はクライアント証明書を確かめるため、クライアントの ca.crt はサーバ証明書を確かめるため本番では、サーバ用とクライアント用で CA を分けることも多い(この試験では1つの CA で両方を出す)どちらも「自分の鍵と証明書」と「相手を確かめる CA 証明書」を持つ
図11 STEP 1 で作るファイルと置き場所

CA を作る(CA で操作)

openssl req -x509 -newkey ec -pkeyopt ec_paramgen_curve:P-256 -nodes -keyout ca.key -out ca.crt -days 30 -subj "/CN=Demo Root CA"

サーバ証明書を発行する(サーバ用:serverAuth)

printf "subjectAltName=DNS:api.example.test,DNS:localhost\nextendedKeyUsage=serverAuth\n" > server.ext
openssl req -newkey ec -pkeyopt ec_paramgen_curve:P-256 -nodes -keyout server.key -out server.csr -subj "/CN=api.example.test"
openssl x509 -req -in server.csr -CA ca.crt -CAkey ca.key -CAcreateserial -out server.crt -days 7 -extfile server.ext

クライアント証明書を発行する(クライアントで①、CA で③、図8の①〜④)

printf "subjectAltName=DNS:app01.example.test\nextendedKeyUsage=clientAuth\n" > client.ext
openssl req -newkey ec -pkeyopt ec_paramgen_curve:P-256 -nodes -keyout client.key -out client.csr -subj "/CN=app01"
openssl x509 -req -in client.csr -CA ca.crt -CAkey ca.key -CAcreateserial -out client.crt -days 7 -extfile client.ext
Certificate request self-signature ok
subject=CN = app01

実際の手順では、1行目と2行目(鍵ペアと CSR の作成)をクライアントで行い、CSR だけを CA に渡して、3行目を CA で行います。ここでは1台で続けて実行しています。

クライアント証明書の中身を見る

openssl x509 -in client.crt -noout -subject -issuer -ext subjectAltName,extendedKeyUsage
subject=CN = app01
issuer=CN = Demo Root CA
X509v3 Subject Alternative Name: 
    DNS:app01.example.test
X509v3 Extended Key Usage: 
    TLS Web Client Authentication

発行者は Demo Root CA、名前は app01.example.test、用途は「TLS Web Client Authentication」(clientAuth)です。サーバ証明書のほうは、用途が「TLS Web Server Authentication」になります。

クライアント証明書を求めるサーバを起動する(図8の⑥〜⑦)

テスト用のサーバとして openssl s_server を使います。クライアント証明書について決めるのは3つのオプションです。

openssl s_server … -CAfile ca.crt -Verify 1 -verify_return_error -www-cert / -keyサーバ証明書と秘密鍵(TLS と同じ)⑬-CAfile ca.crtクライアント証明書を確かめる CA⑱-Verify 1証明書を必須にする(1 はたどる段数)⑫-verify_return_error確認に失敗したら接続を切る⑱-www簡単なページを返す㉒-CAfile が「どの CA のクライアント証明書を信じるか」、-Verify が「必須か」を決める
図12 STEP 2 の s_server のオプション(右の番号は図9の手順)

クライアント証明書を必須にして起動する(サーバで操作、図8の⑥⑦)

openssl s_server -accept 8443 -cert server.crt -key server.key -CAfile ca.crt -Verify 1 -verify_return_error -www

-CAfile に、クライアント証明書を確かめる CA を指定します。-Verify は証明書を必須にするオプションで、数字はたどる証明書の段数の上限です(小文字の -verify にすると、求めはするが必須にはしません)。-verify_return_error を付けないと、確認に失敗してもメッセージを出すだけで接続を続けるので、試すときは付けておきます。なお、この s_server は「誰か」を決める⑧の対応表を持たないので、Demo Root CA が出した clientAuth の証明書なら通します。

s_client でつなぐ(図9の⑨〜⑲)

クライアントは、-cert と -key でクライアント証明書と秘密鍵を指定します。

クライアント側(s_client)-CAfile ca.crtサーバ証明書を確かめる CA⑭-cert client.crt⑮で送るクライアント証明書⑮-key client.key⑯の署名に使う秘密鍵⑯サーバ側(s_server の表示)depth=1 CN = Demo Root CA信じる CA まで、たどれたdepth=0 CN = app01クライアントは app01verify return:1確認が通ったクライアントは「証明書と鍵」を出し、サーバは「信じる CA」でたどって app01 と分かる
図13 STEP 3 の s_client のオプションと、サーバ側の表示

クライアント証明書を付けてつなぐ(クライアントで操作)

echo Q | openssl s_client -connect localhost:8443 -servername api.example.test -CAfile ca.crt -cert client.crt -key client.key -brief
CONNECTION ESTABLISHED
Protocol version: TLSv1.3
Ciphersuite: TLS_AES_256_GCM_SHA384
Requested Signature Algorithms: ECDSA+SHA256:ECDSA+SHA384:ECDSA+SHA512:Ed25519:Ed448:RSA-PSS+SHA256:RSA-PSS+SHA384:RSA-PSS+SHA512:RSA-PSS+SHA256:RSA-PSS+SHA384:RSA-PSS+SHA512:RSA+SHA256:RSA+SHA384:RSA+SHA512:ECDSA+SHA224:RSA+SHA224
Peer certificate: CN = api.example.test
Hash used: SHA256
Signature type: ECDSA
Verification: OK
Server Temp Key: X25519, 253 bits
DONE

クライアント側の Verification: OK は、⑭でサーバ証明書の確認が通ったという意味です。クライアント証明書の確認の結果は、サーバ側に出ます。

サーバ側の表示(図9の⑱)

verify depth is 1, must return a certificate
Using default temp DH parameters
ACCEPT
depth=1 CN = Demo Root CA
verify return:1
depth=0 CN = app01
verify return:1

1行目の must return a certificate は、-Verify で証明書を必須にしたことを示しています。depth=1 が CA、depth=0 がクライアント証明書で、どちらも verify return:1(確認が通った)です。

ハンドシェイクのメッセージを見る(図9の⑨〜⑰)

-trace を付けると、やり取りしたメッセージが表示されます。メッセージの名前と長さの行だけを取り出しました。

-trace で見たメッセージ→ClientHello←ServerHello←EncryptedExtensions←CertificateRequest←Certificate(サーバ)←CertificateVerify(サーバ)←Finished→Certificate(クライアント)→CertificateVerify(クライアント)→Finished⑫ サーバが証明書を求める⑮ クライアント証明書を送る⑯ 自分の秘密鍵で署名鍵のマーク:暗号化されたレコードTLS との違いは3つだけ。クライアント証明書も暗号化された中で送られる
図14 STEP 4 の -trace で見たメッセージ

メッセージの名前を見る(クライアントで操作)

echo Q | openssl s_client -connect localhost:8443 -servername api.example.test -CAfile ca.crt -cert client.crt -key client.key -trace 2>&1 | grep -E "^(Sent|Received) Record|^    [A-Za-z]+, Length"
Sent Record
    ClientHello, Length=309
Received Record
    ServerHello, Length=118
Received Record
Received Record
    EncryptedExtensions, Length=2
Received Record
    CertificateRequest, Length=74
Received Record
    Certificate, Length=844
Received Record
    CertificateVerify, Length=74
Received Record
    Finished, Length=48
Sent Record
Sent Record
    Certificate, Length=823
Sent Record
    CertificateVerify, Length=76
Sent Record
    Finished, Length=48
Sent Record

サーバから CertificateRequest が届き(⑫)、サーバの Certificate・CertificateVerify・Finished の後で、クライアントが自分の Certificate(⑮)・CertificateVerify(⑯)・Finished(⑰)を送っています。ServerHello の後と、クライアントが Certificate を送る前に、中身の表示がない Received Record と Sent Record があります。grep で絞らずに見ると、どちらも TLS の記事でも見た互換のためのダミーのメッセージ(ChangeCipherSpec)で、最後の Sent Record は接続を終えるときのアラートでした。

curl でクライアント証明書を付けて API を呼ぶ(図9の⑳〜㉒)

curl では --cert と --key を使います。s_server の -www は、接続の情報をページにして返すので、サーバが受け取ったクライアント証明書の欄だけを取り出しました。

curl–cert・–keys_server(-www)mTLS で接続クライアント証明書 app01 を出すサーバが確かめるDemo Root CA まで・clientAuthGET /暗号化された中ページClient certificate の欄ページの中身:Issuer: CN=Demo Root CASubject: CN=app01・TLS Web Client Authenticationサーバのアプリには「どの証明書でつないできたか」が渡る。これで相手を決める
図15 STEP 5 の curl の流れ

クライアント証明書を付けて呼ぶ(クライアントで操作)

curl -sS --cacert ca.crt --cert client.crt --key client.key https://localhost:8443/ | grep -E "^New, |^Client certificate|Issuer: |Subject: |TLS Web Client"
New, TLSv1.3, Cipher is TLS_AES_256_GCM_SHA384
Client certificate
        Issuer: CN=Demo Root CA
        Subject: CN=app01
                TLS Web Client Authentication

サーバ側には、つないできた証明書の発行者・サブジェクト・用途が渡っています。本物の API では、アプリやゲートウェイが、この情報から app01 だと決め(⑲)、app01 に許した処理だけを行います(㉑)。

証明書の指紋(x5t#S256)を計算する(図10の❷❺)

証明書に結びついたトークンで使う x5t#S256 の値を、client.crt から計算してみます。

client.crtPEM(テキスト)DERバイト列にSHA-25632バイトのハッシュbase64url= を取る0GAQkN7mTAx7bRqY1jaMgJr-DDoPhJ_2B3aHBwBXLMY← x5t#S256 の値トークンの cnf に入る形(RFC 8705 の例)“cnf”: { “x5t#S256”: “…この値…” }証明書の「指紋」を、決まった手順で短い文字列にしたもの。API はこれと比べる
図16 STEP 6 で x5t#S256 を計算する手順

x5t#S256 の値を計算する

openssl x509 -in client.crt -outform DER | openssl dgst -sha256 -binary | base64 | tr '+/' '-_' | tr -d '='
0GAQkN7mTAx7bRqY1jaMgJr-DDoPhJ_2B3aHBwBXLMY

比べるために、SHA-256 の指紋を16進数で見る

openssl x509 -in client.crt -noout -fingerprint -sha256
sha256 Fingerprint=D0:60:10:90:DE:E6:4C:0C:7B:6D:1A:98:D6:36:8C:80:9A:FE:0C:3A:0F:84:9F:F6:07:76:87:07:00:57:2C:C6

2つは同じハッシュ値を、base64url と16進数で書いたものです(先頭の D0 60 10 90 が base64url の 0GAQkN にあたります)。RFC 8705 の例では、アクセストークンの中に次の形で入ります。

RFC 8705 の cnf の例(RFC の本文より)

  {
    "iss": "https://server.example.com",
    "sub": "ty.webb@example.com",
    "exp": 1493726400,
    "nbf": 1493722800,
    "cnf":{
      "x5t#S256": "bwcK0esc3ACC3DB2Y5_lESsXE8o9ltc05O89jdN-dg2"
    }
  }

ここからは Microsoft の場合です

Azure App Service

Azure App Service では、アプリの設定でクライアント証明書を有効にできます(Basic 以上のプラン)。大事なのは、TLS を終えるのがアプリではなく、App Service の前段(フロントエンドのロードバランサ)だという点です。

クライアント(app01)App Service(フロントエンド)アプリのコード❶ mTLS で接続クライアント証明書を出す❷ TLS をここで終える証明書は確かめずに渡す❸ リクエストを転送X-ARR-ClientCert ヘッダに証明書❹ 証明書を確かめる期限・発行者指紋・名前❺ 通すか決めるだめなら 403❻ レスポンスApp Service は証明書を「渡すだけ」。確かめるのはアプリのコード
図17 Azure App Service での mTLS の流れ(❶〜❻)
項目最初の図(図9)App Service(この図)
TLS を終えるところサーバ(API)そのものApp Service のフロントエンド(❷)
証明書の確認TLS の段階でサーバが確かめる(⑱)App Service は確かめずに渡し、アプリのコードが確かめる(❹)
証明書の渡し方TLS の中X-ARR-ClientCert ヘッダ(Base64)で転送する(❸)
だめなときハンドシェイクで断るアプリが 403 などを返す(❺)

Microsoft のドキュメントは、App Service はクライアント証明書をアプリに転送する以外のことはしないので、アプリのコードで確かめる必要がある、と書いています。ドキュメントのサンプルでも、指紋・サブジェクト・発行者・有効期間を確かめ、だめなら 403 を返しています。ASP.NET Core には、転送された証明書を読み取るミドルウェアがあります。また、HTTP(暗号化なし)でアクセスされるとクライアント証明書は届かないので、証明書が必要なアプリでは HTTP を許さないようにします。

クライアント証明書を有効にする(Azure CLI)

az webapp update --set clientCertEnabled=true --name <app-name> --resource-group <group-name>

Bicep で設定する場合(抜き出し)

    clientCertEnabled: true
    clientCertMode: 'Required'
    clientCertExclusionPaths: '/sample1;/sample2'
クライアント証明書のモード動き
Requiredすべてのリクエストでクライアント証明書が必要
Optional証明書は使っても使わなくてもよい。既定でクライアントに求める(ブラウザなら選ぶ画面が出る)
Optional Interactive User証明書は使っても使わなくてもよい。既定では求めない
除外パスと再ネゴシエーション

「Optional Interactive User」のモードか、証明書を求めないパス(除外パス、clientCertExclusionPaths)を使うと、App Service は TLS の再ネゴシエーション(つないだ後でもう一度ハンドシェイクする)で証明書を求めます。TLS 1.3 と HTTP/2 は再ネゴシエーションに対応していないので、この設定では使えません。また、100 KB を超える POST・PUT は 403 で失敗します。ハンドシェイクの中で証明書を求めたいときは、モードを Required か Optional にし、除外パスを外します。

Azure API Management

API Management(APIM)では、ゲートウェイがクライアント証明書を受け取り、validate-client-certificate ポリシーで確かめられます。信頼できる CA までたどれるか、期限、失効(既定はすべて確かめる)に加えて、指紋・サブジェクト・SAN の DNS 名・発行者などを「identity」として最大10個まで書いておき、どれかに合う証明書だけを通せます。指紋(thumbprint)は SHA-1 の値です。

  • クラシックの Developer・Basic・Standard・Premium では、カスタムドメインの「Negotiate client certificate」を、Consumption と v2 のプランでは「Request client certificate」を有効にします。
  • Application Gateway の後ろに APIM を置くと、Application Gateway が TLS を終えるので、クライアントの証明書は APIM に届きません。サーバ変数で証明書の情報を渡す方法が案内されています。

Microsoft Entra ID の「証明書」は mTLS ではない

Entra ID のアプリ登録で使う「証明書の資格情報」も、証明書の秘密鍵で署名する点は同じですが、mTLS とは別のしくみです。アプリは証明書の秘密鍵で JWT(クライアントアサーション)に署名し、client_assertion としてトークンのエンドポイントに送ります。TLS の段階ではクライアント証明書を出しません。

mTLS(この記事)TLS のハンドシェイクで証明書を出すCertificateVerify に秘密鍵で署名サーバ(API や認可サーバ)がTLS の段階で確かめる例:RFC 8705 の tls_client_authEntra ID の証明書の資格情報TLS はふつう(サーバ証明書だけ)証明書の秘密鍵で JWT に署名するclient_assertion として送る(private_key_jwt)ヘッダに x5t#S256 で証明書を示すどちらも「証明書の秘密鍵で署名する」が、署名を載せる場所が違う
図18 mTLS と Entra ID の証明書の資格情報の違い

JWT のヘッダには、どの証明書かを示すために x5t#S256(証明書の SHA-256 の指紋)を入れます。STEP 6 で計算したものと同じ形の値です。このしくみはクライアントクレデンシャルの記事の続きとして見ると分かりやすいです。

よくある間違い

間違いどうするか
信じる CA の証明書なら、誰でも通しているSAN や指紋などを登録した値と比べて、通す相手を決める
証明書を確かめるのを、前段の仕事だと思い込むApp Service のように、前段は渡すだけの場合がある。どこで確かめるかを確認する
サーバ証明書をクライアント証明書の代わりに使うclientAuth の用途を持つ証明書を発行する。公的なサーバ証明書は clientAuth を含まなくなっている
mTLS が通ったので、何でも許すmTLS は認証まで。権限は対応表やトークンのスコープで決める
クライアントの秘密鍵を、設定ファイルと一緒に配る秘密鍵は Key Vault や証明書ストアなど、守られた場所に置き、期限と失効を管理する

うまくいかないとき

mTLS の失敗は、クライアントには短いアラートしか届かず、詳しい理由はサーバ側のログに出ます。テスト用の環境で、よくある3つの失敗をわざと起こしました。

起きたことクライアントに出たエラーサーバ側の理由証明書を出さなかったalert certificate requiredpeer did not return a certificate信じない CA の証明書alert unknown caerror 20:発行者が見つからないサーバ用の証明書を出したalert unsupported certificateerror 26:用途が合わないクライアントに届くのは短いアラートだけ。詳しい理由はサーバ側のログで見るTLS 1.3 ではクライアントが先にハンドシェイクを終えるので、つながった後にアラートが届くことがあるmTLS の失敗は「出さない・信じない CA・用途違い」の3つが多い
図19 mTLS がうまくいかないとき

証明書を出さなかった

クライアント証明書を付けずに、証明書が必須のサーバにつなぎます。

クライアント(証明書なし)サーバ(-Verify 1)❶ CertificateRequestここまでは⑨〜⑬と同じ❷ 出せる証明書がない空の Certificate を用意する❸ Certificate(空)・FinishedCertificateVerify は送らない❹ 自分の手順は終わりCONNECTION ESTABLISHED❺ 証明書がない必須なので断る❻ alert certificate_requiredアラートの番号は 116❼ 読もうとしてエラーSSL alert number 116TLS 1.3 では、クライアントは先に終わるので「つながった」と出てから断られる
図20 証明書を出さなかったときの流れ(❶〜❼)

クライアント証明書なしでつなぐ

echo Q | openssl s_client -connect localhost:8443 -servername api.example.test -CAfile ca.crt -brief
CONNECTION ESTABLISHED
Protocol version: TLSv1.3
Ciphersuite: TLS_AES_256_GCM_SHA384
Requested Signature Algorithms: ECDSA+SHA256:ECDSA+SHA384:ECDSA+SHA512:Ed25519:Ed448:RSA-PSS+SHA256:RSA-PSS+SHA384:RSA-PSS+SHA512:RSA-PSS+SHA256:RSA-PSS+SHA384:RSA-PSS+SHA512:RSA+SHA256:RSA+SHA384:RSA+SHA512:ECDSA+SHA224:RSA+SHA224
Peer certificate: CN = api.example.test
Hash used: SHA256
Signature type: ECDSA
Verification: OK
Server Temp Key: X25519, 253 bits
4077D11BC17F0000:error:0A00045C:SSL routines:ssl3_read_bytes:tlsv13 alert certificate required:../ssl/record/rec_layer_s3.c:1599:SSL alert number 116

サーバ側の表示

40275245057F0000:error:0A0000C7:SSL routines:tls_process_client_certificate:peer did not return a certificate:../ssl/statem/statem_srvr.c:3509:

先に CONNECTION ESTABLISHED と出てから、certificate required のアラートで切られています。TLS 1.3 では、クライアントが Finished を送った時点でクライアント側のハンドシェイクは終わり、サーバの判断はその後に届くからです。RFC 8446 では、証明書を求められて出せる証明書がないクライアントは、空の Certificate を送ります。そのときサーバは、認証なしで続けるか、certificate_required のアラートで終えるかを選べます。

信じていない CA の証明書を出した

別の CA(Other CA)で、同じ CN=app01 の証明書を作って出します。

別の CA の証明書でつなぐ

openssl req -x509 -newkey ec -pkeyopt ec_paramgen_curve:P-256 -nodes -keyout other-ca.key -out other-ca.crt -days 30 -subj "/CN=Other CA"
openssl req -newkey ec -pkeyopt ec_paramgen_curve:P-256 -nodes -keyout rogue.key -out rogue.csr -subj "/CN=app01"
openssl x509 -req -in rogue.csr -CA other-ca.crt -CAkey other-ca.key -CAcreateserial -out rogue.crt -days 7 -extfile client.ext
curl -sS -o /dev/null --cacert ca.crt --cert rogue.crt --key rogue.key https://localhost:8443/
curl: (56) OpenSSL SSL_read: OpenSSL/3.0.13: error:0A000418:SSL routines::tlsv1 alert unknown ca, errno 0

サーバ側の表示

depth=0 CN = app01
verify error:num=20:unable to get local issuer certificate
40275245057F0000:error:0A000086:SSL routines:tls_process_client_certificate:certificate verify failed:../ssl/statem/statem_srvr.c:3522:

名前が同じ app01 でも、サーバが信じる CA(Demo Root CA)までたどれないので断られます。PKI の記事で見た error 20 と同じです。

サーバ用の証明書を出した

Demo Root CA が出した証明書でも、用途がサーバ用(serverAuth)の server.crt をクライアント証明書として出します。

サーバ用の証明書でつなぐ

curl -sS -o /dev/null --cacert ca.crt --cert server.crt --key server.key https://localhost:8443/
curl: (56) OpenSSL SSL_read: OpenSSL/3.0.13: error:0A000413:SSL routines::sslv3 alert unsupported certificate, errno 0

サーバ側の表示

depth=0 CN = api.example.test
verify error:num=26:unsuitable certificate purpose
40275245057F0000:error:0A000086:SSL routines:tls_process_client_certificate:certificate verify failed:../ssl/statem/statem_srvr.c:3522:

CA は信じられても、用途が clientAuth でないので断られます。クライアントに届くアラートの名前(unknown ca、unsupported certificate など)で、どの確認で止まったかの見当がつきます。期限切れや失効などのエラーの番号は、PKI の記事で見たものと同じです。

クライアントの表示サーバ側の理由確認すること
certificate requiredpeer did not return a certificateクライアントに証明書と鍵を指定したか。CertificateRequest の CA の一覧に合う証明書を持っているか
unknown caerror 20サーバの「信じる CA」に、発行した CA(と中間 CA)が入っているか
unsupported certificateerror 26証明書の用途に clientAuth があるか

まとめ

項目内容
mTLS とはTLS のハンドシェイクで、クライアントも証明書を出し、秘密鍵で署名して自分を示す
増えるメッセージCertificateRequest(サーバ)、Certificate と CertificateVerify(クライアント)
サーバが確かめること信じる CA までのつながり・期限と失効・clientAuth の用途・CertificateVerify の署名
別に決めることどの証明書が誰か、その相手に何を許すか
トークンとの組み合わせcnf の x5t#S256 でトークンを証明書に結びつけ、盗まれても使えないようにする(RFC 8705)
Azure App Service前段で TLS を終え、X-ARR-ClientCert で転送する。確かめるのはアプリ
Azure API Managementvalidate-client-certificate ポリシーで確かめる

参考にした資料

この記事は、次の標準と公式ドキュメントで内容を確かめて書きました。仕様や対応状況は変わることがあるので、最新の情報はリンク先で確かめてください。

あとがき

実際に試してみて意外だったのは、証明書を出さずにつないだとき、クライアントには先に「つながった」と表示されることでした。TLS 1.3 ではクライアントのほうが先にハンドシェイクを終えるので、断られたことは後から届きます。また、App Service のように前段で TLS が終わる構成では、証明書を確かめる責任がアプリに移ることも、図にして改めて整理できました。次の記事では、こうした証明書や API キーなどの秘密の値を、どこにどう置いて管理するかを扱います。

コメント