使用 c# 使用证书进行 SSL 客户端身份验证
SSL Client Authentication with certificate using c#
我需要创建一个 c# 应用程序,它必须使用 SSL 向服务器发送 API 请求。我需要创建客户端身份验证。我已经有了服务器 CA 证书、客户端证书 (cer)、客户端私钥 (pem) 和密码。我找不到有关如何创建客户端连接的示例。有人可以建议我从哪里开始解释一段小代码吗?在我手中,我有客户端证书 (PEM)、客户端代理密钥和客户端密钥的密码。我不知道从哪里开始编写向服务器发送请求的代码
前段时间我创建了 this POC for client authentication with certificate in .Net Core. It uses idunno.Authentication package that is now build-in in .Net Core。我的 POC 现在可能有点过时了,但它对你来说可能是一个很好的起点。
首先创建一个扩展方法来添加证书到HttpClientHandler
:
public static class HttpClientHandlerExtensions
{
public static HttpClientHandler AddClientCertificate(this HttpClientHandler handler, X509Certificate2 certificate)
{
handler.ClientCertificateOptions = ClientCertificateOption.Manual;
handler.ClientCertificates.Add(certificate);
return handler;
}
}
然后另一种扩展方法将证书添加到IHttpClientBuilder
public static IHttpClientBuilder AddClientCertificate(this IHttpClientBuilder httpClientBuilder, X509Certificate2 certificate)
{
httpClientBuilder.ConfigureHttpMessageHandlerBuilder(builder =>
{
if (builder.PrimaryHandler is HttpClientHandler handler)
{
handler.AddClientCertificate(certificate);
}
else
{
throw new InvalidOperationException($"Only {typeof(HttpClientHandler).FullName} handler type is supported. Actual type: {builder.PrimaryHandler.GetType().FullName}");
}
});
return httpClientBuilder;
}
然后加载证书并在HttpClientFactory
中注册HttpClient
var cert = CertificateFinder.FindBySubject("your-subject");
services
.AddHttpClient("ClientWithCertificate", client => { client.BaseAddress = new Uri(ServerUrl); })
.AddClientCertificate(cert);
现在,当您使用工厂创建的客户端时,它会自动将您的证书与请求一起发送;
public async Task SendRequest()
{
var client = _httpClientFactory.CreateClient("ClientWithCertificate");
....
}
这里有很多选项,所以根据问题的简洁性,我不能 100% 确定要走哪条路。我创建了一个基本的 aspnet.core WebApi 项目,其中包含“天气预报”控制器作为测试。这里没有显示很多错误检查,并且有很多关于密钥和证书如何存储或不存储的假设,甚至 OS 这是为了什么(不是 OS 一样重要,但密钥存储不同)。
另请注意,使用 OpenSsl 创建的证书不包含 Web 服务器证书中的私钥。为此,您必须将证书和私钥组合成 Pkcs12/PFX 格式。
例如(对于 Web 服务器,不一定是客户端,但您真的可以在任何地方使用 PFX...)。
openssl pkcs12 -export -out so-selfsigned-ca-root-x509.pfx -inkey so-root-ca-rsa-private-key.pem -in so-selfsigned-ca-root-x509.pem
考虑控制台应用程序中的这个 Main 方法。我添加到其中的唯一非 BCL 包(用于 PEM 私钥)是 Portable.BouncyCastle。如果您使用的是 .NET Core 5.0(几天前发布),那里有 PEM 选项。假设您还没有,这个示例使用的是 NetCoreApp 3.1。
The appSettings.json example file:
{
"HttpClientRsaArtifacts": {
"ClientCertificateFilename": "so-x509-client-cert.pem",
"ClientPrivateKeyFilename": "so-client-private-key.pem"
}
}
private static async Task Main(string[] args)
{
IConfiguration config = new ConfigurationBuilder().AddJsonFile("appSettings.json").Build();
const string mainAppSettingsKey = "HttpClientRsaArtifacts";
var clientCertificateFileName = config[$"{mainAppSettingsKey}:ClientCertificateFilename"];
var clientPrivKeyFileName = config[$"{mainAppSettingsKey}:ClientPrivateKeyFilename"];
var clientCertificate = new X509Certificate2(clientCertificateFileName);
var httpClientHandler = new HttpClientHandler();
httpClientHandler.ClientCertificates.Add(clientCertificate);
httpClientHandler.ClientCertificateOptions = ClientCertificateOption.Manual;
httpClientHandler.ServerCertificateCustomValidationCallback = ByPassCertErrorsForTestPurposesDoNotDoThisInTheWild;
httpClientHandler.CheckCertificateRevocationList = false;
var httpClient = new HttpClient(httpClientHandler);
httpClient.BaseAddress = new Uri("https://localhost:5001/");
var httpRequestMessage = new HttpRequestMessage(
HttpMethod.Get,
"weatherforecast");
// This is "the connection" (and API call)
using var response = await httpClient.SendAsync(
httpRequestMessage,
HttpCompletionOption.ResponseHeadersRead);
var stream = await response.Content.ReadAsStreamAsync();
var jsonDocument = await JsonDocument.ParseAsync(stream);
var options = new JsonSerializerOptions
{
WriteIndented = true,
};
Console.WriteLine(
JsonSerializer.Serialize(
jsonDocument,
options));
}
private static bool ByPassCertErrorsForTestPurposesDoNotDoThisInTheWild(
HttpRequestMessage httpRequestMsg,
X509Certificate2 certificate,
X509Chain x509Chain,
SslPolicyErrors policyErrors)
{
var certificateIsTestCert = certificate.Subject.Equals("O=Internet Widgits Pty Ltd, S=Silicon Valley, C=US");
return certificateIsTestCert && x509Chain.ChainElements.Count == 1 &&
x509Chain.ChainStatus[0].Status == X509ChainStatusFlags.UntrustedRoot;
}
如果您想从 PEM 文件加载私钥,可以使用 Bouncy Castle 轻松完成。例如,要从 PEM 文件中导入一个私钥,然后使用它来创建一个 RSA 实例来对数据或哈希进行签名,您可以像这样获取 RSA 实例:
private static RSA LoadClientPrivateKeyFromPemFile(string clientPrivateKeyFileName)
{
if (!File.Exists(clientPrivateKeyFileName))
{
throw new FileNotFoundException(
"The client private key PEM file could not be found",
clientPrivateKeyFileName);
}
var clientPrivateKeyPemText = File.ReadAllText(clientPrivateKeyFileName);
using var reader = new StringReader(clientPrivateKeyPemText);
var pemReader = new PemReader(reader);
var keyParam = pemReader.ReadObject();
// GET THE PRIVATE KEY PARAMETERS
RsaPrivateCrtKeyParameters privateKeyParams = null;
// This is the case if the PEM file has is a "traditional" RSA PKCS#1 content
// The private key file with begin and end with -----BEGIN RSA PRIVATE KEY----- and -----END RSA PRIVATE KEY-----
if (keyParam is AsymmetricCipherKeyPair asymmetricCipherKeyPair)
{
privateKeyParams = (RsaPrivateCrtKeyParameters)asymmetricCipherKeyPair.Private;
}
// This is to check if it is a Pkcs#8 PRIVATE KEY ONLY or a public key (-----BEGIN PRIVATE KEY----- and -----END PRIVATE KEY-----)
if (keyParam is AsymmetricKeyParameter asymmetricKeyParameter)
{
privateKeyParams = (RsaPrivateCrtKeyParameters)asymmetricKeyParameter;
}
var rsaPrivateKeyParameters = DotNetUtilities.ToRSAParameters(privateKeyParams);
// CREATE A NEW RSA INSTANCE WITH THE PRIVATE KEY PARAMETERS (THIS IS THE PRIVATE KEY)
return RSA.Create(rsaPrivateKeyParameters);
}
最后,如果您想使用私钥(使用上述示例从 PEM 文件获得)对数据进行签名,您可以在 System.Security.Cryptography.RSA class 上使用标准加密和签名方法.例如
var signedData = rsaInstanceWithPrivateKey.SignData(
data,
HashAlgorithmName.SHA256,
RSASignaturePadding.Pkcs1);
...然后在使用 HttpRequestMessage 调用 SendAsync 之前将其作为 ByteArrayContent 添加到 HttpRequestMessage。
var byteArrayContent = new ByteArrayContent(signedData);
var httpRequestMessage = new HttpRequestMessage(
HttpMethod.Post,
"/myapiuri");
httpRequestMessage.Content = byteArrayContent;
您曾提到您使用相同的私钥创建所有内容,因此如果网络服务器端是这种情况,您将能够验证签名并解密您在此示例中从客户端发送的内容.
同样,这里有很多选项和细微差别。
使用 Bouncy Castle PEM reader 您可以使用密码注入 IPasswordFinder 实现。
例如:
/// <summary>
/// Required when using the Bouncy Castle PEM reader for PEM artifacts with passwords.
/// </summary>
class BcPemPasswordFinder : IPasswordFinder
{
private readonly string m_password;
public BcPemPasswordFinder(string password)
{
m_password = password;
}
/// <summary>
/// Required by the IPasswordFinder interface
/// </summary>
/// <returns>System.Char[].</returns>
public char[] GetPassword()
{
return m_password.ToCharArray();
}
}
这是我最初发布的 LoadClientPrivateKeyFromPemFile 的修改版本(在此示例中,为简洁起见,密码是硬编码的),您可以在其中将 IPasswordFinder 注入实例。
private static RSA LoadClientPrivateKeyFromPemFile(string clientPrivateKeyFileName)
{
if (!File.Exists(clientPrivateKeyFileName))
{
throw new FileNotFoundException(
"The client private key PEM file could not be found",
clientPrivateKeyFileName);
}
var clientPrivateKeyPemText = File.ReadAllText(clientPrivateKeyFileName);
using var reader = new StringReader(clientPrivateKeyPemText);
// Instantiate password finder here
var passwordFinder = new BcPemPasswordFinder("P@ssword");
// Pass the IPasswordFinder instance into the PEM PemReader...
var pemReader = new PemReader(reader, passwordFinder);
var keyParam = pemReader.ReadObject();
// GET THE PRIVATE KEY PARAMETERS
RsaPrivateCrtKeyParameters privateKeyParams = null;
// This is the case if the PEM file has is a "traditional" RSA PKCS#1 content
// The private key file with begin and end with -----BEGIN RSA PRIVATE KEY----- and -----END RSA PRIVATE KEY-----
if (keyParam is AsymmetricCipherKeyPair asymmetricCipherKeyPair)
{
privateKeyParams = (RsaPrivateCrtKeyParameters)asymmetricCipherKeyPair.Private;
}
// This is to check if it is a Pkcs#8 PRIVATE KEY ONLY or a public key (-----BEGIN PRIVATE KEY----- and -----END PRIVATE KEY-----)
if (keyParam is AsymmetricKeyParameter asymmetricKeyParameter)
{
privateKeyParams = (RsaPrivateCrtKeyParameters)asymmetricKeyParameter;
}
var rsaPrivateKeyParameters = DotNetUtilities.ToRSAParameters(privateKeyParams);
// CREATE A NEW RSA INSTANCE WITH THE PRIVATE KEY PARAMETERS (THIS IS THE PRIVATE KEY)
return RSA.Create(rsaPrivateKeyParameters);
}
我需要创建一个 c# 应用程序,它必须使用 SSL 向服务器发送 API 请求。我需要创建客户端身份验证。我已经有了服务器 CA 证书、客户端证书 (cer)、客户端私钥 (pem) 和密码。我找不到有关如何创建客户端连接的示例。有人可以建议我从哪里开始解释一段小代码吗?在我手中,我有客户端证书 (PEM)、客户端代理密钥和客户端密钥的密码。我不知道从哪里开始编写向服务器发送请求的代码
前段时间我创建了 this POC for client authentication with certificate in .Net Core. It uses idunno.Authentication package that is now build-in in .Net Core。我的 POC 现在可能有点过时了,但它对你来说可能是一个很好的起点。
首先创建一个扩展方法来添加证书到HttpClientHandler
:
public static class HttpClientHandlerExtensions
{
public static HttpClientHandler AddClientCertificate(this HttpClientHandler handler, X509Certificate2 certificate)
{
handler.ClientCertificateOptions = ClientCertificateOption.Manual;
handler.ClientCertificates.Add(certificate);
return handler;
}
}
然后另一种扩展方法将证书添加到IHttpClientBuilder
public static IHttpClientBuilder AddClientCertificate(this IHttpClientBuilder httpClientBuilder, X509Certificate2 certificate)
{
httpClientBuilder.ConfigureHttpMessageHandlerBuilder(builder =>
{
if (builder.PrimaryHandler is HttpClientHandler handler)
{
handler.AddClientCertificate(certificate);
}
else
{
throw new InvalidOperationException($"Only {typeof(HttpClientHandler).FullName} handler type is supported. Actual type: {builder.PrimaryHandler.GetType().FullName}");
}
});
return httpClientBuilder;
}
然后加载证书并在HttpClientFactory
HttpClient
var cert = CertificateFinder.FindBySubject("your-subject");
services
.AddHttpClient("ClientWithCertificate", client => { client.BaseAddress = new Uri(ServerUrl); })
.AddClientCertificate(cert);
现在,当您使用工厂创建的客户端时,它会自动将您的证书与请求一起发送;
public async Task SendRequest()
{
var client = _httpClientFactory.CreateClient("ClientWithCertificate");
....
}
这里有很多选项,所以根据问题的简洁性,我不能 100% 确定要走哪条路。我创建了一个基本的 aspnet.core WebApi 项目,其中包含“天气预报”控制器作为测试。这里没有显示很多错误检查,并且有很多关于密钥和证书如何存储或不存储的假设,甚至 OS 这是为了什么(不是 OS 一样重要,但密钥存储不同)。
另请注意,使用 OpenSsl 创建的证书不包含 Web 服务器证书中的私钥。为此,您必须将证书和私钥组合成 Pkcs12/PFX 格式。
例如(对于 Web 服务器,不一定是客户端,但您真的可以在任何地方使用 PFX...)。
openssl pkcs12 -export -out so-selfsigned-ca-root-x509.pfx -inkey so-root-ca-rsa-private-key.pem -in so-selfsigned-ca-root-x509.pem
考虑控制台应用程序中的这个 Main 方法。我添加到其中的唯一非 BCL 包(用于 PEM 私钥)是 Portable.BouncyCastle。如果您使用的是 .NET Core 5.0(几天前发布),那里有 PEM 选项。假设您还没有,这个示例使用的是 NetCoreApp 3.1。
The appSettings.json example file:
{
"HttpClientRsaArtifacts": {
"ClientCertificateFilename": "so-x509-client-cert.pem",
"ClientPrivateKeyFilename": "so-client-private-key.pem"
}
}
private static async Task Main(string[] args)
{
IConfiguration config = new ConfigurationBuilder().AddJsonFile("appSettings.json").Build();
const string mainAppSettingsKey = "HttpClientRsaArtifacts";
var clientCertificateFileName = config[$"{mainAppSettingsKey}:ClientCertificateFilename"];
var clientPrivKeyFileName = config[$"{mainAppSettingsKey}:ClientPrivateKeyFilename"];
var clientCertificate = new X509Certificate2(clientCertificateFileName);
var httpClientHandler = new HttpClientHandler();
httpClientHandler.ClientCertificates.Add(clientCertificate);
httpClientHandler.ClientCertificateOptions = ClientCertificateOption.Manual;
httpClientHandler.ServerCertificateCustomValidationCallback = ByPassCertErrorsForTestPurposesDoNotDoThisInTheWild;
httpClientHandler.CheckCertificateRevocationList = false;
var httpClient = new HttpClient(httpClientHandler);
httpClient.BaseAddress = new Uri("https://localhost:5001/");
var httpRequestMessage = new HttpRequestMessage(
HttpMethod.Get,
"weatherforecast");
// This is "the connection" (and API call)
using var response = await httpClient.SendAsync(
httpRequestMessage,
HttpCompletionOption.ResponseHeadersRead);
var stream = await response.Content.ReadAsStreamAsync();
var jsonDocument = await JsonDocument.ParseAsync(stream);
var options = new JsonSerializerOptions
{
WriteIndented = true,
};
Console.WriteLine(
JsonSerializer.Serialize(
jsonDocument,
options));
}
private static bool ByPassCertErrorsForTestPurposesDoNotDoThisInTheWild(
HttpRequestMessage httpRequestMsg,
X509Certificate2 certificate,
X509Chain x509Chain,
SslPolicyErrors policyErrors)
{
var certificateIsTestCert = certificate.Subject.Equals("O=Internet Widgits Pty Ltd, S=Silicon Valley, C=US");
return certificateIsTestCert && x509Chain.ChainElements.Count == 1 &&
x509Chain.ChainStatus[0].Status == X509ChainStatusFlags.UntrustedRoot;
}
如果您想从 PEM 文件加载私钥,可以使用 Bouncy Castle 轻松完成。例如,要从 PEM 文件中导入一个私钥,然后使用它来创建一个 RSA 实例来对数据或哈希进行签名,您可以像这样获取 RSA 实例:
private static RSA LoadClientPrivateKeyFromPemFile(string clientPrivateKeyFileName)
{
if (!File.Exists(clientPrivateKeyFileName))
{
throw new FileNotFoundException(
"The client private key PEM file could not be found",
clientPrivateKeyFileName);
}
var clientPrivateKeyPemText = File.ReadAllText(clientPrivateKeyFileName);
using var reader = new StringReader(clientPrivateKeyPemText);
var pemReader = new PemReader(reader);
var keyParam = pemReader.ReadObject();
// GET THE PRIVATE KEY PARAMETERS
RsaPrivateCrtKeyParameters privateKeyParams = null;
// This is the case if the PEM file has is a "traditional" RSA PKCS#1 content
// The private key file with begin and end with -----BEGIN RSA PRIVATE KEY----- and -----END RSA PRIVATE KEY-----
if (keyParam is AsymmetricCipherKeyPair asymmetricCipherKeyPair)
{
privateKeyParams = (RsaPrivateCrtKeyParameters)asymmetricCipherKeyPair.Private;
}
// This is to check if it is a Pkcs#8 PRIVATE KEY ONLY or a public key (-----BEGIN PRIVATE KEY----- and -----END PRIVATE KEY-----)
if (keyParam is AsymmetricKeyParameter asymmetricKeyParameter)
{
privateKeyParams = (RsaPrivateCrtKeyParameters)asymmetricKeyParameter;
}
var rsaPrivateKeyParameters = DotNetUtilities.ToRSAParameters(privateKeyParams);
// CREATE A NEW RSA INSTANCE WITH THE PRIVATE KEY PARAMETERS (THIS IS THE PRIVATE KEY)
return RSA.Create(rsaPrivateKeyParameters);
}
最后,如果您想使用私钥(使用上述示例从 PEM 文件获得)对数据进行签名,您可以在 System.Security.Cryptography.RSA class 上使用标准加密和签名方法.例如
var signedData = rsaInstanceWithPrivateKey.SignData(
data,
HashAlgorithmName.SHA256,
RSASignaturePadding.Pkcs1);
...然后在使用 HttpRequestMessage 调用 SendAsync 之前将其作为 ByteArrayContent 添加到 HttpRequestMessage。
var byteArrayContent = new ByteArrayContent(signedData);
var httpRequestMessage = new HttpRequestMessage(
HttpMethod.Post,
"/myapiuri");
httpRequestMessage.Content = byteArrayContent;
您曾提到您使用相同的私钥创建所有内容,因此如果网络服务器端是这种情况,您将能够验证签名并解密您在此示例中从客户端发送的内容.
同样,这里有很多选项和细微差别。
使用 Bouncy Castle PEM reader 您可以使用密码注入 IPasswordFinder 实现。
例如:
/// <summary>
/// Required when using the Bouncy Castle PEM reader for PEM artifacts with passwords.
/// </summary>
class BcPemPasswordFinder : IPasswordFinder
{
private readonly string m_password;
public BcPemPasswordFinder(string password)
{
m_password = password;
}
/// <summary>
/// Required by the IPasswordFinder interface
/// </summary>
/// <returns>System.Char[].</returns>
public char[] GetPassword()
{
return m_password.ToCharArray();
}
}
这是我最初发布的 LoadClientPrivateKeyFromPemFile 的修改版本(在此示例中,为简洁起见,密码是硬编码的),您可以在其中将 IPasswordFinder 注入实例。
private static RSA LoadClientPrivateKeyFromPemFile(string clientPrivateKeyFileName)
{
if (!File.Exists(clientPrivateKeyFileName))
{
throw new FileNotFoundException(
"The client private key PEM file could not be found",
clientPrivateKeyFileName);
}
var clientPrivateKeyPemText = File.ReadAllText(clientPrivateKeyFileName);
using var reader = new StringReader(clientPrivateKeyPemText);
// Instantiate password finder here
var passwordFinder = new BcPemPasswordFinder("P@ssword");
// Pass the IPasswordFinder instance into the PEM PemReader...
var pemReader = new PemReader(reader, passwordFinder);
var keyParam = pemReader.ReadObject();
// GET THE PRIVATE KEY PARAMETERS
RsaPrivateCrtKeyParameters privateKeyParams = null;
// This is the case if the PEM file has is a "traditional" RSA PKCS#1 content
// The private key file with begin and end with -----BEGIN RSA PRIVATE KEY----- and -----END RSA PRIVATE KEY-----
if (keyParam is AsymmetricCipherKeyPair asymmetricCipherKeyPair)
{
privateKeyParams = (RsaPrivateCrtKeyParameters)asymmetricCipherKeyPair.Private;
}
// This is to check if it is a Pkcs#8 PRIVATE KEY ONLY or a public key (-----BEGIN PRIVATE KEY----- and -----END PRIVATE KEY-----)
if (keyParam is AsymmetricKeyParameter asymmetricKeyParameter)
{
privateKeyParams = (RsaPrivateCrtKeyParameters)asymmetricKeyParameter;
}
var rsaPrivateKeyParameters = DotNetUtilities.ToRSAParameters(privateKeyParams);
// CREATE A NEW RSA INSTANCE WITH THE PRIVATE KEY PARAMETERS (THIS IS THE PRIVATE KEY)
return RSA.Create(rsaPrivateKeyParameters);
}