udpkey.c: Use correct name and key size when client derives MAC key.
[udpkey] / udpkey.c
index 4d72079..769bdb1 100644 (file)
--- a/udpkey.c
+++ b/udpkey.c
@@ -469,12 +469,58 @@ static void debug_ge(const char *what, group *g, ge *X)
 }
 #endif
 
+/*----- Protocol summary --------------------------------------------------*
+ *
+ * There are two protocol versions.  The original version works as follows.
+ *
+ *   * Request
+ *     memz            KEYTAG  tag of wanted secret
+ *     ge              U       public vector
+ *
+ *   * Response
+ *     ge              V       public vector: V = v P
+ *     ge              W       encrypted clue: W = R - Y = r P - v U
+ *     mem[TAGSZ]      TAG     MAC tag on ciphertext
+ *     mem[KSZ]        CT      secret, encrypted with Z = r X
+ *
+ * The new version provides forward secrecy, which involves additional flows.
+ *
+ *   * Greeting
+ *     u8              0       marker byte for new protocol
+ *     u8              1       packet type
+ *     mem8            KEYTAG  wanted secret tag
+ *
+ *   * Challenge
+ *     u8              17      packet type
+ *     u32             REF     server's reference
+ *     ge              R       public DLIES vector: R = r P
+ *     ge              W       masked DH vector: W = V - Y = v P - r X
+ *
+ *   * Response
+ *     u8              0       marker byte for new protocol
+ *     u8              2       packet type
+ *     mem8            KEYTAG  wanted secret tag
+ *     u32             REF     reference from challenge
+ *     ge              U       public DH vector
+ *     mem[HASHSZ]     H0      hash; H0||H1 = H(U, V, Z), where Z = v U
+ *
+ *   * Reply
+ *     u8              18      packet type
+ *     mem[TAGSZ]      TAG     MAC tag on ciphertext
+ *     mem[KSZ]        CT      secret, encrypted with H1
+ */
+
+#define FWS_GREET 0x01
+#define FWS_CHALL 0x11
+#define FWS_RESP 0x02
+#define FWS_REPLY 0x12
+
 /*----- Listening for requests --------------------------------------------*/
 
 /* Rate limiting parameters.
  *
  * There's a probabilistic rate-limiting mechanism.  A counter starts at 0.
- * Every time we oricess a request, we increment the counter.  The counter
+ * Every time we process a request, we increment the counter.  The counter
  * drops by RATE_REFILL every second.  If the counter is below RATE_CREDIT
  * then the request is processed; otherwise it is processed with probability
  * 1/(counter - RATE_CREDIT).
@@ -482,34 +528,285 @@ static void debug_ge(const char *what, group *g, ge *X)
 #define RATE_REFILL 10                 /* Credits per second. */
 #define RATE_CREDIT 1000               /* Initial credit. */
 
+static time_t now;
+
+/* Secrets table.
+ *
+ * The server doesn't want to maintain state for each client.  Instead, we
+ * generate a global secret, and derive per-client secrets from it.  A secret
+ * needs to have an expiry time (at which point we won't use it for new
+ * requests) and a deletion time (at which point we just forget that it ever
+ * existed).  This lets us roll over to a new secret without leaving existing
+ * clients completely in the lurch.
+ *
+ * Secrets are kept in a linked list, ordered by expiry time.  At any given
+ * time there is at most one unexpired secret (because we only make a new one
+ * when the old one expires).
+ */
+
+struct secret {
+  struct secret *next;
+  uint32 seq;
+  time_t t_exp, t_del;
+  octet x[32];
+};
+static struct secret *secrets = 0, *live_secret = 0;
+static uint32 next_secret_seq = 0;
+#define T_SECEXP 30
+#define T_SECDEL 45
+
+static void kill_dead_secrets(void)
+{
+  struct secret *s = secrets, *ss;
+
+  for (s = secrets; s && s->t_del <= now; s = ss) {
+    ss = s->next;
+    DESTROY(s);
+  }
+  secrets = 0;
+  if (!s) live_secret = 0;
+}
+
+static struct secret *find_secret(uint32 seq)
+{
+  struct secret *s;
+
+  kill_dead_secrets();
+  for (s = secrets; s; s = s->next)
+    if (s->seq == seq) return (s);
+  return (0);
+}
+
+static struct secret *fresh_secret(void)
+{
+  struct secret *s;
+
+  if (live_secret && live_secret->t_exp > now) return (live_secret);
+  kill_dead_secrets();
+
+  s = CREATE(struct secret);
+  s->seq = next_secret_seq++;
+  s->next = 0;
+  rand_get(RAND_GLOBAL, s->x, sizeof(s->x));
+  s->t_exp = now + T_SECEXP; s->t_del = now + T_SECDEL;
+  if (live_secret) live_secret->next = s;
+  else secrets = s;
+  live_secret = s;
+  return (s);
+}
+
+static int fetch_key(const char *tag, struct sockaddr_in *sin,
+                    key **ky, struct kinfo *k)
+{
+  dstr d = DSTR_INIT, dd = DSTR_INIT;
+  key_data **kkd;
+  char *p, *q;
+  const char *pp;
+  struct in_addr in;
+  int ch, mlen, rc = -1;
+
+  /* Find the key. */
+  kfupdate();
+  if (key_qtag(kf, tag, &d, ky, &kkd)) {
+    complain(LOG_WARNING, "unknown key tag `%s' from %s:%d",
+            tag, inet_ntoa(sin->sin_addr), ntohs(sin->sin_port));
+    goto done;
+  }
+
+  /* And make sure that it has the right shape. */
+  if (((*ky)->k->e & KF_ENCMASK) != KENC_BINARY) {
+    complain(LOG_ERR, "key %s is not plain binary data", d.buf);
+    goto done;
+  }
+
+  /* Find the list of clients, and look up the caller's address in the
+   * list.  Entries have the form ADDRESS[/LEN][=TAG] and are separated by
+   * `;'.
+   */
+  if ((pp = key_getattr(kf, *ky, "clients")) == 0) {
+    complain(LOG_WARNING,
+            "key %s requested from %s:%d has no `clients' attribute",
+            d.buf, inet_ntoa(sin->sin_addr), ntohs(sin->sin_port));
+    goto done;
+  }
+  dstr_puts(&dd, pp);
+  p = dd.buf;
+  while (*p) {
+    q = p;
+    while (isdigit((unsigned char)*q) || *q == '.') q++;
+    ch = *q; *q++ = 0;
+    if (!inet_aton(p, &in)) goto skip;
+    if (ch != '/')
+      mlen = 32;
+    else {
+      p = q;
+      while (isdigit((unsigned char)*q)) q++;
+      ch = *q; *q++ = 0;
+      mlen = atoi(p);
+    }
+    if (((sin->sin_addr.s_addr ^ in.s_addr) &
+        htonl(0xffffffff << (32 - mlen))) == 0)
+      goto match;
+  skip:
+    if (!ch) break;
+    p = q;
+    while (*p && *p != ';') p++;
+    if (*p) p++;
+  }
+  complain(LOG_WARNING, "access to key %s denied to %s:%d",
+          d.buf, inet_ntoa(sin->sin_addr), ntohs(sin->sin_port));
+  goto done;
+
+match:
+  /* Build a tag name for the caller's KEM key, either from the client
+   * match or the source address.
+   */
+  if (ch != '=') {
+    DRESET(&dd);
+    dstr_puts(&dd, "client-");
+    dstr_puts(&dd, inet_ntoa(sin->sin_addr));
+    p = dd.buf;
+  } else {
+    p = q;
+    while (*q && *q != ';') q++;
+    if (*q == ';') *q++ = 0;
+  }
+
+  /* Report the match. */
+  complain(LOG_NOTICE, "client %s:%d (`%s') requests key %s",
+          inet_ntoa(sin->sin_addr), ntohs(sin->sin_port), p, d.buf);
+
+  /* Load the KEM key. */
+  if (loadkey(p, k, 0)) goto done;
+  D( debug_ge("X", k.g, k.X); )
+
+  /* All complete. */
+  rc = 0;
+
+done:
+  /* Clean everything up. */
+  dstr_destroy(&d);
+  dstr_destroy(&dd);
+  if (rc) k_free(k);
+  return (rc);
+}
+
+static int respond_v0(buf *bin, buf *bout, struct sockaddr_in *sin)
+{
+  ge *R = 0, *U = 0, *V = 0, *W = 0, *Y = 0, *Z = 0;
+  mp *r = MP_NEW, *v = MP_NEW;
+  octet *kk, *t, *tt;
+  char *p;
+  size_t sz;
+  ghash *h = 0;
+  gmac *m = 0;
+  gcipher *c = 0;
+  struct kinfo k;
+  key *ky;
+  size_t ksz;
+  int rc = -1;
+
+  /* Clear out the key state. */
+  k_init(&k);
+
+  /* Extract the key tag name. */
+  if ((p = buf_getmemz(bin, &sz)) == 0) {
+    complain(LOG_WARNING, "invalid key tag from %s:%d",
+            inet_ntoa(sin->sin_addr), ntohs(sin->sin_port));
+    goto done;
+  }
+
+  /* Find the client's key and check that it's allowed. */
+  if (fetch_key(p, sin, &ky, &k)) goto done;
+
+  /* Read the caller's ephemeral key. */
+  R = G_CREATE(k.g); W = G_CREATE(k.g);
+  U = G_CREATE(k.g); V = G_CREATE(k.g);
+  Y = G_CREATE(k.g); Z = G_CREATE(k.g);
+  if (G_FROMBUF(k.g, bin, U)) {
+    complain(LOG_WARNING, "failed to read ephemeral vector from %s:%d",
+            inet_ntoa(sin->sin_addr), ntohs(sin->sin_port));
+    goto done;
+  }
+  D( debug_ge("U", k.g, U); )
+  if (BLEFT(bin)) {
+    complain(LOG_WARNING, "trailing junk in request from %s:%d",
+            inet_ntoa(sin->sin_addr), ntohs(sin->sin_port));
+    goto done;
+  }
+
+  /* Ephemeral Diffie--Hellman.  Choose v in GF(q) at random; compute
+   * V = v P and -Y = (-v) U.
+   */
+  v = mprand_range(v, k.g->r, &rand_global, 0);
+  G_EXP(k.g, V, k.g->g, v);
+  D( debug_mp("v", v); debug_ge("V", k.g, V); )
+  v = mp_sub(v, k.g->r, v);
+  G_EXP(k.g, Y, U, v);
+  D( debug_ge("-Y", k.g, Y); )
+
+  /* DLIES.  Choose r in GF(q) at random; compute R = r P and Z = r X.  Mask
+   * the clue R as W = R - Y.  (Doing the subtraction here makes life easier
+   * at the other end, since we can determine -Y by negating v whereas the
+   * recipient must subtract vectors which may be less efficient.)
+   */
+  r = mprand_range(r, k.g->r, &rand_global, 0);
+  G_EXP(k.g, R, k.g->g, r);
+  D( debug_mp("r", r); debug_ge("R", k.g, R); )
+  G_EXP(k.g, Z, k.X, r);
+  G_MUL(k.g, W, R, Y);
+  D( debug_ge("Z", k.g, Z); debug_ge("W", k.g, W); )
+
+  /* Derive encryption and integrity keys. */
+  derive(&k, R, Z, "cipher", k.cc->name, k.cc->keysz, &kk, &ksz);
+  c = GC_INIT(k.cc, kk, ksz);
+  derive(&k, R, Z, "mac", k.mc->name, k.mc->keysz, &kk, &ksz);
+  m = GM_KEY(k.mc, kk, ksz);
+
+  /* Build the ciphertext and compute a MAC tag over it. */
+  rc = 0;
+  if (G_TOBUF(k.g, bout, V) ||
+      G_TOBUF(k.g, bout, W))
+    goto done;
+  if ((t = buf_get(bout, k.tagsz)) == 0) goto done;
+  sz = ky->k->u.k.sz;
+  if (BENSURE(bout, sz)) goto done;
+  GC_ENCRYPT(c, ky->k->u.k.k, BCUR(bout), sz);
+  h = GM_INIT(m);
+  GH_HASH(h, BCUR(bout), sz);
+  tt = GH_DONE(h, 0); memcpy(t, tt, k.tagsz);
+  BSTEP(bout, sz);
+
+done:
+  /* Clear everything up and go home. */
+  if (R) G_DESTROY(k.g, R);
+  if (U) G_DESTROY(k.g, U);
+  if (V) G_DESTROY(k.g, V);
+  if (W) G_DESTROY(k.g, W);
+  if (Y) G_DESTROY(k.g, Y);
+  if (Z) G_DESTROY(k.g, Z);
+  if (c) GC_DESTROY(c);
+  if (m) GM_DESTROY(m);
+  if (h) GH_DESTROY(h);
+  if (r) MP_DROP(r);
+  if (v) MP_DROP(v);
+  k_free(&k);
+  return (rc);
+}
+
 static int dolisten(int argc, char *argv[])
 {
   int sk;
-  char *p, *q, ch;
-  const char *pp;
+  char *p;
   char *aspec;
   ssize_t n;
-  size_t sz;
   fd_set fdin;
   struct sockaddr_in sin;
-  struct in_addr in;
-  int mlen;
   socklen_t len;
   buf bin, bout;
-  dstr d = DSTR_INIT, dd = DSTR_INIT;
   FILE *fp = 0;
-  key *ky;
-  key_data **kkd;
-  mp *r = MP_NEW, *v = MP_NEW;
-  ge *R = 0, *U = 0, *V = 0, *W = 0, *Y = 0, *Z = 0;
-  ghash *h = 0;
-  gmac *m = 0;
-  gcipher *c = 0;
-  octet *kk, *t, *tt;
-  size_t ksz;
-  struct kinfo k;
   unsigned bucket = 0, toks;
-  time_t last = 0, now;
+  time_t last = 0;
 
   /* Set up the socket address. */
   sin.sin_family = AF_INET;
@@ -542,9 +839,6 @@ static int dolisten(int argc, char *argv[])
 
   for (;;) {
 
-    /* Clear out the key state. */
-    k_init(&k);
-
     /* Wait for something to happen. */
     FD_ZERO(&fdin);
     FD_SET(sk, &fdin);
@@ -558,7 +852,7 @@ static int dolisten(int argc, char *argv[])
     if (n < 0) {
       if (errno != EAGAIN && errno != EINTR)
        complain(LOG_ERR, "unexpected receive error: %s", strerror(errno));
-      goto again;
+      continue;
     }
 
     /* Refill the bucket, and see whether we should reject this packet. */
@@ -570,182 +864,32 @@ static int dolisten(int argc, char *argv[])
     last = now;
     if (bucket > RATE_CREDIT &&
        grand_range(&rand_global, bucket - RATE_CREDIT))
-      goto again;
+      continue;
     bucket++;
 
     /* Set up the input buffer for parsing the request. */
     buf_init(&bin, ibuf, n);
-
-    /* Extract the key tag name. */
-    if ((p = buf_getmemz(&bin, &sz)) == 0) {
-      complain(LOG_WARNING, "invalid key tag from %s:%d",
-              inet_ntoa(sin.sin_addr), ntohs(sin.sin_port));
-      goto again;
-    }
-
-    /* Find the key. */
-    kfupdate();
-    if (key_qtag(kf, p, &d, &ky, &kkd)) {
-      complain(LOG_WARNING, "unknown key tag `%s' from %s:%d",
-              p, inet_ntoa(sin.sin_addr), ntohs(sin.sin_port));
-      goto again;
-    }
-
-    /* And make sure that it has the right shape. */
-    if ((ky->k->e & KF_ENCMASK) != KENC_BINARY) {
-      complain(LOG_ERR, "key %s is not plain binary data", d.buf);
-      goto again;
-    }
-
-    /* Find the list of clients, and look up the caller's address in the
-     * list.  Entries have the form ADDRESS[/LEN][=TAG] and are separated by
-     * `;'.
-     */
-    if ((pp = key_getattr(kf, ky, "clients")) == 0) {
-      complain(LOG_WARNING,
-              "key %s requested from %s:%d has no `clients' attribute",
-              d.buf, inet_ntoa(sin.sin_addr), ntohs(sin.sin_port));
-      goto again;
-    }
-    dstr_puts(&dd, pp);
-    p = dd.buf;
-    while (*p) {
-      q = p;
-      while (isdigit((unsigned char)*q) || *q == '.') q++;
-      ch = *q; *q++ = 0;
-      if (!inet_aton(p, &in)) goto skip;
-      if (ch != '/')
-       mlen = 32;
-      else {
-       p = q;
-       while (isdigit((unsigned char)*q)) q++;
-       ch = *q; *q++ = 0;
-       mlen = atoi(p);
-      }
-      if (((sin.sin_addr.s_addr ^ in.s_addr) &
-          (0xffffffff << (32 - mlen))) == 0)
-       goto match;
-    skip:
-      if (!ch) break;
-      p = q;
-      while (*p && *p != ';') p++;
-      if (*p) p++;
-    }
-    complain(LOG_WARNING, "access to key %s denied to %s:%d",
-            d.buf, inet_ntoa(sin.sin_addr), ntohs(sin.sin_port));
-    goto again;
-
-  match:
-    /* Build a tag name for the caller's KEM key, either from the client
-     * match or the source address.
-     */
-    if (ch != '=') {
-      DRESET(&dd);
-      dstr_puts(&dd, "client-");
-      dstr_puts(&dd, inet_ntoa(sin.sin_addr));
-      p = dd.buf;
-    } else {
-      p = q;
-      while (*q && *q != ';') q++;
-      if (*q == ';') *q++ = 0;
-    }
-
-    /* Report the match. */
-    complain(LOG_NOTICE, "client %s:%d (`%s') requests key %s",
-            inet_ntoa(sin.sin_addr), ntohs(sin.sin_port), p, d.buf);
-
-    /* Load the KEM key. */
-    if (loadkey(p, &k, 0)) goto again;
-    D( debug_ge("X", k.g, k.X); )
-
-    /* Read the caller's ephemeral key. */
-    R = G_CREATE(k.g); W = G_CREATE(k.g);
-    U = G_CREATE(k.g); V = G_CREATE(k.g);
-    Y = G_CREATE(k.g); Z = G_CREATE(k.g);
-    if (G_FROMBUF(k.g, &bin, U)) {
-      complain(LOG_WARNING, "failed to read ephemeral vector from %s:%d",
-              inet_ntoa(sin.sin_addr), ntohs(sin.sin_port));
-      goto again;
-    }
-    D( debug_ge("U", k.g, U); )
-    if (BLEFT(&bin)) {
-      complain(LOG_WARNING, "trailing junk in request from %s:%d",
-              inet_ntoa(sin.sin_addr), ntohs(sin.sin_port));
-      goto again;
-    }
-
-    /* Ephemeral Diffie--Hellman.  Choose v in GF(q) at random; compute
-     * V = v P and -Y = (-v) U.
-     */
-    v = mprand_range(v, k.g->r, &rand_global, 0);
-    G_EXP(k.g, V, k.g->g, v);
-    D( debug_mp("v", v); debug_ge("V", k.g, V); )
-    v = mp_sub(v, k.g->r, v);
-    G_EXP(k.g, Y, U, v);
-    D( debug_ge("-Y", k.g, Y); )
-
-    /* DLIES.  Choose r in GF(q) at random; compute R = r P and Z = r X.
-     * Mask the clue R as W = R - Y.  (Doing the subtraction here makes life
-     * easier at the other end, since we can determine -Y by negating v
-     * whereas the recipient must subtract vectors which may be less
-     * efficient.)
-     */
-    r = mprand_range(r, k.g->r, &rand_global, 0);
-    G_EXP(k.g, R, k.g->g, r);
-    D( debug_mp("r", r); debug_ge("R", k.g, R); )
-    G_EXP(k.g, Z, k.X, r);
-    G_MUL(k.g, W, R, Y);
-    D( debug_ge("Z", k.g, Z); debug_ge("W", k.g, W); )
-
-    /* Derive encryption and integrity keys. */
-    derive(&k, R, Z, "cipher", k.cc->name, k.cc->keysz, &kk, &ksz);
-    c = GC_INIT(k.cc, kk, ksz);
-    derive(&k, R, Z, "mac", k.mc->name, k.mc->keysz, &kk, &ksz);
-    m = GM_KEY(k.mc, kk, ksz);
-
-    /* Build the ciphertext and compute a MAC tag over it. */
     buf_init(&bout, obuf, sizeof(obuf));
-    if (G_TOBUF(k.g, &bout, V) ||
-       G_TOBUF(k.g, &bout, W))
-      goto bad;
-    if ((t = buf_get(&bout, k.tagsz)) == 0) goto bad;
-    sz = ky->k->u.k.sz;
-    if (BENSURE(&bout, sz)) goto bad;
-    GC_ENCRYPT(c, ky->k->u.k.k, BCUR(&bout), sz);
-    h = GM_INIT(m);
-    GH_HASH(h, BCUR(&bout), sz);
-    tt = GH_DONE(h, 0); memcpy(t, tt, k.tagsz);
-    BSTEP(&bout, sz);
+
+    /* Handle the client's message. */
+    if (respond_v0(&bin, &bout, &sin)) continue;
 
     /* Send the reply packet back to the caller. */
+    if (!BOK(&bout)) goto bad;
     if (sendto(sk, BBASE(&bout), BLEN(&bout), 0,
               (struct sockaddr *)&sin, len) < 0) {
       complain(LOG_ERR, "failed to send response to %s:%d: %s",
               inet_ntoa(sin.sin_addr), ntohs(sin.sin_port),
               strerror(errno));
-      goto again;
+      continue;
     }
 
-    goto again;
+    continue;
 
   bad:
     /* Report a problem building the reply. */
     complain(LOG_ERR, "failed to construct response to %s:%d",
             inet_ntoa(sin.sin_addr), ntohs(sin.sin_port));
-
-  again:
-    /* Free stuff for the next iteration. */
-    DRESET(&d); DRESET(&dd);
-    if (R) { G_DESTROY(k.g, R); R = 0; }
-    if (U) { G_DESTROY(k.g, U); U = 0; }
-    if (V) { G_DESTROY(k.g, V); V = 0; }
-    if (W) { G_DESTROY(k.g, W); W = 0; }
-    if (Y) { G_DESTROY(k.g, Y); Y = 0; }
-    if (Z) { G_DESTROY(k.g, Z); Z = 0; }
-    if (c) { GC_DESTROY(c); c = 0; }
-    if (m) { GM_DESTROY(m); m = 0; }
-    if (h) { GH_DESTROY(h); h = 0; }
-    k_free(&k);
   }
 
   return (-1);
@@ -755,6 +899,7 @@ static int dolisten(int argc, char *argv[])
 
 struct query {
   struct query *next;
+  const char *tag;
   octet *k;
   size_t sz;
   struct server *s;
@@ -764,16 +909,151 @@ struct server {
   struct server *next;
   struct sockaddr_in sin;
   struct kinfo k;
+  const struct client_protocol *proto;
   mp *u;
   ge *U;
   octet *h;
 };
 
+struct client_protocol {
+  const char *name;
+  int (*setup)(struct query *, struct server *);
+  int (*receive)(struct query *, struct server *, buf *, buf *);
+  int (*retransmit)(struct query *, struct server *, buf *);
+};
+
 /* Record a successful fetch of key material for a query Q.  The data starts
  * at K and is SZ bytes long.  The data is copied: it's safe to overwrite it.
  */
-static void donequery(struct query *q, const void *k, size_t sz)
-  { q->k = xmalloc(sz); memcpy(q->k, k, sz); q->sz = sz; nq--; }
+static int donequery(struct query *q, struct server *s,
+                     const void *k, size_t sz)
+{
+  octet *tt;
+  ghash *h = 0;
+  int diffp;
+
+  /* If we have a hash, check that the fragment matches it. */
+  if (s && s->h) {
+    h = GH_INIT(s->k.hc);
+    GH_HASH(h, k, sz);
+    tt = GH_DONE(h, 0);
+    diffp = memcmp(tt, s->h, h->ops->c->hashsz);
+    GH_DESTROY(h);
+    if (diffp) {
+      moan("response from %s:%d doesn't match hash",
+          inet_ntoa(s->sin.sin_addr), ntohs(s->sin.sin_port));
+      return (-1);
+    }
+  }
+
+  /* Stash a copy of the key fragment for later. */
+  q->k = xmalloc(sz);
+  memcpy(q->k, k, sz);
+  q->sz = sz; nq--;
+
+  /* All good. */
+  return (0);
+}
+
+static int setup_v0(struct query *q, struct server *s)
+{
+  /* Choose an ephemeral private key u.  Let x be our private key.  We
+   * compute U = u P and transmit this.
+   */
+  s->u = mprand_range(MP_NEW, s->k.g->r, &rand_global, 0);
+  s->U = G_CREATE(s->k.g);
+  G_EXP(s->k.g, s->U, s->k.g->g, s->u);
+  D( debug_mp("u", s->u); debug_ge("U", s->k.g, s->U); )
+
+  return (0);
+}
+
+static int retransmit_v0(struct query *q, struct server *s, buf *bout)
+{
+  buf_putstrz(bout, q->tag);
+  G_TOBUF(s->k.g, bout, s->U);
+  return (0);
+}
+
+static int receive_v0(struct query *q, struct server *s, buf *bin, buf *bout)
+{
+  ge *R, *V = 0, *W = 0, *Y = 0, *Z = 0;
+  octet *kk, *t, *tt;
+  gcipher *c = 0;
+  gmac *m = 0;
+  ghash *h = 0;
+  size_t n, ksz;
+  octet *p;
+  int rc = -1;
+
+  R = G_CREATE(s->k.g);
+  V = G_CREATE(s->k.g); W = G_CREATE(s->k.g);
+  Y = G_CREATE(s->k.g); Z = G_CREATE(s->k.g);
+  if (G_FROMBUF(s->k.g, bin, V)) {
+    moan("invalid Diffie--Hellman vector from %s:%d",
+        inet_ntoa(s->sin.sin_addr), ntohs(s->sin.sin_port));
+    goto done;
+  }
+  if (G_FROMBUF(s->k.g, bin, W)) {
+    moan("invalid clue vector from %s:%d",
+        inet_ntoa(s->sin.sin_addr), ntohs(s->sin.sin_port));
+    goto done;
+  }
+  D( debug_ge("V", s->k.g, V); debug_ge("W", s->k.g, W); )
+
+  /* We have V and W from the server; determine Y = u V, R = W + Y and
+   * Z = x R, and then derive the symmetric keys.
+   */
+  G_EXP(s->k.g, Y, V, s->u);
+  G_MUL(s->k.g, R, W, Y);
+  G_EXP(s->k.g, Z, R, s->k.x);
+  D( debug_ge("R", s->k.g, R);
+     debug_ge("Y", s->k.g, Y);
+     debug_ge("Z", s->k.g, Z); )
+  derive(&s->k, R, Z, "cipher", s->k.cc->name, s->k.cc->keysz, &kk, &ksz);
+  c = GC_INIT(s->k.cc, kk, ksz);
+  derive(&s->k, R, Z, "mac", s->k.mc->name, s->k.mc->keysz, &kk, &ksz);
+  m = GM_KEY(s->k.mc, kk, ksz);
+
+  /* Find where the MAC tag is. */
+  if ((t = buf_get(bin, s->k.tagsz)) == 0) {
+    moan("missing tag from %s:%d",
+        inet_ntoa(s->sin.sin_addr), ntohs(s->sin.sin_port));
+    goto done;
+  }
+
+  /* Check the integrity of the ciphertext against the tag. */
+  p = BCUR(bin); n = BLEFT(bin);
+  h = GM_INIT(m);
+  GH_HASH(h, p, n);
+  tt = GH_DONE(h, 0);
+  if (!ct_memeq(t, tt, s->k.tagsz)) {
+    moan("incorrect tag from %s:%d",
+        inet_ntoa(s->sin.sin_addr), ntohs(s->sin.sin_port));
+    goto done;
+  }
+
+  /* Decrypt the result and declare this server done. */
+  GC_DECRYPT(c, p, p, n);
+  rc = donequery(q, s, p, n);
+
+done:
+  /* Clear up and go home. */
+  if (R) G_DESTROY(s->k.g, R);
+  if (V) G_DESTROY(s->k.g, V);
+  if (W) G_DESTROY(s->k.g, W);
+  if (Y) G_DESTROY(s->k.g, Y);
+  if (Z) G_DESTROY(s->k.g, Z);
+  if (c) GC_DESTROY(c);
+  if (m) GM_DESTROY(m);
+  if (h) GH_DESTROY(h);
+  return (rc);
+}
+
+static const struct client_protocol prototab[] = {
+  { "v0", setup_v0, receive_v0, retransmit_v0 },
+  { 0 }
+};
 
 /* Initialize a query to a remote server. */
 static struct query *qinit_net(const char *tag, const char *spec)
@@ -781,11 +1061,13 @@ static struct query *qinit_net(const char *tag, const char *spec)
   struct query *q;
   struct server *s, **stail;
   dstr d = DSTR_INIT, dd = DSTR_INIT;
+  const struct client_protocol *proto;
   hex_ctx hc;
   char *p, *pp, ch;
 
   /* Allocate the query block. */
   q = CREATE(struct query);
+  q->tag = tag;
   stail = &q->s;
 
   /* Put the spec somewhere we can hack at it. */
@@ -813,6 +1095,20 @@ static struct query *qinit_net(const char *tag, const char *spec)
     ch = *pp; *pp++ = 0;
     s->sin.sin_port = htons(getport(p));
 
+    /* See if there's a protocol name. */
+    if (ch != '?')
+      p = "v0";
+    else {
+      p = pp;
+      pp += strcspn(pp, ";#=");
+      ch = *pp; *pp++ = 0;
+    }
+    for (proto = prototab; proto->name; proto++)
+      if (strcmp(proto->name, p) == 0) goto found_proto;
+    die(1, "unknown protocol name `%s'", p);
+  found_proto:
+    s->proto = proto;
+
     /* If there's a key tag then extract that; otherwise use a default. */
     if (ch != '=')
       p = "udpkey-kem";
@@ -824,14 +1120,6 @@ static struct query *qinit_net(const char *tag, const char *spec)
     if (loadkey(p, &s->k, 1)) exit(1);
     D( debug_mp("x", s->k.x); debug_ge("X", s->k.g, s->k.X); )
 
-    /* Choose an ephemeral private key u.  Let x be our private key.  We
-     * compute U = u P and transmit this.
-     */
-    s->u = mprand_range(MP_NEW, s->k.g->r, &rand_global, 0);
-    s->U = G_CREATE(s->k.g);
-    G_EXP(s->k.g, s->U, s->k.g->g, s->u);
-    D( debug_mp("u", s->u); debug_ge("U", s->k.g, s->U); )
-
     /* Link the server on. */
     *stail = s; stail = &s->next;
 
@@ -850,6 +1138,9 @@ static struct query *qinit_net(const char *tag, const char *spec)
       ch = *pp++;
     }
 
+    /* Initialize the protocol. */
+    if (s->proto->setup(q, s)) die(1, "failed to initialize protocol");
+
     /* If there are more servers, then continue parsing. */
     if (!ch) break;
     else if (ch != ';') die(1, "invalid syntax: expected `;'");
@@ -876,11 +1167,11 @@ static struct query *qinit_file(const char *tag, const char *file)
   if (snarf(file, &k, &sz))
     die(1, "failed to read `%s': %s", file, strerror(errno));
   q->s = 0;
-  donequery(q, k, sz);
+  donequery(q, 0, k, sz);
   return (q);
 }
 
-/* Reransmission and timeout parameters. */
+/* Retransmission and timeout parameters. */
 #define TO_NEXT(t) (((t) + 2)*4/3)     /* Timeout growth function */
 #define TO_MAX 30                      /* When to give up */
 
@@ -895,15 +1186,10 @@ static int doquery(int argc, char *argv[])
   fd_set fdin;
   struct timeval now, when, tv;
   struct sockaddr_in sin;
-  ge *R, *V = 0, *W = 0, *Y = 0, *Z = 0;
-  octet *kk, *t, *tt;
-  gcipher *c = 0;
-  gmac *m = 0;
-  ghash *h = 0;
   socklen_t len;
   unsigned next = 0;
   buf bin, bout;
-  size_t n, j, ksz;
+  size_t n, j;
   ssize_t nn;
 
   /* Create a socket.  We just use the one socket for everything.  We don't
@@ -943,8 +1229,7 @@ static int doquery(int argc, char *argv[])
        if (q->k) continue;
        for (s = q->s; s; s = s->next) {
          buf_init(&bout, obuf, sizeof(obuf));
-         buf_putstrz(&bout, tag);
-         G_TOBUF(s->k.g, &bout, s->U);
+         if (s->proto->retransmit(q, s, &bout)) continue;
          if (BBAD(&bout)) {
            moan("overflow while constructing request!");
            continue;
@@ -976,12 +1261,12 @@ static int doquery(int argc, char *argv[])
          else if (errno == EINTR) continue;
          else {
            moan("error receiving reply: %s", strerror(errno));
-           goto again;
+           continue;
          }
        }
 
-       /* Wee whether this corresponds to any of our servers.  Don't just
-        * check the active servers, since this may be late replies caused by
+       /* See whether this corresponds to any of our servers.  Don't just
+        * check the active servers, since this may be late a reply caused by
         * retransmissions or similar.
         */
        for (q = qq; q; q = q->next) {
@@ -993,89 +1278,27 @@ static int doquery(int argc, char *argv[])
        }
        moan("received reply from unexpected source %s:%d",
             inet_ntoa(sin.sin_addr), ntohs(sin.sin_port));
-       goto again;
+       continue;
 
       found:
        /* If the query we found has now been satisfied, ignore this packet.
         */
-       if (q->k) goto again;
-
-       /* Start parsing the reply. */
-       buf_init(&bin, ibuf, nn);
-       R = G_CREATE(s->k.g);
-       V = G_CREATE(s->k.g); W = G_CREATE(s->k.g);
-       Y = G_CREATE(s->k.g); Z = G_CREATE(s->k.g);
-       if (G_FROMBUF(s->k.g, &bin, V)) {
-         moan("invalid Diffie--Hellman vector from %s:%d",
-              inet_ntoa(sin.sin_addr), ntohs(sin.sin_port));
-         goto again;
-       }
-       if (G_FROMBUF(s->k.g, &bin, W)) {
-         moan("invalid clue vector from %s:%d",
-              inet_ntoa(sin.sin_addr), ntohs(sin.sin_port));
-         goto again;
-       }
-       D( debug_ge("V", s->k.g, V); debug_ge("W", s->k.g, W); )
+       if (q->k) continue;
 
-       /* We have V and W from the server; determine Y = u V, R = W + Y and
-        * Z = x R, and then derive the symmetric keys.
+       /* Parse the reply, and either finish the job or get a message to
+        * send back to the server.
         */
-       G_EXP(s->k.g, Y, V, s->u);
-       G_MUL(s->k.g, R, W, Y);
-       G_EXP(s->k.g, Z, R, s->k.x);
-       D( debug_ge("R", s->k.g, R);
-          debug_ge("Y", s->k.g, Y);
-          debug_ge("Z", s->k.g, Z); )
-       derive(&s->k, R, Z, "cipher", s->k.cc->name, s->k.cc->keysz,
-              &kk, &ksz);
-       c = GC_INIT(s->k.cc, kk, ksz);
-       derive(&s->k, R, Z, "mac", s->k.cc->name, s->k.cc->keysz,
-              &kk, &ksz);
-       m = GM_KEY(s->k.mc, kk, ksz);
-
-       /* Find where the MAC tag is. */
-       if ((t = buf_get(&bin, s->k.tagsz)) == 0) {
-         moan("missing tag from %s:%d",
-              inet_ntoa(sin.sin_addr), ntohs(sin.sin_port));
-         goto again;
-       }
-
-       /* Check the integrity of the ciphertext against the tag. */
-       p = BCUR(&bin); n = BLEFT(&bin);
-       h = GM_INIT(m);
-       GH_HASH(h, p, n);
-       tt = GH_DONE(h, 0);
-       if (!ct_memeq(t, tt, s->k.tagsz)) {
-         moan("incorrect tag from %s:%d",
-              inet_ntoa(sin.sin_addr), ntohs(sin.sin_port));
-         goto again;
-       }
-
-       /* Decrypt the result and declare this server done. */
-       GC_DECRYPT(c, p, p, n);
-       if (s->h) {
-         GH_DESTROY(h);
-         h = GH_INIT(s->k.hc);
-         GH_HASH(h, p, n);
-         tt = GH_DONE(h, 0);
-         if (memcmp(tt, s->h, h->ops->c->hashsz) != 0) {
-           moan("response from %s:%d doesn't match hash",
-                inet_ntoa(sin.sin_addr), ntohs(sin.sin_port));
-           goto again;
-         }
+       buf_init(&bin, ibuf, nn);
+       buf_init(&bout, obuf, sizeof(obuf));
+       if (s->proto->receive(q, s, &bin, &bout)) continue;
+       if (q->k) continue;
+       if (!BLEN(&bout) && s->proto->retransmit(q, s, &bout)) continue;
+       if (BBAD(&bout)) {
+         moan("overflow while constructing request!");
+         continue;
        }
-       donequery(q, p, n);
-
-      again:
-       /* Tidy things up for the next run through. */
-       if (R) { G_DESTROY(s->k.g, R); R = 0; }
-       if (V) { G_DESTROY(s->k.g, V); V = 0; }
-       if (W) { G_DESTROY(s->k.g, W); W = 0; }
-       if (Y) { G_DESTROY(s->k.g, Y); Y = 0; }
-       if (Z) { G_DESTROY(s->k.g, Z); Z = 0; }
-       if (c) { GC_DESTROY(c); c = 0; }
-       if (m) { GM_DESTROY(m); m = 0; }
-       if (h) { GH_DESTROY(h); h = 0; }
+       sendto(sk, BBASE(&bout), BLEN(&bout), 0,
+              (struct sockaddr *)&s->sin, sizeof(s->sin));
       }
     }
   }
@@ -1105,7 +1328,7 @@ static int doquery(int argc, char *argv[])
 static void usage(FILE *fp)
 {
   pquis(fp, "Usage: \n\
-       $ [-OPTS] LABEL {ADDR:PORT | FILE} ...\n\
+       $ [-OPTS] LABEL {ADDR:PORT[=TAG][#HASH];... | FILE} ...\n\
        $ [-OPTS] -l [ADDR:]PORT\n\
 ");
 }