]> git.ipfire.org Git - thirdparty/ipxe.git/commitdiff
[tls] Split received records over multiple I/O buffers
authorMichael Brown <mcb30@ipxe.org>
Wed, 26 Sep 2012 20:42:23 +0000 (21:42 +0100)
committerMichael Brown <mcb30@ipxe.org>
Thu, 27 Sep 2012 00:56:01 +0000 (01:56 +0100)
TLS servers are not obliged to implement the RFC3546 maximum fragment
length extension, and many common servers (including OpenSSL, as used
in Apache's mod_ssl) do not do so.  iPXE may therefore have to cope
with TLS records of up to 16kB.  Allocations for 16kB have a
non-negligible chance of failing, causing the TLS connection to abort.

Fix by maintaining the received record as a linked list of I/O
buffers, rather than a single contiguous buffer.  To reduce memory
pressure, we also decrypt in situ, and deliver the decrypted data via
xfer_deliver_iob() rather than xfer_deliver_raw().

Signed-off-by: Michael Brown <mcb30@ipxe.org>
src/include/ipxe/tls.h
src/net/tls.c

index 2af864dfeb2ae420f72e618b152689d5418eded3..f8a754096e6860bdc2b1f2fd130c1f2f882197f4 100644 (file)
@@ -19,6 +19,7 @@ FILE_LICENCE ( GPL2_OR_LATER );
 #include <ipxe/sha256.h>
 #include <ipxe/x509.h>
 #include <ipxe/pending.h>
+#include <ipxe/iobuf.h>
 
 /** A TLS header */
 struct tls_header {
@@ -264,14 +265,35 @@ struct tls_session {
        uint64_t rx_seq;
        /** RX state */
        enum tls_rx_state rx_state;
-       /** Offset within current RX state */
-       size_t rx_rcvd;
        /** Current received record header */
        struct tls_header rx_header;
-       /** Current received raw data buffer */
-       void *rx_data;
+       /** Current received record header (static I/O buffer) */
+       struct io_buffer rx_header_iobuf;
+       /** List of received data buffers */
+       struct list_head rx_data;
 };
 
+/** RX I/O buffer size
+ *
+ * The maximum fragment length extension is optional, and many common
+ * implementations (including OpenSSL) do not support it.  We must
+ * therefore be prepared to receive records of up to 16kB in length.
+ * The chance of an allocation of this size failing is non-negligible,
+ * so we must split received data into smaller allocations.
+ */
+#define TLS_RX_BUFSIZE 4096
+
+/** Minimum RX I/O buffer size
+ *
+ * To simplify manipulations, we ensure that no RX I/O buffer is
+ * smaller than this size.  This allows us to assume that the MAC and
+ * padding are entirely contained within the final I/O buffer.
+ */
+#define TLS_RX_MIN_BUFSIZE 512
+
+/** RX I/O buffer alignment */
+#define TLS_RX_ALIGN 16
+
 extern int add_tls ( struct interface *xfer, const char *name,
                     struct interface **next );
 
index ec2763b57822d9a6efd1a99ea8a3ab4cef0f9b97..45b7e5250b9f3075b645015893c3abaa023b221b 100644 (file)
@@ -101,6 +101,14 @@ FILE_LICENCE ( GPL2_OR_LATER );
 #define EINFO_EINVAL_RX_STATE                                          \
        __einfo_uniqify ( EINFO_EINVAL, 0x0c,                           \
                          "Invalid receive state" )
+#define EINVAL_MAC __einfo_error ( EINFO_EINVAL_MAC )
+#define EINFO_EINVAL_MAC                                               \
+       __einfo_uniqify ( EINFO_EINVAL, 0x0d,                           \
+                         "Invalid MAC" )
+#define EINVAL_NON_DATA __einfo_error ( EINFO_EINVAL_NON_DATA )
+#define EINFO_EINVAL_NON_DATA                                          \
+       __einfo_uniqify ( EINFO_EINVAL, 0x0e,                           \
+                         "Overlength non-data record" )
 #define EIO_ALERT __einfo_error ( EINFO_EIO_ALERT )
 #define EINFO_EIO_ALERT                                                        \
        __einfo_uniqify ( EINFO_EINVAL, 0x01,                           \
@@ -125,10 +133,6 @@ FILE_LICENCE ( GPL2_OR_LATER );
 #define EINFO_ENOMEM_TX_CIPHERTEXT                                     \
        __einfo_uniqify ( EINFO_ENOMEM, 0x05,                           \
                          "Not enough space for transmitted ciphertext" )
-#define ENOMEM_RX_PLAINTEXT __einfo_error ( EINFO_ENOMEM_RX_PLAINTEXT )
-#define EINFO_ENOMEM_RX_PLAINTEXT                                      \
-       __einfo_uniqify ( EINFO_ENOMEM, 0x06,                           \
-                         "Not enough space for received plaintext" )
 #define ENOMEM_RX_DATA __einfo_error ( EINFO_ENOMEM_RX_DATA )
 #define EINFO_ENOMEM_RX_DATA                                           \
        __einfo_uniqify ( EINFO_ENOMEM, 0x07,                           \
@@ -159,7 +163,7 @@ FILE_LICENCE ( GPL2_OR_LATER );
                          "Handshake verification failed" )
 #define EPROTO_VERSION __einfo_error ( EINFO_EPROTO_VERSION )
 #define EINFO_EPROTO_VERSION                                           \
-       __einfo_uniqify ( EINFO_EINVAL, 0x01,                           \
+       __einfo_uniqify ( EINFO_EPROTO, 0x01,                           \
                          "Illegal protocol version upgrade" )
 
 static int tls_send_plaintext ( struct tls_session *tls, unsigned int type,
@@ -295,13 +299,18 @@ struct rsa_digestinfo_prefix rsa_md5_sha1_prefix __rsa_digestinfo_prefix = {
 static void free_tls ( struct refcnt *refcnt ) {
        struct tls_session *tls =
                container_of ( refcnt, struct tls_session, refcnt );
+       struct io_buffer *iobuf;
+       struct io_buffer *tmp;
 
        /* Free dynamically-allocated resources */
        tls_clear_cipher ( tls, &tls->tx_cipherspec );
        tls_clear_cipher ( tls, &tls->tx_cipherspec_pending );
        tls_clear_cipher ( tls, &tls->rx_cipherspec );
        tls_clear_cipher ( tls, &tls->rx_cipherspec_pending );
-       free ( tls->rx_data );
+       list_for_each_entry_safe ( iobuf, tmp, &tls->rx_data, list ) {
+               list_del ( &iobuf->list );
+               free_iob ( iobuf );
+       }
        x509_chain_put ( tls->chain );
 
        /* Free TLS structure itself */
@@ -1013,7 +1022,7 @@ static int tls_send_client_hello ( struct tls_session *tls ) {
        hello.extensions.max_fragment_length_len
                = htons ( sizeof ( hello.extensions.max_fragment_length ) );
        hello.extensions.max_fragment_length.max
-               = TLS_MAX_FRAGMENT_LENGTH_2048;
+               = TLS_MAX_FRAGMENT_LENGTH_4096;
 
        return tls_send_handshake ( tls, &hello, sizeof ( hello ) );
 }
@@ -1703,31 +1712,71 @@ static int tls_new_handshake ( struct tls_session *tls,
  *
  * @v tls              TLS session
  * @v type             Record type
- * @v data             Plaintext record
- * @v len              Length of plaintext record
+ * @v rx_data          List of received data buffers
  * @ret rc             Return status code
  */
 static int tls_new_record ( struct tls_session *tls, unsigned int type,
-                           const void *data, size_t len ) {
+                           struct list_head *rx_data ) {
+       struct io_buffer *iobuf;
+       int ( * handler ) ( struct tls_session *tls, const void *data,
+                           size_t len );
+       int rc;
+
+       /* Deliver data records to the plainstream interface */
+       if ( type == TLS_TYPE_DATA ) {
+
+               /* Fail unless we are ready to receive data */
+               if ( ! tls_ready ( tls ) )
+                       return -ENOTCONN;
 
+               /* Deliver each I/O buffer in turn */
+               while ( ( iobuf = list_first_entry ( rx_data, struct io_buffer,
+                                                    list ) ) ) {
+                       list_del ( &iobuf->list );
+                       if ( ( rc = xfer_deliver_iob ( &tls->plainstream,
+                                                      iobuf ) ) != 0 ) {
+                               DBGC ( tls, "TLS %p could not deliver data: "
+                                      "%s\n", tls, strerror ( rc ) );
+                               return rc;
+                       }
+               }
+               return 0;
+       }
+
+       /* For all other records, fail unless we have exactly one I/O buffer */
+       iobuf = list_first_entry ( rx_data, struct io_buffer, list );
+       assert ( iobuf != NULL );
+       list_del ( &iobuf->list );
+       if ( ! list_empty ( rx_data ) ) {
+               DBGC ( tls, "TLS %p overlength non-data record\n", tls );
+               return -EINVAL_NON_DATA;
+       }
+
+       /* Determine handler */
        switch ( type ) {
        case TLS_TYPE_CHANGE_CIPHER:
-               return tls_new_change_cipher ( tls, data, len );
+               handler = tls_new_change_cipher;
+               break;
        case TLS_TYPE_ALERT:
-               return tls_new_alert ( tls, data, len );
+               handler = tls_new_alert;
+               break;
        case TLS_TYPE_HANDSHAKE:
-               return tls_new_handshake ( tls, data, len );
-       case TLS_TYPE_DATA:
-               if ( ! tls_ready ( tls ) )
-                       return -ENOTCONN;
-               return xfer_deliver_raw ( &tls->plainstream, data, len );
+               handler = tls_new_handshake;
+               break;
        default:
                /* RFC4346 says that we should just ignore unknown
                 * record types.
                 */
+               handler = NULL;
                DBGC ( tls, "TLS %p ignoring record type %d\n", tls, type );
-               return 0;
+               break;
        }
+
+       /* Handle record and free I/O buffer */
+       if ( handler )
+               rc = handler ( tls, iobuf->data, iob_len ( iobuf ) );
+       free_iob ( iobuf );
+       return rc;
 }
 
 /******************************************************************************
@@ -1737,10 +1786,57 @@ static int tls_new_record ( struct tls_session *tls, unsigned int type,
  ******************************************************************************
  */
 
+/**
+ * Initialise HMAC
+ *
+ * @v cipherspec       Cipher specification
+ * @v ctx              Context
+ * @v seq              Sequence number
+ * @v tlshdr           TLS header
+ */
+static void tls_hmac_init ( struct tls_cipherspec *cipherspec, void *ctx,
+                           uint64_t seq, struct tls_header *tlshdr ) {
+       struct digest_algorithm *digest = cipherspec->suite->digest;
+
+       hmac_init ( digest, ctx, cipherspec->mac_secret, &digest->digestsize );
+       seq = cpu_to_be64 ( seq );
+       hmac_update ( digest, ctx, &seq, sizeof ( seq ) );
+       hmac_update ( digest, ctx, tlshdr, sizeof ( *tlshdr ) );
+}
+
+/**
+ * Update HMAC
+ *
+ * @v cipherspec       Cipher specification
+ * @v ctx              Context
+ * @v data             Data
+ * @v len              Length of data
+ */
+static void tls_hmac_update ( struct tls_cipherspec *cipherspec, void *ctx,
+                             const void *data, size_t len ) {
+       struct digest_algorithm *digest = cipherspec->suite->digest;
+
+       hmac_update ( digest, ctx, data, len );
+}
+
+/**
+ * Finalise HMAC
+ *
+ * @v cipherspec       Cipher specification
+ * @v ctx              Context
+ * @v mac              HMAC to fill in
+ */
+static void tls_hmac_final ( struct tls_cipherspec *cipherspec, void *ctx,
+                            void *hmac ) {
+       struct digest_algorithm *digest = cipherspec->suite->digest;
+
+       hmac_final ( digest, ctx, cipherspec->mac_secret,
+                    &digest->digestsize, hmac );
+}
+
 /**
  * Calculate HMAC
  *
- * @v tls              TLS session
  * @v cipherspec       Cipher specification
  * @v seq              Sequence number
  * @v tlshdr           TLS header
@@ -1748,21 +1844,15 @@ static int tls_new_record ( struct tls_session *tls, unsigned int type,
  * @v len              Length of data
  * @v mac              HMAC to fill in
  */
-static void tls_hmac ( struct tls_session *tls __unused,
-                      struct tls_cipherspec *cipherspec,
+static void tls_hmac ( struct tls_cipherspec *cipherspec,
                       uint64_t seq, struct tls_header *tlshdr,
                       const void *data, size_t len, void *hmac ) {
        struct digest_algorithm *digest = cipherspec->suite->digest;
-       uint8_t digest_ctx[digest->ctxsize];
+       uint8_t ctx[digest->ctxsize];
 
-       hmac_init ( digest, digest_ctx, cipherspec->mac_secret,
-                   &digest->digestsize );
-       seq = cpu_to_be64 ( seq );
-       hmac_update ( digest, digest_ctx, &seq, sizeof ( seq ) );
-       hmac_update ( digest, digest_ctx, tlshdr, sizeof ( *tlshdr ) );
-       hmac_update ( digest, digest_ctx, data, len );
-       hmac_final ( digest, digest_ctx, cipherspec->mac_secret,
-                    &digest->digestsize, hmac );
+       tls_hmac_init ( cipherspec, ctx, seq, tlshdr );
+       tls_hmac_update ( cipherspec, ctx, data, len );
+       tls_hmac_final ( cipherspec, ctx, hmac );
 }
 
 /**
@@ -1877,8 +1967,7 @@ static int tls_send_plaintext ( struct tls_session *tls, unsigned int type,
        plaintext_tlshdr.length = htons ( len );
 
        /* Calculate MAC */
-       tls_hmac ( tls, cipherspec, tls->tx_seq, &plaintext_tlshdr,
-                  data, len, mac );
+       tls_hmac ( cipherspec, tls->tx_seq, &plaintext_tlshdr, data, len, mac );
 
        /* Allocate and assemble plaintext struct */
        if ( is_stream_cipher ( cipher ) ) {
@@ -1945,36 +2034,25 @@ static int tls_send_plaintext ( struct tls_session *tls, unsigned int type,
  * Split stream-ciphered record into data and MAC portions
  *
  * @v tls              TLS session
- * @v plaintext                Plaintext record
- * @v plaintext_len    Length of record
- * @ret data           Data
- * @ret len            Length of data
- * @ret digest         MAC digest
+ * @v rx_data          List of received data buffers
+ * @v mac              MAC to fill in
  * @ret rc             Return status code
  */
 static int tls_split_stream ( struct tls_session *tls,
-                             void *plaintext, size_t plaintext_len,
-                             void **data, size_t *len, void **digest ) {
-       void *content;
-       size_t content_len;
-       void *mac;
-       size_t mac_len;
-
-       /* Decompose stream-ciphered data */
-       mac_len = tls->rx_cipherspec.suite->digest->digestsize;
-       if ( plaintext_len < mac_len ) {
-               DBGC ( tls, "TLS %p received underlength record\n", tls );
-               DBGC_HD ( tls, plaintext, plaintext_len );
+                             struct list_head *rx_data, void **mac ) {
+       size_t mac_len = tls->rx_cipherspec.suite->digest->digestsize;
+       struct io_buffer *iobuf;
+
+       /* Extract MAC */
+       iobuf = list_last_entry ( rx_data, struct io_buffer, list );
+       assert ( iobuf != NULL );
+       if ( iob_len ( iobuf ) < mac_len ) {
+               DBGC ( tls, "TLS %p received underlength MAC\n", tls );
+               DBGC_HD ( tls, iobuf->data, iob_len ( iobuf ) );
                return -EINVAL_STREAM;
        }
-       content_len = ( plaintext_len - mac_len );
-       content = plaintext;
-       mac = ( content + content_len );
-
-       /* Fill in return values */
-       *data = content;
-       *len = content_len;
-       *digest = mac;
+       iob_unput ( iobuf, mac_len );
+       *mac = iobuf->tail;
 
        return 0;
 }
@@ -1983,65 +2061,56 @@ static int tls_split_stream ( struct tls_session *tls,
  * Split block-ciphered record into data and MAC portions
  *
  * @v tls              TLS session
- * @v plaintext                Plaintext record
- * @v plaintext_len    Length of record
- * @ret data           Data
- * @ret len            Length of data
- * @ret digest         MAC digest
+ * @v rx_data          List of received data buffers
+ * @v mac              MAC to fill in
  * @ret rc             Return status code
  */
 static int tls_split_block ( struct tls_session *tls,
-                            void *plaintext, size_t plaintext_len,
-                            void **data, size_t *len,
-                            void **digest ) {
-       void *iv;
+                            struct list_head *rx_data, void **mac ) {
+       size_t mac_len = tls->rx_cipherspec.suite->digest->digestsize;
+       struct io_buffer *iobuf;
        size_t iv_len;
-       void *content;
-       size_t content_len;
-       void *mac;
-       size_t mac_len;
-       void *padding;
+       uint8_t *padding_final;
+       uint8_t *padding;
        size_t padding_len;
-       unsigned int i;
-
-       /* Sanity check */
-       if ( plaintext_len < 1 ) {
-               DBGC ( tls, "TLS %p received underlength record\n", tls );
-               DBGC_HD ( tls, plaintext, plaintext_len );
-               return -EINVAL_BLOCK;
-       }
 
        /* TLSv1.1 and later use an explicit IV */
+       iobuf = list_first_entry ( rx_data, struct io_buffer, list );
        iv_len = ( ( tls->version >= TLS_VERSION_TLS_1_1 ) ?
                   tls->rx_cipherspec.suite->cipher->blocksize : 0 );
-
-       /* Decompose block-ciphered data */
-       mac_len = tls->rx_cipherspec.suite->digest->digestsize;
-       padding_len = *( ( uint8_t * ) ( plaintext + plaintext_len - 1 ) );
-       if ( plaintext_len < ( iv_len + mac_len + padding_len + 1 ) ) {
-               DBGC ( tls, "TLS %p received underlength record\n", tls );
-               DBGC_HD ( tls, plaintext, plaintext_len );
+       if ( iob_len ( iobuf ) < iv_len ) {
+               DBGC ( tls, "TLS %p received underlength IV\n", tls );
+               DBGC_HD ( tls, iobuf->data, iob_len ( iobuf ) );
                return -EINVAL_BLOCK;
        }
-       content_len = ( plaintext_len - iv_len - mac_len - padding_len - 1 );
-       iv = plaintext;
-       content = ( iv + iv_len );
-       mac = ( content + content_len );
-       padding = ( mac + mac_len );
-
-       /* Verify padding bytes */
-       for ( i = 0 ; i < padding_len ; i++ ) {
-               if ( *( ( uint8_t * ) ( padding + i ) ) != padding_len ) {
+       iob_pull ( iobuf, iv_len );
+
+       /* Extract and verify padding */
+       iobuf = list_last_entry ( rx_data, struct io_buffer, list );
+       padding_final = ( iobuf->tail - 1 );
+       padding_len = *padding_final;
+       if ( ( padding_len + 1 ) > iob_len ( iobuf ) ) {
+               DBGC ( tls, "TLS %p received underlength padding\n", tls );
+               DBGC_HD ( tls, iobuf->data, iob_len ( iobuf ) );
+               return -EINVAL_BLOCK;
+       }
+       iob_unput ( iobuf, ( padding_len + 1 ) );
+       for ( padding = iobuf->tail ; padding < padding_final ; padding++ ) {
+               if ( *padding != padding_len ) {
                        DBGC ( tls, "TLS %p received bad padding\n", tls );
-                       DBGC_HD ( tls, plaintext, plaintext_len );
+                       DBGC_HD ( tls, padding, padding_len );
                        return -EINVAL_PADDING;
                }
        }
 
-       /* Fill in return values */
-       *data = content;
-       *len = content_len;
-       *digest = mac;
+       /* Extract MAC */
+       if ( iob_len ( iobuf ) < mac_len ) {
+               DBGC ( tls, "TLS %p received underlength MAC\n", tls );
+               DBGC_HD ( tls, iobuf->data, iob_len ( iobuf ) );
+               return -EINVAL_BLOCK;
+       }
+       iob_unput ( iobuf, mac_len );
+       *mac = iobuf->tail;
 
        return 0;
 }
@@ -2051,71 +2120,65 @@ static int tls_split_block ( struct tls_session *tls,
  *
  * @v tls              TLS session
  * @v tlshdr           Record header
- * @v ciphertext       Ciphertext record
+ * @v rx_data          List of received data buffers
  * @ret rc             Return status code
  */
 static int tls_new_ciphertext ( struct tls_session *tls,
                                struct tls_header *tlshdr,
-                               const void *ciphertext ) {
+                               struct list_head *rx_data ) {
        struct tls_header plaintext_tlshdr;
        struct tls_cipherspec *cipherspec = &tls->rx_cipherspec;
        struct cipher_algorithm *cipher = cipherspec->suite->cipher;
-       size_t record_len = ntohs ( tlshdr->length );
-       void *plaintext = NULL;
-       void *data;
-       size_t len;
+       struct digest_algorithm *digest = cipherspec->suite->digest;
+       uint8_t ctx[digest->ctxsize];
+       uint8_t verify_mac[digest->digestsize];
+       struct io_buffer *iobuf;
        void *mac;
-       size_t mac_len = cipherspec->suite->digest->digestsize;
-       uint8_t verify_mac[mac_len];
+       size_t len = 0;
        int rc;
 
-       /* Allocate buffer for plaintext */
-       plaintext = malloc ( record_len );
-       if ( ! plaintext ) {
-               DBGC ( tls, "TLS %p could not allocate %zd bytes for "
-                      "decryption buffer\n", tls, record_len );
-               rc = -ENOMEM_RX_PLAINTEXT;
-               goto done;
+       /* Decrypt the received data */
+       list_for_each_entry ( iobuf, &tls->rx_data, list ) {
+               cipher_decrypt ( cipher, cipherspec->cipher_ctx,
+                                iobuf->data, iobuf->data, iob_len ( iobuf ) );
        }
 
-       /* Decrypt the record */
-       cipher_decrypt ( cipher, cipherspec->cipher_ctx,
-                        ciphertext, plaintext, record_len );
-
        /* Split record into content and MAC */
        if ( is_stream_cipher ( cipher ) ) {
-               if ( ( rc = tls_split_stream ( tls, plaintext, record_len,
-                                              &data, &len, &mac ) ) != 0 )
-                       goto done;
+               if ( ( rc = tls_split_stream ( tls, rx_data, &mac ) ) != 0 )
+                       return rc;
        } else {
-               if ( ( rc = tls_split_block ( tls, plaintext, record_len,
-                                             &data, &len, &mac ) ) != 0 )
-                       goto done;
+               if ( ( rc = tls_split_block ( tls, rx_data, &mac ) ) != 0 )
+                       return rc;
+       }
+
+       /* Calculate total length */
+       DBGC2 ( tls, "Received plaintext data:\n" );
+       list_for_each_entry ( iobuf, rx_data, list ) {
+               DBGC2_HD ( tls, iobuf->data, iob_len ( iobuf ) );
+               len += iob_len ( iobuf );
        }
 
        /* Verify MAC */
        plaintext_tlshdr.type = tlshdr->type;
        plaintext_tlshdr.version = tlshdr->version;
        plaintext_tlshdr.length = htons ( len );
-       tls_hmac ( tls, cipherspec, tls->rx_seq, &plaintext_tlshdr,
-                  data, len, verify_mac);
-       if ( memcmp ( mac, verify_mac, mac_len ) != 0 ) {
+       tls_hmac_init ( cipherspec, ctx, tls->rx_seq, &plaintext_tlshdr );
+       list_for_each_entry ( iobuf, rx_data, list ) {
+               tls_hmac_update ( cipherspec, ctx, iobuf->data,
+                                 iob_len ( iobuf ) );
+       }
+       tls_hmac_final ( cipherspec, ctx, verify_mac );
+       if ( memcmp ( mac, verify_mac, sizeof ( verify_mac ) ) != 0 ) {
                DBGC ( tls, "TLS %p failed MAC verification\n", tls );
-               DBGC_HD ( tls, plaintext, record_len );
-               goto done;
+               return -EINVAL_MAC;
        }
 
-       DBGC2 ( tls, "Received plaintext data:\n" );
-       DBGC2_HD ( tls, data, len );
-
        /* Process plaintext record */
-       if ( ( rc = tls_new_record ( tls, tlshdr->type, data, len ) ) != 0 )
-               goto done;
+       if ( ( rc = tls_new_record ( tls, tlshdr->type, rx_data ) ) != 0 )
+               return rc;
 
-       rc = 0;
- done:
-       free ( plaintext );
-       return rc;
+       return 0;
 }
 
 /******************************************************************************
@@ -2195,20 +2258,61 @@ static struct interface_descriptor tls_plainstream_desc =
  */
 static int tls_newdata_process_header ( struct tls_session *tls ) {
        size_t data_len = ntohs ( tls->rx_header.length );
+       size_t remaining = data_len;
+       size_t frag_len;
+       struct io_buffer *iobuf;
+       struct io_buffer *tmp;
+       int rc;
 
-       /* Allocate data buffer now that we know the length */
-       assert ( tls->rx_data == NULL );
-       tls->rx_data = malloc ( data_len );
-       if ( ! tls->rx_data ) {
-               DBGC ( tls, "TLS %p could not allocate %zd bytes "
-                      "for receive buffer\n", tls, data_len );
-               return -ENOMEM_RX_DATA;
+       /* Allocate data buffers now that we know the length */
+       assert ( list_empty ( &tls->rx_data ) );
+       while ( remaining ) {
+
+               /* Calculate fragment length.  Ensure that no block is
+                * smaller than TLS_RX_MIN_BUFSIZE (by increasing the
+                * allocation length if necessary).
+                */
+               frag_len = remaining;
+               if ( frag_len > TLS_RX_BUFSIZE )
+                       frag_len = TLS_RX_BUFSIZE;
+               remaining -= frag_len;
+               if ( remaining < TLS_RX_MIN_BUFSIZE ) {
+                       frag_len += remaining;
+                       remaining = 0;
+               }
+
+               /* Allocate buffer */
+               iobuf = alloc_iob_raw ( frag_len, TLS_RX_ALIGN, 0 );
+               if ( ! iobuf ) {
+                       DBGC ( tls, "TLS %p could not allocate %zd of %zd "
+                              "bytes for receive buffer\n", tls,
+                              remaining, data_len );
+                       rc = -ENOMEM_RX_DATA;
+                       goto err;
+               }
+
+               /* Ensure tailroom is exactly what we asked for.  This
+                * will result in unaligned I/O buffers when the
+                * fragment length is unaligned, which can happen only
+                * before we switch to using a block cipher.
+                */
+               iob_reserve ( iobuf, ( iob_tailroom ( iobuf ) - frag_len ) );
+
+               /* Add I/O buffer to list */
+               list_add_tail ( &iobuf->list, &tls->rx_data );
        }
 
        /* Move to data state */
        tls->rx_state = TLS_RX_DATA;
 
        return 0;
+
+ err:
+       list_for_each_entry_safe ( iobuf, tmp, &tls->rx_data, list ) {
+               list_del ( &iobuf->list );
+               free_iob ( iobuf );
+       }
+       return rc;
 }
 
 /**
@@ -2218,22 +2322,31 @@ static int tls_newdata_process_header ( struct tls_session *tls ) {
  * @ret rc             Returned status code
  */
 static int tls_newdata_process_data ( struct tls_session *tls ) {
+       struct io_buffer *iobuf;
        int rc;
 
+       /* Move current buffer to end of list */
+       iobuf = list_first_entry ( &tls->rx_data, struct io_buffer, list );
+       list_del ( &iobuf->list );
+       list_add_tail ( &iobuf->list, &tls->rx_data );
+
+       /* Continue receiving data if any space remains */
+       iobuf = list_first_entry ( &tls->rx_data, struct io_buffer, list );
+       if ( iob_tailroom ( iobuf ) )
+               return 0;
+
        /* Process record */
        if ( ( rc = tls_new_ciphertext ( tls, &tls->rx_header,
-                                        tls->rx_data ) ) != 0 )
+                                        &tls->rx_data ) ) != 0 )
                return rc;
 
        /* Increment RX sequence number */
        tls->rx_seq += 1;
 
-       /* Free data buffer */
-       free ( tls->rx_data );
-       tls->rx_data = NULL;
-
        /* Return to header state */
+       assert ( list_empty ( &tls->rx_data ) );
        tls->rx_state = TLS_RX_HEADER;
+       iob_unput ( &tls->rx_header_iobuf, sizeof ( tls->rx_header ) );
 
        return 0;
 }
@@ -2250,22 +2363,22 @@ static int tls_cipherstream_deliver ( struct tls_session *tls,
                                      struct io_buffer *iobuf,
                                      struct xfer_metadata *xfer __unused ) {
        size_t frag_len;
-       void *buf;
-       size_t buf_len;
        int ( * process ) ( struct tls_session *tls );
+       struct io_buffer *dest;
        int rc;
 
        while ( iob_len ( iobuf ) ) {
+
                /* Select buffer according to current state */
                switch ( tls->rx_state ) {
                case TLS_RX_HEADER:
-                       buf = &tls->rx_header;
-                       buf_len = sizeof ( tls->rx_header );
+                       dest = &tls->rx_header_iobuf;
                        process = tls_newdata_process_header;
                        break;
                case TLS_RX_DATA:
-                       buf = tls->rx_data;
-                       buf_len = ntohs ( tls->rx_header.length );
+                       dest = list_first_entry ( &tls->rx_data,
+                                                 struct io_buffer, list );
+                       assert ( dest != NULL );
                        process = tls_newdata_process_data;
                        break;
                default:
@@ -2275,20 +2388,18 @@ static int tls_cipherstream_deliver ( struct tls_session *tls,
                }
 
                /* Copy data portion to buffer */
-               frag_len = ( buf_len - tls->rx_rcvd );
-               if ( frag_len > iob_len  ( iobuf ) )
-                       frag_len = iob_len ( iobuf );
-               memcpy ( ( buf + tls->rx_rcvd ), iobuf->data, frag_len );
-               tls->rx_rcvd += frag_len;
+               frag_len = iob_len ( iobuf );
+               if ( frag_len > iob_tailroom ( dest ) )
+                       frag_len = iob_tailroom ( dest );
+               memcpy ( iob_put ( dest, frag_len ), iobuf->data, frag_len );
                iob_pull ( iobuf, frag_len );
 
                /* Process data if buffer is now full */
-               if ( tls->rx_rcvd == buf_len ) {
+               if ( iob_tailroom ( dest ) == 0 ) {
                        if ( ( rc = process ( tls ) ) != 0 ) {
                                tls_close ( tls, rc );
                                goto done;
                        }
-                       tls->rx_rcvd = 0;
                }
        }
        rc = 0;
@@ -2521,6 +2632,9 @@ int add_tls ( struct interface *xfer, const char *name,
        tls->handshake_digest = &sha256_algorithm;
        tls->handshake_ctx = tls->handshake_sha256_ctx;
        tls->tx_pending = TLS_TX_CLIENT_HELLO;
+       iob_populate ( &tls->rx_header_iobuf, &tls->rx_header, 0,
+                      sizeof ( tls->rx_header ) );
+       INIT_LIST_HEAD ( &tls->rx_data );
 
        /* Add pending operations for server and client Finished messages */
        pending_get ( &tls->client_negotiation );