从公钥正确创建RSACryptoServiceProvider

时间:2017-01-23 13:59:08

标签: c# rsa pem asn.1 rsacryptoserviceprovider

我目前正在尝试仅从已解码的PEM文件创建RSACryptoServiceProvider对象。经过几天的搜索,我确实设法解决了一个有效的解决方案,但它不是一个可以生产就绪的解决方案。

简而言之,为了从组成PEM文件中的公钥的字节创建RSACryptoServiceProvider对象,我必须创建指定keysize的对象(当前使用SHA256,特别是2048),然后使用RSAParametersExponent集导入Modulus对象。我是这样做的;

byte[] publicKeyBytes = Convert.FromBase64String(deserializedPublicKey.Replace("-----BEGIN PUBLIC KEY-----", "")
                                                                      .Replace("-----END PUBLIC KEY-----", ""));

// extract the modulus and exponent based on the key data
byte[] exponentData = new byte[3];
byte[] modulusData = new byte[256];
Array.Copy(publicKeyBytes, publicKeyBytes.Length - exponentData.Length, exponentData, 0, exponentData.Length);
Array.Copy(publicKeyBytes, 9, modulusData, 0, modulusData.Length);


// import the public key data (base RSA - works)
RSACryptoServiceProvider rsa = new RSACryptoServiceProvider(dwKeySize: 2048);
RSAParameters rsaParam = rsa.ExportParameters(false);
rsaParam.Modulus = modulusData;
rsaParam.Exponent = exponentData;
rsa.ImportParameters(rsaParam);

虽然这有效,但假设deserializedPublicKey恰好是270个字节并且我需要的模数在位置9处找到并且总是长度为256个字节是不可行的。

如何在给定一组公钥字节的情况下更改此值以正确选择模数和指数字节?我试图理解ASN.1标准,但很少运气找到我需要的东西 - 标准有些拜占庭。

感谢任何帮助。

3 个答案:

答案 0 :(得分:19)

您不需要导出现有参数,然后重新导入它们。这会强制您的机器生成RSA密钥然后将其丢弃。因此,为构造函数指定密钥大小并不重要(如果您不使用它赢得的密钥,通常会产生一个......)

公钥文件是DER编码的blob。

-----BEGIN PUBLIC KEY-----
MIGgMA0GCSqGSIb3DQEBAQUAA4GOADCBigKBggC8rLGlNJ17NaWArDs5mOsV6/kA
7LMpvx91cXoAshmcihjXkbWSt+xSvVry2w07Y18FlXU9/3unyYctv34yJt70SgfK
Vo0QF5ksK0G/5ew1cIJM8fSxWRn+1RP9pWIEryA0otCP8EwsyknRaPoD+i+jL8zT
SEwV8KLlRnx2/HYLVQkCAwEAAQ==
-----END PUBLIC KEY-----

如果您取得PEM装甲内的内容,它就是Base64编码的字节数组。

30 81 A0 30 0D 06 09 2A 86 48 86 F7 0D 01 01 01 
05 00 03 81 8E 00 30 81 8A 02 81 82 00 BC AC B1 
A5 34 9D 7B 35 A5 80 AC 3B 39 98 EB 15 EB F9 00 
EC B3 29 BF 1F 75 71 7A 00 B2 19 9C 8A 18 D7 91 
B5 92 B7 EC 52 BD 5A F2 DB 0D 3B 63 5F 05 95 75 
3D FF 7B A7 C9 87 2D BF 7E 32 26 DE F4 4A 07 CA 
56 8D 10 17 99 2C 2B 41 BF E5 EC 35 70 82 4C F1 
F4 B1 59 19 FE D5 13 FD A5 62 04 AF 20 34 A2 D0 
8F F0 4C 2C CA 49 D1 68 FA 03 FA 2F A3 2F CC D3 
48 4C 15 F0 A2 E5 46 7C 76 FC 76 0B 55 09 02 03 
01 00 01 

ITU-T X.690定义了如何读取在基本编码规则(BER),规范编码规则(CER,我从未见过明确使用过的CER)和可分辨编码规则(DER)下编码的内容。在很大程度上,CER限制BER和DER限制CER,使DER最容易阅读。 (ITU-T X.680描述了抽象语法记法一(ASN.1),它是DER是二进制编码的语法)

我们现在可以做一些解析:

30

这标识了一个SEQUENCE(0x10),其中CONSTRUCTED位设置为(0x20),这意味着它包含其他DER / tagged值。 (SEQUENCE总是在DER中构建)

81 A0

下一部分是长度。由于它具有高位设置(> 0x7F),因此第一个字节是"长度长度"值。它表示真实长度在下一个1字节(lengthLength & 0x7F)中编码。因此,该SEQUENCE的内容总共为160个字节。 (在这种情况下,"其余的数据",但SEQUENCE可能包含在其他内容中)。所以,让我们阅读内容:

30 0D

我们再次看到我们的CONSTRUCTED SEQUENCE(0x30),其长度值为0x0D,因此我们有一个13字节的有效载荷。

06 09 2A 86 48 86 F7 0D 01 01 01 05 00 

06是OBJECT IDENTIFIER,具有0x09字节有效负载。 OID有一个稍微不直观的编码,但这个编码等同于文本表示1.2.840.113549.1.1.1,即id-rsaEncryptionhttp://www.oid-info.com/get/1.2.840.113549.1.1.1)。

这仍然留给我们两个字节(05 00),我们看到它是一个NULL(有0字节的有效载荷,因为,它是“空”)。

所以到目前为止我们已经

SEQUENCE
  SEQUENCE
    OID 1.2.840.113549.1.1.1
    NULL
  143 more bytes.

继续:

03 81 8E 00

03表示BIT STRING。 BIT STRING编码为[tag] [length] [未使用位数]。未使用的位基本上始终为零。所以这是一个比特序列,0x8E字节长,并且所有这些都被使用。

从技术上讲,我们应该停在那里,因为没有设置CONSTRUCTED。但是因为我们碰巧知道了这个结构的格式,所以我们将值视为CONSTRUCTED位的设置:

30 81 8A

这里是我们的朋友CONSTRUCTED SEQUENCE,0x8A有效负载字节,方便地对应于"剩下的所有内容"。

02 81 82

02标识一个INTEGER,这个有0x82个有效负载字节:

00 BC AC B1 A5 34 9D 7B 35 A5 80 AC 3B 39 98 EB 
15 EB F9 00 EC B3 29 BF 1F 75 71 7A 00 B2 19 9C 
8A 18 D7 91 B5 92 B7 EC 52 BD 5A F2 DB 0D 3B 63 
5F 05 95 75 3D FF 7B A7 C9 87 2D BF 7E 32 26 DE 
F4 4A 07 CA 56 8D 10 17 99 2C 2B 41 BF E5 EC 35 
70 82 4C F1 F4 B1 59 19 FE D5 13 FD A5 62 04 AF 
20 34 A2 D0 8F F0 4C 2C CA 49 D1 68 FA 03 FA 2F 
A3 2F CC D3 48 4C 15 F0 A2 E5 46 7C 76 FC 76 0B 
55 09 

前导0x00将违反DER,但下一个字节的高位设置除外。这意味着0x00用于保持符号位不被设置,这使其成为正数。

02 03 01 00 01

另一个INTEGER,3个字节,值01 00 01。我们已经完成了。

SEQUENCE
  SEQUENCE
    OID 1.2.840.113549.1.1.1
    NULL
  BIT STRING
    SEQUENCE
      INTEGER 00 BC AC ... 0B 55 09
      INTEGER 01 00 01

收获https://tools.ietf.org/html/rfc5280我们发现这看起来很像SubjectPublicKeyInfo结构:

SubjectPublicKeyInfo  ::=  SEQUENCE  {
  algorithm            AlgorithmIdentifier,
  subjectPublicKey     BIT STRING  }

AlgorithmIdentifier  ::=  SEQUENCE  {
  algorithm               OBJECT IDENTIFIER,
  parameters              ANY DEFINED BY algorithm OPTIONAL  }
                            -- contains a value of the type
                            -- registered for use with the
                            -- algorithm object identifier value

当然,它不知道RSA公钥格式是什么。但是oid-info网站告诉我们查看RFC 2313,我们在哪里看到

An RSA public key shall have ASN.1 type RSAPublicKey:

RSAPublicKey ::= SEQUENCE {
  modulus INTEGER, -- n
  publicExponent INTEGER -- e }

这就是说我们读的第一个INTEGER是Modulus值,第二个是(public)Exponent。

DER编码是big-endian,也是RSAParameters编码,但对于RSAParameters,您需要从模数中删除前导0x00值。

虽然这并不像给你代码那样容易,但是在给定这些信息的情况下为RSA密钥编写解析器应该相当简单。我建议您将其写为internal static RSAParameters ReadRsaPublicKey(...),然后您只需要执行

RSAParameters rsaParameters = ReadRsaPublicKey(...);

using (RSA rsa = RSA.Create())
{
    rsa.ImportParameters(rsaParameters);
    // things you want to do with the key go here
}

答案 1 :(得分:5)

经过很长一段时间,搜索和bartonjs未完成的回复,这样做的代码实际上是直截了当的,尽管对于不熟悉公众结构的人来说有点不直观键。

公钥PEM可以描述各种键类型,而不仅仅是RSA,而不是new RSACryptoServiceProvider(pemBytes),我们必须根据其结构/语法ASN.1解析PEM,然后告诉它如果它是RSA密钥(它可能是一系列其他密钥)。知道了;

const string rsaOid = "1.2.840.113549.1.1.1";   // found under System.Security.Cryptography.CngLightup.RsaOid but it's marked as private
Oid oid = new Oid(rsaOid);
AsnEncodedData keyValue = new AsnEncodedData(publicKeyBytes);           // see question
AsnEncodedData keyParam = new AsnEncodedData(new byte[] { 05, 00 });    // ASN.1 code for NULL
PublicKey pubKeyRdr = new PublicKey(oid, keyParam, keyValue);
var rsaCryptoServiceProvider = (RSACryptoServiceProvider)pubKeyRdr.Key;

注意:以上代码生产就绪!您需要在对象创建周围放置适当的防护(例如,公钥可能不是RSA),转换为RSACryptoServiceProvider等。这里的代码示例很简单,说明它可以合理地完成干净。

我是怎么得到的?通过ILSpy中的Cryptographic命名空间,我注意到AsnEncodedDatabartonjs的描述敲响了铃声。做了更多的研究,我发现this帖子(看起来很熟悉?)。这是试图确定具体的密钥大小,但它会在此过程中创建必要的RSACryptoServiceProvider

我将bartonjs的回答留作接受,这是正确的。上面的代码就是这项研究的结果,我将其留在这里,以便其他想要做同样事情的人可以干净利落地完成,而不会像我在OP中那样进行任何阵列复制。

此外,出于解码和测试目的,您可以使用ASN.1解码器here检查您的公钥是否可解析。

<强>更新

在.NET路线图中,使用easier为核心&gt; 2.1.0制作此ASN.1 parsing

更新2

现在Core .NET 2.1.1中有一个私有实现。 MS正在进行dogfooding直到满意为止,并且我们(希望)会在后续版本中看到公共API。

答案 2 :(得分:1)

PEM文件只是一系列base64编码的DER文件,.net允许直接导入DER文件,所以你可以这样做(我假设你只是使用公钥,因为你声明你使用它只):

byte[] certBytes = Convert.FromBase64String(deserializedPublicKey
    .Replace("-----BEGIN PUBLIC KEY-----", "")
    .Replace("-----END PUBLIC KEY-----", ""));

X509Certificate2 cert =  new X509Certificate2(certBytes);
RSACryptoServiceProvider publicKeyProvider = 
(RSACryptoServiceProvider)cert.PublicKey.Key;