OPENSSL源码阅读

从S_SERVER说起

Posted by 大狗 on September 4, 2020

OPENSSL源码阅读

前言

我最近有点烦,想找点副业不知道干啥,化悲愤为思考,写点文章吧。一般人阅读OPENSSL的源码是先说有几个文件,每个文件干啥。我打算从S_SERVER文件说起,因为做实验的时候一般都是自己搭建TLS 服务器。这次不会细致到每个点都扣,不值得也没有必要,读书不求甚解,至于精微之处则慢慢品味。

从S_SERVER说起

openssl自带一个s_server服务器,可以完成NST颁发,计算PSK,握手的每个计算。如果想从代码层面研究TLS1.3的握手流程,那么openssl就很适合。当然openssl的代码写的非常直接,我司几个人看的时候都把openssl骂了一顿。学习学习倒是极好的。

前置知识

s_server的使用方法大致如下,openssl s_server -accept “port” -key “key_file” -cert “cert_file” -ciphersuite “xxx” -tls1_2,所有双引号的内容都是自己制定的。然后带”-“的都是选项。

源码阅读

打开s_server.c文件,主函数是s_server_main.c。s_server_main函数刚打开实际上是一大堆状态,CTX等用于环境的基本设置,就不多介绍了。唯一需要注意的是

	const SSL_METHOD *meth = TLS_server_method();
	...
	int min_version = 0, max_version = 0, prot_opt = 0, no_prot_opt = 0;

meth就是server端服务的主程序,而min_version和max_version本质上对应于服务端支持的版本范围。此外还有几个全局变量要注意:stateless全局变量,用于标识服务端完全不存储任何客户端信息,这里针对的是办法TICKET IDENTITY

	case OPT_STATELESS:
            stateless = 1;
            break;

s_server_main首先分配SSL设置环境(SSL configuration context),接着验证参数的有效性,之后再给SSL设置环境打上服务端和命令行模式标签。标签打上以后就开始根据参数设置各种变量。下面的代码就设置了服务端支持的版本范围,比方说加了标签”TLS1_2”,那么就设置最大版本和最小版本都是TLS1.2。

	cctx = SSL_CONF_CTX_new();
    vpm = X509_VERIFY_PARAM_new();
    if (cctx == NULL || vpm == NULL)
        goto end;
    SSL_CONF_CTX_set_flags(cctx,
                           SSL_CONF_FLAG_SERVER | SSL_CONF_FLAG_CMDLINE);

    prog = opt_init(argc, argv, s_server_options);
    while ((o = opt_next()) != OPT_EOF) {
        if (IS_PROT_FLAG(o) && ++prot_opt > 1) {
            BIO_printf(bio_err, "Cannot supply multiple protocol flags\n");
            goto end;
        }
	...
	case OPT_SSL3:
            min_version = SSL3_VERSION;
            max_version = SSL3_VERSION;
            break;
        case OPT_TLS1_3:
            min_version = TLS1_3_VERSION;
            max_version = TLS1_3_VERSION;
            break;
        case OPT_TLS1_2:
            min_version = TLS1_2_VERSION;
            max_version = TLS1_2_VERSION;
            break;
        case OPT_TLS1_1:
            min_version = TLS1_1_VERSION;
            max_version = TLS1_1_VERSION;
            break;
        case OPT_TLS1:
            min_version = TLS1_VERSION;
            max_version = TLS1_VERSION;

上面的初始化代码初始化了支持的版本,是否支持max_early_data,最大接受early data的字节之后。函数首先载入证书和私钥.如果有证书链文件再载入证书链文件。

	if (nocert == 0) {
        s_key = load_key(s_key_file, s_key_format, 0, pass, engine,
                         "server certificate private key file");
        if (s_key == NULL)
            goto end;

        s_cert = load_cert(s_cert_file, s_cert_format,
                           "server certificate file");

        if (s_cert == NULL)
            goto end;
        if (s_chain_file != NULL) {
            if (!load_certs(s_chain_file, &s_chain, FORMAT_PEM, NULL,
                            "server certificate chain"))
                goto end;
        }

之后载入crl文件,也就是判断证书是否有效的文件。

	 if (crl_file != NULL) {
        X509_CRL *crl;
        crl = load_crl(crl_file, crl_format, "CRL");
        if (crl == NULL)
            goto end;
        crls = sk_X509_CRL_new_null();
        if (crls == NULL || !sk_X509_CRL_push(crls, crl)) {
            BIO_puts(bio_err, "Error adding CRL\n");
            ERR_print_errors(bio_err);
            X509_CRL_free(crl);
            goto end;
        }
    }

基本的配置都载入完成了,就可以初始化CTX了。这里需要注意的是:ctx和ctx2都是静态全局变量,下面的代码初始化CTX并使用CCTX来初始化CTX,同时设置CTX的各种属性,比方说最大支持版本,最小支持版本,设置是否异步(async),设置支持early data,支持接受的最大的early data的长度。

	ctx = SSL_CTX_new(meth);
    if (ctx == NULL) {
        ERR_print_errors(bio_err);
        goto end;
    }

    SSL_CTX_clear_mode(ctx, SSL_MODE_AUTO_RETRY);

    if (sdebug)
        ssl_ctx_security_debug(ctx, sdebug);

    if (!config_ctx(cctx, ssl_args, ctx))
        goto end;

    if (ssl_config) {
        if (SSL_CTX_config(ctx, ssl_config) == 0) {
            BIO_printf(bio_err, "Error using configuration \"%s\"\n",
                       ssl_config);
            ERR_print_errors(bio_err);
            goto end;
        }
    }

#ifndef OPENSSL_NO_SCTP
    if (protocol == IPPROTO_SCTP && sctp_label_bug == 1)
        SSL_CTX_set_mode(ctx, SSL_MODE_DTLS_SCTP_LABEL_LENGTH_BUG);
#endif

    if (min_version != 0
        && SSL_CTX_set_min_proto_version(ctx, min_version) == 0)
        goto end;
    if (max_version != 0
        && SSL_CTX_set_max_proto_version(ctx, max_version) == 0)
        goto end;

最后进入了服务器搭建的关键部分了。根据协议,设置服务器的主函数,这实际上是个回调函数。do_server本质就是绑定服务器到一个地址。注意,这里实现了写代码的时候一个很关键的地方,分层的想法。面向四层的操作交给一个四层的函数去做,面向四层一下的操作给四层以下的去做,这种想法很有用。因为我们只关注TLS层,所以就不对四层以下的多做解释了。有兴趣可以自己看。解析来函数跳转到sv_body,sv_body本身就是提供TLS服务的关键,里面包含的是一个自动机。下一节我们再细说。

	 if (rev)
        server_cb = rev_body;
    else if (www)
        server_cb = www_body;
    else
        server_cb = sv_body;
#ifdef AF_UNIX
    if (socket_family == AF_UNIX
        && unlink_unix_path)
        unlink(host);
#endif
    do_server(&accept_socket, host, port, socket_family, socket_type, protocol,
              server_cb, context, naccept, bio_s_out);
    print_stats(bio_s_out, ctx);
    ret = 0;

结束

我个人很反感这段代码,配置这块我觉得最好单独拆一个函数出来,然后里面不同的部分分开。现在这样子看的太费劲了。

结尾的闲言碎语

写到这里差不多就可以结束了,实际上Session Identity还经常需要添加ALPN信息,就不多说了。TLS这块还有啥不明白的直接告诉我就成了 狗头的赞赏码.jpg