Skip to content

crypto: support provider-only SM4 cipher modes - #65399

Open
PickBas wants to merge 1 commit into
nodejs:mainfrom
JaneaSystems:issue-64866-updated
Open

crypto: support provider-only SM4 cipher modes#65399
PickBas wants to merge 1 commit into
nodejs:mainfrom
JaneaSystems:issue-64866-updated

Conversation

@PickBas

@PickBas PickBas commented Aug 19, 2026

Copy link
Copy Markdown
Contributor

crypto: support provider-only SM4 cipher modes

sm4-gcm, sm4-ccm and sm4-xts exist only as fetchable provider algorithms in OpenSSL 3. There is no legacy EVP_CIPHER for them, so EVP_get_cipherbyname() / EVP_get_cipherbynid() cannot resolve them and EVP_CIPHER_do_all_sorted(), which walks the legacy name table, never reports them. createCipheriv() threw Unknown cipher, getCipherInfo() returned undefined, and getCiphers() omitted them.

Changes

  • Cipher::FromName() / Cipher::FromNid() fall back to EVP_CIPHER_fetch().
  • Cipher::getNid() recovers the nid from the algorithm name, since a fetched cipher inherits its nid from the legacy implementation it does not have. Without this, getCipherInfo(name) reports no nid and cannot round-trip through getCipherInfo(nid).
  • Cipher::ForEach() probes for the provider-only modes so getCiphers() lists them.
  • Fetched instances are reference counted while Cipher is a non-owning wrapper, so they are cached for the process lifetime. The cache key includes the library context's default property state, because crypto.setFips() changes it at runtime and a cipher fetched beforehand must not stay usable afterwards.

Tests

  • test-crypto-sm4-aead.js - RFC 8998 A.1/A.2 known-answer vectors for GCM and CCM, XTS round-trip, tag tampering, getCipherInfo name/nid round-trip, case-insensitive lookup, and negative cases for unknown names and nids.
  • test-crypto-sm4-fips.js - enabling FIPS at runtime must invalidate an already fetched SM4 cipher. Skipped unless a FIPS provider is available.

Fixes: #64866

@nodejs-github-bot

Copy link
Copy Markdown
Collaborator

Review requested:

  • @nodejs/crypto
  • @nodejs/security-wg

@nodejs-github-bot nodejs-github-bot added dependencies Pull requests that update a dependency file. needs-ci PRs that need a full CI run. labels Aug 19, 2026
Fixes: nodejs#64866
Co-authored-by: StefanStojanovic <stefan.stojanovic@janeasystems.com>
Signed-off-by: Kirill Saied <sayed.kirill@gmail.com>
@PickBas
PickBas force-pushed the issue-64866-updated branch from 6cb1a8a to 491dc6d Compare August 19, 2026 11:27
@codecov

codecov Bot commented Aug 19, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 90.11%. Comparing base (de333e8) to head (491dc6d).
⚠️ Report is 213 commits behind head on main.

Additional details and impacted files
@@            Coverage Diff             @@
##             main   #65399      +/-   ##
==========================================
- Coverage   90.30%   90.11%   -0.20%     
==========================================
  Files         759      752       -7     
  Lines      247648   251864    +4216     
  Branches    46696    47355     +659     
==========================================
+ Hits       223644   226970    +3326     
- Misses      15466    16230     +764     
- Partials     8538     8664     +126     

see 203 files with indirect coverage changes

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@jasnell
jasnell requested review from panva and tniessen August 20, 2026 02:29

@panva panva left a comment

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 needs to wait for and be rebased on #63411 before landing. Both PRs modify the fetched-cipher lookup, ownership, metadata, and provider-only enumeration paths. #63411 now provides owned fetched handles and the authenticated-mode handling required for AES-SIV and AES-GCM-SIV.

The unrestricted fallback here currently makes every provider-fetchable cipher appear supported. On this head, AES-SIV resolves through createCipheriv() even though setAAD() and getAuthTag() fail, and provider algorithms without an OBJ NID can be reported as name: 'undef', nid: 0.

Please retain #63411's ownership and metadata handling, then extend its explicit eligibility and enumeration paths for SM4-GCM, SM4-CCM, and SM4-XTS. The process-lifetime fetched-cipher cache should not be carried over.

The FIPS test also needs correction: getCiphers() is memoized before the FIPS transition, and the test can run with OpenSSL 3.0 even though SM4-GCM requires OpenSSL 3.1.

Comment thread deps/ncrypto/ncrypto.cc
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().

Comment thread deps/ncrypto/ncrypto.cc
#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.

Comment thread deps/ncrypto/ncrypto.cc
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.

Comment thread deps/ncrypto/ncrypto.cc
// 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.

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.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.

@panva panva added the blocked PRs that are blocked by other issues or PRs. label Aug 20, 2026
@panva

panva commented Aug 20, 2026

Copy link
Copy Markdown
Member

Blocked by #63411

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

blocked PRs that are blocked by other issues or PRs. dependencies Pull requests that update a dependency file. needs-ci PRs that need a full CI run.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

crypto: SM4-GCM and SM4-CCM not available despite OpenSSL 3.5 bundling the implementation

3 participants