RSA.docx
《RSA.docx》由会员分享,可在线阅读,更多相关《RSA.docx(40页珍藏版)》请在冰豆网上搜索。
RSA
[三、RSA的快速实现]
华中科技大学密码学课程设计报告
专业班级:
[信息安全0903]
学生姓名:
[曹晨业]
指导教师:
[崔国华]
完成时间:
2018年9月9日
RSA算法简介:
RSA算法基于一个十分简单的数论事实:
将两个大素数相乘十分容易,但那时想要对其乘积进行因式分解却极其困难,因此可以将乘积公开作为加密密钥。
RSA公开密钥密码体制。
所谓的公开密钥密码体制就是使用不同的加密密钥与解密密钥,是一种“由已知加密密钥推导出解密密钥在计算上是不可行的”密码体制。
在公开密钥密码体制中,加密密钥(即公开密钥)PK是公开信息,而解密密钥(即秘密密钥)SK是需要保密的。
加密算法E和解密算法D也都是公开的。
虽然秘密密钥SK是由公开密钥PK决定的,但却不能根据PK计算出SK。
正是基于这种理论,1978年出现了著名的RSA算法,它通常是先生成一对RSA密钥,其中之一是保密密钥,由用户保存;另一个为公开密钥,可对外公开,甚至可在网络服务器中注册。
为提高保密强度,RSA密钥至少为500位长,一般推荐使用1024位。
这就使加密的计算量很大。
为减少计算量,在传送信息时,常采用传统加密方法与公开密钥加密方法相结合的方式,即信息采用改进的DES或IDEA对话密钥加密,然后使用RSA密钥加密对话密钥和信息摘要。
对方收到信息后,用不同的密钥解密并可核对信息摘要。
RSA算法是第一个能同时用于加密和数字签名的算法,也易于理解和操作。
RSA是被研究得最广泛的公钥算法,从提出到现在的三十多年里,经历了各种攻击的考验,逐渐为人们接受,普遍认为是目前最优秀的公钥方案之一。
RSA的安全性依赖于大数的因子分解,但并没有从理论上证明破译RSA的难度与大数分解难度等价。
即RSA的重大缺陷是无法从理论上把握它的保密性能如何,而且密码学界多数人士倾向于因子分解不是NPC问题。
RSA的缺点主要有:
A)产生密钥很麻烦,受到素数产生技术的限制,因而难以做到一次一密。
B)分组长度太大,为保证安全性,n至少也要600bits以上,使运算代价很高,尤其是速度较慢,较对称密码算法慢几个数量级;且随着大数分解技术的发展,这个长度还在增加,不利于数据格式的标准化。
目前,SET(SecureElectronicTransaction)协议中要求CA采用2048bits长的密钥,其他实体使用1024比特的密钥。
C)RSA密钥长度随着保密级别提高,增加很快。
实验目的:
通过实际编程加深对RSA算法加密、脱密和密钥产生过程的理解。
实验原理:
1.大数存储和四则运算
根据RSA算法的要求,为了实现大数的各种复杂运算,需要首先实现大数存储和基本四则运算的功能。
当今开源的大数运算C++类有很多,多用于数学分析、天文计算等,本文选用了一个流行的大数类型,并针对RSA算法和本项目的具体需要对其进行了扩充和改进。
下面简单介绍大数存储和四则运算的实现原理。
最先完成的功能是大数的存储,存储功能由flex_unit类提供。
和普通的类型一样,每一个大数对应一个flex_unit的实例。
类flex_unit中,用一个无符号整数指针unsigned*a指向一块内存空间的首地址,这块内存空间用来存储一个大数,所以可以说,大数是被存储在一个以unsigned为单元的线性组中。
在方法voidreserve(unsignedx)中通过C++的new来给a开辟空间,当flex_unit的实例中被存入比当前存储的数更大的数时,就会调用reserve来增加存储空间,但是当flex_unit的实例中被存入比当前存储的数更小的数时,存储空间并不会自动紧缩,这是为了在运算的时候提高执行效率。
结合指针a,有两个重要的无符号整数来控制存储,unsignedz和unsignedn,z是被分配空间的单元数,随数字变大不断增大,不会自己紧缩,而n是当前存储的大数所占的单元数,组成一个大数的各unsigned单元的存入和读出由set、get方法完成,变量n是只读的。
类型unsigned在32位机是32位的,所以对于flex_unit这个大数类来说,每个大数最大可以达到个字节长,这已经超过了32位机通常的最大内存容量,所以是足够进行RSA所需要的各种运算的。
图3-2形象的说明了大数存储类flex_unit对大数的管理。
图3-2flex_unit对大数的管理
在flex_unit的存储功能基础上,将其派生,得到vlong_value,在vlong_value中实现四则运算函数,并实现强制转换运算符unsigned,以方便大数类型和普通整数的互相赋值。
当大数被强制转换为unsigned时,将取其最低四字节的值。
四则运算实现的原理十分简单,都是按最基本的算术原理实现的,四则运算过程的本质就是按一定数制对数字的计算,比如相加,就是低位单元对齐,逐单元相加并进位,减法同理。
而乘除法和取余也都是按照竖式运算的原理实现,并进行了必要的优化。
虽然实现了四则运算函数,但是若是程序里的运算都要调用函数,显得烦琐而且看起来不美观,所以我们另写一个类vlong,关联(Associate,即使用vlong_value类型的对象或其指针作为成员)vlong_value,在vlong重载运算符。
这样,当我们操作vlong大数对象的时候,就可以像使用一个简单类型一样使用各种运算符号了。
之所以将vlong_value的指针作为成员而不是直接构造的对象,也是为了提高执行效率,因为大型对象的拷贝要消耗不少机器时间。
2.大数幂模与乘模运算Montgomery幂模算法
在实现了vlong类型后,大数的存储和四则运算的功能都完成了。
考虑到RSA算法需要进行幂模运算,需要准备实现这些运算的方法。
所以写一个vlong的友元,完成幂模运算功能。
幂模运算是RSA算法中比重最大的计算,最直接地决定了RSA算法的性能,针对快速幂模运算这一课题,西方现代数学家提出了很多的解决方案。
经查阅相关数学著作,发现通常都是依据乘模的性质
,先将幂模运算化简为乘模运算。
通常的分解习惯是指数不断的对半分,如果指数是奇数,就先减去一变成偶数,然后再对半分,例如求D=
,E=15,可分解为如下6个乘模运算。
归纳分析以上方法,对于任意指数E,可采用如图3-3的算法流程计算。
图3-3幂模运算分解为乘模运算的一种流程
按照上述流程,列举两个简单的幂模运算实例来形象的说明这种方法。
①求
的值
开始D=1P=2mod17=2E=15
E奇数D=DPmodn=2P=PPmodn=4E=(E-1)/2=7
E奇数D=DPmodn=8P=PPmodn=16E=(E-1)/2=3
E奇数D=DPmodn=9P=PPmodn=1E=(E-1)/2=1
E奇数D=DPmodn=9P=PPmodn=1E=(E-1)/2=0
最终D=9即为所求。
②求
的值
开始D=1P=2mod17=2E=8
E偶数D=1P=PPmodn=4E=E/2=4
E偶数D=1P=PPmodn=3E=E/2=2
E偶数D=1P=PPmodn=9E=E/2=1
E奇数D=DPmodn=9P=不需要计算E=(E-1)/2=0
最终D=9即为所求。
观察上述算法,发现E根据奇偶除以二或减一除以二实际就是二进制的移位操作,所以要知道需要如何乘模变量,并不需要反复对E进行除以二或减一除以二的操作,只需要验证E的二进制各位是0还是1就可以了。
同样是计算
,下面给出从右到左扫描二进制位进行的幂模算法描述,设中间变量D,P,E的二进制各位下标从左到右为u,u-1,u-2,…,0。
Powmod(C,E,n)
{
D=1;
P=Cmodn;
fori=0toudo
{
if(Ei=1)D=D*P(modn);
P=P*P(modn);
}
returnD;
}
选择与模数n互素的基数R=2k,n满足2k-1≤n<2k,n应为奇数。
并且选择R-1及n’,满足0对于0≤mM(m)
{
if(t≥n)return(t-n);
elsereturnt;
}
因为
,故t为整数;同时
,得
。
由于
,M(m)中t结果范围是0≤t<2n,返回时如果t不小于n,应返回t-n。
本软件程序中,RSA核心运算使用的乘模算法就是M(A*B)。
虽然M(A*B)并不是乘模所需要的真正结果,但只要在幂模算法中进行相应的修改,就可以调用这个乘模算法进行计算了。
本软件起初未使用Montgomery乘模算法时,加密速度比使用Montgomery乘模算法慢,但速度相差不到一个数量级。
将上述乘模算法结合前面叙述的幂模算法,构成标准Montgomery幂模算法,即本软件所使用的流程,叙述如下。
M(m)
{
k=(m*n’)modR;
x=(m+k*n)/R;
if(x>=n)x-=n;
returnx;
}
exp(C,E,n)
{
D=R-n;
P=C*Rmodn;
i=0;
while(true)
{
if(E的当前二进制位Ei==1)D=M(D*P);//从低位到高位检测二进制位
i+=1;
if(i==E的二进制位数)break;
P=M(P*P);
}
returnD*R-1(modn);
}
在具体的实现中,对应monty类的mul和exp方法。
全局函数modexp初始化monty对象并调用其exp方法,使用的时候直接调用modexp即可。
3.寻找素数Eratosthenes筛选与Fermat素数测试
首先在需要寻找素数的整数范围内对整数进行筛选,把所有确知为合数的整数排除出去。
程序中构造了一个数组b[],大小为一轮素数搜索的范围,记搜索范围大小为SS。
b[0]到b[SS]分别对应大数start到start+SS。
b[]中所有元素先初始化为1,如果对应的大数确定为合数,就将b[]中对应的元素置为0。
最后,只需对那些b[]中为1的元素对应的大数进行比较确切的素数测试即可,只要被测试的数是素数概率达到一定门限,就判这个数为素数。
这样做既保证了这段程序可以在短时间内执行完,又保证了可以以比较高的准确度得到素数。
函数find_prime先把b[]的所有元素赋值为1,然后按参数start给标记数组b[]的各元素赋0值。
下面描述标记数组b[]的赋0值算法。
首先,在类Prime_factory_san被构造的时候,构造函数中从2开始搜寻一些小素数,记录在数组pl[]中,共记录NP个。
这些小素数用来当作因子,他们的倍数将被从大素数搜索范围内剔除(即把数组b[]的对应元素标记为0),剔除的程序代码如下。
for(i=0;i{
unsignedp=pl[i];
unsignedr=start%vlong(p);
if(r)r=p-r;
while(r{
b[r]=0;
r+=p;
}
}
这里利用start对各小素数因子p求模的办法,得到当前p在素数搜索范围内的最小倍数在b[]中的对应位置,将其剔除后,不断后移p个位置,将这个小素数因子p在搜索范围内的所有倍数全部剔除,如图3-4所示。
在完成对所有小素数因子的类似操作后,他们的倍数在搜索范围内的位置标记b[r]被全部标记为0。
实际上这就是Eratosthenes筛选法。
图3-4在素数搜索范围内剔除小素数因子p的倍数
接下来,对可能为素数的数(即标记数组b[]中值为1的元素对应的数)进行素数测试。
取一个与p互素的整数A,对于大素数p来说应该满足Ap-1modp=1,但是我们把p代入一个大整数,满足这个关系的数不一定是素数。
这时我们改变A,进行多次测试,如果多次测试都通过,这个数是素数的概率就比较大。
按这种原理,我们编写素数测试函数如下。
intis_probable_prime_san(constvlong&p)
{
constrep=4;//测试次数
constunsignedany[rep]={2,3,5,7};//测试用的底数
for(unsignedi=0;iif(modexp(any[i],p-vlong
(1),p)!
=vlong
(1))return0;
//modexp是幂模函数,按上一小节叙述的算法编码。
//这里modexp计算any[i]p-1modp。
return1;
}
测试通过,程序就判定这个数为找到的素数,将找到的素数返回给上层程序使用。
在这里其实有一个不可忽视的问题,就是得到一个测试通过的合数。
对于这种情况,RSA算法加密解密是否还可以实现,是一个需要从数学角度论证的问题。
因为得到素数的概率很高,经过一整天的生成密钥和加密操作,没有发现失败的密钥,所以本文暂没有对这个问题进行讨论。
综上所述,总结素数寻找的流程,如图3-5所示。
图3-5函数find_prime寻找素数的流程框图
得到了大素数,即RSA算法中的p、q,我们就可以计算出密钥,进行加密等操作了。
4.按常规RSA算法实现加密与解密
最后,类RSA_san基于前面的准备工作,实现RSA密钥生成和加解密的功能为了方便阅读,整个类的源程序中,所使用的变量字母均和RSA算法协议中一致。
在类RSA_san的构造函数里,执行准备一对随机密钥的操作。
之后可以直接使用类的其他成员进行RSA加解密操作,也可以载入以前保存的密钥或再次随机生成密钥。
类中各成员频繁的用到字符串和vlong类型的转换,因为大数是用字符串置入的,而把大数读出,也是保存在字符指针指向的一段内存空间里,所以也是字符串。
所以,需要实现一系列的编码转换函数,比如将unsigned指针指向的一段空间里保存的一个大数,表示成十六进制形式的字符串文本。
编码转换通常是用C风格的指针操作和sprintf函数来完成。
需要加密和解密的数据也是通过字符串参数置入的。
由于字符串的结尾字符“\0”实际上也可能是需要加密的数据,所以置入的串长度并不能以“\0”来决定,程序里引入一个unsigned类型的参数来决定置入的串长度,这样就解决了加密连0数据时候被截断的问题。
因为是对文件加密的软件,需要加密的数据通常并不止几字节,这时由上层程序将数据按用户的设置分块,分别加密或解密。
本软件默认的分块大小是1字节,即逐个字节作为参数,调用C++核心模块中的方法。
5、Rabin-Miller素数测试:
Rabin-Miller算法是典型的验证一个数字是否为素数的方法。
判断素数的方法是Rabin-Miller概率测试,那么他具体的流程是什么呢。
假设我们要判断n是不是素数,首先我们必须保证n是个奇数,那么我们就可以把n表示为n=(2^r)*s+1,注意s也必须是一个奇数。
然后我们就要选择一个随机的整数a(1<=a<=n-1),接下来我们就是要判断a^s=1(modn)或a^((2^j)*s)=-1(modn)(0<=j如果任意一式成立,我们就说n通过了测试,但是有可能不是素数也能通过测试。
所以我们通常要做多次这样的测试,以确保我们得到的是一个素数。
(DDS的标准是要经过50次测试)
采用Rabin-Miller算法进行验算
首先选择一个代测的随机数p,计算b,b是2整除p-1的次数。
然后计算m,使得n=1+(2^b)m。
(1)选择一个小于p的随机数a。
(2)设j=0且z=a^mmodp
(3)如果z=1或z=p-1,那麽p通过测试,可能使素数
(4)如果j>0且z=1,那麽p不是素数
(5)设j=j+1。
如果j且z<>p-1,设z=z^2modp,然后回到(4)。
如果z=p-1,那麽p通过测试,可能为素数。
(6)如果j=b且z<>p-1,不是素数
附代码:
#include
#include
#include
//随机数产生器
//产生m~n之间的一个随机数
unsignedlongrandom(unsignedlongm,unsignedlongn)
{
srand((unsignedlong)time(NULL));
return(unsignedlong)(m+rand()%n);
}
//模幂函数
//返回X^YmodN
longPowMod(longx,longy,longn)
{
longs,t,u;
s=1;t=x;u=y;
while(u){
if(u&1)s=(s*t)%n;
u>>=1;
t=(t*t)%n;
}
returns;
}
//Rabin-Miller素数测试,通过测试返回1,否则返回0。
//n是待测素数。
//注意:
通过测试并不一定就是素数,非素数通过测试的概率是1/4
intRabinMillerKnl(unsignedlongn)
{
unsignedlongb,m,j,v,i;
//先计算出m、j,使得n-1=m*2^j,其中m是正奇数,j是非负整数
m=n-1;
j=0;
while(!
(m&1))
{
++j;
m>>=1;
}
//随机取一个b,2<=bb=random(2,m);
//计算v=b^mmodn
v=PowMod(b,m,n);
//如果v==1,通过测试
if(v==1)
{
return1;
}
i=1;
//如果v=n-1,通过测试
while(v!
=n-1)
{
//如果i==l,非素数,结束
if(i==j)
{
return0;
}
//v=v^2modn,i=i+1
v=PowMod(v,2,n);
i++;
}
return1;
}
Intmain()
{
unsignedlongp;
intcount=0;
cout<<"请输入一个数字"<cin>>p;
for(inttemp=0;temp<5;temp++)
{
if(RabinMillerKnl(p))
{
count++;
}
else
break;
}
if(count==5)
cout<<"一共通过5次测试,是素数!
"<else
cout<<"不是素数"<return0;
}
6、Stein法求最大公约数
欧几里德算法是计算两个数最大公约数的传统算法,他无论从理论还是从效率上都是很好的。
但是他有一个致命的缺陷,这个缺陷只有在大素数时才会显现出来。
考虑现在的硬件平台,一般整数最多也就是64位,对于这样的整数,计算两个数之间的模是很简单的。
对于字长为32位的平台,计算两个不超过32位的整数的模,只需要一个指令周期,而计算64位以下的整数模,也不过几个周期而已。
但是对于更大的素数,这样的计算过程就不得不由用户来设计,为了计算两个超过64位的整数的模,用户也许不得不采用类似于多位数除法手算过程中的试商法,这个过程不但复杂,而且消耗了很多CPU时间。
对于现代密码算法,要求计算128位以上的素数的情况比比皆是,设计这样的程序迫切希望能够抛弃除法和取模。
Stein算法由J.Stein1961年提出,这个方法也是计算两个数的最大公约数。
和欧几里德算法算法不同的是,Stein算法只有整数的移位和加减法,这对于程序设计者是一个福音。
为了说明Stein算法的正确性,首先必须注意到以下结论:
∙gcd(a,a)=a,也就是一个数和他自身的公约数是其自身
∙gcd(ka,kb)=kgcd(a,b),也就是最大公约数运算和倍乘运算可以交换,特殊的,当k=2时,说明两个偶数的最大公约数必然能被2整除
有了上述规律就可以给出Stein算法如下:
1如果A=0,B是最大公约数,算法结束
2如果B=0,A是最大公约数,算法结束
3设置A1=A、B1=B和C1=1
4如果An和Bn都是偶数,则An+1=An/2,Bn+1=Bn/2,Cn+1=Cn*2(注意,乘2只要把整数左移一位即可,除2只要把整数右移一位即可)
5如果An是偶数,Bn不是偶数,则An+1=An/2,Bn+1=Bn,Cn+1=Cn(很显然啦,2不是奇数的约数)
6如果Bn是偶数,An不是偶数,则Bn+1=Bn/2,An+1=An,Cn+1=Cn(很显然啦,2不是奇数的约数)
7如果An和Bn都不是偶数,则An+1=|An-Bn|,Bn+1=min(An,Bn),Cn+1=Cn
8n++,转4
这个算法的原理很显然,所以就不再证明了。
现在考察一下该算法和欧几里德方法效率上的差别。
考虑欧几里德算法,最恶劣的情况是,每次迭代a=2b-1,这样,迭代后,r=b-1。
如果a小于2N,这样大约需要4N次迭代。
而考虑Stein算法,每次迭代后,显然AN+1BN+1≤ANBN/2,最大迭代次数也不超过4N次。
也就是说,迭代次数几乎是相等的。
但是,需要注意的是,对于大素数,试商法将使每次迭代都更复杂,因此对于大素数Stein将更有优势。
7、拉宾-米勒测试:
这是一个相当快速的随机算法(有较小的可能性错误),用于判断一个大数是否是素数。
快速素数检验是目前大部分公钥密码体系的关键。
米勒-拉宾检验的内容是:
要测试N是否为质数,首先将N-1分解为2^sd。
在每次测试开始时,先随机选一个介于[1,n-1的整数a,之后如果对所有的rin[0,s-1],若a^dmodN<>1且a^{2^{rd}}modN<>-1,则N是合数。
否则,N有3/4的机率为质数。
8、Montgomery快速幂模算法:
附代码:
viewplaincopytoclipboardprint?
unsignedintpower(unsignedintn,unsignedintp)
{
//计算n的p次方
unsignedinttmp=1;
while(p>1)
{
/