# 第 11 篇:ASN.1 與編碼格式 - 為什麼憑證看起來像亂碼?
## 前言:解開憑證「亂碼」的謎團
在第 10 篇,我們了解了 X.509 憑證的邏輯結構:它包含哪些欄位(version、subject、issuer、publicKey 等)、每個欄位的用途、憑證鏈如何透過 subject 和 issuer 配對。
這些概念很清楚。但當你實際查看憑證檔案時,會看到這樣的東西:
```
30 82 02 cc 30 82 02 34 a0 03 02 01 02 02 10 1a 2b 3c 4d...
```
或是這樣:
```
-----BEGIN CERTIFICATE-----
MIICzDCCAbSgAwIBAgIQGis8TTBAQEBAQEBAQEBAQEBAQDANBgkqhkiG9w0BAQUF
ADBOMQswCQYDVQQGEwJVUzEQMA4GA1UEChMHRXF1aWZheDEtMCsGA1UECxMkRXF1
...
-----END CERTIFICATE-----
```
**完全看不懂!這跟我們理解的「version、subject、publicKey」結構有什麼關係?**
這就是本篇要解開的謎題:
- 為什麼憑證要用這種「亂碼」格式儲存?
- ASN.1 和 DER 編碼是什麼?
- DER、BER、PEM 有什麼差異?
- 如何把「邏輯結構」轉換成「二進位格式」?
理解這些後,你就能完全掌握憑證的「外在形式」和「內在結構」之間的關係。
## 從問題開始:如何在電腦間傳遞結構化資料?
### 🏗️ 生活中的類比:傢俱的包裝
想像你在 IKEA 買了一張桌子:
**方法 1:組裝好直接搬**
- ❌ 佔空間
- ❌ 運輸困難
- ❌ 容易損壞
**方法 2:拆解打包**
- ✅ 省空間
- ✅ 運輸方便
- ✅ 有標準的組裝說明書
**重點:需要一套「拆解」和「組裝」的標準規則**
### 💻 數位世界的挑戰
當兩台電腦要交換資料(例如:憑證)時:
**問題 1:資料結構不同**
```
電腦 A(Python):
cert = {
'version': 3,
'subject': 'Apple Inc.',
'publicKey': bytes([0x04, 0xa1, ...])
}
電腦 B(Java):
Certificate cert = new Certificate();
cert.setVersion(3);
cert.setSubject("Apple Inc.");
```
資料結構不一樣!如何確保雙方理解相同的資料?
**問題 2:資料型別不同**
- 數字怎麼表示?(int? long? BigInteger?)
- 字串用什麼編碼?(UTF-8? ASCII?)
- 日期時間怎麼表示?
**這就是為什麼需要 ASN.1!**
## ASN.1 是什麼?
### 📜 正式定義
**ASN.1** = **A**bstract **S**yntax **N**otation **O**ne(抽象語法標記法一號)
- 1984 年由 ITU-T 和 ISO 聯合制定
- 一種「描述資料結構」的語言
- 獨立於程式語言和硬體平台
### 🎯 ASN.1 的作用
**ASN.1 做兩件事:**
1. **定義資料結構**(就像建築藍圖)
2. **編碼規則**(如何把資料打包成二進位)
### 📝 實際例子:Person 結構
**用 ASN.1 定義:**
```asn1
Person ::= SEQUENCE {
name UTF8String,
age INTEGER,
email UTF8String OPTIONAL
}
```
**這個定義說:**
- Person 是一個序列(SEQUENCE)
- 包含名字(UTF8 字串)
- 包含年齡(整數)
- 可選的 email(UTF8 字串)
**好處:**
- Python、Java、C++ 看到同樣的定義
- 都知道如何解析這個結構
- 不會產生誤解
## ASN.1 的基本類型
### 🧱 Universal Types(通用型別)
ASN.1 定義了一些基本的資料型別:
| ASN.1 類型 | Tag | 說明 | 例子 |
|-----------|-----|------|------|
| **BOOLEAN** | 0x01 | 布林值 | TRUE / FALSE |
| **INTEGER** | 0x02 | 整數 | 42, -100, 999999 |
| **BIT STRING** | 0x03 | 位元字串 | 01011010 |
| **OCTET STRING** | 0x04 | 位元組字串 | 二進位資料 |
| **NULL** | 0x05 | 空值 | NULL |
| **OBJECT IDENTIFIER** | 0x06 | 物件識別碼 | 1.2.840.10045.3.1.7 |
| **UTF8String** | 0x0C | UTF-8 字串 | "Apple Inc." |
| **SEQUENCE** | 0x30 | 序列(有序集合) | {a, b, c} |
| **SET** | 0x31 | 集合(無序) | {x, y, z} |
### 🏷️ Tag(標籤)的概念
每種類型都有一個「標籤」(Tag),就像商品的條碼:
```
Tag: 0x02(INTEGER)
↓
資料: 42
↓
編碼: 02 01 2a
│ │ └─ 值:42 (0x2a)
│ └──── 長度:1 byte
└─────── 類型:INTEGER
```
### 🔢 Object Identifier (OID)
這是 ASN.1 中最重要的概念之一!
**OID 是什麼?**
- 全球唯一的「物件」識別碼
- 用點號分隔的數字序列
- 例如:`1.2.840.10045.3.1.7`
**層級結構:**
```
1 (ISO)
└── 2 (ISO member body)
└── 840 (美國)
└── 10045 (ANSI X9.62)
└── 3 (id-publicKeyType)
└── 1 (id-ecPublicKey)
└── 7 (secp256r1 / P-256)
```
**在憑證中的應用:**
```
OID 1.2.840.113635.100.8.2 = Apple App Attest Extension
│ │ │ │ │ └─ 2 (包含 nonce 等 attestation 相關資料)
│ │ │ │ └──── 8 (App Attest)
│ │ │ └──────── 100 (Apple Extensions)
│ │ └─────────────── 113635 (Apple Inc.)
│ └─────────────────── 840 (美國)
└─────────────────────── 2 (ISO member body)
```
## 編碼規則:DER vs BER
ASN.1 只定義「資料結構」,還需要「編碼規則」來打包成二進位。
### 🎁 BER(Basic Encoding Rules)
**特性:**
- 最基本的編碼規則
- **允許多種編碼方式**(同樣的資料可以有不同的編碼)
- 靈活但不唯一
**結構:**
```
┌─────────┬─────────┬─────────┐
│ Tag │ Length │ Value │
│ (類型) │ (長度) │ (值) │
└─────────┴─────────┴─────────┘
```
**例子:編碼數字 127**
方式 1:
```
02 01 7f
│ │ └─ 值:127
│ └──── 長度:1
└─────── Tag:INTEGER
```
方式 2(長格式長度):
```
02 81 01 7f
│ │ │ └─ 值:127
│ │ └──── 長度:1
│ └─────── 長度的長度:1
└────────── Tag:INTEGER
```
**問題:** 同樣的資料,兩種編碼!
### 💎 DER(Distinguished Encoding Rules)
**特性:**
- BER 的子集
- **只有一種編碼方式**(確定性)
- 用於需要數位簽章的場合
**為什麼憑證用 DER?**
還記得第 10 篇提到的問題嗎?
```
CA 簽章憑證:
1. 對憑證內容計算 hash
2. 用私鑰簽章 hash
驗證時:
1. 重新計算憑證的 hash
2. 用公鑰驗證簽章
```
**如果編碼不唯一會怎樣?**
```
原始編碼:02 01 7f → Hash: abc123
修改編碼:02 81 01 7f → Hash: def456 (不同!)
簽章驗證失敗!(即使內容一樣)
```
**所以:**
- ✅ DER 確保「同樣的資料只有一種編碼」
- ✅ 簽章才能正確驗證
- ✅ X.509 憑證必須用 DER
### 📏 DER 的編碼規則
**長度編碼:**
**短格式(值 ≤ 127):**
```
長度 = 42
編碼: 2a
```
**長格式(值 > 127):**
```
長度 = 500 (0x01f4)
編碼: 82 01 f4
│ └───┴─ 實際長度(2 bytes)
└──────── 0x82 = 10000010
│ └─ 長度佔 2 bytes
└──────── 長格式標記
```
**SEQUENCE 編碼例子:**
```asn1
Person ::= SEQUENCE {
name UTF8String,
age INTEGER
}
實際資料:
name = "Alice"
age = 30
```
**DER 編碼:**
```
30 0d -- SEQUENCE, 長度 13
0c 05 41 6c 69 63 65 -- UTF8String "Alice"
│ │ └────┬────────┘
│ │ └─ "Alice" (ASCII)
│ └────────── 長度:5
└───────────── Tag:UTF8String (0x0C)
02 01 1e -- INTEGER 30
│ │ └─ 值:30 (0x1E)
│ └──── 長度:1
└─────── Tag:INTEGER (0x02)
```
## PEM 格式:讓二進位變成文字
### 📧 為什麼需要 PEM?
**問題:**
- DER 是二進位格式
- Email 和一些協定只能傳輸文字
- 怎麼辦?
**解決方案:PEM**
**PEM** = **P**rivacy **E**nhanced **M**ail
### 🔄 PEM 的轉換過程
```
1. 原始資料(DER 二進位)
30 82 02 cc 30 82 02 34...
2. Base64 編碼
MIICzDCCAbSgAwIBAgIQGis8...
3. 加上標頭和標尾
-----BEGIN CERTIFICATE-----
MIICzDCCAbSgAwIBAgIQGis8...
-----END CERTIFICATE-----
```
### 📝 PEM 格式詳解
**結構:**
```
-----BEGIN <TYPE>-----
<Base64 編碼的資料,每行 64 字元>
-----END <TYPE>-----
```
**常見的 TYPE:**
```
CERTIFICATE → X.509 憑證
PRIVATE KEY → 私鑰
PUBLIC KEY → 公鑰
CERTIFICATE REQUEST → CSR (憑證簽章請求)
```
**實際例子:**
```
-----BEGIN CERTIFICATE-----
MIICzDCCAbSgAwIBAgIQGis8TTBAQEBAQEBAQEBAQEBAQDANBgkqhkiG9w0BAQUF
ADBOMQswCQYDVQQGEwJVUzEQMA4GA1UEChMHRXF1aWZheDEtMCsGA1UECxMkRXF1
aWZheCBTZWN1cmUgQ2VydGlmaWNhdGUgQXV0aG9yaXR5MB4XDTA5MDYxMjEwNTAy
...
-----END CERTIFICATE-----
```
## 三種格式的對比與轉換
### 📊 格式對比
| 特性 | BER | DER | PEM |
|------|-----|-----|-----|
| **編碼方式** | 二進位 | 二進位 | Base64 文字 |
| **唯一性** | ❌ 不唯一 | ✅ 唯一 | ✅ 唯一(基於 DER) |
| **用途** | 一般資料交換 | 數位簽章 | Email、文字傳輸 |
| **檔案副檔名** | .ber | .der | .pem, .crt, .cer |
| **可讀性** | ❌ 二進位 | ❌ 二進位 | ✅ 文字(但仍需解碼) |
| **大小** | 小 | 小 | 大(+33%,Base64 的開銷) |
### 🔄 格式轉換
**DER → PEM:**
```bash
openssl x509 -in cert.der -inform DER -out cert.pem -outform PEM
```
**PEM → DER:**
```bash
openssl x509 -in cert.pem -inform PEM -out cert.der -outform DER
```
**程式化轉換(Python):**
```python
import base64
# DER → PEM
def der_to_pem(der_bytes, label="CERTIFICATE"):
b64 = base64.b64encode(der_bytes).decode('ascii')
# 每 64 字元換行
pem_lines = [b64[i:i+64] for i in range(0, len(b64), 64)]
pem = f"-----BEGIN {label}-----\n"
pem += '\n'.join(pem_lines)
pem += f"\n-----END {label}-----\n"
return pem
# PEM → DER
def pem_to_der(pem_string):
# 移除標頭、標尾、換行
lines = pem_string.strip().split('\n')
b64 = ''.join([line for line in lines if not line.startswith('-----')])
return base64.b64decode(b64)
```
## 實際應用:憑證的不同表示
### 🔍 同一張憑證的三種面貌
**1. DER 格式(二進位):**
```
30 82 02 cc 30 82 02 34 a0 03 02 01 02 02 10 1a...
```
- 最緊湊
- 電腦直接處理
- 用於實際傳輸(App Attest 的 x5c)
**2. PEM 格式(文字):**
```
-----BEGIN CERTIFICATE-----
MIICzDCCAbSgAwIBAgIQGis8...
-----END CERTIFICATE-----
```
- 可以複製貼上
- Email 友善
- 設定檔常用
**3. 文字解析(人類可讀):**
```
Certificate:
Data:
Version: 3 (0x2)
Serial Number: ...
Issuer: CN=Apple App Attestation CA
Subject: CN=6d2ac4845f13...
Public Key: EC Public Key (256 bit)
```
- 用 OpenSSL 或線上工具產生
- 除錯用
- 不是標準格式
### 📂 檔案副檔名的混亂
**注意:副檔名不可靠!**
| 副檔名 | 可能的格式 |
|--------|-----------|
| .pem | PEM(通常) |
| .der | DER(通常) |
| .crt | PEM 或 DER(都可能) |
| .cer | PEM 或 DER(都可能) |
| .key | PEM 或 DER(都可能) |
**判斷方式:**
```python
def detect_format(data):
if data.startswith(b'-----BEGIN'):
return 'PEM'
elif data[0] == 0x30: # SEQUENCE tag
return 'DER (可能是 BER)'
else:
return '未知格式'
```
## 為什麼密碼學系統選擇 ASN.1?
### ✅ 優點
**1. 標準化**
- 1984 年就有的國際標準
- 所有系統都支援
- 不會有相容性問題
**2. 類型安全**
- 明確定義每個欄位的類型
- 不會把字串當數字解析
**3. 擴展性**
- OID 系統提供無限擴展可能
- 新增欄位不會破壞舊版本
**4. 效率**
- DER 編碼緊湊
- 二進位格式,解析快
### ❌ 缺點
**1. 學習曲線陡**
- 概念抽象
- 文件難懂
- 新手不友善
**2. 除錯困難**
- 二進位格式看不懂
- 需要專門工具
**3. 靈活性不足**
- 結構一旦定義就難更改
- 不像 JSON 那樣彈性
### 🆚 與現代格式的對比
| 特性 | ASN.1/DER | JSON | CBOR |
|------|-----------|------|------|
| **標準化時間** | 1984 | 2001 | 2013 |
| **可讀性** | ❌ 低 | ✅ 高 | ❌ 低 |
| **體積** | 小 | 大 | 很小 |
| **類型安全** | ✅ 強 | ❌ 弱 | ✅ 強 |
| **擴展性** | ✅ OID | ✅ 彈性 | ✅ 彈性 |
| **學習曲線** | 陡 | 平緩 | 中等 |
| **適用場景** | 密碼學、電信 | Web API | IoT、密碼學 |
## 本文小結:理解編碼的重要性
✅ **ASN.1**:定義資料結構的標準語言
✅ **DER**:唯一確定的編碼方式(用於簽章)
✅ **BER**:靈活的編碼方式(允許多種編碼)
✅ **PEM**:DER 的 Base64 文字版本(方便傳輸)
✅ **OID**:全球唯一的物件識別系統
### 🎓 與前面文章的連結
**第 9 篇(CBOR)→ 現代的編碼方式**
- CBOR 和 DER 都是二進位編碼
- CBOR 更簡單、更現代
- DER 更古老、更複雜
**第 10 篇(X.509)→ 為什麼憑證用 DER**
- 憑證需要數位簽章
- 簽章需要唯一編碼
- 所以選擇 DER 而不是 BER
### 📊 編碼方式的演進
```
1984: ASN.1/DER → 密碼學標準(X.509、PKCS)
↓
2001: JSON → Web API 革命(簡單、可讀)
↓
2013: CBOR → 結合優點(二進位 + 簡單)
```
**為什麼還在用 ASN.1/DER?**
- 歷史遺留(X.509 憑證從 1988 年就用)
- 標準成熟(所有系統都支援)
- 更換成本高(整個 PKI 體系都要改)
## 下一步學習
下一篇《第 12 篇:X9.62 橢圓曲線公鑰格式》會介紹:
- ECC 公鑰如何表示成二進位(X9.62 格式)
- 未壓縮格式(0x04 || X || Y)與壓縮格式
- 如何將 X9.62 raw point 包裝成 SPKI 格式
這會連結系列二的 ECC 數學知識與實際的公鑰格式!
---
## 💡 補充資料
### 常見問題
**Q: 我需要深入學習 ASN.1 嗎?**
A: 不用!理解基本概念就夠了。實際開發用現成函式庫(OpenSSL、cryptography)就好。
**Q: 如何判斷檔案是 DER 還是 PEM?**
A:
- PEM:文字檔,開頭是 `-----BEGIN`
- DER:二進位檔,開頭通常是 `0x30`(SEQUENCE)
**Q: 為什麼叫「抽象」語法?**
A: 因為 ASN.1 只定義「資料結構」,不管實際如何編碼。編碼由 DER/BER 等規則決定。
**Q: OID 會用完嗎?**
A: 不會!OID 是階層式的,每個組織可以在自己的分支下無限分配。
### 實用工具
**線上 ASN.1 解析器:**
- https://lapo.it/asn1js/ - 視覺化 ASN.1 結構
- https://certlogik.com/decoder/ - 憑證解碼器
**命令列工具:**
```bash
# 查看 DER 檔案結構
openssl asn1parse -in file.der -inform DER
# 查看 PEM 檔案結構
openssl asn1parse -in file.pem -inform PEM
# 查看憑證詳細資訊
openssl x509 -in cert.pem -text -noout
```
**Python 函式庫:**
- `pyasn1` - ASN.1 編碼/解碼
- `cryptography` - 密碼學操作(內建 ASN.1 支援)
### 進階閱讀(選讀)
- [ITU-T X.680 - ASN.1 規格](https://www.itu.int/rec/T-REC-X.680/)
- [ITU-T X.690 - DER/BER 編碼規則](https://www.itu.int/rec/T-REC-X.690/)
- [RFC 5280 - X.509 in ASN.1](https://tools.ietf.org/html/rfc5280)
- [A Layman's Guide to ASN.1, BER, and DER](http://luca.ntop.org/Teaching/Appunti/asn1.html)