crypto: support provider-only SM4 cipher modes · nodejs/node@491dc6d · GitHub
Skip to content

Commit 491dc6d

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 491dc6d

3 files changed

Lines changed: 323 additions & 3 deletions

File tree

‎deps/ncrypto/ncrypto.cc‎

Lines changed: 74 additions & 3 deletions
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+
'00112233445566778899AABBCCDDEEFFFFEEDDCCBBAA99887766554433221100',
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

0 commit comments

Comments
 (0)