merged usage() information from master
authorMichael Rash <mbr@cipherdyne.org>
Mon, 9 Jul 2012 02:00:13 +0000 (22:00 -0400)
committerMichael Rash <mbr@cipherdyne.org>
Mon, 9 Jul 2012 02:00:13 +0000 (22:00 -0400)
1  2 
lib/fko.h
lib/fko_context.h
lib/fko_funcs.c
server/incoming_spa.c
test/test-fwknop.pl

diff --combined lib/fko.h
+++ b/lib/fko.h
@@@ -33,8 -33,6 +33,8 @@@
  
  #include <time.h>
  
 +#include "rijndael.h"   /* For encryption modes */
 +
  #ifdef __cplusplus
  extern "C" {
  #endif
@@@ -93,20 -91,6 +93,20 @@@ typedef enum 
      FKO_LAST_ENCRYPTION_TYPE /* Always leave this as the last one */
  } fko_encryption_type_t;
  
 +/* Symmetric encryption modes derived from rijndael.h
 +*/
 +typedef enum {
 +    FKO_ENC_MODE_UNKNOWN = 0,
 +    FKO_ENC_MODE_ECB  = MODE_ECB,
 +    FKO_ENC_MODE_CBC  = MODE_CBC,
 +    FKO_ENC_MODE_CFB  = MODE_CFB,
 +    FKO_ENC_MODE_PCBC = MODE_PCBC,
 +    FKO_ENC_MODE_OFB  = MODE_OFB,
 +    FKO_ENC_MODE_CTR  = MODE_CTR,
 +    FKO_ENC_MODE_ASYMMETRIC,  /* placeholder when GPG is used */
 +    FKO_LAST_ENC_MODE /* Always leave this as the last one */
 +} fko_encryption_mode_t;
 +
  /* FKO ERROR_CODES
   *
   * Note: If you change this list in any way, please be sure to make the
@@@ -176,7 -160,6 +176,7 @@@ typedef enum 
  #define FKO_DEFAULT_MSG_TYPE    FKO_ACCESS_MSG
  #define FKO_DEFAULT_DIGEST      FKO_DIGEST_SHA256
  #define FKO_DEFAULT_ENCRYPTION  FKO_ENCRYPTION_RIJNDAEL
 +#define FKO_DEFAULT_ENC_MODE    MODE_CBC
  
  /* The context holds the global state and config options, as
   * well as some intermediate results during processing. This
@@@ -206,8 -189,7 +206,8 @@@ enum 
  /* General api calls
  */
  DLL_API int fko_new(fko_ctx_t *ctx);
 -DLL_API int fko_new_with_data(fko_ctx_t *ctx, const char *enc_msg, const char *dec_key);
 +DLL_API int fko_new_with_data(fko_ctx_t *ctx, const char *enc_msg, const char *dec_key,
 +    int encryption_mode);
  DLL_API void fko_destroy(fko_ctx_t ctx);
  DLL_API int fko_spa_data_final(fko_ctx_t ctx, const char *enc_key);
  
@@@ -224,8 -206,9 +224,10 @@@ DLL_API int fko_set_spa_server_auth(fko
  DLL_API int fko_set_spa_client_timeout(fko_ctx_t ctx, const int timeout);
  DLL_API int fko_set_spa_digest_type(fko_ctx_t ctx, const short digest_type);
  DLL_API int fko_set_spa_digest(fko_ctx_t ctx);
+ DLL_API int fko_set_raw_spa_digest_type(fko_ctx_t ctx, const short raw_digest_type);
+ DLL_API int fko_set_raw_spa_digest(fko_ctx_t ctx);
  DLL_API int fko_set_spa_encryption_type(fko_ctx_t ctx, const short encrypt_type);
 +DLL_API int fko_set_spa_encryption_mode(fko_ctx_t ctx, const int encrypt_mode);
  DLL_API int fko_set_spa_data(fko_ctx_t ctx, const char *enc_msg);
  
  /* Data processing and misc utility functions
@@@ -252,9 -235,10 +254,11 @@@ DLL_API int fko_get_spa_nat_access(fko_
  DLL_API int fko_get_spa_server_auth(fko_ctx_t ctx, char **server_auth);
  DLL_API int fko_get_spa_client_timeout(fko_ctx_t ctx, int *client_timeout);
  DLL_API int fko_get_spa_digest_type(fko_ctx_t ctx, short *spa_digest_type);
+ DLL_API int fko_get_raw_spa_digest_type(fko_ctx_t ctx, short *raw_spa_digest_type);
  DLL_API int fko_get_spa_digest(fko_ctx_t ctx, char **spa_digest);
+ DLL_API int fko_get_raw_spa_digest(fko_ctx_t ctx, char **raw_spa_digest);
  DLL_API int fko_get_spa_encryption_type(fko_ctx_t ctx, short *spa_enc_type);
 +DLL_API int fko_get_spa_encryption_mode(fko_ctx_t ctx, int *spa_enc_mode);
  DLL_API int fko_get_spa_data(fko_ctx_t ctx, char **spa_data);
  
  DLL_API int fko_get_version(fko_ctx_t ctx, char **version);
diff --combined lib/fko_context.h
@@@ -63,12 -63,17 +63,18 @@@ struct fko_context 
      /* FKO SPA user-settable message encoding types */
      short  digest_type;
      short  encryption_type;
 +    int    encryption_mode;
  
      /* Computed or predefined data */
      char           *version;
      char           *digest;
  
+     /* Digest of raw encrypted/base64 data - this is used
+      * for replay attack detection
+     */
+     char           *raw_digest;
+     short           raw_digest_type;
      /* Computed processed data (encodings, etc.) */
      char           *encoded_msg;
      char           *encrypted_msg;
diff --combined lib/fko_funcs.c
@@@ -133,18 -133,6 +133,18 @@@ fko_new(fko_ctx_t *r_ctx
          return res;
      }
  
 +    /* Default Encryption Mode (Rijndael in EBC mode for backwards
 +     * compatibility - it recommended to change this to CBC mode)
 +    */
 +    ctx->initval = FKO_CTX_INITIALIZED;
 +    res = fko_set_spa_encryption_mode(ctx, FKO_DEFAULT_ENC_MODE);
 +    ctx->initval = 0;
 +    if(res != FKO_SUCCESS)
 +    {
 +        fko_destroy(ctx);
 +        return res;
 +    }
 +
  #if HAVE_LIBGPGME
      /* Set gpg signature verify on.
      */
   * and parsing the provided data into the context data.
  */
  int
 -fko_new_with_data(fko_ctx_t *r_ctx, const char *enc_msg, const char *dec_key)
 +fko_new_with_data(fko_ctx_t *r_ctx, const char *enc_msg,
 +    const char *dec_key, int encryption_mode)
  {
      fko_ctx_t   ctx;
      int         res = FKO_SUCCESS; /* Are we optimistic or what? */
          return(FKO_ERROR_MEMORY_ALLOCATION);
      }
  
 +    /* Default Encryption Mode (Rijndael in CBC mode)
 +    */
 +    ctx->initval = FKO_CTX_INITIALIZED;
 +    res = fko_set_spa_encryption_mode(ctx, encryption_mode);
 +    ctx->initval = 0;
 +    if(res != FKO_SUCCESS)
 +    {
 +        fko_destroy(ctx);
 +        return res;
 +    }
 +
      /* Consider it initialized here.
      */
      ctx->initval = FKO_CTX_INITIALIZED;
      FKO_SET_CTX_INITIALIZED(ctx);
  
 -    /* If a decryption password is provided, go ahead and decrypt and
 -     * decode.
 +    /* If a decryption key is provided, go ahead and decrypt and decode.
      */
      if(dec_key != NULL)
      {
@@@ -261,6 -238,9 +261,9 @@@ fko_destroy(fko_ctx_t ctx
          if(ctx->digest != NULL)
              free(ctx->digest);
  
+         if(ctx->raw_digest != NULL)
+             free(ctx->raw_digest);
          if(ctx->encoded_msg != NULL)
              free(ctx->encoded_msg);
  
diff --combined server/incoming_spa.c
@@@ -110,6 -110,64 +110,64 @@@ preprocess_spa_data(fko_srv_options_t *
      return(FKO_SUCCESS);
  }
  
+ /* For replay attack detection
+ */
+ static int
+ get_raw_digest(char **digest, char *pkt_data)
+ {
+     fko_ctx_t    ctx = NULL;
+     char        *tmp_digest = NULL;
+     int          res = FKO_SUCCESS;
+     /* initialize an FKO context with no decryption key just so
+      * we can get the outer message digest
+     */
+     res = fko_new_with_data(&ctx, (char *)pkt_data, NULL);
+     if(res != FKO_SUCCESS)
+     {
+         log_msg(LOG_WARNING, "Error initializing FKO context from SPA data: %s",
+             fko_errstr(res));
+         fko_destroy(ctx);
+         return(SPA_MSG_FKO_CTX_ERROR);
+     }
+     res = fko_set_raw_spa_digest_type(ctx, FKO_DEFAULT_DIGEST);
+     if(res != FKO_SUCCESS)
+     {
+         log_msg(LOG_WARNING, "Error setting digest type for SPA data: %s",
+             fko_errstr(res));
+         fko_destroy(ctx);
+         return(SPA_MSG_DIGEST_ERROR);
+     }
+     res = fko_set_raw_spa_digest(ctx);
+     if(res != FKO_SUCCESS)
+     {
+         log_msg(LOG_WARNING, "Error setting digest for SPA data: %s",
+             fko_errstr(res));
+         fko_destroy(ctx);
+         return(SPA_MSG_DIGEST_ERROR);
+     }
+     res = fko_get_raw_spa_digest(ctx, &tmp_digest);
+     if(res != FKO_SUCCESS)
+     {
+         log_msg(LOG_WARNING, "Error getting digest from SPA data: %s",
+             fko_errstr(res));
+         fko_destroy(ctx);
+         return(SPA_MSG_DIGEST_ERROR);
+     }
+     *digest = strdup(tmp_digest);
+     if (digest == NULL)
+         return SPA_MSG_ERROR;
+     fko_destroy(ctx);
+     return res;
+ }
  /* Popluate a spa_data struct from an initialized (and populated) FKO context.
  */
  static int
@@@ -152,6 -210,22 +210,22 @@@ get_spa_data_fields(fko_ctx_t ctx, spa_
      return(res);
  }
  
+ /* Check for access.conf stanza SOURCE match based on SPA packet
+  * source IP
+ */
+ static int
+ is_src_match(acc_stanza_t *acc, const uint32_t ip)
+ {
+     while (acc)
+     {
+         if(compare_addr_list(acc->source_list, ip))
+             return 1;
+         acc = acc->next;
+     }
+     return 0;
+ }
  /* Process the SPA packet data
  */
  void
@@@ -162,9 -236,10 +236,10 @@@ incoming_spa(fko_srv_options_t *opts
      */
      fko_ctx_t       ctx = NULL;
  
-     char            *spa_ip_demark, *gpg_id;
+     char            *spa_ip_demark, *gpg_id, *raw_digest = NULL;
      time_t          now_ts;
-     int             res, status, ts_diff, enc_type, found_acc_sip=0, stanza_num=0;
+     int             res, status, ts_diff, enc_type, stanza_num=0;
+     int             added_replay_digest = 0;
  
      spa_pkt_info_t *spa_pkt = &(opts->spa_pkt);
  
          return;
      }
  
+     if (is_src_match(opts->acc_stanzas, ntohl(spa_pkt->packet_src_ip)))
+     {
+         if(strncasecmp(opts->config[CONF_ENABLE_DIGEST_PERSISTENCE], "Y", 1) == 0)
+             /* Check for a replay attack
+             */
+             res = get_raw_digest(&raw_digest, (char *)spa_pkt->packet_data);
+             if(res != FKO_SUCCESS)
+             {
+                 if (raw_digest != NULL)
+                     free(raw_digest);
+                 return;
+             }
+             if (raw_digest == NULL)
+                 return;
+             if (is_replay(opts, raw_digest) != SPA_MSG_SUCCESS)
+                 return;
+     }
+     else
+     {
+         log_msg(LOG_WARNING,
+             "No access data found for source IP: %s", spadat.pkt_source_ip
+         );
+         return;
+     }
+     /* Now that we know there is a matching access.conf stanza and the
+      * incoming SPA packet is not a replay, see if we should grant any
+      * access
+     */
      while(acc)
      {
          stanza_num++;
              continue;
          }
  
-         found_acc_sip = 1;
          log_msg(LOG_INFO, "(stanza #%d) SPA Packet from IP: %s received with access source match",
              stanza_num, spadat.pkt_source_ip);
  
          if(enc_type == FKO_ENCRYPTION_RIJNDAEL)
          {
              if(acc->key != NULL)
 -                res = fko_new_with_data(&ctx, (char *)spa_pkt->packet_data, acc->key);
 +                res = fko_new_with_data(&ctx,
 +                    (char *)spa_pkt->packet_data, acc->key, acc->encryption_mode);
              else
              {
                  log_msg(LOG_ERR,
          else if(enc_type == FKO_ENCRYPTION_GPG)
          {
              /* For GPG we create the new context without decrypting on the fly
 -             * so we can set some  GPG parameters first.
 +             * so we can set some GPG parameters first.
              */
              if(acc->gpg_decrypt_pw != NULL)
              {
 -                res = fko_new_with_data(&ctx, (char *)spa_pkt->packet_data, NULL);
 +                res = fko_new_with_data(&ctx, (char *)spa_pkt->packet_data, NULL,
 +                        acc->encryption_mode);
                  if(res != FKO_SUCCESS)
                  {
                      log_msg(LOG_WARNING,
              continue;
          }
  
-         /* Do we have a valid FKO context?
+         /* Do we have a valid FKO context?  Did the SPA decrypt properly?
          */
          if(res != FKO_SUCCESS)
          {
              continue;
          }
  
+         /* Add this SPA packet into the replay detection cache
+         */
+         if (! added_replay_digest)
+         {
+             res = add_replay(opts, raw_digest);
+             if (res != SPA_MSG_SUCCESS)
+             {
+                 log_msg(LOG_WARNING, "(stanza #%d) Could not add digest to replay cache",
+                     stanza_num);
+                 if(ctx != NULL)
+                     fko_destroy(ctx);
+                 acc = acc->next;
+                 continue;
+             }
+             added_replay_digest = 1;
+         }
          /* At this point, we assume the SPA data is valid.  Now we need to see
           * if it meets our access criteria.
          */
              }
          }
  
-         /* Check for replays if so configured.
-         */
-         if(strncasecmp(opts->config[CONF_ENABLE_DIGEST_PERSISTENCE], "Y", 1) == 0)
-         {
-             res = replay_check(opts, ctx);
-             if(res != 0) /* non-zero means we have seen this packet before. */
-             {
-                 if(ctx != NULL)
-                     fko_destroy(ctx);
-                 acc = acc->next;
-                 continue;
-             }
-         }
          /* Populate our spa data struct for future reference.
          */
          res = get_spa_data_fields(ctx, &spadat);
          break;
      }
  
-     if(! found_acc_sip)
-     {
-         log_msg(LOG_WARNING,
-             "No access data found for source IP: %s", spadat.pkt_source_ip
-         );
-     }
+     if (raw_digest != NULL)
+         free(raw_digest);
  
      return;
  }
diff --combined test/test-fwknop.pl
@@@ -4,7 -4,6 +4,7 @@@ use File::Copy
  use File::Path;
  use IO::Socket;
  use Data::Dumper;
 +use Cwd;
  use Getopt::Long 'GetOptions';
  use strict;
  
@@@ -23,16 -22,12 +23,17 @@@ my $gpg_client_home_dir = "$conf_dir/cl
  my $nat_conf            = "$conf_dir/nat_fwknopd.conf";
  my $default_conf        = "$conf_dir/default_fwknopd.conf";
  my $default_access_conf = "$conf_dir/default_access.conf";
 +my $ecb_mode_access_conf = "$conf_dir/ecb_mode_access.conf";
 +my $ctr_mode_access_conf = "$conf_dir/ctr_mode_access.conf";
 +my $cfb_mode_access_conf = "$conf_dir/cfb_mode_access.conf";
 +my $ofb_mode_access_conf = "$conf_dir/ofb_mode_access.conf";
  my $expired_access_conf = "$conf_dir/expired_stanza_access.conf";
  my $future_expired_access_conf = "$conf_dir/future_expired_stanza_access.conf";
  my $expired_epoch_access_conf = "$conf_dir/expired_epoch_stanza_access.conf";
  my $invalid_expire_access_conf = "$conf_dir/invalid_expire_access.conf";
 +my $invalid_source_access_conf = "$conf_dir/invalid_source_access.conf";
  my $force_nat_access_conf = "$conf_dir/force_nat_access.conf";
+ my $dual_key_usage_access_conf = "$conf_dir/dual_key_usage_access.conf";
  my $gpg_access_conf     = "$conf_dir/gpg_access.conf";
  my $default_digest_file = "$run_dir/digest.cache";
  my $default_pid_file    = "$run_dir/fwknopd.pid";
@@@ -76,7 -71,11 +77,11 @@@ my $test_include = ''
  my @tests_to_include = ();
  my $test_exclude = '';
  my @tests_to_exclude = ();
+ my %valgrind_flagged_fcns = ();
+ my %valgrind_flagged_fcns_unique = ();
  my $list_mode = 0;
+ my $diff_dir1 = '';
+ my $diff_dir2 = '';
  my $loopback_intf = '';
  my $anonymize_results = 0;
  my $current_test_file = "$output_dir/init";
@@@ -87,9 -86,7 +92,9 @@@ my $valgrind_str = ''
  my $saved_last_results = 0;
  my $diff_mode = 0;
  my $enable_recompilation_warnings_check = 0;
 +my $enable_profile_coverage_check = 0;
  my $sudo_path = '';
 +my $gcov_path = '';
  my $platform = '';
  my $help = 0;
  my $YES = 1;
@@@ -119,11 -116,13 +124,14 @@@ exit 1 unless GetOptions
      'test-exclude=s'    => \$test_exclude,
      'exclude=s'         => \$test_exclude,  ### synonym
      'enable-recompile-check' => \$enable_recompilation_warnings_check,
 +    'enable-profile-coverage-check' => \$enable_profile_coverage_check,
      'List-mode'         => \$list_mode,
      'enable-valgrind'   => \$use_valgrind,
      'valgrind-path=s'   => \$valgrindCmd,
+     'output-dir=s'      => \$output_dir,
      'diff'              => \$diff_mode,
+     'diff-dir1=s'       => \$diff_dir1,
+     'diff-dir2=s'       => \$diff_dir2,
      'help'              => \$help
  );
  
@@@ -602,6 -601,25 +610,25 @@@ my @tests = 
      {
          'category' => 'Rijndael SPA',
          'subcategory' => 'client+server',
+         'detail'   => 'dual usage access key (tcp/80 http)',
+         'err_msg'  => 'could not complete SPA cycle',
+         'function' => \&spa_cycle,
+         'cmdline'  => "LD_LIBRARY_PATH=$lib_dir $valgrind_str " .
+             "$fwknopCmd -A tcp/80 -a $fake_ip -D $loopback_ip --get-key " .
+             "$local_key_file --verbose --verbose",
+         'fwknopd_cmdline'  => "LD_LIBRARY_PATH=$lib_dir $valgrind_str " .
+             "$fwknopdCmd -c $default_conf -a $dual_key_usage_access_conf " .
+             "-d $default_digest_file -p $default_pid_file $intf_str",
+         ### check for the first stanza that does not allow tcp/80 - the
+         ### second stanza allows this
+         'server_positive_output_matches' => [qr/stanza #1\)\sOne\sor\smore\srequested\sprotocol\/ports\swas\sdenied/],
+         'fw_rule_created' => $NEW_RULE_REQUIRED,
+         'fw_rule_removed' => $NEW_RULE_REMOVED,
+         'fatal'    => $NO
+     },
+     {
+         'category' => 'Rijndael SPA',
+         'subcategory' => 'client+server',
          'detail'   => 'packet aging (past) (tcp/22 ssh)',
          'err_msg'  => 'old SPA packet accepted',
          'function' => \&spa_cycle,
      {
          'category' => 'Rijndael SPA',
          'subcategory' => 'client+server',
 +        'detail'   => 'invalid SOURCE (tcp/22 ssh)',
 +        'err_msg'  => 'SPA packet accepted',
 +        'function' => \&spa_cycle,
 +        'cmdline'  => $default_client_args,
 +        'fwknopd_cmdline'  => "LD_LIBRARY_PATH=$lib_dir $valgrind_str " .
 +            "$fwknopdCmd -c $default_conf -a $invalid_source_access_conf " .
 +            "-d $default_digest_file -p $default_pid_file $intf_str",
 +        'server_positive_output_matches' => [qr/Fatal\serror\sparsing\sIP\sto\sint/],
 +        'fw_rule_created' => $REQUIRE_NO_NEW_RULE,
 +        'fatal'    => $NO
 +    },
 +    {
 +        'category' => 'Rijndael SPA',
 +        'subcategory' => 'client+server',
          'detail'   => 'expired stanza (tcp/22 ssh)',
          'err_msg'  => 'SPA packet accepted',
          'function' => \&spa_cycle,
          'server_conf' => $nat_conf,
          'fatal'    => $NO
      },
 +    {
 +        'category' => 'Rijndael SPA',
 +        'subcategory' => 'client+server',
 +        'detail'   => 'ECB mode (tcp/22 ssh)',
 +        'err_msg'  => 'could not complete SPA cycle',
 +        'function' => \&spa_cycle,
 +        'cmdline'  => "$default_client_args -M ecb",
 +        'fwknopd_cmdline'  => "LD_LIBRARY_PATH=$lib_dir $valgrind_str " .
 +            "$fwknopdCmd -c $default_conf -a $ecb_mode_access_conf " .
 +            "-d $default_digest_file -p $default_pid_file $intf_str",
 +        'server_negative_output_matches' => [qr/Decryption\sfailed/i],
 +        'fw_rule_created' => $NEW_RULE_REQUIRED,
 +        'fw_rule_removed' => $NEW_RULE_REMOVED,
 +        'fatal'    => $NO
 +    },
 +    {
 +        'category' => 'Rijndael SPA',
 +        'subcategory' => 'client+server',
 +        'detail'   => 'CFB mode (tcp/22 ssh)',
 +        'err_msg'  => 'could not complete SPA cycle',
 +        'function' => \&spa_cycle,
 +        'cmdline'  => "$default_client_args -M cfb",
 +        'fwknopd_cmdline'  => "LD_LIBRARY_PATH=$lib_dir $valgrind_str " .
 +            "$fwknopdCmd -c $default_conf -a $cfb_mode_access_conf " .
 +            "-d $default_digest_file -p $default_pid_file $intf_str",
 +        'server_negative_output_matches' => [qr/Decryption\sfailed/i],
 +        'fw_rule_created' => $NEW_RULE_REQUIRED,
 +        'fw_rule_removed' => $NEW_RULE_REMOVED,
 +        'fatal'    => $NO
 +    },
 +    {
 +        'category' => 'Rijndael SPA',
 +        'subcategory' => 'client+server',
 +        'detail'   => 'CTR mode (tcp/22 ssh)',
 +        'err_msg'  => 'could not complete SPA cycle',
 +        'function' => \&spa_cycle,
 +        'cmdline'  => "$default_client_args -M ctr",
 +        'fwknopd_cmdline'  => "LD_LIBRARY_PATH=$lib_dir $valgrind_str " .
 +            "$fwknopdCmd -c $default_conf -a $ctr_mode_access_conf " .
 +            "-d $default_digest_file -p $default_pid_file $intf_str",
 +        'server_negative_output_matches' => [qr/Decryption\sfailed/i],
 +        'fw_rule_created' => $NEW_RULE_REQUIRED,
 +        'fw_rule_removed' => $NEW_RULE_REMOVED,
 +        'fatal'    => $NO
 +    },
 +    {
 +        'category' => 'Rijndael SPA',
 +        'subcategory' => 'client+server',
 +        'detail'   => 'OFB mode (tcp/22 ssh)',
 +        'err_msg'  => 'could not complete SPA cycle',
 +        'function' => \&spa_cycle,
 +        'cmdline'  => "$default_client_args -M ofb",
 +        'fwknopd_cmdline'  => "LD_LIBRARY_PATH=$lib_dir $valgrind_str " .
 +            "$fwknopdCmd -c $default_conf -a $ofb_mode_access_conf " .
 +            "-d $default_digest_file -p $default_pid_file $intf_str",
 +        'server_negative_output_matches' => [qr/Decryption\sfailed/i],
 +        'fw_rule_created' => $NEW_RULE_REQUIRED,
 +        'fw_rule_removed' => $NEW_RULE_REMOVED,
 +        'fatal'    => $NO
 +    },
 +
 +    {
 +        'category' => 'Rijndael SPA',
 +        'subcategory' => 'client+server',
 +        'detail'   => 'mode mismatch (tcp/22 ssh)',
 +        'err_msg'  => 'server accepted mismatch enc mode',
 +        'function' => \&spa_cycle,
 +        'cmdline'  => "$default_client_args -M ecb",
 +        'fwknopd_cmdline'  => "LD_LIBRARY_PATH=$lib_dir $valgrind_str " .
 +            "$fwknopdCmd -c $default_conf -a $default_access_conf " .
 +            "-d $default_digest_file -p $default_pid_file $intf_str",
 +        'server_positive_output_matches' => [qr/Decryption\sfailed/i],
 +        'fw_rule_created' => $REQUIRE_NO_NEW_RULE,
 +        'fatal'    => $NO
 +    },
  
      {
          'category' => 'Rijndael SPA',
          'fwknopd_cmdline'  => $default_server_gpg_args,
          'fatal'    => $NO
      },
      {
          'category' => 'GnuPG (GPG) SPA',
          'subcategory' => 'server',
          'function' => \&digest_cache_structure,
          'fatal'    => $NO
      },
 +
 +    {
 +        'category' => 'profile coverage',
 +        'detail'   => 'gcov profile coverage',
 +        'err_msg'  => 'profile coverage failed',
 +        'function' => \&profile_coverage,
 +        'fatal'    => $NO
 +    },
  );
  
+ if ($use_valgrind) {
+     push @tests,
+         {
+             'category' => 'valgrind output',
+             'subcategory' => 'flagged functions',
+             'detail'   => '',
+             'err_msg'  => 'could not parse flagged functions',
+             'function' => \&parse_valgrind_flagged_functions,
+             'fatal'    => $NO
+         };
+ }
  my %test_keys = (
      'category'        => $REQUIRED,
      'subcategory'     => $OPTIONAL,
@@@ -1431,7 -1363,8 +1469,8 @@@ sub process_include_exclude() 
      if (@tests_to_include) {
          my $found = 0;
          for my $test (@tests_to_include) {
-             if ($msg =~ /$test/) {
+             if ($msg =~ /$test/ or ($use_valgrind
+                     and $msg =~ /valgrind\soutput/)) {
                  $found = 1;
                  last;
              }
  }
  
  sub diff_test_results() {
+     $diff_dir1 = "${output_dir}.last" unless $diff_dir2;
+     $diff_dir2 = $output_dir unless $diff_dir1;
      die "[*] Need results from a previous run before running --diff"
-         unless -d "${output_dir}.last";
-     die "[*] Current results set does not exist." unless -d $output_dir;
+         unless -d $diff_dir2;
+     die "[*] Current results set does not exist." unless -d $diff_dir1;
  
      my %current_tests  = ();
      my %previous_tests = ();
  
      ### Only diff results for matching tests (parse the logfile to see which
      ### test numbers match across the two test cycles).
-     &build_results_hash(\%current_tests, $output_dir);
-     &build_results_hash(\%previous_tests, "${output_dir}.last");
+     &build_results_hash(\%current_tests, $diff_dir1);
+     &build_results_hash(\%previous_tests, $diff_dir2);
  
      for my $test_msg (sort {$current_tests{$a}{'num'} <=> $current_tests{$b}{'num'}}
                  keys %current_tests) {
@@@ -1493,25 -1430,25 +1536,25 @@@ sub diff_results() 
      ### remove CMD timestamps
      my $cmd_search_re = qr/^\S+\s.*?\s\d{4}\sCMD\:/;
  
-     for my $file ("${output_dir}.last/${previous_num}.test",
-         "${output_dir}.last/${previous_num}_fwknopd.test",
-         "${output_dir}/${current_num}.test",
-         "${output_dir}/${current_num}_fwknopd.test",
+     for my $file ("$diff_dir1/${previous_num}.test",
+         "$diff_dir1/${previous_num}_fwknopd.test",
+         "$diff_dir2/${current_num}.test",
+         "$diff_dir2/${current_num}_fwknopd.test",
      ) {
          system qq{perl -p -i -e 's|$valgrind_search_re||' $file} if -e $file;
          system qq{perl -p -i -e 's|$cmd_search_re|CMD:|' $file} if -e $file;
      }
  
-     if (-e "${output_dir}.last/${previous_num}.test"
-             and -e "${output_dir}/${current_num}.test") {
-         system "diff -u ${output_dir}.last/${previous_num}.test " .
-             "${output_dir}/${current_num}.test";
+     if (-e "$diff_dir1/${previous_num}.test"
+             and -e "$diff_dir2/${current_num}.test") {
+         system "diff -u $diff_dir1/${previous_num}.test " .
+             "$diff_dir2/${current_num}.test";
      }
  
-     if (-e "${output_dir}.last/${previous_num}_fwknopd.test"
-             and -e "${output_dir}/${current_num}_fwknopd.test") {
-         system "diff -u ${output_dir}.last/${previous_num}_fwknopd.test " .
-             "${output_dir}/${current_num}_fwknopd.test";
+     if (-e "$diff_dir1/${previous_num}_fwknopd.test"
+             and -e "$diff_dir2/${current_num}_fwknopd.test") {
+         system "diff -u $diff_dir1/${previous_num}_fwknopd.test " .
+             "$diff_dir2/${current_num}_fwknopd.test";
      }
  
      return;
@@@ -1553,8 -1490,7 +1596,8 @@@ sub compile_warnings() 
  
      ### look for compilation warnings - something like:
      ###     warning: ‘test’ is used uninitialized in this function
 -    return 0 if &file_find_regex([qr/\swarning:\s/, qr/gcc\:.*\sunused/], $current_test_file);
 +    return 0 if &file_find_regex([qr/\swarning:\s/, qr/gcc\:.*\sunused/],
 +        $current_test_file);
  
      ### the new binaries should exist
      unless (-e $fwknopCmd and -x $fwknopCmd) {
      return 1;
  }
  
 +sub profile_coverage() {
 +
 +    ### check for any *.gcno files - if they don't exist, then fwknop was
 +    ### not compiled with profile support
 +    unless (glob('../client/*.gcno') and glob('../server/*.gcno')) {
 +        &write_test_file("[-] ../client/*.gcno and " .
 +            "../server/*.gcno files do not exist.\n", $current_test_file);
 +        return 0;
 +    }
 +
 +    my $curr_dir = getcwd() or die $!;
 +
 +    ### gcov -b ../client/*.gcno
 +    for my $dir ('../client', '../server', '../lib/.libs') {
 +        next unless -d $dir;
 +        chdir $dir or die $!;
 +        system "$gcov_path -b -u *.gcno > /dev/null 2>&1";
 +        chdir $curr_dir or die $!;
 +
 +        &run_cmd(qq|grep "called 0 returned" $dir/*.gcov|,
 +                $cmd_out_tmp, $current_test_file);
 +    }
 +
 +    return 1;
 +}
 +
  sub binary_exists() {
      my $test_hr = shift;
      return 0 unless $test_hr->{'binary'};
@@@ -2630,10 -2540,6 +2673,10 @@@ sub init() 
          push @tests_to_exclude, 'recompilation';
      }
  
 +    unless ($enable_profile_coverage_check) {
 +        push @tests_to_exclude, 'profile coverage';
 +    }
 +
      $sudo_path = &find_command('sudo');
  
      unless ((&find_command('cc') or &find_command('gcc')) and &find_command('make')) {
          push @tests_to_exclude, 'recompilation';
      }
  
 +    $gcov_path = &find_command('gcov');
 +
 +    if ($gcov_path) {
 +        if ($enable_profile_coverage_check) {
 +            for my $extension ('*.gcov', '*.gcda') {
 +                ### remove profile output from any previous run
 +                system qq{find .. -name $extension | xargs rm 2> /dev/null};
 +            }
 +        }
 +    } else {
 +        push @tests_to_exclude, 'profile coverage';
 +    }
 +
      open UNAME, "uname |" or die "[*] Could not execute uname: $!";
      while (<UNAME>) {
          if (/linux/i) {
@@@ -2722,6 -2615,42 +2765,42 @@@ sub identify_loopback_intf() 
      return;
  }
  
+ sub parse_valgrind_flagged_functions() {
+     for my $file (glob("$output_dir/*.test")) {
+         my $type = 'server';
+         $type = 'client' if $file =~ /\d\.test/;
+         open F, "< $file" or die $!;
+         while (<F>) {
+             ### ==30969==    by 0x4E3983A: fko_set_username (fko_user.c:65)
+             if (/^==.*\sby\s\S+\:\s(\S+)\s(.*)/) {
+                 $valgrind_flagged_fcns{$type}{"$1 $2"}++;
+                 $valgrind_flagged_fcns_unique{$type}{$1}++;
+             }
+         }
+         close F;
+     }
+     open F, ">> $current_test_file" or die $!;
+     for my $type ('client', 'server') {
+         print F "\n[+] fwknop $type functions (unique view):\n";
+         next unless defined $valgrind_flagged_fcns_unique{$type};
+         for my $fcn (sort {$valgrind_flagged_fcns_unique{$type}{$b}
+                 <=> $valgrind_flagged_fcns_unique{$type}{$a}}
+                 keys %{$valgrind_flagged_fcns_unique{$type}}) {
+             printf F "    %5d : %s\n", $valgrind_flagged_fcns_unique{$type}{$fcn}, $fcn;
+         }
+         print F "\n[+] fwknop $type functions (with call line numbers):\n";
+         for my $fcn (sort {$valgrind_flagged_fcns{$type}{$b}
+                 <=> $valgrind_flagged_fcns{$type}{$a}} keys %{$valgrind_flagged_fcns{$type}}) {
+             printf F "    %5d : %s\n", $valgrind_flagged_fcns{$type}{$fcn}, $fcn;
+         }
+         next unless defined $valgrind_flagged_fcns{$type};
+     }
+     close F;
+     return 1;
+ }
  sub is_fw_rule_active() {
      my $test_hr = shift;
  
@@@ -2839,5 -2768,42 +2918,42 @@@ sub logr() 
  }
  
  sub usage() {
-     return;
+     print <<_HELP_;
+ [+] $0 <options>
+     -A   --Anonymize-results      - Prepare anonymized results at:
+                                     $tarfile
+     --diff                        - Compare the results of one test run to
+                                     another.  By default this compares output
+                                     in ${output_dir}.last to $output_dir
+     --diff-dir1=<path>            - Left hand side of diff directory path,
+                                     default is: ${output_dir}.last
+     --diff-dir2=<path>            - Right hand side of diff directory path,
+                                     default is: $output_dir
+     --include=<regex>             - Specify a regex to be used over test
+                                     names that must match.
+     --exclude=<regex>             - Specify a regex to be used over test
+                                     names that must not match.
+     --enable-recompile            - Recompile fwknop sources and look for
+                                     compilation warnings.
+     --enable-valgrind             - Run every test underneath valgrind.
+     --List                        - List test names.
+     --loopback-intf=<intf>        - Specify loopback interface name (default
+                                     depends on the OS where the test suite
+                                     is executed).
+     --output-dir=<path>           - Path to output directory, default is:
+                                     $output_dir
+     --fwknop-path=<path>          - Path to fwknop binary, default is:
+                                     $fwknopCmd
+     --fwknopd-path=<path>         - Path to fwknopd binary, default is:
+                                     $fwknopdCmd
+     --libfko-path=<path>          - Path to libfko, default is:
+                                     $libfko_bin
+     --valgrind-path=<path>        - Path to valgrind, default is:
+                                     $valgrindCmd
+     -h   --help                   - Display usage on STDOUT and exit.
+ _HELP_
+     exit 0;
  }