Index: src/clone.c ================================================================== --- src/clone.c +++ src/clone.c @@ -37,19 +37,22 @@ ** ** Options: ** ** --admin-user|-A USERNAME Make USERNAME the administrator ** --private Also clone private branches +** --certbundle NAME Use certificate bundle NAME for https +** connections ** */ void clone_cmd(void){ char *zPassword; const char *zDefaultUser; /* Optional name of the default user */ int nErr = 0; int bPrivate; /* Also clone private branches */ bPrivate = find_option("private",0,0)!=0; + g.urlCertBundle = find_option("certbundle",0,1); url_proxy_options(); if( g.argc < 4 ){ usage("?OPTIONS? FILE-OR-URL NEW-REPOSITORY"); } db_open_config(0); Index: src/http_ssl.c ================================================================== --- src/http_ssl.c +++ src/http_ssl.c @@ -29,21 +29,40 @@ ** ** 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> +#endif + +#include "config.h" +#include "http_ssl.h" + +/* +** Make sure the CERT table exists in the ~/.fossil database. +** +** This routine must be called in between two calls to db_swap_databases(). +*/ +static void create_cert_table_if_not_exist(void){ + static const char zSql[] = + @ CREATE TABLE IF NOT EXISTS certs( + @ name TEXT NOT NULL, + @ type TEXT NOT NULL, + @ filepath TEXT NOT NULL, + @ PRIMARY KEY(name, type) + @ ); + ; + db_multi_exec(zSql); +} + +#ifdef FOSSIL_ENABLE_SSL /* ** 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: @@ -51,10 +70,11 @@ 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; +static char *pempasswd = 0; /* Passphrase used to unlock key */ /* ** Clear the SSL error message */ @@ -78,10 +98,34 @@ ** Return the current SSL error message */ const char *ssl_errmsg(void){ return sslErrMsg; } + +/* +** Called by SSL when a passphrase protected file needs to be unlocked. +** We cache the passphrase so the user doesn't have to re-enter it for each new +** connection. +*/ +static int ssl_passwd_cb(char *buf, int size, int rwflag, void *userdata){ + if( userdata==0 ){ + Blob passwd; + prompt_for_password("\nPEM unlock passphrase: ", &passwd, 0); + strncpy(buf, (char *)blob_str(&passwd), size); + buf[size-1] = '\0'; + blob_reset(&passwd); + pempasswd = strdup(buf); + if( !pempasswd ){ + fossil_panic("Unable to allocate memory for PEM passphrase."); + } + SSL_CTX_set_default_passwd_cb_userdata(sslCtx, pempasswd); + }else{ + strncpy(buf, (char *)userdata, size); + } + + return strlen(buf); +} /* ** Call this routine once before any other use of the SSL interface. ** This routine does initial configuration of the SSL module. */ @@ -91,10 +135,12 @@ 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)); + SSL_CTX_set_default_passwd_cb(sslCtx, ssl_passwd_cb); + SSL_CTX_set_default_passwd_cb_userdata(sslCtx, NULL); sslIsInit = 1; } } /* @@ -129,16 +175,19 @@ ** Return the number of errors. */ int ssl_open(void){ X509 *cert; int hasSavedCertificate = 0; -char *connStr ; + 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. - */ + ** (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; @@ -145,14 +194,14 @@ } iBio = BIO_new_ssl_connect(sslCtx); BIO_get_ssl(iBio, &ssl); SSL_set_mode(ssl, SSL_MODE_AUTO_RETRY); - if( iBio==NULL ) { + if( iBio==NULL ){ ssl_set_errmsg("SSL: cannot open SSL (%s)", ERR_reason_error_string(ERR_get_error())); - return 1; + return 1; } connStr = mprintf("%s:%d", g.urlName, g.urlPort); BIO_set_conn_hostname(iBio, connStr); free(connStr); @@ -216,39 +265,45 @@ X509_free(cert); return 0; } /* -** Save certificate to global config. +** Save certificate to global certificate/key store. */ void ssl_save_certificate(X509 *cert){ BIO *mem; - char *zCert, *zHost; + char *zCert; 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); + db_swap_connections(); + create_cert_table_if_not_exist(); + db_begin_transaction(); + db_multi_exec("REPLACE INTO certs(name,type,filepath) " + "VALUES(%Q,'scert',%Q)", g.urlName, zCert); + db_end_transaction(0); + db_swap_connections(); BIO_free(mem); } /* -** Get certificate for g.urlName from global config. +** Get certificate for g.urlName from global certificate/key store. ** Return NULL if no certificate found. */ X509 *ssl_get_certificate(void){ - char *zHost, *zCert; + char *zCert; BIO *mem; X509 *cert; - zHost = mprintf("cert:%s", g.urlName); - zCert = db_get(zHost, NULL); - free(zHost); - if ( zCert==NULL ) + db_swap_connections(); + create_cert_table_if_not_exist(); + zCert = db_text(0, "SELECT filepath FROM certs WHERE name=%Q" + " AND type='scert'", g.urlName); + db_swap_connections(); + 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); @@ -286,6 +341,275 @@ pContent = (void*)&((char*)pContent)[got]; } return total; } +/* +** If a certbundle has been specified on the command line, then use it to look +** up certificates and keys, and then store the URL-certbundle association in +** the global database. If no certbundle 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 *zBundleName = NULL; + char *cafile; + char *capath; + char *certfile; + char *keyfile; + + if( g.urlCertBundle ){ + char *zName; + zName = mprintf("certbundle:%s", g.urlName); + db_set(zName, g.urlCertBundle, 1); + free(zName); + zBundleName = strdup(g.urlCertBundle); + }else{ + db_swap_connections(); + zBundleName = db_text(0, "SELECT value FROM global_config" + " WHERE name='certbundle:%q'", g.urlName); + db_swap_connections(); + } + if( !zBundleName ){ + /* No cert bundle specified on command line or found cached for URL */ + return; + } + + db_swap_connections(); + create_cert_table_if_not_exist(); + cafile = db_text(0, "SELECT filepath FROM certs WHERE name=%Q" + " AND type='cafile'", zBundleName); + capath = db_text(0, "SELECT filepath FROM certs WHERE name=%Q" + " AND type='capath'", zBundleName); + 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'", zBundleName); + certfile = db_text(0, "SELECT filepath FROM certs WHERE name=%Q" + " AND type='ccert'", zBundleName); + db_swap_connections(); + + if( certfile ){ + /* If a client certificate is explicitly specified, but a key is not, then + ** assume the key is in the same file as the certificate. + */ + if( !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( certfile && keyfile && !SSL_CTX_check_private_key(sslCtx) ){ + fossil_fatal("SSL: Private key does not match the certificate public " + "key."); + } + } + + if( keyfile != certfile ){ + free(keyfile); + } + free(certfile); + free(capath); + free(cafile); +} #endif /* FOSSIL_ENABLE_SSL */ + + +/* +** COMMAND: cert +** +** Usage: %fossil cert SUBCOMMAND ... +** +** Manage/bundle PKI client keys/certificates and CA certificates for SSL +** certificate chain verifications. +** +** %fossil cert add NAME ?--key KEYFILE? ?--cert CERTFILE? +** ?--cafile CAFILE? ?--capath CAPATH? +** +** Create a certificate bundle 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 formats must be PEM. +** +** %fossil cert list +** +** List all certificate bundles, their values and their URL +** associations. +** +** %fossil cert disassociate URL +** +** Disassociate URL from any certificate bundle. +** +** %fossil cert delete NAME +** +** Remove the certificate bundle NAME and all its URL associations. +** +*/ +void cert_cmd(void){ + int n; + const char *zCmd = "list"; /* Default sub-command */ + 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 KEYFILE? ?--cert CERTFILE? ?--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(); + create_cert_table_if_not_exist(); + db_begin_transaction(); + if( db_exists("SELECT 1 FROM certs WHERE name='%q'", zContainer)!=0 ){ + db_end_transaction(0); + fossil_fatal("certificate bundle \"%s\" already exists", zContainer); + } + 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 *bndl = NULL; + + db_open_config(0); + db_swap_connections(); + create_cert_table_if_not_exist(); + + 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, bndl)!=0 ){ + free(bndl); + bndl = 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 'certbundle:%%' AND value=%Q" + " ORDER BY name", bndl); + free(bndl); + + 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+11); + } + + 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='certbundle:%q'", + zURL); + if( db_changes() == 0 ){ + fossil_warning("No certificate bundle associated with URL \"%s\".", + zURL); + }else{ + printf("%s disassociated from its certificate bundle.\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(); + create_cert_table_if_not_exist(); + db_begin_transaction(); + db_multi_exec("DELETE FROM certs WHERE name=%Q", zContainer); + if( db_changes() == 0 ){ + fossil_warning("No certificate bundle named \"%s\" found", + zContainer); + }else{ + printf("%d entries removed\n", db_changes()); + } + db_multi_exec("DELETE FROM global_config WHERE name LIKE 'certbundle:%%'" + " 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"); + } +} Index: src/main.c ================================================================== --- src/main.c +++ src/main.c @@ -102,10 +102,11 @@ char *urlPasswd; /* Password for http: */ char *urlCanonical; /* Canonical representation of the URL */ char *urlProxyAuth; /* Proxy-Authorizer: string */ char *urlFossil; /* The path of the ?fossil=path suffix on ssh: */ int dontKeepUrl; /* Do not persist the URL */ + const char *urlCertBundle; /* Which ceritificate bundle to use for URL */ const char *zLogin; /* Login name. "" if not logged in. */ int useLocalauth; /* No login required if from 127.0.0.1 */ int noPswd; /* Logged in without password (on 127.0.0.1) */ int userUid; /* Integer user id */ Index: src/sync.c ================================================================== --- src/sync.c +++ src/sync.c @@ -96,10 +96,11 @@ const char *zPw = 0; int configSync = 0; int urlOptional = find_option("autourl",0,0)!=0; g.dontKeepUrl = find_option("once",0,0)!=0; *pPrivate = find_option("private",0,0)!=0; + g.urlCertBundle = find_option("certbundle",0,1); url_proxy_options(); db_find_and_open_repository(0, 0); db_open_config(0); if( g.argc==2 ){ zUrl = db_get("last-sync-url", 0); @@ -150,11 +151,16 @@ ** saved. ** ** Use the --private option to pull private branches from the ** remote repository. ** -** See also: clone, push, sync, remote-url +** Use the "--certbundle NAME" option to specify the name of the +** certificate/key bundle to use for https connections. If this option +** is not specified, a cached value associated with the URL will be +** used if it exists. +** +** See also: cert, clone, push, sync, remote-url */ void pull_cmd(void){ int syncFlags; int bPrivate; process_sync_args(&syncFlags, &bPrivate); @@ -179,11 +185,16 @@ ** saved. ** ** Use the --private option to push private branches to the ** remote repository. ** -** See also: clone, pull, sync, remote-url +** Use the "--certbundle NAME" option to specify the name of the +** certificate/key bundle to use for https connections. If this option +** is not specified, a cached value associated with the URL will be +** used if it exists. +** +** See also: cert, clone, pull, sync, remote-url */ void push_cmd(void){ int syncFlags; int bPrivate; process_sync_args(&syncFlags, &bPrivate); @@ -214,11 +225,16 @@ ** saved. ** ** Use the --private option to sync private branches with the ** remote repository. ** -** See also: clone, push, pull, remote-url +** Use the "--certbundle NAME" option to specify the name of the +** certificate/key bundle to use for https connections. If this option +** is not specified, a cached value associated with the URL will be +** used if it exists. +** +** See also: cert, clone, push, pull, remote-url */ void sync_cmd(void){ int syncFlags; int bPrivate; process_sync_args(&syncFlags, &bPrivate);