Netty、MINA、Twisted一起學系列10:SSL / TLS

程式設計師私房菜發表於2019-02-01

文章已獲得作者授權,原文地址:xxgblog.com/2017/02/27/mina-netty-twisted-11

不使用 SSL/TLS 的網路通訊,一般都是明文傳輸,網路傳輸內容在傳輸過程中很容易被竊聽甚至篡改,非常不安全。SSL/TLS 協議就是為了解決這些安全問題而設計的。SSL/TLS 協議位於 TCP/IP 協議之上,各個應用層協議之下,使網路傳輸的內容透過加密演算法加密,並且只有伺服器和客戶端可以加密解密,中間人即使抓到資料包也無法解密獲取傳輸的內容,從而避免安全問題。例如廣泛使用的 HTTPS 協議即是在 TCP 協議和 HTTP 協議之間加了一層 SSL/TLS 協議,下一篇文章中有 HTTPS 更詳細的介紹。

1. 相關術語

在學習SSL/TLS協議之前,首先要了解一些相關概念:

對稱加密:加密和解密都採用同一個金鑰,常用的演算法有DES、3DES、AES,相對於非對稱加密演算法更簡單速度更快。

非對稱加密:和對稱加密演算法不同,非對稱加密演算法會有兩個金鑰:公鑰(可以公開的)和私鑰(私有的),例如客戶端如果使用公鑰加密,那麼即時其他人有公鑰也無法解密,只能透過伺服器私有的私鑰解密。RSA演算法即是典型的非對稱加密演算法。

數字證照:數字證照是一個包含公鑰並且透過權威機構發行的一串資料,數字證照很多需要付費購買,也有免費的,另外也可以自己生成數字證照,本文中將會採用自簽名的方式生成數字證照。

2. SSL/TLS流程

使用SSL/TLS協議的伺服器和客戶端開始通訊之前,會先進行一個握手階段:

1)客戶端發出請求:這一步客戶端會生成一個隨機數傳給伺服器。

2)伺服器回應:這一步伺服器會返回給客戶端一個伺服器數字證照(證照中包含用於加密的公鑰),另外伺服器也會生成一個隨機數給客戶端。

3)客戶端回應:這一步客戶端首先會校驗數字證照的合法性,然後會再生成一個隨機數,這個隨機數會使用第2步中的公鑰採用非對稱加密演算法(例如RSA演算法)進行加密後傳給伺服器,密文只能透過伺服器的私鑰來解密。

4)伺服器最後回應:握手結束。

握手結束後,客戶端和伺服器都有上面握手階段的三個隨機數。客戶端和伺服器都透過這三個隨機生成一個金鑰,接下來所有的通訊內容都使用這個金鑰透過對稱加密演算法加密傳輸,伺服器和客戶端才開始進行安全的通訊。

3. 生成私鑰和證照

使用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

4. SSL/TLS伺服器

接下來在Netty、MINA、Twisted一起學系列02:TCP訊息邊界問題及按行分割訊息 一文的基礎上,加上SSL/TLS層。

1)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");
   }
}

2)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();
   }
}

3)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()

5. 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();
       }

   }
}

來自 “ ITPUB部落格 ” ,連結:http://blog.itpub.net/31558358/viewspace-2565392/,如需轉載,請註明出處,否則將追究法律責任。

相關文章