mozilla在安全方面主要分為三大塊:1.SSL協議的實現;2.Crypto庫的實現;3.PKCS#11的實現最頂層是ssl的實現,和openssl不同,mozilla是自上而下設計的,因此mozilla並沒有抽象出諸如BIO或者EVP之類的通用機制,可以它實現了一個PRFileDesc結構體,和BIO是很類似的;涉及到安全的實現就是2和3了,其中3定義了介面,2給與了軟實現,當然你可以載入plugin使用硬體實現。三者合一形成了mozilla的安全架構。
有幾個重要的結構體先瞭解一下:sslSocketStr結構和openssl中的ssl_st結構體非常類似:
struct sslSocketStr {
PRFileDesc * fd; //一個檔案IO分發表,類似linux核心的file_operations,僅僅作為介面定義。
const sslSocketOps * ops //實現PRFileDesc的介面定義;
...
sslSecurityInfo sec; //操作ssl通道的方式,比如加密,解密之類
...
const char *url; //目的地址,瀏覽器接受的url
...
sslHandshakeFunc handshake; //函數可以作為鎖來使用
sslHandshakeFunc nextHandshake;
sslHandshakeFunc securityHandshake;
//回呼函數,timeout,lock,暫存緩衝,線程,ssl握手方法,狀態
};
上面的一個sslSocketStr代表了一個串連,並且描述了展示層即ssl協議,和openssl類似,mozilla也永術語“PUSH”來將一個安全性描述元壓入一個普通的傳輸層通訊端(類似OpenSSL的BIO機制,後面會說),比如你訪問http://www.google.com.hk,那麼敲下斷行符號的時候就會建立一個sslSocketStr,其中url就是你要訪問的google的網址,某些網站需要安全連線,如果需要安全的話隨之就會初始化sslSocketStr中的fd中的PRIOMethods為ssl_methods,它完成了通訊端IO的幾乎所有的語義,在openssl中,該結構體由SSL_METHOD實現,原理和此處的mozilla幾乎一致:
static const PRIOMethods ssl_methods = {
PR_DESC_LAYERED,
...
ssl_Read, //read
ssl_Write, //write
ssl_Recv, //recv
ssl_Send, //send
...
};
static const sslSocketOps ssl_secure_ops = {
...
ssl_SecureRecv,
ssl_SecureSend,
...
};
為何要有PRIOMethods和sslSocketOps兩個結構體呢?看起來一個就夠了,答案是為了實現分層模型,PRIOMethods並沒有具體的實現策略,只是表示一個“協議層”的語義,對於ssl的具體實現當然沒有必要在ssl層就全做掉,因為即使選擇ssl也可以有不同的策略,也就需要再來一個結構體了,特別是,這樣可以不加區分地統一使用一致的SSL介面,在不實現ssl的情況下,可以再實現一個ssl_default_ops(security/nss/lib/ssl/sslsock.c)。
ssl_Do1stHandshake完成了ssl握手時的狀態機器轉換,mozilla並不區分client方法和server方法,而是完全按照RFC2246的規定來實現ssl,對於client端,在ssl_SecureConnect中會發起對server的串連,也就是發送一個ClientHello訊息,此時會將sslSocketStr的securityHandshake設定為ssl2_BeginClientHandshake(僅對ssl2來說),接下來任何的讀寫ssl,都會調用PRIOMethods中的recv/send函數,然後就回進入到ssl_Do1stHandshake狀態機器,在其中client會調用到這個剛剛設定的ssl2_BeginClientHandshake函數,在該函數最後,按照RFC2246的規定,會進行如下設定:
ss->handshake = ssl_GatherRecord1stHandshake;
ss->nextHandshake = ssl2_HandleServerHelloMessage;
握手回呼函數handshake自己維護狀態機器,這一點和openssl不同,在handshake的調用最後,函數根據調用結果和目前狀態將nexthandshake按照RFC2246設定成下一個狀態,然後在ssl_Do1stHandshake將狀態機器向前推進:
int ssl_Do1stHandshake(sslSocket *ss)
{
do {
if (ss->handshake == 0) {
ss->handshake = ss->nextHandshake;
ss->nextHandshake = 0;
}
if (ss->handshake == 0) {
ss->handshake = ss->securityHandshake;
ss->securityHandshake = 0;
}
...
//握手完畢時要退出迴圈
rv = (*ss->handshake)(ss);
++loopCount;
} while (rv != SECFailure);
...
}
ssl握手的後半部分會根據協商內容設定cipher,設定cipher也就部分初始化了sslSocketStr的sec欄位,在ssl2_CreateSessionCypher(由狀態處理函數ssl2_XYZSetupSessionCypher調用)確定ss->sec.enc函數指標PK11_CipherOp,從此,所有的資料轉送都要經過加密,加密的方法就是PK11_CipherOp,mozilla通過封裝p11介面來統一處理安全通道的配置以及使用過程。
PRFileDesc作為一個解耦層(很重要,不僅僅解除了各個模組耦合,而且還實現了一個分層的模型),將操作路由到sslSocketOps,而sslSocketOps中同樣可以根據ssl的不同狀態或者不同配置將操作繼續路由,比如在ssl_SecureSend中會調用:
rv = (*ss->sec.send)(ss, buf, len, flags);
可見sec欄位的send也是一個可變更的回呼函數,總體的過程就是:
首先ss->fd->methods->send[ssl_Send]:
rv = (*ss->ops->send)(ss, (const unsigned char*)buf, len, flags);[ssl_SecureSend]:
rv = (*ss->sec.send)(ss, buf, len, flags);[ssl2_SendBlock]:
ssl_DefSend.
最終ssl_DefSend將資料發送了出去,通過socket將資料發送了出去。
上述的過程不是很難理解,而且實現的很合理,mozilla更合理的一個地方就是通過PKCS#11介面來統一封裝所有的安全操作,為何通過p11封裝呢?我覺得p11主要用於用戶端,解決了用戶端安全加密的一系列問題,而瀏覽器也在用戶端,因此使用p11會十分方便,雖然可能機器上沒有usbkey,但是軟實現的P11一樣好用,畢竟p11隻是一個規範,p11的軟實現會調用mozilla安全架構的另一部分,那就是crypto介面(類似openssl的EVP)。