65.9K
CodeProject 正在变化。 阅读更多。
Home

在 .NET 中使用 CAPICOM 进行 ASCII/UTF8 内容的数字签名

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.62/5 (17投票s)

2005年2月26日

7分钟阅读

viewsIcon

214253

downloadIcon

3483

修改由 TlbImp.exe 生成的 CAPICOM 运行时可调用包装器 (RCW),以使 CAPICOM 能够处理 UTF8/ASCII 内容的数字签名(主要由 Java 签名)。

概述

在 .NET 1.0-1.1 中,除了使用 Microsoft Platform SDK 中包含的 CAPICOM COM 库外,我们没有太多选择来处理 PKCS7 签名数据。CAPICOM 提供了 SignedData 对象来生成/验证附加和分离的数字签名以及许多其他有用的对象。

我在处理数字签名时遇到的一个常见问题是,如何在 .NET 中生成 Java 系统可以验证的 PKCS7 格式的数字签名。当你在 .NET 平台中验证 Java 系统生成的数字签名时,也会出现同样的问题。问题在于,CAPICOM 在生成/验证签名时只接受 Unicode 内容,而基于 Java 的系统通常使用 UTF8 或 ASCII 内容进行数字签名,这在使用 .NET 代码中的 CAPICOM 时实际上会引发一些问题。本文将向您展示如何解决以下问题。

问题

假设有以下场景:您有一个基于 .NET 的应用程序(BizTalk/ASP.NET 应用程序/Web 服务等),并希望使用 XML 消息和 PKCS7 格式的数字签名与基于 Java 的系统进行集成。让我们说您的系统需要通过发送和接收签名 XML 消息来实现与该外部系统的双向通信。为了发送消息,您需要对 XML 消息进行签名,以便 Java 系统可以验证。在接收时,您需要在进行任何进一步处理之前验证 Java 系统生成的签名。如果您决定从 .NET 代码中使用 CAPICOM,那么您很可能会在此场景中遇到以下问题。

验证附加签名

数字签名的附加形式将明文嵌入 PKCS7 封包中。您需要先验证签名,然后检索内容。您可能需要编写类似这样的代码来执行此操作

public string VerifyAttachedSignature(string base64SignedContent)
{
    CAPICOM.SignedData signedData = new CAPICOM.SignedData();
    signedData.Verify(base64SignedContent, false,
      CAPICOM.CAPICOM_SIGNED_DATA_VERIFY_FLAG.CAPICOM_VERIFY_SIGNATURE_ONLY);
    string clearText = signedData.Content;
    return clearText; 
}

即使代码完美地验证了签名,如果您在尝试检索 signedData.Content 字段以从签名中提取明文时,如果 Java 系统在签名生成过程中使用了 UTF8/ASCII 编码,您将遇到问题。问题在于,返回的 clearText 被期望是 Unicode,但实际上是一个二进制字符串。一种解决方案是使用 CAPICOM 中的 Utilities 对象获取 UTF8(或 ASCII)字节数组,然后使用 System.Text.Encoding 类将其转换回 .NET 字符串。然后代码变成

public string VerifyAttachedSignature(string base64SignedContent)
{
    CAPICOM.SignedData signedData = new CAPICOM.SignedData();
    signedData.Verify(base64SignedContent, false,
      CAPICOM.CAPICOM_SIGNED_DATA_VERIFY_FLAG.CAPICOM_VERIFY_SIGNATURE_ONLY);
    //assuming the content is ASCII or UTF8
    string binaryString = signedData.Content;
    CAPICOM.Utilities utility = new CAPICOM.Utilities();
    string clearText =System.Text.Encoding.UTF8.GetString(
            (byte[])utility.BinaryStringToByteArray(binaryString));
    return clearText; 
}

这似乎解决了问题,但实际上没有!如果明文内容的字符数是偶数,这段代码才能正常工作!这是因为 COM 互操作在从 signedData.Content 获取 binaryString 时,如果实际内容包含奇数个字符,在转换为 UNICODE 时会截断最后一个字符。

验证分离签名

分离签名的形式与此类似,但更具戏剧性。请看下面这段使用 CAPICOM 验证分离签名的代码

public void VerifyDetachedSignature(string clearTextMessage,
                                    string base64SignedContent)
{
    CAPICOM.SignedData signedData = new CAPICOM.SignedData();
    CAPICOM.Utilities utility = new CAPICOM.Utilities();
    //assuming the content is ASCII or UTF8
    signedData.Content=utility.ByteArrayToBinaryString(
       System.Text.Encoding.UTF8.GetBytes(clearTextMessage));
    signedData.Verify(base64SignedContent, true, 
       CAPICOM.CAPICOM_SIGNED_DATA_VERIFY_FLAG.CAPICOM_VERIFY_SIGNATURE_ONLY); 
}

您可能已经注意到,我使用了相同的技巧,通过 CAPICOM.Utilities 对象的 ByteArrayToBinaryString 方法将 .NET 字符串转换为二进制字符串。这将使我们能够将 ASCII/UTF8 内容作为 UNICODE 内容传递给 CAPICOM 进行验证。然而,同样的问题在这里发生,后果将无法处理。如果 clearTextMessage 包含奇数个字符,我们将永远无法将实际内容传递给 CAPICOM,因此验证将始终失败!

生成附加签名

Java 代码要求我们使用 ASCII/UTF8 编码的字符串生成签名。典型的代码如下

public string GenerateAttachedSignature(string clearTextMessage, 
             CAPICOM.Certificate myClientCertificate)
{
    CAPICOM.SignedData signedData = new CAPICOM.SignedDataClass();
    CAPICOM.Utilities utility = new CAPICOM.UtilitiesClass();
    //Content has to be UTF8 as our Java friend expects in this format 
    signedData.Content = utility.ByteArrayToBinaryString(
         System.Text.Encoding.UTF8.GetBytes(clearTextMessage));
    CAPICOM.Signer signer = new CAPICOM.Signer();
    signer.Certificate = myClientCertificate;
    string signedMessage = signedData.Sign(signer, false,
        CAPICOM.CAPICOM_ENCODING_TYPE.CAPICOM_ENCODE_BASE64);
    return signedMessage; 
}

这段代码解决了附加 UTF8 内容的问题,并生成了我们的 Java 朋友可以验证的签名。然而,由于我之前提到的截断问题,如果字符数为奇数,他们提取的数据的最后一个字符将被截断。有一个解决方案,可能适用于所有场景,也可能不适用于所有场景;在签名之前在 clearTextMessage 末尾添加一个额外的空格就可以解决问题,如下所示

...
signedData.Content = utility.ByteArrayToBinaryString(
    System.Text.Encoding.UTF8.GetBytes(clearTextMessage + " "));
...

生成分离签名

分离方面的问题仍然很难处理

public string GenerateDetachedSignature(string clearTextMessageWithEvenCharacters, 
          CAPICOM.Certificate myClientCertificate)
{
    CAPICOM.SignedData signedData = new CAPICOM.SignedDataClass();
    CAPICOM.Utilities utility = new CAPICOM.UtilitiesClass();
    //Content has to be UTF8 as our Java friend expects in this format 
    signedData.Content = utility.ByteArrayToBinaryString(
           System.Text.Encoding.UTF8.GetBytes(
                       clearTextMessageWithEvenCharacters));
    CAPICOM.Signer signer = new CAPICOM.Signer();
    signer.Certificate = myClientCertificate;
    string signedMessage = signedData.Sign(signer, true, 
        CAPICOM.CAPICOM_ENCODING_TYPE.CAPICOM_ENCODE_BASE64);
    return signedMessage; 
}

上面提供的代码要求调用者发送偶数个字符的明文才能正常工作,否则它将生成一个签名,其中最后一个字符被截断,这可能会或可能不会被基于 Java 的系统处理。

解决方案

正如您所看到的,问题的根源在于 CAPICOM 在验证和生成数字签名时只处理 Unicode 字符串。CAPICOM 实际上是由 Microsoft 设计用于 VB 等语言的,因此 CAPICOM 类型库中的方法参数定义为 BSTR,这实际上是 VB 版本的 Unicode。BSTR 实际上是一个指向 Unicode 字符数组的 32 位指针,前面有一个 4 字节长度,后面跟着一个 2 字节的 null 字符(ANSI 0)。根据我在 VB 6 中的实验,我发现这些问题在那里都不存在,即 Utilities 类的方法不会截断最后一个字符,一切都正常工作。这实际上引起了我对使用 .NET 世界中的 CAPICOM 所需的运行时可调用包装器 (RCW) 的注意。看起来当我试图将 ANSI BSTR(二进制字符串)从 .NET 传递或检索到 RCW 时,正是它在转换为 Unicode 时造成了截断。

基于以上所有因素,我决定修改生成的 RCW,以避免在托管世界和非托管 COM 世界之间发生任何封送处理。我仍然会使用 Utilities 类来生成和检索二进制字符串,但我会阻止任何 BSTR 与 .NET 字符串之间的转换。最佳选择是使用 .NET 的 IntPtr 而不是 BSTR 封送,因为 BSTR 实际上是一个指针。

第一步,我使用 ILDASM.EXE 命令将 .NET RCW CAPICOM.dll 反编译为 IL 代码

C:\CapicomWork>ILDASM CAPICOM.dll /OUT:CAPICOM.IL

ILDASM 生成了两个文件:CAPICOM.IL,其中包含 CAPICOM RCW 的 IL 代码,以及 CAPICOM.res 资源文件。我打开了 CAPICOM.IL 并找到了 SignedDataClass 的 Content 字段声明位置,如下所示

.method public hidebysig newslot specialname virtual 
instance void set_Content([in] string marshal( bstr) pVal) 
                                                runtime managed internalcall
{
    .custom instance void 
       [mscorlib]System.Runtime.InteropServices.DispIdAttribute::.ctor(int32) 
                                                = ( 01 00 00 00 00 00 00 00 ) 
    .override CAPICOM.ISignedData::set_Content
} // end of method SignedDataClass::set_Content

.method public hidebysig newslot specialname virtual 
instance string 
marshal( bstr) 
get_Content() runtime managed internalcall
{
    .custom instance void 
       [mscorlib]System.Runtime.InteropServices.DispIdAttribute::.ctor(int32) 
                                                = ( 01 00 00 00 00 00 00 00 ) 
    .override CAPICOM.ISignedData::get_Content
} // end of method SignedDataClass::get_Content

由于 Content 是一个属性,IL 代码包含两个用于 get 和 set 操作的方法:get_Contentset_Content。从上面的 IL 代码可以看出,Content 字段实际上被解释为字符串并作为 BSTR 进行封送。我将上面的 IL 代码更改为以下内容

.method public hidebysig newslot specialname virtual 
instance void set_Content([in] native int pVal) runtime managed internalcall
{
    .custom instance void 
      [mscorlib]System.Runtime.InteropServices.DispIdAttribute::.ctor(int32) 
                                               = ( 01 00 00 00 00 00 00 00 ) 
    .override CAPICOM.ISignedData::set_Content
} // end of method SignedDataClass::set_Content

.method public hidebysig newslot specialname virtual 
instance native int 
get_Content() runtime managed internalcall
{
    .custom instance void 
      [mscorlib]System.Runtime.InteropServices.DispIdAttribute::.ctor(int32) 
                                               = ( 01 00 00 00 00 00 00 00 ) 
    .override CAPICOM.ISignedData::get_Content
} // end of method SignedDataClass::get_Content

您注意到,我将 string marshal (bstr) 替换为 native int,这实际上是 C# 中的 IntPtr。我必须对 ISignedDataSignedData 接口进行相同的更改,以避免任何冲突。

然而,仅凭此更改还不够,因为在 .NET 端很难生成正确的 BSTR IntPtr。另一个问题是在附加签名内容提取的情况下检索正确的字符串。我应该能够以字节数组的形式获取内容,以避免潜在的数据截断。

Utilities 类是帮助我解决这些挑战的最佳选择,因为我可以使用 ByteArrayToBinaryString 方法传递字节数组并获取 BSTR 指针,而无需额外的工作。同样,使用 BinaryStringToByteArray 方法将使我能够处理第二个问题,即检索附加到消息中的正确消息。我在 IL 中将 UtilitiesClass(以及 IUtilities 接口)更改如下

method public hidebysig newslot virtual 
instance object 
marshal( struct) 
BinaryStringToByteArray([in] native int
                               BinaryString) runtime managed internalcall
{
    .custom instance void 
      [mscorlib]System.Runtime.InteropServices.DispIdAttribute::.ctor(int32)
                                              = ( 01 00 06 00 00 00 00 00 ) 
    .override CAPICOM.IUtilities::BinaryStringToByteArray
} // end of method UtilitiesClass::BinaryStringToByteArray

.method public hidebysig newslot virtual 
instance native int 
ByteArrayToBinaryString([in] object marshal( struct) varByteArray)
                                     runtime managed internalcall
{
    .custom instance void 
      [mscorlib]System.Runtime.InteropServices.DispIdAttribute::.ctor(int32) 
                                               = ( 01 00 07 00 00 00 00 00 ) 
    .override CAPICOM.IUtilities::ByteArrayToBinaryString
} // end of method UtilitiesClass::ByteArrayToBinaryString

然后保存 CAPICOM.IL,并从命令提示符运行以下命令,使用 .NET 的 ILASM 工具重新构建 RCW CAPICOM.dll

C:\CapicomWork>ilasm /dll CAPICOM.IL /
                      resource=CAPICOM.res /KEY=mykey.snk /output=Capicom.dll

最终代码

成功使用 ILASM 构建后,我重写了数字签名例程,如下所示

public string SignFromText(string plaintextMessage, 
                     bool bDetached, Encoding encodingType) 
{
    CAPICOM.SignedData signedData = 
                      new CAPICOM.SignedDataClass();
    CAPICOM.Utilities u = new CAPICOM.UtilitiesClass();
    signedData.set_Content(u.ByteArrayToBinaryString(
                     encodingType.GetBytes(plaintextMessage)));
    CAPICOM.Signer signer = new CAPICOM.Signer();
    signer.Certificate = ClientCert;
    this._signedContent = signedData.Sign(signer, bDetached, 
            CAPICOM.CAPICOM_ENCODING_TYPE.CAPICOM_ENCODE_BASE64);
    return _signedContent; 
}
public bool VerifyDetachedSignature(string plaintextMessage, 
                   string signedContent, Encoding encodingType) 
{
    try
    {
        this._clearText = plaintextMessage;
        this._signedContent = signedContent;
        CAPICOM.SignedData signedData = 
                         new CAPICOM.SignedDataClass();
        CAPICOM.Utilities u = new CAPICOM.UtilitiesClass();
        signedData.set_Content(u.ByteArrayToBinaryString(
                   encodingType.GetBytes(plaintextMessage)));
        signedData.Verify(_signedContent,true, 
           CAPICOM.CAPICOM_SIGNED_DATA_VERIFY_FLAG
                    .CAPICOM_VERIFY_SIGNATURE_ONLY);
        SignerCert=null;
        CAPICOM.Signer s = (CAPICOM.Signer) signedData.Signers[1];
        SignerCert = (CAPICOM.Certificate)s.Certificate; 
        return true; 
    }
    catch(COMException e)
    {
        return false;
    }
}
public bool VerifyAttachedSignature(string signedContent,
                                           Encoding encodingType) 
{
    try
    {
        this._signedContent = signedContent;
        CAPICOM.Utilities u = new CAPICOM.Utilities();
        CAPICOM.SignedData signedData = new CAPICOM.SignedData();
        signedData.Verify(_signedContent,false, 
          CAPICOM.CAPICOM_SIGNED_DATA_VERIFY_FLAG.CAPICOM_VERIFY_SIGNATURE_ONLY);
        SignerCert=null;
        CAPICOM.Signer s = (CAPICOM.Signer) signedData.Signers[1];
        SignerCert = (CAPICOM.Certificate)s.Certificate; 
        this._clearText = encodingType.GetString(
            (byte[])u.BinaryStringToByteArray(signedData.get_Content()));
        return true; 
   }
   catch(COMException e)
   {
        return false;
   }
}

上面粗体显示的行实际显示了应用 CAPICOM RCW 更改后的代码差异。我们不能再使用 SignedData.Content 属性,因为 C# 不支持返回 IntPtr 的属性,所以我们在需要时必须显式指定 get_Contentset_Content 方法。encodingType 参数使我们能够以我们喜欢的任何编码传递或检索内容。SignFromText 接受 bDetached 参数来区分分离/附加签名请求,如果您想生成分离签名,则应将其设置为 true

使用示例源文件

为了正确运行示例,您需要安装以下软件

  • .NET 1.1(可以从 Microsoft 网站下载)。
  • CAPICOM 库 v2.1.0.0(可以从 Microsoft 网站下载)。
  • 已安装个人存储(当前用户或计算机存储)的带有私钥的证书。
© . All rights reserved.