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