从 public 密钥正确创建 RSACryptoServiceProvider

Correctly create RSACryptoServiceProvider from public key

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

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

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 字节是不可行的。

如何更改它以在给定一组 public 关键字节的情况下正确挑选出模数和指数字节?我试图理解 ASN.1 标准,但运气不佳,无法从中找到我需要的东西 - 标准有点拜占庭。

感谢任何帮助。

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

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

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

您不需要导出现有参数然后 re-import 在它们之上。这会强制您的机器生成一个 RSA 密钥,然后将其丢弃。因此,为构造函数指定密钥大小无关紧要(如果您不使用密钥,它不会生成密钥……通常)。

public 密钥文件是 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 defines how to read things encoded under Basic Encoding Rules (BER), Canonical Encoding Rules (CER, which I've never seen explicitly used), and Distinguished Encoding Rules (DER). For the most part CER restricts BER and DER restricts CER, making DER the easiest to read. (ITU-T X.680描述了Abstract Syntax Notation One (ASN.1),这是DER作为二进制编码的文法)

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

30

这标识了带有 CONSTRUCTED 位集 (0x20) 的序列 (0x10),这意味着它包含其他 DER/tagged 值。 (序列总是在 DER 中构造)

81 A0

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

30 0D

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

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

06 是 OBJECT IDENTIFIER,具有 0x09 字节有效负载。 OID有点non-intuitive编码,但是这个相当于文本表示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。 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://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 public密钥格式是什么。但是 oid-info 站点告诉我们查看 RFC 2313,在那里我们看到

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

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

也就是说,我们读取的第一个整数是模数值,第二个是 (public)指数。

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
}

经过大量时间的搜索和 bartonjs 的出色响应,执行此操作的代码最终实际上很简单,尽管对于不熟悉 [=] 结构的人来说有点不直观90=]键。

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

一个public密钥PEM可以描述多种密钥类型,不仅仅是RSA所以而不是像new RSACryptoServiceProvider(pemBytes)这样的东西,我们必须根据它的structure/syntax来解析PEM, ASN.1,然后它会告诉我们它是否是 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;

注意:以上代码生产就绪!您需要在对象创建周围设置适当的保护措施(例如 public 密钥可能不是 RSA)、转换为 RSACryptoServiceProvider 等。此处的代码示例很短,说明它可以做得相当干净。

我是怎么得到这个的?通过 ILSpy 中的 Cryptographic 命名空间深入探索,我注意到 AsnEncodedData 响起了带有 bartonjs 描述的铃声。在进行更多研究后,我偶然发现了 this post(看起来很眼熟?)。这是试图专门确定密钥大小,但它会在此过程中创建必要的 RSACryptoServiceProvider

我将 bartonjs 的回答保留为已接受,这是正确的。上面的代码是该研究的结果,我将其保留在这里,以便其他希望做同样事情的人可以干净利落地这样做,而无需像我在 OP 中那样进行任何数组复制黑客攻击。

此外,出于解码和测试目的,您可以使用 ASN.1 解码器 here.

检查您的 public 密钥是否可解析

更新

在 .NET 路线图上为 Core >2.1.0 制作这个 easier with ASN.1 parsing

更新 2

Core .NET 2.1.1 中现在有一个私有实现。 MS 正在测试,直到满意为止,我们将(希望)在后续版本中看到 public API。

更新 3

正如我通过问题 here 发现的那样,以上信息不完整。缺少的是使用此解决方案加载的 public 密钥是从加载的 public+ 私钥对以编程方式生成的密钥。从密钥对(不仅仅是 public 密钥)创建 RSACryptoServiceProvider 后,您可以仅导出 public 字节并将它们编码为 public 密钥 PEM。这样做将与此处的解决方案兼容。这是怎么回事?

将 public + 私钥对加载到 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);

如果您从此密钥创建 PEM,您将能够使用前面描述的方法将其重新加载。为什么这是不同的?对 cert.GetPublicKey() 的调用实际上 return 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) 编码 public 密钥字节具有以下结构;

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

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