一、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)相關:http://www.lxweimin.com/p/a2067de6b7ac
國密算法--Openssl 實現國密算法(基礎介紹和產生秘鑰對):https://blog.csdn.net/weixin_33849942/article/details/93292870
GMEllipticCurveCrypto:https://github.com/ankitthakur/GMEllipticCurveCrypto