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 == this) return 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 == null) throw 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