如何使用SSPI通过OpenSSL与TCP服务器完成认证?
看起来你在尝试用Windows SSPI的Schannel组件编写TLS客户端,连接OpenSSL搭建的TCP服务器时,卡在了第一次调用InitializeSecurityContext就返回SEC_E_INVALID_TOKEN的问题上,我来帮你梳理下问题根源和解决方向。
首先,你的代码里藏着一个致命的低级错误
你在定义SecBufferDesc(安全缓冲区描述符)的时候,犯了一个很容易忽略的参数错误:
SecBufferDesc indesc = {SECBUFFER_VERSION, sizeof(inbuffers), inbuffers}; SecBufferDesc outdesc = {SECBUFFER_VERSION, sizeof(outbuffers), outbuffers};
SecBufferDesc的第二个成员cBuffers代表的是缓冲区的数量,而不是缓冲区数组的字节大小!你现在传递的sizeof(inbuffers)是2 * sizeof(SecBuffer)的字节数(比如32字节左右),但正确的值应该是缓冲区的个数:2(对应inbuffers里的2个元素)和1(对应outbuffers里的1个元素)。
这个错误会让Schannel无法正确解析你传入的缓冲区配置,直接返回SEC_E_INVALID_TOKEN。修正后的代码应该是这样的:
SecBufferDesc indesc = {SECBUFFER_VERSION, 2, inbuffers}; SecBufferDesc outdesc = {SECBUFFER_VERSION, 1, outbuffers};
接下来,你需要遵循SSPI TLS的完整握手流程
即使修正了上面的错误,你当前的代码也只走了第一步的开头,完整的TLS握手需要多轮InitializeSecurityContext调用,流程大概是这样的:
第一次调用
InitializeSecurityContext:- 这时候你还没收到服务器的任何数据,输入缓冲区保持空状态即可
- 调用成功后会返回
SEC_I_CONTINUE_NEEDED,同时outbuffers里会生成TLS的ClientHello数据 - 记得后续用
FreeContextBuffer释放SSPI自动分配的内存(因为你加了ISC_REQ_ALLOCATE_MEMORY标记)
发送ClientHello并接收服务器响应:
- 把
outbuffers[0]里的ClientHello数据通过socket发送给服务器 - 接收服务器返回的TLS响应包(里面包含ServerHello、服务器证书、ServerHelloDone等内容)
- 把接收到的完整数据填充到输入缓冲区的
SECBUFFER_TOKEN字段中
- 把
循环调用直到握手完成:
- 后续调用
InitializeSecurityContext时,要传入已经创建好的ctx上下文句柄 - 每次调用后,如果返回
SEC_I_CONTINUE_NEEDED,就把输出的token发送给服务器,再接收新的响应输入 - 直到调用返回
SEC_E_OK,这就代表TLS握手正式完成了
- 后续调用
还有几个细节要注意
- 证书名称匹配:
InitializeSecurityContext的第三个参数(SEC_CHAR*)HOST,必须和服务器证书里的CN(通用名称)或者SAN(主题备用名称)完全一致。如果服务器用的是IP地址签发的证书,这里填IP;如果是域名证书,就填对应的域名,不然后续会触发证书验证失败。 - TLS版本兼容:你在
SCHANNEL_CRED里指定了只允许TLS1.2,要确认你的OpenSSL服务器确实启用了TLS1.2,没有把这个版本禁用掉。 - 流模式适配:你设置了
ISC_REQ_STREAM标记,Schannel的流模式会自动处理TLS消息的边界拆分,不用你手动解析TLS记录头,这和OpenSSL的字节流服务器是兼容的,但要确保socket的收发操作能拿到完整的TLS数据包。
先把缓冲区描述符的错误修正,你应该就能让第一次InitializeSecurityContext调用返回正常的SEC_I_CONTINUE_NEEDED,之后按照上面的流程一步步走,就能完成和OpenSSL服务器的TLS握手啦。
备注:内容来源于stack exchange,提问作者m3sfit

