原文: What’s in a SSH RSA key pair?
你也许有自己被保护好的ssh密钥对,它很大可能是基于ssh-keygen的默认选项RSA生成的。
RSA是一个非常简单却相当精彩的算法,这篇文章会展示SSH RSA密钥对里包含了什么,并且让你知道这些值可以用来怎么玩,还会教你怎样只用一个计算器来加密数据。
RSA算法是基于质数和大整数质数分解难的原理。这篇文章不是为了介绍RSA的,不过这里有一个快速的的回顾。大部分情况下我会使用和 Wikipedia 上一样的符号:首先你产生两个大的质数, p 和 q。φ = (p-1)(q-1)。选择一个e和φ互质,d ≡ e^-1 mod φ。
这样就能产生公钥(e, n),私钥(d, n)。如果要加密一个数字或者消息m, 可以通过计算 c ≡ m e mod n,得出加密后的c,计算m ≡ c d mod n来解密。
这是一个非常简单的同余运算,不过当你用ssh-keygen生成一组密钥对的时候,你得到的却是一些难以读懂而且看起来有点吓人的文件, id_rsa
和 id_rsa.pub
。这里是一些id_rsa里的内容(没设密码的):
-----BEGIN RSA PRIVATE KEY----- MIIBygIBAAJhANj3rl3FhzmOloVCXXesVPs1Wa++fIBX7BCZ5t4lmMh36KGzkQmn jDJcm+O9nYhoPx6Bf+a9yz0HfzbfA5OpqQAyC/vRTVDgHhGXY6HFP/lyWQ8DRzCh tsuP6eq9RYHnxwIBIwJhAKdf+4oqqiUWOZn//vXrV3/19LrGJYeU ... -----END RSA PRIVATE KEY-----
我们怎样才能从这一团糟中得到我们漂亮的RSA的参数呢?
简单的方式是通过openssl: (我先为下面文章中的垃圾数据道个歉)
<code><span><a href="/cdn-cgi/l/email-protection" data-cfemail="c9bfa0ada8bb89bfa0ada8bba1a6a5aca7">[email protected]</a> <script data-cfhash="f9e31" type="text/javascript">/* <![CDATA[ */!function(t,e,r,n,c,a,p){try{t=document.currentScript||function(){for(t=document.getElementsByTagName('script'),e=t.length;e--;)if(t[e].getAttribute('data-cfhash'))return t[e]}();if(t&&(c=t.previousSibling)){p=t.parentNode;if(a=c.getAttribute('data-cfemail')){for(e='',r='0x'+a.substr(0,2)|0,n=2;a.length-n;n+=2)e+='%'+('0'+('0x'+a.substr(n,2)^r).toString(16)).slice(-2);p.replaceChild(document.createTextNode(decodeURIComponent(e)),c)}p.removeChild(t)}}catch(u){}}()/* ]]> */ </script> ~/.ssh $ openssl rsa -text -noout < id_rsa </span><span>Private-Key: (768 bit) </span><span>modulus: </span><span>00:d8:f7:ae:5d:c5:87:39:8e:96:85:42:5d:77:ac: </span><span>54:fb:35:59:af:be:7c:80:57:ec:10:99:e6:de:25: </span><span>... </span><span>publicExponent: 35 (0x23) </span><span>privateExponent: </span><span>00:a7:5f:fb:8a:2a:aa:25:16:39:99:ff:fe:f5:eb: </span><span>57:7f:f5:f4:ba:c6:25:87:94:48:64:93:fb:3d:a7: </span><span>... </span><span>prime1: </span><span>... </span><span>prime2: </span><span>... </span><span>exponent1: </span><span>... </span><span>exponent2: </span><span>... </span><span>coefficient: </span><span>...</span></code>
这里的modulus就是n, publicExponent是e, privateExponent是d, prime1和prime2是p和q,exponent1是 这篇Wikipedia文章 中提到的dP,exponent2是dQ,coefficient是qInv。
只有前三个是执行加密和解密必须的,剩下的三个是为了优化计算,而两个质数是用来验证的。
有趣的是要注意尽管从RSA的视角来看私钥只是(d,n),OpenSSH的私钥还包含了e,p,q和剩下的几个。这样它就可以从私钥中生成公钥。否则给出(d,n)要找出e,和给出(e,n)要找出d一样困难,除非e是通常选择的一个非常小的数,这样可以很快的猜到。
如果我们把这些16进制的字符串放在一行,去掉冒号,并且改成大写,就可以用bc来处理和转换成十机制格式。
<code><span># If you don't want to do this yourself, see end for a script </span><span><a href="/cdn-cgi/l/email-protection" data-cfemail="16607f72776456607f7277647e797a7378">[email protected]</a> <script data-cfhash="f9e31" type="text/javascript">/* <![CDATA[ */!function(t,e,r,n,c,a,p){try{t=document.currentScript||function(){for(t=document.getElementsByTagName('script'),e=t.length;e--;)if(t[e].getAttribute('data-cfhash'))return t[e]}();if(t&&(c=t.previousSibling)){p=t.parentNode;if(a=c.getAttribute('data-cfemail')){for(e='',r='0x'+a.substr(0,2)|0,n=2;a.length-n;n+=2)e+='%'+('0'+('0x'+a.substr(n,2)^r).toString(16)).slice(-2);p.replaceChild(document.createTextNode(decodeURIComponent(e)),c)}p.removeChild(t)}}catch(u){}}()/* ]]> */ </script> ~/.ssh $ { echo 'ibase=16'; cat | tr -d ':/n ' | tr a-f A-F; echo; } | bc </span><span> </span><span>00:d8:f7:ae:5d:c5:87:39:8e:96:85:42:5d:77:ac: </span><span>54:fb:35:59:af:be:7c:80:57:ec:10:99:e6:de:25: </span><span>98:c8:77:e8:a1:b3:91:09:a7:8c:32:5c:9b:e3:bd: </span><span>.... </span><span>Ctrl-d to end input </span><span>13158045936463264355006370413708684112837853704660293756254884673628/ </span><span>63292...</span></code>
我们还需要一个计算同余幂的函数,因为如果要通过计算b e 来计算b e % m 会非常慢。幸运的是bc是可编程的。
<code><span><a href="/cdn-cgi/l/email-protection" data-cfemail="c0b6a9a4a1b280b6a9a4a1b2a8afaca5ae">[email protected]</a> <script data-cfhash="f9e31" type="text/javascript">/* <![CDATA[ */!function(t,e,r,n,c,a,p){try{t=document.currentScript||function(){for(t=document.getElementsByTagName('script'),e=t.length;e--;)if(t[e].getAttribute('data-cfhash'))return t[e]}();if(t&&(c=t.previousSibling)){p=t.parentNode;if(a=c.getAttribute('data-cfemail')){for(e='',r='0x'+a.substr(0,2)|0,n=2;a.length-n;n+=2)e+='%'+('0'+('0x'+a.substr(n,2)^r).toString(16)).slice(-2);p.replaceChild(document.createTextNode(decodeURIComponent(e)),c)}p.removeChild(t)}}catch(u){}}()/* ]]> */ </script> ~/.ssh $ bc </span><span>bc 1.06.94 </span><span>Copyright 1991-1994, 1997, 1998, 2000, 2004, 2006 Free Software Foundation, Inc. </span><span>This is free software with ABSOLUTELY NO WARRANTY. </span><span>For details type `warranty'. </span><span> </span><span># Our powermod function: </span><span>define pmod(b,e,m) { if(e == 0 ) return 1; if(e == 1) return b%m; rest=pmod(b^2%m,e/2,m); if((e%2) == 1) return (b*rest)%m else return rest; } </span><span> </span><span> </span><span>#Define some variables (this time unabbreviated) </span><span>n=13158045936463264355006370413708684112837853704660293756254884673628/ </span><span>63292777770859554071108633728590995985653161363101078779505801640963/ </span><span>48597350763180843221886116453606059623113097963206649790257715468881/ </span><span>4303031148479239044926138311 </span><span> </span><span>e=35 </span><span> </span><span>d=10150492579557375359576342890575270601332058572166512326253768176799/ </span><span>23111571423234513140569517447770196903218153051479115016036905320557/ </span><span>80231250287900874055062921398102953416891810163858645414303785372309/ </span><span>5688315939617076008144563059 </span><span> </span><span> </span><span> </span><span># Encrypt the number 12345 </span><span>c=pmod(12345, e, n) </span><span> </span><span># Show the encrypted number </span><span>c </span><span>15928992191730477535088375321366468550579140816267293144554503305092/ </span><span>03492035891240033089011563910196180080894311697511846432462334632873/ </span><span>53515625 </span><span> </span><span> </span><span>#Decrypt the number </span><span>pmod(c, d, n) </span><span> </span><span>12345</span></code>
耶,我们已经成功的用真实的RSA参数来加密和解密数据!
所以,公钥里面放的是什么?
<code><span>ssh-rsa AAAAB3NzaC1yc2EAAAABIwAAAGEA2PeuXcWHOY6WhUJdd6xU+zVZr758gFfsEJnm3iWYyHfoobORCaeMMlyb472diGg/HoF/5r3LPQd/Nt8Dk6mpADIL+9FNUOAeEZdjocU/+XJZDwNHMKG2y4/p6r1FgefH <a href="/cdn-cgi/l/email-protection" data-cfemail="5f29363b3e2d1f29363b3e2d3730333a31712c2f3e32">[email protected]</a> <script data-cfhash="f9e31" type="text/javascript">/* <![CDATA[ */!function(t,e,r,n,c,a,p){try{t=document.currentScript||function(){for(t=document.getElementsByTagName('script'),e=t.length;e--;)if(t[e].getAttribute('data-cfhash'))return t[e]}();if(t&&(c=t.previousSibling)){p=t.parentNode;if(a=c.getAttribute('data-cfemail')){for(e='',r='0x'+a.substr(0,2)|0,n=2;a.length-n;n+=2)e+='%'+('0'+('0x'+a.substr(n,2)^r).toString(16)).slice(-2);p.replaceChild(document.createTextNode(decodeURIComponent(e)),c)}p.removeChild(t)}}catch(u){}}()/* ]]> */ </script></span></code>
这是一个非常简单的文件格式,我不知道有什么现成的工具可以解码它。不过只要通过base64解码中间的字符串,就会发现它是4个字节的长度,跟着对应长度的数据。尝试三次以后就能得到密钥的类型,e和n。
我的是00 00 00 07,跟着7个字节的”ssh-rsa”。然后是 00 00 00 01跟着一个字节的0x23(35, 我们的e)。最后是00 00 00 61跟着0x61=97字节的模数n。
如果你想手动的解码私钥,可以用base64解码中间的部分,你会得到一个 ASN.1 编码的一串数字。
这是一部分注解后的解码的16进制私钥:
30 82 01 ca - Sequence, 0x01CA bytes 02 01: Integer, 1 byte 00 02 61: - Integer, 0x61 bytes (n). 00 d8 f7 ae 5d c5 87 39 8e 96 ... Same as from openssl! 02 01: - Integer, 1 byte, 0x23=35 (e) 23 02 61 - Integer, 0x61 bytes (d) 00 a7 5f fb 8a 2a aa 25 16 39 ... ...
这里有个bash脚本 可以用来解码私钥,输出给bc用的变量定义和函数,你可以自己试着自己玩一下。它会解码ASN.1,在密钥设密码时需要使用openssl。
在执行的时候,把输出粘贴到到bc,你就能得到n, e, d, p, q和一些其它变量,encrypt(m)和decrypt(c)加上一个verify()函数,它会在key是合法的时候返回1,这些函数都很简单直接。
玩的开心!