Index: backend/pgsql.t ================================================================== --- backend/pgsql.t +++ backend/pgsql.t @@ -118,19 +118,31 @@ ]]; }; actor_auth_pw = { params = {pstring,rawstring,pstring,lib.store.inet}, sql = [[ - select a.aid, a.uid, a.name from parsav_auth as a + select a.aid, a.uid, a.name, a.blacklist from parsav_auth as a left join parsav_actors as u on u.id = a.uid where (a.uid is null or u.handle = $1::text or ( a.uid = 0 and a.name = $1::text )) and (a.kind = 'trust' or (a.kind = $2::text and a.cred = $3::bytea)) and (a.netmask is null or a.netmask >> $4::inet) order by blacklist desc limit 1 ]]; + }; + actor_auth_challenge = { + params = {pstring,pstring,lib.store.inet}, sql = [[ + select a.aid, a.uid, a.name, a.blacklist, a.cred + from parsav_auth as a + left join parsav_actors as u on u.id = a.uid + where a.kind = 'challenge-' || $1::text and + (a.netmask is null or a.netmask >> $3::inet) and + (a.uid is null or u.handle = $2::text or + (a.uid = 0 and a.name = $2::text)) + order by blacklist desc + ]]; }; actor_enum_local = { params = {}, sql = [[ select id, nym, handle, origin, bio, @@ -294,10 +306,21 @@ params = {uint64, binblob, int64, pstring}, sql = [[ insert into parsav_auth (uid, name, kind, cred, valperiod, comment) values ( $1::bigint, (select handle from parsav_actors where id = $1::bigint), 'pw-sha256', $2::bytea, + $3::bigint, $4::text + ) on conflict (name,kind,cred) do update set comment = $4::text returning aid + ]] + }; + + auth_create_rsa = { + params = {uint64, binblob, int64, pstring}, sql = [[ + insert into parsav_auth (uid, name, kind, cred, valperiod, comment) values ( + $1::bigint, + (select handle from parsav_actors where id = $1::bigint), + 'challenge-rsa', $2::bytea, $3::bigint, $4::text ) on conflict (name,kind,cred) do update set comment = $4::text returning aid ]] }; @@ -1294,12 +1317,12 @@ end]; actor_auth_pw = [terra( src: &lib.store.source, ip: lib.store.inet, - username: lib.mem.ptr(int8), - cred: lib.mem.ptr(int8) + username: pstring, + cred: pstring ): {uint64, uint64, pstring} [ checksha(`src, 256, ip, username, cred) ] -- most common [ checksha(`src, 512, ip, username, cred) ] -- most secure [ checksha(`src, 384, ip, username, cred) ] -- weird @@ -1307,10 +1330,53 @@ -- TODO: check pbkdf2-hmac -- TODO: check OTP return 0, 0, pstring.null() end]; + + actor_auth_challenge = [terra( + src: &lib.store.source, + ip: lib.store.inet, + username: pstring, + sig: binblob, + token: pstring + ): {uint64, uint64, pstring} + -- we need to iterate through all the challenge types. right now that's just RSA + lib.dbg('checking against token ', {token.ptr,token.ct}) + var rsakeys = queries.actor_auth_challenge.exec(src, 'rsa', username, ip) + var toprops = [terra(res: &pqr, i: intptr) + return { + aid = res:int(uint64, i, 0); + uid = res:int(uint64, i, 1); + name = res:_string(i, 2); + blacklist = res:bool(i, 3); + pubkey = res:bin(i, 4); + } + end] + if rsakeys.sz > 0 then defer rsakeys:free() + for i=0, rsakeys.sz do var props = toprops(&rsakeys, i) + lib.dbg('loading next RSA pubkey') + var pub = lib.crypt.loadpub(props.pubkey.ptr, props.pubkey.ct) + if pub.ok then defer pub.val:free() + lib.dbg('checking pubkey against response') + var vfy, secl = lib.crypt.verify(&pub.val, token.ptr, token.ct, sig.ptr, sig.ct) + if vfy then + lib.dbg('signature verified') + if props.blacklist then lib.dbg('key blacklisted!') goto fail end + var dupname = lib.str.dup(props.name.ptr) + return props.aid, props.uid, pstring {dupname, props.name.ct} + end + else lib.warn('invalid pubkey in authentication table for user ',{props.name.ptr, props.name.ct}) end + end + end + + -- and so on + + lib.dbg('no challenges were successful') + ::fail::return 0, 0, pstring.null() + end]; + actor_stats = [terra(src: &lib.store.source, uid: uint64) var r = queries.actor_stats.exec(src, uid) if r.sz == 0 then lib.bail('error fetching actor stats!') end var s: lib.store.actor_stats @@ -1617,10 +1683,25 @@ if r.sz == 0 then return 0 end var aid = r:int(uint64,0,0) r:free() return aid end]; + + auth_attach_rsa = [terra( + src: &lib.store.source, + uid: uint64, + reset: bool, + pub: binblob, + comment: pstring + ): uint64 + if reset then queries.auth_purge_type.exec(src, nil, uid, 'challenge-%') end + var r = queries.auth_create_rsa.exec(src, uid, pub, lib.osclock.time(nil), comment) + if r.sz == 0 then return 0 end + var aid = r:int(uint64,0,0) + r:free() + return aid + end]; auth_privs_set = [terra( src: &lib.store.source, aid: uint64, set: lib.store.privset Index: crypt.t ================================================================== --- crypt.t +++ crypt.t @@ -12,10 +12,11 @@ } const.maxpemsz = math.floor((const.keybits / 8)*6.4) + 128 -- idk why this formula works but it basically seems to const.maxdersz = const.maxpemsz -- FIXME this is a safe value but obvs not the correct one local ctx = lib.pk.mbedtls_pk_context +terra ctx:free() lib.pk.mbedtls_pk_free(self) end local struct hashalg { id: uint8 bytes: intptr } local m = { pemfile = uint8[const.maxpemsz]; const = const; @@ -80,16 +81,22 @@ else return lib.pk.mbedtls_pk_write_key_pem(key, buf, const.maxpemsz) == 0 end end -terra m.der(pub: bool, key: &ctx, buf: &uint8): intptr +local binblob = lib.mem.ptr(uint8) +terra m.der(pub: bool, key: &ctx, buf: &uint8): binblob + var ofs: intptr if pub then - return lib.pk.mbedtls_pk_write_pubkey_der(key, buf, const.maxdersz) + ofs = lib.pk.mbedtls_pk_write_pubkey_der(key, buf, const.maxdersz) else - return lib.pk.mbedtls_pk_write_key_der(key, buf, const.maxdersz) + ofs = lib.pk.mbedtls_pk_write_key_der(key, buf, const.maxdersz) end + return binblob { + ptr = buf + (const.maxdersz - ofs); + ct = ofs; + } end m.destroy = lib.dispatch { [ctx] = function(v) return `lib.pk.mbedtls_pk_free(&v) end; @@ -106,17 +113,36 @@ lib.rsa.mbedtls_rsa_gen_key(rsa, callbacks.randomize, nil, const.keybits, 65537) return pk end -terra m.loadpriv(buf: &uint8, len: intptr): ctx - lib.dbg('parsing saved keypair') +terra m.loadpriv(buf: &uint8, len: intptr): lib.stat(ctx) + lib.dbg('parsing saved private key') + + var pk: ctx + lib.pk.mbedtls_pk_init(&pk) + var rt = lib.pk.mbedtls_pk_parse_key(&pk, buf, len + 1, nil, 0) + if rt == 0 then + return [lib.stat(ctx)] { ok = true, val = pk } + else + lib.pk.mbedtls_pk_free(&pk) + return [lib.stat(ctx)] { ok = false } + end +end + +terra m.loadpub(buf: &uint8, len: intptr): lib.stat(ctx) + lib.dbg('parsing saved key') var pk: ctx lib.pk.mbedtls_pk_init(&pk) - lib.pk.mbedtls_pk_parse_key(&pk, buf, len + 1, nil, 0) - return pk + var rt = lib.pk.mbedtls_pk_parse_public_key(&pk, buf, len) + if rt == 0 then + return [lib.stat(ctx)] { ok = true, val = pk } + else + lib.pk.mbedtls_pk_free(&pk) + return [lib.stat(ctx)] { ok = false, error = rt } + end end terra m.sign(pk: &ctx, txt: rawstring, len: intptr) lib.dbg('signing message') var osz: intptr = 0 @@ -135,11 +161,11 @@ return sig end terra m.verify(pk: &ctx, txt: rawstring, len: intptr, - sig: rawstring, siglen: intptr): {bool, uint8} + sig: &uint8, siglen: intptr): {bool, uint8} lib.dbg('verifying signature') var osz: intptr = 0 var hash: uint8[64] -- there does not appear to be any way to extract the hash algorithm @@ -153,13 +179,13 @@ {lib.md.MBEDTLS_MD_SHA1, 'sha1', 1}, -- uncommon hashes {lib.md.MBEDTLS_MD_SHA384, 'sha384', 2}, {lib.md.MBEDTLS_MD_SHA224, 'sha224', 2}, -- bad hashes - {lib.md.MBEDTLS_MD_MD5, 'md5', 0}, - {lib.md.MBEDTLS_MD_MD4, 'md4', 0}, - {lib.md.MBEDTLS_MD_MD2, 'md2', 0} + {lib.md.MBEDTLS_MD_MD5, 'md5', 0} + --{lib.md.MBEDTLS_MD_MD4, 'md4', 0}, + --{lib.md.MBEDTLS_MD_MD2, 'md2', 0} ) for i = 0, [algs.type.N] do var hk, aname, secl = algs[i] @@ -188,10 +214,16 @@ terra m.hmaca(alg: hashalg, key: lib.mem.ptr(uint8), txt: lib.mem.ptr(int8)) var buf = lib.mem.heapa(uint8, alg.bytes) m.hmac(alg, key, txt, buf.ptr) return buf end + +terra m.hmacp(p: &lib.mem.pool, alg: hashalg, key: lib.mem.ptr(uint8), txt: lib.mem.ptr(int8)) + var buf = p:alloc(uint8, alg.bytes) + m.hmac(alg, key, txt, buf.ptr) + return buf +end terra m.hotp(key: &(uint8[10]), counter: uint64) var hmac: uint8[20] var ctr = [lib.mem.ptr(int8)]{ptr = [&int8](&counter), ct = 8} m.hmac(m.alg.sha1, @@ -202,7 +234,26 @@ var p: uint8[4] for i=0,4 do p[i] = hmac[ofs + i] end return (@[&uint32](&p)) and 0x7FFFFFFF -- one hopes it's that easy end + +local splitwords = macro(function(str) + local words = {} + for w in str:asvalue():gmatch('(%g+)') do words[#words + 1] = w end + return `arrayof(lib.str.t, [words]) +end) + +terra m.cryptogram(a: &lib.str.acc, len: intptr) + var words = splitwords [[ + alpha beta gamma delta epsilon psi eta nu omicron omega + red crimson green verdant golden silver blue cyan navy + carnelian opal sapphire amethyst ruby jade emerald + chalice peacock cabernet windmill saxony tunnel waterspout + ]] + for i = 0, len do + a:ppush(words[m.random(intptr,0,[words.type.N])]):lpush '-' + end + a:ipush(m.random(uint32,0,99999)) +end return m ADDED doc/auth.md Index: doc/auth.md ================================================================== --- doc/auth.md +++ doc/auth.md @@ -0,0 +1,59 @@ +# credentials & authentication + +parsav features a highly flexibly authentication system, and provides alternatives to simple per-user passwords to help users keep their accounts secure. you can create as many credentials for you account as you wish, and tie them to specific IP addresses or regions. you can even restrict the capabilities of a given credential -- for instance, you could have one password that allows full access and one that only allows posting new tweets when logged in with it. + +## mechanisms + +you're not limited to passwords, however. parsav intends to support a very wide range of authentication mechanisms; this page lists all the mechanisms that have been implemented so far. + +### password auth + +of course, parsav lets you access your account with passwords/passphrases like most other software. when you create a new password, you'll be asked to enter it twice to confirm that you haven't mistyped it; you'll then be able to use it by typing it into the login screen. + +passwords have many disadvantages, however. they're easy to steal, easy to forget, they get more and more difficult to use the more secure they are, and people have an awful habit of *writing them down.* if you don't have any problems memorizing long, secure passwords, this may not be a problem for you. but even then, there's no way around the fundamental problem that passwords are *static* -- you only have to slip up once (say, by typing it into a username field by mistake just as the black helicopters are passing overhead) and then the password is compromised forever. + +of course, if you have a static IP address, you can get around some of the insecurity by setting a netmask on the password -- it won't do mongolian bitcoin scammers much good if only IPs from mecklenburg-vorpommern are allowed to use it. netmasks are however best used on VPNs, LANs, and similar arrangements where you have an absolute guarantee of a static IP address. for other circumstances, *challenge auth* may be a worthwhile means of improving your security. + +### challenge auth + +parsav also supports *challenge auth,* which is a form of authentication where are presented with a *challenge token* at login and have to provide with a response digest based on the token to authenticate yourself. this mechanism has the very useful property that the same digest can only be used for a very short period of time, after which they are permanently deactivated, giving you a bit of protection even if your HTTP session is exposed to a man-in-the-middle. due to the way they're implemented, they're effectively immune to shouldersurfing. challenge auth is generally based on cryptographic keys. + +right now, the only form of challenge authentication supported is RSA asymmetric keypairs, though other methods based on elliptic curve cryptography and shared secrets are planned. an RSA keypair is a pair of very long numbers -- called the *public key* and the *private key* -- with special mathematical properties: anyone who holds the public key can encrypt data such that only the person with the private key can read it, and whoever holds the public key can place a digital signature on a piece of data such that anyone with the public key can confirm the data was endorsed by the holder of the private key. (private keys can of course be encrypted with a password; the advantage this has over normal passwords is that the password never leaves your computer's memory.) so when you log in with RSA challenge auth, you'll be given a short string to sign with your private key. all you have to do is paste the signature into the "digest" box and you'll be logged in. + +keypairs are bit more complex to use than passwords, however. you have to use a special tool to create them. on linux and other unix-like systems, you can do this with the `openssl` command: + + $ openssl genrsa 2048 -out private.pem + # creates a reasonably secure 2048-bit private key + + $ openssl genrsa 4096 -out private.pem + # creates an *extremely secure 4096-bit key + + $ openssl genrsa 2048 -aes256 -out private.pem + # pass -aes256 to encrypt your key + +once you've created your private key with a command like one of the above, you'll need to separate out a public key. if you used the `-aes256` flag, you'll be prompted for your password. (keep in mind, this password *cannot* be recovered if it is forgotten!) + + $ openssl rsa -in private.pem -pubout -out public.pem + +`public.pem` is the file you'll want to copy and paste into the text box when you add this keypair as a credential to your parsav account. do *not* ever upload `private.pem` anywhere! if you ever do so by accident, delete the keypair credential from every account that uses it immediately, as you have irreversibly compromised their security. + +finally, you'll need to use this key to actually sign things: + + $ echo -n "this is the string that will be signed" | openssl dgst -sha256 -sign private.pem | openssl base64 + +this command is somewhat complex, so you may want to write a short script to save yourself some time. on computers with the X windows system, you can use the following convenient command to encrypt whatever is currently in the clipboard: + + $ xsel -bo | openssl dgst -sha256 -sign private.pem | openssl base64 | xsel -bi + +if you later want to change the password on your private key, you can use this command to do so: + + $ openssl rsa -in private.pem -aes256 -out private.pem + # omit the -aes256 to remove the encryption + +## managing credentials + +you can use the "security" panel in the configuration menu to manage your credentials. this panel has a wide range of options. firstly, if you suspect someone may have unwanted access to your account, you can press the "invalidate other sessions" button to instantly log out every computer but your own. of course, this will only briefly inconvenience evildoers if they have your password -- it's mainly useful for instances where you forgot to log out of a public computer, or one that belongs to someone else. + +you can manage existing credentials with the "revoke" button, which wipes out a selected credential so it can no longer be used to log in (and logs out every device logged in under it!), or `reset`, which lets you change the credentials without affecting their privilege sets. + +finally, you can create new credentials by picking the desired properties (what privileges and netmask they are restricted to, if any) and pressing the relevant button. Index: doc/load.lua ================================================================== --- doc/load.lua +++ doc/load.lua @@ -1,9 +1,10 @@ local path = ... local sources = { -- user section acl = {title = 'access control lists'}; + auth = {title = 'credentials & authentication', priv = 'account'}; -- admin section --overview = {title = 'server overview', priv = 'config'}; invocation = {title = 'daemon invocation', priv = 'config'}; usr = {title = 'user accounting', priv = {'elevate','demote','purge','herald'}}; --srvcfg = {title = 'server configuration policies', priv = 'config'}; @@ -12,15 +13,16 @@ --pgsql = {title = 'pgsql', priv = 'config', parent = 'backends'}; } local util = dofile 'common.lua' local ingest = function(filename) - return (util.exec { 'cmark', '--smart', '--unsafe', (path..'/'..filename) }):gsub('\n','') + return (util.exec { 'cmark', '--smart', '--unsafe', (path..'/'..filename) }) + --:gsub('\n','') end local doc = {} for n,meta in pairs(sources) do doc[n] = { name = n; text = ingest(n .. '.md'); meta = meta; } end return doc Index: mgtool.t ================================================================== --- mgtool.t +++ mgtool.t @@ -293,14 +293,12 @@ srv:conprep(lib.store.prepmode.conf) do var newkp = lib.crypt.genkp() -- generate server privkey var kbuf: uint8[lib.crypt.const.maxdersz] - var privsz = lib.crypt.der(false,&newkp, kbuf) - dlg:server_setup_self(dbmode.arglist(1), [lib.mem.ptr(uint8)] { - ptr = &kbuf[0], ct = privsz - }) + var derkey = lib.crypt.der(false,&newkp, kbuf) + dlg:server_setup_self(dbmode.arglist(1), derkey) end dlg:conf_set('instance-name', dbmode.arglist(1)) dlg:conf_set('domain', dbmode.arglist(1)) do var sec: int8[65] gensec(&sec[0]) Index: parsav.md ================================================================== --- parsav.md +++ parsav.md @@ -104,18 +104,25 @@ * ☐ pw-extern-cyrus: try to authenticate against saslauthd * ☐ pw-extern-dovecot: try to authenticate against a dovecot SASL socket * ☐ pw-extern-krb5: abuse MIT kerberos as a password verifier * ☐ pw-extern-imap: abuse an email server as a password verifier * (extra credit) ☐ pw-extern-radius: verify a user against a radius server +* ☐ http-oauth: automatically created when a user grants access to an oauth application, consisting of a series of TLVs. these generally should not be created or fiddled with manually +* ☐ http-gssapi: log in with a kerberos principle through the http-authenticate "negotiate" mechanism. do any browsers actually support this?? +* ☐ http-extern-header: a value of `H=V` where `H` is a header passed by an app server such as nginx, and `V` is the required value. could be used to e.g. tie parsav into an existing client certificate verification infrastructure with minimal effort. +* ☐ http-extern-header: a value of `H=V` where `H` is a header passed by an app server such as nginx, and `V` is the required value. could be used to tie parsav into an existing client certificate verification infrastructure with minimal effort. * ☐ api-digest-sha{…}: a value that can be hashed with the current epoch to derive a temporary access key without logging in. these are used for API calls, sent in the header `X-API-Key`. +* ☐ api-token-sha{…}: a password (ideally a very long, randomly generated one) that can be sent in the headers to automatically authenticate the user. far less secure than `api-digest-*`! * ☐ otp-time-sha1: a TOTP PSK: the first two bytes represent the step, the third byte the OTP length, and the remaining ten bytes the secret key * ☐ tls-cert-fp: a fingerprint of a client certificate * ☐ tls-cert-ca: a value of the form `fp/key=value` where a client certificate with the property `key=value` (e.g. `uid=cyberlord19`) signed by a certificate authority matching the given fingerprint `fp` can authenticate the user -* ☐ challenge-rsa-sha256: an RSA public key. the user is presented with a challenge and must sign it with the corresponding private key using SHA256. -* ☐ challenge-ecc-sha256: a Curve25519 public key. the user is presented with a challenge and must sign it with the corresponding private key using SHA256. -* ☐ challenge-ecc448-sha256: a Curve448 public key. the user is presented with a challenge and must sign it with the corresponding private key using SHA256. +* ☐ challenge-rsa: an RSA public key. the user is presented with a challenge and must sign it with the corresponding private key using any one of the supported hash algorithms, ideally SHA512 or -256. +* ☐ challenge-ecc a Curve25519 public key. the user is presented with a challenge and must sign it with a supported hash algorithm +* ☐ challenge-ecc448: a Curve448 public key. the user is presented with a challenge and must sign it with the corresponding private key using a supported hash algorithm. * ☑ trust: authentication always succeeds (or fails, if blacklisted). only use in combination with netmask!!! + +we should also look into support for various kinds of hardware auth. we already have TPM support through RSA auth, but external devices like security keys should be supported as well. ## legal parsav is released under the terms of the EUPL v1.2. copies of this license are included in the repository. by contributing any intellectual property to this project, you reassign ownership and all attendant rights over that intellectual property to the current maintainer. this is to ensure that the project can be relicensed without difficulty in the unlikely event that it is necessary. Index: parsav.t ================================================================== --- parsav.t +++ parsav.t @@ -312,11 +312,11 @@ end set.bits = {} set.idvmap = {} for i,v in ipairs(tbl) do set.idvmap[v] = i - set.bits[v] = quote var b: set b:clear() b:setbit(i, true) in b end + set.bits[v] = quote var b: set b:clear() b:setbit([i-1], true) in b end end set.metamethods.__add = macro(function(self,other) local new = symbol(set) local q = quote var [new] new:clear() end for i = 0, bytes - 1 do Index: render/conf/sec.t ================================================================== --- render/conf/sec.t +++ render/conf/sec.t @@ -37,22 +37,35 @@ credmgr.credlist = cl:finalize() end credmgr:append(&a) --if credmgr.credlist.ct > 0 then credmgr.credlist:free() end else + var time = lib.osclock.time(nil) + var timestr: int8[26] lib.osclock.ctime_r(&time, ×tr[0]) + var cmt = co:stra(48) + cmt:lpush('enrolled over http on '):push(×tr[0],0) if new:cmp('pw') then var d: data.view.conf_sec_pwnew - var time = lib.osclock.time(nil) - var timestr: int8[26] lib.osclock.ctime_r(&time, ×tr[0]) - var cmt = co:stra(48) - cmt:lpush('enrolled over http on '):push(×tr[0],0) d.comment = cmt:finalize() var st = d:poolstr(&co.srv.pool) --d.comment:free() return st - elseif new:cmp('challenge') then + elseif new:cmp('rsa') then + var c = co:stra(64) + lib.crypt.cryptogram(&c, 8) + var cptr = c:finalize(); + var hmac = lib.crypt.hmacp(&co.srv.pool, lib.crypt.alg.sha256, co.srv.cfg.secret:blob(), cptr); -- TODO should expire after 10min + var hmacte: int8[lib.math.shorthand.maxlen] + var hmacte_len = lib.math.shorthand.gen(lib.math.truncate64(hmac.ptr, hmac.ct), &hmacte[0]) + var d = data.view.conf_sec_keynew { + comment = cmt:finalize(); + nonce = cptr; + noncevld = pstr { ptr = &hmacte[0], ct = hmacte_len }; + } + + return d:poolstr(&co.srv.pool) -- we're going to break the rules a bit and do database munging from -- the rendering code, because doing otherwise in this case would be -- genuinely nightmarish elseif new:cmp('otp') then elseif new:cmp('api') then Index: render/login.t ================================================================== --- render/login.t +++ render/login.t @@ -6,10 +6,12 @@ title = 'instance logon'; class = 'login'; cache = false; } + var how = co:ppostv('how') + if user == nil then var form = data.view.login_username { loginmsg = msg; } if form.loginmsg.ptr == nil then @@ -17,11 +19,21 @@ end doc.body = form:tostr() elseif creds:sz() == 0 then co:complain(403,'access denied','your host is not eligible to authenticate as this user') return - elseif creds:sz() == 1 then + elseif creds:sz() == 1 or how:ref() then + var newcreds: lib.store.credset + if how:ref() then + if how:cmp('pw') then newcreds = creds and [lib.store.credset.bits.pw] + elseif how:cmp('chlg') then newcreds = creds and [lib.store.credset.bits.challenge] + elseif how:cmp('otp') then newcreds = creds and [lib.store.credset.bits.otp] + elseif how:cmp('trust') then newcreds = creds and [lib.store.credset.bits.trust] + else co:complain(400, 'bad request', 'the requested authentication method is not available') return end + creds = &newcreds + end + if creds.trust() then -- TODO log in immediately return end @@ -31,31 +43,62 @@ } if creds.pw() then ch.challenge = 'enter the password associated with your account' ch.label = 'password' ch.method = 'pw' - ch.auto = 'current-password'; + ch.inputfield = ''; elseif creds.otp() then ch.challenge = 'enter a valid one-time password for your account' ch.label = 'OTP code' ch.method = 'otp' - ch.auto = 'one-time-code'; + ch.inputfield = ''; elseif creds.challenge() then - ch.challenge = 'sign the challenge token: ...' + var tok = co:stra(128) + var chlg = co:stra(128) + var input = co:stra(256) + var time = lib.osclock.time(nil) + + lib.crypt.cryptogram(&tok,6) + chlg:lpush 'sign the challenge token ' + :push(tok.buf,tok.sz) + :lpush '' + + ch.challenge = chlg:finalize() ch.label = 'digest' ch.method = 'challenge' - ch.auto = 'one-time-code'; + input:lpush '' + + ch.inputfield = input:finalize() else - co:complain(500,'login failure','unknown login method') + co:complain(400,'login failure','no usable login methods are available') return end - doc.body = ch:tostr() - else - -- pick a method + doc.body = ch:poolstr(&co.srv.pool) + else -- pick a method + var a = co:stra(400) + var username = lib.html.sanitize(&co.srv.pool, pstr{user.handle,0}, true) + a:lpush '

multiple authentication mechanisms are available. select one to continue.

' + if creds.trust() then a:lpush '' end + if creds.pw() then a:lpush '' end + if creds.otp() then a:lpush '' end + if creds.challenge() then a:lpush '' end + a:lpush '
' + doc.body = a:finalize() end co:stdpage(doc) - doc.body:free() + --doc.body:free() end return login_form Index: route.t ================================================================== --- route.t +++ route.t @@ -142,13 +142,38 @@ else var aid: uint64 = 0 lib.dbg('authentication attempt beginning') -- attempt login with provided method if lib.str.ncmp('pw', am, lib.math.biggest(2,aml)) == 0 and chrs ~= nil then aid = co.srv:actor_auth_pw(co.peer, - [lib.mem.ptr(int8)]{ptr=usn,ct=usnl}, - [lib.mem.ptr(int8)]{ptr=chrs,ct=chrsl}) - elseif lib.str.ncmp('otp', am, lib.math.biggest(2,aml)) == 0 and chrs ~= nil then + pstring {ptr=usn,ct=usnl}, + pstring {ptr=chrs,ct=chrsl}) + elseif lib.str.ncmp('challenge', am, lib.math.biggest(9,aml)) == 0 and chrs ~= nil then + lib.dbg('challenge attempt beginning') + var s_time = co:ppostv('time') + var s_vfy = co:ppostv('vfy') + var token = co:ppostv('token') + if s_time:ref() and s_vfy:ref() and token:ref() then + lib.dbg('checking hmac validity') + var vftok = co:stra(128) vftok:ppush(token):ppush(s_time) + var hmac = lib.crypt.hmacp(&co.srv.pool, lib.crypt.alg.sha256, co.srv.cfg.secret:blob(), vftok:finalize()) + var vfy, vfyok = lib.math.shorthand.parse(s_vfy.ptr, s_vfy.ct) + if vfyok and lib.math.truncate64(hmac.ptr,hmac.ct) == vfy then + lib.dbg('checking expiration time') + var time, timeok = lib.math.shorthand.parse(s_time.ptr, s_time.ct) + if timeok and lib.osclock.time(nil) - time < [2 * 60] then -- two minutes + lib.dbg('decoding base64') + var bin = co.srv.pool:alloc(uint8, chrsl) + var binlen: intptr + if lib.b64.mbedtls_base64_decode(bin.ptr, bin.ct, &binlen, [&uint8](chrs), chrsl) == 0 then + lib.dbg('running signature <',{chrs,chrsl},'> against challenge keys for token [', {token.ptr,token.ct}, ']') + aid = co.srv:actor_auth_challenge(co.peer, + pstring {usn,usnl}, binblob{bin.ptr,binlen}, token) + end + end + end + end + elseif lib.str.ncmp('otp', am, lib.math.biggest(3,aml)) == 0 and chrs ~= nil then lib.dbg('using otp auth') -- ··· -- else lib.dbg('invalid auth method') end -- error out @@ -341,23 +366,61 @@ co:installkey('?',co.aid) return elseif act:cmp( 'newcred') then var cmt = co:ppostv('comment') var pw = co:ppostv('newpw') + var rsapub = co:ppostv('newrsa'):blob() var aid: uint64 = 0 if pw:ref() then var cpw = co:ppostv('rptpw') if not pw:cmp(cpw) then co:complain(400,'enrollment failure','the passwords you supplied do not match') return end aid = co.srv:auth_attach_pw(uid, false, pw, cmt) - else - var key = co:ppostv('newkey') - if key:ref() then + elseif rsapub:ref() then + var sig = co:ppostv('sig') + var nonce = co:ppostv('nonce') + var s_noncevld = co:ppostv('noncevld') + var noncevld, ok = lib.math.shorthand.parse(s_noncevld.ptr, s_noncevld.ct) + if not ok then + co:complain(403,'try harder next time','you call that cryptanalysis?') + return + end + var fr = co.srv.pool:frame() + var hmac = lib.crypt.hmacp(&co.srv.pool, lib.crypt.alg.sha256, co.srv.cfg.secret:blob(), nonce) + if not lib.math.truncate64(hmac.ptr, hmac.ct) == noncevld then + co:complain(403,'nice try','what exactly are you trying to accomplish here, buddy') + return + end + + var pkres = lib.crypt.loadpub(rsapub.ptr,rsapub.ct+1) -- needs NUL + if not pkres.ok then + co:complain(400,'invalid key','the key you have supplied is not a valid PEM or DER file') + return + end + var pk = pkres.val + defer pk:free() + + var decoded = co.srv.pool:alloc(uint8,sig.ct) + var decoded_sz: intptr = 0 + if lib.b64.mbedtls_base64_decode(decoded.ptr,sig.ct,&decoded_sz,[&uint8](sig.ptr),sig.ct) ~= 0 then + co:complain(400,'invalid signature','the signature you supplied is not encoded in valid base64') + return + end + + var vfy, secl = lib.crypt.verify(&pk, nonce.ptr, nonce.ct, decoded.ptr, decoded_sz) + if not vfy then + co:complain(403,'verification failed','the signature you supplied does not match the required nonce') + return end + + var dbuf: uint8[lib.crypt.const.maxdersz] + var derkey = lib.crypt.der(true, &pk, &dbuf[0]) + aid = co.srv:auth_attach_rsa(co.who.id, false, derkey, cmt) + co.srv.pool:reset(fr) end if aid ~= 0 then lib.dbg('setting credential restrictions') var privs = [(function() local check = quote end Index: srv.t ================================================================== --- srv.t +++ srv.t @@ -820,37 +820,73 @@ end end return cs, ok end -terra srv:actor_auth_pw(ip: lib.store.inet, user: pstring, pw: pstring): uint64 - for i=0,self.sources.ct do - if self.sources(i).backend ~= nil and - self.sources(i).backend.actor_auth_pw ~= nil then - var aid,uid,newhnd = self.sources(i):actor_auth_pw(ip,user,pw) - if aid ~= 0 then - if uid == 0 then - lib.dbg('new user just logged in, creating account entry') - var kbuf: uint8[lib.crypt.const.maxdersz] - var na = lib.store.actor.mk(&kbuf[0]) - na.handle = newhnd.ptr - var newuid: uint64 - if self.sources(i).backend.actor_create ~= nil then - newuid = self.sources(i):actor_create(&na) - else newuid = self:actor_create(&na) end +local function mk_auth_fn(suffix,...) + local syms = {...} + local name = 'actor_auth_' .. suffix + srv.methods[name] = terra(self: &srv, ip: lib.store.inet, user: pstring, [syms]): uint64 + for i=0,self.sources.ct do + if self.sources(i).backend ~= nil and + self.sources(i).backend.[name] ~= nil then + var aid,uid,newhnd = self.sources(i):[name](ip,user, [syms]) + if aid ~= 0 then + if uid == 0 then + lib.dbg('new user just logged in, creating account entry') + var kbuf: uint8[lib.crypt.const.maxdersz] + var na = lib.store.actor.mk(&kbuf[0]) + na.handle = newhnd.ptr + var newuid: uint64 + if self.sources(i).backend.actor_create ~= nil then + newuid = self.sources(i):actor_create(&na) + else newuid = self:actor_create(&na) end - if self.sources(i).backend.actor_auth_register_uid ~= nil then - self.sources(i):actor_auth_register_uid(aid,newuid) + if self.sources(i).backend.actor_auth_register_uid ~= nil then + self.sources(i):actor_auth_register_uid(aid,newuid) + end end + return aid end - return aid end end + + return 0 end - - return 0 + srv.methods[name].name = name end + +mk_auth_fn('pw', symbol(pstring)) +mk_auth_fn('challenge', symbol(lib.mem.ptr(uint8)), symbol(pstring)) + +--terra srv:actor_auth_pw(ip: lib.store.inet, user: pstring, pw: pstring): uint64 +-- for i=0,self.sources.ct do +-- if self.sources(i).backend ~= nil and +-- self.sources(i).backend.actor_auth_pw ~= nil then +-- var aid,uid,newhnd = self.sources(i):actor_auth_pw(ip,user,pw) +-- if aid ~= 0 then +-- if uid == 0 then +-- lib.dbg('new user just logged in, creating account entry') +-- var kbuf: uint8[lib.crypt.const.maxdersz] +-- var na = lib.store.actor.mk(&kbuf[0]) +-- na.handle = newhnd.ptr +-- var newuid: uint64 +-- if self.sources(i).backend.actor_create ~= nil then +-- newuid = self.sources(i):actor_create(&na) +-- else newuid = self:actor_create(&na) end +-- +-- if self.sources(i).backend.actor_auth_register_uid ~= nil then +-- self.sources(i):actor_auth_register_uid(aid,newuid) +-- end +-- end +-- return aid +-- end +-- end +-- end +-- +-- return 0 +--end terra cfgcache.methods.load :: {&cfgcache} -> {} terra cfgcache:init(o: &srv) self.overlord = o self:load() Index: static/style.scss ================================================================== --- static/style.scss +++ static/style.scss @@ -381,38 +381,53 @@ text-align: center; menu:first-of-type { margin-top: 0.3in; } img.icon { width: 1.875in; height: 1.875in; } } -div.login { - @extend %box; - width: 4in; - padding: 0.4in; - > .msg { - text-align: center; - padding: 0.3in; - } - > .msg:first-child { padding-top: 0; } - > .user { - width: max-content; margin: auto; - background: tone(-20%,-0.3); - border: 1px solid black; - color: tone(-50%); - padding: 0.1in; - > img { display: block; width: 1in; height: 1in; margin: auto; border: 1px solid black; } - > .name { @extend %serif; text-align: center; font-size: 130%; font-weight: bold; margin-top: 0.08in; } - } - >form { - display: grid; - grid-template-columns: 1fr 1fr; - grid-template-rows: 1.2em 1fr 1fr; - grid-gap: 5px; - > label, input, button { display: block; } - > label { grid-column: 1 / 3; grid-row: 1/2; font-weight: bold } - > input { grid-column: 1 / 3; grid-row: 2/3; } - > button { grid-column: 2 / 3; grid-row: 3/4; } - > a { @extend %button; grid-column: 1 / 2; grid-row: 3/4; } +body.login { + form.auth-select { + @extend %box; + width: 3in; + padding: 0.4in; + p { text-align: center; } + menu { + %button { + display: block; + width: 100%; + & + %button { border-top: none; } + } + } + } + div.login { + @extend %box; + width: 4in; + padding: 0.4in; + > .msg { + text-align: center; + padding: 0.3in; + } + > .msg:first-child { padding-top: 0; } + > .user { + width: max-content; margin: auto; + background: tone(-20%,-0.3); + border: 1px solid black; + color: tone(-50%); + padding: 0.1in; + > img { display: block; width: 1in; height: 1in; margin: auto; border: 1px solid black; } + > .name { @extend %serif; text-align: center; font-size: 130%; font-weight: bold; margin-top: 0.08in; } + } + >form { + display: grid; + grid-template-columns: 1fr 1fr; + grid-template-rows: 1.2em max-content max-content; + grid-gap: 5px; + > label, input, button { display: block; } + > label { grid-column: 1 / 3; grid-row: 1/2; font-weight: bold } + > input, textarea { grid-column: 1 / 3; grid-row: 2/3; } + > button { grid-column: 2 / 3; grid-row: 3/4; } + > a { @extend %button; grid-column: 1 / 2; grid-row: 3/4; } + } } } form.compose { @extend %box; @@ -482,26 +497,34 @@ right: 0.1in; margin: 0.1in; padding: 0.1in; &:hover { font-weight: bold; } } + p { text-align: justify; } } code { @extend %teletype; background: tone(-55%); - border: 1px inset tone(-20%); + // border: 1px inset tone(-20%); padding: 2px 6px; font-size: 1.5ex !important; letter-spacing: 1.3px; padding-bottom: 3px; - border-radius: 2px; + border-radius: 4px; vertical-align: baseline; box-shadow: 1px 1px 1px black; } -pre { @extend %teletype; white-space: pre-wrap; } +pre { + @extend %teletype; + white-space: pre-wrap; + > code:only-child { + display: block; + padding: 0.1in; + } +} div.thread { margin-left: 0.3in; & + article.post { margin-top: 0.3in; } } @@ -699,10 +722,11 @@ @extend %serif; box-sizing: border-box; padding: 0.08in 0.1in; border: 1px solid black; background: tone(-55%); + user-select: all; } > input, textarea, .txtbox { display: block; width: 100%; } Index: store.t ================================================================== --- store.t +++ store.t @@ -164,20 +164,18 @@ return true end terra m.actor.methods.mk(kbuf: &uint8) var newkp = lib.crypt.genkp() - var privsz = lib.crypt.der(false,&newkp,kbuf) + var derkey = lib.crypt.der(false,&newkp,kbuf) return m.actor { id = 0; nym = nil; handle = nil; origin = 0; bio = nil; avatar = nil; knownsince = lib.osclock.time(nil); rights = m.rights_default(); avatarid = 0; - epithet = nil, key = [lib.mem.ptr(uint8)] { - ptr = &kbuf[0], ct = privsz - }; + epithet = nil, key = derkey; } end struct m.actor_stats { posts: intptr @@ -386,10 +384,16 @@ -> {uint64, uint64, pstr} -- handles password-based logins against hashed passwords -- origin: inet -- handle: rawstring -- token: rawstring + actor_auth_challenge: {&m.source, m.inet, pstr, lib.mem.ptr(uint8), pstr } + -> {uint64, uint64, pstr} + -- origin: inet + -- handle: rawstring + -- response: rawstring + -- challenge token: pstring actor_auth_tls: {&m.source, m.inet, rawstring} -> {uint64, uint64, pstr} -- handles implicit authentication performed as part of an TLS connection -- origin: inet -- fingerprint: rawstring @@ -418,11 +422,11 @@ actor_rel_calc: {&m.source, uint64, uint64} -> m.relationship auth_enum_uid: {&m.source, uint64} -> lib.mem.lstptr(m.auth) auth_enum_handle: {&m.source, rawstring} -> lib.mem.lstptr(m.auth) auth_attach_pw: {&m.source, uint64, bool, pstr, pstr} -> uint64 - auth_attach_key: {&m.source, uint64, bool, pstr, pstr} -> {} + auth_attach_rsa: {&m.source, uint64, bool, lib.mem.ptr(uint8), pstr} -> uint64 -- uid: uint64 -- reset: bool (delete other passwords?) -- pw: pstring -- comment: pstring auth_privs_set: {&m.source, uint64, m.privset} -> {} Index: str.t ================================================================== --- str.t +++ str.t @@ -60,10 +60,16 @@ end terra ty:ffw() var newp = m.ffw(self.ptr,self.ct) var newct = self.ct - (newp - self.ptr) return ty { ptr = newp, ct = newct } + end + terra ty:blob() + return byteptr { + ptr = [&uint8](self.ptr); + ct = self.ct; + } end end install_funcs(strptr) install_funcs(strref) Index: view/conf-sec-credmg.tpl ================================================================== --- view/conf-sec-credmg.tpl +++ view/conf-sec-credmg.tpl @@ -5,10 +5,11 @@ @credlist + @?auth

you can associate extra credentials with this account. you can also limit how much of this account’s authority these credentials can be used to exercise — for instance, it might be useful to create API keys that can read the account timeline, but not post as the account owner or access any of his administrative powers. if you don't select a capability set, the credential will be able to wield the full scope of the associated account‘s powers.

@@ -27,10 +28,10 @@ + -
ADDED view/conf-sec-keynew.tpl Index: view/conf-sec-keynew.tpl ================================================================== --- view/conf-sec-keynew.tpl +++ view/conf-sec-keynew.tpl @@ -0,0 +1,26 @@ +
+
+ + +
+
+ + +
+

to confirm your ownership of the private key, you'll need to sign the nonce provided below before it expires in 10 minutes. on unix-like OSes, you can usually use the openssl utility for this.

+ echo -n @nonce | openssl dgst -sha256 -sign privkey.pem | openssl base64 +
+ +
@nonce
+ + +
+
+ + +
+ + + cancel + +
Index: view/load.lua ================================================================== --- view/load.lua +++ view/load.lua @@ -22,10 +22,11 @@ 'conf'; 'conf-profile'; 'conf-sec'; 'conf-sec-credmg'; 'conf-sec-pwnew'; + 'conf-sec-keynew'; 'conf-user-ctl'; } local ingest = function(filename) local hnd = io.open(path..'/'..filename) Index: view/login-challenge.tpl ================================================================== --- view/login-challenge.tpl +++ view/login-challenge.tpl @@ -5,10 +5,10 @@
@challenge
- + @inputfield cancel
Index: view/login-username.tpl ================================================================== --- view/login-username.tpl +++ view/login-username.tpl @@ -1,8 +1,8 @@
@loginmsg
- +