在雲端 Mac 驗收 iOS Ed25519 簽章:標準向量與位元組契約

在雲端 Mac 驗收 iOS Ed25519 簽章:標準向量與位元組契約

在雲端 Mac 驗收 iOS Ed25519 簽章:標準向量與位元組契約

跨時區團隊交接簽章設定時,經常遇到「伺服器端簽署成功,iOS 卻驗簽失敗」的情況:雙方顯示的 JSON 一模一樣,重新產生金鑰也沒有改善。先在雲端 Mac 建立不依賴業務 API 的驗簽基準,再核對原始位元組,才能釐清問題出在密碼學 API 的呼叫、傳輸解碼,還是訊息重建。以下使用 Swift 與 CryptoKit,僅採用公開測試資料,不需要正式環境的私密金鑰。

先確定演算法與輸入契約

約定使用一般 Ed25519,公開金鑰採用 32 位元組的原始表示,簽章採用 64 位元組的原始表示。CryptoKit 對應的型別是 Curve25519.Signing.PublicKey;不要因為名稱相近,就改用金鑰協商的型別。

簽署端必須說明,簽署的對象是回應本文的原始位元組,還是經過特定規範化處理的結果。若簽署原始本文,用戶端就應先驗簽再解析,不能解析 JSON 後重新編碼,再拿來驗證。若雙方約定採用規範化處理,就必須固定規則版本與測試樣本。

Ed25519 簽章能證明訊息與受信任的公開金鑰相符,但不提供保密性,也不會自動防止重放攻擊。若公開金鑰來自同一份可遭替換的回應,即使驗簽成功,仍不足以建立信任。

另外也要區分一般 Ed25519 與 Ed25519ph。先對訊息計算摘要,再交給一般介面,並不等於改用預雜湊變體;協定不能只用「都是 Ed25519」就省略這些條件。

以公開標準向量跑通最小驗證

在已安裝開發工具的雲端 Mac 上執行 xcrun swift --version,記錄工具鏈版本與 sw_vers 的輸出。將以下內容存成 verify.swift,再執行 xcrun swift verify.swift。程式碼採用 RFC 8032 的空訊息測試向量;這些公開的十六進位資料不是業務金鑰。

import Foundation
import CryptoKit

enum FixtureError: Error {
    case invalidHex
    case checkFailed
}

func hex(_ text: String) throws -> Data {
    let chars = Array(text.utf8)
    guard chars.count % 2 == 0 else {
        throw FixtureError.invalidHex
    }
    var bytes = [UInt8]()
    for offset in stride(from: 0, to: chars.count, by: 2) {
        let pair = String(
            decoding: chars[offset..<(offset + 2)],
            as: UTF8.self
        )
        guard let value = UInt8(pair, radix: 16) else {
            throw FixtureError.invalidHex
        }
        bytes.append(value)
    }
    return Data(bytes)
}

func require(_ value: Bool) throws {
    guard value else {
        throw FixtureError.checkFailed
    }
}

let publicBytes = try hex(
    "d75a980182b10ab7d54bfed3c964073a0" +
    "ee172f3daa62325af021a68f707511a"
)
let signature = try hex(
    "e5564300c360ac729086e2cc806e828a8" +
    "4877f1eb8e5d974d873e0652249015555" +
    "5fb8821590a33bacc61e39701cf9b46bd" +
    "25bf5f0595bbe24655141438e7a100b"
)
try require(publicBytes.count == 32)
try require(signature.count == 64)

let key = try Curve25519.Signing.PublicKey(
    rawRepresentation: publicBytes
)
try require(key.isValidSignature(signature, for: Data()))
try require(!key.isValidSignature(
    signature, for: Data([0x00])
))

var altered = signature
altered[altered.startIndex] ^= 1
try require(!key.isValidSignature(altered, for: Data()))
try require(!key.isValidSignature(
    Data(signature.dropLast()), for: Data()
))

var rejectedKey = false
do {
    _ = try Curve25519.Signing.PublicKey(
        rawRepresentation: Data(publicBytes.dropLast())
    )
} catch {
    rejectedKey = true
}
try require(rejectedKey)

腳本成功時不會輸出任何內容;失敗時會拋出錯誤,並以非零狀態結束。這裡使用會拋出錯誤的檢查,而非 assert,以免斷言在最佳化編譯時被移除。先確認這組固定資料能通過,再接入業務樣本,否則同時排查網路與密碼學 API,只會增加需要考量的變因。

將文字差異縮小為位元組差異

保留一份簽署前的訊息檔案,以及一份用戶端實際收到的檔案。公開測試資料可以先用 cmp 比較,再用 xxd -g 1 找出第一個差異;正式環境的本文可能含有敏感資訊,不應直接寫入共用日誌。

檢查時,先從容易被編輯器更動的內容著手:結尾是否多了換行、換行採用 LF 還是 CRLF、檔案開頭是否帶有 UTF-8 BOM,以及字元是否經過 Unicode 正規化。即使帶重音符號的字元看起來相同,也可能對應不同的位元組序列。

建立檔案時,不要隨手使用 echo,因為它可能附加換行。讀取訊息時應直接使用檔案的原始資料,不要先轉成字串,再轉回資料。若協定確實簽署字串,就明確指定 UTF-8,並針對結尾換行、空訊息及組合字元分別建立固定測試資料。

獨立驗收傳輸封裝

若 API 傳送的是 Base64,應先解碼,再檢查長度;128 個十六進位字元不代表簽章有 128 位元組。原始公開金鑰也不等於帶有標頭資訊的金鑰檔案,不能將封裝後的整份資料直接傳入 rawRepresentation。

解碼失敗時,不得改用空資料繼續驗證。內部應將「封裝格式無效」、「長度錯誤」與「簽章不符」區分為不同的錯誤型別,對外則應避免揭露不必要的解析細節。

建立能找出接線錯誤的反例集

最小反例集至少要涵蓋:正確訊息搭配正確簽章、訊息增加一個零位元組、簽章翻轉一個位元、簽章遭截短、公開金鑰遭截短,以及使用另一把有效的公開金鑰驗證原簽章。最後一項必須使用真正有效且不同的公開金鑰,不能只靠損壞的公開金鑰觸發初始化失敗。

業務層還應加入「本文正確,但簽章屬於上一則訊息」的樣本,檢查程式碼是否錯誤地重用了快取結果。若簽署內容包含版本、有效期限或用途識別,也要確認這些欄位確實包含在被簽署的位元組中,而不只是放在未簽署的外層物件裡。

這些測試要驗證的是拒絕路徑,不應用隨機破壞資料來取代固定測試資料。只有固定樣本,才能讓不同節點、不同班次的成員重現同一個失敗。

以證據清單完成跨時區交接

交接時,記錄提交編號、系統與工具鏈版本、演算法變體、公開金鑰識別、訊息位元組數、簽章位元組數,以及各測試案例的結果。公開測試資料可以一併保存;真實業務訊息則應依敏感等級控管保存,摘要也不能取代原始訊息來完成驗簽。

命令列基準通過後,將相同的固定測試資料加入 iOS 測試目標,呼叫應用程式實際使用的驗簽封裝。這樣才能涵蓋 Base64 解碼、公開金鑰選擇與訊息讀取路徑,而不只是證明 CryptoKit 本身能正常運作。

最終驗收標準不是「驗過一次」,而是正確輸入能穩定通過、每個反例都明確失敗、兩端對被簽署的位元組沒有歧義,而且另一班成員不需要私密金鑰,就能重新執行全部檢查。

常見問題

客戶端驗收需要取得簽署端的私鑰嗎?

不需要。可信公鑰、原始訊息與簽章即可驗證。測試可保存公開標準向量,但不應把正式環境私鑰交給驗收流程。

JSON 欄位內容相同,為何仍然驗簽失敗?

Ed25519 驗證的是位元組,不是 JSON 語意。欄位順序、空白與 Unicode 表示不同都會改變輸入;應驗證原始訊息,或事先約定雙方一致的正規化格式。

為下一項任務選擇雲端 Mac

請依團隊主要所在地選擇節點,使用整台專屬的實體 Mac mini。透過瀏覽器使用遠端桌面,需要較新版本的瀏覽器與穩定的網路;下單前請確認規格、計費週期及實際供應狀況。

讓下一次連線更靠近團隊

先確認規格與計費週期,再選擇靠近團隊主要使用地區的節點。