對任何重要的 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 擴展(請參閱參考資料).安裝說明非常簡單,
我不想在這裡重複.
名為 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 = trus