Skip to content

Commit

Permalink
TX key update support, RX time and PN reporting, general refactoring
Browse files Browse the repository at this point in the history
- Adds an RX time field to the OSSL_QRX_PKT structure.

- Adds a timekeeping argument to ossl_demux_new which is used to determine
  packet reception time.

- Adds a decoded PN field to the OSSL_QRX_PKT structure.
  This has to be decoded by the QRX anyway, and its omission was an oversight.

- Key update support for the TX side.

- Minor refactoring.

Reviewed-by: Paul Dale <pauli@openssl.org>
Reviewed-by: Tomas Mraz <tomas@openssl.org>
(Merged from #18949)
  • Loading branch information
hlandau authored and t8m committed Sep 2, 2022
1 parent 1957148 commit 948c656
Show file tree
Hide file tree
Showing 12 changed files with 1,479 additions and 325 deletions.
15 changes: 14 additions & 1 deletion include/internal/quic_demux.h
Expand Up @@ -13,6 +13,7 @@
# include <openssl/ssl.h>
# include "internal/quic_types.h"
# include "internal/bio_addr.h"
# include "internal/time.h"

/*
* QUIC Demuxer
Expand Down Expand Up @@ -110,6 +111,12 @@ struct quic_urxe_st {
* is zeroed.
*/
BIO_ADDR peer, local;

/*
* Time at which datagram was received (or ossl_time_zero()) if a now
* function was not provided).
*/
OSSL_TIME time;
};

/* Accessors for URXE buffer. */
Expand Down Expand Up @@ -163,10 +170,16 @@ typedef void (ossl_quic_demux_cb_fn)(QUIC_URXE *e, void *arg);
* connections used on a socket. default_urxe_alloc_len is the buffer size to
* receive datagrams into; it should be a value large enough to contain any
* received datagram according to local MTUs, etc.
*
* now is an optional function used to determine the time a datagram was
* received. now_arg is an opaque argument passed to the function. If now is
* NULL, ossl_time_zero() is used as the datagram reception time.
*/
QUIC_DEMUX *ossl_quic_demux_new(BIO *net_bio,
size_t short_conn_id_len,
size_t default_urxe_alloc_len);
size_t default_urxe_alloc_len,
OSSL_TIME (*now)(void *arg),
void *now_arg);

/*
* Destroy a demuxer. All URXEs must have been released back to the demuxer
Expand Down
119 changes: 76 additions & 43 deletions include/internal/quic_record_rx.h
Expand Up @@ -34,6 +34,9 @@ typedef struct ossl_qrx_args_st {

/* Initial reference PN used for RX. */
QUIC_PN init_largest_pn[QUIC_PN_SPACE_NUM];

/* Initial key phase. For debugging use only; always 0 in real use. */
unsigned char init_key_phase_bit;
} OSSL_QRX_ARGS;

/* Instantiates a new QRX. */
Expand Down Expand Up @@ -92,7 +95,7 @@ int ossl_qrx_remove_dst_conn_id(OSSL_QRX *qrx,
*
* To transition the RX side of an EL from WAITING_FOR_KEYS to HAVE_KEYS, call
* ossl_qrx_provide_secret (for the INITIAL EL, use of
* ossl_qrl_provide_initial_secret is recommended).
* ossl_quic_provide_initial_secret is recommended).
*
* Once keys have been provisioned for an EL, you call
* ossl_qrx_discard_enc_level to transition the EL to the DISCARDED state. You
Expand Down Expand Up @@ -129,22 +132,22 @@ int ossl_qrx_remove_dst_conn_id(OSSL_QRX *qrx,
* the QRX if it is not needed, for example if the QRX is being instantiated to
* take over handling of an existing connection which has already passed the
* INITIAL phase. This avoids the unnecessary derivation of INITIAL keys where
* they are not needed. In the ordinary case, ossl_qrx_provide_secret_initial
* they are not needed. In the ordinary case, ossl_quic_provide_initial_secret
* should be called immediately after instantiation.
*/

/*
* Provides a secret to the QRX, which arises due to an encryption level change.
* enc_level is a QUIC_ENC_LEVEL_* value. To initialise the INITIAL encryption
* level, it is recommended to use ossl_qrl_provide_initial_secret instead.
* level, it is recommended to use ossl_quic_provide_initial_secret instead.
*
* You should seek to call this function for a given EL before packets of that
* EL arrive and are processed by the QRX. However, if packets have already
* arrived for a given EL, the QRX will defer processing of them and perform
* processing of them when this function is eventually called for the EL in
* question.
*
* suite_id is a QRX_SUITE_* value which determines the AEAD function used for
* suite_id is a QRL_SUITE_* value which determines the AEAD function used for
* the QRX.
*
* The secret passed is used directly to derive the "quic key", "quic iv" and
Expand Down Expand Up @@ -218,6 +221,15 @@ typedef struct ossl_qrx_pkt_st {
* datagrams containing INITIAL packets), as required by RFC 9000.
*/
size_t datagram_len;

/* The PN which was decoded for the packet, if the packet has a PN field. */
QUIC_PN pn;

/*
* Time the packet was received, or ossl_time_zero() if the demuxer is not
* using a now() function.
*/
OSSL_TIME time;
} OSSL_QRX_PKT;

/*
Expand All @@ -226,9 +238,9 @@ typedef struct ossl_qrx_pkt_st {
* On success, all fields of *pkt are filled and 1 is returned.
* Else, returns 0.
*
* The resources referenced by pkt->hdr, pkt->data and pkt->peer will remain
* allocated at least until the user frees them by calling ossl_qrx_release_pkt,
* which must be called once you are done with the packet.
* The resources referenced by pkt->hdr, pkt->hdr->data and pkt->peer will
* remain allocated at least until the user frees them by calling
* ossl_qrx_release_pkt, which must be called once you are done with the packet.
*/
int ossl_qrx_read_pkt(OSSL_QRX *qrx, OSSL_QRX_PKT *pkt);

Expand Down Expand Up @@ -326,7 +338,7 @@ int ossl_qrx_set_early_validation_cb(OSSL_QRX *qrx,
* Two keys and a timer
*
* "Alternatively, endpoints can retain only two sets of packet protection
* neys, swapping previous keys for next after enough time has passed to
* keys, swapping previous keys for next after enough time has passed to
* allow for reordering in the network. In this case, the KP bit alone can
* be used to select keys."
*
Expand All @@ -342,11 +354,11 @@ int ossl_qrx_set_early_validation_cb(OSSL_QRX *qrx,
* PROVISIONED
* _______________________________
* | |
* UNPROVISIONED --|----> NORMAL <----------\ |------> DROPPED
* UNPROVISIONED --|----> NORMAL <----------\ |------> DISCARDED
* | | | |
* | | | |
* | v | |
* | UPDATE_CONFIRMED | |
* | UPDATING | |
* | | | |
* | | | |
* | v | |
Expand All @@ -362,16 +374,16 @@ int ossl_qrx_set_early_validation_cb(OSSL_QRX *qrx,
* recorded. When a flipped Key Phase bit is detected, the RX attempts to
* decrypt and authenticate the received packet with the 'next' keys rather than
* the 'current' keys. If (and only if) this authentication is successful, we
* move to the UPDATE_CONFIRMED state. (An attacker in the network could flip
* move to the UPDATING state. (An attacker in the network could flip
* the Key Phase bit randomly, so it is essential we do nothing until AEAD
* authentication is complete.)
*
* In the UPDATE_CONFIRMED state, we know a key update is occurring and record
* In the UPDATING state, we know a key update is occurring and record
* the new Key Phase bit value as the newly current value, but we still keep the
* old keys around so that we can still process any packets which were still in
* flight when the key update was initiated. In the UPDATE_CONFIRMED state, a
* flight when the key update was initiated. In the UPDATING state, a
* Key Phase bit value different to the current expected value is treated not as
* the initiation of another key update, but a reference to our old keys.
* the initiation of another key update, but a reference to our old keys.
*
* Eventually we will be reasonably sure we are not going to receive any more
* packets with the old keys. At this point, we can transition to the COOLDOWN
Expand All @@ -386,21 +398,25 @@ int ossl_qrx_set_early_validation_cb(OSSL_QRX *qrx,
* as a request for a Key Update, but this request is ignored and the packet is
* treated as malformed. We do this to allow mitigation against malicious peers
* trying to initiate an excessive number of Key Updates. The timeout for the
* transition from UPDATE_CONFIRMED to COOLDOWN is recommended as adequate for
* transition from UPDATING to COOLDOWN is recommended as adequate for
* this purpose in itself by the RFC, so the normal additional timeout value for
* the transition from COOLDOWN to normal is zero (immediate transition).
*
* A summary of each state:
*
* Exp KP Uses Keys KS0 KS1 If Non-Expected KP Bit
* ------ --------- ------ ----- ----------------------
* NORMAL 0 Keyset 0 Gen 0 Gen 1 → UPDATE_CONFIRMED
* UPDATE_CONFIRMED 1 Keyset 1 Gen 0 Gen 1 Use Keyset 0
* COOLDOWN 1 Keyset 1 Erased Gen 1 Ignore Packet
* Epoch Exp KP Uses Keys KS0 KS1 If Non-Expected KP Bit
* ----- ------ --------- ------ ----- ----------------------
* NORMAL 0 0 Keyset 0 Gen 0 Gen 1 → UPDATING
* UPDATING 1 1 Keyset 1 Gen 0 Gen 1 Use Keyset 0
* COOLDOWN 1 1 Keyset 1 Erased Gen 1 Ignore Packet (*)
*
* NORMAL 1 1 Keyset 1 Gen 2 Gen 1 → UPDATING
* UPDATING 2 0 Keyset 0 Gen 2 Gen 1 Use Keyset 1
* COOLDOWN 2 0 Keyset 0 Gen 2 Erased Ignore Packet (*)
*
* NORMAL 1 Keyset 1 Gen 2 Gen 1 → UPDATE_CONFIRMED
* UPDATE_CONFIRMED 0 Keyset 0 Gen 2 Gen 1 Use Keyset 1
* COOLDOWN 0 Keyset 0 Gen 2 Erased Ignore Packet
* (*) Actually implemented by attempting to decrypt the packet with the
* wrong keys (which ultimately has the same outcome), as recommended
* by RFC 9001 to avoid creating timing channels.
*
* Note that the key material for the next key generation ("key epoch") is
* always kept in the NORMAL state (necessary to avoid side-channel attacks).
Expand All @@ -411,20 +427,22 @@ int ossl_qrx_set_early_validation_cb(OSSL_QRX *qrx,
* and making the necessary calls to the TX side by detecting changes to the
* return value of ossl_qrx_get_key_epoch().
*
* The above states (NORMAL, UPDATE_CONFIRMED, COOLDOWN) can themselves be
* The above states (NORMAL, UPDATING, COOLDOWN) can themselves be
* considered substates of the PROVISIONED state. Providing a secret to the QRX
* for an EL transitions from UNPROVISIONED, the initial state, to PROVISIONED
* (NORMAL). Dropping key material for an EL transitions from whatever the
* current substate of the PROVISIONED state is to the DROPPED state, which is
* current substate of the PROVISIONED state is to the DISCARDED state, which is
* the terminal state.
*
* Note that non-1RTT ELs cannot undergo key update, therefore a non-1RT EL is
* always in the NORMAL substate if it is in the PROVISIONED state.
*/

/*
* Return the current RX key epoch. This is initially zero and is incremented by
* one for every Key Update successfully signalled by the peer.
* Return the current RX key epoch for the 1-RTT encryption level. This is
* initially zero and is incremented by one for every Key Update successfully
* signalled by the peer. If the 1-RTT EL has not yet been provisioned or has
* been discarded, returns UINT64_MAX.
*
* A necessary implication of this API is that the least significant bit of the
* returned value corresponds to the currently expected Key Phase bit, though
Expand All @@ -438,21 +456,33 @@ int ossl_qrx_set_early_validation_cb(OSSL_QRX *qrx,
* and use it to initiate a key update on the TX side.
*
* The value returned by this function increments specifically at the transition
* from the NORMAL to the UPDATE_CONFIRMED state discussed above.
* from the NORMAL to the UPDATING state discussed above.
*/
uint64_t ossl_qrx_get_key_epoch(OSSL_QRX *qrx);

/*
* The caller should call this after the UPDATE_CONFIRMED state is reached,
* after a timeout to be determined by the caller.
* Sets an optional callback which will be called when the key epoch changes.
*
* The callback is optional and can be unset by passing NULL for cb.
* cb_arg is an opaque value passed to cb.
*/
typedef void (ossl_qrx_key_update_cb)(void *arg);

int ossl_qrx_set_key_update_cb(OSSL_QRX *qrx,
ossl_qrx_key_update_cb *cb, void *cb_arg);

/*
* Relates to the 1-RTT encryption level. The caller should call this after the
* UPDATING state is reached, after a timeout to be determined by the caller.
*
* This transitions from the UPDATE_CONFIRMED state to the COOLDOWN state (if
* still in the UPDATE_CONFIRMED state). If normal is 1, then transitions from
* This transitions from the UPDATING state to the COOLDOWN state (if
* still in the UPDATING state). If normal is 1, then transitions from
* the COOLDOWN state to the NORMAL state. Both transitions can be performed at
* once if desired.
*
* If in the normal state, or if in the COOLDOWN state and normal is 0, this is
* a no-op and returns 1.
* a no-op and returns 1. Returns 0 if the 1-RTT EL has not been provisioned or
* has been dropped.
*
* It is essential that the caller call this within a few PTO intervals of a key
* update occurring (as detected by the caller in a call to
Expand All @@ -470,19 +500,22 @@ int ossl_qrx_key_update_timeout(OSSL_QRX *qrx, int normal);
/*
* Returns the number of seemingly forged packets which have been received by
* the QRX. If this value reaches the value returned by
* ossl_qrx_get_max_epoch_forged_pkt_count(), all further received encrypted
* packets will be discarded without processing; thus, callers should trigger a
* key update on the TX side (which will cause the peer to trigger a key update
* on our RX side) well before this occurs.
* ossl_qrx_get_max_epoch_forged_pkt_count() for a given EL, all further
* received encrypted packets for that EL will be discarded without processing.
*
* Note that the forged packet limit is for the connection lifetime, thus it is
* not reset by a key update. It is suggested that the caller terminate the
* connection a reasonable margin before the limit is reached. However, the
* exact limit imposed does vary by EL due to the possibility that different ELs
* use different AEADs.
*/
uint64_t ossl_qrx_get_cur_epoch_forged_pkt_count(OSSL_QRX *qrx,
uint32_t enc_level);
uint64_t ossl_qrx_get_cur_forged_pkt_count(OSSL_QRX *qrx);

/*
* Returns the maximum number of forged packets which the record layer
* will permit to be verified using the current set of RX keys.
* Returns the maximum number of forged packets which the record layer will
* permit to be verified using this QRX instance.
*/
uint64_t ossl_qrx_get_max_epoch_forged_pkt_count(OSSL_QRX *qrx,
uint32_t enc_level);
uint64_t ossl_qrx_get_max_forged_pkt_count(OSSL_QRX *qrx,
uint32_t enc_level);

#endif
11 changes: 11 additions & 0 deletions include/internal/quic_record_tx.h
Expand Up @@ -163,7 +163,18 @@ typedef struct ossl_qtx_pkt_st {
* encoded packet should be by setting pkt->hdr->pn_len. This function takes
* care of the PN encoding. Set pkt->pn to the desired PN.
*
* Note that 1-RTT packets do not have a DCID Length field, therefore the DCID
* length must be understood contextually. This function assumes the caller
* knows what it is doing and will serialize a DCID of whatever length is given.
* It is the caller's responsibility to ensure it uses a consistent DCID length
* for communication with any given set of remote peers.
*
* The packet is queued regardless of whether it is able to be sent immediately.
* This enables packets to be batched and sent at once on systems which support
* system calls to send multiple datagrams in a single system call (see
* BIO_sendmmsg). To flush queued datagrams to the network, see
* ossl_qtx_flush_net().
*
* Returns 1 on success or 0 on failure.
*/
int ossl_qtx_write_pkt(OSSL_QTX *qtx, const OSSL_QTX_PKT *pkt);
Expand Down
20 changes: 10 additions & 10 deletions include/internal/quic_types.h
Expand Up @@ -31,16 +31,16 @@ static ossl_unused ossl_inline uint32_t
ossl_quic_enc_level_to_pn_space(uint32_t enc_level)
{
switch (enc_level) {
case QUIC_ENC_LEVEL_INITIAL:
return QUIC_PN_SPACE_INITIAL;
case QUIC_ENC_LEVEL_HANDSHAKE:
return QUIC_PN_SPACE_HANDSHAKE;
case QUIC_ENC_LEVEL_0RTT:
case QUIC_ENC_LEVEL_1RTT:
return QUIC_PN_SPACE_APP;
default:
assert(0);
return QUIC_PN_SPACE_APP;
case QUIC_ENC_LEVEL_INITIAL:
return QUIC_PN_SPACE_INITIAL;
case QUIC_ENC_LEVEL_HANDSHAKE:
return QUIC_PN_SPACE_HANDSHAKE;
case QUIC_ENC_LEVEL_0RTT:
case QUIC_ENC_LEVEL_1RTT:
return QUIC_PN_SPACE_APP;
default:
assert(0);
return QUIC_PN_SPACE_APP;
}
}

Expand Down

0 comments on commit 948c656

Please sign in to comment.