在 Day 30 中,我們學習了數位簽章(如 Ed25519 與 RSA-PSS)如何確保資料的完整性與不可否認性。然而,非對稱密碼學面臨著一個致命的信任前提:「你如何確定手上的這把公鑰,真的屬於你以為的那個人或機構?」
如果攻擊者 Eve 在網路傳輸中發送了自己的公鑰,並宣稱自己是 google.com 或 bank.com.tw,那麼對 Eve 公鑰進行的任何簽章驗證都會順利通過。這就是經典的中間人攻擊(Man-in-the-Middle Attack, MITM)。
為了解決「公鑰真實性」的根本問題,密碼學界建立了 公鑰基礎設施(Public Key Infrastructure, PKI) 體系,透過數位憑證將「身份(Identity)」與「公鑰(Public Key)」進行具備法律與技術效力的綁定。本章將剖析 數位憑證本質、X.509 憑證結構、CSR 簽發流程、信任鏈(Chain of Trust) 以及 台灣在地化的 PKI 憑證生態系。
如果把非對稱密碼學比喻為建立安全通道的基石,那麼 數位憑證(Digital Certificate,簡稱憑證) 就是這座建築的「數位身分證」或「網路護照」。
在日常生活裡,如果有人自稱是警察,你不會只憑他身上的對講機(公鑰)就相信他,而是會要求看他的「警察證(憑證)」,並確認上面有「警政署(可信任的發照機構,CA)」的防偽印章與簽名。
數位憑證的本質,就是一個經由可信任第三方機構(Certificate Authority, CA)簽署的電子檔案。它的核心使命只有一個:權威地證明「某把公鑰,確實屬於某個特定的人、公司或網站域名」。
【實體世界:護照】 【數位世界:X.509 憑證】
┌──────────────────────────┐ ┌──────────────────────────┐
│ 持有者照片 & 個人資料 │ │ 伺服器域名 (example.com) │
│ 外交部發行與官方防偽鋼印 │ <====> │ 持有者的公鑰 (Public Key)│
│ 有效期限 (Issue/Expiry) │ │ CA 的數位簽章 (Signature)│
└──────────────────────────┘ │ 有效期限 (Not Before/After)│
└──────────────────────────┘
[www.example.com](https://www.example.com)、公司統一編號或個人姓名)。PKI 體系中最廣泛使用的憑證格式為 X.509 v3 標準。一個標準的 X.509 v3 憑證包含以下核心欄位:
+-----------------------------------------------------------------------+
| X.509 v3 Certificate |
+-----------------------------------------------------------------------+
| Version : v3 (0x02) |
| Serial Number : 0x4A8F9E... (CA 簽發的唯一隨機序號) |
| Signature Algorithm : ecdsa-with-SHA256 / sha256WithRSAEncryption|
| Issuer (簽發者/CA 資訊) : CN=DigiCert Global Root G2, O=DigiCert Inc|
| Validity (有效期限) : Not Before: 2026-01-01, Not After: 2027-01-01|
| Subject (持有者/主體資訊) : CN=www.example.com, O=Example Corp |
| Subject Public Key Info : Algorithm: EC (secp256r1) |
| Public Key: 04:A1:8F... |
| Extensions (v3 擴充欄位) : |
| ├── Basic Constraints : CA:FALSE (標記此憑證是否能簽發其他憑證) |
| ├── Key Usage : Digital Signature, Key Encipherment |
| ├── Extended Key Usage : TLS Web Server Authentication |
| └── Subject Alt Name : DNS:example.com, DNS:*.example.com (SAN) |
+-----------------------------------------------------------------------+
| Signature Value (CA 對上述內容的數位簽章) |
| e.g. 30:45:02:21:00:8F... |
+-----------------------------------------------------------------------+
| 欄位名稱 | 工程定義與安全性作用 |
|---|---|
| Serial Number | CA 分配的唯一序號。必須採用高熵隨機數,防範憑證碰撞攻擊。 |
| Basic Constraints | 最重要的安全擴充。CA:TRUE 代表該憑證為 CA,可簽發下級憑證;CA:FALSE 代表終端憑證(Leaf Cert)。若驗證端未校驗此欄位,攻擊者可用一般網站憑證偽造為 CA 並簽發任意域名憑證。 |
| Subject Alternative Name (SAN) | 現代瀏覽器與 TLS 用戶端強制校驗的欄位。記載憑證適用的所有域名(如 *.example.com)。傳統的 Common Name (CN) 已被棄用。 |
| Key Usage / EKU | 限制金鑰用途(如僅限「Web 伺服器身份驗證」或「電子簽章」),落實權限最小化原則。 |
申請數位憑證時,金鑰對(Key Pair)的生成與維護遵守最高原則:私鑰永遠在本地生成,且絕對不能傳送給 CA 或離機!
為此,申請者必須生成 CSR(憑證簽署申請書,RFC 2986 / PKCS#10) 並傳送給 CA:
[ 申請者伺服器 (Subscriber) ] [ 憑證頒發機構 (CA) ]
│ │
├─ 1. 本地生成金鑰對 (k_priv, Q_pub) │
│ 👉 私鑰 k_priv 保留於本地 │
├─ 2. 填寫身份資訊 (Subject DN, SAN) │
├─ 3. 用自己的 k_priv 對 (資訊+Q_pub) 簽署 ──> 產生 CSR │
│ │
├───────────────────── 4. 傳送 CSR ──────────────────────>│
│ │
│ ├─ 5. 驗證 CSR 內部的 PoP 簽章
│ ├─ 6. 執行身份審驗 (DV / OV / EV)
│ └─ 7. 使用 CA 私鑰對 Q_pub 簽署
│<────────────────── 8. 發還 X.509 憑證 ───────────────────┤
在瞭解 PKI 通用理論後,我們聚焦於台灣實務開發(如政府報稅、公文系統、公司登記、證券下單與醫院電子病歷)中最常見的在地化 PKI 體系:
[ 台灣 PKI 信任體系 ]
│
┌──────────────────────────────────┴──────────────────────────────────┐
▼ ▼
【政府網際網路單一窗口 PKI (GPKI)】 【金融與證券 PKI (Financial PKI)】
│ │
├── 國家級根簽發機構 (GRCA) └── 台灣網路認證 (TWCA)
│ │
├── 內政部自然人憑證 (MOICA) ───► 個人身份認證、報稅、健保/勞保 └── 證期憑證 / 金融憑證
├── 經濟部工商憑證 (MOEACA) ───► 法人/公司憑證 (載有統編)、電子發票、採購網 └── 股票/期貨高安規下單簽章
└── 衛福部醫事憑證 (HCA) ───► 醫師/護理師電子病歷簽章
台灣政府建立了 GPKI (Government PKI) 體系,頂層為國家級根簽發機構(GRCA, Government Root CA),其 Root 憑證預先內建於常見作業系統與 Java cacerts 中。GPKI 下轄多個專用 CA:
Subject 欄位中強制記載公司統一編號(Tax ID)。作業系統與 JVM(如 cacerts)不可能預先內建全球數百萬個網站的憑證,而是採用分層信任鏈(Chain of Trust):
[ Root CA Certificate (根憑證) ] ────────── 自簽憑證 (Self-Signed)
│ 預先內建於 OS / Java TrustStore
▼ (使用 Root CA 私鑰簽署)
[ Intermediate CA Certificate (中繼憑證) ] ── 隔離保護 Root CA 私鑰
│ 平時在線執行日常簽發業務
▼ (使用 Intermediate CA 私鑰簽署)
[ End-Entity / Leaf Certificate (終端憑證) ] ─ 安裝於 Web 伺服器 (例如 google.com)
Basic Constraints 為 CA:FALSE,不可再簽發下級憑證。當憑證私鑰洩漏或公司倒閉時,CA 必須廢止該憑證:
以下程式碼演示如何使用 Java 原生 JCA (java.security.cert.CertificateFactory) 解析 PEM 格式憑證、提取 SAN 欄位,並校驗憑證效期與數位簽章:
import java.io.ByteArrayInputStream;
import java.nio.charset.StandardCharsets;
import java.security.PublicKey;
import java.security.cert.CertificateFactory;
import java.security.cert.X509Certificate;
import java.util.Collection;
import java.util.List;
public class X509CertificateValidatorExample {
// 模擬 PEM 格式的範例 X.509 憑證
private static final String SAMPLE_PEM_CERTIFICATE =
"-----BEGIN CERTIFICATE-----\n" +
"MIIA/TCCAWWgAwIBAgIUY3a123456789ABCDEF01MAoGCCqGSM49BAMCMDExCzAJ\n" +
"BgNVBAYTAlRXIxEzARBgNVBAoMCkRlbW8gQ0EgSW5jMRMwEQYDVQQDDApEZW1v\n" +
"IFJvb3QgQ0EwHhcNMjYwMTAxMDAwMDAwWhcNMjcwMTAxMDAwMDAwWjA1MQswCQYD\n" +
"VQQGE3JUVzETMBEGA1UECgwKRGVtbyBPcmcxFTATBgNVBAMMDGV4YW1wbGUuY29t\n" +
"MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAE7p9K8L3X9Z8mY... [省略] ...\n" +
"-----END CERTIFICATE-----";
public static void main(String[] args) throws Exception {
System.out.println("=== Day 31 - Java JCA X.509 憑證解析與驗證實戰 ===");
// 1. 初始化 X.509 憑證工廠
CertificateFactory certFactory = CertificateFactory.getInstance("X.509");
// 2. 解析 PEM 憑證為 X509Certificate 物件
ByteArrayInputStream certInputStream = new ByteArrayInputStream(SAMPLE_PEM_CERTIFICATE.getBytes(StandardCharsets.UTF_8));
X509Certificate certificate = (X509Certificate) certFactory.generateCertificate(certInputStream);
// 3. 提取並印出憑證核心欄位
System.out.println("\n--- [ 憑證基本欄位解析 ] ---");
System.out.println("憑證版本 (Version) : v" + certificate.getVersion());
System.out.println("憑證序號 (Serial Number): " + certificate.getSerialNumber().toString(16));
System.out.println("簽章演算法 (Sig Algo) : " + certificate.getSigAlgName());
System.out.println("主體 DN (Subject DN) : " + certificate.getSubjectX500Principal().getName());
System.out.println("簽發者 DN (Issuer DN) : " + certificate.getIssuerX500Principal().getName());
System.out.println("生效日期 (Not Before) : " + certificate.getNotBefore());
System.out.println("到期日期 (Not After) : " + certificate.getNotAfter());
// 4. 解析 SAN (Subject Alternative Name) 擴充欄位
System.out.println("\n--- [ 擴充欄位: Subject Alternative Names (SAN) ] ---");
Collection<List<?>> sanEntries = certificate.getSubjectAlternativeNames();
if (sanEntries != null) {
for (List<?> entry : sanEntries) {
Integer type = (Integer) entry.get(0); // 2 代表 DNS Name
String name = (String) entry.get(1);
if (type == 2) {
System.out.println("DNS 域名: " + name);
}
}
} else {
System.out.println("此憑證未配置 SAN 欄位。");
}
// 5. 校驗憑證時間效期
try {
certificate.checkValidity();
System.out.println("憑證時間效期檢查: 有效 (Within Validity Period)");
} catch (Exception e) {
System.err.println("憑證時間效期檢查: 已過期或尚未生效 (" + e.getMessage() + ")");
}
// 6. 驗證數位簽章 (驗證憑證是否由 Issuer 的公鑰簽署)
PublicKey issuerPublicKey = certificate.getPublicKey(); // 若為根憑證,以自身公鑰校驗
try {
certificate.verify(issuerPublicKey);
System.out.println("憑證數位簽章校驗: 成功 (Signature Verified by Issuer Public Key)");
} catch (Exception e) {
System.err.println("憑證數位簽章校驗: 失敗 (" + e.getMessage() + ")");
}
}
}