diff --git a/engine/client/cl_main.c b/engine/client/cl_main.c index 93d318829..7821142db 100644 --- a/engine/client/cl_main.c +++ b/engine/client/cl_main.c @@ -289,15 +289,6 @@ static struct int numadr; int nextadr; netadr_t adr[8]; //addresses that we're trying to transfer to, one entry per dns result, eg both ::1 AND 127.0.0.1 -#ifdef HAVE_DTLS - enum - { //not relevant when given a direct dtls address. - DTLS_DISABLE, - DTLS_TRY, - DTLS_REQUIRE, - DTLS_ACTIVE, - } dtlsupgrade; -#endif int protocol; //nq/qw/q2/q3. guessed based upon server replies int subprotocol; //the monkeys are trying to eat me. struct @@ -320,13 +311,20 @@ static struct enum coninfomode_e { CIM_DEFAULT, //sends both a qw getchallenge and nq connect (also with postfixed getchallenge so modified servers can force getchallenge) - CIM_NQONLY, //disables getchallenge (so fte servers treat us as an nq server). should not be used for dpp7 servers. + CIM_NQONLY, //disables getchallenge (so fte servers treat us as an nq client). should not be used for dpp7 servers. CIM_QEONLY, //forces dtls and uses a different nq netchan version } mode; + enum coninfospec_e + { + CIS_DEFAULT, //default + CIS_JOIN, //force join + CIS_OBSERVE, //force observe + } spec; int defaultport; int tries; //increased each try, every fourth trys nq connect packets. unsigned char guid[64]; //client->server guid (so doesn't change with transfers) -// qbyte fingerprint[5*4]; //sha1 hash of accepted dtls certs + + struct dtlspeercred_s peercred; } connectinfo; qboolean nomaster; @@ -595,6 +593,29 @@ char *CL_GUIDString(netadr_t *adr) return connectinfo.guid; } +static void CL_ConnectAbort(const char *format, ...) +{ //stops trying to connect, doesn't affect the _current_ connection, so usable for transfers. + va_list argptr; + char reason[1024]; + + if (format) + { + va_start (argptr, format); + Q_vsnprintfz (reason, sizeof(reason), format,argptr); + va_end (argptr); + + Cvar_Set(&cl_disconnectreason, reason); + Con_Printf (CON_ERROR"%s\n", reason); + } +#ifdef HAVE_DTLS + while (connectinfo.numadr) + NET_DTLS_Disconnect(cls.sockets, &connectinfo.adr[--connectinfo.numadr]); +#endif + connectinfo.numadr = 0; + SCR_EndLoadingPlaque(); + connectinfo.trying = false; +} + /* ======================= CL_SendConnectPacket @@ -658,13 +679,27 @@ static void CL_SendConnectPacket (netadr_t *to) t1 = Sys_DoubleTime (); + if (connectinfo.peercred.hash && net_enable_dtls.ival>0) + { + char cert[8192]; + char digest[DIGEST_MAXSIZE]; + int sz = NET_GetConnectionCertificate(cls.sockets, to, QCERT_PEERCERTIFICATE, cert, sizeof(cert)); + if (sz <= 0 || memcmp(connectinfo.peercred.digest, digest, CalcHash(connectinfo.peercred.hash, digest, sizeof(digest), cert, sz))) + { //FIXME: we may have already pinned the bad cert, which may cause issues when reconnecting without FP info later. + if (NET_GetConnectionCertificate(cls.sockets, to, QCERT_ISENCRYPTED, NULL, 0)<0) + CL_ConnectAbort ("Fingerprint specified, but server did not report any certificate\n"); + else + CL_ConnectAbort ("Server certificate does not match specified fingerprint\n"); + return; + } + } + if (!to) { to = &addr; if (!NET_StringToAdr (cls.servername, PORT_DEFAULTSERVER, to)) { - Con_TPrintf ("CL_SendConnectPacket: Bad server address \"%s\"\n", cls.servername); - connectinfo.trying = false; + CL_ConnectAbort ("CL_SendConnectPacket: Bad server address \"%s\"\n", cls.servername); return; } } @@ -675,8 +710,7 @@ static void CL_SendConnectPacket (netadr_t *to) if (!NET_IsClientLegal(to)) { - Con_TPrintf ("Illegal server address\n"); - connectinfo.trying = false; + CL_ConnectAbort("Illegal server address\n"); return; } @@ -715,10 +749,12 @@ static void CL_SendConnectPacket (netadr_t *to) Q_strncatz(data, va("\\prx\\%s", cls.servername), sizeof(data)); *a = '@'; } + if (connectinfo.spec==CIS_OBSERVE) + Q_strncatz(data, "\\spectator\\1", sizeof(data)); //the info itself { static const char *prioritykeys[] = {"name", "password", "spectator", "lang", "rate", "team", "topcolor", "bottomcolor", "skin", "_", "*", NULL}; - static const char *ignorekeys[] = {"prx", "*z_ext", NULL}; + const char *ignorekeys[] = {"prx", "*z_ext", (connectinfo.spec!=CIS_DEFAULT)?"spectator":NULL, NULL}; InfoBuf_ToString(&cls.userinfo[0], data+strlen(data), sizeof(data)-strlen(data), prioritykeys, ignorekeys, NULL, &cls.userinfosync, &cls.userinfo[0]); } if (connectinfo.protocol == CP_QUAKEWORLD) //zquake extension info. @@ -808,32 +844,18 @@ static void CL_ResolvedServer(void *vctx, void *data, size_t a, size_t b) if (!ctx->found) { - Cvar_Set(&cl_disconnectreason, va("Bad server address \"%s\"", ctx->servername)); - Con_TPrintf ("Bad server address \"%s\"\n", ctx->servername); - connectinfo.trying = false; - SCR_EndLoadingPlaque(); + CL_ConnectAbort("Bad server address \"%s\"\n", ctx->servername); return; } #ifdef HAVE_DTLS for (i = 0; i < ctx->found; i++) { - if (connectinfo.dtlsupgrade == DTLS_ACTIVE || connectinfo.mode==CIM_QEONLY) + if (net_enable_dtls.ival>=4 || connectinfo.mode==CIM_QEONLY)// || (connectinfo.peercred.hash && net_enable_dtls.ival >= 1)) { //if we've already established a dtls connection, stick with it if (ctx->adr[i].prot == NP_DGRAM) ctx->adr[i].prot = NP_DTLS; } - else if (connectinfo.adr[i].prot == NP_DTLS) - { //dtls connections start out with regular udp, and upgrade to dtls once its established that the server supports it. - //FIXME: remove this block once our new netcode is better established. - connectinfo.dtlsupgrade = DTLS_REQUIRE; - ctx->adr[i].prot = NP_DGRAM; - } - else - { - //hostname didn't specify dtls. upgrade if we're allowed, but don't mandate it. - //connectinfo.dtlsupgrade = DTLS_TRY; - } } #endif @@ -846,11 +868,11 @@ static void CL_ResolvedServer(void *vctx, void *data, size_t a, size_t b) static void CL_ResolveServer(void *vctx, void *data, size_t a, size_t b) { struct resolvectx_s *ctx = vctx; - const char *host = strrchr(cls.servername+1, '@'); + const char *host = strrchr(ctx->servername+1, '@'); if (host) host++; else - host = cls.servername; + host = ctx->servername; ctx->found = NET_StringToAdr2 (host, connectinfo.defaultport, ctx->adr, countof(ctx->adr), NULL); @@ -1109,9 +1131,7 @@ void CL_CheckForResend (void) connectinfo.nextadr = 0; if (!connectinfo.numadr) { - Con_TPrintf ("CL_CheckForResend: Bad server address \"%s\"\n", cls.servername); - connectinfo.trying = false; - SCR_EndLoadingPlaque(); + CL_ConnectAbort("CL_CheckForResend: Bad server address \"%s\"\n", cls.servername); return; } NET_AdrToString(data, sizeof(data), &connectinfo.adr[connectinfo.nextadr]); @@ -1200,6 +1220,9 @@ void CL_CheckForResend (void) else connectinfo.clogged = false; //do the prints and everything. + if (!cls.sockets) //only if its needed... we don't want to keep using a new port unless we have to + NET_InitClient(false); + #ifdef HAVE_DTLS if (connectinfo.numadr>0 && connectinfo.adr[0].prot == NP_DTLS) { //get through the handshake first, instead of waiting for a 5-sec timeout between polls. @@ -1208,17 +1231,16 @@ void CL_CheckForResend (void) case NETERR_CLOGGED: //temporary failure connectinfo.clogged = true; return; + case NETERR_DISCONNECTED: + CL_ConnectAbort("DTLS Certificate Verification Failure\n"); + break; + case NETERR_NOROUTE: //not an error here, just means we need to send a new handshake. + break; default: break; } } - - if (connectinfo.dtlsupgrade != DTLS_ACTIVE) #endif - { - if (!cls.sockets) //only if its needed... we don't want to keep using a new port unless we have to - NET_InitClient(false); - } t1 = Sys_DoubleTime (); if (!connectinfo.istransfer) @@ -1246,10 +1268,7 @@ void CL_CheckForResend (void) to = &connectinfo.adr[connectinfo.nextadr%connectinfo.numadr]; if (!NET_IsClientLegal(to)) { - Cvar_Set(&cl_disconnectreason, va("Illegal server address")); - Con_TPrintf ("Illegal server address\n"); - SCR_EndLoadingPlaque(); - connectinfo.trying = false; + CL_ConnectAbort ("Illegal server address\n"); return; } @@ -1274,12 +1293,9 @@ void CL_CheckForResend (void) connectinfo.clogged = false; if (connectinfo.tries == 0 && connectinfo.nextadr < connectinfo.numadr) - if (!NET_EnsureRoute(cls.sockets, "conn", cls.servername, to)) + if (!NET_EnsureRoute(cls.sockets, "conn", &connectinfo.peercred, to)) { - Cvar_Set(&cl_disconnectreason, va("Unable to establish connection to %s\n", cls.servername)); - Con_Printf ("Unable to establish connection to %s\n", cls.servername); - connectinfo.trying = false; - SCR_EndLoadingPlaque(); + CL_ConnectAbort ("Unable to establish connection to %s\n", cls.servername); return; } @@ -1377,32 +1393,92 @@ void CL_CheckForResend (void) } else { - Cvar_Set(&cl_disconnectreason, va("No route to \"%s\", giving up\n", cls.servername)); - Con_TPrintf ("No route to host, giving up\n"); - connectinfo.trying = false; - SCR_EndLoadingPlaque(); + CL_ConnectAbort ("No route to host, giving up\n"); NET_CloseClient(); } } } -static void CL_BeginServerConnect(const char *host, int port, qboolean noproxy, enum coninfomode_e mode) +static void CL_BeginServerConnect(char *host, int port, qboolean noproxy, enum coninfomode_e mode, enum coninfospec_e spec) { - if (!strncmp(host, "localhost", 9)) - noproxy = true; //FIXME: resolve the address here or something so that we don't end up using a proxy for lan addresses. + const char *schemeend = strstr(host, "://"); + char *arglist; - if (strstr(host, "://") || !*cl_proxyaddr.string || noproxy) - Q_strncpyz (cls.servername, host, sizeof(cls.servername)); + Q_strncpyz(cls.serverurl, host, sizeof(cls.serverurl)); + + if (schemeend) + { + const char *schemestart = strchr(host, ':'); + int schemelen; + //if its one of our explicit protocols then use the url as-is + const char *netschemes[] = {"udp", "udp4", "udp6", "ipx", "tcp", "tcp4", "tcp6", /*ipx*/"spx", "ws", "wss", "tls", "dtls", "ice", "rtc", "ices", "rtcs", "irc", "udg", "unix"}; + int i; + size_t slen; + + if (!schemestart || schemestart==schemeend) + schemestart = host; + else + schemestart++; + schemelen = schemeend-schemestart; + + Q_strncpyz (cls.servername, "", sizeof(cls.servername)); + for (i = 0; i < countof(netschemes); i++) + { + slen = strlen(netschemes[i]); + if (schemelen == slen && !strncmp(schemestart, netschemes[i], slen)) + { + Q_strncpyz (cls.servername, host, sizeof(cls.servername)); //oh. will probably be okay then + break; + } + } + if (!*cls.servername) + { //not some '/foo' name, not rtc:// either... + char *sl = strchr(schemeend+3, '/'); + if (sl) + { + if (!strncmp(sl, "/observe", 8)) + { + if (spec == CIS_DEFAULT) + spec = CIS_OBSERVE; + else if (spec != CIS_OBSERVE) + Con_Printf("Ignoring 'observe'\n"); + memmove(sl, sl+8, strlen(sl+8)+1); + } + else if (!strncmp(sl, "/join", 5)) + { + if (spec == CIS_DEFAULT) + spec = CIS_JOIN; + else if (spec != CIS_OBSERVE) + Con_Printf("Ignoring 'join'\n"); + memmove(sl, sl+5, strlen(sl+5)+1); + } + else if (!strncmp(sl, "/", 1) && (sl[1] == 0 || sl[1]=='?')) + { + //current spectator mode + memmove(sl, sl+1, strlen(sl+1)+1); + } + } + Q_strncpyz (cls.servername, schemeend+3, sizeof(cls.servername)); //probably some game-specific mess that we don't know + } + } else - Q_snprintfz(cls.servername, sizeof(cls.servername), "%s@%s", host, cl_proxyaddr.string); + { + if (!strncmp(host, "localhost", 9)) + noproxy = true; //FIXME: resolve the address here or something so that we don't end up using a proxy for lan addresses. + + if (strstr(host, "://") || !*cl_proxyaddr.string || noproxy) + Q_strncpyz (cls.servername, host, sizeof(cls.servername)); + else + Q_snprintfz(cls.servername, sizeof(cls.servername), "%s@%s", host, cl_proxyaddr.string); + } + + arglist = strchr(cls.servername, '?'); if (!port) port = cl_defaultport.value; -#ifdef HAVE_DTLS - while (connectinfo.numadr) - NET_DTLS_Disconnect(cls.sockets, &connectinfo.adr[--connectinfo.numadr]); -#endif + + CL_ConnectAbort(NULL); memset(&connectinfo, 0, sizeof(connectinfo)); if (*cl_disconnectreason.string) Cvar_Set(&cl_disconnectreason, ""); @@ -1410,15 +1486,31 @@ static void CL_BeginServerConnect(const char *host, int port, qboolean noproxy, connectinfo.defaultport = port; connectinfo.protocol = CP_UNKNOWN; connectinfo.mode = mode; + connectinfo.spec = spec; -#ifdef HAVE_DTLS - if (net_enable_dtls.ival >= 3) - connectinfo.dtlsupgrade = DTLS_REQUIRE; - else if (net_enable_dtls.ival >= 2) - connectinfo.dtlsupgrade = DTLS_TRY; - else - connectinfo.dtlsupgrade = DTLS_DISABLE; -#endif + connectinfo.peercred.name = cls.servername; + if (arglist) + { + *arglist++ = 0; + while (*arglist) + { + char *e = strchr(arglist, '&'); + if (e) + *e=0; + if (!strncasecmp(arglist, "fp=", 3)) + { + Base64_DecodeBlock(arglist+3, arglist+strlen(arglist), connectinfo.peercred.digest, sizeof(connectinfo.peercred.digest)); + connectinfo.peercred.hash = &hash_sha1; + } + else + Con_Printf(CON_WARNING"uri arg not known: \"%s\"\n", arglist); + + if (e) + arglist=e+1; + else + break; + } + } SCR_SetLoadingStage(LS_CONNECTION); CL_CheckForResend(); @@ -1434,9 +1526,11 @@ void CL_BeginServerReconnect(void) } #endif #ifdef HAVE_DTLS - if (connectinfo.numadr>0) - NET_DTLS_Disconnect(cls.sockets, &connectinfo.adr[0]); - connectinfo.dtlsupgrade = 0; + { + int i; + for (i = 0; i < connectinfo.numadr; i++) + NET_DTLS_Disconnect(cls.sockets, &connectinfo.adr[i]); + } #endif #ifdef SUPPORT_ICE while (connectinfo.numadr) //remove any ICE addresses. probably we'll end up with no addresses left leaving us free to re-resolve giving us the original(ish) rtc connection. @@ -1474,11 +1568,11 @@ void CL_Transfer_f(void) return; } + CL_ConnectAbort(NULL); server = Cmd_Argv (1); if (!*server) { //if they didn't specify a server, abort any active transfer/connection. - connectinfo.trying = false; return; } @@ -1529,7 +1623,7 @@ void CL_Connect_f (void) #endif CL_Disconnect_f (); - CL_BeginServerConnect(server, 0, false, CIM_DEFAULT); + CL_BeginServerConnect(server, 0, false, CIM_DEFAULT, CIS_DEFAULT); } #if defined(CL_MASTER) && defined(HAVE_PACKET) static void CL_ConnectBestRoute_f (void) @@ -1564,7 +1658,7 @@ static void CL_ConnectBestRoute_f (void) else #endif CL_Disconnect_f (); - CL_BeginServerConnect(server, 0, true, CIM_DEFAULT); + CL_BeginServerConnect(server, 0, true, CIM_DEFAULT, CIS_DEFAULT); } #endif @@ -1591,9 +1685,7 @@ static void CL_Join_f (void) CL_Disconnect_f (); - Cvar_Set(&spectator, "0"); - - CL_BeginServerConnect(server, 0, false, CIM_DEFAULT); + CL_BeginServerConnect(server, 0, false, CIM_DEFAULT, CIS_JOIN); } void CL_Observe_f (void) @@ -1621,7 +1713,7 @@ void CL_Observe_f (void) Cvar_Set(&spectator, "1"); - CL_BeginServerConnect(server, 0, false, CIM_DEFAULT); + CL_BeginServerConnect(server, 0, false, CIM_DEFAULT, CIS_OBSERVE); } #ifdef NQPROT @@ -1646,7 +1738,7 @@ void CLNQ_Connect_f (void) CL_Disconnect_f (); - CL_BeginServerConnect(server, 26000, true, mode); + CL_BeginServerConnect(server, 26000, true, mode, CIS_DEFAULT/*doesn't really do spec/join stuff, but if the server asks for our info later...*/); } #endif @@ -2242,9 +2334,7 @@ void CL_Disconnect_f (void) #endif CL_Disconnect (NULL); - - connectinfo.trying = false; - + CL_ConnectAbort(NULL); NET_CloseClient(); (void)CSQC_UnconnectedInit(); @@ -2930,6 +3020,7 @@ void CL_Packet_f (void) int i, l; char *in, *out; netadr_t adr; + struct dtlspeercred_s cred = {Cmd_Argv(1)}; if (Cmd_Argc() != 3) { @@ -3019,7 +3110,7 @@ void CL_Packet_f (void) if (!cls.sockets) NET_InitClient(false); - if (!NET_EnsureRoute(cls.sockets, "packet", Cmd_Argv(1), &adr)) + if (!NET_EnsureRoute(cls.sockets, "packet", &cred, &adr)) return; NET_SendPacket (cls.sockets, out-send, send, &adr); @@ -3340,7 +3431,8 @@ void CL_ConnectionlessPacket (void) { if (CL_IsPendingServerAddress(&net_from)) { - if (!NET_EnsureRoute(cls.sockets, "redir", cls.servername, &adr)) + struct dtlspeercred_s cred = {cls.servername}; //FIXME + if (!NET_EnsureRoute(cls.sockets, "redir", &cred, &adr)) Con_Printf (CON_ERROR"Unable to redirect to %s\n", data); else { @@ -3358,29 +3450,20 @@ void CL_ConnectionlessPacket (void) else if (!strcmp(s, "reject")) { //generic rejection. stop trying. char *data = MSG_ReadStringLine(); - Con_Printf ("reject\n%s\n", data); + Con_Printf ("reject\n"); if (CL_IsPendingServerAddress(&net_from)) - { - Cvar_Set(&cl_disconnectreason, va("%s\n", data)); - connectinfo.trying = false; - } + CL_ConnectAbort("%s\n", data); return; } else if (!strcmp(s, "badname")) { //rejected purely because of player name if (CL_IsPendingServerAddress(&net_from)) - { - Cvar_Set(&cl_disconnectreason, va("bad player name\n")); - connectinfo.trying = false; - } + CL_ConnectAbort("bad player name\n"); } else if (!strcmp(s, "badaccount")) { //rejected because username or password is wrong if (CL_IsPendingServerAddress(&net_from)) - { - Cvar_Set(&cl_disconnectreason, va("invalid username or password\n")); - connectinfo.trying = false; - } + CL_ConnectAbort("invalid username or password\n"); } Con_Printf ("f%s\n", s); @@ -3606,40 +3689,50 @@ void CL_ConnectionlessPacket (void) } #ifdef HAVE_DTLS - if (candtls && net_from.prot == NP_DGRAM && (connectinfo.dtlsupgrade || candtls > 1) && !NET_IsEncrypted(&net_from)) + if ((candtls && net_enable_dtls.ival) && net_from.prot == NP_DGRAM && (net_enable_dtls.ival>1 || candtls > 1) && !NET_IsEncrypted(&net_from)) { - //c2s getchallenge - //s2c c%u\0DTLS=$candtls + //c2s getchallenge //<> - //c2s dtlsconnect %u - //s2c dtlsopened + //c2s dtlsconnect %u [REALTARGET] + //s2c dtlsopened //c2s DTLS(getchallenge) //DTLS(etc) - //NOTE: the dtlsconnect/dtlsopened parts are redundant and the non-dtls parts are entirely optional (and should be skipped the client requries/knows the server supports dtls) + //NOTE: the dtlsconnect/dtlsopened parts are redundant and the non-dtls parts are now entirely optional (and should be skipped if the client requries/knows the server supports dtls) //the challenge response includes server capabilities, so we still need the getchallenge/response part of the handshake despite dtls making the actual challenge part redundant. //getchallenge has to be done twice, with the outer one only reporting whether dtls can/should be used. //this means the actual connect packet is already over dtls, which protects the user's userinfo. //FIXME: do rcon via dtls too, but requires tracking pending rcon packets until the handshake completes. - //server says it can do dtls, but will still need to ask it to allocate extra resources for us. + //server says it can do dtls, but will still need to ask it to allocate extra resources for us (I hadn't gotten dtls cookies working properly at that point). - char *pkt; - //qwfwd proxy routing - char *at; - if ((at = strrchr(cls.servername, '@'))) + if (net_enable_dtls.ival>0) { - *at = 0; - pkt = va("%c%c%c%c""dtlsconnect %i %s", 255, 255, 255, 255, connectinfo.challenge, cls.servername); - *at = '@'; + char *pkt; + //qwfwd proxy routing. it doesn't support it yet, but hey, if its willing to forward the dtls packets its all good. + char *at; + if ((at = strrchr(cls.servername, '@'))) + { + *at = 0; + pkt = va("%c%c%c%c""dtlsconnect %i %s", 255, 255, 255, 255, connectinfo.challenge, cls.servername); + *at = '@'; + } + else + pkt = va("%c%c%c%c""dtlsconnect %i", 255, 255, 255, 255, connectinfo.challenge); + NET_SendPacket (cls.sockets, strlen(pkt), pkt, &net_from); + return; + } + else if (candtls >= 3) + { + Cvar_Set(&cl_disconnectreason, va("DTLS is disabled, but server requires it. not connecting\n")); + connectinfo.trying = false; + Con_Printf("DTLS is disabled, but server requires it. Set ^[/net_enable_dtls 1^] before connecting again.\n"); + return; } - else - pkt = va("%c%c%c%c""dtlsconnect %i", 255, 255, 255, 255, connectinfo.challenge); - NET_SendPacket (cls.sockets, strlen(pkt), pkt, &net_from); - return; } - if (connectinfo.dtlsupgrade == DTLS_REQUIRE && !NET_IsEncrypted(&net_from)) + if (net_enable_dtls.ival>=3 && !NET_IsEncrypted(&net_from)) { Cvar_Set(&cl_disconnectreason, va("Server does not support/allow dtls. not connecting\n")); connectinfo.trying = false; @@ -3802,10 +3895,9 @@ void CL_ConnectionlessPacket (void) return; memset(&cred, 0, sizeof(cred)); - cred.peer.name = cls.servername; + cred.peer = connectinfo.peercred; if (NET_DTLS_Create(cls.sockets, &net_from, &cred)) { - connectinfo.dtlsupgrade = DTLS_ACTIVE; connectinfo.numadr = 1; //fixate on this resolved address. connectinfo.adr[0] = net_from; connectinfo.adr[0].prot = NP_DTLS; @@ -3813,11 +3905,7 @@ void CL_ConnectionlessPacket (void) connectinfo.time = 0; //send a new challenge NOW. } else - { - if (connectinfo.dtlsupgrade == DTLS_TRY) - connectinfo.dtlsupgrade = DTLS_DISABLE; - Con_Printf ("unable to establish dtls route\n"); - } + CL_ConnectAbort("Unable to initialise dtls driver. You may need to adjust tls_provider or disable dtls with ^[/net_enable_dtls 0^]\n"); //this is a local issue, and not a result on remote packets. #else Con_Printf ("dtlsopened (unsupported)\n"); #endif @@ -4112,7 +4200,7 @@ void CLNQ_ConnectionlessPacket(void) else { //send a dummy packet. - //this makes our local nat think we initialised the conversation, so that we can receive the. + //this makes our local firewall think we initialised the conversation, so that we can receive their packets. however this only works if our nat uses the same public port for private ports. Netchan_Transmit(&cls.netchan, 1, "\x01", 2500); } return; @@ -5422,9 +5510,9 @@ NORETURN void VARGS Host_EndGame (const char *message, ...) SCR_EndLoadingPlaque(); CL_Disconnect (string); + CL_ConnectAbort(NULL); SV_UnspawnServer(); - connectinfo.trying = false; Cvar_Set(&cl_shownet, "0"); @@ -6215,7 +6303,7 @@ qboolean Host_RunFile(const char *fname, int nlen, vfsfile_t *file) // "quake2:rtc://broker:port/game" // "qw://[stream@]host[:port]/COMMAND" join, spectate, qtvplay //we'll chop off any non-auth prefix, its just so we can handle multiple protocols via a single uri scheme. - char *t, *cmd; + char *t, *cmd, *args; const char *url; char buffer[8192]; const char *schemestart = strchr(fname, ':'); @@ -6261,6 +6349,15 @@ qboolean Host_RunFile(const char *fname, int nlen, vfsfile_t *file) t[urilen] = 0; url = t+schemelen; + *buffer = 0; + for (args = t+schemelen; *args; args++) + { + if (*args == '?') + { + *args++ = 0; + break; + } + } for (cmd = t+schemelen; *cmd; cmd++) { if (*cmd == '/') diff --git a/engine/client/client.h b/engine/client/client.h index 03df673e7..a73af3be3 100644 --- a/engine/client/client.h +++ b/engine/client/client.h @@ -492,7 +492,8 @@ typedef struct infobuf_t userinfo[MAX_SPLITS]; infosync_t userinfosync; - char servername[MAX_OSPATH]; // name of server from original connect + char serverurl[MAX_OSPATH*4]; // eg qw://foo:27500/join?fp=blah + char servername[MAX_OSPATH]; // internal parsing, eg dtls://foo:27500 struct ftenet_connections_s *sockets; diff --git a/engine/client/m_multi.c b/engine/client/m_multi.c index d9b8fde5c..deb5bed6c 100644 --- a/engine/client/m_multi.c +++ b/engine/client/m_multi.c @@ -774,6 +774,20 @@ void M_Menu_GameOptions_f (void) y+=4; info->hostnameedit = MC_AddEdit (menu, 64, 160, y, "Hostname", name.string);y+=info->hostnameedit->common.height; info->publicgame = MC_AddCombo (menu, 64, 160, y, "Public", publicoptions, bound(0, sv_public.ival+1, 4));y+=8; +#if !defined(FTE_TARGET_WEB) && defined(HAVE_DTLS) + { + extern cvar_t net_enable_dtls; + static const char *encoptions[] = + { + "None", + "Accept", + "Request", + "Require", + NULL + }; + MC_AddCvarCombo (menu, 64, 160, y, "DTLS Encryption", &net_enable_dtls, encoptions, NULL);y+=8; + } +#endif y+=4; for (players = 0; players < sizeof(numplayeroptions)/ sizeof(numplayeroptions[0]); players++) @@ -1110,6 +1124,16 @@ void M_Menu_Network_f (void) "Smooth Demos Only", NULL }; +#ifdef HAVE_DTLS + extern cvar_t net_enable_dtls; + static const char *dtlsopts[] = { + "Disabled", + "Accept", + "Request", + "Require", + NULL + }; +#endif static const char *smoothingvalues[] = {"0", "1", "2", NULL}; extern cvar_t cl_download_csprogs, cl_download_redirection, requiredownloads, cl_solid_players; extern cvar_t cl_predict_players, cl_lerp_smooth, cl_predict_extrapolate; @@ -1122,6 +1146,9 @@ void M_Menu_Network_f (void) MB_EDITCVARSLIM("Network FPS", "cl_netfps", "Sets ammount of FPS used to communicate with server (sent and received)"), MB_EDITCVARSLIM("Rate", "rate", "Maximum bytes per second that the server should send to the client"), MB_EDITCVARSLIM("Download Rate", "drate", "Maximum bytes per second that the server should send maps and demos to the client"), +#ifdef HAVE_DTLS + MB_COMBOCVAR("DTLS Encryption", net_enable_dtls, dtlsopts, NULL, "Use this to avoid snooping. Certificates will be pinned."), +#endif MB_SPACING(4), MB_CHECKBOXCVARTIP("Require Download", requiredownloads, 0, "Ignore downloaded content sent to the client and connect immediately"), MB_CHECKBOXCVARTIP("Redirect Download", cl_download_redirection, 0, "Whether the client will ignore download redirection from servers"), diff --git a/engine/client/net_master.c b/engine/client/net_master.c index ca2a7f8c9..5f7d55782 100644 --- a/engine/client/net_master.c +++ b/engine/client/net_master.c @@ -5,6 +5,7 @@ clientside master queries and server ping/polls #include "quakedef.h" #include "cl_master.h" +#include "netinc.h" #define FAVOURITESFILE "favourites.txt" @@ -496,7 +497,10 @@ static void SV_Master_Worker_Resolved(void *ctx, void *data, size_t a, size_t b) { //tcp masters require a route if (NET_AddrIsReliable(na)) - NET_EnsureRoute(svs.sockets, master->cv.name, master->cv.string, na); + { + struct dtlspeercred_s cred = {master->cv.string}; + NET_EnsureRoute(svs.sockets, master->cv.name, &cred, na); + } //q2+qw masters are given a ping to verify that they're still up switch (master->protocol) diff --git a/engine/common/common.c b/engine/common/common.c index 80c9d14d9..bbf3f8625 100644 --- a/engine/common/common.c +++ b/engine/common/common.c @@ -6891,9 +6891,9 @@ static int Base64_Decode(char inp) return (inp-'a') + 26; if (inp >= '0' && inp <= '9') return (inp-'0') + 52; - if (inp == '+') + if (inp == '+' || inp == '-') return 62; - if (inp == '/') + if (inp == '/' || inp == '_') return 63; //if (inp == '=') //padding char return 0; //invalid @@ -6929,6 +6929,23 @@ size_t Base64_EncodeBlock(const qbyte *in, size_t length, char *out, size_t outs *out = 0; return out-start; } +size_t Base64_EncodeBlockURI(const qbyte *in, size_t length, char *out, size_t outsize) +{ //special uri-safe version (also trims) + outsize = Base64_EncodeBlock(in, length, out, outsize); + for (length = 0; length < outsize; length++) + { + if (out[length] == '+') + out[length] = '-'; + else if (out[length] == '/') + out[length] = '_'; + else if (out[length] == '=') + { //truncate it here. + out[length] = 0; + return length; + } + } + return outsize; +} size_t Base64_DecodeBlock(const char *in, const char *in_end, qbyte *out, size_t outsize) { qbyte *start = out; diff --git a/engine/common/log.c b/engine/common/log.c index e58004cd4..a11a6dcb8 100644 --- a/engine/common/log.c +++ b/engine/common/log.c @@ -695,7 +695,10 @@ static void CertLog_Write(void) VFS_PUTS(f, certhex); VFS_PRINTF(f, "\" %i\n", l->trusted?true:false); } + VFS_CLOSE(f); } + else + Con_Printf(CON_ERROR"Unable to write %s\n", CERTLOG_FILENAME); } static void CertLog_Purge(void) { @@ -753,6 +756,7 @@ static void CertLog_Import(const char *filename) } CertLog_Update(addressstring, certdata, certsize, atoi(trusted)); } + VFS_CLOSE(f); } static void CertLog_UntrustAll_f(void) { @@ -795,6 +799,8 @@ qboolean CertLog_ConnectOkay(const char *hostname, void *cert, size_t certsize, extern cvar_t net_enable_dtls; struct certlog_s *l; qboolean trusted = (net_enable_dtls.ival >= 2); + char digest[DIGEST_MAXSIZE]; + char fp[DIGEST_MAXSIZE*2+1]; if (certlog_curprompt) return false; @@ -808,11 +814,20 @@ qboolean CertLog_ConnectOkay(const char *hostname, void *cert, size_t certsize, { //cert is new, but we don't care about full trust. don't bother to prompt when the user doesn't much care. //(but do pin so we at least know when its MITMed after the fact) Con_Printf(CON_WARNING"Auto-Pinning certificate for %s."CON_DEFAULT" ^[/seta %s 2^]+ for actual security.\n", hostname, net_enable_dtls.name); + if (certsize) + Base64_EncodeBlockURI(digest, CalcHash(&hash_sha1, digest, sizeof(digest), cert, certsize), fp, sizeof(fp)); + else + strcpy(fp, ""); + Con_Printf(S_COLOR_GRAY" fp: %s\n", fp); CertLog_Update(hostname, cert, certsize, false); CertLog_Write(); } else if (!l || l->certsize != certsize || memcmp(l->cert, cert, certsize) || (trusted && !l->trusted)) { //new or different + if (certsize) + Base64_EncodeBlockURI(digest, CalcHash(&hash_sha1, digest, sizeof(digest), cert, certsize), fp, sizeof(fp)); + else + strcpy(fp, ""); if (qrenderer) { unsigned int i; @@ -820,7 +835,7 @@ qboolean CertLog_ConnectOkay(const char *hostname, void *cert, size_t certsize, char *text; const char *accepttext; const char *lines[] = { - va(localtext("Certificate for %s\n"), hostname), + va(localtext("Certificate for %s\n(fp:"S_COLOR_GRAY"%s"S_COLOR_WHITE")\n"), hostname, fp), (certlogproblems&CERTLOG_WRONGHOST)?localtext("^1Certificate does not match host\n"):"", ((certlogproblems&(CERTLOG_MISSINGCA|CERTLOG_WRONGHOST))==CERTLOG_MISSINGCA)?localtext("^1Certificate authority is untrusted.\n"):"", (certlogproblems&CERTLOG_EXPIRED)?localtext("^1Expired Certificate\n"):"", diff --git a/engine/common/net.h b/engine/common/net.h index 3994fc337..447d96ca0 100644 --- a/engine/common/net.h +++ b/engine/common/net.h @@ -151,7 +151,8 @@ neterr_t NET_SendPacket (struct ftenet_connections_s *col, int length, const voi int NET_LocalAddressForRemote(struct ftenet_connections_s *collection, netadr_t *remote, netadr_t *local, int idx); void NET_PrintAddresses(struct ftenet_connections_s *collection); qboolean NET_AddressSmellsFunny(netadr_t *a); -qboolean NET_EnsureRoute(struct ftenet_connections_s *collection, char *routename, char *host, netadr_t *adr); +struct dtlspeercred_s; +qboolean NET_EnsureRoute(struct ftenet_connections_s *collection, char *routename, const struct dtlspeercred_s *peerinfo, netadr_t *adr); void NET_TerminateRoute(struct ftenet_connections_s *collection, netadr_t *adr); void NET_PrintConnectionsStatus(struct ftenet_connections_s *collection); @@ -191,9 +192,13 @@ qboolean FTENET_AddToCollection(struct ftenet_connections_s *col, const char *na enum certprops_e { - QCERT_PEERFINGERPRINT + QCERT_ISENCRYPTED, //0 or error + QCERT_PEERSUBJECT, //null terminated. should be a hash of the primary cert, ignoring chain. + QCERT_PEERCERTIFICATE, //should be the primary cert, ignoring chain. no fixed maximum size required, mostly 2k but probably best to allow at leasy 5k.. or 8k. + + QCERT_LOCALCERTIFICATE, //the cert we're using/advertising. may have no context. to tell people what fp to expect. }; -size_t NET_GetConnectionCertificate(struct ftenet_connections_s *col, netadr_t *a, enum certprops_e prop, char *out, size_t outsize); +int NET_GetConnectionCertificate(struct ftenet_connections_s *col, netadr_t *a, enum certprops_e prop, char *out, size_t outsize); #ifdef HAVE_DTLS struct dtlscred_s; @@ -207,7 +212,7 @@ extern cvar_t dtls_psk_hint, dtls_psk_user, dtls_psk_key; #ifdef SUPPORT_ICE neterr_t ICE_SendPacket(size_t length, const void *data, netadr_t *to); void ICE_Terminate(netadr_t *to); //if we kicked the client/etc, kill their ICE too. -qboolean ICE_IsEncrypted(netadr_t *to); +int ICE_GetPeerCertificate(netadr_t *to, enum certprops_e prop, char *out, size_t outsize); void ICE_Init(void); #endif extern cvar_t timeout; diff --git a/engine/common/net_ice.c b/engine/common/net_ice.c index 64a0a01a7..750db26dd 100644 --- a/engine/common/net_ice.c +++ b/engine/common/net_ice.c @@ -2103,9 +2103,12 @@ static qboolean QDECL ICE_Set(struct icestate_s *con, const char *prop, const ch } else if (!strcmp(value, STRINGIFY(ICE_FAILED))) { - con->state = ICE_FAILED; - if (net_ice_debug.ival >= 1) - Con_Printf(S_COLOR_GRAY"[%s]: ice state failed\n", con->friendlyname); + if (con->state != ICE_FAILED) + { + con->state = ICE_FAILED; + if (net_ice_debug.ival >= 1) + Con_Printf(S_COLOR_GRAY"[%s]: ice state failed\n", con->friendlyname); + } } else if (!strcmp(value, STRINGIFY(ICE_CONNECTED))) { @@ -4805,7 +4808,7 @@ qboolean ICE_WasStun(ftenet_connections_t *col) return false; } #ifdef SUPPORT_ICE -qboolean ICE_IsEncrypted(netadr_t *to) +int ICE_GetPeerCertificate(netadr_t *to, enum certprops_e prop, char *out, size_t outsize) { #ifdef HAVE_DTLS struct icestate_s *con; @@ -4813,12 +4816,14 @@ qboolean ICE_IsEncrypted(netadr_t *to) { if (NET_CompareAdr(to, &con->qadr)) { - if (con->dtlsstate) - return true; + if (con->dtlsstate && con->dtlsfuncs->GetPeerCertificate) + return con->dtlsfuncs->GetPeerCertificate(con->dtlsstate, prop, out, outsize); + else if (prop==QCERT_ISENCRYPTED && con->dtlsstate) + return 0; } } #endif - return false; + return -1; } void ICE_Terminate(netadr_t *to) { @@ -5243,14 +5248,16 @@ handleerror: if (cl == -1) { b->error = true; -// Con_Printf("Broker closed connection: %s\n", data); + if (net_ice_debug.ival) + Con_Printf(S_COLOR_GRAY"[%s]: Broker lost connection: %s\n", b->ice?b->ice->friendlyname:"?", *data?data:""); } else if (cl >= 0 && cl < b->numclients) { + if (net_ice_debug.ival) + Con_Printf(S_COLOR_GRAY"[%s]: Broker lost connection: %s\n", b->clients[cl].ice?b->clients[cl].ice->friendlyname:"?", *data?data:""); if (b->clients[cl].ice) iceapi.Close(b->clients[cl].ice, false); b->clients[cl].ice = NULL; -// Con_Printf("Broker closing connection: %s\n", data); } break; case ICEMSG_NAMEINUSE: @@ -5273,13 +5280,23 @@ handleerror: Z_ReallocElements((void**)&b->clients, &b->numclients, cl+1, sizeof(b->clients[0])); } if (cl >= 0 && cl < b->numclients) + { FTENET_ICE_Establish(b, cl, &b->clients[cl].ice); + + if (net_ice_debug.ival) + Con_Printf(S_COLOR_GRAY"[%s]: New client spotted...\n", b->clients[cl].ice?b->clients[cl].ice->friendlyname:"?"); + } + else if (net_ice_debug.ival) + Con_Printf(S_COLOR_GRAY"[%s]: New client spotted, but index is unusable\n", "?"); } else { // Con_DPrintf("Server found: %s\n", data); FTENET_ICE_Establish(b, cl, &b->ice); b->serverid = cl; + + if (net_ice_debug.ival) + Con_Printf(S_COLOR_GRAY"[%s]: Relay to server now open\n", b->ice?b->ice->friendlyname:"?"); } break; case ICEMSG_OFFER: //we received an offer from a client @@ -5299,19 +5316,31 @@ handleerror: { if (cl >= 0 && cl < b->numclients && b->clients[cl].ice) { + if (net_ice_debug.ival) + Con_Printf(S_COLOR_GRAY"[%s]: Got offer:\n%s\n", b->clients[cl].ice?b->clients[cl].ice->friendlyname:"?", data); iceapi.Set(b->clients[cl].ice, "sdpoffer", data); iceapi.Set(b->clients[cl].ice, "state", STRINGIFY(ICE_CONNECTING)); FTENET_ICE_SendOffer(b, cl, b->clients[cl].ice, "sdpanswer"); + break; } + + if (net_ice_debug.ival) + Con_Printf(S_COLOR_GRAY"[%s]: Got bad offer/answer:\n%s\n", b->clients[cl].ice?b->clients[cl].ice->friendlyname:"?", data); } else { if (b->ice) { + if (net_ice_debug.ival) + Con_Printf(S_COLOR_GRAY"[%s]: Got answer:\n%s\n", b->ice?b->ice->friendlyname:"?", data); iceapi.Set(b->ice, "sdpanswer", data); iceapi.Set(b->ice, "state", STRINGIFY(ICE_CONNECTING)); + break; } + + if (net_ice_debug.ival) + Con_Printf(S_COLOR_GRAY"[%s]: Got bad offer/answer:\n%s\n", b->ice?b->ice->friendlyname:"?", data); } break; case ICEMSG_CANDIDATE: @@ -5327,12 +5356,20 @@ handleerror: if (b->generic.islisten) { if (cl >= 0 && cl < b->numclients && b->clients[cl].ice) + { + if (net_ice_debug.ival) + Con_Printf(S_COLOR_GRAY"[%s]: Got candidate:\n%s\n", b->clients[cl].ice->friendlyname, data); iceapi.Set(b->clients[cl].ice, "sdp", data); + } } else { if (b->ice) + { + if (net_ice_debug.ival) + Con_Printf(S_COLOR_GRAY"[%s]: Got candidate:\n%s\n", b->ice->friendlyname, data); iceapi.Set(b->ice, "sdp", data); + } } break; } @@ -5479,4 +5516,4 @@ void ICE_Init(void) Cvar_Register(&net_ice_debug, "networking"); Cmd_AddCommand("net_ice_show", ICE_Show_f); } -#endif \ No newline at end of file +#endif diff --git a/engine/common/net_ssl_gnutls.c b/engine/common/net_ssl_gnutls.c index 32e117ac5..3382c7fe0 100644 --- a/engine/common/net_ssl_gnutls.c +++ b/engine/common/net_ssl_gnutls.c @@ -99,6 +99,7 @@ #define GNUTLS_X509_STUFF \ + GNUTLS_FUNC(gnutls_certificate_server_set_request,void,(gnutls_session_t session, gnutls_certificate_request_t req)) \ GNUTLS_FUNC(gnutls_sec_param_to_pk_bits,unsigned int,(gnutls_pk_algorithm_t algo, gnutls_sec_param_t param)) \ GNUTLS_FUNC(gnutls_x509_crt_init,int,(gnutls_x509_crt_t * cert)) \ GNUTLS_FUNC(gnutls_x509_crt_deinit,void,(gnutls_x509_crt_t cert)) \ @@ -108,6 +109,7 @@ GNUTLS_FUNC(gnutls_x509_crt_set_expiration_time,int,(gnutls_x509_crt_t cert, time_t exp_time)) \ GNUTLS_FUNC(gnutls_x509_crt_set_serial,int,(gnutls_x509_crt_t cert, const void *serial, size_t serial_size)) \ GNUTLS_FUNC(gnutls_x509_crt_set_dn,int,(gnutls_x509_crt_t crt, const char *dn, const char **err)) \ + GNUTLS_FUNC(gnutls_x509_crt_get_dn3,int,(gnutls_x509_crt_t crt, gnutls_datum_t * dn, unsigned flags)) \ GNUTLS_FUNC(gnutls_x509_crt_set_issuer_dn,int,(gnutls_x509_crt_t crt,const char *dn, const char **err)) \ GNUTLS_FUNC(gnutls_x509_crt_set_key,int,(gnutls_x509_crt_t crt, gnutls_x509_privkey_t key)) \ GNUTLS_FUNC(gnutls_x509_crt_export2,int,(gnutls_x509_crt_t cert, gnutls_x509_crt_fmt_t format, gnutls_datum_t * out)) \ @@ -123,7 +125,9 @@ GNUTLS_FUNC(gnutls_pubkey_init,int,(gnutls_pubkey_t * key)) \ GNUTLS_FUNC(gnutls_pubkey_deinit,void,(gnutls_pubkey_t key)) \ GNUTLS_FUNC(gnutls_pubkey_import_x509,int,(gnutls_pubkey_t key, gnutls_x509_crt_t crt, unsigned int flags)) \ - GNUTLS_FUNC(gnutls_pubkey_verify_hash2,int,(gnutls_pubkey_t key, gnutls_sign_algorithm_t algo, unsigned int flags, const gnutls_datum_t * hash, const gnutls_datum_t * signature)) + GNUTLS_FUNC(gnutls_pubkey_verify_hash2,int,(gnutls_pubkey_t key, gnutls_sign_algorithm_t algo, unsigned int flags, const gnutls_datum_t * hash, const gnutls_datum_t * signature)) \ + GNUTLS_FUNC(gnutls_certificate_get_ours,const gnutls_datum_t*,(gnutls_session_t session)) \ + GNUTLS_FUNC(gnutls_certificate_get_crt_raw,int,(gnutls_certificate_credentials_t sc, unsigned idx1, unsigned idx2, gnutls_datum_t * cert)) #define GNUTLS_FUNCS \ @@ -548,7 +552,7 @@ static int QDECL SSL_CheckFingerprint(gnutls_session_t session) if (!memcmp(digest, file->peerdigest, file->peerhashfunc->digestsize)) return 0; } - Con_DPrintf(CON_ERROR "%s: rejecting certificate\n", file->certname); + Con_Printf(CON_ERROR "%s: rejecting certificate\n", file->certname); return GNUTLS_E_CERTIFICATE_ERROR; } #endif @@ -1084,7 +1088,10 @@ qboolean SSL_InitGlobal(qboolean isserver) #endif } else + { qgnutls_certificate_set_verify_function (xcred[isserver], SSL_CheckCert); +// qgnutls_certificate_set_retrieve_function (xcred[isserver], SSL_FindClientCert); + } #endif } else @@ -1151,7 +1158,7 @@ static int GetPSKForServer(gnutls_session_t sess, char **username, gnutls_datum_ if (strcmp(svhint, dtls_psk_user.string) || CalcHashInt(&hash_sha1, dtls_psk_key.string, strlen(dtls_psk_key.string)) != 0x3dd348e4) { Con_Printf(CON_WARNING "Possible QEx Server, please set your ^[%s\\type\\%s^] and ^[%s\\type\\%s^] cvars correctly, their current values are likely to crash the server.\n", dtls_psk_user.name,dtls_psk_user.name, dtls_psk_key.name,dtls_psk_key.name); - return 0; //don't report anything. + return -1; //don't report anything. } } } @@ -1174,7 +1181,10 @@ static int GetPSKForServer(gnutls_session_t sess, char **username, gnutls_datum_ static qboolean SSL_InitConnection(gnutlsfile_t *newf, qboolean isserver, qboolean datagram) { // Initialize TLS session - qgnutls_init (&newf->session, GNUTLS_NONBLOCK|(isserver?GNUTLS_SERVER:GNUTLS_CLIENT)|(datagram?GNUTLS_DATAGRAM:0)); + qgnutls_init (&newf->session, ((newf->certcred)?GNUTLS_FORCE_CLIENT_CERT:0) + |GNUTLS_NONBLOCK + |(isserver?GNUTLS_SERVER:GNUTLS_CLIENT) + |(datagram?GNUTLS_DATAGRAM:0)); if (!isserver) qgnutls_server_name_set(newf->session, GNUTLS_NAME_DNS, newf->certname, strlen(newf->certname)); @@ -1182,6 +1192,7 @@ static qboolean SSL_InitConnection(gnutlsfile_t *newf, qboolean isserver, qboole if (newf->certcred) { + qgnutls_certificate_server_set_request(newf->session, GNUTLS_CERT_REQUIRE); //we will need to validate their fingerprint. qgnutls_credentials_set (newf->session, GNUTLS_CRD_CERTIFICATE, newf->certcred); qgnutls_set_default_priority (newf->session); } @@ -1192,6 +1203,8 @@ static qboolean SSL_InitConnection(gnutlsfile_t *newf, qboolean isserver, qboole qgnutls_credentials_set (newf->session, GNUTLS_CRD_ANON, anoncred[isserver]); #else #ifdef HAVE_DTLS + qgnutls_certificate_server_set_request(newf->session, GNUTLS_CERT_REQUEST); //request a cert, we'll use it for fingerprints. + if (datagram && !isserver) { //do psk as needed. we can still do the cert stuff if the server isn't doing psk. gnutls_psk_client_credentials_t pskcred; @@ -1402,16 +1415,22 @@ static void *GNUDTLS_CreateContext(const dtlscred_t *credinfo, void *cbctx, nete // Sys_Printf("DTLS_CreateContext: server=%i\n", isserver); - if (credinfo && credinfo->local.cert && credinfo->local.key && credinfo->peer.hash) + if (credinfo && ((credinfo->local.cert && credinfo->local.key) || credinfo->peer.hash)) { - gnutls_datum_t pub = {credinfo->local.cert, credinfo->local.certsize}, - priv = {credinfo->local.key, credinfo->local.keysize}; qgnutls_certificate_allocate_credentials (&newf->certcred); - qgnutls_certificate_set_x509_key_mem(newf->certcred, &pub, &priv, GNUTLS_X509_FMT_DER); + if (credinfo->local.cert && credinfo->local.key) + { + gnutls_datum_t pub = {credinfo->local.cert, credinfo->local.certsize}, + priv = {credinfo->local.key, credinfo->local.keysize}; + qgnutls_certificate_set_x509_key_mem(newf->certcred, &pub, &priv, GNUTLS_X509_FMT_DER); + } - newf->peerhashfunc = credinfo->peer.hash; - memcpy(newf->peerdigest, credinfo->peer.digest, newf->peerhashfunc->digestsize); - qgnutls_certificate_set_verify_function (newf->certcred, SSL_CheckFingerprint); + if (credinfo->peer.hash) + { + newf->peerhashfunc = credinfo->peer.hash; + memcpy(newf->peerdigest, credinfo->peer.digest, newf->peerhashfunc->digestsize); + qgnutls_certificate_set_verify_function (newf->certcred, SSL_CheckFingerprint); + } } SSL_SetCertificateName(newf, credinfo?credinfo->peer.name:NULL); @@ -1442,6 +1461,9 @@ static neterr_t GNUDTLS_Transmit(void *ctx, const qbyte *data, size_t datasize) return NETERR_DISCONNECTED; } + if (!datasize) + return NETERR_SENT; + ret = qgnutls_record_send(f->session, data, datasize); if (ret < 0) { @@ -1462,17 +1484,39 @@ static neterr_t GNUDTLS_Received(void *ctx, sizebuf_t *message) if (f->challenging) { - int cli_addr = 0xdeadbeef; //FIXME: replace with client's IP:port. + size_t asize; + safeswitch (net_from.type) + { + case NA_LOOPBACK: asize = 0; break; + case NA_IP: asize = sizeof(net_from.address.ip); break; + case NA_IPV6: asize = sizeof(net_from.address.ip6); break; + case NA_IPX: asize = sizeof(net_from.address.ipx); break; +#ifdef UNIXSOCKETS + case NA_UNIX: asize = (qbyte*)&net_from.address.un.path[net_from.address.un.len]-(qbyte*)&net_from.address; break; //unlikely to be spoofed... +#endif +#ifdef IRCCONNECT + //case NA_IRC: +#endif +#ifdef HAVE_WEBSOCKCL + //case NA_WEBSOCKET: //basically web browser. +#endif +#ifdef SUPPORT_ICE + case NA_ICE: asize = strlen(net_from.address.icename); break; +#endif + case NA_INVALID: + safedefault: return NETERR_NOROUTE; + } + memset(&f->prestate, 0, sizeof(f->prestate)); ret = qgnutls_dtls_cookie_verify(&cookie_key, - &cli_addr, sizeof(cli_addr), + &net_from.address, asize, message->data, message->cursize, &f->prestate); if (ret == GNUTLS_E_BAD_COOKIE) { qgnutls_dtls_cookie_send(&cookie_key, - &cli_addr, sizeof(cli_addr), + &net_from.address, asize, &f->prestate, (gnutls_transport_ptr_t)f, DTLS_Push); return NETERR_CLOGGED; @@ -1599,6 +1643,74 @@ static neterr_t GNUDTLS_Timeouts(void *ctx) return NETERR_SENT; } +static int GNUDTLS_GetPeerCertificate(void *ctx, enum certprops_e prop, char *out, size_t outsize) +{ + gnutlsfile_t *f = (gnutlsfile_t *)ctx; + if (f && (f->challenging || f->handshaking)) + return -1; //no cert locked down yet... + safeswitch(prop) + { + case QCERT_ISENCRYPTED: + return 0; //well, should be... + case QCERT_PEERSUBJECT: + { + unsigned int certcount; + const gnutls_datum_t *const certlist = qgnutls_certificate_get_peers(f->session, &certcount); + if (certlist) + { + gnutls_x509_crt_t cert = NULL; + gnutls_datum_t dn={NULL}; + qgnutls_x509_crt_init(&cert); + qgnutls_x509_crt_import(cert, certlist, GNUTLS_X509_FMT_DER); + qgnutls_x509_crt_get_dn3(cert, &dn, 0); + if (dn.size >= outsize) + dn.size = -1; //too big... + else + { + memcpy(out, dn.data, dn.size); + out[dn.size] = 0; + } + (*qgnutls_free)(dn.data); + qgnutls_x509_crt_deinit(cert); + return (int)dn.size; + } + } + return -1; + case QCERT_PEERCERTIFICATE: + { + unsigned int certcount; + const gnutls_datum_t *const certlist = qgnutls_certificate_get_peers(f->session, &certcount); + if (certlist && certlist->size <= outsize) + { + memcpy(out, certlist->data, certlist->size); + return certlist->size; + } + } + return -1; + case QCERT_LOCALCERTIFICATE: + { + const gnutls_datum_t *cert; + gnutls_datum_t d; + + if (f) + cert = qgnutls_certificate_get_ours(f->session); + else //no actual context? get our default dtls server cert. + { + qgnutls_certificate_get_crt_raw (xcred[true], 0/*first chain*/, 0/*primary one*/, &d); + cert = &d; + } + if (cert->size <= outsize) + { + memcpy(out, cert->data, cert->size); + return cert->size; + } + } + return -1; + safedefault: + return -1; //dunno what you want from me. + } +} + static qboolean GNUDTLS_GenTempCertificate(const char *subject, struct dtlslocalcred_s *qcred) { gnutls_datum_t priv = {NULL}, pub = {NULL}; @@ -1681,7 +1793,7 @@ static const dtlsfuncs_t dtlsfuncs_gnutls = GNUDTLS_Transmit, GNUDTLS_Received, GNUDTLS_Timeouts, - NULL, + GNUDTLS_GetPeerCertificate, GNUDTLS_GenTempCertificate }; static const dtlsfuncs_t *GNUDTLS_InitServer(void) diff --git a/engine/common/net_ssl_winsspi.c b/engine/common/net_ssl_winsspi.c index a2495c7e8..8a25c5c78 100644 --- a/engine/common/net_ssl_winsspi.c +++ b/engine/common/net_ssl_winsspi.c @@ -2,9 +2,21 @@ #if defined(HAVE_WINSSPI) /*regarding HAVE_DTLS -DTLS1.0 is supported from win8 onwards -Its also meant to be supported from some RDP server patch on win7, but I can't get it to work. -I've given up for now. +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" @@ -12,6 +24,7 @@ I've given up for now. #define SECURITY_WIN32 #include #include +#include #include #define SP_PROT_TLS1_1_SERVER 0x00000100 @@ -22,14 +35,21 @@ I've given up for now. #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_DTLS_SERVER) -#define USE_PROT_DGRAM_CLIENT (SP_PROT_DTLS_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" @@ -45,6 +65,13 @@ I've given up for now. #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 + //hungarian ensures we hit no macros. static struct @@ -57,6 +84,7 @@ static struct 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); @@ -67,11 +95,26 @@ static struct 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[] = @@ -83,6 +126,7 @@ void SSL_Init(void) {(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"}, @@ -94,9 +138,26 @@ void SSL_Init(void) {(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} }; @@ -104,13 +165,15 @@ void SSL_Init(void) 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; + 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) +#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 { @@ -146,9 +209,11 @@ typedef struct { CredHandle cred; SecHandle sechnd; - int headersize, footersize; + 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); @@ -200,7 +265,7 @@ static void SSPI_Error(sslfile_t *f, const char *error, ...) f->stream = NULL; } -static void SSPI_TryFlushCryptOut(sslfile_t *f) +static neterr_t SSPI_TryFlushCryptOut(sslfile_t *f) { int sent; if (f->outcrypt.avail) @@ -208,21 +273,24 @@ static void SSPI_TryFlushCryptOut(sslfile_t *f) #ifdef HAVE_DTLS if (f->transmit) { - f->transmit(f->cbctx, f->outcrypt.data, f->outcrypt.avail); + neterr_t e = f->transmit(f->cbctx, f->outcrypt.data, f->outcrypt.avail); f->outcrypt.avail = 0; - return; + return e; } #endif sent = VFS_WRITE(f->stream, f->outcrypt.data, f->outcrypt.avail); } else - return; + return NETERR_SENT; - if (sent > 0) + 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) @@ -265,9 +333,10 @@ static void SSPI_Decode(sslfile_t *f) SecBuff[3].BufferType = SECBUFFER_EMPTY; //space for extra marker ss = secur.pDecryptMessage(&f->sechnd, &BuffDesc, 0, &ulQop); - - if (ss < 0) + 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) @@ -278,6 +347,7 @@ static void SSPI_Decode(sslfile_t *f) { 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; @@ -318,7 +388,7 @@ static void SSPI_Decode(sslfile_t *f) } //convert outgoing data->crypt -static void SSPI_Encode(sslfile_t *f) +static neterr_t SSPI_Encode(sslfile_t *f) { SECURITY_STATUS ss; SecBufferDesc BuffDesc; @@ -329,16 +399,16 @@ static void SSPI_Encode(sslfile_t *f) { SSPI_TryFlushCryptOut(f); if (f->outcrypt.avail) - return; //don't flood too much + return NETERR_CLOGGED; //don't flood too much } //don't corrupt the handshake data. if (f->handshaking) - return; + return NETERR_CLOGGED; if (!f->outraw.avail) - return; + return NETERR_SENT; BuffDesc.ulVersion = SECBUFFER_VERSION; BuffDesc.cBuffers = 4; @@ -364,8 +434,12 @@ static void SSPI_Encode(sslfile_t *f) if (ss < 0) { - SSPI_Error(f, "EncryptMessage failed\n"); - return; + 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; @@ -374,20 +448,20 @@ static void SSPI_Encode(sslfile_t *f) if (SSPI_CopyIntoBuffer(&f->outcrypt, SecBuff[0].pvBuffer, SecBuff[0].cbBuffer, true) < SecBuff[0].cbBuffer) { SSPI_Error(f, "crypt buffer overflowed\n"); - return; + 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; + 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; + return NETERR_DISCONNECTED; } - SSPI_TryFlushCryptOut(f); + return SSPI_TryFlushCryptOut(f); } char *narrowen(char *out, size_t outlen, wchar_t *wide); @@ -412,7 +486,8 @@ static DWORD VerifyKnownCertificates(DWORD status, wchar_t *domain, qbyte *data, status = SEC_E_OK; else #endif - status = TRUST_E_FAIL; + if (SUCCEEDED(status)) + status = TRUST_E_EXPLICIT_DISTRUST; } return status; } @@ -431,7 +506,7 @@ static DWORD VerifyKnownCertificates(DWORD status, wchar_t *domain, qbyte *data, 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_FAIL; + status = TRUST_E_EXPLICIT_DISTRUST; } BZ_Free(knowncert); } @@ -518,27 +593,28 @@ static DWORD VerifyServerCertificate(PCCERT_CONTEXT pServerCert, PWSTR pwszServe 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 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_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 = "(unknown)"; 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); @@ -558,21 +634,84 @@ static DWORD VerifyServerCertificate(PCCERT_CONTEXT pServerCert, PWSTR pwszServe } static PCCERT_CONTEXT SSPI_GetServerCertificate(void) { + //frankly this is all kinda fucked static PCCERT_CONTEXT ret; - char *issuertext = "CN=127.0.0.1, O=\"FTE QuakeWorld\", OU=Testing, C=TR"; + 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; - if (ret) + 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 += 2; //2 years hence. woo + 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)); @@ -580,11 +719,28 @@ static PCCERT_CONTEXT SSPI_GetServerCertificate(void) 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( - 0, + prov, &issuerblob, 0, - NULL, + &kpi, &sigalg, NULL, &expiredate, @@ -594,18 +750,92 @@ static PCCERT_CONTEXT SSPI_GetServerCertificate(void) { //try and downgrade the signature algo if it failed. sigalg.pszObjId = szOID_RSA_SHA1RSA; ret = crypt.pCertCreateSelfSignCertificate( - 0, + prov, &issuerblob, 0, - NULL, + &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; } @@ -632,9 +862,14 @@ static void SSPI_GenServerCredentials(sslfile_t *f) } ss = secur.pAcquireCredentialsHandleA (NULL, UNISP_NAME_A, SECPKG_CRED_INBOUND, NULL, &SchannelCred, NULL, NULL, &f->cred, &Lifetime); - if (ss < 0) + if (FAILED(ss) && f->datagram) + { //try again with just dtls1, for cred, &Lifetime); + } + if (FAILED(ss)) { - SSPI_Error(f, localtext("WinSSPI: AcquireCredentialsHandle failed\n")); + SSPI_Error(f, localtext("WinSSPI: AcquireCredentialsHandle failed %#x\n"), ss); return; } } @@ -652,9 +887,6 @@ static void SSPI_Handshake (sslfile_t *f) int i; qboolean retries = 5; -// char buf1[128]; -// char buf2[128]; - retry: if (f->outcrypt.avail) @@ -686,6 +918,8 @@ retry: return; //gave up. else if (f->handshaking == HS_STARTCLIENT) { + PCCERT_CONTEXT cert; + //no input data yet. f->handshaking = HS_CLIENT; @@ -694,13 +928,25 @@ retry: 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*/ - ss = secur.pAcquireCredentialsHandleA (NULL, UNISP_NAME_A, SECPKG_CRED_OUTBOUND, NULL, &SchannelCred, NULL, NULL, &f->cred, &Lifetime); - if (ss < 0) + //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) { - SSPI_Error(f, localtext("WINSSPI: AcquireCredentialsHandle failed\n")); - return; + 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) @@ -730,11 +976,11 @@ retry: 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, &f->sechnd, &OutBuffDesc, &ContextAttributes, &Lifetime); + 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) - { -// Con_Printf("SEC_E_INCOMPLETE_MESSAGE\n"); + { //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; @@ -780,6 +1026,14 @@ retry: 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; @@ -789,15 +1043,26 @@ retry: i = 1; OutSecBuff[i++].BufferType = SECBUFFER_EXTRA; - OutSecBuff[i++].BufferType = 17/*SECBUFFER_ALERT*/; + 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) +#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, - ServerMessageAttribute|(f->datagram?ASC_REQ_DATAGRAM:ASC_REQ_STREAM), SECURITY_NETWORK_DREP, &f->sechnd, + 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; @@ -809,12 +1074,11 @@ retry: SSPI_ExpandBuffer(&f->incrypt, f->incrypt.datasize+1024); return; } -// else -// Con_Printf("InitializeSecurityContextA %x\n", ss); - f->handshaking = HS_SERVER; +// 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) + 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; @@ -827,20 +1091,23 @@ retry: if (ss == SEC_I_INCOMPLETE_CREDENTIALS) { - SSPI_Error(f, localtext("server requires credentials\n")); - return; + //FIXME: load/generate our identity, redo the pAcquireCredentialsHandleA + Con_TPrintf(CON_WARNING"server requires credentials, attempting to ignore\n"); + goto retry; } - if (ss < 0) + if (FAILED(ss)) { + const char *fname = (f->handshaking>=HS_STARTSERVER)?"AcceptSecurityContext":"InitializeSecurityContext"; switch(ss) { - case SEC_E_ALGORITHM_MISMATCH: SSPI_Error(f, "InitializeSecurityContext failed: SEC_E_ALGORITHM_MISMATCH\n"); break; - case SEC_E_INVALID_HANDLE: SSPI_Error(f, "InitializeSecurityContext failed: SEC_E_INVALID_HANDLE\n"); break; - case SEC_E_ILLEGAL_MESSAGE: SSPI_Error(f, "InitializeSecurityContext failed: SEC_E_ILLEGAL_MESSAGE\n"); break; - case SEC_E_INVALID_TOKEN: SSPI_Error(f, "InitializeSecurityContext failed: SEC_E_INVALID_TOKEN\n"); break; - case SEC_E_INVALID_PARAMETER: SSPI_Error(f, "InitializeSecurityContext failed: SEC_E_INVALID_PARAMETER\n"); break; - default: SSPI_Error(f, "InitializeSecurityContext failed: %lx\n", (long)ss); break; + 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; } @@ -861,9 +1128,20 @@ retry: 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) @@ -876,7 +1154,7 @@ retry: } if (VerifyServerCertificate(remotecert, f->wpeername, 0, f->datagram)) { - SSPI_Error(f, localtext("Error validating certificante\n")); + SSPI_Error(f, localtext("Error validating certificate\n")); return; } } @@ -893,7 +1171,10 @@ retry: { 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 @@ -950,7 +1231,7 @@ static int QDECL SSPI_WriteBytes (struct vfsfile_s *file, const void *buffer, in //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 false; + return 0; bytestowrite = SSPI_CopyIntoBuffer(&f->outraw, buffer, bytestowrite, false); @@ -996,27 +1277,54 @@ static qboolean QDECL SSPI_Close (struct vfsfile_s *file) } #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; - int i = 0; - int err; - unsigned int c; -// const char *localname; - const char *peername; - if (!source || !SSL_Inited()) return NULL; - if (server) - { -// localname = servername; - peername = ""; - } - else - { -// localname = ""; - peername = servername; - } /* if (server) //unsupported @@ -1024,22 +1332,11 @@ static vfsfile_t *SSPI_OpenVFS(const char *servername, vfsfile_t *source, qboole */ newf = Z_Malloc(sizeof(*newf)); - while(*peername) + if (!CleanUpHostname(server?"":servername, newf->wpeername, countof(newf->wpeername))) { - c = utf8_decode(&err, peername, (void*)&peername); - if (c > WCHAR_MAX) - err = true; //no 16bit surrogates. they're evil. - else if (i == sizeof(newf->wpeername)/sizeof(newf->wpeername[0]) - 1) - err = true; //no space to store it - else - newf->wpeername[i++] = c; - if (err) - { - Z_Free(newf); - return NULL; - } + Z_Free(newf); + return NULL; } - newf->wpeername[i] = 0; newf->handshaking = server?HS_STARTSERVER:HS_STARTCLIENT; newf->stream = source; @@ -1085,7 +1382,7 @@ static int SSPI_GetChannelBinding(vfsfile_t *vf, qbyte *binddata, size_t *bindsi switch(secur.pQueryContextAttributesA(&f->sechnd, SECPKG_ATTR_UNIQUE_BINDINGS, &bindings)) { case SEC_E_OK: - if (bindings.Bindings->cbApplicationDataLength <= *bindsize) + 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; @@ -1108,8 +1405,6 @@ static int SSPI_GetChannelBinding(vfsfile_t *vf, qbyte *binddata, size_t *bindsi #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) { - int i = 0; - const char *remotehost = credinfo->peer.name; sslfile_t *ctx; if (!SSL_Inited()) return NULL; @@ -1120,23 +1415,11 @@ static void *SSPI_DTLS_CreateContext(const dtlscred_t *credinfo, void *cbctx, ne ctx->cbctx = cbctx; ctx->transmit = push; - while(*remotehost) + if (!CleanUpHostname((credinfo&&credinfo->peer.name)?credinfo->peer.name:"", ctx->wpeername, countof(ctx->wpeername))) { - int err; - int c = utf8_decode(&err, remotehost, (void*)&remotehost); - if (c > WCHAR_MAX) - err = true; //no 16bit surrogates. they're evil. - else if (i == sizeof(ctx->wpeername)/sizeof(ctx->wpeername[0]) - 1) - err = true; //no space to store it - else - ctx->wpeername[i++] = c; - if (err) - { - Z_Free(ctx); - return NULL; - } + Z_Free(ctx); + return NULL; } - ctx->wpeername[i] = 0; SSPI_ExpandBuffer(&ctx->outraw, 8192); SSPI_ExpandBuffer(&ctx->outcrypt, 65536); @@ -1158,31 +1441,33 @@ static void SSPI_DTLS_DestroyContext(void *vctx) static neterr_t SSPI_DTLS_Transmit(void *ctx, const qbyte *data, size_t datasize) { - int ret; sslfile_t *f = (sslfile_t *)ctx; -//Con_Printf("DTLS_Transmit: %i\n", datasize); + 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); - - if (f->handshaking) - { - SSPI_Handshake(f); - - if (f->handshaking == HS_ERROR) - ret = NETERR_DISCONNECTED; - else - ret = NETERR_CLOGGED; //not ready yet - } - else - { - SSPI_Encode(f); - ret = NETERR_SENT; - } - - return ret; + return SSPI_Encode(f); } static neterr_t SSPI_DTLS_Received(void *ctx, sizebuf_t *msg) @@ -1190,8 +1475,6 @@ static neterr_t SSPI_DTLS_Received(void *ctx, sizebuf_t *msg) int ret; sslfile_t *f = (sslfile_t *)ctx; -//Con_Printf("DTLS_Received: %i\n", datasize); - f->incrypt.data = msg->data; f->incrypt.avail = f->incrypt.datasize = msg->cursize; @@ -1208,7 +1491,7 @@ static neterr_t SSPI_DTLS_Received(void *ctx, sizebuf_t *msg) SSPI_Decode(f); ret = NETERR_SENT; - if (f->inraw.avail > msg->maxsize) + if (f->inraw.avail < msg->maxsize) msg->cursize = f->inraw.avail; else msg->cursize = msg->maxsize; @@ -1223,38 +1506,179 @@ static neterr_t SSPI_DTLS_Timeouts(void *ctx) sslfile_t *f = (sslfile_t *)ctx; if (f->handshaking) { -// SSPI_Handshake(f); + 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: + safedefault: + return -1; + } +} + static const dtlsfuncs_t dtlsfuncs_schannel = { SSPI_DTLS_CreateContext, - NULL, + 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) +static const dtlsfuncs_t *SSPI_DTLS_InitServer(void) { - //FIXME: at this point, schannel is still returning errors when I try acting as a server. - //so just block any attempt to use this as a server. - //clients don't need/get certs. + //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 - -//#include //windows sucks too much to actually include this. oh well. +#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; @@ -1325,14 +1749,15 @@ static enum hashvalidation_e SSPI_VerifyHash(const qbyte *hashdata, size_t hashs return VH_INCORRECT; //its bad return VH_UNSUPPORTED; //some weird transient error...? } +#endif ftecrypto_t crypto_sspi = { - "WinSSPI", + "SChannel", SSPI_OpenVFS, SSPI_GetChannelBinding, SSPI_DTLS_InitClient, - NULL,//SSPI_DTLS_InitServer, + SSPI_DTLS_InitServer, SSPI_VerifyHash, NULL,//SSPI_GenerateHash, }; diff --git a/engine/common/net_wins.c b/engine/common/net_wins.c index 1498a50c8..173cdfab0 100644 --- a/engine/common/net_wins.c +++ b/engine/common/net_wins.c @@ -155,6 +155,9 @@ cvar_t net_enable_websockets = CVARD("net_enable_websockets", "1", "If enabled, #if defined(HAVE_SERVER) static void QDECL NET_Enable_DTLS_Changed(struct cvar_s *var, char *oldvalue) { + char cert[8192]; + int certsize; + char digest[DIGEST_MAXSIZE]; var->ival = var->value; //set up the default value if (!*var->string) @@ -165,6 +168,12 @@ static void QDECL NET_Enable_DTLS_Changed(struct cvar_s *var, char *oldvalue) svs.sockets->dtlsfuncs = (var->ival)?DTLS_InitServer():NULL; if (!svs.sockets->dtlsfuncs && var->ival >= 2) Con_Printf("%sUnable to set %s to \"%s\", no DTLS provider available.\n", (var->ival >= 2)?CON_ERROR:CON_WARNING, var->name, var->string); + + certsize = svs.sockets->dtlsfuncs?svs.sockets->dtlsfuncs->GetPeerCertificate(NULL, QCERT_LOCALCERTIFICATE, cert, sizeof(cert)):-1; + if (certsize > 0) + InfoBuf_SetStarBlobKey(&svs.info, "*fp", cert, Base64_EncodeBlockURI(digest, CalcHash(&hash_sha1, digest, sizeof(digest), cert, certsize), cert, sizeof(cert))); + else + InfoBuf_SetStarKey(&svs.info, "*fp", ""); } } cvar_t net_enable_dtls = CVARAFCD("net_enable_dtls", "", "sv_listen_dtls", 0, NET_Enable_DTLS_Changed, "Controls serverside dtls support.\n0: dtls blocked, not advertised.\n1: clientside choice.\n2: used where possible (recommended setting).\n3: disallow non-dtls clients (sv_port_tcp should be eg tls://[::]:27500 to also disallow unencrypted tcp connections)."); @@ -1565,6 +1574,7 @@ size_t NET_StringToAdr2 (const char *s, int defaultport, netadr_t *a, size_t num netproto_t prot; netadrtype_t afhint; char *path; + char *args; struct { @@ -1793,6 +1803,10 @@ size_t NET_StringToAdr2 (const char *s, int defaultport, netadr_t *a, size_t num } } + args = strchr(s, '?'); + if (args) + *args=0; + path = strchr(s, '/'); #if !defined(HAVE_WEBSOCKCL) && defined(SUPPORT_ICE) if (path == s && fs_manifest->rtcbroker && *fs_manifest->rtcbroker) @@ -1829,6 +1843,9 @@ size_t NET_StringToAdr2 (const char *s, int defaultport, netadr_t *a, size_t num a[i].prot = prot; } + if (args) + *args='?'; + return result; } @@ -2132,7 +2149,7 @@ qboolean NET_IsEncrypted(netadr_t *adr) if (adr->type == NA_LOOPBACK) return true; //might as well claim it, others can't snoop on it so... #ifdef SUPPORT_ICE - if (adr->type == NA_ICE && ICE_IsEncrypted(adr)) + if (adr->type == NA_ICE && ICE_GetPeerCertificate(adr, QCERT_ISENCRYPTED, NULL, 0)==0) return true; #endif #if defined(FTE_TARGET_WEB) @@ -3230,7 +3247,8 @@ qboolean NET_DTLS_Decode(ftenet_connections_t *col) switch(peer->funcs->Received(peer->dtlsstate, &net_message)) { case NETERR_DISCONNECTED: - NET_DTLS_DisconnectPeer(col, peer); + if (col->islisten) + NET_DTLS_DisconnectPeer(col, peer); net_message.cursize = 0; break; case NETERR_NOROUTE: @@ -3254,36 +3272,33 @@ qboolean NET_DTLS_Decode(ftenet_connections_t *col) #endif -size_t NET_GetConnectionCertificate(struct ftenet_connections_s *col, netadr_t *a, enum certprops_e prop, char *out, size_t outsize) +int NET_GetConnectionCertificate(struct ftenet_connections_s *col, netadr_t *a, enum certprops_e prop, char *out, size_t outsize) { if (!col) - return 0; + return -1; - switch(prop) - { - default: - break; - case QCERT_PEERFINGERPRINT: -#if 0//def HAVE_DTLS - if (a->prot == NP_DTLS) - { - struct dtlspeer_s *peer; - { - a->prot = NP_DGRAM; - for (peer = col->dtls; peer; peer = peer->next) - { - if (NET_CompareAdr(&peer->addr, a)) - break; - } - a->prot = NP_DTLS; - } - if (peer) - return peer->funcs->GetPeerCertificate(peer->dtlsstate, data, length); - } +#ifdef SUPPORT_ICE + if (a->type == NA_ICE) + return ICE_GetPeerCertificate(a, prop, out, outsize); #endif - return 0; +#ifdef HAVE_DTLS + if (a->prot == NP_DTLS) + { + struct dtlspeer_s *peer; + { + a->prot = NP_DGRAM; + for (peer = col->dtls; peer; peer = peer->next) + { + if (NET_CompareAdr(&peer->addr, a)) + break; + } + a->prot = NP_DTLS; + } + if (peer && peer->funcs->GetPeerCertificate) + return peer->funcs->GetPeerCertificate(peer->dtlsstate, prop, out, outsize); } - return 0; +#endif + return -1; } @@ -8212,7 +8227,7 @@ neterr_t NET_SendPacket (ftenet_connections_t *collection, int length, const voi return NET_SendPacketCol (collection, length, data, to); } -qboolean NET_EnsureRoute(ftenet_connections_t *collection, char *routename, char *host, netadr_t *adr) +qboolean NET_EnsureRoute(ftenet_connections_t *collection, char *routename, const struct dtlspeercred_s *peerinfo, netadr_t *adr) { switch(adr->prot) { @@ -8225,11 +8240,11 @@ qboolean NET_EnsureRoute(ftenet_connections_t *collection, char *routename, char case NP_DTLS: #ifdef HAVE_DTLS adr->prot = NP_DGRAM; - if (NET_EnsureRoute(collection, routename, host, adr)) + if (NET_EnsureRoute(collection, routename, peerinfo, adr)) { dtlscred_t cred; memset(&cred, 0, sizeof(cred)); - cred.peer.name = host; + cred.peer = *peerinfo; if (NET_DTLS_Create(collection, adr, &cred)) { adr->prot = NP_DTLS; @@ -8243,14 +8258,14 @@ qboolean NET_EnsureRoute(ftenet_connections_t *collection, char *routename, char case NP_WSS: case NP_TLS: case NP_STREAM: - if (!FTENET_AddToCollection(collection, routename, host, adr->type, adr->prot)) + if (!FTENET_AddToCollection(collection, routename, peerinfo->name, adr->type, adr->prot)) return false; - Con_Printf("Establishing connection to %s\n", host); + Con_Printf("Establishing connection to %s\n", peerinfo->name); break; #if defined(SUPPORT_ICE) || defined(FTE_TARGET_WEB) case NP_RTC_TCP: case NP_RTC_TLS: - if (!FTENET_AddToCollection(collection, routename, host, adr->type, adr->prot)) + if (!FTENET_AddToCollection(collection, routename, peerinfo->name, adr->type, adr->prot)) return false; break; #endif @@ -9478,7 +9493,10 @@ int QDECL VFSTCP_ReadBytes (struct vfsfile_s *file, void *buffer, int bytestorea { trying = VFSTCP_IsStillConnecting(tf->sock); if (trying < 0) + { tf->readaborted = trying; + tf->writeaborted = true; + } else if (trying) return 0; tf->conpending = false; @@ -9509,18 +9527,22 @@ int QDECL VFSTCP_ReadBytes (struct vfsfile_s *file, void *buffer, int bytestorea case NET_ENOTCONN: Con_Printf("connection to \"%s\" failed\n", tf->peer); tf->readaborted = VFS_ERROR_NORESPONSE; + tf->writeaborted = true; break; case NET_ECONNABORTED: Con_DPrintf("connection to \"%s\" aborted\n", tf->peer); tf->readaborted = VFS_ERROR_NORESPONSE; + tf->writeaborted = true; break; case NET_ETIMEDOUT: Con_Printf("connection to \"%s\" timed out\n", tf->peer); tf->readaborted = VFS_ERROR_NORESPONSE; + tf->writeaborted = true; break; case NET_ECONNREFUSED: Con_DPrintf("connection to \"%s\" refused\n", tf->peer); tf->readaborted = VFS_ERROR_REFUSED; + tf->writeaborted = true; break; case NET_ECONNRESET: Con_DPrintf("connection to \"%s\" reset\n", tf->peer); @@ -9579,7 +9601,7 @@ int QDECL VFSTCP_WriteBytes (struct vfsfile_s *file, const void *buffer, int byt tf->conpending = false; } - len = send(tf->sock, buffer, bytestoread, 0); + len = send(tf->sock, buffer, bytestoread, MSG_NOSIGNAL); if (len == -1 || len == 0) { int reason = VFS_ERROR_UNSPECIFIED; @@ -9591,9 +9613,13 @@ int QDECL VFSTCP_WriteBytes (struct vfsfile_s *file, const void *buffer, int byt return 0; //nothing available yet. case NET_ETIMEDOUT: Con_Printf("connection to \"%s\" timed out\n", tf->peer); + tf->writeaborted = true; + tf->conpending = false; return VFS_ERROR_NORESPONSE; //don't bother trying to read if we never connected. case NET_ECONNREFUSED: //peer sent a reset instead of accepting a new connection Con_DPrintf("connection to \"%s\" refused\n", tf->peer); + tf->writeaborted = true; + tf->conpending = false; return VFS_ERROR_REFUSED; //don't bother trying to read if we never connected. case NET_ECONNABORTED: //peer closed its socket Con_Printf("connection to \"%s\" aborted\n", tf->peer); @@ -9608,6 +9634,8 @@ int QDECL VFSTCP_WriteBytes (struct vfsfile_s *file, const void *buffer, int byt case EPIPE: #endif Con_Printf("connection to \"%s\" failed\n", tf->peer); + tf->writeaborted = true; + tf->conpending = false; return VFS_ERROR_NORESPONSE; //don't bother trying to read if we never connected. default: Sys_Printf("tcp socket error %i (%s)\n", e, tf->peer); diff --git a/engine/common/netinc.h b/engine/common/netinc.h index 52935ab7e..d4e9e4d85 100644 --- a/engine/common/netinc.h +++ b/engine/common/netinc.h @@ -223,6 +223,9 @@ #ifndef INVALID_SOCKET #define INVALID_SOCKET -1 #endif +#ifndef MSG_NOSIGNAL + #define MSG_NOSIGNAL 0 //available on linux, no idea about other unixes. don't bug out too much... (d)tls needs this to not get constant SIGPIPE errors +#endif #ifndef INADDR_LOOPBACK #define INADDR_LOOPBACK 0x7f000001 @@ -372,7 +375,7 @@ typedef struct dtlsfuncs_s neterr_t (*Transmit)(void *ctx, const qbyte *data, size_t datasize); neterr_t (*Received)(void *ctx, sizebuf_t *message); //operates in-place... neterr_t (*Timeouts)(void *ctx); - void (*GetPeerCertificate)(void *ctx); + int (*GetPeerCertificate)(void *ctx, enum certprops_e prop, char *out, size_t outsize); qboolean (*GenTempCertificate)(const char *subject, struct dtlslocalcred_s *cred); } dtlsfuncs_t; #ifdef HAVE_DTLS diff --git a/engine/server/pr_cmds.c b/engine/server/pr_cmds.c index 0ceb8aaee..b8e3f7c16 100644 --- a/engine/server/pr_cmds.c +++ b/engine/server/pr_cmds.c @@ -6462,6 +6462,18 @@ char *PF_infokey_Internal (int entnum, const char *key) sprintf(ov, "%d", SV_CalcPing (&svs.clients[entnum-1], true)); else if (!strcmp(key, "guid")) sprintf(ov, "%s", pl->guid); + else if (!strcmp(key, "*cert_sha1")) + { + char buf[8192]; + char digest[DIGEST_MAXSIZE]; + int certsize = NET_GetConnectionCertificate(svs.sockets, &controller->netchan.remote_address, QCERT_PEERCERTIFICATE, buf, sizeof(buf)); + if (certsize <= 0) + value = ""; + else + Base64_EncodeBlockURI(digest,CalcHash(&hash_sha1, digest, sizeof(digest), buf, certsize), ov, sizeof(ov)); + } + else if (!strcmp(key, "*cert_dn")) + NET_GetConnectionCertificate(svs.sockets, &controller->netchan.remote_address, QCERT_PEERSUBJECT, ov, sizeof(ov)); else if (!strcmp(key, "challenge")) sprintf(ov, "%u", pl->challenge); else if (!strcmp(key, "*userid")) @@ -12567,7 +12579,7 @@ void PR_ResetBuiltins(progstype_t type) //fix all nulls to PF_FIXME and add any } } if (!BuiltinList[i].name) - Con_Printf("Failed to map builtin %s to %i specified in fte_bimap.dat\n", com_token, binum); + Con_Printf("Failed to map builtin %s to %i specified in fte_bimap.txt\n", com_token, binum); } } } @@ -13688,6 +13700,8 @@ void PR_DumpPlatform_f(void) {"INFOKEY_P_CSQCACTIVE","const string", QW|NQ, D("Client has csqc enabled. CSQC ents etc will be sent to this player."), 0, "\"csqcactive\""}, {"INFOKEY_P_SVPING", "const string", QW|NQ, NULL, 0, "\"svping\""}, {"INFOKEY_P_GUID", "const string", QW|NQ, D("Some hash string which should be reasonably unique to this player's quake installation."), 0, "\"guid\""}, + {"INFOKEY_P_CERT_SHA1", "const string", QW|NQ, D("Obtains the client's (d)tls certificate's fingerprint."), 0, "\"*cert_sha1\""}, + {"INFOKEY_P_CERT_DN", "const string", QW|NQ, D("Obtains the client's (d)tls certificate's Distinguished Name string."), 0, "\"*cert_dn\""}, {"INFOKEY_P_CHALLENGE", "const string", QW|NQ, NULL, 0, "\"challenge\""}, {"INFOKEY_P_USERID", "const string", QW|NQ, NULL, 0, "\"*userid\""}, {"INFOKEY_P_DOWNLOADPCT","const string",QW|NQ, D("The client's download percentage for the current file. Additional files are not known."), 0, "\"download\""}, diff --git a/engine/server/server.h b/engine/server/server.h index a43045794..6c930f069 100644 --- a/engine/server/server.h +++ b/engine/server/server.h @@ -1649,7 +1649,7 @@ typedef struct { qboolean hasauthed; qboolean isreverse; - char challenge[64]; + char challenge[64]; //aka nonce } qtvpendingstate_t; int SV_MVD_GotQTVRequest(vfsfile_t *clientstream, char *headerstart, char *headerend, qtvpendingstate_t *p); #endif diff --git a/engine/server/sv_ccmds.c b/engine/server/sv_ccmds.c index 596981282..47f419fe7 100644 --- a/engine/server/sv_ccmds.c +++ b/engine/server/sv_ccmds.c @@ -2139,7 +2139,7 @@ static void SV_Status_f (void) int i; client_t *cl; float cpu; - char *s, *p; + char *s, *p, *sec; char adr[MAX_ADR_SIZE]; float pi, po, bi, bo; @@ -2209,6 +2209,10 @@ static void SV_Status_f (void) else s = "private"; Con_TPrintf("public : %s\n", s); + +#ifdef HAVE_DTLS + Con_TPrintf("fingerprint : "S_COLOR_GRAY"%s\n", InfoBuf_ValueForKey(&svs.info, "*fp")); +#endif switch(svs.gametype) { #ifdef Q3SERVER @@ -2223,38 +2227,38 @@ static void SV_Status_f (void) #endif default: - Con_TPrintf("client types :%s", sv_listen_qw.ival?" QW":""); + Con_TPrintf("client types :%s", sv_listen_qw.ival?" ^[QW\\tip\\This is "FULLENGINENAME"'s standard protocol.^]":""); #ifdef NQPROT - Con_TPrintf("%s%s", (sv_listen_nq.ival==2)?" -NQ":(sv_listen_nq.ival?" NQ":""), sv_listen_dp.ival?" DP":""); + Con_TPrintf("%s%s", (sv_listen_nq.ival==2)?" ^[-NQ\\tip\\Allows 'Net'/'Normal' Quake clients to connect, with cookies and extensions that might confuse some old clients^]":(sv_listen_nq.ival?" ^[NQ\\tip\\Vanilla/Normal Quake protocol with maximum compatibility^]":""), sv_listen_dp.ival?" ^[DP\\tip\\Explicitly recognise connection requests from DP clients.^]":""); #endif #ifdef QWOVERQ3 if (sv_listen_q3.ival) Con_Printf(" Q3"); #endif #ifdef HAVE_DTLS if (net_enable_dtls.ival >= 3) - Con_Printf(" DTLS-only"); + Con_Printf(" ^[DTLS-only\\tip\\Insecure clients (those without support for DTLS) will be barred from connecting.^]"); else if (net_enable_dtls.ival) - Con_Printf(" DTLS"); + Con_Printf(" ^[DTLS\\tip\\Clients may optionally connect via DTLS for added security^]"); #endif Con_Printf("\n"); #if defined(TCPCONNECT) && !defined(CLIENTONLY) Con_TPrintf("tcp services :"); #if defined(HAVE_SSL) if (net_enable_tls.ival) - Con_Printf(" TLS"); + Con_Printf(" ^[TLS\\tip\\Clients are able to connect with Transport Layer Security for the other services, allowing for the use of tls://, wss:// or https:// schemes when their underlaying protocol is enabled.^]"); #endif #ifdef HAVE_HTTPSV if (net_enable_http.ival) - Con_Printf(" HTTP"); + Con_Printf(" ^[HTTP\\tip\\This server also acts as a web server. This might be useful to allow hosting demos or stats.^]"); if (net_enable_rtcbroker.ival) - Con_Printf(" RTC"); + Con_Printf(" ^[RTC\\tip\\This server is set up to act as a webrtc broker, allowing clients+servers to locate each other instead of playing on this server.^]"); if (net_enable_websockets.ival) - Con_Printf(" WebSocket"); + Con_Printf(" ^[WebSocket\\tip\\Clients can use the ws:// or possibly wss:// schemes to connect to this server, potentially from browser ports. This may be laggy.^]"); #endif if (net_enable_qizmo.ival) - Con_Printf(" Qizmo"); + Con_Printf(" ^[Qizmo\\tip\\Compatible with the tcp connection feature of qizmo, equivelent to 'connect tcp://ip:port' in FTE.^]"); if (net_enable_qtv.ival) - Con_Printf(" QTV"); + Con_Printf(" ^[QTV\\tip\\Allows receiving streamed mvd data from this server.^]"); Con_Printf("\n"); #endif break; @@ -2269,7 +2273,7 @@ static void SV_Status_f (void) Con_TPrintf("map uptime : %s\n", ShowTime(sv.world.physicstime)); //show the current map+name (but hide name if its too long or would be ugly) if (columns >= 80 && *sv.mapname && strlen(sv.mapname) < 45 && !strchr(sv.mapname, '\n')) - Con_TPrintf ("current map : %s (%s)\n", svs.name, sv.mapname); + Con_TPrintf ("current map : %s "S_COLOR_GRAY"(%s)\n", svs.name, sv.mapname); else Con_TPrintf ("current map : %s\n", svs.name); @@ -2367,7 +2371,7 @@ static void SV_Status_f (void) #define COLUMNS C_FRAGS C_USERID C_ADDRESS C_NAME C_RATE C_PING C_DROP C_DLP C_DLS C_PROT C_ADDRESS2 #define C_FRAGS COLUMN(0, "frags", if (cl->spectator==1)Con_Printf("%-5s ", "spec"); else Con_Printf("%5i ", (int)cl->old_frags)) #define C_USERID COLUMN(1, "userid", Con_Printf("%6i ", (int)cl->userid)) -#define C_ADDRESS COLUMN(2, "address ", Con_Printf("%-16.16s", s)) +#define C_ADDRESS COLUMN(2, "address ", Con_Printf("%s%-16.16s", sec, s)) #define C_NAME COLUMN(3, "name ", Con_Printf("%-16.16s", cl->name)) #define C_RATE COLUMN(4, " hz", Con_Printf("%4i ", (cl->frameunion.frames&&cl->netchan.frame_rate>0)?(int)(0.5f+1/cl->netchan.frame_rate):0)) #define C_PING COLUMN(5, "ping", Con_Printf("%4i ", (int)SV_CalcPing (cl, false))) @@ -2437,6 +2441,13 @@ static void SV_Status_f (void) else s = NET_BaseAdrToString (adr, sizeof(adr), &cl->netchan.remote_address); + if (NET_IsLoopBackAddress(&cl->netchan.remote_address)) + sec = ""; + else if (NET_IsEncrypted(&cl->netchan.remote_address)) + sec = S_COLOR_GREEN; + else + sec = S_COLOR_RED; + safeswitch(cl->protocol) { case SCP_BAD: p = "-----"; break; @@ -2728,7 +2739,9 @@ void SV_User_f (void) client_t *cl; int clnum=-1; unsigned int u; - char buf[256]; + char buf[8192]; + qbyte digest[DIGEST_MAXSIZE]; + int certsize; extern cvar_t sv_userinfo_bytelimit, sv_userinfo_keylimit; static const char *pext1names[32] = { "setview", "scale", "lightstylecol", "trans", "view2", "builletens", "accuratetimings", "sounddbl", "fatness", "hlbsp", "bullet", "hullsize", "modeldbl", "entitydbl", "entitydbl2", "floatcoords", @@ -2813,7 +2826,16 @@ void SV_User_f (void) Con_Printf("\n"); } - Con_Printf("ip: %s\n", NET_AdrToString(buf, sizeof(buf), &cl->netchan.remote_address)); + Con_Printf("ip: %s%s\n", NET_IsEncrypted(&cl->netchan.remote_address)?S_COLOR_GREEN:S_COLOR_RED, NET_AdrToString(buf, sizeof(buf), &cl->netchan.remote_address)); + certsize = NET_GetConnectionCertificate(svs.sockets, &cl->netchan.remote_address, QCERT_PEERCERTIFICATE, buf, sizeof(buf)); + if (certsize <= 0) + strcpy(buf, ""); + else + Base64_EncodeBlockURI(digest,CalcHash(&hash_sha1, digest, sizeof(digest), buf, certsize), buf, sizeof(buf)); + Con_Printf("fp: %s\n", buf); + if (NET_GetConnectionCertificate(svs.sockets, &cl->netchan.remote_address, QCERT_PEERSUBJECT, buf, sizeof(buf)) < 0) + strcpy(buf, ""); + Con_Printf("dn: %s\n", buf); switch(cl->realip_status) { case 1: diff --git a/engine/server/sv_main.c b/engine/server/sv_main.c index de95c5685..c4fbd92c4 100644 --- a/engine/server/sv_main.c +++ b/engine/server/sv_main.c @@ -1127,19 +1127,67 @@ CONNECTIONLESS COMMANDS ============================================================================== */ +const char *SV_ProtocolNameForClient(client_t *cl) +{ + //okay, that failed... + safeswitch (cl->protocol) + { + case SCP_QUAKEWORLD: + if (cl->fteprotocolextensions2 & PEXT2_REPLACEMENTDELTAS) + return "fteqw"; //changes enough to be significant. assumed to include csqc. + return "quakeworld"; + case SCP_BAD: + return "bot"; + case SCP_QUAKE2: + return "quake2"; + case SCP_QUAKE3: + return "quake3"; + case SCP_NETQUAKE: + if (cl->fteprotocolextensions2 & PEXT2_REPLACEMENTDELTAS) + return "ftenq"; //changes enough to be significant. assumed to include csqc. + if (cl->qex) + return "qex"; + if (cl->proquake_angles_hack) + return "proquake"; + return "vanilla"; + case SCP_BJP3: + return "bjp3"; + case SCP_FITZ666: + //this gets messy... probably we should distinguish more + if (cl->fteprotocolextensions2 & PEXT2_REPLACEMENTDELTAS) + return "ftenq"; //changes enough to be significant. assumed to include csqc. + if (cl->qex) + return "qex"; + if (cl->netchan.netprim.coordtype != COORDTYPE_FIXED_13_3 || cl->netchan.netprim.anglesize != 1) + return "rmq"; //while fte tends not to care, most people consider them separate. + return "fitz"; + case SCP_DARKPLACES6: + return "dp6"; + case SCP_DARKPLACES7: + return "dp7"; + safedefault: + return "unknown"; + } +} + char *SV_PlayerPublicAddress(client_t *cl) { //returns a string containing the client's IP address, as permitted for viewing by other clients. //if something useful is actually returned, it should be masked. - return "private"; + //we hide it entirely out of private info caution. most nq clients expect a #.#.#.INVALID type address. + //it should be fine to put other stuff here though, we put client version instead, if we know it. + const char *ver = InfoBuf_ValueForKey(&cl->userinfo, "*ver"); + const char *prot = SV_ProtocolNameForClient(cl); + + return va("prot %s %s", prot, ver); //something so they can't confuse ip parsing so easily nor pass them off as some other protocol. } -#define STATUS_OLDSTYLE 0 +#define STATUS_OLDSTYLE 0 //equivelent to STATUS_SERVERINFO|STATUS_PLAYERS #define STATUS_SERVERINFO 1 #define STATUS_PLAYERS 2 #define STATUS_SPECTATORS 4 #define STATUS_SPECTATORS_AS_PLAYERS 8 //for ASE - change only frags: show as "S" -#define STATUS_SHOWTEAMS 16 -#define STATUS_QTVLIST 32 //qtv destid "name" "streamid@host:port" numviewers +#define STATUS_SHOWTEAMS 16 +#define STATUS_QTVLIST 32 //qtv destid "name" "streamid@host:port" numviewers /* ================ @@ -1768,14 +1816,16 @@ qboolean SVC_GetChallenge (qboolean respond_dp) #endif #ifdef HAVE_DTLS - if (net_enable_dtls.ival/* || !*net_enable_dtls.string*/) + if (net_enable_dtls.ival>0/* || !*net_enable_dtls.string*/ && svs.sockets->dtlsfuncs) { lng = LittleLong(PROTOCOL_VERSION_DTLSUPGRADE); memcpy(over, &lng, sizeof(lng)); over+=sizeof(lng); - if (net_enable_dtls.ival >= 2) - lng = LittleLong(2); //required + if (net_enable_dtls.ival >= 3) + lng = LittleLong(3); //required + else if (net_enable_dtls.ival >= 2) + lng = LittleLong(2); //encouraged else lng = LittleLong(1); //supported memcpy(over, &lng, sizeof(lng)); @@ -2606,7 +2656,8 @@ void SV_DoDirectConnect(svconnectinfo_t *fte_restrict info) { if (spectator_password.string[0] && stricmp(spectator_password.string, "none") && - strcmp(spectator_password.string, s) ) + strcmp(spectator_password.string, s) && + !NET_IsLoopBackAddress(&info->adr)) { // failed Con_TPrintf ("%s:spectator password failed\n", NET_AdrToString (adrbuf, sizeof(adrbuf), &info->adr)); SV_RejectMessage (info->protocol, "requires a spectator password\n\n"); @@ -2621,7 +2672,8 @@ void SV_DoDirectConnect(svconnectinfo_t *fte_restrict info) s = Info_ValueForKey (info->userinfo, "password"); if (password.string[0] && stricmp(password.string, "none") && - strcmp(password.string, s) ) + strcmp(password.string, s) && + !NET_IsLoopBackAddress(&info->adr)) { Con_TPrintf ("%s:password failed\n", NET_AdrToString (adrbuf, sizeof(adrbuf), &info->adr)); SV_RejectMessage (info->protocol, "server requires a password\n\n"); @@ -3056,10 +3108,15 @@ void SV_DoDirectConnect(svconnectinfo_t *fte_restrict info) //this is the upper bound of the mtu, if its too high we'll get EMSGSIZE and we'll reduce it. //however, if it drops below newcl->netchan.message.maxsize then we'll start to see undeliverable reliables, which means dropped clients. newcl->netchan.mtu = MAX_DATAGRAM; //vanilla qw clients are assumed to have an mtu of this size. - if (info->mtu >= 64) + if (info->mtu >= 300) //anything smaller is someone being intentionally malicious. { //if we support application fragmenting, then we can send massive reliables without too much issue newcl->netchan.mtu = info->mtu; newcl->netchan.message.maxsize = sizeof(newcl->netchan.message_buf); + + if (info->adr.type == NA_ICE) + newcl->netchan.mtu -= 48+12; //dtls+sctp overhead + else if (info->adr.prot == NP_DTLS || info->adr.prot == NP_TLS) + newcl->netchan.mtu -= 48; //dtls overhead } else { //otherwise we can't fragment the packets, and the only way to honour the mtu is to send less data. yay for more round-trips. @@ -3665,9 +3722,6 @@ void SVC_DirectConnect(int expectedreliablesequence) } msg_badread=false; - if (!*info.guid) - NET_GetConnectionCertificate(svs.sockets, &net_from, QCERT_PEERFINGERPRINT, info.guid, sizeof(info.guid)); - info.adr = net_from; if (MSV_ClusterLogin(&info)) return; @@ -4500,16 +4554,18 @@ qboolean SVNQ_ConnectionlessPacket(void) /*dual-stack client, supporting either DP or QW protocols*/ SVC_GetChallenge (false); } +#ifdef HAVE_DTLS + else if (net_enable_dtls.ival > 2 && (net_from.prot == NP_DGRAM || net_from.prot == NP_STREAM || net_from.prot == NP_WS) && net_from.type != NA_LOOPBACK && !NET_IsEncrypted(&net_from)) + { + SV_RejectMessage (SCP_NETQUAKE, "This server requires the use of DTLS/TLS/WSS.\n"); + return true; + } +#endif else { //legacy pure-nq (though often DP). if (progstype == PROG_H2) { - SZ_Clear(&sb); - MSG_WriteLong(&sb, 0); - MSG_WriteByte(&sb, CCREP_REJECT); - MSG_WriteString(&sb, "NQ clients are not supported with hexen2 gamecode\n"); - *(int*)sb.data = BigLong(NETFLAG_CTL+sb.cursize); - NET_SendPacket(svs.sockets, sb.cursize, sb.data, &net_from); + SV_RejectMessage (SCP_NETQUAKE, "NQ clients are not supported with hexen2 gamecode\n"); return true; //not our version... } if (NET_WasSpecialPacket(svs.sockets)) diff --git a/plugins/net_ssl_openssl.c b/plugins/net_ssl_openssl.c index 0db6c6094..6c592c126 100644 --- a/plugins/net_ssl_openssl.c +++ b/plugins/net_ssl_openssl.c @@ -21,6 +21,7 @@ struct fte_certctx_s { const char *peername; qboolean dtls; + qboolean failure; hashfunc_t *hash; //if set peer's cert MUST match the specified digest (with this hash function) qbyte digest[DIGEST_MAXSIZE]; @@ -44,6 +45,8 @@ static int OSSL_Bio_FWrite(BIO *h, const char *buf, int size) BIO_set_retry_write(h); r = -1; //paranoia } +// else if (r < 0) +// Con_DPrintf("ossl Error: %i\n", r); return r; } static int OSSL_Bio_FRead(BIO *h, char *buf, int size) @@ -242,7 +245,12 @@ static int OSSL_Verify_Peer(int preverify_ok, X509_STORE_CTX *x509_ctx) uctx->hash->terminate(digest, hctx); //return 1 for success - return !memcmp(digest, uctx->digest, uctx->hash->digestsize); + if (memcmp(digest, uctx->digest, uctx->hash->digestsize)) + { + uctx->failure = true; + return 0; + } + return 1; } if(preverify_ok == 0) @@ -914,7 +922,22 @@ static void OSSL_DestroyContext(void *ctx) static neterr_t OSSL_Transmit(void *ctx, const qbyte *data, size_t datasize) { //we're sending data ossldtls_t *o = (ossldtls_t*)ctx; - int r = BIO_write(o->bio, data, datasize); + int r; + if (datasize == 0) //liveness test. return clogged while handshaking and sent when finished. openssl doesn't like 0-byte writes. + { + if (o->cert.failure) + return NETERR_DISCONNECTED; //actual security happens elsewhere. + else if (SSL_is_init_finished(o->ssl)) + return NETERR_SENT; //go on, send stuff. + else if (BIO_should_retry(o->bio)) + { + if (BIO_do_handshake(o->bio) == 1) + return NETERR_SENT; + } + return NETERR_CLOGGED; //can't send yet. + } + else + r = BIO_write(o->bio, data, datasize); if (r <= 0) { if (BIO_should_io_special(o->bio)) @@ -941,7 +964,7 @@ static neterr_t OSSL_Received(void *ctx, sizebuf_t *message) int r; if (!message) - r = 0; + r = BIO_read(o->bio, NULL, 0); else { o->pending = message->data; @@ -981,7 +1004,52 @@ static neterr_t OSSL_Timeouts(void *ctx) return OSSL_Received(ctx, NULL); } -qboolean OSSL_GenTempCertificate(const char *subject, struct dtlslocalcred_s *cred) +static int OSSL_GetPeerCertificate(void *ctx, enum certprops_e prop, char *out, size_t outsize) +{ + ossldtls_t *o = (ossldtls_t*)ctx; + X509 *cert; + + safeswitch(prop) + { + case QCERT_ISENCRYPTED: + return 0; //not an error. + case QCERT_PEERCERTIFICATE: + cert = SSL_get_peer_certificate(o->ssl); + goto returncert; + case QCERT_LOCALCERTIFICATE: + cert = vhost.servercert; + goto returncert; +returncert: + if (cert) + { + size_t blobsize = i2d_X509(cert, NULL); + qbyte *end = out; + if (blobsize <= outsize) + i2d_X509(cert, &end); + return end-(qbyte*)out; + } + return -1; + case QCERT_PEERSUBJECT: + { + int r; + X509 *cert = SSL_get_peer_certificate(o->ssl); + if (cert) + { + X509_NAME *iname = X509_get_subject_name(cert); + BIO *bio = BIO_new(BIO_s_mem()); + X509_NAME_print_ex(bio, iname, 0, XN_FLAG_RFC2253); + r = BIO_read(bio, out, outsize-1); + out[(r<0)?0:r] = 0; + BIO_free(bio); + return r; + } + } + return -1; + safedefault: + return -1; + } +} +static qboolean OSSL_GenTempCertificate(const char *subject, struct dtlslocalcred_s *cred) { EVP_PKEY*pkey = EVP_PKEY_new(); RSA *rsa = RSA_new(); @@ -1033,7 +1101,7 @@ static dtlsfuncs_t ossl_dtlsfuncs = OSSL_Transmit, OSSL_Received, OSSL_Timeouts, - NULL, + OSSL_GetPeerCertificate, OSSL_GenTempCertificate, }; static const dtlsfuncs_t *OSSL_InitClient(void) @@ -1172,6 +1240,8 @@ static void OSSL_PluginShutdown(void) EVP_PKEY_free(vhost.privatekey); BIO_meth_free(biometh_vfs); BIO_meth_free(biometh_dtls); + + memset(&vhost, 0, sizeof(vhost)); } static qboolean OSSL_PluginMayShutdown(void) {