1. 程式人生 > >讀OkHttp3原始碼(二):CertificatePinner(鎖定證書)

讀OkHttp3原始碼(二):CertificatePinner(鎖定證書)

okhttp3 public final class CertificatePinner extends Object

類介紹:

該類用於約束哪些證書是可信的。 鎖定證書可以防止對證書頒發機構相關的攻擊。 它還阻止通過使用者已知或未知的中間證書頒發機構建立的連線。 這個類目前鎖定了一個證書的主題公鑰資訊,如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.萬用字元模式規則:

  • 星號*只允許出現在最左邊的域名標籤中,並且必須是該標籤(即,必須匹配整個最左邊的標籤)。例如,允許*.example.com,而*a.example.com, a*.example.com, a b.example.com, a.*.example.com不允許。
  • 星號@{code *}不能跨域名標籤匹配。例如,*.example.com匹配test.example.com,但不匹配sub.test.example.com。
  • 不允許為單標籤域名使用萬用字元模式。
  • 如果主機名直接或通過萬用字元模式鎖定,將使用直接或萬用字元固定。 例如:*.example.com用pin1pin1固定,a.example.com用pin2固定,檢查a.example.com將使用pin1和pin2

3.警告: 證書鎖定是危險的!

鎖定證書限制了伺服器團隊更新TLS證書的能力。通過鎖定證書,可以增加操作複雜性,並限制在證書頒發機構之間遷移的能力。如果沒有伺服器的TLS管理員的許可,不要使用證書固定!

成員變數:

1.private final List<Pin> pins;

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握手成功後,建立連線之前呼叫。

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

    /**
     * @deprecated 被 {@link #check(String, List)}替換.
     */
    public void check(String hostname, Certificate... peerCertificates)
            throws SSLPeerUnverifiedException {
        check(hostname, Arrays.asList(peerCertificates));
    }

    /**
     * 返回匹配主機名的鎖定證書列表。如果主機名沒有鎖定證書,則返回空列表。
     */
    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;
    }

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