从公钥正确创建RSACryptoServiceProvider

Dis*_*nky 9 c# rsa asn.1 rsacryptoserviceprovider pem

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

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

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);
Run Code Online (Sandbox Code Playgroud)

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

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

任何帮助表示赞赏.

bar*_*njs 25

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

公钥文件是DER编码的blob.

-----BEGIN PUBLIC KEY-----
MIGgMA0GCSqGSIb3DQEBAQUAA4GOADCBigKBggC8rLGlNJ17NaWArDs5mOsV6/kA
7LMpvx91cXoAshmcihjXkbWSt+xSvVry2w07Y18FlXU9/3unyYctv34yJt70SgfK
Vo0QF5ksK0G/5ew1cIJM8fSxWRn+1RP9pWIEryA0otCP8EwsyknRaPoD+i+jL8zT
SEwV8KLlRnx2/HYLVQkCAwEAAQ==
-----END PUBLIC KEY-----
Run Code Online (Sandbox Code Playgroud)

如果您获取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 
Run Code Online (Sandbox Code Playgroud)

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

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

30
Run Code Online (Sandbox Code Playgroud)

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

81 A0
Run Code Online (Sandbox Code Playgroud)

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

30 0D
Run Code Online (Sandbox Code Playgroud)

我们看到我们的CONSTRUCTED SEQUENCE again(0x30),其长度值为0x0D,所以我们有一个13字节的有效载荷.

06 09 2A 86 48 86 F7 0D 01 01 01 05 00 
Run Code Online (Sandbox Code Playgroud)

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

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

所以到目前为止我们有

SEQUENCE
  SEQUENCE
    OID 1.2.840.113549.1.1.1
    NULL
  143 more bytes.
Run Code Online (Sandbox Code Playgroud)

继续:

03 81 8E 00
Run Code Online (Sandbox Code Playgroud)

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

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

30 81 8A
Run Code Online (Sandbox Code Playgroud)

这是我们的朋友CONSTRUCTED SEQUENCE,0x8A有效负载字节,方便地对应"剩下的一切".

02 81 82
Run Code Online (Sandbox Code Playgroud)

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 
Run Code Online (Sandbox Code Playgroud)

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

02 03 01 00 01
Run Code Online (Sandbox Code Playgroud)

另一个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
Run Code Online (Sandbox Code Playgroud)

收获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
Run Code Online (Sandbox Code Playgroud)

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

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

RSAPublicKey ::= SEQUENCE {
  modulus INTEGER, -- n
  publicExponent INTEGER -- e }
Run Code Online (Sandbox Code Playgroud)

这就是说我们读的第一个INTEGER是模数值,第二个是(公共)指数.

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

虽然这并不像给你代码那么容易,但是在给定这些信息的情况下为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
}
Run Code Online (Sandbox Code Playgroud)

  • https://github.com/sevenTiny/SevenTiny.Bantina/blob/b5503b5597383ca6085ceb4aa5fa054918a4bd73/10-Code/SevenTiny.Bantina/Security/RSACommon.cs =>方法CreateRsaProviderFromPublicKey完成工作! (6认同)
  • 在这个答案之后的 4 年里,对此的支持也刚刚内置:“key.ImportSubjectPublicKeyInfo(derBytes, out int bytesRead)”。但答案仍然解释了该方法的作用。 (2认同)

Dis*_*nky 6

经过很多时间,搜索和bartonjs的出色响应之后,完成此操作的代码实际上直截了当,尽管对于不熟悉公钥结构的任何人来说,这有点不直观。

TL; DR基本上,如果您的公钥来自非.NET来源,则此答案将无济于事,因为.NET无法提供一种本地解析正确格式的PEM的方法。但是,如果生成PEM的代码是基于.NET的,则此答案描述了仅公开密钥的PEM的创建以及如何将其重新加载。

公钥PEM可以描述各种密钥类型,而不仅仅是RSA,而不仅仅是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;
Run Code Online (Sandbox Code Playgroud)

注意:上面的代码尚未投入生产!您需要围绕对象创建(例如,公钥可能不是RSA),强制转换为RSACryptoServiceProvider等等采取适当的防护措施。此处的代码示例简短说明了它可以合理地完成。

我是怎么得到的?我在ILSpy的Cryptographic名称空间中摸索着,发现AsnEncodedData它与bartonjs的描述息息相关。做更多的研究,我偶然发现了这篇文章(看起来很熟悉?)。这是在尝试具体确定密钥大小,但在此过程中会创建必要的密钥RSACryptoServiceProvider

我将bartonjs的答案留为“接受”,这是正确的。上面的代码是该研究的结果,我将其留在这里,以便其他希望做到这一点的人可以干净地做到这一点,而无需像我在OP中那样进行任何数组复制黑客。

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

更新

通过.NET路线图,可以通过针对内核> 2.1.0的ASN.1解析使此操作变得更加容易

更新2

现在,Core .NET 2.1.1中有一个私有实现。MS一直在苦苦挣扎,直到满意为止,我们(希望)在后续版本中看到公共API。

更新3

我在这里通过一个问题发现,以上信息不完整。缺少的是,与此解决方案一起加载的公钥是从加载的公钥/私钥对中以编程方式生成的密钥。一旦RSACryptoServiceProvider从密钥对(不仅是公共密钥)创建了,就可以仅导出公共字节并将其编码为公共密钥PEM。这样做将与此处的解决方案兼容。这是什么啊

将公钥+私钥对加载到中RSACryptoServiceProvider,然后像这样导出它;

var cert = new X509Certificate2(keypairBytes, password,
                                X509KeyStorageFlags.Exportable 
                                | X509KeyStorageFlags.MachineKeySet);
var partialAsnBlockWithPublicKey = cert.GetPublicKey();

// export bytes to PEM format
var base64Encoded = Convert.ToBase64String(partialAsnBlockWithPublicKey, Base64FormattingOptions.InsertLineBreaks);
var pemHeader = "-----BEGIN PUBLIC KEY-----";
var pemFooter = "-----END PUBLIC KEY-----";
var pemFull = string.Format("{0}\r\n{1}\r\n{2}", pemHeader, base64Encoded, pemFooter);
Run Code Online (Sandbox Code Playgroud)

如果使用此密钥创建PEM,则可以使用前面介绍的方法将其重新加载。为什么有什么不同?对cert.GetPublicKey()的调用实际上将返回ASN.1块结构。

SEQUENCE(2 elem)
  INTEGER (2048 bit)
  INTEGER 65537
Run Code Online (Sandbox Code Playgroud)

这实际上是一个不完整的DER斑点,但.NET可以解码(它在编写本文时,.NET不支持完整的ASN.1解析和生成-https: //github.com/dotnet/designs/issues/11)。

正确的DER(ASN.1)编码的公共密钥字节具有以下结构;

SEQUENCE(2 elem)
  SEQUENCE(2 elem)
     OBJECT IDENTIFIER   "1.2.840.113549.1.1.1" - rsaEncryption(PKCS #1)
     NULL
BIT STRING(1 elem)
  SEQUENCE(2 elem)
    INTEGER (2048 bit)
    INTEGER 65537
Run Code Online (Sandbox Code Playgroud)

好的,因此上面的代码为您提供了可以加载的公共密钥(种类)。它很丑陋,技术上也不完整,但是确实使用.NET自己的RSACryptoServiceProvider.GetPublicCert()方法输出。稍后加载公共密钥时,构造函数可以使用这些相同的字节。不幸的是,它不是真正的,完整的PEM。我们仍在等待.NET Core 3.0>中MS的ASN.1解析器。