该类用于约束哪些证书是可信的。 锁定证书可以防止对证书颁发机构相关的攻击。 它还阻止通过用户已知或未知的中间证书颁发机构建立的连接。 这个类目前锁定了一个证书的主题公钥信息,如Adam Langley的博客所述。公钥不是HTTP公钥锁定(HPKP)中的base64 SHA-256哈希,就是Chromium静态证书中的SHA-1 base64哈希。 HTTP Public Key Pinning (HPKP) Chromium静态证书 。
1.设置固定证书:
理解锁定主机最简单的方法是打开错误配置的锁定,并在连接失败时读取预期配置。 一定要在可信的网络上完成,不要使用像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.com和www.publicobject.com,必须配置这两个主机名。
2.通配符模式规则:
3.警告: 证书锁定是危险的!
锁定证书限制了服务器团队更新TLS证书的能力。通过锁定证书,可以增加操作复杂性,并限制在证书颁发机构之间迁移的能力。如果没有服务器的TLS管理员的许可,不要使用证书固定!
1.private final List
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 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 peerCertificates)
throws SSLPeerUnverifiedException {
List 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握手成功后,建立连接之前调用。
private final TrustRootIndex trustRootIndex; 可信根索引
public static final CertificatePinner DEFAULT = new Builder().build();
最后贴上所有源码:
public final class CertificatePinner {
public static final CertificatePinner DEFAULT = new Builder().build();
private final List pins;
private final TrustRootIndex trustRootIndex;
private CertificatePinner(Builder builder) {
this.pins = Util.immutableList(builder.pins);
this.trustRootIndex = builder.trustRootIndex;
}
/**
* 确认锁定主机名的至少一个证书在peerCertificates中。如果没有锁定主机名的证书,则什么也不做。OkHttp在TLS握手成功后调用,但在连接之前调用。
* @throws SSLPeerUnverifiedException 不匹配锁定主机名的证书。
*/
public void check(String hostname, List peerCertificates)
throws SSLPeerUnverifiedException {
List 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());
}
/**
* @deprecated 被 {@link #check(String, List)}替换.
*/
public void check(String hostname, Certificate... peerCertificates)
throws SSLPeerUnverifiedException {
check(hostname, Arrays.asList(peerCertificates));
}
/**
* 返回匹配主机名的锁定证书列表。如果主机名没有锁定证书,则返回空列表。
*/
List findMatchingPins(String hostname) {
List result = Collections.emptyList();
for (Pin pin : pins) {
if (pin.matches(hostname)) {
if (result.isEmpty()) result = new ArrayList<>();
result.add(pin);
}
}
return result;
}
Builder newBuilder() {
return new Builder(this);
}
/**
* Returns the SHA-256 of {@code certificate}'s public key.
*
* 在OkHttp 3.1.2及之前版本中,这返回了公钥的SHA-1散列。这两种类型都受支持,但SHA-256是首选。
*/
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 Util.sha1(ByteString.of(x509Certificate.getPublicKey().getEncoded()));
}
static ByteString sha256(X509Certificate x509Certificate) {
return Util.sha256(ByteString.of(x509Certificate.getPublicKey().getEncoded()));
}
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();
}
}
/**
* 构建已配置的固定证书。
*/
public static final class Builder {
private final List 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);
}
}
}