Repository: nginx/nginx
File: src/event/ngx_event_openssl.c (C)

Code before:
    if (X509_set_ex_data(x509, ngx_ssl_certificate_name_index, cert->data)
        == 0)
    {
        ngx_ssl_error(NGX_LOG_EMERG, ssl->log, 0, "X509_set_ex_data() failed");
        X509_free(x509);
        sk_X509_pop_free(chain, X509_free);
        return NGX_ERROR;
    }

    if (ssl->certs.elts == NULL) {
        if (ngx_array_init(&ssl->certs, cf->pool, 1, sizeof(X509 *))
            != NGX_OK)
        {
            X509_free(x509);
            sk_X509_pop_free(chain, X509_free);
            return NGX_ERROR;
        }
    }

    /* track replacing certificates of the same type */

    id = EVP_PKEY_id(X509_get0_pubkey(x509));
    elm = ssl->certs.elts;

    for (k = 0; k < ssl->certs.nelts; k++, elm++) {
        if (EVP_PKEY_id(X509_get0_pubkey(*elm)) == id) {
            X509_free(*elm);
            break;
        }
    }

    if (k == ssl->certs.nelts) {
        elm = ngx_array_push(&ssl->certs);
        if (elm == NULL) {
            X509_free(x509);
            sk_X509_pop_free(chain, X509_free);
            return NGX_ERROR;
        }
    }

    *elm = x509;

    /*
     * Note that x509 is not freed here, but will be instead freed in
     * ngx_ssl_cleanup_ctx().  This is because we need to preserve all
     * certificates to be able to iterate all of them through ssl->certs,
     * while OpenSSL can free a certificate if it is replaced with another
     * certificate of the same type.
     */

#ifdef SSL_CTX_set0_chain

Review comment (suggestion):
Id-based deduplication does not seem safe, because that's not a sufficient approximation of the OpenSSL internals.

OpenSSL 3.x supports multiple provider-loaded private key types, registered during the `SSL_CTX` initialization. For any of those we will get `EVP_PKEY_KEYMGMT`, but internally the certs will occupy different slots on the `ctx->cert` (as determined in the second loop of `ssl_cert_lookup_by_pkey`).

Another thing to keep in mind is that we may get a different key type on retry. Let's say, a pair of RSA keys get overwritten with ECDSA ones. In this case we will keep the already discarded RSA certificate in `ssl->certs`.

Code after:
    if (X509_set_ex_data(x509, ngx_ssl_certificate_name_index, cert->data)
        == 0)
    {
        ngx_ssl_error(NGX_LOG_EMERG, ssl->log, 0, "X509_set_ex_data() failed");
        X509_free(x509);
        sk_X509_pop_free(chain, X509_free);
        return NGX_ERROR;
    }

    if (ssl->certs.elts == NULL) {
        if (ngx_array_init(&ssl->certs, cf->pool, 1, sizeof(X509 *))
            != NGX_OK)
        {
            X509_free(x509);
            sk_X509_pop_free(chain, X509_free);
            return NGX_ERROR;
        }
    }

    if (elm == NULL) {
        elm = ngx_array_push(&ssl->certs);
        if (elm == NULL) {
            X509_free(x509);
            sk_X509_pop_free(chain, X509_free);
            return NGX_ERROR;
        }

    } else {
        X509_free(*elm);
    }

    *elm = x509;

    /*
     * Note that x509 is not freed here, but will be instead freed in
     * ngx_ssl_cleanup_ctx().  This is because we need to preserve all
     * certificates to be able to iterate all of them through ssl->certs,
     * while OpenSSL can free a certificate if it is replaced with another
     * certificate of the same type.
     */

#ifdef SSL_CTX_set0_chain

    if (SSL_CTX_set0_chain(ssl->ctx, chain) == 0) {
        ngx_ssl_error(NGX_LOG_EMERG, ssl->log, 0,
                      "SSL_CTX_set0_chain(\"%s\") failed", cert->data);
        sk_X509_pop_free(chain, X509_free);
        return NGX_ERROR;
    }

#else