使用Proverif分析TLS 1.3协议
文章内容简介
TLS(传输层安全)协议是一种广泛使用的网络协议,它主要用来建立服务器和客户端之间的安全通道。本文对TLS 1.3的18草案就行详细解析,并用proverif对其进行建模已经进行安全性论证。本文是中国科学技术大学形式化方法的大作业,难免有些仓促,如有问题敬请指正。
密码学基础知识
Diffie-Hellman密钥交换协议
在加密通信过程中,有时要用到共享密钥(shared key),这个密钥为通信双方所共享,但是要保证不可以被攻击者发现。Diffie-Hellman密钥交换协议确保即使攻击者截获通信信道上的所有信息,都无法在可以接受的时间内算出它们的共享密钥。
该协议用到了原根的一个性质:假设 q q q是 p p p的一个原根,则q的幂构成模p的简化剩余系,即q的幂模p可以取到1到p-1的任意一个值。具体证明可以参见https://www.cnblogs.com/philolif/p/number-theory-6.html。由求q的幂模p的值很容易,但是已知这个值,反过来求指数的过程计算周期却很长,基本只能用试错法,计算时间复杂度和p-1同量级。如果p非常大,那么逆向求解是无法有效做到的。
通信双方首先约定一个大素数p和它的原根g,这两个量是无需保护的。接下来A生产随机数a,将 g a ( m o d p ) g^a(mod p) ga(modp)通过公共信道发给B,接下来B生成随机数b,将 g a b ( m o d p ) g^{ab}(mod p) gab(modp)作为密钥,并将 g b g^b gb发给A,随后A将 ( g b ) a = g b a ( m o d p ) (g^b)^a=g^{ba}(mod p) (gb)a=gba(modp)作为密钥。由于 g a b = g b a g^{ab}=g^{ba} gab=gba,A和B便得到公共密钥。
攻击者从信道上可以截获 g a g^a ga和 g b g^b gb,然而从这两者还原 g a b g^{ab} gab是无法做到的,这就确保了协议的安全性。
然而,到此为止我们仅赋予了攻击者监听信息的权力。如果再考虑攻击者综合利用信息、伪造并重发送的情况,之前提到的通信方式仍然是不安全的:攻击者可以使用中间人攻击来获得共享密钥。为应对中间人攻击,可以使用数字签名。
接下来使用Proverif工具来对Diffie-Hellman密钥交换协议进行建模。这里不考虑数字签名,因为在最终的验证程序中数字签名额外生成并与通信消息绑在一起。定义dh_ideal(element,bitstring)为求幂函数,将element类型的参数求对另一个参数的幂,得到另一个element类型的值。这里的element可以理解成完全剩余系中的元素。除此之外,再考虑一种特殊情况: g a g^a ga模p只有极少数的可能值,以至于可以通过试错法破解共享密钥。出现这种情况的原因可能是g不是原根。不妨考虑极端情况: g a g^a ga模p只有一个可能值,记为BadElement。为了区分两种加密方式,我们再定义一种方法dh_exp,它接受一个参数标记使用正常的DH算法(StrongDH)或者使用上面提到的出现BadElement的方法(WeakDH)。如果是StrongDH,在底数是BadElement时返回BadElememt,否则按dh_ideal进行计算;如果是WeakDH,则返回BadElememt。此外为保证 g a b = g b a g^{ab}=g^{ba} gab=gba这一代数性质,需要定义一个方程确保dh_ideal(dh_ideal(G,x),y) = dh_ideal(dh_ideal(G,y),x)。
完整代码如下:
(********************************************************)
(* Diffie-Hellman with small/bad subgroup attacks. See Logjam, Cross-Protocol *)
(********************************************************)
type group.
const StrongDH: group [data].
const WeakDH: group [data].
type element.
fun e2b(element): bitstring [data].
const BadElement: element [data].
const G: element [data].
fun dh_ideal(element,bitstring):element.
equation forall x:bitstring, y:bitstring;
dh_ideal(dh_ideal(G,x),y) =
dh_ideal(dh_ideal(G,y),x).
fun dh_exp(group,element,bitstring):element
reduc forall g:group, e:element, x:bitstring;
dh_exp(WeakDH,e,x) = BadElement
otherwise forall g:group, e:element, x:bitstring;
dh_exp(StrongDH,BadElement,x) = BadElement
otherwise forall g:group, e:element, x:bitstring;
dh_exp(StrongDH,e,x) = dh_ideal(e,x).
letfun dh_keygen(g:group) =
new x:bitstring;
let gx = dh_exp(g,G,x) in
(x,gx).
AEAD加密
相比传统加密算法,AEAD加密算法支持解密前进行验证,以得知密钥正确性。为了便于理解,此处选一种AEAD加密分析进行说明。
E&M (Encryption and MAC):首先对密文进行加密,然后使用同一密钥进行MAC运算,将Concat(Ciphertext,MAC)发给对方。对方收到消息后先解密,然后将解密的信息重新做MAC运算,如果和收到的MAC值相同,则验证成功,说明密钥正确。(下图选自wiki)
有时,可以添加nonce(如序列号)唯一地标定每次加密过程。
在安全认证中,我们需要再定义一种弱AEAD加密算法,它允许对加密的数据进行修改,定义相应方法为aead_forged,接受两个参数,第一个为预期修改的信息p,第二个是加密后的信息。输出的信息经过解密后变成p。除此之外,攻击者可以使用aead_leak方法直接由加密过的数据得到被加密的值。
Proverif建模如下:
(********************************************************)
(* Authenticated Encryption with Additional Data *)
(* extended with with weak/strong algorithms: See Lucky13, Beast, RC4 *)
(********************************************************)
type ae_alg.
const WeakAE, StrongAE: ae_alg.
type ae_key.
fun b2ae(bitstring):ae_key [data].
fun aead_enc(ae_alg, ae_key, bitstring, bitstring, bitstring): bitstring. (*编码*)
fun aead_forged(bitstring,bitstring): bitstring.
fun aead_dec(ae_alg, ae_key, bitstring, bitstring, bitstring): bitstring (*解码*)
reduc forall a:ae_alg, k:ae_key, n:bitstring, p:bitstring, ad:bitstring;
aead_dec(a, k, n, ad, aead_enc(a, k, n, ad, p)) = p
otherwise forall a:ae_alg, k:ae_key, n:bitstring, p:bitstring, ad:bitstring,p':bitstring,ad':bitstring;
aead_dec(WeakAE, k, n, ad, aead_forged(p,aead_enc(WeakAE, k, n, ad', p'))) = p.
fun aead_leak(bitstring):bitstring
reduc forall k:ae_key, n:bitstring, ad:bitstring, x:bitstring;
aead_leak(aead_enc(WeakAE,k,n,ad,x)) = x.
Hash
可以将一个对象映射到某一个范围中的值。如果有两个不同对象映射到同一个值,则称为哈希冲突。一般来讲,可以通过检测两个值的Hash值是否相等,在不知道这两个具体值的情况下判断它们是否相等。和上面类似,我们可以定义一种“脆弱”的哈希算法,它很容易产生冲突。这样两个不相同的数可能被错误地被认证成相同。假设该算法下所有数据的哈希值全部被映射为collision。
哈希的proverif建模:
(********************************************************)
(* Hash Functions, including those with collisions. See SLOTH *)
(********************************************************)
type hash_alg.
const StrongHash: hash_alg [data].
const WeakHash: hash_alg [data].
const collision:bitstring [data].
fun hash_ideal(bitstring):bitstring.
fun hash(hash_alg,bitstring): bitstring
reduc forall x:bitstring;
hash(WeakHash,x) = collision
otherwise forall x:bitstring;
hash(StrongHash,x) = hash_ideal(x).
HMAC
Hash方法可以较好地用于验证,但生成的哈希值很容易伪造。HMAC可以理解为使用密钥进行哈希,具体定义为:
H M A C ( K , m ) = H ( ( K ′ X O R o p a d ) ∣ ∣ H ( ( K ′ X O R i p a d ) ∣ ∣ m ) ) HMAC(K,m)=H((K'\ XOR\ opad)||H((K'\ XOR\ ipad)||m)) HMAC(K,m)=H((K′ XOR opad)∣∣H((K′ XOR ipad)∣∣m))
当K’大于某个长度时,K’=H(K),否则K’=K。双竖线表示连接。opad和ipad解释如下:
- opad is the block-sized outer padding, consisting of repeated bytes valued 0x5c
- ipad is the block-sized inner padding, consisting of repeated bytes valued 0x36
当然在Proverif中,可以隐藏很多实现的细节。定义一个名为mac_key的类型用于标记HMAC中的密钥hmac_ideal输入密钥和待编码数据,输出编码后的数据。hmac在mac_key的基础上添加哈希算法作为第一个参数。如果是WeakHash,输出的永远都是collision,否则按hmac_ideal就行正常编码。
(********************************************************)
(* HMAC *)
(********************************************************)
type mac_key.
fun b2mk(bitstring):mac_key [data,typeConverter].
fun hmac_ideal(mac_key,bitstring): bitstring.
fun hmac(hash_alg,mac_key,bitstring):bitstring
reduc forall k:mac_key, x:bitstring;
hmac(WeakHash,k, x) = collision
otherwise forall x:bitstring, k:mac_key;
hmac(StrongHash,k, x) = hmac_ideal(k,x).
签名验证
签名方拥有一个私钥,将使用私钥对待签名数据进行加密,随后将加密后的签名和原数据发给检验方。检验方收到数据后,使用公钥对签名进行解密,如果解密后的数据和原数据相同,则说明数据为签名方所发。由于签名需要用到私钥,任何未得知私钥的攻击者都无法构造签名。
在Proverif中可以定义两个类型:公钥pubkey和私钥privkey。对于任意一个私钥,可以假设总可以找到对应的公钥,即定义pk(privkey),返回pubkey。但反过来,基于安全性,不存在对应的sk(pubkey),也就是说不能根据公钥就获取私钥。verify对使用私钥k对信息x的签名sign(k,x)进行验证。给定k对应的公钥pk(k)和x,如果签名信息是sign(k,x),则验证成功,应该返回true。整体代码如下:
(********************************************************)
(* Public Key Signatures *)
(********************************************************)
type privkey.
type pubkey.
fun pk(privkey): pubkey.
const NoPubKey:pubkey.
(* RSA Signatures, typically the argument is a hash over some data *)
fun sign(privkey,bitstring):bitstring.
fun verify(pubkey,bitstring,bitstring): bool
reduc forall k:privkey, x:bitstring;
verify(pk(k),x,sign(k,x)) = true.
HKDF密钥派生算法
HKDF是基于HMAC的密钥派生算法,它可以将初始密钥扩展成更强大的密钥。
主要分为两部:提取和扩展。提取是把密钥转化为固定长度的伪随机数,扩展是将提取到的伪随机数扩展到指定长度。
提取的具体过程可以用如下公式表示:
H K D F − E x t r a c t ( s a l t , I K M ) = H M A C ( s a l t , I K M ) HKDF-Extract(salt,IKM)=HMAC(salt,IKM) HKDF−Extract(salt,IKM)=HMAC(salt,IKM)得到PRK,其中salt是盐,IKM是原始密钥。
扩展可以用如下公式表示:
H K D F − E x p a n d ( P R K , i n f o , L ) = O K M HKDF-Expand(PRK,info,L)=OKM HKDF−Expand(PRK,info,L)=OKM,OKM是长度为L的密钥输出,info是可选上下文和应用程序特定信息。
首先计算一系列哈希值,记为T(n),其中n从0到255。
T(0)=长度为0的空字符串
T ( 1 ) = H M A C ( P R K , T ( 0 ) ∣ i n f o ∣ 0 x 01 ) T(1)=HMAC(PRK,T(0)|info|0x01) T(1)=HMAC(PRK,T(0)∣info∣0x01)
T ( 2 ) = H M A C ( P R K , T ( 1 ) ∣ i n f o ∣ 0 x 02 ) T(2)=HMAC(PRK,T(1)|info|0x02) T(2)=HMAC(PRK,T(1)∣info∣0x02)
T ( 3 ) = H M A C ( P R K , T ( 2 ) ∣ i n f o ∣ 0 x 03 ) T(3)=HMAC(PRK,T(2)|info|0x03) T(3)=HMAC(PRK,T(2)∣info∣0x03)
直到计算到T(255)。
我们将这些T(n)连接起来,记为 T = T ( 1 ) ∣ T ( 2 ) ∣ ⋯ ∣ T ( N ) T=T(1)|T(2)|\cdots|T(N) T=T(1)∣T(2)∣⋯∣T(N),OKM即是T的前L个字节。
在TLS 1.3中,还需要用到HKDF-Expand-Label和Derive-Secret,它们的定义如下:
HKDF-Expand-Label(Secret, Label, HashValue, Length) =
HKDF-Expand(Secret, HkdfLabel, Length)
struct {
uint16 length