# 第 12 篇: X9.62 橢圓曲線公鑰格式 ## 前言: 從 App Attest 公鑰提取說起 在實作 Apple App Attest 時,你會遇到橢圓曲線公鑰。這些公鑰需要用標準格式表示和傳輸,**X9.62** 就是定義橢圓曲線點(公鑰)編碼方式的標準。 **本文重點:** - X9.62 格式:橢圓曲線點如何表示成二進位(`04 || X || Y`) - SPKI 結構:X9.62 點如何包裝成完整的公鑰(加上演算法資訊) - 在 App Attest 中的應用:憑證裡的公鑰如何組織 ## 什麼是 X9.62? **X9.62** 是 ANSI(美國國家標準協會)制定的橢圓曲線密碼學標準,定義了: - 橢圓曲線參數 - 公鑰/私鑰格式 - 數位簽章演算法 **本文重點**: X9.62 定義的**橢圓曲線點(公鑰)編碼格式** ## 橢圓曲線上的點 ### 數學基礎回顧 橢圓曲線上的一個點由兩個座標組成: ``` 點 P = (X, Y) 其中: - X 是橫座標 - Y 是縱座標 - 兩者都是有限域上的大整數 ``` **P-256 曲線為例:** - X: 256 bits = 32 bytes - Y: 256 bits = 32 bytes ### 為什麼需要編碼格式? 橢圓曲線公鑰本質上就是曲線上的一個點 (X, Y),但: 1. 如何將這個點儲存成二進位資料? 2. 如何在網路上傳輸? 3. 如何讓不同系統互通? **X9.62 標準定義了統一的編碼方式** ## X9.62 點格式詳解 ### 1. 未壓縮格式 (Uncompressed) 這是最直觀的格式: ``` 格式: 0x04 || X || Y 結構: ├── 0x04: 1 byte (標記為「未壓縮」) ├── X: 32 bytes (橫座標) └── Y: 32 bytes (縱座標) 總長度: 65 bytes (P-256) ``` **範例 (P-256):** ``` 04 [32 bytes X] [32 bytes Y] │ └─────────┬──────────┘ └─────────┬──────────┘ │ X 座標 Y 座標 未壓縮標記 ``` **實際資料:** ``` 04 1a2b3c4d5e6f7a8b9c0d1e2f3a4b5c6d7e8f9a0b1c2d3e4f5a6b7c8d9e0f1a2b (X) 9a8b7c6d5e4f3a2b1c0d9e8f7a6b5c4d3e2f1a0b9c8d7e6f5a4b3c2d1e0f9a8b (Y) ``` ### 2. 壓縮格式 (Compressed) 由於橢圓曲線方程式的對稱性,給定 X 座標,Y 座標只有兩個可能值(正負)。 ``` 格式: 0x02/0x03 || X 結構: ├── 0x02 或 0x03: 1 byte │ ├── 0x02: Y 是偶數 │ └── 0x03: Y 是奇數 └── X: 32 bytes (橫座標) 總長度: 33 bytes (P-256) ``` **節省空間:** - 未壓縮: 65 bytes - 壓縮: 33 bytes - 節省: 49% **為什麼 App Attest 用未壓縮格式?** - 相容性更好 - 處理更簡單 - 空間差異不大(只有 32 bytes) ### 3. 特殊情況: 無限遠點 ``` 格式: 0x00 這代表橢圓曲線的「無限遠點」(identity element) 在實務中很少用到 ``` ## X9.62 在 SPKI 中的位置 X9.62 點通常不會單獨使用,而是被包裝在 **SPKI(SubjectPublicKeyInfo)結構**中: ``` SubjectPublicKeyInfo (SPKI) - 完整的公鑰格式 ├── AlgorithmIdentifier │ ├── algorithm: id-ecPublicKey (1.2.840.10045.2.1) │ └── parameters: secp256r1 (1.2.840.10045.3.1.7) └── subjectPublicKey: BIT STRING └── 04 || X || Y ← X9.62 點在這裡 ``` **關鍵理解:** - X9.62 = 只有點座標(65 bytes) - SPKI = X9.62 + 演算法資訊(~91 bytes) - 憑證裡儲存 SPKI DER,函式庫通常輸出 SPKI PEM **實務上:**從憑證提取公鑰得到的是完整 SPKI(`-----BEGIN PUBLIC KEY-----`),可直接使用。詳見「實務考量」章節。 ## 從 X9.62 建構 SPKI(罕見情境) **如果**你只有 X9.62 原始點資料(例如從其他來源獲得),才需要手動建構 SPKI: ``` 步驟 1: 準備 X9.62 點資料 ├── 已有: 04 || X || Y (65 bytes) └── 確認格式正確 步驟 2: 建構 AlgorithmIdentifier ├── algorithm OID: 1.2.840.10045.2.1 (id-ecPublicKey) └── parameters OID: 1.2.840.10045.3.1.7 (secp256r1/P-256) 步驟 3: 建構 SPKI 結構 ├── algorithmIdentifier (步驟 2) └── subjectPublicKey (BIT STRING 包裝 X9.62 點) 步驟 4: DER 編碼整個 SPKI 步驟 5: (可選) PEM 編碼 └── Base64(DER) + 頭尾標記 ``` **結果:** ``` -----BEGIN PUBLIC KEY----- MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAE... -----END PUBLIC KEY----- ``` ### 關鍵 OID 說明 **算法 OID: 1.2.840.10045.2.1** ``` 1 (ISO) └── 2 (ISO member body) └── 840 (美國) └── 10045 (ANSI X9.62) └── 2 (publicKeyType) └── 1 (id-ecPublicKey) ``` **曲線 OID: 1.2.840.10045.3.1.7** ``` 1.2.840.10045 (ANSI X9.62) └── 3 (curves) └── 1 (prime curves) └── 7 (secp256r1 / P-256) ``` ## 在 App Attest 中的應用 **憑證中的位置:** ``` CBOR Attestation Object └── attStmt.x5c[0] (憑證) └── subjectPublicKeyInfo (SPKI) └── subjectPublicKey └── 04 || X || Y ← X9.62 點 ``` **實務處理:** 1. 從憑證提取 SPKI(函式庫自動處理) 2. 儲存為 PEM 供後續使用 3. 驗證簽章時直接使用(函式庫會提取內部的 X9.62) ## 實務考量 ### 1. 從憑證提取公鑰的正確理解 **常見誤解:** ❌ 從憑證提取的是 X9.62 點資料,需要轉換成 SPKI ✅ 從憑證提取的就是 SPKI 結構,其內部包含 X9.62 點 **為什麼密碼學函式庫要求 SPKI 格式?** - SPKI 包含完整資訊:演算法(EC)+ 曲線參數(P-256)+ 點資料(X9.62) - X9.62 只有點座標,缺少曲線資訊 - 函式庫需要知道用哪條曲線才能驗證簽章 **⚠️ 特別注意:PHP phpseclib 的行為** 如果你用 PHP phpseclib 解析憑證,會看到: ```php $cert['tbsCertificate']['subjectPublicKeyInfo']['subjectPublicKey'] = "-----BEGIN PUBLIC KEY-----\nMFkw...\n-----END PUBLIC KEY-----" ``` **❓ 為什麼會看到 PEM 格式?這是哪一層的資料?** **答案:這是函式庫的「便利功能」** ``` 憑證 DER 裡實際儲存的: subjectPublicKeyInfo (SPKI 結構,DER 編碼) ├── algorithm (DER) └── subjectPublicKey (BIT STRING, DER) └── 04 || X || Y ← X9.62 raw bytes phpseclib 處理後輸出: ['subjectPublicKeyInfo'] ├── ['algorithm'] ✅ 保持原樣 └── ['subjectPublicKey'] ⚠️ 被轉換了! └── "-----BEGIN PUBLIC KEY-----..." ↑ 這是把整個 SPKI 重新編碼成 PEM! ``` **為什麼會這樣?** 1. phpseclib 讀取憑證中的 SPKI DER 資料 2. 為了方便使用者,自動轉成 PEM 格式 3. 把 PEM 字串放在 `['subjectPublicKey']` key 下 4. 雖然 key 名稱是 `subjectPublicKey`,但內容是「整個 SPKI 的 PEM」 **這容易造成的誤解:** - ❌ 誤解:「這個 key 應該是 BIT STRING 的內容(X9.62 點)」 - ✅ 真相:「這是整個 SPKI 結構,被函式庫轉成 PEM 格式了」 **實際對應關係:** ``` ASN.1 結構: subjectPublicKeyInfo (SPKI) ├── algorithm └── subjectPublicKey (BIT STRING) ← 這裡才是 X9.62 raw bytes PHP phpseclib 解析結果: ['subjectPublicKeyInfo'] ├── ['algorithm'] └── ['subjectPublicKey'] ← 這裡是整個 SPKI 的 PEM!不是 raw bytes! ``` **總結:** - 你從 phpseclib 拿到的 `subjectPublicKey` 值已經是完整可用的 SPKI PEM - 可以直接用於驗證簽章 - 如果要提取內部的 X9.62 raw bytes,需要再解析這個 PEM ### 3. 如何查看 SPKI 內部的 X9.62 點 如果你想「看到」SPKI PEM 內部的 X9.62 點資料,可以使用: **OpenSSL 工具:** ```bash openssl ec -pubin -in pubkey.pem -text -noout # 輸出會顯示:pub: 04:1a:2b:3c...(X9.62 點) ``` **概念理解:** - SPKI PEM 是完整可用的公鑰 - X9.62 點在其內部(BIT STRING 欄位) - 大部分情況不需要提取,直接使用 SPKI 即可 ## 與其他格式的對比 ### X9.62 vs SPKI - 包含關係,不是轉換關係 **關鍵理解:SPKI 包含 X9.62** ``` 你看到的 PEM 公鑰檔案: ┌─────────────────────────────────────────┐ │ -----BEGIN PUBLIC KEY----- │ ← 這整個是 SPKI │ MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAE│ │ ... │ │ -----END PUBLIC KEY----- │ └─────────────────────────────────────────┘ │ Base64 解碼 ▼ SPKI (DER 格式,~91 bytes for P-256) ├── AlgorithmIdentifier (演算法資訊) │ ├── algorithm: id-ecPublicKey │ └── parameters: secp256r1 (P-256) └── subjectPublicKey (BIT STRING) └── X9.62 點資料 (65 bytes) ← 在這裡! ├── 0x04 (未壓縮標記) ├── X 座標 (32 bytes) └── Y 座標 (32 bytes) ``` **對比表:** | 特徵 | X9.62 點 | SPKI 結構 | |------|---------|-----------| | **內容** | 只有點座標(X, Y) | 演算法 + 曲線參數 + X9.62 點 | | **大小** | 65 bytes (P-256 未壓縮) | ~91 bytes (DER) | | **包含曲線資訊** | ❌ | ✅ | | **能直接驗證簽章** | ❌(缺少曲線資訊) | ✅ | | **你從憑證提取到的** | ❌ | ✅ 就是這個! | | **PEM 格式標頭** | (沒有獨立 PEM) | `-----BEGIN PUBLIC KEY-----` | | **使用場景** | 嵌入在 SPKI 內部 | 完整的公鑰檔案 | **重點:** - ❌ **錯誤理解**:從憑證提取 X9.62,需要轉換成 SPKI - ✅ **正確理解**:從憑證提取的就是 SPKI,X9.62 在其內部 ### X9.62 vs JWK (JSON Web Key) ```json JWK 格式範例: { "kty": "EC", "crv": "P-256", "x": "base64url(X)", "y": "base64url(Y)" } ``` **對比:** - X9.62: 二進位緊湊格式 - JWK: JSON 文字格式 - 都表示同一個點,只是編碼不同 ## 其他橢圓曲線的 X9.62 格式 ### P-384 (secp384r1) ``` 未壓縮格式: ├── 0x04: 1 byte ├── X: 48 bytes └── Y: 48 bytes 總長: 97 bytes 壓縮格式: ├── 0x02/0x03: 1 byte └── X: 48 bytes 總長: 49 bytes ``` ### P-521 (secp521r1) ``` 未壓縮格式: ├── 0x04: 1 byte ├── X: 66 bytes └── Y: 66 bytes 總長: 133 bytes 壓縮格式: ├── 0x02/0x03: 1 byte └── X: 66 bytes 總長: 67 bytes ``` **注意:** P-521 是 521 bits,不是 512 bits (66 bytes = 528 bits,但實際只用 521) ## 安全性考量 ### 1. 點驗證的重要性 **必須驗證點在曲線上:** ``` 給定點 (X, Y),驗證: y² ≡ x³ + ax + b (mod p) 如果不驗證: - 可能接受無效的公鑰 - 某些攻擊可能利用無效點 ``` ### 2. 小子群攻擊 (Small Subgroup Attack) 如果不驗證點的階(order),攻擊者可能: - 選擇一個小階的點 - 洩漏私鑰資訊 **防禦:** - 使用標準曲線(如 P-256) - 驗證點在正確的子群中 ### 3. 曲線混淆攻擊 攻擊者可能: - 提供其他曲線的點 - 聲稱是 P-256 的點 **防禦:** - 明確指定曲線參數 - 驗證點確實在預期曲線上 ## 總結 ### 核心概念回顧 1. **X9.62 是什麼:** - ANSI 橢圓曲線標準 - 定義點的編碼格式 - 業界標準,廣泛使用 2. **兩種格式:** - 未壓縮: `0x04 || X || Y` (65 bytes for P-256) - 壓縮: `0x02/0x03 || X` (33 bytes for P-256) 3. **在 App Attest 中的角色:** - 憑證的 SPKI 欄位包含 X9.62 格式的點資料 - SPKI 是完整可用的公鑰格式,可直接用於驗證 - 理解層次關係:SPKI 包含 X9.62,X9.62 是其中一部分 4. **與其他格式的關係:** - X9.62 是「點的表示」(只有座標) - SPKI 是「公鑰的完整描述」(點 + 演算法 + 曲線參數) - ASN.1 DER 是 SPKI 的編碼方式 - PEM 是 DER 的 Base64 文字版本 ### 為什麼這篇文章很重要? 在實作 App Attest 時: - ✅ 理解 SPKI 和 X9.62 的層次關係(包含關係,不是轉換關係) - ✅ 知道從憑證提取的 SPKI 已經可以直接使用 - ✅ 明白 X9.62 是 SPKI 內部的點資料格式 - ✅ 避免誤以為需要「轉換」的常見錯誤 ### 與系列文章的關聯 **前置知識:** - 系列二第 6 篇: ECC 原理 → 理解橢圓曲線點的數學意義 - 第 10 篇: X.509 憑證 → 知道公鑰在憑證的位置 - 第 11 篇: ASN.1 編碼 → 理解 SPKI 的結構 **後續應用:** - 系列四第 16 篇: Apple App Attest 實例解析 → 實際應用 X9.62 格式轉換 --- **系列**: 密碼學資料格式與編碼 (12/15) **關鍵詞**: X9.62, 橢圓曲線, 公鑰格式, SPKI, P-256, 格式轉換 ## 參考資源 - [ANSI X9.62 標準](https://www.google.com/search?q=ANSI+X9.62) - [RFC 5480: Elliptic Curve Cryptography Subject Public Key Information](https://www.rfc-editor.org/rfc/rfc5480) - [SEC 1: Elliptic Curve Cryptography](https://www.secg.org/sec1-v2.pdf)