diff --git a/CMakeLists.txt b/CMakeLists.txt index c4a183b2074..1eaedebd0e9 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -4317,6 +4317,20 @@ if(WOLFSSL_LIBZ) list(APPEND WOLFSSL_INCLUDE_DIRS ${ZLIB_INCLUDE_DIRS}) endif() +# TLS 1.3 Certificate Compression (RFC 8879) +add_option("WOLFSSL_CERT_COMPRESSION" + "Enable TLS 1.3 Certificate Compression, RFC 8879 (requires WOLFSSL_LIBZ) (default: disabled)" + "no" "yes;no") +if(WOLFSSL_CERT_COMPRESSION) + if(NOT WOLFSSL_LIBZ) + message(FATAL_ERROR "WOLFSSL_CERT_COMPRESSION requires zlib. Add -DWOLFSSL_LIBZ=yes.") + endif() + if(NOT WOLFSSL_TLS13) + message(FATAL_ERROR "WOLFSSL_CERT_COMPRESSION requires TLS 1.3. Do not disable WOLFSSL_TLS13.") + endif() + list(APPEND WOLFSSL_DEFINITIONS "-DHAVE_TLS_EXTENSIONS" "-DWOLFSSL_CERT_COMPRESSION") +endif() + #################################################### # Maximum key size options (parity with configure.ac) @@ -4876,6 +4890,7 @@ if(WOLFSSL_EXAMPLES) tests/api/test_evp_pkey.c tests/api/test_certman.c tests/api/test_tls13.c + tests/api/test_tls13_cert_compression.c tests/api/test_tls13_bounds.c tests/api/test_tls13_features.c tests/srp.c diff --git a/cmake/options.h.in b/cmake/options.h.in index c04a5f53d32..ac5ad7fb0f6 100644 --- a/cmake/options.h.in +++ b/cmake/options.h.in @@ -737,6 +737,8 @@ extern "C" { #cmakedefine WOLFSSL_CHECK_ALERT_ON_ERR #undef HAVE_LIBZ #cmakedefine HAVE_LIBZ +#undef WOLFSSL_CERT_COMPRESSION +#cmakedefine WOLFSSL_CERT_COMPRESSION #undef WOLFSSL_HARDEN_TLS #cmakedefine WOLFSSL_HARDEN_TLS @WOLFSSL_HARDEN_TLS@ diff --git a/configure.ac b/configure.ac index f48ec5d4aa1..d8ad32643f7 100644 --- a/configure.ac +++ b/configure.ac @@ -11190,6 +11190,26 @@ AC_ARG_WITH([libz], ] ) +# TLS 1.3 Certificate Compression (RFC 8879) +AC_ARG_ENABLE([cert-compression], + [AS_HELP_STRING([--enable-cert-compression],[Enable TLS 1.3 Certificate Compression, RFC 8879 (requires --with-libz) (default: disabled)])], + [ ENABLED_CERT_COMPRESSION=$enableval ], + [ ENABLED_CERT_COMPRESSION=no ] + ) + +if test "x$ENABLED_CERT_COMPRESSION" = "xyes" +then + if test "x$ENABLED_LIBZ" = "xno" + then + AC_MSG_ERROR([--enable-cert-compression requires zlib. Add --with-libz.]) + fi + if test "x$ENABLED_TLS13" = "xno" + then + AC_MSG_ERROR([--enable-cert-compression requires TLS 1.3. Do not disable TLS 1.3.]) + fi + AM_CFLAGS="$AM_CFLAGS -DHAVE_TLS_EXTENSIONS -DWOLFSSL_CERT_COMPRESSION" +fi + # PKCS#11 AC_ARG_ENABLE([pkcs11], @@ -14779,6 +14799,7 @@ echo " * Supported Elliptic Curves: $ENABLED_SUPPORTED_CURVES" echo " * FFDHE only in client: $ENABLED_FFDHE_ONLY" echo " * Session Ticket: $ENABLED_SESSION_TICKET" echo " * Session cache ref (deprec): $ENABLED_SESSION_CACHE_REF" +echo " * Certificate Compression: $ENABLED_CERT_COMPRESSION" echo " * Extended Master Secret: $ENABLED_EXTENDED_MASTER" echo " * Renegotiation Indication: $ENABLED_RENEGOTIATION_INDICATION" echo " * Secure Renegotiation: $ENABLED_SECURE_RENEGOTIATION" diff --git a/doc/dox_comments/header_files/compress.h b/doc/dox_comments/header_files/compress.h index 2d225ee4709..3a7dcc7aac2 100644 --- a/doc/dox_comments/header_files/compress.h +++ b/doc/dox_comments/header_files/compress.h @@ -198,3 +198,224 @@ int wc_DeCompress_ex(byte* out, word32 outSz, const byte* in, word32 inSz, int wc_DeCompressDynamic(byte** out, int max, int memoryType, const byte* in, word32 inSz, int windowBits, void* heap); + +/*! + \ingroup Compression + + \brief Checks whether a compression algorithm is compiled into this build + and usable with the wc_CompressionData functions. The algorithm ids are + the TLS CertificateCompressionAlgorithm code points (RFC 8879), e.g. + WC_ZLIB. + + \return 1 if the algorithm is supported + \return 0 if the algorithm is not supported, or is WC_NO_COMPRESSION + + \param alg compression algorithm id to check + + _Example_ + \code + if (wc_IsCompressionAlgSupported(WC_ZLIB)) { + // zlib can be used + } + \endcode + + \sa wc_CompressionData_InitComp + \sa wc_CompressionData_InitDeComp +*/ +byte wc_IsCompressionAlgSupported(word16 alg); + +/*! + \ingroup Compression + + \brief Initializes a wc_CompressionData object to decompress the given + compressed data. The object does not take ownership of data; it is only + read from and must stay valid until the object is decompressed or freed. + If reusing an object, call wc_CompressionData_Free on it first. + + \return 0 on success + \return BAD_FUNC_ARG if cd or data is NULL, uncompSz is 0, or alg is not + supported + + \param cd object to initialize + \param data buffer holding the compressed data + \param compSz size of the compressed data in bytes + \param uncompSz exact size of the data once decompressed + \param alg compression algorithm used to compress data + + _Example_ + \code + wc_CompressionData cd; + byte compressed[] = { // compressed data }; + word32 uncompSz = // exact decompressed size; + + if (wc_CompressionData_InitDeComp(&cd, compressed, sizeof(compressed), + uncompSz, WC_ZLIB) == 0 && + wc_CompressionData_DeCompress(&cd) == 0) { + // cd.data holds cd.uncompressedSz bytes of decompressed data + } + wc_CompressionData_Free(&cd); + \endcode + + \sa wc_CompressionData_DeCompress + \sa wc_CompressionData_DeCompToBuf + \sa wc_CompressionData_Free +*/ +int wc_CompressionData_InitDeComp(wc_CompressionData* cd, + const byte* data, word32 compSz, word32 uncompSz, + word16 alg); + +/*! + \ingroup Compression + + \brief Initializes a wc_CompressionData object to compress the given data. + The object does not take ownership of data; it is only read from and must + stay valid until the object is compressed or freed. If reusing an object, + call wc_CompressionData_Free on it first. + + \return 0 on success + \return BAD_FUNC_ARG if cd or data is NULL, uncompSz is 0, or alg is not + supported + + \param cd object to initialize + \param data buffer holding the data to compress + \param uncompSz size of data in bytes + \param alg compression algorithm to use + + _Example_ + \code + wc_CompressionData cd; + byte msg[] = { // data to compress }; + + if (wc_CompressionData_InitComp(&cd, msg, sizeof(msg), WC_ZLIB) == 0 && + wc_CompressionData_Compress(&cd) == 0) { + // cd.data holds cd.compressedSz bytes of compressed data + } + wc_CompressionData_Free(&cd); + \endcode + + \sa wc_CompressionData_Compress + \sa wc_CompressionData_CompToBuf + \sa wc_CompressionData_Free +*/ +int wc_CompressionData_InitComp(wc_CompressionData* cd, + const byte* data, word32 uncompSz, word16 alg); + +/*! + \ingroup Compression + + \brief Sets the heap hint used for buffers that + wc_CompressionData_Compress and wc_CompressionData_DeCompress allocate. + Call after the Init function, since Init clears the object. + + \return 0 on success + \return BAD_FUNC_ARG if cd is NULL + + \param cd initialized object + \param heap heap hint (can be NULL) + + \sa wc_CompressionData_Compress + \sa wc_CompressionData_DeCompress +*/ +int wc_CompressionData_SetHeap(wc_CompressionData* cd, void* heap); + +/*! + \ingroup Compression + + \brief Releases the buffer owned by the object (the output of a previous + Compress or DeCompress call), zeroizing it first, and clears the object. + A buffer passed to an Init function is not freed. Safe to call with NULL. + + \return none No returns. + + \param cd object to free + + \sa wc_CompressionData_InitComp + \sa wc_CompressionData_InitDeComp +*/ +void wc_CompressionData_Free(wc_CompressionData* cd); + +/*! + \ingroup Compression + + \brief Compresses the object's data into a newly allocated buffer, which + the object then owns. On success cd->data points at the compressed data + and cd->compressedSz holds its size. Compression fails when the output + does not fit in the uncompressed size. + + \return 0 on success + \return BAD_FUNC_ARG if data is NULL, not initialized for compression, or + the algorithm is not supported + \return MEMORY_E if allocation fails + \return COMPRESS_E or another negative value if compression fails + + \param data object initialized with wc_CompressionData_InitComp + + \sa wc_CompressionData_InitComp + \sa wc_CompressionData_CompToBuf +*/ +int wc_CompressionData_Compress(wc_CompressionData* data); + +/*! + \ingroup Compression + + \brief Compresses the object's data into a caller-supplied buffer. The + object is not modified. + + \return the number of compressed bytes written to out on success + \return BAD_FUNC_ARG if data or out is NULL or the algorithm is not + supported + \return COMPRESS_E or another negative value if compression fails, + including when out is too small + + \param data object initialized with wc_CompressionData_InitComp + \param out buffer to write the compressed data to + \param outSz size of out in bytes + + \sa wc_CompressionData_Compress +*/ +int wc_CompressionData_CompToBuf(const wc_CompressionData* data, + byte* out, word32 outSz); + +/*! + \ingroup Compression + + \brief Decompresses the object's data into a newly allocated buffer of + cd->uncompressedSz bytes, which the object then owns. On success cd->data + points at the decompressed data. Decompression fails unless the output is + exactly the uncompressed size given to wc_CompressionData_InitDeComp. + + \return 0 on success + \return BAD_FUNC_ARG if data is NULL, not initialized for decompression, + or the algorithm is not supported + \return MEMORY_E if allocation fails + \return BUFFER_E if the decompressed size is too small + \return other negative values if decompression fails + + \param data object initialized with wc_CompressionData_InitDeComp + + \sa wc_CompressionData_InitDeComp + \sa wc_CompressionData_DeCompToBuf +*/ +int wc_CompressionData_DeCompress(wc_CompressionData* data); + +/*! + \ingroup Compression + + \brief Decompresses the object's data into a caller-supplied buffer. The + object is not modified. + + \return the number of decompressed bytes written to out on success + \return BAD_FUNC_ARG if data or out is NULL or the algorithm is not + supported + \return BUFFER_E if outSz is smaller than the uncompressed size, or the + decompressed size does not match it + \return other negative values if decompression fails + + \param data object initialized with wc_CompressionData_InitDeComp + \param out buffer to write the decompressed data to + \param outSz size of out in bytes + + \sa wc_CompressionData_DeCompress +*/ +int wc_CompressionData_DeCompToBuf(const wc_CompressionData* data, + byte* out, word32 outSz); diff --git a/doc/dox_comments/header_files/ssl.h b/doc/dox_comments/header_files/ssl.h index b50c86850a4..c4a883f9374 100644 --- a/doc/dox_comments/header_files/ssl.h +++ b/doc/dox_comments/header_files/ssl.h @@ -14871,6 +14871,95 @@ int wolfSSL_CTX_no_ticket_TLSv13(WOLFSSL_CTX* ctx); */ int wolfSSL_no_ticket_TLSv13(WOLFSSL* ssl); +/*! + \ingroup Setup + + \brief This function sets the certificate compression algorithms + (RFC 8879) that WOLFSSL objects created from this context will offer, in + order of preference. Defaults to negotiate all supported compression + algorithms by the build (see wc_IsCompressionAlgSupported()). + Passing an empty alg list turns off certificate compression. + + Available when wolfSSL is built with --enable-cert-compression and + --with-libz. Certificate compression is available with (D)TLS 1.3. + + \param [in,out] ctx a pointer to a WOLFSSL_CTX Object. + \param [in] algs array of RFC 8879 algorithm IDs, most preferred first. + Every entry must be supported by this build (see + wc_IsCompressionAlgSupported()). May be NULL only when count is 0. + \param [in] count number of entries in algs, from 0 to 127. A count of 0 + turns off certificate compression. + + \return WOLFSSL_SUCCESS if successful. + \return BAD_FUNC_ARG if ctx is NULL, algs is NULL while count is not 0, + count is negative or greater than 127, or an entry in algs is not a + supported algorithm. + \return MEMORY_E if the copy of the list could not be allocated. + + _Example_ + \code + int ret; + WOLFSSL_CTX* ctx; + const word16 algs[] = { WC_ZLIB }; + ... + ret = wolfSSL_CTX_set_cert_compression_algs(ctx, algs, + (int)(sizeof(algs) / sizeof(algs[0]))); + if (ret != WOLFSSL_SUCCESS) { + // failed to set compression algorithms + } + \endcode + + \sa wolfSSL_set_cert_compression_algs + \sa wc_IsCompressionAlgSupported +*/ +int wolfSSL_CTX_set_cert_compression_algs(WOLFSSL_CTX* ctx, + const word16* algs, int count); + +/*! + \ingroup Setup + + \brief This function sets the certificate compression algorithms + (RFC 8879) that the wolfSSL object will negotiate. Defaults to negotiate + all supported compression algorithms by the build (see + wc_IsCompressionAlgSupported()). Passing an empty alg list turns off + certificate compression. + + Available when wolfSSL is built with --enable-cert-compression and + --with-libz. Certificate compression is available with (D)TLS 1.3. + + \param [in,out] ssl a pointer to a WOLFSSL structure, created using + wolfSSL_new(). + \param [in] algs array of RFC 8879 algorithm IDs, most preferred first. + Every entry must be supported by this build (see + wc_IsCompressionAlgSupported()). May be NULL only when count is 0. + \param [in] count number of entries in algs, from 0 to 127. A count of 0 + turns off certificate compression. + + \return WOLFSSL_SUCCESS if successful. + \return BAD_FUNC_ARG if ssl is NULL, algs is NULL while count is not 0, + count is negative or greater than 127, or an entry in algs is not a + supported algorithm. + \return MEMORY_E if the copy of the list could not be allocated. + + _Example_ + \code + int ret; + WOLFSSL* ssl; + const word16 algs[] = { WC_ZLIB }; + ... + ret = wolfSSL_set_cert_compression_algs(ssl, algs, + (int)(sizeof(algs) / sizeof(algs[0]))); + if (ret != WOLFSSL_SUCCESS) { + // failed to set compression algorithms + } + \endcode + + \sa wolfSSL_CTX_set_cert_compression_algs + \sa wc_IsCompressionAlgSupported +*/ +int wolfSSL_set_cert_compression_algs(WOLFSSL* ssl, + const word16* algs, int count); + /*! \ingroup Setup diff --git a/src/dtls13.c b/src/dtls13.c index 81d0430b224..4169b46e834 100644 --- a/src/dtls13.c +++ b/src/dtls13.c @@ -230,6 +230,7 @@ static byte Dtls13TypeIsEncrypted(enum HandShakeType hs_type) case finished: case certificate_status: case key_update: + case compressed_certificate: case request_connection_id: case new_connection_id: case change_cipher_hs: @@ -1806,6 +1807,7 @@ int Dtls13CheckEpoch(WOLFSSL* ssl, enum HandShakeType type) } break; case certificate_request: + case compressed_certificate: case certificate: case certificate_verify: case finished: @@ -2151,8 +2153,11 @@ int Dtls13HandshakeSend(WOLFSSL* ssl, byte* message, word16 outputSize, arrived out-of-order (before the server finished) so likely an ACK was already sent. In the worst case we will ACK the server retranmission*/ - if (handshakeType == certificate || handshakeType == finished || - handshakeType == server_hello || handshakeType == client_hello) + if (handshakeType == certificate || + handshakeType == compressed_certificate || + handshakeType == finished || + handshakeType == server_hello || + handshakeType == client_hello) Dtls13RtxFlushAcks(ssl); } diff --git a/src/internal.c b/src/internal.c index e3052a49425..f9117d9f726 100644 --- a/src/internal.c +++ b/src/internal.c @@ -3355,6 +3355,11 @@ void SSL_CtxResourceFree(WOLFSSL_CTX* ctx) XFREE(ctx->suites, ctx->heap, DYNAMIC_TYPE_SUITES); ctx->suites = NULL; +#ifdef WOLFSSL_CERT_COMPRESSION + XFREE(ctx->compressionAlgPrefList, ctx->heap, DYNAMIC_TYPE_TLSX); + ctx->compressionAlgPrefList = NULL; +#endif + #ifndef NO_DH XFREE(ctx->serverDH_G.buffer, ctx->heap, DYNAMIC_TYPE_PUBLIC_KEY); ctx->serverDH_G.buffer = NULL; @@ -9285,6 +9290,18 @@ int InitSSL(WOLFSSL* ssl, WOLFSSL_CTX* ctx, int writeDup) ssl->options.noTicketTls12 = ctx->noTicketTls12; #endif +#ifdef WOLFSSL_CERT_COMPRESSION + /* inherit the context's compression alg list; each object owns a copy */ + if (ctx->compressionAlgPrefList != NULL) { + ret = wolfSSL_set_cert_compression_algs(ssl, + ctx->compressionAlgPrefList, ctx->compressionAlgPrefListLen); + if (ret != WOLFSSL_SUCCESS) + return ret; + } + /* after the list copy, which clears the flag */ + ssl->noOfferCompressionAlgPrefList = ctx->noOfferCompressionAlgPrefList; +#endif + #ifdef WOLFSSL_MULTICAST InitSSL_Multicast(ssl, ctx); #endif @@ -10315,6 +10332,14 @@ void wolfSSL_ResourceFree(WOLFSSL* ssl) #ifdef HAVE_TLS_EXTENSIONS FreeSSL_Extensions(ssl); #endif /* HAVE_TLS_EXTENSIONS */ +#ifdef WOLFSSL_CERT_COMPRESSION + wc_CompressionData_Free(ssl->compressedCert); + if (ssl->compressedCert != NULL) + XFREE(ssl->compressedCert, ssl->heap, DYNAMIC_TYPE_TMP_BUFFER); + ssl->compressedCert = NULL; + XFREE(ssl->compressionAlgPrefList, ssl->heap, DYNAMIC_TYPE_TLSX); + ssl->compressionAlgPrefList = NULL; +#endif #if defined(WOLFSSL_APACHE_MYNEWT) && !defined(WOLFSSL_LWIP) if (ssl->mnCtx) { mynewt_ctx_clear(ssl->mnCtx); @@ -10667,6 +10692,13 @@ void FreeHandshakeResources(WOLFSSL* ssl) * !WOLFSSL_POST_HANDSHAKE_AUTH */ #endif /* HAVE_TLS_EXTENSIONS && !NO_TLS */ +#ifdef WOLFSSL_CERT_COMPRESSION + wc_CompressionData_Free(ssl->compressedCert); + if (ssl->compressedCert != NULL) + XFREE(ssl->compressedCert, ssl->heap, DYNAMIC_TYPE_TMP_BUFFER); + ssl->compressedCert = NULL; +#endif + #if defined(HAVE_OCSP) { size_t i; @@ -13201,6 +13233,7 @@ int MsgCheckEncryption(WOLFSSL* ssl, byte type, byte encrypted) case finished: case certificate_status: case key_update: + case compressed_certificate: case request_connection_id: case new_connection_id: if (!encrypted) { @@ -13265,6 +13298,7 @@ int MsgCheckEncryption(WOLFSSL* ssl, byte type, byte encrypted) case end_of_early_data: case request_connection_id: case new_connection_id: + case compressed_certificate: case message_hash: case no_shake: default: @@ -13312,6 +13346,7 @@ static int MsgCheckBoundary(const WOLFSSL* ssl, byte type, case client_key_exchange: case certificate_status: case key_update: + case compressed_certificate: case change_cipher_hs: case request_connection_id: case new_connection_id: @@ -13351,6 +13386,7 @@ static int MsgCheckBoundary(const WOLFSSL* ssl, byte type, break; case hello_retry_request: case encrypted_extensions: + case compressed_certificate: case key_update: case request_connection_id: case new_connection_id: @@ -13380,6 +13416,7 @@ static int MsgCheckBoundary(const WOLFSSL* ssl, byte type, case hello_retry_request: case encrypted_extensions: case certificate: + case compressed_certificate: case server_key_exchange: case certificate_request: case server_hello_done: diff --git a/src/ssl.c b/src/ssl.c index 757b7f00cf4..017e9347263 100644 --- a/src/ssl.c +++ b/src/ssl.c @@ -5819,6 +5819,9 @@ size_t wolfSSL_get_client_random(const WOLFSSL* ssl, unsigned char* out, ssl->options.certYieldPending = 0; #endif ssl->recordSzOverhead = 0; + /* Drop any half-built outgoing message state. */ + ssl->fragOffset = 0; + ssl->options.buildMsgState = BUILD_MSG_BEGIN; #ifdef WOLFSSL_TLS13_STREAM_CERT_VERIFY /* Drop any half-sent streamed CertificateVerify. Left in place, the * resume guard in SendTls13CertificateVerify would fire on the next @@ -5827,7 +5830,12 @@ size_t wolfSSL_get_client_random(const WOLFSSL* ssl, unsigned char* out, DYNAMIC_TYPE_TMP_BUFFER); ssl->buffers.certVerifyMsg.buffer = NULL; ssl->buffers.certVerifyMsg.length = 0; - ssl->fragOffset = 0; +#endif +#ifdef WOLFSSL_CERT_COMPRESSION + wc_CompressionData_Free(ssl->compressedCert); + XFREE(ssl->compressedCert, ssl->heap, DYNAMIC_TYPE_TMP_BUFFER); + ssl->compressedCert = NULL; + ssl->peerCertCompressionAlg = WC_NO_COMPRESSION; #endif ssl->options.processReply = 0; /* doProcessInit */ ssl->options.havePeerVerify = 0; diff --git a/src/ssl_api_hs.c b/src/ssl_api_hs.c index a5f0653ac2a..ea5ae20a1a8 100644 --- a/src/ssl_api_hs.c +++ b/src/ssl_api_hs.c @@ -1932,6 +1932,7 @@ static int wolfssl_state_string_recv_state(const WOLFSSL* ssl) case encrypted_extensions: state = WOLFSSL_SS_SERVER_ENCRYPTEDEXTENSIONS; break; + case compressed_certificate: case certificate: if (ssl->options.side == WOLFSSL_SERVER_END) { state = WOLFSSL_SS_CLIENT_CERT; diff --git a/src/tls.c b/src/tls.c index 24e1c622f3c..5c6687af28f 100644 --- a/src/tls.c +++ b/src/tls.c @@ -148,6 +148,10 @@ #include #endif +#ifdef WOLFSSL_CERT_COMPRESSION + #include +#endif + #include #ifndef NO_TLS @@ -8338,6 +8342,220 @@ static int TLSX_SetSignatureAlgorithmsCert(TLSX** extensions, #define SAC_PARSE TLSX_SignatureAlgorithmsCert_Parse #endif /* WOLFSSL_TLS13 */ +/******************************************************************************/ +/* Certificate Compression */ +/******************************************************************************/ + +#if defined(WOLFSSL_TLS13) && !defined(NO_CERTS) && \ + defined(WOLFSSL_CERT_COMPRESSION) + +/* List of algs offered in the certificate_compression extension, most + * preferred first. */ +static const word16 TLSX_CertCompression_DefaultAlgs[] = { +#ifdef HAVE_LIBZ + WC_ZLIB, +#endif +}; + +static void TLSX_CertCompression_FreeAll(byte* data, void* heap) +{ + (void)heap; + + if (data != NULL) + XFREE(data, heap, DYNAMIC_TYPE_TLSX); +} + +int TLSX_UseCertCompression(WOLFSSL* ssl, void* heap) +{ + int ret = 0; + TLSX* extension; + + if (ssl == NULL) { + return BAD_FUNC_ARG; + } + + if (ssl->noOfferCompressionAlgPrefList == 1) + return 0; + + extension = TLSX_Find(ssl->extensions, TLSX_CERT_COMPRESSION); + if (extension == NULL) { + /* certificate_compression(27) extension format is: + * |num following bytes + * v + * +-------------------------+ + * |<1 byte>|<2 byte>[0..127]| + * +-------------------------+ + * ^ + * | list of alg Ids + */ + byte* data = NULL; + word32 i; + byte len = 1; /* 1 for the size of the len byte */ + byte* dataPtr; + const word16* list = NULL; + + /* use the user's list if set, else the built-in default */ + if (ssl->compressionAlgPrefList == NULL) { + len += (byte)(XELEM_CNT(TLSX_CertCompression_DefaultAlgs) * + OPAQUE16_LEN); + list = TLSX_CertCompression_DefaultAlgs; + } + else { + len += (byte)(ssl->compressionAlgPrefListLen * OPAQUE16_LEN); + list = ssl->compressionAlgPrefList; + } + data = (byte*)XMALLOC(len, heap, DYNAMIC_TYPE_TLSX); + if (data == NULL) + return MEMORY_ERROR; + /* length of the alg list so sub self */ + *data = (byte)(len - sizeof(*data)); + /* skip past len byte */ + dataPtr = data + 1; + for (i = 0; i < (word32)((len - 1) / 2); i++) { + /* write each alg Id in to wire order list of Opaque 16s */ + c16toa(list[i], dataPtr); + dataPtr += 2; + } + ret = TLSX_Push(&ssl->extensions, TLSX_CERT_COMPRESSION, data, heap); + if (ret != 0) + XFREE(data, heap, DYNAMIC_TYPE_TLSX); + } + return ret; +} + +/* Get the size of the Certificate Compression extension's data. + * + * algs Our supported algorithm list, laid out as it goes on the wire: + * [8-bit length in bytes]<1 or more 16-bit algorithm IDs>. + * msgType Type of message to put the extension into. + * pSz Size of the extension data - accumulated into. + * returns SANITY_MSG_E when the message is not allowed to have the extension, + * BAD_FUNC_ARG when there is no algorithm list and 0 otherwise. + */ +static int TLSX_CertCompression_GetSize(byte* algs, byte msgType, word16* pSz) +{ + /* RFC 8879 Section 3: ClientHello and CertificateRequest only. */ + if (msgType != client_hello && msgType != certificate_request) { + WOLFSSL_ERROR_VERBOSE(SANITY_MSG_E); + return SANITY_MSG_E; + } + if (algs == NULL) { + WOLFSSL_ERROR_VERBOSE(BAD_FUNC_ARG); + return BAD_FUNC_ARG; + } + + *pSz += (word16)(OPAQUE8_LEN + algs[0]); + + return 0; +} + +/* Write the Certificate Compression extension into the buffer. + * + * data Our supported algorithm list, laid out as it goes on the wire: + * [8-bit length in bytes]<1 or more 16-bit algorithm IDs>. + * output The buffer to write the extension into. + * msgType Type of message to put the extension into. + * pSz Size of the data written - accumulated into. + * returns SANITY_MSG_E when the message is not allowed to have the extension, + * BAD_FUNC_ARG when there is no algorithm list and 0 otherwise. + */ +static int TLSX_CertCompression_Write(byte* data, byte* output, byte msgType, + word16* pSz) +{ + byte len; + + /* RFC 8879 Section 3: ClientHello and CertificateRequest only. */ + if (msgType != client_hello && msgType != certificate_request) { + WOLFSSL_ERROR_VERBOSE(SANITY_MSG_E); + return SANITY_MSG_E; + } + if (data == NULL) { + WOLFSSL_ERROR_VERBOSE(BAD_FUNC_ARG); + return BAD_FUNC_ARG; + } + + /* each alg is 2 byte so if the len is odd we are not good */ + if ((*data & 1) != 0) { + WOLFSSL_ERROR_VERBOSE(SANITY_MSG_E); + return SANITY_MSG_E; + } + + /* length cannot be 0 if this ext is present algorithms<2...2^8-2> */ + if (*data == 0) { + WOLFSSL_ERROR_VERBOSE(SANITY_MSG_E); + return SANITY_MSG_E; + } + + len = (byte)(*data + OPAQUE8_LEN); + + XMEMCPY(output, data, len); + + *pSz += (word16)(len); + + return 0; +} + +/* Parse the Certificate Compression extension. + * + * ssl The SSL/TLS object. + * input The buffer with the extension data. + * length The length of the extension data must be less than 1 + 254 and odd. + * returns 0 on success, otherwise failure. + */ +static int TLSX_CertCompression_Parse(WOLFSSL* ssl, const byte* input, + word16 length) +{ + byte len; + word16 i; + + /* set default */ + ssl->peerCertCompressionAlg = WC_NO_COMPRESSION; + if (ssl->noOfferCompressionAlgPrefList) + /* skip we have this feat turned off */ + return 0; + + /* algorithms<2..2^8-2>: length byte plus at least one 2-byte alg id. */ + if (length < OPAQUE8_LEN + OPAQUE16_LEN) { + WOLFSSL_ERROR_VERBOSE(BUFFER_ERROR); + return BUFFER_ERROR; + } + + len = input[0]; + if (len < OPAQUE16_LEN || len > 254 || (len & 1) != 0 || + length != (word16)(OPAQUE8_LEN + len)) { + WOLFSSL_ERROR_VERBOSE(BUFFER_ERROR); + return BUFFER_ERROR; + } + + /* Peer's list is in its preference order. An all-unsupported list is not + * an error - we just send an uncompressed Certificate. */ + for (i = OPAQUE8_LEN; i + OPAQUE16_LEN <= OPAQUE8_LEN + len && + ssl->peerCertCompressionAlg == WC_NO_COMPRESSION; + i += OPAQUE16_LEN) { + word16 alg; + ato16(input + i, &alg); + if (ssl->compressionAlgPrefListLen > 0) { + word32 subIndex = 0; + for (; subIndex < ssl->compressionAlgPrefListLen; subIndex++) { + if (alg == ssl->compressionAlgPrefList[subIndex]) { + ssl->peerCertCompressionAlg = alg; + break; + } + } + } + /* default if user has not set a list */ + else if (wc_IsCompressionAlgSupported(alg)) { + ssl->peerCertCompressionAlg = alg; + } + } + return 0; +} + +#define CC_GET_SIZE TLSX_CertCompression_GetSize +#define CC_WRITE TLSX_CertCompression_Write +#define CC_PARSE TLSX_CertCompression_Parse +#endif /* WOLFSSL_TLS13 && !NO_CERTS && WOLFSSL_CERT_COMPRESSION */ + /******************************************************************************/ /* Key Share */ @@ -15640,6 +15858,13 @@ void TLSX_FreeAll(TLSX* list, void* heap) break; #endif + #ifdef WOLFSSL_CERT_COMPRESSION + case TLSX_CERT_COMPRESSION: + WOLFSSL_MSG("Cert Compression extension free"); + TLSX_CertCompression_FreeAll((byte*)extension->data, heap); + break; + #endif + #ifdef WOLFSSL_POST_HANDSHAKE_AUTH case TLSX_POST_HANDSHAKE_AUTH: WOLFSSL_MSG("Post-Handshake Authentication extension free"); @@ -15899,6 +16124,14 @@ static int TLSX_GetSize(TLSX* list, byte* semaphore, byte msgType, length += cbShim; break; #endif + #ifdef WOLFSSL_CERT_COMPRESSION + case TLSX_CERT_COMPRESSION: + cbShim = 0; + ret = CC_GET_SIZE((byte*)extension->data, msgType, &cbShim); + length += cbShim; + break; + #endif + #ifdef WOLFSSL_POST_HANDSHAKE_AUTH case TLSX_POST_HANDSHAKE_AUTH: @@ -16197,6 +16430,16 @@ static int TLSX_Write(TLSX* list, byte* output, byte* semaphore, break; #endif + #ifdef WOLFSSL_CERT_COMPRESSION + case TLSX_CERT_COMPRESSION: + WOLFSSL_MSG("Certificate Compression extension to write"); + cbShim = 0; + ret = CC_WRITE((byte*)extension->data, output + offset, + msgType, &cbShim); + offset += cbShim; + break; + #endif + #ifdef WOLFSSL_POST_HANDSHAKE_AUTH case TLSX_POST_HANDSHAKE_AUTH: WOLFSSL_MSG("Post-Handshake Authentication extension to write"); @@ -16761,6 +17004,14 @@ int TLSX_PopulateExtensions(WOLFSSL* ssl, byte isServer) return ret; } #endif +#if !defined(NO_CERTS) && defined(WOLFSSL_CERT_COMPRESSION) + /* This extension is always added to the client if it has not been + * turned off with the set func. It is added to the server when a + * certificate request is sent. */ + ret = TLSX_UseCertCompression(ssl, ssl->heap); + if (ret != 0) + return ret; +#endif #if defined(HAVE_SUPPORTED_CURVES) if (!ssl->options.userCurves && !ssl->ctx->userCurves) { @@ -17490,6 +17741,7 @@ static int TLSX_CustomExt_IsKnown(word16 ext_type) case TLSXT_SERVER_CERTIFICATE: case TLSXT_ENCRYPT_THEN_MAC: case TLSXT_EXTENDED_MASTER_SECRET: + case TLSXT_CERT_COMPRESSION: case TLSXT_CERT_WITH_EXTERN_PSK: case TLSXT_SESSION_TICKET: case TLSXT_PRE_SHARED_KEY: @@ -17848,6 +18100,9 @@ int TLSX_GetRequestSize(WOLFSSL* ssl, byte msgType, word32* pLength) #ifdef WOLFSSL_EARLY_DATA TURN_ON(semaphore, TLSX_ToSemaphore(TLSX_EARLY_DATA)); #endif + #ifdef WOLFSSL_CERT_COMPRESSION + TURN_ON(semaphore, TLSX_ToSemaphore(TLSX_CERT_COMPRESSION)); + #endif #ifdef WOLFSSL_TLS13_COOKIE TURN_ON(semaphore, TLSX_ToSemaphore(TLSX_COOKIE)); #endif @@ -17891,6 +18146,12 @@ int TLSX_GetRequestSize(WOLFSSL* ssl, byte msgType, word32* pLength) /* TLSX_STATUS_REQUEST is enabled: the server may request the client * to staple an OCSP response with its CertificateRequest. */ TURN_OFF(semaphore, TLSX_ToSemaphore(TLSX_STATUS_REQUEST)); + #ifdef WOLFSSL_CERT_COMPRESSION + /* RFC 8879 Section 3: compress_certificate may be sent in + * CertificateRequest to tell the client how it may compress its own + * Certificate message. */ + TURN_OFF(semaphore, TLSX_ToSemaphore(TLSX_CERT_COMPRESSION)); + #endif } #endif #if defined(HAVE_ECH) @@ -18083,6 +18344,9 @@ int TLSX_WriteRequest(WOLFSSL* ssl, byte* output, byte msgType, word32* pOffset) #ifdef WOLFSSL_EARLY_DATA TURN_ON(semaphore, TLSX_ToSemaphore(TLSX_EARLY_DATA)); #endif + #ifdef WOLFSSL_CERT_COMPRESSION + TURN_ON(semaphore, TLSX_ToSemaphore(TLSX_CERT_COMPRESSION)); + #endif #ifdef WOLFSSL_TLS13_COOKIE TURN_ON(semaphore, TLSX_ToSemaphore(TLSX_COOKIE)); #endif @@ -18136,6 +18400,12 @@ int TLSX_WriteRequest(WOLFSSL* ssl, byte* output, byte msgType, word32* pOffset) /* TLSX_STATUS_REQUEST is enabled: the server may request the client * to staple an OCSP response with its CertificateRequest. */ TURN_OFF(semaphore, TLSX_ToSemaphore(TLSX_STATUS_REQUEST)); + #ifdef WOLFSSL_CERT_COMPRESSION + /* RFC 8879 Section 3: compress_certificate may be sent in + * CertificateRequest to tell the client how it may compress its own + * Certificate message. */ + TURN_OFF(semaphore, TLSX_ToSemaphore(TLSX_CERT_COMPRESSION)); + #endif } #endif #endif @@ -19340,6 +19610,25 @@ WOLFSSL_TEST_VIS int TLSX_Parse(WOLFSSL* ssl, const byte* input, word16 length, break; #endif + #ifdef WOLFSSL_CERT_COMPRESSION + case TLSX_CERT_COMPRESSION: + WOLFSSL_MSG("Certificate Compression extension received"); + #ifdef WOLFSSL_DEBUG_TLS + WOLFSSL_BUFFER(input + offset, size); + #endif + + if (!IsAtLeastTLSv1_3(ssl->version)) + break; + + if (msgType != client_hello && + msgType != certificate_request) { + WOLFSSL_ERROR_VERBOSE(EXT_NOT_ALLOWED); + return EXT_NOT_ALLOWED; + } + ret = CC_PARSE(ssl, input + offset, size); + break; + #endif + #ifdef WOLFSSL_POST_HANDSHAKE_AUTH case TLSX_POST_HANDSHAKE_AUTH: WOLFSSL_MSG("Post Handshake Authentication extension received"); diff --git a/src/tls13.c b/src/tls13.c index ae47eb39e60..41839028315 100644 --- a/src/tls13.c +++ b/src/tls13.c @@ -137,6 +137,7 @@ #include #include #include +#include #ifdef NO_INLINE #include #else @@ -6663,6 +6664,11 @@ static int DoTls13CertificateRequest(WOLFSSL* ssl, const byte* input, *inOutIdx += OPAQUE16_LEN; if ((*inOutIdx - begin) + len > size) return BUFFER_ERROR; +#ifdef WOLFSSL_CERT_COMPRESSION + /* reset this for a new request we don't want to compress if we don't + * get the certificate_compression extension this request */ + ssl->peerCertCompressionAlg = WC_NO_COMPRESSION; +#endif /* RFC 9846 Section 4.4.2: CertificateRequest.extensions has a lower bound of * 0, so an empty extensions block is parsed rather than rejected here. A * request missing the mandatory signature_algorithms extension is caught by @@ -8424,6 +8430,11 @@ int DoTls13ClientHello(WOLFSSL* ssl, const byte* input, word32* inOutIdx, } #endif +#ifdef WOLFSSL_CERT_COMPRESSION + /* reset this for a new request we don't want to compress if we don't + * get the certificate_compression extension this request */ + ssl->peerCertCompressionAlg = WC_NO_COMPRESSION; +#endif /* Parse extensions */ if ((ret = TLSX_Parse(ssl, input + args->idx, totalExtSz, client_hello, ssl->clSuites))) { @@ -9367,6 +9378,13 @@ static int SendTls13CertificateRequest(WOLFSSL* ssl, byte* reqCtx, return ret; } +#ifdef WOLFSSL_CERT_COMPRESSION + ret = TLSX_UseCertCompression(ssl, ssl->heap); + if (ret != 0) { + return ret; + } +#endif + i = RECORD_HEADER_SZ + HANDSHAKE_HEADER_SZ; #ifdef WOLFSSL_DTLS13 if (ssl->options.dtls) @@ -10391,7 +10409,7 @@ static word32 AddCertExt(WOLFSSL* ssl, byte* cert, word32 len, word16 extSz, copySz = len + extSz - idx - i; if (extSz == OPAQUE16_LEN) { - if (copySz <= fragSz) { + if (copySz <= fragSz - i) { /* Empty extension */ output[i++] = 0; output[i++] = 0; @@ -10408,6 +10426,120 @@ static word32 AddCertExt(WOLFSSL* ssl, byte* cert, word32 len, word16 extSz, return i; } +/* Layout of a TLS v1.3 Certificate message */ +typedef struct Tls13CertMsg { + byte* certReqCtx; + word32 certSz; + word32 certChainSz; + word32 headerSz; + word32 listSz; + word32 totalextSz; + word32 payloadSz; + word16 extSz[MAX_CERT_EXTENSIONS]; + byte certReqCtxLen; +} Tls13CertMsg; + +/* Write the start of a TLS v1.3 Certificate message body: the request + * context, the certificate list length and, when sending a certificate, the + * leaf certificate's length. + * + * output The buffer to write to. + * msg The layout of the Certificate message. + * returns the number of bytes written. + */ +static word32 WriteTls13CertHeader(byte* output, const Tls13CertMsg* msg) +{ + const byte* certReqCtx = msg->certReqCtx; + byte certReqCtxLen = msg->certReqCtxLen; + word32 listSz = msg->listSz; + word32 certSz = msg->certSz; + + word32 i = 0; + + output[i++] = certReqCtxLen; + if (certReqCtxLen > 0) { + XMEMCPY(output + i, certReqCtx, certReqCtxLen); + i += certReqCtxLen; + } + c32to24(listSz, output + i); + i += CERT_HEADER_SZ; + if (certSz > 0) { + c32to24(certSz, output + i); + i += CERT_HEADER_SZ; + } + + return i; +} + +/* Write part of the certificate list entries of a TLS v1.3 Certificate + * message: the leaf certificate and its extensions, then each chain + * certificate with its length and extensions. + * + * When OCSP stapling is compiled in on the server, the stapled response + * buffer (ssl->buffers.certExts[]) of each entry that is fully written is + * freed, so the same entries cannot be written a second time. + * + * ssl SSL/TLS object. + * msg Certificate message layout: certificate sizes and extension + * sizes of each entry. + * pos Offset into the entries, after the leaf's length, to start at. + * output The buffer to write to. + * outSz The maximum number of bytes to write. + * returns the number of bytes written. + */ +static word32 WriteTls13CertEntries(WOLFSSL* ssl, const Tls13CertMsg* msg, + word32 pos, byte* output, word32 outSz, + byte shouldFreeExt) +{ + word32 written = 0; + word32 start = 0; + word32 entrySz; + word32 idx = 0; + word32 len; + word32 l; + word16 extIdx = 0; + byte* cert; + + if (msg->certSz == 0) + return 0; + + cert = ssl->buffers.certificate->buffer; + len = msg->certSz; + for (;;) { + entrySz = len + msg->extSz[extIdx]; + if (pos < start + entrySz) { + if (written == outSz) + break; + l = AddCertExt(ssl, cert, len, msg->extSz[extIdx], pos - start, + outSz - written, output + written, extIdx); + written += l; + pos += l; + if (pos < start + entrySz) + break; + } + #if defined(HAVE_CERTIFICATE_STATUS_REQUEST) && !defined(NO_WOLFSSL_SERVER) + if (shouldFreeExt) + FreeDer(&ssl->buffers.certExts[extIdx]); + #else + (void)shouldFreeExt; + #endif + start += entrySz; + + if (msg->certChainSz == 0) + break; + cert = ssl->buffers.certChain->buffer + idx; + len = NextCert(ssl->buffers.certChain->buffer, + ssl->buffers.certChain->length, &idx); + if (len == 0) + break; + #if defined(HAVE_CERTIFICATE_STATUS_REQUEST) && !defined(NO_WOLFSSL_SERVER) + if (extIdx + 1 < MAX_CERT_EXTENSIONS) + extIdx++; + #endif + } + + return written; +} #if defined(HAVE_CERTIFICATE_STATUS_REQUEST) && !defined(NO_WOLFSSL_SERVER) static int SetupOcspResp(WOLFSSL* ssl) @@ -10708,51 +10840,33 @@ static int CheckCertChainSigAlgo(WOLFSSL* ssl) } #endif /* !NO_CERTS && !WOLFSSL_NO_SIGALG */ -/* handle generation TLS v1.3 certificate (11) */ -/* Send the certificate for this end and any CAs that help with validation. - * This message is always encrypted in TLS v1.3. + +/* Pick the certificate to send and work out the layout of the Certificate + * message. * * ssl The SSL/TLS object. + * msg The layout, filled in. * returns 0 on success, otherwise failure. */ -static int SendTls13Certificate(WOLFSSL* ssl) +static int Tls13CertMsgSetup(WOLFSSL* ssl, Tls13CertMsg* msg) { int ret = 0; - word32 certSz, certChainSz, headerSz, listSz, payloadSz; - word16 extSz[MAX_CERT_EXTENSIONS]; - word16 extIdx = 0; - word32 maxFragment; - word32 totalextSz = 0; - word32 len = 0; - word32 idx = 0; - word32 offset = 0; - word32 entrySz = 0; - byte* p = NULL; - byte certReqCtxLen = 0; + word16 extIdx; sword32 length; -#ifdef WOLFSSL_POST_HANDSHAKE_AUTH - byte* certReqCtx = NULL; -#endif #ifndef WOLFSSL_NO_SIGALG int chainRet; #endif - #ifdef OPENSSL_EXTRA WOLFSSL_X509* x509 = NULL; WOLFSSL_EVP_PKEY* pkey = NULL; #endif - WOLFSSL_START(WC_FUNC_CERTIFICATE_SEND); - WOLFSSL_ENTER("SendTls13Certificate"); - - XMEMSET(extSz, 0, sizeof(extSz)); - - ssl->options.buildingMsg = 1; + XMEMSET(msg, 0, sizeof(*msg)); #ifdef WOLFSSL_POST_HANDSHAKE_AUTH if (ssl->options.side == WOLFSSL_CLIENT_END && ssl->certReqCtx != NULL) { - certReqCtxLen = ssl->certReqCtx->len; - certReqCtx = &ssl->certReqCtx->ctx; + msg->certReqCtxLen = ssl->certReqCtx->len; + msg->certReqCtx = &ssl->certReqCtx->ctx; } #endif @@ -10811,11 +10925,11 @@ static int SendTls13Certificate(WOLFSSL* ssl) #endif if (ssl->options.sendVerify == SEND_BLANK_CERT) { - certSz = 0; - certChainSz = 0; - headerSz = OPAQUE8_LEN + certReqCtxLen + CERT_HEADER_SZ; - length = (sword32)headerSz; - listSz = 0; + msg->certSz = 0; + msg->certChainSz = 0; + msg->headerSz = OPAQUE8_LEN + msg->certReqCtxLen + CERT_HEADER_SZ; + length = (sword32)msg->headerSz; + msg->listSz = 0; } else { if (!ssl->buffers.certificate || !ssl->buffers.certificate->buffer) { @@ -10823,17 +10937,17 @@ static int SendTls13Certificate(WOLFSSL* ssl) return NO_CERT_ERROR; } /* Certificate Data */ - certSz = ssl->buffers.certificate->length; + msg->certSz = ssl->buffers.certificate->length; if (ssl->buffers.certChainCnt > MAX_CHAIN_DEPTH) { WOLFSSL_MSG("Certificate chain count exceeds maximum depth"); return MAX_CHAIN_ERROR; } /* Cert Req Ctx Len | Cert Req Ctx | Cert List Len | Cert Data Len */ - headerSz = OPAQUE8_LEN + certReqCtxLen + CERT_HEADER_SZ + - CERT_HEADER_SZ; + msg->headerSz = OPAQUE8_LEN + msg->certReqCtxLen + CERT_HEADER_SZ + + CERT_HEADER_SZ; /* set empty extension as default */ - for (extIdx = 0; extIdx < (word16)XELEM_CNT(extSz); extIdx++) - extSz[extIdx] = OPAQUE16_LEN; + for (extIdx = 0; extIdx < (word16)XELEM_CNT(msg->extSz); extIdx++) + msg->extSz[extIdx] = OPAQUE16_LEN; #if defined(HAVE_CERTIFICATE_STATUS_REQUEST) && !defined(NO_WOLFSSL_SERVER) /* Staple our own OCSP response with the Certificate. Normally only the @@ -10858,92 +10972,87 @@ static int SendTls13Certificate(WOLFSSL* ssl) if ((1 + ssl->buffers.certChainCnt) > MAX_CERT_EXTENSIONS) ret = MAX_CERT_EXTENSIONS_ERR; if (ret == 0) - ret = WriteCSRToBuffer(ssl, &ssl->buffers.certExts[0], &extSz[0], + ret = WriteCSRToBuffer(ssl, &ssl->buffers.certExts[0], + &msg->extSz[0], 1 /* +1 for leaf */ + (word16)ssl->buffers.certChainCnt); if (ret < 0) return ret; - totalextSz += ret; + msg->totalextSz += (word32)ret; ret = 0; /* Clear to signal no error */ } else #endif { /* Leaf cert empty extension size */ - totalextSz += OPAQUE16_LEN; + msg->totalextSz += OPAQUE16_LEN; /* chain cert empty extension size */ - totalextSz += OPAQUE16_LEN * ssl->buffers.certChainCnt; + msg->totalextSz += OPAQUE16_LEN * ssl->buffers.certChainCnt; } /* Length of message data with one certificate and extensions. */ - length = (sword32)(headerSz + certSz + totalextSz); + length = (sword32)(msg->headerSz + msg->certSz + msg->totalextSz); /* Length of list data with one certificate and extensions. */ - listSz = CERT_HEADER_SZ + certSz + totalextSz; + msg->listSz = CERT_HEADER_SZ + msg->certSz + msg->totalextSz; /* Send rest of chain if sending cert (chain has leading size/s). */ - if (certSz > 0 && ssl->buffers.certChainCnt > 0) { - p = ssl->buffers.certChain->buffer; + if (msg->certSz > 0 && ssl->buffers.certChainCnt > 0) { /* Chain length including extensions. */ - certChainSz = ssl->buffers.certChain->length; + msg->certChainSz = ssl->buffers.certChain->length; - length += certChainSz; - listSz += certChainSz; + length += (sword32)msg->certChainSz; + msg->listSz += msg->certChainSz; } else - certChainSz = 0; + msg->certChainSz = 0; } - payloadSz = (word32)length; - - if (ssl->fragOffset != 0) - length -= (ssl->fragOffset + headerSz); + msg->payloadSz = (word32)length; - maxFragment = (word32)wolfssl_local_GetMaxPlaintextSize(ssl); - - extIdx = 0; + return ret; +} - /* Only ssl->fragOffset survives a WANT_WRITE, so a resume inside the chain - * has to rebuild the walk cursor from it. */ - if (certChainSz > 0 && ssl->fragOffset >= certSz + extSz[0]) { - word32 chainPos = ssl->fragOffset - (certSz + extSz[0]); +/* Finish sending a TLS v1.3 Certificate, or the message sent in its place. + * + * ssl The SSL/TLS object. + * ret The result of sending the message. + */ +static void Tls13CertificateSendDone(WOLFSSL* ssl, int ret) +{ + if (ret != WC_NO_ERR_TRACE(WANT_WRITE)) { + /* Clean up the fragment offset. */ + ssl->options.buildingMsg = 0; + ssl->fragOffset = 0; + if (ssl->options.side == WOLFSSL_SERVER_END) + ssl->options.serverState = SERVER_CERT_COMPLETE; + } - #if defined(HAVE_CERTIFICATE_STATUS_REQUEST) && !defined(NO_WOLFSSL_SERVER) - /* The leaf is behind us and its buffer was rebuilt above. */ - FreeDer(&ssl->buffers.certExts[0]); - #endif +#ifdef WOLFSSL_POST_HANDSHAKE_AUTH + if (ssl->options.side == WOLFSSL_CLIENT_END && ssl->certReqCtx != NULL) { + CertReqCtx* ctx = ssl->certReqCtx; + ssl->certReqCtx = ssl->certReqCtx->next; + XFREE(ctx, ssl->heap, DYNAMIC_TYPE_TMP_BUFFER); + } +#endif +} - while (chainPos > 0) { - word32 prevIdx = idx; +/* Write a TLS v1.3 Certificate message into records and send them. Resumes + * from ssl->fragOffset after a WANT_WRITE. + * + * ssl The SSL object. + * msg The layout of the message. + * returns 0 on success, otherwise failure. + */ +static int SendTls13CertificateRecords(WOLFSSL* ssl, const Tls13CertMsg* msg) +{ + int ret = 0; + word32 maxFragment; + word32 copySz; + sword32 length = (sword32)msg->payloadSz; - len = NextCert(ssl->buffers.certChain->buffer, - ssl->buffers.certChain->length, &idx); - if (len == 0) - break; - #if defined(HAVE_CERTIFICATE_STATUS_REQUEST) && \ - !defined(NO_WOLFSSL_SERVER) - if (extIdx + 1 < MAX_CERT_EXTENSIONS) - extIdx++; - #endif - entrySz = len + extSz[extIdx]; + if (ssl->fragOffset != 0) + length -= (sword32)(ssl->fragOffset + msg->headerSz); - if (chainPos < entrySz) { - /* Resume part way through this entry. */ - p = ssl->buffers.certChain->buffer + prevIdx; - offset = chainPos; - chainPos = 0; - } - else { - /* Entry already sent in full; stay primed for the next one. */ - #if defined(HAVE_CERTIFICATE_STATUS_REQUEST) && \ - !defined(NO_WOLFSSL_SERVER) - /* Its buffer was rebuilt above and nothing writes it again. */ - FreeDer(&ssl->buffers.certExts[extIdx]); - #endif - chainPos -= entrySz; - offset = 0; - entrySz = 0; - } - } - } + maxFragment = (word32)wolfssl_local_GetMaxPlaintextSize(ssl); while (length > 0 && ret == 0) { byte* output = NULL; @@ -10959,15 +11068,14 @@ static int SendTls13Certificate(WOLFSSL* ssl) #endif /* WOLFSSL_DTLS13 */ if (ssl->fragOffset == 0) { - if (headerSz + certSz + totalextSz + certChainSz <= - maxFragment - HANDSHAKE_HEADER_SZ) { - fragSz = headerSz + certSz + totalextSz + certChainSz; + if (msg->payloadSz <= maxFragment - HANDSHAKE_HEADER_SZ) { + fragSz = msg->payloadSz; } #ifdef WOLFSSL_DTLS13 else if (ssl->options.dtls){ /* short-circuit the fragmentation logic here. DTLS fragmentation will be done in dtls13HandshakeSend() */ - fragSz = headerSz + certSz + totalextSz + certChainSz; + fragSz = msg->payloadSz; } #endif /* WOLFSSL_DTLS13 */ else { @@ -10998,86 +11106,22 @@ static int SendTls13Certificate(WOLFSSL* ssl) output = GetOutputBuffer(ssl); if (ssl->fragOffset == 0) { - AddTls13FragHeaders(output, fragSz, 0, payloadSz, certificate, ssl); + AddTls13FragHeaders(output, fragSz, 0, msg->payloadSz, certificate, + ssl); - /* Request context. */ - output[i++] = certReqCtxLen; - #ifdef WOLFSSL_POST_HANDSHAKE_AUTH - if (certReqCtxLen > 0) { - XMEMCPY(output + i, certReqCtx, certReqCtxLen); - i += certReqCtxLen; - } - #endif - length -= OPAQUE8_LEN + certReqCtxLen; - fragSz -= OPAQUE8_LEN + certReqCtxLen; - /* Certificate list length. */ - c32to24(listSz, output + i); - i += CERT_HEADER_SZ; - length -= CERT_HEADER_SZ; - fragSz -= CERT_HEADER_SZ; - /* Leaf certificate data length. */ - if (certSz > 0) { - c32to24(certSz, output + i); - i += CERT_HEADER_SZ; - length -= CERT_HEADER_SZ; - fragSz -= CERT_HEADER_SZ; - } + copySz = WriteTls13CertHeader(output + i, msg); + i += copySz; + length -= (sword32)copySz; + fragSz -= copySz; } else AddTls13RecordHeader(output, fragSz, handshake, ssl); - if (extIdx == 0) { - if (certSz > 0 && ssl->fragOffset < certSz + extSz[0]) { - /* Put in the leaf certificate with extensions. */ - word32 copySz = AddCertExt(ssl, ssl->buffers.certificate->buffer, - certSz, extSz[0], ssl->fragOffset, fragSz, - output + i, 0); - i += copySz; - ssl->fragOffset += copySz; - length -= copySz; - fragSz -= copySz; - if (ssl->fragOffset == certSz + extSz[0]) - FreeDer(&ssl->buffers.certExts[0]); - } - } - if (certChainSz > 0 && fragSz > 0) { - /* Put in the CA certificates with extensions. */ - while (fragSz > 0) { - word32 l; - - if (offset == entrySz) { - /* Find next CA certificate to write out. */ - offset = 0; - /* Point to the start of current cert in chain buffer. */ - p = ssl->buffers.certChain->buffer + idx; - len = NextCert(ssl->buffers.certChain->buffer, - ssl->buffers.certChain->length, &idx); - if (len == 0) - break; - #if defined(HAVE_CERTIFICATE_STATUS_REQUEST) && \ - !defined(NO_WOLFSSL_SERVER) - if (extIdx + 1 < MAX_CERT_EXTENSIONS) - extIdx++; - #endif - /* Certificate and its extensions make up the entry. */ - entrySz = len + extSz[extIdx]; - } - /* Write out certificate and extension. */ - l = AddCertExt(ssl, p, len, extSz[extIdx], offset, fragSz, - output + i, extIdx); - i += l; - ssl->fragOffset += l; - length -= l; - fragSz -= l; - offset += l; - - if (extIdx != 0 && extIdx < MAX_CERT_EXTENSIONS && - ssl->buffers.certExts[extIdx] != NULL && - offset == entrySz) { - FreeDer(&ssl->buffers.certExts[extIdx]); - } - } - } + copySz = WriteTls13CertEntries(ssl, msg, ssl->fragOffset, output + i, + fragSz, 1); + i += copySz; + ssl->fragOffset += copySz; + length -= (sword32)copySz; if ((int)i - RECORD_HEADER_SZ < 0) { WOLFSSL_MSG("Send Cert bad inputSz"); @@ -11125,27 +11169,308 @@ static int SendTls13Certificate(WOLFSSL* ssl) } } - if (ret != WC_NO_ERR_TRACE(WANT_WRITE)) { - /* Clean up the fragment offset. */ - ssl->options.buildingMsg = 0; - ssl->fragOffset = 0; - if (ssl->options.side == WOLFSSL_SERVER_END) - ssl->options.serverState = SERVER_CERT_COMPLETE; + Tls13CertificateSendDone(ssl, ret); + + return ret; +} + +/* handle generation TLS v1.3 certificate (11) */ +/* Send the certificate for this end and any CAs that help with validation. + * This message is always encrypted in TLS v1.3. + * + * ssl The SSL/TLS object. + * returns 0 on success, otherwise failure. + */ +static int SendTls13Certificate(WOLFSSL* ssl) +{ + int ret; + Tls13CertMsg msg; + + WOLFSSL_START(WC_FUNC_CERTIFICATE_SEND); + WOLFSSL_ENTER("SendTls13Certificate"); + + ssl->options.buildingMsg = 1; + + ret = Tls13CertMsgSetup(ssl, &msg); + if (ret != 0) + return ret; + + ret = SendTls13CertificateRecords(ssl, &msg); + + WOLFSSL_LEAVE("SendTls13Certificate", ret); + WOLFSSL_END(WC_FUNC_CERTIFICATE_SEND); + + return ret; +} + +#ifdef WOLFSSL_CERT_COMPRESSION +/* Serialize the uncompressed Certificate message into a new buffer. */ +static int BuildTls13CertMsg(WOLFSSL* ssl, const Tls13CertMsg* msg, + byte** out) +{ + byte* buf = (byte*)XMALLOC(msg->payloadSz, ssl->heap, + DYNAMIC_TYPE_TMP_BUFFER); + + if (buf == NULL) + return MEMORY_ERROR; + if (WriteTls13CertHeader(buf, msg) != msg->headerSz || + WriteTls13CertEntries(ssl, msg, 0, buf + msg->headerSz, + msg->payloadSz - msg->headerSz, 0) != + msg->payloadSz - msg->headerSz) { + XFREE(buf, ssl->heap, DYNAMIC_TYPE_TMP_BUFFER); + return BUFFER_ERROR; } + *out = buf; + return 0; +} -#ifdef WOLFSSL_POST_HANDSHAKE_AUTH - if (ssl->options.side == WOLFSSL_CLIENT_END && ssl->certReqCtx != NULL) { - CertReqCtx* ctx = ssl->certReqCtx; - ssl->certReqCtx = ssl->certReqCtx->next; - XFREE(ctx, ssl->heap, DYNAMIC_TYPE_TMP_BUFFER); +static int CompressCertificateMsg(WOLFSSL* ssl, byte* uncompMsg, + word32 uncompMsgSz) +{ + int ret = 0; + + /* create our compression object */ + if (ret == 0 && wc_CompressionData_InitComp(ssl->compressedCert, + uncompMsg, uncompMsgSz, + ssl->peerCertCompressionAlg) != 0) { + WOLFSSL_MSG("Could not create compression object"); + ret = BUFFER_ERROR; } + + if (ret == 0 && wc_CompressionData_SetHeap(ssl->compressedCert, ssl->heap) + != 0) { + WOLFSSL_MSG("Could not set heap on compressed cert object"); + ret = BUFFER_ERROR; + } + + if (ret == 0 && wc_CompressionData_Compress(ssl->compressedCert) != 0) { + WOLFSSL_MSG("Could not compress cert msg"); + ret = BUFFER_ERROR; + } + + /* uncompMsg is only borrowed; the caller keeps or frees it */ + return ret; +} + +/* handle generation TLS v1.3 compressed_certificate (25) */ +/* Send the certificate for this end and any CAs that help with validation. + * This message is always encrypted in TLS v1.3. + * + * ssl The SSL/TLS object. + * returns 0 on success, otherwise failure. + */ +static int SendTls13CompressedCertificate(WOLFSSL* ssl) +{ + int ret = 0; + Tls13CertMsg msg; + word32 maxFragmentSz; + word32 fullCompCertMsgSz = 0; + + WOLFSSL_START(WC_FUNC_CERTIFICATE_SEND); + WOLFSSL_ENTER("SendTls13CompressedCertificate"); + + ssl->options.buildingMsg = 1; + + /* refresh the compressed cert object when starting new message */ + if (ssl->fragOffset == 0) { + byte* certMsg = NULL; + ret = Tls13CertMsgSetup(ssl, &msg); + if (ret != 0) + return ret; + + /* Nothing to compress fallback to uncompressed path */ + if (msg.certSz == 0) { + WOLFSSL_LEAVE("SendTls13CompressedCertificate", ret); + WOLFSSL_END(WC_FUNC_CERTIFICATE_SEND); + return SendTls13CertificateRecords(ssl, &msg); + } + ret = BuildTls13CertMsg(ssl, &msg, &certMsg); + if (ret != 0) + return ret; + /* reuse our compression data object if we can */ + if (ssl->compressedCert != NULL) { + wc_CompressionData_Free(ssl->compressedCert); + } + /* else make a new object */ + else { + ssl->compressedCert = (wc_CompressionData*)XMALLOC( + sizeof(*ssl->compressedCert), ssl->heap, + DYNAMIC_TYPE_TMP_BUFFER); + if (ssl->compressedCert == NULL) { + XFREE(certMsg, ssl->heap, DYNAMIC_TYPE_TMP_BUFFER); + return MEMORY_ERROR; + } + } + ret = CompressCertificateMsg(ssl, certMsg, msg.payloadSz); + XFREE(certMsg, ssl->heap, DYNAMIC_TYPE_TMP_BUFFER); + if (ret != 0) { + WOLFSSL_MSG("Could not compress Certificate falling back " + "to sending plain cert"); + wc_CompressionData_Free(ssl->compressedCert); + XFREE(ssl->compressedCert, ssl->heap, DYNAMIC_TYPE_TMP_BUFFER); + ssl->compressedCert = NULL; + ssl->peerCertCompressionAlg = WC_NO_COMPRESSION; + return SendTls13CertificateRecords(ssl, &msg); + } +#if defined(HAVE_CERTIFICATE_STATUS_REQUEST) && !defined(NO_WOLFSSL_SERVER) + /* If compression succeeded we need to free the cert extensions + * in ssl object. These were not freed in BuildTls13CertMsg to leave + * them available for the plain cert fallback if compression failed */ + else { + int i = 0; + for(; i < (int)XELEM_CNT(ssl->buffers.certExts); i++) { + FreeDer(&ssl->buffers.certExts[i]); + } + } #endif + } - WOLFSSL_LEAVE("SendTls13Certificate", ret); + /* should never be null here */ + if (ssl->compressedCert == NULL || + ssl->fragOffset > ssl->compressedCert->compressedSz) { + WOLFSSL_MSG("No compressed cert message to resume."); + return SANITY_MSG_E; + } + + maxFragmentSz = (word32)wolfssl_local_GetMaxPlaintextSize(ssl); + + /* calc size of compressed_certificate msg */ + fullCompCertMsgSz += ssl->compressedCert->compressedSz - ssl->fragOffset; + if (ssl->fragOffset == 0) { + fullCompCertMsgSz += COMP_CERT_HEADER_SZ; + } + + while (fullCompCertMsgSz > 0 && ret == 0) { + byte* output = NULL; + word32 fragSz = 0; + word32 idx = RECORD_HEADER_SZ; + int sendSz = RECORD_HEADER_SZ; + +#ifdef WOLFSSL_DTLS13 + if (ssl->options.dtls) { + idx = Dtls13GetRlHeaderLength(ssl, 1); + sendSz = (int)idx; + } +#endif + if (ssl->fragOffset == 0) { + if (fullCompCertMsgSz <= maxFragmentSz - HANDSHAKE_HEADER_SZ) { + fragSz = fullCompCertMsgSz; + } + else { + fragSz = maxFragmentSz - HANDSHAKE_HEADER_SZ; + } + +#ifdef WOLFSSL_DTLS13 + if (ssl->options.dtls) { + fragSz = fullCompCertMsgSz; + sendSz += DTLS_HANDSHAKE_EXTRA; + idx += DTLS_HANDSHAKE_EXTRA; + } +#endif + sendSz += fragSz + HANDSHAKE_HEADER_SZ; + idx += HANDSHAKE_HEADER_SZ; + } + else { + fragSz = min((word32) fullCompCertMsgSz, maxFragmentSz); + sendSz += fragSz; + } + + sendSz += MAX_MSG_EXTRA; + + if ((ret = CheckAvailableSize(ssl, sendSz)) != 0) { + return ret; + } + + output = GetOutputBuffer(ssl); + + if (ssl->fragOffset == 0) { + AddTls13FragHeaders(output, fragSz, 0, fullCompCertMsgSz, + compressed_certificate, ssl); + /* add alg id */ + c16toa((word16)ssl->compressedCert->compressionAlg, output + idx); + idx += OPAQUE16_LEN; + c32to24(ssl->compressedCert->uncompressedSz, output + idx); + idx += OPAQUE24_LEN; + c32to24(ssl->compressedCert->compressedSz, output + idx); + idx += OPAQUE24_LEN; + fragSz -= COMP_CERT_HEADER_SZ; + fullCompCertMsgSz -= COMP_CERT_HEADER_SZ; + } + else { + AddTls13RecordHeader(output, fragSz, handshake, ssl); + } + + /* just keep sending compressed bytes */ + XMEMCPY(output + idx, ssl->compressedCert->data + ssl->fragOffset, + fragSz); + + fullCompCertMsgSz -= fragSz; + idx += fragSz; + ssl->fragOffset += fragSz; + + if ((int)idx - RECORD_HEADER_SZ < 0) { + WOLFSSL_MSG("Send Cert bad inputSz"); + return BUFFER_E; + } + +#ifdef WOLFSSL_DTLS13 + if (ssl->options.dtls) { + ssl->options.buildingMsg = 0; + ssl->fragOffset = 0; + if ((word32)sendSz > WOLFSSL_MAX_16BIT || idx > WOLFSSL_MAX_16BIT) { + WOLFSSL_MSG("Send Cert DTLS size exceeds word16"); + return BUFFER_E; + } + ret = Dtls13HandshakeSend(ssl, output, (word16)sendSz, (word16)idx, + compressed_certificate, 1); + } + else +#endif + { + sendSz = BuildTls13Message(ssl, output, sendSz, + output + RECORD_HEADER_SZ, (int)(idx - RECORD_HEADER_SZ), + handshake, 1, 0, 0); + if (sendSz < 0) + return sendSz; +#if defined(WOLFSSL_CALLBACKS) || defined(OPENSSL_EXTRA) + if (ssl->hsInfoOn) + AddPacketName(ssl, "CompressedCertificate"); + if (ssl->toInfoOn) { + ret = AddPacketInfo(ssl, "CompressedCertificate", handshake, + output, sendSz, WRITE_PROTO, 0, ssl->heap); + if (ret != 0) + return ret; + } +#endif + ssl->buffers.outputBuffer.length += (word32)sendSz; + ssl->options.buildingMsg = 0; + if (!ssl->options.groupMessages) + ret = SendBuffered(ssl); + } + } + + Tls13CertificateSendDone(ssl, ret); + + if ((ret != WC_NO_ERR_TRACE(WANT_WRITE) +#ifdef WOLFSSL_DTLS13 + /* Dtls13HandshakeSend has sent the entire message so we want + * to free no matter what */ + || ssl->options.dtls +#endif + ) + ) { + wc_CompressionData_Free(ssl->compressedCert); + XFREE(ssl->compressedCert, ssl->heap, DYNAMIC_TYPE_TMP_BUFFER); + ssl->compressedCert = NULL; + ssl->peerCertCompressionAlg = WC_NO_COMPRESSION; + } + + WOLFSSL_LEAVE("SendTls13CompressedCertificate", ret); WOLFSSL_END(WC_FUNC_CERTIFICATE_SEND); return ret; } +#endif #if (!defined(NO_RSA) || defined(HAVE_ECC) || defined(HAVE_ED25519) || \ defined(HAVE_ED448) || defined(HAVE_FALCON) || \ @@ -12536,6 +12861,161 @@ static int DoTls13Certificate(WOLFSSL* ssl, byte* input, word32* inOutIdx, return ret; } + +#ifdef WOLFSSL_CERT_COMPRESSION +static int CompressionAlgWasRequested(WOLFSSL* ssl, + word16 alg) +{ + byte i = 0; + byte* cursor = NULL; + TLSX* ext; + byte len; + if (ssl->extensions == NULL) + return 0; + ext = TLSX_Find(ssl->extensions, TLSX_CERT_COMPRESSION); + if (ext == NULL) + return 0; + + cursor = (byte*)ext->data; + len = *cursor; + cursor += 1; + for (; i < len; i += 2, cursor += 2) { + word16 reqAlg = 0; + ato16(cursor, &reqAlg); + if (alg == reqAlg) + return 1; + } + return 0; +} + +/* handle processing compressed TLS v1.3 certificate (25) */ +/* Parse and handle a TLS v1.3 Certificate message. + * + * Wraps DoTls13Certificate which sees the cert after it is decompressed + * + * ssl The SSL/TLS object. + * input The message buffer. + * inOutIdx On entry, the index into the message buffer of Certificate. + * On exit, the index of byte after the Certificate message. + * totalSz The length of the current handshake message. + * returns 0 on success and otherwise failure. + */ +int DoTls13CompressedCertificate(WOLFSSL* ssl, byte* input, + word32* inOutIdx, word32 totalSz) +{ + int ret = 0; + word32 idx = *inOutIdx; + word16 alg; + word32 uncompSz; + word32 compSz; + word32 certIdx = 0; + + WOLFSSL_START(WC_FUNC_CERTIFICATE_DO); + WOLFSSL_ENTER("DoTls13CompressedCertificate"); + + if (totalSz < COMP_CERT_HEADER_SZ) + ERROR_OUT(BUFFER_ERROR, exit_dcc); + + ato16(input + idx, &alg); + idx += OPAQUE16_LEN; + c24to32(input + idx, &uncompSz); + idx += OPAQUE24_LEN; + c24to32(input + idx, &compSz); + idx += OPAQUE24_LEN; + + if (compSz == 0 || compSz != totalSz - COMP_CERT_HEADER_SZ) + ERROR_OUT(BUFFER_ERROR, exit_dcc); + + /* check if compression alg is linked in */ + if (!wc_IsCompressionAlgSupported(alg)) { + WOLFSSL_MSG("Alg sent was not expected"); + SendAlert(ssl, alert_fatal, illegal_parameter); + ERROR_OUT(DECOMPRESS_E, exit_dcc); + } + + if (!CompressionAlgWasRequested(ssl, alg)) { + WOLFSSL_MSG("Alg sent was not requested"); + SendAlert(ssl, alert_fatal, illegal_parameter); + ERROR_OUT(DECOMPRESS_E, exit_dcc); + } + + /* if not NULL we already have the decompressed cert in hand and just + * needed to recall this func due to a pending or want read */ + if (ssl->compressedCert == NULL) { + if (uncompSz == 0 || uncompSz > MAX_CERTIFICATE_SZ + OPAQUE8_LEN + + /* 255 ie. 2^8-1 is the max len of the certificate context + * RFC-9846 section 4.5.1 */ + CERT_HEADER_SZ + 255) { + WOLFSSL_MSG("CompressedCertificate uncompressed_length too big"); + SendAlert(ssl, alert_fatal, bad_certificate); + ERROR_OUT(DECOMPRESS_E, exit_dcc); + } + + ssl->compressedCert = (wc_CompressionData*)XMALLOC( + sizeof(wc_CompressionData), ssl->heap, DYNAMIC_TYPE_TMP_BUFFER); + + if (ssl->compressedCert == NULL) { + ERROR_OUT(MEMORY_ERROR, exit_dcc); + } + + if (wc_CompressionData_InitDeComp(ssl->compressedCert, input + idx, + compSz, uncompSz, alg) != 0) { + WOLFSSL_MSG("Could not create compression object"); + SendAlert(ssl, alert_fatal, bad_certificate); + ERROR_OUT(DECOMPRESS_E, exit_dcc); + } + + if (wc_CompressionData_SetHeap(ssl->compressedCert, ssl->heap) != 0) { + WOLFSSL_MSG("Could not set heap on compressed cert object"); + ERROR_OUT(MEMORY_E, exit_dcc); + } + + if ((ret = wc_CompressionData_DeCompress(ssl->compressedCert)) != 0) { + WOLFSSL_MSG("Could not decompress cert"); + if (ret == WC_NO_ERR_TRACE(MEMORY_E)) + ERROR_OUT(ret, exit_dcc); + SendAlert(ssl, alert_fatal, bad_certificate); + ERROR_OUT(DECOMPRESS_E, exit_dcc); + } + + if (ssl->compressedCert->isCompressed || + ssl->compressedCert->uncompressedSz != uncompSz) { + SendAlert(ssl, alert_fatal, bad_certificate); + ERROR_OUT(DECOMPRESS_E, exit_dcc); + } + } + /* quick state check */ + else if (ssl->compressedCert->isCompressed != 0 || + ssl->compressedCert->compressionAlg != alg || + ssl->compressedCert->uncompressedSz != uncompSz) { + ERROR_OUT(SANITY_MSG_E, exit_dcc); + } + + ret = DoTls13Certificate(ssl, ssl->compressedCert->data, &certIdx, + ssl->compressedCert->uncompressedSz); + + if (ret == 0) + *inOutIdx = idx + compSz; + +exit_dcc: + + if (ssl->compressedCert != NULL +#if defined(WOLFSSL_ASYNC_CRYPT) || defined(WOLFSSL_NONBLOCK_OCSP) + && !(ret == WC_NO_ERR_TRACE(WC_PENDING_E) || + ret == WC_NO_ERR_TRACE(OCSP_WANT_READ)) +#endif + ) { + wc_CompressionData_Free(ssl->compressedCert); + XFREE(ssl->compressedCert, ssl->heap, DYNAMIC_TYPE_TMP_BUFFER); + ssl->compressedCert = NULL; + } + + WOLFSSL_LEAVE("DoTls13CompressedCertificate", ret); + WOLFSSL_END(WC_FUNC_CERTIFICATE_DO); + + return ret; +} +#endif /* WOLFSSL_CERT_COMPRESSION */ #endif #if (!defined(NO_RSA) || defined(HAVE_ECC) || defined(HAVE_ED25519) || \ @@ -15311,7 +15791,11 @@ static int SanityCheckTls13MsgReceived(WOLFSSL* ssl, byte type) break; #endif - + #ifdef WOLFSSL_CERT_COMPRESSION + /* compressed certificate and certificate are valid in the same + * states. */ + case compressed_certificate: + #endif case certificate: /* Valid on both sides. */ #ifndef NO_WOLFSSL_CLIENT @@ -16049,7 +16533,8 @@ int DoTls13HandShakeMsgType(WOLFSSL* ssl, byte* input, word32* inOutIdx, if (ssl->options.handShakeState == HANDSHAKE_DONE && type != session_ticket && type != certificate_request && - type != certificate && type != key_update && type != finished + type != certificate && type != compressed_certificate && + type != key_update && type != finished #if defined(WOLFSSL_DTLS13) && defined(WOLFSSL_DTLS_CID) && type != request_connection_id && type != new_connection_id #endif @@ -16223,6 +16708,13 @@ int DoTls13HandShakeMsgType(WOLFSSL* ssl, byte* input, word32* inOutIdx, /* Messages received by both client and server. */ #if !defined(NO_CERTS) && (!defined(NO_WOLFSSL_CLIENT) || \ !defined(WOLFSSL_NO_CLIENT_AUTH)) +#ifdef WOLFSSL_CERT_COMPRESSION + case compressed_certificate: + WOLFSSL_MSG("processing compressed certificate"); + ret = DoTls13CompressedCertificate(ssl, input, inOutIdx, size); + break; +#endif + case certificate: WOLFSSL_MSG("processing certificate"); ret = DoTls13Certificate(ssl, input, inOutIdx, size); @@ -16920,13 +17412,28 @@ int wolfSSL_connect_TLSv13(WOLFSSL* ssl) #ifndef NO_CERTS if ((!ssl->options.resuming || TLS13_AFTER_HANDSHAKE(ssl)) && ssl->options.sendVerify) { - ssl->error = SendTls13Certificate(ssl); - if (ssl->error != 0) { - wolfssl_local_MaybeCheckAlertOnErr(ssl, ssl->error); - WOLFSSL_ERROR(ssl->error); - return WOLFSSL_FATAL_ERROR; +#ifdef WOLFSSL_CERT_COMPRESSION + if (wc_IsCompressionAlgSupported( + ssl->peerCertCompressionAlg)) { + ssl->error = SendTls13CompressedCertificate(ssl); + if (ssl->error != 0) { + wolfssl_local_MaybeCheckAlertOnErr(ssl, ssl->error); + WOLFSSL_ERROR(ssl->error); + return WOLFSSL_FATAL_ERROR; + } + WOLFSSL_MSG("sent: compressed_certificate"); + } + else +#endif + { + ssl->error = SendTls13Certificate(ssl); + if (ssl->error != 0) { + wolfssl_local_MaybeCheckAlertOnErr(ssl, ssl->error); + WOLFSSL_ERROR(ssl->error); + return WOLFSSL_FATAL_ERROR; + } + WOLFSSL_MSG("sent: certificate"); } - WOLFSSL_MSG("sent: certificate"); } #endif @@ -17276,6 +17783,117 @@ int wolfSSL_CTX_no_ticket_TLSv13(WOLFSSL_CTX* ctx) return 0; } +#ifdef WOLFSSL_CERT_COMPRESSION +/* Validate a list of certificate compression algorithms and replace the list + * in dst with a copy of it. + * + * dst The list to replace; freed and set to the new copy on success. + * dstLen Number of algorithm IDs in dst; set on success. + * algs RFC 8879 algorithm IDs, most preferred first. + * count Number of algorithm IDs. + * heap Heap hint for the allocation. + * returns BAD_FUNC_ARG when algs is NULL, count is out of range, or an + * algorithm is not supported, MEMORY_E on allocation failure and + * WOLFSSL_SUCCESS on success. + */ +static int CertCompression_SetAlgs(word16** dst, + byte* dstLen, const word16* algs, int count, void* heap) +{ + int i; + word16* list; + + /* the wire list is at most 254 bytes of 2-byte IDs */ + if (algs == NULL || count < 1 || count > 127) + return BAD_FUNC_ARG; + for (i = 0; i < count; i++) { + if (!wc_IsCompressionAlgSupported(algs[i])) + return BAD_FUNC_ARG; + } + + list = (word16*)XMALLOC(sizeof(*list) * (size_t)count, + heap, DYNAMIC_TYPE_TLSX); + if (list == NULL) + return MEMORY_E; + XMEMCPY(list, algs, sizeof(*list) * (size_t)count); + XFREE(*dst, heap, DYNAMIC_TYPE_TLSX); + *dst = list; + *dstLen = (byte)count; + (void)heap; + + return WOLFSSL_SUCCESS; +} + +/* Set the certificate compression algorithms to offer, in order of + * preference, for every WOLFSSL created from this context afterwards. + * Replaces the built-in default list: {ZLIB} + * + * If count is 0 certificate_compression is turned off. + * + * ctx The SSL/TLS CTX object. + * algs RFC 8879 algorithm IDs, most preferred first. + * count Number of algorithm IDs. + * returns BAD_FUNC_ARG when ctx or algs is NULL, count is out of range, or an + * algorithm is not supported, MEMORY_E on allocation failure and + * WOLFSSL_SUCCESS on success. + */ +int wolfSSL_CTX_set_cert_compression_algs(WOLFSSL_CTX* ctx, + const word16* algs, int count) +{ + int ret; + + if (ctx == NULL) + return BAD_FUNC_ARG; + + if (count == 0) { + ctx->noOfferCompressionAlgPrefList = 1; + return WOLFSSL_SUCCESS; + } + + ret = CertCompression_SetAlgs(&ctx->compressionAlgPrefList, + &ctx->compressionAlgPrefListLen, algs, count, ctx->heap); + if (ret == WOLFSSL_SUCCESS) + ctx->noOfferCompressionAlgPrefList = 0; + return ret; +} + +/* Set the certificate compression algorithms to offer, in order of + * preference. Replaces the list inherited from the context, or the + * built-in default list: {ZLIB} + * + * If count is 0 certificate_compression is turned off. + * + * ssl The SSL/TLS object. + * algs RFC 8879 algorithm IDs, most preferred first. + * count Number of algorithm IDs. + * returns BAD_FUNC_ARG when ssl or algs is NULL, count is out of range, or an + * algorithm is not supported, MEMORY_E on allocation failure and + * WOLFSSL_SUCCESS on success. + */ +int wolfSSL_set_cert_compression_algs(WOLFSSL* ssl, + const word16* algs, int count) +{ + int ret; + + if (ssl == NULL) + return BAD_FUNC_ARG; + + if (count == 0) { + /* clear out the extension if already added */ + TLSX_Remove(&ssl->extensions, TLSX_CERT_COMPRESSION, ssl->heap); + ssl->noOfferCompressionAlgPrefList = 1; + return WOLFSSL_SUCCESS; + } + + ret = CertCompression_SetAlgs(&ssl->compressionAlgPrefList, + &ssl->compressionAlgPrefListLen, algs, count, ssl->heap); + if (ret == WOLFSSL_SUCCESS) { + TLSX_Remove(&ssl->extensions, TLSX_CERT_COMPRESSION, ssl->heap); + ssl->noOfferCompressionAlgPrefList = 0; + } + return ret; +} +#endif /* WOLFSSL_CERT_COMPRESSION */ + /* Do not send a ticket after TLS v1.3 handshake for resumption. * * ssl The SSL/TLS object. @@ -18219,7 +18837,18 @@ int wolfSSL_accept_TLSv13(WOLFSSL* ssl) case TLS13_CERT_REQ_SENT : #ifndef NO_CERTS if (!ssl->options.resuming && ssl->options.sendVerify) { - if ((ssl->error = SendTls13Certificate(ssl)) != 0) { +#ifdef WOLFSSL_CERT_COMPRESSION + if (wc_IsCompressionAlgSupported( + ssl->peerCertCompressionAlg)) { + ssl->error = SendTls13CompressedCertificate(ssl); + } + else +#endif + { + ssl->error = SendTls13Certificate(ssl); + } + + if (ssl->error != 0) { WOLFSSL_ERROR(ssl->error); return WOLFSSL_FATAL_ERROR; } diff --git a/tests/api.c b/tests/api.c index 1774ea5c40f..4c5799cca67 100644 --- a/tests/api.c +++ b/tests/api.c @@ -317,6 +317,7 @@ #include #include #include +#include #include #include #if !defined(NO_CERTS) && defined(WOLFSSL_ASN_TEMPLATE) && defined(HAVE_ECC) @@ -43966,6 +43967,7 @@ TEST_CASE testCases[] = { TEST_DECL(test_wolfSSL_set_options), TEST_TLS13_DECLS, + TEST_TLS13_CERT_COMPRESSION_DECLS, TEST_TLS13_BOUNDS_DECLS, TEST_TLS13_FEATURES_DECLS, diff --git a/tests/api/include.am b/tests/api/include.am index c67ed918039..5a447ddbf55 100644 --- a/tests/api/include.am +++ b/tests/api/include.am @@ -145,6 +145,7 @@ tests_unit_test_SOURCES += tests/api/test_evp_pkey.c tests_unit_test_SOURCES += tests/api/test_certman.c # TLS 1.3 specific tests_unit_test_SOURCES += tests/api/test_tls13.c +tests_unit_test_SOURCES += tests/api/test_tls13_cert_compression.c tests_unit_test_SOURCES += tests/api/test_tls13_bounds.c tests_unit_test_SOURCES += tests/api/test_tls13_features.c endif @@ -272,6 +273,7 @@ EXTRA_DIST += tests/api/test_evp_cipher.h EXTRA_DIST += tests/api/test_evp_pkey.h EXTRA_DIST += tests/api/test_certman.h EXTRA_DIST += tests/api/test_tls13.h +EXTRA_DIST += tests/api/test_tls13_cert_compression.h EXTRA_DIST += tests/api/test_tls13_bounds.h EXTRA_DIST += tests/api/test_tls13_features.h diff --git a/tests/api/test_compress.c b/tests/api/test_compress.c index 07ac81fd3e7..e4d1da52b47 100644 --- a/tests/api/test_compress.c +++ b/tests/api/test_compress.c @@ -28,9 +28,7 @@ * for the same reason. INT_MAX is used below. */ #include -#ifdef HAVE_LIBZ - #include -#endif +#include #include #include #include @@ -136,6 +134,234 @@ static int test_tls_compression_ssl_ready(WOLFSSL* ssl) } #endif /* TEST_TLS_COMPRESSION_ANY */ +#ifdef HAVE_LIBZ +static const word16 algList[] = { + WC_ZLIB, +}; + +static int test_wc_CompressionData_RoundTrip(void) +{ + EXPECT_DECLS; + word32 i = 0; + wc_CompressionData cd = {0}; + static byte data[3000]; + /* we don't need complex data here we are not testing if our + * compression algs compress correctly just that decomp -> comp -> decomp + * is working losslessly */ + XMEMSET(data, 'a', sizeof(data)); + for (i = 0; i < XELEM_CNT(algList); i ++) { + if (!wc_IsCompressionAlgSupported((word16)algList[i])) { + continue; + } + ExpectIntEQ(wc_CompressionData_InitComp(&cd, data, sizeof(data), + algList[i]), 0); + ExpectIntEQ(wc_CompressionData_Compress(&cd), 0); + ExpectIntGT(sizeof(data), cd.compressedSz); + ExpectIntEQ(wc_CompressionData_DeCompress(&cd), 0); + ExpectIntEQ(sizeof(data), cd.uncompressedSz); + ExpectIntEQ(XMEMCMP(cd.data, data, sizeof(data)), 0); + } + wc_CompressionData_Free(&cd); + return EXPECT_RESULT(); +} + +static const struct { + word16 alg; + const byte* compressedData; + word32 compressedSz; /* compressed data may contain 0x00 bytes */ + const byte* uncompressedData;} compressedTestVectors[] = { + /* Hello, world! Test vector for zlib. */ + {WC_ZLIB, + /* compressed data */ + (const byte*)"\x78\x01\xf3\x48\xcd\xc9\xc9\xd7\x51\x28\xcf\x2f" + "\xca\x49\x51\x54\x08\x49\x2d\x2e\x51\x28\x4b\x4d" + "\x2e\xc9\x2f\x52\x48\xcb\x2f\x52\xa8\xca\xc9\x4c" + "\xd2\x53\xf0\x20\xac\x06\x00\x7d\xd8\x18\xe5", + 47, + /* uncompressed message */ + (const byte*)"Hello, world! Test vector for zlib. " + "Hello, world! Test vector for zlib."}, + /* add more vectors here */ +}; + +static int test_wc_CompressionData_InitWithCompressedData(void) +{ + EXPECT_DECLS; + word32 i; + wc_CompressionData cd; + for (i = 0; i < XELEM_CNT(compressedTestVectors); i ++) { + word32 uncompLen = 0; + if (!wc_IsCompressionAlgSupported( + (word16)compressedTestVectors[i].alg)) { + /* skip test */ + continue; + } + + uncompLen = (word32)XSTRLEN( + (const char*)compressedTestVectors[i].uncompressedData); + ExpectIntEQ(wc_CompressionData_InitComp(&cd, + compressedTestVectors[i].uncompressedData, + uncompLen, compressedTestVectors[i].alg), 0); + ExpectIntEQ(wc_CompressionData_Compress(&cd), 0); + ExpectIntLE((int)cd.compressedSz, (int)uncompLen); + wc_CompressionData_Free(&cd); + + ExpectIntEQ(wc_CompressionData_InitDeComp(&cd, + compressedTestVectors[i].compressedData, + compressedTestVectors[i].compressedSz, + (word32)XSTRLEN( + (const char*)compressedTestVectors[i].uncompressedData), + compressedTestVectors[i].alg), 0); + ExpectIntEQ(wc_CompressionData_DeCompress(&cd), 0); + ExpectIntEQ((int)cd.uncompressedSz, (int)uncompLen); + ExpectIntEQ(XMEMCMP(cd.data, compressedTestVectors[i].uncompressedData, + cd.uncompressedSz), 0); + wc_CompressionData_Free(&cd); + } + + return EXPECT_RESULT(); +} + +static int test_wc_CompressionData_BadArgs(void) +{ + EXPECT_DECLS; + word16 badAlgId = 0xFFFF; + word32 i; + wc_CompressionData cd = {0}; + byte data[10]; + byte outBuf[10]; + word32 outBufSz = sizeof(outBuf); + XMEMSET(data, 'a', sizeof(data)); + for (i = 0; i < XELEM_CNT(algList); i ++) { + if (!wc_IsCompressionAlgSupported((word16)algList[i])) { + continue; + } + /* --- init with uncompressed data --- */ + ExpectIntNE(wc_CompressionData_InitComp(NULL, data, sizeof(data), + algList[i]), 0); + ExpectIntNE(wc_CompressionData_Compress(&cd), 0); + ExpectIntNE(wc_CompressionData_DeCompress(&cd), 0); + ExpectIntNE(wc_CompressionData_CompToBuf(&cd, outBuf, outBufSz), 0); + ExpectIntNE(wc_CompressionData_DeCompToBuf(&cd, outBuf, outBufSz), 0); + + ExpectIntNE(wc_CompressionData_InitComp(&cd, NULL, sizeof(data), + algList[i]), 0); + ExpectIntNE(wc_CompressionData_Compress(&cd), 0); + ExpectIntNE(wc_CompressionData_DeCompress(&cd), 0); + ExpectIntNE(wc_CompressionData_CompToBuf(&cd, outBuf, outBufSz), 0); + ExpectIntNE(wc_CompressionData_DeCompToBuf(&cd, outBuf, outBufSz), 0); + + ExpectIntNE(wc_CompressionData_InitComp(&cd, data, 0, + algList[i]), 0); + ExpectIntNE(wc_CompressionData_Compress(&cd), 0); + ExpectIntNE(wc_CompressionData_DeCompress(&cd), 0); + ExpectIntNE(wc_CompressionData_CompToBuf(&cd, outBuf, outBufSz), 0); + ExpectIntNE(wc_CompressionData_DeCompToBuf(&cd, outBuf, outBufSz), 0); + + ExpectIntNE(wc_CompressionData_InitComp(&cd, data, sizeof(data), + badAlgId), 0); + ExpectIntNE(wc_CompressionData_Compress(&cd), 0); + ExpectIntNE(wc_CompressionData_DeCompress(&cd), 0); + ExpectIntNE(wc_CompressionData_CompToBuf(&cd, outBuf, outBufSz), 0); + ExpectIntNE(wc_CompressionData_DeCompToBuf(&cd, outBuf, outBufSz), 0); + /* --- init with uncompressed data --- */ + + /* --- init with compressed data --- */ + ExpectIntNE(wc_CompressionData_InitDeComp(NULL, data, sizeof(data), + sizeof(data), algList[i]), 0); + ExpectIntNE(wc_CompressionData_DeCompress(&cd), 0); + ExpectIntNE(wc_CompressionData_Compress(&cd), 0); + ExpectIntNE(wc_CompressionData_DeCompToBuf(&cd, outBuf, outBufSz), 0); + ExpectIntNE(wc_CompressionData_CompToBuf(&cd, outBuf, outBufSz), 0); + + ExpectIntNE(wc_CompressionData_InitDeComp(&cd, NULL, sizeof(data), + sizeof(data), algList[i]), 0); + ExpectIntNE(wc_CompressionData_DeCompress(&cd), 0); + ExpectIntNE(wc_CompressionData_Compress(&cd), 0); + ExpectIntNE(wc_CompressionData_DeCompToBuf(&cd, outBuf, outBufSz), 0); + ExpectIntNE(wc_CompressionData_CompToBuf(&cd, outBuf, outBufSz), 0); + + ExpectIntNE(wc_CompressionData_InitDeComp(&cd, data, sizeof(data), + 0, algList[i]), 0); + ExpectIntNE(wc_CompressionData_DeCompress(&cd), 0); + ExpectIntNE(wc_CompressionData_Compress(&cd), 0); + ExpectIntNE(wc_CompressionData_DeCompToBuf(&cd, outBuf, outBufSz), 0); + ExpectIntNE(wc_CompressionData_CompToBuf(&cd, outBuf, outBufSz), 0); + + ExpectIntNE(wc_CompressionData_InitDeComp(&cd, data, sizeof(data), + sizeof(data), badAlgId), 0); + ExpectIntNE(wc_CompressionData_DeCompress(&cd), 0); + ExpectIntNE(wc_CompressionData_Compress(&cd), 0); + ExpectIntNE(wc_CompressionData_DeCompToBuf(&cd, outBuf, outBufSz), 0); + ExpectIntNE(wc_CompressionData_CompToBuf(&cd, outBuf, outBufSz), 0); + /* --- init with compressed data --- */ + + ExpectIntNE(wc_CompressionData_Compress(NULL), 0); + ExpectIntNE(wc_CompressionData_DeCompress(NULL), 0); + ExpectIntNE(wc_CompressionData_CompToBuf(NULL, outBuf, outBufSz), 0); + ExpectIntNE(wc_CompressionData_DeCompToBuf(NULL, outBuf, outBufSz), 0); + + ExpectIntEQ(wc_CompressionData_InitComp(&cd , data, sizeof(data), + WC_ZLIB), 0); + ExpectIntNE(wc_CompressionData_CompToBuf(&cd, NULL, outBufSz), 0); + ExpectIntNE(wc_CompressionData_DeCompToBuf(&cd, NULL, outBufSz), 0); + ExpectIntNE(wc_CompressionData_CompToBuf(&cd, outBuf, 0), 0); + ExpectIntNE(wc_CompressionData_DeCompToBuf(&cd, outBuf, 0), 0); + } + wc_CompressionData_Free(&cd); + return EXPECT_RESULT(); +} + +static int test_wc_CompressionData_ToBuffer(void) +{ + EXPECT_DECLS; + word32 i; + wc_CompressionData cd; + for (i = 0; i < XELEM_CNT(compressedTestVectors); i ++) { + word32 uncompLen = 0; + byte outBuf[100] = {0}; + word32 outBufSz = sizeof(outBuf); + word16 alg = compressedTestVectors[i].alg; + if (!wc_IsCompressionAlgSupported(alg)) { + /* skip test */ + continue; + } + + uncompLen = (word32)XSTRLEN( + (const char*)compressedTestVectors[i].uncompressedData); + + ExpectIntEQ(wc_CompressionData_InitComp(&cd, + compressedTestVectors[i].uncompressedData, uncompLen, alg), 0); + /* check that compression succeeds */ + ExpectIntGE(wc_CompressionData_CompToBuf(&cd, outBuf, outBufSz),0); + wc_CompressionData_Free(&cd); + + ExpectIntEQ(wc_CompressionData_InitDeComp(&cd, + compressedTestVectors[i].compressedData, + compressedTestVectors[i].compressedSz, uncompLen, alg), 0); + ExpectIntEQ(wc_CompressionData_DeCompToBuf(&cd, outBuf, outBufSz), + (int)uncompLen); + wc_CompressionData_Free(&cd); + } + return EXPECT_RESULT(); +} + +#endif /* HAVE_LIBZ */ + +int test_wc_CompressionData(void) +{ + EXPECT_DECLS; +#ifdef HAVE_LIBZ + ExpectIntEQ(test_wc_CompressionData_RoundTrip(), TEST_SUCCESS); + ExpectIntEQ(test_wc_CompressionData_InitWithCompressedData(), + TEST_SUCCESS); + ExpectIntEQ(test_wc_CompressionData_BadArgs(), TEST_SUCCESS); + ExpectIntEQ(test_wc_CompressionData_ToBuffer(), TEST_SUCCESS); + +#endif + return EXPECT_RESULT(); +} + #ifdef TEST_TLS_COMPRESSION /* Payload big enough to fill a record. Made of one repeated byte so the diff --git a/tests/api/test_compress.h b/tests/api/test_compress.h index 8bfc8732a89..8a78b3bf275 100644 --- a/tests/api/test_compress.h +++ b/tests/api/test_compress.h @@ -25,6 +25,7 @@ #include int test_wc_CompressDecisionCoverage(void); +int test_wc_CompressionData(void); int test_wolfSSL_tls_compression(void); int test_wolfSSL_tls_compression_multi_record(void); int test_wolfSSL_tls_compression_client_hello(void); @@ -37,6 +38,7 @@ int test_wolfSSL_dtls_compression_off(void); #define TEST_COMPRESS_DECLS \ TEST_DECL_GROUP("compress", test_wc_CompressDecisionCoverage), \ + TEST_DECL_GROUP("compress", test_wc_CompressionData), \ TEST_DECL_GROUP("compress", test_wolfSSL_tls_compression), \ TEST_DECL_GROUP("compress", test_wolfSSL_tls_compression_multi_record), \ TEST_DECL_GROUP("compress", test_wolfSSL_tls_compression_client_hello), \ diff --git a/tests/api/test_tls13_cert_compression.c b/tests/api/test_tls13_cert_compression.c new file mode 100644 index 00000000000..0bc8811e2c8 --- /dev/null +++ b/tests/api/test_tls13_cert_compression.c @@ -0,0 +1,1166 @@ +/* test_tls13_cert_compression.c + * + * Copyright (C) 2006-2026 wolfSSL Inc. + * + * This file is part of wolfSSL. + * + * wolfSSL is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 3 of the License, or + * (at your option) any later version. + * + * wolfSSL is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1335, USA + */ + +#include + +#ifdef NO_INLINE +#include +#else +#define WOLFSSL_MISC_INCLUDED +#include +#endif + +#include +#include +#include +#include +#include +#include + +#if defined(WOLFSSL_TLS13) && defined(WOLFSSL_CERT_COMPRESSION) && \ + defined(HAVE_SSL_MEMIO_TESTS_DEPENDENCIES) && \ + defined(HAVE_MANUAL_MEMIO_TESTS_DEPENDENCIES) +#define TEST_TLS13_CERT_COMPRESSION +#endif + +#ifdef TEST_TLS13_CERT_COMPRESSION +static const word16 certCompZlibOnly[] = { WC_ZLIB }; + +/* Rename the compress_certificate extension in the ClientHello sitting in the + * server's input buffer to a GREASE type (RFC 8701) so the server ignores it + * and sends a plain Certificate. + * returns 1 when the extension was found and renamed, 0 otherwise. */ +static int test_cert_compression_hide_ext(struct test_memio_ctx *ctx) +{ + /* compress_certificate(27) extension as the client writes it when its + * list is set to just zlib: type, extension length, list length, alg id. */ + static const byte certCompZlibExt[] = { 0x00, 0x1b, 0x00, 0x03, + 0x02, 0x00, 0x01 }; + int i; + + for (i = 0; i + (int)sizeof(certCompZlibExt) <= ctx->s_len; i++) { + if (XMEMCMP(ctx->s_buff + i, certCompZlibExt, + sizeof(certCompZlibExt)) == 0) { + ctx->s_buff[i] = 0x0a; + ctx->s_buff[i + 1] = 0x0a; + return 1; + } + } + return 0; +} + +/* Run the client's first flight and the server's reply, and report how many + * bytes the server's flight (ServerHello .. Finished) took on the wire. + * hideExt when set the server never sees the compress_certificate extension, + * so it sends an uncompressed Certificate. */ +static int test_cert_compression_server_flight_sz(int hideExt, int *flightSz) +{ + EXPECT_DECLS; + struct test_memio_ctx testContext; + WOLFSSL_CTX *clientContext = NULL; + WOLFSSL_CTX *serverContext = NULL; + WOLFSSL *clientSSL = NULL; + WOLFSSL *serverSSL = NULL; + + XMEMSET(&testContext, 0, sizeof(testContext)); + ExpectIntEQ(test_memio_setup(&testContext, &clientContext, &serverContext, + &clientSSL, &serverSSL, + wolfTLSv1_3_client_method, + wolfTLSv1_3_server_method), + 0); + ExpectIntEQ( + wolfSSL_set_cert_compression_algs(clientSSL, certCompZlibOnly, + (int)XELEM_CNT(certCompZlibOnly)), + WOLFSSL_SUCCESS); + + /* ClientHello */ + ExpectIntEQ(wolfSSL_connect(clientSSL), WOLFSSL_FATAL_ERROR); + ExpectIntEQ(wolfSSL_get_error(clientSSL, WOLFSSL_FATAL_ERROR), + WOLFSSL_ERROR_WANT_READ); + if (hideExt) { + ExpectIntEQ(test_cert_compression_hide_ext(&testContext), 1); + } + + /* ServerHello .. Finished, then the server waits on the client */ + ExpectIntEQ(wolfSSL_accept(serverSSL), WOLFSSL_FATAL_ERROR); + ExpectIntEQ(wolfSSL_get_error(serverSSL, WOLFSSL_FATAL_ERROR), + WOLFSSL_ERROR_WANT_READ); + *flightSz = testContext.c_len; + + wolfSSL_free(clientSSL); + wolfSSL_free(serverSSL); + wolfSSL_CTX_free(clientContext); + wolfSSL_CTX_free(serverContext); + return EXPECT_RESULT(); +} + +/* Send a message each way over an established connection. */ +static int test_cert_compression_exchange(WOLFSSL *clientSSL, + WOLFSSL *serverSSL) +{ + EXPECT_DECLS; + static const char msg[] = "cert compression round trip"; + char reply[sizeof(msg)]; + + XMEMSET(reply, 0, sizeof(reply)); + ExpectIntEQ(wolfSSL_write(clientSSL, msg, (int)sizeof(msg)), + (int)sizeof(msg)); + ExpectIntEQ(wolfSSL_read(serverSSL, reply, (int)sizeof(reply)), + (int)sizeof(msg)); + ExpectIntEQ(XMEMCMP(reply, msg, sizeof(msg)), 0); + + XMEMSET(reply, 0, sizeof(reply)); + ExpectIntEQ(wolfSSL_write(serverSSL, msg, (int)sizeof(msg)), + (int)sizeof(msg)); + ExpectIntEQ(wolfSSL_read(clientSSL, reply, (int)sizeof(reply)), + (int)sizeof(msg)); + ExpectIntEQ(XMEMCMP(reply, msg, sizeof(msg)), 0); + return EXPECT_RESULT(); +} + +#ifdef HAVE_MAX_FRAGMENT +/* --- WANT_WRITE resumption harness ----------------------------------------- + * + * test_memio's own simulate_want_write is all-or-nothing, so a counting send + * callback is layered over test_memio_write_cb instead: write number cc_ww_at + * fails with WANT_WRITE once and every other write goes through. Sweeping + * cc_ww_at across the whole flight interrupts each record in turn, including + * the ones in the middle of a fragmented CompressedCertificate. */ +static int cc_ww_at = -1; +static int cc_ww_n = 0; + +static int test_cert_compression_send_cb(WOLFSSL *ssl, char *buf, int sz, + void *ctx) +{ + if (cc_ww_n++ == cc_ww_at) + return WOLFSSL_CBIO_ERR_WANT_WRITE; + return test_memio_write_cb(ssl, buf, sz, ctx); +} + +/* Compressed size of the certificates an SSL object would send. */ +static int test_cert_compression_chain_comp_sz(WOLFSSL *ssl, word32 *compSz) +{ + EXPECT_DECLS; + wc_CompressionData cd; + byte *raw = NULL; + word32 rawSz = 0; + word32 leafSz = 0; + + XMEMSET(&cd, 0, sizeof(cd)); + *compSz = 0; + ExpectNotNull(ssl->buffers.certificate); + if (EXPECT_SUCCESS()) { + leafSz = ssl->buffers.certificate->length; + rawSz = leafSz; + if (ssl->buffers.certChain != NULL) + rawSz += ssl->buffers.certChain->length; + } + ExpectNotNull(raw = (byte *)XMALLOC(rawSz, NULL, DYNAMIC_TYPE_TMP_BUFFER)); + if (EXPECT_SUCCESS()) { + XMEMCPY(raw, ssl->buffers.certificate->buffer, leafSz); + if (ssl->buffers.certChain != NULL) { + XMEMCPY(raw + leafSz, ssl->buffers.certChain->buffer, + ssl->buffers.certChain->length); + } + } + ExpectIntEQ(wc_CompressionData_InitComp(&cd, raw, rawSz, WC_ZLIB), 0); + ExpectIntEQ(wc_CompressionData_Compress(&cd), 0); + if (EXPECT_SUCCESS()) + *compSz = cd.compressedSz; + wc_CompressionData_Free(&cd); + XFREE(raw, NULL, DYNAMIC_TYPE_TMP_BUFFER); + return EXPECT_RESULT(); +} + +/* One handshake with write number 'at' of 'side' (0 server, 1 client) + * interrupted by WANT_WRITE. The server presents a chain and, for mutual + * auth, the client does too; max_fragment_length 2^9 makes the compressed + * certificates span several records so the fragOffset != 0 path runs. + * writes set to the number of writes 'side' made, so the caller knows when + * the sweep has covered the whole flight. */ +static int test_cert_compression_frag_round(method_provider cm, + method_provider sm, int mutual, + int side, int at, int *writes) +{ + EXPECT_DECLS; + struct test_memio_ctx testContext; + WOLFSSL_CTX *clientContext = NULL; + WOLFSSL_CTX *serverContext = NULL; + WOLFSSL *clientSSL = NULL; + WOLFSSL *serverSSL = NULL; + word32 compSz = 0; + + XMEMSET(&testContext, 0, sizeof(testContext)); + ExpectIntEQ(test_memio_setup(&testContext, &clientContext, &serverContext, + &clientSSL, &serverSSL, cm, sm), + 0); + ExpectIntEQ(wolfSSL_use_certificate_chain_file(serverSSL, svrCertFile), + WOLFSSL_SUCCESS); + ExpectIntEQ(test_cert_compression_chain_comp_sz(serverSSL, &compSz), + TEST_SUCCESS); + /* the compressed chain must not fit one 2^9 record, or the + * fragmentation path is never reached */ + ExpectIntGT(compSz, 512); + if (mutual) { + ExpectIntEQ( + wolfSSL_CTX_load_verify_locations(serverContext, cliCertFile, NULL), + WOLFSSL_SUCCESS); + wolfSSL_set_verify( + serverSSL, + WOLFSSL_VERIFY_PEER | WOLFSSL_VERIFY_FAIL_IF_NO_PEER_CERT, NULL); + ExpectIntEQ(wolfSSL_use_certificate_chain_file(clientSSL, cliCertFile), + WOLFSSL_SUCCESS); + ExpectIntEQ(wolfSSL_use_PrivateKey_file(clientSSL, cliKeyFile, + WOLFSSL_FILETYPE_PEM), + WOLFSSL_SUCCESS); + } + ExpectIntEQ(wolfSSL_UseMaxFragment(clientSSL, WOLFSSL_MFL_2_9), + WOLFSSL_SUCCESS); + wolfSSL_SSLSetIOSend(side == 0 ? serverSSL : clientSSL, + test_cert_compression_send_cb); + + cc_ww_at = at; + cc_ww_n = 0; + ExpectIntEQ(test_memio_do_handshake(clientSSL, serverSSL, 64, NULL), 0); + *writes = cc_ww_n; + cc_ww_at = -1; + /* nothing left over from the compressed send on either side */ + ExpectNull(serverSSL == NULL ? NULL : serverSSL->compressedCert); + ExpectNull(clientSSL == NULL ? NULL : clientSSL->compressedCert); + ExpectIntEQ(test_cert_compression_exchange(clientSSL, serverSSL), + TEST_SUCCESS); + + wolfSSL_free(clientSSL); + wolfSSL_free(serverSSL); + wolfSSL_CTX_free(clientContext); + wolfSSL_CTX_free(serverContext); + return EXPECT_RESULT(); +} + +/* Sweep a WANT_WRITE over every write 'side' makes during the handshake, + * starting with one uninterrupted run to learn how many writes there are. */ +static int test_cert_compression_frag_sweep(method_provider cm, + method_provider sm, int mutual, + int side) +{ + EXPECT_DECLS; + int writes = 0; + int n = 0; + int at; + + ExpectIntEQ( + test_cert_compression_frag_round(cm, sm, mutual, side, -1, &writes), + TEST_SUCCESS); + for (at = 0; at < writes && EXPECT_SUCCESS(); at++) { + ExpectIntEQ( + test_cert_compression_frag_round(cm, sm, mutual, side, at, &n), + TEST_SUCCESS); + } + return EXPECT_RESULT(); +} +#endif /* HAVE_MAX_FRAGMENT */ +#endif /* TEST_TLS13_CERT_COMPRESSION */ + +/* Full TLS 1.3 handshakes with RFC 8879 certificate compression: + * - server auth: the server's Certificate goes out as a + * CompressedCertificate and the client decompresses and verifies it; + * - mutual auth: the CertificateRequest carries the extension so the + * client's Certificate is compressed too and the server verifies it; + * - the server's flight is smaller when the client offers compression than + * when the server never sees the offer, proving compression was used. */ +int test_tls13_cert_compression_roundTrip(void) +{ + EXPECT_DECLS; +#ifdef TEST_TLS13_CERT_COMPRESSION + struct test_memio_ctx testContext; + WOLFSSL_CTX *clientContext = NULL; + WOLFSSL_CTX *serverContext = NULL; + WOLFSSL *clientSSL = NULL; + WOLFSSL *serverSSL = NULL; + int compressedSz = 0; + int uncompressedSz = 0; + + /* --- server auth, default alg list --- */ + XMEMSET(&testContext, 0, sizeof(testContext)); + ExpectIntEQ(test_memio_setup(&testContext, &clientContext, &serverContext, + &clientSSL, &serverSSL, + wolfTLSv1_3_client_method, + wolfTLSv1_3_server_method), + 0); + ExpectIntEQ(test_memio_do_handshake(clientSSL, serverSSL, 10, NULL), 0); + ExpectIntEQ(test_cert_compression_exchange(clientSSL, serverSSL), + TEST_SUCCESS); + + wolfSSL_free(clientSSL); + clientSSL = NULL; + wolfSSL_free(serverSSL); + serverSSL = NULL; + wolfSSL_CTX_free(clientContext); + clientContext = NULL; + wolfSSL_CTX_free(serverContext); + serverContext = NULL; + + /* --- mutual auth, both certificates compressed --- */ + XMEMSET(&testContext, 0, sizeof(testContext)); + ExpectIntEQ(test_memio_setup(&testContext, &clientContext, &serverContext, + &clientSSL, &serverSSL, + wolfTLSv1_3_client_method, + wolfTLSv1_3_server_method), + 0); + /* client-cert.pem is self signed so it is its own CA */ + ExpectIntEQ( + wolfSSL_CTX_load_verify_locations(serverContext, cliCertFile, NULL), + WOLFSSL_SUCCESS); + wolfSSL_set_verify( + serverSSL, WOLFSSL_VERIFY_PEER | WOLFSSL_VERIFY_FAIL_IF_NO_PEER_CERT, + NULL); + ExpectIntEQ(wolfSSL_use_certificate_file(clientSSL, cliCertFile, + WOLFSSL_FILETYPE_PEM), + WOLFSSL_SUCCESS); + ExpectIntEQ(wolfSSL_use_PrivateKey_file(clientSSL, cliKeyFile, + WOLFSSL_FILETYPE_PEM), + WOLFSSL_SUCCESS); + ExpectIntEQ(test_memio_do_handshake(clientSSL, serverSSL, 10, NULL), 0); + ExpectIntEQ(test_cert_compression_exchange(clientSSL, serverSSL), + TEST_SUCCESS); + + wolfSSL_free(clientSSL); + clientSSL = NULL; + wolfSSL_free(serverSSL); + serverSSL = NULL; + wolfSSL_CTX_free(clientContext); + clientContext = NULL; + wolfSSL_CTX_free(serverContext); + serverContext = NULL; + + /* --- compression actually shrank the server's flight --- */ + ExpectIntEQ(test_cert_compression_server_flight_sz(0, &compressedSz), + TEST_SUCCESS); + ExpectIntEQ(test_cert_compression_server_flight_sz(1, &uncompressedSz), + TEST_SUCCESS); + ExpectIntGT(compressedSz, 0); + ExpectIntLT(compressedSz, uncompressedSz); +#endif /* TEST_TLS13_CERT_COMPRESSION */ + return EXPECT_RESULT(); +} + +int test_tls13_cert_compression_turnoff(void) +{ + EXPECT_DECLS; +#ifdef TEST_TLS13_CERT_COMPRESSION + struct test_memio_ctx testContext; + WOLFSSL_CTX *clientContext = NULL; + WOLFSSL_CTX *serverContext = NULL; + WOLFSSL *clientSSL = NULL; + WOLFSSL *serverSSL = NULL; + + /* --- server auth, default alg list --- */ + XMEMSET(&testContext, 0, sizeof(testContext)); + ExpectIntEQ(test_memio_setup(&testContext, &clientContext, &serverContext, + &clientSSL, &serverSSL, + wolfTLSv1_3_client_method, + wolfTLSv1_3_server_method), + 0); + + /* turn off cert compression */ + ExpectIntEQ(wolfSSL_set_cert_compression_algs(clientSSL, NULL, 0), + WOLFSSL_SUCCESS); + + /* ClientHello */ + ExpectIntEQ(wolfSSL_connect(clientSSL), WOLFSSL_FATAL_ERROR); + ExpectIntEQ(wolfSSL_get_error(clientSSL, WOLFSSL_FATAL_ERROR), + WOLFSSL_ERROR_WANT_READ); + + /* check if clientSSL has the compress_certificate extension */ + ExpectNull(TLSX_Find(clientSSL->extensions, TLSX_CERT_COMPRESSION)); + + wolfSSL_free(clientSSL); + clientSSL = NULL; + wolfSSL_free(serverSSL); + serverSSL = NULL; + wolfSSL_CTX_free(clientContext); + clientContext = NULL; + wolfSSL_CTX_free(serverContext); + serverContext = NULL; + + /* --- turned off on the CTX: inherited by objects created from it, even + * when the CTX also holds an alg list --- */ + XMEMSET(&testContext, 0, sizeof(testContext)); + ExpectIntEQ(test_memio_setup(&testContext, &clientContext, &serverContext, + NULL, NULL, wolfTLSv1_3_client_method, + wolfTLSv1_3_server_method), + 0); + ExpectIntEQ( + wolfSSL_CTX_set_cert_compression_algs(clientContext, certCompZlibOnly, + (int)XELEM_CNT(certCompZlibOnly)), + WOLFSSL_SUCCESS); + ExpectIntEQ(wolfSSL_CTX_set_cert_compression_algs(clientContext, NULL, 0), + WOLFSSL_SUCCESS); + ExpectIntEQ(test_memio_setup(&testContext, &clientContext, &serverContext, + &clientSSL, &serverSSL, + wolfTLSv1_3_client_method, + wolfTLSv1_3_server_method), + 0); + + ExpectIntEQ(wolfSSL_connect(clientSSL), WOLFSSL_FATAL_ERROR); + ExpectIntEQ(wolfSSL_get_error(clientSSL, WOLFSSL_FATAL_ERROR), + WOLFSSL_ERROR_WANT_READ); + ExpectNull(TLSX_Find(clientSSL->extensions, TLSX_CERT_COMPRESSION)); + + wolfSSL_free(clientSSL); + clientSSL = NULL; + wolfSSL_free(serverSSL); + serverSSL = NULL; + + /* --- re-enabled on the CTX after turning it off --- */ + XMEMSET(&testContext, 0, sizeof(testContext)); + ExpectIntEQ( + wolfSSL_CTX_set_cert_compression_algs(clientContext, certCompZlibOnly, + (int)XELEM_CNT(certCompZlibOnly)), + WOLFSSL_SUCCESS); + ExpectIntEQ(test_memio_setup(&testContext, &clientContext, &serverContext, + &clientSSL, &serverSSL, + wolfTLSv1_3_client_method, + wolfTLSv1_3_server_method), + 0); + + ExpectIntEQ(wolfSSL_connect(clientSSL), WOLFSSL_FATAL_ERROR); + ExpectIntEQ(wolfSSL_get_error(clientSSL, WOLFSSL_FATAL_ERROR), + WOLFSSL_ERROR_WANT_READ); + ExpectNotNull(TLSX_Find(clientSSL->extensions, TLSX_CERT_COMPRESSION)); + + wolfSSL_free(clientSSL); + clientSSL = NULL; + wolfSSL_free(serverSSL); + serverSSL = NULL; + + /* --- re-enabled on the SSL after turning it off --- */ + XMEMSET(&testContext, 0, sizeof(testContext)); + ExpectIntEQ(test_memio_setup(&testContext, &clientContext, &serverContext, + &clientSSL, NULL, wolfTLSv1_3_client_method, + wolfTLSv1_3_server_method), + 0); + ExpectIntEQ(wolfSSL_set_cert_compression_algs(clientSSL, NULL, 0), + WOLFSSL_SUCCESS); + ExpectIntEQ( + wolfSSL_set_cert_compression_algs(clientSSL, certCompZlibOnly, + (int)XELEM_CNT(certCompZlibOnly)), + WOLFSSL_SUCCESS); + + ExpectIntEQ(wolfSSL_connect(clientSSL), WOLFSSL_FATAL_ERROR); + ExpectIntEQ(wolfSSL_get_error(clientSSL, WOLFSSL_FATAL_ERROR), + WOLFSSL_ERROR_WANT_READ); + ExpectNotNull(TLSX_Find(clientSSL->extensions, TLSX_CERT_COMPRESSION)); + + wolfSSL_free(clientSSL); + wolfSSL_free(serverSSL); + wolfSSL_CTX_free(clientContext); + wolfSSL_CTX_free(serverContext); +#endif /* TEST_TLS13_CERT_COMPRESSION */ + return EXPECT_RESULT(); +} + +/* RFC 8879 CompressedCertificate larger than one record: max_fragment_length + * 2^9 with a certificate chain on each side, and a WANT_WRITE injected at + * every write of the sender so each fragment boundary is resumed from. */ +int test_tls13_cert_compression_fragment(void) +{ + EXPECT_DECLS; +#if defined(TEST_TLS13_CERT_COMPRESSION) && defined(HAVE_MAX_FRAGMENT) + /* server's compressed Certificate */ + ExpectIntEQ(test_cert_compression_frag_sweep( + wolfTLSv1_3_client_method, wolfTLSv1_3_server_method, 0, 0), + TEST_SUCCESS); + /* client's compressed Certificate */ + ExpectIntEQ(test_cert_compression_frag_sweep( + wolfTLSv1_3_client_method, wolfTLSv1_3_server_method, 1, 1), + TEST_SUCCESS); +#endif + return EXPECT_RESULT(); +} + +/* The DTLS 1.3 path hands the whole CompressedCertificate to + * Dtls13HandshakeSend, which does its own fragmentation. Run it once plainly, + * then with max_fragment_length and a WANT_WRITE sweep on each side. */ +int test_tls13_cert_compression_dtls13(void) +{ + EXPECT_DECLS; +#if defined(TEST_TLS13_CERT_COMPRESSION) && defined(WOLFSSL_DTLS13) + struct test_memio_ctx testContext; + WOLFSSL_CTX *clientContext = NULL; + WOLFSSL_CTX *serverContext = NULL; + WOLFSSL *clientSSL = NULL; + WOLFSSL *serverSSL = NULL; + + XMEMSET(&testContext, 0, sizeof(testContext)); + ExpectIntEQ(test_memio_setup(&testContext, &clientContext, &serverContext, + &clientSSL, &serverSSL, + wolfDTLSv1_3_client_method, + wolfDTLSv1_3_server_method), + 0); + ExpectIntEQ( + wolfSSL_CTX_load_verify_locations(serverContext, cliCertFile, NULL), + WOLFSSL_SUCCESS); + wolfSSL_set_verify( + serverSSL, WOLFSSL_VERIFY_PEER | WOLFSSL_VERIFY_FAIL_IF_NO_PEER_CERT, + NULL); + ExpectIntEQ(wolfSSL_use_certificate_file(clientSSL, cliCertFile, + WOLFSSL_FILETYPE_PEM), + WOLFSSL_SUCCESS); + ExpectIntEQ(wolfSSL_use_PrivateKey_file(clientSSL, cliKeyFile, + WOLFSSL_FILETYPE_PEM), + WOLFSSL_SUCCESS); + ExpectIntEQ(test_memio_do_handshake(clientSSL, serverSSL, 10, NULL), 0); + ExpectNull(serverSSL == NULL ? NULL : serverSSL->compressedCert); + ExpectNull(clientSSL == NULL ? NULL : clientSSL->compressedCert); + ExpectIntEQ(test_cert_compression_exchange(clientSSL, serverSSL), + TEST_SUCCESS); + + wolfSSL_free(clientSSL); + wolfSSL_free(serverSSL); + wolfSSL_CTX_free(clientContext); + wolfSSL_CTX_free(serverContext); + +#ifdef HAVE_MAX_FRAGMENT + ExpectIntEQ(test_cert_compression_frag_sweep(wolfDTLSv1_3_client_method, + wolfDTLSv1_3_server_method, 0, + 0), + TEST_SUCCESS); + ExpectIntEQ(test_cert_compression_frag_sweep(wolfDTLSv1_3_client_method, + wolfDTLSv1_3_server_method, 1, + 1), + TEST_SUCCESS); +#endif +#endif + return EXPECT_RESULT(); +} + +#if defined(TEST_TLS13_CERT_COMPRESSION) && defined(WOLFSSL_POST_HANDSHAKE_AUTH) +/* Replace the algorithm list of the server's compress_certificate extension + * with an ID nobody implements, so the client falls back to a plain + * Certificate. Used to get an uncompressed baseline for the same flight. */ +static int test_cert_compression_poison_offer(WOLFSSL *ssl) +{ + TLSX *ext; + + for (ext = ssl->extensions; ext != NULL; ext = ext->next) { + if (ext->type == TLSX_CERT_COMPRESSION && ext->data != NULL) { + byte *data = (byte *)ext->data; + /* */ + data[1] = 0xff; + data[2] = 0xfe; + return 1; + } + } + return 0; +} + +/* Post-handshake auth: report the size of the client's reply to the + * server's CertificateRequest (CompressedCertificate or Certificate, + * CertificateVerify, Finished) and check the server accepted it. */ +static int test_cert_compression_pha_flight_sz(int poison, int *flightSz) +{ + EXPECT_DECLS; + struct test_memio_ctx testContext; + WOLFSSL_CTX *clientContext = NULL; + WOLFSSL_CTX *serverContext = NULL; + WOLFSSL *clientSSL = NULL; + WOLFSSL *serverSSL = NULL; +#ifdef KEEP_PEER_CERT + WOLFSSL_X509 *peer = NULL; +#endif + char buf[16]; + + XMEMSET(&testContext, 0, sizeof(testContext)); + /* The client's certificate goes on the CTX: one set on the SSL is + * unloaded at the end of the handshake, before the request arrives. The + * pre-made CTX makes test_memio_setup skip its CA load and IO setup. */ + ExpectNotNull(clientContext = wolfSSL_CTX_new(wolfTLSv1_3_client_method())); + ExpectIntEQ( + wolfSSL_CTX_load_verify_locations(clientContext, caCertFile, NULL), + WOLFSSL_SUCCESS); + ExpectIntEQ(wolfSSL_CTX_use_certificate_file(clientContext, cliCertFile, + WOLFSSL_FILETYPE_PEM), + WOLFSSL_SUCCESS); + ExpectIntEQ(wolfSSL_CTX_use_PrivateKey_file(clientContext, cliKeyFile, + WOLFSSL_FILETYPE_PEM), + WOLFSSL_SUCCESS); + wolfSSL_SetIORecv(clientContext, test_memio_read_cb); + wolfSSL_SetIOSend(clientContext, test_memio_write_cb); + ExpectIntEQ(test_memio_setup(&testContext, &clientContext, &serverContext, + &clientSSL, &serverSSL, + wolfTLSv1_3_client_method, + wolfTLSv1_3_server_method), + 0); + ExpectIntEQ( + wolfSSL_CTX_load_verify_locations(serverContext, cliCertFile, NULL), + WOLFSSL_SUCCESS); + wolfSSL_set_verify( + serverSSL, WOLFSSL_VERIFY_PEER | WOLFSSL_VERIFY_POST_HANDSHAKE, NULL); + ExpectIntEQ(wolfSSL_allow_post_handshake_auth(clientSSL), 0); + ExpectIntEQ(test_memio_do_handshake(clientSSL, serverSSL, 10, NULL), 0); +#ifdef KEEP_PEER_CERT + ExpectNull(wolfSSL_get_peer_certificate(serverSSL)); +#endif + + if (poison && EXPECT_SUCCESS()) { + /* The server only adds compress_certificate when it sends a + * CertificateRequest, so add it now to have an offer to poison. The + * request reuses the existing entry. */ + ExpectIntEQ(TLSX_UseCertCompression(serverSSL, serverSSL->heap), 0); + ExpectIntEQ(test_cert_compression_poison_offer(serverSSL), 1); + } + + /* OPENSSL_COMPATIBLE_DEFAULTS turns on message grouping, which would + * hold the CertificateRequest until the server's next write. */ + ExpectIntEQ(wolfSSL_clear_group_messages(serverSSL), WOLFSSL_SUCCESS); + ExpectIntEQ(wolfSSL_request_certificate(serverSSL), WOLFSSL_SUCCESS); + testContext.s_len = 0; + /* client reads the CertificateRequest and answers it */ + ExpectIntEQ(wolfSSL_read(clientSSL, buf, sizeof(buf)), WOLFSSL_FATAL_ERROR); + ExpectIntEQ(wolfSSL_get_error(clientSSL, WOLFSSL_FATAL_ERROR), + WOLFSSL_ERROR_WANT_READ); + *flightSz = testContext.s_len; + ExpectNull(clientSSL == NULL ? NULL : clientSSL->compressedCert); + /* server reads and verifies the answer */ + ExpectIntEQ(wolfSSL_read(serverSSL, buf, sizeof(buf)), WOLFSSL_FATAL_ERROR); + ExpectIntEQ(wolfSSL_get_error(serverSSL, WOLFSSL_FATAL_ERROR), + WOLFSSL_ERROR_WANT_READ); +#ifdef KEEP_PEER_CERT + /* a reference the caller frees when OPENSSL_EXTRA is defined */ + ExpectNotNull(peer = wolfSSL_get_peer_certificate(serverSSL)); + wolfSSL_X509_free(peer); +#endif + ExpectIntEQ(test_cert_compression_exchange(clientSSL, serverSSL), + TEST_SUCCESS); + + wolfSSL_free(clientSSL); + wolfSSL_free(serverSSL); + wolfSSL_CTX_free(clientContext); + wolfSSL_CTX_free(serverContext); + return EXPECT_RESULT(); +} +#endif + +/* Post-handshake auth: the CertificateRequest the server sends after the + * handshake carries compress_certificate, so the client answers with a + * CompressedCertificate. It is smaller than the plain Certificate the client + * sends when the offer is changed to an unknown algorithm. */ +int test_tls13_cert_compression_pha(void) +{ + EXPECT_DECLS; +#if defined(TEST_TLS13_CERT_COMPRESSION) && defined(WOLFSSL_POST_HANDSHAKE_AUTH) + int compressedSz = 0; + int uncompressedSz = 0; + + ExpectIntEQ(test_cert_compression_pha_flight_sz(0, &compressedSz), + TEST_SUCCESS); + ExpectIntEQ(test_cert_compression_pha_flight_sz(1, &uncompressedSz), + TEST_SUCCESS); + ExpectIntGT(compressedSz, 0); + ExpectIntLT(compressedSz, uncompressedSz); +#endif + return EXPECT_RESULT(); +} + +#ifdef TEST_TLS13_CERT_COMPRESSION +/* Payload for a CompressedCertificate that decompresses cleanly, so every + * rejection below comes from the field under test. Its content is never + * parsed as a Certificate. It has to be compressible: the compressor gives + * up when the output would not be smaller than the input. */ +static byte cc_payload[64]; + +/* Build alg(2) | uncompressed_length(3) | compressed_length(3) | body. */ +static word32 test_cert_compression_build(byte *out, word16 alg, + word32 uncompSz, word32 compSz, + const byte *body, word32 bodySz) +{ + c16toa(alg, out); + c32to24(uncompSz, out + OPAQUE16_LEN); + c32to24(compSz, out + OPAQUE16_LEN + OPAQUE24_LEN); + XMEMCPY(out + COMP_CERT_HEADER_SZ, body, bodySz); + return COMP_CERT_HEADER_SZ + bodySz; +} + +/* Feed one CompressedCertificate body to a fresh client and check the error + * it returns, that nothing is leaked in ssl->compressedCert and, when + * expAlert >= 0, that a fatal alert with that description was sent. */ +static int test_cert_compression_reject(byte *msg, word32 msgSz, int expErr, + int expAlert, int noRequest) +{ + EXPECT_DECLS; + struct test_memio_ctx testContext; + WOLFSSL_CTX *clientContext = NULL; + WOLFSSL_CTX *serverContext = NULL; + WOLFSSL *clientSSL = NULL; + WOLFSSL *serverSSL = NULL; + word32 idx = 0; + + XMEMSET(&testContext, 0, sizeof(testContext)); + ExpectIntEQ(test_memio_setup(&testContext, &clientContext, &serverContext, + &clientSSL, &serverSSL, + wolfTLSv1_3_client_method, + wolfTLSv1_3_server_method), + 0); + if (!noRequest) { + ExpectIntEQ(TLSX_UseCertCompression(clientSSL, clientSSL->heap), 0); + } + if (EXPECT_SUCCESS()) { + ExpectIntEQ(DoTls13CompressedCertificate(clientSSL, msg, &idx, msgSz), + expErr); + ExpectIntEQ(idx, 0); + ExpectNull(clientSSL->compressedCert); + } + if (expAlert >= 0) { + /* level and description are the last two bytes of the record */ + ExpectIntGE(testContext.s_len, RECORD_HEADER_SZ + ALERT_SIZE); + if (EXPECT_SUCCESS()) { + ExpectIntEQ(testContext.s_buff[0], alert); + ExpectIntEQ(testContext.s_buff[testContext.s_len - 2], alert_fatal); + ExpectIntEQ(testContext.s_buff[testContext.s_len - 1], expAlert); + } + } + else { + ExpectIntEQ(testContext.s_len, 0); + } + + wolfSSL_free(clientSSL); + wolfSSL_free(serverSSL); + wolfSSL_CTX_free(clientContext); + wolfSSL_CTX_free(serverContext); + return EXPECT_RESULT(); +} +#endif /* TEST_TLS13_CERT_COMPRESSION */ + +/* Each field of a CompressedCertificate that DoTls13CompressedCertificate + * checks, broken one at a time. The message is encrypted on the wire, so it + * is handed to the parser directly rather than edited in the memio buffer. */ +int test_tls13_cert_compression_malformed(void) +{ + EXPECT_DECLS; +#ifdef TEST_TLS13_CERT_COMPRESSION + wc_CompressionData cd; + byte msg[COMP_CERT_HEADER_SZ + 64]; + byte body[64]; + word32 compSz = 0; + word32 uncompSz = (word32)sizeof(cc_payload); + word32 msgSz; + + XMEMSET(&cd, 0, sizeof(cd)); + ExpectIntEQ(wc_CompressionData_InitComp(&cd, cc_payload, uncompSz, WC_ZLIB), + 0); + ExpectIntEQ(wc_CompressionData_Compress(&cd), 0); + ExpectIntLE(cd.compressedSz, sizeof(body)); + if (EXPECT_SUCCESS()) { + compSz = cd.compressedSz; + XMEMCPY(body, cd.data, compSz); + } + wc_CompressionData_Free(&cd); + + /* shorter than the fixed header */ + msgSz = test_cert_compression_build(msg, WC_ZLIB, uncompSz, compSz, body, + compSz); + ExpectIntEQ(test_cert_compression_reject(msg, COMP_CERT_HEADER_SZ - 1, + BUFFER_ERROR, -1, 0), + TEST_SUCCESS); + + /* compressed_length larger / smaller than the bytes that follow */ + msgSz = test_cert_compression_build(msg, WC_ZLIB, uncompSz, compSz + 1, + body, compSz); + ExpectIntEQ(test_cert_compression_reject(msg, msgSz, BUFFER_ERROR, -1, 0), + TEST_SUCCESS); + msgSz = test_cert_compression_build(msg, WC_ZLIB, uncompSz, compSz - 1, + body, compSz); + ExpectIntEQ(test_cert_compression_reject(msg, msgSz, BUFFER_ERROR, -1, 0), + TEST_SUCCESS); + + /* compressed_length of 0 */ + msgSz = test_cert_compression_build(msg, WC_ZLIB, uncompSz, 0, body, 0); + ExpectIntEQ(test_cert_compression_reject(msg, msgSz, BUFFER_ERROR, -1, 0), + TEST_SUCCESS); + + /* algorithm IDs that were never offered: brotli, zstd, unassigned */ + msgSz = test_cert_compression_build(msg, 2, uncompSz, compSz, body, compSz); + ExpectIntEQ(test_cert_compression_reject(msg, msgSz, DECOMPRESS_E, + illegal_parameter, 0), + TEST_SUCCESS); + msgSz = test_cert_compression_build(msg, 3, uncompSz, compSz, body, compSz); + ExpectIntEQ(test_cert_compression_reject(msg, msgSz, DECOMPRESS_E, + illegal_parameter, 0), + TEST_SUCCESS); + msgSz = test_cert_compression_build(msg, 0xffff, uncompSz, compSz, body, + compSz); + ExpectIntEQ(test_cert_compression_reject(msg, msgSz, DECOMPRESS_E, + illegal_parameter, 0),TEST_SUCCESS); + /* 0 is "no compression", which is not a valid algorithm to receive */ + msgSz = test_cert_compression_build(msg, WC_NO_COMPRESSION, uncompSz, + compSz, body, compSz); + ExpectIntEQ(test_cert_compression_reject(msg, msgSz, DECOMPRESS_E, + illegal_parameter, 0), TEST_SUCCESS); + + /* uncompressed_length of 0 and above MAX_CERTIFICATE_SZ + room */ + msgSz = test_cert_compression_build(msg, WC_ZLIB, 0, compSz, body, compSz); + ExpectIntEQ(test_cert_compression_reject(msg, msgSz, DECOMPRESS_E, + bad_certificate, 0), + TEST_SUCCESS); + if (MAX_CERTIFICATE_SZ < 0xffffff) { + msgSz = test_cert_compression_build( + msg, WC_ZLIB, MAX_CERTIFICATE_SZ + 300, compSz, body, compSz); + ExpectIntEQ(test_cert_compression_reject(msg, msgSz, DECOMPRESS_E, + bad_certificate, 0), + TEST_SUCCESS); + } + + /* uncompressed_length one short of / one past the real output */ + msgSz = test_cert_compression_build(msg, WC_ZLIB, uncompSz - 1, compSz, + body, compSz); + ExpectIntEQ(test_cert_compression_reject(msg, msgSz, DECOMPRESS_E, + bad_certificate, 0), + TEST_SUCCESS); + msgSz = test_cert_compression_build(msg, WC_ZLIB, uncompSz + 1, compSz, + body, compSz); + ExpectIntEQ(test_cert_compression_reject(msg, msgSz, DECOMPRESS_E, + bad_certificate, 0), + TEST_SUCCESS); + + /* corrupt zlib stream: bad header byte, bad adler32, truncated stream */ + msgSz = test_cert_compression_build(msg, WC_ZLIB, uncompSz, compSz, body, + compSz); + msg[COMP_CERT_HEADER_SZ] ^= 0xff; + ExpectIntEQ(test_cert_compression_reject(msg, msgSz, DECOMPRESS_E, + bad_certificate, 0), + TEST_SUCCESS); + msgSz = test_cert_compression_build(msg, WC_ZLIB, uncompSz, compSz, body, + compSz); + msg[msgSz - 1] ^= 0x01; + ExpectIntEQ(test_cert_compression_reject(msg, msgSz, DECOMPRESS_E, + bad_certificate, 0), + TEST_SUCCESS); + msgSz = test_cert_compression_build(msg, WC_ZLIB, uncompSz, compSz - 4, + body, compSz - 4); + ExpectIntEQ(test_cert_compression_reject(msg, msgSz, DECOMPRESS_E, + bad_certificate, 0), + TEST_SUCCESS); + + /* check that unrequested compressed cert is not processed */ + msgSz = test_cert_compression_build(msg, WC_ZLIB, uncompSz, compSz, body, + compSz); + ExpectIntEQ(test_cert_compression_reject(msg, msgSz, DECOMPRESS_E, + illegal_parameter, 1), + TEST_SUCCESS); +#endif + return EXPECT_RESULT(); +} + + +#if defined(TEST_TLS13_CERT_COMPRESSION) && defined(USE_WOLFSSL_MEMORY) && \ + !defined(WOLFSSL_STATIC_MEMORY) && !defined(WOLFSSL_DEBUG_MEMORY) && \ + defined(HAVE_LIBZ) && !defined(WOLFSSL_TRACK_MEMORY) +/* An allocator that fails every request of at least CC_FAIL_MIN_SZ bytes + * while armed. zlib gets its memory through XMALLOC and its deflate state is + * made of 64KB blocks, far above anything the handshake itself allocates, so + * only the compression fails. */ +#define CC_FAIL_MIN_SZ (32 * 1024) +static int cc_fail_armed = 0; +static int cc_fail_hits = 0; + +static void *test_cert_compression_fail_malloc(size_t size) +{ + if (cc_fail_armed && size >= CC_FAIL_MIN_SZ) { + cc_fail_hits++; + return NULL; + } + return malloc(size); +} + +static void test_cert_compression_fail_free(void *ptr) +{ + free(ptr); +} + +static void *test_cert_compression_fail_realloc(void *ptr, size_t size) +{ + if (cc_fail_armed && size >= CC_FAIL_MIN_SZ) { + cc_fail_hits++; + return NULL; + } + return realloc(ptr, size); +} + +#if defined(HAVE_OCSP) && defined(HAVE_CERTIFICATE_STATUS_REQUEST) && \ + !defined(NO_RSA) && !defined(NO_SHA) +#define TEST_CERT_COMPRESSION_STAPLE +/* Good OCSP response for certs/ocsp/server1-cert.pem, handed to the server by + * its OCSP IO callback so it has something to staple. */ +static byte cc_staple_resp[4096]; +static int cc_staple_respSz = 0; +static int cc_staple_calls = 0; + +static int test_cert_compression_staple_io_cb(void *ioCtx, const char *url, + int urlSz, unsigned char *req, + int reqSz, + unsigned char **respBuf) +{ + (void)ioCtx; + (void)url; + (void)urlSz; + (void)req; + (void)reqSz; + + cc_staple_calls++; + /* static buffer: the free callback registered with this one is NULL */ + *respBuf = cc_staple_resp; + return cc_staple_respSz; +} + +/* TLS 1.3 handshake where the server staples an OCSP response to its leaf + * CertificateEntry and the client offers zlib and requires the staple. + * failAlloc when set, compression cannot allocate while the server writes + * its flight, so the staple has to go out in the plain Certificate. + * flightSz set to the size of the server's flight (ServerHello .. Finished). + */ +static int test_cert_compression_staple_round(int failAlloc, int *flightSz) +{ + EXPECT_DECLS; + struct test_memio_ctx testContext; + WOLFSSL_CTX *clientContext = NULL; + WOLFSSL_CTX *serverContext = NULL; + WOLFSSL *clientSSL = NULL; + WOLFSSL *serverSSL = NULL; + wolfSSL_Malloc_cb prevMalloc = NULL; + wolfSSL_Free_cb prevFree = NULL; + wolfSSL_Realloc_cb prevRealloc = NULL; + + cc_staple_calls = 0; + *flightSz = 0; + + /* The CTXs are configured before any SSL is made from them, since each + * SSL latches the certificate at wolfSSL_new(). */ + XMEMSET(&testContext, 0, sizeof(testContext)); + ExpectIntEQ(test_memio_setup(&testContext, &clientContext, &serverContext, + NULL, NULL, wolfTLSv1_3_client_method, + wolfTLSv1_3_server_method), + 0); + + /* server1 is the certificate the response answers for, sent with its + * intermediate so the client can build a path to the root */ + ExpectIntEQ(wolfSSL_CTX_use_certificate_chain_file( + serverContext, "./certs/ocsp/server1-chain-noroot.pem"), + WOLFSSL_SUCCESS); + ExpectIntEQ(wolfSSL_CTX_use_PrivateKey_file(serverContext, + "./certs/ocsp/server1-key.pem", + WOLFSSL_FILETYPE_PEM), + WOLFSSL_SUCCESS); + /* the server verifies the response before stapling it */ + ExpectIntEQ(wolfSSL_CTX_load_verify_locations( + serverContext, "./certs/ocsp/root-ca-cert.pem", NULL), + WOLFSSL_SUCCESS); + ExpectIntEQ( + wolfSSL_CTX_load_verify_locations( + serverContext, "./certs/ocsp/intermediate1-ca-cert.pem", NULL), + WOLFSSL_SUCCESS); + ExpectIntEQ(wolfSSL_CTX_EnableOCSPStapling(serverContext), WOLFSSL_SUCCESS); + /* server1 carries no AuthInfo, so point the lookup at a dummy responder */ + ExpectIntEQ( + wolfSSL_CTX_SetOCSP_OverrideURL(serverContext, "http://dummy.test"), + WOLFSSL_SUCCESS); + ExpectIntEQ( + wolfSSL_CTX_EnableOCSP(serverContext, WOLFSSL_OCSP_NO_NONCE | + WOLFSSL_OCSP_URL_OVERRIDE), + WOLFSSL_SUCCESS); + ExpectIntEQ(wolfSSL_CTX_SetOCSP_Cb(serverContext, + test_cert_compression_staple_io_cb, NULL, + NULL), + WOLFSSL_SUCCESS); + + /* the client needs the intermediate too, to verify the responder + * certificate inside the stapled response */ + ExpectIntEQ(wolfSSL_CTX_load_verify_locations( + clientContext, "./certs/ocsp/root-ca-cert.pem", NULL), + WOLFSSL_SUCCESS); + ExpectIntEQ( + wolfSSL_CTX_load_verify_locations( + clientContext, "./certs/ocsp/intermediate1-ca-cert.pem", NULL), + WOLFSSL_SUCCESS); + ExpectIntEQ(wolfSSL_CTX_EnableOCSPStapling(clientContext), WOLFSSL_SUCCESS); + /* a Certificate that lost the staple fails the handshake */ + ExpectIntEQ(wolfSSL_CTX_EnableOCSPMustStaple(clientContext), + WOLFSSL_SUCCESS); + + ExpectIntEQ(test_memio_setup(&testContext, &clientContext, &serverContext, + &clientSSL, &serverSSL, + wolfTLSv1_3_client_method, + wolfTLSv1_3_server_method), + 0); + wolfSSL_set_verify(clientSSL, WOLFSSL_VERIFY_PEER, NULL); + ExpectIntEQ(wolfSSL_UseOCSPStapling(clientSSL, WOLFSSL_CSR_OCSP, 0), + WOLFSSL_SUCCESS); + ExpectIntEQ( + wolfSSL_set_cert_compression_algs(clientSSL, certCompZlibOnly, + (int)XELEM_CNT(certCompZlibOnly)), + WOLFSSL_SUCCESS); + + /* ClientHello */ + ExpectIntEQ(wolfSSL_connect(clientSSL), WOLFSSL_FATAL_ERROR); + ExpectIntEQ(wolfSSL_get_error(clientSSL, WOLFSSL_FATAL_ERROR), + WOLFSSL_ERROR_WANT_READ); + + /* ServerHello .. Finished */ + if (failAlloc) { + ExpectIntEQ(wolfSSL_GetAllocators(&prevMalloc, &prevFree, &prevRealloc), + 0); + ExpectIntEQ(wolfSSL_SetAllocators(test_cert_compression_fail_malloc, + test_cert_compression_fail_free, + test_cert_compression_fail_realloc), + 0); + cc_fail_hits = 0; + cc_fail_armed = 1; + } + ExpectIntEQ(wolfSSL_accept(serverSSL), WOLFSSL_FATAL_ERROR); + if (failAlloc) { + cc_fail_armed = 0; + (void)wolfSSL_SetAllocators(prevMalloc, prevFree, prevRealloc); + /* compression was attempted and failed */ + ExpectIntGT(cc_fail_hits, 0); + } + ExpectIntEQ(wolfSSL_get_error(serverSSL, WOLFSSL_FATAL_ERROR), + WOLFSSL_ERROR_WANT_READ); + *flightSz = testContext.c_len; + /* the server fetched a response to staple */ + ExpectIntGT(cc_staple_calls, 0); + ExpectNull(serverSSL == NULL ? NULL : serverSSL->compressedCert); + + /* the client accepts the Certificate only with the staple in it */ + ExpectIntEQ(test_memio_do_handshake(clientSSL, serverSSL, 10, NULL), 0); + ExpectIntEQ(test_cert_compression_exchange(clientSSL, serverSSL), + TEST_SUCCESS); + + wolfSSL_free(clientSSL); + wolfSSL_free(serverSSL); + wolfSSL_CTX_free(clientContext); + wolfSSL_CTX_free(serverContext); + return EXPECT_RESULT(); +} +#endif /* HAVE_OCSP && HAVE_CERTIFICATE_STATUS_REQUEST && ... */ +#endif + +/* When the Certificate cannot be compressed the server falls back to sending + * a plain Certificate and the handshake still completes. With OCSP stapling + * the fallback Certificate must still carry the staple. */ +int test_tls13_comp_cert_fallback(void) +{ + EXPECT_DECLS; +#if defined(TEST_TLS13_CERT_COMPRESSION) && defined(USE_WOLFSSL_MEMORY) && \ + !defined(WOLFSSL_STATIC_MEMORY) && !defined(WOLFSSL_DEBUG_MEMORY) && \ + defined(HAVE_LIBZ) && !defined(WOLFSSL_TRACK_MEMORY) + struct test_memio_ctx testContext; + WOLFSSL_CTX *clientContext = NULL; + WOLFSSL_CTX *serverContext = NULL; + WOLFSSL *clientSSL = NULL; + WOLFSSL *serverSSL = NULL; + wolfSSL_Malloc_cb prevMalloc = NULL; + wolfSSL_Free_cb prevFree = NULL; + wolfSSL_Realloc_cb prevRealloc = NULL; + int plainSz = 0; + int compressedSz = 0; + word16 algs[] = { WC_ZLIB }; +#ifdef TEST_CERT_COMPRESSION_STAPLE + XFILE f = XBADFILE; +#endif + + /* reference sizes of the server flight with a plain and a compressed + * Certificate */ + ExpectIntEQ(test_cert_compression_server_flight_sz(1, &plainSz), + TEST_SUCCESS); + ExpectIntEQ(test_cert_compression_server_flight_sz(0, &compressedSz), + TEST_SUCCESS); + ExpectIntLT(compressedSz, plainSz); + + XMEMSET(&testContext, 0, sizeof(testContext)); + ExpectIntEQ(test_memio_setup(&testContext, &clientContext, &serverContext, + &clientSSL, &serverSSL, + wolfTLSv1_3_client_method, + wolfTLSv1_3_server_method), + 0); + + /* this test is zlib specific and must negotiate with zlib */ + ExpectIntEQ(wolfSSL_set_cert_compression_algs(clientSSL, algs, 1), + WOLFSSL_SUCCESS); + + /* ClientHello */ + ExpectIntEQ(wolfSSL_connect(clientSSL), WOLFSSL_FATAL_ERROR); + ExpectIntEQ(wolfSSL_get_error(clientSSL, WOLFSSL_FATAL_ERROR), + WOLFSSL_ERROR_WANT_READ); + + /* ServerHello .. Finished with compression unable to allocate */ + ExpectIntEQ(wolfSSL_GetAllocators(&prevMalloc, &prevFree, &prevRealloc), 0); + ExpectIntEQ(wolfSSL_SetAllocators(test_cert_compression_fail_malloc, + test_cert_compression_fail_free, + test_cert_compression_fail_realloc), + 0); + cc_fail_hits = 0; + cc_fail_armed = 1; + ExpectIntEQ(wolfSSL_accept(serverSSL), WOLFSSL_FATAL_ERROR); + cc_fail_armed = 0; + (void)wolfSSL_SetAllocators(prevMalloc, prevFree, prevRealloc); + ExpectIntEQ(wolfSSL_get_error(serverSSL, WOLFSSL_FATAL_ERROR), + WOLFSSL_ERROR_WANT_READ); + + /* compression was attempted and failed, and a plain Certificate went + * out in its place */ + ExpectIntGT(cc_fail_hits, 0); + ExpectIntEQ(testContext.c_len, plainSz); + ExpectNull(serverSSL == NULL ? NULL : serverSSL->compressedCert); + + ExpectIntEQ(test_memio_do_handshake(clientSSL, serverSSL, 10, NULL), 0); + ExpectIntEQ(test_cert_compression_exchange(clientSSL, serverSSL), + TEST_SUCCESS); + + wolfSSL_free(clientSSL); + wolfSSL_free(serverSSL); + wolfSSL_CTX_free(clientContext); + wolfSSL_CTX_free(serverContext); + +#ifdef TEST_CERT_COMPRESSION_STAPLE + /* --- OCSP stapling: the status_request extension in the leaf + * CertificateEntry survives the fallback to a plain Certificate --- */ + ExpectTrue((f = XFOPEN("./certs/ocsp/test-leaf-response.der", "rb")) != + XBADFILE); + if (f != XBADFILE) { + cc_staple_respSz = + (int)XFREAD(cc_staple_resp, 1, sizeof(cc_staple_resp), f); + XFCLOSE(f); + } + ExpectIntGT(cc_staple_respSz, 0); + ExpectIntLT(cc_staple_respSz, (int)sizeof(cc_staple_resp)); + + compressedSz = 0; + plainSz = 0; + ExpectIntEQ(test_cert_compression_staple_round(0, &compressedSz), + TEST_SUCCESS); + ExpectIntEQ(test_cert_compression_staple_round(1, &plainSz), TEST_SUCCESS); + /* the first flight was compressed and the second was not */ + ExpectIntGT(compressedSz, 0); + ExpectIntLT(compressedSz, plainSz); +#endif +#endif + return EXPECT_RESULT(); +} diff --git a/tests/api/test_tls13_cert_compression.h b/tests/api/test_tls13_cert_compression.h new file mode 100644 index 00000000000..b5b32712fe0 --- /dev/null +++ b/tests/api/test_tls13_cert_compression.h @@ -0,0 +1,45 @@ +/* test_tls13_cert_compression.h + * + * Copyright (C) 2006-2026 wolfSSL Inc. + * + * This file is part of wolfSSL. + * + * wolfSSL is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 3 of the License, or + * (at your option) any later version. + * + * wolfSSL is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1335, USA + */ + +#ifndef WOLFSSL_TEST_TLS13_CERT_COMPRESSION_H +#define WOLFSSL_TEST_TLS13_CERT_COMPRESSION_H + +#include + +int test_tls13_cert_compression_roundTrip(void); +int test_tls13_cert_compression_fragment(void); +int test_tls13_cert_compression_dtls13(void); +int test_tls13_cert_compression_pha(void); +int test_tls13_cert_compression_malformed(void); +int test_tls13_comp_cert_fallback(void); +int test_tls13_cert_compression_turnoff(void); + +#define TEST_TLS13_CERT_COMPRESSION_DECLS \ + TEST_DECL_GROUP("tls13", test_tls13_cert_compression_roundTrip), \ + TEST_DECL_GROUP("tls13", test_tls13_cert_compression_fragment), \ + TEST_DECL_GROUP("tls13", test_tls13_cert_compression_dtls13), \ + TEST_DECL_GROUP("tls13", test_tls13_cert_compression_pha), \ + TEST_DECL_GROUP("tls13", test_tls13_cert_compression_malformed), \ + TEST_DECL_GROUP("tls13", test_tls13_comp_cert_fallback), \ + TEST_DECL_GROUP("tls13", test_tls13_cert_compression_turnoff) + + +#endif /* WOLFSSL_TEST_TLS13_CERT_COMPRESSION_H */ diff --git a/tests/api/test_tls_msgtype.c b/tests/api/test_tls_msgtype.c index cce5fa1bb61..7c8cbd410cd 100644 --- a/tests/api/test_tls_msgtype.c +++ b/tests/api/test_tls_msgtype.c @@ -2629,3 +2629,62 @@ int test_tls_msgtype_psk_write_chosen(void) #endif return EXPECT_RESULT(); } + +/* ---- compress_certificate (RFC 8879) message-type gates ------------------ */ +/* RFC 8879 Section 3 permits the extension in ClientHello and + * CertificateRequest only. Every other handshake message must be refused. */ +int test_tls_msgtype_cert_compression(void) +{ + EXPECT_DECLS; +#if defined(WOLFSSL_TLS13) && defined(WOLFSSL_CERT_COMPRESSION) && \ + !defined(NO_CERTS) && !defined(NO_WOLFSSL_CLIENT) && \ + !defined(NO_WOLFSSL_SERVER) && !defined(NO_TLS) && \ + defined(HAVE_TLS_EXTENSIONS) + WOLFSSL_CTX* ctx = NULL; + WOLFSSL* ssl = NULL; + byte buf[16]; + word16 len; + Suites suites; + /* A structurally valid single-algorithm list, so the message-type gate is + * what decides the result rather than a length check. */ + const byte body[] = { 0x02, 0x00, 0x01 }; + + ExpectNotNull(ctx = wolfSSL_CTX_new(wolfTLSv1_3_client_method())); + ExpectNotNull(ssl = wolfSSL_new(ctx)); + XMEMSET(&suites, 0, sizeof(suites)); + + /* Allowed. */ + len = build_ext_with_body(buf, TLSXT_CERT_COMPRESSION, body, + (word16)sizeof(body)); + ExpectIntEQ(TLSX_Parse(ssl, buf, len, client_hello, &suites), 0); + + len = build_ext_with_body(buf, TLSXT_CERT_COMPRESSION, body, + (word16)sizeof(body)); + ExpectIntEQ(TLSX_Parse(ssl, buf, len, certificate_request, &suites), 0); + + /* Refused everywhere else. */ + len = build_ext_with_body(buf, TLSXT_CERT_COMPRESSION, body, + (word16)sizeof(body)); + ExpectIntEQ(TLSX_Parse(ssl, buf, len, server_hello, NULL), + WC_NO_ERR_TRACE(EXT_NOT_ALLOWED)); + + len = build_ext_with_body(buf, TLSXT_CERT_COMPRESSION, body, + (word16)sizeof(body)); + ExpectIntEQ(TLSX_Parse(ssl, buf, len, encrypted_extensions, NULL), + WC_NO_ERR_TRACE(EXT_NOT_ALLOWED)); + + len = build_ext_with_body(buf, TLSXT_CERT_COMPRESSION, body, + (word16)sizeof(body)); + ExpectIntEQ(TLSX_Parse(ssl, buf, len, certificate, NULL), + WC_NO_ERR_TRACE(UNSUPPORTED_EXTENSION)); + + len = build_ext_with_body(buf, TLSXT_CERT_COMPRESSION, body, + (word16)sizeof(body)); + ExpectIntEQ(TLSX_Parse(ssl, buf, len, finished, NULL), + WC_NO_ERR_TRACE(EXT_NOT_ALLOWED)); + + wolfSSL_free(ssl); + wolfSSL_CTX_free(ctx); +#endif + return EXPECT_RESULT(); +} diff --git a/tests/api/test_tls_msgtype.h b/tests/api/test_tls_msgtype.h index 69fcc48060a..706ceb2baed 100644 --- a/tests/api/test_tls_msgtype.h +++ b/tests/api/test_tls_msgtype.h @@ -70,6 +70,7 @@ int test_tls_msgtype_tca_parse_gates(void); int test_tls_msgtype_tca_find(void); int test_tls_msgtype_tca_new_alloc(void); int test_tls_msgtype_psk_write_chosen(void); +int test_tls_msgtype_cert_compression(void); #define TEST_TLS_MSGTYPE_DECLS \ TEST_DECL_GROUP("tls", test_tls_msgtype_arg_guard), \ @@ -119,6 +120,7 @@ int test_tls_msgtype_psk_write_chosen(void); TEST_DECL_GROUP("tls", test_tls_msgtype_tca_parse_gates), \ TEST_DECL_GROUP("tls", test_tls_msgtype_tca_find), \ TEST_DECL_GROUP("tls", test_tls_msgtype_tca_new_alloc), \ - TEST_DECL_GROUP("tls", test_tls_msgtype_psk_write_chosen) + TEST_DECL_GROUP("tls", test_tls_msgtype_psk_write_chosen), \ + TEST_DECL_GROUP("tls", test_tls_msgtype_cert_compression) #endif /* TESTS_API_TEST_TLS_MSGTYPE_H */ diff --git a/tests/api/test_tls_parse.c b/tests/api/test_tls_parse.c index 0c908fac6f7..b38fe6b46bc 100644 --- a/tests/api/test_tls_parse.c +++ b/tests/api/test_tls_parse.c @@ -29,6 +29,9 @@ #ifndef NO_DH #include #endif +#ifdef WOLFSSL_CERT_COMPRESSION +#include +#endif /* Several helpers below are called only from test bodies whose feature guards * differ, so a configuration can compile in none of their callers. */ @@ -108,6 +111,24 @@ static word16 test_tls_parse_build_ext(byte* out, word16 outCap, return (word16)(4 + bodyLen); } +/* Returns a pointer to the first occurrence of 'needle' within 'hay', or NULL. + * Used to locate one extension record inside a written extensions block + * without depending on where the writer happened to place it. */ +TEST_TLS_PARSE_UNUSED +static const byte* test_tls_parse_find_bytes(const byte* hay, word16 hayLen, + const byte* needle, word16 needleLen) +{ + word16 i; + + if (needleLen == 0 || hayLen < needleLen) + return NULL; + for (i = 0; i <= (word16)(hayLen - needleLen); i++) { + if (XMEMCMP(hay + i, needle, needleLen) == 0) + return hay + i; + } + return NULL; +} + /* A small counting allocator used to force a single, targeted malloc * failure. Installed narrowly around the call under test and restored * immediately after, so it never affects unrelated allocations. @@ -3246,3 +3267,182 @@ int test_TLSX_KeyShare_process(void) #endif return EXPECT_RESULT(); } + +/* ---- Certificate Compression (RFC 8879) ---------------------------------- */ +/* The extension's own machinery only: which algorithm a peer's list selects, + * and that a well-formed list survives a size/write round trip. Nothing here + * compresses or decompresses a certificate. */ +int test_TLSX_CertCompression_parse(void) +{ + EXPECT_DECLS; +#if defined(WOLFSSL_TLS13) && defined(WOLFSSL_CERT_COMPRESSION) && \ + !defined(NO_CERTS) && !defined(NO_TLS) && !defined(NO_WOLFSSL_CLIENT) && \ + !defined(NO_WOLFSSL_SERVER) && defined(HAVE_TLS_EXTENSIONS) + WOLFSSL_CTX* ctx = NULL; + WOLFSSL* ssl = NULL; + byte ext[16]; + word16 extLen; + + /* CertificateCompressionAlgorithms: algorithms<2..2^8-2>, i.e. a 1-byte + * length in bytes followed by that many bytes of 2-byte algorithm IDs. */ + const byte zlibOnly[] = { 0x02, 0x00, WC_ZLIB }; + /* brotli and zstd: registered, but not implemented by this build. */ + const byte unsupported[] = { 0x04, 0x00, 0x02, 0x00, 0x03 }; + /* OPENSSL offer list and order */ + const byte opensslList[] = { 0x06, 0x00, 0x02, 0x00, WC_ZLIB, + 0x00, 0x03 }; + + /* Malformed bodies, each rejected before any algorithm is looked at. */ + const byte truncated[] = { 0x02, 0x00 }; /* shorter than 3 */ + const byte emptyList[] = { 0x00 }; /* no algorithms */ + const byte oddLen[] = { 0x03, 0x00, WC_ZLIB, 0x00 }; /* len not even */ + const byte lenMismatch[] = { 0x04, 0x00, WC_ZLIB }; /* len > body */ + + ExpectNotNull(ctx = wolfSSL_CTX_new(wolfTLSv1_3_client_method())); + ExpectNotNull(ssl = wolfSSL_new(ctx)); + if (ssl != NULL) { + Suites* suites = (Suites*)WOLFSSL_SUITES(ssl); + + /* Nothing negotiated until a list has been parsed. */ + ExpectIntEQ(ssl->peerCertCompressionAlg, WC_NO_COMPRESSION); + + /* A list naming only zlib selects zlib. */ + extLen = test_tls_parse_build_ext(ext, sizeof(ext), + TLSXT_CERT_COMPRESSION, zlibOnly, (word16)sizeof(zlibOnly)); + ExpectIntEQ(TLSX_Parse(ssl, ext, extLen, client_hello, suites), 0); + ExpectIntEQ(ssl->peerCertCompressionAlg, WC_ZLIB); + + /* RFC 8879 Section 3 makes the extension advisory: a list naming only + * algorithms we do not implement is accepted, and simply selects + * nothing. It must not fail the handshake. */ + ssl->peerCertCompressionAlg = WC_NO_COMPRESSION; + extLen = test_tls_parse_build_ext(ext, sizeof(ext), + TLSXT_CERT_COMPRESSION, unsupported, + (word16)sizeof(unsupported)); + ExpectIntEQ(TLSX_Parse(ssl, ext, extLen, client_hello, suites), 0); + ExpectIntEQ(ssl->peerCertCompressionAlg, WC_NO_COMPRESSION); + + /* zlib is found even when an unsupported algorithm precedes it. */ + ssl->peerCertCompressionAlg = WC_NO_COMPRESSION; + extLen = test_tls_parse_build_ext(ext, sizeof(ext), + TLSXT_CERT_COMPRESSION, opensslList, + (word16)sizeof(opensslList)); + ExpectIntEQ(TLSX_Parse(ssl, ext, extLen, client_hello, suites), 0); + ExpectIntEQ(ssl->peerCertCompressionAlg, WC_ZLIB); + + /* Malformed lists are rejected, and leave the selection alone. */ + ssl->peerCertCompressionAlg = WC_NO_COMPRESSION; + + extLen = test_tls_parse_build_ext(ext, sizeof(ext), + TLSXT_CERT_COMPRESSION, truncated, (word16)sizeof(truncated)); + ExpectIntEQ(TLSX_Parse(ssl, ext, extLen, client_hello, suites), + WC_NO_ERR_TRACE(BUFFER_ERROR)); + + extLen = test_tls_parse_build_ext(ext, sizeof(ext), + TLSXT_CERT_COMPRESSION, emptyList, (word16)sizeof(emptyList)); + ExpectIntEQ(TLSX_Parse(ssl, ext, extLen, client_hello, suites), + WC_NO_ERR_TRACE(BUFFER_ERROR)); + + extLen = test_tls_parse_build_ext(ext, sizeof(ext), + TLSXT_CERT_COMPRESSION, oddLen, (word16)sizeof(oddLen)); + ExpectIntEQ(TLSX_Parse(ssl, ext, extLen, client_hello, suites), + WC_NO_ERR_TRACE(BUFFER_ERROR)); + + extLen = test_tls_parse_build_ext(ext, sizeof(ext), + TLSXT_CERT_COMPRESSION, lenMismatch, + (word16)sizeof(lenMismatch)); + ExpectIntEQ(TLSX_Parse(ssl, ext, extLen, client_hello, suites), + WC_NO_ERR_TRACE(BUFFER_ERROR)); + + ExpectIntEQ(ssl->peerCertCompressionAlg, WC_NO_COMPRESSION); + + /* The server's other legal slot, CertificateRequest, parses the same + * list the same way. */ + extLen = test_tls_parse_build_ext(ext, sizeof(ext), + TLSXT_CERT_COMPRESSION, zlibOnly, (word16)sizeof(zlibOnly)); + ExpectIntEQ(TLSX_Parse(ssl, ext, extLen, certificate_request, suites), + 0); + ExpectIntEQ(ssl->peerCertCompressionAlg, WC_ZLIB); + } + wolfSSL_free(ssl); + wolfSSL_CTX_free(ctx); +#endif + return EXPECT_RESULT(); +} + +int test_TLSX_CertCompression_write(void) +{ + EXPECT_DECLS; +#if defined(WOLFSSL_TEST_STATIC_BUILD) && defined(WOLFSSL_TLS13) && \ + defined(WOLFSSL_CERT_COMPRESSION) && !defined(NO_CERTS) && \ + !defined(NO_TLS) && !defined(NO_WOLFSSL_CLIENT) && \ + !defined(NO_WOLFSSL_SERVER) && !defined(NO_FILESYSTEM) && \ + defined(HAVE_TLS_EXTENSIONS) + WOLFSSL_CTX* ctx = NULL; + WOLFSSL* ssl = NULL; + byte out[2048]; + word32 len; + word32 off; + /* type(2) + length(2) + body: list length 2, then zlib. */ + const byte wire[] = { 0x00, 0x1B, 0x00, 0x03, 0x02, 0x00, WC_ZLIB }; + + /* Client: the extension goes into the ClientHello. */ + ExpectNotNull(ctx = wolfSSL_CTX_new(wolfTLSv1_3_client_method())); + ExpectNotNull(ssl = wolfSSL_new(ctx)); + if (ssl != NULL) { + ExpectIntEQ(TLSX_PopulateExtensions(ssl, 0), 0); + ExpectNotNull(TLSX_Find(ssl->extensions, TLSX_CERT_COMPRESSION)); + + len = 0; + ExpectIntEQ(TLSX_GetRequestSize(ssl, client_hello, &len), 0); + ExpectIntGT(len, 0); + ExpectIntLE(len, sizeof(out)); + + off = 0; + XMEMSET(out, 0, sizeof(out)); + ExpectIntEQ(TLSX_WriteRequest(ssl, out, client_hello, &off), 0); + /* The size pass and the write pass must agree, or every extension + * after this one lands at the wrong offset. */ + ExpectIntEQ(off, len); + ExpectNotNull(test_tls_parse_find_bytes(out, (word16)off, wire, + (word16)sizeof(wire))); + } + wolfSSL_free(ssl); + ssl = NULL; + wolfSSL_CTX_free(ctx); + ctx = NULL; + + /* Server: the extension's only other legal slot is CertificateRequest, + * and only when we will actually ask the client for a certificate. */ + ExpectNotNull(ctx = test_tls_parse_server_ctx(wolfTLSv1_3_server_method())); + if (ctx != NULL) + wolfSSL_CTX_set_verify(ctx, WOLFSSL_VERIFY_PEER, NULL); + ExpectNotNull(ssl = wolfSSL_new(ctx)); + if (ssl != NULL) { + ExpectIntEQ(TLSX_PopulateExtensions(ssl, 1), 0); + /* The server only adds compress_certificate when it builds a + * CertificateRequest (see SendTls13CertificateRequest). */ + ExpectIntEQ(TLSX_UseCertCompression(ssl, ssl->heap), 0); + ExpectNotNull(TLSX_Find(ssl->extensions, TLSX_CERT_COMPRESSION)); + + len = 0; + ExpectIntEQ(TLSX_GetRequestSize(ssl, certificate_request, &len), 0); + ExpectIntGT(len, 0); + ExpectIntLE(len, sizeof(out)); + + off = 0; + XMEMSET(out, 0, sizeof(out)); + ExpectIntEQ(TLSX_WriteRequest(ssl, out, certificate_request, &off), 0); + ExpectIntEQ(off, len); + /* The certificate_request semaphore is deny-by-default, so this also + * covers the extension being explicitly re-enabled there. */ + ExpectNotNull(test_tls_parse_find_bytes(out, (word16)off, wire, + (word16)sizeof(wire))); + } + wolfSSL_free(ssl); + ssl = NULL; + wolfSSL_CTX_free(ctx); + ctx = NULL; +#endif + return EXPECT_RESULT(); +} diff --git a/tests/api/test_tls_parse.h b/tests/api/test_tls_parse.h index 1d2d8bcd290..ceb78d78f66 100644 --- a/tests/api/test_tls_parse.h +++ b/tests/api/test_tls_parse.h @@ -42,6 +42,8 @@ int test_TLSX_KeyShare_negotiate(void); int test_TLSX_KeyShare_gen(void); int test_TLSX_KeyShare_freesizewrite(void); int test_TLSX_KeyShare_process(void); +int test_TLSX_CertCompression_parse(void); +int test_TLSX_CertCompression_write(void); #define TEST_TLS_PARSE_DECLS \ TEST_DECL_GROUP("tls", test_TLSX_ALPN_parse), \ @@ -63,6 +65,8 @@ int test_TLSX_KeyShare_process(void); TEST_DECL_GROUP("tls", test_TLSX_KeyShare_negotiate), \ TEST_DECL_GROUP("tls", test_TLSX_KeyShare_gen), \ TEST_DECL_GROUP("tls", test_TLSX_KeyShare_freesizewrite), \ - TEST_DECL_GROUP("tls", test_TLSX_KeyShare_process) + TEST_DECL_GROUP("tls", test_TLSX_KeyShare_process), \ + TEST_DECL_GROUP("tls", test_TLSX_CertCompression_parse), \ + TEST_DECL_GROUP("tls", test_TLSX_CertCompression_write) #endif /* TESTS_API_TEST_TLS_PARSE_H */ diff --git a/tests/quic.c b/tests/quic.c index df899b27ddb..8879918c8a6 100644 --- a/tests/quic.c +++ b/tests/quic.c @@ -1384,6 +1384,10 @@ static void check_crypto_records(QuicTestContext *from, OutputBuffer *out, check_rec = check_ee; break; case certificate: + #ifdef WOLFSSL_CERT_COMPRESSION + /* a compressed Certificate counts as a Certificate here */ + case compressed_certificate: + #endif rec_name = "Certificate"; break; case certificate_verify: diff --git a/wolfcrypt/src/compress.c b/wolfcrypt/src/compress.c index cec5b7b8bba..581eac3566a 100644 --- a/wolfcrypt/src/compress.c +++ b/wolfcrypt/src/compress.c @@ -20,11 +20,12 @@ */ #include +#include -#ifdef HAVE_LIBZ +/* zlib backend */ +#ifdef HAVE_LIBZ -#include #ifdef NO_INLINE #include #else @@ -50,7 +51,7 @@ static void myFree(void* opaque, void* memory) } -#ifdef HAVE_MCAPI +#if defined(HAVE_MCAPI) #define DEFLATE_DEFAULT_WINDOWBITS 11 #define DEFLATE_DEFAULT_MEMLEVEL 1 #else @@ -347,3 +348,241 @@ int wc_DeCompressDynamic(byte** out, int maxSz, int memoryType, #endif /* HAVE_LIBZ */ +#ifdef WOLFSSL_HAVE_COMPRESSION_BACKEND + +/* Start of compression object interfaces */ +int wc_CompressionData_InitDeComp(wc_CompressionData* cd, + const byte* data, word32 compSz, word32 uncompSz, word16 alg) +{ + if (cd == NULL) + return BAD_FUNC_ARG; + + XMEMSET(cd, 0, sizeof(*cd)); + + if (data == NULL || uncompSz == 0 || !wc_IsCompressionAlgSupported(alg)) { + return BAD_FUNC_ARG; + } + + cd->compressionAlg = alg; + cd->compressedSz = compSz; + cd->uncompressedSz = uncompSz; + /* not owned, so it is only read from and never freed */ + cd->data = (byte*)(wc_ptr_t)data; + cd->isCompressed = 1; + return 0; +} + +int wc_CompressionData_InitComp(wc_CompressionData* cd, const byte* data, + word32 uncompSz, word16 alg) +{ + if (cd == NULL) + return BAD_FUNC_ARG; + + XMEMSET(cd, 0, sizeof(*cd)); + + if (data == NULL || uncompSz == 0 || !wc_IsCompressionAlgSupported(alg)) { + return BAD_FUNC_ARG; + } + + + cd->compressionAlg = alg; + cd->uncompressedSz = uncompSz; + /* not owned, so it is only read from and never freed */ + cd->data = (byte*)(wc_ptr_t)data; + return 0; +} + +void wc_CompressionData_Free(wc_CompressionData* cd) +{ + void* heap; + + if (cd == NULL) + return; + + heap = cd->heap; + if (cd->dataIsOwned) { + if (cd->data != NULL) { + ForceZero(cd->data, cd->isCompressed ? cd->compressedSz : + cd->uncompressedSz); + XFREE(cd->data, heap, DYNAMIC_TYPE_TMP_BUFFER); + } + } + ForceZero(cd, sizeof(wc_CompressionData)); +} + +int wc_CompressionData_CompToBuf(const wc_CompressionData* data, byte* out, + word32 outSz) +{ + int ret = 0; + + if (data == NULL || data->data == NULL || data->isCompressed || + data->uncompressedSz == 0 || out == NULL) { + return BAD_FUNC_ARG; + } + + switch (data->compressionAlg) { + case WC_ZLIB: +#ifdef HAVE_LIBZ + ret = wc_Compress(out, outSz, + data->data, data->uncompressedSz, 0); + break; +#endif + + /* implement more compression algs here */ + case WC_NO_COMPRESSION: + default: + ret = BAD_FUNC_ARG; + break; + } + + return ret; +} + +int wc_CompressionData_Compress(wc_CompressionData* data) +{ + int ret; + byte* out; + + if (data == NULL || data->data == NULL || data->isCompressed || + data->uncompressedSz == 0) { + return BAD_FUNC_ARG; + } + + if (!wc_IsCompressionAlgSupported(data->compressionAlg)) { + return BAD_FUNC_ARG; + } + + out = (byte*)XMALLOC(data->uncompressedSz, data->heap, + DYNAMIC_TYPE_TMP_BUFFER); + if (out == NULL) + return MEMORY_E; + + ret = wc_CompressionData_CompToBuf(data, out, data->uncompressedSz); + + if (ret <= 0) { + XFREE(out, data->heap, DYNAMIC_TYPE_TMP_BUFFER); + return (ret == 0) ? COMPRESS_E : ret; + } + + if (data->dataIsOwned) { + ForceZero(data->data, data->uncompressedSz); + XFREE(data->data, data->heap, DYNAMIC_TYPE_TMP_BUFFER); + } + data->data = out; + data->isCompressed = 1; + data->compressedSz = (word32)ret; + data->dataIsOwned = 1; + + /* free last bit of extra memory */ +#ifndef WOLFSSL_NO_REALLOC + { + byte* tmp = (byte*)XREALLOC(out, data->compressedSz, data->heap, + DYNAMIC_TYPE_TMP_BUFFER); + if (tmp != NULL) { + data->data = tmp; + } + } +#endif + + return 0; +} + +int wc_CompressionData_DeCompToBuf(const wc_CompressionData* data, + byte* out, word32 outSz) +{ + int ret = 0; + + if (data == NULL || data->data == NULL || !data->isCompressed || + data->compressedSz == 0 || data->uncompressedSz == 0 || + out == NULL) { + return BAD_FUNC_ARG; + } + + if (outSz < data->uncompressedSz) { + return BUFFER_E; + } + + switch (data->compressionAlg) { + case WC_ZLIB: +#ifdef HAVE_LIBZ + ret = wc_DeCompress_ex(out, outSz, + data->data, data->compressedSz, 15); + if (ret >= 0 && (word32)ret != data->uncompressedSz) { + return BUFFER_E; + } + return ret; +#endif + + /* implement more compression algs here */ + case WC_NO_COMPRESSION: + default: + ret = BAD_FUNC_ARG; + break; + } + + return ret; +} + +int wc_CompressionData_DeCompress(wc_CompressionData* data) +{ + int ret; + byte* out; + + if (data == NULL || data->data == NULL || !data->isCompressed || + data->compressedSz == 0 || data->uncompressedSz == 0) { + return BAD_FUNC_ARG; + } + + if (!wc_IsCompressionAlgSupported(data->compressionAlg)) { + return BAD_FUNC_ARG; + } + + out = (byte*)XMALLOC(data->uncompressedSz, data->heap, + DYNAMIC_TYPE_TMP_BUFFER); + if (out == NULL) + return MEMORY_E; + + ret = wc_CompressionData_DeCompToBuf(data, out, data->uncompressedSz); + + if (ret < 0) { + XFREE(out, data->heap, DYNAMIC_TYPE_TMP_BUFFER); + return ret; + } + + if (data->dataIsOwned) { + ForceZero(data->data, data->compressedSz); + XFREE(data->data, data->heap, DYNAMIC_TYPE_TMP_BUFFER); + } + data->data = out; + data->isCompressed = 0; + data->uncompressedSz = (word32)ret; + data->dataIsOwned = 1; + + return 0; +} + +int wc_CompressionData_SetHeap(wc_CompressionData* cd, void* heap) +{ + if (cd == NULL) + return BAD_FUNC_ARG; + + cd->heap = heap; + return 0; +} + + +byte wc_IsCompressionAlgSupported(word16 alg) +{ + switch (alg) { +#ifdef HAVE_LIBZ + case WC_ZLIB: + return 1; +#endif + + case WC_NO_COMPRESSION: + default: + return 0; + } +} + +#endif /* WOLFSSL_HAVE_COMPRESSION_BACKEND */ diff --git a/wolfssl/internal.h b/wolfssl/internal.h index aa09cf16213..f9684e9cce2 100644 --- a/wolfssl/internal.h +++ b/wolfssl/internal.h @@ -170,6 +170,9 @@ #include #endif +#if defined(WOLFSSL_CERT_COMPRESSION) || defined(HAVE_LIBZ) + #include +#endif #ifdef HAVE_LIBZ #include "zlib.h" @@ -1645,6 +1648,7 @@ enum Misc { DTLS13_HANDSHAKE_HEADER_SZ = 12, /* sizeof(Dtls13HandshakeHeader) */ RECORD_HEADER_SZ = 5, /* type + version + len(2) */ CERT_HEADER_SZ = 3, /* always 3 bytes */ + COMP_CERT_HEADER_SZ = 8, /* alg<2> + uncompSz <3> + compSz <3> */ REQ_HEADER_SZ = 2, /* cert request header sz */ HINT_LEN_SZ = 2, /* length of hint size field */ TRUNCATED_HMAC_SZ = 10, /* length of hmac w/ truncated hmac extension */ @@ -2087,6 +2091,14 @@ WOLFSSL_LOCAL int NamedGroupIsPqcHybrid(int group); #endif #endif +/* TLS 1.3 Certificate Compression (RFC 8879) needs TLS 1.3 and other relevant + * macros*/ +#if defined(WOLFSSL_CERT_COMPRESSION) && defined (HAVE_TLS_EXTENSIONS) && \ + (!defined(WOLFSSL_TLS13) || !defined(HAVE_LIBZ) || defined(NO_CERTS)) + #error WOLFSSL_CERT_COMPRESSION needs WOLFSSL_TLS13, HAVE_LIBZ, not \ + NO_CERTS, and HAVE_TLS_EXTENSIONS. +#endif + /* Max certificate extensions in TLS1.3 */ #if defined(HAVE_CERTIFICATE_STATUS_REQUEST) /* Number of extensions to set each OCSP response */ @@ -2297,6 +2309,14 @@ WOLFSSL_LOCAL int DoTls13Finished(WOLFSSL* ssl, const byte* input, word32* inOut #endif WOLFSSL_TEST_VIS int DoApplicationData(WOLFSSL* ssl, byte* input, word32* inOutIdx, int sniff); +#if defined(WOLFSSL_TLS13) && defined(WOLFSSL_CERT_COMPRESSION) && \ + !defined(NO_CERTS) +#ifdef WOLFSSL_API_PREFIX_MAP + #define DoTls13CompressedCertificate wolfSSL_DoTls13CompressedCertificate +#endif +WOLFSSL_TEST_VIS int DoTls13CompressedCertificate(WOLFSSL* ssl, byte* input, + word32* inOutIdx, word32 totalSz); +#endif /* TLS v1.3 needs these */ WOLFSSL_LOCAL int HandleTlsResumption(WOLFSSL* ssl, Suites* clSuites); #ifdef WOLFSSL_TLS13 @@ -3250,6 +3270,7 @@ typedef struct Options Options; #define TLSXT_SERVER_CERTIFICATE 0x0014 /* RFC8446 */ #define TLSXT_ENCRYPT_THEN_MAC 0x0016 /* RFC 7366 */ #define TLSXT_EXTENDED_MASTER_SECRET 0x0017 /* HELLO_EXT_EXTMS */ +#define TLSXT_CERT_COMPRESSION 0x001b /* RFC 8879 */ #define TLSXT_CERT_WITH_EXTERN_PSK 0x0021 /* RFC 9973 */ #define TLSXT_SESSION_TICKET 0x0023 #define TLSXT_PRE_SHARED_KEY 0x0029 @@ -3299,6 +3320,9 @@ typedef enum { TLSX_EXTENDED_MASTER_SECRET = TLSXT_EXTENDED_MASTER_SECRET, TLSX_SESSION_TICKET = TLSXT_SESSION_TICKET, #ifdef WOLFSSL_TLS13 + #ifdef WOLFSSL_CERT_COMPRESSION + TLSX_CERT_COMPRESSION = TLSXT_CERT_COMPRESSION, + #endif #ifdef WOLFSSL_EARLY_DATA TLSX_EARLY_DATA = TLSXT_EARLY_DATA, #endif @@ -3706,6 +3730,14 @@ WOLFSSL_LOCAL int ProcessChainOCSPRequest(WOLFSSL* ssl); WOLFSSL_LOCAL int CreateOcspRequest(WOLFSSL* ssl, OcspRequest* request, DecodedCert* cert, byte* certData, word32 length); #endif + +#ifdef WOLFSSL_CERT_COMPRESSION +#ifdef WOLFSSL_API_PREFIX_MAP + #define TLSX_UseCertCompression wolfSSL_TLSX_UseCertCompression +#endif +WOLFSSL_TEST_VIS int TLSX_UseCertCompression(WOLFSSL* ssl, void* heap); +#endif + /** Certificate Status Request v2 - RFC 6961 */ #ifdef HAVE_CERTIFICATE_STATUS_REQUEST_V2 @@ -4583,6 +4615,13 @@ struct WOLFSSL_CTX { word16 group[WOLFSSL_MAX_GROUP_COUNT]; byte numGroups; #endif +#ifdef WOLFSSL_CERT_COMPRESSION + /* list of offered compression algs, copied to each new WOLFSSL. + * NULL = use the built-in default list */ + byte noOfferCompressionAlgPrefList; + byte compressionAlgPrefListLen; + word16* compressionAlgPrefList; +#endif #ifdef WOLFSSL_EARLY_DATA word32 maxEarlyDataSz; #if defined(WOLFSSL_TLS13) && defined(HAVE_SESSION_TICKET) && !defined(NO_TLS) @@ -7236,6 +7275,16 @@ struct WOLFSSL { word32 earlyDataSz; byte earlyDataStatus; #endif +#ifdef WOLFSSL_CERT_COMPRESSION + /* RFC 8879 algorithm ID; WC_NO_COMPRESSION = none negotiated */ + word16 peerCertCompressionAlg; + wc_CompressionData* compressedCert; + /* list of offered compression algs; NULL = use the built-in default, + * This also determines what we are willing to send */ + byte noOfferCompressionAlgPrefList; + byte compressionAlgPrefListLen; + word16* compressionAlgPrefList; +#endif #if defined(OPENSSL_EXTRA) WOLFSSL_STACK* supportedCiphers; /* Used in wolfSSL_get_ciphers_compat */ WOLFSSL_STACK* peerCertChain; /* Used in wolfSSL_get_peer_cert_chain */ @@ -7520,6 +7569,7 @@ enum HandShakeType { finished = 20, certificate_status = 22, key_update = 24, + compressed_certificate = 25, /* RFC 8879 TLS1.3 > only */ change_cipher_hs = 55, /* simulate unique handshake type for sanity checks. record layer change_cipher conflicts with handshake finished */ diff --git a/wolfssl/ssl.h b/wolfssl/ssl.h index a5b50f11ab2..02c49b3771b 100644 --- a/wolfssl/ssl.h +++ b/wolfssl/ssl.h @@ -77,6 +77,10 @@ #include "prefix_ssl.h" #endif +#ifdef WOLFSSL_CERT_COMPRESSION + #include +#endif + #ifdef LIBWOLFSSL_VERSION_STRING #define WOLFSSL_VERSION LIBWOLFSSL_VERSION_STRING #endif @@ -1545,6 +1549,12 @@ WOLFSSL_API int wolfSSL_preferred_group(WOLFSSL* ssl); WOLFSSL_API int wolfSSL_connect_TLSv13(WOLFSSL* ssl); WOLFSSL_API int wolfSSL_accept_TLSv13(WOLFSSL* ssl); +#ifdef WOLFSSL_CERT_COMPRESSION +WOLFSSL_API int wolfSSL_CTX_set_cert_compression_algs(WOLFSSL_CTX* ctx, + const word16* algs, int count); +WOLFSSL_API int wolfSSL_set_cert_compression_algs(WOLFSSL* ssl, + const word16* algs, int count); +#endif #ifdef WOLFSSL_EARLY_DATA #define WOLFSSL_EARLY_DATA_NOT_SENT 0 diff --git a/wolfssl/wolfcrypt/compress.h b/wolfssl/wolfcrypt/compress.h index d230e9beae4..7b04192a54a 100644 --- a/wolfssl/wolfcrypt/compress.h +++ b/wolfssl/wolfcrypt/compress.h @@ -19,8 +19,7 @@ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1335, USA */ -/*! - \file wolfssl/wolfcrypt/compress.h +/*! \file wolfssl/wolfcrypt/compress.h */ @@ -29,18 +28,37 @@ #include -#ifdef HAVE_LIBZ #ifdef __cplusplus extern "C" { #endif +enum { +/* compression alg Ids used by tls certificate compression defined + * in RFC 8879, They cannot change but are defined here so they are consistent + * across wolfSSL */ + WC_NO_COMPRESSION = 0, + WC_ZLIB = 1 +}; + +#if defined (HAVE_LIBZ) /* || defined (future backend) */ + #define WOLFSSL_HAVE_COMPRESSION_BACKEND +#endif + +#ifdef HAVE_LIBZ + + #define COMPRESS_FIXED 1 #define LIBZ_WINBITS_GZIP 16 +/* These are a set of zlib interface functions. They provide access to + * setting flags and behavior for when the high-level functions are + * not set up for your use case + * + * These are called by the high-level interface */ WOLFSSL_API int wc_Compress(byte*, word32, const byte*, word32, word32); WOLFSSL_API int wc_Compress_ex(byte* out, word32 outSz, const byte* in, word32 inSz, word32 flags, word32 windowBits); @@ -50,11 +68,135 @@ WOLFSSL_API int wc_DeCompress_ex(byte* out, word32 outSz, const byte* in, WOLFSSL_API int wc_DeCompressDynamic(byte** out, int max, int memoryType, const byte* in, word32 inSz, int windowBits, void* heap); +#endif + +#ifdef WOLFSSL_HAVE_COMPRESSION_BACKEND +/** + * Check if a compression alg is supported + * + * @param alg alg you want to check is supported or not + * @return 1 if supported, 0 if not + */ +WOLFSSL_API byte wc_IsCompressionAlgSupported(word16 alg); + +/* Used in high-level interfaces for compression backends + * + * This struct tracks memory and state of the data as it is compressed and + * decompressed */ +typedef struct wc_CompressionData { + byte* data; + /* this is the heap that all allocated data that CompressionData is + * related to will use */ + void* heap; + word32 compressedSz; + word32 uncompressedSz; + word16 compressionAlg; /* 0 is no compression is set, WC_ZLIB is zlib */ + /* if true then the data buffer is freed when replaced by + * new compressed/uncompressed data when *_[De]Compress is called + * + * This also determines if the data buffer will be freed when + * wc_CompressionData_Free is called */ + byte dataIsOwned; + /* is the data compressed */ + byte isCompressed; +}wc_CompressionData; + +/* Init a new wc_CompressionData object from compressed data. This initial data + * buffer is not owned by the wc_CompressionData object and is the + * responsibility of the caller + * Note: if reusing a wc_CompressionData object it must have + * wc_CompressionData_Free called on it before reiniting + * + * @param cd wc_CompressionData object to initialize + * @param data This is a buffer of data already compressed. + * @param compSz The size of the data buffer. + * @param uncompSz The exact size of the data buffer when uncompressed + * @param alg The id of the compression algorithm used to compress the data + * Options-{WC_ZLIB} + * @return 0 if success, negative value on error + * */ +WOLFSSL_API int wc_CompressionData_InitDeComp(wc_CompressionData* cd, + const byte* data, word32 compSz, word32 uncompSz, + word16 alg); + +/* Init a new wc_CompressionData object from decompressed data. This initial + * data buffer is not owned by the wc_CompressionData object and is the + * responsibility of the caller. + * + * Note: if reusing a wc_CompressionData object it must have + * wc_CompressionData_Free called on it before reiniting + * + * @param cd wc_CompressionData object to initialize + * @param data This is a buffer of data that is uncompressed. + * @param uncompSz The exact size of the data buffer + * @param alg The id of the compression algorithm used to compress the data + * Options-{WC_ZLIB} + * @return 0 if success, negative value on error + * */ +WOLFSSL_API int wc_CompressionData_InitComp(wc_CompressionData* cd, + const byte* data, word32 uncompSz, word16 alg); + +/* set the heap that *_Compress and *_DeCompress will use output buffer + * allocation */ +WOLFSSL_API int wc_CompressionData_SetHeap(wc_CompressionData* cd, + void* heap); + +/* release all internal data that is owned by the wc_CompressionData object */ +WOLFSSL_API void wc_CompressionData_Free(wc_CompressionData* cd); + +/* Compress data inside of wc_CompressionData object. This call allocates a + * new buffer to Compress into; if the buffer already in the object is + * from a previous *_DeCompress call it is owned by this object and is freed. + * If it is the buffer from the *_InitComp call it is not freed. + * + * @param data initialized wc_CompressionData object that set for compression + * @return 0 on success or negative value on error. + * Compressed data is in wc_CompressionData on success along with context + * about the compression. + */ +WOLFSSL_API int wc_CompressionData_Compress(wc_CompressionData* data); + +/* Compressed data into the output buffer owned by the caller. The + * wc_CompressionData object is left un-touched. + * + * @param data initialized wc_CompressionData object that set for compression + * @param out output buffer compressed data will land in with space for + * compressed data + * @param outSz size of output buffer + * @returns number of bytes written on success or negaitve error code otherwise + */ +WOLFSSL_API int wc_CompressionData_CompToBuf(const wc_CompressionData* data, + byte* out, word32 outSz); + +/* Decompress data inside of wc_CompressionData object. This call allocates a + * new buffer to decompress into; if the buffer already in the object is + * from a previous *_Compress call it is owned by this object and is freed. + * If it is the buffer from the *_InitDeComp call it is not freed. + * + * @param data initialized wc_CompressionData object that set for compression + * @return 0 on success or negative value on error. + * Compressed data is in wc_CompressionData on success along with context + * about the compression. + */ +WOLFSSL_API int wc_CompressionData_DeCompress(wc_CompressionData* data); + +/* Decompressed data into the output buffer owned by the caller. The + * wc_CompressionData object is left un-touched. + * + * @param data initialized wc_CompressionData object that set for compression + * @param out output buffer decompressed data will land in with space for + * decompressed data + * @param outSz size of output buffer + * @returns number of bytes written on success or negaitve error code otherwise + */ +WOLFSSL_API int wc_CompressionData_DeCompToBuf(const wc_CompressionData* data, + byte* out, word32 outSz); + +#endif /* WOLFSSL_HAVE_COMPRESSION_BACKEND */ + #ifdef __cplusplus } /* extern "C" */ #endif - -#endif /* HAVE_LIBZ */ #endif /* WOLF_CRYPT_COMPRESS_H */