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






4.62/5 (17投票s)
2005年2月26日
7分钟阅读

214253

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_Content
和 set_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
。我必须对 ISignedData
和 SignedData
接口进行相同的更改,以避免任何冲突。
然而,仅凭此更改还不够,因为在 .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_Content
或 set_Content
方法。encodingType
参数使我们能够以我们喜欢的任何编码传递或检索内容。SignFromText
接受 bDetached
参数来区分分离/附加签名请求,如果您想生成分离签名,则应将其设置为 true
。
使用示例源文件
为了正确运行示例,您需要安装以下软件
- .NET 1.1(可以从 Microsoft 网站下载)。
- CAPICOM 库 v2.1.0.0(可以从 Microsoft 网站下载)。
- 已安装个人存储(当前用户或计算机存储)的带有私钥的证书。