(* Symmetric key encryption *)
type key.
fun senc(bitstring, key): bitstring.
reduc forall m: bitstring, k: key; sdec(senc(m,k),k) = m.
(* Asymmetric key encryption *)
type skey.
type pkey.
fun pk(skey): pkey.
fun aenc(bitstring, pkey): bitstring.
reduc forall m: bitstring, sk: skey; adec(aenc(m,pk(sk)),sk) = m.
(* Digital signatures *)
type sskey.
type spkey.
fun spk(sskey): spkey.
fun sign(bitstring, sskey): bitstring.
reduc forall m: bitstring, ssk: sskey; getmess(sign(m,ssk)) = m.
reduc forall m: bitstring, ssk: sskey; checksign(sign(m,ssk),spk(ssk)) = m.
free c:channel.
free s:bitstring [private].
query attacker(s).
event acceptsClient(key).
event acceptsServer(key,pkey).
event termClient(key,pkey).
event termServer(key).
query x:key,y:pkey; event(termClient(x,y))==>event(acceptsServer(x,y)).
query x:key; inj-event(termServer(x))==>inj-event(acceptsClient(x)).
let clientA(pkA:pkey,skA:skey,pkB:spkey) =
out(c,pkA);
in(c,x:bitstring);
let y = adec(x,skA) in
let (=pkA,=pkB,k:key) = checksign(y,pkB) in
(*=pkA,对自己的公钥进行确认,证明确实是发给自己的*)
event acceptsClient(k);
out(c,senc(s,k));
event termClient(k,pkA).
let serverB(pkB:spkey,skB:sskey,pkA:pkey) =
in(c,pkX:pkey);
new k:key;
event acceptsServer(k,pkX);
out(c,aenc(sign((pkX,pkB,k),skB),pkX));
(*签名里添加了pkX,便于收到此消息的客户端确认确实是发给自己的*)
in(c,x:bitstring);
let z = sdec(x,k) in
if pkX = pkA then event termServer(k).
process
new skA:skey;
new skB:sskey;
let pkA = pk(skA) in out(c,pkA);
let pkB = spk(skB) in out(c,pkB);
( (!clientA(pkA,skA,pkB)) | (!serverB(pkB,skB,pkA)) )
运行:
--------------------------------------------------------------
Verification summary:
Query not attacker(s[]) is true.
Query event(termClient(x_2,y_1)) ==> event(acceptsServer(x_2,y_1)) is true.
Query inj-event(termServer(x_2)) ==> inj-event(acceptsClient(x_2)) is true.
--------------------------------------------------------------
其中,c
是常量的名称,t
是它的类型。可以通过声明相同类型t
的几个常量。
构造函数可以通过附加[data]
来声明为数据项:
声明为数据的构造函数类似于元组:攻击者可以构造和分解数据构造函数。换句话说,声明数据构造函数f
隐式地声明n
个析构函数映射f(x1,…,xn)
到xi
,其中i∈{1,……,n}
。
可以通过模式匹配逆数据构造函数。T1, . . . , Tn
的类型是f
的参数类型,因此当Ti
是变量时,可以省略它的类型。例如,与声明在一起:
构造函数声明的数据不能声明为私有的。
数据构造函数的一个应用程序是类型转换。由于类型不匹配,类型系统偶尔很难将函数应用于参数。这可以通过类型转换来克服。定义如下:
其中,类型转换器tc
接受t
类型的输入,并返回t'
类型的结果。请注意,由于构造函数是数据构造函数,因此攻击者可以从术语tc(M)
中恢复术语M
。直观地说,关键字typeConverter
意味着该函数是标识函数(identity function),因此除了改变类型之外没有任何效果。默认情况下,类型用于输入协议,但在协议验证期间,ProVerif会忽略类型。因此,typeConverter
的功能将被删除。(这种行为允许ProVerif检测类型缺陷攻击,其中攻击者混合不同类型的数据。
从t'
到t
的反向类型转换应通过模式匹配来执行:
其中,M为t'
类型,而x
为t
类型。允许使用此构造函数,因为类型转换器是数据构造函数。当定义从类型t
到t'
的类型转换器tc(t):t'
时,所有类型t
的元素都可以转换为类型t'
,但t'
类型只能转换为类型t
的元素是形式tc(M)
的元素。因此,例如,从表示128位键的类型键定义类型转换器到类型比特字符串,但不是在另一个方向上,因为所有128bit key都是bitstring,但只有一些bitstring是128bit key。
自然数得到本机支持,并具有内置类型nat
。在内部,ProVerif遵循Peano
公理建模自然数,即它考虑一个nat
类型的常数0
和一个继任者的数据构造函数。因此,所有的自然数都是术语,可以与其他用户定义的函数一起使用。如果一个项是常数0或自然数,则说是一个自然数。图中扩展了术语的语法,以考虑操作自然数的内置内缀函数。
最后,ProVerif有一个内置的布尔函数是nat
检查一个项是否为一个自然数,即,is_nat(M)
返回true
,当且仅当M
等于模等分理论到一个自然数。
两个任意项M
之间的加法是不允许的。序关系>、<、>=、<=
内部是用布尔型的解构器来实现的,如果关系的两边都是自然数那么就按日常理解的关系来做比较返回true
或者false
,只要有一边不是自然数,那么这个比较就会失败。另外,M - i
中如果M < i
那么也会失败,因为ProVerif中不允许出现负数。
由于自然数是用Peano Axiom生成的,所以攻击者可以生成所有的自然数。ProVerif不允许自己定义新的自然数。用户自定义的constructor
也不能把nat作为返回类型,但是可以作为constructor
的参数,所以是可以作为destructor
(析构函数)的参数和返回类型的。
type key.
free c:channel.
free s:bitstring [private].
fun ienc(nat,key):bitstring.
(* constructor:用key对nat加密 *)
fun idec(bitstring,key):nat
(* destructor:用key对加密结果解密,如果本来明文是x,现在得到的是x-1 *)
reduc forall x:nat,y:key;idec(ienc(x+1,y),y)=x.
query attacker(s);
process
new k:key;(
out(c,ienc(2,k))
|in(c,x:nat);in(c,y:bitstring);if x+3>idec(y,k) then out(c,s)
)
x + 3 > 1
是恒成立的,所以攻击者只要随便向通道c
里传入一个数就能让这个判定成立,从而让s
发送出去完成窃取:
free c:channel.
free A:bitstring.
free B:bitstring.
process
in(c,(x:bitstring,y:bitstring));
if x=A||x=B then
let z=(if y=A then new n:bitstring;(x,n) else (x,y)) in
out(c,z)
先从channel c
读取两个bitstring x
、y
,如果x
是A
或者B
就把z
从通道c
发送出去,如果y=A
就让z=(x,n)
,否则就让z=(x,y)
。
in(c,(x:bitsring,y:bitstring));
if((x=A)||(x=B)) then
new n:bitstring;
let z:bitstring=(if(y=A) then (x,n) else (x,y))in
out(c,z)
table d(t1, ..., tn).
在进程里面对table
进行填充和访问,但是没法删除,table
不能被攻击者访问(就像数据库里的数据没法直接被攻击者获取一样)。
d
里插入数据(M1,...,Mn)
,然后执行过程P
。d
里获取匹配模式(T1,...,Tn)
的数据,如果能获取到相匹配的数据就执行P
,否则就执行Q
。(T1,...,Tn)
的数据,获取到的数据还要能满足条件M
,如果成功就执行P
,否则执行Q
。
从table d里获取匹配模式(T1,...,Tn)
的数据,条件满足的时候这个term就取用in后面的term,否则就取用else后面的term。
相
表达的是进程执行的不同“阶段”,通过给进程指令加入标号来区分不同的阶段。
默认的不带标号的指令都是在0
阶段,所以不能显式地指定0
阶段。
阶段的语义是,从0
阶段开始执行,当从第 i
阶段到第 i+1
阶段的时候,会去执行第 i + 1
阶段的指令,并且所有≥i+2
阶段的指令都会被丢弃。
阶段可以用来证明前向安全(forward secrecy)协议,它是指即使长期使用的主密钥泄漏,也不会导致过去的会话密钥泄漏。
(* Symmetric key encryption *)
type key.
fun senc(bitstring, key): bitstring.
reduc forall m: bitstring, k: key; sdec(senc(m,k),k) = m.
(* Asymmetric key encryption *)
type skey.
type pkey.
fun pk(skey): pkey.
fun aenc(bitstring, pkey): bitstring.
reduc forall m: bitstring, sk: skey; adec(aenc(m,pk(sk)),sk) = m.
(* Digital signatures *)
type sskey.
type spkey.
fun spk(sskey): spkey.
fun sign(bitstring, sskey): bitstring.
reduc forall m: bitstring, ssk: sskey; getmess(sign(m,ssk)) = m.
reduc forall m: bitstring, ssk: sskey; checksign(sign(m,ssk),spk(ssk)) = m.
free c:channel.
free s:bitstring [private].
query attacker(s).
let clientA(pkA:pkey,skA:skey,pkB:spkey) =
out(c,pkA);
in(c,x:bitstring);
let y = adec(x,skA) in
let (=pkA,=pkB,k:key) = checksign(y,pkB) in
out(c,senc(s,k)).
let serverB(pkB:spkey,skB:sskey,pkA:pkey) =
in(c,pkX:pkey);
new k:key;
out(c,aenc(sign((pkX,pkB,k),skB),pkX));
in(c,x:bitstring);
let z = sdec(x,k).
process
new skA:skey;
new skB:sskey;
let pkA = pk(skA) in out(c,pkA);
let pkB = spk(skB) in out(c,pkB);
( (!clientA(pkA,skA,pkB)) | (!serverB(pkB,skB,pkA)) |
phase 1; out(c, skB) )
(*phase1时泄露skB*)
*
:在phase1
的时候把skB
泄露,phase0
时的s
依然是安全的。
--------------------------------------------------------------
Verification summary:
Query not attacker_p1(s[]) is true.
--------------------------------------------------------------
如果泄露的是skA
:
phase 1; out(c, skA) )
phase0
时的s
就不再安全。
--------------------------------------------------------------
Verification summary:
Query not attacker_p1(s[]) is false.
--------------------------------------------------------------
关键字sync
用来实现process
的全局同步,它的功能有点像上面学习的phase
。同步有一个等级和一个标记:
sync t [tag]
其中t
是同步的等级(level),tag
是同步的标签。具有相同等级和相同标签的同步被视为相同的同步,所以相同的同步只能被用于if
、let
、let ... suchthat
、get
的不同分支里,因为同一时刻只能有一个分支在执行,所以同一时刻至多只有一个“相同的同步”能到达。
全局同步是按level升序执行的,在执行level为t
的同步的时候,进程会执行到level为t
的同步的所有tag
都到达的程序点。
比如,假设t
就是最小同步等级,然后在这个等级的tag
有tag1
,…,tagn
,那么进程就会执行到到达tag1
,…,tagn
这些程序点(各一个),然后就会一起执行掉这些同步命令,然后再继续往下执行。
和phase
不同的是,同步是不会丢弃进程片段的。
同步的tag确定方式:
sync t [tag]
来指定,如果用户忽略了tag
,只写sync t
,那么ProVerif会分配一个新tag
。tag
的前缀(prefix),形如[sync: tag prefix p]
,然后在宏展开的时候就会把这个前缀补上,比如:let P(x:bitstring)=
sync 1 [t];
out(c,x).
process
P(a) [sync:tag prefix T1] | [sync:tag prefix T2]
yields the process
sync 1 [T1_T];out(c,a)|sync 1 [T2,T];out(c,b)
把T1
和T2
这两个前缀加到T
前面去了,并且补上下划线变成T1_T
和T2_T
两个tag。这个功能是有必要的,当一个process macro被复用的时候,特别是多个做并发的时候,按理说这些同步点的tag就应该是不一样的(不然就成了“相同的同步”),所以即使[sync: tag prefix p]
被省略了,实际上ProVerif也会自动加入一个新的前缀让它们区分开。如果要强制不加前缀,那么就使用[sync: no tag prefix]
。
对于测试分支,不同分支里的同步应该有相同的tag名,不然的话就容易导致同步的阻塞(block),因为两个分支只能走一个但是缺要求同步的时候走到所有的不同的tag标记点。所以应该这样写:
if...then(...sync1 [T];...)else(...sync1[T];...)
or
if...then(...P(...)[sync:tag prefix T])
else(...P(...)[sync:tag prefix T])
分别对应process macro内和外的写法。
将解构器的能力扩展,现在它可以被定义为:
相当于有一系列的重写规则(rewrite rule),对于实际拿到的一个析构操作,到析构器里从上到下看,如果某一条重写规则是可应用的(applied,其实就是命中了这条规则,就是所有的项都能正确的match上),那么就用这条规则。如果所有的重写规则都遍历完一遍没有能应用上的,那么这个destructor就失败(fail)了。
在这个定义下,eq(M, N)
都是string
并且是同一个东西的时候被规约成true
,都是string
不同的两项的时候被规约成false
。如果这两条重写规则都不命中的话,解构操作就会失败,也就是说上面的解构器不能处理这样一个问题:有参数失败的情况。要解除这个限制,ProVerif引入了表示失败的特殊值fail
,比如下面的例子:
当出true时候就把第一项x吐出来,否则当出false,或者出失败fail的时候就把第二项y吐出来。一个“可能失败的t类型变量x”(不妨理解成C#里的可空类型修饰符T?,类型T?相比T多了一个取值null)可以表示成x : t or fail,所以上面的解构器可以这样写:
在这个case里,如果要搞成能处理x和y的失败问题,可以这样:
另外,由于引入了fail这个关键字,可以用定制的Axiom来对fail做捕获和给出兜底值,比如下面的例子里兜底值是c0
:
一些密码学操作(比如Diffie-Hellman
密钥协商)不能被编码为destructor,因为里面涉及代数学操作。ProVerif针对这种情况提供了一种替代模型——equation
,形如:
其中M和N是用前面的类型为t1…tn的变量x1…xn通过constructor构建而成的项,当然这两个项里面也可以完全没有用到这些变量(那这两项就是常量了),这种时候前面对这n个变量的类型声明会被忽略。可以一次性定义多条等式:
其中option的取值可以为[convergent
]、[linear
]或者为空,分别对应ProVerif里equation所的一些局限性。
例如,对Diffie-Hellman密钥协定方法建模时,由于要对求幂的操作进行建模,具体是要建模
对称加密也可以用equation来描述(添加[data]
声明为数据项):
有时,由不仅仅是一个构造函数或析构函数应用程序组成的术语会重复很多次,ProVerif提供了一个宏机制,以定义一个表示该项的函数符号,并避免重复,函数宏由以下声明进行定义:
f
是从M
的类型推导,[or fail]
控制传入的参数失败时处理方式,如果不加,类型失败时直接返回fail
,M
是富集项。
如果or fail
缺失并且参数失败,则函数宏也会失败。例如,与定义在一起使用
如果or fail
存在,且参数失败,则失败值将传递给函数宏,例如可以捕获它,并返回一些非失败结果。例如,对h
的定义与上面相同,以下定义为f
观察到,由于引入了额外的名称,导致了概率密码学的使用增加了模型的复杂性。这可能会减缓分析过程。
与上面的函数宏类似,进程宏也可以使用类型为t或or fail
的参数进行声明:
每个参数类型之后的可选的or fail
,允许用户在进程的某些参数失败时控制进程的行为:
ProVerif依赖于符号化的Dolev-Yao密码学模型,所以结果不能应用于计算模型,如果要用计算模型可以考虑CryptoVerif等其它工具。
哈希函数
哈希函数表示为一个没有关联析构函数的一元析构函数或方程h
。该构造函数将接受并返回一个位字符串作为输入。因此,我们的定义如下:
没有任何相关的析构函数或等式理论捕获了加密哈希函数的前图像阻、第二前图像阻和碰撞阻特性。事实上,确保了更强的性质:这个哈希函数模型接近随机Oracle模型。
对称加密
对称加密最基本的形式化是基于解密为析构函数的加密。然而,更接近实际的加密方案的形式化如下:
此析构函数允许攻击者测试是否使用同一密钥构建了两个密文。这种析构函数的存在对于可达性属性(保密、通信)没有任何区别,因为它不能允许攻击者构造它无法构造的术语。然而,它确实对观测等价性质有所影响。(请注意,将加密密钥交给攻击者,以模拟一个不隐藏密钥的方案,显然是一个严重的错误。)
非对称加密
也可以模拟加密泄漏密钥。由于加密密钥是公共的,我们可以通过将密钥交给攻击者:
非对称加密的另一种(和等价的)形式考虑一元构造函数pk’,sk’接受类型种子的参数,以捕获从某个send’构造密钥对的概念。
非确定性的(也就是带有概率的)数字签名,也可以分为带有消息恢复(Message Recovery,从签名中获取签名之前的原消息)机制的:
和不能获取原来的消息的(不带Message Recovery机制):
这种签名方法是把原始消息隐藏了,在做验证的时候把消息m传入,这样来检测是否能验证成功。
如果想要重新引入泄露消息的机制,就只要加个解构器就可以了(也就是给攻击者建模能从中获取消息的能力):
如果想建模公钥泄露,那么也是加一个解构器,使得能从签名结果取出公钥(也就是给攻击者建模这样一个能力:对于每个签名结果总是知道使用了哪个公钥来签名):
消息认证码
形式上和不带解构器的哈希很像,也是随机预言模型,但是建模的时候多提供了一个密钥: