Introduction
首先TLS:可以理解TLS为一个加密/解密黑盒,例如TLS1.2、SSL3.0等等都是不同的黑盒种类,而其中选取什么加密算法 指的是该黑盒的运行参数。
TLS怎么用:可以理解成“一头plaintext进,另一头ciphertext出”,经TCP通道,送达另一端黑盒“ciphertext进,piantext出”
一般使用TLS over TCP,首先TLS不参与TCP握手,TCP保证了数据包有序 不丢包。TLS黑盒是“时间无关的”,也就是说无论TCP链路延迟波动,他只是一个“加密/解密”的盒子。
TLS黑盒:是 openssl套件 中的一个 组件,该黑盒的接口见下文 第三部分 TLS API
证书、X509、证书链
以上https网站为例,网站开发者私有private key,公开public certification,这两个是一对儿。
另外有一“可信机构”,发布了它的public cert保存在访客电脑中,“可信机构”自己持有private key。
网站开发者的public cert 经 “可信机构的cert签名” 这一动作后,“网站的cert”就会位于“可信机构的cert”下方,形成了一个链。
如果证书都层层签名,就组成了证书链,用户信任某个证书,在其下方(直接或间接通过其签名)的证书都是“可信的”。
换个角度说,“可信机构”的证书,和“网站开发者”的证书,本质上没什么区别,只是“谁签名谁”决定了 谁在链的顶端。
OpenSSL BIO
可以理解成一个“tunnel”,以membio为例,一端连接“TLS黑盒”,另一端是内存的一个数组。
<懒得写了 直接看代码吧>
BIO *alice, *bob;
int rv = BIO_new_bio_pair(&(alice), 5, &(bob), 10);
const char hellomsg[] = "Hello world!";
int sz1 = BIO_write(alice, hellomsg, sizeof(hellomsg)); //sz1 == 5
logger.info("Alice said {}/{} bytes.", sz1, sizeof(hellomsg));
//此时bio写满了
int sz2 = BIO_write(alice, hellomsg, sizeof(hellomsg)); //sz2 == -1
logger.info("Alice said {}/{} bytes.", sz2, sizeof(hellomsg));
//此时写不进去了
//--- 开始读 ---
int p1 = BIO_pending(bob);
logger.info("Bob recived 1> {} bytes.", p1); //p1 == 5
char* buf = new char[p1+999];
int rdsz = BIO_read(bob, buf, p1+999); //rdsz == 5
logger.info("Bob read {}, {} bytes.\n", string(buf, rdsz), rdsz);
delete[] buf;
//读了5个,读没了
logger.info("Bob recived 2> {} bytes.", rdsz=BIO_pending(bob)); //rdsz == 0
buf = new char[99];
rdsz = BIO_read(bob, buf, 99); //rdsz == 0
delete[] buf;
logger.info("Bob received: {} bytes.\n", rdsz);
//没得可读,返回0
“TLS黑盒” 的 TLS API
上文说到TLS是个黑盒,那操作黑盒总该有接口,如下:
- SSL_connect() 通过 底层通道 发送“tls layer创建请求”数据包,也就是“tls黑盒”初始化协商,
- SSL_accept() 通过 底层通道 接收connect请求 形成“虚拟的layer”,此时“tls黑盒”初始化完成
- plain = SSL_read() 从“tls黑盒”读数据,得到plain text,“tls黑盒”会从 底层通道 读密文并自动解密
- SSL_write(plain) 向“tls黑盒”写plain text,“tls黑盒”会自动加密并通过 底层通道 送走
- SSL_set_bio() 或 SSL_set_fd() 指定“cipher text”流入流出黑盒的 底层通道,例如某socket,某stream,某个内存区域 (在Openssl里底层都是BIO,一个统一的“数据流通道”)
看了上述接口后,数据流应该就比较清晰了。官方文档很长,虽然不是循序渐进,但是总结的也很精炼。
TLS初始化过程
创建
SSL_CTX
,其中规定了- 校验对方证书时的用到的CA,我方public cert和private key
- TLS版本(例如 同时支持TLS1.2 1.3 等等)
SSL_CTX *ctx = SSL_CTX_new(TLS_method());
SSL_CTX
相当于创建 下面说的TLS Connection的 option
创建 TLS连接
SSL *conn= new SSL_new(ctx);
- 这就是“TLS黑盒”,可以在conn上使用上述接口
Server Side样例 假设我们已经初始化好了一个 socket stream: fd
int fd = 999; // a socket descriptor
SSL_CTX *ctx = new SSL_CTX(); // create option
SSL *conn = SSL_new(ctx); // create connection
SSL_set_fd(conn, fd); // set cipher-data in/out tunnel
SSL_accept(conn); // confirm a "virtual ssl layer" over "fd tunnel"
while(1) {
SSL_read(conn, rdbuf, rdsize);
SSL_write(conn, wdata, wsize);
}
Client Side样例 假设我们已经初始化好了一个 socket stream: fd
int fd = 999; // a socket descriptor
SSL_CTX *ctx = new SSL_CTX(); // create option
SSL *conn = SSL_new(ctx); // create connection
SSL_set_fd(conn, fd); // set cipher-data in/out tunnel
SSL_connect(conn); // request a "virtual ssl layer" over "fd tunnel"
while(1) {
SSL_read(conn, rdbuf, rdsize);
SSL_write(conn, wdata, wsize);
}
说了这么久也没说到BIO,如果上述“TLS黑盒”不使用fd,改使用membio
BIO *iossl, *ionet;
conn = SSL_new(ctx);
BIO_new_bio_pair(&iossl, 0, &ionet, 0);
SSL_set_bio(conn, iossl, iossl);
//这样,我们就可以通过下述命令,从SSL conn抽水/注水
BIO_read(ionet, buf, sizeof(buf)); //从“TLS黑盒”抽水
write_to_network(buf); //将抽出来的水送走
d = read_from_network(); //从网上读到新来的数据
BIO_write(ionet, d, sizeof(d)); //把新来的数据push到“TLS黑盒”中
插播下SSL的ERROR说明
- SSL_ERROR_WANT_READ 该条指令由于流入黑盒的BIO数据不够用,不能完成,须向该流入通道继续灌入数据后,重新调用SSL指令
- SSL_ERROR_WANT_WRITE 该条指令由于流出黑盒的BIO拥堵,数据写不完,需在BIO另一端 将该流出通道抽干后,重新调用SSL指令
其实blocked-io和unblocked在逻辑上不差啥,我们可以从这几个角度考虑:
- 谁新建了一个事件,驱动“代码继续执行”
- 谁霸占“代码执行的线程”
以上面Clien/Server Side代码举例,如果
- 在
ssl_read
,ssl_write
时候可以临时返回,他就放弃占有线程 - 从网络上读一个数据包,通过 底层BIO 送进“tls黑盒”后,唤醒刚才临时返回的位置,那“从网络上读一个数据包”就是一个事件,驱动了代码继续执行。
那么我们就可以依照这个思路搭起来下述框架
- 一个 “由外部触发的函数onData”,注意是被其他地方调起
- 一个函数flushBIO,通过bio从TLS黑盒抽水,并送走(送进大海,不会拥堵)
- 当某操作SSL_<*>遇到 SSL_ERROR_WANT_WRITE,说明他不能向底层bio写入足够完成此命令的数据,应该flushBIO后重试,因为flushBIO不会拥堵,因此可以无限重试直到该条操作完成
当某操作遇SSL_ERROR_WANT_READ时,说明现有的收到的数据不足以完成当前SSL_<*>命令,直接return,等待onData重新触发该流程
以上述思路完成onData的negocation部分:string onData(ciphertext) { for (int i=0; i<ciphertext.size();) { i += BIO_write(ionet, ciphertext.c_str(), ciphertext.size() - i); if (status == Status::NEGOCIATION) { int rv = triggerNegociationRoutine(); if (rv == SSL_ERROR_WANT_READ) continue; else ; //handle internal error here. } if (status == Status::ESTABLISHED) ; // do something here...... } } //@return errno of SSL_get_error() int triggerNegociationRoutine() { long e = SSL_ERROR_NONE; do { //可能bio被“SSL黑盒”屡次写满,需要多次调用flushIONet();,例如bio大小为1 byte if (is_server_side) rv = SSL_accept(conn); else rv = SSL_connect(conn); flushIONet(); //假设此函数可以totally clear bio if (rv <= 0) e = SSL_get_error(conn, rv); else { e = SSL_ERROR_NONE; status = Status::ESTABLISHED; } }while(e == SSL_ERROR_WANT_WRITE); return e; }
简单分析一下:
triggerNegociationRoutine()
可以由2个地方触发:初始化时候触发以发起连接,onData触发以继续。
在triggerNegociationRoutine()
内部会将要送出去的bio排净,最终得到“WANT_READ”或“NOERROR”或“其他的fatal error”状态。
具体sample代码见附件