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

Unified Diff: components/cronet/android/api/src/org/chromium/net/CronetEngine.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, 1 month 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 side-by-side diff with in-line comments
Download patch
Index: components/cronet/android/api/src/org/chromium/net/CronetEngine.java
diff --git a/components/cronet/android/api/src/org/chromium/net/CronetEngine.java b/components/cronet/android/api/src/org/chromium/net/CronetEngine.java
index 6e7d1e0940ccc2fa4668cc24373cd866e90866ea..ccd01adc68124359169146cdbc913466cfba873e 100644
--- a/components/cronet/android/api/src/org/chromium/net/CronetEngine.java
+++ b/components/cronet/android/api/src/org/chromium/net/CronetEngine.java
@@ -6,6 +6,7 @@ package org.chromium.net;
import android.content.Context;
import android.support.annotation.IntDef;
+import android.util.Base64;
import android.util.Log;
import org.json.JSONArray;
@@ -16,13 +17,19 @@ import java.io.File;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.reflect.Constructor;
+import java.net.IDN;
import java.net.Proxy;
import java.net.URL;
import java.net.URLConnection;
import java.net.URLStreamHandlerFactory;
+import java.util.Collection;
+import java.util.Date;
+import java.util.HashSet;
import java.util.List;
import java.util.Map;
+import java.util.Set;
import java.util.concurrent.Executor;
+import java.util.regex.Pattern;
/**
* An engine to process {@link UrlRequest}s, which uses the best HTTP stack
@@ -35,6 +42,8 @@ public abstract class CronetEngine {
* then {@link #build} is called to create the {@code CronetEngine}.
*/
public static class Builder {
+ private static final Pattern INVALID_PKP_HOST_NAME = Pattern.compile("^[0-9\\.]*$");
+
private final JSONObject mConfig;
private final Context mContext;
@@ -303,6 +312,113 @@ public abstract class CronetEngine {
}
/**
+ * Adds public key pins for a given host.
pauljensen 2015/12/01 20:50:12 Do we allow intermediate certificates signed by th
pauljensen 2015/12/01 20:50:12 I think we need to make this a lot more verbose.
pauljensen 2015/12/01 20:50:12 This would probably be a good place to include a l
kapishnikov 2015/12/02 22:11:36 Yes, we do allow intermediate certificates signed
kapishnikov 2015/12/02 22:11:36 Done.
+ *
+ * @param hostName name of the host to which the public keys should be pinned. A host that
+ * consists of digits and the dot character only is treated as invalid.
pauljensen 2015/12/01 20:50:13 nit: of digits and the dot character only->of only
pauljensen 2015/12/01 20:50:13 Actually I think we might want to get rid of this
kapishnikov 2015/12/02 22:11:36 Done.
kapishnikov 2015/12/02 22:11:36 The problem here is that the API not only restrict
+ * @param pinsSha256 a collection of pins. Each pin is the SHA-256 cryptographic
+ * hash of DER-encoded ASN.1 representation of Subject Public
pauljensen 2015/12/01 20:50:12 of->of the
kapishnikov 2015/12/02 22:11:36 Done. I also added comments regarding the backup k
+ * Key Info (SPKI) of the host X.509 certificate. Use
pauljensen 2015/12/01 20:50:13 host->host's
kapishnikov 2015/12/02 22:11:36 Done.
+ * {@link java.security.cert.Certificate#getPublicKey()
+ * Certificate.getPublicKey} and
pauljensen 2015/12/01 20:50:13 }->()}
kapishnikov 2015/12/02 22:11:36 Done.
+ * {@link java.security.Key#getEncoded() Key.getEncoded}
pauljensen 2015/12/01 20:50:12 ditto
kapishnikov 2015/12/02 22:11:36 Done.
+ * to obtain DER-encoded ASN.1 representation of SPKI.
pauljensen 2015/12/01 20:50:12 SPKI->the SPKI
kapishnikov 2015/12/02 22:11:36 Done.
+ * @param includeSubdomains indicates whether the pinning policy should be applied to
+ * subdomains of {@code hostName}.
+ * @param expirationDate specifies the expiration date for the pins.
pauljensen 2015/12/01 20:50:12 What happens when they expire? do we fall back to
kapishnikov 2015/12/02 22:11:36 Yes, that is correct. When the pins expire, the cl
+ * @return the builder to facilitate chaining.
+ * @throws NullPointerException if one of the input parameters is null.
pauljensen 2015/12/01 20:50:12 null->{@code null}
pauljensen 2015/12/01 20:50:13 one->any is->are
kapishnikov 2015/12/02 22:11:36 Done.
kapishnikov 2015/12/02 22:11:36 Done.
+ * @throws IllegalArgumentException if the given host name is invalid or the
pauljensen 2015/12/01 20:50:12 or the->or
kapishnikov 2015/12/02 22:11:36 Done.
+ * {@code pinsSha256} collection contains a byte array
pauljensen 2015/12/01 20:50:12 collection contains->contains
kapishnikov 2015/12/02 22:11:36 Done.
+ * that does not represent a valid SHA-256 hash.
+ */
+ public Builder addPublicKeyPins(String hostName, Collection<byte[]> pinsSha256,
+ boolean includeSubdomains, Date expirationDate) {
+ if (hostName == null) {
+ throw new NullPointerException("The hostname cannot be null");
+ }
+ if (pinsSha256 == null) {
+ throw new NullPointerException("The collection of SHA256 pins cannot be null");
+ }
pauljensen 2015/12/01 20:50:12 null check expirationDate also, and test
kapishnikov 2015/12/02 22:11:36 Done. Added tests to check the exception when any
+ String idnHostName = validateHostNameForPinningAndConvert(hostName);
+ try {
+ // Add PKP_LIST JSON array element if it is not present.
+ JSONArray pkpList = mConfig.optJSONArray(CronetEngineBuilderList.PKP_LIST);
+ if (pkpList == null) {
+ pkpList = new JSONArray();
+ mConfig.put(CronetEngineBuilderList.PKP_LIST, pkpList);
+ }
+
+ // Convert the pin to BASE64 encoding.
+ Set<String> hashes = new HashSet<>(pinsSha256.size());
pauljensen 2015/12/01 20:50:13 I assume you're using HashSet to eliminate duplica
kapishnikov 2015/12/02 22:11:36 Good point. From the client point of view it does
+ for (byte[] pinSha256 : pinsSha256) {
+ hashes.add(convertSha256ToBase64WithPrefix(pinSha256));
+ }
+
+ // Add new element to PKP_LIST JSON array.
+ JSONObject pkp = new JSONObject();
+ pkp.put(CronetEngineBuilderList.PKP_HOST, idnHostName);
+ pkp.put(CronetEngineBuilderList.PKP_PIN_HASHES, new JSONArray(hashes));
+ pkp.put(CronetEngineBuilderList.PKP_INCLUDE_SUBDOMAINS, includeSubdomains);
+ // The expiration time is passed as a double, in seconds since January 1, 1970.
+ pkp.put(CronetEngineBuilderList.PKP_EXPIRATION_DATE,
+ (double) expirationDate.getTime() / 1000);
+ pkpList.put(pkp);
+ } catch (JSONException e) {
+ // This exception should never happen.
+ throw new RuntimeException(
+ "Failed to add pubic key pins with the given arguments", e);
+ }
+ return this;
+ }
+
+ /**
+ * Converts a given SHA256 array of bytes to BASE64 encoded string and prepends
+ * {@code sha256/} prefix to it. The format corresponds to the format that is expected by
+ * {@code net::HashValue} class.
+ *
+ * @param sha256 SHA256 bytes to convert to BASE64.
+ * @return the BASE64 encoded SHA256 with the prefix.
+ * @throws IllegalArgumentException if the provided pin is invalid.
+ */
+ private static String convertSha256ToBase64WithPrefix(byte[] sha256) {
+ if (sha256 == null || sha256.length != 32) {
+ throw new IllegalArgumentException("Public key pin is invalid");
+ }
+ return "sha256/" + Base64.encodeToString(sha256, Base64.NO_WRAP);
+ }
+
+ /**
+ * Checks whether a given string represents a valid host name for PKP and converts it
+ * to ASCII Compatible Encoding representation according to RFC 1122, RFC 1123 and
+ * RFC 3490. This method is more restrictive than required by RFC 7469. Thus, a host
+ * that contains digits and the dot character only is considered invalid.
+ *
+ * Note: Currently Cronet doesn't have native implementation of host name validation that
+ * can be used. There is code that parses a provided URL but doesn't ensure its
+ * correctness. The implementation relies on {@code getaddrinfo} function.
+ *
+ * @see <a href="https://tools.ietf.org/html/rfc7469#section-2.3.3>RFC 7469</a>
+ * @param hostName host name to check and convert.
+ * @return true if the string is a valid host name.
+ * @throws IllegalArgumentException if the the given string does not represent a valid
+ * hostname.
+ */
+ private static String validateHostNameForPinningAndConvert(String hostName)
+ throws IllegalArgumentException {
+ if (INVALID_PKP_HOST_NAME.matcher(hostName).matches()) {
+ throw new IllegalArgumentException("Hostname " + hostName + " is illegal."
+ + " A hostname should not consist of digits and/or dots only.");
+ }
+ try {
+ return IDN.toASCII(hostName, IDN.USE_STD3_ASCII_RULES);
+ } catch (IllegalArgumentException ex) {
+ throw new IllegalArgumentException("Hostname " + hostName + " is illegal."
+ + " The name of the host does not comply with RFC 1122 and RFC 1123.");
+ }
+ }
+
+ /**
* Sets experimental options to be used in Cronet.
*
* @param options JSON formatted experimental options.

Powered by Google App Engine
This is Rietveld 408576698