symm/strobe.c: Implement Hamburg's STROBE framework.
[catacomb] / symm / strobe.c
diff --git a/symm/strobe.c b/symm/strobe.c
new file mode 100644 (file)
index 0000000..8d4d42d
--- /dev/null
@@ -0,0 +1,694 @@
+/* -*-c-*-
+ *
+ * The STROBE protocol framework
+ *
+ * (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 <assert.h>
+#include <ctype.h>
+#include <string.h>
+
+#include <mLib/buf.h>
+
+#include "keccak1600.h"
+#include "strobe.h"
+
+/*----- Magic constants ---------------------------------------------------*/
+
+#define DDATA 0x04
+#define DRATE 0x80
+
+/*----- Utilities ---------------------------------------------------------*/
+
+/* --- @crank@ --- *
+ *
+ * Arguments:  @strobe_ctx *ctx@ = pointer to context block to initialize
+ *
+ * Returns:    ---
+ *
+ * Use:                Cycle the Keccak-p[1600, n] duplex function.
+ */
+
+static void crank(strobe_ctx *ctx)
+{
+  kludge64 t[25];
+  octet *p;
+  unsigned i;
+
+  /* Ensure that we've not overstepped the rate bound. */
+  assert(ctx->n <= ctx->r - 2);
+
+  /* Apply the cSHAKE and rate padding. */
+  ctx->buf[ctx->n] ^= ctx->n0;
+  ctx->buf[ctx->n + 1] ^= DDATA;
+  ctx->buf[ctx->r - 1] ^= DRATE;
+
+  /* Cycle the sponge. */
+  for (i = 0, p = ctx->buf; i < ctx->r/8; i++)
+    { LOAD64_L_(t[i], p); p += 8; }
+  keccak1600_set(&ctx->k, t, ctx->r/8);
+  keccak1600_p(&ctx->k, &ctx->k, 24);
+  keccak1600_extract(&ctx->k, t, ctx->r/8);
+  for (i = 0, p = ctx->buf; i < ctx->r/8; i++)
+    { STORE64_L_(p, t[i]); p += 8; }
+
+  /* Restart at the beginning of the buffer, and note this as a
+   * continuation.
+   */
+  ctx->n = ctx->n0 = 0;
+}
+
+/* --- @xorbuf@ --- *
+ *
+ * Arguments:  @octet *z@ = pointer to output buffer
+ *             @const octet *x, *y@ = pointer to input buffers
+ *             @size_t sz@ = common buffer length
+ *
+ * Returns:    ---
+ *
+ * Use:                Store the bytewise XOR of the buffers @x@ and @y@ in @z@.
+ *             The @x@ and @y@ may be equal, but otherwise the buffers must
+ *             not overlap.
+ */
+
+static void xorbuf(octet *z, const octet *x, const octet *y, size_t sz)
+  { size_t i; for (i = 0; i < sz; i++) *z++ = *x++ ^ *y++; }
+
+/* --- @nonzerop@ --- *
+ *
+ * Arguments:  @const octet *x@ = pointer to input buffer
+ *             @size_t sz@ = buffer length
+ *
+ * Returns:    ---
+ *
+ * Use:                If any byte of @x@ is nonzero, then return a nonzero value
+ *             between 1 and 255 inclusive; otherwise return zero.
+ */
+
+static unsigned nonzerop(const octet *x, size_t sz)
+{
+  unsigned z = 0;
+  size_t i;
+
+  for (i = 0; i < sz; i++) z |= *x++;
+  return (z);
+}
+
+/* --- @unequalp@ --- *
+ *
+ * Arguments:  @const octet *x, *y@ = pointer to input buffers
+ *             @size_t sz@ = common buffer length
+ *
+ * Returns:    ---
+ *
+ * Use:                If any respective bytes of @x@ and @y@ are unequal, then
+ *             return a nonzero value between 1 and 255 inclusive; otherwise
+ *             return zero.
+ */
+
+static unsigned unequalp(const octet *x, const octet *y, size_t sz)
+{
+  unsigned z = 0;
+  size_t i;
+
+  for (i = 0; i < sz; i++) z |= *x++ ^ *y++;
+  return (z);
+}
+
+/* --- @process_buffer@ --- *
+ *
+ * Arguments:  @strobe_ctx *ctx@ = pointer to context block
+ *             @const octet *p@ = pointer to input buffer
+ *             @octet *q@ = pointer to output buffer
+ *             @size_t sz@ = common buffer length
+ *
+ * Returns:    ---
+ *
+ * Use:                Process a portion of a STROBE input small enough to be
+ *             satisfied from the internal buffer.
+ */
+
+static void process_buffer(strobe_ctx *ctx,
+                          const octet *p, octet *q, size_t sz)
+{
+  octet *b = ctx->buf + ctx->n;
+  unsigned z = 0;
+
+  if (!(ctx->f&STRBF_CRYPTO)) {
+    /* No crypto to do.  The `output' would be equal to the input, so that's
+     * rather uninteresting (and, indeed, forbidden).  If there's input, then
+     * mix it into the state.
+     */
+
+    if (p && (ctx->f&STRBF_VRFOUT)) z |= nonzerop(p, sz);
+    if (p) xorbuf(b, b, p, sz);
+  } else if (!(ctx->f&STRBF_MIXOUT)) {
+    /* Mix the input into the sponge state.  That means that the new state
+     * will be equal to the output.
+     */
+
+    if (p) xorbuf(b, b, p, sz);
+    if (ctx->f&STRBF_VRFOUT) z |= nonzerop(b, sz);
+    if (q) memcpy(q, b, sz);
+  } else if (p) {
+    /* Mix the output into the sponge state, so the new state will in fact be
+     * equal to the input.  If the input and output buffers are equal then we
+     * have a dance to do.
+     */
+
+    if (!q) {
+      if (ctx->f&STRBF_VRFOUT) z |= unequalp(p, b, sz);
+      memcpy(b, p, sz);
+    } else {
+      xorbuf(q, p, b, sz);
+      if (q != p) memcpy(b, p, sz);
+      else xorbuf(b, b, q, sz);
+      if (ctx->f&STRBF_VRFOUT) z |= nonzerop(q, sz);
+    }
+  } else {
+    /* As above, only the input is hardwired to zero.  That means that we
+     * copy state bytes to the output (if any), and just clobber the state
+     * when we're done.
+     */
+
+    if (q) memcpy(q, b, sz);
+    memset(b, 0, sz);
+  }
+
+  /* Set the @STRBF_NZERO@ flag if @z@ is nonzero.  If @z@ is zero then
+   * subtracting one will set all of its bits, so, in particular, bits
+   * 8--15.  Otherwise, @z@ is between 1 and 255, so bits 8--15 are clear and
+   * will remain so when we subtract one.
+   */
+  if (ctx->f&STRBF_VRFOUT) ctx->f |= ((z - 1)&STRBF_NZERO) ^ STRBF_NZERO;
+
+  /* Update the buffer cursor. */
+  ctx->n += sz;
+}
+
+/*----- Interface ---------------------------------------------------------*/
+
+/* --- @strobe_init@ --- *
+ *
+ * Arguments:  @strobe_ctx *ctx@ = pointer to context block to initialize
+ *             @unsigned lambda@ = security parameter, in bits (must be a
+ *                     multiple of 32)
+ *
+ * Returns:    ---
+ *
+ * Use:                Initialize a STROBE context for use.
+ */
+
+void strobe_init(strobe_ctx *ctx, unsigned lambda)
+{
+  const char v[] = "STROBEv1.0.2";
+  kludge64 t[25];
+  octet *p;
+  buf b;
+  unsigned n, i;
+
+  /* Check the security parameter. */
+  assert(lambda%32 == 0); assert(lambda <= 704);
+  ctx->r = (1600 - 2*lambda)/8;
+
+  /* Set up the initial cSHAKE framing. */
+  buf_init(&b, ctx->buf, ctx->r);
+  buf_putu8(&b, 1); buf_putu8(&b, ctx->r);
+  buf_putu8(&b, 1); buf_putu8(&b, 0);
+  buf_putu8(&b, 1); buf_putu8(&b, 8*(sizeof(v) - 1));
+  buf_put(&b, v, sizeof(v) - 1);
+  assert(BOK(&b));
+  n = BLEN(&b); if (n%8) memset(ctx->buf + n, 0, 8 - n%8);
+
+  /* Cycle the sponge once initially, and get the first output buffer. */
+  keccak1600_init(&ctx->k);
+  for (i = 0, p = ctx->buf; i < (n + 7)/8; i++)
+    { LOAD64_L_(t[i], p); p += 8; }
+  keccak1600_set(&ctx->k, t, (n + 7)/8);
+  keccak1600_p(&ctx->k, &ctx->k, 24);
+  keccak1600_extract(&ctx->k, t, ctx->r/8);
+  for (i = 0, p = ctx->buf; i < ctx->r/8; i++)
+    { STORE64_L_(p, t[i]); p += 8; }
+
+  /* Initialize the other parts of the state. */
+  ctx->n = ctx->n0 = 0; ctx->f = 0;
+}
+
+/* --- @strobe_begin@ --- *
+ *
+ * Arguments:  @strobe_ctx *ctx@ = pointer to context block
+ *             @unsigned op@ = bitmask of flags
+ *
+ * Returns:    ---
+ *
+ * Use:                Begin a STROBE operation.  The flags determine the behaviour
+ *             of the @strobe_process@ and @strobe_done@ functions.
+ *
+ *               * The @I@ bit determines the primary direction of data
+ *                 movement.  If it's clear, data comes from the application
+ *                 into STROBE.  If it's set, data comes from STROBE towards
+ *                 the application.
+ *
+ *               * The @C@ bit activates cryptographic processing.  If it's
+ *                 clear, then the input and output data would be equal, so
+ *                 @dest@ must be null.  If it's set, then input data is
+ *                 XORed with the keystream on its way to the output.
+ *
+ *               * The @A@ bit determines whether the application is
+ *                 engaged.  If it's set, then the input or output buffer
+ *                 (according to whether @I@ is clear or set, respectively)
+ *                 holds the application data.  If it's clear, and @I@ is
+ *                 clear, then zero bytes are fed in; if @I@ is set, then
+ *                 the output is compared with zero, and @strobe_done@
+ *                 reports the outcome of this comparison.
+ *
+ *               * The @T@ bit determines whether the transport is engaged.
+ *                 If it's set, then the input or output buffer (according
+ *                 to whether @I@ is set or clear, respectively) holds
+ *                 transport data.  If it's clear, and @I@ is set, then zero
+ *                 bytes are fed in; if @I@ is clear, then the output is
+ *                 discarded.
+ *
+ *               * The @M@ bit marks the data as metadata, but has no other
+ *                 effect.
+ */
+
+void strobe_begin(strobe_ctx *ctx, unsigned op)
+{
+  /* Preliminary checking.  We shouldn't have an operation underway, and the
+   * operation shouldn't have reserved bits set.
+   */
+  assert(!(ctx->f&STRBF_ACTIVE)); assert(!(op&~STRBF_VALIDMASK));
+
+  /* Reset our operation state. */
+  ctx->f &= STRBF_STMASK;
+
+  /* Operation framing.  Chain back to the start of the previous frame and
+   * write the new operation code.  Set the sticky asymmetry bit here if
+   * necessary.
+   */
+  ctx->buf[ctx->n++] ^= ctx->n0; ctx->n0 = ctx->n;
+  if (ctx->n >= ctx->r - 2) crank(ctx);
+  if (!(op&STRBF_T))
+    ctx->buf[ctx->n++] ^= U8(op);
+  else {
+    if (!(ctx->f&STRBF_INIT)) ctx->f |= STRBF_INIT | (op&STRBF_I);
+    ctx->buf[ctx->n++] ^= U8(op ^ ctx->f);
+  }
+  if (ctx->n >= ctx->r - 2 || (op&STRBF_C)) crank(ctx);
+
+  /* The operation is now underway. */
+  ctx->f |= STRBF_ACTIVE;
+
+  /* Determine whether we expect input and/or output. */
+  if (op&(op&STRBF_I ? STRBF_T : STRBF_A))
+    ctx->f |= STRBF_WANTIN;
+  if ((op&STRBF_C) && op&(op&STRBF_I ? STRBF_A : STRBF_T))
+    ctx->f |= STRBF_WANTOUT;
+
+  /* Determine whether the keystream is engaged, and how it fits in. */
+  if (op&STRBF_C) {
+    ctx->f |= STRBF_CRYPTO;
+    if ((op&(STRBF_I | STRBF_T)) != STRBF_T) ctx->f |= STRBF_MIXOUT;
+  }
+
+  /* Determine whether the output is supposed to be all-bytes-zero. */
+  if ((op&(STRBF_I | STRBF_A | STRBF_T)) == (STRBF_I | STRBF_T))
+    ctx->f |= STRBF_VRFOUT;
+
+  /* The operation is now underway. */
+  ctx->f |= STRBF_ACTIVE;
+}
+
+/* --- @strobe_process@ --- *
+ *
+ * Arguments:  @strobe_ctx *ctx@ = pointer to context block
+ *             @const void *src@ = pointer to input data, or null
+ *             @void *dest@ = pointer to output data, or null
+ *             @size_t sz@ = common buffer length
+ *
+ * Returns:    ---
+ *
+ * Use:                Process data through the active STROBE operation.  The exact
+ *             behaviour depends on the flags passed to @strobe_begin@; see
+ *             that function for details.  If @src@ is null, then the
+ *             behaviour is as if the input consists of @sz@ zero bytes.  If
+ *             @dest@ in null, then the output is discarded.
+ */
+
+void strobe_process(strobe_ctx *ctx, const void *src, void *dest, size_t sz)
+{
+  const octet *p = src; octet *q = dest;
+  unsigned spare;
+
+  /* Make sure that things are set up properly. */
+  assert(ctx->f&STRBF_ACTIVE);
+  if (!(ctx->f&STRBF_WANTIN)) assert(!src);
+  if (!(ctx->f&STRBF_WANTOUT)) assert(!dest);
+
+  /* Work through the input. */
+  spare = ctx->r - ctx->n - 2;
+  if (sz < spare)
+    { process_buffer(ctx, p, q, sz); return; }
+  if (ctx->n) {
+    process_buffer(ctx, p, q, spare); crank(ctx);
+    if (p) { p += spare; }
+    if (q) { q += spare; }
+    sz -= spare;
+  }
+
+  while (sz >= ctx->r - 2) {
+    process_buffer(ctx, p, q, ctx->r - 2); crank(ctx);
+    if (p) { p += ctx->r - 2; }
+    if (q) { q += ctx->r - 2; }
+    sz -= ctx->r - 2;
+  }
+  if (sz) process_buffer(ctx, p, q, sz);
+}
+
+/* --- @strobe_done@ --- *
+ *
+ * Arguments:  @strobe_ctx *ctx@ = pointer to context block
+ *
+ * Returns:    Zero on success; @-1@ on verification failure (if @I@ and @T@
+ *                     are set and @A@ is clear)
+ *
+ * Use:                Concludes a STROBE operation, returning the result.
+ */
+
+int strobe_done(strobe_ctx *ctx)
+{
+  assert(ctx->f&STRBF_ACTIVE); ctx->f &= ~STRBF_ACTIVE;
+  if (ctx->f&STRBF_VRFOUT) return (-(int)((ctx->f/STRBF_NZERO)&1u));
+  else return (0);
+}
+
+/* --- @strobe_key@, @strobe_ad@, @strobe_@prf@, @strobe_clrout@,
+ *     @strobe_clrin@, @strobe_encout@, @strobe_encin@, @strobe_macout@,
+ *     @strobe_macin@, @strobe_ratchet@ --- *
+ *
+ * Arguments:  @strobe_ctx *ctx@ = pointer to context block
+ *
+ * Returns:    @strobe_macin@ returns zero on success, or @-1@ on
+ *                     verification failure
+ *
+ * Use:                Perform a STROBE operation on a single buffer.
+ */
+
+static int op(strobe_ctx *ctx, unsigned f0, unsigned f1,
+             const void *src, void *dest, size_t sz)
+{
+  assert(!(f1&~STRBF_M));
+
+  strobe_begin(ctx, f0 | f1);
+  strobe_process(ctx, src, dest, sz);
+  return (strobe_done(ctx));
+}
+
+void strobe_key(strobe_ctx *ctx, unsigned f, const void *k, size_t sz)
+  { op(ctx, STROBE_KEY, f, k, 0, sz); }
+
+void strobe_ad(strobe_ctx *ctx, unsigned f, const void *h, size_t sz)
+  { op(ctx, STROBE_AD, f, h, 0, sz); }
+
+void strobe_prf(strobe_ctx *ctx, unsigned f, void *t, size_t sz)
+  { op(ctx, STROBE_PRF, f, 0, t, sz); }
+
+void strobe_clrout(strobe_ctx *ctx, unsigned f, const void *m, size_t sz)
+  { op(ctx, STROBE_CLROUT, f, m, 0, sz); }
+
+void strobe_clrin(strobe_ctx *ctx, unsigned f, const void *m, size_t sz)
+  { op(ctx, STROBE_CLRIN, f, m, 0, sz); }
+
+void strobe_encout(strobe_ctx *ctx, unsigned f,
+                  const void *m, void *c, size_t sz)
+  { op(ctx, STROBE_ENCOUT, f, m, c, sz); }
+
+void strobe_encin(strobe_ctx *ctx, unsigned f,
+                 const void *c, void *m, size_t sz)
+  { op(ctx, STROBE_ENCIN, f, c, m, sz); }
+
+void strobe_macout(strobe_ctx *ctx, unsigned f, void *t, size_t sz)
+  { op(ctx, STROBE_MACOUT, f, 0, t, sz); }
+
+int strobe_macin(strobe_ctx *ctx, unsigned f, const void *t, size_t sz)
+  { return (op(ctx, STROBE_MACIN, f, t, 0, sz)); }
+
+void strobe_ratchet(strobe_ctx *ctx, unsigned f, size_t sz)
+  { op(ctx, STROBE_RATCHET, f, 0, 0, sz); }
+
+/*----- Test rig ----------------------------------------------------------*/
+
+#ifdef TEST_RIG
+
+#include <stdlib.h>
+#include <string.h>
+
+#include <mLib/hex.h>
+#include <mLib/testrig.h>
+
+#define NSTATE 16
+
+static strobe_ctx states[NSTATE];
+
+static void dump(int rc, char win, const void *p, size_t sz)
+{
+  dstr d = DSTR_INIT;
+  const char *q = p;
+  size_t i;
+  codec *hex;
+  int printable;
+
+  if (!p) {
+    if (!rc) putchar(win);
+    else putchar('-');
+  } else {
+    for (i = 0, printable = 1; i < sz; i++)
+      if (!isprint((unsigned char)q[i])) { printable = 0; break; }
+    if (printable)
+      printf("`%s'", q);
+    else {
+      hex = hex_class.encoder(CDCF_LOWERC, 0, 0);
+      hex->ops->code(hex, p, sz, &d);
+      dstr_write(&d, stdout);
+      hex->ops->destroy(hex);
+    }
+  }
+  dstr_destroy(&d);
+  putchar('\n');
+}
+
+typedef int opfunc(strobe_ctx *, unsigned, const void *, void *, size_t);
+
+static int op_init(strobe_ctx *ctx, unsigned f,
+                  const void *p, void *q, size_t sz)
+  { strobe_init(ctx, sz); return (0); }
+
+static int op_copy(strobe_ctx *ctx, unsigned f,
+                  const void *p, void *q, size_t sz)
+  { *ctx = states[sz]; return (0); }
+
+static int op_begin(strobe_ctx *ctx, unsigned f,
+                  const void *p, void *q, size_t sz)
+  { strobe_begin(ctx, f); return (0); }
+
+static int op_process(strobe_ctx *ctx, unsigned f,
+                  const void *p, void *q, size_t sz)
+  { strobe_process(ctx, p, q, sz); return (0); }
+
+static int op_done(strobe_ctx *ctx, unsigned f,
+                  const void *p, void *q, size_t sz)
+  { return (strobe_done(ctx)); }
+
+static int op_key(strobe_ctx *ctx, unsigned f,
+                  const void *p, void *q, size_t sz)
+  { strobe_key(ctx, f, p, sz); return (0); }
+
+static int op_ad(strobe_ctx *ctx, unsigned f,
+                  const void *p, void *q, size_t sz)
+  { strobe_ad(ctx, f, p, sz); return (0); }
+
+static int op_prf(strobe_ctx *ctx, unsigned f,
+                  const void *p, void *q, size_t sz)
+  { strobe_prf(ctx, f, q, sz); return (0); }
+
+static int op_clrout(strobe_ctx *ctx, unsigned f,
+                  const void *p, void *q, size_t sz)
+  { strobe_clrout(ctx, f, p, sz); return (0); }
+
+static int op_clrin(strobe_ctx *ctx, unsigned f,
+                  const void *p, void *q, size_t sz)
+  { strobe_clrin(ctx, f, p, sz); return (0); }
+
+static int op_encout(strobe_ctx *ctx, unsigned f,
+                  const void *p, void *q, size_t sz)
+  { strobe_encout(ctx, f, p, q, sz); return (0); }
+
+static int op_encin(strobe_ctx *ctx, unsigned f,
+                  const void *p, void *q, size_t sz)
+  { strobe_encin(ctx, f, p, q, sz); return (0); }
+
+static int op_macout(strobe_ctx *ctx, unsigned f,
+                  const void *p, void *q, size_t sz)
+  { strobe_macout(ctx, f, q, sz); return (0); }
+
+static int op_macin(strobe_ctx *ctx, unsigned f,
+                  const void *p, void *q, size_t sz)
+  { return (strobe_macin(ctx, f, p, sz)); }
+
+static int op_ratchet(strobe_ctx *ctx, unsigned f,
+                     const void *p, void *q, size_t sz)
+  { strobe_ratchet(ctx, f, sz); return (0); }
+
+static const struct optab {
+  const char *name;
+  opfunc *op;
+} optab[] = {
+#define OP(op) { #op, op_##op }
+  OP(init), OP(copy),
+  OP(begin), OP(process), OP(done),
+  OP(key), OP(ad), OP(prf),
+  OP(clrout), OP(clrin),
+  OP(encout), OP(encin),
+  OP(macout), OP(macin),
+  OP(ratchet),
+  { 0 }
+#undef OP
+};
+
+static int verify(dstr v[])
+{
+  int r;
+  strobe_ctx *ctx;
+  const char *p;
+  char *q;
+  const struct optab *op;
+  dstr d0 = DSTR_INIT, d1 = DSTR_INIT;
+  codec *hex;
+  unsigned f;
+  const void *src, *destref;
+  void *dest;
+  size_t sz;
+  int rc, rcref;
+  int ok;
+
+  /* First, get the register number. */
+  r = *(int *)v[0].buf; ctx = &states[r];
+
+  /* Next job is to parse the command and flags. */
+  q = v[1].buf; p = q; q += strcspn(q, "/"); if (*q) *q++ = 0;
+  for (op = optab; op->name; op++)
+    if (!strcmp(op->name, p)) goto found_op;
+  abort();
+found_op:
+
+  f = 0;
+  for (p = q; *p; p++) {
+    switch (*p) {
+      case 'I': f |= STRBF_I; break;
+      case 'C': f |= STRBF_C; break;
+      case 'A': f |= STRBF_A; break;
+      case 'T': f |= STRBF_T; break;
+      case 'M': f |= STRBF_M; break;
+      default: abort();
+    }
+  }
+
+  /* Convert the source parameter. */
+  p = v[2].buf;
+  if (*p == '*')
+    { src = 0; sz = strtoul(p + 1, 0, 0); }
+  else if (*p == '=')
+    { src = p + 1; sz = v[2].len - 1; }
+  else if (*p == '!') {
+    hex = hex_class.decoder(CDCF_IGNCASE);
+    rc = hex->ops->code(hex, p + 1, v[2].len - 1, &d0); assert(!rc);
+    src = d0.buf; sz = d0.len;
+    hex->ops->destroy(hex);
+  } else
+    abort();
+
+  /* Convert the destination parameter. */
+  p = v[3].buf;
+  if (*p == '+')
+    { destref = 0; rcref = 0; assert(v[3].len == 1); }
+  else if (*p == '-')
+    { destref = 0; rcref = -1; assert(v[3].len == 1); }
+  else if (*p == '=')
+    { destref = p + 1; assert(sz == v[3].len - 1); rcref = 0; }
+  else if (*p == '!') {
+    hex = hex_class.decoder(CDCF_IGNCASE);
+    rc = hex->ops->code(hex, p + 1, v[3].len - 1, &d1); assert(!rc);
+    destref = d1.buf; assert(sz == d1.len);
+    hex->ops->destroy(hex);
+    rcref = 0;
+  } else
+    abort();
+  if (!destref) dest = 0;
+  else dest = xmalloc(sz);
+
+  /* Do the operation. */
+  rc = op->op(ctx, f, src, dest, sz);
+
+  /* Check we got the right answer. */
+  ok = (rc == rcref && (!destref || !memcmp(dest, destref, sz)));
+  if (!ok) {
+    printf("failed test\n");
+    printf("       state = %d\n", r);
+    printf("   operation = %s%s%s%s%s%s%s\n",
+          op->name,
+          f ? "/" : "",
+          f&STRBF_I ? "I" : "",
+          f&STRBF_A ? "A" : "",
+          f&STRBF_C ? "C" : "",
+          f&STRBF_T ? "T" : "",
+          f&STRBF_M ? "M" : "");
+    printf("       input = "); dump(0, '*', src, sz);
+    printf("    computed = "); dump(rc, '+', dest, sz);
+    printf("    expected = "); dump(rcref, '+', destref, sz);
+  }
+
+  dstr_destroy(&d0);
+  dstr_destroy(&d1);
+  free(dest);
+  return (ok);
+}
+
+static test_chunk tests[] = {
+  { "strobe", verify,
+    { &type_int, &type_string, &type_string, &type_string, 0 } },
+  { 0, 0, { 0 } }
+};
+
+int main(int argc, char *argv[])
+{
+  test_run(argc, argv, tests, SRCDIR "/t/strobe");
+  return (0);
+}
+
+#endif
+
+/*----- That's all, folks -------------------------------------------------*/