Chromium Code Reviews| 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. |