.NET实现XML Encryption 1.1 ECDH-ES解密失败问题求助
.NET实现ECDH-ES XML加解密的问题
我正在.NET环境中开发支持ECDH-ES的XML加解密应用,客户端需用EC密钥对处理消息,但该加密方法无官方实现。目前我尝试用W3C密钥协商测试用例进行解密验证,通过官方XmlEnc和XmlDsig的XSD生成类解析XML,但在解密加密密钥阶段遇到了问题。
以第一个测试用例为例,我已完成以下步骤:
- 读取
xenc:OriginatorKeyInfo标签的属性与值,重建为ECDiffieHellmanCngPublicKey作为发起方公钥; - 通过
xenc:RecipientKeyInfo/dsig:X509Data/dsig:X509IssuerSerial中的颁发者名称和序列号,在p12密钥库文件中找到对应的接收方私钥。
核心问题
.NET没有内置的Concatenation Key Derivation Function(ConcatKDF)实现,我使用了开源JOSE-JWT的.NET版本(非XML专用,预期兼容),同时自行编写了AES Key Wrap代码。调用ConcatKDF.DeriveEcdhKey方法时,我尝试过128(测试用例使用AES-128-KW)、192、256三种keyBitLength参数,均未抛出异常,但在解包加密密钥时始终出现完整性校验错误。
改用Bouncy Castle .NET库时,无需传入keyBitLength参数,但同样在解包加密密钥步骤失败。相关代码如下:
XmlDocument xml = new XmlDocument(); xml.LoadXml(File.ReadAllText(cipherTextFile.FullName)); XmlElement encDataElement = XmlUtilities.FindXmlElement(xml, "EncryptedData", XmlSec.DefaultNamespaces.XMLENC_10); EncryptedData encryptedData = new EncryptedData(); encryptedData.LoadXml(encDataElement); XmlElement keyInfoElement = encryptedData.KeyInfo.GetXml(); XmlElement encKeyElement = XmlUtilities.FindXmlElement(keyInfoElement, "EncryptedKey", XmlSec.DefaultNamespaces.XMLENC_10); EncryptedKey encryptedKey = new EncryptedKey(); encryptedKey.LoadXml(encKeyElement); XmlElement encKeyInfoElement = encryptedKey.KeyInfo.GetXml(); XmlElement agreeMethodElement = XmlUtilities.FindXmlElement(encKeyInfoElement, "AgreementMethod", XmlSec.DefaultNamespaces.XMLENC_10); AgreementMethodType agreeMethod = XmlUtilities.Deserialize<AgreementMethodType>(agreeMethodElement); ECKeyValueType originatorKeyValue = XmlUtilities.Deserialize<ECKeyValueType>((agreeMethod.OriginatorKeyInfo.Items.FirstOrDefault() as KeyValueType).Item as XmlElement); NamedCurveType namedCurve = (originatorKeyValue.Item as NamedCurveType); Oid originatorCurveOid = new Oid(namedCurve.URI.Replace("urn:oid:", String.Empty)); Byte[] originatorQX = new Byte[(originatorKeyValue.PublicKey.Length - 1) / 2]; Byte[] originatorQY = new Byte[originatorQX.Length]; Buffer.BlockCopy(originatorKeyValue.PublicKey, 1, originatorQX, 0, originatorQX.Length); Buffer.BlockCopy(originatorKeyValue.PublicKey, originatorQX.Length + 1, originatorQY, 0, originatorQY.Length); ECParameters originatorEcParams = new ECParameters() { Q = new ECPoint() { X = originatorQX, Y = originatorQY }, Curve = ECCurve.CreateFromOid(originatorCurveOid) }; ECDiffieHellman originatorPublicKey = ECDiffieHellman.Create(originatorEcParams); X509IssuerSerialType recipientIssuerAndSn = null; X509DataType recipientX509Data = agreeMethod.RecipientKeyInfo.Items.FirstOrDefault() as X509DataType; for (Int32 i = 0; i < recipientX509Data.ItemsElementName.Length; i++) { if (recipientX509Data.ItemsElementName[i] == ItemsChoiceType.X509Certificate) { X509Certificate2 recipientCer = new X509Certificate2(recipientX509Data.Items[i] as Byte[]); recipientIssuerAndSn = new X509IssuerSerialType() { X509IssuerName = recipientCer.Issuer, X509SerialNumber = recipientCer.SerialNumber }; break; } if (recipientX509Data.ItemsElementName[i] == ItemsChoiceType.X509IssuerSerial) { recipientIssuerAndSn = recipientX509Data.Items[i] as X509IssuerSerialType; break; } } X500DistinguishedName recipientIssuer = new X500DistinguishedName(recipientIssuerAndSn.X509IssuerName); Byte[] ecPrivateKey = null; foreach (FileInfo ks in keyStores) { using (X509Certificate2 p12 = new X509Certificate2(File.ReadAllBytes(ks.FullName), "passwd", X509KeyStorageFlags.Exportable)) { if (p12.HasPrivateKey && p12.IssuerName.Name.Equals(recipientIssuer.Name) && p12.SerialNumber.Equals(recipientIssuerAndSn.X509SerialNumber)) { ECDsa ecdsaPK = p12.GetECDsaPrivateKey(); ecPrivateKey = ecdsaPK.ExportECPrivateKey(); break; } } } ECDiffieHellman recipientPrivKey = ECDiffieHellman.Create(); recipientPrivKey.ImportECPrivateKey(ecPrivateKey, out _); KeyDerivationMethodType keyDerivationMethod = XmlUtilities.Deserialize<KeyDerivationMethodType>(agreeMethod.Any.FirstOrDefault()); if (KeyDerivationAlgorithms.ConcatKDF.Equals(keyDerivationMethod.Algorithm)) { ConcatKDFParamsType concatKdfParams = XmlUtilities.Deserialize<ConcatKDFParamsType>(keyDerivationMethod.Any.FirstOrDefault()); DigestMethodType digestMethod = concatKdfParams.DigestMethod; Oid digestOid = DigestMethods.GetOid(digestMethod.Algorithm); Byte[] derivedKey = ConcatKDF.DeriveEcdhKey(originatorPublicKey, recipientPrivKey, 128, concatKdfParams.AlgorithmID, concatKdfParams.PartyVInfo, concatKdfParams.PartyUInfo, concatKdfParams.SuppPubInfo); Byte[] encryptedCek = encryptedKey.CipherData.CipherValue; Byte[] cek = AesKeyWrap.Unwrap(encryptedCek, derivedKey); // 此处抛出完整性错误,校验和失败 }
解密流程与疑问
我理解的解密流程如下:
- 从加密XML中提取发起方(临时)公钥;
- 根据XML中的接收方标识,在密钥库中找到对应私钥;
- 将
ConcatKDFParams的属性值转换为字节数组并拼接; - 使用发起方公钥、接收方私钥及拼接后的参数执行ConcatKDF密钥派生;
- 以ConcatKDF生成的密钥为输入,通过AES Key Wrap(是否为AES-ECB模式?)解包加密的内容加密密钥(CEK)——此步骤失败,是否因ConcatKDF输出错误?
- 根据
EncryptionMethod的算法属性解密XML内容。
我存在以下疑问:
- 解密步骤是否存在错误或遗漏?
- XML Encryption 1.1的测试用例是否可正常解密?
- 为何Bouncy Castle库也无法完成解包?
- 为何ECDH-ES的XML加密没有现成实现?
补充验证
我验证了简单密钥协商的结果,.NET内置类、Bouncy Castle及Bouncy Castle FIPS生成的共享密钥字节完全一致,代码如下:
// .NET内置类密钥协商 public static Byte[] DeriveKey(ECDiffieHellman recipientPrivKey, ECDiffieHellman otherPartyKey) { // 用ECDH生成共享密钥 Byte[] sharedSecret = recipientPrivKey.DeriveKeyMaterial(otherPartyKey.PublicKey); return sharedSecret; } // Bouncy Castle密钥协商 public static Byte[] EcdhKeyExchange(ECPrivateKeyParameters recipientPrivParams, ECPublicKeyParameters otherPartyParams) { IBasicAgreement keyAgree = AgreementUtilities.GetBasicAgreement("ECDH"); keyAgree.Init(recipientPrivParams); BigInteger sharedSecret = keyAgree.CalculateAgreement(otherPartyParams); Byte[] sharedSecretBytes = sharedSecret.ToByteArray(); return sharedSecretBytes; } // Bouncy Castle FIPS密钥协商 public static Byte[] BasicAgreementWithCofactor(AsymmetricECPrivateKey recipientPrivKey, AsymmetricECPublicKey otherPartyKey) { IAgreementCalculatorService dhFact = CryptoServicesRegistrar.CreateService(recipientPrivKey); IAgreementCalculator<FipsEC.AgreementParameters> agreement = dhFact.CreateAgreementCalculator(FipsEC.Cdh); return agreement.Calculate(otherPartyKey); }
由此可以确定问题大概率出在ConcatKDF的实现上,现在需要明确:
- 对ConcatKDF做哪些修改才能生成可用于AES解包加密密钥的正确输出?
- 是否存在测试用例数据错误导致解密时校验和失败?
- 后续计划尝试interop测试文件验证解密可行性。
内容的提问来源于stack exchange,提问作者azur3
相关产品推荐
相关产品推荐

