Published Nov 13, 2023

C++ Addons in Node.js: Critical Factors for Seamless Integration

Celigo

Celigo

Node.js, with its powerful JavaScript runtime, provides developers with a wide range of capabilities for building scalable and efficient applications. However, sometimes, a need arises to interact with low-level system libraries or leverage existing C/C++ codebases for specialized functionalities. This is where Node.js C++ Addons come into play, enabling seamless integration of native C++ code with Node.js applications.

Here, we discuss Node.js C++ addons and focus on a specific use case where we attempted to incorporate a C++ addon to interact with the OpenSSL cryptographic library. OpenSSL is widely known for providing robust security protocols and cryptographic functions essential for online secure communication.

By leveraging C++ addons, we sought to harness the power of OpenSSL within our Node.js application, expecting performance gains and enhanced security.

We encountered unexpected challenges and roadblocks that required immediate attention on our journey. This article aims to illuminate the issues we faced, the debugging and troubleshooting process, and the solutions we implemented to overcome these obstacles. We intend to share our experiences so fellow developers experiencing similar challenges can derive value from our experiences.

We’ll explore the interplay between Node.js, C++ addons, and OpenSSL and unravel the complexities of integrating C++ addons in Node.js applications to interact with the OpenSSL layer.

Problem Statement

Integrator.io is a cloud platform by Celigo that facilitates integrations between various applications. To support integrations with applications using the AS2 protocol, we rely on PKCS7-based cryptographic operations.

Node.js, with its default crypto module, lacks support for PKCS7-based cryptographic operations, including encryption, decryption, verification, and signing. To address this limitation, we turned to a native C++ addon constructed using the OpenSSL library. However, this approach introduced a series of perplexing errors, some illustrated below. 

JavaScript

Emitted ‘error’ event on TLSSocket instance at:

at TLSSocket._emitTLSError (_tls_wrap.js:893:10)

at TLSWrap.onerror (_tls_wrap.js:416:11) {

library: ‘PEM routines’,

function: ‘PEM_read_bio_ex’,

reason: ‘bad base64 decode’,

code: ‘ERR_SSL_BAD_BASE64_DECODE

The errors are being seen intermittently, like 1 out of 1000 requests being served by the API server, which is built in Node.js, and resulting in the Node.js process crash. Please note that the built API server doesn’t solely serve the PKCS7 requests alone (encrypt, decrypt, verify, etc.) but also the other requests interacting with the tls and crypto modules in Node.js.

Root Cause

Further research uncovered a crucial insight: Node.js utilizes libuv in the background to attain asynchronous behavior for system calls, including all operations within the “crypto” module. Although inherently blocking, these operations are intelligently managed by Node.js to execute asynchronously with the help of libuv. Below is the diagram which depicts this behavior:

As we are also invoking the OpenSSL functions (PKCS7_verify, PKCS7_encrypt, etc.,) in the main thread, this results in the race conditions in the OpenSSL layer, causing random errors. In other words, there could be a scenario where Node.js main thread and libuv thread access the OpenSSL code simultaneously.

Below is the diagram to make this behavior more clear:

Below is the statement we have seen in the OpenSSL official documentation confirming the same behavior.

(Note that OpenSSL uses several global data structures that will be implicitly shared whenever multiple threads use OpenSSL.) Multi-threaded applications will crash at random if not handled properly.

Solution

Once we have identified the root cause, we execute the C++ addon-related operations in an isolated process one request at a time to prevent the race conditions.

This is a key learning method. Whenever we use a C++ addon that interacts with underlying OS modules, it is better to run them in an isolated process so that they don’t cause any unexpected behavior.

At a high level, openssl library functions like PKCS7_verify, PKCS7_encrypt, etc., are being called from the native C++ addon that was built.

Below is the sample code line in our addon, which invokes the PKCS7_verify method.

C/C++

ret = !PKCS7_verify(p7, certStack, st, cont, out, flags);

// code line which depicts how the openssl module is being invoked from the addon.

Key Learnings

Below are the key learnings in this journey which might be helpful to the other developers:

  • Always depend on the Node.js built-in functionality to interact with low-level system libraries like OpenSSL.
  • Avoid using C++ addons/node modules that have C++ addons built into them.
  • If we have to use C++ addons, running the corresponding functionality in an isolated process is better by executing one request at a time.