設備證明 Level 3:伺服器端統一驗證兩個平台
Chance Lu · 學習文章 · 2026-10-22 15:32
Chance Lu
前三站講的是「每個平台怎麼運作」:L0 的骨架(平台在保險箱裡蓋章,伺服器端驗章 )、L1 的 Apple App Attest、L2 的 Google Play Integrity。這一站是最後一站,講你的伺服器要怎麼設計,才能同時接住兩邊 。
這站學完,你要能:
① 畫出一套伺服器驗證架構:發 challenge → 綁請求內容 → 分流驗證 → 正規化 → 依策略回應。
② 說出 Apple 和 Google 各要存什麼、各要呼叫什麼。
③ 說出「附了證明但驗不過」和「根本拿不到證明」各該怎麼處理,以及理由。
④ 說出設備證明在整體防線裡的位置。
怎麼看這站的標記 :官方文件寫的內容,會在句尾附上出處。官方沒有要求、是設計上的選擇,會標 設計建議(非官方) ,你可以依自己的情況調整。
起點:L2 的對照表,伺服器要吸收的四個差異
Apple App Attest Google Play Integrity 伺服器要處理什麼
誰驗 你的伺服器自己驗 Google 伺服器解密驗證 兩套驗證邏輯
一次性註冊 要,伺服器存公鑰 不用伺服器端註冊 只有 Apple 要一張註冊表
防重送 challenge+counter standard 由 Google 擋大量重複使用 兩種防重送手段
拿到什麼 證明原料,結論自己算 判斷好的判定結果 要翻成同一種「共同判定」
先認得這站的新名詞
名詞 白話
驗證器(verifier) 伺服器裡專門驗某個平台證明的那一塊程式。Apple 一個、Google 一個。
註冊表 伺服器存「Apple 金鑰」的地方:keyId 對應到公鑰和最新的 counter。就是 L1 說的印鑑樣本存檔 。
正規化 把兩個平台長得不一樣的結果,翻譯成同一種格式 ,後面的程式只看這一種。
共同判定 正規化之後的結果,例如「App 可信嗎、裝置可信嗎、這次請求乾淨嗎」。
策略 「看到什麼判定,就怎麼回應」的規則表。
角色不變 :App 遞件、平台蓋章、你的伺服器驗章(或請 Google 的驗件窗口代驗,就是 L2 說的拿去窗口拆封)並做決定。這站所有新東西都在「你的伺服器」裡。
統一流程的骨架
App iOS/Android
你的伺服器
① 發 challenge 一次性、有時效
註冊入口 只有 Apple
③ 分流 看是哪個平台
Apple 驗證器 驗章、challenge、counter
註冊表 keyId → 公鑰+counter 寫入前查公鑰沒綁別人
Google 驗證器 requestHash、時間戳
④ 正規化 共同判定
⑤ 策略 決定回應
Google 伺服器 解密 token
② 請求+證明 (已綁請求內容)
回應:放行/限制/加驗/拒絕
圖 A:統一驗證架構。橘框的註冊入口只有 Apple 用;Google 那一路要呼叫 Google 伺服器。編號對應下面五步(點圖可放大)
發 challenge :
Apple:伺服器要提供隨機、一次性的 challenge,並記住它(Apple:Validating 頁)。WWDC26 也說,註冊(attestation)要由伺服器控制何時發起 ,理由是讓 App 維持在安全的每秒請求量以內。
Google standard 請求不需要伺服器發 challenge(Android:standard 頁)。
兩個平台共用同一個發 challenge 入口,給 challenge 很短的有效時間,並綁定發給哪個使用者或 session 設計建議(非官方) 。
綁定請求內容 :本系列把 Apple 的 clientDataHash(assertion 時)和 Google 的 requestHash 對照來看,它們都是「把這次請求的內容做成雜湊,交給平台一起簽進去」,所以可以用同一套程式產生。這個對照是本系列的整理,官方沒有這樣並列。
按平台分流驗證 :看請求來自哪個平台,交給對應的驗證器,檢查項目照 L1、L2 的官方清單。
正規化成共同判定 :兩個驗證器都輸出同一種格式 設計建議(非官方) 。
依策略回應 :用分級回應,而不是只有放行和拒絕(Android:Overview 頁)。兩個平台共用同一張策略表 設計建議(非官方) 。
註冊入口(只有 Apple) :App 送來 attestation object → Apple 驗證器驗過 → 寫入註冊表;寫入前確認這把公鑰沒有綁在別的使用者身上(Apple:Validating 頁)。
Google 那一路要不要也自己發 challenge?
官方怎麼說 :standard 請求由 Google 阻止同一個 token 被大量 重複使用(原文 reused many times),不是保證只能用一次(Android:standard 頁)。
設計建議 :Google 那一路也發 challenge,放進 requestHash 要雜湊的內容裡,就多一道「只能用一次」的檢查,兩個平台也能共用同一段程式 設計建議(非官方) 。
代價 :多一趟往返(先要 challenge 再送請求),而且伺服器要存發出去的 challenge。
為什麼值得 :因為 Google 擋的是「大量」重複使用,所以少量重送仍要靠你自己的一次性檢查擋下;對敏感操作來說,多一趟往返換這道檢查通常划算。
一句話:一個入口發 challenge,兩個驗證器各驗各的,翻成同一種判定,再用同一張策略表回應;只有 Apple 多一條註冊入口。
平台差異怎麼吸收:各存什麼、各呼叫什麼
Apple 驗證器 Google 驗證器
要存什麼 註冊表:公鑰(綁定使用者和裝置)、最新 counter、receipt;開發和正式環境分開存;一個使用者可能有多組(Apple:Validating 頁) 發出去、還沒用掉的 challenge(Apple:Validating 頁) service account 的憑證,用來呼叫 Google API(Android:standard 頁) classic:發過或看過的 nonce 紀錄(Android:classic 頁) 若採用「Google 那一路也發 challenge」:發出去、還沒用掉的 challenge 設計建議(非官方)
要呼叫什麼 驗證時不用呼叫 Apple;只有選用的風險指標,才用 receipt 去問 Apple 伺服器(Apple:Assessing fraud risk 頁) 每次都呼叫 decodeIntegrityToken 請 Google 解密(Android:standard 頁)
註冊時多做的事 確認這把公鑰沒有綁在別的使用者身上(Apple:Validating 頁) 無
Google 不用存每台裝置的資料 :官方的 standard 流程裡沒有「伺服器存裝置資料」這一步,所以你的伺服器不必為每台 Android 裝置建檔。
另外有 device recall :Google 提供替每台裝置存少量自訂資料的功能,資料存在 Google 伺服器,App 重裝後仍在(Android:Overview 頁)。
位元組層級的驗證步驟(OID、X9.62 等),照 L1 收合區塊的逐步對照表做,這裡不重講。
兩道一次性檢查都要原子化
原子化 =「讀出舊值、比較、寫入新值」一次做完,中間不讓別的請求插隊。Apple 那一路有兩道一次性檢查:challenge 的消耗 和 counter 的更新 。
攻擊者把同一個 assertion 同時 送兩次。
如果兩個請求都先讀到「challenge 還沒用過」、「counter 是 7」,就都會通過,重送成功。
所以兩道檢查都要一次完成 設計建議(非官方) :
第一道防線是 challenge :只有當這個 challenge 還沒被用過,才把它標成已用。
counter :只有當註冊表裡存的 counter 小於 這次收到的 counter,才寫入。
原子化的做法與副作用(實作時看)
做法 :資料庫的條件更新(寫入時帶上條件,並確認真的更新到一列),或對同一個 keyId 加鎖 設計建議(非官方) 。
官方規則的副作用 :Apple 要求 counter 大於上一次,WWDC26 說要 strictly increasing。所以同一台裝置的正常請求如果亂序到達(counter 9 比 8 先到),8 就會被拒。
怎麼減少這種情況 :讓 App 對同一把金鑰的敏感請求一個一個送,或被拒時重新要 challenge 再試 設計建議(非官方) 。
可疑訊號 :WWDC26 提醒,counter 停住或往回退,可能表示有被改過的 App 複本。
一句話:Apple 那一路存註冊表、自己驗;Google 那一路幾乎不存東西、每次請 Google 解密;challenge 和 counter 兩道一次性檢查都要原子化。
正規化:共同判定長什麼樣
下面是一種共同判定的格式,欄位名稱是本系列自訂的 設計建議(非官方) 。
共同判定 在問什麼 Apple 來源 Google 來源
請求乾淨 沒被重送、沒被竄改嗎? 簽章驗得過、challenge 對得上、counter 有往上加 requestDetails 對上、時間戳夠新
App 可信 是我那個沒被改過的 App 嗎? 註冊時 attestation 通過+這次用註冊過的金鑰簽+RP ID 對上 appRecognitionVerdict
取得管道/授權 使用者是從官方商店取得的嗎? 無對應 appLicensingVerdict
裝置等級 裝置可信到哪一級? 沒有分級(對應方式見下) deviceRecognitionVerdict 的標籤
額外風險訊號 有沒有其他可疑跡象? 選用的風險指標、counter 停住或倒退 recentDeviceActivity、environmentDetails
被清空的判定 :Google 重複解密同一個 token 時,裝置判定會變空、App 判定變成 UNEVALUATED(Android:standard 頁),所以在這張表裡自然會落到最低一級,不必另外處理。
Apple 通過後算哪一級 :視同 MEETS_DEVICE_INTEGRITY,再依 receipt 的風險指標偏高、或 counter 異常往下調 設計建議(非官方) 。
為什麼不能硬畫成完全對等 :Apple 驗過,代表 Apple 替「這把金鑰在 Apple 硬體上、跑在沒被改過的 App 裡」作證;但 Apple 自己也說,App Attest 無法百分之百找出作業系統被改過的裝置(Apple:DeviceCheck 頁)。
從判定到回應:決策圖
收到請求 可能附有證明
① 有證明、拿得到結果? 支援、服務正常、配額夠
沒證明/拿不到結果 當成「不可信」那一級
② 驗得過嗎? 附了證明就一定要驗過
驗不過 → 拒絕
③ 可信到哪一級? App 可信+裝置等級
高 → 放行
中 → 限制功能
低 → 加驗或請使用者補救
不可信 → 依敏感度限制或加驗
否
是
否
是
圖 B:從共同判定到分級回應。整張決策圖是設計建議(非官方);分級回應的想法來自 Google 官方建議(點圖可放大)
附了證明但驗不過 :一律當成「請求不乾淨」,直接拒絕,不走降級 設計建議(非官方) 。
沒附證明,或拿不到結果 (不支援、故障、配額用完、重試後仍失敗):當成「不可信」那一級,交給策略表決定 設計建議(非官方) 。這個模型來自 Google:它的錯誤處理指引說,重試三次仍失敗,就當作沒通過所有檢查(Android:Error codes 頁);本系列把這個模型套用到所有「拿不到結果」的情況。
加驗 =不直接放行,多要求一步驗證,例如再輸入密碼、做「我不是機器人」驗證,或請使用者自己補救 (L2 的補救對話框)。
分級回應 :Google 官方舉的例子是 Allow、Allow with limits、Allow with limits after CAPTCHA completion、Deny(Android:Overview 頁)。「高/中/低/不可信」怎麼切,要看你的業務 設計建議(非官方) 。
高等級的裝置比較少 :只認最高等級會擋掉很多人(Android:Overview 頁)。
一句話:附了證明但驗不過就拒絕;拿不到證明就當「不可信」那一級;驗過了再看可信到哪一級。
拿不到證明時:各種情況怎麼處理
情況 官方怎麼說 設計上怎麼接
裝置不支援 Apple:isSupported 是 false 時優雅略過,伺服器那邊 "can no longer require assertions"(Apple:Establishing 頁) Android:兩種請求都需要 Google Play Store 與 Google Play services(Android:classic 頁) 「不支援」是 App 自己回報的,伺服器無法驗證,所以攻擊者可以假裝不支援;因此高風險操作不能因為「不支援」就放行 設計建議(非官方) 。另一面,L0 引過的 OWASP Platform Lock-in 提醒:太依賴平台服務,會把用其他 Android 版本的正常使用者擋在外面,所以低風險操作要留一條路。
平台服務故障、逾時 Apple:serverUnavailable 是 attestation 時連不到 Apple(Apple:DCError 頁),而 assertion 在裝置本機產生(WWDC26),所以只影響新註冊 ,已註冊的裝置照常能驗 Google:每次請求都要靠 Google 伺服器解密;官方建議事先規劃大規模故障時後端怎麼運作(Android:Overview 頁) 替伺服器呼叫 decodeIntegrityToken 設逾時上限,逾時就當成「拿不到結果」 設計建議(非官方)
配額用完、被限流 Google:預設每天 10,000 次,可申請提高,建議設用量警報(Android:Setup 頁);官方也建議在盡量接近要保護的操作時才要求判定(Android:Overview 頁,原文講的是 app access risk 判定) Apple:被限流時要能把請求量降下來(Apple:Preparing 頁);assertion 通常只留給敏感時刻(Apple:Establishing 頁) 照官方方式處理:退避、提前申請提高配額、只在適當時機才要求證明
重試後仍失敗 Google:背景操作 (App 端的指引)用指數退避,重試三次仍失敗就當作沒通過所有檢查(Android:Error codes 頁);使用者主動操作時,官方建議給看得懂的錯誤訊息,請使用者重試、開網路或更新 Play Store(Android:Overview 頁) 當成「拿不到結果」,交給策略表
重裝 App、換新機 Apple:金鑰在重裝、換機、從備份還原後失效,要重新註冊;不要一看到新金鑰就拒絕(Apple:Establishing 頁、WWDC26) Google:官方 standard 流程裡伺服器沒有存裝置資料,所以 App 重新準備即可 允許重新註冊。Apple 官方建議只在這些情況才產生新金鑰,因為 "Keeping the key count low on a device helps when trying to detect certain kinds of fraud"(Apple:Establishing 頁)。官方是以裝置為單位;延伸成以帳號為單位,記錄同一帳號短時間內換了幾把金鑰、異常時加驗 設計建議(非官方)
拿不到證明時:fail-open 還是 fail-closed
fail-open =拿不到證明時,先放行(但限制) ;fail-closed =拿不到證明時,先擋下(加驗或拒絕) 。
低風險操作 (例如領每日簽到獎勵):fail-open,放行但限制,靠其他訊號補 設計建議(非官方) 。
高風險操作 (例如轉出點數):fail-closed,改成加驗,而不是直接拒絕。Google 官方建議:遇到高價值或敏感功能,"instead of denying access outright. When possible, challenge risky traffic before allowing high-value actions to proceed"(Android:Overview 頁「Challenge risky traffic when accessing high value or sensitive features」)。
理由 :fail-closed 會讓平台一出問題就擋到所有正常使用者;fail-open 會讓攻擊者故意製造「拿不到證明」來繞過:Google 也說,拿不到結果的原因可能包括 "an attempt by an attacker"(Android:Error codes 頁)。依敏感度分開處理,是在這兩個風險之間取捨。
一句話:拿不到證明時,當成「不可信」那一級;低風險放行但限制,高風險加驗或擋下。
上線策略
先只收集、不擋人 :Google 官方建議先接上 API、不執行,了解現有使用者會拿到什麼判定,再估算影響(Android:Overview 頁)。Apple 那一路也照樣做,觀察一段時間後再從最敏感的操作開始逐步收緊 設計建議(非官方) 。
分批上線與配額(實作時看)
Apple :官方的經驗法則是,每個 App 每天均勻增加不超過 1,000 萬名使用者;另外建議 attestKey 所有安裝加起來每秒少於 100 次(Apple:Preparing 頁)。1,000 萬/天換算約每秒 116 次,所以真正卡住的是每秒 100 次 那一條。
Google :預設每天 10,000 次;申請提高最多要一週處理(Android:Setup 頁),所以要在上線前先申請好。流量大幅變化要逐步上線(Android:Setup 頁)。
兩邊的限制要一起排進同一個上線計畫 設計建議(非官方) 。
建議監控的指標(實作時看)
Google 配額用量,並設用量警報(Android:Setup 頁)。
Apple attestKey 的限流錯誤,要能即時把請求量降下來(Apple:Preparing 頁);WWDC26 也說 "Implement an exponential back-off scheme to avoid hitting global rate limits."
counter 停住或倒退的次數(WWDC26)。
各平台「拿不到證明」的比例,依錯誤種類分開看 設計建議(非官方) 。
共同判定各等級的分布,以及各種回應的比例 設計建議(非官方) 。
驗證延遲,特別是呼叫 Google 解密的時間 設計建議(非官方) 。
同一帳號新金鑰的數量 設計建議(非官方) 。
一句話:先觀察再收緊;Apple 真正的上限是每秒 100 次,Google 的配額要在上線前申請好。
設備證明在整體防線裡的位置
它只是一層訊號 :Google 說 Play Integrity 最好搭配其他訊號,不該是唯一的防濫用機制(Android:Overview 頁);Apple 說沒有任何單一政策能消除所有詐騙,App Attest 提供的是可以整合進整體風險評估的資訊(Apple:DeviceCheck 頁)。
OWASP 的定位 :MASVS 把「驗證平台的完整性」(MASVS-RESILIENCE-1)和「防竄改機制」(MASVS-RESILIENCE-2)歸在韌性控制裡。韌性控制是額外的保護 ,原文說 "The absence of these measures does not in itself constitute a vulnerability";App 仍然要依自己的威脅模型(according to their threat model),滿足 MASVS 其他所有控制項(OWASP MASVS-RESILIENCE)。
所以它不能取代 :登入驗證、權限檢查、伺服器端的業務規則(例如一個帳號一天能兌換幾次)。設備證明回答的是「是不是正版 App、真裝置」,不是「這個人有沒有權限做這件事」。
一句話:設備證明讓攻擊變貴,但登入、權限和業務規則還是得在伺服器端做好。
全系列回顧
L0 :伺服器不能相信 App 自己說的話;設備證明靠三塊積木撐著:簽章與憑證鏈、硬體保護的金鑰、challenge。
L1 :Apple 先註冊一次(印鑑登記),之後每次請求蓋章,你的伺服器自己驗章 。
L2 :Google 把判定結果裝進密封信封,拿去 Google 的驗件窗口代驗 。
L3 :你的伺服器用一套流程統一接住兩邊 ,再依策略分級回應。
✔ 過關標準
能不看筆記回答下面 5 題,這條 roadmap 就走完了。
畫出統一驗證的五個步驟,以及 Apple 多出來的那條路。 看答案 收合 發 challenge → 綁定請求內容(clientDataHash/requestHash)→ 按平台分流給 Apple 或 Google 驗證器 → 正規化成共同判定 → 依策略分級回應。Apple 另有註冊入口:驗 attestation、寫入註冊表,並確認公鑰沒綁在別的使用者上。
Apple 和 Google 驗證器各要存什麼、各要呼叫什麼? 看答案 收合 Apple:存註冊表(公鑰、counter、receipt)和發出去的 challenge;驗證時不用呼叫 Apple。Google standard:存 service account 憑證,不必存每台裝置的資料(若自己發 challenge 就要存 challenge),每次呼叫 decodeIntegrityToken 請 Google 解密。
為什麼 challenge 和 counter 都要原子化? 看答案 收合 同一個 assertion 同時送兩次時,兩個請求可能都讀到「challenge 沒用過、counter 是舊值」,都通過,重送就成功了。第一道防線是 challenge 的消耗,第二道是 counter 的更新,兩道都要一次做完。(這是設計建議,官方只要求 challenge 一次性、counter 大於上次。)
「附了證明但驗不過」和「拿不到證明」各怎麼處理?fail-open 和 fail-closed 是什麼? 看答案 收合 驗不過:直接拒絕。拿不到證明:當成「不可信」那一級,交給策略表。fail-open=拿不到證明時先放行但限制,適合低風險操作;fail-closed=拿不到證明時先加驗或擋下,適合高風險操作。
設備證明能取代登入驗證嗎?為什麼? 看答案 收合 不能。它只回答「是不是正版 App、真裝置」。Google、Apple、OWASP 都說它只是整體防線的一部分;登入、權限和業務規則仍要在伺服器端做。
延伸閱讀
想把 Apple 那一路做完整:Apple 的「Validating apps that connect to your server」和「Attestation Object Validation Guide」。
想把 Google 那一路做完整:Android 的「Integrity verdicts」和 Overview 頁的安全建議段落。
想從威脅模型看整體防線:OWASP MASVS 的 MASVS-RESILIENCE,以及 MASTG 的對應測試。
參考來源