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 */
16package com.squareup.okhttp;
17
18import com.squareup.okhttp.internal.Util;
19import java.security.cert.Certificate;
20import java.security.cert.X509Certificate;
21import java.util.Arrays;
22import java.util.LinkedHashMap;
23import java.util.LinkedHashSet;
24import java.util.List;
25import java.util.Map;
26import java.util.Set;
27import javax.net.ssl.SSLPeerUnverifiedException;
28import okio.ByteString;
29
30import 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 */
132public 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