[ Team LiB ] Previous Section Next Section

8.19 Minimizing the Window of Vulnerability When Authenticating Without a PKI

8.19.1 Problem

You have an application (typically a client) that is likely to receive from a server identifying information such as a certificate or key that may not necessarily be able to be automatically verified—for example, because there is no PKI.

Without a way to absolutely defend against man-in-the-middle attacks in an automated fashion, you want to do the best that you can, either by having the user manually do certificate validation or by limiting the window of vulnerability to the first connection.

8.19.2 Solution

Either provide the user with trusted certificate information over a secure channel and allow him to enter that information, or prompt the user the first time you see a certificate, and remember it for subsequent connections.

These solutions push the burden of authentication off onto the user.

8.19.3 Discussion

It is common for small organizations to host some kind of a server that is SSL-enabled without a certificate that has been issued by a third-party CA such as VeriSign. Most often, such an organization issues its own certificate using its own CA. A prime example would be an SSL-enabled POP3 or SMTP server. Unfortunately, when this is the case, your software needs to have some way of allowing the client to indicate that the certificate presented by the server is acceptable.

There are two basic ways to do this:

  • Provide the user with some way to add the CA's certificate to a list of trusted certificates. This is certainly a good idea, and any program that verifies certificates should support this capability.

  • Prompt the user, asking if the certificate is acceptable. If the user answers yes, the certificate should be remembered, and the user is never prompted again. This approach could conceivably be something of an automated way of performing the first solution. In this way, the user need not go looking for the certificate and add it manually. It is not necessarily the most secure of solutions, but for many applications, the risk is acceptable.

Prompting the user works for other things besides certificates. Public keys are a good example of another type of identifying information that works well; in fact, public keys are employed by many SSH clients. When connecting to an SSH server for the first time, many SSH clients present the user with the fingerprint of the server's key and ask whether to terminate the connection, remember the key for future connections, or allow it for use only this one time. Often, the key is associated with the server's IP address, so if the key is remembered and the same server ever presents a different key, the user is notified that the key has changed, and that there is some possibility that the server has been compromised.

Be aware that the security provided by this recipe is not as strong as that provided by using a PKI (described in Chapter 10). There still exists the possibility that an attacker might mount a man-in-the-middle attack, particularly if the client has never connected to the server before and has no record of the server's credentials. Even if the client has the server's credentials, and they do not match, the client may opt to continue anyway, thinking that perhaps the server has regenerated its certificate or public key. The most common scenario, though, is that the user will not understand the warnings presented and the implications of proceeding when a change in server credentials is detected.

All of the work required for this recipe is on the client side. First, some kind of store is required to remember the information that is being presented by the server. Typically, this would be some kind of file on disk. For this recipe, we are going to concentrate on certificates and keys.

For certificates, we will store the entire certificate in Privacy Enhanced Mail (PEM) format (see Recipe 7.17). We will put one certificate in one file, and name that file in such a manner that OpenSSL can use it in a directory lookup. This entails computing the hash of the certificate's subject name and using it for the filename. You will generally want to provide a verify callback function in an spc_x509store_t object (see Recipe 10.5) that will ask the user whether to accept the certificate if OpenSSL has failed to verify it. The user could be presented with an option to reject the certificate, accept it this once, or accept and remember it. In the latter case, we'll save the certificate in an spc_x509store_t object in the directory identified in the call to spc_x509store_setcapath( ).

#include <stdio.h>
#include <string.h>
#include <unistd.h>
#include <openssl/ssl.h>
#include <openssl/x509.h>
   
char *spc_cert_filename(char *path, X509 *cert) {
  int  length;
  char *filename;
   
  length = strlen(path) + 11;
  if (!(filename = (char *)malloc(length + 1))) return 0;
  snprintf(filename, length + 1, "%s/%08lx.0", path, X509_subject_name_hash(cert));
  return filename;
}
   
int spc_remember_cert(char *path, X509 *cert) {
  int  result;
  char *filename;
  FILE *fp;
   
  if (!(filename = spc_cert_filename(path, cert))) return 0;
  if (!(fp = fopen(filename, "w"))) {
    free(filename);
    return 0;
  }
  result = PEM_write_X509(fp, cert);
  fclose(fp);
  if (!result) remove(filename);
  free(filename);
  return result;
}
   
int spc_verifyandmaybesave_callback(int ok, X509_STORE_CTX *store) {
  int             err;
  SSL             *ssl_ptr;
  char            answer[80], name[256];
  X509            *cert;
  SSL_CTX         *ctx;
  spc_x509store_t *spc_store;
   
  if (ok) return ok;
   
  cert = X509_STORE_CTX_get_current_cert(store);
  printf("An error has occurred with the following certificate:\n");
  X509_NAME_oneline(X509_get_issuer_name(cert), name, sizeof(name));
  printf("    Issuer Name:  %s\n", name);
  X509_NAME_oneline(X509_get_subject_name(cert), name, sizeof(name));
  printf("    Subject Name: %s\n", name);
  err = X509_STORE_CTX_get_error(store);
  printf("    Error Reason: %s\n", X509_verify_cert_error_string(err));
  for (;;) {
    printf("Do you want to [r]eject this certificate, [a]ccept and remember it, "
           "or allow\nits use for only this [o]ne time? ");
    if (!fgets(answer, sizeof(answer), stdin)) continue;
   
    if (answer[0] =  = 'r' || answer[0] =  = 'R') return 0;
    if (answer[0] =  = 'o' || answer[0] =  = 'O') return 1;
    if (answer[0] =  = 'a' || answer[0] =  = 'A') break;
  }
   
  ssl_ptr = (SSL *)X509_STORE_CTX_get_app_data(store);
  ctx = SSL_get_SSL_CTX(ssl_ptr);
  spc_store = (spc_x509store_t *)SSL_CTX_get_app_data(ctx);
  if (!spc_store->capath || !spc_remember_cert(spc_store->capath, cert))
    printf("Error remembering certificate!  It will be accepted this one time "
           "only.\n");
  return 1;
}

For keys, we will store the base64-encoded key in a flat file, much as OpenSSH does. We will also associate the IP address of the server that presented the key so that we can determine when the server's key has changed and warn the user. When we receive a key that we'd like to check to see whether we already know about it, we can call spc_lookup_key( ) with the filename of the key store, the IP number we received the key from, and the key we've just received. If we do not know anything about the key or if some kind of error occurs, 0 is returned. If we know about the key, and everything matches—that is, the IP numbers and the keys are the same—1 is returned. If we have a key stored for the IP number and it does not match the key we have just received, -1 is returned.

If you have multiple servers running on the same system, you need to make sure that they each keep separate caches so that the keys and IP numbers do not collide.

#include <ctype.h>
#include <stdio.h>
#include <string.h>
#include <openssl/evp.h>
#include <sys/types.h>
#include <netinet/in.h>
#include <arpa/inet.h>
   
static int get_keydata(EVP_PKEY *key, char **keydata) {
  BIO   *b64 = 0, *bio = 0;
  int   keytype, length;
  char  *dummy;
   
  *keydata = 0;
  keytype = EVP_PKEY_type(key->type);
  if (!(length = i2d_PublicKey(key, 0))) goto error_exit;
  if (!(dummy = *keydata = (char *)malloc(length))) goto error_exit;
  i2d_PublicKey(key, (unsigned char **)&dummy);
   
  if (!(bio = BIO_new(BIO_s_mem(  )))) goto error_exit;
  if (!(b64 = BIO_new(BIO_f_base64(  )))) goto error_exit;
  BIO_set_flags(b64, BIO_FLAGS_BASE64_NO_NL);
  if (!(bio = BIO_push(b64, bio))) goto error_exit;
  b64 = 0;
  BIO_write(bio, *keydata, length);
   
  free(*keydata);  *keydata = 0;
  if (!(length = BIO_get_mem_data(bio, &dummy))) goto error_exit;
  if (!(*keydata = (char *)malloc(length + 1))) goto error_exit;
  memcpy(*keydata, dummy, length);
  (*keydata)[length - 1] = '\0';
  return keytype;
   
error_exit:
  if (b64) BIO_free_all(b64);
  if (bio) BIO_free_all(bio);
  if (*keydata) free(*keydata);
  *keydata = 0;
  return EVP_PKEY_NONE;
}
   
static int parse_line(char *line, char **ipnum, int *keytype, char **keydata) {
  char  *end, *p, *tmp;
   
  /* we expect leading and trailing whitespace to be stripped already */
  for (p = line;  *p && !isspace(*p);  p++);
  if (!*p) return 0;
  *ipnum = line;
   
  for (*p++ = '\0';  *p && isspace(*p);  p++);
  for (tmp = p;  *p && !isspace(*p);  p++);
  *keytype = (int)strtol(tmp, &end, 0);
  if (*end && !isspace(*end)) return 0;
   
  for (p = end;  *p && isspace(*p);  p++);
  for (tmp = p;  *p && !isspace(*p);  p++);
  if (*p) return 0;
  *keydata = tmp;
   
  return 1;
}
   
int spc_lookup_key(char *filename, char *ipnum, EVP_PKEY *key) {
  int   bufsize = 0, length, keytype, lineno = 0, result = 0, store_keytype;
  char  *buffer = 0, *keydata, *line, *store_ipnum, *store_keydata, tmp[1024];
  FILE  *fp = 0;
   
  keytype = get_keydata(key, &keydata);
  if (keytype =  = EVP_PKEY_NONE || !keydata) goto end;
   
  if (!(fp = fopen(filename, "r"))) goto end;
  while (fgets(tmp, sizeof(tmp), fp)) {
    length = strlen(tmp);
    buffer = (char *)realloc(buffer, bufsize + length + 1);
    memcpy(buffer + bufsize, tmp, length + 1);
    bufsize += length;
    if (buffer[bufsize - 1] != '\n') continue;
    while (bufsize && (buffer[bufsize - 1] =  = '\r' || buffer[bufsize - 1] =  = '\n'))
      bufsize--;
    buffer[bufsize] = '\0';
    bufsize = 0;
    lineno++;
   
    for (line = buffer;  isspace(*line);  line++);
    for (length = strlen(line);  length && isspace(line[length - 1]);  length--);
    line[length - 1] = '\0';
    /* blank lines and lines beginning with # or ; are ignored */
    if (!length || line[0] =  = '#' || line[0] =  = ';') continue;
    if (!parse_line(line, &store_ipnum, &store_keytype, &store_keydata)) {
      fprintf(stderr, "%s:%d: parse error\n", filename, lineno);
      continue;
    }
    if (inet_addr(store_ipnum) != inet_addr(ipnum)) continue;
    if (store_keytype != keytype || strcasecmp(store_keydata, keydata))
      result = -1;
    else result = 1;
    break;
  }
   
end:
  if (buffer) free(buffer);
  if (keydata) free(keydata);
  if (fp) fclose(fp);
  return result;
}

If spc_lookup_key( ) returns 0, indicating that we do not know anything about the key, the user should be prompted in much the same way we did for certificates. If the user elects to remember the key, the spc_remember_key( ) function will add the key information to the key store so that the next time spc_lookup_key( ) is called, it will be found.

int spc_remember_key(char *filename, char *ipnum, EVP_PKEY *key) {
  int   keytype, result = 0;
  char  *keydata;
  FILE  *fp = 0;
   
  keytype = get_keydata(key, &keydata);
  if (keytype =  = EVP_PKEY_NONE || !keydata) goto end;
  if (!(fp = fopen(filename, "a"))) goto end;
  fprintf(fp, "%s %d %s\n", ipnum, keytype, keydata);
  result = 1;
   
end:
  if (keydata) free(keydata);
  if (fp) fclose(fp);
  return result;
}
   
int spc_accept_key(char *filename, char *ipnum, EVP_PKEY *key) {
  int   result;
  char  answer[80];
   
  result = spc_lookup_key(filename, ipnum, key);
  if (result =  = 1) return 1;
  if (result =  = -1) {
    for (;;) {
      printf("FATAL ERROR!  A different key has been received from the server "
              "%s\nthan we have on record.  Do you wish to continue? ", ipnum);
      if (!fgets(answer, sizeof(answer), stdin)) continue;
      if (answer[0] =  = 'Y' || answer[0] =  = 'y') return 1;
      if (answer[0] =  = 'N' || answer[0] =  = 'n') return 0;
    }
  }
   
  for (;;) {
    printf("WARNING!  The server %s has presented has presented a key for which "
           "we have no\nprior knowledge.  Do you want to [r]eject the key, "
           "[a]ccept and remember it,\nor allow its use for only this [o]ne "
           "time? ", ipnum);
    if (!fgets(answer, sizeof(answer), stdin)) continue;
    if (answer[0] =  = 'r' || answer[0] =  = 'R') return 0;
    if (answer[0] =  = 'o' || answer[0] =  = 'O') return 1;
    if (answer[0] =  = 'a' || answer[0] =  = 'A') break;
  }
   
  if (!spc_remember_key(filename, ipnum, key))
    printf("Error remembering the key!  It will be accepted this one time only "
           "instead.\n");
  return 1;
}

8.19.4 See Also

Recipe 7.17, Recipe 10.5

    [ Team LiB ] Previous Section Next Section