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