OKHttp锁定证书CertificatePinner

OKHttp的CertificatePinner类用于约束哪些证书是可信的。锁定证书可以防止对证书颁发机构相关的攻击。它还阻止通过用户已知或未知的中间证书颁发机构建立的连接。这个类目前锁定了一个证书的主题公钥信息,如Adam Langley的博客所述。公钥不是HTTP公钥锁定(HPKP)中的base64 SHA-256哈希,就是Chromium静态证书中的SHA-1 base64哈希。HTTP Public Key Pinning (HPKP) Chromium静态证书。

设置固定证书

理解锁定主机最简单的方法是打开错误配置的锁定,并在连接失败时读取预期配置。一定要在可信的网络上完成,不要使用像Charles或Fiddler这样的中间工具。例如,要锁定:https://publicobject.com,请从一个错误的配置开始

String hostname = "publicobject.com";
     CertificatePinner certificatePinner = new CertificatePinner.Builder()
         .add(hostname, "sha256/AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=")
         .build();
     OkHttpClient client = new OkHttpClient();
     client.setCertificatePinner(certificatePinner);

     Request request = new Request.Builder()
         .url("https://" + hostname)
         .build();
     client.newCall(request).execute();

正如预期的那样,以一个证书锁定异常而失败了:

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)

接下来,将异常中的公钥散列粘贴到证书pinner的配置中

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是每个主机名和/或每个通配符模式。要同时使用publicobject.comwww.publicobject.com,必须配置这两个主机名。

通配符模式规则

  • 星号*只允许出现在最左边的域名标签中,并且必须是该标签(即必须匹配整个最左边的标签)。例如,允许*.example.com,而*a.example.com, a*.example.com, a*b.example.com, a.*.example.com不允许
  • 星号*不能跨域名标签匹配。例如:*.example.com匹配test.example.com,但不匹配sub.test.example.com
  • 不允许为单标签域名使用通配符模式
    如果主机名直接或通过通配符模式锁定,将使用直接或通配符固定。例如:*.example.compin1固定,a.example.compin2固定,检查a.example.com将使用pin1pin2

警告: 证书锁定是危险的!

锁定证书限制了服务器团队更新TLS证书的能力。通过锁定证书,可以增加操作复杂性,并限制在证书颁发机构之间迁移的能力。如果没有服务器的TLS管理员的许可,不要使用证书固定!

静态内部类Pin

Pin是CertificatePinner的静态内部类。直接上源码:

static final class Pin {
    /**
     * 主机名,如example.com或如*.example.com的一种形式。
     */
    final String pattern;
    /**
     * 或者sha1/或者sha256/.
     */
    final String hashAlgorithm;
    /**
     * 使用{@link #hashAlgorithm}的固定证书的哈希。
     */
    final ByteString hash;

    Pin(String pattern, String pin) {
        this.pattern = pattern;
        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.equals(hostname)) return true;

        int firstDot = hostname.indexOf('.');
        return pattern.startsWith("*.")
                && hostname.regionMatches(false, firstDot + 1, pattern, 2, pattern.length() - 2);
    }

    @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();
    }
}

看代码以及注释不难理解Pin类就是锁定证书类。Pin类中,直接主机名或主机名通配符、哈希算法、哈希码一一对应。

pins这个成员变量是个list集合,那么是怎么维护的呢。首先看添加:

CertificatePinner的构造使用的构造器模式,添加方法在构造类里面:

/**
 * 为{@code pattern}添加固定证书。
 *
 * @param pattern 小写主机名或通配符模式(如*.example.com)。
 * @param pins SHA-256或SHA-1哈希。每个pin都是证书主题公钥信息的散列,以base64编码,前缀为sha256/或sha1/。
 */
public Builder add(String pattern, String... pins) {
    if (pattern == null) throw new IllegalArgumentException("pattern == null");

    for (String pin : pins) {
        this.pins.add(new Pin(pattern, pin));
    }

    return this;
}

构造类的完整代码:

public static final class Builder {
    private final List<Pin> pins = new ArrayList<>();
    private TrustRootIndex trustRootIndex;

    public Builder() {
    }

    Builder(CertificatePinner certificatePinner) {
        this.pins.addAll(certificatePinner.pins);
        this.trustRootIndex = certificatePinner.trustRootIndex;
    }

    public Builder trustRootIndex(TrustRootIndex trustRootIndex) {
        this.trustRootIndex = trustRootIndex;
        return this;
    }

    /**
     * 为{@code pattern}添加固定证书。
     *
     * @param pattern 小写主机名或通配符模式(如*.example.com)。
     * @param pins SHA-256或SHA-1哈希。每个pin都是证书主题公钥信息的散列,以base64编码,前缀为sha256/或sha1/。
     */
    public Builder add(String pattern, String... pins) {
        if (pattern == null) throw new IllegalArgumentException("pattern == null");

        for (String pin : pins) {
            this.pins.add(new Pin(pattern, pin));
        }

        return this;
    }

    public CertificatePinner build() {
        return new CertificatePinner(this);
    }
}

其次用到成员变量pins的地方就是:

public void check(String hostname, List<Certificate> peerCertificates)
        throws SSLPeerUnverifiedException {
    List<Pin> pins = findMatchingPins(hostname);
    if (pins.isEmpty()) return;

    if (trustRootIndex != null) {
        peerCertificates = new CertificateChainCleaner(trustRootIndex).clean(peerCertificates);
    }

    for (int c = 0, certsSize = peerCertificates.size(); c < certsSize; c++) {
        X509Certificate x509Certificate = (X509Certificate) peerCertificates.get(c);

        //懒惰地计算每个证书的散列。
        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();
            }
        }
    }

    //如果我们找不到匹配的锁定证书,抛出异常。
    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());
}

check方法的作用是确认锁定主机名的至少一个证书在peerCertificates中。如果没有锁定主机名的证书,则什么也不做。OkHttp在TLS握手成功后,建立连接之前调用。

版权声明:
作者:Joe.Ye
链接:https://www.appblog.cn/index.php/2023/03/25/okhttp-lock-certificate-certificate-pinner/
来源:APP全栈技术分享
文章版权归作者所有,未经允许请勿转载。

THE END
分享
二维码
打赏
海报
OKHttp锁定证书CertificatePinner
OKHttp的CertificatePinner类用于约束哪些证书是可信的。锁定证书可以防止对证书颁发机构相关的攻击。它还阻止通过用户已知或未知的中间证书颁发机构建立的连……
<<上一篇
下一篇>>
文章目录
关闭
目 录