/*
* APOP, CRAM - MD5 challenge/response authentication
*
* The client does not authenticate the server, hence no CAI.
*
* Protocol:
*
* S -> C: random@domain
* C -> S: user hex-response
* S -> C: ok
*
* Note that this is the protocol between factotum and the local
* program, not between the two factotums. The information
* exchanged here is wrapped in the APOP protocol by the local
* programs.
*
* If S sends "bad [msg]" instead of "ok", that is a hint that the key is bad.
* The protocol goes back to "C -> S: user hex-response".
*/
#include "std.h"
#include "dat.h"
extern Proto apop, cram;
static int
apopcheck(Key *k)
{
if(!strfindattr(k->attr, "user") || !strfindattr(k->privattr, "!password")){
werrstr("need user and !password attributes");
return -1;
}
return 0;
}
static int
apopclient(Conv *c)
{
char *chal, *pw, *res;
int astype, nchal, npw, ntry, ret;
uchar resp[MD5dlen];
Attr *attr;
DigestState *ds;
Key *k;
chal = nil;
k = nil;
res = nil;
ret = -1;
attr = c->attr;
if(c->proto == &apop)
astype = AuthApop;
else if(c->proto == &cram)
astype = AuthCram;
else{
werrstr("bad proto");
goto out;
}
c->state = "find key";
k = keyfetch(c, "%A %s", attr, c->proto->keyprompt);
if(k == nil)
goto out;
c->state = "read challenge";
if((nchal = convreadm(c, &chal)) < 0)
goto out;
for(ntry=1;; ntry++){
if(c->attr != attr)
freeattr(c->attr);
c->attr = addattrs(copyattr(attr), k->attr);
if((pw = strfindattr(k->privattr, "!password")) == nil){
werrstr("key has no password (cannot happen?)");
goto out;
}
npw = strlen(pw);
switch(astype){
case AuthApop:
ds = md5((uchar*)chal, nchal, nil, nil);
md5((uchar*)pw, npw, resp, ds);
break;
case AuthCram:
hmac_md5((uchar*)chal, nchal, (uchar*)pw, npw, resp, nil);
break;
}
/* C->S: APOP user hex-response\n */
/*
if(ntry == 1)
c->state = "write user";
else{
sprint(c->statebuf, "write user (auth attempt #%d)", ntry);
c->state = c->statebuf;
}
if(convprint(c, "%s", strfindattr(k->attr, "user")) < 0)
goto out;
*/
c->state = "write response";
if(convprint(c, "%.*H", sizeof resp, resp) < 0)
goto out;
c->state = "read result";
if(convreadm(c, &res) < 0)
goto out;
if(strcmp(res, "ok") == 0)
break;
if(strncmp(res, "bad ", 4) != 0){
werrstr("bad result: %s", res);
goto out;
}
c->state = "replace key";
if((k = keyreplace(c, k, "%s", res+4)) == nil){
c->state = "auth failed";
werrstr("%s", res+4);
goto out;
}
free(res);
res = nil;
}
werrstr("succeeded");
ret = 0;
out:
keyclose(k);
free(chal);
if(c->attr != attr)
freeattr(attr);
return ret;
}
/* shared with auth dialing routines */
typedef struct ServerState ServerState;
struct ServerState
{
int asfd;
Key *k;
Ticketreq tr;
Ticket t;
char *dom;
char *hostid;
};
enum
{
APOPCHALLEN = 128
};
static int apopchal(ServerState*, int, char[APOPCHALLEN]);
static int apopresp(ServerState*, char*, char*);
static int
apopserver(Conv *c)
{
char chal[APOPCHALLEN], *user, *resp;
ServerState s;
int astype, ret;
Attr *a;
ret = -1;
user = nil;
resp = nil;
memset(&s, 0, sizeof s);
s.asfd = -1;
if(c->proto == &apop)
astype = AuthApop;
else if(c->proto == &cram)
astype = AuthCram;
else{
werrstr("bad proto");
goto out;
}
c->state = "find key";
if((s.k = plan9authkey(c->attr)) == nil)
goto out;
a = copyattr(s.k->attr);
a = delattr(a, "proto");
c->attr = addattrs(c->attr, a);
freeattr(a);
c->state = "authdial";
s.hostid = strfindattr(s.k->attr, "user");
s.dom = strfindattr(s.k->attr, "dom");
if((s.asfd = xioauthdial(nil, s.dom)) < 0){
werrstr("authdial %s: %r", s.dom);
goto out;
}
c->state = "authchal";
if(apopchal(&s, astype, chal) < 0)
goto out;
c->state = "write challenge";
if(convprint(c, "%s", chal) < 0)
goto out;
for(;;){
c->state = "read user";
if(convreadm(c, &user) < 0)
goto out;
c->state = "read response";
if(convreadm(c, &resp) < 0)
goto out;
c->state = "authwrite";
switch(apopresp(&s, user, resp)){
default:
case -1:
goto out;
case 0:
c->state = "write status";
if(convprint(c, "bad authentication failed") < 0)
goto out;
break;
case 1:
c->done = 1;
c->active = 0;
c->state = "write status";
if(convprint(c, "ok") < 0)
goto out;
goto ok;
}
free(user);
free(resp);
user = nil;
resp = nil;
}
ok:
ret = 0;
c->attr = addcap(c->attr, c->sysuser, &s.t);
out:
keyclose(s.k);
free(user);
free(resp);
xioclose(s.asfd);
return ret;
}
static int
apopchal(ServerState *s, int astype, char chal[APOPCHALLEN])
{
char trbuf[TICKREQLEN];
Ticketreq tr;
memset(&tr, 0, sizeof tr);
tr.type = astype;
if(strlen(s->hostid) >= sizeof tr.hostid){
werrstr("hostid too long");
return -1;
}
strcpy(tr.hostid, s->hostid);
if(strlen(s->dom) >= sizeof tr.authdom){
werrstr("domain too long");
return -1;
}
strcpy(tr.authdom, s->dom);
convTR2M(&tr, trbuf);
if(xiowrite(s->asfd, trbuf, TICKREQLEN) != TICKREQLEN)
return -1;
if(xioasrdresp(s->asfd, chal, APOPCHALLEN) <= 5)
return -1;
s->tr = tr;
return 0;
}
static int
apopresp(ServerState *s, char *user, char *resp)
{
char tabuf[TICKETLEN+AUTHENTLEN];
char trbuf[TICKREQLEN];
int len;
Authenticator a;
Ticket t;
Ticketreq tr;
tr = s->tr;
if(memrandom(tr.chal, CHALLEN) < 0)
return -1;
if(strlen(user) >= sizeof tr.uid){
werrstr("uid too long");
return -1;
}
strcpy(tr.uid, user);
convTR2M(&tr, trbuf);
if(xiowrite(s->asfd, trbuf, TICKREQLEN) != TICKREQLEN)
return -1;
len = strlen(resp);
if(len != 2*MD5dlen){
werrstr("response not MD5 digest");
return -1;
}
if(xiowrite(s->asfd, resp, len) != len)
return -1;
if(xioasrdresp(s->asfd, tabuf, TICKETLEN+AUTHENTLEN) != TICKETLEN+AUTHENTLEN)
return 0;
convM2T(tabuf, &t, s->k->priv);
if(t.num != AuthTs
|| memcmp(t.chal, tr.chal, sizeof tr.chal) != 0){
werrstr("key mismatch with auth server");
return -1;
}
convM2A(tabuf+TICKETLEN, &a, t.key);
if(a.num != AuthAc
|| memcmp(a.chal, tr.chal, sizeof a.chal) != 0
|| a.id != 0){
werrstr("key2 mismatch with auth server");
return -1;
}
s->t = t;
return 1;
}
static Role
apoproles[] =
{
"client", apopclient,
"server", apopserver,
0
};
Proto apop = {
"apop",
apoproles,
"user? !password?",
apopcheck,
nil
};
Proto cram = {
"cram",
apoproles,
"user? !password?",
apopcheck,
nil
};
|