OpenSSL TLS1.3初探

@vrqq  June 20, 2020

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_readssl_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代码见附件


添加新评论