Skip to content

Commit 5fb415c

Browse files
authored
Merge pull request #139 from BitGo/WP-6407-mtls-fingerprints-fix
fix: require allowed fingerprints list for mtls
2 parents a4462af + 43985a8 commit 5fb415c

File tree

2 files changed

+200
-0
lines changed

2 files changed

+200
-0
lines changed

src/__tests__/appUtils.test.ts

Lines changed: 186 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,186 @@
1+
import 'should';
2+
import { createMtlsMiddleware } from '../shared/appUtils';
3+
import { TlsMode } from '../initConfig';
4+
5+
describe('appUtils', () => {
6+
describe('createMtlsMiddleware', () => {
7+
describe('Empty mTLS fingerprint allowlist validation', () => {
8+
it('should reject connection when mTLS is enabled but fingerprint allowlist is empty', () => {
9+
const middleware = createMtlsMiddleware({
10+
tlsMode: TlsMode.MTLS,
11+
clientCertAllowSelfSigned: false,
12+
mtlsAllowedClientFingerprints: [],
13+
});
14+
15+
const mockReq = {
16+
socket: {
17+
getPeerCertificate: () => ({
18+
subject: { CN: 'test-client' },
19+
issuer: { CN: 'test-ca', O: 'Test Org', OU: 'Test Unit' },
20+
fingerprint256: 'AB:CD:EF:12:34:56:78:90:AB:CD:EF:12:34:56:78:90',
21+
}),
22+
},
23+
} as any;
24+
25+
const mockRes = {
26+
status: function (code: number) {
27+
this.statusCode = code;
28+
return this;
29+
},
30+
json: function (body: any) {
31+
this.body = body;
32+
return this;
33+
},
34+
statusCode: 0,
35+
body: {},
36+
} as any;
37+
38+
let nextCalled = false;
39+
const mockNext = () => {
40+
nextCalled = true;
41+
};
42+
43+
middleware(mockReq, mockRes, mockNext);
44+
45+
nextCalled.should.be.false();
46+
mockRes.statusCode.should.equal(403);
47+
mockRes.body.should.have.property('error', 'mTLS Authentication Failed');
48+
mockRes.body.should.have.property(
49+
'message',
50+
'No client certificate fingerprints configured',
51+
);
52+
mockRes.body.should.have
53+
.property('details')
54+
.match(/MTLS_ALLOWED_CLIENT_FINGERPRINTS must be configured/);
55+
});
56+
57+
it('should reject connection when mTLS is enabled but fingerprint allowlist is undefined', () => {
58+
const middleware = createMtlsMiddleware({
59+
tlsMode: TlsMode.MTLS,
60+
clientCertAllowSelfSigned: false,
61+
mtlsAllowedClientFingerprints: undefined,
62+
});
63+
64+
const mockReq = {
65+
socket: {
66+
getPeerCertificate: () => ({
67+
subject: { CN: 'test-client' },
68+
issuer: { CN: 'test-ca', O: 'Test Org', OU: 'Test Unit' },
69+
fingerprint256: 'AB:CD:EF:12:34:56:78:90:AB:CD:EF:12:34:56:78:90',
70+
}),
71+
},
72+
} as any;
73+
74+
const mockRes = {
75+
status: function (code: number) {
76+
this.statusCode = code;
77+
return this;
78+
},
79+
json: function (body: any) {
80+
this.body = body;
81+
return this;
82+
},
83+
statusCode: 0,
84+
body: {},
85+
} as any;
86+
87+
let nextCalled = false;
88+
const mockNext = () => {
89+
nextCalled = true;
90+
};
91+
92+
middleware(mockReq, mockRes, mockNext);
93+
94+
nextCalled.should.be.false();
95+
mockRes.statusCode.should.equal(403);
96+
mockRes.body.should.have.property('error', 'mTLS Authentication Failed');
97+
mockRes.body.should.have.property(
98+
'message',
99+
'No client certificate fingerprints configured',
100+
);
101+
});
102+
103+
it('should accept connection when mTLS is enabled and certificate is in allowlist', () => {
104+
const middleware = createMtlsMiddleware({
105+
tlsMode: TlsMode.MTLS,
106+
clientCertAllowSelfSigned: false,
107+
mtlsAllowedClientFingerprints: ['ABCDEF1234567890ABCDEF1234567890'],
108+
});
109+
110+
const mockReq = {
111+
socket: {
112+
getPeerCertificate: () => ({
113+
subject: { CN: 'test-client' },
114+
issuer: { CN: 'test-ca', O: 'Test Org', OU: 'Test Unit' },
115+
fingerprint256: 'AB:CD:EF:12:34:56:78:90:AB:CD:EF:12:34:56:78:90',
116+
}),
117+
},
118+
} as any;
119+
120+
const mockRes = {
121+
status: function (code: number) {
122+
this.statusCode = code;
123+
return this;
124+
},
125+
json: function (body: any) {
126+
this.body = body;
127+
return this;
128+
},
129+
statusCode: 0,
130+
body: {},
131+
} as any;
132+
133+
let nextCalled = false;
134+
const mockNext = () => {
135+
nextCalled = true;
136+
};
137+
138+
middleware(mockReq, mockRes, mockNext);
139+
140+
nextCalled.should.be.true();
141+
mockRes.statusCode.should.equal(0); // Status not set means success
142+
});
143+
144+
it('should allow empty allowlist when TLS mode is disabled', () => {
145+
const middleware = createMtlsMiddleware({
146+
tlsMode: TlsMode.DISABLED,
147+
clientCertAllowSelfSigned: false,
148+
mtlsAllowedClientFingerprints: [],
149+
});
150+
151+
const mockReq = {
152+
socket: {
153+
getPeerCertificate: () => ({
154+
subject: { CN: 'test-client' },
155+
issuer: { CN: 'test-ca', O: 'Test Org', OU: 'Test Unit' },
156+
fingerprint256: 'AB:CD:EF:12:34:56:78:90:AB:CD:EF:12:34:56:78:90',
157+
}),
158+
},
159+
} as any;
160+
161+
const mockRes = {
162+
status: function (code: number) {
163+
this.statusCode = code;
164+
return this;
165+
},
166+
json: function (body: any) {
167+
this.body = body;
168+
return this;
169+
},
170+
statusCode: 0,
171+
body: {},
172+
} as any;
173+
174+
let nextCalled = false;
175+
const mockNext = () => {
176+
nextCalled = true;
177+
};
178+
179+
middleware(mockReq, mockRes, mockNext);
180+
181+
nextCalled.should.be.true();
182+
mockRes.statusCode.should.equal(0); // No rejection when TLS is disabled
183+
});
184+
});
185+
});
186+
});

src/shared/appUtils.ts

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -146,6 +146,20 @@ export function createMtlsMiddleware(config: {
146146

147147
// If client cert is provided, validate it
148148
if (hasValidClientCert) {
149+
if (config.tlsMode === TlsMode.MTLS) {
150+
if (
151+
!config.mtlsAllowedClientFingerprints ||
152+
config.mtlsAllowedClientFingerprints.length === 0
153+
) {
154+
return res.status(403).json({
155+
error: 'mTLS Authentication Failed',
156+
message: 'No client certificate fingerprints configured',
157+
details:
158+
'MTLS_ALLOWED_CLIENT_FINGERPRINTS must be configured when mTLS mode is enabled',
159+
});
160+
}
161+
}
162+
149163
// Check if self-signed certificates are allowed
150164
if (!config.clientCertAllowSelfSigned && clientCert.issuer.CN === clientCert.subject.CN) {
151165
return res.status(403).json({

0 commit comments

Comments
 (0)