Skip to content

MS_ASPNETClientCert

nishi_74322014 edited this page Aug 21, 2026 · 1 revision

ASP.NET+クライアント証明書

概要

  • 「Certificate Binding」
    (OAuth 2.0 Mutual TLS Client Authentication and Certificate Bound Access Tokens
    MS_OAuthMTLS.md))で必要になったため。

  • 開発環境で設定する場合は、1 台の中で、

    • クライアント設定と
    • サーバ設定の

    両方を行う必要がある。

補足(mTLS の全体像): 本ページが扱うのは
**クライアント証明書による相互 TLS 認証(mTLS)**の設定である。
各節に入る前に、全体の流れを押さえておく。

【通常の TLS と mTLS】★
   通常の TLS(片方向)
     クライアント →→ サーバ証明書を検証 →→ サーバ
     → 「サーバが本物か」だけを確認する

   【mTLS(相互 TLS)】
     クライアント ←→ 互いに証明書を検証 ←→ サーバ
     → 「クライアントが誰か」も TLS 層で確認する ★
     → ID/パスワードやトークンより前の段階で
       接続そのものを拒否できる

【使われる場面】
   ・【金融 API(FAPI)】★ ─ 本ページの動機である
     OAuth 2.0 mTLS Client Authentication
   ・システム間連携(B2B の専用 API)
   ・ゼロトラスト環境でのデバイス認証
   ・Kubernetes のサービス間通信(サービス メッシュ)

【証明書の流れ(本ページの構成)】
   ① クライアントに証明書を入れる(ブラウザ / HttpClient)
   ② Web サーバで「証明書を要求する」よう設定する
      (IIS / IIS Express / Kestrel / Apache / nginx)
   ③ アプリ側で受け取った証明書を読む
      (ASP.NET / ASP.NET Core)

クライアント

クライアントがサーバにクライアント証明書を渡すよう構成する。

移行メモ(誤字): 移行元では「クラアント」(「イ」が欠落)と
記載されている箇所が複数あったため、「クライアント」に統一した。

Browser

Windows

Mac

HttpClient

補足(HttpClient でクライアント証明書を付ける現在の書き方):
.NET Core 以降は SocketsHttpHandler が既定になり、書き方が整理された。

// .NET Core / .NET 5 以降
var handler = new HttpClientHandler();
handler.ClientCertificates.Add(
    new X509Certificate2("client.pfx", "password"));
// 自己署名のサーバ証明書を検証したい場合(開発時のみ)
// handler.ServerCertificateCustomValidationCallback =
//     HttpClientHandler.DangerousAcceptAnyServerCertificateValidator;
var client = new HttpClient(handler);
【HttpClient の使い方の注意(本題より重要)】★
 ① 【using で毎回捨ててはいけない】
     → ソケットが TIME_WAIT で滞留し、
       【ソケット枯渇】を起こす
     → 静的に 1 つ持つか、
       【IHttpClientFactory】を使う ★
 ② 【長生きさせすぎても DNS 変更に追随しない】
     → IHttpClientFactory が
       ハンドラを定期的に作り直して解決する
 ③ 証明書付きのハンドラは
     → 名前付きクライアントとして DI に登録する
         services.AddHttpClient("mtls")
             .ConfigurePrimaryHttpMessageHandler(() => handler);
【証明書の置き場所】★
   ・開発  … ファイル(.pfx)+ ユーザー シークレット
   ・本番  … 【Windows 証明書ストア】または
             【Azure Key Vault】等 ★
     → パスワード付き pfx をリポジトリに置かない
   ・X509Certificate2 は IDisposable
     → .NET 6 以降、明示的な破棄が推奨されている

サーバ

  • サーバがクライアントからクライアント証明書を受けるよう構成する。
  • クライアント証明書(証明書 を参照)は
    Web -> AP に HTTP ヘッダで渡される。
    • 例えば、Azure Web Apps では、X-ARR-ClientCert ヘッダを使用して渡される。
    • 他の中間層では、X-Client-Cert ヘッダなどが使用されるらしい。

移行メモ(誤字): 移行元の「例えは、Azure Web Apps では」を
例えば」に修正した。

補足(ヘッダで渡す方式のセキュリティ上の注意): 本節が指摘する
「Web → AP に HTTP ヘッダで渡される」という構成には、重大な前提がある。

【なぜヘッダで渡すのか】★
   TLS の終端(証明書の検証)は
   【前段のリバース プロキシ / ロード バランサ】が行う
     → アプリ サーバには
       平文の HTTP で転送される
     → よって証明書の情報は
       【HTTP ヘッダに詰め替えて】渡すしかない

   代表的なヘッダ名
     Azure App Service : X-ARR-ClientCert(Base64 の DER)★
     nginx             : 設定次第(X-SSL-CERT 等を自分で定義)
     Apache            : SSL_CLIENT_CERT(環境変数)
     AWS ALB           : X-Amzn-Mtls-Clientcert
     Kubernetes Ingress: 実装依存
   → 【標準がない】ため、原文の「らしい」は正しい ★

【致命的な落とし穴】★★
   このヘッダは【クライアントが自分で付けられる】
     → 前段プロキシが
       【受信した同名ヘッダを必ず削除・上書きする】
       設定になっていないと、
       【任意の証明書になりすませる】
     → 認証が完全に破られる

   【必ず確認すること】
     ① プロキシが該当ヘッダを【常に上書き】する設定か
     ② アプリ サーバが
        【プロキシからの接続しか受け付けない】か
        (ネットワーク分離 / 相互認証)
     ③ ASP.NET Core なら
        ForwardedHeadersOptions の
        KnownProxies / KnownNetworks を設定する ★

Windows

IIS

applicationhost.configIIS Express を参照)に
以下の様な設定を行う。

<security>
  <access sslFlags="Ssl, SslNegotiateCert, SslRequireCert" />
  ...
  <authentication>
    <iisClientCertificateMappingAuthentication enabled="true">
    </iisClientCertificateMappingAuthentication>
  ...
</security>

補足(sslFlags の意味): 上記 3 つのフラグは役割が違うので
明示しておく。

【sslFlags の組み合わせ】★
   Ssl                … SSL/TLS を必須にする
   SslNegotiateCert   … クライアント証明書を【要求する】
                        (提示されなくても接続は続く)
   SslRequireCert     … クライアント証明書を【必須にする】★
                        (なければ 403.7 で拒否)
   Ssl128             … 128bit 以上を要求(現在は無意味)

   → 「証明書があれば使う、無ければパスワード認証」
     という併用型にしたい場合は
     【SslRequireCert を外す】★

【applicationhost.config の場所】
   ・VS 2015 以降のソリューション固有
       <ソリューション>\.vs\config\applicationhost.config ★
   ・ユーザー既定
       %USERPROFILE%\Documents\IISExpress\config\applicationhost.config
   → 前者を編集しないと反映されないことが多い

Kestrel

補足(Kestrel でクライアント証明書を要求する現在の書き方):
本ページ執筆時(.NET Core 2.0 頃)は Issue を追う必要があったが、
現在は正式な API がある

// Program.cs(.NET 6 以降の最小 API 形式)
builder.WebHost.ConfigureKestrel(o => {
    o.ConfigureHttpsDefaults(https => {
        https.ClientCertificateMode =
            ClientCertificateMode.RequireCertificate;   // ★
        https.ClientCertificateValidation =
            (cert, chain, errors) => {
                // 自己署名や独自 CA を許容する場合の独自検証
                return true;   // ← 本番では必ず実装すること
            };
    });
});
【ClientCertificateMode の 4 値】★
   NoCertificate        … 要求しない(既定)
   AllowCertificate     … 要求するが、無くても接続を許す
   RequireCertificate   … 必須。無ければ接続を切る ★
   DelayCertificate     … 【接続後に改めて要求する】
                          (.NET 5 以降。特定のパスだけ
                           証明書を求めたい場合に使う)★

【認証ミドルウェアとして扱う】★
   Microsoft.AspNetCore.Authentication.Certificate
   パッケージを使うと、
   証明書を【ClaimsPrincipal に変換】できる

     builder.Services
       .AddAuthentication(CertificateAuthenticationDefaults.AuthenticationScheme)
       .AddCertificate(o => {
           o.AllowedCertificateTypes = CertificateTypes.All;
           o.Events = new CertificateAuthenticationEvents {
               OnCertificateValidated = ctx => { /* 検証 */ }
           };
       });

   → [Authorize] で保護でき、
     User.Identity から証明書の主体を取れる ★

Linux

Kestrel

Apache

nginx

移行メモ(未執筆): 移行元では Linux 配下の 3 節
(Kestrel / Apache / nginx)が見出しのみで本文がない
見出しは残したうえで、以下に補足を追記した。

補足(Linux 側の設定の要点): 未執筆の 3 節について骨子を示す。

【Kestrel(Linux)】★
   → 前掲の Windows 版と【全く同じ】
     ClientCertificateMode の設定でよい
   → Kestrel はクロスプラットフォームであり、
     OS による差はない
   ※ 証明書ストアの扱いだけが違う
     (Linux には Windows 証明書ストアがないため
      ファイル or 環境変数から読む)
【Apache(mod_ssl)】
   SSLVerifyClient  require        # 必須にする ★
   SSLVerifyDepth   2
   SSLCACertificateFile /path/ca.crt

   # AP サーバへ転送する場合
   SSLOptions +ExportCertData      # SSL_CLIENT_CERT を有効化 ★
   RequestHeader set X-Client-Cert "%{SSL_CLIENT_CERT}s"
   RequestHeader unset X-Client-Cert early   # ← 【なりすまし対策】★
【nginx】
   ssl_client_certificate /path/ca.crt;
   ssl_verify_client on;           # optional にすると任意 ★
   ssl_verify_depth   2;

   location / {
     proxy_pass http://backend;
     proxy_set_header X-Client-Cert  $ssl_client_escaped_cert;  # ★
     proxy_set_header X-Client-Verify $ssl_client_verify;
     proxy_set_header X-Client-DN     $ssl_client_s_dn;
   }

   ※ $ssl_client_cert は【改行を含む】ため
     ヘッダに載せると壊れる
     → 【$ssl_client_escaped_cert】(URL エンコード済み)を使う ★
   ※ proxy_set_header で毎回上書きすることが
     そのまま【なりすまし対策】になる

Web Forms

  • Page
// System.Web.HttpRequest
HttpClientCertificate cs = Request.ClientCertificate;

MVC

  • Controller
// System.Web.HttpRequestBase
HttpClientCertificate cs = Request.ClientCertificate;

WebAPI

  • ApiController
// System.Net.Http.HttpRequestMessage
// System.Net.Http.HttpRequestMessageExtensions
X509Certificate2 cs = Request.GetClientCertificate();

移行メモ(誤字): 移行元の「ApiControler」を「ApiController」に修正した。

MVC

// Microsoft.AspNetCore.Http.HttpRequest
X509Certificate2 x509 = Request.HttpContext.Connection.ClientCertificate;

WebAPI

補足(作者の推測への回答と、現在の実装): 「HTTP ヘッダの標準が無いため
実装されていないのでは?」という推測は、概ね正しい

【なぜ ASP.NET と ASP.NET Core で違ったのか】★
   ASP.NET(.NET Framework)
     → 実行基盤が【IIS 一択】
     → IIS が検証済みの証明書を
       ワーカープロセスへ渡す仕組み(ISAPI)が
       【最初から決まっていた】
     → HttpRequest.ClientCertificate として提供できた

   ASP.NET Core
     → 【Kestrel / IIS / HTTP.sys / nginx / Apache】と
       ホストが多様
     → 「証明書がどう届くか」が
       ホストごとに違うため、
       【共通 API を先に作れなかった】★
     → 原文の推測は本質を突いている

【解決の経緯】
   ・ConnectionInfo.ClientCertificate
       → Kestrel が直接 TLS を終端する場合の
         共通の入り口として整備された
   ・【前段プロキシ経由の場合】は依然として別問題
     → Microsoft.AspNetCore.Authentication.Certificate と
       【CertificateForwarding ミドルウェア】が用意された ★

         builder.Services.AddCertificateForwarding(o => {
             o.CertificateHeader = "X-ARR-ClientCert";
             o.HeaderConverter = h =>
                 new X509Certificate2(Convert.FromBase64String(h));
         });
         ...
         app.UseCertificateForwarding();   // UseAuthentication より前 ★

     → これによりヘッダ経由でも
       ConnectionInfo.ClientCertificate と
       同じように扱えるようになった
【WebAPI 節が「-」である点について】
   ASP.NET Core では
   【MVC と WebAPI が同一のパイプライン】であるため、
   別扱いする必要がない。
     → ControllerBase でも同じく
       HttpContext.Connection.ClientCertificate で取れる ★
   → 原文が「-」としているのは、この意味で正しい

クライアント証明書

証明書 の該当節も参照)

以下で生成してみたが、ダメ。

Visual Studio

  • Visual Studio の署名で作成した PFX であれば利用可能だった。

  • OpenSSL と何が違うか不明。何か属性が足りてないのか?

  • 一応、使用するには、

    • ブラウザから使用できるように設定する。

    • サーバから使用できるように設定する。

      • Windows
      • Linux

    両方を行う必要がある。

補足(「何が違うか不明」への回答:おそらく EKU): 作者が保留にした
この疑問には、典型的な答えがある。

【最有力の原因:拡張キー使用法(EKU)】★
   証明書には【何に使ってよいか】を示す拡張がある
     Extended Key Usage (EKU)
       ・serverAuth      (1.3.6.1.5.5.7.3.1) … サーバ認証
       ・【clientAuth】  (1.3.6.1.5.5.7.3.2) … クライアント認証 ★
       ・codeSigning     (1.3.6.1.5.5.7.3.3) … コード署名

   ・OpenSSL の素朴な手順で作ると
     【EKU が入らない】ことが多い
     → IIS / Windows は
       clientAuth のない証明書を
       【クライアント証明書として選ばせない】★

   ・Visual Studio の「署名」で作る PFX は
     codeSigning が入るが、
     環境によっては通ってしまうことがある
     → 原文が「VS の PFX なら使えた」としているのは
       この辺りの差と考えられる

【OpenSSL で正しく作る】
   # 拡張を書いたファイル(client_ext.cnf)
   keyUsage = critical, digitalSignature, keyEncipherment
   extendedKeyUsage = clientAuth          # ← これ ★
   subjectAltName = email:user@example.com

   openssl x509 -req -in client.csr -CA ca.crt -CAkey ca.key \
     -CAcreateserial -out client.crt -days 365 \
     -extfile client_ext.cnf              # ← 拡張を適用 ★

   openssl pkcs12 -export -inkey client.key -in client.crt \
     -certfile ca.crt -out client.pfx     # CA も同梱する ★
【他によくある原因】
 ② 【CA 証明書が信頼されていない】
     → サーバ側の「信頼されたルート証明機関」に
       CA を入れる必要がある
     → 自己署名(CA を介さない)だと
       チェーンが作れず弾かれる ★
 ③ 【PFX に秘密鍵が入っていない】
     → -export 時に -inkey を忘れている
 ④ 【証明書ストアの置き場所】
     → ブラウザ(Chrome/Edge)は
       Windows の【現在のユーザー > 個人】を見る
     → サーバ側は【ローカル コンピューター】
 ⑤ 【古い署名アルゴリズム】
     → SHA-1 / 1024bit RSA は
       現在の Windows / ブラウザで【拒否される】★
     → SHA-256 / RSA 2048 以上、または ECDSA P-256

参考

自己署名証明書の場合

(OAuth 2.0 Mutual TLS Client Authentication and Certificate Bound Access Tokens
MS_OAuthMTLS.md)の該当節を参照)

クライアント側

サーバー側

Bouncy Castle で HTTP ヘッダから取得した pem を読む的なイメージでイイのでは?

補足(今なら Bouncy Castle は不要): この検討は
.NET Core 2.x 時代のものであり、現在は標準 API で完結する

// PEM からの読み込み(.NET 5 以降)★
var cert = X509Certificate2.CreateFromPem(pemCertText);
// 秘密鍵も含める場合(.NET 6 以降)
var certWithKey = X509Certificate2.CreateFromPem(pemCert, pemKey);
【標準化された経緯】★
   .NET Framework 時代
     → PEM の読み書きが標準 API になく、
       【Bouncy Castle】が定番だった
   .NET 5
     → X509Certificate2.CreateFromPem 追加
   .NET 6
     → ImportFromPem / ExportPkcs8PrivateKeyPem 等が
       RSA / ECDsa にも追加され、【一通り揃った】

   → 今から書くなら
     【外部ライブラリなしで PEM を扱える】★
   ※ Bouncy Castle が今も要るのは、
     CMS / OpenPGP / 特殊な曲線 / 独自 ASN.1 など
     標準 API が扱わない領域
【ヘッダから受け取った証明書の検証で必ずやること】★
 ① チェーン検証(信頼された CA から辿れるか)
      var chain = new X509Chain();
      chain.ChainPolicy.RevocationMode = X509RevocationMode.Online;
      chain.Build(cert);
 ② 有効期限(NotBefore / NotAfter)
 ③ 【失効確認】(CRL / OCSP)★
      → 退職者・紛失端末の締め出しはここでしかできない
 ④ EKU に clientAuth が含まれるか
 ⑤ 【想定した主体(Subject / SAN / 拇印)と一致するか】★
      → 「正しい CA が出した証明書」であることと
        「その利用者であること」は別 ★
      → OAuth mTLS では
        クライアント登録時の拇印と突き合わせる

Tags: 移行, .NET開発, .NET Core, ASP.NET, ASP.NET MVC, セキュリティ, 暗号化, 証明書

NetDevInfraWiki

マイクロソフト系技術情報 Wiki
Open 棟梁 Wiki

(未着手)

開発基盤部会 Wiki

移行管理: DONETODO

Clone this wiki locally