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