0

我搜索了 StackOverflow 试图找到类似的问题,但没有遇到过,所以我发布了这个问题。

我正在尝试使用 Microsoft 的 SChannel 库编写一个 C++ HTTPS 客户端,并且在分块消息传输时遇到随机错误。这个问题似乎只发生在很长的下载中——短的下载通常可以正常工作。大多数情况下,代码都能正常工作——即使是长时间下载——但偶尔 recv() 命令会优雅地超时,断开我的 TLS 会话,在其他时候,我会收到一个不完整的最后一个数据包。随机错误似乎是服务器用于传递数据的不同大小的块和加密块的结果。我知道我需要处理这种变化,但是虽然这在未加密的 HTTP 连接上很容易解决,但加密方面给我带来了问题。

首先是超时问题,大约 5% 的时间我请求大型 HTTP 请求(来自单个 HTTP GET 请求的大约 10 MB 数据)。

超时是因为在最后一个块上我指定了比阻塞套接字上剩余的数据更大的接收缓冲区。对此的明显解决方法是只请求下一个块所需的字节数,这就是我所做的。但是由于某种原因,从每个请求中收到的数量少于我的请求,但解密后似乎没有丢失任何数据。我猜这一定是由于数据流中的一些压缩,但我不知道。无论如何,如果它使用压缩,我不知道如何将解密的未压缩字节流的大小转换为压缩的加密字节流的大小,包括加密头和尾,以请求准确的字节数。谁能帮我做到这一点?

另一种方法是让我连续查找两个 CR+LF,这也表示 HTTPS 响应的结束。但是由于数据是加密的,我无法弄清楚如何逐字节查看。SChannel 的 DecryptMessage() 似乎是按块进行解密,而不是逐字节进行。该论坛中的任何人都可以就如何逐字节解密以使我能够查找分块输出的结尾提供任何建议吗?

第二个问题是 DecryptMessage 有时会错误地认为它在我到达消息的实际结尾之前完成了解密。结果行为是我继续下一个 HTTP 请求,并且我得到了前一个响应的其余部分,我希望看到新请求的标头。

显而易见的解决方案是检查解密消息的内容,看看我们是否真的到达了终点,如果没有,在发送下一个 HTTP 请求之前尝试接收更多数据。但是当我这样做并尝试解密时,我会收到一条解密错误消息。

任何人都可以就策略提供的任何建议/帮助将不胜感激。我已经附上了 HTTP 正文的读取/解密过程的相关代码部分——我不包括标题读取和解析,因为它可以正常工作。

    do
    {
        // Note this receives large files OK, but I can't tell when I hit the end of the buffer, and this
        // hangs.  Need to consider a non-blocking socket? 

//      numBytesReceived = recv(windowsSocket, (char*)inputBuffer, inputBufSize, 0);
        m_ErrorLog << "Next read size expected " << nextReadSize << endl;
        numBytesReceived = recv(windowsSocket, (char*)inputBuffer, nextReadSize, 0);
        m_ErrorLog << "NumBytesReceived = " << numBytesReceived << endl;
        if (m_BinaryBufLen + numBytesReceived > m_BinaryBufAllocatedSize)
            ::EnlargeBinaryBuffer(m_BinaryBuffer,m_BinaryBufAllocatedSize,m_BinaryBufLen,numBytesReceived+1);
        memcpy(m_BinaryBuffer+m_BinaryBufLen,inputBuffer,numBytesReceived);
        m_BinaryBufLen += numBytesReceived;

        lenStartDecryptedChunk = decryptedBodyLen;

        do
        {
            // Decrypt the received data. 

            Buffers[0].pvBuffer     = m_BinaryBuffer;
            Buffers[0].cbBuffer     = m_BinaryBufLen;
            Buffers[0].BufferType   = SECBUFFER_DATA;  // Initial Type of the buffer 1
            Buffers[1].BufferType   = SECBUFFER_EMPTY; // Initial Type of the buffer 2 
            Buffers[2].BufferType   = SECBUFFER_EMPTY; // Initial Type of the buffer 3 
            Buffers[3].BufferType   = SECBUFFER_EMPTY; // Initial Type of the buffer 4 

            Message.ulVersion       = SECBUFFER_VERSION;    // Version number
            Message.cBuffers        = 4;                                    // Number of buffers - must contain four SecBuffer structures.
            Message.pBuffers        = Buffers;                        // Pointer to array of buffers
            scRet = m_pSSPI->DecryptMessage(phContext, &Message, 0, NULL);
            if (scRet == SEC_E_INCOMPLETE_MESSAGE)
                break;
            if( scRet == SEC_I_CONTEXT_EXPIRED )
            {
                m_ErrorLog << "Server shut down connection before I finished reading" << endl;
                m_ErrorLog << "# of Bytes Requested = " << nextReadSize << endl;
                m_ErrorLog << "# of Bytes received = " << numBytesReceived << endl;
                m_ErrorLog << "Decrypted data to this point = " << endl;
                m_ErrorLog << decryptedBody << endl;
                m_ErrorLog << "BinaryData just decrypted: " << endl;
                m_ErrorLog << Buffers[0].pvBuffer << endl;
                break; // Server signalled end of session
            }
            if( scRet != SEC_E_OK && 
                scRet != SEC_I_RENEGOTIATE && 
                scRet != SEC_I_CONTEXT_EXPIRED ) 
            { 
                DisplaySECError((DWORD)scRet,errmsg);
                m_ErrorLog << "CSISPDoc::ReadDecrypt(): " << "Failed to decrypt message--Error=" << errmsg;
                if (decryptedBody)
                    m_ErrorLog << decryptedBody << endl;
                return scRet; 
            }
            // Locate data and (optional) extra buffers.

            pDataBuffer  = NULL;
            pExtraBuffer = NULL;
            for(i = 1; i < 4; i++)
            {
                if( pDataBuffer  == NULL && Buffers[i].BufferType == SECBUFFER_DATA  ) 
                    pDataBuffer  = &Buffers[i];
                if( pExtraBuffer == NULL && Buffers[i].BufferType == SECBUFFER_EXTRA ) 
                    pExtraBuffer = &Buffers[i];
            }

            // Display the decrypted data.

            if(pDataBuffer)
            {
                length = pDataBuffer->cbBuffer;
                if( length ) // check if last two chars are CR LF
                {
                    buff = (PBYTE)pDataBuffer->pvBuffer; // printf( "n-2= %d, n-1= %d \n", buff[length-2], buff[length-1] );
                    if (decryptedBodyLen+length+1 > decryptedBodyAllocatedSize)
                        ::EnlargeBuffer(decryptedBody,decryptedBodyAllocatedSize,decryptedBodyLen,length+1);
                    memcpy_s(decryptedBody+decryptedBodyLen,decryptedBodyAllocatedSize-decryptedBodyLen,buff,length);
                    decryptedBodyLen += length;
                    m_ErrorLog << buff << endl;
                }
            }

            // Move any "extra" data to the input buffer -- this has not yet been decrypted.

            if(pExtraBuffer)
            {
                MoveMemory(m_BinaryBuffer, pExtraBuffer->pvBuffer, pExtraBuffer->cbBuffer);
                m_BinaryBufLen = pExtraBuffer->cbBuffer; // printf("inputStrLen= %d  \n", inputStrLen);
            }
        }
        while (pExtraBuffer); 


        if (decryptedBody)
        {
            if (incompletePacket)
                p1 = decryptedBody + lenStartFragmentedPacket;
            else
                p1 = decryptedBody + lenStartDecryptedChunk;
            p2 = p1;
            pEndDecryptedBody = decryptedBody+decryptedBodyLen;

            if (lastDecryptRes != SEC_E_INCOMPLETE_MESSAGE)
                chunkSizeBlock = true;

            do
            {
                while (p2 < pEndDecryptedBody && (*p2 != '\r' || *(p2+1) != '\n'))
                    p2++;

                // if we're here, we probably found the end of the current line.  The pattern we are
                // reading is chunk length, chunk, chunk length, chunk,...,chunk lenth (==0)

                if (*p2 == '\r' && *(p2+1) == '\n') // new line character -- found chunk size
                {
                    if (chunkSizeBlock) // reading the size of the chunk
                    {
                        pStartHexNum = SkipWhiteSpace(p1,p2);
                        pEndHexNum = SkipWhiteSpaceBackwards(p1,p2);
                        chunkSize = HexCharToInt(pStartHexNum,pEndHexNum);
                        p2 += 2; // skip past the newline character
                        chunkSizeBlock = false;
                        if (!chunkSize)  // chunk size of 0 means we're done
                        {
                            bulkReadDone = true;
                            p2 += 2;  // skip past the final CR+LF
                        }
                        nextReadSize = chunkSize+8; // chunk + CR/LF + next chunk size (4 hex digits) + CR/LF + encryption header/trailer
                    }
                    else // copy the actual chunk
                    {
                        if (p2-p1 != chunkSize)
                        {
                            m_ErrorLog << "Warning: Actual chunk size of " << p2 - p1 << " != stated chunk size = " << chunkSize << endl;
                        }
                        else
                        {
                        // copy over the actual chunk data // 
                            if (m_HTTPBodyLen + chunkSize > m_HTTPBodyAllocatedSize)
                                ::EnlargeBuffer(m_HTTPBody,m_HTTPBodyAllocatedSize,m_HTTPBodyLen,chunkSize+1);
                            memcpy_s(m_HTTPBody+m_HTTPBodyLen,m_HTTPBodyAllocatedSize,p1,chunkSize);
                            m_HTTPBodyLen += chunkSize;
                            m_HTTPBody[m_HTTPBodyLen] = 0;  // null-terminate
                            p2 += 2; // skip over chunk and end of line characters
                            chunkSizeBlock = true;
                            chunkSize = 0;
                            incompletePacket = false;
                            lenStartFragmentedPacket = 0;
                        }
                    }
                    p1 = p2; // move to start of next chunk field
                }
                else // got to end of encrypted body with no CR+LF found --> fragmeneted chunk.  So we need to read and decrypt at least one more chunk 
                {
                    incompletePacket = true;
                    lenStartFragmentedPacket = p1-decryptedBody;
                }
            }   
            while (p2 < pEndDecryptedBody);
            lastDecryptRes = scRet;
        }
    }
    while (scRet == SEC_E_INCOMPLETE_MESSAGE && !bulkReadDone);
4

2 回答 2

0

TLS 不支持逐字节解密。

TLS 1.2 将其输入分成最大 16 kiB 的块,然后将它们加密为稍大的密文块,因为需要加密 IV/nonce 和完整性保护标签/MAC。在整个块可用之前,不可能解密一个块。您可以在https://www.rfc-editor.org/rfc/rfc5246#section-6.2找到完整的详细信息。

由于您已经能够解密前几个块(包含标头),因此您应该能够读取 HTTP 长度,以便您至少知道您期望的明文长度,然后您可以将其与数字进行比较您从流中解密的字节数。但是,这不会告诉您需要多少字节的密文——您可以通过调用获得片段大小的上限m_pSPPI->QueryContextAttributes(),然后在尝试读取之前至少读取该字节数或直到流结束解密。

您是否尝试过查看其他示例? http://www.coastrd.com/c-schannel-smtp似乎包含基于 SChannel 的 TLS 客户端的详细示例。

于 2020-04-14T02:15:51.920 回答
0

我终于能够弄清楚这一点。我通过解密每个 TCP/IP 数据包来解决此问题,以检查解密数据包中的 CR+LF+CR+LF,而不是我一直在做的事情——尝试将所有加密数据包合并到一个缓冲区中解密它。

关于“挂起”问题,我认为发生的事情是 recv() 没有返回,因为实际接收的数据量小于我预期的接收大小。但实际上发生的事情是我实际上已经收到了整个传输,但我没有意识到这一点。因此,当实际上没有更多数据要接收时,我进行了额外的 recv() 调用。没有更多数据要接收的事实是导致连接超时(导致“挂起”)的原因。

出现截断问题是因为我无法检测到加密流中的 CR+LF+CR+LF 序列,并且我错误地认为 SChannel 仅在处理整个响应时才在 DecryptMessage() 上返回 SEC_E_OK。

一旦我能够通过零碎与批量解密来检测消息的真正结尾,这两个问题都被消除了。

为了弄清楚这一点,我不得不完全重构来自www.coastRD.com的示例 SChannel 代码。虽然www.coastRD.com代码通常非常有用,但它是为 SMTP 传输编写的,而不是分块 HTTP 编码。此外,它的编写方式很难遵循处理消息接收和处理方式变化的逻辑。最后,我花了很多时间“破解” Schannel 以了解它的行为方式以及在哪些条件下返回哪些代码,因为不幸的是,在任何 Microsoft 文档(我已经看到)中都没有讨论这些内容。

我需要了解的第一件事是 SChannel 如何尝试解密消息。在 Schannel 中,加密消息的前 13 个字节是加密头,最后 16 个字节是加密尾。我仍然不知道预告片做了什么,但我确实意识到加密标头从未真正加密/解密。第一个 5 字节只是“应用程序数据”的 TLS 记录头(十六进制代码 0x17),后面是定义使用的 TLS 版本的两个字节,后面是 TLS 记录片段大小的 2 个字节,后面是前导 0 和一个字节我还没有想通。

这很重要的原因是 DecryptMessage() 仅在记录类型为“应用程序数据”时才有效。对于任何其他记录类型(例如 TLS 握手“完成的消息”),DecryptMessage() 甚至都不会尝试解密它——它只会返回一个 SEC_E_DECRYPT_FAILURE 代码。

此外,我需要了解,在使用分块传输编码时,DecryptMessage() 通常无法一次性解密接收缓冲区的全部内容。为了成功处理接收缓冲区的全部内容和服务器 HTTPS 响应的其余部分,我需要了解 DecryptMessage() 的两个关键返回码——SEC_E_OK 和 SEC_E_INCOMPLETE_MESSAGE。

当我收到 SEC_E_OK 时,这意味着 DecryptMessage() 能够成功解密至少部分接收缓冲区。发生这种情况时,第一个 13 个字节(加密标头)保持不变。但是,紧跟在标头后面的字节被就地解密,然后是加密尾端(也没有改变)。通常,在加密尾部结束后,接收缓冲区中仍有额外的加密数据,这些数据也没有变化。

由于我使用的是 www.coastRD.com 代码中描述的 SecBufferDesc 输出缓冲区结构和 4 个 SecBuffer 结构,因此我需要了解这些实际上并不是 4 个单独的缓冲区——它们只是指向接收缓冲区中不同位置的指针。第一个缓冲区是指向加密头的指针。第二个缓冲区是指向解密数据开头的指针。第三个缓冲区是指向加密尾部开始的指针。最后,第 4 个缓冲区是指向 DecryptMessage() 在上次调用时无法处理的“额外”加密数据的指针。

一旦我弄清楚了,我意识到我需要将解密的数据(第二个缓冲区中的指针)复制到一个单独的缓冲区中,因为稍后可能会覆盖接收缓冲区。

如果第 4 个缓冲区中没有“额外”数据,我暂时就完成了——但这是例外而不是规则。

如果有额外的数据(通常情况下),我需要将该数据向前移动到接收缓冲区的最开始,并且我需要再次调用 DecryptMessage()。这解密了下一个块,我将该数据附加到我已经复制到单独缓冲区的数据中,并重复此过程,直到接收缓冲区中没有更多数据可供解密,或者我收到 SEC_E_INCOMPLETE_MESSAGE。

如果我收到 SEC_E_INCOMPLETE_MESSAGE,则接收缓冲区中剩余的数据没有改变。它没有被解密,因为它是一个不完整的加密块。因此,我需要再次调用 recv() 从服务器获取更多加密数据以完成加密块。

一旦发生这种情况,我将新收到的数据附加到接收缓冲区。我将它附加到接收缓冲区的内容而不是覆盖它,因为后一种方法会覆盖加密块的开头,在我下次调用 DecryptMessage() 时产生 SEC_E_DECRYPT_FAILURE 消息。

一旦我将这个新数据块附加到接收缓冲区,我重复上述步骤以解密接收缓冲区的内容,并继续重复整个过程,直到我收到关于接收中最后一块数据的 SEC_E_OK 消息缓冲。

但我还不一定完成——服务器可能仍在发送数据。在这一点停止是导致我偶尔遇到的截断问题的原因。

所以我现在检查了解密数据的最后 4 个字节以查找 CR+LF+CR+LF。如果我找到了那个序列,我就知道我已经收到并解密了一个完整的 HTTPS 响应。

但是如果我没有,我需要再次调用 recv() 并重复上面的过程,直到我看到数据末尾的 CR+LF+FR+LF 序列。

一旦我实施了这个过程,我就能够明确地确定加密的 HTTPS 响应的结束,这可以防止我在没有剩余数据时进行不必要的 recv() 调用,防止“挂起”,以及过早截断响应.

对于冗长的答案,我深表歉意,但鉴于缺乏关于 SChannel 及其 DecryptMessage() 等函数的文档,我认为对我所学知识的描述可能对其他可能也在努力使用 SChannel 处理 TLS HTTP 响应的人有所帮助.

再次感谢 user3553031 在 7 个多月前试图帮助我解决这个问题——这些尝试帮助我缩小了问题的范围。

于 2020-12-02T02:00:38.893 回答