Microsoft.AspNetCore.Authentication.Certificate 包含類似 ASP.NET Core 憑證驗證的實作。 憑證認證是在 TLS 層級完成,之後才會進入 ASP.NET Core。 更準確地說,這個功能是一個認證處理器,會驗證憑證,然後給你一個事件,讓你可以將該憑證解析成 ClaimsPrincipal。
你必須配置你的伺服器以便使用 IIS、Kestrel、Azure Web Apps 或你所偏好的解決方案來進行憑證驗證。
本文說明如何在 ASP.NET Core 中,為 IIS 和 HTTP.sys 配置憑證驗證,並提供呼叫各種方法和屬性操作的範例。
檢視代理與負載平衡器的情境
憑證驗證是一種有狀態的情境,主要用於代理伺服器或負載平衡器不處理客戶端與伺服器之間流量的場合。 如果使用了代理或負載平衡器,則只有在符合特定條件的情況下,憑證驗證才會有效。
- 處理驗證。
- 將使用者驗證資訊傳遞至應用程式 (例如,在要求標頭中),以處理驗證資訊。
在使用 Proxy 和負載平衡器的環境中,憑證驗證的替代方案是 Active Directory 同盟服務 (ADFS) 搭配 OpenID Connect (OIDC)。
開始
取得 HTTPS 憑證、套用憑證,並設定伺服器需要憑證。
在 Web 應用程式中:
新增對Microsoft.AspNetCore.Authentication.Certificate NuGet 套件的參考。
在 Program.cs 檔案中呼叫該
builder.Services.AddAuthentication(CertificateAuthenticationDefaults.AuthenticationScheme).AddCertificate(...);方法。 提供一個代表,讓OnCertificateValidated事件處理者完成隨請求發送的客戶端憑證的任何補充驗證。 把這些資訊轉換成ClaimsPrincipal數值,並將其設定到context.Principal屬性上。
若認證失敗,該處理器會回傳 403 (Forbidden) 回應,而非你預期的回應 401 (Unauthorized)。 處理器回傳不同的回應,因為它預期認證會在初始 TLS 連線期間發生。 到達事件處理器時,已經來不及了。 無法將連線從匿名連線升級為具有憑證的連線。
UseAuthentication此方法必須設定HttpContext.User為ClaimsPrincipal由憑證產生的值。 例如:
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddAuthentication(
CertificateAuthenticationDefaults.AuthenticationScheme)
.AddCertificate();
var app = builder.Build();
app.UseAuthentication();
app.MapGet("/", () => "Hello World!");
app.Run();
上述範例示範新增憑證驗證的預設方式。 處理常式透過使用一般的憑證屬性來建構使用者主體。
設定憑證驗證
處理常式 CertificateAuthenticationOptions 有一些內建驗證,這是您應該在憑證上執行的最小驗證。 每個設定預設都會啟用。 以下章節說明如何操作這些設定。
AllowedCertificateTypes = Chained, SelfSigned, 或 All (Chained 或 SelfSigned)
預設值:CertificateTypes.Chained
這項檢查會驗證只允許適當的憑證類型。 如果應用程式使用自我簽署憑證,則必須將此選項設定為 CertificateTypes.All 或 CertificateTypes.SelfSigned。
鏈式信任驗證模式
用戶端所提供的憑證必須鏈結至受信任的根憑證。 此檢查可控制哪些信任存放區包含這些根憑證。
根據預設,處理常式會使用系統信任存放區。 如果所呈現的客戶端憑證需要鏈接到系統信任存儲中不存在的根憑證,你可以將選項設置為X509ChainTrustMode.CustomRootTrust,以便處理器使用指定的CustomTrustStore屬性。
CustomTrustStore
預設值:空白 X509Certificate2Collection
若處理常式的屬性 ChainTrustValidationMode 設為 X509ChainTrustMode.CustomRootTrust,此 X509Certificate2Collection 物件包含所有用於驗證用戶端憑證的憑證,這些憑證一直到並包括受信任的根憑證。
當客戶端呈現屬於多層次憑證鏈的憑證時,該 CustomTrustStore 屬性必須包含鏈中所有發出的憑證。
驗證證書使用
預設值:true
這項檢查會驗證用戶端提供的憑證是否具有用戶端驗證延伸金鑰使用 (EKU),或完全沒有 EKU。 如規格所述,如果未指定 EKU,則所有 EKU 都會被視為有效。
驗證有效期
預設值:true
這項檢查會驗證憑證是否在其有效期間內。 每次請求時,處理程序會確保在提交時有效的憑證在當前會話期間未過期。
撤銷標記
預設值:X509RevocationFlag.ExcludeRoot
指定用來檢查鏈結中哪個憑證是否被撤銷的旗標。
只有在憑證鏈結至根憑證時,才會執行撤銷檢查。
撤銷模式
預設值:X509RevocationMode.Online
旗標,指定撤銷檢查的執行方式。
指定進行線上檢查時,可能會導致長時間的延遲,因為需要聯絡憑證授權機構。
撤銷檢查僅在憑證與根憑證鏈結時執行。
常見問題:我可以設定我的應用程式只在特定路徑上要求憑證嗎?
這種做法不可能。 憑證交換會在 HTTPS 對話開始時完成。 這個操作是在該連線收到第一個請求之前由伺服器執行的,因此無法根據任何請求欄位來進行範圍設定。
程序處理事件
事件處理程式有兩個事件:
OnAuthenticationFailed:如果在驗證期間發生例外狀況,則會進行呼叫,並可讓您做出反應。OnCertificateValidated:在憑證驗證通過、通過驗證並建立預設主體後呼叫。 此事件可讓您執行自己的驗證,並擴展或取代主體。 範例包括:判斷您的服務是否識別該憑證。
自行構建本體,例如以下範例:
builder.Services.AddAuthentication( CertificateAuthenticationDefaults.AuthenticationScheme) .AddCertificate(options => { options.Events = new CertificateAuthenticationEvents { OnCertificateValidated = context => { var claims = new[] { new Claim( ClaimTypes.NameIdentifier, context.ClientCertificate.Subject, ClaimValueTypes.String, context.Options.ClaimsIssuer), new Claim( ClaimTypes.Name, context.ClientCertificate.Subject, ClaimValueTypes.String, context.Options.ClaimsIssuer) }; context.Principal = new ClaimsPrincipal( new ClaimsIdentity(claims, context.Scheme.Name)); context.Success(); return Task.CompletedTask; } }; });
如果入站憑證無法滿足你的額外驗證,請打電話 context.Fail("failure reason") 告知失敗原因。
如需更佳的功能,請呼叫在相依性注入中註冊的服務,以連線到資料庫或其他類型的使用者存放區。 使用傳遞至代理的上下文來存取服務。 請考慮下列範例:
builder.Services.AddAuthentication(
CertificateAuthenticationDefaults.AuthenticationScheme)
.AddCertificate(options =>
{
options.Events = new CertificateAuthenticationEvents
{
OnCertificateValidated = context =>
{
var validationService = context.HttpContext.RequestServices
.GetRequiredService<ICertificateValidationService>();
if (validationService.ValidateCertificate(context.ClientCertificate))
{
var claims = new[]
{
new Claim(
ClaimTypes.NameIdentifier,
context.ClientCertificate.Subject,
ClaimValueTypes.String, context.Options.ClaimsIssuer),
new Claim(
ClaimTypes.Name,
context.ClientCertificate.Subject,
ClaimValueTypes.String, context.Options.ClaimsIssuer)
};
context.Principal = new ClaimsPrincipal(
new ClaimsIdentity(claims, context.Scheme.Name));
context.Success();
}
return Task.CompletedTask;
}
};
});
就概念而言,憑證的驗證是授權問題。 例如,你可以在授權政策中新增對發行者或指紋的檢查,而不是在 OnCertificateValidated 處理程序內部。
將伺服器設定為需要憑證
以下章節說明如何配置伺服器,要求特定解決方案的憑證,包括 Kestrel、IIS、Azure、自訂網頁代理及 Azure Web Apps。
Kestrel
在 Program.cs 檔案中,請配置 Kestrel 如下:
var builder = WebApplication.CreateBuilder(args);
builder.Services.Configure<KestrelServerOptions>(options =>
{
options.ConfigureHttpsDefaults(options =>
options.ClientCertificateMode = ClientCertificateMode.RequireCertificate);
});
注意
當端點是在呼叫方法Listen先呼叫ConfigureHttpsDefaults該方法建立時,該端點並未套用預設值。
IIS
在 IIS 管理員中完成下列步驟:
- 在 連結 標籤中,選擇你的網站。
- 在 功能檢視 視窗中,雙擊 SSL 設定。
- 選擇 「要求 SSL 」勾選框。
- 在客戶 憑證 選項中,選擇 「必需」。
Azure 和自訂 Web Proxy
欲了解更多如何配置憑證轉發中介軟體的資訊,請參閱 主機與部署文件。
在 Azure Web Apps 中的憑證認證
Azure 不需要轉送設定。 憑證轉發中介軟體負責設定設定。
注意
此案例需要憑證轉送中介軟體。
更多資訊請參見 在你的應用程式程式碼中使用 TLS/SSL 憑證(Azure文件)。
自訂網頁代理中的憑證認證
AddCertificateForwarding 方法可用來指定:
- 用戶端標頭名稱。
- 如何透過此屬性載入憑證
HeaderConverter。
在自訂 Web Proxy 中,憑證會以自訂要求標頭的形式傳遞,例如 X-SSL-CERT。 要使用該憑證,請在 Program.cs 檔案中設定憑證轉發:
builder.Services.AddCertificateForwarding(options =>
{
options.CertificateHeader = "X-SSL-CERT";
options.HeaderConverter = headerValue =>
{
X509Certificate2? clientCertificate = null;
if (!string.IsNullOrWhiteSpace(headerValue))
{
clientCertificate = new X509Certificate2(StringToByteArray(headerValue));
}
return clientCertificate!;
static byte[] StringToByteArray(string hex)
{
var numberChars = hex.Length;
var bytes = new byte[numberChars / 2];
for (int i = 0; i < numberChars; i += 2)
{
bytes[i / 2] = Convert.ToByte(hex.Substring(i, 2), 16);
}
return bytes;
}
};
});
如果 NGINX 搭配反向代理的設定 proxy_set_header ssl-client-cert $ssl_client_escaped_cert ,或應用程式透過 NGINX Ingress 部署在 Kubernetes 上,客戶端憑證會以 URL 編碼的形式傳遞給應用程式。 若要使用憑證,請將它解碼如下:
builder.Services.AddCertificateForwarding(options =>
{
options.CertificateHeader = "ssl-client-cert";
options.HeaderConverter = (headerValue) =>
{
X509Certificate2? clientCertificate = null;
if (!string.IsNullOrWhiteSpace(headerValue))
{
clientCertificate = X509Certificate2.CreateFromPem(
WebUtility.UrlDecode(headerValue));
}
return clientCertificate!;
};
});
在 Program.cs 檔案裡加入中介軟體。
UseCertificateForwarding該方法在呼叫 UseAuthentication 和 UseAuthorization 方法之前被呼叫:
var app = builder.Build();
app.UseCertificateForwarding();
app.UseAuthentication();
app.UseAuthorization();
不同的類別可用來實作驗證邏輯。 因為在此範例中使用相同的自我簽署憑證,因此請確認只能使用您的憑證。 確認客戶端憑證與伺服器憑證的指紋是否相符。 否則,任何憑證都可以使用,且足以進行認證。 證書隨後會被用於 AddCertificate 方法內。 如果你使用中間證書或子證書,也可以在這裡驗證主體或發行人。
using System.Security.Cryptography.X509Certificates;
namespace CertAuthSample.Snippets;
public class SampleCertificateValidationService : ICertificateValidationService
{
public bool ValidateCertificate(X509Certificate2 clientCertificate)
{
// Don't hardcode passwords in production code.
// Use a certificate thumbprint or Azure Key Vault.
var expectedCertificate = new X509Certificate2(
Path.Combine("/path/to/pfx"), "1234");
return clientCertificate.Thumbprint == expectedCertificate.Thumbprint;
}
}
實作一個帶有憑證和 IHttpClientFactory 的 HttpClient
以下範例中,客戶端憑證是利用處理程序的HttpClientHandler屬性加入至ClientCertificates。 此處理器可以用於名為 HttpClient 的實例中,並搭配 ConfigurePrimaryHttpMessageHandler 方法。 此情境在 Program.cs 檔案中設定:
var clientCertificate =
new X509Certificate2(
Path.Combine(_environment.ContentRootPath, "sts_dev_cert.pfx"), "1234");
builder.Services.AddHttpClient("namedClient", c =>
{
}).ConfigurePrimaryHttpMessageHandler(() =>
{
var handler = new HttpClientHandler();
handler.ClientCertificates.Add(clientCertificate);
return handler;
});
然後,您可以使用 IHttpClientFactory 來獲取具有處理常式和憑證的具名實例。
CreateClient使用Program.cs檔案中定義的客戶端名稱的方法來取得實例。 HTTP 請求可依需求使用用戶端發送:
public class SampleHttpService
{
private readonly IHttpClientFactory _httpClientFactory;
public SampleHttpService(IHttpClientFactory httpClientFactory)
=> _httpClientFactory = httpClientFactory;
public async Task<JsonDocument> GetAsync()
{
var httpClient = _httpClientFactory.CreateClient("namedClient");
var httpResponseMessage = await httpClient.GetAsync("https://example.com");
if (httpResponseMessage.IsSuccessStatusCode)
{
return JsonDocument.Parse(
await httpResponseMessage.Content.ReadAsStringAsync());
}
throw new ApplicationException($"Status code: {httpResponseMessage.StatusCode}");
}
}
如果正確的憑證傳送至伺服器,則會傳回資料。 若未收到憑證或錯誤憑證,伺服器會回傳 HTTP 403 狀態碼。
PowerShell 中的憑證
建立憑證是設定此流程中最困難的部分。 你可以用 New-SelfSignedCertificate PowerShell cmdlet 建立根憑證。 建立憑證時,請使用強密碼。 重要的是要像範例中所示,加入 KeyUsageProperty 參數 和 KeyUsage 參數。
建立根憑證授權中心
以下程式碼說明如何在根建立憑證授權單位(CA):
New-SelfSignedCertificate -DnsName "root_ca_dev_damienbod.com", "root_ca_dev_damienbod.com" -CertStoreLocation "cert:\LocalMachine\My" -NotAfter (Get-Date).AddYears(20) -FriendlyName "root_ca_dev_damienbod.com" -KeyUsageProperty All -KeyUsage CertSign, CRLSign, DigitalSignature
$mypwd = ConvertTo-SecureString -String "1234" -Force -AsPlainText
Get-ChildItem -Path cert:\localMachine\my\"The thumbprint..." | Export-PfxCertificate -FilePath C:\git\root_ca_dev_damienbod.pfx -Password $mypwd
Export-Certificate -Cert cert:\localMachine\my\"The thumbprint..." -FilePath root_ca_dev_damienbod.crt
注意
-DnsName 參數值必須符合應用程式的部署目標。 例如,用於開發的「localhost」。
在受信任的根目錄中安裝
根憑證必須在您的主機系統受信任。 預設情況下,僅信任憑證授權單位建立的根憑證。 關於如何信任 Windows 根憑證的資訊,請參考 Windows documentation 或 Import-CertificatePowerShell 指令碼。
使用中級證書
中繼憑證現在可以從根憑證建立。 這種方法並非所有使用案例都必須,但你可能需要建立許多憑證,或啟用或停用憑證群組。 需要 TextExtension 參數,才能在憑證的基本條件約束中設定路徑長度。
然後可以將中繼憑證新增到 Windows 主機系統中的受信任中繼憑證。
$mypwd = ConvertTo-SecureString -String "1234" -Force -AsPlainText
$parentcert = ( Get-ChildItem -Path cert:\LocalMachine\My\"The thumbprint of the root..." )
New-SelfSignedCertificate -certstorelocation cert:\localmachine\my -dnsname "intermediate_dev_damienbod.com" -Signer $parentcert -NotAfter (Get-Date).AddYears(20) -FriendlyName "intermediate_dev_damienbod.com" -KeyUsageProperty All -KeyUsage CertSign, CRLSign, DigitalSignature -TextExtension @("2.5.29.19={text}CA=1&pathlength=1")
Get-ChildItem -Path cert:\localMachine\my\"The thumbprint..." | Export-PfxCertificate -FilePath C:\git\AspNetCoreCertificateAuth\Certs\intermediate_dev_damienbod.pfx -Password $mypwd
Export-Certificate -Cert cert:\localMachine\my\"The thumbprint..." -FilePath intermediate_dev_damienbod.crt
從中繼憑證建立子憑證
子憑證可以從中繼憑證建立。 這份子證明是最終實體。 你不需要再建立更多子憑證。
$parentcert = ( Get-ChildItem -Path cert:\LocalMachine\My\"The thumbprint from the Intermediate certificate..." )
New-SelfSignedCertificate -certstorelocation cert:\localmachine\my -dnsname "child_a_dev_damienbod.com" -Signer $parentcert -NotAfter (Get-Date).AddYears(20) -FriendlyName "child_a_dev_damienbod.com"
$mypwd = ConvertTo-SecureString -String "1234" -Force -AsPlainText
Get-ChildItem -Path cert:\localMachine\my\"The thumbprint..." | Export-PfxCertificate -FilePath C:\git\AspNetCoreCertificateAuth\Certs\child_a_dev_damienbod.pfx -Password $mypwd
Export-Certificate -Cert cert:\localMachine\my\"The thumbprint..." -FilePath child_a_dev_damienbod.crt
從根憑證建立子憑證
您也可以直接從根憑證建立子憑證。
$rootcert = ( Get-ChildItem -Path cert:\LocalMachine\My\"The thumbprint from the root cert..." )
New-SelfSignedCertificate -certstorelocation cert:\localmachine\my -dnsname "child_a_dev_damienbod.com" -Signer $rootcert -NotAfter (Get-Date).AddYears(20) -FriendlyName "child_a_dev_damienbod.com"
$mypwd = ConvertTo-SecureString -String "1234" -Force -AsPlainText
Get-ChildItem -Path cert:\localMachine\my\"The thumbprint..." | Export-PfxCertificate -FilePath C:\git\AspNetCoreCertificateAuth\Certs\child_a_dev_damienbod.pfx -Password $mypwd
Export-Certificate -Cert cert:\localMachine\my\"The thumbprint..." -FilePath child_a_dev_damienbod.crt
範例:根證書 - 中介證書 - 證書
以下範例展示了根憑證、中介憑證及子憑證的設定:
$mypwdroot = ConvertTo-SecureString -String "1234" -Force -AsPlainText
$mypwd = ConvertTo-SecureString -String "1234" -Force -AsPlainText
New-SelfSignedCertificate -DnsName "root_ca_dev_damienbod.com", "root_ca_dev_damienbod.com" -CertStoreLocation "cert:\LocalMachine\My" -NotAfter (Get-Date).AddYears(20) -FriendlyName "root_ca_dev_damienbod.com" -KeyUsageProperty All -KeyUsage CertSign, CRLSign, DigitalSignature
Get-ChildItem -Path cert:\localMachine\my\0C89639E4E2998A93E423F919B36D4009A0F9991 | Export-PfxCertificate -FilePath C:\git\root_ca_dev_damienbod.pfx -Password $mypwdroot
Export-Certificate -Cert cert:\localMachine\my\0C89639E4E2998A93E423F919B36D4009A0F9991 -FilePath root_ca_dev_damienbod.crt
$rootcert = ( Get-ChildItem -Path cert:\LocalMachine\My\0C89639E4E2998A93E423F919B36D4009A0F9991 )
New-SelfSignedCertificate -certstorelocation cert:\localmachine\my -dnsname "child_a_dev_damienbod.com" -Signer $rootcert -NotAfter (Get-Date).AddYears(20) -FriendlyName "child_a_dev_damienbod.com" -KeyUsageProperty All -KeyUsage CertSign, CRLSign, DigitalSignature -TextExtension @("2.5.29.19={text}CA=1&pathlength=1")
Get-ChildItem -Path cert:\localMachine\my\BA9BF91ED35538A01375EFC212A2F46104B33A44 | Export-PfxCertificate -FilePath C:\git\AspNetCoreCertificateAuth\Certs\child_a_dev_damienbod.pfx -Password $mypwd
Export-Certificate -Cert cert:\localMachine\my\BA9BF91ED35538A01375EFC212A2F46104B33A44 -FilePath child_a_dev_damienbod.crt
$parentcert = ( Get-ChildItem -Path cert:\LocalMachine\My\BA9BF91ED35538A01375EFC212A2F46104B33A44 )
New-SelfSignedCertificate -certstorelocation cert:\localmachine\my -dnsname "child_b_from_a_dev_damienbod.com" -Signer $parentcert -NotAfter (Get-Date).AddYears(20) -FriendlyName "child_b_from_a_dev_damienbod.com"
Get-ChildItem -Path cert:\localMachine\my\141594A0AE38CBBECED7AF680F7945CD51D8F28A | Export-PfxCertificate -FilePath C:\git\AspNetCoreCertificateAuth\Certs\child_b_from_a_dev_damienbod.pfx -Password $mypwd
Export-Certificate -Cert cert:\localMachine\my\141594A0AE38CBBECED7AF680F7945CD51D8F28A -FilePath child_b_from_a_dev_damienbod.crt
當你使用根憑證、中間憑證或子憑證時,可以透過拇指列印或公開金鑰(PublicKey)來驗證這些憑證:
using System.Security.Cryptography.X509Certificates;
namespace CertAuthSample.Snippets;
public class SampleCertificateThumbprintsValidationService : ICertificateValidationService
{
private readonly string[] validThumbprints = new[]
{
"141594A0AE38CBBECED7AF680F7945CD51D8F28A",
"0C89639E4E2998A93E423F919B36D4009A0F9991",
"BA9BF91ED35538A01375EFC212A2F46104B33A44"
};
public bool ValidateCertificate(X509Certificate2 clientCertificate)
=> validThumbprints.Contains(clientCertificate.Thumbprint);
}
快取憑證驗證結果
.NET 5 及後續版本支援啟用驗證結果快取的功能。 快取大幅提升了憑證認證的效能,因為驗證是一項昂貴的操作。
根據預設,憑證驗證會停用快取。 要啟用快取,請呼叫 AddCertificateCache 檔案中的方法:
builder.Services.AddAuthentication(
CertificateAuthenticationDefaults.AuthenticationScheme)
.AddCertificate()
.AddCertificateCache(options =>
{
options.CacheSize = 1024;
options.CacheEntryExpiration = TimeSpan.FromMinutes(2);
});
預設快取實作會將結果儲存在記憶體中。 您可以實作 ICertificateValidationCache,並使用相依性插入將它註冊,以提供您自己的快取。 例如: services.AddSingleton<ICertificateValidationCache, YourCache>() 。
使用可選的用戶端憑證
本節提供應用程式的資訊,這些應用程式必須使用憑證來保護應用程式的子集。 例如,應用程式中的 Razor 頁面或控制器可能需要用戶端憑證。 此情境帶來一些挑戰:
- 用戶端憑證是 TLS 的功能,不是 HTTP 功能。
- 用戶端憑證是逐連線協商的,通常在連線開始時,在任何 HTTP 資料可用之前。
實作可選客戶端憑證有兩種方法:
- 選項一:使用獨立的主機名稱(SNI)並進行重定向。 雖然此選項配置較繁複,但因其適用於大多數環境與協定而被推薦。
- 選項二:在 HTTP 請求時重新協商。 這種做法有幾個限制,並不建議這麼做。
獨立主機(SNI)
在連線開始時,只會知道伺服器名稱指示 (SNI)†。 用戶端憑證可以依主機名稱設定,因此一台主機需要憑證,另一台則不需要。
為領域和子網域設定綁定。
例如,在
contoso.com和myClient.contoso.com上設定繫結。contoso.com主機不需要用戶端憑證,但myClient.contoso.com需要。如需詳細資訊,請參閱下列資源:
IIS
HTTP.sys
.NET 5 及之後版本則提供更方便的重定向支援,以取得可選的用戶端憑證。 如需詳細資訊,請參閱選擇性憑證範例。
對於需要客戶端憑證且沒有用戶端憑證的網頁應用程式請求,請使用客戶端憑證保護子網域重新導向到同一頁面。
例如,重新導向至
myClient.contoso.com/requestedPage。 由於請求發送到的主機名稱myClient.contoso.com/requestedPage與contoso.com/requestedPage不同,因此用戶端會建立一個不同的連線,並提供用戶端憑證。如需詳細資訊,請參閱 ASP.NET Core 中的授權簡介。
† 伺服器名稱指示(SNI)是一種 TLS 擴充功能,用於將虛擬網域納入 SSL 協商的一部分。 這種方法實際上意味著虛擬網域名稱或主機名稱可以用來識別網路端點。
TLS 重新談判
TLS 重新協商是一個用戶端與伺服器可以重新評估個別連線加密需求的過程,包括在先前未提供用戶端憑證的情況下請求該憑證。 TLS 重新交涉有安全性風險,不建議這麼做,因為:
- 在 HTTP/1.1 中,伺服器必須先緩衝或處理任何正在傳輸中的 HTTP 資料,例如 POST 請求體,以確保連線在重新協商時是清空的。 否則,重新談判可能會停止回應或失敗。
- HTTP/2 和 HTTP/3 明確禁止重新交涉。
- 重新交涉有安全性風險。 TLS 1.3 已移除整個連線的重新交涉,並將它取代為新的擴充功能,使得連線開始時只要求用戶端憑證。 此機制會透過相同的 API 公開,但仍受限於緩衝和 HTTP 通訊協定版本的先前條件約束。
此功能的實作與配置會依伺服器與框架版本而異,詳見以下章節。
IIS
IIS 會代表您管理用戶端憑證交涉。 應用程式的子區段可以啟用 SslRequireCert 選項來協商這些要求的用戶端憑證。 欲了解更多資訊,請參閱 IIS 文件中的配置。
IIS 會自動緩衝任何請求體資料,直到達到設定的大小限制後再進行重新協商。 超過限制的要求會遭到 413 回應的拒絕。 此限制預設為 48 KB,可透過設定 uploadReadAheadSize 屬性來設定。
HttpSys
HttpSys 有兩個設定來控制用戶端憑證協商,兩者都應該設定。 第一個是在 netsh.exe 檔案中的 http add sslcert clientcertnegotiation=enable/disable。 此旗標指示是否在連線開始時協商客戶端憑證。 將值設為 disable 以獲得可選的客戶端憑證。 更多資訊請參考 http add sslcertnetsh 文件中的參數使用說明。
另一個設定是屬性 ClientCertificateMethod 。 當設定為 AllowRenegotation 時,用戶端憑證可以在要求期間重新協商。
注意
應用程式應先緩衝或消耗任何請求體資料,然後再嘗試重新協商。 否則,請求可能會變得無法回應。
應用程式可以先檢查 ClientCertificate 屬性,以查看憑證是否可用。 如果無法使用,請確保請求實體已被讀取,再調用 GetClientCertificateAsync 方法進行協商。
GetClientCertificateAsync 如果客戶端拒絕提供,則可以回傳一個空憑證。
注意
ClientCertificate 性質的行為在 .NET 6 發生了變化。 更多資訊,請參見 GitHub 問題#466。
Kestrel
Kestrel 控制客戶證書與 ClientCertificateMode 屬性的協商。
.NET 6 及以後版本為 DelayCertificate 屬性提供了 ClientCertificateMode 選項。 當這個選項被設定時,應用程式可以檢查 ClientCertificate 屬性來查看證書是否可用。 如果無法使用,請確保請求主體已被處理,再調用 GetClientCertificateAsync 方法進行協商。
GetClientCertificateAsync 如果客戶端拒絕提供,則可以回傳一個空憑證。
注意
應用程式應先緩衝或消耗任何請求體資料,然後再嘗試重新協商。 否則, GetClientCertificateAsync 可能會拋出例外, InvalidOperationException:客戶端串流需要在重新協商前被排空。
如果你依 SNI 主機名稱程式化設定 TLS,呼叫 UseHttps 過載(.NET 6 或更新版本),該過載會接收 TlsHandshakeCallbackOptions 類別物件。 此選項透過 AllowDelayedClientCertificateNegotation 屬性控制客戶端憑證重新協商。 欲了解更多資訊,請參閱 ListenOptionsHttpsExtensions.UseHttps 方法。
Microsoft.AspNetCore.Authentication.Certificate 包含類似 ASP.NET Core 憑證驗證的實作。 憑證認證發生在 TLS 層級,遠早於 ASP.NET Core 之前。 更準確地說,這個認證處理器會驗證憑證,並給你一個事件,讓你可以將憑證解析成一個 ClaimsPrincipal 值。
設定您的伺服器進行憑證驗證,無論它是 IIS、Kestrel、Azure Web Apps 或您使用的任何其他工具。
Proxy 和負載平衡器案例
憑證驗證是一種具狀態情節,主要使用於 Proxy 或負載平衡器不會處理用戶端與伺服器之間流量的情況。 如果使用了 Proxy 或負載平衡器,只有當 Proxy 或負載平衡器符合特定條件時,憑證驗證才有效:
- 處理驗證。
- 將使用者驗證資訊傳遞至應用程式 (例如,在要求標頭中),以處理驗證資訊。
在使用 Proxy 和負載平衡器的環境中,憑證驗證的替代方案是 Active Directory 同盟服務 (ADFS) 搭配 OpenID Connect (OIDC)。
開始
取得 HTTPS 憑證、安裝憑證,並設定伺服器以要求憑證。
在您的 Web 應用程式中,新增對 Microsoft.AspNetCore.Authentication.Certificate 套件的參考。 然後在 Startup.ConfigureServices 方法中,使用您的選項呼叫 services.AddAuthentication(CertificateAuthenticationDefaults.AuthenticationScheme).AddCertificate(...);,提供委派給 OnCertificateValidated,以便對傳送要求的用戶端憑證執行任何補充驗證。 將該資訊轉換成 ClaimsPrincipal ,並在 context.Principal 屬性上加以設定。
如果驗證失敗,此處理程式會傳回 403 (Forbidden) 回應,而不是 401 (Unauthorized),正如您所預期。 原因是驗證應該在初始 TLS 連線期間發生。 到達達處理常式時,已經來不及了。 無法將連線從匿名連線升級為具有憑證的連線。
同時在 app.UseAuthentication(); 方法中新增 Startup.Configure。 否則,HttpContext.User 不會被設定為從憑證創建的 ClaimsPrincipal。 例如:
public void ConfigureServices(IServiceCollection services)
{
services.AddAuthentication(
CertificateAuthenticationDefaults.AuthenticationScheme)
.AddCertificate()
// Adding an ICertificateValidationCache results in certificate auth caching the results.
// The default implementation uses a memory cache.
.AddCertificateCache();
// All other service configuration
}
public void Configure(IApplicationBuilder app, IHostingEnvironment env)
{
app.UseAuthentication();
// All other app configuration
}
上述範例示範新增憑證驗證的預設方式。 處理常式會使用常用憑證屬性來建構使用者主體。
設定憑證驗證
處理常式 CertificateAuthenticationOptions 有一些內建驗證,這是您應該在憑證上執行的最小驗證。 每個設定預設都會啟用。
AllowedCertificateTypes = Chained, SelfSigned, 或 All (Chained 或 SelfSigned)
預設值:CertificateTypes.Chained
這項檢查會驗證只允許適當的憑證類型。 如果應用程式使用自我簽署憑證,則必須將此選項設定為 CertificateTypes.All 或 CertificateTypes.SelfSigned。
驗證證書使用
預設值:true
這項檢查會驗證用戶端提供的憑證是否具有用戶端驗證延伸金鑰使用 (EKU),或完全沒有 EKU。 如規格所述,如果未指定 EKU,則所有 EKU 都會被視為有效。
驗證有效期
預設值:true
這項檢查會驗證憑證是否在其有效期間內。 每次請求時,處理程序會確保在提交時有效的憑證在當前會話期間未過期。
撤銷標記
預設值:X509RevocationFlag.ExcludeRoot
指定用來檢查鏈結中哪個憑證是否被撤銷的旗標。
只有在憑證鏈結至根憑證時,才會執行撤銷檢查。
撤銷模式
預設值:X509RevocationMode.Online
旗標,指定撤銷檢查的執行方式。
指定進行線上檢查時,可能會導致長時間的延遲,因為需要聯絡憑證授權機構。
只有在憑證鏈結至根憑證時,才會執行撤銷檢查。
我可以將應用程式設定為只在特定路徑上要求憑證嗎?
這是不可能的。 請記住,憑證交換是在 HTTPS 交談開始時完成的,伺服器會在該連線上收到第一個要求之前完成,因此無法根據任何要求欄位來設定範圍。
事件處理程序
事件處理程式有兩個事件:
-
OnAuthenticationFailed:如果在驗證期間發生例外狀況,則會進行呼叫,並可讓您做出反應。 -
OnCertificateValidated:在憑證驗證完成、通過驗證並建立預設主體後被呼叫。 此事件可讓您執行自己的驗證,並擴展或取代主體。 如需範例,包括:判斷您的服務是否識別該憑證。
建構您自己的原則。 請考慮
Startup.ConfigureServices中的下列範例:services.AddAuthentication( CertificateAuthenticationDefaults.AuthenticationScheme) .AddCertificate(options => { options.Events = new CertificateAuthenticationEvents { OnCertificateValidated = context => { var claims = new[] { new Claim( ClaimTypes.NameIdentifier, context.ClientCertificate.Subject, ClaimValueTypes.String, context.Options.ClaimsIssuer), new Claim(ClaimTypes.Name, context.ClientCertificate.Subject, ClaimValueTypes.String, context.Options.ClaimsIssuer) }; context.Principal = new ClaimsPrincipal( new ClaimsIdentity(claims, context.Scheme.Name)); context.Success(); return Task.CompletedTask; } }; });
如果您發現輸入憑證不符合額外的驗證,請呼叫 context.Fail("failure reason") 且附有失敗原因。
針對實際功能,您可能想要呼叫已在相依性注入中註冊的服務,以連接到資料庫或其他類型的使用者資料存儲。 使用傳遞至您的委派的上下文來存取服務。 請考慮 Startup.ConfigureServices 中的下列範例:
services.AddAuthentication(
CertificateAuthenticationDefaults.AuthenticationScheme)
.AddCertificate(options =>
{
options.Events = new CertificateAuthenticationEvents
{
OnCertificateValidated = context =>
{
var validationService =
context.HttpContext.RequestServices
.GetRequiredService<ICertificateValidationService>();
if (validationService.ValidateCertificate(
context.ClientCertificate))
{
var claims = new[]
{
new Claim(
ClaimTypes.NameIdentifier,
context.ClientCertificate.Subject,
ClaimValueTypes.String,
context.Options.ClaimsIssuer),
new Claim(
ClaimTypes.Name,
context.ClientCertificate.Subject,
ClaimValueTypes.String,
context.Options.ClaimsIssuer)
};
context.Principal = new ClaimsPrincipal(
new ClaimsIdentity(claims, context.Scheme.Name));
context.Success();
}
return Task.CompletedTask;
}
};
});
就概念而言,憑證的驗證是授權問題。 例如,在授權原則中加入對簽發者或指紋的檢查,而非在 OnCertificateValidated 內,是完全可以接受的。
將伺服器設定為需要憑證
Kestrel
在 Program.cs 中設定 Kestrel ,如下所示:
public static void Main(string[] args)
{
CreateHostBuilder(args).Build().Run();
}
public static IHostBuilder CreateHostBuilder(string[] args)
{
return Host.CreateDefaultBuilder(args)
.ConfigureWebHostDefaults(webBuilder =>
{
webBuilder.UseStartup<Startup>();
webBuilder.ConfigureKestrel(o =>
{
o.ConfigureHttpsDefaults(o =>
o.ClientCertificateMode = ClientCertificateMode.RequireCertificate);
});
});
}
注意
透過呼叫 Listen之前呼叫 ConfigureHttpsDefaults 所建立的端點,不會套用預設值。
IIS
在 IIS 管理員中完成下列步驟:
- 從[連線]索引標籤中選取您的網站。
- 按兩下[功能檢視]視窗中的[SSL 設定]選項。
- 勾選[需要 SSL]核取方塊,然後選取[用戶端憑證]區段中的[需要]選項按鈕。
Azure 和自訂 Web Proxy
如需如何設定憑證轉送中介軟體,請參閱主機和部署檔。
在 Azure Web Apps 中使用憑證驗證
Azure 不需要轉送設定。 轉送設定是由憑證轉送中介軟體所設定。
注意
此案例需要憑證轉送中介軟體。
如需詳細資訊,請參閱在 Azure App 服務 的程式碼中使用 TLS/SSL 憑證 (Azure 檔)。
在自訂 Web Proxy 中使用憑證驗證
AddCertificateForwarding 方法可用來指定:
- 用戶端標頭名稱。
- 如何載入憑證 (使用
HeaderConverter屬性)。
在自訂 Web Proxy 中,憑證會以自訂要求標頭的形式傳遞,例如 X-SSL-CERT。 若要使用它,請在 Startup.ConfigureServices 中設定憑證轉送:
public void ConfigureServices(IServiceCollection services)
{
services.AddCertificateForwarding(options =>
{
options.CertificateHeader = "X-SSL-CERT";
options.HeaderConverter = (headerValue) =>
{
X509Certificate2 clientCertificate = null;
if(!string.IsNullOrWhiteSpace(headerValue))
{
byte[] bytes = StringToByteArray(headerValue);
clientCertificate = new X509Certificate2(bytes);
}
return clientCertificate;
};
});
}
private static byte[] StringToByteArray(string hex)
{
int NumberChars = hex.Length;
byte[] bytes = new byte[NumberChars / 2];
for (int i = 0; i < NumberChars; i += 2)
{
bytes[i / 2] = Convert.ToByte(hex.Substring(i, 2), 16);
}
return bytes;
}
如果應用程式是由 NGINX 使用設定 proxy_set_header ssl-client-cert $ssl_client_escaped_cert 進行反向代理,或是部署在 Kubernetes 並通過 NGINX Ingress 控制器,則用戶端憑證會以 URL 編碼格式傳遞至應用程式。 若要使用憑證,請將它解碼如下:
在 Startup.ConfigureServices (Startup.cs) 中:
services.AddCertificateForwarding(options =>
{
options.CertificateHeader = "ssl-client-cert";
options.HeaderConverter = (headerValue) =>
{
X509Certificate2 clientCertificate = null;
if (!string.IsNullOrWhiteSpace(headerValue))
{
string certPem = WebUtility.UrlDecode(headerValue);
clientCertificate = X509Certificate2.CreateFromPem(certPem);
}
return clientCertificate;
};
});
Startup.Configure 方法接著會新增中介軟體。 呼叫 UseCertificateForwarding 會在呼叫 UseAuthentication 和 UseAuthorization 之前進行。
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
...
app.UseRouting();
app.UseCertificateForwarding();
app.UseAuthentication();
app.UseAuthorization();
app.UseEndpoints(endpoints =>
{
endpoints.MapControllers();
});
}
不同的類別可用來實作驗證邏輯。 因為在此範例中使用相同的自我簽署憑證,因此請確認只能使用您的憑證。 驗證用戶端憑證和伺服器憑證的指紋是否相符,否則可以使用任何憑證,而且足以進行驗證。 這會在 AddCertificate 方法內使用。 如果您使用中繼或子憑證,您也可以在這裡驗證主體或簽發者。
using System.IO;
using System.Security.Cryptography.X509Certificates;
namespace AspNetCoreCertificateAuthApi
{
public class MyCertificateValidationService
{
public bool ValidateCertificate(X509Certificate2 clientCertificate)
{
// Do not hardcode passwords in production code
// Use thumbprint or key vault
var cert = new X509Certificate2(
Path.Combine("sts_dev_cert.pfx"), "1234");
if (clientCertificate.Thumbprint == cert.Thumbprint)
{
return true;
}
return false;
}
}
}
使用安全憑證和 HttpClientHandler 實作 HttpClient
HttpClientHandler 可以直接新增到 HttpClient 類別的建構函式中。 建立HttpClient實例時需要小心。 接著,HttpClient 會隨每個請求發送憑證。
private async Task<JsonDocument> GetApiDataUsingHttpClientHandler()
{
var cert = new X509Certificate2(Path.Combine(_environment.ContentRootPath, "sts_dev_cert.pfx"), "1234");
var handler = new HttpClientHandler();
handler.ClientCertificates.Add(cert);
var client = new HttpClient(handler);
var request = new HttpRequestMessage()
{
RequestUri = new Uri("https://localhost:44379/api/values"),
Method = HttpMethod.Get,
};
var response = await client.SendAsync(request);
if (response.IsSuccessStatusCode)
{
var responseContent = await response.Content.ReadAsStringAsync();
var data = JsonDocument.Parse(responseContent);
return data;
}
throw new ApplicationException($"Status code: {response.StatusCode}, Error: {response.ReasonPhrase}");
}
實作一個 HttpClient,使用憑證和來自 IHttpClientFactory 的具名 HttpClient
在下列範例中,會將用戶端憑證透過處理常式的 ClientCertificates 屬性新增至 HttpClientHandler。 接著,這個處理常式可以使用 HttpClient 方法,在 ConfigurePrimaryHttpMessageHandler 的具名實例中使用。 這是在 Startup.ConfigureServices 中設定:
var clientCertificate =
new X509Certificate2(
Path.Combine(_environment.ContentRootPath, "sts_dev_cert.pfx"), "1234");
services.AddHttpClient("namedClient", c =>
{
}).ConfigurePrimaryHttpMessageHandler(() =>
{
var handler = new HttpClientHandler();
handler.ClientCertificates.Add(clientCertificate);
return handler;
});
接著,您可以使用 IHttpClientFactory 來取得具有指定處理常式和憑證的實例。 在 CreateClient 類別中定義的用戶端名稱的 Startup 方法會用來取得實例。 您可以視需要使用用戶端傳送 HTTP 要求。
private readonly IHttpClientFactory _clientFactory;
public ApiService(IHttpClientFactory clientFactory)
{
_clientFactory = clientFactory;
}
private async Task<JsonDocument> GetApiDataWithNamedClient()
{
var client = _clientFactory.CreateClient("namedClient");
var request = new HttpRequestMessage()
{
RequestUri = new Uri("https://localhost:44379/api/values"),
Method = HttpMethod.Get,
};
var response = await client.SendAsync(request);
if (response.IsSuccessStatusCode)
{
var responseContent = await response.Content.ReadAsStringAsync();
var data = JsonDocument.Parse(responseContent);
return data;
}
throw new ApplicationException($"Status code: {response.StatusCode}, Error: {response.ReasonPhrase}");
}
如果正確的憑證傳送至伺服器,則會傳回資料。 如果沒有傳送憑證或錯誤的憑證,則會傳回 HTTP 403 狀態碼。
在 PowerShell 中建立憑證
建立憑證是設定此流程中最困難的部分。 您可以使用 New-SelfSignedCertificate PowerShell Cmdlet 來建立根憑證。 建立憑證時,請使用強式密碼。 請務必新增 KeyUsageProperty 參數和 KeyUsage 參數,如下所示。
建立根 CA
New-SelfSignedCertificate -DnsName "root_ca_dev_damienbod.com", "root_ca_dev_damienbod.com" -CertStoreLocation "cert:\LocalMachine\My" -NotAfter (Get-Date).AddYears(20) -FriendlyName "root_ca_dev_damienbod.com" -KeyUsageProperty All -KeyUsage CertSign, CRLSign, DigitalSignature
$mypwd = ConvertTo-SecureString -String "1234" -Force -AsPlainText
Get-ChildItem -Path cert:\localMachine\my\"The thumbprint..." | Export-PfxCertificate -FilePath C:\git\root_ca_dev_damienbod.pfx -Password $mypwd
Export-Certificate -Cert cert:\localMachine\my\"The thumbprint..." -FilePath root_ca_dev_damienbod.crt
注意
-DnsName 參數值必須符合應用程式的部署目標。 例如,用於開發的「localhost」。
在受信任的根目錄中安裝
在您的主機系統上,根憑證必須被信任。 憑證授權單位單位未建立的根憑證預設不會受到信任。 如需如何在 Windows 上信任根憑證的資訊,請參閱此問題。
中繼憑證
中繼憑證現在可以從根憑證建立。 並非所有使用案例都需要此項目,但您可能需要建立許多憑證,或需要啟動或停用憑證群組。 需要 TextExtension 參數,才能在憑證的基本條件約束中設定路徑長度。
然後,可以將中繼憑證新增至 Windows 主機系統中的受信任中繼憑證。
$mypwd = ConvertTo-SecureString -String "1234" -Force -AsPlainText
$parentcert = ( Get-ChildItem -Path cert:\LocalMachine\My\"The thumbprint of the root..." )
New-SelfSignedCertificate -certstorelocation cert:\localmachine\my -dnsname "intermediate_dev_damienbod.com" -Signer $parentcert -NotAfter (Get-Date).AddYears(20) -FriendlyName "intermediate_dev_damienbod.com" -KeyUsageProperty All -KeyUsage CertSign, CRLSign, DigitalSignature -TextExtension @("2.5.29.19={text}CA=1&pathlength=1")
Get-ChildItem -Path cert:\localMachine\my\"The thumbprint..." | Export-PfxCertificate -FilePath C:\git\AspNetCoreCertificateAuth\Certs\intermediate_dev_damienbod.pfx -Password $mypwd
Export-Certificate -Cert cert:\localMachine\my\"The thumbprint..." -FilePath intermediate_dev_damienbod.crt
從中繼憑證建立子憑證
子憑證可以從中繼憑證建立。 這是終端實體,不需要建立更多子憑證。
$parentcert = ( Get-ChildItem -Path cert:\LocalMachine\My\"The thumbprint from the Intermediate certificate..." )
New-SelfSignedCertificate -certstorelocation cert:\localmachine\my -dnsname "child_a_dev_damienbod.com" -Signer $parentcert -NotAfter (Get-Date).AddYears(20) -FriendlyName "child_a_dev_damienbod.com"
$mypwd = ConvertTo-SecureString -String "1234" -Force -AsPlainText
Get-ChildItem -Path cert:\localMachine\my\"The thumbprint..." | Export-PfxCertificate -FilePath C:\git\AspNetCoreCertificateAuth\Certs\child_a_dev_damienbod.pfx -Password $mypwd
Export-Certificate -Cert cert:\localMachine\my\"The thumbprint..." -FilePath child_a_dev_damienbod.crt
從根憑證建立子憑證
您也可以直接從根憑證建立子憑證。
$rootcert = ( Get-ChildItem -Path cert:\LocalMachine\My\"The thumbprint from the root cert..." )
New-SelfSignedCertificate -certstorelocation cert:\localmachine\my -dnsname "child_a_dev_damienbod.com" -Signer $rootcert -NotAfter (Get-Date).AddYears(20) -FriendlyName "child_a_dev_damienbod.com"
$mypwd = ConvertTo-SecureString -String "1234" -Force -AsPlainText
Get-ChildItem -Path cert:\localMachine\my\"The thumbprint..." | Export-PfxCertificate -FilePath C:\git\AspNetCoreCertificateAuth\Certs\child_a_dev_damienbod.pfx -Password $mypwd
Export-Certificate -Cert cert:\localMachine\my\"The thumbprint..." -FilePath child_a_dev_damienbod.crt
範例根 - 中繼憑證 - 憑證
$mypwdroot = ConvertTo-SecureString -String "1234" -Force -AsPlainText
$mypwd = ConvertTo-SecureString -String "1234" -Force -AsPlainText
New-SelfSignedCertificate -DnsName "root_ca_dev_damienbod.com", "root_ca_dev_damienbod.com" -CertStoreLocation "cert:\LocalMachine\My" -NotAfter (Get-Date).AddYears(20) -FriendlyName "root_ca_dev_damienbod.com" -KeyUsageProperty All -KeyUsage CertSign, CRLSign, DigitalSignature
Get-ChildItem -Path cert:\localMachine\my\0C89639E4E2998A93E423F919B36D4009A0F9991 | Export-PfxCertificate -FilePath C:\git\root_ca_dev_damienbod.pfx -Password $mypwdroot
Export-Certificate -Cert cert:\localMachine\my\0C89639E4E2998A93E423F919B36D4009A0F9991 -FilePath root_ca_dev_damienbod.crt
$rootcert = ( Get-ChildItem -Path cert:\LocalMachine\My\0C89639E4E2998A93E423F919B36D4009A0F9991 )
New-SelfSignedCertificate -certstorelocation cert:\localmachine\my -dnsname "child_a_dev_damienbod.com" -Signer $rootcert -NotAfter (Get-Date).AddYears(20) -FriendlyName "child_a_dev_damienbod.com" -KeyUsageProperty All -KeyUsage CertSign, CRLSign, DigitalSignature -TextExtension @("2.5.29.19={text}CA=1&pathlength=1")
Get-ChildItem -Path cert:\localMachine\my\BA9BF91ED35538A01375EFC212A2F46104B33A44 | Export-PfxCertificate -FilePath C:\git\AspNetCoreCertificateAuth\Certs\child_a_dev_damienbod.pfx -Password $mypwd
Export-Certificate -Cert cert:\localMachine\my\BA9BF91ED35538A01375EFC212A2F46104B33A44 -FilePath child_a_dev_damienbod.crt
$parentcert = ( Get-ChildItem -Path cert:\LocalMachine\My\BA9BF91ED35538A01375EFC212A2F46104B33A44 )
New-SelfSignedCertificate -certstorelocation cert:\localmachine\my -dnsname "child_b_from_a_dev_damienbod.com" -Signer $parentcert -NotAfter (Get-Date).AddYears(20) -FriendlyName "child_b_from_a_dev_damienbod.com"
Get-ChildItem -Path cert:\localMachine\my\141594A0AE38CBBECED7AF680F7945CD51D8F28A | Export-PfxCertificate -FilePath C:\git\AspNetCoreCertificateAuth\Certs\child_b_from_a_dev_damienbod.pfx -Password $mypwd
Export-Certificate -Cert cert:\localMachine\my\141594A0AE38CBBECED7AF680F7945CD51D8F28A -FilePath child_b_from_a_dev_damienbod.crt
使用根、中繼或子憑證時,您可以視需要使用指紋或 PublicKey 來驗證憑證。
using System.Collections.Generic;
using System.IO;
using System.Security.Cryptography.X509Certificates;
namespace AspNetCoreCertificateAuthApi
{
public class MyCertificateValidationService
{
public bool ValidateCertificate(X509Certificate2 clientCertificate)
{
return CheckIfThumbprintIsValid(clientCertificate);
}
private bool CheckIfThumbprintIsValid(X509Certificate2 clientCertificate)
{
var listOfValidThumbprints = new List<string>
{
"141594A0AE38CBBECED7AF680F7945CD51D8F28A",
"0C89639E4E2998A93E423F919B36D4009A0F9991",
"BA9BF91ED35538A01375EFC212A2F46104B33A44"
};
if (listOfValidThumbprints.Contains(clientCertificate.Thumbprint))
{
return true;
}
return false;
}
}
}
憑證驗證快取
.NET 5 或更新版本支援啟用驗證結果快取的功能。 快取大幅提升憑證驗證的效能,因為驗證需要耗費大量資源。
根據預設,憑證驗證會停用快取。 要啟用快取,請在 Startup.ConfigureServices 中呼叫 AddCertificateCache:
public void ConfigureServices(IServiceCollection services)
{
services.AddAuthentication(
CertificateAuthenticationDefaults.AuthenticationScheme)
.AddCertificate()
.AddCertificateCache(options =>
{
options.CacheSize = 1024;
options.CacheEntryExpiration = TimeSpan.FromMinutes(2);
});
}
預設快取實作會將結果儲存在記憶體中。 您可以實作 ICertificateValidationCache,並使用相依性插入將它註冊,以提供您自己的快取。 例如: services.AddSingleton<ICertificateValidationCache, YourCache>() 。
選擇性用戶端憑證
本節提供應用程式的資訊,這些應用程式必須使用憑證來保護應用程式的子集。 例如,應用程式中的 Razor 頁面或控制器可能需要用戶端憑證。 這會以用戶端憑證的形式呈現挑戰:
- 是 TLS 功能,而不是 HTTP 功能。
- 會針對每個連線進行交涉,通常是在連線開始時於任何 HTTP 資料可用之前完成。
實作選擇性用戶端憑證的方法有兩種:
- 使用不同的主機名稱 (SNI) 和重新導向。 雖然設定時需要要更多工作,但建議您這麼做,因為它適用於大部分的環境和通訊協定。
- 在 HTTP 請求期間重協商。 這樣會有一些限制,不建議使用。
個別主機 (SNI)
在連線開始時,只會知道伺服器名稱指示 (SNI)†。 每個主機名稱都可以設定用戶端憑證,讓一個主機需要它們,而另一個主機則不需要它們。
- 設定網域和子網域的繫結:
- 例如,在
contoso.com和myClient.contoso.com上設定繫結。contoso.com主機不需要用戶端憑證,但myClient.contoso.com需要。 - 如需詳細資訊,請參閱:
- 例如,在
.NET 5 或更新版本新增更方便的支援,以重新導向以取得選擇性客戶端憑證。 如需詳細資訊,請參閱選擇性憑證範例。
- 對於需要用戶端憑證且沒有用戶端憑證的 Web 應用程式要求:
- 使用受用戶端憑證保護的子網域,重新導向至相同的頁面。
- 例如,重新導向至
myClient.contoso.com/requestedPage。 由於myClient.contoso.com/requestedPage的主機名稱不同於contoso.com/requestedPage,因此用戶端會建立另一個連線,並提供用戶端憑證。 - 如需詳細資訊,請參閱 ASP.NET Core 中的授權簡介。
†伺服器名稱指示 (SNI) 是 TLS 延伸模組,可將虛擬網域納入 SSL 交涉的一部分。 這實際上表示可以使用虛擬域名或主機名稱來識別網路端點。
重新交涉
TLS 重新協商是一個用戶端和伺服器可重新評估個別連線加密需求的流程,包括如果先前未提供用戶端憑證時可以要求提供憑證。 TLS 重新交涉有安全性風險,不建議這麼做,因為:
- 在 HTTP/1.1 中,伺服器必須先緩衝或處理任何正在傳輸中的 HTTP 資料,例如 POST 請求主體,以確保連線已準備好進行重新協商。 否則,重新交涉可能會停止回應或失敗。
- HTTP/2 和 HTTP/3 明確禁止重新交涉。
- 重新交涉有安全性風險。 TLS 1.3 已移除整個連線的重新交涉,並將它取代為新的擴充功能,使得連線開始時只要求用戶端憑證。 此機制會透過相同的 API 公開,但仍受限於緩衝和 HTTP 通訊協定版本的先前條件約束。
這項功能的實作和設定因伺服器和架構版本而異。
IIS
IIS 會代表您管理用戶端憑證交涉。 應用程式的子區段可以啟用 SslRequireCert 選項來協商這些要求的用戶端憑證。 如需詳細資訊,請參閱 IIS 文件中的設定。
IIS 會在重新交涉之前,自動緩衝任何要求本文資料,直到達到設定的大小限制為止。 超過限制的要求會遭到 413 回應的拒絕。 此限制預設為 48KB,並可透過設定 uploadReadAheadSize 來設定。
HttpSys
HttpSys 有兩個設定可控制用戶端憑證交涉,而且兩者都應該設定。 第一個位於 netsh.exe 的 http add sslcert clientcertnegotiation=enable/disable 下。 此旗標表示用戶端憑證是否應在連線開始時交涉,且應設定為選擇性用戶端憑證的 disable。 如需詳細資訊,請參閱 netsh 文件。
另一個設定是 ClientCertificateMethod。 當設定為 AllowRenegotation 時,用戶端憑證可以在要求期間重新協商。
注意應用程式應先緩衝或取用任何請求主體資料再嘗試重新交涉,否則該請求可能無法回應。
已知問題是,啟用 AllowRenegotation 後,當存取 ClientCertificate 屬性時,可能會導致重新協商同期進行。 呼叫 GetClientCertificateAsync 方法來避免這種情況。 在 .NET 6 中此問題已解決。 如需詳細資訊,請參閱這個 GitHub 問題。 請注意,如果用戶端拒絕提供 null 憑證,GetClientCertificateAsync 可能會傳回 Null 憑證。
Kestrel
Kestrel 使用 [ClientCertificateMode] 選項控制用戶端憑證交涉。
針對 .NET 5 或更早版本,Kestrel 不支持在連線開始後重新協商以獲取客戶端憑證。 這項功能已在 .NET 6 中新增。
Microsoft.AspNetCore.Authentication.Certificate 包含類似 ASP.NET Core 憑證驗證的實作。 遠在到達 ASP.NET Core 之前,就會會在 TLS 層級發生憑證驗證。 更準確地說,這是一個驗證處理常式,該常式會驗證憑證,然後提供可將該憑證解析為 ClaimsPrincipal的事件。
設定您的伺服器進行憑證驗證,無論它是 IIS、Kestrel、Azure Web Apps 或您使用的任何其他工具。
Proxy 和負載平衡器案例
憑證驗證是一種有狀態的情境,主要用於代理伺服器或負載平衡器不處理客戶端與伺服器之間流量的場合。 如果使用了代理或負載平衡器,則只有在符合特定條件的情況下,憑證驗證才會有效。
- 處理驗證。
- 將使用者驗證資訊傳遞至應用程式 (例如,在要求標頭中),以處理驗證資訊。
在使用 Proxy 和負載平衡器的環境中,憑證驗證的替代方案是 Active Directory 同盟服務 (ADFS) 搭配 OpenID Connect (OIDC)。
開始
取得 HTTPS 憑證、套用憑證,並設定伺服器需要憑證。
在您的 Web 應用程式中,新增對 Microsoft.AspNetCore.Authentication.Certificate 套件的參考。 然後在 Startup.ConfigureServices 方法中,使用您的選項呼叫 services.AddAuthentication(CertificateAuthenticationDefaults.AuthenticationScheme).AddCertificate(...);,提供委派給 OnCertificateValidated,以便對傳送要求的用戶端憑證執行任何補充驗證。 將該資訊轉換成 ClaimsPrincipal ,並在 context.Principal 屬性上加以設定。
如果驗證失敗,此處理程式會傳回 403 (Forbidden) 回應,而不是 401 (Unauthorized),正如您所預期。 原因是驗證應該在初始 TLS 連線期間發生。 到達事件處理器時,已經來不及了。 無法將連線從匿名連線升級為具有憑證的連線。
同時在 app.UseAuthentication(); 方法中新增 Startup.Configure。 否則,HttpContext.User 不會被設定為從憑證創建的 ClaimsPrincipal。 例如:
public void ConfigureServices(IServiceCollection services)
{
services.AddAuthentication(
CertificateAuthenticationDefaults.AuthenticationScheme)
.AddCertificate();
// All other service configuration
}
public void Configure(IApplicationBuilder app, IHostingEnvironment env)
{
app.UseAuthentication();
// All other app configuration
}
上述範例示範新增憑證驗證的預設方式。 處理常式會使用常用憑證屬性來建構使用者主體。
設定憑證驗證
處理常式 CertificateAuthenticationOptions 有一些內建驗證,這是您應該在憑證上執行的最小驗證。 每個設定預設都會啟用。
AllowedCertificateTypes = Chained, SelfSigned, 或 All (Chained 或 SelfSigned)
預設值:CertificateTypes.Chained
這項檢查會驗證只允許適當的憑證類型。 如果應用程式使用自我簽署憑證,則必須將此選項設定為 CertificateTypes.All 或 CertificateTypes.SelfSigned。
驗證證書使用
預設值:true
這項檢查會驗證用戶端提供的憑證是否具有用戶端驗證延伸金鑰使用 (EKU),或完全沒有 EKU。 如規格所述,如果未指定 EKU,則所有 EKU 都會被視為有效。
驗證有效期
預設值:true
這項檢查會驗證憑證是否在其有效期間內。 每次請求時,處理程序會確保在提交時有效的憑證在當前會話期間未過期。
撤銷標記
預設值:X509RevocationFlag.ExcludeRoot
指定用來檢查鏈結中哪個憑證是否被撤銷的旗標。
只有在憑證鏈結至根憑證時,才會執行撤銷檢查。
撤銷模式
預設值:X509RevocationMode.Online
旗標,指定撤銷檢查的執行方式。
指定進行線上檢查時,可能會導致長時間的延遲,因為需要聯絡憑證授權機構。
只有在憑證鏈結至根憑證時,才會執行撤銷檢查。
我可以將應用程式設定為只在特定路徑上要求憑證嗎?
這是不可能的。 請記住,憑證交換是在 HTTPS 交談開始時完成的,伺服器會在該連線上收到第一個要求之前完成,因此無法根據任何要求欄位來設定範圍。
事件處理程序
事件處理程式有兩個事件:
-
OnAuthenticationFailed:如果在驗證期間發生例外狀況,則會進行呼叫,並可讓您做出反應。 -
OnCertificateValidated:在憑證驗證完成、通過驗證並建立預設主體後被呼叫。 此事件可讓您執行自己的驗證,並擴展或取代主體。 如需範例,包括:判斷您的服務是否識別該憑證。
建構您自己的原則。 請考慮
Startup.ConfigureServices中的下列範例:services.AddAuthentication( CertificateAuthenticationDefaults.AuthenticationScheme) .AddCertificate(options => { options.Events = new CertificateAuthenticationEvents { OnCertificateValidated = context => { var claims = new[] { new Claim( ClaimTypes.NameIdentifier, context.ClientCertificate.Subject, ClaimValueTypes.String, context.Options.ClaimsIssuer), new Claim(ClaimTypes.Name, context.ClientCertificate.Subject, ClaimValueTypes.String, context.Options.ClaimsIssuer) }; context.Principal = new ClaimsPrincipal( new ClaimsIdentity(claims, context.Scheme.Name)); context.Success(); return Task.CompletedTask; } }; });
如果您發現輸入憑證不符合額外的驗證,請呼叫 context.Fail("failure reason") 且附有失敗原因。
針對實際功能,您可能想要呼叫已在相依性注入中註冊的服務,以連接到資料庫或其他類型的使用者資料存儲。 使用傳遞至您的委派的上下文來存取服務。 請考慮 Startup.ConfigureServices 中的下列範例:
services.AddAuthentication(
CertificateAuthenticationDefaults.AuthenticationScheme)
.AddCertificate(options =>
{
options.Events = new CertificateAuthenticationEvents
{
OnCertificateValidated = context =>
{
var validationService =
context.HttpContext.RequestServices
.GetRequiredService<ICertificateValidationService>();
if (validationService.ValidateCertificate(
context.ClientCertificate))
{
var claims = new[]
{
new Claim(
ClaimTypes.NameIdentifier,
context.ClientCertificate.Subject,
ClaimValueTypes.String,
context.Options.ClaimsIssuer),
new Claim(
ClaimTypes.Name,
context.ClientCertificate.Subject,
ClaimValueTypes.String,
context.Options.ClaimsIssuer)
};
context.Principal = new ClaimsPrincipal(
new ClaimsIdentity(claims, context.Scheme.Name));
context.Success();
}
return Task.CompletedTask;
}
};
});
就概念而言,憑證的驗證是授權問題。 例如,在授權原則中加入對簽發者或指紋的檢查,而非在 OnCertificateValidated 內,是完全可以接受的。
將伺服器設定為需要憑證
Kestrel
在 Program.cs 中設定 Kestrel ,如下所示:
public static void Main(string[] args)
{
CreateHostBuilder(args).Build().Run();
}
public static IHostBuilder CreateHostBuilder(string[] args)
{
return Host.CreateDefaultBuilder(args)
.ConfigureWebHostDefaults(webBuilder =>
{
webBuilder.UseStartup<Startup>();
webBuilder.ConfigureKestrel(o =>
{
o.ConfigureHttpsDefaults(o =>
o.ClientCertificateMode = ClientCertificateMode.RequireCertificate);
});
});
}
注意
透過呼叫 Listen之前呼叫 ConfigureHttpsDefaults 所建立的端點,不會套用預設值。
IIS
在 IIS 管理員中完成下列步驟:
- 從[連線]索引標籤中選取您的網站。
- 按兩下[功能檢視]視窗中的[SSL 設定]選項。
- 勾選[需要 SSL]核取方塊,然後選取[用戶端憑證]區段中的[需要]選項按鈕。
Azure 和自訂 Web Proxy
如需如何設定憑證轉送中介軟體,請參閱主機和部署檔。
在 Azure Web Apps 中使用憑證驗證
Azure 不需要轉送設定。 轉送設定是由憑證轉送中介軟體所設定。
注意
此案例需要憑證轉送中介軟體。
如需詳細資訊,請參閱在 Azure App 服務 的程式碼中使用 TLS/SSL 憑證 (Azure 檔)。
在自訂 Web Proxy 中使用憑證驗證
AddCertificateForwarding 方法可用來指定:
- 用戶端標頭名稱。
- 如何載入憑證 (使用
HeaderConverter屬性)。
在自訂 Web Proxy 中,憑證會以自訂要求標頭的形式傳遞,例如 X-SSL-CERT。 若要使用它,請在 Startup.ConfigureServices 中設定憑證轉送:
public void ConfigureServices(IServiceCollection services)
{
services.AddCertificateForwarding(options =>
{
options.CertificateHeader = "X-SSL-CERT";
options.HeaderConverter = (headerValue) =>
{
X509Certificate2 clientCertificate = null;
if(!string.IsNullOrWhiteSpace(headerValue))
{
byte[] bytes = StringToByteArray(headerValue);
clientCertificate = new X509Certificate2(bytes);
}
return clientCertificate;
};
});
}
private static byte[] StringToByteArray(string hex)
{
int NumberChars = hex.Length;
byte[] bytes = new byte[NumberChars / 2];
for (int i = 0; i < NumberChars; i += 2)
{
bytes[i / 2] = Convert.ToByte(hex.Substring(i, 2), 16);
}
return bytes;
}
如果應用程式由 NGINX 使用配置 proxy_set_header ssl-client-cert $ssl_client_escaped_cert 進行反向代理,或是使用 NGINX Ingress 在 Kubernetes 上部署,用戶端憑證會以 URL 編碼格式 傳遞至應用程式。 若要使用憑證,請將它解碼如下:
將 System.Net 的命名空間新增至 Startup.cs 頂端:
using System.Net;
在 Startup.ConfigureServices 中:
services.AddCertificateForwarding(options =>
{
options.CertificateHeader = "ssl-client-cert";
options.HeaderConverter = (headerValue) =>
{
X509Certificate2 clientCertificate = null;
if (!string.IsNullOrWhiteSpace(headerValue))
{
var bytes = UrlEncodedPemToByteArray(headerValue);
clientCertificate = new X509Certificate2(bytes);
}
return clientCertificate;
};
});
新增 UrlEncodedPemToByteArray 方法:
private static byte[] UrlEncodedPemToByteArray(string urlEncodedBase64Pem)
{
var base64Pem = WebUtility.UrlDecode(urlEncodedBase64Pem);
var base64Cert = base64Pem
.Replace("-----BEGIN CERTIFICATE-----", string.Empty)
.Replace("-----END CERTIFICATE-----", string.Empty)
.Trim();
return Convert.FromBase64String(base64Cert);
}
Startup.Configure 方法接著會新增中介軟體。 在呼叫 UseAuthentication 和 UseAuthorization 之前會先呼叫 UseCertificateForwarding:
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
...
app.UseRouting();
app.UseCertificateForwarding();
app.UseAuthentication();
app.UseAuthorization();
app.UseEndpoints(endpoints =>
{
endpoints.MapControllers();
});
}
不同的類別可用來實作驗證邏輯。 因為在此範例中使用相同的自我簽署憑證,因此請確認只能使用您的憑證。 驗證用戶端憑證和伺服器憑證的指紋是否相符,否則可以使用任何憑證,而且足以進行驗證。 這會在 AddCertificate 方法內使用。 如果您使用中繼憑證或子憑證,您也可以在此驗證主體或發行者。
using System.IO;
using System.Security.Cryptography.X509Certificates;
namespace AspNetCoreCertificateAuthApi
{
public class MyCertificateValidationService
{
public bool ValidateCertificate(X509Certificate2 clientCertificate)
{
// Do not hardcode passwords in production code
// Use thumbprint or key vault
var cert = new X509Certificate2(
Path.Combine("sts_dev_cert.pfx"), "1234");
if (clientCertificate.Thumbprint == cert.Thumbprint)
{
return true;
}
return false;
}
}
}
使用憑證和HttpClientHandler來實作HttpClient
HttpClientHandler 可以直接新增到 HttpClient 類別的建構函式中。 建立 HttpClient 實例時應該小心。 接著,HttpClient 會隨每個要求傳送憑證。
private async Task<JsonDocument> GetApiDataUsingHttpClientHandler()
{
var cert = new X509Certificate2(Path.Combine(_environment.ContentRootPath, "sts_dev_cert.pfx"), "1234");
var handler = new HttpClientHandler();
handler.ClientCertificates.Add(cert);
var client = new HttpClient(handler);
var request = new HttpRequestMessage()
{
RequestUri = new Uri("https://localhost:44379/api/values"),
Method = HttpMethod.Get,
};
var response = await client.SendAsync(request);
if (response.IsSuccessStatusCode)
{
var responseContent = await response.Content.ReadAsStringAsync();
var data = JsonDocument.Parse(responseContent);
return data;
}
throw new ApplicationException($"Status code: {response.StatusCode}, Error: {response.ReasonPhrase}");
}
使用憑證和來自 IHttpClientFactory 的具名 HttpClient 來實作 HttpClient
以下範例示範如何使用處理常式中的 ClientCertificates 屬性將用戶端憑證新增至 HttpClientHandler。 接著,這個處理常式可以使用 HttpClient 方法,應用於 ConfigurePrimaryHttpMessageHandler 的命名實例中。 這是在 Startup.ConfigureServices 中設定:
var clientCertificate =
new X509Certificate2(
Path.Combine(_environment.ContentRootPath, "sts_dev_cert.pfx"), "1234");
services.AddHttpClient("namedClient", c =>
{
}).ConfigurePrimaryHttpMessageHandler(() =>
{
var handler = new HttpClientHandler();
handler.ClientCertificates.Add(clientCertificate);
return handler;
});
然後,您可以使用 IHttpClientFactory 來獲取具有處理常式和憑證的具名實例。 在 CreateClient 類別中定義的用戶端名稱的 Startup 方法會用來取得實例。 您可以視需要使用用戶端傳送 HTTP 要求。
private readonly IHttpClientFactory _clientFactory;
public ApiService(IHttpClientFactory clientFactory)
{
_clientFactory = clientFactory;
}
private async Task<JsonDocument> GetApiDataWithNamedClient()
{
var client = _clientFactory.CreateClient("namedClient");
var request = new HttpRequestMessage()
{
RequestUri = new Uri("https://localhost:44379/api/values"),
Method = HttpMethod.Get,
};
var response = await client.SendAsync(request);
if (response.IsSuccessStatusCode)
{
var responseContent = await response.Content.ReadAsStringAsync();
var data = JsonDocument.Parse(responseContent);
return data;
}
throw new ApplicationException($"Status code: {response.StatusCode}, Error: {response.ReasonPhrase}");
}
如果正確的憑證傳送至伺服器,則會傳回資料。 如果沒有傳送憑證或錯誤的憑證,則會傳回 HTTP 403 狀態碼。
在 PowerShell 中建立憑證
建立憑證是設定此流程中最困難的部分。 您可以使用 New-SelfSignedCertificate PowerShell Cmdlet 來建立根憑證。 建立憑證時,請使用強式密碼。 請務必新增 KeyUsageProperty 參數和 KeyUsage 參數,如下所示。
建立根 CA
New-SelfSignedCertificate -DnsName "root_ca_dev_damienbod.com", "root_ca_dev_damienbod.com" -CertStoreLocation "cert:\LocalMachine\My" -NotAfter (Get-Date).AddYears(20) -FriendlyName "root_ca_dev_damienbod.com" -KeyUsageProperty All -KeyUsage CertSign, CRLSign, DigitalSignature
$mypwd = ConvertTo-SecureString -String "1234" -Force -AsPlainText
Get-ChildItem -Path cert:\localMachine\my\"The thumbprint..." | Export-PfxCertificate -FilePath C:\git\root_ca_dev_damienbod.pfx -Password $mypwd
Export-Certificate -Cert cert:\localMachine\my\"The thumbprint..." -FilePath root_ca_dev_damienbod.crt
注意
-DnsName 參數值必須符合應用程式的部署目標。 例如,用於開發的「localhost」。
在受信任的根目錄中安裝
您的主機系統上必須信任根憑證。 憑證授權單位單位未建立的根憑證預設不會受到信任。 如需如何在 Windows 上信任根憑證的資訊,請參閱此問題。
中繼憑證
中繼憑證現在可以從根憑證建立。 並非所有使用案例都需要此項目,但您可能需要建立許多憑證,或需要啟動或停用憑證群組。 需要 TextExtension 參數,才能在憑證的基本條件約束中設定路徑長度。
然後可以將中繼憑證新增到 Windows 主機系統中的受信任中繼憑證。
$mypwd = ConvertTo-SecureString -String "1234" -Force -AsPlainText
$parentcert = ( Get-ChildItem -Path cert:\LocalMachine\My\"The thumbprint of the root..." )
New-SelfSignedCertificate -certstorelocation cert:\localmachine\my -dnsname "intermediate_dev_damienbod.com" -Signer $parentcert -NotAfter (Get-Date).AddYears(20) -FriendlyName "intermediate_dev_damienbod.com" -KeyUsageProperty All -KeyUsage CertSign, CRLSign, DigitalSignature -TextExtension @("2.5.29.19={text}CA=1&pathlength=1")
Get-ChildItem -Path cert:\localMachine\my\"The thumbprint..." | Export-PfxCertificate -FilePath C:\git\AspNetCoreCertificateAuth\Certs\intermediate_dev_damienbod.pfx -Password $mypwd
Export-Certificate -Cert cert:\localMachine\my\"The thumbprint..." -FilePath intermediate_dev_damienbod.crt
從中繼憑證建立子憑證
子憑證可以從中繼憑證建立。 這是終端實體,不需要建立更多子憑證。
$parentcert = ( Get-ChildItem -Path cert:\LocalMachine\My\"The thumbprint from the Intermediate certificate..." )
New-SelfSignedCertificate -certstorelocation cert:\localmachine\my -dnsname "child_a_dev_damienbod.com" -Signer $parentcert -NotAfter (Get-Date).AddYears(20) -FriendlyName "child_a_dev_damienbod.com"
$mypwd = ConvertTo-SecureString -String "1234" -Force -AsPlainText
Get-ChildItem -Path cert:\localMachine\my\"The thumbprint..." | Export-PfxCertificate -FilePath C:\git\AspNetCoreCertificateAuth\Certs\child_a_dev_damienbod.pfx -Password $mypwd
Export-Certificate -Cert cert:\localMachine\my\"The thumbprint..." -FilePath child_a_dev_damienbod.crt
從根憑證建立子憑證
您也可以直接從根憑證建立子憑證。
$rootcert = ( Get-ChildItem -Path cert:\LocalMachine\My\"The thumbprint from the root cert..." )
New-SelfSignedCertificate -certstorelocation cert:\localmachine\my -dnsname "child_a_dev_damienbod.com" -Signer $rootcert -NotAfter (Get-Date).AddYears(20) -FriendlyName "child_a_dev_damienbod.com"
$mypwd = ConvertTo-SecureString -String "1234" -Force -AsPlainText
Get-ChildItem -Path cert:\localMachine\my\"The thumbprint..." | Export-PfxCertificate -FilePath C:\git\AspNetCoreCertificateAuth\Certs\child_a_dev_damienbod.pfx -Password $mypwd
Export-Certificate -Cert cert:\localMachine\my\"The thumbprint..." -FilePath child_a_dev_damienbod.crt
範例根 - 中繼憑證 - 憑證
$mypwdroot = ConvertTo-SecureString -String "1234" -Force -AsPlainText
$mypwd = ConvertTo-SecureString -String "1234" -Force -AsPlainText
New-SelfSignedCertificate -DnsName "root_ca_dev_damienbod.com", "root_ca_dev_damienbod.com" -CertStoreLocation "cert:\LocalMachine\My" -NotAfter (Get-Date).AddYears(20) -FriendlyName "root_ca_dev_damienbod.com" -KeyUsageProperty All -KeyUsage CertSign, CRLSign, DigitalSignature
Get-ChildItem -Path cert:\localMachine\my\0C89639E4E2998A93E423F919B36D4009A0F9991 | Export-PfxCertificate -FilePath C:\git\root_ca_dev_damienbod.pfx -Password $mypwdroot
Export-Certificate -Cert cert:\localMachine\my\0C89639E4E2998A93E423F919B36D4009A0F9991 -FilePath root_ca_dev_damienbod.crt
$rootcert = ( Get-ChildItem -Path cert:\LocalMachine\My\0C89639E4E2998A93E423F919B36D4009A0F9991 )
New-SelfSignedCertificate -certstorelocation cert:\localmachine\my -dnsname "child_a_dev_damienbod.com" -Signer $rootcert -NotAfter (Get-Date).AddYears(20) -FriendlyName "child_a_dev_damienbod.com" -KeyUsageProperty All -KeyUsage CertSign, CRLSign, DigitalSignature -TextExtension @("2.5.29.19={text}CA=1&pathlength=1")
Get-ChildItem -Path cert:\localMachine\my\BA9BF91ED35538A01375EFC212A2F46104B33A44 | Export-PfxCertificate -FilePath C:\git\AspNetCoreCertificateAuth\Certs\child_a_dev_damienbod.pfx -Password $mypwd
Export-Certificate -Cert cert:\localMachine\my\BA9BF91ED35538A01375EFC212A2F46104B33A44 -FilePath child_a_dev_damienbod.crt
$parentcert = ( Get-ChildItem -Path cert:\LocalMachine\My\BA9BF91ED35538A01375EFC212A2F46104B33A44 )
New-SelfSignedCertificate -certstorelocation cert:\localmachine\my -dnsname "child_b_from_a_dev_damienbod.com" -Signer $parentcert -NotAfter (Get-Date).AddYears(20) -FriendlyName "child_b_from_a_dev_damienbod.com"
Get-ChildItem -Path cert:\localMachine\my\141594A0AE38CBBECED7AF680F7945CD51D8F28A | Export-PfxCertificate -FilePath C:\git\AspNetCoreCertificateAuth\Certs\child_b_from_a_dev_damienbod.pfx -Password $mypwd
Export-Certificate -Cert cert:\localMachine\my\141594A0AE38CBBECED7AF680F7945CD51D8F28A -FilePath child_b_from_a_dev_damienbod.crt
使用根、中繼或子憑證時,您可以視需要使用指紋或 PublicKey 來驗證憑證。
using System.Collections.Generic;
using System.IO;
using System.Security.Cryptography.X509Certificates;
namespace AspNetCoreCertificateAuthApi
{
public class MyCertificateValidationService
{
public bool ValidateCertificate(X509Certificate2 clientCertificate)
{
return CheckIfThumbprintIsValid(clientCertificate);
}
private bool CheckIfThumbprintIsValid(X509Certificate2 clientCertificate)
{
var listOfValidThumbprints = new List<string>
{
"141594A0AE38CBBECED7AF680F7945CD51D8F28A",
"0C89639E4E2998A93E423F919B36D4009A0F9991",
"BA9BF91ED35538A01375EFC212A2F46104B33A44"
};
if (listOfValidThumbprints.Contains(clientCertificate.Thumbprint))
{
return true;
}
return false;
}
}
}
選擇性用戶端憑證
本節提供應用程式的資訊,這些應用程式必須使用憑證來保護應用程式的子集。 例如,應用程式中的 Razor 頁面或控制器可能需要用戶端憑證。 這會以用戶端憑證的形式呈現挑戰:
- 是 TLS 功能,而不是 HTTP 功能。
- 每個連線會在開始時進行協商,且通常是在任何 HTTP 資料可用之前。
實作選擇性用戶端憑證的方法有兩種:
- 使用獨立的主機名稱 (SNI) 和重新導向。 雖然設定時需要要更多工作,但建議您這麼做,因為它適用於大部分的環境和通訊協定。
- 在 HTTP 請求期間重新談判。 這樣會有一些限制,不建議使用。
個別主機 (SNI)
在連線開始時,只會知道伺服器名稱指示 (SNI)†。 每個主機名稱都可以設定用戶端憑證,讓一個主機需要它們,而另一個主機則不需要它們。
- 設定網域和子網域的繫結:
- 例如,在
contoso.com和myClient.contoso.com上設定繫結。contoso.com主機不需要用戶端憑證,但myClient.contoso.com需要。 - 如需詳細資訊,請參閱:
-
KestrelASP.NET Core 中的網頁伺服器:
- ListenOptions.UseHttps
- ClientCertificateMode
- 注意 Kestrel 目前不支援一個繫結上的多個 TLS 組態,您將需要兩個具有唯一 IP 或連接埠的繫結。 如需詳細資訊,請參閱這個 GitHub 問題。
- IIS
- HTTP.sys:設定 Windows Server
-
KestrelASP.NET Core 中的網頁伺服器:
- 例如,在
.NET 5 或更新版本新增更方便的支援,以重新導向以取得選擇性客戶端憑證。 如需詳細資訊,請參閱選擇性憑證範例。
- 對於向 Web 應用程式發出的請求,需要客戶端憑證但尚未提供該憑證的情況:
- 使用受用戶端憑證保護的子網域,重新導向至相同的頁面。
- 例如,重新導向至
myClient.contoso.com/requestedPage。 由於請求發送到的主機名稱myClient.contoso.com/requestedPage與contoso.com/requestedPage不同,因此用戶端會建立一個不同的連線,並提供用戶端憑證。 - 如需詳細資訊,請參閱 ASP.NET Core 中的授權簡介。
†伺服器名稱指示 (SNI) 是 TLS 延伸模組,可將虛擬網域納入 SSL 交涉的一部分。 這意味著虛擬網域名稱或主機名稱可用來識別網路端點。
重新交涉
TLS 重新協商是一個用戶端和伺服器可重新評估個別連線加密需求的流程,包括如果先前未提供用戶端憑證時可以要求提供憑證。 TLS 重新交涉有安全性風險,不建議這麼做,因為:
- 在 HTTP/1.1 中,伺服器必須先緩衝或處理任何正在傳輸中的 HTTP 資料,例如 POST 請求主體,以確保連線已準備好進行重新協商。 否則,重新交涉可能會停止回應或失敗。
- HTTP/2 和 HTTP/3 明確禁止重新交涉。
- 重新交涉有安全性風險。 TLS 1.3 已移除整個連線的重新交涉,並將它取代為新的擴充功能,使得連線開始時只要求用戶端憑證。 此機制會透過相同的 API 公開,但仍受限於緩衝和 HTTP 通訊協定版本的先前條件約束。
這項功能的實作和設定因伺服器和架構版本而異。
IIS
IIS 會代表您管理用戶端憑證交涉。 應用程式的子區段可以啟用 SslRequireCert 選項來協商這些要求的用戶端憑證。 如需詳細資訊,請參閱 IIS 文件中的設定。
IIS 會在重新交涉之前,自動緩衝任何要求本文資料,直到達到設定的大小限制為止。 超過限制的要求會遭到 413 回應的拒絕。 此限制預設為 48KB,並可透過設定 uploadReadAheadSize 來設定。
HttpSys
HttpSys 有兩個設定可控制用戶端憑證交涉,而且兩者都應該設定。 第一個位於 netsh.exe 的 http add sslcert clientcertnegotiation=enable/disable 下。 此旗標表示用戶端憑證是否應在連線開始時交涉,且應設定為選擇性用戶端憑證的 disable。 如需詳細資訊,請參閱 netsh 文件。
另一個設定是 ClientCertificateMethod。 當設定為 AllowRenegotation 時,用戶端憑證可以在要求期間重新協商。
注意應用程式應先緩衝或取用任何請求主體資料再嘗試重新交涉,否則該請求可能無法回應。
已知問題是,啟用 AllowRenegotation 後,當存取 ClientCertificate 屬性時,可能會導致重新協商同期進行。 呼叫 GetClientCertificateAsync 方法來避免這種情況。 在 .NET 6 中此問題已解決。 如需詳細資訊,請參閱這個 GitHub 問題。 請注意,如果用戶端拒絕提供 null 憑證,GetClientCertificateAsync 可能會傳回 Null 憑證。
Kestrel
Kestrel 使用 [ClientCertificateMode] 選項控制用戶端憑證交涉。
針對 .NET 5 或更早版本,Kestrel 不支持在連線開始後重新協商以獲取客戶端憑證。 這項功能已在 .NET 6 中新增。
歡迎在GitHub第 #18720 的討論串中留下問題、評論及其他關於可選客戶憑證的回饋。