187 lines
7.3 KiB
JavaScript
187 lines
7.3 KiB
JavaScript
/* jslint node:true */
|
|
|
|
'use strict';
|
|
|
|
var acme = require('./cert/acme.js'),
|
|
assert = require('assert'),
|
|
caas = require('./cert/caas.js'),
|
|
config = require('./config.js'),
|
|
constants = require('./constants.js'),
|
|
debug = require('debug')('box:src/certificates'),
|
|
fs = require('fs'),
|
|
nginx = require('./nginx.js'),
|
|
path = require('path'),
|
|
paths = require('./paths.js'),
|
|
safe = require('safetydance'),
|
|
settings = require('./settings.js'),
|
|
sysinfo = require('./sysinfo.js'),
|
|
util = require('util'),
|
|
waitForDns = require('./waitfordns.js'),
|
|
x509 = require('x509');
|
|
|
|
exports = module.exports = {
|
|
installAdminCertificate: installAdminCertificate,
|
|
autoRenew: autoRenew,
|
|
setFallbackCertificate: setFallbackCertificate,
|
|
setAdminCertificate: setAdminCertificate,
|
|
CertificatesError: CertificatesError,
|
|
validateCertificate: validateCertificate,
|
|
ensureCertificate: ensureCertificate
|
|
};
|
|
|
|
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';
|
|
|
|
function installAdminCertificate(callback) {
|
|
settings.getTlsConfig(function (error, tlsConfig) {
|
|
if (error) return callback(error);
|
|
|
|
if (tlsConfig.provider === 'caas') return callback();
|
|
|
|
waitForDns(config.adminFqdn(), sysinfo.getIp(), config.fqdn(), function (error) {
|
|
if (error) return callback(error); // this cannot happen because we retry forever
|
|
|
|
ensureCertificate(config.adminFqdn(), function (error, certFilePath, keyFilePath) {
|
|
if (error) {
|
|
debug('Error obtaining certificate. Proceed anyway', error);
|
|
return callback();
|
|
}
|
|
|
|
nginx.configureAdmin(certFilePath, keyFilePath, callback);
|
|
});
|
|
});
|
|
});
|
|
}
|
|
|
|
function autoRenew() {
|
|
debug('will automatically renew certs');
|
|
}
|
|
|
|
// 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;
|
|
}
|
|
|
|
function setFallbackCertificate(cert, key, callback) {
|
|
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));
|
|
|
|
nginx.reload(function (error) {
|
|
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));
|
|
|
|
nginx.configureAdmin(certFilePath, keyFilePath, callback);
|
|
}
|
|
|
|
function ensureCertificate(domain, callback) {
|
|
assert.strictEqual(typeof domain, 'string');
|
|
assert.strictEqual(typeof callback, 'function');
|
|
|
|
settings.getTlsConfig(function (error, tlsConfig) {
|
|
if (error) return callback(error);
|
|
|
|
var api = tlsConfig.provider === 'caas' ? caas : acme;
|
|
|
|
var certFilePath = path.join(paths.APP_CERTS_DIR, domain + '.cert');
|
|
var keyFilePath = path.join(paths.APP_CERTS_DIR, domain + '.key');
|
|
|
|
if (fs.existsSync(certFilePath) && fs.existsSync(keyFilePath)) {
|
|
debug('ensureCertificate: %s. certificate already exists at %s', domain, certFilePath);
|
|
return callback(null, certFilePath, keyFilePath); // TODO: check if cert needs renewal
|
|
}
|
|
|
|
debug('Using %s to get certificate for %s', tlsConfig.provider, domain);
|
|
|
|
api.getCertificate(domain, paths.APP_CERTS_DIR, function (error) {
|
|
if (error) return callback(error);
|
|
|
|
callback(null, certFilePath, keyFilePath);
|
|
});
|
|
});
|
|
}
|