implement Sshserve; include "sys.m"; sys: Sys; include "draw.m"; include "ipints.m"; ipints: IPints; IPint: import ipints; include "crypt.m"; crypt: Crypt; PK, SK: import crypt; include "env.m"; env: Env; include "sh.m"; sh: Sh; include "wait.m"; wait: Wait; include "arg.m"; include "sshio.m"; sshio: Sshio; Conn, Msg: import sshio; recvmsg: import sshio; error, debug: import sshio; Sshserve: module { init: fn(nil: ref Draw->Context, argl: list of string); }; AuthRpc: adt {}; debuglevel := 0; cipherlist := "blowfish rc4 3des"; ciphers: list of Cipher; authlist := "rsa password tis"; authsrvs: list of Auth; maxmsg := 256*1024; serverpriv: ref SK.RSA; serverkey: ref PK.RSA; hostpriv: ref SK.RSA; init(nil: ref Draw->Context, args: list of string) { sys = load Sys Sys->PATH; ipints = load IPints IPints->PATH; crypt = load Crypt Crypt->PATH; env = load Env Env->PATH; sh = load Sh Sh->PATH; sshio = load Sshio Sshio->PATH; sshio->init(); wait = load Wait Wait->PATH; wait->init(); # fmtinstall('B', mpfmt); # fmtinstall('H', encodefmt); sys->pctl(Sys->NEWPGRP|Sys->FORKFD|Sys->FORKNS|Sys->FORKENV, nil); keyfile: string; arg := load Arg Arg->PATH; arg->setusage("sshserve [-A authlist] [-c cipherlist] [-k keyfile] client-ip-address"); arg->init(args); while((o := arg->opt()) != 0){ case o { 'D' => debuglevel = int arg->earg(); 'A' => authlist = arg->earg(); 'c' => cipherlist = arg->earg(); 'k' => keyfile = arg->earg(); * => arg->usage(); } } args = arg->argv(); if(len args != 1) arg->usage(); arg = nil; sys->dup(2, 1); # if(keyfile != nil) # ; # read hostpriv from file # sshlog("connect from %s", c.host); authsrvs = loadlist("auth", authlist, authload); ciphers = loadlist("cipher", cipherlist, cipherload); hostpriv = crypt->rsagen(1024, 6, 0); serverpriv = crypt->rsagen(768, 6, 0); serverkey = serverpriv.pk; { versioning(sys->fildes(0)); c := Conn.mk(hd args, sys->fildes(0)); c.setkey(hostpriv.pk); authenticate(c); comms(c); }exception e{ "fail:*" => raise e; "error*" => notegrp(sys->pctl(0, nil), "error"); raise "fail:"+e; } } authload(f: string): Auth { return load Auth f; } cipherload(f: string): Cipher { return load Cipher f; } loadlist[T](sort: string, set: string, loadf: ref fn(f: string): T): list of T { l: list of T; (nil, fld) := sys->tokenize(set, " \t,"); for(; fld != nil; fld = tl fld){ f := "/dis/ssh/"+sort+hd fld+".dis"; m := loadf(f); if(m == nil) error(sys->sprint("unknown %s scheme %s (%s)", sort, hd fld, f)); l = m :: l; } return l; } comms(c: ref Conn) { (kidpid, infd, waiting) := prelude(c); Work: for(;;)alt{ (m, nil) := <-c.in => if(m == nil){ notegrp(kidpid, "hungup"); exit; } case m.mtype { * => sshio->badmsg(m, 0, nil); SSH_MSG_DISCONNECT => notegrp(kidpid, "hungup"); sysfatal("client disconnected"); SSH_CMSG_STDIN_DATA => if(infd != nil){ n := m.get4(); sys->write(infd, m.getbytes(n), n); } SSH_CMSG_EOF => infd = nil; SSH_CMSG_EXIT_CONFIRMATION => # sent by some clients as dying breath notegrp(kidpid, "hungup"); break Work; SSH_CMSG_WINDOW_SIZE => ; # we don't care } (pid, nil, status) := <-waiting => if(pid == kidpid){ if(status != "" && status != "0"){ m := Msg.mk(SSH_MSG_DISCONNECT, 4+Sys->UTFmax*len status); m.putstring(status); sendmsg(c, m); }else{ m := Msg.mk(SSH_SMSG_EXITSTATUS, 4); m.put4(0); sendmsg(c, m); } sendmsg(c, nil); break Work; } } notegrp(sys->pctl(0, nil), "done"); } prelude(c: ref Conn): (int, ref Sys->FD, chan of (int, string, string)) { for(;;){ m := recvmsg(c, -1); if(m == nil) return (-1, nil, nil); case m.mtype { * => sendmsg(c, Msg.mk(SSH_SMSG_FAILURE, 0)); SSH_MSG_DISCONNECT => sysfatal("client disconnected"); SSH_CMSG_REQUEST_PTY => sendmsg(c, Msg.mk(SSH_SMSG_SUCCESS, 0)); SSH_CMSG_MAX_PACKET_SIZE => n := m.get4(); if(n >= 32 && n <= SSH_MAX_MSG){ maxmsg = n; sendmsg(c, Msg.mk(SSH_SMSG_SUCCESS, 0)); }else sendmsg(c, Msg.mk(SSH_SMSG_FAILURE, 0)); SSH_CMSG_EXEC_SHELL => return startcmd(c, nil); SSH_CMSG_EXEC_CMD => cmd := m.getstring(); return startcmd(c, cmd); } } } copyout(c: ref Conn, fd: ref Sys->FD, mtype: int) { buf := array[8192] of byte; max := len buf; if(max > maxmsg-32) # 32 is an overestimate of packet overhead max = maxmsg-32; if(max <= 0) sysfatal("maximum message size too small"); while((n := sys->read(fd, buf, max)) > 0){ m := Msg.mk(mtype, 4+n); m.put4(n); m.putbytes(buf, n); sendmsg(c, m); } } send_ssh_smsg_public_key(c: ref Conn, cookie: array of byte) { m := Msg.mk(SSH_SMSG_PUBLIC_KEY, 2048); m.putbytes(cookie, COOKIELEN); m.putpk(serverkey); m.putpk(c.hostkey); m.put4(c.flags); ciphermask := 0; for(l1 := ciphers; l1 != nil; l1 = tl l1) ciphermask |= 1<<(hd l1)->id(); m.put4(ciphermask); authmask := 0; for(l2 := authsrvs; l2 != nil; l2 = tl l2) authmask |= 1<<(hd l2)->id(); m.put4(authmask); sendmsg(c, m); } rpcdecrypt(rpc: ref AuthRpc, b: ref IPint): ref IPint { raise "rpcdecrypt"; # p := array of byte b.iptostr(16); # if(auth_rpc(rpc, "write", p, len p) != ARok) # sysfatal("factotum rsa write: %r"); # if(auth_rpc(rpc, "read", nil, 0) != ARok) # sysfatal("factotum rsa read: %r"); # return strtomp(rpc.arg, nil, 16, nil); } recv_ssh_cmsg_session_key(c: ref Conn, rpc: ref AuthRpc, cookie: array of byte) { m := recvmsg(c, SSH_CMSG_SESSION_KEY); id := m.get1(); c.cipher = nil; for(l := ciphers; l != nil; l = tl l) if((hd l)->id() == id){ c.cipher = hd l; break; } if(c.cipher == nil) sysfatal(sys->sprint("invalid cipher %d selected", id)); if(!sshio->eqbytes(m.getbytes(COOKIELEN), cookie, len cookie)) sysfatal("bad cookie"); serverkeylen := serverkey.n.bits(); hostkeylen := c.hostkey.n.bits(); ksmall, kbig: ref SK.RSA; if(serverkeylen+128 <= hostkeylen){ ksmall = serverpriv; kbig = nil; }else if(hostkeylen+128 <= serverkeylen){ ksmall = nil; kbig = serverpriv; }else sysfatal("server session and host keys do not differ by at least 128 bits"); b := m.getipint(); debug(DBG_CRYPTO, sys->sprint("encrypted with kbig is %s\n", b.iptostr(16))); if(kbig != nil) b = sshio->rsadecrypt(kbig, b); else # b = rpcdecrypt(rpc, b); b = sshio->rsadecrypt(hostpriv, b); b = sshio->rsaunpad(b); sshio->debug(DBG_CRYPTO, sys->sprint("encrypted with ksmall is %s\n", b.iptostr(16))); if(ksmall != nil) b = sshio->rsadecrypt(ksmall, b); else # b = rpcdecrypt(rpc, b); b = sshio->rsadecrypt(hostpriv, b); b = sshio->rsaunpad(b); debug(DBG_CRYPTO, sys->sprint("munged is %s\n", b.iptostr(16))); n := (b.bits()+7)/8; if(n < SESSKEYLEN) sysfatal("client sent short session key"); buf := array[SESSKEYLEN] of byte; sshio->iptorjustbe(b, buf, SESSKEYLEN); for(i := 0; i < SESSIDLEN; i++) buf[i] ^= c.sessid[i]; c.sesskey[0: ] = buf[0: SESSKEYLEN]; debug(DBG_CRYPTO, sys->sprint("unmunged is %.*s\n", SESSKEYLEN*2, sshio->hex(buf))); c.flags = m.get4(); } authsrvuser(c: ref Conn) { m := recvmsg(c, SSH_CMSG_USER); user := m.getstring(); c.user = user; inited := 0; ai: ref Auth->AuthInfo; while(authsrvs != nil && ai == nil){ # # # # * clumsy: if the client aborted the auth_tis early # # * we don't send a new failure. we check this by # # * looking at c->unget, which is only used in that # # * case. # # if(c.unget == nil) sendmsg(c, Msg.mk(SSH_SMSG_FAILURE, 0)); m = recvmsg(c, -1); for(l := authsrvs; l != nil; l = tl l) if((hd l)->firstmsg() == m.mtype){ bit := 1 << (hd l)->id(); if((inited & bit) == 0){ (hd l)->init(sshio); inited |= bit; } ai = (hd l)->authsrv(c, m); break; } if(l == nil) sshio->badmsg(m, 0, nil); } sendmsg(c, Msg.mk(SSH_SMSG_SUCCESS, 0)); # if(noworld(ai.cuid)) # ns := "/lib/namespace.noworld"; # else # ns = nil; # if(auth_chuid(ai, ns) < 0){ # sshlog("auth_chuid to %s: %r", ai.cuid); # sysfatal("auth_chuid: %r"); # } # sshlog("logged in as %q", ai.user); if(ai != nil) sys->print("logged in as %q\n", ai.user); } keyjunk() { p: array of byte; m: ref IPint; rpc: ref AuthRpc; key: ref PK.RSA; # # # # BUG: should use `attr' to get the key attributes # # after the read, but that's not implemented yet. # # # if((b = Bopen("/mnt/factotum/ctl", OREAD)) == nil) # sysfatal("open /mnt/factotum/ctl: %r"); # while((p = Brdline(b, '\n')) != nil){ # if(strstr(p, " proto=rsa ") != nil && strstr(p, " service=sshserve ") != nil) # break; # } # if(p == nil) # sysfatal("no sshserve keys found in /mnt/factotum/ctl"); # a = _parseattr(p); # Bterm(b); # key = rsaprivalloc(); # if((p = _strfindattr(a, "n")) == nil) # sysfatal("no n in sshserve key"); # if((key.n = IPint.strtoip(p, 16)) == nil) # sysfatal("bad n in sshserve key"); # if((p = _strfindattr(a, "ek")) == nil) # sysfatal("no ek in sshserve key"); # if((key.ek = IPint.strtoip(p, 16)) == nil) # sysfatal("bad ek in sshserve key"); # _freeattr(a); # if((afd = sys->open("/mnt/factotum/rpc", ORDWR)) == nil) # sysfatal("open /mnt/factotum/rpc: %r"); # if((rpc = auth_allocrpc(afd)) == nil) # sysfatal("auth_allocrpc: %r"); # p = "proto=rsa role=client service=sshserve"; # if(auth_rpc(rpc, "start", p, len p) != ARok) # sysfatal("auth_rpc start %s: %r", p); # if(auth_rpc(rpc, "read", nil, 0) != ARok) # sysfatal("auth_rpc read: %r"); # m = strtomp(rpc.arg, nil, 16, nil); # if(mpcmp(m, key.n) != 0) # sysfatal("key in /mnt/factotum/ctl does not match rpc key"); # mpfree(m); # c.hostkey = key; } versioning(fd: ref Sys->FD) { sys->fprint(fd, "SSH-1.5-Inferno\n"); (maj, min, err_or_id) := sshio->readversion(fd); if(maj < 0) sysfatal(err_or_id); if(maj != 1 || min < 5) sysfatal(sys->sprint("protocol mismatch; got %s, need SSH-1.x for x >= 5", err_or_id)); } authenticate(c: ref Conn) { rpc: ref AuthRpc; cookie := array[COOKIELEN] of {* => byte sshio->fastrand()}; c.sessid = sshio->calcsessid(c.hostkey.n, serverkey.n, cookie); send_ssh_smsg_public_key(c, cookie); recv_ssh_cmsg_session_key(c, rpc, cookie); # afd = nil; c.cipher->init(c.sesskey, 1); # turns on encryption sendmsg(c, Msg.mk(SSH_SMSG_SUCCESS, 0)); authsrvuser(c); } startcmd(c: ref Conn, cmd: string): (int, ref Sys->FD, chan of (int, string, string)) { pfd := array[3] of {* => array[2] of ref Sys->FD}; for(i := 0; i < 3; i++) if(sys->pipe(pfd[i]) < 0) sysfatal(sys->sprint("pipe: %r")); wfd := sys->open("#p/"+string sys->pctl(0, nil)+"/wait", Sys->OREAD); if(wfd == nil) sysfatal(sys->sprint("open wait: %r")); pidc := chan of int; spawn startcmd1(c, cmd, pfd, pidc); kidpid := <-pidc; (nil, waited) := wait->monitor(wfd); spawn copyout(c, pfd[1][0], SSH_SMSG_STDOUT_DATA); pfd[1][0] = nil; spawn copyout(c, pfd[2][0], SSH_SMSG_STDERR_DATA); pfd[2][0] = nil; return (kidpid, pfd[0][0], waited); } startcmd1(c: ref Conn, cmd: string, pfd: array of array of ref Sys->FD, pidc: chan of int) { sysname := env->getenv("sysname"); tz := env->getenv("timezone"); sys->pctl(Sys->FORKFD, nil); for(i := 0; i < len pfd; i++) if(sys->dup(pfd[i][1].fd, i) < 0) sysfatal(sys->sprint("dup: %r")); pfd = nil; sys->pctl(Sys->NEWPGRP|Sys->FORKNS|Sys->FORKENV|Sys->NEWFD, 0::1::2::nil); pidc <-= sys->pctl(0, nil); env->setenv("user", c.user); if(sysname != nil) env->setenv("sysname", sysname); if(tz != nil) env->setenv("tz", tz); if(sys->chdir("/usr/"+c.user) < 0) sys->chdir("/"); if(cmd != nil){ env->setenv("service", "rx"); status := sh->run(nil, list of {"/dis/sh.dis", "-lc", cmd}); if(status != nil) raise "fail:"+status; }else{ env->setenv("service", "con"); #execl("/bin/ip/telnetd", "telnetd", "-tn", nil); # TO DO: just for echo and line editing sys->fprint(sys->fildes(2), "sshserve: cannot run /dis/ip/telnetd: %r"); } } sysfatal(s: string) { sys->print("sysfatal: %s\n", s); notegrp(sys->pctl(0, nil), "zap"); exit; } notegrp(pid: int, nil: string) { fd := sys->open("#p/"+string pid+"/ctl", Sys->OWRITE); if(fd != nil) sys->fprint(fd, "killgrp"); } sendmsg(c: ref Conn, m: ref Msg) { c.out <-= m; }