diff --git a/lib/modules/SSL.pmod/Connection.pike b/lib/modules/SSL.pmod/Connection.pike index 59923928add4ae507abf12d4e5e4726091b7e9c6..d193c1f405e6fffa09d9cb3ba4846734e867521b 100644 --- a/lib/modules/SSL.pmod/Connection.pike +++ b/lib/modules/SSL.pmod/Connection.pike @@ -79,6 +79,8 @@ string(8bit) server_random; private constant Packet = .Packet; private constant Alert = .Alert; +int(0..3) tickets_enabled = 0; + // RFC 7301 (ALPN) 3.1: // Unlike many other TLS extensions, this extension does not establish // properties of the session, only of the connection. When session diff --git a/lib/modules/SSL.pmod/Context.pike b/lib/modules/SSL.pmod/Context.pike index ad265a48c1febe74a286ee411bfee21837ad2e9e..5b232a6ab7af526ae755fa3f94f89642d8d05027 100644 --- a/lib/modules/SSL.pmod/Context.pike +++ b/lib/modules/SSL.pmod/Context.pike @@ -150,6 +150,9 @@ Alert alert_factory(object con, //! Allows the client to select which of several domains hosted on //! the same server it wants to connect to. Required by many //! websites (@rfc{6066:3@}). +//! @value Constants.EXTENSION_session_ticket +//! Support session resumption without server-side state +//! (@rfc{4507@} and @rfc{5077@}). //! @value Constants.EXTENSION_next_protocol_negotiation //! Not supported by Pike. The server side will just check that //! the client packets are correctly formatted. @@ -190,6 +193,7 @@ multiset(int) extensions = (< EXTENSION_signature_algorithms, EXTENSION_elliptic_curves, EXTENSION_server_name, + EXTENSION_session_ticket, EXTENSION_next_protocol_negotiation, EXTENSION_signed_certificate_timestamp, EXTENSION_early_data, @@ -1100,6 +1104,64 @@ Session lookup_session(string id) return 0; } +//! Decode a session ticket and return the corresponding session +//! if valid or zero if invalid. +//! +//! @note +//! The default implementation just calls @[lookup_session()]. +//! +//! Override this function (and @[encode_ticket()]) to implement +//! server-side state-less session resumption. +//! +//! @seealso +//! @[encode_ticket()], @[lookup_session()] +Session decode_ticket(string(8bit) ticket) +{ + return lookup_session(ticket); +} + +//! Generate a session ticket for a session. +//! +//! @note +//! The default implementation just generates a random ticket +//! and calls @[record_session()] to store it. +//! +//! Over-ride this function (and @[decode_ticket()]) to implement +//! server-side state-less session resumption. +//! +//! @returns +//! Returns @expr{0@} (zero) on failure (ie cache disabled), and +//! an array on success: +//! @array +//! @elem string(8bit) 0 +//! Non-empty string with the ticket. +//! @elem int +//! Lifetime hint for the ticket. +//! @endarray +//! +//! @seealso +//! @[decode_ticket()], @[record_session()], @rfc{4507:3.3@} +array(string(8bit)|int) encode_ticket(Session session) +{ + if (!use_cache) return 0; + string(8bit) ticket = session->ticket; + if (!sizeof(ticket||"")) { + do { + ticket = random(32); + } while(session_cache[ticket]); + // FIXME: Should we update the fields here? + // Consider moving this to the caller. + session->ticket = ticket; + session->ticket_expiry_time = time(1) + 3600; + } + string(8bit) orig_id = session->identity; + session->identity = ticket; + record_session(session); + session->identity = orig_id; + // FIXME: Calculate the lifetime from the ticket_expiry_time field? + return ({ ticket, 3600 }); +} + //! Create a new session. Session new_session() { @@ -1118,7 +1180,7 @@ Session new_session() //! Add a session to the cache (if caching is enabled). void record_session(Session s) { - if (use_cache && s->identity) + if (use_cache && sizeof(s->identity||"")) { if( sizeof(session_cache) > max_sessions ) { diff --git a/lib/modules/SSL.pmod/ServerConnection.pike b/lib/modules/SSL.pmod/ServerConnection.pike index 0faab53f1937cf5cc7a8d6a2bbbdcbd87d99270a..4fecdf6060c3a8799a84dd8a174b7a16ecda4743 100644 --- a/lib/modules/SSL.pmod/ServerConnection.pike +++ b/lib/modules/SSL.pmod/ServerConnection.pike @@ -109,6 +109,12 @@ protected Packet server_hello_packet() return Buffer(); }; + ext (EXTENSION_session_ticket, tickets_enabled) { + // RFC 4507 and RFC 5077. + SSL3_DEBUG_MSG("SSL.ServerConnection: Accepting session tickets.\n"); + return Buffer(); + }; + ext (EXTENSION_application_layer_protocol_negotiation, !!application_protocol) { return Buffer()->add_string_array(({application_protocol}), 1, 2); @@ -171,6 +177,17 @@ protected Packet certificate_request_packet(Context context) return handshake_packet(HANDSHAKE_certificate_request, struct); } +protected Packet new_session_ticket_packet(int lifetime_hint, + string(8bit) ticket) +{ + SSL3_DEBUG_MSG("SSL.ServerConnection: New session ticket.\n"); + Buffer struct = Buffer(); + if (lifetime_hint < 0) lifetime_hint = 0; + struct->add_int(lifetime_hint, 4); + struct->add_hstring(ticket, 2); + return handshake_packet(HANDSHAKE_new_session_ticket, struct); +} + //! Renegotiate the connection (server initiated). //! //! Sends a @[hello_request] to force a new round of handshaking. @@ -313,7 +330,7 @@ int(-1..1) handle_handshake(int type, Buffer input, Stdio.Buffer raw) if (type != HANDSHAKE_client_hello) COND_FATAL(1, ALERT_unexpected_message, "Expected client_hello.\n"); - string session_id; + string(8bit) session_id; int cipher_len; array(int) cipher_suites; array(int) compression_methods; @@ -556,6 +573,15 @@ int(-1..1) handle_handshake(int type, Buffer input, Stdio.Buffer raw) "Invalid NPN extension.\n"); break; + case EXTENSION_session_ticket: + SSL3_DEBUG_MSG("SSL.ServerConnection: Got session ticket.\n"); + tickets_enabled = 1; + // NB: RFC 4507 and 5077 differ in encoding here. + // Apparently no implementations actually followed + // the RFC 4507 encoding. + session->ticket = extension_data->read(); + break; + case EXTENSION_signed_certificate_timestamp: COND_FATAL(sizeof(extension_data), ALERT_handshake_failure, "Invalid signed certificate timestamp extension.\n"); @@ -788,17 +814,41 @@ int(-1..1) handle_handshake(int type, Buffer input, Stdio.Buffer raw) ALERT_illegal_parameter, "Illegal with compression in TLS 1.3 and later.\n"); - Session old_session = sizeof(session_id) && - context->lookup_session(session_id); - if (old_session && old_session->reusable_as(session)) - { - SSL3_DEBUG_MSG("SSL.ServerConnection: Reusing session %O\n", - session_id); - - /* Reuse session */ - session = old_session; - reuse = 1; - } + Session old_session; + if (tickets_enabled) { + SSL3_DEBUG_MSG("SSL.ServerConnection: Decoding ticket: %O...\n", + session->ticket); + old_session = sizeof(session->ticket) && + context->decode_ticket(session->ticket); + + // RFC 4507 3.4: + // If a server is planning on issuing a SessionTicket to a + // client that does not present one, it SHOULD include an + // empty Session ID in the ServerHello. If the server + // includes a non-empty session ID, then it is indicating + // intent to use stateful session resume. + //[...] + // If the server accepts the ticket and the Session ID is + // not empty, then it MUST respond with the same Session + // ID present in the ClientHello. + + session->identity = ""; + if (old_session) { + old_session->identity = session_id; + } + } else { + old_session = sizeof(session_id) && + context->lookup_session(session_id); + } + if (old_session && old_session->reusable_as(session)) + { + SSL3_DEBUG_MSG("SSL.ServerConnection: Reusing session %O\n", + session_id); + + /* Reuse session */ + session = old_session; + reuse = 1; + } /* TLS 1.3 or later. * @@ -871,12 +921,36 @@ int(-1..1) handle_handshake(int type, Buffer input, Stdio.Buffer raw) session->set_compression_method(compression_methods[0]); + Session old_session; + if (tickets_enabled) { + SSL3_DEBUG_MSG("SSL.ServerConnection: Decoding ticket: %O...\n", + session->ticket); + old_session = sizeof(session->ticket) && + context->decode_ticket(session->ticket); + + // RFC 4507 3.4: + // If a server is planning on issuing a SessionTicket to a + // client that does not present one, it SHOULD include an + // empty Session ID in the ServerHello. If the server + // includes a non-empty session ID, then it is indicating + // intent to use stateful session resume. + //[...] + // If the server accepts the ticket and the Session ID is + // not empty, then it MUST respond with the same Session + // ID present in the ClientHello. + + session->identity = ""; + if (old_session) { + old_session->identity = session_id; + } + } else { #ifdef SSL3_DEBUG - if (sizeof(session_id)) - werror("SSL.ServerConnection: Looking up session %O\n", session_id); + if (sizeof(session_id)) + werror("SSL.ServerConnection: Looking up session %O\n", session_id); #endif - Session old_session = sizeof(session_id) && - context->lookup_session(session_id); + old_session = sizeof(session_id) && + context->lookup_session(session_id); + } if (old_session && old_session->reusable_as(session)) { SSL3_DEBUG_MSG("SSL.ServerConnection: Reusing session %O\n", @@ -886,6 +960,13 @@ int(-1..1) handle_handshake(int type, Buffer input, Stdio.Buffer raw) session = old_session; send_packet(server_hello_packet()); + if (tickets_enabled) { + SSL3_DEBUG_MSG("SSL.ServerConnection: Resending ticket.\n"); + int lifetime_hint = [int](session->ticket_expiration_time - time(1)); + string(8bit) ticket = session->ticket; + send_packet(new_session_ticket_packet(lifetime_hint, ticket)); + } + new_cipher_states(); send_packet(change_cipher_packet()); if(version == PROTOCOL_SSL_3_0) @@ -1180,6 +1261,18 @@ int(-1..1) handle_handshake(int type, Buffer input, Stdio.Buffer raw) if (!reuse) { if (version < PROTOCOL_TLS_1_3) { + if (tickets_enabled) { + array(string(8bit)|int) ticket_info = + context->encode_ticket(session); + if (ticket_info) { + SSL3_DEBUG_MSG("SSL.ServerConnection: Sending ticket %O.\n", + ticket_info); + session->ticket = [string(8bit)](ticket_info[0]); + session->ticket_expiry_time = [int](ticket_info[1] + time(1)); + send_packet(new_session_ticket_packet([int](ticket_info[1]), + [string(8bit)](ticket_info[0]))); + } + } send_packet(change_cipher_packet()); // We've already received the CCS from the peer. expect_change_cipher--; diff --git a/lib/modules/SSL.pmod/Session.pike b/lib/modules/SSL.pmod/Session.pike index 5aeb12e946c47cc0b6a24fd344caf52420fb78fe..54b1812f7e2d8a3fe339bed46ce871546694f5b2 100644 --- a/lib/modules/SSL.pmod/Session.pike +++ b/lib/modules/SSL.pmod/Session.pike @@ -24,6 +24,14 @@ int last_activity = time(); //! Identifies the session to the server string(8bit) identity; +//! Alternative identification of the session to the server. +//! @seealso +//! @rfc{4507@}, @rfc{5077@} +string(8bit) ticket; + +//! Expiry time for @[ticket]. +int ticket_expiry_time; + //! Always COMPRESSION_null. int compression_algorithm;