這是密碼學(xué)筆記第三篇烤低。之前兩篇分析了 RSA 算法和橢圓曲線密碼學(xué)的基本原理,從中可以知道 RSA 算法的本質(zhì)是大整數(shù)質(zhì)數(shù)因子分解计露,橢圓曲線密碼學(xué)的本質(zhì)是曲線上的打點啤挎,樸素的原理后面處處閃耀著數(shù)學(xué)之光。從理論到實踐炭玫,這篇文章來解析下日常使用的密鑰文件格式奈嘿。
文章主要解決下面幾個問題:
- 使用
openssl
和ssh-keygen
生成的 RSA 和 ECC 的密鑰文件里面存了什么內(nèi)容,用的什么編碼規(guī)則吞加? - 有些 RSA 私鑰頭部是
-----BEGIN RSA PRIVATE KEY-----
裙犹,而有些又是-----BEGIN PRIVATE KEY-----
,它們存儲的內(nèi)容有什么區(qū)別衔憨? - 使用
openssl
生成的公鑰跟ssh-keygen
生成的密鑰對中的公鑰格式不一樣叶圃,它們有什么區(qū)別? -
openssl
和ssh-keygen
都支持輸入密碼對私鑰加密以增強安全性巫财,它們加密方式分別是什么盗似?有什么不同之處嗎哩陕?
1 ASN.1
ASN.1(Abstract Syntax Notation One) 是一種數(shù)據(jù)描述語言平项,跟 protobuf 和 Thrift 這些接口描述語言類似赫舒,它通過模塊(module) 來描述數(shù)據(jù)結(jié)構(gòu)。ASN.1 最早用于電信領(lǐng)域闽瓢,后來在計算機密碼學(xué)中也有廣泛應(yīng)用接癌。
ASN.1 定義了一些數(shù)據(jù)類型來描述數(shù)據(jù)結(jié)構(gòu),包括基礎(chǔ)類型(如整數(shù)類型 INTEGER扣讼,布爾類型 BOOLEAN缺猛,字符串類型 OCTET STRING,BIT STRING)和結(jié)構(gòu)化類型(如結(jié)構(gòu)體類型 SEQUENCE椭符,列表類型 SEQUENCE OF 等)荔燎,完整類型列表見 [ASN.1 Types]。類型通常都有個類型標(biāo)簽销钝,其中類型標(biāo)簽分為通用的有咨、應(yīng)用自定義的、上下文特定的蒸健、以及私有的 4 種類型座享。其中通用的類型標(biāo)簽如下:
Type | Tag Number |
---|---|
INTEGER | 0x02 |
BIT STRING | 0x03 |
OCTET STRING | 0x04 |
NULL | 0x05 |
OBJECT IDENTIFIER | 0x06 |
SEQUENCE and SEQUENCE OF | 0x10 |
IA5String | 0x16 |
UTCTime | 0x17 |
下面用ASN.1定義了一個名為 FooProtocol 的模塊,其中包含一個結(jié)構(gòu)體類型字段 FooQuestion似忧。結(jié)構(gòu)體 FooQuestion 包括一個整形字段 id 和一個字符串類型字段 question(IA5String 是不包括控制字符的字符串類型)渣叛。
FooProtocol DEFINITIONS ::= BEGIN
FooQuestion ::= SEQUENCE {
id INTEGER,
question IA5String
}
END
ASN.1 只是描述了數(shù)據(jù)結(jié)構(gòu),并沒有指定怎么編碼數(shù)據(jù)盯捌。因此淳衙,出現(xiàn)了多種編碼規(guī)則以方便數(shù)據(jù)在網(wǎng)絡(luò)上傳輸和不同終端間交互。比較常見的有 XER, JER, BER, DER等饺著。如待編碼的數(shù)據(jù)如下:
myQuestion FooQuestion ::= SEQUENCE {
id 5,
question "Anybody there?"
}
使用各編碼規(guī)則編碼結(jié)果如下滤祖,其中 XER 和 JER 不用多說,BER 和 DER 是最常見的密鑰文件編碼規(guī)則瓶籽,下一節(jié)詳細(xì)分析匠童。
XER(XML Encoding Rules)
<FooQuestion>
<id>5</id>
<question>Anybody there?</question>
</FooQuestion>
JER(JSON Encoding Rules)
{ "id" : 5, "question" : "Anybody there?" }
BER(Basic Encoding Rules)
30 13 02 01 05 16 0e 41 6e 79 62 6f 64 79 20 74 68 65 72 65 3f
DER(Distinguished Encoding Rules)
30 13 02 01 05 16 0e 41 6e 79 62 6f 64 79 20 74 68 65 72 65 3f
2 編碼規(guī)則
前文提到 ASN.1 只是定義了數(shù)據(jù)結(jié)構(gòu),并未規(guī)定具體的編碼方式塑顺,與之對應(yīng)的有多種編碼規(guī)則汤求。ASN.1 與特定的編碼規(guī)則一起通過使用獨立于計算機架構(gòu)和編程語言的方法來描述數(shù)據(jù)結(jié)構(gòu),為結(jié)構(gòu)化數(shù)據(jù)的表示严拒、編碼扬绪、傳輸、解碼提供了便利裤唠。本節(jié)主要介紹密鑰和證書中常見的編碼規(guī)則 BER挤牛,DER,以及由 DER 衍生出的密鑰文件格式 PEM种蘸。
BER (Basic Encoding Rules)
BER 是基礎(chǔ)編碼規(guī)則墓赴,編碼結(jié)構(gòu)包括類型標(biāo)志竞膳、長度,值以及結(jié)束符(可選)诫硕,每個字段以 8bit 即字節(jié)進(jìn)行分割坦辟。
----------------------------------------------------
| Identifier | Length | Contents | End-of-contents |
| octets | octets | octets | octets |
----------------------------------------------------
Identifier: 類型標(biāo)志,就是ASN.1 規(guī)定的類型章办,只是除了標(biāo)簽號(tag number)外锉走,還加了 3 位,第 7藕届,8 位用于區(qū)分是通用的標(biāo)簽類型還是其他標(biāo)簽類型挪蹭, 第 6 位 用于區(qū)分是基礎(chǔ)類型還是結(jié)構(gòu)化類型。Identifier 結(jié)構(gòu)如下休偶,后面我們會看到密鑰中的結(jié)構(gòu)體類型 SEQUENCE 的 Identifier 為 0x30嚣潜,即是由這個格式而來(0011000)。
---------------------------------
| 8 | 7 | 6 | 5 | 4 | 3 | 2 | 1 |
---------------------------------
|Class |P/C| Tag Number |
---------------------------------
- Class:如果是通用類型標(biāo)簽則為 00椅贱,應(yīng)用自定義的類型標(biāo)簽則為 01懂算,上下文特定類型標(biāo)簽 10,私有類型標(biāo)簽 11庇麦。
- P/C:如果是基礎(chǔ)類型计技,則為 0,結(jié)構(gòu)化類型為 1山橄。
- Tag Number:就是 ASN.1 中定義的數(shù)據(jù)類型標(biāo)簽號垮媒。
Length: 分三種情況,
- 1)數(shù)據(jù)長度 < 128:則 Length 的 8bit首位為0航棱,其他7位表示數(shù)據(jù)長度睡雇。
- 2)數(shù)據(jù)長度 >= 128:則 Length 的第一個8bit為
0x8?
,其中?
是后面跟的是長度饮醇。比如0x81
表示后面一個字節(jié)為長度它抱,如果是0x82
則表示后面兩個字節(jié)為長度,以此類推朴艰。 - 3)如果數(shù)據(jù)長度未知观蓄,則
Length=0x80
,并增加End-of-contents=00 00
結(jié)束標(biāo)記祠墅。
Contents & End-of-contents: 數(shù)據(jù)內(nèi)容侮穿。對于未知數(shù)據(jù)長度的數(shù)據(jù)類型被碗,才有 End-of-contents凳忙,為00 00
宵喂。
實例:
- 使用 OCTET STRING 編碼字符串
Hello
空民,為04 05 48 65 6C 6C 6F
壶谒,即類型為04
黔姜,長度為05
甘有,內(nèi)容為0x48 65 6C 6C 6F
土居,即Hello
的 ASCII 碼。 - 使用 INTEGER 編碼整數(shù) 129娶耍,為
02 81 81
。 - 結(jié)構(gòu)化類型是包含了多個數(shù)據(jù)類型的復(fù)合類型饼酿,后面詳細(xì)分析榕酒。
DER (Distinguished Encoding Rules)
DER 是典型的 Tag-Length-Value(TLV)
編碼方式,是 PKCS 密鑰體系常用的編碼故俐。
DER 是 BER 的子集想鹰,編碼規(guī)則幾乎一樣,不過去掉了 BER 的一些靈活性药版,多了幾個限制:
- 如果數(shù)據(jù)長度在 0-127 之間辑舷,則 Length 必須使用第 1 種編碼方式。
- 如果數(shù)據(jù)長度 >= 128槽片,則 Length 必須使用第 2 種編碼方式何缓,且 Length 必須用最少的字節(jié)編碼,如果能用 2 字節(jié)的則不能用 3 字節(jié)还栓。
- 數(shù)據(jù)要用明確長度的編碼方式碌廓,不支持 Length 的第3種編碼即未知數(shù)據(jù)長度+結(jié)束標(biāo)記的方式。
注意:ASN.1 規(guī)定整型 INTEGER 需要支持正整數(shù)剩盒、負(fù)整數(shù)和零谷婆。BER/DER 使用大端模式存儲 INTEGER,并通過最高位來編碼正負(fù)數(shù)(最高位0為正數(shù)辽聊,1為負(fù)數(shù))纪挎。 如果密鑰參數(shù)值最高位為 1,則 BER/DER 編碼會在參數(shù)前額外加 0x00
以表示正數(shù)跟匆,這也就是為什么有時候密鑰參數(shù)前面會多出1字節(jié) 0x00
的原因异袄。
PEM(Privacy Enhanced Mail): 因為 DER 編碼是二進(jìn)制數(shù)據(jù),早期的 Email 不能發(fā)送附件玛臂,也不方便直接傳輸二進(jìn)制數(shù)據(jù)([原因])隙轻,因此密鑰文件通常會在 DER 編碼基礎(chǔ)上進(jìn)行 Base64 編碼,這就是經(jīng)彻缚看到的密鑰文件格式 PEM玖绿。PEM 最早是用來增強郵件安全性的,不過沒有被廣泛接受叁巨,最后卻是在密碼學(xué)中得到了發(fā)揚光大斑匪,如 openssl 和 ssh-keygen 工具生成的公私鑰文件默認(rèn)都采用 PEM 格式。需要注意的是,PEM 本身不是 ASN.1 的編碼規(guī)則蚀瘸,它只是 Base64-encoded DER
狡蝶。
3 密鑰格式解析
ASN.1 編碼規(guī)則只定義了數(shù)據(jù)編碼方式,但是并沒有賦予數(shù)據(jù)意義贮勃。公鑰密碼學(xué)標(biāo)準(zhǔn) PKCS (Public Key Cryptography Standards) 和公鑰基礎(chǔ)設(shè)施 PKIX(Public-Key Infrastructure X.509) 等使用 ASN.1 的類型定義密鑰和證書的格式和編碼贪惹,以描述公私鑰和證書屬性。 需要注意寂嘉,PKCS 雖然名字是公鑰密碼學(xué)標(biāo)準(zhǔn)奏瞬,它其實也包括私鑰格式標(biāo)準(zhǔn)。 這兩個標(biāo)準(zhǔn)的內(nèi)容浩瀚如煙泉孩,本節(jié)只分析常見幾種密鑰相關(guān)的部分硼端。
3.1 PKCS #1
PKCS #1 是 RSA Cryptography Specifications
,即 RSA 密碼學(xué)規(guī)范寓搬,它在 [rfc8017] 中有詳細(xì)說明珍昨,定義了 RSA 密鑰文件的格式和編碼方式,以及加解密句喷、簽名镣典、填充的基礎(chǔ)算法。
RSA 密鑰格式
RSA 公私鑰的 ASN.1 類型定義如下唾琼,根據(jù)類型定義和數(shù)據(jù)編碼骆撇,就能解析出 RSA 公私鑰中的參數(shù)了(參數(shù)含義請參考我之前的《RSA算法原理解析》一文)。
RSAPublicKey ::= SEQUENCE {
modulus INTEGER, -- n
publicExponent INTEGER -- e
}
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
}
使用 openssl 生成的一對 RSA 公私鑰 (示例為方便展示父叙,用的 1024 位密鑰神郊,實際中請使用 2048 位以上)
$ openssl genrsa -out prikey.p1 1024
$ openssl rsa -in prikey.p1 -pubout -RSAPublicKey_out > pubkey.p1
PKCS#1 格式解析如下:公鑰的 SEQUENCE 包括 RSA 公鑰參數(shù) n 和 e 兩個屬性。RSA 私鑰則首先是版本號 0趾唱,然后是 RSA 私鑰的 8 個參數(shù)涌乳。
加密私鑰
可以對 PKCS#1 的私鑰進(jìn)行加密,如 ssh-keygen 可以指定 passphrase
(測試的密碼是 testtest)加密 RSA 私鑰甜癞,加密后的私鑰 enc_prikey.p1
格式如下:
-----BEGIN RSA PRIVATE KEY-----
Proc-Type: 4,ENCRYPTED
DEK-Info: AES-128-CBC,8C2A8D6593F411D7336B842037B5200B
EncryptedRSAPrivateKey
-----END RSA PRIVATE KEY-----
DEK-Info 里面指明了加密算法是 AES-128-CBC夕晓,IV 是 8C2A8D6593F411D7336B842037B5200B
,AES加密的實際密碼=md5(設(shè)定密碼 + IV的前8個字節(jié))悠咱≌袅荆可以使用 openssl aes-128-cbc
驗證加密結(jié)果是否與 EncryptedRSAPrivateKey 一致。
$ tail -n +2 prikey.p1 | grep -v 'END RSA' | base64 -d |
openssl aes-128-cbc -e -iv 8C2A8D6593F411D7336B842037B5200B -K $(python -c "exec(\"import hashlib\\nprint hashlib.md5(bytearray('testtest') + bytearray.fromhex('8C2A8D6593F411D7')).hexdigest()\")") | base64
3.2 PKCS #8
PKCS#8 是 Private-Key Information Syntax Standard
析既,即私鑰格式相關(guān)的標(biāo)準(zhǔn)躬贡,它不像 PKCS#1 只支持 RSA,而是支持各種類型的私鑰眼坏。PKCS#8 私鑰文件格式中首尾并未說明私鑰算法類型拂玻,算法類型在數(shù)據(jù)中標(biāo)識。PKCS#8 中的私鑰也支持加密。
未加密私鑰格式
未加密私鑰格式的 ASN.1 類型定義如下(參見 [rfc5958] ):
OneAsymmetricKey ::= SEQUENCE {
version Version,
privateKeyAlgorithm PrivateKeyAlgorithmIdentifier,
privateKey PrivateKey,
attributes [0] Attributes OPTIONAL,
...,
[[2: publicKey [1] PublicKey OPTIONAL ]],
...}
Version ::= INTEGER # 版本號檐蚜。
PrivateKeyAlgorithmIdentifier ::= SEQUENCE { # 密鑰算法標(biāo)識
algorithm OBJECT IDENTIFIER,
parameters ANY DEFINED BY algorithm OPTIONAL }
PrivateKey ::= OCTET STRING # 不同類型的私鑰格式不同魄懂,比如 RSA 的是 RSAPrivateKey類型,而 ECC 的是 ECPrivateKey 類型闯第。
Attributes ::= SET OF Attribute # 跟公鑰相關(guān)的屬性市栗,比如證書什么的,在公私鑰中通常為空咳短。
PublicKey ::= BIT STRING # 不同類型密鑰包含的公鑰內(nèi)容也不同填帽。
RSA 私鑰格式
可以使用 openssl 將 PKCS#1 格式的私鑰 prikey.p1 轉(zhuǎn)換成 PKCS#8 格式的 prikey.p8,如下:
$ openssl pkcs8 -in prikey.p1 -topk8 -out prikey.p8 -nocrypt
私鑰格式解析如下:
- version:版本號诲泌,目前值為 0盲赊。
- privateKeyAlgorithm:私鑰算法铣鹏,
rsaEncryption
的OBJECT IDENTIFIER
是1.2.840.113549.1.1.1
敷扫,具體含義參見 [這里]。 - privateKey:私鑰诚卸,OCTET STRING 類型葵第,里面其實封裝了一個 RSAPrivateKey 類型,跟 PKCS#1 一樣合溺。
- attributes 和 publicKey 為空卒密。
ECC 私鑰格式
橢圓曲線類型的私鑰格式在 rfc5915 中定義如下:
ECPrivateKey ::= SEQUENCE {
version INTEGER { ecPrivkeyVer1(1) } (ecPrivkeyVer1),
privateKey OCTET STRING,
parameters [0] ECParameters {{ NamedCurve }} OPTIONAL,
publicKey [1] BIT STRING OPTIONAL
}
使用 openssl 創(chuàng)建一個 PKCS#8 格式的 ecc 密鑰,采用 prime256v1 曲線:
# 生成傳統(tǒng)格式的 ECC 私鑰棠赛,類似 PKCS#1 那樣哮奇,只包含 privateKey,密鑰類型在頭部 -----BEGIN EC PRIVATE KEY----- 標(biāo)識睛约,橢圓曲線在 parameters 標(biāo)識鼎俘。
$ openssl ecparam -name prime256v1 -genkey -noout -out ecc_prikey.tradfile
# 轉(zhuǎn)換為 PKCS#8 格式
$ openssl pkcs8 -topk8 -in ecc_prikey.tradfile -out ecc_prikey.p8 -nocrypt
$ cat ecc_prikey.p8
-----BEGIN PRIVATE KEY-----
MIGHAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBG0wawIBAQQgMbahscIGpSZ6NULI
iQ/pTI9ZcvFdXKtjN1bAGO2bxvahRANCAATwq1k9rx/8neP8MqVR7UuJ98bLFsU5
jpueH0ougZNWrsKUki0cgKDGrb3C8Q2NMRO336ve22Xk674lk/ZDHkAV
-----END PRIVATE KEY-----
ASN.1 格式解析如下:
- 前面部分是算法標(biāo)識
1.2.840.10045.2.1(ecPublicKey) 和 1.2.840.10045.3.1.7 (prime256v1)
。 - 后面是私鑰信息辩涝,其中包括了版本號 1贸伐,OCTET STRING 類型私鑰
31B6A1...F6
,BIT STRING 類型的公鑰0000 0100 1111 0000...
怔揩。
當(dāng)然通過 openssl
可以直接解析出公私鑰和曲線類型捉邢,如下:
$ openssl ec -in ecc_prikey.p8 -noout -text
read EC key
Private-Key: (256 bit)
priv:
31:b6:a1:b1:c2:06:a5:26:7a:35:42:c8:89:0f:e9:
4c:8f:59:72:f1:5d:5c:ab:63:37:56:c0:18:ed:9b:
c6:f6
pub:
04:f0:ab:59:3d:af:1f:fc:9d:e3:fc:32:a5:51:ed:
4b:89:f7:c6:cb:16:c5:39:8e:9b:9e:1f:4a:2e:81:
93:56:ae:c2:94:92:2d:1c:80:a0:c6:ad:bd:c2:f1:
0d:8d:31:13:b7:df:ab:de:db:65:e4:eb:be:25:93:
f6:43:1e:40:15
ASN1 OID: prime256v1
NIST CURVE: P-256
在上一篇《橢圓曲線密碼學(xué)原理分析》一文知道,橢圓曲線的密鑰生成其實就是一個公式 P = nG
商膊,n 就是私鑰伏伐,G 是基點,P 是公鑰晕拆。注意到這里公鑰的第一個字節(jié) 04
表示公鑰格式是 uncompressed format
秘案,即非壓縮格式,也就是把點的 X 和 Y 坐標(biāo)合到一起作為公鑰。壓縮格式就是只用 X 坐標(biāo)或者 Y 坐標(biāo)中的一個阱高,另一個坐標(biāo)根據(jù)曲線方程可以求得([rfc5480] 有詳細(xì)說明)赚导。可以通過 libnum 庫來驗證下公私鑰的準(zhǔn)確性赤惊。
$ cat ecc.py
from libnum.ecc import Curve
curve = Curve(
a=0xffffffff00000001000000000000000000000000fffffffffffffffffffffffc,
b=0x5ac635d8aa3a93e7b3ebbd55769886bc651d06b0cc53b0f63bce3c3e27d2604b,
p=0xffffffff00000001000000000000000000000000ffffffffffffffffffffffff,
g = (0x6b17d1f2e12c4247f8bce6e563a440f277037d812deb33a0f4a13945d898c296,
0x4fe342e2fe1a7f9b8ee7eb4a7c0f9e162bce33576b315ececbb6406837bf51f5)
)
pri = 0x31b6a1b1c206a5267a3542c8890fe94c8f5972f15d5cab633756c018ed9bc6f6
pub = curve.power(curve.g, pri)
print hex(pub[0]), hex(pub[1])
$ python ecc.py
('0xf0ab593daf1ffc9de3fc32a551ed4b89f7c6cb16c5398e9b9e1f4a2e819356aeL',
'0xc294922d1c80a0c6adbdc2f10d8d3113b7dfabdedb65e4ebbe2593f6431e4015L')
加密私鑰格式
PKCS#8 里面對私鑰加密提供了 PBES2(Password-Based Encryption Scheme 2)加密模式支持吼旧。通過 PBKDF2(Password-Based Key Derivation Function 2) 對原始密碼進(jìn)行多次哈希處理作為加密密碼以增強破解難度,然后用對稱加密算法 AES 或者 DES 對私鑰進(jìn)行加密未舟。
PBKDF2 是一種 CPU 密集型算法圈暗,但是如果使用 GPU 陣列或者 FPGA 來破解還是相對容易。在密碼存儲中現(xiàn)在更傾向于用 Bcrypt裕膀,它不僅是 CPU 運算密集员串,而且是內(nèi)存密集,破解難度會更高一些昼扛。不過總的來說寸齐, PBKDF2 比 ssh-keygen 的 md5 方式生存密碼安全性會高很多。
PKCS#8 加密類型私鑰的 ASN.1 類型定義如下:
EncryptedPrivateKeyInfo ::= SEQUENCE {
encryptionAlgorithm EncryptionAlgorithmIdentifier,
encryptedData EncryptedData }
EncryptionAlgorithmIdentifier ::= AlgorithmIdentifier
{ CONTENT-ENCRYPTION,
{ KeyEncryptionAlgorithms } }
EncryptedData ::= OCTET STRING
使用 openssl 對 PKCS#8 格式的密鑰加密是很方便的抄谐,默認(rèn)就支持(生成密鑰時不加 -nocrypt
參數(shù)即可)渺鹦。加密后的 ecc_prikey.p8 格式解析如下:
- 其中加密模式是
pkcs5PBES2
,密鑰生成算法是PBKDF2
蛹含,參數(shù)salt= E20EED9A112B7BFA毅厚,iteration=2048
,哈希算法是hmacWithSHA256
浦箱。 - 對稱加密算法是
aes256-cbc
吸耿,參數(shù)iv=27579581D081AEDA083889370232AD1A
。 - 最后一行
11E9C5C2....
就是加密私鑰 encryptedData酷窥。
加密過程解析:
- 先使用密鑰生成算法 PBKDF2 生成加密密碼咽安,python 可以用
backports.pbkdf2
模塊。
import os, binascii
from backports.pbkdf2 import pbkdf2_hmac
salt = binascii.unhexlify('E20EED9A112B7BFA')
passwd = b"testtest"
key = pbkdf2_hmac("sha256", passwd, salt, 2048, 32)
print("Derived key:", binascii.hexlify(key))
# 輸出: ('Derived key:', 'bf48084fd98fcbacd8e024166efb7232c897282fe7e4ff836db3f3d81e32ede9')
- 然后使用 openssl 的 aes256-cbc 加密原私鑰竖幔,可以驗證跟 encryptedData 是一樣的板乙。
$ tail -n +2 ecc_prikey.p8 | grep -v 'END '| base64 -d |
openssl aes-256-cbc -e -iv 27579581D081AEDA083889370232AD1A -K bf48084fd98fcbacd8e024166efb7232c897282fe7e4ff836db3f3d81e32ede9 | hexdump -C
00000000 11 e9 c5 c2 4c c3 2d bb fa 84 b9 fb db f1 d1 ff |....L.-.........|
00000010 f0 6a 5b fa c3 a6 88 cd 02 4c ac 52 84 f4 cb c1 |.j[......L.R....|
......
00000080 8f 72 96 7a 58 aa 1f 5a 6f c1 bf dc 43 1a 46 26 |.r.zX..Zo...C.F&|
3.3 PKIX
前面提到 PKCS#8 定義了通用的私鑰格式支持各類私鑰,在 PKIX ([rfc5280]) 中也定義了通用的公鑰格式拳氢。其中包括算法標(biāo)識和公鑰內(nèi)容募逞,算法標(biāo)識 AlgorithmIdentifier 與前面私鑰中的 PrivateAlgorithmIdentifier 是類似的。
SubjectPublicKeyInfo ::= SEQUENCE {
algorithm AlgorithmIdentifier,
subjectPublicKey BIT STRING }
AlgorithmIdentifier ::= SEQUENCE {
algorithm OBJECT IDENTIFIER,
parameters ANY DEFINED BY algorithm OPTIONAL
}
將之前的 PKCS#1 格式的 RSA 公鑰轉(zhuǎn)換成 PKIX 的格式:
$ openssl rsa -RSAPublicKey_in -in ../pk1/pubkey.p1 -pubout > pubkey.pkix
PKIX 格式的公鑰解析如下馋评,包括公鑰算法 rsaEncryption 和 RSA 公鑰參數(shù) n 和 e放接。
3.4 openssl 和 ssh-keygen 生成公鑰格式區(qū)別
相信大家會發(fā)現(xiàn) ssh-keygen -t rsa
生成的 RSA 密鑰對中公鑰格式跟 PKCS#1 和 PKIX 中的都不一樣。ssh-keygen 生成的公鑰 id_rsa.pub 如下所示:
ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAAAgQC+gCiA//vUMu/2dYj9oGUpY2TCw5/AtkfI2cWvl7hOkliQd7uI61gE9BV5w+Ib+HnjAB9lFYS4A8rlpRlkH9a+mCN2K/Oh5dhoonxat4qeHB5XDvmImUfdOGayT5l176KWP4ftGJt+8ygRpo05zcbuBrd/KxFZ7KDiQyXRvRv9mw== vagrant@stretch
這是 openssh 使用的一種專屬格式:
[type-name] [base64-encoded-ssh-public-key] [comment]
其中 base64-encoded-ssh-public-key
并沒有使用 ASN.1 的數(shù)據(jù)類型定義和 DER 編碼留特,而是使用的 SSH 協(xié)議定義的格式(見 [rfc4251])纠脾,分為 3 部分:
string "ssh-rsa"
mpint e
mpint n
這里的 string 類型和 mpint 類型在 rfc4251 中定義玛瘸,其中 mpint是使用字符串的方式來存儲整數(shù),它們都會在數(shù)據(jù)前先用4字節(jié)存儲數(shù)據(jù)長度苟蹈。id_rsa.pub 解析后格式如下所示:
00 00 00 07 73 73 68 2d 72 73 61 | 長度 7糊渊,值 ssh-rsa|
00 00 00 03 01 00 01 | e 長度 3,值 0x010001 |
00 00 00 81 00 be 80 28 80 ff fb d4 |n 長度 127慧脱,值 0x00 be 80 28 80...|
......
也可以將 ssh-keygen 生成的公鑰轉(zhuǎn)換成 PKCS#1 的格式:
$ ssh-keygen -f id_rsa.pub -e -m pem > pubkey.p1
4 總結(jié)
PKCS 和 PKIX 使用 ASN.1 定義了密鑰的數(shù)據(jù)結(jié)構(gòu)渺绒,并使用 DER 編碼規(guī)則編碼密鑰,最終使用 PEM (Base64-encoded DER) 格式將密鑰數(shù)據(jù)存儲在密鑰文件中菱鸥。
PKCS#1 首部會標(biāo)識密鑰算法類型宗兼,PKCS#8 則是在密鑰數(shù)據(jù)中有字段專門存儲密鑰算法。openssh 使用的公鑰格式是 SSH 協(xié)議中定義的氮采,雖然參數(shù)值一樣殷绍,但是編碼方式與 PKCS 和 PKIX 標(biāo)準(zhǔn)都不同。
ssh-keygen 是對原始密碼和初始向量經(jīng)過一個簡單規(guī)則 md5 后生成加密密碼鹊漠,然后使用 aes128-cbc 對稱加密主到。而 openssl 則是對原始密碼采用 PBKDF2 算法生成加密密碼,然后使用 aes256-cbc 對稱加密贸呢,openssl 的加密方式更安全一些镰烧。
參考資料
- https://en.wikipedia.org/wiki/Abstract_Syntax_Notation_One
- https://en.wikipedia.org/wiki/PKCS
- https://martin.kleppmann.com/2013/05/24/improving-security-of-ssh-private-keys.html
- https://cryptobook.nakov.com/mac-and-key-derivation/pbkdf2
- https://blog.ndpar.com/2017/04/17/p1-p8/
- https://lapo.it/asn1js (解析ASN.1的在線工具拢军,比 openssl asn1parser 清晰一點)