概述
事情的緣由很簡單,工作上在做 LINE SDK 的開發(fā),在拿 token 的時候有一步額外的驗證:從 Server 會發(fā)回一個 JWT (JSON Web Token),客戶端需要對這個 JWT 進行簽名和內(nèi)容的驗證,以確保信息沒有被人篡改。
推薦閱讀:iOS開發(fā)——2019 最新 BAT面試題合集(持續(xù)更新中)
Server 在簽名中使用的算法類型會在 JWT 中寫明,驗證簽名所需要的公鑰 ID 也可以在 JWT 中找到。這個公鑰是以 JWK (JSON Web Key) 的形式公開,客戶端拿到 JWK 后即可在本地對收到的 JWT 進行驗證。用一張圖的話,大概是這樣:
作為一個開發(fā)者,有一個學習的氛圍跟一個交流圈子特別重要,這是一個我的iOS交流群:638302184,不管你是小白還是大牛歡迎入駐 ,分享BAT,阿里面試題、面試經(jīng)驗,討論技術, 大家一起交流學習成長!
群內(nèi)提供數(shù)據(jù)結構與算法、底層進階、swift、逆向、整合面試題等免費資料
附上一份收集的各大廠面試題(附答案) ! 群文件直接獲取
各大廠面試題
步驟
如果你現(xiàn)在對下面說步驟不理解的話 (這挺正常的,畢竟這篇文章都還沒正式開始 ??),可以先跳過這部分,等我們有一些基礎知識以后再回頭看看就好。如果你很清楚這些步驟的話,那真是好棒棒,你應該能無壓力閱讀該系列剩余部分內(nèi)容了。
LINE SDK 里使用 JWT 驗證用戶的邏輯如下:
- 向登錄服務器請求 access token,登錄服務器返回 access token,同時返回一個 JWT。
- JWT 中包含應該使用的算法和密鑰的 ID。通過密鑰 ID,去找預先定義好的 Host 拿到 JWK 形式的該 ID 的密鑰。
- 將 1 的 JWT 和 2 的密鑰轉換為 Security.framework 接受的形式,進行簽名驗證。
這個過程想法很簡單,但會涉及到一系列比較基礎的密碼學知識和標準的閱讀,難度不大,但是枯燥乏味。另外,由于 iOS 并沒有直接將 JWK 轉換為 native 的 SecKey
的方式,自己也沒有任何密碼學的基礎,所以在處理密鑰轉換上也花了一些工夫。為了后來者能比較順利地處理相關內(nèi)容 (包括 JWT 解析驗證,JWK 特別是 RSA 和 EC 算法的密鑰轉換等),也為了過一段時間自己還能有地方回憶這些內(nèi)容,所以將一些關鍵的理論知識和步驟記錄下來。
系列文章的內(nèi)容
整個系列會比較長,為了閱讀壓力小一些,我會分成三個部分:
- 基礎 - 什么是 JWT 以及 JOSE (本文)
- 理論 - JOSE 中的簽名和驗證流程
- 實踐 - 如何使用 Security.framework 處理 JOSE 中的驗證
全部讀完的話應該能對網(wǎng)絡相關的密碼學有一個膚淺的了解,特別是常見的簽名算法和密鑰種類,編碼規(guī)則,怎么處理拿到的密鑰,怎么做簽名驗證等等。如果你在工作中有相關需求,但不知道如何下手的話,可以仔細閱讀整個系列,并參看開源的 LINE SDK Swift 的相關實現(xiàn),甚至直接 copy 部分代碼 (如果可以的話,也請順便點一下 star)。如果你只是感興趣想要簡單了解的話,可以只看 JOSE 和 JWT 的基礎概念和理論流程部分的內(nèi)容,作為知識面的擴展,等以后有實際需要了再回頭看實踐部分的內(nèi)容。
在文章結尾,我還列舉了一些常見的問題,包括筆者自己在學習時的思考和最后的選擇。如果您有什么見解,也歡迎發(fā)表在評論里,我會繼續(xù)總結和補充。
聲明:筆者自身對密碼學也是初學,而本文介紹的密碼學知識也都是自己的一些理解,同時盡量不涉及過于原理性的內(nèi)容,一切以普通工程師實用為目標原則。其中可以想象在很多地方會有理解的錯誤,還請多包涵。如您發(fā)現(xiàn)問題,也往不吝賜教指正,感激不盡。
JWT 以及 JOSE
什么是 JWT
估計大部分 Swift 的開發(fā)者對 JWT 會比較陌生,所以先簡單介紹一下它是什么,以及可以用來做什么。JWT (JSON Web Token) 是一個編碼后的字符串,比如:
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.
eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.
SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c
一個典型的 JWT 由三部分組成,通過點號 .
進行分割。每個部分都是經(jīng)過 Base64Url 編碼的字符串。第一部分 (Header) 和第二部分 (Payload) 在解碼后應該是有效的 JSON,最后一部分 (簽名) 是通過一定算法作用在前兩部分上所得到的簽名數(shù)據(jù)。接收方可以通過這個簽名數(shù)據(jù)來驗證 token 的 Header 及 Payload 部分的數(shù)據(jù)是否可信。
為了視覺上看起來輕松一些,在上面的 JWT 例子中每個點號后加入了換行。實際的 JWT 中不應該存在任何換行的情況。
嚴格來說,JWT 有兩種實現(xiàn),分別是 JWS (JSON Web Signature) 和 JWE (JSON Web Encryption)。由于 JWS 的應用更為廣泛,所以一般說起 JWT 大家默認會認為是 JWS。JWS 的 Payload 是 Base64Url 的明文,而 JWE 的數(shù)據(jù)則是經(jīng)過加密的。相對地,相比于 JWS 的三個部分,JWE 有五個部分組成。本文中提到 JWT 的時候,所指的都是用于簽名認證的 JWS 實現(xiàn)。
關于 Base64Url 編碼和處理,在本文后面部分會再提到。
Header
Header 包含了 JWT 的一些元信息。我們可以嘗試將上面的 eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9
這個 Header 解碼,得到:
{"alg":"HS256","typ":"JWT"}
關于在數(shù)據(jù)的不同格式之間互相轉換 (明文,Base64,Hex Bytes 等),我推薦這個非常不錯的 web app。
在 JWT Header 中,”alg” 是必須指定的值,它表示這個 JWT 的簽名方式。上例中 JWT 使用的是 HS256
進行簽名,也就是使用 SHA-256 作為摘要算法的 HMAC。常見的選擇還有 RS256
,ES256
等等。總結一下:
-
HSXXX
或者說 HMAC:一種對稱算法 (symmetric algorithm),也就是加密密鑰和解密密鑰是同一個。類似于我們創(chuàng)建 zip 文件時設定的密碼,驗證方需要知道和簽名方同樣的密鑰,才能得到正確的驗證結果。 -
RSXXX
:使用 RSA 進行簽名。RSA 是一種基于極大整數(shù)做因數(shù)分解的非對稱算法 (asymmetric algorithm)。相比于對稱算法的 HMAC 只有一對密鑰,RSA 使用成對的公鑰 (public key) 和私鑰 (private key) 來進行簽名和驗證。大多數(shù) HTTPS 中驗證證書和加密傳輸數(shù)據(jù)使用的是 RSA 算法。 -
ESXXX
:使用 橢圓曲線數(shù)字簽名算法 (ECDSA) 進行簽名。和 RSA 類似,它也是一種非對稱算法。不過它是基于橢圓曲線的。ECDSA 最著名的使用場景是比特幣的數(shù)字簽名。 -
PSXXX
: 和RSXXX
類似使用 RSA 算法,但是使用 PSS 作為 padding 進行簽名。作為對比,RSXXX
中使用的是 PKCS1-v1_5 的 padding。
如果你對這些介紹一頭霧水,也不必擔心。關于各個算法的一些更細節(jié)的內(nèi)容,會在后面實踐部分再詳細說明。現(xiàn)在,你只需要知道 Header 中 “alg” key 為我們指明了簽名所使用的簽名算法和散列算法。我們之后需要依據(jù)這里的指示來驗證簽名。
除了 “alg” 外,在 Header 中發(fā)行方還可以放入其他有幫助的內(nèi)容。JWS 的標準定義了一些預留的 Header key。在本文中,除了 “alg” 以外,我們還會用到 “kid”,它用來表示在驗證時所需要的,從 JWK Host 中獲取的公鑰的 key ID。現(xiàn)在我們先集中于 JWT 的構造,之后在 JWK 的部分我們再對它的使用進行介紹。
Payload
Payload 是想要進行交換的實際有意義的數(shù)據(jù)部分。上面例子解碼后的 Payload 部分是:
{"sub":"1234567890","name":"John Doe","iat":1516239022}
和 Header 類似,payload 中也有一些預先定義和保留的 key,我們稱它們?yōu)?claim。常見的預定義的 key 包括有:
- “iss” (Issuer):JWT 的簽發(fā)者名字,一般是公司名或者項目名
- “sub” (Subject):JWT 的主題
- “exp” (Expiration Time):過期時間,在這個時間之后應當視為無效
- “iat” (Issued At):發(fā)行時間,在這個時間之前應當視為無效
當然,你還可以在 Payload 里添加任何你想要傳遞的信息。
我們在驗證簽名后,就可以檢查 Payload 里的各個條目是否有效:比如發(fā)行者名字是否正確,這個 JWT 是否在有效期內(nèi)等等。因為一旦簽名檢查通過,我們就可以保證 Payload 的東西是可靠的,所以這很適合用來進行消息驗證。
注意,在 JWS 里,Header 和 Payload 是 Base64Url 編碼的明文,所以你不應該用 JWS 來傳輸任何敏感信息。如果你需要加密,應該選擇 JWE。
Signature
一個 JWT 的最后一部分是簽名。首先對 Header 和 Payload 的原文進行 Base64Url 編碼,然后用 .
將它們連接起來,最后扔給簽名散列算法進行簽名,把簽名得到的數(shù)據(jù)再 Base64Url 編碼,就能得到這個簽名了。寫成偽代碼的話,是這樣的:
// 比如使用 RS256 簽名:
let 簽名數(shù)據(jù): Data = RS256簽名算法(Base64Url(string: Header).Base64Url(string: Payload), 私鑰)
let 簽名: String = Base64Url(data: 簽名數(shù)據(jù))
最后,把編碼后的 Header,Payload 和 Signature 都用 .
連在一起,就是我們收發(fā)的 JWT 了。
什么是 JOSE
JWT 其實是 JOSE 這個更大的概念中的一個組成部分。JOSE (Javascript Object Signing and Encryption) 定義了一系列標準,用來規(guī)范在網(wǎng)絡傳輸中使用 JSON 的方式。我們在上面介紹過了JWS 和 JWE,在這一系列概念中還有兩個比較重要,而且相互關聯(lián)的概念:JWK 和 JWA。它們一起組成了整個 JOSE 體系。
JWK
不管簽名驗證還是加密解密,都離不開密鑰。JWK (JSON Web Key) 解決的是如何使用 JSON 來表示一個密鑰這件事。
RSA 的公鑰由模數(shù) (modulus) 和指數(shù) (exponent) 組成,一個典型的代表 RSA 公鑰的 JWK 如下:
{
"alg": "RS256",
"n": "ryQICCl6NZ5gDKrnSztO3Hy8PEUcuyvg_ikC-VcIo2SFFSf18a3IMYldIugqqqZCs4_4uVW3sbdLs_6PfgdX7O9D22ZiFWHPYA2k2N744MNiCD1UE-tJyllUhSblK48bn-v1oZHCM0nYQ2NqUkvSj-hwUU3RiWl7x3D2s9wSdNt7XUtW05a_FXehsPSiJfKvHJJnGOX0BgTvkLnkAOTdOrUZ_wK69Dzu4IvrN4vs9Nes8vbwPa_ddZEzGR0cQMt0JBkhk9kU_qwqUseP1QRJ5I1jR4g8aYPL_ke9K35PxZWuDp3U0UPAZ3PjFAh-5T-fc7gzCs9dPzSHloruU-glFQ",
"use": "sig",
"kid": "b863b534069bfc0207197bcf831320d1cdc2cee2",
"e": "AQAB",
"kty": "RSA"
}
模數(shù) n
和指數(shù) e
構成了密鑰最關鍵的數(shù)據(jù)部分,這兩部分都是 Base64Url 編碼的大數(shù)字。
關于 RSA 的原理,不在本文范圍內(nèi),你可以在其他很多地方找到相關信息。
如果你接觸過幾個 RSA 密鑰,可能會發(fā)現(xiàn) “e” 的值基本都是 “AQAB”。這并不是巧合,這是數(shù)字 65537 (0x 01 00 01) 的 Base64Url 表示。選擇 AQAB 作為指數(shù)已經(jīng)是業(yè)界標準,它同時兼顧了運算效率和安全性能。同樣,這部分內(nèi)容也超出了本文范疇。
類似地,一個典型的 ECDSA 的 JWK 內(nèi)容如下:
{
"kty":"EC",
"alg":"ES256",
"use":"sig",
"kid":"3829b108279b26bcfcc8971e348d116",
"crv":"P-256",
"x":"EVs_o5-uQbTjL3chynL4wXgUg2R9q9UU8I5mEovUf84",
"y":"AJBnuQ4EiMnCqfMPWiZqB4QdbAd0E7oH50VpuZ1P087G"
}
決定一個 ECDSA 公鑰的參數(shù)有三個: “crv” 定義使用的密鑰所使用的加密曲線,一般可能值為 “P-256”,”P-384” 和 “P-521”。”x” 和 “y” 是選取的橢圓曲線點的座標值,根據(jù)曲線 “crv” 的不同,這個值的長度也會有區(qū)別;另外,推薦使用的散列算法也會隨著 “crv” 的變化有所不同:
crv | x/y 的字節(jié)長度 | 散列算法 |
---|---|---|
P-256 | 32 | SHA-256 |
P-384 | 48 | SHA-384 |
P-521 | 66 | SHA-512 |
注意
P-521
對應的是SHA-512
,不是SHA-521
(不存在 521 位的散列算法 ??)
同樣,使用的曲線也決定了簽名的長度。在使用 ECDSA 對數(shù)據(jù)簽名時,通過橢圓曲線計算得到 r 和 s 兩個值。這兩個值的字節(jié)長度也應該符合上表。
細心的同學可能會發(fā)現(xiàn)上面的 ECDSA 密鑰中 “y” 的值轉換為 hex 表示后是 33 個字節(jié):
00 90 67 b9 0e 04 88 c9 c2 a9 f3 0f 5a 26 6a 07 84 1d 6c 07 74 13 ba 07 e7 45 69 b9 9d 4f d3 ce c6
我們知道,在密鑰中 “x” 和 “y” 都是大的整數(shù),但是在某些安全框架的實現(xiàn) (比如一些版本的 OpenSSL) 中,使用的會是普通的整數(shù)類型 (Int),而非無符號整數(shù) (UInt)。而如果一個數(shù)字首 bit 為 1 的話,在有符號的整數(shù)系統(tǒng)中會被認為是負數(shù)。在這里,”y” 原本第一個 byte 其實是
0x90
(bit 表示是 0b_1001_0000),首 bit 為 1,為了避免被誤認為負數(shù),有的實現(xiàn)會在前面添加0x00
。但是實際上把這樣一個 33 byte 的值作為 “y” 放在 JWK 中,是不符合標準的。如果你遇到了這種情況,可以和負責服務器的小伙伴商量一下讓他先處理一下,給你正確的 key。當然,你也可以自己在客戶端檢查和處理長度不符合預期的問題,以增強本地代碼的健壯性。在這個例子中,如果服務器在生成 JWK 時就幫我們處理了
0x00
的問題的話,那么 “y” 的值應該是
kGe5DgSIycKp8w9aJmoHhB1sB3QTugfnRWm5nU_TzsY
我們還會在后面看到更多的處理
0x00
添加或刪除的情況,對于首字節(jié)是0x80
(0b_1000_0000
) 或者以上的值,我們可能都需要考慮具體實現(xiàn)是接受 Int 還是 UInt 的問題。
JWA
JWA (JSON Web Algorithms) 定義的就是在 JWT 和 JWK 中涉及的算法了,它為每種算法定義了具體可能存在哪些參數(shù),和參數(shù)的表示規(guī)則。比如上面 JWK 例子中的 “n”,”e”,”x”,”y”,”crv” 都是在 JWA 標準中定義的。它為如何使用 JWK,如何驗證 JWT 提供支持和指導。
除了 RSA 和 ECDSA 以外,JWA 里還定義了 AES 相關的加密算法,不過這部分內(nèi)容和 JWS 沒什么關系。另外,在簽名算法定義的后面,也附帶了如果使用簽名和如何進行驗證的簡單說明。我們在之后會對 JOSE 中的簽名和驗證過程進行更詳細的解釋。
小結
本文簡述了 JWT 和 JOSE 的相關基礎概念。您現(xiàn)在對 JWT 是什么,JOSE 有哪些組成部分,以及它們大概長什么樣有一定了解。
你可以訪問 JWT.io 來實際試試看創(chuàng)建和驗證一個 JWT 的過程。如果你想要更深入了解 JWT 的內(nèi)容和定義的話,JWT.io 還提供了免費的 JWT Handbook,里面有更詳細的介紹。我們在系列文章的最后還會對 JWT 的應用場景,適用范圍和存在的風險進行補充說明。
系列文章后面兩篇,會分別針對 JOSE 中的簽名和驗證過程以及作為 iOS 開發(fā)者如何使用 Security.frame 來處理 JOSE 相關的概念實踐進行更詳細的說明。