MAJOR: Updates for Authenticated Web UI and CLI #30
| @ -768,7 +768,6 @@ state.keystoreSecure = !keystore.insecure; | |||||||
| keystore.all().then(function (list) { | keystore.all().then(function (list) { | ||||||
|   var keyext = '.key.jwk.json'; |   var keyext = '.key.jwk.json'; | ||||||
|   var key; |   var key; | ||||||
|   var convert; |  | ||||||
|   // TODO create map by account and index into that map to get the master key
 |   // TODO create map by account and index into that map to get the master key
 | ||||||
|   // and sort keys in the process
 |   // and sort keys in the process
 | ||||||
|   list.some(function (el) { |   list.some(function (el) { | ||||||
| @ -778,14 +777,6 @@ keystore.all().then(function (list) { | |||||||
|       return true; |       return true; | ||||||
|     } |     } | ||||||
|   }); |   }); | ||||||
|   if (!key) { |  | ||||||
|     list.some(function (el) { |  | ||||||
|       if (el.password.kty) { |  | ||||||
|         convert = el.password; |  | ||||||
|         return true; |  | ||||||
|       } |  | ||||||
|     }); |  | ||||||
|   } |  | ||||||
| 
 | 
 | ||||||
|   if (key) { |   if (key) { | ||||||
|     state.key = key; |     state.key = key; | ||||||
| @ -796,7 +787,6 @@ keystore.all().then(function (list) { | |||||||
| 
 | 
 | ||||||
|   return keypairs.generate().then(function (pair) { |   return keypairs.generate().then(function (pair) { | ||||||
|     var jwk = pair.private; |     var jwk = pair.private; | ||||||
|     if (convert) { jwk = convert; } |  | ||||||
|     return keypairs.thumbprint({ jwk: jwk }).then(function (kid) { |     return keypairs.thumbprint({ jwk: jwk }).then(function (kid) { | ||||||
|       jwk.kid = kid; |       jwk.kid = kid; | ||||||
|       return keystore.set(kid + keyext, jwk).then(function () { |       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 pkg = require('../package.json'); | ||||||
| 
 | 
 | ||||||
|  | var crypto = require('crypto'); | ||||||
| //var url = require('url');
 | //var url = require('url');
 | ||||||
| var path = require('path'); | var path = require('path'); | ||||||
| var os = require('os'); | var os = require('os'); | ||||||
| @ -29,6 +30,7 @@ var startTime = Date.now(); | |||||||
| var connectTimes = []; | var connectTimes = []; | ||||||
| var isConnected = false; | var isConnected = false; | ||||||
| var eggspress = require('../lib/eggspress.js'); | var eggspress = require('../lib/eggspress.js'); | ||||||
|  | var keypairs = require('keypairs'); | ||||||
| 
 | 
 | ||||||
| var TelebitRemote = require('../lib/daemon/index.js').TelebitRemote; | var TelebitRemote = require('../lib/daemon/index.js').TelebitRemote; | ||||||
| 
 | 
 | ||||||
| @ -370,6 +372,50 @@ controllers.relay = function (req, res) { | |||||||
|     res.end(JSON.stringify(resp)); |     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) { | function jsonEggspress(req, res, next) { | ||||||
|   /* |   /* | ||||||
| @ -438,7 +484,7 @@ function jwtEggspress(req, res, next) { | |||||||
| } | } | ||||||
| 
 | 
 | ||||||
| function verifyJws(jwk, jws) { | 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 alg = 'SHA' + jws.header.alg.replace(/[^\d]+/i, ''); | ||||||
|     var sig = ecdsaAsn1SigToJwtSig(jws.header.alg, jws.signature); |     var sig = ecdsaAsn1SigToJwtSig(jws.header.alg, jws.signature); | ||||||
|     return require('crypto') |     return require('crypto') | ||||||
| @ -799,6 +845,16 @@ function handleApi() { | |||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|   // TODO turn strings into regexes to match beginnings
 |   // 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.use(/\b(relay)\b/, controllers.relay); | ||||||
|   app.get(/\b(config)\b/, getConfigOnly); |   app.get(/\b(config)\b/, getConfigOnly); | ||||||
|   app.use(/\b(init|config)\b/, initOrConfig); |   app.use(/\b(init|config)\b/, initOrConfig); | ||||||
| @ -1374,36 +1430,69 @@ state.net = state.net || { | |||||||
|   } |   } | ||||||
| }; | }; | ||||||
| 
 | 
 | ||||||
|  | var jwks = []; | ||||||
| var token; | var token; | ||||||
| var tokenname = "access_token.jwt"; | var tokenname = "access_token.jwt"; | ||||||
| // backwards-compatibility shim
 |  | ||||||
| try { | try { | ||||||
|  |   // backwards-compatibility shim
 | ||||||
|   var tokenpath = path.join(path.dirname(state._confpath), 'access_token.txt'); |   var tokenpath = path.join(path.dirname(state._confpath), 'access_token.txt'); | ||||||
|   token = fs.readFileSync(tokenpath, 'ascii').trim(); |   token = fs.readFileSync(tokenpath, 'ascii').trim(); | ||||||
|   keystore.set(tokenname, token).then(onKeystore).catch(function (err) { |   keystore.set(tokenname, token).then(onKeystore).catch(function (err) { | ||||||
|     console.error('keystore failure:'); |     console.error('keystore failure:'); | ||||||
|     console.error(err); |     console.error(err); | ||||||
|   }); |   }); | ||||||
| } catch(e) { | } catch(e) { onKeystore(); } | ||||||
|   onKeystore(); |  | ||||||
| } |  | ||||||
| var jwks = []; |  | ||||||
| function onKeystore() { | function onKeystore() { | ||||||
|   return keystore.all().then(function (list) { |   return keystore.all().then(function (list) { | ||||||
|  |     var keyext = '.key.jwk.json'; | ||||||
|  |     var pubext = '.pub.jwk.json'; | ||||||
|  |     var key; | ||||||
|     list.forEach(function (el) { |     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) { |       if (tokenname === el.account) { | ||||||
|         token = el.password; |         token = el.password; | ||||||
|         return; |         return; | ||||||
|       } |       } | ||||||
|       // these are secret because just adding the
 | 
 | ||||||
|       // willy-nilly to the fs can allow arbitrary tokens
 |       // find trusted public keys
 | ||||||
|       if (/\.pub\.jwk\.json$/.test(el.account)) { |       // (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
 |         // pre-parsed
 | ||||||
|         jwks.push(el.password); |         jwks.push(el.password); | ||||||
|         return; |         return; | ||||||
|       } |       } | ||||||
|  | 
 | ||||||
|  |       console.log("unrecognized password: %s", el.account); | ||||||
|     }); |     }); | ||||||
|  | 
 | ||||||
|  |     if (key) { | ||||||
|  |       state.key = key; | ||||||
|  |       state.pub = keypairs.neuter({ jwk: key }); | ||||||
|       fs.readFile(confpath, 'utf8', parseConfig); |       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 |   , 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, '/') |     .replace(/_/g, '/') | ||||||
|     .replace(/=/g, '') |     .replace(/=/g, '') | ||||||
|  | |||||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user