iT邦幫忙

2026 iThome 鐵人賽

0
Software Development

我是Java工程師,關於密碼學我想懂的不多系列 第 34 篇

Day33 - 金鑰與憑證容器標準

  • 分享至 

  • xImage
  •  

前言

在 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)的建立與讀取。

一、 二進位與文字編碼:DER 與 PEM 的本質差異

所有非對稱金鑰(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 vs. PEM 對照表

比較維度 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 Standard)

為避免不同系統間的金鑰格式混亂,公鑰密碼學標準(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)               (二進位密碼保護容器:含私鑰 + 憑證鏈)

1. PKCS#1 vs. PKCS#8(私鑰格式演進)

  • PKCS#1:早期專為 RSA 設計的私鑰格式(標頭為 BEGIN RSA PRIVATE KEY),無法存放 ECC 或 Ed25519 金鑰。
  • PKCS#8(RFC 5208,現代標準):通用的私鑰包裝標準(標頭為 BEGIN PRIVATE KEY)。內部包含演算法識別碼(Algorithm Identifier)與私鑰主體,能統一封裝 RSA、ECC、Ed25519 等所有非對稱私鑰。Java 原生 KeyFactory 預設僅支援 PKCS#8 格式。

2. PKCS#12(.p12 / .pfx 金鑰保護容器)

PKCS#12(RFC 7292) 是業界標準的密碼保護二進位容器。它能將私鑰與對應的X.509 憑證鏈(Certificate Chain)打包儲存於單一檔案中,並透過對稱加密(如 AES-256)與口令(Password)進行整體保護。

三、 台灣實務案例:證期憑證 (.pfx / .p12) 運作機制

在台灣進行股票或期貨網路交易時,券商會要求客戶下載「證期憑證」(由台灣網路認證公司 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 KeyStore (JKS vs. PKCS12) 容器演進

Java 語言使用 java.security.KeyStore 抽象類別來管理安全憑證與金鑰。歷史上有兩種主要的 KeyStore 實現:

                    ┌──> 舊版 JKS (Java KeyStore): Java 專屬特有格式,安全性較弱,已棄用
[ Java KeyStore ] ──┤
                    └──> 現代 PKCS12 (.p12 / .pfx): 跨語言國際標準,JDK 9+ 預設

KeyStore 內部的兩種 Entry 類型

  1. KeyEntry(Private Key Entry):存放「私鑰」以及與之綁定的「X.509 憑證鏈」。用於證期下單簽章或 TLS 伺服器端身份證明。
  2. TrustedCertificateEntry:僅存放信任的「公鑰憑證」(例如 TWCA 或 Root CA)。常用於 TrustStore。

JDK 演進提醒:自 JDK 9 開始,Oracle 官方已將 KeyStore 預設格式切換為 PKCS12。在新專案中,嚴禁繼續使用 JKS 格式,應全面採用 PKCS12。

五、 Java JCA 實戰:PEM 解析與 PKCS12 KeyStore 操作

在不依賴第三方庫的前提下,標準 Java JCA 可以透過 PKCS8EncodedKeySpec 與 CertificateFactory 原生解析 PEM 文字,並動態操作 PKCS12 容器(包括載入證期憑證 .pfx 檔案)。

以下範例演示完整流程:

  1. 將 PEM 格式的私鑰與憑證解析為 Java 物件。
  2. 建立一個受密碼保護的 PKCS12 金鑰庫(.p12)並寫入磁碟。
  3. 從磁碟重新載入 PKCS12 (.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());
            }
        }
    }
}


上一篇
Day32 - 數位憑證與公鑰基礎設施 (PKI)
系列文
我是Java工程師,關於密碼學我想懂的不多 共 34 篇
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言