Support elliptic curves, and bigger hashes.
[become] / src / check.c
index 9ed0b6f..f6560d5 100644 (file)
@@ -1,13 +1,13 @@
 /* -*-c-*-
  *
- * $Id: check.c,v 1.1 1997/07/21 13:47:53 mdw Exp $
+ * $Id: check.c,v 1.14 2004/04/17 10:46:08 mdw Exp $
  *
  * Check validity of requests
  *
- * (c) 1997 EBI
+ * (c) 1998 EBI
  */
 
-/*----- Licencing notice --------------------------------------------------*
+/*----- Licensing notice --------------------------------------------------*
  *
  * This file is part of `become'
  *
  * GNU General Public License for more details.
  *
  * You should have received a copy of the GNU General Public License
- * along with `become'; if not, write to the Free Software
- * Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
- */
-
-/*----- Revision history --------------------------------------------------*
- *
- * $Log: check.c,v $
- * Revision 1.1  1997/07/21 13:47:53  mdw
- * Initial revision
- *
+ * along with `become'; if not, write to the Free Software Foundation,
+ * Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
  */
 
 /*----- Header files ------------------------------------------------------*/
 
 #include <arpa/inet.h>
 
+#include <fcntl.h>
 #include <netdb.h>
 #include <unistd.h>
 
+/* --- mLib headers --- */
+
+#include <mLib/alloc.h>
+#include <mLib/quis.h>
+#include <mLib/report.h>
+#include <mLib/sym.h>
+#include <mLib/trace.h>
+
+/* --- Catacomb headers --- */
+
+#include <catacomb/buf.h>
+#include <catacomb/gdsa.h>
+#include <catacomb/key.h>
+#include <catacomb/dh.h>
+#include <catacomb/ec-keys.h>
+#include <catacomb/mp.h>
+#include <catacomb/noise.h>
+#include <catacomb/rand.h>
+
 /* --- Local headers --- */
 
 #include "become.h"
 #include "config.h"
-#include "crypt.h"
-#include "idea.h"
 #include "lexer.h"
 #include "name.h"
+#include "netg.h"
 #include "rule.h"
-#include "parser.h"
-#include "tx.h"
-#include "utils.h"
+#include "parse.h"
+#include "userdb.h"
 
-/*----- Main code ---------------------------------------------------------*/
+/*----- Client-end network support ----------------------------------------*/
 
-/* --- @check__client@ --- *
+#ifndef NONETWORK
+
+/* --- @check__send@ --- *
+ *
+ * Arguments:  @char *buf@ = pointer to encrypted request
+ *             @size_t sz@ = size of request
+ *             @int fd@ = socket to send from
+ *             @struct sockaddr_in *serv@ = pointer to table of servers
+ *             @size_t n_serv@ = number of servers
+ *
+ * Returns:    ---
+ *
+ * Use:                Sends the request packet to the list of servers.  If the
+ *             message couldn't be sent to any of them, an error is
+ *             reported.
+ */
+
+static void check__send(char *buf, size_t sz, int fd,
+                       struct sockaddr_in *serv, size_t n_serv)
+{
+  size_t i;
+  int ok = 0;
+  int err = 0;
+
+  for (i = 0; i < n_serv; i++) {
+    if (sendto(fd, buf, sz, 0,
+              (struct sockaddr *)(serv + i), sizeof(serv[i])) < 0) {
+      T( trace(TRACE_CLIENT, "client: send to %s failed: %s",
+              inet_ntoa(serv[i].sin_addr), strerror(errno)); )
+      err = errno;
+    } else
+      ok = 1;
+  }
+
+  if (!ok)
+    die(1, "couldn't send request to server: %s", strerror(err));
+}
+
+/* --- @check__ask@ --- *
  *
  * Arguments:  @request *rq@ = pointer to request buffer
- *             @const char *serv@ = pointer to the server
- *             @int port@ = port number to use
+ *             @struct sockaddr_in *serv@ = pointer to table of servers
+ *             @size_t n_serv@ = number of servers
  *
  * Returns:    Nonzero if OK, zero if forbidden
  *
- * Use:                Contacts a server to decide whether the request is OK.
+ * Use:                Contacts a number of servers to decide whether the request
+ *             is OK.
  */
 
-static int check__client(request *rq, const char *serv, int port)
+static int check__ask(request *rq, struct sockaddr_in *serv, size_t n_serv)
 {
+  static int tbl[] = { 0, 5, 10, 20, -1 };
+
+  char buff[2048], rbuff[2048];
+  size_t rqlen;
+  gdsa g;
+  const char *p;
+  ghash *h;
+  key_packdef *kp;
+  buf b;
   int fd;
-  struct sockaddr_in ssin;
-  unsigned char crq[crq_size];
-  unsigned char k[IDEA_KEYSIZE];
-  unsigned char sk[IDEA_KEYSIZE];
-  time_t t;
-  pid_t pid;
+  struct sockaddr_in sin;
+  socklen_t slen;
+  ssize_t sz;
+  int ans;
+  fd_set fds;
+  struct timeval start, now, tv;
+  gdsa_sig s;
+  key_file f;
+  key *k;
+  key_iter ki;
+  int ind;
+  size_t i;
+
+  /* --- Open the public keyring --- */
+
+  if ((key_open(&f, file_PUBKEY, KOPEN_READ, key_moan, 0)) != 0)
+    die(1, "couldn't open public keyring");
+
+  /* --- Build the request packet --- */
+
+  rand_noisesrc(RAND_GLOBAL, &noise_source);
+  rand_seed(RAND_GLOBAL, 160);
+  buf_init(&b, buff, sizeof(buff));
+  rand_get(RAND_GLOBAL, buf_get(&b, 64), 64);
+  buf_putu32(&b, rq->from);
+  buf_putu32(&b, rq->to);
+  buf_putu16(&b, strlen(rq->cmd));
+  buf_put(&b, rq->cmd, strlen(rq->cmd));
+  rqlen = BLEN(&b);
 
   /* --- Create my socket --- */
 
   if ((fd = socket(PF_INET, SOCK_DGRAM, 0)) < 0)
-    die("couldn't create socket: %s", strerror(errno));
+    die(1, "couldn't create socket: %s", strerror(errno));
+  if (fcntl(fd, F_SETFD, 1) < 0)
+    die(1, "couldn't set close-on-exec flag for socket: %s", strerror(errno));
 
   /* --- Bind myself to some address --- */
 
-  {
-    struct sockaddr_in sin;
-
-    sin.sin_family = AF_INET;
-    sin.sin_port = 0;
-    sin.sin_addr.s_addr = htonl(INADDR_ANY);
-
-    if (bind(fd, (struct sockaddr *)&sin, sizeof(sin)) < 0)
-      die("couldn't bind socket to address: %s", strerror(errno));
-  }
-
-  /* --- Find the server's address --- */
+  sin.sin_family = AF_INET;
+  sin.sin_port = 0;
+  sin.sin_addr.s_addr = htonl(INADDR_ANY);
 
-  {
-    struct hostent *he;
-
-    /* --- Resolve the server's name --- */
+  if (bind(fd, (struct sockaddr *)&sin, sizeof(sin)) < 0)
+    die(1, "couldn't bind socket to address: %s", strerror(errno));
 
-    if ((he = gethostbyname(serv)) == 0)
-      die("couldn't find server host `%s'", serv);
+  /* --- Find out when we are --- */
 
-    /* --- Build the address block --- */
+  gettimeofday(&start, 0);
+  ind = 0;
 
-    ssin.sin_family = AF_INET;
-    ssin.sin_port = htons(port);
-    memcpy(&ssin.sin_addr, he->h_addr, sizeof(struct in_addr));
-  }
+  /* --- Now loop until everything's done --- */
 
-  /* --- Read in the encryption key --- */
+  for (;;) {
+    gettimeofday(&now, 0);
 
-  {
-    FILE *fp;
+    /* --- If the current timer has expired, find one that hasn't --- *
+     *
+     * Also resend the request after I've found a timer which is still
+     * extant.  If there aren't any, report an error.
+     */
 
-    if ((fp = fopen(file_KEY, "r")) == 0) {
-      die("couldn't open key file `%s': %s", file_KEY,
-         strerror(errno));
+    if (now.tv_sec >= start.tv_sec + tbl[ind] &&
+       now.tv_usec >= start.tv_usec) {
+      do {
+       ind++;
+       if (tbl[ind] < 0)
+         die(1, "no reply from servers");
+      } while (now.tv_sec >= start.tv_sec + tbl[ind] &&
+              now.tv_usec >= start.tv_usec);
+      check__send(buff, rqlen, fd, serv, n_serv);
+      T( trace(TRACE_CLIENT, "client: send request to servers"); )
     }
-    tx_getBits(k, 128, fp);
-  }
-
-  /* --- Now build a request packet --- */
 
-  t = time(0);
-  pid = getpid();
-  crypt_packRequest(rq, crq, t, pid, k, sk);
+    /* --- Now wait for a packet to arrive --- */
 
-  /* --- Send the packet to the server --- */
+    if (now.tv_usec > start.tv_usec) {
+      now.tv_usec -= 1000000;
+      now.tv_sec += 1;
+    }
+    tv.tv_sec = start.tv_sec + tbl[ind] - now.tv_sec;
+    tv.tv_usec = start.tv_usec - now.tv_usec;
+
+    /* --- Sort out file descriptors to watch --- */
+
+    FD_ZERO(&fds);
+    FD_SET(fd, &fds);
+
+    /* --- Wait for them --- */
+
+    i = select(FD_SETSIZE, &fds, 0, 0, &tv);
+    if (i == 0 || (i < 0 && errno == EINTR))
+      continue;
+    if (i < 0)
+      die(1, "error waiting for reply: %s", strerror(errno));
+
+    /* --- Read the reply data --- */
+
+    slen = sizeof(sin);
+    if ((sz = recvfrom(fd, (char *)rbuff, sizeof(rbuff), 0,
+                      (struct sockaddr *)&sin, &slen)) < 0)
+      die(1, "error reading server's reply: %s", strerror(errno));
+
+    IF_TRACING(TRACE_CLIENT, {
+      struct hostent *h = gethostbyaddr((char *)&sin.sin_addr,
+                                       sizeof(sin.sin_addr), AF_INET);
+      trace(TRACE_CLIENT, "client: reply received from %s port %i",
+           h ? h->h_name : inet_ntoa(sin.sin_addr),
+           ntohs(sin.sin_port));
+    })
+
+    /* --- Verify the sender --- *
+     *
+     * This is more to avoid confusion than for security: an active
+     * attacker is quite capable of forging the source address.  We rely
+     * on the signature in the reply packet for authentication.
+     */
+
+    for (i = 0; i < n_serv; i++) {
+      if (sin.sin_addr.s_addr == serv[i].sin_addr.s_addr &&
+         sin.sin_port == serv[i].sin_port)
+       break;
+    }
+    if (i >= n_serv) {
+      T( trace(TRACE_CLIENT, "client: reply from unknown host"); )
+      continue;
+    }
 
-  if (sendto(fd, (char *)crq, sizeof(crq), 0,
-            (struct sockaddr *)&ssin, sizeof(ssin)) < 0) {
-    burn(k);
-    die("couldn't send request to server: %s", strerror(errno));
+    /* --- The hash length varies with the key --- *
+     *
+     * So we have to unpack once for each key.  This isn't too bad.
+     */
+
+    g.r = &rand_global;
+    g.u = 0;
+    key_mkiter(&ki, &f);
+    while ((k = key_next(&ki)) != 0) {
+      if (key_expired(k)) continue;
+      if (strcmp(k->type, "become") != 0) continue;
+
+      /* --- Get a hash function --- */
+
+      if ((p = key_getattr(&f, k, "hash")) == 0)
+       p = "sha";
+      if ((g.h = ghash_byname(p)) == 0)
+       continue;
+
+      /* --- Unpack the key --- */
+
+      p = key_getattr(&f, k, "sig");
+      if (!p || strcmp(p, "dsa") == 0) {
+       dh_pub dp;
+       kp = key_fetchinit(dh_pubfetch, 0, &dp);
+       if (key_fetch(kp, k)) goto fail_1;
+       if ((g.g = group_prime(&dp.dp)) == 0) goto fail_1;
+       g.p = G_CREATE(g.g);
+       if (G_FROMINT(g.g, g.p, dp.y)) goto fail_2;
+      } else if (strcmp(p, "ecdsa") == 0) {
+       ec_pub ep;
+       ec_info ei;
+       kp = key_fetchinit(ec_pubfetch, 0, &ep);
+       if (key_fetch(kp, k)) goto fail_1;
+       if (ec_getinfo(&ei, ep.cstr)) goto fail_1;
+       g.g = group_ec(&ei);
+       g.p = G_CREATE(g.g);
+       if (G_FROMEC(g.g, g.p, &ep.p)) goto fail_2;
+      } else
+       goto fail_0;
+
+      /* --- Unpack the response --- */
+
+      h = GH_INIT(g.h);
+      GH_HASH(h, buff, rqlen);
+      buf_init(&b, rbuff, sz);
+      if (buf_ensure(&b, g.h->hashsz)) goto fail_3;
+      if (memcmp(BCUR(&b), GH_DONE(h, 0), g.h->hashsz) != 0) goto fail_3;
+      BSTEP(&b, g.h->hashsz);
+      if ((ans = buf_getbyte(&b)) < 0) goto fail_3;
+      GH_DESTROY(h);
+
+      /* --- Verify the signature --- */
+
+      h = gdsa_beginhash(&g);
+      GH_HASH(h, BBASE(&b), BLEN(&b));
+      gdsa_endhash(&g, h);
+      s.r = s.s = 0;
+      if ((s.r = buf_getmp(&b)) == 0 ||
+         (s.s = buf_getmp(&b)) == 0)
+       goto fail_4;
+      if (gdsa_verify(&g, &s, GH_DONE(h, 0)))
+       goto fail_4;
+
+      mp_drop(s.r); mp_drop(s.s);
+      GH_DESTROY(h);
+      G_DESTROY(g.g, g.p);
+      G_DESTROYGROUP(g.g);
+      key_fetchdone(kp);
+      key_close(&f);
+      return (ans);
+
+      /* --- Tidy up and try again --- */
+
+    fail_4:
+      mp_drop(s.r); mp_drop(s.s);
+    fail_3:
+      GH_DESTROY(h);
+      G_DESTROY(g.g, g.p);
+    fail_2:
+      G_DESTROYGROUP(g.g);
+    fail_1:
+      key_fetchdone(kp);
+    fail_0:
+      continue;
+    }
+    
+    T( trace(TRACE_CLIENT,
+            "client: invalid or corrupt reply packet"); )
   }
-  burn(k);
-
-  /* --- Now wait for a reply --- */
 
-  {
-    fd_set fds;
-    struct timeval when, now, tv;
+  die(1, "internal error: can't get here in check__ask");
+  return (0);
+}
 
-    gettimeofday(&when, 0);
-    when.tv_sec += 10;
+/* --- @check__client@ --- *
+ *
+ * Arguments:  @request *rq@ = pointer to a request block
+ *             @FILE *fp@ = file containing server configuration
+ *
+ * Returns:    Nonzero if OK, zero if forbidden
+ *
+ * Use:                Asks one or several servers whether a request is acceptable.
+ */
 
-    for (;;) {
-      int i;
+int check__client(request *rq, FILE *fp)
+{
+  /* --- Format of the file --- *
+   *
+   * The `servers' file contains entries of the form
+   *
+   *    %%\syntax{<host> [`:' <port>]}%%
+   *
+   * separates by whitespace.  I build them all into an array of socket
+   * addresses and pass the whole lot to another function.
+   */
 
-      /* --- Sort out when to return --- */
+  struct sockaddr_in *serv;            /* Array of servers */
+  size_t n_serv, max_serv;             /* Number and maximum number */
 
-      gettimeofday(&now, 0);
-      if (now.tv_usec > when.tv_usec) {
-       now.tv_usec -= 1000000;
-       now.tv_sec += 1;
-      }
-      tv.tv_sec = when.tv_sec - now.tv_sec;
-      tv.tv_usec = when.tv_usec - now.tv_usec;
+  /* --- Initialise the server array --- */
 
-      /* --- Sort out file descriptors to watch --- */
+  T( trace(TRACE_CLIENT, "client: reading server definitions"); )
+  n_serv = 0; max_serv = 4;            /* Four seems reasonable */
+  serv = xmalloc(sizeof(*serv) * max_serv);
 
-      FD_ZERO(&fds);
-      FD_SET(fd, &fds);
+  /* --- Start reading the file --- */
 
-      /* --- Wait for them --- */
+  {
+    char buff[256], *p, *l;            /* A buffer and pointers for it */
+    int port;                          /* Default port for servers */
+    int state;                         /* Current parser state */
+    struct in_addr t_host;             /* Temp place for an address*/
+    int t_port;                                /* Temp place for a port */
+    int ch;                            /* The current character */
+
+    /* --- Parser states --- */
+
+    enum {
+      st_start,                                /* Waiting to begin */
+      st_host,                         /* Reading a new hostname */
+      st_colon,                                /* Expecting a colon, maybe */
+      st_preport,                      /* Waiting before reading port */
+      st_port,                         /* Reading a port number */
+      st_commit,                       /* Commit a newly read server */
+      st_done                          /* Finished reading the file */
+    };
+
+    /* --- Find a default port --- */
+
+    {
+      struct servent *s = getservbyname(quis(), "udp");
+      port = (s ? s->s_port : htons(SERVER_PORT));
+    }
 
-      i = select(FD_SETSIZE, &fds, 0, 0, &tv);
-      if (i == 0)
-       die("no answer from server");
-      if (i < 0)
-       die("error waiting for reply: %s", strerror(errno));
+    /* --- Initialise for scanning the file --- */
 
-      /* --- A reply should be waiting now --- */
+    state = st_host;
+    p = buff;
+    l = buff + sizeof(buff);
+    t_port = port;
+    ch = getc(fp);
 
-      {
-       struct sockaddr_in sin;
-       int slen = sizeof(sin);
-       unsigned char buff[256];
-       int answer;
+    /* --- Go for it --- */
+
+    while (state != st_done) {
+      switch (state) {
+
+       /* --- Initial whitespace before hostname --- */
+
+       case st_start:
+         if (ch == EOF)
+           state = st_done;
+         else if (isspace((unsigned char)ch))
+           ch = getc(fp);
+         else
+           state = st_host;
+         break;
+
+       /* --- Read a host name --- */
+
+       case st_host:
+         if (p == l)
+           die(1, "string too long in `" file_SERVER "'");
+         if (ch != EOF && !isspace((unsigned char)ch) && ch != ':') {
+           *p++ = ch;
+           ch = getc(fp);
+         } else {
+           struct hostent *h;
+
+           *p++ = 0;
+           if ((h = gethostbyname(buff)) == 0)
+             die(1, "unknown host `%s' in `" file_SERVER "'", buff);
+           memcpy(&t_host, h->h_addr, sizeof(t_host));
+           state = st_colon;
+         }
+         break;
+
+       /* --- Waiting for a colon coming up --- */
+
+       case st_colon:
+         if (ch == EOF)
+           state = st_commit;
+         else if (isspace((unsigned char)ch))
+           ch = getc(fp);
+         else if (ch == ':') {
+           state = st_preport;
+           ch = getc(fp);
+         }
+         else
+           state = st_commit;
+         break;
+
+       /* --- Clearing whitespace before a port number --- */
+
+       case st_preport:
+         if (ch == EOF)
+           state = st_commit;
+         else if (isspace((unsigned char)ch))
+           ch = getc(fp);
+         else {
+           state = st_port;
+           p = buff;
+         }
+         break;
+
+       /* --- Read a port number --- */
+
+       case st_port:
+         if (p == l)
+           die(1, "string too long in `" file_SERVER "'");
+         if (ch != EOF && !isspace((unsigned char)ch) && ch != ':') {
+           *p++ = ch;
+           ch = getc(fp);
+         } else {
+           struct servent *s;
+
+           *p++ = 0;
+           s = getservbyname(buff, "udp");
+           if (!s && isdigit((unsigned char)buff[0]))
+             t_port = htons(atoi(buff));
+           else if (!s)
+             die(1, "unknown service `%s' in `" file_SERVER "'", buff);
+           else
+             t_port = s->s_port;
+           state = st_commit;
+         }
+         break;
+
+       /* --- A server has been read successfully --- */
+
+       case st_commit:
+         if (n_serv == max_serv) {
+           max_serv *= 2;
+           serv = xrealloc(serv, n_serv * sizeof(*serv),
+                           max_serv * sizeof(*serv));
+         }
+         serv[n_serv].sin_family = AF_INET;
+         serv[n_serv].sin_addr = t_host;
+         serv[n_serv].sin_port = t_port;
+         n_serv++;
+         state = st_start;
+         p = buff;
+         t_port = port;
+         break;
+
+       /* --- A safety net for a broken parser --- */
+
+       default:
+         die(1, "internal error: can't get here in check__client");
+         break;          
+      }
+    }
+  }
 
-       /* --- Read the reply data --- */
+  fclose(fp);
 
-       if (recvfrom(fd, (char *)buff, sizeof(buff), 0,
-                    (struct sockaddr *)&sin, &slen) < 0)
-         die("error reading server's reply: %s", strerror(errno));
+  /* --- Now start sending requests --- */
 
-       /* --- Verify the sender --- */
+  if (!n_serv)
+    die(1, "no servers specified in `" file_SERVER "'");
 
-       if (sin.sin_addr.s_addr != ssin.sin_addr.s_addr ||
-           sin.sin_port != ssin.sin_port)
-         continue;
-    
-       /* --- Unpack and verify the response --- */
+  IF_TRACING(TRACE_CLIENT, {
+    size_t i;
 
-       answer = crypt_unpackReply(buff, sk, t, pid);
-       if (answer < 0)
-         continue;
-       return (answer);
-      }
+    for (i = 0; i < n_serv; i++) {
+      trace(TRACE_CLIENT, "client: server %s port %i",
+           inet_ntoa(serv[i].sin_addr), ntohs(serv[i].sin_port));
     }
-  }
-
-  die("internal error: can't get here in check_client");
-  return (0);
+  })
+  return (check__ask(rq, serv, n_serv));
 }
 
+#endif
+
+/*----- Main checking function --------------------------------------------*/
+
 /* --- @check@ --- *
  *
  * Arguments:  @request *rq@ = pointer to request buffer
@@ -240,45 +585,27 @@ int check(request *rq)
 
   /* --- Check if we need to talk to a server --- */
 
-  if ((fp = fopen(file_SERVER, "r")) != 0) {
-    char buff[64];
-    int port;
-    int ch;
-
-    if (fscanf(fp, " %63[^: \t] ", buff) < 1)
-      die("error in `%s'", file_SERVER);
-    ch = getc(fp);
-    if (ch == ':') {
-      char b[64];
-      struct servent *se;
-
-      fscanf(fp, "%s", b);
-      if ((se = getservbyname(b, 0)) != 0)
-       port = ntohs(se->s_port);
-      else if ((port = atoi(b)) == 0)
-       die("error in `%s'", file_SERVER);
-    } else {
-      struct servent *se;
-
-      if ((se = getservbyname(quis(), "udp")) == 0)
-       die("no idea which port to use");
-      port = ntohs(se->s_port);
-    }
-    fclose(fp);
-    return (check__client(rq, buff, port));
-  }
+#ifndef NONETWORK
+  if ((fp = fopen(file_SERVER, "r")) != 0)
+    return (check__client(rq, fp));
+#endif
 
-  /* --- Read the configuration in and go --- */
+  /* --- Otherwise do this all the old-fashioned way --- */
 
   if ((fp = fopen(file_RULES, "r")) == 0) {
-    die("couldn't read configuration file `%s': %s",
+    die(1, "couldn't read configuration file `%s': %s",
        file_RULES, strerror(errno));
   }
 
+  userdb_init();
+  userdb_local();
+  userdb_yp();
+  netg_init();
   name_init();
   rule_init();
   lexer_scan(fp);
-  yyparse();
+  parse();
+  fclose(fp);
 
   return (rule_check(rq));
 }