/* ** Copyright (c) 2009 D. Richard Hipp ** ** This program is free software; you can redistribute it and/or ** modify it under the terms of the Simplified BSD License (also ** known as the "2-Clause License" or "FreeBSD License".) ** ** 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. ** ** Author contact information: ** drh@hwaci.com ** http://www.hwaci.com/drh/ ** ******************************************************************************* ** ** This file manages low-level SSL communications. ** ** This file implements a singleton. A single SSL connection may be active ** at a time. State information is stored in static variables. The identity ** of the server is held in global variables that are set by url_parse(). ** ** SSL support is abstracted out into this module because Fossil can ** be compiled without SSL support (which requires OpenSSL library) */ #include "config.h" #include "http_ssl.h" #ifdef FOSSIL_ENABLE_SSL #include #include #include #include #include #include /* ** There can only be a single OpenSSL IO connection open at a time. ** State information about that IO is stored in the following ** local variables: */ static int sslIsInit = 0; /* True after global initialization */ static BIO *iBio = 0; /* OpenSSL I/O abstraction */ static char *sslErrMsg = 0; /* Text of most recent OpenSSL error */ static SSL_CTX *sslCtx; /* SSL context */ static SSL *ssl; static struct { /* Accept this SSL cert for this session only */ char *zHost; /* Subject or host name */ char *zHash; /* SHA2-256 hash of the cert */ } sException; static int sslNoCertVerify = 0; /* Do not verify SSL certs */ /* ** Clear the SSL error message */ static void ssl_clear_errmsg(void){ free(sslErrMsg); sslErrMsg = 0; } /* ** Set the SSL error message. */ void ssl_set_errmsg(const char *zFormat, ...){ va_list ap; ssl_clear_errmsg(); va_start(ap, zFormat); sslErrMsg = vmprintf(zFormat, ap); va_end(ap); } /* ** Return the current SSL error message */ const char *ssl_errmsg(void){ return sslErrMsg; } /* ** When a server requests a client certificate that hasn't been provided, ** display a warning message explaining what to do next. */ static int ssl_client_cert_callback(SSL *ssl, X509 **x509, EVP_PKEY **pkey){ fossil_warning("The remote server requested a client certificate for " "authentication. Specify the pathname to a file containing the PEM " "encoded certificate and private key with the --ssl-identity option " "or the ssl-identity setting."); return 0; /* no cert available */ } /* ** Call this routine once before any other use of the SSL interface. ** This routine does initial configuration of the SSL module. */ void ssl_global_init(void){ const char *zCaSetting = 0, *zCaFile = 0, *zCaDirectory = 0; const char *identityFile; if( sslIsInit==0 ){ SSL_library_init(); SSL_load_error_strings(); ERR_load_BIO_strings(); OpenSSL_add_all_algorithms(); sslCtx = SSL_CTX_new(SSLv23_client_method()); /* Disable SSLv2 and SSLv3 */ SSL_CTX_set_options(sslCtx, SSL_OP_NO_SSLv2|SSL_OP_NO_SSLv3); /* Set up acceptable CA root certificates */ zCaSetting = db_get("ssl-ca-location", 0); if( zCaSetting==0 || zCaSetting[0]=='\0' ){ /* CA location not specified, use platform's default certificate store */ X509_STORE_set_default_paths(SSL_CTX_get_cert_store(sslCtx)); }else{ /* User has specified a CA location, make sure it exists and use it */ switch( file_isdir(zCaSetting, ExtFILE) ){ case 0: { /* doesn't exist */ fossil_fatal("ssl-ca-location is set to '%s', " "but is not a file or directory", zCaSetting); break; } case 1: { /* directory */ zCaDirectory = zCaSetting; break; } case 2: { /* file */ zCaFile = zCaSetting; break; } } if( SSL_CTX_load_verify_locations(sslCtx, zCaFile, zCaDirectory)==0 ){ fossil_fatal("Failed to use CA root certificates from " "ssl-ca-location '%s'", zCaSetting); } } /* Load client SSL identity, preferring the filename specified on the ** command line */ if( g.zSSLIdentity!=0 ){ identityFile = g.zSSLIdentity; }else{ identityFile = db_get("ssl-identity", 0); } if( identityFile!=0 && identityFile[0]!='\0' ){ if( SSL_CTX_use_certificate_chain_file(sslCtx,identityFile)!=1 || SSL_CTX_use_PrivateKey_file(sslCtx,identityFile,SSL_FILETYPE_PEM)!=1 ){ fossil_fatal("Could not load SSL identity from %s", identityFile); } } /* Register a callback to tell the user what to do when the server asks ** for a cert */ SSL_CTX_set_client_cert_cb(sslCtx, ssl_client_cert_callback); sslIsInit = 1; } } /* ** Call this routine to shutdown the SSL module prior to program exit. */ void ssl_global_shutdown(void){ if( sslIsInit ){ SSL_CTX_free(sslCtx); ssl_clear_errmsg(); sslIsInit = 0; } } /* ** Close the currently open SSL connection. If no connection is open, ** this routine is a no-op. */ void ssl_close(void){ if( iBio!=NULL ){ (void)BIO_reset(iBio); BIO_free_all(iBio); iBio = NULL; } } /* See RFC2817 for details */ static int establish_proxy_tunnel(UrlData *pUrlData, BIO *bio){ int rc, httpVerMin; char *bbuf; Blob snd, reply; int done=0,end=0; blob_zero(&snd); blob_appendf(&snd, "CONNECT %s:%d HTTP/1.1\r\n", pUrlData->hostname, pUrlData->proxyOrigPort); blob_appendf(&snd, "Host: %s:%d\r\n", pUrlData->hostname, pUrlData->proxyOrigPort); if( pUrlData->proxyAuth ){ blob_appendf(&snd, "Proxy-Authorization: %s\r\n", pUrlData->proxyAuth); } blob_append(&snd, "Proxy-Connection: keep-alive\r\n", -1); blob_appendf(&snd, "User-Agent: %s\r\n", get_user_agent()); blob_append(&snd, "\r\n", 2); BIO_write(bio, blob_buffer(&snd), blob_size(&snd)); blob_reset(&snd); /* Wait for end of reply */ blob_zero(&reply); do{ int len; char buf[256]; len = BIO_read(bio, buf, sizeof(buf)); blob_append(&reply, buf, len); bbuf = blob_buffer(&reply); len = blob_size(&reply); while(end < len) { if(bbuf[end] == '\r') { if(len - end < 4) { /* need more data */ break; } if(memcmp(&bbuf[end], "\r\n\r\n", 4) == 0) { done = 1; break; } } end++; } }while(!done); sscanf(bbuf, "HTTP/1.%d %d", &httpVerMin, &rc); blob_reset(&reply); return rc; } /* ** Invoke this routine to disable SSL cert verification. After ** this call is made, any SSL cert that the server provides will ** be accepted. Communication will still be encrypted, but the ** client has no way of knowing whether it is talking to the ** real server or a man-in-the-middle imposter. */ void ssl_disable_cert_verification(void){ sslNoCertVerify = 1; } /* ** Open an SSL connection. The identify of the server is determined ** as follows: ** ** pUrlData->name Name of the server. Ex: fossil-scm.org ** g.url.name Name of the proxy server, if proxying. ** pUrlData->port TCP/IP port to use. Ex: 80 ** ** Return the number of errors. */ int ssl_open(UrlData *pUrlData){ X509 *cert; const char *zRemoteHost; ssl_global_init(); if( pUrlData->useProxy ){ int rc; char *connStr = mprintf("%s:%d", g.url.name, pUrlData->port); BIO *sBio = BIO_new_connect(connStr); free(connStr); if( BIO_do_connect(sBio)<=0 ){ ssl_set_errmsg("SSL: cannot connect to proxy %s:%d (%s)", pUrlData->name, pUrlData->port, ERR_reason_error_string(ERR_get_error())); ssl_close(); return 1; } rc = establish_proxy_tunnel(pUrlData, sBio); if( rc<200||rc>299 ){ ssl_set_errmsg("SSL: proxy connect failed with HTTP status code %d", rc); return 1; } pUrlData->path = pUrlData->proxyUrlPath; iBio = BIO_new_ssl(sslCtx, 1); BIO_push(iBio, sBio); zRemoteHost = pUrlData->hostname; }else{ iBio = BIO_new_ssl_connect(sslCtx); zRemoteHost = pUrlData->name; } if( iBio==NULL ) { ssl_set_errmsg("SSL: cannot open SSL (%s)", ERR_reason_error_string(ERR_get_error())); return 1; } BIO_get_ssl(iBio, &ssl); #if (SSLEAY_VERSION_NUMBER >= 0x00908070) && !defined(OPENSSL_NO_TLSEXT) if( !SSL_set_tlsext_host_name(ssl, zRemoteHost)){ fossil_warning("WARNING: failed to set server name indication (SNI), " "continuing without it.\n"); } #endif SSL_set_mode(ssl, SSL_MODE_AUTO_RETRY); #if OPENSSL_VERSION_NUMBER >= 0x010002000 if( !sslNoCertVerify ){ X509_VERIFY_PARAM *param = 0; param = SSL_get0_param(ssl); if( !X509_VERIFY_PARAM_set1_host(param, zRemoteHost, strlen(zRemoteHost)) ){ fossil_fatal("failed to set hostname."); } /* SSL_set_verify(ssl, SSL_VERIFY_PEER, 0); */ } #endif if( !pUrlData->useProxy ){ char *connStr = mprintf("%s:%d", pUrlData->name, pUrlData->port); BIO_set_conn_hostname(iBio, connStr); free(connStr); if( BIO_do_connect(iBio)<=0 ){ ssl_set_errmsg("SSL: cannot connect to host %s:%d (%s)", pUrlData->name, pUrlData->port, ERR_reason_error_string(ERR_get_error())); ssl_close(); return 1; } } if( BIO_do_handshake(iBio)<=0 ) { ssl_set_errmsg("Error establishing SSL connection %s:%d (%s)", pUrlData->useProxy?pUrlData->hostname:pUrlData->name, pUrlData->useProxy?pUrlData->proxyOrigPort:pUrlData->port, ERR_reason_error_string(ERR_get_error())); ssl_close(); return 1; } /* Check if certificate is valid */ cert = SSL_get_peer_certificate(ssl); if ( cert==NULL ){ ssl_set_errmsg("No SSL certificate was presented by the peer"); ssl_close(); return 1; } if( !sslNoCertVerify && SSL_get_verify_result(ssl)!=X509_V_OK ){ int x, desclen; char *desc, *prompt; Blob ans; char cReply; BIO *mem; unsigned char md[EVP_MAX_MD_SIZE]; char zHash[EVP_MAX_MD_SIZE*2+1]; unsigned int mdLength = (int)sizeof(md); memset(md, 0, sizeof(md)); zHash[0] = 0; /* MMNNFFPPS */ #if OPENSSL_VERSION_NUMBER >= 0x010000000 x = X509_digest(cert, EVP_sha256(), md, &mdLength); #else x = X509_digest(cert, EVP_sha1(), md, &mdLength); #endif if( x ){ int j; for(j=0; j>4]; zHash[j*2+1] = "0123456789abcdef"[md[j]&0xf]; } zHash[j*2] = 0; } if( ssl_certificate_exception_exists(pUrlData, zHash) ){ /* Ignore the failure because an exception exists */ ssl_one_time_exception(pUrlData, zHash); }else{ /* Tell the user about the failure and ask what to do */ mem = BIO_new(BIO_s_mem()); BIO_puts(mem, " subject: "); X509_NAME_print_ex(mem, X509_get_subject_name(cert), 0, XN_FLAG_ONELINE); BIO_puts(mem, "\n issuer: "); X509_NAME_print_ex(mem, X509_get_issuer_name(cert), 0, XN_FLAG_ONELINE); BIO_printf(mem, "\n sha256: %s", zHash); desclen = BIO_get_mem_data(mem, &desc); prompt = mprintf("Unable to verify SSL cert from %s\n%.*s\n" "accept this cert and continue (y/N/fingerprint)? ", pUrlData->name, desclen, desc); BIO_free(mem); prompt_user(prompt, &ans); free(prompt); cReply = blob_str(&ans)[0]; if( cReply!='y' && cReply!='Y' && fossil_stricmp(blob_str(&ans),zHash)!=0 ){ X509_free(cert); ssl_set_errmsg("SSL cert declined"); ssl_close(); blob_reset(&ans); return 1; } blob_reset(&ans); ssl_one_time_exception(pUrlData, zHash); prompt_user("remember this exception (y/N)? ", &ans); cReply = blob_str(&ans)[0]; if( cReply=='y' || cReply=='Y') { db_open_config(0,0); ssl_remember_certificate_exception(pUrlData, zHash); } blob_reset(&ans); } } /* Set the Global.zIpAddr variable to the server we are talking to. ** This is used to populate the ipaddr column of the rcvfrom table, ** if any files are received from the server. */ { /* As soon as libressl implements ** BIO_ADDR_hostname_string/BIO_get_conn_address. ** check here for the correct LIBRESSL_VERSION_NUMBER too. For now: disable */ #if defined(OPENSSL_VERSION_NUMBER) && OPENSSL_VERSION_NUMBER >= 0x10100000L \ && !defined(LIBRESSL_VERSION_NUMBER) char *ip = BIO_ADDR_hostname_string(BIO_get_conn_address(iBio),1); g.zIpAddr = mprintf("%s", ip); OPENSSL_free(ip); #else /* IPv4 only code */ const unsigned char *ip; ip = (const unsigned char*)BIO_ptr_ctrl(iBio,BIO_C_GET_CONNECT,2); g.zIpAddr = mprintf("%d.%d.%d.%d", ip[0], ip[1], ip[2], ip[3]); #endif } X509_free(cert); return 0; } /* ** Remember that the cert with the given hash is a acceptable for ** use with pUrlData->name. */ LOCAL void ssl_remember_certificate_exception( UrlData *pUrlData, const char *zHash ){ db_set_mprintf(zHash, 1, "cert:%s", pUrlData->name); } /* ** Return true if the there exists a certificate exception for ** pUrlData->name that matches the hash. */ LOCAL int ssl_certificate_exception_exists( UrlData *pUrlData, const char *zHash ){ char *zName, *zValue; if( fossil_strcmp(sException.zHost,pUrlData->name)==0 && fossil_strcmp(sException.zHash,zHash)==0 ){ return 1; } zName = mprintf("cert:%s", pUrlData->name); zValue = db_get(zName,0); fossil_free(zName); return zValue!=0 && strcmp(zHash,zValue)==0; } /* ** Remember zHash as an acceptable certificate for this session only. */ LOCAL void ssl_one_time_exception( UrlData *pUrlData, const char *zHash ){ fossil_free(sException.zHost); sException.zHost = fossil_strdup(pUrlData->name); fossil_free(sException.zHash); sException.zHash = fossil_strdup(zHash); } /* ** Send content out over the SSL connection. */ size_t ssl_send(void *NotUsed, void *pContent, size_t N){ size_t total = 0; while( N>0 ){ int sent = BIO_write(iBio, pContent, N); if( sent<=0 ){ if( BIO_should_retry(iBio) ){ continue; } break; } total += sent; N -= sent; pContent = (void*)&((char*)pContent)[sent]; } return total; } /* ** Receive content back from the SSL connection. */ size_t ssl_receive(void *NotUsed, void *pContent, size_t N){ size_t total = 0; while( N>0 ){ int got = BIO_read(iBio, pContent, N); if( got<=0 ){ if( BIO_should_retry(iBio) ){ continue; } break; } total += got; N -= got; pContent = (void*)&((char*)pContent)[got]; } return total; } #endif /* FOSSIL_ENABLE_SSL */ /* ** COMMAND: tls-config* ** ** Usage: %fossil tls-config [SUBCOMMAND] [OPTIONS...] [ARGS...] ** ** This command is used to view or modify the TLS (Transport Layer ** Security) configuration for Fossil. TLS (formerly SSL) is the ** encryption technology used for secure HTTPS transport. ** ** Sub-commands: ** ** show Show the TLS configuration ** ** remove-exception DOMAIN... Remove TLS cert exceptions ** for the domains listed. Or if ** the --all option is specified, ** remove all TLS cert exceptions. */ void test_tlsconfig_info(void){ #if !defined(FOSSIL_ENABLE_SSL) fossil_print("TLS disabled in this build\n"); #else const char *zCmd; size_t nCmd; int nHit = 0; db_find_and_open_repository(OPEN_OK_NOT_FOUND|OPEN_SUBSTITUTE,0); db_open_config(1,0); zCmd = g.argc>=3 ? g.argv[2] : "show"; nCmd = strlen(zCmd); if( strncmp("show",zCmd,nCmd)==0 ){ const char *zName, *zValue; size_t nName; Stmt q; fossil_print("OpenSSL-version: %s (0x%09x)\n", SSLeay_version(SSLEAY_VERSION), OPENSSL_VERSION_NUMBER); fossil_print("OpenSSL-cert-file: %s\n", X509_get_default_cert_file()); fossil_print("OpenSSL-cert-dir: %s\n", X509_get_default_cert_dir()); zName = X509_get_default_cert_file_env(); zValue = fossil_getenv(zName); if( zValue==0 ) zValue = ""; nName = strlen(zName); fossil_print("%s:%*s%s\n", zName, 18-nName, "", zValue); zName = X509_get_default_cert_dir_env(); zValue = fossil_getenv(zName); if( zValue==0 ) zValue = ""; nName = strlen(zName); fossil_print("%s:%*s%s\n", zName, 18-nName, "", zValue); nHit++; fossil_print("ssl-ca-location: %s\n", db_get("ssl-ca-location","")); fossil_print("ssl-identity: %s\n", db_get("ssl-identity","")); db_prepare(&q, "SELECT name FROM global_config" " WHERE name GLOB 'cert:*'" "UNION ALL " "SELECT name FROM config" " WHERE name GLOB 'cert:*'" " ORDER BY name" ); while( db_step(&q)==SQLITE_ROW ){ fossil_print("exception: %s\n", db_column_text(&q,0)+5); } db_finalize(&q); }else if( strncmp("remove-exception",zCmd,nCmd)==0 ){ int i; Blob sql; char *zSep = "("; db_begin_transaction(); blob_init(&sql, 0, 0); if( g.argc==4 && find_option("all",0,0)!=0 ){ blob_append_sql(&sql, "DELETE FROM global_config WHERE name GLOB 'cert:*';\n" "DELETE FROM global_config WHERE name GLOB 'trusted:*';\n" "DELETE FROM config WHERE name GLOB 'cert:*';\n" "DELETE FROM config WHERE name GLOB 'trusted:*';\n" ); }else{ if( g.argc<4 ){ usage("remove-exception DOMAIN-NAME ..."); } blob_append_sql(&sql,"DELETE FROM global_config WHERE name IN "); for(i=3; i