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