Mocking HTTPS Services in Node.js for Fun and Profit
- 2/11/2018
- ·
- #development
- #process
We all know by now that Transport Layer Security (TLS) is a must in production. But in local development, where trust is established and configuration is expensive, secure connections add not-strictly-necessary complexity that might be better off ignored.
In many systems, an unencrypted mode simplifies configuration and debugging in local environments. Others sidestep the problem by always running unencrypted, delegating SSL off to a trusted proxy. Both are fine solutions that trade a bit of configuration-management complexity for development ease-of-use. Neither precludes end-to-end security in the wild.
But what if a local service needs to interact with a TLS-enabled service belonging to someone else that (quite rightly) refuses to allow unencrypted connections? In production, we aim at the live service and TLS just works. But for local development, we may occasionally need to mock the third party’s behavior with predictable state. Since there’s no chance we’ll have access to the third-party keys, our best bet may be to replace the external X.509 certificate chain with a little something of our own.
While the fake certificate chain we’re about to set up can be reused with any
application stack, we’ll use node.js (and its platform-agnostic
NODE_EXTRA_CA_CERTS
flag) to sidestep system-level CA configuration and get a
mock up and running.
First, we’re going to need some credentials.
Setting up keys
Let’s use OpenSSL to create a key pair for our local certificate authority.
$ mkdir certs && cd certs
$ openssl req -nodes -new -x509 -keyout ca.key -out ca.crt \
-subj "/C=US/ST=OH/L=Dayton/O=My Inc/OU=DevOps/CN=/emailAddress=root@localhost"
Next, let’s create a server key and sign it with the CA certificate:
$ openssl req -nodes -newkey 1024 -keyout server.key -out server.csr \
-subj "/C=US/ST=OH/L=Dayton/O=My Inc/OU=DevOps/CN=localhost/emailAddress=root@localhost"
$ openssl x509 -req -CAkey ca.key -CA ca.crt -CAcreateserial \
-days 10000 -in server.csr -out server.crt
Creating the service
All we need to set up a simple demo server is node’s built-in https module, configured to use our newly-created server certificate.
// server.js
const https = require('https');
const fs = require('fs');
const path = require('path');
const { PORT } = process.env;
const options = {
key: fs.readFileSync(path.resolve(__dirname, 'certs', 'server.key')),
cert: fs.readFileSync(path.resolve(__dirname, 'certs', 'server.crt')),
};
const server = https.createServer(options, (req, res) =>
res.end('Hello, world'));
server.listen(PORT, (err) => {
if (!err) {
console.log(`Listening on ${PORT}`);
}
});
We can also create a small client to verify the connection:
// client.js
const https = require('https');
const { PORT } = process.env;
https.get(`https://localhost:${PORT}`, (res) => {
console.log(res.statusCode);
if (res.statusCode !== 200) {
throw new Error(`Expected 200, got ${res.statusCode}`);
}
res.on('data', data => process.stdout.write(data));
});
That’s all the code we need to set up a TLS-enabled server, but there’s a catch. If we try to connect to the server, we’ll see that something isn’t right.
$ PORT=3200 node server.js &
$ PORT=3200 node client.js
Error: unable to verify the first certificate
at TLSSocket.onConnectSecure (_tls_wrap.js:1036:34)
at TLSSocket.emit (events.js:159:13)
at TLSSocket._finishInit (_tls_wrap.js:637:8)
In other words, the TLS library won’t touch a suspicious-looking certificate. In this case (and in this case only–TLS exists for a reason!) we can can get the mock running by convincing node that (all evidence to the contrary) this certificate is valid.
All we need to do is make our local CA certificate available to
the client at runtime using
NODE_EXTRA_CA_CERTS_FILE
.
Just set the variable, run the server again, and—
$ PORT=8082 NODE_EXTRA_CA_CERTS=./certs/ca.crt node client.js
Hello, world