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