Https全揭秘系列 - 实践与分析
Posted 框架那些事儿
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Https全揭秘系列 - 实践与分析相关的知识,希望对你有一定的参考价值。
Https全揭秘系列 - 实践与分析
通过前面两篇文章,我们对于Https协议和TLS握手协议有了一定的理解,但是我认为不经过真正实践的话根本无法掌握这个协议,大家看完了过几天就应该忘的一干二净了。
这篇文章的标题是实践与分析,实践刚才讲了是为了真正掌握,那么为什么要加个分析呢?因为真正商用场景下关于Https故障的排查关键就在于如何去分析,至于分析有两种方法,下一篇文章我将针对实际工作中遇到的问题结合这两种方法进行分析。
话不多说,开始今天的文章
1.构建一个完整流程
这一章我将构造一个最简单的服务端与客户端,麻雀虽小五脏俱全,对于TLS握手过程的理解还是够用的。
1.1 服务端
服务端我使用Jetty作为容器,搭载一个简单的Servlet。由于代码非常简单,直接贴上完整代码。
/**
* Server
*
* @author AwingCorsair
*/
public class Jetty9HttpsStarter {
private static Logger logger = LoggerFactory.getLogger(Jetty9HttpsStarter.class);
private int maxThreads;
private String host = "127.0.0.1";
private int port = 8083;
/**
* false = ONE_WAY_CERTIFICATE, true = TWO_WAY_CERTIFICATE
*/
private boolean needClientAuth;
private String tlsVersion;
private String keystoreType;
private String truststoreType;
private String keystorePath;
private String truststorePath;
private String keystorePassword;
private String truststorePassword;
private void start() {
initParams();
QueuedThreadPool pool = new QueuedThreadPool();
pool.setName("jetty");
pool.setMinThreads(maxThreads);
pool.setMaxThreads(maxThreads);
Server server = new Server(pool);
Connector connector = createSSLConnector(host, port, server);
server.setConnectors(new Connector[] {
connector
});
ServletContextHandler handler = new ServletContextHandler(ServletContextHandler.NO_SESSIONS);
handler.setContextPath("/");
handler.addServlet(JettyServlet.class,"");
server.setHandler(handler);
try {
server.start();
server.join();
}
catch (Exception e) {
logger.error("server down!", e);
}
}
/**
* init server params
*/
private void initParams() {
maxThreads = 6;
needClientAuth = true;
host = "127.0.0.1";
port = 8083;
tlsVersion = "TLSv1.2";
keystoreType = "JKS";
keystorePath = "src/main/resources/certs/server.keystore";
keystorePassword = "password";
truststoreType = "JKS";
truststorePath = "src/main/resources/certs/server.truststore";
truststorePassword = "password";
}
/**
* init SSL params
*
* @param host server host
* @param port port
* @param server server
* @return SSL Connector
*/
private Connector createSSLConnector(String host, int port, Server server) {
SslConnectionFactory connectionFactory = new SslConnectionFactory();
SslContextFactory contextFactory = connectionFactory.getSslContextFactory();
contextFactory.setKeyStoreType(keystoreType);
contextFactory.setKeyStorePath(keystorePath);
contextFactory.setKeyStorePassword(keystorePassword);
contextFactory.setTrustStoreType(truststoreType);
contextFactory.setTrustStorePath(truststorePath);
contextFactory.setTrustStorePassword(truststorePassword);
contextFactory.setNeedClientAuth(needClientAuth);
contextFactory.setProtocol(tlsVersion);
HttpConfiguration httpsConfig = new HttpConfiguration();
httpsConfig.setSecurePort(port);
httpsConfig.setSecureScheme("https");
httpsConfig.addCustomizer(new SecureRequestCustomizer());
ServerConnector sslConnector = new ServerConnector(server, connectionFactory, new HttpConnectionFactory(httpsConfig));
sslConnector.setPort(port);
sslConnector.setHost(host);
server.addConnector(sslConnector);
return sslConnector;
}
public static void main(String[] args) {
Jetty9HttpsStarter starter = new Jetty9HttpsStarter();
starter.start();
}
}
我将上一篇文章中创建的server.keystore、server.truststore、client.keystore、client.truststore这四个文件放到了项目的src/main/resources/certs目录下。
在initParams方法中我初始化了构建服务端和Https的参数,这里面needClientAuth需要注意一下,Jetty使用这个参数来指定单向认证还是双向认证,注释中我也写了true情况下是TWOWAYCERTIFICATE也就是双向认证。
Jetty和Tomcat类似,都可以使用配置文件或者代码来初始化参数,这里我按照习惯使用代码初始化。
通过这些代码,我们就创建了一个部署在127.0.0.1:8083上的Jetty服务端,至于JettyServlet这个Servlet就是一个空的Servlet不做任何工作,服务端使用TLSv1.2协议。
1.2 客户端
客户端使用Apache的HttpClient
/**
* Client
*
* @author AwingCorsair
*/
public class Client {
private static Logger logger = LoggerFactory.getLogger(Client.class);
private static int ONE_WAY_CERTIFICATE = 1;
private static int TWO_WAY_CERTIFICATE = 2;
private String serverUrl;
private String tlsVersion;
private String keystoreType;
private String keystorePath;
private String keystorePassword;
private String truststoreType;
private String truststorePath;
private String truststorePassword;
private KeyStore getKeyStore(String password, String keyStorePath, String keystoreType) throws Exception {
KeyStore ks = KeyStore.getInstance(keystoreType);
FileInputStream in = new FileInputStream(keyStorePath);
ks.load(in,password.toCharArray());
in.close();
return ks;
}
/**
* init Params used for SSL
*/
private void initParams() {
serverUrl = "https://127.0.0.1:8093";
tlsVersion = "TLSv1.2";
keystoreType = "JKS";
keystorePath = "src/main/resources/certs/client.keystore";
keystorePassword = "password";
truststoreType = "JKS";
truststorePath = "src/main/resources/certs/client.truststore";
truststorePassword = "password";
}
/**
* 初始化SSLFactory
*
* @return SSLContext
* @throws Exception exception
*/
private SSLContext getSSLContext(int certificateMethod) throws Exception {
SSLContext ctx;
if (certificateMethod == ONE_WAY_CERTIFICATE) {
ctx = SSLContext.getInstance(tlsVersion);
X509TrustManager tm = new X509TrustManager() {
@Override
public void checkClientTrusted(X509Certificate[] chain,
String authType) throws CertificateException {
}
@Override
public void checkServerTrusted(X509Certificate[] chain,
String authType) throws CertificateException {
// chain[0].checkValidity();
}
@Override
public X509Certificate[] getAcceptedIssuers() {
return null;
}
};
ctx.init(null, new TrustManager[] { tm }, null);
}
else {
KeyManagerFactory keyManagerFactory = KeyManagerFactory.getInstance(KeyManagerFactory.getDefaultAlgorithm());
KeyStore keyStore = getKeyStore(keystorePassword, keystorePath, keystoreType);
keyManagerFactory.init(keyStore, keystorePassword.toCharArray());
TrustManagerFactory trustManagerFactory = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm());
KeyStore trustKeyStore = getKeyStore(truststorePassword, truststorePath, truststoreType);
trustManagerFactory.init(trustKeyStore);
ctx = SSLContext.getInstance(tlsVersion);
ctx.init(keyManagerFactory.getKeyManagers(), trustManagerFactory.getTrustManagers(), null);
}
return ctx;
}
private void sendRequest() {
try {
initParams();
SSLContext sslcontext = getSSLContext(TWO_WAY_CERTIFICATE);
SSLConnectionSocketFactory socketFactory = new
SSLConnectionSocketFactory(sslcontext,
SSLConnectionSocketFactory.STRICT_HOSTNAME_VERIFIER); // Socket
HttpClient client =
HttpClients.custom().setSSLSocketFactory(socketFactory).build();
HttpGet httpget = new HttpGet(serverUrl);
HttpResponse response = client.execute(httpget);
logger.debug(EntityUtils.toString(response.getEntity()));
logger.debug("Response Code (Apache): " + response.getStatusLine().getStatusCode());
} catch (Exception e) {
logger.error("HttpsURLConnection Failed", e);
}
}
public static void main(String[] args) {
Client client = new Client();
client.sendRequest();
}
}
注意getSSLContext方法,这里同服务端一样,也是区分了单双向认证。
完整的项目源码可以使用Git直接拷贝下来
git clone https://github.com/AwingCorsair/HttpsExample.git
2.调试与抓包
调试Https的整个流程,我认为一个最重要的点就在于一定要使用"-Djavax.net.debug=ssl"这个JVM参数
IDEA和Eclipse可分别参照下面图片进行配置。
现在,我们启动服务端,在控制台中我们将看到以下几段信息
***
found key for : server
chain [0] = [
[
Version: V3
Subject: CN=127.0.0.1, OU=A, O=B, L=C, ST=D, C=E
Signature Algorithm: SHA1withDSA, OID = 1.2.840.10040.4.3
Key: Sun DSA Public Key
Parameters:DSA
p: fd7f5381 1d751229 52df4a9c 2eece4e7 f611b752 3cef4400 c31e3f80 b6512669
455d4022 51fb593d 8d58fabf c5f5ba30 f6cb9b55 6cd7813b 801d346f f26660b7
6b9950a5 a49f9fe8 047b1022 c24fbba9 d7feb7c6 1bf83b57 e7c6a8a6 150f04fb
83f6d3c5 1ec30235 54135a16 9132f675 f3ae2b61 d72aeff2 2203199d d14801c7
q: 9760508f 15230bcc b292b982 a2eb840b f0581cf5
g: f7e1a085 d69b3dde cbbcab5c 36b857b9 7994afbb fa3aea82 f9574c0b 3d078267
5159578e bad4594f e6710710 8180b449 167123e8 4c281613 b7cf0932 8cc8a6e1
3c167a8b 547c8d28 e0a3ae1e 2bb3a675 916ea37f 0bfa2135 62f1fb62 7a01243b
cca4f1be a8519089 a883dfe1 5ae59f06 928b665e 807b5525 64014c3b fecf492a
y:
f574b9d7 7be01a3d 556de6e2 c2c5bc66 a01179ac d7e9da3f 38536a4e 76a99f30
64ab9cb7 1c168217 7413e13f 7f3006a1 e512443e d192515c e6348a1d 4ed8b583
6cd49855 b2a6af94 8c7fad0b 5b2fdea8 faa5c0f5 bc3ac38c 66ce3df6 4f47ed9a
07e93889 76c822c7 4961f63e b297ef78 2ab2f489 7707d75f 6d0b6b60 2982f0e1
Validity: [From: Sat Dec 09 17:36:37 CST 2017,
To: Fri Mar 09 17:36:37 CST 2018]
Issuer: CN=127.0.0.1, OU=A, O=B, L=C, ST=D, C=E
SerialNumber: [ 02b2afcf]
Certificate Extensions: 1
[1]: ObjectId: 2.5.29.14 Criticality=false
SubjectKeyIdentifier [
KeyIdentifier [
0000: 2E 23 3F E3 D9 F1 D9 84 C0 1C 46 E9 0D 54 51 E2 .#?.......F..TQ.
0010: 97 F3 25 9C ..%.
]
]
]
Algorithm: [SHA1withDSA]
Signature:
0000: 30 2C 02 14 2F 64 42 DD 46 C7 B3 30 7E 1C 98 18 0,../dB.F..0....
0010: 6A F0 1F E7 E2 60 DE 4F 02 14 7B 11 3D 9E 82 6F j....`.O....=..o
0020: 6A CD A0 BE 8C 06 11 23 E0 D3 8A 65 E6 0A j......#...e..
]
这一段信息显示了我们服务端加载的Keystore中的信息,还记得我们前一篇文章中创建的服务端Keystore中只有一个别名为server的私钥么,现在正确加载进来了,证书签名、加密算法都显示在上面。
***
adding as trusted cert:
Subject: CN=127.0.0.1, OU=1, O=2, L=3, ST=4, C=5
Issuer: CN=127.0.0.1, OU=1, O=2, L=3, ST=4, C=5
Algorithm: DSA; Serial number: 0x225ee3b3
Valid from Sat Dec 09 18:01:04 CST 2017 until Fri Mar 09 18:01:04 CST 2018
接下来这段相信大家也能看明白,这是读取到的Truststore中的内容,也就是我们上篇文章中导入的客户端的证书信息。
Ignoring unavailable cipher suite: TLS_DHE_DSS_WITH_AES_256_GCM_SHA384
Ignoring unavailable cipher suite: TLS_DH_anon_WITH_AES_256_CBC_SHA
Ignoring unavailable cipher suite: TLS_DH_anon_WITH_AES_256_CBC_SHA256
Ignoring unavailable cipher suite: TLS_RSA_WITH_AES_256_CBC_SHA
Ignoring unavailable cipher suite: TLS_DHE_RSA_WITH_AES_256_GCM_SHA384
trigger seeding of SecureRandom
done seeding SecureRandom
Using SSLEngineImpl.
这一串信息是我们服务端不使用的加密套件,这个与TLS版本有关。至于这么多加密套件有什么意义我们后面再讲。
接下来我们启动客户端,这里我直接贴出除了加密套件以外的完整信息
***
found key for : client
chain [0] = [
[
Version: V3
Subject: CN=127.0.0.1, OU=1, O=2, L=3, ST=4, C=5
Signature Algorithm: SHA1withDSA, OID = 1.2.840.10040.4.3
Key: Sun DSA Public Key
Parameters:DSA
p: fd7f5381 1d751229 52df4a9c 2eece4e7 f611b752 3cef4400 c31e3f80 b6512669
455d4022 51fb593d 8d58fabf c5f5ba30 f6cb9b55 6cd7813b 801d346f f26660b7
6b9950a5 a49f9fe8 047b1022 c24fbba9 d7feb7c6 1bf83b57 e7c6a8a6 150f04fb
83f6d3c5 1ec30235 54135a16 9132f675 f3ae2b61 d72aeff2 2203199d d14801c7
q: 9760508f 15230bcc b292b982 a2eb840b f0581cf5
g: f7e1a085 d69b3dde cbbcab5c 36b857b9 7994afbb fa3aea82 f9574c0b 3d078267
5159578e bad4594f e6710710 8180b449 167123e8 4c281613 b7cf0932 8cc8a6e1
3c167a8b 547c8d28 e0a3ae1e 2bb3a675 916ea37f 0bfa2135 62f1fb62 7a01243b
cca4f1be a8519089 a883dfe1 5ae59f06 928b665e 807b5525 64014c3b fecf492a
y:
5c8669f5 e86cb449 f258012c c09f2343 e33cf4ca ad03a4e8 9e7bc9ad 73ce4f80
2bae112a 0d039e18 853528e1 e4ff0dec ea588bd6 37773bc1 e35931fd 46a059e7
0f3eefd6 d20e5eb7 8365e9c0 c8ea75a0 f214377e b0392262 1d58afdd 301dcd13
c76e34a2 393502e0 13da1af8 521b6e5d 7ee1a035 33276eac 07966f43 e2df3c4f
Validity: [From: Sat Dec 09 18:01:04 CST 2017,
To: Fri Mar 09 18:01:04 CST 2018]
Issuer: CN=127.0.0.1, OU=1, O=2, L=3, ST=4, C=5
SerialNumber: [ 225ee3b3]
Certificate Extensions: 1
[1]: ObjectId: 2.5.29.14 Criticality=false
SubjectKeyIdentifier [
KeyIdentifier [
0000: 10 CE 04 49 99 F1 CA A6 C3 94 B9 69 C1 2C 64 3C ...I.......i.,d<
0010: D0 BC F3 CC ....
]
]
]
Algorithm: [SHA1withDSA]
Signature:
0000: 30 2D 02 14 03 92 35 EF EC 41 5E 7B C1 3A 15 E7 0-....5..A^..:..
0010: F2 5E 98 B8 88 FB 5B 1B 02 15 00 83 CC 6B 47 CA .^....[......kG.
0020: 79 06 1D 04 5E 35 09 4A 3A E4 70 56 FD 29 F5 y...^5.J:.pV.).
]
***
adding as trusted cert:
Subject: CN=127.0.0.1, OU=A, O=B, L=C, ST=D, C=E
Issuer: CN=127.0.0.1, OU=A, O=B, L=C, ST=D, C=E
Algorithm: DSA; Serial number: 0x2b2afcf
Valid from Sat Dec 09 17:36:37 CST 2017 until Fri Mar 09 17:36:37 CST 2018
如果你理解了我刚才讲的服务端的相关解释,那么这个客户端的就一目了然了,客户端加载了别名为client的私钥作为自己的证书,信任了别名为server的证书。
通过这些信息,你可以明确知道自己的程序到底加载了那些证书,信任了哪些证书,这些信息对于故障排查至关重要。
由于篇幅原因,本篇文章到此为止,下一篇文章中我将结合上面JVM参数以及Wireshark抓包两种方式来具体分析整个握手过程。
如果您觉得我的文章对您有帮助,请点个赞或者转发此文章,感谢您的支持!
以上是关于Https全揭秘系列 - 实践与分析的主要内容,如果未能解决你的问题,请参考以下文章
倒计时2天 | 张钹院士领衔,AI开发者大会20大论坛议程全揭秘!