Skip to content
Snippets Groups Projects
Commit 372b2a05 authored by Henrik (Grubba) Grubbström's avatar Henrik (Grubba) Grubbström
Browse files

SSL.ServerConnection: Session tickets (RFC 4507 and RFC 5077).

Server side support for session tickets.

Note that the default ticket encoding is to use the session_id,
it thus uses server side state. The ticket encoding can be changed
by overriding {en,de}code_ticket() in SSL.Context.

Implementation verified against OpenSSL's s_client.
parent 175f8fa2
Branches
Tags
No related merge requests found
...@@ -79,6 +79,8 @@ string(8bit) server_random; ...@@ -79,6 +79,8 @@ string(8bit) server_random;
private constant Packet = .Packet; private constant Packet = .Packet;
private constant Alert = .Alert; private constant Alert = .Alert;
int(0..3) tickets_enabled = 0;
// RFC 7301 (ALPN) 3.1: // RFC 7301 (ALPN) 3.1:
// Unlike many other TLS extensions, this extension does not establish // Unlike many other TLS extensions, this extension does not establish
// properties of the session, only of the connection. When session // properties of the session, only of the connection. When session
......
...@@ -150,6 +150,9 @@ Alert alert_factory(object con, ...@@ -150,6 +150,9 @@ Alert alert_factory(object con,
//! Allows the client to select which of several domains hosted on //! Allows the client to select which of several domains hosted on
//! the same server it wants to connect to. Required by many //! the same server it wants to connect to. Required by many
//! websites (@rfc{6066:3@}). //! 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 //! @value Constants.EXTENSION_next_protocol_negotiation
//! Not supported by Pike. The server side will just check that //! Not supported by Pike. The server side will just check that
//! the client packets are correctly formatted. //! the client packets are correctly formatted.
...@@ -190,6 +193,7 @@ multiset(int) extensions = (< ...@@ -190,6 +193,7 @@ multiset(int) extensions = (<
EXTENSION_signature_algorithms, EXTENSION_signature_algorithms,
EXTENSION_elliptic_curves, EXTENSION_elliptic_curves,
EXTENSION_server_name, EXTENSION_server_name,
EXTENSION_session_ticket,
EXTENSION_next_protocol_negotiation, EXTENSION_next_protocol_negotiation,
EXTENSION_signed_certificate_timestamp, EXTENSION_signed_certificate_timestamp,
EXTENSION_early_data, EXTENSION_early_data,
...@@ -1100,6 +1104,64 @@ Session lookup_session(string id) ...@@ -1100,6 +1104,64 @@ Session lookup_session(string id)
return 0; 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. //! Create a new session.
Session new_session() Session new_session()
{ {
...@@ -1118,7 +1180,7 @@ Session new_session() ...@@ -1118,7 +1180,7 @@ Session new_session()
//! Add a session to the cache (if caching is enabled). //! Add a session to the cache (if caching is enabled).
void record_session(Session s) void record_session(Session s)
{ {
if (use_cache && s->identity) if (use_cache && sizeof(s->identity||""))
{ {
if( sizeof(session_cache) > max_sessions ) if( sizeof(session_cache) > max_sessions )
{ {
......
...@@ -109,6 +109,12 @@ protected Packet server_hello_packet() ...@@ -109,6 +109,12 @@ protected Packet server_hello_packet()
return Buffer(); 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) ext (EXTENSION_application_layer_protocol_negotiation, !!application_protocol)
{ {
return Buffer()->add_string_array(({application_protocol}), 1, 2); return Buffer()->add_string_array(({application_protocol}), 1, 2);
...@@ -171,6 +177,17 @@ protected Packet certificate_request_packet(Context context) ...@@ -171,6 +177,17 @@ protected Packet certificate_request_packet(Context context)
return handshake_packet(HANDSHAKE_certificate_request, struct); 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). //! Renegotiate the connection (server initiated).
//! //!
//! Sends a @[hello_request] to force a new round of handshaking. //! 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) ...@@ -313,7 +330,7 @@ int(-1..1) handle_handshake(int type, Buffer input, Stdio.Buffer raw)
if (type != HANDSHAKE_client_hello) if (type != HANDSHAKE_client_hello)
COND_FATAL(1, ALERT_unexpected_message, "Expected client_hello.\n"); COND_FATAL(1, ALERT_unexpected_message, "Expected client_hello.\n");
string session_id; string(8bit) session_id;
int cipher_len; int cipher_len;
array(int) cipher_suites; array(int) cipher_suites;
array(int) compression_methods; array(int) compression_methods;
...@@ -556,6 +573,15 @@ int(-1..1) handle_handshake(int type, Buffer input, Stdio.Buffer raw) ...@@ -556,6 +573,15 @@ int(-1..1) handle_handshake(int type, Buffer input, Stdio.Buffer raw)
"Invalid NPN extension.\n"); "Invalid NPN extension.\n");
break; 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: case EXTENSION_signed_certificate_timestamp:
COND_FATAL(sizeof(extension_data), ALERT_handshake_failure, COND_FATAL(sizeof(extension_data), ALERT_handshake_failure,
"Invalid signed certificate timestamp extension.\n"); "Invalid signed certificate timestamp extension.\n");
...@@ -788,17 +814,41 @@ int(-1..1) handle_handshake(int type, Buffer input, Stdio.Buffer raw) ...@@ -788,17 +814,41 @@ int(-1..1) handle_handshake(int type, Buffer input, Stdio.Buffer raw)
ALERT_illegal_parameter, ALERT_illegal_parameter,
"Illegal with compression in TLS 1.3 and later.\n"); "Illegal with compression in TLS 1.3 and later.\n");
Session old_session = sizeof(session_id) && Session old_session;
context->lookup_session(session_id); if (tickets_enabled) {
if (old_session && old_session->reusable_as(session)) SSL3_DEBUG_MSG("SSL.ServerConnection: Decoding ticket: %O...\n",
{ session->ticket);
SSL3_DEBUG_MSG("SSL.ServerConnection: Reusing session %O\n", old_session = sizeof(session->ticket) &&
session_id); context->decode_ticket(session->ticket);
/* Reuse session */ // RFC 4507 3.4:
session = old_session; // If a server is planning on issuing a SessionTicket to a
reuse = 1; // 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. /* TLS 1.3 or later.
* *
...@@ -871,12 +921,36 @@ int(-1..1) handle_handshake(int type, Buffer input, Stdio.Buffer raw) ...@@ -871,12 +921,36 @@ int(-1..1) handle_handshake(int type, Buffer input, Stdio.Buffer raw)
session->set_compression_method(compression_methods[0]); 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 #ifdef SSL3_DEBUG
if (sizeof(session_id)) if (sizeof(session_id))
werror("SSL.ServerConnection: Looking up session %O\n", session_id); werror("SSL.ServerConnection: Looking up session %O\n", session_id);
#endif #endif
Session old_session = sizeof(session_id) && old_session = sizeof(session_id) &&
context->lookup_session(session_id); context->lookup_session(session_id);
}
if (old_session && old_session->reusable_as(session)) if (old_session && old_session->reusable_as(session))
{ {
SSL3_DEBUG_MSG("SSL.ServerConnection: Reusing session %O\n", 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) ...@@ -886,6 +960,13 @@ int(-1..1) handle_handshake(int type, Buffer input, Stdio.Buffer raw)
session = old_session; session = old_session;
send_packet(server_hello_packet()); 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(); new_cipher_states();
send_packet(change_cipher_packet()); send_packet(change_cipher_packet());
if(version == PROTOCOL_SSL_3_0) if(version == PROTOCOL_SSL_3_0)
...@@ -1180,6 +1261,18 @@ int(-1..1) handle_handshake(int type, Buffer input, Stdio.Buffer raw) ...@@ -1180,6 +1261,18 @@ int(-1..1) handle_handshake(int type, Buffer input, Stdio.Buffer raw)
if (!reuse) if (!reuse)
{ {
if (version < PROTOCOL_TLS_1_3) { 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()); send_packet(change_cipher_packet());
// We've already received the CCS from the peer. // We've already received the CCS from the peer.
expect_change_cipher--; expect_change_cipher--;
......
...@@ -24,6 +24,14 @@ int last_activity = time(); ...@@ -24,6 +24,14 @@ int last_activity = time();
//! Identifies the session to the server //! Identifies the session to the server
string(8bit) identity; 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. //! Always COMPRESSION_null.
int compression_algorithm; int compression_algorithm;
......
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Please register or to comment