Skip to content

Commit 6cb1a8a

Browse files
crypto: support provider-only SM4 cipher modes
Fixes: #64866 Co-authored-by: StefanStojanovic <stefan.stojanovic@janeasystems.com> Signed-off-by: Kirill Saied <sayed.kirill@gmail.com>
1 parent de333e8 commit 6cb1a8a

3 files changed

Lines changed: 323 additions & 3 deletions

File tree

deps/ncrypto/ncrypto.cc

Lines changed: 74 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -14,9 +14,12 @@
1414
#endif
1515
#include <algorithm>
1616
#include <array>
17+
#include <cctype>
1718
#include <climits>
1819
#include <cstring>
20+
#include <mutex>
1921
#include <string_view>
22+
#include <unordered_map>
2023
#if OPENSSL_VERSION_MAJOR >= 3
2124
#include <openssl/core_names.h>
2225
#include <openssl/params.h>
@@ -4482,11 +4485,52 @@ bool SSLCtxPointer::setCipherSuites(const char* ciphers) {
44824485
// ============================================================================
44834486

44844487
const Cipher Cipher::FromName(const char* name) {
4485-
return Cipher(EVP_get_cipherbyname(name));
4488+
if (const EVP_CIPHER* cipher = EVP_get_cipherbyname(name)) {
4489+
return Cipher(cipher);
4490+
}
4491+
#if NCRYPTO_USE_OPENSSL3_PROVIDER
4492+
// Ciphers such as SM4-GCM only exist as fetchable provider algorithms.
4493+
// Cipher does not own what it points at, so the fetched reference is kept
4494+
// for the lifetime of the process instead of being freed.
4495+
MarkPopErrorOnReturn mark_pop_error_on_return;
4496+
4497+
static std::mutex fetched_mutex;
4498+
static auto& fetched_ciphers =
4499+
*new std::unordered_map<std::string, const EVP_CIPHER*>();
4500+
4501+
// A fetch is resolved against the library context's default properties,
4502+
// which setFipsEnabled() changes at runtime. Key on that state as well so
4503+
// that a cipher fetched before the switch cannot outlive it.
4504+
std::string key(EVP_default_properties_is_fips_enabled(nullptr) ? "fips:"
4505+
: "");
4506+
key.append(name);
4507+
std::transform(key.begin(), key.end(), key.begin(), [](unsigned char c) {
4508+
return static_cast<char>(std::tolower(c));
4509+
});
4510+
4511+
std::lock_guard<std::mutex> lock(fetched_mutex);
4512+
if (auto it = fetched_ciphers.find(key); it != fetched_ciphers.end()) {
4513+
return Cipher(it->second);
4514+
}
4515+
if (const EVP_CIPHER* fetched = EVP_CIPHER_fetch(nullptr, name, nullptr)) {
4516+
fetched_ciphers[key] = fetched;
4517+
return Cipher(fetched);
4518+
}
4519+
#endif
4520+
return Cipher();
44864521
}
44874522

44884523
const Cipher Cipher::FromNid(int nid) {
4489-
return Cipher(EVP_get_cipherbynid(nid));
4524+
if (const EVP_CIPHER* cipher = EVP_get_cipherbynid(nid)) {
4525+
return Cipher(cipher);
4526+
}
4527+
#if NCRYPTO_USE_OPENSSL3_PROVIDER
4528+
// May be a provider-only cipher; resolving by name falls back to a fetch.
4529+
if (const char* name = OBJ_nid2sn(nid)) {
4530+
return FromName(name);
4531+
}
4532+
#endif
4533+
return Cipher();
44904534
}
44914535

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

45734617
int Cipher::getNid() const {
45744618
if (!cipher_) return 0;
4575-
return EVP_CIPHER_nid(cipher_);
4619+
int nid = EVP_CIPHER_nid(cipher_);
4620+
#if NCRYPTO_USE_OPENSSL3_PROVIDER
4621+
if (nid == NID_undef) {
4622+
// Provider-only ciphers inherit a nid from the legacy implementation they
4623+
// do not have, so recover it from the algorithm name.
4624+
if (const char* name = EVP_CIPHER_get0_name(cipher_)) {
4625+
nid = OBJ_sn2nid(name);
4626+
if (nid == NID_undef) nid = OBJ_ln2nid(name);
4627+
}
4628+
}
4629+
#endif
4630+
return nid;
45764631
}
45774632

45784633
std::string_view Cipher::getModeLabel() const {
@@ -6240,6 +6295,22 @@ void Cipher::ForEach(Cipher::CipherNameCallback callback) {
62406295
array_push_back<EVP_CIPHER>,
62416296
#endif
62426297
&context);
6298+
6299+
#if NCRYPTO_USE_OPENSSL3_PROVIDER
6300+
// EVP_CIPHER_do_all_sorted() walks the legacy name table, so provider-only
6301+
// algorithms have to be probed for by name.
6302+
static constexpr const char* kProviderOnlyCiphers[] = {
6303+
"sm4-gcm",
6304+
"sm4-ccm",
6305+
"sm4-xts",
6306+
};
6307+
for (const char* name : kProviderOnlyCiphers) {
6308+
if (EVP_CIPHER* fetched = EVP_CIPHER_fetch(nullptr, name, nullptr)) {
6309+
EVP_CIPHER_free(fetched);
6310+
context.cb(name);
6311+
}
6312+
}
6313+
#endif
62436314
#endif
62446315
}
62456316

Lines changed: 201 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,201 @@
1+
'use strict';
2+
const common = require('../common');
3+
if (!common.hasCrypto) common.skip('missing crypto');
4+
5+
const { hasOpenSSL } = require('../common/crypto');
6+
const assert = require('assert');
7+
const crypto = require('crypto');
8+
9+
// SM4-GCM and SM4-CCM are provider-only algorithms in OpenSSL 3.x (they
10+
// have no legacy EVP_CIPHER implementation) and are available in the
11+
// default provider since OpenSSL 3.1. SM4-XTS was added in OpenSSL 3.2.
12+
// Refs: https://github.com/nodejs/node/issues/64866
13+
if (!hasOpenSSL(3, 1)) common.skip('SM4 AEAD modes require OpenSSL >= 3.1');
14+
15+
if (crypto.getFips()) common.skip('SM4 is not FIPS-approved');
16+
17+
const ciphers = crypto.getCiphers();
18+
if (!ciphers.includes('sm4-cbc'))
19+
common.skip('SM4 support is disabled in this build');
20+
21+
// The provider-only SM4 modes must be reported by getCiphers().
22+
assert(ciphers.includes('sm4-gcm'));
23+
assert(ciphers.includes('sm4-ccm'));
24+
const hasSm4Xts = hasOpenSSL(3, 2);
25+
if (hasSm4Xts) assert(ciphers.includes('sm4-xts'));
26+
27+
// getCipherInfo() must resolve provider-only ciphers, both by name and
28+
// by nid.
29+
{
30+
const info = crypto.getCipherInfo('sm4-gcm');
31+
assert(info);
32+
assert.strictEqual(info.name, 'sm4-gcm');
33+
assert.strictEqual(info.nid, 1248);
34+
assert.strictEqual(info.mode, 'gcm');
35+
assert.strictEqual(info.keyLength, 16);
36+
assert.strictEqual(info.ivLength, 12);
37+
assert.deepStrictEqual(crypto.getCipherInfo(info.nid), info);
38+
}
39+
40+
{
41+
const info = crypto.getCipherInfo('sm4-ccm');
42+
assert(info);
43+
assert.strictEqual(info.name, 'sm4-ccm');
44+
assert.strictEqual(info.nid, 1249);
45+
assert.strictEqual(info.mode, 'ccm');
46+
assert.strictEqual(info.keyLength, 16);
47+
assert.strictEqual(info.ivLength, 12);
48+
assert.deepStrictEqual(crypto.getCipherInfo(info.nid), info);
49+
}
50+
51+
if (hasSm4Xts) {
52+
const info = crypto.getCipherInfo('sm4-xts');
53+
assert(info);
54+
assert.strictEqual(info.name, 'sm4-xts');
55+
assert.strictEqual(info.nid, 1290);
56+
assert.strictEqual(info.mode, 'xts');
57+
assert.strictEqual(info.keyLength, 32);
58+
assert.deepStrictEqual(crypto.getCipherInfo(info.nid), info);
59+
}
60+
61+
// Test vectors from RFC 8998, appendix A.
62+
const kKey = Buffer.from('0123456789ABCDEFFEDCBA9876543210', 'hex');
63+
const kIv = Buffer.from('00001234567800000000ABCD', 'hex');
64+
const kAad = Buffer.from('FEEDFACEDEADBEEFFEEDFACEDEADBEEFABADDAD2', 'hex');
65+
const kPlaintext = Buffer.from(
66+
'AAAAAAAAAAAAAAAABBBBBBBBBBBBBBBB' +
67+
'CCCCCCCCCCCCCCCCDDDDDDDDDDDDDDDD' +
68+
'EEEEEEEEEEEEEEEEFFFFFFFFFFFFFFFF' +
69+
'EEEEEEEEEEEEEEEEAAAAAAAAAAAAAAAA',
70+
'hex',
71+
);
72+
73+
// RFC 8998, appendix A.1.
74+
{
75+
const kCiphertext = Buffer.from(
76+
'17F399F08C67D5EE19D0DC9969C4BB7D' +
77+
'5FD46FD3756489069157B282BB200735' +
78+
'D82710CA5C22F0CCFA7CBF93D496AC15' +
79+
'A56834CBCF98C397B4024A2691233B8D',
80+
'hex',
81+
);
82+
const kAuthTag = Buffer.from('83DE3541E4C2B58177E065A9BF7B62EC', 'hex');
83+
84+
const cipher = crypto.createCipheriv('sm4-gcm', kKey, kIv);
85+
cipher.setAAD(kAad);
86+
const ciphertext = Buffer.concat([cipher.update(kPlaintext), cipher.final()]);
87+
assert.deepStrictEqual(ciphertext, kCiphertext);
88+
assert.deepStrictEqual(cipher.getAuthTag(), kAuthTag);
89+
90+
const decipher = crypto.createDecipheriv('sm4-gcm', kKey, kIv);
91+
decipher.setAAD(kAad);
92+
decipher.setAuthTag(kAuthTag);
93+
const plaintext = Buffer.concat([
94+
decipher.update(kCiphertext),
95+
decipher.final(),
96+
]);
97+
assert.deepStrictEqual(plaintext, kPlaintext);
98+
99+
// A tampered authentication tag must be rejected.
100+
const badTag = Buffer.from(kAuthTag);
101+
badTag[0] ^= 1;
102+
const failing = crypto.createDecipheriv('sm4-gcm', kKey, kIv);
103+
failing.setAAD(kAad);
104+
failing.setAuthTag(badTag);
105+
failing.update(kCiphertext);
106+
assert.throws(() => failing.final(), {
107+
message: /Unsupported state or unable to authenticate data/,
108+
});
109+
}
110+
111+
// RFC 8998, appendix A.2.
112+
{
113+
const kCiphertext = Buffer.from(
114+
'48AF93501FA62ADBCD414CCE6034D895' +
115+
'DDA1BF8F132F042098661572E7483094' +
116+
'FD12E518CE062C98ACEE28D95DF4416B' +
117+
'ED31A2F04476C18BB40C84A74B97DC5B',
118+
'hex',
119+
);
120+
const kAuthTag = Buffer.from('16842D4FA186F56AB33256971FA110F4', 'hex');
121+
122+
const cipher = crypto.createCipheriv('sm4-ccm', kKey, kIv, {
123+
authTagLength: 16,
124+
});
125+
cipher.setAAD(kAad, { plaintextLength: kPlaintext.length });
126+
const ciphertext = Buffer.concat([cipher.update(kPlaintext), cipher.final()]);
127+
assert.deepStrictEqual(ciphertext, kCiphertext);
128+
assert.deepStrictEqual(cipher.getAuthTag(), kAuthTag);
129+
130+
const decipher = crypto.createDecipheriv('sm4-ccm', kKey, kIv, {
131+
authTagLength: 16,
132+
});
133+
decipher.setAuthTag(kAuthTag);
134+
decipher.setAAD(kAad, { plaintextLength: kCiphertext.length });
135+
const plaintext = Buffer.concat([
136+
decipher.update(kCiphertext),
137+
decipher.final(),
138+
]);
139+
assert.deepStrictEqual(plaintext, kPlaintext);
140+
}
141+
142+
// There are no official SM4-XTS test vectors; do a round-trip instead.
143+
if (hasSm4Xts) {
144+
const key = Buffer.from(
145+
'00112233445566778899AABBCCDDEEFF' + 'FFEEDDCCBBAA99887766554433221100',
146+
'hex',
147+
);
148+
const iv = Buffer.from('000102030405060708090A0B0C0D0E0F', 'hex');
149+
150+
const cipher = crypto.createCipheriv('sm4-xts', key, iv);
151+
const ciphertext = Buffer.concat([cipher.update(kPlaintext), cipher.final()]);
152+
assert.strictEqual(ciphertext.length, kPlaintext.length);
153+
assert.notDeepStrictEqual(ciphertext, kPlaintext);
154+
155+
const decipher = crypto.createDecipheriv('sm4-xts', key, iv);
156+
const plaintext = Buffer.concat([
157+
decipher.update(ciphertext),
158+
decipher.final(),
159+
]);
160+
assert.deepStrictEqual(plaintext, kPlaintext);
161+
}
162+
163+
// Cipher name lookup is case-insensitive, including for fetched
164+
// provider-only ciphers.
165+
{
166+
const cipher = crypto.createCipheriv('SM4-GCM', kKey, kIv);
167+
cipher.setAAD(kAad);
168+
cipher.update(kPlaintext);
169+
cipher.final();
170+
}
171+
172+
// The EVP_CIPHER_fetch() fallback must not make unknown algorithms resolve.
173+
{
174+
const unknown = 'sm4-gcm-not-a-real-cipher';
175+
176+
// Repeated, so that a failed lookup is not cached as a success.
177+
for (let i = 0; i < 2; i++) {
178+
assert.strictEqual(crypto.getCipherInfo(unknown), undefined);
179+
assert.throws(() => crypto.createCipheriv(unknown, kKey, kIv), {
180+
code: 'ERR_CRYPTO_UNKNOWN_CIPHER',
181+
message: 'Unknown cipher',
182+
});
183+
}
184+
185+
// Unknown names must not leave anything behind on the OpenSSL error queue
186+
// for the next operation to trip over.
187+
const cipher = crypto.createCipheriv('sm4-gcm', kKey, kIv);
188+
cipher.setAAD(kAad);
189+
assert.strictEqual(
190+
Buffer.concat([cipher.update(kPlaintext), cipher.final()]).length,
191+
kPlaintext.length,
192+
);
193+
}
194+
195+
// Cipher::FromNid() now falls back to a name lookup. A nid that is a valid
196+
// object identifier but not a cipher must still resolve to nothing.
197+
{
198+
assert.strictEqual(crypto.getCipherInfo(672), undefined); // NID_sha256
199+
assert.strictEqual(crypto.getCipherInfo(0), undefined); // NID_undef
200+
assert.strictEqual(crypto.getCipherInfo(-1), undefined);
201+
}
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
// Flags: --expose-internals
2+
'use strict';
3+
const common = require('../common');
4+
if (!common.hasCrypto) common.skip('missing crypto');
5+
6+
if (process.features.openssl_is_boringssl)
7+
common.skip('BoringSSL does not support FIPS');
8+
9+
const { internalBinding } = require('internal/test/binding');
10+
const { testFipsCrypto } = internalBinding('crypto');
11+
if (!testFipsCrypto()) common.skip('no FIPS provider available');
12+
13+
const assert = require('assert');
14+
const crypto = require('crypto');
15+
16+
// Also covers --force-fips, which makes setFips() throw.
17+
if (crypto.getFips()) common.skip('FIPS is already enabled');
18+
19+
if (!crypto.getCiphers().includes('sm4-cbc'))
20+
common.skip('SM4 support is disabled in this build');
21+
22+
// Provider-only ciphers are fetched once and cached. Enabling FIPS changes the
23+
// default properties every fetch is resolved against, so the cached instance
24+
// must not survive the switch: SM4 is not FIPS-approved.
25+
// Refs: https://github.com/nodejs/node/issues/64866
26+
27+
const key = Buffer.alloc(16);
28+
const iv = Buffer.alloc(12);
29+
30+
// Populate the cache while FIPS is still disabled.
31+
assert(crypto.getCiphers().includes('sm4-gcm'));
32+
crypto.createCipheriv('sm4-gcm', key, iv);
33+
34+
crypto.setFips(true);
35+
assert.strictEqual(crypto.getFips(), 1);
36+
37+
assert(!crypto.getCiphers().includes('sm4-gcm'));
38+
assert.strictEqual(crypto.getCipherInfo('sm4-gcm'), undefined);
39+
assert.throws(() => crypto.createCipheriv('sm4-gcm', key, iv), {
40+
code: 'ERR_CRYPTO_UNKNOWN_CIPHER',
41+
});
42+
43+
// Disabling FIPS again must make it available once more.
44+
crypto.setFips(false);
45+
assert.strictEqual(crypto.getFips(), 0);
46+
47+
assert(crypto.getCiphers().includes('sm4-gcm'));
48+
crypto.createCipheriv('sm4-gcm', key, iv);

0 commit comments

Comments
 (0)