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 okhttp3;
17
18 import java.security.cert.Certificate;
19 import java.security.cert.X509Certificate;
20 import java.util.ArrayList;
21 import java.util.Arrays;
22 import java.util.Collections;
23 import java.util.LinkedHashSet;
24 import java.util.List;
25 import java.util.Objects;
26 import java.util.Set;
27 import javax.annotation.Nullable;
28 import javax.net.ssl.SSLPeerUnverifiedException;
29 import okhttp3.internal.tls.CertificateChainCleaner;
30 import okio.ByteString;
31
32 /**
33  * Constrains which certificates are trusted. Pinning certificates defends against attacks on
34  * certificate authorities. It also prevents connections through man-in-the-middle certificate
35  * authorities either known or unknown to the application's user.
36  *
37  * <p>This class currently pins a certificate's Subject Public Key Info as described on <a
38  * href="http://goo.gl/AIx3e5">Adam Langley's Weblog</a>. Pins are either base64 SHA-256 hashes as
39  * in <a href="http://tools.ietf.org/html/rfc7469">HTTP Public Key Pinning (HPKP)</a> or SHA-1
40  * base64 hashes as in Chromium's <a href="http://goo.gl/XDh6je">static certificates</a>.
41  *
42  * <h3>Setting up Certificate Pinning</h3>
43  *
44  * <p>The easiest way to pin a host is turn on pinning with a broken configuration and read the
45  * expected configuration when the connection fails. Be sure to do this on a trusted network, and
46  * without man-in-the-middle tools like <a href="http://charlesproxy.com">Charles</a> or <a
47  * href="http://fiddlertool.com">Fiddler</a>.
48  *
49  * <p>For example, to pin {@code https://publicobject.com}, start with a broken
50  * configuration: <pre>   {@code
51  *
52  *     String hostname = "publicobject.com";
53  *     CertificatePinner certificatePinner = new CertificatePinner.Builder()
54  *         .add(hostname, "sha256/AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=")
55  *         .build();
56  *     OkHttpClient client = OkHttpClient.Builder()
57  *         .certificatePinner(certificatePinner)
58  *         .build();
59  *
60  *     Request request = new Request.Builder()
61  *         .url("https://" + hostname)
62  *         .build();
63  *     client.newCall(request).execute();
64  * }</pre>
65  *
66  * As expected, this fails with a certificate pinning exception: <pre>   {@code
67  *
68  * javax.net.ssl.SSLPeerUnverifiedException: Certificate pinning failure!
69  *   Peer certificate chain:
70  *     sha256/afwiKY3RxoMmLkuRW1l7QsPZTJPwDS2pdDROQjXw8ig=: CN=publicobject.com, OU=PositiveSSL
71  *     sha256/klO23nT2ehFDXCfx3eHTDRESMz3asj1muO+4aIdjiuY=: CN=COMODO RSA Secure Server CA
72  *     sha256/grX4Ta9HpZx6tSHkmCrvpApTQGo67CYDnvprLg5yRME=: CN=COMODO RSA Certification Authority
73  *     sha256/lCppFqbkrlJ3EcVFAkeip0+44VaoJUymbnOaEUk7tEU=: CN=AddTrust External CA Root
74  *   Pinned certificates for publicobject.com:
75  *     sha256/AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=
76  *   at okhttp3.CertificatePinner.check(CertificatePinner.java)
77  *   at okhttp3.Connection.upgradeToTls(Connection.java)
78  *   at okhttp3.Connection.connect(Connection.java)
79  *   at okhttp3.Connection.connectAndSetOwner(Connection.java)
80  * }</pre>
81  *
82  * Follow up by pasting the public key hashes from the exception into the
83  * certificate pinner's configuration: <pre>   {@code
84  *
85  *     CertificatePinner certificatePinner = new CertificatePinner.Builder()
86  *       .add("publicobject.com""sha256/afwiKY3RxoMmLkuRW1l7QsPZTJPwDS2pdDROQjXw8ig=")
87  *       .add("publicobject.com""sha256/klO23nT2ehFDXCfx3eHTDRESMz3asj1muO+4aIdjiuY=")
88  *       .add("publicobject.com""sha256/grX4Ta9HpZx6tSHkmCrvpApTQGo67CYDnvprLg5yRME=")
89  *       .add("publicobject.com""sha256/lCppFqbkrlJ3EcVFAkeip0+44VaoJUymbnOaEUk7tEU=")
90  *       .build();
91  * }</pre>
92  *
93  * Pinning is per-hostname and/or per-wildcard pattern. To pin both {@code publicobject.com} and
94  * {@code www.publicobject.com}, you must configure both hostnames.
95  *
96  * <p>Wildcard pattern rules:
97  * <ol>
98  *     <li>Asterisk {@code *} is only permitted in the left-most domain name label and must be the
99  *         only character in that label (i.e., must match the whole left-most label). For example,
100  *         {@code *.example.com} is permitted, while {@code *a.example.com}, {@code a*.example.com},
101  *         {@code a*b.example.com}, {@code a.*.example.com} are not permitted.
102  *     <li>Asterisk {@code *} cannot match across domain name labels. For example,
103  *         {@code *.example.com} matches {@code test.example.com} but does not match
104  *         {@code sub.test.example.com}.
105  *     <li>Wildcard patterns for single-label domain names are not permitted.
106  * </ol>
107  *
108  * If hostname pinned directly and via wildcard pattern, both direct and wildcard pins will be used.
109  * For example: {@code *.example.com} pinned with {@code pin1} and {@code a.example.com} pinned with
110  * {@code pin2}, to check {@code a.example.com} both {@code pin1} and {@code pin2} will be used.
111  *
112  * <h3>Warning: Certificate Pinning is Dangerous!</h3>
113  *
114  * <p>Pinning certificates limits your server team's abilities to update their TLS certificates. By
115  * pinning certificates, you take on additional operational complexity and limit your ability to
116  * migrate between certificate authorities. Do not use certificate pinning without the blessing of
117  * your server's TLS administrator!
118  *
119  * <h4>Note about self-signed certificates</h4>
120  *
121  * <p>{@link CertificatePinner} can not be used to pin self-signed certificate if such certificate
122  * is not accepted by {@link javax.net.ssl.TrustManager}.
123  *
124  * @see <a href="https://www.owasp.org/index.php/Certificate_and_Public_Key_Pinning"> OWASP:
125  * Certificate and Public Key Pinning</a>
126  */

127 public final class CertificatePinner {
128   public static final CertificatePinner DEFAULT = new Builder().build();
129
130   private final Set<Pin> pins;
131   private final @Nullable CertificateChainCleaner certificateChainCleaner;
132
133   CertificatePinner(Set<Pin> pins, @Nullable CertificateChainCleaner certificateChainCleaner) {
134     this.pins = pins;
135     this.certificateChainCleaner = certificateChainCleaner;
136   }
137
138   @Override public boolean equals(@Nullable Object other) {
139     if (other == thisreturn true;
140     return other instanceof CertificatePinner
141         && (Objects.equals(certificateChainCleaner,
142         ((CertificatePinner) other).certificateChainCleaner)
143         && pins.equals(((CertificatePinner) other).pins));
144   }
145
146   @Override public int hashCode() {
147     int result = Objects.hashCode(certificateChainCleaner);
148     result = 31 * result + pins.hashCode();
149     return result;
150   }
151
152   /**
153    * Confirms that at least one of the certificates pinned for {@code hostname} is in {@code
154    * peerCertificates}. Does nothing if there are no certificates pinned for {@code hostname}.
155    * OkHttp calls this after a successful TLS handshake, but before the connection is used.
156    *
157    * @throws SSLPeerUnverifiedException if {@code peerCertificates} don't match the certificates
158    * pinned for {@code hostname}.
159    */

160   public void check(String hostname, List<Certificate> peerCertificates)
161       throws SSLPeerUnverifiedException {
162     List<Pin> pins = findMatchingPins(hostname);
163     if (pins.isEmpty()) return;
164
165     if (certificateChainCleaner != null) {
166       peerCertificates = certificateChainCleaner.clean(peerCertificates, hostname);
167     }
168
169     for (int c = 0, certsSize = peerCertificates.size(); c < certsSize; c++) {
170       X509Certificate x509Certificate = (X509Certificate) peerCertificates.get(c);
171
172       // Lazily compute the hashes for each certificate.
173       ByteString sha1 = null;
174       ByteString sha256 = null;
175
176       for (int p = 0, pinsSize = pins.size(); p < pinsSize; p++) {
177         Pin pin = pins.get(p);
178         if (pin.hashAlgorithm.equals("sha256/")) {
179           if (sha256 == null) sha256 = sha256(x509Certificate);
180           if (pin.hash.equals(sha256)) return// Success!
181         } else if (pin.hashAlgorithm.equals("sha1/")) {
182           if (sha1 == null) sha1 = sha1(x509Certificate);
183           if (pin.hash.equals(sha1)) return// Success!
184         } else {
185           throw new AssertionError("unsupported hashAlgorithm: " + pin.hashAlgorithm);
186         }
187       }
188     }
189
190     // If we couldn't find a matching pin, format a nice exception.
191     StringBuilder message = new StringBuilder()
192         .append("Certificate pinning failure!")
193         .append("\n  Peer certificate chain:");
194     for (int c = 0, certsSize = peerCertificates.size(); c < certsSize; c++) {
195       X509Certificate x509Certificate = (X509Certificate) peerCertificates.get(c);
196       message.append("\n    ").append(pin(x509Certificate))
197           .append(": ").append(x509Certificate.getSubjectDN().getName());
198     }
199     message.append("\n  Pinned certificates for ").append(hostname).append(":");
200     for (int p = 0, pinsSize = pins.size(); p < pinsSize; p++) {
201       Pin pin = pins.get(p);
202       message.append("\n    ").append(pin);
203     }
204     throw new SSLPeerUnverifiedException(message.toString());
205   }
206
207   /** @deprecated replaced with {@link #check(String, List)}. */
208   public void check(String hostname, Certificate... peerCertificates)
209       throws SSLPeerUnverifiedException {
210     check(hostname, Arrays.asList(peerCertificates));
211   }
212
213   /**
214    * Returns list of matching certificates' pins for the hostname. Returns an empty list if the
215    * hostname does not have pinned certificates.
216    */

217   List<Pin> findMatchingPins(String hostname) {
218     List<Pin> result = Collections.emptyList();
219     for (Pin pin : pins) {
220       if (pin.matches(hostname)) {
221         if (result.isEmpty()) result = new ArrayList<>();
222         result.add(pin);
223       }
224     }
225     return result;
226   }
227
228   /** Returns a certificate pinner that uses {@code certificateChainCleaner}. */
229   CertificatePinner withCertificateChainCleaner(
230       @Nullable CertificateChainCleaner certificateChainCleaner) {
231     return Objects.equals(this.certificateChainCleaner, certificateChainCleaner)
232         ? this
233         : new CertificatePinner(pins, certificateChainCleaner);
234   }
235
236   /**
237    * Returns the SHA-256 of {@code certificate}'s public key.
238    *
239    * <p>In OkHttp 3.1.2 and earlier, this returned a SHA-1 hash of the public key. Both types are
240    * supported, but SHA-256 is preferred.
241    */

242   public static String pin(Certificate certificate) {
243     if (!(certificate instanceof X509Certificate)) {
244       throw new IllegalArgumentException("Certificate pinning requires X509 certificates");
245     }
246     return "sha256/" + sha256((X509Certificate) certificate).base64();
247   }
248
249   static ByteString sha1(X509Certificate x509Certificate) {
250     return ByteString.of(x509Certificate.getPublicKey().getEncoded()).sha1();
251   }
252
253   static ByteString sha256(X509Certificate x509Certificate) {
254     return ByteString.of(x509Certificate.getPublicKey().getEncoded()).sha256();
255   }
256
257   static final class Pin {
258     private static final String WILDCARD = "*.";
259     /** A hostname like {@code example.com} or a pattern like {@code *.example.com}. */
260     final String pattern;
261     /** The canonical hostname, i.e. {@code EXAMPLE.com} becomes {@code example.com}. */
262     final String canonicalHostname;
263     /** Either {@code sha1/} or {@code sha256/}. */
264     final String hashAlgorithm;
265     /** The hash of the pinned certificate using {@link #hashAlgorithm}. */
266     final ByteString hash;
267
268     Pin(String pattern, String pin) {
269       this.pattern = pattern;
270       this.canonicalHostname = pattern.startsWith(WILDCARD)
271           ? HttpUrl.get("http://" + pattern.substring(WILDCARD.length())).host()
272           : HttpUrl.get("http://" + pattern).host();
273       if (pin.startsWith("sha1/")) {
274         this.hashAlgorithm = "sha1/";
275         this.hash = ByteString.decodeBase64(pin.substring("sha1/".length()));
276       } else if (pin.startsWith("sha256/")) {
277         this.hashAlgorithm = "sha256/";
278         this.hash = ByteString.decodeBase64(pin.substring("sha256/".length()));
279       } else {
280         throw new IllegalArgumentException("pins must start with 'sha256/' or 'sha1/': " + pin);
281       }
282
283       if (this.hash == null) {
284         throw new IllegalArgumentException("pins must be base64: " + pin);
285       }
286     }
287
288     boolean matches(String hostname) {
289       if (pattern.startsWith(WILDCARD)) {
290         int firstDot = hostname.indexOf('.');
291         return (hostname.length() - firstDot - 1) == canonicalHostname.length()
292             && hostname.regionMatches(false, firstDot + 1, canonicalHostname, 0,
293             canonicalHostname.length());
294       }
295
296       return hostname.equals(canonicalHostname);
297     }
298
299     @Override public boolean equals(Object other) {
300       return other instanceof Pin
301           && pattern.equals(((Pin) other).pattern)
302           && hashAlgorithm.equals(((Pin) other).hashAlgorithm)
303           && hash.equals(((Pin) other).hash);
304     }
305
306     @Override public int hashCode() {
307       int result = 17;
308       result = 31 * result + pattern.hashCode();
309       result = 31 * result + hashAlgorithm.hashCode();
310       result = 31 * result + hash.hashCode();
311       return result;
312     }
313
314     @Override public String toString() {
315       return hashAlgorithm + hash.base64();
316     }
317   }
318
319   /** Builds a configured certificate pinner. */
320   public static final class Builder {
321     private final List<Pin> pins = new ArrayList<>();
322
323     /**
324      * Pins certificates for {@code pattern}.
325      *
326      * @param pattern lower-case host name or wildcard pattern such as {@code *.example.com}.
327      * @param pins SHA-256 or SHA-1 hashes. Each pin is a hash of a certificate's Subject Public Key
328      * Info, base64-encoded and prefixed with either {@code sha256/} or {@code sha1/}.
329      */

330     public Builder add(String pattern, String... pins) {
331       if (pattern == nullthrow new NullPointerException("pattern == null");
332
333       for (String pin : pins) {
334         this.pins.add(new Pin(pattern, pin));
335       }
336
337       return this;
338     }
339
340     public CertificatePinner build() {
341       return new CertificatePinner(new LinkedHashSet<>(pins), null);
342     }
343   }
344 }
345