navy 7.0.0 → 7.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/lib/cli/__tests__/https.js +193 -1
- package/lib/cli/https.js +103 -18
- package/lib/cli/program.js +11 -1
- package/lib/util/__tests__/https.js +187 -10
- package/lib/util/__tests__/install-root-ca.js +282 -0
- package/lib/util/https.js +166 -40
- package/lib/util/install-root-ca.js +114 -0
- package/package.json +1 -1
|
@@ -18,6 +18,8 @@ describe('cli/https', function () {
|
|
|
18
18
|
let createCertStub;
|
|
19
19
|
let generateRootCaStub;
|
|
20
20
|
let removeCertStub;
|
|
21
|
+
let issueLeafCertForUrlStub;
|
|
22
|
+
let installRootCaToTrustStoreStub;
|
|
21
23
|
let fsStub;
|
|
22
24
|
let consoleLogStub;
|
|
23
25
|
let httpsCli;
|
|
@@ -40,6 +42,12 @@ describe('cli/https', function () {
|
|
|
40
42
|
createCertStub = sandbox.stub().resolves();
|
|
41
43
|
generateRootCaStub = sandbox.stub().resolves();
|
|
42
44
|
removeCertStub = sandbox.stub().resolves();
|
|
45
|
+
issueLeafCertForUrlStub = sandbox.stub().resolves({
|
|
46
|
+
certPath: '/out/host.crt',
|
|
47
|
+
keyPath: '/out/host.key',
|
|
48
|
+
caCopyPath: '/out/navy-root-ca.crt'
|
|
49
|
+
});
|
|
50
|
+
installRootCaToTrustStoreStub = sandbox.stub();
|
|
43
51
|
fsStub = {
|
|
44
52
|
existsSync: sandbox.stub().returns(true),
|
|
45
53
|
readdirSync: sandbox.stub().returns(['web.local.crt', 'api.local.crt', 'something.txt'])
|
|
@@ -63,7 +71,11 @@ describe('cli/https', function () {
|
|
|
63
71
|
'../util/https': {
|
|
64
72
|
createCert: createCertStub,
|
|
65
73
|
generateRootCa: generateRootCaStub,
|
|
66
|
-
removeCert: removeCertStub
|
|
74
|
+
removeCert: removeCertStub,
|
|
75
|
+
issueLeafCertForUrl: issueLeafCertForUrlStub
|
|
76
|
+
},
|
|
77
|
+
'../util/install-root-ca': {
|
|
78
|
+
installRootCaToTrustStore: installRootCaToTrustStoreStub
|
|
67
79
|
},
|
|
68
80
|
fs: fsStub
|
|
69
81
|
});
|
|
@@ -205,6 +217,186 @@ describe('cli/https', function () {
|
|
|
205
217
|
const checkedPaths = fsStub.existsSync.getCalls().map(c => c.args[0]);
|
|
206
218
|
(0, _chai.expect)(checkedPaths.some(p => p === '/custom-ca/ca.crt')).to.equal(true);
|
|
207
219
|
});
|
|
220
|
+
it('should accept a single string service name', async function () {
|
|
221
|
+
await httpsCli('web', {
|
|
222
|
+
navy: 'env-1'
|
|
223
|
+
});
|
|
224
|
+
(0, _chai.expect)(createCertStub.callCount).to.equal(1);
|
|
225
|
+
});
|
|
226
|
+
it('should ignore non-string entries in a services array', async function () {
|
|
227
|
+
await httpsCli(['web', '', null, 42, 'api'], {
|
|
228
|
+
navy: 'env-1'
|
|
229
|
+
});
|
|
230
|
+
(0, _chai.expect)(createCertStub.callCount).to.equal(2);
|
|
231
|
+
});
|
|
232
|
+
it('should treat unsupported service arg types as an empty list', async function () {
|
|
233
|
+
fsStub.existsSync.returns(false);
|
|
234
|
+
await httpsCli(42, {
|
|
235
|
+
navy: 'env-1'
|
|
236
|
+
});
|
|
237
|
+
(0, _chai.expect)(createCertStub.called).to.equal(false);
|
|
238
|
+
(0, _chai.expect)(generateRootCaStub.called).to.equal(false);
|
|
239
|
+
});
|
|
240
|
+
it('should treat an empty string service arg as list mode', async function () {
|
|
241
|
+
fsStub.existsSync.returns(false);
|
|
242
|
+
await httpsCli('', {
|
|
243
|
+
navy: 'env-1'
|
|
244
|
+
});
|
|
245
|
+
(0, _chai.expect)(createCertStub.called).to.equal(false);
|
|
246
|
+
});
|
|
247
|
+
});
|
|
248
|
+
describe('validation', function () {
|
|
249
|
+
async function expectNavyError(promise, messagePattern) {
|
|
250
|
+
let caught;
|
|
251
|
+
try {
|
|
252
|
+
await promise;
|
|
253
|
+
} catch (err) {
|
|
254
|
+
caught = err;
|
|
255
|
+
}
|
|
256
|
+
(0, _chai.expect)(caught).to.be.instanceof(_errors.NavyError);
|
|
257
|
+
(0, _chai.expect)(caught.message).to.match(messagePattern);
|
|
258
|
+
}
|
|
259
|
+
it('should reject combining disable with setup', async function () {
|
|
260
|
+
await expectNavyError(httpsCli([], {
|
|
261
|
+
disable: 'web',
|
|
262
|
+
setup: true
|
|
263
|
+
}), /cannot be combined/);
|
|
264
|
+
});
|
|
265
|
+
it('should reject combining disable with all', async function () {
|
|
266
|
+
await expectNavyError(httpsCli([], {
|
|
267
|
+
disable: 'web',
|
|
268
|
+
all: true
|
|
269
|
+
}), /cannot be combined/);
|
|
270
|
+
});
|
|
271
|
+
it('should reject combining disable with issue', async function () {
|
|
272
|
+
await expectNavyError(httpsCli([], {
|
|
273
|
+
disable: 'web',
|
|
274
|
+
issue: 'https://host'
|
|
275
|
+
}), /cannot be combined/);
|
|
276
|
+
});
|
|
277
|
+
it('should reject combining setup with all', async function () {
|
|
278
|
+
await expectNavyError(httpsCli([], {
|
|
279
|
+
setup: true,
|
|
280
|
+
all: true
|
|
281
|
+
}), /Use only one/);
|
|
282
|
+
});
|
|
283
|
+
it('should reject combining setup with issue', async function () {
|
|
284
|
+
await expectNavyError(httpsCli([], {
|
|
285
|
+
setup: true,
|
|
286
|
+
issue: 'https://host'
|
|
287
|
+
}), /Use only one/);
|
|
288
|
+
});
|
|
289
|
+
it('should reject combining all with issue', async function () {
|
|
290
|
+
await expectNavyError(httpsCli([], {
|
|
291
|
+
all: true,
|
|
292
|
+
issue: 'https://host'
|
|
293
|
+
}), /Use only one/);
|
|
294
|
+
});
|
|
295
|
+
});
|
|
296
|
+
describe('setup mode', function () {
|
|
297
|
+
it('should generate the root CA and install it in the trust store', async function () {
|
|
298
|
+
getConfigStub.returns({
|
|
299
|
+
tlsRootCaDir: '/custom-ca'
|
|
300
|
+
});
|
|
301
|
+
await httpsCli([], {
|
|
302
|
+
setup: true
|
|
303
|
+
});
|
|
304
|
+
(0, _chai.expect)(generateRootCaStub.calledOnce).to.equal(true);
|
|
305
|
+
(0, _chai.expect)(installRootCaToTrustStoreStub.calledOnce).to.equal(true);
|
|
306
|
+
(0, _chai.expect)(installRootCaToTrustStoreStub.firstCall.args[0]).to.equal('/custom-ca/ca.crt');
|
|
307
|
+
const printed = consoleLogStub.getCalls().map(c => c.args[0] || '').join('\n');
|
|
308
|
+
(0, _chai.expect)(printed).to.contain('trust store');
|
|
309
|
+
});
|
|
310
|
+
it('should reject service names with setup', async function () {
|
|
311
|
+
let caught;
|
|
312
|
+
try {
|
|
313
|
+
await httpsCli(['web'], {
|
|
314
|
+
setup: true
|
|
315
|
+
});
|
|
316
|
+
} catch (err) {
|
|
317
|
+
caught = err;
|
|
318
|
+
}
|
|
319
|
+
(0, _chai.expect)(caught).to.be.instanceof(_errors.NavyError);
|
|
320
|
+
(0, _chai.expect)(caught.message).to.contain('does not take service names');
|
|
321
|
+
});
|
|
322
|
+
});
|
|
323
|
+
describe('all mode', function () {
|
|
324
|
+
it('should enable HTTPS for every available service', async function () {
|
|
325
|
+
await httpsCli([], {
|
|
326
|
+
all: true,
|
|
327
|
+
navy: 'env-1'
|
|
328
|
+
});
|
|
329
|
+
(0, _chai.expect)(createCertStub.callCount).to.equal(2);
|
|
330
|
+
(0, _chai.expect)(reconfigureHTTPProxyStub.calledOnce).to.equal(true);
|
|
331
|
+
const printed = consoleLogStub.getCalls().map(c => c.args[0] || '').join('\n');
|
|
332
|
+
(0, _chai.expect)(printed).to.contain('web');
|
|
333
|
+
(0, _chai.expect)(printed).to.contain('api');
|
|
334
|
+
});
|
|
335
|
+
it('should generate the root CA when cert files are missing', async function () {
|
|
336
|
+
fsStub.existsSync.callsFake(p => !p.endsWith('ca.crt') && !p.endsWith('ca.key'));
|
|
337
|
+
await httpsCli([], {
|
|
338
|
+
all: true,
|
|
339
|
+
navy: 'env-1'
|
|
340
|
+
});
|
|
341
|
+
(0, _chai.expect)(generateRootCaStub.calledOnce).to.equal(true);
|
|
342
|
+
});
|
|
343
|
+
it('should warn and return when no services are available', async function () {
|
|
344
|
+
navyStub.getAvailableServiceNames.resolves([]);
|
|
345
|
+
await httpsCli([], {
|
|
346
|
+
all: true,
|
|
347
|
+
navy: 'env-1'
|
|
348
|
+
});
|
|
349
|
+
(0, _chai.expect)(createCertStub.called).to.equal(false);
|
|
350
|
+
const printed = consoleLogStub.getCalls().map(c => c.args[0] || '').join('\n');
|
|
351
|
+
(0, _chai.expect)(printed).to.contain('No services found');
|
|
352
|
+
});
|
|
353
|
+
it('should reject service names with all', async function () {
|
|
354
|
+
let caught;
|
|
355
|
+
try {
|
|
356
|
+
await httpsCli(['web'], {
|
|
357
|
+
all: true
|
|
358
|
+
});
|
|
359
|
+
} catch (err) {
|
|
360
|
+
caught = err;
|
|
361
|
+
}
|
|
362
|
+
(0, _chai.expect)(caught).to.be.instanceof(_errors.NavyError);
|
|
363
|
+
(0, _chai.expect)(caught.message).to.contain('does not take service names');
|
|
364
|
+
});
|
|
365
|
+
});
|
|
366
|
+
describe('issue mode', function () {
|
|
367
|
+
it('should issue a leaf cert for an external URL', async function () {
|
|
368
|
+
const cwdStub = sandbox.stub(process, 'cwd').returns('/current');
|
|
369
|
+
await httpsCli([], {
|
|
370
|
+
issue: 'https://external.local',
|
|
371
|
+
navy: 'env-1'
|
|
372
|
+
});
|
|
373
|
+
(0, _chai.expect)(issueLeafCertForUrlStub.calledOnce).to.equal(true);
|
|
374
|
+
(0, _chai.expect)(issueLeafCertForUrlStub.firstCall.args).to.eql(['https://external.local', '/current']);
|
|
375
|
+
const printed = consoleLogStub.getCalls().map(c => c.args[0] || '').join('\n');
|
|
376
|
+
(0, _chai.expect)(printed).to.contain('/out/host.crt');
|
|
377
|
+
(0, _chai.expect)(printed).to.contain('/out/host.key');
|
|
378
|
+
(0, _chai.expect)(printed).to.contain('/out/navy-root-ca.crt');
|
|
379
|
+
cwdStub.restore();
|
|
380
|
+
});
|
|
381
|
+
it('should honour issueOut when provided', async function () {
|
|
382
|
+
await httpsCli([], {
|
|
383
|
+
issue: 'https://external.local',
|
|
384
|
+
issueOut: '/custom/out'
|
|
385
|
+
});
|
|
386
|
+
(0, _chai.expect)(issueLeafCertForUrlStub.firstCall.args[1]).to.equal('/custom/out');
|
|
387
|
+
});
|
|
388
|
+
it('should reject service names with issue', async function () {
|
|
389
|
+
let caught;
|
|
390
|
+
try {
|
|
391
|
+
await httpsCli(['web'], {
|
|
392
|
+
issue: 'https://external.local'
|
|
393
|
+
});
|
|
394
|
+
} catch (err) {
|
|
395
|
+
caught = err;
|
|
396
|
+
}
|
|
397
|
+
(0, _chai.expect)(caught).to.be.instanceof(_errors.NavyError);
|
|
398
|
+
(0, _chai.expect)(caught.message).to.contain('does not take service names');
|
|
399
|
+
});
|
|
208
400
|
});
|
|
209
401
|
});
|
|
210
402
|
});
|
package/lib/cli/https.js
CHANGED
|
@@ -5,16 +5,64 @@ Object.defineProperty(exports, "__esModule", {
|
|
|
5
5
|
value: true
|
|
6
6
|
});
|
|
7
7
|
exports.default = _default;
|
|
8
|
+
var _path = _interopRequireDefault(require("path"));
|
|
8
9
|
var _chalk = _interopRequireDefault(require("chalk"));
|
|
9
10
|
var _errors = require("../errors");
|
|
10
11
|
var _navy = require("../navy");
|
|
11
12
|
var _config = require("../config");
|
|
12
13
|
var _httpProxy = require("../http-proxy");
|
|
13
14
|
var _https = require("../util/https");
|
|
15
|
+
var _installRootCa = require("../util/install-root-ca");
|
|
14
16
|
var _fs = _interopRequireDefault(require("fs"));
|
|
17
|
+
function normaliseServiceArgs(services) {
|
|
18
|
+
if (services == null) {
|
|
19
|
+
return [];
|
|
20
|
+
}
|
|
21
|
+
if (Array.isArray(services)) {
|
|
22
|
+
const result = [];
|
|
23
|
+
for (const s of services) {
|
|
24
|
+
if (typeof s === 'string' && s !== '') {
|
|
25
|
+
result.push(s);
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
return result;
|
|
29
|
+
}
|
|
30
|
+
if (typeof services === 'string') {
|
|
31
|
+
return services ? [services] : [];
|
|
32
|
+
}
|
|
33
|
+
return [];
|
|
34
|
+
}
|
|
35
|
+
async function issueHttpsForServices(navy, serviceNames, availableServices) {
|
|
36
|
+
const httpsReadyServices = [];
|
|
37
|
+
for (const service of serviceNames) {
|
|
38
|
+
if (!availableServices.includes(service)) {
|
|
39
|
+
console.log(`❌ ${service} not found, skipping`);
|
|
40
|
+
continue;
|
|
41
|
+
}
|
|
42
|
+
const serviceUrl = await navy.url(service);
|
|
43
|
+
try {
|
|
44
|
+
await (0, _https.createCert)({
|
|
45
|
+
serviceUrl
|
|
46
|
+
});
|
|
47
|
+
httpsReadyServices.push(service);
|
|
48
|
+
} catch (error) {
|
|
49
|
+
console.log(error);
|
|
50
|
+
throw new _errors.NavyError(`Could not generate TLS cert for ${service}`);
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
return httpsReadyServices;
|
|
54
|
+
}
|
|
15
55
|
async function _default(services, opts) {
|
|
16
56
|
const tlsRootCaDir = (0, _config.getConfig)().tlsRootCaDir || _config.DEFAULT_TLS_ROOT_CA_DIR;
|
|
17
57
|
const configDir = (0, _config.getConfigDir)();
|
|
58
|
+
const serviceList = normaliseServiceArgs(services);
|
|
59
|
+
const hasIssue = Boolean(opts.issue);
|
|
60
|
+
if (opts.disable && (opts.setup || opts.all || hasIssue)) {
|
|
61
|
+
throw new _errors.NavyError('`--disable` cannot be combined with `--setup`, `--all`, or `--issue`.');
|
|
62
|
+
}
|
|
63
|
+
if ([opts.setup, opts.all, hasIssue].filter(Boolean).length > 1) {
|
|
64
|
+
throw new _errors.NavyError('Use only one of `--setup`, `--all`, or `--issue`.');
|
|
65
|
+
}
|
|
18
66
|
if (opts.disable) {
|
|
19
67
|
await (0, _https.removeCert)(opts);
|
|
20
68
|
|
|
@@ -31,7 +79,60 @@ async function _default(services, opts) {
|
|
|
31
79
|
console.log();
|
|
32
80
|
return;
|
|
33
81
|
}
|
|
34
|
-
if (
|
|
82
|
+
if (opts.setup) {
|
|
83
|
+
if (serviceList.length > 0) {
|
|
84
|
+
throw new _errors.NavyError('`navy https --setup` does not take service names. Run `navy https <service>…` separately to issue service certificates.');
|
|
85
|
+
}
|
|
86
|
+
await (0, _https.generateRootCa)();
|
|
87
|
+
const caCrt = _path.default.join(tlsRootCaDir, 'ca.crt');
|
|
88
|
+
(0, _installRootCa.installRootCaToTrustStore)(caCrt);
|
|
89
|
+
console.log();
|
|
90
|
+
console.log(_chalk.default.green('✅ Navy root CA is installed in your trust store (where supported).'));
|
|
91
|
+
console.log(_chalk.default.dim('Browsers that use their own NSS store (e.g. Firefox) may still need the CA imported there.'));
|
|
92
|
+
console.log();
|
|
93
|
+
return;
|
|
94
|
+
}
|
|
95
|
+
if (opts.all) {
|
|
96
|
+
if (serviceList.length > 0) {
|
|
97
|
+
throw new _errors.NavyError('`navy https --all` does not take service names. Omit them, or name services without `--all`.');
|
|
98
|
+
}
|
|
99
|
+
if (!_fs.default.existsSync(`${tlsRootCaDir}/ca.crt`) || !_fs.default.existsSync(`${tlsRootCaDir}/ca.key`)) {
|
|
100
|
+
await (0, _https.generateRootCa)();
|
|
101
|
+
}
|
|
102
|
+
const navy = await (0, _navy.getNavy)(opts.navy);
|
|
103
|
+
await navy.ensurePluginsLoaded();
|
|
104
|
+
const availableServices = await navy.getAvailableServiceNames();
|
|
105
|
+
if (availableServices.length === 0) {
|
|
106
|
+
console.log();
|
|
107
|
+
console.log(_chalk.default.yellow('No services found in this navy; nothing to enable HTTPS for.'));
|
|
108
|
+
console.log();
|
|
109
|
+
return;
|
|
110
|
+
}
|
|
111
|
+
const httpsReadyServices = await issueHttpsForServices(navy, availableServices, availableServices);
|
|
112
|
+
await (0, _httpProxy.reconfigureHTTPProxy)({
|
|
113
|
+
restart: true,
|
|
114
|
+
navyFile: await navy.getNavyFile()
|
|
115
|
+
});
|
|
116
|
+
console.log();
|
|
117
|
+
console.log(_chalk.default.green(`✅ Service(s) ${httpsReadyServices.join(', ')} now accessible via HTTPS🔒`));
|
|
118
|
+
console.log();
|
|
119
|
+
return;
|
|
120
|
+
}
|
|
121
|
+
if (hasIssue) {
|
|
122
|
+
if (serviceList.length > 0) {
|
|
123
|
+
throw new _errors.NavyError('`navy https --issue` does not take service names.');
|
|
124
|
+
}
|
|
125
|
+
const outDir = opts.issueOut ? _path.default.resolve(opts.issueOut) : process.cwd();
|
|
126
|
+
const paths = await (0, _https.issueLeafCertForUrl)(String(opts.issue), outDir);
|
|
127
|
+
console.log();
|
|
128
|
+
console.log(_chalk.default.green('✅ TLS certificate issued for use outside Navy:'));
|
|
129
|
+
console.log(` ${paths.certPath}`);
|
|
130
|
+
console.log(` ${paths.keyPath}`);
|
|
131
|
+
console.log(` ${paths.caCopyPath} (root CA; configure clients to trust this or append as a chain file)`);
|
|
132
|
+
console.log();
|
|
133
|
+
return;
|
|
134
|
+
}
|
|
135
|
+
if (serviceList.length === 0) {
|
|
35
136
|
if (!_fs.default.existsSync(`${configDir}/tls-certs`)) return;
|
|
36
137
|
const files = _fs.default.readdirSync(`${configDir}/tls-certs`);
|
|
37
138
|
const urls = files.filter(file => file.endsWith('.crt')).map(crt => {
|
|
@@ -48,23 +149,7 @@ async function _default(services, opts) {
|
|
|
48
149
|
const navy = await (0, _navy.getNavy)(opts.navy);
|
|
49
150
|
await navy.ensurePluginsLoaded();
|
|
50
151
|
const availableServices = await navy.getAvailableServiceNames();
|
|
51
|
-
const httpsReadyServices =
|
|
52
|
-
for (const service of services) {
|
|
53
|
-
if (!availableServices.includes(service)) {
|
|
54
|
-
console.log(`❌ ${service} not found, skipping`);
|
|
55
|
-
continue;
|
|
56
|
-
}
|
|
57
|
-
const serviceUrl = await navy.url(service);
|
|
58
|
-
try {
|
|
59
|
-
await (0, _https.createCert)({
|
|
60
|
-
serviceUrl
|
|
61
|
-
});
|
|
62
|
-
httpsReadyServices.push(service);
|
|
63
|
-
} catch (error) {
|
|
64
|
-
console.log(error);
|
|
65
|
-
throw new _errors.NavyError(`Could not generate TLS cert for ${service}`);
|
|
66
|
-
}
|
|
67
|
-
}
|
|
152
|
+
const httpsReadyServices = await issueHttpsForServices(navy, serviceList, availableServices);
|
|
68
153
|
await (0, _httpProxy.reconfigureHTTPProxy)({
|
|
69
154
|
restart: true,
|
|
70
155
|
navyFile: await navy.getNavyFile()
|
package/lib/cli/program.js
CHANGED
|
@@ -171,7 +171,7 @@ _commander.program.command('health').option('-e, --navy [env]', `set the navy na
|
|
|
171
171
|
_commander.program.command('wait-for-healthy [services...]').option('-e, --navy [env]', `set the navy name to be used [${defaultNavy}]`, defaultNavy).description('Waits for the given services to be healthy').action(lazyRequire('./wait-for-healthy'));
|
|
172
172
|
_commander.program.command('use-tag <service> <tag>').option('-e, --navy [env]', `set the navy name to be used [${defaultNavy}]`, defaultNavy).description('Uses a specific tag for the given service').action(basicCliWrapper('useTag'));
|
|
173
173
|
_commander.program.command('reset-tag <service>').option('-e, --navy [env]', `set the navy name to be used [${defaultNavy}]`, defaultNavy).description('Resets any tag override on the given service').action(basicCliWrapper('resetTag'));
|
|
174
|
-
_commander.program.command('https [services...]').option('-e, --navy [env]', `set the navy name to be used [${defaultNavy}]`, defaultNavy).option('-d, --disable <service>', 'disable https (deletes cert) for a given service', null).description('Prints or enables HTTPS services').action(lazyRequire('./https')).on('--help', () => console.log(`
|
|
174
|
+
_commander.program.command('https [services...]').option('-e, --navy [env]', `set the navy name to be used [${defaultNavy}]`, defaultNavy).option('-d, --disable <service>', 'disable https (deletes cert) for a given service', null).option('-s, --setup', 'generate the Navy root CA (if needed) and install it in the system trust store').option('-a, --all', 'enable HTTPS for every service in this navy').option('-i, --issue <url>', 'issue a TLS certificate and key for a URL (non-Navy service), signed with the Navy root CA').option('-o, --issue-out <dir>', 'output directory for --issue (default: current working directory)').description('Prints or enables HTTPS services').action(lazyRequire('./https')).on('--help', () => console.log(`
|
|
175
175
|
Examples:
|
|
176
176
|
List urls of services that listen on https:
|
|
177
177
|
$ navy https
|
|
@@ -179,8 +179,18 @@ _commander.program.command('https [services...]').option('-e, --navy [env]', `se
|
|
|
179
179
|
Enable https for mywebservice and anotherwebservice services
|
|
180
180
|
$ navy https mywebservice anotherwebservice
|
|
181
181
|
|
|
182
|
+
Enable https for all services
|
|
183
|
+
$ navy https --all
|
|
184
|
+
|
|
182
185
|
Disable https for mywebservice
|
|
183
186
|
$ navy https -d mywebservice
|
|
187
|
+
|
|
188
|
+
Generate the Navy root CA and install it for this user (no service names)
|
|
189
|
+
$ navy https --setup
|
|
190
|
+
|
|
191
|
+
Issue a cert for an external host (writes leaf key/cert + navy-root-ca.crt)
|
|
192
|
+
$ navy https --issue https://api.example.test
|
|
193
|
+
$ navy https --issue https://api.example.test -o ./tls
|
|
184
194
|
`));
|
|
185
195
|
_commander.program.command('use-port <service> <internal> <external>').option('-e, --navy [env]', `set the navy name to be used [${defaultNavy}]`, defaultNavy).description('Uses a specific external port for the given service and internal port').action(basicCliWrapper('usePort'));
|
|
186
196
|
_commander.program.command('reset-port <service> <internal>').option('-e, --navy [env]', `set the navy name to be used [${defaultNavy}]`, defaultNavy).description('Resets a specific external port mapping set by use-port').action(basicCliWrapper('resetPort'));
|
|
@@ -18,6 +18,7 @@ describe('util/https', function () {
|
|
|
18
18
|
let pkiStub;
|
|
19
19
|
let mdStub;
|
|
20
20
|
let httpsModule;
|
|
21
|
+
let certObj;
|
|
21
22
|
function loadModule() {
|
|
22
23
|
httpsModule = _proxyquire.default.noCallThru()('../https', {
|
|
23
24
|
'../config': {
|
|
@@ -52,9 +53,21 @@ describe('util/https', function () {
|
|
|
52
53
|
mkdirSync: sandbox.stub(),
|
|
53
54
|
unlinkSync: sandbox.stub(),
|
|
54
55
|
readFileSync: sandbox.stub().returns('--PEM--'),
|
|
55
|
-
writeFileSync: sandbox.stub()
|
|
56
|
+
writeFileSync: sandbox.stub(),
|
|
57
|
+
copyFileSync: sandbox.stub()
|
|
56
58
|
};
|
|
57
|
-
const
|
|
59
|
+
const caCertFromPem = {
|
|
60
|
+
subject: {
|
|
61
|
+
attributes: [{
|
|
62
|
+
name: 'commonName',
|
|
63
|
+
value: 'ca'
|
|
64
|
+
}]
|
|
65
|
+
},
|
|
66
|
+
generateSubjectKeyIdentifier: sandbox.stub().returns({
|
|
67
|
+
getBytes: () => 'ca-subject-key-id'
|
|
68
|
+
})
|
|
69
|
+
};
|
|
70
|
+
certObj = {
|
|
58
71
|
publicKey: null,
|
|
59
72
|
serialNumber: null,
|
|
60
73
|
validity: {
|
|
@@ -81,14 +94,7 @@ describe('util/https', function () {
|
|
|
81
94
|
publicKeyToPem: sandbox.stub().returns('pub-pem'),
|
|
82
95
|
certificateToPem: sandbox.stub().returns('cert-pem'),
|
|
83
96
|
privateKeyFromPem: sandbox.stub().returns('priv-key-obj'),
|
|
84
|
-
certificateFromPem: sandbox.stub().returns(
|
|
85
|
-
subject: {
|
|
86
|
-
attributes: [{
|
|
87
|
-
name: 'commonName',
|
|
88
|
-
value: 'ca'
|
|
89
|
-
}]
|
|
90
|
-
}
|
|
91
|
-
})
|
|
97
|
+
certificateFromPem: sandbox.stub().returns(caCertFromPem)
|
|
92
98
|
};
|
|
93
99
|
mdStub = {
|
|
94
100
|
sha256: {
|
|
@@ -297,5 +303,176 @@ describe('util/https', function () {
|
|
|
297
303
|
}
|
|
298
304
|
(0, _chai.expect)(caught).to.be.instanceof(_errors.NavyError);
|
|
299
305
|
});
|
|
306
|
+
it('should prefer the parsed hostname over the raw certName segment', async function () {
|
|
307
|
+
fsStub.existsSync.callsFake(p => {
|
|
308
|
+
if (p === '/cfg/tls-certs') return true;
|
|
309
|
+
if (p === '/default-ca-dir') return true;
|
|
310
|
+
if (p === '/default-ca-dir/ca.crt' || p === '/default-ca-dir/ca.key') return true;
|
|
311
|
+
return false;
|
|
312
|
+
});
|
|
313
|
+
await httpsModule.createCert({
|
|
314
|
+
serviceUrl: 'https://tls-host.example.com:8443/app'
|
|
315
|
+
});
|
|
316
|
+
(0, _chai.expect)(certObj.setSubject.firstCall.args[0][0].value).to.equal('tls-host.example.com');
|
|
317
|
+
});
|
|
318
|
+
it('should keep certName when serviceUrl is not a valid URL', async function () {
|
|
319
|
+
fsStub.existsSync.callsFake(p => {
|
|
320
|
+
if (p === '/cfg/tls-certs') return true;
|
|
321
|
+
if (p === '/default-ca-dir') return true;
|
|
322
|
+
if (p === '/default-ca-dir/ca.crt' || p === '/default-ca-dir/ca.key') return true;
|
|
323
|
+
return false;
|
|
324
|
+
});
|
|
325
|
+
await httpsModule.createCert({
|
|
326
|
+
serviceUrl: 'https://bad['
|
|
327
|
+
});
|
|
328
|
+
const writtenPaths = fsStub.writeFileSync.getCalls().map(c => c.args[0]);
|
|
329
|
+
(0, _chai.expect)(writtenPaths.some(p => p.endsWith('bad[.key'))).to.equal(true);
|
|
330
|
+
});
|
|
331
|
+
it('should fall back to certName when serviceUrl has no hostname', async function () {
|
|
332
|
+
fsStub.existsSync.callsFake(p => {
|
|
333
|
+
if (p === '/cfg/tls-certs') return true;
|
|
334
|
+
if (p === '/default-ca-dir') return true;
|
|
335
|
+
if (p === '/default-ca-dir/ca.crt' || p === '/default-ca-dir/ca.key') return true;
|
|
336
|
+
return false;
|
|
337
|
+
});
|
|
338
|
+
await httpsModule.createCert({
|
|
339
|
+
serviceUrl: 'https://?q=1'
|
|
340
|
+
});
|
|
341
|
+
(0, _chai.expect)(certObj.setSubject.firstCall.args[0][0].value).to.equal('?q=1');
|
|
342
|
+
});
|
|
343
|
+
it('should wrap non-Error signing failures in NavyError', async function () {
|
|
344
|
+
fsStub.existsSync.callsFake(p => {
|
|
345
|
+
if (p === '/cfg/tls-certs') return true;
|
|
346
|
+
if (p === '/default-ca-dir') return true;
|
|
347
|
+
if (p === '/default-ca-dir/ca.crt' || p === '/default-ca-dir/ca.key') return true;
|
|
348
|
+
return false;
|
|
349
|
+
});
|
|
350
|
+
pkiStub.certificateFromPem.callsFake(() => {
|
|
351
|
+
const failure = 'string failure';
|
|
352
|
+
throw failure;
|
|
353
|
+
});
|
|
354
|
+
let caught;
|
|
355
|
+
try {
|
|
356
|
+
await httpsModule.createCert({
|
|
357
|
+
serviceUrl: 'https://web.local'
|
|
358
|
+
});
|
|
359
|
+
} catch (err) {
|
|
360
|
+
caught = err;
|
|
361
|
+
}
|
|
362
|
+
(0, _chai.expect)(caught).to.be.instanceof(_errors.NavyError);
|
|
363
|
+
(0, _chai.expect)(caught.message).to.equal('string failure');
|
|
364
|
+
});
|
|
365
|
+
it('should use IP SAN when hostName is an IP address', async function () {
|
|
366
|
+
fsStub.existsSync.callsFake(p => {
|
|
367
|
+
if (p === '/cfg/tls-certs') return true;
|
|
368
|
+
if (p === '/default-ca-dir') return true;
|
|
369
|
+
if (p === '/default-ca-dir/ca.crt' || p === '/default-ca-dir/ca.key') return true;
|
|
370
|
+
return false;
|
|
371
|
+
});
|
|
372
|
+
await httpsModule.createCert({
|
|
373
|
+
hostName: '127.0.0.1'
|
|
374
|
+
});
|
|
375
|
+
const extensions = certObj.setExtensions.firstCall.args[0];
|
|
376
|
+
const sanExt = extensions.find(ext => ext.name === 'subjectAltName');
|
|
377
|
+
(0, _chai.expect)(sanExt.altNames).to.eql([{
|
|
378
|
+
type: 7,
|
|
379
|
+
ip: '127.0.0.1'
|
|
380
|
+
}]);
|
|
381
|
+
});
|
|
382
|
+
});
|
|
383
|
+
describe('hostNameFromIssueUrl', function () {
|
|
384
|
+
it('should throw when the URL is empty', function () {
|
|
385
|
+
(0, _chai.expect)(() => httpsModule.hostNameFromIssueUrl('')).to.throw(/must not be empty/);
|
|
386
|
+
(0, _chai.expect)(() => httpsModule.hostNameFromIssueUrl(' ')).to.throw(/must not be empty/);
|
|
387
|
+
});
|
|
388
|
+
it('should prepend https when no scheme is supplied', function () {
|
|
389
|
+
(0, _chai.expect)(httpsModule.hostNameFromIssueUrl('example.local')).to.equal('example.local');
|
|
390
|
+
});
|
|
391
|
+
it('should accept an explicit scheme', function () {
|
|
392
|
+
(0, _chai.expect)(httpsModule.hostNameFromIssueUrl('http://example.local')).to.equal('example.local');
|
|
393
|
+
});
|
|
394
|
+
it('should throw when the URL cannot be parsed', function () {
|
|
395
|
+
(0, _chai.expect)(() => httpsModule.hostNameFromIssueUrl('http://')).to.throw(/Invalid URL/);
|
|
396
|
+
});
|
|
397
|
+
it('should throw when no hostname can be determined', function () {
|
|
398
|
+
(0, _chai.expect)(() => httpsModule.hostNameFromIssueUrl('file:///tmp/cert.crt')).to.throw(/Could not determine a hostname/);
|
|
399
|
+
});
|
|
400
|
+
});
|
|
401
|
+
describe('issueLeafCertForUrl', function () {
|
|
402
|
+
it('should write cert, key, and CA copy files to the output dir', async function () {
|
|
403
|
+
fsStub.existsSync.callsFake(p => {
|
|
404
|
+
if (p === '/default-ca-dir') return true;
|
|
405
|
+
if (p === '/default-ca-dir/ca.crt' || p === '/default-ca-dir/ca.key') return true;
|
|
406
|
+
return false;
|
|
407
|
+
});
|
|
408
|
+
const result = await httpsModule.issueLeafCertForUrl('https://issued.local', '/out/certs');
|
|
409
|
+
(0, _chai.expect)(fsStub.mkdirSync.calledWith('/out/certs', {
|
|
410
|
+
recursive: true
|
|
411
|
+
})).to.equal(true);
|
|
412
|
+
(0, _chai.expect)(fsStub.writeFileSync.callCount).to.equal(2);
|
|
413
|
+
(0, _chai.expect)(fsStub.copyFileSync.calledOnce).to.equal(true);
|
|
414
|
+
(0, _chai.expect)(result.certPath).to.equal('/out/certs/issued.local.crt');
|
|
415
|
+
(0, _chai.expect)(result.keyPath).to.equal('/out/certs/issued.local.key');
|
|
416
|
+
(0, _chai.expect)(result.caCopyPath).to.equal('/out/certs/navy-root-ca.crt');
|
|
417
|
+
});
|
|
418
|
+
it('should sanitise unsafe characters in the output file base name', async function () {
|
|
419
|
+
fsStub.existsSync.callsFake(p => {
|
|
420
|
+
if (p === '/default-ca-dir') return true;
|
|
421
|
+
if (p === '/default-ca-dir/ca.crt' || p === '/default-ca-dir/ca.key') return true;
|
|
422
|
+
return false;
|
|
423
|
+
});
|
|
424
|
+
const result = await httpsModule.issueLeafCertForUrl('https://[::1]', '/out');
|
|
425
|
+
(0, _chai.expect)(result.certPath).to.equal('/out/___1_.crt');
|
|
426
|
+
(0, _chai.expect)(result.keyPath).to.equal('/out/___1_.key');
|
|
427
|
+
});
|
|
428
|
+
it('should wrap non-Error build failures in NavyError', async function () {
|
|
429
|
+
fsStub.existsSync.callsFake(p => {
|
|
430
|
+
if (p === '/default-ca-dir') return true;
|
|
431
|
+
if (p === '/default-ca-dir/ca.crt' || p === '/default-ca-dir/ca.key') return true;
|
|
432
|
+
return false;
|
|
433
|
+
});
|
|
434
|
+
pkiStub.certificateFromPem.callsFake(() => {
|
|
435
|
+
const failure = 'forge broke';
|
|
436
|
+
throw failure;
|
|
437
|
+
});
|
|
438
|
+
let caught;
|
|
439
|
+
try {
|
|
440
|
+
await httpsModule.issueLeafCertForUrl('https://issued.local', '/out');
|
|
441
|
+
} catch (err) {
|
|
442
|
+
caught = err;
|
|
443
|
+
}
|
|
444
|
+
(0, _chai.expect)(caught).to.be.instanceof(_errors.NavyError);
|
|
445
|
+
(0, _chai.expect)(caught.message).to.equal('forge broke');
|
|
446
|
+
});
|
|
447
|
+
it('should wrap build failures in NavyError', async function () {
|
|
448
|
+
fsStub.existsSync.callsFake(p => {
|
|
449
|
+
if (p === '/default-ca-dir') return true;
|
|
450
|
+
if (p === '/default-ca-dir/ca.crt' || p === '/default-ca-dir/ca.key') return true;
|
|
451
|
+
return false;
|
|
452
|
+
});
|
|
453
|
+
pkiStub.certificateFromPem.throws(new Error('forge broke'));
|
|
454
|
+
let caught;
|
|
455
|
+
try {
|
|
456
|
+
await httpsModule.issueLeafCertForUrl('https://issued.local', '/out');
|
|
457
|
+
} catch (err) {
|
|
458
|
+
caught = err;
|
|
459
|
+
}
|
|
460
|
+
(0, _chai.expect)(caught).to.be.instanceof(_errors.NavyError);
|
|
461
|
+
(0, _chai.expect)(caught.message).to.equal('forge broke');
|
|
462
|
+
});
|
|
463
|
+
it('should use IP SAN when issuing for an IP address URL', async function () {
|
|
464
|
+
fsStub.existsSync.callsFake(p => {
|
|
465
|
+
if (p === '/default-ca-dir') return true;
|
|
466
|
+
if (p === '/default-ca-dir/ca.crt' || p === '/default-ca-dir/ca.key') return true;
|
|
467
|
+
return false;
|
|
468
|
+
});
|
|
469
|
+
await httpsModule.issueLeafCertForUrl('https://192.168.1.10', '/out');
|
|
470
|
+
const extensions = certObj.setExtensions.firstCall.args[0];
|
|
471
|
+
const sanExt = extensions.find(ext => ext.name === 'subjectAltName');
|
|
472
|
+
(0, _chai.expect)(sanExt.altNames).to.eql([{
|
|
473
|
+
type: 7,
|
|
474
|
+
ip: '192.168.1.10'
|
|
475
|
+
}]);
|
|
476
|
+
});
|
|
300
477
|
});
|
|
301
478
|
});
|
|
@@ -0,0 +1,282 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
var _interopRequireDefault = require("@babel/runtime/helpers/interopRequireDefault");
|
|
4
|
+
var _chai = require("chai");
|
|
5
|
+
var _sinon = _interopRequireDefault(require("sinon"));
|
|
6
|
+
var _proxyquire = _interopRequireDefault(require("proxyquire"));
|
|
7
|
+
var _path = _interopRequireDefault(require("path"));
|
|
8
|
+
var _errors = require("../../errors");
|
|
9
|
+
/* eslint-env mocha */
|
|
10
|
+
|
|
11
|
+
describe('util/install-root-ca', function () {
|
|
12
|
+
let sandbox;
|
|
13
|
+
let fsStub;
|
|
14
|
+
let osStub;
|
|
15
|
+
let execFileSyncStub;
|
|
16
|
+
let installRootCaModule;
|
|
17
|
+
let platformStub;
|
|
18
|
+
const caPath = '/tmp/navy/ca.crt';
|
|
19
|
+
function certutilPath(systemRoot = 'C:\\Windows') {
|
|
20
|
+
return _path.default.join(systemRoot, 'System32', 'certutil.exe');
|
|
21
|
+
}
|
|
22
|
+
function expectThrownMessage(fn, messagePattern) {
|
|
23
|
+
let caught;
|
|
24
|
+
try {
|
|
25
|
+
fn();
|
|
26
|
+
} catch (err) {
|
|
27
|
+
caught = err;
|
|
28
|
+
}
|
|
29
|
+
(0, _chai.expect)(caught).to.not.equal(undefined);
|
|
30
|
+
(0, _chai.expect)(String(caught.message)).to.match(messagePattern);
|
|
31
|
+
}
|
|
32
|
+
function loadModule() {
|
|
33
|
+
installRootCaModule = _proxyquire.default.noCallThru()('../install-root-ca', {
|
|
34
|
+
'../errors': {
|
|
35
|
+
NavyError: _errors.NavyError
|
|
36
|
+
},
|
|
37
|
+
fs: fsStub,
|
|
38
|
+
os: osStub,
|
|
39
|
+
child_process: {
|
|
40
|
+
execFileSync: execFileSyncStub
|
|
41
|
+
}
|
|
42
|
+
});
|
|
43
|
+
}
|
|
44
|
+
beforeEach(function () {
|
|
45
|
+
sandbox = _sinon.default.createSandbox();
|
|
46
|
+
fsStub = {
|
|
47
|
+
existsSync: sandbox.stub()
|
|
48
|
+
};
|
|
49
|
+
osStub = {
|
|
50
|
+
homedir: sandbox.stub().returns('/Users/tester')
|
|
51
|
+
};
|
|
52
|
+
execFileSyncStub = sandbox.stub();
|
|
53
|
+
platformStub = sandbox.stub(process, 'platform').value('darwin');
|
|
54
|
+
loadModule();
|
|
55
|
+
});
|
|
56
|
+
afterEach(function () {
|
|
57
|
+
sandbox.restore();
|
|
58
|
+
});
|
|
59
|
+
describe('caughtErrorMessage', function () {
|
|
60
|
+
it('should return nullish values as strings', function () {
|
|
61
|
+
(0, _chai.expect)(installRootCaModule.caughtErrorMessage(null)).to.equal('null');
|
|
62
|
+
(0, _chai.expect)(installRootCaModule.caughtErrorMessage(undefined)).to.equal('undefined');
|
|
63
|
+
});
|
|
64
|
+
it('should return message text when it is present', function () {
|
|
65
|
+
(0, _chai.expect)(installRootCaModule.caughtErrorMessage({
|
|
66
|
+
message: 'object failed'
|
|
67
|
+
})).to.equal('object failed');
|
|
68
|
+
(0, _chai.expect)(installRootCaModule.caughtErrorMessage(new Error('error failed'))).to.equal('error failed');
|
|
69
|
+
});
|
|
70
|
+
it('should fall back to String(e) when no usable message exists', function () {
|
|
71
|
+
(0, _chai.expect)(installRootCaModule.caughtErrorMessage('plain failure')).to.equal('plain failure');
|
|
72
|
+
(0, _chai.expect)(installRootCaModule.caughtErrorMessage({})).to.equal('[object Object]');
|
|
73
|
+
(0, _chai.expect)(installRootCaModule.caughtErrorMessage({
|
|
74
|
+
message: ''
|
|
75
|
+
})).to.equal('[object Object]');
|
|
76
|
+
});
|
|
77
|
+
});
|
|
78
|
+
describe('installRootCaToTrustStore', function () {
|
|
79
|
+
it('should throw when the CA file does not exist', function () {
|
|
80
|
+
fsStub.existsSync.returns(false);
|
|
81
|
+
expectThrownMessage(() => installRootCaModule.installRootCaToTrustStore(caPath), /CA certificate not found/);
|
|
82
|
+
});
|
|
83
|
+
describe('macOS', function () {
|
|
84
|
+
beforeEach(function () {
|
|
85
|
+
platformStub.value('darwin');
|
|
86
|
+
fsStub.existsSync.callsFake(p => p === caPath || p.endsWith('login.keychain-db'));
|
|
87
|
+
});
|
|
88
|
+
it('should add the CA to the login keychain when found', function () {
|
|
89
|
+
installRootCaModule.installRootCaToTrustStore(caPath);
|
|
90
|
+
(0, _chai.expect)(execFileSyncStub.calledOnce).to.equal(true);
|
|
91
|
+
(0, _chai.expect)(execFileSyncStub.firstCall.args[0]).to.equal('security');
|
|
92
|
+
(0, _chai.expect)(execFileSyncStub.firstCall.args[1]).to.eql(['add-trusted-cert', '-d', '-r', 'trustRoot', '-k', '/Users/tester/Library/Keychains/login.keychain-db', caPath]);
|
|
93
|
+
});
|
|
94
|
+
it('should fall back to login.keychain when login.keychain-db is missing', function () {
|
|
95
|
+
fsStub.existsSync.callsFake(p => p === caPath || p.endsWith('login.keychain'));
|
|
96
|
+
installRootCaModule.installRootCaToTrustStore(caPath);
|
|
97
|
+
(0, _chai.expect)(execFileSyncStub.firstCall.args[1][5]).to.equal('/Users/tester/Library/Keychains/login.keychain');
|
|
98
|
+
});
|
|
99
|
+
it('should throw when no login keychain can be found', function () {
|
|
100
|
+
fsStub.existsSync.callsFake(p => p === caPath);
|
|
101
|
+
expectThrownMessage(() => installRootCaModule.installRootCaToTrustStore(caPath), /Could not find your macOS login keychain/);
|
|
102
|
+
});
|
|
103
|
+
it('should wrap security command failures in NavyError', function () {
|
|
104
|
+
execFileSyncStub.throws(new Error('security failed'));
|
|
105
|
+
expectThrownMessage(() => installRootCaModule.installRootCaToTrustStore(caPath), /Could not add the Navy root CA/);
|
|
106
|
+
});
|
|
107
|
+
it('should use error.message when the thrown value exposes one', function () {
|
|
108
|
+
execFileSyncStub.throws({
|
|
109
|
+
message: 'security object failed'
|
|
110
|
+
});
|
|
111
|
+
expectThrownMessage(() => installRootCaModule.installRootCaToTrustStore(caPath), /security object failed/);
|
|
112
|
+
});
|
|
113
|
+
it('should stringify non-Error exec failures', function () {
|
|
114
|
+
execFileSyncStub.throws('plain failure');
|
|
115
|
+
expectThrownMessage(() => installRootCaModule.installRootCaToTrustStore(caPath), /plain failure/);
|
|
116
|
+
});
|
|
117
|
+
it('should stringify null exec failures', function () {
|
|
118
|
+
execFileSyncStub.callsFake(() => {
|
|
119
|
+
const thrown = null;
|
|
120
|
+
throw thrown;
|
|
121
|
+
});
|
|
122
|
+
expectThrownMessage(() => installRootCaModule.installRootCaToTrustStore(caPath), /null/);
|
|
123
|
+
});
|
|
124
|
+
});
|
|
125
|
+
describe('linux', function () {
|
|
126
|
+
beforeEach(function () {
|
|
127
|
+
platformStub.value('linux');
|
|
128
|
+
fsStub.existsSync.callsFake(p => p === caPath);
|
|
129
|
+
});
|
|
130
|
+
it('should install via update-ca-certificates on Debian-style systems', function () {
|
|
131
|
+
fsStub.existsSync.callsFake(p => {
|
|
132
|
+
if (p === caPath) return true;
|
|
133
|
+
if (p === '/usr/sbin/update-ca-certificates') return true;
|
|
134
|
+
return false;
|
|
135
|
+
});
|
|
136
|
+
installRootCaModule.installRootCaToTrustStore(caPath);
|
|
137
|
+
(0, _chai.expect)(execFileSyncStub.callCount).to.equal(2);
|
|
138
|
+
(0, _chai.expect)(execFileSyncStub.firstCall.args[0]).to.equal('sudo');
|
|
139
|
+
(0, _chai.expect)(execFileSyncStub.firstCall.args[1]).to.eql(['cp', caPath, '/usr/local/share/ca-certificates/navy-dev-ca.crt']);
|
|
140
|
+
(0, _chai.expect)(execFileSyncStub.secondCall.args[1]).to.eql(['/usr/sbin/update-ca-certificates']);
|
|
141
|
+
});
|
|
142
|
+
it('should wrap Debian install failures in NavyError', function () {
|
|
143
|
+
fsStub.existsSync.callsFake(p => {
|
|
144
|
+
if (p === caPath) return true;
|
|
145
|
+
if (p === '/usr/sbin/update-ca-certificates') return true;
|
|
146
|
+
return false;
|
|
147
|
+
});
|
|
148
|
+
execFileSyncStub.throws(new Error('sudo denied'));
|
|
149
|
+
expectThrownMessage(() => installRootCaModule.installRootCaToTrustStore(caPath), /update-ca-certificates/);
|
|
150
|
+
});
|
|
151
|
+
it('should use error.message for Debian install failures when present', function () {
|
|
152
|
+
fsStub.existsSync.callsFake(p => {
|
|
153
|
+
if (p === caPath) return true;
|
|
154
|
+
if (p === '/usr/sbin/update-ca-certificates') return true;
|
|
155
|
+
return false;
|
|
156
|
+
});
|
|
157
|
+
execFileSyncStub.throws({
|
|
158
|
+
message: 'debian object failed'
|
|
159
|
+
});
|
|
160
|
+
expectThrownMessage(() => installRootCaModule.installRootCaToTrustStore(caPath), /debian object failed/);
|
|
161
|
+
});
|
|
162
|
+
it('should stringify null Debian install failures', function () {
|
|
163
|
+
fsStub.existsSync.callsFake(p => {
|
|
164
|
+
if (p === caPath) return true;
|
|
165
|
+
if (p === '/usr/sbin/update-ca-certificates') return true;
|
|
166
|
+
return false;
|
|
167
|
+
});
|
|
168
|
+
execFileSyncStub.callsFake(() => {
|
|
169
|
+
const thrown = null;
|
|
170
|
+
throw thrown;
|
|
171
|
+
});
|
|
172
|
+
expectThrownMessage(() => installRootCaModule.installRootCaToTrustStore(caPath), /null/);
|
|
173
|
+
});
|
|
174
|
+
it('should install via update-ca-trust on Fedora-style systems', function () {
|
|
175
|
+
fsStub.existsSync.callsFake(p => {
|
|
176
|
+
if (p === caPath) return true;
|
|
177
|
+
if (p === '/usr/bin/update-ca-trust') return true;
|
|
178
|
+
return false;
|
|
179
|
+
});
|
|
180
|
+
installRootCaModule.installRootCaToTrustStore(caPath);
|
|
181
|
+
(0, _chai.expect)(execFileSyncStub.callCount).to.equal(2);
|
|
182
|
+
(0, _chai.expect)(execFileSyncStub.secondCall.args[1]).to.eql(['/usr/bin/update-ca-trust', 'extract']);
|
|
183
|
+
});
|
|
184
|
+
it('should wrap Fedora install failures in NavyError', function () {
|
|
185
|
+
fsStub.existsSync.callsFake(p => {
|
|
186
|
+
if (p === caPath) return true;
|
|
187
|
+
if (p === '/usr/bin/update-ca-trust') return true;
|
|
188
|
+
return false;
|
|
189
|
+
});
|
|
190
|
+
execFileSyncStub.throws(new Error('trust failed'));
|
|
191
|
+
expectThrownMessage(() => installRootCaModule.installRootCaToTrustStore(caPath), /update-ca-trust/);
|
|
192
|
+
});
|
|
193
|
+
it('should use error.message for Fedora install failures when present', function () {
|
|
194
|
+
fsStub.existsSync.callsFake(p => {
|
|
195
|
+
if (p === caPath) return true;
|
|
196
|
+
if (p === '/usr/bin/update-ca-trust') return true;
|
|
197
|
+
return false;
|
|
198
|
+
});
|
|
199
|
+
execFileSyncStub.throws({
|
|
200
|
+
message: 'fedora object failed'
|
|
201
|
+
});
|
|
202
|
+
expectThrownMessage(() => installRootCaModule.installRootCaToTrustStore(caPath), /fedora object failed/);
|
|
203
|
+
});
|
|
204
|
+
it('should stringify null Fedora install failures', function () {
|
|
205
|
+
fsStub.existsSync.callsFake(p => {
|
|
206
|
+
if (p === caPath) return true;
|
|
207
|
+
if (p === '/usr/bin/update-ca-trust') return true;
|
|
208
|
+
return false;
|
|
209
|
+
});
|
|
210
|
+
execFileSyncStub.callsFake(() => {
|
|
211
|
+
const thrown = null;
|
|
212
|
+
throw thrown;
|
|
213
|
+
});
|
|
214
|
+
expectThrownMessage(() => installRootCaModule.installRootCaToTrustStore(caPath), /null/);
|
|
215
|
+
});
|
|
216
|
+
it('should throw when no supported Linux tooling is present', function () {
|
|
217
|
+
expectThrownMessage(() => installRootCaModule.installRootCaToTrustStore(caPath), /Automatic Linux install needs/);
|
|
218
|
+
});
|
|
219
|
+
});
|
|
220
|
+
describe('windows', function () {
|
|
221
|
+
beforeEach(function () {
|
|
222
|
+
platformStub.value('win32');
|
|
223
|
+
fsStub.existsSync.callsFake(p => p === caPath);
|
|
224
|
+
});
|
|
225
|
+
it('should add the CA via certutil when available', function () {
|
|
226
|
+
const certutil = certutilPath();
|
|
227
|
+
fsStub.existsSync.callsFake(p => p === caPath || p === certutil);
|
|
228
|
+
installRootCaModule.installRootCaToTrustStore(caPath);
|
|
229
|
+
(0, _chai.expect)(execFileSyncStub.calledOnce).to.equal(true);
|
|
230
|
+
(0, _chai.expect)(execFileSyncStub.firstCall.args[0]).to.equal(certutil);
|
|
231
|
+
(0, _chai.expect)(execFileSyncStub.firstCall.args[1]).to.eql(['-user', '-addstore', 'Root', caPath]);
|
|
232
|
+
});
|
|
233
|
+
it('should use SystemRoot from the environment when set', function () {
|
|
234
|
+
const previousSystemRoot = process.env.SystemRoot;
|
|
235
|
+
process.env.SystemRoot = 'D:\\WinRoot';
|
|
236
|
+
const certutil = certutilPath('D:\\WinRoot');
|
|
237
|
+
fsStub.existsSync.callsFake(p => p === caPath || p === certutil);
|
|
238
|
+
try {
|
|
239
|
+
installRootCaModule.installRootCaToTrustStore(caPath);
|
|
240
|
+
(0, _chai.expect)(execFileSyncStub.firstCall.args[0]).to.equal(certutil);
|
|
241
|
+
} finally {
|
|
242
|
+
if (previousSystemRoot === undefined) {
|
|
243
|
+
delete process.env.SystemRoot;
|
|
244
|
+
} else {
|
|
245
|
+
process.env.SystemRoot = previousSystemRoot;
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
});
|
|
249
|
+
it('should throw when certutil.exe is missing', function () {
|
|
250
|
+
expectThrownMessage(() => installRootCaModule.installRootCaToTrustStore(caPath), /certutil.exe not found/);
|
|
251
|
+
});
|
|
252
|
+
it('should wrap certutil failures in NavyError', function () {
|
|
253
|
+
const certutil = certutilPath();
|
|
254
|
+
fsStub.existsSync.callsFake(p => p === caPath || p === certutil);
|
|
255
|
+
execFileSyncStub.throws(new Error('certutil failed'));
|
|
256
|
+
expectThrownMessage(() => installRootCaModule.installRootCaToTrustStore(caPath), /Trusted Root store/);
|
|
257
|
+
});
|
|
258
|
+
it('should use error.message for certutil failures when present', function () {
|
|
259
|
+
const certutil = certutilPath();
|
|
260
|
+
fsStub.existsSync.callsFake(p => p === caPath || p === certutil);
|
|
261
|
+
execFileSyncStub.throws({
|
|
262
|
+
message: 'certutil object failed'
|
|
263
|
+
});
|
|
264
|
+
expectThrownMessage(() => installRootCaModule.installRootCaToTrustStore(caPath), /certutil object failed/);
|
|
265
|
+
});
|
|
266
|
+
it('should stringify null certutil failures', function () {
|
|
267
|
+
const certutil = certutilPath();
|
|
268
|
+
fsStub.existsSync.callsFake(p => p === caPath || p === certutil);
|
|
269
|
+
execFileSyncStub.callsFake(() => {
|
|
270
|
+
const thrown = null;
|
|
271
|
+
throw thrown;
|
|
272
|
+
});
|
|
273
|
+
expectThrownMessage(() => installRootCaModule.installRootCaToTrustStore(caPath), /null/);
|
|
274
|
+
});
|
|
275
|
+
});
|
|
276
|
+
it('should throw on unsupported platforms', function () {
|
|
277
|
+
platformStub.value('freebsd');
|
|
278
|
+
fsStub.existsSync.returns(true);
|
|
279
|
+
expectThrownMessage(() => installRootCaModule.installRootCaToTrustStore(caPath), /not supported on freebsd/);
|
|
280
|
+
});
|
|
281
|
+
});
|
|
282
|
+
});
|
package/lib/util/https.js
CHANGED
|
@@ -7,8 +7,13 @@ Object.defineProperty(exports, "__esModule", {
|
|
|
7
7
|
exports.createCert = createCert;
|
|
8
8
|
exports.generateRootCa = generateRootCa;
|
|
9
9
|
exports.getCertsPath = getCertsPath;
|
|
10
|
+
exports.hostNameFromIssueUrl = hostNameFromIssueUrl;
|
|
11
|
+
exports.issueLeafCertForUrl = issueLeafCertForUrl;
|
|
10
12
|
exports.removeCert = removeCert;
|
|
11
13
|
var _path = _interopRequireDefault(require("path"));
|
|
14
|
+
var _crypto = _interopRequireDefault(require("crypto"));
|
|
15
|
+
var _net = _interopRequireDefault(require("net"));
|
|
16
|
+
var _url = require("url");
|
|
12
17
|
var _config = require("../config");
|
|
13
18
|
var _errors = require("../errors");
|
|
14
19
|
var _chalk = _interopRequireDefault(require("chalk"));
|
|
@@ -16,6 +21,136 @@ var _fs = _interopRequireDefault(require("fs"));
|
|
|
16
21
|
var _nodeForge = require("node-forge");
|
|
17
22
|
var _navy = require("../navy");
|
|
18
23
|
const debug = require('debug')('navy:https');
|
|
24
|
+
|
|
25
|
+
/** Random positive DER INTEGER serial (hex), for unique X.509 serialNumber fields. */
|
|
26
|
+
function randomSerialHex(numBytes = 16) {
|
|
27
|
+
const buf = _crypto.default.randomBytes(numBytes);
|
|
28
|
+
buf[0] &= 0x7f;
|
|
29
|
+
return buf.toString('hex');
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/** Hostname (or IP string) from a user-supplied URL for TLS CN/SAN. */
|
|
33
|
+
function hostNameFromIssueUrl(urlString) {
|
|
34
|
+
const trimmed = (urlString || '').trim();
|
|
35
|
+
if (!trimmed) {
|
|
36
|
+
throw new _errors.NavyError('URL for --issue must not be empty.');
|
|
37
|
+
}
|
|
38
|
+
const withScheme = /^[a-zA-Z][a-zA-Z0-9+.-]*:/.test(trimmed) ? trimmed : `https://${trimmed}`;
|
|
39
|
+
let parsed;
|
|
40
|
+
try {
|
|
41
|
+
parsed = new _url.URL(withScheme);
|
|
42
|
+
} catch (e) {
|
|
43
|
+
throw new _errors.NavyError(`Invalid URL for --issue: ${urlString}`);
|
|
44
|
+
}
|
|
45
|
+
if (!parsed.hostname) {
|
|
46
|
+
throw new _errors.NavyError(`Could not determine a hostname from URL: ${urlString}`);
|
|
47
|
+
}
|
|
48
|
+
return parsed.hostname;
|
|
49
|
+
}
|
|
50
|
+
function safeCertFileBase(host) {
|
|
51
|
+
return host.replace(/[^a-zA-Z0-9._-]/g, '_');
|
|
52
|
+
}
|
|
53
|
+
function subjectAltNamesForHost(host) {
|
|
54
|
+
if (_net.default.isIP(host)) {
|
|
55
|
+
return [{
|
|
56
|
+
type: 7,
|
|
57
|
+
ip: host
|
|
58
|
+
}];
|
|
59
|
+
}
|
|
60
|
+
return [{
|
|
61
|
+
type: 2,
|
|
62
|
+
value: host
|
|
63
|
+
}];
|
|
64
|
+
}
|
|
65
|
+
function buildSignedLeafPems(certName, tlsRootCaDir, sanAltNames) {
|
|
66
|
+
const caCertString = _fs.default.readFileSync(`${tlsRootCaDir}/ca.crt`, 'utf8');
|
|
67
|
+
const caKeyString = _fs.default.readFileSync(`${tlsRootCaDir}/ca.key`, 'utf8');
|
|
68
|
+
const privateCAKey = _nodeForge.pki.privateKeyFromPem(caKeyString);
|
|
69
|
+
const keys = _nodeForge.pki.rsa.generateKeyPair(2048);
|
|
70
|
+
const cert = _nodeForge.pki.createCertificate();
|
|
71
|
+
const caCert = _nodeForge.pki.certificateFromPem(caCertString);
|
|
72
|
+
cert.publicKey = keys.publicKey;
|
|
73
|
+
cert.serialNumber = randomSerialHex(16);
|
|
74
|
+
cert.validity.notBefore = new Date();
|
|
75
|
+
cert.validity.notBefore.setDate(cert.validity.notBefore.getDate() - 1);
|
|
76
|
+
cert.validity.notAfter = new Date();
|
|
77
|
+
cert.validity.notAfter.setFullYear(cert.validity.notBefore.getFullYear() + 2);
|
|
78
|
+
const attrs = [{
|
|
79
|
+
name: 'commonName',
|
|
80
|
+
value: certName
|
|
81
|
+
}, {
|
|
82
|
+
name: 'organizationName',
|
|
83
|
+
value: 'navy-dev'
|
|
84
|
+
}];
|
|
85
|
+
cert.setSubject(attrs);
|
|
86
|
+
cert.setIssuer(caCert.subject.attributes);
|
|
87
|
+
const caSubjectKeyId = caCert.generateSubjectKeyIdentifier().getBytes();
|
|
88
|
+
cert.setExtensions([{
|
|
89
|
+
name: 'basicConstraints',
|
|
90
|
+
cA: false,
|
|
91
|
+
critical: true
|
|
92
|
+
}, {
|
|
93
|
+
name: 'keyUsage',
|
|
94
|
+
digitalSignature: true,
|
|
95
|
+
keyEncipherment: true,
|
|
96
|
+
critical: true
|
|
97
|
+
}, {
|
|
98
|
+
name: 'extKeyUsage',
|
|
99
|
+
serverAuth: true
|
|
100
|
+
}, {
|
|
101
|
+
name: 'subjectAltName',
|
|
102
|
+
altNames: sanAltNames
|
|
103
|
+
}, {
|
|
104
|
+
name: 'subjectKeyIdentifier'
|
|
105
|
+
}, {
|
|
106
|
+
name: 'authorityKeyIdentifier',
|
|
107
|
+
keyIdentifier: caSubjectKeyId
|
|
108
|
+
}]);
|
|
109
|
+
cert.sign(privateCAKey, _nodeForge.md.sha256.create());
|
|
110
|
+
return {
|
|
111
|
+
privateKey: _nodeForge.pki.privateKeyToPem(keys.privateKey),
|
|
112
|
+
certificate: _nodeForge.pki.certificateToPem(cert)
|
|
113
|
+
};
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* Issues a leaf TLS certificate and key for an arbitrary host, signed with the Navy root CA,
|
|
118
|
+
* plus a copy of the root CA PEM for configuring trust / full chain on a non-Navy service.
|
|
119
|
+
*/
|
|
120
|
+
async function issueLeafCertForUrl(urlString, outputDir) {
|
|
121
|
+
const certName = hostNameFromIssueUrl(urlString);
|
|
122
|
+
const tlsRootCaDir = (0, _config.getConfig)().tlsRootCaDir || _config.DEFAULT_TLS_ROOT_CA_DIR;
|
|
123
|
+
await generateRootCa();
|
|
124
|
+
const out = _path.default.resolve(outputDir);
|
|
125
|
+
// $FlowIgnore
|
|
126
|
+
_fs.default.mkdirSync(out, {
|
|
127
|
+
recursive: true
|
|
128
|
+
});
|
|
129
|
+
const base = safeCertFileBase(certName);
|
|
130
|
+
const san = subjectAltNamesForHost(certName);
|
|
131
|
+
let pem;
|
|
132
|
+
try {
|
|
133
|
+
pem = buildSignedLeafPems(certName, tlsRootCaDir, san);
|
|
134
|
+
} catch (e) {
|
|
135
|
+
throw new _errors.NavyError(e instanceof Error ? e.message : String(e));
|
|
136
|
+
}
|
|
137
|
+
const certPath = _path.default.join(out, `${base}.crt`);
|
|
138
|
+
const keyPath = _path.default.join(out, `${base}.key`);
|
|
139
|
+
const caCertSrc = _path.default.join(tlsRootCaDir, 'ca.crt');
|
|
140
|
+
const caCopyPath = _path.default.join(out, 'navy-root-ca.crt');
|
|
141
|
+
_fs.default.writeFileSync(keyPath, pem.privateKey, {
|
|
142
|
+
mode: 0o600
|
|
143
|
+
});
|
|
144
|
+
_fs.default.writeFileSync(certPath, pem.certificate, {
|
|
145
|
+
mode: 0o644
|
|
146
|
+
});
|
|
147
|
+
_fs.default.copyFileSync(caCertSrc, caCopyPath);
|
|
148
|
+
return {
|
|
149
|
+
certPath,
|
|
150
|
+
keyPath,
|
|
151
|
+
caCopyPath
|
|
152
|
+
};
|
|
153
|
+
}
|
|
19
154
|
function getCertsPath(create = false) {
|
|
20
155
|
const certsPath = _path.default.join((0, _config.getConfigDir)(), 'tls-certs');
|
|
21
156
|
if (!_fs.default.existsSync(certsPath)) {
|
|
@@ -70,7 +205,7 @@ async function generateRootCa() {
|
|
|
70
205
|
debug('Creating self-signed certificate...');
|
|
71
206
|
const cert = _nodeForge.pki.createCertificate();
|
|
72
207
|
cert.publicKey = keys.publicKey;
|
|
73
|
-
cert.serialNumber =
|
|
208
|
+
cert.serialNumber = randomSerialHex(8);
|
|
74
209
|
cert.validity.notBefore = new Date();
|
|
75
210
|
cert.validity.notAfter = new Date();
|
|
76
211
|
cert.validity.notAfter.setFullYear(cert.validity.notBefore.getFullYear() + 5);
|
|
@@ -83,13 +218,24 @@ async function generateRootCa() {
|
|
|
83
218
|
}];
|
|
84
219
|
cert.setSubject(attrs);
|
|
85
220
|
cert.setIssuer(attrs);
|
|
221
|
+
|
|
222
|
+
// Trust stores (macOS keychain, NSS, Windows) expect a CA to assert keyCertSign (and
|
|
223
|
+
// usually cRLSign) on the issuer; leaf certs need SAN + keyUsage for modern TLS clients.
|
|
86
224
|
cert.setExtensions([{
|
|
87
225
|
name: 'basicConstraints',
|
|
88
|
-
cA: true
|
|
226
|
+
cA: true,
|
|
227
|
+
critical: true,
|
|
228
|
+
pathLenConstraint: 0
|
|
229
|
+
}, {
|
|
230
|
+
name: 'keyUsage',
|
|
231
|
+
keyCertSign: true,
|
|
232
|
+
cRLSign: true,
|
|
233
|
+
critical: true
|
|
89
234
|
}, {
|
|
90
235
|
name: 'subjectKeyIdentifier'
|
|
91
236
|
}, {
|
|
92
|
-
name: 'authorityKeyIdentifier'
|
|
237
|
+
name: 'authorityKeyIdentifier',
|
|
238
|
+
keyIdentifier: true
|
|
93
239
|
}]);
|
|
94
240
|
try {
|
|
95
241
|
// self-sign certificate
|
|
@@ -125,45 +271,25 @@ async function createCert(opts) {
|
|
|
125
271
|
return;
|
|
126
272
|
}
|
|
127
273
|
await generateRootCa();
|
|
128
|
-
const caCertString = _fs.default.readFileSync(`${tlsRootCaDir}/ca.crt`, 'utf8');
|
|
129
|
-
const caKeyString = _fs.default.readFileSync(`${tlsRootCaDir}/ca.key`, 'utf8');
|
|
130
274
|
debug(`Generating cert for ${certName} in ${certsPath}`);
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
}, {
|
|
145
|
-
name: 'organizationName',
|
|
146
|
-
value: 'navy-dev'
|
|
147
|
-
}];
|
|
275
|
+
let commonName = certName;
|
|
276
|
+
if (opts.serviceUrl && !opts.hostName) {
|
|
277
|
+
try {
|
|
278
|
+
const hostname = new _url.URL(opts.serviceUrl).hostname;
|
|
279
|
+
if (hostname) {
|
|
280
|
+
commonName = hostname;
|
|
281
|
+
}
|
|
282
|
+
} catch (e) {
|
|
283
|
+
// keep commonName as certName
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
const san = subjectAltNamesForHost(commonName);
|
|
287
|
+
let pem;
|
|
148
288
|
try {
|
|
149
|
-
|
|
150
|
-
cert.setIssuer(caCert.subject.attributes);
|
|
151
|
-
cert.setExtensions([{
|
|
152
|
-
name: 'extKeyUsage',
|
|
153
|
-
serverAuth: true
|
|
154
|
-
}]);
|
|
155
|
-
cert.sign(privateCAKey, _nodeForge.md.sha256.create());
|
|
156
|
-
|
|
157
|
-
// PEM-format keys and cert
|
|
158
|
-
const pem = {
|
|
159
|
-
privateKey: _nodeForge.pki.privateKeyToPem(keys.privateKey),
|
|
160
|
-
certificate: _nodeForge.pki.certificateToPem(cert)
|
|
161
|
-
// publicKey: pki.publicKeyToPem(keys.publicKey),
|
|
162
|
-
};
|
|
163
|
-
_fs.default.writeFileSync(`${certsPath}/${certName}.key`, pem.privateKey);
|
|
164
|
-
_fs.default.writeFileSync(`${certsPath}/${certName}.crt`, pem.certificate);
|
|
165
|
-
// fs.writeFileSync(`${certsPath}/${certName}.pub.key`, pem.publicKey)
|
|
289
|
+
pem = buildSignedLeafPems(commonName, tlsRootCaDir, san);
|
|
166
290
|
} catch (e) {
|
|
167
|
-
throw new _errors.NavyError(e);
|
|
291
|
+
throw new _errors.NavyError(e instanceof Error ? e.message : String(e));
|
|
168
292
|
}
|
|
293
|
+
_fs.default.writeFileSync(`${certsPath}/${certName}.key`, pem.privateKey);
|
|
294
|
+
_fs.default.writeFileSync(`${certsPath}/${certName}.crt`, pem.certificate);
|
|
169
295
|
}
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
var _interopRequireDefault = require("@babel/runtime/helpers/interopRequireDefault");
|
|
4
|
+
Object.defineProperty(exports, "__esModule", {
|
|
5
|
+
value: true
|
|
6
|
+
});
|
|
7
|
+
exports.caughtErrorMessage = caughtErrorMessage;
|
|
8
|
+
exports.installRootCaToTrustStore = installRootCaToTrustStore;
|
|
9
|
+
var _child_process = require("child_process");
|
|
10
|
+
var _fs = _interopRequireDefault(require("fs"));
|
|
11
|
+
var _os = _interopRequireDefault(require("os"));
|
|
12
|
+
var _path = _interopRequireDefault(require("path"));
|
|
13
|
+
var _errors = require("../errors");
|
|
14
|
+
function caughtErrorMessage(e) {
|
|
15
|
+
if (e == null) {
|
|
16
|
+
return String(e);
|
|
17
|
+
}
|
|
18
|
+
if (e.message != null && e.message !== '') {
|
|
19
|
+
return String(e.message);
|
|
20
|
+
}
|
|
21
|
+
return String(e);
|
|
22
|
+
}
|
|
23
|
+
function assertCaFile(caCrtPath) {
|
|
24
|
+
if (!_fs.default.existsSync(caCrtPath)) {
|
|
25
|
+
throw new _errors.NavyError(`CA certificate not found at ${caCrtPath}`);
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
function installMacOSUserTrust(caCrtPath) {
|
|
29
|
+
const home = _os.default.homedir();
|
|
30
|
+
const candidates = [_path.default.join(home, 'Library/Keychains/login.keychain-db'), _path.default.join(home, 'Library/Keychains/login.keychain')];
|
|
31
|
+
const keychain = candidates.find(p => _fs.default.existsSync(p));
|
|
32
|
+
if (!keychain) {
|
|
33
|
+
throw new _errors.NavyError('Could not find your macOS login keychain. Import the CA manually from Keychain Access:\n' + ` File → Import Items… → choose ${caCrtPath}\n` + 'Then double‑click the certificate and set “When using this certificate” to “Always Trust”.');
|
|
34
|
+
}
|
|
35
|
+
try {
|
|
36
|
+
(0, _child_process.execFileSync)('security', ['add-trusted-cert', '-d', '-r', 'trustRoot', '-k', keychain, caCrtPath], {
|
|
37
|
+
stdio: 'inherit'
|
|
38
|
+
});
|
|
39
|
+
} catch (e) {
|
|
40
|
+
const msg = caughtErrorMessage(e);
|
|
41
|
+
const hint = 'If this failed because the certificate is already installed, remove the old “navy-dev-ca” ' + 'entry from Keychain Access and run `navy https --setup` again.\n\n' + 'For a system‑wide trust store (requires an administrator password), run:\n' + ` sudo security add-trusted-cert -d -r trustRoot -k /Library/Keychains/System.keychain ${caCrtPath}`;
|
|
42
|
+
throw new _errors.NavyError(`Could not add the Navy root CA to your login keychain.\n${hint}\n\n(${msg})`);
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
function installLinux(caCrtPath) {
|
|
46
|
+
const debianScript = '/usr/sbin/update-ca-certificates';
|
|
47
|
+
const fedoraScript = '/usr/bin/update-ca-trust';
|
|
48
|
+
if (_fs.default.existsSync(debianScript)) {
|
|
49
|
+
const dest = '/usr/local/share/ca-certificates/navy-dev-ca.crt';
|
|
50
|
+
try {
|
|
51
|
+
(0, _child_process.execFileSync)('sudo', ['cp', caCrtPath, dest], {
|
|
52
|
+
stdio: 'inherit'
|
|
53
|
+
});
|
|
54
|
+
(0, _child_process.execFileSync)('sudo', [debianScript], {
|
|
55
|
+
stdio: 'inherit'
|
|
56
|
+
});
|
|
57
|
+
} catch (e) {
|
|
58
|
+
const msg = caughtErrorMessage(e);
|
|
59
|
+
throw new _errors.NavyError('Could not install the Navy root CA using update-ca-certificates.\n' + `Copy ${caCrtPath} to ${dest} (with sudo) and run \`sudo update-ca-certificates\`, or consult your distro’s docs.\n\n(${msg})`);
|
|
60
|
+
}
|
|
61
|
+
return;
|
|
62
|
+
}
|
|
63
|
+
if (_fs.default.existsSync(fedoraScript)) {
|
|
64
|
+
const dest = '/etc/pki/ca-trust/source/anchors/navy-dev-ca.crt';
|
|
65
|
+
try {
|
|
66
|
+
(0, _child_process.execFileSync)('sudo', ['cp', caCrtPath, dest], {
|
|
67
|
+
stdio: 'inherit'
|
|
68
|
+
});
|
|
69
|
+
(0, _child_process.execFileSync)('sudo', [fedoraScript, 'extract'], {
|
|
70
|
+
stdio: 'inherit'
|
|
71
|
+
});
|
|
72
|
+
} catch (e) {
|
|
73
|
+
const msg = caughtErrorMessage(e);
|
|
74
|
+
throw new _errors.NavyError('Could not install the Navy root CA using update-ca-trust.\n' + `Copy ${caCrtPath} to ${dest} (with sudo) and run \`sudo update-ca-trust extract\`.\n\n(${msg})`);
|
|
75
|
+
}
|
|
76
|
+
return;
|
|
77
|
+
}
|
|
78
|
+
throw new _errors.NavyError('Automatic Linux install needs `update-ca-certificates` (Debian/Ubuntu) or `update-ca-trust` (Fedora/RHEL).\n' + 'Install the CA yourself, for example:\n' + ` sudo cp ${caCrtPath} /usr/local/share/ca-certificates/navy-dev-ca.crt && sudo update-ca-certificates`);
|
|
79
|
+
}
|
|
80
|
+
function installWindows(caCrtPath) {
|
|
81
|
+
const systemRoot = process.env.SystemRoot || 'C:\\Windows';
|
|
82
|
+
const certutil = _path.default.join(systemRoot, 'System32', 'certutil.exe');
|
|
83
|
+
if (!_fs.default.existsSync(certutil)) {
|
|
84
|
+
throw new _errors.NavyError(`certutil.exe not found under ${systemRoot}. Import ${caCrtPath} manually:\n` + ' certmgr.msc → Trusted Root Certification Authorities → Certificates → right‑click → All Tasks → Import');
|
|
85
|
+
}
|
|
86
|
+
try {
|
|
87
|
+
(0, _child_process.execFileSync)(certutil, ['-user', '-addstore', 'Root', caCrtPath], {
|
|
88
|
+
stdio: 'inherit'
|
|
89
|
+
});
|
|
90
|
+
} catch (e) {
|
|
91
|
+
const msg = caughtErrorMessage(e);
|
|
92
|
+
throw new _errors.NavyError('Could not add the Navy root CA to the current user’s Trusted Root store.\n' + `Open certmgr.msc and import ${caCrtPath} under Trusted Root Certification Authorities.\n\n(${msg})`);
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Installs the Navy development root CA into the OS trust store where supported
|
|
98
|
+
* (macOS login keychain, Debian/Ubuntu update-ca-certificates, Fedora/RHEL update-ca-trust,
|
|
99
|
+
* Windows per-user Trusted Roots). May invoke sudo on Linux.
|
|
100
|
+
*/
|
|
101
|
+
function installRootCaToTrustStore(caCrtPath) {
|
|
102
|
+
const abs = _path.default.resolve(caCrtPath);
|
|
103
|
+
assertCaFile(abs);
|
|
104
|
+
const platform = process.platform;
|
|
105
|
+
if (platform === 'darwin') {
|
|
106
|
+
installMacOSUserTrust(abs);
|
|
107
|
+
} else if (platform === 'linux') {
|
|
108
|
+
installLinux(abs);
|
|
109
|
+
} else if (platform === 'win32') {
|
|
110
|
+
installWindows(abs);
|
|
111
|
+
} else {
|
|
112
|
+
throw new _errors.NavyError(`Automatic root CA install is not supported on ${platform}. Trust ${abs} manually as a root CA.`);
|
|
113
|
+
}
|
|
114
|
+
}
|