Home | History | Annotate | Download | only in okhttp
      1 /*
      2  * Copyright (C) 2014 Square, Inc.
      3  *
      4  * Licensed under the Apache License, Version 2.0 (the "License");
      5  * you may not use this file except in compliance with the License.
      6  * You may obtain a copy of the License at
      7  *
      8  *      http://www.apache.org/licenses/LICENSE-2.0
      9  *
     10  * Unless required by applicable law or agreed to in writing, software
     11  * distributed under the License is distributed on an "AS IS" BASIS,
     12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
     13  * See the License for the specific language governing permissions and
     14  * limitations under the License.
     15  */
     16 package com.squareup.okhttp;
     17 
     18 import com.squareup.okhttp.internal.Util;
     19 import java.security.cert.Certificate;
     20 import java.security.cert.X509Certificate;
     21 import java.util.Arrays;
     22 import java.util.LinkedHashMap;
     23 import java.util.LinkedHashSet;
     24 import java.util.List;
     25 import java.util.Map;
     26 import java.util.Set;
     27 import javax.net.ssl.SSLPeerUnverifiedException;
     28 import okio.ByteString;
     29 
     30 import static java.util.Collections.unmodifiableSet;
     31 
     32 /**
     33  * Constrains which certificates are trusted. Pinning certificates defends
     34  * against attacks on certificate authorities. It also prevents connections
     35  * through man-in-the-middle certificate authorities either known or unknown to
     36  * the application's user.
     37  *
     38  * <p>This class currently pins a certificate's Subject Public Key Info as
     39  * described on <a href="http://goo.gl/AIx3e5">Adam Langley's Weblog</a>. Pins
     40  * are base-64 SHA-1 hashes, consistent with the format Chromium uses for <a
     41  * href="http://goo.gl/XDh6je">static certificates</a>. See Chromium's <a
     42  * href="http://goo.gl/4CCnGs">pinsets</a> for hostnames that are pinned in that
     43  * browser.
     44  *
     45  * <h3>Setting up Certificate Pinning</h3>
     46  * The easiest way to pin a host is turn on pinning with a broken configuration
     47  * and read the expected configuration when the connection fails. Be sure to
     48  * do this on a trusted network, and without man-in-the-middle tools like <a
     49  * href="http://charlesproxy.com">Charles</a> or <a
     50  * href="http://fiddlertool.com">Fiddler</a>.
     51  *
     52  * <p>For example, to pin {@code https://publicobject.com}, start with a broken
     53  * configuration: <pre>   {@code
     54  *
     55  *     String hostname = "publicobject.com";
     56  *     CertificatePinner certificatePinner = new CertificatePinner.Builder()
     57  *         .add(hostname, "sha1/AAAAAAAAAAAAAAAAAAAAAAAAAAA=")
     58  *         .build();
     59  *     OkHttpClient client = new OkHttpClient();
     60  *     client.setCertificatePinner(certificatePinner);
     61  *
     62  *     Request request = new Request.Builder()
     63  *         .url("https://" + hostname)
     64  *         .build();
     65  *     client.newCall(request).execute();
     66  * }</pre>
     67  *
     68  * As expected, this fails with a certificate pinning exception: <pre>   {@code
     69  *
     70  * javax.net.ssl.SSLPeerUnverifiedException: Certificate pinning failure!
     71  *   Peer certificate chain:
     72  *     sha1/DmxUShsZuNiqPQsX2Oi9uv2sCnw=: CN=publicobject.com, OU=PositiveSSL
     73  *     sha1/SXxoaOSEzPC6BgGmxAt/EAcsajw=: CN=COMODO RSA Domain Validation Secure Server CA
     74  *     sha1/blhOM3W9V/bVQhsWAcLYwPU6n24=: CN=COMODO RSA Certification Authority
     75  *     sha1/T5x9IXmcrQ7YuQxXnxoCmeeQ84c=: CN=AddTrust External CA Root
     76  *   Pinned certificates for publicobject.com:
     77  *     sha1/AAAAAAAAAAAAAAAAAAAAAAAAAAA=
     78  *   at com.squareup.okhttp.CertificatePinner.check(CertificatePinner.java)
     79  *   at com.squareup.okhttp.Connection.upgradeToTls(Connection.java)
     80  *   at com.squareup.okhttp.Connection.connect(Connection.java)
     81  *   at com.squareup.okhttp.Connection.connectAndSetOwner(Connection.java)
     82  * }</pre>
     83  *
     84  * Follow up by pasting the public key hashes from the exception into the
     85  * certificate pinner's configuration: <pre>   {@code
     86  *
     87  *     CertificatePinner certificatePinner = new CertificatePinner.Builder()
     88  *       .add("publicobject.com", "sha1/DmxUShsZuNiqPQsX2Oi9uv2sCnw=")
     89  *       .add("publicobject.com", "sha1/SXxoaOSEzPC6BgGmxAt/EAcsajw=")
     90  *       .add("publicobject.com", "sha1/blhOM3W9V/bVQhsWAcLYwPU6n24=")
     91  *       .add("publicobject.com", "sha1/T5x9IXmcrQ7YuQxXnxoCmeeQ84c=")
     92  *       .build();
     93  * }</pre>
     94  *
     95  * Pinning is per-hostname and/or per-wildcard pattern. To pin both
     96  * {@code publicobject.com} and {@code www.publicobject.com}, you must
     97  * configure both hostnames.
     98  *
     99  * <p>Wildcard pattern rules:
    100  * <ol>
    101  *   <li>Asterisk {@code *} is only permitted in the left-most
    102  *       domain name label and must be the only character in that label
    103  *       (i.e., must match the whole left-most label). For example,
    104  *       {@code *.example.com} is permitted, while {@code *a.example.com},
    105  *       {@code a*.example.com}, {@code a*b.example.com}, {@code a.*.example.com}
    106  *       are not permitted.
    107  *   <li>Asterisk {@code *} cannot match across domain name labels.
    108  *       For example, {@code *.example.com} matches {@code test.example.com}
    109  *       but does not match {@code sub.test.example.com}.
    110  *   <li>Wildcard patterns for single-label domain names are not permitted.
    111  * </ol>
    112  *
    113  * If hostname pinned directly and via wildcard pattern, both
    114  * direct and wildcard pins will be used. For example: {@code *.example.com} pinned
    115  * with {@code pin1} and {@code a.example.com} pinned with {@code pin2},
    116  * to check {@code a.example.com} both {@code pin1} and {@code pin2} will be used.
    117  *
    118  * <h3>Warning: Certificate Pinning is Dangerous!</h3>
    119  * Pinning certificates limits your server team's abilities to update their TLS
    120  * certificates. By pinning certificates, you take on additional operational
    121  * complexity and limit your ability to migrate between certificate authorities.
    122  * Do not use certificate pinning without the blessing of your server's TLS
    123  * administrator!
    124  *
    125  * <h4>Note about self-signed certificates</h4>
    126  * {@link CertificatePinner} can not be used to pin self-signed certificate
    127  * if such certificate is not accepted by {@link javax.net.ssl.TrustManager}.
    128  *
    129  * @see <a href="https://www.owasp.org/index.php/Certificate_and_Public_Key_Pinning">
    130  *     OWASP: Certificate and Public Key Pinning</a>
    131  */
    132 public final class CertificatePinner {
    133   public static final CertificatePinner DEFAULT = new Builder().build();
    134 
    135   private final Map<String, Set<ByteString>> hostnameToPins;
    136 
    137   private CertificatePinner(Builder builder) {
    138     this.hostnameToPins = Util.immutableMap(builder.hostnameToPins);
    139   }
    140 
    141   /**
    142    * Confirms that at least one of the certificates pinned for {@code hostname}
    143    * is in {@code peerCertificates}. Does nothing if there are no certificates
    144    * pinned for {@code hostname}. OkHttp calls this after a successful TLS
    145    * handshake, but before the connection is used.
    146    *
    147    * @throws SSLPeerUnverifiedException if {@code peerCertificates} don't match
    148    *     the certificates pinned for {@code hostname}.
    149    */
    150   public void check(String hostname, List<Certificate> peerCertificates)
    151       throws SSLPeerUnverifiedException {
    152 
    153     Set<ByteString> pins = findMatchingPins(hostname);
    154 
    155     if (pins == null) return;
    156 
    157     for (int i = 0, size = peerCertificates.size(); i < size; i++) {
    158       X509Certificate x509Certificate = (X509Certificate) peerCertificates.get(i);
    159       if (pins.contains(sha1(x509Certificate))) return; // Success!
    160     }
    161 
    162     // If we couldn't find a matching pin, format a nice exception.
    163     StringBuilder message = new StringBuilder()
    164         .append("Certificate pinning failure!")
    165         .append("\n  Peer certificate chain:");
    166     for (int i = 0, size = peerCertificates.size(); i < size; i++) {
    167       X509Certificate x509Certificate = (X509Certificate) peerCertificates.get(i);
    168       message.append("\n    ").append(pin(x509Certificate))
    169           .append(": ").append(x509Certificate.getSubjectDN().getName());
    170     }
    171     message.append("\n  Pinned certificates for ").append(hostname).append(":");
    172     for (ByteString pin : pins) {
    173       message.append("\n    sha1/").append(pin.base64());
    174     }
    175     throw new SSLPeerUnverifiedException(message.toString());
    176   }
    177 
    178   /** @deprecated replaced with {@link #check(String, List)}. */
    179   public void check(String hostname, Certificate... peerCertificates)
    180       throws SSLPeerUnverifiedException {
    181     check(hostname, Arrays.asList(peerCertificates));
    182   }
    183 
    184   /**
    185    * Returns list of matching certificates' pins for the hostname
    186    * or {@code null} if hostname does not have pinned certificates.
    187    */
    188   Set<ByteString> findMatchingPins(String hostname) {
    189     Set<ByteString> directPins   = hostnameToPins.get(hostname);
    190     Set<ByteString> wildcardPins = null;
    191 
    192     int indexOfFirstDot = hostname.indexOf('.');
    193     int indexOfLastDot  = hostname.lastIndexOf('.');
    194 
    195     // Skip hostnames with one dot symbol for wildcard pattern search
    196     //   example.com   will  be skipped
    197     //   a.example.com won't be skipped
    198     if (indexOfFirstDot != indexOfLastDot) {
    199       // a.example.com -> search for wildcard pattern *.example.com
    200       wildcardPins = hostnameToPins.get("*." + hostname.substring(indexOfFirstDot + 1));
    201     }
    202 
    203     if (directPins == null && wildcardPins == null) return null;
    204 
    205     if (directPins != null && wildcardPins != null) {
    206       Set<ByteString> pins = new LinkedHashSet<>();
    207       pins.addAll(directPins);
    208       pins.addAll(wildcardPins);
    209       return pins;
    210     }
    211 
    212     if (directPins != null) return directPins;
    213 
    214     return wildcardPins;
    215   }
    216 
    217   /**
    218    * Returns the SHA-1 of {@code certificate}'s public key. This uses the
    219    * mechanism Moxie Marlinspike describes in <a
    220    * href="https://github.com/moxie0/AndroidPinning">Android Pinning</a>.
    221    */
    222   public static String pin(Certificate certificate) {
    223     if (!(certificate instanceof X509Certificate)) {
    224       throw new IllegalArgumentException("Certificate pinning requires X509 certificates");
    225     }
    226     return "sha1/" + sha1((X509Certificate) certificate).base64();
    227   }
    228 
    229   private static ByteString sha1(X509Certificate x509Certificate) {
    230     return Util.sha1(ByteString.of(x509Certificate.getPublicKey().getEncoded()));
    231   }
    232 
    233   /** Builds a configured certificate pinner. */
    234   public static final class Builder {
    235     private final Map<String, Set<ByteString>> hostnameToPins = new LinkedHashMap<>();
    236 
    237     /**
    238      * Pins certificates for {@code hostname}.
    239      *
    240      * @param hostname lower-case host name or wildcard pattern such as {@code *.example.com}.
    241      * @param pins SHA-1 hashes. Each pin is a SHA-1 hash of a
    242      *     certificate's Subject Public Key Info, base64-encoded and prefixed with
    243      *     {@code sha1/}.
    244      */
    245     public Builder add(String hostname, String... pins) {
    246       if (hostname == null) throw new IllegalArgumentException("hostname == null");
    247 
    248       Set<ByteString> hostPins = new LinkedHashSet<>();
    249       Set<ByteString> previousPins = hostnameToPins.put(hostname, unmodifiableSet(hostPins));
    250       if (previousPins != null) {
    251         hostPins.addAll(previousPins);
    252       }
    253 
    254       for (String pin : pins) {
    255         if (!pin.startsWith("sha1/")) {
    256           throw new IllegalArgumentException("pins must start with 'sha1/': " + pin);
    257         }
    258         ByteString decodedPin = ByteString.decodeBase64(pin.substring("sha1/".length()));
    259         if (decodedPin == null) {
    260           throw new IllegalArgumentException("pins must be base64: " + pin);
    261         }
    262         hostPins.add(decodedPin);
    263       }
    264 
    265       return this;
    266     }
    267 
    268     public CertificatePinner build() {
    269       return new CertificatePinner(this);
    270     }
    271   }
    272 }
    273