最近很流行自簽證書進行 HTTPS 解密,然後就有不少人通過修改蘋果的內購回執實現對 App 的破解。實際上驗證購買應該是 App 連接服務器,服務器來和蘋果的服務器進行通信,然後將結果發送給 App 的——但不少開發者(包括個人開發者以及企業開發者)懶得去專門維護服務器,所以直接用 App 和蘋果的服務器進行通信,這就給中間人攻擊提供了機會。
中間人攻擊,就是說當 A 和 B 進行通信時,C 對 A 假裝自己是 B,對 B 假裝自己是 A,這樣通信過程就從 A <—> B 變成了 A <–> C <–> 乙, 此時 A 和 B 都還以為對方是可信的,可通信的內容早不知道被 C 改了多少了。
通常來說我們使用 HTTPS 加密傳輸就已經能夠很好的對抗中間人攻擊了,但對於這種特殊情況,用戶主動去信任一個“不安全”(自己簽名)的證書進行 HTTPS 解密,那 HTTPS 加密在客戶端就成了不可信的。
這種情況下,我們就需要自己實現對數據的保護。
安全性和完整性
對數據進行保護,主要是兩點,“安全性”和“完整性”,前者保護數據不被第三方窺探,後者則保證你的數據在通信的過程當中不會被篡改,當然,通常來講加密也一定程度保證了數據的完整性,畢竟如果被篡改了,可能也就無法解密了。
但只依靠加密,就給中間人攻擊提供了機會。所以,我們要在 HTTPS 的安全性前提下,給自身代碼添加完整性驗證。
簽名
所謂“簽名”,和現實中的簽名不同,這裡的簽名是指通過一些特殊的算法,將數據的特徵提取出來,算法保證了一旦數據被改變,哪怕是一個符號,那麼提取的特徵就完全不同了,這樣,我們只要在發送數據時包含這個特徵,在收到後重新驗證它,就能確保數據在傳輸過程中沒有被篡改。
重簽名
有一個問題就是如果中間人在改了數據之後重新簽名,我們假定選擇的特徵提取算法被洩露了,中間人在更改了數據之後使用相同的算法重新簽名(也就是提取特徵),那客戶端依然會認為數據是完整的——所以我們要在客戶端和服務端約定一個“鹽”值,你也可以理解為密碼。
它不能讓你把數據從特徵值裡恢復出來(都說了是提取特徵,並不是加密,這個過程是不可逆的),但雙方約定了密碼之後在簽名時將這個“密碼”追加或者前置在數據中,這樣一旦中間人更改了數據,他即使可以使用相同的算法重新簽名,由於不知道我們預先設置的密碼,他簽出來的結果還是不同,這樣客戶端就知道這個數據是被篡改過的了。
重放攻擊
既然無法對數據進行修改,那麼駭客還可以抓取原本生效的數據報,重新再來發送,這樣服務器或者客戶端驗證了簽名和加密,數據完全正確,但實際上卻是來自另一個版本客戶端的傳輸數據——要應對這個情況,就要在之前數據結構的基礎上,再增加一個概念——“時間戳”,通常我們使用 utc 的時間戳,其實就是從格林威治時間1970年01月01日到現在的總計秒數——當然你也可以用其他的能精確到秒(甚至毫秒)的時間格式——把時間戳也追加在數據裡一起進行簽名,避免時間戳被篡改,然後客戶端和服務端進行通信時,檢查時間戳是否在允許範圍內,比如1秒內,或者30秒內等等,如果間隔太久,即時簽名驗證通過,也不能信任這個數據了,這樣,就避免了重放攻擊。
代碼
簽名的算法有很多,比如常見的 MD5、SHA1 等等等等,這些算法都可以直接拿來簽名使用,這裡我使用了 RSA 證書籤名,實際上就是用更長更複雜的密碼。
我們把要傳輸的數據和時間戳放在一起生成一個字符串,然後對字符串進行 SHA256 提取特徵,然後將特徵用 RSA 證書私鑰簽名;
等到客戶端收到數據,就將數據和時間戳用相同的方式生成字符串,進行 SHA256 提取特徵,然後用公鑰進行簽名驗證。
這樣公鑰私鑰的配對,好處在於,不像約定密碼那樣容易洩露——畢竟密碼就寫在程序中,很容易被找到——公鑰可以隨意傳播,它的作用僅僅是驗證簽名而不能對數據進行簽名,無需擔心洩露問題。
生成密鑰
打開任意你喜歡的終端,輸入命令 的openssl ,然後再輸入命令 genrsa -出 私人的.ESTAB 1024 來生成私鑰,後邊的數字越大就越安全,但你也要權衡簽名的時間成本,通常來說,1024 足夠了。
最後輸入命令 RSA -在 私人的.ESTAB -出版 -出 上市.ESTAB 生成公鑰,這個公鑰就是你要放在客戶端中分發到最終用戶手中的密鑰了。(使用命令 出口 來退出 openssl)
實際上,生成的密鑰文件其實就是一個純文本文檔,密鑰本質上是二進制的,但它們都被 base64 編碼成文本保存。
使用命令 cat 私人的.ESTAB 來查看你的私鑰:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
-----BEGIN RSA PRIVATE KEY----- MIICXQIBAAKBgQDKoeRzRVf8WoRSDYYqUzThpYCr90jfdFwTSXHJ526K8C6TEwdT UA+CFPQPRUg9jrYgFcown+J2myzO8BRLynD+XHb9ilLb49Mqk2CvDt/yK32lgHv3 QVx14Dpb6h8isjncSF965fxBxlHGbvPwnHkJ9etRIYdYV3QpYohFszH3wQIDAQAB gjIJ/dmBAkEA0QarqdWXZYbse1XIrQgBYTdVH9fNyLs1e1sBmNxlo4QMm/Le5a5L XenorEjnpjw5YpEJFDS4ijUI3dSzylC+QQJARqcD6TGbUUioobWB4L9GD7SPVFxZ AoGAFhKqkw/ztK6biWClw8iKkyX3LURjsMu5F/TBK3BFb2cYe7bv7lhjSBVGPL+c TfBU0IvvGXrhLXBb4jLu0w67Xhggwwfc86vlZ8eLcrmYVat7N6amiBmYsw20GVi LhF7zbYPIPGbHw+crP13THiYIYkHKJWsQDr8SXoNQ96TQsInTXUAmF2gzs/AwdQg UFmePbo1G2BXqMA43JxqbIQwOLZ03zdw6GHj6EVlx369IAECQQD4K2R3K8ah50Yz c3+EgcxRoO4bNuCFDA8VO/InP1ONMFuXLt1MbCj0ru1yFCyamc63NEUDAQJBALt7 PjGgsKCRuj6NnOcGDSbDWIitKZhnwfqYkAApfsiBQkYGO0LLaDIeAWG2KoCB9/6e lAQZnYPpOcCubWyDq4ECQQCrRDf0gVjPtipnPPS/sGN8m1Ds4znDDChhRlw74MI5 FydvHFumChPe1Dj2I/BWeG1gA4ymXV1tE9phskV3XZfa -----END RSA PRIVATE KEY----- |
使用命令 cat 上市.ESTAB 來查看公鑰:
1 2 3 4 5 6 |
-----BEGIN PUBLIC KEY----- MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDKoeRzRVf8WoRSDYYqUzThpYCr 90jfdFwTSXHJ526K8C6TEwdTUA+CFPQPRUg9jrYgFcown+J2myzO8BRLynD+XHb9 ilLb49Mqk2CvDt/yK32lgHv3QVx14Dpb6h8isjncSF965fxBxlHGbvPwnHkJ9etR IYdYV3QpYohFszH3wQIDAQAB -----END PUBLIC KEY----- |
蟒蛇
為了方便使用,我們直接把上面私鑰的代碼複製,用一個變量來保存這個私鑰:
1 2 3 4 5 6 7 8 |
#私钥文件 priKey = '''-----BEGIN RSA PRIVATE KEY----- MIICXQIBAAKBgQDKoeRzRVf8WoRSDYYqUzThpYCr90jfdFwTSXHJ526K8C6TEwdT UA+CFPQPRUg9jrYgFcown+J2myzO8BRLynD+XHb9ilLb49Mqk2CvDt/yK32lgHv3 …… lAQZnYPpOcCubWyDq4ECQQCrRDf0gVjPtipnPPS/sGN8m1Ds4znDDChhRlw74MI5 FydvHFumChPe1Dj2I/BWeG1gA4ymXV1tE9phskV3XZfa -----END RSA PRIVATE KEY-----''' |
然後是簽名代碼:
1 2 3 4 5 6 |
def sign(data): key = RSA.importKey(priKey) h = SHA256.new(data.encode('utf8')) signer = PKCS1_v1_5.new(key) signature = signer.sign(h) return base64.b64encode(signature).decode('utf8') |
這裡 標誌() 直接傳入字符串即可,它會將你傳入的字符串轉換成 UTF-8 編碼的二進制數據進行 SHA256 摘要,然後將摘要進行簽名,最終將簽名結果轉換為 base64 編碼的字符串輸出。
Swift
這裡我們同樣,將公鑰保存到變量中,注意只複製 base64 編碼部分,不要開頭和結尾的 -----開始 上市 鍵----- 和 -----結束 上市 鍵----- :
1 |
private var publicKey = "MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDKoeRzRVf8WoRSDYYqUzThpYCr90jfdFwTSXHJ526K8C6TEwdTUA+CFPQPRUg9jrYgFcown+J2myzO8BRLynD+XHb9ilLb49Mqk2CvDt/yK32lgHv3QVx14Dpb6h8isjncSF965fxBxlHGbvPwnHkJ9etRIYdYV3QpYohFszH3wQIDAQAB" |
上文中輸出的密鑰都是一行一行的,這裡我們將它們無縫拼接在一起,形成一個長串。
記得引用框架 進口 通用加密
然後我們寫一個函數來生成二進制公鑰:
1 2 3 4 5 6 7 |
private func getPublicSecKey() -> SecKey? { let keyBase64 = Data(base64Encoded: publicKey, options: .ignoreUnknownCharacters)! as CFData let sec = SecKeyCreateWithData(keyBase64, [kSecAttrType: kSecAttrKeyTypeRSA, kSecAttrKeyClass: kSecAttrKeyClassPublic] as NSDictionary, nil) return sec } |
最終來對數據進行簽名驗證:
1 2 3 4 5 6 7 |
func sign(content:String, sign:String) ->Bool { let sha256Data = sha256(data: content.data(using: .utf8)!) let encryptedData = Data(base64Encoded: sign) let pubSecKey = getPublicSecKey() let verified = SecKeyVerifySignature(pubSecKey!, .rsaSignatureDigestPKCS1v15SHA256, sha256Data as CFData, encryptedData! as CFData, nil) return verified } |
只要傳入被簽名的字符串,再傳入簽名的 base64 編碼,函數就會自動對字符串進行 SHA256 摘要,然後簽名恢復成二進制數據,最後通過系統內置算法,使用你的公鑰,對摘要和簽名進行驗證,驗證通過,返回 true 。
後記
使用不同的代碼在不同的平台進行互通是一件很困難的事情,簡單的加密算法和方式往往會在不同的平台得出不一樣的結果,同一個語言,在同一個平台,是最容易的,但不同的語言就很複雜,具體實現時一定要注意這個問題。在嘗試了無數種方式後,最終我還是使用 RSA 簽名實現了跨語言平台互通,希望這篇文章對你有用。
參考文獻
本文由 落格博客 原創撰寫:落格博客 » Swift Python 互通 Json 數據簽名
轉載請保留出處和原文鏈接:https://www.logcg.com/archives/3318.html