十年网站开发经验 + 多家企业客户 + 靠谱的建站团队
量身定制 + 运营维护+专业推广+无忧售后,网站问题一站解决
Openssl之PEM系列
创新互联专注于如皋网站建设服务及定制,我们拥有丰富的企业做网站经验。 热诚为您提供如皋营销型网站建设,如皋网站制作、如皋网页设计、如皋网站官网定制、小程序定制开发服务,打造如皋网络公司原创品牌,更为您提供如皋网站排名全网营销落地服务。
PEM全称是Privacy Enhanced Mail,该标准定义了加密一个准备要发送邮件的标准,主要用来将各种对象保存成PEM格式,并将PEM格式的各种对象读取到相应的结构中。它的基本流程是这样的:
Proc-Type,4:ENCRYPTED
DEK-Info: cipher-name, ivec其中,第一个头信息标注了该文件是否进行了加密,该头信息可能的值包括ENCRYPTED(信息已经加密和签名)、MIC-ONLY(信息经过数字签名但没有加密)、MIC-CLEAR(信息经过数字签名但是没有加密、也没有进行编码,可使用非PEM格式阅读)以及CLEAR(信息没有签名和加密并且没有进行编码,该项好象是openssl自身的扩展,但是并没有真正实现);;第二个头信息标注了加密的算法以及使用的ivec参量,ivec其实在这儿提供的应该是一个随机产生的数据序列,与块加密算法中要使用到的初始化变量(IV)不一样。
—–BEGIN PRIVACY-ENHANCED MESSAGE—–
在这些信息的后面加上如下形式尾标注信息:
—–END PRIVACY-ENHANCED MESSAGE—–
上面是openssl的PEM文件的基本结构,需要注意的是,Openssl并没有实现PEM的全部标准,它只是对openssl中需要使用的一些选项做了实现,详细的PEM格式,请参考RFC1421-1424。
下面是一个PEM编码的经过加密的DSA私钥的例子:
—–BEGIN DSA PRIVATE KEY—–
Proc-Type: 4,ENCRYPTED
DEK-Info: DES-EDE3-CBC,F80EEEBEEA7386C4
GZ9zgFcHOlnhPoiSbVi/yXc9mGoj44A6IveD4UlpSEUt6Xbse3Fr0KHIUyQ3oGnSmClKoAp/eOTb5Frhto85SzdsxYtac+X1v5XwdzAMy2KowHVk1N8A5jmE2OlkNPNtof132MNlo2cyIRYaa35PPYBGNCmUm7YcYS8O90YtkrQZZTf4+2C4kllhMcdkQwkrFWSWC8YOQ7w0LHb4cX1FejHHom9Nd/0PN3vn3UyySvfOqoR7nbXkrpHXmPIr0hxXRcF0aXcV/CzZ1/nfXWQf4o3+oD0T22SDoVcZY60IzI0oIc3pNCbDV3uKNmgekrFdqOUJ+QW8oWp7oefRx62iBfIeC8DZunohMXaWAQCU0sLQOR4yEdeUCnzCSywe0bG1diD0KYaEe+Yub1BQH4aLsBgDjardgpJRTQLq0DUvw0/QGO1irKTJzegEDNVBKrVnV4AHOKT1CUKqvGNRP1UnccUDTF6miOAtaj/qpzra7sSk7dkGBvIEeFoAg84kfh9hhVvF1YyzC9bwZepruoqoUwke/WdNIR5ymOVZ/4Liw0JdIOcq+atbdRX08niqIRkfdsZrUj4leo3zdefYUQ7w4N2Ns37yDFq7
—–END DSA PRIVATE KEY—–
有时候PEM编码的东西并没有经过加密,只是简单进行了BASE64编码,下面是一个没有加密的证书请求的例子:
—–BEGIN CERTIFICATE REQUEST—–
MIICVTCCAhMCAQAwUzELMAkGA1UEBhMCQVUxEzARBgNVBAgTClNvbWUtU3RhdGUxITAfBgNVBAoTGEludGVybmV0IFdpZGdpdHMgUHR5IEx0ZDEMMAoGA1UEAxMDUENBMIIBtTCCASkGBSsOAwIMMIIBHgKBgQCnP26Fv0FqKX3wn0cZMJCaCR3aajMexT2GlrMV4FMuj+BZgnOQPnUxmUd6UvuF5NmmezibaIqEm4fGHrV+hktTW1nPcWUZiG7OZq5riDb77Cjcwtelu+UsOSZL2ppwGJU3lRBWI/YV7boEXt45T/23Qx+1pGVvzYAR5HCVW1DNSQIVAPcHMe36bAYD1YWKHKycZedQZmVvAoGATd9MA6aRivUZb1BGJZnlaG8w42nh6bNdmLsohkj83pkEP1+IDJxzJA0gXbkqmj8YlifkYofBe3RiU/xhJ6h7kQmdtvFNnFQPWAbuSXQHzlV+I84W9srcWmEBfslxtU323DQph3j2XiCTs9v15AlsQReVkusBtXOlan7YMu0OArgDgYUAAoGBAKbtuR5AdW+ICjCFe2ixjUiJJzM2IKwe6NZEMXg39+HQ1UTPTmfLZLps+rZfolHDXuRKMXbGFdSF0nXYzotPCzi7GauwEJTZyr27ZZjA1C6apGSQ9GzuwNvZ4rCXystVEagAS8OQ4H3D4dWS17Zg31ICb5o4E5r0z09o/Uz46u0VoAAwCQYFKw4DAhsFAAMxADAuAhUArRubTxsbIXy3AhtjQ943AbNBnSICFQCu+g1iW3jwF+gOcbroD4S/ZcvB3w==
—–END CERTIFICATE REQUEST—–
可以看到,该文件没有了前面两个头信息。大家如果经常使用openssl的应用程序,就对这些文件格式很熟悉了。
openssl中定义的PEM相关结构体如下(openssl\pem.h),这些结构体是所有PEM系列函数的基础。
下面定义的是PEM一个高层应用结构,该结构通过PEM_SealInit进行初始化,最后使用PEM_SealFinal进行释放,该结构定义了PEM中要使用的编码算法、信息摘要算法以及加密算法。
typedef struct PEM_Encode_Seal_st
{
EVP_ENCODE_CTX encode;
EVP_MD_CTX md;
EVP_CIPHER_CTX cipher;
} PEM_ENCODE_SEAL_CTX;
下面定义了PEM_CTX中的一个子结构,用来保存用户的信息
typedef struct pem_recip_st
{
char *name;
X509_NAME *dn;
int cipher;
int key_enc;
} PEM_USER;
下面是PEM主结构体PEM_CTX结构的定义,我们将在注释里面对必要的参数进行说明。
typedef struct pem_ctx_st
{
int type;//结构类型
struct
{
int version;//版本号
int mode;//编码方式
} proc_type;//Proc_Type字段信息,包括版本号和编码方式
char *domain;
struct
{
int cipher;
} DEK_info;//定义了PEM中DEK_info字段的信息
PEM_USER *originator;
int num_recipient;
PEM_USER **recipient;
#ifndef OPENSSL_NO_STACK
STACK *x509_chain;//保存证书链
#else
char *x509_chain; //保存证书链
#endif
EVP_MD *md; //签名算法类型,指定了信息摘要算法和签名算法
int md_enc; //信息摘要算法是否进行了加密(签名)
int md_len; //摘要信息的长度
char *md_data; //摘要信息,可以是经过了加密(签名)的信息
EVP_CIPHER *dec;//数据加密算法
int key_len; //密钥长度
unsigned char *key; //加密密钥
int data_enc; //数据是否加密标志
int data_len; //数据长度
unsigned char *data; //数据
} PEM_CTX;
下面我们对PEM_CTX结构体中一些重要的参数做详细的说明
该参数指明了PEM_CTX结构的类型,目前包括了以下定义的类型:
#define PEM_OBJ_UNDEF 0
#define PEM_OBJ_X509 1
#define PEM_OBJ_X509_REQ 2
#define PEM_OBJ_CRL 3
#define PEM_OBJ_SSL_SESSION 4
#define PEM_OBJ_PRIV_KEY 10
#define PEM_OBJ_PRIV_RSA 11
#define PEM_OBJ_PRIV_DSA 12
#define PEM_OBJ_PRIV_DH 13
#define PEM_OBJ_PUB_RSA 14
#define PEM_OBJ_PUB_DSA 15
#define PEM_OBJ_PUB_DH 16
#define PEM_OBJ_DHPARAMS 17
#define PEM_OBJ_DSAPARAMS 18
#define PEM_OBJ_PRIV_RSA_PUBLIC 19
可以看到,这些类型基本上包括了所有openssl中要使用的基本结构
该参数是保存了PEM标准中Proc_Type字段的信息(参考《openssl之PEM系列之1》),可以看到,该结构包括两个字段,第一个字段version是版本号,第二个字段mode是信息的编码方式,目前定义了四种,如下:
#define PEM_TYPE_ENCRYPTED 10
#define PEM_TYPE_MIC_ONLY 20
#define PEM_TYPE_MIC_CLEAR 30
#define PEM_TYPE_CLEAR 40
这四个值的意义可以参考《openssl之PEM系列之1》。值得注意是,在openssl实现的PEM文件中,最后一个PEM_TYPE_CLEAR其实并没有用到。
该参数定义了PEM中DEK_info字段的信息,本来该参数应该含有两个字段,包括加密算法和IV。但是由于历史原因,openssl中原有的非标准的IV字段在新版的openssl中取消了,所以就剩下一个算法定义了,目前支持的算法如下述的定义:
#define PEM_DEK_DES_CBC 40
#define PEM_DEK_IDEA_CBC 45
#define PEM_DEK_DES_EDE 50
#define PEM_DEK_DES_ECB 60
#define PEM_DEK_RSA 70
#define PEM_DEK_RSA_MD2 80
#define PEM_DEK_RSA_MD5 90
PEM系列函数中很多参数是相同意义的,也就是说通用的。本节将对这些通用参数的意义进行介绍,以便于后述章节能够更方便流畅地进行PEM系列函数的介绍。
如果函数有该参数,则定义了进行数据读写BIO接口。
如果函数包含了该参数,则定义了进行数据读写的FILE指针。
PEM读操作的系列函数都有TYPE **x 和返回TYEP *指针的参数。这里的TYPE可以为任何函数要使用的结构体,如DSA或X509之类的。如果参数x是NULL,那么该参数将被忽略。如果x不是NULL,但是*x是NULL,那么返回的结构体就会写入到*x里面。如果x和*x都不是NULL,那么函数就试图重用*x中的结构体。这中函数总是返回一个执行结构体的指针(x的值),如果出错,就返回NULL。
enc参数定义了PEM函数写私钥的时候采用的加密算法。加密是在PEM层进行的。如果该参数为NULL,那么私钥就会以不加密的形式写入相应的接口。
cb参数定义了回调函数,该回调函数在加密PEM结构体(一般来说是私钥)需要口令的时候使用。
主要在PEM写系列函数里面使用,如果该参数不为NULL,那么kstr中klen字节数据就用来作为口令,此时,cb参数就被忽略了。
如果cb参数为NULL,而u参数不为NULL,那么u参数就是一个以NULL结束的字符串用作口令。如果cb和u参数都是NULL,那么缺省的回调函数就会并使用,该函数一般在当前的终端提示输入口令,并且关掉了回显功能。
因为缺省的回调函数基于终端的,有时候不适合使用(如GUI程序),所以可以使用替换的回调函数。回到函数的形式如下:
int cb(char *buf, int size, int rwflag, void *u);
在该函数中,buf是保存口令的参数。size是考虑最大的长度(如buf的长度)。rwflag是一个读写标志,0的时候为读操作,1的时候为写操作。当rwflag为1的时候,典型的函数一般会要求用户验证口令(如输入两次)。u参数跟上述PEM函数的u参数意义是一样的,它允许应用程序使用固定的数据作为参数传给回调函数。回调函数必须返回口令字符的数目,如果出错返回0。
本次介绍的函数是处理PEM结构里面一些字段信息的函数,这些函数在一般应用中可能不会用到,但是深入一点的应用,恐怕就避免不了。此外,了解这些应用,对于加深对PEM结构的理解也是很有好处的。下面是其中相关一些函数的定义(openssl\pem.h):
int PEM_get_EVP_CIPHER_INFO(char *header, EVP_CIPHER_INFO *cipher);
int PEM_do_header (EVP_CIPHER_INFO *cipher, unsigned char *data,long *len,pem_password_cb *callback,void *u);
void PEM_proc_type(char *buf, int type);
void PEM_dek_info(char *buf, const char *type, int len, char *str);
该函数是通过给定参数type返回一个标准的PEM文件的Proc-Type字段信息。返回的信息写入到buf参数里面去,所以要求buf分配的内存空间必须足够大。事实上,该函数返回的字符串不外乎下面四种结果:
当type为PEM_TYPE_ENCRYPTED,返回字符串为”Proc-Type: 4,ENCRYPTED\n”
当type为PEM_TYPE_MIC_CLEAR,返回字符串为”Proc-Type: 4,MIC-CLEAR\n”
当type为PEM_TYPE_MIC_ONLY,返回字符串为”Proc-Type: 4,MIC-ONLY\n”
当type为其它值时,返回字符串为 “Proc-Type: 4,BAD-TYPE\n”
事实上,虽然上字段信息中有MIC(信息摘要)选项,但openssl的PEM库并没有实现MIC计算的功能。当然,可以通过使用RSA-MD系列函数将PEM的数据信息进行摘要并将该结果作为PEM的MIC。你可以通过PEM_dek_info函数产生MIC-info头信息,然后写入到PEM结构中,不过据openssl的说明,这需要的时间可能会比较长,大概5分钟左右。
该函数跟上述函数相似,是根据type参数生成DEK-info字段的信息,返回并写入到buf里面。参数str里应该是提供了ivec变量的值,参数len是str的长度(单位是字节)。在这里,参数type应该为加密算法的名字,原则上这个字符串可以是任意的,但是为了其它程序能够正确解释该字段,你可以先得到算法相应的NID,然后通过调用nid2sn得到该算法的简称作为type参数。例如我们需要在PEM_ASN1_write_bio中使用算法结构enc,那么可以调用下面函数:
objstr=OBJ_nid2sn(EVP_CIPHER_nid(enc));
此时objstr就是一个包含了算法enc的简称的字符串。然后我们就可以通过下面的语句在PEM_dek_info函数中使用这个字符串了:
PEM_dek_info(buf,objstr,8,(char *)iv);
该函数并非顾名思义,事实上它完成了对一个PEM编码对象的的解密工作(如果该PEM对象需要进行解密),该函数通常是被PEM_read_bio所调用的。在调用该函数之前,应该已经将PEM文件的一些头信息得到,以便于正确进行解密操作。其中,DEK-info字段的信息应该在调用本函数之前进行正确的处理,从而通过该字段的名字和ivec得到相应的EVP_CIPHER结构信息和IV变量,作为本函数的cipher参数。
如果PEM文件没有DEK-info字段,那么该函数简单返回1,操作成功,因为不需要进行解密操作。如果不是的话,那么该函数就需要一个口令来进行解密。首先,它会试图从callback参数(一个回调函数)中得到该口令。回调函数的格式如下:
callback(buffer, blen, verify)
其中,参数buffer是保存返回口令的地方,blen是buffer的最大长度,verify参数是指明是否需要口令验证(就是要求用户输入两次相同的口令),默认的是0。
如果callback参数为NULL,而u参数不为NULL,那么u参数就会以NULL为结束符的字符串作为口令写入到buffer中;如果callback和u参数都为NULL,那么就会调用缺省的callback函数(关于u的具体意义,请参考《openssl之PEM系列之3》)。PEM_do_header函数得到口令后,就使用该口令(包括长度信息)跟cipher参数种的ivec变量一起对数据进行解密。解密后的数据保存在data中,长度信息保存在plen中。该函数操作成功返回1,否则返回0。
该函数一般也被PEM_read_bio函数调用。在调用该函数之前,PEM的Proc-Type头信息应该已经作为明文被读入到header参数中。如果header为NULL,那么函数成功返回1,因为没有什么头信息要处理。如果不为NULL,那么该函数首先确定header信息是否以“Proc-Type:4,ENCRYPTED”开头,如果是其它形式的,该函数将返回0,不进行处理。之后,函数开始读取DEK-info字段的信息,然后函数通过该字段的加密算法名字使用EVP_get_cihperbyname得到一个EVP_CIPHER结构,并保存在参数cipher->cipher中;然后函数再通过调用内部的函数得到ivec的值,并保存在cipher->iv中。成功操作返回1,否则返回0。
需要注意的是,因为该函数调用了EVP_get_cipherbyname,所以在调用本函数前,应该先调用EVP_add_cipher和EVP_add_alias,或者调用SSLeay_add_all_algorithms,从而将所有加密算法的信息载入到程序中。具体的情况请参考《openssl之EVP系列》相关章节。
该系列函数完成了对PEM对象以及相关密钥和IV向量的加密编码工作,以便于数据的保存和传送,主要包括以下函数(openssl\pem.h):
int PEM_SealInit(PEM_ENCODE_SEAL_CTX *ctx, EVP_CIPHER *type,EVP_MD *md_type, unsigned char **ek, int *ekl,unsigned char *iv, EVP_PKEY **pubk, int npubk);
void PEM_SealUpdate(PEM_ENCODE_SEAL_CTX *ctx, unsigned char *out, int *outl,unsigned char *in, int inl);
int PEM_SealFinal(PEM_ENCODE_SEAL_CTX *ctx, unsigned char *sig,int *sigl,unsigned char *out, int *outl, EVP_PKEY *priv);
void PEM_SignInit(EVP_MD_CTX *ctx, EVP_MD *type);
void PEM_SignUpdate(EVP_MD_CTX *ctx,unsigned char *d,unsigned int cnt);
int PEM_SignFinal(EVP_MD_CTX *ctx, unsigned char *sigret,unsigned int *siglen, EVP_PKEY *pkey);
void ERR_load_PEM_strings();
其中,PEM_Seal*系列函数完成了对PEM对象、密钥和IV变量的加密编码工作,PEM_Sign系列函数完成了对PEM进行数字签名的工作。
该函数为后续的PEM_SealUpdate和PEM_SealFinal函数做初始化工作。首先,该函数使用参数md_type调用函数EVP_SignInit对信息摘要结构ctx->md进行初始化。然后,该函数通过参数type找到相应的EVP_CIPHER结构,产生适用于该算法的密钥和ivec变量并保存在该算法结构中,然后使用参数pubk的公钥调用函数EVP_SealInit对该密钥进行加密。加密后的秘钥保存在参数ek里面,其长度保存在ekl里面,这些数据都是调用了EVP_EncodeUpdate函数经过了BASE64编码的。因为密钥和IV已经保存在ctx->cipher中,所以,可以被后续的函数用来对PEM对象进行加密处理。该函数成功操作返回正值,否则返回0或-1。
需要注意的是,因为本函数也使用了加密算法名字查找算法结构,所以在调用本函数之前必须加载该静态算法结构栈。
该函数用来完成对PEM对象信息体的加密和编码,使用的加密密钥是PEM_SealInit函数产生的。该函数对参数in中的inl个字节的数据采用ctx->cipher提供的对称加密算法结构(已经包含了密钥和IV)进行加密操作,然后调用EVP_EncodeUpdate进行BASE64编码后保存在参数out里面,outl是out里有效数据的长度信息。在此同时,该函数也调用函数EVP_SignUpdate函数使用ctx->md的摘要算法结构对参数in里的数据进行了信息摘要操作,不过暂时没有输出,等调用了PEM_SealFinal函数的时候进行输出。
需要注意的是,该函对输入的信息in的长度做了限制,不能大于1200字节,否则将超过1200字节的信息简单丢弃。
该函数完成整个PEM_Seal系列的操作。首先,它完成了之前使用PEM_SealUpdate函数进行处理的数据的对称加密工作,将数据进行BASE64编码并输出到参数out,outl保存了out数据的有效长度。同时,该函数还完成了信息摘要工作,并使用参数priv的私钥对该信息进行签名(加密),将结果经过BASE64编码后输出到参数sig,sigl是sig有效数据的长度信息。该函数成功操作返回1,否则返回0。
需要注意的是,该函数运行完后,就将ctx->md和ctx->cipher结构释放清除掉了,所以如果你想保存对称加密算法使用的密钥和IV的话,你需要在调用本函数之前就保存一个备份。当然,一般情况下是不会这么做的,因为这些密钥应该是临时密钥,只用来加密一个信息。
完成上述三个函数的操作之后,你就得到了加密后的密钥、IV(从PEM_SealInit函数)以及PEM对象信息体,并且这些都是经过BASE64编码的。然后,你就可以将这些信息发送给接受方了。对方接受到这些信息后,使用他自己的私钥以及你的公钥,就能进行正确的数据解密和验证。
这三个函数完成的功能跟EVP_Sign系列函数是一样的,其实,前面两个函数就简单调用了EVP_SignInit和EVP_SignUpdate函数。PEM_SignFinal则调用EVP_SignFinal函数完成信息摘要和签名(使用参数pkey的私钥)之后,调用了EVP_EncodeBlock对签名信息进行了BASE64编码,然后将编码后的签名信息保存在参数sigret,siglen保存了sigret有效数据的长度。PEM_SignFinal函数成功返回1,否则返回0。
该函数使用了PEM库的错误代码信息对错误处理库进行初始化,必须在使用任何PEM系列函数之前调用该函数。
PEM提供了一系列底层的进行数据读写操作的IO函数,在后面章节叙述到的PEM对象的IO函数都是这些函数的宏定义,所以虽然一般不要直接调用这些函数,做一个清楚的了解还是必要的。这些函数定义如下(openssl\pem.h):
int PEM_read_bio(BIO *bp, char **name, char **header,unsigned char **data,long *len);
int PEM_write_bio(BIO *bp,const char *name,char *hdr,unsigned char *data,long len);
int PEM_bytes_read_bio(unsigned char **pdata, long *plen, char **pnm, const char *name, BIO *bp,pem_password_cb *cb, void *u);
char *PEM_ASN1_read_bio(char *(*d2i)(),const char *name,BIO *bp,char **x,pem_password_cb *cb, void *u);
int PEM_ASN1_write_bio(int (*i2d)(),const char *name,BIO *bp,char *x,const EVP_CIPHER *enc,unsigned char *kstr,int klen,pem_password_cb *cb, void *u);
STACK_OF(X509_INFO) *PEM_X509_INFO_read_bio(BIO *bp, STACK_OF(X509_INFO) *sk, pem_password_cb *cb, void *u);
int PEM_X509_INFO_write_bio(BIO *bp,X509_INFO *xi, EVP_CIPHER *enc,unsigned char *kstr, int klen, pem_password_cb *cd, void *u);
int PEM_read(FILE *fp, char **name, char **header,unsigned char **data,long *len);
int PEM_write(FILE *fp,char *name,char *hdr,unsigned char *data,long len);
char *PEM_ASN1_read(char *(*d2i)(),const char *name,FILE *fp,char **x,pem_password_cb *cb, void *u);
int PEM_ASN1_write(int (*i2d)(),const char *name,FILE *fp,char *x,const EVP_CIPHER *enc,unsigned char *kstr,int klen,pem_password_cb *callback, void *u);
STACK_OF(X509_INFO) *PEM_X509_INFO_read(FILE *fp, STACK_OF(X509_INFO) *sk,pem_password_cb *cb, void *u);
可以看到,这些函数中有很多参数在第3部分介绍过,在此将不再详细介绍。
该函数从文件fp里面读取一个PEM编码的信息。该函数将文件里BEIGIN后面的字符作为对象名保存在参数name里面;将BEGIN所在行和下一个空白行之间的所有信息都读入到参数header里面,如果之间没有信息,就将header设置为NULL;然后将信息体进行BASE64解码放置到data参数里面,len是data参数的有效数据长度。该函数成功返回1,失败返回0。
该函数完成了跟PEM_read相同的功能,只不过读取对象是BIO。事实上,PEM_read是通过调用本函数完成其功能的。该函数成功返回1,失败返回0。
该函数将name参数的数据放在BEGIN头的后面,写入到fp文件;之后将参数hdr信息写入到文件,并在后面写入一个空白行;最后将data参数len字节的数据进行BASE64编码,写入到文件中,并最后加上END头信息,返回PEM信息体的长度,失败返回0。
该函数跟PEM_write函数功能一样,只是操作对象是BIO。事实上,PEM_write函数就是调用本函数完成其功能的。成功返回PEM信息体的长度,失败返回0。
该函数先调用PEM_read函数读取PEM编码的对象信息,然后调用PEM_get_EVP_CIPHER_INFO函数处理PEM格式中的DEK-info字段信息,以决定信息采用的加密算法和ivec值;加入PEM信息是加密了的,接下来就调用PEM_do_header函数解密信息体(参考第4部分),然后调用d2i函数将它进行DER解码转换成内部定义个类型,保存在x参数中。成功返回指向x的指针,否则返回NULL。
注意,参数name必须是BEIGIN头后面的PEM文件数据。因为函数调用了PEM_get_EVP_CIPHER_INFO函数,所以为了函数能成功执行,必须在调用本函数前加载算法。虽然事实上任何类型数据都可以进行加密,但一般来说只有RSA私钥需要加密。本函数可以从一个文件中读取一些列对象。
该函数功能跟PEM_ASN1_read函数一样,不过操作对象是BIO。事实上,PEM_ASN1_read函数是调用本函数完成其功能的。成功返回指向x的指针,否则返回NULL。
该函数将对象x使用i2d参数提供的函数转换城DER编码的数据,接下来,如果enc参数不为NULL,就使用enc的加密算法加密这些数据。参数kstr是用来产生加密密钥的,klen是kstr的有效长度。如果enc不是NULL,但是kstr是NULL,那么就会使用callback函数提示用户输入口令并获取加密数据;如果此时callback为NULL,但是u不为NULL,那么就是使用u作为产生加密密钥的字符串,假定u应该是NULL结束的字符串;如果callback和u都为NULL,那就会使用缺省的callback函数获取口令。然后数据就被进行BASE64编码写入到fp文件中,加上BEIGIN开始头信息、END结束头信息、Type-Proc字段和DEK-info字段(如果数据被加密了)。加密密钥在函数调用完之后就被清除了。成功操作返回1,否则返回0。
该函数实现的功能跟PEM_ASN1_write一样,不过操作对象是BIO。事实上PEM_ASN1_write函数是调用本函数完成其功能的。成功操作返回1,否则返回0。
该函数完成的功能跟PEM_ASN1_read是一样的,除了它自动根据BEGIN头信息调用了相应的d2i系列函数,目前支持的类型d2i_X509、d2i_X509_AUX、d2i_X509_CRL、d2i_RSAPrivateKey和d2i_DSAPrivateKey。该函数会对文件中的所有对象进行处理直到出错或处理完毕。所有被处理好的对象都保存在堆栈sk中。因为有可能有些对象是加密的,所以提供了参数cb和u。参数cb和u的意义参照第3部分。成功返回处理好的堆栈指针,否则返回NULL。
该函数完成的功能跟PEM_X509_INFO_read函数一样,除了操作对象是BIO之外。事实上,PEM_X509_INFO_read函数是调用本函数完成其功能的。成功返回处理好的堆栈指针,否则返回NULL。
该函数完成的功能也跟PEM_ASN1_write_bio一样。除了它从参数xi中读取每一部分对象,分别使用参数xi->x_pkey和xi->x509并使用相应的i2d函数进行PEM编码成独立的信息,并写入到bio中。同样,可能要求用户输入口令生成加密密钥,相关的参数cb、enc、kstr、klen以及u的意义参考前面的函数以及第3部分。该函数成功返回1,否则返回0。
openssl基本上为其定义的每种对象都提供了用PEM格式进行读写的IO函数。在这种意义上说,PEM格式只是包含了头信息的BASE64编码的数据而已。这些函数基本上是基于第6部分所介绍的函数实现的,也就是说,他们多大部分只是这些函数的宏定义而已。因为我们在第3部分已经详细介绍了PEM系列函数的通用参数,所以本文对这些通用参数不再作详细的说明。
对于每个对象,openssl一般提供了四个函数,比如名为Name的对象,提供的四个函数名就如下形式:
PEM_read_bio_Name()
PEM_read_Name()
PEM_write_bio_Name()
PEM_write_Name()
可以看到,有两个是读操作函数,两个是写操作函数。其中,两个读操作函数或两个写操作函数都是功能相同的,不过就是对象一个为文件句柄,一个为BIO罢了。此外,所有对象的读函数如果操作成功,返回相应对象的指针,否则返回NULL;而写函数则成功操作返回非0值,失败返回0。下面我们对这些函数简单分类介绍。
EVP_PKEY *PEM_read_bio_PrivateKey(BIO *bp, EVP_PKEY **x,pem_password_cb *cb, void *u);
EVP_PKEY *PEM_read_PrivateKey(FILE *fp, EVP_PKEY **x,pem_password_cb *cb, void *u);
int PEM_write_bio_PrivateKey(BIO *bp, EVP_PKEY *x, const EVP_CIPHER *enc,unsigned char *kstr, int klen,pem_password_cb *cb, void *u);
int PEM_write_PrivateKey(FILE *fp, EVP_PKEY *x, const EVP_CIPHER *enc,unsigned char *kstr, int klen,pem_password_cb *cb, void *u);
这些函数用PEM格式对一个EVP_PKEY结构的私钥进行读写操作。写操作函数可以处理RSA或DSA类型的私钥。读操作函数还能透明的处理用PKCS#8格式加密和解密的私钥。
1.往文件中写入不加密的私钥的例子
if (!PEM_write_PrivateKey(fp, key, NULL, NULL, 0, 0, NULL))
{
/* 错误处理代码 */
}
2.往BIO中写入一个私钥,采用3DES加密,加密口令提示输入的例子
if (!PEM_write_bio_PrivateKey(bp, key, EVP_des_ede3_cbc(), NULL, 0, 0, NULL))
{
/* 错误处理代码 */
}
3.从BIO重读取一个私钥,使用”hello”作为解密口令的例子
key = PEM_read_bio_PrivateKey(bp, NULL, 0, “hello”);
if (key == NULL)
{
/* 错误处理代码 */
}
4.从BIO中读取一个私钥,并使用回调函数获得解密口令的例子
key = PEM_read_bio_PrivateKey(bp, NULL, pass_cb, “My Private Key”);
if (key == NULL)
{
/* 错误处理代码 */
}
本文继续介绍PEM对象的读写IO函数,请参看第7部分以便更好理解本文。
int PEM_write_bio_PKCS8PrivateKey(BIO *bp, EVP_PKEY *x, const EVP_CIPHER *enc,char *kstr, int klen,pem_password_cb *cb, void *u);
int PEM_write_PKCS8PrivateKey(FILE *fp, EVP_PKEY *x, const EVP_CIPHER *enc,char *kstr, int klen,pem_password_cb *cb, void *u);
这两个函数使用PKCS#8标准保存EVP_PKEY里面的私钥到文件或者BIO中,并采用PKCS#5 v2.0的标准加密私钥。enc参数定义了使用的加密算法。跟其他PEM的IO函数不一样的是,本函数的加密是基于PKCS#8层次上的,而不是基于PEM信息字段的,所以这两个函数也是单独实现的函数,而不是宏定义函数。如果enc参数为NULL,那么就不会执行加密操作,只是使用PKCS#8私钥信息结构。成功执行返回大于0 的数,否则返回0。
使用这两个函数保存的PEM对象可以使用上篇文章介绍的PEM_read_bio_PrivateKey或PEM_read_PrivateKey读出来。
下面是一个将私钥保存为PKCS#8格式,并使用3DES算法进行加密,使用的口令是”hello”的例子
if (!PEM_write_bio_PKCS8PrivateKey(bp, key, EVP_des_ede3_cbc(), NULL, 0, 0, “hello”))
{
/*出错处理代码*/
}
int PEM_write_bio_PKCS8PrivateKey_nid(BIO *bp, EVP_PKEY *x, int nid,char *kstr, int klen,pem_password_cb *cb, void *u);
int PEM_write_PKCS8PrivateKey_nid(FILE *fp, EVP_PKEY *x, int nid,char *kstr, int klen,pem_password_cb *cb, void *u);
这两个函数也是单独实现的函数,而不是宏定义函数。他们也是将私钥保存成PKCS#8格式,但是采用的方式是PKCS#5 v1.5或者PKCS#12进行私钥的加密。nid参数指定了相应的加密算法,其值应该为相应对象的NID。成功执行返回大于0 的数,否则返回0。
使用这两个函数保存的PEM对象可以使用上篇文章介绍的PEM_read_bio_PrivateKey或PEM_read_PrivateKey读出来。
EVP_PKEY *PEM_read_bio_PUBKEY(BIO *bp, EVP_PKEY **x,pem_password_cb *cb, void *u);
EVP_PKEY *PEM_read_PUBKEY(FILE *fp, EVP_PKEY **x,pem_password_cb *cb, void *u);
int PEM_write_bio_PUBKEY(BIO *bp, EVP_PKEY *x);
int PEM_write_PUBKEY(FILE *fp, EVP_PKEY *x);
这四个函数对EVP_PKEY结构的公钥进行PEM格式的读写处理。公钥是作为SubjectPublicKeyInfo存储结构进行编码的。
RSA *PEM_read_bio_RSAPrivateKey(BIO *bp, RSA **x,pem_password_cb *cb, void *u);
RSA *PEM_read_RSAPrivateKey(FILE *fp, RSA **x,pem_password_cb *cb, void *u);
int PEM_write_bio_RSAPrivateKey(BIO *bp, RSA *x, const EVP_CIPHER *enc,unsigned char *kstr, int klen,pem_password_cb *cb, void *u);
int PEM_write_RSAPrivateKey(FILE *fp, RSA *x, const EVP_CIPHER *enc,unsigned char *kstr, int klen,pem_password_cb *cb, void *u);
这四个函数对RSA结构的RSA私钥进行PEM格式的读写处理。它使用跟PrivateKey相同的函数进行处理,但如果私钥类型不是RSA,就会返回错误信息。
RSA *PEM_read_bio_RSAPublicKey(BIO *bp, RSA **x,pem_password_cb *cb, void *u);
RSA *PEM_read_RSAPublicKey(FILE *fp, RSA **x,pem_password_cb *cb, void *u);
int PEM_write_bio_RSAPublicKey(BIO *bp, RSA *x);
int PEM_write_RSAPublicKey(FILE *fp, RSA *x);
这四个函数是对RSA结构的公钥进行PEM格式的读写处理。本函数使用PKCS#1 RSAPublicKey结构标准对RSA公钥进行编码操作。
RSA *PEM_read_bio_RSA_PUBKEY(BIO *bp, RSA **x,pem_password_cb *cb, void *u);
RSA *PEM_read_RSA_PUBKEY(FILE *fp, RSA **x,pem_password_cb *cb, void *u);
int PEM_write_bio_RSA_PUBKEY(BIO *bp, RSA *x);
int PEM_write_RSA_PUBKEY(FILE *fp, RSA *x);
这四个函数也是对RSA结构的公钥进行PEM格式的读写处理。但是本函数使用SubjectPublicKeyInfo结构标准对RSA公钥进行编码操作,如果公钥类型不是RSA,就出错返回失败信息。
本文继续介绍PEM对象的读写IO函数,请参看第7部分和第8部分以便更好理解本文。
DSA *PEM_read_bio_DSAPrivateKey(BIO *bp, DSA **x,pem_password_cb *cb, void *u);
DSA *PEM_read_DSAPrivateKey(FILE *fp, DSA **x,pem_password_cb *cb, void *u);
int PEM_write_bio_DSAPrivateKey(BIO *bp, DSA *x, const EVP_CIPHER *enc,unsigned char *kstr, int klen,pem_password_cb *cb, void *u);
int PEM_write_DSAPrivateKey(FILE *fp, DSA *x, const EVP_CIPHER *enc,unsigned char *kstr, int klen,pem_password_cb *cb, void *u);
这些函数对以DSA结构存储的DSA私钥进行PEM格式的IO读写。它们使用的处理格式跟PrivateKey系列函数是相同的,但是如果私钥不是DSA类型的,则出错返回。
DSA *PEM_read_bio_DSA_PUBKEY(BIO *bp, DSA **x,pem_password_cb *cb, void *u);
DSA *PEM_read_DSA_PUBKEY(FILE *fp, DSA **x,pem_password_cb *cb, void *u);
int PEM_write_bio_DSA_PUBKEY(BIO *bp, DSA *x);
int PEM_write_DSA_PUBKEY(FILE *fp, DSA *x);
这些函数对以DSA结构存储的DSA公钥进行PEM格式的IO读写。该公钥是以SubjectPublicKeyInfo结构进行编码的,如果公钥不是DSA类型,则将会出错返回。
DSA *PEM_read_bio_DSAparams(BIO *bp, DSA **x, pem_password_cb *cb, void *u);
DSA *PEM_read_DSAparams(FILE *fp, DSA **x, pem_password_cb *cb, void *u);
int PEM_write_bio_DSAparams(BIO *bp, DSA *x);
int PEM_write_DSAparams(FILE *fp, DSA *x);
这些函数对以DSA结构存储的DSA参数进行PEM格式的IO读写操作。
DH *PEM_read_bio_DHparams(BIO *bp, DH **x, pem_password_cb *cb, void *u);
DH *PEM_read_DHparams(FILE *fp, DH **x, pem_password_cb *cb, void *u);
int PEM_write_bio_DHparams(BIO *bp, DH *x);
int PEM_write_DHparams(FILE *fp, DH *x);
这些函数对以DH结构保存的DH参数进行PEM格式的IO读写操作,这些参数采用了PKCS#3的DH参数结构进行编码。
X509 *PEM_read_bio_X509(BIO *bp, X509 **x, pem_password_cb *cb, void *u);
X509 *PEM_read_X509(FILE *fp, X509 **x, pem_password_cb *cb, void *u);
int PEM_write_bio_X509(BIO *bp, X509 *x);
int PEM_write_X509(FILE *fp, X509 *x);
这些函数对以X509结构保存的X509证书进行PEM格式的IO读写操作,这些函数也可以对信任X509证书进行相同的操作,但是信任设置信息会丢失。
X509 *PEM_read_bio_X509_AUX(BIO *bp, X509 **x, pem_password_cb *cb, void *u);
X509 *PEM_read_X509_AUX(FILE *fp, X509 **x, pem_password_cb *cb, void *u);
int PEM_write_bio_X509_AUX(BIO *bp, X509 *x);
int PEM_write_X509_AUX(FILE *fp, X509 *x);
这些函数对以X509结构保存的信任X509证书进行PEM格式的IO读写操作。
X509_REQ *PEM_read_bio_X509_REQ(BIO *bp, X509_REQ **x,pem_password_cb *cb, void *u);
X509_REQ *PEM_read_X509_REQ(FILE *fp, X509_REQ **x,pem_password_cb *cb, void *u);
int PEM_write_bio_X509_REQ(BIO *bp, X509_REQ *x);
int PEM_write_X509_REQ(FILE *fp, X509_REQ *x);
int PEM_write_bio_X509_REQ_NEW(BIO *bp, X509_REQ *x);
int PEM_write_X509_REQ_NEW(FILE *fp, X509_REQ *x);
这些函数对以X509_REQ结构存储的符合PKCS#10标准的证书请求进行PEM格式的IO读写操作。不同的是,X509_REQ系列写函数使用CERTIFICATE REQUEST作为头,而X509_REQ_NEW系列写函数则采用NEW CERTIFICATE REQUEST作为头(一些CA要求这种格式)。而X509_REQ读函数对这两种情况都能处理,所以没有X509_REQ_NEW的读函数了。
X509_CRL *PEM_read_bio_X509_CRL(BIO *bp, X509_CRL **x,pem_password_cb *cb, void *u);
X509_CRL *PEM_read_X509_CRL(FILE *fp, X509_CRL **x,pem_password_cb *cb, void *u);
int PEM_write_bio_X509_CRL(BIO *bp, X509_CRL *x);
int PEM_write_X509_CRL(FILE *fp, X509_CRL *x);
这些函数对以X509_CRL结构存储的X509 CRL进行PEM格式的IO读写操作。
PKCS7 *PEM_read_bio_PKCS7(BIO *bp, PKCS7 **x, pem_password_cb *cb, void *u);
PKCS7 *PEM_read_PKCS7(FILE *fp, PKCS7 **x, pem_password_cb *cb, void *u);
int PEM_write_bio_PKCS7(BIO *bp, PKCS7 *x);
int PEM_write_PKCS7(FILE *fp, PKCS7 *x);
这些汉森对以PKCS7结构存储的PKCS#7内容信息进行PEM格式的IO读写操作。
NETSCAPE_CERT_SEQUENCE *PEM_read_bio_NETSCAPE_CERT_SEQUENCE(BIO *bp,NETSCAPE_CERT_SEQUENCE **x,pem_password_cb *cb, void *u);
NETSCAPE_CERT_SEQUENCE *PEM_read_NETSCAPE_CERT_SEQUENCE(FILE *fp,NETSCAPE_CERT_SEQUENCE **x,pem_password_cb *cb, void *u);
int PEM_write_bio_NETSCAPE_CERT_SEQUENCE(BIO *bp, NETSCAPE_CERT_SEQUENCE *x);
int PEM_write_NETSCAPE_CERT_SEQUENCE(FILE *fp, NETSCAPE_CERT_SEQUENCE *x);
这些函数对以NETSCAPE_CERT_SEQUENCE结构存储的Netscape证书序列进行PEM格式的IO读写操作。
在PEM读写的过程中,特别对于私钥文件,可能经常要使用到获取口令的回调函数,在签名我们介绍的一些列函数也可以看出,基本上都是带有回调函数的参数的。openssl缺省的回调函数是基于命令行的,在许多情况下可能并不适应,这就要求用户自己定义回调函数。在前面的相关章节,我们已经介绍了该回调函数的格式,现在我们给出一个回调函数的实现例子。
int pass_cb(char *buf, int size, int rwflag, void *u);
{
int len;
char *tmp;
/* rwflag是一个标准,如果为1,可能还需要作些别的处理工作*/
printf(“输入口令: \”%s\”\n”, u);
/* 这里应该是得到口令的代码*/
tmp = “hello”;
len = strlen(tmp);
if (len <= 0) return 0;
/* 如果口令超出给定长度,就把多余的删掉 */
if (len > size) len = size;
memcpy(buf, tmp, len);
return len;
}
PEM系列函数的格式和参数基本相同,下面是一个常犯的导致错误的用法。
X509 *x;
PEM_read_bio_X509(bp, &x, 0, NULL);
这样的用法会导致出现不可预测的错误,因为x并没有进行初始化,分配内存空间,而接下来调用的函数却会往x里面写入数据,导致内存非法操作。这也是openssl本身没有处理好的一个BUG.
全球可信CA机构