Chromium Code Reviews
chromiumcodereview-hr@appspot.gserviceaccount.com (chromiumcodereview-hr) | Please choose your nickname with Settings | Help | Chromium Project | Gerrit Changes | Sign out
(153)

Side by Side Diff: components/cronet/android/test/javatests/src/org/chromium/net/PkpTest.java

Issue 1407263010: [Cronet] Public key pinning for Java API (Closed) Base URL: https://chromium.googlesource.com/chromium/src.git@master
Patch Set: Comments & Rebase Created 5 years ago
Use n/p to move between diff chunks; N/P to move between comments. Draft comments are only viewable by you.
Jump to:
View unified diff | Download patch
OLDNEW
(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);
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 * Asserts that the response from the server contains an PKP error.
274 * TODO(kapishnikov): currently QUIC returns ERR_QUIC_PROTOCOL_ERROR instead of expected
275 * ERR_SSL_PINNED_KEY_NOT_IN_CERT_CHAIN error code when the pin doesn't matc h.
276 * This method should be changed when the bug is resolved. See http://crbug. com/548378
277 */
278 private void assertErrorResponse() {
279 assertNotNull("Expected an error", mListener.mError);
280 int errorCode = mListener.mError.netError();
281 boolean correctErrorCode = errorCode == NetError.ERR_SSL_PINNED_KEY_NOT_ IN_CERT_CHAIN
282 || errorCode == NetError.ERR_QUIC_PROTOCOL_ERROR;
283 assertTrue(String.format("Incorrect error code. Expected %s or %s but re ceived %s",
284 NetError.ERR_SSL_PINNED_KEY_NOT_IN_CERT_CHAIN,
285 NetError.ERR_QUIC_PROTOCOL_ERROR, errorCode),
286 correctErrorCode);
287 }
288
289 /**
290 * Asserts a successful response with response code 200.
291 */
292 private void assertSuccessfulResponse() {
293 if (mListener.mError != null) {
294 fail("Did not expect an error but got error code " + mListener.mErro r.mNetError);
295 }
296 assertNotNull("Expected non-null response from the server", mListener.mR esponseInfo);
297 assertEquals(200, mListener.mResponseInfo.getHttpStatusCode());
298 }
299
300 private void createCronetEngineBuilder() {
301 // Set common CronetEngine parameters
302 mBuilder = new CronetEngine.Builder(getContext());
303 mBuilder.enableQUIC(true);
304 mBuilder.addQuicHint(QuicTestServer.getServerHost(), QuicTestServer.getS erverPort(),
305 QuicTestServer.getServerPort());
306 mBuilder.setStoragePath(CronetTestFramework.getTestStorage(getContext()) );
307 mBuilder.enableHttpCache(CronetEngine.Builder.HTTP_CACHE_DISK_NO_HTTP, 1 000 * 1024);
308 mBuilder.setMockCertVerifierForTesting(MockCertVerifier.createMockCertVe rifier(CERTS_USED));
309 }
310
311 private void startCronetFramework() {
312 mTestFramework = startCronetTestFrameworkWithUrlAndCronetEngineBuilder(n ull, mBuilder);
313 }
314
315 private void shutdownCronetEngine() {
316 if (mTestFramework != null && mTestFramework.mCronetEngine != null) {
317 mTestFramework.mCronetEngine.shutdown();
318 }
319 }
320
321 private void registerHostResolver() {
322 long urlRequestContextAdapter = ((CronetUrlRequestContext) mTestFramewor k.mCronetEngine)
323 .getUrlRequestContextAdapter();
324 NativeTestServer.registerHostResolverProc(urlRequestContextAdapter, fals e);
pauljensen 2015/12/01 20:50:13 Should we move this to CronetTestBase as it looks
kapishnikov 2015/12/02 22:11:36 It is a good idea. The similar logic is used in Sd
325 }
326
327 private byte[] generateSomeSha256() {
328 byte[] sha256 = new byte[32];
329 Arrays.fill(sha256, (byte) 58);
330 return sha256;
331 }
332
333 private void addPkpSha256(
334 String host, byte[] pinHashValue, boolean includeSubdomain, int maxA geInSec) {
335 Collection<byte[]> hashes = new ArrayList<>();
336 hashes.add(pinHashValue);
337 mBuilder.addPublicKeyPins(host, hashes, includeSubdomain, dateInFuture(m axAgeInSec));
338 }
339
340 private void sendRequestAndWaitForResult() {
341 mListener = new TestUrlRequestCallback();
342
343 String quicURL = mServerUrl + "/simple.txt";
344 UrlRequest.Builder requestBuilder = new UrlRequest.Builder(
345 quicURL, mListener, mListener.getExecutor(), mTestFramework.mCro netEngine);
346 requestBuilder.build().start();
347 mListener.blockForDone();
348 }
349
350 private X509Certificate readCertFromFileInPemFormat(String certFileName) thr ows Exception {
351 byte[] certDer = CertTestUtil.pemToDer(CertTestUtil.CERTS_DIRECTORY + ce rtFileName);
352 CertificateFactory certFactory = CertificateFactory.getInstance("X.509") ;
353 return (X509Certificate) certFactory.generateCertificate(new ByteArrayIn putStream(certDer));
354 }
355
356 private Date dateInFuture(int secondsIntoFuture) {
357 Calendar cal = Calendar.getInstance();
358 cal.add(Calendar.SECOND, secondsIntoFuture);
359 return cal.getTime();
360 }
361
362 private void assertNoExceptionWhenHostNameIsValid(String hostName) {
363 try {
364 addPkpSha256(hostName, generateSomeSha256(), INCLUDE_SUBDOMAINS, DIS TANT_FUTURE);
365 } catch (IllegalArgumentException ex) {
366 fail("Host name " + hostName + " should be valid but the exception w as thrown: "
367 + ex.toString());
368 }
369 }
370
371 private void assertExceptionWhenHostNameIsInvalid(String hostName) {
372 try {
373 addPkpSha256(hostName, generateSomeSha256(), INCLUDE_SUBDOMAINS, DIS TANT_FUTURE);
374 } catch (IllegalArgumentException ex) {
375 // Expected exception.
376 return;
377 }
378 fail("Expected IllegalArgumentException when passing " + hostName + " ho st name");
379 }
380 }
OLDNEW

Powered by Google App Engine
This is Rietveld 408576698