From 6b45eeae4441a5a7a88f63a8c92ef26842a5f663 Mon Sep 17 00:00:00 2001 From: "Kyle J. McKay" Date: Thu, 3 Dec 2015 11:20:34 -0800 Subject: [PATCH] network.c: allow specifying server cert chain To explain why this is needed, consider this comment from the Apache mod_ssl ssl_engine_init.c source file: /* * Optionally configure extra server certificate chain certificates. * This is usually done by OpenSSL automatically when one of the * server cert issuers are found under SSLCACertificatePath or in * SSLCACertificateFile. But because these are intended for client * authentication it can conflict. For instance when you use a * Global ID server certificate you've to send out the intermediate * CA certificate, too. When you would just configure this with * SSLCACertificateFile and also use client authentication mod_ssl * would accept all clients also issued by this CA. Obviously this * isn't what we want in this situation. So this feature here exists * to allow one to explicity configure CA certificates which are * used only for the server certificate chain. */ Note that SSLCACertificateFile corresponds to lighttpd's ssl.ca-file directive. However, lighttpd does not have an explicit directive to set the server's sertificate chain. It sets only the server's certificate (excluding any necessary intermediate certificates) using the SSL_CTX_use_certificate function. The server's certificate chain, if it were to be configured explicitly, would be set using the SSL_CTX_add_extra_chain_cert function. However, lighttpd never calls the SSL_CTX_add_extra_chain_cert function. But consider this information from the man page documentation about the SSL_CTX_use_certificate function: SSL_CTX_use_certificate() loads the certificate x into ctx, SSL_use_certificate() loads x into ssl. The rest of the certificates needed to form the complete certificate chain can be specified using the SSL_CTX_add_extra_chain_cert(3) function. SSL_CTX_use_certificate_file() loads the first certificate stored in file into ctx. The formatting type of the certificate must be specified from the known types SSL_FILETYPE_PEM, SSL_FILETYPE_ASN1. SSL_use_certificate_file() loads the certificate from file into ssl. See the NOTES section on why SSL_CTX_use_certificate_chain_file() should be preferred. SSL_CTX_use_certificate_chain_file() loads a certificate chain from file into ctx. The certificates must be in PEM format and must be sorted starting with the subject's certificate (actual client or server certificate), followed by intermediate CA certificates if applicable, and ending at the highest level (root) CA. There is no corresponding function working on a single SSL object. SSL_CTX_use_certificate_chain_file() is only applicable to PEM formatting. Files of type SSL_FILETYPE_PEM can contain more than one item. SSL_CTX_use_certificate_chain_file() adds the first certificate found in the file to the certificate store. The other certificates are added to the store of chain certificates using SSL_CTX_add_extra_chain_cert(3). There exists only one extra chain store, so that the same chain is appended to both types of certificates, RSA and DSA! If it is not intended to use both type of certificate at the same time, it is recommended to use the SSL_CTX_use_certificate_chain_file() instead of the SSL_CTX_use_certificate_file() function in order to allow the use of complete certificate chains even when no trusted CA storage is used or when the CA issuing the certificate shall not be added to the trusted CA storage. We could switch from using the SSL_CTX_use_certificate function to the SSL_CTX_use_certificate_chain_file function instead so that an optional server certificate chain can simply be concatenated onto the end of the file specified using the ssl.pemfile directive. This requires only the very simplest change. The problem with such a change is that the file may not be read until after lighttpd has dropped privileges. If the file has restricted privileges because it contains the server certificate's key it may not be readable at that time. However, the solution is simple. Just set a separate server certificate key file with restricted permissions and relax the permissions on the pemfile that then contains only non-sensitive certificates but no key. Of course lighttpd has no ssl.keyfile directive making this impossible. The alternative is to load the entire chain at the time the ssl.pemfile is originally read and then call SSL_CTX_add_extra_chain_cert for each extra certificate in the chain. The final solution requires a bit more code than just using SSL_CTX_use_certificate_chain_file would, but it's more compatible. Also, while we we're mucking around with the SSL function calls, we take the opportunity to correct the error return checking. Most SSL function calls are documented as returning 1 on success. If it's not 1 then it cannot be assumed to be successful, so checking for '< 0' for failure or '!0' for success is simply incorrect. We correct all these checks. Signed-off-by: Kyle J. McKay --- src/base.h | 2 +- src/configfile.c | 4 ++-- src/network.c | 64 +++++++++++++++++++++++++++++++++++++++++++------------- src/server.c | 2 +- 4 files changed, 54 insertions(+), 18 deletions(-) diff --git a/src/base.h b/src/base.h index 4c748a57..12c91d2f 100644 --- a/src/base.h +++ b/src/base.h @@ -322,7 +322,7 @@ typedef struct { SSL_CTX *ssl_ctx; /* not patched */ /* SNI per host: with COMP_SERVER_SOCKET, COMP_HTTP_SCHEME, COMP_HTTP_HOST */ EVP_PKEY *ssl_pemfile_pkey; - X509 *ssl_pemfile_x509; + STACK_OF(X509) *ssl_pemfile_x509s; STACK_OF(X509_NAME) *ssl_ca_file_cert_names; #endif } specific_config; diff --git a/src/configfile.c b/src/configfile.c index 72dabb70..8d47ae61 100644 --- a/src/configfile.c +++ b/src/configfile.c @@ -355,7 +355,7 @@ int config_setup_connection(server *srv, connection *con) { PATCH(ssl_pemfile); #ifdef USE_OPENSSL - PATCH(ssl_pemfile_x509); + PATCH(ssl_pemfile_x509s); PATCH(ssl_pemfile_pkey); #endif PATCH(ssl_ca_file); @@ -429,7 +429,7 @@ int config_patch_connection(server *srv, connection *con, comp_key_t comp) { } else if (buffer_is_equal_string(du->key, CONST_STR_LEN("ssl.pemfile"))) { PATCH(ssl_pemfile); #ifdef USE_OPENSSL - PATCH(ssl_pemfile_x509); + PATCH(ssl_pemfile_x509s); PATCH(ssl_pemfile_pkey); #endif } else if (buffer_is_equal_string(du->key, CONST_STR_LEN("ssl.ca-file"))) { diff --git a/src/network.c b/src/network.c index 24a435c2..fd77fc34 100644 --- a/src/network.c +++ b/src/network.c @@ -112,7 +112,7 @@ static int network_ssl_servername_callback(SSL *ssl, int *al, server *srv) { config_patch_connection(srv, con, COMP_HTTP_SCHEME); config_patch_connection(srv, con, COMP_HTTP_HOST); - if (NULL == con->conf.ssl_pemfile_x509 || NULL == con->conf.ssl_pemfile_pkey) { + if (NULL == con->conf.ssl_pemfile_x509s || NULL == con->conf.ssl_pemfile_pkey) { /* x509/pkey available <=> pemfile was set <=> pemfile got patched: so this should never happen, unless you nest $SERVER["socket"] */ log_error_write(srv, __FILE__, __LINE__, "ssb", "SSL:", "no certificate/private key for TLS server name", con->tlsext_server_name); @@ -120,14 +120,14 @@ static int network_ssl_servername_callback(SSL *ssl, int *al, server *srv) { } /* first set certificate! setting private key checks whether certificate matches it */ - if (!SSL_use_certificate(ssl, con->conf.ssl_pemfile_x509)) { + if (SSL_use_certificate(ssl, sk_X509_value(con->conf.ssl_pemfile_x509s, 0)) != 1) { log_error_write(srv, __FILE__, __LINE__, "ssb:s", "SSL:", "failed to set certificate for TLS server name", con->tlsext_server_name, ERR_error_string(ERR_get_error(), NULL)); return SSL_TLSEXT_ERR_ALERT_FATAL; } - if (!SSL_use_PrivateKey(ssl, con->conf.ssl_pemfile_pkey)) { + if (SSL_use_PrivateKey(ssl, con->conf.ssl_pemfile_pkey) != 1) { log_error_write(srv, __FILE__, __LINE__, "ssb:s", "SSL:", "failed to set private key for TLS server name", con->tlsext_server_name, ERR_error_string(ERR_get_error(), NULL)); @@ -522,35 +522,71 @@ typedef enum { } network_backend_t; #ifdef USE_OPENSSL -static X509* x509_load_pem_file(server *srv, const char *file) { +static STACK_OF(X509)* x509s_load_pem_file(server *srv, const char *file) { BIO *in; X509 *x = NULL; + STACK_OF(X509) *x509s = NULL; + int err = 0; in = BIO_new(BIO_s_file()); if (NULL == in) { log_error_write(srv, __FILE__, __LINE__, "S", "SSL: BIO_new(BIO_s_file()) failed"); goto error; } + x509s = sk_X509_new_null(); + if (NULL == x509s) { + log_error_write(srv, __FILE__, __LINE__, "S", "SSL: sk_X509_new() failed"); + goto error; + } if (BIO_read_filename(in,file) <= 0) { log_error_write(srv, __FILE__, __LINE__, "SSS", "SSL: BIO_read_filename('", file,"') failed"); goto error; } - x = PEM_read_bio_X509(in, NULL, NULL, NULL); + while (NULL != (x = PEM_read_bio_X509(in, NULL, NULL, NULL))) { + if (!sk_X509_push(x509s, x)) { + log_error_write(srv, __FILE__, __LINE__, "S", "SSL: sk_X509_push() failed"); + goto error; + } + } + if (sk_X509_num(x509s) > 0 && (err = ERR_peek_error()) > 0) { + if (ERR_GET_LIB(err) == ERR_LIB_PEM && ERR_GET_REASON(err) == PEM_R_NO_START_LINE) { + while (ERR_get_error() > 0) { + /* discard EOF error */ + } + err = 0; + } + } - if (NULL == x) { - log_error_write(srv, __FILE__, __LINE__, "SSS", "SSL: couldn't read X509 certificate from '", file,"'"); + if (sk_X509_num(x509s) < 1 || err > 0) { + log_error_write(srv, __FILE__, __LINE__, "SSS", "SSL: couldn't read X509 certificate(s) from '", file,"'"); goto error; } BIO_free(in); - return x; + return x509s; error: if (NULL != in) BIO_free(in); + if (NULL != x509s) sk_X509_pop_free(x509s, X509_free); return NULL; } +static int SSL_CTX_use_certificate_chain(SSL_CTX *ctx, STACK_OF(X509) *x509s) { + int i; + + if (sk_X509_num(x509s) < 1 || SSL_CTX_use_certificate(ctx, sk_X509_value(x509s, 0)) != 1) { + return 0; + } + + for (i=1; i < sk_X509_num(x509s); ++i) { + if (SSL_CTX_add_extra_chain_cert(ctx, X509_dup(sk_X509_value(x509s, i))) != 1) + return 0; + } + + return 1; +} + static EVP_PKEY* evp_pkey_load_pem_file(server *srv, const char *file) { BIO *in; EVP_PKEY *x = NULL; @@ -595,10 +631,10 @@ static int network_openssl_load_pemfile(server *srv, size_t ndx) { } #endif - if (NULL == (s->ssl_pemfile_x509 = x509_load_pem_file(srv, s->ssl_pemfile->ptr))) return -1; + if (NULL == (s->ssl_pemfile_x509s = x509s_load_pem_file(srv, s->ssl_pemfile->ptr))) return -1; if (NULL == (s->ssl_pemfile_pkey = evp_pkey_load_pem_file(srv, s->ssl_pemfile->ptr))) return -1; - if (!X509_check_private_key(s->ssl_pemfile_x509, s->ssl_pemfile_pkey)) { + if (!X509_check_private_key(sk_X509_value(s->ssl_pemfile_x509s, 0), s->ssl_pemfile_pkey)) { log_error_write(srv, __FILE__, __LINE__, "sssb", "SSL:", "Private key does not match the certificate public key, reason:", ERR_error_string(ERR_get_error(), NULL), @@ -892,13 +928,13 @@ int network_init(server *srv) { SSL_CTX_set_verify_depth(s->ssl_ctx, s->ssl_verifyclient_depth); } - if (SSL_CTX_use_certificate(s->ssl_ctx, s->ssl_pemfile_x509) < 0) { + if (SSL_CTX_use_certificate_chain(s->ssl_ctx, s->ssl_pemfile_x509s) != 1) { log_error_write(srv, __FILE__, __LINE__, "ssb", "SSL:", ERR_error_string(ERR_get_error(), NULL), s->ssl_pemfile); return -1; } - if (SSL_CTX_use_PrivateKey(s->ssl_ctx, s->ssl_pemfile_pkey) < 0) { + if (SSL_CTX_use_PrivateKey(s->ssl_ctx, s->ssl_pemfile_pkey) != 1) { log_error_write(srv, __FILE__, __LINE__, "ssb", "SSL:", ERR_error_string(ERR_get_error(), NULL), s->ssl_pemfile); return -1; @@ -915,8 +951,8 @@ int network_init(server *srv) { SSL_CTX_set_mode(s->ssl_ctx, SSL_CTX_get_mode(s->ssl_ctx) | SSL_MODE_ACCEPT_MOVING_WRITE_BUFFER); # ifndef OPENSSL_NO_TLSEXT - if (!SSL_CTX_set_tlsext_servername_callback(s->ssl_ctx, network_ssl_servername_callback) || - !SSL_CTX_set_tlsext_servername_arg(s->ssl_ctx, srv)) { + if (SSL_CTX_set_tlsext_servername_callback(s->ssl_ctx, network_ssl_servername_callback) != 1 || + SSL_CTX_set_tlsext_servername_arg(s->ssl_ctx, srv) != 1) { log_error_write(srv, __FILE__, __LINE__, "ss", "SSL:", "failed to initialize TLS servername callback, openssl library does not support TLS servername extension"); return -1; diff --git a/src/server.c b/src/server.c index e096de1b..9697010a 100644 --- a/src/server.c +++ b/src/server.c @@ -315,7 +315,7 @@ static void server_free(server *srv) { #ifdef USE_OPENSSL SSL_CTX_free(s->ssl_ctx); EVP_PKEY_free(s->ssl_pemfile_pkey); - X509_free(s->ssl_pemfile_x509); + if (NULL != s->ssl_pemfile_x509s) sk_X509_pop_free(s->ssl_pemfile_x509s, X509_free); if (NULL != s->ssl_ca_file_cert_names) sk_X509_NAME_pop_free(s->ssl_ca_file_cert_names, X509_NAME_free); #endif free(s); -- 2.4.10