navy
Version:
Quick and powerful development environments using Docker and Docker Compose
478 lines (476 loc) • 19.8 kB
JavaScript
;
var _interopRequireDefault = require("@babel/runtime/helpers/interopRequireDefault");
var _chai = require("chai");
var _sinon = _interopRequireDefault(require("sinon"));
var _proxyquire = _interopRequireDefault(require("proxyquire"));
var _errors = require("../../errors");
/* eslint-env mocha */
describe('util/https', function () {
let sandbox;
let getConfigStub;
let getConfigDirStub;
let DEFAULT_TLS_ROOT_CA_DIR;
let getNavyStub;
let navyStub;
let fsStub;
let pkiStub;
let mdStub;
let httpsModule;
let certObj;
function loadModule() {
httpsModule = _proxyquire.default.noCallThru()('../https', {
'../config': {
getConfig: getConfigStub,
getConfigDir: getConfigDirStub,
DEFAULT_TLS_ROOT_CA_DIR
},
'../errors': {
NavyError: _errors.NavyError
},
'../navy': {
getNavy: getNavyStub
},
fs: fsStub,
'node-forge': {
pki: pkiStub,
md: mdStub
}
});
}
beforeEach(function () {
sandbox = _sinon.default.createSandbox();
getConfigStub = sandbox.stub().returns({});
getConfigDirStub = sandbox.stub().returns('/cfg');
DEFAULT_TLS_ROOT_CA_DIR = '/default-ca-dir';
navyStub = {
url: sandbox.stub().resolves('https://web.local')
};
getNavyStub = sandbox.stub().returns(navyStub);
fsStub = {
existsSync: sandbox.stub().returns(true),
mkdirSync: sandbox.stub(),
unlinkSync: sandbox.stub(),
readFileSync: sandbox.stub().returns('--PEM--'),
writeFileSync: sandbox.stub(),
copyFileSync: sandbox.stub()
};
const caCertFromPem = {
subject: {
attributes: [{
name: 'commonName',
value: 'ca'
}]
},
generateSubjectKeyIdentifier: sandbox.stub().returns({
getBytes: () => 'ca-subject-key-id'
})
};
certObj = {
publicKey: null,
serialNumber: null,
validity: {
notBefore: new Date(),
notAfter: new Date()
},
setSubject: sandbox.stub(),
setIssuer: sandbox.stub(),
setExtensions: sandbox.stub(),
sign: sandbox.stub(),
subject: {
attributes: []
}
};
pkiStub = {
rsa: {
generateKeyPair: sandbox.stub().returns({
publicKey: 'pub',
privateKey: 'priv'
})
},
createCertificate: sandbox.stub().returns(certObj),
privateKeyToPem: sandbox.stub().returns('priv-pem'),
publicKeyToPem: sandbox.stub().returns('pub-pem'),
certificateToPem: sandbox.stub().returns('cert-pem'),
privateKeyFromPem: sandbox.stub().returns('priv-key-obj'),
certificateFromPem: sandbox.stub().returns(caCertFromPem)
};
mdStub = {
sha256: {
create: sandbox.stub().returns('digest')
}
};
loadModule();
});
afterEach(function () {
sandbox.restore();
});
describe('getCertsPath', function () {
it('should return the certs path when it exists', function () {
fsStub.existsSync.returns(true);
const result = httpsModule.getCertsPath();
(0, _chai.expect)(result).to.contain('tls-certs');
(0, _chai.expect)(fsStub.mkdirSync.called).to.equal(false);
});
it('should return an empty string when path does not exist and create is false', function () {
fsStub.existsSync.returns(false);
const result = httpsModule.getCertsPath();
(0, _chai.expect)(result).to.equal('');
(0, _chai.expect)(fsStub.mkdirSync.called).to.equal(false);
});
it('should create the path when create is true and it does not exist', function () {
fsStub.existsSync.returns(false);
const result = httpsModule.getCertsPath(true);
(0, _chai.expect)(fsStub.mkdirSync.calledOnce).to.equal(true);
(0, _chai.expect)(fsStub.mkdirSync.firstCall.args[0]).to.contain('tls-certs');
(0, _chai.expect)(fsStub.mkdirSync.firstCall.args[1]).to.eql({
recursive: true
});
(0, _chai.expect)(result).to.contain('tls-certs');
});
});
describe('removeCert', function () {
it('should unlink each existing cert/key file', async function () {
fsStub.existsSync.callsFake(p => p === '/cfg/tls-certs' || p.endsWith('web.local.crt') || p.endsWith('web.local.key'));
navyStub.url.resolves('https://web.local');
await httpsModule.removeCert({
navy: 'env-1',
disable: 'web'
});
(0, _chai.expect)(fsStub.unlinkSync.callCount).to.equal(2);
const paths = fsStub.unlinkSync.getCalls().map(c => c.args[0]);
(0, _chai.expect)(paths.some(p => p.endsWith('web.local.crt'))).to.equal(true);
(0, _chai.expect)(paths.some(p => p.endsWith('web.local.key'))).to.equal(true);
});
it('should skip files that do not exist', async function () {
fsStub.existsSync.callsFake(p => p === '/cfg/tls-certs');
await httpsModule.removeCert({
navy: 'env-1',
disable: 'web'
});
(0, _chai.expect)(fsStub.unlinkSync.called).to.equal(false);
});
it('should wrap unlink errors in NavyError', async function () {
fsStub.existsSync.callsFake(p => p === '/cfg/tls-certs' || p.endsWith('.crt') || p.endsWith('.key'));
fsStub.unlinkSync.throws(new Error('permission denied'));
let caught;
try {
await httpsModule.removeCert({
navy: 'env-1',
disable: 'web'
});
} catch (err) {
caught = err;
}
(0, _chai.expect)(caught).to.be.instanceof(_errors.NavyError);
});
});
describe('generateRootCa', function () {
it('should create the root CA dir when missing', async function () {
fsStub.existsSync.callsFake(p => {
if (p === '/default-ca-dir') return false;
return false;
});
await httpsModule.generateRootCa();
(0, _chai.expect)(fsStub.mkdirSync.calledWith('/default-ca-dir')).to.equal(true);
(0, _chai.expect)(fsStub.writeFileSync.calledThrice).to.equal(true);
});
it('should wrap mkdir errors in NavyError', async function () {
fsStub.existsSync.returns(false);
fsStub.mkdirSync.throws(new Error('permission denied'));
let caught;
try {
await httpsModule.generateRootCa();
} catch (err) {
caught = err;
}
(0, _chai.expect)(caught).to.be.instanceof(_errors.NavyError);
});
it('should skip generation when CA already exists', async function () {
fsStub.existsSync.callsFake(p => p.endsWith('ca.crt') || p.endsWith('ca.key') || p === '/default-ca-dir');
await httpsModule.generateRootCa();
(0, _chai.expect)(pkiStub.rsa.generateKeyPair.called).to.equal(false);
(0, _chai.expect)(fsStub.writeFileSync.called).to.equal(false);
});
it('should generate, sign, and write CA files when missing', async function () {
fsStub.existsSync.callsFake(p => p === '/default-ca-dir');
await httpsModule.generateRootCa();
(0, _chai.expect)(pkiStub.rsa.generateKeyPair.calledWith(2048)).to.equal(true);
(0, _chai.expect)(pkiStub.createCertificate.calledOnce).to.equal(true);
(0, _chai.expect)(fsStub.writeFileSync.calledThrice).to.equal(true);
const writtenPaths = fsStub.writeFileSync.getCalls().map(c => c.args[0]);
(0, _chai.expect)(writtenPaths.some(p => p.endsWith('ca.key'))).to.equal(true);
(0, _chai.expect)(writtenPaths.some(p => p.endsWith('ca.pub.key'))).to.equal(true);
(0, _chai.expect)(writtenPaths.some(p => p.endsWith('ca.crt'))).to.equal(true);
});
it('should use a custom root CA dir when supplied via config', async function () {
getConfigStub.returns({
tlsRootCaDir: '/custom-ca'
});
fsStub.existsSync.callsFake(p => p === '/custom-ca');
await httpsModule.generateRootCa();
const writtenPaths = fsStub.writeFileSync.getCalls().map(c => c.args[0]);
(0, _chai.expect)(writtenPaths.every(p => p.startsWith('/custom-ca/'))).to.equal(true);
});
it('should wrap signing errors in NavyError', async function () {
fsStub.existsSync.callsFake(p => p === '/default-ca-dir');
const cert = pkiStub.createCertificate();
cert.sign = sandbox.stub().throws(new Error('sign failure'));
pkiStub.createCertificate.returns(cert);
let caught;
try {
await httpsModule.generateRootCa();
} catch (err) {
caught = err;
}
(0, _chai.expect)(caught).to.be.instanceof(_errors.NavyError);
});
});
describe('createCert', function () {
it('should skip creation when the cert already exists', async function () {
fsStub.existsSync.callsFake(p => p === '/cfg/tls-certs' || p === '/cfg/tls-certs/web.local.crt');
await httpsModule.createCert({
serviceUrl: 'https://web.local'
});
(0, _chai.expect)(fsStub.writeFileSync.called).to.equal(false);
});
it('should generate, sign, and write a service cert', async function () {
fsStub.existsSync.callsFake(p => {
if (p === '/cfg/tls-certs') return true;
if (p === '/default-ca-dir') return true;
if (p === '/default-ca-dir/ca.crt' || p === '/default-ca-dir/ca.key') return true;
return false;
});
await httpsModule.createCert({
serviceUrl: 'https://web.local'
});
(0, _chai.expect)(pkiStub.privateKeyFromPem.calledOnce).to.equal(true);
(0, _chai.expect)(pkiStub.certificateFromPem.calledOnce).to.equal(true);
(0, _chai.expect)(fsStub.writeFileSync.calledTwice).to.equal(true);
const writtenPaths = fsStub.writeFileSync.getCalls().map(c => c.args[0]);
(0, _chai.expect)(writtenPaths.some(p => p.endsWith('web.local.key'))).to.equal(true);
(0, _chai.expect)(writtenPaths.some(p => p.endsWith('web.local.crt'))).to.equal(true);
});
it('should use opts.hostName when supplied', async function () {
fsStub.existsSync.callsFake(p => {
if (p === '/cfg/tls-certs') return true;
if (p === '/default-ca-dir') return true;
if (p === '/default-ca-dir/ca.crt' || p === '/default-ca-dir/ca.key') return true;
return false;
});
await httpsModule.createCert({
hostName: 'custom-host'
});
const writtenPaths = fsStub.writeFileSync.getCalls().map(c => c.args[0]);
(0, _chai.expect)(writtenPaths.some(p => p.endsWith('custom-host.key'))).to.equal(true);
(0, _chai.expect)(writtenPaths.some(p => p.endsWith('custom-host.crt'))).to.equal(true);
});
it('should use a custom tlsRootCaDir from config', async function () {
getConfigStub.returns({
tlsRootCaDir: '/custom-ca'
});
fsStub.existsSync.callsFake(p => {
if (p === '/cfg/tls-certs') return true;
if (p === '/custom-ca') return true;
if (p === '/custom-ca/ca.crt' || p === '/custom-ca/ca.key') return true;
return false;
});
await httpsModule.createCert({
serviceUrl: 'https://web.local'
});
const readPaths = fsStub.readFileSync.getCalls().map(c => c.args[0]);
(0, _chai.expect)(readPaths.some(p => p === '/custom-ca/ca.crt')).to.equal(true);
(0, _chai.expect)(readPaths.some(p => p === '/custom-ca/ca.key')).to.equal(true);
});
it('should wrap signing errors in NavyError', async function () {
fsStub.existsSync.callsFake(p => {
if (p === '/cfg/tls-certs') return true;
if (p === '/default-ca-dir') return true;
if (p === '/default-ca-dir/ca.crt' || p === '/default-ca-dir/ca.key') return true;
return false;
});
const cert = pkiStub.createCertificate();
cert.sign = sandbox.stub().throws(new Error('sign fail'));
pkiStub.createCertificate.returns(cert);
let caught;
try {
await httpsModule.createCert({
serviceUrl: 'https://web.local'
});
} catch (err) {
caught = err;
}
(0, _chai.expect)(caught).to.be.instanceof(_errors.NavyError);
});
it('should prefer the parsed hostname over the raw certName segment', async function () {
fsStub.existsSync.callsFake(p => {
if (p === '/cfg/tls-certs') return true;
if (p === '/default-ca-dir') return true;
if (p === '/default-ca-dir/ca.crt' || p === '/default-ca-dir/ca.key') return true;
return false;
});
await httpsModule.createCert({
serviceUrl: 'https://tls-host.example.com:8443/app'
});
(0, _chai.expect)(certObj.setSubject.firstCall.args[0][0].value).to.equal('tls-host.example.com');
});
it('should keep certName when serviceUrl is not a valid URL', async function () {
fsStub.existsSync.callsFake(p => {
if (p === '/cfg/tls-certs') return true;
if (p === '/default-ca-dir') return true;
if (p === '/default-ca-dir/ca.crt' || p === '/default-ca-dir/ca.key') return true;
return false;
});
await httpsModule.createCert({
serviceUrl: 'https://bad['
});
const writtenPaths = fsStub.writeFileSync.getCalls().map(c => c.args[0]);
(0, _chai.expect)(writtenPaths.some(p => p.endsWith('bad[.key'))).to.equal(true);
});
it('should fall back to certName when serviceUrl has no hostname', async function () {
fsStub.existsSync.callsFake(p => {
if (p === '/cfg/tls-certs') return true;
if (p === '/default-ca-dir') return true;
if (p === '/default-ca-dir/ca.crt' || p === '/default-ca-dir/ca.key') return true;
return false;
});
await httpsModule.createCert({
serviceUrl: 'https://?q=1'
});
(0, _chai.expect)(certObj.setSubject.firstCall.args[0][0].value).to.equal('?q=1');
});
it('should wrap non-Error signing failures in NavyError', async function () {
fsStub.existsSync.callsFake(p => {
if (p === '/cfg/tls-certs') return true;
if (p === '/default-ca-dir') return true;
if (p === '/default-ca-dir/ca.crt' || p === '/default-ca-dir/ca.key') return true;
return false;
});
pkiStub.certificateFromPem.callsFake(() => {
const failure = 'string failure';
throw failure;
});
let caught;
try {
await httpsModule.createCert({
serviceUrl: 'https://web.local'
});
} catch (err) {
caught = err;
}
(0, _chai.expect)(caught).to.be.instanceof(_errors.NavyError);
(0, _chai.expect)(caught.message).to.equal('string failure');
});
it('should use IP SAN when hostName is an IP address', async function () {
fsStub.existsSync.callsFake(p => {
if (p === '/cfg/tls-certs') return true;
if (p === '/default-ca-dir') return true;
if (p === '/default-ca-dir/ca.crt' || p === '/default-ca-dir/ca.key') return true;
return false;
});
await httpsModule.createCert({
hostName: '127.0.0.1'
});
const extensions = certObj.setExtensions.firstCall.args[0];
const sanExt = extensions.find(ext => ext.name === 'subjectAltName');
(0, _chai.expect)(sanExt.altNames).to.eql([{
type: 7,
ip: '127.0.0.1'
}]);
});
});
describe('hostNameFromIssueUrl', function () {
it('should throw when the URL is empty', function () {
(0, _chai.expect)(() => httpsModule.hostNameFromIssueUrl('')).to.throw(/must not be empty/);
(0, _chai.expect)(() => httpsModule.hostNameFromIssueUrl(' ')).to.throw(/must not be empty/);
});
it('should prepend https when no scheme is supplied', function () {
(0, _chai.expect)(httpsModule.hostNameFromIssueUrl('example.local')).to.equal('example.local');
});
it('should accept an explicit scheme', function () {
(0, _chai.expect)(httpsModule.hostNameFromIssueUrl('http://example.local')).to.equal('example.local');
});
it('should throw when the URL cannot be parsed', function () {
(0, _chai.expect)(() => httpsModule.hostNameFromIssueUrl('http://')).to.throw(/Invalid URL/);
});
it('should throw when no hostname can be determined', function () {
(0, _chai.expect)(() => httpsModule.hostNameFromIssueUrl('file:///tmp/cert.crt')).to.throw(/Could not determine a hostname/);
});
});
describe('issueLeafCertForUrl', function () {
it('should write cert, key, and CA copy files to the output dir', async function () {
fsStub.existsSync.callsFake(p => {
if (p === '/default-ca-dir') return true;
if (p === '/default-ca-dir/ca.crt' || p === '/default-ca-dir/ca.key') return true;
return false;
});
const result = await httpsModule.issueLeafCertForUrl('https://issued.local', '/out/certs');
(0, _chai.expect)(fsStub.mkdirSync.calledWith('/out/certs', {
recursive: true
})).to.equal(true);
(0, _chai.expect)(fsStub.writeFileSync.callCount).to.equal(2);
(0, _chai.expect)(fsStub.copyFileSync.calledOnce).to.equal(true);
(0, _chai.expect)(result.certPath).to.equal('/out/certs/issued.local.crt');
(0, _chai.expect)(result.keyPath).to.equal('/out/certs/issued.local.key');
(0, _chai.expect)(result.caCopyPath).to.equal('/out/certs/navy-root-ca.crt');
});
it('should sanitise unsafe characters in the output file base name', async function () {
fsStub.existsSync.callsFake(p => {
if (p === '/default-ca-dir') return true;
if (p === '/default-ca-dir/ca.crt' || p === '/default-ca-dir/ca.key') return true;
return false;
});
const result = await httpsModule.issueLeafCertForUrl('https://[::1]', '/out');
(0, _chai.expect)(result.certPath).to.equal('/out/___1_.crt');
(0, _chai.expect)(result.keyPath).to.equal('/out/___1_.key');
});
it('should wrap non-Error build failures in NavyError', async function () {
fsStub.existsSync.callsFake(p => {
if (p === '/default-ca-dir') return true;
if (p === '/default-ca-dir/ca.crt' || p === '/default-ca-dir/ca.key') return true;
return false;
});
pkiStub.certificateFromPem.callsFake(() => {
const failure = 'forge broke';
throw failure;
});
let caught;
try {
await httpsModule.issueLeafCertForUrl('https://issued.local', '/out');
} catch (err) {
caught = err;
}
(0, _chai.expect)(caught).to.be.instanceof(_errors.NavyError);
(0, _chai.expect)(caught.message).to.equal('forge broke');
});
it('should wrap build failures in NavyError', async function () {
fsStub.existsSync.callsFake(p => {
if (p === '/default-ca-dir') return true;
if (p === '/default-ca-dir/ca.crt' || p === '/default-ca-dir/ca.key') return true;
return false;
});
pkiStub.certificateFromPem.throws(new Error('forge broke'));
let caught;
try {
await httpsModule.issueLeafCertForUrl('https://issued.local', '/out');
} catch (err) {
caught = err;
}
(0, _chai.expect)(caught).to.be.instanceof(_errors.NavyError);
(0, _chai.expect)(caught.message).to.equal('forge broke');
});
it('should use IP SAN when issuing for an IP address URL', async function () {
fsStub.existsSync.callsFake(p => {
if (p === '/default-ca-dir') return true;
if (p === '/default-ca-dir/ca.crt' || p === '/default-ca-dir/ca.key') return true;
return false;
});
await httpsModule.issueLeafCertForUrl('https://192.168.1.10', '/out');
const extensions = certObj.setExtensions.firstCall.args[0];
const sanExt = extensions.find(ext => ext.name === 'subjectAltName');
(0, _chai.expect)(sanExt.altNames).to.eql([{
type: 7,
ip: '192.168.1.10'
}]);
});
});
});