OpenSSL抽象IO(I/O abstraction,即BIO)是OpenSSL对于IO类型的抽象封装,包括:内存、文件、日志、标准输入输出、socket(TCP/UDP)、加/解密、摘要和SSL通道等。OpenSSL BIO通过回调函数为用户隐藏了底层实现细节,所有类型的BIO的调用大体上是类似的。BIO中的数据能从一个BIO传送到另外一个BIO或者是应用程序。
本文假设你已经安装好了OpenSSL,并且持有一份1.1.1的源码。
BIO的头文件为bio.h,源文件实现在crypto/bio目录中。
主要结构:
struct bio_st {
const BIO_METHOD *method;
/* bio, mode, argp, argi, argl, ret */
BIO_callback_fn callback;
BIO_callback_fn_ex callback_ex;
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 */
CRYPTO_REF_COUNT references;
uint64_t num_read;
uint64_t num_write;
CRYPTO_EX_DATA ex_data;
CRYPTO_RWLOCK *lock;
};
typedef struct bio_st BIO;
这个结构定义了抽象IO的数据结构。主要字段含义:
method —— 抽象IO的函数方法集合。
init —— 初使化标志,对于文件IO来说,当使用BIO_set_fp()关联文件指针时,则置该标志为1。
shutdown —— 关闭标志,当该值不为0时,需要释放资源。
flags —— 用来存放控制标志集合,如BIO_CLOSE。
num —— 用来存放关联的整型句柄。
ptr —— 用来存放关联的指针型句柄。
next_bio —— 抽象IO是用链表来管理的,这里表示链接下一个节点。
prev_bio —— 链接的上一个节点。
struct bio_method_st {
int type;
char *name;
int (*bwrite) (BIO *, const char *, size_t, size_t *);
int (*bwrite_old) (BIO *, const char *, int);
int (*bread) (BIO *, char *, size_t, size_t *);
int (*bread_old) (BIO *, char *, int);
int (*bputs) (BIO *, const char *);
int (*bgets) (BIO *, char *, int);
long (*ctrl) (BIO *, int, long, void *);
int (*create) (BIO *);
int (*destroy) (BIO *);
long (*callback_ctrl) (BIO *, int, BIO_info_cb *);
};
typedef struct bio_method_st BIO_METHOD;
这个结构定义了具体的抽象IO的方法集合。主要字段含义:
type —— 抽象IO的类型。
name —— 抽象IO的名称。
bwrite —— 抽象IO的写操作函数实现。
bread —— 抽象IO的读操作函数实现。
ctrl —— 抽象IO的控制方法,关于控制方法,在头文件中,BIO会引入非常多的宏,最终都会传入到这里来,交给具体的抽象类型来处理。
对每种抽象IO类型来说,都有其对应的BIO结构及BIO_METHOD方法集合实现。
在1.1.1中,大多数的数据结构已经不再向使用者开放,从封装的角度来看,这是更合理的。如果你在头文件中找不到结构定义,不妨去源码中搜一搜。
主要函数:
BIO *BIO_new(const BIO_METHOD *type);
根据BIO方法集合,创建BIO对象,返回其指针。
其内部实现为:
{
BIO *bio = OPENSSL_zalloc(sizeof(*bio));
bio->method = method;
bio->shutdown = 1;
bio->references = 1;
if (method->create != NULL && !method->create(bio)) {
BIOerr(BIO_F_BIO_NEW, ERR_R_INIT_FAIL);
CRYPTO_free_ex_data(CRYPTO_EX_INDEX_BIO, bio, &bio->ex_data);
CRYPTO_THREAD_lock_free(bio->lock);
goto err;
}
return bio;
err:
OPENSSL_free(bio);
return NULL;
}
可以看到先初使化BIO数据结构,然后关联抽象方法,并进行具体的create调用。
注意:此方法并不确保抽象IO完成了初使化,比如对文件抽像IO来说,还需要绑定FILE指针。
int BIO_free(BIO *a);
释放BIO对象。
成功返回1,失败返回0。
其实现实现为:
{
if (a == NULL)
return 0;
if ((a->method != NULL) && (a->method->destroy != NULL))
a->method->destroy(a);
OPENSSL_free(a);
return 1;
}
可以看到关闭抽象IO、释放资源。
const BIO_METHOD *BIO_s_file(void);
这个函数返回一个默认的FILE抽象方法集合。
其内部实现为:
{
return &methods_filep;
}
可以看到直接返回一个预定义的BIO_METHOD对象指针。
关于methods_filep的定义:
static const BIO_METHOD methods_filep = {
BIO_TYPE_FILE,
"FILE pointer",
bwrite_conv,
file_write,
bread_conv,
file_read,
file_puts,
file_gets,
file_ctrl,
file_new,
file_free,
NULL,
};
类似的,OpenSSL支持的抽象IO方法集合还有:
const BIO_METHOD *BIO_s_mem(void);
const BIO_METHOD *BIO_s_socket(void);
const BIO_METHOD *BIO_s_connect(void);
const BIO_METHOD *BIO_s_accept(void);
const BIO_METHOD *BIO_s_fd(void);
const BIO_METHOD *BIO_s_log(void);
const BIO_METHOD *BIO_s_bio(void);
const BIO_METHOD *BIO_s_null(void);
const BIO_METHOD *BIO_s_datagram(void);
const BIO_METHOD *BIO_f_null(void);
const BIO_METHOD *BIO_f_buffer(void);
const BIO_METHOD *BIO_f_linebuffer(void);
const BIO_METHOD *BIO_f_zlib(void);
const BIO_METHOD *BIO_f_md(void);
const BIO_METHOD *BIO_f_base64(void);
const BIO_METHOD *BIO_f_cipher(void);
const BIO_METHOD *BIO_f_reliable(void);
const BIO_METHOD *BIO_f_ssl(void);
BIO *BIO_new_file(const char *filename, const char *mode);
这个函数创建BIO,并且关联抽象方法,并完成文件指针的绑定。
其内部实现为:
{
BIO *ret;
FILE *file = openssl_fopen(filename, mode);
int fp_flags = BIO_CLOSE;
if ((ret = BIO_new(BIO_s_file())) == NULL) {
fclose(file);
return NULL;
}
BIO_clear_flags(ret, BIO_FLAGS_UPLINK);
BIO_set_fp(ret, file, fp_flags);
return ret;
}
BIO *BIO_new_fp(FILE *stream, int close_flag);
这个函数创建BIO,并且关联抽象方法,并完成文件指针的绑定。
其内部实现为:
{
BIO *ret;
if ((ret = BIO_new(BIO_s_file())) == NULL)
return NULL;
/* redundant flag, left for documentation purposes */
BIO_set_flags(ret, BIO_FLAGS_UPLINK);
BIO_set_fp(ret, stream, close_flag);
return ret;
}
类似的,OpenSSL提供的直接根据相关参数创建BIO的函数还有:
BIO *BIO_new_mem_buf(const void *buf, int len);
BIO *BIO_new_dgram(int fd, int close_flag);
BIO *BIO_new_socket(int sock, int close_flag);
BIO *BIO_new_connect(const char *host_port);
BIO *BIO_new_accept(const char *host_port);
BIO *BIO_new_fd(int fd, int close_flag);
int BIO_new_bio_pair(BIO **bio1, size_t writebuf1,
BIO *BIO_new_ssl(SSL_CTX *ctx, int client);
BIO *BIO_new_ssl_connect(SSL_CTX *ctx);
BIO *BIO_new_buffer_ssl_connect(SSL_CTX *ctx);
const char *BIO_method_name(const BIO *b);
这个函数返回BIO的名称。
int BIO_method_type(const BIO *b);
这个函数返回BIO的类型。
关于类型取值:
/* There are the classes of BIOs */
# define BIO_TYPE_DESCRIPTOR 0x0100 /* socket, fd, connect or accept */
# define BIO_TYPE_FILTER 0x0200
# define BIO_TYPE_SOURCE_SINK 0x0400
/* These are the 'types' of BIOs */
# define BIO_TYPE_NONE 0
# define BIO_TYPE_MEM ( 1|BIO_TYPE_SOURCE_SINK)
# define BIO_TYPE_FILE ( 2|BIO_TYPE_SOURCE_SINK)
# define BIO_TYPE_FD ( 4|BIO_TYPE_SOURCE_SINK|BIO_TYPE_DESCRIPTOR)
# define BIO_TYPE_SOCKET ( 5|BIO_TYPE_SOURCE_SINK|BIO_TYPE_DESCRIPTOR)
# define BIO_TYPE_NULL ( 6|BIO_TYPE_SOURCE_SINK)
# define BIO_TYPE_SSL ( 7|BIO_TYPE_FILTER)
# define BIO_TYPE_MD ( 8|BIO_TYPE_FILTER)
# define BIO_TYPE_BUFFER ( 9|BIO_TYPE_FILTER)
# define BIO_TYPE_CIPHER (10|BIO_TYPE_FILTER)
# define BIO_TYPE_BASE64 (11|BIO_TYPE_FILTER)
# define BIO_TYPE_CONNECT (12|BIO_TYPE_SOURCE_SINK|BIO_TYPE_DESCRIPTOR)
# define BIO_TYPE_ACCEPT (13|BIO_TYPE_SOURCE_SINK|BIO_TYPE_DESCRIPTOR)
# define BIO_TYPE_NBIO_TEST (16|BIO_TYPE_FILTER)/* server proxy BIO */
# define BIO_TYPE_NULL_FILTER (17|BIO_TYPE_FILTER)
# define BIO_TYPE_BIO (19|BIO_TYPE_SOURCE_SINK)/* half a BIO pair */
# define BIO_TYPE_LINEBUFFER (20|BIO_TYPE_FILTER)
# define BIO_TYPE_DGRAM (21|BIO_TYPE_SOURCE_SINK|BIO_TYPE_DESCRIPTOR)
# define BIO_TYPE_ASN1 (22|BIO_TYPE_FILTER)
# define BIO_TYPE_COMP (23|BIO_TYPE_FILTER)
# ifndef OPENSSL_NO_SCTP
# define BIO_TYPE_DGRAM_SCTP (24|BIO_TYPE_SOURCE_SINK|BIO_TYPE_DESCRIPTOR)
# endif
#define BIO_TYPE_START 128
int BIO_read(BIO *b, void *data, int dlen);
从BIO读取一块数据,返回读取的字节数。
通常成功返回>0,失败返回<=0。
int BIO_write(BIO *b, const void *data, int dlen);
向BIO写入一块数据,返回写入的字节数。
通常成功返回>0,失败返回<=0。
int BIO_gets(BIO *bp, char *buf, int size);
从BIO读取一行数据,返回读取的字节数。
通常成功返回>0,失败返回<=0。
int BIO_puts(BIO *bp, const char *buf);
向BIO写入C字符串数据,返回写入的字节数。
通常成功返回>0,失败返回<=0。
long BIO_ctrl(BIO *bp, int cmd, long larg, void *parg);
向BIO写入一条控制指令。
成功返回1,失败返回0。
void *BIO_ptr_ctrl(BIO *bp, int cmd, long larg);
BIO_ctrl()的指针版。
long BIO_int_ctrl(BIO *bp, int cmd, long larg, int iarg);
BIO_ctrl()的整数版。
BIO *BIO_push(BIO *b, BIO *append);
连接两个BIO,将append链接到b尾部。
BIO *BIO_pop(BIO *b);
将b从链中弹出,解除b与其它BIO的链表关系。
BIO *BIO_next(BIO *b);
获取链接到b的下一个BIO。
BIO *BIO_find_type(BIO *b, int bio_type);
在b链接中搜索指定类型的BIO。
void BIO_free_all(BIO *a);
释放整个BIO链表。
助记宏函数:
由于BIO支持的种类非常多,而且每类BIO还分别实现了自己的ctrl函数,这个ctrl函数的参数是不确定的,BIO规范只是约定了接口形式。因此,BIO_ctrl的调用非常复杂,OpenSSL为了助记,将一些常用的调用整理为宏,这里只摘录一部分,如下:
# define BIO_NOCLOSE 0x00
# define BIO_CLOSE 0x01
# define BIO_get_fp(b,fpp) BIO_ctrl(b,BIO_C_GET_FILE_PTR,0,(char *)(fpp))
# define BIO_set_fp(b,fp,c) BIO_ctrl(b,BIO_C_SET_FILE_PTR,c,(char *)(fp))
# define BIO_get_fd(b,c) BIO_ctrl(b,BIO_C_GET_FD,0,(char *)(c))
# define BIO_set_fd(b,fd,c) BIO_int_ctrl(b,BIO_C_SET_FD,c,fd)
# define BIO_set_ssl(b,ssl,c) BIO_ctrl(b,BIO_C_SET_SSL,c,(char *)(ssl))
# define BIO_set_md(b,md) BIO_ctrl(b,BIO_C_SET_MD,1,(char *)(md))
# define BIO_set_mem_buf(b,bm,c) BIO_ctrl(b,BIO_C_SET_BUF_MEM,c,(char *)(bm))
使用举例1:
下面这个例子演示了内存抽象IO的创建、读写和释放操作。
#include <stdio.h>
#include <stdlib.h>
#include <stdint.h>
#include <unistd.h>
#include <openssl/bio.h>
namespace dakuang {}
int main(int argc, char* argv[])
{
BIO* bio = BIO_new(BIO_s_mem());
int nBytesWrite = BIO_write(bio, "hello", 5);
printf("write size:[%d] \n", nBytesWrite);
int nBytesPending = BIO_ctrl_pending(bio);
printf("pending size:[%d] \n", nBytesPending);
char* pBuf = (char *)malloc(nBytesPending + 1);
int nBytesRead = BIO_read(bio, pBuf, nBytesPending);
printf("read size:[%d] \n", nBytesRead);
if (nBytesRead > 0)
{
pBuf[nBytesRead] = '\0';
printf("read:[%s] \n", pBuf);
}
free(pBuf);
BIO_free(bio);
return 0;
}
输出:
write size:[5]
pending size:[5]
read size:[5]
read:[hello]
使用举例2:
下面这个例子演示了文件抽象IO的创建、读写和释放操作。
#include <stdio.h>
#include <stdlib.h>
#include <stdint.h>
#include <unistd.h>
#include <openssl/bio.h>
namespace dakuang {}
int main(int argc, char* argv[])
{
BIO* bio = BIO_new_file("testbio.txt", "w");
int nBytesWrite = BIO_write(bio, "hello", 5);
printf("write size:[%d] \n", nBytesWrite);
BIO_free(bio);
bio = BIO_new_file("testbio.txt", "r");
while (true)
{
char sBuf[128] = {0};
int nBytesRead = BIO_read(bio, sBuf, sizeof(sBuf)-1);
printf("read size:[%d] \n", nBytesRead);
if (nBytesRead <= 0)
break;
printf("read body:[%s] \n", sBuf);
}
BIO_free(bio);
return 0;
}
输出:
write size:[5]
read size:[5]
read body:[hello]
read size:[0]
使用举例3:
下面这个例子演示了套接口抽象IO的创建、读取和释放操作。
#include <stdio.h>
#include <stdlib.h>
#include <stdint.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <netinet/in.h>
#include <openssl/bio.h>
namespace dakuang {}
int main(int argc, char* argv[])
{
int sock = socket(AF_INET, SOCK_STREAM, 0);
struct sockaddr_in sinto;
sinto.sin_family = AF_INET;
sinto.sin_addr.s_addr = inet_addr("127.0.0.1");
sinto.sin_port = htons(9999);
int ret = connect(sock, (struct sockaddr*)&sinto, sizeof(sinto));
if (ret < 0)
{
printf("connect failed! \n");
return -1;
}
printf("connect ok! \n");
BIO* pBio = BIO_new_socket(sock, BIO_NOCLOSE);
while (true)
{
char sBuf[128] = {0};
int nBytesRead = BIO_read(pBio, sBuf, sizeof(sBuf)-1);
printf("read size:%d \n", nBytesRead);
if (nBytesRead <= 0)
break;
printf("read body:[%s] \n", sBuf);
}
BIO_free(pBio);
close(sock);
return 0;
}
启动服务器:
nc -lv4 9999
运行这个例子,并且在nc端输入hello,输出:
connect ok!
read size:[6]
read body:[hello
]
read size:[0]
使用举例4:
下面这个例子演示了使用抽象IO进行MD5摘要计算的过程。
#include <stdio.h>
#include <stdlib.h>
#include <stdint.h>
#include <unistd.h>
#include <openssl/crypto.h>
#include <openssl/bio.h>
#include <openssl/evp.h>
namespace dakuang {}
int main(int argc, char* argv[])
{
BIO* bioMD = BIO_new(BIO_f_md());
const EVP_MD* md = EVP_md5();
BIO_set_md(bioMD, md);
BIO* bioNull = BIO_new(BIO_s_null());
BIO_push(bioMD, bioNull);
int nBytesWrite = BIO_write(bioMD, "hello", 5);
printf("write:[%d] \n", nBytesWrite);
char sBuf[128] = {0};
int nBytesRead = BIO_gets(bioMD, sBuf, sizeof(sBuf) - 1);
printf("read %d : [%s] \n", nBytesRead, OPENSSL_buf2hexstr((const unsigned char*)sBuf, nBytesRead));
BIO_free(bioNull);
BIO_free(bioMD);
return 0;
}
输出:
write:[5]
read 16 : [5D:41:40:2A:BC:4B:2A:76:B9:71:9D:91:10:17:C5:92]
关于MD BIO,从源码实现来看,其尾部必须链接一个过滤BIO,并且其行为如下:
- 每次MD BIO写入数据之前,先向尾部的过滤BIO尝试写入,以过滤BIO成功写入的数据为准,再向自已写入。
- 每次MD BIO读取数据之前,先向尾部的过滤BIO尝试读取,如果读取到数据,要向自已写入。
- MD BIO只在BIO_gets()方法生成摘要。
使用举例5:
下面这个例子演示了使用抽象IO进行DES加解密的过程。
#include <stdio.h>
#include <stdlib.h>
#include <stdint.h>
#include <string.h>
#include <openssl/crypto.h>
#include <openssl/bio.h>
#include <openssl/evp.h>
namespace dakuang {}
int main(int argc, char* argv[])
{
char sKey[] = "12345678";
char sIV[] = "12345678";
char sText[] = "hello";
char sCipher[128] = {0};
int nCipherLen = 0;
{
BIO* bioCipher = BIO_new(BIO_f_cipher());
const EVP_CIPHER* cipher = EVP_des_ecb();
BIO_set_cipher(bioCipher, cipher, (const unsigned char*)sKey, (const unsigned char*)sIV, 1);
BIO* bioNull = BIO_new(BIO_s_null());
BIO_push(bioCipher, bioNull);
int nBytesWrite = BIO_write(bioCipher, sText, strlen(sText));
printf("write:[%d] \n", nBytesWrite);
char sBuf[128] = {0};
int nBytesRead = BIO_read(bioCipher, sBuf, sizeof(sBuf) - 1);
printf("read %d : [%s] \n", nBytesRead, OPENSSL_buf2hexstr((const unsigned char*)sBuf, nBytesRead));
memcpy(sCipher, sBuf, nBytesRead);
nCipherLen = nBytesRead;
BIO_free(bioNull);
BIO_free(bioCipher);
}
{
BIO* bioCipher = BIO_new(BIO_f_cipher());
const EVP_CIPHER* cipher = EVP_des_ecb();
BIO_set_cipher(bioCipher, cipher, (const unsigned char*)sKey, (const unsigned char*)sIV, 0);
BIO* bioNull = BIO_new(BIO_s_null());
BIO_push(bioCipher, bioNull);
int nBytesWrite = BIO_write(bioCipher, sCipher, nCipherLen);
printf("write:[%d] \n", nBytesWrite);
char sBuf[128] = {0};
int nBytesRead = BIO_read(bioCipher, sBuf, sizeof(sBuf) - 1);
printf("read %d : [%s] \n", nBytesRead, sBuf);
BIO_free(bioNull);
BIO_free(bioCipher);
}
return 0;
}
输出:
write:[5]
read 8 : [BA:16:C6:A0:25:71:25:AF]
write:[8]
read 5 : [hello]