Skip to content
Browse files

Add SNI support

If multiple certificates are specified, try to perform Server Name
Indication to serve the most appropriate one. We fall back to the last
certificate presented if none of the previous ones match, making it a
useful place to put a star cert.

A few caveats:
- Certificate names are compared as case-insensitive strings, without
  any special logic for dealing with wildcards. The current workaround
  is to always place wildcard certificates last, where they act as the
  default catch-all.
- Certificates are examined in order. The first certificate that matches
  any given request will be used.
- The name -> certificate mapping is stored in a singly linked list.
  This performs very well for use with a handful of certificates, none
  of which have very many Subject Alternative Names, however sites which
  must serve a large number of certificates or names might find a linear
  list scan on every new connection too slow.
  • Loading branch information...
1 parent d78fd49 commit b09da7e7db009ccf8f08ae1ceeae5b0c5e6368a2 @zenazn zenazn committed Jun 27, 2012
Showing with 230 additions and 62 deletions.
  1. +5 −1 README.md
  2. +28 −12 configuration.c
  3. +6 −1 configuration.h
  4. +191 −48 stud.c
View
6 README.md
@@ -83,7 +83,11 @@ Usage
-----
The only required argument is a path to a PEM file that contains the certificate
-(or a chain of certificates) and private key.
+(or a chain of certificates) and private key. If multiple certificates are
+given, `stud` will attempt to perform SNI (Server Name Indication) on new
+connections, by comparing the indicated name with the names on each of the
+certificates, in order. The first certificate that matches will be used. If none
+of the certificates matches, the last certificate will be used as the default.
Detail about the entire set of options can be found by invoking `stud -h`:
View
40 configuration.c
@@ -124,7 +124,7 @@ stud_config * config_new (void) {
r->BACK_IP = strdup("127.0.0.1");
r->BACK_PORT = strdup("8000");
r->NCORES = 1;
- r->CERT_FILE = NULL;
+ r->CERT_FILES = NULL;
r->CIPHER_SUITE = NULL;
r->ENGINE = NULL;
r->BACKLOG = 100;
@@ -161,7 +161,14 @@ void config_destroy (stud_config *cfg) {
if (cfg->FRONT_PORT != NULL) free(cfg->FRONT_PORT);
if (cfg->BACK_IP != NULL) free(cfg->BACK_IP);
if (cfg->BACK_PORT != NULL) free(cfg->BACK_PORT);
- if (cfg->CERT_FILE != NULL) free(cfg->CERT_FILE);
+ if (cfg->CERT_FILES != NULL) {
+ struct cert_files *curr = cfg->CERT_FILES, *next;
+ while (cfg->CERT_FILES != NULL) {
+ next = curr->NEXT;
+ free(curr);
+ curr = next;
+ }
+ }
if (cfg->CIPHER_SUITE != NULL) free(cfg->CIPHER_SUITE);
if (cfg->ENGINE != NULL) free(cfg->ENGINE);
@@ -689,8 +696,12 @@ void config_param_validate (char *k, char *v, stud_config *cfg, char *file, int
else if (! S_ISREG(st.st_mode)) {
config_error_set("Invalid x509 certificate PEM file '%s': Not a file.", v);
r = 0;
- } else
- config_assign_str(&cfg->CERT_FILE, v);
+ } else {
+ struct cert_files *cert = calloc(1, sizeof(*cert));
+ config_assign_str(&cert->CERT_FILE, v);
+ cert->NEXT = cfg->CERT_FILES;
+ cfg->CERT_FILES = cert;
+ }
}
}
else {
@@ -946,9 +957,11 @@ void config_print_default (FILE *fd, stud_config *cfg) {
fprintf(fd, "\n");
fprintf(fd, "# SSL x509 certificate file. REQUIRED.\n");
+ fprintf(fd, "# List multiple certs to use SNI. Certs are used in the order they\n");
+ fprintf(fd, "# are listed; the last cert listed will be used if none of the others match\n");
fprintf(fd, "#\n");
fprintf(fd, "# type: string\n");
- fprintf(fd, FMT_QSTR, CFG_PEM_FILE, config_disp_str(cfg->CERT_FILE));
+ fprintf(fd, FMT_QSTR, CFG_PEM_FILE, "");
fprintf(fd, "\n");
fprintf(fd, "# SSL protocol.\n");
@@ -1115,7 +1128,7 @@ void config_print_usage (char *prog, stud_config *cfg) {
void config_parse_cli(int argc, char **argv, stud_config *cfg) {
static int tls = 0, ssl = 0;
static int client = 0;
- int c;
+ int c, i;
int test_only = 0;
char *prog;
@@ -1289,19 +1302,22 @@ void config_parse_cli(int argc, char **argv, stud_config *cfg) {
config_die("Shared cache update listener is defined, but shared cache is disabled.");
#endif
- // argv leftovers, do we have pem file as an argument?
+ // Any arguments left are presumed to be PEM files
argc -= optind;
argv += optind;
- if (argv != NULL && argv[0] != NULL)
- config_param_validate(CFG_PEM_FILE, argv[0], cfg, NULL, 0);
- else if ((cfg->PMODE == SSL_SERVER) && (cfg->CERT_FILE == NULL || strlen(cfg->CERT_FILE) < 1))
+ for (i = 0; i < argc; i++) {
+ config_param_validate(CFG_PEM_FILE, argv[i], cfg, NULL, 0);
+ }
+ if (cfg->PMODE == SSL_SERVER && cfg->CERT_FILES == NULL) {
config_die("No x509 certificate PEM file specified!");
+ }
// was this only a test?
if (test_only) {
- fprintf(stderr, "Trying to initialize SSL context with certificate '%s'\n", cfg->CERT_FILE);
- if (! init_openssl())
+ fprintf(stderr, "Trying to initialize SSL contexts with your certificates");
+ if (!init_openssl()) {
config_die("Error initializing OpenSSL.");
+ }
printf("%s configuration looks ok.\n", basename(prog));
exit(0);
}
View
7 configuration.h
@@ -31,6 +31,11 @@ typedef enum {
SSL_CLIENT
} PROXY_MODE;
+struct cert_files {
+ char *CERT_FILE;
+ struct cert_files *NEXT;
+};
+
/* configuration structure */
struct __stud_config {
ENC_TYPE ETYPE;
@@ -46,7 +51,7 @@ struct __stud_config {
char *BACK_IP;
char *BACK_PORT;
long NCORES;
- char *CERT_FILE;
+ struct cert_files *CERT_FILES;
char *CIPHER_SUITE;
char *ENGINE;
int BACKLOG;
View
239 stud.c
@@ -53,11 +53,13 @@
#include <sched.h>
#include <signal.h>
-#include <openssl/x509.h>
#include <openssl/ssl.h>
+#include <openssl/x509.h>
+#include <openssl/x509v3.h>
+#include <openssl/x509.h>
#include <openssl/err.h>
#include <openssl/engine.h>
-#include <openssl/bio.h>
+#include <openssl/asn1.h>
#include <ev.h>
#include "ringbuffer.h"
@@ -81,6 +83,13 @@
# define SOL_TCP IPPROTO_TCP
#endif
+/* Do we have SNI support? */
+#ifndef OPENSSL_NO_TLSEXT
+#ifndef SSL_CTRL_SET_TLSEXT_HOSTNAME
+#define OPENSSL_NO_TLSEXT
+#endif
+#endif
+
/* Globals */
static struct ev_loop *loop;
static struct addrinfo *backaddr;
@@ -89,7 +98,7 @@ static ev_io listener;
static int listener_socket;
static int child_num;
static pid_t *child_pids;
-static SSL_CTX *ssl_ctx;
+static SSL_CTX *default_ctx;
static SSL_SESSION *client_session;
#ifdef USE_SHARED_CACHE
@@ -113,6 +122,22 @@ typedef enum _SHUTDOWN_REQUESTOR {
SHUTDOWN_SSL
} SHUTDOWN_REQUESTOR;
+#ifndef OPENSSL_NO_TLSEXT
+/*
+ * SSL context linked list. Someday it might be nice to have a more clever data
+ * structure here, but assuming the number of SNI certs is small it probably
+ * doesn't matter.
+ */
+typedef struct ctx_list {
+ char *servername;
+ SSL_CTX *ctx;
+ struct ctx_list *next;
+} ctx_list;
+
+static ctx_list *sni_ctxs;
+
+#endif /* OPENSSL_NO_TLSEXT */
+
/*
* Proxied State
*
@@ -290,7 +315,7 @@ static void handle_shcupd(struct ev_loop *loop, ev_io *w, int revents) {
/* drop too unsync updates */
r -= sizeof(uint32_t);
encdate = *((uint32_t *)&msg[r]);
- if (!(abs((int)(int32_t)now-ntohl(encdate)) < SSL_CTX_get_timeout(ssl_ctx)))
+ if (!(abs((int)(int32_t)now-ntohl(encdate)) < SSL_CTX_get_timeout(default_ctx)))
continue;
shctx_sess_add(msg, r, now);
@@ -529,82 +554,104 @@ RSA *load_rsa_privatekey(SSL_CTX *ctx, const char *file) {
return rsa;
}
-/* Init library and load specified certificate.
- * Establishes a SSL_ctx, to act as a template for
- * each connection */
-SSL_CTX * init_openssl() {
- SSL_library_init();
- SSL_load_error_strings();
- SSL_CTX *ctx = NULL;
+#ifndef OPENSSL_NO_TLSEXT
+/*
+ * Switch the context of the current SSL object to the most appropriate one
+ * based on the SNI header
+ */
+int sni_switch_ctx(SSL *ssl, int *al, void *data) {
+ (void)data;
+ (void)al;
+ const char *servername;
+ const ctx_list *cl;
+
+ servername = SSL_get_servername(ssl, TLSEXT_NAMETYPE_host_name);
+ if (!servername) return SSL_TLSEXT_ERR_NOACK;
+
+ // For now, just compare servernames as case insensitive strings. Someday,
+ // it might be nice to Do The Right Thing around star certs.
+ for (cl = sni_ctxs; cl != NULL; cl = cl->next) {
+ if (strcasecmp(servername, cl->servername) == 0) {
+ SSL_set_SSL_CTX(ssl, cl->ctx);
+ return SSL_TLSEXT_ERR_NOACK;
+ }
+ }
+
+ return SSL_TLSEXT_ERR_NOACK;
+}
+#endif /* OPENSSL_NO_TLSEXT */
+
+
+/*
+ * Initialize an SSL context
+ */
+
+SSL_CTX *make_ctx(const char *pemfile) {
+ SSL_CTX *ctx;
RSA *rsa;
long ssloptions = SSL_OP_NO_SSLv2 | SSL_OP_ALL |
SSL_OP_NO_SESSION_RESUMPTION_ON_RENEGOTIATION;
- if (CONFIG->ETYPE == ENC_TLS)
- ctx = SSL_CTX_new((CONFIG->PMODE == SSL_CLIENT) ? TLSv1_client_method() : TLSv1_server_method());
- else if (CONFIG->ETYPE == ENC_SSL)
- ctx = SSL_CTX_new((CONFIG->PMODE == SSL_CLIENT) ? SSLv23_client_method() : SSLv23_server_method());
- else
- assert(CONFIG->ETYPE == ENC_TLS || CONFIG->ETYPE == ENC_SSL);
-
#ifdef SSL_OP_NO_COMPRESSION
ssloptions |= SSL_OP_NO_COMPRESSION;
#endif
+ if (CONFIG->ETYPE == ENC_TLS) {
+ ctx = SSL_CTX_new((CONFIG->PMODE == SSL_CLIENT) ?
+ TLSv1_client_method() : TLSv1_server_method());
+ } else if (CONFIG->ETYPE == ENC_SSL) {
+ ctx = SSL_CTX_new((CONFIG->PMODE == SSL_CLIENT) ?
+ SSLv23_client_method() : SSLv23_server_method());
+ } else {
+ assert(CONFIG->ETYPE == ENC_TLS || CONFIG->ETYPE == ENC_SSL);
+ return NULL; // Won't happen, but gcc was complaining
+ }
+
SSL_CTX_set_options(ctx, ssloptions);
SSL_CTX_set_info_callback(ctx, info_callback);
- if (CONFIG->ENGINE) {
- ENGINE *e = NULL;
- ENGINE_load_builtin_engines();
- if (!strcmp(CONFIG->ENGINE, "auto"))
- ENGINE_register_all_complete();
- else {
- if ((e = ENGINE_by_id(CONFIG->ENGINE)) == NULL ||
- !ENGINE_init(e) ||
- !ENGINE_set_default(e, ENGINE_METHOD_ALL)) {
- ERR_print_errors_fp(stderr);
- exit(1);
- }
- LOG("{core} will use OpenSSL engine %s.\n", ENGINE_get_id(e));
- ENGINE_finish(e);
- ENGINE_free(e);
+ if (CONFIG->CIPHER_SUITE) {
+ if (SSL_CTX_set_cipher_list(ctx, CONFIG->CIPHER_SUITE) != 1) {
+ ERR_print_errors_fp(stderr);
}
}
- if (CONFIG->CIPHER_SUITE)
- if (SSL_CTX_set_cipher_list(ctx, CONFIG->CIPHER_SUITE) != 1)
- ERR_print_errors_fp(stderr);
-
- if (CONFIG->PREFER_SERVER_CIPHERS)
+ if (CONFIG->PREFER_SERVER_CIPHERS) {
SSL_CTX_set_options(ctx, SSL_OP_CIPHER_SERVER_PREFERENCE);
+ }
-
- if (CONFIG->PMODE == SSL_CLIENT)
+ if (CONFIG->PMODE == SSL_CLIENT) {
return ctx;
+ }
/* SSL_SERVER Mode stuff */
- if (SSL_CTX_use_certificate_chain_file(ctx, CONFIG->CERT_FILE) <= 0) {
+ if (SSL_CTX_use_certificate_chain_file(ctx, pemfile) <= 0) {
ERR_print_errors_fp(stderr);
exit(1);
}
- rsa = load_rsa_privatekey(ctx, CONFIG->CERT_FILE);
- if(!rsa) {
+ rsa = load_rsa_privatekey(ctx, pemfile);
+ if (!rsa) {
ERR("Error loading rsa private key\n");
exit(1);
}
- if (SSL_CTX_use_RSAPrivateKey(ctx,rsa) <= 0) {
+ if (SSL_CTX_use_RSAPrivateKey(ctx, rsa) <= 0) {
ERR_print_errors_fp(stderr);
exit(1);
}
#ifndef OPENSSL_NO_DH
- init_dh(ctx, CONFIG->CERT_FILE);
+ init_dh(ctx, pemfile);
#endif /* OPENSSL_NO_DH */
+#ifndef OPENSSL_NO_TLSEXT
+ if (!SSL_CTX_set_tlsext_servername_callback(ctx, sni_switch_ctx)) {
+ ERR("Error setting up SNI support\n");
+ }
+#endif /* OPENSSL_NO_TLSEXT */
+
#ifdef USE_SHARED_CACHE
if (CONFIG->SHARED_CACHE) {
if (shared_context_init(ctx, CONFIG->SHARED_CACHE) < 0) {
@@ -631,6 +678,102 @@ SSL_CTX * init_openssl() {
return ctx;
}
+/* Init library and load specified certificate.
+ * Establishes a SSL_ctx, to act as a template for
+ * each connection */
+void init_openssl() {
+ SSL_library_init();
+ SSL_load_error_strings();
+
+ assert(CONFIG->CERT_FILES != NULL);
+
+ // The first file (i.e., the last file listed in config) is always the
+ // "default" cert
+ default_ctx = make_ctx(CONFIG->CERT_FILES->CERT_FILE);
+
+#ifndef OPENSSL_NO_TLSEXT
+ {
+ struct cert_files *cf;
+ int i;
+ SSL_CTX *ctx;
+ X509 *x509;
+ BIO *f;
+
+ STACK_OF(GENERAL_NAME) *names = NULL;
+ GENERAL_NAME *name;
+
+#define PUSH_CTX(asn1_str, ctx) \
+ do { \
+ struct ctx_list *cl; \
+ cl = calloc(1, sizeof(*cl)); \
+ ASN1_STRING_to_UTF8((unsigned char **)&cl->servername, asn1_str); \
+ cl->ctx = ctx; \
+ cl->next = sni_ctxs; \
+ sni_ctxs = cl; \
+ } while (0)
+
+ // Go through the list of PEMs and make some SSL contexts for them. We also
+ // keep track of the names associated with each cert so we can do SNI on
+ // them later
+ for (cf = CONFIG->CERT_FILES->NEXT; cf != NULL; cf = cf->NEXT) {
+ ctx = make_ctx(cf->CERT_FILE);
+ f = BIO_new(BIO_s_file());
+ // TODO: error checking
+ if (!BIO_read_filename(f, cf->CERT_FILE)) {
+ ERR("Could not read cert '%s'\n", cf->CERT_FILE);
+ }
+ x509 = PEM_read_bio_X509_AUX(f, NULL, NULL, NULL);
+ BIO_free(f);
+
+ // First, look for Subject Alternative Names
+ names = X509_get_ext_d2i(x509, NID_subject_alt_name, NULL, NULL);
+ for (i = 0; i < sk_GENERAL_NAME_num(names); i++) {
+ name = sk_GENERAL_NAME_value(names, i);
+ if (name->type == GEN_DNS) {
+ PUSH_CTX(name->d.dNSName, ctx);
+ }
+ }
+ if (sk_GENERAL_NAME_num(names) > 0) {
+ sk_GENERAL_NAME_pop_free(names, GENERAL_NAME_free);
+ // If we actally found some, don't bother looking any further
+ continue;
+ } else if (names != NULL) {
+ sk_GENERAL_NAME_pop_free(names, GENERAL_NAME_free);
+ }
+
+ // Now we're left looking at the CN on the cert
+ X509_NAME *x509_name = X509_get_subject_name(x509);
+ i = X509_NAME_get_index_by_NID(x509_name, NID_commonName, -1);
+ if (i < 0) {
+ ERR("Could not find Subject Alternative Names or a CN on cert %s\n",
+ cf->CERT_FILE);
+ }
+ X509_NAME_ENTRY *x509_entry = X509_NAME_get_entry(x509_name, i);
+ PUSH_CTX(x509_entry->value, ctx);
+ }
+ }
+#undef APPEND_CTX
+#endif /* OPENSSL_NO_TLSEXT */
+
+ if (CONFIG->ENGINE) {
+ ENGINE *e = NULL;
+ ENGINE_load_builtin_engines();
+ if (!strcmp(CONFIG->ENGINE, "auto"))
+ ENGINE_register_all_complete();
+ else {
+ if ((e = ENGINE_by_id(CONFIG->ENGINE)) == NULL ||
+ !ENGINE_init(e) ||
+ !ENGINE_set_default(e, ENGINE_METHOD_ALL)) {
+ ERR_print_errors_fp(stderr);
+ exit(1);
+ }
+ LOG("{core} will use OpenSSL engine %s.\n", ENGINE_get_id(e));
+ ENGINE_finish(e);
+ ENGINE_free(e);
+ }
+ }
+}
+
static void prepare_proxy_line(struct sockaddr* ai_addr) {
tcp_proxy_line[0] = 0;
char tcp6_address_string[INET6_ADDRSTRLEN];
@@ -1397,7 +1540,7 @@ static void handle_connections() {
ev_timer_start(loop, &timer_ppid_check);
ev_io_init(&listener, (CONFIG->PMODE == SSL_CLIENT) ? handle_clear_accept : handle_accept, listener_socket, EV_READ);
- listener.data = ssl_ctx;
+ listener.data = default_ctx;
ev_io_start(loop, &listener);
ev_loop(loop, 0);
@@ -1686,8 +1829,8 @@ int main(int argc, char **argv) {
}
#endif /* USE_SHARED_CACHE */
- /* load certificate, pass to handle_connections */
- ssl_ctx = init_openssl();
+ /* load certificates, pass to handle_connections */
+ init_openssl();
if (CONFIG->CHROOT && CONFIG->CHROOT[0])
change_root();

1 comment on commit b09da7e

@fcicq
fcicq commented on b09da7e Oct 29, 2012

please see #122, bug introduced by this commit.

Please sign in to comment.
Something went wrong with that request. Please try again.