使用案例: 我有一个用例,其中客户端生成私钥和公钥,并将 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 的密钥格式称为 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)公钥一样好甚至更好.
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);