OLD | NEW |
(Empty) | |
| 1 // Copyright 2015 The Chromium Authors. All rights reserved. |
| 2 // Use of this source code is governed by a BSD-style license that can be |
| 3 // found in the LICENSE file. |
| 4 |
| 5 package org.chromium.net; |
| 6 |
| 7 import android.test.suitebuilder.annotation.SmallTest; |
| 8 |
| 9 import org.chromium.base.test.util.Feature; |
| 10 import org.chromium.net.test.util.CertTestUtil; |
| 11 |
| 12 import java.io.ByteArrayInputStream; |
| 13 import java.security.cert.CertificateFactory; |
| 14 import java.security.cert.X509Certificate; |
| 15 import java.util.Arrays; |
| 16 import java.util.Calendar; |
| 17 import java.util.Date; |
| 18 import java.util.HashSet; |
| 19 import java.util.Set; |
| 20 |
| 21 /** |
| 22 * Public-Key-Pinning tests of Cronet Java API. |
| 23 */ |
| 24 public class PkpTest extends CronetTestBase { |
| 25 private static final String CERT_USED = "quic_test.example.com.crt"; |
| 26 private static final String[] CERTS_USED = {CERT_USED}; |
| 27 private static final int DISTANT_FUTURE = Integer.MAX_VALUE; |
| 28 private static final boolean INCLUDE_SUBDOMAINS = true; |
| 29 private static final boolean EXCLUDE_SUBDOMAINS = false; |
| 30 |
| 31 private CronetTestFramework mTestFramework; |
| 32 private CronetEngine.Builder mBuilder; |
| 33 private TestUrlRequestCallback mListener; |
| 34 private String mServerUrl; // https://test.example.com:6121 |
| 35 private String mServerHost; // test.example.com |
| 36 private String mDomain; // example.com |
| 37 |
| 38 @Override |
| 39 protected void setUp() throws Exception { |
| 40 super.setUp(); |
| 41 // Start QUIC Test Server |
| 42 System.loadLibrary("cronet_tests"); |
| 43 QuicTestServer.startQuicTestServer(getContext()); |
| 44 mServerUrl = QuicTestServer.getServerURL(); |
| 45 mServerHost = QuicTestServer.getServerHost(); |
| 46 mDomain = mServerHost.substring(mServerHost.indexOf('.') + 1, mServerHos
t.length()); |
| 47 createCronetEngineBuilder(); |
| 48 } |
| 49 |
| 50 @Override |
| 51 protected void tearDown() throws Exception { |
| 52 QuicTestServer.shutdownQuicTestServer(); |
| 53 shutdownCronetEngine(); |
| 54 super.tearDown(); |
| 55 } |
| 56 |
| 57 /** |
| 58 * Tests the case when the pin hash does not match. The client is expected t
o |
| 59 * receive the error response. |
| 60 * |
| 61 * @throws Exception |
| 62 */ |
| 63 @SmallTest |
| 64 @Feature({"Cronet"}) |
| 65 public void testErrorCodeIfPinDoesNotMatch() throws Exception { |
| 66 byte[] nonMatchingHash = generateSomeSha256(); |
| 67 addPkpSha256(mServerHost, nonMatchingHash, EXCLUDE_SUBDOMAINS, DISTANT_F
UTURE); |
| 68 startCronetFramework(); |
| 69 registerHostResolver(); |
| 70 sendRequestAndWaitForResult(); |
| 71 |
| 72 assertErrorResponse(); |
| 73 } |
| 74 |
| 75 /** |
| 76 * Tests the case when the pin hash matches. The client is expected to |
| 77 * receive the successful response with the response code 200. |
| 78 * |
| 79 * @throws Exception |
| 80 */ |
| 81 @SmallTest |
| 82 @Feature({"Cronet"}) |
| 83 public void testSuccessIfPinMatches() throws Exception { |
| 84 // Get PKP hash of the real certificate |
| 85 X509Certificate cert = readCertFromFileInPemFormat(CERT_USED); |
| 86 byte[] matchingHash = CertTestUtil.getPublicKeySha256(cert); |
| 87 |
| 88 addPkpSha256(mServerHost, matchingHash, EXCLUDE_SUBDOMAINS, DISTANT_FUTU
RE); |
| 89 startCronetFramework(); |
| 90 registerHostResolver(); |
| 91 sendRequestAndWaitForResult(); |
| 92 |
| 93 assertSuccessfulResponse(); |
| 94 } |
| 95 |
| 96 /** |
| 97 * Tests the case when the pin hash does not match and the client accesses t
he subdomain of |
| 98 * the configured PKP host with includeSubdomains flag set to true. The clie
nt is |
| 99 * expected to receive the error response. |
| 100 * |
| 101 * @throws Exception |
| 102 */ |
| 103 @SmallTest |
| 104 @Feature({"Cronet"}) |
| 105 public void testIncludeSubdomainsFlagEqualTrue() throws Exception { |
| 106 byte[] nonMatchingHash = generateSomeSha256(); |
| 107 addPkpSha256(mDomain, nonMatchingHash, INCLUDE_SUBDOMAINS, DISTANT_FUTUR
E); |
| 108 startCronetFramework(); |
| 109 registerHostResolver(); |
| 110 sendRequestAndWaitForResult(); |
| 111 |
| 112 assertErrorResponse(); |
| 113 } |
| 114 |
| 115 /** |
| 116 * Tests the case when the pin hash does not match and the client accesses t
he subdomain of |
| 117 * the configured PKP host with includeSubdomains flag set to false. The cli
ent is expected to |
| 118 * receive the successful response with the response code 200. |
| 119 * |
| 120 * @throws Exception |
| 121 */ |
| 122 @SmallTest |
| 123 @Feature({"Cronet"}) |
| 124 public void testIncludeSubdomainsFlagEqualFalse() throws Exception { |
| 125 byte[] nonMatchingHash = generateSomeSha256(); |
| 126 addPkpSha256(mDomain, nonMatchingHash, EXCLUDE_SUBDOMAINS, DISTANT_FUTUR
E); |
| 127 startCronetFramework(); |
| 128 registerHostResolver(); |
| 129 sendRequestAndWaitForResult(); |
| 130 |
| 131 assertSuccessfulResponse(); |
| 132 } |
| 133 |
| 134 /** |
| 135 * Tests the case when the mismatching pin is set for some host that is diff
erent from the one |
| 136 * the client wants to access. In that case the other host pinning policy sh
ould not be applied |
| 137 * and the client is expected to receive the successful response with the re
sponse code 200. |
| 138 * |
| 139 * @throws Exception |
| 140 */ |
| 141 @SmallTest |
| 142 @Feature({"Cronet"}) |
| 143 public void testSuccessIfNoPinSpecified() throws Exception { |
| 144 byte[] nonMatchingHash = generateSomeSha256(); |
| 145 addPkpSha256("otherhost.com", nonMatchingHash, INCLUDE_SUBDOMAINS, DISTA
NT_FUTURE); |
| 146 startCronetFramework(); |
| 147 registerHostResolver(); |
| 148 sendRequestAndWaitForResult(); |
| 149 |
| 150 assertSuccessfulResponse(); |
| 151 } |
| 152 |
| 153 /** |
| 154 * Tests mismatching pins that will expire in 10 seconds. The pins should be
still valid and |
| 155 * enforced during the request; thus returning PIN mismatch error. |
| 156 * |
| 157 * @throws Exception |
| 158 */ |
| 159 @SmallTest |
| 160 @Feature({"Cronet"}) |
| 161 public void testSoonExpiringPin() throws Exception { |
| 162 final int tenSecondsAhead = 10; |
| 163 byte[] nonMatchingHash = generateSomeSha256(); |
| 164 addPkpSha256(mServerHost, nonMatchingHash, EXCLUDE_SUBDOMAINS, tenSecond
sAhead); |
| 165 startCronetFramework(); |
| 166 registerHostResolver(); |
| 167 sendRequestAndWaitForResult(); |
| 168 |
| 169 assertErrorResponse(); |
| 170 } |
| 171 |
| 172 /** |
| 173 * Tests mismatching pins that expired 1 second ago. Since the pins have exp
ired, they |
| 174 * should not be enforced during the request; thus a successful response is
expected. |
| 175 * |
| 176 * @throws Exception |
| 177 */ |
| 178 @SmallTest |
| 179 @Feature({"Cronet"}) |
| 180 public void testRecentlyExpiredPin() throws Exception { |
| 181 final int oneSecondAgo = -1; |
| 182 byte[] nonMatchingHash = generateSomeSha256(); |
| 183 addPkpSha256(mServerHost, nonMatchingHash, EXCLUDE_SUBDOMAINS, oneSecond
Ago); |
| 184 startCronetFramework(); |
| 185 registerHostResolver(); |
| 186 sendRequestAndWaitForResult(); |
| 187 |
| 188 assertSuccessfulResponse(); |
| 189 } |
| 190 |
| 191 /** |
| 192 * Tests that host pinning is not persisted between multiple CronetEngine in
stances. |
| 193 * |
| 194 * @throws Exception |
| 195 */ |
| 196 @SmallTest |
| 197 @Feature({"Cronet"}) |
| 198 public void testPinsAreNotPersisted() throws Exception { |
| 199 byte[] nonMatchingHash = generateSomeSha256(); |
| 200 addPkpSha256(mServerHost, nonMatchingHash, EXCLUDE_SUBDOMAINS, DISTANT_F
UTURE); |
| 201 startCronetFramework(); |
| 202 registerHostResolver(); |
| 203 sendRequestAndWaitForResult(); |
| 204 assertErrorResponse(); |
| 205 shutdownCronetEngine(); |
| 206 |
| 207 // Restart Cronet engine and try the same request again. Since the pins
are not persisted, |
| 208 // a successful response is expected. |
| 209 createCronetEngineBuilder(); |
| 210 startCronetFramework(); |
| 211 registerHostResolver(); |
| 212 sendRequestAndWaitForResult(); |
| 213 assertSuccessfulResponse(); |
| 214 } |
| 215 |
| 216 /** |
| 217 * Tests that the client receives {@code InvalidArgumentException} when the
pinned host name |
| 218 * is invalid. |
| 219 * |
| 220 * @throws Exception |
| 221 */ |
| 222 @SmallTest |
| 223 @Feature({"Cronet"}) |
| 224 public void testHostNameArgumentValidation() throws Exception { |
| 225 final String label63 = "123456789-123456789-123456789-123456789-12345678
9-123456789-123"; |
| 226 final String host255 = label63 + "." + label63 + "." + label63 + "." + l
abel63; |
| 227 // Valid host names. |
| 228 assertNoExceptionWhenHostNameIsValid("domain.com"); |
| 229 assertNoExceptionWhenHostNameIsValid("my-domain.com"); |
| 230 assertNoExceptionWhenHostNameIsValid("section4.domain.info"); |
| 231 assertNoExceptionWhenHostNameIsValid("44.domain44.info"); |
| 232 assertNoExceptionWhenHostNameIsValid("very.long.long.long.long.long.long
.long.domain.com"); |
| 233 assertNoExceptionWhenHostNameIsValid("host"); |
| 234 assertNoExceptionWhenHostNameIsValid("новости.ру"); |
| 235 assertNoExceptionWhenHostNameIsValid("самые-последние.новости.рус"); |
| 236 assertNoExceptionWhenHostNameIsValid("最新消息.中国"); |
| 237 // Checks max size of the host label (63 characters) |
| 238 assertNoExceptionWhenHostNameIsValid(label63 + ".com"); |
| 239 // Checks max size of the host name (255 characters) |
| 240 assertNoExceptionWhenHostNameIsValid(host255); |
| 241 assertNoExceptionWhenHostNameIsValid("127.0.0.z"); |
| 242 |
| 243 // Invalid host names. |
| 244 assertExceptionWhenHostNameIsInvalid("domain.com:300"); |
| 245 assertExceptionWhenHostNameIsInvalid("-domain.com"); |
| 246 assertExceptionWhenHostNameIsInvalid("domain-.com"); |
| 247 assertExceptionWhenHostNameIsInvalid("http://domain.com"); |
| 248 assertExceptionWhenHostNameIsInvalid("domain.com:"); |
| 249 assertExceptionWhenHostNameIsInvalid("domain.com/"); |
| 250 assertExceptionWhenHostNameIsInvalid("новости.ру:"); |
| 251 assertExceptionWhenHostNameIsInvalid("новости.ру/"); |
| 252 assertExceptionWhenHostNameIsInvalid("_http.sctp.www.example.com"); |
| 253 assertExceptionWhenHostNameIsInvalid("http.sctp._www.example.com"); |
| 254 // Checks a host that exceeds max allowed length of the host label (63 c
haracters) |
| 255 assertExceptionWhenHostNameIsInvalid(label63 + "4.com"); |
| 256 // Checks a host that exceeds max allowed length of hostname (255 charac
ters) |
| 257 assertExceptionWhenHostNameIsInvalid(host255.substring(3) + ".com"); |
| 258 assertExceptionWhenHostNameIsInvalid("FE80:0000:0000:0000:0202:B3FF:FE1E
:8329"); |
| 259 assertExceptionWhenHostNameIsInvalid("[2001:db8:0:1]:80"); |
| 260 |
| 261 // Invalid host names for PKP that contain IPv4 addresses |
| 262 // or names with digits and dots only. |
| 263 assertExceptionWhenHostNameIsInvalid("127.0.0.1"); |
| 264 assertExceptionWhenHostNameIsInvalid("68.44.222.12"); |
| 265 assertExceptionWhenHostNameIsInvalid("256.0.0.1"); |
| 266 assertExceptionWhenHostNameIsInvalid("127.0.0.1.1"); |
| 267 assertExceptionWhenHostNameIsInvalid("127.0.0"); |
| 268 assertExceptionWhenHostNameIsInvalid("127.0.0."); |
| 269 assertExceptionWhenHostNameIsInvalid("127.0.0.299"); |
| 270 } |
| 271 |
| 272 /** |
| 273 * Tests that NullPointerException is thrown if the host name or the collect
ion of pins or |
| 274 * the expiration date is null. |
| 275 */ |
| 276 @SmallTest |
| 277 @Feature({"Cronet"}) |
| 278 public void testNullArguments() { |
| 279 verifyExceptionWhenAddPkpArgumentIsNull(true, false, false); |
| 280 verifyExceptionWhenAddPkpArgumentIsNull(false, true, false); |
| 281 verifyExceptionWhenAddPkpArgumentIsNull(false, false, true); |
| 282 verifyExceptionWhenAddPkpArgumentIsNull(false, false, false); |
| 283 } |
| 284 |
| 285 /** |
| 286 * Tests that IllegalArgumentException is thrown if SHA1 is passed as the va
lue of a pin. |
| 287 */ |
| 288 @SmallTest |
| 289 @Feature({"Cronet"}) |
| 290 public void testIllegalArgumentExceptionWhenPinValueIsSHA1() { |
| 291 byte[] sha1 = new byte[20]; |
| 292 try { |
| 293 addPkpSha256(mServerHost, sha1, EXCLUDE_SUBDOMAINS, DISTANT_FUTURE); |
| 294 } catch (IllegalArgumentException ex) { |
| 295 // Expected exception |
| 296 return; |
| 297 } |
| 298 fail("Expected IllegalArgumentException with pin value: " + Arrays.toStr
ing(sha1)); |
| 299 } |
| 300 |
| 301 /** |
| 302 * Asserts that the response from the server contains an PKP error. |
| 303 * TODO(kapishnikov): currently QUIC returns ERR_QUIC_PROTOCOL_ERROR instead
of expected |
| 304 * ERR_SSL_PINNED_KEY_NOT_IN_CERT_CHAIN error code when the pin doesn't matc
h. |
| 305 * This method should be changed when the bug is resolved. See http://crbug.
com/548378 |
| 306 */ |
| 307 private void assertErrorResponse() { |
| 308 assertNotNull("Expected an error", mListener.mError); |
| 309 int errorCode = mListener.mError.netError(); |
| 310 boolean correctErrorCode = errorCode == NetError.ERR_SSL_PINNED_KEY_NOT_
IN_CERT_CHAIN |
| 311 || errorCode == NetError.ERR_QUIC_PROTOCOL_ERROR; |
| 312 assertTrue(String.format("Incorrect error code. Expected %s or %s but re
ceived %s", |
| 313 NetError.ERR_SSL_PINNED_KEY_NOT_IN_CERT_CHAIN, |
| 314 NetError.ERR_QUIC_PROTOCOL_ERROR, errorCode), |
| 315 correctErrorCode); |
| 316 } |
| 317 |
| 318 /** |
| 319 * Asserts a successful response with response code 200. |
| 320 */ |
| 321 private void assertSuccessfulResponse() { |
| 322 if (mListener.mError != null) { |
| 323 fail("Did not expect an error but got error code " + mListener.mErro
r.mNetError); |
| 324 } |
| 325 assertNotNull("Expected non-null response from the server", mListener.mR
esponseInfo); |
| 326 assertEquals(200, mListener.mResponseInfo.getHttpStatusCode()); |
| 327 } |
| 328 |
| 329 private void createCronetEngineBuilder() { |
| 330 // Set common CronetEngine parameters |
| 331 mBuilder = new CronetEngine.Builder(getContext()); |
| 332 mBuilder.enableQUIC(true); |
| 333 mBuilder.addQuicHint(QuicTestServer.getServerHost(), QuicTestServer.getS
erverPort(), |
| 334 QuicTestServer.getServerPort()); |
| 335 mBuilder.setStoragePath(CronetTestFramework.getTestStorage(getContext())
); |
| 336 mBuilder.enableHttpCache(CronetEngine.Builder.HTTP_CACHE_DISK_NO_HTTP, 1
000 * 1024); |
| 337 mBuilder.setMockCertVerifierForTesting(MockCertVerifier.createMockCertVe
rifier(CERTS_USED)); |
| 338 } |
| 339 |
| 340 private void startCronetFramework() { |
| 341 mTestFramework = startCronetTestFrameworkWithUrlAndCronetEngineBuilder(n
ull, mBuilder); |
| 342 } |
| 343 |
| 344 private void shutdownCronetEngine() { |
| 345 if (mTestFramework != null && mTestFramework.mCronetEngine != null) { |
| 346 mTestFramework.mCronetEngine.shutdown(); |
| 347 } |
| 348 } |
| 349 |
| 350 private void registerHostResolver() { |
| 351 long urlRequestContextAdapter = ((CronetUrlRequestContext) mTestFramewor
k.mCronetEngine) |
| 352 .getUrlRequestContextAdapter(); |
| 353 NativeTestServer.registerHostResolverProc(urlRequestContextAdapter, fals
e); |
| 354 } |
| 355 |
| 356 private byte[] generateSomeSha256() { |
| 357 byte[] sha256 = new byte[32]; |
| 358 Arrays.fill(sha256, (byte) 58); |
| 359 return sha256; |
| 360 } |
| 361 |
| 362 private void addPkpSha256( |
| 363 String host, byte[] pinHashValue, boolean includeSubdomain, int maxA
geInSec) { |
| 364 Set<byte[]> hashes = new HashSet<>(); |
| 365 hashes.add(pinHashValue); |
| 366 mBuilder.addPublicKeyPins(host, hashes, includeSubdomain, dateInFuture(m
axAgeInSec)); |
| 367 } |
| 368 |
| 369 private void sendRequestAndWaitForResult() { |
| 370 mListener = new TestUrlRequestCallback(); |
| 371 |
| 372 String quicURL = mServerUrl + "/simple.txt"; |
| 373 UrlRequest.Builder requestBuilder = new UrlRequest.Builder( |
| 374 quicURL, mListener, mListener.getExecutor(), mTestFramework.mCro
netEngine); |
| 375 requestBuilder.build().start(); |
| 376 mListener.blockForDone(); |
| 377 } |
| 378 |
| 379 private X509Certificate readCertFromFileInPemFormat(String certFileName) thr
ows Exception { |
| 380 byte[] certDer = CertTestUtil.pemToDer(CertTestUtil.CERTS_DIRECTORY + ce
rtFileName); |
| 381 CertificateFactory certFactory = CertificateFactory.getInstance("X.509")
; |
| 382 return (X509Certificate) certFactory.generateCertificate(new ByteArrayIn
putStream(certDer)); |
| 383 } |
| 384 |
| 385 private Date dateInFuture(int secondsIntoFuture) { |
| 386 Calendar cal = Calendar.getInstance(); |
| 387 cal.add(Calendar.SECOND, secondsIntoFuture); |
| 388 return cal.getTime(); |
| 389 } |
| 390 |
| 391 private void assertNoExceptionWhenHostNameIsValid(String hostName) { |
| 392 try { |
| 393 addPkpSha256(hostName, generateSomeSha256(), INCLUDE_SUBDOMAINS, DIS
TANT_FUTURE); |
| 394 } catch (IllegalArgumentException ex) { |
| 395 fail("Host name " + hostName + " should be valid but the exception w
as thrown: " |
| 396 + ex.toString()); |
| 397 } |
| 398 } |
| 399 |
| 400 private void assertExceptionWhenHostNameIsInvalid(String hostName) { |
| 401 try { |
| 402 addPkpSha256(hostName, generateSomeSha256(), INCLUDE_SUBDOMAINS, DIS
TANT_FUTURE); |
| 403 } catch (IllegalArgumentException ex) { |
| 404 // Expected exception. |
| 405 return; |
| 406 } |
| 407 fail("Expected IllegalArgumentException when passing " + hostName + " ho
st name"); |
| 408 } |
| 409 |
| 410 private void verifyExceptionWhenAddPkpArgumentIsNull( |
| 411 boolean hostNameIsNull, boolean pinsAreNull, boolean expirationDataI
sNull) { |
| 412 String hostName = hostNameIsNull ? null : "some-host.com"; |
| 413 Set<byte[]> pins = pinsAreNull ? null : new HashSet<byte[]>(); |
| 414 Date expirationDate = expirationDataIsNull ? null : new Date(); |
| 415 |
| 416 boolean shouldThrowNpe = hostNameIsNull || pinsAreNull || expirationData
IsNull; |
| 417 try { |
| 418 mBuilder.addPublicKeyPins(hostName, pins, INCLUDE_SUBDOMAINS, expira
tionDate); |
| 419 } catch (NullPointerException ex) { |
| 420 if (!shouldThrowNpe) { |
| 421 fail("Null pointer exception was not expected: " + ex.toString()
); |
| 422 } |
| 423 return; |
| 424 } |
| 425 if (shouldThrowNpe) { |
| 426 fail("NullPointerException was expected"); |
| 427 } |
| 428 } |
| 429 } |
OLD | NEW |