You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

.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); // 此处抛出完整性错误,校验和失败
}

解密流程与疑问

我理解的解密流程如下:

  1. 从加密XML中提取发起方(临时)公钥;
  2. 根据XML中的接收方标识,在密钥库中找到对应私钥;
  3. 将ConcatKDFParams的属性值转换为字节数组并拼接;
  4. 使用发起方公钥、接收方私钥及拼接后的参数执行ConcatKDF密钥派生;
  5. 以ConcatKDF生成的密钥为输入,通过AES Key Wrap(是否为AES-ECB模式?)解包加密的内容加密密钥(CEK)——此步骤失败,是否因ConcatKDF输出错误?
  6. 根据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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.18 03:48:10