在三方介面對接中,偶爾會遇到需要傳遞證書的情況,這種方式其實是在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.crt
、server.p12
、server.crt
、server.key
、client.p12
和 client2.p12
。
下面先來看看 ASP.NET Core 直接對外的情況,也就是不依賴 nginx 或 IIS 的情況。
基於 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 的吧。
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_client 和 ssl_client_certificate !
一個是設定開啟使用者端證書的認證,一個是驗證的使用者端證書的關鍵。
這裡的 ssl_client_certificate 用了根證書,為的是可以驗證多個使用者端證書,當然這裡也可以用使用者端證書。
把 webapi 和 nginx 都執行起來。
這個時候存取,就會提示, No required SSL certificate was sent。
用上面的控制檯程式,再存取看看。
正確的證書,可以正常返回,錯誤的證書會返回 400 The SSL certificate error。
基於反向代理的話,操作起來就簡單了一點。
如果是雲負載均衡,只需要按他們的要求上傳對應的證書即可。
講了 nginx,不講講 IIS,好像有點說不過去。
那就再看看 IIS 的設定吧。
在 Windows 伺服器安裝好 IIS 和託管捆綁包後,要先把我們的根證書安裝到可信的根證書裡面。
然後進行部署,繫結好伺服器端證書後,確認可以正常存取。
然後進行雙向認證的設定。
在對應站點上面的 SSL 設定
,把 要求 SSL
和 必需
兩個勾上即可。
後面再存取的時候,就會提示選擇證書
選擇正確的證書後就可以正常存取了。
然後我們再用前面的控制檯程式存取,結果如下。
可以發現和前面的結果是一樣的,不同的是錯誤返回的內容不一樣。
上面提到的都是一些自建的場景,其實對雲負載均衡的結合使用也是 OK 的。
雙向認證,在一些安全要求比較高的場景下,用途還是比較大的,相比較單向認證的話會麻煩一些。
本文範例程式碼:https://github.com/catcherwong-archive/2023/tree/main/MutualTLSAuthentication