// 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 #include #include #include #include #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& 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 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 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 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 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 lk(g_identityMtx); return g_enabled; } bool ServerTlsIdentity::IsReady() { std::lock_guard lk(g_identityMtx); return g_ready && g_credValid; } bool ServerTlsIdentity::IsLoadError() { std::lock_guard lk(g_identityMtx); return g_loadError; } CredHandle ServerTlsIdentity::Cred() { std::lock_guard 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 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 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& plain) { std::lock_guard 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& wire) { std::lock_guard 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 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; }