/* ** Copyright (c) 2009 D. Richard Hipp ** ** This program is free software; you can redistribute it and/or ** modify it under the terms of the GNU General Public ** License version 2 as published by the Free Software Foundation. ** ** 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 along with this library; if not, write to the ** Free Software Foundation, Inc., 59 Temple Place - Suite 330, ** Boston, MA 02111-1307, USA. ** ** 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" #ifdef FOSSIL_ENABLE_SSL #include <openssl/bio.h> #include <openssl/ssl.h> #include <openssl/err.h> #include "http_ssl.h" #include <assert.h> #include <sys/types.h> /* ** 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; /* OpenSSL I/O abstraction */ static char *sslErrMsg = 0; /* Text of most recent OpenSSL error */ static SSL_CTX *sslCtx; /* SSL context */ static SSL *ssl; /* ** Clear the SSL error message */ static void ssl_clear_errmsg(void){ free(sslErrMsg); sslErrMsg = 0; } /* ** Set the SSL error message. */ void ssl_set_errmsg(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; } /* ** 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){ 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()); X509_STORE_set_default_paths(SSL_CTX_get_cert_store(sslCtx)); 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); } } /* ** Open an SSL connection. The identify of the server is determined ** by global varibles that are set using url_parse(): ** ** g.urlName Name of the server. Ex: www.fossil-scm.org ** g.urlPort TCP/IP port to use. Ex: 80 ** ** Return the number of errors. */ int ssl_open(void){ X509 *cert; int hasSavedCertificate = 0; char *connStr; ssl_global_init(); /* If client certificate/key has been set, load them into the SSL context. */ ssl_load_client_authfiles(); /* Get certificate for current server from global config and ** (if we have it in config) add it to certificate store. */ cert = ssl_get_certificate(); if ( cert!=NULL ){ X509_STORE_add_cert(SSL_CTX_get_cert_store(sslCtx), cert); X509_free(cert); hasSavedCertificate = 1; } iBio = BIO_new_ssl_connect(sslCtx); BIO_get_ssl(iBio, &ssl); SSL_set_mode(ssl, SSL_MODE_AUTO_RETRY); if( iBio==NULL ){ ssl_set_errmsg("SSL: cannot open SSL (%s)", ERR_reason_error_string(ERR_get_error())); return 1; } connStr = mprintf("%s:%d", g.urlName, g.urlPort); 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)", g.urlName, g.urlPort, 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)", g.urlName, g.urlPort, 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( SSL_get_verify_result(ssl) != X509_V_OK ){ char *desc, *prompt; char *warning = ""; Blob ans; BIO *mem; mem = BIO_new(BIO_s_mem()); X509_NAME_print_ex(mem, X509_get_subject_name(cert), 2, XN_FLAG_MULTILINE); BIO_puts(mem, "\n\nIssued By:\n\n"); X509_NAME_print_ex(mem, X509_get_issuer_name(cert), 2, XN_FLAG_MULTILINE); BIO_write(mem, "", 1); // null-terminate mem buffer BIO_get_mem_data(mem, &desc); if( hasSavedCertificate ){ warning = "WARNING: Certificate doesn't match the " "saved certificate for this host!"; } prompt = mprintf("\nUnknown SSL certificate:\n\n%s\n\n%s\n" "Accept certificate [a=always/y/N]? ", desc, warning); BIO_free(mem); prompt_user(prompt, &ans); free(prompt); if( blob_str(&ans)[0]!='y' && blob_str(&ans)[0]!='a' ) { X509_free(cert); ssl_set_errmsg("SSL certificate declined"); ssl_close(); return 1; } if( blob_str(&ans)[0]=='a' ) { ssl_save_certificate(cert); } blob_reset(&ans); } X509_free(cert); return 0; } /* ** Save certificate to global config. */ void ssl_save_certificate(X509 *cert){ BIO *mem; char *zCert, *zHost; mem = BIO_new(BIO_s_mem()); PEM_write_bio_X509(mem, cert); BIO_write(mem, "", 1); // null-terminate mem buffer BIO_get_mem_data(mem, &zCert); zHost = mprintf("cert:%s", g.urlName); db_set(zHost, zCert, 1); free(zHost); BIO_free(mem); } /* ** Get certificate for g.urlName from global config. ** Return NULL if no certificate found. */ X509 *ssl_get_certificate(void){ char *zHost, *zCert; BIO *mem; X509 *cert; zHost = mprintf("cert:%s", g.urlName); zCert = db_get(zHost, NULL); free(zHost); if ( zCert==NULL ) return NULL; mem = BIO_new(BIO_s_mem()); BIO_puts(mem, zCert); cert = PEM_read_bio_X509(mem, NULL, 0, NULL); free(zCert); BIO_free(mem); return cert; } /* ** Send content out over the SSL connection. */ size_t ssl_send(void *NotUsed, void *pContent, size_t N){ size_t sent; size_t total = 0; while( N>0 ){ sent = BIO_write(iBio, pContent, N); if( sent<=0 ) 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 got; size_t total = 0; while( N>0 ){ got = BIO_read(iBio, pContent, N); if( got<=0 ) break; total += got; N -= got; pContent = (void*)&((char*)pContent)[got]; } return total; } #if 0 /* ** Read client certificate and key, if set, and store them in the SSL context ** to allow communication with servers which are configured to verify client ** certificates and certificate chains. ** We only support PEM and don't support password protected keys. ** ** Always try the environment variables first, and if they aren't set, then ** use the global config. */ void ssl_load_client_authfiles(void){ char *cafile; char *capath; char *certfile; char *keyfile; cafile = ssl_get_and_set_file_ref("FOSSIL_CAFILE", "cafile"); capath = ssl_get_and_set_file_ref("FOSSIL_CAPATH", "capath"); if( cafile || capath ){ /* The OpenSSL documentation warns that if several CA certificates match ** the same name, key identifier and serial number conditions, only the ** first will be examined. The caveat situation is when one stores an ** expired CA certificate among the valid ones. ** Simply put: Do not mix expired and valid certificates. */ if( SSL_CTX_load_verify_locations(sslCtx, cafile, capath) == 0){ fossil_fatal("SSL: Unable to load CA verification file/path"); } }else{ fossil_warning("SSL: CA file/path missing for certificate verification."); } certfile = ssl_get_and_set_file_ref("FOSSIL_CCERT", "ccert"); if( !certfile ){ free(capath); free(cafile); return; } keyfile = ssl_get_and_set_file_ref("FOSSIL_CKEY", "ckey"); /* Assume the key is in the certificate file if key file was not specified */ if( certfile && !keyfile ){ keyfile = certfile; } if( SSL_CTX_use_certificate_file(sslCtx, certfile, SSL_FILETYPE_PEM) <= 0 ){ fossil_fatal("SSL: Unable to open client certificate in %s.", certfile); } if( SSL_CTX_use_PrivateKey_file(sslCtx, keyfile, SSL_FILETYPE_PEM) <= 0 ){ fossil_fatal("SSL: Unable to open client key in %s.", keyfile); } if( !SSL_CTX_check_private_key(sslCtx) ){ fossil_fatal("SSL: Private key does not match the certificate public " "key."); } free(keyfile); free(certfile); free(capath); free(cafile); } #endif /* ** If an certgroup has been specified on the command line, then use it to look ** up certificates and keys, and then store the URL-certgroup association in ** the global database. If no certgroup has been specified on the command line, ** see if there's an entry for the url in global_config, and use it if ** applicable. */ void ssl_load_client_authfiles(void){ char *zGroupName = NULL; char *cafile; char *capath; char *certfile; char *keyfile; if( g.urlCertGroup ){ char *zName; zName = mprintf("certgroup:%s", g.urlName); db_set(zName, g.urlCertGroup, 1); free(zName); zGroupName = strdup(g.urlCertGroup); }else{ db_swap_connections(); zGroupName = db_text(0, "SELECT value FROM global_config" " WHERE name='certgroup:%q'", g.urlName); db_swap_connections(); } if( !zGroupName ){ /* No cert group specified or found cached */ return; } db_swap_connections(); cafile = db_text(0, "SELECT filepath FROM certs WHERE name=%Q" " AND type='cafile'", zGroupName); capath = db_text(0, "SELECT filepath FROM certs WHERE name=%Q" " AND type='capath'", zGroupName); db_swap_connections(); if( cafile || capath ){ /* The OpenSSL documentation warns that if several CA certificates match ** the same name, key identifier and serial number conditions, only the ** first will be examined. The caveat situation occurs when one stores an ** expired CA certificate among the valid ones. ** Simply put: Do not mix expired and valid certificates. */ if( SSL_CTX_load_verify_locations(sslCtx, cafile, capath)==0 ){ fossil_fatal("SSL: Unable to load CA verification file/path"); } } db_swap_connections(); keyfile = db_text(0, "SELECT filepath FROM certs WHERE name=%Q" " AND type='ckey'", zGroupName); certfile = db_text(0, "SELECT filepath FROM certs WHERE name=%Q" " AND type='ccert'", zGroupName); db_swap_connections(); if( SSL_CTX_use_certificate_file(sslCtx, certfile, SSL_FILETYPE_PEM)<=0 ){ fossil_fatal("SSL: Unable to open client certificate in %s.", certfile); } if( SSL_CTX_use_PrivateKey_file(sslCtx, keyfile, SSL_FILETYPE_PEM)<=0 ){ fossil_fatal("SSL: Unable to open client key in %s.", keyfile); } if( !SSL_CTX_check_private_key(sslCtx) ){ fossil_fatal("SSL: Private key does not match the certificate public " "key."); } free(keyfile); free(certfile); free(capath); free(cafile); } #if 0 /* ** Get SSL authentication file reference from environment variable. If set, ** then store varaible in global config. If environment variable was not set, ** attempt to get variable from global config. **/ char *ssl_get_and_set_file_ref(const char *envvar, const char *dbvar){ char *zVar; char *zTmp; zTmp = mprintf("%s:%s", dbvar, g.urlName); zVar = getenv(envvar); if( zVar ){ zVar = strdup(zVar); if( zVar == NULL ){ fossil_fatal("Unable to allocate memory for %s value.", envvar); } db_set(zTmp, zVar, 1); }else{ zVar = db_get(zTmp, NULL); } free(zTmp); return zVar; } #endif /* ** COMMAND: cert ** ** Usage: %fossil cert SUBCOMMAND ... ** ** Manage/group PKI keys/certificates to be able to use client ** certificates and register CA certificates for SSL verifications. ** ** %fossil cert add NAME ?--key KEYFILE? ?--cert CERTFILE? ** ?--cafile CAFILE? ?--capath CAPATH? ** ** Create a certificate group NAME with the associated ** certificates/keys. If a client certificate is specified but no ** key, it is assumed that the key is located in the client ** certificate file. The file format must be PEM. ** ** %fossil cert list ** ** List all credential groups, their values and their URL ** associations. ** ** %fossil cert disassociate URL ** ** Disassociate URL from any credential group(s). ** ** %fossil cert delete NAME ** ** Remove the credential group NAME and all it's associated URL ** associations. ** */ void cert_cmd(void){ int n; const char *zCmd = "list"; if( g.argc>=3 ){ zCmd = g.argv[2]; } n = strlen(zCmd); if( strncmp(zCmd, "add", n)==0 ){ const char *zContainer; const char *zCKey; const char *zCCert; const char *zCAFile; const char *zCAPath; if( g.argc<5 ){ usage("add NAME ?--key CLIENTKEY? ?--cert CLIENTCERT? ?--cafile CAFILE? " "?--capath CAPATH?"); } zContainer = g.argv[3]; zCKey = find_option("key",0,1); zCCert = find_option("cert",0,1); zCAFile = find_option("cafile",0,1); zCAPath = find_option("capath",0,1); /* If a client certificate was specified, but a key was not, assume the * key is stored in the same file as the certificate. */ if( !zCKey && zCCert ){ zCKey = zCCert; } db_open_config(0); db_swap_connections(); if( db_exists( "SELECT 1 FROM certs" " WHERE name='%s'", zContainer)!=0 ){ fossil_fatal("certificate group \"%s\" already exists", zContainer); } db_begin_transaction(); if( zCKey ){ db_multi_exec("INSERT INTO certs (name,type,filepath) " "VALUES(%Q,'ckey',%Q)", zContainer, zCKey); } if( zCCert ){ db_multi_exec("INSERT INTO certs (name,type,filepath) " "VALUES(%Q,'ccert',%Q)", zContainer, zCCert); } if( zCAFile ){ db_multi_exec("INSERT INTO certs (name,type,filepath) " "VALUES(%Q,'cafile',%Q)", zContainer, zCAFile); } if( zCAPath ){ db_multi_exec("INSERT INTO certs (name,type,filepath) " "VALUES(%Q,'capath',%Q)", zContainer, zCAPath); } db_end_transaction(0); db_swap_connections(); }else if(strncmp(zCmd, "list", n)==0){ Stmt q; char *grp = NULL; db_open_config(0); db_swap_connections(); db_prepare(&q, "SELECT name,type,filepath FROM certs" " WHERE type NOT IN ('server')" " ORDER BY name,type"); while( db_step(&q)==SQLITE_ROW ){ const char *zCont = db_column_text(&q, 0); const char *zType = db_column_text(&q, 1); const char *zFilePath = db_column_text(&q, 2); if( fossil_strcmp(zCont, grp)!=0 ){ free(grp); grp = strdup(zCont); puts(zCont); } printf("\t%s=%s\n", zType, zFilePath); } db_finalize(&q); /* List the URL associations. */ db_prepare(&q, "SELECT name FROM global_config" " WHERE name LIKE 'certgroup:%%' AND value=%Q" " ORDER BY name", grp); free(grp); while( db_step(&q)==SQLITE_ROW ){ const char *zName = db_column_text(&q, 0); static int first = 1; if( first ) { puts("\tAssociations"); first = 0; } printf("\t\t%s\n", zName+10); } db_swap_connections(); }else if(strncmp(zCmd, "disassociate", n)==0){ const char *zURL; if( g.argc<4 ){ usage("disassociate URL"); } zURL = g.argv[3]; db_open_config(0); db_swap_connections(); db_begin_transaction(); db_multi_exec("DELETE FROM global_config WHERE name='certgroup:%s'", zURL); if( db_changes() == 0 ){ fossil_warning("No certificate group associated with URL \"%s\".", zURL); }else{ printf("%s disassociated from its certificate group.\n", zURL); } db_end_transaction(0); db_swap_connections(); }else if(strncmp(zCmd, "delete", n)==0){ const char *zContainer; if( g.argc<4 ){ usage("delete NAME"); } zContainer = g.argv[3]; db_open_config(0); db_swap_connections(); db_begin_transaction(); db_multi_exec("DELETE FROM certs WHERE name=%Q", zContainer); if( db_changes() == 0 ){ fossil_warning("No certificate group named \"%s\" found", zContainer); }else{ printf("%d entries removed\n", db_changes()); } db_multi_exec("DELETE FROM global_config WHERE name LIKE 'certgroup:%%'" " AND value=%Q", zContainer); if( db_changes() > 0 ){ printf("%d associations removed\n", db_changes()); } db_end_transaction(0); db_swap_connections(); }else{ fossil_panic("cert subcommand should be one of: " "add list disassociate delete"); } } #endif /* FOSSIL_ENABLE_SSL */