我正在尝试将使用 PKCS#12 证书对质询进行签名的参考实现从 Java 移植到 C#。据我了解参考资料,签名应该是 PKCS#1 v2.1 签名(几乎没有注释,也没有其他文档)。 Java 实现使用 Bouncy Castle,这对我来说是一个选择,不过如果我可以使用系统库解决这个问题那就更好了。
尽管尝试了很多方法,但我仍未能生成目标 API 接受的签名。
Java参考代码:
private byte[] signChallenge(final byte[] challenge) {
X509Certificate sigCert = ...;
PrivateKey sigKey = ...;
ContentSigner signer = new JcaContentSignerBuilder("SHA256withRSAandMGF1").setProvider("BC").build(sigKey);
CMSTypedData cmsData = new CMSProcessableByteArray(challenge);
CMSSignedDataGenerator cmsGenerator = new CMSSignedDataGenerator();
cmsGenerator.addSignerInfoGenerator(
new JcaSignerInfoGeneratorBuilder(
new JcaDigestCalculatorProviderBuilder().build()
).build(signer, sigCert)
);
Store certs = new JcaCertStore(Collections.singletonList(sigCert));
cmsGenerator.addCertificates(certs);
CMSSignedData cms = cmsGenerator.generate(cmsData, false);
byte[] signedMessage = cms.getEncoded();
return signedMessage;
}
首次尝试使用 Bouncy Castle 进行移植:
private byte[] SignChallenge(byte[] challenge)
{
var sigCert = DotNetUtilities.FromX509Certificate(Cert);
var sigKey = DotNetUtilities.GetRsaKeyPair(Cert.GetRSAPrivateKey()).Private;
var signer = SignerUtilities.InitSigner("SHA256withRSAandMGF1", true, sigKey, new());
var cmsData = new CmsProcessableByteArray(challenge);
var cmsGenerator = new CmsSignedDataGenerator();
var factory = new Asn1SignatureFactory("SHA256withRSAandMGF1", sigKey);
cmsGenerator.AddSignerInfoGenerator(new SignerInfoGeneratorBuilder().Build(factory, sigCert));
cmsGenerator.AddCertificate(sigCert);
var cms = cmsGenerator.Generate(cmsData, false);
var signedMessage = cms.GetEncoded();
return signedMessage;
}
如您所见,除了两行使用 Jca 类(Java 特定的)之外,这主要是 1:1 的副本。这会生成一个前约 30 个字节匹配的签名,但除此之外它不再匹配,并且对于相同的输入具有不同的总长度(Java 中为 2324 字节,而 C# 中为 2231 字节)。
我的第二次尝试是尝试使用 System.Security.Cryptography.Pkcs:
private byte[] SignChallenge(byte[] challenge)
{
var contentInfo = new System.Security.Cryptography.Pkcs.ContentInfo(challenge);
var signedCms = new SignedCms(contentInfo, true);
var signer = new CmsSigner(Cert);
signer.DigestAlgorithm = Oid.FromFriendlyName("SHA256", OidGroup.HashAlgorithm);
signer.SignaturePadding = RSASignaturePadding.Pss;
signedCms.ComputeSignature(signer);
var signedMessage = signedCms.Encode();
return signedMessage;
}
这更简洁,但也不走运。
我能够通过将签名写入文件并使用 OpenSSL 检查它们来解决这个问题:
openssl cms -in <signature file> -noout -cmsout -print -inform DER
这显示了一些差异,主要的一个是我生成的签名没有签名属性。参考签名具有 contentType (1.2.840.113549.1.9.3)、signingTime (1.2.840.113549.1.9.5)、messageDigest (1.2.840.113549.1.9.4) 和 CMSAlgorithmProtection (1.2.840.113549.1.9.52)。
前三个属性在 Bouncy Castle 和 System.Security.Cryptography.Pkcs 中都很容易添加,但 CMSAlgorithmProtection 需要特殊处理。在 Bouncy Castle 中,使用
DefaultSignedAttributeTableGenerator
时应该自动生成该属性,但是 相关代码当前已被注释掉。不过,我可以自己构建该属性:
var signatureAlgorithm = DefaultSignatureAlgorithmFinder.Instance.Find("SHA256withRSAandMGF1");
var algorithmProtection = new CmsAlgorithmProtection(new(new("2.16.840.1.101.3.4.2.1")), 1, signatureAlgorithm);
var attributeTable = new Org.BouncyCastle.Asn1.Cms.AttributeTable(new Dictionary<DerObjectIdentifier, object>
{
[CmsAttributes.CmsAlgorithmProtect] = new Org.BouncyCastle.Asn1.Cms.Attribute(CmsAttributes.CmsAlgorithmProtect, new DerSet(algorithmProtection))
});
cmsGenerator.AddSignerInfoGenerator(
new SignerInfoGeneratorBuilder()
.WithSignedAttributeGenerator(new DefaultSignedAttributeTableGenerator(attributeTable))
.Build(new Asn1SignatureFactory(algorithm, sigKey), sigCert));
这段代码最终产生了一个有效的签名。
我无法仅使用 System.Security.Cryptography.Pkcs 生成有效签名。我能够利用 Bouncy Castle
CmsAlgorithmProtection
类生成工作签名属性,但签名算法的编码 (CMS_ContentInfo -> d.signedData ->signerInfos ->signatureAlgorithm) 似乎略有不同,并且缺少一些 NULL 值。不过,Bouncy Castle 解决方案目前已经足够好了。