[WIP] more new account

This commit is contained in:
AJ ONeal 2019-03-26 03:22:15 -06:00
parent 7f49650c48
commit ffc95b4ddf
5 changed files with 201 additions and 82 deletions

View File

@ -25,6 +25,7 @@ var camelCopy = recase.camelCopy.bind(recase);
//var snakeCopy = recase.snakeCopy.bind(recase); //var snakeCopy = recase.snakeCopy.bind(recase);
var urequest = require('@coolaj86/urequest'); var urequest = require('@coolaj86/urequest');
var urequestAsync = require('util').promisify(urequest);
var common = require('../lib/cli-common.js'); var common = require('../lib/cli-common.js');
var defaultConfPath = path.join(os.homedir(), '.config/telebit'); var defaultConfPath = path.join(os.homedir(), '.config/telebit');
@ -336,8 +337,6 @@ var RC;
function parseConfig(err, text) { function parseConfig(err, text) {
function handleConfig(err, config) { function handleConfig(err, config) {
if (err) { throw err; }
state.config = config; state.config = config;
var verstrd = [ pkg.name + ' daemon v' + state.config.version ]; var verstrd = [ pkg.name + ' daemon v' + state.config.version ];
if (state.config.version && state.config.version !== pkg.version) { if (state.config.version && state.config.version !== pkg.version) {
@ -682,7 +681,52 @@ function parseConfig(err, text) {
}); });
} }
RC.request({ service: 'config', method: 'GET' }, handleConfig); var bootState = {};
function bootstrap() {
// Create / retrieve account (sign-in, more or less)
// TODO hit directory resource /.well-known/openid-configuration -> acme_uri (?)
// Occassionally rotate the key just for the sake of testing the key rotation
return urequestAsync({ method: 'HEAD', url: RC.resolve('/acme/new-nonce') }).then(function (resp) {
var nonce = resp.headers['replay-nonce'];
var newAccountUrl = RC.resolve('/new-acct');
return keypairs.signJws({
jwk: state.key
, protected: {
// alg will be filled out automatically
jwk: state.pub
, nonce: nonce
, url: newAccountUrl
}
, payload: JSON.stringify({
// We can auto-agree here because the client is the user agent of the primary user
termsOfServiceAgreed: true
, contact: [] // I don't think we have email yet...
//, externalAccountBinding: null
})
}).then(function (jws) {
return urequestAsync({
url: newAccountUrl
, json: jws
, headers: { "Content-Type": 'application/jose+json' }
}).then(function (resp) {
console.log('resp.body:');
console.log(resp.body);
if (!resp.body || 'valid' !== resp.body.status) {
throw new Error("did not successfully create or restore account");
}
return RC.requestAsync({ service: 'config', method: 'GET' }).catch(function (err) {
console.error(err.stack);
process.exit(27);
}).then(handleConfig);
});
});
}).catch(RC.createErrorHandler(bootstrap, bootState, function (err) {
console.error(err);
process.exit(17);
}));
}
bootstrap();
} }
var parsers = { var parsers = {

View File

@ -16,6 +16,7 @@ var crypto = require('crypto');
var path = require('path'); var path = require('path');
var os = require('os'); var os = require('os');
var fs = require('fs'); var fs = require('fs');
var fsp = fs.promises;
var urequest = require('@coolaj86/urequest'); var urequest = require('@coolaj86/urequest');
var urequestAsync = require('util').promisify(urequest); var urequestAsync = require('util').promisify(urequest);
var common = require('../lib/cli-common.js'); var common = require('../lib/cli-common.js');
@ -110,8 +111,20 @@ function getServername(servernames, sub) {
})[0]; })[0];
} }
/*global Promise*/
var _savingConfig = Promise.resolve();
function saveConfig(cb) { function saveConfig(cb) {
fs.writeFile(confpath, YAML.safeDump(snakeCopy(state.config)), cb); // simple sequencing chain so that write corruption is not possible
_savingConfig = _savingConfig.then(function () {
return fsp.writeFile(confpath, YAML.safeDump(snakeCopy(state.config))).then(function () {
try {
cb();
} catch(e) {
console.error(e.stack);
process.exit(47);
}
}).catch(cb);
});
} }
var controllers = {}; var controllers = {};
controllers.http = function (req, res) { controllers.http = function (req, res) {
@ -395,7 +408,8 @@ controllers.newNonce = function (req, res) {
//var indexUrl = "https://acme-staging-v02.api.letsencrypt.org/index" //var indexUrl = "https://acme-staging-v02.api.letsencrypt.org/index"
var port = (state.config.ipc && state.config.ipc.port || state._ipc.port || undefined); var port = (state.config.ipc && state.config.ipc.port || state._ipc.port || undefined);
var indexUrl = "http://localhost:" + port + "/index"; var indexUrl = "http://localhost:" + port + "/index";
res.headers.set("Link", "Link: <" + indexUrl + ">;rel=\"index\""); res.headers.set("Link", "<" + indexUrl + ">;rel=\"index\"");
res.headers.set("Cache-Control", "max-age=0, no-cache, no-store");
res.headers.set("Pragma", "no-cache"); res.headers.set("Pragma", "no-cache");
//res.headers.set("Strict-Transport-Security", "max-age=604800"); //res.headers.set("Strict-Transport-Security", "max-age=604800");
res.headers.set("X-Frame-Options", "DENY"); res.headers.set("X-Frame-Options", "DENY");
@ -418,11 +432,13 @@ controllers.newAccount = function (req, res) {
// req.body.contact: [ 'mailto:email' ] // req.body.contact: [ 'mailto:email' ]
res.statusCode = 422; res.statusCode = 422;
res.send({ error: { message: "jws signed payload should contain a valid mailto:email in the contact array" } }); res.send({ error: { message: "jws signed payload should contain a valid mailto:email in the contact array" } });
return;
} }
if (!req.body.termsOfServiceAgreed) { if (!req.body.termsOfServiceAgreed) {
// req.body.termsOfServiceAgreed: true // req.body.termsOfServiceAgreed: true
res.statusCode = 422; res.statusCode = 422;
res.send({ error: { message: "jws signed payload should have termsOfServiceAgreed: true" } }); res.send({ error: { message: "jws signed payload should have termsOfServiceAgreed: true" } });
return;
} }
// We verify here regardless of whether or not it was verified before, // We verify here regardless of whether or not it was verified before,
@ -435,37 +451,64 @@ controllers.newAccount = function (req, res) {
return; return;
} }
var jwk = req.jws.header.jwk;
return keypairs.thumbprint({ jwk: jwk }).then(function (thumb) {
// Note: we can get any number of account requests // Note: we can get any number of account requests
// and these need to be stored for some space of time // and these need to be stored for some space of time
// to await verification. // to await verification.
// we'll have to expire them somehow and prevent DoS // we'll have to expire them somehow and prevent DoS
// check if this account already exists // check if this account already exists
DB.accounts.some(function (/*jwk*/) { var account;
// calculate thumbprint from jwk DB.accounts.some(function (acc) {
// TODO calculate thumbprint from jwk
// find a key with matching jwk // find a key with matching jwk
if (acc.thumb === thumb) {
account = acc;
return true;
}
// TODO ACME requires kid to be the account URL (STUPID!!!)
// rather than the key id (as decided by the key issuer)
// not sure if it's necessary to handle it that way though
}); });
// TODO fail if onlyReturnExisting is not false
// req.body.onlyReturnExisting: false
res.statusCode = 500; var myBaseUrl = (req.connection.encrypted ? 'https' : 'http') + '://' + req.headers.host;
res.send({ if (!account) {
error: { message: "not implemented" }, // fail if onlyReturnExisting is not false
"id": 0, // 5937234, if (req.body.onlyReturnExisting) {
"key": req.jws.header.jwk, // TODO trim to basics res.statusCode = 422;
/*{ res.send({ error: { message: "onlyReturnExisting is set, so there's nothing to do" } });
"kty": "EC", return;
"crv": "P-256", }
"x": "G7kuV4JiqZs-GztrzpsmUM7Raf9tDUELWt5O337sTqw", res.statusCode = 201;
"y": "n5SFz9z2i-ZF_zu5aoS9t9O8y_g2qfonXv3Cna2e39k" account = {};
},*/ account._id = crypto.randomBytes(16).toString('base64');
"contact": req.body.contact, // [ "mailto:john.doe@gmail.com" ], // TODO be better about this
account.location = myBaseUrl + '/acme/accounts/' + account._id;
account.thumb = thumb;
account.pub = jwk;
account.contact = req.body.contact;
DB.accounts.push(account);
state.config.accounts = DB.accounts;
saveConfig(function () {});
}
var result = {
status: 'valid'
, contact: account.contact // [ "mailto:john.doe@gmail.com" ],
, orders: account.location + '/orders'
// optional / off-spec
, id: account._id
, jwk: account.pub
/*
// I'm not sure if we have the real IP through telebit's network wrapper at this point // I'm not sure if we have the real IP through telebit's network wrapper at this point
// TODO we also need to set X-Forwarded-Addr as a proxy // TODO we also need to set X-Forwarded-Addr as a proxy
"initialIp": req.connection.remoteAddress, //"128.187.116.28", "initialIp": req.connection.remoteAddress, //"128.187.116.28",
"createdAt": (new Date()).toISOString(), // "2018-04-17T21:29:10.833305103Z", "createdAt": (new Date()).toISOString(), // "2018-04-17T21:29:10.833305103Z",
"status": "invalid" //"valid" */
}); };
res.setHeader('Location', account.location);
res.send(result);
/* /*
Cache-Control: max-age=0, no-cache, no-store Cache-Control: max-age=0, no-cache, no-store
Content-Type: application/json Content-Type: application/json
@ -477,6 +520,7 @@ controllers.newAccount = function (req, res) {
*/ */
}); });
}); });
});
}; };
function jsonEggspress(req, res, next) { function jsonEggspress(req, res, next) {
@ -550,7 +594,7 @@ function verifyJws(jwk, jws) {
return 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 crypto
.createVerify(alg) .createVerify(alg)
.update(jws.protected + '.' + jws.payload) .update(jws.protected + '.' + jws.payload)
.verify(pem, sig, 'base64'); .verify(pem, sig, 'base64');
@ -915,14 +959,32 @@ function handleApi() {
} }
// TODO turn strings into regexes to match beginnings // TODO turn strings into regexes to match beginnings
app.use('/.well-known/openid-configuration', function (req, res) {
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");
if ('OPTIONS' === req.method) { res.end(); return; }
res.send({
jwks_uri: 'http://localhost/.well-known/jwks.json'
, acme_uri: 'http://localhost/acme/directory'
});
});
app.use('/acme', function acmeCors(req, res, next) { app.use('/acme', function acmeCors(req, res, next) {
// Taken from New-Nonce // Taken from New-Nonce
res.headers.set("Access-Control-Allow-Headers", "Content-Type"); res.headers.set("Access-Control-Allow-Headers", "Content-Type");
res.headers.set("Access-Control-Allow-Origin", "*"); res.headers.set("Access-Control-Allow-Origin", "*");
res.headers.set("Access-Control-Expose-Headers", "Link, Replay-Nonce, Location"); res.headers.set("Access-Control-Expose-Headers", "Link, Replay-Nonce, Location");
res.headers.set("Access-Control-Max-Age", "86400"); res.headers.set("Access-Control-Max-Age", "86400");
if ('OPTIONS' === req.method) { res.end(); return; }
next(); next();
}); });
app.use('/acme/directory', function (req, res) {
res.send({
'new-nonce': '/acme/new-nonce'
, 'new-account': '/acme/new-acct'
});
});
app.use('/acme/new-nonce', controllers.newNonce); app.use('/acme/new-nonce', controllers.newNonce);
app.use('/acme/new-acct', controllers.newAccount); app.use('/acme/new-acct', controllers.newAccount);
app.use(/\b(relay)\b/, controllers.relay); app.use(/\b(relay)\b/, controllers.relay);
@ -1098,6 +1160,7 @@ function parseConfig(err, text) {
} }
state.config = camelCopy(state.config || {}) || {}; state.config = camelCopy(state.config || {}) || {};
DB.accounts = state.config.accounts || [];
run(); run();

View File

@ -72,6 +72,49 @@ module.exports.create = function (state) {
} }
var RC = {}; var RC = {};
RC.resolve = function (pathstr) {
// TODO use real hostname and return reqOpts rather than string?
return 'http://localhost:' + RC.port({}).port.toString() + '/' + pathstr.replace(/^\//, '');
};
RC.port = function (reqOpts) {
var fs = require('fs');
var portFile = path.join(path.dirname(state._ipc.path), 'telebit.port');
if (fs.existsSync(portFile)) {
reqOpts.host = 'localhost';
reqOpts.port = parseInt(fs.readFileSync(portFile, 'utf8').trim(), 10);
if (!state.ipc) {
state.ipc = {};
}
state.ipc.type = 'port';
state.ipc.path = path.dirname(state._ipc.path);
state.ipc.port = reqOpts.port;
} else {
reqOpts.socketPath = state._ipc.path;
}
return reqOpts;
};
RC.createErrorhandler = function (replay, opts, cb) {
return function (err) {
// ENOENT - never started, cleanly exited last start, or creating socket at a different path
// ECONNREFUSED - leftover socket just needs to be restarted
if ('ENOENT' === err.code || 'ECONNREFUSED' === err.code) {
if (opts._taketwo) {
cb(err);
return;
}
require('../../usr/share/install-launcher.js').install({ env: process.env }, function (err) {
if (err) { cb(err); return; }
opts._taketwo = true;
setTimeout(function () {
replay(opts, cb);
}, 2500);
});
return;
}
cb(err);
};
};
RC.request = function request(opts, fn) { RC.request = function request(opts, fn) {
if (!opts) { opts = {}; } if (!opts) { opts = {}; }
var service = opts.service || 'config'; var service = opts.service || 'config';
@ -93,44 +136,12 @@ module.exports.create = function (state) {
method: method method: method
, path: url , path: url
}; };
var fs = require('fs'); reqOpts = RC.port(reqOpts);
var portFile = path.join(path.dirname(state._ipc.path), 'telebit.port');
if (fs.existsSync(portFile)) {
reqOpts.host = 'localhost';
reqOpts.port = parseInt(fs.readFileSync(portFile, 'utf8').trim(), 10);
if (!state.ipc) {
state.ipc = {};
}
state.ipc.type = 'port';
state.ipc.path = path.dirname(state._ipc.path);
state.ipc.port = reqOpts.port;
} else {
reqOpts.socketPath = state._ipc.path;
}
var req = http.request(reqOpts, function (resp) { var req = http.request(reqOpts, function (resp) {
makeResponder(service, resp, fn); makeResponder(service, resp, fn);
}); });
req.on('error', function (err) { req.on('error', RC.createErrorHandler(RC.request, opts, fn));
// ENOENT - never started, cleanly exited last start, or creating socket at a different path
// ECONNREFUSED - leftover socket just needs to be restarted
if ('ENOENT' === err.code || 'ECONNREFUSED' === err.code) {
if (opts._taketwo) {
fn(err);
return;
}
require('../../usr/share/install-launcher.js').install({ env: process.env }, function (err) {
if (err) { fn(err); return; }
opts._taketwo = true;
setTimeout(function () {
RC.request(opts, fn);
}, 2500);
});
return;
}
fn(err);
});
// Simple GET // Simple GET
if ('POST' !== method || !opts.data) { if ('POST' !== method || !opts.data) {
@ -150,7 +161,8 @@ module.exports.create = function (state) {
// alg will be filled out automatically // alg will be filled out automatically
jwk: state.pub jwk: state.pub
, nonce: require('crypto').randomBytes(16).toString('hex') // TODO get from server , nonce: require('crypto').randomBytes(16).toString('hex') // TODO get from server
, url: 'https://' + reqOpts.host + reqOpts.path // TODO make localhost exceptional
, url: RC.resolve(reqOpts.path)
} }
, payload: JSON.stringify(opts.data) , payload: JSON.stringify(opts.data)
}).then(function (jws) { }).then(function (jws) {

6
package-lock.json generated
View File

@ -435,9 +435,9 @@
} }
}, },
"keypairs": { "keypairs": {
"version": "1.2.12", "version": "1.2.14",
"resolved": "https://registry.npmjs.org/keypairs/-/keypairs-1.2.12.tgz", "resolved": "https://registry.npmjs.org/keypairs/-/keypairs-1.2.14.tgz",
"integrity": "sha512-zYjYdDvo7G4AIkkZVM3WEJBTRUIrFzYswYNqCxcCPHUsgbBBdewSHAH1CiaQ+VA6Yb7BLEPIv8gFrRz5wJrgsw==", "integrity": "sha512-ZoZfZMygyB0QcjSlz7Rh6wT2CJasYEHBPETtmHZEfxuJd7bnsOG5AdtPZqHZBT+hoHvuWCp/4y8VmvTvH0Y9uA==",
"requires": { "requires": {
"eckles": "^1.4.1", "eckles": "^1.4.1",
"rasha": "^1.2.4" "rasha": "^1.2.4"

View File

@ -58,7 +58,7 @@
"greenlock": "^2.6.7", "greenlock": "^2.6.7",
"js-yaml": "^3.11.0", "js-yaml": "^3.11.0",
"keyfetch": "^1.1.8", "keyfetch": "^1.1.8",
"keypairs": "^1.2.12", "keypairs": "^1.2.14",
"mkdirp": "^0.5.1", "mkdirp": "^0.5.1",
"proxy-packer": "^2.0.2", "proxy-packer": "^2.0.2",
"ps-list": "^5.0.0", "ps-list": "^5.0.0",