Files
2026-02-25 23:45:53 +01:00

696 lines
20 KiB
C

#ifdef TLS_ENABLED
#ifdef TLS_SCHANNEL
#include <assert.h>
#include <stdio.h>
#include <string.h>
#define SECURITY_WIN32
#include <windows.h>
#include <wincrypt.h>
#include <security.h>
#include <schannel.h>
#include "tls.h"
#pragma comment(lib, "secur32.lib")
#pragma comment(lib, "crypt32.lib")
#pragma comment(lib, "ncrypt.lib")
#define TLS_BUF_LIMIT (32 * 1024)
// ============================================================
// Certificate loading
// ============================================================
// Read entire file into malloc'd buffer. Caller must free.
static char *read_file(const char *path, int *out_len)
{
FILE *f = fopen(path, "rb");
if (!f) return NULL;
fseek(f, 0, SEEK_END);
long len = ftell(f);
fseek(f, 0, SEEK_SET);
if (len <= 0 || len > 1024 * 1024) {
fclose(f);
return NULL;
}
char *buf = malloc((size_t) len + 1);
if (!buf) {
fclose(f);
return NULL;
}
size_t nread = fread(buf, 1, (size_t) len, f);
fclose(f);
buf[nread] = '\0';
*out_len = (int) nread;
return buf;
}
// Decode PEM base64 content to DER binary. Returns malloc'd buffer.
static BYTE *pem_to_der(const char *pem, int pem_len, DWORD *out_len)
{
DWORD len = 0;
if (!CryptStringToBinaryA(pem, pem_len, CRYPT_STRING_BASE64HEADER,
NULL, &len, NULL, NULL))
return NULL;
BYTE *der = malloc(len);
if (!der) return NULL;
if (!CryptStringToBinaryA(pem, pem_len, CRYPT_STRING_BASE64HEADER,
der, &len, NULL, NULL)) {
free(der);
return NULL;
}
*out_len = len;
return der;
}
// Import an in-memory PFX blob, acquire SChannel credential.
static int load_pfx_blob(const BYTE *pfx_data, DWORD pfx_len,
CredHandle *cred_out, PCCERT_CONTEXT *cert_ctx_out)
{
CRYPT_DATA_BLOB blob;
blob.pbData = (BYTE *) pfx_data;
blob.cbData = pfx_len;
HCERTSTORE store = PFXImportCertStore(&blob, L"", CRYPT_EXPORTABLE);
if (!store) return -1;
// Find first cert with a private key
PCCERT_CONTEXT cert_ctx = NULL;
while ((cert_ctx = CertEnumCertificatesInStore(store, cert_ctx)) != NULL) {
HCRYPTPROV_OR_NCRYPT_KEY_HANDLE hkey = 0;
DWORD key_spec = 0;
BOOL caller_free = FALSE;
if (CryptAcquireCertificatePrivateKey(cert_ctx,
CRYPT_ACQUIRE_ALLOW_NCRYPT_KEY_FLAG | CRYPT_ACQUIRE_SILENT_FLAG,
NULL, &hkey, &key_spec, &caller_free)) {
if (caller_free && hkey) {
if (key_spec == CERT_NCRYPT_KEY_SPEC)
NCryptFreeObject(hkey);
else
CryptReleaseContext(hkey, 0);
}
break;
}
}
if (!cert_ctx) {
CertCloseStore(store, 0);
return -1;
}
PCCERT_CONTEXT dup_ctx = CertDuplicateCertificateContext(cert_ctx);
SCHANNEL_CRED sc_cred = {0};
sc_cred.dwVersion = SCHANNEL_CRED_VERSION;
sc_cred.cCreds = 1;
sc_cred.paCred = &dup_ctx;
sc_cred.grbitEnabledProtocols = SP_PROT_TLS1_2_SERVER;
TimeStamp expiry;
SECURITY_STATUS ss = AcquireCredentialsHandleA(
NULL, UNISP_NAME_A, SECPKG_CRED_INBOUND,
NULL, &sc_cred, NULL, NULL,
cred_out, &expiry);
if (ss != SEC_E_OK) {
CertFreeCertificateContext(dup_ctx);
CertCloseStore(store, 0);
return -1;
}
*cert_ctx_out = dup_ctx;
// Keep store open — SChannel needs access to the cert+key
return 0;
}
// Load PEM cert+key using pure CryptoAPI (no external tools).
// Decodes PEM, imports key into a temporary CAPI keyset, exports as
// in-memory PFX, then imports via PFXImportCertStore.
static int load_pem_credential(string cert_file, string key_file,
CredHandle *cred_out, PCCERT_CONTEXT *cert_ctx_out)
{
int result = -1;
char cert_z[1024], key_z[1024], container[64];
char *cert_pem = NULL, *key_pem = NULL;
BYTE *cert_der = NULL, *key_der = NULL;
BYTE *rsa_blob = NULL, *pkcs8_buf = NULL;
PCCERT_CONTEXT cert_ctx = NULL;
HCRYPTPROV hprov = 0;
HCRYPTKEY hkey = 0;
HCERTSTORE mem_store = NULL;
CRYPT_DATA_BLOB pfx = {0};
int cert_pem_len, key_pem_len;
DWORD cert_der_len, key_der_len, rsa_blob_len, pkcs8_len;
snprintf(container, sizeof(container), "tls_tmp_%lu",
(unsigned long) GetCurrentProcessId());
// Null-terminate file paths
if (cert_file.len >= (int) sizeof(cert_z)) goto done;
memcpy(cert_z, cert_file.ptr, cert_file.len);
cert_z[cert_file.len] = '\0';
if (key_file.len >= (int) sizeof(key_z)) goto done;
memcpy(key_z, key_file.ptr, key_file.len);
key_z[key_file.len] = '\0';
// --- Certificate: PEM -> DER -> CERT_CONTEXT ---
cert_pem = read_file(cert_z, &cert_pem_len);
if (!cert_pem) goto done;
cert_der = pem_to_der(cert_pem, cert_pem_len, &cert_der_len);
if (!cert_der) goto done;
cert_ctx = CertCreateCertificateContext(
X509_ASN_ENCODING | PKCS_7_ASN_ENCODING, cert_der, cert_der_len);
if (!cert_ctx) goto done;
// --- Private key: PEM -> DER -> CAPI RSA blob ---
key_pem = read_file(key_z, &key_pem_len);
if (!key_pem) goto done;
key_der = pem_to_der(key_pem, key_pem_len, &key_der_len);
if (!key_der) goto done;
if (strstr(key_pem, "-----BEGIN PRIVATE KEY-----")) {
// PKCS#8: unwrap to get inner RSA key
if (!CryptDecodeObjectEx(X509_ASN_ENCODING, PKCS_PRIVATE_KEY_INFO,
key_der, key_der_len, CRYPT_DECODE_ALLOC_FLAG,
NULL, &pkcs8_buf, &pkcs8_len))
goto done;
CRYPT_PRIVATE_KEY_INFO *pki = (CRYPT_PRIVATE_KEY_INFO *) pkcs8_buf;
if (!CryptDecodeObjectEx(X509_ASN_ENCODING, PKCS_RSA_PRIVATE_KEY,
pki->PrivateKey.pbData, pki->PrivateKey.cbData,
CRYPT_DECODE_ALLOC_FLAG, NULL, &rsa_blob, &rsa_blob_len))
goto done;
} else {
// Traditional RSA (BEGIN RSA PRIVATE KEY)
if (!CryptDecodeObjectEx(X509_ASN_ENCODING, PKCS_RSA_PRIVATE_KEY,
key_der, key_der_len, CRYPT_DECODE_ALLOC_FLAG,
NULL, &rsa_blob, &rsa_blob_len))
goto done;
}
// --- Import key into temporary CAPI keyset ---
CryptAcquireContextA(&hprov, container, MS_ENH_RSA_AES_PROV_A,
PROV_RSA_AES, CRYPT_DELETEKEYSET);
hprov = 0;
if (!CryptAcquireContextA(&hprov, container, MS_ENH_RSA_AES_PROV_A,
PROV_RSA_AES, CRYPT_NEWKEYSET))
goto done;
if (!CryptImportKey(hprov, rsa_blob, rsa_blob_len, 0, CRYPT_EXPORTABLE, &hkey))
goto done;
// --- Bind key to cert, export PFX in memory ---
{
wchar_t containerW[64];
MultiByteToWideChar(CP_ACP, 0, container, -1, containerW, 64);
CRYPT_KEY_PROV_INFO prov_info = {0};
prov_info.pwszContainerName = containerW;
prov_info.pwszProvName = (LPWSTR) L"Microsoft Enhanced RSA and AES Cryptographic Provider";
prov_info.dwProvType = PROV_RSA_AES;
prov_info.dwKeySpec = AT_KEYEXCHANGE;
if (!CertSetCertificateContextProperty(cert_ctx,
CERT_KEY_PROV_INFO_PROP_ID, 0, &prov_info))
goto done;
}
mem_store = CertOpenStore(CERT_STORE_PROV_MEMORY, 0, 0, 0, NULL);
if (!mem_store) goto done;
if (!CertAddCertificateContextToStore(mem_store, cert_ctx,
CERT_STORE_ADD_ALWAYS, NULL))
goto done;
// Two-pass PFX export: get size, then export
pfx.cbData = 0;
pfx.pbData = NULL;
if (!PFXExportCertStoreEx(mem_store, &pfx, L"", NULL, EXPORT_PRIVATE_KEYS))
goto done;
pfx.pbData = malloc(pfx.cbData);
if (!pfx.pbData) goto done;
if (!PFXExportCertStoreEx(mem_store, &pfx, L"", NULL, EXPORT_PRIVATE_KEYS))
goto done;
// --- Import PFX blob and acquire SChannel credential ---
result = load_pfx_blob(pfx.pbData, pfx.cbData, cred_out, cert_ctx_out);
done:
free(pfx.pbData);
if (mem_store) CertCloseStore(mem_store, 0);
if (hkey) CryptDestroyKey(hkey);
if (hprov) CryptReleaseContext(hprov, 0);
{ HCRYPTPROV tmp = 0;
CryptAcquireContextA(&tmp, container, MS_ENH_RSA_AES_PROV_A,
PROV_RSA_AES, CRYPT_DELETEKEYSET); }
if (pkcs8_buf) LocalFree(pkcs8_buf);
if (rsa_blob) LocalFree(rsa_blob);
free(key_der);
free(key_pem);
free(cert_der);
free(cert_pem);
if (cert_ctx) CertFreeCertificateContext(cert_ctx);
return result;
}
// ============================================================
// Global init/free (no-ops for SChannel)
// ============================================================
void tls_global_init(void)
{
}
void tls_global_free(void)
{
}
// ============================================================
// Server init/free
// ============================================================
int tls_server_init(TLS_Server *server, string cert_file, string key_file)
{
memset(server, 0, sizeof(*server));
server->num_certs = 0;
int ret = load_pem_credential(cert_file, key_file, &server->cred, &server->cert_ctx);
if (ret < 0)
return -1;
return 0;
}
void tls_server_free(TLS_Server *server)
{
FreeCredentialsHandle(&server->cred);
if (server->cert_ctx) {
HCRYPTPROV_OR_NCRYPT_KEY_HANDLE hkey = 0;
DWORD key_spec = 0;
BOOL caller_free = FALSE;
if (CryptAcquireCertificatePrivateKey(server->cert_ctx,
CRYPT_ACQUIRE_ONLY_NCRYPT_KEY_FLAG, NULL, &hkey, &key_spec, &caller_free)) {
if (caller_free && hkey)
NCryptFreeObject(hkey);
}
CertFreeCertificateContext(server->cert_ctx);
}
for (int i = 0; i < server->num_certs; i++)
FreeCredentialsHandle(&server->certs[i].cred);
}
int tls_server_add_cert(TLS_Server *server, string domain, string cert_file, string key_file)
{
if (server->num_certs >= TLS_CERT_LIMIT)
return -1;
TLS_Cert *cert = &server->certs[server->num_certs];
if (domain.len >= (int) sizeof(cert->domain))
return -1;
PCCERT_CONTEXT cert_ctx = NULL;
int ret = load_pem_credential(cert_file, key_file, &cert->cred, &cert_ctx);
if (ret < 0)
return -1;
if (cert_ctx) {
HCRYPTPROV_OR_NCRYPT_KEY_HANDLE hkey = 0;
DWORD key_spec = 0;
BOOL caller_free = FALSE;
if (CryptAcquireCertificatePrivateKey(cert_ctx,
CRYPT_ACQUIRE_ONLY_NCRYPT_KEY_FLAG, NULL, &hkey, &key_spec, &caller_free)) {
if (caller_free && hkey)
NCryptFreeObject(hkey);
}
CertFreeCertificateContext(cert_ctx);
}
memcpy(cert->domain, domain.ptr, domain.len);
cert->domain[domain.len] = '\0';
server->num_certs++;
return 0;
}
// ============================================================
// Connection init/free
// ============================================================
int tls_conn_init(TLS_Conn *conn, TLS_Server *server)
{
memset(conn, 0, sizeof(*conn));
conn->ctx_valid = false;
conn->handshake = true;
conn->cred = &server->cred;
byte_queue_init(&conn->in_buf, TLS_BUF_LIMIT);
byte_queue_init(&conn->out_buf, TLS_BUF_LIMIT);
conn->pending = NULL;
conn->pending_off = 0;
conn->pending_len = 0;
SecInvalidateHandle(&conn->ctx);
return 0;
}
void tls_conn_free(TLS_Conn *conn)
{
if (conn->ctx_valid)
DeleteSecurityContext(&conn->ctx);
byte_queue_free(&conn->in_buf);
byte_queue_free(&conn->out_buf);
free(conn->pending);
}
// ============================================================
// Handshake
// ============================================================
int tls_conn_handshake(TLS_Conn *conn)
{
assert(conn->handshake);
// Read available ciphertext from in_buf
string in = byte_queue_read_buf(&conn->in_buf);
if (!in.ptr || in.len == 0) {
byte_queue_read_ack(&conn->in_buf, 0);
return 0;
}
int in_avail = (int) in.len;
// Input buffers
SecBuffer in_bufs[2];
in_bufs[0].BufferType = SECBUFFER_TOKEN;
in_bufs[0].pvBuffer = in.ptr;
in_bufs[0].cbBuffer = (unsigned long) in_avail;
in_bufs[1].BufferType = SECBUFFER_EMPTY;
in_bufs[1].pvBuffer = NULL;
in_bufs[1].cbBuffer = 0;
SecBufferDesc in_desc;
in_desc.ulVersion = SECBUFFER_VERSION;
in_desc.cBuffers = 2;
in_desc.pBuffers = in_bufs;
// Output buffers
SecBuffer out_bufs[1];
out_bufs[0].BufferType = SECBUFFER_TOKEN;
out_bufs[0].pvBuffer = NULL;
out_bufs[0].cbBuffer = 0;
SecBufferDesc out_desc;
out_desc.ulVersion = SECBUFFER_VERSION;
out_desc.cBuffers = 1;
out_desc.pBuffers = out_bufs;
DWORD flags = ASC_REQ_STREAM
| ASC_REQ_SEQUENCE_DETECT
| ASC_REQ_REPLAY_DETECT
| ASC_REQ_CONFIDENTIALITY
| ASC_REQ_ALLOCATE_MEMORY;
DWORD out_flags = 0;
TimeStamp expiry;
SECURITY_STATUS ss = AcceptSecurityContext(
conn->cred,
conn->ctx_valid ? &conn->ctx : NULL,
&in_desc,
flags,
0,
conn->ctx_valid ? NULL : &conn->ctx,
&out_desc,
&out_flags,
&expiry);
if (ss == SEC_E_OK || ss == SEC_I_CONTINUE_NEEDED)
conn->ctx_valid = true;
// Copy output token to out_buf
if (out_bufs[0].pvBuffer && out_bufs[0].cbBuffer > 0) {
byte_queue_write_setmincap(&conn->out_buf, out_bufs[0].cbBuffer);
byte_queue_write(&conn->out_buf, out_bufs[0].pvBuffer, out_bufs[0].cbBuffer);
FreeContextBuffer(out_bufs[0].pvBuffer);
}
// Calculate how much input was consumed
int consumed = in_avail;
if (in_bufs[1].BufferType == SECBUFFER_EXTRA && in_bufs[1].cbBuffer > 0)
consumed = in_avail - (int) in_bufs[1].cbBuffer;
if (ss == SEC_E_INCOMPLETE_MESSAGE) {
// SChannel didn't consume anything
byte_queue_read_ack(&conn->in_buf, 0);
return 0;
}
byte_queue_read_ack(&conn->in_buf, consumed);
if (ss == SEC_I_CONTINUE_NEEDED)
return 0;
if (ss == SEC_E_OK) {
conn->handshake = false;
ss = QueryContextAttributes(&conn->ctx, SECPKG_ATTR_STREAM_SIZES, &conn->sizes);
if (ss != SEC_E_OK)
return -1;
return 1;
}
return -1;
}
// ============================================================
// Network I/O (ciphertext ↔ socket)
// ============================================================
char *tls_conn_net_write_buf(TLS_Conn *conn, int *cap)
{
byte_queue_write_setmincap(&conn->in_buf, 4096);
string bv = byte_queue_write_buf(&conn->in_buf);
if (!bv.ptr || bv.len == 0) {
byte_queue_write_ack(&conn->in_buf, 0);
return NULL;
}
*cap = (int) bv.len;
return (char *) bv.ptr;
}
void tls_conn_net_write_ack(TLS_Conn *conn, int num)
{
byte_queue_write_ack(&conn->in_buf, num);
}
char *tls_conn_net_read_buf(TLS_Conn *conn, int *num)
{
string bv = byte_queue_read_buf(&conn->out_buf);
if (!bv.ptr || bv.len == 0) {
byte_queue_read_ack(&conn->out_buf, 0);
return NULL;
}
*num = (int) bv.len;
return (char *) bv.ptr;
}
void tls_conn_net_read_ack(TLS_Conn *conn, int num)
{
byte_queue_read_ack(&conn->out_buf, num);
}
// ============================================================
// Application I/O (encrypt/decrypt)
// ============================================================
int tls_conn_app_write(TLS_Conn *conn, char *src, int num)
{
assert(!conn->handshake);
if (num <= 0) return 0;
int max_msg = (int) conn->sizes.cbMaximumMessage;
if (num > max_msg)
num = max_msg;
int header_size = (int) conn->sizes.cbHeader;
int trailer_size = (int) conn->sizes.cbTrailer;
int total = header_size + num + trailer_size;
// Ensure output buffer has enough space
byte_queue_write_setmincap(&conn->out_buf, total);
string bv = byte_queue_write_buf(&conn->out_buf);
if (!bv.ptr || (int) bv.len < total) {
// Try with less data
if (!bv.ptr || (int) bv.len < header_size + trailer_size + 1) {
byte_queue_write_ack(&conn->out_buf, 0);
return 0;
}
num = (int) bv.len - header_size - trailer_size;
total = header_size + num + trailer_size;
}
char *out_ptr = (char *) bv.ptr;
// Copy plaintext into the data portion
memcpy(out_ptr + header_size, src, num);
// Set up SecBuffers for in-place encryption
SecBuffer bufs[4];
bufs[0].BufferType = SECBUFFER_STREAM_HEADER;
bufs[0].pvBuffer = out_ptr;
bufs[0].cbBuffer = (unsigned long) header_size;
bufs[1].BufferType = SECBUFFER_DATA;
bufs[1].pvBuffer = out_ptr + header_size;
bufs[1].cbBuffer = (unsigned long) num;
bufs[2].BufferType = SECBUFFER_STREAM_TRAILER;
bufs[2].pvBuffer = out_ptr + header_size + num;
bufs[2].cbBuffer = (unsigned long) trailer_size;
bufs[3].BufferType = SECBUFFER_EMPTY;
bufs[3].pvBuffer = NULL;
bufs[3].cbBuffer = 0;
SecBufferDesc desc;
desc.ulVersion = SECBUFFER_VERSION;
desc.cBuffers = 4;
desc.pBuffers = bufs;
SECURITY_STATUS ss = EncryptMessage(&conn->ctx, 0, &desc, 0);
if (ss != SEC_E_OK) {
byte_queue_write_ack(&conn->out_buf, 0);
return -1;
}
int written = (int)(bufs[0].cbBuffer + bufs[1].cbBuffer + bufs[2].cbBuffer);
byte_queue_write_ack(&conn->out_buf, written);
return num;
}
int tls_conn_app_read(TLS_Conn *conn, char *dst, int cap)
{
assert(!conn->handshake);
// Drain any pending plaintext from a previous partial read
if (conn->pending_len > 0) {
int n = conn->pending_len;
if (n > cap) n = cap;
memcpy(dst, conn->pending + conn->pending_off, n);
conn->pending_off += n;
conn->pending_len -= n;
if (conn->pending_len == 0) {
free(conn->pending);
conn->pending = NULL;
conn->pending_off = 0;
}
return n;
}
string in = byte_queue_read_buf(&conn->in_buf);
if (!in.ptr || in.len == 0) {
byte_queue_read_ack(&conn->in_buf, 0);
return 0;
}
int in_avail = (int) in.len;
// DecryptMessage operates in-place
SecBuffer bufs[4];
bufs[0].BufferType = SECBUFFER_DATA;
bufs[0].pvBuffer = in.ptr;
bufs[0].cbBuffer = (unsigned long) in_avail;
bufs[1].BufferType = SECBUFFER_EMPTY;
bufs[1].pvBuffer = NULL;
bufs[1].cbBuffer = 0;
bufs[2].BufferType = SECBUFFER_EMPTY;
bufs[2].pvBuffer = NULL;
bufs[2].cbBuffer = 0;
bufs[3].BufferType = SECBUFFER_EMPTY;
bufs[3].pvBuffer = NULL;
bufs[3].cbBuffer = 0;
SecBufferDesc desc;
desc.ulVersion = SECBUFFER_VERSION;
desc.cBuffers = 4;
desc.pBuffers = bufs;
SECURITY_STATUS ss = DecryptMessage(&conn->ctx, &desc, 0, NULL);
if (ss == SEC_E_INCOMPLETE_MESSAGE) {
byte_queue_read_ack(&conn->in_buf, 0);
return 0;
}
if (ss != SEC_E_OK && ss != SEC_I_RENEGOTIATE) {
byte_queue_read_ack(&conn->in_buf, 0);
return -1;
}
// Find decrypted data and extra ciphertext buffers
SecBuffer *data_buf = NULL;
SecBuffer *extra_buf = NULL;
for (int i = 0; i < 4; i++) {
if (bufs[i].BufferType == SECBUFFER_DATA)
data_buf = &bufs[i];
else if (bufs[i].BufferType == SECBUFFER_EXTRA)
extra_buf = &bufs[i];
}
int result = 0;
if (data_buf && data_buf->cbBuffer > 0) {
int total = (int) data_buf->cbBuffer;
int n = total;
if (n > cap) n = cap;
memcpy(dst, data_buf->pvBuffer, n);
result = n;
// Save excess plaintext for next call
int leftover = total - n;
if (leftover > 0) {
conn->pending = malloc(leftover);
if (conn->pending) {
memcpy(conn->pending, (char *) data_buf->pvBuffer + n, leftover);
conn->pending_off = 0;
conn->pending_len = leftover;
}
}
}
// Consume processed input, keeping any extra ciphertext
int consumed = in_avail;
if (extra_buf && extra_buf->cbBuffer > 0)
consumed = in_avail - (int) extra_buf->cbBuffer;
byte_queue_read_ack(&conn->in_buf, consumed);
if (ss == SEC_I_RENEGOTIATE)
return result > 0 ? result : 0;
return result;
}
int tls_conn_needs_flushing(TLS_Conn *conn)
{
return !byte_queue_empty(&conn->out_buf);
}
#endif // TLS_SCHANNEL
#endif // TLS_ENABLED