[转]Android 根证书管理与证书验证(4)

转自 https://sq.163yun.com/blog/article/188783289427574784

私有 CA 签名证书的应用

自签名证书是无需别的证书为其签名来证明其合法性的证书,根证书都是自签名证书。私有 CA 签名证书则是指,为域名证书签名的 CA,其合法有效性没有得到广泛的认可,该 CA 的根证书没有被内置到系统中。

在实际的开发过程中,有时为了节省昂贵的购买证书的费用,而想要自己给自己的服务器的域名签发域名证书,这即是私有 CA 签名的证书。为了能够使用这种证书,需要在客户端预埋根证书,并对客户端证书合法性验证的过程进行干预,通过我们预埋的根证书为服务端的证书做合法性验证,而不依赖系统的根证书库。

自定义 javax.net.ssl.SSLSocket 的代价太高,通常不会通过自定义 javax.net.ssl.SSLSocket 来修改服务端证书的合法性验证过程。以此为基础,从上面的分析中不难看出,要想定制 OpenSSLSocketImpl 的证书验证过程,则必然要改变 SSLParametersImpl,要改变 OpenSSLSocketImplSSLParametersImpl,则必然需要修改 SSLSocketFactory。修改 SSLSocketFactory 常常是一个不错的方法。

在 Java 中,SSLContext 正是被设计用于这一目的。创建定制了 SSLParametersImpl,即定制了 TrustManagerSSLSocketFactory 的方法如下:

        TrustManager[] trustManagers = new TrustManager[] { new HelloX509TrustManager() };;

        SSLContext context = null;
        try {
            context = SSLContext.getInstance("TLS");
            context.init(null, trustManagers, new SecureRandom());
        } catch (NoSuchAlgorithmException e) {
            Log.i(TAG,"NoSuchAlgorithmException INFO:"+e.getMessage());
        } catch (KeyManagementException e) {
            Log.i(TAG, "KeyManagementException INFO:" + e.getMessage());
        }

        HttpsURLConnection.setDefaultSSLSocketFactory(context.getSocketFactory());

SSLContext 的相关方法实现(位于libcore/ojluni/src/main/java/javax/net/ssl/SSLContext.java)如下:

    private final SSLContextSpi contextSpi;
. . . . . .
    public static SSLContext getInstance(String protocol)
            throws NoSuchAlgorithmException {
        GetInstance.Instance instance = GetInstance.getInstance
                ("SSLContext", SSLContextSpi.class, protocol);
        return new SSLContext((SSLContextSpi)instance.impl, instance.provider,
                protocol);
    }
. . . . . .
    public final void init(KeyManager[] km, TrustManager[] tm,
                                SecureRandom random)
        throws KeyManagementException {
        contextSpi.engineInit(km, tm, random);
    }

    /**
     * Returns a <code>SocketFactory</code> object for this
     * context.
     *
     * @return the <code>SocketFactory</code> object
     * @throws IllegalStateException if the SSLContextImpl requires
     *          initialization and the <code>init()</code> has not been called
     */
    public final SSLSocketFactory getSocketFactory() {
        return contextSpi.engineGetSocketFactory();
    }

其中 SSLContextSpiOpenSSLContextImpl,该类的实现(位于external/conscrypt/src/main/java/org/conscrypt/OpenSSLContextImpl.java)如下:

package org.conscrypt;

import java.io.IOException;
import java.security.GeneralSecurityException;
import java.security.KeyManagementException;
import java.security.SecureRandom;
import javax.net.ssl.KeyManager;
import javax.net.ssl.SSLContextSpi;
import javax.net.ssl.SSLEngine;
import javax.net.ssl.SSLServerSocketFactory;
import javax.net.ssl.SSLSocketFactory;
import javax.net.ssl.TrustManager;

/**
 * OpenSSL-backed SSLContext service provider interface.
 */
public class OpenSSLContextImpl extends SSLContextSpi {

    /**
     * The default SSLContextImpl for use with
     * SSLContext.getInstance("Default"). Protected by the
     * DefaultSSLContextImpl.class monitor.
     */
    private static DefaultSSLContextImpl DEFAULT_SSL_CONTEXT_IMPL;

    /** TLS algorithm to initialize all sockets. */
    private final String[] algorithms;

    /** Client session cache. */
    private final ClientSessionContext clientSessionContext;

    /** Server session cache. */
    private final ServerSessionContext serverSessionContext;

    protected SSLParametersImpl sslParameters;

    /** Allows outside callers to get the preferred SSLContext. */
    public static OpenSSLContextImpl getPreferred() {
        return new TLSv12();
    }

    protected OpenSSLContextImpl(String[] algorithms) {
        this.algorithms = algorithms;
        clientSessionContext = new ClientSessionContext();
        serverSessionContext = new ServerSessionContext();
    }

    /**
     * Constuctor for the DefaultSSLContextImpl.
     *
     * @param dummy is null, used to distinguish this case from the public
     *            OpenSSLContextImpl() constructor.
     */
    protected OpenSSLContextImpl() throws GeneralSecurityException, IOException {
        synchronized (DefaultSSLContextImpl.class) {
            this.algorithms = null;
            if (DEFAULT_SSL_CONTEXT_IMPL == null) {
                clientSessionContext = new ClientSessionContext();
                serverSessionContext = new ServerSessionContext();
                DEFAULT_SSL_CONTEXT_IMPL = (DefaultSSLContextImpl) this;
            } else {
                clientSessionContext = DEFAULT_SSL_CONTEXT_IMPL.engineGetClientSessionContext();
                serverSessionContext = DEFAULT_SSL_CONTEXT_IMPL.engineGetServerSessionContext();
            }
            sslParameters = new SSLParametersImpl(DEFAULT_SSL_CONTEXT_IMPL.getKeyManagers(),
                    DEFAULT_SSL_CONTEXT_IMPL.getTrustManagers(), null, clientSessionContext,
                    serverSessionContext, algorithms);
        }
    }

    /**
     * Initializes this {@code SSLContext} instance. All of the arguments are
     * optional, and the security providers will be searched for the required
     * implementations of the needed algorithms.
     *
     * @param kms the key sources or {@code null}
     * @param tms the trust decision sources or {@code null}
     * @param sr the randomness source or {@code null}
     * @throws KeyManagementException if initializing this instance fails
     */
    @Override
    public void engineInit(KeyManager[] kms, TrustManager[] tms, SecureRandom sr)
            throws KeyManagementException {
        sslParameters = new SSLParametersImpl(kms, tms, sr, clientSessionContext,
                serverSessionContext, algorithms);
    }

    @Override
    public SSLSocketFactory engineGetSocketFactory() {
        if (sslParameters == null) {
            throw new IllegalStateException("SSLContext is not initialized.");
        }
        return Platform.wrapSocketFactoryIfNeeded(new OpenSSLSocketFactoryImpl(sslParameters));
    }

如我们前面讨论,验证服务端证书合法性是 PKI 体系中,保障系统安全极为关键的环节。如果不验证服务端证书的合法性,则即使部署了 HTTPS,HTTPS 也将形同虚设,毫无价值。因而在我们自己实现的 X509TrustManager 中,加载预埋的根证书,并据此验证服务端证书的合法性必不可少,这一检查在 checkServerTrusted() 中完成。然而为了使我们实现的 X509TrustManager 功能更完备,在根据我们预埋的根证书验证失败后,我们再使用系统默认的 X509TrustManager 做验证,像下面这样:

    private final class HelloX509TrustManager implements X509TrustManager {
        private X509TrustManager mSystemDefaultTrustManager;
        private X509Certificate mCertificate;

        private HelloX509TrustManager() {
            mCertificate = loadRootCertificate();
            mSystemDefaultTrustManager = systemDefaultTrustManager();
        }

        private X509Certificate loadRootCertificate() {
            String certName = "netease.crt";
            X509Certificate certificate = null;
            InputStream certInput = null;
            try {
                certInput = new BufferedInputStream(MainActivity.this.getAssets().open(certName));
                CertificateFactory certificateFactory = CertificateFactory.getInstance("X.509");
                certificate = (X509Certificate) certificateFactory.generateCertPath(certInput).getCertificates().get(0);
            } catch (IOException e) {
                e.printStackTrace();
            } catch (CertificateException e) {
                e.printStackTrace();
            } finally {
                if (certInput != null) {
                    try {
                        certInput.close();
                    } catch (IOException e) {
                    }
                }
            }
            return certificate;
        }

        private X509TrustManager systemDefaultTrustManager() {
            try {
                TrustManagerFactory trustManagerFactory = TrustManagerFactory.getInstance(
                        TrustManagerFactory.getDefaultAlgorithm());
                trustManagerFactory.init((KeyStore) null);
                TrustManager[] trustManagers = trustManagerFactory.getTrustManagers();
                if (trustManagers.length != 1 || !(trustManagers[0] instanceof X509TrustManager)) {
                    throw new IllegalStateException("Unexpected default trust managers:"
                            + Arrays.toString(trustManagers));
                }
                return (X509TrustManager) trustManagers[0];
            } catch (GeneralSecurityException e) {
                throw new AssertionError(); // The system has no TLS. Just give up.
            }
        }

        @Override
        public void checkClientTrusted(X509Certificate[] chain, String authType) throws CertificateException {
            mSystemDefaultTrustManager.checkClientTrusted(chain, authType);
        }

        @Override
        public void checkServerTrusted(X509Certificate[] chain, String authType) throws CertificateException {
            for (X509Certificate certificate : chain) {
                try {
                    certificate.verify(mCertificate.getPublicKey());
                    return;
                } catch (NoSuchAlgorithmException e) {
                    e.printStackTrace();
                } catch (InvalidKeyException e) {
                    e.printStackTrace();
                } catch (NoSuchProviderException e) {
                    e.printStackTrace();
                } catch (SignatureException e) {
                    e.printStackTrace();
                }
            }
            mSystemDefaultTrustManager.checkServerTrusted(chain, authType);
        }

        @Override
        public X509Certificate[] getAcceptedIssuers() {
            return mSystemDefaultTrustManager.getAcceptedIssuers();
        }
    }

此外,也可以不自己实现 X509TrustManager,而仅仅修改 X509TrustManager 所用的根证书库,就像下面这样:

    private TrustManager[] createX509TrustManager() {
        CertificateFactory cf = null;
        InputStream in = null;
        TrustManager[] trustManagers = null
        try {
            cf = CertificateFactory.getInstance("X.509");
            in = getAssets().open("ca.crt");
            Certificate ca = cf.generateCertificate(in);

            KeyStore keystore = KeyStore.getInstance(KeyStore.getDefaultType());
            keystore.load(null, null);
            keystore.setCertificateEntry("ca", ca);

            String tmfAlgorithm = TrustManagerFactory.getDefaultAlgorithm();
            TrustManagerFactory tmf = TrustManagerFactory.getInstance(tmfAlgorithm);
            tmf.init(keystore);

            trustManagers = tmf.getTrustManagers();
        } catch (CertificateException e) {
            e.printStackTrace();
        } catch (NoSuchAlgorithmException e) {
            e.printStackTrace();
        } catch (KeyStoreException e) {
            e.printStackTrace();
        } catch (IOException e1) {
            e1.printStackTrace();
        } finally {
            if (in != null) {
                try {
                    in.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }

        return trustManagers;
    }

自己实现 X509TrustManager 接口和通过 TrustManagerFactory,仅定制 KeyStore 这两种创建 X509TrustManager 对象的方式,当然是后一种方式更好一些了。如我们前面看到的,系统的 X509TrustManager 实现 RootTrustManager 集成自 X509ExtendedTrustManager,而不是直接实现的 X509TrustManager 接口 。JCA 的接口层也在随着新的安全协议和 SSL 库的发展在不断扩展,在具体的 Java 加密服务实现中,可能会实现并依赖这些扩展的功能,如上面看到的 X509TrustManager,而且加密服务的实现中常常通过反射,来动态依赖一些扩展的接口。因而,自己实现 X509TrustManager 接口时,以及其它加密相关的接口时,如 SSLSocket 等,可能会破坏一些功能。

很多时候可以看到,为了使用私有 CA 签名的证书,而定制域名匹配验证的逻辑,即自己实现 HostnameVerifier。不过通常情况下,网络库都会按照规范对域名与证书的匹配性做严格的检查,因而不是那么地有必要,除非域名证书有什么不那么规范的地方。

关于证书钉扎,在使用私有 CA 签名的证书时,通常似乎也没有那么必要。

参考文章:

Android https 自定义 证书 问题
Android实现https网络通信之添加指定信任证书/信任所有证书
HTTPS(含SNI)业务场景“IP直连”方案说明
HTTP Public Key Pinning 介绍
Java https请求 HttpsURLConnection
Done。

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 214,922评论 6 497
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 91,591评论 3 389
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 160,546评论 0 350
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 57,467评论 1 288
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 66,553评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,580评论 1 293
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,588评论 3 414
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,334评论 0 270
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,780评论 1 307
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,092评论 2 330
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,270评论 1 344
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,925评论 5 338
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,573评论 3 322
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,194评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,437评论 1 268
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,154评论 2 366
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,127评论 2 352