#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:
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;
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 */
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;
}
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,
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 */
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) {
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->scratch,st->log,M_ERR,
+ st->defhash);
if (fixed_pubkey) {
fixed_pubkey->dispose(fixed_pubkey->st);
}