/*
 * 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 com.fq.threelib.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 com.fq.threelib.okhttp3.internal.tls.CertificateChainCleaner;
import com.fq.threelib.okio.ByteString;

import static com.fq.threelib.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.
 *
 * <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 {@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 {@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(
      @Nullable 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.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. */
  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);
    }
  }
}
