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.ArrayList;
22import java.util.Arrays;
23import java.util.LinkedHashMap;
24import java.util.List;
25import java.util.Map;
26import javax.net.ssl.SSLPeerUnverifiedException;
27import okio.ByteString;
28
29import static java.util.Collections.unmodifiableList;
30
31/**
32 * Constrains which certificates are trusted. Pinning certificates defends
33 * against attacks on certificate authorities. It also prevents connections
34 * through man-in-the-middle certificate authorities either known or unknown to
35 * the application's user.
36 *
37 * <p>This class currently pins a certificate's Subject Public Key Info as
38 * described on <a href="http://goo.gl/AIx3e5">Adam Langley's Weblog</a>. Pins
39 * are base-64 SHA-1 hashes, consistent with the format Chromium uses for <a
40 * href="http://goo.gl/XDh6je">static certificates</a>. See Chromium's <a
41 * href="http://goo.gl/4CCnGs">pinsets</a> for hostnames that are pinned in that
42 * browser.
43 *
44 * <h3>Setting up Certificate Pinning</h3>
45 * The easiest way to pin a host is turn on pinning with a broken configuration
46 * and read the expected configuration when the connection fails. Be sure to
47 * do this on a trusted network, and without man-in-the-middle tools like <a
48 * href="http://charlesproxy.com">Charles</a> or <a
49 * href="http://fiddlertool.com">Fiddler</a>.
50 *
51 * <p>For example, to pin {@code https://publicobject.com}, start with a broken
52 * configuration: <pre>   {@code
53 *
54 *     String hostname = "publicobject.com";
55 *     CertificatePinner certificatePinner = new CertificatePinner.Builder()
56 *         .add(hostname, "sha1/BOGUSPIN")
57 *         .build();
58 *     OkHttpClient client = new OkHttpClient();
59 *     client.setCertificatePinner(certificatePinner);
60 *
61 *     Request request = new Request.Builder()
62 *         .url("https://" + hostname)
63 *         .build();
64 *     client.newCall(request).execute();
65 * }</pre>
66 *
67 * As expected, this fails with a certificate pinning exception: <pre>   {@code
68 *
69 * javax.net.ssl.SSLPeerUnverifiedException: Certificate pinning failure!
70 *   Peer certificate chain:
71 *     sha1/DmxUShsZuNiqPQsX2Oi9uv2sCnw=: CN=publicobject.com, OU=PositiveSSL
72 *     sha1/SXxoaOSEzPC6BgGmxAt/EAcsajw=: CN=COMODO RSA Domain Validation Secure Server CA
73 *     sha1/blhOM3W9V/bVQhsWAcLYwPU6n24=: CN=COMODO RSA Certification Authority
74 *     sha1/T5x9IXmcrQ7YuQxXnxoCmeeQ84c=: CN=AddTrust External CA Root
75 *   Pinned certificates for publicobject.com:
76 *     sha1/BOGUSPIN
77 *   at com.squareup.okhttp.CertificatePinner.check(CertificatePinner.java)
78 *   at com.squareup.okhttp.Connection.upgradeToTls(Connection.java)
79 *   at com.squareup.okhttp.Connection.connect(Connection.java)
80 *   at com.squareup.okhttp.Connection.connectAndSetOwner(Connection.java)
81 * }</pre>
82 *
83 * Follow up by pasting the public key hashes from the exception into the
84 * certificate pinner's configuration: <pre>   {@code
85 *
86 *     CertificatePinner certificatePinner = new CertificatePinner.Builder()
87 *       .add("publicobject.com", "sha1/DmxUShsZuNiqPQsX2Oi9uv2sCnw=")
88 *       .add("publicobject.com", "sha1/SXxoaOSEzPC6BgGmxAt/EAcsajw=")
89 *       .add("publicobject.com", "sha1/blhOM3W9V/bVQhsWAcLYwPU6n24=")
90 *       .add("publicobject.com", "sha1/T5x9IXmcrQ7YuQxXnxoCmeeQ84c=")
91 *       .build();
92 * }</pre>
93 *
94 * Pinning is per-hostname. To pin both {@code publicobject.com} and {@code
95 * www.publicobject.com}, you must configure both hostnames.
96 *
97 * <h3>Warning: Certificate Pinning is Dangerous!</h3>
98 * Pinning certificates limits your server team's abilities to update their TLS
99 * certificates. By pinning certificates, you take on additional operational
100 * complexity and limit your ability to migrate between certificate authorities.
101 * Do not use certificate pinning without the blessing of your server's TLS
102 * administrator!
103 *
104 * <h4>Note about self-signed certificates</h4>
105 * {@link CertificatePinner} can not be used to pin self-signed certificate
106 * if such certificate is not accepted by {@link javax.net.ssl.TrustManager}.
107 *
108 * @see <a href="https://www.owasp.org/index.php/Certificate_and_Public_Key_Pinning">
109 *     OWASP: Certificate and Public Key Pinning</a>
110 */
111public final class CertificatePinner {
112  public static final CertificatePinner DEFAULT = new Builder().build();
113
114  private final Map<String, List<ByteString>> hostnameToPins;
115
116  private CertificatePinner(Builder builder) {
117    hostnameToPins = Util.immutableMap(builder.hostnameToPins);
118  }
119
120  /**
121   * Confirms that at least one of the certificates pinned for {@code hostname}
122   * is in {@code peerCertificates}. Does nothing if there are no certificates
123   * pinned for {@code hostname}. OkHttp calls this after a successful TLS
124   * handshake, but before the connection is used.
125   *
126   * @throws SSLPeerUnverifiedException if {@code peerCertificates} don't match
127   *     the certificates pinned for {@code hostname}.
128   */
129  public void check(String hostname, List<Certificate> peerCertificates)
130      throws SSLPeerUnverifiedException {
131    List<ByteString> pins = hostnameToPins.get(hostname);
132    if (pins == null) return;
133
134    for (int i = 0, size = peerCertificates.size(); i < size; i++) {
135      X509Certificate x509Certificate = (X509Certificate) peerCertificates.get(i);
136      if (pins.contains(sha1(x509Certificate))) return; // Success!
137    }
138
139    // If we couldn't find a matching pin, format a nice exception.
140    StringBuilder message = new StringBuilder()
141        .append("Certificate pinning failure!")
142        .append("\n  Peer certificate chain:");
143    for (int i = 0, size = peerCertificates.size(); i < size; i++) {
144      X509Certificate x509Certificate = (X509Certificate) peerCertificates.get(i);
145      message.append("\n    ").append(pin(x509Certificate))
146          .append(": ").append(x509Certificate.getSubjectDN().getName());
147    }
148    message.append("\n  Pinned certificates for ").append(hostname).append(":");
149    for (int i = 0, size = pins.size(); i < size; i++) {
150      ByteString pin = pins.get(i);
151      message.append("\n    sha1/").append(pin.base64());
152    }
153    throw new SSLPeerUnverifiedException(message.toString());
154  }
155
156  /** @deprecated replaced with {@link #check(String, List)}. */
157  public void check(String hostname, Certificate... peerCertificates)
158      throws SSLPeerUnverifiedException {
159    check(hostname, Arrays.asList(peerCertificates));
160  }
161
162  /**
163   * Returns the SHA-1 of {@code certificate}'s public key. This uses the
164   * mechanism Moxie Marlinspike describes in <a
165   * href="https://github.com/moxie0/AndroidPinning">Android Pinning</a>.
166   */
167  public static String pin(Certificate certificate) {
168    if (!(certificate instanceof X509Certificate)) {
169      throw new IllegalArgumentException("Certificate pinning requires X509 certificates");
170    }
171    return "sha1/" + sha1((X509Certificate) certificate).base64();
172  }
173
174  private static ByteString sha1(X509Certificate x509Certificate) {
175    return Util.sha1(ByteString.of(x509Certificate.getPublicKey().getEncoded()));
176  }
177
178  /** Builds a configured certificate pinner. */
179  public static final class Builder {
180    private final Map<String, List<ByteString>> hostnameToPins = new LinkedHashMap<>();
181
182    /**
183     * Pins certificates for {@code hostname}. Each pin is a SHA-1 hash of a
184     * certificate's Subject Public Key Info, base64-encoded and prefixed with
185     * "sha1/".
186     */
187    public Builder add(String hostname, String... pins) {
188      if (hostname == null) throw new IllegalArgumentException("hostname == null");
189
190      List<ByteString> hostPins = new ArrayList<>();
191      List<ByteString> previousPins = hostnameToPins.put(hostname, unmodifiableList(hostPins));
192      if (previousPins != null) {
193        hostPins.addAll(previousPins);
194      }
195
196      for (String pin : pins) {
197        if (!pin.startsWith("sha1/")) {
198          throw new IllegalArgumentException("pins must start with 'sha1/': " + pin);
199        }
200        ByteString decodedPin = ByteString.decodeBase64(pin.substring("sha1/".length()));
201        if (decodedPin == null) {
202          throw new IllegalArgumentException("pins must be base64: " + pin);
203        }
204        hostPins.add(decodedPin);
205      }
206
207      return this;
208    }
209
210    public CertificatePinner build() {
211      return new CertificatePinner(this);
212    }
213  }
214}
215