manage renewals perfectly :-)

This commit is contained in:
AJ ONeal 2015-12-20 05:13:41 +00:00
parent a6b1b5cfa6
commit 563f3ae3eb
5 changed files with 275 additions and 139 deletions

View File

@ -137,8 +137,11 @@ LE.create = function (defaults, handlers, backend) {
// If you do not check these things, then someone could attack you
// and cause you, in return, to have your ip be rate-limit blocked
//
console.warn("\n[TODO]: node-letsencrypt: `validate(hostnames, cb)` needs to be implemented");
console.warn("(it'll work fine without it, but for security - and convenience - it should be implemented\n");
//console.warn("\n[TODO]: node-letsencrypt: `validate(hostnames, cb)` needs to be implemented");
//console.warn("(it'll work fine without it, but for security - and convenience - it should be implemented\n");
// UPDATE:
// it's actually probably better that we don't do this here and instead
// take care of it in the approveRegistrationCallback in letsencrypt-express
cb(null, true);
}
, _registerHelper: function (args, cb) {

View File

@ -72,7 +72,8 @@ function createAccount(args, handlers) {
});
}
function getAccount(accountId, args, handlers) {
function getAccount(args, handlers) {
var accountId = args.accountId;
var accountDir = path.join(args.accountsDir, accountId);
var files = {};
var configs = ['meta.json', 'private_key.json', 'regr.json'];

View File

@ -31,131 +31,205 @@ function getAcmeUrls(args) {
});
}
function writeCertificateAsync(result, args, defaults, handlers) {
if (args.debug) {
console.log("got certificate!");
}
result.fullchain = result.cert + '\n' + result.ca;
function readRenewalConfig(args) {
var pyconf = PromiseA.promisifyAll(require('pyconf'));
return pyconf.readFileAsync(args.renewalPath).then(function (obj) {
return obj;
return pyconf.readFileAsync(args.renewalPath).then(function (pyobj) {
return pyobj;
}, function () {
return pyconf.readFileAsync(path.join(__dirname, 'renewal.conf.tpl')).then(function (obj) {
return obj;
});
}).then(function (obj) {
obj.checkpoint = parseInt(obj.checkpoint, 10) || 0;
var liveDir = args.liveDir || path.join(args.configDir, 'live', args.domains[0]);
var certPath = args.certPath || obj.cert || path.join(liveDir, 'cert.pem');
var fullchainPath = args.fullchainPath || obj.fullchain || path.join(liveDir, 'fullchain.pem');
var chainPath = args.chainPath || obj.chain || path.join(liveDir, 'chain.pem');
var privkeyPath = args.privkeyPath || obj.privkey
//|| args.domainPrivateKeyPath || args.domainKeyPath || obj.keyPath
|| path.join(liveDir, 'privkey.pem');
if (args.debug) {
console.log('################ privkeyPath ################');
console.log(privkeyPath);
}
var archiveDir = args.archiveDir || path.join(args.configDir, 'archive', args.domains[0]);
var checkpoint = obj.checkpoint.toString();
var certArchive = path.join(archiveDir, 'cert' + checkpoint + '.pem');
var fullchainArchive = path.join(archiveDir, 'fullchain' + checkpoint + '.pem');
var chainArchive = path.join(archiveDir, 'chain'+ checkpoint + '.pem');
var privkeyArchive = path.join(archiveDir, 'privkey' + checkpoint + '.pem');
return mkdirpAsync(archiveDir).then(function () {
return PromiseA.all([
sfs.writeFileAsync(certArchive, result.cert, 'ascii')
, sfs.writeFileAsync(chainArchive, result.ca || result.chain, 'ascii')
, sfs.writeFileAsync(fullchainArchive, result.fullchain, 'ascii')
, sfs.writeFileAsync(privkeyArchive, result.key || result.privkey || args.domainPrivateKeyPem, 'ascii')
]);
}).then(function () {
return mkdirpAsync(liveDir);
}).then(function () {
return PromiseA.all([
sfs.writeFileAsync(certPath, result.cert, 'ascii')
, sfs.writeFileAsync(chainPath, result.ca || result.chain, 'ascii')
, sfs.writeFileAsync(fullchainPath, result.fullchain, 'ascii')
, sfs.writeFileAsync(privkeyPath, result.key || result.privkey || args.domainPrivateKeyPem, 'ascii')
]);
}).then(function () {
obj.checkpoint += 1;
var updates = {
account: args.accountId || args.account.id
, cert: certPath
, privkey: privkeyPath
, chain: chainPath
, fullchain: fullchainPath
, configDir: args.configDir
, workDir: args.workDir
, tos: args.agreeTos && true
, http01Port: args.http01Port
, keyPath: args.domainPrivateKeyPath || args.privkeyPath
, email: args.email
, domains: args.domains
, rsaKeySize: args.rsaKeySize
, checkpoints: obj.checkpoint
// TODO XXX what's the deal with these? they don't make sense
// are they just old junk? or do they have a meaning that I don't know about?
, fullchainPath: path.join(args.configDir, 'chain.pem')
, certPath: path.join(args.configDir, 'cert.pem')
, chainPath: path.join(args.configDir, 'chain.pem')
// TODO XXX end
// yes, it's an array. weird, right?
, webrootPath: args.webrootPath && [args.webrootPath] || []
, server: args.server || args.acmeDiscoveryUrl
, logsDir: args.logsDir
};
// final section is completely dynamic
// :hostname = :webroot_path
args.domains.forEach(function (hostname) {
updates[hostname] = args.webrootPath;
});
// must write back to the original object or
// annotations will be lost
Object.keys(updates).forEach(function (key) {
obj[key] = updates[key];
});
return mkdirpAsync(path.dirname(args.renewalPath)).then(function () {
return pyconf.writeFileAsync(args.renewalPath, obj);
});
}).then(function () {
return {
certPath: certPath
, chainPath: chainPath
, fullchainPath: fullchainPath
, privkeyPath: privkeyPath
// some ambiguity here...
, privkey: result.key || result.privkey || args.domainPrivateKeyPem
, fullchain: result.fullchain || result.cert
, chain: result.ca || result.chain
// especially this one... might be cert only, might be fullchain
, cert: result.cert
, issuedAt: Date.now()
, lifetime: defaults.lifetime || handlers.lifetime
};
return pyconf.readFileAsync(path.join(__dirname, 'renewal.conf.tpl')).then(function (pyobj) {
return pyobj;
});
});
}
function getCertificateAsync(account, args, defaults, handlers) {
function writeRenewalConfig(args) {
var pyobj = args.pyobj;
pyobj.checkpoints = parseInt(pyobj.checkpoints, 10) || 0;
var pyconf = PromiseA.promisifyAll(require('pyconf'));
var liveDir = args.liveDir || path.join(args.configDir, 'live', args.domains[0]);
var certPath = args.certPath || pyobj.cert || path.join(liveDir, 'cert.pem');
var fullchainPath = args.fullchainPath || pyobj.fullchain || path.join(liveDir, 'fullchain.pem');
var chainPath = args.chainPath || pyobj.chain || path.join(liveDir, 'chain.pem');
var privkeyPath = args.privkeyPath || pyobj.privkey
//|| args.domainPrivateKeyPath || args.domainKeyPath || pyobj.keyPath
|| path.join(liveDir, 'privkey.pem');
if (args.debug) {
console.log('################ privkeyPath ################');
console.log(privkeyPath);
}
var updates = {
account: args.account.id
, configDir: args.configDir
, domains: args.domains
, email: args.email
, tos: args.agreeTos && true
// yes, it's an array. weird, right?
, webrootPath: args.webrootPath && [args.webrootPath] || []
, server: args.server || args.acmeDiscoveryUrl
, privkey: privkeyPath
, fullchain: fullchainPath
, cert: certPath
, chain: chainPath
, http01Port: args.http01Port
, keyPath: args.domainPrivateKeyPath || args.privkeyPath
, rsaKeySize: args.rsaKeySize
, checkpoints: pyobj.checkpoints
/* // TODO XXX what's the deal with these? they don't make sense
// are they just old junk? or do they have a meaning that I don't know about?
, fullchainPath: path.join(args.configDir, 'chain.pem')
, certPath: path.join(args.configDir, 'cert.pem')
, chainPath: path.join(args.configDir, 'chain.pem')
*/ // TODO XXX end
, workDir: args.workDir
, logsDir: args.logsDir
};
// final section is completely dynamic
// :hostname = :webroot_path
args.domains.forEach(function (hostname) {
updates[hostname] = args.webrootPath;
});
// must write back to the original pyobject or
// annotations will be lost
Object.keys(updates).forEach(function (key) {
pyobj[key] = updates[key];
});
return mkdirpAsync(path.dirname(args.renewalPath)).then(function () {
return pyconf.writeFileAsync(args.renewalPath, pyobj);
}).then(function () {
return pyobj;
});
}
function getOrCreateRenewal(args) {
return readRenewalConfig(args).then(function (pyobj) {
var minver = pyobj.checkpoints >= 0;
args.pyobj = pyobj;
if (!minver) {
args.checkpoints = 0;
pyobj.checkpoints = 0;
return writeRenewalConfig(args);
}
// args.account.id = pyobj.account
// args.configDir = args.configDir || pyobj.configDir;
args.checkpoints = pyobj.checkpoints;
args.agreeTos = (args.agreeTos || pyobj.tos) && true;
args.email = args.email || pyobj.email;
args.domains = args.domains || pyobj.domains;
// yes, it's an array. weird, right?
args.webrootPath = args.webrootPath || pyobj.webrootPath[0];
args.server = args.server || args.acmeDiscoveryUrl || pyobj.server;
args.certPath = args.certPath || pyobj.cert;
args.privkeyPath = args.privkeyPath || pyobj.privkey;
args.chainPath = args.chainPath || pyobj.chain;
args.fullchainPath = args.fullchainPath || pyobj.fullchain;
//, workDir: args.workDir
//, logsDir: args.logsDir
args.rsaKeySize = args.rsaKeySize || pyobj.rsaKeySize;
args.http01Port = args.http01Port || pyobj.http01Port;
args.domainKeyPath = args.domainPrivateKeyPath || args.domainKeyPath || args.keyPath || pyobj.keyPath;
return writeRenewalConfig(args);
});
}
function writeCertificateAsync(args, defaults, handlers) {
if (args.debug) {
console.log("got certificate!");
}
var obj = args.pyobj;
var result = args.pems;
result.fullchain = result.cert + '\n' + result.ca;
obj.checkpoints = parseInt(obj.checkpoints, 10) || 0;
var liveDir = args.liveDir || path.join(args.configDir, 'live', args.domains[0]);
var certPath = args.certPath || obj.cert || path.join(liveDir, 'cert.pem');
var fullchainPath = args.fullchainPath || obj.fullchain || path.join(liveDir, 'fullchain.pem');
var chainPath = args.chainPath || obj.chain || path.join(liveDir, 'chain.pem');
var privkeyPath = args.privkeyPath || obj.privkey
//|| args.domainPrivateKeyPath || args.domainKeyPath || obj.keyPath
|| path.join(liveDir, 'privkey.pem');
if (args.debug) {
console.log('################ privkeyPath ################');
console.log(privkeyPath);
}
var archiveDir = args.archiveDir || path.join(args.configDir, 'archive', args.domains[0]);
var checkpoints = obj.checkpoints.toString();
var certArchive = path.join(archiveDir, 'cert' + checkpoints + '.pem');
var fullchainArchive = path.join(archiveDir, 'fullchain' + checkpoints + '.pem');
var chainArchive = path.join(archiveDir, 'chain'+ checkpoints + '.pem');
var privkeyArchive = path.join(archiveDir, 'privkey' + checkpoints + '.pem');
return mkdirpAsync(archiveDir).then(function () {
return PromiseA.all([
sfs.writeFileAsync(certArchive, result.cert, 'ascii')
, sfs.writeFileAsync(chainArchive, result.ca || result.chain, 'ascii')
, sfs.writeFileAsync(fullchainArchive, result.fullchain, 'ascii')
, sfs.writeFileAsync(privkeyArchive, result.key || result.privkey || args.domainPrivateKeyPem, 'ascii')
]);
}).then(function () {
return mkdirpAsync(liveDir);
}).then(function () {
return PromiseA.all([
sfs.writeFileAsync(certPath, result.cert, 'ascii')
, sfs.writeFileAsync(chainPath, result.ca || result.chain, 'ascii')
, sfs.writeFileAsync(fullchainPath, result.fullchain, 'ascii')
, sfs.writeFileAsync(privkeyPath, result.key || result.privkey || args.domainPrivateKeyPem, 'ascii')
]);
}).then(function () {
obj.checkpoints += 1;
args.checkpoints += 1;
return writeRenewalConfig(args);
}).then(function () {
return {
certPath: certPath
, chainPath: chainPath
, fullchainPath: fullchainPath
, privkeyPath: privkeyPath
// some ambiguity here...
, privkey: result.key || result.privkey || args.domainPrivateKeyPem
, fullchain: result.fullchain || result.cert
, chain: result.ca || result.chain
// especially this one... might be cert only, might be fullchain
, cert: result.cert
, issuedAt: Date.now()
, lifetime: defaults.lifetime || handlers.lifetime
};
});
}
function getCertificateAsync(args, defaults, handlers) {
var account = args.account;
return leCrypto.generateRsaKeypairAsync(args.rsaKeySize, 65537).then(function (domainKey) {
if (args.debug) {
console.log("get certificate");
@ -214,11 +288,12 @@ function getCertificateAsync(account, args, defaults, handlers) {
}
});
}).then(function (results) {
return writeCertificateAsync(results, args, defaults, handlers);
args.pems = results;
return writeCertificateAsync(args, defaults, handlers);
});
}
function getOrCreateDomainCertificate(account, args, defaults, handlers) {
function getOrCreateDomainCertificate(args, defaults, handlers) {
return fetchFromConfigLiveDir(args).then(function (certs) {
// if nothing, register and save
// if something, check date (don't register unless 30+ days)
@ -228,15 +303,15 @@ function getOrCreateDomainCertificate(account, args, defaults, handlers) {
//console.log(certs);
if (!certs) {
// no certs, seems like a good time to get some
return getCertificateAsync(account, args, defaults, handlers);
return getCertificateAsync(args, defaults, handlers);
}
else if (certs.issuedAt > (27 * 24 * 60 * 60 * 1000)) {
// cert is at least 27 days old we can renew that
return getCertificateAsync(account, args, defaults, handlers);
return getCertificateAsync(args, defaults, handlers);
}
else if (args.duplicate) {
// YOLO! I be gettin' fresh certs 'erday! Yo!
return getCertificateAsync(account, args, defaults, handlers);
return getCertificateAsync(args, defaults, handlers);
}
else {
console.warn('[WARN] Ignoring renewal attempt for certificate less than 27 days old. Use args.duplicate to force.');
@ -244,16 +319,10 @@ function getOrCreateDomainCertificate(account, args, defaults, handlers) {
return certs;
}
});
};
}
function getOrCreateAcmeAccount(args, defaults, handlers) {
var pyconf = PromiseA.promisifyAll(require('pyconf'));
var server = args.server;
var acmeHostname = require('url').parse(server).hostname;
var configDir = args.configDir;
args.renewalPath = args.renewalPath || path.join(configDir, 'renewal', args.domains[0] + '.conf');
args.accountsDir = args.accountsDir || path.join(configDir, 'accounts', acmeHostname, 'directory');
return pyconf.readFileAsync(args.renewalPath).then(function (renewal) {
var accountId = renewal.account;
@ -279,7 +348,8 @@ function getOrCreateAcmeAccount(args, defaults, handlers) {
if (args.debug) {
console.log('[LE] use account');
}
return Accounts.getAccount(accountId, args, handlers);
args.accountId = accountId;
return Accounts.getAccount(args, handlers);
} else {
if (args.debug) {
console.log('[LE] create account');
@ -322,13 +392,25 @@ module.exports.create = function (defaults, handlers) {
copy = merge(args, defaults);
tplCopy(copy);
if (args.debug) {
if (copy.debug) {
console.log('[LE DEBUG] reg domains', args.domains);
}
var url = require('url');
var acmeLocation = url.parse(copy.server);
var acmeHostpath = path.join(acmeLocation.hostname, acmeLocation.pathname);
copy.renewalPath = copy.renewalPath || path.join(copy.configDir, 'renewal', copy.domains[0] + '.conf');
copy.accountsDir = copy.accountsDir || path.join(copy.configDir, 'accounts', acmeHostpath);
return getOrCreateAcmeAccount(copy, defaults, handlers).then(function (account) {
console.log("account", account);
args.account = account;
return getOrCreateDomainCertificate(account, copy, defaults, handlers);
//console.log("account", account);
copy.account = account;
return getOrCreateRenewal(copy).then(function (pyobj) {
copy.pyobj = pyobj;
return getOrCreateDomainCertificate(copy, defaults, handlers);
});
});
}
, fetchAsync: function (args) {

View File

@ -40,7 +40,7 @@ authenticator = webroot
domains = :hostnames #comma,delimited,list
rsa_key_size = :rsa_key_size
# starts at 0 and increments at every renewal
checkpoints = :checkpoint_count
checkpoints = -1
manual_test_mode = False
apache = False
cert_path = :cert_path

50
tests/pyconf-write.js Normal file
View File

@ -0,0 +1,50 @@
'use strict';
var PromiseA = require('bluebird');
var pyconf = PromiseA.promisifyAll(require('pyconf'));
var mkdirpAsync = PromiseA.promisify(require('mkdirp'));
var path = require('path');
pyconf.readFileAsync(path.join(__dirname, 'lib', 'renewal.conf.tpl')).then(function (obj) {
var domains = ['example.com', 'www.example.com'];
var webrootPath = '/tmp/www/example.com';
console.log(obj);
var keys = obj.__keys;
var lines = obj.__lines;
obj.__keys = null;
obj.__lines = null;
var updates = {
account: 'ACCOUNT_ID'
, cert: 'CERT_PATH'
, privkey: 'PRIVATEKEY_PATH'
, configDir: 'CONFIG_DIR'
, tos: true
, http01Port: 80
, domains: domains
};
// final section is completely dynamic
// :hostname = :webroot_path
domains.forEach(function (hostname) {
updates[hostname] = webrootPath;
});
// must write back to the original object or
// annotations will be lost
Object.keys(updates).forEach(function (key) {
obj[key] = updates[key];
});
var renewalPath = '/tmp/letsencrypt/renewal/example.com.conf';
return mkdirpAsync(path.dirname(renewalPath)).then(function () {
console.log(obj);
obj.__keys = keys;
obj.__lines = lines;
return pyconf.writeFileAsync(renewalPath, obj);
});
});