Chromium Code Reviews| 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.ArrayList; | |
| 16 import java.util.Arrays; | |
| 17 import java.util.Calendar; | |
| 18 import java.util.Collection; | |
| 19 import java.util.Date; | |
| 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); | |
|
nharper
2015/11/19 23:47:54
nit: calls to addPkpSha256 alternate between inlin
kapishnikov
2015/11/20 16:38:03
Done.
| |
| 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 addPkpSha256(mDomain, generateSomeSha256(), INCLUDE_SUBDOMAINS, DISTANT_ FUTURE); | |
| 107 startCronetFramework(); | |
| 108 registerHostResolver(); | |
| 109 sendRequestAndWaitForResult(); | |
| 110 | |
| 111 assertErrorResponse(); | |
| 112 } | |
| 113 | |
| 114 /** | |
| 115 * Tests the case when the pin hash does not match and the client accesses t he subdomain of | |
| 116 * the configured PKP host with includeSubdomains flag set to false. The cli ent is expected to | |
| 117 * receive the successful response with the response code 200. | |
| 118 * | |
| 119 * @throws Exception | |
| 120 */ | |
| 121 @SmallTest | |
| 122 @Feature({"Cronet"}) | |
| 123 public void testIncludeSubdomainsFlagEqualFalse() throws Exception { | |
| 124 addPkpSha256(mDomain, generateSomeSha256(), EXCLUDE_SUBDOMAINS, DISTANT_ FUTURE); | |
| 125 startCronetFramework(); | |
| 126 registerHostResolver(); | |
| 127 sendRequestAndWaitForResult(); | |
| 128 | |
| 129 assertSuccessfulResponse(); | |
| 130 } | |
| 131 | |
| 132 /** | |
| 133 * Tests the case when the mismatching pin is set for some host that is diff erent from the one | |
| 134 * the client wants to access. In that case the other host pinning policy sh ould not be applied | |
| 135 * and the client is expected to receive the successful response with the re sponse code 200. | |
| 136 * | |
| 137 * @throws Exception | |
| 138 */ | |
| 139 @SmallTest | |
| 140 @Feature({"Cronet"}) | |
| 141 public void testSuccessIfNoPinSpecified() throws Exception { | |
| 142 addPkpSha256("otherhost.com", generateSomeSha256(), INCLUDE_SUBDOMAINS, DISTANT_FUTURE); | |
| 143 startCronetFramework(); | |
| 144 registerHostResolver(); | |
| 145 sendRequestAndWaitForResult(); | |
| 146 | |
| 147 assertSuccessfulResponse(); | |
| 148 } | |
| 149 | |
| 150 /** | |
| 151 * Tests mismatching pins that will expire in 10 seconds. The pins should be still valid and | |
| 152 * enforced during the request; thus returning PIN mismatch error. | |
| 153 * | |
| 154 * @throws Exception | |
| 155 */ | |
| 156 @SmallTest | |
| 157 @Feature({"Cronet"}) | |
| 158 public void testSoonExpiringPin() throws Exception { | |
| 159 final int tenSecondsAhead = 10; | |
| 160 byte[] nonMatchingHash = generateSomeSha256(); | |
| 161 addPkpSha256(mServerHost, nonMatchingHash, EXCLUDE_SUBDOMAINS, tenSecond sAhead); | |
| 162 startCronetFramework(); | |
| 163 registerHostResolver(); | |
| 164 sendRequestAndWaitForResult(); | |
| 165 | |
| 166 assertErrorResponse(); | |
| 167 } | |
| 168 | |
| 169 /** | |
| 170 * Tests mismatching pins that expired 1 second ago. Since the pins have exp ired, they | |
| 171 * should not be enforced during the request; thus a successful response is expected. | |
| 172 * | |
| 173 * @throws Exception | |
| 174 */ | |
| 175 @SmallTest | |
| 176 @Feature({"Cronet"}) | |
| 177 public void testRecentlyExpiredPin() throws Exception { | |
| 178 final int oneSecondAgo = -1; | |
| 179 byte[] nonMatchingHash = generateSomeSha256(); | |
| 180 addPkpSha256(mServerHost, nonMatchingHash, EXCLUDE_SUBDOMAINS, oneSecond Ago); | |
| 181 startCronetFramework(); | |
| 182 registerHostResolver(); | |
| 183 sendRequestAndWaitForResult(); | |
| 184 | |
| 185 assertSuccessfulResponse(); | |
| 186 } | |
| 187 | |
| 188 /** | |
| 189 * Tests that host pinning is not persisted between multiple CronetEngine in stances. | |
| 190 * | |
| 191 * @throws Exception | |
| 192 */ | |
| 193 @SmallTest | |
| 194 @Feature({"Cronet"}) | |
| 195 public void testPinsAreNotPersisted() throws Exception { | |
| 196 byte[] nonMatchingHash = generateSomeSha256(); | |
| 197 addPkpSha256(mServerHost, nonMatchingHash, EXCLUDE_SUBDOMAINS, DISTANT_F UTURE); | |
| 198 startCronetFramework(); | |
| 199 registerHostResolver(); | |
| 200 sendRequestAndWaitForResult(); | |
| 201 assertErrorResponse(); | |
| 202 shutdownCronetEngine(); | |
| 203 | |
| 204 // Restart Cronet engine and try the same request again. Since the pins are not persisted, | |
| 205 // a successful response is expected. | |
| 206 createCronetEngineBuilder(); | |
| 207 startCronetFramework(); | |
| 208 registerHostResolver(); | |
| 209 sendRequestAndWaitForResult(); | |
| 210 assertSuccessfulResponse(); | |
| 211 } | |
| 212 | |
| 213 /** | |
| 214 * Tests that the client receives {@code InvalidArgumentException} when the pinned host name | |
| 215 * is invalid. | |
| 216 * | |
| 217 * @throws Exception | |
| 218 */ | |
| 219 @SmallTest | |
| 220 @Feature({"Cronet"}) | |
| 221 public void testHostNameArgumentValidation() throws Exception { | |
| 222 assertExceptionWhenHostNameIsInvalid("http://domain.com"); | |
| 223 assertExceptionWhenHostNameIsInvalid("domain.com:4000"); | |
| 224 assertExceptionWhenHostNameIsInvalid("domain.com/mydoc.txt"); | |
| 225 assertExceptionWhenHostNameIsInvalid("128.55.0.1"); | |
| 226 } | |
| 227 | |
| 228 /** | |
| 229 * Asserts that the response from the server contains an PKP error. | |
| 230 * TODO(kapishnikov): currently QUIC returns ERR_QUIC_PROTOCOL_ERROR instead of expected | |
| 231 * ERR_SSL_PINNED_KEY_NOT_IN_CERT_CHAIN error code when the pin doesn't matc h. | |
| 232 * This method should be changed when the bug is resolved. See http://crbug. com/548378 | |
| 233 */ | |
| 234 private void assertErrorResponse() { | |
| 235 assertNotNull("Expected an error", mListener.mError); | |
| 236 int errorCode = mListener.mError.netError(); | |
| 237 boolean correctErrorCode = errorCode == NetError.ERR_SSL_PINNED_KEY_NOT_ IN_CERT_CHAIN | |
| 238 || errorCode == NetError.ERR_QUIC_PROTOCOL_ERROR; | |
| 239 assertTrue(String.format("Incorrect error code. Expected %s or %s but re ceived %s", | |
| 240 NetError.ERR_SSL_PINNED_KEY_NOT_IN_CERT_CHAIN, | |
| 241 NetError.ERR_QUIC_PROTOCOL_ERROR, errorCode), | |
| 242 correctErrorCode); | |
| 243 } | |
| 244 | |
| 245 /** | |
| 246 * Asserts a successful response with response code 200. | |
| 247 */ | |
| 248 private void assertSuccessfulResponse() { | |
| 249 if (mListener.mError != null) { | |
| 250 fail("Did not expect an error but got error code " + mListener.mErro r.mNetError); | |
| 251 } | |
| 252 assertNotNull("Expected non-null response from the server", mListener.mR esponseInfo); | |
| 253 assertEquals(200, mListener.mResponseInfo.getHttpStatusCode()); | |
| 254 } | |
| 255 | |
| 256 private void createCronetEngineBuilder() { | |
| 257 // Set common CronetEngine parameters | |
| 258 mBuilder = new CronetEngine.Builder(getContext()); | |
| 259 mBuilder.enableQUIC(true); | |
| 260 mBuilder.addQuicHint(QuicTestServer.getServerHost(), QuicTestServer.getS erverPort(), | |
| 261 QuicTestServer.getServerPort()); | |
| 262 mBuilder.setStoragePath(CronetTestFramework.getTestStorage(getContext()) ); | |
| 263 mBuilder.enableHttpCache(CronetEngine.Builder.HTTP_CACHE_DISK_NO_HTTP, 1 000 * 1024); | |
| 264 mBuilder.setMockCertVerifierForTesting(MockCertVerifier.createMockCertVe rifier(CERTS_USED)); | |
| 265 } | |
| 266 | |
| 267 private void startCronetFramework() { | |
| 268 mTestFramework = startCronetTestFrameworkWithUrlAndCronetEngineBuilder(n ull, mBuilder); | |
| 269 } | |
| 270 | |
| 271 private void shutdownCronetEngine() { | |
| 272 if (mTestFramework != null && mTestFramework.mCronetEngine != null) { | |
| 273 mTestFramework.mCronetEngine.shutdown(); | |
| 274 } | |
| 275 } | |
| 276 | |
| 277 private void registerHostResolver() { | |
| 278 long urlRequestContextAdapter = ((CronetUrlRequestContext) mTestFramewor k.mCronetEngine) | |
| 279 .getUrlRequestContextAdapter(); | |
| 280 NativeTestServer.registerHostResolverProc(urlRequestContextAdapter, fals e); | |
| 281 } | |
| 282 | |
| 283 private byte[] generateSomeSha256() { | |
| 284 byte[] sha256 = new byte[32]; | |
| 285 Arrays.fill(sha256, (byte) 58); | |
| 286 return sha256; | |
| 287 } | |
| 288 | |
| 289 private void addPkpSha256( | |
| 290 String host, byte[] pinHashValue, boolean includeSubdomain, int maxA geInSec) { | |
| 291 Collection<byte[]> hashes = new ArrayList<>(); | |
| 292 hashes.add(pinHashValue); | |
| 293 mBuilder.addPublicKeyPins(host, hashes, includeSubdomain, dateInFuture(m axAgeInSec)); | |
| 294 } | |
| 295 | |
| 296 private void sendRequestAndWaitForResult() { | |
| 297 mListener = new TestUrlRequestCallback(); | |
| 298 | |
| 299 String quicURL = mServerUrl + "/simple.txt"; | |
| 300 UrlRequest.Builder requestBuilder = new UrlRequest.Builder( | |
| 301 quicURL, mListener, mListener.getExecutor(), mTestFramework.mCro netEngine); | |
| 302 requestBuilder.build().start(); | |
| 303 mListener.blockForDone(); | |
| 304 } | |
| 305 | |
| 306 private X509Certificate readCertFromFileInPemFormat(String certFileName) thr ows Exception { | |
| 307 byte[] certDer = CertTestUtil.pemToDer(CertTestUtil.CERTS_DIRECTORY + ce rtFileName); | |
| 308 CertificateFactory certFactory = CertificateFactory.getInstance("X.509") ; | |
| 309 return (X509Certificate) certFactory.generateCertificate(new ByteArrayIn putStream(certDer)); | |
| 310 } | |
| 311 | |
| 312 private Date dateInFuture(int secondsIntoFuture) { | |
| 313 Calendar cal = Calendar.getInstance(); | |
| 314 cal.add(Calendar.SECOND, secondsIntoFuture); | |
| 315 return cal.getTime(); | |
| 316 } | |
| 317 | |
| 318 private void assertExceptionWhenHostNameIsInvalid(String hostName) { | |
| 319 try { | |
| 320 addPkpSha256(hostName, generateSomeSha256(), INCLUDE_SUBDOMAINS, DIS TANT_FUTURE); | |
| 321 } catch (IllegalArgumentException ex) { | |
| 322 // Expected exception. | |
| 323 return; | |
| 324 } | |
| 325 fail("Expected IllegalArgumentException when passing " + hostName + " ho st name"); | |
| 326 } | |
| 327 } | |
| OLD | NEW |