start moving openssl commands into openssl.js
This commit is contained in:
@@ -4,10 +4,6 @@ exports = module.exports = {
|
||||
setUserCertificate, // per location certificate
|
||||
setFallbackCertificate, // per domain certificate
|
||||
|
||||
generateFallbackCertificate,
|
||||
|
||||
validateCertificate,
|
||||
|
||||
getMailCertificate,
|
||||
getDirectoryServerCertificate,
|
||||
|
||||
@@ -41,7 +37,6 @@ const acme2 = require('./acme2.js'),
|
||||
blobs = require('./blobs.js'),
|
||||
BoxError = require('./boxerror.js'),
|
||||
constants = require('./constants.js'),
|
||||
crypto = require('node:crypto'),
|
||||
dashboard = require('./dashboard.js'),
|
||||
debug = require('debug')('box:reverseproxy'),
|
||||
dns = require('./dns.js'),
|
||||
@@ -54,7 +49,7 @@ const acme2 = require('./acme2.js'),
|
||||
Location = require('./location.js'),
|
||||
mailServer = require('./mailserver.js'),
|
||||
network = require('./network.js'),
|
||||
os = require('node:os'),
|
||||
openssl = require('./openssl.js'),
|
||||
path = require('node:path'),
|
||||
paths = require('./paths.js'),
|
||||
safe = require('safetydance'),
|
||||
@@ -72,36 +67,16 @@ function nginxLocation(s) {
|
||||
return `~ ^(?!(${re.slice(1)}))`; // negative regex assertion - https://stackoverflow.com/questions/16302897/nginx-location-not-equal-to-regex
|
||||
}
|
||||
|
||||
async function getCertificateDates(cert) {
|
||||
assert.strictEqual(typeof cert, 'string');
|
||||
|
||||
const [error, result] = await safe(shell.spawn('openssl', ['x509', '-startdate', '-enddate', '-subject', '-noout'], { encoding: 'utf8', input: cert }));
|
||||
if (error) return { startDate: null, endDate: null } ; // some error
|
||||
|
||||
const lines = result.trim().split('\n');
|
||||
const notBefore = lines[0].split('=')[1];
|
||||
const notBeforeDate = new Date(notBefore);
|
||||
|
||||
const notAfter = lines[1].split('=')[1];
|
||||
const notAfterDate = new Date(notAfter);
|
||||
|
||||
const daysLeft = (notAfterDate - new Date())/(24 * 60 * 60 * 1000);
|
||||
debug(`expiryDate: ${lines[2]} notBefore=${notBefore} notAfter=${notAfter} daysLeft=${daysLeft}`);
|
||||
|
||||
return { startDate: notBeforeDate, endDate: notAfterDate };
|
||||
}
|
||||
|
||||
// checks if the certificate matches the options provided by user (like wildcard, le-staging etc)
|
||||
async function providerMatches(domainObject, cert) {
|
||||
assert.strictEqual(typeof domainObject, 'object');
|
||||
assert.strictEqual(typeof cert, 'string');
|
||||
|
||||
const [error, subjectAndIssuer] = await safe(shell.spawn('openssl', ['x509', '-noout', '-subject', '-issuer'], { encoding: 'utf8', input: cert }));
|
||||
const [error, subjectAndIssuer] = await safe(openssl.getSubjectAndIssuer(cert));
|
||||
if (error) return false; // something bad happenned
|
||||
|
||||
const subject = subjectAndIssuer.match(/^subject=(.*)$/m)[1];
|
||||
const { subject, issuer } = subjectAndIssuer;
|
||||
const domain = subject.substr(subject.indexOf('=') + 1).trim(); // subject can be /CN=, CN=, CN = and other forms
|
||||
const issuer = subjectAndIssuer.match(/^issuer=(.*)$/m)[1];
|
||||
const isWildcardCert = domain.includes('*');
|
||||
const isLetsEncryptProd = issuer.includes('Let\'s Encrypt') && !issuer.includes('STAGING');
|
||||
|
||||
@@ -121,41 +96,6 @@ async function providerMatches(domainObject, cert) {
|
||||
return !mismatch;
|
||||
}
|
||||
|
||||
// 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)
|
||||
async function validateCertificate(subdomain, domain, certificate) {
|
||||
assert.strictEqual(typeof subdomain, 'string');
|
||||
assert.strictEqual(typeof domain, 'string');
|
||||
assert(certificate && typeof certificate, 'object');
|
||||
|
||||
const { cert, key } = certificate;
|
||||
|
||||
// check for empty cert and key strings
|
||||
if (!cert && key) throw new BoxError(BoxError.BAD_FIELD, 'missing cert');
|
||||
if (cert && !key) throw new BoxError(BoxError.BAD_FIELD, 'missing key');
|
||||
|
||||
// -checkhost checks for SAN or CN exclusively. SAN takes precedence and if present, ignores the CN.
|
||||
const fqdn = dns.fqdn(subdomain, domain);
|
||||
|
||||
const [checkHostError, checkHostOutput] = await safe(shell.spawn('openssl', ['x509', '-noout', '-checkhost', fqdn], { encoding: 'utf8', input: cert }));
|
||||
if (checkHostError) throw new BoxError(BoxError.BAD_FIELD, 'Could not validate certificate');
|
||||
if (checkHostOutput.indexOf('does match certificate') === -1) throw new BoxError(BoxError.BAD_FIELD, `Certificate is not valid for this domain. Expecting ${fqdn}`);
|
||||
|
||||
// check if public key in the cert and private key matches. pkey below works for RSA and ECDSA keys
|
||||
const [pubKeyError1, pubKeyFromCert] = await safe(shell.spawn('openssl', ['x509', '-noout', '-pubkey'], { encoding: 'utf8', input: cert }));
|
||||
if (pubKeyError1) throw new BoxError(BoxError.BAD_FIELD, 'Could not get public key from cert');
|
||||
const [pubKeyError2, pubKeyFromKey] = await safe(shell.spawn('openssl', ['pkey', '-pubout'], { encoding: 'utf8', input: key }));
|
||||
if (pubKeyError2) throw new BoxError(BoxError.BAD_FIELD, 'Could not get public key from private key');
|
||||
|
||||
if (pubKeyFromCert !== pubKeyFromKey) throw new BoxError(BoxError.BAD_FIELD, 'Public key does not match the certificate.');
|
||||
|
||||
// check expiration
|
||||
const [error] = await safe(shell.spawn('openssl', ['x509', '-checkend', '0'], { input: cert }));
|
||||
if (error) throw new BoxError(BoxError.BAD_FIELD, 'Certificate has expired');
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
async function notifyCertChange() {
|
||||
await mailServer.checkCertificate();
|
||||
await shell.sudo([ RESTART_SERVICE_CMD, 'box' ], {}); // directory server
|
||||
@@ -172,37 +112,6 @@ async function reload() {
|
||||
if (error) throw new BoxError(BoxError.NGINX_ERROR, `Error reloading nginx: ${error.message}`);
|
||||
}
|
||||
|
||||
// this is used in migration - 20211006200150-domains-ensure-fallbackCertificate.js
|
||||
async function generateFallbackCertificate(domain) {
|
||||
assert.strictEqual(typeof domain, 'string');
|
||||
|
||||
const certFilePath = path.join(os.tmpdir(), `${domain}-${crypto.randomBytes(4).readUInt32LE(0)}.cert`);
|
||||
const keyFilePath = path.join(os.tmpdir(), `${domain}-${crypto.randomBytes(4).readUInt32LE(0)}.key`);
|
||||
|
||||
const opensslConf = safe.fs.readFileSync('/etc/ssl/openssl.cnf', 'utf8');
|
||||
const cn = domain;
|
||||
|
||||
debug(`generateFallbackCertificate: domain=${domain} cn=${cn}`);
|
||||
|
||||
// SAN must contain all the domains since CN check is based on implementation if SAN is found. -checkhost also checks only SAN if present!
|
||||
const opensslConfWithSan = `${opensslConf}\n[SAN]\nsubjectAltName=DNS:${domain},DNS:*.${cn}\n`;
|
||||
const configFile = path.join(os.tmpdir(), 'openssl-' + crypto.randomBytes(4).readUInt32LE(0) + '.conf');
|
||||
safe.fs.writeFileSync(configFile, opensslConfWithSan, 'utf8');
|
||||
// the days field is chosen to be less than 825 days per apple requirement (https://support.apple.com/en-us/HT210176)
|
||||
await shell.spawn('openssl', ['req', '-x509', '-newkey', 'rsa:2048', '-keyout', keyFilePath, '-out', certFilePath, '-days', '800', '-subj', `/CN=*.${cn}`, '-extensions', 'SAN', '-config', configFile, '-nodes'], { encoding: 'utf8 '});
|
||||
safe.fs.unlinkSync(configFile);
|
||||
|
||||
const cert = safe.fs.readFileSync(certFilePath, 'utf8');
|
||||
if (!cert) throw new BoxError(BoxError.FS_ERROR, safe.error.message);
|
||||
safe.fs.unlinkSync(certFilePath);
|
||||
|
||||
const key = safe.fs.readFileSync(keyFilePath, 'utf8');
|
||||
if (!key) throw new BoxError(BoxError.FS_ERROR, safe.error.message);
|
||||
safe.fs.unlinkSync(keyFilePath);
|
||||
|
||||
return { cert, key };
|
||||
}
|
||||
|
||||
async function setFallbackCertificate(domain, certificate) {
|
||||
assert.strictEqual(typeof domain, 'string');
|
||||
assert(certificate && typeof certificate === 'object');
|
||||
@@ -250,7 +159,7 @@ async function needsRenewal(cert, options) {
|
||||
assert.strictEqual(typeof cert, 'string');
|
||||
assert.strictEqual(typeof options, 'object');
|
||||
|
||||
const { startDate, endDate } = await getCertificateDates(cert);
|
||||
const { startDate, endDate } = await openssl.getCertificateDates(cert);
|
||||
const now = new Date();
|
||||
|
||||
let isExpiring;
|
||||
@@ -387,6 +296,19 @@ async function writeCertificate(location) {
|
||||
return { certFilePath, keyFilePath };
|
||||
}
|
||||
|
||||
async function getKey(certName) {
|
||||
assert.strictEqual(typeof certName, 'string');
|
||||
|
||||
const key = await blobs.getString(`${blobs.CERT_PREFIX}-${certName}.key`);
|
||||
if (key) {
|
||||
debug(`ensureKey: reuse existing key for ${certName}`);
|
||||
return key;
|
||||
}
|
||||
|
||||
debug(`ensureKey: generating new key for ${certName}`);
|
||||
return await openssl.generateKey('secp256r1');
|
||||
};
|
||||
|
||||
async function ensureCertificate(location, options, auditSource) {
|
||||
assert.strictEqual(typeof location, 'object');
|
||||
assert.strictEqual(typeof options, 'object');
|
||||
@@ -408,7 +330,7 @@ async function ensureCertificate(location, options, auditSource) {
|
||||
}
|
||||
|
||||
const certName = getAcmeCertificateNameSync(fqdn, domainObject);
|
||||
const key = await blobs.getString(`${blobs.CERT_PREFIX}-${certName}.key`);
|
||||
const key = await getKey(certName); // generates one on the fly. we only save the key in db if we end up using it
|
||||
const cert = await blobs.getString(`${blobs.CERT_PREFIX}-${certName}.cert`);
|
||||
|
||||
if (key && cert) {
|
||||
@@ -422,9 +344,9 @@ async function ensureCertificate(location, options, auditSource) {
|
||||
}
|
||||
|
||||
debug(`ensureCertificate: ${fqdn} needs acme cert`);
|
||||
const [error, result] = await safe(acme2.getCertificate(fqdn, domainObject));
|
||||
const [error, result] = await safe(acme2.getCertificate(fqdn, domainObject, key));
|
||||
|
||||
await blobs.setString(`${blobs.CERT_PREFIX}-${certName}.key`, result.key);
|
||||
await blobs.setString(`${blobs.CERT_PREFIX}-${certName}.key`, key);
|
||||
await blobs.setString(`${blobs.CERT_PREFIX}-${certName}.cert`, result.cert);
|
||||
await blobs.setString(`${blobs.CERT_PREFIX}-${certName}.csr`, result.csr);
|
||||
|
||||
@@ -626,7 +548,7 @@ async function cleanupCerts(locations, auditSource, progressCallback) {
|
||||
if (certNamesInUse.has(certName)) continue;
|
||||
|
||||
const cert = await blobs.getString(certId);
|
||||
const { endDate } = await getCertificateDates(cert);
|
||||
const { endDate } = await openssl.getCertificateDates(cert);
|
||||
if (!endDate) continue; // some error
|
||||
|
||||
if (now - endDate >= (60 * 60 * 24 * 30 * 6 * 1000)) { // expired 6 months ago and not in use
|
||||
@@ -731,8 +653,10 @@ async function writeDefaultConfig(options) {
|
||||
debug('writeDefaultConfig: create new cert');
|
||||
|
||||
const cn = 'cloudron-' + (new Date()).toISOString(); // randomize date a bit to keep firefox happy
|
||||
// the days field is chosen to be less than 825 days per apple requirement (https://support.apple.com/en-us/HT210176)
|
||||
await shell.spawn('openssl', ['req', '-x509', '-newkey', 'rsa:2048', '-keyout', keyFilePath, '-out', certFilePath, '-days', '800', '-subj', `/CN=${cn}`, '-nodes'], { encoding: 'utf8' });
|
||||
|
||||
const { cert, key } = await openssl.generateCertificate(cn);
|
||||
if (!safe.fs.writeFileSync(keyFilePath, key)) throw new BoxError(BoxError.FS_ERROR, safe.error);
|
||||
if (!safe.fs.writeFileSync(certFilePath, cert)) throw new BoxError(BoxError.FS_ERROR, safe.error);
|
||||
}
|
||||
|
||||
const data = {
|
||||
|
||||
Reference in New Issue
Block a user