Reputation: 147
I am working on a use case where I need to override the certificate used during TLS connections by injecting a custom certificate through the SSL_CTX_load_verify_locations function from OpenSSL. I have successfully implemented this solution in a Python application using LD_PRELOAD to override the OpenSSL function at runtime.
However, the same approach is not working with Node.js. After some research, I found that Node.js embeds OpenSSL internally rather than relying on dynamically linked OpenSSL libraries. As a result, it seems that my LD_PRELOAD trick is ineffective. I am looking for a way to force Node.js to use the system-installed OpenSSL (shared library) instead of the embedded one, without rebuilding Node.js from source.
Below are the details of my setup and what I have tried so far.
C Code to Override SSL_CTX_load_verify_locations
#define _GNU_SOURCE
#include <dlfcn.h>
#include <openssl/ssl.h>
#include <stdio.h>
// Function pointer to hold the original SSL_CTX_load_verify_locations function
int (*original_SSL_CTX_load_verify_locations)(SSL_CTX *ctx, const char *CAfile, const char *CApath) = NULL;
// Custom SSL_CTX_load_verify_locations function
int SSL_CTX_load_verify_locations(SSL_CTX *ctx, const char *CAfile, const char *CApath) {
// Load the original function if it hasn't been loaded yet
if (!original_SSL_CTX_load_verify_locations) {
original_SSL_CTX_load_verify_locations = (int (*)(SSL_CTX *, const char *, const char *))
dlsym(RTLD_NEXT, "SSL_CTX_load_verify_locations");
const char *error = dlerror();
if (error != NULL) {
fprintf(stderr, "Error loading original SSL_CTX_load_verify_locations: %s\n", error);
return -1;
}
}
const char *my_CAfile = "/Users/gouravkumar/Desktop/Keploy/Lima-workspace/gk_workspace/tls-ebpf/sample-apps/keploy.crt";
printf("\n\nOverriding CAfile with %s\n\n", my_CAfile);
// Call the original function with the overridden CAfile
return original_SSL_CTX_load_verify_locations(ctx, my_CAfile, CApath);
}
How I Compile the Code
clang-14 -I/usr/include/openssl -L/usr/lib/aarch64-linux-gnu -fPIC -shared -o ssl_override.so ssl_override.c -lssl -lcrypto
Node.js Code
const express = require('express');
const axios = require('axios');
const fs = require('fs');
const https = require('https');
const app = express();
const CERTIFICATE_PATH = '/Users/gouravkumar/Desktop/Keploy/Lima-workspace/gk_workspace/tls-ebpf/sample-apps/ninja.crt';
const httpsAgent = new https.Agent({
ca: fs.readFileSync(CERTIFICATE_PATH),
keepAlive: false,
});
app.get('/catFact', async (req, res) => {
try {
const response = await axios.get('https://catfact.ninja/fact', { httpsAgent });
res.status(200).json(response.data);
} catch (error) {
res.status(500).json({
error: 'SSL certificate validation failed',
details: error.message
});
}
});
const PORT = 8080;
app.listen(PORT, () => {
console.log(`Server is running on http://0.0.0.0:${PORT}`);
});
How I Run the Node.js Application
LD_PRELOAD=/Users/gouravkumar/Desktop/Keploy/Lima-workspace/gk_workspace/tls-ebpf/tls-change-cert/ssl_override.so node index.js
What I Tried by referring Force node.js to use non-distro copy of OpenSSL.
export LD_LIBRARY_PATH=/usr/lib/aarch64-linux-gnu:$LD_LIBRARY_PATH
export LD_PRELOAD="/usr/lib/aarch64-linux-gnu/libssl.so.3 \
/usr/lib/aarch64-linux-gnu/libcrypto.so.3 \
/Users/gouravkumar/Desktop/Keploy/Lima-workspace/gk_workspace/tls-ebpf/tls-change-cert/ssl_override.so"
node index.js
➜ lsof -p $(pidof node) | grep ssl
node 507762 gouravkumar mem REG 0,40 70040 /path/to/ssl_override.so
node 507762 gouravkumar mem REG 254,1 737192 /usr/lib/aarch64-linux-gnu/libssl.so.3
What I Need Help With
How can I force Node.js to use the system-installed OpenSSL shared libraries (like libssl.so.3 and libcrypto.so.3) instead of the embedded version?
Is there a way to ensure Node.js dynamically loads the system’s OpenSSL without rebuilding it from source?
Are there any known workarounds or configurations (like setting specific environment variables) to achieve this?
Process versions in Node.js:
➜ tls-support git:(main) ✗ node -p "process.versions"
{
node: '18.20.4',
acorn: '8.11.3',
ada: '2.7.8',
ares: '1.28.1',
base64: '0.5.2',
brotli: '1.0.9',
cjs_module_lexer: '1.2.2',
cldr: '44.1',
icu: '74.2',
llhttp: '6.1.1',
modules: '108',
napi: '9',
nghttp2: '1.61.0',
nghttp3: '0.7.0',
ngtcp2: '1.3.0',
openssl: '3.0.13+quic',
simdutf: '5.2.4',
tz: '2024a',
undici: '5.28.4',
unicode: '15.1',
uv: '1.44.2',
uvwasi: '0.0.19',
v8: '10.2.154.26-node.37',
zlib: '1.3.0.1-motley'
}
Upvotes: 1
Views: 143
Reputation: 1564
Okay, let's break down your question into two:
Indeed, OpenSSL is statically linked, Node.js does not use the system-installed OpenSSL libraries at runtime. This design choice means that you cannot easily override or replace the OpenSSL version or its configurations used by Node.js without rebuilding it from the source, at least to my understanding.
Here is how you can build node: https://patrickdesjardins.com/blog/how-to-build-nodejs-with-openssl Thanks, Patrick!
However, if you need only override the certificate, there are ways to do it:
1 The simplest way would be to leverage
NODE_EXTRA_CA_CERTS
environment variable:
`NODE_EXTRA_CA_CERTS=/path/to/custom-ca.crt node your-app.js`
This will just extend the list of trusted certificates you can use
in your app.
2 Monkey-patch the tls method:
const tls = require('tls');
const fs = require('fs');
// Read your custom CA certificate
const customCa = fs.readFileSync('/path/to/custom-ca.crt');
// Store the original method
const originalCreateSecureContext = tls.createSecureContext;
// Override the method
tls.createSecureContext = function (options = {}) {
// Ensure 'ca' is an array and include the custom certificate
options.ca = options.ca || [];
if (!Array.isArray(options.ca)) {
options.ca = [options.ca];
}
options.ca.push(customCa);
// Call the original method with modified options
return originalCreateSecureContext.call(tls, options);
};
3 Inject it into https lib programmatically
const https = require('https');
const fs = require('fs');
// Read your custom CA certificate
const customCa = fs.readFileSync('/path/to/custom-ca.crt');
// Create an HTTPS agent that includes your custom CA
const httpsAgent = new https.Agent({
ca: customCa,
});
// Use the custom agent in your HTTPS request
https.get('https://example.com', { agent: httpsAgent }, (res) => {
// Handle the response
res.on('data', (d) => {
process.stdout.write(d);
});
}).on('error', (e) => {
console.error(e);
});
Upvotes: 0