Files
SimpleRemoter/client/TlsChannel.cpp
T
yuanyuanxianganddeepseek-v4-pro ca0d7626d2 Feat: One-way TLS transport for the device port
Add optional one-way TLS on the device port (6543), enabled per client via a
pinned trust anchor, so new clients encrypt their transport while legacy
plaintext clients keep working unchanged.

Server (server/go): sniff each accepted connection and wrap TLS peers
(ClientHello 0x16 0x03 + handshake type 0x01) in crypto/tls with MinVersion
TLS 1.2; everything else stays on the plaintext path. TLS is enabled only when
both YAMA_TLS_CERT and YAMA_TLS_KEY are set, and a cert/key load failure fails
startup loudly rather than silently serving plaintext.

Client (client/, Windows): Schannel one-way TLS. When the trust anchor is
present — registry value Software\<exehash>\settings\tls_cert (REG_SZ holding
the PEM), with fallback to settings.ini [settings] tls_cert or yama-ca.pem
beside the exe — every outbound connection (main + sub-connections) completes
a TLS 1.2 handshake before the app protocol and is strictly pinned to that
anchor via a chain engine with hExclusiveRoot; any handshake/cert failure is
fail-closed (no plaintext downgrade). Send uses EncryptMessage; Recv uses
DecryptMessage and reads plaintext from the stream-mode DATA segment (bufs[1])
rather than the record header.

Co-Authored-By: deepseek-v4-pro
2026-10-14 21:10:21 +02:00

495 lines
18 KiB
C++
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// TlsChannel.cpp — 见 TlsChannel.h 头注释。仅 _WIN32 编译。
#ifdef _WIN32
#include "stdafx.h"
#include "TlsChannel.h"
#include "common/iniFile.h" // config(settings.ini 文件回退) + GetRegistryName/GetCurrentUserRegistryKey(注册表正文)
#include <wincrypt.h> // 证书/链引擎 API
#include <winreg.h> // RegOpenKeyExA / RegQueryValueExA
#include <io.h> // _access
#include <fstream> // std::ifstream
#include <iterator> // std::istreambuf_iterator
#include <string>
#include <mutex> // std::call_once
#pragma comment(lib, "secur32.lib")
#pragma comment(lib, "crypt32.lib")
namespace {
std::once_flag g_tlsOnce;
bool g_tlsEnabled = false; // TLS 会话可用:send/recv 走加密
bool g_tlsError = false; // 证书存在但加载失败:拒绝连接(fail-closed)
HCERTSTORE g_tlsStore = NULL;
HCERTCHAINENGINE g_tlsEngine = NULL; // 只信任 g_tlsStore 的链引擎(严格钉扎)
// 解析信任锚证书路径:优先 settings.ini 的 [settings] tls_cert,其次可执行文件同目录
// yama-ca.pem。
std::string ResolveCertPath()
{
std::string certPath;
{
config cfg;
certPath = cfg.GetStr("settings", "tls_cert", "");
}
if (certPath.empty()) {
char path[MAX_PATH];
if (!GetModuleFileNameA(NULL, path, sizeof(path)))
return std::string(); // 拿不到 exe 路径,视为未配置
char* p = path + strlen(path);
while (p > path && *p != '\\') --p;
strcpy(p + 1, "yama-ca.pem");
certPath = path;
}
return certPath;
}
// 读取注册表 REG_SZ(动态扩容)。iniFile::GetStr 用 512 字节固定缓冲,会把 PEM 正文
// (约 1.3~1.8KB)截断成空,故此处自行两段式 RegQueryValueEx 读取。
// 返回 true 表示值存在且为 REG_SZ,out 为去掉末尾 '\0' 后的内容;否则 false。
bool ReadRegistrySz(HKEY root, const std::string& subKey, const std::string& valueName, std::string& out)
{
HKEY hKey = NULL;
if (RegOpenKeyExA(root, subKey.c_str(), 0, KEY_READ, &hKey) != ERROR_SUCCESS)
return false;
DWORD type = 0, size = 0;
LONG r = RegQueryValueExA(hKey, valueName.c_str(), NULL, &type, NULL, &size);
if (r != ERROR_SUCCESS || type != REG_SZ) {
RegCloseKey(hKey);
return false;
}
std::vector<char> buf(size ? size : 1, 0);
r = RegQueryValueExA(hKey, valueName.c_str(), NULL, &type,
reinterpret_cast<LPBYTE>(buf.data()), &size);
RegCloseKey(hKey);
if (r != ERROR_SUCCESS)
return false;
if (size > 0 && buf[size - 1] == '\0')
--size; // REG_SZ 的 size 含末尾 '\0'
out.assign(buf.data(), size);
return true;
}
// 取信任锚 PEM 正文。优先注册表 Software\<exehash>\settings\tls_cert(REG_SZ 直接存
// PEM 正文,便于后续服务端下发/撤销),其次 settings.ini [settings] tls_cert 路径、
// exe 同目录 yama-ca.pem 文件(向后兼容旧的文件投递方式)。
// 返回 true 表示已配置(pem 可能为空:值/文件存在但内容为空,由 LoadTrustAnchor 判为
// fail-closed);false 表示完全未配置(走明文)。source 描述来源,仅用于日志。
bool ReadTrustAnchor(std::string& pem, std::string& source)
{
// 1) 注册表正文(首选)。值存在(即便为空)即视为「已配置」,交由 LoadTrustAnchor
// 对空/损坏内容判 fail-closed,与「空文件 → fail-closed」语义保持一致,避免空值
// 被静默当成未配置而回退明文。
std::string regSubKey = GetRegistryName() + "\\settings";
std::string regVal;
if (ReadRegistrySz(GetCurrentUserRegistryKey(), regSubKey, "tls_cert", regVal)) {
pem = regVal;
source = "registry " + regSubKey + "\\tls_cert";
return true;
}
// 2) 文件回退:settings.ini [settings] tls_cert 路径 → exe 同目录 yama-ca.pem
std::string certPath = ResolveCertPath();
if (certPath.empty() || _access(certPath.c_str(), 0) != 0)
return false; // 无证书 → 明文
std::ifstream f(certPath.c_str(), std::ios::binary);
if (!f) {
Mprintf("[TLS] cannot open %s\n", certPath.c_str());
return false;
}
pem.assign((std::istreambuf_iterator<char>(f)), std::istreambuf_iterator<char>());
source = certPath;
return true;
}
// 解码 PEM/DER 信任锚正文为 DER,并加入内存信任锚存储。
bool LoadTrustAnchor(const std::string& pem, const std::string& source, HCERTSTORE* outStore)
{
if (pem.empty()) {
Mprintf("[TLS] empty certificate %s\n", source.c_str());
return false;
}
// PEM(-----BEGIN CERTIFICATE----- base64)或 DER → DER。CRYPT_STRING_ANY 两者都认,
// 失败再退回 CRYPT_STRING_BASE64HEADER(纯 PEM)。
DWORD derLen = 0;
if (!CryptStringToBinaryA(pem.data(), (DWORD)pem.size(), CRYPT_STRING_ANY,
NULL, &derLen, NULL, NULL) || derLen == 0) {
if (!CryptStringToBinaryA(pem.data(), (DWORD)pem.size(), CRYPT_STRING_BASE64HEADER,
NULL, &derLen, NULL, NULL) || derLen == 0) {
Mprintf("[TLS] failed to decode certificate %s\n", source.c_str());
return false;
}
}
std::vector<BYTE> der(derLen);
if (!CryptStringToBinaryA(pem.data(), (DWORD)pem.size(), CRYPT_STRING_ANY,
der.data(), &derLen, NULL, NULL)) {
if (!CryptStringToBinaryA(pem.data(), (DWORD)pem.size(), CRYPT_STRING_BASE64HEADER,
der.data(), &derLen, NULL, NULL)) {
Mprintf("[TLS] failed to decode certificate %s\n", source.c_str());
return false;
}
}
PCCERT_CONTEXT cert = CertCreateCertificateContext(X509_ASN_ENCODING, der.data(), derLen);
if (!cert) {
Mprintf("[TLS] invalid certificate %s\n", source.c_str());
return false;
}
HCERTSTORE store = CertOpenStore(CERT_STORE_PROV_MEMORY, X509_ASN_ENCODING,
0, CERT_STORE_CREATE_NEW_FLAG, NULL);
if (!store) {
CertFreeCertificateContext(cert);
Mprintf("[TLS] CertOpenStore failed\n");
return false;
}
if (!CertAddCertificateContextToStore(store, cert, CERT_STORE_ADD_REPLACE_EXISTING, NULL)) {
CertCloseStore(store, 0);
CertFreeCertificateContext(cert);
Mprintf("[TLS] CertAddCertificateContextToStore failed\n");
return false;
}
CertFreeCertificateContext(cert);
*outStore = store;
return true;
}
void InitTls()
{
std::string pem, source;
if (!ReadTrustAnchor(pem, source)) {
// 无证书 → 明文(默认,与老客户端一致)
g_tlsEnabled = false;
g_tlsError = false;
Mprintf("[TLS] disabled: no trust anchor configured, plaintext mode\n");
return;
}
// 证书已配置:管理员已「颁发」,必须走 TLS;加载失败即拒绝连接(fail-closed),
// 不能静默回退明文。
HCERTSTORE store = NULL;
if (!LoadTrustAnchor(pem, source, &store)) {
g_tlsEnabled = false;
g_tlsError = true;
Mprintf("[TLS] FATAL: trust anchor (%s) present but failed to load; refusing connections (fail-closed)\n",
source.c_str());
return;
}
// 严格钉扎:链引擎只信任该 store 里的根证书,排除系统根库。否则攻击者拿一张任意
// 公共 CA 签发的证书就能通过 CertGetCertificateChain(默认引擎信任系统根库)。
CERT_CHAIN_ENGINE_CONFIG ec = {};
ec.cbSize = sizeof(ec);
ec.hExclusiveRoot = store;
HCERTCHAINENGINE engine = NULL;
if (!CertCreateCertificateChainEngine(&ec, &engine)) {
CertCloseStore(store, 0);
g_tlsEnabled = false;
g_tlsError = true;
Mprintf("[TLS] FATAL: CertCreateCertificateChainEngine failed; refusing connections (fail-closed)\n");
return;
}
g_tlsStore = store;
g_tlsEngine = engine;
g_tlsEnabled = true;
g_tlsError = false;
Mprintf("[TLS] enabled: trust anchor loaded (%s), all connections use TLS\n", source.c_str());
}
} // namespace
bool TlsEnabled()
{
std::call_once(g_tlsOnce, InitTls);
return g_tlsEnabled;
}
bool TlsLoadError()
{
std::call_once(g_tlsOnce, InitTls);
return g_tlsError;
}
// ---- TlsChannel ----
void TlsChannel::CloseHandles()
{
std::lock_guard<std::recursive_mutex> lk(m_ctxMtx);
if (m_ctxValid) { DeleteSecurityContext(&m_ctx); m_ctx = {}; m_ctxValid = false; }
if (m_credValid) { FreeCredentialsHandle(&m_cred); m_cred = {}; m_credValid = false; }
m_ready = false;
m_raw.clear();
m_pending.clear();
}
bool TlsChannel::SendAll(SOCKET sock, const void* data, int len)
{
const char* p = (const char*)data;
int sent = 0;
while (sent < len) {
int n = ::send(sock, p + sent, len - sent, 0);
if (n == SOCKET_ERROR || n == 0)
return false;
sent += n;
}
return true;
}
bool TlsChannel::VerifyServerCert()
{
PCCERT_CONTEXT remote = NULL;
SECURITY_STATUS sc = QueryContextAttributesA(&m_ctx, SECPKG_ATTR_REMOTE_CERT_CONTEXT, &remote);
if (sc != SEC_E_OK || !remote)
return false;
if (!g_tlsStore || !g_tlsEngine) {
CertFreeCertificateContext(remote);
return false;
}
CERT_CHAIN_PARA para = {};
para.cbSize = sizeof(para);
PCCERT_CHAIN_CONTEXT chain = NULL;
// g_tlsEngine 的 hExclusiveRoot 已把信任锚限制为 g_tlsStore;hAdditionalStore 再传一次
// 该 store 供链引擎查找 CA/中间证书。只接受「无任何错误位」的链(含时间有效性)。
// 不设置任何 CERT_CHAIN_REVOCATION_CHECK_* 位 = 不做吊销检查(私有 CA 无 CRL/OCSP,
// 且这些位会触发联网,反而失败);只保留 CERT_CHAIN_DISABLE_AIA 禁止联网取中间证书。
BOOL ok = CertGetCertificateChain(g_tlsEngine, remote, NULL, g_tlsStore, &para,
CERT_CHAIN_DISABLE_AIA,
NULL, &chain);
bool trusted = false;
if (ok && chain) {
trusted = (chain->TrustStatus.dwErrorStatus == CERT_TRUST_NO_ERROR);
}
if (chain) CertFreeCertificateChain(chain);
CertFreeCertificateContext(remote);
return trusted;
}
bool TlsChannel::Handshake(SOCKET sock)
{
std::lock_guard<std::recursive_mutex> lk(m_ctxMtx);
CloseHandles(); // 重连时清理上一次会话(recursive 锁,此处可重入)
SCHANNEL_CRED cred = {};
cred.dwVersion = SCHANNEL_CRED_VERSION;
cred.dwFlags = SCH_CRED_MANUAL_CRED_VALIDATION | SCH_CRED_NO_DEFAULT_CREDS;
cred.grbitEnabledProtocols = SP_PROT_TLS1_2_CLIENT; // TLS 1.2(与 Go 端 MinVersion 对齐)
TimeStamp tsExpiry = {};
SECURITY_STATUS sc = AcquireCredentialsHandleA(
NULL, (SEC_CHAR*)"Microsoft Unified Security Protocol Provider",
SECPKG_CRED_OUTBOUND, NULL, &cred, NULL, NULL, &m_cred, &tsExpiry);
if (sc != SEC_E_OK) {
Mprintf("[TLS] AcquireCredentialsHandle failed: 0x%08X\n", (unsigned)sc);
return false;
}
m_credValid = true;
DWORD flags = ISC_REQ_SEQUENCE_DETECT | ISC_REQ_REPLAY_DETECT | ISC_REQ_CONFIDENTIALITY
| ISC_REQ_ALLOCATE_MEMORY | ISC_REQ_STREAM | ISC_REQ_MANUAL_CRED_VALIDATION;
SecBuffer outBuf = { 0, SECBUFFER_TOKEN, NULL };
SecBufferDesc outDesc = { SECBUFFER_VERSION, 1, &outBuf };
ULONG attrs = 0;
// 首次调用产生 ClientHello
sc = InitializeSecurityContextA(&m_cred, NULL, NULL, flags, 0, SECURITY_NATIVE_DREP,
NULL, 0, &m_ctx, &outDesc, &attrs, &tsExpiry);
m_ctxValid = true; // 上下文已分配,之后必须 DeleteSecurityContext
if (sc != SEC_I_CONTINUE_NEEDED && sc != SEC_E_OK) {
Mprintf("[TLS] InitializeSecurityContext(initial) failed: 0x%08X\n", (unsigned)sc);
CloseHandles();
return false;
}
if (outBuf.cbBuffer > 0 && outBuf.pvBuffer) {
if (!SendAll(sock, outBuf.pvBuffer, (int)outBuf.cbBuffer)) {
FreeContextBuffer(outBuf.pvBuffer);
CloseHandles();
return false;
}
FreeContextBuffer(outBuf.pvBuffer);
}
std::vector<BYTE> hs;
char raw[8192];
while (sc == SEC_I_CONTINUE_NEEDED || sc == SEC_E_INCOMPLETE_MESSAGE ||
sc == SEC_I_INCOMPLETE_CREDENTIALS)
{
int r = recv(sock, raw, sizeof(raw), 0);
if (r == SOCKET_ERROR || r == 0) {
Mprintf("[TLS] handshake recv failed/EOF\n");
CloseHandles();
return false;
}
hs.insert(hs.end(), (BYTE*)raw, (BYTE*)raw + r);
SecBuffer inBufs[2] = {
{ (ULONG)hs.size(), SECBUFFER_TOKEN, hs.data() },
{ 0, SECBUFFER_EMPTY, NULL },
};
SecBufferDesc inDesc = { SECBUFFER_VERSION, 2, inBufs };
outBuf = { 0, SECBUFFER_TOKEN, NULL };
outDesc = { SECBUFFER_VERSION, 1, &outBuf };
sc = InitializeSecurityContextA(&m_cred, &m_ctx, NULL, flags, 0, SECURITY_NATIVE_DREP,
&inDesc, 0, NULL, &outDesc, &attrs, &tsExpiry);
// 裁剪已消费的输入,仅保留 SECBUFFER_EXTRA 指向的未消费尾部
if (inBufs[1].BufferType == SECBUFFER_EXTRA) {
int extra = inBufs[1].cbBuffer;
int consumed = (int)hs.size() - extra;
hs.erase(hs.begin(), hs.begin() + consumed);
} else {
hs.clear();
}
if (outBuf.cbBuffer > 0 && outBuf.pvBuffer) {
if (!SendAll(sock, outBuf.pvBuffer, (int)outBuf.cbBuffer)) {
FreeContextBuffer(outBuf.pvBuffer);
CloseHandles();
return false;
}
FreeContextBuffer(outBuf.pvBuffer);
}
}
if (sc != SEC_E_OK) {
Mprintf("[TLS] handshake failed: 0x%08X\n", (unsigned)sc);
CloseHandles();
return false;
}
if (!VerifyServerCert()) {
Mprintf("[TLS] server certificate not trusted against yama-ca.pem (fail-closed)\n");
CloseHandles();
return false;
}
sc = QueryContextAttributesA(&m_ctx, SECPKG_ATTR_STREAM_SIZES, &m_sizes);
if (sc != SEC_E_OK) {
Mprintf("[TLS] QueryContextAttributes(StreamSizes) failed: 0x%08X\n", (unsigned)sc);
CloseHandles();
return false;
}
m_ready = true;
Mprintf("[TLS] handshake OK (TLS 1.2, server cert trusted)\n");
return true;
}
int TlsChannel::Recv(SOCKET sock, char* out, int outLen)
{
std::lock_guard<std::recursive_mutex> lk(m_ctxMtx);
if (!m_ready || outLen <= 0) {
Mprintf("[TLS] Recv called but m_ready=%d (outLen=%d), returning SOCKET_ERROR\n",
(int)m_ready, outLen);
return SOCKET_ERROR;
}
if (!m_pending.empty()) {
int n = (int)m_pending.size() < outLen ? (int)m_pending.size() : outLen;
memcpy(out, m_pending.data(), n);
m_pending.erase(m_pending.begin(), m_pending.begin() + n);
return n;
}
char raw[16384];
for (;;) {
int r = recv(sock, raw, sizeof(raw), 0);
if (r == SOCKET_ERROR)
return SOCKET_ERROR;
if (r == 0)
return 0; // 对端关闭(此时无 pending)
m_raw.insert(m_raw.end(), (BYTE*)raw, (BYTE*)raw + r);
for (;;) {
SecBuffer bufs[4] = {
{ (ULONG)m_raw.size(), SECBUFFER_DATA, 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; // 记录不完整,再 recv
if (sc == SEC_I_CONTEXT_EXPIRED)
return 0; // close_notify
if (sc == SEC_I_RENEGOTIATE || FAILED(sc))
return SOCKET_ERROR;
// 流模式 DecryptMessage 会把输入 SECBUFFER_DATA 重组为 4 段:
// bufs[0]=STREAM_HEADER(记录头+nonce)、bufs[1]=DATA(明文)、
// bufs[2]=STREAM_TRAILER(标签)、bufs[3]=EXTRA(下一条记录)。
// 明文在 bufs[1].pvBuffer,不在 m_raw 首字节。
int decLen = bufs[1].cbBuffer;
const BYTE* plain = (const BYTE*)bufs[1].pvBuffer;
int extra = (bufs[3].BufferType == SECBUFFER_EXTRA) ? bufs[3].cbBuffer : 0;
int consumed = (int)m_raw.size() - extra;
m_pending.insert(m_pending.end(), plain, plain + decLen);
m_raw.erase(m_raw.begin(), m_raw.begin() + consumed);
if (!m_pending.empty()) {
int n = (int)m_pending.size() < outLen ? (int)m_pending.size() : outLen;
memcpy(out, m_pending.data(), n);
m_pending.erase(m_pending.begin(), m_pending.begin() + n);
return n;
}
if (m_raw.empty())
break; // 无明文(如 alert/CCS 记录),继续 recv
}
}
}
int TlsChannel::Send(SOCKET sock, const char* data, int len)
{
std::lock_guard<std::recursive_mutex> lk(m_ctxMtx);
if (!m_ready)
return SOCKET_ERROR;
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, data + 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 total > 0 ? total : SOCKET_ERROR;
}
int wireLen = bufs[0].cbBuffer + bufs[1].cbBuffer + bufs[2].cbBuffer;
if (!SendAll(sock, rec.data(), wireLen)) {
return total > 0 ? total : SOCKET_ERROR;
}
total += chunk;
}
return total;
}
#endif // _WIN32