X-Git-Url: http://www.chiark.greenend.org.uk/ucgi/~ianmdlvl/git?a=blobdiff_plain;f=site.c;h=3ce60fe48a5d6d7b8b81c58f3956fdf6cf72efa6;hb=ffbf811dd1b9e7f390ac2fa497e15764a87694ff;hp=37968896ccb50f1d07370a7ff582d1697a158327;hpb=627bf9eb662972a9b2d923bfdeb6cc01ac249095;p=secnet.git diff --git a/site.c b/site.c index 3796889..3ce60fe 100644 --- 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: @@ -142,6 +145,7 @@ static cstring_t state_name(uint32_t state) #define LOG_DUMP 0x00000100 #define LOG_ERROR 0x00000400 #define LOG_PEER_ADDRS 0x00000800 +#define LOG_SIGKEYS 0x00001000 static struct flagstr log_event_table[]={ { "unexpected", LOG_UNEXPECTED }, @@ -155,8 +159,9 @@ static struct flagstr log_event_table[]={ { "dump-packets", LOG_DUMP }, { "errors", LOG_ERROR }, { "peer-addrs", LOG_PEER_ADDRS }, + { "sigkeys", LOG_SIGKEYS }, { "default", LOG_SETUP_INIT|LOG_SETUP_TIMEOUT| - LOG_ACTIVATE_KEY|LOG_TIMEOUT_KEY|LOG_SEC|LOG_ERROR }, + LOG_ACTIVATE_KEY|LOG_TIMEOUT_KEY|LOG_SEC|LOG_ERROR|LOG_SIGKEYS }, { "all", 0xffffffff }, { NULL, 0 } }; @@ -314,8 +319,8 @@ struct site { struct resolver_if *resolver; struct log_if *log; struct random_if *random; - struct sigprivkey_if *privkey; - struct sigpubkey_if *pubkey; + struct privcache_if *privkeys; + struct sigprivkey_if *privkey_fixed; struct transform_if **transforms; int ntransforms; struct dh_if *dh; @@ -344,6 +349,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; @@ -392,6 +400,7 @@ static uint32_t event_log_priority(struct site *st, uint32_t event) case LOG_DUMP: return M_DEBUG; case LOG_ERROR: return M_ERR; case LOG_PEER_ADDRS: return M_DEBUG; + case LOG_SIGKEYS: return M_INFO; default: return M_ERR; } } @@ -517,6 +526,13 @@ static void dispose_transform(struct transform_inst_if **transform_var) type=buf_unprepend_uint32((b)); \ if (type!=(t)) return False; } while(0) +static _Bool type_is_msg23(uint32_t type) +{ + switch (type) { + case LABEL_MSG2: case CASES_MSG3_KNOWN: return True; + default: return False; + } +} static _Bool type_is_msg34(uint32_t type) { switch (type) { @@ -546,8 +562,13 @@ struct msg { char *pk; int32_t hashlen; 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; + static int32_t wait_timeout(struct site *st) { int32_t t = st->wait_timeout_mean; int8_t factor; @@ -629,6 +650,7 @@ static bool_t generate_msg(struct site *st, uint32_t type, cstring_t what, { string_t dhpub; unsigned minor; + int ki; st->retries=st->setup_retries; BUF_ALLOC(&st->buffer,what); @@ -647,6 +669,50 @@ static bool_t generate_msg(struct site *st, uint32_t type, cstring_t what, if (type_is_msg34(type)) { buf_append_uint16(&st->buffer,st->mtu_target); } + if (type_is_msg23(type)) { + buf_append_uint8(&st->buffer,st->peerkeys_kex->nkeys); + for (ki=0; kipeerkeys_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)) { + assert(prompt->n_pubkeys_accepted_nom>0); + for (ki=0; + kin_pubkeys_accepted_nom && kipubkeys_accepted[ki]; + if (st->privkeys) { + privkey=st->privkeys->lookup(st->privkeys->st,kid,st->log); + if (privkey) goto privkey_found; + } else { + if (sigkeyid_equal(&keyid_zero,kid)) { + privkey=st->privkey_fixed; + goto privkey_found; + } + } + } + uint32_t class = slog_start(st,LOG_ERROR); + if (class) { + slilog_part(st->log,class,"no suitable private key, peer wanted"); + for (ki=0; + kin_pubkeys_accepted_nom && kilog,class, " " SIGKEYID_PR_FMT, + SIGKEYID_PR_VAL(prompt->pubkeys_accepted[ki])); + } + if (prompt->n_pubkeys_accepted_nom > MAX_SIG_KEYS) + slilog_part(st->log,class," +%d", + prompt->n_pubkeys_accepted_nom - MAX_SIG_KEYS); + slilog_part(st->log,class,"\n"); + } + return False; + + privkey_found: + buf_append_uint8(&st->buffer,ki); + } + append_string_xinfo_done(&st->buffer,&xia); buf_append_string(&st->buffer,st->remotename); @@ -667,10 +733,10 @@ static bool_t generate_msg(struct site *st, uint32_t type, cstring_t what, buf_append_string(&st->buffer,dhpub); free(dhpub); - bool_t ok=st->privkey->sign(st->privkey->st, - st->buffer.start, - st->buffer.size, - &st->buffer); + bool_t ok=privkey->sign(privkey->st, + st->buffer.start, + st->buffer.size, + &st->buffer); if (!ok) goto fail; return True; @@ -699,7 +765,9 @@ static bool_t unpick_msg(struct site *st, uint32_t type, { unsigned minor; + 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); @@ -717,6 +785,23 @@ static bool_t unpick_msg(struct site *st, uint32_t type, CHECK_AVAIL(&m->remote.extrainfo,2); m->remote_mtu=buf_unprepend_uint16(&m->remote.extrainfo); } + if (type_is_msg23(type) && m->remote.extrainfo.size) { + m->n_pubkeys_accepted_nom = buf_unprepend_uint8(&m->remote.extrainfo); + if (!m->n_pubkeys_accepted_nom) return False; + for (int ki_nom=0; ki_nomn_pubkeys_accepted_nom; ki_nom++) { + CHECK_AVAIL(&m->remote.extrainfo,KEYIDSZ); + struct sigkeyid *kid = buf_unprepend(&m->remote.extrainfo,KEYIDSZ); + if (ki_nompubkeys_accepted[ki_nom] = kid; + } + } else { + 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); @@ -753,7 +838,13 @@ static bool_t unpick_msg(struct site *st, uint32_t type, m->pk=buf_unprepend(msg,m->pklen); m->hashlen=msg->start-m->hashstart; - if (!st->pubkey->unpick(st->pubkey->st,msg,&m->sig)) { + 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; } @@ -806,8 +897,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); + 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 + * + * live file, loaded on startup, updated by secnet + * (only). * in-memory peerkeys_current is kept + * synced with this file + * + * ~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. + * + * ~proc update file being processed by secnet. + * only secnet may write or remove. + * + * ~incoming update file from peer, being received by secnet + * may be incomplete, unverified, or even malicious + * only secnet may write or remove. + * + * ~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; } @@ -901,9 +1178,27 @@ static bool_t generate_msg3(struct site *st, const struct msg *prompt) static bool_t process_msg3_msg4(struct site *st, struct msg *m) { /* Check signature and store g^x mod m */ - if (!st->pubkey->check(st->pubkey->st, - m->hashstart,m->hashlen, - &m->sig)) { + 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; kipeerkeys_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)) { slog(st,LOG_SEC,"msg3/msg4 signature failed check!"); return False; } @@ -1531,6 +1826,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); @@ -2126,6 +2422,17 @@ static void site_childpersist_clearkeys(void *sst, uint32_t newphase) crypto operations, but that's a task for another day. */ } +static void setup_sethash(struct site *st, dict_t *dict, + struct hash_if **hash, 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); +} +#define SETUP_SETHASH(k) do{ \ + if ((k)->sethash) \ + setup_sethash(st,dict, &hash,loc, (k)->sethash,(k)->st); \ +}while(0) + static list_t *site_apply(closure_t *self, struct cloc loc, dict_t *context, list_t *args) { @@ -2144,6 +2451,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); @@ -2218,23 +2528,48 @@ 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); - st->privkey=find_cl_if(dict,"local-key",CL_SIGPRIVKEY,True,"site",loc); + struct hash_if *hash=0; + + st->privkeys=find_cl_if(dict,"key-cache",CL_PRIVCACHE,False,"site",loc); + if (!st->privkeys) { + st->privkey_fixed= + find_cl_if(dict,"local-key",CL_SIGPRIVKEY,True,"site",loc); + 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); + 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); - if (st->privkey->sethash || st->pubkey->sethash) { - struct hash_if *hash=find_cl_if(dict,"hash",CL_HASH,True,"site",loc); - if (st->privkey->sethash) st->privkey->sethash(st->privkey->st,hash); - if (st->pubkey->sethash) st->pubkey->sethash(st->pubkey->st,hash); - } - #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));