]> git.ipfire.org Git - thirdparty/openssl.git/commitdiff
Augment RETRY validation token
authorAndrew Dinh <andrewd@openssl.org>
Wed, 27 Nov 2024 20:35:16 +0000 (12:35 -0800)
committerNeil Horman <nhorman@openssl.org>
Mon, 17 Feb 2025 16:27:33 +0000 (11:27 -0500)
Adds fields to the QUIC RETRY packet validation token:
timestamp, remote_addr, odcid, & rscid.

Also adds functionality to validate the token once returned by the client.

Note that this does not encrypt the token yet.

Also check that the RSCID stored in the RETRY validation
token matches the DCID in the header.

Reviewed-by: Neil Horman <nhorman@openssl.org>
Reviewed-by: Saša Nedvědický <sashan@openssl.org>
Reviewed-by: Matt Caswell <matt@openssl.org>
Reviewed-by: Tomas Mraz <tomas@openssl.org>
(Merged from https://github.com/openssl/openssl/pull/26048)

ssl/quic/quic_port.c
ssl/quic/quic_tserver.c
test/recipes/75-test_quicapi_data/ssltraceref-zlib.txt
test/recipes/75-test_quicapi_data/ssltraceref.txt

index ffe9c2947fb6384951742642df69f2323f30bddd..1a6254a3b201a5ac2be6fef9993234b4c771e95c 100644 (file)
@@ -34,29 +34,41 @@ 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.
+ * This struct is used to store information related to a validation token.
  *
- * @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::is_retry
+ * True iff this validation token is for a token sent in a RETRY packet.
+ * Otherwise, this token is from a NEW_TOKEN_packet. Iff this value is true,
+ * then ODCID and RSCID are set.
  *
- * @var validation_token::token_odcid
+ * @var validation_token::timestamp
+ * Time that the validation token was minted.
+ *
+ * @var validation_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.
+ * This will only be valid for validation tokens from RETRY packets.
+ *
+ * @var validation_token::rscid
+ * DCID that the client will use as the DCID of the subsequent initial packet
+ * i.e the "new" DCID.
+ * This will only be valid for validation tokens from RETRY packets.
+ *
+ * @var validation_token::remote_addr_len
+ * Length of the following character array.
  *
- * @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.
+ * @var validation_token::remote_addr
+ * A character array holding the raw address of the client requesting the
+ * connection.
  */
-struct validation_token {
-    char token_buf[sizeof("openssltoken") - 1];
-    QUIC_CONN_ID token_odcid;
-    char integrity_tag[QUIC_RETRY_INTEGRITY_TAG_LEN];
-};
+typedef struct validation_token {
+    OSSL_TIME timestamp;
+    QUIC_CONN_ID odcid;
+    QUIC_CONN_ID rscid;
+    size_t remote_addr_len;
+    unsigned char *remote_addr;
+    unsigned char is_retry;
+} QUIC_VALIDATION_TOKEN;
 
 DEFINE_LIST_OF_IMPL(ch, QUIC_CHANNEL);
 DEFINE_LIST_OF_IMPL(incoming_ch, QUIC_CHANNEL);
@@ -647,9 +659,133 @@ 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))
+static void cleanup_validation_token(QUIC_VALIDATION_TOKEN *token)
+{
+    OPENSSL_free(token->remote_addr);
+}
+
+/**
+ * @brief Generates a validation token for a RETRY packet.
+ *
+ * @param peer  Address of the client peer receiving the packet.
+ * @param odcid DCID of the connection attempt.
+ * @param rscid Retry source connection ID of the connection attempt.
+ * @param token Address of token to fill data.
+ *
+ * @return 1 if validation token is filled successfully, 0 otherwise.
+ */
+static int generate_retry_token(BIO_ADDR *peer, QUIC_CONN_ID odcid,
+                                QUIC_CONN_ID rscid, QUIC_VALIDATION_TOKEN *token)
+{
+    token->is_retry = 1;
+    token->timestamp = ossl_time_now();
+    token->remote_addr = NULL;
+    token->odcid = odcid;
+    token->rscid = rscid;
+
+    if (!BIO_ADDR_rawaddress(peer, NULL, &token->remote_addr_len)
+        || token->remote_addr_len == 0
+        || (token->remote_addr = OPENSSL_malloc(token->remote_addr_len)) == NULL
+        || !BIO_ADDR_rawaddress(peer, token->remote_addr,
+                                &token->remote_addr_len)) {
+        cleanup_validation_token(token);
+        return 0;
+    }
+
+    return 1;
+}
+
+/**
+ * @brief Marshals a validation token into a new buffer.
+ *
+ * Dynamically allocates |buffer| and stores the size of |buffer| in BUFFER_LEN.
+ * Note that it will also allocate an extra QUIC_RETRY_INTEGRITY_TAG_LEN bytes.
+ * The caller is responsible for freeing |buffer|.
+ *
+ * @param token      Validation token.
+ * @param buffer     Callee will allocate a buffer and store the address here.
+ * @param buffer_len Size of |buffer|.
+ */
+static int marshal_validation_token(QUIC_VALIDATION_TOKEN *token,
+                                    unsigned char **buffer, size_t *buffer_len)
+{
+    WPACKET wpkt = {0};
+    BUF_MEM *buf_mem = BUF_MEM_new();
+
+    if (buf_mem == NULL || (token->is_retry != 0 && token->is_retry != 1))
+        return 0;
+
+    if (!WPACKET_init(&wpkt, buf_mem)
+        || !WPACKET_memset(&wpkt, token->is_retry, 1)
+        || !WPACKET_memcpy(&wpkt, &token->timestamp,
+                           sizeof(token->timestamp))
+        || (token->is_retry
+            && (!WPACKET_sub_memcpy_u8(&wpkt, &token->odcid.id,
+                                       token->odcid.id_len)
+                || !WPACKET_sub_memcpy_u8(&wpkt, &token->rscid.id,
+                                          token->rscid.id_len)))
+        || !WPACKET_sub_memcpy_u8(&wpkt, token->remote_addr, token->remote_addr_len)
+        || !WPACKET_allocate_bytes(&wpkt, QUIC_RETRY_INTEGRITY_TAG_LEN, NULL)
+        || !WPACKET_get_total_written(&wpkt, buffer_len)
+        || !WPACKET_finish(&wpkt)) {
+        WPACKET_cleanup(&wpkt);
+        BUF_MEM_free(buf_mem);
+        return 0;
+    }
+
+    *buffer = (unsigned char *)buf_mem->data;
+    buf_mem->data = NULL;
+    BUF_MEM_free(buf_mem);
+    return 1;
+}
+
+/**
+ * @brief Parses contents of a buffer into a validation token.
+ *
+ * VALIDATION_TOKEN should already be initalized. Does some basic sanity checks.
+ *
+ * @param token   Validation token to fill data in.
+ * @param buf     Buffer of previously marshaled validation token.
+ * @param buf_len Length of |buf|.
+ */
+static int parse_validation_token(QUIC_VALIDATION_TOKEN *token,
+                                  const unsigned char *buf, size_t buf_len)
+{
+    PACKET pkt, subpkt;
+
+    if (buf == NULL || token == NULL)
+        return 0;
+
+    token->remote_addr = NULL;
+
+    if (!PACKET_buf_init(&pkt, buf, buf_len)
+        || !PACKET_copy_bytes(&pkt, &token->is_retry, sizeof(token->is_retry))
+        || !(token->is_retry == 0 || token->is_retry == 1)
+        || !PACKET_copy_bytes(&pkt, (unsigned char *)&token->timestamp,
+                              sizeof(token->timestamp))
+        || (token->is_retry
+            && (!PACKET_get_length_prefixed_1(&pkt, &subpkt)
+                || (token->odcid.id_len = (unsigned char)PACKET_remaining(&subpkt))
+                    > QUIC_MAX_CONN_ID_LEN
+                || !PACKET_copy_bytes(&subpkt,
+                                      (unsigned char *)&token->odcid.id,
+                                      token->odcid.id_len)
+                || !PACKET_get_length_prefixed_1(&pkt, &subpkt)
+                || (token->rscid.id_len = (unsigned char)PACKET_remaining(&subpkt))
+                    > QUIC_MAX_CONN_ID_LEN
+                || !PACKET_copy_bytes(&subpkt, (unsigned char *)&token->rscid.id,
+                                      token->rscid.id_len)))
+        || !PACKET_get_length_prefixed_1(&pkt, &subpkt)
+        || (token->remote_addr_len = PACKET_remaining(&subpkt)) == 0
+        || (token->remote_addr = OPENSSL_malloc(token->remote_addr_len)) == NULL
+        || !PACKET_copy_bytes(&subpkt, token->remote_addr, token->remote_addr_len)
+        || PACKET_remaining(&pkt) != 0) {
+        cleanup_validation_token(token);
+        return 0;
+    }
+
+    return 1;
+}
 
 /**
  * @brief Sends a QUIC Retry packet to a client.
@@ -678,22 +814,13 @@ static void port_send_retry(QUIC_PORT *port,
                             QUIC_PKT_HDR *client_hdr)
 {
     BIO_MSG msg[1];
-    unsigned char buffer[512];
+    unsigned char buffer[512], *token_buf = NULL;
     WPACKET wpkt;
-    size_t written;
-    QUIC_PKT_HDR hdr;
-    struct validation_token token;
-    size_t token_len = TOKEN_LEN;
-    unsigned char *integrity_tag;
+    size_t written, token_buf_len;
+    QUIC_PKT_HDR hdr = {0};
+    QUIC_VALIDATION_TOKEN token = {0};
     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
@@ -708,23 +835,31 @@ static void port_send_retry(QUIC_PORT *port,
      */
     ok = ossl_quic_lcidm_get_unused_cid(port->lcidm, &hdr.src_conn_id);
     if (ok == 0)
-        return;
+        goto err;
+
+    /* Generate retry validation token */
+    if (!generate_retry_token(peer, client_hdr->dst_conn_id,
+                              hdr.src_conn_id, &token)
+        || !marshal_validation_token(&token, &token_buf, &token_buf_len)
+        || !ossl_assert(token_buf_len >= QUIC_RETRY_INTEGRITY_TAG_LEN))
+        goto err;
 
     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;
+    hdr.len = token_buf_len;
+    hdr.data = token_buf;
     ok = ossl_quic_calculate_retry_integrity_tag(port->engine->libctx,
                                                  port->engine->propq, &hdr,
                                                  &client_hdr->dst_conn_id,
-                                                 integrity_tag);
+                                                 token_buf + token_buf_len
+                                                 - QUIC_RETRY_INTEGRITY_TAG_LEN);
     if (ok == 0)
-        return;
+        goto err;
 
-    hdr.token = (unsigned char *)&token;
-    hdr.token_len = token_len;
+    hdr.token = hdr.data;
+    hdr.token_len = hdr.len;
 
     msg[0].data = buffer;
     msg[0].peer = peer;
@@ -733,20 +868,20 @@ static void port_send_retry(QUIC_PORT *port,
 
     ok = WPACKET_init_static_len(&wpkt, buffer, sizeof(buffer), 0);
     if (ok == 0)
-        return;
+        goto err;
 
     ok = ossl_quic_wire_encode_pkt_hdr(&wpkt, client_hdr->dst_conn_id.id_len,
                                        &hdr, NULL);
     if (ok == 0)
-        return;
+        goto err;
 
     ok = WPACKET_get_total_written(&wpkt, &msg[0].data_len);
     if (ok == 0)
-        return;
+        goto err;
 
     ok = WPACKET_finish(&wpkt);
     if (ok == 0)
-        return;
+        goto err;
 
     /*
      * TODO(QUIC SERVER) need to retry this in the event it return EAGAIN
@@ -756,6 +891,9 @@ static void port_send_retry(QUIC_PORT *port,
         ERR_raise_data(ERR_LIB_SSL, SSL_R_QUIC_NETWORK_ERROR,
                        "port retry send failed due to network BIO I/O error");
 
+err:
+    cleanup_validation_token(&token);
+    OPENSSL_free(token_buf);
 }
 
 /**
@@ -853,39 +991,96 @@ static void port_send_version_negotiation(QUIC_PORT *port, BIO_ADDR *peer,
  * 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`.
+ * token is from a RETRY packet, the function extracts the original connection
+ * ID (ODCID)/original source connection ID (SCID) and stores it in the provided
+ * parameters. If the token is from a NEW_TOKEN packet, the values will be
+ * derived instead.
  *
  * @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,
+ * @param port  Pointer to the QUIC port from which to send the packet.
+ * @param peer  Address of the client peer receiving the packet.
+ * @param odcid Pointer to the connection ID structure to store the ODCID if the
+ *              token is valid.
+ * @param scid  Pointer to the connection ID structure to store the SCID if the
+ *              token is valid.
+ *
+ * @return      1 if the token is valid and ODCID/SCID are successfully set.
  *              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.
- * -
+ * - Token length meets the required minimum.
+ * - Buffer matches expected format.
+ * - Peer address matches previous connection address.
+ * - Token has not expired. Currently set to 10 seconds for tokens from RETRY
+ *   packets and 60 minutes for tokens from NEW_TOKEN packets. This may be
+ *   configurable in the future.
  */
-static int port_validate_token(QUIC_PKT_HDR *hdr, QUIC_CONN_ID *odcid)
+static int port_validate_token(QUIC_PKT_HDR *hdr, QUIC_PORT *port,
+                               BIO_ADDR *peer, QUIC_CONN_ID *odcid,
+                               QUIC_CONN_ID *scid)
 {
-    int valid;
-    struct validation_token *token;
-
-    memset(odcid, 0, sizeof(QUIC_CONN_ID));
+    int ret = 0;
+    QUIC_VALIDATION_TOKEN token = { 0 };
+    unsigned long long time_diff;
+    size_t remote_addr_len;
+    unsigned char *remote_addr = NULL;
+    OSSL_TIME now = ossl_time_now();
+
+    if (!parse_validation_token(&token, hdr->token, hdr->token_len))
+        goto err;
 
-    token = (struct validation_token *)hdr->token;
-    if (token == NULL || hdr->token_len <= (TOKEN_LEN - QUIC_RETRY_INTEGRITY_TAG_LEN))
-        return 0;
+    /*
+     * Validate token timestamp. Current time should not be before the token
+     * timestamp.
+     */
+    if (ossl_time_compare(now, token.timestamp) < 0)
+        goto err;
+    time_diff = ossl_time2seconds(ossl_time_abs_difference(token.timestamp,
+                                                           now));
+    if ((token.is_retry && time_diff > 10)
+        || (!token.is_retry && time_diff > 3600))
+        goto err;
 
-    valid = memcmp(token->token_buf, "openssltoken", sizeof("openssltoken") - 1);
-    if (valid != 0)
-        return 0;
+    /* Validate remote address */
+    if (!BIO_ADDR_rawaddress(peer, NULL, &remote_addr_len)
+        || remote_addr_len != token.remote_addr_len
+        || (remote_addr = OPENSSL_malloc(remote_addr_len)) == NULL
+        || !BIO_ADDR_rawaddress(peer, remote_addr, &remote_addr_len)
+        || memcmp(remote_addr, token.remote_addr, remote_addr_len) != 0)
+        goto err;
 
-    odcid->id_len = token->token_odcid.id_len;
-    memcpy(odcid->id, token->token_odcid.id, token->token_odcid.id_len);
+    /*
+     * Set ODCID and SCID. If the token is from a RETRY packet, retrieve both
+     * from the token. Otherwise, generate a new ODCID and use the header's
+     * source connection ID for SCID.
+     */
+    if (token.is_retry) {
+        /*
+         * We're parsing a packet header before its gone through AEAD validation
+         * here, so there is a chance we are dealing with corrupted data. Make
+         * Sure the dcid encoded in the token matches the headers dcid to
+         * mitigate that.
+         * TODO(QUIC SERVER): Consider handling AEAD validation at the port
+         * level rather than the QRX/channel level to eliminate the need for
+         * this.
+         */
+        if (token.rscid.id_len != hdr->dst_conn_id.id_len
+            || memcmp(&token.rscid.id, &hdr->dst_conn_id.id,
+                      token.rscid.id_len) != 0)
+            goto err;
+        *odcid = token.odcid;
+        *scid = token.rscid;
+    } else {
+        if (!ossl_quic_lcidm_get_unused_cid(port->lcidm, odcid))
+            goto err;
+        *scid = hdr->src_conn_id;
+    }
 
-    return 1;
+    ret = 1;
+err:
+    cleanup_validation_token(&token);
+    OPENSSL_free(remote_addr);
+    return ret;
 }
 
 /*
@@ -899,7 +1094,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;
+    QUIC_CONN_ID odcid, scid;
     uint64_t cause_flags = 0;
 
     /* Don't handle anything if we are no longer running. */
@@ -991,11 +1186,11 @@ static void port_default_packet_handler(QUIC_URXE *e, void *arg,
     if (hdr.token == NULL) {
         port_send_retry(port, &e->peer, &hdr);
         goto undesirable;
-    } else if (port_validate_token(&hdr, &odcid) == 0) {
+    } else if (port_validate_token(&hdr, port, &e->peer, &odcid, &scid) != 1) {
         goto undesirable;
     }
 
-    port_bind_channel(port, &e->peer, &hdr.src_conn_id, &hdr.dst_conn_id,
+    port_bind_channel(port, &e->peer, &scid, &hdr.dst_conn_id,
                       &odcid, &new_ch);
 
     /*
index 37ada76a76da09563fc6498441f49dd28a1729b7..43c5417eba78c37d5101589be8793f2a33214ce7 100644 (file)
@@ -43,9 +43,6 @@ struct quic_tserver_st {
     /* SSL for the underlying TLS connection */
     SSL *tls;
 
-    /* The current peer L4 address. AF_UNSPEC if we do not have a peer yet. */
-    BIO_ADDR        cur_peer_addr;
-
     /* Are we connected to a peer? */
     unsigned int    connected       : 1;
 };
index 2e5db31c307798499a45129c415da9e453cf2951..c3fd7cb55159122f14b70bce1c2d9f0c99a8bdb4 100644 (file)
@@ -85,7 +85,7 @@ Sent Packet
 Sent Datagram
   Length: 1200
 Received Datagram
-  Length: 52
+  Length: 63
 Sent Frame: Crypto
     Offset: 0
     Len: 263
@@ -95,8 +95,8 @@ Sent Packet
   Version: 0x00000001
   Destination Conn Id: 0x????????????????
   Source Conn Id: <zero length id>
-  Payload length: 1157
-  Token: ??????????????????????????????????????????
+  Payload length: 1146
+  Token: ????????????????????????????????????????????????????????????????
   Packet Number: 0x00000001
 Sent Datagram
   Length: 1200
@@ -308,8 +308,8 @@ Sent Packet
   Version: 0x00000001
   Destination Conn Id: 0x????????????????
   Source Conn Id: <zero length id>
-  Payload length: 1076
-  Token: ??????????????????????????????????????????
+  Payload length: 1065
+  Token: ????????????????????????????????????????????????????????????????
   Packet Number: 0x00000002
 Sent Packet
   Packet Type: Handshake
index 148e154adfea3a4f1c67806f9c6663e655653793..2cf5c4f6917a8995cf00197f69fc0e5c11deeaeb 100644 (file)
@@ -83,7 +83,7 @@ Sent Packet
 Sent Datagram
   Length: 1200
 Received Datagram
-  Length: 52
+  Length: 63
 Sent Frame: Crypto
     Offset: 0
     Len: 256
@@ -93,8 +93,8 @@ Sent Packet
   Version: 0x00000001
   Destination Conn Id: 0x????????????????
   Source Conn Id: <zero length id>
-  Payload length: 1157
-  Token: ??????????????????????????????????????????
+  Payload length: 1146
+  Token: ????????????????????????????????????????????????????????????????
   Packet Number: 0x00000001
 Sent Datagram
   Length: 1200
@@ -306,8 +306,8 @@ Sent Packet
   Version: 0x00000001
   Destination Conn Id: 0x????????????????
   Source Conn Id: <zero length id>
-  Payload length: 1076
-  Token: ??????????????????????????????????????????
+  Payload length: 1065
+  Token: ????????????????????????????????????????????????????????????????
   Packet Number: 0x00000002
 Sent Packet
   Packet Type: Handshake