美文网首页
Android App 安全的HTTPS 通信

Android App 安全的HTTPS 通信

作者: SDY_0656 | 来源:发表于2017-10-29 18:00 被阅读0次
     对于数字证书相关概念、Android 里 https 通信代码就不再复述了,直接讲问题。缺少相应的安全校验很容易导致中间人攻击,而漏洞的形式主要有以下3种:
    

    1.自定义X509TrustManager
    在使用HttpsURLConnection发起 HTTPS 请求的时候,提供了一个自定义的X509TrustManager,未实现安全校验逻辑,下面片段就是常见的容易犯错的代码片段。如果不提供自定义的X509TrustManager,代码运行起来可能会报异常(原因下文解释),初学者就很容易在不明真相的情况下提供了一个自定义的X509TrustManager,却忘记正确地实现相应的方法。本文重点介绍这种场景的处理方式。

    TrustManager tm = new X509TrustManager() {
        public void checkClientTrusted(X509Certificate[] chain, String authType)
                throws CertificateException {
                  //do nothing,接受任意客户端证书
        }
    
        public void checkServerTrusted(X509Certificate[] chain, String authType)
                throws CertificateException {
                  //do nothing,接受任意服务端证书
        }
    
        public X509Certificate[] getAcceptedIssuers() {
            return null;
        }
    };
    
    sslContext.init(null, new TrustManager[] { tm }, null);
    

    2.自定义了HostnameVerifier
    在握手期间,如果 URL 的主机名和服务器的标识主机名不匹配,则验证机制可以回调此接口的实现程序来确定是否应该允许此连接。如果回调内实现不恰当,默认接受所有域名,则有安全风险。代码示例。

    HostnameVerifier hnv = new HostnameVerifier() {
      @Override
      public boolean verify(String hostname, SSLSession session) {
        // Always return true,接受任意域名服务器
        return true;
      }
    };
    HttpsURLConnection.setDefaultHostnameVerifier(hnv);
    

    3.信任所有主机名

       SSLSocketFactory sf = new MySSLSocketFactory(trustStore);
    sf.setHostnameVerifier(SSLSocketFactory.ALLOW_ALL_HOSTNAME_VERIFIER);
    

    下面介绍修复方案:
    Android 手机有一套共享证书的机制,如果目标 URL 服务器下发的证书不在已信任的证书列表里,或者该证书是自签名的,不是由权威机构颁发,那么会出异常。对于我们这种非浏览器 app 来说,如果提示用户去下载安装证书,可能会显得比较诡异。幸好还可以通过自定义的验证机制让证书通过验证。验证的思路有两种:
    1.方案1
    不论是权威机构颁发的证书还是自签名的,打包一份到 app 内部,比如存放在 asset 里或者直接写成一个字符串。通过这份内置的证书初始化一个KeyStore,然后用这个KeyStore去引导生成的TrustManager来提供验证,具体代码如下:
    ```
    public static SSLSocketFactory setCertificates(InputStream... certificates) {
    try {
    CertificateFactory certificateFactory = CertificateFactory.getInstance("X.509");
    KeyStore keyStore = KeyStore.getInstance(KeyStore.getDefaultType());
    keyStore.load(null);
    int index = 0;
    for (InputStream certificate : certificates) {
    String certificateAlias = Integer.toString(index++);
    keyStore.setCertificateEntry(certificateAlias, certificateFactory.generateCertificate(certificate));

                try {
                    if (certificate != null)
                        certificate.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
    
            TrustManagerFactory trustManagerFactory = TrustManagerFactory.getInstance(
                    TrustManagerFactory.getDefaultAlgorithm());
            trustManagerFactory.init(keyStore);
            TrustManager[] trustManagers = trustManagerFactory.getTrustManagers();
            if (trustManagers.length != 1 || !(trustManagers[0] instanceof X509TrustManager)) {
                throw new IllegalStateException("Unexpected default trust managers:"
                        + Arrays.toString(trustManagers));
            }
            X509TrustManager trustManager = (X509TrustManager) trustManagers[0];
    
            SSLContext sslContext = SSLContext.getInstance("TLS");
            sslContext.init(null, new TrustManager[]{trustManager}, null);
    
            return sslContext.getSocketFactory();
        } catch (Exception e) {
            e.printStackTrace();
        }
        return null;
    }
    
    
    2.方案2
     同方案1,打包一份到证书到 app 内部,但不通过KeyStore去引导生成的TrustManager,而是干脆直接自定义一个TrustManager,自己实现校验逻辑;校验逻辑主要包括:
      1)服务器证书是否过期
      2)证书签名是否合法
    

    try {
    CertificateFactory cf = CertificateFactory.getInstance("X.509");
    // uwca.crt 打包在 asset 中,该证书可以从https://itconnect.uw.edu/security/securing-computer/install/safari-os-x/下载
    InputStream caInput = new BufferedInputStream(getAssets().open("uwca.crt"));
    final Certificate ca;
    try {
    ca = cf.generateCertificate(caInput);
    Log.i("Longer", "ca=" + ((X509Certificate) ca).getSubjectDN());
    Log.i("Longer", "key=" + ((X509Certificate) ca).getPublicKey());
    } finally {
    caInput.close();
    }
    // Create an SSLContext that uses our TrustManager
    SSLContext context = SSLContext.getInstance("TLSv1","AndroidOpenSSL");
    context.init(null, new TrustManager[]{
    new X509TrustManager() {
    @Override
    public void checkClientTrusted(X509Certificate[] chain,
    String authType)
    throws CertificateException {

              }
    
              @Override
              public void checkServerTrusted(X509Certificate[] chain,
                      String authType)
                      throws CertificateException {
                  for (X509Certificate cert : chain) {
    
                      // Make sure that it hasn't expired.
                      cert.checkValidity();
    
                      // Verify the certificate's public key chain.
                      try {
                          cert.verify(((X509Certificate) ca).getPublicKey());
                      } catch (NoSuchAlgorithmException e) {
                          e.printStackTrace();
                      } catch (InvalidKeyException e) {
                          e.printStackTrace();
                      } catch (NoSuchProviderException e) {
                          e.printStackTrace();
                      } catch (SignatureException e) {
                          e.printStackTrace();
                      }
                  }
              }
    
              @Override
              public X509Certificate[] getAcceptedIssuers() {
                  return new X509Certificate[0];
              }
          }
    

    }, null);

    URL 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);
    } catch (CertificateException e) {
    e.printStackTrace();
    } catch (IOException e) {
    e.printStackTrace();
    } catch (NoSuchAlgorithmException e) {
    e.printStackTrace();
    } catch (KeyManagementException e) {
    e.printStackTrace();
    } catch (NoSuchProviderException e) {
    e.printStackTrace();
    }

      3)自定义HostnameVerifier
        简单的话就是根据域名进行字符串匹配校验;业务复杂的话,还可以结合配置中心、白名单、黑名单、正则匹配等多级别动态校验;总体来说逻辑还是比较简单的,反正只要正确地实现那个方法。
    

    HostnameVerifier hnv = new HostnameVerifier() {
    @Override
    public boolean verify(String hostname, SSLSession session) {
    //示例
    if("yourhostname".equals(hostname)){
    return true;
    } else {
    HostnameVerifier hv =
    HttpsURLConnection.getDefaultHostnameVerifier();
    return hv.verify(hostname, session);
    }
    }
    };

     4)主机名验证策略改成严格模式
    

    SSLSocketFactory sf = new MySSLSocketFactory(trustStore);
    sf.setHostnameVerifier(SSLSocketFactory.STRICT_HOSTNAME_VERIFIER);

    
    参考文章:http://pingguohe.net/2016/02/26/Android-App-secure-ssl.html
    

    相关文章

      网友评论

          本文标题:Android App 安全的HTTPS 通信

          本文链接:https://www.haomeiwen.com/subject/xlwypxtx.html