# 第 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)