Skip to content

Potential mismatch in permit a valid encrypted record containing padding without application content #11086

Description

@LiD0209

Potential mismatch in permit a valid encrypted record containing padding without application content

Problem Description

Static triage found a plausible mismatch in wolfSSL's handling of the requirement to permit a valid encrypted record containing padding without application content. This candidate targets the baseline case, and the focused source-backed follow-up still leaves the concern unresolved.

This report deduplicates multiple candidate-level records that resolved to the same root cause.

Standard Requirement

  • Official standard: RFC 8446
  • Section: Appendix E Overview of Security Properties (lines 8274-8276)

as well as padding-only cover
traffic to conceal the difference between periods of transmission and
periods of silence.

Interpretation:

Applies an application requests padding-only cover traffic. The implementation must permit a valid encrypted record containing padding without application content. It must not require nonempty application content for every cover-traffic record.

Relevant Source Code

The reviewed wolfSSL tree contains adjacent TLS 1.3 machinery for this Appendix E rule, but the strict no_issue contract is still blocked by a broader security-property, deployment, build, or application-policy gap.

src/tls13.c:3290-3440

/* Build SSL Message, encrypted.
 * TLS v1.3 encryption is AEAD only.
 *
 * ssl         The SSL/TLS object.
 * output      The buffer to write record message to.
 * outSz       Size of the buffer being written into.
 * input       The record data to encrypt (excluding record header).
 * inSz        The size of the record data.
 * type        The recorder header content type.
 * hashOutput  Whether to hash the unencrypted record data.
 * sizeOnly    Only want the size of the record message.
 * asyncOkay   If non-zero can return WC_PENDING_E, otherwise blocks on crypto
 * returns the size of the encrypted record message or negative value on error.
 */
int BuildTls13Message(WOLFSSL* ssl, byte* output, int outSz, const byte* input,
                int inSz, int type, int hashOutput, int sizeOnly, int asyncOkay)
{
    int ret;
    BuildMsg13Args* args;
    BuildMsg13Args  lcl_args;

    WOLFSSL_ENTER("BuildTls13Message");

    if (ssl == NULL) {
        return BAD_FUNC_ARG;
    }

#ifdef WOLFSSL_ASYNC_CRYPT
    ret = WC_NO_PENDING_E;
    if (asyncOkay) {
        WOLFSSL_ASSERT_SIZEOF_GE(ssl->async->args, *args);

        if (ssl->async == NULL) {
            ssl->async = (struct WOLFSSL_ASYNC*)
                    XMALLOC(sizeof(struct WOLFSSL_ASYNC), ssl->heap,
                            DYNAMIC_TYPE_ASYNC);
            if (ssl->async == NULL)
                return MEMORY_E;
        }
        args = (BuildMsg13Args*)ssl->async->args;

        ret = wolfSSL_AsyncPop(ssl, &ssl->options.buildMsgState);
        if (ret != WC_NO_ERR_TRACE(WC_NO_PENDING_E)) {
            /* Check for error */
            if (ret < 0)
                goto exit_buildmsg;
        }
    }
    else
#endif
    {
        args = &lcl_args;
    }

    /* Reset state */
#ifdef WOLFSSL_ASYNC_CRYPT
    if (ret == WC_NO_ERR_TRACE(WC_NO_PENDING_E))
#endif
    {
        ret = 0;
        ssl->options.buildMsgState = BUILD_MSG_BEGIN;
        XMEMSET(args, 0, sizeof(BuildMsg13Args));

        args->headerSz = RECORD_HEADER_SZ;
#ifdef WOLFSSL_DTLS13
        if (ssl->options.dtls)
            args->headerSz = Dtls13GetRlHeaderLength(ssl, 1);
#endif /* WOLFSSL_DTLS13 */

        args->sz = args->headerSz + (word32)inSz;
        args->idx  = args->headerSz;

    #ifdef WOLFSSL_ASYNC_CRYPT
        if (asyncOkay)
            ssl->async->freeArgs = FreeBuildMsg13Args;
    #endif
    }

    switch (ssl->options.buildMsgState) {
        case BUILD_MSG_BEGIN:
        {
           /* catch mistaken sizeOnly parameter */
            if (sizeOnly) {
                if (output || input) {
                    WOLFSSL_MSG("BuildTls13Message with sizeOnly "
                                "doesn't need input or output");
                    return BAD_FUNC_ARG;
                }
            }
            else if (output == NULL || input == NULL) {
                return BAD_FUNC_ARG;
            }

            /* Record layer content type at the end of record data. */
            args->sz++;
            /* Authentication data at the end. */
            args->sz += ssl->specs.aead_mac_size;
#ifdef WOLFSSL_DTLS13
            /* Pad to minimum length */
            if (ssl->options.dtls &&
                    args->sz < (word32)Dtls13MinimumRecordLength(ssl)) {
                args->paddingSz = Dtls13MinimumRecordLength(ssl) - args->sz;
                args->sz = Dtls13MinimumRecordLength(ssl);
            }
#endif
            if (sizeOnly)
                return (int)args->sz;

            if (args->sz > (word32)outSz) {
                WOLFSSL_MSG("Oops, want to write past output buffer size");
                return BUFFER_E;
            }

            /* Record data length. */
            args->size = (word16)(args->sz - args->headerSz);
            /* Write/update the record header with the new size.
             * Always have the content type as application data for encrypted
             * messages in TLS v1.3.
             */

            if (ssl->options.dtls) {
#ifdef WOLFSSL_DTLS13
                Dtls13RlAddCiphertextHeader(ssl, output, args->size);
#endif /* WOLFSSL_DTLS13 */
            }
            else {
                AddTls13RecordHeader(output, args->size, application_data, ssl);
            }

            /* TLS v1.3 can do in place encryption. */
            if (input != output + args->idx)
                XMEMCPY(output + args->idx, input, (size_t)inSz);
            args->idx += (word32)inSz;

            ssl->options.buildMsgState = BUILD_MSG_HASH;
        }
        FALL_THROUGH;

        case BUILD_MSG_HASH:
        {
            if (hashOutput) {
                ret = HashOutput(ssl, output, (int)args->headerSz + inSz, 0);
                if (ret != 0)
                    goto exit_buildmsg;
            }

            /* The real record content type goes at the end of the data. */
            output[args->idx++] = (byte)type;
            /* Double check that any necessary padding is zero'd out */
            XMEMSET(output + args->idx, 0, args->paddingSz);
            args->idx += args->paddingSz;

TLS 1.3 record protection is AEAD-only, appends the real content type inside the ciphertext, and zero-fills any inner padding bytes.

src/internal.c:23636-23668

static int removeMsgInnerPadding(WOLFSSL* ssl)
{
    word32 i = ssl->buffers.inputBuffer.idx +
        ssl->curSize;
    if (ssl->specs.cipher_type == aead)
        i -= ssl->specs.aead_mac_size;
    else
        i -= ssl->keys.padSz + MacSize(ssl);

    /* check that the end of the logical length doesn't extend
     * past the real buffer */
    if (i > ssl->buffers.inputBuffer.length || i == 0) {
        WOLFSSL_ERROR(BUFFER_ERROR);
        return BUFFER_ERROR;
    }

    /* Remove padding from end of plain text. */
    for (--i; i > ssl->buffers.inputBuffer.idx; i--) {
        if (ssl->buffers.inputBuffer.buffer[i] != 0)
            break;
    }

    /* Get the real content type from the end of the data. */
    ssl->curRL.type = ssl->buffers.inputBuffer.buffer[i];
    if (ssl->curRL.type == 0) {
        SendAlert(ssl, alert_fatal, unexpected_message);
        WOLFSSL_ERROR(PARSE_ERROR);
        return PARSE_ERROR;
    }
    /* consider both contentType byte and MAC as padding */
    ssl->keys.padSz = ssl->buffers.inputBuffer.idx
        + ssl->curSize - i;
    return 0;

TLS 1.3 inner plaintext processing strips trailing zero padding and treats the final non-zero byte as the true content type.

Runtime Evidence

The runtime pass executed a source-backed positive control and a focused padding-path check. Both exited successfully. The positive control confirmed that wolfSSL can build and parse TLS 1.3 inner plaintext with zero-valued padding bytes. The focused check confirmed the internal padding mechanics but did not find an application-facing operation that emits an arbitrary-length padding-only cover record with no application content.

The observation leaves two interpretations open: the library may be missing a usable cover-traffic control, or the Appendix E security-property guidance may leave that behavior to the application. Because the run did not distinguish those cases, it does not constitute a confirmed protocol failure.

Inconsistency Reason

  • A plausible mismatch or under-specified library-vs-application responsibility remains after static review.

Decision Reason

  • Static triage identified a plausible mismatch or proof gap, and the source-backed positive control plus focused reproducer completed without closing the concern strongly enough to downgrade it to no_issue. The source-backed reproducer confirmed TLS 1.3 inner-padding mechanics, but did not close whether wolfSSL exposes an application-facing arbitrary-length cover-traffic control for this Appendix E requirement.

Remaining Uncertainty

  • A more focused source-backed reproducer or deployment-pinned check is still needed to distinguish a real implementation gap from a proof gap. The source-backed reproducer confirmed TLS 1.3 inner-padding mechanics, but did not close whether wolfSSL exposes an application-facing arbitrary-length cover-traffic control for this Appendix E requirement.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

No labels
No labels

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions