MAJOR: Updates for Authenticated Web UI and CLI #30

Open
coolaj86 wants to merge 77 commits from next into master
2 changed files with 104 additions and 21 deletions
Showing only changes of commit 7a9cc7cb77 - Show all commits

View File

@ -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 () {

View File

@ -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, '')