• 聊一聊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;
    }
    

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

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

    下面写个控制台用 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 的吧。

    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。

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

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

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

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

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

    那就再看看 IIS 的配置吧。

    IIS 部署

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

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

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

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

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

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

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

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

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

    总结

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

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

    参考资料

  • 相关阅读:
    【OPENCV_系列电子PDF图书连载】计算机视觉从入门到精通完整学习路线专栏
    C++之函数指针、指针函数以及相关问题
    原生 JS 实现 HTML 转 Markdown,以及其实现逻辑(html2md.js 或 html2markdown.js)
    华为云确定性运维,为政务云平台稳定可靠运行保驾护航
    Qi标准无线供电模块如何处理噪声抑制语音通讯接收灵敏度
    高级深入--day23
    Coroutines协程示例
    开关电源反激式线圈分析
    以K近邻算法为例,使用网格搜索GridSearchCV优化模型最佳参数
    dubbo的Failed to save registry store file问题
  • 原文地址:https://www.cnblogs.com/catcher1994/p/17122324.html