RSA 我应该使用 X.509 还是 PKCS #1

问题描述 投票:0回答:2

使用案例: 我有一个用例,其中客户端生成私钥和公钥,并将 Base 64 编码的公钥发送到服务器。

在服务器端,我将使用此公钥加密消息并将加密的消息发送到客户端,客户端使用其私钥解密该消息。商定的算法是“RSA”。

问题出在服务器端,我发现某些键正在使用

X509EncodedKeySpec
作为关键规范

byte[] publicBytes = Base64.decodeBase64(base64EncodedPubKey);
X509EncodedKeySpec keySpec = new X509EncodedKeySpec(publicBytes);
KeyFactory keyFactory = KeyFactory.getInstance("RSA");
PublicKey pubKey = keyFactory.generatePublic(keySpec);

虽然有些键使用

Caused by: java.security.InvalidKeyException: IOException: algid parse error, not a sequence
会引发异常 (
X509EncodedKeySpec
),但使用
RSAPublicKeySpec
可以工作:

byte[] publicBytes = Base64.decodeBase64(base64EncodedPubKey);
org.bouncycastle.asn1.pkcs.RSAPublicKey.RSAPublicKey pkcs1PublicKey = org.bouncycastle.asn1.pkcs.RSAPublicKey.RSAPublicKey.getInstance(publicBytes);
BigInteger modulus = pkcs1PublicKey.getModulus();
BigInteger publicExponent = pkcs1PublicKey.getPublicExponent();
RSAPublicKeySpec keySpec = new RSAPublicKeySpec(modulus, publicExponent);
KeyFactory keyFactory = KeyFactory.getInstance("RSA");
PublicKey pubKey = keyFactory.generatePublic(keySpec);

所以,我了解到客户端和服务器需要就是否使用达成一致:

PKCS #1
X.509
用于对密钥进行编码。 我的问题是哪一种更适合我的用例? 有什么时候使用哪种格式的指南吗?

java public-key-encryption public-key
2个回答
10
投票

差别很小。 Java 的密钥格式称为 X.509,更准确地称为 X.509 中定义的 ASN.1 结构

SubjectPublicKeyInfo
(或 SPKI),或者在 RFC5280 秒 4.1 中定义,更方便,是处理大量灵活算法的一种非常简单的方法:它由一个子结构
AlgorithmIdentifier
组成,用于标识算法及其参数(如果适用),然后是一个不透明的位字符串,其中包含某种格式的实际关键信息(编码)取决于 AlgorithmIdentifier(所识别的算法)。

对于 RSA,算法相关部分 ASN.1 结构

RSAPublicKey
在 PKCS1 或更方便地定义于 RFC8017 附录 A.1.1 及其早期版本,并在 RFC3279 秒 2.3.1 中重复。因此,对于 RSA,X.509 (SPKI) 格式包含 PKCS1 格式,并且由于 RSA 没有参数(或至少与密钥相关的参数),唯一真正的区别是 X.509 格式明确指定密钥是 RSA——在您的应用程序中您已经知道了。

您已经发现,vanilla(Oracle-was-Sun-now-OpenJDK)Java 加密,又名 JCA Java 加密架构,直接仅支持 X.509 (SPKI) 格式,这是一个小优势。但是,如果您使用 BouncyCastle,那么来回转换比 Q 中的代码要容易得多;您只需使用

org.bouncycastle.asn1.x509.SubjectPublicKeyInfo
类来添加或丢弃 AlgorithmIdentifier:

    // test data source
    KeyStore ks = KeyStore.getInstance("JKS"); ks.load (new FileInputStream (args[0]), args[1].toCharArray());
    byte[] spkienc = ks.getCertificate(args[2]).getPublicKey().getEncoded();
    System.out.println (DatatypeConverter.printHexBinary(spkienc));

    // extract PKCS1 part of original SPKI
    byte[] pkcs1enc = SubjectPublicKeyInfo.getInstance(spkienc).parsePublicKey().getEncoded();
    System.out.println (DatatypeConverter.printHexBinary(pkcs1enc));

    // rebuild SPKI from the PKCS1
    AlgorithmIdentifier algid = new AlgorithmIdentifier(PKCSObjectIdentifiers.rsaEncryption, DERNull.INSTANCE);
    byte[] spki2enc = new SubjectPublicKeyInfo (algid, pkcs1enc).getEncoded();
    System.out.println (DatatypeConverter.printHexBinary(spki2enc));

请参阅我对类似 golang x509.MarshalPKIXPublicKey vs x509.MarshalPKCS1PublicKey() 的回答,尤其是以下链接:
将SubjectPublicKeyInfo格式的公钥转换为RSAPublicKey格式java
在 Java 中生成 PKCS#1 格式的 RSA 密钥
传输 RSA 公钥、javaME、充气城堡时出现问题

如果你没有BouncyCastle,那就有点难了;您需要编写部分 ASN.1 解析器或生成器。完整的 ASN.1 处理相当复杂,但对于这种情况,您只需要一个还不错的小子集。 (是的,这是微弱的赞美。)如果我有更多时间,我可能会稍后添加。

一个更大的潜在问题是您的密钥未经身份验证。公钥分发的困难部分比微小的格式细节更难,是确保分发合法密钥。如果攻击者可以用他们的公钥替换正确的公钥,那么受害者就会以攻击者可以轻松读取的方式加密所谓的秘密数据,并且所有花哨的加密代码都完全毫无价值。

这就是为什么大多数实际系统不分发裸公钥,而是分发允许验证密钥是否正确的证书。有一些证书方案,但迄今为止最广泛的是 X.509 及其 Internet 配置文件 PKIX——事实上,我上面引用的 RFC,5280 和 3279,都是 PKIX 的一部分。 SSL-now-TLS 使用 X.509。代码签名使用 X.509。 S/MIME 电子邮件使用 X.509。 (PGP/GPG 使用不同类型的证书,不是 X.509,但仍然是证书。)并且(vanilla)Java 直接支持 X.509 证书,与“X.509”(SPKI)公钥一样好甚至更好.


0
投票

Per @dave_thompson_085 - 我必须为我在 Xamarin 上所做的工作编写 PCKS1 编码。这是代码:

    /// <summary>
    /// Encode length for ASN.1
    /// </summary>
    /// <param name="stream"></param>
    /// <param name="length"></param>
    /// <exception cref="ArgumentOutOfRangeException"></exception>
    public static void EncodeLength(BinaryWriter stream, int length)
    {
        if (length < 0)
            throw new ArgumentOutOfRangeException(nameof(length), "Length must be non-negative");
        if (length < 0x80)
        {
            // Short form
            stream.Write((byte)length);
        }
        else
        {
            // Long form
            var temp = length;
            var bytesRequired = 0;
            while (temp > 0)
            {
                temp >>= 8;
                bytesRequired++;
            }
            stream.Write((byte)(bytesRequired | 0x80));
            for (var i = bytesRequired - 1; i >= 0; i--)
            {
                stream.Write((byte)(length >> (8 * i) & 0xff));
            }
        }
    }

    /// <summary>
    /// Encode data big-endian for ASN.1
    /// </summary>
    /// <param name="stream"></param>
    /// <param name="value"></param>
    /// <param name="forceUnsigned"></param>
    public static void EncodeIntegerBigEndian(
        BinaryWriter stream, byte[] value, bool forceUnsigned = true
    )
    {
        stream.Write((byte)0x02); // INTEGER
        var prefixZeros = 0;
        for (var i = 0; i < value.Length; i++)
        {
            if (value[i] != 0) break;
            prefixZeros++;
        }
        if (value.Length - prefixZeros == 0)
        {
            EncodeLength(stream, 1);
            stream.Write((byte)0);
        }
        else
        {
            if (forceUnsigned && value[prefixZeros] > 0x7f)
            {
                // Add a prefix zero to force unsigned if the MSB is 1
                EncodeLength(stream, value.Length - prefixZeros + 1);
                stream.Write((byte)0);
            }
            else
            {
                EncodeLength(stream, value.Length - prefixZeros);
            }
            for (var i = prefixZeros; i < value.Length; i++)
            {
                stream.Write(value[i]);
            }
        }
    }

    /// <summary>
    /// Convenience method for Xamarin
    /// </summary>
    /// <param name="rsaParameters"></param>
    /// <returns></returns>
    public static byte[] ToRSAPublicKey(
        RSAParameters rsaParameters
    )
    {
        // write the parameters to PKCS1 format
        // see: https://www.di-mgt.com.au/docs/examplesPKCS.txt
        using (var stream = new MemoryStream())
        {
            var writer = new BinaryWriter(stream);

            // PKCS1 is tres simple: SEQUENCE { Modulus; Exponent }

            // write the SEQUENCE code
            writer.Write(SEQUENCE_ID);

            // create parameters and their total length
            byte[] innerParametersBytes;
            int innerParametersLength;
            using (var innerParametersStream = new MemoryStream())
            {
                var innerParametersWriter = new BinaryWriter(
                    innerParametersStream
                );
                EncodeIntegerBigEndian(
                    innerParametersWriter, rsaParameters.Modulus
                );
                EncodeIntegerBigEndian(
                    innerParametersWriter, rsaParameters.Exponent
                );
                innerParametersWriter.Flush();
                innerParametersBytes = innerParametersStream.GetBuffer();
                innerParametersLength = (int)innerParametersStream.Length;
            }

            // write length of the parameters
            EncodeLength(
                writer, innerParametersLength
            );

            // write the actual parameter bytes
            writer.Write(innerParametersBytes, 0, innerParametersLength);

            // all done :)
            writer.Flush();
            var result = stream.ToArray();
            return result;
        }
    }

用法是:

var rsaProvider = "[some_object]" as RSACryptoServiceProvider;
var rsaParameters = rsaProvider.ExportParameters(false);
return ToRSAPublicKey(rsaParameters);
© www.soinside.com 2019 - 2024. All rights reserved.