chiark / gitweb /
Support expiry of other peers' public keys.
[tripe] / keyexch.c
index 1cf79fc8e4f2245d08fc5d6ca4f47e74ff3794d2..c17f4009f95d54f9c199bdfbb31ee50666ebed94 100644 (file)
--- a/keyexch.c
+++ b/keyexch.c
@@ -1,6 +1,6 @@
 /* -*-c-*-
  *
- * $Id: keyexch.c,v 1.1 2001/02/03 20:26:37 mdw Exp $
+ * $Id: keyexch.c,v 1.4 2001/06/22 19:40:36 mdw Exp $
  *
  * Key exchange protocol
  *
 /*----- Revision history --------------------------------------------------* 
  *
  * $Log: keyexch.c,v $
+ * Revision 1.4  2001/06/22 19:40:36  mdw
+ * Support expiry of other peers' public keys.
+ *
+ * Revision 1.3  2001/06/19 22:07:09  mdw
+ * Cosmetic fixes.
+ *
+ * Revision 1.2  2001/02/16 21:24:27  mdw
+ * Rewrite for new key exchange protocol.
+ *
  * Revision 1.1  2001/02/03 20:26:37  mdw
  * Initial checkin.
  *
 /*----- Tunable parameters ------------------------------------------------*/
 
 #define T_VALID MIN(2)
-#define T_QUIET SEC(5)
 #define T_RETRY SEC(10)
-#define T_NEWCHAL SEC(5)
-
-/*----- Handy macros ------------------------------------------------------*/
-
-#define FREECTX(kx) do {                                               \
-  keyexch *_kkx = (kx);                                                        \
-  if (_kkx->f & KXF_INIT) {                                            \
-    mp_drop(_kkx->my_x); mp_drop(_kkx->my_gx); mp_drop(_kkx->my_gxy);  \
-    mp_drop(_kkx->your_gx); mp_drop(_kkx->your_gxy);                   \
-  }                                                                    \
-} while (0)
-
-#define INITCTX(kx, now) do {                                          \
-  keyexch *_kx = (kx);                                                 \
-  time_t _now = (now);                                                 \
-  FREECTX(_kx);                                                                \
-  kx->my_x = kx->my_gx = kx->my_gxy = 0;                               \
-  kx->your_gx = kx->your_gxy = 0;                                      \
-  kx->t_valid = _now + T_VALID;                                                \
-  kx->t_qchal = kx->t_qresp = 0;                                       \
-  kx->t_newchal = 0;                                                   \
-  kx->f = (kx->f | KXF_INIT) & ~(KXF_MYH | KXF_YOURH |                 \
-                                KXF_REPLY | KXF_DONE);                 \
-} while (0)
-
-#define NEWCHAL(kx) do {                                               \
-  kx->f &= ~(KXF_YOURH | KXF_MYH);                                     \
-  mp_drop(kx->your_gx); kx->your_gx = 0;                               \
-  mp_drop(kx->your_gxy); kx->your_gxy = 0;                             \
-} while (0)
-
-#define ISVALID(kx, now) ((kx)->t_valid > (now))
-#define ISQ_CHAL(kx, now) ((kx)->t_qchal > (now))
-#define ISQ_RESP(kx, now) ((kx)->t_qresp > (now))
-#define ISNEWCHAL(kx, now) ((kx)->t_newchal > (now))
 
-/*----- Main code ---------------------------------------------------------*/
+#define ISVALID(kx, now) ((now) < (kx)->t_valid)
+
+/*----- Various utilities -------------------------------------------------*/
 
 /* --- @hashmp@ --- *
  *
- * Arguments:  @rmd160_ctx *r@ = pointer to hash context
+ * Arguments:  @HASH_CTX *r@ = pointer to hash context
  *             @mp *m@ = pointer to multiprecision integer
  *
  * Returns:    ---
  *
  * Use:                Adds the hash of a multiprecision integer to the context.
- *             Corrupts @buf_o@.
+ *             Corrupts @buf_t@.
  */
 
-static void hashmp(rmd160_ctx *r, mp *m)
+static void hashmp(HASH_CTX *r, mp *m)
 {
   buf b;
-  buf_init(&b, buf_o, sizeof(buf_o));
+  buf_init(&b, buf_t, sizeof(buf_t));
   buf_putmp(&b, m);
   assert(BOK(&b));
-  rmd160_hash(r, BBASE(&b), BLEN(&b));
+  HASH(r, BBASE(&b), BLEN(&b));
 }
 
 /* --- @timer@ --- *
@@ -140,340 +115,912 @@ static void settimer(keyexch *kx, time_t t)
   kx->f |= KXF_TIMER;
 }
 
-/* --- @update@ --- *
+/*----- Challenge management ----------------------------------------------*/
+
+/* --- Notes on challenge management --- *
  *
- * Arguments:  @keyexch *kx@ = pointer to key exchange context
+ * We may get multiple different replies to our key exchange; some will be
+ * correct, some inserted by attackers.  Up until @KX_THRESH@, all challenges
+ * received will be added to the table and given a full response.  After
+ * @KX_THRESH@ distinct challenges are received, we return only a `cookie':
+ * our existing challenge, followed by a hash of the sender's challenge.  We
+ * do %%\emph{not}%% give a bare challenge a reply slot at this stage.  All
+ * properly-formed cookies are assigned a table slot: if none is spare, a
+ * used slot is randomly selected and destroyed.  A cookie always receives a
+ * full reply.
+ */
+
+/* --- @kxc_destroy@ --- *
+ *
+ * Arguments:  @kxchal *kxc@ = pointer to the challenge block
  *
  * Returns:    ---
  *
- * Use:                Updates the information in the key exchange context.  Call
- *             this after new information has arrived.  Expects that the
- *             context is actually valid.  Doesn't send any packets.
- *             Assumes that everything in the context is known to be
- *             correct.
+ * Use:                Disposes of a challenge block.
  */
 
-static void update(keyexch *kx)
+static void kxc_destroy(kxchal *kxc)
 {
-  rmd160_ctx r;
-  octet h[RMD160_HASHSZ];
-  mp *k_shared;
-  buf b;
+  if (kxc->f & KXF_TIMER)
+    sel_rmtimer(&kxc->t);
+  mp_drop(kxc->c);
+  mp_drop(kxc->r);
+  ks_drop(kxc->ks);
+  DESTROY(kxc);
+}
 
-  /* --- Give up if there's nothing more to do --- */
+/* --- @kxc_stoptimer@ --- *
+ *
+ * Arguments:  @kxchal *kxc@ = pointer to the challenge block
+ *
+ * Returns:    ---
+ *
+ * Use:                Stops the challenge's retry timer from sending messages.
+ *             Useful when the state machine is in the endgame of the
+ *             exchange.
+ */
 
-  if (kx->f & KXF_DONE)
-    return;
+static void kxc_stoptimer(kxchal *kxc)
+{
+  if (kxc->f & KXF_TIMER)
+    sel_rmtimer(&kxc->t);
+}
 
-  /* --- If we've just started, generate a new challenge --- */
+/* --- @kxc_new@ --- *
+ *
+ * Arguments:  @keyexch *kx@ = pointer to key exchange block
+ *
+ * Returns:    A pointer to the challenge block.
+ *
+ * Use:                Returns a pointer to a new challenge block to fill in.
+ */
 
-  if (!kx->my_x) {
-    T( trace(T_KEYEXCH, "keyexch: generating new challenge"); )
-    kx->my_x = mprand_range(MP_NEWSEC, kx->kpub.dp.q, &rand_global, 0);
-    kx->my_gx = mpmont_exp(&mg, MP_NEW, kx->kpub.dp.g, kx->my_x);
-    kx->my_gxy = mpmont_exp(&mg, MP_NEW, kx->kpub.y, kx->my_x);
-    IF_TRACING(T_KEYEXCH, IF_TRACING(T_CRYPTO, {
-      trace(T_CRYPTO, "crypto: secret = %s", mpstr(kx->my_x));
-      trace(T_CRYPTO, "crypto: public value = %s", mpstr(kx->my_gx));
-      trace(T_CRYPTO, "crypto: expected reply = %s", mpstr(kx->my_gxy));
-    }))
+static kxchal *kxc_new(keyexch *kx)
+{
+  kxchal *kxc;
+  unsigned i;
+
+  /* --- If we're over reply threshold, discard one at random --- */
+
+  if (kx->nr < KX_NCHAL)
+    i = kx->nr++;
+  else {
+    i = rand_global.ops->range(&rand_global, KX_NCHAL);
+    kxc_destroy(kx->r[i]);
   }
 
-  /* --- If I don't have your challenge, I can't do anything more --- */
+  /* --- Fill in the new structure --- */
 
-  if (!kx->your_gx)
-    return;
+  kxc = CREATE(kxchal);
+  kxc->c = 0;
+  kxc->r = 0;
+  kxc->ks = 0;
+  kxc->kx = kx;
+  kxc->f = 0;
+  kx->r[i] = kxc;
+  return (kxc);
+}
 
-  /* --- If I've not computed my hash, I should do that --- */
+/* --- @kxc_bychal@ --- *
+ *
+ * Arguments:  @keyexch *kx@ = pointer to key exchange block
+ *             @mp *c@ = challenge from remote host
+ *
+ * Returns:    Pointer to the challenge block, or null.
+ *
+ * Use:                Finds a challenge block, given its challenge.
+ */
+
+static kxchal *kxc_bychal(keyexch *kx, mp *c)
+{
+  unsigned i;
 
-  if (!(kx->f & KXF_MYH)) {    
-    T( trace(T_KEYEXCH, "keyexch: computing my hash"); )
-    rmd160_init(&r);
-    hashmp(&r, kx->my_gx);
-    hashmp(&r, kx->your_gx);
-    hashmp(&r, kx->my_gxy);
-    rmd160_done(&r, kx->my_h);
-    IF_TRACING(T_KEYEXCH, trace_block(T_CRYPTO, "crypto: my hash",
-                                     kx->my_h, sizeof(kx->my_h)); )
-    kx->f |= KXF_MYH;
+  for (i = 0; i < kx->nr; i++) {
+    if (MP_EQ(c, kx->r[i]->c))
+      return (kx->r[i]);
   }
+  return (0);
+}
+
+/* --- @kxc_byhc@ --- *
+ *
+ * Arguments:  @keyexch *kx@ = pointer to key exchange block
+ *             @const octet *hc@ = challenge hash from remote host
+ *
+ * Returns:    Pointer to the challenge block, or null.
+ *
+ * Use:                Finds a challenge block, given a hash of its challenge.
+ */
+
+static kxchal *kxc_byhc(keyexch *kx, const octet *hc)
+{
+  unsigned i;
+
+  for (i = 0; i < kx->nr; i++) {
+    if (memcmp(hc, kx->r[i]->hc, HASHSZ) == 0)
+      return (kx->r[i]);
+  }
+  return (0);
+}
+
+/* --- @kxc_answer@ --- *
+ *
+ * Arguments:  @keyexch *kx@ = pointer to key exchange block
+ *             @kxchal *kxc@ = pointer to challenge block
+ *
+ * Returns:    ---
+ *
+ * Use:                Sends a reply to the remote host, according to the data in
+ *             this challenge block.
+ */
+
+static void kxc_answer(keyexch *kx, kxchal *kxc);
+
+static void kxc_timer(struct timeval *tv, void *v)
+{
+  kxchal *kxc = v;
+  kxc->f &= ~KXF_TIMER;
+  kxc_answer(kxc->kx, kxc);
+}
 
-  /* --- If I've received a full challenge, answer it --- *
+static void kxc_answer(keyexch *kx, kxchal *kxc)
+{
+  stats *st = p_stats(kx->p);
+  buf *b = p_txstart(kx->p, MSG_KEYEXCH | (kxc->r ? KX_REPLY : KX_CHAL));
+  struct timeval tv;
+  buf bb;
+
+  /* --- Build the reply packet --- */
+
+  if (!kxc->r)
+    buf_putmp(b, kx->c);
+  else
+    buf_put(b, kx->hc, HASHSZ);
+  buf_put(b, kxc->hc, HASHSZ);
+  buf_put(b, kxc->hrx, HASHSZ);
+
+  /* --- Maybe send an actual reply, if we have one --- */
+
+  if (!kxc->r) {
+    T( trace(T_KEYEXCH, "keyexch: resending challenge to `%s'",
+            p_name(kx->p)); )
+  } else {
+    T( trace(T_KEYEXCH, "keyexch: sending reply to `%s'", p_name(kx->p)); )
+    buf_init(&bb, buf_i, sizeof(buf_i));
+    buf_putmp(&bb, kxc->r);
+    buf_flip(&bb);
+    ks_encrypt(kxc->ks, &bb, b);
+  }
+
+  /* --- Update the statistics --- */
+
+  if (BOK(b)) {
+    st->n_kxout++;
+    st->sz_kxout += BLEN(b);
+    p_txend(kx->p);
+  }
+
+  /* --- Schedule another resend --- */
+
+  if (kxc->f & KXF_TIMER)
+    sel_rmtimer(&kxc->t);
+  gettimeofday(&tv, 0);
+  tv.tv_sec += T_RETRY;
+  sel_addtimer(&sel, &kxc->t, &tv, kxc_timer, kxc);
+  kxc->f |= KXF_TIMER;
+}
+
+/*----- Individual message handlers ---------------------------------------*/
+
+/* --- @getreply@ --- *
+ *
+ * Arguments:  @keyexch *kx@ = pointer to key exchange context
+ *             @mp *c@ = a challenge
+ *             @const octet *hrx@ = the supplied expected-reply hash
+ *
+ * Returns:    A pointer to the reply, or null if the reply-hash was wrong.
+ *
+ * Use:                Computes replies to challenges.
+ */
+
+static mp *getreply(keyexch *kx, mp *c, const octet *hrx)
+{
+  mp *r = mpmont_exp(&mg, MP_NEW, c, kpriv.x);
+  HASH_CTX h;
+  octet buf[HASHSZ];
+
+  HASH_INIT(&h);
+  HASH_STRING(&h, "tripe-expected-reply");
+  hashmp(&h, c);
+  hashmp(&h, kx->c);
+  hashmp(&h, r);
+  HASH_DONE(&h, buf);
+  IF_TRACING(T_KEYEXCH, IF_TRACING(T_CRYPTO, {
+    trace(T_CRYPTO, "crypto: computed reply = %s", mpstr(r));
+    trace_block(T_CRYPTO, "crypto: computed reply hash", buf, HASHSZ);
+  }))
+  if (memcmp(buf, hrx, HASHSZ) != 0) {
+    a_warn("invalid expected-reply hash from `%s'", p_name(kx->p));
+    mp_drop(r);
+    return (0);
+  }
+  return (r);
+}
+
+/* --- @dochallenge@ --- *
+ *
+ * Arguments:  @keyexch *kx@ = pointer to key exchange block
+ *             @unsigned msg@ = message code for the packet
+ *             @buf *b@ = buffer containing the packet
+ *
+ * Returns:    Zero if OK, nonzero if the packet was rejected.
+ *
+ * Use:                Processes a packet containing a challenge.
+ */
+
+static int dochallenge(keyexch *kx, unsigned msg, buf *b)
+{
+  mp *c = 0;
+  const octet *hc = 0, *hrx = 0;
+  kxchal *kxc;
+  HASH_CTX h;
+
+  /* --- Ensure that we're in a sensible state --- */
+
+  if (kx->s != KXS_CHAL) {
+    a_warn("unexpected challenge from `%s'", p_name(kx->p));
+    goto bad;
+  }
+
+  /* --- Unpack the packet --- */
+
+  if ((c = buf_getmp(b)) == 0 ||
+      (msg >= KX_COOKIE && (hc = buf_get(b, HASHSZ)) == 0) ||
+      (msg >= KX_CHAL && (hrx = buf_get(b, HASHSZ)) == 0) ||
+      BLEFT(b)) {
+    a_warn("malformed packet from `%s'", p_name(kx->p));
+    goto bad;
+  }
+
+  IF_TRACING(T_KEYEXCH, IF_TRACING(T_CRYPTO, {
+    trace(T_CRYPTO, "crypto: challenge = %s", mpstr(c));
+    if (hc) trace_block(T_CRYPTO, "crypto: cookie", hc, HASHSZ);
+    if (hrx) trace_block(T_CRYPTO, "crypto: response hash", hrx, HASHSZ);
+  }))
+
+  /* --- First, handle a bare challenge --- *
    *
-   * If it turns out to be wrong, clear the appropriate bits of data.
+   * If the table is heavily loaded, just emit a cookie and return.
    */
 
-  if ((kx->f & KXF_YOURH) && !kx->your_gxy) {
-    kx->your_gxy = mpmont_exp(&mg, MP_NEW, kx->your_gx, kpriv.x);
-    rmd160_init(&r);
-    hashmp(&r, kx->your_gx);
-    hashmp(&r, kx->my_gx);
-    hashmp(&r, kx->your_gxy);
-    rmd160_done(&r, h);
-    IF_TRACING(T_KEYEXCH, trace_block(T_CRYPTO, "crypto: computed hash",
-                                     h, sizeof(h)); )
-    if (memcmp(h, kx->your_h, sizeof(h)) != 0) {
-      IF_TRACING(T_KEYEXCH, {
-       trace_block(T_CRYPTO, "crypto: expected hash",
-                   kx->your_h, sizeof(kx->your_h));
-       trace(T_KEYEXCH, "keyexch: hashes don't match: botched");
-      })
-      NEWCHAL(kx);
-      return;
+  if (!hc && kx->nr >= KX_THRESH) {
+    T( trace(T_KEYEXCH, "keyexch: too many challenges -- sending cookie"); )
+    b = p_txstart(kx->p, MSG_KEYEXCH | KX_COOKIE);
+    buf_putmp(b, kx->c);
+    HASH_INIT(&h);
+    HASH_STRING(&h, "tripe-cookie");
+    hashmp(&h, c);
+    HASH_DONE(&h, buf_get(b, HASHSZ));
+    p_txend(kx->p);
+    goto tidy;
+  }
+
+  /* --- Discard a packet with an invalid cookie --- */
+
+  if (hc && memcmp(hc, kx->hc, HASHSZ) != 0) {
+    a_warn("incorrect cookie from `%s'", p_name(kx->p));
+    goto bad;
+  }
+
+  /* --- Find a challenge block for this packet --- *
+   *
+   * If there isn't one already, create a new one.
+   */
+
+  if ((kxc = kxc_bychal(kx, c)) == 0) {
+    size_t x, y, z;
+    mp *r;
+
+    /* --- Be careful here --- *
+     *
+     * If this is a full challenge, and it's the first time I've seen it, I
+     * want to be able to throw it away before committing a table entry to
+     * it.
+     */
+
+    if (!hrx)
+      kxc = kxc_new(kx);       
+    else {
+      if ((r = getreply(kx, c, hrx)) == 0)
+       goto bad;
+      kxc = kxc_new(kx);
+      kxc->r = r;
     }
+    kxc->c = mp_copy(c);
+
+    /* --- Work out the cookie for this challenge --- */
+
+    HASH_INIT(&h);
+    HASH_STRING(&h, "tripe-cookie");
+    hashmp(&h, kxc->c);
+    HASH_DONE(&h, kxc->hc);    
+
+    /* --- Compute the expected-reply hash --- */
+
+    HASH_INIT(&h);
+    HASH_STRING(&h, "tripe-expected-reply");
+    hashmp(&h, kx->c);
+    hashmp(&h, kxc->c);
+    hashmp(&h, kx->rx);
+    HASH_DONE(&h, kxc->hrx);
+
+    /* --- Work out the shared key --- */
+
+    r = mpmont_exp(&mg, MP_NEW, c, kx->alpha);
+
+    /* --- Compute the switch messages --- */
+
+    HASH_INIT(&h); HASH_STRING(&h, "tripe-switch-request");
+    hashmp(&h, kx->c); hashmp(&h, kxc->c);
+    HASH_DONE(&h, kxc->hswrq_out);
+    HASH_INIT(&h); HASH_STRING(&h, "tripe-switch-confirm");
+    hashmp(&h, kx->c); hashmp(&h, kxc->c);
+    HASH_DONE(&h, kxc->hswok_out);
+
+    HASH_INIT(&h); HASH_STRING(&h, "tripe-switch-request");
+    hashmp(&h, kxc->c); hashmp(&h, kx->c);
+    HASH_DONE(&h, kxc->hswrq_in);
+    HASH_INIT(&h); HASH_STRING(&h, "tripe-switch-confirm");
+    hashmp(&h, kxc->c); hashmp(&h, kx->c);
+    HASH_DONE(&h, kxc->hswok_in);
+
+    IF_TRACING(T_KEYEXCH, IF_TRACING(T_CRYPTO, {
+      trace_block(T_CRYPTO, "crypto: computed cookie", kxc->hc, HASHSZ);
+      trace_block(T_CRYPTO, "crypto: my reply hash", kxc->hrx, HASHSZ);
+      trace(T_CRYPTO, "crypto: shared secret = %s", mpstr(r));
+      trace_block(T_CRYPTO, "crypto: outbound switch request",
+                 kxc->hswrq_out, HASHSZ);
+      trace_block(T_CRYPTO, "crypto: outbound switch confirm",
+                 kxc->hswok_out, HASHSZ);
+      trace_block(T_CRYPTO, "crypto: inbound switch request",
+                 kxc->hswrq_in, HASHSZ);
+      trace_block(T_CRYPTO, "crypto: inbound switch confirm",
+                 kxc->hswok_in, HASHSZ);
+    }))
+
+    /* --- Create a new symmetric keyset --- */
+
+    buf_init(b, buf_o, sizeof(buf_o));
+    buf_putmp(b, kx->c); x = BLEN(b);
+    buf_putmp(b, kxc->c); y = BLEN(b);
+    buf_putmp(b, r); z = BLEN(b);
+    assert(BOK(b));
+
+    kxc->ks = ks_gen(BBASE(b), x, y, z);
+    mp_drop(r);
   }
 
-  /* --- If I have a good reply, compute a shared key --- */
+  /* --- Answer the challenge if we need to --- */
 
-  if ((kx->f & KXF_YOURH) && (kx->f & KXF_REPLY)) {
-    k_shared = mpmont_exp(&mg, MP_NEW, kx->your_gx, kx->my_x);
-    IF_TRACING(T_KEYEXCH, {
-      trace(T_KEYEXCH, "keyexch: computed shared key");
-      trace(T_CRYPTO, "crypto: shared key = %s", mpstr(k_shared));
-    })
-    buf_init(&b, buf_o, sizeof(buf_o));
-    buf_putmp(&b, k_shared); assert(BOK(&b));
-    settimer(kx, ks_gen(kx->ks, BBASE(&b), BLEN(&b)));
-    mp_drop(k_shared);
-    BURN(buf_o);
-    kx->f |= KXF_DONE;
+  if (hrx && !kxc->r) {
+    mp *r;
+    if ((r = getreply(kx, c, hrx)) == 0)
+      goto bad;
+    kxc->r = r;
   }
+
+  kxc_answer(kx, kxc);
+
+  /* --- Tidy up and go home --- */
+
+tidy:
+  mp_drop(c);
+  return (0);
+
+bad:
+  mp_drop(c);
+  return (-1);
 }
 
-/* --- @resend_chal@, @resent_resp@ --- *
+/* --- @resend@ --- *
  *
  * Arguments:  @keyexch *kx@ = pointer to key exchange context
- *             @time_t now@ = the time right now
  *
  * Returns:    ---
  *
- * Use:                Sends packets to the remote host, according to the various
- *             timers and information available.
+ * Use:                Sends the next message for a key exchange.
  */
 
-void resend_chal(keyexch *kx, time_t now)
+static void resend(keyexch *kx)
 {
+  kxchal *kxc;
+  buf bb;
+  stats *st = p_stats(kx->p);
   buf *b;
 
-  if (kx->f & KXF_DONE)
-    return;
-  if (ISQ_CHAL(kx, now)) {
-    T( trace(T_KEYEXCH, "keyexch: not sending a new challenge yet"); )
-    return;
+  switch (kx->s) {
+    case KXS_CHAL:
+      T( trace(T_KEYEXCH, "keyexch: sending prechallenge to `%s'",
+              p_name(kx->p)); )
+      b = p_txstart(kx->p, MSG_KEYEXCH | KX_PRECHAL);
+      buf_putmp(b, kx->c);
+      break;
+    case KXS_COMMIT:
+      T( trace(T_KEYEXCH, "keyexch: sending switch request to `%s'",
+              p_name(kx->p)); )
+      kxc = kx->r[0];
+      b = p_txstart(kx->p, MSG_KEYEXCH | KX_SWITCH);
+      buf_put(b, kx->hc, HASHSZ);
+      buf_put(b, kxc->hc, HASHSZ);
+      buf_init(&bb, buf_i, sizeof(buf_i));
+      buf_putmp(&bb, kxc->r);
+      buf_put(&bb, kxc->hswrq_out, HASHSZ);
+      buf_flip(&bb);
+      ks_encrypt(kxc->ks, &bb, b);
+      break;
+    case KXS_SWITCH:
+      T( trace(T_KEYEXCH, "keyexch: sending switch confirmation to `%s'",
+              p_name(kx->p)); )
+      kxc = kx->r[0];
+      b = p_txstart(kx->p, MSG_KEYEXCH | KX_SWITCHOK);
+      buf_init(&bb, buf_i, sizeof(buf_i));
+      buf_put(&bb, kxc->hswok_out, HASHSZ);
+      buf_flip(&bb);
+      ks_encrypt(kxc->ks, &bb, b);
+      break;
+    default:
+      abort();
   }
-  if (!kx->your_gx) {
-    T( trace(T_KEYEXCH, "keyexch: sending prechallenge"); )
-    b = p_txstart(kx->p, MSG_PRECHALLENGE);
-  } else {
-    T( trace(T_KEYEXCH, "keyexch: sending challenge"); )
-    b = p_txstart(kx->p, MSG_CHALLENGE);
-    buf_put(b, kx->my_h, sizeof(kx->my_h));
-  }
-  buf_putmp(b, kx->my_gx);
-  p_txend(kx->p);
-  kx->t_qchal = now + T_QUIET;
-  settimer(kx, now + T_RETRY);
+
+  if (BOK(b)) {
+    st->n_kxout++;
+    st->sz_kxout += BLEN(b);
+    p_txend(kx->p);
+  }
+
+  if (kx->s < KXS_SWITCH)
+    settimer(kx, time(0) + T_RETRY);
 }
 
-void resend_resp(keyexch *kx, time_t now)
+/* --- @matchreply@ --- *
+ *
+ * Arguments:  @keyexch *kx@ = pointer to key exchange context
+ *             @const octet *hc_in@ = a hash of his challenge
+ *             @const octet *hc_out@ = a hash of my challenge (cookie)
+ *             @const octet *krx@ = his expected-reply hash (optional)
+ *             @buf *b@ = encrypted remainder of the packet
+ *
+ * Returns:    A pointer to the challenge block if OK, or null on failure.
+ *
+ * Use:                Checks a reply or switch packet, ensuring that its contents
+ *             are sensible and correct.  If they are, @*b@ is set to point
+ *             to the remainder of the encrypted data, and the correct
+ *             challenge is returned.
+ */
+
+static kxchal *matchreply(keyexch *kx, const octet *hc_in,
+                         const octet *hc_out, const octet *hrx, buf *b)
 {
-  buf *b;
+  kxchal *kxc;
+  buf bb;
+  mp *r = 0;
+
+  /* --- Check the plaintext portions of the data --- */
+
+  IF_TRACING(T_KEYEXCH, IF_TRACING(T_CRYPTO, {
+    trace_block(T_CRYPTO, "crypto: challenge", hc_in, HASHSZ);
+    trace_block(T_CRYPTO, "crypto: cookie", hc_out, HASHSZ);
+    if (hrx) trace_block(T_CRYPTO, "crypto: response hash", hrx, HASHSZ);
+  }))
+  if (memcmp(hc_out, kx->hc, HASHSZ) != 0) {
+    a_warn("incorrect cookie from `%s'", p_name(kx->p));
+    goto bad;
+  }
+  if ((kxc = kxc_byhc(kx, hc_in)) == 0) {
+    a_warn("received reply for unknown challenge from `%s'", p_name(kx->p));
+    goto bad;
+  }
 
-  if (!kx->your_gxy)
-    return;
-  if (ISQ_RESP(kx, now)) {
-    T( trace(T_KEYEXCH, "keyexch: not sending a new response yet"); )
-    return;
+  /* --- Maybe compute a reply for the challenge --- */
+
+  if (!kxc->r) {
+    if (!hrx) {
+      a_warn("unexpected switch request from `%s'", p_name(kx->p));
+      goto bad;
+    }
+    if ((r = getreply(kx, kxc->c, hrx)) == 0)
+      goto bad;
+    kxc->r = r;
+    r = 0;
   }
-  T( trace(T_KEYEXCH, "keyexch: sending response"); )
-  b = p_txstart(kx->p, MSG_RESPONSE);
-  buf_putmp(b, kx->your_gxy);
-  p_txend(kx->p);
-  kx->t_qresp = now + T_QUIET;
+
+  /* --- Decrypt the rest of the packet --- */
+
+  buf_init(&bb, buf_o, sizeof(buf_o));
+  if (ks_decrypt(kxc->ks, b, &bb)) {
+    a_warn("failed to decrypt reply from `%s'", p_name(kx->p));
+    goto bad;
+  }
+  buf_init(b, BBASE(&bb), BLEN(&bb));
+  if ((r = buf_getmp(b)) == 0) {
+    a_warn("invalid reply packet from `%s'", p_name(kx->p));
+    goto bad;
+  }
+  IF_TRACING(T_KEYEXCH, IF_TRACING(T_CRYPTO, {
+    trace(T_CRYPTO, "crypto: reply = %s", mpstr(r));
+  }))
+  if (!mp_eq(r, kx->rx)) {
+    a_warn("incorrect reply from `%s'", p_name(kx->p));
+    goto bad;
+  }
+
+  /* --- Done --- */
+
+  mp_drop(r);
+  return (kxc);
+
+bad:
+  mp_drop(r);
+  return (0);
 }
 
-/* --- @kx_start@ --- *
+/* --- @commit@ --- *
  *
  * Arguments:  @keyexch *kx@ = pointer to key exchange context
+ *             @kxchal *kxc@ = pointer to challenge to commit to
  *
  * Returns:    ---
  *
- * Use:                Stimulates a key exchange.  If a key exchage is in progress,
- *             a new challenge is sent (unless the quiet timer forbids
- *             this); if no exchange is in progress, one is commenced.
+ * Use:                Commits to a particular challenge as being the `right' one,
+ *             since a reply has arrived for it.
  */
 
-void kx_start(keyexch *kx)
+static void commit(keyexch *kx, kxchal *kxc)
 {
-  time_t now = time(0);
+  unsigned i;
 
-  if (!ISVALID(kx, now))
-    INITCTX(kx, now);
-  update(kx);
-  resend_chal(kx, now);
+  for (i = 0; i < kx->nr; i++) {
+    if (kx->r[i] != kxc)
+      kxc_destroy(kx->r[i]);
+  }
+  kx->r[0] = kxc;
+  kx->nr = 1;
+  kxc_stoptimer(kxc);
+  ksl_link(kx->ks, kxc->ks);  
 }
 
-/* --- @dochallenge@ --- *
+/* --- @doreply@ --- *
+ *
+ * Arguments:  @keyexch *kx@ = pointer to key exchange context
+ *             @buf *b@ = buffer containing packet
+ *
+ * Returns:    Zero if OK, nonzero if the packet was rejected.
+ *
+ * Use:                Handles a reply packet.  This doesn't handle the various
+ *             switch packets: they're rather too different.
+ */
+
+static int doreply(keyexch *kx, buf *b)
+{
+  const octet *hc_in, *hc_out, *hrx;
+  kxchal *kxc;
+
+  if (kx->s != KXS_CHAL && kx->s != KXS_COMMIT) {
+    a_warn("unexpected reply from `%s'", p_name(kx->p));
+    goto bad;
+  }
+  if ((hc_in = buf_get(b, HASHSZ)) == 0 ||
+      (hc_out = buf_get(b, HASHSZ)) == 0 ||
+      (hrx = buf_get(b, HASHSZ)) == 0) {
+    a_warn("invalid reply packet from `%s'", p_name(kx->p));
+    goto bad;
+  }
+  if ((kxc = matchreply(kx, hc_in, hc_out, hrx, b)) == 0)
+    goto bad;
+  if (BLEFT(b)) {
+    a_warn("invalid reply packet from `%s'", p_name(kx->p));
+    goto bad;
+  }
+  if (kx->s == KXS_CHAL) {
+    commit(kx, kxc);
+    kx->s = KXS_COMMIT;
+  }
+  resend(kx);
+  return (0);
+
+bad:
+  return (-1);
+}
+
+/* --- @doswitch@ --- *
+ *
+ * Arguments:  @keyexch *kx@ = pointer to key exchange block
+ *             @buf *b@ = pointer to buffer containing packet
+ *
+ * Returns:    Zero if OK, nonzero if the packet was rejected.
+ *
+ * Use:                Handles a reply with a switch request bolted onto it.
+ */
+
+static int doswitch(keyexch *kx, buf *b)
+{
+  const octet *hc_in, *hc_out, *hswrq;
+  kxchal *kxc;
+
+  if ((hc_in = buf_get(b, HASHSZ)) == 0 ||
+      (hc_out = buf_get(b, HASHSZ)) == 0) {
+    a_warn("invalid switch request from `%s'", p_name(kx->p));
+    goto bad;
+  }
+  if ((kxc = matchreply(kx, hc_in, hc_out, 0, b)) == 0)
+    goto bad;
+  if ((hswrq = buf_get(b, HASHSZ)) == 0 || BLEFT(b)) {
+    a_warn("invalid switch request from `%s'", p_name(kx->p));
+    goto bad;
+  }
+  IF_TRACING(T_KEYEXCH, {
+    trace_block(T_CRYPTO, "crypto: switch request hash", hswrq, HASHSZ);
+  })
+  if (memcmp(hswrq, kxc->hswrq_in, HASHSZ) != 0) {
+    a_warn("incorrect switch request hash from `%s'", p_name(kx->p));
+    goto bad;
+  }
+  switch (kx->s) {
+    case KXS_CHAL:
+      commit(kx, kxc);
+    case KXS_COMMIT:
+      ks_activate(kxc->ks);
+      settimer(kx, ks_tregen(kxc->ks));
+      kx->s = KXS_SWITCH;
+      break;
+  }
+  resend(kx);
+  return (0);
+
+bad:
+  return (-1);
+}
+
+/* --- @doswitchok@ --- *
+ *
+ * Arguments:  @keyexch *kx@ = pointer to key exchange block
+ *             @buf *b@ = pointer to buffer containing packet
+ *
+ * Returns:    Zero if OK, nonzero if the packet was rejected.
+ *
+ * Use:                Handles a reply with a switch request bolted onto it.
+ */
+
+static int doswitchok(keyexch *kx, buf *b)
+{
+  const octet *hswok;
+  kxchal *kxc;
+  buf bb;
+
+  if (kx->s < KXS_COMMIT) {
+    a_warn("unexpected switch confirmation from `%s'", p_name(kx->p));
+    goto bad;
+  }
+  kxc = kx->r[0];
+  buf_init(&bb, buf_o, sizeof(buf_o));
+  if (ks_decrypt(kxc->ks, b, &bb)) {
+    a_warn("failed to decrypt switch confirmation from `%s'", p_name(kx->p));
+    goto bad;
+  }
+  buf_init(b, BBASE(&bb), BLEN(&bb));
+  if ((hswok = buf_get(b, HASHSZ)) == 0 || BLEFT(b)) {
+    a_warn("invalid switch confirmation from `%s'", p_name(kx->p));
+    goto bad;
+  }
+  IF_TRACING(T_KEYEXCH, {
+    trace_block(T_CRYPTO, "crypto: switch confirmation hash", hswok, HASHSZ);
+  })
+  if (memcmp(hswok, kxc->hswok_in, HASHSZ) != 0) {
+    a_warn("incorrect switch confirmation hash from `%s'", p_name(kx->p));
+    goto bad;
+  }
+  if (kx->s < KXS_SWITCH) {
+    ks_activate(kxc->ks);
+    settimer(kx, ks_tregen(kxc->ks));
+    kx->s = KXS_SWITCH;
+  }
+  return (0);
+
+bad:
+  return (-1);  
+}
+
+/*----- Main code ---------------------------------------------------------*/
+
+/* --- @stop@ --- *
  *
  * Arguments:  @keyexch *kx@ = pointer to key exchange context
- *             @time_t now@ = the current time
- *             @mp *m@ = new challenge received
- *             @const octet *h@ = challenge hash (if any)
  *
  * Returns:    ---
  *
- * Use:                Common code for handling challenge messages.  The caller
- *             should have successfullly unpacked the challenge structure.
+ * Use:                Stops a key exchange dead in its tracks.  Throws away all of
+ *             the context information.  The context is left in an
+ *             inconsistent state.  The only functions which understand this
+ *             state are @kx_free@ and @kx_init@ (which cause it internally
+ *             it), and @start@ (which expects it to be the prevailing
+ *             state).
  */
 
-static void dochallenge(keyexch *kx, time_t now, mp *m, const octet *h)
+static void stop(keyexch *kx)
 {
-  unsigned f = 0;
-#define f_newchal 1u
-#define f_newhash 2u
-#define f_new (f_newchal | f_newhash)
-#define f_match 4u
-#define f_good (f_new | f_match)
-#define f_ignore 8u
-#define f_reset 16u
-#define f_change (f_new | f_reset)
-
-  /* --- Restart the process if necessary --- */
-
-  if (!ISVALID(kx, now))
-    INITCTX(kx, now);
-  if (!ISNEWCHAL(kx, now))
-    f |= f_ignore;
-
-  /* --- Sort out what to actually do --- */
-  
-  if (!kx->your_gx) {
-    f |= f_newchal;
-    if (h)
-      f |= f_newhash;
-  } else if (mp_eq(kx->your_gx, m)) {
-    if (!h || memcmp(h, kx->your_h, sizeof(kx->your_h)) == 0)
-      f |= f_match;
-    else if (!(kx->f & KXF_YOURH))
-      f |= f_newhash;
-  }
-
-  /* --- Update the values in the context --- */
-
-  if (f & f_good)
-    f &= ~f_ignore;
-  else if (!(f & f_ignore)) {
-    NEWCHAL(kx);
-    f |= f_reset;
-  }
-  if (f & (f_newchal | f_reset))
-    kx->your_gx = MP_COPY(m);
-  if (f & (f_newhash | f_reset)) {
-    memcpy(kx->your_h, h, sizeof(kx->your_h));
-    kx->f |= KXF_YOURH;  
-  }
-  if (f & f_new)
-    kx->t_qchal = 0;    
-
-  if (!(f & f_good)) {
-    a_warn("%s nonmatching challenge from `%s'",
-          (f & f_ignore) ? "rejecting" : "accepting", p_name(kx->p));
-  } else {
-    T( trace(T_KEYEXCH, "keyexch: good challenge (%s, %s) from `%s'",
-            (f & f_newchal) ? "new gxy" : "match gxy",
-            (f & f_newhash) ? "new hash" : "match hash", p_name(kx->p)); )
-  }
-  if (f & f_change)
-    update(kx);
-
-  if (f & f_new)
-    resend_chal(kx, now);
-#undef f_newchal
-#undef f_newhash
-#undef f_new
-#undef f_match
-#undef f_good
-#undef f_ignore
-#undef f_change
+  unsigned i;
+
+  if (kx->f & KXF_DEAD)
+    return;
+
+  if (kx->f & KXF_TIMER)
+    sel_rmtimer(&kx->t);
+  for (i = 0; i < kx->nr; i++)
+    kxc_destroy(kx->r[i]);
+  mp_drop(kx->alpha);
+  mp_drop(kx->c);
+  mp_drop(kx->rx);
+  kx->t_valid = 0;
+  kx->f |= KXF_DEAD;
+  kx->f &= ~KXF_TIMER;
 }
 
-/* --- @kx_prechallenge@, @kx_challenge@ --- *
+/* --- @start@ --- *
  *
- * Arguments:  @keyexch *kx@ = pointer to key exhange context
- *             @buf *b@ = pointer to buffer containing the packet
+ * Arguments:  @keyexch *kx@ = pointer to key exchange context
+ *             @time_t now@ = the current time
  *
  * Returns:    ---
  *
- * Use:                Handle prechallenges and challenges.
+ * Use:                Starts a new key exchange with the peer.  The context must be
+ *             in the bizarre state left by @stop@ or @kx_init@.
  */
 
-void kx_prechallenge(keyexch *kx, buf *b)
+static void start(keyexch *kx, time_t now)
 {
-  time_t now = time(0);
-  mp *m;
+  HASH_CTX h;
+
+  assert(kx->f & KXF_DEAD);
+
+  kx->f &= ~KXF_DEAD;
+  kx->nr = 0;
+  kx->alpha = mprand_range(MP_NEW, kpriv.dp.q, &rand_global, 0);
+  kx->c = mpmont_exp(&mg, MP_NEW, kpriv.dp.g, kx->alpha);
+  kx->rx = mpmont_exp(&mg, MP_NEW, kx->kpub.y, kx->alpha);
+  kx->s = KXS_CHAL;
+  kx->t_valid = now + T_VALID;
+
+  HASH_INIT(&h);
+  HASH_STRING(&h, "tripe-cookie");
+  hashmp(&h, kx->c);
+  HASH_DONE(&h, kx->hc);
+
+  IF_TRACING(T_KEYEXCH, {
+    trace(T_KEYEXCH, "keyexch: creating new challenge");
+    IF_TRACING(T_CRYPTO, {
+      trace(T_CRYPTO, "crypto: secret = %s", mpstr(kx->alpha));
+      trace(T_CRYPTO, "crypto: challenge = %s", mpstr(kx->c));
+      trace(T_CRYPTO, "crypto: expected response = %s", mpstr(kx->rx));
+      trace_block(T_CRYPTO, "crypto: challenge cookie", kx->hc, HASHSZ);
+    })
+  })
+}
 
-  if ((m = buf_getmp(b, MP_NEW)) == 0 || BLEFT(b)) {
-    a_warn("malformed prechallenge from `%s'", p_name(kx->p));
-    goto tidy;
+/* --- @checkpub@ --- *
+ *
+ * Arguments:  @keyexch *kx@ = pointer to key exchange context
+ *
+ * Returns:    Zero if OK, nonzero if the peer's public key has expired.
+ *
+ * Use:                Deactivates the key-exchange until the peer acquires a new
+ *             public key.
+ */
+
+static int checkpub(keyexch *kx)
+{
+  time_t now;
+  if (kx->f & KXF_DEAD)
+    return (-1);
+  now = time(0);
+  if (KEY_EXPIRED(now, kx->texp_kpub)) {
+    stop(kx);
+    a_warn("public key for `%s' has expired", p_name(kx->p));
+    dh_pubfree(&kx->kpub);
+    kx->f &= ~KXF_PUBKEY;
+    return (-1);
   }
-  dochallenge(kx, now, m, 0);
-tidy:
-  mp_drop(m);
+  return (0);
 }
 
-void kx_challenge(keyexch *kx, buf *b)
+/* --- @kx_start@ --- *
+ *
+ * Arguments:  @keyexch *kx@ = pointer to key exchange context
+ *
+ * Returns:    ---
+ *
+ * Use:                Stimulates a key exchange.  If a key exchage is in progress,
+ *             a new challenge is sent (unless the quiet timer forbids
+ *             this); if no exchange is in progress, one is commenced.
+ */
+
+void kx_start(keyexch *kx)
 {
   time_t now = time(0);
-  mp *m = 0;
-  const octet *h;
 
-  if (buf_ensure(b, RMD160_HASHSZ) ||
-      (h = BCUR(b), BSTEP(b, RMD160_HASHSZ), 
-       (m = buf_getmp(b, MP_NEW)) == 0 || BLEFT(b))) {
-    a_warn("malformed challenge from `%s'", p_name(kx->p));
-    goto tidy;
+  if (checkpub(kx))
+    return;
+  if (!ISVALID(kx, now)) {
+    stop(kx);
+    start(kx, now);
   }
-  dochallenge(kx, now, m, h);
-  resend_resp(kx, now);
-tidy:
-  mp_drop(m);
+  resend(kx);
 }
 
-/* --- @kx_response@ --- *
+/* --- @kx_message@ --- *
  *
  * Arguments:  @keyexch *kx@ = pointer to key exchange context
- *             @buf *b@ = a buffer containing the packet to read
+ *             @unsigned msg@ = the message code
+ *             @buf *b@ = pointer to buffer containing the packet
  *
  * Returns:    ---
  *
- * Use:                Reads a response from the buffer and handles it.
+ * Use:                Reads a packet containing key exchange messages and handles
+ *             it.
  */
 
-void kx_response(keyexch *kx, buf *b)
+void kx_message(keyexch *kx, unsigned msg, buf *b)
 {
   time_t now = time(0);
-  mp *m;
+  stats *st = p_stats(kx->p);
+  size_t sz = BSZ(b);
+  int rc;
+
+#ifndef NTRACE
+  static const char *const pkname[] = {
+    "prechallenge", "cookie", "challenge",
+    "reply", "switch request", "switch confirmation"
+  };
+#endif
+
+  if (checkpub(kx))
+    return;
 
-  if ((m = buf_getmp(b, MP_NEW)) == 0 || BLEFT(b)) {
-    a_warn("malformed response from `%s'", p_name(kx->p));
-    goto tidy;
-  }
-  if (!ISVALID(kx, now))
-    INITCTX(kx, now);
-  if (!(kx->f & KXF_MYH)) {
-    a_warn("premature response from `%s'", p_name(kx->p));
-    goto tidy;
+  if (!ISVALID(kx, now)) {
+    stop(kx);
+    start(kx, now);
   }
-  if (!mp_eq(m, kx->my_gxy)) {
-    a_warn("incorrect response from `%s'", p_name(kx->p));
-    goto tidy;
+
+  T( trace(T_KEYEXCH, "keyexch: processing %s packet from `%s'",
+          msg < KX_NMSG ? pkname[msg] : "unknown", p_name(kx->p)); )
+
+  switch (msg) {
+    case KX_PRECHAL:
+    case KX_COOKIE:
+    case KX_CHAL:
+      rc = dochallenge(kx, msg, b);
+      break;
+    case KX_REPLY:
+      rc = doreply(kx, b);
+      break;
+    case KX_SWITCH:
+      rc = doswitch(kx, b);
+      break;
+    case KX_SWITCHOK:
+      rc = doswitchok(kx, b);
+      break;
+    default:
+      a_warn("unexpected key exchange message type %u from `%p'",
+            p_name(kx->p));
+      rc = -1;
+      break;
   }
-  T( trace(T_KEYEXCH, "keyexch: valid response from `%s'", p_name(kx->p)); )
-  kx->f |= KXF_REPLY;
-  update(kx);
 
-tidy:
-  mp_drop(m);
+  if (rc)
+    st->n_reject++;
+  else {
+    st->n_kxin++;
+    st->sz_kxin += sz;
+  }
 }
 
 /* --- @kx_free@ --- *
@@ -487,10 +1034,9 @@ tidy:
 
 void kx_free(keyexch *kx)
 {
-  if (kx->f & KXF_TIMER)
-    sel_rmtimer(&kx->t);
-  FREECTX(kx);
-  dh_pubfree(&kx->kpub);
+  stop(kx);
+  if (kx->f & KXF_PUBKEY)
+    dh_pubfree(&kx->kpub);
 }
 
 /* --- @kx_newkeys@ --- *
@@ -509,14 +1055,18 @@ void kx_newkeys(keyexch *kx)
 {
   dh_pub dp;
 
-  if (km_getpubkey(p_name(kx->p), &dp))
+  if (km_getpubkey(p_name(kx->p), &dp, &kx->texp_kpub))
     return;
-  dh_pubfree(&kx->kpub);
+  if (kx->f & KXF_PUBKEY)
+    dh_pubfree(&kx->kpub);
   kx->kpub = dp;
-  if (!(kx->f & KXF_DONE)) {
+  kx->f |= KXF_PUBKEY;
+  if ((kx->f & KXF_DEAD) || kx->s != KXS_SWITCH) {
     T( trace(T_KEYEXCH, "keyexch: restarting key negotiation with `%s'",
             p_name(kx->p)); )
-    INITCTX(kx, time(0));
+    stop(kx);
+    start(kx, time(0));
+    resend(kx);
   }
 }
 
@@ -537,11 +1087,11 @@ int kx_init(keyexch *kx, peer *p, keyset **ks)
 {
   kx->ks = ks;
   kx->p = p;
-  kx->f = 0;
-  if (km_getpubkey(p_name(p), &kx->kpub))
+  if (km_getpubkey(p_name(p), &kx->kpub, &kx->texp_kpub))
     return (-1);
-  kx->t_valid = 0;
-  kx_start(kx);
+  kx->f = KXF_DEAD | KXF_PUBKEY;
+  start(kx, time(0));
+  resend(kx);
   return (0);
 }