为高级 JSSE 开发人员定制 SSL(二)

SimpleSSLClient 内幕

我们将研究的第一个客户机应用程序根本不能做什么。但是,在后面的示例中我们会扩展它来阐述更高级的功能。设置 SimpleSSLClient 的目的是为了方便地添加子类。打算覆盖下面四个方法:

  • main() 当然是在从命令行运行类时被调用。对于每个子类, main() 必须构造一个合适类的对象,并调用对象上的 runClient()close() 方法。这些方法是在超类 ― SimpleSSLClient 上提供的,并且不打算被覆盖。
  • handleCommandLineOption()displayUsage() 允许每个子类在命令行上添加选项,而无需更新父类。它们都从 runClient() 方法调用。
  • getSSLSocketFactory() 是一个有趣的方法。JSSE 安全套接字始终是从 SSLSocketFactory 对象构造的。通过构造一个定制的套接字工厂,我们可以定制 JSSE 的行为。为了将来练习的目的,每个 SimpleSSLClient 子类都实现该方法,并相应定制 SSLSocketFactory

目前,SimpleSSLClient 仅能理解 -host-port 参数,这允许用户把客户机指向服务器。在这第一个基本示例中, getSSLSocketFactory 返回(JVM 范围的)缺省工厂,如下所示:

protected SSLSocketFactory getSSLSocketFactory()
  throws IOException, GeneralSecurityException
{
  return (SSLSocketFactory)SSLSocketFactory.getDefault();
}

从子类的 main() 方法调用的 runClient() 方法,负责处理命令行参数,然后从子类获取 SSLSocketFactory 来使用。然后它使用 connect() 方法连接到服务器,并且使用 transmit() 方法在安全通道上开始传输数据。

connect() 方法相当简单。在使用 SSLSocketFactory 连接到服务器之后,它调用安全套接字上的 startHandshake 。这迫使 JSSE 完成 SSL 握手阶段,并因而触发服务器端上的 HandshakeCompletedListener 。尽管 JSSE 确实会自动启动握手,但是仅当数据首次通过套接字发送时它才这样做。因为用户在键盘上输入消息之前我们不会发送任何数据,但是我们希望服务器立即报告连接,所以我们需要使用 startHandshake 强制进行握手。

transmit() 方法同样相当简单。它的首要任务把输入源包装到适当的 Reader ,如下所示:

 
BufferedReader reader=new BufferedReader(
  new InputStreamReader(in));

我们使用 BufferedReader ,因为它将帮我们把输入分割成单独的行。

接下来, transmit() 方法把输出流 ― 在本案例中,由安全套接字提供 OutputStream ― 包装到适当的 Writer 中。服务器希望文本是以 UTF-8 编码的,因此我们可以让 OutputStreamWriter 使用下列编码:

 
writer=new OutputStreamWriter(socket.getOutputStream(), "UTF-8");

主循环很简单;正如您在清单 3 中看到的,它看起来更象 SimpleSSLServer 中 InputDisplayer 的主循环:

清单 3. SimpleSSLClient 主循环
boolean done=false;
while (!done) {
  String line=reader.readLine();
  if (line!=null) {
    writer.write(line);
    writer.write('/n');
    writer.flush();
  }
  else done=true;
}

基本的 JSSE 服务器和客户机代码就只有这些。现在,我们可以继续扩展 SimpleSSLClient,并且看看一些其它 getSSLSocketFactory 实现。

自制的 KeyStore

还记得我们是如何运行 SimpleSSLClient 的吗?命令如下:

 
java -Djavax.net.ssl.keyStore=clientKeys
   -Djavax.net.ssl.keyStorePassword=password 
   -Djavax.net.ssl.trustStore=clientTrust 
   -Djavax.net.ssl.trustStorePassword=password SimpleSSLClient

命令简直太长了!幸运的是,该示例及接下来的示例将为您演示如何设置一个带有到 KeyStore 和 TrustStore 的硬编码路径的 SSLSocketFactory 。除了减少上述命令的长度之外,您将学习的技术将允许您设置多个 SSLSocketFactory 对象,每个对象都带有不同的 KeyStore 和 TrustStore 设置。如果没有这种配置,JVM 中的每个安全连接必须使用相同的 KeyStore 和 TrustStore。尽管对于较小的应用程序而言这是可以接受的,但是较大的应用程序可能需要连接到多个代表许多不同用户的对等方。

介绍 CustomKeyStoreClient

对于第一个示例,我们将使用示例应用程序 CustomKeyStoreClient(可在本文的源代码中找到)来动态定义一个 KeyStore。在研究源代码之前,让我们看看正在使用的 CustomKeyStoreClient。对于这个练习,我们将指定 TrustStore 而不是 KeyStore。在 CustomKeyStoreClient 命令行上输入下列参数,我们将看到出现的结果:

 
java -Djavax.net.ssl.trustStore=clientTrust
   -Djavax.net.ssl.trustStorePassword=password CustomKeyStoreClient

假定客户机连接良好,服务器将报告说提供的证书是有效的。连接成功,因为 CustomKeyStoreClient.java 已经硬编码了 KeyStore 的名称( clientKeys )和密码( password )。如果您为客户机 KeyStore 选择了另外的文件名或密码,那么可以使用新的命令行选项 -ks-kspass 来指定它们。

研究一下 CustomKeystoreClient.java 的源代码, getSSLSocketFactory 做的第一件事是调用助手方法 getKeyManagers() 。稍后我们将考虑这是如何工作的;目前只是注明它返回 KeyManager 对象数组,已经利用必需的 KeyStore 文件和密码对其进行了设置。

清单 4. CustomKeyStoreClient.getSSLSocketFactory
protected SSLSocketFactory getSSLSocketFactory()
  throws IOException, GeneralSecurityException
{
  // Call getKeyManagers to get suitable key managers
  KeyManager[] kms=getKeyManagers();
  // Now construct a SSLContext using these KeyManagers. We
  // specify a null TrustManager and SecureRandom, indicating that the
  // defaults should be used.
  SSLContext context=SSLContext.getInstance("SSL");
  context.init(kms, null, null);
  // Finally, we get a SocketFactory, and pass it to SimpleSSLClient.
  SSLSocketFactory ssf=context.getSocketFactory();
  return ssf;
}

获得 KeyManager 数组之后, getSSLSocketFactory 执行一些对所有 JSSE 定制通常都很重要的设置工作。为了构造 SSLSocketFactory ,应用程序获取一个 SSLContext 实例,对其进行初始化,然后使用 SSLContext 生成一个 SSLSocketFactory

当得到 SSLContext 时,我们指定 "SSL" 的协议;我们也可以在这放入特定的 SSL(或 TLS)协议版本,并且强制通信在特定的级别发生。通过指定 "SSL" ,我们允许 JSSE 缺省至它能支持的最高级别。

SSLContext.init 的第一个参数是要使用的 KeyManager 数组。第二个参数(这里保留为 null)类似于 TrustManager 对象数组,稍后我们将使用它们。通过让第二个参数为 null,我们告诉 JSSE 使用缺省的 TrustStore,它从 javax.net.ssl.trustStorejavax.net.ssl.trustStorePassword 系统属性挑选设置。第三个参数允许我们覆盖 JSSE 的随机数生成器(RNG)。RNG 是 SSL 的一个敏感领域,误用该参数会致使连接变得不安全。我们让该参数为 null,这样允许 JSSE 使用缺省的 ― 并且安全的!― SecureRandom 对象。

装入 KeyStore

接下来,我们将研究 getKeyManagers 如何装入和初始化 KeyManagers 数组。先从清单 5 中的代码开始,然后我们将讨论正在发生什么。

清单 5. 装入和初始化 KeyManagers
protected KeyManager[] getKeyManagers()
  throws IOException, GeneralSecurityException
{
  // First, get the default KeyManagerFactory.
  String alg=KeyManagerFactory.getDefaultAlgorithm();
  KeyManagerFactory kmFact=KeyManagerFactory.getInstance(alg);
    
  // Next, set up the KeyStore to use. We need to load the file into
  // a KeyStore instance.
  FileInputStream fis=new FileInputStream(keyStore);
  KeyStore ks=KeyStore.getInstance("jks");
  ks.load(fis, keyStorePassword.toCharArray());
  fis.close();
  // Now we initialize the TrustManagerFactory with this KeyStore
  kmFact.init(ks, keyStorePassword.toCharArray());
  // And now get the TrustManagers
  KeyManager[] kms=kmFact.getKeyManagers();
  return kms;
}

首要工作是获取 KeyManagerFactory ,但是要这样做,我们需要知道将使用哪种算法。幸运的是,JSSE 使缺省的 KeyManagerFactory 算法可用。可以使用 ssl.KeyManagerFactory.algorithm 安全性属性配置缺省算法。

接下来, getKeyManagers() 方法装入 KeyStore 文件。这其中包括从文件建立一个 InputStream 、获取一个 KeyStore 实例,以及从 InputStream 装入 KeyStore 。除了 InputStreamKeyStore 需要知道流的格式(我们使用缺省的 "jks" )和存储密码。存储密码必须作为字符数组提供。

CustomKeyStoreClient 包导入

为了访问 KeyStore 类,我们必须导入 javax.net.ssljava.security.cert 。其它类(如 SSLContextKeyManagerFactory )从 J2SE 1.4 起,是 javax.net.ssl 的成员。在 J2SE 1.2 或 1.3 中,这些类的位置不是标准的;例如,Sun JSSE 实现把它们放在 com.sun.net.ssl 中。

要说明的一个可能很有用的窍门是, KeyStore.load 会获取任何 InputStream 。您的应用程序可以从任何地方构建这些流;除了文件,您可以通过网络、从移动设备获取流,或者甚至直接生成流。

装入 KeyStore 之后,我们使用它来初始化以前创建的 KeyManagerFactory 。我们需要再次指定一个密码,这次是单独的证书密码。通常,对于 JSSE 而言,KeyStore 中的每个证书都需要具备与 KeyStore 本身相同的密码。自己构造 KeyManagerFactory 可以克服这个限制。

KeyManagerFactory 初始化之后,它通常使用 getKeyManagers() 方法获取相应的 KeyManager 对象的数组。

对于 CustomKeyStoreClient 而言,我们已经研究了如何从任意的位置(本文使用本地文件系统)装入 KeyStore,以及如何让证书和 KeyStore 本身使用不同的密码。稍后我们将研究如何允许 KeyStore 中的每个证书拥有不同的密码。尽管在本示例中我们着重于客户机端,但是,我们可以在服务器端使用相同的技术来构建适当的 SSLServerSocketFactory 对象。

CustomTrustStoreClient 包导入

同样,本示例中使用的类会出现在不同 JSSE 供应商的不同包中。在 J2SE 1.4 中, TrustManagerFactory 位于 javax.net.ssl 中;在 J2SE 1.2 或 1.3 中,通常它位于 com.sun.net.ssl 中。

使用您自己的 TrustStore

覆盖 JSSE 的缺省 TrustStore 非常类似于我们刚才用 KeyStore 所做的工作,这并不令人惊奇。我们已经设置了 CustomTrustStoreClient(可在本文的源代码中找到)来使用硬编码的 KeyStore 和硬编码的 TrustStore。开始运行它所需做的全部工作就是输入命令: java CustomTrustStoreClient

CustomTrustStoreClient 希望 KeyStore 将是一个名为 clientKeys 并且密码为 password 的文件。希望 TrustStore 将是一个名为 clientTrust ,密码为 password 的文件。就象使用 CustomKeyStoreClient 一样,可以使用 -ks-kspass-ts-tspass 参数覆盖这些缺省值。

getSSLSocketFactory() 在许多方面与 CustomKeyStoreClient 中相同方法是一样的。我们甚至从 CustomKeyStoreClient 调用 getKeyManagers() 方法来获取与前面示例中相同的定制的 KeyManager 对象数组。但是这时 getSSLSocketFactory 还必须获取一个定制的 TrustManager 对象数组。在清单 6 中,我们可以看到 getSSLSocketFactory 如何使用助手方法 getTrustManagers() 获取定制的 TrustManager 对象:

清单 6. getSSLSocketFactory 如何使用 TrustManagers
protected SSLSocketFactory getSSLSocketFactory()
  throws IOException, GeneralSecurityException
{
  // Call getTrustManagers to get suitable trust managers
  TrustManager[] tms=getTrustManagers();
    
  // Call getKeyManagers (from CustomKeyStoreClient) to get suitable
  // key managers
  KeyManager[] kms=getKeyManagers();
  // Next construct and initialize a SSLContext with the KeyStore and
  // the TrustStore. We use the default SecureRandom.
  SSLContext context=SSLContext.getInstance("SSL");
  context.init(kms, tms, null);
  // Finally, we get a SocketFactory, and pass it to SimpleSSLClient.
  SSLSocketFactory ssf=context.getSocketFactory();
  return ssf;
}

这时,当初始化上下文时,我们覆盖了 KeyStore 和 TrustStore。但是,我们仍然让 JSSE 通过传递 null 作为第三个参数来使用它缺省的 SecureRandom

getTrustManagers 也非常类似于 CustomKeyStoreClient 的等价物同样不足为奇,如清单 7 所示:

清单 7. 装入和初始化 TrustManagers
protected TrustManager[] getTrustManagers()
  throws IOException, GeneralSecurityException
{
  // First, get the default TrustManagerFactory.
  String alg=TrustManagerFactory.getDefaultAlgorithm();
  TrustManagerFactory tmFact=TrustManagerFactory.getInstance(alg);
    
  // Next, set up the TrustStore to use. We need to load the file into
  // a KeyStore instance.
  FileInputStream fis=new FileInputStream(trustStore);
  KeyStore ks=KeyStore.getInstance("jks");
  ks.load(fis, trustStorePassword.toCharArray());
  fis.close();
  // Now we initialize the TrustManagerFactory with this KeyStore
  tmFact.init(ks);
  // And now get the TrustManagers
  TrustManager[] tms=tmFact.getTrustManagers();
  return tms;
}

就象以前一样, getTrustManagers() 方法首先根据缺省算法实例化一个 TrustManagerFactory 。然后将 TrustStore 文件装入 KeyStore 对象 ― 是的,命名不大恰当 ― 并且初始化 TrustManagerFactory

跟 KeyStore 等价物不同,请注意,当初始化 TrustManagerFactory 时,无需提供密码。不象私钥,可信的证书无需利用单独的密码进行保护。

到目前为止,我们已经研究了如何动态地覆盖 KeyStore 和 TrustStore。到这两个示例都完成时,您应该非常清楚如何设置 KeyManagerFactoryTrustManagerFactory ,并使用这些来“播种”一个 SSLContext 。最后的示例有点烦琐:我们将构建自己的 KeyManager 实现。

定制 KeyManager 设置:选择别名

当运行客户机应用程序的以前版本时,您是否注意到了服务器显示的是哪个证书 DN?我们故意设置客户机 KeyStore 以获得两个可接受的证书,一个用于 Alice,另一个用于 Bob。在这个案例中,JSSE 将选择任何一个它认为合适的证书。在我的安装中,似乎始终选取 Bob 的证书,但是您的 JSSE 的行为可能有所不同。

我们的示例应用程序 ― SelectAliasClient 允许您选择提供哪个证书。因为我们在 Keystore 中按照别名 alicebob 命名了每个证书,所以要选择 Alice 的证书可输入命令: java SelectAliasClient -alias alice

当客户机连接并且 SSL 握手完成时,服务器将用如下所示进行响应:

1: New connection request
1: Request from CN=Alice, OU=developerWorks, O=IBM, L=Winchester, 
      ST=Hampshire, C=UK

(或者创建 Alice 的证书时所选的任何值)。类似地,如果选择 Bob 的证书,请输入: java SelectAliasClient -alias bob ,服务器将报告下述信息:

2: New connection request
2: Request from CN=Bob, OU=developerWorks, O=IBM, L=Winchester, 
      ST=Hampshire, C=UK

定制 KeyManager 实现

为了强制选择一个特殊的别名,我们将编写一个 X509KeyManager 实现, KeyManager 通常由 JSSE 使用来进行 SSL 通信。我们的实现将包含一个真正的 X509KeyManager ,并且简单地通过它传递大多数的调用。它拦截的唯一方法是 chooseClientAlias() ;我们的实现检查以便了解所需的别名有效还是无效,如果有效,则返回它。

在 SSL 握手阶段, X509KeyManager 接口使用许多方法来检索密钥,然后使用它来标识对等方。在 参考资料部分可以找到所有方法的参考。下列方法对于本练习很重要:

  • getClientAliases()getServerAliases() 分别为使用 SSLSocketSSLServerSocket 提供了有效的别名数组。
  • chooseClientAlias()chooseServerAlias() 返回单个有效的别名。
  • getCertificateChain()getPrivateKey() 每个都把别名作为参数,并返回有关已标识证书的信息。

定制 KeyManager 中的控制流

控制流的工作如下所示:

  1. JSSE 调用 chooseClientAlias 以发现要使用的别名。
  2. chooseClientAlias 在真实的 X509KeyManager 上调用 getClientAliases 来发现一个有效的别名列表,以便于它能检查所需的别名是否有效。
  3. JSSE 通过指定正确的别名调用 X509KeyManagergetCertificateChaingetPrivateKey ,X509KeyManager 让调用可以访问被包装的 KeyManager。

KeyManager AliasForcingKeyManager()chooseClientAlias() 方法实际上需要多次调用 getClientAliases() ,一次对应一个 JSSE 安装支持的密钥类型,如清单 8 所示:

清单 8. 强制别名的选择
public String chooseClientAlias(String[] keyType, Principal[] issuers,
                                Socket socket)
{
  // For each keyType, call getClientAliases on the base KeyManager
  // to find valid aliases. If our requested alias is found, select it
  // for return.
  boolean aliasFound=false;
  for (int i=0; i<keyType.length && !aliasFound; i++) {
    String[] validAliases=baseKM.getClientAliases(keyType[i], issuers);
    if (validAliases!=null) {
      for (int j=0; j<validAliases.length && !aliasFound; j++) {
        if (validAliases[j].equals(alias)) aliasFound=true;
      }
    }
  }
  if (aliasFound) return alias;
  else return null;
}

AliasForcingKeyManager 需要 X509KeyManager 的其它五种方法的实现;这些只是调用它们在 baseKM 上的对应部分。

目前,它仍然使用 AliasForcingKeyManager ,而不是通常的 KeyManager 。这发生在 getSSLSocketFactory 中,它首先从其它示例中调用 getKeyManagersgetTrustManagers ,但是接着将每个从 getKeyManagers 返回的 KeyManager 封装进一个 AliasForcingKeyManager 实例,如清单 9 所示:

清单 9. 封装 X509KeyManagers
protected SSLSocketFactory getSSLSocketFactory() 
  throws IOException, GeneralSecurityException
{
  // Call the superclasses to get suitable trust and key managers
  KeyManager[] kms=getKeyManagers();
  TrustManager[] tms=getTrustManagers();
  // If the alias has been specified, wrap recognized KeyManagers
  // in AliasForcingKeyManager instances.
  if (alias!=null) {
    for (int i=0; i<kms.length; i++) {
      // We can only deal with instances of X509KeyManager
      if (kms[i] instanceof X509KeyManager)
        kms[i]=new AliasForcingKeyManager((X509KeyManager)kms[i], alias);
    }
  }
  // Now construct a SSLContext using these (possibly wrapped)
  // KeyManagers, and the TrustManagers. We still use a null
  // SecureRandom, indicating that the defaults should be used.
  SSLContext context=SSLContext.getInstance("SSL");
  context.init(kms, tms, null);
  // Finally, we get a SocketFactory, and pass it to SimpleSSLClient.
  SSLSocketFactory ssf=context.getSocketFactory();
  return ssf;
}
KeyManager 重新打包

J2SE 1.2 和 1.3 中的 KeyManagerX509KeyManager 在 J2SE 1.4 中都从供应商特定的包中移到了 javax.net.ssl 中;当接口移动时, X509KeyManager 方法说明会略微发生一点变化。

可以使用本文探讨的技术覆盖 KeyManager 的任何方面。类似地,可以使用它们代替 TrustManager ,更改 JSSE 的机制以决定是否信任从远程对等方流出的证书。

 

本文已经讨论了相当多的技巧和技术,因此让我们以快速回顾来结束本文。现在您应当基本了解如何:

  • 使用 HandshakeCompletedListener 收集有关连接的信息
  • SSLContext 获取 SSLSocketFactory
  • 使用定制、动态的 TrustStore 或 KeyStore
  • 放宽 KeyStore 密码与单个证书密码必须匹配的 JSSE 限制
  • 使用您自己的 KeyManager 强制选择标识证书

在适当的地方,我还建议扩展这些技术以用于各种应用程序案例。在您自己的实现中封装 X509KeyManager 的技巧可用于 JSSE 中的许多其它类,当然,利用 TrustStoreKeyStore 可以做更有趣的事情,而不只是装入硬编码的文件。

不管您如何选择实现本文演示的高级 JSSE 定制,任何一个都不是随便就可以实现的。在调整 SSL 内部机理的时候,请牢记:一个错误就会致使您连接变得不安全,这很重要。

本文源码下载:本文源码

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值