【问题标题】:SSL Client Authentication with certificate using c#使用 c# 使用证书进行 SSL 客户端身份验证
【发布时间】:2021-02-11 15:18:05
【问题描述】:

我需要创建一个 C# 应用程序,该应用程序必须使用 SSL 将 API 请求发送到服务器。我需要创建客户端身份验证。我已经拥有服务器 CA 证书、客户端证书 (cer)、客户端私钥 (pem) 和密码。我找不到有关如何创建客户端连接的示例。有人可以建议我从一个很好解释的小代码开始吗?在我手中,我有客户端证书 (PEM)、客户端证明密钥和客户端密钥的密码。我不知道从哪里开始编写向服务器发送请求的代码

【问题讨论】:

  • 您应该为 SSL 使用 TLS 1.2/1.3。没有为连接做任何特别的事情。如果在发出 HTTP 请求之前 URL 包含 HTTPS,则会自动执行 TLS 身份验证。所以你可以使用任何http客户端。在某些情况下,如果您的操作系统没有自动尝试 1.2/1.3,您必须专门添加一条指令来指定 TLS 版本。
  • 看看here.
  • 嗨 Lorenzo,关于客户端私钥,您是否尝试对希望通过 HTTPS 发送到服务器的数据进行签名? CA 证书是自签名的吗?它是否用于创建 X509 CA 证书和客户端证书?
  • @ivnext 是的,CA 证书是自签名的,用于创建其他证书。数据已签名

标签: c# authentication ssl ssl-certificate client


【解决方案1】:

前段时间,我创建了this POC,用于在.Net Core 中使用证书进行客户端身份验证。它使用idunno.Authentication 包,现在是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");
    ....
}

【讨论】:

  • 我会尽快检查!
  • 官方文档中的 This(和下一个)部分展示了如何通过请求发送证书,之前的所有部分都描述了如何配置服务器端以要求和验证它。
  • 如果可能的话,我会在几天内检查,如果有效,我会感谢你
【解决方案2】:

这里有很多选择,所以根据问题的简洁性,我不能 100% 确定该走哪条路。我创建了一个基本的 aspnet.core WebApi 项目,其中有“天气预报”控制器作为测试。这里没有显示很多错误检查,并且有很多关于如何存储或不存储密钥和证书的假设,甚至是用于什么操作系统(不是操作系统很重要,而是关键商店不同)。

另请注意,使用 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 类上的标准加密和签名方法。例如

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 阅读器,您可以使用密码注入 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);
}

【讨论】:

  • 在我使用 Curl 的那一刻测试 API,它可以工作,我正在使用这个 curl 命令。 : curl -k --key LMIS_JPN_CLIENT_CERT_KEY.pem --cert LMIS_JPN_CLIENT_CERT_PEM.cer:pass1556677 -H "Accept: application/json" 127.0.0.1/SiteInfo
  • 该行只是将私钥文件名的名称从示例顶部显示的 appSettings.json 中提取出来。您还可以硬编码私钥文件名的名称(只是一个配置选项)。在高层次上,私钥用于[签名]数据(或它的哈希),这使具有密钥对的相应公钥的接收者保证(例如)数据是由所有者发送的私钥。它是加密的另一面,通常使用公钥加密数据,只有私钥可以解密。
  • 我很沮丧...使用 python 我可以用一行来管理所有内容(在这种情况下,我删除了密码)对于 c# 不存在类似的东西吗?... requests.get(' 127.0.0.1/SiteInfo', cert=('cer.cer', 'pem.pem'), verify=False)
  • C# 有点“接近金属”,可以这么说。通用语言的一个优势在于,如果它不存在开箱即用的情况下,您几乎可以创建您想要的任何自定义 API(例如,类似于您的表达式的东西)。但这确实需要一些努力。我不知道您的示例中显示的类似 Python 的 API 之类的东西,但可能有人编写的 NUGET 包与该语法相似。
  • 对不起,如果我回到 clientPrivKeyFileName 。现在,几个小时后 :-) 我删除了密码.. 所以现在我应该使用证书文件和 provateKey 文件。在您的第一个示例中,分配了 clientPrivKeyFileName 但从未在代码中使用过。
猜你喜欢
  • 2014-04-04
  • 2012-01-10
  • 1970-01-01
  • 1970-01-01
  • 2015-10-29
  • 2018-07-05
  • 2019-01-14
  • 2012-04-24
  • 1970-01-01
相关资源
最近更新 更多