聊一聊HTTPS雙向認證的簡單應用

2023-02-16 06:00:42

背景

在三方介面對接中,偶爾會遇到需要傳遞證書的情況,這種方式其實是在SSL握手過程中會同時驗證使用者端和伺服器的身份,這就是我們常說的 雙向認證

雙向認證需要伺服器和使用者端提供身份認證,只能是伺服器允許的客戶方能存取,安全性相對於要高一些。

下面老黃用幾個小例子來演示一下雙向認證的簡單應用。

準備工作

由於離不開證書,所以我們需要提前生成好幾個證書,這裡用 OpenSSL 來生成一個自簽名的。

2 個根證書,1 個伺服器端證書,2個不是同一個根證書下面的使用者端證書

# 根證書
openssl genrsa -out ca.key 4096
openssl req -new -key ca.key -out ca.csr -days 365
openssl x509 -req -in ca.csr -signkey ca.key -out ca.crt -days 365

# 伺服器端證書
openssl genrsa -out server.key 4096
openssl req -new -key server.key -out server.csr -days 365
openssl x509 -req -in server.csr -out server.crt -CA ca.crt  -CAkey ca.key  -CAcreateserial -days 365
openssl pkcs12 -export -in server.crt -inkey server.key -out server.p12

# 使用者端證書
openssl genrsa -out client.key 4096
openssl req -new -key client.key -out client.csr -days 365
openssl x509 -req -in client.csr -out client.crt -CA ca.crt  -CAkey ca.key  -CAcreateserial -days 365
openssl pkcs12 -export -in client.crt -inkey client.key -out client.p12

最後會有下面幾個檔案要在後面的演示中用到: ca.crtserver.p12server.crtserver.keyclient.p12client2.p12

下面先來看看 ASP.NET Core 直接對外的情況,也就是不依賴 nginx 或 IIS 的情況。

ASP.NET Core

基於 minimal api 來演示,主要是在 ConfigureKestrel 做處理。

var builder = WebApplication.CreateBuilder(args);

builder.WebHost.ConfigureKestrel(x => 
{
    x.Listen(IPAddress.Any, 443, listenOptions =>
    {
        var serverCertificate = new X509Certificate2("server.p12", "abc123");
        var httpsConnectionAdapterOptions = new HttpsConnectionAdapterOptions()
        {
            // must provide a valid certificate for authentication
            ClientCertificateMode = ClientCertificateMode.RequireCertificate,
            SslProtocols = System.Security.Authentication.SslProtocols.Tls12,
            
            ClientCertificateValidation = (cer, chain, error) =>
            {
                // valid the client certificate by you way.
                return CusSSLLib.CaHelper.Valid(cer, chain, error);
            },
            ServerCertificate = serverCertificate
        };
        listenOptions.UseHttps(httpsConnectionAdapterOptions);
    });
});

這裡最核心的是 HttpsConnectionAdapterOptions

ServerCertificate 設定成我們上面生成的伺服器端證書。

ClientCertificateMode 設定成 RequireCertificate,表示使用者端在呼叫的時候必須要傳遞證書。

ClientCertificateValidation 就是驗證使用者端證書的邏輯,這裡可以自定義,範例裡面的驗證邏輯主要針對不被信任的根證書做了驗證。

首先是從資原始檔讀取了根證書,然後再去判斷使用者端證書是否匹配。

internal static string CA_DATA = System.Text.Encoding.UTF8.GetString(CAResource.ca).Replace("-----BEGIN CERTIFICATE-----", "")
             .Replace("-----END CERTIFICATE-----", "")
             .Replace("\r", "")
             .Replace("\n", "");

public static bool Valid(X509Certificate2 certificate, X509Chain chain, SslPolicyErrors policy)
{
    // the root certificate
    var validRootCertificates = new[]
    {
         Convert.FromBase64String(CA_DATA),
    };

    foreach (var element in chain.ChainElements)
    {
        foreach (var status in element.ChainElementStatus)
        {
            // untrusted root certificate
            if (status.Status == X509ChainStatusFlags.UntrustedRoot)
            {
                if (validRootCertificates.Any(x => x.SequenceEqual(element.Certificate.RawData)))
                {
                    continue;
                }
            }

            return false;
        }
    }

    return true;
}

到這裡的話,伺服器端已經可以了。

這個時候從瀏覽器存取,大概會看到這個提示。

下面寫個控制檯用 HttpClient 來存取看看。

void DoOk()
{
    var handler = new HttpClientHandler();
    handler.ClientCertificateOptions = ClientCertificateOption.Manual;
    handler.SslProtocols = SslProtocols.Tls12 | SslProtocols.Tls | SslProtocols.None | SslProtocols.Tls11;
    try
    {
        // add client certificate
        var crt = new X509Certificate2(Path.Combine(Directory.GetCurrentDirectory(), "client.p12"), "123456");
        handler.ClientCertificates.Add(crt);
    }
    catch (Exception e)
    {
        Console.WriteLine(e.Message);
    }

    handler.ServerCertificateCustomValidationCallback = (message, cer, chain, errors) =>
    {
        // valid server certificate
        return CusSSLLib.CaHelper.Valid(cer, chain, errors);
    };

    var client = new HttpClient(handler);
    var url = "https://localhost/WeatherForecast";
    var response = client.GetAsync(url).Result;
    Console.WriteLine(response.IsSuccessStatusCode);
    var result = response.Content.ReadAsStringAsync().Result;
    Console.WriteLine(result);
}

這裡要注意,由於伺服器端用的證書也是自己簽名的,所以這裡的驗證也要放開,想省事的話,可以直接 return true;,不過並不建議這樣操作。

下面是執行的結果,是可以正常存取並返回結果的。

我們再換一張不是同一個根證書的使用者端證書。

不出意外的不能正常存取。

不過上面這種情況在實際應用的時候會偏少一點,大部分還是會掛在反向代理或雲負載均衡上面的。

下面先來看看 nginx 的吧。

nginx 反向代理

webapi 這一塊,建立一個專案,有一個可以存取的介面即可,不用新增其他東西,因為證書這一塊的內容都是在 nginx 那一層做了,webapi做原來該做的事情即可。

下面是 nginx 的組態檔

server {
        listen       443 ssl;
        server_name  localhost;

        # server certificate
        ssl_certificate  /etc/nginx/ssl/server.crt;
        ssl_certificate_key /etc/nginx/ssl/server.key;

        # root certificate
        ssl_client_certificate /etc/nginx/ssl/ca.crt;
        # open client certificate verify
        ssl_verify_client on;
        ssl_session_timeout  5m;            

        location / {
            proxy_pass http://webapi;
            index  index.html;
        }
    }

重點關注 ssl_verify_clientssl_client_certificate

一個是設定開啟使用者端證書的認證,一個是驗證的使用者端證書的關鍵。

這裡的 ssl_client_certificate 用了根證書,為的是可以驗證多個使用者端證書,當然這裡也可以用使用者端證書。

把 webapi 和 nginx 都執行起來。

這個時候存取,就會提示, No required SSL certificate was sent。

用上面的控制檯程式,再存取看看。

正確的證書,可以正常返回,錯誤的證書會返回 400 The SSL certificate error。

基於反向代理的話,操作起來就簡單了一點。

如果是雲負載均衡,只需要按他們的要求上傳對應的證書即可。

講了 nginx,不講講 IIS,好像有點說不過去。

那就再看看 IIS 的設定吧。

IIS 部署

在 Windows 伺服器安裝好 IIS 和託管捆綁包後,要先把我們的根證書安裝到可信的根證書裡面。

然後進行部署,繫結好伺服器端證書後,確認可以正常存取。

然後進行雙向認證的設定。

在對應站點上面的 SSL 設定,把 要求 SSL必需 兩個勾上即可。

後面再存取的時候,就會提示選擇證書

選擇正確的證書後就可以正常存取了。

然後我們再用前面的控制檯程式存取,結果如下。

可以發現和前面的結果是一樣的,不同的是錯誤返回的內容不一樣。

上面提到的都是一些自建的場景,其實對雲負載均衡的結合使用也是 OK 的。

總結

雙向認證,在一些安全要求比較高的場景下,用途還是比較大的,相比較單向認證的話會麻煩一些。

本文範例程式碼:https://github.com/catcherwong-archive/2023/tree/main/MutualTLSAuthentication

參考資料