您的位置:首页 > 理论基础 > 计算机网络

聊聊HTTPS与Android安全

2015-10-26 17:38 1581 查看

HTTPS是什么?

 HTTPS并不是一个单独的协议,而是对工作在一加密连接(SSL/TLS)上的常规HTTP协议。通过在TCP和HTTP之间加入TLS(Transport Layer Security)来加密 。(注: TLS为新版本的SSL)

为何需要HTTPS?

不使用SSL/TLS的HTTP通信,就是不加密的通信。所有信息明文传播,带来了三大风险。

- 窃听风险(eavesdropping):第三方可以获知通信内容。
- 篡改风险(tampering):第三方可以修改通信内容。
- 冒充风险(pretending):第三方可以冒充他人身份参与通信。

 SSL/TLS协议是为了解决这三大风险而设计的。那么SSL/TLS如何解决这三大风险呢?

加密传播,防第三方窃听

 HTTPS一般使用的加密与HASH算法如下:

- 非对称加密算法:RSA,DSA/DSS
- 对称加密算法:AES,RC4,3DES
- HASH算法:MD5,SHA1,SHA256

对称加密

加密和解密的密钥是相同的.
假设A和B之间通信,使用对称密钥是123.
A和B写信需要用123加密.
B收到A的信息,同样需要123解密。
 对称加密的最大风险在于密钥一旦泄露,那么被截获的信件内容就会被破解.

非对称加密

加密和解密的密钥是不同的分为私钥和公钥。
私钥: 只有一份, 保存在收信人手中。不会在通信中传输,不会被泄露。
公钥:可以有多份,保存在写信人手中。
假设B要给A写信: A先生成一对公钥(123)和私钥(456)
A要把自己的公钥(456)通过某种安全的方式(数字证书,下文会提到)交给B B用公钥(456)加密信件内容然后发给A
A使用私钥(123)解开内容。
因此即使Step2时公钥泄露,那么即使B给A写的信被截获,由于没有A的私钥依然无法被解开。

具有校验机制,防止被篡改

使用HASH算法进行校验内容是否被篡改。

配备数字证书,防止身份被冒充

数字证书就是互联网通讯中标志通讯各方身份信息的一串数字,提供了一种在Internet上验证通信实体身份的方式,数字证书不是数字身份证,而是身份认证机构盖在数字身份证上的一个章或印(或者说加在数字身份证上的一个签名)。它是由权威机构——CA机构,又称为证书授权(Certificate Authority)中心发行的,人们可以在网上用它来识别对方的身份。

  数字证书是一个文件。此文件保存了加密过的用户的信息及公钥。



数字证书在HTTPS的什么时候会用上呢?这里就要提到HTTPS握手。

HTTPS握手过程



服务端返回的数字证书(此为用户证书),客户端(浏览器)会进行如下验证:

1)遍历计算机以及浏览器中保存的根证书(Root CA)和中间证书              (Intermediate CA),若其中某个根证书或中间证书的公钥可以解开server端的证书,获得server的公钥和server域名. 否则握手失败。
整个HTTPS通信的唯一核心保障就是可信的根证书。

2) 判断证书中的域名是否和正在访问的域名相同。否则认证失败,浏览器会提示证书不可信。但是握手加密依然可以继续。


客户端(浏览器)产生一个随机的对称密钥

使用证书中服务端的公钥加密此对称密钥
将此已经加密过的对称密钥发给server。至此client和server都通过可靠的手段拥有对称密钥.
通过对称密钥加密Http的通信内容。
 通过上述的握手过程可以知道,非对称密钥只是用来加密传输对称密钥的。因为可以保证传输中的对称密钥在传输的过程中无法被破解。所以握手之后的内容通信就是安全的。

 握手过程中涉及到两种证书:

Https握手时Server发给Client的证书成为用户证书
Client所在计算机中原本保存的根证书(ROOT CA)和中间证书(Intermediate CA)

Android开发中使用的Https

 当Android端有使用https的需求的时候,如果继续保持http的方式进行网络请求,就容易出现连接失败的问题。这是因为大多数情况下,Https服务器所使用的根证书是自签名的。如果设备的信任证书列表中不包含此签名机构,就会连接失败。出现这样的问题。

 一般有两种解决方案:

让HttpClient信任所有的服务器证书,这种方法安全性则差一些,但实现相对简单。
在发起Https连接之前,将服务器证书加到HttpClient的信任证书列表中,这个相对来说比较复杂一些,很容易出错;
下面讲解第一种的实现原理。

当实例化HttpClinet对象时要绑定https连接所使用的端口号,这里绑定了443(443是https默认的端口号,就像http的默认端口是80)。

信任所有证书

一个常见的信任所有证书的Https请求,这里我们以baidu的为例。

<code class="hljs java has-numbering"><span class="hljs-keyword">private</span> <span class="hljs-keyword">void</span> <span class="hljs-title">getHttps</span>() {
String https = <span class="hljs-string">"https://www.baidu.com/"</span>;
<span class="hljs-keyword">try</span> {
SSLContext sslContext = SSLContext.getInstance(<span class="hljs-string">"TLS"</span>);
sslContext.init(<span class="hljs-keyword">null</span>, <span class="hljs-keyword">new</span> TrustManager[] { <span class="hljs-keyword">new</span> ITrustManager() }, <span class="hljs-keyword">new</span> SecureRandom());
HttpsURLConnection.setDefaultSSLSocketFactory(sslContext.getSocketFactory());
HttpsURLConnection.setDefaultHostnameVerifier(<span class="hljs-keyword">new</span> IHostnameVerifier());
HttpsURLConnection conn = (HttpsURLConnection) <span class="hljs-keyword">new</span> URL(https).openConnection();
conn.setDoOutput(<span class="hljs-keyword">true</span>);
conn.setDoInput(<span class="hljs-keyword">true</span>);
conn.connect();
BufferedReader br = <span class="hljs-keyword">new</span> BufferedReader(<span class="hljs-keyword">new</span> InputStreamReader(conn.getInputStream()));
StringBuffer sb = <span class="hljs-keyword">new</span> StringBuffer();
String line;
<span class="hljs-keyword">while</span> ((line = br.readLine()) != <span class="hljs-keyword">null</span>)
sb.append(line);
text.setText(sb.toString());
} <span class="hljs-keyword">catch</span> (Exception e) {
Log.e(<span class="hljs-keyword">this</span>.getClass().getName(), e.getMessage());
}
}

<span class="hljs-javadoc">/**
* 此类是用于主机名验证的基接口。
* 在握手期间,如果 URL 的主机名和服务器的标识主机名不匹配,则验证机制可以回调此接口的实现程序来确定是否应该允许此连接。
* 策略可以是基于证书的或依赖于其他验证方案。
* 当验证 URL 主机名使用的默认规则失败时使用这些回调。
*
*<span class="hljs-javadoctag"> @author</span> zhoushengtao
*
*/</span>
<span class="hljs-keyword">private</span> <span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">IHostnameVerifier</span> <span class="hljs-keyword">implements</span> <span class="hljs-title">HostnameVerifier</span> {</span>

<span class="hljs-javadoc">/**
* 验证主机名和服务器验证方案的匹配是可接受的。
*
* (这里我们所有的都接受)
*
*<span class="hljs-javadoctag"> @param</span> hostname - 主机名
*<span class="hljs-javadoctag"> @param</span> session - 到主机的连接上使用的 SSLSession
*
*<span class="hljs-javadoctag"> @return</span> 如果主机名是可接受的,则返回 true
*/</span>
<span class="hljs-annotation">@Override</span>
<span class="hljs-keyword">public</span> <span class="hljs-keyword">boolean</span> <span class="hljs-title">verify</span>(String hostname, SSLSession session) {
Log.d(<span class="hljs-string">"https_test"</span>, <span class="hljs-string">"verify hostname = "</span> + hostname );
<span class="hljs-keyword">for</span> (String name : session.getValueNames()) {

Log.d(<span class="hljs-string">"https_test"</span>, <span class="hljs-string">"verify session "</span>+ name +<span class="hljs-string">" = "</span> + session.getValue(name));
}
<span class="hljs-keyword">return</span> <span class="hljs-keyword">true</span>;
}

}

<span class="hljs-javadoc">/**
* 此接口的实例管理使用哪一个 X509 证书来验证远端的安全套接字。
* 决定是根据信任的证书授权、证书撤消列表、在线状态检查或其他方式做出的。
*
*<span class="hljs-javadoctag"> @author</span> zhoushengtao
*
*/</span>
<span class="hljs-keyword">private</span> <span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">ITrustManager</span> <span class="hljs-keyword">implements</span> <span class="hljs-title">X509TrustManager</span> {</span>

<span class="hljs-javadoc">/**
*
*  给出同位体提供的部分或完整的证书链,构建到可信任的根的证书路径,
*  并且返回是否可以确认和信任将其用于基于验证类型的客户端 SSL 验证。
*  验证类型由实际使用的证书确定。
*   例如,如果使用 RSAPublicKey,则 authType 应为 "RSA"。检查是否大小写敏感的。
*
*  <span class="hljs-javadoctag"> @param</span> chain - 同位体的证书链
*  <span class="hljs-javadoctag"> @param</span> authType - 基于客户端证书的验证类型
*  <span class="hljs-javadoctag"> @throws</span> CertificateException - 如果证书链不受此 TrustManager 信任。
*
*/</span>
<span class="hljs-annotation">@Override</span>
<span class="hljs-keyword">public</span> <span class="hljs-keyword">void</span> <span class="hljs-title">checkClientTrusted</span>(X509Certificate[] chain, String authType) <span class="hljs-keyword">throws</span> CertificateException {
Log.d(<span class="hljs-string">"https_test"</span>, <span class="hljs-string">"checkClientTrusted authType = "</span> + authType);
<span class="hljs-keyword">for</span> (X509Certificate certificate : chain) {
Log.d(<span class="hljs-string">"https_test"</span>, <span class="hljs-string">"checkClientTrusted certificate = "</span> + certificate.toString());
}

}

<span class="hljs-javadoc">/**
* 给出同位体提供的部分或完整的证书链,构建到可信任的根的证书路径,
* 并且返回是否可以确认和信任将其用于基于验证类型的服务器 SSL 验证。
* 验证类型是表示为一个 String 的密码套件的密钥交换算法部分,例如 "RSA"、"DHE_DSS"。
*
* 注:对于一些可输出的密码套件,密钥交换算法是在运行时的联络期间确定的。
* 例如,对于 TLS_RSA_EXPORT_WITH_RC4_40_MD5,当临时的 RSA 密钥
*  用于密钥交换时 authType 应为 RSA_EXPORT,当使用来自服务器证书的密钥时 authType
*  应为 RSA。检查是否大小写敏感的。
*
* <span class="hljs-javadoctag"> @param</span> chain - 同位体的证书链
* <span class="hljs-javadoctag"> @param</span> authType - 使用的密钥交换算法
* <span class="hljs-javadoctag"> @throws</span> CertificateException - 如果证书链不受此 TrustManager 信任。
*/</span>
<span class="hljs-annotation">@Override</span>
<span class="hljs-keyword">public</span> <span class="hljs-keyword">void</span> <span class="hljs-title">checkServerTrusted</span>(X509Certificate[] chain, String authType) <span class="hljs-keyword">throws</span> CertificateException {
Log.d(<span class="hljs-string">"https_test"</span>, <span class="hljs-string">"checkServerTrusted authType = "</span> + authType);
<span class="hljs-keyword">for</span> (X509Certificate certificate : chain) {
Log.d(<span class="hljs-string">"https_test"</span>, <span class="hljs-string">"checkServerTrusted certificate = "</span> + certificate.toString());
}
}

<span class="hljs-javadoc">/**
* 返回受验证同位体信任的认证中心的数组。
*
*<span class="hljs-javadoctag"> @return</span> 可接受的 CA 发行者证书的非 null(可能为空)的数组。
*/</span>
<span class="hljs-annotation">@Override</span>
<span class="hljs-keyword">public</span> X509Certificate[] <span class="hljs-title">getAcceptedIssuers</span>() {
<span class="hljs-keyword">return</span> <span class="hljs-keyword">null</span>;
}

}</code><ul style="display: block;" class="pre-numbering"><li>1</li><li>2</li><li>3</li><li>4</li><li>5</li><li>6</li><li>7</li><li>8</li><li>9</li><li>10</li><li>11</li><li>12</li><li>13</li><li>14</li><li>15</li><li>16</li><li>17</li><li>18</li><li>19</li><li>20</li><li>21</li><li>22</li><li>23</li><li>24</li><li>25</li><li>26</li><li>27</li><li>28</li><li>29</li><li>30</li><li>31</li><li>32</li><li>33</li><li>34</li><li>35</li><li>36</li><li>37</li><li>38</li><li>39</li><li>40</li><li>41</li><li>42</li><li>43</li><li>44</li><li>45</li><li>46</li><li>47</li><li>48</li><li>49</li><li>50</li><li>51</li><li>52</li><li>53</li><li>54</li><li>55</li><li>56</li><li>57</li><li>58</li><li>59</li><li>60</li><li>61</li><li>62</li><li>63</li><li>64</li><li>65</li><li>66</li><li>67</li><li>68</li><li>69</li><li>70</li><li>71</li><li>72</li><li>73</li><li>74</li><li>75</li><li>76</li><li>77</li><li>78</li><li>79</li><li>80</li><li>81</li><li>82</li><li>83</li><li>84</li><li>85</li><li>86</li><li>87</li><li>88</li><li>89</li><li>90</li><li>91</li><li>92</li><li>93</li><li>94</li><li>95</li><li>96</li><li>97</li><li>98</li><li>99</li><li>100</li><li>101</li><li>102</li><li>103</li><li>104</li><li>105</li><li>106</li><li>107</li><li>108</li><li>109</li><li>110</li><li>111</li><li>112</li><li>113</li><li>114</li><li>115</li><li>116</li><li>117</li><li>118</li></ul>

受信任的Https请求

当然信任所有的证书,风险性还是很大的。

一般我们的操作步骤是:

导出公钥:在浏览器上用https访问tomcat,查看其证书,并另存为一个文件(存成了X.509格式:xxxx.cer),这里我们到处的是baidu.cer
导入公钥:把xxxx.cer放在Android的assets文件夹中,以方便在运行时通过代码读取此证书。
使用HttpClient请求,代码如下:

<code class="hljs javascript has-numbering"> public <span class="hljs-built_in">String</span> requestHTTPSPage(<span class="hljs-built_in">String</span> mUrl) {
InputStream inputStream = <span class="hljs-literal">null</span>;
<span class="hljs-built_in">String</span> result = <span class="hljs-string">""</span>;
<span class="hljs-keyword">try</span> {
inputStream = getAssets().open(<span class="hljs-string">"baidu.cer"</span>); <span class="hljs-comment">// 下载的证书放到项目中的assets目录中</span>
CertificateFactory cerFactory = CertificateFactory.getInstance(<span class="hljs-string">"X.509"</span>);
Certificate cer = cerFactory.generateCertificate(inputStream);
KeyStore keyStore = KeyStore.getInstance(<span class="hljs-string">"PKCS12"</span>, <span class="hljs-string">"BC"</span>);
keyStore.load(<span class="hljs-literal">null</span>, <span class="hljs-literal">null</span>);
keyStore.setCertificateEntry(<span class="hljs-string">"trust"</span>, cer);

SSLSocketFactory socketFactory = <span class="hljs-keyword">new</span> SSLSocketFactory(keyStore);
<span class="hljs-comment">// Https 默认请求端口 443</span>
Scheme scheme = <span class="hljs-keyword">new</span> Scheme(<span class="hljs-string">"https"</span>, socketFactory, <span class="hljs-number">443</span>);
HttpClient mHttpClient = <span class="hljs-keyword">new</span> DefaultHttpClient();
mHttpClient.getConnectionManager().getSchemeRegistry().register(scheme);

BufferedReader reader = <span class="hljs-literal">null</span>;
<span class="hljs-keyword">try</span> {
Log.d(TAG, <span class="hljs-string">"executeGet is in,murl:"</span> + mUrl);
HttpGet request = <span class="hljs-keyword">new</span> HttpGet();
request.setURI(<span class="hljs-keyword">new</span> URI(mUrl));
HttpResponse response = mHttpClient.execute(request);
<span class="hljs-keyword">if</span> (response.getStatusLine().getStatusCode() != <span class="hljs-number">200</span>) {
request.abort();
<span class="hljs-keyword">return</span> result;
}

reader = <span class="hljs-keyword">new</span> BufferedReader(<span class="hljs-keyword">new</span> InputStreamReader(response.getEntity().getContent()));
StringBuffer buffer = <span class="hljs-keyword">new</span> StringBuffer();
<span class="hljs-built_in">String</span> line = <span class="hljs-literal">null</span>;
<span class="hljs-keyword">while</span> ((line = reader.readLine()) != <span class="hljs-literal">null</span>) {
buffer.append(line);
}
result = buffer.toString();
Log.d(TAG, <span class="hljs-string">"mUrl="</span> + mUrl + <span class="hljs-string">"\nresult = "</span> + result);
} <span class="hljs-keyword">catch</span> (Exception e) {
e.printStackTrace();
} <span class="hljs-keyword">finally</span> {
<span class="hljs-keyword">if</span> (reader != <span class="hljs-literal">null</span>) {
reader.close();
}
}
} <span class="hljs-keyword">catch</span> (Exception e) {
e.printStackTrace();
} <span class="hljs-keyword">finally</span> {
<span class="hljs-keyword">try</span> {
<span class="hljs-keyword">if</span> (inputStream != <span class="hljs-literal">null</span>)
inputStream.close();
} <span class="hljs-keyword">catch</span> (IOException e) {
e.printStackTrace();
}
}
<span class="hljs-keyword">return</span> result;
}</code><ul style="display: block;" class="pre-numbering"><li>1</li><li>2</li><li>3</li><li>4</li><li>5</li><li>6</li><li>7</li><li>8</li><li>9</li><li>10</li><li>11</li><li>12</li><li>13</li><li>14</li><li>15</li><li>16</li><li>17</li><li>18</li><li>19</li><li>20</li><li>21</li><li>22</li><li>23</li><li>24</li><li>25</li><li>26</li><li>27</li><li>28</li><li>29</li><li>30</li><li>31</li><li>32</li><li>33</li><li>34</li><li>35</li><li>36</li><li>37</li><li>38</li><li>39</li><li>40</li><li>41</li><li>42</li><li>43</li><li>44</li><li>45</li><li>46</li><li>47</li><li>48</li><li>49</li><li>50</li><li>51</li><li>52</li><li>53</li><li>54</li><li>55</li></ul>


中间人攻击

中间人攻击(Man-in-the-MiddleAttack,简称“MITM攻击”)是一种“间接”的入侵攻击




 假设A是服务器,B是用户,B向A发起HTTPS连接,于是A需要将自己的公钥发给B。中间人C通过某种手段可以截获并伪造AB之间的通讯(比如GFW或者共享wifi等)。

 那么C可以伪造一份A的公钥,并保有这分假公钥的私钥;然后截获A发给B的真公钥,并伪造自己的身份,让B认为自己就是A,并把伪造的公钥发给B;

 如此一来,B会通过伪造的公钥给A发送密文,而C就可以截获这些密文并利用手中的私钥轻易的解密这些密文了;

 然后将这些密文通过正确的公钥转发给服务器A,这样AB之间的通讯仍将继续,AB在毫不知情的情况下被中间人把证书“偷梁换柱”,从而达成了中间人攻击,AB之间的非对称加密形同虚设,从而TLS协议的对称加密的密钥就能被C轻易的获取。

 如此,TLS完全告破。

总结:如果你安装了 中间人(代理服务器)自己生成的根证书,那么就中招了。

举个例子:

Fiddler对https的抓取就是靠中间人攻击的方式。




打开Fiddler ,Tools->Fiddler options->HTTPS中的https capture,那么fidder会提示你安装一个fiddler自己生成的根证书(fiddler自己作为CA)名为DO_NOT_TRUST_FIDDLER_ROOT的根证书。



之后当浏览器访问的每个https的域名(如www.baidu.com等):

fiddler会作为客户端先解开https内容

使用自己作为CA(名为DO_NOT_TRUST_FIDDLER_ROOT)的私钥,对baidu这个域名颁发一个用户证书。

https握手时向浏览器发送这个用户证书

浏览器收到fiddler作为server端返回的https回应时:



 尝试解开server发来的用户证书,由于已经安装了fiddler的根证书,因此可以解开。

 其中的域名是baidu.com,同浏览器访问的域名一致,因此证书这一部分就验证成功了。接下来的握手就自然可以完成。

 那么,CA机构到底是什么呢?

CA,证书链,根证书

数字证书认证机构(CA)

它的出现就是为了防止中间人攻击的。
防止中间人攻击,说白了就是要确保B收到的A的公钥(证书)真实有效,这样数字证书认证机构应运而生。

数字证书认证机构说白了就相当于一个受信任的中间人。CA有一对根密钥,其公钥称为根证书。

A向CA申请一个证书,则CA利用其私钥加密A的公钥,其结果就是“服务器A,通过CA验证的证书”。

而在用户的操作系统(或者浏览器)中,会集成世界范围内所有被信任的CA的根证书。

这样,用户B在收到A发送给他的证书后,需要利用CA的根证书(公钥)解密后才能得到正确的公钥,如此一来,就完成了对A发送过来的信息的验证,证明了A的正身,不是C伪造的假证书,从而达成了中间人攻击的防范。


数字证书从何而来

对于根证书(ROOT CA)和中间证书(Intermediate CA)

在用户的操作系统(或者浏览器)中,会集成世界范围内所有被信任的CA的根证书。
安装软件时顺带安装,如支付宝安全控件等,此时软件会请求管理员权限(流氓…)。
用户自己下载安装,如12306网站
对于用户证书

比如www.alipay.com的证书,是支付宝公司通过如versign之类相关认证机构去资质审核以及缴费获得的。那么https访问中,server发来的数字证书长啥样呢?

支付宝www.alipay.com的证书

点击浏览器导航栏左上角的小锁







www.alipay.com证书路径中有三层,表示三级证书链。那么什么是证书链呢?

证书链

CA证书分为两类:

根证书(Root CA)
中间证书(Intermediate CA)。
 但是根证书的使用是收到严格限制的,不可能对于每一类用户都使用根证书去签发子数字证书,所以就有了中间证书的概念。

 中间证书由根证书或上一级中间证书签发,它可以再往下级签发数字证书。

 例如我们自己为某个域名申请了证书 My CA,那么对于三级证书链,它的签发过程如下:

Root CA 签发 Intermediate CA, Intermediate CA 签发 My CA这时我们就可以用My CA去给域名作数字认证了。

 上面讲到的签发关系很像链式结构,所以被称作证书链。

 验证的过程可想而知,就是签发的逆过程,这是通过证书链来完成的:

- 浏览器会在计算机以及浏览器的证书列表中查找此CA是否可信, 如果有则认为My CA是可信的;
- 如果没有,继续往上找,直到根证书:
- 如果根证书是可信的,那么整条证书链就是可信的;
- 如果根证书不可信,那么My CA将被认作是不可信的,浏览器就会发出警告。

所以说,对于刚才www.alipay.com的3级证书链来说:

根证书(ROOT CA)是Versign Class3 Public xxxx
中间证书(Intermediate CA)是 Symantec Class 3 Secure Server xxxx

如何查看已经安装的数字证书

对于windows平台:win+R, 输入certmgr.msc
对于Ubuntu: 放在/usr/share/ca-certificates/中

有风险的根证书



 查看了自己电脑的证书,发现很多根证书都不知道从何而来的,(估计是其他插件捆绑安装来的)

 整个HTTPS通信的唯一核心保障就是可信的根证书。这种自己安装不可信的根证书会有遭到中间人攻击的风险。

(一)http://blog.csdn.net/yzzst/article/details/46693685
(二)http://blog.csdn.net/yzzst/article/details/46739581
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: