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

加密互操作性:数字签名

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.98/5 (44投票s)

2008年4月26日

CPOL

21分钟阅读

viewsIcon

230873

downloadIcon

12527

使用 Crypto++、Java 和 C# 签名和验证消息。

引言

Crypto++ 邮件列表偶尔会收到关于在各种库之间创建和验证数字签名的问题。本文将探讨 Crypto++、C# 和 Java 之间的消息签名和验证。此外,C# 示例展示了 AsnKeyBuilderAsnKeyParser,它们允许我们以 PKCS#8 和 X.509 格式序列化和重构密钥。这使我们摆脱了 CLR 使用 RFC 3275 不规则格式进行 XML 序列化的限制。RFC 3275 规定了 XML 签名语法和处理 [13]。第 4.4.1 节和 4.4.2 节规定了与 DSA(和 RSA)参数相关的定义,例如 KeyInfo 元素和 KeyValue 元素。

数字签名算法将用作测试用例。选择它有几个原因。首先是其普及性。其次,正如我们将在下面看到的,由于每个消息的随机变量,相同的密钥和消息会创建不同的签名。接下来,DSA 签名至少以三种不同的格式表示,这导致了必要的转换。最后,我们将使用字符串和流而不是字节数组,这会增加更多的互操作性问题。

下面,我们将看到已签名消息是元组 { 消息,签名 }。当我们验证消息时,我们需要消息、签名和签名者的公钥。这揭示了两个问题领域。第一个问题是密钥及其交换。第二个是定义到底要签名和稍后验证什么。

第一个问题在密码学互操作性:密钥[1] 中进行了探讨。密钥互操作性文章讨论了使用 PKCS#8 和 X.509 以可移植方式在 Crypto++、Java 和 C# 中导入和导出公钥和私钥。

本文将探讨第二个问题——理解将要(或已经)签名的内容。与之前的文章一样,我们详细研究了该过程,以便在出现问题时,我们可以理解原因,然后纠正问题。本文将探讨的主题如下。尽管字符串和流的影响很早就出现,但我们最后讨论这个主题。

  • 数字签名
    • 密钥生成
    • 消息签名
    • 消息验证
  • 签名格式
    • IEEE P1363
    • DER 编码
    • OpenPGP
  • 生成密钥、签名和验证
    • Crypto++
    • Java
    • C#
  • 字符串和流
    • Crypto++
    • Java
    • C#

我们的示例将使用 FIPS 186-2 [11] 中指定的数字签名标准。该标准规定了三种批准的签名方案。我们将使用数字签名(DS)算法,而不是 RSA 数字签名算法(RSASS)或椭圆曲线数字签名算法(ECDSA)。

FIPS 186-2 规定使用 1024 位 p、160 位 q 和 SHA-1 作为哈希。FIPS 186-3 [2] 使用更大的哈希(SHA-2)、更大的 p 值(高达 3072 位)和更大的 q 值(高达 256 位)。FIPS 186-3 目前处于草案状态。

下载次数

有三个可用的下载。每个存档都是用于创建和验证签名的项目。对于那些只想要源代码的人,表 1 列出了感兴趣的下载。

文件名 语言
CryptoPPInteropSign.zip C++/Crypto++
JavaInteropSign.zip Java
CSInteropSign.zip C#
表 1:源代码存档

数字签名

数字签名是手写签名的电子等效物。它使用公钥和私钥对进行操作。签名者使用私钥对消息进行签名,验证者使用公钥确认消息上的签名。

DSA 是 ElGamal 签名系统 [12] 的特例。DSA 的安全性源于离散对数。实际上有两个实例问题:第一个是乘法群 Zp 中的对数,其中适用索引计算方法。第二个是循环子群 q 中的对数问题,其中当前方法以平方根时间运行。

DSA 是附录签名方案。这意味着消息必须呈现给验证函数。这与恢复签名方案形成对比。在恢复系统中,消息被折叠到签名中,因此消息不必与签名一起发送。验证例程将从恢复系统中的签名中提取消息。

密钥生成

DSA 密钥的生成方式如下 [12]。下面,q 的大小由 FIPS 186 固定为 160 位。尽管最初的 FIPS 186 规范 [7] 规定 p 在 512 到 1024 位之间(含),但 FIPS 186-2 [11] 将 p 固定为 1024。这意味着某些库在第三步强制执行 1024 位的位大小。

  1. 选择一个素数 q,使得 2159 < q < 2160
  2. 选择 t 使得 0 ≤ t ≤ 8
  3. 选择一个素数 p,使得 2511+64t < p < 2512+64t,并额外满足 q 整除 (p-1)
  4. 选择 Z*p 中阶为 q 的唯一循环群的生成元 α
  5. 要计算 α,在 Z*p 中选择一个元素 g 并计算 g(p-1)/q mod p
  6. 如果 α = 1,则使用不同的 g 再次执行第五步
  7. 选择一个随机数 a 使得 1 ≤ aq-1
  8. 计算 y = αa mod p

公钥是 (p, q, α, y)。私钥是 a。我们通常会遇到私钥指定为 x

消息签名

要使用附录方案签署任意大小的文档,需要执行两个步骤

  • 对文档进行哈希
  • 使用私钥解密文档的哈希,就像它是密文的一个实例一样

在 DSA 中,签名任意长度的二进制消息 m(文档)的详细信息如下 [12]。请注意,我们正在签名二进制消息(在这个级别没有字符串的概念),并且消息可以是任何长度。由于消息可以是任何长度,因此使用哈希函数 — h(m) 对消息进行摘要。

  1. 生成一个随机的每消息值 k,使得 0 < k < q
  2. 计算 r = (αk mod p) mod q
  3. 如果 r = 0,则使用不同的 k 再次执行第一步
  4. 计算 k-1 mod q
  5. 计算 s = k-1{h(m) + ar} mod q
  6. 如果 s = 0,则使用不同的 k 再次执行第一步

m 上的签名是 (r, s)。消息 m 和 (r, s) 应该发送给验证者。我们需要注意的是,rs 都是 20 字节,因为使用 q(一个 160 位的值)执行了模还原(步骤 2 和 5)。当我们在 Crypto++ 和 C#(它们使用 IEEE P1363 签名格式)以及 Java(它使用签名的 DER 编码)之间开始验证消息时,这将变得很重要。

消息验证

要使用附录方案验证任意大小的文档,需要执行三个步骤

  • 对文档进行哈希
  • 使用签名者的公钥加密先前生成的文档哈希(来自消息签名过程的步骤 2)
  • 验证消息验证过程步骤 1 中恢复的哈希是否与消息验证过程步骤 2 中计算的哈希匹配

上述的简短说明是,我们在删除签名者的加密操作后,将我们计算的文档哈希与签名者计算的文档哈希进行比较。DSA 详细信息如下 [12]。下面,请记住 (r, s) 是二进制消息 m 上的签名,其中 h(m) 摘要任意长度的消息。

  1. 获取公钥 (p, q, α, y)
  2. 验证 0 < r < q 和 0 < s < q(否则拒绝签名)
  3. 计算 w = s-1 mod q
  4. 计算 u1 = w•h(m) mod q
  5. 计算 u2 = rw mod q
  6. 计算 v = (αu1yu2 mod p) mod q

当且仅当 v = r 时,签名才有效。

签名格式

对于我们这些遵循密码互操作性:密钥[1] 的人来说,我们还没有完成标准和格式。Crypto++ 支持三种格式,其中 IEEE P1363 是库的原生格式。其余两种格式是 DER 编码和 OpenPGP。如果我们收到 P1363 以外的格式,我们将使用 Crypto++ 的 DSAConvertSignatureFormat 将签名 (r, s) 转换为 P1363 格式。

回想一下 m 上的签名是 (r, s)。从我们对消息签名的探索中,回想一下 q 是 160 位。rs 都是使用 q 进行模还原的余数,因此每个都是 160 位(20 字节)。

IEEE P1363

Crypto++ 和 C# 都使用 IEEE P1363 [9] 中描述的格式。P1363 签名是 rs 的连接,表示为 r || s。连接导致签名的长度恰好为 40 字节。

DER 编码

Java 使用 (r, s) 的 DER 编码。根据 Java 密码体系结构 API 规范和参考 [8],签名的语法如下。Java 签名与 RFC 3279(互联网 X.509 公钥基础设施的算法和标识符 [14])的 DSS-Sig-Value 一致。请参阅第 2.2.2 节,DSA 签名算法。

SEQUENCE ::= {
  r INTEGER,
  s INTEGER }

看来我们无法从 Java 请求任何其他格式。这在 Crypto++ 中只会造成轻微的不便,因为 Crypto++ 库提供了转换例程。

在 C# 中,我们需要将格式从 DER 转换为 P1363。要为 C# 创建一个 DSASignatureConverter 类,请查看 AsnKeyParser 的代码。转换器没有太多内容——它是一个定义良好的结构。调用 NextSequence 以删除外部序列,然后返回两个解析的整数 rs 的连接。在返回 r || s 之前,验证每个的长度是否为 20 字节(或相应调整)。请参阅下面的讨论 CryptoInteropSign.aspx?msg=3240277#xx3240277xx

OpenPGP

OpenPGP 在 RFC 2440,“OpenPGP 消息格式”[10] 中指定。OpenPGP 使用签名包来表示消息上的签名。在 DSA 的情况下,这些是两个 MPI(多精度整数)rs。第 5.2.2 节指定了版本 3 签名包格式,而第 5.2.3 节指定了版本 4 签名包格式。同样,Crypto++ 库提供了转换例程。

生成密钥、签名和验证

本节将探讨签名和验证过程。密钥生成已在密钥互操作性中讨论过,因此我们将重点关注 DSA 所需的内容。我们还将详细介绍 Crypto++,因为它不如 Java 和 C# 文档齐全。最后,为了实现互操作性,我们将加密转换应用于使用 UTF-8 编码的字符串的字节数组。我们的消息将是宽字符串“Crypto Interop: \u9aa8”,如图 1 所示。

The Message to be Signed
图 1:要签名的消息

Crypto++

密钥生成

为了生成用于消息签名的 DSA 密钥,我们在 Crypto++ 中执行以下操作。尽管我们可以使用 DSA::Signer 构造函数生成密钥,但我们选择推迟,以便我们可以使用该库。生成密钥后,我们通过覆盖 PKCS8PrivateKey 类的 Save 来保存它。PrivateKeyPublicKey 类没有复制构造函数,因此对 AccessPrivateKeyAccessPublicKey 的调用会接收引用。

DSA::Signer signer;
PrivateKey& privateKey = signer.AccessPrivateKey();

privateKey.GenerateRandom( prng );
privateKey.Save(FileSink("private.dsa.cpp.key"));

然后我们构建一个验证器对象。我们这样做是为了我们可以访问该对的公钥。不幸的是,我们无法通过私钥访问它。然后我们使用覆盖的 X509PublicKey::Save 保存它。

DSA::Verifier verifier( signer );
PublicKey& publicKey = verifier.AccessPublicKey();
publicKey.Save(FileSink("public.dsa.cpp.key"));

消息签名

我们使用 Crypto++ 签名消息如下。我们从一个宽字符串开始。然后将消息转换为 UTF-8 字符串并存储在 narrow 中。

// Crypto++ Load Private Key
DSA::Signer signer;
...

// Convert Wide String to UTF-8
wstring wide = L"Crypto Interop: \u9aa8";
string narrow;

WideCharToMultiByte( UTF8, ... );
...

const byte* data = narrow.c_str();
int length = narrow.length();

// Set up for SignMessage
byte* signature = new byte[ signer.MaxSignatureLength() ];

// PGP RandPool
AutoSeededRandomPool prng;
size_t length = signer.SignMessage( prng, data, length, signature );

使用 WideCharToMultiByte 将宽字符串转换为 UTF8 后,我们设置一个缓冲区(signature)来保存签名。签名最多为 MaxSignatureLength 字节。Crypto++ 返回 0x28(40 字节)作为最大 DSA 签名长度。我们预计会这样,因为签名 (r, s) 是两个 160 位余数的连接。

最后,我们调用签名者对象上的 SignMessage。由于每个消息变量 kSignMessage 需要一个伪随机源。该函数返回签名的实际长度(以字节为单位),它也是 MaxSignatureLength。接下来,我们将消息(我们签名的)和签名都保存到文件中。请注意,我们不能假设原始字符串 (L"Crypto Interop: \u9aa8") 将在编译和字符串和流构造后实际签名。

// mfs: message filestream
// sfs: signature filestream
ofstream mfs, sfs;
mfs.open("dsa.cpp.msg", ios_base::binary );
sfs.open("dsa.cpp.sig", ios_base::binary );

// Save Message which was Signed
mfs.write( narrow.c_str(), narrow.length() );

// Save Signature on Message
sfs.write( (const char*)signature, length );

在图 2 中,我们检查文件 out.cpp.msg 中的消息内容。我们看到字符串上的常规 UTF-8 压缩,除了最后一个汉字扩展为三个字节。

Message File Contents
图 2:消息文件内容

接下来,我们检查在同一消息上创建多个签名的结果以及文件 dsa.cpp.sig 的内容。我们使用相同的私钥运行两次例程,并在图 3 中并排比较结果。如果我们回顾消息签名过程,我们需要选择一个随机的每消息值 k。由于 k 是随机的,算法在同一消息上生成不同的签名。

Signatures on Message, Identical Messages
图 3:由于随机 k 导致消息上的不同签名

重要的是,我们必须区分在图 2 中,dsa.cpp.msg 是我们签名的消息,而不是原始字符串。当 Java 或 C# 验证我们的 Crypto++ 消息时,它们将验证此文件中的字节,然后重建原始字符串。

消息签名(DER 编码)

如果我们的消息和签名需要 DER 编码以用于 Java 等系统,我们将执行以下操作。下面,在签名过程之后和将签名写入磁盘之前检查该过程。

// Determine size of required buffer
length = DSAConvertSignatureFormat( NULL, 0, DSA_DER,
    signature.c_str(),signature.length, DSA_P1363 );

// A buffer for the conversion
byte* buffer = new byte[ length ];

// We are P1363 format. Java desires DER encoding
length = DSAConvertSignatureFormat( buffer, length, DSA_DER, 
    signature.c_str(), signature.length(), DSA_P1363 );

消息验证

验证过程将放弃标准库的流,转而使用 Crypto++ FileSourceFileSource 会将文件内容放入 std::string 中。在 Crypto++ 的以下用法中,字符串类似于向量——它是字节的集合。Crypto++ 使用 Unix 管道范式。密钥是源,所以我们需要一个目的地——FileSink

// std::string used as a byte array
string message, signature;

FileSource( "dsa.cpp.msg", true, new StringSink( message ) );
FileSource( "dsa.cpp.sig", true, new StringSink( signature ) );

接下来我们验证消息。回想一下 Crypto++ 是字节进、字节出——因此需要 const byte* 类型转换。

bool result = verifier.VerifyMessage(
    (const byte*)message.c_str(), message.length(),
    (const byte*)signature.c_str(), signature.length() );

最后,转换回宽字符串,结果如图 4 所示。

图 4:消息验证和转换

消息验证(DER 编码)

回想一下,Java 对 m 上的签名 (r, s) 进行 DER 编码。当我们从 Java 收到 DER 编码的签名时,我们执行以下操作。

FileSource( "dsa.java.msg", true, new StringSink( message ) );
FileSource( "dsa.java.sig", true, new StringSink( signature ) );

// First, a buffer for the conversion
size_t length = verifier.SignatureLength();
byte* buffer = new byte[ length ];

// DER encoded from Java. We desire P1363 format
length = DSAConvertSignatureFormat( buffer, length,
    DSA_P1363, signature.c_str(), signature.length(), DSA_DER );

// Reinitialize signature so that it can be used
//   in the verifier below with minimal effort
signature = string( (const char*)buffer, length );
delete[] buffer;

// Verify the Signature on the Message
bool result = verifier.VerifyMessage(
   (const byte*)message.c_str(), message.length(),
   (const byte*)signature.c_str(), signature.length() );

Java

Java 因其更好的文档而更受欢迎,因此以下内容是为了完整性而呈现的。Java 加密扩展 (JCE) 参考指南 [8] 回答了大多数问题。

密钥生成

我们用于在 Java 中创建 DSA 密钥对的代码如下。例程完成后,我们将使用 getBytes 序列化密钥以备将来使用。getBytes 返回对象的默认格式,即 PKCS#8 或 X.509 消息。

KeyPairGenerator kpg = KeyPairGenerator.getInstance("DSA");

kpg.initialize(1024, new SecureRandom());
KeyPair keys = kpg.generateKeyPair();

PrivateKey privateKey = keys.getPrivate();
PublicKey publicKey = keys.getPublic();

消息签名

要使用我们生成的密钥对消息进行签名,我们执行以下操作。

// Retrieve the Private Key
PrivateKey privateKey = LoadPrivateKey("private.dsa.java.key");

// Create the signer object
Signature signer = Signature.getInstance("DSA");
signer.initSign(privateKey, new SecureRandom());

// Prepare the Message
String s = "Crypto Interop: \u9aa8";

// Save the binary of the String which we will sign
byte[] message = s.getBytes("UTF-8");

// Sign the message
signer.update(message);
byte[] signature = signer.sign();

然后我们将字节数组 messagesignature 保存到磁盘,以供其他库进行验证。

消息验证

验证消息如下。下面,我们验证在 C# 中生成的消息。

// Load the public
PublicKey publicKey = LoadPublicKey("public.dsa.cs.key");

// Load the message from file
byte[] message = LoadMessageFile("dsa.cs.msg");    

// Load the signature on the message from file
byte[] signature = LoadSignatureFile("dsa.cs.sig");

// Initialize Signature Object
Signature verifier = Signature.getInstance("DSA");
verifier.initVerify(publicKey);

// Load the message into the verifier
verifier.update(message);

// Verify the Signature on the Message
boolean result = verifier.verify(signature);

与 Crypto++ 和 C# 不同,Java 代码期望消息 m 上的签名 (r, s) 采用 DER 编码格式。尝试验证 P1363 签名会导致编码异常。作为一种解决方法,我们的 Crypto++ 和 C# 源代码将对 Java 的签名进行 DER 编码。

C#

密钥生成

密码学互操作性:密钥 [1] 是对生成、加载和保存密钥的相当全面的处理,因此我们只回顾基础知识。下面我们创建一个密钥对以在 C# 中使用。

CspParameters csp = new CspParameters();
csp.KeyContainerName = "DSA Test (OK to Delete)";

csp.ProviderType = PROV_DSS_DH;    // 13
csp.KeyNumber = AT_SIGNATURE;      // 2

DSACryptoServiceProvider dsa = new DSACryptoServiceProvider(1024, csp);

// Keys
DSAParameters privateKey = dsa.ExportParameters(true);
DSAParameters publicKey = dsa.ExportParameters(false);

由于我们使用了接受 CSP 和整数位数的 DSACryptoServiceProvider,因此构造函数为我们创建了一个密钥对。我们还根据 MSDN 指定 PROV_DSS_DHAT_SIGNATURE(如果愿意,我们实际上可以指定 PROV_DSS)。最后,我们通过调用 ExportParameters 导出密钥。然后我们使用 AsnKeyBuilder 类将 DSAParameters 转换为 PKCS#8 或 X.509 编码密钥以进行序列化。

消息签名

要对消息进行签名,我们执行以下操作。dsa 是上面的服务提供商。请记住,签名是 P1363 格式的,因此 DSASignatureFormatter 执行 rs(两个 20 字节数组)的连接。

DSASignatureFormatter signer = new DSASignatureFormatter(dsa);

//Set the hash algorithm to SHA1.
signer.SetHashAlgorithm("SHA1");

String m = "Crypto Interop: \u9aa8";
Encoding e = Encoding.GetEncoding("UTF-8");
byte[] message = e.GetBytes(m);

// Hash the Message
SHA1 sha = new SHA1CryptoServiceProvider();
byte[] hash = sha.ComputeHash(message);

// Create the Signature for h(m)
byte[] signature = signer.CreateSignature(hash);

然后我们将序列化消息 m 和消息 m 上的签名 (r, s)。

消息验证

有关加载 DSA 密钥的详细信息,请参阅密码互操作性:密钥 [1]。我们使用 AsnKeyParserLoadPublicKeyLoadPrivateKey 如下重建公钥或私钥。AsnKeyParser 处理解析使用 ASN.1 语法描述的密钥的繁重工作。如果密钥格式错误,请准备好捕获 BerDecodeError

AsnKeyParser keyParser = new AsnKeyParser("public.dsa.cs.key");
DSAParameters publicKey = keyParser.ParseDSAPublicKey();

接下来我们打开容器。在这种情况下,DSACryptoServiceProvider 使用只接受 CSP 的构造函数(而不是 CSP 和整数位数)。这向提供程序指示我们不希望生成密钥对。请**注意**,我们使用 PROV_DSS 而不是 PROV_DSS_DH,因为我们不再有 J 和种子等参数。

CspParameters csp = new CspParameters();
csp.KeyContainerName = "DSA Test (OK to Delete)";

csp.ProviderType = PROV_DSS;      // 3
csp.KeyNumber = AT_SIGNATURE;     // 2
// Load key into provider
DSACryptoServiceProvider dsa = new DSACryptoServiceProvider(csp);

dsa.ImportParameters(publicKey);

一旦提供程序在调用 ImportParameters 时接受我们的参数,该练习就变得学术化了。图 5 显示了一个使用 AsnKeyParser 重建的私钥。注意缺少 seed 和参数 J(群因子)。这是由于 PKCS#8 和 X.509——没有序列化参数的规范。

PKCS#8 Key Parameters
图 5:PKCS#8 私钥参数

下面,我们读取构成消息和签名的 byte[] 数组。

byte[] message = LoadMessage();
byte[] signature = LoadSignature();

最后,是验证签名的代码。有趣的是,DSASignatureDeformatter 不接受哈希对象。我们必须提供一个字符串来描述我们对 SetHashAlgorithm 的选择。

SHA1 sha = new SHA1CryptoServiceProvider();
byte[] hash = sha.ComputeHash(message);

// Verifier
DSASignatureDeformatter verifier = new DSASignatureDeformatter(dsa);
verifier.SetHashAlgorithm("SHA1");

bool result = verifier.VerifySignature(hash, signature);

请注意,C# 可能会抛出 CryptographicException,指出“DSA 签名的长度不是 40 字节”。我们预计 Java 会出现这种情况,因为 Java 使用 DER 编码,而 C# 使用 P1363 格式。

DER Encoded Signature
图 6:DER 编码签名异常

在 C# 中,我们需要将格式从 DER 转换为 P1363。要为 C# 创建一个 DSASignatureConverter 类,请查看 AsnKeyParser 的代码。调用 NextSequence 以删除外部序列,然后返回两个解析的整数 rs 的连接。在返回 r || s 之前,验证每个的长度是否为 20 字节(或相应调整)。请参阅下面的讨论 CryptoInteropSign.aspx?msg=3240277#xx3240277xx

如果我们退出 Main 时收到密码异常,声明“密钥集不存在”,我们应该显式处置容器。示例中的多个方法打开了一个名为“DSA Test (OK to Delete)”的容器,每个方法都设置了 PersistKeyInCsp = false。当发生垃圾回收时,每个托管对象都会尝试删除共享的本机资源。为了避免这种情况,我们必须通过在打开资源的方法中调用 DisposeCloseClear 来完成对象,这通常不建议这样做。

Keyset Does Not Exist Exception
图 7:密钥集不存在异常

字符串和流

尽管字符串和流非常方便,但它们在签名和验证消息时给我们带来了最多的问题。这是因为字符串只是最终成为字节数组的编码,然后对其应用加密转换。当我们从字符串转换为字节数组时,通常会在两个地方之一引入不一致。

第一个问题可能是在允许流为字符串转换选择编码时引入的。第二个问题是当程序员(一个实现签名者,另一个实现验证者)为同一字符串选择不同的编码转换时引入的。请注意,当我们显式使用字节数组时,这种情况不会发生。

由于我们有时会使用字符串,我们需要决定使用哪种类型的编码。为此,Unicode 联盟建议将 UTF-8 用于数据交换 [3]。由于几乎所有主要库都支持 UTF-8,我们将始终使用它以获得一致的结果。我们选择 UTF-8 是在互操作性、压缩和信道效率之间的一种折衷。我们还应该意识到,还有其他 Unicode 字符集(例如 SCSU 和 BOCU-1)对于存储和数据交换更有效 [4,5]。

该联盟还定义了 UTF-7 和 UTF-8 等字符集之间如何进行转换。当使用代码页 CP_UTF8 时,转换算法在 Windows 函数 WideCharToMultiByteMultiByteToWideChar 中实现 [6]。

Crypto++

Crypto++ 对于字符串和流是不可知的。对于 Crypto++,它是字节进,字节出。与 Java 和 C# 不同,没有接受高级字符串的方法。但是,C++ 标准库在使用流时确实会影响字符串。

我们已经知道 Visual Studio 使用 UTF-16 编码。在下面,我们将探讨流对 Visual C++ 中字符串的影响。对于我们的第一个示例,请考虑下面列出的程序。

wstring ws = L"crypto";
wofstream ofs;

ofs.open("out.cpp.bin", ios_base::binary);
ofs << ws;

ofs.close();

当我们在图 8 中检查其文件输出时,我们看到尽管我们使用的是标准库的宽版本并指定了二进制模式,但仍然发生了转换。

Wide Stream Binary Output
图 8:宽流二进制输出

为了进一步调查,我们指定了汉字“骨”(U+9AA8)。我们可以使用一个欧洲代码点,但我们不妨深入探讨这个主题。不幸的是,标准 C++ 库在这种情况下完全失败了。在下面的图 9 中,Visual Studio IntelliSense 正确显示了字符,而标准库生成了一个空文件。事实上,删除二进制模式仍然会产生这个结果。这是 Microsoft 流类的一个已知问题。

Failed Wide Stream Binary Output
图 9:宽流二进制输出失败

为了解决这个问题,我们有两种选择。第一个解决方法涉及迭代宽字符串的字符,同时将它们单独写入流中,如下所示。最终效果是我们正在写入一个 UTF-16 流。根据我们选择输出字节的方式,我们实现了一个大端(UTF-16BE)或小端(UTF-16LE)流。单个亚洲字符的结果如图 10 所示。

wstring::const_iterator it = ws.begin();
for( ; it != ws.end; it++ )
{
    // Little Endian       
    ofs.put( (*it & 0x00FF) );
    ofs.put( (*it & 0xFF00)>>8 )
}
UTF-16LE Output
图 10:UTF-16LE 输出

在第二种方法中,我们使用 WideCharToMultiByte [7] 和多字节流。最终效果是我们正在写入一个 UTF-8 流。更正后的程序如下所示。图 11 显示了输出,即 UTF-8 编码的宽字符串 - 0xE9 0xAA 0xA8。

wstring ws = L"\u9aa8";
char* utf = NULL;

// UTF-8 Encode
int nChars = WideCharToMultiByte( CP_UTF8, 0, ws.c_str(), -1, NULL, 0, NULL, FALSE );
utf = new char[ nChars ];
WideCharToMultiByte( CP_UTF8, 0, ws.c_str(), -1, utf, nChars, NULL, FALSE );

ofstream ofs;
ofs << utf;
...
UTF-8 Output
图 11:UTF-8 输出

Java

现在我们将探讨 Java 中发生的情况。我们有两种情况需要检查:使用流的 writeUTF 方法写入字符串;以及在字符串上调用 getBytes 的效果。首先我们检查下面的 writeUTF

DataOutputStream dos = new DataOutputStream( new FileOutpuStream("out.java.bin"));

String s = "crypto";

dos.writeUTF(s);

在图 12 中,我们看到 DataOutputStream 的方法以 16 位长度作为编码前缀。对于互操作性而言,这可能是一个糟糕的选择。

Java DataOutputStream Output
图 12:Java DataOutputStream 输出

接下来,我们修改 Java 程序以探索从 getBytes 返回的各种字节数组。

DataOutputStream dos = new DataOutputStream( new FileOutputStream("out.java.bin"));

String s = "crypto";
byte[] b = s.getBytes();

dos.write(b, 0, b.length);

使用 getBytes,我们的输出与图 13 相同——字符串已使用平台的默认字符集进行编码。接下来我们运行程序,请求 UTF-16 编码

byte[] b = s.getBytes("UTF16");

在图 13 中,我们看到一个带有字节顺序标记的大端数组。同样,对于互操作性而言,这可能是一个糟糕的选择。

Java getBytes(UTF16) Output
图 13:Java getBytes("UTF16") 输出

当我们使用 getBytes("UTF8")getBytes("UTF32") 运行 Java 程序时,我们发现没有字节顺序标记写入流。在 UTF-32 的情况下,数组再次是大端。我们观察到使用 UTF-8 的预期(和期望的)结果。

C#

我们的第一个 C# 示例检查了使用 StreamWriter 写入字符串时使用默认编码的结果。程序如下所示,其输出如图 14 所示。

using (TextWriter writer = new StreamWriter("out.cs.bin"))
{
    String s = "crypto";

    writer.Write(s);
}
C# StreamWriter Output
图 14:C# StreamWriter 输出

与 Java 一样,我们观察到流使用默认编码 UTF-8。接下来我们修改程序以使用 UTF-16 编码。

using (BinaryWriter writer = new BinaryWriter(
    new FileStream("out.cs.bin", FileMode.Create, FileAccess.ReadWrite)))
{
  String s = "crypto";
  Encoding e = Encoding.GetEncoding("UTF-16");

  byte[] b = e.GetBytes(s);

  writer.Write(b);
}

结果如图 15 所示。我们观察到写入了一个小端流。此外,与 Java 不同,没有字节顺序标记。

C# BinaryWriter Output
图 15:C# BinaryWriter 输出

与 Java 一样,我们必须在使用 UTF-16 时处理字节顺序。这再次引导我们使用 UTF-8,它没有字节顺序和字节顺序标记问题。

致谢

  • Wei Dai for Crypto++ 及其在 Crypto++ 邮件列表上的宝贵帮助
  • A. Brooke Stephens 博士,他为我打下了密码学基础

校验和

  • CryptoPPInteropSign.zip
    • MD5: 2647DE3E5E06A07F8CD05F911D75DC3B
    • SHA-1: 74DBED5386D64C9041EE66CFCF884C79F8961B0C
  • JavaInteropSign.zip
    • MD5: BA74E602379395177681172EFC73591E
    • SHA-1: B38D7755F6D6D9D4C6ADBE698EF77B771236AB4C
  • CSInteropSign.zip
    • MD5: 20A78F6E7817F523923CE2E0B21E95E9
    • SHA-1: 2D69E88935B549993D974C8A41D0091BE3547C5A

参考文献

[1] J. Walton,《密码互操作性:密钥》,2008 年 4 月,CryptoInteropKeys.aspx。

[2] FIPS 186-3 草案,《数字签名标准》,http://csrc.nist.gov/publications/drafts/fips_186-3/Draft-FIPS-186-3%20_March2006.pdf。

[3] Unicode 联盟,《用于处理的 UTF-16》,Unicode 技术说明 #12,http://unicode.org/notes/tn12/。

[4] Unicode 联盟,《Unicode 压缩概览》,Unicode 技术说明 #14,http://unicode.org/notes/tn14/。

[5] Unicode 联盟,《Unicode 文本快速压缩算法》,Unicode 技术说明 #31,http://unicode.org/notes/tn31/。

[6] D. Schmitt,《Microsoft Windows 国际化编程》,Microsoft Press,ISBN 1-5723-1956-9。

[7] MSDN,《WideCharToMultiByte》,http://msdn2.microsoft.com/en-us/library/ms776420(VS.85).aspx。

[8] 《Java 密码学体系结构 (JCA) 参考指南》,http://java.sun.com/javase/6/docs/technotes/guides/security/crypto/CryptoSpec.html。

[9] IEEE P1363,《公钥密码学标准规范》。

[10] RFC 2440,《OpenPGP 消息格式》,1998 年 11 月,http://www.ietf.org/rfc/rfc2440.txt。

[11] FIPS 186-2,《数字签名标准》,2007 年 1 月,http://csrc.nist.gov/publications/fips/fips186-2/fips186-2-change1.pdf。

[12] A. Menenzes 等人,《应用密码学手册》,CRC Press,ISBN 0-8493-8523-7,第 451-2 页。

[13] RFC 3275,《XML-Signature 语法和处理》,2002 年 3 月,http://www.ietf.org/rfc/rfc3275.txt。

[14] RFC 3279,《互联网 X.509 公钥基础设施的算法和标识符》,2002 年 4 月,http://www.ietf.org/rfc/rfc3279.txt。

© . All rights reserved.