一、前言
在經歷了人生的很多至暗時刻后,你讀到了這篇文章,你會后悔甚至憤怒:為什么你沒有早點寫出這篇文章?!
你的至暗時刻包括:
1.你所在的項目需要對接銀行,對方需要你提供一個加密證書。你手上只有一個六級英語證書,不確定這個是否滿足對方需求。由于你遲遲無法提供正確的證書,項目因此延期,加薪計劃泡湯,月供斷了,女朋友分手了,你感覺人生完了。
2. 你老驥伏櫪 2 個月,終于搞懂了.crt 格式證書。加入到新項目,項目在進行證書托管改造。哈哈,這題我會,就是把證書文件上傳到托管系統。你對項目組成員大喝一聲,放開那些證書,讓我來!擠進去一看,是陳年老項目了,根本沒有證書,當時使用是公鑰和私鑰,如何公鑰和私鑰變成證書??由于你遲遲無法提供正確的證書,項目因此延期,加薪計劃泡湯,月供斷了,女朋友分手了,你感覺人生完了。
3. 你臥薪嘗膽 3 個月,摸清楚了 SSL 證書的來龍去脈。躊躇滿志加入到新項目,你向項目經理痛陳血淚史,經此一役,你已經成長為安全證書方面的專家。項目經理喜出望外,正好項目在進行數據安全改造,數據庫需要啟用 SSL,來得正是時候,不著急,明天下班前提供幾個密鑰文件就行。越明日,下班前半小時,你緩緩走向項目經理,“你要的貨到了”,便排出三個證書,這個是 key 文件,這個是公鑰文件,這個是證書文件。項目經理點點頭又搖搖頭,我要的是JKS 文件呀。你說,明天提供。越明日,下班前的半個小時,你把 JKS 格式文件交給項目經理,項目經理點點頭又搖搖頭,密碼呢?沒有密碼怎么行?由于你遲遲無法提供正確的證書,項目因此延期,加薪計劃泡湯,月供斷了,女朋友分手了,你感覺人生完了。
本文將從以下幾部分來揭示 RSA 密鑰文件的鮮為人知的秘密:
- RSA 算法數學基礎
- RSA秘鑰體系六層模型
- RSA 工具使用
- RSA密鑰使用場景
注:雖然密鑰與證書嚴格意義上并不等同,但為了表述方便,沒有特殊指定的話,本文中的密鑰一詞涵蓋了公鑰,私鑰,證書等概念。
二、RSA 算法數學基礎
RSA 算法是基于數論的,RSA算法的復雜性的基礎在于一個大數的素數分解是NP難題,非常難破解。RSA 算法相關的數學概念:
對于任意一個數 x,可以計算出 y:
[圖片上傳失敗...(image-b9fe3d-1611021868265)]
通過 y,可以計算出 x:
[圖片上傳失敗...(image-6b9a6f-1611021868265)]
也就是說,x 通過數對 (m,e) 生成了 y 后,可以通過數對 (m, d) 將 y 還原成 x。
這里,我們實際上演示了RSA加密解密的數學過程。通過公式 (1),根據 x 計算得出 y 的過程就是加密,通過公式 (2),根據 y 計算得出 x 的過程就是解密。
在實際應用中,RSA 算法通過公鑰進行加密,私鑰進行解密,因此數對 (m,e) 就是公鑰,(m, d) 就是私鑰。
實際上為了提高私鑰解密速度,私鑰會保存一些中間結果,例如 p, q, e, 等等。
所以在實際應用中,可以通過私鑰導出公鑰。
三、 RSA秘鑰六層模型
為了方便理解RSA密鑰的原理,本人創造性地發明了RSA密鑰六層模型概念。每一層定義了自己的職責和邊界,層級越低,其表示的內容越傾向于抽象和理論;層級越高,其表示的內容越傾向于實際應用。
- Data:數據層,定義了RSA密鑰的數學概念(m,e,p,q等)或者參與實體(subject, issuer等)。
- Serialization:序列化層,定義了將復雜數據結構序列化的方法。
- Structure:結構層,定義了不同格式的RSA密鑰的數據組織形式。
- Text:文本層,定義了將二進制的密鑰轉換成文本的方法。
- Presentation:表現層,定義了文本格式密鑰的表現形式。
- Application:應用層,定義了RSA密鑰使用的各種場景。
下面對每一層進行具體說明。
3.2 數據層
從上文可知,秘鑰是一個數據結構,每個結構包含了 2 個或更多的成員(公鑰包含 m 和 e,私鑰包含 m,d,e 以及其他一些中間結果)。為了將這些數據結構保存在文件中,需要定義某種格式對秘鑰進行序列化。
3.3 序列化層
目前常見的定義數據結構的格式包括 JSON 和 XML 等文本格式。
比如,理論上我們可以把公鑰定義為一個 JSON:
JSON格式密鑰
{
"m":"15",
"e":"3"
}
或者,也可以把私鑰定義為一個 XML:
<?xml>
<key>
<module>15</module>
<e>3</e>
<d>3</d>
<p>3</p>
<q>5</q>
<key>
但是 RSA 發明的時候,這兩種格式都還不存在。因此科學家們選擇了當時比較流行的語法格式ASN.1。
3.3.1 ASN.1
ASN.1 全稱是 Abstract Syntax Notation dot one,(抽象語法記號第1版)。數字1被ISO加在ASN的后邊,是為了保持ASN的開放性,可以讓以后功能更加強大的ASN被命名為ASN.2等,但至今也沒有出現。
ASN.1描述了一種對數據進行表示、編碼、傳輸和解碼的數據格式。它提供了一整套正規的格式用于描述對象的結構,而不管語言上如何執行及這些數據的具體指代,也不用去管到底是什么樣的應用程序。
3.3.2 ASN.1 編碼規則
ASN.1的具體語法可以參考維基百科(https://zh.wikipedia.org/wiki/ASN.1),在此只作簡要說明。
ASN.1 中數據類型表示是 T-L-V 的形式:頭 2 個字節代表數據類型,接下來的 2 個字節代表字節長度,V 代表具體值。常見的基礎類型的值包括 Integer, UTF8String, 復合結構包括 SEQUENCE, SET.秘鑰和證書都是 SEQUENCE 類型,而 SEQUENCE 的 type 是 0x30,且長度是大于 127 的,因此第2 個字節是 0x82. ASN.1 編碼表示的數據是二進制數據,通常通過 BASE64 轉化成字符串保存在 pem 文件中,而 0x3082 經過 BASE64 編碼后,就是字符串 MI,因此所有 PEM 文件存儲的秘鑰開始的前兩個字符是 MI。
BER, CER, DER 是 ASN.1 編碼規則。其中 DER(Distinguish Encode Rules) 是無歧義編碼規則,保證相同的數據結構產生的序列化結果也相同的。
ASN.1 只是定義了抽象數據的序列化方式,但是具體的編碼還需要進一步定義。
嚴格來說,ASN.1 還不是一種定義數據的格式,而是一種語法標準,按照這種標準,可以制定各種各樣的格式。
3.4 結構層
根據秘鑰文件用途不同,以下標準定義了不同的結構來對秘鑰數據進行 ASN.1 編碼。通常而言,不同格式的秘鑰暗示了不同的結構。
- **pkcs#1 **用于定義 RSA 公鑰、私鑰結構
- pkcs#7 用于定義證書鏈
- **pkcs#8 **用于定義任何算法公私鑰
- **pkcs#12 **用于定義私鑰證書
- X.509 定義公鑰證書
這些格式的具體區別比較參見下文3.5.2
3.5 表現層
可以看到 ASN.1 及其編碼規則(BER, CER, DER)定義的是二進制規則,保存在文件中也是二進制格式。由于當時的電子郵件標準不支持二進制內容的傳輸,如果秘鑰文件通過電子郵件傳輸,就需要將二進制文件轉換成文本文件。這就是 PEM(Privacy-Enhanced Mail, 私密增強郵件)的由來。因此,PEM 文件中保存的秘鑰內容是 ASN.1 編碼生成的二進制內容,再進行 base64 編碼后的文本。
另外,為了方便用戶識別是何種格式,中文件的首尾加上一行表示身份的文本。PEM 文件一般包含三部分:首行標簽,BASE64 編碼的文本數據,尾行標簽。
-----BEGIN <label>-----
<BASE64 ENCODED DATA>
-----END <label>-----
針對不同的格式,<label> 值不一樣。
3.5.2 PEM 文件格式小結
[圖片上傳失敗...(image-b68b57-1611021868265)]
3.6 應用層
在實際使用中,不僅僅需要使用公私鑰對數據進行加解密,還需要根據不同的使用場景,解決密鑰的分發、驗證等。第5節列舉了RSA密鑰的一些常見使用場景。
四、工具
4.1 openssl
注意:下面的命令中-RSAPublicKey_in, -RSAPublicKey_out選項需要openssl1.0以上版本支持,如果報錯,請檢查 openssl 版本。
4.1.1 創建秘鑰文件
# 生成 pkcs#1 格式2048位的私鑰
openssl genrsa -out private.pem 2048
#從私鑰中提取 pkcs#8 格式公鑰
openssl rsa -in private.pem -out public.pem -pubout
#從私鑰中提取 pkcs#1 格式公鑰
openssl rsa -in private.pem -out public.pem -RSAPublicKey_out
4.1.2 秘鑰文件格式轉換
#pkcs#1 公鑰轉換成 pkcs#8 公鑰
openssl rsa -in public.pem -out public-pkcs8.pem -RSAPublicKey_in
#pkcs#8 公鑰轉換成 pkcs#1 公鑰
openssl rsa -in public-pkcs8.pem -out public-pkcs1.pem -pubin -RSAPublicKey_out
#pkcs#1 私鑰轉換成 pkcs#8 私鑰
openssl pkcs8 -in private.pem -out private-pkcs8.pem -topk8
#pkcs#8 私鑰轉換成 pkcs#1 私鑰
openssl rsa -in private-pkcs8.pem -out private-pkcs1.pem
4.1.3 查看秘鑰文件信息
#查看公鑰信息
openssl rsa -in public.pem -pubin -text -noout
#查看私鑰信息
openssl rsa -in private.pem -text -noout
4.1.4 證書
RSA證書
#從現有私鑰創建 CSR 文件
openssl req -key private.pem -out request.csr -new
#從現有 CSR 文件和私鑰中創建證書,有效期365天
openssl x509 -req -in request.csr -signkey private.pem -out cert.crt -days 365
#生成全新證書和私鑰
openssl req -nodes -newkey rsa:2048 -keyout root.key -out root.crt -x509 -days 365
#通 過 現 有 證 書 和 私 鑰 (作 為CA ) 為 其 他 CSR 文 件 簽 名
openssl x509 -req -in child.csr -days 365 -CA root.crt -CAkey root.key -set_serial 01 -out child.crt
#查看證書信息
openssl x509 -in child.crt -text -noout
#從證書中提取公鑰
openssl x509 -pubkey -noout -in child.crt > public.pem
4.1.5 JKS
#將CA證書轉換成JKS格式
keytool -importcert -alias Cacert -file ca.crt -keystore truststoremysql.jks -storepass password123
#將client.crt和client.key轉換成PKCS#12格式
openssl pkcs12 -export -in client.crt -inkey client.key -name "mysqlclient" -passout pass:mypassword -out client-keystore.p12
#將PKCS#12格式轉換成JKS格式
keytool -importkeystore -srckeystore client-keystore.p12 -srcstoretype pkcs12 -srcstorepass mypassword -destkeystore clientstore.jks -deststoretype JKS -deststorepass password456
五、 RSA密鑰使用場景
5.1 HTTPS單向認證
由于HTTP協議是明文傳輸,為了保證HTTP報文不被泄露和篡改,HTTPS通過SSL/TLS協議對HTTP報文進行加解密。
簡單來說,HTTPS協議要求客戶端和服務端建立連接的過程中,首先進行會話密鑰交換,然后使用該會話密鑰對通信報文進行加解密。整個通信過程如下:
服務端通過4.1.4所示方法創建RSA證書server.crt和私鑰server.key,并在WEB服務器中進行配置。
客戶端與服務端建立連接,服務端向客戶端發送證書server.crt。
客戶端對服務端證書進行校驗,并隨機生成會話密鑰,將通過服務端證書對會話密鑰進行加密,傳給服務端。
服務端通過server.key對加密后的會話密鑰進行解密,獲得會話密鑰原文。
客戶端通過會話密鑰對HTTP報文進行加密,傳給服務端。
服務端通過會話密鑰對HTTP加密報文進行解密,獲得HTTP報文原文。
服務端通過會話密鑰對HTTP響應報文進行加密,返回給客戶端。
客戶端通過會話密鑰對HTTP響應報文進行解密,獲得HTTP響應報文原文。
(圖1. HTTPS單向認證)
5.2 HTTPS雙向認證
5.1節描述的HTTPS場景是一個通用場景,整個過程只有客戶端對于服務端的驗證,即客戶端拿到服務端的證書后,會對證書進行有效性驗證,比如是否是CA簽名的,是否仍處于有效期內等。這種單向驗證在瀏覽器訪問等場景中沒有問題,因為這種服務設計地目的就是對外數以萬計的用戶提供服務。但是在某些場景,比如說僅對特定企業、商戶提供服務,服務端需要對客戶端進行驗證,通過驗證的受信客戶端才能正常。
訪問服務端時,就需要用到HTTPS雙向認證。
HTTPS雙向認證的過程,就是在HTTPS單向認證的基礎之上,增進服務端對客戶端的認證。解決方案的思路就是,客戶端保存客戶端證書client.crt,但是客戶端證書不是客戶端自己簽名或者CA簽名,而是由服務端的root.key進行簽名。在HTTPS雙向認證過程中,客戶端需要將客戶端證書client.crt發送給服務端,服務端使用root.key進行驗證無誤后,方可進行后續通信;否則,該客戶端即非受信客戶端,服務端拒絕提供后續服務。
具體通信過程如下所示:
服務端通過4.1.4所示方法創建RSA證書server.crt和私鑰server.key,并在WEB服務器中進行配置。
客戶端與服務端建立連接,服務端向客戶端發送證書server.crt。
客戶端對服務端證書進行校驗,驗證通過后繼續后續流程;驗證不通過則斷開連接,流程結束。
服務端向客戶端發送報文,請求客戶端發送客戶端證書。
客戶端向服務端發送客戶端證書。
服務端通過root.key對客戶端證書進行驗證,驗證無誤進行后續流程;否則斷開連接,流程結束。
客戶端隨機生成會話密鑰,將通過服務端證書對會話密鑰進行加密,傳給服務端。
服務端通過server.key對加密后的會話密鑰進行解密,獲得會話密鑰原文。
客戶端通過會話密鑰對HTTP報文進行加密,傳給服務端。
服務端通過會話密鑰對HTTP加密報文進行解密,獲得HTTP報文原文。
服務端通過會話密鑰對HTTP響應報文進行加密,返回給客戶端。
客戶端通過會話密鑰對HTTP響應報文進行解密,獲得HTTP響應報文原文。
可以看出,向較于HTTPS單向認證過程,HTTPS雙向認證過程在客戶端驗證服務端證書之后,在向服務端發送加密的會話密鑰之前,會增加客戶端向服務端發送客戶端證書client.crt,服務端對該證書進行驗證的過程。
[圖片上傳失敗...(image-65cc35-1611021868265)]
(圖2. HTTPS雙向認證)
5.3 MySQL開啟 SSL
MySQL提供SSL的原理,與HTTPS類似,不同之處在于MySQL提供的服務的對象不會是成千上萬的普通用戶,因此對于CA的需求并不高。
因此實際CA證書通常都是服務端自己生成。
與HTTPS類似,MySQL提供兩種形式的SSL認證機制:單向認證和雙向認證。
5.3.1 MySQL的SSL單向認證
(1)服務端配置文件:ca.crt, server.crt, server.key,其中server.crt由ca.crt簽名生成。
(2)客戶端配置文件:ca.crt,ca.crt與服務端的ca.crt相同。
(3)客戶端生成JKS文件
keytool -importcert -alias Cacert -file ca.crt -keystore truststoremysql.jks -storepass password123
(4)通過jdbc字符串配置SSL選項和JKS文件
verifyServerCertificate=true&useSSL=true&requireSSL=true&trustCertificateKeyStoreUrl=file:./truststoremysql.jks&trustCertificateKeyStorePassword=password123
5.3.2 MySQL的SSL雙向認證
(1)服務端配置文件:ca.crt, server.crt, server.key, 其中server.crt由ca.crt簽名生成。
(2)客戶端配置文件:ca.crt, client.crt, client.key, 其中ca.crt與服務端的ca.crt相同, client.crt由ca.crt簽名生成。
(3)客戶端生成trustKeyStore文件
keytool -importcert -alias Cacert -file ca.crt -keystore truststore.jks -storepass password123
(4)客戶端生成clientKeyStore文件
keytool -importcert -alias Cacert -file ca.crt -keystore clientstore.jks -storepass password456
(5)通過jdbc字符串配置SSL選項和JKS文件
verifyServerCertificate=true&useSSL=true&requireSSL=true&trustCertificateKeyStoreUrl=file:./truststore.jks&trustCertificateKeyStorePassword=password123&clientCertificateKeyStoreUrl=file:./clientstore.jks&clientCertificateKeyStorePassword=password456
關于MySQL的SSL認證更多細節可以參考:
附錄A 不同格式的 ASN.1 編碼
A.1 pkcs#1
A.1.1 公鑰
RSAPublicKey ::= SEQUENCE {
modulus INTEGER , -- n
publicExponent INTEGER -- e
}
A.1.2 私鑰
RSAPrivateKey ::= SEQUENCE {
version Version ,
modulus INTEGER , -- n
publicExponent INTEGER , -- e
privateExponent INTEGER , -- d
prime1 INTEGER , -- p
prime2 INTEGER , -- q
exponent1 INTEGER , -- d mod (p-1)
exponent2 INTEGER , -- d mod (q-1)
coefficient INTEGER , -- (inverse of q) mod p
otherPrimeInfos OtherPrimeInfos OPTIONAL
}
A.2 pkcs#8
A.2.1 pkcs#8 公鑰
PublicKeyInfo ::= SEQUENCE {
algorithm AlgorithmIdentifier ,
PublicKey BIT STRING
}
AlgorithmIdentifier ::= SEQUENCE {
algorithm OBJECT IDENTIFIER ,
parameters ANY DEFINED BY algorithm OPTIONAL
}
A.2.2 pkcs#8 私鑰
OneAsymmetricKey ::= SEQUENCE {
version Version ,
privateKeyAlgorithm PrivateKeyAlgorithmIdentifier ,
privateKey PrivateKey ,
attributes [0] Attributes OPTIONAL ,
...,
[[2: publicKey [1] PublicKey OPTIONAL ]],
...
}
PrivateKey ::= OCTET STRING
-- Content varies based on type of key. The
-- algorithm identifier dictates the format of
-- the key.
A.3 X.509
A.3.1 X.509 證書
Certificate ::= SEQUENCE {
tbsCertificate TBSCertificate ,
signatureAlgorithm AlgorithmIdentifier ,
signatureValue BIT STRING
}
TBSCertificate ::= SEQUENCE {
version [0] EXPLICIT Version DEFAULT v1,
serialNumber CertificateSerialNumber ,
signature AlgorithmIdentifier ,
issuer Name,
validity Validity ,
subject Name,
subjectPublicKeyInfo SubjectPublicKeyInfo ,
issuerUniqueID [1] IMPLICIT UniqueIdentifier OPTIONAL ,
-- If present , version MUST be v2 or v3
subjectUniqueID [2] IMPLICIT UniqueIdentifier OPTIONAL ,
-- If present , version MUST be v2 or v3
extensions [3] EXPLICIT Extensions OPTIONAL
-- If present , version MUST be v3
}
Version ::= INTEGER { v1(0), v2(1), v3(2) }
CertificateSerialNumber ::= INTEGER
Validity ::= SEQUENCE {
notBefore Time,
notAfter Time
}
Time ::= CHOICE {
utcTime UTCTime ,
generalTime GeneralizedTime
}
UniqueIdentifier ::= BIT STRING
SubjectPublicKeyInfo ::= SEQUENCE {
algorithm AlgorithmIdentifier ,
subjectPublicKey BIT STRING
}
Extensions ::= SEQUENCE SIZE (1..MAX) OF Extension
Extension ::= SEQUENCE {
extnID OBJECT IDENTIFIER ,
critical BOOLEAN DEFAULT FALSE ,
extnValue OCTET STRING
-- contains the DER encoding of an ASN.1 value
-- corresponding to the extension type identified
-- by extnID
}
作者:Zhu Ran ,來自vivo互聯網技術團隊