什麼是SSL/TLS
不使用SSL/TLS的網絡通信,一般都是明文傳輸,網絡傳輸內容在傳輸過程中很容易被竊聽甚至篡改,非常不安全。SSL/TLS協議就是爲了解決這些安全問題而設計的。SSL/TLS協議位於TCP/IP協議之上,各個應用層協議之下,使網絡傳輸的內容通過加密算法加密,並且只有服務器和客戶端可以加密解密,中間人即使抓到數據包也無法解密獲取傳輸的內容,從而避免安全問題。例如廣泛使用的HTTPS協議即是在TCP協議和HTTP協議之間加了一層SSL/TLS協議。
相關術語
在學習SSL/TLS協議之前,首先要了解一些相關概念:
- 對稱加密:加密和解密都採用同一個密鑰,常用的算法有DES、3DES、AES,相對於非對稱加密算法更簡單速度更快。
- 非對稱加密:和對稱加密算法不同,非對稱加密算法會有兩個密鑰:公鑰(可以公開的)和私鑰(私有的),例如客戶端如果使用公鑰加密,那麼即時其他人有公鑰也無法解密,只能通過服務器私有的私鑰解密。RSA算法即是典型的非對稱加密算法。
- 數字證書:數字證書是一個包含公鑰並且通過權威機構發行的一串數據,數字證書很多需要付費購買,也有免費的,另外也可以自己生成數字證書,本文中將會採用自簽名的方式生成數字證書。
SSL/TLS流程
使用SSL/TLS協議的服務器和客戶端開始通信之前,會先進行一個握手階段:
- 客戶端發出請求:這一步客戶端會生成一個隨機數傳給服務器;
- 服務器迴應:這一步服務器會返回給客戶端一個服務器數字證書(證書中包含用於加密的公鑰),另外服務器也會生成一個隨機數給客戶端;
- 客戶端迴應:這一步客戶端首先會校驗數字證書的合法性,然後會再生成一個隨機數,這個隨機數會使用第2步中的公鑰採用非對稱加密算法(例如RSA算法)進行加密後傳給服務器,密文只能通過服務器的私鑰來解密。
- 服務器最後迴應:握手結束。
握手結束後,客戶端和服務器都有上面握手階段的三個隨機數。客戶端和服務器都通過這三個隨機生成一個密鑰,接下來所有的通信內容都使用這個密鑰通過對稱加密算法加密傳輸,服務器和客戶端纔開始進行安全的通信。
如果看到這裏還是一臉懵逼,可以參考SSL/TLS協議運行機制的概述更深入地瞭解SSL/TLS流程,本文不再過多介紹。
生成私鑰和證書
使用openssl來生成私鑰和證書:
openssl req -x509 -newkey rsa:2048 -nodes -days 365 -keyout private.pem -out cert.crt
運行以上命令後,會在當前目錄下生成一個私鑰文件(private.pem)和一個證書文件(cert.crt)。
生成的私鑰和證書Twisted、Netty可以直接使用,然而MINA對私鑰文件的格式的要求,要將pem格式轉換成der格式,實際上就是將文本文件私鑰轉成二進制文件私鑰。openssl將private.pem轉成private.der私鑰文件:
openssl pkcs8 -topk8 -inform PEM -in private.pem -outform DER -nocrypt -out private.der
SSL/TLS服務器
接下來在http://xxgblog.com/2014/08/21/mina-netty-twisted-2/一文的基礎上,加上SSL/TLS層。
MINA
MINA可以通過SslFilter來實現SSL/TLS,初始化SslFilter的代碼比較繁瑣:
public class MinaServer {
public static void main(String[] args) throws Exception {
String certPath = "/Users/wucao/Desktop/ssl/cert.crt"; // 證書
String privateKeyPath = "/Users/wucao/Desktop/ssl/private.der"; // 私鑰
// 證書
// https://docs.oracle.com/javase/7/docs/api/java/security/cert/X509Certificate.html
InputStream inStream = null;
Certificate certificate = null;
try {
inStream = new FileInputStream(certPath);
CertificateFactory cf = CertificateFactory.getInstance("X.509");
certificate = cf.generateCertificate(inStream);
} finally {
if (inStream != null) {
inStream.close();
}
}
// 私鑰
PKCS8EncodedKeySpec keySpec = new PKCS8EncodedKeySpec(Files.readAllBytes(new File(privateKeyPath).toPath()));
PrivateKey privateKey = KeyFactory.getInstance("RSA").generatePrivate(keySpec);
KeyStore ks = KeyStore.getInstance(KeyStore.getDefaultType());
ks.load(null, null);
Certificate[] certificates = {certificate};
ks.setKeyEntry("key", privateKey, "".toCharArray(), certificates);
KeyManagerFactory kmf = KeyManagerFactory.getInstance(KeyManagerFactory.getDefaultAlgorithm());
kmf.init(ks, "".toCharArray());
SSLContext sslContext = SSLContext.getInstance("TLS");
sslContext.init(kmf.getKeyManagers(), null, null);
IoAcceptor acceptor = new NioSocketAcceptor();
DefaultIoFilterChainBuilder chain = acceptor.getFilterChain();
chain.addLast("ssl", new SslFilter(sslContext)); // SslFilter需要放在最前面
chain.addLast("codec", new ProtocolCodecFilter(new TextLineCodecFactory(Charset.forName("UTF-8"), "\r\n", "\r\n")));
acceptor.setHandler(new TcpServerHandle());
acceptor.bind(new InetSocketAddress(8080));
}
}
class TcpServerHandle extends IoHandlerAdapter {
@Override
public void exceptionCaught(IoSession session, Throwable cause)
throws Exception {
cause.printStackTrace();
}
@Override
public void messageReceived(IoSession session, Object message)
throws Exception {
String line = (String) message;
System.out.println("messageReceived:" + line);
}
@Override
public void sessionCreated(IoSession session) throws Exception {
System.out.println("sessionCreated");
}
@Override
public void sessionClosed(IoSession session) throws Exception {
System.out.println("sessionClosed");
}
}
Netty
Netty通過添加一個SslHandler來實現SSL/TLS,相對MINA來說代碼就比較簡潔:
public class NettyServer {
public static void main(String[] args) throws InterruptedException, SSLException {
File certificate = new File("/Users/wucao/Desktop/ssl/cert.crt"); // 證書
File privateKey = new File("/Users/wucao/Desktop/ssl/private.pem"); // 私鑰
final SslContext sslContext = SslContextBuilder.forServer(certificate, privateKey).build();
EventLoopGroup bossGroup = new NioEventLoopGroup();
EventLoopGroup workerGroup = new NioEventLoopGroup();
try {
ServerBootstrap b = new ServerBootstrap();
b.group(bossGroup, workerGroup)
.channel(NioServerSocketChannel.class)
.childHandler(new ChannelInitializer<SocketChannel>() {
@Override
public void initChannel(SocketChannel ch)
throws Exception {
ChannelPipeline pipeline = ch.pipeline();
// SslHandler要放在最前面
SslHandler sslHandler = sslContext.newHandler(ch.alloc());
pipeline.addLast(sslHandler);
pipeline.addLast(new LineBasedFrameDecoder(80));
pipeline.addLast(new StringDecoder(CharsetUtil.UTF_8));
pipeline.addLast(new TcpServerHandler());
}
});
ChannelFuture f = b.bind(8080).sync();
f.channel().closeFuture().sync();
} finally {
workerGroup.shutdownGracefully();
bossGroup.shutdownGracefully();
}
}
}
class TcpServerHandler extends ChannelInboundHandlerAdapter {
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) {
String line = (String) msg;
System.out.println("channelRead:" + line);
}
@Override
public void channelActive(ChannelHandlerContext ctx) {
System.out.println("channelActive");
}
@Override
public void channelInactive(ChannelHandlerContext ctx) {
System.out.println("channelInactive");
}
@Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) {
cause.printStackTrace();
ctx.close();
}
}
Twisted
Twisted實現SSL/TLS也是非常簡單的,將reactor.listenTCP替換爲reactor.listenSSL即可
# -*- coding:utf-8 –*-
from twisted.protocols.basic import LineOnlyReceiver
from twisted.internet.protocol import Factory
from twisted.internet import reactor, ssl
sslContext = ssl.DefaultOpenSSLContextFactory(
'/Users/wucao/Desktop/ssl/private.pem', # 私鑰
'/Users/wucao/Desktop/ssl/cert.crt', # 公鑰
)
class TcpServerHandle(LineOnlyReceiver):
def connectionMade(self):
print 'connectionMade'
def connectionLost(self, reason):
print 'connectionLost'
def lineReceived(self, data):
print 'lineReceived:' + data
factory = Factory()
factory.protocol = TcpServerHandle
reactor.listenSSL(8080, factory, sslContext)
reactor.run()
SSL/TLS客戶端
這裏還是使用Java來寫一個SSL/TLS客戶端,用來測試以上三個服務器程序。需要注意的是,在上面SSL/TLS流程的介紹中,SSL/TLS握手階段的第2步服務器會將證書傳給客戶端,第3步客戶端會校驗證書的合法性,所以下面的代碼首先會讓客戶端信任openssl生成的證書,才能正確的完成SSL/TLS握手。
public class SSLClient {
public static void main(String args[]) throws Exception {
// 客戶端信任改證書,將用於校驗服務器傳過來的證書的合法性
String certPath = "/Users/wucao/Desktop/ssl/cert.crt";
InputStream inStream = null;
Certificate certificate = null;
try {
inStream = new FileInputStream(certPath);
CertificateFactory cf = CertificateFactory.getInstance("X.509");
certificate = cf.generateCertificate(inStream);
} finally {
if (inStream != null) {
inStream.close();
}
}
KeyStore ks = KeyStore.getInstance(KeyStore.getDefaultType());
ks.load(null, null);
ks.setCertificateEntry("cert", certificate);
TrustManagerFactory tmf = TrustManagerFactory.getInstance("sunx509");
tmf.init(ks);
SSLContext sslContext = SSLContext.getInstance("TLS");
sslContext.init(null, tmf.getTrustManagers(), null);
SSLSocketFactory socketFactory = sslContext.getSocketFactory();
Socket socket = null;
OutputStream out = null;
try {
socket = socketFactory.createSocket("localhost", 8080);
out = socket.getOutputStream();
// 請求服務器
String lines = "牀前明月光\r\n疑是地上霜\r\n舉頭望明月\r\n低頭思故鄉\r\n";
byte[] outputBytes = lines.getBytes("UTF-8");
out.write(outputBytes);
out.flush();
} finally {
// 關閉連接
out.close();
socket.close();
}
}
}
MINA、Netty、Twisted一起學系列
MINA、Netty、Twisted一起學(一):實現簡單的TCP服務器
MINA、Netty、Twisted一起學(二):TCP消息邊界問題及按行分割消息
MINA、Netty、Twisted一起學(三):TCP消息固定大小的前綴(Header)
MINA、Netty、Twisted一起學(四):定製自己的協議
MINA、Netty、Twisted一起學(五):整合protobuf
MINA、Netty、Twisted一起學(六):session
MINA、Netty、Twisted一起學(七):發佈/訂閱(Publish/Subscribe)
MINA、Netty、Twisted一起學(八):HTTP服務器
MINA、Netty、Twisted一起學(九):異步IO和回調函數
MINA、Netty、Twisted一起學(十一):SSL/TLS
MINA、Netty、Twisted一起學(十二):HTTPS