P2P 遇上 SSL

保证对等机之间的通信安全

Todd E. Sundsted(todd-p2p@etcee.com)
首席设计师,PointFire
2001 年 10 月

对任何重要的 P2P 应用程序而言,对等机之间的安全通信都是一个核心要求。尽管安全的细节依赖于如何使用该应用程序和该应用程序将要保护什么,但通过使用现有技术,例如 SSL 实现强壮的、一般用途的安全通常是可能的。本月,Todd Sundsted 演示如何在 P2P 安全中使用 SSL(通过 JSSE)。

上月我们考察了 p2p 应用程序中的信任角色。信任的等级是衡量我们确信程度的尺度,即我们正与之通信的人是否是我们以为的那个人,以及我们正访问的资源是否是我们以为的那些。我们也研究了用于在所有分布式应用程序,包括 p2p 应用程序中建立信任的三个构件:认证、授权和加密。

现在我们将通过修改我们的简单 p2p 应用程序把上个月的课程应用到实践中。特别地,我们将用 X.509 证书扩展该应用以支持 P2P 认证和加密。我们将在将来的文章中处理授权问题。

安全认证

一个应用程序的安全需求在很大程度上依赖于将如何使用该应用程序和该应用程序将要保护什么。不过,用现有技术实现强大的、一般用途的安全通常是可能的。认证就是一个很好的示例。

当顾客想从 Web 站点购买某个产品时,顾客和 Web 站点都要进行认证。顾客通常是以提供名字和密码的方式来认证他自己。另一方面,Web 站点通过交换一块签名数据和一个有效的 X.509 证书(作为 SSL 握手的一部分)来认证它自己。顾客的浏览器验证该证书并用所附的公用密钥验证签名数据。一旦双方都认证了,则交易就可以开始了。

SSL 能用相同的机制处理服务器认证(就如在上面的示例中)和客户机认证。Web 站点典型地对客户机认证不依赖 SSL — 要求用户提供密码是较容易的。而 SSL 客户机和服务器认证对于透明认证是完美的,对等机 — 如 p2p 应用程序中的对等机之间一定会发生透明认证。

安全套接字层(Secure Sockets Layer(SSL))

SSL 是一种安全协议,它为网络(例如因特网)的通信提供私密性。SSL 使应用程序在通信时不用担心被窃听和篡改。

SSL 实际上是共同工作的两个协议:“SSL 记录协议”(SSL Record Protocol)和“SSL 握手协议”(SSL Handshake Protocol)。“SSL 记录协议”是两个协议中较低级别的协议,它为较高级别的协议,例如 SSL 握手协议对数据的变长的记录进行加密和解密。SSL 握手协议处理应用程序凭证的交换和验证。

当一个应用程序(客户机)想和另一个应用程序(服务器)通信时,客户机打开一个与服务器相连接的套接字连接。然后,客户机和服务器对安全连接进行协商。作为协商的一部分,服务器向客户机作自我认证。客户机可以选择向服务器作或不作自我认证。一旦完成了认证并且建立了安全连接,则两个应用程序就可以安全地进行通信。请参阅参考资料以获得更多有关 SSL 的信息。

按照惯例,我将把发起该通信的对等机看作客户机,另一个对等机则看作服务器,不管连接之后它们充当什么角色。

在 Java 应用程序如何使用 SSL

用于 Java 应用程序的 SSL 由“Java 安全套接字扩展”(Java Secure Socket Extension(JSSE))提供。JSSE 是最近发布的 JDK 1.4 Beta 测试版的一个标准部件,但对早些版本的 Java 平台它是作为一个扩展可用的。

JSSE 用 SSL 作它的安全套接字的底层机制。JSSE 安全套接字除了支持透明认证和加密之外,其工作方式与常规套接字相似。因为它们看起来也与普通套接字(它们是类 java.net.Socket 和类 java.net.ServerSocket 的子类)相似,所以使用 JSSE 的多数代码不用修改。受到影响最多的代码是那些处理安全套接字工厂(secure socket factory)的创建和初始化的代码。

如果您想在早于版本 1.4 的 Java 平台中使用 JSSE,那么您将不得不自己去下载并安装 JSSE 扩展(请参阅参考资料)。安装说明非常简单,所以我不想在这里重复。

模型

图 1 说明在对等机之间的通信中 SSL 将充当的角色。

图 1. 工作中的 SSL
图 1. 工作中的 SSL

名为 A 和 B 的两台对等机想安全地进行通信。 在我们简单的 p2p 应用程序的环境中,对等机 A 想查询对等机 B 上的一个资源。

每个对等机都有包含其专用密钥的一个数据库(名为 keystore)和包含其公用密钥的证书。密码保护数据库的内容。该数据库还包含一个或多个来自被信任的对等机的自签名证书。

对等机 A 发起这项事务,每台对等机相互认证,两台对等机协商采用的密码及其长度并建立一个安全通道。完成这些操作之后,每个对等机都知道它正在跟谁交谈并且知道通道是安全的。

初始化

因为 JSSE 和 SSL 的介绍对初始化代码有很大影响,所以让我们来考察对等机 A 中负责初始化的代码。

清单 1. 安全初始化代码

   // Each peer has an identity that must be locally (but not globally)
   // unique.  This identity and its associated public and private keys
   // are stored in a keystore and protected by a password.  Each
   // peer also has a name that must be globally unique.
   String stringIdentity = null;
   String stringPassword = null;
   String stringName = null;

   // The code that prompts the user for his/her identity
   // and password goes here.  the user's name is
   // generated (if necessary) later.

   // Create home directory.  This is a very portable way
   // to create a home directory, but it has its problems --
   // the various flavors of Microsoft Windows put the directory
   // in widely different locations in the directory hierarchy.
   String stringHome = System.getProperty("user.home") + File.separator + "p2p";
   File fileHome = new File(stringHome);
   if (fileHome.exists() == false)
     fileHome.mkdirs();

   // Create keystore.  We must run an external process to create the
   // keystore, because the security APIs don't expose enough
   // functionality to do this inline.  I haven't tested this widely enough
   // to know how portable this code is, but it works on everything I
   // tried it on.
   String stringKeyStore = stringHome + File.separator + "keystore";
   File fileKeyStore = new File(stringKeyStore);
   if (fileKeyStore.exists() == false)
   {
     System.out.println("Creating keystore...");
     byte [] arb = new byte [16];
     SecureRandom securerandom = SecureRandom.getInstance("SHA1PRNG");
     securerandom.nextBytes(arb);
     stringName = new String(Base64.encode(arb));
     String [] arstringCommand = new String []
     {
       System.getProperty("java.home") + File.separator + "bin" + File.separator + "keytool",
       "-genkey",
       "-alias", stringIdentity,
       "-keyalg", "RSA",
       "-keysize", "1024",
       "-dname", "CN=" + stringName,
       "-keystore", stringHome + File.separator + "keystore",
       "-keypass", stringPassword,
       "-storetype", "JCEKS",
       "-storepass", stringPassword
     };
     Process process = Runtime.getRuntime().exec(arstringCommand);
     process.waitFor();
     InputStream inputstream2 = process.getInputStream();
     IOUtils.copy(inputstream2, System.out);
     InputStream inputstream3 = process.getErrorStream();
     IOUtils.copy(inputstream3, System.out);
     if (process.exitValue() != 0)
       System.exit(-1);
   }

   // Once the application has created/located the keystore, it
   // opens it and creates a KeyStore instance from the data
   // in it.
   char [] archPassword = stringPassword.toCharArray();
   FileInputStream fileinputstream = new FileInputStream(stringHome + File.separator +"keystore");
   KeyStore keystore = KeyStore.getInstance("JCEKS");
   try
   {
     keystore.load(fileinputstream, archPassword);
   }
   catch (IOException ioexception)
   {
     System.out.println("Cannot load keystore.  Password may be wrong.");
     System.exit(-3);
   }
   if (keystore.containsAlias(stringIdentity) == false)
   {
     System.out.println("Cannot locate identity.");
     System.exit(-2);
   }

   // Create key manager.  The key manager holds this peer's
   // private key.
   KeyManagerFactory keymanagerfactory = KeyManagerFactory.getInstance("SunX509");
   keymanagerfactory.init(keystore, archPassword);
   KeyManager [] arkeymanager = keymanagerfactory.getKeyManagers();

   // Create trust manager.  The trust manager hold other peers'
   // certificates.
   TrustManagerFactory trustmanagerfactory = TrustManagerFactory.getInstance("SunX509");
   trustmanagerfactory.init(keystore);
   TrustManager [] artrustmanager = trustmanagerfactory.getTrustManagers();

   // Create SSL context.
   SSLContext sslcontext = SSLContext.getInstance("SSL");
   SecureRandom securerandom = SecureRandom.getInstance("SHA1PRNG");
   sslcontext.init(arkeymanager, artrustmanager, securerandom);

   // Create factories.
   m_socketfactory = sslcontext.getSocketFactory();
   m_serversocketfactory = sslcontext.getServerSocketFactory();
   m_keystore = keystore;

当用户第一次启动应用程序时,应用程序提示输入一个身份(别名)和一个密码。身份只用于对对等机进行本地识别 — 它没有全局意义。应用程序生成一个随机的 128 位(16 字节)的字符串,应用程序用这个字符串对对等机进行全局识别并将它转换成字母数字字符串。应用程序用身份、密码和名字创建 keystore 和公开/专用密钥对,密钥对存储在 keystore 中。密码保护 keystore 中的信息。

我确信您已经注意到我创建初始 keystore 时采取的办法。应用程序把 keytool 启动为一个外部进程,keytool 创建 keystore。我不得不这样做,因为公共的 Java 安全 API 不提供创建证书的工具 — 这些功能隐藏在 JSSE 中,它的 API 未发布。启动 keytool 来创建 keystore 的最大缺点是要冒用户提供的密码被公开的风险,用户提供的密码作为参数列表的一部分被传递进去。

应用程序创建了 keystore 之后,就打开 keystore 并将它装入内存(如果我们已经能够直接创建 keystore,则我们就可以免去这一步骤)。应用程序从 keystore 创建一个密钥管理器(key manager)和一个信任管理器(trust manager)。密钥管理器管理密钥,这些密钥用于在安全套接字对面相对应用程序的对等机来认证它。信任管理器管理证书,这些证书用于对位于安全套接字另一端的对等机进行认证。

最后,应用程序创建一个 SSLContext 实例,这个实例充当安全套接字工厂的工厂。应用程序创建 SocketFactory 和 ServerSocketFactory 类的安全版本并将它们用于稍后的通信。

用户界面

余下代码的大部分跟我们的前发行版是相同的。(请参阅“p2p 应用程序框架”获取详细信息。从参考资料下载完整的源代码。)这是 JSSE 带来的好处之一 — 用 JSSE 创建的安全套接字看起来和常规套接字相似,工作起来也和常规套接字相似。支持代码也没什么不同(除了创建和初始化代码必须修改外)。然而,还是有和用户界面有关的改动。大部分这些改动是受到了我想让这个应用程序对那些只想快速地设置这个应用程序来试验的愿望而激发的。

作为对读者反馈的回应,我改善了命令行界面。对等机及其资源存在于一个层次结构中,用户用命令 ls 和 cd 访问这个结构。ls 命令列出层次结构中某个特定点的内容(与 UNIX 的 ls 命令或 DOS 的 dir 命令所做的相似),cd 命令改变层次结构中的当前位置(再一次与 UNIX 和 DOS 中的同名命令相似)。cd .. 用于在层次结构中进行回退。

为使应用程序更容易使用,我已经创建了两个 zip 文件,每一个都包含该应用程序的一个已经适当配置了的实例。一个实例在端口 7776 进行通信,另一个在端口 7777。它们也都已被配置成彼此认证,为的是能够建立安全通道。这些 zip 文件的名称为 peerA.zip 和 peerB.zip(请参阅参考资料)。“自述文件”(README)提供了更多详细的使用说明。

结束语

在 p2p 应用程序中加入 SSL 是提供简单却强大的安全的极好办法。下个月,我们将继续我们的 P2P 之旅,考察更成熟的对等机发现方法。

参考资料

  • 下载 p2p 应用程序框架、对等机 A 和对等机 B 的源代码。

  • 您可以把 JSSE 当作 Java 1.4 测试版的一个部件或前版本的一个独立扩展来使用。

  • Netscape 提供 SSL 介绍。

  • IBM Research Division 一直在做关于 安全主题的很多项目。在 Security Research in Java and Distributed Object Systems 和 Secure Signed Documents 阅读关于 IBM Research 的努力的更多信息。

  • 以前从未用过套接字?采用这本实用的 Java 套接字教程(developerWorks,2001 年 8 月),它将教您如何使用套接字来处理现实世界中碰到的典型问题。

  • Joseph Sinclair 给 developerWorks 写了一个有趣的系列,讨论高风险(high-stakes)系统中的安全问题。 第 1 部分(2001 年 3 月)研究 Java 技术如何可以用于使系统安全,在这个系统中,错误身份的后果特别有* 破坏性。第 2 部分(2001 年 6 月)讨论三种用于识别用户的常见手段,突出了它们的优势和弱点。

  • developerWorks 主持了一整块的主题区域,致力于实际使用中的安全问题。

  • 阅读 Todd 的正在进行的 对等计算系列的全部文章。

  • 在 developerWorks Java 技术专区查找更多 Java 参考资料。

关于作者

Todd Sundsted 自从有桌面型的计算机以来一直在编写程序。虽然原来是对用 C++ 创建分布式应用程序感兴趣,但在 Java 成为构建此类应用程序的明显选择时,Todd 就转移到了 Java 编程语言。除了写作,Todd 还是 PointFire 公司的联合创始人之一和首席设计师。请通过 todd-p2p@etcee.com 与 Todd 联系。

Contributors: FHL