greenlock.js/backends/ursa.js

256 lines
8.5 KiB
JavaScript

'use strict';
var PromiseA = require('bluebird');
var path = require('path');
var fs = PromiseA.promisifyAll(require('fs'));
var cutils = PromiseA.promisifyAll(require('crypto-utils-ursa'));
//var futils = require('letsencrypt-forge/lib/crypto-utils');
var requestAsync = PromiseA.promisify(require('request'));
var lef = PromiseA.promisifyAll(require('letsencrypt-forge'));
var knownUrls = ['new-authz', 'new-cert', 'new-reg', 'revoke-cert'];
var ipc = {}; // in-process cache
//function noop() {}
function getAcmeUrls(args) {
var now = Date.now();
// TODO check response header on request for cache time
if ((now - ipc.acmeUrlsUpdatedAt) < 10 * 60 * 1000) {
return PromiseA.resolve(ipc.acmeUrls);
}
return requestAsync({
url: args.server
}).then(function (data) {
if (4 !== Object.keys(data).length) {
console.warn("This Let's Encrypt / ACME server has been updated with urls that this client doesn't understand");
}
if (!knownUrls.every(function (url) {
return data[url];
})) {
console.warn("This Let's Encrypt / ACME server is missing urls that this client may need.");
}
ipc.acmeUrlsUpdatedAt = Date.now();
ipc.acmeUrls = {
newAuthz: data['new-authz']
, newCert: data['new-cert']
, newReg: data['new-reg']
, revokeCert: data['revoke-cert']
};
return ipc.acmeUrls;
});
}
function createAccount(args, handlers) {
var mkdirpAsync = PromiseA.promisify(require('mkdirp'));
var os = require("os");
var localname = os.hostname();
// TODO support ECDSA
// arg.rsaBitLength args.rsaExponent
return cutils.generateRsaKeypairAsync(args.rsaBitLength, args.rsaExponent).then(function (obj) {
/* obj = { privateKeyPem, publicKeyPem, publicKeyMd5 } */
var accountId = obj.publicKeyMd5; // I would have chosen sha1 or sha2... but whatever
var accountDir = path.join(args.accountsDir, accountId);
var isoDate = new Date().toISOString();
/*
files.accountId = accountId; // md5sum(publicKeyPem)
files.publicKeyPem = keypair.publicKeyPem; // ascii PEM: ----BEGIN...
files.privateKeyPem = keypair.privateKeyPem; // ascii PEM: ----BEGIN...
files.privateKeyJson = keypair.private_key; // json { n: ..., e: ..., iq: ..., etc }
*/
// TODO register
return lef.registerNewAccountAsync({
email: args.email
, domains: Array.isArray(args.domains) || (args.domains||'').split(',')
, newReg: args.server
, debug: args.debug || handlers.debug
, webroot: args.webrootPath
, setChallenge: function (domain, key, value, done) {
args.domains = [domain];
handlers.setChallenge(args, key, value, done);
}
, removeChallenge: function (domain, key, done) {
args.domains = [domain];
handlers.removeChallenge(args, key, done);
}
, agreeToTerms: function (tosUrl, agree) {
// args.email = email;
args.tosUrl = tosUrl;
handlers.agreeToTerms(args, agree);
}
// TODO send either privateKeyPem or privateKeyJson or privateKeyJwk (?)
, privateKeyPem: obj.privateKeyPem
}).then(function (body) {
if ('string' === typeof body) {
try {
body = JSON.parse(body);
} catch(e) {
// ignore
}
}
return mkdirpAsync(args.accountDir, function () {
var jwk = cutils.toAcmePrivateKey(obj.privateKeyPem);
// meta.json {"creation_host": "ns1.redirect-www.org", "creation_dt": "2015-12-11T04:14:38Z"}
// private_key.json { "e", "d", "n", "q", "p", "kty", "qi", "dp", "dq" }
// regr.json { "body" }
/*
{ body:
{ contact: [ 'mailto:coolaj86@gmail.com' ],
agreement: 'https://letsencrypt.org/documents/LE-SA-v1.0.1-July-27-2015.pdf',
key:
{ e: 'AQAB',
kty: 'RSA',
n: '...' } },
uri: 'https://acme-v01.api.letsencrypt.org/acme/reg/71272',
new_authzr_uri: 'https://acme-v01.api.letsencrypt.org/acme/new-authz',
terms_of_service: 'https://letsencrypt.org/documents/LE-SA-v1.0.1-July-27-2015.pdf' }
*/
return PromiseA.all([
fs.writeFileAsync(path.join(accountDir, 'meta.json'), JSON.stringify({ creation_host: localname, creation_dt: isoDate }), 'utf8')
, fs.writeFileAsync(path.join(accountDir, 'private_key.json'), JSON.stringify(jwk), 'utf8')
, fs.writeFileAsync(path.join(accountDir, 'regr.json'), JSON.stringify({ body: body }), 'utf8')
]);
});
});
});
}
function getAccount(args, accountId) {
var accountDir = path.join(args.accountsDir, accountId);
var files = {};
var configs = ['meta.json', 'private_key.json', 'regr.json'];
return PromiseA.all(configs.map(function (filename) {
var keyname = filename.slice(0, -5);
return fs.readFileAsync(path.join(accountDir, filename), 'utf8').then(function (text) {
var data;
try {
data = JSON.parse(text);
} catch(e) {
files[keyname] = { error: e };
return;
}
files[keyname] = data;
}, function (err) {
files[keyname] = err;
});
})).then(function () {
if (!Object.keys(files).every(function (key) {
return !files[key].error;
})) {
console.warn("Account '" + accountId + "' was currupt. No big deal (I think?). Creating a new one...");
return createAccount(args);
}
return cutils.parseAccountPrivateKeyAsync(files.private_key).then(function (keypair) {
files.accountId = accountId; // md5sum(publicKeyPem)
files.publicKeyPem = keypair.publicKeyPem; // ascii PEM: ----BEGIN...
files.privateKeyPem = keypair.privateKeyPem; // ascii PEM: ----BEGIN...
files.privateKeyJson = keypair.private_key; // json { n: ..., e: ..., iq: ..., etc }
return files;
});
});
}
function getAccountByEmail(args) {
// If we read 10,000 account directories looking for
// just one email address, that could get crazy.
// We should have a folder per email and list
// each account as a file in the folder
// TODO
return PromiseA.resolve(null);
}
module.exports.create = function (defaults, opts) {
var LE = require('./');
var pyconf = PromiseA.promisifyAll(require('pyconf'));
if (!opts) {
opts = {};
}
/*
defaults.webroot = true;
defaults.renewByDefault = true;
defaults.text = true;
*/
defaults.server = defaults.server || LE.liveServer;
var wrapped = {
registerAsync: function (args) {
args.server = args.server || defaults.server || LE.liveServer; // https://acme-v01.api.letsencrypt.org/directory
var hostname = require('url').parse(args.server).hostname;
var configDir = args.configDir || defaults.configDir || LE.configDir;
args.renewalDir = args.renewalDir || path.join(configDir, 'renewal', hostname + '.conf');
args.accountsDir = args.accountsDir || path.join(configDir, 'accounts', hostname, 'directory');
pyconf.readFileAsync(args.renewalDir).then(function (renewal) {
return renewal.account;
}, function (err) {
if ("EENOENT" === err.code) {
return getAccountByEmail(args);
}
return err;
}).then(function (accountId) {
// Note: the ACME urls are always fetched fresh on purpose
return getAcmeUrls(args).then(function (urls) {
args._acmeUrls = urls;
if (accountId) {
return getAccount(args, accountId);
} else {
return createAccount(args);
}
});
}).then(function (account) {
throw new Error("IMPLEMENTATION NOT COMPLETE");
});
/*
return fs.readdirAsync(accountsDir, function (nodes) {
return PromiseA.all(nodes.map(function (node) {
var reMd5 = /[a-f0-9]{32}/i;
if (reMd5.test(node)) {
}
}));
});
*/
}
, fetchAsync: function (args) {
var hostname = args.domains[0];
var crtpath = defaults.configDir + defaults.fullchainTpl.replace(/:hostname/, hostname);
var privpath = defaults.configDir + defaults.privkeyTpl.replace(/:hostname/, hostname);
return PromiseA.all([
fs.readFileAsync(privpath, 'ascii')
, fs.readFileAsync(crtpath, 'ascii')
// stat the file, not the link
, fs.statAsync(crtpath)
]).then(function (arr) {
return {
key: arr[0] // privkey.pem
, cert: arr[1] // fullchain.pem
// TODO parse centificate for lifetime / expiresAt
, issuedAt: arr[2].mtime.valueOf()
};
}, function () {
return null;
});
}
};
return wrapped;
};