Bug 50812

Summary: mod_ssl SSLProxyMachineCertificateFile can't use a 2+ depth certificate when server only returns root CA's on its Acceptable client certificate CA names
Product: Apache httpd-2 Reporter: william d <will.dutt+httpd>
Component: mod_sslAssignee: Apache HTTPD Bugs Mailing List <bugs>
Status: RESOLVED FIXED    
Severity: enhancement CC: DRuggeri, shell_layer-apachesf, will.dutt+httpd
Priority: P2 Keywords: FixedInTrunk
Version: 2.2.17   
Target Milestone: ---   
Hardware: All   
OS: All   

Description william d 2011-02-20 22:44:46 UTC
I am trying to connect to a IIS server that has the "VeriSign Class 3 Public Primary Certification Authority - G5" root certificate and intermediates. It has been configured to only send the root CA's when the client (httpd) asks for it.

My Client Certificate was signed by an Intermediate Certificate "VeriSign Class 3 International Server CA - G3" which was signed by "VeriSign Class 3 Public Primary Certification Authority - G5"

I can manually create the connection with the client certificate using 'openssl s_client' and chat to the sever manually with the same configuration as the VirtualHost.

I have configured my Apache server to ProxySSL our requests to this server.

Yet I see 403.7 errors when I try and connect to the server via httpd.

Here is my VirtualHost.

<VirtualHost *:8075>
  DocumentRoot "/www/proxy-a.provider/htdocs"
  ServerName *:8075

#  LogLevel info
  LogLevel debug
  ErrorLog /var/log/www/proxy-a.provider/error_log
  CustomLog /var/log/www/proxy-a.provider/access_log timed

  <IfModule mod_ssl.c>
    SSLEngine off
    SSLProxyEngine on
    SSLCipherSuite ALL:!ADH:!EXPORT56:RC4+RSA:+HIGH:+MEDIUM:+LOW:+SSLv2:+EXP
    SSLCACertificatePath /www/proxy-a.provider/conf/ssl/CA
    SSLProxyCACertificatePath /www/proxy-a.provider/conf/ssl/CA
    SSLProxyMachineCertificateFile /www/proxy-a.provider/conf/ssl/certs/client.crt
  </IfModule>


  <IfModule mod_proxy.c>
    ProxyRequests    Off
    RewriteEngine    On
    RewriteLogLevel  1
    RewriteLog       /var/log/www/proxy-a.provider/rewrite_log

## Added following line to skip rewrite for error documents.
    RewriteCond %{REQUEST_URI} !^/error
    RewriteRule      ^/(.*)$ https://provder.a/$1 [E=SERVER:provder.a,P,L]
    RewriteRule      .* - [F]
    ProxyPassReverse / https://provder.a
  </IfModule>
</VirtualHost>

The Debug logs has 

[Mon Feb 21 12:32:18 2011] [debug] ssl_engine_kernel.c(1760): OpenSSL: Loop: SSLv3 write client certificate A
[Mon Feb 21 12:32:18 2011] [debug] ssl_engine_kernel.c(1760): OpenSSL: Loop: SSLv3 write client key exchange A
[Mon Feb 21 12:32:18 2011] [debug] ssl_engine_kernel.c(1760): OpenSSL: Loop: SSLv3 write change cipher spec A
[Mon Feb 21 12:32:18 2011] [debug] ssl_engine_kernel.c(1760): OpenSSL: Loop: SSLv3 write finished A
[Mon Feb 21 12:32:18 2011] [debug] ssl_engine_kernel.c(1760): OpenSSL: Loop: SSLv3 flush data
[Mon Feb 21 12:32:18 2011] [debug] ssl_engine_kernel.c(1760): OpenSSL: Loop: SSLv3 read server certificate request A
[Mon Feb 21 12:32:18 2011] [debug] ssl_engine_kernel.c(1760): OpenSSL: Loop: SSLv3 read server done A
[Mon Feb 21 12:32:18 2011] [debug] ssl_engine_kernel.c(1526): Proxy client certificate callback: (*:8075) entered
[Mon Feb 21 12:32:18 2011] [debug] ssl_engine_kernel.c(1571): Proxy client certificate callback: (*:8075) no client certificate found!


The Servers Acceptable client certificate CA names as are follows 



/C=US/O=VeriSign, Inc./OU=Class 1 Public Primary Certification Authority - G2/OU=(c) 1998 VeriSign, Inc. - For authorized use only/OU=VeriSign Trust Network

/C=US/O=VeriSign, Inc./OU=Class 4 Public Primary Certification Authority - G2/OU=(c) 1998 VeriSign, Inc. - For authorized use only/OU=VeriSign Trust Network

/C=US/O=VeriSign, Inc./OU=VeriSign Trust Network/OU=(c) 2006 VeriSign, Inc. - For authorized use only/CN=VeriSign Class 3 Public Primary Certification Authority - G5

/C=US/O=VeriSign, Inc./OU=Class 3 Public Primary Certification Authority




The problem occurs in this code snippit /modules/ssl/ssl_engine_kernel.c  in that it does not check the CA chain higher than just the parent.

int ssl_callback_proxy_cert(SSL *ssl, MODSSL_CLIENT_CERT_CB_ARG_TYPE **x509, EVP_PKEY **pkey)
{
    conn_rec *c = (conn_rec *)SSL_get_app_data(ssl);
    server_rec *s = c->base_server;
    SSLSrvConfigRec *sc = mySrvConfig(s);
    X509_NAME *ca_name, *issuer;
    X509_INFO *info;
    STACK_OF(X509_NAME) *ca_list;
    STACK_OF(X509_INFO) *certs = sc->proxy->pkp->certs;
    int i, j;

    ap_log_error(APLOG_MARK, APLOG_DEBUG, 0, s,
                 SSLPROXY_CERT_CB_LOG_FMT "entered",
                 sc->vhost_id);

    if (!certs || (sk_X509_INFO_num(certs) <= 0)) {
        ap_log_error(APLOG_MARK, APLOG_WARNING, 0, s,
                     SSLPROXY_CERT_CB_LOG_FMT
                     "downstream server wanted client certificate "
                     "but none are configured", sc->vhost_id);
        return FALSE;
    }

    ca_list = SSL_get_client_CA_list(ssl);

    if (!ca_list || (sk_X509_NAME_num(ca_list) <= 0)) {
        /*
         * downstream server didn't send us a list of acceptable CA certs,
         * so we send the first client cert in the list.
         */
        info = sk_X509_INFO_value(certs, 0);

        modssl_proxy_info_log(s, info, "no acceptable CA list");

        modssl_set_cert_info(info, x509, pkey);

        return TRUE;
    }

    for (i = 0; i < sk_X509_NAME_num(ca_list); i++) {
        ca_name = sk_X509_NAME_value(ca_list, i);

        for (j = 0; j < sk_X509_INFO_num(certs); j++) {
            info = sk_X509_INFO_value(certs, j);
            issuer = X509_get_issuer_name(info->x509);

            if (X509_NAME_cmp(issuer, ca_name) == 0) {
                modssl_proxy_info_log(s, info, "found acceptable cert");

                modssl_set_cert_info(info, x509, pkey);

                return TRUE;
            }
        }
    }

    ap_log_error(APLOG_MARK, APLOG_DEBUG, 0, s,
                 SSLPROXY_CERT_CB_LOG_FMT
                 "no client certificate found!?", sc->vhost_id);

    return FALSE;
}
Comment 1 Joe Orton 2011-02-25 12:06:49 UTC
The code is working as designed; the IIS server should be configured to send the intermediate G3 CA cert in the list of acceptable CA names in the client cert request.
Comment 2 Carl Antuar 2011-05-10 06:57:16 UTC
@Joe Orton:
The TLS specification indicates that it's acceptable for the server to simply send a root CA and expect a client CA falling under that root.

See http://tools.ietf.org/html/rfc5246

Section 7.4.4, in reference to the list of CA names in the client certificate request, provides that
"These distinguished names may specify a desired distinguished name for a root CA or for a subordinate CA; thus, this message can be used to describe known roots as well as a desired authorization space."

Section 7.4.6, in reference to the client certificate message, provides that 
"This message conveys the client's certificate chain to the server"
and
"If the certificate_authorities list in the certificate request message was non-empty, one of the certificates in the certificate chain SHOULD be issued by one of the listed CAs."

These references strongly suggest that the list of acceptable CAs does not need to include every level of the certificate chain; it can simply include the root and let the client determine which certificates ultimately come under that root.

If searching multiple levels of the certificate chain is too complex, then I recommend as a minimum that the fallback action, when 'ssl_callback_proxy_cert' does not find a client certificate matching any on the acceptable CA list, should be to send the first configured client certificate, as if the acceptable CA list were empty.
Comment 3 Keith Burdis 2011-05-13 09:23:03 UTC
If we don't want to change the existing behaviour, perhaps an additional ForceSendClientCert option (or similar) could be added to SSLOptions to force sending the first configured client certificate if no acceptable client certs could be found.
Comment 4 william d 2011-05-19 01:06:03 UTC
I agree with Keith Burdis

Please add ForceSendClientCert option (or similar)  to SSLOptions
Comment 5 Jim Mittler 2011-08-17 20:27:01 UTC
Hit this problem today too. I'm all for any solution that let's us force send the Client Certificate. It looks like the code is already setup to do this in the absence of any CAs coming from the server side.
Comment 6 Daniel Ruggeri 2011-08-23 20:07:39 UTC
Hello;
   The following patches should add a new directive. The file should contain all intermediary CA's used by all of your clients. On init, a chain for each certificate will be created and mod_proxy will use that chain if it finds an issuing name that is not a direct signer of a client cert.

I have tested and confirmed this works on Linux for configurations using one or more client certificate with a chain to one or more different root CA's
  http://people.apache.org/~druggeri/patches/httpd-2.2.19-SSLProxyMachineCertificateChainFile.patch
  http://people.apache.org/~druggeri/patches/httpd-trunk-SSLProxyMachineCertificateChainFile.patch

FWIW, the trunk patch has been applied.
Comment 7 Daniel Ruggeri 2013-05-10 21:26:59 UTC
The SSLProxyMachineCertificateChainFile directive new in 2.2.22 addresses this problem.