Here is a more "radical" attempt at solving this problem. In private discussions with Peter Sylvester it became clear that the solution I proposed last August would only superficially address the problem.
I'm therefore attaching a more comprehensive patch (against OpenSSL_1_0_0_stable), which includes the following modifications: - tlsext_hostname is dropped from the SSL session object (i.e., the hostname is now only stored in the SSL connection object) - servername_done is no longer used - parsing of the SNI extension is moved to the same place where the ticket handling currently happens (see below for more on the reasoning behind this) - the SNI callback is executed immediately after the extension has been parsed successfully (now it happens even before the ticket is decrypted) As Peter is currently busy with other things, my hope is that "someone else" could have a look at / comment on it (in particular someone who's first name starts with "S" ;-). I wouldn't consider it a "perfect" solution - as it still requires two passes over the TLS extensions -, but maybe it's still something which could make it into 1.0.0? The most important issue with the current code and session resumption is that when SNI comes into play, retrieval of a session from the cache always happens *before* the SNI extension is parsed (and therefore before the callback is executed). To make sure that a session is only resumed within the proper SSL_CTX, OpenSSL uses the "sid_ctx" session context id - but (at least) in the case of mod_ssl, this sid_ctx can only be properly set when the SNI callback is actually executed. Looking at ssl3_get_client_hello() from a high-level perspective, this implies that the SNI callback must be executed before the sid_ctx comparison happens (in ssl_get_prev_session(), more precisely). The most straightforward option to achieve this is to handle both the ticket and the SNI extension at the same time - i.e., morph tls1_process_ticket() into tls1_process_servername_and_ticket()... which is part of the proposed changes. I realize that the above explanations might be too terse, actually - but instead of writing lengthy explanation in advance, maybe it's more efficient if we continue the discussion in Q&A mode, if necessary? I'll try my best to answer questions about the proposed patch. Thanks for listening! Kaspar
Index: ssl/ssl.h =================================================================== RCS file: /openssl-cvs/openssl/ssl/ssl.h,v retrieving revision 1.221.2.24 diff -p -u -r1.221.2.24 ssl.h --- ssl/ssl.h 6 Jan 2010 17:37:38 -0000 1.221.2.24 +++ ssl/ssl.h 9 Jan 2010 07:28:24 -0000 @@ -500,7 +500,6 @@ typedef struct ssl_session_st * efficient and to implement a maximum cache size. */ struct ssl_session_st *prev,*next; #ifndef OPENSSL_NO_TLSEXT - char *tlsext_hostname; #ifndef OPENSSL_NO_EC size_t tlsext_ecpointformatlist_length; unsigned char *tlsext_ecpointformatlist; /* peer's list */ @@ -1135,11 +1134,6 @@ struct ssl_st void *arg); void *tlsext_debug_arg; char *tlsext_hostname; - int servername_done; /* no further mod of servername - 0 : call the servername extension callback. - 1 : prepare 2, allow last ack just after in server callback. - 2 : don't call servername callback, no ack in server hello - */ /* certificate status request info */ /* Status type or -1 if no status type */ int tlsext_status_type; Index: ssl/ssl_asn1.c =================================================================== RCS file: /openssl-cvs/openssl/ssl/ssl_asn1.c,v retrieving revision 1.36.2.5 diff -p -u -r1.36.2.5 ssl_asn1.c --- ssl/ssl_asn1.c 30 Oct 2009 14:06:18 -0000 1.36.2.5 +++ ssl/ssl_asn1.c 9 Jan 2010 07:28:24 -0000 @@ -106,7 +106,6 @@ typedef struct ssl_session_asn1_st ASN1_INTEGER timeout; ASN1_INTEGER verify_result; #ifndef OPENSSL_NO_TLSEXT - ASN1_OCTET_STRING tlsext_hostname; ASN1_INTEGER tlsext_tick_lifetime; ASN1_OCTET_STRING tlsext_tick; #endif /* OPENSSL_NO_TLSEXT */ @@ -123,7 +122,7 @@ int i2d_SSL_SESSION(SSL_SESSION *in, uns unsigned char buf[4],ibuf1[LSIZE2],ibuf2[LSIZE2]; unsigned char ibuf3[LSIZE2],ibuf4[LSIZE2],ibuf5[LSIZE2]; #ifndef OPENSSL_NO_TLSEXT - int v6=0,v9=0,v10=0; + int v9=0,v10=0; unsigned char ibuf6[LSIZE2]; #endif #ifndef OPENSSL_NO_COMP @@ -233,12 +232,6 @@ int i2d_SSL_SESSION(SSL_SESSION *in, uns } #ifndef OPENSSL_NO_TLSEXT - if (in->tlsext_hostname) - { - a.tlsext_hostname.length=strlen(in->tlsext_hostname); - a.tlsext_hostname.type=V_ASN1_OCTET_STRING; - a.tlsext_hostname.data=(unsigned char *)in->tlsext_hostname; - } if (in->tlsext_tick) { a.tlsext_tick.length= in->tlsext_ticklen; @@ -294,8 +287,6 @@ int i2d_SSL_SESSION(SSL_SESSION *in, uns M_ASN1_I2D_len_EXP_opt(&a.tlsext_tick_lifetime, i2d_ASN1_INTEGER,9,v9); if (in->tlsext_tick) M_ASN1_I2D_len_EXP_opt(&(a.tlsext_tick), i2d_ASN1_OCTET_STRING,10,v10); - if (in->tlsext_hostname) - M_ASN1_I2D_len_EXP_opt(&(a.tlsext_hostname), i2d_ASN1_OCTET_STRING,6,v6); #ifndef OPENSSL_NO_COMP if (in->compress_meth) M_ASN1_I2D_len_EXP_opt(&(a.comp_id), i2d_ASN1_OCTET_STRING,11,v11); @@ -331,10 +322,6 @@ int i2d_SSL_SESSION(SSL_SESSION *in, uns v4); if (in->verify_result != X509_V_OK) M_ASN1_I2D_put_EXP_opt(&a.verify_result,i2d_ASN1_INTEGER,5,v5); -#ifndef OPENSSL_NO_TLSEXT - if (in->tlsext_hostname) - M_ASN1_I2D_put_EXP_opt(&(a.tlsext_hostname), i2d_ASN1_OCTET_STRING,6,v6); -#endif /* OPENSSL_NO_TLSEXT */ #ifndef OPENSSL_NO_PSK if (in->psk_identity_hint) M_ASN1_I2D_put_EXP_opt(&(a.psk_identity_hint), i2d_ASN1_OCTET_STRING,7,v7); @@ -522,21 +509,6 @@ SSL_SESSION *d2i_SSL_SESSION(SSL_SESSION else ret->verify_result=X509_V_OK; -#ifndef OPENSSL_NO_TLSEXT - os.length=0; - os.data=NULL; - M_ASN1_D2I_get_EXP_opt(osp,d2i_ASN1_OCTET_STRING,6); - if (os.data) - { - ret->tlsext_hostname = BUF_strndup((char *)os.data, os.length); - OPENSSL_free(os.data); - os.data = NULL; - os.length = 0; - } - else - ret->tlsext_hostname=NULL; -#endif /* OPENSSL_NO_TLSEXT */ - #ifndef OPENSSL_NO_PSK os.length=0; os.data=NULL; Index: ssl/ssl_lib.c =================================================================== RCS file: /openssl-cvs/openssl/ssl/ssl_lib.c,v retrieving revision 1.176.2.17 diff -p -u -r1.176.2.17 ssl_lib.c --- ssl/ssl_lib.c 7 Jan 2010 19:05:03 -0000 1.176.2.17 +++ ssl/ssl_lib.c 9 Jan 2010 07:28:24 -0000 @@ -1465,14 +1465,12 @@ const char *SSL_get_servername(const SSL if (type != TLSEXT_NAMETYPE_host_name) return NULL; - return s->session && !s->tlsext_hostname ? - s->session->tlsext_hostname : - s->tlsext_hostname; + return s->tlsext_hostname; } int SSL_get_servername_type(const SSL *s) { - if (s->session && (!s->tlsext_hostname ? s->session->tlsext_hostname : s->tlsext_hostname)) + if (s->session && s->tlsext_hostname) return TLSEXT_NAMETYPE_host_name; return -1; } Index: ssl/ssl_locl.h =================================================================== RCS file: /openssl-cvs/openssl/ssl/ssl_locl.h,v retrieving revision 1.100.2.10 diff -p -u -r1.100.2.10 ssl_locl.h --- ssl/ssl_locl.h 8 Dec 2009 11:38:18 -0000 1.100.2.10 +++ ssl/ssl_locl.h 9 Jan 2010 07:28:24 -0000 @@ -1062,8 +1062,8 @@ int ssl_check_serverhello_tlsext(SSL *s) #else #define tlsext_tick_md EVP_sha256 #endif -int tls1_process_ticket(SSL *s, unsigned char *session_id, int len, - const unsigned char *limit, SSL_SESSION **ret); +int tls1_process_servername_and_ticket(SSL *s, unsigned char *session_id, int len, + const unsigned char *limit, SSL_SESSION **ret); #endif EVP_MD_CTX* ssl_replace_hash(EVP_MD_CTX **hash,const EVP_MD *md) ; void ssl_clear_hash_ctx(EVP_MD_CTX **hash); Index: ssl/ssl_sess.c =================================================================== RCS file: /openssl-cvs/openssl/ssl/ssl_sess.c,v retrieving revision 1.74.2.1 diff -p -u -r1.74.2.1 ssl_sess.c --- ssl/ssl_sess.c 19 Apr 2009 18:03:13 -0000 1.74.2.1 +++ ssl/ssl_sess.c 9 Jan 2010 07:28:24 -0000 @@ -205,7 +205,6 @@ SSL_SESSION *SSL_SESSION_new(void) ss->next=NULL; ss->compress_meth=0; #ifndef OPENSSL_NO_TLSEXT - ss->tlsext_hostname = NULL; #ifndef OPENSSL_NO_EC ss->tlsext_ecpointformatlist_length = 0; ss->tlsext_ecpointformatlist = NULL; @@ -367,14 +366,6 @@ int ssl_get_new_session(SSL *s, int sess } #ifndef OPENSSL_NO_TLSEXT sess_id_done: - if (s->tlsext_hostname) { - ss->tlsext_hostname = BUF_strdup(s->tlsext_hostname); - if (ss->tlsext_hostname == NULL) { - SSLerr(SSL_F_SSL_GET_NEW_SESSION, ERR_R_INTERNAL_ERROR); - SSL_SESSION_free(ss); - return 0; - } - } #ifndef OPENSSL_NO_EC if (s->tlsext_ecpointformatlist) { @@ -437,7 +428,7 @@ int ssl_get_prev_session(SSL *s, unsigne if (len > SSL_MAX_SSL_SESSION_ID_LENGTH) goto err; #ifndef OPENSSL_NO_TLSEXT - r = tls1_process_ticket(s, session_id, len, limit, &ret); + r = tls1_process_servername_and_ticket(s, session_id, len, limit, &ret); if (r == -1) { fatal = 1; @@ -714,7 +713,6 @@ void SSL_SESSION_free(SSL_SESSION *ss) if (ss->peer != NULL) X509_free(ss->peer); if (ss->ciphers != NULL) sk_SSL_CIPHER_free(ss->ciphers); #ifndef OPENSSL_NO_TLSEXT - if (ss->tlsext_hostname != NULL) OPENSSL_free(ss->tlsext_hostname); if (ss->tlsext_tick != NULL) OPENSSL_free(ss->tlsext_tick); #ifndef OPENSSL_NO_EC ss->tlsext_ecpointformatlist_length = 0; Index: ssl/t1_lib.c =================================================================== RCS file: /openssl-cvs/openssl/ssl/t1_lib.c,v retrieving revision 1.64.2.12 diff -p -u -r1.64.2.12 t1_lib.c --- ssl/t1_lib.c 7 Jan 2010 19:05:03 -0000 1.64.2.12 +++ ssl/t1_lib.c 9 Jan 2010 07:28:24 -0000 @@ -513,7 +513,7 @@ unsigned char *ssl_add_serverhello_tlsex ret+=2; if (ret>=limit) return NULL; /* this really never occurs, but ... */ - if (!s->hit && s->servername_done == 1 && s->session->tlsext_hostname != NULL) + if (s->tlsext_hostname) { if ((long)(limit - ret - 4) < 0) return NULL; @@ -633,7 +633,6 @@ int ssl_parse_clienthello_tlsext(SSL *s, unsigned char *data = *p; int renegotiate_seen = 0; - s->servername_done = 0; s->tlsext_status_type = -1; if (data >= (d+n-2)) @@ -656,103 +655,16 @@ int ssl_parse_clienthello_tlsext(SSL *s, if (s->tlsext_debug_cb) s->tlsext_debug_cb(s, 0, type, data, size, s->tlsext_debug_arg); -/* The servername extension is treated as follows: - - Only the hostname type is supported with a maximum length of 255. - - The servername is rejected if too long or if it contains zeros, - in which case an fatal alert is generated. - - The servername field is maintained together with the session cache. - - When a session is resumed, the servername call back invoked in order - to allow the application to position itself to the right context. - - The servername is acknowledged if it is new for a session or when - it is identical to a previously used for the same session. - Applications can control the behaviour. They can at any time - set a 'desirable' servername for a new SSL object. This can be the - case for example with HTTPS when a Host: header field is received and - a renegotiation is requested. In this case, a possible servername - presented in the new client hello is only acknowledged if it matches - the value of the Host: field. - - Applications must use SSL_OP_NO_SESSION_RESUMPTION_ON_RENEGOTIATION - if they provide for changing an explicit servername context for the session, - i.e. when the session has been established with a servername extension. - - On session reconnect, the servername extension may be absent. - -*/ - - if (type == TLSEXT_TYPE_server_name) + if (type == TLSEXT_TYPE_session_ticket) { - unsigned char *sdata; - int servname_type; - int dsize; - - if (size < 2) - { - *al = SSL_AD_DECODE_ERROR; - return 0; - } - n2s(data,dsize); - size -= 2; - if (dsize > size ) - { - *al = SSL_AD_DECODE_ERROR; - return 0; - } - - sdata = data; - while (dsize > 3) - { - servname_type = *(sdata++); - n2s(sdata,len); - dsize -= 3; - - if (len > dsize) - { - *al = SSL_AD_DECODE_ERROR; - return 0; - } - if (s->servername_done == 0) - switch (servname_type) - { - case TLSEXT_NAMETYPE_host_name: - if (s->session->tlsext_hostname == NULL) - { - if (len > TLSEXT_MAXLEN_host_name || - ((s->session->tlsext_hostname = OPENSSL_malloc(len+1)) == NULL)) - { - *al = TLS1_AD_UNRECOGNIZED_NAME; - return 0; - } - memcpy(s->session->tlsext_hostname, sdata, len); - s->session->tlsext_hostname[len]='\0'; - if (strlen(s->session->tlsext_hostname) != len) { - OPENSSL_free(s->session->tlsext_hostname); - s->session->tlsext_hostname = NULL; - *al = TLS1_AD_UNRECOGNIZED_NAME; - return 0; - } - s->servername_done = 1; - - } - else - s->servername_done = strlen(s->session->tlsext_hostname) == len - && strncmp(s->session->tlsext_hostname, (char *)sdata, len) == 0; - - break; - - default: - break; - } - - dsize -= len; - } - if (dsize != 0) + if (s->tls_session_ticket_ext_cb && + !s->tls_session_ticket_ext_cb(s, data, size, s->tls_session_ticket_ext_cb_arg)) { - *al = SSL_AD_DECODE_ERROR; + *al = TLS1_AD_INTERNAL_ERROR; return 0; } - } - #ifndef OPENSSL_NO_EC else if (type == TLSEXT_TYPE_ec_point_formats && s->version != DTLS1_VERSION) @@ -843,15 +755,6 @@ int ssl_parse_clienthello_tlsext(SSL *s, } } #endif - else if (type == TLSEXT_TYPE_session_ticket) - { - if (s->tls_session_ticket_ext_cb && - !s->tls_session_ticket_ext_cb(s, data, size, s->tls_session_ticket_ext_cb_arg)) - { - *al = TLS1_AD_INTERNAL_ERROR; - return 0; - } - } else if (type == TLSEXT_TYPE_renegotiate) { if(!ssl_parse_clienthello_renegotiate_ext(s, data, size, al)) @@ -987,7 +890,6 @@ int ssl_parse_serverhello_tlsext(SSL *s, unsigned short size; unsigned short len; unsigned char *data = *p; - int tlsext_servername = 0; int renegotiate_seen = 0; if (data >= (d+n-2)) @@ -1014,7 +916,6 @@ int ssl_parse_serverhello_tlsext(SSL *s, *al = TLS1_AD_UNRECOGNIZED_NAME; return 0; } - tlsext_servername = 1; } #ifndef OPENSSL_NO_EC @@ -1125,27 +1026,6 @@ int ssl_parse_serverhello_tlsext(SSL *s, return 0; } - if (!s->hit && tlsext_servername == 1) - { - if (s->tlsext_hostname) - { - if (s->session->tlsext_hostname == NULL) - { - s->session->tlsext_hostname = BUF_strdup(s->tlsext_hostname); - if (!s->session->tlsext_hostname) - { - *al = SSL_AD_UNRECOGNIZED_NAME; - return 0; - } - } - else - { - *al = SSL_AD_DECODE_ERROR; - return 0; - } - } - } - *p = data; ri_check: @@ -1306,11 +1186,6 @@ int ssl_check_clienthello_tlsext(SSL *s) */ #endif - if (s->ctx != NULL && s->ctx->tlsext_servername_callback != 0) - ret = s->ctx->tlsext_servername_callback(s, &al, s->ctx->tlsext_servername_arg); - else if (s->initial_ctx != NULL && s->initial_ctx->tlsext_servername_callback != 0) - ret = s->initial_ctx->tlsext_servername_callback(s, &al, s->initial_ctx->tlsext_servername_arg); - /* If status request then ask callback what to do. * Note: this must be called after servername callbacks in case * the certificate has changed. @@ -1410,10 +1285,8 @@ int ssl_check_clienthello_tlsext(SSL *s) ssl3_send_alert(s,SSL3_AL_WARNING,al); return 1; - case SSL_TLSEXT_ERR_NOACK: - s->servername_done=0; - default: - return 1; + default: + return 1; } } @@ -1525,30 +1398,25 @@ int ssl_check_serverhello_tlsext(SSL *s) ssl3_send_alert(s,SSL3_AL_WARNING,al); return 1; - case SSL_TLSEXT_ERR_NOACK: - s->servername_done=0; - default: - return 1; + default: + return 1; } } /* Since the server cache lookup is done early on in the processing of client - * hello and other operations depend on the result we need to handle any TLS - * session ticket extension at the same time. + * hello and other operations depend on the result we need to handle the + * TLS SNI and session ticket extensions at the same time. */ -int tls1_process_ticket(SSL *s, unsigned char *session_id, int len, - const unsigned char *limit, SSL_SESSION **ret) +int tls1_process_servername_and_ticket(SSL *s, unsigned char *session_id, + int len, const unsigned char *limit, + SSL_SESSION **ret) { /* Point after session ID in client hello */ const unsigned char *p = session_id + len; - unsigned short i; - - /* If tickets disabled behave as if no ticket present - * to permit stateful resumption. - */ - if (SSL_get_options(s) & SSL_OP_NO_TICKET) - return 1; + const unsigned char *ticket = NULL; + unsigned short i, ticket_size = 0; + int r = SSL_TLSEXT_ERR_OK, al = TLS1_AD_UNRECOGNIZED_NAME; if ((s->version <= SSL3_VERSION) || !limit) return 1; @@ -1582,37 +1450,146 @@ int tls1_process_ticket(SSL *s, unsigned n2s(p, type); n2s(p, size); if (p + size > limit) - return 1; + break; if (type == TLSEXT_TYPE_session_ticket) { - /* If tickets disabled indicate cache miss which will - * trigger a full handshake - */ - if (SSL_get_options(s) & SSL_OP_NO_TICKET) - return 1; /* If zero length note client will accept a ticket * and indicate cache miss to trigger full handshake */ if (size == 0) { s->tlsext_ticket_expected = 1; - return 0; /* Cache miss */ } - if (s->tls_session_secret_cb) + else if (!s->tls_session_secret_cb) { - /* Indicate cache miss here and instead of - * generating the session from ticket now, - * trigger abbreviated handshake based on - * external mechanism to calculate the master - * secret later. */ - return 0; + ticket = p; + ticket_size = size; } - return tls_decrypt_ticket(s, p, size, session_id, len, - ret); + } + /* The servername extension is treated as follows: + * - Only the hostname type is supported with a maximum length + * of 255. + * - Only the first entry in the server_name_list is parsed. + * - The servername is rejected if too long or if it contains + * zeros, in which case a fatal alert is generated. + * - When a session is resumed, the servername call back + * is invoked before decrypting the ticket, to allow + * the application to position itself to the right context. + * - On session resumption, the servername extension must + * be present, as suggested/recommended by RFC 5246, + * section 7.4.1.4. + */ + else if (type == TLSEXT_TYPE_server_name) + { + const unsigned char *sdata; + int servname_type; + int dsize; + int slen; + + if (size < 2) + { + al = TLS1_AD_DECODE_ERROR; + goto err; + } + n2s(p,dsize); + size -= 2; + if (dsize > size ) + { + al = TLS1_AD_DECODE_ERROR; + goto err; + } + + sdata = p; + while (dsize > 3) + { + servname_type = *(sdata++); + n2s(sdata,slen); + dsize -= 3; + + if (slen > dsize) + { + al = TLS1_AD_DECODE_ERROR; + goto err; + } + switch (servname_type) + { + case TLSEXT_NAMETYPE_host_name: + if (slen > TLSEXT_MAXLEN_host_name) + { + al = TLS1_AD_UNRECOGNIZED_NAME; + goto err; + } + if (s->tlsext_hostname) + { + OPENSSL_free(s->tlsext_hostname); + s->tlsext_hostname = NULL; + } + if ((s->tlsext_hostname = OPENSSL_malloc(slen+1)) == NULL) + { + al = TLS1_AD_INTERNAL_ERROR; + goto err; + } + memcpy(s->tlsext_hostname, sdata, slen); + s->tlsext_hostname[slen]='\0'; + if (strlen(s->tlsext_hostname) != slen) + { + OPENSSL_free(s->tlsext_hostname); + s->tlsext_hostname = NULL; + al = TLS1_AD_UNRECOGNIZED_NAME; + goto err; + } + /* tlsext_hostname has been + * successfully set, so skip + * the remainder of the SNI + * extension + */ + slen = dsize; + break; + + default: + break; + } + + dsize -= slen; + } + if (dsize != 0) + { + al = TLS1_AD_DECODE_ERROR; + goto err; + } } p += size; } - return 1; + + if (s->ctx && s->ctx->tlsext_servername_callback) + r = s->ctx->tlsext_servername_callback(s, &al, s->ctx->tlsext_servername_arg); + else if (s->initial_ctx && s->initial_ctx->tlsext_servername_callback) + r = s->initial_ctx->tlsext_servername_callback(s, &al, s->initial_ctx->tlsext_servername_arg); + + switch (r) + { + case SSL_TLSEXT_ERR_ALERT_WARNING: + ssl3_send_alert(s, SSL3_AL_WARNING, + TLS1_AD_UNRECOGNIZED_NAME); + break; + + case SSL_TLSEXT_ERR_ALERT_FATAL: + goto err; + + default: + break; + } + + if (ticket && ticket_size) + return tls_decrypt_ticket(s, ticket, ticket_size, session_id, + len, ret); + else + return (s->tlsext_ticket_expected || s->tls_session_secret_cb) ? + 0 : 1; + +err: + ssl3_send_alert(s, SSL3_AL_FATAL, al); + return -1; } static int tls_decrypt_ticket(SSL *s, const unsigned char *etick, int eticklen,