烦恼一般都是想太多了。

0%

RSA及相关库实现

应大佬要求来了解一下 RSA 相关的东西,顺便了解一下 Crypto++ 对于 RSA 的实现。

RAS 简单介绍

digraph { {rank=same p q} n[label="n"] p -> q[style=dashed,label=X,arrowhead=none] {p q} -> n fn[label="φ(n)"] n -> fn e[label="e\n1< e < φ(n)"] d[label="d\n e 对 φ(n) 模反元素 \n 使 φ(n) mod e 为1"] fn -> d e -> d[style=dashed] e -> fn[label="互质",style=dashed,dir=both] subgraph cluster_public{ label=公钥 n e } subgraph cluster_private{ label=私钥 n d } }

加密:
解密:

Crypto++ 实现

Keys

常规的 Key 操作,如:生成、验证、加载、保存、导入、导出等在 Keys and Formats 进行了讨论。

Crypto++ 的 RSA 实现在 rsa.h 中进行了 typedef

struct RSA
{
typedef RSAFunction PublicKey;
typedef InvertibleRSAFunction PrivateKey;
};

InvertibleRSAFunction 提供了一个 GenerateRandomWithKeySize 函数来生成 Key ,密钥对。

GenerateRandomWithKeySize (RandomNumberGenerator &rng, unsigned int keySize)

这个方法的参数是一个 [RandomNumberGenerator](https://cryptopp.com/wiki/RandomNumberGenerator) 和用 bit 来指定的 模(modulus) 的大小。 GenerateRandomWithKeySize 以模的大小来调用 GenerateRandom,然后生成一个加密的 NameValuePairs
在很多的示例代码中,作为展示,InvertibleRSAFunction 都会如下使用来建立逻辑上独立的部分:

///////////////////////////////////////
// Generate Parameters
InvertibleRSAFunction params;
params.GenerateRandomWithKeySize(rng, 3072);

///////////////////////////////////////
// Create Keys
RSA::PrivateKey privateKey(params);
RSA::PublicKey publicKey(params);

实际上,为了提高可读性,应该使用 RSA::PrivateKey ,而不是 InvertibleRSAFunction;而 RSAES_PKCS1v15_Encryptor, RSAES_OAEP_SHA_Encryptor, 由你的朋友们使用。一个更详细的,看待密钥的生成、加载等的参考 Keys and Formats

///////////////////////////////////////
// Create Keys
RSA::PrivateKey privateKey;
privateKey.GenerateRandomWithKeySize(rng, 3072);

RSA::PublicKey publicKey(privateKey);

一旦,Key 被建立,那么他们就可以被传递给 Encryptors, Decryptors, Signers, and Verifiers. 使用了。
如果想要将 Key 持久化到硬盘,使用 Save 函数。如果要从磁盘加载,使用 Load 函数。

如果你有一个由 {n, e, d} 组成的私钥,而你又对 PKCS #1 中指定的所有参数的计算感兴趣的话,看 RSA CRT Key 这里。另外,Mounir Idrassi 提供了一个开源工具:RSA Converter

如果我们需要导入 {n, e, d} 私钥,或者 {n, e} 公钥,使用 InitializeRSA::PublicKeyRSA::PrivateKey 都提供了这个函数的重载。

加密 Encryption

高级的 RSA encryption schemes 通过 RSAES 导出,它们如下定义。

template <class STANDARD>
struct RSAES : public TF_ES<STANDARD, RSA>
{ };

模板参数 STANDARD,指定了额外的算法。如,当需要处理 PKCS 的时候,PKCS1v15 就会作为参数传递。

注意:用 PKCS v1.5 进行 RSA 加密应该非常小心的使用,因为会有 padding oracle attacksA bad couple of years for the cryptographic token industry

TF_ES 类导出了与 RSA 使用时的一些常用特性。TF 代表了 trapdoor 函数的意思,ES 代表了加密的组合。这里有些存在——在遇到 cryptlib.h 中的 TF_EncryptorBaseTF_DecryptorBase 前,至少两层的模板。为了更方便的使用这个库,这里提供了两个 typedef。第一个基于 PKCS #1,第二个是基于 OAEPSHA。因为 PKCS 对齐的实现问题,我们应该优先使用 OAEP 对齐。

typedef RSAES<PKCS1v15>::Decryptor RSAES_PKCS1v15_Decryptor;
typedef RSAES<PKCS1v15>::Encryptor RSAES_PKCS1v15_Encryptor;

typedef RSAES<OAEP<SHA> >::Decryptor RSAES_OAEP_SHA_Decryptor;
typedef RSAES<OAEP<SHA> >::Encryptor RSAES_OAEP_SHA_Encryptor;

签名 Signature

高级的 RSA signature schemes 通过 RSASS 导出,它的定义如下:

template <class STANDARD, class H>
struct RSASS : public TF_SS<STANDARD, H, RSA>
{ };

RSAES 一样,RSASS 也需要一个 STANDARDTF 代表了 trapdoor 函数,SS 代表了签名组合。除了 STANDARD 参数外,一个单一方式的哈希函数必须通过 H 来提供。签名者和验签者感兴趣的类是 PK_SignerPK_Verifier,这两个也在 cryptlib.h 中。后一个类提供了如 MaxSignatureLenght, SignMessage, VerifyMessage 这些我们熟悉的函数。

Crypto++ RSA 对一个 Signature Scheme with Appendix (SSA) 的实现如下进行了 typedef 。注意,两个额外哈希函数都在 PKCS (MD2 and MD5)进行了指定,但不应该使用它们。

typedef RSASS<PKCS1v15, SHA>::Signer RSASSA_PKCS1v15_SHA_Signer;
typedef RSASS<PKCS1v15, SHA>::Verifier RSASSA_PKCS1v15_SHA_Verifier;

Crypto++ RSA 对一个 Signature Scheme with Recovery (PSSR)的实现和 SSA 类似。然而,这里并没有 typedef,所以这些对象必须使用模板语法进行声明。下面, PSSR (from pssr.h) 进行了传参 (恢复算法),另又指定了一个 HASH 算法。

RSASS<PSSR, SHA1>::Signer signer;
RSASS<PSSR, SHA1>::Verifier verifier;

如果期待 Whirlpool 那么应该像下面这样操作。

RSASS<PSSR, Whirlpool>::Signer signer;
RSASS<PSSR, Whirlpool>::Verifier verifier;

如果对 SHA-2 家族的哈希感兴趣,那么应该使用 SHA256

RSASS<PSSR, SHA256>::Signer signer;
RSASS<PSSR, SHA256>::Verifier verifier;

尽管签名的类型不同(PSSR vs SSA),我们很容易的通过公钥或者私钥来构造签名者和验签者。

// Generate or Load keys
RSA::PrivateKey privateKey;
RSA::PublicKey publicKey;
...

RSASS<PSSR, SHA1>::Signer signer(privateKey);
// Create signature
...

RSASS<PSSR, SHA1>::Verifier verifier(publicKey);
// Verify signature on message
...

例子

密钥生成

///////////////////////////////////////
// Pseudo Random Number Generator
AutoSeededRandomPool rng;

///////////////////////////////////////
// Generate Parameters
InvertibleRSAFunction params;
params.GenerateRandomWithKeySize(rng, 3072);

///////////////////////////////////////
// Generated Parameters
const Integer& n = params.GetModulus();
const Integer& p = params.GetPrime1();
const Integer& q = params.GetPrime2();
const Integer& d = params.GetPrivateExponent();
const Integer& e = params.GetPublicExponent();

///////////////////////////////////////
// Dump
cout << "RSA Parameters:" << endl;
cout << " n: " << n << endl;
cout << " p: " << p << endl;
cout << " q: " << q << endl;
cout << " d: " << d << endl;
cout << " e: " << e << endl;
cout << endl;

///////////////////////////////////////
// Create Keys
RSA::PrivateKey privateKey(params);
RSA::PublicKey publicKey(params);

密钥生成的性能

try
{
AutoSeededRandomPool prng;
RSA::PrivateKey rsa;
ThreadUserTimer timer(TimerBase::MILLISECONDS);

timer.StartTimer();

const int bits = 2048;
rsa.GenerateRandomWithKeySize(prng, bits);

///////////////////////////////////////

unsigned long elapsed = timer.GetCurrentTimerValue();
unsigned long ticks = timer.TicksPerSecond();
unsigned long seconds = elapsed / ticks;

// days, hours, minutes, seconds, 100th seconds
unsigned int d=0, h=0, m=0, s=0, p=0;

p = ((elapsed * 100) / ticks) % 100;
s = seconds % 60;
m = (seconds / 60) % 60;
h = (seconds / 60 / 60) % 60;
d = (seconds / 60 / 60 / 24) % 24;

float fs = (seconds + ((float)p/100));

///////////////////////////////////////

stringstream ss;

if(d) {
ss << d << ((d == 1) ? " day, " : " days, ");
goto print_hours;
}

if(h) {
print_hours:
ss << h << ((h == 1) ? " hour, " : " hours, ");
goto print_minutes;
}

if(m) {
print_minutes:
ss << m << ((m == 1) ? " minute, " : " minutes, ");
}

ss << s << ((s == 1) ? " second" : " seconds");

cout << "Elapsed time for " << bits << " RSA key: ";
cout << fixed << setprecision(2) << fs << "s";
if(seconds)
cout << " (" << ss.str() << ")";
cout << endl;
}

catch(CryptoPP::Exception& e)
{
...
}

Encryption Scheme (OAEP using SHA)

先的代码演示了使用 OAEP 进行 RSA 加密。完成的 RSA 加密的例子可以在 RSA Encryption Schemes 查看。

////////////////////////////////////////////////
// Generate keys
AutoSeededRandomPool rng;

InvertibleRSAFunction params;
params.GenerateRandomWithKeySize(rng, 3072);

RSA::PrivateKey privateKey(params);
RSA::PublicKey publicKey(params);

string plain="RSA Encryption", cipher, recovered;

////////////////////////////////////////////////
// Encryption
RSAES_OAEP_SHA_Encryptor e(publicKey);

StringSource ss1(plain, true,
new PK_EncryptorFilter(rng, e,
new StringSink(cipher)
) // PK_EncryptorFilter
); // StringSource

////////////////////////////////////////////////
// Decryption
RSAES_OAEP_SHA_Decryptor d(privateKey);

StringSource ss2(cipher, true,
new PK_DecryptorFilter(rng, d,
new StringSink(recovered)
) // PK_DecryptorFilter
); // StringSource

cout << "Recovered plain text" << endl;

Signature Scheme (PKCS v1.5)

尽管和 RSA-SSA 类似, RSASSA_PKCS1v15_SHA_Signer and RSASSA_PKCS1v15_SHA_Verifier 使用 PKCS v1.5 对齐。RSASSA_PKCS1v15_<*Digest*>_SignerRSASSA_PKCS1v15_<*Digest*>_VerifierMD2MD5 变量 不应该 被使用。完整的 RSA 签名组合的代码见 RSA Signature Schemes.

Note: RSA encryption with PKCS v1.5 padding should be used with caution due to padding oracle attacks. See A bad couple of years for the cryptographic token industry.

// Generate or Load keys
RSA::PrivateKey privateKey = ...;
RSA::PublicKey publicKey = ...;

// Message
string message = "RSA Signature", signature;

////////////////////////////////////////////////
// Sign and Encode
RSASSA_PKCS1v15_SHA_Signer signer(privateKey);

StringSource ss1(message, true,
new SignerFilter(rng, signer,
new StringSink(signature)
) // SignerFilter
); // StringSource

////////////////////////////////////////////////
// Verify and Recover
RSASSA_PKCS1v15_SHA_Verifier verifier(publicKey);

StringSource ss2(message+signature, true,
new SignatureVerificationFilter(
verifier, NULL,
SignatureVerificationFilter::THROW_EXCEPTION
) // SignatureVerificationFilter
); // StringSource

cout << "Verified signature on message" << endl;

Keys and Formats

在 Crypto++ 中,有多种密钥,如 RSA/DSA,但是它们的概念是一致的。这是因为,Crypto++ 的继承(从PublicKeyPrivateKey 衍生的对象)和组合(从 PublicKeyAlgorithmPrivateKeyAlgorithm 衍生的对象)。

通过,我们应该使用密钥的 LoadSave 函数来达到与其他库(如 OpenSSL,Java,微软的 .Net)的交互性。LoadSave 在 PKCS#8 的PrivateKeyInfo 和 X509 的 SubjectPublicKeyInfo 上进行操作。

X.509,IETF 的 Privacy Enhanced Mail(PEM) 和 PKCS #1(RSA 加密标准)在他们对 RSA 公钥的定义是相同的。

如果密钥只是在程序内工作,我们可以使用 PKCS #8 的 PrivateKeyPrivateKeyInfo 的一部分)和 X.509 的PublicKeySubjectPublicKeyInfo 的一部分)。 PKCS #8 的 PrivateKey and X.509’s PublicKey 会节省字节和处理的时间。

当写出 X.509 和 PKCS #8 密钥的时候必须使用 ASN.1。实际上,密钥写出的时候必须使用 DER 编码,然后使用 BER 编码来读取。BERDER 更宽松一些——这个标准的目的是为了通过 更严格的写入宽松的读取 来推动交互性。See X.690, Specification of Basic Encoding Rules (BER), Canonical Encoding Rules (CER) and Distinguished Encoding Rules (DER).

最后, 两个 Code Project 文章涵盖了 Crypto++, 密钥和交互性。, See Cryptographic Interoperability: Keys and Cryptographic Interoperability: Digital Signatures.

Crypto++ Key Formats

在内部,Crypto++ 使用一个遵循真实世界对象的关键表示。然而,一个 Crypto++ 密钥的原始字节的 dump,在离开了这个库后就没有什么意义了。例如,Crypto++ 并不 通过 ASN.1 来编码 PKCS #8 格式的密钥进行表示,所以呢,一个 dump 是没有什么互操作性的。Crypto++ 提供 允许库与很多流行的格式进行工作的方法,包括了 PKCS #8 和 X.509。

Private Key Format

Logical depiction of PrivateKeyInfo

Crypto++ 使用的外部私钥格式是 PKCS #8: Private-Key Information Syntax Standard。PKCS 标准规定了这种密钥的 ASN.1 编码。 ASN.1 is Abstract Syntax Notation ,它在 ITU 的 X.690, Specification of Basic Encoding Rules (BER), Canonical Encoding Rules (CER) and Distinguished Encoding Rules (DER) 进行了标准化。

概念上,这种格式有两个部分。

  1. 包消息。如 OID、版本和属性。
  2. 密钥的实际序列化后的字节。

在 PKCS #8 中,第一部分被叫做 PrivateKeyInfo,第二部分叫做 PrivateKey

实际上,我们应该将第二部分想做是 XxxPrivateKey,其中 ‘Xxx’ 会在知道了 OID 后进行设置。例如,如果 OID 是 rsaEncryptionXxxPrivateKey 就会变成 RsaPrivateKey。 如果 OID 是 dsa,那么就是 DsaPrivateKey

上面图片中显示的私钥是 00 B7 67 B4 ...。它是使用 ASN.1 字节字符串 进行编码。

PrivateKeyInfo 包围了 PrivateKey

Public Key Format

Logical depiction of PublicKeyInfo

Crypto++ 使用的外部公钥格式是 X.509: Public-key and attribute certificate frameworks 。和 PKCS #8 一样,密钥使用 ASN.1 编码。

概念上,也是两部分:

  1. 包信息。如 OID
  2. 真实序列化后的密钥。(这和上面描述 PKCS #8 听起来是一样的)。

在 X.509 中,第一部分叫做 PublicKeyInfo,第二部分叫做 PublicKey。如 XxxPrivateKey,当我们知道了 OID 后就会进行设置。

PublicKey 和 PKCS #8 使用 ASN.1 来编码有些不同。上面的图片中展示了样例数据 101010111…。这个数据使用的是 ASN.1 的二进制字符串。在 PKCS #8 中,PublicKeyInfo 包围了 PublicKey。

Crypto++ PKCS8PrivateKey

RSA::PrivateKeyDSA::PrivateKey 是 Crypto++ 中封装了大多数 RSA 和 DSA 的标准密钥(ie, PKCS, X.509, IEEE, and IETF)。 它们都从一个 PrivateKey 继承,更重要的是 PKCS8PrivateKey。PKCS8PrivateKey 类的文档在 [PKCS8PrivateKey Class Reference](http://www.cryptopp.com/docs/ref/class_p_k_c_s8_private_key.html),其中展示了这个类提供了Load, Save` 函数。

Crypto++ X509PublicKey

类似, RSA::PublicKeyDSA::PublicKey 都从一个 PublicKeyX509PublicKey继承。X509PublicKey 类的文档位于X509PublicKey Class Reference ,其中也提供了 Load, Save 方法。

Generating, Validating, Saving, and Loading Keys

我们需要一个程序来生成 RSA/DSA 的密钥对,这是cryptopp-key-gen。 cryptopp-key-gen 为了演示目的而生成与保存这些密钥。RSA 密钥可以用来进行加密 (see RSA Encryption Schemes) 或者签名 (see RSA Signature Schemes); DSA 与 Digital Signature Algorithm一起使用。

Note: RSA::PrivateKeyDSA::PrivateKey 是 Crypto++ 中与 PKCS #8 PrivateKeyInfo 等价的东西。 类似, RSA::PublicKeyDSA::PublicKey 是与 X.509 PublicKeyInfo 等价的。如果我们想要保存和加载一个 PKCS #8 XxxPrivateKey 格式或 X.509 XxxPublicKey 格式, 查看 BER and DER Encoding

////////////////////////////////////////////////////////////////////////////////////

RSA::PrivateKey rsaPrivate;
rsaPrivate.GenerateRandomWithKeySize(rnd, 3072);

RSA::PublicKey rsaPublic(rsaPrivate);

SavePrivateKey("rsa-private.key", rsaPrivate);
SavePublicKey("rsa-public.key", rsaPublic);

////////////////////////////////////////////////////////////////////////////////////

DSA::PrivateKey dsaPrivate;
dsaPrivate.GenerateRandomWithKeySize(rnd, 1024);

DSA::PublicKey dsaPublic;
dsaPrivate.MakePublicKey(dsaPublic);

SavePrivateKey("dsa-private.key", dsaPrivate);
SavePublicKey("dsa-public.key", dsaPublic);

Generating Keys

RSA/DSA 的密钥生成非常的类似。RSA::PrivateKeyDSA::PrivateKey 都从 Crypto++ 的 PrivateKey (documented at PrivateKey Class Reference)继承, PrivateKey 提供了 GenerateRandomGenerateRandomWithKeySize。Crypto++ 方法都使用一个 RandomNumberGenerator (documented at RandomNumberGenerator Class Reference),然后使用一个生成器和字节大小。

当选择字节大小的时候,当前的最佳实践(从 NIST 参考)规定我们使用一个 security level of 128 (80 and 112 bits of security 在新系统中不应该再使用)。这意味着 RSA 应该使用 3072 字节的模。这也意味着一个 DSS Version1 或 DSS version 2 的 DSA 密钥在当前的安全性要求太弱了(一 个 1024 字节的 DSA 提供了大概 80 个理论上的安全字节)。

下面的代码中,我们创建一个 3072 字节的 RSA 密钥,1024 字节的 DSA 密钥(DSS Version2 )。

AutoSeededRandomPool 是一个 PGP 伪随机数生成器。 如果需要更安全的生成器,使用 X917RNG (from ANSI 9.17 Appendix C and based on DES) 或 AutoSeededX917RNG based on the AES block cipher.

AutoSeededRandomPool rnd;

RSA::PrivateKey rsaPrivate;
rsaPrivate.GenerateRandomWithKeySize(rnd, 3072);

DSA::PrivateKey dsaPrivate;
dsaPrivate.GenerateRandomWithKeySize(rnd, 1024);

我们通过如下方式从私钥来生成公钥。RSA::PublicKey 允许我们在构造器中使用私钥。 DSA::PublicKey 需要我们手动执行 MakePublicKey.

RSA::PublicKey rsaPublic(rsaPrivate);

DSA::PublicKey dsaPublic;
dsaPrivate.MakePublicKey(dsaPublic);

Saving Keys

Crypto++ 的文档很完善。如果我们查找 .Save(注意,这是一个方法,所以前面有 .),最少有 25 个文件使用了这个方法。关注一下 test.cpp, 它是 cryptest.exe 的一部分, 它将会生成和保存密钥。不幸的是, GenerateRSAKey 写出的是一个 hex encoded 密钥, 这就不能被 dumpasn1 这样的工具来读取了。

再看 validate2.cpp,我们发现很多加载和保存到 byte queues.。一个 ByteQueue 相对于 sink 来说是一个和选择,因为它 使用了一个安全的分配器(allocator) - SecByteBlock ——它保证当 ByteQueue 的析构器运行的时候将密钥的所有信息清 0。下面是其中的一个例子:

ByteQueue queue;
priv.AccessKey().SavePrecomputation(queue);

先调用 AccessKey 再调用 SavePrecomputation,我们已经有密钥了(所以这里没必要调用 AccessKey),而且我们只对保存密钥本身感兴趣 (而不是预计算过程和门函数相关的东西)。在库中我们保存 KEY 的代码如下,(私钥也是类似的):

void SavePublicKey(const string& filename, const PublicKey& key)
{
ByteQueue queue;
key.Save(queue);

Save(filename, queue);
}

SavePublicKeySavePrivateKey 使用 Crypto++ PublicKeyPrivateKey ,简单的将密钥写到文件。在面向对象的精神下,SavePublicKeySavePrivateKey 调用 Save。cryptopp-key-gen 对 Save 和库中对 Save 使用基本相同(之所以相同是因为,面向对象的接口编程有很多的函数调用,但并没有实际上做更多事情)。

如果我们从面向对象和接口编程换个个视角(复制 cryptopp-key-gen 的 Save),我们将会调用 CopyTo and MessageEnd ,这种搞法是严重禁止的。

void Save(const string& filename, const BufferedTransformation& bt)
{
FileSink file(filename.c_str());

bt.CopyTo(file);
file.MessageEnd();
}

Loading Keys

虽然我们想将加载密钥作为一个练习给读者,但有一个文档来演示如何加载一个 KEY 将会更好。

PKCS8PrivateKeyX509PublicKey 提供了SaveLoad。要使用 Load, 我们创建了三个短小的封装函数(下面展示了两个),它是我们的对 Save使用的对照。数据将会从一个 FileSource 流向 BufferedTransformation,最终被 KEY 加载。

void LoadPublicKey(const string& filename, PublicKey& key);
void Load(const string& filename, BufferedTransformation& bt)

cryptopp-key-gen’s 底层函数 Load 用文件数据填充了调用者的 BufferedTransformation

void Load(const string& filename, BufferedTransformation& bt)
{
FileSource file(filename.c_str(), true /*pumpAll*/);

file.TransferTo(bt);
bt.MessageEnd();
}

LoadPublicKey 加载 Crypto++ key:

void LoadPublicKey(const string& filename, PublicKey& key)
{
ByteQueue queue;
Load(filename, queue);

key.Load(queue);
}

最后,我们可以验证一下:

RSA::PrivateKey key1, key2;
key1.GenerateRandomWithKeySize(rnd, 3072);

SavePrivateKey("rsa-roundtrip.key", key1);
LoadPrivateKey("rsa-roundtrip.key", key2);

if(key1.GetModulus() != key2.GetModulus() ||
key1.GetPublicExponent() != key2.GetPublicExponent() ||
key1.GetPrivateExponent() != key2.GetPrivateExponent())
{
throw runtime_error("key data did not round trip");
}

Validating Keys

KEY 在加载后应该被验证,无论是库生成的还是故意如此,因为你并不知道是谁生成了 KEY,或者是生成 KEY 的工具是什么(KEY 可能会有一些缺陷)。决不要 在一个未经验证的 KEY 上应用密码,因为有可能这个 KEY 有允许密码恢复的 BUG 在。决不要 使用一个未经验证的 KEY (包括数字签名有效)恢复的数据。这些警告与 “糟糕的” 用例有关(例如,不诚实的参与者);而不是预期的 ”好“用例(人们与他人公平竞争)。

CryptoMaterial,被 PublicKey and PrivateKey 继承,提供了 ValidateValidate 需要一个 RandomNumberGenerator 和级别参数,返回一个布尔值。 Validate 的注释在 CryptoMaterial state (see the CryptoMaterial Class Reference):

Level 表示验证严格性:

0 - 使用这个对象不会导致崩溃或者异常(rng 被忽略)

1 - 这个对象将能正确工作 (encrypt, sign, etc.) correctly (但可能不会检查弱 KEY 等等)

2 - 确认这个对象和正确工作,并检查安全性

3 - 做一个可能长时间的检查

实际上,我们最少应该进行第二级别的校验,最好是级别三。级别三是首选的是因为有些应用不能容忍扩展处理。级别三可能会在一些场景下需要;如,数据是高度敏感的或它进行操作决策(解密或者验证之后)非常严格。在 KEY 生成或者加载后进行验证的代码如下:

if(!rsaPrivate.Validate(rnd, 3))
throw runtime_error("Rsa private key validation failed");

if(!dsaPrivate.Validate(rnd, 3))
throw runtime_error("Dsa private key validation failed");

GnuPG ElGamal Keys

GnuPG generated ElGamal keys will fail validation at level two and higher. This is because GnuPG ElGamal keys use Lim-Lee primes, and not strong or safe primes. GnuPG’s documentation on ElGamal and its keys is available at Prime-Number-Generator Subsystem Architecture. The paper that discusses the problem that motivated GnuPG and its use of Lim-Lee primes is A Key Recovery Attack on Discrete Log-based Schemes Using a Prime Order Subgroup by Chae Hoon Lim and Pil Joong Lee.

To validate the GnuPG ElGamal key, you should obtain the unique factorization of p (from Section 4 of Lim and Lee’s paper: [math]\displaystyle{ p = q p_1 p_2 p_3 … p_n) }[/math]. Then ensure each [math]\displaystyle{ p_i }[/math] is prime and [math]\displaystyle{ |p_i| \gt l }[/math]. [math]\displaystyle{ l }[/math] is approximately the size of [math]\displaystyle{ q }[/math], and [math]\displaystyle{ q }[/math] is chosen due to Schnorr’s subgroup attacks. To obtain the unique factorization, you can get it from the peer or factor it. According to Werner Kock of GnuPG, the unique factorization was available in secring.gpg up to (and including) version 1.4.1. After version 1.4.1, you will have to factor the key. (Note: According to GnuPG’s Release Notes, version 1.4.1 was superseded in July, 2005).

If you find you need to use GnuPG and its ElGamal schemes, it is probably easiest to generate the keys using Crypto++ (since the key will validate in Crypto++ and other libraries), and then import the key into GnuPG for use.

BER and DER Encoding

img

DER 编码 RSA 的公钥和私钥。

Private Key FormatPublic Key Format 两节中,我们知道 PKCS #8 和 X.509 有一个包围在外面的 PrivateKeyInfoSubjectPublicKeyInfo。这个结构包含了如 OID 和版本等信息。在 ASN.1 中,这个结构就是算法进行操作的关键之处。 如,一个 RsaPublicKey 将会有一个公开的指数 ‘e’ 和模 ‘n’,而 DsaPrivateKey 将会有 ‘p’, ‘q’, ‘g’, and ‘x’.

Crypto++ DEREncodePrivateKey, DEREncodePublicKey, BERDecodePrivateKey, BERDecodePublicKey 函数来保存和加载 KEY 的真实序列化部分。当使用编码函数的时候,要记住,你将会失去与其他库的互操作性。

使用 DER 进行编码写出(严格的编码规则),使用 BER 进行解码读取(宽松的解码规则),这是 ITU 规定的,目的是为了推动互操作性,也有 Postel’s Law 精神。

The code below is available in cryptopp-key-encode.zip.

DER Encoding

下面的代码展示了保存 RSA 公钥的 密钥部分。注意我们必须使用 RSA::PrivateKey and RSA::PublicKey.

int main(int argc, char** argv)
{
AutoSeededRandomPool rnd;

try
{
RSA::PrivateKey rsaPrivate;
rsaPrivate.GenerateRandomWithKeySize(rnd, 3072);

RSA::PublicKey rsaPublic(rsaPrivate);

EncodePrivateKey("rsa-private.key", rsaPrivate);
EncodePublicKey("rsa-public.key", rsaPublic);

cout << "Successfully generated and saved RSA keys" << endl;
}

catch(CryptoPP::Exception& e)
{
cerr << e.what() << endl;
return -1;
}

return 0;
}

void EncodePrivateKey(const string& filename, const RSA::PrivateKey& key)
{
ByteQueue queue;
key.DEREncodePrivateKey(queue);

Encode(filename, queue);
}

void EncodePublicKey(const string& filename, const RSA::PublicKey& key)
{
ByteQueue queue;
key.DEREncodePublicKey(queue);

Encode(filename, queue);
}

void Encode(const string& filename, const BufferedTransformation& bt)
{
FileSink file(filename.c_str());

bt.CopyTo(file);
file.MessageEnd();
}

BER Decoding

BER 解码有点不灵活。 DEREncodePrivateKey and DEREncodePublicKey 的通用接口是:

BERDecodePrivateKey (BufferedTransformation &bt, bool parametersPresent, size_t size)
BERDecodePublicKey (BufferedTransformation &bt, bool parametersPresent, size_t size)

查看 rsa.cpp 中的代码, parametersPresentsize 都没有被使用 (parametersPresent 只有 DL_PrivateKey_EC<EC>需要, 它是 elliptic curve key 的基类), 所以我们传递 false。 尽管没有使用,我们传递 queue 中的字节数作为 size.

int main(int argc, char** argv)
{
AutoSeededRandomPool rnd;

try
{
RSA::PrivateKey k1;
DecodePrivateKey("rsa-private.key", k1);

RSA::PublicKey k2;
DecodePublicKey("rsa-public.key", k2);

cout << "Successfully loaded RSA keys" << endl;

////////////////////////////////////////////////////////////////////////////////////

if(!k1.Validate(rnd, 3))
throw runtime_error("Rsa private key validation failed");

if(!k2.Validate(rnd, 3))
throw runtime_error("Rsa public key validation failed");

cout << "Successfully validated RSA keys" << endl;

////////////////////////////////////////////////////////////////////////////////////

if(k1.GetModulus() != k2.GetModulus() ||
k1.GetPublicExponent() != k2.GetPublicExponent())
{
throw runtime_error("key data did not round trip");
}

cout << "Successfully round-tripped RSA keys" << endl;
}

catch(CryptoPP::Exception& e)
{
cerr << e.what() << endl;
return -1;
}

return 0;
}

void DecodePrivateKey(const string& filename, RSA::PrivateKey& key)
{
ByteQueue queue;

Decode(filename, queue);
key.BERDecodePrivateKey(queue, false /*paramsPresent*/, queue.MaxRetrievable());
}

void DecodePublicKey(const string& filename, RSA::PublicKey& key)
{
ByteQueue queue;

Decode(filename, queue);
key.BERDecodePublicKey(queue, false /*paramsPresent*/, queue.MaxRetrievable());
}

void Decode(const string& filename, BufferedTransformation& bt)
{
FileSource file(filename.c_str(), true /*pumpAll*/);

file.TransferTo(bt);
bt.MessageEnd();
}

Hex and Base64 Encoding

img

二进制和 Base64 编码的单个 RSA KEY。

二进制的 KEY,某些时候会被叫做 raw 或者 uncooked(通常这是应用在数据上,而不是特指 KEY)。

有时,这是有利(或者必须)于添加一些额外处理——如,16 进制编码或者 base64 编码。一个已经进行了额外处理的 raw KEY 有时会被程为 cookedbaked

在上面的图片中,花点时间就可以看出来,二进制字符串 0x30 0x82 ... 可能表明这是一个 raw ASN.1 。类似,也能识别出一个以 ASCII 字符串 MII... 开头的可能是一个 Base64 编码的 ASN.1 文件。

下面是 16 进制或 Base64 编码的展示。如何解码就留给读者了。

Hex Encoding and Decoding

如果我们想要在保存 KEY 的过程中用 16 进制进行编码,我们将会添加如下签名:

void SaveHexPrivateKey(const string& filename, const PrivateKey& key);
void SaveHexPublicKey(const string& filename, const PublicKey& key);

void SaveHex(const string& filename, const BufferedTransformation& bt);

最后,16 进制编码的实现如下:

void SaveHexPrivateKey(const string& filename, const PrivateKey& key)
{
ByteQueue queue;
key.Save(queue);

SaveHex(filename, queue);
}

void SaveHexPublicKey(const string& filename, const PublicKey& key)
{
ByteQueue queue;
key.Save(queue);

SaveHex(filename, queue);
}

void SaveHex(const string& filename, const BufferedTransformation& bt)
{
HexEncoder encoder;

bt.CopyTo(encoder);
encoder.MessageEnd();

Save(filename, encoder);
}

Base64 Encoding and Decoding

如果要在保存的过程中进行 Base64 编码,添加如下签名:

void SaveBase64PrivateKey(const string& filename, const PrivateKey& key);
void SaveBase64PublicKey(const string& filename, const PublicKey& key);

void SaveBase64(const string& filename, const BufferedTransformation& bt);

实现:

void SaveBase64PrivateKey(const string& filename, const PrivateKey& key)
{
ByteQueue queue;
key.Save(queue);

SaveBase64(filename, queue);
}

void SaveBase64PublicKey(const string& filename, const PublicKey& key)
{
ByteQueue queue;
key.Save(queue);

SaveBase64(filename, queue);
}

void SaveBase64(const string& filename, const BufferedTransformation& bt)
{
Base64Encoder encoder;

bt.CopyTo(encoder);
encoder.MessageEnd();

Save(filename, encoder);
}

PEM Encoded Keys

Crypto++ 并不从 PEM 编码转换回来,或者转换出去。如果需要读写 PEM 编码的 KEY,有两种选择。

  1. 添加 PEM Pack 到库中并重新编译。
  2. 使用下面的代码将 PEM 编码的 KEY 转换到 BER/DER 编码的 KEY,再使用 BERDecodePrivateKey 加载到 PubliceKey or PrivateKey 结构
string RSA_PRIV_KEY =
"-----BEGIN RSA PRIVATE KEY-----\n"
"MIIBOgIBAAJBAK8Q+ToR4tWGshaKYRHKJ3ZmMUF6jjwCS/u1A8v1tFbQiVpBlxYB\n"
"paNcT2ENEXBGdmWqr8VwSl0NBIKyq4p0rhsCAQMCQHS1+3wL7I5ZzA8G62Exb6RE\n"
"INZRtCgBh/0jV91OeDnfQUc07SE6vs31J8m7qw/rxeB3E9h6oGi9IVRebVO+9zsC\n"
"IQDWb//KAzrSOo0P0yktnY57UF9Q3Y26rulWI6LqpsxZDwIhAND/cmlg7rUz34Pf\n"
"SmM61lJEmMEjKp8RB/xgghzmCeI1AiEAjvVVMVd8jCcItTdwyRO0UjWU4JOz0cnw\n"
"5BfB8cSIO18CIQCLVPbw60nOIpUClNxCJzmMLbsrbMcUtgVS6wFomVvsIwIhAK+A\n"
"YqT6WwsMW2On5l9di+RPzhDT1QdGyTI5eFNS+GxY\n"
"-----END RSA PRIVATE KEY-----";

static string HEADER = "-----BEGIN RSA PRIVATE KEY-----";
static string FOOTER = "-----END RSA PRIVATE KEY-----";

size_t pos1, pos2;
pos1 = RSA_PRIV_KEY.find(HEADER);
if(pos1 == string::npos)
throw runtime_error("PEM header not found");

pos2 = RSA_PRIV_KEY.find(FOOTER, pos1+1);
if(pos2 == string::npos)
throw runtime_error("PEM footer not found");

// Start position and length
pos1 = pos1 + HEADER.length();
pos2 = pos2 - pos1;
string keystr = RSA_PRIV_KEY.substr(pos1, pos2);

// Base64 decode, place in a ByteQueue
ByteQueue queue;
Base64Decoder decoder;

decoder.Attach(new Redirector(queue));
decoder.Put((const byte*)keystr.data(), keystr.length());
decoder.MessageEnd();

// Write to file for inspection
FileSink fs("decoded-key.der");
queue.CopyTo(fs);
fs.MessageEnd();

try
{
CryptoPP::RSA::PrivateKey rsaPrivate;
rsaPrivate.BERDecodePrivateKey(queue, false /*paramsPresent*/, queue.MaxRetrievable());

// BERDecodePrivateKey is a void function. Here's the only check
// we have regarding the DER bytes consumed.
ASSERT(queue.IsEmpty());

AutoSeededRandomPool prng;
bool valid = rsaPrivate.Validate(prng, 3);
if(!valid)
cerr << "RSA private key is not valid" << endl;

cout << "N:" << rsaPrivate.GetModulus() << endl;
cout << "E:" << rsaPrivate.GetPublicExponent() << endl;
cout << "D:" << rsaPrivate.GetPrivateExponent() << endl;

}
catch (const Exception& ex)
{
cerr << ex.what() << endl;
exit (1);
}

如果 KEY 是用密码保护的,使用下面的 OpenSSL commands 来移除密码并转换到 PKCS #8。

openssl pkcs8 -nocrypt -in rsa-key.pem -inform PEM -topk8 -outform DER -out rsa-key.der

High Level Objects

使用 Crypto++’s 的高级对象会显得很简单,如 RSAES_PKCS1v15_Decryptor, RSAES_OAEP_SHA_Decryptor, and RSASSA_PKCS1v15_SHA_Signer 来生成密钥对, 而不是使用 RSA::PrivateKey。这是因为高级的对象将拥有域相关问题的信息(及相关的逻辑),比如使用 safe primes versus random primes。有疑问的时候,就使用高级对象。

Decryptors and Signers

通常来说, Crypto++的高级对象,如 RSAES_OAEP_SHA_Decryptor and RSASSA_PKCS1v15_SHA_Signer 拥有一个 PrivateKey。更正确点,decryptors 继承自 PK_Decryptor , signers 继承自 PK_Signer,最终都是从 PrivateKeyAlgorithm 继承。See PK_Decryptor Class Reference, PK_Signer Class Reference, and PrivateKeyAlgorithm Class Reference.

Encryptors and Verifiers

类似于 decryptors and signers, Crypto++ 高级对象如 RSAES_OAEP_SHA_Encryptor and RSASSA_PKCS1v15_SHA_Verifier 拥有一个 PublicKey。Encryptors 继承自 PK_Encryptor ,verifiers 继承自PK_Verifier,它们最终都继承自 PublicKeyAlgorithm. See PK_Encryptor Class Reference, PK_Verifier Class Reference, and PublicKeyAlgorithm Class Reference.

RSAESOAEP_SHA{Encryptor|Decryptor}

下面展示了如何使用 RSAES_OAEP_SHA_Decryptor and RSAES_OAEP_SHA_Encryptor来创建一个公私钥对。 高级对象使用组合 (“HAS A”),我们在 KEY 上调用方法前使用 AccessKey 来获取 KEY。

RSAES_OAEP_SHA_Decryptor rsaPrivate;
rsaPrivate.AccessKey().GenerateRandomWithKeySize(rnd, 3072);
RSAES_OAEP_SHA_Encryptor rsaPublic(rsaPrivate);

SavePrivateKey("rsa-private.key", rsaPrivate.AccessKey());
SavePublicKey("rsa-public.key", rsaPublic.AccessKey());

RSASSAPKCS1v15_SHA{Signer|Verifier}

下面是介绍如何使用 RSASSA_PKCS1v15_SHA_Signer and RSASSA_PKCS1v15_SHA_Verifier建立公私钥对。这些对象也使用组合 (“HAS A”),我们在 KEY 上调用方法前使用 AccessKey 来获取 KEY。

RSASSA_PKCS1v15_SHA_Signer rsaPrivate;
rsaPrivate.AccessKey().GenerateRandomWithKeySize(rnd, 3072);
RSASSA_PKCS1v15_SHA_Verifier rsaPublic(rsaPrivate);

SavePrivateKey("rsa-private.key", rsaPrivate.AccessKey());
SavePublicKey("rsa-public.key", rsaPublic.AccessKey());

Dumping PKCS #8 and X.509 Keys

现在我们已经对 KEY 的格式有了点了解了,现在我们该更深入一些了。如果要 dump KEY, Peter Guttman’s dumpasn1 在命令行上工作得很好。 visual world, Objective Systems has a free ASN.1 Viewer.

RSA PublicKey

重看 cryptopp-key-gen 保存 rsa-public.key 为 X.509 PublicKeyInfo 类型。在编程术语中,下面就是 PublicKeyInfo 大概的样子 (定义直接从 X.509 来)。

PublicKeyInfo ::= SEQUENCE {
algorithm AlgorithmIdentifier,
PublicKey BIT STRING
}

AlgorithmIdentifier ::= SEQUENCE {
algorithm ALGORITHM.id,
parameters ALGORITHM.type OPTIONAL
}

X.509 规定,这个格式包含了一个内围的 PublicKey, 但并没有指定这个 PublicKey 是什么。因为 OID 的存在,对于 RSA 公钥,类型实际上是RSAPublicKey。在这个情况下,我们可以从 X.509 或 PKCS #1 获取定义 (他们是一样的):

RSAPublicKey ::= SEQUENCE {
modulus INTEGER,
publicExponent INTEGER
}

大多数时候,公钥指数 e工作为一个库的标识符(使用默认值情况下): Crypto++ 使用 17, GnuTLS, Java, and OpenSSL 使用 65537, and Microsoft’s .Net 使用 3。注意,e 在公钥中一般会是比较低的权重,这是为了加快计算速度。

ASN.1 dump of rsa-public.key

上面就是一个 rsa 公钥的 dump 截图。PublicKeyInfo 从 0 字节开始,有 221 字节长,这是一个 ASN.1 序列。一个嵌套的序列从 偏移 3 开始,隐含表明第一个序列是 3 字节。第二个序列保存一个 OID: 1.2.840.113549.1.1.1,,这是一个 RSA KEY,还有一个 ASN.1 NULL (算法类型)。

从 偏移 18 开始( 203 字节), RSAPublicKey 开始。从上面的图上我们可以看出公钥的模,n00 F8 54 F4 ... D9 D2 15 CC ... DB 7C 5F 2D (dumpasn -a 会将所有字节列出)。后面的就代表了公钥中的e,指数。

当无法解析某些 OID 的时候,可以通过 OID Repository 有的时候还需要搜索一下网页,才知道具体是什么。

RSA PrivateKey

img

使用 dumpasn1 dump RSA 私钥。

Dumping 一个 ASN.1 编码的私钥和公钥类似。. 格式是一个PrivateKeyInfo (不是 * PublicKeyInfo)。结构如下,包含了一个版本及可选的属性字段。

PrivateKeyInfo ::= SEQUENCE {
version Version,
privateKeyAlgorithm PrivateKeyAlgorithmIdentifier,
privateKey PrivateKey,
attributes [0] IMPLICIT Attributes OPTIONAL
}

我们关心的是被 PrivateKeyInfo 包围的 PrivateKey 。和 PublicKey 类似,, 因为 PrivateKey 有一个 RSA OID 在 PrivateKeyInfo,类型就是 RSAPrivateKey.

先就是包含一些我们常见概念的结构了:e, d, p, q。指数用来预计算一些东西,这样的话,KEY 的拥有者就不需要使用 e, d, n 和中国剩余定理来重新计算一些拥有的内部值了。

RSAPrivateKey ::= SEQUENCE {
version Version,
modulus INTEGER,
publicExponent INTEGER,
privateExponent INTEGER,
prime1 INTEGER,
prime2 INTEGER,
exponent1 INTEGER,
exponent2 INTEGER,
coefficient INTEGER
}

DSA PublicKey

imgDSA public key

dump a DSA 的步骤和 RSA 类似。 DSA 的 OID 是 1.2.840.10040.4.1。DSA 会在 可选参数中包含 DSS 的域参数。

PublicKeyInfo ::= SEQUENCE {
algorithm AlgorithmIdentifier,
PublicKey BIT STRING
}

AlgorithmIdentifier ::= SEQUENCE {
algorithm ALGORITHM.id,
parameters Dss-Parms
}

Dss-Parms ::= SEQUENCE {
p INTEGER,
q INTEGER,
g INTEGER
}

DSA 的公钥只是一个简单的指数 Y,通过 bit string 编码。

DSAPublicKey ::= BITSTRING {
publicExponent INTEGER
}

DSA PrivateKey

imgDSA private key

DSA 也使用 OID 1.2.840.10040.4.1 。公钥使用的是 bit string(编码公钥指数),私钥使用的是 octect string 来编码私钥指数 X

PrivateKeyInfo ::= SEQUENCE {
version Version,
algorithm AlgorithmIdentifier,
PrivateKey OCTETSTRING
}

AlgorithmIdentifier ::= SEQUENCE {
algorithm ALGORITHM.id,
parameters Dss-Parms
}

Dss-Parms ::= SEQUENCE {
p INTEGER,
q INTEGER,
g INTEGER
}

DSAPrivateKey ::= OCTETSTRING {
privateExponent INTEGER
}

OpenSSL Commands

下面提供的命令是为了让需要与 OpenSSL 库互操作的同学们使用。

默认情况下,Crypto++ 使用 DER 编码,而 OpenSSL 使用 PEM 编码。PEM 编码是一个 base64 编码,包括了头部和尾部。我们可以用 -inform-outform 来在 DER 和 PEM 间转换。

$ openssl genrsa -out rsa-openssl.pem 256
Generating RSA private key, 256 bit long modulus
...
$ cat rsa-openssl.pem
-----BEGIN RSA PRIVATE KEY-----
MIGrAgEAAiEA1+FPfSd+EaXYYU0LlPZ0K+k3uWFUb+s/8NxV9PEl1psCAwEAAQIg
RjBfY9W/S4WcgKZIKbqnsjYuQO5nEAGAjx/XY9za48ECEQD2EpdAXBwXDBC9NQGD
yUAJAhEA4JblIvDA6WOWr4KO79oCgwIQUfoFi3pkHUV2uiHTfFzqSQIRAJOcWamn
VwOOgwGD6/JU9YUCEQDUVTl+0TNEiA3SbsvbRya7
-----END RSA PRIVATE KEY-----

genpkey(1) can also be used.

RSA Public Key (SubjectPublicKeyInfo)

下面的命令创建一个 PKCS #1/X.509 格式的公钥,它可以被 Crypto++ 使用。复习一下它的格式是 SubjectPublicKeyInfo 内含一个 OID (1.2.840.113549.1.1.1), 以及 RSAPublicKey (the inner key material).

$ openssl genrsa -out rsa-openssl.pem 3072
$ openssl rsa -in rsa-openssl.pem -pubout -outform DER -out rsa-openssl.der

RSA Private Key (PrivateKeyInfo)

下面的命令创建一个未加密的 PKCS #8 格式 RSA 私钥,可以被 Crypto++ 使用。格式是 PrivateKeyInfo 加一个 OID (1.2.840.113549.1.1.1), version, and the RSAPrivateKey (the inner key material).

$ openssl genrsa -out rsa-openssl.pem 3072
$ openssl pkcs8 -nocrypt -in rsa-openssl.pem -inform PEM -topk8 -outform DER -out rsa-openssl.der

DSA Public Key (SubjectPublicKeyInfo)

PKCS #1/X.509 格式的 DSA 公钥,Crypto++ 可用。

格式:SubjectPublicKeyInfo 内有一个 OID (1.2.840.10040.4.1), DSS parameters, and the DSAPublicKey (the inner key material).

$ openssl dsaparam -out dsa-param-openssl.pem 1024
$ openssl gendsa -out dsa-openssl.pem dsa-param-openssl.pem
$ openssl dsa -in dsa-openssl.pem -pubout -outform DER -out dsa-openssl.der

DSA Private Key (PrivateKeyInfo)

PKCS #1/X.509 格式未加密的 DSA 私钥,Crypto++ 可用。

格式:PrivateKeyInfo 内一个 OID (1.2.840.10040.4.1), version, DSS parameters, and the DSAPrivateKey (the inner key material).

$ openssl dsaparam -out dsa-param-openssl.pem 1024
$ openssl gendsa -out dsa-openssl.pem dsa-param-openssl.pem
$ openssl pkcs8 -nocrypt -in dsa-openssl.pem -inform PEM -topk8 -outform DER -out dsa-openssl.der

ECC Public Key (SubjectPublicKeyInfo)

The following commands create a ECC public key in PKCS #1/X.509 format which Crypto++ can consume. The curve is SEC’s prime 256.

$ openssl ecparam -param_enc explicit -name secp256k1 -genkey -outform PEM -out ec-openssl.pem
$ openssl ec -param_enc explicit -inform PEM -in ec-openssl.pem -pubout -outform DER -out ec-openssl.der

ECC Private Key (PrivateKeyInfo)

The following command creates an unencrypted ECC private key in PKCS #8 format which Crypto++ can consume. The curve is SEC’s prime 256.

$ openssl ecparam -name secp256k1 -genkey -param_enc explicit -outform DER -out ec-openssl.der

GnuTLS Commands

The following GnuTLS commands should help those who use Gnu’s secure transport library, GnuTLS.

$ gnutls-cli --version
gnutls-cli (GnuTLS) 2.8.5
Copyright (C) 2009 Free Software Foundation, Inc.
License GPLv3+: GNU GPL version 3 or later <http://gnu.org/licenses/gpl.html>.
...
Written by Nikos Mavrogiannopoulos.

RSA Public Key (SubjectPublicKeyInfo)

According to Nikos Mavrogiannopoulos, GnuTLS 2.11 and above can use certtool‘s --certificate-pubkey switch. See How to Create/Derive DER Encoded Public Keys (PKCS #1/X.509 SubjectPublicKeyInfo)?.

RSA Private Key (PrivateKeyInfo)

$ certtool --generate-privkey --pkcs8 --outder --bits 3072 --outfile rsa-gnutls.der

DSA Public Key (SubjectPublicKeyInfo)

According to Nikos Mavrogiannopoulos, GnuTLS 2.11 and above can use certtool‘s --certificate-pubkey switch. See How to Create/Derive DER Encoded Public Keys (PKCS #1/X.509 SubjectPublicKeyInfo)?.

DSA Private Key (PrivateKeyInfo)

$ certtool --dsa --generate-privkey --pkcs8 --outder --bits 1024 --outfile dsa-gnutls.der