TLS自动机实现时一些不错地方
前言
实现TLS自动机的时候我们实际上做了不少东西,有不少地方都挺巧的。2020/12/9更新了一下,因为忘得差不多了,补充下自动机的代码。
##
SESSION方面
对于SESSION方面的优化我们做了两点
- TLS1.3 VS方面我们直接放弃了SESSION ID的做法,直接用SESSION TICKET,这样子服务端的存储大大缓解,只需要每次客户端服务端做个校验即可。
- TLS1.3之前的版本,我们设计的数据模型为:一个大的hash表用于参与到查找,每个vhost存有等同于最大线程数的cache entry用来存储cache,每个cache entry由锁来保护,本身每个L4线程绑核。使用SESSION ID的前四字节
#define SESSION_CACHE_KEY(sp) (*((uint32_t *)((sp)->session_id)))
计算hp,使用#define SSL_SC_KEY(sp) ((SESSION_CACHE_KEY(sp) & SSL_SC_HASH_SIZE ) % SSL_SC_LOCK_GRAIN_SIZE)
计算添加的cache entry。简单来说,我们使用了细粒度锁,锁粒度为最大线程数。会出现多个不通hp存储到同一个cache entry的情况。至于hash表的结构就一句话带过了:数组下面挂一个二叉树
typedef struct ssl_vhost {
TAILQ_ENTRY(ssl_vhost) next_vhost;
/* session cache entries */
vh_schead_smp_t session_cache[SSL_SC_LOCK_GRAIN_SIZE];
}...
struct mtx ssl_session_lock[SSL_SC_LOCK_GRAIN_SIZE];
TLS1.3方面
自动机综述
Record自动机
TLS自动机本质是分层的,下层为RECORD LAYER,上层为HANDSHAKE LAYER。这种设计方式是反直觉的,但是本质上还是简化了研发和DEBUG的难度,
Record Layer和Handshake Layer是相互嵌套的,Record Layer不断的转,当收集到足够的信息或者上层自动机足够的消息发完了会不断继续转。从本质来说Record Layer就是做加解密的事情的,但是需要注意的是,Record Layer 会多转一次,直到遇见当前卡/SOFTSSL正在计算的flag才会退出。
while (!done) {
if (ssl_record_state[sslp->rstate].rfunc == NULL) {
sslstats.ssls_record_null_state++;
if (sslp->error_code == 0) {
sslp->error_code = RST_ID_FROM_SSL_20;
}
ssl_error(sslp);
return SSL_ERROR;
}
action = (*ssl_record_state[sslp->rstate].rfunc)(sslp);
switch (action) {
case RECORD_ACTION_ERROR:
/* Bug 23415, chenyl, 20090824 */
if (sslp->error_code == 0) {
sslp->error_code = RST_ID_FROM_SSL_9d;
}
/* Bug 23415, end */
ssl_error(sslp);
done = 1;
break;
case RECORD_ACTION_CLOSE:
if (ssl_close(sslp) == SSL_OK && sslp->tcp) {
conn_insert_event(sslp->tcp);
}
done = 1;
break;
case RECORD_ACTION_END:
done = 1;
break;
case RECORD_ACTION_CONTINUE:
continue;
case RECORD_ACTION_NOTIFY_SSL:
if (sslp->flags &
(SSL_FLAG_CLOSE_PENDING | SSL_FLAG_SENDANDCLOSE)) {
/* upper layer isn't interested anymore */
continue;
}
if (ssl_state_continue(sslp) != SSL_OK) {
done = 1;
}
break;
default:
sslstats.ssls_record_bad_action++;
if (sslp->error_code == 0) {
sslp->error_code = RST_ID_FROM_SSL_21;
}
ssl_error(sslp);
return SSL_ERROR;
}
}
来看看TLS1.3的Record Layer,首先判断是不是在卡/softssl里面做计算,如果是的话直接退出。否则继续,然后就是一个饥饿阻止的操作:因为可能同时有数据要加密和解密,如果上次做的是解密,那么本次就加密。避免服务端的数据被”饿死”。
如果不是饥饿阻止的情况,那么检查报文是否为SSLv3报文,然后检查报文最外层的合法性。之后进入新状态,等待下层送上来足够的报文。等待完整报文阶段也检查是否在做加解密。首先检查当前报文的种类,握手报文/应用数据报文,决定是否做加解密操作,然后根据当前握手自动机的状态和SSLP是否有cipher,选择不同的解密Context,比方说当前是硬件接收0-RTT数据,就需要调用n5_early_context。如果是软加解密情况,指教调用pending_context即可。加解密的单位是一整个RECORD报文,
这里需要加个特殊的地方,如果context为NULL,且该报文是个应用数据报文,说明这是一个我们不接受的0-RTT数据,这个时候直接destroy这个数据包,因为客户端可能不清楚我们不该接收这个数据包,而发了很多0-RTT数据,接着回到等待报文也就是SSL_RECORD_START阶段。
我们接着说,上面如果是应用数据,那么我们送卡做解密,RECORD 自动机进入DECRYPT阶段,DECRYPT阶段并不是解密,解密是卡做的,DECRYPT阶段是解密完之后的处理。如果是握手报文,调用handle_data做分类,确定是不是上送。解密和握手报文最终调用handle_data来上送。
HANDSHAKE自动机
我做的是REUSE和0-RTT自动机,握手自动机还是非常简单的说起来。检查ClientHello是不是有PSK拓展,有的话做PSK verify,我拆成了hmac verify + ticket identity decrypt + ticket lifetime chekc & sni check & ticket lifettime mismatch + compute hmac & binder key + handshake hash + binder verify。后面的计算不需要赘述
对于0-RTT来说,发送完了finished消息之后(这里先发送finished是正常的,因为客户端可以一直发0-RTT数据直到收到服务端的finished报文)就需要计算early secret,此时还是在handshake自动机里转。 接收early data的阶段我称为TLSV13S_S_PRE_ESTABLISHED状态,实际上和ESTABLISHED阶段没区别的,函数是一个。对于END_OF_EARLY_DATA的判断实在ESTABLISHED函数里面判断的,0-RTT
TLS1.3的0-RTT数据
对于VS端的0-RTT是个比较麻烦的问题,即使后面是HTTPS的连接,你也得等0-RTT收完了才能发送,贼蛋疼。这个地方我们做了很多东西
- soft/hard context的分配和管理,这个地方存在的问题是,完整握手情况下计算了key share才会分配context,而0-RTT数据必须立刻算出early_context并存储early_secret,当受到END_OF_EARLY_DATA时也有必要立刻释放early_context
- 如果客户端带了0-rtt数据,我们接受,两边都不是TCPS,那么我们直接调用向后端建立连接,也就是立刻向后端建立pipe,所谓的建立pipe并不是只建管道,而是pipe_open调用后面的握手函数,开始握手并建立连接。也就是说此刻vs端的record layer本质上就是个解密的操作,解密完了丢到后面去,没有直接关系了。可能由问题,如果服务端数据来了,这时候怎么办?没啥问题,是可以发送服务端回送的数据的,完整性虽然没保证,但是还是可以的。参照TLS1.3 RFC Page17的流程图。
- 建立连接
-
如果两边都是TCPS,我们决定接受0-RTT数据那就需要缓存客户端0-RTT数据再发送到后端,我们先缓存0-RTT数据,直到我们验证了客户端的finished报文再通知后面可以发送early_data了。前面开PIPE的时候我们就不会直接让后面客户端握手,而是让客户端进入
TLSV13S_C_WAIT_CLIENT_EARLY_DATA_END
状态,然后直接让这个握手函数先暂停,等vs这边的消息。我们会将消息放到user_event队列。每个ATCP线程处理完当前的业务之后会调用clicktcp_process_events
处理user_event队列,从而使得rs端连接继续进行。 -
对于TCPS2TCPS的0-RTT模式,
TLSV13S_C_WAIT_COMPUTED_EARLY_DATA_FIN
状态下必须等待RECORD LAYER的数据发送完,record layer和handshake layer的交互是交替进行。因为finished必须由handshake secret保护,而不是early secret。 - 无论是VS还是RS端的0-RTT数据都要注意secret的变化,因为序号变化了。
TLS1.3的KEY UPDATE流程
收到KEY UPDATE的时候,需要判断更新哪端的握手环境,是只需要更新对端还是同时需要更新本端?为了解决更新本端的问题,我添加了一个新状态TLSV13_S_PREPARE_KEY_UPDATE
,该状态负责清空本地的发送缓存,简单来说是让握手自动机停止运转,而record layer一直加密,直到加密完成。
TLS1.3的SESSION TICKET设计
RECORD LAYER和 HANDSHAKE LAYER的交互
OPENSSL实现自动机的时候没有将record layer和handshake layer分开,这样子的好处是简单,问题在于过于追求通用性,可读性和理解很差。而USTACK系统拆分了record layer和handshake layer好处是好理解,问题是因为是异步自动机,两个自动机怎么协调,包括状态的变化等等怎么做合适?需要分析下TLS的驱动
TLS1.3之前的版本
TLS的驱动来自两个方面,下层送上来数据和卡/soft context送过来数据(本质上是加解密操作)。record_in存储下层协议栈,record_out存储要发送的明文。
- 客户端先送来明文CLIENTHELLO,我们解析CLIENTHELLO,此时record_out必然为空,好,此时要送CLIENTHELLO报文进handshake layer。
- handshake layer拼凑好了,Server hello,Certificate,ServerKeyExchange,和ServerHelloDone,都放到了record_out里,等待客户端消息,但是此时都是明文。然后等待客户端明文。
- 客户端消息ClientKeyExchange还是明文,发送FINSIEHD并等待FINISHED
整个流程可见,到了发送应用数据的时候必然是先接受加密数据A0,从而有回复待加密数据B1,此时即使有已接受的加密数据A1,也得先把B1加密发出去。
TLS1.3版本数据
TLS1.3的驱动来自三个方面,下层送上来数据,卡/soft context送过来数据(本质上是加解密操作)与自动机自发的触发。record_in存储下层协议栈,record_out存储要发送的明文。
如果是完整的握手流程本身是必须本地算完才会有对端数据的,但是如果客户端发送了0-rtt数据和end_of_early_data数据。那么必然是record_in和record_out同时有数据,且需要先发record_out的明文,因此我们能得出record layer和handshake layer的交互必须满足下面几个条件
- 进入handshake layer之前必须进入record layer,record layer必然是handshake layer的下层。
- 每次进入record layer只有两种情况,等待某某消息加密完成并写到pcb里,或者从起始状态启动,等待明文/密文消息做加密/解密
- 如果同时由加密和解密要做,上一次做的是解密,那么这次就一定要做加密。这个地方要简单的解释解释,
TLS协议栈和卡的交互
- 这部分也值得好好说道说道,每个L4线程都会调用
ssl_card_poll_generic
,尝试判断卡是不是算完了数据。这里判断的方法为轮询每个atcp线程自己的入队列,校验任务是否完成,之所以使用轮询是因为中断会侵犯切换上下文,打扰性能。如果卡刚入栈ticks间隔小于2,那就直接返回。否则继续轮询队列。注意,这里我们需要区分异步队列和同步队列,同步队列运算非常快,异步队列运算非常慢。 - 每个卡在入队列和出队列的时候都是用的是?
内存方面
内存方面是一个需要注意的问题,slab管理器。
结尾的闲言碎语
写到这里差不多就可以结束了,就不多说了。