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);