#include "quakedef.h" #if defined(HAVE_WINSSPI) /*regarding HAVE_DTLS DTLS1.0 is supported from win8 onwards, or patched-win7 DTLS1.2 is supported from win10-1607 onwards. unlike the other tls providers we use pfx files (to work around microsoft making it so fucking hard to actually import/export the damn private key any other way) pfx files are encrypted with your username as a password convert from pem to pfx: openssl pkcs12 -inkey private.pem -in fullchain.pem -export -nodes -out identity.pfx -passout pass:$USER (in the above formula, swap out $USER for %USERNAME% if you're using a windows cmd prompt) you can start fte with an arg like the following for a fully CA-signed cert: fteqwsv -pfx c:/foo/bar.pfx (otherwise it'll use identity.pfx from your basedir, autogenerated with some dodgy info) TODO: RTC connections do not validate the client peer. TODO: get someone else to figure out the PSK stuff. no way can I test/debug/write that without documentation nor working drivers. Note: testing this stuff is a pain when eg browsers do NOT support DTLS1.0 any more. */ #include "winquake.h" #include "netinc.h" #define SECURITY_WIN32 #include #include #include #include #define SP_PROT_TLS1_1_SERVER 0x00000100 #define SP_PROT_TLS1_1_CLIENT 0x00000200 #define SP_PROT_TLS1_2_SERVER 0x00000400 #define SP_PROT_TLS1_2_CLIENT 0x00000800 #define SP_PROT_DTLS_SERVER 0x00010000 #define SP_PROT_DTLS_CLIENT 0x00020000 #define SP_PROT_DTLS1_0_SERVER SP_PROT_DTLS_SERVER #define SP_PROT_DTLS1_0_CLIENT SP_PROT_DTLS_CLIENT #define SP_PROT_DTLS1_2_SERVER 0x00040000 #define SP_PROT_DTLS1_2_CLIENT 0x00080000 #define SP_PROT_DTLS1_X_SERVER (SP_PROT_DTLS1_0_SERVER | SP_PROT_DTLS1_2_SERVER) #define SP_PROT_DTLS1_X_CLIENT (SP_PROT_DTLS1_0_CLIENT | SP_PROT_DTLS1_2_CLIENT) //avoid the use of outdated/insecure protocols //so no ssl2/ssl3 #define USE_PROT_SERVER (SP_PROT_TLS1_SERVER | SP_PROT_TLS1_1_SERVER | SP_PROT_TLS1_2_SERVER) #define USE_PROT_CLIENT (SP_PROT_TLS1_CLIENT | SP_PROT_TLS1_1_CLIENT | SP_PROT_TLS1_2_CLIENT) #define USE_PROT_DGRAM_SERVER (SP_PROT_DTLS1_X_SERVER) #define USE_PROT_DGRAM_CLIENT (SP_PROT_DTLS1_X_CLIENT) #ifndef szOID_RSA_SHA512RSA #define szOID_RSA_SHA512RSA "1.2.840.113549.1.1.13" #endif #ifndef SCH_CRED_SNI_CREDENTIAL #define SCH_CRED_SNI_CREDENTIAL 0x00080000 #endif #ifndef SEC_I_MESSAGE_FRAGMENT #define SEC_I_MESSAGE_FRAGMENT 0x00090364L #endif #ifndef SEC_E_INVALID_PARAMETER #define SEC_E_INVALID_PARAMETER 0x8009035DL #endif #ifndef SECBUFFER_ALERT #define SECBUFFER_ALERT 17 #endif #ifndef SECPKG_ATTR_DTLS_MTU #define SECPKG_ATTR_DTLS_MTU 34 #endif #ifndef CRYPT_ARCHIVABLE #define CRYPT_ARCHIVABLE 0x00004000 #endif //hungarian ensures we hit no macros. static struct { dllhandle_t *lib; SECURITY_STATUS (WINAPI *pDecryptMessage) (PCtxtHandle,PSecBufferDesc,ULONG,PULONG); SECURITY_STATUS (WINAPI *pEncryptMessage) (PCtxtHandle,ULONG,PSecBufferDesc,ULONG); SECURITY_STATUS (WINAPI *pAcquireCredentialsHandleA) (SEC_CHAR*,SEC_CHAR*,ULONG,PLUID,PVOID,SEC_GET_KEY_FN,PVOID,PCredHandle,PTimeStamp); // SECURITY_STATUS (WINAPI *pInitializeSecurityContextA) (PCredHandle,PCtxtHandle,SEC_CHAR*,ULONG,ULONG,ULONG,PSecBufferDesc,ULONG,PCtxtHandle,PSecBufferDesc,PULONG,PTimeStamp); SECURITY_STATUS (WINAPI *pInitializeSecurityContextW) (PCredHandle,PCtxtHandle,SEC_WCHAR*,ULONG,ULONG,ULONG,PSecBufferDesc,ULONG,PCtxtHandle,PSecBufferDesc,PULONG,PTimeStamp); SECURITY_STATUS (WINAPI *pAcceptSecurityContext) (PCredHandle,PCtxtHandle,PSecBufferDesc,unsigned long,unsigned long,PCtxtHandle,PSecBufferDesc,unsigned long SEC_FAR *,PTimeStamp); SECURITY_STATUS (WINAPI *pCompleteAuthToken) (PCtxtHandle,PSecBufferDesc); SECURITY_STATUS (WINAPI *pSetContextAttributesA) (PCtxtHandle,unsigned long,void*,unsigned long); SECURITY_STATUS (WINAPI *pQueryContextAttributesA) (PCtxtHandle,ULONG,PVOID); SECURITY_STATUS (WINAPI *pFreeCredentialsHandle) (PCredHandle); SECURITY_STATUS (WINAPI *pDeleteSecurityContext) (PCtxtHandle); } secur; static struct { dllhandle_t *lib; BOOL (WINAPI *pCertGetCertificateChain) (HCERTCHAINENGINE,PCCERT_CONTEXT,LPFILETIME,HCERTSTORE,PCERT_CHAIN_PARA,DWORD,LPVOID,PCCERT_CHAIN_CONTEXT*); BOOL (WINAPI *pCertVerifyCertificateChainPolicy) (LPCSTR,PCCERT_CHAIN_CONTEXT,PCERT_CHAIN_POLICY_PARA,PCERT_CHAIN_POLICY_STATUS); void (WINAPI *pCertFreeCertificateChain) (PCCERT_CHAIN_CONTEXT); PCCERT_CONTEXT (WINAPI *pCertCreateCertificateContext) (DWORD dwCertEncodingType, const BYTE *pbCertEncoded, DWORD cbCertEncoded); DWORD (WINAPI *pCertNameToStrA) (DWORD dwCertEncodingType, PCERT_NAME_BLOB pName, DWORD dwStrType, LPCSTR psz, DWORD csz); BOOL (WINAPI *pCertFreeCertificateContext) (PCCERT_CONTEXT pCertContext); PCCERT_CONTEXT (WINAPI *pCertCreateSelfSignCertificate) (HCRYPTPROV,PCERT_NAME_BLOB,DWORD,PCRYPT_KEY_PROV_INFO,PCRYPT_ALGORITHM_IDENTIFIER,PSYSTEMTIME,PSYSTEMTIME,PCERT_EXTENSIONS); BOOL (WINAPI *pCertStrToNameA) (DWORD,LPCSTR,DWORD,void *,BYTE *,DWORD *,LPCSTR *); HCERTSTORE (WINAPI *pCertOpenStore) (LPCSTR lpszStoreProvider, DWORD dwEncodingType, HCRYPTPROV hCryptProv, DWORD dwFlags, const void *pvPara); BOOL (WINAPI *pCertAddCertificateContextToStore) (HCERTSTORE hCertStore, PCCERT_CONTEXT pCertContext, DWORD dwAddDisposition, PCCERT_CONTEXT *ppStoreContext); BOOL (WINAPI *pPFXExportCertStoreEx) (HCERTSTORE hStore, CRYPT_DATA_BLOB *pPFX, LPCWSTR szPassword, void *pvPara, DWORD dwFlags); BOOL (WINAPI *pCertCloseStore) (HCERTSTORE hCertStore, DWORD dwFlags); HCERTSTORE (WINAPI *pPFXImportCertStore) (CRYPT_DATA_BLOB *pPFX, LPCWSTR szPassword, DWORD dwFlags); PCCERT_CONTEXT (WINAPI *pCertFindCertificateInStore) (HCERTSTORE hCertStore, DWORD dwCertEncodingType, DWORD dwFindFlags, DWORD dwFindType, const void *pvFindPara, PCCERT_CONTEXT pPrevCertContext); BOOL (WINAPI *pCryptAcquireCertificatePrivateKey) (PCCERT_CONTEXT pCert, DWORD dwFlags, void *pvParameters, HCRYPTPROV *phCryptProvOrNCryptKey, DWORD *pdwKeySpec, BOOL *pfCallerFreeProvOrNCryptKey); BOOL (WINAPI *pCertSetCertificateContextProperty) (PCCERT_CONTEXT pCertContext, DWORD dwPropId, DWORD dwFlags, const void *pvData); } crypt; static struct { dllhandle_t *lib; BOOL (WINAPI *pCryptAcquireContextW) (HCRYPTPROV *phProv, LPCWSTR szContainer, LPCWSTR szProvider, DWORD dwProvType, DWORD dwFlags); BOOL (WINAPI *pCryptGenKey) (HCRYPTPROV hProv, ALG_ID Algid, DWORD dwFlags, HCRYPTKEY *phKey); } advapi; void SSL_Init(void) { dllfunction_t secur_functable[] = { {(void**)&secur.pDecryptMessage, "DecryptMessage"}, {(void**)&secur.pEncryptMessage, "EncryptMessage"}, {(void**)&secur.pAcquireCredentialsHandleA, "AcquireCredentialsHandleA"}, // {(void**)&secur.pInitializeSecurityContextA, "InitializeSecurityContextA"}, {(void**)&secur.pInitializeSecurityContextW, "InitializeSecurityContextW"}, {(void**)&secur.pAcceptSecurityContext, "AcceptSecurityContext"}, {(void**)&secur.pCompleteAuthToken, "CompleteAuthToken"}, {(void**)&secur.pSetContextAttributesA, "SetContextAttributesA"}, {(void**)&secur.pQueryContextAttributesA, "QueryContextAttributesA"}, {(void**)&secur.pFreeCredentialsHandle, "FreeCredentialsHandle"}, {(void**)&secur.pDeleteSecurityContext, "DeleteSecurityContext"}, {NULL, NULL} }; dllfunction_t crypt_functable[] = { {(void**)&crypt.pCertGetCertificateChain, "CertGetCertificateChain"}, {(void**)&crypt.pCertVerifyCertificateChainPolicy, "CertVerifyCertificateChainPolicy"}, {(void**)&crypt.pCertFreeCertificateChain, "CertFreeCertificateChain"}, {(void**)&crypt.pCertCreateCertificateContext, "CertCreateCertificateContext"}, {(void**)&crypt.pCertNameToStrA, "CertNameToStrA"}, {(void**)&crypt.pCertFreeCertificateContext, "CertFreeCertificateContext"}, {(void**)&crypt.pCertCreateSelfSignCertificate, "CertCreateSelfSignCertificate"}, {(void**)&crypt.pCertStrToNameA, "CertStrToNameA"}, {(void**)&crypt.pCertOpenStore, "CertOpenStore"}, {(void**)&crypt.pCertAddCertificateContextToStore, "CertAddCertificateContextToStore"}, {(void**)&crypt.pPFXExportCertStoreEx, "PFXExportCertStoreEx"}, {(void**)&crypt.pCertCloseStore, "CertCloseStore"}, {(void**)&crypt.pPFXImportCertStore, "PFXImportCertStore"}, {(void**)&crypt.pCertFindCertificateInStore, "CertFindCertificateInStore"}, {(void**)&crypt.pCryptAcquireCertificatePrivateKey, "CryptAcquireCertificatePrivateKey"}, {(void**)&crypt.pCertSetCertificateContextProperty, "CertSetCertificateContextProperty"}, {NULL, NULL} }; dllfunction_t advapi_functable[] = { {(void**)&advapi.pCryptAcquireContextW, "CryptAcquireContextW"}, {(void**)&advapi.pCryptGenKey, "CryptGenKey"}, {NULL, NULL} }; if (!secur.lib) secur.lib = Sys_LoadLibrary("secur32.dll", secur_functable); if (!crypt.lib) crypt.lib = Sys_LoadLibrary("crypt32.dll", crypt_functable); if (!advapi.lib) advapi.lib = Sys_LoadLibrary("advapi32.dll", advapi_functable); } qboolean SSL_Inited(void) { return !!secur.lib && !!crypt.lib && !!advapi.lib; } #define MessageAttribute (ISC_REQ_SEQUENCE_DETECT | ISC_REQ_REPLAY_DETECT | ISC_REQ_CONFIDENTIALITY | /*ISC_REQ_EXTENDED_ERROR |*/ ISC_REQ_ALLOCATE_MEMORY | ISC_REQ_MANUAL_CRED_VALIDATION | ISC_REQ_USE_SUPPLIED_CREDS) struct sslbuf { size_t datasize; char *data; size_t avail; }; typedef struct { vfsfile_t funcs; vfsfile_t *stream; wchar_t wpeername[256]; qboolean datagram; enum { HS_ESTABLISHED, HS_ERROR, HS_STARTCLIENT, HS_CLIENT, HS_STARTSERVER, HS_SERVER } handshaking; struct sslbuf outraw; struct sslbuf outcrypt; struct sslbuf inraw; struct sslbuf incrypt; CredHandle cred; SecHandle sechnd; int headersize, mtu, footersize; //schannel is strict about its mtus. char headerdata[1024], footerdata[1024]; double resendtimer; //for the client so it can keep retrying the handshake. #ifdef HAVE_DTLS void *cbctx; neterr_t (*transmit)(void *cbctx, const qbyte *data, size_t datasize); #endif } sslfile_t; static int SSPI_ExpandBuffer(struct sslbuf *buf, size_t bytes) { if (bytes < buf->datasize) return buf->datasize; Z_ReallocElements((void**)&buf->data, &buf->datasize, bytes, 1); return bytes; } static int SSPI_CopyIntoBuffer(struct sslbuf *buf, const void *data, unsigned int bytes, qboolean expand) { if (bytes > buf->datasize - buf->avail) { if (!expand || SSPI_ExpandBuffer(buf, buf->avail + bytes + 1024) < buf->avail + bytes) bytes = buf->datasize - buf->avail; } memcpy(buf->data + buf->avail, data, bytes); buf->avail += bytes; return bytes; } static void SSPI_Error(sslfile_t *f, const char *error, ...) { va_list argptr; char string[1024]; va_start (argptr, error); vsnprintf (string,sizeof(string)-1, error,argptr); va_end (argptr); f->handshaking = HS_ERROR; if (*string) { if (f->datagram) Con_Printf(CON_ERROR "%s", string); else Sys_Printf(CON_ERROR "%s", string); } if (f->stream) VFS_CLOSE(f->stream); secur.pDeleteSecurityContext(&f->sechnd); secur.pFreeCredentialsHandle(&f->cred); f->stream = NULL; } static neterr_t SSPI_TryFlushCryptOut(sslfile_t *f) { int sent; if (f->outcrypt.avail) { #ifdef HAVE_DTLS if (f->transmit) { neterr_t e = f->transmit(f->cbctx, f->outcrypt.data, f->outcrypt.avail); f->outcrypt.avail = 0; return e; } #endif sent = VFS_WRITE(f->stream, f->outcrypt.data, f->outcrypt.avail); } else return NETERR_SENT; if (f->datagram) f->outcrypt.avail = 0; //anything unsent is a dropped packet... else if (sent > 0) { memmove(f->outcrypt.data, f->outcrypt.data + sent, f->outcrypt.avail - sent); f->outcrypt.avail -= sent; } return NETERR_SENT; } static int SSPI_CheckNewInCrypt(sslfile_t *f) { int newd; if (!f->stream) return VFS_ERROR_EOF; newd = VFS_READ(f->stream, f->incrypt.data+f->incrypt.avail, f->incrypt.datasize - f->incrypt.avail); if (newd < 0) return newd; else f->incrypt.avail += newd; return 0; } //convert inbound crypt->data static void SSPI_Decode(sslfile_t *f) { SECURITY_STATUS ss; SecBufferDesc BuffDesc; SecBuffer SecBuff[4]; ULONG ulQop = 0; SecBuffer *extra = NULL; int i; if (!f->incrypt.avail) return; BuffDesc.ulVersion = SECBUFFER_VERSION; BuffDesc.cBuffers = countof(SecBuff); BuffDesc.pBuffers = SecBuff; SecBuff[0].BufferType = SECBUFFER_DATA; SecBuff[0].cbBuffer = f->incrypt.avail; SecBuff[0].pvBuffer = f->incrypt.data; SecBuff[1].BufferType = SECBUFFER_EMPTY; //space for header SecBuff[2].BufferType = SECBUFFER_EMPTY; //space for footer SecBuff[3].BufferType = SECBUFFER_EMPTY; //space for extra marker ss = secur.pDecryptMessage(&f->sechnd, &BuffDesc, 0, &ulQop); if (FAILED(ss)) { if (f->datagram) return; //some sort of corruption? ignore that packet. if (ss == SEC_E_INCOMPLETE_MESSAGE) { if (f->incrypt.avail == f->incrypt.datasize) SSPI_ExpandBuffer(&f->incrypt, f->incrypt.datasize+1024); return; //no error if its incomplete, we can just get more data later on. } switch(ss) { case SEC_E_DECRYPT_FAILURE: SSPI_Error(f, "DecryptMessage failed: SEC_E_DECRYPT_FAILURE\n", ss); break; case SEC_E_INVALID_HANDLE: SSPI_Error(f, "DecryptMessage failed: SEC_E_INVALID_HANDLE\n"); break; case SEC_E_UNFINISHED_CONTEXT_DELETED: SSPI_Error(f, "DecryptMessage failed: SEC_E_UNFINISHED_CONTEXT_DELETED\n"); break; //peer aborted? default: SSPI_Error(f, "DecryptMessage failed: %0#lx\n", ss); break; } return; } for (i = 0; i < BuffDesc.cBuffers; i++) { switch(SecBuff[i].BufferType) { case SECBUFFER_DATA: if (SSPI_CopyIntoBuffer(&f->inraw, SecBuff[i].pvBuffer, SecBuff[i].cbBuffer, true) != SecBuff[i].cbBuffer) SSPI_Error(f, "outraw buffer overflowed\n"); break; case SECBUFFER_EXTRA: if (extra) SSPI_Error(f, "multiple extra buffers\n"); extra = &SecBuff[i]; break; case SECBUFFER_EMPTY: case SECBUFFER_MISSING: case SECBUFFER_STREAM_TRAILER: case SECBUFFER_STREAM_HEADER: break; default: SSPI_Error(f, "got unexpected buffer type\n"); break; } } //retain the extra. if there's no extra then mark it so. if (extra) { memmove(f->incrypt.data, f->incrypt.data + (f->incrypt.avail - extra->cbBuffer), extra->cbBuffer); f->incrypt.avail = extra->cbBuffer; } else f->incrypt.avail = 0; } //convert outgoing data->crypt static neterr_t SSPI_Encode(sslfile_t *f) { SECURITY_STATUS ss; SecBufferDesc BuffDesc; SecBuffer SecBuff[4]; ULONG ulQop = 0; if (f->outcrypt.avail) { SSPI_TryFlushCryptOut(f); if (f->outcrypt.avail) return NETERR_CLOGGED; //don't flood too much } //don't corrupt the handshake data. if (f->handshaking) return NETERR_CLOGGED; if (!f->outraw.avail) return NETERR_SENT; BuffDesc.ulVersion = SECBUFFER_VERSION; BuffDesc.cBuffers = 4; BuffDesc.pBuffers = SecBuff; SecBuff[0].BufferType = SECBUFFER_STREAM_HEADER; SecBuff[0].cbBuffer = f->headersize; SecBuff[0].pvBuffer = f->headerdata; SecBuff[1].BufferType = SECBUFFER_DATA; SecBuff[1].cbBuffer = f->outraw.avail; SecBuff[1].pvBuffer = f->outraw.data; SecBuff[2].BufferType = SECBUFFER_STREAM_TRAILER; SecBuff[2].cbBuffer = f->footersize; SecBuff[2].pvBuffer = f->footerdata; SecBuff[3].BufferType = SECBUFFER_EMPTY; SecBuff[3].cbBuffer = 0; SecBuff[3].pvBuffer = NULL; ss = secur.pEncryptMessage(&f->sechnd, ulQop, &BuffDesc, 0); if (ss < 0) { switch (ss) { case SEC_E_ENCRYPT_FAILURE: SSPI_Error(f, "EncryptMessage failed SEC_E_ENCRYPT_FAILURE (in: %i, max out %i)\n", f->outraw.avail, f->outcrypt.avail); default: SSPI_Error(f, "EncryptMessage failed %x\n", ss); } return NETERR_DISCONNECTED; } f->outraw.avail = 0; //fixme: these should be made non-fatal. if (SSPI_CopyIntoBuffer(&f->outcrypt, SecBuff[0].pvBuffer, SecBuff[0].cbBuffer, true) < SecBuff[0].cbBuffer) { SSPI_Error(f, "crypt buffer overflowed\n"); return NETERR_DISCONNECTED; } if (SSPI_CopyIntoBuffer(&f->outcrypt, SecBuff[1].pvBuffer, SecBuff[1].cbBuffer, true) < SecBuff[1].cbBuffer) { SSPI_Error(f, "crypt buffer overflowed\n"); return NETERR_DISCONNECTED; } if (SSPI_CopyIntoBuffer(&f->outcrypt, SecBuff[2].pvBuffer, SecBuff[2].cbBuffer, true) < SecBuff[2].cbBuffer) { SSPI_Error(f, "crypt buffer overflowed\n"); return NETERR_DISCONNECTED; } return SSPI_TryFlushCryptOut(f); } char *narrowen(char *out, size_t outlen, wchar_t *wide); static DWORD VerifyKnownCertificates(DWORD status, wchar_t *domain, qbyte *data, size_t datasize, qboolean datagram) { size_t knownsize; void *knowncert; char realdomain[256]; unsigned int probs = 0; if (datagram) { if (status == CERT_E_UNTRUSTEDROOT || status == CERT_E_UNTRUSTEDTESTROOT) probs |= CERTLOG_MISSINGCA; if (status == CERT_E_EXPIRED) probs |= CERTLOG_EXPIRED; if (status == SEC_E_WRONG_PRINCIPAL) probs |= CERTLOG_WRONGHOST; if (status == CERT_E_UNTRUSTEDROOT || SUCCEEDED(status)) { #ifndef SERVERONLY if (CertLog_ConnectOkay(narrowen(realdomain, sizeof(realdomain), domain), data, datasize, probs)) status = SEC_E_OK; else #endif if (SUCCEEDED(status)) status = TRUST_E_EXPLICIT_DISTRUST; } return status; } narrowen(realdomain, sizeof(realdomain), domain); knowncert = TLS_GetKnownCertificate(realdomain, &knownsize); if (knowncert) { if (knownsize == datasize && !memcmp(data, knowncert, datasize)) { //what we know about matched if (status == CERT_E_UNTRUSTEDROOT || status == CERT_E_EXPIRED) status = SEC_E_OK; } else { if (status != CERT_E_EXPIRED) Con_Printf("%ls has an unexpected certificate\n", domain); if (status == SEC_E_OK) //we (think) we know better. status = TRUST_E_EXPLICIT_DISTRUST; } BZ_Free(knowncert); } #ifndef SERVERONLY //self-signed and expired certs are understandable in many situations. //prompt and cache (although this connection attempt will fail). if (status == CERT_E_UNTRUSTEDROOT || status == CERT_E_UNTRUSTEDTESTROOT) probs |= CERTLOG_MISSINGCA; else if (status == CERT_E_EXPIRED) probs |= CERTLOG_EXPIRED; else if (status == SEC_E_WRONG_PRINCIPAL) probs |= CERTLOG_WRONGHOST; else if (status != SEC_E_OK) probs |= CERTLOG_UNKNOWN; if (status == CERT_E_UNTRUSTEDROOT || status == CERT_E_UNTRUSTEDTESTROOT || status == CERT_E_EXPIRED) if (CertLog_ConnectOkay(realdomain, data, datasize, probs)) return SEC_E_OK; #endif return status; } static DWORD VerifyServerCertificate(PCCERT_CONTEXT pServerCert, PWSTR pwszServerName, DWORD dwCertFlags, qboolean datagram) { HTTPSPolicyCallbackData polHttps; CERT_CHAIN_POLICY_PARA PolicyPara; CERT_CHAIN_POLICY_STATUS PolicyStatus; CERT_CHAIN_PARA ChainPara; PCCERT_CHAIN_CONTEXT pChainContext; DWORD Status; LPSTR rgszUsages[] = { szOID_PKIX_KP_SERVER_AUTH, szOID_SERVER_GATED_CRYPTO, szOID_SGC_NETSCAPE }; DWORD cUsages = sizeof(rgszUsages) / sizeof(LPSTR); if(pServerCert == NULL) return SEC_E_WRONG_PRINCIPAL; if(!*pwszServerName) return SEC_E_WRONG_PRINCIPAL; // Build certificate chain. memset(&ChainPara, 0, sizeof(ChainPara)); ChainPara.cbSize = sizeof(ChainPara); ChainPara.RequestedUsage.dwType = USAGE_MATCH_TYPE_OR; ChainPara.RequestedUsage.Usage.cUsageIdentifier = cUsages; ChainPara.RequestedUsage.Usage.rgpszUsageIdentifier = rgszUsages; if (!crypt.pCertGetCertificateChain(NULL, pServerCert, NULL, pServerCert->hCertStore, &ChainPara, 0, NULL, &pChainContext)) { Status = GetLastError(); Sys_Printf("Error %#lx returned by CertGetCertificateChain!\n", Status); } else { // Validate certificate chain. memset(&polHttps, 0, sizeof(HTTPSPolicyCallbackData)); polHttps.cbStruct = sizeof(HTTPSPolicyCallbackData); polHttps.dwAuthType = AUTHTYPE_SERVER; polHttps.fdwChecks = dwCertFlags; polHttps.pwszServerName = pwszServerName; memset(&PolicyPara, 0, sizeof(PolicyPara)); PolicyPara.cbSize = sizeof(PolicyPara); PolicyPara.pvExtraPolicyPara = &polHttps; memset(&PolicyStatus, 0, sizeof(PolicyStatus)); PolicyStatus.cbSize = sizeof(PolicyStatus); if (!crypt.pCertVerifyCertificateChainPolicy(CERT_CHAIN_POLICY_SSL, pChainContext, &PolicyPara, &PolicyStatus)) { Status = GetLastError(); Sys_Printf("Error %#lx returned by CertVerifyCertificateChainPolicy!\n", Status); } else { Status = VerifyKnownCertificates(PolicyStatus.dwError, pwszServerName, pServerCert->pbCertEncoded, pServerCert->cbCertEncoded, datagram); if (Status) { char fmsg[512]; char *err; switch (Status) { case CERT_E_EXPIRED: err = "CERT_E_EXPIRED"; break; case CERT_E_VALIDITYPERIODNESTING: err = "CERT_E_VALIDITYPERIODNESTING"; break; case CERT_E_ROLE: err = "CERT_E_ROLE"; break; case CERT_E_PATHLENCONST: err = "CERT_E_PATHLENCONST"; break; case CERT_E_CRITICAL: err = "CERT_E_CRITICAL"; break; case CERT_E_PURPOSE: err = "CERT_E_PURPOSE"; break; case CERT_E_ISSUERCHAINING: err = "CERT_E_ISSUERCHAINING"; break; case CERT_E_MALFORMED: err = "CERT_E_MALFORMED"; break; case CERT_E_UNTRUSTEDROOT: err = "CERT_E_UNTRUSTEDROOT"; break; case CERT_E_CHAINING: err = "CERT_E_CHAINING"; break; case TRUST_E_FAIL: err = "TRUST_E_FAIL"; break; case TRUST_E_EXPLICIT_DISTRUST: err = "blocked"; break; case CERT_E_REVOKED: err = "CERT_E_REVOKED"; break; case CERT_E_UNTRUSTEDTESTROOT: err = "CERT_E_UNTRUSTEDTESTROOT"; break; case CERT_E_REVOCATION_FAILURE: err = "CERT_E_REVOCATION_FAILURE"; break; case CERT_E_CN_NO_MATCH: err = fmsg; Q_strncpyz(fmsg, "Certificate is for ", sizeof(fmsg)); crypt.pCertNameToStrA(X509_ASN_ENCODING, &pServerCert->pCertInfo->Subject, 0, fmsg+strlen(fmsg), sizeof(fmsg)-strlen(fmsg)); break; case CERT_E_WRONG_USAGE: err = "CERT_E_WRONG_USAGE"; break; default: err = va("%#x", (int)Status); break; } Con_Printf(CON_ERROR "Error verifying certificate for '%ls': %s\n", pwszServerName, err); if (tls_ignorecertificateerrors.ival) { Con_Printf(CON_WARNING "pretending it didn't happen... (tls_ignorecertificateerrors is set)\n"); Status = SEC_E_OK; } } else Status = SEC_E_OK; } crypt.pCertFreeCertificateChain(pChainContext); } return Status; } static PCCERT_CONTEXT SSPI_GetServerCertificate(void) { //frankly this is all kinda fucked static PCCERT_CONTEXT ret; static const char *issuertext = "CN=127.0.0.1, O=\"FTE QuakeWorld\", OU=Fallback, C=QW"; wchar_t *const container = L"diediedie"; static const int provtype = PROV_RSA_SCHANNEL; static wchar_t *const provname = MS_DEF_RSA_SCHANNEL_PROV_W; CRYPT_KEY_PROV_INFO kpi; CERT_NAME_BLOB issuerblob; HCRYPTPROV prov = 0; HCRYPTKEY hkey = 0; //nope, not registry related. CRYPT_ALGORITHM_IDENTIFIER sigalg; SYSTEMTIME expiredate; int i; const char *pfxname = NULL; qofs_t fsz = 0; CRYPT_DATA_BLOB pfxblob = {0, NULL}; HCERTSTORE store; wchar_t password[512]; DWORD fucksake = countof(password); static qboolean tried; if (ret||tried) return ret; tried = true; i = COM_CheckParm("-pfx"); if (i && i < com_argc-1) pfxname = com_argv[i+1]; if (pfxname) pfxblob.pbData = FS_MallocFile(pfxname, FS_SYSTEM, &fsz); if (!pfxblob.pbData) pfxblob.pbData = FS_MallocFile("identity.pfx", FS_ROOT, &fsz); if (pfxblob.pbData) { pfxblob.cbData = fsz; if (!GetUserNameW(password, &fucksake)) //use their username as a password. at least it'll block most accidental redistriction. should prolly mix in their computer name, w/e *password = 0; store = crypt.pPFXImportCertStore(&pfxblob, password, 0); if (!store) //try a couple of other passwords, for people creating their own manually. store = crypt.pPFXImportCertStore(&pfxblob, L"", 0); if (!store) store = crypt.pPFXImportCertStore(&pfxblob, NULL, 0); if (store) { HCRYPTPROV hprov = 0; DWORD keyspec = 0; BOOL willbefalse = false; ret = crypt.pCertFindCertificateInStore(store, X509_ASN_ENCODING|PKCS_7_ASN_ENCODING, 0, CERT_FIND_ANY, NULL, NULL); if (ret && crypt.pCryptAcquireCertificatePrivateKey(ret, 0, NULL, &hprov, &keyspec, &willbefalse)) { char fdn[1024]; char digest[DIGEST_MAXSIZE]; char b64[DIGEST_MAXSIZE*2+1]; size_t dgsz = CalcHash(&hash_sha1, digest, sizeof(digest), ret->pbCertEncoded, ret->cbCertEncoded); Base64_EncodeBlock(digest, dgsz, b64, sizeof(b64)); Con_Printf("Loaded Certificate fingerprint is %s\n", b64); crypt.pCertNameToStrA(ret->dwCertEncodingType, &ret->pCertInfo->Subject, CERT_X500_NAME_STR, fdn, sizeof(fdn)); Con_Printf("Loaded Certificate DN: %s\n", fdn); return ret; } } Con_Printf(CON_ERROR"pfx certificate failed to load.\n"); pfxname = NULL; //don't overwrite an overridden name. it } else if (pfxname) Con_Printf(CON_WARNING"Generating new pfx file: %s\n", pfxname); memset(&sigalg, 0, sizeof(sigalg)); sigalg.pszObjId = szOID_RSA_SHA512RSA; GetSystemTime(&expiredate); expiredate.wYear += 5; //5 years hence. woo expiredate.wDay = 1; //work around feb nightmares... not to be confused with Week-Day... memset(&issuerblob, 0, sizeof(issuerblob)); crypt.pCertStrToNameA(X509_ASN_ENCODING, issuertext, CERT_X500_NAME_STR, NULL, issuerblob.pbData, &issuerblob.cbData, NULL); issuerblob.pbData = Z_Malloc(issuerblob.cbData); crypt.pCertStrToNameA(X509_ASN_ENCODING, issuertext, CERT_X500_NAME_STR, NULL, issuerblob.pbData, &issuerblob.cbData, NULL); advapi.pCryptAcquireContextW(&prov, container, provname, provtype, CRYPT_NEWKEYSET|CRYPT_MACHINE_KEYSET); if (!prov) { //try again. fucking retarded api. if (!advapi.pCryptAcquireContextW(&prov, container, provname, provtype, CRYPT_MACHINE_KEYSET)) { Con_Printf(CON_ERROR"CryptAcquireContext failed.\n"); return NULL; } } advapi.pCryptGenKey(prov, AT_KEYEXCHANGE, CRYPT_EXPORTABLE|CRYPT_ARCHIVABLE, &hkey); kpi.pwszContainerName = container; kpi.pwszProvName = provname; kpi.dwProvType = provtype; kpi.dwFlags = CERT_SET_KEY_CONTEXT_PROP_ID; kpi.cProvParam = 0; kpi.dwKeySpec = AT_KEYEXCHANGE; ret = crypt.pCertCreateSelfSignCertificate( prov, &issuerblob, 0, &kpi, &sigalg, NULL, &expiredate, NULL ); if (!ret) { //try and downgrade the signature algo if it failed. sigalg.pszObjId = szOID_RSA_SHA1RSA; ret = crypt.pCertCreateSelfSignCertificate( prov, &issuerblob, 0, &kpi, &sigalg, NULL, &expiredate, NULL ); } if (!ret) Con_Printf(CON_ERROR"Certificate generation failed...\n"); else { //this is stupid and redundant, yet apparently still needed. kpi.pwszContainerName = container; kpi.pwszProvName = provname; kpi.dwProvType = provtype; kpi.dwFlags = CRYPT_MACHINE_KEYSET; kpi.dwKeySpec = AT_KEYEXCHANGE; crypt.pCertSetCertificateContextProperty(ret, CERT_KEY_PROV_INFO_PROP_ID, 0, &kpi); { HCRYPTPROV hprov = 0; DWORD keyspec = 0; BOOL willbefalse = false; if (!crypt.pCryptAcquireCertificatePrivateKey(ret, 0, NULL, &hprov, &keyspec, &willbefalse)) { Con_Printf(CON_ERROR"Private key is defective.\n"); return NULL; } } //write it to disk { wchar_t password[512]; CRYPT_DATA_BLOB blob = {0}; HCERTSTORE store = crypt.pCertOpenStore(CERT_STORE_PROV_MEMORY, 0, 0, CERT_STORE_CREATE_NEW_FLAG, NULL); if (store) { if (crypt.pCertAddCertificateContextToStore(store, ret, CERT_STORE_ADD_ALWAYS, NULL)) { DWORD fucksake = countof(password); if (!GetUserNameW(password, &fucksake)) //use their username as a password. at least it'll block most accidental redistriction. should prolly mix in their computer name, w/e *password = 0; if (crypt.pPFXExportCertStoreEx(store, &blob, password, NULL, EXPORT_PRIVATE_KEYS|REPORT_NO_PRIVATE_KEY|REPORT_NOT_ABLE_TO_EXPORT_PRIVATE_KEY)) { blob.pbData = alloca(blob.cbData); if (crypt.pPFXExportCertStoreEx(store, &blob, password, NULL, EXPORT_PRIVATE_KEYS|REPORT_NO_PRIVATE_KEY|REPORT_NOT_ABLE_TO_EXPORT_PRIVATE_KEY)) { if (blob.cbData) { if (!FS_WriteFile(pfxname?pfxname:"identity.pfx", blob.pbData, blob.cbData, pfxname?FS_SYSTEM:FS_ROOT)) Con_Printf(CON_ERROR"FS_WriteFile(%s) failed\n", pfxname?pfxname:"identity.pfx"); } else Con_Printf(CON_ERROR"PFXExportCertStoreEx no data\n"); } else Con_Printf(CON_ERROR"PFXExportCertStoreEx failed\n"); } else Con_Printf(CON_ERROR"PFXExportCertStoreEx failed\n"); } else Con_Printf(CON_ERROR"CertAddCertificateContextToStore failed\n"); crypt.pCertCloseStore(store, 0); } else Con_Printf(CON_ERROR"CertOpenStore failed\n"); } { char fdn[1024]; char digest[DIGEST_MAXSIZE]; char b64[DIGEST_MAXSIZE*2+1]; size_t dgsz = CalcHash(&hash_sha1, digest, sizeof(digest), ret->pbCertEncoded, ret->cbCertEncoded); Base64_EncodeBlock(digest, dgsz, b64, sizeof(b64)); Con_Printf("Generated Certificate fingerprint is %s\n", b64); crypt.pCertNameToStrA(ret->dwCertEncodingType, &ret->pCertInfo->Subject, CERT_X500_NAME_STR, fdn, sizeof(fdn)); Con_Printf("Generated Certificate DN: %s\n", fdn); } } Z_Free(issuerblob.pbData); return ret; } static void SSPI_GenServerCredentials(sslfile_t *f) { SECURITY_STATUS ss; TimeStamp Lifetime; SCHANNEL_CRED SchannelCred; PCCERT_CONTEXT cred; memset(&SchannelCred, 0, sizeof(SchannelCred)); SchannelCred.dwVersion = SCHANNEL_CRED_VERSION; SchannelCred.grbitEnabledProtocols = f->datagram?USE_PROT_DGRAM_SERVER:USE_PROT_SERVER; SchannelCred.dwFlags |= SCH_CRED_NO_SYSTEM_MAPPER|SCH_CRED_DISABLE_RECONNECTS; /*don't use windows login info or anything*/ cred = SSPI_GetServerCertificate(); SchannelCred.cCreds = 1; SchannelCred.paCred = &cred; if (!cred) { SSPI_Error(f, localtext("Unable to load/generate certificate\n")); return; } ss = secur.pAcquireCredentialsHandleA (NULL, UNISP_NAME_A, SECPKG_CRED_INBOUND, NULL, &SchannelCred, NULL, NULL, &f->cred, &Lifetime); if (FAILED(ss) && f->datagram) { //try again with just dtls1, for cred, &Lifetime); } if (FAILED(ss)) { SSPI_Error(f, localtext("WinSSPI: AcquireCredentialsHandle failed %#x\n"), ss); return; } } static void SSPI_Handshake (sslfile_t *f) { SECURITY_STATUS ss; TimeStamp Lifetime; SecBufferDesc OutBuffDesc; SecBuffer OutSecBuff[8]; SecBufferDesc InBuffDesc; SecBuffer InSecBuff[8]; ULONG ContextAttributes; SCHANNEL_CRED SchannelCred; int i; qboolean retries = 5; retry: if (f->outcrypt.avail) { //don't let things build up too much SSPI_TryFlushCryptOut(f); if (f->outcrypt.avail) return; } //FIXME: skip this if we've had no new data since last time OutBuffDesc.ulVersion = SECBUFFER_VERSION; OutBuffDesc.cBuffers = countof(OutSecBuff); OutBuffDesc.pBuffers = OutSecBuff; OutSecBuff[0].BufferType = SECBUFFER_TOKEN; OutSecBuff[0].cbBuffer = f->outcrypt.datasize - f->outcrypt.avail; OutSecBuff[0].pvBuffer = f->outcrypt.data + f->outcrypt.avail; for (i = 0; i < OutBuffDesc.cBuffers; i++) { OutSecBuff[i].BufferType = SECBUFFER_EMPTY; OutSecBuff[i].pvBuffer = NULL; OutSecBuff[i].cbBuffer = 0; } if (f->handshaking == HS_ERROR) return; //gave up. else if (f->handshaking == HS_STARTCLIENT) { PCCERT_CONTEXT cert; //no input data yet. f->handshaking = HS_CLIENT; memset(&SchannelCred, 0, sizeof(SchannelCred)); SchannelCred.dwVersion = SCHANNEL_CRED_VERSION; SchannelCred.grbitEnabledProtocols = f->datagram?USE_PROT_DGRAM_CLIENT:USE_PROT_CLIENT; SchannelCred.dwFlags |= SCH_CRED_SNI_CREDENTIAL | SCH_CRED_NO_DEFAULT_CREDS; /*don't use windows login info or anything*/ //just always use the same credentials, cos we're lazy and lame, and windows users don't give a shit about privacy anyway. cert = SSPI_GetServerCertificate(); if (cert) { SchannelCred.cCreds = 1; SchannelCred.paCred = &cert; } ss = secur.pAcquireCredentialsHandleA (NULL, UNISP_NAME_A, SECPKG_CRED_OUTBOUND, NULL, &SchannelCred, NULL, NULL, &f->cred, &Lifetime); if (FAILED(ss) && f->datagram) { SchannelCred.grbitEnabledProtocols = SP_PROT_DTLS1_0_CLIENT; ss = secur.pAcquireCredentialsHandleA (NULL, UNISP_NAME_A, SECPKG_CRED_OUTBOUND, NULL, &SchannelCred, NULL, NULL, &f->cred, &Lifetime); } if (ss < 0) { SSPI_Error(f, localtext("WINSSPI: AcquireCredentialsHandle failed (%x)\n"), (int)ss); return; } ss = secur.pInitializeSecurityContextW (&f->cred, NULL, f->wpeername, MessageAttribute|(f->datagram?ISC_REQ_DATAGRAM:ISC_REQ_STREAM), 0, SECURITY_NATIVE_DREP, NULL, 0, &f->sechnd, &OutBuffDesc, &ContextAttributes, &Lifetime); } else if (f->handshaking == HS_CLIENT) { //only if we actually have data. if (!f->incrypt.avail && !f->datagram) return; InBuffDesc.ulVersion = SECBUFFER_VERSION; InBuffDesc.cBuffers = 4; InBuffDesc.pBuffers = InSecBuff; i = 0; if (f->incrypt.avail) { InSecBuff[i].BufferType = SECBUFFER_TOKEN; InSecBuff[i].cbBuffer = f->incrypt.avail; InSecBuff[i].pvBuffer = f->incrypt.data; i++; } for (; i < InBuffDesc.cBuffers; i++) { InSecBuff[i].BufferType = SECBUFFER_EMPTY; InSecBuff[i].pvBuffer = NULL; InSecBuff[i].cbBuffer = 0; } ss = secur.pInitializeSecurityContextW (&f->cred, &f->sechnd, NULL, MessageAttribute|(f->datagram?ISC_REQ_DATAGRAM:ISC_REQ_STREAM), 0, SECURITY_NETWORK_DREP, &InBuffDesc, 0, NULL, &OutBuffDesc, &ContextAttributes, &Lifetime); if (ss == SEC_E_INCOMPLETE_MESSAGE) { //TLS splits the data randomly, so this should not be considered fatal // Con_Printf("SEC_E_INCOMPLETE_MESSAGE (available %i)\n", (int)f->incrypt.avail); if (!f->datagram && f->incrypt.avail == f->incrypt.datasize) SSPI_ExpandBuffer(&f->incrypt, f->incrypt.datasize+1024); return; } else if (ss == SEC_E_INVALID_TOKEN) { // Con_Printf("SEC_E_INVALID_TOKEN\n"); if (f->datagram) return; //our udp protocol may have non-dtls packets mixed in. besides, we don't want to die from spoofed packets. } // else if (ss == SEC_I_MESSAGE_FRAGMENT) // Con_Printf("SEC_I_MESSAGE_FRAGMENT\n"); // else if (ss == SEC_I_CONTINUE_NEEDED) // Con_Printf("SEC_I_CONTINUE_NEEDED\n"); // else // Con_Printf("InitializeSecurityContextA %x\n", ss); //any extra data should still remain for the next time around. this might be more handshake data or payload data. if (InSecBuff[1].BufferType == SECBUFFER_EXTRA) { memmove(f->incrypt.data, f->incrypt.data + (f->incrypt.avail - InSecBuff[1].cbBuffer), InSecBuff[1].cbBuffer); f->incrypt.avail = InSecBuff[1].cbBuffer; } else f->incrypt.avail = 0; } else if (f->handshaking == HS_STARTSERVER || f->handshaking == HS_SERVER) { //only if we actually have data. if (!f->incrypt.avail) return; InBuffDesc.ulVersion = SECBUFFER_VERSION; InBuffDesc.cBuffers = countof(InSecBuff); InBuffDesc.pBuffers = InSecBuff; i = 0; if (f->incrypt.avail) { InSecBuff[i].BufferType = SECBUFFER_TOKEN; InSecBuff[i].cbBuffer = f->incrypt.avail; InSecBuff[i].pvBuffer = f->incrypt.data; i++; } if (f->datagram) { //for dtls's cookie InSecBuff[i].BufferType = SECBUFFER_EXTRA; InSecBuff[i].cbBuffer = 11; InSecBuff[i].pvBuffer = "Hello World"; i++; } for (; i < InBuffDesc.cBuffers; i++) { InSecBuff[i].BufferType = SECBUFFER_EMPTY; InSecBuff[i].pvBuffer = NULL; InSecBuff[i].cbBuffer = 0; } i = 1; OutSecBuff[i++].BufferType = SECBUFFER_EXTRA; OutSecBuff[i++].BufferType = SECBUFFER_ALERT; #define ServerMessageAttribute (ASC_REQ_SEQUENCE_DETECT | ASC_REQ_REPLAY_DETECT | ASC_REQ_CONFIDENTIALITY | /*ASC_REQ_EXTENDED_ERROR |*/ ASC_REQ_ALLOCATE_MEMORY) ContextAttributes = ServerMessageAttribute|(f->datagram?ASC_REQ_DATAGRAM:ASC_REQ_STREAM); // if (f->peerhash) //for ICE // ContextAttributes |= ASC_REQ_MUTUAL_AUTH; ss = secur.pAcceptSecurityContext(&f->cred, (f->handshaking==HS_SERVER)?&f->sechnd:NULL, &InBuffDesc, ContextAttributes, SECURITY_NETWORK_DREP, &f->sechnd, &OutBuffDesc, &ContextAttributes, NULL); if (f->datagram && ss == SEC_I_CONTINUE_NEEDED && f->handshaking != HS_SERVER) { //this seems wrong, but schannel complains if we don't continue to pass null above. secur.pDeleteSecurityContext(&f->sechnd); //avoid leaks memset(&f->sechnd, 0, sizeof(f->sechnd)); } else f->handshaking = HS_SERVER; if (ss == SEC_E_INVALID_TOKEN) { //sender sent us something that wasn't (d)tls. // Con_Printf("SEC_E_INVALID_TOKEN\n"); if (f->datagram) return; } else if (ss == SEC_E_INCOMPLETE_MESSAGE) { // Con_Printf("SEC_E_INCOMPLETE_MESSAGE\n"); if (!f->datagram && f->incrypt.avail == f->incrypt.datasize) SSPI_ExpandBuffer(&f->incrypt, f->incrypt.datasize+1024); return; } // else if (FAILED(ss)) // Con_Printf("AcceptSecurityContext %x\n", ss); //any extra data should still remain for the next time around. this might be more handshake data or payload data. if (InSecBuff[1].BufferType == SECBUFFER_EXTRA && !f->datagram) { memmove(f->incrypt.data, f->incrypt.data + (f->incrypt.avail - InSecBuff[1].cbBuffer), InSecBuff[1].cbBuffer); f->incrypt.avail = InSecBuff[1].cbBuffer; } else f->incrypt.avail = 0; } else return; if (ss == SEC_I_INCOMPLETE_CREDENTIALS) { //FIXME: load/generate our identity, redo the pAcquireCredentialsHandleA Con_TPrintf(CON_WARNING"server requires credentials, attempting to ignore\n"); goto retry; } if (FAILED(ss)) { const char *fname = (f->handshaking>=HS_STARTSERVER)?"AcceptSecurityContext":"InitializeSecurityContext"; switch(ss) { case SEC_E_ALGORITHM_MISMATCH: SSPI_Error(f, "%s failed: SEC_E_ALGORITHM_MISMATCH\n", fname); break; case SEC_E_INVALID_HANDLE: SSPI_Error(f, "%s failed: SEC_E_INVALID_HANDLE\n", fname); break; case SEC_E_ILLEGAL_MESSAGE: SSPI_Error(f, "%s failed: SEC_E_ILLEGAL_MESSAGE\n", fname); break; case SEC_E_INVALID_TOKEN: SSPI_Error(f, "%s failed: SEC_E_INVALID_TOKEN\n", fname); break; case SEC_E_INVALID_PARAMETER: SSPI_Error(f, "%s failed: SEC_E_INVALID_PARAMETER\n", fname); break; case SEC_E_INTERNAL_ERROR: SSPI_Error(f, "%s failed: SEC_E_INTERNAL_ERROR\n", fname); break; default: SSPI_Error(f, "%s failed: %lx\n", fname, (long)ss); break; } return; } if ((SEC_I_COMPLETE_NEEDED == ss) || (SEC_I_COMPLETE_AND_CONTINUE == ss)) { ss = secur.pCompleteAuthToken (&f->sechnd, &OutBuffDesc); if (ss < 0) { SSPI_Error(f, "CompleteAuthToken failed\n"); return; } } //its all okay and established if we get this far. if (ss == SEC_E_OK) { SecPkgContext_StreamSizes strsizes; CERT_CONTEXT *remotecert; // Con_Printf("ContextAttributes: %x (%s, %s)\n", ContextAttributes, (f->handshaking == HS_SERVER)?"sv":"cl", f->datagram?"dtls":"tls"); if (f->datagram) { //ask for a bit bigger. we use sendto for mtu guessing, schannel spitting out errors for arbitrary lower values is just annoying - it knows nothing about the actual connection. ULONG mtu = 8192; secur.pSetContextAttributesA(&f->sechnd, SECPKG_ATTR_DTLS_MTU, &mtu, sizeof(mtu)); } secur.pQueryContextAttributesA(&f->sechnd, SECPKG_ATTR_STREAM_SIZES, &strsizes); f->headersize = strsizes.cbHeader; f->footersize = strsizes.cbTrailer; f->mtu = strsizes.cbMaximumMessage - (f->headersize+f->footersize); //compute the maximum payload we can actually get with that. if (f->handshaking != HS_SERVER) { //server takes an annonymous client. client expects a proper certificate. if (*f->wpeername) { ss = secur.pQueryContextAttributesA(&f->sechnd, SECPKG_ATTR_REMOTE_CERT_CONTEXT, &remotecert); if (ss != SEC_E_OK) { SSPI_Error(f, localtext("unable to read server's certificate\n")); return; } if (VerifyServerCertificate(remotecert, f->wpeername, 0, f->datagram)) { SSPI_Error(f, localtext("Error validating certificate\n")); return; } } else Sys_Printf("SSL/TLS Server name not specified, skipping verification\n"); } f->handshaking = HS_ESTABLISHED; } //send early, send often. #ifdef HAVE_DTLS if (f->transmit) { for (i = 0; i < OutBuffDesc.cBuffers; i++) if (OutSecBuff[i].BufferType == SECBUFFER_TOKEN && OutSecBuff[i].cbBuffer) { f->resendtimer = realtime+0.2; f->transmit(f->cbctx, OutSecBuff[i].pvBuffer, OutSecBuff[i].cbBuffer); } } else #endif { i = 0; if (SSPI_CopyIntoBuffer(&f->outcrypt, OutSecBuff[i].pvBuffer, OutSecBuff[i].cbBuffer, true) < OutSecBuff[i].cbBuffer) { SSPI_Error(f, "crypt overflow\n"); return; } SSPI_TryFlushCryptOut(f); } if (f->handshaking == HS_ESTABLISHED) SSPI_Encode(f); else if (ss == SEC_I_MESSAGE_FRAGMENT) //looks like we can connect faster if we loop when we get this result. if (retries --> 0) goto retry; } static int QDECL SSPI_ReadBytes (struct vfsfile_s *file, void *buffer, int bytestoread) { sslfile_t *f = (sslfile_t *)file; int err = SSPI_CheckNewInCrypt(f); if (f->handshaking) { SSPI_Handshake(f); return err; } SSPI_Encode(f); SSPI_Decode(f); bytestoread = min(bytestoread, f->inraw.avail); if (bytestoread) { memcpy(buffer, f->inraw.data, bytestoread); f->inraw.avail -= bytestoread; memmove(f->inraw.data, f->inraw.data + bytestoread, f->inraw.avail); } else { if (err) return err; } return bytestoread; } static int QDECL SSPI_WriteBytes (struct vfsfile_s *file, const void *buffer, int bytestowrite) { sslfile_t *f = (sslfile_t *)file; //don't endlessly accept data faster than we can push it out. //we'll buffer a little, but don't go overboard if (f->outcrypt.avail > 8192) return 0; bytestowrite = SSPI_CopyIntoBuffer(&f->outraw, buffer, bytestowrite, false); if (f->handshaking) { SSPI_CheckNewInCrypt(f); //make sure its ticking over SSPI_Handshake(f); } else { SSPI_Encode(f); } return bytestowrite; } static qboolean QDECL SSPI_Seek (struct vfsfile_s *file, qofs_t pos) { SSPI_Error((sslfile_t*)file, "unable to seek on streams\n"); return false; } static qofs_t QDECL SSPI_Tell (struct vfsfile_s *file) { SSPI_Error((sslfile_t*)file, "unable to seek on streams\n"); return 0; } static qofs_t QDECL SSPI_GetLen (struct vfsfile_s *file) { return 0; } static qboolean QDECL SSPI_Close (struct vfsfile_s *file) { sslfile_t *f = (sslfile_t *)file; qboolean success = f->stream != NULL; SSPI_Error(f, ""); Z_Free(f->outraw.data); Z_Free(f->outcrypt.data); Z_Free(f->inraw.data); Z_Free(f->incrypt.data); Z_Free(f); return success; } #include static qboolean CleanUpHostname(const char *hostname, wchar_t *out, size_t outcount) { //strip any fte-specific junk, like dtls:// or [] or : const char *hostnameend; const char *host = strstr(hostname, "://"); int i = 0; if (host) hostname = host+3; hostnameend = hostname+strlen(hostname); //any dtls:// prefix will have been stripped now. if (*hostname == '[') { //eg: [::1]:foo - skip the lead [ and strip the ] and any trailing data (hopefully just a :port or nothing) hostname++; host = strchr(hostname, ']'); if (host) hostnameend = host; } else { //eg: 127.0.0.1:port - strip the port number if specified. host = strchr(hostname, ':'); if (host) hostnameend = host; } while(hostname < hostnameend) { int err; int c = utf8_decode(&err, hostname, (void*)&hostname); if (c > WCHAR_MAX) err = true; //no 16bit surrogates. they're evil. else if (i == outcount - 1) err = true; //no space to store it else out[i++] = c; if (err) { out[i] = 0; return false; } } out[i] = 0; return true; } static vfsfile_t *SSPI_OpenVFS(const char *servername, vfsfile_t *source, qboolean server) { sslfile_t *newf; if (!source || !SSL_Inited()) return NULL; /* if (server) //unsupported return NULL; */ newf = Z_Malloc(sizeof(*newf)); if (!CleanUpHostname(server?"":servername, newf->wpeername, countof(newf->wpeername))) { Z_Free(newf); return NULL; } newf->handshaking = server?HS_STARTSERVER:HS_STARTCLIENT; newf->stream = source; newf->funcs.Close = SSPI_Close; newf->funcs.Flush = NULL; newf->funcs.GetLen = SSPI_GetLen; newf->funcs.ReadBytes = SSPI_ReadBytes; newf->funcs.Seek = SSPI_Seek; newf->funcs.Tell = SSPI_Tell; newf->funcs.WriteBytes = SSPI_WriteBytes; newf->funcs.seekstyle = SS_UNSEEKABLE; SSPI_ExpandBuffer(&newf->outraw, 8192); SSPI_ExpandBuffer(&newf->outcrypt, 8192); SSPI_ExpandBuffer(&newf->inraw, 8192); SSPI_ExpandBuffer(&newf->incrypt, 8192); if (server) SSPI_GenServerCredentials(newf); return &newf->funcs; } #ifndef SECPKG_ATTR_UNIQUE_BINDINGS #define SECPKG_ATTR_UNIQUE_BINDINGS 25 typedef struct _SecPkgContext_Bindings { unsigned long BindingsLength; SEC_CHANNEL_BINDINGS *Bindings; } SecPkgContext_Bindings, *PSecPkgContext_Bindings; #endif static int SSPI_GetChannelBinding(vfsfile_t *vf, qbyte *binddata, size_t *bindsize) { int ret; sslfile_t *f = (sslfile_t*)vf; SecPkgContext_Bindings bindings; if (vf->Close != SSPI_Close) return -2; //not one of ours. bindings.BindingsLength = 0; bindings.Bindings = NULL; ret = 0; switch(secur.pQueryContextAttributesA(&f->sechnd, SECPKG_ATTR_UNIQUE_BINDINGS, &bindings)) { case SEC_E_OK: if (bindings.Bindings->cbApplicationDataLength <= *bindsize && !Q_strncasecmp(((char*)bindings.Bindings)+bindings.Bindings->dwApplicationDataOffset, "tls-unique:", 11)) { //will contain 'tls-unique:BINARYDATA' *bindsize = bindings.Bindings->cbApplicationDataLength-11; memcpy(binddata, ((unsigned char*) bindings.Bindings) + bindings.Bindings->dwApplicationDataOffset+11, bindings.Bindings->cbApplicationDataLength-11); ret = 1; } //FIXME: leak //secur.pFreeContextBuffer(bindings.Bindings); break; case SEC_E_UNSUPPORTED_FUNCTION: ret = -1; //schannel doesn't support it. too old an OS, I guess. break; default: break; } return ret; } #include "netinc.h" #if defined(HAVE_DTLS) static void *SSPI_DTLS_CreateContext(const dtlscred_t *credinfo, void *cbctx, neterr_t(*push)(void *cbctx, const qbyte *data, size_t datasize), qboolean isserver) { sslfile_t *ctx; if (!SSL_Inited()) return NULL; ctx = Z_Malloc(sizeof(*ctx)); ctx->datagram = true; ctx->handshaking = isserver?HS_STARTSERVER:HS_STARTCLIENT; ctx->cbctx = cbctx; ctx->transmit = push; if (!CleanUpHostname((credinfo&&credinfo->peer.name)?credinfo->peer.name:"", ctx->wpeername, countof(ctx->wpeername))) { Z_Free(ctx); return NULL; } SSPI_ExpandBuffer(&ctx->outraw, 8192); SSPI_ExpandBuffer(&ctx->outcrypt, 65536); SSPI_ExpandBuffer(&ctx->inraw, 8192); SSPI_ExpandBuffer(&ctx->incrypt, 65536); if (isserver) SSPI_GenServerCredentials(ctx); else SSPI_Handshake(ctx); //begin the initial handshake now return ctx; } static void SSPI_DTLS_DestroyContext(void *vctx) { SSPI_Close(vctx); } static neterr_t SSPI_DTLS_Transmit(void *ctx, const qbyte *data, size_t datasize) { sslfile_t *f = (sslfile_t *)ctx; if (!datasize) { //we use these as a way to probe whether its sendable or not yet. if (f->handshaking) { if (f->resendtimer < realtime) SSPI_Handshake(f); //keep trying... return NETERR_CLOGGED; } return NETERR_SENT; } if (f->handshaking) SSPI_Handshake(f); //keep trying... if (f->handshaking == HS_ERROR) return NETERR_DISCONNECTED; if (f->handshaking) return NETERR_CLOGGED; //not ready yet if (datasize >= f->mtu) return NETERR_MTU; //we're not allowed. //sspi likes writing over the source data. make sure nothing is hurt by copying it out first. f->outraw.avail = 0; SSPI_CopyIntoBuffer(&f->outraw, data, datasize, true); return SSPI_Encode(f); } static neterr_t SSPI_DTLS_Received(void *ctx, sizebuf_t *msg) { int ret; sslfile_t *f = (sslfile_t *)ctx; f->incrypt.data = msg->data; f->incrypt.avail = f->incrypt.datasize = msg->cursize; if (f->handshaking) { SSPI_Handshake(f); ret = NETERR_CLOGGED; //not ready yet if (f->handshaking == HS_ERROR) ret = NETERR_DISCONNECTED; } else { SSPI_Decode(f); ret = NETERR_SENT; if (f->inraw.avail < msg->maxsize) msg->cursize = f->inraw.avail; else msg->cursize = msg->maxsize; memcpy(msg->data, f->inraw.data, msg->cursize); f->inraw.avail = 0; } f->incrypt.data = NULL; return ret; } static neterr_t SSPI_DTLS_Timeouts(void *ctx) { sslfile_t *f = (sslfile_t *)ctx; if (f->handshaking) { SSPI_DTLS_Transmit(ctx, NULL, 0); return NETERR_CLOGGED; } return NETERR_SENT; } static qboolean SSPI_DTLS_CheckConnection(void *cbctx, void *peeraddr, size_t peeraddrsize, void *indata, size_t insize, neterr_t(*push)(void *cbctx, const qbyte *data, size_t datasize), void (*EstablishTrueContext)(void **cbctx, void *state)) { //we got a packet that might be a 'dtls hello' packet. try figuring out what's going on. SECURITY_STATUS ss; SecBufferDesc OutBuffDesc; SecBuffer OutSecBuff[8]; SecBufferDesc InBuffDesc; SecBuffer InSecBuff[8]; ULONG ContextAttributes; int i; char replymessage[4094]; static sslfile_t *pending; if (!pending) { pending = SSPI_DTLS_CreateContext(NULL, NULL, NULL, true); if (!pending) return false; } InBuffDesc.ulVersion = SECBUFFER_VERSION; InBuffDesc.cBuffers = countof(InSecBuff); InBuffDesc.pBuffers = InSecBuff; i = 0; InSecBuff[i].BufferType = SECBUFFER_TOKEN; InSecBuff[i].cbBuffer = insize; InSecBuff[i].pvBuffer = indata; i++; //for dtls's cookie InSecBuff[i].BufferType = SECBUFFER_EXTRA; InSecBuff[i].cbBuffer = peeraddrsize; InSecBuff[i].pvBuffer = peeraddr; i++; for (; i < InBuffDesc.cBuffers; i++) { InSecBuff[i].BufferType = SECBUFFER_EMPTY; InSecBuff[i].pvBuffer = NULL; InSecBuff[i].cbBuffer = 0; } OutBuffDesc.ulVersion = SECBUFFER_VERSION; OutBuffDesc.cBuffers = countof(OutSecBuff); OutBuffDesc.pBuffers = OutSecBuff; OutSecBuff[0].BufferType = SECBUFFER_TOKEN; OutSecBuff[0].cbBuffer = sizeof(replymessage); OutSecBuff[0].pvBuffer = replymessage; for (i = 1; i < OutBuffDesc.cBuffers; i++) { OutSecBuff[i].BufferType = SECBUFFER_EMPTY; OutSecBuff[i].pvBuffer = NULL; OutSecBuff[i].cbBuffer = 0; } i = 1; OutSecBuff[i++].BufferType = SECBUFFER_EXTRA; OutSecBuff[i++].BufferType = SECBUFFER_ALERT; ContextAttributes = ServerMessageAttribute|ASC_REQ_DATAGRAM; ss = secur.pAcceptSecurityContext(&pending->cred, NULL, &InBuffDesc, ContextAttributes, SECURITY_NETWORK_DREP, &pending->sechnd, &OutBuffDesc, &ContextAttributes, NULL); //expect SEC_I_CONTINUE_NEEDED for a outgoing challenge (cookie request) //expect SEC_I_MESSAGE_FRAGMENT for anything more. for (i = 0; i < OutBuffDesc.cBuffers; i++) if (OutSecBuff[i].BufferType == SECBUFFER_TOKEN && OutSecBuff[i].cbBuffer) push(cbctx, OutSecBuff[i].pvBuffer, OutSecBuff[i].cbBuffer); if (ss == SEC_I_MESSAGE_FRAGMENT) { //looks like we got a reply to the dtls handshake. lock down its ip. pending->cbctx = cbctx; pending->transmit = push; pending->handshaking = HS_SERVER; EstablishTrueContext(&pending->cbctx, pending); pending = NULL; return true; } //try to avoid memory leaks. this stuff isn't documented nearly well enough. secur.pDeleteSecurityContext(&pending->sechnd); memset(&pending->sechnd, 0, sizeof(pending->sechnd)); return false; } qboolean SSPI_DTLS_GenTempCertificate(const char *subject, struct dtlslocalcred_s *cred) { //exporting+importing certs here is just too damn painful. use a single cert and a dummy value for the key. for webrtc this SHOULD be okay, even if its expired... PCCERT_CONTEXT cert = SSPI_GetServerCertificate(); if (!cert) return false; cred->cert = memcpy(BZ_Malloc(cert->cbCertEncoded), cert->pbCertEncoded, cert->cbCertEncoded); cred->certsize = cert->cbCertEncoded; cred->key = NULL; cred->keysize = 0; return true; } static int SSPI_DTLS_GetPeerCertificate(void *ctx, enum certprops_e prop, char *out, size_t outsize) { sslfile_t *f = (sslfile_t *)ctx; CERT_CONTEXT *cert = NULL; safeswitch(prop) { case QCERT_ISENCRYPTED: if (f->handshaking == HS_ESTABLISHED) return 0; //handshake is done, its all encrypted. return -1; //still pending, doesn't count as encrypted yet. case QCERT_PEERCERTIFICATE: secur.pQueryContextAttributesA(&f->sechnd, SECPKG_ATTR_REMOTE_CERT_CONTEXT, &cert); if (cert && cert->cbCertEncoded <= outsize) { memcpy(out, cert->pbCertEncoded, cert->cbCertEncoded); return cert->cbCertEncoded; } return -1; case QCERT_PEERSUBJECT: secur.pQueryContextAttributesA(&f->sechnd, SECPKG_ATTR_REMOTE_CERT_CONTEXT, &cert); if (cert && cert->cbCertEncoded <= outsize) return crypt.pCertNameToStrA(cert->dwCertEncodingType, &cert->pCertInfo->Subject, CERT_X500_NAME_STR, out, outsize); return -1; case QCERT_LOCALCERTIFICATE: case QCERT_LOBBYSTATUS: case QCERT_LOBBYSENDCHAT: safedefault: return -1; } } static const dtlsfuncs_t dtlsfuncs_schannel = { SSPI_DTLS_CreateContext, SSPI_DTLS_CheckConnection, SSPI_DTLS_DestroyContext, SSPI_DTLS_Transmit, SSPI_DTLS_Received, SSPI_DTLS_Timeouts, SSPI_DTLS_GetPeerCertificate, SSPI_DTLS_GenTempCertificate }; static const dtlsfuncs_t *SSPI_DTLS_InitServer(void) { //make sure we can load a cert... if (!SSL_Inited() || !SSPI_GetServerCertificate()) return NULL; //otherwise refuse to run as a server instead of causing cert issues with every restart for users. return &dtlsfuncs_schannel; } static const dtlsfuncs_t *SSPI_DTLS_InitClient(void) { return &dtlsfuncs_schannel; } #endif #if defined(_MSC_VER) && (_MSC_VER < 1900) #define SSPI_VerifyHash NULL //old versions of msvc are crippled... #else #define STATUS_SUCCESS ((NTSTATUS)0x00000000) #define STATUS_INVALID_SIGNATURE ((NTSTATUS)0xC000A000) #include static enum hashvalidation_e SSPI_VerifyHash(const qbyte *hashdata, size_t hashsize, const qbyte *pemcert, size_t pemcertsize, const qbyte *signdata, size_t signsize) { NTSTATUS status; BCRYPT_KEY_HANDLE pubkey; const char *pem = pemcert; const char *pemend; qbyte *der; size_t dersize; static const void *(WINAPI *pCertCreateContext) (DWORD dwContextType, DWORD dwEncodingType, const BYTE *pbEncoded, DWORD cbEncoded, DWORD dwFlags, PCERT_CREATE_CONTEXT_PARA pCreatePara); static BOOL (WINAPI *pCryptImportPublicKeyInfoEx2) (DWORD dwCertEncodingType, PCERT_PUBLIC_KEY_INFO pInfo, DWORD dwFlags, void *pvAuxInfo, BCRYPT_KEY_HANDLE *phKey); static BOOL (WINAPI *pCertFreeCertificateContext) (PCCERT_CONTEXT pCertContext); static dllhandle_t *crypt32; static dllfunction_t crypt32funcs[] = { {(void**)&pCertCreateContext, "CertCreateContext"}, {(void**)&pCryptImportPublicKeyInfoEx2, "CryptImportPublicKeyInfoEx2"}, //WARNING: fails on wine. {(void**)&pCertFreeCertificateContext, "CertFreeCertificateContext"}, {NULL,NULL} }; static NTSTATUS (WINAPI *pBCryptVerifySignature) (BCRYPT_KEY_HANDLE hKey, VOID *pPaddingInfo, PUCHAR pbHash, ULONG cbHash, PUCHAR pbSignature, ULONG cbSignature, ULONG dwFlags); static NTSTATUS (WINAPI *pBCryptDestroyKey) (BCRYPT_KEY_HANDLE hKey); static dllhandle_t *bcrypt; static dllfunction_t bcryptfuncs[] = { {(void**)&pBCryptVerifySignature, "BCryptVerifySignature"}, {(void**)&pBCryptDestroyKey, "BCryptDestroyKey"}, {NULL,NULL} }; if (!crypt32) crypt32 = Sys_LoadLibrary("crypt32.dll", crypt32funcs); if (!bcrypt) bcrypt = Sys_LoadLibrary("bcrypt.dll", bcryptfuncs); if (!crypt32 || !bcrypt) { Con_Printf("Unable to obtain required crypto functions\n"); return VH_UNSUPPORTED; } if (!pem) return VH_AUTHORITY_UNKNOWN; //no public cert/key for authority. pem = strstr(pem, "-----BEGIN CERTIFICATE-----"); if (!pem) return VH_UNSUPPORTED; //not a pem pem += strlen("-----BEGIN CERTIFICATE-----"); pemend = strstr(pem, "-----END CERTIFICATE-----"); if (!pemend) return VH_UNSUPPORTED; dersize = Base64_DecodeBlock(pem, pemend, NULL, 0); //guess der = alloca(dersize); dersize = Base64_DecodeBlock(pem, pemend, der, dersize); //okay, now its in binary der format. //make sense of the cert and pull out its public key... { const CERT_CONTEXT* cert = pCertCreateContext(CERT_STORE_CERTIFICATE_CONTEXT, X509_ASN_ENCODING, der, dersize, 0, NULL); if (!pCryptImportPublicKeyInfoEx2(X509_ASN_ENCODING, &cert->pCertInfo->SubjectPublicKeyInfo, 0, NULL, &pubkey)) return VH_UNSUPPORTED; pCertFreeCertificateContext(cert); } //yay, now we can do what we actually wanted in the first place. status = pBCryptVerifySignature(pubkey, NULL, (qbyte*)hashdata, hashsize, (qbyte*)signdata, signsize, 0); pBCryptDestroyKey(pubkey); if (status == STATUS_SUCCESS) return VH_CORRECT; //its okay else if (status == STATUS_INVALID_SIGNATURE) return VH_INCORRECT; //its bad return VH_UNSUPPORTED; //some weird transient error...? } #endif ftecrypto_t crypto_sspi = { "SChannel", SSPI_OpenVFS, SSPI_GetChannelBinding, SSPI_DTLS_InitClient, SSPI_DTLS_InitServer, SSPI_VerifyHash, NULL,//SSPI_GenerateHash, }; #endif