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…
Reference in New Issue