/********************************************************************\ * BitlBee -- An IRC to other IM-networks gateway * * * * Copyright 2002-2004 Wilmer van der Gaast and others * \********************************************************************/ /* SSL module - GnuTLS version */ /* This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License with the Debian GNU/Linux distribution in /usr/share/common-licenses/GPL; if not, write to the Free Software Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA */ #include #include #include #include #include #include "proxy.h" #include "ssl_client.h" #include "sock.h" #include "stdlib.h" #include "bitlbee.h" int ssl_errno = 0; static gboolean initialized = FALSE; #include #if defined(ULONG_MAX) && ULONG_MAX > 4294967295UL #define GNUTLS_STUPID_CAST (long) #else #define GNUTLS_STUPID_CAST (int) #endif #define SSLDEBUG 0 struct scd { ssl_input_function func; gpointer data; int fd; gboolean established; int inpa; char *hostname; gboolean verify; gnutls_session session; gnutls_certificate_credentials xcred; }; static gboolean ssl_connected( gpointer data, gint source, b_input_condition cond ); static gboolean ssl_starttls_real( gpointer data, gint source, b_input_condition cond ); static gboolean ssl_handshake( gpointer data, gint source, b_input_condition cond ); void ssl_init( void ) { if( initialized ) return; gnutls_global_init(); initialized = TRUE; atexit( gnutls_global_deinit ); } void *ssl_connect( char *host, int port, ssl_input_function func, gpointer data ) { struct scd *conn = g_new0( struct scd, 1 ); conn->fd = proxy_connect( host, port, ssl_connected, conn ); conn->func = func; conn->data = data; conn->inpa = -1; if( conn->fd < 0 ) { g_free( conn ); return NULL; } return conn; } void *ssl_starttls( int fd, char *hostname, gboolean verify, ssl_input_function func, gpointer data ) { struct scd *conn = g_new0( struct scd, 1 ); conn->fd = fd; conn->func = func; conn->data = data; conn->inpa = -1; conn->hostname = hostname; /* For now, SSL verification is globally enabled by setting the cafile setting in bitlbee.conf. Commented out by default because probably not everyone has this file in the same place and plenty of folks may not have the cert of their private Jabber server in it. */ conn->verify = verify && global.conf->cafile; /* This function should be called via a (short) timeout instead of directly from here, because these SSL calls are *supposed* to be *completely* asynchronous and not ready yet when this function (or *_connect, for examle) returns. Also, errors are reported via the callback function, not via this function's return value. In short, doing things like this makes the rest of the code a lot simpler. */ b_timeout_add( 1, ssl_starttls_real, conn ); return conn; } static gboolean ssl_starttls_real( gpointer data, gint source, b_input_condition cond ) { struct scd *conn = data; return ssl_connected( conn, conn->fd, B_EV_IO_WRITE ); } static int verify_certificate_callback( gnutls_session_t session ) { unsigned int status; const gnutls_datum_t *cert_list; unsigned int cert_list_size; int gnutlsret; int verifyret = 0; gnutls_x509_crt_t cert; const char *hostname; hostname = gnutls_session_get_ptr(session ); gnutlsret = gnutls_certificate_verify_peers2( session, &status ); if( gnutlsret < 0 ) return VERIFY_CERT_ERROR; if( status & GNUTLS_CERT_INVALID ) verifyret |= VERIFY_CERT_INVALID; if( status & GNUTLS_CERT_REVOKED ) verifyret |= VERIFY_CERT_REVOKED; if( status & GNUTLS_CERT_SIGNER_NOT_FOUND ) verifyret |= VERIFY_CERT_SIGNER_NOT_FOUND; if( status & GNUTLS_CERT_SIGNER_NOT_CA ) verifyret |= VERIFY_CERT_SIGNER_NOT_CA; if( status & GNUTLS_CERT_INSECURE_ALGORITHM ) verifyret |= VERIFY_CERT_INSECURE_ALGORITHM; if( status & GNUTLS_CERT_NOT_ACTIVATED ) verifyret |= VERIFY_CERT_NOT_ACTIVATED; if( status & GNUTLS_CERT_EXPIRED ) verifyret |= VERIFY_CERT_EXPIRED; /* The following check is already performed inside * gnutls_certificate_verify_peers2, so we don't need it. * if( gnutls_certificate_type_get( session ) != GNUTLS_CRT_X509 ) * return GNUTLS_E_CERTIFICATE_ERROR; */ if( gnutls_x509_crt_init( &cert ) < 0 ) return VERIFY_CERT_ERROR; cert_list = gnutls_certificate_get_peers( session, &cert_list_size ); if( cert_list == NULL || gnutls_x509_crt_import( cert, &cert_list[0], GNUTLS_X509_FMT_DER ) < 0 ) return VERIFY_CERT_ERROR; if( !gnutls_x509_crt_check_hostname( cert, hostname ) ) { verifyret |= VERIFY_CERT_INVALID; verifyret |= VERIFY_CERT_WRONG_HOSTNAME; } gnutls_x509_crt_deinit( cert ); return verifyret; } static gboolean ssl_connected( gpointer data, gint source, b_input_condition cond ) { struct scd *conn = data; if( source == -1 ) { conn->func( conn->data, 0, NULL, cond ); g_free( conn ); return FALSE; } ssl_init(); gnutls_certificate_allocate_credentials( &conn->xcred ); if( conn->verify && global.conf->cafile ) { gnutls_certificate_set_x509_trust_file( conn->xcred, global.conf->cafile, GNUTLS_X509_FMT_PEM ); gnutls_certificate_set_verify_flags( conn->xcred, GNUTLS_VERIFY_ALLOW_X509_V1_CA_CRT ); } gnutls_init( &conn->session, GNUTLS_CLIENT ); if( conn->verify ) gnutls_session_set_ptr( conn->session, (void *) conn->hostname ); #if GNUTLS_VERSION_NUMBER < 0x020c00 gnutls_transport_set_lowat( conn->session, 0 ); #endif gnutls_set_default_priority( conn->session ); gnutls_credentials_set( conn->session, GNUTLS_CRD_CERTIFICATE, conn->xcred ); sock_make_nonblocking( conn->fd ); gnutls_transport_set_ptr( conn->session, (gnutls_transport_ptr) GNUTLS_STUPID_CAST conn->fd ); return ssl_handshake( data, source, cond ); } static gboolean ssl_handshake( gpointer data, gint source, b_input_condition cond ) { struct scd *conn = data; int st, stver; if( ( st = gnutls_handshake( conn->session ) ) < 0 ) { if( st == GNUTLS_E_AGAIN || st == GNUTLS_E_INTERRUPTED ) { conn->inpa = b_input_add( conn->fd, ssl_getdirection( conn ), ssl_handshake, data ); } else { conn->func( conn->data, 0, NULL, cond ); gnutls_deinit( conn->session ); gnutls_certificate_free_credentials( conn->xcred ); closesocket( conn->fd ); g_free( conn ); } } else { if( conn->verify && ( stver = verify_certificate_callback( conn->session ) ) != 0 ) { conn->func( conn->data, stver, NULL, cond ); gnutls_deinit( conn->session ); gnutls_certificate_free_credentials( conn->xcred ); closesocket( conn->fd ); g_free( conn ); } else { /* For now we can't handle non-blocking perfectly everywhere... */ sock_make_blocking( conn->fd ); conn->established = TRUE; conn->func( conn->data, 0, conn, cond ); } } return FALSE; } int ssl_read( void *conn, char *buf, int len ) { int st; if( !((struct scd*)conn)->established ) { ssl_errno = SSL_NOHANDSHAKE; return -1; } st = gnutls_record_recv( ((struct scd*)conn)->session, buf, len ); ssl_errno = SSL_OK; if( st == GNUTLS_E_AGAIN || st == GNUTLS_E_INTERRUPTED ) ssl_errno = SSL_AGAIN; if( SSLDEBUG && getenv( "BITLBEE_DEBUG" ) && st > 0 ) len = write( 2, buf, st ); return st; } int ssl_write( void *conn, const char *buf, int len ) { int st; if( !((struct scd*)conn)->established ) { ssl_errno = SSL_NOHANDSHAKE; return -1; } st = gnutls_record_send( ((struct scd*)conn)->session, buf, len ); ssl_errno = SSL_OK; if( st == GNUTLS_E_AGAIN || st == GNUTLS_E_INTERRUPTED ) ssl_errno = SSL_AGAIN; if( SSLDEBUG && getenv( "BITLBEE_DEBUG" ) && st > 0 ) len = write( 2, buf, st ); return st; } int ssl_pending( void *conn ) { if( conn == NULL ) return 0; if( !((struct scd*)conn)->established ) { ssl_errno = SSL_NOHANDSHAKE; return 0; } return gnutls_record_check_pending( ((struct scd*)conn)->session ) != 0; } void ssl_disconnect( void *conn_ ) { struct scd *conn = conn_; if( conn->inpa != -1 ) b_event_remove( conn->inpa ); if( conn->established ) gnutls_bye( conn->session, GNUTLS_SHUT_WR ); closesocket( conn->fd ); if( conn->session ) gnutls_deinit( conn->session ); if( conn->xcred ) gnutls_certificate_free_credentials( conn->xcred ); g_free( conn ); } int ssl_getfd( void *conn ) { return( ((struct scd*)conn)->fd ); } b_input_condition ssl_getdirection( void *conn ) { return( gnutls_record_get_direction( ((struct scd*)conn)->session ) ? B_EV_IO_WRITE : B_EV_IO_READ ); } size_t ssl_des3_encrypt( const unsigned char *key, size_t key_len, const unsigned char *input, size_t input_len, const unsigned char *iv, unsigned char **res ) { gcry_cipher_hd_t gcr; gcry_error_t st; ssl_init(); *res = g_malloc( input_len ); st = gcry_cipher_open( &gcr, GCRY_CIPHER_3DES, GCRY_CIPHER_MODE_CBC, 0 ) || gcry_cipher_setkey( gcr, key, key_len ) || gcry_cipher_setiv( gcr, iv, 8 ) || gcry_cipher_encrypt( gcr, *res, input_len, input, input_len ); gcry_cipher_close( gcr ); if( st == 0 ) return input_len; g_free( *res ); return 0; }