Key-Generation

為什麼大多數時候 X25519 私鑰根據使用的功能略有不匹配,但公鑰總是匹配(兩者的種子相同)?

  • December 24, 2021

我正試圖從 aseed到 a SigningKey,並獲得一個PrivateKey(加密密鑰)。我正在使用 NaCl / libsodium。

我創建了下面的程式碼,結果很有趣。結果pk1.private_keypk2.private_key匹配大約 3% 的時間。然而,公鑰匹配 100%,所有生成都以相同的seed. 這裡發生了什麼?

  • pk1通過使用獲得PrivateKey.from_seed(seed)
  • pk2通過使用獲得SigningKey(seed).to_curve25519_private_key()

不匹配的範例(它們接近但不相等):

# 1st byte mismatch by 0x01
seed:  f8d9e54a23971beebf2552c1a50ade6150cd051321398394f515e8d4b1ba0404
priv1: c1fd4612ee8ef24d295210a277e196e6bb4a9ae6b93f98d93f197860fe5dc048
priv2: c0fd4612ee8ef24d295210a277e196e6bb4a9ae6b93f98d93f197860fe5dc048

# 1st byte mismatch by 0x03
seed:  d612a66f92ee2f42ab1f7ea9a712a47c815843d21fc988b1d202459f235b6410
priv1: f33d5e80bb556333e2961c9868b1dc7e548836ee56808689ca022f1a19fe86bb
priv2: f03d5e80bb556333e2961c9868b1dc7e548836ee56808689ca022f1a19fe867b

# 1st byte mismatch by 0x01, last byte mismatch by 0x40
seed:  10b7e1c66cf08005a22289158a088e028160f892dc6c20d43025be4690aaed85
priv1: 194898f65d117579d50e80a9b7e07bd048bfd1300d55561dac9dfaed4ef02109
priv2: 184898f65d117579d50e80a9b7e07bd048bfd1300d55561dac9dfaed4ef02149
from nacl.signing import SigningKey
from nacl.public import PrivateKey, PublicKey, Box, SealedBox
from nacl.bindings import crypto_sign_SEEDBYTES
from nacl.utils import StringFixer, random

def run(debug=False):
   seed = random(crypto_sign_SEEDBYTES)
   pk1 = PrivateKey.from_seed(seed)
   pk2 = SigningKey(seed).to_curve25519_private_key()
   if debug:
       print('seed:  ', seed.hex()) 
       print('priv1: ', pk1._private_key.hex())
       print('priv2: ', pk2._private_key.hex())
       print('pub1:  ', bytes(pk1.public_key).hex())
       print('pub2:  ', bytes(pk2.public_key).hex())
   return seed, pk1, pk2

runs = 10000
private_key_match = 0
public_key_match = 0
both_match = 0

for i in range(runs):
   if i % 500 == 0:
       print(i, 'of', runs)
   seed, pk1, pk2 = run()
   x = pk1._private_key == pk2._private_key
   y = bytes(pk1.public_key) == bytes(pk2.public_key)
   if x:
       private_key_match += 1
   if y:
       public_key_match += 1
   if x and y:
       both_match += 1

print('private key match:', private_key_match)
print('public key match: ', public_key_match)
print('both match:       ', both_match)

在數學上,Curve25519私鑰是 $ \mathbb{F}_{2^{255}-19} $ , 即整數模 $ 2^{255}-19 $ . 這自然可以表示為之間的整數 $ 0 $ 和 $ 2^{255}-19-1 $ ,它本身可以用 255 位來表示。由於實際電腦使用 8 位字節,因此最小的實際表示是 32 字節字元串。

有一種表示 Curve25519 私鑰的標準格式,在RFC 7748 §5中有描述。這種格式編碼之間的整數 $ 0 $ 和 $ 2^{255}-19-1 $ 作為小端順序的 32 字節(256 位)字元串。

一個 32 字節的字元串可以表示範圍內的數字 $ [0,2^{256}-1] $ , 大於 $ [0,2^{255}-19-1] $ . 此外,由於各種原因,並非該範圍內的所有數字 $ [0,2^{255}-19-1] $ 很好(參見Curve25519 關鍵結構Genkin 等人的“May the Fourth Be With You: A Microarchitectural Side Channel Attack on several Real-World Applications of Curve25519”)。約束是:

  • 數字必須介於 $ 2^{254} $ 和 $ 2^{255}-19 $ ,所以 256 位數的最高有效兩位必須是 0 和 1。
  • 該數字必須是 8 的倍數,因此最低有效三位必須為 0。

RFC 7748 §5 規定,在生成私鑰時,必須採用隨機(或偽隨機,視情況而定)32 字節字元串,並強制上述五位為其強制值。Curve25519 的實現將私鑰作為 32 字節字元串形式的輸入,必須在開始對密鑰表示的數字進行計算之前應用此遮罩。

nacl.public.PrivateKey.from_seed返回原始的 32 字節字元串。nacl.signing.SigningKey(seed)執行屏蔽,因此to_curve25519_private_key以規範的屏蔽形式導出值。

遮罩涉及 5 位,所以給定一個隨機種子,有一個 $ 1/2^5 = 1/32 \approx 3% $ 有可能它已經具有這 5 位的正確值。

引用自:https://crypto.stackexchange.com/questions/97771