1

聊一聊HTTPS双向认证的简单应用 - Catcher8

 1 year ago
source link: https://www.cnblogs.com/catcher1994/p/17122324.html
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

聊一聊HTTPS双向认证的简单应用

在三方接口对接中,偶尔会遇到需要传递证书的情况,这种方式其实是在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;
}

到这里的话,服务端已经可以了。

这个时候从浏览器访问,大概会看到这个提示。

558945-20230215114915924-1357701405.png

下面写个控制台用 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;,不过并不建议这样操作。

下面是运行的结果,是可以正常访问并返回结果的。

558945-20230215114926515-765378593.png

我们再换一张不是同一个根证书的客户端证书。

558945-20230215114934824-652670248.png

不出意外的不能正常访问。

不过上面这种情况在实际应用的时候会偏少一点,大部分还是会挂在反向代理或云负载均衡上面的。

下面先来看看 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。

558945-20230215114948435-438316477.png

用上面的控制台程序,再访问看看。

558945-20230215114954592-218688050.png

正确的证书,可以正常返回,错误的证书会返回 400 The SSL certificate error。

基于反向代理的话,操作起来就简单了一点。

如果是云负载均衡,只需要按他们的要求上传对应的证书即可。

讲了 nginx,不讲讲 IIS,好像有点说不过去。

那就再看看 IIS 的配置吧。

IIS 部署

在 Windows 服务器安装好 IIS 和托管捆绑包后,要先把我们的根证书安装到可信的根证书里面。

558945-20230215224046023-1640133718.png

然后进行部署,绑定好服务端证书后,确认可以正常访问。

558945-20230215115007393-1241186656.png

然后进行双向认证的配置。

在对应站点上面的 SSL 配置,把 要求 SSL必需 两个勾上即可。

558945-20230215115012785-945159161.png
558945-20230215115018462-943800345.png

后面再访问的时候,就会提示选择证书

558945-20230215115027194-303006332.png

选择正确的证书后就可以正常访问了。

然后我们再用前面的控制台程序访问,结果如下。

558945-20230215115035100-1429718180.png

可以发现和前面的结果是一样的,不同的是错误返回的内容不一样。

上面提到的都是一些自建的场景,其实对云负载均衡的结合使用也是 OK 的。

双向认证,在一些安全要求比较高的场景下,用途还是比较大的,相比较单向认证的话会麻烦一些。

本文示例代码:https://github.com/catcherwong-archive/2023/tree/main/MutualTLSAuthentication


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK