openssl中BIO的使用

发布于:2025-05-10 ⋅ 阅读:(5) ⋅ 点赞:(0)

BIO是OpenSSL的一个重要的结构和概念,是对数据IO与传递处理的一种类型抽象和功能封装,这里所说的数据IO与传递的“介质”包括:内存、文件、日志、标准设备、网络socket等,“处理”包括简单、加/解密、摘要、ssl协议下的读/写等的数据变换。

本文主要介绍了BIO的结构和使用方法,并以示例方式给出了一种对BIO的封装,可为下一步与socket的相关操作结合,编写自定义的“加密/解密流”。

本文示例适用于openssl3.0+。

1.概念

BIO是针对数据传递而设计的逻辑结构,因此它基本的概念模型是“管道”:数据由BIO一端进入,另一端流出,中间也可能进行数据变换。这样的“管道”结构可以依据是否有“缓存”,出入口等条件进行划分。

OpenSSL中将BIO分为两种类型:

  • Filter BIO:就是纯管道型BIO,数据不能保存在其中。
  • source/sink BIO:自带有容器的BIO,数据进行缓存,如果进一步细分,source BIO就是只出不进BIO,sink BIO就是只进不出BIO。

两种类型的BIO的概念示意图如上图所示。可以想象,如果将BIO首尾连接起来,就会构成BIO链,也如上图所示。BIO链在使用上,虽然仅对首尾BIO进行了读写操作,但是这种操作是会依次传递给下一个BIO的,因而BIO链逻辑上看作是一个复杂的BIO。

2.类型

OpenSSL中已经预定义了若干种BIO,直接可以使用它们。

主要Source/Sink类型BIO

  •  BIO_s_file/ BIO_s_fd:文件BIO,BIO_s_file对应FILE*,而BIO_s_fd对应POSIX文件描述符,它们可用于写入和读取文件。
  • BIO_s_socket:网络socketBIO,用于通过网络进行通信。
  • BIO_s_null: 空BIO,类似/dev/null,只能写入,读取数据会导致EOF。
  • BIO_s_mem:内存BIO,用于写入和读取内存。
  • BIO_s_bio:一种特别的BIO,被称为BIO pair,后文单独说明。

主要Filter类型BIO

  • BIO_f_base64:base64 BIO,通过此BIO的BIO_write将数据编码为base64格式, BIO_read通过此BIO解码base64格式的数据。
  • BIO_f_cipher:密码BIO,通过它的数据会被加/解密,密码算法可以设置。
  • BIO_f_md:摘要计算BIO,它不会修改通过它的数据,而仅计算流经其中的数据摘要,摘要算法可以设置,使用特殊功能检索计算出的摘要。
  • BIO_f_buffer:缓冲BIO,它也不会更改通过它的数据。写入此BIO的数据被缓冲,因此并非每次对该BIO的写入操作都会导致将数据写入下一个BIO。至于阅读,情况类似。这样可以减少位于缓冲IO后面的BIO上的IO操作数。
  • BIO_f_ssl :SSL/TLS 协议BIO,通过它的数据会按照协议规则进行加解密
  • 3.基本使用函数

创建/释放函数为:

BIO *BIO_new(const BIO_METHOD *)
BIO_free_all(BIO *)

设置/控制基本函数为:

BIO_ctrl(BIO *,int,long ,void *)

以此函数为基础,定义了一些方便使用的宏:BIO_reset,BIO_tell,BIO_eof,BIO_flush等
读写操作的基本函数为:

int BIO_read_ex(BIO *, void *, size_t, size_t *)
int BIO_write_ex(BIO *, const void *, size_t, size_t *)

对于BIO链,bio_st结构中有变量next_bio,prev_bio,可以指向其前后的BIO,这也是BIO链式操作的基础。BIO链的基本操作函数为:

BIO * BIO_push(BIO *a,BIO *b);
BIO * BIO_pop(BIO *b);

前者将b链接到a之后,返回b,后者将b从链条上摘除,返回b,原来的链条依然完整。

BIO还有一些辅助函数,例如处理错误,获取状态等函数。

4.BIO Pair

BIO对是一种比较特别的BIO,它由两个BIO组成,但从它的实现代码来看,它似乎是与BIO平行的一种实现方式,因而不能单纯的用BIO链来说明,它的逻辑结构如下图所示。

BIO pair连接两个外部端A,B,从外部来看,A端写,则可从B端读出;B端写,则可从A端读出。从内部来看,有两个内存型BIOA和BIOB,分别与A端和B端相连,它们有各自的缓存,A端存入和B端读取的数据,利用的是BIOA缓存,B端写入A端读取则利用的是BIOB的缓存。

因此BIO pair类似于“双向有缓存管道”,从任一端写入,另一端读出,由于内存缓存的利用,使得一端的读/写操作都是“即刻”完成的,它不用关心另一端什么时候做写/读操作,这就是典型的异步操作。目前很多网络通信库采用的都是异步读写,因而BIO pair这种应用模型是OpenSSL适配这些网络库的一个重要方法。

创建BIO pair的简洁方法是BIO_new_bio_pair,它实际上是 BIO_new, BIO_make_bio_pair,  BIO_set_write_buf_size的组合。

5.构造自定义BIO类型

OpenSSL已定义了若干BIO,当然也可以自定义一个。下面的示例构造了一个简单的CMYBIO,以此来说明BIO的工作原理。

class CMYBIO
{
private:
	static int write_ex(BIO* h, const char* buf, size_t num, size_t * len)
	{
		printf("CMYBIO::write_ex\n");
		for (int i = 0; i < num; ++i)  printf("%c", buf[i]);
		printf("\n");
		*len = num;
		return 1;
	}
	static int read_ex(BIO* h, char* buf, size_t size, size_t* len)
	{
		printf("CMYBIO::read_ex\n");
		unsigned int* opt = (unsigned int*)BIO_get_data(h);
		if (*opt == 0)
		{
			size = size > 6 ? 6 : size;
			memcpy(buf, "openss", size);
			*len = size;
//			BIO_clear_retry_flags(h);
//			BIO_copy_next_retry(h);
			return 1;
		}
		if (*opt == 1)
		{
			size = 9;
			memcpy(buf, "MTIzNDU2\n", size);           //base64("123456")
			*len = size;
			BIO_clear_retry_flags(h);
//			BIO_copy_next_retry(h);
			return 1;
		}
		return 0;
	}
	static long ctrl(BIO* h, int cmd, long arg1, void* arg2)
	{
		printf("CMYBIO::ctrl[%d]\n", cmd);
		if (cmd == 0xff)
		{
			unsigned int* opt = (unsigned int*)BIO_get_data(h);
			*opt = arg1;
		}
		return 1;
	}
	static int create(BIO* bio)
	{
		printf("CMYBIO::create\n");
		BIO_set_init(bio, 1);            //”init” must be set explicitly,otherwise “read”/”write” methods will be invoked. Although it seems odd, the “framework” would not do It for you by the returned value. 
		unsigned int* opt =(unsigned int*) malloc(sizeof(int));
		*opt = 0;
		BIO_set_data(bio,opt);
		return 1;
	}
	static int destory(BIO* bio)
	{
		printf("CMYBIO::destory\n");
		unsigned int* opt=(unsigned int*)BIO_get_data(bio);
		free(opt);
		return 1;
	}
	static BIO_METHOD* method;
public:
	static BIO_METHOD* BIO_s_my()
	{
		return CMYBIO::method;
	}
	static void UnInit()
	{
		BIO_meth_free(CMYBIO::method);
	}

	static void Init(void)
	{
		CMYBIO::method = BIO_meth_new((100 | BIO_TYPE_SOURCE_SINK), "My BIO");
		int r;
		r = BIO_meth_set_create(CMYBIO::method, CMYBIO::create);
		r = BIO_meth_set_destroy(CMYBIO::method, CMYBIO::destory);
		r = BIO_meth_set_write_ex(CMYBIO::method,  CMYBIO::write_ex);
		r=BIO_meth_set_read_ex(CMYBIO::method, CMYBIO::read_ex);
		r=BIO_meth_set_puts(CMYBIO::method,  nullptr);
		r=BIO_meth_set_gets(CMYBIO::method, nullptr);        
		r=BIO_meth_set_ctrl(CMYBIO::method, CMYBIO::ctrl);
		r=BIO_meth_set_callback_ctrl(CMYBIO::method, nullptr);
	}

};

BIO_METHOD* CMYBIO::method=nullptr;

下面的代码使用了上面构造的CMYBIO,其中第一段是单独使用CMYBIO,第二段是与BIO_f_base64组成链式应用。CMYBIO作为单独应用,构造的功能已经够用,但组成链式应用,由于链式调用需要在每个具体的实现方法内来完成,因此上面的代码还不够,为简化,这里仅把CMYBIO作为链式应用的最后一级来使用。

long bio_cb(BIO* b, int oper, const char* argp, size_t len, int argi, long argl, int ret, size_t* processed)
{
	printf("bio callback:%d, %u\n", oper,(unsigned int)len);
	return 1;
}
void test_bio()
{
	BIO* bmy = NULL;
	size_t s = 0;
	int len = 0;
	char* out = NULL;
	char cc[24];
	memset(cc, 0, sizeof(cc));

	CMYBIO::Init();

	bmy = BIO_new(CMYBIO::BIO_s_my());
	BIO_set_callback_ex(bmy, bio_cb);

	len = BIO_write_ex(bmy, "openssl", 7, &s);
	printf("BIO_write_ex return [%d, %u]\n\n", len, (unsigned int)s);

	len = 7;
	out = (char*)OPENSSL_malloc(len);
	memset(out, 0, len);
	len = BIO_read_ex(bmy, out, len-1, &s);
	printf("BIO_read_ex return [%d,%u]%s\n", len, (unsigned int)s, out);
	OPENSSL_free(out);
	BIO_free(bmy);

	printf("--------------------------------\n");
	BIO* b64 = BIO_new(BIO_f_base64());
	BIO_set_flags(b64, BIO_FLAGS_BASE64_NO_NL);
	bmy = BIO_new(CMYBIO::BIO_s_my());

	BIO_push(b64, bmy);
	len=BIO_write_ex(b64, "123456", 6, &s);
	printf("BIO_write_ex return [%d, %u]\n\n", len, (unsigned int)s);
	BIO_flush(b64);             //important!!!

	BIO_ctrl(bmy, 0xff, 1, nullptr);
	len = BIO_read_ex(b64, cc, sizeof(cc)- 1, &s);
	printf("BIO_read_ex return [%d,%u]%s\n\n", len, (unsigned int)s, cc);
	//OPENSSL_free(out);
	BIO_free_all(b64);

	CMYBIO::UnInit();
}

整个过程输出如下图所示。前一个示例主要展示CMYBIO中各方法的调用过程,其中还设置了“钩子”函数,从输出可以清晰看出BIO的工作过程。后一个示例稍有些麻烦,其中增加了CMYBIO内部的设置,从而可以实现不同的效果。

需要注意的是,BIO_f_base64有其特殊之处,对于“write”,需要在完成后调用flush才能正确工作,对于“read”,编码串结束的标志是“\n”,但还是要设置BIO_set_flags(b, BIO_FLAGS_BASE64_NO_NL),否则可能会有错误。


网站公告

今日签到

点亮在社区的每一天
去签到