1 /** 2 * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except 3 * in compliance with the License. You may obtain a copy of the License at 4 * 5 * <p>http://www.apache.org/licenses/LICENSE-2.0 6 * 7 * <p>Unless required by applicable law or agreed to in writing, software distributed under the 8 * License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either 9 * express or implied. See the License for the specific language governing permissions and 10 * limitations under the License. 11 */ 12 package com.google.security.wycheproof; 13 14 import static org.junit.Assert.assertEquals; 15 16 import com.google.gson.JsonElement; 17 import com.google.gson.JsonObject; 18 import java.security.GeneralSecurityException; 19 import java.security.InvalidAlgorithmParameterException; 20 import java.security.NoSuchAlgorithmException; 21 import java.util.Arrays; 22 import java.util.Locale; 23 import javax.crypto.Mac; 24 import javax.crypto.spec.IvParameterSpec; 25 import javax.crypto.spec.SecretKeySpec; 26 import org.junit.Test; 27 import org.junit.runner.RunWith; 28 import org.junit.runners.JUnit4; 29 30 /** This test uses test vectors in JSON format to test MAC primitives. */ 31 @RunWith(JUnit4.class) 32 public class JsonMacTest { 33 34 /** Convenience method to get a byte array from an JsonObject */ getBytes(JsonObject obj, String name)35 protected static byte[] getBytes(JsonObject obj, String name) throws Exception { 36 return JsonUtil.asByteArray(obj.get(name)); 37 } 38 arrayEquals(byte[] a, byte[] b)39 protected static boolean arrayEquals(byte[] a, byte[] b) { 40 if (a.length != b.length) { 41 return false; 42 } 43 byte res = 0; 44 for (int i = 0; i < a.length; i++) { 45 res |= (byte) (a[i] ^ b[i]); 46 } 47 return res == 0; 48 } 49 50 /** 51 * Computes a MAC. 52 * 53 * @param algorithm the algorithm. 54 * @param key the key bytes 55 * @param msg the message to MAC. 56 * @param tagSize the expected size of the tag in bits. 57 * @return the tag 58 * @throws GeneralSecurityException if the algorithm or the parameter sizes are not supported or 59 * if the initialization failed. For example one case are GMACs with a tag size othe than 128 60 * bits, since the JCE interface does not seem to support such a specification. 61 */ computeMac(String algorithm, byte[] key, byte[] msg, int tagSize)62 protected static byte[] computeMac(String algorithm, byte[] key, byte[] msg, int tagSize) 63 throws GeneralSecurityException { 64 Mac mac = Mac.getInstance(algorithm); 65 algorithm = algorithm.toUpperCase(Locale.ENGLISH); 66 if (algorithm.startsWith("HMAC")) { 67 SecretKeySpec keySpec = new SecretKeySpec(key, algorithm); 68 // TODO(bleichen): Is there a provider independent truncation? 69 // The class javax.xml.crypto.dsig.spec.HMACParameterSpec would allow to 70 // truncate HMAC tags as follows: 71 // <pre> 72 // HMACParameterSpec params = new HMACParameterSpec(tagSize); 73 // mac.init(keySpec, params); 74 // mac.update(msg); 75 // return mac.doFinal(); 76 // </pre> 77 // But this class is often not supported. Hence the computation here, just computes a 78 // full length tag and truncates it. The drawback of having to truncate tags is that 79 // the caller has to compare truncated tags during verification. 80 mac.init(keySpec); 81 mac.update(msg); 82 byte[] tag = mac.doFinal(); 83 return Arrays.copyOf(tag, tagSize / 8); 84 } else { 85 throw new NoSuchAlgorithmException(algorithm); 86 } 87 } 88 89 /** 90 * Tests a randomized MAC (i.e. a message authetication that takes an additional IV as parameter) 91 * against test vectors. 92 * 93 * @param filename the JSON file with the test vectors. 94 */ testMac(String filename)95 public void testMac(String filename) throws Exception { 96 // Checking preconditions. 97 JsonObject test = JsonUtil.getTestVectors(filename); 98 String algorithm = test.get("algorithm").getAsString(); 99 try { 100 Mac.getInstance(algorithm); 101 } catch (NoSuchAlgorithmException ex) { 102 System.out.println("Algorithm is not supported. Skipping test for " + algorithm); 103 return; 104 } 105 106 int numTests = test.get("numberOfTests").getAsInt(); 107 int cntTests = 0; 108 int passedTests = 0; 109 int errors = 0; 110 for (JsonElement g : test.getAsJsonArray("testGroups")) { 111 JsonObject group = g.getAsJsonObject(); 112 int tagSize = group.get("tagSize").getAsInt(); 113 for (JsonElement t : group.getAsJsonArray("tests")) { 114 cntTests++; 115 JsonObject testcase = t.getAsJsonObject(); 116 int tcid = testcase.get("tcId").getAsInt(); 117 String tc = "tcId: " + tcid + " " + testcase.get("comment").getAsString(); 118 byte[] key = getBytes(testcase, "key"); 119 byte[] msg = getBytes(testcase, "msg"); 120 byte[] expectedTag = getBytes(testcase, "tag"); 121 // Result is one of "valid", "invalid", "acceptable". 122 // "valid" are test vectors with matching plaintext, ciphertext and tag. 123 // "invalid" are test vectors with invalid parameters or invalid ciphertext and tag. 124 // "acceptable" are test vectors with weak parameters or legacy formats. 125 String result = testcase.get("result").getAsString(); 126 127 byte[] computedTag = null; 128 try { 129 computedTag = computeMac(algorithm, key, msg, tagSize); 130 } catch (GeneralSecurityException ex) { 131 // Some libraries restrict key size or tag size. Hence valid MACs might be 132 // rejected. 133 continue; 134 } catch (IllegalArgumentException ex) { 135 // Thrown by javax.crypto.spec.SecretKeySpec (e.g. when the key is empty). 136 continue; 137 } 138 139 boolean eq = arrayEquals(expectedTag, computedTag); 140 if (result.equals("invalid")) { 141 if (eq) { 142 // Some test vectors use invalid parameters that should be rejected. 143 // E.g. an implementation must not allow AES-GMAC with an IV of length 0, 144 // since this leaks the authentication key. 145 System.out.println("Computed mac for test case " + tc); 146 errors++; 147 } 148 } else { 149 if (eq) { 150 passedTests++; 151 } else { 152 System.out.println( 153 "Incorrect tag for " 154 + tc 155 + " expected:" 156 + TestUtil.bytesToHex(expectedTag) 157 + " computed:" 158 + TestUtil.bytesToHex(computedTag)); 159 errors++; 160 } 161 } 162 } 163 } 164 System.out.println("passed Tests for " + algorithm + ":" + passedTests); 165 assertEquals(0, errors); 166 assertEquals(numTests, cntTests); 167 } 168 169 /** 170 * Returns an initialized instance of a randomized MAC. 171 * 172 * @param algorithm the algorithm. 173 * @param key the key bytes 174 * @param iv the bytes of the initialization vector 175 * @param tagSize the expected size of the tag in bits. 176 * @return an initialized instance of a MAC. 177 * @throws GeneralSecurityException if the algorithm or the parameter sizes are not supported or 178 * if the initialization failed. For example one case are GMACs with a tag size othe than 128 179 * bits, since the JCE interface does not seem to support such a specification. 180 */ getInitializedMacWithIv(String algorithm, byte[] key, byte[] iv, int tagSize)181 protected static Mac getInitializedMacWithIv(String algorithm, byte[] key, byte[] iv, int tagSize) 182 throws GeneralSecurityException { 183 Mac mac = Mac.getInstance(algorithm); 184 algorithm = algorithm.toUpperCase(Locale.ENGLISH); 185 if (algorithm.equals("AES-GMAC")) { 186 SecretKeySpec keySpec = new SecretKeySpec(key, "AES"); 187 if (tagSize != 128) { 188 throw new InvalidAlgorithmParameterException("only 128-bit tag is supported"); 189 } 190 IvParameterSpec params = new IvParameterSpec(iv); 191 // TODO(bleichen): I'm unaware of a method that allows to specify the tag size in JCE. 192 // E.g. the following parameter specification does not work (at least not in BC): 193 // GCMParameterSpec params = new GCMParameterSpec(tagSize, iv); 194 mac.init(keySpec, params); 195 return mac; 196 } else { 197 throw new NoSuchAlgorithmException(algorithm); 198 } 199 } 200 201 /** 202 * Tests a randomized MAC (i.e. a message authetication that takes an additional IV as 203 * parameter) against test vectors. 204 * 205 * @param filename the JSON file with the test vectors. 206 * @param algorithm the JCE name of the algorithm to test. 207 */ testMacWithIv(String filename, String algorithm)208 public void testMacWithIv(String filename, String algorithm) throws Exception { 209 // Checking preconditions. 210 try { 211 Mac.getInstance(algorithm); 212 } catch (NoSuchAlgorithmException ex) { 213 System.out.println("Algorithm is not supported. Skipping test for " + algorithm); 214 return; 215 } 216 217 JsonObject test = JsonUtil.getTestVectors(filename); 218 int numTests = test.get("numberOfTests").getAsInt(); 219 int cntTests = 0; 220 int passedTests = 0; 221 int errors = 0; 222 for (JsonElement g : test.getAsJsonArray("testGroups")) { 223 JsonObject group = g.getAsJsonObject(); 224 int tagSize = group.get("tagSize").getAsInt(); 225 for (JsonElement t : group.getAsJsonArray("tests")) { 226 cntTests++; 227 JsonObject testcase = t.getAsJsonObject(); 228 int tcid = testcase.get("tcId").getAsInt(); 229 String tc = "tcId: " + tcid + " " + testcase.get("comment").getAsString(); 230 byte[] key = getBytes(testcase, "key"); 231 byte[] iv = getBytes(testcase, "iv"); 232 byte[] msg = getBytes(testcase, "msg"); 233 byte[] expectedTag = getBytes(testcase, "tag"); 234 // Result is one of "valid", "invalid", "acceptable". 235 // "valid" are test vectors with matching plaintext, ciphertext and tag. 236 // "invalid" are test vectors with invalid parameters or invalid ciphertext and tag. 237 // "acceptable" are test vectors with weak parameters or legacy formats. 238 String result = testcase.get("result").getAsString(); 239 240 Mac mac; 241 try { 242 mac = getInitializedMacWithIv(algorithm, key, iv, tagSize); 243 } catch (GeneralSecurityException ex) { 244 // Some libraries restrict key size, iv size and tag size. 245 // Because of the initialization of the Mac might fail. 246 continue; 247 } catch (IllegalArgumentException ex) { 248 // Thrown by javax.crypto.spec.SecretKeySpec (e.g. when the key is empty). 249 continue; 250 } 251 252 byte[] computedTag = mac.doFinal(msg); 253 boolean eq = arrayEquals(expectedTag, computedTag); 254 if (result.equals("invalid")) { 255 if (eq) { 256 // Some test vectors use invalid parameters that should be rejected. 257 // E.g. an implementation must not allow AES-GMAC with an IV of length 0, 258 // since this leaks the authentication key. 259 System.out.println("Computed mac for test case " + tc); 260 errors++; 261 } 262 } else { 263 if (eq) { 264 passedTests++; 265 } else { 266 System.out.println( 267 "Incorrect tag for " 268 + tc 269 + " expected:" 270 + TestUtil.bytesToHex(expectedTag) 271 + " computed:" 272 + TestUtil.bytesToHex(computedTag)); 273 errors++; 274 } 275 } 276 } 277 } 278 System.out.println("passed Tests for " + algorithm + ":" + passedTests); 279 assertEquals(0, errors); 280 assertEquals(numTests, cntTests); 281 } 282 283 @Test testHmacSha1()284 public void testHmacSha1() throws Exception { 285 testMac("hmac_sha1_test.json"); 286 } 287 288 @Test testHmacSha224()289 public void testHmacSha224() throws Exception { 290 testMac("hmac_sha224_test.json"); 291 } 292 293 @Test testHmacSha256()294 public void testHmacSha256() throws Exception { 295 testMac("hmac_sha256_test.json"); 296 } 297 298 @Test testHmacSha384()299 public void testHmacSha384() throws Exception { 300 testMac("hmac_sha384_test.json"); 301 } 302 303 @Test testHmacSha512()304 public void testHmacSha512() throws Exception { 305 testMac("hmac_sha512_test.json"); 306 } 307 308 @Test testHmacSha3_224()309 public void testHmacSha3_224() throws Exception { 310 testMac("hmac_sha3_224_test.json"); 311 } 312 313 @Test testHmacSha3_256()314 public void testHmacSha3_256() throws Exception { 315 testMac("hmac_sha3_256_test.json"); 316 } 317 318 @Test testHmacSha3_384()319 public void testHmacSha3_384() throws Exception { 320 testMac("hmac_sha3_384_test.json"); 321 } 322 323 @Test testHmacSha3_512()324 public void testHmacSha3_512() throws Exception { 325 testMac("hmac_sha3_512_test.json"); 326 } 327 328 @Test testAesGmac()329 public void testAesGmac() throws Exception { 330 testMacWithIv("gmac_test.json", "AES-GMAC"); 331 } 332 } 333