greenlock.js/backends/ursa.js

262 lines
8.6 KiB
JavaScript
Raw Normal View History

2015-12-15 11:37:39 +00:00
'use strict';
var PromiseA = require('bluebird');
var path = require('path');
var fs = PromiseA.promisifyAll(require('fs'));
2015-12-15 12:01:05 +00:00
var cutils = PromiseA.promisifyAll(require('../lib/crypto-utils-ursa'));
2015-12-15 11:37:39 +00:00
//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
2015-12-15 13:11:19 +00:00
}).then(function (resp) {
var data = resp.body;
if ('string' === typeof data) {
try {
data = JSON.parse(data);
} catch(e) {
return PromiseA.reject(e);
}
}
2015-12-15 11:37:39 +00:00
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");
2015-12-15 13:11:19 +00:00
console.warn(data);
2015-12-15 11:37:39 +00:00
}
if (!knownUrls.every(function (url) {
return data[url];
})) {
console.warn("This Let's Encrypt / ACME server is missing urls that this client may need.");
2015-12-15 13:11:19 +00:00
console.warn(data);
2015-12-15 11:37:39 +00:00
}
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
2015-12-15 12:45:36 +00:00
return cutils.generateRsaKeypairAsync(args.rsaBitLength, args.rsaExponent).then(function (pems) {
/* pems = { privateKeyPem, privateKeyJwk, publicKeyPem, publicKeyMd5 } */
2015-12-15 11:37:39 +00:00
return lef.registerNewAccountAsync({
email: args.email
2015-12-15 13:11:19 +00:00
, newReg: args._acmeUrls.newReg
2015-12-15 11:37:39 +00:00
, debug: args.debug || handlers.debug
, agreeToTerms: function (tosUrl, agree) {
2015-12-15 12:45:36 +00:00
// args.email = email; // already there
2015-12-15 11:37:39 +00:00
args.tosUrl = tosUrl;
handlers.agreeToTerms(args, agree);
}
2015-12-15 12:45:36 +00:00
, accountPrivateKeyPem: pems.privateKeyPem
2015-12-15 11:37:39 +00:00
}).then(function (body) {
2015-12-15 13:21:26 +00:00
if (body instanceof Buffer) {
body = body.toString('utf8');
}
2015-12-15 11:37:39 +00:00
if ('string' === typeof body) {
try {
body = JSON.parse(body);
} catch(e) {
// ignore
}
}
2015-12-15 12:45:36 +00:00
2015-12-15 13:21:26 +00:00
var accountDir = path.join(args.accountsDir, pems.publicKeyMd5);
return mkdirpAsync(accountDir).then(function () {
2015-12-15 12:45:36 +00:00
var isoDate = new Date().toISOString();
var accountMeta = {
creation_host: localname
, creation_dt: isoDate
};
2015-12-15 11:37:39 +00:00
// 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" }
2015-12-15 12:45:36 +00:00
// regr.json:
2015-12-15 11:37:39 +00:00
/*
{ body:
{ contact: [ 'mailto:coolaj86@gmail.com' ],
agreement: 'https://letsencrypt.org/documents/LE-SA-v1.0.1-July-27-2015.pdf',
2015-12-15 12:45:36 +00:00
key: { e: 'AQAB', kty: 'RSA', n: '...' } },
2015-12-15 11:37:39 +00:00
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([
2015-12-15 12:45:36 +00:00
fs.writeFileAsync(path.join(accountDir, 'meta.json'), JSON.stringify(accountMeta), 'utf8')
, fs.writeFileAsync(path.join(accountDir, 'private_key.json'), JSON.stringify(pems.privateKeyJwk), 'utf8')
2015-12-15 11:37:39 +00:00
, fs.writeFileAsync(path.join(accountDir, 'regr.json'), JSON.stringify({ body: body }), 'utf8')
2015-12-15 12:45:36 +00:00
]).then(function () {
return pems;
});
2015-12-15 11:37:39 +00:00
});
});
});
}
2015-12-15 12:45:36 +00:00
function getAccount(accountId, args, handlers) {
2015-12-15 11:37:39 +00:00
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) {
2015-12-15 12:45:36 +00:00
files[keyname] = { error: err };
2015-12-15 11:37:39 +00:00
});
})).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...");
2015-12-15 12:45:36 +00:00
return createAccount(args, handlers);
2015-12-15 11:37:39 +00:00
}
return cutils.parseAccountPrivateKeyAsync(files.private_key).then(function (keypair) {
files.accountId = accountId; // md5sum(publicKeyPem)
2015-12-15 12:45:36 +00:00
files.publicKeyMd5 = accountId; // md5sum(publicKeyPem)
2015-12-15 11:37:39 +00:00
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);
}
2015-12-15 12:45:36 +00:00
module.exports.create = function (defaults, handlers) {
2015-12-15 12:12:15 +00:00
var LE = require('../');
2015-12-15 11:37:39 +00:00
var pyconf = PromiseA.promisifyAll(require('pyconf'));
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
2015-12-15 12:12:15 +00:00
var acmeHostname = require('url').parse(args.server).hostname;
2015-12-15 11:37:39 +00:00
var configDir = args.configDir || defaults.configDir || LE.configDir;
2015-12-15 12:12:15 +00:00
args.renewalDir = args.renewalDir || path.join(configDir, 'renewal', args.domains[0] + '.conf');
args.accountsDir = args.accountsDir || path.join(configDir, 'accounts', acmeHostname, 'directory');
2015-12-15 11:37:39 +00:00
2015-12-15 13:11:19 +00:00
return pyconf.readFileAsync(args.renewalDir).then(function (renewal) {
2015-12-15 11:37:39 +00:00
return renewal.account;
}, function (err) {
if ("EENOENT" === err.code) {
2015-12-15 12:45:36 +00:00
return getAccountByEmail(args, handlers);
2015-12-15 11:37:39 +00:00
}
2015-12-15 12:12:15 +00:00
return PromiseA.reject(err);
2015-12-15 11:37:39 +00:00
}).then(function (accountId) {
// Note: the ACME urls are always fetched fresh on purpose
return getAcmeUrls(args).then(function (urls) {
args._acmeUrls = urls;
if (accountId) {
2015-12-15 12:45:36 +00:00
return getAccount(accountId, args, handlers);
2015-12-15 11:37:39 +00:00
} else {
2015-12-15 13:11:19 +00:00
return createAccount(args, handlers);
2015-12-15 11:37:39 +00:00
}
});
}).then(function (account) {
2015-12-15 12:45:36 +00:00
/*
, domains: Array.isArray(args.domains) || (args.domains||'').split(',')
, webroot: args.webrootPath
, accountPrivateKeyPem: obj.privateKeyPem
, 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);
}
*/
2015-12-15 12:12:15 +00:00
console.log(account);
2015-12-15 11:37:39 +00:00
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;
};