Skip to content

Commit 7a18523

Browse files
chlundeSebastian McKenzie
authored and
Sebastian McKenzie
committed
Custom CA trust store with config option 'cafile' (#736)
Add support for local registries with TLS/SSL certificates issued by private CAs certificates or self-signed certificates. References #606, #631
1 parent a3eaa55 commit 7a18523

File tree

7 files changed

+171
-3
lines changed

7 files changed

+171
-3
lines changed

Diff for: __tests__/__mocks__/request.js

+4-2
Original file line numberDiff line numberDiff line change
@@ -49,11 +49,13 @@ const httpMock = {
4949
request(options: Object, callback?: ?Function): ClientRequest {
5050
const alias = getRequestAlias(options);
5151
const loc = path.join(CACHE_DIR, `${alias}.bin`);
52+
// allow the client to bypass the local fs fixture cache by adding nocache to the query string
53+
const allowCache = options.uri.href.indexOf('nocache') == -1;
5254

5355
// TODO better way to do this
54-
const httpModule = options.port === 443 ? https : http;
56+
const httpModule = options.uri.href.startsWith('https:') ? https : http;
5557

56-
if (fs.existsSync(loc)) {
58+
if (allowCache && fs.existsSync(loc)) {
5759
// cached
5860
options.agent = null;
5961
options.socketPath = null;

Diff for: __tests__/fixtures/certificates/cacerts.pem

+57
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
the first CA is a random CA not used in this test, to verify
2+
that multiple CAs work
3+
-----BEGIN CERTIFICATE-----
4+
MIIFjTCCA3WgAwIBAgIRANOxciY0IzLc9AUoUSrsnGowDQYJKoZIhvcNAQELBQAw
5+
TzELMAkGA1UEBhMCVVMxKTAnBgNVBAoTIEludGVybmV0IFNlY3VyaXR5IFJlc2Vh
6+
cmNoIEdyb3VwMRUwEwYDVQQDEwxJU1JHIFJvb3QgWDEwHhcNMTYxMDA2MTU0MzU1
7+
WhcNMjExMDA2MTU0MzU1WjBKMQswCQYDVQQGEwJVUzEWMBQGA1UEChMNTGV0J3Mg
8+
RW5jcnlwdDEjMCEGA1UEAxMaTGV0J3MgRW5jcnlwdCBBdXRob3JpdHkgWDMwggEi
9+
MA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCc0wzwWuUuR7dyXTeDs2hjMOrX
10+
NSYZJeG9vjXxcJIvt7hLQQWrqZ41CFjssSrEaIcLo+N15Obzp2JxunmBYB/XkZqf
11+
89B4Z3HIaQ6Vkc/+5pnpYDxIzH7KTXcSJJ1HG1rrueweNwAcnKx7pwXqzkrrvUHl
12+
Npi5y/1tPJZo3yMqQpAMhnRnyH+lmrhSYRQTP2XpgofL2/oOVvaGifOFP5eGr7Dc
13+
Gu9rDZUWfcQroGWymQQ2dYBrrErzG5BJeC+ilk8qICUpBMZ0wNAxzY8xOJUWuqgz
14+
uEPxsR/DMH+ieTETPS02+OP88jNquTkxxa/EjQ0dZBYzqvqEKbbUC8DYfcOTAgMB
15+
AAGjggFnMIIBYzAOBgNVHQ8BAf8EBAMCAYYwEgYDVR0TAQH/BAgwBgEB/wIBADBU
16+
BgNVHSAETTBLMAgGBmeBDAECATA/BgsrBgEEAYLfEwEBATAwMC4GCCsGAQUFBwIB
17+
FiJodHRwOi8vY3BzLnJvb3QteDEubGV0c2VuY3J5cHQub3JnMB0GA1UdDgQWBBSo
18+
SmpjBH3duubRObemRWXv86jsoTAzBgNVHR8ELDAqMCigJqAkhiJodHRwOi8vY3Js
19+
LnJvb3QteDEubGV0c2VuY3J5cHQub3JnMHIGCCsGAQUFBwEBBGYwZDAwBggrBgEF
20+
BQcwAYYkaHR0cDovL29jc3Aucm9vdC14MS5sZXRzZW5jcnlwdC5vcmcvMDAGCCsG
21+
AQUFBzAChiRodHRwOi8vY2VydC5yb290LXgxLmxldHNlbmNyeXB0Lm9yZy8wHwYD
22+
VR0jBBgwFoAUebRZ5nu25eQBc4AIiMgaWPbpm24wDQYJKoZIhvcNAQELBQADggIB
23+
ABnPdSA0LTqmRf/Q1eaM2jLonG4bQdEnqOJQ8nCqxOeTRrToEKtwT++36gTSlBGx
24+
A/5dut82jJQ2jxN8RI8L9QFXrWi4xXnA2EqA10yjHiR6H9cj6MFiOnb5In1eWsRM
25+
UM2v3e9tNsCAgBukPHAg1lQh07rvFKm/Bz9BCjaxorALINUfZ9DD64j2igLIxle2
26+
DPxW8dI/F2loHMjXZjqG8RkqZUdoxtID5+90FgsGIfkMpqgRS05f4zPbCEHqCXl1
27+
eO5HyELTgcVlLXXQDgAWnRzut1hFJeczY1tjQQno6f6s+nMydLN26WuU4s3UYvOu
28+
OsUxRlJu7TSRHqDC3lSE5XggVkzdaPkuKGQbGpny+01/47hfXXNB7HntWNZ6N2Vw
29+
p7G6OfY+YQrZwIaQmhrIqJZuigsrbe3W+gdn5ykE9+Ky0VgVUsfxo52mwFYs1JKY
30+
2PGDuWx8M6DlS6qQkvHaRUo0FMd8TsSlbF0/v965qGFKhSDeQoMpYnwcmQilRh/0
31+
ayLThlHLN81gSkJjVrPI0Y8xCVPB4twb1PFUd2fPM3sA1tJ83sZ5v8vgFv2yofKR
32+
PB0t6JzUA81mSqM3kxl5e+IZwhYAyO0OTg3/fs8HqGTNKd9BqoUwSRBzp06JMg5b
33+
rUCGwbCUDI0mxadJ3Bz4WxR6fyNpBK2yAinWEsikxqEt
34+
-----END CERTIFICATE-----
35+
comments should be stripped
36+
-----BEGIN CERTIFICATE-----
37+
MIIDfzCCAmegAwIBAgIJAM4nWEf/MeHVMA0GCSqGSIb3DQEBCwUAMFYxCzAJBgNV
38+
BAYTAlhYMRUwEwYDVQQHDAxEZWZhdWx0IENpdHkxHDAaBgNVBAoME0RlZmF1bHQg
39+
Q29tcGFueSBMdGQxEjAQBgNVBAMMCWxvY2FsaG9zdDAeFw0xNjEwMTIwNTQ3NDla
40+
Fw0yNjEwMTAwNTQ3NDlaMFYxCzAJBgNVBAYTAlhYMRUwEwYDVQQHDAxEZWZhdWx0
41+
IENpdHkxHDAaBgNVBAoME0RlZmF1bHQgQ29tcGFueSBMdGQxEjAQBgNVBAMMCWxv
42+
Y2FsaG9zdDCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBALVCI5Ma6AR0
43+
oDv/OqactDMR4pA6CZJnNDYbjRIkjsXi3pXuAXbQ8J/lD7EKNhu8wxrM0PZvdE1s
44+
ERjgFmZlYYTJkoDQUr9HagwAhAHrUuAq6FsmogLOl1L4QmeddCxExLdTePJAvVxc
45+
+fr3mk7I9Kt5FV1kDOZMyGqkLwKvcGXjx4ue+ZMgQZjWoU4Om7ktA57siAUkrxQg
46+
SonEnlSAlEQy6/wRSR2XQ85e+o1JHMIti4+h/Soo4BmHetec7zKvHjsL1kuT9s43
47+
YiVlj660cc/Rmqt61OqPrumeKeVLRLmOD71l0yjU8QEwvAvvzFwdTZQSPUVGb4Je
48+
9Apxn7MNnKMCAwEAAaNQME4wHQYDVR0OBBYEFFzPIkvacjl8MFC6eRCwSTyW5z9c
49+
MB8GA1UdIwQYMBaAFFzPIkvacjl8MFC6eRCwSTyW5z9cMAwGA1UdEwQFMAMBAf8w
50+
DQYJKoZIhvcNAQELBQADggEBAKN9RXWgMuEwhLYzG///duWTcIC7UfamGDVlezxa
51+
BkR+VGkRSlo4v+MaZKscG2D/NGh4PP5PQZr7okLQY6MIKmcFkIN1BDziEVfKICFs
52+
AIrcajDLyHaLcAmZv2VJe1sz12pmGZG7uTOncMAngZoNDNI0f4djzvmRd9nn4yGo
53+
o8vhLMzgdXhp3T7yaCjpbpZUd1bnggJXz9MO76EBcCkS3+HcRRr/0KMD/tk5tZUx
54+
s35hnRrJi9HvhFjZbJA/KGG8DJp4oyzEfbufmUJ7OdbMn++W3NtG1BrRhbKmH5og
55+
tpfAr88iJ6BFaXV/4JIxc4Fga8dvjJR3ueh5pT2om/rUzkQ=
56+
-----END CERTIFICATE-----
57+
another comment

Diff for: __tests__/fixtures/certificates/server-cert.pem

+21
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
-----BEGIN CERTIFICATE-----
2+
MIIDfzCCAmegAwIBAgIJAM4nWEf/MeHVMA0GCSqGSIb3DQEBCwUAMFYxCzAJBgNV
3+
BAYTAlhYMRUwEwYDVQQHDAxEZWZhdWx0IENpdHkxHDAaBgNVBAoME0RlZmF1bHQg
4+
Q29tcGFueSBMdGQxEjAQBgNVBAMMCWxvY2FsaG9zdDAeFw0xNjEwMTIwNTQ3NDla
5+
Fw0yNjEwMTAwNTQ3NDlaMFYxCzAJBgNVBAYTAlhYMRUwEwYDVQQHDAxEZWZhdWx0
6+
IENpdHkxHDAaBgNVBAoME0RlZmF1bHQgQ29tcGFueSBMdGQxEjAQBgNVBAMMCWxv
7+
Y2FsaG9zdDCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBALVCI5Ma6AR0
8+
oDv/OqactDMR4pA6CZJnNDYbjRIkjsXi3pXuAXbQ8J/lD7EKNhu8wxrM0PZvdE1s
9+
ERjgFmZlYYTJkoDQUr9HagwAhAHrUuAq6FsmogLOl1L4QmeddCxExLdTePJAvVxc
10+
+fr3mk7I9Kt5FV1kDOZMyGqkLwKvcGXjx4ue+ZMgQZjWoU4Om7ktA57siAUkrxQg
11+
SonEnlSAlEQy6/wRSR2XQ85e+o1JHMIti4+h/Soo4BmHetec7zKvHjsL1kuT9s43
12+
YiVlj660cc/Rmqt61OqPrumeKeVLRLmOD71l0yjU8QEwvAvvzFwdTZQSPUVGb4Je
13+
9Apxn7MNnKMCAwEAAaNQME4wHQYDVR0OBBYEFFzPIkvacjl8MFC6eRCwSTyW5z9c
14+
MB8GA1UdIwQYMBaAFFzPIkvacjl8MFC6eRCwSTyW5z9cMAwGA1UdEwQFMAMBAf8w
15+
DQYJKoZIhvcNAQELBQADggEBAKN9RXWgMuEwhLYzG///duWTcIC7UfamGDVlezxa
16+
BkR+VGkRSlo4v+MaZKscG2D/NGh4PP5PQZr7okLQY6MIKmcFkIN1BDziEVfKICFs
17+
AIrcajDLyHaLcAmZv2VJe1sz12pmGZG7uTOncMAngZoNDNI0f4djzvmRd9nn4yGo
18+
o8vhLMzgdXhp3T7yaCjpbpZUd1bnggJXz9MO76EBcCkS3+HcRRr/0KMD/tk5tZUx
19+
s35hnRrJi9HvhFjZbJA/KGG8DJp4oyzEfbufmUJ7OdbMn++W3NtG1BrRhbKmH5og
20+
tpfAr88iJ6BFaXV/4JIxc4Fga8dvjJR3ueh5pT2om/rUzkQ=
21+
-----END CERTIFICATE-----

Diff for: __tests__/fixtures/certificates/server-key.pem

+28
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
-----BEGIN PRIVATE KEY-----
2+
MIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQC1QiOTGugEdKA7
3+
/zqmnLQzEeKQOgmSZzQ2G40SJI7F4t6V7gF20PCf5Q+xCjYbvMMazND2b3RNbBEY
4+
4BZmZWGEyZKA0FK/R2oMAIQB61LgKuhbJqICzpdS+EJnnXQsRMS3U3jyQL1cXPn6
5+
95pOyPSreRVdZAzmTMhqpC8Cr3Bl48eLnvmTIEGY1qFODpu5LQOe7IgFJK8UIEqJ
6+
xJ5UgJREMuv8EUkdl0POXvqNSRzCLYuPof0qKOAZh3rXnO8yrx47C9ZLk/bON2Il
7+
ZY+utHHP0ZqretTqj67pninlS0S5jg+9ZdMo1PEBMLwL78xcHU2UEj1FRm+CXvQK
8+
cZ+zDZyjAgMBAAECggEBAKRSKlgRG2f2ptDdaDlldMOboi6oPsc3wpCO14wsEjb5
9+
nlqDo1YowwvhqCESpcztil7AcWwHzILnxnQrqoL3w7mS17rpoSqBPnVU/leTE9Xf
10+
cDg6RMOQsITqRaETkB8V1NRx2wKbiE+0hndrgruL2KufIKxCqKMb1tE+uNORYq8q
11+
kFIBvUnhjLFzepZ749wYTx2Tkdrfe3Y3sW2a17LRr43s0Z41skX/9iz7GdIW8OLD
12+
Edfw7COvlxZQjtmLqwmoFd5YjOo8u6C6kr74H8OLplGN35cPc+L57kFqKvxV42Lq
13+
y+EirIZ3b8uItsuOPIf92snpfK9CmdwTCx5lrMmpqjkCgYEA8QKr8xmI3fhUxPD8
14+
9PYQlIa+pKMtX1sKgS3sA2ZD9bwRC9HmkXhYTYsrakbX2VaGe1dmc/7KMaNaVkrT
15+
+BfMk9SACnoul7dXzV6amzM7AdrZYkJgejbmSJtYGz3YnHQTz4ipaffHfsKoWdvs
16+
LdvPgBBdfbYsvGxB5ya9kvcsLyUCgYEAwIgawo1mU5KC8BXOvzKSKJ6vTULtnF6d
17+
zf1OGv6atrS36Sg3ycqPL+kUy44bl1ckjh+VMp6rty/5Z/K1PKkyNniznxj9Epwu
18+
4O/aI/Q7SrKZmjH64gXWBmyj8doNHM3nFF9mSIjRromovuUeOcpFlATBCe0Pxpxt
19+
sGlhjnrMVicCgYBvIwlBv9uiaBpG+s3a9AEvTHdrGigZGbVdXlzAMI9UKNY/ehp1
20+
qGYn0+5AQszUVxcKl4ISKUL54tcMhdL7S5Y18T7eFfuYUJ53gJGQ0e366/1kVzGA
21+
CgLlJmVZoopZkxlzkRR2XiErbf4N+eEOQJeN+X3zM2ert8woGHBA7iP81QKBgBi0
22+
3oo8zvbGhFr+0WsjuDHSOzi07/zy/1khulYoef4cLsWSzaXtgnZpeKuubsf6/Mvo
23+
LaMzTWHSnDTEppFEPRdUYeh2snMi67kdzmZyvvEU/jUVWNaMXSyx4E/25Vve6Fpq
24+
65s/Q3kcXTUx/bD4zfjyqzr02uNny4Op4kUAaRxdAoGBAK0t4FVXTOWIf6xxgGMZ
25+
BBkNu6N1n/Io3pliCpYpZrBE/6NEOaEeniyaM0BbAatR+SemaYICnEj31ueonlIs
26+
PLUi9H177Cnu2DZwBMaWx78r260RIxF+bcdTBjQ51fXt7/44okFjHmCcRVA1q/Rc
27+
WdI83+niMywS/XVfLAMhnFWR
28+
-----END PRIVATE KEY-----

Diff for: __tests__/util/request-manager.js

+36
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
/* @flow */
2+
/* eslint max-len: 0 */
3+
4+
import {NoopReporter} from '../../src/reporters/index.js';
5+
import Config from '../../src/config.js';
6+
import type {ConfigOptions} from '../../src/config.js';
7+
import * as fs from '../../src/util/fs.js';
8+
9+
jasmine.DEFAULT_TIMEOUT_INTERVAL = 60000;
10+
11+
const https = require('https');
12+
const path = require('path');
13+
14+
async function createConfig(opts: ConfigOptions = {}): Promise<Config> {
15+
const config = new Config(new NoopReporter());
16+
await config.init(opts);
17+
return config;
18+
}
19+
20+
test('RequestManager.request with cafile', async () => {
21+
let body;
22+
const options = {
23+
key: await fs.readFile(path.join(__dirname, '..', 'fixtures', 'certificates', 'server-key.pem')),
24+
cert: await fs.readFile(path.join(__dirname, '..', 'fixtures', 'certificates', 'server-cert.pem')),
25+
};
26+
const server = https.createServer(options, (req, res) => { res.end('ok'); });
27+
try {
28+
server.listen(0);
29+
const config = await createConfig({'cafile': path.join(__dirname, '..', 'fixtures', 'certificates', 'cacerts.pem')});
30+
const port = server.address().port;
31+
body = await config.requestManager.request({url: `https://localhost:${port}/?nocache`, headers: {Connection: 'close'}});
32+
} finally {
33+
server.close();
34+
}
35+
expect(body).toBe('ok');
36+
});

Diff for: src/config.js

+3-1
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ const invariant = require('invariant');
1717
const path = require('path');
1818
const url = require('url');
1919

20-
type ConfigOptions = {
20+
export type ConfigOptions = {
2121
cwd?: ?string,
2222
cacheFolder?: ?string,
2323
tempFolder?: ?string,
@@ -29,6 +29,7 @@ type ConfigOptions = {
2929
captureHar?: boolean,
3030
ignorePlatform?: boolean,
3131
ignoreEngines?: boolean,
32+
cafile?: ?string,
3233

3334
// Loosely compare semver for invalid cases like "0.01.0"
3435
looseSemver?: ?boolean,
@@ -178,6 +179,7 @@ export default class Config {
178179
httpProxy: String(this.getOption('proxy') || ''),
179180
httpsProxy: String(this.getOption('https-proxy') || ''),
180181
strictSSL: Boolean(this.getOption('strict-ssl')),
182+
cafile: String(opts.cafile || this.getOption('cafile') || ''),
181183
});
182184
}
183185

Diff for: src/util/request-manager.js

+22
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import type RequestT from 'request';
1313
const RequestCaptureHar = require('request-capture-har');
1414
const invariant = require('invariant');
1515
const url = require('url');
16+
const fs = require('fs');
1617

1718
const successHosts = map();
1819
const controlOffline = network.isOffline();
@@ -39,6 +40,7 @@ type RequestParams<T> = {
3940
body?: mixed,
4041
proxy?: string,
4142
encoding?: ?string,
43+
ca?: Array<string>,
4244
forever?: boolean,
4345
strictSSL?: boolean,
4446
headers?: {
@@ -67,6 +69,7 @@ export default class RequestManager {
6769
this.offlineQueue = [];
6870
this.captureHar = false;
6971
this.httpsProxy = null;
72+
this.ca = null;
7073
this.httpProxy = null;
7174
this.strictSSL = true;
7275
this.userAgent = '';
@@ -85,6 +88,7 @@ export default class RequestManager {
8588
httpsProxy: ?string;
8689
httpProxy: ?string;
8790
strictSSL: boolean;
91+
ca: ?Array<string>;
8892
offlineQueue: Array<RequestOptions>;
8993
queue: Array<Object>;
9094
max: number;
@@ -102,6 +106,7 @@ export default class RequestManager {
102106
httpProxy?: string,
103107
httpsProxy?: string,
104108
strictSSL?: boolean,
109+
cafile?: string,
105110
}) {
106111
if (opts.userAgent != null) {
107112
this.userAgent = opts.userAgent;
@@ -126,6 +131,19 @@ export default class RequestManager {
126131
if (opts.strictSSL !== null && typeof opts.strictSSL !== 'undefined') {
127132
this.strictSSL = opts.strictSSL;
128133
}
134+
135+
if (opts.cafile != null && opts.cafile != '') {
136+
// The CA bundle file can contain one or more certificates with comments/text between each PEM block.
137+
// tls.connect wants an array of certificates without any comments/text, so we need to split the string
138+
// and strip out any text in between the certificates
139+
try {
140+
const bundle = fs.readFileSync(opts.cafile).toString();
141+
const hasPemPrefix = (block) => block.startsWith('-----BEGIN ');
142+
this.ca = bundle.split(/(-----BEGIN .*\r?\n[^-]+\r?\n--.*)/).filter(hasPemPrefix);
143+
} catch (err) {
144+
this.reporter.error(`Could not open cafile: ${err.message}`);
145+
}
146+
}
129147
}
130148

131149
/**
@@ -340,6 +358,10 @@ export default class RequestManager {
340358
params.proxy = proxy;
341359
}
342360

361+
if (this.ca != null) {
362+
params.ca = this.ca;
363+
}
364+
343365
const request = this._getRequestModule();
344366
const req = request(params);
345367

0 commit comments

Comments
 (0)