一、OpenSSL简介
在计算机网络上,OpenSSL是一个开放源代码的软件库包,应用程序可以使用这个包来进行安全通信,避免窃听,同时确认另一端连接者的身份。
它提供的主要功能有: SSL协议实现(包括SSLv2、 SSLv3和TLSv1)、大量软算法(对称/非对称/摘要)、大数运算、非对称算法密钥生成、 ASN.1编解码库、证书请求(PKCS10)编解码、数字证书编解码、 CRL编解码、OCSP协议、数字证书验证、 PKCS7标准实现和PKCS12个人数字证书格式实现等功能。OpenSSL采用C语言作为开发语言,这使得它具有优秀的跨平台性能。 OpenSSL支持Linux、 UNIX、 windows、 Mac等平台。广泛被应用在互联网的网页服务器上。
1.1、OpenSSL安装
下载路径:https://www.OpenSSL.org/source/
安装步骤
$ tar zxvf OpenSSL-1.1.1i.tar.gz
$ cd OpenSSL-1.1.1i
$ ./config --prefix=/usr/local/OpenSSL
$ make
$ sudo make install
1.2、OpenSSL源码结构
OpenSSL源代码主要由eay库、 ssl库、工具源码、范例源码以及测试源码组成。
(一)eay库
eay库是基础的库函数,提供了很多功能。源代码放在crypto目录下。包括如下内容:
-
asn1 DER编码解码(crypto/asn1目录),包含了基本asn1对象的编解码以及数字证书请求、数字证书、 CRL撤销列表以及PKCS8等最基本的编解码函数。这些函数主要通过宏来实现。
-
抽象IO(BIO,crypto/bio目录),本目录下的函数对各种输入输出进行抽象,包括文件、内存、标准输入输出、 socket和SSL协议等。
-
大数运算(crypto/bn目录),本目录下的文件实现了各种大数运算。这些大数运算主要用于非对称算法中密钥生成以及各种加解密操作。另外还为用户提供了大量辅助函数,比如内存与大数之间的相互转换。
-
字符缓存操作(crypto/buffer目录)
-
配置文件读取(crypto/conf目录), OpenSSL主要的配置文件为OpenSSL.cnf。本目录下的函数实现了对这种格式配置文件的读取操作。
-
DSO(动态共享对象,crypto/dso目录),本目录下的文件主要抽象了各种平台的动态库加载函数,为用户提供统一接口。
-
硬件引擎(crypto/engine目录),硬件引擎接口。用户如果要写自己的硬件引擎,必须实现它所规定的接口。
-
错误处理(crypto/err目录),当程序出现错误时, OpenSSL能以堆栈的形式显示各个错误。本目录下只有基本的错误处理接口,具体的的错误信息由各个模块提供。各个模块专门用于错误处理的文件一般为*_err…c文件。
-
对称算法、非对称算法及摘要算法封装(crypto/evp目录)。
-
HMAC(crypto/hmac目录),实现了基于对称算法的MAC。
-
hash表(crypto/lhash目录),实现了散列表数据结构。 OpenSSL中很多数据结构都是以散列表来存放的。比如配置信息、 ssl session和asn.1对象信息等。
-
数字证书在线认证(crypto/ocsp目录),实现了ocsp协议的编解码以及证书有效性计算等功能。
-
PEM文件格式处理(crypto/pem),用于生成和读取各种PEM格式文件,包括各种密钥、数字证书请求、数字证书、 PKCS7消息和PKCS8消息等。
-
pkcs7消息语法(crypto/pkcs7目录),主要实现了构造和解析PKCS7消息;
-
pkcs12个人证书格式(crypto/pckcs12目录),主要实现了pkcs12证书的构造和解析。
-
队列(crypto/pqueue目录),实现了队列数据结构,主要用于DTLS。
-
随机数(crypto/rand目录),实现了伪随机数生成,支持用户自定义随机数生成。
-
堆栈(crypto/stack目录),实现了堆栈数据结构。
-
线程支持(crypto/threads), OpenSSL支持多线程,但是用户必须实现相关接口。
-
文本数据库(crypto/txt_db目录)。
-
x509数字证书(crypto/x509目录和crypto/x509v3),包括数字证书申请、数字证书和CRL的构造、解析和签名验证等功能;
-
对称算法(crypto/aes、 crypto/bf、 crypto/cast、 ccrypto/omp和crypto/des等目录)。
-
非对称算法(crypto/dh、 crypto/dsa、 crypto/ec和crypto/ecdh)。
-
摘要算法(crypto/md2、 crypto/md4、 crypto/md5和crypto/sha)以及密钥交换/认证算法(crypto/dh 和crypto/krb5)。
(二)ssl库
ssl库所有源代码在ssl目录下,包括了sslv2、 sslv3、 tlsv1和DTLS的源代码。各个版本基本上都有客户端源码(* _clnt.c)、服务源码(* _srvr.c)、通用源码(* _both.c)、底层包源码( * _pkt.c)、方法源码(* _meth.c)以及协议相关的各种密钥计算源码(* _enc.c)等,都很有规律。
(三)工具源码
工具源码主要在crypto/apps目录下,默认编译时只编译成OpenSSL可执行文件。该命令包含了各种命令工具。此目录下的各个源码可以单独进行编译。
(四)范例源码
范例源码在demo目录下,另外engines目录给出了OpenSSL支持的几种硬件的engines源码,也可以作为engine编写参考。
(五)测试源码
测试源码主要在test目录下。
1.3、OpenSSL学习方法
1)建立学习环境
建立一个供调试的OpenSSL环境 linux或者其他平台。
2)学习OpenSSL的命令
通过OpenSSL命令的学习,对OpenSSL有基本的了解。
3)学习OpenSSL源代码并调试
对于OpenSSL函数的学习,主要查看OpenSSL自身是如何调用的,或者查看函数的实现。对于OpenSSL中只有实现而没有调用的函数,读者需要自己写源码或研究源代码去学习。主要需要学习的源代码有:
- apps目录下的各个程序,对应于OpenSSL的各项命令;
- demos下的各种源代码;
- engines下的各种engine实现;
- test目录下的各种源代码。
4)学会使用 OpenSSL 的 asn.1 编解码
OpenSSL中很多函数和源码都涉及到asn1编解码,比如数字证书申请、数字证书、 crl、ocsp、 pkcs7、 pkcs8、 pkcs12等。
5)查找资料
- Linux下主要用man就能查看OpenSSL命令和函数的帮助。
- Windows用户可用到www.openss.org去查看在线帮助文档,或者用linux下的命令man2html将帮助文档装换为html格式。
- 用户也可以访问OpenSSL.cn论坛来学习OpenSSL。
二、OpenSSL常用功能
OpenSSL的功能非常丰富,本文主要介绍常用的功能,包括:哈希表、BIO、BASE64编解码、摘要算法(MD4、MD5、SHA1)、公钥算法RSA等。
2.1 哈希表
假设有10亿条数据:
(1)如何快速查找一个数据是否存在?–bitmap
(2)如何存储,快速查找是否存在?–布隆过滤器+哈希表
2.1.1 定义
在一般的数据结构如线性表和树中,记录在结构中的相对位置是与记录的关键字之间不存在确定的关系,在结构中查找记录时需进行一系列的关键字比较。这一类查找方法建立在“比较”的基础上,查找的效率与比较次数密切相关。理想的情况是能直接找到需要的记录,因此必须在记录的存储位置和它的关键字之间建立确定的对应关系,使每个关键字和结构中一个唯一的存储位置相对应。在查找时,只需根据这个对应关系找到给定值。这种对应关系既是哈希函数,按这个思想建立的表为哈希表。哈希表存在冲突现象:不同的关键字可能得到同一哈希地址。在建造哈希表时不仅要设定一个好的哈希函数,而且要设定一种处理冲突的方法。
2.1.2 数据结构
OpenSSL函数使用哈希表来加快查询操作,并能存放任意形式的数据,比如配置文件的读取、内存分配中被分配内存的信息等。其源码在crypto/lhash目录下。
OpenSSL中的哈希表数据结构在lhash_local.h中定义如下:
struct lhash_node_st {
void *data; //用于存放数据地址
struct lhash_node_st *next; //下一个数据地址
unsigned long hash; //数据哈希计算值
};
struct lhash_st {
OPENSSL_LH_NODE **b; //指针数组用于存放所有的数据,数组中的每一个值为数据链表的头指针
OPENSSL_LH_COMPFUNC comp; //用于存放数据比较函数地址
OPENSSL_LH_HASHFUNC hash; //用于存放计算哈希值函数的地址
unsigned int num_nodes; //链表个数
unsigned int num_alloc_nodes; //为b分配空间的大小
unsigned int p;
unsigned int pmax;
unsigned long up_load; /* load times 256 */
unsigned long down_load; /* load times 256 */
unsigned long num_items;
unsigned long num_expands;
unsigned long num_expand_reallocs;
unsigned long num_contracts;
unsigned long num_contract_reallocs;
TSAN_QUALIFIER unsigned long num_hash_calls;
TSAN_QUALIFIER unsigned long num_comp_calls;
unsigned long num_insert;
unsigned long num_replace;
unsigned long num_delete;
unsigned long num_no_delete;
TSAN_QUALIFIER unsigned long num_retrieve;
TSAN_QUALIFIER unsigned long num_retrieve_miss;
TSAN_QUALIFIER unsigned long num_hash_comps;
int error;
};
基本结构如下示图:
2.1.3 函数说明
1)创建hash
LHASH *lh_new(LHASH_HASH_FN_TYPE h, LHASH_COMP_FN_TYPE c)
功能:生成hash表
说明:输入参数h为哈希函数, c为比较函数。这两个函数都是回调函数。
因为哈希表用于存放任意的数据结构,哈希表存放、查询、删除等操作都需要比较数据和进行哈希运算,而哈希表不知道用户数据如何进行比较,也不知道用户数据结构中需要对哪些关键项进行散列运算。所以,用户必须提供这两个回调函数。
2)往哈希表中添加数据
void *lh_insert(LHASH *lh, void *data)
功能:往哈希表中添加数据。
说明:data为需要添加数据结构的指针地址。
3)遍历哈希数据
void lh_doall(LHASH *lh, LHASH_DOALL_FN_TYPE func)
功能:处理哈希表中的所有数据。
说明:func为外部提供的回调函数,本函数遍历所有存储在哈希表中的数据,每个数据被func处理。
4)检索
void *lh_retrieve(LHASH *lh, const void *data)
功能:查询数据。
说明:从哈希表中查询数据,data为i数据结构地址,此数据结构中必须提供关键项(这些关键项对应用户提供的哈希函数和比较函数)以供查询,如果查询成功,返回数据结构的地址,否则返回NULL。比如SSL握手中服务端查询以前存储的SESSION时,它需要提供其中关键的几项:
SSL_SESSION *ret=NULL,data;
data.ssl_version=s->version;
data.session_id_length=len;
memcpy(data.session_id,session_id,len);
ret=(SSL_SESSION *)lh_retrieve(s->ctx->sessions,&data);
5)删除
void *lh_delete(LHASH *lh, const void *data)
功能:删除散列表中的一个数据。
说明:data为数据结构指针。
6)释放
void lh_free(LHASH *lh)
功能:释放哈希表。
2.1.4 编程示例
#include <stdio.h>
#include <openssl/lhash.h>
#define NAME_LENGTH 32
typedef struct _Person {
char name[NAME_LENGTH];
int high;
char otherInfo[NAME_LENGTH];
} Person;
static int person_cmp(const void *a, const void *b) {
char *namea = ((Person*)a)->name;
char *nameb = ((Person*)b)->name;
return strcmp(namea, nameb);
}
void print_value(void *a) {
Person *p = (Person*)a;
printf("name: %s\n", p->name);
printf("high: %d\n", p->high);
printf("other info : %s\n", p->otherInfo);
}
int main() {
_LHASH *h = lh_new(NULL, person_cmp);
if (h == NULL) {
printf("err.\n");
return -1;
}
Person p1 = {"Zhangsan", 170, "xxxx"};
Person p2 = {"Lisi", 175, "xxxx"};
Person p3 = {"Wangwu", 170, "xxxx"};
Person p4 = {"Zhaoliu", 170, "xxxx"};
lh_insert(h, &p1);
lh_insert(h, &p2);
lh_insert(h, &p3);
lh_insert(h, &p4);
lh_doall(h, print_value);
printf("\n\n\n------------------------------\n\n\n");
void *data = lh_retrieve(h, (const char *)"Zhangsan");
if (data == NULL) {
return -1;
}
print_value(data);
lh_free(h);
return 0;
}
2.2 BIO
2.2.1 定义
OpenSSL抽象IO(I/O abstraction,即BIO)是OpenSSL对于io类型的抽象封装,包括:内存、文件、日志、标准输入输出、 socket( TCP/UDP)、加/解密、摘要和ssl通道等。
OpenSSL BIO通过回调函数为用户隐藏了底层实现细节,所有类型的bio的调用大体上是类似的。 BIO中的数据能从一个BIO传送到另外一个BIO或者是应用程序。
2.2.2 数据结构
BIO数据结构主要有2个,在crypto/bio.h(不同版本位置可能有差异)中定义如下:
1)BIO_METHOD
typedef struct bio_method_st{
int type; //具体BIO类型
const char *name; //具体BIO名字
int (*bwrite)(BIO *, const char *, int); //具体BIO写操作回调函数
int (*bread)(BIO *, char *, int); //具体BIO读操作回调函数
int (*bputs)(BIO *, const char *); //具体BIO中写入字符串回调函数
int (*bgets)(BIO *, char *, int); //具体BIO中读取字符串函数
long (*ctrl)(BIO *, int, long, void *); //具体BIO的控制回调函数
int (*create)(BIO *); //生成具体BIO回调函数
int (*destroy)(BIO *); //销毁具体BIO回调函数
long (*callback_ctrl)(BIO *, int, bio_info_cb *); //具体BIO控制回调函数,与ctrl回调函数不一样,该函数可由调用者(而不是实现者)来实现,然后通过BIO_set_callback等函数来设置
} BIO_METHOD;
2)BIO
struct bio_st {
BIO_METHOD *method;
/* bio, mode, argp, argi, argl, ret */
long (*callback)(struct bio_st *,int,const char *,int, long,long);
char *cb_arg; /* first argument for the callback */
int init;
int shutdown;
int flags; /* extra storage */
int retry_reason;
int num;
void *ptr;
struct bio_st *next_bio; /* used by filter BIOs */
struct bio_st *prev_bio; /* used by filter BIOs */
int references;
nsigned long num_read;
unsigned long num_write;
CRYPTO_EX_DATA ex_data;
};
结构体参数说明:
- init:具体句柄初始化标记,初始化后为1。比如文件BIO中,通过BIO_set_fp关联一个文件指针时,该标记则置1; socket BIO中通过BIO_set_fd关联一个链接时设置该标记为1。
- shutdown: BIO关闭标记,当该值不为0时,释放资源;改值可以通过控制函数来设置。
- flags:有些 BIO 实现需要它来控制各个函数的行为。比如文件 BIO 默认该值为
- BIO_FLAGS_UPLINK,这时文件读操作调用 UP_fread 函数而不是调用 fread 函数。
- retry_reason:重试原因,主要用在socket和ssl BIO 的异步阻塞。比如socket bio中,遇到WSAEWOULDBLOCK错误时, OpenSSL告诉用户的操作需要重试。
- num:该值因具体BIO而异,比如socket BIO中num用来存放链接字。
- ptr:指针,具体bio有不同含义。比如文件BIO中它用来存放文件句柄; mem bio中它用来存放内存地址; connect bio中它用来存放BIO_CONNECT数据, accept bio中它用来存放BIO_ACCEPT数据。
- next_bio:下一个BIO地址, BIO数据可以从一个BIO传送到另一个BIO,该值指明了下一个BIO的地址。
- references:被引用数量。
- num_read: BIO中已读取的字节数。
- num_write: BIO中已写入的字节数。
- ex_data:用于存放额外数据。
2.2.3 函数说明
(一)BIO相关函数
BIO_new_file(生成新文件)和BIO_get_fd(设置网络链接)等。
(二)通用抽象函数
BIO_read和BIO_write,另外,有很多函数是由宏定义通过控制函数BIO_ctrl实现,比如BIO_set_nbio、BIO_get_fd和BIO_eof等等。
2.2.4 编程示例
(一)Memory BIO
#include <stdio.h>
#include <OpenSSL/bio.h>
int main()
{
BIO *b=NULL;
int len=0;
char *out=NULL;
b=BIO_new(BIO_s_mem()); //生成一个mem类型的BIO
len=BIO_write(b,"OpenSSL",7); //将字符串"OpenSSL"写入bio
len=BIO_printf(b,"%s","zcp"); //将字符串"zcp"写入bio
len=BIO_ctrl_pending(b); //得到缓冲区中待读取大小
out=(char *)OPENSSL_malloc(len);
len=BIO_read(b,out,len); //将bio中的内容写入out缓冲区
OPENSSL_free(out);
BIO_free(b);
return 0;
}
(二)Socket BIO
示例:获取本机的web服务信息
1)服务端
#include <stdio.h>
#include <OpenSSL/bio.h>
#include <string.h>
int main()
{
BIO *b=NULL,*c=NULL;
int sock,ret,len;
char *addr=NULL;
char out[80];
sock=BIO_get_accept_socket("2323",0);
b=BIO_new_socket(sock, BIO_NOCLOSE);
ret=BIO_accept(sock,&addr);
BIO_set_fd(b,ret,BIO_NOCLOSE);
while(1)
{
memset(out,0,80);
len=BIO_read(b,out,80);
if(out[0]=='q')
break;
printf("%s",out);
}
BIO_free(b);
return 0;
}
2)客户端
#include <OpenSSL/bio.h>
int main()
{
BIO *cbio, *out;
int len;
char tmpbuf[1024];
cbio = BIO_new_connect("localhost:http"); //建立连接到本地web服务的BIO
out = BIO_new_fp(stdout, BIO_NOCLOSE); //生成一个输出到屏幕的BIO
if(BIO_do_connect(cbio) <= 0)
{
fprintf(stderr, "Error connecting to server\n");
}
BIO_puts(cbio, "GET / HTTP/1.0\n\n"); //通过BIO发送数据
for(;;)
{
len = BIO_read(cbio, tmpbuf, 1024); //将web服务响应的数据写入缓存,此函数循环调用直到无数据。
if(len <= 0)
break;
BIO_write(out, tmpbuf, len); //通过BIO打印收到的数据
}
BIO_free(cbio);
BIO_free(out);
return 0;
}
(三)File BIO
#include <stdio.h>
#include <OpenSSL/bio.h>
int main()
{
BIO *b=NULL;
int len=0,outlen=0;
char *out=NULL;
b=BIO_new_file("bf.txt","w");
len=BIO_write(b,"OpenSSL",4);
len=BIO_printf(b,"%s","zcp");
BIO_free(b);
b=BIO_new_file("bf.txt","r");
len=BIO_pending(b);
len=50;
out=(char *)OPENSSL_malloc(len);
len=1;
while(len>0)
{
len=BIO_read(b,out+outlen,1);
outlen+=len;
}
BIO_free(b);
free(out);
return 0;
}
(四)MD BIO
本示例用md BIO对字符串"opessl"进行md5摘要。
#include <OpenSSL/bio.h>
#include <OpenSSL/evp.h>
int main()
{
BIO *bmd=NULL,*b=NULL;
const EVP_MD *md=EVP_md5();
int len;
char tmp[1024];
bmd=BIO_new(BIO_f_md()); //生成一个md BIO
BIO_set_md(bmd,md); //设置md BIO 为md5 BIO
b= BIO_new(BIO_s_null()); //生成一个 null BIO
b=BIO_push(bmd,b); //构造BIO 链,md5 BIO在顶部
len=BIO_write(b,"OpenSSL",7); //将字符串送入BIO做摘要
len=BIO_gets(b,tmp,1024); //将摘要结果写入tmp缓冲区
BIO_free(b);
return 0;
}
(五)cipher BIO
本示例采用cipher BIO对字符串"OpenSSL"进行加密和解密,本示例编译需要用c++编译器。
其中key为对称密钥,iv为初始化向量。
#include <string.h>
#include <OpenSSL/bio.h>
#include <OpenSSL/evp.h>
int main()
{
/* 加密 */
BIO *bc=NULL,*b=NULL;
const EVP_CIPHER *c=EVP_des_ecb();
int len,i;
char tmp[1024];
unsigned char key[8],iv[8];
for(i=0;i<8;i++)
{
memset(&key[i],i+1,1);
memset(&iv[i],i+1,1);
}
bc=BIO_new(BIO_f_cipher());
BIO_set_cipher(bc,c,key,iv,1); //设置加密BI
b= BIO_new(BIO_s_null());
b=BIO_push(bc,b);
len=BIO_write(b,"OpenSSL",7);
len=BIO_read(b,tmp,1024); //获取加/解密结果
BIO_free(b);
/* 解密 */
BIO *bdec=NULL,*bd=NULL;
const EVP_CIPHER *cd=EVP_des_ecb();
bdec=BIO_new(BIO_f_cipher());
BIO_set_cipher(bdec,cd,key,iv,0); //设置解密BIO
bd= BIO_new(BIO_s_null());
bd=BIO_push(bdec,bd);
len=BIO_write(bdec,tmp,len);
len=BIO_read(bdec,tmp,1024); //获取加/解密结果
BIO_free(bdec);
return 0;
}
(六)ssl BIO
函数用ssl bio来链接mybank.icbc.com.cn的https服务,并请求首页文件。其中SSLeay_add_ssl_algorithms和OpenSSL_add_all_algorithms函数必不可少,否则不能找到ssl加密套件并且不能找到各种算法。
#include <OpenSSL/bio.h>
#include <OpenSSL/ssl.h>
int main()
{
BIO *sbio, *out;
int len;
char tmpbuf[1024];
SSL_CTX *ctx;
SSL *ssl;
SSLeay_add_ssl_algorithms();
OpenSSL_add_all_algorithms();
ctx = SSL_CTX_new(SSLv3_client_method());
sbio = BIO_new_ssl_connect(ctx);
BIO_get_ssl(sbio, &ssl);
if(!ssl) {
fprintf(stderr, "Can not locate SSL pointer\n");
return 0;
}
SSL_set_mode(ssl, SSL_MODE_AUTO_RETRY);
BIO_set_conn_hostname(sbio, "mybank.icbc.com.cn:https");
out = BIO_new_fp(stdout, BIO_NOCLOSE);
BIO_printf(out,”链接中….\n”);
if(BIO_do_connect(sbio) <= 0) {
fprintf(stderr, "Error connecting to server\n");
return 0;
}
if(BIO_do_handshake(sbio) <= 0) {
fprintf(stderr, "Error establishing SSL connection\n");
return 0;
}
BIO_puts(sbio, "GET / HTTP/1.0\n\n");
for(;;) {
len = BIO_read(sbio, tmpbuf, 1024);
if(len <= 0)
break;
BIO_write(out, tmpbuf, len);
}
BIO_free_all(sbio);
BIO_free(out);
return 0;
}
2.3 BASE64编解码
BASE64编码是一种常用的将十六进制数据转换为可见字符的编码。与ASCII码相比,它占用的空间较小。BASE64编码在rfc3548种定义。
2.3.1 BASE编码原理
将数据编码成BASE64编码时,以3字节数据为一组,转换为24bit的二进制数,将24bit的二进制数分成四组,每组6bit。对于每一组数字:0~63。然后根据这个数字查表即得到结果。表如下:
比如有数据:0x30 0x82 0x02,编码过程如下:
1)得到16进制数据:30 82 02
2)得到二进制数据:00110000 10000010 00000010
3)每6bit分组:001100 001000 001000 000010
4)得到数字:12 8 8 2
5)根据表查询得到结果:M I I C
BASE64填充:在不够的情况下在右边加0。有3种情况:
1)输入数据比特数是24的整数倍(输入字节为3字节整数倍),则无填充;
2)输入数据最后编码的是1个字节(输入数据字节数除3余1),即8比特,则需要填充2个“==”,因为要补齐6比特,需要加2个00;
3)输入数据最后编码是2个字节(输入数据字节数除3余2),则需要填充1个“=”,因为补齐6比特,需要加一个00。
举例:对0x30编码
1))0x30的二进制为: 00110000
2) 分组为: 001100 00
3)填充2个00: 001100 000000
4)得到数字: 12 0
5) 查表得到的编码为MA,另外加上两个 ==
最终编码为:MA==
BASE64解码是编码的逆过程。解码时,将BASE64编码根据表展开,根据有几个等号去掉结尾的几个00,然后每8bit恢复即可。
2.3.2 BASE函数
OpenSSL中用于base64编解码的函数主要有:
1) 编码函数
- EVP_EncodeInit 编码前初始化上下文。
- EVP_EncodeUpdate 进行BASE64编码,本函数可多次调用。
- EVP_EncodeFinal 进行BASE64编码,并输出结果。
- EVP_EncodeBlock 进行BASE64编码。
2)解码函数
- EVP_DecodeInit 解码前初始化上下文。
- EVP_DecodeUpdate BASE64解码,本函数可多次调用。
- EVP_DecodeFinal BASE64解码,并输出结果。
- EVP_DecodeBlock BASE64解码,可单独调用。
2.3.3 BASE编程示例
#include <string.h>
#include <openssl/evp.h>
int main() {
unsigned char in[100], base64[150], decode[100];
EVP_ENCODE_CTX ectx;
EVP_EncodeInit(&ectx);
int i = 0;
for (i = 0;i < 100;i ++) {
in[i] = i;
}
int outl, inl = 100;
EVP_EncodeUpdate(&ectx, base64, &outl, in, inl);
int total = outl;
EVP_EncodeFinal(&ectx, base64+total, &outl);
printf("%s\n", base64);
return 0;
}
2.4 摘要算法(MD4、MD5、SHA1)
摘要算法是一种能产生特殊输出格式的算法,这种算法的特点是:无论用户输入什么长度的原始数据,经过计算后输出的密文都是固定长度的,这种算法的原理是根据一定的运算规则对原数据进行某种形式的提取,这种提取就是摘要,被摘要的数据内容与原数据有密切联系,只要原数据稍有改变,输出的“摘要”便完全不同,因此,基于这种原理的算法便能对数据完整性提供较为健全的保障。但是,由于输出的密文是提取原数据经过处理的定长值,所以它已经不能还原为原数据,即消息摘要算法是不可逆的,理论上无法通过反向运算取得原数据内容,因此它通常只能被用来做数据完整性验证。
摘要是安全协议中不可或却的要素,特别是身份认证与签名。用户需要对数据进行签名时,不可能对大的数据进行运算,这样会严重影响性能。如果只对摘要结果进行计算,则会提供运算速度。
如今常用的“消息摘要”算法经历了多年验证发展而保留下来的算法已经不多,这其中包括MD2、 MD4、 MD5、SHA、 SHA-1/256/383/512等。常用的摘要算法主要有MD5和SHA1。 MD5的输出结果为16字节, SHA1的输出结果为20字节。
2.4.1 OpenSSL摘要实现说明
OpenSSL摘要实现的源码位于crypto目录下的各个子目录下,如下所示:
- crypto/ripemd: ripemd摘要实现(包括汇编代码)及其测试程序;
- crypto/md2: md2摘要实现及其测试程序;
- crypto/mdc2: mdc2摘要实现及其测试程序;
- crypto/md4: md4摘要实现及其测试程序;
- crypto/md5: md5摘要实现及其测试程序;
- crypto/sha: sha、 sha1、 sha256、 sha512实现及其测试程序(包含汇编源码)。
上述各种摘要源码在OpenSSL中都是底层的函数,相对独立,能单独提取出来,而不必包含OpenSSL的libcrypto库(因为这个库一般比较大)。
2.4.2 函数说明
所有摘要算法都有以下几个函数,其中xxx为具体的摘要算法名称:
1)xxx_Init:初始化上下文,用于多数据摘要。
2)xxx_Update:进行摘要计算,可以运行多次,对多个数据摘要。
3)xxx_Final:进行摘要计算,该函数与1)和2)一起使用。
4)xxx:对一个数据进行摘要,该函数由上述1) 2)和3)实现,只是XXX_Update只调用一次。
2.4.3 编码示例
int main()
{
unsigned char in[] = "EVP_ENCODE_CTX ectx, dctx; \
unsigned char in[100], base64[150], decode[100]; \
EVP_EncodeInit(&ectx); \
int i = 0; \
for (i = 0;i < 100;i ++) { \
in[i] = i;\
} ";
unsigned char out[128] = {0};
int n = strlen(in);
int i = 0;
MD4(in, n, out);
printf("MD4 result: \n"); // 16 byte
for (i = 0;i < 16;i ++) {
printf("%x", out[i]);
}
printf("\n");
MD5(in, n, out);
printf("MD5 result: \n"); // 16 byte
for (i = 0;i < 16;i ++) {
printf("%x", out[i]);
}
printf("\n");
SHA(in, n, out);
printf("SHA result: \n"); // 20 byte
for (i = 0;i < 16;i ++) {
printf("%x", out[i]);
}
printf("\n");
SHA1(in, n, out);
printf("SHA1 result: \n"); // 20 byte
for (i = 0;i < 20;i ++) {
printf("%x", out[i]);
}
printf("\n");
SHA256(in, n, out);
printf("SHA256 result: \n"); // 32 byte
for (i = 0;i < 32;i ++) {
printf("%x", out[i]);
}
printf("\n");
SHA512(in, n, out);
printf("SHA512 result: \n"); // 64 byte
for (i = 0;i < 64;i ++) {
printf("%x", out[i]);
}
printf("\n");
}
2.5 公钥算法RSA
2.5.1 RSA简介
RSA算法是一个广泛使用的公钥算法。其密钥包括公钥和私钥。它能用于数字签名、身份认证以及密钥交换。 RSA密钥长度一般使用1024位或者更高。 RSA密钥信息主要包括:
- n:模数
- e:公钥指数
- d:私钥指数
- p:最初的大素数
- q:最初的大素数
- dmp1:e*dmp1=1(mod(p-1))
- dmq1:e*dmp1=1(mod(q-1))
- iqmp:e*iqmp=1(mod p)
2.5.2 OpenSSL的RSA实现
OpenSSL的RSA实现源码在crypto/rsa目录下。它实现了RSA PKCS1标准。主要源码结构如下:
-
rsa.h:定义 RSA 数据结构以及 RSA_METHOD,定义了 RSA 的各种函数。
-
rsa_asn1.c:实现了RSA密钥的DER编码和解码,包括公钥和私钥。
-
rsa_chk.c:RSA密钥检查。
-
rsa_eay.c:OpenSSL实现的一种RSA_METHOD,作为其默认的一种RSA计算实现方式。此文件未实现rsa_sign、rsa_verify和rsa_keygen回调函数。
-
rsa_err.c:RSA错误处理。
-
rsa_gen.c:RSA密钥生成,如果RSA_METHOD中的rsa_keygen回调函数不为空,则调用它,否则调用其内部实现。
-
rsa_lib.c:主要实现了RSA运算的四个函数(公钥/私钥,加密/解密),它们都调用了RSA_METHOD中相应都回调函数。
-
rsa_none.c:实现了一种填充和去填充。
-
rsa_null.c:实现了一种空的RSA_METHOD。
-
rsa_oaep.c:实现了oaep填充与去填充。
-
rsa_pk1.c:实现了pkcs1填充与去填充。
-
rsa_sign.c:实现了RSA的签名和验签。
-
rsa_ssl.c:实现了ssl填充。
-
rsa_x931.c:实现了一种填充和去填充。
2.5.3 RSA 签名与验证过程
(一)签名过程
1)对用户数据进行摘要;
2)构造X509_SIG结构并DER编码,其中包括了摘要算法以及摘要结果。
3)对2)的结果进行填充,填满RSA密钥长度字节数。比如1024位RSA密钥必须填满128字节。具体的填充方式由用户指定。
4)对3)的结果用RSA私钥加密。
RSA_eay_private_encrypt函数实现了3)和4)过程。
(二)验证过程
1)对数据用RSA公钥解密,得到签名过程中2)的结果。
2)去除1)结果的填充。
3)从2)的结果中得到摘要算法,以及摘要结果。
4)将原数据根据 3)中得到摘要算法进行摘要计算。
5)比较4)与签名过程中1)的结果。
RSA_eay_public_decrypt实现了1)和2)过程。
2.5.4 RSA数据结构
RSA主要数据结构定义在crypto/rsa/rsa.h中。
(一)RSA_METHOD
struct rsa_meth_st
{
const char *name;
int (*rsa_pub_enc)(int flen,const unsigned char *from,unsigned char*to,RSA *rsa,int padding);
int (*rsa_pub_dec)(int flen,const unsigned char *from,unsigned char*to,RSA *rsa,int padding);
int (*rsa_priv_enc)(int flen,const unsigned char *from,unsigned char*to,RSA *rsa,int padding);
int (*rsa_priv_dec)(int flen,const unsigned char *from,unsigned char*to,RSA *rsa,int padding);
/* 其他函数 */
int (*rsa_sign)(int type,const unsigned char *m, unsigned int
m_length,unsigned char *sigret, unsigned int *siglen, const RSA *rsa);
int (*rsa_verify)(int dtype,const unsigned char *m, unsigned int
m_length,unsigned char *sigbuf, unsigned int siglen, const RSA *rsa);
int (*rsa_keygen)(RSA *rsa, int bits, BIGNUM *e, BN_GENCB *cb);
};
参数说明:
- name: RSA_METHOD名称;
- rsa_pub_enc:公钥加密函数, padding为其填充方式,输入数据不能太长,否则无法填充;
- rsa_pub_dec:公钥解密函数, padding为其去除填充的方式,输入数据长度为RSA密钥长度的字节数;
- rsa_priv_enc:私钥加密函数, padding为其填充方式,输入数据长度不能太长,否则无法填充;
- rsa_priv_dec:私钥解密函数, padding为其去除填充的方式,输入数据长度为RSA密钥长度的字节数;
- rsa_sign:签名函数;
- rsa_verify:验签函数;
- rsa_keygen: RSA密钥对生成函数。
用户可实现自己的RSA_METHOD来替换OpenSSL提供的默认方法。
(二)RSA
RSA数据结构中包含了公/私钥信息(如果仅有n和e,则表明是公钥),定义如下:
struct rsa_st {
/* 其他 */
const RSA_METHOD *meth;
ENGINE *engine;
BIGNUM *n;
BIGNUM *e;
BIGNUM *d;
BIGNUM *p;
BIGNUM *q;
BIGNUM *dmp1;
BIGNUM *dmq1;
BIGNUM *iqmp;
CRYPTO_EX_DATA ex_data;
int references;
/* 其他数据项 */
};
参数说明:
- meth: RSA_METHOD结构,指明了本RSA密钥的各种运算函数地址;
- engine:硬件引擎;
- n, e, d, p, q, dmp1, dmq1, iqmp: RSA密钥的各个值;
- ex_data:扩展数据结构,用于存放用户数据;
- references: RSA结构引用数。
2.5.5 函数说明
1) RSA_check_key:检查RSA密钥。
2) RSA_new:生成一个RSA密钥结构,并采用默认的rsa_pkcs1_eay_meth RSA_METHOD方法。
3) RSA_free:释放RSA结构。
- RSA *RSA_generate_key(int bits, unsigned long e_value,void (*callback)(int,int,void *), void *cb_arg) :生成RSA密钥, bits是模数比特数, e_value是公钥指数e, callback回调函数由用户实现,用于干预密钥生成过程中的一些运算,可为空。
5) RSA_get_default_method:获取默认的RSA_METHOD,为rsa_pkcs1_eay_meth。
6) RSA_get_ex_data:获取扩展数据。
7) RSA_get_method:获取RSA结构的RSA_METHOD。
8) 各种填充方式函数:
RSA_padding_add_none
RSA_padding_add_PKCS1_OAEP
RSA_padding_add_PKCS1_type_1(私钥加密的填充)
RSA_padding_add_PKCS1_type_2(公钥加密的填充)
RSA_padding_add_SSLv23
9) 各种去除填充函数:
RSA_padding_check_none
RSA_padding_check_PKCS1_OAEP
RSA_padding_check_PKCS1_type_1
RSA_padding_check_PKCS1_type_2
RSA_padding_check_SSLv23
RSA_PKCS1_SSLeay
10) int RSA_print(BIO *bp, const RSA *x, int off):将RSA信息输出到BIO中, off为输出信息在BIO中的偏移量,比如是屏幕BIO,则表示打印信息的位置离左边屏幕边缘的距离。
11) int DSA_print_fp(FILE *fp, const DSA *x, int off):将RSA信息输出到FILE中, off为输出偏移量。
12) RSA_public_decrypt:RSA公钥解密。
13) RSA_public_encrypt:RSA公钥加密。
14) RSA_set_default_method/ RSA_set_method:设置RSA结构中的method,当用户实现了一个RSA_METHOD时,调用此函数来设置,使RSA运算采用用户的方法。
15) RSA_set_ex_data:设置扩展数据。
16) RSA_sign:RSA签名。
17) RSA_sign_ASN1_OCTET_STRING:另外一种RSA签名,不涉及摘要算法,它将输入数据作为ASN1_OCTET_STRING进行DER编码,然后直接调用RSA_private_encrypt进行计算。
18) RSA_size:获取RSA密钥长度字节数。
19) RSA_up_ref:给RSA密钥增加一个引用。
20) RSA_verify:RSA验证。
21) RSA_verify_ASN1_OCTET_STRING:另一种RSA验证,不涉及摘要算法,与RSA_sign_ASN1_OCTET_STRING对应。
22) RSAPrivateKey_asn1_meth:获取 RSA 私钥的 ASN1_METHOD,包括 i2d、 d2i、 new 和 free 函数地址。
23) RSAPrivateKey_dup:复制RSA私钥。
24) RSAPublicKey_dup:复制RSA公钥。
2.5.6 编程示例
(一)密钥生成
#include <OpenSSL/rsa.h>
int main()
{
RSA *r;
int bits=512,ret;
unsigned long e=RSA_3;
BIGNUM *bne;
r=RSA_generate_key(bits,e,NULL,NULL);
RSA_print_fp(stdout,r,11);
RSA_free(r);
bne=BN_new();
ret=BN_set_word(bne,e);
r=RSA_new();
ret=RSA_generate_key_ex(r,bits,bne,NULL);
if(ret!=1)
{
printf("RSA_generate_key_ex err!\n");
return -1;
}
RSA_free(r);
return 0;
}
(二)RSA加密运算
#include <OpenSSL/rsa.h>
#include <OpenSSL/sha.h>
int main()
{
RSA *r;
int bits=1024,ret,len,flen,padding,i;
unsigned long e=RSA_3;
BIGNUM *bne;
unsigned char *key,*p;
BIO *b;
unsigned char from[500],to[500],out[500];
bne=BN_new();
ret=BN_set_word(bne,e);
r=RSA_new();
ret=RSA_generate_key_ex(r,bits,bne,NULL);
if(ret!=1)
{
printf("RSA_generate_key_ex err!\n");
return -1;
}
/* 私钥i2d */
b=BIO_new(BIO_s_mem());
ret=i2d_RSAPrivateKey_bio(b,r);
key=malloc(1024);
len=BIO_read(b,key,1024);
BIO_free(b);
b=BIO_new_file("rsa.key","w");
ret=i2d_RSAPrivateKey_bio(b,r);
BIO_free(b);
/* 私钥d2i */
/* 公钥i2d */
/* 公钥d2i */
/* 私钥加密 */
flen=RSA_size(r);
printf("please select private enc padding : \n");
printf("1.RSA_PKCS1_PADDING\n");
printf("3.RSA_NO_PADDING\n");
printf("5.RSA_X931_PADDING\n");
scanf("%d",&padding);
if(padding==RSA_PKCS1_PADDING)
flen-=11;
else if(padding==RSA_X931_PADDING)
flen-=2;
else if(padding==RSA_NO_PADDING)
flen=flen;
else
{
printf("rsa not surport !\n");
return -1;
}
for(i=0;i<flen;i++)
memset(&from[i],i,1);
len=RSA_private_encrypt(flen,from,to,r,padding);
if(len<=0)
{
printf("RSA_private_encrypt err!\n");
return -1;
}
len=RSA_public_decrypt(len,to,out,r,padding);
if(len<=0)
{
printf("RSA_public_decrypt err!\n");
return -1;
}
if(memcmp(from,out,flen))
{
printf("err!\n");
return -1;
}
/* */
printf("please select public enc padding : \n");
printf("1.RSA_PKCS1_PADDING\n");
printf("2.RSA_SSLV23_PADDING\n");
printf("3.RSA_NO_PADDING\n");
printf("4.RSA_PKCS1_OAEP_PADDING\n");
scanf("%d",&padding);
flen=RSA_size(r);
if(padding==RSA_PKCS1_PADDING)
flen-=11;
else if(padding==RSA_SSLV23_PADDING)
flen-=11;
else if(padding==RSA_X931_PADDING)
flen-=2;
else if(padding==RSA_NO_PADDING)
flen=flen;
else if(padding==RSA_PKCS1_OAEP_PADDING)
flen=flen-2 * SHA_DIGEST_LENGTH-2 ;
else
{
printf("rsa not surport !\n");
return -1;
}
for(i=0;i<flen;i++)
memset(&from[i],i+1,1);
len=RSA_public_encrypt(flen,from,to,r,padding);
if(len<=0)
{
printf("RSA_public_encrypt err!\n");
return -1;
}
len=RSA_private_decrypt(len,to,out,r,padding);
if(len<=0)
{
printf("RSA_private_decrypt err!\n");
return -1;
}
if(memcmp(from,out,flen))
{
printf("err!\n");
return -1;
}
printf("test ok!\n");
RSA_free(r);
return 0;
}
(三)签名与验证
#include <string.h>
#include <OpenSSL/objects.h>
#include <OpenSSL/rsa.h>
int main()
{
int ret;
RSA *r;
int i,bits=1024,signlen,datalen,alg,nid;
unsigned long e=RSA_3;
BIGNUM *bne;
unsigned char data[100],signret[200];
bne=BN_new();
ret=BN_set_word(bne,e);
r=RSA_new();
ret=RSA_generate_key_ex(r,bits,bne,NULL);
if(ret!=1)
{
printf("RSA_generate_key_ex err!\n");
return -1;
}
for(i=0;i<100;i++)
memset(&data[i],i+1,1);
printf("please select digest alg: \n");
printf("1.NID_md5\n");
printf("2.NID_sha\n");
printf("3.NID_sha1\n");
printf("4.NID_md5_sha1\n");
scanf("%d",&alg);
if(alg==1)
{
datalen=55;
nid=NID_md5;
}
else if(alg==2)
{
datalen=55;
nid=NID_sha;
}
else if(alg==3)
{
datalen=55;
nid=NID_sha1;
}
else if(alg==4)
{
datalen=36;
nid=NID_md5_sha1;
}
ret=RSA_sign(nid,data,datalen,signret,&signlen,r);
if(ret!=1)
{
printf("RSA_sign err!\n");
RSA_free(r);
return -1;
}
ret=RSA_verify(nid,data,datalen,signret,signlen,r);
if(ret!=1)
{
printf("RSA_verify err!\n");
RSA_free(r);
return -1;
}
RSA_free(r);
printf("test ok!\n");
return 0;
}
注意:本示例并不是真正的数据签名示例,因为没有做摘要计算。
ret=RSA_sign(nid,data,datalen,signret,&signlen,r)将需要运算的数据放入X509_ALGOR数据结构并将其DER编码,对编码结果做RSA_PKCS1_PADDING再进行私钥加密。
被签名数据应该是摘要之后的数据,而本例没有先做摘要,直接将数据拿去做运算。因此datalen不能太长,要保证RSA_PKCS1_PADDING私钥加密运算时输入数据的长度限制。ret=RSA_verify(nid,data,datalen,signret,signlen,r)用来验证签名。