web-dev-qa-db-ja.com

クライアント証明書を使用してWeb APIで認証および承認する方法

クライアント証明書を使用して、Web APIを使用してデバイスの認証と承認を試み、潜在的なソリューションの問題を解決するための簡単な概念実証を開発しました。クライアント証明書がWebアプリケーションで受信されないという問題に直面しています。多くの人がこの問題を報告しました このQ&Aを含む ですが、誰も答えがありません。私の希望は、この問題を復活させるための詳細を提供し、できれば私の問題に対する答えを得ることにあります。私は他の解決策を受け入れています。主な要件は、C#で記述されたスタンドアロンプ​​ロセスがWeb APIを呼び出し、クライアント証明書を使用して認証できることです。

このPOCのWeb APIは非常にシンプルで、単一の値を返すだけです。属性を使用して、HTTPSが使用されていること、およびクライアント証明書が存在することを検証します。

public class SecureController : ApiController
{
    [RequireHttps]
    public string Get(int id)
    {
        return "value";
    }

}

RequireHttpsAttributeのコードは次のとおりです。

public class RequireHttpsAttribute : AuthorizationFilterAttribute 
{ 
    public override void OnAuthorization(HttpActionContext actionContext) 
    { 
        if (actionContext.Request.RequestUri.Scheme != Uri.UriSchemeHttps) 
        { 
            actionContext.Response = new HttpResponseMessage(System.Net.HttpStatusCode.Forbidden) 
            { 
                ReasonPhrase = "HTTPS Required" 
            }; 
        } 
        else 
        {
            var cert = actionContext.Request.GetClientCertificate();
            if (cert == null)
            {
                actionContext.Response = new HttpResponseMessage(System.Net.HttpStatusCode.Forbidden)
                {
                    ReasonPhrase = "Client Certificate Required"
                }; 

            }
            base.OnAuthorization(actionContext); 
        } 
    } 
}

このPOCでは、クライアント証明書の可用性を確認しています。これが動作したら、証明書のリストに対して検証するために、証明書に情報のチェックを追加できます。

このWebアプリケーションのSSLのIISの設定は次のとおりです。

enter image description here

クライアント証明書を使用してリクエストを送信するクライアントのコードは次のとおりです。これはコンソールアプリケーションです。

    private static async Task SendRequestUsingHttpClient()
    {
        WebRequestHandler handler = new WebRequestHandler();
        X509Certificate certificate = GetCert("ClientCertificate.cer");
        handler.ClientCertificates.Add(certificate);
        handler.ServerCertificateValidationCallback = new RemoteCertificateValidationCallback(ValidateServerCertificate);
        handler.ClientCertificateOptions = ClientCertificateOption.Manual;
        using (var client = new HttpClient(handler))
        {
            client.BaseAddress = new Uri("https://localhost:44398/");
            client.DefaultRequestHeaders.Accept.Clear();
            client.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json"));

            HttpResponseMessage response = await client.GetAsync("api/Secure/1");
            if (response.IsSuccessStatusCode)
            {
                string content = await response.Content.ReadAsStringAsync();
                Console.WriteLine("Received response: {0}",content);
            }
            else
            {
                Console.WriteLine("Error, received status code {0}: {1}", response.StatusCode, response.ReasonPhrase);
            }
        }
    }

    public static bool ValidateServerCertificate(
      object sender,
      X509Certificate certificate,
      X509Chain chain,
      SslPolicyErrors sslPolicyErrors)
    {
        Console.WriteLine("Validating certificate {0}", certificate.Issuer);
        if (sslPolicyErrors == SslPolicyErrors.None)
            return true;

        Console.WriteLine("Certificate error: {0}", sslPolicyErrors);

        // Do not allow this client to communicate with unauthenticated servers.
        return false;
    }

このテストアプリを実行すると、RequireHttpsAttributeに入り、クライアント証明書が見つからないことを示す「Client Certificate Required」という理由フレーズで403 Forbiddenのステータスコードが返されます。これをデバッガーで実行すると、証明書がロードされてWebRequestHandlerに追加されることを確認しました。証明書は、ロードされているCERファイルにエクスポートされます。秘密鍵を含む完全な証明書は、Webアプリケーションサーバーのローカルマシンの個人および信頼されたルートストアにあります。このテストでは、クライアントとWebアプリケーションが同じマシンで実行されています。

Fiddlerを使用してこのWeb APIメソッドを呼び出し、同じクライアント証明書を添付すれば問題なく動作します。 Fiddlerを使用する場合、RequireHttpsAttributeのテストに合格し、成功のステータスコード200を返し、期待値を返します。

HttpClientがリクエストでクライアント証明書を送信せず、解決策を見つけたという同じ問題に誰かが遭遇しましたか?

更新1:

また、秘密キーを含む証明書ストアから証明書を取得しようとしました。取得方法は次のとおりです。

    private static X509Certificate2 GetCert2(string hostname)
    {
        X509Store myX509Store = new X509Store(StoreName.My, StoreLocation.LocalMachine);
        myX509Store.Open(OpenFlags.ReadWrite);
        X509Certificate2 myCertificate = myX509Store.Certificates.OfType<X509Certificate2>().FirstOrDefault(cert => cert.GetNameInfo(X509NameType.SimpleName, false) == hostname);
        return myCertificate;
    }

この証明書が正しく取得され、クライアント証明書コレクションに追加されていることを確認しました。しかし、サーバーコードがクライアント証明書を取得しない場合と同じ結果が得られました。

完全を期すために、ファイルから証明書を取得するために使用されるコードを次に示します。

    private static X509Certificate GetCert(string filename)
    {
        X509Certificate Cert = X509Certificate.CreateFromCertFile(filename);
        return Cert;

    }

ファイルから証明書を取得すると、タイプX509Certificateのオブジェクトが返され、証明書ストアから取得すると、タイプX509Certificate2になります。 X509CertificateCollection.Addメソッドは、X509Certificateのタイプを予期しています。

更新2:私はまだこれを解明しようとしており、多くの異なるオプションを試しましたが、役に立ちませんでした。

  • ローカルホストではなくホスト名で実行するようにWebアプリケーションを変更しました。
  • SSLを要求するようにWebアプリケーションを設定しました
  • 証明書がクライアント認証用に設定されており、信頼されたルートにあることを確認しました
  • Fiddlerでクライアント証明書をテストする以外に、Chromeでも検証しました。

これらのオプションを試している間のある時点で、機能し始めました。その後、変更のバックアウトを開始して、何が機能するのかを確認しました。それは働き続けた。次に、信頼されたルートから証明書を削除して、これが必要であることを検証し、機能しなくなったため、証明書を信頼されたルートに戻しても機能しなくなることがありました。 Chromeは、使用されている証明書の入力を求められず、Chromeでは失敗しますが、Fiddlerでは引き続き動作します。私が行方不明になっているいくつかの魔法の構成が必要です。

また、バインディングで「クライアント証明書のネゴシエート」を有効にしようとしましたが、Chromeはまだクライアント証明書を要求しません。 「netsh http show sslcert」を使用した設定は次のとおりです。

 IP:port                 : 0.0.0.0:44398
 Certificate Hash        : 429e090db21e14344aa5d75d25074712f120f65f
 Application ID          : {4dc3e181-e14b-4a21-b022-59fc669b0914}
 Certificate Store Name  : MY
 Verify Client Certificate Revocation    : Disabled
 Verify Revocation Using Cached Client Certificate Only    : Disabled
 Usage Check    : Enabled
 Revocation Freshness Time : 0
 URL Retrieval Timeout   : 0
 Ctl Identifier          : (null)
 Ctl Store Name          : (null)
 DS Mapper Usage    : Disabled
 Negotiate Client Certificate    : Enabled

私が使用しているクライアント証明書は次のとおりです。

enter image description here

enter image description here

enter image description here

私は問題が何であるかについて当惑しています。私はこれを理解するのを助けることができる誰に対しても報奨金を追加しています。

38
Kevin Junghans

トレースは、問題が何であるかを見つけるのに役立ちました(その提案についてFabianに感謝します)。さらにテストして、クライアント証明書を別のサーバー(Windows Server 2012)で動作させることができることを発見しました。開発マシン(Window 7)でこれをテストしていたので、このプロセスをデバッグできました。したがって、トレースをIISサーバーと比較し、機能したサーバーと機能しなかったサーバーを比較することで、トレースログ内の関連する行を特定できました。クライアント証明書が機能したログの一部を次に示します。これは送信の直前の設定です

System.Net Information: 0 : [17444] InitializeSecurityContext(In-Buffers count=2, Out-Buffer length=0, returned code=CredentialsNeeded).
System.Net Information: 0 : [17444] SecureChannel#54718731 - We have user-provided certificates. The server has not specified any issuers, so try all the certificates.
System.Net Information: 0 : [17444] SecureChannel#54718731 - Selected certificate:

クライアント証明書が失敗したマシンのトレースログは次のようになります。

System.Net Information: 0 : [19616] InitializeSecurityContext(In-Buffers count=2, Out-Buffer length=0, returned code=CredentialsNeeded).
System.Net Information: 0 : [19616] SecureChannel#54718731 - We have user-provided certificates. The server has specified 137 issuer(s). Looking for certificates that match any of the issuers.
System.Net Information: 0 : [19616] SecureChannel#54718731 - Left with 0 client certificates to choose from.
System.Net Information: 0 : [19616] Using the cached credential handle.

サーバーが137の発行者を指定したことを示す行に注目すると、これが見つかりました 私の問題に似ているQ&A 。私の解決策は、証明書が信頼されたルートにあるため、回答としてマークされたものではありませんでした。答えは その下のもの レジストリを更新する場所です。レジストリキーに値を追加しました。

HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Control\SecurityProviders\SCHANNEL

値名:SendTrustedIssuerList値タイプ:REG_DWORD値データ:0(False)

この値をレジストリに追加すると、Windows 7マシンで動作し始めました。これはWindows 7の問題のようです。

7
Kevin Junghans

更新:

Microsoftの例:

https://docs.Microsoft.com/en-us/Azure/app-service/app-service-web-configure-tls-mutual-auth#special-considerations-for-certificate-validation

オリジナル

これが、クライアント証明書を機能させ、特定のルートCAが発行したことと、特定の証明書であることを確認する方法です。

最初に<src>\.vs\config\applicationhost.configを編集し、次の変更を行いました:<section name="access" overrideModeDefault="Allow" />

これにより、<system.webServer>web.configを編集し、IIS Expressでクライアント認証を必要とする次の行を追加できます。 注:開発目的でこれを編集しましたが、本番環境でのオーバーライドは許可しません。

実稼働環境では、このようなガイドに従ってIISをセットアップします。

https://medium.com/@hafizmohammedg/configuring-client-certificates-on-iis-95aef4174ddb

web.config:

<security>
  <access sslFlags="Ssl,SslNegotiateCert,SslRequireCert" />
</security>

APIコントローラー:

[RequireSpecificCert]
public class ValuesController : ApiController
{
    // GET api/values
    public IHttpActionResult Get()
    {
        return Ok("It works!");
    }
}

属性:

public class RequireSpecificCertAttribute : AuthorizationFilterAttribute
{
    public override void OnAuthorization(HttpActionContext actionContext)
    {
        if (actionContext.Request.RequestUri.Scheme != Uri.UriSchemeHttps)
        {
            actionContext.Response = new HttpResponseMessage(System.Net.HttpStatusCode.Forbidden)
            {
                ReasonPhrase = "HTTPS Required"
            };
        }
        else
        {
            X509Certificate2 cert = actionContext.Request.GetClientCertificate();
            if (cert == null)
            {
                actionContext.Response = new HttpResponseMessage(System.Net.HttpStatusCode.Forbidden)
                {
                    ReasonPhrase = "Client Certificate Required"
                };

            }
            else
            {
                X509Chain chain = new X509Chain();

                //Needed because the error "The revocation function was unable to check revocation for the certificate" happened to me otherwise
                chain.ChainPolicy = new X509ChainPolicy()
                {
                    RevocationMode = X509RevocationMode.NoCheck,
                };
                try
                {
                    var chainBuilt = chain.Build(cert);
                    Debug.WriteLine(string.Format("Chain building status: {0}", chainBuilt));

                    var validCert = CheckCertificate(chain, cert);

                    if (chainBuilt == false || validCert == false)
                    {
                        actionContext.Response = new HttpResponseMessage(System.Net.HttpStatusCode.Forbidden)
                        {
                            ReasonPhrase = "Client Certificate not valid"
                        };
                        foreach (X509ChainStatus chainStatus in chain.ChainStatus)
                        {
                            Debug.WriteLine(string.Format("Chain error: {0} {1}", chainStatus.Status, chainStatus.StatusInformation));
                        }
                    }
                }
                catch (Exception ex)
                {
                    Debug.WriteLine(ex.ToString());
                }
            }

            base.OnAuthorization(actionContext);
        }
    }

    private bool CheckCertificate(X509Chain chain, X509Certificate2 cert)
    {
        var rootThumbprint = WebConfigurationManager.AppSettings["rootThumbprint"].ToUpper().Replace(" ", string.Empty);

        var clientThumbprint = WebConfigurationManager.AppSettings["clientThumbprint"].ToUpper().Replace(" ", string.Empty);

        //Check that the certificate have been issued by a specific Root Certificate
        var validRoot = chain.ChainElements.Cast<X509ChainElement>().Any(x => x.Certificate.Thumbprint.Equals(rootThumbprint, StringComparison.InvariantCultureIgnoreCase));

        //Check that the certificate thumbprint matches our expected thumbprint
        var validCert = cert.Thumbprint.Equals(clientThumbprint, StringComparison.InvariantCultureIgnoreCase);

        return validRoot && validCert;
    }
}

次に、別のWebプロジェクトからテストされた、このようなクライアント認証でAPIを呼び出すことができます。

[RoutePrefix("api/certificatetest")]
public class CertificateTestController : ApiController
{

    public IHttpActionResult Get()
    {
        var handler = new WebRequestHandler();
        handler.ClientCertificateOptions = ClientCertificateOption.Manual;
        handler.ClientCertificates.Add(GetClientCert());
        handler.UseProxy = false;
        var client = new HttpClient(handler);
        var result = client.GetAsync("https://localhost:44331/api/values").GetAwaiter().GetResult();
        var resultString = result.Content.ReadAsStringAsync().GetAwaiter().GetResult();
        return Ok(resultString);
    }

    private static X509Certificate GetClientCert()
    {
        X509Store store = null;
        try
        {
            store = new X509Store(StoreName.My, StoreLocation.CurrentUser);
            store.Open(OpenFlags.OpenExistingOnly | OpenFlags.ReadOnly);

            var certificateSerialNumber= "‎81 c6 62 0a 73 c7 b1 aa 41 06 a3 ce 62 83 ae 25".ToUpper().Replace(" ", string.Empty);

            //Does not work for some reason, could be culture related
            //var certs = store.Certificates.Find(X509FindType.FindBySerialNumber, certificateSerialNumber, true);

            //if (certs.Count == 1)
            //{
            //    var cert = certs[0];
            //    return cert;
            //}

            var cert = store.Certificates.Cast<X509Certificate>().FirstOrDefault(x => x.GetSerialNumberString().Equals(certificateSerialNumber, StringComparison.InvariantCultureIgnoreCase));

            return cert;
        }
        finally
        {
            store?.Close();
        }
    }
}
6
Ogglas

実際には、多くの信頼されたルート証明書を必要とする同様の問題がありました。新しくインストールされたウェブサーバーには、数百ものものがありました。私たちのルートは文字Zで始まり、リストの最後になりました。

問題は、IISがクライアントに最初の20の信頼されたルートのみをクライアントに送信し、残りを切り捨てたであったことです。数年前、ツールの名前を思い出せません... IIS管理スイートの一部でしたが、Fiddlerも同様です。エラーを認識した後、不要な多くの信頼されたルートを削除しました。これは試行錯誤で行われたため、削除するものに注意してください。

クリーンアップ後、すべてが魅力的に機能しました。

1
Brandtware

HttpClientが完全なクライアント証明書(秘密キーを含む)にアクセスできることを確認してください。

ファイル「ClientCertificate.cer」を使用してGetCertを呼び出しています。これにより、秘密キーが含まれていないという前提になります。Windows内ではpfxファイルである必要があります。 Windows証明書ストアから証明書にアクセスし、指紋を使用して検索することをお勧めします。

指紋をコピーするときは注意してください:証明書管理で表示するときに、印刷できない文字がいくつかあります(文字列をnotepad ++にコピーして、表示された文字列の長さを確認します)。

1
Daniel Nachtrub

ソースコードを見ると、秘密キーに何らかの問題があるに違いないと思います。

実際には、渡された証明書のタイプがX509Certificate2で​​あり、秘密鍵があるかどうかを確認します。

秘密キーが見つからない場合は、CurrentUserストアで、次にLocalMachineストアで証明書を見つけようとします。証明書が見つかると、秘密鍵が存在するかどうかを確認します。

(クラスSecureChannnelの ソースコード メソッドEnsurePrivateKeyを参照)

そのため、インポートしたファイル(.cer-秘密キーなし、または.pfx-秘密キーあり)およびどのストアに応じて、正しいファイルが見つからず、Request.ClientCertificateが入力されません。

ネットワークトレース をアクティブにして、これをデバッグしてみてください。次のような出力が得られます。

  • 証明書ストアで一致する証明書を見つけようとしています
  • LocalMachineストアまたはCurrentUserストアで証明書が見つかりません。
0
Fabian

最近似たような問題に遭遇し、ファビアンのアドバイスに従って実際に解決策を導きました。クライアント証明書では、次の2つのことを確認する必要があります。

  1. 秘密鍵は、実際には証明書の一部としてエクスポートされています。

  2. アプリを実行しているアプリケーションプールIDは、上記の秘密キーにアクセスできます。

私たちの場合、私は:

  1. エクスポートチェックボックスをオンにして、プライベートサーバーが送信されたことを確認しながら、pfxファイルをローカルサーバーストアにインポートします。
  2. MMCコンソールを使用して、使用するサービスアカウントに証明書の秘密キーへのアクセスを許可します。

他の回答で説明されている信頼されたルートの問題は有効なものであり、私たちの場合の問題ではありませんでした。

0