Feat: One-way TLS transport for the MFC server
Add optional one-way TLS to the MFC server (server/2015Remote) so the device port can serve both TLS clients and legacy plaintext clients on the same port, mirroring the Go server. Server: on a connection's first bytes, sniff for a TLS ClientHello (0x16 0x03 + handshake type 0x01). When [tls] Enabled=1 and the identity loads, hand the socket to a short-lived blocking handshake thread (Option A) running Schannel AcceptSecurityContext, then decrypt on recv and encrypt on send; plaintext connections replay the sniffed bytes straight into the existing parse path, so the plaintext code path is otherwise unchanged. The identity is loaded once from [tls] CertPath/KeyPath as PEM + CNG (PKCS#8) — the same key pair the Go server uses — and a missing [tls] section, an empty value, or a load failure leaves plaintext serving intact and fail-closes any TLS ClientHello. Also fix two transport-reliability bugs that surfaced end-to-end: - send path: encrypt only the freshly appended plaintext tail and post a single IOWrite wake-up guarded by a bSendPending flag, so back-to-back small sends (terminal resize -> COMMAND_NEXT -> resize) no longer run concurrent WSASends over the same mutable buffer (client saw DecryptMessage failures, with WSAGetLastError() = 6001 / ERROR_DECRYPTION_FAILED); - client Recv: keep the handshake's unconsumed tail bytes in m_raw and drain them before the next recv, and log the real SECURITY_STATUS on DecryptMessage failure instead of the stale Winsock error. Co-Authored-By: deepseek-v4-pro
This commit is contained in:
1 parent
ca0d7626d2
commit
96b9eea70a
16 files changed
+1298
-38
No files matched your search
@@ -0,0 +1,604 @@
|
||||
// ServerTlsSession.cpp — 服务端单向 TLS(Schannel 入站)实现
|
||||
//
|
||||
// 与客户端 TlsChannel.cpp 对称,但走 AcceptSecurityContext 入站握手、DecryptMessage/
|
||||
// EncryptMessage 流式加解密。证书/私钥加载为 PEM + CNG(NCryptImportKey PKCS#8),
|
||||
// 以便与 Go 服务端(server/go 的 crypto/tls,YAMA_TLS_CERT/KEY)共用同一对密钥。
|
||||
#include "StdAfx.h"
|
||||
#include "ServerTlsSession.h"
|
||||
#include "Server.h" // CONTEXT_OBJECT::FreeTls()
|
||||
#include "common/iniFile.h" // config(GetInt/GetStr)
|
||||
|
||||
#include <ncrypt.h>
|
||||
#include <bcrypt.h>
|
||||
|
||||
#include <fstream>
|
||||
#include <iterator>
|
||||
#include <sstream>
|
||||
|
||||
#pragma comment(lib, "secur32.lib")
|
||||
#pragma comment(lib, "crypt32.lib")
|
||||
#pragma comment(lib, "ncrypt.lib")
|
||||
|
||||
namespace
|
||||
{
|
||||
// 进程级身份(进程启动加载一次;未来 UI/指令热切换时改为引用计数 + 惰性释放)。
|
||||
std::mutex g_identityMtx;
|
||||
bool g_enabled = false;
|
||||
bool g_ready = false;
|
||||
bool g_loadError = false;
|
||||
CredHandle g_cred = {};
|
||||
bool g_credValid = false;
|
||||
PCCERT_CONTEXT g_cert = NULL; // 与入站凭据同生命周期,FreeIdentity 时释放
|
||||
NCRYPT_KEY_HANDLE g_persistedKey = 0; // 已持久化到 CNG KSP 的私钥句柄,FreeIdentity 时删除
|
||||
|
||||
// 持久化私钥在 CNG KSP 中的名字。用固定名 + NCRYPT_OVERWRITE_KEY_FLAG,重启时覆盖旧键,
|
||||
// 避免残留;同名键的密钥材料与 server.key 相同,覆盖无副作用。
|
||||
const wchar_t* g_keyName = L"YamaServerTlsKey";
|
||||
|
||||
// 一次性读文件为 std::string(二进制安全)。
|
||||
bool ReadFileText(const std::string& path, std::string& out)
|
||||
{
|
||||
std::ifstream f(path, std::ios::binary);
|
||||
if (!f.is_open())
|
||||
return false;
|
||||
std::ostringstream ss;
|
||||
ss << f.rdbuf();
|
||||
if (f.bad())
|
||||
return false;
|
||||
out = ss.str();
|
||||
return true;
|
||||
}
|
||||
|
||||
// PEM(-----BEGIN ...----- 包裹的 base64)解码为 DER。失败返回 false。
|
||||
// CRYPT_STRING_ANY 会自动识别带头的 base64(PEM),对证书与 PKCS#8 私钥均适用。
|
||||
bool PemToDer(const std::string& pem, std::vector<BYTE>& der)
|
||||
{
|
||||
if (pem.empty())
|
||||
return false;
|
||||
DWORD need = 0;
|
||||
if (!CryptStringToBinaryA(pem.data(), (DWORD)pem.size(), CRYPT_STRING_ANY,
|
||||
NULL, &need, NULL, NULL))
|
||||
return false;
|
||||
der.resize(need);
|
||||
return CryptStringToBinaryA(pem.data(), (DWORD)pem.size(), CRYPT_STRING_ANY,
|
||||
der.data(), &need, NULL, NULL) == TRUE;
|
||||
}
|
||||
|
||||
// 从 PEM 文本里定位私钥块的类型,用于给出可操作的错误提示。
|
||||
enum KeyFmt { KF_PKCS8, KF_PKCS1, KF_UNKNOWN };
|
||||
KeyFmt DetectKeyFormat(const std::string& keyPem)
|
||||
{
|
||||
if (keyPem.find("-----BEGIN PRIVATE KEY-----") != std::string::npos)
|
||||
return KF_PKCS8;
|
||||
if (keyPem.find("-----BEGIN RSA PRIVATE KEY-----") != std::string::npos ||
|
||||
keyPem.find("-----BEGIN EC PRIVATE KEY-----") != std::string::npos)
|
||||
return KF_PKCS1;
|
||||
return KF_UNKNOWN;
|
||||
}
|
||||
}
|
||||
|
||||
// =====================================================================================
|
||||
// ServerTlsIdentity —— 进程级服务端身份(可重入加载,禁止 call_once,供未来热切换)
|
||||
// =====================================================================================
|
||||
|
||||
void ServerTlsIdentity::FreeIdentity()
|
||||
{
|
||||
// 先释放凭据,再释放证书上下文,最后删除持久化私钥(凭据/证书可能仍按名引用该键)。
|
||||
if (g_credValid)
|
||||
{
|
||||
FreeCredentialsHandle(&g_cred);
|
||||
g_credValid = false;
|
||||
}
|
||||
g_cred = {};
|
||||
if (g_cert)
|
||||
{
|
||||
CertFreeCertificateContext(g_cert);
|
||||
g_cert = NULL;
|
||||
}
|
||||
if (g_persistedKey)
|
||||
{
|
||||
NCryptDeleteKey(g_persistedKey, 0); // 删除并释放句柄
|
||||
g_persistedKey = 0;
|
||||
}
|
||||
}
|
||||
|
||||
bool ServerTlsIdentity::Load(config& cfg)
|
||||
{
|
||||
std::lock_guard<std::mutex> lk(g_identityMtx);
|
||||
|
||||
bool enabled = cfg.GetInt("tls", "Enabled", 0) != 0;
|
||||
std::string certPath = cfg.GetStr("tls", "CertPath", "");
|
||||
std::string keyPath = cfg.GetStr("tls", "KeyPath", "");
|
||||
|
||||
g_enabled = enabled;
|
||||
|
||||
// 先释放旧身份(本轮只在启动时调用一次,不存在「旧身份仍被并发握手使用」的问题;
|
||||
// 未来热切换需先引用计数、再惰性释放旧凭据)。
|
||||
FreeIdentity();
|
||||
g_ready = false;
|
||||
g_loadError = false;
|
||||
|
||||
if (!enabled)
|
||||
{
|
||||
Mprintf("[TLS] server TLS disabled ([tls] Enabled=0) — plaintext only\n");
|
||||
return true;
|
||||
}
|
||||
|
||||
if (certPath.empty() || keyPath.empty())
|
||||
{
|
||||
g_loadError = true;
|
||||
Mprintf("[TLS] FATAL: [tls] Enabled=1 but CertPath/KeyPath empty; "
|
||||
"refusing TLS ClientHello (fail-closed)\n");
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!LoadIdentity(certPath, keyPath))
|
||||
{
|
||||
g_loadError = true;
|
||||
return false;
|
||||
}
|
||||
|
||||
g_ready = true;
|
||||
Mprintf("[TLS] server TLS enabled: cert=%s key=%s\n", certPath.c_str(), keyPath.c_str());
|
||||
return true;
|
||||
}
|
||||
|
||||
bool ServerTlsIdentity::LoadIdentity(const std::string& certPath, const std::string& keyPath)
|
||||
{
|
||||
std::string certPem, keyPem;
|
||||
if (!ReadFileText(certPath, certPem))
|
||||
{
|
||||
Mprintf("[TLS] FATAL: cannot read cert file %s\n", certPath.c_str());
|
||||
return false;
|
||||
}
|
||||
if (!ReadFileText(keyPath, keyPem))
|
||||
{
|
||||
Mprintf("[TLS] FATAL: cannot read key file %s\n", keyPath.c_str());
|
||||
return false;
|
||||
}
|
||||
|
||||
// 1) 证书:PEM → DER → PCCERT_CONTEXT。
|
||||
std::vector<BYTE> certDer;
|
||||
if (!PemToDer(certPem, certDer))
|
||||
{
|
||||
Mprintf("[TLS] FATAL: cert file %s is not a valid PEM\n", certPath.c_str());
|
||||
return false;
|
||||
}
|
||||
PCCERT_CONTEXT cert = CertCreateCertificateContext(X509_ASN_ENCODING,
|
||||
certDer.data(), (DWORD)certDer.size());
|
||||
if (cert == NULL)
|
||||
{
|
||||
Mprintf("[TLS] FATAL: CertCreateCertificateContext failed (%u)\n", GetLastError());
|
||||
return false;
|
||||
}
|
||||
|
||||
// 2) 私钥:PEM → DER → 持久化到 CNG KSP(仅支持 PKCS#8;PKCS#1 给出可操作的转换提示)。
|
||||
//
|
||||
// 关键:Schannel 入站凭据(AcquireCredentialsHandle)需要「能按名重开」的持久化私钥。
|
||||
// NCryptImportKey 产出的是匿名临时键,绑定 CERT_NCRYPT_KEY_HANDLE_PROP_ID 后 Schannel 仍
|
||||
// 拿不到(AcquireCredentialsHandle 返回 SEC_E_NO_CREDENTIALS 0x8009030e)。因此先把
|
||||
// PKCS#8 导入为临时键、导出为裸 RSA 私钥 BLOB(NCryptSetProperty 只认裸 BLOB,不认
|
||||
// PKCS#8),再写入持久化键并 finalize,最后用 CERT_KEY_PROV_INFO_PROP_ID 按名引用。
|
||||
std::vector<BYTE> keyDer;
|
||||
NCRYPT_KEY_HANDLE persistedKey = 0;
|
||||
if (DetectKeyFormat(keyPem) == KF_PKCS8)
|
||||
{
|
||||
if (!PemToDer(keyPem, keyDer))
|
||||
{
|
||||
Mprintf("[TLS] FATAL: key file %s is not a valid PEM\n", keyPath.c_str());
|
||||
CertFreeCertificateContext(cert);
|
||||
return false;
|
||||
}
|
||||
// 显式打开 Microsoft Software Key Storage Provider。hProvider 传 NULL 会让
|
||||
// NCryptImportKey 走「默认提供者」解析,实测返回 NTE_INVALID_HANDLE(0x80090026),
|
||||
// 故必须显式取得 KSP 句柄(临时键独立于 hProv,导入后即可释放)。
|
||||
NCRYPT_PROV_HANDLE hProv = 0;
|
||||
SECURITY_STATUS st = NCryptOpenStorageProvider(&hProv, MS_KEY_STORAGE_PROVIDER, 0);
|
||||
if (st != ERROR_SUCCESS)
|
||||
{
|
||||
Mprintf("[TLS] FATAL: NCryptOpenStorageProvider failed 0x%08x\n", (unsigned)st);
|
||||
CertFreeCertificateContext(cert);
|
||||
return false;
|
||||
}
|
||||
|
||||
// 2a) PKCS#8 → 临时键。
|
||||
NCRYPT_KEY_HANDLE tmp = 0;
|
||||
st = NCryptImportKey(hProv, NULL, NCRYPT_PKCS8_PRIVATE_KEY_BLOB, NULL,
|
||||
&tmp, keyDer.data(), (DWORD)keyDer.size(), 0);
|
||||
if (st == ERROR_SUCCESS)
|
||||
{
|
||||
// 2b) 允许明文导出,导出为裸 RSA 私钥 BLOB。
|
||||
DWORD policy = NCRYPT_ALLOW_PLAINTEXT_EXPORT_FLAG | NCRYPT_ALLOW_EXPORT_FLAG;
|
||||
NCryptSetProperty(tmp, NCRYPT_EXPORT_POLICY_PROPERTY, (PBYTE)&policy, sizeof(policy), 0);
|
||||
|
||||
std::vector<BYTE> raw;
|
||||
DWORD rawLen = 0;
|
||||
st = NCryptExportKey(tmp, NULL, BCRYPT_RSAFULLPRIVATE_BLOB, NULL, NULL, 0, &rawLen, 0);
|
||||
if (st == ERROR_SUCCESS)
|
||||
{
|
||||
raw.resize(rawLen);
|
||||
st = NCryptExportKey(tmp, NULL, BCRYPT_RSAFULLPRIVATE_BLOB, NULL,
|
||||
raw.data(), rawLen, &rawLen, 0);
|
||||
}
|
||||
|
||||
// 2c) 写入持久化键并 finalize(落到 KSP,可被 Schannel 按名重开)。
|
||||
if (st == ERROR_SUCCESS)
|
||||
st = NCryptCreatePersistedKey(hProv, &persistedKey, BCRYPT_RSA_ALGORITHM,
|
||||
g_keyName, AT_KEYEXCHANGE, NCRYPT_OVERWRITE_KEY_FLAG);
|
||||
if (st == ERROR_SUCCESS)
|
||||
st = NCryptSetProperty(persistedKey, BCRYPT_RSAFULLPRIVATE_BLOB,
|
||||
raw.data(), (DWORD)raw.size(), 0);
|
||||
if (st == ERROR_SUCCESS)
|
||||
st = NCryptFinalizeKey(persistedKey, 0);
|
||||
|
||||
NCryptFreeObject(tmp);
|
||||
}
|
||||
NCryptFreeObject(hProv);
|
||||
|
||||
if (st != ERROR_SUCCESS)
|
||||
{
|
||||
Mprintf("[TLS] FATAL: failed to persist PKCS#8 key for %s (0x%08x)\n",
|
||||
keyPath.c_str(), (unsigned)st);
|
||||
if (persistedKey)
|
||||
NCryptFreeObject(persistedKey);
|
||||
CertFreeCertificateContext(cert);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
// PKCS#1(RSA/EC PRIVATE KEY)或未知格式:本次不支持,给转换命令。
|
||||
Mprintf("[TLS] FATAL: key file %s must be PKCS#8 (-----BEGIN PRIVATE KEY-----). "
|
||||
"Convert with: openssl pkcs8 -topk8 -nocrypt -in %s -out %s\n",
|
||||
keyPath.c_str(), keyPath.c_str(), keyPath.c_str());
|
||||
CertFreeCertificateContext(cert);
|
||||
return false;
|
||||
}
|
||||
|
||||
// 3) 绑定私钥到证书上下文:按名引用持久化键。Schannel 经 CryptAcquireCertificatePrivateKey
|
||||
// 在 MS_KEY_STORAGE_PROVIDER 里用 pwszContainerName 重开该键。
|
||||
CRYPT_KEY_PROV_INFO provInfo = {};
|
||||
provInfo.pwszContainerName = (LPWSTR)g_keyName;
|
||||
provInfo.pwszProvName = (LPWSTR)MS_KEY_STORAGE_PROVIDER;
|
||||
provInfo.dwProvType = 0; // 0 = CNG(无 CAPI provider type)
|
||||
provInfo.dwKeySpec = AT_KEYEXCHANGE;
|
||||
if (!CertSetCertificateContextProperty(cert, CERT_KEY_PROV_INFO_PROP_ID, 0, &provInfo))
|
||||
{
|
||||
Mprintf("[TLS] FATAL: CertSetCertificateContextProperty(CERT_KEY_PROV_INFO_PROP_ID) "
|
||||
"failed (%u)\n", GetLastError());
|
||||
NCryptDeleteKey(persistedKey, 0); // 删除并释放句柄
|
||||
CertFreeCertificateContext(cert);
|
||||
return false;
|
||||
}
|
||||
// 绑定后持久化键句柄交进程级 g_persistedKey,随 FreeIdentity 删除(覆盖旧键,避免残留)。
|
||||
g_persistedKey = persistedKey;
|
||||
|
||||
// 4) 入站凭据:显式指定服务端证书,不请求客户端证书(单向 TLS)。
|
||||
SCHANNEL_CRED sc = {};
|
||||
sc.dwVersion = SCHANNEL_CRED_VERSION;
|
||||
sc.cCreds = 1;
|
||||
sc.paCred = &cert;
|
||||
sc.grbitEnabledProtocols = SP_PROT_TLS1_2_SERVER;
|
||||
// NO_DEFAULT_CREDS 避免回退到系统默认凭据,确保只用我们提供的证书/私钥。
|
||||
//(单向 TLS 不请求客户端证书,故无需 SCH_CRED_MANUAL_CRED_VALIDATION。)
|
||||
sc.dwFlags = SCH_CRED_NO_DEFAULT_CREDS;
|
||||
|
||||
TimeStamp tsExpiry = {};
|
||||
SECURITY_STATUS st = AcquireCredentialsHandleA(
|
||||
NULL, (SEC_CHAR*)"Microsoft Unified Security Protocol Provider", SECPKG_CRED_INBOUND,
|
||||
NULL, &sc, NULL, NULL, &g_cred, &tsExpiry);
|
||||
|
||||
if (st != SEC_E_OK)
|
||||
{
|
||||
Mprintf("[TLS] FATAL: AcquireCredentialsHandle(SECPKG_CRED_INBOUND) failed 0x%08x\n",
|
||||
(unsigned)st);
|
||||
CertFreeCertificateContext(cert); // 无凭据引用,安全释放
|
||||
g_cred = {};
|
||||
if (g_persistedKey) // 回滚:删除已持久化的私钥,避免残留
|
||||
{
|
||||
NCryptDeleteKey(g_persistedKey, 0); // 删除并释放句柄
|
||||
g_persistedKey = 0;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
// 成功:把证书上下文生命周期交给进程级 g_cert,随 FreeIdentity 释放,
|
||||
// 保证在 FreeCredentialsHandle 之前私钥一直可用。
|
||||
g_cert = cert;
|
||||
g_credValid = true;
|
||||
return true;
|
||||
}
|
||||
|
||||
bool ServerTlsIdentity::IsEnabled()
|
||||
{
|
||||
std::lock_guard<std::mutex> lk(g_identityMtx);
|
||||
return g_enabled;
|
||||
}
|
||||
|
||||
bool ServerTlsIdentity::IsReady()
|
||||
{
|
||||
std::lock_guard<std::mutex> lk(g_identityMtx);
|
||||
return g_ready && g_credValid;
|
||||
}
|
||||
|
||||
bool ServerTlsIdentity::IsLoadError()
|
||||
{
|
||||
std::lock_guard<std::mutex> lk(g_identityMtx);
|
||||
return g_loadError;
|
||||
}
|
||||
|
||||
CredHandle ServerTlsIdentity::Cred()
|
||||
{
|
||||
std::lock_guard<std::mutex> lk(g_identityMtx);
|
||||
return g_cred;
|
||||
}
|
||||
|
||||
// =====================================================================================
|
||||
// ServerTlsSession —— 单连接会话(握手 + 加解密)
|
||||
// =====================================================================================
|
||||
|
||||
void ServerTlsSession::CloseHandles()
|
||||
{
|
||||
if (m_ctxValid)
|
||||
{
|
||||
DeleteSecurityContext(&m_ctx);
|
||||
m_ctxValid = false;
|
||||
}
|
||||
m_ctx = {};
|
||||
m_ready = false;
|
||||
m_raw.clear();
|
||||
}
|
||||
|
||||
// 握手线程在 IOCP 的子套接字上做同步 I/O。该套接字可能是非阻塞的(监听套接字经
|
||||
// WSAEventSelect 置为非阻塞后,accept 出的子套接字继承非阻塞态;线程里 FIONBIO=0 也
|
||||
// 不一定即时生效)。故 recv/send 遇 WSAEWOULDBLOCK 时用 select 等可读/可写再重试,
|
||||
// 而非当致命错误,避免「客户端回包还没到就误判握手失败」。
|
||||
static bool WaitSocketReady(SOCKET sock, bool forRead, int timeoutMs)
|
||||
{
|
||||
fd_set fds;
|
||||
FD_ZERO(&fds);
|
||||
FD_SET(sock, &fds);
|
||||
timeval tv;
|
||||
tv.tv_sec = timeoutMs / 1000;
|
||||
tv.tv_usec = (timeoutMs % 1000) * 1000;
|
||||
return select(0, forRead ? &fds : NULL, forRead ? NULL : &fds, NULL, &tv) > 0;
|
||||
}
|
||||
|
||||
bool ServerTlsSession::SendAll(SOCKET sock, const void* data, int len)
|
||||
{
|
||||
const BYTE* p = (const BYTE*)data;
|
||||
int sent = 0;
|
||||
while (sent < len)
|
||||
{
|
||||
int n = send(sock, (const char*)p + sent, len - sent, 0);
|
||||
if (n == SOCKET_ERROR && WSAGetLastError() == WSAEWOULDBLOCK)
|
||||
{
|
||||
if (!WaitSocketReady(sock, false, 15000))
|
||||
return false;
|
||||
continue; // 非阻塞套接字发送缓冲满:等可写后重试
|
||||
}
|
||||
if (n <= 0)
|
||||
return false;
|
||||
sent += n;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
bool ServerTlsSession::Handshake(SOCKET sock, const BYTE* initial, int initialLen)
|
||||
{
|
||||
std::lock_guard<std::mutex> lk(m_mtx);
|
||||
if (m_ctxValid)
|
||||
CloseHandles(); // 防御:会话应只握手一次
|
||||
|
||||
CredHandle cred = ServerTlsIdentity::Cred();
|
||||
|
||||
// 单向 TLS:服务端不请求客户端证书,故无需 ASC_REQ_MANUAL_CRED_VALIDATION
|
||||
//(该标志只在 mTLS 校验客户端证书时有意义;SDK 的 ASC 命名空间里亦无此常量)。
|
||||
DWORD req = ASC_REQ_SEQUENCE_DETECT | ASC_REQ_REPLAY_DETECT | ASC_REQ_CONFIDENTIALITY |
|
||||
ASC_REQ_ALLOCATE_MEMORY | ASC_REQ_STREAM;
|
||||
|
||||
std::vector<BYTE> hs(initial, initial + initialLen);
|
||||
CtxtHandle ctx = {};
|
||||
bool ctxValid = false;
|
||||
TimeStamp tsExpiry = {};
|
||||
ULONG attrs = 0;
|
||||
SECURITY_STATUS sc = SEC_I_CONTINUE_NEEDED;
|
||||
|
||||
for (;;)
|
||||
{
|
||||
SecBuffer inBufs[2] = {
|
||||
{ (ULONG)hs.size(), SECBUFFER_TOKEN, hs.empty() ? NULL : hs.data() },
|
||||
{ 0, SECBUFFER_EMPTY, NULL },
|
||||
};
|
||||
SecBufferDesc inDesc = { SECBUFFER_VERSION, 2, inBufs };
|
||||
SecBuffer outBuf = { 0, SECBUFFER_TOKEN, NULL };
|
||||
SecBufferDesc outDesc = { SECBUFFER_VERSION, 1, &outBuf };
|
||||
|
||||
sc = AcceptSecurityContext(&cred, ctxValid ? &ctx : NULL, &inDesc, req,
|
||||
SECURITY_NATIVE_DREP, ctxValid ? NULL : &ctx,
|
||||
&outDesc, &attrs, &tsExpiry);
|
||||
ctxValid = true;
|
||||
|
||||
// 发出服务端握手记录(ServerHello/Certificate/... 或 Finished)。
|
||||
if (outBuf.cbBuffer > 0 && outBuf.pvBuffer)
|
||||
{
|
||||
bool ok = SendAll(sock, outBuf.pvBuffer, (int)outBuf.cbBuffer);
|
||||
FreeContextBuffer(outBuf.pvBuffer);
|
||||
if (!ok)
|
||||
{
|
||||
sc = SEC_E_INTERNAL_ERROR;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// 计算已消费字节:SECBUFFER_EXTRA 里是「未消费的尾部」(下一条握手消息 / 首条应用数据)。
|
||||
// 必须在 SEC_E_OK / 失败判定之前处理:否则握手完成时 hs 里仍混着已消费的握手记录,
|
||||
// 紧跟 Finished 的首条应用数据(客户端登录包)会随 hs 一起被丢弃,导致随机丢包。
|
||||
if (inBufs[1].BufferType == SECBUFFER_EXTRA)
|
||||
{
|
||||
int extra = (int)inBufs[1].cbBuffer;
|
||||
hs.erase(hs.begin(), hs.begin() + (hs.size() - extra));
|
||||
}
|
||||
else if (sc != SEC_E_INCOMPLETE_MESSAGE && sc != SEC_I_INCOMPLETE_CREDENTIALS)
|
||||
{
|
||||
hs.clear(); // 已全部消费,继续等下一轮握手消息
|
||||
}
|
||||
// 否则:SEC_E_INCOMPLETE_MESSAGE —— 保留 hs(不完整消息),续收补齐。
|
||||
|
||||
if (sc == SEC_E_OK)
|
||||
break;
|
||||
if (FAILED(sc) && sc != SEC_E_INCOMPLETE_MESSAGE && sc != SEC_I_INCOMPLETE_CREDENTIALS)
|
||||
{
|
||||
Mprintf("[TLS] AcceptSecurityContext failed 0x%08x (in=%u consumed? extra=%u)\n",
|
||||
(unsigned)sc, inBufs[0].cbBuffer, inBufs[1].BufferType == SECBUFFER_EXTRA ? inBufs[1].cbBuffer : 0);
|
||||
break; // 真正的失败
|
||||
}
|
||||
|
||||
char raw[8192];
|
||||
int r;
|
||||
for (;;)
|
||||
{
|
||||
r = recv(sock, raw, sizeof(raw), 0);
|
||||
if (r == SOCKET_ERROR && WSAGetLastError() == WSAEWOULDBLOCK)
|
||||
{
|
||||
if (!WaitSocketReady(sock, true, 15000))
|
||||
{
|
||||
r = SOCKET_ERROR;
|
||||
break; // 等可读超时
|
||||
}
|
||||
continue; // 非阻塞套接字暂无数据:等可读后重试
|
||||
}
|
||||
break;
|
||||
}
|
||||
if (r <= 0)
|
||||
{
|
||||
Mprintf("[TLS] handshake recv failed/EOF (r=%d err=%d)\n", r, WSAGetLastError());
|
||||
sc = SEC_E_INTERNAL_ERROR;
|
||||
break;
|
||||
}
|
||||
hs.insert(hs.end(), (BYTE*)raw, (BYTE*)raw + r);
|
||||
}
|
||||
|
||||
if (sc != SEC_E_OK)
|
||||
{
|
||||
Mprintf("[TLS] handshake failed: final status 0x%08x (fail-closed)\n", (unsigned)sc);
|
||||
if (ctxValid)
|
||||
DeleteSecurityContext(&ctx);
|
||||
return false;
|
||||
}
|
||||
|
||||
// 握手成功后 hs 里残留的「未消费尾部」是紧跟 Finished 的首条应用数据(TLS 记录)。
|
||||
// 交给 Decrypt 处理,否则首包会丢(表现为随机登录超时/重连/协议错位)。
|
||||
if (!hs.empty())
|
||||
m_raw.insert(m_raw.end(), hs.begin(), hs.end());
|
||||
|
||||
// 查询流式记录尺寸(加密封装用)。
|
||||
SecPkgContext_StreamSizes sizes = {};
|
||||
if (QueryContextAttributes(&ctx, SECPKG_ATTR_STREAM_SIZES, &sizes) != SEC_E_OK)
|
||||
{
|
||||
DeleteSecurityContext(&ctx);
|
||||
return false;
|
||||
}
|
||||
|
||||
m_ctx = ctx;
|
||||
m_ctxValid = true;
|
||||
m_sizes = sizes;
|
||||
m_ready = true;
|
||||
return true;
|
||||
}
|
||||
|
||||
int ServerTlsSession::Decrypt(const BYTE* cipher, int len, std::vector<BYTE>& plain)
|
||||
{
|
||||
std::lock_guard<std::mutex> lk(m_mtx);
|
||||
if (!m_ready)
|
||||
return -1;
|
||||
if (len <= 0)
|
||||
return 0;
|
||||
|
||||
m_raw.insert(m_raw.end(), cipher, cipher + len);
|
||||
|
||||
for (;;)
|
||||
{
|
||||
SecBuffer bufs[4] = {
|
||||
{ (ULONG)m_raw.size(), SECBUFFER_DATA, m_raw.empty() ? NULL : m_raw.data() },
|
||||
{ 0, SECBUFFER_EMPTY, NULL },
|
||||
{ 0, SECBUFFER_EMPTY, NULL },
|
||||
{ 0, SECBUFFER_EMPTY, NULL },
|
||||
};
|
||||
SecBufferDesc desc = { SECBUFFER_VERSION, 4, bufs };
|
||||
|
||||
SECURITY_STATUS sc = DecryptMessage(&m_ctx, &desc, 0, NULL);
|
||||
if (sc == SEC_E_INCOMPLETE_MESSAGE)
|
||||
break; // 记录不完整,等下一轮 WSARecv
|
||||
if (sc == SEC_I_CONTEXT_EXPIRED) {
|
||||
Mprintf("[TLS] DecryptMessage: peer close_notify (SEC_I_CONTEXT_EXPIRED)\n");
|
||||
return -1; // 对端 close_notify / 会话结束
|
||||
}
|
||||
if (sc == SEC_I_RENEGOTIATE || FAILED(sc)) {
|
||||
Mprintf("[TLS] DecryptMessage failed 0x%08x (raw=%u)\n", (unsigned)sc, (unsigned)m_raw.size());
|
||||
return -1;
|
||||
}
|
||||
|
||||
int decLen = (int)bufs[1].cbBuffer;
|
||||
const BYTE* p = (const BYTE*)bufs[1].pvBuffer;
|
||||
int extra = (bufs[3].BufferType == SECBUFFER_EXTRA) ? (int)bufs[3].cbBuffer : 0;
|
||||
int consumed = (int)m_raw.size() - extra;
|
||||
|
||||
if (decLen > 0 && p)
|
||||
plain.insert(plain.end(), p, p + decLen);
|
||||
|
||||
m_raw.erase(m_raw.begin(), m_raw.begin() + consumed);
|
||||
if (m_raw.empty())
|
||||
break;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
int ServerTlsSession::Encrypt(const BYTE* plain, int len, std::vector<BYTE>& wire)
|
||||
{
|
||||
std::lock_guard<std::mutex> lk(m_mtx);
|
||||
if (!m_ready)
|
||||
return -1;
|
||||
if (len <= 0)
|
||||
return 0;
|
||||
|
||||
ULONG maxMsg = m_sizes.cbMaximumMessage > 0 ? m_sizes.cbMaximumMessage : 16384;
|
||||
int total = 0;
|
||||
while (total < len)
|
||||
{
|
||||
int chunk = (len - total > (int)maxMsg) ? (int)maxMsg : (len - total);
|
||||
|
||||
std::vector<BYTE> rec(m_sizes.cbHeader + chunk + m_sizes.cbTrailer);
|
||||
BYTE* hdr = rec.data();
|
||||
BYTE* body = hdr + m_sizes.cbHeader;
|
||||
BYTE* trailer = body + chunk;
|
||||
memcpy(body, plain + total, chunk);
|
||||
|
||||
SecBuffer bufs[4] = {
|
||||
{ m_sizes.cbHeader, SECBUFFER_STREAM_HEADER, hdr },
|
||||
{ (ULONG)chunk, SECBUFFER_DATA, body },
|
||||
{ m_sizes.cbTrailer, SECBUFFER_STREAM_TRAILER, trailer },
|
||||
{ 0, SECBUFFER_EMPTY, NULL },
|
||||
};
|
||||
SecBufferDesc desc = { SECBUFFER_VERSION, 4, bufs };
|
||||
|
||||
SECURITY_STATUS sc = EncryptMessage(&m_ctx, 0, &desc, 0);
|
||||
if (FAILED(sc))
|
||||
return -1; // 握手已建立,此处失败视为致命(调用方关闭连接)
|
||||
|
||||
int wireLen = (int)(bufs[0].cbBuffer + bufs[1].cbBuffer + bufs[2].cbBuffer);
|
||||
wire.insert(wire.end(), rec.data(), rec.data() + wireLen);
|
||||
total += chunk;
|
||||
}
|
||||
return total;
|
||||
}
|
||||
|
||||
// =====================================================================================
|
||||
// CONTEXT_OBJECT::FreeTls —— 释放每连接 TLS 会话(定义在此以便 delete 完整类型)
|
||||
// =====================================================================================
|
||||
|
||||
void CONTEXT_OBJECT::FreeTls()
|
||||
{
|
||||
delete tls;
|
||||
tls = NULL;
|
||||
}
|
||||
Reference in new issue
Block a user