SSL与TLS,SSLTLS
SSL与TLS,SSLTLS
1.Java 密码术体系结构
概述
Java 平台的安全性和加密功能在过去几年里已经获得了巨大发展。JDK 1.4(又名 Merlin)发行版现在捆绑了许多安全性相关的包,包括 Java 密码术扩展(Java Cryptography Extension (JCE))、Java 安全套接字扩展(Java Secure Socket Extension (JSSE))以及 Java 认证和授权服务(Java Authentication and Authorization Service (JAAS))。所有这些组件都是 Java 密码术体系结构(JCA)的组成部分,如下图所示:
JCA 和 JSSE
JCA 的最重要特性之一是它不依赖于任何一种特殊的加密算法。每种广为人知的加密算法都各有利弊,且新加密算法的开发一直在进行。JCA 允许开发出新算法时将它们插入。它使用密码服务提供程序(CSP)的概念,密码服务提供程序类似于安全性插件。CSP 提供特殊算法的实现。JDK 1.4 捆绑了一些 CSP,这些 CSP(包括 SunJSSE)提供许多标准算法;总而言之,这些 CSP 足以满足大多数使用。
JSSE 为 Java 2 平台提供安全套接字通信。更精确地说,它实现了安全套接字层(Secure Socket Layer (SSL))和传输层安全性(Transport Layer Security (TLS)),这是两种实现因特网上安全通信的标准化协议。SSL 和 TLS 都依赖于公钥密码术,它在下一页中描述。
公钥密码术
许多密码算法存在的一个问题是它们需要分发共享密钥。密码就是一个很好的共享密钥示例。共享密钥存在的问题是:在能启动安全通信之前,必须在通信实体之间共享它们。然而,该共享过程可能受到窃听,这会导致先有鸡还是先有蛋的问题:在我们能安全地交换数据之前,必须首先安全地交换密钥。
1976 年,Whitfield Diffie 和 Martin 通过公钥密码术的创建解决了这个问题。在 Diffie-Hellman 公钥系统中,每个通信方都拥有一对密钥 ― 一个公钥和一个私钥。私钥只有通信方才知道,而公钥可以提供给任何人。使用其中一个密钥加密的数据只能使用另一个密钥解密。
这样,如果想创建只能由特殊的一方读取的消息,则您使用他们的公钥进行加密,然后他们使用其私钥来解密该消息。
同样,如果您使用私钥加密消息,则拥有您公钥副本的任何人都可以使用它来解密该消息。这使接收端的人员确信消息来自于您而不是别人,因为只有您拥有自己的私钥。用这种方法加密的消息具有您的数字签名。
证书和认证中心
证书是由可信方数字签署以证明它是有效公钥。这个可信方称为认证中心(CA)。在某种意义上,CA 为公钥确实属于拥有它的人提供了证明书。
您可以付费使用商业 CA,或者创建您自己的证书 ― 它完全取决于当在数字领域中证明身份时,您想掌握多少权限。如果一个实体签署了它自己的公钥,则称该公钥为自签署证书。在本教程中,我们会一直使用自签署证书。
SSL 和 TLS
正如前面提到的,JSSE 框架与 SunJSSE 提供程序一起实现了 SSL 和 TLS 协议组 ― TLS 目前是 SSL 的最新版本。
SSL 使用公钥密码术来交换一组共享密钥,然后使用标准共享密钥加密来交换数据。共享密钥同时用于加密数据(使别人无法读取数据)以及认证数据(确保数据不是来自冒名顶替者)。
SSL 握手协议
在可以通过 SSL 连接发送数据之前,两端必须协商并交换密钥信息。这称为握手协议。这里我们将不会过多讨论有关握手协议的详细信息,因为它对于我们的目的来说不是必需的。为实现我们的目的,您需要了解握手涉及下列步骤:
-
服务器将其证书发送到客户机,然后客户机验证服务器证书。
-
客户机将其证书发送到服务器,然后服务器验证客户机证书。
- 客户机使用服务器的公钥对密码信息进行加密,然后将它发送到服务器。连接的每一端都使用这个密码信息生成相同的秘钥,然后使用这些秘钥来发送数据。
客户机认证(第 2 步)是可选的:服务器可以请求客户机提供它的证书,但它不是必需作出这样的请求。在示例中,我们将使用客户机认证。
密钥文件
JSSE 实现 SSL 和 TLS 协议。这些协议使用公钥加密以确保通过因特网发送的消息的隐私性。在公钥加密系统中,客户机和服务器都必须有一对密钥:一个公钥和一个私钥。在传递消息之前,我们必须生成这些密钥。
一旦生成了密钥,我们将为客户机端提供包含其公钥和私钥的文件。该文件还包括服务器的公钥证书的副本。密钥存储在一个称为密钥库(keystore)的特别格式文件中。
下面的表描述了我们将使用的密钥库文件。
密钥库文件 | 所含内容 | 发送目的地 |
client.private | 客户机的公钥/私钥对 | 客户机端 |
server.public | 服务器的公钥证书 | 客户机端 |
server.private | 服务器的公钥/私钥对 | 服务器端 |
client.public | 客户机公钥证书 | 服务器端 |
密钥保护
服务器也有一个包含其自身公钥和私钥以及客户机的公钥证书的文件。回想一下,可以自由给出公钥 ― 没必要向任何其它一方隐藏它们。
客户机/服务器连接的每一端只有它正常工作所需的密钥文件,这是很重要的。特别是,只有服务器才有其自身私钥的副本,这一点很重要。如果这一密钥落入坏人之手,损失将会很惨重,因为它将在本质上允许坏家伙用服务器标识伪装自己。
2.密钥管理
概述
密钥生成和操作是使用 keytool 程序执行的,它包含在 JDK 1.4 的 JSSE 包中。keytool 可用于各种用途。这里,我们将使用它来创建公钥/私钥对,然后从这些对中抽取公钥证书并将它们放置在其自身的文件中。
生成密钥对
下列 keytool 命令用于生成新的公钥/私钥对:
keytool -genkey -keystore [filename] |
当运行这个命令时,将询问您一系列问题。这些问题把您看作一个实体(您的姓名、组织和类似信息)。您提供的信息将用于创建自签署证书,该证书用公钥关联信息并证明该关联的真实性。还会要求您输入密钥库的密码,以及您正在创建的密钥对的密码(可选)。
从命令行着手
下面是一条完整命令,它生成公钥/私钥对并且它指定了所有必需的实体信息,而不询问任何有关身份的问题;那些信息是直接在命令行上提供的。随后的表说明了命令中的每个选项。
keytool -genkey -alias clientprivate -keystore client.private -storetype JKS -keyalg rsa -dname "CN=Your Name, OU=Your Organizational Unit, O=Your Organization, L=Your City, S=Your State, C=Your Country" -storepass clientpw -keypass clientpw |
选项 | 含义 |
-genkey | 告诉 keytool 生成密钥对。 |
-alias clientprivate | 标识密钥库中的新密钥对。 |
-keystore client.private | 将文件 client.private 用作密钥库。 |
-storetype JKS | 声明密钥库的类型。JKS 是缺省值。 |
-keyalg rsa | 声明要使用的算法;我们正在使用 RSA 公钥算法,它是缺省值。 |
-dname "CN=Your Name..." | 提供有关拥有密钥对的实体的信息。 |
-storepass clientpw | 指定整个密钥库的密码。 |
-keypass clientpw | 指定新密钥对的密码。 |
客户机和服务器密钥对
准备算法的第一步是为客户机和服务器各生成一个公钥/私钥对。下列命令将生成文件 client.private,它是客户机的密钥对:
keytool -genkey -alias clientprivate -keystore client.private -storetype JKS -keyalg rsa -dname "CN=Client Name, OU=Client Organizational Unit, O=Client Organization, L=Client City, S=Client State, C=Client Country" -storepass clientpw -keypass clientpw |
下面是生成文件 server.private 的命令,它是服务器的密钥对:
keytool -genkey -alias serverprivate -keystore server.private -storetype JKS -keyalg rsa -dname "CN=Server Name, OU=Server Organizational Unit, O=Server Organization, L=Server City, S=Server State, C=Server Country" -storepass serverpw -keypass serverpw |
正如密钥文件提到的,需要将客户机和服务器的私钥文件安装在特定位置。client.private 文件安装在客户机端;server.private 文件安装在服务器端。
抽取公钥
下一步是抽取公钥以便将它们与客户机和服务器一起安装。具体说,客户机端软件必须拥有服务器端的公钥,反之亦然。
为了获取公钥,我们从 client.private 和 server.private 文件中抽取公钥,并将它们放在临时文件中。然后,将它们插入各自密钥库中,分别称为 client.public 和 server.public。
将公钥从专用密钥库复制到公用密钥库 ― 例如,从 client.private 到 client.public 时,使用临时密钥文件 temp.key 来暂时保存每个公钥。完成导出/导入过程后,您会想要从当前目录中除去 temp.key。
导出/导入命令
我们将使用 keytool -export
命令将公钥抽取到一个文件,然后使用 keytool -import
命令将它插入新的密钥库。下面是抽取客户机公钥的命令:
keytool -export -alias clientprivate -keystore client.private -file temp.key -storepass clientpw |
还有将客户机私钥插入其自身密钥库的命令:
keytool -import -noprompt -alias clientpublic -keystore client.public -file temp.key -storepass clientpublic |
我们还将抽取并存储服务器的公钥。下面是抽取密钥的命令:
keytool -export -alias serverprivate -keystore server.private -file temp.key -storepass serverpw |
还有将密钥放入其自身密钥库的命令:
keytool -import -noprompt -alias serverpublic -keystore server.public -file temp.key -storepass serverpublic |
脚本 generatekeys.sh(用于 UNIX)以及 generatekeys.bat(用于 DOS 或 Microsoft Windows)自动为您生成客户机和服务器密钥文件,然后清除任何临时文件。(文章末尾提供下载)
3.使用 JSSE 套接字
概述
因为我们想使通信是私有的,所以将使用 JSSE 安全套接字替代常规套接字。除了安全套接字透明地对通过它们传递的任何数据进行加密外,使用安全套接字的方法与使用常规套接字的方法是相同的。
开始讨论 JSSE 如何创建和管理安全连接之前,我们应该回顾一下如何启动和接受常规的、非安全连接。
非安全套接字:回顾
下列代码片段通常用于启动套接字连接。这个示例创建至远程计算机 host
上的端口 port
的新的 Socket
连接:
Socket socket = new Socket( host, port ); |
类似地,下列代码演示了如何侦听进入的连接。这个示例创建了侦听端口 port 的 ServerSocket
,然后进入无限循环,接受并处理进入的连接:
ServerSocket serverSocket = new ServerSocket( port ); while (true) { Socket socket = serverSocket.accept(); doSomethingWithNewConnection( socket ); } |
安全套接字以一种非常类似的方式工作,但是在我们可以实现它们以用于示例之前,必须完成几个步骤。我们将在后面几页完成这些步骤。
连接设置
要启动到远程服务器的安全套接字连接,我们必须执行下列步骤:
-
创建
SecureRandom
,它是安全随机数的来源。安全随机数是随机程度很高的数,它足以使加密免受攻击。 -
创建包含远程服务器公钥的
KeyStore
对象。这是从 server.public 读取的。 -
创建包含客户机公钥/私钥对(包括其公钥证书)的
KeyStore
对象。这是从 client.private 读取的。 -
从远程服务器的
KeyStore
创建TrustManagerFactory
。这用来认证远程服务器。 -
从客户机的
KeyStore
创建KeyManagerFactory
。这用来加密和解密数据。 -
使用
KeyManagerFactory
、TrustManagerFactory
和SecureRandom
创建SSLContext
对象。 -
使用
SSLContext
创建SSLSocketFactory
。 -
使用
SSLSocketFactory
创建SSLSocket
,除了是安全的以外,其操作和常规Socket
一样。
侦听设置
要侦听进入的连接,我们必须执行一组类似步骤:
-
创建
SecureRandom
,它是安全随机数的来源。 -
创建包含远程客户机公钥的
KeyStore
对象。这是从 client.public 读取的。 -
创建包含服务器公钥/私钥对的(包括其公钥证书)
KeyStore
对象。这是从 server.private 读取的。 -
从远程客户机的
KeyStore
创建TrustManagerFactory
。这用来认证远程客户机。 -
从服务器的
KeyStore
创建KeyManagerFactory
。这用来加密和解密数据。 -
使用
KeyManagerFactory
、TrustManagerFactory
和SecureRandom
创建SSLContext
对象。 -
使用
SSLContext
创建SSLServerSocketFactory
。 -
使用
SSLServerSocketFactory
创建SSLServerSocket
,除了是安全的以外,其操作和常规ServerSocket
一样。 -
调用
SSLServerSocket
的accept()
方法以等待进入的连接。
这些步骤都相当复杂,但是过程每次都是相同的,因此进行下去并观察它是如何工作的是很有意义的。在后面几页中,我们将遍历执行这些步骤的代码。我们将只详细地检查客户机端过程,因为服务器端过程几乎是相同的。接下来,我们将对两端之间的差别进行说明。
创建 SecureRandom
创建 SecureRandom
对象很容易;只要在您的代码中使用下列行:
secureRandom = new SecureRandom(); secureRandom.nextInt(); |
第一行实际创建 SecureRandom
。创建 SecureRandom
需要很多计算,并且这一计算可能直到实际使用时才会执行。通过在这里调用nextInt()
方法,我们开始了创建过程,并且确保在程序开始时就发生延迟,而不是以后再发生延迟,那时会给我们带来不便。
创建密钥库
接下来,我们需要创建一些 KeyStore
对象。我们使用静态方法 KeyStore.getInstance()
创建一个空的 KeyStore
,然后使用其load()
方法对它进行初始化,如下所示:
private void setupServerKeystore() throws GeneralSecurityException, IOException { serverKeyStore = KeyStore.getInstance( "JKS" ); serverKeyStore.load( new FileInputStream( "server.public" ), "serverpublic".toCharArray() ); } |
注:我们已经创建了 "JKS"
类型的 KeyStore
;这是 JCA 中使用的标准密钥库格式。
读取密钥库
在前面几页中,我们从包含服务器端公钥的 server.public 中读取密钥信息。我们还需要从 client.private 中读取客户机密钥对,如下所示:
private void setupClientKeyStore() throws GeneralSecurityException, IOException { clientKeyStore = KeyStore.getInstance( "JKS" ); clientKeyStore.load( new FileInputStream( "client.private" ), passphrase.toCharArray() ); } |
设置工厂
下一步是使用我们已经创建的 KeyStore
对象来初始化密钥并信任管理器工厂。我们将从服务器密钥库创建 TrustManagerFactory
;这将用来认证(即,开始信任)远程服务器:
TrustManagerFactory tmf = TrustManagerFactory.getInstance( "SunX509" ); tmf.init( serverKeyStore ); |
注:TrustManagerFactory
的类型是 "SunX509"
;509 是我们在这一程序中始终使用的证书协议名称。在第二行代码中,TrustManagerFactory
是用服务器的密钥库装入的。
我们还必须从客户机的 KeyStore
创建 KeyManagerFactory
,如下所示:
KeyManagerFactory kmf = KeyManagerFactory.getInstance( "SunX509" ); kmf.init( clientKeyStore, passphrase.toCharArray() ); |
创建 SSLContext
我们快要结束安全套接字设置了,因此再坚持一下!下一步是创建 SSLContext
。SSLContext
包含目前为止我们提到的所有密钥和证书信息,并且使用它来创建 SSLSocketFactory
,接着再由 SSLSocketFactory
创建安全套接字。
一旦您在应用程序开始处创建了 SSLContext
,就可以将它用于您需要建立的每个连接,只要每个连接使用同一密钥。
要创建 SSLContext
,我们使用工厂和 SecureRandom
,如下所示:
sslContext = SSLContext.getInstance( "TLS" ); sslContext.init( kmf.getKeyManagers(), tmf.getTrustManagers(), secureRandom ); |
注:我们已经创建了 "TLS"
类型的 SSLContext
。正如您所猜想的,这代表传输层安全性,它是安全套接字层或 SSL 的新名称。然后,我们使用前几步创建的 TrustManagerFactory
和 KeyManagerFactory
对象来对它进行初始化。
建立连接
这是最后一步了。目前为止我们所做的所有工作为我们提供了 SSLContext
,我们将使用它来建立到远程机器的连接,如下面的代码样本所示:
SSLSocketFactory sf = sslContext.getSocketFactory(); SSLSocket socket = (SSLSocket)sf.createSocket( host, port ); |
我们已经建立了到机器 host 上的端口 port 的安全连接。
服务器端设置
设置服务器端与设置客户机端基本相同,因此我们将不会详细讨论。当然,服务器从 client.public 和 server.private 读取其密钥信息而不是从 server.public 和 client.private 中读取。
另外,执行最后一步(建立连接)的代码对于服务器端有一点差别,如下所示:
SSLServerSocketFactory sf = sslContext.getServerSocketFactory(); SSLServerSocket ss = (SSLServerSocket)sf.createServerSocket( port ); ss.setNeedClientAuth( true ); |
注:我们调用了 SSLServerSocket.setNeedClientAuth()
。这是表示客户机应该对自己进行认证的服务器调用。缺省情况下,客户机应用程序不会对它们自己进行认证,因此如果您想让客户机认证成为握手过程的一部分,则必须进行这一调用。
附件:
相关文章
- 暂无相关文章
用户点评