MAJOR: Updates for Authenticated Web UI and CLI #30
| @ -768,7 +768,6 @@ state.keystoreSecure = !keystore.insecure; | ||||
| keystore.all().then(function (list) { | ||||
|   var keyext = '.key.jwk.json'; | ||||
|   var key; | ||||
|   var convert; | ||||
|   // TODO create map by account and index into that map to get the master key
 | ||||
|   // and sort keys in the process
 | ||||
|   list.some(function (el) { | ||||
| @ -778,14 +777,6 @@ keystore.all().then(function (list) { | ||||
|       return true; | ||||
|     } | ||||
|   }); | ||||
|   if (!key) { | ||||
|     list.some(function (el) { | ||||
|       if (el.password.kty) { | ||||
|         convert = el.password; | ||||
|         return true; | ||||
|       } | ||||
|     }); | ||||
|   } | ||||
| 
 | ||||
|   if (key) { | ||||
|     state.key = key; | ||||
| @ -796,7 +787,6 @@ keystore.all().then(function (list) { | ||||
| 
 | ||||
|   return keypairs.generate().then(function (pair) { | ||||
|     var jwk = pair.private; | ||||
|     if (convert) { jwk = convert; } | ||||
|     return keypairs.thumbprint({ jwk: jwk }).then(function (kid) { | ||||
|       jwk.kid = kid; | ||||
|       return keystore.set(kid + keyext, jwk).then(function () { | ||||
|  | ||||
							
								
								
									
										113
									
								
								bin/telebitd.js
									
									
									
									
									
								
							
							
						
						
									
										113
									
								
								bin/telebitd.js
									
									
									
									
									
								
							| @ -11,6 +11,7 @@ try { | ||||
| 
 | ||||
| var pkg = require('../package.json'); | ||||
| 
 | ||||
| var crypto = require('crypto'); | ||||
| //var url = require('url');
 | ||||
| var path = require('path'); | ||||
| var os = require('os'); | ||||
| @ -29,6 +30,7 @@ var startTime = Date.now(); | ||||
| var connectTimes = []; | ||||
| var isConnected = false; | ||||
| var eggspress = require('../lib/eggspress.js'); | ||||
| var keypairs = require('keypairs'); | ||||
| 
 | ||||
| var TelebitRemote = require('../lib/daemon/index.js').TelebitRemote; | ||||
| 
 | ||||
| @ -370,6 +372,50 @@ controllers.relay = function (req, res) { | ||||
|     res.end(JSON.stringify(resp)); | ||||
|   }); | ||||
| }; | ||||
| controllers._nonces = {}; | ||||
| controllers._requireNonce = function (req, res, next) { | ||||
| 	var nonce = req.jws && req.jws.protected && req.jws.protected.nonce; | ||||
| 	var active = (Date.now() - controllers._nonces[nonce]) < (4 * 60 * 60 * 1000); | ||||
| 	if (!active) { | ||||
| 		// TODO proper headers and error message
 | ||||
| 		res.end({ "error": "invalid or expired nonce", "error_code": "ENONCE" }); | ||||
| 		return; | ||||
| 	} | ||||
| 	delete controllers._nonces[nonce]; | ||||
| 	controllers._issueNonce(req, res); | ||||
| 	next(); | ||||
| }; | ||||
| controllers._issueNonce = function (req, res) { | ||||
|   var nonce = toUrlSafe(crypto.randomBytes(16).toString('base64')); | ||||
|   // TODO associate with a TLS session
 | ||||
|   controllers._nonces[nonce] = Date.now(); | ||||
|   res.headers.set("Replay-Nonce", nonce); | ||||
| 	return nonce; | ||||
| }; | ||||
| controllers.newNonce = function (req, res) { | ||||
|   res.statusCode = 200; | ||||
| 	res.headers.set("Cache-Control", "max-age=0, no-cache, no-store"); | ||||
| 	// TODO
 | ||||
| 	//res.headers.set("Date", "Sun, 10 Mar 2019 08:04:45 GMT");
 | ||||
| 	// is this the expiration of the nonce itself? methinks maybe so
 | ||||
| 	//res.headers.set("Expires", "Sun, 10 Mar 2019 08:04:45 GMT");
 | ||||
| 	// TODO use one of the registered domains
 | ||||
| 	//var indexUrl = "https://acme-staging-v02.api.letsencrypt.org/index"
 | ||||
|   var port = (state.config.ipc && state.config.ipc.port || state._ipc.port || undefined); | ||||
| 	var indexUrl = "http://localhost:" + port + "/index"; | ||||
| 	res.headers.set("Link", "Link: <" + indexUrl + ">;rel=\"index\""); | ||||
| 	res.headers.set("Pragma", "no-cache"); | ||||
|   //res.headers.set("Strict-Transport-Security", "max-age=604800");
 | ||||
|   res.headers.set("X-Frame-Options", "DENY"); | ||||
| 
 | ||||
|   res.end(""); | ||||
| }; | ||||
| controllers.newAccount = function (req, res) { | ||||
| 	controllers._requireNonce(req, res, function () { | ||||
| 		res.statusCode = 500; | ||||
| 		res.end("not implemented yet"); | ||||
| 	}); | ||||
| }; | ||||
| 
 | ||||
| function jsonEggspress(req, res, next) { | ||||
|   /* | ||||
| @ -438,7 +484,7 @@ function jwtEggspress(req, res, next) { | ||||
| } | ||||
| 
 | ||||
| function verifyJws(jwk, jws) { | ||||
|   return require('keypairs').export({ jwk: jwk }).then(function (pem) { | ||||
|   return keypairs.export({ jwk: jwk }).then(function (pem) { | ||||
|     var alg = 'SHA' + jws.header.alg.replace(/[^\d]+/i, ''); | ||||
|     var sig = ecdsaAsn1SigToJwtSig(jws.header.alg, jws.signature); | ||||
|     return require('crypto') | ||||
| @ -799,6 +845,16 @@ function handleApi() { | ||||
|   } | ||||
| 
 | ||||
|   // TODO turn strings into regexes to match beginnings
 | ||||
| 	app.use('/acme', function acmeCors(req, res, next) { | ||||
| 		// Taken from New-Nonce
 | ||||
| 		res.headers.set("Access-Control-Allow-Headers", "Content-Type"); | ||||
| 		res.headers.set("Access-Control-Allow-Origin", "*"); | ||||
| 		res.headers.set("Access-Control-Expose-Headers", "Link, Replay-Nonce, Location"); | ||||
| 		res.headers.set("Access-Control-Max-Age", "86400"); | ||||
| 		next(); | ||||
| 	}); | ||||
|   app.use('/acme/new-nonce', controllers.newNonce); | ||||
|   app.use('/acme/new-acct', controllers.newAccount); | ||||
|   app.use(/\b(relay)\b/, controllers.relay); | ||||
|   app.get(/\b(config)\b/, getConfigOnly); | ||||
|   app.use(/\b(init|config)\b/, initOrConfig); | ||||
| @ -1374,36 +1430,69 @@ state.net = state.net || { | ||||
|   } | ||||
| }; | ||||
| 
 | ||||
| var jwks = []; | ||||
| var token; | ||||
| var tokenname = "access_token.jwt"; | ||||
| // backwards-compatibility shim
 | ||||
| try { | ||||
|   // backwards-compatibility shim
 | ||||
|   var tokenpath = path.join(path.dirname(state._confpath), 'access_token.txt'); | ||||
|   token = fs.readFileSync(tokenpath, 'ascii').trim(); | ||||
|   keystore.set(tokenname, token).then(onKeystore).catch(function (err) { | ||||
|     console.error('keystore failure:'); | ||||
|     console.error(err); | ||||
|   }); | ||||
| } catch(e) { | ||||
|   onKeystore(); | ||||
| } | ||||
| var jwks = []; | ||||
| } catch(e) { onKeystore(); } | ||||
| function onKeystore() { | ||||
|   return keystore.all().then(function (list) { | ||||
|     var keyext = '.key.jwk.json'; | ||||
|     var pubext = '.pub.jwk.json'; | ||||
|     var key; | ||||
|     list.forEach(function (el) { | ||||
|       // find key
 | ||||
|       if (keyext === el.account.slice(-keyext.length) | ||||
|         && el.password.kty && el.password.kid) { | ||||
|         key = el.password; | ||||
|         return; | ||||
|       } | ||||
| 
 | ||||
|       // find token
 | ||||
|       if (tokenname === el.account) { | ||||
|         token = el.password; | ||||
|         return; | ||||
|       } | ||||
|       // these are secret because just adding the
 | ||||
|       // willy-nilly to the fs can allow arbitrary tokens
 | ||||
|       if (/\.pub\.jwk\.json$/.test(el.account)) { | ||||
| 
 | ||||
|       // find trusted public keys
 | ||||
|       // (if we sign these we could probably just store them to the fs,
 | ||||
|       // but we do want some way to know that they weren't just willy-nilly
 | ||||
|       // added to the fs my any old program)
 | ||||
|       if (pubext === el.account.slice(-pubext.length)) { | ||||
|         // pre-parsed
 | ||||
|         jwks.push(el.password); | ||||
|         return; | ||||
|       } | ||||
| 
 | ||||
|       console.log("unrecognized password: %s", el.account); | ||||
|     }); | ||||
| 
 | ||||
|     if (key) { | ||||
|       state.key = key; | ||||
|       state.pub = keypairs.neuter({ jwk: key }); | ||||
|       fs.readFile(confpath, 'utf8', parseConfig); | ||||
|       return; | ||||
|     } | ||||
| 
 | ||||
|     return keypairs.generate().then(function (pair) { | ||||
|       var jwk = pair.private; | ||||
|       return keypairs.thumbprint({ jwk: jwk }).then(function (kid) { | ||||
|         jwk.kid = kid; | ||||
|         return keystore.set(kid + keyext, jwk).then(function () { | ||||
|           var size = (jwk.crv || Buffer.from(jwk.n, 'base64').byteLength * 8); | ||||
|           console.info("Generated new %s %s private key with thumbprint %s", jwk.kty, size, kid); | ||||
|           state.key = jwk; | ||||
|           fs.readFile(confpath, 'utf8', parseConfig); | ||||
|         }); | ||||
|       }); | ||||
|     }); | ||||
|   }); | ||||
| } | ||||
| }()); | ||||
| @ -1437,7 +1526,11 @@ function ecdsaAsn1SigToJwtSig(alg, b64sig) { | ||||
|   , Buffer.from([0x02, s.byteLength]), s | ||||
|   ]); | ||||
| 
 | ||||
|   return buf.toString('base64') | ||||
|   return toUrlSafe(buf.toString('base64')); | ||||
| } | ||||
| 
 | ||||
| function toUrlSafe(b64) { | ||||
|   return b64 | ||||
|     .replace(/-/g, '+') | ||||
|     .replace(/_/g, '/') | ||||
|     .replace(/=/g, '') | ||||
|  | ||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user