Skip to content

Commit 94270ff

Browse files
committed
SM9: Cipher.SM9 decrypts in the mode it was configured with rather than the one the ciphertext's enType names, and neither data-encapsulation mode now produces or accepts a 16-byte C2.
1 parent 04e1bf3 commit 94270ff

7 files changed

Lines changed: 462 additions & 27 deletions

File tree

‎core/src/main/java/org/bouncycastle/crypto/engines/SM9Engine.java‎

Lines changed: 56 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,15 @@
3434
* Usage follows the {@link SM2Engine} pattern: construct with the desired mode,
3535
* {@code init(true, new ParametersWithRandom(recipientKey, random))} to encrypt or
3636
* {@code init(false, userKey)} to decrypt, then {@link #processBlock(byte[], int, int)}.
37+
* <p>
38+
* The mode is the caller's to keep: C1 || C3 || C2 does not record which method produced it
39+
* and C3 = MAC(K2, C2) does not cover the choice, so a ciphertext has to be decrypted by an
40+
* engine constructed for the mode it was encrypted in. The two methods take K1 and K2 from the
41+
* same KDF call when C2 is 16 bytes long - the one length at which a ciphertext of either method
42+
* would pass the other's MAC check - and neither mode therefore produces or accepts a 16-byte C2:
43+
* the stream mode refuses a 16-byte message, the SM4 mode a message of fewer than 16 bytes, which
44+
* pads to one block, and both refuse a 16-byte C2 on decryption. A message of fewer than 16 bytes
45+
* has to be sent in stream mode, and one of exactly 16 bytes in SM4 mode.
3746
*/
3847
public class SM9Engine
3948
{
@@ -65,6 +74,23 @@ private Mode()
6574
*/
6675
private static final int MAX_K1_LEN = (Integer.MAX_VALUE / 8) - K2_LEN;
6776

77+
/**
78+
* K1_len for the SM4 method: the SM4 key, 128 bits. In the stream method K1_len is the message
79+
* length, and GM/T 0044.4 takes K1 || K2 from one KDF(C1 || w || ID_B, K1_len + K2_len) in either
80+
* method, so a stream-mode ciphertext with a 16-byte C2 and a one-block SM4 ciphertext (a message
81+
* of 0 to 15 bytes, padded) that share a C1 derive the same K1 and K2, and each passes the other
82+
* method's MAC check. Nothing in C1 || C3 || C2 says which method produced it, so a recipient
83+
* cannot tell the two apart; both modes refuse that one length instead, on encryption and on
84+
* decryption, rather than let a ciphertext of one method decrypt under the other. Refusing it in
85+
* the stream mode alone would protect only a recipient that does so: the ciphertext whose key is
86+
* given away - a stream-mode decryption of it hands back K1 xor C2 - is the one-block SM4 one,
87+
* and its sender cannot know whether the recipient's implementation refuses the length, so the
88+
* SM4 mode does not produce it. With no sender producing a 16-byte C2 in either mode the SM4 mode
89+
* has no reason to accept one, and a 16-byte stream-mode C2 offered to it would pass the MAC check
90+
* and decrypt to a random block, accepted as a message whenever its padding happened to be valid.
91+
*/
92+
private static final int SM4_K1_LEN = 16;
93+
6894
private final Mode mode;
6995

7096
private boolean forEncryption;
@@ -180,7 +206,7 @@ private byte[] encrypt(byte[] message)
180206
{
181207
// K1_len is the message length, so an empty message has no K1 to test
182208
// against zero and the retry loop would never terminate; the SM4 mode
183-
// handles empty input (one padding block).
209+
// refuses it too, as it does every message of fewer than 16 bytes - see below.
184210
throw new InvalidCipherTextException("SM9 stream mode cannot encrypt an empty message");
185211
}
186212

@@ -190,11 +216,25 @@ private byte[] encrypt(byte[] message)
190216
Fp12 g = master.pairingWithP2();
191217
BigInteger n = SM9Curve.N;
192218

193-
int k1Len = (mode == Mode.SM4) ? 16 : message.length;
219+
int k1Len = (mode == Mode.SM4) ? SM4_K1_LEN : message.length;
194220
if (k1Len > MAX_K1_LEN)
195221
{
196222
throw new InvalidCipherTextException("SM9 message too long for the stream mode KDF");
197223
}
224+
if (mode == Mode.STREAM && k1Len == SM4_K1_LEN)
225+
{
226+
// the one length at which the KDF call, and so K1 and K2, coincide with the SM4
227+
// method's - see SM4_K1_LEN; a 16-byte stream-mode C2 is refused on decryption too,
228+
// so there is nothing to gain by producing one
229+
throw new InvalidCipherTextException("SM9 stream mode cannot encrypt a 16-byte message");
230+
}
231+
if (mode == Mode.SM4 && message.length < SM4_K1_LEN)
232+
{
233+
// fewer than 16 bytes pad to a single SM4 block, the same 16-byte C2 from this method's
234+
// side - see SM4_K1_LEN; a stream-mode recipient that does not refuse the length would
235+
// decrypt it to K1 xor C2, and the sender cannot tell which recipients do
236+
throw new InvalidCipherTextException("SM9 SM4 mode cannot encrypt a message shorter than 16 bytes");
237+
}
198238

199239
for (;;)
200240
{
@@ -235,12 +275,25 @@ private byte[] decrypt(byte[] ciphertext)
235275
byte[] c3 = Arrays.copyOfRange(ciphertext, 64, 96);
236276
byte[] c2 = Arrays.copyOfRange(ciphertext, 96, ciphertext.length);
237277

238-
int k1Len = (mode == Mode.SM4) ? 16 : c2.length;
278+
int k1Len = (mode == Mode.SM4) ? SM4_K1_LEN : c2.length;
239279
if (k1Len > MAX_K1_LEN)
240280
{
241281
// checked before the pairing so an over-long ciphertext is rejected without paying for one
242282
throw new InvalidCipherTextException("SM9 ciphertext too long for the stream mode KDF");
243283
}
284+
if (mode == Mode.STREAM && k1Len == SM4_K1_LEN)
285+
{
286+
// the one C2 length at which an SM4-mode ciphertext passes this mode's MAC check and
287+
// would decrypt to K1 xor C2 - see SM4_K1_LEN; also refused before the pairing
288+
throw new InvalidCipherTextException("SM9 stream-mode ciphertext has a 16-byte C2");
289+
}
290+
if (mode == Mode.SM4 && c2.length == SM4_K1_LEN)
291+
{
292+
// and the C2 length this mode no longer produces, at which a 16-byte stream-mode
293+
// ciphertext passes this mode's MAC check and would decrypt to a random block, a
294+
// message whenever its padding happened to be valid - see SM4_K1_LEN
295+
throw new InvalidCipherTextException("SM9 SM4-mode ciphertext has a 16-byte C2");
296+
}
244297

245298
Fp12 w = SM9Pairing.pairing(c1, userKey.getPrivatePoint());
246299
byte[] k = SM9Sm3.kdf(Arrays.concatenate(c1b, SM9Pairing.toBytes(w), userKey.getIdentity()), (k1Len + K2_LEN) * 8);

‎core/src/test/java/org/bouncycastle/crypto/test/RegressionTest.java‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -160,6 +160,7 @@ public class RegressionTest
160160
new ShortWrapCipherTextTest(),
161161
new SM4Test(),
162162
new SM9KEMTest(),
163+
new SM9EngineTest(),
163164
new SM9SignerTest(),
164165
new SM9KeyExchangeTest(),
165166
new DSTU7624Test(),
Lines changed: 201 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,201 @@
1+
package org.bouncycastle.crypto.test;
2+
3+
import java.math.BigInteger;
4+
5+
import org.bouncycastle.crypto.AsymmetricCipherKeyPair;
6+
import org.bouncycastle.crypto.CryptoServicesRegistrar;
7+
import org.bouncycastle.crypto.InvalidCipherTextException;
8+
import org.bouncycastle.crypto.KeyGenerationParameters;
9+
import org.bouncycastle.crypto.engines.SM9Engine;
10+
import org.bouncycastle.crypto.generators.SM9EncMasterKeyPairGenerator;
11+
import org.bouncycastle.crypto.params.ParametersWithRandom;
12+
import org.bouncycastle.crypto.params.SM9EncMasterPrivateKeyParameters;
13+
import org.bouncycastle.crypto.params.SM9EncMasterPublicKeyParameters;
14+
import org.bouncycastle.crypto.params.SM9EncPrivateKeyParameters;
15+
import org.bouncycastle.crypto.params.SM9EncPublicKeyParameters;
16+
import org.bouncycastle.util.Arrays;
17+
import org.bouncycastle.util.Strings;
18+
import org.bouncycastle.util.encoders.Hex;
19+
import org.bouncycastle.util.test.SimpleTest;
20+
21+
/**
22+
* Tests of the SM9 public-key encryption engine (GM/T 0044.4-2016) at the lightweight layer.
23+
* The GM/T 0044.5 known answers for both data-encapsulation methods are checked through the
24+
* provider in the jce SM9CipherTest; this test covers what the raw C1 || C3 || C2 form leaves to
25+
* the caller - the engine's mode has to be the one the sender used, nothing in the ciphertext
26+
* records it - and both modes' refusal of the one C2 length, 16 bytes, at which the two
27+
* methods' KDF calls coincide and a ciphertext of either passes the other's MAC check.
28+
*/
29+
public class SM9EngineTest
30+
extends SimpleTest
31+
{
32+
// The GM/T 0044.5-2016 Annex D encryption master private key ke, and a one-block SM4-mode
33+
// ciphertext C1 || C3 || C2 of the message "one block" to the identity "Bob" under it, made with
34+
// the annex's r by this engine before its SM4 mode stopped encrypting messages of fewer than 16
35+
// bytes - so C1 is the annex's C1 and the K1 and K2 behind C2 and C3 are its method b) K1 and K2.
36+
private static final BigInteger ANNEX_D_KE =
37+
new BigInteger("01EDEE3778F441F8DEA3D9FA0ACC4E07EE36C93F9A08618AF4AD85CEDE1C22", 16);
38+
private static final byte[] ONE_BLOCK_SM4 = Hex.decode(
39+
"2445471164490618E1EE20528FF1D545B0F14C8BCAA44544F03DAB5DAC07D8FF"
40+
+ "42FFCA97D57CDDC05EA405F2E586FEB3A6930715532B8000759F13059ED59AC0"
41+
+ "059C700E0E8FEE2801B3EEA529A39390C9138881914C3CAD9E1331EA9E430E9F"
42+
+ "195527A7B90D2A8CE59D01C20EC36E06");
43+
44+
public String getName()
45+
{
46+
return "SM9Engine";
47+
}
48+
49+
public void performTest()
50+
throws Exception
51+
{
52+
SM9EncMasterKeyPairGenerator kpGen = new SM9EncMasterKeyPairGenerator();
53+
kpGen.init(new KeyGenerationParameters(CryptoServicesRegistrar.getSecureRandom(), 256));
54+
AsymmetricCipherKeyPair master = kpGen.generateKeyPair();
55+
byte[] identity = Strings.toByteArray("Bob");
56+
SM9EncPublicKeyParameters bobPublic =
57+
((SM9EncMasterPublicKeyParameters)master.getPublic()).getUserPublicKey(identity);
58+
SM9EncPrivateKeyParameters bobKey =
59+
((SM9EncMasterPrivateKeyParameters)master.getPrivate()).generateUserKey(identity, SM9EncMasterPrivateKeyParameters.HID);
60+
61+
// both methods round-trip, the stream method at the lengths either side of 16
62+
int[] streamLengths = { 1, 15, 17, 32 };
63+
for (int i = 0; i != streamLengths.length; i++)
64+
{
65+
byte[] message = message(streamLengths[i]);
66+
byte[] ciphertext = encrypt(SM9Engine.Mode.STREAM, bobPublic, message);
67+
isTrue("SM9 stream-mode C2 is the message length at " + message.length + " bytes",
68+
ciphertext.length == 96 + message.length);
69+
isTrue("SM9 stream-mode round-trip at " + message.length + " bytes",
70+
Arrays.areEqual(message, decrypt(SM9Engine.Mode.STREAM, bobKey, ciphertext)));
71+
}
72+
int[] sm4Lengths = { 16, 17, 32 };
73+
for (int i = 0; i != sm4Lengths.length; i++)
74+
{
75+
byte[] message = message(sm4Lengths[i]);
76+
byte[] ciphertext = encrypt(SM9Engine.Mode.SM4, bobPublic, message);
77+
isTrue("SM9 SM4-mode C2 is the padded message at " + message.length + " bytes",
78+
ciphertext.length == 96 + ((message.length / 16) + 1) * 16);
79+
isTrue("SM9 SM4-mode round-trip at " + message.length + " bytes",
80+
Arrays.areEqual(message, decrypt(SM9Engine.Mode.SM4, bobKey, ciphertext)));
81+
}
82+
83+
// a one-block SM4 ciphertext (a message of 0 to 15 bytes, so |C2| = 16) offered to a
84+
// stream-mode engine: same C1 and the same KDF call, so the same K1 and K2 and a MAC that
85+
// checks - the engine would return K1 xor C2, from which K1 and then the message follow.
86+
// Refused by length, before the pairing.
87+
SM9EncPrivateKeyParameters annexKey = new SM9EncMasterPrivateKeyParameters(ANNEX_D_KE)
88+
.generateUserKey(identity, SM9EncMasterPrivateKeyParameters.HID);
89+
isTrue("SM9 one-block SM4 ciphertext has a 16-byte C2", ONE_BLOCK_SM4.length == 96 + 16);
90+
try
91+
{
92+
decrypt(SM9Engine.Mode.STREAM, annexKey, ONE_BLOCK_SM4);
93+
fail("SM9 stream-mode engine decrypted a one-block SM4-mode ciphertext");
94+
}
95+
catch (InvalidCipherTextException e)
96+
{
97+
isTrue("SM9 stream-mode 16-byte C2 rejection message",
98+
"SM9 stream-mode ciphertext has a 16-byte C2".equals(e.getMessage()));
99+
}
100+
101+
// and the stream mode will not produce a 16-byte C2 either
102+
try
103+
{
104+
encrypt(SM9Engine.Mode.STREAM, bobPublic, message(16));
105+
fail("SM9 stream-mode engine encrypted a 16-byte message");
106+
}
107+
catch (InvalidCipherTextException e)
108+
{
109+
isTrue("SM9 stream-mode 16-byte message rejection message",
110+
"SM9 stream mode cannot encrypt a 16-byte message".equals(e.getMessage()));
111+
}
112+
113+
// nor will the SM4 mode, which is why the ciphertext above is a stored one: a recipient
114+
// that refuses the length is protected by that, but the message given away is the SM4-mode
115+
// sender's, who cannot tell whether the recipient does - so no message that pads to one
116+
// block is encrypted
117+
int[] oneBlockLengths = { 0, 1, 15 };
118+
for (int i = 0; i != oneBlockLengths.length; i++)
119+
{
120+
try
121+
{
122+
encrypt(SM9Engine.Mode.SM4, bobPublic, message(oneBlockLengths[i]));
123+
fail("SM9 SM4-mode engine encrypted a " + oneBlockLengths[i] + "-byte message");
124+
}
125+
catch (InvalidCipherTextException e)
126+
{
127+
isTrue("SM9 SM4-mode short message rejection message at " + oneBlockLengths[i] + " bytes",
128+
"SM9 SM4 mode cannot encrypt a message shorter than 16 bytes".equals(e.getMessage()));
129+
}
130+
}
131+
132+
// and with neither mode producing a 16-byte C2 the SM4 mode does not accept one - here
133+
// the stored ciphertext, genuine and once decryptable, in its own mode
134+
try
135+
{
136+
decrypt(SM9Engine.Mode.SM4, annexKey, ONE_BLOCK_SM4);
137+
fail("SM9 SM4-mode engine decrypted a ciphertext with a 16-byte C2");
138+
}
139+
catch (InvalidCipherTextException e)
140+
{
141+
isTrue("SM9 SM4-mode 16-byte C2 rejection message",
142+
"SM9 SM4-mode ciphertext has a 16-byte C2".equals(e.getMessage()));
143+
}
144+
145+
// at every other length the two methods take K2 from different offsets of the KDF output,
146+
// so a ciphertext of one fails the other's MAC check: a two-block SM4 ciphertext offered
147+
// to the stream mode, and a 32-byte stream ciphertext offered to the SM4 mode
148+
byte[] twoBlocks = encrypt(SM9Engine.Mode.SM4, bobPublic, message(16));
149+
isTrue("SM9 two-block SM4 ciphertext has a 32-byte C2", twoBlocks.length == 96 + 32);
150+
try
151+
{
152+
decrypt(SM9Engine.Mode.STREAM, bobKey, twoBlocks);
153+
fail("SM9 stream-mode engine decrypted a two-block SM4-mode ciphertext");
154+
}
155+
catch (InvalidCipherTextException e)
156+
{
157+
isTrue("SM9 stream-mode MAC rejection message", "SM9 MAC check failed".equals(e.getMessage()));
158+
}
159+
byte[] stream32 = encrypt(SM9Engine.Mode.STREAM, bobPublic, message(32));
160+
try
161+
{
162+
decrypt(SM9Engine.Mode.SM4, bobKey, stream32);
163+
fail("SM9 SM4-mode engine decrypted a 32-byte stream-mode ciphertext");
164+
}
165+
catch (InvalidCipherTextException e)
166+
{
167+
isTrue("SM9 SM4-mode MAC rejection message", "SM9 MAC check failed".equals(e.getMessage()));
168+
}
169+
}
170+
171+
private static byte[] message(int length)
172+
{
173+
byte[] message = new byte[length];
174+
for (int i = 0; i != length; i++)
175+
{
176+
message[i] = (byte)(i + 1);
177+
}
178+
return message;
179+
}
180+
181+
private static byte[] encrypt(SM9Engine.Mode mode, SM9EncPublicKeyParameters recipient, byte[] message)
182+
throws InvalidCipherTextException
183+
{
184+
SM9Engine engine = new SM9Engine(mode);
185+
engine.init(true, new ParametersWithRandom(recipient, CryptoServicesRegistrar.getSecureRandom()));
186+
return engine.processBlock(message, 0, message.length);
187+
}
188+
189+
private static byte[] decrypt(SM9Engine.Mode mode, SM9EncPrivateKeyParameters userKey, byte[] ciphertext)
190+
throws InvalidCipherTextException
191+
{
192+
SM9Engine engine = new SM9Engine(mode);
193+
engine.init(false, userKey);
194+
return engine.processBlock(ciphertext, 0, ciphertext.length);
195+
}
196+
197+
public static void main(String[] args)
198+
{
199+
runTest(new SM9EngineTest());
200+
}
201+
}

0 commit comments

Comments
 (0)