本文主要是为了搞懂https相关知识以及应用,语言保证通俗易懂,清晰。网上有很多文章但都太碎片,这种文章看多了反倒会把自己搞晕
什么是https?
传统的http都是明文传播,且不验证服务器安全性。所以在http协议层下加了个ssl层。ssl层在3.0以上又叫tls层。
SSL/TLS如何保证安全?原理?
保证安全的前置问题是,http传递信息的不安全在哪里?思考清楚这个问题我们才能理解ssl的思想。
- 危险站点,我们通过浏览器浏览的网页需要被确保是安全正规的,否则其中可能有木马或偷取用户隐私或钓鱼网站等。
- 中间人攻击,传输的数据很容易被代理服务器获取和修改
- 冒充客户端,对于部分服务器来说,客户端的验证也是必要的,比如银行交易相关数据请求,必须要求规定的客户端发出才安全。
如何解决这些问题就是ssl要做的事情。看下ssl握手(不同于http的三次握手四次挥手)的过程。握手过程有五步
第一步,爱丽丝给出协议版本号、一个客户端生成的随机数(Client random),以及客户端支持的加密方法。
第二步,鲍勃确认双方使用的加密方法,并给出数字证书、以及一个服务器生成的随机数(Server random)。
第三步,爱丽丝确认数字证书有效,然后生成一个新的随机数(Premaster secret),并使用数字证书中的公钥,加密这个随机数,发给鲍勃。
第四步,鲍勃使用自己的私钥,获取爱丽丝发来的随机数(即Premaster secret)。
第五步,爱丽丝和鲍勃根据约定的加密方法,使用前面的三个随机数,生成"对话密钥"(session key),用来加密接下来的整个对话过程。
爱丽丝表示客户端,鲍勃表示服务器。这个过程看着简洁,但是我最初看的时候,感觉不够深刻,一看就会,一问就懵。那是因为这里有一些细节没弄懂,觉得懂了,实际没懂。
-
客户端支持的加密方法是什么?
看这张charles抓包的截图,实际上就是一个对称加密的方法,服务器会从其中选择一个(Server Chosen)。秘钥就是三个随机数生成的对话秘钥。
客户端如何证明服务器的证书有效的呢?
证书包含以下信息:申请者公钥、申请者的组织信息和个人信息、签发机构 CA 的信息、有效时间、证书序列号等信息的明文,同时包含一个签名;
签名的产生算法:首先,使用散列函数计算公开的明文信息的信息摘要,然后,采用 CA 的私钥对信息摘要进行加密,密文即签名;
只要用证书公钥解开签名信息,然后将解密的信息和证书中的信息进行对比。同时保证本地信任该组织证书。浏览器或手机支持的证书都可以在手机中查到,也可以手动禁用他们,或安装其他证书。公钥和私钥?
相信很多人这个都懂,但是我还是想说下我对私钥公钥的理解,其实很简单。
公钥是给别人用的,私钥是你自己保存的。所以你想证明你的身份 私钥加密下,别人用你的公钥能解密那就证明你的身份了。同样还有个作用就是我用你的公钥加密,除了你自己能解开,别人是解不开的,这也保证了数据安全性。看到了吧,就这两把钥匙就保证了数据安全和对方身份。
搞懂这三点再看流程是不是更简单了。简单的说就是 1. 客户端校验了服务器的证书,保证服务器安全 2. 客户端和服务器约定算法保证数据安全。那么开篇的三个问题前两个就解决掉了。第三个就涉及到双向认证了,后面再说。
到这儿还是想考下,为什么这样就解决中间人攻击了呢?
首先客户端发送加密算法套件和随机数给服务器。代理服务器拿到了你发送的这些东西,原封不动再将这些数据发给服务器。服务器收到后,将自己的证书及生成的随机数发给代理服务器。代理服务器又原封不动的将证书和随机数发给客户端。客户端使用服务器证书加密自己生成的随机数,然后发送给代理服务器。这个时候代理服务器由于没有证书私钥,无法解密内容。代理服务器只能依然原封不动的发给服务器。服务器用私钥成功解密,然后选择其中一个加密算法,用三个随机数作为数据加密密钥。客户端和服务器开始了真正的数据交流,然后处在中间的代理服务器只能懵逼的传递数据。
代理服务器就真的没办法解密数据了吗?如果第二步中服务器将证书发给代理服务器,而代理服务器偷梁换柱将自己证书发给客户端。客户端将生成的随机数加密并发送给代理服务器。代理服务器用自己的私钥解开,然后用服务器公钥加密下,再发送给服务器。这样就可以操作客户端数据了。
那么怎么解决中间人替换证书呢?
客户端不知道真正的服务器证书是什么,所以才会信任中间人给的证书。那么问题也好解决,就是要客户端确定服务器证书,将证书放到本地。这样第二步中代理服务器替换证书后,客户端不理会代理服务器给的证书,而直接用本地证书去加密。然后再发送给代理服务器,代理服务器由于没有私钥,也解不开,所以依然只能懵逼的做一个中间人。
可能这个时候你还是有疑问,那服务器证书的验证还有意义吗?内置证书的情况,就默认信任该服务器证书了,所以证书也就没有验证的必要了。
如何设置本地证书?
java的okHttp中设置代码如下:
public CustomTrust() {
X509TrustManager trustManager;
SSLSocketFactory sslSocketFactory;
try {
trustManager = trustManagerForCertificates(trustedCertificatesInputStream());
SSLContext sslContext = SSLContext.getInstance("TLS");
sslContext.init(null, new TrustManager[] { trustManager }, null);
sslSocketFactory = sslContext.getSocketFactory();
} catch (GeneralSecurityException e) {
throw new RuntimeException(e);
}
client = new OkHttpClient.Builder()
.sslSocketFactory(sslSocketFactory, trustManager)
.build();
}
private InputStream trustedCertificatesInputStream() {
// PEM files for root certificates of Comodo and Entrust. These two CAs are sufficient to view
// https://publicobject.com (Comodo) and https://squareup.com (Entrust). But they aren't
// sufficient to connect to most HTTPS sites including https://godaddy.com and https://visa.com.
// Typically developers will need to get a PEM file from their organization's TLS administrator.
String comodoRsaCertificationAuthority = ""
+ "-----BEGIN CERTIFICATE-----\n"
+ "MIIF2DCCA8CgAwIBAgIQTKr5yttjb+Af907YWwOGnTANBgkqhkiG9w0BAQwFADCB\n"
+ ...
+ "NVOFBkpdn627G190\n"
+ "-----END CERTIFICATE-----\n";
String entrustRootCertificateAuthority = ""
+ "-----BEGIN CERTIFICATE-----\n"
+ "MIIEkTCCA3mgAwIBAgIERWtQVDANBgkqhkiG9w0BAQUFADCBsDELMAkGA1UEBhMC\n"
+ ...
+ "-----END CERTIFICATE-----\n";
return new Buffer()
.writeUtf8(comodoRsaCertificationAuthority)
.writeUtf8(entrustRootCertificateAuthority)
.inputStream();
}
private X509TrustManager trustManagerForCertificates(InputStream in)
throws GeneralSecurityException {
CertificateFactory certificateFactory = CertificateFactory.getInstance("X.509");
Collection<? extends Certificate> certificates = certificateFactory.generateCertificates(in);
if (certificates.isEmpty()) {
throw new IllegalArgumentException("expected non-empty set of trusted certificates");
}
// Put the certificates a key store.
char[] password = "password".toCharArray(); // Any password will work.
KeyStore keyStore = newEmptyKeyStore(password);
int index = 0;
for (Certificate certificate : certificates) {
String certificateAlias = Integer.toString(index++);
keyStore.setCertificateEntry(certificateAlias, certificate);
}
// Use it to build an X509 trust manager.
KeyManagerFactory keyManagerFactory = KeyManagerFactory.getInstance(
KeyManagerFactory.getDefaultAlgorithm());
keyManagerFactory.init(keyStore, password);
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));
}
return (X509TrustManager) trustManagers[0];
}
private KeyStore newEmptyKeyStore(char[] password) throws GeneralSecurityException {
try {
KeyStore keyStore = KeyStore.getInstance(KeyStore.getDefaultType());
InputStream in = null; // By convention, 'null' creates an empty key store.
keyStore.load(in, password);
return keyStore;
} catch (IOException e) {
throw new AssertionError(e);
}
}
代码不多解释了,不是重点。
这样基本可以保证数据安全传输了,解决了中间人攻击和服务器验证问题。但是还不够完美,最好是能对客户端也进行验证。那么这就是最后一个问题了 如何实现双向认证?
sslContext.init(KeyManager[],TrustManager[]null);
//KeyManager[] 第一个参数是授权的密钥管理器,用来授权验证。TrustManager[]第二个是被授权的证书管理器,用来验证服务器端的证书。
//第三个参数是一个随机数值,可以填写null。如果只是服务器传输数据给客户端来验证,就传入第一个参数就可以,客户端构建环境就传入第二个参数。
//双向认证的话,就同时使用两个管理器。
到这里就结束了,希望以后遇到https问题能够更好的解决