加密互操作性:密钥





5.00/5 (61投票s)
以 PKCS#8 和 X.509 格式导入和导出加密密钥,使用 Crypto++、C# 和 Java。
- 下载 C++ 代码 - 4.03 KB
- 下载 C# 代码 - 32.2 KB
- 下载 Java 代码 - 9.49 KB
下载文件的校验和可在此处找到 此处.
引言
Crypto++ 邮件列表偶尔会收到关于从其他库导入公钥和私钥,以及导出密钥以供外部库使用的询问。本文将演示如何在 Crypto++、C# 和 Java 之间迁移 RSA 密钥材料。此外,我们将探讨在 C# 中加载 RSA 和 DSA 密钥的特殊情况,因为 CLR 会带来一些有趣的问题。
表面上看,我们期望 Crypto++ 会是最难的,而 C# 和 Java 会是最容易的。实际上,Crypto++ 和 Java 是最容易实现互操作性的库。这是因为 CLR 缺乏对密钥对的标准化的序列化支持。
当我们阅读诸如 Porting Java Public Key Hash to C# .NET [14] 等文章时,我们就会发现 C# 可能造成的麻烦。开发人员被迫偏离成熟的 PKCS #8 和 X.509 密钥格式,以便 C# 可以按照 RFC 3275 中的 XML 格式导入或导出密钥,XML-Signature Syntax and Processing [26]。第 4.4.1 和 4.4.2 节指定了与 DSA(和 RSA)参数相关的定义,例如 KeyInfo
元素和 KeyValue
元素。为了克服 C# 中的这一限制,我们将使用 AsnKeyBuilder
和 AsnKeyParser
,它们允许我们将密钥序列化并在 PKCS#8 和 X.509 中重构。
我们将在下面详细探讨该过程。这样做是为了在出现问题时,我们能够快速地识别和纠正问题。在此过程中,将提供对各种标准的引用,供在出现问题时进一步阅读。将要讨论的主题列在下面。
- PKCS 和 X.509
- PKCS
- X.509
- 密钥语法
- PublicKeyInfo
- PrivateKeyInfo
- EncryptedPrivateKeyInfo
- RSAPublicKey
- RSAPrivateKey
- 密钥格式
- RSA 公钥
- RSA 私钥
- DSA 公钥
- DSA 私钥
- RSA 密码系统
- 公钥和私钥生成
- 公钥加密
- 私钥解密
- RSAPrivateKey 语法与 RSA 私钥
- 生成、保存和加载密钥
- Crypto++
- Java
- C#
- ASN.1
- INTEGER
- OBJECT IDENTIFIER
- BIT STRING
- OCTET STRING
- NULL
- SEQUENCE
虽然理解 ASN.1 对于读写密钥是必需的,但它被放在最后介绍,因为它仅用于完整性。在访问需要基本 ASN.1 知识的其它章节时,请理解它是一个表示层协议,就像任何其他表示层协议一样,例如 Base64 编码和解码。另外,请记住 ASN.1 类似于一种编程语言——有语言、语法和产生式。
最后,一个 ASN.1 转储器将非常有用。像 Objective System's ASN.1 View 这样的图形工具,或 Peter Guttman 的 dumpasn1 这样的命令行工具都很好用。在 Guttman 的页面上,请阅读 X.509 风格指南。花时间访问 Michel Gallant 的 JavaScience。Gallant 博士为 MSDN 在 .NET 加密领域撰写了多篇文章,并提供 .NET 和 Java 源代码。另外,您可能还对 加密互操作性:数字签名 [22] 感兴趣,该文章探讨了在 C++、Java 和 C# 之间使用 DSA 签名时遇到的问题。
下载次数
文章开头有三个可用的下载。每个存档都是一个用于创建和验证的项目。对于只想获取源代码的人来说,表 1 标明了感兴趣的下载。
文件名 | 语言 |
CryptoPPInteropKeys.zip | C++/Crypto++ |
JavaInteropKeys.zip | Java |
CSInteropKeys.zip | C# |
表 1:源代码存档 |
PKCS 和 X.509
RSA 的公钥和私钥可以使用 PKCS#1、PKCS#8 和 X.509 在系统之间移动。本节将探讨每个标准定义的格式。对于有兴趣了解完整规范的人来说,RSA 的 FTP 站方便地提供了 PKCS 系列。如果 PKCS#1, v1.5 [6] 已经过时(例如,多素数 RSA),请参阅 RFC 3447 以了解该标准的 2.1 版本 [21]。关于 ITU-T X 系列出版物(包括 X.509 和 X.690),请访问 ITU 网站。
PKCS
PKCS 是 Public Key Cryptography Standard(公钥密码学标准)的缩写。该标准由 RSA labs [4] 维护。目前有 10 个标准,编号为 1 到 15(PKCS#2 和 PKCS#4 已合并到 PKCS#1;PKCS#13 和 PKCS#14 被列为开发中)[5]。在这些标准中,PKCS#1: RSA Cryptography Standard 和 PKCS#8: Private-Key Information Syntax Standard 比较重要。
使用 ASN.1,PKCS#1 定义了 RSAPublicKey
和 RSAPrivateKey
类型。然而,RSAPublicKey
和 RSAPrivateKey
并不足够——这些类型仅仅定义了整数序列。为了达到下一级抽象(正确的“打包’),我们需要 PKCS#8 来定义 PrivateKeyInfo
,以及 X.509 来定义 PublicKeyInfo
[2]。
X.509 证书
公钥证书是由一个实体发出的数字签名声明,表明另一个实体的公钥是真实的。已签名的证书将实体绑定到公钥。证书允许我们(用户)确认公钥所有者的身份。此外,它还允许我们(用户)确认公钥的真实性。如果公钥被篡改,证书上的签名将不再有效。实体信息被篡改或更改时也是如此。
公钥证书最常见的形式之一是 X.509。我们在互联网上经常使用 X.509 证书。在 Web 浏览器和 SSL 的情况下,我们在交易中通常不知道我们正在信任谁。但我们信任一个认证机构(CA),如 Verisign 或 Comodo,它们签发了持有公钥的另一方的证书。因此,Verisign 或 Comodo 通过签署该组织的证书,来证明提供公钥的人、团体或组织的身份。此外,由于认证机构签发了公钥证书,我们就知道该公钥是真实的。我们可以追溯 X.509 证书签名者的谱系,直到一个我们信任的 CA。在根源上,该机构签署了自己的证书,这使得一切都 OK。
密钥语法
为了实现互操作性,我们使用 PKCS#1、PKCS#8 和 X.509 中提供的四种格式。通过使用 PublicKeyInfo
和 PrivateKeyInfo
,我们可以通过指定所需的 OID(对象标识符)来编码几乎所有密码系统的公钥和私钥。要获取用于指定 AlgorithmIdentifier
的算法和标识符列表,请参阅 RFC 3279 [17] 和 RFC 4055 [18]。
![]() ![]() 图 1:逻辑密钥布局
|
PublicKeyInfo
尽管 X.509 比我们需要的要繁重,但它提供了第一种格式:PublicKeyInfo
[7]。确切地说,X.509 将其定义为 SubjectPublicKeyInfo
,公钥是 SubjectPublicKey
。我们选择删除 'Subject' 以达到美观和与 PKCS 的一致性。
PublicKeyInfo ::= SEQUENCE {
algorithm AlgorithmIdentifier,
PublicKey BIT STRING }
根据 X.509,AlgorithmIdentifier
定义为
AlgorithmIdentifier ::= SEQUENCE {
algorithm ALGORITHM.id,
parameters ALGORITHM.type OPTIONAL }
Algorithm
是 RSA 的 OID,即 rsaEncryption
(1.2.840.113549.1.1.1)。可选的 Parameters
通常在 RSA 中不存在。然而,它们在 DSS 中存在,我们将在下面的密钥格式中进行探讨。
AlgorithmIdentifier
的语法更复杂。它被描绘得非常宽泛。ALGORITHM
是一个类,ALGORITHM.id
是一个类型。ALGORITHM.type
是一个开放类型,具有额外的约束。施加的约束取决于 ALGORITHM.id
的值。这意味着如果 ALGORITHM.id
是一个 OID,ALGORITHM.type
将采用特定的语法。如果 ALGORITHM.id
是第二个不同的 OID,ALGORITHM.type
将采用一个[可能]不同的语法。
最后,PublicKey
(下面将讨论)被编码为一个位串。细节很重要:公钥被编码为一个位串,而私钥则不是。
PrivateKeyInfo
转向 PKCS #8,我们找到了第二种格式 PrivateKeyInfo
的语法,如下所示 [9]。
PrivateKeyInfo ::= SEQUENCE {
version Version,
privateKeyAlgorithm PrivateKeyAlgorithmIdentifier,
privateKey PrivateKey,
attributes [0] IMPLICIT Attributes OPTIONAL }
和
Version ::= INTEGER
PrivateKeyAlgorithmIdentifier ::= AlgorithmIdentifier
PrivateKey ::= OCTET STRING
AlgorithmIdentifier
再次是 RSA 的 OID,即 rsaEncryption
(1.2.840.113549.1.1.1)。PublicKeyInfo
OID 和 PrivateKeyInfo
OID 之间没有区别。两者都指定 rsaEncryption
。
回想一下,公钥被编码为位串。PrivateKeyInfo
将私钥编码为字节串。
EncryptedPrivateKeyInfo
最后,PKCS#8 定义了 EncryptedPrivateKeyInfo
,它标准化了加密的私钥(适合存储)。虽然最佳实践建议我们使用它,但我们不会深入探讨 EncryptedPrivateKeyInfo
。
EncryptedPrivateKeyInfo ::= SEQUENCE {
encryptionAlgorithm EncryptionAlgorithmIdentifier,
encryptedData EncryptedData }
和
EncryptionAlgorithmIdentifier ::= AlgorithmIdentifier
EncryptedData ::= OCTET STRING
EncryptedData
是加密私钥信息(RSAPrivateKey
)的结果 [8]。如果感兴趣,请参阅 PKCS#8 的第 7 节。EncryptionAlgorithmIdentifier
指定了加密算法(请参阅 PKCS #5, Password-Based Encryption Standard [19])。由于 PKCS #5 已经过时(使用 8 字节算法和 MD2 和 MD5),我们还需要查阅 RFC 2898, Password-Based Cryptography Specification, Version 2.0 [20] 以获取最新规范。
RSAPublicKey
PKCS #1 定义了第三种语法 RSAPublicKey
,如下所示 [6]。对于追求细节的人来说,该类型在 X.509 中指定,并在 PKCS#1 中保留以兼容。
RSAPublicKey ::= SEQUENCE {
modulus INTEGER,
publicExponent INTEGER }
RSAPrivateKey
进一步阅读 PKCS #1,我们找到了最后一种语法 RSAPrivateKey
[6]。
RSAPrivateKey ::= SEQUENCE {
version Version,
modulus INTEGER,
publicExponent INTEGER,
privateExponent INTEGER,
prime1 INTEGER,
prime2 INTEGER,
exponent1 INTEGER,
exponent2 INTEGER,
coefficient INTEGER }
和
Version ::= INTEGER
密钥格式
本节将介绍密钥的程序化结构,以有意义的方式将前面的章节联系起来。如果细节决定成败,那么关键在于找到 PublicKeyInfo.PublicKey
和 PrivateKeyInfo.PrivateKey
的语法(规范)。
RSA 公钥
SEQUENCE // PublicKeyInfo
+- SEQUENCE // AlgorithmIdentifier
+- OID // 1.2.840.113549.1.1.1
+- NULL // Optional Parameters
+- BITSTRING // PublicKey
+- SEQUENCE // RSAPublicKey
+- INTEGER(N) // N
+- INTEGER(E) // E
RSA 私钥
SEQUENCE // PrivateKeyInfo
+- INTEGER // Version - 0 (v1998)
+- SEQUENCE // AlgorithmIdentifier
+- OID // 1.2.840.113549.1.1.1
+- NULL // Optional Parameters
+- OCTETSTRING // PrivateKey
+- SEQUENCE // RSAPrivateKey
+- INTEGER(0) // Version - v1998(0)
+- INTEGER(N) // N
+- INTEGER(E) // E
+- INTEGER(D) // D
+- INTEGER(P) // P
+- INTEGER(Q) // Q
+- INTEGER(DP) // d mod p-1
+- INTEGER(DQ) // d mod q-1
+- INTEGER(Inv Q) // INV(q) mod p
DSA 密钥
如果我们对 Digital Signature Standard [16] 和 IEEE 的 P1363 [23] 中定义的数字签名算法密钥感兴趣,那么密钥将如下所示。DSA 使用 OptionalParameters
来表示曲线的域参数(回想一下,RSA 中的 OptionalParameters
为 null)。DSA 域参数的语法可以在 RFC 3279 [17] 及其补充 RFC 4055 [18] 中找到。DSAPublicKey
和 DSAPrivateKey
的语法如下所示。请注意,两种密钥都缺少 RSA 密钥中存在的额外 SEQUENCE
。最后,DSAPrivateKey
不包含版本字段。
Dss-Parms ::= SEQUENCE {
p INTEGER,
q INTEGER,
g INTEGER }
DSA 公钥
SEQUENCE // PublicKeyInfo
+- SEQUENCE // AlgorithmIdentifier
+- OID // 1.2.840.10040.4.1
+- SEQUENCE // DSS-Params (Optional Parameters)
+- INTEGER(P) // P
+- INTEGER(Q) // Q
+- INTEGER(G) // G
+- BITSTRING // PublicKey
+- INTEGER(Y) // DSAPublicKey Y
DSA 私钥
SEQUENCE // PrivateKeyInfo
+- INTEGER // Version
+- SEQUENCE // AlgorithmIdentifier
+- OID // 1.2.840.10040.4.1
+- SEQUENCE // DSS-Params (Optional Parameters)
+- INTEGER(P) // P
+- INTEGER(Q) // Q
+- INTEGER(G) // G
+- OCTETSTRING // PrivateKey
+- INTEGER(X) // DSAPrivateKey X
RSA 密码系统
RSA 是 Ron Rivest、Adi Shamir 和 Leonard Adleman 的作品。该系统于 1977 年开发,并由麻省理工学院获得专利。RSA 专利已于 2000 年 9 月到期,并随后进入公共领域。尽管 Rivest、Shamir 和 Adleman 通常被认为是发现者,但 Clifford Cocks(GCHQ - 英国 NSA 对等机构的首席数学家)早在 1973 年就描述了该系统。然而,Cocks 没有发表,因为这项工作被认为是机密的,所以功劳归于 Rivest、Shamir 和 Adleman。
公钥和私钥生成
要生成密钥对,我们执行以下步骤 [10]
- 生成两个大的随机(且不同)素数 p 和 q
- 计算 n = pq 和 Φ = (p-1)(q-1)
- 选择一个随机整数 e,使其具有以下属性
- 1 < e < Φ
- gcd(e, Φ) = 1
- 计算 d,使其具有以下属性
- 1 < d < Φ
- ed ≡ 1 mod Φ
当我们计算 d 时,我们将使用扩展欧几里得算法 [10]。e 被称为加密指数,d 被称为解密指数。公钥是 (n, e);私钥是 d。
公钥加密
要加密消息,我们执行以下步骤 [10]
- 获取实体的公钥
- 将消息表示为整数 m,使得 0 ≤ m ≤ n-1
- 计算 c = me mod n
然后我们将密文 c 发送给该实体。
私钥解密
要解密消息,我们执行以下步骤 [10]
- 计算 m = cd mod n
RSAPrivateKey 语法与 RSA 私钥
从《公钥和私钥生成》中,我们知道我们的密钥包括 d、e 和 n,但 RSAPrivateKey
语法指定了八个参数。摘自 PKCS#1
RSA 私钥在逻辑上只包含模数 n 和私钥指数 d。公钥指数 e 的存在是为了方便地从私钥派生出公钥。值 p、q、d mod (p-1) 和 q-1 mod p 的存在是为了提高效率……一个不包含所有额外值的私钥语法,如果公钥已知,就可以轻松地转换为此处定义的语法。[12]
生成、保存和加载密钥
在可以通过 SneakerNet 交换密钥之前,我们必须生成一个密钥对。我们生成的密钥是临时的,可以删除。有时,我们会遇到“临时”或“瞬时”等术语,用于描述可丢弃的密钥。
最佳实践建议我们为密钥交换和签名使用不同的密钥。这被称为密钥分离 [11]。(请记住,我们忽略了我们将私钥保存在 PrivateKeyInfo
格式而不是 EncryptedPrivateKeyInfo
格式的事实)。这意味着我们至少应该有两组密钥对。虽然 Crypto++ 不区分,但 Java 和 C# 会询问我们的意图,因为 Java 和 C# 希望我们将密钥放入“密钥存储”中以供将来使用。
下面,我们将以面向库的方式而不是按每个步骤(生成、保存和加载)按库进行检查代码。更容易按库分组所有操作并整体讨论该库。在所有情况下,密钥的文件名都将以 <<type>>.rsa.<library>.key 的形式命名。因此,使用 Crypto++ 生成的公钥将被命名为 public.rsa.cpp.key,而 Java 私钥将被命名为 private.rsa.java.key。
Crypto++
在 Crypto++ 中生成 RSA 密钥相对简单,一旦我们知道了需要使用的类。在 Crypto++ 中,私钥由 InvertibleRSAFunction
表示,而公钥由 RSAFunction
表示。尽管 Crypto++ 提供了许多 typedef
s 来简化库的使用,但明显缺少 RSAPublicKey
和 RSAPrivateKey
。为此,我们将执行 typedef
,以便代码看起来更符合我们的习惯。
RSAFunction
类继承自 X509PublicKey(以及其他类)。InvertibleRSAFunction
类继承自 RSAFunction
和 PKCS8PrivateKey
(以及其他类)。这些类提供了常用的函数,例如 GetPrime1
、GetPrime2
、GetModulus
和 GetPrivateExponent
。
密钥生成
下面的代码使用默认参数(e = 17)创建 RSA 公钥和私钥。与 Java 一样,我们正在使用的构造函数需要一个伪随机数生成器。
// Convenience
typedef InvertibleRSAFunction RSAPrivateKey;
typedef RSAFunction RSAPublicKey;
// PGP RandPool design
AutoSeededRandomPool prng;
// Private Key
RSAPrivateKey privateKey;
privateKey.Initialize( prng, 1024 /*, e=17*/)
// Public Key
RSAPublicKey publicKey( privateKey );
如果我们想通过参数初始化 RSAPrivateKey
,我们还有两个额外的 Initialize
函数(为简洁起见,已移除 const
引用)
Initialize(Integer n, Integer e, Integer d)
Initialize(Integer n, Integer e, Integer d, Integer p,
Integer q, Integer dp, Integer dq, Integer u)
RSAPublicKey
有一个构造函数和一个初始化器。唯一的构造函数接受一个 RSAFunction
参数。回想一下,InvertibleRSAFunction
类继承自 RSAFunction
。这解释了为什么下面的代码可以编译。
RSAPublicKey publicKey( privateKey );
如果我们想用参数 e
和 n
初始化密钥,我们将使用
Initialize(Integer n, Integer e)
保存密钥
回想一下,InvertibleRSAFunction
类和 RSAFunction
类分别继承自 X509PublicKey
或 PKCS8PrivateKey
。X509PublicKey
和 PKCS8PrivateKey
都继承自 ASN1Object
类,该类提供了 Load
和 Save
函数。InvertibleRSAFunction
类和 RSAFunction
重写了 Load
和 Save
。因此,序列化我们的 Crypto++ 密钥的代码是
// Save as PKCS #8 (using ASN.1 DER Encoding )
privateKey.Save( FileSink("private.rsa.cpp.key") );
// Save as X.509 (using ASN.1 DER Encoding )
publicKey.Save( FileSink("public.rsa.cpp.key") );
Crypto++ 使用 Unix 管道范例,所以我们需要一个目的地(密钥是源)。这就是 FileSink
的作用。FileSink
会将密钥的内容放入一个文件中。我们可以使用任何 ASN.1 转储程序检查密钥,如下面的图 2 所示。
![]() 图 2:使用 dumpasn1 部分私钥转储
|
在图 2 中,我们观察到几项。首先,在文件偏移量 0 处,我们看到一个序列。有 628 个内容字节。偏移量 0 标记了 PrivateKeyInfo
的开始。在文件位置 4,我们看到版本(PrivateKeyInfo.version
)。接下来,我们看到一个序列。在序列之后,我们在偏移量 9 处看到 PrivateKeyInfo.privateKeyAlgorithm
(RSA 的 OID)。在位置 22,我们看到包裹 RSAPrivateKey
的字节串。
偏移量 26 开始是 RSAPrivateKey
。有 602 个内容字节。在偏移量 30 处,我们有 RSAPrivateKey.version
(0 = v1998)。偏移量 33 开始是 RSAPrivateKey.Modulus
。我们注意到模数是 129 字节,尽管我们生成了一个 1024 位(128 字节)的密钥。这是因为 Crypto++ 正确地添加了一个前导的 0x00 字节(回想一下 ASN.1 整数是有符号的)。最后,第 165 行转储了 RSAPrivateKey.publicExponent
,它是 17。其余字段不可见。
加载密钥
加载密钥同样简单。下面,我们加载在 C# 中生成的密钥。我们使用 FileSource
的一个 true
参数来帮助编译器选择 FileSource
构造函数。
// Load as PKCS #8 (using ASN.1 BER Decoding )
privateKey.Load( FileSource( "private.rsa.cs.key", true ) );
// Load as X.509 (using ASN.1 BER Decoding )
publicKey.Load( FileSource( "public.rsa.cs.key", true ) );
如果我们不小心使用了 DEREncodePublicKey
或 DEREncodePrivateKey
(如下所示,而不是 Save
)
publicKey.DEREncodePublicKey ( FileSink("public.rsa.cpp.key") );
我们将产生错误的结果。这是因为只写入了 ASN.1 整数(PKCS#1 RSAPrivateKey
或 PKCS#1 RSAPublicKey
)。包装器——PrivateKeyInfo
(PKCS#8) 和 PublicKeyInfo
(X.509)——不存在。
![]() 图 3:RSAPublicKey(非 PublicKeyInfo)的序列化
|
在图 3 中,我们注意到存在两个 ASN.1 编码的整数(n
和 e
)。然而,诸如外部 ASN.1 位串和 RSA 的 OID (1.2.840.113549.1.1.1) 等元素却缺失了。
为了完整起见,我们将再次调用错误的函数来加载密钥。我们尝试使用 BERDecodePublicKey
加载一个编码的 RSAPublicKey
。然而,文件中实际存在的是一个 PublicKeyInfo
消息。我们期望 Crypto++ 会抛出一个“BER Decode Error”,事实也确实如此。
publicKey.BERDecodePublicKey(FileSource( "public.rsa.cpp.key", true ));
Java
Java 因其更好的文档而更受欢迎,因此以下内容仅供参考。Java Cryptography Extension (JCE) Reference Guide [13] 回答了大部分问题。
密钥生成
要生成我们的 RSA 密钥,我们执行以下步骤。
// Java Cryptography Provider
KeyPairGenerator kpg = KeyPairGenerator.getInstance("RSA");
// Initialize and Generate
kpg.initialize(1024, new SecureRandom());
KeyPair keys = kpg.generateKeyPair();
// Retrieve keys
RSAPrivateKey privateKey = (RSAPrivateKey)keys.getPrivate();
RSAPublicKey publicKey = (RSAPublicKey)keys.getPublic();
保存密钥
在 Java 中保存密钥稍微复杂一些,因为我们必须构建一个 FileOutputStream
。下面,getEncoded
以其主要编码格式(PKCS#8 或 X.509)返回密钥。
DataOutputStream dos = new DataOutputStream( new FileOutputStream("private.rsa.java.key"));
dos.write(privateKey.getEncoded);
dos.close();
加载密钥
Java 中加载密钥的代码如下。需要 FileInputStream
,因为使用 InputStream
的 available
只报告可以非阻塞读取的字节数。
// Retrieve bytes
FileInputStream fis = new FileInputStream("private.rsa.java.key")
DataInputStream dis = new DataInputStream(fis);
byte[] octets = new byte[(int) fis.length()];
dis.readFully(octets);
dis.close();
// Reconstruct Key
PKCS8EncodedKeySpec spec = new PKCS8EncodedKeySpec(b);
KeyFactory factory = KeyFactory.getInstance("RSA");
RSAPrivateKey privateKey = (RSAPrivateKey)factory.generatePrivate(spec);
上面,RSAPrivateKey
提供了对 d
和 n
的访问。因为我们使用了完整的语法写入了私钥,我们实际上可以使用
RSAPrivateCrtKey privateKey = (RSAPrivateCrtKey)factory.generatePrivate(spec);
如果我们正在读取公钥,上面的代码将修改如下。
FileInputStream fis = new FileInputStream("public.rsa.java.key");
DataInputStream dis = new DataInputStream(fis);
...
X509EncodedKeySpec spec = new X509EncodedKeySpec(b);
KeyFactory factory = KeyFactory.getInstance("RSA");
RSAPublicKey publicKey = (RSAPublicKey)factory.generatePublic(spec);
C#
C# 是最复杂的例子,紧随其 CAPICOM 遗产。此外,我们在导入密钥时必须遵循一些不成文的规则。本节将尝试探讨我们在 CLR 范围内工作时面临的问题。
RSA 密钥生成
我们的 C# 第一个任务是生成密钥对。根据 MSDN,我们为 RSA 设置了 ProviderType
和 KeyNumber
。请注意,尝试将 ProviderType
设置为 PROV_RSA_FULL
或 KeyNumber
设置为 AT_SIGNATURE
以外的值,都会导致 CryptographicException
,并提示 'Provider type not defined',因为 CLR 实现的 RSACryptoServiceProvider
。
![]() 图 4:Provider Type Not Defined
|
我们使用 RSACryptoServiceProvider
,它接受 CspParameters
和一个整数位计数,因为我们希望库为我们创建密钥对。我们还配置了其他参数以满足我们的需求,例如容器名称。然后我们调用 ExportParameters
来检索密钥。
CspParameters csp = new CspParameters();
csp.KeyContainerName = "RSA Test (OK to Delete)";
csp.ProviderType = PROV_RSA_FULL; // 1
csp.KeyNumber = AT_KEYEXCHANGE; // 1
RSACryptoServiceProvider rsa = new RSACryptoServiceProvider(1024, csp);
RSAParameters privateKey = rsa.ExportParameters(true);
RSAParameters publicKey = rsa.ExportParameters(false);
请注意,如果我们使用的是 Win32 API,我们将检索一个 PUBLICKEYBLOB
(或 PRIVATEKEYBLOB
),其中包含公钥或私钥 blob。Win32 blob 将是小端序,我们之后需要将其转换为大端序。当我们使用 C# 访问 RSAParameters
时,它是以大端序返回的,因此在序列化之前我们不需要反转字节序。
保存 RSA 密钥
接下来,我们需要序列化密钥。由于 C# 不支持我们的需求(除非我们想要 XML),我们使用 AsnKeyBuilder
类来提供此功能。公平地说,对于 CLR,我们可以使用 P/Invoke 和 Win32 API。Gallant 博士在 JavaScience 上演示了这种技术。
AsnKeyBuilder
提供了四个静态方法来准备密钥进行序列化。有两个方法适用于 RSAParameters
,另外两个方法适用于 DSAParameters
。两个 RSA 方法是
PublicKeyToX509(RSAParameters publicKey)
PrivateKeyToPKCS8(RSAParameters privateKey)
每个方法都只是从 RSAParameters
中提取相关值,并将它们打包到合适的 ASN.1 对象中。每个方法都返回一个 AsnMessage
,它是底层字节数组的轻量级包装器。为了简单起见,我们使用 AsnMessage
而不是 PKCS#8 和 X.509 密钥的独立类。要保存密钥,我们将执行以下操作
AsnMessage privateEncoded = PrivateKeyToPKCS8(privateKey);
SaveEncodedKey("private.rsa.cs.key", privateEncoded.GetBytes());
AsnMessage publicEncoded = PublicKeyToX509(publicKey);
SaveEncodedKey("public.rsa.cs.key", publicEncoded.GetBytes());
SaveEncodedKey
方法只是包装了一个 BinaryWriter
internal static void SaveEncodedKey(String filename, byte[] encoded)
{
using (BinaryWriter writer = new BinaryWriter(
new FileStream(filename, FileMode.Create, FileAccess.ReadWrite)))
{
writer.Write(encoded);
}
}
在图 5 中,我们检查使用 C# 生成的私钥
![]() 图 5:C# 生成的 RSA 私钥
|
由于 CLR 的本机密钥格式是 RSAParameters
或 DSAParameters
,我们可能会错误地将密钥保存为错误的格式(参数没有公钥或私钥的概念——它包含所有信息)。写入操作将成功——问题将在检索密钥时才变得明显。下面是一个将密钥保存为错误格式的示例。
// Should be using PrivatKeyToPKCS8(...)
AsnMessage privateEncoded = PublicKeyToX509(privateKey);
SaveEncodedKey("private.rsa.cs.key", privateEncoded.GetBytes());
加载 RSA 密钥
接下来,我们使用 Java 加载私钥以验证编码的正确性。请注意图 5 中偏移量 33 处的模数和偏移量 165 处的公钥指数与我们在图 6 中 Java 程序中显示的匹配。
![]() 图 6:C# 生成的 RSA 私钥
|
现在,我们将检查加载 X.509 编码的 PublicKeyInfo
的情况。这就是我们的 AsnKeyBuilder
类写入文件(并在图 6 中被 Java 使用)的格式。首先,我们构造一个 AsnKeyParser
,将文件路径传递给构造函数。在这种情况下,我们使用的是公钥。没有通用性损失——我们也可以传递私钥路径并调用 ParseRSAPrivateKey
。在两种情况下,我们解析后都会返回一个 RSAParameters
。
AsnKeyParser keyParser = new AsnKeyParser("public.rsa.cs.key");
RSAParameters publicKey = keyParser.ParseRSAPublicKey();
接下来,我们构造一个 CspParameters
来传递给 RSACryptoServiceProvider
构造函数。仅使用接受 CspParameters
的构造函数,我们就不会触发密钥生成。这是有道理的,因为我们正在从文件中恢复密钥。
CspParameters csp = new CspParameters;
csp.KeyContainerName = "RSA Test (OK to Delete)";
csp.ProviderType = PROV_RSA_FULL; // 1
csp.KeyNumber = AT_KEYEXCHANGE; // 1
RSACryptoServiceProvider rsa = new RSACryptoServiceProvider(csp);
rsa.PersistKeyInCsp = false;
与密钥生成不同,我们不限于仅使用 MSDN 指定的 ProviderType
和 KeyNumber
的值。然而,RSA 实现有点受限,因此选择其他类型会导致加密异常。
我们可以选择导入公钥或私钥,具体取决于我们所需的密钥用途。下面,我们选择一个可用于加密或签名的公钥,然后使用 ImportParameters
导入我们解析的 RSAParameters
。
rsa.ImportParameters(publicKey);
最后,我们在使用完提供程序后调用 Clear
。
rsa.Clear();
如果我们疏忽调用 Clear
,将会出现有趣的错误和异常。例如,当运行本文的示例代码时,作者会在程序退出 Main
时收到一个 CryptographicException
,提示“Keyset does not exist”。
![]() 图 7:退出 Main 时出现 CryptograhicException
|
此外,应用程序事件日志中会写入一个事件,提示 '.NET Runtime version 2.0.50727.1433 — Fatal Execution Engine Error (79FFEE24) (80131506)'。
![]() 图 8:应用程序日志
|
原因并不明显。在示例中,我们调用 CreateRsaKeys
和 LoadRSAKeys
,它们不共享任何参数(以模拟通过 SneakerNet 进行的密钥交换)。然而,每个方法都会打开一个名为 'RSA Test (OK to Delete)' 的容器,并且每个方法都设置 PersistKeyInCsp = false
。当垃圾回收发生时,每个托管对象都会尝试释放共享的本机资源。为了避免这种情况,我们必须在打开资源的方法中调用 Dispose
、Close
或 Clear
来完成对象。
DSA 密钥生成
接下来我们关注 DSA。在密钥生成过程中,我们执行与 RSA 基本相同的步骤。然而,根据 MSDN,我们指定 ProviderType
为 PROV_DSS_DH
。对于 DSA,我们还可以使用 KeyNumber
为 AT_SIGNATURE
。当我们构造提供程序时,我们使用一个接受 int
来指定大小的构造函数。同样,这会使提供程序创建密钥。
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);
密钥对创建后,我们调用提供程序的 ExportParameters
来检索密钥。最后,我们使用 AsnKeyBuilder
来序列化密钥。图 9 显示了我们私钥的结果。
DSAParameters privateKey = dsa.ExportParameters(true);
AsnMessage key = AsnKeyBuilder.PrivateKeyToPKCS8(privateKey);
![]() 图 9:C# DSA 私钥
|
保存 DSA 密钥
与 RSA 一样,我们使用 PrivateKeyToPKCS8
或 PublicKeyToX509
方法将 RSA 密钥以 PKCS #8 或 X.509 格式保存
AsnMessage privateEncoded = PrivateKeyToPKCS8(privateKey);
SaveEncodedKey("private.dsa.cs.key", privateEncoded.GetBytes());
AsnMessage publicEncoded = PublicKeyToX509(publicKey);
SaveEncodedKey("public.dsa.cs.key", publicEncoded.GetBytes());
加载 DSA 密钥
接下来我们开始打开容器。在这种情况下,DSACryptoServiceProvider
使用一个只接受 CSP 的构造函数(而不是 CSP 和整数位计数)。这表明提供程序不希望我们生成密钥对。请注意,我们使用的是 PROV_DSS
而不是 PROV_DSS_DH
,因为我们不再有 J
和 seed 等参数。
CspParameters csp = new CspParameters();
csp.ProviderType = PROV_DSS; // 3
csp.KeyNumber = AT_SIGNATURE; // 2
DSACryptoServiceProvider dsa = new DSACryptoServiceProvider(csp);
上面,我们使用 PROV_DSS
构造了一个提供程序,该提供程序仅接受 CspParameters
,因为我们不需要运行时生成新的密钥对。然后,我们将使用 AsnKeyParser
来返回一个 DSAParameters
密钥。
AsnKeyParser keyParser = new AsnKeyParser("private.dsa.cs.key");
DSAParameters privateKey = keyParser.ParseDSAPrivateKey();
常见错误
接下来,我们将探讨密钥导入失败的最常见原因,这可能导致图 10 中显示的普遍的“Bad Data”异常。
![]() 图 10:Bad Data
|
首先,我们不能为 DSA 指定 AT_EXCHANGE
的 KeyNumber
。这应该很明显,因为 DSA 是一个签名算法。如果我们尝试 PROV_DSS_DH
/AT_EXCHANGE
对,我们会收到“The specified cryptographic service provider (CSP) does not support this key algorithm.”(指定的加密服务提供程序 (CSP) 不支持此密钥算法。)
![]() 图 11:PROV_DSS_DH/AT_EXCHANGE
|
接下来是我们的密钥解析器。当我们导出密钥时,我们写入了四个整数(p
、q
、g
和 x
或 y
)。ASN.1 整数使用 2 的补码表示法进行有符号(请参阅下面关于 ASN.1 整数的讨论)。因此,我们根据需要添加一个 0x00 字节以确保它们根据 ASN.1 语法为正数。我们在图 9 中观察到这一点:在文件偏移量 24 处,域参数 p
前面有一个值为 0x00 的字节(q
在偏移量 156 处,g
在偏移量 179 处也是如此)。
然而,当我们尝试将该值导入服务提供程序时,我们会收到“Bad Data”异常。当我们导入密钥时,必须去除 0x00(如果存在)。RSA 似乎不受此限制的影响,这让我们怀疑 DSA 在内部验证时失败,因为它认为参数大小为 1024+8 = 1032 位。我们的 AsnKeyParser
尝试在下面检查此条件。values
是解析的 ASN.1 整数的内容字节。
byte[] r = null;
if ( (values.Length > 1) && (0x00 == values[0]))
{
r = new byte[values.Length - 1];
Array.Copy(values, 1, r, 0, values.Length - 1);
}
回想一下,我们使用 ProviderType
为 PROV_DSS_DH
和 KeyNumber
为 AT_EXCHANGE
创建了密钥。这会导致密钥参数如图 12 所示。
![]() 图 12:DSAParameters
|
然而,当我们重建使用 PKCS#8 或 X.509 序列化的密钥时,结果将类似于图 13 所示。
![]() 图 13:PKCS#8 序列化的私有 DSA 密钥
|
由于 PKCS#8 和 X.509 不序列化验证参数,因此我们不能使用 PROV_DSS_DH
。在这种情况下,我们必须指定 ProviderType
= PROV_DSS
,而不是 PROV_DSS_DH
。使用 PROV_DSS_DH
将导致“Bad Data”。缺失的值,如 seed
和 J
(组参数因子),允许我们验证派生的域参数。有关 DSA 签名参数的详细信息,请参阅 Cryptographic Interoperability: Digital Signatures [22]。服务提供程序的 FromXMLString
不受此限制,因为它写入所有参数。
为了完整起见,RFC 2492 [24](和 ANSI X9.42 [25])包含了 Diffie-Hellman 密钥交换的 ASN.1 语法,如下所示。对于缺失的 C# 参数的语法如下所示
DomainParameters ::= SEQUENCE {
p INTEGER,
g INTEGER,
q INTEGER,
j INTEGER OPTIONAL,
validationParms ValidationParms OPTIONAL }
和
ValidationParms ::= SEQUENCE {
seed BIT STRING,
pgenCounter INTEGER }
OID 是 1.2.840.10046.2.1。这是否可以加载到 Java 中进行 DSA 操作值得怀疑。10046 是 OID 树中的 ANSI-x942 弧,而 10040(用于 DSA)是 x9-57 弧。
如果我们尝试使用 ToXMLString
将公钥导出为私钥,我们会捕获一个异常,提示“Key not valid for use in specified state”(密钥无效,无法在指定状态下使用),如图 14 所示。
// Load public key
AsnKeyParser keyParser = new AsnKeyParser(...);
RSAParameters publicKey = keyParser.ParseRSAPublicKey();
...
rsa.ImportParameters(publicKey);
// Export as private key
String xml = rsa.ToXmlString(true);
![]() 图 14:PKCS #8 私钥/ToXmlString
|
使用 ToXMLString
写入一个在读取 PKCS#8 或 X.509 DSA 密钥(RSA 密钥没有验证参数)后得到的密钥,将生成一个包含域参数 P、Q、G 以及公钥 Y 或私钥 X 的文件。这是预期的,因为该密钥没有 J、seed 或 counter。
![]() 图 15:从 PKCS#8 编码的密钥进行 ToXMLString
|
根据 RFC 3275,第 4.4.2.1 节,这是有效的密钥语法
参数 seed 和 pgenCounter 用于 [DSS] 中指定的 DSA 素数生成算法。因此,它们是可选的,但必须同时存在或同时不存在。
有了我们新获得的知识,我们将尝试破解提供程序。首先,我们将 DSA 密钥以 XML 格式写入文件。然后,我们将 seed
删除,如图 16 所示。
![]() 图 16:XML 编码的密钥
|
接下来我们复制密钥并删除 seed。当我们尝试加载 bad.dsa.key.xml 时,我们会捕获异常“Input string does not contain a valid encoding of the 'DSA' 'Seed' parameter.”(输入字符串不包含“DSA”的“Seed”参数的有效编码。)当我们只删除 counter 时,也会收到类似的异常。
![]() 图 17:由于缺少 Seed 参数而导致的异常
|
ASN.1
ASN.1 是 Abstract Syntax Notation One(抽象语法标记法一),它是一个表示层协议。它是一种描述数据和数据属性的正式语言 [3]。ASN.1 编码规则由 ITU 在 X.690(X.208 已于 2002 年弃用)[1] 中指定。有关 ASN.1 及其使用的问题,请访问 ASN.1 Consortium。加入他们的邮件列表,然后将问题发送到 asn1@asn1.org。
有三种编码类型——BER、CER 和 DER。每种都提供不同程度的编码自由度,BER 最不严格,DER 最严格。我们通常发现应用程序实现 DER 编码器和 BER 解码器。也就是说,一个应用程序尝试编写最正确的 ASN.1 标记法,而读取最不正确的语法。
例如,如果我们想对字符串 "Crypto Interop" 进行编码,单个编码字符串将满足 BER、CER 和 DER。但是,如果我们使用 BER(最“宽松”的),我们也可以用三个字符串表示它们将被连接起来:“Crypto”,“ ”,“Interop”。这种字符串连接不是有效的 DER 编码。有关 BER、CER 和 DER 编码的更多信息,请参阅 X.690 的第 8、9 和 10 节。有关 CER 和 DER 对 BER 编码的限制,请参阅 X.690 的第 11 节。
任何规则都有例外,这个也不例外。根据 PKCS#8,“…[在] RSA 私钥中,……内容是 RSAPrivateKey 类型的值的 BER 编码”[8]。因此,编码器可以使用不那么受限制的 BER 编码。
基本的 ASN.1 单位是 Octet(字节),它是一个 8 位字节。ASN.1 编码由字节组成,通常有三个部分:标识符 (Identifier)、长度 (Length) 和内容 (Contents)(除了 Null
类型,它只有两个部分,以及使用不确定长度编码时有四个部分)。有关更多信息,请参阅 X.690,第 8.1 节。
标识符进一步细分:低 5 位是 Tag
号,高 3 位是位字段,由 Class
和 Primitive
/Constructed
字段组成。对我们来说,高 3 位通常是 0,所以我们的标签号就是标识符(序列编码时除外)。标签号表示类型——整数、位串、可打印字符串等。还有我们不使用的用户定义类型。
长度指定了内容字节的大小(我们要编码的数据值)。长度有三种编码方式:短(确定形式)、长(确定形式)和不确定形式。我们只使用前两种形式(第三种等同于运行时长度编码)。在短形式中,只有一个字节。该字节的最高位为零,其余七位指定后续内容字节的数量。在长形式中,最高位为一。其余七位指定后续字节的数量,这些字节指定了长度。例如,如表 2 所示。
长度字节(S) | 含义 |
0x01 | MSB 最高位为 0,内容字节长度为 1 |
0x02 | MSB 最高位为 0,内容字节长度为 2 |
0x81 0x01 | MSB 最高位为 1,下一个字节是长度(内容长度 = 1) |
0x81 0x02 | MSB 最高位为 1,下一个字节是长度(内容长度 = 2) |
0x82 0x01 0xFF | MSB 最高位为 1,下一个两个字节是长度(内容长度 = 0x01FF) |
0x82 0x7F 0xFF | MSB 最高位为 1,下一个两个字节是长度(内容长度 = 0x7FFF) |
0x83 0x00 0x7F 0xFF | MSB 最高位为 1,下一个三个字节是长度(内容长度 = 0x7FFF) |
0x83 0x07 0xFF 0xFF | MSB 最高位为 1,下一个三个字节是长度(内容长度 = 0x07FFFF) |
表 2:长度编码示例 |
从上面可以看出,我们可以用几种方式编码长度为 1 的值:“0x01”、“0x81 0x01”和“0x82 0x00 0x01”。BER,作为最宽松的编码,将允许所有这三种。DER 最严格,只允许“0x01”(来自 X.690,第 10.1 节:应使用长度编码的确定形式,并以最少的字节数编码)。
我们只使用规范中的一部分元素:INTEGER、BIT STRING、OCTET STRING、NULL、OBJECT IDENTIFIER 和 SEQUENCE,它们将在下面解释。
INTEGER
ASN.1 整数被分配标签号 2。它是一个使用 2 的补码表示的有符号整数(与大多数个人计算机相同)。因为它是有符号的,如果我们想表示一个最高位被设置的正整数,我们必须在内容字节前面加上 0x00。例如,如表 3 所示。有关更多信息,请参阅 X.690,第 8.3 节。
值 | 整数编码 |
1 | 0x01 |
-1 | 0xFF |
2 | 0x02 |
-2 | 0xFE |
255 (0xFF) | 0x00 0xFF |
-255 (0xFF) | 0xFF 0x01 |
表 3:整数编码示例 |
在进行信息交换的加密应用程序中,当我们选择一个 8 的倍数(例如 512 位或 1024 位)的密钥大小时,我们就能确保 2 的补码问题。作为一个具体的例子,假设我们想要一个 512 位的模数。我们需要两个随机素数p和q,每个素数都有 256 位长。我们向伪随机数生成器请求 256 位。素数是奇数,所以我们将数字(p 或 q)的最低位设置为 1。为了确保数字是所需的大小(256 位),我们将最高位设置为 1。然后我们测试数字的素性。
因为我们设置了最高位为 1,如果没有修改内容字节,ASN.1 整数将被解释为负数。对等系统可能(但不应该)将该数字解释为负数。这可能导致我们的对等方拒绝密钥。因此,在传输密钥材料之前,我们在内容字节前面加上 0x00,以确保接收到一个正数。我们在图 2 的示例中看到为了确保正整数而添加前缀的情况。
BIT STRING
ASN.1 位串被分配标签号 3。ASN.1 原始位串是一个初始字节,后跟零个、一个或多个后续字节。初始字节是一个丢弃计数,指定有多少尾随位未使用。如果没有丢弃位,初始字节为 0x00,并且必须介于 0 到 7 之间(含)。当位数不是 8 的倍数时使用。
例如,要编码位串 1111 1111 1111,该字符串必须是 8 的倍数,所以我们的内容字节将是 0x04 0xFF,0xF0。0xFF 0xF0(1111 1111 1111 0000)是位串,而 0x04 指定有四位未使用。
位串还有一个构造变体,它不使用初始字节(我们不使用)。有关更多信息,请参阅 X.690,第 8.6 节。
OCTET STRING
ASN.1 字节串被分配标签号 4。ASN.1 字节串是零个、一个或多个字节。与整数和位串不同,没有特殊规则需要记住。有关更多信息,请参阅 X.690,第 8.7 节。
NULL
ASN.1 null 被分配标签号 5。与其他类型不同,此对象仅由标识符和长度组成,长度为 0。因此,null 编码是 0x05 0x00。
OBJECT IDENTIFIER
ASN.1 OID 被分配标签号 6。ASN.1 对树的弧进行特殊打包,类似于长度编码。对我们而言,它是 ASN.1 编码的对象标识符 1.2.840.113549.1.1。有关更多信息,请参阅 X.690,第 8.19 节。
SEQUENCE
ASN.1 序列被分配标签号 16。然而,它是一个构造对象,所以我们遇到的标识符是 0x30 (0x10 | 0x20)。序列充当有序容器(这与集合相反,集合充当无序容器)。集合可以包含零个、一个或多个元素。序列的内容字节是它所包含的类型的字节。
例如,要将一个 INTEGER(值为 4)包装在 SEQUENCE 中,我们的编码将是 0x30 0x03 0x02 0x01 0x04。0x02 0x01 0x04 是整数 4,它成为序列的内容字节。再举一个例子,一个没有元素的序列编码为 0x30 00。有关更多信息,请参阅 X.690,第 8.9 节。
致谢
- Wei Dai for Crypto++ 及其在 Crypto++ 邮件列表上的宝贵帮助
- A. Brooke Stephens 博士,他为我打下了密码学基础
校验和
- CryptoPPInteropKeys.zip
- MD5: 79D51470C98CCAB607D3A9DCFA35E572
- SHA-1: 0F41CF6A6EDC78CE10C383470B22F20EC85B3808
- JavaInteropKeys.zip
- MD5: 0700318CB99866DB1C2FBF6AB669B919
- SHA-1: 412B9379912FED187A3EAE803C03DA256CC39B7C
- CSInteropKeys.zip
- MD5: CCC3B793F929B8F2CED38E4D31F4B052
- SHA-1: 023F5B78CC4A13D889AD32C1654B0558F1E5B338
参考文献
- Specification of Basic Encoding Rules (BER), Canonical Encoding Rules (CER), and Distinguished Encoding Rules (DER), X.690, 2002 年 8 月。
- The Directory: Public-key and Attribute Certificate Frameworks, X.509, 2005 年 8 月。
- W. Richard Stevens, TCP/IP Illustrated, Volume 1: The Protocols, Addison Wesley Publishing, ISBN 0-2016-3346-9, p. 387。
- Public-Key Cryptography Standards (PKCS), RSA Laboratories。
- What is PKCS?, RSA Laboratories。
- PKCS #1: RSA Encryption Standard, RSA Laboratories, 1993 年 11 月版本 1.5, p. 6。
- The Directory: Public-key and Attribute Certificate Frameworks, X.509, 2005 年 8 月, p. 12。
- PKCS #8: Private-Key Information Syntax Standard, RSA Laboratories, Version 1.2, 1993 年 11 月, pp. 3-4。
- PKCS #8: Private-Key Information Syntax Standard, RSA Laboratories, Version 1.2, 1993 年 11 月, pp. 2-3。
- A. Menenzes, et. al., Handbook of Applied Cryptography, CRC Press, ISBN 0-8493-8523-7, p. 286。
- A. Menenzes, et. al., Handbook of Applied Cryptography, CRC Press, ISBN 0-8493-8523-7, p. 567。
- PKCS #8: Private-Key Information Syntax Standard, RSA Laboratories, Version 1.2, 1993 年 11 月, p. 7。
- Java Cryptography Architecture (JCA) Reference Guide.
- Shaheryar Ch Porting Java Public Key Hash to C# .NET。
- FIPS 186-2, Digital Signature Standard。
- RFC 3279, Algorithms and Identifiers for the Internet X.509 Public Key Infrastructure Certificate and Certificate Revocation List (CRL) Profile。
- RFC 4055, Additional Algorithms and Identifiers for RSA Cryptography for use in the Internet X.509 Public Key Infrastructure Certificate and Certificate Revocation List (CRL) Profile。
- PKCS #5: Password-Based Encryption Standard, RSA Laboratories, Version 1.5, 1993 年 11 月。
- RFC 2898, Password-Based Cryptography Specification, Version 2.0, 2000 年 9 月。
- RFC 3447, Public-Key Cryptography Standards (PKCS) #1: RSA Cryptography Specifications Version 2.1, 2003 年 2 月。
- J. Walton, Cryptographic Interoperability: Digital Signatures。
- IEEE P1363, Standard Specifications For Public-Key Cryptography。
- RFC 2459, Internet X.509 Public Key Infrastructure Certificate and CRL Profile, 1999 年 1 月。
- ANSI X9.42, Public Key Cryptography for the Financial Services Industry: Agreement of Symmetric Keys Using Discrete Logarithm Cryptography, 2003 年 1 月。
- RFC 3275, XML-Signature Syntax and Processing, 2002 年 3 月。