一、ECC椭圆曲线加密算法原理
1.1、椭圆曲线介绍
ECC(Elliptic Curves Cryptography,椭圆曲线加密)是一种公开密钥算法。1985年,Neal Koblitz和Victor Miller分别独立提出了ECC。基本形状可以参考如下图:
1.2、图解椭圆曲线
椭圆曲线有两个特点:
- 如果你在上方随便画一个点,那么下方也一定有一个对称的点,上下方距离水平线X轴是相同的。
- 随便在图形上画两个点,让这两个点连成线然后延长会经过第三个点,当然除了垂直线以外。
根据这两个特点我们就可以做文章了。如果有A,B两个点,延长以后会经过第三个点,而这第三个点以X轴为中心是会有一个点与其对称,我们就把这个对称的点先称为C点。这里头就像是运算过程,A和B得出C,在这里我就把运算过程称为 “点运算”,A点B得到C,这个 “点运算”其实就是椭园曲线上的加法运算,但为了让你们不与传统加法运算弄混,这里先使用“点运算”的说法。
现在我们把A和C进行连线,同样经过了第三个点,第三个点也有一个对称的点,这里称为D点,也就是A点C得到D,我们再把A和D连线,也经过了第三个点,第三个点也有一个对称的点,这里称为E点,也就是A点D得到E。
问题:已知起点是A,终点是E,请问起点A经过多少次点运算得到E?我们很难知道经过了多少次,这就很符合我们前面说的公钥加密的特点:正向简单,逆向困难
我们还要考虑一种特殊情況,如果我们取一个点,命名为P,然后画一条直线,结果发现这条直线只能与椭圆曲线相交于一个点,而不是刚刚所说的一共有三个点,这种情況怎么定义运算呢?
首先解释一下这是一条切线,如果不记得什么是切线,那就想象这里有一个圆,而切线是垂直于经过切点的半径,还不明白也没关系,现在点P身处的这条直线相交椭圆曲线后,也有一个对称的点,这里先命名为Q点, 现在重点来了!因为点P初始没有和其他点连成线,不是刚刚那种开始就有两个点来连线,因此这里就认为是P点P的运算,也就是自己点自己,所以Q就是P点P的运算结果,也就是两介P得到Q,我们就把点Q称为2P,因为是两个P得出的点,你也可以理解为P+P=2P。
现在P与2P连线,也经过了第三个点,第三个点也有一个对称的点,P+2P就等于3P,这个点就是3P,那么这个过程可以一直延续下去,我们是可以得到6P这个点的,6P这个点就很有灵性了,比如3P点3P,两次的话可以得到6P,也就是3P乘以2得到6P,那2P点2P,三次的话也可以得到6P,也就是2P乘以3得到6P,其实就是简单的乘法问题,只不过是椭园曲线上的,不要小看这简单的表示方法,等会你可能就会对着屏幕说“握草”。
1.3、笛福赫尔曼(Diffie-Hellman)算法
DH 算法又称“Diffie–Hellman 算法”,像往常的算法名字一样,这是用俩个数学牛人的名字来命名的算法,实现安全的密钥交换,通讯双方在完全没有对方任何预先信息的条件下通过不安全信道创建起一个密钥。
如下图示例:首先两个要沟通的对象需要确定两个参数,参数P和参数G,参数P,故名意思是一个质数,因为P是Prime的缩写,而参数G是Generator的缩写,这里就不详细解释G的缘由了,因为涉及到一些数学的知识。这里我选一个简单的质数23,因为23乘以1等于23,没有其它整数相乘可以得到23了,而参数G这里选择5,这两个参数是可以公开的,所以黑客知道也没关系。
现在就可以套用公式1了,5的随机数次方除以23来求余数,这个公式也是公开的,黑客知道也没毛病,现在两边要各自生成一个随机数,蒜老大生成了6,油大叔生成了15,各自生成的随机数套入这个公开的公式,也就是蒜老大进行5的6次方要除以23得到余数8,油大叔进行5的15次方要除以23得到余数19,各自把生成的余数发送给对方。
对方收到后套用公式2,各自收到的余数的随机数次方除以参数P求出新的余数,对于蒜老大,就是用收到的19进行6次方运算再除以23得到余数2,这里的数字6就是蒜大哥自己生成的随机数,23就是前面定义好的参数P,对于油大叔来说,就是用收到的8进行15次方运算再除以23得到余数2,这里的数字15就是油大叔自己生成的随机数,23也是前面定义好的参数P,现在大家可以看到两边得到的余数都是一样的,都是数字2,两边就可以用这个数字2来对后续的对话进行加密了,没人知道原来他们用这个2来加密后续的对话,当然了实际上的随机数和质数其实并没有这么简单,通常都建议质数至少要有2048比特的长度,就是为了防止破解。
1.4、ECDH算法
ECDH是Elliptic Curve Diffie-Hellman,它一种基于ECC的密钥协商算法。ECDH是笛福赫尔曼(Diffie-Hellman)算法的变种,它是一种密钥协商协议,定义了密钥怎么样在通信双方之间生成和交换。其思路过程与笛福赫尔曼密钥协商算法基本相同,只是在具体的协商计算中使用ECC。
如下图示例:Alice需要生成一个私钥小a,然后再确定椭圆曲线上的一个点:G,这个G点是公开的,是大家都可以有的G点,接着Alice需要生成公钥大A,公钥就利用前面说到的椭圆曲线来运算,也就是公钥大A等于小a点G,这里的意思就是G这个点进行点运算,次数是a,也就是G点G点G点…一共a次,得到了椭圆曲线上的点大A,现在Alice把大A和G发送给Bob,也就是大A和G是公开的,有同学可能就在想,别人知道大A和G,那小a不就很容易算出来吗?其实前面已经说了,一定不容易,知道起点和终点,但是中间经历多少次是非常难知道的,这就是把椭园曲线加进来的奥义。
Bob收到后,也生成了一个私钥小b,然后生成椭园曲线上的一个新点(大B),这个大B就是G点进行小b次运算得到的,也就是G点G点G点…一共b次,现在Bob把生成的大B发送给Alice,别人知道大B和大G两个点也很难得到小b,还是那句话:中间经历多少次很难知道。现在Alice用私钥小a和收到的大B进行运算得到新的密钥,Bob用私钥小b和收到的大A进行运算也得到了新的密钥,这个新的密钥就只有他们知道,也就是会话密钥,而且这个密钥必须是相同的。相信你对于这个运算还有点懵,你们看Alice这边,大B其实就等于小b点G,直接从Bob那边把等式代入就明白了,而在Bob这边,大A其实就等于小a点G,直接从Alice那边把等式代入就明白了,这样一看就知道密钥是相同的,只不过小a和小b互换了位置。如果你还看不出,假设a等于3,b等于2,不管是2乘以3,还是3乘以2,其实就等于6G了,也就是前面提及到的运算方法,这个密钥交换过程也就是ECDHE的原理,ECDHE就是椭园曲线和DH混合起来的密钥交换算法。
二、openssl库的使用
2.1、openssl编译生成静态库
- 首先下载openssl库:https://github.com/krzyzanowskim/OpenSSL
- Makefile文件,修改版本号,我这里使用 3.1.0 版本,然后也可以选择注释掉"frameworks"的打包
- 20-apple.conf文件修改iOS配置参数,去除"-fembed-bitcode"。然后修改最低支持的版本'-mios-version-min=9.0‘
- build.sh文件最下方,注释"build_macos"、"build_catalyst",我们只打iOS环境的包。还可以选择注释掉"build "x86_64"",只打真机包。
- 修改homebrew的配置文件,详情参考:https://blog.csdn.net/zhanghao143lina/article/details/128656499
- 最后执行make指令即可,打出包在Frameworks文件夹或者iphoneos文件夹中
2.2、openssl生成密钥对
可下载 Demo,相关方法存放在ZJHOpenSSLTool
类中
详见下面代码及注释:
/// 生成ECC曲线:256r1
+ (int)generateEccCurve:(EC_GROUP **)g_group_tem ec_key:(EC_KEY **)ec_key_tem {
// 初始化一个空算法组:这里只是用EC_GROUP_new生成一个空的group, 然后由p,a,b等参数来填充group,
// 再以这个group为基础去生成曲线上的点
EC_GROUP *g_group = EC_GROUP_new(EC_GFp_mont_method());
*g_group_tem = g_group;
// 新建的密钥结构体(EC_KEY_new),此时还没有公私钥信息
EC_KEY *ec_key = EC_KEY_new();
*ec_key_tem = ec_key;
// BN_CTX openssl中加密算法结构体,里面包含各种加密算法的函数指针
BN_CTX *g_ctx = NULL;
// 大数初始化
BIGNUM *p, *a, *b, *gx, *gy, *z;
p = BN_new();
a = BN_new();
b = BN_new();
gx = BN_new();
gy = BN_new();
z = BN_new();
// 将国密算法的参数转为大数。这里是把定义的曲线常量转换成大数表式,这样才能使用openssl中的接口。
BN_hex2bn(&p, _P);
BN_hex2bn(&a, _a);
BN_hex2bn(&b, _b);
BN_hex2bn(&gx, _Gx);
BN_hex2bn(&gy, _Gy);
BN_hex2bn(&z, _n); // 素数P的阶
int ret = -1; // 返回码
do {
// 先确定sm2曲线:设置素数域椭圆曲线参数
if (!EC_GROUP_set_curve_GFp(g_group, p, a, b, g_ctx)) {
ret = -2;
break;
}
// 取曲线上的三个点
EC_POINT* point_p = EC_POINT_new(g_group);
// 设置基点坐标:设置素数域椭圆曲线上点point的几何坐标
if (!EC_POINT_set_affine_coordinates_GFp(g_group, point_p, gx, gy, g_ctx)) {
ret = -3;
break;
}
// 确定P点是否在曲线上
if (!EC_POINT_is_on_curve(g_group, point_p, g_ctx)) {
ret = -4;
break;
}
// 设置椭圆曲线的基G,完成了国密曲线:generator、order和cofactor为输入参数
if(!EC_GROUP_set_generator(g_group, point_p, z, BN_value_one())) {
ret = -5;
break;
}
// 生成ECKey
if (!EC_KEY_set_group(ec_key, g_group)) {
ret = -6;
break;
}
if (point_p != NULL) {
EC_POINT_free(point_p);
}
ret = 0;
} while (NO);
return ret;
}
/// 生成公私钥对
+ (NSArray *)generateEccKeyPair {
EC_GROUP *g_group = NULL;
EC_KEY *ec_key = NULL;
NSData *privateKeyData = nil;
NSData *publicKeyData = nil;
int ret = -1; // 返回码
do {
// 生成曲线
ret = [self generateEccCurve:&g_group ec_key:&ec_key];
if (ret != 0) {
break;
} else {
ret = -1; // 重置一下
}
// 生成秘钥对,在曲线上生成秘钥对,生成椭圆曲线公私钥
if(!EC_KEY_generate_key(ec_key)) {
ret = -7;
break;
}
unsigned char pri[32] = {0};
// EC_KEY_get0_private_key(读取私钥信息)
BN_bn2bin(EC_KEY_get0_private_key(ec_key), pri); // 大数转二进制
privateKeyData = [NSData dataWithBytes:pri length:32]; // 转换私钥Data
// NSLog(@"privateKeyData : %@", self.privateKeyData);
// EC_KEY_get0_public_key(读取公钥信息)
const EC_POINT *pub_key;
unsigned char pubbuf[1024] = {0};
pub_key = EC_KEY_get0_public_key(ec_key);
/* 功能:将点的仿射坐标(以压缩或者不压缩形式)转化成字符串
输入:group,point,form【压缩方式】,len【允许的字符串大小上限】 输出:buf【字符串】
返回:转化得到的字符串长度 or 1【point=∞】*/
size_t buflen = EC_POINT_point2oct(g_group, pub_key, EC_KEY_get_conv_form(ec_key), pubbuf, sizeof(pubbuf), NULL);
publicKeyData = [NSData dataWithBytes:pubbuf length:buflen]; // 转换公钥Data
// NSLog(@"publicKeyData : %@", self.publicKeyData);
ret = 0; // 处理成功
} while (NO);
if (g_group != NULL) { // 释放资源
EC_GROUP_free(g_group);
}
if (ec_key != NULL) {
EC_KEY_free(ec_key);
}
if (ret < 0) {
NSLog(@"生成密钥对失败 code :%d", ret);
}
if (privateKeyData && publicKeyData) { // 成功返回数据
return @[privateKeyData, publicKeyData];
}
return nil; // 失败返回空
}
2.3、openssl的ECDH方法
详见下面代码及注释:
/// ECDH 密钥协商
+ (NSData *)computeECDHWithPublicKey:(NSString *)publicKey
privateKey:(NSString *)privateKey {
if (!publicKey || publicKey.length == 0 || !privateKey || privateKey.length == 0) {
return nil;
}
if (publicKey.length == 128) { // 可能没有公约的首位数据,这里拼接一下04
publicKey = [NSString stringWithFormat:@"04%@",publicKey];
}
const char *public_key = publicKey.UTF8String; // 公钥
const char *private_key = privateKey.UTF8String; // 私钥
EC_GROUP *g_group = NULL;
EC_KEY *ec_key = NULL;
EC_POINT *pub_point = NULL; // 公钥
BIGNUM *pri_big_num = NULL; // 私钥
NSData *ecdhKeyData = nil; // 协商出的密钥数据
int ret = -1; // 返回码
do {
// 生成曲线
ret = [self generateEccCurve:&g_group ec_key:&ec_key];
if (ret != 0) {
break;
} else {
ret = -1; // 重置一下
}
// 公钥转换为 EC_POINT
pub_point = EC_POINT_new(g_group);
EC_POINT_hex2point(g_group, public_key, pub_point, NULL);
// 私钥转换为 BIGNUM 并存储在 EC_KEY 中
if (!BN_hex2bn(&pri_big_num, private_key)) {
ret = -7;
break;
}
/* 功能:设置密钥的点群信息 输入:key,group
输出:key【设置好了密钥的点群信息】*/
if (!EC_KEY_set_group(ec_key, g_group)) {
ret = -8;
break;
}
// 设置私钥
if (!EC_KEY_set_private_key(ec_key, pri_big_num)) {
ret = -9;
break;
}
OPENSSL_FILE;
OPENSSL_LINE;
size_t outlen = 32;
uint8_t *ecdh_text = (uint8_t *)OPENSSL_zalloc(outlen + 1);
int retCode = ECDH_compute_key(ecdh_text, outlen, pub_point, ec_key, 0);
if (retCode <= 0) {
ret = -10;
break;
}
ecdhKeyData = [NSData dataWithBytes:ecdh_text length:outlen];
OPENSSL_free(ecdh_text);
ret = 0; // 处理成功
} while (NO);
if (g_group != NULL) { // 释放资源
EC_GROUP_free(g_group);
}
if (ec_key != NULL) {
EC_KEY_free(ec_key);
}
if (pub_point != NULL) {
EC_POINT_free(pub_point);
}
if (pri_big_num != NULL) {
BN_free(pri_big_num);
}
if (ret < 0) {
NSLog(@"密钥协商失败 code :%d", ret);
}
return ecdhKeyData;
}
三、GMEllipticCurveCrypto库的使用
可下载 Demo,相关方法存放在ViewController
类中
3.1、GMEllipticCurveCrypto库简介
GMEllipticCurveCrypto 是包含椭圆曲线数字签名算法(ECDSA)和椭圆曲线Diffie-Hellman(ECDH)的Objective-C库。ECDSA允许使用私钥生成签名,并使用公钥进行验证。ECDH允许两个身份使用自己的私钥和彼此的公钥来生成共享密钥,然后可用于加密。该库主要基于easy-ecc库(https://github.com/kmackay/easy-ecc)。
- 支持:secp128r1, secp192r1, secp256r1, secp384r1
- 基于私钥或公钥自动检测曲线
- 支持键作为原始字节或base64编码的字符串
- BSD 2条款许可证
3.2、创建公私钥示例
/// GMEllipticCurveCrypto生成密钥对
- (void)demoGMEllipticCurveCryptoGenerateEccKeyPair {
// 公钥长度相关问题:https://stackoverflow.com/questions/69402678/swift-generate-shared-key-using-ecdh
GMEllipticCurveCrypto *crypto =
[GMEllipticCurveCrypto generateKeyPairForCurve:GMEllipticCurveSecp256r1];
NSData *pub1 = crypto.publicKey; // 32位公钥
NSData *pub2 = [crypto decompressPublicKey:pub1]; // 还原成65位公钥
NSLog(@"Public Key data1: %@", pub1);
NSLog(@"Public Key data2: %@", pub2);
NSLog(@"Private Key data: %@", crypto.privateKey);
NSLog(@"Public Key: %@", crypto.publicKeyBase64);
NSLog(@"Private Key: %@", crypto.privateKeyBase64);
NSLog(@"");
//
}
代码中默认生成的密钥长度为32位,是压缩后的,如果想变成65位的,需要调用 \- (NSData*)decompressPublicKey:(NSData*)publicKey;
,不过该放在默认是没有开放出来的,可以手动添加到头文件中GMEllipticCurveCrypto.h
。参考链接:https://stackoverflow.com/questions/69402678/swift-generate-shared-key-using-ecdh
另外需要注意的是生成的publicKeyBase64数据,iOS中没有 ASN.1 OID 的标头数据,需要自己拼接下,具体处理可参考链接:https://blog.csdn.net/wei372889893/article/details/120494575
3.3、ECDH方法示例
/// GMEllipticCurveCrypto的ECDH方法
- (void)demoGMEllipticCurveCryptoECDH {
/* 公钥:04E3517069E8D411FDD070C9141B4C22A7B29628CE9988689CB38B148F426376BBA00ECA56E3B641C9B349A6DC64BC20F916D71CBE95D28490C82F079C6BBFECFE
私钥:C77337BB1EEDBA2B9C8C366E6EE525788156D90771CF51742D9CBFDAEEE52326
协商结果:9B9E0AAD7D0FE03BD9BC326DABB44B1C1FC547B8FD0708F6C1C15075001B7B7F
*/
NSString *pubStr = @"04E3517069E8D411FDD070C9141B4C22A7B29628CE9988689CB38B148F426376BBA00ECA56E3B641C9B349A6DC64BC20F916D71CBE95D28490C82F079C6BBFECFE";
NSString *priStr = @"C77337BB1EEDBA2B9C8C366E6EE525788156D90771CF51742D9CBFDAEEE52326";
NSData *pubData = [self dataFromHexString:pubStr];
NSData *priData = [self dataFromHexString:priStr];
// Alice performs...
GMEllipticCurveCrypto *Alice =
[GMEllipticCurveCrypto cryptoForCurve: GMEllipticCurveSecp256r1];
alice.privateKey = priData;
NSData *pubData2 = [alice compressPublicKey:pubData]; // 压缩公钥
NSData *shareKey = [alice sharedSecretForPublicKey:pubData2];
NSLog(@"Shared Secret: %@", shareKey);
NSString *shareKeyStr = [self hexDataToNSString:shareKey];
NSLog(@"***ZJH keyDataStr : %@", shareKeyStr);
NSLog(@"");
}
参考链接:
DH算法 | 迪菲-赫尔曼Diffie–Hellman 密钥交换:https://www.bilibili.com/video/BV1sY4y1p78s/?spm_id_from=333.788&vd_source=7d8a08755bacd471929384973dc151c0
公钥加密技术ECC椭圆曲线加密算法原理:https://www.bilibili.com/video/BV1BY411M74G/?spm_id_from=333.337.search-card.all.click&vd_source=7d8a08755bacd471929384973dc151c0
椭圆曲线加密(Elliptic Curve Cryptography)相关:https://www.jianshu.com/p/a2067de6b7ac
国密算法--Openssl 实现国密算法(基础介绍和产生秘钥对):https://blog.csdn.net/weixin_33849942/article/details/93292870
GMEllipticCurveCrypto:https://github.com/ankitthakur/GMEllipticCurveCrypto