Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
77 changes: 74 additions & 3 deletions deps/ncrypto/ncrypto.cc
Original file line number Diff line number Diff line change
Expand Up @@ -14,9 +14,12 @@
#endif
#include <algorithm>
#include <array>
#include <cctype>
#include <climits>
#include <cstring>
#include <mutex>
#include <string_view>
#include <unordered_map>
#if OPENSSL_VERSION_MAJOR >= 3
#include <openssl/core_names.h>
#include <openssl/params.h>
Expand Down Expand Up @@ -4482,11 +4485,52 @@ bool SSLCtxPointer::setCipherSuites(const char* ciphers) {
// ============================================================================

const Cipher Cipher::FromName(const char* name) {
return Cipher(EVP_get_cipherbyname(name));
if (const EVP_CIPHER* cipher = EVP_get_cipherbyname(name)) {
return Cipher(cipher);
}
#if NCRYPTO_USE_OPENSSL3_PROVIDER
// Ciphers such as SM4-GCM only exist as fetchable provider algorithms.
// Cipher does not own what it points at, so the fetched reference is kept
// for the lifetime of the process instead of being freed.
MarkPopErrorOnReturn mark_pop_error_on_return;

static std::mutex fetched_mutex;
static auto& fetched_ciphers =
*new std::unordered_map<std::string, const EVP_CIPHER*>();

// A fetch is resolved against the library context's default properties,
// which setFipsEnabled() changes at runtime. Key on that state as well so
// that a cipher fetched before the switch cannot outlive it.
std::string key(EVP_default_properties_is_fips_enabled(nullptr) ? "fips:"

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Reading the FIPS state and performing the fetch are not synchronized with setFipsCrypto(). This code uses fetched_mutex, while FIPS transitions use a separate fips_mutex.

A worker can read the state as enabled, the main thread can disable FIPS, and the worker can then fetch a non-FIPS implementation and store it under the fips: key. If FIPS is enabled again, that cached implementation is reused.

Please retain #63411's owned, per-Cipher fetched handles when rebasing instead of introducing this process-lifetime property cache.

: "");
key.append(name);
std::transform(key.begin(), key.end(), key.begin(), [](unsigned char c) {
return static_cast<char>(std::tolower(c));
});

std::lock_guard<std::mutex> lock(fetched_mutex);
if (auto it = fetched_ciphers.find(key); it != fetched_ciphers.end()) {
return Cipher(it->second);
}
if (const EVP_CIPHER* fetched = EVP_CIPHER_fetch(nullptr, name, nullptr)) {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This cannot be an unrestricted fallback. It makes every fetchable provider cipher a Node-supported cipher before CipherBase has implemented that mode's contract. With this head on OpenSSL 3.0, createCipheriv('aes-128-siv', ...) succeeds, while setAAD() and getAuthTag() both throw ERR_CRYPTO_INVALID_STATE; the cipher is also absent from getCiphers().

Please rebase on #63411 and only return a fetched cipher when its mode or name is explicitly supported by Node. The SM4 additions should extend that eligibility rather than accepting every successful EVP_CIPHER_fetch().

fetched_ciphers[key] = fetched;
return Cipher(fetched);
}
#endif
return Cipher();
}

const Cipher Cipher::FromNid(int nid) {
return Cipher(EVP_get_cipherbynid(nid));
if (const EVP_CIPHER* cipher = EVP_get_cipherbynid(nid)) {
return Cipher(cipher);
}
#if NCRYPTO_USE_OPENSSL3_PROVIDER
// May be a provider-only cipher; resolving by name falls back to a fetch.
if (const char* name = OBJ_nid2sn(nid)) {
return FromName(name);
}
#endif
return Cipher();
}

const Cipher Cipher::FromCtx(const CipherCtxPointer& ctx) {
Expand Down Expand Up @@ -4572,7 +4616,18 @@ int Cipher::getBlockSize() const {

int Cipher::getNid() const {
if (!cipher_) return 0;
return EVP_CIPHER_nid(cipher_);
int nid = EVP_CIPHER_nid(cipher_);
#if NCRYPTO_USE_OPENSSL3_PROVIDER
if (nid == NID_undef) {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This recovery only works when the provider algorithm's name is registered in the OBJ database. The generic fallback above also exposes algorithms without an OBJ NID: with OpenSSL 3.0, getCipherInfo('aes-128-cbc-cts') resolves but reports name: 'undef' and nid: 0.

Please preserve #63411's behavior of using EVP_CIPHER_get0_name() when no NID exists and omitting nid from the JavaScript result when it remains NID_undef.

// Provider-only ciphers inherit a nid from the legacy implementation they
// do not have, so recover it from the algorithm name.
if (const char* name = EVP_CIPHER_get0_name(cipher_)) {
nid = OBJ_sn2nid(name);
if (nid == NID_undef) nid = OBJ_ln2nid(name);
}
}
#endif
return nid;
}

std::string_view Cipher::getModeLabel() const {
Expand Down Expand Up @@ -6240,6 +6295,22 @@ void Cipher::ForEach(Cipher::CipherNameCallback callback) {
array_push_back<EVP_CIPHER>,
#endif
&context);

#if NCRYPTO_USE_OPENSSL3_PROVIDER
// EVP_CIPHER_do_all_sorted() walks the legacy name table, so provider-only
// algorithms have to be probed for by name.
static constexpr const char* kProviderOnlyCiphers[] = {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

#63411 adds provider-only AES-SIV and AES-GCM-SIV probes in this same region. Please extend its existing probe helper and preserve both sets of names when rebasing. Taking only this side drops SIV/GCM-SIV from getCiphers(), while taking only #63411's side drops these SM4 modes.

"sm4-gcm",
"sm4-ccm",
"sm4-xts",
};
for (const char* name : kProviderOnlyCiphers) {
if (EVP_CIPHER* fetched = EVP_CIPHER_fetch(nullptr, name, nullptr)) {
EVP_CIPHER_free(fetched);
context.cb(name);
}
}
#endif
#endif
}

Expand Down
201 changes: 201 additions & 0 deletions test/parallel/test-crypto-sm4-aead.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,201 @@
'use strict';
const common = require('../common');
if (!common.hasCrypto) common.skip('missing crypto');

const { hasOpenSSL } = require('../common/crypto');
const assert = require('assert');
const crypto = require('crypto');

// SM4-GCM and SM4-CCM are provider-only algorithms in OpenSSL 3.x (they
// have no legacy EVP_CIPHER implementation) and are available in the
// default provider since OpenSSL 3.1. SM4-XTS was added in OpenSSL 3.2.
// Refs: https://github.com/nodejs/node/issues/64866
if (!hasOpenSSL(3, 1)) common.skip('SM4 AEAD modes require OpenSSL >= 3.1');

if (crypto.getFips()) common.skip('SM4 is not FIPS-approved');

const ciphers = crypto.getCiphers();
if (!ciphers.includes('sm4-cbc'))
common.skip('SM4 support is disabled in this build');

// The provider-only SM4 modes must be reported by getCiphers().
assert(ciphers.includes('sm4-gcm'));
assert(ciphers.includes('sm4-ccm'));
const hasSm4Xts = hasOpenSSL(3, 2);
if (hasSm4Xts) assert(ciphers.includes('sm4-xts'));

// getCipherInfo() must resolve provider-only ciphers, both by name and
// by nid.
{
const info = crypto.getCipherInfo('sm4-gcm');
assert(info);
assert.strictEqual(info.name, 'sm4-gcm');
assert.strictEqual(info.nid, 1248);
assert.strictEqual(info.mode, 'gcm');
assert.strictEqual(info.keyLength, 16);
assert.strictEqual(info.ivLength, 12);
assert.deepStrictEqual(crypto.getCipherInfo(info.nid), info);
}

{
const info = crypto.getCipherInfo('sm4-ccm');
assert(info);
assert.strictEqual(info.name, 'sm4-ccm');
assert.strictEqual(info.nid, 1249);
assert.strictEqual(info.mode, 'ccm');
assert.strictEqual(info.keyLength, 16);
assert.strictEqual(info.ivLength, 12);
assert.deepStrictEqual(crypto.getCipherInfo(info.nid), info);
}

if (hasSm4Xts) {
const info = crypto.getCipherInfo('sm4-xts');
assert(info);
assert.strictEqual(info.name, 'sm4-xts');
assert.strictEqual(info.nid, 1290);
assert.strictEqual(info.mode, 'xts');
assert.strictEqual(info.keyLength, 32);
assert.deepStrictEqual(crypto.getCipherInfo(info.nid), info);
}

// Test vectors from RFC 8998, appendix A.
const kKey = Buffer.from('0123456789ABCDEFFEDCBA9876543210', 'hex');
const kIv = Buffer.from('00001234567800000000ABCD', 'hex');
const kAad = Buffer.from('FEEDFACEDEADBEEFFEEDFACEDEADBEEFABADDAD2', 'hex');
const kPlaintext = Buffer.from(
'AAAAAAAAAAAAAAAABBBBBBBBBBBBBBBB' +
'CCCCCCCCCCCCCCCCDDDDDDDDDDDDDDDD' +
'EEEEEEEEEEEEEEEEFFFFFFFFFFFFFFFF' +
'EEEEEEEEEEEEEEEEAAAAAAAAAAAAAAAA',
'hex',
);

// RFC 8998, appendix A.1.
{
const kCiphertext = Buffer.from(
'17F399F08C67D5EE19D0DC9969C4BB7D' +
'5FD46FD3756489069157B282BB200735' +
'D82710CA5C22F0CCFA7CBF93D496AC15' +
'A56834CBCF98C397B4024A2691233B8D',
'hex',
);
const kAuthTag = Buffer.from('83DE3541E4C2B58177E065A9BF7B62EC', 'hex');

const cipher = crypto.createCipheriv('sm4-gcm', kKey, kIv);
cipher.setAAD(kAad);
const ciphertext = Buffer.concat([cipher.update(kPlaintext), cipher.final()]);
assert.deepStrictEqual(ciphertext, kCiphertext);
assert.deepStrictEqual(cipher.getAuthTag(), kAuthTag);

const decipher = crypto.createDecipheriv('sm4-gcm', kKey, kIv);
decipher.setAAD(kAad);
decipher.setAuthTag(kAuthTag);
const plaintext = Buffer.concat([
decipher.update(kCiphertext),
decipher.final(),
]);
assert.deepStrictEqual(plaintext, kPlaintext);

// A tampered authentication tag must be rejected.
const badTag = Buffer.from(kAuthTag);
badTag[0] ^= 1;
const failing = crypto.createDecipheriv('sm4-gcm', kKey, kIv);
failing.setAAD(kAad);
failing.setAuthTag(badTag);
failing.update(kCiphertext);
assert.throws(() => failing.final(), {
message: /Unsupported state or unable to authenticate data/,
});
}

// RFC 8998, appendix A.2.
{
const kCiphertext = Buffer.from(
'48AF93501FA62ADBCD414CCE6034D895' +
'DDA1BF8F132F042098661572E7483094' +
'FD12E518CE062C98ACEE28D95DF4416B' +
'ED31A2F04476C18BB40C84A74B97DC5B',
'hex',
);
const kAuthTag = Buffer.from('16842D4FA186F56AB33256971FA110F4', 'hex');

const cipher = crypto.createCipheriv('sm4-ccm', kKey, kIv, {
authTagLength: 16,
});
cipher.setAAD(kAad, { plaintextLength: kPlaintext.length });
const ciphertext = Buffer.concat([cipher.update(kPlaintext), cipher.final()]);
assert.deepStrictEqual(ciphertext, kCiphertext);
assert.deepStrictEqual(cipher.getAuthTag(), kAuthTag);

const decipher = crypto.createDecipheriv('sm4-ccm', kKey, kIv, {
authTagLength: 16,
});
decipher.setAuthTag(kAuthTag);
decipher.setAAD(kAad, { plaintextLength: kCiphertext.length });
const plaintext = Buffer.concat([
decipher.update(kCiphertext),
decipher.final(),
]);
assert.deepStrictEqual(plaintext, kPlaintext);
}

// There are no official SM4-XTS test vectors; do a round-trip instead.
if (hasSm4Xts) {
const key = Buffer.from(
'00112233445566778899AABBCCDDEEFFFFEEDDCCBBAA99887766554433221100',
'hex',
);
const iv = Buffer.from('000102030405060708090A0B0C0D0E0F', 'hex');

const cipher = crypto.createCipheriv('sm4-xts', key, iv);
const ciphertext = Buffer.concat([cipher.update(kPlaintext), cipher.final()]);
assert.strictEqual(ciphertext.length, kPlaintext.length);
assert.notDeepStrictEqual(ciphertext, kPlaintext);

const decipher = crypto.createDecipheriv('sm4-xts', key, iv);
const plaintext = Buffer.concat([
decipher.update(ciphertext),
decipher.final(),
]);
assert.deepStrictEqual(plaintext, kPlaintext);
}

// Cipher name lookup is case-insensitive, including for fetched
// provider-only ciphers.
{
const cipher = crypto.createCipheriv('SM4-GCM', kKey, kIv);
cipher.setAAD(kAad);
cipher.update(kPlaintext);
cipher.final();
}

// The EVP_CIPHER_fetch() fallback must not make unknown algorithms resolve.
{
const unknown = 'sm4-gcm-not-a-real-cipher';

// Repeated, so that a failed lookup is not cached as a success.
for (let i = 0; i < 2; i++) {
assert.strictEqual(crypto.getCipherInfo(unknown), undefined);
assert.throws(() => crypto.createCipheriv(unknown, kKey, kIv), {
code: 'ERR_CRYPTO_UNKNOWN_CIPHER',
message: 'Unknown cipher',
});
}

// Unknown names must not leave anything behind on the OpenSSL error queue
// for the next operation to trip over.
const cipher = crypto.createCipheriv('sm4-gcm', kKey, kIv);
cipher.setAAD(kAad);
assert.strictEqual(
Buffer.concat([cipher.update(kPlaintext), cipher.final()]).length,
kPlaintext.length,
);
}

// Cipher::FromNid() now falls back to a name lookup. A nid that is a valid
// object identifier but not a cipher must still resolve to nothing.
{
assert.strictEqual(crypto.getCipherInfo(672), undefined); // NID_sha256
assert.strictEqual(crypto.getCipherInfo(0), undefined); // NID_undef
assert.strictEqual(crypto.getCipherInfo(-1), undefined);
}
48 changes: 48 additions & 0 deletions test/parallel/test-crypto-sm4-fips.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
// Flags: --expose-internals
'use strict';
const common = require('../common');
if (!common.hasCrypto) common.skip('missing crypto');

if (process.features.openssl_is_boringssl)
common.skip('BoringSSL does not support FIPS');

const { internalBinding } = require('internal/test/binding');
const { testFipsCrypto } = internalBinding('crypto');
if (!testFipsCrypto()) common.skip('no FIPS provider available');

const assert = require('assert');
const crypto = require('crypto');

// Also covers --force-fips, which makes setFips() throw.
if (crypto.getFips()) common.skip('FIPS is already enabled');

if (!crypto.getCiphers().includes('sm4-cbc'))
common.skip('SM4 support is disabled in this build');

// Provider-only ciphers are fetched once and cached. Enabling FIPS changes the
// default properties every fetch is resolved against, so the cached instance
// must not survive the switch: SM4 is not FIPS-approved.
// Refs: https://github.com/nodejs/node/issues/64866

const key = Buffer.alloc(16);
const iv = Buffer.alloc(12);

// Populate the cache while FIPS is still disabled.
assert(crypto.getCiphers().includes('sm4-gcm'));

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

test-crypto-sm4-aead.js correctly gates these modes on OpenSSL 3.1, but this test only checks for sm4-cbc. A FIPS-capable OpenSSL 3.0 build has SM4-CBC but not SM4-GCM, so it reaches this assertion and fails before exercising the FIPS transition.

Please import hasOpenSSL from ../common/crypto and add the same hasOpenSSL(3, 1) skip used by the AEAD test.

crypto.createCipheriv('sm4-gcm', key, iv);

crypto.setFips(true);
assert.strictEqual(crypto.getFips(), 1);

assert(!crypto.getCiphers().includes('sm4-gcm'));

@panva panva Aug 20, 2026

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This assertion cannot observe the transition as written. crypto.getCiphers() is wrapped in cachedResult() in lib/internal/crypto/util.js, and the calls above populate that JavaScript cache before setFips(true). This call therefore returns the same pre-FIPS list.

Please either make the public cipher-list cache FIPS-state-aware, or remove the dynamic getCiphers() assertions and retain the direct getCipherInfo() / createCipheriv() checks for the in-process transition.

assert.strictEqual(crypto.getCipherInfo('sm4-gcm'), undefined);
assert.throws(() => crypto.createCipheriv('sm4-gcm', key, iv), {
code: 'ERR_CRYPTO_UNKNOWN_CIPHER',
});

// Disabling FIPS again must make it available once more.
crypto.setFips(false);
assert.strictEqual(crypto.getFips(), 0);

assert(crypto.getCiphers().includes('sm4-gcm'));
crypto.createCipheriv('sm4-gcm', key, iv);
Loading