寫給開發人員的實用密碼學(七)—— 非對稱金鑰加密演算法 RSA/ECC

於清樂發表於2022-03-19

本文部分內容翻譯自 Practical-Cryptography-for-Developers-Book,筆者補充了密碼學歷史以及 openssl 命令示例,並重寫了 RSA/ECC 演算法原理、程式碼示例等內容。

《寫給開發人員的實用密碼學》系列文章目錄:

一、公鑰密碼學 / 非對稱密碼學

在介紹非對稱金鑰加密方案和演算法之前,我們首先要了解公鑰密碼學的概念。

密碼學的歷史

從第一次世界大戰、第二次世界大戰到 1976 年這段時期密碼的發展階段,被稱為「近代密碼階段」。
在近代密碼階段,所有的密碼系統都使用對稱密碼演算法——使用相同的金鑰進行加解密。
當時使用的密碼演算法在擁有海量計算資源的現代人看來都是非常簡單的,我們經常看到各種講述一二戰的諜戰片,基本都包含破譯電報的片段。

第一二次世界大戰期間,無線電被廣泛應用於軍事通訊,圍繞無線電通訊的加密破解攻防戰極大地影響了戰局。

公元20世紀初,第一次世界大戰進行到關鍵時刻,英國破譯密碼的專門機構「40號房間」利用繳獲的德國密碼本破譯了著名的「齊默爾曼電報」,其內容顯示德國打算聯合墨西哥對抗可能會參戰的美國,這促使美國放棄中立對德宣戰,從而徹底改變了一戰的走勢。

1943 年,美國從破譯的日本電報中得知山本五十六將於 4 月 18 日乘中型轟炸機,由 6 架戰鬥機護航,到中途島視察。美國總統羅斯福親自做出決定截擊山本,山本乘坐的飛機在去往中途島的路上被美軍擊毀,戰爭天才山本五十六機毀人亡,日本海軍從此一蹶不振。

此外,在二次世界大戰中,美軍將印第安納瓦霍土著語言作為密碼使用,並特別徵募使用印第安納瓦霍通訊兵。在二次世界大戰日美的太平洋戰場上,美國海軍軍部讓北墨西哥和亞歷桑那印第安納瓦霍族人使用納瓦霍語進行情報傳遞。納瓦霍語的語法、音調及詞彙都極為獨特,不為世人所知道,當時納瓦霍族以外的美國人中,能聽懂這種語言的也就一二十人。這是密碼學語言學的成功結合,納瓦霍語密碼成為歷史上從未被破譯的密碼。

在 1976 年 Malcolm J. Williamson 公開發表了現在被稱為「Diffie–Hellman 金鑰交換,DHKE」的演算法,並提出了「公鑰密碼學」的概念,這是密碼學領域一項劃時代的發明,它宣告了「近代密碼階段」的終結,是「現代密碼學」的起點。

言歸正傳,對稱密碼演算法的問題有兩點:

  • 需要安全的通道進行金鑰交換」,早期最常見的是面對面交換金鑰
  • 每個點對點通訊都需要使用不同的金鑰,金鑰的管理會變得很困難
    • 如果你需要跟 100 個朋友安全通訊,你就要維護 100 個不同的對稱金鑰,而且還得確保它們不洩漏。

這會導致巨大的「金鑰交換」跟「金鑰儲存與管理」的成本。「公鑰密碼學」最大的優勢就是,它解決了這兩個問題:

  • 「公鑰密碼學」可以在不安全的通道上安全地進行金鑰交換,第三方即使監聽到通訊過程,但是(幾乎)無法破解出金鑰。
  • 每個人只需要公開自己的公鑰,就可以跟其他任何人安全地通訊。
    • 如果你需要跟 100 個朋友安全通訊,你們只需要公開自己的公鑰。傳送訊息時使用對方的公鑰加密,接收訊息時使用自己的公鑰解密即可。
    • 只有你自己的私鑰需要保密,所有的公鑰都可以公開,這就顯著降低了金鑰的維護成本。

因此公鑰密碼學成為了現代密碼學的基石,而「公鑰密碼學」的誕生時間 1976 年被認為是現代密碼學的開端。

公鑰密碼學的概念

公鑰密碼系統的金鑰始終以公鑰 + 私鑰對的形式出現,公鑰密碼系統提供數學框架和演算法來生成公鑰+私鑰對。
公鑰通常與所有人共享,而私鑰則保密。
公鑰密碼系統在設計時就確保了幾乎不可能從其公開的公鑰逆向演算出對應的私鑰。

公鑰密碼系統主要有三大用途:加密與解密、簽名與驗證、金鑰交換
每種演算法都需要使用到公鑰和私鑰,比如由公鑰加密的訊息只能由私鑰解密,由私鑰簽名訊息需要用公鑰驗證。

由於加密解密、簽名驗證均需要兩個不同的金鑰,故「公鑰密碼學」也被稱為「非對稱密碼學」。

比較著名的公鑰密碼系統有:RSA、ECC(橢圓曲線密碼學)、ElGamal、Diffie-Hellman、ECDH、ECDSA 和 EdDSA。許多密碼演算法都是以這些密碼系統為基礎實現的,例如 RSA 簽名、RSA 加密/解密、ECDH 金鑰交換以及 ECDSA 和 EdDSA 簽名。

量子安全性

參考文件:https://en.wikipedia.org/wiki/Post-quantum_cryptography

目前流行的公鑰密碼系統基本都依賴於 IFP(整數分解問題)、DLP(離散對數問題)或者 ECDLP(橢圓曲線離散對數問題),這導致這些演算法都是量子不安全(quantum-unsafe)的。

如果人類進入量子時代,IFP / DLP / ECDLP 的難度將大大降低,目前流行的 RSA、ECC、ElGamal、Diffie-Hellman、ECDH、ECDSA 和 EdDSA 等公鑰密碼演算法都將被淘汰。

目前已經有一些量子安全的公鑰密碼系統問世,但是因為它們需要更長的金鑰、更長的簽名等原因,目前還未被廣泛使用。

一些量子安全的公鑰密碼演算法舉例:NewHope、NTRU、GLYPH、BLISS、XMSS、Picnic 等,有興趣的可以自行搜尋相關文件。

二、非對稱加密方案簡介

非對稱加密要比對稱加密複雜,有如下幾個原因:

  • 使用金鑰對進行加解密,導致其演算法更為複雜
  • 只能加密/解密很短的訊息
    • 在 RSA 系統中,輸入訊息應該被轉換為大整數(例如使用 OAEP 填充),然後才能進行加密。
  • 一些非對稱密碼系統(如 ECC)不直接提供加密能力,需要結合使用更復雜的方案才能實現加解密

此外,非對稱密碼比對稱密碼慢非常多。比如 RSA 加密比 AES 慢 1000 倍,跟 ChaCha20 就更沒法比了。

為了解決上面提到的這些困難並支援加密任意長度的訊息,現代密碼學使用「非對稱加密方案」來實現訊息加解密。
又因為「對稱加密方案」具有速度快、支援加密任意長度訊息等特性,「非對稱加密方案」通常直接直接組合使用對稱加密演算法非對稱加密演算法。比如「金鑰封裝機制 KEM(key encapsulation mechanisms))」與「整合加密方案 IES(Integrated Encryption Scheme)」

1. 金鑰封裝機制 KEM

金鑰封裝機制 KEM 的加密流程(使用公鑰加密傳輸對稱金鑰):

金鑰封裝機制 KEM 的解密流程(使用私鑰解密出對稱金鑰,然後再使用這個對稱金鑰解密資料):

RSA-OAEP, RSA-KEM, ECIES-KEM 和 PSEC-KEM. 都是 KEM 加密方案。

金鑰封裝(Key encapsulation)與金鑰包裹(Key wrapping)

主要區別在於使用的是對稱加密演算法、還是非對稱加密演算法:

  • 金鑰封裝(Key encapsulation)指使用非對稱密碼演算法的公鑰加密另一個金鑰。
  • 金鑰包裹(Key wrapping)指使用對稱密碼演算法加密另一個金鑰。

2. 整合加密方案 IES

整合加密方案 (IES) 在金鑰封裝機制(KEM)的基礎上,新增了金鑰派生演算法 KDF、訊息認證演算法 MAC 等其他密碼學演算法以達成更高的安全性。

在 IES 方案中,非對稱演算法(如 RSA 或 ECC)跟 KEM 一樣,都是用於加密或封裝對稱金鑰,然後通過對稱金鑰(如 AES 或 Chacha20)來加密輸入訊息。

DLIES(離散對數整合加密方案)和 ECIES(橢圓曲線整合加密方案)都是 IES 方案。

三、RSA 密碼系統

RSA 密碼系統是最早的公鑰密碼系統之一,它基於 RSA 問題和整數分解問題 (IFP)的計算難度。
RSA 演算法以其作者(Rivest–Shamir–Adleman)的首字母命名。

RSA 演算法在計算機密碼學的早期被廣泛使用,至今仍然是數字世界應用最廣泛的密碼演算法。
但是隨著 ECC 密碼學的發展,ECC 正在非對稱密碼系統中慢慢佔據主導地位,因為它比 RSA 具有更高的安全性和更短的金鑰長度。

RSA 演算法提供如下幾種功能:

  • 金鑰對生成:生成隨機私鑰(通常大小為 1024-4096 位)和相應的公鑰。
  • 加密解密:使用公鑰加密訊息(訊息要先轉換為 [0...key_length] 範圍內的整數),然後使用金鑰解密。
  • 數字簽名:簽署訊息(使用私鑰)和驗證訊息簽名(使用公鑰)。
    • 數字簽名實際上是通過 Hash 演算法 + 加密解密功能實現的。後面會介紹到,它與一般加解密流程的區別,在於數字簽名使用私鑰加密,再使用公鑰解密。
  • 金鑰交換:安全地傳輸金鑰,用於以後的加密通訊。

RSA 可以使用不同長度的金鑰:1024、2048、3072、4096、8129、16384 甚至更多位。目前 3072 位及以上的金鑰長度被認為是安全的,曾經大量使用的 2048 位 RSA 現在被破解的風險在不斷提升,已經不推薦使用了。

更長的金鑰提供更高的安全性,但會消耗更多的計算時間,因此需要在安全性和速度之間進行權衡。
非常長的 RSA 金鑰(例如 50000 位或 65536 位)對於實際使用可能太慢,例如金鑰生成可能需要幾分鐘到幾個小時。

RSA 金鑰對生成

RSA 金鑰對的生成跟前面介紹的「DHKE 金鑰交換演算法」會有些類似,但是要更復雜一點。

首先看下我們怎麼使用 openssl 生成一個 1024 位的 RSA 金鑰對(僅用做演示,實際應用中建議 3072 位):

OpenSSL 是目前使用最廣泛的網路加密演算法庫,支援非常多流行的現代密碼學演算法,幾乎所有作業系統都會內建 openssl.

# 生成 1024 位的 RSA 私鑰
❯ openssl genrsa -out rsa-private-key.pem 1024
Generating RSA private key, 1024 bit long modulus
.................+++
.....+++
e is 65537 (0x10001)

# 使用私鑰生成對應的公鑰檔案
❯ openssl rsa -in rsa-private-key.pem -pubout -out rsa-public-key.pem
writing RSA key

# 檢視私鑰內容
❯ cat rsa-private-key.pem
-----BEGIN RSA PRIVATE KEY-----
MIICXAIBAAKBgQDNE8QZLJZXREOeWZ2ilAzGC4Kjq/PfsFzrXGj8g3IaS4/J3JrB
o3qEq/k9XoRzOmNPyvWCj2FAY7A099d7qX4ztthBpUM2ePDIYDvhL0EpfQqbhe+Q
aagcFpuKTshGR2wBjH0Cl1/WxJkfIUMmWYU+m4iKLw9KfLX6BjmSgWB6HQIDAQAB
AoGADb5NXgKG8MI6ZdpLniGd2Yfb8WwMo+kF0SAYSRPmCa0WrciC9ocmJs3/ngU/
ixlWnnpTibRiKBaGMIaLglYRhvbvibUo8PH4woIidTho2e6swF2aqILk6YFJDpxX
FCFdbXM4Cm2MqbD4VtmhCYqbvuiyEUci83YrRP0jJGNt0GECQQDyZgdi8JlFQFH8
1QRHjLN57v5bHQamv7Qb77hlbdbg1wTYO+H8tsOB181TEHA7uN8hxkzyYZy+goRx
n0hvJcQXAkEA2JWhCb7oG1eal1aUdgofxhlWnkoFeWHay2zgDWSqmGKyDt0Cb1jq
XTdN9dchnqfptWN2/QPLDgM+/9g39/zv6wJATC1sXNeoE29nVMHNGn9JWCSXoyK4
GGdevvjTRm0Cfp6UUzBekQEO6Btd16Du5JXw6bhcLkAm9mgmH18jcGq5+QJBALnr
aDv3d0PRZdE372WMt03UfniOzjgueiVaJtMYcSEyx+reabKvvy+ZxACfVirdtU+S
PJhhYzN6MeBp+VGV/VUCQBXz0LyM08roWi6DiaRwJIbYx+WCKEOGXQ9QsZND+sGr
pOpugr3mcUge5dcZGKtsOUx2xRVmg88nSWMQVkTlsjQ=
-----END RSA PRIVATE KEY-----

# 檢視私鑰的詳細引數
❯ openssl rsa -noout -text -in rsa-private-key.pem
Private-Key: (1024 bit)
modulus:
    00:cd:13:c4:19:2c:96:57:44:43:9e:59:9d:a2:94:
    0c:c6:0b:82:a3:ab:f3:df:b0:5c:eb:5c:68:fc:83:
    72:1a:4b:8f:c9:dc:9a:c1:a3:7a:84:ab:f9:3d:5e:
    84:73:3a:63:4f:ca:f5:82:8f:61:40:63:b0:34:f7:
    d7:7b:a9:7e:33:b6:d8:41:a5:43:36:78:f0:c8:60:
    3b:e1:2f:41:29:7d:0a:9b:85:ef:90:69:a8:1c:16:
    9b:8a:4e:c8:46:47:6c:01:8c:7d:02:97:5f:d6:c4:
    99:1f:21:43:26:59:85:3e:9b:88:8a:2f:0f:4a:7c:
    b5:fa:06:39:92:81:60:7a:1d
publicExponent: 65537 (0x10001)
privateExponent:
    0d:be:4d:5e:02:86:f0:c2:3a:65:da:4b:9e:21:9d:
    d9:87:db:f1:6c:0c:a3:e9:05:d1:20:18:49:13:e6:
    09:ad:16:ad:c8:82:f6:87:26:26:cd:ff:9e:05:3f:
    8b:19:56:9e:7a:53:89:b4:62:28:16:86:30:86:8b:
    82:56:11:86:f6:ef:89:b5:28:f0:f1:f8:c2:82:22:
    75:38:68:d9:ee:ac:c0:5d:9a:a8:82:e4:e9:81:49:
    0e:9c:57:14:21:5d:6d:73:38:0a:6d:8c:a9:b0:f8:
    56:d9:a1:09:8a:9b:be:e8:b2:11:47:22:f3:76:2b:
    44:fd:23:24:63:6d:d0:61
prime1:
    00:f2:66:07:62:f0:99:45:40:51:fc:d5:04:47:8c:
    b3:79:ee:fe:5b:1d:06:a6:bf:b4:1b:ef:b8:65:6d:
    d6:e0:d7:04:d8:3b:e1:fc:b6:c3:81:d7:cd:53:10:
    70:3b:b8:df:21:c6:4c:f2:61:9c:be:82:84:71:9f:
    48:6f:25:c4:17
prime2:
    00:d8:95:a1:09:be:e8:1b:57:9a:97:56:94:76:0a:
    1f:c6:19:56:9e:4a:05:79:61:da:cb:6c:e0:0d:64:
    aa:98:62:b2:0e:dd:02:6f:58:ea:5d:37:4d:f5:d7:
    21:9e:a7:e9:b5:63:76:fd:03:cb:0e:03:3e:ff:d8:
    37:f7:fc:ef:eb
exponent1:
    4c:2d:6c:5c:d7:a8:13:6f:67:54:c1:cd:1a:7f:49:
    58:24:97:a3:22:b8:18:67:5e:be:f8:d3:46:6d:02:
    7e:9e:94:53:30:5e:91:01:0e:e8:1b:5d:d7:a0:ee:
    e4:95:f0:e9:b8:5c:2e:40:26:f6:68:26:1f:5f:23:
    70:6a:b9:f9
exponent2:
    00:b9:eb:68:3b:f7:77:43:d1:65:d1:37:ef:65:8c:
    b7:4d:d4:7e:78:8e:ce:38:2e:7a:25:5a:26:d3:18:
    71:21:32:c7:ea:de:69:b2:af:bf:2f:99:c4:00:9f:
    56:2a:dd:b5:4f:92:3c:98:61:63:33:7a:31:e0:69:
    f9:51:95:fd:55
coefficient:
    15:f3:d0:bc:8c:d3:ca:e8:5a:2e:83:89:a4:70:24:
    86:d8:c7:e5:82:28:43:86:5d:0f:50:b1:93:43:fa:
    c1:ab:a4:ea:6e:82:bd:e6:71:48:1e:e5:d7:19:18:
    ab:6c:39:4c:76:c5:15:66:83:cf:27:49:63:10:56:
    44:e5:b2:34

# 檢視私鑰內容
❯ cat rsa-public-key.pem 
-----BEGIN PUBLIC KEY-----
MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDNE8QZLJZXREOeWZ2ilAzGC4Kj
q/PfsFzrXGj8g3IaS4/J3JrBo3qEq/k9XoRzOmNPyvWCj2FAY7A099d7qX4ztthB
pUM2ePDIYDvhL0EpfQqbhe+QaagcFpuKTshGR2wBjH0Cl1/WxJkfIUMmWYU+m4iK
Lw9KfLX6BjmSgWB6HQIDAQAB
-----END PUBLIC KEY-----

# 檢視公鑰的引數
❯ openssl rsa -noout -text -pubin -in rsa-public-key.pem
Public-Key: (1024 bit)
Modulus:
    00:cd:13:c4:19:2c:96:57:44:43:9e:59:9d:a2:94:
    0c:c6:0b:82:a3:ab:f3:df:b0:5c:eb:5c:68:fc:83:
    72:1a:4b:8f:c9:dc:9a:c1:a3:7a:84:ab:f9:3d:5e:
    84:73:3a:63:4f:ca:f5:82:8f:61:40:63:b0:34:f7:
    d7:7b:a9:7e:33:b6:d8:41:a5:43:36:78:f0:c8:60:
    3b:e1:2f:41:29:7d:0a:9b:85:ef:90:69:a8:1c:16:
    9b:8a:4e:c8:46:47:6c:01:8c:7d:02:97:5f:d6:c4:
    99:1f:21:43:26:59:85:3e:9b:88:8a:2f:0f:4a:7c:
    b5:fa:06:39:92:81:60:7a:1d
Exponent: 65537 (0x10001)

RSA 描述的私鑰的結構如下(其中除 \(n, d\) 之外的都是冗餘資訊):

  • modulus: 模數 \(n\)
  • publicExponent: 公指數 \(e\),固定為 65537 (0x10001)
  • privateExponent: 私鑰指數 \(d\)
  • prime1: 質數 p,用於計算 \(n\)
  • prime2: 質數 q,用於計算 \(n\)
  • exponent1: 用於加速 RSA 運算的中國剩餘定理指數一,\(d \mod (p-1)\)
  • exponent2: 用於加速 RSA 運算的中國剩餘定理指數二,\(d \mod (q-1)\)
  • coefficient: 用於加速 RSA 運算的中國剩餘定理係數,\(q^{-1} \mod p\)

再看下 RSA 公鑰的結構:

  • modulus: 模數 \(n\)
  • exponent: 公指數 \(e\),固定為 65537 (0x10001)

可以看到私鑰檔案中就已經包含了公鑰的所有引數,實際上我們也是使用 openssl rsa -in rsa-private-key.pem -pubout -out rsa-public-key.pem 命令通過私鑰生成出的對應的公鑰檔案。

下面就介紹下具體的金鑰對生成流程,搞清楚 openssl 生成出的這個私鑰,各項引數分別是什麼含義:

這裡不會詳細介紹其中的各種數學證明,具體的請參考維基百科。

  • 隨機選擇兩個不相等的質數 \(p\)\(q\)
    • p 跟 q 應該非常大,但是長度相差幾個整數,這樣會使得破解更加困難
  • 計算出模數 \(n = pq\)
  • 計算尤拉函式的值 \(\phi(n) = \phi(pq) = (p-1)(q-1)\)
  • 選擇公指數 \(e\),要求 \(1 < e < \lambda (n)\),且 \(e\)\(\phi(n)\) 互質,即 \(\gcd(e, \phi(n)) = 1\)
    • 目前 openssl 預設使用 65537 (0x10001)
    • 曾經也有使用過 3 作為 e 的值,但是目前 3 已被證明不夠安全
  • 計算出使等式 \(ed \equiv 1 \bmod \phi(n)\) 成立的值 \(d\),它就是我們的私鑰指數
    • 上述等式的含義:\(ed\)\(\phi(n)\) 的餘數為 \(1\)
    • 等式可轉換為 \(ed = 1 + \phi(n) \cdot k\),其中 \(k\) 為整數。
    • 移項得 \(e d + \phi(n) \cdot y = 1 = \gcd(e, \phi(n))\),其中 \(y=-k\)
    • 上面的等式可使用擴充歐幾里得演算法求解,wiki 有給出此演算法的 Python 實現,非常簡潔。
  • 使用 \((n, e)\) 組成公鑰,使用 \((n, d)\) 組成私鑰。其他引數可以儲存在私鑰中,也可丟棄。
    • \(p, q, \phi(n), d\) 四個引數都必須保密,絕不能洩漏!
  • 在現有算力下,想要通過公鑰的 \((n, e)\) 推算出 \(d\) 是非常困難的,這保證了 RSA 演算法的安全性。

下面我們使用 Python 來通過 \(p,q,e\) 計算出 \(n, d\) 來,跟 openssl 列印的對比下,看看是否一致。

# pip install cryptography==36.0.1
from pathlib import Path
from cryptography.hazmat.primitives import serialization

key_path = Path("./rsa-private-key.pem")
private_key = serialization.load_pem_private_key(
    key_path.read_bytes(),
    password=None,
)
private = private_key.private_numbers()
public = private_key.public_key().public_numbers()
p = private.p
q = private.q
e = public.e
phi_n = (p-1) * (q-1)

def extended_euclidean(a, b):
    """
      擴充歐幾里得演算法,能在計算出 a 與 b 的最大公約數的同時,給出 ax + by = gcd(a, b) 中的 x 與 y 的值
      程式碼來自 wiki: https://zh.wikipedia.org/wiki/%E6%89%A9%E5%B1%95%E6%AC%A7%E5%87%A0%E9%87%8C%E5%BE%97%E7%AE%97%E6%B3%95
    """
    old_s, s = 1, 0
    old_t, t = 0, 1
    old_r, r = a, b
    if b == 0:
        return 1, 0, a
    else:
        while(r!=0):
            q = old_r // r
            old_r, r = r, old_r-q*r
            old_s, s = s, old_s-q*s
            old_t, t = t, old_t-q*t
    return old_s, old_t, old_r

# 我們只需要 d,y 可忽略,而餘數 remainder 肯定為 1,也可忽略
d, y, remainder = extended_euclidean(e, phi_n)

n = p * q
print(f"{hex(n)=}")
# => hex(n)='0xcd13c4192c965744439e599da2940cc60b82a3abf3dfb05ceb5c68fc83721a4b8fc9dc9ac1a37a84abf93d5e84733a634fcaf5828f614063b034f7d77ba97e33b6d841a5433678f0c8603be12f41297d0a9b85ef9069a81c169b8a4ec846476c018c7d02975fd6c4991f21432659853e9b888a2f0f4a7cb5fa06399281607a1d'
print(f"{hex(d)=}")
# => hex(d)='0xdbe4d5e0286f0c23a65da4b9e219dd987dbf16c0ca3e905d120184913e609ad16adc882f6872626cdff9e053f8b19569e7a5389b46228168630868b82561186f6ef89b528f0f1f8c28222753868d9eeacc05d9aa882e4e981490e9c5714215d6d73380a6d8ca9b0f856d9a1098a9bbee8b2114722f3762b44fd2324636dd061'

對比 RSA 的輸出,可以發現去掉冒號後,dn 的值是完全相同的。

RSA 加密與解密

RSA 加密演算法,一次只能加密一個小於 \(n\) 的非負整數,假設明文為整數 \(msg\),加密演算法如下:

\[\text{encryptedMsg} = msg^e \mod n \]

通常的手段是,先使用 EAOP 將被加密訊息編碼成一個個符合條件的整數,再使用上述公式一個個加密。

解密的方法,就是對被每一段加密的資料 \(encryptedMsg\),進行如下運算:

\[\text{decryptedMsg} = \text{encryptedMsg}^d \mod n \]

解密運算的證明如下(證明需要用到 \(0 \le msg \lt n\)):

\[\begin{alignedat}{2} \text{decryptedMsg} &= &\text{encryptedMsg}^d &\mod n \\\\ &= &{(msg^e \mod n)}^d &\mod n \\\\ &= &{msg^e}^d &\mod n \\\\ &= &msg &\mod n \\\\ &= &msg \end{alignedat} \]

因為非對稱加解密非常慢,對於較大的檔案,通常會使用非對稱加密來加密資料,RSA 只被用於加密「對稱加密」的金鑰。

下面我們用 Python 來驗證下上述加解密流程:

# pip install cryptography==36.0.1
from pathlib import Path
from cryptography.hazmat.primitives import serialization

# 私鑰
key_path = Path("./rsa-private-key.pem")
private_key = serialization.load_pem_private_key(
    key_path.read_bytes(),
    password=None,
)
private = private_key.private_numbers()
public = private_key.public_key().public_numbers()
d = private.d

# 公鑰
n = public.n
e = public.e

def int_to_bytes(x: int) -> bytes:
    return x.to_bytes((x.bit_length() + 7) // 8, 'big')

def int_from_bytes(xbytes: bytes) -> int:
    return int.from_bytes(xbytes, 'big')

def fast_power_modular(b: int, p: int, m: int):
    """
    快速模冪運算 b^p % m
    Complexity O(log p)
    因為 RSA 的底數跟指數都非常大,如果先進行冪運算,最後再取模,計算結果會越來越大,導致速度非常非常慢
    邊進行冪運算,邊取模,可以極大地提升計算速度
    """
    res = 1
    while p:
        if p & 0x1:
          if p & 0x1: res *= b
        b = b ** 2 % m
        p >>= 1
    return res % m

# 明文
original_msg = b"an example"
print(f"{original_msg=}")

# 加密
msg_int = int_from_bytes(original_msg)
encrypt_int = msg_int ** e % n
encrypt_msg = int_to_bytes(encrypt_int)
print(f"{encrypt_msg=}")

# 解密
# decrypt_int = encrypt_int ** d % n # 因為 d 非常大,直接使用公式計算會非常非常慢,所以不能這麼算
decrypt_int = fast_power_modular(encrypt_int, d, n)
decrypt_msg = int_to_bytes(decrypt_int)
print(f"{decrypt_msg=}")  # 應該與原資訊完全一致

RSA 數字簽名

前面證明了可以使用公鑰加密,再使用私鑰解密。

實際上從上面的證明也可以看出來,順序是完全可逆的,先使用私鑰加密,再使用公鑰解密也完全是可行的。這種運算被我們用在數字簽名演算法中。

數字簽名的方法為:

  • 首先計算原始資料的 Hash 值,比如 SHA256
  • 使用私鑰對計算出的 Hash 值進行加密,得到數字簽名
  • 其他人使用公開的公鑰進行解密出 Hash 值,再對原始資料計算 Hash 值對比,如果一致,就說明資料未被篡改

Python 演示:

# pip install cryptography==36.0.1
from hashlib import sha512
from pathlib import Path
from cryptography.hazmat.primitives import serialization

key_path = Path("./rsa-private-key.pem")
private_key = serialization.load_pem_private_key(
    key_path.read_bytes(),
    password=None,
)
private = private_key.private_numbers()
public = private_key.public_key().public_numbers()
d = private.d
n = public.n
e = public.e

# RSA sign the message
msg = b'A message for signing'
hash = int.from_bytes(sha512(msg).digest(), byteorder='big')
signature = pow(hash, d, n)
print("Signature:", hex(signature))

# RSA verify signature
msg = b'A message for signing'
hash = int.from_bytes(sha512(msg).digest(), byteorder='big')
hashFromSignature = pow(signature, e, n)
print("Signature valid:", hash == hashFromSignature)

四、ECC 密碼系統

ECC 橢圓曲線密碼學,於 1985 年被首次提出,並於 2004 年開始被廣泛應用。
ECC 被認為是 RSA 的繼任者,新一代的非對稱加密演算法。

其最大的特點在於相同密碼強度下,ECC 的金鑰和簽名的大小都要顯著低於 RSA. 256bits 的 ECC 金鑰,安全性與 3072bits 的 RSA 金鑰安全性相當。

其次 ECC 的金鑰對生成、金鑰交換與簽名演算法的速度都要比 RSA 快。

橢圓曲線的數學原理簡介

在數學中,橢圓曲線(Elliptic Curves)是一種平面曲線,由如下方程定義的點的集合組成(\(A-J\) 均為常數):

\[Ax^3 + Bx^2y + Cxy^2 + Dy^3 + Ex^2 + Fxy + Gy^2 + Hx + Iy + J = 0 \]

而 ECC 只使用了其中很簡單的一個子集(\(a, b\) 均為常數):

\[y^2 = x^3 + ax + b \]

比如著名的 NIST 曲線 secp256k1 就是基於如下橢圓曲線方程:

\[y^2 = x^3 + 7 \]

橢圓曲線大概長這麼個形狀:

橢圓曲線跟橢圓的關係,就猶如雷鋒跟雷峰塔、Java 跟 JavaScript...

你可以通過如下網站手動調整 \(a\)\(b\) 的值,拖動曲線的交點:
https://www.desmos.com/calculator/ialhd71we3?lang=zh-CN

橢圓曲線上的運算

數學家在橢圓曲線上定義了一些運算規則,ECC 就依賴於這些規則,下面簡單介紹下我們用得到的部分。

1. 加法與負元

對於曲線上的任意兩點 \(A\)\(B\),我們定義過 \(A, B\) 的直線與曲線的交點為 \(-(A+B)\),而 \(-(A+B)\) 相對於 x 軸的對稱點即為 \(A+B\):

上述描述一是定義了橢圓曲線的加法規則,二是定義了橢圓曲線上的負元運算。

2. 二倍運算

在加法規則中,如果 \(A=B\),我們定義曲線在 \(A\) 點的切線與曲線的交點為 \(-2A\),於是得到二倍運算的規則:

3. 無窮遠點

對於 \((-A) + A\) 這種一個值與其負元本身相加的情況,我們會發現過這兩點的直線沒有交點,前面定義的加法規則在這種情況下失效。
為了解決這個問題,我們假設這直線與橢圓曲線相交於無窮遠點 \(O_{\infty}\).

4. k 倍運算

我們在前面已經定義了橢圓曲線上的加法運算二倍運算以及無窮遠點,有了這三個概念,我們就能定義k 倍運算 了。

K 倍運算最簡單的計算方法,就是不斷地進行加法運算,但是也有許多更高效的演算法。
其中最簡單的演算法是「Double-and-add」,它要求首先 \(k\) 拆分成如下形式

\[k = k_{0}+2k_{1}+2^{2}k_{2}+\cdots +2^{m}k_{m} \\\\ \text{其中} k_{0}~..~k_{m}\in \{0,1\},m=\lfloor \log _{2}{k}\rfloor \]

然後再迭代計算其中各項的值,它的運算複雜度為 \(log_{2}(k)\).

因 Double 和 add 的執行時間不同,根據執行時間就可以知道是執行 Double 還是 add,間接可以推算出 k. 因此這個演算法會有計時攻擊的風險。
基於「Double-and-add」修改的蒙哥馬利階梯(Montgomery Ladder)是可以避免計時分析的作法,這裡就不詳細介紹了。

5. 有限域上的橢圓曲線

橢圓曲線是連續且無限的,而計算機卻更擅長處理離散的、存在上限的整數,因此 ECC 使用「有限域上的橢圓曲線」進行計算。

「有限域(也被稱作 Galois Filed, 縮寫為 GF)」顧名思義,就是指只有有限個數值的域。

有限域上的橢圓曲線方程,通過取模的方式將曲線上的所有值都對映到同一個有限域內。
有限域 \(\mathbb {F} _{p}\) 上的 EC 橢圓曲線方程為:

\[y^2 = x^3 + ax + b (\mod p), 0 \le x \le p \]

目前主要有兩種有限域在 ECC 中被廣泛應用:

  • 以素數為模的整數域: \(\mathbb {F} _{p}\)
    • 在通用處理器上計算很快
  • 以 2 的冪為模的整數域: \(\mathbb {F} _{2^{m}}\)
    • 當使用專用硬體時,計算速度很快

通過限制 x 為整數,並使用取模進行了對映後,橢圓曲線的形狀已經面目全非了,它的加減法也不再具有幾何意義。
但是它的一些特性仍然跟橢圓曲線很類似,各種公式基本加個 \(\mod p\) 就變成了它的有限域版本:

  • 無窮遠點 \(O_{\infty}\) 是零元,\(O_{\infty} + O_{\infty} = O_{\infty}\)\(O_{\infty} + P = P\)
  • \(P_{x, y}\) 的負元為 \(P_{x, -y}\),,並且有 \(P + (-P) = O_{\infty}\)
  • \(P * 0 = O_{\infty}\)
  • 如果 \(P_{x1, y1} + Q_{x2, y2} = R_{x3, y3}\),則其座標有如下關係
    • \(x3 = (k^2 - x1 - x2) \mod p\)
    • \(y3 = (k(x1 - x3) - y1) \mod p\)
    • 斜率 \(k\) 的計算
      • 如果 \(P=Q\),則 \(k=\dfrac {3x^{2}+a} {2y_{1}}\)
      • 否則 $k=\dfrac {y_{2}-y_{1}} {x_{2}-x_{1}} $

ECDLP 橢圓曲線離散對數問題

前面已經介紹了橢圓曲線上的 k 倍運算 及相關的高效演算法,但是我們還沒有涉及到除法。

橢圓曲線上的除法是一個尚未被解決的難題——「ECDLP 橢圓曲線離散對數問題」:

已知 \(kG\) 與基點 \(G\),求整數 \(k\) 的值。

目前並沒有有效的手段可以快速計算出 \(k\) 的值。
比較直觀的方法應該是從基點 \(G\) 開始不斷進行加法運算,直到結果與 \(kG\) 相等。

目前已知的 ECDLP 最快的解法所需步驟為 \(\sqrt{k}\),而 k 倍運算高效演算法前面已經介紹過了,所需步驟為 \(log_2(k)\)
\(k\) 非常大的情況下,它們的計算用時將會有指數級的差距。

橢圓曲線上的 k 倍運算與素數上的冪運算很類似,因此 ECC 底層的數學難題 ECDLP 與 RSA 的離散對數問題 DLP 也有很大相似性。

ECC 金鑰對生成

首先,跟 RSA 一樣,讓我們先看下怎麼使用 openssl 生成一個使用 prime256v1 曲線的 ECC 金鑰對:

# 列出 openssl 支援的所有曲線名稱
openssl ecparam -list_curves

# 生成 ec 演算法的私鑰,使用 prime256v1 演算法,金鑰長度 256 位。(強度大於 2048 位的 RSA 金鑰)
openssl ecparam -genkey -name prime256v1 -out ecc-private-key.pem
# 通過金鑰生成公鑰
openssl ec -in ecc-private-key.pem -pubout -out ecc-public-key.pem

# 檢視私鑰內容
❯ cat ecc-private-key.pem
-----BEGIN EC PARAMETERS-----
BggqhkjOPQMBBw==
-----END EC PARAMETERS-----
-----BEGIN EC PRIVATE KEY-----
MHcCAQEEIGm3wT/m4gDaoJGKfAHDXV2BVtdyb/aPTITJR5B6KVEtoAoGCCqGSM49
AwEHoUQDQgAE5IEIorw0WU5+om/UgfyYSKosiGO6Hpe8hxkqL5GUVPyu4LJkfw/e
99zhNJatliZ1Az/yCKww5KrXC8bQ9wGQvw==
-----END EC PRIVATE KEY-----

# 檢視私鑰的詳細引數
❯ openssl ec -noout -text -in ecc-private-key.pem
read EC key
Private-Key: (256 bit)
priv:
    69:b7:c1:3f:e6:e2:00:da:a0:91:8a:7c:01:c3:5d:
    5d:81:56:d7:72:6f:f6:8f:4c:84:c9:47:90:7a:29:
    51:2d
pub: 
    04:e4:81:08:a2:bc:34:59:4e:7e:a2:6f:d4:81:fc:
    98:48:aa:2c:88:63:ba:1e:97:bc:87:19:2a:2f:91:
    94:54:fc:ae:e0:b2:64:7f:0f:de:f7:dc:e1:34:96:
    ad:96:26:75:03:3f:f2:08:ac:30:e4:aa:d7:0b:c6:
    d0:f7:01:90:bf
ASN1 OID: prime256v1
NIST CURVE: P-256

# 檢視公鑰內容
❯ cat ecc-public-key.pem 
-----BEGIN PUBLIC KEY-----
MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAE5IEIorw0WU5+om/UgfyYSKosiGO6
Hpe8hxkqL5GUVPyu4LJkfw/e99zhNJatliZ1Az/yCKww5KrXC8bQ9wGQvw==
-----END PUBLIC KEY-----

# 檢視公鑰的引數
❯ openssl ec -noout -text -pubin -in ecc-public-key.pem
read EC key
Private-Key: (256 bit)
pub: 
    04:e4:81:08:a2:bc:34:59:4e:7e:a2:6f:d4:81:fc:
    98:48:aa:2c:88:63:ba:1e:97:bc:87:19:2a:2f:91:
    94:54:fc:ae:e0:b2:64:7f:0f:de:f7:dc:e1:34:96:
    ad:96:26:75:03:3f:f2:08:ac:30:e4:aa:d7:0b:c6:
    d0:f7:01:90:bf
ASN1 OID: prime256v1
NIST CURVE: P-256

可以看到 ECC 演算法的公鑰私鑰都比 RSA 小了非常多,資料量小,卻能帶來同等的安全強度,這是 ECC 相比 RSA 最大的優勢。

私鑰的引數:

  • priv: 私鑰,一個 256bits 的大整數,對應我們前面介紹的 \(k 倍運算\)中的 \(k\)
  • pub: 公鑰,是一個橢圓曲線(EC)上的座標 \({x, y}\),也就是我們 well-known 的基點 \(G\)
  • ASN1 OID: prime256v1, 橢圓曲線的名稱
  • NIST CURVE: P-256

使用安全隨機數生成器即可直接生成出 ECC 的私鑰 priv,因此 ECC 的金鑰對生成速度非常快。

ECDH 金鑰交換

這個在前面寫給開發人員的實用密碼學(五)—— 金鑰交換 DHKE 與完美前向保密 PFS已經介紹過了,不過這裡再複述一遍:

  • Alice 跟 Bob 協商好橢圓曲線的各項引數,以及基點 G,這些引數都是公開的。
  • Alice 生成一個隨機的 ECC 金鑰對(公鑰:\(alicePrivate * G\), 私鑰: \(alicePrivate\)
  • Bob 生成一個隨機的 ECC 金鑰對(公鑰:\(bobPrivate * G\), 私鑰: \(bobPrivate\)
  • 兩人通過不安全的通道交換公鑰
  • Alice 將 Bob 的公鑰乘上自己的私鑰,得到共享金鑰 \(sharedKey = (bobPrivate * G) * alicePrivate\)
  • Bob 將 Alice 的公鑰乘上自己的私鑰,得到共享金鑰 \(sharedKey = (alicePrivate * G) * bobPrivate\)
  • 因為 \((a * G) * b = (b * G) * a\),Alice 與 Bob 計算出的共享金鑰應該是相等的

這樣兩方就通過 ECDH 完成了金鑰交換。
而 ECDH 的安全性,則由 ECDLP 問題提供保證。

ECC 加密與解密

ECC 本身並沒有提供加密與解密的功能,但是我們可以藉助 ECDH 迂迴實現加解密。流程如下:

  • Bob 想要將訊息 M 安全地傳送給 Alice,他手上已經擁有了 Alice 的 ECC 公鑰 alicePubKey
  • Bob 首先使用如下演算法生成出「共享金鑰」+「密文公鑰」
    • 隨機生成一個臨時 ECC 金鑰對
      • 私鑰:安全隨機數 ciphertextPrivKey
      • 公鑰:ciphertextPubKey = ciphertextPrivKey * G
    • 使用 ECDH 計算出共享金鑰:\(sharedECCKey = alicePubKey * ciphertextPrivKey\)
  • Bob 使用「共享金鑰」與對稱加密演算法加密訊息,得到密文 C
    • 比如使用 AES-256-GCM 或者 ChaCha20-Poly1305 進行對稱加密
  • Bob 將 C + ciphertextPubKey 打包傳輸給 Alice
  • Alice 使用 ciphertextPubKey 與自己的私鑰計算出共享金鑰 sharedECCKey = ciphertextPubKey * alicePrivKey
  • Alice 使用計算出的共享金鑰解密 C 得到訊息 M

實際上就是訊息的傳送方先生成一個臨時的 ECC 金鑰對,然後藉助 ECDH 協議計算出共享金鑰用於加密。
訊息的接收方同樣通過 ECDH 協議計算出共享金鑰再解密資料。

使用 Python 演示如下:

# pip install tinyec  # <= ECC 曲線庫
from tinyec import registry
import secrets

# 使用這條曲線進行演示
curve = registry.get_curve('brainpoolP256r1')

def compress_point(point):
    return hex(point.x) + hex(point.y % 2)[2:]

def ecc_calc_encryption_keys(pubKey):
    """
    安全地生成一個隨機 ECC 金鑰對,然後按 ECDH 流程計算出共享金鑰 sharedECCKey
    最後返回(共享金鑰, 臨時 ECC 公鑰 ciphertextPubKey)
    """
    ciphertextPrivKey = secrets.randbelow(curve.field.n)
    ciphertextPubKey = ciphertextPrivKey * curve.g
    sharedECCKey = pubKey * ciphertextPrivKey
    return (sharedECCKey, ciphertextPubKey)

def ecc_calc_decryption_key(privKey, ciphertextPubKey):
    sharedECCKey = ciphertextPubKey * privKey
    return sharedECCKey

# 1. 首先生成出 Alice 的 ECC 金鑰對
privKey = secrets.randbelow(curve.field.n)
pubKey = privKey * curve.g
print("private key:", hex(privKey))
print("public key:", compress_point(pubKey))

# 2. Alice 將公鑰傳送給 Bob

# 3. Bob 使用 Alice 的公鑰生成出(共享金鑰, 臨時 ECC 公鑰 ciphertextPubKey)
(encryptKey, ciphertextPubKey) = ecc_calc_encryption_keys(pubKey)
print("ciphertext pubKey:", compress_point(ciphertextPubKey))
print("encryption key:", compress_point(encryptKey))

# 4. Bob 使用共享金鑰 encryptKey 加密資料,然後將密文與 ciphertextPubKey 一起傳送給 Alice

# 5. Alice 使用自己的私鑰 + ciphertextPubKey 計算出共享金鑰 decryptKey
decryptKey = ecc_calc_decryption_key(privKey, ciphertextPubKey)
print("decryption key:", compress_point(decryptKey))

# 6. Alice 使用 decryptKey 解密密文得到原始訊息

ECC 數字簽名

前面已經介紹了 RSA 簽名,這裡介紹下基於 ECC 的簽名演算法。

基於 ECC 的簽名演算法主要有兩種:ECDSA 與 EdDSA,以及 EdDSA 的變體。
其中 ECDSA 演算法稍微有點複雜,而安全強度跟它基本一致的 EdDSA 的演算法更簡潔更易於理解,在使用特定曲線的情況下 EdDSA 還要比 ECDSA 更快一點,因此現在通常更推薦使用 EdDSA 演算法。

EdDSA 與 Ed25519 簽名演算法

EdDSA(Edwards-curve Digital Signature Algorithm)是一種現代的安全數字簽名演算法,它使用專為效能優化的橢圓曲線,如 255bits 曲線 edwards25519 和 448bits 曲線 edwards448.

EdDSA 簽名演算法及其變體 Ed25519 和 Ed448 在技術上在 RFC8032 中進行了描述。

首先,使用者需要基於 edwards25519 或者 edwards448 曲線,生成一個 ECC 金鑰對。
生成私鑰的時候,演算法首先生成一個隨機數,然後會對隨機數做一些變換以確保安全性,防範計時攻擊等攻擊手段。
對於 edwards25519 公私鑰都是 32 位元組,而對於 edwards448 公私鑰都是 57 位元組。

對於 edwards25519 輸出的簽名長度為 64 位元組,而對於 Ed448 輸出為 114 位元組。

具體的演算法雖然比 ECDSA 簡單,但還是有點難度的,這裡就直接略過了。

下面給出個 ed25519 的計算示例:

# pip install cryptography==36.0.1
from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PrivateKey

# 也可用 openssl 生成,都沒啥毛病
private_key = Ed25519PrivateKey.generate()

# 簽名
signature = private_key.sign(b"my authenticated message")

# 顯然 ECC 的公鑰 kG 也能直接從私鑰 k 生成
public_key = private_key.public_key()
# 驗證
# Raises InvalidSignature if verification fails
public_key.verify(signature, b"my authenticated message")

ed448 的程式碼也完全類似:

# pip install cryptography==36.0.1
from cryptography.hazmat.primitives.asymmetric.ed448 import Ed448PrivateKey

private_key = Ed448PrivateKey.generate()
signature = private_key.sign(b"my authenticated message")
public_key = private_key.public_key()
# Raises InvalidSignature if verification fails
public_key.verify(signature, b"my authenticated message")

密碼學常用橢圓曲線介紹

在介紹密碼學中的常用橢圓曲線前,需要先介紹一下橢圓曲線的(order)以及輔助因子(cofactor)這兩個概念。

首先還得介紹下數學中「迴圈群」的概念,它是指能由單個元素所生成的群,在 ECC 中這就是預先定義好的基點 \(G\).

一個有限域上的橢圓曲線可以形成一個有限「迴圈代數群」,它由曲線上的所有點組成。橢圓曲線的被定義為該曲線上所有點的個數(包括無窮遠點)。

「有些曲線 + G」形成一個單一迴圈群,這一個群包含了曲線上的所有點。而其他的曲線加上 G 點則形成多個不相交的迴圈子群,每個子群包含了曲線的一個子集。
對於上述第二種情況,曲線上的點將被拆分到 h 個迴圈子群中,每個子群的都是 r,這時整個群的階 \(n = h * r\). 子群的個數 h 被稱為輔助因子

有限域上的橢圓曲線的階都是有限的,也就是說對於曲線上任意一點 \(G\),我們計算它的數乘 \(kG\),隨著整數 \(k\) 的增大,一定會存在某個 \(k\) 使 \(kG = O_{\infty}\) 成立,然後 \(k\) 繼續增大時,因為 $O_{\infty} * P = \(O_{\infty}\)\(kG\) 的值就固定為 $$O_{\infty}$ 了,更大的 \(k\) 值已經失去了意義。

因此 ECC 中要求 \(kG\) 中的私鑰 \(k\) 符合條件 \(0 \le k \le r\),也就是說總的私鑰數量是受 \(r\) 限制的。

輔助因子通過用如下公式表示:

\[h = n / r \]

其中 \(n\) 是曲線的階,\(r\) 是每個子群的階,\(h\) 是輔助因子。
如果曲線形成了一個單一迴圈群,那顯然 \(h = 1\),否則 \(h > 1\)

舉例如下:

  • secp256k1 的輔助因子為 1
  • Curve25519 的輔助因子為 8
  • Curve448 的輔助因子為 4

生成點 G

生成點 G 的選擇是很有講究的,雖然每個迴圈子群都包含有很多個生成點,但是 ECC 只會謹慎的選擇其中一個。
首先 G 點必須要能生成出整個迴圈子群,其次還需要有儘可能高的計算效能。

數學上已知某些橢圓曲線上,不同的生成點生成出的迴圈子群,階也是不同的。如果 G 點選得不好,可能會導致生成出的子群的階較小。
前面我們已經提過子群的階 \(r\) 會限制總的私鑰數量,導致演算法強度變弱!因此不恰當的 \(G\) 點可能會導致我們遭受「小子群攻擊」。
為了避免這種風險,建議儘量使用被廣泛使用的加密庫,而不是自己擼一個。

橢圓曲線的域引數

ECC橢圓曲線由一組橢圓曲線域引數描述,如曲線方程引數、場引數和生成點座標。這些引數在各種密碼學標準中指定,你可以網上搜到相應的 RFC 或 NIST 文件。

這些標準定義了一組命名曲線的引數,例如 secp256k1、P-521、brainpoolP512t1 和 SM2. 這些加密標準中描述的有限域上的橢圓曲線得到了密碼學家的充分研究和分析,並被認為具有一定的安全強度。

也有一些密碼學家(如 Daniel Bernstein)認為,官方密碼標準中描述的大多數曲線都是「不安全的」,並定義了他們自己的密碼標準,這些標準在更廣泛的層面上考慮了 ECC 安全性。

開發人員應該僅使用各項標準文件給出的、經過密碼學家充分研究的命名曲線。

secp256k1

此曲線被應用在比特幣中,它的域引數如下:

  • p (modulus) = 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEFFFFFC2F
  • n (order; size; the count of all possible EC points) = 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEBAAEDCE6AF48A03BBFD25E8CD0364141
  • a (方程 \(y^2 ≡ x^3 + a\*x + b \(\mod p\)\) 中的常數) = 0x0000000000000000000000000000000000000000000000000000000000000000
  • b (方程 \(y^2 ≡ x^3 + a\*x + b \(\mod p\)\) 中的常數)= 0x0000000000000000000000000000000000000000000000000000000000000007
  • g (the curve generator point G {x, y}) = (0x79BE667EF9DCBBAC55A06295CE870B07029BFCDB2DCE28D959F2815B16F81798, 0x483ada7726a3c4655da4fbfc0e1108a8fd17b448a68554199c47d08ffb10d4b8)
  • h (cofactor, typically 1) = 01
Edwards 曲線

橢圓曲線方程除了我們前面使用的 Weierstrass 形式 $$y^2 = (x^3 + ax + b) \mod p$$ 外,還可以被寫成其他多種形式,這些不同的形式是雙有理等價的(表示筆著也不懂什麼叫「雙有理等價」...)。
不同的方式形式在計算機的數值計算上可能會存在區別。

為了效能考慮,ECC 在部分場景下會考慮使用 Edwards 曲線形式進行計算,該方程形式如下:

\[x^{2}+y^{2}=1+dx^{2}y^{2} \]

畫個圖長這樣:

知名的 Edwards 曲線有:

  • Curve1174 (251-bit)
  • Curve25519 (255-bit)
  • Curve383187 (383-bit)
  • Curve41417 (414-bit)
  • Curve448 (448-bit)
  • E-521 (521-bit)
  • ...
Curve25519, X25519 和 Ed25519

https://cryptography.io/en/latest/hazmat/primitives/asymmetric/ed25519/

只要域引數選得好,Edwards 就可以以非常高的效能實現 ECC 金鑰交換、數字簽名、混合加密方案。

一個例子就是 Curve25519,它是 Edwards 曲線,其 Montgomery 形式的定義如下:

\[y^{2}=x^{3}+486662x^{2}+x \]

其被定義在有限域 \(\mathbb {F} _{p}\) 上,\(p = 2255 - 19\), 其他域引數如下:

  • n = 2252 + 0x14def9dea2f79cd65812631a5cf5d3ed
  • 輔因子 h = 8

雖然此曲線並未以 Edwards 形式定義,但是它已被證明與如下扭曲 Edwards 曲線(edwards25519)雙有理等價:

\[-x^2 + y^2 = 1 + 37095705934669439343138083508754565189542113879843219016388785533085940283555 x^2 y^2 \]

上面給出的這種 Edwards 形式與前文給出的 Weierstrass 形式完全等價,是專為計算速度優化而設計成這樣的。

Curve25519 由 Daniel Bernstein 領導的密碼學家團隊精心設計,在多個設計和實現層面上達成了非常高的效能,同時不影響安全性。

Curve25519 的構造使其避免了許多潛在的實現缺陷。根據設計,它不受定時攻擊的影響,並且它接受任何 32 位元組的字串作為有效的公鑰,並且不需要驗證。
它能提供 125.8bits 的安全強度(有時稱為 ~ 128bits 安全性)

Curve25519 的私鑰為 251 位,通常編碼為 256 位整數(32 個位元組,64 個十六進位制數字)。
公鑰通常也編碼為 256 位整數(255 位 y 座標 + 1 位 x 座標),這對開發人員來說非常方便。

基於 Curve25519 派生出了名為 X25519 的 ECDH 演算法,以及基於 EdDSA 的高速數字簽名演算法 Ed25519.

Curve448, X448 和 Ed448

https://cryptography.io/en/latest/hazmat/primitives/asymmetric/ed448/

Curve448(Curve448-Goldilocks)是一種非扭曲 Edwards 曲線,它的方程定義如下:

\[x^2 + y^2 = 1 - 39081 x^2 y^2 \]

其被定義在有限域 \(\mathbb {F} _{p}\) 上,\(p = 2448 - 2224 - 1\),其他域引數:

  • n = 2446 - 0x8335dc163bb124b65129c96fde933d8d723a70aadc873d6d54a7bb0d
  • 輔助因子 h = 4

與 Curve25519 一樣,Curve448 也等價於前面給出的 Weierstrass 形式,選擇 Edwards 形式主要是因為它能顯著提升效能。

Curve448 提供 222.8 位的安全強度。
Curve448 的私鑰為 446 位,通常編碼為 448 位整數(56 個位元組,112 個十六進位制數字)。
公鑰也被編碼為 448 位整數。

基於 Curve448 派生出了名為 X448 的 ECDH 演算法,以及基於 EdDSA 的高速數字簽名演算法 Ed448.

該選擇哪種橢圓曲線

首先,Bernstein 的 SafeCurves 標準列出了符合一組 ECC 安全要求的安全曲線,可訪問 https://safecurves.cr.yp.to 瞭解此標準。

此外對於我們前面介紹的 Curve448 與 Curve25519,可以從效能跟安全性方面考量:

  • 要更好的效能,可以接受弱一點的安全性:選擇 Curve25519
  • 要更好的安全性,可以接受比 Curve25519 慢 3 倍的計算速度:選擇 Curve448

如果你的應用場景中暫時還很難用上 Curve448/Curve25519,你可以考慮一些應用更廣泛的其他曲線,但是一定要遵守如下安全規範:

  • 模數 p 應該至少有 256 位
    • 比如 secp224k1 secp192k1 啥的就可以掃進歷史塵埃裡了
  • 暫時沒有想補充的,可以參考 https://safecurves.cr.yp.to

目前在 TLS 協議以及 JWT 簽名演算法中,目前應該最廣泛的橢圓曲線仍然是 NIST 系列:

  • P-256: 到目前為止 P-256 應該仍然是應用最為廣泛的橢圓曲線
    • 在 openssl 中對應的名稱為 prime256v1
  • P-384
    • 在 openssl 中對應的名稱為 secp384r1
  • P-521
    • 在 openssl 中對應的名稱為 secp521r1

但是我們也看到 Curve25519 正在越來越流行,因為美國政府有前科,NIST 標準被懷疑可能有後門,目前很多人都在推動使用 Curve25519 等社群方案取代掉 NIST 標準曲線。

對於 openssl,如下命令會列出 openssl 支援的所有曲線:

openssl ecparam -list_curves

ECIES - 整合加密方案

在文章開頭我們已經介紹了整合加密方案 (IES),它在金鑰封裝機制(KEM)的基礎上,新增了金鑰派生演算法 KDF、訊息認證演算法 MAC 等其他密碼學演算法以達成我們對訊息的安全性、真實性、完全性的需求。

而 ECIES 也完全類似,是在 ECC + 對稱加密演算法的基礎上,新增了許多其他的密碼學演算法實現的。

ECIES 是一個加密框架,而不是某種固定的演算法。它可以通過插拔不同的演算法,形成不同的實現。
比如「secp256k1 + Scrypt + AES-GCM + HMAC-SHA512」。

大概就介紹到這裡吧,後續就請在需要用到時自行探索相關的細節咯。

參考

相關文章