前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >深入理解nginx的https sni机制

深入理解nginx的https sni机制

作者头像
码农心语
发布2024-04-09 15:55:45
2640
发布2024-04-09 15:55:45
举报
文章被收录于专栏:码农心语码农心语

1. 概述

  SNI(Server Name Indication)是一种TLS(Transport Layer Security)协议的扩展,用于在建立加密连接时指定服务器的主机名。在使用单个IP地址和端口提供多个域名的服务时,SNI是非常有用的。  当客户端发起TLS握手时,它会发送一个包含所请求主机名的扩展,这样服务器就可以根据这个主机名选择合适的证书来完成握手。这使得服务器能够在同一IP地址和端口上为多个域名提供加密连接,而不需要为每个域名分配一个独立的IP地址。  对于HTTPS网站来说,SNI是至关重要的,因为它允许服务器在同一IP地址上为多个域名提供加密连接,不需要为每个域名单独部署一台服务器,从而降低了运维成本并提高了灵活性。  在使用SNI时,服务器端必须能够根据客户端发送的SNI信息来选择正确的证书进行握手。通常,服务器端配置会包含多个虚拟主机的证书信息,以便根据收到的SNI信息选择正确的证书来完成握手。  总的来说,SNI允许客户端在TLS握手期间指定所请求的主机名,从而使服务器能够根据主机名选择正确的证书,实现一个IP地址上多个域名的加密连接。

  本文基于nginx,对sni的实现原理进行深入的分析。

2. 初识sni

  有图有真相,先上一张抓包图,如下图:

 在ssl握手的第一个报文ClientHello中我们可以看到server_name的扩展信息,里面包含了当前请求的网站域名www.test.com。  当服务器收到这个报文后,将会解析出server_name扩展信息,这样子就可以在还没有收到客户端发送的HTTP报文前,即SSL握手阶段就提前知道了客户端需要访问的域名,服务器从而可以从容地在握手阶段选择绑定了该域名的SSL证书,来完成整个握手的过程。  需要强调一下的是,每个从CA申请下来的证书是会绑定域名的,SSL证书可以绑定一个或者多个域名,甚至是泛域名,这样子当浏览器在用https访问网站的时候,服务器会将配置的证书发送给浏览器,浏览器会根据拿到的证书进行检查,包括检查当前访问的域名是不是在证书中列出的域名列表中,如果不是的话,浏览器就会显示不安全网站的警告,甚至拒绝用户访问该网站。

3. nginx的ssl证书配置指令

  nginx和ssl相关的配置指令很多,但是和证书配置相关的指令主要包括ssl_certificate、ssl_certificate_key、 ssl_password_file这三个。下面分别来说明一下:

3.1 ssl_certificate

代码语言:javascript
复制
语  法:  ssl_certificate file;
默认值:  —
上下文:  http, server
代码语言:javascript
复制

  这个指令用于给一个虚拟主机配置一个PEM格式的证书文件,如果除了主证书外还需要指定证书链中的中间证书,它们应该按照以下顺序在同一个文件中指定:首先是主证书,然后是中间证书。一个以 PEM 格式的私钥也可以放在同一个文件中。  从nginx 1.10.0版本开始,可以配置多个ssl_certificate以便加载不同类型的证书,如RSA and ECDSA等。  从nginx 1.15.9版本开始,如果openssl版本大于等于1.0.2, 那么nginx可以支持证书文件名嵌入动态变量,这样子可以很将配置书写成下面的格式,如:

代码语言:javascript
复制
ssl_certificate     $ssl_server_name.crt;
ssl_certificate_key $ssl_server_name.key;
代码语言:javascript
复制

  从而能够自动在握手的时候加载用户请求所对应域名的证书文件,方便了运维配置工作,当然这样子配置可能有一定的性能上的损失。  nginx也支持直接将证书文件的内容用data:$variable的形式来设置,而这个variable的值可以用nginx插件来设置,这样子就完全不需要文件了,便于程序根据实际需要更加灵活第动态加载证书。

3.2 ssl_certificate_key

代码语言:javascript
复制
语  法:  ssl_certificate_key file;
默认值:  —
上下文:  http, server
代码语言:javascript
复制

  这个指令用于给一个虚拟主机配置证书文件对应的证书私钥,与ssl_certificate需要一一对应。同样支持文件名嵌入动态变量,和data:$variable方式加载证书,另外还支持engine:name:id格式的配置,用来让nginx从openssl的某个engine中获取指定id的证书私钥。

3.3 ssl_password_file

代码语言:javascript
复制
语  法:	ssl_password_file file;
默认值:	—
上下文:	http, server

   配置一个用于解密证书私钥的密码本文件,密码本文件每个密码一行,nginx依次尝试解密。当然,如果证书私钥没有加密,那么ssl_password_file是可以不进行配置的。

4. nginx源码分析

4.1 给ssl上下文的初始化

  ssl上下文的初始化是在ngx_http_ssl_merge_srv_conf函数中进行的,这个时候配置文件中的证书、密钥、加密文件的配置已经读取到了,本函数在这里执行main和server级别的配置的合并,然后就是创建ssl上下文,最后加载证书到ssl上下文了,源码如下:

代码语言:javascript
复制
if (ngx_ssl_ciphers(cf, &conf->ssl, &conf->ciphers,
                        conf->prefer_server_ciphers)
        != NGX_OK)
    {
        return NGX_CONF_ERROR;
    }

  /* 如果ssl_certificate和ssl_certificate_key配置的有动态变量,
     会进行变量的解析工作 */
    if (ngx_http_ssl_compile_certificates(cf, conf) != NGX_OK) {
        return NGX_CONF_ERROR;
    }

  /* conf->certificate_values不为NULL
     表示ssl_certificate和ssl_certificate_key配置含有动态变量  */
    if (conf->certificate_values) {

#ifdef SSL_R_CERT_CB_ERROR

        /* install callback to lookup certificates */
        /* 向ssl底层注册一个证书选择的回调函数 */
        SSL_CTX_set_cert_cb(conf->ssl.ctx, ngx_http_ssl_certificate, conf);

#else
        ngx_log_error(NGX_LOG_EMERG, cf->log, 0,
                      "variables in "
                      "\"ssl_certificate\" and \"ssl_certificate_key\" "
                      "directives are not supported on this platform");
        return NGX_CONF_ERROR;
#endif

    } else if (conf->certificates) {

        /* configure certificates */
        /* 配置的证书是静态文件且不包含动态变量,那么直接将证书加载到ssl上下文 */
        if (ngx_ssl_certificates(cf, &conf->ssl, conf->certificates,
                                 conf->certificate_keys, conf->passwords)
            != NGX_OK)
        {
            return NGX_CONF_ERROR;
        }
    }
......

  上面的代码会判断配置的证书是否静态文件,如果是静态文件则在这个阶段就直接将证书加载到ssl上下文中,因为这个阶段信息已经很清楚了,后续就不需要加载了;如果不是静态文件,那么这个阶段是没办法知道要加载的证书到底是什么内容的,要等到最终进行ssl握手的时候才能知晓,所以nginx通过SSL_CTX_set_cert_cb注册了一个回调函数ngx_http_ssl_certificate,最终在需要加载证书的时候就会回调这个函数来获取真正的证书内容。

  当然,还有一个需要关注的是下面的代码,这个代码也是在ngx_http_ssl_merge_srv_conf函数中执行的,源码如下:

代码语言:javascript
复制
#ifdef SSL_CTRL_SET_TLSEXT_HOSTNAME

    if (SSL_CTX_set_tlsext_servername_callback(conf->ssl.ctx,
                                               ngx_http_ssl_servername)
        == 0)
    {
        ngx_log_error(NGX_LOG_WARN, cf->log, 0,
            "nginx was built with SNI support, however, now it is linked "
            "dynamically to an OpenSSL library which has no tlsext support, "
            "therefore SNI is not available");
    }

#endif

  这段代码注册了sni的回调函数ngx_http_ssl_servername到ssl上下文中。

4.2 连接初始化

  在4.1节中所述的ssl上下文准备好以后,ssl连接当然是还没有建立的,只能说仍然只是停留在配置阶段,那么接下去可以想到客户端发起了tcp连接,nginx接受了这个连接,就需要开始对这个连接进行初始化,连接的初始化过程是由ngx_http_init_connection函数来完成的。那么如果开启了https,就会执行如下代码:

代码语言:javascript
复制
#if (NGX_HTTP_SSL)
    {
    ngx_http_ssl_srv_conf_t  *sscf;

    sscf = ngx_http_get_module_srv_conf(hc->conf_ctx, ngx_http_ssl_module);

    if (sscf->enable || hc->addr_conf->ssl) {
        hc->ssl = 1;
        c->log->action = "SSL handshaking";
        rev->handler = ngx_http_ssl_handshake;
    }
    }
#endif


  这段代码给当前连接的读事件设置了一个回调函数,即ngx_http_ssl_handshake函数,它用来进行ssl的握手操作。那么当nginx从这个连接上收到请求数据的时候就会开始执行ssl握手操作。在ngx_http_ssl_handshake函数中,有以下这段代码:

代码语言:javascript
复制
if (ngx_ssl_create_connection(&sscf->ssl, c, NGX_SSL_BUFFER)
    != NGX_OK)
  {
    ngx_http_close_connection(c);
    return;
  }

  这段代码用之前启动阶段准备好的ssl上下文和当前的socket连接来创建一个新的ssl连接,这样子就将当前的socket连接和ssl上下文关联起来了。后面就是真正的ssl握手操作了,在ngx_http_ssl_handshake代码里有:

代码语言:javascript
复制
 rc = ngx_ssl_handshake(c);

  在ngx_ssl_handshake函数里面会发起异步的ssl握手操作,这里略过。

4.3 处理sni回调

  在握手期间,ssl底层逻辑会解析ClientHello数据报文,发现有sni数据后,就回调前面设置好的ngx_http_ssl_servername函数了。下面来分析一下ngx_http_ssl_servername函数的实现:

代码语言:javascript
复制
int
ngx_http_ssl_servername(ngx_ssl_conn_t *ssl_conn, int *ad, void *arg)
{
#if defined(T_INGRESS_SHARED_MEMORY_PB) && OPENSSL_VERSION_NUMBER >= 0x10101000L
    return SSL_TLSEXT_ERR_OK;
#endif

    ngx_int_t                  rc;
    ngx_str_t                  host;
    const char                *servername;
    ngx_connection_t          *c;
    ngx_http_connection_t     *hc;
    ngx_http_ssl_srv_conf_t   *sscf;
    ngx_http_core_loc_conf_t  *clcf;
    ngx_http_core_srv_conf_t  *cscf;

    c = ngx_ssl_get_connection(ssl_conn);

    if (c->ssl->handshaked) {
        *ad = SSL_AD_NO_RENEGOTIATION;
        return SSL_TLSEXT_ERR_ALERT_FATAL;
    }

    hc = c->data;

  /* 通过ssl连接获取到ClientHello握手信息中的sni域名 */
    servername = SSL_get_servername(ssl_conn, TLSEXT_NAMETYPE_host_name);

    if (servername == NULL) {
        ngx_log_debug0(NGX_LOG_DEBUG_HTTP, c->log, 0,
                       "SSL server name: null");
        goto done;
    }

    ngx_log_debug1(NGX_LOG_DEBUG_HTTP, c->log, 0,
                   "SSL server name: \"%s\"", servername);

    host.len = ngx_strlen(servername);

    if (host.len == 0) {
        goto done;
    }

    host.data = (u_char *) servername;

    rc = ngx_http_validate_host(&host, c->pool, 1);

    if (rc == NGX_ERROR) {
        goto error;
    }

    if (rc == NGX_DECLINED) {
        goto done;
    }

  /* 根据请求的sni域名查找对应的是哪个nginx中配置的server,
     并得到nginx http核心模块的srv配置 */
    rc = ngx_http_find_virtual_server(c, hc->addr_conf->virtual_names, &host,
                                      NULL, &cscf);

    if (rc == NGX_ERROR) {
        goto error;
    }

    if (rc == NGX_DECLINED) {
        goto done;
    }

    hc->ssl_servername = ngx_palloc(c->pool, sizeof(ngx_str_t));
    if (hc->ssl_servername == NULL) {
        goto error;
    }

    *hc->ssl_servername = host;

    hc->conf_ctx = cscf->ctx;

    clcf = ngx_http_get_module_loc_conf(hc->conf_ctx, ngx_http_core_module);

    ngx_set_connection_log(c, clcf->error_log);

    sscf = ngx_http_get_module_srv_conf(hc->conf_ctx, ngx_http_ssl_module);

    c->ssl->buffer_size = sscf->buffer_size;

    if (sscf->ssl.ctx) {
      /* 将当前ssl连接的切换成sni域名对应的ssl上下文 */
        if (SSL_set_SSL_CTX(ssl_conn, sscf->ssl.ctx) == NULL) {
            goto error;
        }

        /*
         * SSL_set_SSL_CTX() only changes certs as of 1.0.0d
         * adjust other things we care about
         */

        SSL_set_verify(ssl_conn, SSL_CTX_get_verify_mode(sscf->ssl.ctx),
                       SSL_CTX_get_verify_callback(sscf->ssl.ctx));

        SSL_set_verify_depth(ssl_conn, SSL_CTX_get_verify_depth(sscf->ssl.ctx));

#if OPENSSL_VERSION_NUMBER >= 0x009080dfL
        /* only in 0.9.8m+ */
        SSL_clear_options(ssl_conn, SSL_get_options(ssl_conn) &
                                    ~SSL_CTX_get_options(sscf->ssl.ctx));
#endif

        SSL_set_options(ssl_conn, SSL_CTX_get_options(sscf->ssl.ctx));

#ifdef SSL_OP_NO_RENEGOTIATION
        SSL_set_options(ssl_conn, SSL_OP_NO_RENEGOTIATION);
#endif
    }

done:

    sscf = ngx_http_get_module_srv_conf(hc->conf_ctx, ngx_http_ssl_module);

    if (sscf->reject_handshake) {
        c->ssl->handshake_rejected = 1;
        *ad = SSL_AD_UNRECOGNIZED_NAME;
        return SSL_TLSEXT_ERR_ALERT_FATAL;
    }

    return SSL_TLSEXT_ERR_OK;

error:

    *ad = SSL_AD_INTERNAL_ERROR;
    return SSL_TLSEXT_ERR_ALERT_FATAL;
}

  代码的主要逻辑就是从ssl连接中获取sni域名,根据sni域名查找nginx配置的server,得到该server的nginx http 核心模块的配置,在这个配置里面有前面加载的ssl上下文,然后就可以将当前ssl连接切换到对应的ssl上下文,最后执行后续的握手工作。  握手后续的工作就是发送配置的证书文件,加密握手等工作,不再赘述。对于sni来说,整个过程应该是已经结束了,但是我们还要关心证书的加载问题,对于配置文件中配置的静态文件证书,那么很简单,ssl上下文中已经加载了证书,后面就不需要再加载了;而对于动态的证书,那么就需要进行证书的加载工作。证书加载就轮到前面设置的回调函数ngx_http_ssl_certificate来工作了。

4.4 动态证书的加载

&emsp 动态证书的加载工作是由ngx_http_ssl_certificate来完成的,其源码如下:

代码语言:javascript
复制
int
ngx_http_ssl_certificate(ngx_ssl_conn_t *ssl_conn, void *arg)
{
    ngx_str_t                  cert, key;
    ngx_uint_t                 i, nelts;
    ngx_connection_t          *c;
    ngx_http_request_t        *r;
    ngx_http_ssl_srv_conf_t   *sscf;
    ngx_http_complex_value_t  *certs, *keys;

    c = ngx_ssl_get_connection(ssl_conn);

    if (c->ssl->handshaked) {
        return 0;
    }

  /* 这里分配一个临时的ngx_http_request_t, 因为后面解析动态证书的时候有用到动态变量,
     获取动态变量是需要以一个ngx_http_request_t作为上下文的 */
    r = ngx_http_alloc_request(c);
    if (r == NULL) {
        return 0;
    }

    r->logged = 1;

    sscf = arg;

    nelts = sscf->certificate_values->nelts;
    certs = sscf->certificate_values->elts;
    keys = sscf->certificate_key_values->elts;

    for (i = 0; i < nelts; i++) {
    
    /* 将配置的证书中的动态变量进行求值,获取真实的配置信息 */
        if (ngx_http_complex_value(r, &certs[i], &cert) != NGX_OK) {
            goto failed;
        }

        ngx_log_debug1(NGX_LOG_DEBUG_HTTP, c->log, 0,
                       "ssl cert: \"%s\"", cert.data);

    /* 将配置的证书私钥中的动态变量进行求值,获取真实的配置信息 */
        if (ngx_http_complex_value(r, &keys[i], &key) != NGX_OK) {
            goto failed;
        }

        ngx_log_debug1(NGX_LOG_DEBUG_HTTP, c->log, 0,
                       "ssl key: \"%s\"", key.data);

    /* 加载证书 */
        if (ngx_ssl_connection_certificate(c, r->pool, &cert, &key,
                                           sscf->passwords)
            != NGX_OK)
        {
            goto failed;
        }
    }

    ngx_http_free_request(r, 0);
    c->log->action = "SSL handshaking";
    c->destroyed = 0;
    return 1;

failed:

    ngx_http_free_request(r, 0);
    c->log->action = "SSL handshaking";
    c->destroyed = 0;
    return 0;
}

  代码的主要逻辑就是将配置中的变量进行求值操作,从而得到完整的配置内容,然后调用ngx_ssl_connection_certificate进行证书的加载。由于一个nginx server虚拟主机可以配置多个不同加密算法的证书,所以需要遍历加载多个证书,因此在代码中有一个循环逻辑。

  下面来看看ngx_ssl_connection_certificate是如何来加载动态证书的,其源码如下:

代码语言:javascript
复制
ngx_int_t
ngx_ssl_connection_certificate(ngx_connection_t *c, ngx_pool_t *pool,
    ngx_str_t *cert, ngx_str_t *key, ngx_array_t *passwords)
{
    char            *err;
    X509            *x509;
    EVP_PKEY        *pkey;
    STACK_OF(X509)  *chain;

  /* 加载pem证书 */
    x509 = ngx_ssl_load_certificate(pool, &err, cert, &chain);
    if (x509 == NULL) {
        if (err != NULL) {
            ngx_ssl_error(NGX_LOG_ERR, c->log, 0,
                          "cannot load certificate \"%s\": %s",
                          cert->data, err);
        }

        return NGX_ERROR;
    }
    
  /* 将加载的pem证书配置到当前的ssl连接中 */
    if (SSL_use_certificate(c->ssl->connection, x509) == 0) {
        ngx_ssl_error(NGX_LOG_ERR, c->log, 0,
                      "SSL_use_certificate(\"%s\") failed", cert->data);
        X509_free(x509);
        sk_X509_pop_free(chain, X509_free);
        return NGX_ERROR;
    }

    X509_free(x509);

#ifdef SSL_set0_chain

    /*
     * SSL_set0_chain() is only available in OpenSSL 1.0.2+,
     * but this function is only called via certificate callback,
     * which is only available in OpenSSL 1.0.2+ as well
     */

    if (SSL_set0_chain(c->ssl->connection, chain) == 0) {
        ngx_ssl_error(NGX_LOG_ERR, c->log, 0,
                      "SSL_set0_chain(\"%s\") failed", cert->data);
        sk_X509_pop_free(chain, X509_free);
        return NGX_ERROR;
    }

#endif
  /* 加载pem证书私钥 */
    pkey = ngx_ssl_load_certificate_key(pool, &err, key, passwords);
    if (pkey == NULL) {
        if (err != NULL) {
            ngx_ssl_error(NGX_LOG_ERR, c->log, 0,
                          "cannot load certificate key \"%s\": %s",
                          key->data, err);
        }

        return NGX_ERROR;
    }
    
  /* 将加载的pem证书私钥配置到当前的ssl连接中 */
    if (SSL_use_PrivateKey(c->ssl->connection, pkey) == 0) {
        ngx_ssl_error(NGX_LOG_ERR, c->log, 0,
                      "SSL_use_PrivateKey(\"%s\") failed", key->data);
        EVP_PKEY_free(pkey);
        return NGX_ERROR;
    }

    EVP_PKEY_free(pkey);

    return NGX_OK;
}

  其主要分为两个步骤,一个是加载证书到ssl连接中,一个是加载私钥连接中。最后就是看看关键的ngx_ssl_load_certificate和ngx_ssl_load_certificate_key两个函数的实现逻辑了:

代码语言:javascript
复制
static X509 *
ngx_ssl_load_certificate(ngx_pool_t *pool, char **err, ngx_str_t *cert,
    STACK_OF(X509) **chain)
{
    BIO     *bio;
    X509    *x509, *temp;
    u_long   n;

    if (ngx_strncmp(cert->data, "data:", sizeof("data:") - 1) == 0) {
    /* 对于data:形式的配置,直接取cert->data中的内容到bio */
    
        bio = BIO_new_mem_buf(cert->data + sizeof("data:") - 1,
                              cert->len - (sizeof("data:") - 1));
        if (bio == NULL) {
            *err = "BIO_new_mem_buf() failed";
            return NULL;
        }

    } else {

        if (ngx_get_full_name(pool, (ngx_str_t *) &ngx_cycle->conf_prefix, cert)
            != NGX_OK)
        {
            *err = NULL;
            return NULL;
        }

    /* 对于物理文件,则从文件中读取内容到bio */
        bio = BIO_new_file((char *) cert->data, "r");
        if (bio == NULL) {
            *err = "BIO_new_file() failed";
            return NULL;
        }
    }

    /* certificate itself */
  /* 从bio读取第一个证书生成证书对象  */
    x509 = PEM_read_bio_X509_AUX(bio, NULL, NULL, NULL);
    if (x509 == NULL) {
        *err = "PEM_read_bio_X509_AUX() failed";
        BIO_free(bio);
        return NULL;
    }

    /* rest of the chain */
  /* 下面是证书链的处理,将主证书后面的中间证书加载到证书链中 */
    *chain = sk_X509_new_null();
    if (*chain == NULL) {
        *err = "sk_X509_new_null() failed";
        BIO_free(bio);
        X509_free(x509);
        return NULL;
    }

    for ( ;; ) {
    /* 读取下一个证书 */
        temp = PEM_read_bio_X509(bio, NULL, NULL, NULL);
        if (temp == NULL) {
            n = ERR_peek_last_error();

            if (ERR_GET_LIB(n) == ERR_LIB_PEM
                && ERR_GET_REASON(n) == PEM_R_NO_START_LINE)
            {
                /* end of file */
                ERR_clear_error();
                break;
            }

            /* some real error */

            *err = "PEM_read_bio_X509() failed";
            BIO_free(bio);
            X509_free(x509);
            sk_X509_pop_free(*chain, X509_free);
            return NULL;
        }

    /* 将证书添加到证书链中 */
        if (sk_X509_push(*chain, temp) == 0) {
            *err = "sk_X509_push() failed";
            BIO_free(bio);
            X509_free(x509);
            sk_X509_pop_free(*chain, X509_free);
            return NULL;
        }
    }

    BIO_free(bio);

    return x509;
}

代码语言:javascript
复制
static EVP_PKEY *
ngx_ssl_load_certificate_key(ngx_pool_t *pool, char **err,
    ngx_str_t *key, ngx_array_t *passwords)
{
    BIO              *bio;
    EVP_PKEY         *pkey;
    ngx_str_t        *pwd;
    ngx_uint_t        tries;
    pem_password_cb  *cb;

    if (ngx_strncmp(key->data, "engine:", sizeof("engine:") - 1) == 0) {
    /* 如果是engine类型的配置,则从openssl的指定engine中获取证书私钥 */
#ifndef OPENSSL_NO_ENGINE

        u_char  *p, *last;
        ENGINE  *engine;

        p = key->data + sizeof("engine:") - 1;
        last = (u_char *) ngx_strchr(p, ':');

        if (last == NULL) {
            *err = "invalid syntax";
            return NULL;
        }

        *last = '\0';

        engine = ENGINE_by_id((char *) p);

        if (engine == NULL) {
            *err = "ENGINE_by_id() failed";
            return NULL;
        }

        *last++ = ':';

        pkey = ENGINE_load_private_key(engine, (char *) last, 0, 0);

        if (pkey == NULL) {
            *err = "ENGINE_load_private_key() failed";
            ENGINE_free(engine);
            return NULL;
        }

        ENGINE_free(engine);

        return pkey;

#else

        *err = "loading \"engine:...\" certificate keys is not supported";
        return NULL;

#endif
    }

    if (ngx_strncmp(key->data, "data:", sizeof("data:") - 1) == 0) {

    /* 对于data:形式的配置,直接取key->data中的内容到bio */
        bio = BIO_new_mem_buf(key->data + sizeof("data:") - 1,
                              key->len - (sizeof("data:") - 1));
        if (bio == NULL) {
            *err = "BIO_new_mem_buf() failed";
            return NULL;
        }

    } else {

        if (ngx_get_full_name(pool, (ngx_str_t *) &ngx_cycle->conf_prefix, key)
            != NGX_OK)
        {
            *err = NULL;
            return NULL;
        }
    /* 对于文件形式的配置,从物理文件读取证书私钥内容到bio */
        bio = BIO_new_file((char *) key->data, "r");
        if (bio == NULL) {
            *err = "BIO_new_file() failed";
            return NULL;
        }
    }

    if (passwords) {
        tries = passwords->nelts;
        pwd = passwords->elts;
        cb = ngx_ssl_password_callback;

    } else {
        tries = 1;
        pwd = NULL;
        cb = NULL;
    }

  /* 读取私钥信息,直到第一个成功为止 */
    for ( ;; ) {
    
        pkey = PEM_read_bio_PrivateKey(bio, NULL, cb, pwd);
        if (pkey != NULL) {
            break;
        }

        if (tries-- > 1) {
            ERR_clear_error();
            (void) BIO_reset(bio);
            pwd++;
            continue;
        }

        *err = "PEM_read_bio_PrivateKey() failed";
        BIO_free(bio);
        return NULL;
    }

    BIO_free(bio);

    return pkey;

}

   nginx支持三种方式的证书私钥,一种是engine类型的,一种是data类型的,最后一种是文件类型的,以上代码就是分别处理这三种类型的证书私钥进行加载。  由于即使有证书链存在,但是主证书也永远只有一个,因此,证书私钥也只要一个就可以了,没有证书链这种概念,所以这里只要按序加载一个可用的证书私钥(如果存在多个的话)即可。

  经过以上环节,整个sni的过程,包括证书的加载和选择的流程都已经明确了。

5. 总结

  本文从ssl上下文的初始化、ssl连接的初始化、sni回调处理,到最后动态证书加载的整个流程详细说明了nginx sni的实现过程,nginx的实现逻辑清晰,简单明了,对我们未来自己去实现支持ssl连接请求的服务器有非常好的借鉴意义。

本文参与 腾讯云自媒体分享计划,分享自微信公众号。
原始发表:2024-02-29,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 码农心语 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体分享计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 1. 概述
相关产品与服务
轻量应用服务器
轻量应用服务器(TencentCloud Lighthouse)是新一代开箱即用、面向轻量应用场景的云服务器产品,助力中小企业和开发者便捷高效的在云端构建网站、Web应用、小程序/小游戏、游戏服、电商应用、云盘/图床和开发测试环境,相比普通云服务器更加简单易用且更贴近应用,以套餐形式整体售卖云资源并提供高带宽流量包,将热门软件打包实现一键构建应用,提供极简上云体验。
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档