/*
* Copyright (C) 2014 Square, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package okhttp3;
import java.security.cert.Certificate;
import java.security.cert.X509Certificate;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Set;
import javax.annotation.Nullable;
import javax.net.ssl.SSLPeerUnverifiedException;
import okhttp3.internal.tls.CertificateChainCleaner;
import okio.ByteString;
import static okhttp3.internal.Util.equal;
Constrains which certificates are trusted. Pinning certificates defends against attacks on
certificate authorities. It also prevents connections through man-in-the-middle certificate
authorities either known or unknown to the application's user.
This class currently pins a certificate's Subject Public Key Info as described on Adam Langley's Weblog. Pins are either base64 SHA-256 hashes as
in HTTP Public Key Pinning (HPKP) or SHA-1
base64 hashes as in Chromium's static certificates.
Setting up Certificate Pinning
The easiest way to pin a host is turn on pinning with a broken configuration and read the
expected configuration when the connection fails. Be sure to do this on a trusted network, and
without man-in-the-middle tools like Charles or Fiddler.
For example, to pin https://publicobject.com
, start with a broken configuration:
String hostname = "publicobject.com";
CertificatePinner certificatePinner = new CertificatePinner.Builder()
.add(hostname, "sha256/AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=")
.build();
OkHttpClient client = OkHttpClient.Builder()
.certificatePinner(certificatePinner)
.build();
Request request = new Request.Builder()
.url("https://" + hostname)
.build();
client.newCall(request).execute();
As expected, this fails with a certificate pinning exception:
javax.net.ssl.SSLPeerUnverifiedException: Certificate pinning failure!
Peer certificate chain:
sha256/afwiKY3RxoMmLkuRW1l7QsPZTJPwDS2pdDROQjXw8ig=: CN=publicobject.com, OU=PositiveSSL
sha256/klO23nT2ehFDXCfx3eHTDRESMz3asj1muO+4aIdjiuY=: CN=COMODO RSA Secure Server CA
sha256/grX4Ta9HpZx6tSHkmCrvpApTQGo67CYDnvprLg5yRME=: CN=COMODO RSA Certification Authority
sha256/lCppFqbkrlJ3EcVFAkeip0+44VaoJUymbnOaEUk7tEU=: CN=AddTrust External CA Root
Pinned certificates for publicobject.com:
sha256/AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=
at okhttp3.CertificatePinner.check(CertificatePinner.java)
at okhttp3.Connection.upgradeToTls(Connection.java)
at okhttp3.Connection.connect(Connection.java)
at okhttp3.Connection.connectAndSetOwner(Connection.java)
Follow up by pasting the public key hashes from the exception into the
certificate pinner's configuration:
CertificatePinner certificatePinner = new CertificatePinner.Builder()
.add("publicobject.com", "sha256/afwiKY3RxoMmLkuRW1l7QsPZTJPwDS2pdDROQjXw8ig=")
.add("publicobject.com", "sha256/klO23nT2ehFDXCfx3eHTDRESMz3asj1muO+4aIdjiuY=")
.add("publicobject.com", "sha256/grX4Ta9HpZx6tSHkmCrvpApTQGo67CYDnvprLg5yRME=")
.add("publicobject.com", "sha256/lCppFqbkrlJ3EcVFAkeip0+44VaoJUymbnOaEUk7tEU=")
.build();
Pinning is per-hostname and/or per-wildcard pattern. To pin both publicobject.com
and www.publicobject.com
, you must configure both hostnames. Wildcard pattern rules:
- Asterisk
*
is only permitted in the left-most domain name label and must be the only character in that label (i.e., must match the whole left-most label). For example, *.example.com
is permitted, while *a.example.com
, a*.example.com
, a*b.example.com
, a.*.example.com
are not permitted. - Asterisk
*
cannot match across domain name labels. For example, *.example.com
matches test.example.com
but does not match sub.test.example.com
. - Wildcard patterns for single-label domain names are not permitted.
If hostname pinned directly and via wildcard pattern, both direct and wildcard pins will be used. For example: *.example.com
pinned with pin1
and a.example.com
pinned with pin2
, to check a.example.com
both pin1
and pin2
will be used. Warning: Certificate Pinning is Dangerous!
Pinning certificates limits your server team's abilities to update their TLS certificates. By
pinning certificates, you take on additional operational complexity and limit your ability to
migrate between certificate authorities. Do not use certificate pinning without the blessing of
your server's TLS administrator!
Note about self-signed certificates
CertificatePinner
can not be used to pin self-signed certificate if such certificate is not accepted by TrustManager
.
See Also:
/**
* Constrains which certificates are trusted. Pinning certificates defends against attacks on
* certificate authorities. It also prevents connections through man-in-the-middle certificate
* authorities either known or unknown to the application's user.
*
* <p>This class currently pins a certificate's Subject Public Key Info as described on <a
* href="http://goo.gl/AIx3e5">Adam Langley's Weblog</a>. Pins are either base64 SHA-256 hashes as
* in <a href="http://tools.ietf.org/html/rfc7469">HTTP Public Key Pinning (HPKP)</a> or SHA-1
* base64 hashes as in Chromium's <a href="http://goo.gl/XDh6je">static certificates</a>.
*
* <h3>Setting up Certificate Pinning</h3>
*
* <p>The easiest way to pin a host is turn on pinning with a broken configuration and read the
* expected configuration when the connection fails. Be sure to do this on a trusted network, and
* without man-in-the-middle tools like <a href="http://charlesproxy.com">Charles</a> or <a
* href="http://fiddlertool.com">Fiddler</a>.
*
* <p>For example, to pin {@code https://publicobject.com}, start with a broken
* configuration: <pre> {@code
*
* String hostname = "publicobject.com";
* CertificatePinner certificatePinner = new CertificatePinner.Builder()
* .add(hostname, "sha256/AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=")
* .build();
* OkHttpClient client = OkHttpClient.Builder()
* .certificatePinner(certificatePinner)
* .build();
*
* Request request = new Request.Builder()
* .url("https://" + hostname)
* .build();
* client.newCall(request).execute();
* }</pre>
*
* As expected, this fails with a certificate pinning exception: <pre> {@code
*
* javax.net.ssl.SSLPeerUnverifiedException: Certificate pinning failure!
* Peer certificate chain:
* sha256/afwiKY3RxoMmLkuRW1l7QsPZTJPwDS2pdDROQjXw8ig=: CN=publicobject.com, OU=PositiveSSL
* sha256/klO23nT2ehFDXCfx3eHTDRESMz3asj1muO+4aIdjiuY=: CN=COMODO RSA Secure Server CA
* sha256/grX4Ta9HpZx6tSHkmCrvpApTQGo67CYDnvprLg5yRME=: CN=COMODO RSA Certification Authority
* sha256/lCppFqbkrlJ3EcVFAkeip0+44VaoJUymbnOaEUk7tEU=: CN=AddTrust External CA Root
* Pinned certificates for publicobject.com:
* sha256/AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=
* at okhttp3.CertificatePinner.check(CertificatePinner.java)
* at okhttp3.Connection.upgradeToTls(Connection.java)
* at okhttp3.Connection.connect(Connection.java)
* at okhttp3.Connection.connectAndSetOwner(Connection.java)
* }</pre>
*
* Follow up by pasting the public key hashes from the exception into the
* certificate pinner's configuration: <pre> {@code
*
* CertificatePinner certificatePinner = new CertificatePinner.Builder()
* .add("publicobject.com", "sha256/afwiKY3RxoMmLkuRW1l7QsPZTJPwDS2pdDROQjXw8ig=")
* .add("publicobject.com", "sha256/klO23nT2ehFDXCfx3eHTDRESMz3asj1muO+4aIdjiuY=")
* .add("publicobject.com", "sha256/grX4Ta9HpZx6tSHkmCrvpApTQGo67CYDnvprLg5yRME=")
* .add("publicobject.com", "sha256/lCppFqbkrlJ3EcVFAkeip0+44VaoJUymbnOaEUk7tEU=")
* .build();
* }</pre>
*
* Pinning is per-hostname and/or per-wildcard pattern. To pin both {@code publicobject.com} and
* {@code www.publicobject.com}, you must configure both hostnames.
*
* <p>Wildcard pattern rules:
* <ol>
* <li>Asterisk {@code *} is only permitted in the left-most domain name label and must be the
* only character in that label (i.e., must match the whole left-most label). For example,
* {@code *.example.com} is permitted, while {@code *a.example.com}, {@code a*.example.com},
* {@code a*b.example.com}, {@code a.*.example.com} are not permitted.
* <li>Asterisk {@code *} cannot match across domain name labels. For example,
* {@code *.example.com} matches {@code test.example.com} but does not match
* {@code sub.test.example.com}.
* <li>Wildcard patterns for single-label domain names are not permitted.
* </ol>
*
* If hostname pinned directly and via wildcard pattern, both direct and wildcard pins will be used.
* For example: {@code *.example.com} pinned with {@code pin1} and {@code a.example.com} pinned with
* {@code pin2}, to check {@code a.example.com} both {@code pin1} and {@code pin2} will be used.
*
* <h3>Warning: Certificate Pinning is Dangerous!</h3>
*
* <p>Pinning certificates limits your server team's abilities to update their TLS certificates. By
* pinning certificates, you take on additional operational complexity and limit your ability to
* migrate between certificate authorities. Do not use certificate pinning without the blessing of
* your server's TLS administrator!
*
* <h4>Note about self-signed certificates</h4>
*
* <p>{@link CertificatePinner} can not be used to pin self-signed certificate if such certificate
* is not accepted by {@link javax.net.ssl.TrustManager}.
*
* @see <a href="https://www.owasp.org/index.php/Certificate_and_Public_Key_Pinning"> OWASP:
* Certificate and Public Key Pinning</a>
*/
public final class CertificatePinner {
public static final CertificatePinner DEFAULT = new Builder().build();
private final Set<Pin> pins;
private final @Nullable CertificateChainCleaner certificateChainCleaner;
CertificatePinner(Set<Pin> pins, @Nullable CertificateChainCleaner certificateChainCleaner) {
this.pins = pins;
this.certificateChainCleaner = certificateChainCleaner;
}
@Override public boolean equals(@Nullable Object other) {
if (other == this) return true;
return other instanceof CertificatePinner
&& (equal(certificateChainCleaner, ((CertificatePinner) other).certificateChainCleaner)
&& pins.equals(((CertificatePinner) other).pins));
}
@Override public int hashCode() {
int result = certificateChainCleaner != null ? certificateChainCleaner.hashCode() : 0;
result = 31 * result + pins.hashCode();
return result;
}
Confirms that at least one of the certificates pinned for hostname
is in
peerCertificates
. Does nothing if there are no certificates pinned for hostname
. OkHttp calls this after a successful TLS handshake, but before the connection is used. Throws: - SSLPeerUnverifiedException – if
peerCertificates
don't match the certificates pinned for hostname
.
/**
* Confirms that at least one of the certificates pinned for {@code hostname} is in {@code
* peerCertificates}. Does nothing if there are no certificates pinned for {@code hostname}.
* OkHttp calls this after a successful TLS handshake, but before the connection is used.
*
* @throws SSLPeerUnverifiedException if {@code peerCertificates} don't match the certificates
* pinned for {@code hostname}.
*/
public void check(String hostname, List<Certificate> peerCertificates)
throws SSLPeerUnverifiedException {
List<Pin> pins = findMatchingPins(hostname);
if (pins.isEmpty()) return;
if (certificateChainCleaner != null) {
peerCertificates = certificateChainCleaner.clean(peerCertificates, hostname);
}
for (int c = 0, certsSize = peerCertificates.size(); c < certsSize; c++) {
X509Certificate x509Certificate = (X509Certificate) peerCertificates.get(c);
// Lazily compute the hashes for each certificate.
ByteString sha1 = null;
ByteString sha256 = null;
for (int p = 0, pinsSize = pins.size(); p < pinsSize; p++) {
Pin pin = pins.get(p);
if (pin.hashAlgorithm.equals("sha256/")) {
if (sha256 == null) sha256 = sha256(x509Certificate);
if (pin.hash.equals(sha256)) return; // Success!
} else if (pin.hashAlgorithm.equals("sha1/")) {
if (sha1 == null) sha1 = sha1(x509Certificate);
if (pin.hash.equals(sha1)) return; // Success!
} else {
throw new AssertionError("unsupported hashAlgorithm: " + pin.hashAlgorithm);
}
}
}
// If we couldn't find a matching pin, format a nice exception.
StringBuilder message = new StringBuilder()
.append("Certificate pinning failure!")
.append("\n Peer certificate chain:");
for (int c = 0, certsSize = peerCertificates.size(); c < certsSize; c++) {
X509Certificate x509Certificate = (X509Certificate) peerCertificates.get(c);
message.append("\n ").append(pin(x509Certificate))
.append(": ").append(x509Certificate.getSubjectDN().getName());
}
message.append("\n Pinned certificates for ").append(hostname).append(":");
for (int p = 0, pinsSize = pins.size(); p < pinsSize; p++) {
Pin pin = pins.get(p);
message.append("\n ").append(pin);
}
throw new SSLPeerUnverifiedException(message.toString());
}
Deprecated: replaced with check(String, List<Certificate>)
.
/** @deprecated replaced with {@link #check(String, List)}. */
public void check(String hostname, Certificate... peerCertificates)
throws SSLPeerUnverifiedException {
check(hostname, Arrays.asList(peerCertificates));
}
Returns list of matching certificates' pins for the hostname. Returns an empty list if the
hostname does not have pinned certificates.
/**
* Returns list of matching certificates' pins for the hostname. Returns an empty list if the
* hostname does not have pinned certificates.
*/
List<Pin> findMatchingPins(String hostname) {
List<Pin> result = Collections.emptyList();
for (Pin pin : pins) {
if (pin.matches(hostname)) {
if (result.isEmpty()) result = new ArrayList<>();
result.add(pin);
}
}
return result;
}
Returns a certificate pinner that uses certificateChainCleaner
. /** Returns a certificate pinner that uses {@code certificateChainCleaner}. */
CertificatePinner withCertificateChainCleaner(
@Nullable CertificateChainCleaner certificateChainCleaner) {
return equal(this.certificateChainCleaner, certificateChainCleaner)
? this
: new CertificatePinner(pins, certificateChainCleaner);
}
Returns the SHA-256 of certificate
's public key. In OkHttp 3.1.2 and earlier, this returned a SHA-1 hash of the public key. Both types are
supported, but SHA-256 is preferred.
/**
* Returns the SHA-256 of {@code certificate}'s public key.
*
* <p>In OkHttp 3.1.2 and earlier, this returned a SHA-1 hash of the public key. Both types are
* supported, but SHA-256 is preferred.
*/
public static String pin(Certificate certificate) {
if (!(certificate instanceof X509Certificate)) {
throw new IllegalArgumentException("Certificate pinning requires X509 certificates");
}
return "sha256/" + sha256((X509Certificate) certificate).base64();
}
static ByteString sha1(X509Certificate x509Certificate) {
return ByteString.of(x509Certificate.getPublicKey().getEncoded()).sha1();
}
static ByteString sha256(X509Certificate x509Certificate) {
return ByteString.of(x509Certificate.getPublicKey().getEncoded()).sha256();
}
static final class Pin {
private static final String WILDCARD = "*.";
A hostname like example.com
or a pattern like *.example.com
. /** A hostname like {@code example.com} or a pattern like {@code *.example.com}. */
final String pattern;
The canonical hostname, i.e. EXAMPLE.com
becomes example.com
. /** The canonical hostname, i.e. {@code EXAMPLE.com} becomes {@code example.com}. */
final String canonicalHostname;
Either sha1/
or sha256/
. /** Either {@code sha1/} or {@code sha256/}. */
final String hashAlgorithm;
The hash of the pinned certificate using hashAlgorithm
. /** The hash of the pinned certificate using {@link #hashAlgorithm}. */
final ByteString hash;
Pin(String pattern, String pin) {
this.pattern = pattern;
this.canonicalHostname = pattern.startsWith(WILDCARD)
? HttpUrl.get("http://" + pattern.substring(WILDCARD.length())).host()
: HttpUrl.get("http://" + pattern).host();
if (pin.startsWith("sha1/")) {
this.hashAlgorithm = "sha1/";
this.hash = ByteString.decodeBase64(pin.substring("sha1/".length()));
} else if (pin.startsWith("sha256/")) {
this.hashAlgorithm = "sha256/";
this.hash = ByteString.decodeBase64(pin.substring("sha256/".length()));
} else {
throw new IllegalArgumentException("pins must start with 'sha256/' or 'sha1/': " + pin);
}
if (this.hash == null) {
throw new IllegalArgumentException("pins must be base64: " + pin);
}
}
boolean matches(String hostname) {
if (pattern.startsWith(WILDCARD)) {
int firstDot = hostname.indexOf('.');
return (hostname.length() - firstDot - 1) == canonicalHostname.length()
&& hostname.regionMatches(false, firstDot + 1, canonicalHostname, 0,
canonicalHostname.length());
}
return hostname.equals(canonicalHostname);
}
@Override public boolean equals(Object other) {
return other instanceof Pin
&& pattern.equals(((Pin) other).pattern)
&& hashAlgorithm.equals(((Pin) other).hashAlgorithm)
&& hash.equals(((Pin) other).hash);
}
@Override public int hashCode() {
int result = 17;
result = 31 * result + pattern.hashCode();
result = 31 * result + hashAlgorithm.hashCode();
result = 31 * result + hash.hashCode();
return result;
}
@Override public String toString() {
return hashAlgorithm + hash.base64();
}
}
Builds a configured certificate pinner. /** Builds a configured certificate pinner. */
public static final class Builder {
private final List<Pin> pins = new ArrayList<>();
Pins certificates for pattern
. Params: - pattern – lower-case host name or wildcard pattern such as
*.example.com
. - pins – SHA-256 or SHA-1 hashes. Each pin is a hash of a certificate's Subject Public Key Info, base64-encoded and prefixed with either
sha256/
or sha1/
.
/**
* Pins certificates for {@code pattern}.
*
* @param pattern lower-case host name or wildcard pattern such as {@code *.example.com}.
* @param pins SHA-256 or SHA-1 hashes. Each pin is a hash of a certificate's Subject Public Key
* Info, base64-encoded and prefixed with either {@code sha256/} or {@code sha1/}.
*/
public Builder add(String pattern, String... pins) {
if (pattern == null) throw new NullPointerException("pattern == null");
for (String pin : pins) {
this.pins.add(new Pin(pattern, pin));
}
return this;
}
public CertificatePinner build() {
return new CertificatePinner(new LinkedHashSet<>(pins), null);
}
}
}