从公钥正确创建 RSACryptoServiceProvider

Posted

技术标签:

【中文标题】从公钥正确创建 RSACryptoServiceProvider【英文标题】:Correctly create RSACryptoServiceProvider from public key 【发布时间】:2017-06-08 01:34:47 【问题描述】:

我目前正在尝试仅从解码的 PEM 文件创建 RSACryptoServiceProvider 对象。经过几天的搜索,我确实设法找到了一个可行的解决方案,但它还不能用于生产。

简而言之,为了从构成 PEM 文件中公钥的字节创建 RSACryptoServiceProvider 对象,我必须创建指定密钥大小的对象(具体而言,当前为 2048 使用 SHA256),然后导入带有ExponentModulus 集的RSAParameters 对象。我就是这样做的;

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 标准,但没有找到我需要的东西——这些标准有点拜占庭式。

感谢任何帮助。

【问题讨论】:

【参考方案1】:

经过大量时间搜索和bartonjs 的出色响应,执行此操作的代码最终实际上是直截了当的,尽管对于不熟悉公钥结构的人来说有点不直观。

TL;DR 基本上,如果您的公钥来自非 .NET 源,则此答案将无济于事,因为 .NET 不提供本地解析正确格式的方法聚乙烯醇。 然而,如果生成 PEM 的代码是基于 .NET 的,那么这个答案描述了仅公钥 PEM 的创建以及如何重新加载它。

公钥 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 命名空间向下探索,我注意到 AsnEncodedData 敲响了带有 bartonjs 描述的铃声。做更多的研究,我偶然发现了this 的帖子(看起来很熟悉?)。这是试图专门确定密钥大小,但它会一路创建必要的RSACryptoServiceProvider

我将bartonjs 的回答保留为已接受,这是正确的。上面的代码是该研究的结果,我将其留在这里,以便其他想要做同样的事情的人可以干净地做到这一点,而不会像我在我的 OP 中那样使用任何数组复制黑客。

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

更新

在 .NET 路线图中,将 easier 与 ASN.1 parsing 一起用于 Core >2.1.0。

更新 2

Core .NET 2.1.1 现在有一个私有实现。 MS 正在测试,直到一切顺利,我们(希望)会在后续版本中看到公共 API。

更新 3

正如我通过问题here 发现的,以上信息不完整。缺少的是使用此解决方案加载的公钥是从加载的公钥+私钥对以编程方式生成的公钥。从密钥对(不仅仅是公钥)创建 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\n1\r\n2", pemHeader, base64Encoded, pemFooter);

如果您使用此密钥创建 PEM,您将能够使用前面描述的方法重新加载它。为什么这不一样?对 cert.GetPublicKey() 的调用实际上会返回 ASN.1 块结构;

SEQUENCE(2 elem)
  INTEGER (2048 bit)
  INTEGER 65537

这实际上是一个不完整的 DER blob,但 .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

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

【讨论】:

上面的代码可以简化为前两行简化为:Oid oid = new Oid("RSA"); 这真的有效吗?我遇到了异常,请参阅***.com/questions/58940913/… 了解更多详情。 @markf78,是的,虽然我看到 Reza 遇到了类似的问题,但直到现在我才错过了评论。我会看看你的链接问题【参考方案2】:

您无需导出现有参数,然后在它们之上重新导入。这会迫使您的机器生成一个 RSA 密钥,然后将其丢弃。所以为构造函数指定一个keysize并不重要(如果你不使用key它不会生成一个......通常)。

公钥文件是一个 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,我从未见过明确使用过)和可分辨编码规则 (DER) 下编码的内容。在大多数情况下,CER 限制 BER,DER 限制 CER,使 DER 最容易阅读。 (ITU-T X.680 描述了抽象语法符号一(ASN.1),这是 DER 对其进行二进制编码的语法)

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

30

这标识了一个带有 CONSTRUCTED 位集 (0x20) 的序列 (0x10),这意味着它包含其他 DER/标记值。 (序列总是在 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 是对象标识符,带有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.

继续:

03 81 8E 00

03 表示位串。 BIT STRING 被编码为 [tag] [length] [number of used bits]。未使用的位基本上总是零。所以这是一个位序列,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

另一个整数,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

Harvesing https://www.rfc-editor.org/rfc/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,您需要从 Modulus 中删除前导 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

【讨论】:

github.com/sevenTiny/SevenTiny.Bantina/blob/… => 方法 CreateRsaProviderFromPublicKey 完成了这项工作! 流行的 Bouncy Castle 库也有一个实现来为您获取 RSAParameters。 DotNetUtilities.ToRSAParameters( ... 自此答案以来的 4 年中,也刚刚内置了对此的支持:key.ImportSubjectPublicKeyInfo(derBytes, out int bytesRead)。但答案仍然解释了该方法的作用。【参考方案3】:

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;

【讨论】:

要是这么简单就好了 :-) 如果我将公钥传递给构造函数,它会产生 CryptographicExceptionCannot find the requested object 应该就是这么简单,但似乎 X509Certificate2 要求 DER 文件包含私钥... 作为替代方案,使用 Bouncy Castle,它支持导入 PEM 文件 没错,这就是我使用上述蛮力方法的原因。仅从公钥创建所需的对象非常困难。 Java 有一个很好的实现,C# 有一个名为 BouncyCastle 的旧库,但当前的文档不存在(从字面上看,它是一个空的 wiki),鉴于其当前的法律地位是慈善机构,我不太愿意使用它。所有这些都意味着低级解析方法 我实际上也无法使用 BouncyCastle 做到这一点。现有的帖子和信息非常过时,当前的文档不存在

以上是关于从公钥正确创建 RSACryptoServiceProvider的主要内容,如果未能解决你的问题,请参考以下文章

权限被拒绝(公钥)。 GitLab 突然出现错误。我正确配置了私钥和公钥

NEAR 上公钥的正确长度和格式是啥

从 Bouncy Castle 中的文本创建 RSA 公钥的问题

仅从公钥创建 RSACryptoServiceProvider

解决办法:gpg : 从公钥服务器接收失败:公钥服务器错误

从 X.509 证书中提取 PEM 公钥