navy 7.0.0-alpha.4 → 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/navy/__tests__/index.js +142 -0
- package/lib/navy/index.js +12 -2
- 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
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
|
+
}
|