在上一篇中,搭建好了實驗環境。完整運行一次 IKE/IPSec 協議,收集相關的輸出及抓包,就可以進行協議分析。分析過程中,我們將使用 IKE 進程的屏幕輸出和 Wireshark 抓包,結合相關 RFC,利用 python 進行驗證計算。先看協議的一次完整運行(過濾掉無關報文,如下圖)

下面是 RFC 5996 中對 IKEv2 協議的規范說明

由上可知,IKEv2 協議由兩個階段的交互過程(即兩個來回,共四個報文)組成。第一階段稱為 IKE_SA_INIT 交換。第二階段稱為 IKE_AUTH 交換。通觀報文格式,前面都是 IKE 報文頭 HDR,后跟各類不同類型的載荷。HDR 包含協議發起者和響應者的 Security Parameter Indexes(SPIs),協議版本號,報文長度等一些固定的字段。載荷則是:不同的類型,代表不同的含義。典型載荷說明如下(摘抄自 RFC)
Notation Payload ----------------------------------------- AUTH Authentication CERT Certificate CERTREQ Certificate Request CP Configuration D Delete EAP Extensible Authentication HDR IKE header (not a payload) IDi Identification - Initiator IDr Identification - Responder KE Key Exchange Ni, Nr Nonce N Notify SA Security Association SK Encrypted and Authenticated TSi Traffic Selector - Initiator TSr Traffic Selector - Responder V Vendor ID
來看第一階段交互(IKE_SA_INIT),其第一個報文由 Initiator(Windows 7)發出,后面為 Responder(Linux)的響應報文。兩個報文的載荷基本相同,包括 SAi1/SAr1,KEi/KEr 和 Ni/Nr。SAi1/SAr1 記號中的數字1表示第一階段。
SAi1/SAr1 分別表示發起者和響應者可以支持的密碼算法套件,其作用類似於 TLS 報文中的 Cipher Suites,詳見下圖說明。KEi/KEr 和 Ni/Nr 也是密碼協議中的常客,分別表示雙方的 Diffie–Hellman 密鑰交換內容和一次性隨機數。可見在 IKE 協議中,強制使用 Diffie–Hellman 密鑰交換,從而達到 Perfect Forward Secrecy 效果,在這一點上是略勝 TLS 協議的。此外,第二個報文還包含證書請求載荷(CERTREQ)。因為在 ipsec.conf 配置文件中,啟用了證書認證(authby=pubkey)。
再看實際的抓包內容(第一個 IKE_SA_INIT 報文)

我們發現,除了剛才提到的 Security Association/Key Exchange/Nonce 載荷外,還出現了兩個 Notify 載荷。
Wireshark 中展開解析協議樹,原來是 NAT_DETECTION_SOURCE_IP 和 NAT_DETECTION_DESTINATION_IP 載荷。它們表示支持 NAT 穿越,與密碼學核心功能無關,不再討論。
現在重點看上圖中的 SAi1 載荷。SAi1 包含有 6 個 Proposal,具體的 Proposal 內容(展開經過重排后)為
加密算法 完整性算法 偽隨機數生成函數 Diffie-Hellman 組
[1] 3DES_CBC HMAC_SHA1_96 PRF_HMAC_SHA1 MODP_1024
[2] AES_CBC_256 HMAC_SHA1_96 PRF_HMAC_SHA1 MODP_1024
[3] 3DES_CBC HMAC_SHA2_256_128 PRF_HMAC_SHA2_256 MODP_1024
[4] AES_CBC_256 HMAC_SHA2_256_128 PRF_HMAC_SHA2_256 MODP_1024
[5] 3DES_CBC HMAC_SHA2_384_192 PRF_HMAC_SHA2_384 MODP_1024
[6] AES_CBC_256 HMAC_SHA2_384_192 PRF_HMAC_SHA2_384 MODP_1024
每個 Proposal 由密碼學相關的四元組構成,即加密算法、完整性算法、偽隨機數生成函數、Diffie-Hellman 組。這四元組起什么作用?
暫且不表,繼續看第二個 IKE_SA_INIT 報文,strongSwan 回應的報文內容為:HDR, SAr1, KEr, Nr, N, N, CERTREQ, N。其中 KEr, Nr 和 KEi, Ni 一樣,都是一串二進制格式的內容。三個 Notify 我們不關心。只看 SAr1 和 CERTREQ。SAr1 中只包括一個 Proposal,內容為 3DES_CBC/HMAC_SHA1_96/PRF_HMAC_SHA1/MODP_1024,這是 strongSwan 所支持的密碼算法套件。
對比上面的 SAi1 可知,雙方都同意:(后面要用到的)加密算法/完整性算法/偽隨機數生成函數/Diffie-Hellman 組分別為
3DES_CBC/HMAC_SHA1_96/PRF_HMAC_SHA1/MODP_1024。
再看 CERTREQ (證書請求)載荷,其內容如下

Certificate Authority Data,這是個什么東西?需要對方提供證書就罷了,怎么還有一串二進制內容?查看 RFC 5996,其中有這么一段文字
Certification Authority value is a concatenated list of SHA-1 hashes of the public keys of trusted Certification Authorities (CAs). Each is encoded as the SHA-1 hash of the Subject Public Key Info element (see section 4.1.2.7 of [RFC3280]) from each Trust Anchor certificate. The twenty-octet hashes are concatenated and included with no other formatting.
原來其內容是一連串的 SHA-1 摘要值,而摘要的輸入則是 strongSwan 所信任 CA 證書中的公鑰信息。說得再詳細,就是 RFC 3280 中的 subjectPublicKeyInfo 部分,見下面
TBSCertificate ::= SEQUENCE {
version [0] EXPLICIT Version DEFAULT v1,
serialNumber CertificateSerialNumber,
signature AlgorithmIdentifier,
issuer Name,
validity Validity,
subject Name,
subjectPublicKeyInfo SubjectPublicKeyInfo,
issuerUniqueID [1] IMPLICIT UniqueIdentifier OPTIONAL,
-- If present, version MUST be v2 or v3
subjectUniqueID [2] IMPLICIT UniqueIdentifier OPTIONAL,
-- If present, version MUST be v2 or v3
extensions [3] EXPLICIT Extensions OPTIONAL
-- If present, version MUST be v3
}
SubjectPublicKeyInfo ::= SEQUENCE {
algorithm AlgorithmIdentifier,
subjectPublicKey BIT STRING }
下圖標明了做摘要計算的公鑰信息在 CA 文件中的位置:第一個字節是 0x30,后跟 0x81 0x9F ……,直到黃色背景的最后一個字節(注意是 SEQUENCE SubjectPublicKeyInfo 的 DER 表示)

下面是驗證過程
D:\>dd if=ca.der bs=1 count=162 skip=195 of=subjectPublicKeyInfo iflag=binary oflag=binary
D:\>openssl dgst -sha1 subjectPublicKeyInfo
SHA1(subjectPublicKeyInfo)= 4f9e47fe96b05611c4d7f66dca3f265b32dde04d
看來 strongSwan 比較嚴謹:不僅要求對方提供證書,還要求提供的證書是由 strongSwan 認可的 CA 簽發的。
至此,IKEv2 的第一階段完成。在此階段,雙方就后續要用到的一系列密碼學算法達成一致,並准備好自己的 Diffie-Hellman 密鑰交換值和隨機數。稱第一階段協商的結果為 IKE SA。此外 strongSwan 還要求進行基於證書的身份認證。當然這一切都是明文傳輸。
隨后進入到第二階段,此階段進行 IKE_AUTH 交換,也包括來回兩個報文。第一個報文仍是 Windows 7 發起,第二個報文是 strongSwan 的響應。
回顧第一階段,經過 IKE_SA_INIT 交換后,雙方對后續通信具備了基本的保護能力,包括:報文加密、報文完整性保護。這是為什么?因為經過 SAi1/SAr1 比較,已經協商出四元組:加密算法、完整性算法、偽隨機數生成函數,及 Diffie-Hellman 組。其中,加密算法(即 3DES_CBC)用於報文的加密保護。HMAC_SHA1_96 算法則用於報文的完整性保護。算法達成一致后,下一個問題就是,密鑰從何而來?我們看到,四元組中還有 Diffie-Hellman 組,再加上 IKE_SA_INIT 交換中的 KEi 和 KEr 載荷。熟悉密碼協議的朋友能夠猜到,密鑰應該來自 KEi/KEr 的交換結果。在實際協議中,Diffie-Hellman 密鑰的交換結果,更多是充當“密鑰種子”的作用,而不會去直接參與加、解密等密碼學的基本運算。在這一點上,IKEv2 協議也不例外。重要的是,一旦有了“密鑰種子”,所有直接參與基本運算的密鑰就可以源源不斷地產生。那么如何產生?這就要靠上述四元組中的最后一個元素:偽隨機數生成函數。
思路厘清后,現在就看看 RFC 是如何將理論付諸實踐。先看密鑰種子的生成,在 IKEv2 中,其生成公式為
SKEYSEED = prf(Ni | Nr, g^ir)
上述公式中,g^ir 是 Diffie-Hellman 密鑰交換值,prf 就是四元組中的偽隨機數生成函數(即 PRF_HMAC_SHA1,參見 RFC 2104)。而且 SKEYSEED 並沒有直接把 g^ir 拿過來用,還加上了 Ni、Nr 的影響。協議設計者的思考可謂周全。當然要注意,g^ir 是個秘密值,除了真正的通信雙方,第三方是不知道的。(如果第三方發起中間人攻擊,知曉密鑰交換值,則無法通過后面的數字簽名,故中間人攻擊不予考慮)。
下面是計算 SKEYSEED 的 python 腳本(g^ir 來自 strongSwan 的屏幕調試輸出)
# SKEYSEED = prf(Ni | Nr, g_ir) import binascii from Crypto.Hash import HMAC, SHA Ni = binascii.a2b_hex('305243E21BB674F3EBDA3A370C7688C446F5C189391F55F7C26A91F82D03F2689114AE822042ECD8E6CEDCB6128F09FB') Nr = binascii.a2b_hex('D7E075DB89009F3788B30D2FE6DC77F7CB527D8E2CE091B496A0253767C75188') # Ni、Nr 的長度不一樣 g_ir = binascii.a2b_hex([Diffie-Hellman 密鑰交換結果]) # []用實際內容代替 key = Ni + Nr data = g_ir SKEYSEED = binascii.a2b_hex(HMAC.new(key, data, SHA).hexdigest()) print HMAC.new(key, data, SHA).hexdigest()
密鑰種子 SKEYSEED 得到后,利用偽隨機數生成函數,就可以產生實際參與加解密運算的密鑰。這和 TLS 協議中的思路是一致的。但是,單個偽隨機數生成函數,輸出長度有限,而實際要用到的密鑰,數量比較多,長度會不夠用。所以經常利用計數循環遞增的概念,做到:用一個偽隨機數生成函數,源源不斷地產生需要的密鑰。這是怎么得到的?只要看下 RFC 中的密鑰生成公式,就會明白
{SK_d | SK_ai | SK_ar | SK_ei | SK_er | SK_pi | SK_pr }
= prf+ (SKEYSEED, Ni | Nr | SPIi | SPIr )
prf+ (K,S) = T1 | T2 | T3 | T4 | ...
where:
T1 = prf (K, S | 0x01)
T2 = prf (K, T1 | S | 0x02)
T3 = prf (K, T2 | S | 0x03)
T4 = prf (K, T3 | S | 0x04)
...
其中
SK_d: 生成 IPSec 密鑰材料的密鑰種子
SK_ai:后續 IKEv2 報文的認證密鑰 -- 用於協議發起者
SK_ar:后續 IKEv2 報文的認證密鑰 -- 用於協議響應者
SK_ei:后續 IKEv2 報文的加密密鑰 -- 用於協議發起者
SK_er:后續 IKEv2 報文的加密密鑰 -- 用於協議響應者
SK_pi:生成身份認證(AUTH)載荷時用到 -- 用於協議發起者
SK_pr:生成身份認證(AUTH)載荷時用到 -- 用於協議響應者
按上述公式,函數 prf+() 生成一連串的密鑰流,然后 SK_d、SK_ai、SK_ar、SK_ei、SK_er、SK_pi、SK_pr 依次從密鑰流中取各自所需長度的密鑰。
下面是生成這些密鑰的 python 腳本
SPIi = binascii.a2b_hex('75dd3961203ca3b1') SPIr = binascii.a2b_hex('3cc3328025824d2d') K = SKEYSEED S = Ni + Nr + SPIi + SPIr T = '' TotalKey = '' for i in range(1, 10): # 10 次循環足夠生成所需密鑰 count_byte = binascii.a2b_hex('%02d' % i) # 0x01 0x02 0x03 ... data = T + S + count_byte T = HMAC.new(K, data, SHA).hexdigest() T = binascii.a2b_hex(T) TotalKey += T SK_d = TotalKey[0:20] SK_ai = TotalKey[20:20+20] SK_ar = TotalKey[40:40+20] SK_ei = TotalKey[60:60+24] SK_er = TotalKey[84:84+24] SK_pi = TotalKey[108:108+20] SK_pr = TotalKey[128:128+20] print 'SK_d = ' + binascii.hexlify(SK_d) print 'SK_ai = ' + binascii.hexlify(SK_ai) print 'SK_ar = ' + binascii.hexlify(SK_ar) print 'SK_ei = ' + binascii.hexlify(SK_ei) print 'SK_er = ' + binascii.hexlify(SK_er) print 'SK_pi = ' + binascii.hexlify(SK_pi) print 'SK_pr = ' + binascii.hexlify(SK_pr)
到現在為止,各種算法和密鑰都已算出。注意,這是在第一階段 IKE_SA_INIT 交換后就發生的事。然后進入第二階段(IKE_AUTH 交換)。自然,第二階段的報文就可以應用上面協商出的各種密碼學要素進行保護。具體是如何保護的?先看交互的第一個 IKE_AUTH 報文

IKE_AUTH 報文中除去報文頭,其余所有的載荷都已經受到保護,並且專門用一種 Encrypted and Authenticated 類型的載荷來表示。該載荷的內部格式參見下圖

果然,除了 SK_d(后面將討論),前面討論的所有密鑰(SK_ai/SK_ar、SK_ei/SK_er、SK_pi/SK_pr)都在這里派上用場。黃色背景中的內容表示加密之前的明文。當然僅僅加密還是不夠的,還要對報文進行完整性保護(目前更流行的是加密認證一體化處理,比如用在塊加密中的 AEAD)。密文后跟稱為 Integrity Checksum Data(ICD) 的字段,起到完整性保護作用。需要注意,ICD 是用前面協商的 HMAC_SHA1_96 算法計算得到,並且其保護內容是除去最后的 ICD 字段的整個 IKEv2 報文。現在按此邏輯解開加密報文的面紗,相應 python 腳本如下(以第一個 IKE_AUTH 報文為例)
from Crypto.Cipher import DES, DES3 import binascii IV = binascii.a2b_hex([Initialization Vector]) # []用實際內容代替 cipher = DES3.new(SK_ei, DES.MODE_CBC, IV) ciphertext = binascii.a2b_hex([Encrypted Data]) # []用實際內容代替 plaintext = cipher.decrypt(ciphertext
對比下面 strongSwan 的輸出,完全一致
13[ENC] data after decryption with padding => 1872 bytes @ 0xaf0036a0 13[ENC] 0: 25 00 00 47 09 00 00 00 30 3D 31 0B 30 09 06 03 %..G....0=1.0... 13[ENC] 16: 55 04 06 13 02 43 4E 31 0B 30 09 06 03 55 04 08 U....CN1.0...U.. 13[ENC] 32: 0C 02 48 5A 31 0C 30 0A 06 03 55 04 0A 0C 03 56 ..HZ1.0...U....V 13[ENC] 48: 50 4E 31 13 30 11 06 03 55 04 03 0C 0A 56 50 4E PN1.0...U....VPN 13[ENC] 64: 20 43 6C 69 65 6E 74 26 00 02 77 04 30 82 02 6E Client&..w.0..n
得到明文后,就可以直接計算 Integrity Checksum Data,腳本如下
# 驗證 IKE_AUTH 的 Integrity Checksum Data # 計算范圍:從 IKE_AUTH 報文頭一直延續到報文末尾(除去 Integrity Checksum Data 字段) from Crypto.Hash import HMAC, SHA import binascii data = binascii.a2b_hex([IKE_AUTH 報文除去 ICD 字段]) # []用實際內容代替 IntegrityChecksumData = HMAC.new(SK_ai, data, SHA).hexdigest() print IntegrityChecksumData
得到 IKE_AUTH 明文后,我們來看下它到底是什么內容?這就可以求助強大的 Wireshark,它自帶解密 IKE 載荷的功能,前提是你要告訴它密鑰。下圖是開啟解密功能的界面(Wireshark Version 1.8.5)

解密后的報文如下

故實際報文的結構為:HDR, SK{IDi, CERT, CERTREQ, AUTH, N, CP, SAi2, TSi, TSr},我們來看這些載荷代表什么?
IDi -- 表示發起者的身份信息,展開 Wireshark 協議樹,其內容是發起者證書中的主題字段(Subject: C=CN, ST=HZ, O=VPN, CN=VPN Client),並采用 DER_ASN1_DN 類型編碼

CERT -- 不言而喻,承載內容是發起者的證書。
CERTREQ -- 發起者要求響應者提供證書(用於驗證對方的數字簽名),前面已經討論過細節。
AUTH -- 認證載荷,這是重點

回憶一下,目前只對 IKE_AUTH 報文進行了加密和完整性保護,這做到了 CIA 中的 C(Confidentiality) 和 I(Integrity)。還有一個我個人認為更重要的 A(Authentication,即身份認證。另一種解釋是 Availability,這里采用前者解釋)沒有體現。因為身份認證,或者說敵我識別,應該是在不安全環境中要考慮的首要問題。作為經典的安全協議,IKEv2 自然也考慮了這點,它在 AUTH 載荷中包含了身份認證的信息,只有 AUTH 載荷通過了驗證,才能認為是真正的對方在和自己通信。那么 AUTH 載荷究竟包含什么內容?容易猜到,應該是數字簽名。簽名密鑰是證書對應的私鑰,剩下簽名算法和簽名內容需要確定。對於 RSA 密鑰,簽名算法采用 RFC 3447 中的 RSASSA-PKCS1-v1_5 標准。而簽名內容,則比較復雜,仍以 Initiator 發送的 IKE_AUTH 報文為例,其計算過程如下
RealMessage1 = 發起者的 IKE_SA_INIT 消息 # 完整的 IKE_SA_INIT 報文,從 IKE 頭到報文末尾 InitIDPayload = IDType | RESERVED | IdentificationDataOfInitiator # 見上圖,就是發起者 Identification 載荷的實際內容部分 MACedIDForI = prf(SK_pi, InitIDPayload) # 使用 prf 生成發起者身份信息的 HMAC 值 InitiatorSignedOctets = RealMessage1 | NonceRData | MACedIDForI # 簽名內容
由上可見,簽名內容包括:發起者的 IKE_SA_INIT 報文、響應者的 Nonce 和發起者身份信息的校驗值。最后一項還涉及從雙方 Diffie–Hellman 密鑰交換結果 g_ir 衍生出來的 SK_pi 密鑰。所有這些消息組合起來,如果其簽名正確,足以讓對方相信發起者的身份。下面是最終的計算腳本。
from Crypto.Hash import HMAC, SHA from Crypto.Signature import PKCS1_v1_5 from Crypto.PublicKey import RSA # InitIDPayload = IDType + RESERVED + IdentificationDataOfInitiator InitIDPayload = binascii.a2b_hex([IDi 載荷的實際內容]) # []用實際內容代替 MACedIDForI = binascii.a2b_hex(HMAC.new(SK_pi, InitIDPayload, SHA).hexdigest()) RealMessage1 = binascii.a2b_hex([發起者的 IKE_SA_INIT 報文]) # []用實際內容代替 InitiatorSignedOctets = RealMessage1 + Nr + MACedIDForI key = RSA.importKey(open('clientkey.pem').read(), '123456') hash = SHA.new(InitiatorSignedOctets) signer = PKCS1_v1_5.new(key) AuthenticationPayloadOfInitiator = signer.sign(hash) # PKCS1_v1_5 簽名 print binascii.hexlify(AuthenticationPayloadOfInitiator
與認證載荷相比,剩下的幾個載荷就簡單多了。
N -- 通知載荷:MOBIKE_SUPPORTED
CP -- 配置載荷:Windows 7 向對端請求分配 IP/DNS 等地址

SAi2 -- 這是第二階段交互(IKE_AUTH)中的 Security Association,用 SAi2 表示。此時,雙方還沒有商定如何保護后續的業務流量,所以它們在各自的 IKE_AUTH 交換報文中發送 SAi2 和 SAr2,再次進行協商。這次協商的結果稱為 IPSec SA。可以想像,IPSec SA 的內容,也是相關密碼的要素,包括算法、密鑰等關鍵信息。在抓包文件中,SAi2/SAr2 的主要內容如下
加密算法 完整性算法 SAi1/Proposal-1 ENCR_AES_CBC AUTH_HMAC_SHA1_96 SAi1/Proposal-2 ENCR_3DES AUTH_HMAC_SHA1_96 SAr2/Proposal-1 ENCR_3DES AUTH_HMAC_SHA1_96
最終,雙方同意使用 3DES 和 HMAC_SHA1_96 作為加密和完整性算法。由於雙方身份已經得到確認,所以后續報文的保護,只需要這兩者就可以了。如果你比較細心,會問密鑰從哪里來?沒有密鑰,IPSec SA 是不完整的。答案是密鑰來自 SK_d,后面將討論。
TSi 和 TSr -- 流選擇載荷,與本篇主旨無關,不展開。
基於同樣思路,可以分析響應者發出的 IKE_AUTH 報文。在此不再贅述。
最后歸納 IKEv2 的協議過程:通信雙方在第一階段進行 IKE_SA_INIT 交換,協商的結果為 IKE SA。第二階段 IKE_AUTH 交換受 IKE SA 中定義的密碼學要素保護,雙方同時完成身份認證,並協商出 IPSec SA。IPSec SA 保護后續的通信流量。
至此,IKEv2 協議分析完畢。
參考
RFC 2104 -- HMAC: Keyed-Hashing for Message Authentication
RFC 3280 -- Internet X.509 Public Key Infrastructure Certificate and Certificate Revocation List (CRL) Profile
RFC 3447 -- Public-Key Cryptography Standards (PKCS) #1: RSA Cryptography Specifications Version 2.1
RFC 5996 -- Internet Key Exchange Protocol Version 2 (IKEv2)
RFC 4718 -- IKEv2 Clarifications and Implementation Guidelines
