TLS1.3 OPENSSL 的 EARLY DATA 处理流程

2019-01-26 01:53:25 +08:00
 hxndg

OPENSSL 的代码写的很乱,流程不是十分清楚,TLS1.3 的 EARLY DATA 又是一个穿插错乱的流程,所以写个东西来简单介绍。针对的代码是 S_SERVER 的流程。

如果启动 S_SERVER 时,支持 early_data,那么 OPENSSL 会调用 SSL_read_early_data 函数:

int SSL_read_early_data(SSL *s, void *buf, size_t num, size_t *readbytes)
{
    int ret;

    if (!s->server) {
        SSLerr(SSL_F_SSL_READ_EARLY_DATA, ERR_R_SHOULD_NOT_HAVE_BEEN_CALLED);
        return SSL_READ_EARLY_DATA_ERROR;
    }

    switch (s->early_data_state) {
    case SSL_EARLY_DATA_NONE:
        if (!SSL_in_before(s)) {
            SSLerr(SSL_F_SSL_READ_EARLY_DATA,
                   ERR_R_SHOULD_NOT_HAVE_BEEN_CALLED);
            return SSL_READ_EARLY_DATA_ERROR;
        }
        /* fall through */

    case SSL_EARLY_DATA_ACCEPT_RETRY:
        s->early_data_state = SSL_EARLY_DATA_ACCEPTING;
        ret = SSL_accept(s);
        if (ret <= 0) {
            /* NBIO or error */
            s->early_data_state = SSL_EARLY_DATA_ACCEPT_RETRY;
            return SSL_READ_EARLY_DATA_ERROR;
        }
        /* fall through */

    case SSL_EARLY_DATA_READ_RETRY:
        if (s->ext.early_data == SSL_EARLY_DATA_ACCEPTED) {
            s->early_data_state = SSL_EARLY_DATA_READING;
            ret = SSL_read_ex(s, buf, num, readbytes);
            /*
             * State machine will update early_data_state to
             * SSL_EARLY_DATA_FINISHED_READING if we get an EndOfEarlyData
             * message
             */

很有趣,openssl 先是在 SSL_read_early_data 中调用了 SSL_accept 函数,这个函数本质上就是 state_machine,也就是 read_state_machine 和 write_state_machine,read_state_machine 负责处理读取到的 handshake 握手消息,比方说 client_hello。server 转完了 client_hello 的分析过程,并且把参数都保存下来就转入 write_state_machine,write_state_machine 要做的事情比较多,write_state_machine 分别要 construct_server_hello,construct_change_spec(处于兼容性),construct_encrypted_extension。并且调用 ssl->method->do_write 函数,这个函数模板真正指向 ssl3_do_write 函数。这一大段过程就是 SSL_accept 的过程。也就是说 SSL_accept 函数就是服务端的整个握手流程,请参看 TLS1.3 RFC 的 SERVER STATE MACHINE 流程图 visit https://tools.ietf.org/html/rfc8446#page-121

SSL_accept 握手完成之后,服务端调用 SSL_read_ex(s, buf, num, readbytes)函数读取 early_data 的内容,SSL_read_ex 内部包裹 ssl_read_internal 函数,ssl_read_internal 包裹 s->method->ssl_read,这个函数指针就是 ssl3_read,包裹 ssl3_read_internal 函数,而 ssl3_read_internal 又包裹了 s->method->ssl_read_bytes 也就是 ssl3_read_bytes 函数

int ssl3_read_bytes(SSL *s, int type, int *recvd_type, unsigned char *buf,
                    size_t len, int peek, size_t *readbytes)

根据 type 的类型读取 payload,存储长度到 len,只有三种 type:SSL3_RT_HANDSHAKE (when ssl3_get_message calls us),SSL3_RT_APPLICATION_DATA (when ssl3_read calls us),0 (during a shutdown, no data has to be returned)。也就是说这个函数只负责读取,更多的内容会由上层负责。值得注意的是,为了能在本层分析报文的 payload,也就是报文的种类( alert,handshake protocol ),该函数会把 payload 的头四个字节存储到 s->rlayer.handshake_fragment 中,然后根据预先假定的 type 类型来分析读到的报文。

回到刚才的场景,SSL_read_ex 的报文类型是 SSL3_RT_APPLICATION_DATA,下面的代码时调用 SSL3_read_bytes 的函数:

static int ssl3_read_internal(SSL *s, void *buf, size_t len, int peek,
                              size_t *readbytes)
{
    int ret;

    clear_sys_error();
    if (s->s3->renegotiate)
        ssl3_renegotiate_check(s, 0);
    s->s3->in_read_app_data = 1;
    ret =
        s->method->ssl_read_bytes(s, SSL3_RT_APPLICATION_DATA, NULL, buf, len,
                                  peek, readbytes);
                                  ...

ssl3_read_bytes 函数会不断的读取 payload 到传进去的参数 buf 中,如果这个过程读不到数据,该函数会再次尝试读取出来 pay_load,下面代码在 SSL3_read_bytes 内部:

 do {
            if (len - totalbytes > SSL3_RECORD_get_length(rr))
                n = SSL3_RECORD_get_length(rr);
            else
                n = len - totalbytes;

            memcpy(buf, &(rr->data[rr->off]), n);
            buf += n;
            if (peek) {
                /* Mark any zero length record as consumed CVE-2016-6305 */
                if (SSL3_RECORD_get_length(rr) == 0)
                    SSL3_RECORD_set_read(rr);
            } else {
                SSL3_RECORD_sub_length(rr, n);
                SSL3_RECORD_add_off(rr, n);
                if (SSL3_RECORD_get_length(rr) == 0) {
                    s->rlayer.rstate = SSL_ST_READ_HEADER;
                    SSL3_RECORD_set_off(rr, 0);
                    SSL3_RECORD_set_read(rr);
                }
            }
            if (SSL3_RECORD_get_length(rr) == 0
                || (peek && n == SSL3_RECORD_get_length(rr))) {
                curr_rec++;
                rr++;
            }
            totalbytes += n;
        } while (type == SSL3_RT_APPLICATION_DATA && curr_rec < num_recs
                 && totalbytes < len);
        if (totalbytes == 0) {
            /* We must have read empty records. Get more data */
            goto start;
        }
        if (!peek && curr_rec == num_recs
            && (s->mode & SSL_MODE_RELEASE_BUFFERS)
            && SSL3_BUFFER_get_left(rbuf) == 0)
            ssl3_release_read_buffer(s);
        *readbytes = totalbytes;
        return 1;

如果服务端收到一个 SSL3_RT_APPLICATION_DATA,该消息可能史哥 ALERT 消息,或者 HANDSHAKE 消息,如果是 ALERT 消息,ssl3_read_bytes 会利用 PACKET_get_1 来获取具体的 alert number,alert content。非当服务端收到 End of Early Data 报文时,该报文类型为 SSL3_RT_HANDSHAKE 而不是 SSL3_RT_APPLICATION_DATA 报文,说明我们收到了一个握手报文。服务端会尝试收取到足够长度的握手报文,并在此进入握手函数 s->handshake_func 恢复握手过程,下面的代码在 SSL3_read_bytes 内部。

 if ((s->rlayer.handshake_fragment_len >= 4)
            && !ossl_statem_get_in_handshake(s)) {
        int ined = (s->early_data_state == SSL_EARLY_DATA_READING);

        /* We found handshake data, so we're going back into init */
        ossl_statem_set_in_init(s, 1);

        i = s->handshake_func(s);
        /* SSLfatal() already called if appropriate */
        if (i < 0)
            return i;
        if (i == 0) {
            return -1;
        }

进入 handshake_func 也就是 state_machine 的后续流程不再赘述,参照流程图即可。

握手的函数流程,和一些函数的 API 写不写看心情吧,加上一些链接可以用来学习。 https://www.cnblogs.com/hrhguanli/p/3834585.html https://blog.csdn.net/cwg2552298/article/details/83045448 https://security.stackexchange.com/questions/55667/tls-sequence-number https://tools.ietf.org/html/rfc8446

3546 次点击
所在节点    SSL
2 条回复
isCyan
2019-01-29 23:12:36 +08:00
可能是干货,虽然看不懂,先挽尊
hxndg
2019-01-30 11:01:40 +08:00
@isCyan
简单的代码分析而已,主要的新东西都在 1.3 的 rfc 里面。

这是一个专为移动设备优化的页面(即为了让你能够在 Google 搜索结果里秒开这个页面),如果你希望参与 V2EX 社区的讨论,你可以继续到 V2EX 上打开本讨论主题的完整版本。

https://www.v2ex.com/t/530741

V2EX 是创意工作者们的社区,是一个分享自己正在做的有趣事物、交流想法,可以遇见新朋友甚至新机会的地方。

V2EX is a community of developers, designers and creative people.

© 2021 V2EX