Update the record layer design based on implementation experience
authorMatt Caswell <matt@openssl.org>
Tue, 8 Nov 2022 14:52:55 +0000 (14:52 +0000)
committerHugo Landau <hlandau@openssl.org>
Fri, 18 Nov 2022 07:02:55 +0000 (07:02 +0000)
Reviewed-by: Tomas Mraz <tomas@openssl.org>
Reviewed-by: Hugo Landau <hlandau@openssl.org>
(Merged from https://github.com/openssl/openssl/pull/17969)

doc/designs/quic-design/record-layer.md

index 0f952127a218193dcde49a488e308de797499ff8..a68c4690001e3bf6689a475ea8bda1c7f44180f6 100644 (file)
@@ -1,21 +1,19 @@
 Design Problem: Abstract Record Layer
 =====================================
 
-This document covers the design of an abstract record layer for use in (D)TLS
-and QUIC.
+This document covers the design of an abstract record layer for use in (D)TLS.
+The QUIC record layer is handled separately.
 
 A record within this document refers to a packet of data. It will typically
-contain some form of header data and some payload data, and will often be
+contain some header data and some payload data, and will often be
 cryptographically protected. A record may or may not have a one-to-one
-correspondence with network packets, depending on the protocol and the
-implementation details of an individual record layer.
+correspondence with network packets, depending on the implementation details of
+an individual record layer.
 
-The term record comes from the TLS and DTLS specifications. It is not used in
-QUIC. A TLS record is roughly analagous to a QUIC packet. For the sake of
-simplicity in this document we use the term record to refer to both concepts.
+The term record comes directly from the TLS and DTLS specifications.
 
-Libssl in OpenSSL 3.0 supports a number of different types of record layer,
-and record layer variants:
+Libssl supports a number of different types of record layer, and record layer
+variants:
 
 - Standard TLS record layer
 - Standard DTLS record layer
@@ -23,19 +21,19 @@ and record layer variants:
 
 Within the TLS record layer there are options to handle "multiblock" and
 "pipelining" which are different approaches for supporting the reading or
-writing of multiple records at the same time.
+writing of multiple records at the same time. All record layer variants also
+have to be able to handle different protocol versions.
 
-These different record layer implementations and variants have each been added
-at different times and over many years. The result is that each have taken
-slightly different approaches for achieving the goals that were appropriate at
-the time and the integration points where they have been added are spread
-throughout the code.
+These different record layer implementations, variants and protocol versions
+have each been added at different times and over many years. The result is that
+each took slightly different approaches for achieving the goals that were
+appropriate at the time and the integration points where they were added were
+spread throughout the code.
 
-The introduction of QUIC support will see the implementation of two more record
-layers:
-- QUIC record layer: A record here refers to a QUIC packet
-- QUIC-TLS record layer: This refers to the "inner" TLS implementation used by
-  QUIC. Records here will be QUIC CRYPTO frames.
+The introduction of QUIC support will see the implementation of a new record
+layer, i.e. the QUIC-TLS record layer. This refers to the "inner" TLS
+implementation used by QUIC. Records here will be in the form of QUIC CRYPTO
+frames.
 
 Requirements
 ------------
@@ -61,15 +59,17 @@ lists these requirements that are relevant to the record layer:
   it should be possible for external libraries to be able to use the pluggable
   record layer interface and it should offer a stable ABI (via a provider).
 
-* The internal architecture should allow for the fact that we may want to
-  support "single copy" APIs in the future
-
 The MVP requirements are:
 
 * a pluggable record layer (not public for MVP)
 
-Candidate Solution: Use a METHOD based approach
-------------------------------------------------
+Candidate Solutions that were considered
+----------------------------------------
+
+This section outlines two different solution approaches that were considered for
+the abstract record layer
+
+### Use a METHOD based approach
 
 A METHOD based approach is simply a structure containing function pointers. It
 is a common pattern in the OpenSSL codebase. Different strategies for
@@ -78,8 +78,8 @@ the caller of the METHOD.
 
 In this solution we would seek to implement a different METHOD for each of the
 types of record layer that we support, i.e. there would be one for the standard
-TLS record layer, one for the standard DTLS record layer, one for kernel TLS,
-one for QUIC and one for QUIC-TLS.
+TLS record layer, one for the standard DTLS record layer, one for kernel TLS and
+one for QUIC-TLS.
 
 In the MVP the METHOD approach would be private. However, once it has
 stabilised, it would be straight forward to supply public functions to enable
@@ -91,6 +91,7 @@ the MVP could implement a METHOD based approach, and subsequent releases could
 convert the METHODs into fully fetchable algorithms.
 
 Pros:
+
 * Simple approach that has been used historically in OpenSSL
 * Could be used as the basis for the final public solution
 * Could also be used as the basis for a fetchable solution in a subsequent
@@ -100,13 +101,13 @@ Pros:
   later release
 
 Cons:
+
 * Not consistent with the provider based approach we used for extensibility in
   3.0
 * If this option is implemented and later converted to a fetchable solution then
   some rework might be required
 
-Candidate Solution: Use a provider based approach
--------------------------------------------------
+### Use a provider based approach
 
 This approach is very similar to the alternative METHOD based approach. The
 main difference is that the record layer implementations would be held in
@@ -122,22 +123,30 @@ functions that can be implemented. Additionally implementing the infrastructure
 for a new fetchable operation is more involved than a METHOD based approach.
 
 Pros:
+
 * Consistent with the extensibility solution used in 3.0
 * If this option is implemented immediately in the MVP then it would avoid later
   rework if adopted in a subsequent release
 
 Cons:
+
 * More complicated to implement than the simple METHOD based approach
 * Cannot pass complex objects across the provider boundary
 
+### Selected solution
+
+The METHOD based approach has been selected for MVP, with the expectation that
+subsequent releases will convert it to a full provider based solution accessible
+to third party applications.
 
-Solution Outline: Use a METHOD based approach
----------------------------------------------
+Solution Description: The METHOD based approach
+-----------------------------------------------
 
-This section focuses on the "Use a METHOD based approach" candidate solution
-above and further elaborates a design for how that approach might work.
+This section focuses on the selected approach of using METHODs and further
+elaborates on how the design works.
 
-A proposed internal record method API is given in [Appendix A](#appendix-a).
+A proposed internal record method API is given in
+[Appendix A](#appendix-a-the-internal-record-method-api).
 
 An `OSSL_RECORD_METHOD` represents the implementation of a particular type of
 record layer. It contains a set of function pointers to represent the various
@@ -157,56 +166,40 @@ the kernel TLS record layer once the handshake is complete.
 
 A new `OSSL_RECORD_LAYER` is created by calling the `new` function of the
 associated `OSSL_RECORD_METHOD`, and freed by calling the `free` function. The
-internal structure details of an `OSSL_RECORD_LAYER` are entirely hidden to
-libssl and will be specific to the given `OSSL_RECORD_METHOD`.
-
-The payload for a record that will be written out to the network will be
-assembled by libssl. That payload may be spread across multiple buffers. For
-example a QUIC record (packet) may consist of multiple frames. There might be a
-buffer containing the header data for the first frame, followed by a second
-buffer containing the contents of the frame. A third buffer might contain the
-header data for a second frame, and a fourth buffer might contain the payload
-data for the second frame. There can be an arbitrary number of buffers which,
-when concatenated together, form the total payload for the whole record. This
-approach means that libssl can avoid having to copy all of the data from
-multiple sources into a single buffer before calling the record layer.
-
-All of the above data for a single record will be represented by an
-`OSSL_RECORD_TEMPLATE` structure.
-
-In order to assemble a record, libssl will need to know the maximum length of a
-record that can be supported by the `OSSL_RECORD_LAYER`. In order to support
-this an `OSSL_RECORD_METHOD` will supply a `get_max_record_len()` function to
-query this value. It will be libssl's responsibility to ensure that no record
-exceeds the maximum supported record length.
-
-An `OSSL_RECORD_METHOD` supplies a `write_records` function which libssl can
-call to write one or more records. Libssl will supply an array of
-`OSSL_RECORD_TEMPLATE` objects along with the number of such templates. This
-number is guaranteed to never be greater than the maximum number of records
-that the record layer can handle at one time as returned by the
-`get_max_records()` function.
+parameters to the `new` function also supply all of the cryptographic state
+(e.g. keys, ivs, symmetric encryption algorithms, hash algorithm etc) used by
+the record layer. The internal structure details of an `OSSL_RECORD_LAYER` are
+entirely hidden to the rest of libssl and can be specific to the given
+`OSSL_RECORD_METHOD`. In practice the standard internal TLS, DTLS and KTLS
+`OSSL_RECORD_METHOD`s all use a common `OSSL_RECORD_LAYER` structure. However
+the QUIC-TLS implementation is likely to use a different structure layout.
+
+All of the header and payload data for a single record will be represented by an
+`OSSL_RECORD_TEMPLATE` structure when writing. Libssl will construct a set of
+templates for records to be written out and pass them to the "write" record
+layer. In most cases only a single record is ever written out at one time,
+however there are some cases (such as when using the "pipelining" or
+"multibuffer" optimisations) that multiple records can be written in one go.
+
+It is the record layer's responsibility to know whether it can support multiple
+records in one go or not. It is libssl's responsibility to split the payload
+data into `OSSL_RECORD_TEMPLATE` objects. Libssl will call the record layer's
+`get_max_records()` function to determine how many records a given payload
+should be split into. If that value is more than one, then libssl will construct
+(up to) that number of `OSSL_RECORD_TEMPLATE`s and pass the whole set to the
+record layer's `write_records()` function.
 
 The implementation of the `write_records` function must construct the
 appropriate number of records, apply protection to them as required and then
-write them out to the underlying transport layer BIO. Congestion or flow control
-limits may apply. The maximum amount of data that may be sent at the current
-time is supplied by libssl in the `allowance` parameter. It is the
-`OSSL_RECORD_METHOD`'s responsibily to ensure that no more bytes than
-`allowance` are transmitted via the transport layer BIO. In the event that not
-all the data can be transmitted at the current time (either because of the
-`allowance` limit, or because the underlying transport has indicated a retry),
-then the `write_records` function will return a "retry" response. It is
-permissible for the data to be partially sent, but this is still considered a
-"retry" until all of the data is sent. The `sent` parameter will be filled in
-with the number of bytes sent during this `write_records` call in both a
-success and a retry response.
+write them out to the underlying transport layer BIO. In the event that not
+all the data can be transmitted at the current time (e.g. because the underlying
+transport has indicated a retry), then the `write_records` function will return
+a "retry" response. It is permissible for the data to be partially sent, but
+this is still considered a "retry" until all of the data is sent.
 
 On a success or retry response libssl may free its buffers immediately. The
 `OSSL_RECORD_LAYER` object will have to buffer any untransmitted data until it
-is eventually sent. The move of data from the input buffers to the internal
-`OSSL_RECORD_METHOD` buffer should occur during packet protection and is the
-"single copy" allowed by the requirements.
+is eventually sent.
 
 If a "retry" occurs, then libssl will subsequently call `retry_write_records`
 and continue to do so until a success return value is received. Libssl will
@@ -223,23 +216,81 @@ data contained in it. Each record has an associated opaque handle `rechandle`.
 The record data must remain buffered by the `OSSL_RECORD_LAYER` until it has
 been released via a call to `release_record()`.
 
+A record layer implementation supplies various functions to enable libssl to
+query the current state. In particular:
 
-<a id='appendix-a'></a>Appendix A: An internal record method API
-----------------------------------------------------------------
-A proposed internal recordmethod.h header file for the record method API:
+`unprocessed_read_pending()`: to query whether there is data buffered that has
+already been read from the underlying BIO, but not yet processed.
 
-```` C
-/*
- * Copyright 2022 The OpenSSL Project Authors. All Rights Reserved.
- *
- * Licensed under the Apache License 2.0 (the "License").  You may not use
- * this file except in compliance with the License.  You can obtain a copy
- * in the file LICENSE in the source distribution or at
- * https://www.openssl.org/source/license.html
- */
+`processed_read_pending()`: to query whether there is data buffered that has
+been read from the underlying BIO and has been processed. The data is not
+necessarily application data.
+
+`app_data_pending()`: to query the amount of processed application data that is
+buffered and available for immediate read.
+
+`get_alert_code()`: to query the alert code that should be used in the event
+that a previous attempt to read or write records failed.
+
+`get_state()`: to obtain a printable string to describe the current state of the
+record layer.
+
+`get_compression()`: to obtain information about the compression method
+currently being used by the record layer.
+
+`get_max_record_overhead()`: to obtain the maximum amount of bytes the record
+layer will add to the payload bytes before transmission. This does not include
+any expansion that might occur during compression. Currently this is only
+implemented for DTLS.
+
+In addition, libssl will tell the record layer about various events that might
+occur that are relevant to the record layer's operation:
+
+`set1_bio()`: called if the underlying BIO being used by the record layer has
+been changed.
+
+`set_protocol_version()`: called during protocol version negotiation when a
+specific protocol version has been selected.
+
+`set_plain_alerts()`: to indicate that receiving unencrypted alerts is allowed
+in the current context, even if normally we would expect to receive encrypted
+data. This is only relevant for TLSv1.3.
+
+`set_first_handshake()`: called at the beginning and end of the first handshake
+for any given (D)TLS connection.
 
-#include <openssl/ssl.h>
+`set_max_pipelines()`: called to configure the maximum number of pipelines of
+data that the record layer should process in one go. By default this is 1.
 
+`set_in_init()`: called by libssl to tell the record layer whether we are
+currently `in_init` or not. Defaults to "true".
+
+`set_options()`: called by libssl in the event that the current set of options
+to use has been updated.
+
+`set_max_frag_len()`: called by libssl to set the maximum allowed fragment
+length that is in force at the moment. This might be the result of user
+configuration, or it may be negotiated during the handshake.
+
+`increment_sequence_ctr()`: force the record layer to increment its sequence
+counter. In most cases the record layer will entirely manage its own sequence
+counters. However in the DTLSv1_listen() corner case, libssl needs to initialise
+the record layer with an incremented sequence counter.
+
+`alloc_buffers()`: called by libssl to request that the record layer allocate
+its buffers. This is a hint only and the record layer is expected to manage its
+own buffer allocation and freeing.
+
+`free_buffers()`: called by libssl to request that the record layer free its
+buffers. This is a hint only and the record layer is expected to manage its own
+buffer allocation and freeing.
+
+Appendix A: The internal record method API
+------------------------------------------
+
+The internal recordmethod.h header file for the record method API:
+
+```` C
 /*
  * We use the term "record" here to refer to a packet of data. Records are
  * typically protected via a cipher and MAC, or an AEAD cipher (although not
@@ -250,27 +301,6 @@ A proposed internal recordmethod.h header file for the record method API:
  * refer to both contexts.
  */
 
-
-/*
- * Types of QUIC record layer;
- *
- * QUIC reuses the TLS handshake for agreeing secrets. An SSL object representing
- * a QUIC connection will have an additional SSL object internally representing
- * the TLS state of the QUIC handshake. This internal TLS is referred to as
- * QUIC-TLS in this file.
- * "Records" output from QUIC-TLS contains standard TLS handshake messages and
- * are *not* encrypted directly but are instead wrapped up in plaintext
- * CRYPTO frames. These CRYPTO frames could be collected together with other
- * QUIC frames into a single QUIC packet. The QUIC record layer will then
- * encrypt the whole packet.
- *
- * So we have:
- * QUIC-TLS record layer: outputs plaintext CRYPTO frames containing TLS
- *                        handshake messages only.
- * QUIC record layer: outputs encrypted packets which may contain CRYPTO frames
- *                    or any other type of QUIC frame.
- */
-
 /*
  * An OSSL_RECORD_METHOD is a protcol specific method which provides the
  * functions for reading and writing records for that protocol. Which
@@ -285,33 +315,36 @@ typedef struct ossl_record_method_st OSSL_RECORD_METHOD;
 typedef struct ossl_record_layer_st OSSL_RECORD_LAYER;
 
 
-#define OSSL_RECORD_ROLE_CLIENT 0
-#define OSSL_RECORD_ROLE_SERVER 1
+# define OSSL_RECORD_ROLE_CLIENT 0
+# define OSSL_RECORD_ROLE_SERVER 1
 
-#define OSSL_RECORD_DIRECTION_READ  0
-#define OSSL_RECORD_DIRECTION_WRITE 1
+# define OSSL_RECORD_DIRECTION_READ  0
+# define OSSL_RECORD_DIRECTION_WRITE 1
 
 /*
  * Protection level. For <= TLSv1.2 only "NONE" and "APPLICATION" are used.
  */
-#define OSSL_RECORD_PROTECTION_LEVEL_NONE        0
-#define OSSL_RECORD_PROTECTION_LEVEL_EARLY       1
-#define OSSL_RECORD_PROTECTION_LEVEL_HANDSHAKE   2
-#define OSSL_RECORD_PROTECTION_LEVEL_APPLICATION 3
+# define OSSL_RECORD_PROTECTION_LEVEL_NONE        0
+# define OSSL_RECORD_PROTECTION_LEVEL_EARLY       1
+# define OSSL_RECORD_PROTECTION_LEVEL_HANDSHAKE   2
+# define OSSL_RECORD_PROTECTION_LEVEL_APPLICATION 3
 
+# define OSSL_RECORD_RETURN_SUCCESS           1
+# define OSSL_RECORD_RETURN_RETRY             0
+# define OSSL_RECORD_RETURN_NON_FATAL_ERR    -1
+# define OSSL_RECORD_RETURN_FATAL            -2
+# define OSSL_RECORD_RETURN_EOF              -3
 
 /*
  * Template for creating a record. A record consists of the |type| of data it
- * will contain (e.g. alert, handshake, application data, etc) along with an
- * array of buffers in |bufs| of size |numbufs|. There is a corresponding array
- * of buffer lengths in |buflens|. Concatenating all of the buffer data together
- * would give you the complete plaintext payload to be sent in a single record.
+ * will contain (e.g. alert, handshake, application data, etc) along with a
+ * buffer of payload data in |buf| of length |buflen|.
  */
 struct ossl_record_template_st {
     int type;
-    void **bufs;
-    size_t *buflens;
-    size_t numbufs;
+    unsigned int version;
+    const unsigned char *buf;
+    size_t buflen;
 };
 
 typedef struct ossl_record_template_st OSSL_RECORD_TEMPLATE;
@@ -350,64 +383,86 @@ struct ossl_record_method_st {
      * force at any one time (one for reading and one for writing). In some
      * protocols more than 2 might be used (e.g. in DTLS for retransmitting
      * messages from an earlier epoch).
+     *
+     * The created OSSL_RECORD_LAYER object is stored in *ret on success (or
+     * NULL otherwise). The return value will be one of
+     * OSSL_RECORD_RETURN_SUCCESS, OSSL_RECORD_RETURN_FATAL or
+     * OSSL_RECORD_RETURN_NON_FATAL. A non-fatal return means that creation of
+     * the record layer has failed because it is unsuitable, but an alternative
+     * record layer can be tried instead.
      */
 
     /*
-     * TODO: Will have to be something other than SSL_CIPHER if we make this
-     * fetchable
+     * If we eventually make this fetchable then we will need to use something
+     * other than EVP_CIPHER. Also mactype would not be a NID, but a string. For
+     * now though, this works.
      */
-    OSSL_RECORD_LAYER *new(int vers, int role, int direction, int level,
-                           unsigned char *secret, size_t secretlen,
-                           SSL_CIPHER *c, BIO *transport, BIO_ADDR *local,
-                           BIO_ADDR *peer, OSSL_PARAM *settings,
-                           OSSL_PARAM *options);
-    void free(OSSL_RECORD_LAYER *rl);
-
-    int reset(OSSL_RECORD_LAYER *rl); /* Is this needed? */
+    int (*new_record_layer)(OSSL_LIB_CTX *libctx,
+                            const char *propq, int vers,
+                            int role, int direction,
+                            int level,
+                            uint16_t epoch,
+                            unsigned char *key,
+                            size_t keylen,
+                            unsigned char *iv,
+                            size_t ivlen,
+                            unsigned char *mackey,
+                            size_t mackeylen,
+                            const EVP_CIPHER *ciph,
+                            size_t taglen,
+                            int mactype,
+                            const EVP_MD *md,
+                            COMP_METHOD *comp,
+                            BIO *prev,
+                            BIO *transport,
+                            BIO *next,
+                            BIO_ADDR *local,
+                            BIO_ADDR *peer,
+                            const OSSL_PARAM *settings,
+                            const OSSL_PARAM *options,
+                            const OSSL_DISPATCH *fns,
+                            void *cbarg,
+                            OSSL_RECORD_LAYER **ret);
+    int (*free)(OSSL_RECORD_LAYER *rl);
+
+    int (*reset)(OSSL_RECORD_LAYER *rl); /* Is this needed? */
 
     /* Returns 1 if we have unprocessed data buffered or 0 otherwise */
-    int unprocessed_read_pending(OSSL_RECORD_LAYER *rl);
+    int (*unprocessed_read_pending)(OSSL_RECORD_LAYER *rl);
+
     /*
      * Returns 1 if we have processed data buffered that can be read or 0 otherwise
      * - not necessarily app data
      */
-    int processed_read_pending(OSSL_RECORD_LAYER *rl);
+    int (*processed_read_pending)(OSSL_RECORD_LAYER *rl);
 
     /*
      * The amount of processed app data that is internally bufferred and
      * available to read
      */
-    size_t app_data_pending(OSSL_RECORD_LAYER *rl);
-
-    int write_pending(OSSL_RECORD_LAYER *rl);
-
-
-    /*
-     * Find out the maximum amount of plaintext data that the record layer is
-     * prepared to write in a single record. When calling write_records it is
-     * the caller's responsibility to ensure that no record template exceeds
-     * this maximum when calling write_records.
-     */
-    size_t get_max_record_len(OSSL_RECORD_LAYER *rl);
+    size_t (*app_data_pending)(OSSL_RECORD_LAYER *rl);
 
     /*
      * Find out the maximum number of records that the record layer is prepared
      * to process in a single call to write_records. It is the caller's
      * responsibility to ensure that no call to write_records exceeds this
-     * number of records.
+     * number of records. |type| is the type of the records that the caller
+     * wants to write, and |len| is the total amount of data that it wants
+     * to send. |maxfrag| is the maximum allowed fragment size based on user
+     * configuration, or TLS parameter negotiation. |*preffrag| contains on
+     * entry the default fragment size that will actually be used based on user
+     * configuration. This will always be less than or equal to |maxfrag|. On
+     * exit the record layer may update this to an alternative fragment size to
+     * be used. This must always be less than or equal to |maxfrag|.
      */
-    size_t get_max_records(OSSL_RECORD_LAYER *rl);
+    size_t (*get_max_records)(OSSL_RECORD_LAYER *rl, int type, size_t len,
+                              size_t maxfrag, size_t *preffrag);
 
     /*
      * Write |numtempl| records from the array of record templates pointed to
      * by |templates|. Each record should be no longer than the value returned
      * by get_max_record_len(), and there should be no more records than the
      * value returned by get_max_records().
-     * |allowance| is the maximum amount of "on-the-wire" data that is allowed
-     * to be sent at the moment (including all QUIC headers, but excluding any
-     * UDP/IP headers). After a successful or retry return |*sent| will
-     * be updated with the amount of data that has been sent so far. In the case
-     * of a retry this could be 0.
      * Where possible the caller will attempt to ensure that all records are the
      * same length, except the last record. This may not always be possible so
      * the record method implementation should not rely on this being the case.
@@ -423,24 +478,19 @@ struct ossl_record_method_st {
      *  0 on retry
      * -1 on failure
      */
-    int write_records(OSSL_RECORD_LAYER *rl, OSSL_RECORD_TEMPLATE **templates,
-                      size_t numtempl, size_t allowance, size_t *sent);
+    int (*write_records)(OSSL_RECORD_LAYER *rl, OSSL_RECORD_TEMPLATE *templates,
+                         size_t numtempl);
 
     /*
      * Retry a previous call to write_records. The caller should continue to
      * call this until the function returns with success or failure. After
-     * each retry more of the data may have been incrementally sent. |allowance|
-     * is the amount of "on-the-wire" data that is allowed to be sent at the
-     * moment. After a successful or retry return |*sent| will
-     * be updated with the amount of data that has been sent by this call to
-     * retry_write_records().
+     * each retry more of the data may have been incrementally sent.
      * Returns:
      *  1 on success
      *  0 on retry
      * -1 on failure
      */
-    int retry_write_records(OSSL_RECORD_LAYER *rl, size_t allowance,
-                            size_t *sent);
+    int (*retry_write_records)(OSSL_RECORD_LAYER *rl);
 
     /*
      * Read a record and return the record layer version and record type in
@@ -456,15 +506,99 @@ struct ossl_record_method_st {
      * Internally the the OSSL_RECORD_METHOD the implementation may read/process
      * multiple records in one go and buffer them.
      */
-    int read_record(OSSL_RECORD_LAYER *rl, void **rechandle, int *rversion,
-                    int *type, unsigned char **data, size_t *datalen,
-                    uint16_t *epoch, unsigned char *seq_num);
+    int (*read_record)(OSSL_RECORD_LAYER *rl, void **rechandle, int *rversion,
+                      int *type, unsigned char **data, size_t *datalen,
+                      uint16_t *epoch, unsigned char *seq_num);
     /*
      * Release a buffer associated with a record previously read with
      * read_record. Records are guaranteed to be released in the order that they
      * are read.
      */
-    void release_record(OSSL_RECORD_LAYER *rl, void *rechandle);
+    int (*release_record)(OSSL_RECORD_LAYER *rl, void *rechandle);
+
+    /*
+     * In the event that a fatal error is returned from the functions above then
+     * get_alert_code() can be called to obtain a more details identifier for
+     * the error. In (D)TLS this is the alert description code.
+     */
+    int (*get_alert_code)(OSSL_RECORD_LAYER *rl);
+
+    /*
+     * Update the transport BIO from the one originally set in the
+     * new_record_layer call
+     */
+    int (*set1_bio)(OSSL_RECORD_LAYER *rl, BIO *bio);
+
+    /* Called when protocol negotiation selects a protocol version to use */
+    int (*set_protocol_version)(OSSL_RECORD_LAYER *rl, int version);
+
+    /*
+     * Whether we are allowed to receive unencrypted alerts, even if we might
+     * otherwise expect encrypted records. Ignored by protocol versions where
+     * this isn't relevant
+     */
+    void (*set_plain_alerts)(OSSL_RECORD_LAYER *rl, int allow);
 
+    /*
+     * Called immediately after creation of the record layer if we are in a
+     * first handshake. Also called at the end of the first handshake
+     */
+    void (*set_first_handshake)(OSSL_RECORD_LAYER *rl, int first);
+
+    /*
+     * Set the maximum number of pipelines that the record layer should process.
+     * The default is 1.
+     */
+    void (*set_max_pipelines)(OSSL_RECORD_LAYER *rl, size_t max_pipelines);
+
+    /*
+     * Called to tell the record layer whether we are currently "in init" or
+     * not. Default at creation of the record layer is "yes".
+     */
+    void (*set_in_init)(OSSL_RECORD_LAYER *rl, int in_init);
+
+    /*
+     * Get a short or long human readable description of the record layer state
+     */
+    void (*get_state)(OSSL_RECORD_LAYER *rl, const char **shortstr,
+                      const char **longstr);
+
+    /*
+     * Set new options or modify ones that were originaly specified in the
+     * new_record_layer call.
+     */
+    int (*set_options)(OSSL_RECORD_LAYER *rl, const OSSL_PARAM *options);
+
+    const COMP_METHOD *(*get_compression)(OSSL_RECORD_LAYER *rl);
+
+    /*
+     * Set the maximum fragment length to be used for the record layer. This
+     * will override any previous value supplied for the "max_frag_len"
+     * setting during construction of the record layer.
+     */
+    void (*set_max_frag_len)(OSSL_RECORD_LAYER *rl, size_t max_frag_len);
+
+    /*
+     * The maximum expansion in bytes that the record layer might add while
+     * writing a record
+     */
+    size_t (*get_max_record_overhead)(OSSL_RECORD_LAYER *rl);
+
+    /*
+     * Increment the record sequence number
+     */
+    int (*increment_sequence_ctr)(OSSL_RECORD_LAYER *rl);
+
+    /*
+     * Allocate read or write buffers. Does nothing if already allocated.
+     * Assumes default buffer length and 1 pipeline.
+     */
+    int (*alloc_buffers)(OSSL_RECORD_LAYER *rl);
+
+    /*
+     * Free read or write buffers. Fails if there is pending read or write
+     * data. Buffers are automatically reallocated on next read/write.
+     */
+    int (*free_buffers)(OSSL_RECORD_LAYER *rl);
 };
 ````