You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

SEC_E_INVALID_TOKEN错误成因及修复咨询(附Schannel代码)

关于Schannel代码偶发SEC_E_INVALID_TOKEN错误的问题与修复

问题描述

运行以下基于Schannel的C++代码时,偶发SEC_E_INVALID_TOKEN错误,仅在访问部分站点(如api.openai.com)时出现,重复请求有时正常有时报错。不清楚错误中“token”所指,也未显式传递相关token,需明确错误含义及修复方法。

#define WIN32_LEAN_AND_MEAN
#include <winsock2.h>
#include <windows.h>
#define SECURITY_WIN32
#include <security.h>
#include <schannel.h>
#include <shlwapi.h>
#include <assert.h>
#include <stdio.h>
#include <iostream>

#pragma comment (lib, "ws2_32.lib")
#pragma comment (lib, "secur32.lib")
#pragma comment (lib, "shlwapi.lib")

#define TLS_MAX_PACKET_SIZE (16384+512) // payload + extra over head for header/mac/padding (probably an overestimate)

typedef struct {
    SOCKET sock;
    CredHandle handle;
    CtxtHandle context;
    SecPkgContext_StreamSizes sizes;
    int received;    // byte count in incoming buffer (ciphertext)
    int used;        // byte count used from incoming buffer to decrypt current packet
    int available;   // byte count available for decrypted bytes
    char* decrypted; // points to incoming buffer where data is decrypted inplace
    char incoming[TLS_MAX_PACKET_SIZE];
} tls_socket;


int main() {
    const char* hostname = "api.openai.com";
    //const char* hostname = "badssl.com";
    //const char* hostname = "expired.badssl.com";
    //const char* hostname = "wrong.host.badssl.com";
    //const char* hostname = "self-signed.badssl.com";
    //const char* hostname = "untrusted-root.badssl.com";
    const char* path = "/";

    tls_socket s;
    // initialize windows sockets
    WSADATA wsadata;
    if (WSAStartup(MAKEWORD(2, 2), &wsadata) != 0) {
        return -1;
    }

    // create TCP IPv4 socket
    s.sock = socket(AF_INET, SOCK_STREAM, 0);
    if (s.sock == INVALID_SOCKET) {
        WSACleanup();
        return -1;
    }

    char sport[64];
    wnsprintfA(sport, sizeof(sport), "%u", 443);

    // connect to server
    if (!WSAConnectByNameA(s.sock, hostname, sport, NULL, NULL, NULL, NULL, NULL, NULL))
    {
        closesocket(s.sock);
        WSACleanup();
        return -7;
    }

    // initialize schannel
    {
        SCHANNEL_CRED cred {};
        cred
            .dwVersion = SCHANNEL_CRED_VERSION;
        cred    .dwFlags = SCH_USE_STRONG_CRYPTO          // use only strong crypto alogorithms
                     | SCH_CRED_AUTO_CRED_VALIDATION  // automatically validate server certificate
                     | SCH_CRED_NO_DEFAULT_CREDS;     // no client certificate authentication
            cred.grbitEnabledProtocols = SP_PROT_TLS1_2;  // allow only TLS v1.2
        

        if (AcquireCredentialsHandleA(NULL, UNISP_NAME_A, SECPKG_CRED_OUTBOUND, NULL, &cred, NULL, NULL, &s.handle, NULL) != SEC_E_OK)
        {
            closesocket(s.sock);
            WSACleanup();
            return -1;
        }
    }

    s.received = s.used = s.available = 0;
    s.decrypted = NULL;
    // perform tls handshake
    // 1) call InitializeSecurityContext to create/update schannel context
    // 2) when it returns SEC_E_OK - tls handshake completed
    // 3) when it returns SEC_I_INCOMPLETE_CREDENTIALS - server requests client certificate (not supported here)
    // 4) when it returns SEC_I_CONTINUE_NEEDED - send token to server and read data
    // 5) when it returns SEC_E_INCOMPLETE_MESSAGE - need to read more data from server
    // 6) otherwise read data from server and go to step 1

    CtxtHandle* context = NULL;
    int result = 0;
    for (;;) {
        SecBuffer inbuffers[2] = { 0 };
        inbuffers[0].BufferType = SECBUFFER_TOKEN;
        inbuffers[0].pvBuffer = s.incoming;
        inbuffers[0].cbBuffer = s.received;
        inbuffers[1].BufferType = SECBUFFER_EMPTY;

        SecBuffer outbuffers[1] = { 0 };
        outbuffers[0].BufferType = SECBUFFER_TOKEN;

        SecBufferDesc indesc = { SECBUFFER_VERSION, ARRAYSIZE(inbuffers), inbuffers };
        SecBufferDesc outdesc = { SECBUFFER_VERSION, ARRAYSIZE(outbuffers), outbuffers };

        DWORD flags = ISC_REQ_USE_SUPPLIED_CREDS | ISC_REQ_ALLOCATE_MEMORY | ISC_REQ_CONFIDENTIALITY | ISC_REQ_REPLAY_DETECT | ISC_REQ_SEQUENCE_DETECT | ISC_REQ_STREAM;
        SECURITY_STATUS sec = InitializeSecurityContextA(
            &s.handle,
            context,
            context ? NULL : (SEC_CHAR*)hostname,
            flags,
            0,
            0,
            context ? &indesc : NULL,
            0,
            context ? NULL : &s.context,
            &outdesc,
            &flags,
            NULL);
            if (sec == SEC_E_INVALID_TOKEN) std::cout << "failed\n";
        
    }
}

错误含义解释

这里的token并非OAuth等业务令牌,而是Schannel在TLS握手过程中交换的TLS握手协议报文(如ServerHello、Certificate、Finished等消息数据包)。SEC_E_INVALID_TOKEN表示Schannel收到的握手数据不符合当前握手阶段的预期,可能是:

  • 数据格式损坏、不完整
  • 握手消息顺序错误
  • 数据被截断或拼接错误

问题根源分析

  1. 握手循环逻辑不完整:当前代码仅判断了SEC_E_INVALID_TOKEN,未处理SEC_I_CONTINUE_NEEDED(需发送握手数据到服务器)、SEC_E_INCOMPLETE_MESSAGE(需读取更多数据)等关键返回值,导致握手数据传递不完整,触发无效令牌错误。
  2. 缓冲区管理错误:incoming缓冲区的received、used字段未正确更新,读取新数据时可能覆盖已有握手数据,或未将未处理的数据保留在缓冲区正确位置,导致后续解析失败。
  3. TLS版本兼容性问题:代码强制仅使用TLS1.2,部分站点(如api.openai.com)可能优先协商TLS1.3,导致握手协商过程出现不兼容情况,触发错误。

修复方案

1. 完善握手循环的状态处理

补充对不同返回值的处理逻辑,确保握手数据的发送和接收流程完整:

CtxtHandle* context = NULL;
int result = 0;
for (;;) {
    SecBuffer inbuffers[2] = { 0 };
    inbuffers[0].BufferType = SECBUFFER_TOKEN;
    // 指向未使用的缓冲区数据,避免重复处理已消费的内容
    inbuffers[0].pvBuffer = s.incoming + s.used;
    inbuffers[0].cbBuffer = s.received - s.used;
    inbuffers[1].BufferType = SECBUFFER_EMPTY;

    SecBuffer outbuffers[1] = { 0 };
    outbuffers[0].BufferType = SECBUFFER_TOKEN;

    SecBufferDesc indesc = { SECBUFFER_VERSION, ARRAYSIZE(inbuffers), inbuffers };
    SecBufferDesc outdesc = { SECBUFFER_VERSION, ARRAYSIZE(outbuffers), outbuffers };

    DWORD flags = ISC_REQ_USE_SUPPLIED_CREDS | ISC_REQ_ALLOCATE_MEMORY | ISC_REQ_CONFIDENTIALITY | ISC_REQ_REPLAY_DETECT | ISC_REQ_SEQUENCE_DETECT | ISC_REQ_STREAM;
    SECURITY_STATUS sec = InitializeSecurityContextA(
        &s.handle,
        context,
        context ? NULL : (SEC_CHAR*)hostname,
        flags,
        0,
        0,
        context ? &indesc : NULL,
        0,
        context ? NULL : &s.context,
        &outdesc,
        &flags,
        NULL);

    if (sec == SEC_E_OK) {
        // 握手完成,发送最后一段握手数据(如果有)
        if (outbuffers[0].cbBuffer > 0) {
            send(s.sock, (const char*)outbuffers[0].pvBuffer, outbuffers[0].cbBuffer, 0);
            FreeContextBuffer(outbuffers[0].pvBuffer);
        }
        break;
    } else if (sec == SEC_I_CONTINUE_NEEDED) {
        // 发送生成的握手数据到服务器
        if (outbuffers[0].cbBuffer > 0) {
            send(s.sock, (const char*)outbuffers[0].pvBuffer, outbuffers[0].cbBuffer, 0);
            FreeContextBuffer(outbuffers[0].pvBuffer);
        }
        // 读取服务器的握手响应,追加到缓冲区末尾
        int bytesRead = recv(s.sock, s.incoming + s.received, TLS_MAX_PACKET_SIZE - s.received, 0);
        if (bytesRead <= 0) {
            // 处理读取错误,清理资源
            closesocket(s.sock);
            WSACleanup();
            DeleteSecurityContext(&s.context);
            FreeCredentialsHandle(&s.handle);
            return -1;
        }
        s.received += bytesRead;
        context = &s.context;
    } else if (sec == SEC_E_INCOMPLETE_MESSAGE) {
        // 缓冲区数据不足,读取更多数据追加到缓冲区
        int bytesRead = recv(s.sock, s.incoming + s.received, TLS_MAX_PACKET_SIZE - s.received, 0);
        if (bytesRead <= 0) {
            closesocket(s.sock);
            WSACleanup();
            DeleteSecurityContext(&s.context);
            FreeCredentialsHandle(&s.handle);
            return -1;
        }
        s.received += bytesRead;
    } else if (sec == SEC_E_INVALID_TOKEN) {
        std::cout << "握手数据无效:格式错误或不完整" << std::endl;
        // 清理资源后退出
        closesocket(s.sock);
        WSACleanup();
        if (context) DeleteSecurityContext(&s.context);
        FreeCredentialsHandle(&s.handle);
        return -1;
    } else {
        std::cout << "握手失败,错误码:" << sec << std::endl;
        closesocket(s.sock);
        WSACleanup();
        if (context) DeleteSecurityContext(&s.context);
        FreeCredentialsHandle(&s.handle);
        return -1;
    }
}

2. 优化缓冲区管理

处理完部分握手数据后,将未使用的数据移动到缓冲区开头,避免后续读取数据时出现重叠:

// 当握手消息被消费后,更新缓冲区状态
if (inbuffers[0].cbBuffer > 0) {
    s.used += inbuffers[0].cbBuffer;
    // 如果已处理完所有数据,重置缓冲区
    if (s.used == s.received) {
        s.received = s.used = 0;
    } else {
        // 将未使用的数据移动到缓冲区开头
        memmove(s.incoming, s.incoming + s.used, s.received - s.used);
        s.received -= s.used;
        s.used = 0;
    }
}

3. 放宽TLS版本限制

修改SCHANNEL_CRED的协议版本设置,同时支持TLS1.2和TLS1.3,提升兼容性:

cred.grbitEnabledProtocols = SP_PROT_TLS1_2 | SP_PROT_TLS1_3;

4. 完善资源清理

确保在所有分支中都正确释放Schannel的上下文和凭据资源,避免内存泄漏。


内容的提问来源于stack exchange,提问作者Logos King

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.31 20:15:51