私钥存储发生的事情有点复杂,因为它涉及多年来积累的几层未指定的crud,并为向后兼容而保留。让我们揭开谜底。
对于其加密操作,包括私钥存储(我们目前感兴趣的),OpenSSH依赖于OpenSSL 库。所以 OpenSSH 将支持 OpenSSL 所支持的。
甲私钥是数学对象的一串可以在其是,通常情况下,二值的结构进行编码(即的一串字节,而不是可打印的字符)。让我们假设一个 RSA 密钥。RSA 私钥的格式在PKCS#1 中定义为ASN.1 结构,该结构将使用 DER 编码规则进行编码。
由于许多与加密货币相关的工具在 1990 年代初和中期开始出现,当时电子邮件最流行(网络还很年轻),工具努力使用可以粘贴到电子邮件中的字符(附件这些天文件还不常见)。值得注意的是,有一个早期的标准称为增强隐私的电子邮件,或“PEM”。该标准从未真正部署或使用过,其他系统胜过它(即PGP和S/MIME),但 PEM 的一个特性仍然存在:一种将二进制对象编码为可打印文本的方法。这是PEM 格式。它看起来像这样:
-----BEGIN SOMETHING-----
Some-optional: headers
Base64+Encoded+Data==
-----END SOMETHING-----
所以 PEM 是一种包装器,二进制数据以Base64编码,并添加了页眉和页脚行,其中包括一个类型(“SOMETHING”)。“可选标头”是后来添加的 OpenSSL,它从未被标准化,因此 PEM-with-headers 仅记录为“OpenSSL 的功能”。OpenSSL 文档就是这样,这意味着,为了知道这个过程究竟需要什么,你必须深入研究可怕的 OpenSSL 源代码。
这是一个未加密的 RSA 私钥,采用 PEM 格式:
-----BEGIN RSA PRIVATE KEY-----
MIICXQIBAAKBgQDQ33ndDr5N/AI8y2PzrqGbadLeS5fSf2GsVJx2B2KxhazL2z5O
ufin+wjJ1hW12/zWyQs/9CFYQFrife+PrMUOdLitsmlD3l4lBQ29+XKsmPabtINP
JQ0n4dxgBGeFxTCd4lJwiysmVsXPnNrgQTcx2nirrIk1C7wSW9Ai9W3fZQIDAQAB
AoGBAKiKSvkW9nRSzzNjIwn0da7EG0UIVj+iTZwSwhVzLC32oVH1XTeFVKGnLJZA
y0/tbP2bSBqY0Xc2pp9v4yhZzr6/BUPX+N1FOW8Q5OXHMD4fXSixrX0vYOT8hQuC
ehTAXsStjkZqzCdCsKV9YIduTHoyjL2jG6QBvFQK7kHaYUwZAkEA+rp2b+eBDJrg
lqcPOE2HkCkQcReSW0OIoUgd2tIiPFL8HSNwKvvAAH+QBKL6jvecLswJneecon8Z
jsgn4K/EpwJBANVDultbYq/h3F5FbAQ4r6cMQ2ZmmhMFdt8rRvAdEz18CuobGvAQ
y31hU/InW0n+Z0oHCsIgyowSeCGwRLMJYRMCQGKDXQG+/k+Lku7emPZQUBFucQ1e
a5z8PfTQtxpBMj5thK2WPP5GiDwp4tZPiw8dbvpcJPMsC7k1Iz+cmT6JEUUCQBxz
X54mb+D06bgt3L4nbc+ERE2Z7H4TIYueM2V/C30NWktm+E4Ef5EnddJ9S6Fwbgkj
LV0+kKblI9+iq1eTLb8CQQC+QDF7Y1o4IpDGcu+3WhS/pI/CkXD2pDMJM6rGBgG6
g9D1VTPCx0LZAWK4GdmELhPM+0ePH4P24/VsJY4mvutQ
-----END RSA PRIVATE KEY-----
如您所见,类型是“RSA PRIVATE KEY”。可以通过以下方式探索 ASN.1 结构openssl asn1parse
:
$ openssl asn1parse -i -in keyraw.pem
0:d=0 hl=4 l= 605 cons: SEQUENCE
4:d=1 hl=2 l= 1 prim: INTEGER :00
7:d=1 hl=3 l= 129 prim: INTEGER :D0DF79DD0EBE4DFC023CCB63F3AEA19B69D2DE4B97D27F61AC549C760762B185ACCBDB3E4EB9F8A7FB08C9D615B5DBFCD6C90B3FF42158405AE27DEF8FACC50E74B8ADB26943DE5E25050DBDF972AC98F69BB4834F250D27E1DC60046785C5309DE252708B2B2656C5CF9CDAE0413731DA78ABAC89350BBC125BD022F56DDF65
139:d=1 hl=2 l= 3 prim: INTEGER :010001
144:d=1 hl=3 l= 129 prim: INTEGER :A88A4AF916F67452CF33632309F475AEC41B4508563FA24D9C12C215732C2DF6A151F55D378554A1A72C9640CB4FED6CFD9B481A98D17736A69F6FE32859CEBEBF0543D7F8DD45396F10E4E5C7303E1F5D28B1AD7D2F60E4FC850B827A14C05EC4AD8E466ACC2742B0A57D60876E4C7A328CBDA31BA401BC540AEE41DA614C19
276:d=1 hl=2 l= 65 prim: INTEGER :FABA766FE7810C9AE096A70F384D879029107117925B4388A1481DDAD2223C52FC1D23702AFBC0007F9004A2FA8EF79C2ECC099DE79CA27F198EC827E0AFC4A7
343:d=1 hl=2 l= 65 prim: INTEGER :D543BA5B5B62AFE1DC5E456C0438AFA70C4366669A130576DF2B46F01D133D7C0AEA1B1AF010CB7D6153F2275B49FE674A070AC220CA8C127821B044B3096113
410:d=1 hl=2 l= 64 prim: INTEGER :62835D01BEFE4F8B92EEDE98F65050116E710D5E6B9CFC3DF4D0B71A41323E6D84AD963CFE46883C29E2D64F8B0F1D6EFA5C24F32C0BB935233F9C993E891145
476:d=1 hl=2 l= 64 prim: INTEGER :1C735F9E266FE0F4E9B82DDCBE276DCF84444D99EC7E13218B9E33657F0B7D0D5A4B66F84E047F912775D27D4BA1706E09232D5D3E90A6E523DFA2AB57932DBF
542:d=1 hl=2 l= 65 prim: INTEGER :BE40317B635A382290C672EFB75A14BFA48FC29170F6A4330933AAC60601BA83D0F55533C2C742D90162B819D9842E13CCFB478F1F83F6E3F56C258E26BEEB50
我们在这里认识到 RSA 私钥的组成部分:一些大整数。有关数学详细信息,请参见 PKCS#1。
碰巧 OpenSSL 使用的 PEM 扩展格式支持基于密码的加密。经过一些代码阅读,事实证明加密使用的是 CBC 模式,在 headers 中指定了 IV 和算法;并且密码到密钥的转换依赖于EVP_BytesToKey()
(定义在crypto\evp\evp_key.c
)具有以下特性:
- 这是一个非标准的基于散列的密钥派生函数。
- 用于加密的 IV 也用作盐。
- 哈希函数是 MD5。
- 哈希重复使用n次迭代,但在 PEM 加密的情况下,迭代计数n设置为 1。
KDF 是非标准的令人担忧。重用的盐加密IV是未成年人的担心(这是数学洁净,但可能不是一个真正的问题-而且,至少,还有就是盐)。使用 MD5 也是一个小问题(虽然 MD5 在碰撞方面被彻底破坏,但密钥派生通常依赖于原像抗性,MD5 仍然相当强大,几乎和新的一样好)。迭代计数设置为 1(这意味着根本没有循环)是一个严重的问题。
这意味着如果攻击者试图猜测 PEM 加密密钥的密码,每次尝试的计算成本将是最小的。使用好的 GPU,攻击者每秒可以尝试数十亿次密码。这对舒适来说太快了。基于密码的密钥派生应该是salted和slow,OpenSSL PEM 加密格式在第二点上失败。有关详细讨论,请参阅此答案。
这是一个 PEM 加密的私钥;加密算法设置为 AES-128。密码是“1234”:
-----BEGIN RSA PRIVATE KEY-----
Proc-Type: 4,ENCRYPTED
DEK-Info: AES-128-CBC,8680A1BEAE5661AAD8DA344B7495BCD4
4cvmuk8onrB5IQVRr6xRUBt6yRcjNUGcUWq0CcyX4p4iijANv/S7H5Ga8e5e+12m
k6UUt65mF54Ddh+WE4lHHy5yYEPa25tr/KBMErEhHJxYFiwRwgw/KoF2V8Cpgidd
BA5aeO+5/FmCiTkx/tGYbpE2emfcQ+oNdAKRhIEjIAfItrU4Bj2nQZdiiY0tFEfT
hn5HZ0X1i1yi63nxVGQH+oQQH9+ccPk87cIRLf3IK1B3M0J0j11XDhQdIXwAx9hV
52GXgkk0NX7EtT5Cq3x0Q513e70QA9ua1lt8yaCynkLrYKmMQQCKsLlJDSh+sUyu
ndiVl0g73cUPd962Tp/WCLOV4/DWShfZexfjoibjCkR81OVa9cguYITCXV3QGRCM
wo09DI/INOs1s6FS4ZKugpwgKEX6knh0Fo1i6DdVJQfeQvUo+MhbFjjK0SXT4QWc
4rlQv0Q1YoNn1EzFzsVwx7PhtU9wo4PU1978+582mrJBjteIN9a8z+7lZT1qKynD
BG3XUjnWAq4k5KUj5mEJkSSs2R2AIhHNiSmwmcuzHf67er1KrWvL+g8AXXJ8xLjh
P6ImJeMoEI7P2zb4FvSkQFF5SDjmaPNPpo6xe330EdSSWZTZtcgc9yH++I8ZX9Kb
0UnWic5HTZOx0VLqEqDw+iWufnUDMvq98tGD5c+BQqqofBZae5YNYfko1tCGoz/3
ZygMcOdRqRugur5SiCZnYCnIeQvVNi7nwfp2Bb3K0XMCr12IdeRDuoe45MzoG9zD
hLk0Y3VHS3eANvEsBMAwcyTBjgs8Q3bHdHwnPjVcAo3auOkyXUHZ7DEIxnmvVfaS
-----END RSA PRIVATE KEY-----
由于加密,无法再使用asn1parse
.
PKCS#8是用于编码私钥的无关标准。它实际上是一个包装器。PKCS#8 对象是一个 ASN.1 结构,其中包括一些类型信息,以及作为子对象的私钥。类型信息将声明“这是一个 RSA 私钥”。由于 PKCS#8 是基于 ASN.1 的,它会生成不可打印的二进制文件,因此 OpenSSL 会很高兴地将其再次包装在 PEM 对象中。
因此,这里是与上面相同的 RSA 私钥,作为 PKCS#8 对象,它本身是 PEM 编码的:
-----BEGIN PRIVATE KEY-----
MIICdwIBADANBgkqhkiG9w0BAQEFAASCAmEwggJdAgEAAoGBANDfed0Ovk38AjzL
Y/OuoZtp0t5Ll9J/YaxUnHYHYrGFrMvbPk65+Kf7CMnWFbXb/NbJCz/0IVhAWuJ9
74+sxQ50uK2yaUPeXiUFDb35cqyY9pu0g08lDSfh3GAEZ4XFMJ3iUnCLKyZWxc+c
2uBBNzHaeKusiTULvBJb0CL1bd9lAgMBAAECgYEAqIpK+Rb2dFLPM2MjCfR1rsQb
RQhWP6JNnBLCFXMsLfahUfVdN4VUoacslkDLT+1s/ZtIGpjRdzamn2/jKFnOvr8F
Q9f43UU5bxDk5ccwPh9dKLGtfS9g5PyFC4J6FMBexK2ORmrMJ0KwpX1gh25MejKM
vaMbpAG8VAruQdphTBkCQQD6unZv54EMmuCWpw84TYeQKRBxF5JbQ4ihSB3a0iI8
UvwdI3Aq+8AAf5AEovqO95wuzAmd55yifxmOyCfgr8SnAkEA1UO6W1tir+HcXkVs
BDivpwxDZmaaEwV23ytG8B0TPXwK6hsa8BDLfWFT8idbSf5nSgcKwiDKjBJ4IbBE
swlhEwJAYoNdAb7+T4uS7t6Y9lBQEW5xDV5rnPw99NC3GkEyPm2ErZY8/kaIPCni
1k+LDx1u+lwk8ywLuTUjP5yZPokRRQJAHHNfniZv4PTpuC3cvidtz4RETZnsfhMh
i54zZX8LfQ1aS2b4TgR/kSd10n1LoXBuCSMtXT6QpuUj36KrV5MtvwJBAL5AMXtj
WjgikMZy77daFL+kj8KRcPakMwkzqsYGAbqD0PVVM8LHQtkBYrgZ2YQuE8z7R48f
g/bj9Wwljia+61A=
-----END PRIVATE KEY-----
如您所见,PEM 标头中指示的类型不再是“RSA PRIVATE KEY”,而只是“PRIVATE KEY”。如果我们申请asn1parse
它,我们会得到:
0:d=0 hl=4 l= 631 cons: SEQUENCE
4:d=1 hl=2 l= 1 prim: INTEGER :00
7:d=1 hl=2 l= 13 cons: SEQUENCE
9:d=2 hl=2 l= 9 prim: OBJECT :rsaEncryption
20:d=2 hl=2 l= 0 prim: NULL
22:d=1 hl=4 l= 609 prim: OCTET STRING [HEX DUMP]:30820<skip...>
(我在最后一行删掉了很多字节)。我们看到该结构以一个标识符开头,该标识符表示“这是一个 RSA 私钥”,并且私钥本身作为一个包含在内OCTET STRING
(并且该字符串的内容正是上述基于 ASN.1 的结构)。
PKCS#8 可选地支持基于密码的加密。这是一种非常开放的格式,因此它可能与世界上所有基于密码的加密系统兼容,但软件必须支持它。OpenSSL 支持旧的 DES+MD5 加密,或更新的PBKDF2和可配置的算法。DES(不是 3DES)是一个小问题:DES 相对较弱,因为它的小密钥大小(56 位)使得突破穷举搜索在技术上是可行的(已经完成);但是,对于业余爱好者来说,这将是相当昂贵的。不过,最好使用 PBKDF2 和更好的加密算法。
给定如上所示的原始私钥,这是一个 OpenSSL 命令行,它将其转换为 PKCS#8 对象,具有 3DES 加密和 PBKDF2 用于基于密码的密钥派生:
openssl pkcs8 -topk8 -in keyraw.pem -out keypk8.pem -v2 des3
产生:
-----BEGIN ENCRYPTED PRIVATE KEY-----
MIICxjBABgkqhkiG9w0BBQ0wMzAbBgkqhkiG9w0BBQwwDgQIZT3rvVU85p0CAggA
MBQGCCqGSIb3DQMHBAgtYXWrNG+OYgSCAoCewt8WkgCDaBCSOoe88WTpV2haxUFW
iWkdJQtEkzkpYnwA0E0Bj5CBnSd3EdSRmup0rP9WxzdMe+qx2N+GGLTcmA7pMyBV
XK9OTdiixMWvlG64lrLFtQxoKaxo48zUVobLuRrtaVLvwZ7OpO4hA2zsl6qaWaV7
8GEiAWz28K3DIBDVr1CKpEdFf7epkC7e1/ojJDNAwPiE9rxkaqGHpogqJQKb5s8X
ZyGhVG3rPuwgOxhU5d1G7K6+N9wKYkZXiCmsoqZxD94M3QH8sM8YF41rxBsbPSJ/
7JgGQMOJQxxrdeHSAt5P1iasI7lNXa7HacTZl1nPDXpnpjKA5E/jNMf1EgV+sN3f
pL4GoFvw8zImOF4OHdo9KBz61oKFylQrGQM6WhCsTqsSVZxR0tH8ERSOhhWn2wmy
NgiagfVT4nED9XFInEwTKoXKUjTSOHUmbTl/HF637NrYjSBLgT/e+XBQBmFMSaNc
+KLlJRHpjB8QZ8cIdDFwVIYkmm4Po7h1uYob1d2/4saxjHrtZ8f7GqmT/SGXMpj5
eL0bXDXdjcapDkLx5X0/BYI3AYTlFXEZU0UJT8aad0Fiygw1bLVDR8yDl63Bthlb
gS15LhjqGYGhgX3tARS94HtBvlSAtgV6AB5QjEJfU7jgyu0lFn1hTULmwFJVkjj6
Oy2WeuHseOZ1X45V7DvNcS1iT7fttwQZoSvdks8WulsodpOr7sbtaJbsUUToTxIN
GtNQo9Ce/QAeONmSf8G9jbBURBmLH+kzzzptYcCsVaaUnWPpgebH/WJRa83quPw6
fwy3xZgg9pPHFBiFAG2c3Uuelat/eXhXdW74XlDgOIpmbMfsDxaVOiuM
-----END ENCRYPTED PRIVATE KEY-----
所以现在这是一个“加密的私钥”。让我们看看asn1parse
能说些什么:
0:d=0 hl=4 l= 710 cons: SEQUENCE
4:d=1 hl=2 l= 64 cons: SEQUENCE
6:d=2 hl=2 l= 9 prim: OBJECT :PBES2
17:d=2 hl=2 l= 51 cons: SEQUENCE
19:d=3 hl=2 l= 27 cons: SEQUENCE
21:d=4 hl=2 l= 9 prim: OBJECT :PBKDF2
32:d=4 hl=2 l= 14 cons: SEQUENCE
34:d=5 hl=2 l= 8 prim: OCTET STRING [HEX DUMP]:653DEBBD553CE69D
44:d=5 hl=2 l= 2 prim: INTEGER :0800
48:d=3 hl=2 l= 20 cons: SEQUENCE
50:d=4 hl=2 l= 8 prim: OBJECT :des-ede3-cbc
60:d=4 hl=2 l= 8 prim: OCTET STRING [HEX DUMP]:2D6175AB346F8E62
70:d=1 hl=4 l= 640 prim: OCTET STRING [HEX DUMP]:9EC2DF16920<skip...>
我们在那里看到使用了 PBKDF2。其中OCTET STRING
的内容653DEBBD553CE69D
是 PBKDF2 的盐。的INTEGER
值0800
(即 2048 的十六进制)是迭代计数。加密本身在 CBC 模式下使用 3DES,具有自己随机生成的 IV ( 2D6175AB346F8E62
)。没关系。PBKDF2 默认使用 SHA-1,这不是问题。
碰巧的是,虽然 OpenSSL 支持某种任意的迭代计数(好吧,将其保持在 20 亿以下以避免 32 位有符号整数的问题),openssl pkcs8
命令行工具不允许您从默认的 2048 更改迭代计数,除了将其设置为 1(使用-noiter
选项)。所以这是 2048 或 1,仅此而已。2048 比 1 好得多(比方说,它好 2048 倍),但按照今天的标准,它仍然很低。
总结: OpenSSH 可以接受原始 RSA/PEM 格式的私钥、带加密的 RSA/PEM、不带加密的 PKCS#8 或带加密的 PKCS#8(可以是“旧式”或 PBKDF2)。对于私钥的密码保护,防止可能窃取您的私钥文件副本的攻击者,您真的想使用最后一个选项:PKCS#8 与 PBKDF2 加密。不幸的是,使用openssl
命令行工具,您无法对 PBKDF2 进行太多配置。您不能选择散列函数(即 SHA-1,仅此而已 - 这不是一个真正的问题),更重要的是,您不能选择迭代次数,默认值为 2048,这对于舒适性来说有点低。
您可以使用其他一些具有更高 PBKDF2 迭代次数的工具来加密您的密钥,但我不知道有任何现成的工具可以用于此目的。这将是使用加密库进行一些编程的问题。
无论如何,你最好有一个强密码。15 个随机小写字母(易于输入,不难记住)将提供 70 位的熵,这足以阻止攻击者,即使使用了错误的密码推导(迭代次数为 1)。