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收到的握手数据不符合当前握手阶段的预期,可能是:
- 数据格式损坏、不完整
- 握手消息顺序错误
- 数据被截断或拼接错误
问题根源分析
- 握手循环逻辑不完整:当前代码仅判断了
SEC_E_INVALID_TOKEN,未处理SEC_I_CONTINUE_NEEDED(需发送握手数据到服务器)、SEC_E_INCOMPLETE_MESSAGE(需读取更多数据)等关键返回值,导致握手数据传递不完整,触发无效令牌错误。 - 缓冲区管理错误:
incoming缓冲区的received、used字段未正确更新,读取新数据时可能覆盖已有握手数据,或未将未处理的数据保留在缓冲区正确位置,导致后续解析失败。 - 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
相关产品推荐
相关产品推荐

