diff --git a/migrations/20211006200150-domains-ensure-fallbackCertificate.js b/migrations/20211006200150-domains-ensure-fallbackCertificate.js index 3cd13d579..1e9d6113b 100644 --- a/migrations/20211006200150-domains-ensure-fallbackCertificate.js +++ b/migrations/20211006200150-domains-ensure-fallbackCertificate.js @@ -1,7 +1,7 @@ 'use strict'; const async = require('async'), - reverseProxy = require('../src/reverseproxy.js'), + openssl = require('../src/openssl.js'), safe = require('safetydance'); const NGINX_CERT_DIR = '/home/yellowtent/platformdata/nginx/cert'; @@ -18,7 +18,7 @@ exports.up = function(db, callback) { let fallbackCertificate = safe.JSON.parse(domain.fallbackCertificateJson); if (!fallbackCertificate || !fallbackCertificate.cert || !fallbackCertificate.key) { let error; - [error, fallbackCertificate] = await safe(reverseProxy.generateFallbackCertificate(domain.domain)); + [error, fallbackCertificate] = await safe(openssl.generateCertificate(domain.domain)); if (error) return iteratorDone(error); } diff --git a/src/acme2.js b/src/acme2.js index c454743e2..eec8a179b 100644 --- a/src/acme2.js +++ b/src/acme2.js @@ -14,8 +14,7 @@ const assert = require('node:assert'), crypto = require('node:crypto'), debug = require('debug')('box:cert/acme2'), dns = require('./dns.js'), - fs = require('node:fs'), - os = require('node:os'), + openssl = require('./openssl.js'), path = require('node:path'), paths = require('./paths.js'), promiseRetry = require('./promise-retry.js'), @@ -31,15 +30,17 @@ const CA_PROD_DIRECTORY_URL = 'https://acme-v02.api.letsencrypt.org/directory', // https://www.ietf.org/proceedings/92/slides/slides-92-acme-1.pdf // https://community.letsencrypt.org/t/list-of-client-implementations/2103 -function Acme2(fqdn, domainObject, email, options) { +function Acme2(fqdn, domainObject, email, key, options) { assert.strictEqual(typeof fqdn, 'string'); assert.strictEqual(typeof domainObject, 'object'); assert.strictEqual(typeof email, 'string'); + assert.strictEqual(typeof key, 'string'); assert.strictEqual(typeof options, 'object'); // { profile } this.fqdn = fqdn; this.accountKey = null; this.email = email; + this.key = key; this.accountKeyId = null; const prod = domainObject.tlsConfig.provider.match(/.*-prod/) !== null; // matches 'le-prod' or 'letsencrypt-prod' this.caDirectory = prod ? CA_PROD_DIRECTORY_URL : CA_STAGING_DIRECTORY_URL; @@ -74,15 +75,6 @@ function b64(str) { return urlBase64Encode(buf.toString('base64')); } -async function getModulus(pem) { - assert.strictEqual(typeof pem, 'string'); - - const stdout = await shell.spawn('openssl', ['rsa', '-modulus', '-noout'], { encoding: 'utf8', input: pem }); - const match = stdout.match(/Modulus=([0-9a-fA-F]+)$/m); - if (!match) throw new BoxError(BoxError.OPENSSL_ERROR, 'Could not get modulus'); - return Buffer.from(match[1], 'hex'); -} - Acme2.prototype.sendSignedRequest = async function (url, payload) { assert.strictEqual(typeof url, 'string'); assert.strictEqual(typeof payload, 'string'); @@ -101,7 +93,7 @@ Acme2.prototype.sendSignedRequest = async function (url, payload) { header.jwk = { e: b64(Buffer.from([0x01, 0x00, 0x01])), // exponent - 65537 kty: 'RSA', - n: b64(await getModulus(this.accountKey)) + n: b64(await openssl.getModulus(this.accountKey)) }; } @@ -155,11 +147,6 @@ Acme2.prototype.updateContact = async function (registrationUri) { debug(`updateContact: contact of user updated to ${this.email}`); }; -async function generateAccountKey() { - const acmeAccountKey = await shell.spawn('openssl', ['genrsa', '4096'], { encoding: 'utf8' }); - return acmeAccountKey; -} - Acme2.prototype.ensureAccount = async function () { const payload = { termsOfServiceAgreed: true @@ -170,14 +157,14 @@ Acme2.prototype.ensureAccount = async function () { this.accountKey = await blobs.getString(blobs.ACME_ACCOUNT_KEY); if (!this.accountKey) { debug('ensureAccount: generating new account keys'); - this.accountKey = await generateAccountKey(); + this.accountKey = await openssl.generateKey('rsa4096'); await blobs.setString(blobs.ACME_ACCOUNT_KEY, this.accountKey); } let result = await this.sendSignedRequest(this.directory.newAccount, JSON.stringify(payload)); if (result.status === 403 && result.body.type === 'urn:ietf:params:acme:error:unauthorized') { debug(`ensureAccount: key was revoked. ${result.status} ${result.text}. generating new account key`); - this.accountKey = await generateAccountKey(); + this.accountKey = await openssl.generateKey('rsa4096'); await blobs.setString(blobs.ACME_ACCOUNT_KEY, this.accountKey); result = await this.sendSignedRequest(this.directory.newAccount, JSON.stringify(payload)); } @@ -246,7 +233,7 @@ Acme2.prototype.getKeyAuthorization = async function (token) { const jwk = { e: b64(Buffer.from([0x01, 0x00, 0x01])), // Exponent - 65537 kty: 'RSA', - n: b64(await getModulus(this.accountKey)) + n: b64(await openssl.getModulus(this.accountKey)) }; const shasum = crypto.createHash('sha256'); @@ -298,7 +285,7 @@ Acme2.prototype.signCertificate = async function (finalizationUrl, csrPem) { assert.strictEqual(typeof finalizationUrl, 'string'); assert.strictEqual(typeof csrPem, 'string'); - const csrDer = await shell.spawn('openssl', ['req', '-inform', 'pem', '-outform', 'der'], { input: csrPem }); + const csrDer = await openssl.pemToDer(csrPem); const payload = { csr: b64(csrDer) @@ -311,47 +298,6 @@ Acme2.prototype.signCertificate = async function (finalizationUrl, csrPem) { if (result.status !== 200) throw new BoxError(BoxError.ACME_ERROR, `Failed to sign certificate. Expecting 200, got ${result.status} ${result.text}`); }; -Acme2.prototype.ensureKey = async function () { - const key = await blobs.getString(`${blobs.CERT_PREFIX}-${this.certName}.key`); - if (key) { - debug(`ensureKey: reuse existing key for ${this.cn}`); - return key; - } - - debug(`ensureKey: generating new key for ${this.cn}`); - // same as prime256v1. openssl ecparam -list_curves. we used to use secp384r1 but it doesn't seem to be accepted by few mail servers - const newKey = await shell.spawn('openssl', ['ecparam', '-genkey', '-name', 'secp256r1'], { encoding: 'utf8' }); - return newKey; -}; - -Acme2.prototype.createCsr = async function (key) { - assert.strictEqual(typeof key, 'string'); - - const [error, tmpdir] = await safe(fs.promises.mkdtemp(path.join(os.tmpdir(), 'acme-'))); - if (error) throw new BoxError(BoxError.FS_ERROR, `Error creating temporary directory for openssl config: ${error.message}`); - - const keyFilePath = path.join(tmpdir, 'key'); - if (!safe.fs.writeFileSync(keyFilePath, key)) throw new BoxError(BoxError.FS_ERROR, `Failed to write key file: ${safe.error.message}`); - - // we used to use -addext to the CLI to add these but that arg doesn't work on Ubuntu 16.04 - // empty distinguished_name section is required for Ubuntu 16 openssl - let conf = '[req]\nreq_extensions = v3_req\ndistinguished_name = req_distinguished_name\n' - + '[req_distinguished_name]\n\n' - + '[v3_req]\nbasicConstraints = CA:FALSE\nkeyUsage = nonRepudiation, digitalSignature, keyEncipherment\nsubjectAltName = @alt_names\n' - + '[alt_names]\n'; - - this.altNames.forEach((an, i) => conf += `DNS.${i+1} = ${an}\n`); - - const opensslConfigFile = path.join(tmpdir, 'openssl.conf'); - if (!safe.fs.writeFileSync(opensslConfigFile, conf)) throw new BoxError(BoxError.FS_ERROR, `Failed to write openssl config: ${safe.error.message}`); - - // while we pass the CN anyways, subjectAltName takes precedence - const csrPem = await shell.spawn('openssl', ['req', '-new', '-key', keyFilePath, '-outform', 'PEM', '-subj', `/CN=${this.cn}`, '-config', opensslConfigFile], { encoding: 'utf8' }); - await safe(fs.promises.rm(tmpdir, { recursive: true, force: true })); - debug(`createCsr: csr file created for ${this.cn}`); - return csrPem; // inspect with openssl req -text -noout -in hostname.csr -inform pem -}; - Acme2.prototype.downloadCertificate = async function (certUrl) { assert.strictEqual(typeof certUrl, 'string'); @@ -494,12 +440,11 @@ Acme2.prototype.acmeFlow = async function () { await safe(this.cleanupChallenge(cn, challenge), { debug }); } - const key = await this.ensureKey(); - const csr = await this.createCsr(key); + const csr = await openssl.createCsr(this.key, this.cn, this.altNames); await this.signCertificate(order.finalize, csr); const certUrl = await this.waitForOrder(orderUrl); const cert = await this.downloadCertificate(certUrl); - return { cert, key, csr }; + return { cert, csr }; }; Acme2.prototype.loadDirectory = async function () { @@ -528,9 +473,10 @@ Acme2.prototype.getCertificate = async function () { return result; }; -async function getCertificate(fqdn, domainObject) { +async function getCertificate(fqdn, domainObject, key) { assert.strictEqual(typeof fqdn, 'string'); // this can also be a wildcard domain (for alias domains) assert.strictEqual(typeof domainObject, 'object'); + assert.strictEqual(typeof key, 'string'); // 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. @@ -542,7 +488,7 @@ async function getCertificate(fqdn, domainObject) { return await promiseRetry({ times: 3, interval: 0, debug }, async function () { debug(`getCertificate: for fqdn ${fqdn} and domain ${domainObject.domain}`); - const acme = new Acme2(fqdn, domainObject, email, { /* profile: 'shortlived' */ }); + const acme = new Acme2(fqdn, domainObject, email, key, { /* profile: 'shortlived' */ }); return await acme.getCertificate(); }); } diff --git a/src/domains.js b/src/domains.js index af7ac0d05..d8aa7e640 100644 --- a/src/domains.js +++ b/src/domains.js @@ -27,6 +27,7 @@ const assert = require('node:assert'), eventlog = require('./eventlog.js'), mailServer = require('./mailserver.js'), notifications = require('./notifications.js'), + openssl = require('./openssl.js'), reverseProxy = require('./reverseproxy.js'), safe = require('safetydance'), tld = require('tldjs'), @@ -151,16 +152,16 @@ async function add(domain, data, auditSource) { let fallbackCertificate; if (data.fallbackCertificate) { - await reverseProxy.validateCertificate('test', domain, data.fallbackCertificate); + await openssl.validateCertificate('test', domain, data.fallbackCertificate); fallbackCertificate = data.fallbackCertificate; } else { - fallbackCertificate = await reverseProxy.generateFallbackCertificate(domain); + fallbackCertificate = await openssl.generateCertificate(domain); } let error = validateTlsConfig(tlsConfig, provider); if (error) throw error; - const dkimKey = await mailServer.generateDkimKey(); + const dkimKey = await openssl.generateDkimKey(); let dkimSelector = data.dkimSelector; if (!data.dkimSelector) { @@ -229,7 +230,7 @@ async function setConfig(domain, data, auditSource) { zoneName = domainObject.zoneName; } - if (fallbackCertificate) await reverseProxy.validateCertificate('test', domain, fallbackCertificate); + if (fallbackCertificate) await openssl.validateCertificate('test', domain, fallbackCertificate); const tlsConfigError = validateTlsConfig(tlsConfig, provider); if (tlsConfigError) throw tlsConfigError; diff --git a/src/mailserver.js b/src/mailserver.js index 010d98c90..a26306876 100644 --- a/src/mailserver.js +++ b/src/mailserver.js @@ -4,8 +4,6 @@ exports = module.exports = { restart, start, - generateDkimKey, - onDomainAdded, onDomainRemoved, @@ -25,7 +23,6 @@ exports = module.exports = { const assert = require('node:assert'), BoxError = require('./boxerror.js'), constants = require('./constants.js'), - crypto = require('node:crypto'), debug = require('debug')('box:mailserver'), dns = require('./dns.js'), docker = require('./docker.js'), @@ -37,8 +34,6 @@ const assert = require('node:assert'), Location = require('./location.js'), locks = require('./locks.js'), mail = require('./mail.js'), - os = require('node:os'), - path = require('node:path'), paths = require('./paths.js'), platform = require('./platform.js'), reverseProxy = require('./reverseproxy.js'), @@ -49,25 +44,6 @@ const assert = require('node:assert'), tasks = require('./tasks.js'), users = require('./users.js'); -async function generateDkimKey() { - const publicKeyFilePath = path.join(os.tmpdir(), `dkim-${crypto.randomBytes(4).readUInt32LE(0)}.public`); - const privateKeyFilePath = path.join(os.tmpdir(), `dkim-${crypto.randomBytes(4).readUInt32LE(0)}.private`); - - // https://www.unlocktheinbox.com/dkim-key-length-statistics/ and https://docs.aws.amazon.com/ses/latest/DeveloperGuide/send-email-authentication-dkim-easy.html for key size - await shell.spawn('openssl', ['genrsa', '-out', privateKeyFilePath, '1024'], {}); - await shell.spawn('openssl', ['rsa', '-in', privateKeyFilePath, '-out', publicKeyFilePath, '-pubout', '-outform', 'PEM'], {}); - - const publicKey = safe.fs.readFileSync(publicKeyFilePath, 'utf8'); - if (!publicKey) throw new BoxError(BoxError.FS_ERROR, safe.error.message); - safe.fs.unlinkSync(publicKeyFilePath); - - const privateKey = safe.fs.readFileSync(privateKeyFilePath, 'utf8'); - if (!privateKey) throw new BoxError(BoxError.FS_ERROR, safe.error.message); - safe.fs.unlinkSync(privateKeyFilePath); - - return { publicKey, privateKey }; -} - async function createMailConfig(mailFqdn) { assert.strictEqual(typeof mailFqdn, 'string'); diff --git a/src/openssl.js b/src/openssl.js new file mode 100644 index 000000000..71fe1b4a9 --- /dev/null +++ b/src/openssl.js @@ -0,0 +1,222 @@ +'use strict'; + +exports = module.exports = { + createCsr, + generateKey, + getModulus, + pemToDer, + getCertificateDates, + getSubjectAndIssuer, + generateCertificate, + hasExpired, + getPublicKey, + checkHost, + generateDkimKey, + generateDhparam, + validateCertificate +}; + +const assert = require('node:assert'), + BoxError = require('./boxerror.js'), + crypto = require('node:crypto'), + debug = require('debug')('box:openssl'), + fs = require('node:fs'), + os = require('node:os'), + path = require('node:path'), + safe = require('safetydance'), + shell = require('./shell.js')('openssl'); + +async function generateKey(certName, type) { + debug(`generateKey: generating new key for ${certName} ${type}`); + + if (type === 'rsa4096') { + return await shell.spawn('openssl', ['genrsa', '4096'], { encoding: 'utf8' }); + } else if (type === 'secp384r1') { + // secp384r1 is same as prime256v1. openssl ecparam -list_curves. we used to use secp384r1 but it doesn't seem to be accepted by few mail servers + return await shell.spawn('openssl', ['ecparam', '-genkey', '-name', type], { encoding: 'utf8' }); + } +} + +async function getModulus(pem) { + assert.strictEqual(typeof pem, 'string'); + + const stdout = await shell.spawn('openssl', ['rsa', '-modulus', '-noout'], { encoding: 'utf8', input: pem }); + const match = stdout.match(/Modulus=([0-9a-fA-F]+)$/m); + if (!match) throw new BoxError(BoxError.OPENSSL_ERROR, 'Could not get modulus'); + return Buffer.from(match[1], 'hex'); +} + +async function pemToDer(pem) { + assert.strictEqual(typeof pem, 'string'); + + return await shell.spawn('openssl', ['req', '-inform', 'pem', '-outform', 'der'], { input: pem }); +} + +async function createCsr(key, cn, altNames) { + assert.strictEqual(typeof key, 'string'); + + const [error, tmpdir] = await safe(fs.promises.mkdtemp(path.join(os.tmpdir(), 'acme-'))); + if (error) throw new BoxError(BoxError.FS_ERROR, `Error creating temporary directory for openssl config: ${error.message}`); + + const keyFilePath = path.join(tmpdir, 'key'); + if (!safe.fs.writeFileSync(keyFilePath, key)) throw new BoxError(BoxError.FS_ERROR, `Failed to write key file: ${safe.error.message}`); + + // we used to use -addext to the CLI to add these but that arg doesn't work on Ubuntu 16.04 + // empty distinguished_name section is required for Ubuntu 16 openssl + let conf = '[req]\nreq_extensions = v3_req\ndistinguished_name = req_distinguished_name\n' + + '[req_distinguished_name]\n\n' + + '[v3_req]\nbasicConstraints = CA:FALSE\nkeyUsage = nonRepudiation, digitalSignature, keyEncipherment\nsubjectAltName = @alt_names\n' + + '[alt_names]\n'; + + altNames.forEach((an, i) => conf += `DNS.${i+1} = ${an}\n`); + + const opensslConfigFile = path.join(tmpdir, 'openssl.conf'); + if (!safe.fs.writeFileSync(opensslConfigFile, conf)) throw new BoxError(BoxError.FS_ERROR, `Failed to write openssl config: ${safe.error.message}`); + + // while we pass the CN anyways, subjectAltName takes precedence + const csrPem = await shell.spawn('openssl', ['req', '-new', '-key', keyFilePath, '-outform', 'PEM', '-subj', `/CN=${cn}`, '-config', opensslConfigFile], { encoding: 'utf8' }); + await safe(fs.promises.rm(tmpdir, { recursive: true, force: true })); + debug(`createCsr: csr file created for ${cn}`); + return csrPem; // inspect with openssl req -text -noout -in hostname.csr -inform pem +}; + +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 }; +} + +async function getSubjectAndIssuer(cert) { + assert.strictEqual(typeof cert, 'string'); + + const subjectAndIssuer = await shell.spawn('openssl', ['x509', '-noout', '-subject', '-issuer'], { encoding: 'utf8', input: cert }); + const subject = subjectAndIssuer.match(/^subject=(.*)$/m)[1]; + const issuer = subjectAndIssuer.match(/^issuer=(.*)$/m)[1]; + + return { subject, issuer }; +} + +// this is used in migration - 20211006200150-domains-ensure-fallbackCertificate.js +async function generateCertificate(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(`generateCertificate: 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 hasExpired(cert) { + const [error] = await safe(shell.spawn('openssl', ['x509', '-checkend', '0'], { input: cert })); + return !!error; +} + +async function getPublicKey(pem, type) { + if (type === 'cert') { + const [pubKeyError1, pubKeyFromCert] = await safe(shell.spawn('openssl', ['x509', '-noout', '-pubkey'], { encoding: 'utf8', input: pem })); + if (pubKeyError1) throw new BoxError(BoxError.BAD_FIELD, 'Could not get public key from cert'); + return pubKeyFromCert; + } else if (type === 'key') { + const [pubKeyError2, pubKeyFromKey] = await safe(shell.spawn('openssl', ['pkey', '-pubout'], { encoding: 'utf8', input: pem })); + if (pubKeyError2) throw new BoxError(BoxError.BAD_FIELD, 'Could not get public key from private key'); + return pubKeyFromKey; + } +} + +async function checkHost(cert, fqdn) { + const checkHostOutput = await shell.spawn('openssl', ['x509', '-noout', '-checkhost', fqdn], { encoding: 'utf8', input: cert }); + return checkHostOutput.indexOf('does match certificate') !== -1; +} + +async function generateDkimKey() { + const publicKeyFilePath = path.join(os.tmpdir(), `dkim-${crypto.randomBytes(4).readUInt32LE(0)}.public`); + const privateKeyFilePath = path.join(os.tmpdir(), `dkim-${crypto.randomBytes(4).readUInt32LE(0)}.private`); + + // https://www.unlocktheinbox.com/dkim-key-length-statistics/ and https://docs.aws.amazon.com/ses/latest/DeveloperGuide/send-email-authentication-dkim-easy.html for key size + await shell.spawn('openssl', ['genrsa', '-out', privateKeyFilePath, '1024'], {}); + await shell.spawn('openssl', ['rsa', '-in', privateKeyFilePath, '-out', publicKeyFilePath, '-pubout', '-outform', 'PEM'], {}); + + const publicKey = safe.fs.readFileSync(publicKeyFilePath, 'utf8'); + if (!publicKey) throw new BoxError(BoxError.FS_ERROR, safe.error.message); + safe.fs.unlinkSync(publicKeyFilePath); + + const privateKey = safe.fs.readFileSync(privateKeyFilePath, 'utf8'); + if (!privateKey) throw new BoxError(BoxError.FS_ERROR, safe.error.message); + safe.fs.unlinkSync(privateKeyFilePath); + + return { publicKey, privateKey }; +} + +async function generateDhparam() { + debug('generateDhparam: generating dhparams'); + return await shell.spawn('openssl', ['dhparam', '-dsaparam', '2048'], { encoding: 'utf8' }); +} + +// 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 = subdomain + (subdomain ? '.' : '') + domain; + + const [checkHostError, match] = await safe(checkHost(cert, fqdn)); + if (checkHostError) throw new BoxError(BoxError.BAD_FIELD, 'Could not validate certificate'); + if (!match) 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(getPublicKey(cert, 'cert')); + if (pubKeyError1) throw new BoxError(BoxError.BAD_FIELD, 'Could not get public key from cert'); + const [pubKeyError2, pubKeyFromKey] = await safe(getPublicKey(key, '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 expired = await hasExpired(cert); + if (expired) throw new BoxError(BoxError.BAD_FIELD, 'Certificate has expired'); + + return null; +} diff --git a/src/provision.js b/src/provision.js index e2c04f4f4..9873a851e 100644 --- a/src/provision.js +++ b/src/provision.js @@ -24,10 +24,10 @@ const appstore = require('./appstore.js'), mailServer = require('./mailserver.js'), network = require('./network.js'), oidcClients = require('./oidcclients.js'), + openssl = require('./openssl.js'), platform = require('./platform.js'), reverseProxy = require('./reverseproxy.js'), safe = require('safetydance'), - shell = require('./shell.js')('provision'), semver = require('semver'), paths = require('./paths.js'), system = require('./system.js'), @@ -64,7 +64,7 @@ function setProgress(task, message) { async function ensureDhparams() { if (fs.existsSync(paths.DHPARAMS_FILE)) return; debug('ensureDhparams: generating dhparams'); - const dhparams = await shell.spawn('openssl', ['dhparam', '-dsaparam', '2048'], { encoding: 'utf8' }); + const dhparams = await openssl.generateDhparam(); if (!safe.fs.writeFileSync(paths.DHPARAMS_FILE, dhparams)) throw new BoxError(BoxError.FS_ERROR, `Could not save dhparams.pem: ${safe.error.message}`); } diff --git a/src/reverseproxy.js b/src/reverseproxy.js index 6fb371769..38ab45d12 100644 --- a/src/reverseproxy.js +++ b/src/reverseproxy.js @@ -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 = { diff --git a/src/test/openssl-test.js b/src/test/openssl-test.js new file mode 100644 index 000000000..46e302a3a --- /dev/null +++ b/src/test/openssl-test.js @@ -0,0 +1,132 @@ +/* global it:false */ +/* global describe:false */ + +'use strict'; + +const BoxError = require('../boxerror.js'), + expect = require('expect.js'), + openssl = require('../openssl.js'), + safe = require('safetydance'); + +describe('openssl', function () { + describe('validateCertificate', function () { + const foobarDomain = 'foobar.com'; + const amazingDomain = 'amazing.com'; + /* + Generate these with: + openssl genrsa -out server.key 512 + openssl req -new -key server.key -out server.csr -subj "/C=DE/ST=Berlin/L=Berlin/O=Nebulon/OU=CTO/CN=baz.foobar.com" + openssl x509 -req -days 3650 -in server.csr -signkey server.key -out server.crt + */ + + // foobar.com + const validCert0 = '-----BEGIN CERTIFICATE-----\nMIIBxTCCAW8CFBVWRFizZeUIdp94/l9Qx/+7UM4GMA0GCSqGSIb3DQEBCwUAMGQx\nCzAJBgNVBAYTAkRFMQ8wDQYDVQQIDAZCZXJsaW4xDzANBgNVBAcMBkJlcmxpbjEQ\nMA4GA1UECgwHTmVidWxvbjEMMAoGA1UECwwDQ1RPMRMwEQYDVQQDDApmb29iYXIu\nY29tMB4XDTIwMTEyMjAxNTI0M1oXDTMwMTEyMDAxNTI0M1owZDELMAkGA1UEBhMC\nREUxDzANBgNVBAgMBkJlcmxpbjEPMA0GA1UEBwwGQmVybGluMRAwDgYDVQQKDAdO\nZWJ1bG9uMQwwCgYDVQQLDANDVE8xEzARBgNVBAMMCmZvb2Jhci5jb20wXDANBgkq\nhkiG9w0BAQEFAANLADBIAkEA++BvW/oDsaM57d4Q4GQjkUzjB0/glKLj4P0Y8InS\nhLHOud9Uxz7dIcqHm9x9MOtqTRhtiHNoFLZLsU3a3upr2QIDAQABMA0GCSqGSIb3\nDQEBCwUAA0EAy9Acsgr/lH1rrE8DZov7dvvNjExkC+VO0kujO25aQIGBAtzLp9MG\nEblQ3ZXMBSX4b/nLMjOH8Xr4ZA0GUDgdew==\n-----END CERTIFICATE-----'; + const validKey0 = '-----BEGIN RSA PRIVATE KEY-----\nMIIBPAIBAAJBAPvgb1v6A7GjOe3eEOBkI5FM4wdP4JSi4+D9GPCJ0oSxzrnfVMc+\n3SHKh5vcfTDrak0YbYhzaBS2S7FN2t7qa9kCAwEAAQJBALsBjWyKmcd/2vjCkWEo\nuEefAEhjg+iXb/2RrLyad1TQfgs35UfigcjpWbzT2ScpFZT61ng6hKmclt2OCT9F\nBKECIQD/bjRbGiPq762ikWkfvalgkAAhSoXo2AcD/MsrhWyyPQIhAPxwM7jZRNvO\nng3TJaAgISwwUC9vuaNJQ06Yt02pvoXNAiEAuQipTrGCAWe8vb5ei8rFzxihr3wf\nw0vy0RWoTA+sbPUCIHDFOwXf4bgEJG1unwdacxdHefrHAXold3D8Hh8OrnMdAiEA\nov6sW0C1+maNpoWC+moDGFdImZnej2SDIB5976akWVo=\n-----END RSA PRIVATE KEY-----'; + + // *.foobar.com + const validCert1 = '-----BEGIN CERTIFICATE-----\nMIIByTCCAXMCFEXpWxabfp9Nybi7akGuxKlXdQVsMA0GCSqGSIb3DQEBCwUAMGYx\nCzAJBgNVBAYTAkRFMQ8wDQYDVQQIDAZCZXJsaW4xDzANBgNVBAcMBkJlcmxpbjEQ\nMA4GA1UECgwHTmVidWxvbjEMMAoGA1UECwwDQ1RPMRUwEwYDVQQDDAwqLmZvb2Jh\nci5jb20wHhcNMjAxMTIyMDIxNjQzWhcNMzAxMTIwMDIxNjQzWjBmMQswCQYDVQQG\nEwJERTEPMA0GA1UECAwGQmVybGluMQ8wDQYDVQQHDAZCZXJsaW4xEDAOBgNVBAoM\nB05lYnVsb24xDDAKBgNVBAsMA0NUTzEVMBMGA1UEAwwMKi5mb29iYXIuY29tMFww\nDQYJKoZIhvcNAQEBBQADSwAwSAJBAKMUYf86EG+J6ughAvhKGbIIyOpB3XqnK6KV\nM+r2/DvFx2KGIew7KopkzM2+UThDWE2YTcgL5846QRbx+K5NAXECAwEAATANBgkq\nhkiG9w0BAQsFAANBAJrX5wdszGt0lhDx0w2saJtTM3A6AfYdI7F37rgnvQKwRA0u\nTlN9Ekp4HbZsRi36g3W9zl6nWa3/HWbnBiRNuXk=\n-----END CERTIFICATE-----\n'; + const validKey1 = '-----BEGIN RSA PRIVATE KEY-----\nMIIBOQIBAAJBAKMUYf86EG+J6ughAvhKGbIIyOpB3XqnK6KVM+r2/DvFx2KGIew7\nKopkzM2+UThDWE2YTcgL5846QRbx+K5NAXECAwEAAQJAL/m/GqaqTyXzxXZwuTqT\ndJzA/qmBzqN/YsUiEO24Jp0AVuERlgiKBbxpu0xp8EpDsLTEt6TWWy1p0HIH6e0j\nAQIhANIZkHD6gVxvAMz0tquSprBnylqHngdT/PymDEHHNPv1AiEAxrUTvxV+vmii\n5CCLFTnYTQliKr+PC5qxn2WxV1rPng0CIGTiS55EW0t0LbE8rF40XAAGxn6z8ijY\npnj2jpojOojlAiBoaA6XEXFGFO651QufPISVfb+x3HMJ0t9PdHxo/NMoJQIgbVUh\naQKzUcrgIM2nbg4fLp3+VAh0ZkxNwaeKcsZz0cQ=\n-----END RSA PRIVATE KEY-----\n'; + + // baz.foobar.com + const validCert2 = '-----BEGIN CERTIFICATE-----\nMIIBzTCCAXcCFDnT77DcYIJ5EtAjyN7125yvCVtKMA0GCSqGSIb3DQEBCwUAMGgx\nCzAJBgNVBAYTAkRFMQ8wDQYDVQQIDAZCZXJsaW4xDzANBgNVBAcMBkJlcmxpbjEQ\nMA4GA1UECgwHTmVidWxvbjEMMAoGA1UECwwDQ1RPMRcwFQYDVQQDDA5iYXouZm9v\nYmFyLmNvbTAeFw0yNDEyMDQxNTMwMzRaFw0zNDEyMDIxNTMwMzRaMGgxCzAJBgNV\nBAYTAkRFMQ8wDQYDVQQIDAZCZXJsaW4xDzANBgNVBAcMBkJlcmxpbjEQMA4GA1UE\nCgwHTmVidWxvbjEMMAoGA1UECwwDQ1RPMRcwFQYDVQQDDA5iYXouZm9vYmFyLmNv\nbTBcMA0GCSqGSIb3DQEBAQUAA0sAMEgCQQC3PwF9imDPxtYTEbg+Grz5GSgc6Rqk\nNsMOyeOLDmEF+KiXf1iHZ+6dlXNeE8ULCnTomQ8KQ3VGKT596fqDF+pzAgMBAAEw\nDQYJKoZIhvcNAQELBQADQQCuzLEHQIG5kiWL7gkLMHURCkNqLrxEQmhm3RVNkeGZ\n6GXhS3qKc4PEl6kxK6XZXI/OPimmMGF+CKpJNpx88xMm\n-----END CERTIFICATE-----\n'; + const validKey2 = '-----BEGIN PRIVATE KEY-----\nMIIBVwIBADANBgkqhkiG9w0BAQEFAASCAUEwggE9AgEAAkEAtz8BfYpgz8bWExG4\nPhq8+RkoHOkapDbDDsnjiw5hBfiol39Yh2funZVzXhPFCwp06JkPCkN1Rik+fen6\ngxfqcwIDAQABAkEAgEXGkU6wfkG90RNyWWb2KZkGj1ZNo+4BlPSWJ90k5bv/ZGhN\n02H7ur2Lz8WOzrw/pQsK4g3xD5t+2I6RvzTywQIhANvdaeK13jI2822HHgMdY9Cp\nm3e69OxdPa2/t0+ExH7ZAiEA1VzlKtgI1j6gYQ70sa1N/dAsiFk8K2X7WuHKxeFh\n/CsCIQC7XQdD/OpabnupxfBshRovkqn7MWZRGvBZ5bvoVuNAmQIhAJoXW/6kRUWN\nt0BBj+EeO5xaEz9pyvXA0lZhiZN94ck/AiEAiLXiDwsUjOzcgGeX1wtn3fPlHZ1r\nbI3V4DXKe3AjUms=\n-----END PRIVATE KEY-----\n'; + + /* + Generate these with: + openssl ecparam -genkey -name prime256v1 -out server.key + openssl req -new -key server.key -out server.csr -subj "/C=DE/ST=Berlin/L=Berlin/O=Nebulon/OU=CTO/CN=*.foobar.com" + openssl req -x509 -sha256 -days 3650 -key server.key -in server.csr -out server.crt + */ + + // *.foobar.com + const validCert4 = '-----BEGIN CERTIFICATE-----\nMIICITCCAcegAwIBAgIUThSKBnGJ3TzM3ACzYQinCB5KS0QwCgYIKoZIzj0EAwIw\nZjELMAkGA1UEBhMCREUxDzANBgNVBAgMBkJlcmxpbjEPMA0GA1UEBwwGQmVybGlu\nMRAwDgYDVQQKDAdOZWJ1bG9uMQwwCgYDVQQLDANDVE8xFTATBgNVBAMMDCouZm9v\nYmFyLmNvbTAeFw0yMDExMjIwMTU5MjhaFw0zMDExMjAwMTU5MjhaMGYxCzAJBgNV\nBAYTAkRFMQ8wDQYDVQQIDAZCZXJsaW4xDzANBgNVBAcMBkJlcmxpbjEQMA4GA1UE\nCgwHTmVidWxvbjEMMAoGA1UECwwDQ1RPMRUwEwYDVQQDDAwqLmZvb2Jhci5jb20w\nWTATBgcqhkjOPQIBBggqhkjOPQMBBwNCAARXV7XqwL8dTTdKJ1sngAAgFXBmppsy\n5GLjm49GrDTB2ho6sjjwMUzKKP9jVCRrSlcKwmXNAy75/pPtLkL4A+s/o1MwUTAd\nBgNVHQ4EFgQUWajw1bCj16I+F8ZpjQEMnJb56XkwHwYDVR0jBBgwFoAUWajw1bCj\n16I+F8ZpjQEMnJb56XkwDwYDVR0TAQH/BAUwAwEB/zAKBggqhkjOPQQDAgNIADBF\nAiEA8eeVP+FvAfg4RVjH17DL/zPUBUIsmyTnPm9D7zIAdc0CICZYPU5qrAKA1h5U\n6+8vX4w+EuVQ8vjc8ATl7L/IKdmL\n-----END CERTIFICATE-----\n'; + const validKey4 = '-----BEGIN EC PARAMETERS-----\nBggqhkjOPQMBBw==\n-----END EC PARAMETERS-----\n-----BEGIN EC PRIVATE KEY-----\nMHcCAQEEIAczzARUd1L4KN/2Fl4s5kc1to6QzP9XGPCVLfQdtwbSoAoGCCqGSM49\nAwEHoUQDQgAEV1e16sC/HU03SidbJ4AAIBVwZqabMuRi45uPRqw0wdoaOrI48DFM\nyij/Y1Qka0pXCsJlzQMu+f6T7S5C+APrPw==\n-----END EC PRIVATE KEY-----\n'; + + // cp /etc/ssl/openssl.cnf /tmp/openssl.cnf + // echo -e "[SAN]\nsubjectAltName=DNS:amazing.com,DNS:*.amazing.com\n" >> /tmp/openssl.cnf + // openssl req -x509 -newkey rsa:2048 -keyout amazing.key -out amazing.crt -days 3650 -subj /CN=*.amazing.com -nodes -extensions SAN -config /tmp/openssl.cnf + const validCert3 = '-----BEGIN CERTIFICATE-----\nMIIC3DCCAcSgAwIBAgIJALcStAD5sDWEMA0GCSqGSIb3DQEBCwUAMBgxFjAUBgNV\nBAMMDSouYW1hemluZy5jb20wHhcNMTgwMjA5MjIxMzM2WhcNMjgwMjA3MjIxMzM2\nWjAYMRYwFAYDVQQDDA0qLmFtYXppbmcuY29tMIIBIjANBgkqhkiG9w0BAQEFAAOC\nAQ8AMIIBCgKCAQEAvp8dk13u4vmAfKfRNOO8+rVQ8q+vyR8scc9Euj0pTodLBflM\n2K6Zk0isirRzCL/jd4n1A6QrPeJ+r2J4xtHk2j+pavt8Sa2Go2MzpAe3OTuIqYJf\nUt7Im3f2Lb67itTPrpA2TR3A/dDFlazju+eBd3t3496Do8aBPpXAdOabfPsrv3nE\nx97vrr4tzeK3kG9u7GYuod5gyiwF2t5wSeMWbFk2oqkOCtHRXE77JDKVxIGiepnU\nTnkW9b7jIkiBQ1x0xHG4soewV2ymGHS2XrUHZ45FFMG7yVYpytKT9Iz9ty/z5VcL\nZ6NzgU/pKfQaIe8MpoDpVf5UNeB2DOAAEoJKKwIDAQABoykwJzAlBgNVHREEHjAc\nggthbWF6aW5nLmNvbYINKi5hbWF6aW5nLmNvbTANBgkqhkiG9w0BAQsFAAOCAQEA\nMULk6B9XrVPAole8W66o3WUUOrC7NVjbwZjr+Kp5oQTSo84qacaZS2C3ox/j/TZY\nUuNvoE6gIOHi+inN+G4P76K7NEvm8+Y1CeAyaPq01H4Qy2lk9F5wFMtPqvBZnF9C\nx1MvV30FruHXe5pDfnG1npKECpn2SgE3k6FRHM55u8rTMEm/O4TtsDq+fPqUvyWa\nZuRjPv4qVGGkoPyxA6iffxclpOAXs3JUgLcYoM2vxKC0YSOjHEa0p4uffX063Jgg\nybuy3OKvm+8L6moycX7J+LZK81dDTFDtF7PwrnRbpS4re0i/LSk23jDQvDOLnrAa\nSawRR8+1QHTENBo7dnP+NA==\n-----END CERTIFICATE-----'; + const validKey3 = '-----BEGIN PRIVATE KEY-----\nMIIEvAIBADANBgkqhkiG9w0BAQEFAASCBKYwggSiAgEAAoIBAQC+nx2TXe7i+YB8\np9E047z6tVDyr6/JHyxxz0S6PSlOh0sF+UzYrpmTSKyKtHMIv+N3ifUDpCs94n6v\nYnjG0eTaP6lq+3xJrYajYzOkB7c5O4ipgl9S3sibd/YtvruK1M+ukDZNHcD90MWV\nrOO754F3e3fj3oOjxoE+lcB05pt8+yu/ecTH3u+uvi3N4reQb27sZi6h3mDKLAXa\n3nBJ4xZsWTaiqQ4K0dFcTvskMpXEgaJ6mdROeRb1vuMiSIFDXHTEcbiyh7BXbKYY\ndLZetQdnjkUUwbvJVinK0pP0jP23L/PlVwtno3OBT+kp9Boh7wymgOlV/lQ14HYM\n4AASgkorAgMBAAECggEAdVSVLMcNqlGuv4vAHtDq2lpOaAKxrZbtkWPlxsisqzRl\nfljT7y+RQfHimkG16LXL+iFFWadsIlxOY/+1nZNGTPwQeNQwzVzs2ZbPC3DgW28E\nkGm56NVOHzu4oLGc2DhjWOxVMCRXTSN66sUPK/K0YunxgqXM2zrtBKvCWXI0VLlo\nN/UWAwHf4i0GWRl8u8PvxgMXlSW9p9l6gSsivWRMag9ADwRQ/NSKrRYkiOoRe3vz\nLxXARBvzeZXvOPVLGVRX4SIR7OmS8cC6Ol/rp1/ZFFID7aN+wdzphPSL1UNUriw4\nDv1mxz73SNakgeYSFBoWRS5BsJI01JoCoILsnhVCiQKBgQDyW+k5+j4K17fzwsmi\nyxZ0Nz/ncpkqxVrWYZM3pn7OVkb2NDArimEk53kmJ0hrT84kKJUYDx55R2TpnzpV\nMLmjxgs9TUrzZzsL/DP2ppkfE3OrPS+06OGa5GbURxD6KPvqDtOmU3oFyJ3f4YJR\nVK7RW+zO4sXEpHIxwdBXbYov1QKBgQDJWbt+W5M0sA2D5LrUBNMTvMdNnKH0syc2\nZlcIOdj6HuUIveYpBRq64Jn9VJpXMxQanwE+IUjCpPTa8wF0OA6MZPy6cfovqb8a\ni1/M/lvCoYVS3KHLcTOvTGD3xej0EUj13xWGNu8y3i7Z9/Bl21hEyjd0q0I5OqJx\no9Qa5TGR/wKBgBPfkYpdiMTe14i3ik09FgRFm4nhDcpCEKbPrYC8uF03Ge6KbQDF\nAh5ClN6aDggurRqt8Tvd0YPkZNP7aI8fxbk2PimystiuuFrNPX2WP6warjt2cvkE\nt6s522zAvxWkUrPor1ZONg1PXBLFrSf6J7OnNA3q7oina23FFM52fwRZAoGAZ7l7\nFffU2IKNI9HT0N7/YZ6RSVEUOXuFCsgjs5AhT5BUynERPTZs87I6gb9wltUwWRpq\nSHhbBDJ4FMa0jAtIq1hmvSF0EdOvJ9x+qJqr6JLOnMYd7zDMwFRna5yfigPRgx+9\n9dsc1CaTGiRYyg/5484MTWTgA51KC6Kq5IQHSj8CgYBr9rWgqM8hVCKSt1cMguQV\nTPaV97+u3kV2jFd/aVgDtCDIVvp5TPuqfskE1v3MsSjJ8hfHdYvyxZB8h8T4LlTD\n2HdxwCjVh2qirAvkar2b1mfA6R8msmVaIxBu4MqDcIPqR823klF7A8jSD3MGzYcU\nbnnxMdwgWQkmx0/6/90ZCg==\n-----END PRIVATE KEY-----\n'; + + it('does not allow empty string for cert', async function () { + const [error] = await safe(openssl.validateCertificate('', foobarDomain, { cert: '', key: 'key' })); + expect(error.reason).to.be(BoxError.BAD_FIELD); + }); + + it('does not allow empty string for key', async function () { + const [error] = await safe(openssl.validateCertificate('', foobarDomain, { cert: 'cert', key: '' })); + expect(error.reason).to.be(BoxError.BAD_FIELD); + }); + + it('does not allow invalid cert', async function () { + const [error] = await safe(openssl.validateCertificate('', foobarDomain, { cert: 'someinvalidcert', key: validKey0 })); + expect(error.reason).to.be(BoxError.BAD_FIELD); + }); + + it('does not allow invalid key', async function () { + const [error] = await safe(openssl.validateCertificate('', foobarDomain, { cert: validCert0, key: 'invalidkey' })); + expect(error.reason).to.be(BoxError.BAD_FIELD); + }); + + it('does not allow cert without matching domain', async function () { + const [error] = await safe(openssl.validateCertificate('', 'cloudron.io', { cert: validCert0, key: validKey0 })); + expect(error.reason).to.be(BoxError.BAD_FIELD); + + const [error2] = await safe(openssl.validateCertificate('cloudron.io', foobarDomain, { cert: validCert0, key: validKey0 })); + expect(error2.reason).to.be(BoxError.BAD_FIELD); + }); + + it('allows valid cert with matching domain', async function () { + await openssl.validateCertificate('', foobarDomain, { cert: validCert0, key: validKey0 }); + }); + + it('allows valid cert with matching domain (wildcard)', async function () { + await openssl.validateCertificate('abc', foobarDomain, { cert: validCert1, key: validKey1 }); + }); + + it('does now allow cert without matching domain (wildcard)', async function () { + const [error] = await safe(openssl.validateCertificate('', foobarDomain, { cert: validCert1, key: validKey1 })); + expect(error.reason).to.be(BoxError.BAD_FIELD); + + const [error2] = await safe(openssl.validateCertificate('bar.abc', foobarDomain, { cert: validCert1, key: validKey1 })); + expect(error2.reason).to.be(BoxError.BAD_FIELD); + }); + + it('allows valid cert with matching domain (subdomain)', async function () { + await openssl.validateCertificate('baz', foobarDomain, { cert: validCert2, key: validKey2 }); + }); + + it('does not allow cert without matching domain (subdomain)', async function () { + const [error] = await safe(openssl.validateCertificate('baz', foobarDomain, { cert: validCert0, key: validKey0 })); + expect(error.reason).to.be(BoxError.BAD_FIELD); + }); + + it('does not allow invalid cert/key tuple', async function () { + //expect(openssl.validateCertificate('', foobarDomain, { cert: validCert0, key: validKey1 })).to.be.an(Error); + }); + + it('picks certificate in SAN', async function () { + await openssl.validateCertificate('', amazingDomain, { cert: validCert3, key: validKey3 }); + await openssl.validateCertificate('subdomain', amazingDomain, { cert: validCert3, key: validKey3 }); + }); + + it('allows valid cert with matching domain (subdomain) - ecdsa', async function () { + openssl.validateCertificate('baz', foobarDomain, { cert: validCert4, key: validKey4 }); + }); + }); + + describe('generateFallbackCertificate', function () { + const domain = 'cool.com'; + let result; + + it('can generate fallback certs', async function () { + result = await openssl.generateCertificate(domain); + expect(result).to.be.ok(); + }); + + it('can validate the certs', async function () { + await openssl.validateCertificate('foo', domain, result); + await openssl.validateCertificate('', domain, result); + }); + }); +}); diff --git a/src/test/reverseproxy-test.js b/src/test/reverseproxy-test.js index c25d55e43..1902c2463 100644 --- a/src/test/reverseproxy-test.js +++ b/src/test/reverseproxy-test.js @@ -5,14 +5,12 @@ 'use strict'; -const BoxError = require('../boxerror.js'), - common = require('./common.js'), +const common = require('./common.js'), domains = require('../domains.js'), expect = require('expect.js'), fs = require('node:fs'), paths = require('../paths.js'), - reverseProxy = require('../reverseproxy.js'), - safe = require('safetydance'); + reverseProxy = require('../reverseproxy.js'); describe('Reverse Proxy', function () { const { setup, cleanup, domain, auditSource, app } = common; @@ -21,127 +19,6 @@ describe('Reverse Proxy', function () { before(setup); after(cleanup); - describe('validateCertificate', function () { - const foobarDomain = 'foobar.com'; - const amazingDomain = 'amazing.com'; - /* - Generate these with: - openssl genrsa -out server.key 512 - openssl req -new -key server.key -out server.csr -subj "/C=DE/ST=Berlin/L=Berlin/O=Nebulon/OU=CTO/CN=baz.foobar.com" - openssl x509 -req -days 3650 -in server.csr -signkey server.key -out server.crt - */ - - // foobar.com - const validCert0 = '-----BEGIN CERTIFICATE-----\nMIIBxTCCAW8CFBVWRFizZeUIdp94/l9Qx/+7UM4GMA0GCSqGSIb3DQEBCwUAMGQx\nCzAJBgNVBAYTAkRFMQ8wDQYDVQQIDAZCZXJsaW4xDzANBgNVBAcMBkJlcmxpbjEQ\nMA4GA1UECgwHTmVidWxvbjEMMAoGA1UECwwDQ1RPMRMwEQYDVQQDDApmb29iYXIu\nY29tMB4XDTIwMTEyMjAxNTI0M1oXDTMwMTEyMDAxNTI0M1owZDELMAkGA1UEBhMC\nREUxDzANBgNVBAgMBkJlcmxpbjEPMA0GA1UEBwwGQmVybGluMRAwDgYDVQQKDAdO\nZWJ1bG9uMQwwCgYDVQQLDANDVE8xEzARBgNVBAMMCmZvb2Jhci5jb20wXDANBgkq\nhkiG9w0BAQEFAANLADBIAkEA++BvW/oDsaM57d4Q4GQjkUzjB0/glKLj4P0Y8InS\nhLHOud9Uxz7dIcqHm9x9MOtqTRhtiHNoFLZLsU3a3upr2QIDAQABMA0GCSqGSIb3\nDQEBCwUAA0EAy9Acsgr/lH1rrE8DZov7dvvNjExkC+VO0kujO25aQIGBAtzLp9MG\nEblQ3ZXMBSX4b/nLMjOH8Xr4ZA0GUDgdew==\n-----END CERTIFICATE-----'; - const validKey0 = '-----BEGIN RSA PRIVATE KEY-----\nMIIBPAIBAAJBAPvgb1v6A7GjOe3eEOBkI5FM4wdP4JSi4+D9GPCJ0oSxzrnfVMc+\n3SHKh5vcfTDrak0YbYhzaBS2S7FN2t7qa9kCAwEAAQJBALsBjWyKmcd/2vjCkWEo\nuEefAEhjg+iXb/2RrLyad1TQfgs35UfigcjpWbzT2ScpFZT61ng6hKmclt2OCT9F\nBKECIQD/bjRbGiPq762ikWkfvalgkAAhSoXo2AcD/MsrhWyyPQIhAPxwM7jZRNvO\nng3TJaAgISwwUC9vuaNJQ06Yt02pvoXNAiEAuQipTrGCAWe8vb5ei8rFzxihr3wf\nw0vy0RWoTA+sbPUCIHDFOwXf4bgEJG1unwdacxdHefrHAXold3D8Hh8OrnMdAiEA\nov6sW0C1+maNpoWC+moDGFdImZnej2SDIB5976akWVo=\n-----END RSA PRIVATE KEY-----'; - - // *.foobar.com - const validCert1 = '-----BEGIN CERTIFICATE-----\nMIIByTCCAXMCFEXpWxabfp9Nybi7akGuxKlXdQVsMA0GCSqGSIb3DQEBCwUAMGYx\nCzAJBgNVBAYTAkRFMQ8wDQYDVQQIDAZCZXJsaW4xDzANBgNVBAcMBkJlcmxpbjEQ\nMA4GA1UECgwHTmVidWxvbjEMMAoGA1UECwwDQ1RPMRUwEwYDVQQDDAwqLmZvb2Jh\nci5jb20wHhcNMjAxMTIyMDIxNjQzWhcNMzAxMTIwMDIxNjQzWjBmMQswCQYDVQQG\nEwJERTEPMA0GA1UECAwGQmVybGluMQ8wDQYDVQQHDAZCZXJsaW4xEDAOBgNVBAoM\nB05lYnVsb24xDDAKBgNVBAsMA0NUTzEVMBMGA1UEAwwMKi5mb29iYXIuY29tMFww\nDQYJKoZIhvcNAQEBBQADSwAwSAJBAKMUYf86EG+J6ughAvhKGbIIyOpB3XqnK6KV\nM+r2/DvFx2KGIew7KopkzM2+UThDWE2YTcgL5846QRbx+K5NAXECAwEAATANBgkq\nhkiG9w0BAQsFAANBAJrX5wdszGt0lhDx0w2saJtTM3A6AfYdI7F37rgnvQKwRA0u\nTlN9Ekp4HbZsRi36g3W9zl6nWa3/HWbnBiRNuXk=\n-----END CERTIFICATE-----\n'; - const validKey1 = '-----BEGIN RSA PRIVATE KEY-----\nMIIBOQIBAAJBAKMUYf86EG+J6ughAvhKGbIIyOpB3XqnK6KVM+r2/DvFx2KGIew7\nKopkzM2+UThDWE2YTcgL5846QRbx+K5NAXECAwEAAQJAL/m/GqaqTyXzxXZwuTqT\ndJzA/qmBzqN/YsUiEO24Jp0AVuERlgiKBbxpu0xp8EpDsLTEt6TWWy1p0HIH6e0j\nAQIhANIZkHD6gVxvAMz0tquSprBnylqHngdT/PymDEHHNPv1AiEAxrUTvxV+vmii\n5CCLFTnYTQliKr+PC5qxn2WxV1rPng0CIGTiS55EW0t0LbE8rF40XAAGxn6z8ijY\npnj2jpojOojlAiBoaA6XEXFGFO651QufPISVfb+x3HMJ0t9PdHxo/NMoJQIgbVUh\naQKzUcrgIM2nbg4fLp3+VAh0ZkxNwaeKcsZz0cQ=\n-----END RSA PRIVATE KEY-----\n'; - - // baz.foobar.com - const validCert2 = '-----BEGIN CERTIFICATE-----\nMIIBzTCCAXcCFDnT77DcYIJ5EtAjyN7125yvCVtKMA0GCSqGSIb3DQEBCwUAMGgx\nCzAJBgNVBAYTAkRFMQ8wDQYDVQQIDAZCZXJsaW4xDzANBgNVBAcMBkJlcmxpbjEQ\nMA4GA1UECgwHTmVidWxvbjEMMAoGA1UECwwDQ1RPMRcwFQYDVQQDDA5iYXouZm9v\nYmFyLmNvbTAeFw0yNDEyMDQxNTMwMzRaFw0zNDEyMDIxNTMwMzRaMGgxCzAJBgNV\nBAYTAkRFMQ8wDQYDVQQIDAZCZXJsaW4xDzANBgNVBAcMBkJlcmxpbjEQMA4GA1UE\nCgwHTmVidWxvbjEMMAoGA1UECwwDQ1RPMRcwFQYDVQQDDA5iYXouZm9vYmFyLmNv\nbTBcMA0GCSqGSIb3DQEBAQUAA0sAMEgCQQC3PwF9imDPxtYTEbg+Grz5GSgc6Rqk\nNsMOyeOLDmEF+KiXf1iHZ+6dlXNeE8ULCnTomQ8KQ3VGKT596fqDF+pzAgMBAAEw\nDQYJKoZIhvcNAQELBQADQQCuzLEHQIG5kiWL7gkLMHURCkNqLrxEQmhm3RVNkeGZ\n6GXhS3qKc4PEl6kxK6XZXI/OPimmMGF+CKpJNpx88xMm\n-----END CERTIFICATE-----\n'; - const validKey2 = '-----BEGIN PRIVATE KEY-----\nMIIBVwIBADANBgkqhkiG9w0BAQEFAASCAUEwggE9AgEAAkEAtz8BfYpgz8bWExG4\nPhq8+RkoHOkapDbDDsnjiw5hBfiol39Yh2funZVzXhPFCwp06JkPCkN1Rik+fen6\ngxfqcwIDAQABAkEAgEXGkU6wfkG90RNyWWb2KZkGj1ZNo+4BlPSWJ90k5bv/ZGhN\n02H7ur2Lz8WOzrw/pQsK4g3xD5t+2I6RvzTywQIhANvdaeK13jI2822HHgMdY9Cp\nm3e69OxdPa2/t0+ExH7ZAiEA1VzlKtgI1j6gYQ70sa1N/dAsiFk8K2X7WuHKxeFh\n/CsCIQC7XQdD/OpabnupxfBshRovkqn7MWZRGvBZ5bvoVuNAmQIhAJoXW/6kRUWN\nt0BBj+EeO5xaEz9pyvXA0lZhiZN94ck/AiEAiLXiDwsUjOzcgGeX1wtn3fPlHZ1r\nbI3V4DXKe3AjUms=\n-----END PRIVATE KEY-----\n'; - - /* - Generate these with: - openssl ecparam -genkey -name prime256v1 -out server.key - openssl req -new -key server.key -out server.csr -subj "/C=DE/ST=Berlin/L=Berlin/O=Nebulon/OU=CTO/CN=*.foobar.com" - openssl req -x509 -sha256 -days 3650 -key server.key -in server.csr -out server.crt - */ - - // *.foobar.com - const validCert4 = '-----BEGIN CERTIFICATE-----\nMIICITCCAcegAwIBAgIUThSKBnGJ3TzM3ACzYQinCB5KS0QwCgYIKoZIzj0EAwIw\nZjELMAkGA1UEBhMCREUxDzANBgNVBAgMBkJlcmxpbjEPMA0GA1UEBwwGQmVybGlu\nMRAwDgYDVQQKDAdOZWJ1bG9uMQwwCgYDVQQLDANDVE8xFTATBgNVBAMMDCouZm9v\nYmFyLmNvbTAeFw0yMDExMjIwMTU5MjhaFw0zMDExMjAwMTU5MjhaMGYxCzAJBgNV\nBAYTAkRFMQ8wDQYDVQQIDAZCZXJsaW4xDzANBgNVBAcMBkJlcmxpbjEQMA4GA1UE\nCgwHTmVidWxvbjEMMAoGA1UECwwDQ1RPMRUwEwYDVQQDDAwqLmZvb2Jhci5jb20w\nWTATBgcqhkjOPQIBBggqhkjOPQMBBwNCAARXV7XqwL8dTTdKJ1sngAAgFXBmppsy\n5GLjm49GrDTB2ho6sjjwMUzKKP9jVCRrSlcKwmXNAy75/pPtLkL4A+s/o1MwUTAd\nBgNVHQ4EFgQUWajw1bCj16I+F8ZpjQEMnJb56XkwHwYDVR0jBBgwFoAUWajw1bCj\n16I+F8ZpjQEMnJb56XkwDwYDVR0TAQH/BAUwAwEB/zAKBggqhkjOPQQDAgNIADBF\nAiEA8eeVP+FvAfg4RVjH17DL/zPUBUIsmyTnPm9D7zIAdc0CICZYPU5qrAKA1h5U\n6+8vX4w+EuVQ8vjc8ATl7L/IKdmL\n-----END CERTIFICATE-----\n'; - const validKey4 = '-----BEGIN EC PARAMETERS-----\nBggqhkjOPQMBBw==\n-----END EC PARAMETERS-----\n-----BEGIN EC PRIVATE KEY-----\nMHcCAQEEIAczzARUd1L4KN/2Fl4s5kc1to6QzP9XGPCVLfQdtwbSoAoGCCqGSM49\nAwEHoUQDQgAEV1e16sC/HU03SidbJ4AAIBVwZqabMuRi45uPRqw0wdoaOrI48DFM\nyij/Y1Qka0pXCsJlzQMu+f6T7S5C+APrPw==\n-----END EC PRIVATE KEY-----\n'; - - // cp /etc/ssl/openssl.cnf /tmp/openssl.cnf - // echo -e "[SAN]\nsubjectAltName=DNS:amazing.com,DNS:*.amazing.com\n" >> /tmp/openssl.cnf - // openssl req -x509 -newkey rsa:2048 -keyout amazing.key -out amazing.crt -days 3650 -subj /CN=*.amazing.com -nodes -extensions SAN -config /tmp/openssl.cnf - const validCert3 = '-----BEGIN CERTIFICATE-----\nMIIC3DCCAcSgAwIBAgIJALcStAD5sDWEMA0GCSqGSIb3DQEBCwUAMBgxFjAUBgNV\nBAMMDSouYW1hemluZy5jb20wHhcNMTgwMjA5MjIxMzM2WhcNMjgwMjA3MjIxMzM2\nWjAYMRYwFAYDVQQDDA0qLmFtYXppbmcuY29tMIIBIjANBgkqhkiG9w0BAQEFAAOC\nAQ8AMIIBCgKCAQEAvp8dk13u4vmAfKfRNOO8+rVQ8q+vyR8scc9Euj0pTodLBflM\n2K6Zk0isirRzCL/jd4n1A6QrPeJ+r2J4xtHk2j+pavt8Sa2Go2MzpAe3OTuIqYJf\nUt7Im3f2Lb67itTPrpA2TR3A/dDFlazju+eBd3t3496Do8aBPpXAdOabfPsrv3nE\nx97vrr4tzeK3kG9u7GYuod5gyiwF2t5wSeMWbFk2oqkOCtHRXE77JDKVxIGiepnU\nTnkW9b7jIkiBQ1x0xHG4soewV2ymGHS2XrUHZ45FFMG7yVYpytKT9Iz9ty/z5VcL\nZ6NzgU/pKfQaIe8MpoDpVf5UNeB2DOAAEoJKKwIDAQABoykwJzAlBgNVHREEHjAc\nggthbWF6aW5nLmNvbYINKi5hbWF6aW5nLmNvbTANBgkqhkiG9w0BAQsFAAOCAQEA\nMULk6B9XrVPAole8W66o3WUUOrC7NVjbwZjr+Kp5oQTSo84qacaZS2C3ox/j/TZY\nUuNvoE6gIOHi+inN+G4P76K7NEvm8+Y1CeAyaPq01H4Qy2lk9F5wFMtPqvBZnF9C\nx1MvV30FruHXe5pDfnG1npKECpn2SgE3k6FRHM55u8rTMEm/O4TtsDq+fPqUvyWa\nZuRjPv4qVGGkoPyxA6iffxclpOAXs3JUgLcYoM2vxKC0YSOjHEa0p4uffX063Jgg\nybuy3OKvm+8L6moycX7J+LZK81dDTFDtF7PwrnRbpS4re0i/LSk23jDQvDOLnrAa\nSawRR8+1QHTENBo7dnP+NA==\n-----END CERTIFICATE-----'; - const validKey3 = '-----BEGIN PRIVATE KEY-----\nMIIEvAIBADANBgkqhkiG9w0BAQEFAASCBKYwggSiAgEAAoIBAQC+nx2TXe7i+YB8\np9E047z6tVDyr6/JHyxxz0S6PSlOh0sF+UzYrpmTSKyKtHMIv+N3ifUDpCs94n6v\nYnjG0eTaP6lq+3xJrYajYzOkB7c5O4ipgl9S3sibd/YtvruK1M+ukDZNHcD90MWV\nrOO754F3e3fj3oOjxoE+lcB05pt8+yu/ecTH3u+uvi3N4reQb27sZi6h3mDKLAXa\n3nBJ4xZsWTaiqQ4K0dFcTvskMpXEgaJ6mdROeRb1vuMiSIFDXHTEcbiyh7BXbKYY\ndLZetQdnjkUUwbvJVinK0pP0jP23L/PlVwtno3OBT+kp9Boh7wymgOlV/lQ14HYM\n4AASgkorAgMBAAECggEAdVSVLMcNqlGuv4vAHtDq2lpOaAKxrZbtkWPlxsisqzRl\nfljT7y+RQfHimkG16LXL+iFFWadsIlxOY/+1nZNGTPwQeNQwzVzs2ZbPC3DgW28E\nkGm56NVOHzu4oLGc2DhjWOxVMCRXTSN66sUPK/K0YunxgqXM2zrtBKvCWXI0VLlo\nN/UWAwHf4i0GWRl8u8PvxgMXlSW9p9l6gSsivWRMag9ADwRQ/NSKrRYkiOoRe3vz\nLxXARBvzeZXvOPVLGVRX4SIR7OmS8cC6Ol/rp1/ZFFID7aN+wdzphPSL1UNUriw4\nDv1mxz73SNakgeYSFBoWRS5BsJI01JoCoILsnhVCiQKBgQDyW+k5+j4K17fzwsmi\nyxZ0Nz/ncpkqxVrWYZM3pn7OVkb2NDArimEk53kmJ0hrT84kKJUYDx55R2TpnzpV\nMLmjxgs9TUrzZzsL/DP2ppkfE3OrPS+06OGa5GbURxD6KPvqDtOmU3oFyJ3f4YJR\nVK7RW+zO4sXEpHIxwdBXbYov1QKBgQDJWbt+W5M0sA2D5LrUBNMTvMdNnKH0syc2\nZlcIOdj6HuUIveYpBRq64Jn9VJpXMxQanwE+IUjCpPTa8wF0OA6MZPy6cfovqb8a\ni1/M/lvCoYVS3KHLcTOvTGD3xej0EUj13xWGNu8y3i7Z9/Bl21hEyjd0q0I5OqJx\no9Qa5TGR/wKBgBPfkYpdiMTe14i3ik09FgRFm4nhDcpCEKbPrYC8uF03Ge6KbQDF\nAh5ClN6aDggurRqt8Tvd0YPkZNP7aI8fxbk2PimystiuuFrNPX2WP6warjt2cvkE\nt6s522zAvxWkUrPor1ZONg1PXBLFrSf6J7OnNA3q7oina23FFM52fwRZAoGAZ7l7\nFffU2IKNI9HT0N7/YZ6RSVEUOXuFCsgjs5AhT5BUynERPTZs87I6gb9wltUwWRpq\nSHhbBDJ4FMa0jAtIq1hmvSF0EdOvJ9x+qJqr6JLOnMYd7zDMwFRna5yfigPRgx+9\n9dsc1CaTGiRYyg/5484MTWTgA51KC6Kq5IQHSj8CgYBr9rWgqM8hVCKSt1cMguQV\nTPaV97+u3kV2jFd/aVgDtCDIVvp5TPuqfskE1v3MsSjJ8hfHdYvyxZB8h8T4LlTD\n2HdxwCjVh2qirAvkar2b1mfA6R8msmVaIxBu4MqDcIPqR823klF7A8jSD3MGzYcU\nbnnxMdwgWQkmx0/6/90ZCg==\n-----END PRIVATE KEY-----\n'; - - it('does not allow empty string for cert', async function () { - const [error] = await safe(reverseProxy.validateCertificate('', foobarDomain, { cert: '', key: 'key' })); - expect(error.reason).to.be(BoxError.BAD_FIELD); - }); - - it('does not allow empty string for key', async function () { - const [error] = await safe(reverseProxy.validateCertificate('', foobarDomain, { cert: 'cert', key: '' })); - expect(error.reason).to.be(BoxError.BAD_FIELD); - }); - - it('does not allow invalid cert', async function () { - const [error] = await safe(reverseProxy.validateCertificate('', foobarDomain, { cert: 'someinvalidcert', key: validKey0 })); - expect(error.reason).to.be(BoxError.BAD_FIELD); - }); - - it('does not allow invalid key', async function () { - const [error] = await safe(reverseProxy.validateCertificate('', foobarDomain, { cert: validCert0, key: 'invalidkey' })); - expect(error.reason).to.be(BoxError.BAD_FIELD); - }); - - it('does not allow cert without matching domain', async function () { - const [error] = await safe(reverseProxy.validateCertificate('', 'cloudron.io', { cert: validCert0, key: validKey0 })); - expect(error.reason).to.be(BoxError.BAD_FIELD); - - const [error2] = await safe(reverseProxy.validateCertificate('cloudron.io', foobarDomain, { cert: validCert0, key: validKey0 })); - expect(error2.reason).to.be(BoxError.BAD_FIELD); - }); - - it('allows valid cert with matching domain', async function () { - await reverseProxy.validateCertificate('', foobarDomain, { cert: validCert0, key: validKey0 }); - }); - - it('allows valid cert with matching domain (wildcard)', async function () { - await reverseProxy.validateCertificate('abc', foobarDomain, { cert: validCert1, key: validKey1 }); - }); - - it('does now allow cert without matching domain (wildcard)', async function () { - const [error] = await safe(reverseProxy.validateCertificate('', foobarDomain, { cert: validCert1, key: validKey1 })); - expect(error.reason).to.be(BoxError.BAD_FIELD); - - const [error2] = await safe(reverseProxy.validateCertificate('bar.abc', foobarDomain, { cert: validCert1, key: validKey1 })); - expect(error2.reason).to.be(BoxError.BAD_FIELD); - }); - - it('allows valid cert with matching domain (subdomain)', async function () { - await reverseProxy.validateCertificate('baz', foobarDomain, { cert: validCert2, key: validKey2 }); - }); - - it('does not allow cert without matching domain (subdomain)', async function () { - const [error] = await safe(reverseProxy.validateCertificate('baz', foobarDomain, { cert: validCert0, key: validKey0 })); - expect(error.reason).to.be(BoxError.BAD_FIELD); - }); - - it('does not allow invalid cert/key tuple', async function () { - //expect(reverseProxy.validateCertificate('', foobarDomain, { cert: validCert0, key: validKey1 })).to.be.an(Error); - }); - - it('picks certificate in SAN', async function () { - await reverseProxy.validateCertificate('', amazingDomain, { cert: validCert3, key: validKey3 }); - await reverseProxy.validateCertificate('subdomain', amazingDomain, { cert: validCert3, key: validKey3 }); - }); - - it('allows valid cert with matching domain (subdomain) - ecdsa', async function () { - reverseProxy.validateCertificate('baz', foobarDomain, { cert: validCert4, key: validKey4 }); - }); - }); - - describe('generateFallbackCertificate', function () { - const domain = 'cool.com'; - let result; - - it('can generate fallback certs', async function () { - result = await reverseProxy.generateFallbackCertificate(domain); - expect(result).to.be.ok(); - }); - - it('can validate the certs', async function () { - await reverseProxy.validateCertificate('foo', domain, result); - await reverseProxy.validateCertificate('', domain, result); - }); - }); - describe('configureApp', function () { before(async function () { domainCopy.tlsConfig = { provider: 'fallback' };