• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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