在 Day 32 中,我們學習了 X.509 憑證結構、CSR 流程與 PKI 信任鏈。然而在工程落地上,金鑰與憑證生成後,最棘手的問題莫過於「如何正確儲存、封裝與跨平台傳輸」。
系統整合時,開發者經常遇到 .pem、.der、.p12 與 .pfx 等各種副檔名。台灣開發者在接觸金融下單(證券/期貨憑證)或企業系統時,也常常手忙腳亂。若不理解底層編碼規範與容器標準,極易引發金鑰解析失敗或資安漏洞。
本章將剖析 ASN.1 / DER / PEM 編碼差異、釐清 PKCS#8 與 PKCS#12 標準,以台灣證期憑證(TWCA .pfx)作為真實案例,並透過原生 Java JCA API 實作 PEM 解析與 PKCS12 金鑰庫(KeyStore)的建立與讀取。
所有非對稱金鑰(RSA/ECC)與 X.509 憑證,底層結構皆由 ASN.1(Abstract Syntax Notation One) 語法定義。DER(Distinguished Encoding Rules) 是將 ASN.1 結構序列化為二進位位元組流(Binary Byte Stream)的標準規則,而 PEM(Privacy-Enhanced Mail) 則是 DER 的 Base64 可讀文字包裝。
[ 金鑰 / 憑證資料 (ASN.1 抽象結構) ]
│
▼ (二進位序列化)
[ DER 格式 : 原始二進位位元組流 (Binary) ]
│
▼ (Base64 編碼 + Header/Footer)
[ PEM 格式 : ASCII 可讀文字字串 (Text) ]
| 比較維度 | DER 格式 (Distinguished Encoding Rules) | PEM 格式 (Privacy-Enhanced Mail) |
|---|---|---|
| 資料形態 | 原始二進位(Binary) | Base64 編碼文字(ASCII Text) |
| 可讀性 | 文字編輯器開啟為亂碼 | 文字編輯器可開啟,帶有 -----BEGIN ...----- |
| 常見副檔名 | .der, .cer, .crt |
.pem, .crt, .key, .pub |
| 工程場景 | Java / Windows 內部原生處理、硬體晶片卡 | OpenSSL、Linux、Kubernetes Secret、HTTP Header |
| 跨系統傳輸 | 易因網路文字編碼轉換(CRLF/LF)毀損 | 極度適合文字傳輸與設定檔/版控嵌入 |
為避免不同系統間的金鑰格式混亂,公鑰密碼學標準(PKCS)與 RFC 對私鑰、公鑰與憑證的打包格式進行了明確規範:
常見 PEM 標頭分類與適用標準:
┌─ 私鑰標準 (Private Key)
│ ├── -----BEGIN RSA PRIVATE KEY----- (PKCS#1: 僅限 RSA 私鑰舊規格)
│ └── -----BEGIN PRIVATE KEY----- (PKCS#8: 通用私鑰標準,支援 RSA/ECC/Ed25519)
│
├─ 公鑰與憑證標準 (Public Key & Cert)
│ ├── -----BEGIN CERTIFICATE----- (X.509 數位憑證)
│ └── -----BEGIN PUBLIC KEY----- (X.509 SubjectPublicKeyInfo 通用公鑰)
│
└─ 綜合封包容器 (Container)
└── PKCS#12 (.p12 / .pfx) (二進位密碼保護容器:含私鑰 + 憑證鏈)
BEGIN RSA PRIVATE KEY),無法存放 ECC 或 Ed25519 金鑰。BEGIN PRIVATE KEY)。內部包含演算法識別碼(Algorithm Identifier)與私鑰主體,能統一封裝 RSA、ECC、Ed25519 等所有非對稱私鑰。Java 原生 KeyFactory 預設僅支援 PKCS#8 格式。PKCS#12(RFC 7292) 是業界標準的密碼保護二進位容器。它能將私鑰與對應的X.509 憑證鏈(Certificate Chain)打包儲存於單一檔案中,並透過對稱加密(如 AES-256)與口令(Password)進行整體保護。
在台灣進行股票或期貨網路交易時,券商會要求客戶下載「證期憑證」(由台灣網路認證公司 TWCA 簽發)。這個憑證檔案副檔名通常為 .pfx,本質就是 PKCS#12 容器。
一個匯出的證期憑證 .pfx 檔案,內部同時存放了以下組件:
┌─────────────────────────────────────────────────────────────────────────┐
│ 證期憑證 PKCS#12 容器 (.pfx / .p12) │
│ (受使用者自訂之憑證密碼加密保護) │
│ │
│ 1. 使用者私鑰 (Private Key) ───► 用於下單時對委託封包進行數位簽章 │
│ 2. X.509 數位憑證 (Certificate) ─► 包含客戶公鑰、身分證字號、TWCA 簽章│
│ 3. CA 中繼憑證鏈 (CA Chain) ────► 建立連回 TWCA Root CA 的信任鏈 │
└─────────────────────────────────────────────────────────────────────────┘
[ 客戶端 (手機下單 App / 電腦) ] [ 券商交易伺服器 (Broker) ]
│ │
├─ 1. 從 .pfx 讀出私鑰與 X.509 憑證 │
├─ 2. 將「下單委託資料」用【私鑰】進行數位簽章 │
├─ 3. 發送 (下單資料 + 數位簽章 + X.509 憑證) ─────────> │
│ │
│ ├─ 4. 取出憑證中的【公鑰】
│ ├─ 5. 驗證簽章是否正確
│ └─ 6. 確認交易具備不可否認性,送出下單
觀念澄清:券商伺服器在開戶/憑證申請完成時,就已經留存了你的X.509 憑證(公鑰)。下單時,客戶端使用本地
.pfx中的私鑰進行簽章,券商再以預存或隨封包附帶的公鑰進行驗證,確保交易無法事後賴帳。
Java 語言使用 java.security.KeyStore 抽象類別來管理安全憑證與金鑰。歷史上有兩種主要的 KeyStore 實現:
┌──> 舊版 JKS (Java KeyStore): Java 專屬特有格式,安全性較弱,已棄用
[ Java KeyStore ] ──┤
└──> 現代 PKCS12 (.p12 / .pfx): 跨語言國際標準,JDK 9+ 預設
JDK 演進提醒:自 JDK 9 開始,Oracle 官方已將
KeyStore預設格式切換為 PKCS12。在新專案中,嚴禁繼續使用 JKS 格式,應全面採用 PKCS12。
在不依賴第三方庫的前提下,標準 Java JCA 可以透過 PKCS8EncodedKeySpec 與 CertificateFactory 原生解析 PEM 文字,並動態操作 PKCS12 容器(包括載入證期憑證 .pfx 檔案)。
以下範例演示完整流程:
.p12)並寫入磁碟。.p12 / .pfx) 檔案,取出私鑰與憑證進行檢驗。import java.io.ByteArrayInputStream;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.nio.charset.StandardCharsets;
import java.security.KeyFactory;
import java.security.KeyStore;
import java.security.PrivateKey;
import java.security.cert.Certificate;
import java.security.cert.CertificateFactory;
import java.security.cert.X509Certificate;
import java.security.spec.PKCS8EncodedKeySpec;
import java.util.Base64;
import java.util.Enumeration;
public class KeyStoreAndPemProductionExample {
private static final String KEYSTORE_TYPE_PKCS12 = "PKCS12";
private static final char[] KEYSTORE_PASSWORD = "StorePassword2026!".toCharArray();
private static final String ALIAS_NAME = "twca-stock-cert";
// 模擬 PEM 格式 PKCS#8 私鑰字串
private static final String SAMPLE_PKCS8_PRIVATE_KEY_PEM =
"-----BEGIN PRIVATE KEY-----\n" +
"MIGHAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBG0wawIBAQQg... [省略 Base64內容] ...\n" +
"-----END PRIVATE KEY-----";
// 模擬 PEM 格式 X.509 憑證字串
private static final String SAMPLE_X509_CERTIFICATE_PEM =
"-----BEGIN CERTIFICATE-----\n" +
"MIIB/zCCAWagAwIBAgIUY3a123456789ABCDEF01MAoGCCqGSM49BAMCMDExCzAJ\n" +
"BgNVBAYTAlRXIxEzARBgNVBAoMCkRlbW8gQ0EgSW5jMRMwEQYDVQQDDApEZW1v\n" +
"-----END CERTIFICATE-----";
public static void main(String[] args) throws Exception {
System.out.println("=== Day 32 - PEM 解析與 PKCS12 KeyStore 實戰 ===");
// Step 1: 解析 PEM 私鑰為 PrivateKey 物件 (以 EC 為例)
PrivateKey privateKey = parsePemPrivateKey(SAMPLE_PKCS8_PRIVATE_KEY_PEM, "EC");
System.out.println("PEM PKCS#8 私鑰解析成功,演算法: " + privateKey.getAlgorithm());
// Step 2: 解析 PEM 憑證為 X509Certificate 物件
X509Certificate certificate = parsePemCertificate(SAMPLE_X509_CERTIFICATE_PEM);
System.out.println("PEM X.509 憑證解析成功,Subject: " + certificate.getSubjectX500Principal().getName());
// Step 3: 動態建立 PKCS12 KeyStore 並儲存至磁碟 (.p12 / .pfx)
String p12FilePath = "user-stock-cert.p12";
Certificate[] certChain = new Certificate[]{certificate};
createAndSavePkcs12KeyStore(p12FilePath, ALIAS_NAME, privateKey, certChain, KEYSTORE_PASSWORD);
System.out.println("PKCS12 金鑰庫已成功打包寫入磁碟: " + p12FilePath);
// Step 4: 從磁碟重新載入 PKCS12 (.p12 / .pfx 證期憑證) 檔案並讀取內容
loadAndVerifyPkcs12KeyStore(p12FilePath, KEYSTORE_PASSWORD);
}
/**
* 原生 Java 解析 PEM PKCS#8 私鑰
*/
public static PrivateKey parsePemPrivateKey(String pemPrivateKey, String algorithm) throws Exception {
String base64Key = pemPrivateKey
.replace("-----BEGIN PRIVATE KEY-----", "")
.replace("-----END PRIVATE KEY-----", "")
.replaceAll("\\s+", "");
byte[] derBytes = Base64.getDecoder().decode(base64Key);
PKCS8EncodedKeySpec keySpec = new PKCS8EncodedKeySpec(derBytes);
KeyFactory keyFactory = KeyFactory.getInstance(algorithm);
return keyFactory.generatePrivate(keySpec);
}
/**
* 原生 Java 解析 PEM X.509 憑證
*/
public static X509Certificate parsePemCertificate(String pemCert) throws Exception {
CertificateFactory certFactory = CertificateFactory.getInstance("X.509");
ByteArrayInputStream bais = new ByteArrayInputStream(pemCert.getBytes(StandardCharsets.UTF_8));
return (X509Certificate) certFactory.generateCertificate(bais);
}
/**
* 建立並導出 PKCS12 金鑰庫 (.p12)
*/
public static void createAndSavePkcs12KeyStore(String filePath, String alias, PrivateKey privateKey,
Certificate[] certChain, char[] password) throws Exception {
KeyStore keyStore = KeyStore.getInstance(KEYSTORE_TYPE_PKCS12);
keyStore.load(null, null); // 初始化空白 KeyStore
keyStore.setKeyEntry(alias, privateKey, password, certChain);
try (FileOutputStream fos = new FileOutputStream(filePath)) {
keyStore.store(fos, password);
}
}
/**
* 載入並驗證 PKCS12 (.p12 / .pfx) 金鑰庫
*/
public static void loadAndVerifyPkcs12KeyStore(String filePath, char[] password) throws Exception {
KeyStore keyStore = KeyStore.getInstance(KEYSTORE_TYPE_PKCS12);
try (FileInputStream fis = new FileInputStream(filePath)) {
keyStore.load(fis, password);
}
System.out.println("\n--- [ 檢驗已載入的 PKCS12 容器 (.p12 / .pfx) ] ---");
System.out.println("KeyStore 格式類型: " + keyStore.getType());
Enumeration<String> aliases = keyStore.aliases();
while (aliases.hasMoreElements()) {
String alias = aliases.nextElement();
System.out.println("找到別名 (Alias): " + alias);
// 讀取容器中的私鑰
PrivateKey loadedPrivateKey = (PrivateKey) keyStore.getKey(alias, password);
if (loadedPrivateKey != null) {
System.out.println(" └─ 成功取出私鑰,演算法: " + loadedPrivateKey.getAlgorithm());
}
// 讀取容器中的對應憑證 (公鑰)
Certificate loadedCert = keyStore.getCertificate(alias);
if (loadedCert instanceof X509Certificate x509) {
System.out.println(" └─ 成功取出 X.509 憑證 (含公鑰),Subject: " + x509.getSubjectX500Principal().getName());
System.out.println(" └─ 憑證簽發者 (Issuer): " + x509.getIssuerX500Principal().getName());
}
}
}
}