TL;DR: OpenSSL 支持多种已弃用的旧格式。当输入是 RSA 私钥对时,openssl rsa -pubout 似乎有一个特殊情况。最后,我给出了两个命令序列,让 OP 的加密(和相应的解密)成功;一种是使用 DER 而不是 PEM,另一种是首先发出 DER,然后将其转换为 PEM……奇怪的是,使用openssl rsa,当输入是 DER 编码的公钥时输出现代格式……去数字。无论如何,下面是对 OP 最初尝试失败的原因的详细描述。至于“为什么记录得这么差”……欢迎使用 OpenSSL。好好学习源目录结构,因为它会加快你的 grepping。 :)
长解释(细节取自 OpenSSL 1.0.2m)
OpenSSL rsautl 应用默认 -inform 参数(传入密钥表示)为 format = FORMAT_PEM。
然后它使用如下格式为传入密钥选择读取器:
else if (format == FORMAT_PEMRSA) {
RSA *rsa;
rsa = PEM_read_bio_RSAPublicKey(key, NULL,
(pem_password_cb *)password_callback,
&cb_data);
/* ... */
}
else if (format == FORMAT_PEM) {
pkey = PEM_read_bio_PUBKEY(key, NULL,
(pem_password_cb *)password_callback,
&cb_data);
所以,我们将使用PEM_read_bio_PUBKEY。注意上面FORMAT_PEMRSA 的条目...我们将同时开发通向PEM_read_bio_RSAPublicKey 的路径。这两个阅读器都是通过一些宏定义的
crypto/pem/pem_all.c:427:IMPLEMENT_PEM_rw(PUBKEY, EVP_PKEY, PEM_STRING_PUBLIC, PUBKEY)
crypto/pem/pem_all.c:241:IMPLEMENT_PEM_rw_const(RSAPublicKey, RSA, PEM_STRING_RSA_PUBLIC, RSAPublicKey)
展开到
# define IMPLEMENT_PEM_rw(name, type, str, asn1) \
IMPLEMENT_PEM_read(name, type, str, asn1) \
IMPLEMENT_PEM_write(name, type, str, asn1)
# define IMPLEMENT_PEM_rw_const(name, type, str, asn1) \
IMPLEMENT_PEM_read(name, type, str, asn1) \
IMPLEMENT_PEM_write_const(name, type, str, asn1)
反过来使用
# define IMPLEMENT_PEM_read(name, type, str, asn1) \
IMPLEMENT_PEM_read_bio(name, type, str, asn1) \
IMPLEMENT_PEM_read_fp(name, type, str, asn1)
将实际定义构建为:
# define IMPLEMENT_PEM_read_bio(name, type, str, asn1) \
type *PEM_read_bio_##name(BIO *bp, type **x, pem_password_cb *cb, void *u)\
{ \
return PEM_ASN1_read_bio((d2i_of_void *)d2i_##asn1, str,bp,(void **)x,cb,u); \
}
特别值得注意的是第三个参数,str,对于 PEM_read_bio_PUBKEY 版本,它作为 PEM_STRING_PUBLIC 传递,对于 PEM_read_bio_RsaPublicKey 版本,它作为 PEM_STRING_RSA_PUBLIC 传递。这些字符串分别是,
./crypto/pem/pem.h:122:# define PEM_STRING_PUBLIC "PUBLIC KEY"
./crypto/pem/pem.h:124:# define PEM_STRING_RSA_PUBLIC "RSA PUBLIC KEY"
查看PEM_ASN1_read_bio 的实现,我们看到它调用了PEM_bytes_read_bio
void *PEM_ASN1_read_bio(d2i_of_void *d2i, const char *name, BIO *bp, void **x,
pem_password_cb *cb, void *u)
{
const unsigned char *p = NULL;
unsigned char *data = NULL;
long len;
char *ret = NULL;
if (!PEM_bytes_read_bio(&data, &len, NULL, name, bp, cb, u))
return NULL;
/* ... */
PEM_bytes_read_bio 读取密钥文件,将其分成几部分。 nm 获取----- 部分之间的标记,例如-----BEGIN RSA PUBLIC KEY-----,然后由check_pem 检查。
int PEM_bytes_read_bio(unsigned char **pdata, long *plen, char **pnm,
const char *name, BIO *bp, pem_password_cb *cb,
void *u)
{
EVP_CIPHER_INFO cipher;
char *nm = NULL, *header = NULL;
unsigned char *data = NULL;
long len;
int ret = 0;
for (;;) {
if (!PEM_read_bio(bp, &nm, &header, &data, &len)) {
if (ERR_GET_REASON(ERR_peek_error()) == PEM_R_NO_START_LINE)
ERR_add_error_data(2, "Expecting: ", name);
return 0;
}
if (check_pem(nm, name))
并且check_pem 进行如下检查,其中nm 是在文件中找到的字符串,name 是从PEM_read_bio_XXX 函数中硬编码的位置传递过来的。
static int check_pem(const char *nm, const char *name)
{
/* Normal matching nm and name */
if (!strcmp(nm, name))
return 1;
/* special cases for
PKCS8 format (BEGIN PRIVATE KEY or BEGIN ENCRYPTED PRIVATE KEY)
Various things ending in PARAMETERS
Various X509 related files
PKCS7 format (BEGIN PKCS7 or BEGIN PKCS7 SIGNED DATA)
CMS things (BEGIN CMS) */
/* ... */
它只是在name 和nm 之间执行strcmp。因此,要成功读取带有PEM_read_bio_PUBKEY 的RSA 公钥,它需要以-----BEGIN PUBLIC KEY----- 开头...但是查看OP 指令生成的密钥,我们会找到-----BEGIN RSA PUBLIC KEY-----。但那是对应于PEM_read_bio_RsaPublicKey 的字符串......也许我们可以使用-inform 来选择format = FORMAT_PEMRSA,并让rsautl 以这种方式读取我们的公钥。 rsautl 使用一个名为 str2fmt 的函数来解析 -inform 参数。一起来看看吧:
int str2fmt(char *s)
{
if (s == NULL)
return FORMAT_UNDEF;
if ((*s == 'D') || (*s == 'd'))
return (FORMAT_ASN1);
else if ((*s == 'T') || (*s == 't'))
return (FORMAT_TEXT);
else if ((*s == 'N') || (*s == 'n'))
return (FORMAT_NETSCAPE);
else if ((*s == 'S') || (*s == 's'))
return (FORMAT_SMIME);
else if ((*s == 'M') || (*s == 'm'))
return (FORMAT_MSBLOB);
else if ((*s == '1')
|| (strcmp(s, "PKCS12") == 0) || (strcmp(s, "pkcs12") == 0)
|| (strcmp(s, "P12") == 0) || (strcmp(s, "p12") == 0))
return (FORMAT_PKCS12);
else if ((*s == 'E') || (*s == 'e'))
return (FORMAT_ENGINE);
else if ((*s == 'H') || (*s == 'h'))
return FORMAT_HTTP;
else if ((*s == 'P') || (*s == 'p')) {
if (s[1] == 'V' || s[1] == 'v')
return FORMAT_PVK;
else
return (FORMAT_PEM);
} else
return (FORMAT_UNDEF);
}
不。没有办法让它返回 FORMAT_PEMRSA。
那么,如果我们编辑公钥文件以使标记改为 -----BEGIN PUBLIC KEY----- 会怎样?
$ sed -e "s/RSA PUBLIC/PUBLIC/" public_key.pem > public_key_mod.pem
$ echo "this is the cleartext" | openssl rsautl -encrypt -out encrypted_with_pub_key -pubin -inkey public_key_mod.pem
unable to load Public Key
没有。
如果我们将公钥提取到 DER 而不是 PEM 会怎样?
$ openssl rsa -in private_key.pem -out public_key.der -outform DER -pubout
writing RSA key
$ echo "this is the cleartext" | openssl rsautl -encrypt -out encrypted_with_pub_key -pubin -inkey public_key.der -keyform DER
$ openssl rsautl -decrypt -in encrypted_with_pub_key -inkey private_key.pem
this is the cleartext
成功!也许我们可以让 OpenSSL 将 DER 密钥转换为与rsautl 兼容的形式
$ openssl rsa -in public_key.der -inform DER -pubin -out test.pem
writing RSA key
$ cat test.pem
-----BEGIN PUBLIC KEY-----
MFwwDQYJKoZIhvcNAQEBBQADSwAwSAJBAM3uGdU6YtwI5S8K+GgddW8KhrzmSFVI
6cvBT+XqOuSVo+n8VyUfADHw4rPxjy/dWDpyOxzWdTg8VZ77Vs06af8CAwEAAQ==
-----END PUBLIC KEY-----
$ echo "this is the cleartext" | openssl rsautl -encrypt -out encrypted_with_pub_key -pubin -inkey test.pem
$ openssl rsautl -decrypt -in encrypted_with_pub_key -inkey private_key.pem
this is the cleartext
所以是的...看起来openssl rsa 仅在将BEGIN RSA PRIVATE KEY 作为输入时才写入过时的BEGIN RSA PUBLIC KEY 表单。