chiark / gitweb /
pubkey handling: Call sethash when needed
[secnet.git] / site.c
diff --git a/site.c b/site.c
index 58db825992249ee07d03dff9ea008430e37d0545..e8b507ff0540fc2302420f0eccbb959897e22072 100644 (file)
--- a/site.c
+++ b/site.c
@@ -43,6 +43,7 @@
 #include "util.h"
 #include "unaligned.h"
 #include "magic.h"
+#include "pubkeys.h"
 
 #define SETUP_BUFFER_LEN 2048
 
@@ -60,6 +61,8 @@
 
 #define DEFAULT_MOBILE_PEER_EXPIRY            (2*60)      /* [s] */
 
+#define PEERKEYS_SUFFIX_MAXLEN (sizeof("~incoming")-1)
+
 /* Each site can be in one of several possible states. */
 
 /* States:
@@ -315,10 +318,10 @@ struct site {
     int ncomms;
     struct resolver_if *resolver;
     struct log_if *log;
+    struct hash_if *defhash;
     struct random_if *random;
     struct privcache_if *privkeys;
     struct sigprivkey_if *privkey_fixed;
-    struct sigpubkey_if *pubkey;
     struct transform_if **transforms;
     int ntransforms;
     struct dh_if *dh;
@@ -347,6 +350,9 @@ struct site {
     int resolving_n_results_all;
     int resolving_n_results_stored;
     struct comm_addr resolving_results[MAX_PEER_ADDRS];
+    const char *peerkeys_path;
+    struct pathprefix_template peerkeys_tmpl;
+    struct peer_keyset *peerkeys_current, *peerkeys_kex;
 
     /* The currently established session */
     struct data_key current;
@@ -559,6 +565,7 @@ struct msg {
     struct alg_msg_data sig;
     int n_pubkeys_accepted_nom; /* may be > MAX_SIG_KEYS ! */
     const struct sigkeyid *pubkeys_accepted[MAX_SIG_KEYS];
+    int signing_key_index;
 };
 
 static const struct sigkeyid keyid_zero;
@@ -664,13 +671,11 @@ static bool_t generate_msg(struct site *st, uint32_t type, cstring_t what,
        buf_append_uint16(&st->buffer,st->mtu_target);
     }
     if (type_is_msg23(type)) {
-       /* The code to advertise a public key acceptance list will
-        * come in a moment.  But right now we must add the byte
-        * indicating which key advertised by our peer we used, which
-        * comes after the public key acceptance list.  So for now,
-        * explicitly advertise a list with just 0000000000. */
-       buf_append_uint8(&st->buffer,1);
-       BUF_ADD_OBJ(append,&st->buffer,keyid_zero);
+       buf_append_uint8(&st->buffer,st->peerkeys_kex->nkeys);
+       for (ki=0; ki<st->peerkeys_kex->nkeys; ki++) {
+           struct peer_pubkey *pk = &st->peerkeys_kex->keys[ki];
+           BUF_ADD_OBJ(append,&st->buffer,pk->id);
+       }
     }
     struct sigprivkey_if *privkey=0;
     if (type_is_msg34(type)) {
@@ -763,6 +768,7 @@ static bool_t unpick_msg(struct site *st, uint32_t type,
 
     m->n_pubkeys_accepted_nom=-1;
     m->capab_transformnum=-1;
+    m->signing_key_index=-1;
     m->hashstart=msg->start;
     CHECK_AVAIL(msg,4);
     m->dest=buf_unprepend_uint32(msg);
@@ -792,6 +798,11 @@ static bool_t unpick_msg(struct site *st, uint32_t type,
        m->n_pubkeys_accepted_nom = 1;
        m->pubkeys_accepted[0] = &keyid_zero;
     }
+    if (type_is_msg34(type) && m->remote.extrainfo.size) {
+       m->signing_key_index=buf_unprepend_uint8(&m->remote.extrainfo);
+    } else {
+       m->signing_key_index=0;
+    }
     if (!unpick_name(msg,&m->local)) return False;
     if (type==LABEL_PROD) {
        CHECK_EMPTY(msg);
@@ -827,8 +838,13 @@ static bool_t unpick_msg(struct site *st, uint32_t type,
     CHECK_AVAIL(msg,m->pklen);
     m->pk=buf_unprepend(msg,m->pklen);
     m->hashlen=msg->start-m->hashstart;
-    struct sigpubkey_if *pubkey=st->pubkey;
 
+    if (m->signing_key_index < 0 ||
+       m->signing_key_index >= st->peerkeys_kex->nkeys) {
+       return False;
+    }
+    struct sigpubkey_if *pubkey=
+       st->peerkeys_kex->keys[m->signing_key_index].pubkey;
     if (!pubkey->unpick(pubkey->st,msg,&m->sig)) {
        return False;
     }
@@ -882,8 +898,194 @@ static bool_t check_msg(struct site *st, uint32_t type, struct msg *m,
     return False;
 }
 
+static void peerkeys_maybe_incorporate(struct site *st, const char *file,
+                                      const char *whatmore,
+                                      int logcl_enoent)
+{
+    struct peer_keyset *atsuffix=
+       keyset_load(file,&st->scratch,st->log,logcl_enoent,st->defhash);
+    if (!atsuffix) return;
+
+    if (st->peerkeys_current &&
+       serial_cmp(atsuffix->serial,st->peerkeys_current->serial) <= 0) {
+       slog(st,LOG_SIGKEYS,"keys from %s%s are older, discarding",
+            file,whatmore);
+       keyset_dispose(&atsuffix);
+       int r=unlink(file);
+       if (r) slog(st,LOG_ERROR,"failed to remove old key update %s: %s\n",
+                   st->peerkeys_tmpl.buffer,strerror(errno));
+       return;
+    } else {
+       slog(st,LOG_SIGKEYS,"keys from %s%s are newer, installing",
+            file,whatmore);
+       keyset_dispose(&st->peerkeys_current);
+       st->peerkeys_current=atsuffix;
+       int r=rename(file,st->peerkeys_path);
+       if (r) slog(st,LOG_ERROR,"failed to install key update %s as %s: %s\n",
+                   st->peerkeys_tmpl.buffer,st->peerkeys_path,
+                   strerror(errno));
+    }
+}
+
+static void peerkeys_check_for_update(struct site *st)
+{
+    /* peerkeys files
+     *
+     *  <F>            live file, loaded on startup, updated by secnet
+     *                  (only).  * in-memory peerkeys_current is kept
+     *                  synced with this file
+     *
+     *  <F>~update     update file from config manager, checked before
+     *                  every key exchange.  config manager must rename
+     *                  this file into place; it will be renamed and
+     *                  then removed by secnet.
+     *
+     *  <F>~proc       update file being processed by secnet.
+     *                  only secnet may write or remove.
+     *
+     *  <F>~incoming   update file from peer, being received by secnet
+     *                  may be incomplete, unverified, or even malicious
+     *                  only secnet may write or remove.
+     *
+     *  <F>~tmp        update file from config manager, only mss may
+     *                  write or rename
+     *
+     * secnet discards updates that are not more recent than (by
+     * serial) the live file.  But it may not process updates
+     * immediately.
+     *
+     * The implied keyset to be used is MAX(live, proc, update).
+     * 
+     * secnet does:
+     *  check live vs proc, either mv proc live or rm proc
+     *  if proc doesn't exist, mv update proc
+     *
+     * make-secnet-sites does:
+     *  write: rename something onto update
+     *  read: read update,proc,live in that order and take max
+     *
+     * We support only one concurrent secnet, one concurrent
+     * writing make-secnet-sites, and any number of readers.
+     * We want to maintain a live file at all times as that
+     * is what secnet actually reads at startup and uses.
+     *
+     * Proof that this is sound:
+     *   Let us regard update,proc,live as i=0,1,2
+     *   Files contain public key sets and are manipulated as
+     *    a whole, and we may regard key sets with the same
+     *    serial as equivalent.
+     *   We talk below about reading as if it were atomic.
+     *    Actually the atomic operation is open(2); the
+     *    reading gets whatever that name refers to.  So
+     *    we can model this as an atomic read.
+     *   secnet eventually moves all data into the live file
+     *    or deletes it, so there should be no indefinitely
+     *    stale data; informally this means we can disregard
+     *    the possibility of very old serials and regard
+     *    serials as fully ordered.  (We don't bother with
+     *    a formal proof of this property.)
+     *   Consequently we will only think about the serial
+     *    and not the contents.  We treat absent files as
+     *    minimal (we will write -1 for convenience although
+     *    we don't mean a numerical value).  We write S(i).
+     *
+     * Invariant 1 for secnet's transformations is as follows:
+     *   Each file S(i) is only reduced (to S'(i)) if for some j S'(j)
+     *   >= S(i), with S'(j) either being >= S(i) beforehand, or
+     *   updated atomically together with S(i).
+     *
+     * Proof of invariant 1 for the secnet operations:
+     *   (a) check live vs proc, proc>live, mv:
+     *      j=2, i=1; S'(i)=-1, so S(i) is being reduced.  S'(j) is
+     *      equal to S(i), and the rename is atomic [1], so S'(j) and
+     *      S'(i) are updated simultaneously.  S(j) is being
+     *      increased.  (There are no hazards from concurrent writers;
+     *      only we ourselves (secnet) write to live or proc.)
+     *   (b) check live vs proc, proc<=live, rm:
+     *      j=2, i=1; S'(i)=-1, so S(i) is being reduced.  But
+     *      S(j) is >= $(i) throughout.  (Again, no concurrent
+     *      writer hazards.)
+     *   (c) mv update proc (when proc does not exist):
+     *      j=1, i=0; S(i) is being reduced to -1.  But simultaneously
+     *      S(j) is being increased to the old S(i).  Our precondition
+     *      (proc not existing) is not subject to a concurrent writer
+     *      hazards because only we write to proc; our action is
+     *      atomic and takes whatever update is available (if any).
+     *
+     * Proof of soundness for the mss reading operation:
+     *   Let M be MAX(\forall S) at the point where mss reads update.
+     *   Invariant 2: when mss reads S(k), MAX(K, S(k)..S(2)) >= M,
+     *   where K is the max S it has seen so far.  Clearly this is
+     *   true for k=0 (with K==-1).  secnet's operations never break
+     *   this invariant because if any S() is reduced, another one
+     *   counted must be increased.  mss's step operation
+     *   updates K with S(k), so MAX(K', S(k+1)..)=MAX(K, S(k)..),
+     *   and updates k to k+1, preserving the invariant.
+     *   At the end we have k=3 and K=>M.  Since secnet never
+     *   invents serials, K=M in the absence of an mss update
+     *   with a bigger S.
+     *
+     * Consideration of the mss update operation:
+     *   Successive serials from sites file updates etc. are supposed
+     *   to be increasing.  When this is true, M is increased.  A
+     *   concurrent reading mss which makes its first read after the
+     *   update will get the new data (by the proofs above).  This
+     *   seems to be the required property.
+     *
+     * QED.
+     *
+     * [1] From "Base Specifications issue 7",
+     *  2.9.7 Thread Interactions with Regular File Operations
+     *  All of the following functions shall be atomic with respect to
+     *  each other in the effects specified in POSIX.1-2017 when they
+     *  operate on regular files or symbolic links:
+     *   ... rename ... open ...
+     */
+    if (!st->peerkeys_path) return;
+
+    pathprefix_template_setsuffix(&st->peerkeys_tmpl,"~proc");
+    peerkeys_maybe_incorporate(st,st->peerkeys_tmpl.buffer,
+                              " (found old update)",
+                              M_DEBUG);
+
+    pathprefix_template_setsuffix(&st->peerkeys_tmpl,"~update");
+    const char *inputp=st->peerkeys_tmpl.buffer;
+    if (access(inputp,R_OK)) {
+       if (errno!=ENOENT)
+           slog(st,LOG_ERROR,"cannot access peer key update file %s\n",
+                inputp);
+       return;
+    }
+
+    buffer_init(&st->scratch,0);
+    BUF_ADD_BYTES(append,&st->scratch,
+                 st->peerkeys_tmpl.buffer,
+                 strlen(st->peerkeys_tmpl.buffer)+1);
+    inputp=st->scratch.start;
+
+    pathprefix_template_setsuffix(&st->peerkeys_tmpl,"~proc");
+    const char *oursp=st->peerkeys_tmpl.buffer;
+
+    int r=rename(inputp,oursp);
+    if (r) {
+       slog(st,LOG_ERROR,"failed to claim key update file %s as %s: %s",
+            inputp,oursp,strerror(errno));
+       return;
+    }
+
+    peerkeys_maybe_incorporate(st,oursp," (update)",M_ERR);
+}
+
+
 static bool_t kex_init(struct site *st)
 {
+    keyset_dispose(&st->peerkeys_kex);
+    peerkeys_check_for_update(st);
+    if (!st->peerkeys_current) {
+       slog(st,LOG_SETUP_INIT,"no peer public keys, abandoning key setup");
+       return False;
+    }
+    st->peerkeys_kex = keyset_dup(st->peerkeys_current);
     st->random->generate(st->random->st,NONCELEN,st->localN);
     return True;
 }
@@ -976,9 +1178,25 @@ static bool_t generate_msg3(struct site *st, const struct msg *prompt)
 
 static bool_t process_msg3_msg4(struct site *st, struct msg *m)
 {
-    struct sigpubkey_if *pubkey=st->pubkey;
-
     /* Check signature and store g^x mod m */
+    int ki;
+
+    if (m->signing_key_index >= 0) {
+       if (m->signing_key_index >= st->peerkeys_kex->nkeys)
+           return False;
+       ki=m->signing_key_index;
+    } else {
+       for (ki=0; ki<st->peerkeys_kex->nkeys; ki++)
+           if (sigkeyid_equal(&keyid_zero,&st->peerkeys_kex->keys[ki].id))
+               goto found;
+       /* not found */
+       slog(st,LOG_ERROR,
+            "peer signed with keyid zero, which we do not accept");
+       return False;
+    found:;
+    }
+    struct sigpubkey_if *pubkey=st->peerkeys_kex->keys[ki].pubkey;
+
     if (!pubkey->check(pubkey->st,
                       m->hashstart,m->hashlen,
                       &m->sig)) {
@@ -1609,6 +1827,7 @@ static void enter_state_run(struct site *st)
 
     st->setup_session_id=0;
     transport_peers_clear(st,&st->setup_peers);
+    keyset_dispose(&st->peerkeys_kex);
     FILLZERO(st->localN);
     FILLZERO(st->remoteN);
     dispose_transform(&st->new_transform);
@@ -2205,14 +2424,15 @@ static void site_childpersist_clearkeys(void *sst, uint32_t newphase)
 }
 
 static void setup_sethash(struct site *st, dict_t *dict,
-                         struct hash_if **hash, struct cloc loc,
+                         struct cloc loc,
                          sig_sethash_fn *sethash, void *sigkey_st) {
-    if (!*hash) *hash=find_cl_if(dict,"hash",CL_HASH,True,"site",loc);
-    sethash(sigkey_st,*hash);
+    if (!st->defhash)
+       cfgfatal(loc,"site","other settings imply `hash' key is needed");
+    sethash(sigkey_st,st->defhash);
 }
 #define SETUP_SETHASH(k) do{                                           \
     if ((k)->sethash)                                                  \
-        setup_sethash(st,dict, &hash,loc, (k)->sethash,(k)->st);       \
+        setup_sethash(st,dict,loc, (k)->sethash,(k)->st);      \
 }while(0)
 
 static list_t *site_apply(closure_t *self, struct cloc loc, dict_t *context,
@@ -2233,6 +2453,9 @@ static list_t *site_apply(closure_t *self, struct cloc loc, dict_t *context,
     st->ops.st=st;
     st->ops.control=site_control;
     st->ops.status=site_status;
+    st->peerkeys_path=0;
+    st->peerkeys_tmpl.buffer=0;
+    st->peerkeys_current=st->peerkeys_kex=0;
 
     /* First parameter must be a dict */
     item=list_elem(args,0);
@@ -2307,7 +2530,7 @@ static list_t *site_apply(closure_t *self, struct cloc loc, dict_t *context,
     st->log=find_cl_if(dict,"log",CL_LOG,True,"site",loc);
     st->random=find_cl_if(dict,"random",CL_RANDOMSRC,True,"site",loc);
 
-    struct hash_if *hash=0;
+    st->defhash=find_cl_if(dict,"hash",CL_HASH,True,"site",loc);
 
     st->privkeys=find_cl_if(dict,"key-cache",CL_PRIVCACHE,False,"site",loc);
     if (!st->privkeys) {
@@ -2316,18 +2539,40 @@ static list_t *site_apply(closure_t *self, struct cloc loc, dict_t *context,
        SETUP_SETHASH(st->privkey_fixed);
     }
 
+    struct sigpubkey_if *fixed_pubkey
+       =find_cl_if(dict,"key",CL_SIGPUBKEY,False,"site",loc);
+    st->peerkeys_path=dict_read_string(dict,"peer-keys",fixed_pubkey==0,
+                                      "site",loc);
+    if (st->peerkeys_path) {
+       pathprefix_template_init(&st->peerkeys_tmpl,st->peerkeys_path,
+                                PEERKEYS_SUFFIX_MAXLEN + 1 /* nul */);
+       st->peerkeys_current=keyset_load(st->peerkeys_path,
+                                        &st->scratch,st->log,M_ERR,
+                                        st->defhash);
+       if (fixed_pubkey) {
+           fixed_pubkey->dispose(fixed_pubkey->st);
+       }
+    } else {
+       assert(fixed_pubkey);
+       SETUP_SETHASH(fixed_pubkey);
+       NEW(st->peerkeys_current);
+       st->peerkeys_current->refcount=1;
+       st->peerkeys_current->nkeys=1;
+       st->peerkeys_current->keys[0].id=keyid_zero;
+       st->peerkeys_current->keys[0].pubkey=fixed_pubkey;
+       slog(st,LOG_SIGKEYS,
+            "using old-style fixed peer public key (no `peer-keys')");
+    }
+
     st->addresses=dict_read_string_array(dict,"address",False,"site",loc,0);
     if (st->addresses)
        st->remoteport=dict_read_number(dict,"port",True,"site",loc,0);
     else st->remoteport=0;
-    st->pubkey=find_cl_if(dict,"key",CL_SIGPUBKEY,True,"site",loc);
 
     GET_CLOSURE_LIST("transform",transforms,ntransforms,CL_TRANSFORM);
 
     st->dh=find_cl_if(dict,"dh",CL_DH,True,"site",loc);
 
-    SETUP_SETHASH(st->pubkey);
-
 #define DEFAULT(D) (st->peer_mobile || st->local_mobile        \
                     ? DEFAULT_MOBILE_##D : DEFAULT_##D)
 #define CFG_NUMBER(k,D) dict_read_number(dict,(k),False,"site",loc,DEFAULT(D));