| 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..9170046eb779ed34411aec3f24a54ef4735f2a68 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,18 @@ 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.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 +41,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 +311,135 @@ public abstract class CronetEngine {
|
| }
|
|
|
| /**
|
| + * <p>
|
| + * Pins a set of public keys for a given host. By pinning a set of public keys,
|
| + * {@code pinsSha256}, communication with {@code hostName} is required to
|
| + * authenticate with a certificate with a public key from the set of pinned ones.
|
| + * An app can pin the public key of the root certificate, any of the intermediate
|
| + * certificates or the end-entry certificate. Authentication will fail and secure
|
| + * communication will not be established if none of the public keys is present in the
|
| + * host's certificate chain, even if the host attempts to authenticate with a
|
| + * certificate allowed by the device's trusted store of certificates.
|
| + * </p>
|
| + * <p>
|
| + * Calling this method multiple times with the same host name overrides the previously
|
| + * set pins for the host.
|
| + * </p>
|
| + * <p>
|
| + * More information about the public key pinning can be found in
|
| + * <a href="https://tools.ietf.org/html/rfc7469">RFC 7469</a>.
|
| + * </p>
|
| + *
|
| + * @param hostName name of the host to which the public keys should be pinned. A host that
|
| + * consists only of digits and the dot character is treated as invalid.
|
| + * @param pinsSha256 a set of pins. Each pin is the SHA-256 cryptographic
|
| + * hash of the DER-encoded ASN.1 representation of the Subject Public
|
| + * Key Info (SPKI) of the host's X.509 certificate. Use
|
| + * {@link java.security.cert.Certificate#getPublicKey()
|
| + * Certificate.getPublicKey()} and
|
| + * {@link java.security.Key#getEncoded() Key.getEncoded()}
|
| + * to obtain DER-encoded ASN.1 representation of the SPKI.
|
| + * Although, the method does not mandate the presence of the backup pin
|
| + * that can be used if the control of the primary private key has been
|
| + * lost, it is highly recommended to supply one.
|
| + * @param includeSubdomains indicates whether the pinning policy should be applied to
|
| + * subdomains of {@code hostName}.
|
| + * @param expirationDate specifies the expiration date for the pins.
|
| + * @return the builder to facilitate chaining.
|
| + * @throws NullPointerException if any of the input parameters are {@code null}.
|
| + * @throws IllegalArgumentException if the given host name is invalid or {@code pinsSha256}
|
| + * contains a byte array that does not represent a valid
|
| + * SHA-256 hash.
|
| + */
|
| + public Builder addPublicKeyPins(String hostName, Set<byte[]> pinsSha256,
|
| + boolean includeSubdomains, Date expirationDate) {
|
| + if (hostName == null) {
|
| + throw new NullPointerException("The hostname cannot be null");
|
| + }
|
| + if (pinsSha256 == null) {
|
| + throw new NullPointerException("The set of SHA256 pins cannot be null");
|
| + }
|
| + if (expirationDate == null) {
|
| + throw new NullPointerException("The pin expiration date cannot be null");
|
| + }
|
| + 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. The hash set will eliminate duplications.
|
| + Set<String> hashes = new HashSet<>(pinsSha256.size());
|
| + 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.
|
| + *
|
| + * @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.
|
|
|