文章目录
前言
前段时间遇到个项目问题,涉及RSA加密,研究了几天,终于解决了,情况是这样的,项目使用的是.net framework
的框架,需要对网络传输的个人数据进行加密,首先能从服务器直接获取一个加密用的公钥,使用这个公钥对本地个人数据加密后,再进行传输。
出现的问题是完成加密后的数据在服务器端始终无法解析,折腾了好一段时间才知道是由于服务器端的Java和客户端的C#两者使用的RSA算法的密钥格式不一致,而.net framework
没有提供对于相应格式的密钥的转换支持,C#对RSA相关密钥格式的支持在.net core
之后才完善。
问题找到了,解决方案两条思路,第一找开源库,研究了一下RSA,在github上看了一圈,感觉开源库功能过于全面而复杂,引入项目增加的代码量太大,又看一下解析规则,感觉并不是太复杂,于是尝试第二条路自己解析,从头记录一下解决方案:
1 算法简介
RSA算法是一种广泛使用的非对称加密算法,是目前最优秀的公钥方案之一。RSA算法基于一个数学理论支持,即获取两个大素数相乘的结果很简单,而对它们的乘积进行因式分解却极其困难。
对称加密:同一个密钥可以同时用作加密和解密,称为对称加密。
非对称加密:非对称加密算法需要两个密钥分别进行加密和解密,即公钥和私钥。
1.1 算法步骤
- p、q(两个大素数): 取两个足够大的素数p、q
- N(合数): 令N = p * q
- L: L = (p-1) * (q-1)
- E(公有幂): 使得E与L互质,且1 < E < L
- D(私有幂): 使得(D*E)% L = 1,且1 < D < L
素数:除了1和它自身外,不能被其他自然数整除的大于1的自然数。
合数:除了素数以外的大于1的自然数。
互质:公约数只有1的两个整数,叫做互质整数。
其中p和q是两个很大的素数,N是他们的乘积,用于之后求模,它是个合数,即合数模,E是公钥中的幂指数,D是私钥中的幂指数。
得到的(E,N)即为公钥,(D,N)即为私钥。
公钥与私钥是一对,如果用公钥对数据进行加密,只有用对应的私钥才能解密;如果用私钥对数据进行加密,那么只有用对应的公钥才能解密。
公钥加密——私钥解密
加密过程:密文=(明文^E)mod N
解密过程:明文=(密文^D)mod N
也可以反过来,私钥加密——公钥解密
加密过程:密文=(明文^D)mod N
解密过程:明文=(密文^E)mod N
由于公钥和私钥都是数的组合,我们一般把合数模N的二进制位数长度称作密钥的长度。一般密钥会要求一定的长度,RSA从提出到现在已近二十年,经历了各种攻击的考验,目前被破解的最长RSA密钥是768个二进制位,因此可以认为,768位的密钥不用担心受到除了国家安全管理(NSA)外的其他事物的危害,1024位的密钥几乎已经是安全的。
加、解密的过程是个模指数运算过程。
1.2 算法示例
取p = 3
、q = 11
计算N:N = p * q = 3 * 11 = 33
计算L:L = (p - 1) * (q - 1) = 20
取E = 3
,可满足E与L互质,且1 < E < L
取D = 7
,可满足(D * E) % L = 1
,即(D * 3) % 20 = 1
,且1 < D < L
得到(3, 33)为公钥,(7, 33)为私钥
该例中的N为33,即二进制的10 0001,即例子中密钥的长度为5
例子里的数取得很小以便于理解。
给定要加密的明文“7”,演示加密和解密的过程:
进行“公钥加密、私钥解密”
加密过程:密文 = (明文^E)mod N = (7 ^ 3) % 33 = 343 % 33 = 13
,得到加密后的密文为“13”
解密过程:明文 = (密文^D)mod N = (13 ^ 7) % 33 = 62748517 % 33 = 7
,解回明文“7”
反过来,进行“私钥加密,公钥解密”
加密过程:密文 = (明文^D)mod N = (7 ^ 7) % 33 = 823543 % 33 = 28
,得到加密后的密文为“28”
解密过程:明文 = (密文^E)mod N = (28 ^ 3) % 33 = 21952 % 33 = 7
,解回明文“7”
例子中的明文为数字,加密算法可以用于各种数据信息(数字、字母、符号、图片、音频、视频),现实中的所有数据在计算机中都是使用二进制的“0”、“1”组合的形式表示的,再通过解码可以转换成能够理解的数据信息格式。
2 密钥格式
RSA算法的密钥具有多种不同的描述格式,例如PEM格式、ASN格式、XML格式、DER格式等,使用不同的密钥格式通过对应的规则都可以用来表示密钥对象,解析出各个字段的内容。
2.1 PEM格式
PEM格式又具有多种填充方式从“PKCS#1”一直到“PKCS#15”,常用的是“PKCS#1”和“PKCS#8”,它们的文件头也可能存在区别。
公钥
-----BEGIN PUBLIC KEY-----
MFwwDQYJKoZIhvcNAQEBBQADSwAwSAJBALDTR8eu33xA4ru/JPbEsIapTTF1SzEi
IjAbntCXdEK6xdwsuomv7kL7rVQXef2rzhvzBSyZxuRbf33u6fi4jW8CAwEAAQ==
-----END PUBLIC KEY-----
私钥
-----BEGIN PRIVATE KEY-----
MIIBVQIBADANBgkqhkiG9w0BAQEFAASCAT8wggE7AgEAAkEAsNNHx67ffEDiu78k
9sSwhqlNMXVLMSIiMBue0Jd0QrrF3Cy6ia/uQvutVBd5/avOG/MFLJnG5Ft/fe7p
+LiNbwIDAQABAkEAmKtS5k1OF/HN0VwPhh/8acfzJiinaxyVeAPg8yhQ8OryQxG2
CnqTgG4V2PAMvxX42W+ZqA0zTFXx4EtWmq8FQQIhANjpAY7W6TAidjy2qlmfuSl4
DoY75bKJRsg2GVVYDDyTAiEA0LD88irK80hKj2JeAgEP0NXyYV8QZSuEM5Qk0G3U
0TUCIFpNhwyEhEg50KeuFHWDfX66MLHJtfMCG6m2fA1/vnhpAiEAowF7sdRHDdvr
kS+uajZWGjLizbepYLyq2HbggoUnc/kCICj08MHdsE2excF0rtNi457J57ZhnTsj
9uDBvPY+9JTT
-----END PRIVATE KEY-----
2.2 ASN格式
ASN格式是一串纯Base64字符串,可以看成去掉了PEM格式的头尾描述后的形式。
公钥
MFwwDQYJKoZIhvcNAQEBBQADSwAwSAJBALDTR8eu33xA4ru/JPbEsIapTTF1SzEi
IjAbntCXdEK6xdwsuomv7kL7rVQXef2rzhvzBSyZxuRbf33u6fi4jW8CAwEAAQ==
私钥
MIIBVQIBADANBgkqhkiG9w0BAQEFAASCAT8wggE7AgEAAkEAsNNHx67ffEDiu78k
9sSwhqlNMXVLMSIiMBue0Jd0QrrF3Cy6ia/uQvutVBd5/avOG/MFLJnG5Ft/fe7p
+LiNbwIDAQABAkEAmKtS5k1OF/HN0VwPhh/8acfzJiinaxyVeAPg8yhQ8OryQxG2
CnqTgG4V2PAMvxX42W+ZqA0zTFXx4EtWmq8FQQIhANjpAY7W6TAidjy2qlmfuSl4
DoY75bKJRsg2GVVYDDyTAiEA0LD88irK80hKj2JeAgEP0NXyYV8QZSuEM5Qk0G3U
0TUCIFpNhwyEhEg50KeuFHWDfX66MLHJtfMCG6m2fA1/vnhpAiEAowF7sdRHDdvr
kS+uajZWGjLizbepYLyq2HbggoUnc/kCICj08MHdsE2excF0rtNi457J57ZhnTsj
9uDBvPY+9JTT
2.3 XML格式
XML格式密钥是C#语言下的默认格式,使用XML描述语言将密钥对象的各个字段表示出来。
公钥
<RSAKeyValue>
<Modulus>
riLSQFVDC229P5F+Mkicbkpg5OC8+SeL6hvkJXIGYiN/e4YnprCxuIp5sH9AwWup4WJmObPKd1jOVGm07UwgVU7CDtTaVe1Uuk78yJBwgRuSteQjHYMmH6nG5YHvvONuvkmLnyIKGygJBL+4+Qmd3GaCHRtIrdfShlH3UbPINlM=
</Modulus>
<Exponent>
AQAB
</Exponent>
</RSAKeyValue>
私钥
<RSAKeyValue>
<Modulus>
riLSQFVDC229P5F+Mkicbkpg5OC8+SeL6hvkJXIGYiN/e4YnprCxuIp5sH9AwWup4WJmObPKd1jOVGm07UwgVU7CDtTaVe1Uuk78yJBwgRuSteQjHYMmH6nG5YHvvONuvkmLnyIKGygJBL+4+Qmd3GaCHRtIrdfShlH3UbPINlM=
</Modulus>
<Exponent>
AQAB
</Exponent>
<P>
6yCboYtKzIezMOFzGzzW8dp7SBT8f7jTRzH1ZIKQYKF0Mq/39k80SeUvY578O031+bg6i3cbNvvAhL8XjqTtmQ==
</P>
<Q>
vZgnL5LHnNE5uUW5NBwYvZbIz6hWNzc6kyDGimI8WBBFJOI06IdYGL2VMeGVs4lt5a1tM7T3c6gzBKgDQpL+yw==
</Q>
<DP>
p5tV9YDyr/unq5d6Uxc6bar9qHN1TqJ00VJ2h9BelNNinmM70fPB5U8fSddiG/BGAF3oNdSQrNAm+zmw1DkTOQ==
</DP>
<DQ>
fxS1b1XbJmm3X1A0y5DppGqlP0t+PpRuVp/pdGhUOlLthcN540KU8kBg+IZUaXr8hq6wO7BZDNT5HW3ggYc18Q==
</DQ>
<InverseQ>
q29etXnlszOH0FlQWDL9yLfJ+EruH4VURY1mZGz/+/qvPewUwyEf+EqJkZHVXEijnSa1CiFELK2YE9PhkUp2Xw==
</InverseQ>
<D>
bZUoLqf5KwYCJDDQ85/SIW3ZD++FvF1wpQCsUAwzjCq+nONNrI5hKLqr3bAW9iFkpJshrYpBDV3rah+jZfmUFk/UZeur2+kA2r5r1or34+HiIhT4sehU1lxww4DvTzf1/1ivG4LCvUPoFtT67Zdh8pNEC27N6bFDL8fbSU7GcmE=
</D>
</RSAKeyValue>
2.4 DER格式
DER格式是一种十六进制的密钥格式
公钥
30818902818100C19CFA5EA25F99C482499E3A557C7D0C3ABB375B19900CF4956E39F5B1EACA46A37CEE30DDBEA72979B6DABA11D4B9BD51CB1D79ED667607E65CF53491EE6BE35155072D5EFB96BA0E0FC0B9C1DFEDFA30886F645218CC680E55A7D5568AD59283E9BB3DC82970F6B3F6DD83FB308E2C610F362C71977D428614ED5FB59EFDB10203010001
私钥
3082025D020100028181009951BFB322876658587C207F2AFC2F2638DFD4EEEE925669C4A9487A3774891AEBE638940318B1AE270784FCFBE768C8C989E33B3953D820326DFAC7862AA133F96EE216C1B3F5651D45194CA02E9926E8FF133B2C03BB22BFE8C60B13D4757F263D4A792B188A8183FAB53B193C8AF8AB8EAB7020547C20D5BC90E1B369DBA902030100010281800FD9829ECB28022D89E0331FD25AC5A906E224CA1A81A84B40D85B34BF3CDDDB999D7025E4F80D8E3A5CADA3D58AC3AB56225A0A4A4FDF9CDC79C01E16419BEE71997546C68E6408E5D9C044858DCB6393F285D3EBAAD0C75298F61B33B752EB8E1ABA4D66E5380FF52DE07929A96367673CDAE5945A0C13F3503FABEDB1758D024100C52F9745C13B77968D52F29ACDEE00F2DF07AF8025048348B054B7EFF460097CAB824212447F674B55CF74E489DD399E702D3D655C74484248E05CA2E9DCCEE7024100C70CA9EA361ED73C42627254F33A3FA81AD0AADD64D45A1E536E64C7E31737B7ED3FCC20E03A082673C6E7A7270640F6132AA295FF406D6133090E7D89397EEF024100883505936395065872AAB77683854216824536CF97C2744543B8618E5909F5C3AE5D3DF28C6A4D19D6DE84EA50E905A211EECE18343306AEF2D43869388E1445024100B061FE6776D1D974A276CE4D8CC2FF099DC96EBF84CBCF97B3E2CD177B9A655B6CB6EDD1EC20407CA2778D6B475F794D152AE0ABFE663F06B4CCBFB46A5732AD02404981E8728E85719A319C9A8C4C7D3B162BFA728AD5FD054C0A45A9A625385167F0822D2398680FD75BF29A3C20A4D72D2115ABD06F27B3819214AAC77284518A
3 密钥解析(ASN.1)
不管RSA算法的密钥格式形式如何,其最终目的是表示密钥的内容,RSA算法的密钥最终需要被解析成一个类似结构体的对象,其解析规则遵循ASN.1标准。
ASN.1抽象语法标记(Abstract Syntax Notation One),描述了一种对数据进行表示、编码、传输和解码的数据格式。提供了一整套正规的格式用于描述对象的结构。数字1被加在ASN的后边,是为了保持ASN的开放性,可以让以后功能更加强大的ASN被命名为ASN.2等,但至今也没有出现。
ASN.1中关联了多个标准化编码规则,基本编码规则(BER)、规范编码规则(CER)、识别名编码规则(DER)、压缩编码规则(PER)和 XML编码规则(XER)。其中BER、CER、DER、PER都是二进制编码规则,BER(BasicEncoding Rules)是ASN.1中最早定义的编码规则,其他编码规则是在BER的基础上添加新的规则而构成的。
3.1 BER编码规则
传输语法的格式为TLV三元组<Tag, Length, Value>
,其中Value字段又能继续包含新的TLV三元组,即可以产生嵌套<T, L, <T, L, V>>
。
密钥的各种格式最终都会被转换成二进制,基于八位组的大字节序(Big-Endian)编码方式,高八位在左,低八位在右来解析。例如DER格式的十六进制密钥“30819f300d0609…………
”的转换如下:
十六进制密钥 | 二进制高八位 | 二进制低八位 |
---|---|---|
30 | 0011 | 0000 |
81 | 1000 | 0001 |
9F | 1001 | 1111 |
30 | 0011 | 0000 |
0D | 0000 | 1101 |
06 | 0000 | 0110 |
09 | 0000 | 1001 |
跨语言加密解密的时候需要考虑各个语言的字节序编码方式(Big-Endian与Little-Endian),C/C++语言的字节序跟编译平台的CPU相关,主流的Intel-x86架构采用小字节序,Java语言采用大字节序,用于网络传输的网络字节序是大字节序,多端互操作的情况下,需要考虑大小字节序的转换问题,额外处理。
3.1.1 Tag字段
包含一个或若干个八位组(即字节),以十六进制“30”为例,二进制表示为“0011 0000”,各个位的含义如下:
第7位 | 第6位 | 第5位 | 第4位 | 第3位 | 第2位 | 第1位 | 第0位 |
---|---|---|---|---|---|---|---|
0 | 0 | 1 | 1 | 0 | 0 | 0 | 0 |
标签类型 | 标签类型 | 编码方式 | 值类型 | 值类型 | 值类型 | 值类型 | 值类型 |
第7、6位指明标签分类:
值 | 标签分类 |
---|---|
00 | universal 通用标签 |
01 | application 应用标签 |
10 | context-specific 上下文专用标签 |
11 | private 私有标签 |
3.1.1.1 通用标签
当Tag分类为”universal 通用标签“时,第5位指明标签编码方式:
值 | 编码方式 |
---|---|
0 | primitive 原始类型 |
1 | constructed 构造类型 |
当Tag分类为”universal 通用标签“时,第4~0位:指明标签值的类型:
值(二进制) | 值(十进制) | 值类型 |
---|---|---|
00000 | 0 | 保留 |
00001 | 1 | Boolean 布尔类型 |
00010 | 2 | Integer 整型 |
00011 | 3 | Bit String 位串 |
00100 | 4 | Octet String 字节串(八位位组串) |
00101 | 5 | NULL 空值 |
00110 | 6 | Object Identifier 对象标识符 |
00111 | 7 | Object Description |
01000 | 8 | External,Instance of |
01001 | 9 | Real 实数 |
01010 | 10 | Enumerated 枚举类型 |
01011 | 11 | Embedded PDV |
01100 | 12 | UTF8 String |
01101 | 13 | Relative-oid |
01110 | 14 | 保留 |
01111 | 15 | 保留 |
10000 | 16 | Sequence 序列,Sequence of 单类型序列 |
10001 | 17 | Set集合,Set of 单类型集合 |
10010 | 18 | Numeric String |
10011 | 19 | Printable String |
10100 | 20 | Teletex String,T61 String |
10101 | 21 | Videotex String |
10110 | 22 | IA5 String |
10111 | 23 | UTC Time |
11000 | 24 | Generalized Time |
11001 | 25 | Graphic String |
11010 | 26 | Visible String,ISO646 String |
11011 | 27 | General String |
11100 | 28 | Universal String |
11101 | 29 | Character String |
11110 | 30 | BMP String |
11111 | 31 | 保留 |
3.1.1.2 非通用标签
当Tag分类不为”universal 通用标签“,而是另外三种时,在后续的多个八位组中编码,第一个八位组后五位固定全部为1,其余的八位组最高位为1表示后续还有,为0表示Tag结束。
第一个八位组 | 第二个八位组 | 第二个八位组 | …… | 第n个八位组 |
---|---|---|---|---|
01 11 1111 | 1xxx xxxx | 1xxx xxxx | 1… … | 0xxx xxxx |
高位01表示“application 应用标签” 后五位固定全为1 | 高位为1,表示还在编码 | 高位为1,表示还在编码 | 高位为1,表示还在编码 | 高位为0,表示结束 |
3.1.2 Length字段
Length字段的组织方式有两大类:定长方式和不定长方式,第一个八位组不为0x80表示定长方式,为0x80表示不定长方式:
3.1.2.1 定长方式
定长方式中,按长度是否超过一个八位,又分为短形式、长形式,最高位为”0“表示短形式,最高位为”1“表示长形式:
短形式
各个位的含义如下:
最高位 | 后7位 |
---|---|
0 | xxx xxxx |
0表示短形式 | 表示TLV三元组中的Value字段占用的字节数(范围0~127) |
长形式
各个位的含义如下:
第一个八位组 | …… |
---|---|
1xxx xxxx | …… |
最高位”1“表示长形式 后7位表示需要继续编码的长度(字节数) | 按照第一个八位提供的长度值,继续编码,其数值表示TLV三元组中的Value字段占用的字节数(范围0~256^126-1) |
3.1.2.2 不定长方式
Length所在八位组固定编码为0x80,但在Value编码结束后以两个0x00结尾。
第一个八位组 | …… | 第n-1个八位组 | 第n个八位组 |
---|---|---|---|
1000 0000 | …… | 0000 0000 | 0000 0000 |
0x80表示不定长方式 | 不为0x00,表示还在编码,其数值表示TLV三元组中的Value字段占用的字节数 | 遇到第一个0x00 | 遇到两个连续的0x00表示结束 |
3.1.3 Value字段
该字段内可能包含基础数据,也可能包含嵌套的TLV三元组,需要具体解析。
3.2 公钥解析示例
按照上述规则,解析一个密钥实例,以公钥为例(DER格式)解析方式如下:
30819f300d06092a864886f70d010101050003818d0030818902818100890f1a96bb296740990674217c96afe8bbbc63ba69123a55c87f03afe36f106522c2935a650a6bfd929a575941396d888424e4ee702e33f5ea2275d4d9e8c80c6c503a07c1f471f501e89abd4c6fd169b4c32460e1fd35ff2bbdb3febaa4c28a5b549b20017caea2652761b2a7edb22cb765921e18f1fe9315a8ade66625d11d0203010001
按照ASN.1规范的BER编码规则对每个字节进行解析如下:
字节序号 | 密钥原文 | 对应的二进制 | TLV组 (“····”缩进) | 语义 |
---|---|---|---|---|
0 | 30 | 0011 0000 | T | Tag值 Universal类型 结构体——类型SEQUENCE |
1 | 81 | 1000 0001 | L | Length值 定长 长类型——长度1个字节 |
2 | 9F | 1001 1111 | ——实际长度159个字节(0x9F转十进制) | |
V{ | Value值{ | |||
3 | 30 | 0011 0000 | ····T | Tag值 Universal类型 结构体 类型SEQUENCE |
4 | 0D | 0000 1101 | ····L | Length值 定长 短类型——实际长度13个字节 |
····V{ | Value值{ | |||
5 | 06 | 0000 0110 | ········T | Tag值 Universal类型 原数据——类型OBJECT IDENTIFIER |
6 | 09 | 0000 1001 | ········L | Length值 定长 短类型——实际长度9个字节 |
········V{ | Value值{ | |||
7~11 | 2A 86 48 86 F7 | ············ | ||
12~15 | 0D 01 01 01 | ············ | ||
········} | } | |||
16 | 05 | 0000 0101 | ········T | Tag值 Universal类型 原数据——类型NULL |
17 | 00 | 0000 0000 | ········T | Tag值 Universal类型 原数据——类型BER保留 |
····} | } | |||
18 | 03 | 0000 0011 | ····T | Tag值 Universal类型 原数据——类型BIT STRING |
19 | 81 | 1000 0001 | ····L | Length值 定长 长类型——长度1个字节 |
20 | 8D | 1000 1101 | ····· | ——实际长度141个字节 |
····V{ | Value值{ | |||
21 | 00 | ········ | unused bit | |
22 | 30 | ········T | Tag值 Universal类型 结构体 类型SEQUENCE | |
23 | 81 | 1000 0001 | ········L | Length值 定长 长类型——长度1个字节 |
24 | 89 | 1000 1001 | ········ | ——实际长度137个字节 |
········V{ | Value值{ | |||
25 | 02 | 0000 0010 | ············T | Tag值 Universal类型 原数据——类型INTEGER |
26 | 81 | 1000 0001 | ············L | Length值 定长 长类型——长度1个字节 |
27 | 81 | 1000 0001 | ············ | ——实际长度129个字节 |
············V{ | Value值{ | |||
28 | 00 | ················ | 首字节的MSB=1时,补0 | |
29~156 | 89 0F 1A 96 BB | ················ | ||
29 67 40 99 06 | ················ | |||
74 21 7C 96 AF | ················ | |||
E8 BB BC 63 BA | ················ | |||
69 12 3A 55 C8 | ················ | |||
7F 03 AF E3 6F | ················ | |||
10 65 22 C2 93 | ················ | |||
5A 65 0A 6B FD | ················ | |||
92 9A 57 59 41 | ················ | |||
39 6D 88 84 24 | ················ | 128字节的公钥值modulus | ||
E4 EE 70 2E 33 | ················ | |||
F5 EA 22 75 D4 | ················ | |||
D9 E8 C8 0C 6C | ················ | |||
50 3A 07 C1 F4 | ················ | |||
71 F5 01 E8 9A | ················ | |||
BD 4C 6F D1 69 | ················ | |||
B4 C3 24 60 E1 | ················ | |||
FD 35 FF 2B BD | ················ | |||
B3 FE BA A4 C2 | ················ | |||
8A 5B 54 9B 20 | ················ | |||
01 7C AE A2 65 | ················ | |||
27 61 B2 A7 ED | ················ | |||
B2 2C B7 65 92 | ················ | |||
1E 18 F1 FE 93 | ················ | |||
15 A8 AD E6 66 | ················ | |||
25 D1 1D | ················ | |||
············} | } | |||
157 | 02 | 0000 0010 | ············T | Tag值 Universal类型 原数据——类型INTEGER |
158 | 03 | 0000 0011 | ············L | Length值 定长 短类型——实际长度3个字节 |
············V{ | Value值{ | |||
159 | 01 | ················ | ||
160 | 00 | ················ | 3字节的公有幂值exponent | |
161 | 01 | ················ | ||
············} | } | |||
········} | } | |||
····} | } | |||
} | } |
解析完的公钥对象的结构形式如下:
SEQUENCE{ // 159个字节
SEQUENCE{ // 13个字节
OBJECT IDENTIFIER{ // 9个字节
}
}
BIT STRING{ // 141个字节
SEQUENCE{ // 137个字节
INTEGER{ // 129个字节
补0 // 1个字节
modulus // 128个字节,模数,即N
}
INTEGER{
exponent // 3个字节,公有幂值,即E
}
}
}
}
至此我们已经从DER密钥格式的十六进制串中解析出了模数modulus和公有幂exponent,即公钥内容。
解出了公钥的内容,要向其它几种密钥格式转换,只需要按照对应的格式规则进行处理即可,很多语言都有现呈的类可以进行构造(C#中提供RSACryptoServiceProvider类)。例子中是公钥的解析,在RSA算法中,私钥对象的结构要比公钥多几个字段,但解析方式是一样的,同样遵循ASN.1规范。
4 源代码
最后贴上客户端的C#源码,从DER格式的公钥中解出模数和公有幂:
/**
* 从DER格式的公钥字符串解析出modulus和exponent
*/
internal static KeyValuePair<byte[], byte[]> DecodeHexPublicKey(string hexPublicKey)
{
try
{
byte[] keyBytes = HexStringToByte(hexPublicKey); // 字符串转成十六进制字节数组
if (keyBytes[0] != 0x30) // 第一个字节的文件头必须解析成SEQUENCE结构
{
throw new Exception("head of file cannot decode to SEQUENCE");
}
// 解析主结构体(SEQUENCE结构体)
int mainSequenceLength = 0;
int currentIndex = 1; // 从第二个字节开始解析
mainSequenceLength = DecodeTLV_Length(keyBytes, ref currentIndex); // 获取主SEQUENCE结构体的长度
// 解析inner(SEQUENCE结构体)
int innerSequenceLength = 15; // inner的固定长度15
currentIndex += innerSequenceLength; // 跳过即可
// 解析bitString(Bit String类型)
currentIndex++; // 下标跳过Bit String数据的Tag字段,移向Length字段
int bitStringLength = DecodeTLV_Length(keyBytes, ref currentIndex); // 获取Bit String数据的长度
currentIndex++; // Bit String的Value字段带一个unused bit,跳过
// 解析参数结构体(SEQUENCE结构体)
currentIndex++; // 下标跳过参数结构体的Tag字段,移向Length字段
int paramSequenceLength = DecodeTLV_Length(keyBytes, ref currentIndex); // 获取参数SEQUENCE结构体的长度
// 解析modulus模数N
byte[] modulus = DecodeInteger(keyBytes, ref currentIndex);
// 解析exponent公开幂E
byte[] exponent = DecodeInteger(keyBytes, ref currentIndex);
// 公钥(E,N)
KeyValuePair<byte[], byte[]> result = new KeyValuePair<byte[], byte[]>(modulus, exponent);
return result;
}
catch (Exception ex)
{
Console.WriteLine(ex.Message);
return new KeyValuePair<byte[], byte[]>();
}
}
/**
* 字符串以十六进制的规则转为字节数组
*/
private static byte[] HexStringToByte(string hexString)
{
hexString = hexString.Replace(" ", "");
if ((hexString.Length % 2) != 0)
{
hexString += " ";
}
byte[] returnBytes = new byte[hexString.Length / 2];
for (int i = 0; i < returnBytes.Length; i++)
{
returnBytes[i] = Convert.ToByte(hexString.Substring(i * 2, 2), 16);
}
return returnBytes;
}
/**
* 解码ASN.1规则中的INTEGER类型,返回解码后的数据,处理完成后currentIndex的位置会移到该类型数据末尾的下一个位置
* @param data 密钥字节数组(十六进制)
* @param currentIndex 表示当前需要解码的Integer的Tag字段的位置
*/
private static byte[] DecodeInteger(byte[] dataInteger, ref int currentIndex)
{
if (dataInteger[currentIndex] != 0x02)
{
throw new Exception("head of file cannot decode to INTEGER");
}
currentIndex += 1;
var len = DecodeTLV_Length(dataInteger, ref currentIndex);
int integerStart = currentIndex;
int integerEnd = currentIndex + len - 1;
// 跳过补零位,>0x7f时补0
while (dataInteger[currentIndex] == 0)
{
currentIndex++;
}
if (dataInteger[currentIndex] > 0x7f)
{
integerStart = currentIndex;
}
byte[] result = dataInteger.Where((item, index) => index >= integerStart & index <= integerEnd).ToArray();
currentIndex = integerEnd + 1; // 处理完成后currentIndex移到末尾的下一个位置
return result;
}
/**
* 解码TLV元组中的Length字节,返回实际长度,返回0表示错误,处理完成后currentIndex的位置会移到Length字段末尾的下一个位置
* @param data 密钥字节数组(十六进制)
* @param index 表示当前需要解码的TLV元组中的Length字段的位置
*/
private static int DecodeTLV_Length(byte[] data, ref int currentIndex)
{
try
{
int length = data[currentIndex]; // 获取当前字节,十进制表示
if (length == 0) // 解码不能为:0000 0000,抛出异常
{
throw new Exception("parameter length in TLV cannot be 0x00");
}
else if(length < 0x80) // 解码为:0xxx xxxx,定长式、短形式:最高位为0,后7位表示长度的实际值
{
++currentIndex; // 处理完成后currentIndex移到末尾的下一个位置
return length;
}
else if(length == 0x80) // 解码为:1000 0000,不定长式:以两个连续的0x00结尾
{
int result = 0;
int count = 0; // 连续遇到0x00的次数
while (count != 2)
{
++currentIndex; // 获取下一个字节
int temp = data[currentIndex];
if (temp == 0x00) // 遇到0x00
{
++count;
}
else
{
count = 0;
}
++result;
}
++currentIndex; // 处理完成后currentIndex移到末尾的下一个位置
return result;
}
else // 解码为:1xxx xxxx,定长式、长形式:最高位为1,后7位表示长度的实际值会占用接下的多少个字节数
{
length &= 0x7F; // 当前值和“0111 1111”进行按位与,提取出length后7位的1,后7位的十进制表示长度的实际值占用的字节数
int result = 0;
for (int i = length - 1; i >= 0; i--)
{
++currentIndex; // 获取下一个字节
int temp = data[currentIndex] << (i * 8); // 计算当前字节所在的位置表示的值,每多一个字节需要左移八位
result += temp;
}
++currentIndex; // 处理完成后currentIndex移到末尾的下一个位置
return result;
}
}
catch (Exception ex)
{
Console.WriteLine(ex.Message);
++currentIndex; // 处理完成后currentIndex移到末尾的下一个位置
return 0;
}
}
特别注意:如果场景涉及大字节序和小字节序(Big-Endian与Little-Endian)的转换,需要额外处理。
总结
RSA算法本身的原理并不复杂,其中的密钥解析依赖于密钥的格式规范,各种密钥格式的互转都可以先转换成原始的密钥内容结构体对象,再按照不同的密钥格式规范进行组装,期间可能会涉及到base64字符串的转换、不同进制的转换、大小字节序的转换等问题,本文提供了对十六进制DER格式公钥解析的具体解决方案。
附加资料:跨越千年的RSA算法