Gourav Kumar
Gourav Kumar

Reputation: 147

Forcing Node.js to Use System OpenSSL with LD_PRELOAD

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.

  1. LD_PRELOAD with Multiple Libraries: I tried using both OpenSSL shared libraries and my custom override library in LD_PRELOAD.
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
  1. Using lsof to Verify Loaded Libraries: I confirmed that both my override library and the system’s OpenSSL libraries are loaded into the Node.js process.
➜  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
  1. Result: Despite confirming that the libraries are loaded, the printf statement in the overridden SSL_CTX_load_verify_locations function is not being called. This suggests that Node.js is not invoking the function at runtime, possibly because it relies on its embedded OpenSSL implementation rather than the dynamically linked version.

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?

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

Answers (1)

Pavlo Sobchuk
Pavlo Sobchuk

Reputation: 1564

Okay, let's break down your question into two:

  1. Using system-installed OpenSSL
  2. Overriding the certificate

Using system-installed OpenSSL

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:

Overriding the certificate

1 The simplest way would be to leverage NODE_EXTRA_CA_CERTSenvironment 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

Related Questions