2015-12-10 13:31:47 -08:00
|
|
|
'use strict';
|
|
|
|
|
|
|
|
|
|
var acme = require('./cert/acme.js'),
|
2016-03-17 12:20:02 -07:00
|
|
|
apps = require('./apps.js'),
|
2015-12-10 13:31:47 -08:00
|
|
|
assert = require('assert'),
|
2015-12-14 12:28:00 -08:00
|
|
|
async = require('async'),
|
2015-12-13 19:06:19 -08:00
|
|
|
caas = require('./cert/caas.js'),
|
2015-12-14 23:37:01 -08:00
|
|
|
cloudron = require('./cloudron.js'),
|
2015-12-10 13:31:47 -08:00
|
|
|
config = require('./config.js'),
|
2015-12-11 13:52:21 -08:00
|
|
|
constants = require('./constants.js'),
|
2015-12-11 21:49:24 -08:00
|
|
|
debug = require('debug')('box:src/certificates'),
|
2015-12-11 13:52:21 -08:00
|
|
|
fs = require('fs'),
|
2016-03-19 20:40:03 -07:00
|
|
|
mailer = require('./mailer.js'),
|
2015-12-11 14:37:55 -08:00
|
|
|
nginx = require('./nginx.js'),
|
2015-12-11 13:52:21 -08:00
|
|
|
path = require('path'),
|
2015-12-10 13:31:47 -08:00
|
|
|
paths = require('./paths.js'),
|
2015-12-10 20:31:38 -08:00
|
|
|
safe = require('safetydance'),
|
2015-12-11 22:25:22 -08:00
|
|
|
settings = require('./settings.js'),
|
2015-12-10 20:31:38 -08:00
|
|
|
sysinfo = require('./sysinfo.js'),
|
2016-03-08 09:52:13 -08:00
|
|
|
tld = require('tldjs'),
|
2016-01-13 14:21:23 -08:00
|
|
|
user = require('./user.js'),
|
2015-12-10 20:31:38 -08:00
|
|
|
util = require('util'),
|
2015-12-11 14:37:55 -08:00
|
|
|
waitForDns = require('./waitfordns.js'),
|
2015-12-10 20:31:38 -08:00
|
|
|
x509 = require('x509');
|
2015-12-10 13:31:47 -08:00
|
|
|
|
|
|
|
|
exports = module.exports = {
|
2015-12-11 14:02:58 -08:00
|
|
|
installAdminCertificate: installAdminCertificate,
|
2015-12-10 20:31:38 -08:00
|
|
|
autoRenew: autoRenew,
|
2015-12-11 14:13:24 -08:00
|
|
|
setFallbackCertificate: setFallbackCertificate,
|
2015-12-11 13:52:21 -08:00
|
|
|
setAdminCertificate: setAdminCertificate,
|
|
|
|
|
CertificatesError: CertificatesError,
|
|
|
|
|
validateCertificate: validateCertificate,
|
2016-03-19 13:54:52 -07:00
|
|
|
ensureCertificate: ensureCertificate
|
2015-12-10 13:31:47 -08:00
|
|
|
};
|
|
|
|
|
|
2015-12-14 12:28:00 -08:00
|
|
|
var NOOP_CALLBACK = function (error) { if (error) debug(error); };
|
|
|
|
|
|
2015-12-11 13:43:33 -08:00
|
|
|
function CertificatesError(reason, errorOrMessage) {
|
|
|
|
|
assert.strictEqual(typeof reason, 'string');
|
|
|
|
|
assert(errorOrMessage instanceof Error || typeof errorOrMessage === 'string' || typeof errorOrMessage === 'undefined');
|
|
|
|
|
|
|
|
|
|
Error.call(this);
|
|
|
|
|
Error.captureStackTrace(this, this.constructor);
|
|
|
|
|
|
|
|
|
|
this.name = this.constructor.name;
|
|
|
|
|
this.reason = reason;
|
|
|
|
|
if (typeof errorOrMessage === 'undefined') {
|
|
|
|
|
this.message = reason;
|
|
|
|
|
} else if (typeof errorOrMessage === 'string') {
|
|
|
|
|
this.message = errorOrMessage;
|
|
|
|
|
} else {
|
|
|
|
|
this.message = 'Internal error';
|
|
|
|
|
this.nestedError = errorOrMessage;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
util.inherits(CertificatesError, Error);
|
|
|
|
|
CertificatesError.INTERNAL_ERROR = 'Internal Error';
|
|
|
|
|
CertificatesError.INVALID_CERT = 'Invalid certificate';
|
|
|
|
|
|
2015-12-14 12:28:00 -08:00
|
|
|
function getApi(callback) {
|
|
|
|
|
settings.getTlsConfig(function (error, tlsConfig) {
|
|
|
|
|
if (error) return callback(error);
|
|
|
|
|
|
|
|
|
|
var api = tlsConfig.provider === 'caas' ? caas : acme;
|
|
|
|
|
|
2015-12-17 13:17:46 -08:00
|
|
|
var options = { };
|
2015-12-19 13:48:14 -08:00
|
|
|
options.prod = tlsConfig.provider.match(/.*-prod/) !== null;
|
2016-01-13 14:21:23 -08:00
|
|
|
|
2016-01-13 12:15:27 -08:00
|
|
|
// registering user with an email requires A or MX record (https://github.com/letsencrypt/boulder/issues/1197)
|
|
|
|
|
// we cannot use admin@fqdn because the user might not have set it up.
|
2016-01-13 14:21:23 -08:00
|
|
|
// we simply update the account with the latest email we have each time when getting letsencrypt certs
|
|
|
|
|
// https://github.com/ietf-wg-acme/acme/issues/30
|
|
|
|
|
user.getOwner(function (error, owner) {
|
|
|
|
|
options.email = error ? 'admin@cloudron.io' : owner.email; // can error if not activated yet
|
2015-12-17 13:17:46 -08:00
|
|
|
|
2016-01-13 14:21:23 -08:00
|
|
|
callback(null, api, options);
|
|
|
|
|
});
|
2015-12-14 12:28:00 -08:00
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
2015-12-11 14:02:58 -08:00
|
|
|
function installAdminCertificate(callback) {
|
2015-12-14 23:37:01 -08:00
|
|
|
if (cloudron.isConfiguredSync()) return callback();
|
|
|
|
|
|
2015-12-11 22:25:22 -08:00
|
|
|
settings.getTlsConfig(function (error, tlsConfig) {
|
|
|
|
|
if (error) return callback(error);
|
|
|
|
|
|
|
|
|
|
if (tlsConfig.provider === 'caas') return callback();
|
2015-12-10 13:31:47 -08:00
|
|
|
|
2016-01-05 12:23:07 +01:00
|
|
|
sysinfo.getIp(function (error, ip) {
|
|
|
|
|
if (error) return callback(error);
|
2015-12-11 14:37:55 -08:00
|
|
|
|
2016-03-08 09:52:13 -08:00
|
|
|
var zoneName = tld.getDomain(config.fqdn());
|
|
|
|
|
waitForDns(config.adminFqdn(), ip, zoneName, function (error) {
|
2016-01-05 12:23:07 +01:00
|
|
|
if (error) return callback(error); // this cannot happen because we retry forever
|
2015-12-10 13:31:47 -08:00
|
|
|
|
2016-01-05 12:23:07 +01:00
|
|
|
ensureCertificate(config.adminFqdn(), function (error, certFilePath, keyFilePath) {
|
|
|
|
|
if (error) { // currently, this can never happen
|
|
|
|
|
debug('Error obtaining certificate. Proceed anyway', error);
|
|
|
|
|
return callback();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
nginx.configureAdmin(certFilePath, keyFilePath, callback);
|
|
|
|
|
});
|
2015-12-11 22:25:22 -08:00
|
|
|
});
|
2015-12-11 14:37:55 -08:00
|
|
|
});
|
|
|
|
|
});
|
2015-12-10 13:31:47 -08:00
|
|
|
}
|
|
|
|
|
|
2016-03-19 12:50:31 -07:00
|
|
|
function isExpiringSync(domain, hours) {
|
|
|
|
|
assert.strictEqual(typeof domain, 'string');
|
2016-03-18 22:59:51 -07:00
|
|
|
assert.strictEqual(typeof hours, 'number');
|
2016-03-19 12:50:31 -07:00
|
|
|
|
|
|
|
|
var certFilePath = path.join(paths.APP_CERTS_DIR, domain + '.cert');
|
|
|
|
|
var keyFilePath = path.join(paths.APP_CERTS_DIR, domain + '.key');
|
|
|
|
|
|
2016-03-19 13:22:38 -07:00
|
|
|
if (!fs.existsSync(certFilePath) || !fs.existsSync(keyFilePath)) return 2; // not found
|
2015-12-14 12:38:19 -08:00
|
|
|
|
2016-03-19 13:22:38 -07:00
|
|
|
var result = safe.child_process.spawnSync('/usr/bin/openssl', [ 'x509', '-checkend', String(60 * 60 * hours), '-in', certFilePath ]);
|
2016-03-18 22:59:51 -07:00
|
|
|
|
2016-03-21 08:25:10 -07:00
|
|
|
debug('isExpiringSync: %s %s %s', certFilePath, result.stdout.toString('utf8').trim(), result.status);
|
2016-03-14 22:50:06 -07:00
|
|
|
|
|
|
|
|
return result.status === 1; // 1 - expired 0 - not expired
|
2015-12-14 12:38:19 -08:00
|
|
|
}
|
|
|
|
|
|
2015-12-14 12:40:39 -08:00
|
|
|
function autoRenew(callback) {
|
|
|
|
|
debug('autoRenew: Checking certificates for renewal');
|
|
|
|
|
callback = callback || NOOP_CALLBACK;
|
|
|
|
|
|
2016-03-17 12:20:02 -07:00
|
|
|
apps.getAll(function (error, allApps) {
|
|
|
|
|
if (error) return callback(error);
|
2015-12-14 12:40:39 -08:00
|
|
|
|
2016-03-19 13:54:52 -07:00
|
|
|
allApps.push({ location: 'my' }); // inject fake webadmin app
|
2016-03-18 23:43:56 -07:00
|
|
|
|
2016-03-17 12:20:02 -07:00
|
|
|
var expiringApps = [ ];
|
|
|
|
|
for (var i = 0; i < allApps.length; i++) {
|
|
|
|
|
var appDomain = config.appFqdn(allApps[i].location);
|
2016-03-19 12:50:31 -07:00
|
|
|
if (isExpiringSync(appDomain, 24 * 30)) { // expired or not found
|
|
|
|
|
expiringApps.push(allApps[i]);
|
2016-03-17 12:20:02 -07:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
debug('autoRenew: %j needs to be renewed', expiringApps.map(function (a) { return config.appFqdn(a.location); }));
|
2015-12-14 12:40:39 -08:00
|
|
|
|
2016-03-17 12:20:02 -07:00
|
|
|
getApi(function (error, api, apiOptions) {
|
|
|
|
|
if (error) return callback(error);
|
|
|
|
|
|
|
|
|
|
async.eachSeries(expiringApps, function iterator(app, iteratorCallback) {
|
|
|
|
|
var domain = config.appFqdn(app.location);
|
|
|
|
|
debug('autoRenew: renewing cert for %s with options %j', domain, apiOptions);
|
2015-12-14 12:40:39 -08:00
|
|
|
|
2016-03-17 12:20:02 -07:00
|
|
|
api.getCertificate(domain, apiOptions, function (error) {
|
2016-03-19 13:54:52 -07:00
|
|
|
var certFilePath, keyFilePath;
|
|
|
|
|
|
2016-03-19 20:40:03 -07:00
|
|
|
mailer.certificateRenewed(domain, error ? error.message : '');
|
|
|
|
|
|
2016-03-18 23:26:57 -07:00
|
|
|
if (error) {
|
2016-03-19 18:37:05 -07:00
|
|
|
debug('autoRenew: could not renew cert for %s because %s', domain, error);
|
2016-03-19 13:54:52 -07:00
|
|
|
|
2016-03-19 20:30:01 -07:00
|
|
|
// check if we should fallback if we expire in the coming day
|
|
|
|
|
if (!isExpiringSync(appDomain, 24 * 1)) return iteratorCallback();
|
2016-03-19 18:37:05 -07:00
|
|
|
|
|
|
|
|
debug('autoRenew: using fallback certs for %s since it expires soon', domain, error);
|
|
|
|
|
|
2016-03-19 13:54:52 -07:00
|
|
|
certFilePath = 'cert/host.cert';
|
|
|
|
|
keyFilePath = 'cert/host.key';
|
2016-03-18 23:26:57 -07:00
|
|
|
} else {
|
2016-03-17 12:20:02 -07:00
|
|
|
debug('autoRenew: certificate for %s renewed', domain);
|
2015-12-19 13:47:48 -08:00
|
|
|
|
2016-03-19 13:54:52 -07:00
|
|
|
certFilePath = path.join(paths.APP_CERTS_DIR, domain + '.cert');
|
|
|
|
|
keyFilePath = path.join(paths.APP_CERTS_DIR, domain + '.key');
|
|
|
|
|
}
|
2016-03-18 23:26:57 -07:00
|
|
|
|
2016-03-19 13:54:52 -07:00
|
|
|
// reconfigure and reload nginx. this is required for the case where we got a renewed cert after fallback
|
|
|
|
|
var configureFunc = app.location === constants.ADMIN_LOCATION ?
|
|
|
|
|
nginx.configureAdmin.bind(null, certFilePath, keyFilePath)
|
|
|
|
|
: nginx.configureApp.bind(null, app, certFilePath, keyFilePath);
|
2016-03-19 12:54:11 -07:00
|
|
|
|
2016-03-19 13:54:52 -07:00
|
|
|
configureFunc(function (ignoredError) {
|
|
|
|
|
if (ignoredError) debug('fallbackExpiredCertificates: error reconfiguring app', ignoredError);
|
2016-03-18 23:26:57 -07:00
|
|
|
|
2016-03-19 13:54:52 -07:00
|
|
|
iteratorCallback(); // move to next app
|
|
|
|
|
});
|
|
|
|
|
});
|
2015-12-14 12:40:39 -08:00
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
});
|
2015-12-10 13:31:47 -08:00
|
|
|
}
|
2015-12-10 20:31:38 -08:00
|
|
|
|
|
|
|
|
// note: https://tools.ietf.org/html/rfc4346#section-7.4.2 (certificate_list) requires that the
|
|
|
|
|
// servers certificate appears first (and not the intermediate cert)
|
|
|
|
|
function validateCertificate(cert, key, fqdn) {
|
|
|
|
|
assert(cert === null || typeof cert === 'string');
|
|
|
|
|
assert(key === null || typeof key === 'string');
|
|
|
|
|
assert.strictEqual(typeof fqdn, 'string');
|
|
|
|
|
|
|
|
|
|
if (cert === null && key === null) return null;
|
|
|
|
|
if (!cert && key) return new Error('missing cert');
|
|
|
|
|
if (cert && !key) return new Error('missing key');
|
|
|
|
|
|
|
|
|
|
var content;
|
|
|
|
|
try {
|
|
|
|
|
content = x509.parseCert(cert);
|
|
|
|
|
} catch (e) {
|
|
|
|
|
return new Error('invalid cert: ' + e.message);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// check expiration
|
|
|
|
|
if (content.notAfter < new Date()) return new Error('cert expired');
|
|
|
|
|
|
|
|
|
|
function matchesDomain(domain) {
|
|
|
|
|
if (domain === fqdn) return true;
|
|
|
|
|
if (domain.indexOf('*') === 0 && domain.slice(2) === fqdn.slice(fqdn.indexOf('.') + 1)) return true;
|
|
|
|
|
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// check domain
|
|
|
|
|
var domains = content.altNames.concat(content.subject.commonName);
|
|
|
|
|
if (!domains.some(matchesDomain)) return new Error(util.format('cert is not valid for this domain. Expecting %s in %j', fqdn, domains));
|
|
|
|
|
|
|
|
|
|
// http://httpd.apache.org/docs/2.0/ssl/ssl_faq.html#verify
|
|
|
|
|
var certModulus = safe.child_process.execSync('openssl x509 -noout -modulus', { encoding: 'utf8', input: cert });
|
|
|
|
|
var keyModulus = safe.child_process.execSync('openssl rsa -noout -modulus', { encoding: 'utf8', input: key });
|
|
|
|
|
if (certModulus !== keyModulus) return new Error('key does not match the cert');
|
|
|
|
|
|
|
|
|
|
return null;
|
|
|
|
|
}
|
2015-12-11 13:52:21 -08:00
|
|
|
|
2015-12-11 14:13:24 -08:00
|
|
|
function setFallbackCertificate(cert, key, callback) {
|
2015-12-11 13:52:21 -08:00
|
|
|
assert.strictEqual(typeof cert, 'string');
|
|
|
|
|
assert.strictEqual(typeof key, 'string');
|
|
|
|
|
assert.strictEqual(typeof callback, 'function');
|
|
|
|
|
|
|
|
|
|
var error = validateCertificate(cert, key, '*.' + config.fqdn());
|
|
|
|
|
if (error) return callback(new CertificatesError(CertificatesError.INVALID_CERT, error.message));
|
|
|
|
|
|
|
|
|
|
// backup the cert
|
|
|
|
|
if (!safe.fs.writeFileSync(path.join(paths.APP_CERTS_DIR, 'host.cert'), cert)) return callback(new CertificatesError(CertificatesError.INTERNAL_ERROR, safe.error.message));
|
|
|
|
|
if (!safe.fs.writeFileSync(path.join(paths.APP_CERTS_DIR, 'host.key'), key)) return callback(new CertificatesError(CertificatesError.INTERNAL_ERROR, safe.error.message));
|
|
|
|
|
|
|
|
|
|
// copy over fallback cert
|
|
|
|
|
if (!safe.fs.writeFileSync(path.join(paths.NGINX_CERT_DIR, 'host.cert'), cert)) return callback(new CertificatesError(CertificatesError.INTERNAL_ERROR, safe.error.message));
|
|
|
|
|
if (!safe.fs.writeFileSync(path.join(paths.NGINX_CERT_DIR, 'host.key'), key)) return callback(new CertificatesError(CertificatesError.INTERNAL_ERROR, safe.error.message));
|
|
|
|
|
|
2015-12-11 14:37:55 -08:00
|
|
|
nginx.reload(function (error) {
|
2015-12-11 13:52:21 -08:00
|
|
|
if (error) return callback(new CertificatesError(CertificatesError.INTERNAL_ERROR, error));
|
|
|
|
|
|
|
|
|
|
return callback(null);
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function setAdminCertificate(cert, key, callback) {
|
|
|
|
|
assert.strictEqual(typeof cert, 'string');
|
|
|
|
|
assert.strictEqual(typeof key, 'string');
|
|
|
|
|
assert.strictEqual(typeof callback, 'function');
|
|
|
|
|
|
|
|
|
|
var vhost = config.appFqdn(constants.ADMIN_LOCATION);
|
|
|
|
|
var certFilePath = path.join(paths.APP_CERTS_DIR, vhost + '.cert');
|
|
|
|
|
var keyFilePath = path.join(paths.APP_CERTS_DIR, vhost + '.key');
|
|
|
|
|
|
|
|
|
|
var error = validateCertificate(cert, key, vhost);
|
|
|
|
|
if (error) return callback(new CertificatesError(CertificatesError.INVALID_CERT, error.message));
|
|
|
|
|
|
|
|
|
|
// backup the cert
|
|
|
|
|
if (!safe.fs.writeFileSync(certFilePath, cert)) return callback(new CertificatesError(CertificatesError.INTERNAL_ERROR, safe.error.message));
|
|
|
|
|
if (!safe.fs.writeFileSync(keyFilePath, key)) return callback(new CertificatesError(CertificatesError.INTERNAL_ERROR, safe.error.message));
|
|
|
|
|
|
2015-12-11 14:37:55 -08:00
|
|
|
nginx.configureAdmin(certFilePath, keyFilePath, callback);
|
2015-12-11 13:52:21 -08:00
|
|
|
}
|
2015-12-11 14:15:23 -08:00
|
|
|
|
|
|
|
|
function ensureCertificate(domain, callback) {
|
|
|
|
|
assert.strictEqual(typeof domain, 'string');
|
|
|
|
|
assert.strictEqual(typeof callback, 'function');
|
|
|
|
|
|
2015-12-14 12:38:19 -08:00
|
|
|
// check if user uploaded a specific cert. ideally, we should not mix user certs and automatic certs as we do here...
|
2016-03-19 12:50:31 -07:00
|
|
|
if (!isExpiringSync(domain, 24 * 5)) {
|
2016-03-19 13:22:38 -07:00
|
|
|
var certFilePath = path.join(paths.APP_CERTS_DIR, domain + '.cert');
|
|
|
|
|
var keyFilePath = path.join(paths.APP_CERTS_DIR, domain + '.key');
|
|
|
|
|
|
|
|
|
|
return callback(null, certFilePath, keyFilePath);
|
2015-12-14 12:38:19 -08:00
|
|
|
}
|
|
|
|
|
|
2016-03-19 12:50:31 -07:00
|
|
|
debug('ensureCertificate: %s cert require renewal', domain);
|
|
|
|
|
|
2015-12-17 13:17:46 -08:00
|
|
|
getApi(function (error, api, apiOptions) {
|
2015-12-14 12:38:19 -08:00
|
|
|
if (error) return callback(error);
|
2015-12-11 22:25:22 -08:00
|
|
|
|
2015-12-21 16:14:30 -08:00
|
|
|
debug('ensureCertificate: getting certificate for %s with options %j', domain, apiOptions);
|
2015-12-11 14:15:23 -08:00
|
|
|
|
2015-12-17 13:17:46 -08:00
|
|
|
api.getCertificate(domain, apiOptions, function (error, certFilePath, keyFilePath) {
|
2015-12-15 00:23:57 -08:00
|
|
|
if (error) {
|
|
|
|
|
debug('ensureCertificate: could not get certificate. using fallback certs', error);
|
|
|
|
|
return callback(null, 'cert/host.cert', 'cert/host.key'); // use fallback certs
|
|
|
|
|
}
|
2015-12-14 17:09:40 -08:00
|
|
|
|
|
|
|
|
callback(null, certFilePath, keyFilePath);
|
|
|
|
|
});
|
2015-12-11 14:15:23 -08:00
|
|
|
});
|
|
|
|
|
}
|