網格安全基礎設施(GSI)是 Java" 通用安全服務(Generic Security Service,GSS-API)的實現。GSS 用來在互相通信的應用程序之間安全地交換消息,它在各種底層安全機制(例如 Kerberos)之上提供了對安全服務的一致訪問。在本文中,您將學習如何使用 GSI/GSS-API 擴展和代理證書構建自己的客戶機-服務器應用程序。這是網格中間件所使用的基本身份驗證機制。
通過模仿進行身份驗證
代理證書 是一種模仿證書。模仿(Impersonation)是一種安全技術,允許實體 A 對另外一個實體(實體 B)進行授權,讓 B 可以對其他實體進行驗證,就仿佛 B 是實體 A 一樣。換而言之,B 模仿了 A。
因此為什麼我們會希望使用模仿技術呢?這是因為它有助於解決分布式網絡中關於身份驗證的兩個重要問題:
單點登錄 ?? 為什麼單點登錄這麼重要呢?假設一個用戶需要在多個資源上運行某個進程,使用單點登錄,用戶只需要一次身份驗證,而不用對每個資源都進行身份驗證。
委托 ?? 之所以需要使用委托是因為進程需要代表用戶進行身份驗證。因此,必須向其委托所需的權力。
為便於解釋,假設一個用戶在兩個主機之間啟動一個遠程執行服務。這個服務需要代表用戶在資源上使用單點登錄進行身份驗證,然後必須將自己的權力委托給這兩個主機,這樣它們就可以相互進行身份驗證了。代理證書讓您可以實現這種任務。
相互進行身份驗證
當雙方都具有數字證書並且都信任對這些證書進行簽名的證書權威(CA)時,這兩個實體可以執行相互身份驗證從而彼此證明它們就是自己聲稱的那個實體。信任簽名 CA 實際上意味著它們必須有一些 CA 證書的拷貝(包含公鑰),並且信任這些證書確實來自這些 CA。兩個實體(A 和 B)之間的相互身份驗證過程如下所示:
A 建立一個到 B 的連接。要啟動身份驗證過程, A 將自己的證書發送給 B。這個證書聲明了 A 的身份、公鑰和用來證明該證書的 CA。
B 通過檢查 CA 的數字簽名來確保該證書是有效的,從而確保該 CA 對這個證書進行了簽名,並且這個證書並沒有被篡改過,從而確保這個證書是有效的。(B 必須信任對 A 的證書進行簽名的 CA)。
B 通過生成一條隨機消息並將其發送給 A,請求 A 對其進行加密,從而確保 A 確實是證書所標識的人。 A 使用自己的私鑰對這條消息進行加密,並將結果發回給 B。B 使用 A 的公鑰對消息進行解密。如果解密後的消息與原來的隨機消息相同,那麼 B 就可確定 A 就是它聲稱的那個身份(B 信任 A 的身份)。
第 3 個步驟中相同的操作必須按照相反的順序再執行一次。
現在,A 和 B 就已經相互進行了身份驗證。
代理證書
代理證書包括一個新證書,其中包括一個新公鑰以及一個新私鑰。這個新證書包含一個修改過的所有者的身份,它是由所有者進行簽名的,而不是由 CA 進行簽名的。代理證書具有以下特性:
有限的生命期 ?? 它具有一個特定的截止時間,之後這個代理就不再有效了。
未加密的私鑰 ?? 由於代理很長時間內都不是有效的,因此其私鑰就不需要像所有者的私鑰一樣安全地進行保存。因此,可以使用一個未加密的私鑰將其存儲在臨時空間中,只要存放私匙的文件權限阻止任何人查看該私匙。
一旦創建之後,用戶就可以使用代理證書和私鑰來進行身份驗證,而不需要輸入密碼。
代理證書是對 Globus 項目所創建的 Transport Later Security(TLS)協議的擴展。Globus 結合 Global Grid Forum 一起工作,使得代理成為 TLS 的一個標准擴展,這樣 GSI 代理就可以與其他 TLS 軟件一起使用了。
構建自己的啟用 GSI 的客戶機服務器
我們在這裡要構建的應用程序名字是 ClIEnt 和 Server。它們是使用 Java 編程語言編寫的,需要 Commodity Grid (CoG) Kit 所提供的 GSI 實現和支持庫。構建自己的啟用 GSI 的應用程序非常簡單。ClIEnt 和 Server 的骨架可以分解為如下內容:
讀取命令行參數
在客戶機和服務器之間建立 socket 連接來傳輸數據
加載代理證書
建立安全上下文
如果需要,安全地交換消息
清除工作
讀取命令行參數
ClIEnt 和 Server 的 main 方法需要做的第一件事也是最簡單的事是讀取命令行參數。
ClIEnt 需要使用兩個參數:主機名和要連接的端口。
清單 1. ClIEnt 的主機名和端口
// load arguments
if (args.length < 2)
{
System.err.println("Usage: Java {options} ClIEnt "
+ " {hostName} {port}");
System.exit(-1);
}
String hostName = args[0];
int port = Integer.parseInt(args);
Server 需要一個參數:監聽連接使用的端口號。
清單 2. Server 端口號
// read the command-line arguments
if (args.length != 1) {
System.err.println("Usage: Java {options} Server {localPort}");
System.exit(-1);
}
int localPort = Integer.parseInt(args[0]);
建立 socket 連接
Java GSS-API 為創建和解釋標記(不透明的字節數據)提供了方法。這些標記包含雙方之間安全交換的消息,不過實際進行標記傳輸的方法取決於交換雙方。對於我們的目的來說,在客戶機和服務器之間建立了一個 socket 連接,並使用從 socket 流和安全上下文中構造的流來交換數據。
ClIEnt 需要建立一個到 Server 的 socket 連接,並從中提取輸入/輸出使用的流,如下所示:
清單 3. ClIEnt 建立一個到 Server 的 socket 連接
Socket socket = new Socket(hostName, port);
DataInputStream inStream =
new DataInputStream(socket.getInputStream());
DataOutputStream outStream =
new DataOutputStream(socket.getOutputStream());
System.out.println("ClIEnt: Connected to server "
+ socket.getInetAddress());
服務器應用程序創建了一個 ServerSocket 來監聽這個端口,使用下面的方式給出參數:
ServerSocket ss = new ServerSocket(localPort);
ServerSocket 然後可以等待並接受一個來自客戶機的連接,然後對 I/O 流進行初始化,以便以後與客戶機進行數據交換。
清單 4. ServerSocket 等待並接受來客戶機的連接
Socket socket = ss.accept();
DataInputStream inStream =
new DataInputStream(socket.getInputStream());
DataOutputStream outStream =
new DataOutputStream(socket.getOutputStream());
System.out.println("Got connection from clIEnt "
+ socket.getInetAddress());
Socket 對象用來與客戶機進行通信,它可以通過 ServerSocket 繼續監聽其他客戶機的連接請求。這通常是使用一個循環實現的,例如:
清單 5. Socket 對象與客戶機進行通信
while (true) {
Socket socket = ss.accept();
// Get input and output streams for the connection
// Create a context with the clIEnt
// Exchange messages with the clIEnt
// Clean up
}
這個循環一次只能處理一個客戶機。然而,通過使用線程,可以對服務器進行修改,從而同時處理多個客戶機。
加載代理證書
CoG Kit 是客戶機的 API,它允許客戶機應用程序開發人員和管理員從更高級的框架來使用、管理網格並對網格編程。CoG 提供了 API 來加載並創建代理證書和很多其他東西。例如,調用 CoGPropertIEs.getProxyFile() 方法返回先前由 grid-proxy-init 創建的代理證書的路徑。接下來,這個證書會被加載到一個字節緩沖區中,從而為 GSI 身份驗證轉換成一個 GSSCredential。
CoGProperties cog = CoGPropertIEs.getDefault();
byte proxyBytes[] = readBinFile( cog.getProxyFile() );
下一個步驟是獲得 ExtendedGSSManager 對象的實例。這個類是為其他重要 GSS-API 類提供工廠服務,它提供了有關所支持機制的信息。它可以創建實現下面這 3 個 GSS-API 接口的類實例:GSSName、GSSCredential 和 GSSContext。它還具有幾個方法來查詢可用機制列表和每種機制所支持的名稱類型。默認 ExtendedGSSManager 子類的一個實例可以通過靜態方法 getInstance 來獲得。
ExtendedGSSManager manager =
(ExtendedGSSManager)ExtendedGSSManager.getInstance();
下一個步驟是調用一個工廠方法獲得一組機制的憑證。
清單 6. 調用工廠方法來獲取憑證
GSSCredential credential = manager.createCredential(
proxyBytes, // proxy data
ExtendedGSSCredential.IMPEXP_OPAQUE,
GSSCredential.DEFAULT_LIFETIME, // default life time
null, // OID Mechanism
GSSCredential.INITIATE_AND_ACCEPT);
System.out.println("ClIEnt Credential: "
+ credential.getName()
+ " Remaining life time:"
+ credential.getRemainingLifetime());
參數有:
一個使用諸如 grid-proxy-init 之類的命令創建的導出緩沖區或代理證書的字節數組。
ExtendedGSSCredential.IMPEXP_OPAQUE 意味著導出緩沖區是一個不透明的緩沖區,適合存儲到內存或磁盤中,或者傳遞給其他進程。
一個生存期值 ?? 在本例中,使用的是默認值。
導出憑證希望使用的機制 ?? 可以為空,表示系統默認值。
憑證使用標記 ?? 在本例中,INITIATE_AND_ACCEPT 請求用於上下文的初始化和接受。
建立安全上下文
在兩個應用程序可以使用 ava GSS-API 安全交換消息之前,它們必須使用自己的憑證建立一個聯合的安全上下文。
在 ClIEnt 上,ExtendedGSSManager.createContext 是在發起端創建上下文的工廠方法。第一個參數是目標端的名字。Null 表示底層身份驗證機制所提供的默認值。第二個參數是這種機制的 Object ID(OID)(同樣,null 表示使用默認值)。第三個參數是上一個步驟中的 GSS 憑證,最後一個參數是這個上下文的默認生存期。
清單 7. 建立聯合的安全上下文
GSSContext context = null;
GSIGssOutputStream gssout = null;
GSIGssInputStream gssin = null;
context = manager.createContext(null,
null,
credential,
GSSContext.DEFAULT_LIFETIME);
在對上下文進行實例化之後,在與上下文接收者實際建立上下文之前,上下文的發起者可以選擇設置不同的選項,確定所需要的安全上下文特性:
相互身份驗證 ?? 上下文的發起者始終要對接收者進行身份驗證。如果發起者請求相互身份驗證,那麼接收者也可以對發起者進行身份驗證。
機密性 ?? 請求機密性意味著您請求為上下文方法指定的封裝進行加密。
完整性 ?? 將要求 wrap 和 getMIC 方法的完整性。在請求完整性時,在調用這些方法時會生成一個密碼標記,稱為 Message Integrity Code(MIC)。
context.requestCredDeleg(false);
context.requestMutualAuth(true);
在 Server 上,Server 端所需的唯一參數是憑證。必須獲得 GSI I/O 流才能來回向客戶機發送數據。
GSSContext context = manager.createContext(credential);
GSIGssOutputStream gssOut = new GSIGssOutputStream(outStream, context);
GSIGssInputStream gssIn = new GSIGssInputStream(inStream, context);
在 ClIEnt 實例化一個 GSSContext 並指定所需要的上下文選項之後,它就可以真正與 Server 建立安全上下文。每次交互都會使用一個循環來實現:
調用上下文的 initSecContext 方法 ?? 如果是第一個調用,這個方法就會傳入一個空標記符號。否則,它就會傳入一個最近一次由 Server 發送給 ClIEnt 的標記(這個標記是由 Server 調用 acceptSecContext 而生成的)。
將 initSecContext 所返回的標記(如果存在)發送給 Server ?? 第一次調用 initSecContext 通常會生成一個標記。最後一次調用可能不會返回標記。
檢查上下文是否已經建立 ?? 如果沒有,ClIEnt 就會從 Server 接收另外一個標記,然後開始下一個循環迭代。
initSecContext 所返回的任何標記或從 Server 接收到的任何標記都被放入一個字節數組中,客戶機和服務器應該將其作為不透明的數據對待,在客戶機和服務器之間進行傳遞,並由 Java GSS-API 方法進行解釋。交換消息需要使用一組 GSI I/O 流。
清單 8. GSI I/O 流
gssout = new GSIGssOutputStream(outStream, context);
gssin = new GSIGssInputStream(inStream, context);
byte [] inToken = new byte[0];
byte [] outToken = null;
/*
* Establish a security context
* 1. ClIEnt: sends a secure handshake token
* 2. Server: receives handshake token
* 3. Server: Sends hanshake token back to the clIEnt
* 4. ClIEnt: receives handshake. Security context is established.
*/
while( !context.isEstablished() ) {
outToken = context.initSecContext(inToken, 0, inToken.length);
if (outToken != null) {
gssout.writeToken(outToken);
}
if (!context.isEstablished()) {
inToken = gssin.readHandshakeToken();
}
}
System.out.println("ClIEnt: Security context Established! ");
另外一方面,服務器上下文循環會執行稍有不同的交互:
從 Client 接收一個標記 ?? 這個標記是 ClIEnt initSecContext 調用的結果。
調用上下文的 acceptSecContext 方法,將剛才接收到的標記傳遞給它。
如果 acceptSecContext 返回一個標記,Server 將這個標記發送給 ClIEnt,如果上下文尚未建立,就開始下一個循環。
清單 9. Server 將標記發送給 ClIEnt 並開始下一次循環迭加
while (!context.isEstablished())
{
token = gssIn.readHandshakeToken();
token = context.acceptSecContext(token, 0, token.length);
// Send a token to the peer if one was generated by
// acceptSecContext
if (token != null) {
System.out.println("Server: Will send token of size "
+ token.length
+ " from acceptSecContext.");
gssOut.writeToken(token);
}
}
System.out.println("Server: Context Established! ");
安全交換消息
一旦在 ClIEnt 和 Server 之間建立一個安全上下文之後,它們就可以使用這個上下文來安全地交換消息了。有兩種方法可以為安全交換准備消息:wrap 和 getMIC。實際上有兩個 wrap 方法(和兩個 getMIC 方法),二者之間的區別是輸入消息的位置(一個字節數組或一個輸入流)和輸出消息的輸出位置(一個字節數組返回值或一個輸出流)的表示:
wrap 是消息交換使用的主要方法。簽名是 byte[] wrap (byte[] inBuf, int offset, int len, MessageProp msgProp),其中 (inBuf) 是要發送的消息, (offset) 是消息開始的位置,(len) 是消息的長度,(MessageProp) 用來說明所需要的 Quality-of-Protection(QOP),並指定是否需要進行加密。SOP 值選擇密碼完整性和要使用的加密算法(如果請求使用)。對應於各種 QOP 值的算法是由底層機制的提供者指定的。例如,Kerberos V5 使用的值是在 RFC 1964 中定義的。通常指定 0 作為 QOP 的值來請求默認的 QOP。
getMIC 用來獲取一個包含所提供的消息使用的密碼 MIC 的標記。它通常用來與您這一端確認雙方都有相同的數據,方法僅僅是為數據傳輸一個 MIC,而不需要為雙方傳輸數據本身。簽名是 byte[] getMIC (byte[] inMsg, int offset, int len, MessageProp msgProp),其中 (inMsg) 是要發送的消息,(offset) 是消息開始的位置,而 (len) 是消息的長度。還傳遞了一個 (MessageProp),它用來說明所需要的 QOP。通常會指定 0 作為 QOP 的值來請求默認 QOP。
清除工作
清除操作是由 ClIEnt 和 Server 來執行的,通過關閉 socket,並釋放系統資源和存儲在上下文對象中的密碼信息,然後使這個上下文無效。
清單 10. 關閉 socket 並釋放系統資源和密碼信息
/*
* If mutual authentication did not take place, then only the
* clIEnt was authenticated to the server. Otherwise, both
* clIEnt and server were authenticated to each other.
*/
if (context.getMutualAuthState())
System.out.println("Mutual authentication took place!");
context.dispose();
socket.close();
測試新 GSI 應用程序
在測試這個新的 GSI 客戶機服務器之前,第一個步驟是使用啟用了 GSI 的工具(例如 CoG Kit 或 Globus Toolkit)創建一個代理證書。
圖 1. 使用 CoG 工具包輸出 grid-proxy-init 的調用結果
本文中展示的應用程序可以作為一個 Eclipse Java 項目進行發布。將這個項目導入到您的工作空間中,然後:
啟動 Server
運行 ClIEnt
通過查看 ClIEnt 和 Server 的控制台輸出結果來檢驗相互身份驗證。
輸出結果應該如下所示:
清單 11. 輸出結果
ClIEnt: Connected to server localhost/127.0.0.1
ClIEnt: Loaded proxy file: C:\DOCUME~1\Owner\LOCALS~1\Temp\x509up_u_owner
ClIEnt Credential: /C=US/L=Raleigh/O=ACME/OU=IT/CN=Vladimir Silva Remaining life time:9349
ClIEnt: Security context Established!
ClIEnt is /C=US/L=Raleigh/O=ACME/OU=IT/CN=Vladimir Silva
Server is /C=US/L=Raleigh/O=ACME/OU=IT/CN=Vladimir Silva
ClIEnt: Mutual authentication took place!
Server Credential: /C=US/L=Raleigh/O=ACME/OU=IT/CN=Vladimir Silva
Server: Waiting for incoming connection...
Server: Got connection from clIEnt /127.0.0.1
Server: Will send token of size 1480 from acceptSecContext.
Server: Will send token of size 75 from acceptSecContext.
Server: Context Established!
ClIEnt is /C=US/L=Raleigh/O=ACME/OU=IT/CN=Vladimir Silva
Server is /C=US/L=Raleigh/O=ACME/OU=IT/CN=Vladimir Silva
Server: Mutual authentication took place!
Server: Closing connection with clIEnt /127.0.0.1
Server: Waiting for incoming connection...
如果事情並未預期進行,就請查看下面的故障檢修一節的內容以查看問題的線索。
故障檢修
當您有一個無效或過期的代理時,就會出現一個最簡單的問題。這可能是由於 CoG/GT4 工具包安裝、證書遭到破壞、I/O 錯誤等問題而引起的。常見的錯誤消息如下所示。
清單 11. 錯誤消息
Got connection from clIEnt /127.0.0.1
GSSException: Expired credentials detected
at org.globus.gsi.gssapi.GlobusGSSManagerImpl
.createCredential(GlobusGSSManagerImpl.Java:118)
at org.globus.gsi.gssapi.GlobusGSSManagerImpl
.createCredential(GlobusGSSManagerImpl.Java:64)
...
無效的安全上下文通常會由於上下文循環中的編碼錯誤而產生 ?? 例如,交換使用 GSS 和 GSI 中的方法。記住 GSI 是對 GSS API 的一個擴展 ?? 因此 GSS 客戶機會無法與 GSI 服務器建立一個上下文。
清單 12. 錯誤消息
Waiting for incoming connection...
Got connection from clIEnt /127.0.0.1
unwrap failed. Caused by GSSException:
Security context init/accept not yet called or context deleted
at org.globus.gsi.gssapi.GlobusGSSContextImpl
.checkContext(GlobusGSSContextImpl.Java:1283)
at org.globus.gsi.gssapi.GlobusGSSContextImpl
.unwrap(GlobusGSSContextImpl.Java:831)
網格安全基礎設施(GSI)是 Java Generic Security Services(GSS-API)的一個擴展。GSI 通過對安全服務提供一致的訪問從而在兩個應用程序之間安全地交換消息。本文的目標是通過構建一個簡單的啟用 GSI 的客戶機/服務器應用程序來展示網格應用程序的基本身份驗證機制。