• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 // Copyright 2020 Google LLC
2 //
3 // Licensed under the Apache License, Version 2.0 (the "License");
4 // you may not use this file except in compliance with the License.
5 // You may obtain a copy of the License at
6 //
7 //     https://www.apache.org/licenses/LICENSE-2.0
8 //
9 // Unless required by applicable law or agreed to in writing, software
10 // distributed under the License is distributed on an "AS IS" BASIS,
11 // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 // See the License for the specific language governing permissions and
13 // limitations under the License.
14 
15 package com.google.security.cryptauth.lib.securemessage;
16 
17 import com.google.common.io.BaseEncoding;
18 import com.google.protobuf.ByteString;
19 import com.google.security.annotations.SuppressInsecureCipherModeCheckerNoReview;
20 import com.google.security.cryptauth.lib.securemessage.SecureMessageProto.DhPublicKey;
21 import com.google.security.cryptauth.lib.securemessage.SecureMessageProto.EcP256PublicKey;
22 import com.google.security.cryptauth.lib.securemessage.SecureMessageProto.GenericPublicKey;
23 import com.google.security.cryptauth.lib.securemessage.SecureMessageProto.SimpleRsaPublicKey;
24 import java.math.BigInteger;
25 import java.security.KeyFactory;
26 import java.security.KeyPair;
27 import java.security.KeyPairGenerator;
28 import java.security.NoSuchAlgorithmException;
29 import java.security.PrivateKey;
30 import java.security.PublicKey;
31 import java.security.interfaces.ECPublicKey;
32 import java.security.spec.ECPoint;
33 import java.security.spec.ECPublicKeySpec;
34 import java.security.spec.InvalidKeySpecException;
35 import java.util.Arrays;
36 import javax.crypto.KeyAgreement;
37 import javax.crypto.interfaces.DHPrivateKey;
38 import javax.crypto.interfaces.DHPublicKey;
39 import junit.framework.TestCase;
40 
41 /** Tests for the PublicKeyProtoUtil class. */
42 public class PublicKeyProtoUtilTest extends TestCase {
43 
44   private static final byte[] ZERO_BYTE = {0};
45   private PublicKey ecPublicKey;
46   private PublicKey rsaPublicKey;
47 
48   /**
49    * Diffie Hellman {@link PublicKey}s require special treatment, so we store them specifically as a
50    * {@link DHPublicKey} to minimize casting.
51    */
52   private DHPublicKey dhPublicKey;
53 
54   @Override
setUp()55   public void setUp() {
56     if (!isAndroidOsWithoutEcSupport()) {
57       ecPublicKey = PublicKeyProtoUtil.generateEcP256KeyPair().getPublic();
58     }
59     rsaPublicKey = PublicKeyProtoUtil.generateRSA2048KeyPair().getPublic();
60     dhPublicKey = (DHPublicKey) PublicKeyProtoUtil.generateDh2048KeyPair().getPublic();
61   }
62 
testPublicKeyProtoSpecificEncodeParse()63   public void testPublicKeyProtoSpecificEncodeParse() throws Exception {
64     if (!isAndroidOsWithoutEcSupport()) {
65       assertEquals(
66           ecPublicKey,
67           PublicKeyProtoUtil.parseEcPublicKey(PublicKeyProtoUtil.encodeEcPublicKey(ecPublicKey)));
68     }
69 
70     assertEquals(
71         rsaPublicKey,
72         PublicKeyProtoUtil.parseRsa2048PublicKey(
73             PublicKeyProtoUtil.encodeRsa2048PublicKey(rsaPublicKey)));
74 
75     // DHPublicKey objects don't seem to properly implement equals(), so we have to test that
76     // the individual y and p values match (it is safe to assume g = 2 is used if p is correct).
77     DHPublicKey parsedDHPublicKey =
78         PublicKeyProtoUtil.parseDh2048PublicKey(
79             PublicKeyProtoUtil.encodeDh2048PublicKey(dhPublicKey));
80     assertEquals(dhPublicKey.getY(), parsedDHPublicKey.getY());
81     assertEquals(dhPublicKey.getParams().getP(), parsedDHPublicKey.getParams().getP());
82     assertEquals(dhPublicKey.getParams().getG(), parsedDHPublicKey.getParams().getG());
83   }
84 
testPublicKeyProtoGenericEncodeParse()85   public void testPublicKeyProtoGenericEncodeParse() throws Exception {
86     if (!isAndroidOsWithoutEcSupport()) {
87       assertEquals(
88           ecPublicKey,
89           PublicKeyProtoUtil.parsePublicKey(
90               PublicKeyProtoUtil.encodePaddedEcPublicKey(ecPublicKey)));
91       assertEquals(
92           ecPublicKey,
93           PublicKeyProtoUtil.parsePublicKey(PublicKeyProtoUtil.encodePublicKey(ecPublicKey)));
94     }
95 
96     assertEquals(
97         rsaPublicKey,
98         PublicKeyProtoUtil.parsePublicKey(PublicKeyProtoUtil.encodePublicKey(rsaPublicKey)));
99 
100     // See above explanation for why we treat DHPublicKey objects differently.
101     DHPublicKey parsedDHPublicKey =
102         PublicKeyProtoUtil.parseDh2048PublicKey(
103             PublicKeyProtoUtil.encodeDh2048PublicKey(dhPublicKey));
104     assertEquals(dhPublicKey.getY(), parsedDHPublicKey.getY());
105     assertEquals(dhPublicKey.getParams().getP(), parsedDHPublicKey.getParams().getP());
106     assertEquals(dhPublicKey.getParams().getG(), parsedDHPublicKey.getParams().getG());
107   }
108 
testPaddedECPublicKeyEncodeHasPaddedNullByte()109   public void testPaddedECPublicKeyEncodeHasPaddedNullByte() throws Exception {
110     if (isAndroidOsWithoutEcSupport()) {
111       return;
112     }
113 
114     // Key where the x coordinate is 33 bytes, y coordinate is 32 bytes
115     ECPublicKey maxXByteLengthKey =
116         buildEcPublicKey(
117             BaseEncoding.base64().decode("AM730WQL7ZAmvyAJX4euNdr3+nAIueGlYYGXE6p732h6"),
118             BaseEncoding.base64().decode("JEnmaDpKn0fH4/0kKGb97qUSwI2uT+ta0GLe3V7REfk="));
119     // Key where both coordinates are 33 bytes
120     ECPublicKey maxByteLengthKey =
121         buildEcPublicKey(
122             BaseEncoding.base64().decode("AOg9TQCxFfVdXv7lO/6UVDyiPsu8XDkEWQIPUfqX6UHP"),
123             BaseEncoding.base64().decode("AP/RW8uVyu6QImpbza51CqG1mtBTh5c9pjv9CUwOuB7E"));
124     // Key where both coordinates are 32 bytes
125     ECPublicKey notMaxByteLengthKey =
126         buildEcPublicKey(
127             BaseEncoding.base64().decode("M35bxV8HKr0e8v7f4zuXgw6TYFawvikFdI71u9S1ONI="),
128             BaseEncoding.base64().decode("OXR+xCpD8AR0VR8TeBXA00eIr3rWE6sV6KrOM6MoWsc="));
129     GenericPublicKey encodedMaxXByteLengthKey =
130         PublicKeyProtoUtil.encodePublicKey(maxXByteLengthKey);
131     GenericPublicKey paddedEncodedMaxXByteLengthKey =
132         PublicKeyProtoUtil.encodePaddedEcPublicKey(maxXByteLengthKey);
133     GenericPublicKey encodedMaxByteLengthKey = PublicKeyProtoUtil.encodePublicKey(maxByteLengthKey);
134     GenericPublicKey paddedEncodedMaxByteLengthKey =
135         PublicKeyProtoUtil.encodePaddedEcPublicKey(maxByteLengthKey);
136     GenericPublicKey encodedNotMaxByteLengthKey =
137         PublicKeyProtoUtil.encodePublicKey(notMaxByteLengthKey);
138     GenericPublicKey paddedEncodedNotMaxByteLengthKey =
139         PublicKeyProtoUtil.encodePaddedEcPublicKey(notMaxByteLengthKey);
140 
141     assertEquals(maxXByteLengthKey, PublicKeyProtoUtil.parsePublicKey(encodedMaxXByteLengthKey));
142     assertEquals(
143         maxXByteLengthKey, PublicKeyProtoUtil.parsePublicKey(paddedEncodedMaxXByteLengthKey));
144     assertEquals(maxByteLengthKey, PublicKeyProtoUtil.parsePublicKey(encodedMaxByteLengthKey));
145     assertEquals(
146         maxByteLengthKey, PublicKeyProtoUtil.parsePublicKey(paddedEncodedMaxByteLengthKey));
147     assertEquals(
148         notMaxByteLengthKey, PublicKeyProtoUtil.parsePublicKey(paddedEncodedNotMaxByteLengthKey));
149     assertEquals(
150         notMaxByteLengthKey, PublicKeyProtoUtil.parsePublicKey(encodedNotMaxByteLengthKey));
151 
152     assertEquals(33, paddedEncodedMaxXByteLengthKey.getEcP256PublicKey().getX().size());
153     assertEquals(33, paddedEncodedMaxXByteLengthKey.getEcP256PublicKey().getY().size());
154     assertEquals(0, paddedEncodedMaxXByteLengthKey.getEcP256PublicKey().getX().byteAt(0));
155     assertEquals(0, paddedEncodedMaxXByteLengthKey.getEcP256PublicKey().getY().byteAt(0));
156     assertEquals(33, encodedMaxXByteLengthKey.getEcP256PublicKey().getX().size());
157     assertEquals(32, encodedMaxXByteLengthKey.getEcP256PublicKey().getY().size());
158 
159     assertEquals(33, paddedEncodedMaxByteLengthKey.getEcP256PublicKey().getX().size());
160     assertEquals(33, paddedEncodedMaxByteLengthKey.getEcP256PublicKey().getY().size());
161     assertEquals(0, paddedEncodedMaxByteLengthKey.getEcP256PublicKey().getX().byteAt(0));
162     assertEquals(0, paddedEncodedMaxByteLengthKey.getEcP256PublicKey().getY().byteAt(0));
163     assertEquals(33, encodedMaxByteLengthKey.getEcP256PublicKey().getX().size());
164     assertEquals(33, encodedMaxByteLengthKey.getEcP256PublicKey().getY().size());
165 
166     assertEquals(32, encodedNotMaxByteLengthKey.getEcP256PublicKey().getX().size());
167     assertEquals(32, encodedNotMaxByteLengthKey.getEcP256PublicKey().getY().size());
168     assertEquals(0, paddedEncodedNotMaxByteLengthKey.getEcP256PublicKey().getX().byteAt(0));
169     assertEquals(0, paddedEncodedNotMaxByteLengthKey.getEcP256PublicKey().getY().byteAt(0));
170     assertEquals(33, paddedEncodedNotMaxByteLengthKey.getEcP256PublicKey().getX().size());
171     assertEquals(33, paddedEncodedNotMaxByteLengthKey.getEcP256PublicKey().getY().size());
172   }
173 
174   @SuppressInsecureCipherModeCheckerNoReview
testWrongPublicKeyType()175   public void testWrongPublicKeyType() throws Exception {
176     KeyPairGenerator dsaGen = KeyPairGenerator.getInstance("DSA");
177     dsaGen.initialize(512);
178     PublicKey pk = dsaGen.generateKeyPair().getPublic();
179 
180     if (!isAndroidOsWithoutEcSupport()) {
181       // Try to encode it as EC
182       try {
183         PublicKeyProtoUtil.encodeEcPublicKey(pk);
184         fail();
185       } catch (IllegalArgumentException expected) {
186       }
187 
188       try {
189         PublicKeyProtoUtil.encodePaddedEcPublicKey(pk);
190         fail();
191       } catch (IllegalArgumentException expected) {
192       }
193     }
194 
195     // Try to encode it as RSA
196     try {
197       PublicKeyProtoUtil.encodeRsa2048PublicKey(pk);
198       fail();
199     } catch (IllegalArgumentException expected) {
200     }
201 
202     // Try to encode it as DH
203     try {
204       PublicKeyProtoUtil.encodeDh2048PublicKey(pk);
205       fail();
206     } catch (IllegalArgumentException expected) {
207     }
208 
209     // Try to encode it as Generic
210     try {
211       PublicKeyProtoUtil.encodePublicKey(pk);
212       fail();
213     } catch (IllegalArgumentException expected) {
214     }
215   }
216 
testEcPublicKeyProtoInvalidEncoding()217   public void testEcPublicKeyProtoInvalidEncoding() throws Exception {
218     if (isAndroidOsWithoutEcSupport()) {
219       return;
220     }
221 
222     EcP256PublicKey validProto = PublicKeyProtoUtil.encodeEcPublicKey(ecPublicKey);
223     EcP256PublicKey.Builder invalidProto = EcP256PublicKey.newBuilder(validProto);
224 
225     // Mess up the X coordinate by repeating it twice
226     byte[] newX =
227         CryptoOps.concat(validProto.getX().toByteArray(), validProto.getX().toByteArray());
228     checkParsingFailsFor(invalidProto.setX(ByteString.copyFrom(newX)).build());
229 
230     // Mess up the Y coordinate by erasing it
231     invalidProto = EcP256PublicKey.newBuilder(validProto);
232     checkParsingFailsFor(invalidProto.setY(ByteString.EMPTY).build());
233 
234     // Pick a point that is likely not on the curve by copying X over Y
235     invalidProto = EcP256PublicKey.newBuilder(validProto);
236     checkParsingFailsFor(invalidProto.setY(validProto.getX()).build());
237 
238     // Try the point (0, 0)
239     invalidProto = EcP256PublicKey.newBuilder(validProto);
240     checkParsingFailsFor(
241         invalidProto
242             .setX(ByteString.copyFrom(ZERO_BYTE))
243             .setY(ByteString.copyFrom(ZERO_BYTE))
244             .build());
245   }
246 
checkParsingFailsFor(EcP256PublicKey invalid)247   private void checkParsingFailsFor(EcP256PublicKey invalid) {
248     try {
249       // Should fail to decode
250       PublicKeyProtoUtil.parseEcPublicKey(invalid);
251       fail();
252     } catch (InvalidKeySpecException expected) {
253     }
254   }
255 
testSimpleRsaPublicKeyProtoInvalidEncoding()256   public void testSimpleRsaPublicKeyProtoInvalidEncoding() throws Exception {
257     SimpleRsaPublicKey validProto = PublicKeyProtoUtil.encodeRsa2048PublicKey(rsaPublicKey);
258     SimpleRsaPublicKey.Builder invalidProto;
259 
260     // Double the number of bits in the modulus
261     invalidProto = SimpleRsaPublicKey.newBuilder(validProto);
262     byte[] newN =
263         CryptoOps.concat(validProto.getN().toByteArray(), validProto.getN().toByteArray());
264     checkParsingFailsFor(invalidProto.setN(ByteString.copyFrom(newN)).build());
265 
266     // Set the modulus to 0
267     invalidProto = SimpleRsaPublicKey.newBuilder(validProto);
268     checkParsingFailsFor(invalidProto.setN(ByteString.copyFrom(ZERO_BYTE)).build());
269 
270     // Set the modulus to 65537 (way too small)
271     invalidProto = SimpleRsaPublicKey.newBuilder(validProto);
272     checkParsingFailsFor(
273         invalidProto.setN(ByteString.copyFrom(BigInteger.valueOf(65537).toByteArray())).build());
274   }
275 
checkParsingFailsFor(SimpleRsaPublicKey invalid)276   private static void checkParsingFailsFor(SimpleRsaPublicKey invalid) {
277     try {
278       // Should fail to decode
279       PublicKeyProtoUtil.parseRsa2048PublicKey(invalid);
280       fail();
281     } catch (InvalidKeySpecException expected) {
282     }
283   }
284 
testSimpleDhPublicKeyProtoInvalidEncoding()285   public void testSimpleDhPublicKeyProtoInvalidEncoding() throws Exception {
286     DhPublicKey validProto = PublicKeyProtoUtil.encodeDh2048PublicKey(dhPublicKey);
287     DhPublicKey.Builder invalidProto;
288 
289     // Double the number of bits in the public element encoding
290     invalidProto = DhPublicKey.newBuilder(validProto);
291     byte[] newY =
292         CryptoOps.concat(validProto.getY().toByteArray(), validProto.getY().toByteArray());
293     checkParsingFailsFor(invalidProto.setY(ByteString.copyFrom(newY)).build());
294 
295     // Set the public element to 0
296     invalidProto = DhPublicKey.newBuilder(validProto);
297     checkParsingFailsFor(invalidProto.setY(ByteString.copyFrom(ZERO_BYTE)).build());
298   }
299 
checkParsingFailsFor(DhPublicKey invalid)300   private static void checkParsingFailsFor(DhPublicKey invalid) {
301     try {
302       // Should fail to decode
303       PublicKeyProtoUtil.parseDh2048PublicKey(invalid);
304       fail();
305     } catch (InvalidKeySpecException expected) {
306     }
307   }
308 
testDhKeyAgreementWorks()309   public void testDhKeyAgreementWorks() throws Exception {
310     int minExpectedSecretLength = (PublicKeyProtoUtil.DH_P.bitLength() / 8) - 4;
311 
312     KeyPair clientKeyPair = PublicKeyProtoUtil.generateDh2048KeyPair();
313     KeyPair serverKeyPair = PublicKeyProtoUtil.generateDh2048KeyPair();
314     BigInteger clientY = ((DHPublicKey) clientKeyPair.getPublic()).getY();
315     BigInteger serverY = ((DHPublicKey) serverKeyPair.getPublic()).getY();
316     assertFalse(clientY.equals(serverY)); // DHPublicKeys should not be equal
317 
318     // Run client side of the key exchange
319     byte[] clientSecret = doDhAgreement(clientKeyPair.getPrivate(), serverKeyPair.getPublic());
320     assert (clientSecret.length >= minExpectedSecretLength);
321 
322     // Run the server side of the key exchange
323     byte[] serverSecret = doDhAgreement(serverKeyPair.getPrivate(), clientKeyPair.getPublic());
324     assert (serverSecret.length >= minExpectedSecretLength);
325 
326     assertTrue(Arrays.equals(clientSecret, serverSecret));
327   }
328 
testDh2048PrivateKeyEncoding()329   public void testDh2048PrivateKeyEncoding() throws Exception {
330     KeyPair testPair = PublicKeyProtoUtil.generateDh2048KeyPair();
331     DHPrivateKey sk = (DHPrivateKey) testPair.getPrivate();
332     DHPrivateKey skParsed =
333         PublicKeyProtoUtil.parseDh2048PrivateKey(PublicKeyProtoUtil.encodeDh2048PrivateKey(sk));
334     assertEquals(sk.getX(), skParsed.getX());
335     assertEquals(sk.getParams().getP(), skParsed.getParams().getP());
336     assertEquals(sk.getParams().getG(), skParsed.getParams().getG());
337   }
338 
testParseEcPublicKeyOnLegacyPlatform()339   public void testParseEcPublicKeyOnLegacyPlatform() {
340     if (!PublicKeyProtoUtil.isLegacyCryptoRequired()) {
341       return; // This test only runs on legacy platforms
342     }
343     byte[] pointBytes = {
344       1, 2, 3, 4, 5, 6, 7, 8, 9, 0,
345       1, 2, 3, 4, 5, 6, 7, 8, 9, 0,
346       1, 2, 3, 4, 5, 6, 7, 8, 9, 0,
347       1, 2
348     };
349 
350     try {
351       PublicKeyProtoUtil.parseEcPublicKey(
352           EcP256PublicKey.newBuilder()
353               .setX(ByteString.copyFrom(pointBytes))
354               .setY(ByteString.copyFrom(pointBytes))
355               .build());
356       fail();
357     } catch (InvalidKeySpecException expected) {
358       // Should get this specific exception when EC doesn't work
359     }
360   }
361 
testIsLegacyCryptoRequired()362   public void testIsLegacyCryptoRequired() {
363     assertEquals(isAndroidOsWithoutEcSupport(), PublicKeyProtoUtil.isLegacyCryptoRequired());
364   }
365 
366   /** @return true if running on an Android OS that doesn't support Elliptic Curve algorithms */
isAndroidOsWithoutEcSupport()367   public static boolean isAndroidOsWithoutEcSupport() {
368     try {
369       Class<?> clazz = ClassLoader.getSystemClassLoader().loadClass("android.os.Build$VERSION");
370       int sdkVersion = clazz.getField("SDK_INT").getInt(null);
371       if (sdkVersion < PublicKeyProtoUtil.ANDROID_HONEYCOMB_SDK_INT) {
372         return true;
373       }
374     } catch (ClassNotFoundException e) {
375       // Not running on Android
376       return false;
377     } catch (SecurityException e) {
378       throw new AssertionError(e);
379     } catch (NoSuchFieldException e) {
380       throw new AssertionError(e);
381     } catch (IllegalArgumentException e) {
382       throw new AssertionError(e);
383     } catch (IllegalAccessException e) {
384       throw new AssertionError(e);
385     }
386     return false;
387   }
388 
389   @SuppressInsecureCipherModeCheckerNoReview
doDhAgreement(PrivateKey secretKey, PublicKey peerKey)390   private static byte[] doDhAgreement(PrivateKey secretKey, PublicKey peerKey) throws Exception {
391     KeyAgreement agreement = KeyAgreement.getInstance("DH");
392     agreement.init(secretKey);
393     agreement.doPhase(peerKey, true);
394     return agreement.generateSecret();
395   }
396 
buildEcPublicKey(byte[] encodedX, byte[] encodedY)397   private static ECPublicKey buildEcPublicKey(byte[] encodedX, byte[] encodedY) throws Exception {
398     try {
399       BigInteger wX = new BigInteger(encodedX);
400       BigInteger wY = new BigInteger(encodedY);
401       return (ECPublicKey)
402           KeyFactory.getInstance("EC")
403               .generatePublic(
404                   new ECPublicKeySpec(
405                       new ECPoint(wX, wY),
406                       ((ECPublicKey) PublicKeyProtoUtil.generateEcP256KeyPair().getPublic())
407                           .getParams()));
408     } catch (NoSuchAlgorithmException e) {
409       throw new RuntimeException(e);
410     }
411   }
412 }
413