symm/*-def.h: Overhaul encryption mode testing.
[catacomb] / symm / modes-test.c
diff --git a/symm/modes-test.c b/symm/modes-test.c
new file mode 100644 (file)
index 0000000..bbda53b
--- /dev/null
@@ -0,0 +1,539 @@
+/* -*-c-*-
+ *
+ * Common code for testing encryption modes
+ *
+ * (c) 2018 Straylight/Edgeware
+ */
+
+/*----- Licensing notice --------------------------------------------------*
+ *
+ * This file is part of Catacomb.
+ *
+ * Catacomb is free software: you can redistribute it and/or modify it
+ * under the terms of the GNU Library General Public License as published
+ * by the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * Catacomb 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
+ * Library General Public License for more details.
+ *
+ * You should have received a copy of the GNU Library General Public
+ * License along with Catacomb.  If not, write to the Free Software
+ * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307,
+ * USA.
+ */
+
+/*----- Header files ------------------------------------------------------*/
+
+#include <errno.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+
+#include <unistd.h>
+
+#include <mLib/alloc.h>
+#include <mLib/bits.h>
+#include <mLib/dstr.h>
+#include <mLib/quis.h>
+#include <mLib/report.h>
+
+#include "modes-test.h"
+
+/*----- The reference data ------------------------------------------------*/
+
+#ifdef SMALL_TEST
+static const octet text[] = "A small piece of text for testing encryption.";
+#else
+#define STORY "\
+Once upon a time there were a beautiful princess, a slightly nutty wizard,\n\
+and a watermelon.  Now, the watermelon had decided that it probably wasn't\n\
+going to get very far with the princess unless it did something pretty\n\
+drastic.  So it asked the wizard to turn it into a handsome prince.\n\
+\n\
+At least, this is the way that the wizard viewed the situation.  He might\n\
+have just hallucinated it all; those mushrooms had looked ever so nice.\n\
+\n\
+Back to the point.  The watermelon had expressed its desire not to be a\n\
+watermelon any more.  And the wizard was probably tripping something quite\n\
+powerful.  He hunted around a bit for his staff, and mumbled something\n\
+that film directors would think of as sounding appropriately arcane and\n\
+mystical (but was, in fact, just the ingredients list for an ancient\n\
+remedy for athlete's foot) and *pop*.  Cooked watermelon.  Yuk.\n\
+\n\
+Later in the year, the princess tripped over the hem of her dress, fell\n\
+down a spiral staircase, and died.  The king ordered dressmakers to attach\n\
+safety warnings to long dresses.\n\
+\n\
+And the wizard?         Who cares?\n\
+"
+static const octet text[] = STORY STORY;
+#endif
+
+#define TEXTSZ (sizeof(text))
+
+static const octet key[] = "Penguins rule OK, rhubarb cauliflower",
+  iv[] = "EdgewareCatacomb, parsley, sage, rosemary and thyme";
+
+/*----- Static variables --------------------------------------------------*/
+
+/* Encryption buffers, for ciphertext, recovered plaintext, and consistency
+ * reference.
+ */
+static octet ct[TEXTSZ], pt[TEXTSZ], ref[TEXTSZ];
+
+/* A resizeable buffer for verifying regression data. */
+static octet *t = 0; size_t tsz = 0;
+
+/*----- Diagnostic utilities ----------------------------------------------*/
+
+/* Print the @sz@-byte buffer @p@, beginning at offset @off@ within some
+ * larger buffer, marking block boundaries every @blksz@ bytes.
+ */
+static void hexdump(const octet *p, size_t sz, size_t off, size_t blksz)
+{
+  const octet *q = p + sz;
+  for (sz = 0; p < q; p++, sz++) {
+    printf("%02x", *p);
+    if ((off + sz + 1)%blksz == 0) putchar(':');
+  }
+}
+
+/* Print the buffer @p@, labelling it as @what@, splitting it into three
+ * pieces of sizes @sz0@, @sz1@, and @sz2@ respectively.  Block boundaries
+ * every @blksz@ bytes are shown consistency, independent of the split
+ * positions.
+ */
+static void dump_split(const char *what, size_t blksz, const octet *p,
+                      size_t sz0, size_t sz1, size_t sz2)
+{
+  printf("\t%-16s = ", what);
+  hexdump(p, sz0, 0, blksz);
+  if (sz1) { printf(", "); hexdump(p + sz0, sz1, sz0, blksz); }
+  if (sz2) { printf(", "); hexdump(p + sz0 + sz1, sz2, sz0 + sz1, blksz); }
+  fputc('\n', stdout);
+}
+
+/*----- Regression-data utilities -----------------------------------------*/
+
+/* Regression modes.  We can @CHECK@ existing data, @RECORD@ new data, or
+ * @IGNORE@ the regression testing entirely.
+ */
+enum { IGNORE, RECORD, CHECK };
+
+/* Read or write regression data from/to @fp@ according to @rmode@.  The data
+ * item is described as @what@ in diagnostic messages, and consists of @sz@
+ * bytes beginning at @p@.
+ *
+ * If @rmode@ is @IGNORE@, then this function does nothing; if @rmode@ is
+ * @RECORD@, then it writes @p@ to the output file with some framing; and if
+ * @rmode@ is @CHECK@ then it checks that the next chunk of data from the
+ * file matches @p@.
+ *
+ * Returns zero if all is well or @-1@ on a mismatch; I/O errors are fatal.
+ *
+ * Framing is trivial and consists of a 4-byte big-endian non-inclusive
+ * length prepended to each buffer.  No padding is written to maintain
+ * alignment.
+ */
+static int regress_data(int rmode, FILE *fp, const char *what,
+                       const void *p, size_t sz)
+{
+  octet b[4];
+  size_t psz;
+
+  switch (rmode) {
+    case IGNORE:
+      return (0);
+    case RECORD:
+      STORE32(b, sz);
+      if (!fwrite(b, 4, 1, fp) || !fwrite(p, sz, 1, fp))
+       die(1, "failed to write %s: %s", what, strerror(errno));
+      return (0);
+    case CHECK:
+      if (!fread(b, 4, 1, fp))
+       die(1, "failed to read %s length: %s", what,
+           ferror(fp) ? strerror(errno) : "unexpected eof");
+      psz = LOAD32(b);
+      if (psz != sz)
+       die(1, "incorrect %s length (%lu /= %lu; sync failure?)",
+           what, (unsigned long)psz, (unsigned long)sz);
+      if (tsz < sz) { xfree(t); t = xmalloc(sz); tsz = sz; }
+      if (!fread(t, sz, 1, fp))
+       die(1, "failed to read %s: %s", what,
+           ferror(fp) ? strerror(errno) : "unexpected eof");
+      if (memcmp(p, t, sz) != 0) return (-1);
+      return (0);
+    default:
+      abort();
+  }
+}
+
+/* Read or write framing data from/to @fp@ according to @rmode@.  The framing
+ * item is describd as @what@ in diagnostic messages, and consists of @sz@
+ * bytes beginning at @p@.
+ *
+ * Framing data is used to verify that a recorded regression-data file is
+ * still appropriate for use.  A fatal error is reported on any kind of
+ * failure.
+ */
+static void regress_framing(int rmode, FILE *fp, const char *what,
+                           const void *p, size_t sz)
+{
+  if (regress_data(rmode, fp, what, p, sz))
+    die(1, "regression framing mismatch for %s (bug, or wrong file)", what);
+}
+
+/* Read or write crypto data from/to @fp@ according to @rmode@.  The data
+ * item is describd as @what@ in diagnostic messages, and consists of the
+ * bytes beginning at @p@.  For the purposes of diagnostics, this buffer has
+ * been notionally split into three pieces, with sizes @sz0@, @sz1@, and
+ * @sz2@, respectively.
+ *
+ * If al is well, return zero.  If the crypto data doesn't match the recorded
+ * regression data, then report the mismatch, showing the way in which the
+ * buffer is split, and return -1.  I/O errors are fatal.
+ */
+static int regress_crypto(int rmode, FILE *fp, const char *what, size_t blksz,
+                         const void *p, size_t sz0, size_t sz1, size_t sz2)
+{
+  int rc;
+
+  rc = regress_data(rmode, fp, what, p, sz0 + sz1 + sz2);
+  if (rc) {
+    printf("\nRegression mismatch (split = %lu/%lu/%lu)\n",
+          (unsigned long)sz0, (unsigned long)sz1, (unsigned long)sz2);
+    dump_split("plaintext", blksz, text, sz0, sz1, sz2);
+    dump_split("expected ct", blksz, t, sz0, sz1, sz2);
+    dump_split("computed ct", blksz, p, sz0, sz1, sz2);
+    fputc('\n', stdout);
+  }
+  return (rc);
+}
+
+/*----- Selecting fragment sizes ------------------------------------------*/
+
+/* Return codes from @step@. */
+enum { STEP, LIMIT, RESET };
+
+/* Update @*sz_inout@ the next largest suitable fragment size, up to a
+ * maximum of @max@.
+ *
+ * If the new size is still smaller than the maximum, then return @STEP@.  If
+ * the size is maximal, then return @LIMIT@.  If the size was previously
+ * maximal already, then return @RESET@.
+ *
+ * The sizes here are selected powers of two, and powers of two plus or minus
+ * 1, with the objective of testing how internal buffering is affected when
+ * the cursor is misaligned and realigned with block boundaries.
+ */
+static int step(size_t *sz_inout, size_t max)
+{
+  size_t i;
+
+  static size_t steps[] = {   1,   7,   8,   9,  15,  16,  17,
+                                 63,  64,  65, 255, 256, 257 };
+
+  if (*sz_inout == max) return (RESET);
+  for (i = 0; i < N(steps); i++)
+    if (steps[i] > *sz_inout) {
+      if (steps[i] < max) { *sz_inout = steps[i]; return (STEP); }
+      else break;
+    }
+  *sz_inout = max; return (LIMIT);
+}
+
+/*----- Main code ---------------------------------------------------------*/
+
+/* --- @test_encmode@ --- *
+ *
+ * Arguments:  @const char *name@ = name of the encryption scheme; used to
+ *                     find the regression-test filename
+ *             @size_t ksz@ = key size to use, or zero for `don't care'
+ *             @size_t blksz@ = block size
+ *             @size_t minsz@ = smallest acceptable buffer size, or 1
+ *             @unsigned f@ = various additional flags
+ *             @setupfn *setup@ = key-setup function
+ *             @resetfn *reset@ = state-reset function
+ *             @encfn *enc@ = encryption function
+ *             @decfn *dec@ = decryption function
+ *             @int argc@ = number of command-line arguments
+ *             @char *argv@ = pointer to command-line argument vector
+ *
+ * Returns:    Zero on success, nonzero to report failure.
+ *
+ * Use:                Tests an encryption mode which doesn't have any more formal
+ *             test vectors.
+ *
+ *             The @name@ is used firstly in diagnostic output and secondly
+ *             to form the default filename to use for regression-test data,
+ *             as `.../symm/t/modes/NAME.regress'.
+ *
+ *             The key size @ksz@ is simply passed on back to the @setup@
+ *             function, unless the caller passes in zero, in which case
+ *             @test_encmode@ chooses a key size for itself.
+ *
+ *             The block size @blksz@ is used in failure reports, to draw
+ *             attention to the block structure in the various buffers,
+ *             which may assist with diagnosis.  It's also used to determine
+ *             when to apply a consistency check: see below regarding the
+ *             @TEMF_REFALIGN@ flag.
+ *
+ *             The minimum buffer size @minsz@ expresses a limitation on the
+ *             provided @enc@ and @dec@ functions, that they don't work on
+ *             inputs smaller than @minsz@; accordingly, @test_encmode@ will
+ *             not test such small sizes.  This should be 1 if the mode has
+ *             no limitation.
+ *
+ *             The flags @f@ influence testing in various ways explained
+ *             below.
+ *
+ *             The caller-provided functions are assumed to act on some
+ *             global but hidden state,
+ *
+ *               * @setup@ is (currently, at least) called only once, with
+ *                 the key @k@ and its chosen size @ksz@.
+ *
+ *               * @reset@ is called at the start of each encryption or
+ *                 decryption operation, to program in the initialization
+ *                 vector to use.  Currently, the same IV is used in all of
+ *                 the tests, but this might not always be the case.
+ *
+ *               * @enc@ is called to encrypt a source buffer @s@ and write
+ *                 the ciphertext to a destination @d@; @sz@ is the common
+ *                 size of these buffers.
+ *
+ *               * @dec@ is called to decrypt a source buffer @s@ and write
+ *                 the recovered plaintext to a destination @d@; @sz@ is the
+ *                 common size of these buffers.
+ *
+ *             Finally, @int argc@ and @char *argv@ are the command-line
+ *             arguments provided to @main@; @test_encmode@ parses these and
+ *             alters its behaviour accordingly.
+ *
+ *             Currently, @test_encmode@'s tests are built around a single,
+ *             fairly large, fixed message.  In each test step, the message
+ *             is split into a number of fragments which are encrypted and
+ *             decrypted in turn.
+ *
+ *             The following tests are performed.
+ *
+ *               * The fundamental `round-trip' test, which verifies that
+ *                 the message can be encrypted and then decrypted
+ *                 successfully, if the same fragment boundaries are used in
+ *                 both cases.
+ *
+ *               * A `consistency' test.  Some modes, such as CFB, OFB, and
+ *                 counter, are `resumable': encryption operations are
+ *                 insensitive to the position of fragment boundaries, so a
+ *                 single message can be broken into fragments without
+ *                 affecting the result.  If @TEMF_REFALIGN@ is clear then
+ *                 the mode under test is verified to have this property.
+ *                 If @TEMF_REFALIGN' is set, a weaker property is verified:
+ *                 that encryption is insensitive to the position of
+ *                 /block-aligned/ fragment boundaries only.
+ *
+ *               * A `regression' test, which verifies that the code
+ *                 produces the same ciphertext as a previous version.  By
+ *                 setting command-line arguments appropriately, a test
+ *                 program can be told to record ciphertexts in a (binary)
+ *                 data file.  Usually, instead, the program will read the
+ *                 recorded ciphertexts back and verify that it produces the
+ *                 same data.  For resumable modes, it's only necessary to
+ *                 record single ciphertext, since all the other ciphertexts
+ *                 must be equal by consistency; otherwise all non-block-
+ *                 aligned splits are recorded separately.
+ */
+
+int test_encmode(const char *name,
+                size_t ksz, size_t blksz, size_t minsz, unsigned f,
+                setupfn *setup, resetfn *reset, encfn *enc, encfn *dec,
+                int argc, char *argv[])
+{
+  int ok = 1, refp = 0, i;
+  size_t sz0, sz1, sz2;
+  const char spinner[] = "/-\\|";
+  int rmode = CHECK, spin = isatty(STDOUT_FILENO) ? 0 : -1;
+  int regr;
+  const char *rname = 0, *p;
+  FILE *fp;
+  dstr d = DSTR_INIT;
+
+  ego(argv[0]);
+
+  /* Parse the command-line options. */
+  p = 0; i = 1;
+  for (;;) {
+
+    /* Read the next argument. */
+    if (!p || !*p) {
+      if (i >= argc) break;
+      p = argv[i++];
+      if (strcmp(p, "--") == 0) break;
+      if (p[0] != '-' || p[1] == 0) { i--; break; }
+      p++;
+    }
+
+    /* Interpret an option. */
+    switch (*p++) {
+      case 'h':
+       printf("%s test driver\n"
+              "Usage: %s [-i] [-o|-f FILENAME]\n", QUIS, QUIS);
+       exit(0);
+      case 'i':
+       rmode = IGNORE;
+       break;
+      case 'o':
+       if (!*p) {
+         if (i >= argc) die(1, "option `-o' expects an argument");
+         p = argv[i++];
+       }
+       rmode = RECORD; rname = p; p = 0;
+       break;
+      case 'f':
+       if (!*p) {
+         if (i >= argc) die(1, "option `-f' expects an argument");
+         p = argv[i++];
+       }
+       rmode = CHECK; rname = p; p = 0;
+       break;
+      default:
+       die(1, "option `-%c' unknown", p[-1]);
+    }
+  }
+
+  /* Check there's nothing else left. */
+  if (i < argc) die(1, "trailing junk on command line");
+
+  /* Open the regression-data file. */
+  if (rmode == IGNORE)
+    fp = 0;
+  else {
+    if (!rname) {
+      DRESET(&d); dstr_putf(&d, SRCDIR"/t/modes/%s.regress", name);
+      rname = xstrdup(d.buf);
+    }
+    fp = fopen(rname, rmode == RECORD ? "wb" : "rb");
+    if (!fp)
+      die(1, "failed to open `%s' for %s: %s", rname,
+         rmode == RECORD ? "writing" : "reading", strerror(errno));
+  }
+
+  /* Write a header describing the file, to trap misuse for the wrong mode,
+   * and changes in the text.
+   */
+  DRESET(&d);
+  dstr_putf(&d, "mode=%s, text=%lu", name, (unsigned long)TEXTSZ);
+  regress_framing(rmode, fp, "header", d.buf, d.len);
+
+  /* Start things up. */
+  printf("%s: ", name);
+  setup(key, ksz ? ksz: sizeof(key));
+
+  /* Work through various sizes of up to three fragments.  The middle
+   * fragment is the important one, since it can be misaligned or not at
+   * either end.
+   */
+  sz0 = sz1 = minsz;
+  for (;;) {
+
+    /* If output is to a terminal then display a spinner to keep humans
+     * amused.
+     */
+    if (spin >= 0) {
+      printf("\r%s: [%c]\b\b", name, spinner[spin]); fflush(stdout);
+      spin = (spin + 1)&3;
+    }
+
+    /* Prepare for the test. */
+    sz2 = TEXTSZ - sz1 - sz0;
+    ok = 1;
+
+    /* Encrypt the three fragments. */
+    reset(iv);
+    enc(text, ct, sz0);
+    if (sz1) {
+      memcpy(ct + sz0, text + sz0, sz1);
+      enc(ct + sz0, ct + sz0, sz1);
+    }
+    if (sz2)
+      enc(text + sz0 + sz1, ct + sz0 + sz1, sz2);
+
+    /* Try to check consistency.  We can't do this if (a) the mode is
+     * non-resumable and the fragments sizes are misaligned, or (b) this is
+     * our first pass through and we don't have a consistency reference yet.
+     *
+     * Also, decide whether to deploy the regression test, which we do if and
+     * only if we can't compare against the consistency reference.
+     */
+    regr = 0;
+    if ((f&TEMF_REFALIGN) && (sz0%blksz || sz1%blksz)) regr = 1;
+    else if (!refp) { memcpy(ref, ct, TEXTSZ); regr = 1; refp = 1; }
+    else if (memcmp(ref, ct, TEXTSZ) != 0) {
+      ok = 0;
+      printf("\nConsistency failure (split = %lu/%lu/%lu)\n",
+            (unsigned long)sz0, (unsigned long)sz1, (unsigned long)sz2);
+      dump_split("plaintext", blksz, text, sz0, sz1, sz2);
+      dump_split("reference", blksz, ref, sz0, sz1, sz2);
+      dump_split("ciphertext", blksz, ct, sz0, sz1, sz2);
+      fputc('\n', stdout);
+    }
+
+    /* If we need the regression test then do that.  Write a framing record
+     * to avoid confusion if the policy changes.
+     */
+    if (regr) {
+      DRESET(&d);
+      dstr_putf(&d, "split = %lu/%lu/%lu",
+               (unsigned long)sz0, (unsigned long)sz1, (unsigned long)sz2);
+      regress_framing(rmode, fp, "split", d.buf, d.len);
+      if (regress_crypto(rmode, fp, "regress", blksz, ct, sz0, sz1, sz2))
+       ok = 0;
+    }
+
+    /* Finally, decrypt and check that the round-trip works. */
+    reset(iv);
+    dec(ct, pt, sz0);
+    if (sz1) {
+      memcpy(pt + sz0, ct + sz0, sz1);
+      dec(pt + sz0, pt + sz0, sz1);
+    }
+    if (sz2)
+      dec(ct + sz0 + sz1, pt + sz0 + sz1, sz2);
+    if (memcmp(text, pt, TEXTSZ) != 0) {
+      ok = 0;
+      printf("\nRound-trip failure (split = %lu/%lu/%lu)\n",
+            (unsigned long)sz0, (unsigned long)sz1, (unsigned long)sz2);
+      dump_split("plaintext", blksz, text, sz0, sz1, sz2);
+      dump_split("ciphertext", blksz, ct, sz0, sz1, sz2);
+      dump_split("recovered", blksz, pt, sz0, sz1, sz2);
+      fputc('\n', stdout);
+    }
+
+    /* Update the fragment sizes. */
+    if (!sz1) break;
+    if (step(&sz1, TEXTSZ - sz0) == RESET) {
+      if (step(&sz0, TEXTSZ) == LIMIT) sz1 = 0;
+      else sz1 = minsz;
+    }
+  }
+
+  /* Close the regression data file. */
+  if (fp && (ferror(fp) || fclose(fp)))
+    die(1, "error closing `%s': %s", rname, strerror(errno));
+
+  /* Finish off the eyecandy spinner. */
+  if (spin >= 0) printf("\r%s: [%c] ", name, ok ? '*' : 'X');
+
+  /* Summarize the test result. */
+  if (ok) printf("ok\n");
+  else printf("failed\n");
+
+  /* And we're done. */
+  dstr_destroy(&d);
+  return (!ok);
+}
+
+/*----- That's all, folks -------------------------------------------------*/