All Downloads are FREE. Search and download functionalities are using the official Maven repository.

jvmMain.okhttp3.CertificatePinner.kt Maven / Gradle / Ivy

There is a newer version: 5.0.0-alpha.14
Show newest version
/*
 * 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 javax.net.ssl.SSLPeerUnverifiedException
import okhttp3.internal.filterList
import okhttp3.internal.tls.CertificateChainCleaner
import okhttp3.internal.toCanonicalHost
import okio.ByteString
import okio.ByteString.Companion.decodeBase64
import okio.ByteString.Companion.toByteString

/**
 * 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][langley]. Pins are either base64 SHA-256 hashes as in
 * [HTTP Public Key Pinning (HPKP)][rfc_7469] or SHA-1 base64 hashes as in Chromium's
 * [static certificates][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][charles] or [Fiddler][fiddler].
 *
 * For example, to pin `https://publicobject.com`, start with a broken configuration:
 *
 * ```java
 * 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:
 *
 * ```java
 * 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:
 *
 * ```java
 * 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();
 * ```
 *
 * ## Domain Patterns
 *
 * Pinning is per-hostname and/or per-wildcard pattern. To pin both `publicobject.com` and
 * `www.publicobject.com` you must configure both hostnames. Or you may use patterns to match
 * sets of related domain names. The following forms are permitted:
 *
 *  * **Full domain name**: you may pin an exact domain name like `www.publicobject.com`. It won't
 *    match additional prefixes (`us-west.www.publicobject.com`) or suffixes (`publicobject.com`).
 *
 *  * **Any number of subdomains**: Use two asterisks to like `**.publicobject.com` to match any
 *    number of prefixes (`us-west.www.publicobject.com`, `www.publicobject.com`) including no
 *    prefix at all (`publicobject.com`). For most applications this is the best way to configure
 *    certificate pinning.
 *
 *  * **Exactly one subdomain**: Use a single asterisk like `*.publicobject.com` to match exactly
 *    one prefix (`www.publicobject.com`, `api.publicobject.com`). Be careful with this approach as
 *    no pinning will be enforced if additional prefixes are present, or if no prefixes are present.
 *
 * Note that any other form is unsupported. You may not use asterisks in any position other than
 * the leftmost label.
 *
 * If multiple patterns match a hostname, any match is sufficient. For example, suppose pin A
 * applies to `*.publicobject.com` and pin B applies to `api.publicobject.com`. Handshakes for
 * `api.publicobject.com` are valid if either A's or B's certificate is in the chain.
 *
 * ## 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 [javax.net.ssl.TrustManager].
 *
 * See also [OWASP: Certificate and Public Key Pinning][owasp].
 *
 * [charles]: http://charlesproxy.com
 * [fiddler]: http://fiddlertool.com
 * [langley]: http://goo.gl/AIx3e5
 * [owasp]: https://www.owasp.org/index.php/Certificate_and_Public_Key_Pinning
 * [rfc_7469]: http://tools.ietf.org/html/rfc7469
 * [static_certificates]: http://goo.gl/XDh6je
 */
@Suppress("NAME_SHADOWING")
class CertificatePinner internal constructor(
  val pins: Set,
  internal val certificateChainCleaner: CertificateChainCleaner? = null
) {
  /**
   * 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`.
   */
  @Throws(SSLPeerUnverifiedException::class)
  fun check(hostname: String, peerCertificates: List) {
    return check(hostname) {
      (certificateChainCleaner?.clean(peerCertificates, hostname) ?: peerCertificates)
          .map { it as X509Certificate }
    }
  }

  internal fun check(hostname: String, cleanedPeerCertificatesFn: () -> List) {
    val pins = findMatchingPins(hostname)
    if (pins.isEmpty()) return

    val peerCertificates = cleanedPeerCertificatesFn()

    for (peerCertificate in peerCertificates) {
      // Lazily compute the hashes for each certificate.
      var sha1: ByteString? = null
      var sha256: ByteString? = null

      for (pin in pins) {
        when (pin.hashAlgorithm) {
          "sha256" -> {
            if (sha256 == null) sha256 = peerCertificate.sha256Hash()
            if (pin.hash == sha256) return // Success!
          }
          "sha1" -> {
            if (sha1 == null) sha1 = peerCertificate.sha1Hash()
            if (pin.hash == sha1) return // Success!
          }
          else -> throw AssertionError("unsupported hashAlgorithm: ${pin.hashAlgorithm}")
        }
      }
    }

    // If we couldn't find a matching pin, format a nice exception.
    val message = buildString {
      append("Certificate pinning failure!")
      append("\n  Peer certificate chain:")
      for (element in peerCertificates) {
        append("\n    ")
        append(pin(element))
        append(": ")
        append(element.subjectDN.name)
      }
      append("\n  Pinned certificates for ")
      append(hostname)
      append(":")
      for (pin in pins) {
        append("\n    ")
        append(pin)
      }
    }
    throw SSLPeerUnverifiedException(message)
  }

  @Deprecated(
      "replaced with {@link #check(String, List)}.",
      ReplaceWith("check(hostname, peerCertificates.toList())")
  )
  @Throws(SSLPeerUnverifiedException::class)
  fun check(hostname: String, vararg peerCertificates: Certificate) {
    check(hostname, peerCertificates.toList())
  }

  /**
   * Returns list of matching certificates' pins for the hostname. Returns an empty list if the
   * hostname does not have pinned certificates.
   */
  fun findMatchingPins(hostname: String): List = pins.filterList { matchesHostname(hostname) }

  /** Returns a certificate pinner that uses `certificateChainCleaner`. */
  internal fun withCertificateChainCleaner(
    certificateChainCleaner: CertificateChainCleaner
  ): CertificatePinner {
    return if (this.certificateChainCleaner == certificateChainCleaner) {
      this
    } else {
      CertificatePinner(pins, certificateChainCleaner)
    }
  }

  override fun equals(other: Any?): Boolean {
    return other is CertificatePinner &&
        other.pins == pins &&
        other.certificateChainCleaner == certificateChainCleaner
  }

  override fun hashCode(): Int {
    var result = 37
    result = 41 * result + pins.hashCode()
    result = 41 * result + certificateChainCleaner.hashCode()
    return result
  }

  /** A hostname pattern and certificate hash for Certificate Pinning. */
  class Pin(pattern: String, pin: String) {
    /** A hostname like `example.com` or a pattern like `*.example.com` (canonical form). */
    val pattern: String

    /** Either `sha1` or `sha256`. */
    val hashAlgorithm: String

    /** The hash of the pinned certificate using [hashAlgorithm]. */
    val hash: ByteString

    init {
      require((pattern.startsWith("*.") && pattern.indexOf("*", 1) == -1) ||
          (pattern.startsWith("**.") && pattern.indexOf("*", 2) == -1) ||
          pattern.indexOf("*") == -1) {
        "Unexpected pattern: $pattern"
      }

      this.pattern =
        pattern.toCanonicalHost() ?: throw IllegalArgumentException("Invalid pattern: $pattern")

      when {
        pin.startsWith("sha1/") -> {
          this.hashAlgorithm = "sha1"
          this.hash = pin.substring("sha1/".length).decodeBase64() ?: throw IllegalArgumentException("Invalid pin hash: $pin")
        }
        pin.startsWith("sha256/") -> {
          this.hashAlgorithm = "sha256"
          this.hash = pin.substring("sha256/".length).decodeBase64() ?: throw IllegalArgumentException("Invalid pin hash: $pin")
        }
        else -> throw IllegalArgumentException("pins must start with 'sha256/' or 'sha1/': $pin")
      }
    }

    fun matchesHostname(hostname: String): Boolean {
      return when {
        pattern.startsWith("**.") -> {
          // With ** empty prefixes match so exclude the dot from regionMatches().
          val suffixLength = pattern.length - 3
          val prefixLength = hostname.length - suffixLength
          hostname.regionMatches(hostname.length - suffixLength, pattern, 3, suffixLength) &&
              (prefixLength == 0 || hostname[prefixLength - 1] == '.')
        }
        pattern.startsWith("*.") -> {
          // With * there must be a prefix so include the dot in regionMatches().
          val suffixLength = pattern.length - 1
          val prefixLength = hostname.length - suffixLength
          hostname.regionMatches(hostname.length - suffixLength, pattern, 1, suffixLength) &&
              hostname.lastIndexOf('.', prefixLength - 1) == -1
        }
        else -> hostname == pattern
      }
    }

    fun matchesCertificate(certificate: X509Certificate): Boolean {
        return when (hashAlgorithm) {
          "sha256" -> hash == certificate.sha256Hash()
          "sha1" -> hash == certificate.sha1Hash()
          else -> false
        }
    }

    override fun toString(): String = "$hashAlgorithm/${hash.base64()}"

    override fun equals(other: Any?): Boolean {
      if (this === other) return true
      if (other !is Pin) return false

      if (pattern != other.pattern) return false
      if (hashAlgorithm != other.hashAlgorithm) return false
      if (hash != other.hash) return false

      return true
    }

    override fun hashCode(): Int {
      var result = pattern.hashCode()
      result = 31 * result + hashAlgorithm.hashCode()
      result = 31 * result + hash.hashCode()
      return result
    }
  }

  /** Builds a configured certificate pinner. */
  class Builder {
    val pins = mutableListOf()

    /**
     * Pins certificates for `pattern`.
     *
     * @param pattern lower-case host name or wildcard pattern such as `*.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 `sha256/` or `sha1/`.
     */
    fun add(pattern: String, vararg pins: String) = apply {
      for (pin in pins) {
        this.pins.add(Pin(pattern, pin))
      }
    }

    fun build(): CertificatePinner = CertificatePinner(pins.toSet())
  }

  companion object {
    @JvmField
    val DEFAULT = Builder().build()

    @JvmStatic
    fun X509Certificate.sha1Hash(): ByteString =
      publicKey.encoded.toByteString().sha1()

    @JvmStatic
    fun X509Certificate.sha256Hash(): ByteString =
      publicKey.encoded.toByteString().sha256()

    /**
     * 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.
     */
    @JvmStatic
    fun pin(certificate: Certificate): String {
      require(certificate is X509Certificate) { "Certificate pinning requires X509 certificates" }
      return "sha256/${certificate.sha256Hash().base64()}"
    }
  }
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy