diff --git a/engine/client/cl_main.c b/engine/client/cl_main.c index f1fe93a43..e8d9aff4e 100644 --- a/engine/client/cl_main.c +++ b/engine/client/cl_main.c @@ -1164,6 +1164,7 @@ void CL_CheckForResend (void) switch(NET_SendPacket (cls.sockets, 0, NULL, &connectinfo.adr[0])) { case NETERR_CLOGGED: //temporary failure + connectinfo.clogged = true; return; default: break; @@ -1195,7 +1196,7 @@ void CL_CheckForResend (void) Cvar_ForceSet(&cl_servername, cls.servername); - if (!connectinfo.numadr) + if (!connectinfo.numadr || !cls.sockets) return; //nothing to do yet... if (!connectinfo.clogged) connectinfo.time = realtime+t2-t1; // for retransmit requests @@ -1220,7 +1221,7 @@ void CL_CheckForResend (void) #endif if (connectinfo.istransfer || connectinfo.numadr>1) - Con_TPrintf ("Connecting to %s(%s)...\n", cls.servername, NET_AdrToString(data, sizeof(data), to)); + Con_TPrintf ("Connecting to %s" S_COLOR_GRAY "(%s)" S_COLOR_WHITE "...\n", cls.servername, NET_AdrToString(data, sizeof(data), to)); else Con_TPrintf ("Connecting to %s...\n", cls.servername); } @@ -1365,7 +1366,7 @@ void CL_BeginServerReconnect(void) connectinfo.istransfer = false; connectinfo.time = 0; connectinfo.tries = 0; //re-ensure routes. - connectinfo.nextadr = 0; //should at least be consistent, other than packetloss. yay. :\ + connectinfo.nextadr = 0; //should at least be consistent, other than packetloss. yay. NET_InitClient(false); } @@ -3689,7 +3690,7 @@ void CL_ConnectionlessPacket (void) if (!CL_IsPendingServerAddress(&net_from)) return; - if (NET_DTLS_Create(cls.sockets, &net_from)) + if (NET_DTLS_Create(cls.sockets, &net_from, cls.servername)) { connectinfo.dtlsupgrade = DTLS_ACTIVE; connectinfo.numadr = 1; //fixate on this resolved address. diff --git a/engine/common/log.c b/engine/common/log.c index cffcd6dab..8369f05f0 100644 --- a/engine/common/log.c +++ b/engine/common/log.c @@ -29,7 +29,7 @@ cvar_t log_dosformat = CVARF("log_dosformat", "0", CVAR_NOTFROMSERVER); qboolean log_newline[LOG_TYPES]; #ifdef IPLOG -cvar_t iplog_autodump = CVARFD("ipautodump", "1", CVAR_NOTFROMSERVER, "Enables dumping the 'iplog.txt' file, which contains a log of usernames seen for a given IP, which is useful for detecting fake-nicks."); +cvar_t iplog_autodump = CVARFD("ipautodump", "0", CVAR_ARCHIVE|CVAR_NOTFROMSERVER, "Enables dumping the 'iplog.txt' file, which contains a log of usernames seen for a given IP, which is useful for detecting fake-nicks."); #endif static char log_dir[MAX_OSPATH]; @@ -637,6 +637,8 @@ struct certlog_s { link_t l; char *hostname; + qboolean trusted; //when true, the user has given explicit trust + //when false we buldozed straight through and will only complain when it changes (legacy mode). size_t certsize; qbyte cert[1]; }; @@ -654,7 +656,7 @@ static struct certlog_s *CertLog_Find(const char *hostname) } return NULL; } -static void CertLog_Update(const char *hostname, const void *cert, size_t certsize) +static void CertLog_Update(const char *hostname, const void *cert, size_t certsize, qboolean trusted) { struct certlog_s *l = CertLog_Find(hostname); if (l) @@ -675,7 +677,7 @@ static void CertLog_Write(void) vfsfile_t *f = FS_OpenVFS(CERTLOG_FILENAME, "wb", FS_ROOT); if (f) { - VFS_PRINTF(f, "version 1.0\n"); + VFS_PRINTF(f, "version 1.1\n"); for (l=(struct certlog_s*)certlog.next ; l != (struct certlog_s*)&certlog ; l = (struct certlog_s*)l->l.next) { @@ -690,7 +692,7 @@ static void CertLog_Write(void) certhex[i*2] = 0; VFS_PRINTF(f, "%s \"", l->hostname); VFS_PUTS(f, certhex); - VFS_PRINTF(f, "\"\n"); + VFS_PRINTF(f, "\" %i\n", l->trusted?true:false); } } } @@ -720,6 +722,7 @@ static void CertLog_Import(const char *filename) char addressstring[512]; char certhex[32768]; char certdata[16384]; + char trusted[16]; char line[65536], *l; size_t i, certsize; vfsfile_t *f; @@ -738,6 +741,7 @@ static void CertLog_Import(const char *filename) l = line; l = COM_ParseOut(l, addressstring, sizeof(addressstring)); l = COM_ParseOut(l, certhex, sizeof(certhex)); + l = COM_ParseOut(l, trusted, sizeof(trusted)); certsize = 0; for (i = 0; certsize < sizeof(certdata); i++) @@ -746,7 +750,7 @@ static void CertLog_Import(const char *filename) break; certdata[certsize++] = (hexdecode(certhex[(i<<1)+0])<<4)|hexdecode(certhex[(i<<1)+1]); } - CertLog_Update(addressstring, certdata, certsize); + CertLog_Update(addressstring, certdata, certsize, atoi(trusted)); } } static void CertLog_UntrustAll_f(void) @@ -775,7 +779,7 @@ static void CertLog_Add_Prompted(void *vctx, promptbutton_t button) struct certprompt_s *ctx = vctx; if (button == PROMPT_YES) //button_yes / button_left { - CertLog_Update(ctx->hostname, ctx->cert, ctx->certsize); + CertLog_Update(ctx->hostname, ctx->cert, ctx->certsize, true); CertLog_Write(); CL_BeginServerReconnect(); @@ -786,8 +790,10 @@ static void CertLog_Add_Prompted(void *vctx, promptbutton_t button) certlog_curprompt = NULL; } qboolean CertLog_ConnectOkay(const char *hostname, void *cert, size_t certsize, unsigned int certlogproblems) -{ +{ //this is specifically for dtls certs. + extern cvar_t net_enable_dtls; struct certlog_s *l; + qboolean trusted = (net_enable_dtls.ival >= 2); if (certlog_curprompt) return false; @@ -797,7 +803,14 @@ qboolean CertLog_ConnectOkay(const char *hostname, void *cert, size_t certsize, CertLog_Import(NULL); l = CertLog_Find(hostname); - if (!l || l->certsize != certsize || memcmp(l->cert, cert, certsize)) + if (!l && !trusted) + { //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); + 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 (qrenderer) { @@ -839,6 +852,8 @@ qboolean CertLog_ConnectOkay(const char *hostname, void *cert, size_t certsize, } return false; //can't connect yet... } + else if (!l->trusted) + Con_Printf(CON_WARNING"Server certificate for %s was previously auto-pinned."CON_DEFAULT" ^[/seta %s 2^]+ for actual security.\n", hostname, net_enable_dtls.name); return true; } #endif diff --git a/engine/common/net.h b/engine/common/net.h index 8315de5f9..05c6fcb0f 100644 --- a/engine/common/net.h +++ b/engine/common/net.h @@ -187,10 +187,11 @@ enum certprops_e size_t NET_GetConnectionCertificate(struct ftenet_connections_s *col, netadr_t *a, enum certprops_e prop, char *out, size_t outsize); #ifdef HAVE_DTLS -qboolean NET_DTLS_Create(struct ftenet_connections_s *col, netadr_t *to); +qboolean NET_DTLS_Create(struct ftenet_connections_s *col, netadr_t *to, const char *hostname); qboolean NET_DTLS_Decode(struct ftenet_connections_s *col); qboolean NET_DTLS_Disconnect(struct ftenet_connections_s *col, netadr_t *to); void NET_DTLS_Timeouts(struct ftenet_connections_s *col); +extern cvar_t dtls_psk_hint, dtls_psk_user, dtls_psk_key; #endif extern cvar_t timeout; extern cvar_t tls_ignorecertificateerrors; //evil evil evil. diff --git a/engine/common/net_ssl_gnutls.c b/engine/common/net_ssl_gnutls.c index 3275d302d..2d71a94b2 100644 --- a/engine/common/net_ssl_gnutls.c +++ b/engine/common/net_ssl_gnutls.c @@ -154,6 +154,8 @@ static int (VARGS *qgnutls_credentials_set)(gnutls_session_t, gnutls_credentials static int (VARGS *qgnutls_init)(gnutls_session_t * session, gnutls_connection_end_t con_end); static void (VARGS *qgnutls_deinit)(gnutls_session_t session); static int (VARGS *qgnutls_set_default_priority)(gnutls_session_t session); +static int (VARGS *qgnutls_set_default_priority_append)(gnutls_session_t session, const char *add_prio, const char **err_pos, unsigned flags); + static int (VARGS *qgnutls_certificate_allocate_credentials)(gnutls_certificate_credentials_t *sc); static int (VARGS *qgnutls_anon_allocate_client_credentials)(gnutls_anon_client_credentials_t *sc); static int (VARGS *qgnutls_global_init)(void); @@ -193,8 +195,12 @@ static int (VARGS *qgnutls_dtls_cookie_send)(gnutls_datum_t * key, void *client_ static void (VARGS *qgnutls_dtls_prestate_set)(gnutls_session_t session, gnutls_dtls_prestate_st * prestate); static void (VARGS *qgnutls_dtls_set_mtu)(gnutls_session_t session, unsigned int mtu); -//static int (VARGS *qgnutls_psk_allocate_client_credentials)(gnutls_psk_client_credentials_t *sc); -//static int (VARGS *qgnutls_psk_set_client_credentials)(gnutls_psk_client_credentials_t res, const char *username, const gnutls_datum_t * key, gnutls_psk_key_flags flags); +static int (VARGS *qgnutls_psk_allocate_server_credentials)(gnutls_psk_server_credentials_t *sc); +static void (VARGS *qgnutls_psk_set_server_credentials_function)(gnutls_psk_server_credentials_t cred, gnutls_psk_server_credentials_function *func); +static int (VARGS *qgnutls_psk_set_server_credentials_hint)(gnutls_psk_server_credentials_t res, const char *hint); +static const char *(VARGS *qgnutls_psk_client_get_hint)(gnutls_session_t session); +static int (VARGS *qgnutls_psk_allocate_client_credentials)(gnutls_psk_client_credentials_t *sc); +static void (VARGS *qgnutls_psk_set_client_credentials_function)(gnutls_psk_client_credentials_t cred, gnutls_psk_client_credentials_function *func); #endif static unsigned int (VARGS *qgnutls_sec_param_to_pk_bits)(gnutls_pk_algorithm_t algo, gnutls_sec_param_t param); @@ -254,9 +260,12 @@ static qboolean Init_GNUTLS(void) GNUTLS_FUNC(gnutls_dtls_cookie_verify) \ GNUTLS_FUNC(gnutls_dtls_cookie_send) \ GNUTLS_FUNC(gnutls_dtls_prestate_set) \ - GNUTLS_FUNC(gnutls_dtls_set_mtu) -// GNUTLS_FUNC(gnutls_psk_allocate_client_credentials) -// GNUTLS_FUNC(gnutls_psk_set_client_credentials) + GNUTLS_FUNC(gnutls_dtls_set_mtu) \ + GNUTLS_FUNC(gnutls_psk_allocate_server_credentials) \ + GNUTLS_FUNC(gnutls_psk_set_server_credentials_function) \ + GNUTLS_FUNC(gnutls_psk_set_server_credentials_hint) \ + GNUTLS_FUNC(gnutls_psk_allocate_client_credentials) \ + GNUTLS_FUNC(gnutls_psk_set_client_credentials_function) #else #define GNUTLS_DTLS_STUFF #endif @@ -288,6 +297,8 @@ static qboolean Init_GNUTLS(void) #define GNUTLS_FUNCS \ GNUTLS_FUNC(gnutls_bye) \ + GNUTLS_FUNC(gnutls_alert_get) \ + GNUTLS_FUNC(gnutls_alert_get_name) \ GNUTLS_FUNC(gnutls_perror) \ GNUTLS_FUNC(gnutls_handshake) \ GNUTLS_FUNC(gnutls_transport_set_ptr) \ @@ -340,6 +351,7 @@ static qboolean Init_GNUTLS(void) {(void**)&qgnutls_init, "gnutls_init"}, {(void**)&qgnutls_deinit, "gnutls_deinit"}, {(void**)&qgnutls_set_default_priority, "gnutls_set_default_priority"}, + {(void**)&qgnutls_set_default_priority_append, "gnutls_set_default_priority_append"}, {(void**)&qgnutls_certificate_allocate_credentials, "gnutls_certificate_allocate_credentials"}, {(void**)&qgnutls_anon_allocate_client_credentials, "gnutls_anon_allocate_client_credentials"}, {(void**)&qgnutls_global_init, "gnutls_global_init"}, @@ -378,8 +390,13 @@ static qboolean Init_GNUTLS(void) {(void**)&qgnutls_dtls_cookie_send, "gnutls_dtls_cookie_send"}, {(void**)&qgnutls_dtls_prestate_set, "gnutls_dtls_prestate_set"}, {(void**)&qgnutls_dtls_set_mtu, "gnutls_dtls_set_mtu"}, -// {(void**)&qgnutls_psk_allocate_client_credentials, "gnutls_psk_allocate_client_credentials"}, -// {(void**)&qgnutls_psk_set_client_credentials, "gnutls_psk_set_client_credentials"}, + + {(void**)&qgnutls_psk_allocate_server_credentials, "gnutls_psk_allocate_server_credentials"}, + {(void**)&qgnutls_psk_set_server_credentials_function, "gnutls_psk_set_server_credentials_function"}, + {(void**)&qgnutls_psk_set_server_credentials_hint, "gnutls_psk_set_server_credentials_hint"}, + {(void**)&qgnutls_psk_client_get_hint, "gnutls_psk_client_get_hint"}, + {(void**)&qgnutls_psk_allocate_client_credentials, "gnutls_psk_allocate_client_credentials"}, + {(void**)&qgnutls_psk_set_client_credentials_function, "gnutls_psk_set_client_credentials_function"}, #endif {(void**)&qgnutls_sec_param_to_pk_bits, "gnutls_sec_param_to_pk_bits"}, @@ -459,6 +476,65 @@ typedef struct // int mtu; } gnutlsfile_t; +static void SSL_SetCertificateName(gnutlsfile_t *f, const char *hostname) +{ + int i; + if (hostname) + { + const char *host = strstr(hostname, "://"); + if (host) + hostname = host+3; + //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 && host-hostname < sizeof(f->certname)) + { + memcpy(f->certname, hostname, host-hostname); + f->certname[host-hostname] = 0; + hostname = f->certname; + } + } + else + { //eg: 127.0.0.1:port - strip the port number if specified. + host = strchr(hostname, ':'); + if (host && host-hostname < sizeof(f->certname)) + { + memcpy(f->certname, hostname, host-hostname); + f->certname[host-hostname] = 0; + hostname = f->certname; + } + } + for (i = 0; hostname[i]; i++) + { + if (hostname[i] >= 'a' && hostname[i] <= 'z') + ; + else if (hostname[i] >= 'A' && hostname[i] <= 'Z') + ; + else if (hostname[i] >= '0' && hostname[i] <= '9') + ; + else if (hostname[i] == '-' || hostname[i] == '.') + ; + else + { + hostname = NULL; //something invalid. bum. + break; + } + } + //we should have a cleaned up host name now, ready for (ab)use in certificates. + } + + if (!hostname) + *f->certname = 0; + else if (hostname == f->certname) + ; + else if (strlen(hostname) >= sizeof(f->certname)) + *f->certname = 0; + else + memcpy(f->certname, hostname, strlen(hostname)+1); +} + #define CAFILE "/etc/ssl/certs/ca-certificates.crt" static void SSL_Close(vfsfile_t *vfs) @@ -703,6 +779,7 @@ static int SSL_DoHandshake(gnutlsfile_t *file) { case GNUTLS_E_INSUFFICIENT_CREDENTIALS: case GNUTLS_E_CERTIFICATE_ERROR: err = VFS_ERROR_UNTRUSTED; break; + case GNUTLS_E_SESSION_EOF: case GNUTLS_E_PREMATURE_TERMINATION: err = VFS_ERROR_EOF; break; case GNUTLS_E_PUSH_ERROR: err = file->pusherror; break; case GNUTLS_E_PULL_ERROR: err = file->pullerror; break; @@ -920,6 +997,7 @@ static int DTLS_Pull_Timeout(gnutls_transport_ptr_t p, unsigned int timeout) static gnutls_anon_client_credentials_t anoncred[2]; #else static gnutls_certificate_credentials_t xcred[2]; +static qboolean servercertfail; #endif #ifdef HAVE_DTLS static gnutls_datum_t cookie_key; @@ -987,7 +1065,7 @@ static qboolean SSL_LoadPrivateCert(gnutls_certificate_credentials_t cred) memset(&priv, 0, sizeof(priv)); memset(&pub, 0, sizeof(pub)); - if ((!privf || !pubf) && hostname) + if ((!privf || !pubf))// && hostname) { //not found? generate a new one. //FIXME: how to deal with race conditions with multiple servers on the same host? //delay till the first connection? we at least write both files at the sameish time. @@ -1170,7 +1248,7 @@ qboolean SSL_InitGlobal(qboolean isserver) { #if 1 if (!SSL_LoadPrivateCert(xcred[isserver])) - initstatus[isserver] = -1; + servercertfail = true; #else int ret = -1; char keyfile[MAX_OSPATH]; @@ -1202,12 +1280,39 @@ qboolean SSL_InitGlobal(qboolean isserver) return false; return true; } -#if 0 +#ifdef HAVE_DTLS static int GetPSKForUser(gnutls_session_t sess, const char *username, gnutls_datum_t * key) -{ -Con_Printf("GetPSKForUser: %s\n", username); - key->size = 0; - key->data = key->size?gnutls_malloc(key->size):0; +{ //serverside. name must match what we expect (this isn't very secure), and we return the key we require for that user name. + if (!strcmp(username, dtls_psk_user.string)) + { + key->size = (strlen(dtls_psk_key.string)+1)/2; + key->data = (*qgnutls_malloc)(key->size); + key->size = Base16_DecodeBlock(dtls_psk_key.string, key->data, key->size); + return 0; + } + return -1; +} +static int GetPSKForServer(gnutls_session_t sess, char **username, gnutls_datum_t *key) +{ //clientside. return the appropriate username for the hint, along with the matching key. + //this could be made more fancy with a database, but we'll keep it simple with cvars. + const char *svhint = qgnutls_psk_client_get_hint(sess); + + if (!svhint) + svhint = ""; + + if ((!*dtls_psk_hint.string&&*dtls_psk_user.string) || (*dtls_psk_hint.string&&!strcmp(svhint, dtls_psk_hint.string))) + { //okay, hints match (or ours is unset), report our user as appropriate. + *username = strcpy((*qgnutls_malloc)(strlen(dtls_psk_user.string)+1), dtls_psk_user.string); + + key->size = (strlen(dtls_psk_key.string)+1)/2; + key->data = (*qgnutls_malloc)(key->size); + key->size = Base16_DecodeBlock(dtls_psk_key.string, key->data, key->size); + return 0; + } + else if (!*dtls_psk_user.string && !*dtls_psk_hint.string) + Con_Printf(CON_ERROR"Server requires a Pre-Shared Key (hint: \"%s\"). Please set %s, %s, and %s accordingly.\n", svhint, dtls_psk_hint.name, dtls_psk_user.name, dtls_psk_key.name); + else + Con_Printf(CON_ERROR"Server requires different Pre-Shared Key credentials (hint: \"%s\", expected \"%s\"). Please set %s, %s, and %s accordingly.\n", svhint, dtls_psk_hint.string, dtls_psk_hint.name, dtls_psk_user.name, dtls_psk_key.name); return -1; } #endif @@ -1224,39 +1329,36 @@ static qboolean SSL_InitConnection(gnutlsfile_t *newf, qboolean isserver, qboole //qgnutls_kx_set_priority (newf->session, kx_prio); qgnutls_credentials_set (newf->session, GNUTLS_CRD_ANON, anoncred[isserver]); #else -#if 0//def HAVE_DTLS - if (datagram) - { //use some arbitrary PSK for dtls clients. - if (isserver) - { - gnutls_psk_server_credentials_t pskcred; - qgnutls_psk_allocate_server_credentials(&pskcred); - qgnutls_psk_set_server_credentials_function(pskcred, GetPSKForUser); - qgnutls_psk_set_server_credentials_hint(pskcred, "id-quake-ex-dtls"); - qgnutls_credentials_set(newf->session, GNUTLS_CRD_PSK, pskcred); - } - else - { -#ifdef HAVE_CLIENT - extern cvar_t name; - const char *namestr = name.string; -#else - const char *namestr = "Anonymous"; -#endif - gnutls_psk_client_credentials_t pskcred; - const gnutls_datum_t key = { (void *) "deadbeef", 0 }; - qgnutls_psk_allocate_client_credentials(&pskcred); - qgnutls_psk_set_client_credentials(pskcred, namestr, &key, GNUTLS_PSK_KEY_HEX); +#ifdef HAVE_DTLS + 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; + qgnutls_psk_allocate_client_credentials(&pskcred); + qgnutls_psk_set_client_credentials_function(pskcred, GetPSKForServer); - qgnutls_credentials_set(newf->session, GNUTLS_CRD_PSK, pskcred); - } + qgnutls_set_default_priority_append (newf->session, "+ECDHE-PSK:+DHE-PSK:+PSK", NULL, 0); + qgnutls_credentials_set(newf->session, GNUTLS_CRD_PSK, pskcred); + } + else if (datagram && isserver && (*dtls_psk_user.string || servercertfail)) + { //offer some arbitrary PSK for dtls clients. + gnutls_psk_server_credentials_t pskcred; + qgnutls_psk_allocate_server_credentials(&pskcred); + qgnutls_psk_set_server_credentials_function(pskcred, GetPSKForUser); + if (*dtls_psk_hint.string) + qgnutls_psk_set_server_credentials_hint(pskcred, dtls_psk_hint.string); + + qgnutls_set_default_priority_append (newf->session, ("-KX-ALL:+ECDHE-PSK:+DHE-PSK:+PSK")+(servercertfail?0:8), NULL, 0); + qgnutls_credentials_set(newf->session, GNUTLS_CRD_PSK, pskcred); } else #endif - qgnutls_credentials_set (newf->session, GNUTLS_CRD_CERTIFICATE, xcred[isserver]); + { + // Use default priorities for regular tls sessions + qgnutls_set_default_priority (newf->session); + } #endif - // Use default priorities - qgnutls_set_default_priority (newf->session); + if (xcred[isserver]) + qgnutls_credentials_set (newf->session, GNUTLS_CRD_CERTIFICATE, xcred[isserver]); // tell gnutls how to send/receive data qgnutls_transport_set_ptr (newf->session, newf); @@ -1297,10 +1399,7 @@ static vfsfile_t *GNUTLS_OpenVFS(const char *hostname, vfsfile_t *source, qboole newf->funcs.Tell = SSL_Tell; newf->funcs.seekstyle = SS_UNSEEKABLE; - if (hostname) - Q_strncpyz(newf->certname, hostname, sizeof(newf->certname)); - else - Q_strncpyz(newf->certname, "", sizeof(newf->certname)); + SSL_SetCertificateName(newf, hostname); if (!SSL_InitConnection(newf, isserver, false)) { @@ -1433,7 +1532,7 @@ static void *GNUDTLS_CreateContext(const char *remotehost, void *cbctx, neterr_t // Sys_Printf("DTLS_CreateContext: server=%i\n", isserver); - Q_strncpyz(newf->certname, remotehost?remotehost:"", sizeof(newf->certname)); + SSL_SetCertificateName(newf, remotehost); if (!SSL_InitConnection(newf, isserver, true)) { @@ -1587,7 +1686,7 @@ static qboolean GNUDTLS_CheckConnection(void *cbctx, void *peeraddr, size_t peer //and this is the result... qgnutls_dtls_prestate_set(f->session, &prestate); - qgnutls_dtls_set_mtu(f->session, 1440); + qgnutls_dtls_set_mtu(f->session, 1400); //still need to do the whole certificate thing though. f->handshaking = true; @@ -1628,6 +1727,8 @@ static const dtlsfuncs_t *GNUDTLS_InitServer(void) { if (!SSL_InitGlobal(true)) return NULL; //unable to init a server certificate. don't allow dtls to init. + if (servercertfail && !*dtls_psk_user.string) + return NULL; return &dtlsfuncs_gnutls; } static const dtlsfuncs_t *GNUDTLS_InitClient(void) diff --git a/engine/common/net_wins.c b/engine/common/net_wins.c index 9a9f755f7..f81ee2bad 100644 --- a/engine/common/net_wins.c +++ b/engine/common/net_wins.c @@ -165,13 +165,18 @@ static void QDECL NET_Enable_DTLS_Changed(struct cvar_s *var, char *oldvalue) if (!svs.sockets->dtlsfuncs) { if (var->ival >= 2) - Con_Printf("%sUnable to set %s to \"%s\", no DTLS certificate available.\n", (var->ival >= 2)?CON_ERROR:CON_WARNING, var->name, var->string); + Con_Printf("%sUnable to set %s to \"%s\", no DTLS provider available.\n", (var->ival >= 2)?CON_ERROR:CON_WARNING, var->name, var->string); var->ival = 0; //disable the cvar (internally) if we don't have a usable certificate. this allows us to default the cvar to enabled without it breaking otherwise. } } } 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)."); #endif +#if defined(HAVE_DTLS) +cvar_t dtls_psk_hint = CVARFD("dtls_psk_hint", "", CVAR_NOUNSAFEEXPAND, "For DTLS-PSK handshakes. This specifies the public server identity."); +cvar_t dtls_psk_user = CVARFD("dtls_psk_user", "", CVAR_NOUNSAFEEXPAND, "For DTLS-PSK handshakes. This specifies the username to use when the client+server's hints match."); +cvar_t dtls_psk_key = CVARFD("dtls_psk_key", "", CVAR_NOUNSAFEEXPAND, "For DTLS-PSK handshakes. This specifies the hexadecimal key which must match between client+server. Will only be used when client+server's hint settings match."); +#endif #ifdef HAVE_CLIENT static void QDECL cl_delay_packets_Announce(cvar_t *var, char *oldval) @@ -2997,7 +3002,7 @@ static neterr_t FTENET_DTLS_DoSendPacket(void *cbctx, const qbyte *data, size_t struct dtlspeer_s *peer = cbctx; return NET_SendPacketCol(peer->col, length, data, &peer->addr); } -qboolean NET_DTLS_Create(ftenet_connections_t *col, netadr_t *to) +qboolean NET_DTLS_Create(ftenet_connections_t *col, netadr_t *to, const char *hostname) { extern cvar_t timeout; struct dtlspeer_s *peer; @@ -3010,7 +3015,6 @@ qboolean NET_DTLS_Create(ftenet_connections_t *col, netadr_t *to) } if (!peer) { - char hostname[256]; peer = Z_Malloc(sizeof(*peer)); peer->addr = *to; peer->col = col; @@ -3020,7 +3024,7 @@ qboolean NET_DTLS_Create(ftenet_connections_t *col, netadr_t *to) else peer->funcs = DTLS_InitClient(); if (peer->funcs) - peer->dtlsstate = peer->funcs->CreateContext(NET_BaseAdrToString(hostname, sizeof(hostname), to), peer, FTENET_DTLS_DoSendPacket, col->islisten); + peer->dtlsstate = peer->funcs->CreateContext(hostname, peer, FTENET_DTLS_DoSendPacket, col->islisten); peer->timeout = realtime+timeout.value; if (peer->dtlsstate) @@ -8009,14 +8013,14 @@ qboolean NET_EnsureRoute(ftenet_connections_t *collection, char *routename, char break; case NP_DTLS: adr->prot = NP_DGRAM; - NET_EnsureRoute(collection, routename, host, adr); - if (NET_DTLS_Create(collection, adr)) - { - adr->prot = NP_DTLS; - return true; - } + if (NET_EnsureRoute(collection, routename, host, adr)) + if (NET_DTLS_Create(collection, adr, host)) + { + adr->prot = NP_DTLS; + return true; + } adr->prot = NP_DTLS; - break; + return false; case NP_WS: case NP_WSS: case NP_TLS: @@ -9034,6 +9038,11 @@ void SVNET_RegisterCvars(void) #if defined(HAVE_DTLS) && defined(HAVE_SERVER) Cvar_Register (&net_enable_dtls, "networking"); #endif +#ifdef HAVE_DTLS + Cvar_Register (&dtls_psk_hint, "networking"); + Cvar_Register (&dtls_psk_user, "networking"); + Cvar_Register (&dtls_psk_key, "networking"); +#endif } void NET_CloseServer(void) diff --git a/engine/common/plugin.c b/engine/common/plugin.c index 7ab6f5fa1..852d7753b 100644 --- a/engine/common/plugin.c +++ b/engine/common/plugin.c @@ -1896,6 +1896,14 @@ static void *QDECL PlugBI_GetEngineInterface(const char *interfacename, size_t s Plug_Net_Close, Plug_Net_SetTLSClient, Plug_Net_GetTLSBinding, + + Sys_RandomBytes, + TLS_GetKnownCertificate, +#ifdef HAVE_CLIENT + CertLog_ConnectOkay, +#else + NULL, +#endif }; if (structsize == sizeof(funcs)) return &funcs; diff --git a/engine/server/sv_main.c b/engine/server/sv_main.c index d420750f0..c4f8d51c9 100644 --- a/engine/server/sv_main.c +++ b/engine/server/sv_main.c @@ -217,7 +217,6 @@ char cvargroup_servercontrol[] = "server control variables"; vfsfile_t *sv_fraglogfile; -void SV_AcceptClient (netadr_t *adr, int userid, char *userinfo); void PRH2_SetPlayerClass(client_t *cl, int classnum, qboolean fromqc); void SV_DeDupeName(const char *val, client_t *cl, char *newname, size_t newnamesize); @@ -3424,12 +3423,16 @@ void SVC_DirectConnect(int expectedreliablesequence) // note an extra qbyte is needed to replace spectator key Q_strncpyz (info.userinfo, Cmd_Argv(4), sizeof(info.userinfo)-1); - if (info.protocol == SCP_NETQUAKE) + if (info.protocol >= SCP_NETQUAKE) + { Info_RemoveKey(info.userinfo, "mod"); //its served its purpose. + Info_RemoveKey(info.userinfo, "modver"); //its served its purpose. + Info_RemoveKey(info.userinfo, "flags"); //its served its purpose. + } } #ifdef HAVE_DTLS - if (net_enable_dtls.ival > 2 && (net_from.prot == NP_DGRAM || net_from.prot == NP_STREAM || net_from.prot == NP_WS)) + 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) { SV_RejectMessage (info.protocol, "This server requires the use of DTLS/TLS/WSS.\n"); return; @@ -4118,7 +4121,7 @@ qboolean SV_ConnectionlessPacket (void) else { //NET_DTLS_Disconnect(svs.sockets, &net_from); - if (NET_DTLS_Create(svs.sockets, &net_from)) + if (NET_DTLS_Create(svs.sockets, &net_from, NULL)) Netchan_OutOfBandPrint(NS_SERVER, &net_from, "dtlsopened"); } } diff --git a/plugins/net_ssl_openssl.c b/plugins/net_ssl_openssl.c index b343518e2..0a9bc42d3 100644 --- a/plugins/net_ssl_openssl.c +++ b/plugins/net_ssl_openssl.c @@ -1,7 +1,10 @@ #include "plugin.h" #include "netinc.h" -plugfsfuncs_t *fsfuncs; +static plugfsfuncs_t *fsfuncs; +static plugnetfuncs_t *netfuncs; + +static cvar_t *pdtls_psk_hint, *pdtls_psk_user, *pdtls_psk_key; #undef SHA1 #undef HMAC @@ -230,7 +233,7 @@ static int OSSL_Verify_Peer(int preverify_ok, X509_STORE_CTX *x509_ctx) if (err == X509_V_ERR_CERT_HAS_EXPIRED || err == X509_V_ERR_DEPTH_ZERO_SELF_SIGNED_CERT) { size_t knownsize; - qbyte *knowndata = TLS_GetKnownCertificate(uctx->peername, &knownsize); + qbyte *knowndata = netfuncs->TLS_GetKnownCertificate(uctx->peername, &knownsize); if (knowndata) { //check size_t blobsize; @@ -274,7 +277,7 @@ static int OSSL_Verify_Peer(int preverify_ok, X509_STORE_CTX *x509_ctx) probs |= CERTLOG_UNKNOWN; break; } - if (CertLog_ConnectOkay(uctx->peername, blob, blobsize, probs)) + if (netfuncs->CertLog_ConnectOkay && netfuncs->CertLog_ConnectOkay(uctx->peername, blob, blobsize, probs)) return 1; //ignore the errors... } #endif @@ -399,6 +402,65 @@ static void OSSL_OpenPubKey(void) BIO_free(bio); } +static char *OSSL_SetCertificateName(char *out, const char *hostname) +{ //glorified strcpy... + int i; + if (hostname) + { + const char *host = strstr(hostname, "://"); + if (host) + hostname = host+3; + //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) + { + memcpy(out, hostname, host-hostname); + out[host-hostname] = 0; + hostname = out; + } + } + else + { //eg: 127.0.0.1:port - strip the port number if specified. + host = strchr(hostname, ':'); + if (host) + { + memcpy(out, hostname, host-hostname); + out[host-hostname] = 0; + hostname = out; + } + } + for (i = 0; hostname[i]; i++) + { + if (hostname[i] >= 'a' && hostname[i] <= 'z') + ; + else if (hostname[i] >= 'A' && hostname[i] <= 'Z') + ; + else if (hostname[i] >= '0' && hostname[i] <= '9') + ; + else if (hostname[i] == '-' || hostname[i] == '.') + ; + else + { + hostname = NULL; //something invalid. bum. + break; + } + } + //we should have a cleaned up host name now, ready for (ab)use in certificates. + } + + if (!hostname) + *out = 0; + else if (hostname == out) + ; + else + memcpy(out, hostname, strlen(hostname)+1); + + return out; +} + static vfsfile_t *OSSL_OpenVFS(const char *hostname, vfsfile_t *source, qboolean isserver) { BIO *sink; @@ -406,6 +468,9 @@ static vfsfile_t *OSSL_OpenVFS(const char *hostname, vfsfile_t *source, qboolean if (!OSSL_Init()) return NULL; //FAIL! + if (!hostname) + hostname = ""; + n = calloc(sizeof(*n) + strlen(hostname)+1, 1); n->funcs.ReadBytes = OSSL_FRead; @@ -417,7 +482,7 @@ static vfsfile_t *OSSL_OpenVFS(const char *hostname, vfsfile_t *source, qboolean n->funcs.Flush = NULL; n->funcs.seekstyle = SS_UNSEEKABLE; - n->cert.peername = strcpy((char*)(n+1), hostname); + n->cert.peername = OSSL_SetCertificateName((char*)(n+1), hostname); n->cert.dtls = false; ERR_print_errors_cb(OSSL_PrintError_CB, NULL); @@ -489,6 +554,9 @@ typedef struct { // BIO *sink; qbyte *pending; size_t pendingsize; + + void *peeraddr; + size_t peeraddrsize; } ossldtls_t; static int OSSL_Bio_DWrite(BIO *h, const char *buf, int size) { @@ -539,11 +607,15 @@ static long OSSL_Bio_DCtrl(BIO *h, int cmd, long arg1, void *arg2) return 1; + case BIO_CTRL_DGRAM_GET_PEER: + return 0; + case BIO_CTRL_DGRAM_SET_NEXT_TIMEOUT: //we're non-blocking, so this doesn't affect us. case BIO_CTRL_DGRAM_GET_MTU_OVERHEAD: case BIO_CTRL_WPENDING: case BIO_CTRL_DGRAM_QUERY_MTU: case BIO_CTRL_DGRAM_SET_MTU: + case BIO_CTRL_DGRAM_GET_FALLBACK_MTU: return 0; @@ -576,17 +648,61 @@ static int OSSL_Bio_DDestroy(BIO *h) return 1; } +static int dehex(int i) +{ + if (i >= '0' && i <= '9') + return (i-'0'); + else if (i >= 'A' && i <= 'F') + return (i-'A'+10); + else + return (i-'a'+10); +} +static size_t Base16_DecodeBlock_(const char *in, qbyte *out, size_t outsize) +{ + qbyte *start = out; + if (!out) + return ((strlen(in)+1)/2) + 1; + + for (; ishexcode(in[0]) && ishexcode(in[1]) && outsize > 0; outsize--, in+=2) + *out++ = (dehex(in[0])<<4) | dehex(in[1]); + return out-start; +} + +static unsigned int OSSL_SV_Validate_PSK(SSL *ssl, const char *identity, unsigned char *psk, unsigned int max_psk_len) +{ + if (!strcmp(identity, pdtls_psk_user->string)) + { //Yay! We know this one! + return Base16_DecodeBlock_(pdtls_psk_key->string, psk, max_psk_len); + } + return 0; //0 for error, or something. +} +unsigned int OSSL_CL_Validate_PSK(SSL *ssl, const char *hint, char *identity, unsigned int max_identity_len, unsigned char *psk, unsigned int max_psk_len) +{ //if our hint cvar matches, then report our user+key cvars to the server + if ((!*hint && *pdtls_psk_user->string && !*pdtls_psk_hint->string) || (*hint && !strcmp(hint, pdtls_psk_hint->string))) + { + //FIXME: avoid crashing QE + + Q_strlcpy(identity, pdtls_psk_user->string, max_identity_len); + return Base16_DecodeBlock_(pdtls_psk_key->string, psk, max_psk_len); + } + return 0; //we don't know what to report. +} + static void *OSSL_CreateContext(const char *remotehost, void *cbctx, neterr_t(*push)(void *cbctx, const qbyte *data, size_t datasize), qboolean isserver) { //if remotehost is null then their certificate will not be validated. - ossldtls_t *n = calloc(sizeof(*n) + strlen(remotehost)+1, 1); + ossldtls_t *n; BIO *sink; + if (!remotehost) + remotehost = ""; + n = calloc(sizeof(*n) + strlen(remotehost)+1, 1); + n->cbctx = cbctx; n->push = push; n->ctx = SSL_CTX_new(isserver?DTLS_server_method():DTLS_client_method()); - n->cert.peername = strcpy((char*)(n+1), remotehost); + n->cert.peername = OSSL_SetCertificateName((char*)(n+1), remotehost); n->cert.dtls = true; if (n->ctx) @@ -599,6 +715,28 @@ static void *OSSL_CreateContext(const char *remotehost, void *cbctx, neterr_t(*p SSL_CTX_set_verify_depth(n->ctx, 5); SSL_CTX_set_options(n->ctx, SSL_OP_NO_COMPRESSION); //compression allows guessing the contents of the stream somehow. + if (isserver) + { + if (*pdtls_psk_user->string) + { + if (*pdtls_psk_user->string) + SSL_CTX_use_psk_identity_hint(n->ctx, pdtls_psk_hint->string); + SSL_CTX_set_psk_server_callback(n->ctx, OSSL_SV_Validate_PSK); + } + + if (vhost.servercert && vhost.privatekey) + { + SSL_CTX_use_certificate(n->ctx, vhost.servercert); + SSL_CTX_use_PrivateKey(n->ctx, vhost.privatekey); + assert(1==SSL_CTX_check_private_key(n->ctx)); + } + } + else + { + if (*pdtls_psk_user->string) + SSL_CTX_set_psk_client_callback(n->ctx, OSSL_CL_Validate_PSK); + } + //SSL_CTX_use_certificate_file //FIXME: SSL_CTX_use_certificate_file aka SSL_CTX_use_certificate(PEM_read_bio_X509) //FIXME: SSL_CTX_use_PrivateKey_file aka SSL_CTX_use_PrivateKey(PEM_read_bio_PrivateKey) @@ -619,8 +757,10 @@ static void *OSSL_CreateContext(const char *remotehost, void *cbctx, neterr_t(*p } BIO_get_ssl(n->bio, &n->ssl); + SSL_set_app_data(n->ssl, n); SSL_set_ex_data(n->ssl, ossl_fte_certctx, &n->cert); - SSL_set_tlsext_host_name(n->ssl, remotehost); //let the server know which cert to send + if (*n->cert.peername) + SSL_set_tlsext_host_name(n->ssl, n->cert.peername); //let the server know which cert to send BIO_do_connect(n->bio); ERR_print_errors_cb(OSSL_PrintError_CB, NULL); return n; @@ -629,6 +769,71 @@ static void *OSSL_CreateContext(const char *remotehost, void *cbctx, neterr_t(*p return NULL; } + +static char dtlscookiekey[16]; +static int OSSL_GenCookie(SSL *ssl, unsigned char *cookie, unsigned int *cookie_len) +{ + ossldtls_t *f = SSL_get_app_data(ssl); + + SHA_CTX ctx; + + SHA1_Init(&ctx); + SHA1_Update(&ctx, dtlscookiekey, sizeof(dtlscookiekey)); + SHA1_Update(&ctx, "somethinghashy", 15); + SHA1_Update(&ctx, f->peeraddr, f->peeraddrsize); + SHA1_Final(cookie, &ctx); + *cookie_len = SHA_DIGEST_LENGTH; + + return 1; +} +static int OSSL_VerifyCookie(SSL *ssl, const unsigned char *cookie, unsigned int cookie_len) +{ + unsigned char match[DTLS1_COOKIE_LENGTH]; + unsigned int matchsize; + if (OSSL_GenCookie(ssl, match, &matchsize)) + if (cookie_len == matchsize && !memcmp(cookie, match, matchsize)) + return 1; + return 0; //not valid. +} +qboolean OSSL_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)) +{ + int ret; + static ossldtls_t *pending; + BIO_ADDR *bioaddr = BIO_ADDR_new(); + + if (!pending) + { + pending = OSSL_CreateContext("localhost", cbctx, push, true); + + SSL_CTX_set_cookie_generate_cb(pending->ctx, OSSL_GenCookie); + SSL_CTX_set_cookie_verify_cb(pending->ctx, OSSL_VerifyCookie); + } + + SSL_set_app_data(pending->ssl, pending); + + //make sure its kept current... + pending->cbctx = cbctx; + pending->push = push; + + pending->pending = indata; + pending->pendingsize = insize; + ret = DTLSv1_listen(pending->ssl, bioaddr); + + BIO_ADDR_free(bioaddr); + + if (ret >= 1) + { + pending->pending = NULL; + pending->pendingsize = 0; + + EstablishTrueContext(&pending->cbctx, pending); + pending = NULL; //returned to called. next request gets a new one. + return true; + } + //0 = nonfatal + //-1 = fatal + return false; +} static void OSSL_DestroyContext(void *ctx) { ossldtls_t *o = (ossldtls_t*)ctx; @@ -709,7 +914,7 @@ static neterr_t OSSL_Timeouts(void *ctx) static dtlsfuncs_t ossl_dtlsfuncs = { OSSL_CreateContext, - NULL, + OSSL_CheckConnection, OSSL_DestroyContext, OSSL_Transmit, OSSL_Received, @@ -841,12 +1046,15 @@ static ftecrypto_t crypto_openssl = NULL, }; -void *TLS_GetKnownCertificate(const char *certname, size_t *size){return NULL;} -qboolean CertLog_ConnectOkay(const char *hostname, void *cert, size_t certsize, unsigned int certlogproblems){return false;} qboolean Plug_Init(void) { fsfuncs = plugfuncs->GetEngineInterface(plugfsfuncs_name, sizeof(*fsfuncs)); - if (!fsfuncs) + netfuncs = plugfuncs->GetEngineInterface(plugnetfuncs_name, sizeof(*netfuncs)); + if (!fsfuncs || !netfuncs) return false; + pdtls_psk_hint = cvarfuncs->GetNVFDG("dtls_psk_hint", "", 0, NULL, "DTLS stuff"); + pdtls_psk_user = cvarfuncs->GetNVFDG("dtls_psk_user", "", 0, NULL, "DTLS stuff"); + pdtls_psk_key = cvarfuncs->GetNVFDG("dtls_psk_key", "", 0, NULL, "DTLS stuff"); + netfuncs->RandomBytes(dtlscookiekey, sizeof(dtlscookiekey)); //something random so people can't guess cookies for arbitrary victim IPs. return plugfuncs->ExportInterface("Crypto", &crypto_openssl, sizeof(crypto_openssl)); //export a named interface struct to the engine } \ No newline at end of file diff --git a/plugins/plugin.h b/plugins/plugin.h index eb1ec0b12..0343821e2 100644 --- a/plugins/plugin.h +++ b/plugins/plugin.h @@ -374,6 +374,12 @@ typedef struct //for when you need basic socket access, hopefully rare... F(void, Close, (qhandle_t socket)); F(int, SetTLSClient, (qhandle_t sock, const char *certhostname)); //adds a tls layer to the socket (and specifies the peer's required hostname) F(int, GetTLSBinding, (qhandle_t sock, char *outdata, int *datalen)); //to avoid MITM attacks with compromised cert authorities + + //for (d)tls plugins to use. + F(qboolean, RandomBytes, (qbyte *string, int len)); + F(void *, TLS_GetKnownCertificate, (const char *certname, size_t *size)); + F(qboolean, CertLog_ConnectOkay, (const char *hostname, void *cert, size_t certsize, unsigned int certlogproblems)); + #define N_WOULDBLOCK -1 #define N_FATALERROR -2 #define NET_CLIENTPORT -1