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 () {
|
||||
|
|
115
bin/telebitd.js
115
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);
|
||||
});
|
||||
});
|
||||
});
|
||||
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…
Reference in New Issue