From: Saša Nedvědický Date: Tue, 5 Nov 2024 21:15:55 +0000 (-0500) Subject: Implement Server Address validation using retry packets X-Git-Tag: openssl-3.5.0-alpha1~328 X-Git-Url: http://git.ipfire.org/cgi-bin/gitweb.cgi?a=commitdiff_plain;h=6ba0457c926e19928d39e4800d7f929bc86f525f;p=thirdparty%2Fopenssl.git Implement Server Address validation using retry packets RFC 9000 describes a method for preforming server address validation on QUIC using retry packets. Based on: https://datatracker.ietf.org/doc/html/rfc9000#section-17.2.5.2 We do the following: 1) Client sends an Initial packet without a retry token 2) Server abandons the initial packet and responds with a retry frame which includes a retry token and integrity tag and new SCID 3) Client send the initial packet again, updating the encryption keys for the connection based on the SCID sent in (2), using it as the new DCID, including the retry token/tag provided in (2). 4) Server validates the token in (3) and creates a new connection using the updated DCID from the client to generate its encryption keys Reviewed-by: Neil Horman Reviewed-by: Tomas Mraz (Merged from https://github.com/openssl/openssl/pull/25890) --- diff --git a/include/internal/quic_channel.h b/include/internal/quic_channel.h index 2a741f52cf3..c01d4a025e0 100644 --- a/include/internal/quic_channel.h +++ b/include/internal/quic_channel.h @@ -448,6 +448,10 @@ uint64_t ossl_quic_channel_get_max_idle_timeout_peer_request(const QUIC_CHANNEL /* Get the idle timeout actually negotiated. */ uint64_t ossl_quic_channel_get_max_idle_timeout_actual(const QUIC_CHANNEL *ch); +int ossl_quic_bind_channel(QUIC_CHANNEL *ch, const BIO_ADDR *peer, + const QUIC_CONN_ID *scid, const QUIC_CONN_ID *dcid, + const QUIC_CONN_ID *odcid); + # endif #endif diff --git a/include/internal/quic_lcidm.h b/include/internal/quic_lcidm.h index 4911e042302..f01b985d004 100644 --- a/include/internal/quic_lcidm.h +++ b/include/internal/quic_lcidm.h @@ -252,6 +252,18 @@ int ossl_quic_lcidm_debug_add(QUIC_LCIDM *lcidm, void *opaque, const QUIC_CONN_ID *lcid, uint64_t seq_num); +/* + * Obtain a local connection id which is not used yet. + * Returns 1 on succes, 0 on failure. + */ +int ossl_quic_lcidm_get_unused_cid(QUIC_LCIDM *lcidm, QUIC_CONN_ID *cid); + +/* + * Attempts to bind channel to connection id specified in `lcid`. + * This should be connection ID we generated during client validation. + */ +int ossl_quic_lcidm_bind_channel(QUIC_LCIDM *lcidm, void *opaque, + const QUIC_CONN_ID *lcid); # endif #endif diff --git a/ssl/quic/quic_channel.c b/ssl/quic/quic_channel.c index 02c1213cd57..49ae84d2cd1 100644 --- a/ssl/quic/quic_channel.c +++ b/ssl/quic/quic_channel.c @@ -1736,6 +1736,21 @@ static int ch_generate_transport_params(QUIC_CHANNEL *ch) WPACKET wpkt; int wpkt_valid = 0; size_t buf_len = 0; + QUIC_CONN_ID *id_to_use = NULL; + + /* + * We need to select which connection id to encode in the + * QUIC_TPARAM_ORIG_DCID transport parameter + * If we have an odcid, then this connection was established + * in response to a retry request, and we need to use the connection + * id sent in the first initial packet. + * If we don't have an odcid, then this connection was established + * without a retry and the init_dcid is the connection we should use + */ + if (ch->odcid.id_len == 0) + id_to_use = &ch->init_dcid; + else + id_to_use = &ch->odcid; if (ch->local_transport_params != NULL || ch->got_local_transport_params) goto err; @@ -3382,18 +3397,24 @@ static void ch_on_idle_timeout(QUIC_CHANNEL *ch) ch_record_state_transition(ch, QUIC_CHANNEL_STATE_TERMINATED); } -/* Called when we, as a server, get a new incoming connection. */ -int ossl_quic_channel_on_new_conn(QUIC_CHANNEL *ch, const BIO_ADDR *peer, - const QUIC_CONN_ID *peer_scid, - const QUIC_CONN_ID *peer_dcid) +/** + * @brief Common handler for initializing a new QUIC connection. + * + * This function configures a QUIC channel (`QUIC_CHANNEL *ch`) for a new + * connection by setting the peer address, connection IDs, and necessary + * callbacks. It establishes initial secrets, sets up logging, and performs + * required transitions for the channel state. + * + * @param ch Pointer to the QUIC channel being initialized. + * @param peer Address of the peer to which the channel connects. + * @param peer_scid Peer-specified source connection ID. + * @param peer_dcid Peer-specified destination connection ID. + * @return 1 on success, 0 on failure to set required elements. + */ +static int ch_on_new_conn_common(QUIC_CHANNEL *ch, const BIO_ADDR *peer, + const QUIC_CONN_ID *peer_scid, + const QUIC_CONN_ID *peer_dcid) { - if (!ossl_assert(ch->state == QUIC_CHANNEL_STATE_IDLE && ch->is_server)) - return 0; - - /* Generate an Initial LCID we will use for the connection. */ - if (!ossl_quic_lcidm_generate_initial(ch->lcidm, ch, &ch->cur_local_cid)) - return 0; - /* Note our newly learnt peer address and CIDs. */ ch->cur_peer_addr = *peer; ch->init_dcid = *peer_dcid; @@ -3432,6 +3453,43 @@ int ossl_quic_channel_on_new_conn(QUIC_CHANNEL *ch, const BIO_ADDR *peer, return 1; } +/* Called when we, as a server, get a new incoming connection. */ +int ossl_quic_channel_on_new_conn(QUIC_CHANNEL *ch, const BIO_ADDR *peer, + const QUIC_CONN_ID *peer_scid, + const QUIC_CONN_ID *peer_dcid) +{ + if (!ossl_assert(ch->state == QUIC_CHANNEL_STATE_IDLE && ch->is_server)) + return 0; + + /* Generate an Initial LCID we will use for the connection. */ + if (!ossl_quic_lcidm_generate_initial(ch->lcidm, ch, &ch->cur_local_cid)) + return 0; + + return ch_on_new_conn_common(ch, peer, peer_scid, peer_dcid); +} + +int ossl_quic_bind_channel(QUIC_CHANNEL *ch, const BIO_ADDR *peer, + const QUIC_CONN_ID *peer_scid, + const QUIC_CONN_ID *peer_dcid, + const QUIC_CONN_ID *peer_odcid) +{ + if (peer_dcid == NULL) + return 0; + + if (!ossl_assert(ch->state == QUIC_CHANNEL_STATE_IDLE && ch->is_server)) + return 0; + + ch->cur_local_cid = *peer_dcid; + if (!ossl_quic_lcidm_bind_channel(ch->lcidm, ch, peer_dcid)) + return 0; + + /* + * peer_odcid <=> is initial dst conn id chosen by peer in its + * first initial packet we received without token. + */ + return ch_on_new_conn_common(ch, peer, peer_scid, peer_odcid); +} + SSL *ossl_quic_channel_get0_ssl(QUIC_CHANNEL *ch) { return ch->tls; diff --git a/ssl/quic/quic_lcidm.c b/ssl/quic/quic_lcidm.c index e5948b95e90..ce7e354f3e0 100644 --- a/ssl/quic/quic_lcidm.c +++ b/ssl/quic/quic_lcidm.c @@ -393,6 +393,36 @@ int ossl_quic_lcidm_generate_initial(QUIC_LCIDM *lcidm, initial_lcid, NULL); } +int ossl_quic_lcidm_bind_channel(QUIC_LCIDM *lcidm, void *opaque, + const QUIC_CONN_ID *lcid) +{ + QUIC_LCIDM_CONN *conn; + QUIC_LCID *lcid_obj; + + /* + * the plan is simple: + * make sure the lcid is still unused. + * do the same business as ossl_quic_lcidm_gnerate_initial() does, + * except we will use lcid instead of generating a new one. + */ + if (ossl_quic_lcidm_lookup(lcidm, lcid, NULL, NULL) != 0) + return 0; + + if ((conn = lcidm_upsert_conn(lcidm, opaque)) == NULL) + return 0; + + if ((lcid_obj = lcidm_conn_new_lcid(lcidm, conn, lcid)) == NULL) { + lcidm_delete_conn(lcidm, conn); + return 0; + } + + lcid_obj->seq_num = conn->next_seq_num; + lcid_obj->type = LCID_TYPE_INITIAL; + conn->next_seq_num++; + + return 1; +} + int ossl_quic_lcidm_generate(QUIC_LCIDM *lcidm, void *opaque, OSSL_QUIC_FRAME_NEW_CONN_ID *ncid_frame) @@ -554,3 +584,16 @@ int ossl_quic_lcidm_debug_add(QUIC_LCIDM *lcidm, void *opaque, lcid_obj->type = LCID_TYPE_NCID; return 1; } + +int ossl_quic_lcidm_get_unused_cid(QUIC_LCIDM *lcidm, QUIC_CONN_ID *cid) +{ + int i; + + for (i = 0; i < 10; i++) { + if (lcidm_generate_cid(lcidm, cid) + && lcidm_get0_lcid(lcidm, cid) == NULL) + return 1; /* not found <=> radomly generated cid is unused */ + } + + return 0; +} diff --git a/ssl/quic/quic_port.c b/ssl/quic/quic_port.c index e4bafabd805..83b0a591c49 100644 --- a/ssl/quic/quic_port.c +++ b/ssl/quic/quic_port.c @@ -30,6 +30,34 @@ static void port_default_packet_handler(QUIC_URXE *e, void *arg, const QUIC_CONN_ID *dcid); static void port_rx_pre(QUIC_PORT *port); +/** + * @struct validation_token + * @brief Represents a validation token for secure connection handling. + * + * This struct is used to store information related to a validation token, + * including the token buffer, original connection ID, and an integrity tag + * for secure validation of QUIC connections. + * + * @var validation_token::token_buf + * A character array holding the token data. The size of this array is + * based on the length of the string "openssltoken" minus one for the null + * terminator. + * + * @var validation_token::token_odcid + * An original connection ID (`QUIC_CONN_ID`) used to identify the QUIC + * connection. This ID helps associate the token with a specific connection. + * + * @var validation_token::integrity_tag + * A character array for the integrity tag, with a length defined by + * `QUIC_RETRY_INTEGRITY_TAG_LEN`. This tag is used to verify the integrity + * of the token during the connection process. + */ +struct validation_token { + char token_buf[sizeof("openssltoken") - 1]; + QUIC_CONN_ID token_odcid; + char integrity_tag[QUIC_RETRY_INTEGRITY_TAG_LEN]; +}; + DEFINE_LIST_OF_IMPL(ch, QUIC_CHANNEL); DEFINE_LIST_OF_IMPL(incoming_ch, QUIC_CHANNEL); DEFINE_LIST_OF_IMPL(port, QUIC_PORT); @@ -544,28 +572,27 @@ static void port_rx_pre(QUIC_PORT *port) * connection from it. If a new connection is made, the new channel is written * to *new_ch. */ -static void port_on_new_conn(QUIC_PORT *port, const BIO_ADDR *peer, - const QUIC_CONN_ID *scid, - const QUIC_CONN_ID *dcid, - QUIC_CHANNEL **new_ch) +static void port_bind_channel(QUIC_PORT *port, const BIO_ADDR *peer, + const QUIC_CONN_ID *scid, const QUIC_CONN_ID *dcid, + const QUIC_CONN_ID *odcid, QUIC_CHANNEL **new_ch) { QUIC_CHANNEL *ch; + /* + * If we're running with a simulated tserver, it will already have + * a dummy channel created, use that instead + */ if (port->tserver_ch != NULL) { - /* Specially assign to existing channel */ - if (!ossl_quic_channel_on_new_conn(port->tserver_ch, peer, scid, dcid)) - return; - - *new_ch = port->tserver_ch; + ch = port->tserver_ch; port->tserver_ch = NULL; - return; + } else { + ch = port_make_channel(port, NULL, /* is_server= */1); } - ch = port_make_channel(port, NULL, /*is_server=*/1); if (ch == NULL) return; - if (!ossl_quic_channel_on_new_conn(ch, peer, scid, dcid)) { + if (!ossl_quic_bind_channel(ch, peer, scid, dcid, odcid)) { ossl_quic_channel_free(ch); return; } @@ -620,6 +647,158 @@ static int port_try_handle_stateless_reset(QUIC_PORT *port, const QUIC_URXE *e) return i > 0; } +#define TOKEN_LEN (sizeof("openssltoken") + \ + QUIC_RETRY_INTEGRITY_TAG_LEN - 1 + \ + sizeof(unsigned char)) + +/** + * @brief Sends a QUIC Retry packet to a client. + * + * This function constructs and sends a Retry packet to the specified client + * using the provided connection header information. The Retry packet + * includes a generated validation token and a new connection ID, following + * the QUIC protocol specifications for connection establishment. + * + * @param port Pointer to the QUIC port from which to send the packet. + * @param peer Address of the client peer receiving the packet. + * @param client_hdr Header of the client's initial packet, containing + * connection IDs and other relevant information. + * + * This function performs the following steps: + * - Generates a validation token for the client. + * - Sets the destination and source connection IDs. + * - Calculates the integrity tag and sets the token length. + * - Encodes and sends the packet via the BIO network interface. + * + * Error handling is included for failures in CID generation, encoding, and + * network transmiss + */ +static void port_send_retry(QUIC_PORT *port, + BIO_ADDR *peer, + QUIC_PKT_HDR *client_hdr) +{ + BIO_MSG msg[1]; + unsigned char buffer[512]; + WPACKET wpkt; + size_t written; + QUIC_PKT_HDR hdr; + struct validation_token token; + size_t token_len = TOKEN_LEN; + unsigned char *integrity_tag; + int ok; + + /* TODO(QUIC_SERVER): generate proper validation token */ + memcpy(token.token_buf, "openssltoken", sizeof("openssltoken") - 1); + + token.token_odcid = client_hdr->dst_conn_id; + token_len += token.token_odcid.id_len; + integrity_tag = (unsigned char *)&token.token_odcid + + token.token_odcid.id_len + sizeof(token.token_odcid.id_len); + /* + * 17.2.5.1 Sending a Retry packet + * dst ConnId is src ConnId we got from client + * src ConnId comes from local conn ID manager + */ + memset(&hdr, 0, sizeof(QUIC_PKT_HDR)); + hdr.dst_conn_id = client_hdr->src_conn_id; + /* + * this is the random connection ID, we expect client is + * going to send the ID with next INITIAL packet which + * will also come with token we generate here. + */ + ok = ossl_quic_lcidm_get_unused_cid(port->lcidm, &hdr.src_conn_id); + if (ok == 0) + return; + + hdr.dst_conn_id = client_hdr->src_conn_id; + hdr.type = QUIC_PKT_TYPE_RETRY; + hdr.fixed = 1; + hdr.version = 1; + hdr.len = token_len; + hdr.data = (unsigned char *)&token; + ok = ossl_quic_calculate_retry_integrity_tag(port->engine->libctx, + port->engine->propq, &hdr, + &client_hdr->dst_conn_id, + integrity_tag); + if (ok == 0) + return; + + hdr.token = (unsigned char *)&token; + hdr.token_len = token_len; + + msg[0].data = buffer; + msg[0].peer = peer; + msg[0].local = NULL; + msg[0].flags = 0; + + ok = WPACKET_init_static_len(&wpkt, buffer, sizeof(buffer), 0); + if (ok == 0) + return; + + ok = ossl_quic_wire_encode_pkt_hdr(&wpkt, client_hdr->dst_conn_id.id_len, + &hdr, NULL); + if (ok == 0) + return; + + ok = WPACKET_get_total_written(&wpkt, &msg[0].data_len); + if (ok == 0) + return; + + ok = WPACKET_finish(&wpkt); + if (ok == 0) + return; + + /* + * TODO(QUIC SERVER) need to retry this in the event it return EAGAIN + * on a non-blocking BIO + */ + if (!BIO_sendmmsg(port->net_wbio, msg, sizeof(BIO_MSG), 1, 0, &written)) + ERR_raise_data(ERR_LIB_SSL, SSL_R_QUIC_NETWORK_ERROR, + "port retry send failed due to network BIO I/O error"); + +} + +/** + * @brief Validates a received token in a QUIC packet header. + * + * This function checks the validity of a token contained in the provided + * QUIC packet header (`QUIC_PKT_HDR *hdr`). The validation process involves + * verifying that the token matches an expected format and value. If the + * token is valid, the function extracts the original connection ID (ODCID) + * and stores it in the provided `QUIC_CONN_ID *odcid`. + * + * @param hdr Pointer to the QUIC packet header containing the token. + * @param odcid Pointer to the connection ID structure to store the ODCID if + * the token is valid. + * @return 1 if the token is valid and ODCID is extracted successfully, + * 0 otherwise. + * + * The function performs the following checks: + * - Verifies that the token length meets the required minimum. + * - Confirms the token buffer matches the expected "openssltoken" string. + * - + */ +static int port_validate_token(QUIC_PKT_HDR *hdr, QUIC_CONN_ID *odcid) +{ + int valid; + struct validation_token *token; + + memset(odcid, 0, sizeof(QUIC_CONN_ID)); + + token = (struct validation_token *)hdr->token; + if (token == NULL || hdr->token_len <= (TOKEN_LEN - QUIC_RETRY_INTEGRITY_TAG_LEN)) + return 0; + + valid = memcmp(token->token_buf, "openssltoken", sizeof("openssltoken") - 1); + if (valid != 0) + return 0; + + odcid->id_len = token->token_odcid.id_len; + memcpy(odcid->id, token->token_odcid.id, token->token_odcid.id_len); + + return 1; +} + /* * This is called by the demux when we get a packet not destined for any known * DCID. @@ -631,6 +810,7 @@ static void port_default_packet_handler(QUIC_URXE *e, void *arg, PACKET pkt; QUIC_PKT_HDR hdr; QUIC_CHANNEL *ch = NULL, *new_ch = NULL; + QUIC_CONN_ID odcid; /* Don't handle anything if we are no longer running. */ if (!ossl_quic_port_is_running(port)) @@ -691,18 +871,29 @@ static void port_default_packet_handler(QUIC_URXE *e, void *arg, goto undesirable; /* - * Try to process this as a valid attempt to initiate a connection. - * + * TODO(QUIC SERVER): there should be some logic similar to accounting half-open + * states in TCP. If we reach certain threshold, then we want to + * validate clients. + */ + if (hdr.token == NULL) { + port_send_retry(port, &e->peer, &hdr); + goto undesirable; + } else if (port_validate_token(&hdr, &odcid) == 0) { + goto undesirable; + } + + port_bind_channel(port, &e->peer, &hdr.src_conn_id, &hdr.dst_conn_id, + &odcid, &new_ch); + + /* * The channel will do all the LCID registration needed, but as an * optimization inject this packet directly into the channel's QRX for * processing without going through the DEMUX again. */ - port_on_new_conn(port, &e->peer, &hdr.src_conn_id, &hdr.dst_conn_id, - &new_ch); - if (new_ch != NULL) + if (new_ch != NULL) { ossl_qrx_inject_urxe(new_ch->qrx, e); - - return; + return; + } undesirable: ossl_quic_demux_release_urxe(port->demux, e); diff --git a/ssl/quic/quic_wire_pkt.c b/ssl/quic/quic_wire_pkt.c index 00f4afb7c08..e0018377af2 100644 --- a/ssl/quic/quic_wire_pkt.c +++ b/ssl/quic/quic_wire_pkt.c @@ -554,8 +554,7 @@ int ossl_quic_wire_encode_pkt_hdr(WPACKET *pkt, hdr->src_conn_id.id_len)) return 0; - if (hdr->type == QUIC_PKT_TYPE_VERSION_NEG - || hdr->type == QUIC_PKT_TYPE_RETRY) { + if (hdr->type == QUIC_PKT_TYPE_VERSION_NEG) { if (hdr->len > 0 && !WPACKET_reserve_bytes(pkt, hdr->len, NULL)) return 0; @@ -568,6 +567,12 @@ int ossl_quic_wire_encode_pkt_hdr(WPACKET *pkt, return 0; } + if (hdr->type == QUIC_PKT_TYPE_RETRY) { + if (!WPACKET_memcpy(pkt, hdr->token, hdr->token_len)) + return 0; + return 1; + } + if (!WPACKET_quic_write_vlint(pkt, hdr->len + hdr->pn_len) || !WPACKET_get_total_written(pkt, &off_pn) || !WPACKET_memcpy(pkt, hdr->pn, hdr->pn_len)) diff --git a/test/recipes/75-test_quicapi_data/ssltraceref-zlib.txt b/test/recipes/75-test_quicapi_data/ssltraceref-zlib.txt index 6249195ee36..9ab460994aa 100644 --- a/test/recipes/75-test_quicapi_data/ssltraceref-zlib.txt +++ b/test/recipes/75-test_quicapi_data/ssltraceref-zlib.txt @@ -84,6 +84,22 @@ Sent Packet Packet Number: 0x00000000 Sent Datagram Length: 1200 +Received Datagram + Length: 52 +Sent Frame: Crypto + Offset: 0 + Len: 263 +Sent Frame: Padding +Sent Packet + Packet Type: Initial + Version: 0x00000001 + Destination Conn Id: 0x???????????????? + Source Conn Id: + Payload length: 1157 + Token: ?????????????????????????????????????????? + Packet Number: 0x00000001 +Sent Datagram + Length: 1200 Received Datagram Length: 1200 Received Datagram @@ -97,7 +113,7 @@ Received Packet Token: Packet Number: 0x00000000 Received Frame: Ack (without ECN) - Largest acked: 0 + Largest acked: 1 Ack delay (raw) 0 Ack range count: 0 First ack range: 0 @@ -291,9 +307,9 @@ Sent Packet Version: 0x00000001 Destination Conn Id: 0x???????????????? Source Conn Id: - Payload length: 1097 - Token: - Packet Number: 0x00000001 + Payload length: 1076 + Token: ?????????????????????????????????????????? + Packet Number: 0x00000002 Sent Packet Packet Type: Handshake Version: 0x00000001 diff --git a/test/recipes/75-test_quicapi_data/ssltraceref.txt b/test/recipes/75-test_quicapi_data/ssltraceref.txt index 177677d64dc..c241d375595 100644 --- a/test/recipes/75-test_quicapi_data/ssltraceref.txt +++ b/test/recipes/75-test_quicapi_data/ssltraceref.txt @@ -82,6 +82,22 @@ Sent Packet Packet Number: 0x00000000 Sent Datagram Length: 1200 +Received Datagram + Length: 52 +Sent Frame: Crypto + Offset: 0 + Len: 256 +Sent Frame: Padding +Sent Packet + Packet Type: Initial + Version: 0x00000001 + Destination Conn Id: 0x???????????????? + Source Conn Id: + Payload length: 1157 + Token: ?????????????????????????????????????????? + Packet Number: 0x00000001 +Sent Datagram + Length: 1200 Received Datagram Length: 1200 Received Datagram @@ -95,7 +111,7 @@ Received Packet Token: Packet Number: 0x00000000 Received Frame: Ack (without ECN) - Largest acked: 0 + Largest acked: 1 Ack delay (raw) 0 Ack range count: 0 First ack range: 0 @@ -289,9 +305,9 @@ Sent Packet Version: 0x00000001 Destination Conn Id: 0x???????????????? Source Conn Id: - Payload length: 1097 - Token: - Packet Number: 0x00000001 + Payload length: 1076 + Token: ?????????????????????????????????????????? + Packet Number: 0x00000002 Sent Packet Packet Type: Handshake Version: 0x00000001