-
-
Notifications
You must be signed in to change notification settings - Fork 36.5k
crypto: support provider-only SM4 cipher modes #65399
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -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> | ||
|
|
@@ -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:" | ||
| : ""); | ||
| 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)) { | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 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 |
||
| 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) { | ||
|
|
@@ -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) { | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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, Please preserve #63411's behavior of using |
||
| // 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 { | ||
|
|
@@ -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[] = { | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. |
||
| "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 | ||
| } | ||
|
|
||
|
|
||
| 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); | ||
| } |
| 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')); | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Please import |
||
| crypto.createCipheriv('sm4-gcm', key, iv); | ||
|
|
||
| crypto.setFips(true); | ||
| assert.strictEqual(crypto.getFips(), 1); | ||
|
|
||
| assert(!crypto.getCiphers().includes('sm4-gcm')); | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This assertion cannot observe the transition as written.
|
||
| 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); | ||
There was a problem hiding this comment.
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 usesfetched_mutex, while FIPS transitions use a separatefips_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-
Cipherfetched handles when rebasing instead of introducing this process-lifetime property cache.