在上一篇中,搭建好了实验环境。完整运行一次 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