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
,请从一个错误的配置开始
1 2 3 4 5 6 7 8 9 10 11 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();
正如预期的那样,以一个证书锁定异常而失败了:
1 2 3 4 5 6 7 8 9 10 11 12 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的配置中
1 2 3 4 5 6 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.com
和www.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.com
用pin1
固定,a.example.com
用pin2
固定,检查a.example.com
将使用pin1
和pin2
警告: 证书锁定是危险的! 锁定证书限制了服务器团队更新TLS证书的能力。通过锁定证书,可以增加操作复杂性,并限制在证书颁发机构之间迁移的能力。如果没有服务器的TLS管理员的许可,不要使用证书固定!
静态内部类Pin Pin是CertificatePinner的静态内部类。直接上源码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 static final class Pin { final String pattern; final String 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的构造使用的构造器模式,添加方法在构造类里面:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 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 ; }
构造类的完整代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 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 ; } 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的地方就是:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 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 ; } else if (pin.hashAlgorithm.equals("sha1/" )) { if (sha1 == null ) sha1 = sha1(x509Certificate); if (pin.hash.equals(sha1)) return ; } 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握手成功后,建立连接之前调用。