---
# System prepended metadata

title: 第 17 篇：Apple App Attest - Assertion 驗證流程
tags: [AppCheck系列]

---

# 第 17 篇：Apple App Attest - Assertion 驗證流程

## 前言：每次請求的身份證明

完成 Attestation 註冊後，每次 API 請求都需要附上 **Assertion**：證明「這個請求確實來自註冊過的裝置」。

**本文重點：**
- Assertion 的資料結構（signature + authenticatorData）
- 6 個驗證步驟的詳細解析
- Counter 防重放機制
- 與 Attestation 的差異

**前置知識整合：**
- CBOR（第9篇）→ authenticatorData 結構
- ECDSA（第8篇）→ signature 簽章驗證
- Attestation（第16篇）→ 註冊流程獲得的公鑰

## 從真實資料開始：這是什麼?

當客戶端發送 API 請求時，會同時送出 assertion：

### 📦 API 請求格式

```php
// 客戶端傳送的資料
$assertion = 'omlzaWduYXR1cmVYR...'  // Base64 編碼的 CBOR 資料

// + clientData（與後端協調的資料）
$clientData = $challenge;  // 或其他格式

// + API 請求的資料
$requestBody = '{"action":"transfer","amount":1000}';
```

### 🔓 Assertion CBOR 解碼後的結構

```php
// 後端用 CBOR 解碼 assertion 後得到：
array (
  'signature' => <binary>,           // ECDSA 簽章（二進位）
  'authenticatorData' => <binary>    // 認證資料（二進位）
)
```

**重點：**
- App 傳的是 **CBOR 編碼**的 assertion（類似 Attestation Object）
- 後端需要 **CBOR 解碼**才能拿到 signature 和 authenticatorData
- 相比 Attestation，結構簡單很多（只有兩個欄位）

## 兩個主要部分詳解

### 🔏 Part 1: authenticatorData（認證資料）

```php
'authenticatorData' => 'X@3�P���QL...'  // Base64 編碼的二進位資料
```

**這是什麼？**
- 類似 Attestation 的 authData，但更簡單
- 沒有公鑰資料（因為註冊時已經拿到了）
- 主要包含 counter（防重放攻擊的計數器）

#### authenticatorData 的內部結構

```
authenticatorData (總長度：37 bytes)
├── [0-31] RP ID Hash (32 bytes)
│   └── SHA-256(AppIDPrefix + "." + BundleID)
│       例如：SHA-256("ABC123DEF4.com.example.app")
│
├── [32] Flags (1 byte)
│   └── 0x01 = 00000001
│       └── Bit 0 (UP): User Present = 1
│
└── [33-36] Counter (4 bytes)
    └── 遞增的計數器（每次 +1）
```

### ✍️ Part 2: signature（簽章）

```php
'signature' => 'MEUCIQDj8F...'  // Base64 編碼的 ECDSA 簽章
```

**這是什麼？**
- 使用 Attestation 時建立的私鑰簽署的
- 格式：ECDSA-SHA256 簽章（DER 編碼）
- 簽署對象：`authenticatorData || clientDataHash` 的組合資料

## 完整驗證流程：一步一步來

### 🔍 完整的 6 個驗證步驟（根據 Apple 官方文件）

```
Step 1: 計算 clientDataHash
   ↓ clientDataHash = SHA-256(clientData)
   ↓ clientData 是 App 和後端協調的資料
   ↓ （例如：challenge、requestBody 或其他自訂格式）

Step 2: 串接資料並計算 nonce
   ↓ dataToSign = authenticatorData || clientDataHash
   ↓ nonce = SHA-256(dataToSign)

Step 3: 驗證簽章
   ↓ 使用 Attestation 時儲存的公鑰
   ↓ ECDSA_Verify(publicKey, nonce, signature)

Step 4: 計算 App ID Hash 並驗證 RP ID
   ↓ 計算：SHA-256(AppIDPrefix + "." + BundleID)
   ↓ 比對：是否等於 authenticatorData 的 RP ID Hash

Step 5: 驗證 Counter（防重放攻擊）
   ↓ 從資料庫讀取該 keyId 的上次 counter
   ↓ 比較：新 counter > 舊 counter（首次 > 0）
   ↓ 更新：儲存新 counter 到資料庫

Step 6: 驗證 clientData 中的 challenge
   ↓ 檢查 clientData 包含的 challenge
   ↓ 確認是伺服器先前發送的值

✅ 驗證通過！處理 API 請求
```

**關鍵要點：**
- **Step 1**：clientDataHash = SHA-256(clientData)
- **Step 2**：nonce = SHA-256(authenticatorData || clientDataHash)
- **Step 3**：簽章驗證對象是 nonce
- **Step 5**：Counter 必須嚴格遞增（防止重放攻擊）

### 💡 驗證概念說明

**為什麼要計算 nonce = SHA-256(authenticatorData || clientDataHash)？**

```
dataToSign = authenticatorData || clientDataHash
nonce = SHA-256(dataToSign)

作用：
1. clientDataHash 綁定請求資料（App 和後端協調的內容）
2. authenticatorData 包含：
   - RP ID Hash（綁定 App ID）
   - Counter（防止重放）
   - Flags（確認使用者在場）
3. nonce 確保簽章涵蓋完整的驗證資料

如果只簽 clientData：
❌ 無法防止跨 App 重放（不同 App 可能有相同請求）
❌ 無法防止時間重放（舊請求重新送出）
❌ 無法綁定 App 身份（RP ID 未包含）
```

**clientData 是什麼？**

Apple 官方沒有強制規定格式，由 App 和後端協調：

```
常見做法 1：直接用 challenge
clientData = challenge  // 伺服器事先發的隨機值

常見做法 2：包含更多資訊
clientData = JSON.stringify({
  challenge: "abc123",
  action: "transfer",
  amount: 1000
})

重點：
- App 呼叫 DCAppAttestService.generateAssertion(keyId, clientData)
- 後端接收 clientData 並驗證
- 確保 clientData 包含防重放機制（如 challenge、timestamp）
```

**Counter 如何防重放攻擊？**

```
場景：攻擊者攔截了一個合法請求

請求 1 (合法):
  authenticatorData: counter = 5
  signature: 有效
  ✅ 伺服器驗證通過，更新 counter = 5

攻擊者重放請求 1:
  authenticatorData: counter = 5 (重複！)
  signature: 有效（簽章本身沒問題）
  ❌ 伺服器拒絕：counter 必須 > 5

請求 2 (合法):
  authenticatorData: counter = 6
  ✅ 通過（6 > 5）
```

**實務建議：**
- 必須持久化儲存 counter（資料庫）
- 不可重置 counter（否則重放攻擊可行）
- Counter 異常時應警告（可能遭受攻擊）

## 與 Attestation 的對比

### 📊 流程對比

| 特性 | Attestation（註冊） | Assertion（驗證） |
|------|-------------------|-----------------|
| **時機** | 初次註冊 | 每次 API 請求 |
| **頻率** | 一次或極少 | 高頻（每次請求） |
| **資料大小** | 大（~5KB） | 小（~100 bytes） |
| **包含憑證** | ✅ x5c 憑證鏈 | ❌ 無 |
| **公鑰資料** | ✅ 在 authData 中 | ❌ 已儲存於伺服器 |
| **Counter** | 0 | 遞增（1, 2, 3...） |
| **驗證步驟** | 9 步 | 6 步 |

### 🔄 生命週期

```
1️⃣ Attestation（首次註冊）
   ↓
   伺服器儲存：
   - keyId
   - publicKey
   - counter = 0
   ↓
2️⃣ Assertion（第一次請求）
   ↓
   伺服器驗證 → 更新 counter = 1
   ↓
3️⃣ Assertion（第二次請求）
   ↓
   伺服器驗證 → 更新 counter = 2
   ↓
   ... (持續使用)
```

## 本文小結

✅ **Assertion 結構**：authenticatorData + signature（簡潔高效）  
✅ **6 個驗證步驟**（官方）：clientDataHash → nonce → 簽章 → RP ID → Counter → challenge  
✅ **clientData 彈性**：App 和後端協調格式（challenge、JSON 或其他）  
✅ **Counter 機制**：嚴格遞增，防止重放攻擊  
✅ **nonce 設計**：綁定 authenticatorData 和 clientData，確保完整性  
✅ **性能優勢**：資料小（~100 bytes）、離線驗證、適合高頻請求

### 🎓 格式知識應用

**本文用到的所有格式：**
- **CBOR**（第9篇）→ authenticatorData 結構（精簡版）
- **ECDSA**（第8篇）→ 簽章演算法
- **DER**（第11篇）→ 簽章編碼格式
- **SHA-256**（第8篇）→ clientDataHash 和 RP ID Hash

## 下一篇預告

了解如何驗證每次請求後，最後來看看可選的 **Receipt 風險評估**：

**第18篇：Apple App Attest - Receipt 風險評估**
- PKCS#7 Receipt 完整解析
- Receipt Fields 的 type 編號對應
- Risk Metrics（風險分數）解讀
- 實務應用：何時需要檢查 Receipt？

---

## 💡 補充資料

### 常見問題

**Q: Assertion 可以快取嗎？**
A: ❌ 不行！每次請求都必須產生新的 Assertion（counter 會遞增）。快取會導致 counter 驗證失敗。

**Q: Counter 上限是多少？**
A: 2^32 - 1（約 42 億）。實務上不太可能達到。如果真的到上限，需要重新 Attestation。

**Q: 可以跳過 Counter 驗證嗎？**
A: ❌ 強烈不建議！Counter 是防止重放攻擊的關鍵機制。跳過會嚴重削弱安全性。

**Q: 驗證失敗怎麼辦？**
A:
1. 檢查是否 counter 重複（重放攻擊）
2. 檢查簽章是否有效（可能公鑰錯誤）
3. 檢查 clientDataHash 計算是否正確
4. 記錄詳細錯誤資訊供除錯

### 進階資源

- [Apple App Attest - Assertion 官方文件](https://developer.apple.com/documentation/devicecheck/validating-apps-that-connect-to-your-server)
- [ECDSA 簽章演算法](https://en.wikipedia.org/wiki/Elliptic_Curve_Digital_Signature_Algorithm)
- [防重放攻擊設計模式](https://cheatsheetseries.owasp.org/cheatsheets/Transaction_Authorization_Cheat_Sheet.html)

### 實用工具

- **Base64 解碼器**: https://www.base64decode.org/
- **ECDSA 簽章驗證**: OpenSSL command line
- **Counter 監控**: 建議使用 Prometheus + Grafana
