parsav  Check-in [0d10a378e9]

Overview
Comment:add auth docs and rsa auth
Downloads: Tarball | ZIP archive | SQL archive
Timelines: family | ancestors | descendants | both | trunk
Files: files | file ages | folders
SHA3-256: 0d10a378e90159932431518d4fa74296f1090e4d3025e45f44e9ddbe012d99e7
User & Date: lexi on 2021-01-11 01:53:23
Other Links: manifest | tags
Context
2021-01-11
02:17
enable revoking credentials check-in: 41cbbca855 user: lexi tags: trunk
01:53
add auth docs and rsa auth check-in: 0d10a378e9 user: lexi tags: trunk
2021-01-10
16:44
add avatar panel check-in: 8398fcda5a user: lexi tags: trunk
Changes

Modified backend/pgsql.t from [b388ba7244] to [7d329a757a].

   116    116   				$11::integer,$4::bigint
   117    117   			) returning id
   118    118   		]];
   119    119   	};
   120    120   
   121    121   	actor_auth_pw = {
   122    122   		params = {pstring,rawstring,pstring,lib.store.inet}, sql = [[
   123         -			select a.aid, a.uid, a.name from parsav_auth as a
          123  +			select a.aid, a.uid, a.name, a.blacklist from parsav_auth as a
   124    124   				left join parsav_actors as u on u.id = a.uid
   125    125   			where (a.uid is null or u.handle = $1::text or (
   126    126   					a.uid = 0 and a.name = $1::text
   127    127   				)) and
   128    128   				(a.kind = 'trust' or (a.kind = $2::text and a.cred = $3::bytea)) and
   129    129   				(a.netmask is null or a.netmask >> $4::inet)
   130    130   			order by blacklist desc limit 1
   131    131   		]];
          132  +	};
          133  +	actor_auth_challenge = {
          134  +		params = {pstring,pstring,lib.store.inet}, sql = [[
          135  +			select a.aid, a.uid, a.name, a.blacklist, a.cred 
          136  +			from parsav_auth as a
          137  +				left join parsav_actors as u on u.id = a.uid
          138  +			where a.kind = 'challenge-' || $1::text and
          139  +				(a.netmask is null or a.netmask >> $3::inet) and
          140  +				(a.uid     is null or u.handle  =  $2::text or
          141  +					(a.uid = 0 and a.name = $2::text))
          142  +			order by blacklist desc
          143  +		]];
   132    144   	};
   133    145   
   134    146   	actor_enum_local = {
   135    147   		params = {}, sql = [[
   136    148   			select id, nym, handle, origin, bio,
   137    149   			       null::text, rank, quota, key, epithet,
   138    150   			       knownsince::bigint,
................................................................................
   292    304   
   293    305   	auth_create_pw = {
   294    306   		params = {uint64, binblob, int64, pstring}, sql = [[
   295    307   			insert into parsav_auth (uid, name, kind, cred, valperiod, comment) values (
   296    308   				$1::bigint,
   297    309   				(select handle from parsav_actors where id = $1::bigint),
   298    310   				'pw-sha256', $2::bytea,
          311  +				$3::bigint, $4::text
          312  +			) on conflict (name,kind,cred) do update set comment = $4::text returning aid
          313  +		]]
          314  +	};
          315  +
          316  +	auth_create_rsa = {
          317  +		params = {uint64, binblob, int64, pstring}, sql = [[
          318  +			insert into parsav_auth (uid, name, kind, cred, valperiod, comment) values (
          319  +				$1::bigint,
          320  +				(select handle from parsav_actors where id = $1::bigint),
          321  +				'challenge-rsa', $2::bytea,
   299    322   				$3::bigint, $4::text
   300    323   			) on conflict (name,kind,cred) do update set comment = $4::text returning aid
   301    324   		]]
   302    325   	};
   303    326   
   304    327   	auth_privs_clear = {
   305    328   		params = {uint64}, cmd = true, sql = [[
................................................................................
  1292   1315   		(cs.trust << r:bool(0,3))
  1293   1316   		return cs, true
  1294   1317   	end];
  1295   1318   	 
  1296   1319   	actor_auth_pw = [terra(
  1297   1320   			src: &lib.store.source,
  1298   1321   			ip: lib.store.inet,
  1299         -			username: lib.mem.ptr(int8),
  1300         -			cred: lib.mem.ptr(int8)
         1322  +			username: pstring,
         1323  +			cred: pstring
  1301   1324   		): {uint64, uint64, pstring}
  1302   1325   
  1303   1326   		[ checksha(`src, 256, ip, username, cred) ] -- most common
  1304   1327   		[ checksha(`src, 512, ip, username, cred) ] -- most secure
  1305   1328   		[ checksha(`src, 384, ip, username, cred) ] -- weird
  1306   1329   		[ checksha(`src, 224, ip, username, cred) ] -- weirdest
  1307   1330   
  1308   1331   		-- TODO: check pbkdf2-hmac
  1309   1332   		-- TODO: check OTP
  1310   1333   		return 0, 0, pstring.null()
  1311   1334   	end];
         1335  +
         1336  +	actor_auth_challenge = [terra(
         1337  +		src: &lib.store.source,
         1338  +		ip: lib.store.inet,
         1339  +		username: pstring,
         1340  +		sig: binblob,
         1341  +		token: pstring
         1342  +	): {uint64, uint64, pstring}
         1343  +		-- we need to iterate through all the challenge types. right now that's just RSA
         1344  +		lib.dbg('checking against token ', {token.ptr,token.ct})
         1345  +		var rsakeys = queries.actor_auth_challenge.exec(src, 'rsa', username, ip)
         1346  +		var toprops = [terra(res: &pqr, i: intptr)
         1347  +			return {
         1348  +				aid = res:int(uint64, i, 0);
         1349  +				uid = res:int(uint64, i, 1);
         1350  +				name = res:_string(i, 2);
         1351  +				blacklist = res:bool(i, 3);
         1352  +				pubkey = res:bin(i, 4);
         1353  +			}
         1354  +		end]
         1355  +		if rsakeys.sz > 0 then defer rsakeys:free()
         1356  +			for i=0, rsakeys.sz do var props = toprops(&rsakeys, i)
         1357  +				lib.dbg('loading next RSA pubkey')
         1358  +				var pub = lib.crypt.loadpub(props.pubkey.ptr, props.pubkey.ct)
         1359  +				if pub.ok then defer pub.val:free()
         1360  +					lib.dbg('checking pubkey against response')
         1361  +					var vfy, secl = lib.crypt.verify(&pub.val, token.ptr, token.ct, sig.ptr, sig.ct)
         1362  +					if vfy then
         1363  +						lib.dbg('signature verified')
         1364  +						if props.blacklist then lib.dbg('key blacklisted!') goto fail end
         1365  +						var dupname = lib.str.dup(props.name.ptr)
         1366  +						return props.aid, props.uid, pstring {dupname, props.name.ct}
         1367  +					end
         1368  +				else lib.warn('invalid pubkey in authentication table for user ',{props.name.ptr, props.name.ct}) end
         1369  +			end
         1370  +		end
         1371  +
         1372  +		-- and so on
         1373  +
         1374  +		lib.dbg('no challenges were successful')
         1375  +		::fail::return 0, 0, pstring.null()
         1376  +	end];
         1377  +
  1312   1378   
  1313   1379   	actor_stats = [terra(src: &lib.store.source, uid: uint64)
  1314   1380   		var r = queries.actor_stats.exec(src, uid)
  1315   1381   		if r.sz == 0 then lib.bail('error fetching actor stats!') end
  1316   1382   		var s: lib.store.actor_stats
  1317   1383   		s.posts = r:int(uint64, 0, 0)
  1318   1384   		s.follows = r:int(uint64, 0, 1)
................................................................................
  1615   1681   		if reset then queries.auth_purge_type.exec(src, nil, uid, 'pw-%') end
  1616   1682   		var r = queries.auth_create_pw.exec(src, uid, binblob {ptr = &hash[0], ct = [hash.type.N]}, lib.osclock.time(nil), comment)
  1617   1683   		if r.sz == 0 then return 0 end
  1618   1684   		var aid = r:int(uint64,0,0)
  1619   1685   		r:free()
  1620   1686   		return aid
  1621   1687   	end];
         1688  +
         1689  +	auth_attach_rsa = [terra(
         1690  +		src: &lib.store.source,
         1691  +		uid: uint64,
         1692  +		reset: bool,
         1693  +		pub: binblob,
         1694  +		comment: pstring
         1695  +	): uint64
         1696  +		if reset then queries.auth_purge_type.exec(src, nil, uid, 'challenge-%') end
         1697  +		var r = queries.auth_create_rsa.exec(src, uid, pub, lib.osclock.time(nil), comment)
         1698  +		if r.sz == 0 then return 0 end
         1699  +		var aid = r:int(uint64,0,0)
         1700  +		r:free()
         1701  +		return aid
         1702  +	end];
  1622   1703   
  1623   1704   	auth_privs_set = [terra(
  1624   1705   		src: &lib.store.source,
  1625   1706   		aid: uint64,
  1626   1707   		set: lib.store.privset
  1627   1708   	): {}
  1628   1709   		var map = array([lib.store.privmap])

Modified crypt.t from [f5b057e4fa] to [034fdb64c7].

    10     10   	end;
    11     11   	toobig = -lib.pk.MBEDTLS_ERR_RSA_OUTPUT_TOO_LARGE;
    12     12   }
    13     13   const.maxpemsz = math.floor((const.keybits / 8)*6.4) + 128 -- idk why this formula works but it basically seems to
    14     14   const.maxdersz = const.maxpemsz -- FIXME this is a safe value but obvs not the correct one
    15     15   
    16     16   local ctx = lib.pk.mbedtls_pk_context
           17  +terra ctx:free() lib.pk.mbedtls_pk_free(self) end
    17     18   
    18     19   local struct hashalg { id: uint8 bytes: intptr }
    19     20   local m = {
    20     21   	pemfile = uint8[const.maxpemsz];
    21     22   	const = const;
    22     23   	algsz = {
    23     24   		sha1 =   160/8;
................................................................................
    78     79   	if pub then
    79     80   		return lib.pk.mbedtls_pk_write_pubkey_pem(key, buf, const.maxpemsz) == 0
    80     81   	else
    81     82   		return lib.pk.mbedtls_pk_write_key_pem(key, buf, const.maxpemsz) == 0
    82     83   	end
    83     84   end
    84     85   
    85         -terra m.der(pub: bool, key: &ctx, buf: &uint8): intptr
           86  +local binblob = lib.mem.ptr(uint8)
           87  +terra m.der(pub: bool, key: &ctx, buf: &uint8): binblob
           88  +	var ofs: intptr
    86     89   	if pub then
    87         -		return lib.pk.mbedtls_pk_write_pubkey_der(key, buf, const.maxdersz)
           90  +		ofs = lib.pk.mbedtls_pk_write_pubkey_der(key, buf, const.maxdersz)
    88     91   	else
    89         -		return lib.pk.mbedtls_pk_write_key_der(key, buf, const.maxdersz)
           92  +		ofs = lib.pk.mbedtls_pk_write_key_der(key, buf, const.maxdersz)
    90     93   	end
           94  +	return binblob {
           95  +		ptr = buf + (const.maxdersz - ofs);
           96  +		ct = ofs;
           97  +	}
    91     98   end
    92     99   
    93    100   m.destroy = lib.dispatch {
    94    101   	[ctx] = function(v) return `lib.pk.mbedtls_pk_free(&v) end;
    95    102   
    96    103   	[false] = function(ptr) return `ptr:free() end;
    97    104   }
................................................................................
   104    111   	lib.pk.mbedtls_pk_setup(&pk, lib.pk.mbedtls_pk_info_from_type(lib.pk.MBEDTLS_PK_RSA))
   105    112   	var rsa = [&lib.rsa.mbedtls_rsa_context](pk.pk_ctx)
   106    113   	lib.rsa.mbedtls_rsa_gen_key(rsa, callbacks.randomize, nil, const.keybits, 65537)
   107    114   
   108    115   	return pk
   109    116   end
   110    117   
   111         -terra m.loadpriv(buf: &uint8, len: intptr): ctx
   112         -	lib.dbg('parsing saved keypair')
          118  +terra m.loadpriv(buf: &uint8, len: intptr): lib.stat(ctx)
          119  +	lib.dbg('parsing saved private key')
          120  +
          121  +	var pk: ctx
          122  +	lib.pk.mbedtls_pk_init(&pk)
          123  +	var rt = lib.pk.mbedtls_pk_parse_key(&pk, buf, len + 1, nil, 0)
          124  +	if rt == 0 then
          125  +		return [lib.stat(ctx)] { ok = true, val = pk }
          126  +	else
          127  +		lib.pk.mbedtls_pk_free(&pk)
          128  +		return [lib.stat(ctx)] { ok = false }
          129  +	end
          130  +end
          131  +
          132  +terra m.loadpub(buf: &uint8, len: intptr): lib.stat(ctx)
          133  +	lib.dbg('parsing saved key')
   113    134   
   114    135   	var pk: ctx
   115    136   	lib.pk.mbedtls_pk_init(&pk)
   116         -	lib.pk.mbedtls_pk_parse_key(&pk, buf, len + 1, nil, 0)
   117         -	return pk
          137  +	var rt = lib.pk.mbedtls_pk_parse_public_key(&pk, buf, len)
          138  +	if rt == 0 then
          139  +		return [lib.stat(ctx)] { ok = true, val = pk }
          140  +	else
          141  +		lib.pk.mbedtls_pk_free(&pk)
          142  +		return [lib.stat(ctx)] { ok = false, error = rt }
          143  +	end
   118    144   end
   119    145   
   120    146   terra m.sign(pk: &ctx, txt: rawstring, len: intptr)
   121    147   	lib.dbg('signing message')
   122    148   	var osz: intptr = 0
   123    149   	var sig = lib.mem.heapa(int8, 2048)
   124    150   	var hash: uint8[32]
................................................................................
   133    159   	if ret ~= 0 then lib.bail('could not sign message hash')
   134    160   	else sig:resize(osz) end
   135    161   
   136    162   	return sig
   137    163   end
   138    164   
   139    165   terra m.verify(pk: &ctx, txt: rawstring, len: intptr,
   140         -                        sig: rawstring, siglen: intptr): {bool, uint8}
          166  +                        sig: &uint8, siglen: intptr): {bool, uint8}
   141    167   	lib.dbg('verifying signature')
   142    168   	var osz: intptr = 0
   143    169   	var hash: uint8[64]
   144    170   
   145    171   	-- there does not appear to be any way to extract the hash algorithm
   146    172   	-- from the message, so we just have to try likely algorithms until
   147    173   	-- we find one that fits or give up. a security level is attached
................................................................................
   151    177   		{lib.md.MBEDTLS_MD_SHA256, 'sha256', 2},
   152    178   		{lib.md.MBEDTLS_MD_SHA512, 'sha512', 3},
   153    179   		{lib.md.MBEDTLS_MD_SHA1,   'sha1',   1},
   154    180   		-- uncommon hashes
   155    181   		{lib.md.MBEDTLS_MD_SHA384, 'sha384', 2},
   156    182   		{lib.md.MBEDTLS_MD_SHA224, 'sha224', 2},
   157    183   		-- bad hashes
   158         -		{lib.md.MBEDTLS_MD_MD5,   'md5', 0},
   159         -		{lib.md.MBEDTLS_MD_MD4,   'md4', 0},
   160         -		{lib.md.MBEDTLS_MD_MD2,   'md2', 0}
          184  +		{lib.md.MBEDTLS_MD_MD5,   'md5', 0}
          185  +		--{lib.md.MBEDTLS_MD_MD4,   'md4', 0},
          186  +		--{lib.md.MBEDTLS_MD_MD2,   'md2', 0}
   161    187   	)
   162    188   	
   163    189   	for i = 0, [algs.type.N] do
   164    190   		var hk, aname, secl = algs[i]
   165    191   
   166    192   		lib.dbg('(1/2) trying hash algorithm ',aname)
   167    193   		if lib.md.mbedtls_md(lib.md.mbedtls_md_info_from_type(hk), [&uint8](txt), len, hash) ~= 0 then
................................................................................
   186    212   end
   187    213   
   188    214   terra m.hmaca(alg: hashalg, key: lib.mem.ptr(uint8), txt: lib.mem.ptr(int8))
   189    215   	var buf = lib.mem.heapa(uint8, alg.bytes)
   190    216   	m.hmac(alg, key, txt, buf.ptr)
   191    217   	return buf
   192    218   end
          219  +
          220  +terra m.hmacp(p: &lib.mem.pool, alg: hashalg, key: lib.mem.ptr(uint8), txt: lib.mem.ptr(int8))
          221  +	var buf = p:alloc(uint8, alg.bytes)
          222  +	m.hmac(alg, key, txt, buf.ptr)
          223  +	return buf
          224  +end
   193    225   
   194    226   terra m.hotp(key: &(uint8[10]), counter: uint64)
   195    227   	var hmac: uint8[20]
   196    228   	var ctr = [lib.mem.ptr(int8)]{ptr = [&int8](&counter), ct = 8}
   197    229   	m.hmac(m.alg.sha1,
   198    230   		[lib.mem.ptr(uint8)]{ptr = [&uint8](key), ct = 10},
   199    231   		ctr, hmac)
................................................................................
   200    232   	
   201    233   	var ofs = hmac[19] and 0x0F
   202    234   	var p: uint8[4]
   203    235   	for i=0,4 do p[i] = hmac[ofs + i] end
   204    236   
   205    237   	return (@[&uint32](&p)) and 0x7FFFFFFF -- one hopes it's that easy
   206    238   end
          239  +
          240  +local splitwords = macro(function(str)
          241  +	local words = {}
          242  +	for w in str:asvalue():gmatch('(%g+)') do words[#words + 1] = w end
          243  +	return `arrayof(lib.str.t, [words])
          244  +end)
          245  +
          246  +terra m.cryptogram(a: &lib.str.acc, len: intptr)
          247  +	var words = splitwords [[
          248  +		alpha beta gamma delta epsilon psi eta nu omicron omega
          249  +		red crimson green verdant golden silver blue cyan navy
          250  +		carnelian opal sapphire amethyst ruby jade emerald
          251  +		chalice peacock cabernet windmill saxony tunnel waterspout
          252  +	]]
          253  +	for i = 0, len do
          254  +		a:ppush(words[m.random(intptr,0,[words.type.N])]):lpush '-'
          255  +	end
          256  +	a:ipush(m.random(uint32,0,99999))
          257  +end
   207    258   
   208    259   return m

Added doc/auth.md version [ac7a8d6f57].

            1  +# credentials & authentication
            2  +
            3  +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.
            4  +
            5  +## mechanisms
            6  +
            7  +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.
            8  +
            9  +### password auth
           10  +
           11  +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.
           12  +
           13  +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.
           14  +
           15  +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.
           16  +
           17  +### challenge auth
           18  +
           19  +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.
           20  +
           21  +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.
           22  +
           23  +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:
           24  +
           25  +	$ openssl genrsa 2048 -out private.pem
           26  +	  # creates a reasonably secure 2048-bit private key
           27  +
           28  +	$ openssl genrsa 4096 -out private.pem
           29  +	  # creates an *extremely secure 4096-bit key
           30  +
           31  +	$ openssl genrsa 2048 -aes256 -out private.pem
           32  +	  # pass -aes256 to encrypt your key
           33  +
           34  +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!)
           35  +
           36  +	$ openssl rsa -in private.pem -pubout -out public.pem
           37  +
           38  +`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.
           39  +
           40  +finally, you'll need to use this key to actually sign things:
           41  +
           42  +	$ echo -n "this is the string that will be signed" | openssl dgst -sha256 -sign private.pem | openssl base64
           43  +
           44  +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:
           45  +
           46  +	$ xsel -bo | openssl dgst -sha256 -sign private.pem | openssl base64 | xsel -bi
           47  +
           48  +if you later want to change the password on your private key, you can use this command to do so:
           49  +
           50  +    $ openssl rsa -in private.pem -aes256 -out private.pem
           51  +	  # omit the -aes256 to remove the encryption
           52  +
           53  +## managing credentials
           54  +
           55  +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.
           56  +
           57  +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.
           58  +
           59  +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.

Modified doc/load.lua from [783a358256] to [e78e99763f].

     1      1   local path = ...
     2      2   local sources = {
     3      3   -- user section
     4      4   	acl = {title = 'access control lists'};
            5  +	auth = {title = 'credentials &amp; authentication', priv = 'account'}; 
     5      6   -- admin section
     6      7   	--overview = {title = 'server overview', priv = 'config'};
     7      8   	invocation = {title = 'daemon invocation', priv = 'config'};
     8      9   	usr = {title = 'user accounting', priv = {'elevate','demote','purge','herald'}};
     9     10   	--srvcfg = {title = 'server configuration policies', priv = 'config'};
    10     11   	--discipline = {title = 'disciplinary measures', priv = 'discipline'};
    11     12   	--backends = {title = 'storage backends', priv = 'config'};
    12     13   		--pgsql = {title = 'pgsql', priv = 'config', parent = 'backends'};
    13     14   }
    14     15   
    15     16   local util = dofile 'common.lua'
    16     17   local ingest = function(filename)
    17         -	return (util.exec { 'cmark', '--smart', '--unsafe', (path..'/'..filename) }):gsub('\n','')
           18  +	return (util.exec { 'cmark', '--smart', '--unsafe', (path..'/'..filename) })
           19  +		--:gsub('\n','')
    18     20   end
    19     21   
    20     22   local doc = {}
    21     23   for n,meta in pairs(sources) do doc[n] = {
    22     24   	name = n;
    23     25   	text = ingest(n .. '.md');
    24     26   	meta = meta;
    25     27   } end
    26     28   return doc

Modified mgtool.t from [600c3c1067] to [4fc07a70c1].

   291    291   				dlg:tx_enter()
   292    292   				if dlg:dbsetup() then
   293    293   					srv:conprep(lib.store.prepmode.conf)
   294    294   
   295    295   					do var newkp = lib.crypt.genkp()
   296    296   					 -- generate server privkey
   297    297   						var kbuf: uint8[lib.crypt.const.maxdersz]
   298         -						var privsz = lib.crypt.der(false,&newkp, kbuf)
   299         -						dlg:server_setup_self(dbmode.arglist(1), [lib.mem.ptr(uint8)] {
   300         -							ptr = &kbuf[0], ct = privsz
   301         -						})
          298  +						var derkey = lib.crypt.der(false,&newkp, kbuf)
          299  +						dlg:server_setup_self(dbmode.arglist(1), derkey)
   302    300   					end
   303    301   
   304    302   					dlg:conf_set('instance-name', dbmode.arglist(1))
   305    303   					dlg:conf_set('domain', dbmode.arglist(1))
   306    304   					do var sec: int8[65] gensec(&sec[0])
   307    305   						dlg:conf_set('server-secret', &sec[0])
   308    306   					end

Modified parsav.md from [af14d66fc5] to [23851a52a8].

   102    102   * ☐ pw-pbkdf2-hmac-sha{…}: a password hashed with the Password-Based Key Derivation Function 2 instead of plain SHA2
   103    103   * ☐ pw-extern-ldap: try to authenticate by binding against an LDAP server
   104    104   * ☐ pw-extern-cyrus: try to authenticate against saslauthd
   105    105   * ☐ pw-extern-dovecot: try to authenticate against a dovecot SASL socket
   106    106   * ☐ pw-extern-krb5: abuse MIT kerberos as a password verifier
   107    107   * ☐ pw-extern-imap: abuse an email server as a password verifier
   108    108   * (extra credit) ☐ pw-extern-radius: verify a user against a radius server
          109  +* ☐ 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
          110  +* ☐ http-gssapi: log in with a kerberos principle through the http-authenticate "negotiate" mechanism. do any browsers actually support this??
          111  +* ☐ 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.
          112  +* ☐ 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.
   109    113   * ☐ 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`.
          114  +* ☐ 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-*`!
   110    115   * ☐ 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
   111    116   * ☐ tls-cert-fp: a fingerprint of a client certificate
   112    117   * ☐ 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
   113         -* ☐ 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.
   114         -* ☐ 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.
   115         -* ☐ 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.
          118  +* ☐ 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.
          119  +* ☐ challenge-ecc a Curve25519 public key. the user is presented with a challenge and must sign it with a supported hash algorithm
          120  +* ☐ 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.
   116    121   * ☑ trust: authentication always succeeds (or fails, if blacklisted). only use in combination with netmask!!!
          122  +
          123  +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.
   117    124   
   118    125   ## legal
   119    126   
   120    127   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.
   121    128   
   122    129   ## code of conduct
   123    130   

Modified parsav.t from [04e1a3fbb9] to [392d24dbd5].

   310    310   			self._store[i/8] = self._store[i/8] and not (1 << (i % 8))
   311    311   		end
   312    312   	end
   313    313   	set.bits = {}
   314    314   	set.idvmap = {}
   315    315   	for i,v in ipairs(tbl) do
   316    316   		set.idvmap[v] = i
   317         -		set.bits[v] = quote var b: set b:clear() b:setbit(i, true) in b end
          317  +		set.bits[v] = quote var b: set b:clear() b:setbit([i-1], true) in b end
   318    318   	end
   319    319   	set.metamethods.__add = macro(function(self,other)
   320    320   		local new = symbol(set)
   321    321   		local q = quote var [new] new:clear() end
   322    322   		for i = 0, bytes - 1 do
   323    323   			q = quote [q]
   324    324   				new._store[i] = self._store[i] or other._store[i]

Modified render/conf/sec.t from [157639932a] to [3bc273639f].

    35     35   					end
    36     36   				end
    37     37   				credmgr.credlist = cl:finalize()
    38     38   			end
    39     39   			credmgr:append(&a)
    40     40   			--if credmgr.credlist.ct > 0 then credmgr.credlist:free() end
    41     41   		else
           42  +			var time = lib.osclock.time(nil)
           43  +			var timestr: int8[26] lib.osclock.ctime_r(&time, &timestr[0])
           44  +			var cmt = co:stra(48)
           45  +			cmt:lpush('enrolled over http on '):push(&timestr[0],0)
    42     46   			if new:cmp('pw') then
    43     47   				var d: data.view.conf_sec_pwnew
    44         -				var time = lib.osclock.time(nil)
    45         -				var timestr: int8[26] lib.osclock.ctime_r(&time, &timestr[0])
    46         -				var cmt = co:stra(48)
    47         -				cmt:lpush('enrolled over http on '):push(&timestr[0],0)
    48     48   				d.comment = cmt:finalize()
    49     49   
    50     50   				var st = d:poolstr(&co.srv.pool)
    51     51   				--d.comment:free()
    52     52   				return st
    53         -			elseif new:cmp('challenge') then
           53  +			elseif new:cmp('rsa') then
           54  +				var c = co:stra(64)
           55  +				lib.crypt.cryptogram(&c, 8)
           56  +				var cptr = c:finalize();
           57  +				var hmac = lib.crypt.hmacp(&co.srv.pool, lib.crypt.alg.sha256, co.srv.cfg.secret:blob(), cptr); -- TODO should expire after 10min
           58  +				var hmacte: int8[lib.math.shorthand.maxlen]
           59  +				var hmacte_len = lib.math.shorthand.gen(lib.math.truncate64(hmac.ptr, hmac.ct), &hmacte[0])
           60  +				var d = data.view.conf_sec_keynew {
           61  +					comment = cmt:finalize();
           62  +					nonce = cptr;
           63  +					noncevld = pstr { ptr = &hmacte[0], ct = hmacte_len };
           64  +				}
           65  +
           66  +				return d:poolstr(&co.srv.pool)
    54     67   			-- we're going to break the rules a bit and do database munging from
    55     68   			-- the rendering code, because doing otherwise in this case would be
    56     69   			-- genuinely nightmarish
    57     70   			elseif new:cmp('otp') then
    58     71   			elseif new:cmp('api') then
    59     72   			else return pstr.null() end
    60     73   		end

Modified render/login.t from [434636bebc] to [da67d9c6fd].

     4      4   login_form(co: &lib.srv.convo, user: &lib.store.actor, creds: &lib.store.credset, msg: pstr)
     5      5   	var doc = [lib.srv.convo.page] {
     6      6   		title = 'instance logon';
     7      7   		class = 'login';
     8      8   		cache = false;
     9      9   	}
    10     10   
           11  +	var how = co:ppostv('how')
           12  +
    11     13   	if user == nil then
    12     14   		var form = data.view.login_username {
    13     15   			loginmsg = msg;
    14     16   		}
    15     17   		if form.loginmsg.ptr == nil then
    16     18   			form.loginmsg = 'identify yourself for access to this instance.'
    17     19   		end
    18     20   		doc.body = form:tostr()
    19     21   	elseif creds:sz() == 0 then
    20     22   		co:complain(403,'access denied','your host is not eligible to authenticate as this user')
    21     23   		return
    22         -	elseif creds:sz() == 1 then
           24  +	elseif creds:sz() == 1 or how:ref() then
           25  +		var newcreds: lib.store.credset
           26  +		if how:ref() then
           27  +			if     how:cmp('pw')    then newcreds = creds and [lib.store.credset.bits.pw]
           28  +			elseif how:cmp('chlg')  then newcreds = creds and [lib.store.credset.bits.challenge]
           29  +			elseif how:cmp('otp')   then newcreds = creds and [lib.store.credset.bits.otp]
           30  +			elseif how:cmp('trust') then newcreds = creds and [lib.store.credset.bits.trust]
           31  +			else co:complain(400, 'bad request', 'the requested authentication method is not available') return end
           32  +			creds = &newcreds
           33  +		end
           34  +
    23     35   		if creds.trust() then
    24     36   			-- TODO log in immediately
    25     37   			return
    26     38   		end
    27     39   
    28     40   		var ch = data.view.login_challenge {
    29     41   			handle = user.handle;
    30     42   			name = lib.coalesce(user.nym, user.handle);
    31     43   		}
    32     44   		if creds.pw() then
    33     45   			ch.challenge = 'enter the password associated with your account'
    34     46   			ch.label = 'password'
    35     47   			ch.method = 'pw'
    36         -			ch.auto = 'current-password';
           48  +			ch.inputfield = '<input type="password" autocomplete="current-password" name="response" id="response" autofocus required>';
    37     49   		elseif creds.otp() then
    38     50   			ch.challenge = 'enter a valid one-time password for your account'
    39     51   			ch.label = 'OTP code'
    40     52   			ch.method = 'otp'
    41         -			ch.auto = 'one-time-code';
           53  +			ch.inputfield = '<input type="text" autocomplete="one-time-code" name="response" id="response" autofocus required>';
    42     54   		elseif creds.challenge() then
    43         -			ch.challenge = 'sign the challenge token: <code>...</code>'
           55  +			var tok   = co:stra(128)
           56  +			var chlg  = co:stra(128)
           57  +			var input = co:stra(256)
           58  +			var time = lib.osclock.time(nil)
           59  +
           60  +			lib.crypt.cryptogram(&tok,6)
           61  +			chlg:lpush 'sign the challenge token <code style="display:block;user-select: all">'
           62  +			    :push(tok.buf,tok.sz)
           63  +				:lpush '</code>'
           64  +
           65  +			ch.challenge = chlg:finalize()
    44     66   			ch.label = 'digest'
    45     67   			ch.method = 'challenge'
    46         -			ch.auto = 'one-time-code';
           68  +			input:lpush '<textarea autocomplete="one-time-code" name="response" id="response" autofocus required></textarea><input type="hidden" name="time" value="'
           69  +			     :shpush(time)
           70  +				 :lpush '"><input type="hidden" name="token" value="'
           71  +			     :push(tok.buf,tok.sz)
           72  +			tok:shpush(time)
           73  +			var hmac = lib.crypt.hmacp(&co.srv.pool,
           74  +							lib.crypt.alg.sha256,
           75  +							co.srv.cfg.secret:blob(),
           76  +							tok:finalize())
           77  +			input:lpush '"><input type="hidden" name="vfy" value="'
           78  +				 :shpush(lib.math.truncate64(hmac.ptr, hmac.ct)) -- FIXME this is probably not very secure...
           79  +				 :lpush '">'
           80  +				 
           81  +			ch.inputfield = input:finalize()
    47     82   		else
    48         -			co:complain(500,'login failure','unknown login method')
           83  +			co:complain(400,'login failure','no usable login methods are available')
    49     84   			return
    50     85   		end
    51     86   
    52         -		doc.body = ch:tostr()
    53         -	else
    54         -		-- pick a method
           87  +		doc.body = ch:poolstr(&co.srv.pool)
           88  +	else -- pick a method
           89  +		var a = co:stra(400)
           90  +		var username = lib.html.sanitize(&co.srv.pool, pstr{user.handle,0}, true)
           91  +		a:lpush '<form action="/login" method="post" class="auth-select"><p>multiple authentication mechanisms are available. select one to continue.</p><menu><input type="hidden" name="user" value="':ppush(username):lpush'">'
           92  +		if creds.trust()     then a:lpush '<button name="how" value="trust">trust</button>'    end
           93  +		if creds.pw()        then a:lpush '<button name="how" value="pw">password</button>'    end
           94  +		if creds.otp()       then a:lpush '<button name="how" value="otp">TOTP code</button>'  end
           95  +		if creds.challenge() then a:lpush '<button name="how" value="chlg">challenge</button>' end
           96  +		a:lpush '</menu></form>'
           97  +		doc.body = a:finalize()
    55     98   	end
    56     99   
    57    100   	co:stdpage(doc)
    58         -	doc.body:free()
          101  +	--doc.body:free()
    59    102   end
    60    103   
    61    104   return login_form

Modified route.t from [6caa1366ba] to [cd7a14ae6e].

   140    140   			-- pick an auth method
   141    141   			lib.render.login(co, act.ptr, &cs, pstring.null())
   142    142   		else var aid: uint64 = 0
   143    143   			lib.dbg('authentication attempt beginning')
   144    144   			-- attempt login with provided method
   145    145   			if lib.str.ncmp('pw', am, lib.math.biggest(2,aml)) == 0 and chrs ~= nil then
   146    146   				aid = co.srv:actor_auth_pw(co.peer,
   147         -					[lib.mem.ptr(int8)]{ptr=usn,ct=usnl},
   148         -					[lib.mem.ptr(int8)]{ptr=chrs,ct=chrsl})
   149         -			elseif lib.str.ncmp('otp', am, lib.math.biggest(2,aml)) == 0 and chrs ~= nil then
          147  +					pstring {ptr=usn,ct=usnl},
          148  +					pstring {ptr=chrs,ct=chrsl})
          149  +			elseif lib.str.ncmp('challenge', am, lib.math.biggest(9,aml)) == 0 and chrs ~= nil then
          150  +				lib.dbg('challenge attempt beginning')
          151  +				var s_time = co:ppostv('time')
          152  +				var s_vfy = co:ppostv('vfy')
          153  +				var token = co:ppostv('token')
          154  +				if s_time:ref() and s_vfy:ref() and token:ref() then 
          155  +					lib.dbg('checking hmac validity')
          156  +					var vftok = co:stra(128) vftok:ppush(token):ppush(s_time)
          157  +					var hmac = lib.crypt.hmacp(&co.srv.pool, lib.crypt.alg.sha256, co.srv.cfg.secret:blob(), vftok:finalize())
          158  +					var vfy, vfyok = lib.math.shorthand.parse(s_vfy.ptr, s_vfy.ct)
          159  +					if vfyok and lib.math.truncate64(hmac.ptr,hmac.ct) == vfy then
          160  +						lib.dbg('checking expiration time')
          161  +						var time, timeok = lib.math.shorthand.parse(s_time.ptr, s_time.ct)
          162  +						if timeok and lib.osclock.time(nil) - time < [2 * 60] then -- two minutes
          163  +							lib.dbg('decoding base64')
          164  +							var bin = co.srv.pool:alloc(uint8, chrsl)
          165  +							var binlen: intptr
          166  +							if lib.b64.mbedtls_base64_decode(bin.ptr, bin.ct, &binlen, [&uint8](chrs), chrsl) == 0 then
          167  +								lib.dbg('running signature <',{chrs,chrsl},'> against challenge keys for token [', {token.ptr,token.ct}, ']')
          168  +								aid = co.srv:actor_auth_challenge(co.peer,
          169  +									pstring {usn,usnl}, binblob{bin.ptr,binlen}, token)
          170  +							end
          171  +						end
          172  +					end
          173  +				end
          174  +			elseif lib.str.ncmp('otp', am, lib.math.biggest(3,aml)) == 0 and chrs ~= nil then
   150    175   				lib.dbg('using otp auth')
   151    176   				-- ··· --
   152    177   			else lib.dbg('invalid auth method') end
   153    178   
   154    179   			-- error out
   155    180   			if aid == 0 then
   156    181   				lib.render.login(co, nil, nil,  'authentication failure')
................................................................................
   339    364   		co.who.source:auth_sigtime_user_alter(uid, lib.osclock.time(nil))
   340    365   		-- the current session has been invalidated as well, so we need to immediately install a new authentication cookie with the same aid so the user doesn't need to log back in all over again
   341    366   		co:installkey('?',co.aid)
   342    367   		return
   343    368   	elseif act:cmp( 'newcred') then
   344    369   		var cmt = co:ppostv('comment')
   345    370   		var pw = co:ppostv('newpw')
          371  +		var rsapub = co:ppostv('newrsa'):blob()
   346    372   		var aid: uint64 = 0
   347    373   		if pw:ref() then
   348    374   			var cpw = co:ppostv('rptpw')
   349    375   			if not pw:cmp(cpw) then
   350    376   				co:complain(400,'enrollment failure','the passwords you supplied do not match')
   351    377   				return
   352    378   			end
   353    379   			aid = co.srv:auth_attach_pw(uid, false, pw, cmt)
   354         -		else
   355         -			var key = co:ppostv('newkey')
   356         -			if key:ref() then
          380  +		elseif rsapub:ref() then
          381  +			var sig = co:ppostv('sig')
          382  +			var nonce = co:ppostv('nonce')
          383  +			var s_noncevld = co:ppostv('noncevld')
          384  +			var noncevld, ok = lib.math.shorthand.parse(s_noncevld.ptr, s_noncevld.ct)
          385  +			if not ok then
          386  +				co:complain(403,'try harder next time','you call that cryptanalysis?')
          387  +				return
          388  +			end
   357    389   
          390  +			var fr = co.srv.pool:frame()
          391  +			var hmac = lib.crypt.hmacp(&co.srv.pool, lib.crypt.alg.sha256, co.srv.cfg.secret:blob(), nonce)
          392  +			if not lib.math.truncate64(hmac.ptr, hmac.ct) == noncevld then
          393  +				co:complain(403,'nice try','what exactly are you trying to accomplish here, buddy')
          394  +				return
          395  +			end
          396  +
          397  +			var pkres = lib.crypt.loadpub(rsapub.ptr,rsapub.ct+1) -- needs NUL
          398  +			if not pkres.ok then
          399  +				co:complain(400,'invalid key','the key you have supplied is not a valid PEM or DER file')
          400  +				return
          401  +			end
          402  +			var pk = pkres.val
          403  +			defer pk:free()
          404  +
          405  +			var decoded = co.srv.pool:alloc(uint8,sig.ct)
          406  +			var decoded_sz: intptr = 0
          407  +			if lib.b64.mbedtls_base64_decode(decoded.ptr,sig.ct,&decoded_sz,[&uint8](sig.ptr),sig.ct) ~= 0 then
          408  +				co:complain(400,'invalid signature','the signature you supplied is not encoded in valid base64')
          409  +				return
          410  +			end
          411  +
          412  +			var vfy, secl = lib.crypt.verify(&pk, nonce.ptr, nonce.ct, decoded.ptr, decoded_sz)
          413  +			if not vfy then
          414  +				co:complain(403,'verification failed','the signature you supplied does not match the required nonce')
          415  +				return
   358    416   			end
          417  +
          418  +			var dbuf: uint8[lib.crypt.const.maxdersz]
          419  +			var derkey = lib.crypt.der(true, &pk, &dbuf[0])
          420  +			aid = co.srv:auth_attach_rsa(co.who.id, false, derkey, cmt)
          421  +			co.srv.pool:reset(fr)
   359    422   		end
   360    423   		if aid ~= 0 then
   361    424   			lib.dbg('setting credential restrictions')
   362    425   			var privs = [(function()
   363    426   				local check = quote end
   364    427   				local me = symbol(lib.store.privset)
   365    428   				for i,v in ipairs(lib.store.privset.members) do

Modified srv.t from [2ff93305a1] to [826b7b2edb].

   818    818   			cs = cs + set
   819    819   			ok = iok
   820    820   		end
   821    821   	end
   822    822   	return cs, ok
   823    823   end
   824    824   
   825         -terra srv:actor_auth_pw(ip: lib.store.inet, user: pstring, pw: pstring): uint64
   826         -	for i=0,self.sources.ct do
   827         -		if self.sources(i).backend ~= nil and
   828         -		   self.sources(i).backend.actor_auth_pw ~= nil then
   829         -			var aid,uid,newhnd = self.sources(i):actor_auth_pw(ip,user,pw)
   830         -			if aid ~= 0 then
   831         -				if uid == 0 then
   832         -					lib.dbg('new user just logged in, creating account entry')
   833         -					var kbuf: uint8[lib.crypt.const.maxdersz]
   834         -					var na = lib.store.actor.mk(&kbuf[0])
   835         -					na.handle = newhnd.ptr
   836         -					var newuid: uint64
   837         -					if self.sources(i).backend.actor_create ~= nil then
   838         -						newuid = self.sources(i):actor_create(&na)
   839         -					else newuid = self:actor_create(&na) end
          825  +local function mk_auth_fn(suffix,...)
          826  +	local syms = {...}
          827  +	local name = 'actor_auth_' .. suffix
          828  +	srv.methods[name] = terra(self: &srv, ip: lib.store.inet, user: pstring, [syms]): uint64
          829  +		for i=0,self.sources.ct do
          830  +			if self.sources(i).backend ~= nil and
          831  +			   self.sources(i).backend.[name] ~= nil then
          832  +				var aid,uid,newhnd = self.sources(i):[name](ip,user, [syms])
          833  +				if aid ~= 0 then
          834  +					if uid == 0 then
          835  +						lib.dbg('new user just logged in, creating account entry')
          836  +						var kbuf: uint8[lib.crypt.const.maxdersz]
          837  +						var na = lib.store.actor.mk(&kbuf[0])
          838  +						na.handle = newhnd.ptr
          839  +						var newuid: uint64
          840  +						if self.sources(i).backend.actor_create ~= nil then
          841  +							newuid = self.sources(i):actor_create(&na)
          842  +						else newuid = self:actor_create(&na) end
   840    843   
   841         -					if self.sources(i).backend.actor_auth_register_uid ~= nil then
   842         -						self.sources(i):actor_auth_register_uid(aid,newuid)
          844  +						if self.sources(i).backend.actor_auth_register_uid ~= nil then
          845  +							self.sources(i):actor_auth_register_uid(aid,newuid)
          846  +						end
   843    847   					end
          848  +					return aid
   844    849   				end
   845         -				return aid
   846    850   			end
   847    851   		end
          852  +
          853  +		return 0
   848    854   	end
   849         -
   850         -	return 0
          855  +	srv.methods[name].name = name
   851    856   end
          857  +
          858  +mk_auth_fn('pw',        symbol(pstring))
          859  +mk_auth_fn('challenge', symbol(lib.mem.ptr(uint8)), symbol(pstring))
          860  +
          861  +--terra srv:actor_auth_pw(ip: lib.store.inet, user: pstring, pw: pstring): uint64
          862  +--	for i=0,self.sources.ct do
          863  +--		if self.sources(i).backend ~= nil and
          864  +--		   self.sources(i).backend.actor_auth_pw ~= nil then
          865  +--			var aid,uid,newhnd = self.sources(i):actor_auth_pw(ip,user,pw)
          866  +--			if aid ~= 0 then
          867  +--				if uid == 0 then
          868  +--					lib.dbg('new user just logged in, creating account entry')
          869  +--					var kbuf: uint8[lib.crypt.const.maxdersz]
          870  +--					var na = lib.store.actor.mk(&kbuf[0])
          871  +--					na.handle = newhnd.ptr
          872  +--					var newuid: uint64
          873  +--					if self.sources(i).backend.actor_create ~= nil then
          874  +--						newuid = self.sources(i):actor_create(&na)
          875  +--					else newuid = self:actor_create(&na) end
          876  +--
          877  +--					if self.sources(i).backend.actor_auth_register_uid ~= nil then
          878  +--						self.sources(i):actor_auth_register_uid(aid,newuid)
          879  +--					end
          880  +--				end
          881  +--				return aid
          882  +--			end
          883  +--		end
          884  +--	end
          885  +--
          886  +--	return 0
          887  +--end
   852    888   
   853    889   terra cfgcache.methods.load :: {&cfgcache} -> {}
   854    890   terra cfgcache:init(o: &srv)
   855    891   	self.overlord = o
   856    892   	self:load()
   857    893   end
   858    894   

Modified static/style.scss from [e23a7affc6] to [048cf30682].

   379    379   	margin:auto;
   380    380   	padding: 0.5in;
   381    381   	text-align: center;
   382    382   	menu:first-of-type { margin-top: 0.3in; }
   383    383   	img.icon { width: 1.875in; height: 1.875in; }
   384    384   }
   385    385   
   386         -div.login {
   387         -	@extend %box;
   388         -	width: 4in;
   389         -	padding: 0.4in;
   390         -	> .msg {
   391         -		text-align: center;
   392         -		padding: 0.3in;
   393         -	}
   394         -	> .msg:first-child { padding-top: 0; }
   395         -	> .user {
   396         -		width: max-content; margin: auto;
   397         -		background: tone(-20%,-0.3);
   398         -		border: 1px solid black;
   399         -		color: tone(-50%);
   400         -		padding: 0.1in;
   401         -		> img { display: block; width: 1in; height: 1in; margin: auto; border: 1px solid black; }
   402         -		> .name { @extend %serif; text-align: center; font-size: 130%; font-weight: bold; margin-top: 0.08in; }
   403         -	}
   404         -	>form {
   405         -		display: grid;
   406         -		grid-template-columns: 1fr 1fr;
   407         -		grid-template-rows: 1.2em 1fr 1fr;
   408         -		grid-gap: 5px;
   409         -		> label, input, button { display: block; }
   410         -		> label { grid-column: 1 / 3; grid-row: 1/2; font-weight: bold }
   411         -		> input { grid-column: 1 / 3; grid-row: 2/3; }
   412         -		> button { grid-column: 2 / 3; grid-row: 3/4; }
   413         -		> a { @extend %button; grid-column: 1 / 2; grid-row: 3/4; }
          386  +body.login {
          387  +	form.auth-select {
          388  +		@extend %box;
          389  +		width: 3in;
          390  +		padding: 0.4in;
          391  +		p { text-align: center; }
          392  +		menu {
          393  +			%button {
          394  +				display: block;
          395  +				width: 100%;
          396  +				& + %button { border-top: none; }
          397  +			}
          398  +		}
          399  +	}
          400  +	div.login {
          401  +		@extend %box;
          402  +		width: 4in;
          403  +		padding: 0.4in;
          404  +		> .msg {
          405  +			text-align: center;
          406  +			padding: 0.3in;
          407  +		}
          408  +		> .msg:first-child { padding-top: 0; }
          409  +		> .user {
          410  +			width: max-content; margin: auto;
          411  +			background: tone(-20%,-0.3);
          412  +			border: 1px solid black;
          413  +			color: tone(-50%);
          414  +			padding: 0.1in;
          415  +			> img { display: block; width: 1in; height: 1in; margin: auto; border: 1px solid black; }
          416  +			> .name { @extend %serif; text-align: center; font-size: 130%; font-weight: bold; margin-top: 0.08in; }
          417  +		}
          418  +		>form {
          419  +			display: grid;
          420  +			grid-template-columns: 1fr 1fr;
          421  +			grid-template-rows: 1.2em max-content max-content;
          422  +			grid-gap: 5px;
          423  +			> label, input, button { display: block; }
          424  +			> label { grid-column: 1 / 3; grid-row: 1/2; font-weight: bold }
          425  +			> input, textarea  { grid-column: 1 / 3; grid-row: 2/3; }
          426  +			> button { grid-column: 2 / 3; grid-row: 3/4; }
          427  +			> a { @extend %button; grid-column: 1 / 2; grid-row: 3/4; }
          428  +		}
   414    429   	}
   415    430   }
   416    431   
   417    432   form.compose {
   418    433   	@extend %box;
   419    434   	display: grid;
   420    435   	grid-template-columns: 1.1in 2fr min-content 1fr 1.5fr;
................................................................................
   480    495   		position: absolute;
   481    496   		top: -0.3in;
   482    497   		right: 0.1in;
   483    498   		margin: 0.1in;
   484    499   		padding: 0.1in;
   485    500   		&:hover { font-weight: bold; }
   486    501   	}
          502  +	p { text-align: justify; }
   487    503   }
   488    504   
   489    505   code {
   490    506   	@extend %teletype;
   491    507   	background: tone(-55%);
   492         -	border: 1px inset tone(-20%);
          508  +	// border: 1px inset tone(-20%);
   493    509   	padding: 2px 6px;
   494    510   	font-size: 1.5ex !important;
   495    511   	letter-spacing: 1.3px;
   496    512   	padding-bottom: 3px;
   497         -	border-radius: 2px;
          513  +	border-radius: 4px;
   498    514   	vertical-align: baseline;
   499    515   	box-shadow: 1px 1px 1px black;
   500    516   }
   501    517   
   502         -pre { @extend %teletype; white-space: pre-wrap; }
          518  +pre {
          519  +	@extend %teletype;
          520  +	white-space: pre-wrap;
          521  +	> code:only-child {
          522  +		display: block;
          523  +		padding: 0.1in;
          524  +	}
          525  +}
   503    526   
   504    527   div.thread {
   505    528   	margin-left: 0.3in;
   506    529   	& + article.post { margin-top: 0.3in; }
   507    530   }
   508    531   
   509    532   a[href].username {
................................................................................
   697    720   		> label,summary { display:block; font-weight: bold; padding: 0.03in 0; }
   698    721   		> .txtbox {
   699    722   			@extend %serif;
   700    723   			box-sizing: border-box;
   701    724   			padding: 0.08in 0.1in;
   702    725   			border: 1px solid black;
   703    726   			background: tone(-55%);
          727  +			user-select: all;
   704    728   		}
   705    729   		> input, textarea, .txtbox {
   706    730   			display: block;
   707    731   			width: 100%;
   708    732   		}
   709    733   		> textarea { resize: vertical; min-height: 2in; }
   710    734   	}

Modified store.t from [9b6251fcb3] to [53eb63c414].

   162    162   	end
   163    163   	-- TODO validate fully
   164    164   	return true
   165    165   end
   166    166   
   167    167   terra m.actor.methods.mk(kbuf: &uint8)
   168    168   	var newkp = lib.crypt.genkp()
   169         -	var privsz = lib.crypt.der(false,&newkp,kbuf)
          169  +	var derkey = lib.crypt.der(false,&newkp,kbuf)
   170    170   	return m.actor {
   171    171   		id = 0; nym = nil; handle = nil;
   172    172   		origin = 0; bio = nil; avatar = nil;
   173    173   		knownsince = lib.osclock.time(nil);
   174    174   		rights = m.rights_default();
   175    175   		avatarid = 0;
   176         -		epithet = nil, key = [lib.mem.ptr(uint8)] {
   177         -			ptr = &kbuf[0], ct = privsz
   178         -		};
          176  +		epithet = nil, key = derkey;
   179    177   	}
   180    178   end
   181    179   
   182    180   struct m.actor_stats {
   183    181   	posts: intptr
   184    182   	follows: intptr
   185    183   	followers: intptr
................................................................................
   384    382   			-> {uint64, uint64, pstr}
   385    383   	actor_auth_pw: {&m.source, m.inet, lib.mem.ptr(int8), lib.mem.ptr(int8) }
   386    384   			-> {uint64, uint64, pstr}
   387    385   		-- handles password-based logins against hashed passwords
   388    386   			-- origin: inet
   389    387   			-- handle: rawstring
   390    388   			-- token:  rawstring
          389  +	actor_auth_challenge: {&m.source, m.inet, pstr, lib.mem.ptr(uint8), pstr }
          390  +			-> {uint64, uint64, pstr}
          391  +			-- origin:          inet
          392  +			-- handle:          rawstring
          393  +			-- response:        rawstring
          394  +			-- challenge token: pstring
   391    395   	actor_auth_tls:    {&m.source, m.inet, rawstring}
   392    396   			-> {uint64, uint64, pstr}
   393    397   		-- handles implicit authentication performed as part of an TLS connection
   394    398   			-- origin: inet
   395    399   			-- fingerprint: rawstring
   396    400   	actor_auth_api:    {&m.source, m.inet, rawstring, rawstring} -> uint64
   397    401   			-> {uint64, uint64, pstr}
................................................................................
   416    420   	actor_rel_create: {&m.source, uint16, uint64, uint64} -> {}
   417    421   	actor_rel_destroy: {&m.source, uint16, uint64, uint64} -> {}
   418    422   	actor_rel_calc: {&m.source, uint64, uint64} -> m.relationship
   419    423   
   420    424   	auth_enum_uid:    {&m.source, uint64}    -> lib.mem.lstptr(m.auth)
   421    425   	auth_enum_handle: {&m.source, rawstring} -> lib.mem.lstptr(m.auth)
   422    426   	auth_attach_pw:  {&m.source, uint64, bool, pstr, pstr} -> uint64
   423         -	auth_attach_key: {&m.source, uint64, bool, pstr, pstr} -> {}
          427  +	auth_attach_rsa: {&m.source, uint64, bool, lib.mem.ptr(uint8), pstr} -> uint64
   424    428   		-- uid: uint64
   425    429   		-- reset: bool (delete other passwords?)
   426    430   		-- pw: pstring
   427    431   		-- comment: pstring
   428    432   	auth_privs_set: {&m.source, uint64, m.privset} -> {}
   429    433   	auth_purge_pw: {&m.source, uint64, rawstring} -> {}
   430    434   	auth_purge_otp: {&m.source, uint64, rawstring} -> {}

Modified str.t from [c25d641c64] to [2798df18ea].

    58     58   			end
    59     59   			return true
    60     60   		end
    61     61   		terra ty:ffw()
    62     62   			var newp = m.ffw(self.ptr,self.ct)
    63     63   			var newct = self.ct - (newp - self.ptr)
    64     64   			return ty { ptr = newp, ct = newct }
           65  +		end
           66  +		terra ty:blob()
           67  +			return byteptr {
           68  +				ptr = [&uint8](self.ptr);
           69  +				ct = self.ct;
           70  +			}
    65     71   		end
    66     72   	end
    67     73   	install_funcs(strptr)
    68     74   	install_funcs(strref)
    69     75   
    70     76   	--strptr.methods.cmpl = macro(function(self,other)
    71     77   	--	return `self:cmp(strptr { ptr = [other:asvalue()], ct = [#(other:asvalue())] })

Modified view/conf-sec-credmg.tpl from [c30c9309b5] to [a2babc26c2].

     3      3   	<p>this account can currently be accessed with the credentials listed below. if you fear a credential has been compromised, you can revoke or reset it.</p>
     4      4   	<select size="6" name="cred">
     5      5   		@credlist
     6      6   	</select>
     7      7   	<menu class="horizontal choice">
     8      8   		<button name="act" value="reset">reset</button>
     9      9   		<button name="act" value="revoke">revoke</button>
           10  +		@?auth
    10     11   	</menu>
    11     12   </form>
    12     13   <hr>
    13     14   <form method="get">
    14     15   	<p>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 &mdash; 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.</p>
    15     16   	<div class="check-panel">
    16     17   		<label><input type="checkbox" name="allow-post"> post</label>
................................................................................
    25     26   	<p>you can also specify an IP address range in CIDR format to associate with this credential. if you do so, this credential will only be usable when connecting from an IP address in that range. otherwise, it will be valid when connecting from anywhere on the internet.</p>
    26     27   	<div class="elem">
    27     28   		<label for="netmask">netmask</label>
    28     29   		<input type="text" name="netmask" id="netmask" placeholder="10.0.0.0/8">
    29     30   	</div>
    30     31   	<menu class="vertical choice">
    31     32   		<button name="new" value="pw">new password</button>
           33  +		<button name="new" value="rsa">new RSA key</button>
    32     34   		<button name="new" value="otp">new OTP key</button>
    33     35   		<button name="new" value="api">new API token</button>
    34         -		<button name="new" value="challenge">new challenge key</button>
    35     36   	</menu>
    36     37   </form>

Added view/conf-sec-keynew.tpl version [039cf710c4].

            1  +<form method="post">
            2  +	<div class="elem">
            3  +		<label for="comment">comment</label>
            4  +		<input type="text" id="comment" name="comment" value="@comment" required>
            5  +	</div>
            6  +	<div class="elem">
            7  +		<label for="newkey">public key in PEM format</label>
            8  +		<textarea id="newkey" name="newrsa" required></textarea>
            9  +	</div>
           10  +	<p>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.</p>
           11  +	<code style="display:block; user-select: all">echo -n @nonce | openssl dgst -sha256 -sign privkey.pem | openssl base64</code>
           12  +	<div class="elem">
           13  +		<label>nonce</label>
           14  +		<div class="txtbox">@nonce</div>
           15  +		<input type="hidden" name="nonce" value="@nonce">
           16  +		<input type="hidden" name="noncevld" value="@noncevld">
           17  +	</div>
           18  +	<div class="elem">
           19  +		<label for="sig">nonce signature</label>
           20  +		<textarea id="sig" name="sig" required></textarea>
           21  +	</div>
           22  +	<menu class="choice horizontal">
           23  +		<button name="act" value="newcred">enroll</button>
           24  +		<a class="button" href="?">cancel</a>
           25  +	</menu>
           26  +</form>

Modified view/load.lua from [15344e4760] to [3d222d2b19].

    20     20   	'login-challenge';
    21     21   
    22     22   	'conf';
    23     23   	'conf-profile';
    24     24   	'conf-sec';
    25     25   	'conf-sec-credmg';
    26     26   	'conf-sec-pwnew';
           27  +	'conf-sec-keynew';
    27     28   	'conf-user-ctl';
    28     29   }
    29     30   
    30     31   local ingest = function(filename)
    31     32   	local hnd = io.open(path..'/'..filename)
    32     33   	local txt = hnd:read('*a')
    33     34   	io.close(hnd)

Modified view/login-challenge.tpl from [84fccbb367] to [cc4592543f].

     3      3   		<img src="/avi/@handle">
     4      4   		<div class="name">@!name</div>
     5      5   	</div>
     6      6   	<div class="msg">@challenge</div>
     7      7   	<form action="/login" method="post">
     8      8   		<label for="response">@label</label>
     9      9   		<input type="hidden" name="user" value="@:handle">
    10         -		<input type="password" autocomplete="@auto" name="response" id="response" autofocus required>
           10  +		@inputfield
    11     11   		<button type="submit" name="authmethod" value="@method">authenticate</button>
    12     12   		<a href="/login">cancel</a>
    13     13   	</form>
    14     14   </div>

Modified view/login-username.tpl from [8c165f8ae9] to [c0dae1cbbc].

     1      1   <div class="login">
     2      2   	<div class="msg">@loginmsg</div>
     3      3   	<form action="/login" method="post">
     4      4   		<label for="user">local handle</label>
     5      5   		<input type="text" name="user" id="user" autocomplete="username" autofocus required>
     6         -		<button type="submit">log on</button>
            6  +		<button>log on</button>
     7      7   	</form>
     8      8   </div>