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