Skip to content

Instantly share code, notes, and snippets.

@nuryslyrt
Created September 1, 2021 12:36
Show Gist options
  • Save nuryslyrt/b2e556afd74a71249ff1864f844067f3 to your computer and use it in GitHub Desktop.
Save nuryslyrt/b2e556afd74a71249ff1864f844067f3 to your computer and use it in GitHub Desktop.
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.net.ssl.SSLPeerUnverifiedException;
import okhttp3.internal.tls.CertificateChainCleaner;
import okio.ByteString;
import static okhttp3.internal.Util.equal;
public final class CertificatePinner {
public static final CertificatePinner DEFAULT = new Builder().build();
private final Set<Pin> pins;
private final CertificateChainCleaner certificateChainCleaner;
private CertificatePinner(Set<Pin> pins, CertificateChainCleaner certificateChainCleaner) {
this.pins = pins;
this.certificateChainCleaner = certificateChainCleaner;
}
@Override public boolean equals(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 {@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();
}
}
}
// 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 {@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.
*/
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 {@code certificateChainCleaner}. */
CertificatePinner withCertificateChainCleaner(CertificateChainCleaner certificateChainCleaner) {
return equal(this.certificateChainCleaner, certificateChainCleaner)
? this
: new CertificatePinner(pins, certificateChainCleaner);
}
/**
* 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 {@code example.com} or a pattern like {@code *.example.com}. */
final String pattern;
/** The canonical hostname, i.e. {@code EXAMPLE.com} becomes {@code example.com}. */
final String canonicalHostname;
/** Either {@code sha1/} or {@code sha256/}. */
final String 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.parse("http://" + pattern.substring(WILDCARD.length())).host()
: HttpUrl.parse("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.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. */
public static final class Builder {
private final List<Pin> pins = new ArrayList<>();
/**
* 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);
}
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment