【问题标题】:How to adapt public/private RSA keys of C# for using them as in Java?如何调整 C# 的公钥/私钥 RSA 密钥以便在 Java 中使用它们?
【发布时间】:2018-08-23 06:24:14
【问题描述】:

我有一些服务器通过加密 API 提供对数据的访问。我需要用 C# 编写客户端,它可以创建对服务器的请求并从中读取响应。

为此,我需要创建公共和私有 RSA 密钥并将它们转换为字节数组。我有 java 中的工作示例:

    java.security.KeyPairjava.security.KeyPair keypair = keyGen.genKeyPair();

    byte[] pubKeyBytes = keypair.getPublic().getEncoded();
    byte[] privKeyBytes = keypair.getPrivate().getEncoded();

我尝试在 .NET 中对 C# 做同样的事情:

    RSACryptoServiceProvider keyPair = new RSACryptoServiceProvider(2048);
    var publicKey = keyPair.ExportParameters(false);
    var privateKey = keyPair.ExportParameters(true);

我不知道该怎么做。我有 D、Dp、DQ、InverseQ、Modulus、Exponent 作为 publicKey 和 privateKey 的属性,但在 java 示例中,这些键看起来像单个联合键。我应该使用 D、Dp、DQ、InverseQ、Modulus、Exponent 中的哪一个来完成我的任务?与 java 示例中的方法相同,但在 C# 中的方法是什么?

【问题讨论】:

  • 你需要对字节做什么?将它们发送到 Windows 上的另一个 C# 程序?将它们发送到 Java?只需将它们保存并加载到同一台计算机上?
  • 创建对第三方服务的加密请求。该服务提供了一种描述如何执行此操作的算法。该算法在 49 页的文档中进行了描述。实现此算法需要将密钥作为字节。
  • ExportCspBlob 可能不是您想要的答案。您可能想要查找 SubjectPublicKeyInfo 和 PKCS8。 (或者可能只是 RSAPublicKey 和 RSAPrivateKey,如果它是 RSA-only)。如果到我有手机以外的其他东西时没有更好的答案,我会尝试比这个提示更好的东西:)

标签: java c# cryptography rsa


【解决方案1】:

根据https://docs.oracle.com/javase/7/docs/api/java/security/Key.html#getFormat(),公钥编码的默认值为 X.509 SubjectPublicKeyInfo,私钥为 PKCS#8 PrivateKeyInfo。

有很多关于从 SubjectPublicKeyInfo 创建 RSAParameters 的问题(如 Correctly create RSACryptoServiceProvider from public key),但反之则不多。

如果您通过 RSACryptoServiceProvider 创建密钥,那么新密钥的指数值将始终为 0x010001,这意味着您必须处理的唯一可变大小的数据是模值。这很重要的原因是 SubjectPublicKeyInfo(几乎总是)在 DER(由ITU-T X.690 定义)中编码,它使用长度前缀值。 ASN.1 (ITU-T X.680) 在RFC 5280 中定义为

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

用于 RSA 的 AlgorithmIdentifier 的编码值为

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

(又名序列(OID("1.2.840.113549.1.1.1"), NULL))

subjectPublicKey 的值取决于算法。对于 RSA,它是 RSAPublicKey,在 RFC 3447 中定义为

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

INTEGER 的编码是 02(然后是长度),然后是带符号的 big-endian 值。因此,假设您的指数值为01 00 01,则编码值为02 03 01 00 01。模数长度取决于密钥的大小。

int modulusBytes = parameters.Modulus.Length;

if (parameters.Modulus[0] >= 0x80)
    modulusBytes++;

RSACryptoServiceProvider 应始终创建需要额外字节的密钥,但技术上可能存在不需要的密钥。我们需要它的原因是 parameters.Modulus 是一个无符号大端编码,如果设置了高位,那么我们会将一个负数编码到 RSAPublicKey 中。我们通过插入一个 00 字节来解决这个问题,以保持符号位清晰。

模数的长度字节有点棘手。如果模数可以用 127 个字节或更少(RSA-1015 或更小)表示,那么您只需为该值使用一个字节。否则,您需要最小的字节数来报告该数字,加一。那个额外的字节(实际上是第一个字节)表示长度是多少字节。所以 128-255 是一个字节,81。 256-65535 是两个,所以82

然后我们需要将它包装成一个 BIT STRING 值,这很容易(如果我们忽略困难的部分,因为它们在这里不相关)。然后将其他所有内容封装在一个 SEQUENCE 中,这很容易。

又快又脏,只适用于指数=0x010001 的 2048 位密钥:

private static byte[] s_prefix =
{
    0x30, 0x82, 0x01, 0x22,
          0x30, 0x0D,
                0x06, 0x09, 0x2A, 0x86, 0x48, 0x86, 0xF7, 0x0D, 0x01, 0x01, 0x01,
                0x05, 0x00,
          0x03, 0x82, 0x01, 0x0F,
                0x00,
                0x30, 0x82, 0x01, 0x0A,
                      0x02, 0x82, 0x01, 0x01, 0x00
};

private static byte[] s_suffix = { 0x02, 0x03, 0x01, 0x00, 0x01 };

private static byte[] MakeSubjectPublicInfoEasy2048(RSA rsa)
{
    if (rsa.KeySize != 2048)
        throw new ArgumentException(nameof(rsa));

    RSAParameters rsaParameters = rsa.ExportParameters(false);

    if (Convert.ToBase64String(rsaParameters.Exponent) != "AQAB")
    {
        throw new ArgumentException(nameof(rsa));
    }

    return s_prefix.Concat(rsaParameters.Modulus).Concat(s_suffix).ToArray();
}

或者,对于通用响应(创建大量临时字节[]):

private static byte[] MakeTagLengthValue(byte tag, byte[] value, int index = 0, int length = -1)
{
    if (length == -1)
    {
        length = value.Length - index;
    }

    byte[] data;

    if (length < 0x80)
    {
        data = new byte[length + 2];
        data[1] = (byte)length;
    }
    else if (length <= 0xFF)
    {
        data = new byte[length + 3];
        data[1] = 0x81;
        data[2] = (byte)length;
    }
    else if (length <= 0xFFFF)
    {
        data = new byte[length + 4];
        data[1] = 0x82;
        data[2] = (byte)(length >> 8);
        data[3] = unchecked((byte)length);
    }
    else
    {
        throw new InvalidOperationException("Continue the pattern");
    }

    data[0] = tag;
    int dataOffset = data.Length - length;
    Buffer.BlockCopy(value, index, data, dataOffset, length);
    return data;
}

private static byte[] MakeInteger(byte[] unsignedBigEndianValue)
{
    if (unsignedBigEndianValue[0] >= 0x80)
    {
        byte[] tmp = new byte[unsignedBigEndianValue.Length + 1];
        Buffer.BlockCopy(unsignedBigEndianValue, 0, tmp, 1, unsignedBigEndianValue.Length);
        return MakeTagLengthValue(0x02, tmp);
    }

    for (int i = 0; i < unsignedBigEndianValue.Length; i++)
    {
        if (unsignedBigEndianValue[i] != 0)
        {
            if (unsignedBigEndianValue[i] >= 0x80)
            {
                i--;
            }

            return MakeTagLengthValue(0x02, unsignedBigEndianValue, i);
        }
    }

    // All bytes were 0, encode 0.
    return MakeTagLengthValue(0x02, unsignedBigEndianValue, 0, 1);
}

private static byte[] MakeSequence(params byte[][] data)
{
    return MakeTagLengthValue(0x30, data.SelectMany(a => a).ToArray());
}

private static byte[] MakeBitString(byte[] data)
{
    byte[] tmp = new byte[data.Length + 1];
    // Insert a 0x00 byte for the unused bit count value
    Buffer.BlockCopy(data, 0, tmp, 1, data.Length);
    return MakeTagLengthValue(0x03, tmp);
}

private static byte[] s_rsaAlgorithmId = new byte[] { 0x30, 0x0D, 0x06, 0x09, 0x2A, 0x86, 0x48, 0x86, 0xF7, 0x0D, 0x01, 0x01, 0x01, 0x05, 0x00 };

private static byte[] ExportSubjectPublicKeyInfo(RSA rsa)
{
    RSAParameters parameters = rsa.ExportParameters(false);

    return MakeSequence(
        s_rsaAlgorithmId,
        MakeBitString(
            MakeSequence(
                MakeInteger(parameters.Modulus),
                MakeInteger(parameters.Exponent))));
}

你不应该真的需要编码的私钥。但是,如果您真的这样做,您需要通用方法,因为私钥数据有很大的可变性空间。

PrivateKeyInfoRFC 5208 中定义为

PrivateKeyInfo ::= SEQUENCE {
  version                   Version,
  privateKeyAlgorithm       PrivateKeyAlgorithmIdentifier,
  privateKey                PrivateKey,
  attributes           [0]  IMPLICIT Attributes OPTIONAL }

Version ::= INTEGER

PrivateKeyAlgorithmIdentifier ::= AlgorithmIdentifier

PrivateKey ::= OCTET STRING

Attributes ::= SET OF Attribute

它还说当前版本号是0。

私钥的八位字节串由算法定义。对于 RSA,我们在 RFC 3447 中看到,以及 RSAPublicKey

RSAPrivateKey ::= SEQUENCE {
  version           Version,
  modulus           INTEGER,  -- n
  publicExponent    INTEGER,  -- e
  privateExponent   INTEGER,  -- d
  prime1            INTEGER,  -- p
  prime2            INTEGER,  -- q
  exponent1         INTEGER,  -- d mod (p-1)
  exponent2         INTEGER,  -- d mod (q-1)
  coefficient       INTEGER,  -- (inverse of q) mod p
  otherPrimeInfos   OtherPrimeInfos OPTIONAL }

忽略otherPrimeInfos。它不适用,也不应该适用。因此要使用的版本号为 0。

采用已经定义的实用方法,我们得到其余的

private static byte[] MakeOctetString(byte[] data)
{
    return MakeTagLengthValue(0x04, data);
}

private static byte[] s_integerZero = new byte[] { 0x02, 0x01, 0x00 };

private static byte[] ExportPrivateKeyInfo(RSA rsa)
{
    RSAParameters parameters = rsa.ExportParameters(true);

    return MakeSequence(
        s_integerZero,
        s_rsaAlgorithmId,
        MakeOctetString(
            MakeSequence(
                s_integerZero,
                MakeInteger(parameters.Modulus),
                MakeInteger(parameters.Exponent),
                MakeInteger(parameters.D),
                MakeInteger(parameters.P),
                MakeInteger(parameters.Q),
                MakeInteger(parameters.DP),
                MakeInteger(parameters.DQ),
                MakeInteger(parameters.InverseQ))));
}

.NET Core 的功能路线图让这一切变得更容易(https://github.com/dotnet/corefx/issues/20414 - 不是说导出,但是有导入的地方通常会有导出 :))

将您的输出保存到文件中,您可以使用openssl rsa -inform der -pubin -text -in pub.keyopenssl rsa -inform der -text -in priv.key 进行检查

【讨论】:

  • 我需要在 http 请求的标头中发送公钥(使用 hmac 密码加密,只有我作为此服务的客户端才知道)。响应由我的来自标头的公钥加密,我需要对其进行解密。我知道 keypair.getPublic().getEncoded() 在这个算法中可以正常工作,但它是 java 而不是 c#。如果我使用根据您的示例计算的公钥,我可以通过密钥解密响应吗?
  • @MiraiMann 我不关注。 HMAC 不是加密算法。但是ExportSubjectPublicKeyInfogetPublic().getEncoded()相同,ExportPrivateKeyInfo()getPrivate().getEncoded()相同。
  • 客户端发送此密钥的 RSA 密钥和 HMAC。服务器计算接收到的密钥的 HMAC 并将其与接收到的 HMAC 进行比较以进行验证。无论如何,这是不必要的信息。对此感到抱歉。明天我会在办公室尝试使用您的解决方案。谢谢。
  • 我使用了您示例中的代码,并且我生成的公钥已经过验证。结果,我对响应进行了加密,但尝试使用私钥对其进行解密却失败了。我使用 rsa.Decrypt(src, true/* 我也尝试过 false*/) 并且出现异常“要解密的数据超过了这个 256 字节模数的最大值。”内容大小为 2519609 字节。看来我无法通过这种方式解密它。如何通过我的私钥解密响应?
  • 我认为我的最后一条评论是题外话,还有其他问题。我接受您的回答,因为当前问题已结束。
【解决方案2】:

你需要使用ExportCspBlob方法:

RSACryptoServiceProvider keyPair = new RSACryptoServiceProvider(2048);
var publicKey = keyPair.ExportCspBlob(false);
var privateKey = keyPair.ExportCspBlob(true);

ExportParameters 导出可以计算密钥本身的特定参数。有关这些参数的更多信息,请参阅wiki article

【讨论】:

  • 看起来正是我所需要的。但我会实施我的客户来检查它是否有效,然后接受你的回答。
  • 我查了,不一样。
猜你喜欢
  • 1970-01-01
  • 2015-03-21
  • 2013-05-07
  • 1970-01-01
  • 2013-10-15
  • 2013-07-15
  • 2020-03-06
  • 2013-06-08
  • 2012-01-28
相关资源
最近更新 更多