diff --git a/deps/ncrypto/ncrypto.cc b/deps/ncrypto/ncrypto.cc index b4fa65daa78c..c175d7c62a1a 100644 --- a/deps/ncrypto/ncrypto.cc +++ b/deps/ncrypto/ncrypto.cc @@ -14,9 +14,12 @@ #endif #include #include +#include #include #include +#include #include +#include #if OPENSSL_VERSION_MAJOR >= 3 #include #include @@ -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(); + + // 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(std::tolower(c)); + }); + + std::lock_guard 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)) { + 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) { + // 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, #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[] = { + "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 } diff --git a/test/parallel/test-crypto-sm4-aead.js b/test/parallel/test-crypto-sm4-aead.js new file mode 100644 index 000000000000..a04909948108 --- /dev/null +++ b/test/parallel/test-crypto-sm4-aead.js @@ -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); +} diff --git a/test/parallel/test-crypto-sm4-fips.js b/test/parallel/test-crypto-sm4-fips.js new file mode 100644 index 000000000000..3bb7b4ecb4d3 --- /dev/null +++ b/test/parallel/test-crypto-sm4-fips.js @@ -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')); +crypto.createCipheriv('sm4-gcm', key, iv); + +crypto.setFips(true); +assert.strictEqual(crypto.getFips(), 1); + +assert(!crypto.getCiphers().includes('sm4-gcm')); +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);