Android应用使用https

来源:互联网 发布:美国缩表中国楼市知乎 编辑:程序博客网 时间:2024/05/24 07:37

  • HTTPS定义
  • 握手过程
  • 证书颁发机构 CA
  • 一个由知名 CA 发放证书的网络服务器的HTTPS请求
  • 验证服务器证书的常见问题
    • 颁发服务器证书的 CA 未知
    • 服务器证书不是由 CA 签署的而是自签署
    • 服务器配置缺少中间 CA
  • 主机名验证的常见问题

HTTPS定义

https是介于传输层与应用层间使用安全套接字层 (SSL)(现在技术上称为传输层安全协议 (TLS)),用于在客户端与服务器之间进行加密通信的一个通用构建块。目的是使客户端与主机互信,防止数据被中途窃取。

握手过程

1. 客户端向服务器发送一个消息
2. 服务器响应消息,向其发送自己的证书
3. 客户端向服务器发送使用服务器证书的公共密钥加密过的消息及客户端的证书(服务端是否验证客户端证书在握手过程中可选)
4. 服务器认为客户端有可以访问其证书的私钥
5. 双方以协商的加密方式进行认证

证书颁发机构 (CA)

在典型的 SSL 使用场景中,会使用一个包含公钥及与其匹配的私钥的证书配置服务器。作为 SSL 客户端与服务器握手的一部分,服务器将通过使用公钥加密签署其证书来证明自己具有私钥。

不过,任何人都可以生成他们自己的证书和私钥,因此,一个简单的握手只能说明服务器知道与证书公钥匹配的私钥,除此之外什么都证明不了。解决此问题的一个方法是让客户端拥有其信任的一个或多个证书集。如果证书不在此集合中,则不会信任服务器。

但这个简单的方法有几个缺点。服务器应能够随时间的推移升级到更强的密钥(“密钥旋转”),使用新的公钥替换证书中的公钥。遗憾的是,客户端应用现在必须根据服务器配置发生的变化进行更新。如果服务器不在应用开发者的控制下(例如,如果服务器是一个第三方网络服务),则很容易出现问题。如果应用必须与网络浏览器或电子邮件应用等任意服务器通信,那么,此方法也会带来问题。

为弥补这些缺点,通常使用来自知名颁发者(称为证书颁发机构 (CA))发放的证书配置服务器。主机平台一般包含其信任的知名 CA 的列表。从 Android 4.2 (Jelly Bean) 开始,Android 目前包含在每个版本中更新的 100 多个 CA。CA 具有一个证书和一个私钥,这点与服务器相似。为服务器发放证书时,CA 使用其私钥签署服务器证书。然后,客户端可以验证该服务器是否具有平台已知的 CA 发放的证书。

不过,在解决一些问题的同时,使用 CA 也会引发其他问题。因为 CA 为许多服务器发放证书,因此,您仍需要某种方式来确保您与您需要的服务器通信。为解决这个问题,CA 发放的证书通过 gmail.com 等具体名称或 *.google.com 等通配型主机集识别服务器

以下示例会让这些概念更具体。下面的代码段来自命令行,openssl 工具的 s_client 命令将查看 Wikipedia 的服务器证书信息。它指定端口 443,因为此端口是 HTTPS的默认端口。此命令将 openssl s_client 的输出发送到 openssl x509,后者将根据 X.509 标准格式化与证书有关的信息。具体而言,此命令会要求相关主题,主题包含服务器名称信息和可识别 CA 的颁发者。

$ openssl s_client -connect wikipedia.org:443 | openssl x509 -noout -subject -issuersubject= /serialNumber=sOrr2rKpMVP70Z6E9BT5reY008SJEdYv/C=US/O=*.wikipedia.org/OU=GT03314600/OU=See www.rapidssl.com/resources/cps (c)11/OU=Domain Control Validated - RapidSSL(R)/CN=*.wikipedia.orgissuer= /C=US/O=GeoTrust, Inc./CN=RapidSSL CA

您会看到证书是由 RapidSSL CA 为与 *.wikipedia.org 匹配的服务器发放的。


一个由知名 CA 发放证书的网络服务器的HTTPS请求

URL url = new URL("https://wikipedia.org");URLConnection urlConnection = url.openConnection();InputStream in = urlConnection.getInputStream();copyInputStreamToOutputStream(in, System.out);

验证服务器证书的常见问题

javax.net.ssl.SSLHandshakeException: java.security.cert.CertPathValidatorException: Trust anchor for certification path not found.        at org.apache.harmony.xnet.provider.jsse.OpenSSLSocketImpl.startHandshake(OpenSSLSocketImpl.java:374)        at libcore.net.http.HttpConnection.setupSecureSocket(HttpConnection.java:209)        at libcore.net.http.HttpsURLConnectionImpl$HttpsEngine.makeSslConnection(HttpsURLConnectionImpl.java:478)        at libcore.net.http.HttpsURLConnectionImpl$HttpsEngine.connect(HttpsURLConnectionImpl.java:433)        at libcore.net.http.HttpEngine.sendSocketRequest(HttpEngine.java:290)        at libcore.net.http.HttpEngine.sendRequest(HttpEngine.java:240)        at libcore.net.http.HttpURLConnectionImpl.getResponse(HttpURLConnectionImpl.java:282)        at libcore.net.http.HttpURLConnectionImpl.getInputStream(HttpURLConnectionImpl.java:177)        at libcore.net.http.HttpsURLConnectionImpl.getInputStream(HttpsURLConnectionImpl.java:271)

出现此情况的原因有很多,其中包括:

1.颁发服务器证书的 CA 未知

在这种情况下,由于您具有系统不信任的 CA,将发生 SSLHandshakeException。原因可能是您有一个来自 Android 还未信任的新 CA 的证书,或您的应用在没有 CA 的较旧版本上运行。CA 未知的原因通常是因为它不是公共 CA,而是政府、公司或教育机构等组织发放的仅供自己使用的私有 CA。

幸运的是,您可以指示 HttpsURLConnection 信任特定的 CA 集。此过程可能有点复杂,下面的示例展示了这个过程,从 InputStream 获取一个特定的 CA,用该 CA 创建 KeyStore,然后用后者创建和初始化 TrustManager。TrustManager 是系统用于从服务器验证证书的工具,可以使用一个或多个 CA 从 KeyStore 创建,而创建的 TrustManager 将仅信任这些 CA。

如果是新的 TrustManager,此示例将初始化一个新的 SSLContext,后者可以提供一个 SSLSocketFactory,您可以通过 HttpsURLConnection 用它来替换默认的 SSLSocketFactory。这样一来,连接将使用您的 CA 验证证书。

下面是使用华盛顿大学的机构 CA 的完整示例:

// Load CAs from an InputStream// (could be from a resource or ByteArrayInputStream or ...)CertificateFactory cf = CertificateFactory.getInstance("X.509");// From https://www.washington.edu/itconnect/security/ca/load-der.crtInputStream caInput = new BufferedInputStream(new FileInputStream("load-der.crt"));Certificate ca;try {    ca = cf.generateCertificate(caInput);    System.out.println("ca=" + ((X509Certificate) ca).getSubjectDN());} finally {    caInput.close();}// Create a KeyStore containing our trusted CAsString keyStoreType = KeyStore.getDefaultType();KeyStore keyStore = KeyStore.getInstance(keyStoreType);keyStore.load(null, null);keyStore.setCertificateEntry("ca", ca);// Create a TrustManager that trusts the CAs in our KeyStoreString tmfAlgorithm = TrustManagerFactory.getDefaultAlgorithm();TrustManagerFactory tmf = TrustManagerFactory.getInstance(tmfAlgorithm);tmf.init(keyStore);// Create an SSLContext that uses our TrustManagerSSLContext context = SSLContext.getInstance("TLS");context.init(null, tmf.getTrustManagers(), null);// Tell the URLConnection to use a SocketFactory from our SSLContextURL url = new URL("https://certs.cac.washington.edu/CAtest/");HttpsURLConnection urlConnection =    (HttpsURLConnection)url.openConnection();urlConnection.setSSLSocketFactory(context.getSocketFactory());InputStream in = urlConnection.getInputStream();copyInputStreamToOutputStream(in, System.out);

2.服务器证书不是由 CA 签署的,而是自签署

3.服务器配置缺少中间 CA

2和3的情况创建自己的 TrustManager

class TrustManager implements X509TrustManager{    @Override    public void checkClientTrusted(X509Certificate[] chain, String authType) throws CertificateException {        // TODO Auto-generated method stub    }    @Override    public void checkServerTrusted(X509Certificate[] chain, String authType) throws CertificateException {        // TODO Auto-generated method stub    }    @Override    public X509Certificate[] getAcceptedIssuers() {        // TODO Auto-generated method stub        return null;    }}SSLContext context = null;        try {            context = SSLContext.getInstance("TLS");            TrustManager[] managers = new TrustManager[]{new TrustManager()};            context.init(null, managers, null);        } catch (NoSuchAlgorithmException e) {            // TODO Auto-generated catch block            e.printStackTrace();        } catch (KeyManagementException e) {            // TODO Auto-generated catch block            e.printStackTrace();        }

主机名验证的常见问题

如果没有提供服务器正确的证书,您通常会看到类似于下面的错误:

java.io.IOException: Hostname 'example.com' was not verified        at libcore.net.http.HttpConnection.verifySecureSocketHostname(HttpConnection.java:223)        at libcore.net.http.HttpsURLConnectionImpl$HttpsEngine.connect(HttpsURLConnectionImpl.java:446)        at libcore.net.http.HttpEngine.sendSocketRequest(HttpEngine.java:290)        at libcore.net.http.HttpEngine.sendRequest(HttpEngine.java:240)        at libcore.net.http.HttpURLConnectionImpl.getResponse(HttpURLConnectionImpl.java:282)        at libcore.net.http.HttpURLConnectionImpl.getInputStream(HttpURLConnectionImpl.java:177)        at libcore.net.http.HttpsURLConnectionImpl.getInputStream(HttpsURLConnectionImpl.java:271)

出现此错误的一个原因是服务器配置错误。配置服务器所使用的证书不具有与您尝试连接的服务器匹配的主题或主题备用名称字段。许多不同的服务器可能使用一个证书。例如,使用 openssl s_client -connect google.com:443 | openssl x509 -text 查看 google.com 证书,您不仅可以看到一个支持 .google.com 的主题,而且还能看到适用于 .youtube.com、*.android.com 等的主题备用名称。仅当您要连接的服务器名称没有被证书列为可接受时才会发生这种错误。

不幸的是,还有另外一个原因也会引发此错误,即虚拟托管。当多个使用 HTTP 的主机名共享服务器时,网络服务器可以通过 HTTP/1.1 请求识别客户端正在寻找哪个目标主机名。遗憾的是,使用 HTTPS 会使情况变得复杂,因为服务器必须在看到 HTTP 请求前知道返回哪个证书。为了解决此问题,较新的 SSL 版本(特别是 TLSv.1.0 及更高版本)支持服务器名称指示 (SNI),后者允许 SSL 客户端向服务器指定预期的主机名,以便可以返回正确的证书。

比较极端的替代方法是不使用服务器默认情况下返回的验证程序,而是将 HostnameVerifier 替换为不使用您的虚拟机主机名的验证程序。

// Create an HostnameVerifier that hardwires the expected hostname.// Note that is different than the URL's hostname:// example.com versus example.orgHostnameVerifier hostnameVerifier = new HostnameVerifier() {    @Override    public boolean verify(String hostname, SSLSession session) {        HostnameVerifier hv =            HttpsURLConnection.getDefaultHostnameVerifier();        return hv.verify("example.com", session);    }};// Tell the URLConnection to use our HostnameVerifierURL url = new URL("https://example.org/");HttpsURLConnection urlConnection =    (HttpsURLConnection)url.openConnection();urlConnection.setHostnameVerifier(hostnameVerifier);InputStream in = urlConnection.getInputStream();

许多网站都会介绍一个糟糕的替代解决方案,让您安装一个没用的 TrustManager。如果您这样做还不如不加密通信,因为任何人都可以在公共 WLAN 热点下,使用伪装成您的服务器的代理发送您的用户流量,通过 DNS 欺骗攻击您的用户。然后,攻击者可以记录密码和其他个人数据。此方法之所以有效是因为攻击者可以生成一个证书,且没有可以切实验证证书是否来自值得信任的来源的 TrustManager,从而使您的应用可与任何人通信。因此,不要这样做,暂时性的也不行。如果您可以始终让您的应用信任服务器证书的颁发者,那就这样做吧。

下面是网上看到的一种叫 公钥锁定 的方法class PubKeyManager implements X509TrustManager{    private static String PUB_KEY = "30820122300d06092a864886f70d0101" + "0105000382010f003082010a0282010100b35ea8adaf4cb6db86068a836f3c85" +"5a545b1f0cc8afb19e38213bac4d55c3f2f19df6dee82ead67f70a990131b6bc" + "ac1a9116acc883862f00593199df19ce027c8eaaae8e3121f7f329219464e657" +"2cbf66e8e229eac2992dd795c4f23df0fe72b6ceef457eba0b9029619e0395b8" + "609851849dd6214589a2ceba4f7a7dcceb7ab2a6b60c27c69317bd7ab2135f50" +"c6317e5dbfb9d1e55936e4109b7b911450c746fe0d5d07165b6b23ada7700b00" + "33238c858ad179a82459c4718019c111b4ef7be53e5972e06ca68a112406da38" + "cf60d2f4fda4d1cd52f1da9fd6104d91a34455cd7b328b02525320a35253147b" + "e0b7a5bc860966dc84f10d723ce7eed5430203010001";    public void checkServerTrusted(X509Certificate[] chain, String authType) throws CertificateException{        if (chain == null) {            throw new IllegalArgumentException("checkServerTrusted: X509Certificate array is null");        }        if (!(chain.length > 0)) {            throw new IllegalArgumentException("checkServerTrusted: X509Certificate is empty");        }        if (!(null != authType && authType.equalsIgnoreCase("RSA"))) {            throw new CertificateException("checkServerTrusted: AuthType is not RSA");        }        // Perform customary SSL/TLS checks        try {            TrustManagerFactory tmf = TrustManagerFactory.getInstance("X509");            tmf.init((KeyStore) null);            for (TrustManager trustManager : tmf.getTrustManagers()) {                ((X509TrustManager) trustManager).checkServerTrusted(chain, authType);            }        } catch (Exception e) {            throw new CertificateException(e);        }        // Hack ahead: BigInteger and toString(). We know a DER encoded Public Key begins        // with 0×30 (ASN.1 SEQUENCE and CONSTRUCTED), so there is no leading 0×00 to drop.        RSAPublicKey pubkey = (RSAPublicKey) chain[0].getPublicKey();        String encoded = new BigInteger(1 /* positive */, pubkey.getEncoded()).toString(16);        // Pin it!        final boolean expected = PUB_KEY.equalsIgnoreCase(encoded);        if (!expected) {            throw new CertificateException("checkServerTrusted: Expected public key: " + PUB_KEY + ", got public key:" + encoded);        }    }    @Override    public void checkClientTrusted(X509Certificate[] chain, String authType) throws CertificateException {        // TODO Auto-generated method stub    }    @Override    public X509Certificate[] getAcceptedIssuers() {        // TODO Auto-generated method stub        return null;    }}

参考文献
通过 HTTPS 和 SSL 确保安全
传输层安全
证书和公钥固定

0 1
原创粉丝点击