用C#做HTTPS证书的过期时间检测

背景

遇到过几次在对接第三方时,由于对方域名证书过期,导致请求失败。

虽然现在云平台都有提供证书过期的告警,但是主要维度是针对证书级别的,不是针对某个具体域名,所以这种情况下,会容易出现漏更新的情况,所以还是要有一些额外的手段来辅助。

下面老黄介绍一下用 C# 实现的几种方式来检测证书的过期时间。.

方式一 HttpClient

在用 HttpClient 时,我们有些情况会忽略 ssl 证书的验证,也就是下面这段代码

var httpclientHandler = new HttpClientHandler();
httpclientHandler.ServerCertificateCustomValidationCallback = (sender, cert, chain, error) => true;
var httpClient = new HttpClient(httpclientHandler);

在这个时候,无论被调用方的证书是否有问题,都可以成功请求。

ServerCertificateCustomValidationCallback 这个 Func 就是让客户端进行服务端证书检验的一个回调,可以添加很多自定义的逻辑进去。

下面是它的定义:

public Func<HttpRequestMessage, X509Certificate2?, X509Chain?, SslPolicyErrors, bool>? ServerCertificateCustomValidationCallback { get; set; }

我们就可以通过 X509Certificate2 来进行服务端证书过期时间的判断了。

static bool ValidateCertificate(object sender, X509Certificate? certificate, X509Chain? chain, SslPolicyErrors sslPolicyErrors)
{
    if (certificate == null) return true;

    var expirationDate = DateTime.Parse(certificate.GetExpirationDateString(), CultureInfo.InvariantCulture);
    
    // 提前 x 天预警,正常是提前一个月
    if (expirationDate - DateTime.Today < TimeSpan.FromDays(30))
    {
        throw new NeedRenewException("It's time to renew the certificate!");
    }
    
    if (sslPolicyErrors == SslPolicyErrors.None)
    {
        return true;
    }
    else
    {
        throw new CertPolicyException($"Cert policy errors: {sslPolicyErrors.ToString()}");
    }
}

最后把上面的 ValidateCertificate 加到 HttpClientHandler 里面即可。

static async Task HttpClientSSLCheck(string domain)
{
    HttpClientHandler clientHandler = new()
    {
        ServerCertificateCustomValidationCallback = (sender, cert, chain, sslPolicyErrors) 
                => ValidateCertificate(sender, cert, chain, sslPolicyErrors)
    };

    try
    {
        using CancellationTokenSource cts = new(3000);
        using HttpClient client = new(clientHandler);
        await client.GetAsync($"https://{domain}", cts.Token);

        Console.WriteLine($"{domain} is OK {nameof(HttpClientSSLCheck)}");
    }
    catch (TaskCanceledException)
    {
        Console.WriteLine("canceled");
    }
    catch (Exception e)
    {
        // 集成企微、钉钉或其他类型的通知
        if (e.InnerException is CertPolicyException)
        {
            Console.WriteLine($"{domain} | ex = {e.InnerException.Message}");
        }
        else if (e.InnerException is NeedRenewException)
        {
            Console.WriteLine($"{domain} | need to renew !!");
        }
        else
        {
            Console.WriteLine($"{domain} | ex = {e.Message}");
        }
    }
}

最后就是调用

await HttpClientSSLCheck("github.com");

用C#做HTTPS证书的过期时间检测

方式二 TcpClient

TcpClient 相对 HttpClient 的话会稍微底层一点点,需要借助 SslStream 来完成证书的校验。

NetworkStream -> SslStream

这里会复用上面的 ValidateCertificate 方法。

static async Task TcpClientSSLCheck(string domain)
{
    try
    {
        using TcpClient tcpClient = new();
        using CancellationTokenSource cts = new(3000);
        await tcpClient.ConnectAsync(domain, 443, cts.Token);

        using var stream = tcpClient.GetStream();
        using SslStream ssl = new(stream, false, ValidateCertificate);
        await ssl.AuthenticateAsClientAsync(domain);

        Console.WriteLine($"{domain} is OK {nameof(TcpClientSSLCheck)}");
    }
    catch (TaskCanceledException)
    {
        Console.WriteLine("canceled");
    }
    catch (Exception e)
    {
        HandleException(domain, e);
    }
}

效果和前面是一样的。

方式三 Socket

Socket 相对 TcpClient 和 HttpClient 的话就更加底层一点了,同样需要借助 SslStream 来完成证书的校验。

本质上也是 NetworkStream -> SslStream

static async Task SocketSSLCheck(string domain)
{
    try
    {
        using Socket socket = new(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
        using CancellationTokenSource cts = new(3000);
        await socket.ConnectAsync(domain, 443, cts.Token);

        using SslStream ssl = new (new NetworkStream(socket, false), false, ValidateCertificate);
        await ssl.AuthenticateAsClientAsync(domain);

        Console.WriteLine($"{domain} is OK {nameof(SocketSSLCheck)}");
    }
    catch (TaskCanceledException)
    {
        Console.WriteLine("canceled");
    }
    catch (Exception e)
    {
        HandleException(domain, e);
    }
}

最后来看看三个方式的使用情况

用C#做HTTPS证书的过期时间检测

 

总结

上面的是单个域名的实现,有可能现实场景中我们会针对某个域名下面的所有二级域名进行一次检测,避免出现漏网之鱼,那么这个时候就可以结合域名提供商提供的 API 接口,获取所有的解析,组装二级域名,然后依次去判断证书过期情况。

最后也可以结合 dotnet-tool + 定时任务的方式做到检查和通知的方式。