# 設備證明 Level 0：伺服器為什麼不能相信 App（威脅模型與基本觀念）

Chance Lu

一個 App 上架之後，會被裝進**成千上萬支你看不到、也管不到的手機**。這一關先不碰任何平台的 API，只回答一個問題：**伺服器憑什麼相信「送請求來的，真的是我的正版 App」？**

後面三站（Apple 的做法、Google 的做法、伺服器怎麼統一驗）都建立在這關的觀念上，所以這關是**地基**。

> 這關學完，你要能用自己的話回答這 5 件事：
> ① 為什麼伺服器不能相信 App 自己說「我很安全」？
> ② 「設備證明」是誰、替誰、擔保什麼？
> ③ 數位簽章、憑證鏈、硬體保護的金鑰，各自負責什麼？
> ④ challenge 怎麼擋住「重送」？
> ⑤ 證明能做到什麼、做不到什麼？

## 先認得幾個名詞

下面這幾個詞，第一段馬上會用到。先掃一眼就好，不用背。

| 名詞 | 白話 |
| --- | --- |
| **client（客戶端）** | 使用者手上的 App。對面是你自己管的**伺服器**。 |
| **API** | 伺服器開給 App 呼叫的窗口，例如「兌換優惠券」的窗口。 |
| **請求（request）** | App 透過 API 送給伺服器的一則訊息，例如「幫我兌換這張優惠券」。 |
| **重新打包（repackaging）** | 把 App 的安裝檔拆開、改掉程式，再重新簽名、包回一個「長得一樣」的 App。 |
| **hook（掛鉤）** | App 執行到一半時，**攔下**它呼叫的某個函式，換成攻擊者想要的結果。 |
| **root／越獄** | 拿到手機作業系統的**最高權限**。Android 叫 root，iPhone 叫越獄（jailbreak）。系統原本的保護，這時都能被關掉。 |
| **模擬器（emulator）** | 在電腦上「假裝」成一支手機的軟體。 |
| **重送（replay）** | 把一則以前的合法請求，原封不動再送一次。 |

## 核心問題：App 在別人手上，它說的話不能全信

想像你有一支兌換優惠券的 API `/coupons/redeem`。App 送請求時順便說：「我是正版，這支手機也沒被 root。」

問題是：**這句話是 App 自己說的。**攻擊者至少有六種方法讓它說謊，或乾脆不需要它：

- **① 重新打包**：把「檢查有沒有 root」那段程式刪掉，回報永遠是「沒問題」。
- **② hook**：檔案不動，執行時把檢查函式的回傳值換成「安全」。
- **③ root／越獄的手機**：攻擊者握有整支手機的最高權限，App 看到的環境可以全是假的。
- **④ 模擬器**：一台電腦開幾百支「假手機」，大量註冊、刷優惠。
- **⑤ 腳本／機器人**：根本不用 App，照著請求格式自己寫程式打 API。
- **⑥ 重送**：錄下一則真請求，之後一直重複送。

使用者的手機（你管不到）
  ① 重新打包：改過的 App拆開、刪掉檢查、重新簽名
  ② hook：執行中被竄改把檢查結果換成「安全」
  ③ root／越獄的手機系統的保護都能被關掉
  ④ 模擬器：電腦假裝成手機一台電腦開幾百支
  ⑤ 腳本／機器人沒有 App，直接打 API
  ⑥ 重送把錄下的舊請求再送一次

  你的伺服器收到的請求看起來全都一樣分不出誰是真的

圖 1：六種路線送來的請求，伺服器光看內容很難分辨真假（點圖可放大）

Apple 官方文件說得很直白：**你不能靠 App 自己的邏輯替自己做安全檢查，因為被入侵的 App 可以偽造結果。**OWASP 的行動 App 安全標準（MASVS）則把這類手機端的防護（韌性控制）定位成**額外的保護**：少了它本身不算漏洞，App 仍要依自己的威脅模型滿足其他所有控制項。

> 一句話：從 client 送來的所有東西，包括「我很安全」這句話，都可能是假的。我們需要一個**攻擊者造不出來**的東西。

## 設備證明：請一個攻擊者造不出的「第三方」來擔保

**比喻**：網購時，賣家自己寫「保證正品」沒什麼用。但如果附上一張**原廠開的、有防偽印章的保證書**，你就能自己檢查真假。

**設備證明（attestation）**就是那張保證書：請**作業系統／平台**出具一份證明，內容是「這是某某正版 App，跑在一支真的裝置上」。平台背後有**硬體保護的金鑰**撐腰（下面會講），所以攻擊者很難偽造。

整件事有三個角色：

- **App**：遞件的人。它向平台申請證明，再轉交給伺服器。
- **平台**：開證明的人＝**作業系統＋安全硬體＋平台伺服器**。
- **你的伺服器**：驗證明、做決定的人。

**重點：判定一定在伺服器端做，App 只負責把東西帶過去。**App 會向平台拿一份**它自己改不了**的證明（Apple 用簽章、Google 用加密），交給你的伺服器。Apple 那份由你的伺服器**自己驗**；Google 那份由你的伺服器**送回 Google 解開**、拿回判定結果。理由都一樣：手機上的 App 可能已經被改過，所以不能讓它說了算。

Apple 和 Google 各有一套實作，後面兩站再講。

> 一句話：設備證明＝平台替 App 開的保證書；App 只負責轉交，**伺服器負責驗**。

## 為什麼攻擊者造不出這張保證書？靠三塊積木

保證書之所以可信，靠的是三個觀念。一次講一塊：

1. **數位簽章（加上憑證鏈）**：只有特定的人蓋得出的章，而且查得到是誰擔保的。
2. **硬體保護的金鑰**：那顆章被鎖在手機的保險箱裡，偷不走。
3. **challenge**：每張保證書只能用一次，擋住重送。

### 積木一：數位簽章與憑證鏈——只有它蓋得出來的章

**比喻：印章＋印鑑證明。**一組金鑰分成兩半：

- **私鑰（private key）**＝印章，只有主人拿著。
- **公鑰（public key）**＝公開的印鑑樣本，誰都能拿來比對。

**簽章**：主人用私鑰替一段資料算出一個「章」。**驗證**：別人用公鑰檢查這個章。對得上，就代表兩件事：這段資料**確實出自私鑰主人**，而且**一個字都沒被改過**。

小例子：資料是 `app=com.example.shop`，附上簽章。攻擊者把它改成 `app=com.evil.shop`，簽章就對不上了，驗證失敗。

那攻擊者重新打包後，自己再簽一次不就好了？可以簽，但他只能用**自己的私鑰**，蓋出來的章跟原廠的不一樣。所以簽章擋的不是「有沒有簽」，而是**「是不是對的人簽的」**。

但還有一個問題：**伺服器怎麼知道這把公鑰真的屬於「平台認可的真裝置」**，而不是攻擊者自己生的？

答案是**憑證（certificate）**：一張寫著「這把公鑰屬於誰」的文件，由**上一層的單位蓋章擔保**。上一層又被更上一層擔保，一路到最頂端的**根憑證（root certificate）**。中間那幾層就叫**中間憑證**，整串合起來叫**憑證鏈**。
比喻：行員的章由分行主管蓋章擔保，分行主管的章再由總行蓋章擔保；你只要認得總行的章，就能一路信到行員的章。

根憑證平台公開的伺服器事先就存好
  中間憑證由根憑證蓋章擔保
  裝置金鑰憑證由中間憑證蓋章擔保
  被簽的資料由裝置裡的私鑰簽名
  
  擔保擔保簽名
  
  伺服器驗證：從資料往回，一層一層對到根
  每層的章都對、最後接上事先存好的根 → 才相信

圖 2：上面的線＝誰替誰擔保；下面的線＝伺服器驗證的方向（點圖可放大）

誰來驗這條鏈，看平台：Apple 的做法要**你的伺服器自己驗**，一路接到 Apple 的根；也有平台是由**平台伺服器替你驗**，你只拿回結果（Level 2 再講）。

> 一句話：簽章證明「資料出自這把私鑰、沒被改」；憑證鏈證明「這把私鑰是平台認可的」。

### 積木二：硬體保護的金鑰——印章鎖在保險箱裡

**問題**：如果私鑰就放在手機的一般儲存空間，root 過的手機可以直接把它**複製走**，拿去別的地方簽。

**解法**：手機裡有一塊**跟主系統隔開的安全區**。私鑰在裡面產生，**永遠不出來**；App 只能把資料遞進去，請它簽好再拿回來。

**比喻：櫃檯後面上鎖的蓋章機。**你可以遞紙進去請它蓋章，但沒辦法把印章拿走。

不同平台的名字不一樣，概念相同：

- **Apple：Secure Enclave**。晶片裡一個和主處理器隔開的安全子系統，設計上**就算主系統的核心被攻破**，也能保護裡面的資料。
- **Android：TEE 與 StrongBox**。**TEE**（可信執行環境）是跟一般系統隔開的安全區；**StrongBox** 是一顆**獨立的安全晶片**，更安全但比較慢。金鑰放在這類硬體裡時，即使 Android 系統被入侵，**金鑰也拿不出來**。

**要記住一個細節**：拿不走，不代表不能用。Android 文件說得很清楚：App 被攻破時，攻擊者**偷不到金鑰，但仍然可以在那支手機上使用它**。這點在「做不到什麼」會再用到。

**兩家用法不一樣**：

- **Apple**：會替這支裝置上的 App 產生一把**專屬金鑰**，鎖在 Secure Enclave 裡。你的伺服器記下它的公鑰，之後 App 的每個請求都能請這把金鑰簽名，伺服器就能確認「還是同一支裝置上的同一個 App」。
- **Google**：硬體一樣派上用場，但用在**平台自己判定裝置**那一端；金鑰**不交給你**，你拿到的是一份一次性的判定結果，不能拿來簽之後的每個請求。

> 一句話：私鑰在安全硬體裡產生、永遠不離開，所以簽章**只可能來自那一支實體裝置**。

### 積木三：challenge——讓每張保證書只能用一次

**問題**：攻擊者從一支真手機拿到一張合法證明，然後在模擬器上**一直重複附上它**。簽章是真的、憑證鏈也是真的，前兩塊積木都擋不住。

**解法**：伺服器先發一個 **challenge**（也常叫 **nonce**，意思是「只用一次的數字」）：一串隨機、猜不到的值。平台產生證明時，要把它**一起簽進去**。

伺服器收到證明後，檢查三件事：

1. **對不對**：裡面的 challenge 是不是我剛剛發的那個？
2. **用過沒**：用過一次就作廢，第二次來就拒絕。
3. **過期沒**：只接受在短時間內回來的。

兩家做法不太一樣：

- **Apple**：文件就叫 **challenge**。要你的伺服器**自己發**一次性的隨機 challenge，至少 16 bytes（約 128 位元，亂到猜不中），並記住發了哪個；App 交給平台時，送的是它的雜湊值（參數名 `clientDataHash`）。
- **Google**：文件**不叫 challenge**，而且兩種請求方式名稱不同。舊的一種叫 `nonce`，由你的伺服器發一次性的值；新的一種叫 `requestHash`，由**平台自動防重送**，你只要把「這次請求的內容」做成摘要綁進證明裡，並檢查時間戳（太舊就不收）。Google 也提醒**不要把證明結果快取起來重複用**。

上面「三件事」是自己發 challenge 時的檢查法；平台代管時，同樣的事由平台替你做掉一部分。

> 一句話：challenge 讓證明綁定「這一次」，錄下來再送也沒用。

## 一張圖看完整個流程

把三塊積木放回流程裡，就是下面這張圖。先看線怎麼流，再看下面的步驟說明。

平台作業系統＋安全硬體＋平台伺服器
  App使用者手機上
  你的伺服器唯一可信的裁判
  
  ① 我要一個 challenge
  ② 給你一串亂數（我記下來）
  ③ 請幫我出證明（附 challenge）
  ④ 簽好名的證明
  ⑤ 證明＋原本的請求
  ⑥ 驗證自己驗，或請平台伺服器代驗
  ⑦ 放行／拒絕／另外處理

圖 3：通用流程。有的平台不用你發 challenge、改由平台防重送；但「平台作證 → 伺服器端驗 → 你決定」的骨架都一樣（點圖可放大）

1. **要 challenge**：App 先跟伺服器要一串一次性的亂數。
2. **記下來**：伺服器發出 challenge，同時記住它（之後要比對、要作廢）。
3. **請平台出證明**：App 把 challenge 交給平台。
4. **平台簽名**：安全硬體裡的金鑰替這台裝置作證，平台（含它的伺服器）把 App 身分、裝置狀態、這次的 challenge 一起簽成證明。
5. **轉交**：App 把證明和真正要做的請求一起送給伺服器。
6. **在伺服器端驗證**：有的平台要你自己驗憑證鏈，有的平台由平台伺服器替你驗、你拿回結果；兩種都不在手機上驗。無論哪種，都要確認 challenge 對得上、是自己的 App。
7. **做決定**：通過就放行；不通過要拒絕還是改走別的驗證，是伺服器的**政策**，Level 3 再談。

> 一句話：伺服器出一張一次性的單子 → 平台在保險箱裡蓋章 → 伺服器自己驗章。

## 證明做得到什麼、做不到什麼

先講結論：**證明是一個很強的「風險訊號」，但不是百分之百的保證。**

| ✅ 做得到 | ⚠️ 做不到／要小心 |
| --- | --- |
| 讓改過的 App、模擬器、腳本**很難**拿到有效證明，大幅墊高攻擊成本。 | **不是 100%**。Apple 自己承認：改了作業系統的攻擊者**可能繞過**限制，也無法絕對確定地偵測詐騙。 |
| 給伺服器一個**攻擊者很難偽造**的判斷依據。 | **一支被攻破的真手機**，可能替很多假請求出證明（呼應「拿不走，但能用」）。 |
| challenge 讓**錄下來重送**失效。 | **不能單獨使用**。Google 說它最好搭配其他訊號，不該是唯一的防濫用機制；OWASP 也把這類保護定位為額外的保護，App 仍要依威脅模型滿足其他控制項（例如登入驗證）。 |

### 誤判：正常使用者也可能被擋

- **不是每支裝置都支援**。Apple 文件要求先檢查能不能用，不支援就「優雅地略過」。
- **自己改過系統的正常人**。在 Android 13 以上的手機，平台要能確認**開機程式（bootloader）沒被解鎖**、系統是**原廠認證的版本**，才會認定「這是正版裝置」。自己刷系統的玩家就可能不過。
- **平台服務有流量上限**。Apple 要求分批上線，別讓大量裝置同時打它的服務；Google 預設每個 App 每天 10,000 次請求上限。服務忙或出錯時，好人也拿不到證明。
- **用其他 Android 版本的正常人**。OWASP MASVS 提醒：依賴 App Attest、Play Integrity 這類平台服務，可能把用其他 Android 版本（例如 LineageOS）的正常使用者排除在外，這叫**平台綁定（platform lock-in）**。

所以「沒拿到證明」**不等於**「一定是壞人」。失敗時要怎麼處理，Level 3 再談。

### 隱私：要讓平台參與，就得交出一些資訊

- Google 提醒：放進請求裡給平台比對的值，**Google 看得到明文**，所以敏感資料要先雜湊（壓成一段看不出原文的指紋）或加密再放。
- Apple 會為每支裝置記錄一些**無法重設**的使用指標（例如這支裝置上產生過幾次證明）。這表示**平台會記住這台裝置的使用紀錄**。

> 一句話：證明能**大幅降低風險**，但會誤判、有隱私成本、也不是萬能。它是「加分的訊號」，不是「唯一的門」。

## ✔ 過關標準

能不看筆記回答下面 5 題，這關就過了。

1. App 自己偵測「沒被 root」再回報給伺服器，為什麼不夠？
因為檢查程式本身跑在攻擊者手上。重新打包可以刪掉它，hook 可以把結果換成「安全」，腳本甚至不需要 App。
2. 設備證明是誰、替誰、擔保什麼？誰負責驗？
平台（作業系統＋安全硬體＋平台伺服器）替 App 擔保「這是正版 App、跑在真的裝置上」。App 只轉交；驗證在**伺服器端**做（你的伺服器自己驗，或請平台伺服器代驗）。
3. 簽章、憑證鏈、硬體金鑰各擋掉哪一種造假？
- 簽章：擋「內容被改」。
    - 憑證鏈：擋「攻擊者自己生一把金鑰冒充」。
    - 硬體金鑰：擋「把私鑰複製到別處使用」。
4. 自己發 challenge 時，伺服器收到證明要檢查哪三件事？
是不是我發的、有沒有用過、有沒有過期。三者都過才算數，這樣錄下來重送就沒用。
5. 舉兩個「證明做不到」的例子。
任兩個即可：改了作業系統的攻擊者可能繞過；一支被攻破的真手機能替很多假請求出證明；不支援的裝置或自刷系統的正常人會被誤擋；不能當唯一的防線。

## 下一站

Level 1 會拆開 Apple 的實作，看這套骨架在 iPhone 上長什麼樣。

## 參考來源

- [Apple Developer — Establishing your app's integrity](https://developer.apple.com/documentation/devicecheck/establishing-your-app-s-integrity)（App 不能自己檢查自己、Secure Enclave 金鑰、一次性 challenge 至少 16 bytes、不支援就略過）
- [Apple Developer — Validating apps that connect to your server](https://developer.apple.com/documentation/devicecheck/validating-apps-that-connect-to-your-server)（伺服器發 challenge 並記住、驗證憑證鏈到 Apple 的根）
- [Apple Developer — Assessing fraud risk](https://developer.apple.com/documentation/devicecheck/assessing-fraud-risk)（改了作業系統可能繞過、一支被攻破的裝置服務多人、無法絕對偵測）
- [Apple Developer — Preparing to use the App Attest service](https://developer.apple.com/documentation/devicecheck/preparing-to-use-the-app-attest-service)（無法重設的裝置指標、分批上線）
- [Apple Platform Security — The Secure Enclave](https://support.apple.com/guide/security/the-secure-enclave-sec59b0b31ff/web)（與主處理器隔離、核心被攻破仍能保護）
- [Android Developers — Play Integrity API overview](https://developer.android.com/google/play/integrity/overview)（要搭配其他訊號、明文可見、不要快取、每日請求上限）
- [Android Developers — Make a standard API request](https://developer.android.com/google/play/integrity/standard)（由 Google 伺服器解密驗證、自動防重送、綁定請求內容
- [Android Developers — Integrity verdicts](https://developer.android.com/google/play/integrity/verdicts)（比對請求值與時間戳、Android 13+ 需鎖定開機程式與原廠系統）
- [Android Developers — Key and ID attestation](https://developer.android.com/privacy-and-security/security-key-attestation)（金鑰憑證鏈、在伺服器驗，不在裝置上驗）
- [Android Developers — Android Keystore system](https://developer.android.com/privacy-and-security/keystore)（TEE／StrongBox、金鑰拿不出來但可在裝置上使用）
- [OWASP MASVS — MASVS-RESILIENCE](https://mas.owasp.org/MASVS/11-MASVS-RESILIENCE/)（韌性控制是額外的保護、仍須依威脅模型滿足其他控制項、平台綁定會排除正常使用者）
- [OWASP MASTG — Tampering and Reverse Engineering](https://mas.owasp.org/MASTG/0x04c-Tampering-and-Reverse-Engineering/)（重新打包、執行中 hook、root／越獄、模擬器）
- [MDN — Digital signature](https://developer.mozilla.org/en-US/docs/Glossary/Signature/Security)、[MDN — Digital certificate](https://developer.mozilla.org/en-US/docs/Glossary/Digital_certificate)（私鑰簽、公鑰驗；憑證綁定公鑰並由上層簽發）
