1.双向认证流程
- 客户端发起建立HTTPS连接请求,将SSL协议版本的信息发送给服务端;
- 服务器端将本机的公钥证书(server.crt)发送给客户端;
- 客户端读取公钥证书(server.crt),取出了服务端公钥;
- 客户端将客户端公钥证书(client.crt)发送给服务器端;
- 服务器端使用根证书(root.crt)解密客户端公钥证书,拿到客户端公钥;
- 客户端发送自己支持的加密方案给服务器端;
- 服务器端根据自己和客户端的能力,选择一个双方都能接受的加密方案,使用客户端的公钥加密后发送给客户端;
- 客户端使用自己的私钥解密加密方案,生成一个随机数R,使用服务器公钥加密后传给服务器端;
- 服务端用自己的私钥去解密这个密文,得到了密钥R
- 服务端和客户端在后续通讯过程中就使用这个密钥R进行通信了。
2. 证书准备
从上一章内容中,我们可以总结出来,整个双向认证的流程需要六个证书文件:
- 服务器端公钥证书:server.crt
- 服务器端私钥文件:server.key
- 根证书:root.crt
- 客户端公钥证书:client.crt
- 客户端私钥文件:client.key
- 客户端集成证书(包括公钥和私钥,用于浏览器访问场景):client.p12
所有的这些证书,我们都可以向证书机构去申请签发,一般需要收取一定的证书签发费用,此时我们需要选择大型的证书机构去购买。如果只是企业内部使用,不是给公众使用,也可以自行颁发自签名证书
3. 自签名证书
3.1 、根证书
(1)创建根证书私钥:
openssl genrsa -out root.key 1024
(2)创建根证书请求文件:
openssl req -new -out root.csr -key root.key
后续参数请自行填写,下面是一个例子:
Country Name (2 letter code) [XX]:cn
State or Province Name (full name) []:bj
Locality Name (eg, city) [Default City]:bj
Organization Name (eg, company) [Default Company Ltd]:alibaba
Organizational Unit Name (eg, section) []:test
Common Name (eg, your name or your servers hostname) []:root
Email Address []:a.alibaba.com
A challenge password []:
An optional company name []:
(3)创建根证书:
openssl x509 -req -in root.csr -out root.crt -signkey root.key -CAcreateserial -days 3650
在创建证书请求文件的时候需要注意三点,下面生成服务器请求文件和客户端请求文件均要注意这三点:
- 根证书的Common Name填写root就可以,所有客户端和服务器端的证书这个字段需要填写域名,一定要注意的是,根证书的这个字段和客户端证书、服务器端证书不能一样;
- 其他所有字段的填写,根证书、服务器端证书、客户端证书需保持一致
- 最后的密码可以直接回车跳过。
经过上面三个命令行,我们最终可以得到一个签名有效期为10年的根证书root.crt,后面我们可以用这个根证书去颁发服务器证书和客户端证书。
3.2、 生成自签名服务器端证书
(1)生成服务器端证书私钥:
openssl genrsa -out server.key 1024
(2) 生成服务器证书请求文件,过程和注意事项参考根证书,本节不详述:
openssl req -new -out server.csr -key server.key
(3) 生成服务器端公钥证书
openssl x509 -req -in server.csr -out server.crt -signkey server.key -CA root.crt -CAkey root.key -CAcreateserial -days 3650
经过上面的三个命令,我们得到:
server.key:服务器端的密钥文件 server.crt:有效期十年的服务器端公钥证书,使用根证书和服务器端私钥文件一起生成
3.3、 生成自签名客户端证书
(1)生成客户端证书密钥:
openssl genrsa -out client.key 1024
openssl genrsa -out client2.key 1024
(2) 生成客户端证书请求文件,过程和注意事项参考根证书,本节不详述:
openssl req -new -out client.csr -key client.key
openssl req -new -out client2.csr -key client2.key
(3) 生客户端证书
openssl x509 -req -in client.csr -out client.crt -signkey client.key -CA root.crt -CAkey root.key -CAcreateserial -days 3650
openssl x509 -req -in client2.csr -out client2.crt -signkey client2.key -CA root.crt -CAkey root.key -CAcreateserial -days 3650
(4) 生客户端p12格式证书,需要输入一个密码,选一个好记的,比如123456
openssl pkcs12 -export -clcerts -in client.crt -inkey client.key -out client.p12
openssl pkcs12 -export -clcerts -in client2.crt -inkey client2.key -out client2.p12
重复使用上面的命令,我们得到两套客户端证书:
- client.key / client2.key:客户端的私钥文件
- client.crt / client2.key:有效期十年的客户端证书
使用根证书和客户端私钥一起生成 client.p12/client2.p12,这个证书文件包含客户端的公钥和私钥,主要用来给浏览器访问使用
4. keytool生成证书
使用双向认证的SSL/TLS协议通信,客户端和服务器端都要设置用于证实自己身份的安全证书,并且还要设置信任对方的哪些安全证书。
理论上一共需要准备四个文件,两个keystore文件和两个truststore文件。
通信双方分别拥有一个keystore和一个truststore,keystore用于存放自己的密钥和公钥,truststore用于存放所有需要信任方的公钥。
keytool -genkey -alias catserver -keyalg rsa -keysize 1024 -sigalg sha256withrsa -keypass huawei -keystore catserver.keystore -storepass huawei
keytool -genkey -alias foxclient -keyalg dsa -keysize 512 -sigalg sha1withdsa -keypass huawei -keystore foxclient.keystore -storepass huawei
keytool -export -alias catserver -keystore catserver.keystore -storepass huawei -file catserver.cer
keytool -export -alias foxclient -keystore foxclient.keystore -storepass huawei -file foxclient.cer
keytool -import -alias foxclient -keystore catservertrust.keystore -storepass huawei -file foxclient.cer
keytool -import -alias catserver -keystore foxclienttrust.keystore -storepass huawei -file catserver.cer
5. 使用Java调用
5.1、server代码
public class CatServer implements Runnable, HandshakeCompletedListener {
public static final int SERVER_PORT = 11123;
private final Socket _s;
private String peerCerName;
public CatServer(Socket s) {
_s = s;
}
public static void main(String[] args) throws Exception {
String serverKeyStoreFile = "D:\\code\\mycode\\java-study\\https\\src\\main\\resources\\double\\catserver.keystore";
String serverKeyStorePwd = "huawei";
String catServerKeyPwd = "huawei";
String serverTrustKeyStoreFile = "D:\\code\\mycode\\java-study\\https\\src\\main\\resources\\double\\catservertrust.keystore";
String serverTrustKeyStorePwd = "huawei";
KeyStore serverKeyStore = KeyStore.getInstance("JKS");
serverKeyStore.load(new FileInputStream(serverKeyStoreFile), serverKeyStorePwd.toCharArray());
KeyStore serverTrustKeyStore = KeyStore.getInstance("JKS");
serverTrustKeyStore.load(new FileInputStream(serverTrustKeyStoreFile), serverTrustKeyStorePwd.toCharArray());
KeyManagerFactory kmf = KeyManagerFactory.getInstance(KeyManagerFactory.getDefaultAlgorithm());
kmf.init(serverKeyStore, catServerKeyPwd.toCharArray());
TrustManagerFactory tmf = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm());
tmf.init(serverTrustKeyStore);
SSLContext sslContext = SSLContext.getInstance("TLSv1");
sslContext.init(kmf.getKeyManagers(), tmf.getTrustManagers(), null);
SSLServerSocketFactory sslServerSocketFactory = sslContext.getServerSocketFactory();
SSLServerSocket sslServerSocket = (SSLServerSocket) sslServerSocketFactory.createServerSocket(SERVER_PORT);
sslServerSocket.setNeedClientAuth(true);
while (true) {
SSLSocket s = (SSLSocket) sslServerSocket.accept();
CatServer cs = new CatServer(s);
s.addHandshakeCompletedListener(cs);
new Thread(cs).start();
}
}
@Override
public void run() {
try {
BufferedReader reader = new BufferedReader(new InputStreamReader(_s.getInputStream()));
PrintWriter writer = new PrintWriter(_s.getOutputStream(), true);
writer.println("Welcome~, enter exit to leave.");
String s;
while ((s = reader.readLine()) != null && !s.trim().equalsIgnoreCase("exit")) {
writer.println("Echo: " + s);
}
writer.println("Bye~, " + peerCerName);
} catch (Exception e) {
e.printStackTrace();
} finally {
try {
_s.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
@Override
public void handshakeCompleted(HandshakeCompletedEvent event) {
try {
X509Certificate cert = (X509Certificate) event.getPeerCertificates()[0];
peerCerName = cert.getSubjectX500Principal().getName();
} catch (SSLPeerUnverifiedException ex) {
ex.printStackTrace();
}
}
}
5.2、client代码
public class FoxClient {
public static void main(String[] args) throws Exception {
String clientKeyStoreFile = "D:\\code\\mycode\\java-study\\https\\src\\main\\resources\\double\\foxclient.keystore";
String clientKeyStorePwd = "huawei";
String foxclientKeyPwd = "huawei";
String clientTrustKeyStoreFile = "D:\\code\\mycode\\java-study\\https\\src\\main\\resources\\double\\foxclienttrust.keystore";
String clientTrustKeyStorePwd = "huawei";
KeyStore clientKeyStore = KeyStore.getInstance("JKS");
clientKeyStore.load(new FileInputStream(clientKeyStoreFile), clientKeyStorePwd.toCharArray());
KeyStore clientTrustKeyStore = KeyStore.getInstance("JKS");
clientTrustKeyStore.load(new FileInputStream(clientTrustKeyStoreFile), clientTrustKeyStorePwd.toCharArray());
KeyManagerFactory kmf = KeyManagerFactory.getInstance(KeyManagerFactory.getDefaultAlgorithm());
kmf.init(clientKeyStore, foxclientKeyPwd.toCharArray());
TrustManagerFactory tmf = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm());
tmf.init(clientTrustKeyStore);
SSLContext sslContext = SSLContext.getInstance("TLSv1");
sslContext.init(kmf.getKeyManagers(), tmf.getTrustManagers(), null);
SSLSocketFactory socketFactory = sslContext.getSocketFactory();
Socket socket = socketFactory.createSocket("localhost", CatServer.SERVER_PORT);
PrintWriter out = new PrintWriter(socket.getOutputStream(), true);
BufferedReader in = new BufferedReader(new InputStreamReader(socket.getInputStream()));
send("hello", out);
send("exit", out);
receive(in);
socket.close();
}
public static void send(String s, PrintWriter out) throws IOException {
System.out.println("Sending: " + s);
out.println(s);
}
public static void receive(BufferedReader in) throws IOException {
String s;
while ((s = in.readLine()) != null) {
System.out.println("Reveived: " + s);
}
}
}
参考文章:
更多内容关注微信公众号 ”前后端技术精选“,或者语雀,里面有更多知识:https://www.yuque.com/riverzmm/uu60c9?# 《安全》