要調查網站 TLS 憑證無效問題,最直接做法是從瀏覽器安全連線圖示逐步點開檢視憑證內容:

憑證檢視器會顯示憑證信任鏈,可查看根憑證、中繼憑證及伺服器憑證的詳細資料,但要知道憑證的主體(Subject)、簽署者(Issuer)、有效期間、指紋等資訊,需逐一點開才能看到。

今天處理一個案例,需要比對兩個網站憑證的細節差異,得不斷來回點擊參照,一串頻繁的重複操作下來,不意外地又熔斷了現代王藍田的理智線。暗! 為什麼沒有 CLI 工具能一次檢視網站提供所有憑證的資訊?

是的,如同大家猜想,程式魔人回家第一件事便是打開 VSCode,取出 Github Copilot 魔杖,計劃用 .NET 寫個 CLI 小工具解決困擾。

openssl s_client -connect www.google.com:443 -showcerts 是取得網站傳回憑證最簡單有效的做法,其輸出結果如下,每段 BEGIN CERTIFICATE 與 END CERTIFICATE 包夾部分就是一張憑證,以 Google 網站為例,共有 www.google.com(網站憑證)、GTS CA 1C3(中繼 CA 憑證)、GTS Root R1(根憑證) 三張。

CONNECTED(000001B4)
---
Certificate chain
 0 s:CN = www.google.com
   i:C = US, O = Google Trust Services LLC, CN = GTS CA 1C3
-----BEGIN CERTIFICATE-----
MIIEiDCCA3CgAwIBAgIRANPXtQiQspt9EsIFgI8MDrQwDQYJKoZIhvcNAQELBQAw
...略...
M1bCMmPJnvXxJpPgZqEHuRvDLMMo9lx86KCdiYxo4nDAajRN1mO+oR2m4bgY/qb/
lKQFXifOAi4n4tEn
-----END CERTIFICATE-----
 1 s:C = US, O = Google Trust Services LLC, CN = GTS CA 1C3
   i:C = US, O = Google Trust Services LLC, CN = GTS Root R1
-----BEGIN CERTIFICATE-----
MIIFljCCA36gAwIBAgINAgO8U1lrNMcY9QFQZjANBgkqhkiG9w0BAQsFADBHMQsw
... 略 ...
1IXNDw9bg1kWRxYtnCQ6yICmJhSFm/Y3m6xv+cXDBlHz4n/FsRC6UfTd
-----END CERTIFICATE-----
 2 s:C = US, O = Google Trust Services LLC, CN = GTS Root R1
   i:C = BE, O = GlobalSign nv-sa, OU = Root CA, CN = GlobalSign Root CA
-----BEGIN CERTIFICATE-----
MIIFYjCCBEqgAwIBAgIQd70NbNs2+RrqIQ/E8FjTDTANBgkqhkiG9w0BAQsFADBX
... 略 ...
d0lIKO2d1xozclOzgjXPYovJJIultzkMu34qQb9Sz/yilrbCgj8=
-----END CERTIFICATE-----
---
Server certificate
subject=CN = www.google.com

issuer=C = US, O = Google Trust Services LLC, CN = GTS CA 1C3

---
No client certificate CA names sent
Peer signing digest: SHA256
Peer signature type: ECDSA
Server Temp Key: X25519, 253 bits
---
SSL handshake has read 4295 bytes and written 396 bytes
Verification: OK
---
New, TLSv1.3, Cipher is TLS_AES_256_GCM_SHA384
Server public key is 256 bit
Secure Renegotiation IS NOT supported
Compression: NONE
Expansion: NONE
No ALPN negotiated
Early data was not sent
Verify return code: 0 (ok)
---
HTTP/1.0 400 Bad Request
Content-Type: text/html; charset=UTF-8
Referrer-Policy: no-referrer
Content-Length: 1555
Date: Tue, 17 Oct 2023 14:17:16 GMT

...以下省略...

將 BEGIN CERTIFICATE 到 END CERTIFICATE 內容取出存成 .cer,可在 Windows 檔案總管點開檢視。(Windows 的憑證檢視方式比瀏覽器憑證檢視器友善,能一次看到指紋以外的主要資訊)

OK,接下來我們簡單寫幾行程式,用 Process 跑 openssl -showcerts 接收輸出內容用 Regex 取出憑證,用 X509Certificate2 物件解析輸出,在 Github Copilot 的強大火力支援下,輕輕鬆鬆只用了 32 行搞定。
註:Console.WriteLine 時有順便用 ANSI 顏色控制 突顯比對重點。

using System.Diagnostics;
using System.Security.Cryptography.X509Certificates;
using System.Text.RegularExpressions;
var hostPort = args.FirstOrDefault() ?? "www.google.com:443";
if (!hostPort.Contains(":")) hostPort += ":443";
var p = new Process();
p.StartInfo.FileName = "openssl";
p.StartInfo.Arguments = $"s_client -showcerts -connect {hostPort}";
p.StartInfo.UseShellExecute = false;
p.StartInfo.RedirectStandardOutput = true;
p.StartInfo.RedirectStandardInput = true;
p.StartInfo.RedirectStandardError = true;
p.Start();
p.StandardInput.WriteLine("Q"); //按Q+Enter結束openssl
var output = p.StandardOutput.ReadToEnd();
p.WaitForExit();
Func<string, string> highlightCN = 
    (s) => {
        var m = Regex.Match(s, @"^CN=[^,]+");
        if (!m.Success) return s;
        return "\x1b[33;1m" + m.Value + "\x1b[0m" + s.Substring(m.Value.Length);
    };
foreach (Match certB64 in Regex.Matches(output, "(?ims)-----BEGIN CERTIFICATE-----(?<c>.+?)-----END CERTIFICATE-----"))
{
    var pem = certB64.Groups["c"].Value.Replace("\r", "").Replace("\n", "");
    var cert = new X509Certificate2(Convert.FromBase64String(pem));
    Console.WriteLine($"\x1b[36m{new String('=', 64)}\x1b[0m");
    Console.WriteLine($"發給: {highlightCN(cert.Subject)}");
    Console.WriteLine($"簽發者: {highlightCN(cert.Issuer)}");
    Console.WriteLine($"有效期自 \x1b[33m{cert.NotBefore:yyyy-MM-dd HH:mm:ss}\x1b[0m 到 \x1b[33m{cert.NotAfter:yyyy-MM-dd HH:mm:ss}\x1b[0m");
    Console.WriteLine($"憑證指紋: \x1b[33;3m{cert.Thumbprint}\x1b[0m");
    Console.WriteLine();
}

執行結果如下,讚!

Another coding for fun work to display the TLS certificates returned from web with .NET console application.


Comments

# by kkbruce

如果是已經匯入的憑證,Powershell 的certificate provider是好物。 https://learn.microsoft.com/en-us/powershell/module/microsoft.powershell.security/about/about_certificate_provider?view=powershell-7.3&WT.mc_id=DOP-MVP-4038201

# by Jeffrey

to kkbruce, 感謝分享。

Post a comment