transcrypto 1.1.2__py3-none-any.whl → 1.2.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- transcrypto/aes.py +4 -3
- transcrypto/base.py +84 -30
- transcrypto/dsa.py +153 -19
- transcrypto/elgamal.py +224 -28
- transcrypto/rsa.py +203 -19
- transcrypto/sss.py +159 -20
- transcrypto/transcrypto.py +401 -178
- {transcrypto-1.1.2.dist-info → transcrypto-1.2.0.dist-info}/METADATA +683 -425
- transcrypto-1.2.0.dist-info/RECORD +15 -0
- transcrypto-1.1.2.dist-info/RECORD +0 -15
- {transcrypto-1.1.2.dist-info → transcrypto-1.2.0.dist-info}/WHEEL +0 -0
- {transcrypto-1.1.2.dist-info → transcrypto-1.2.0.dist-info}/licenses/LICENSE +0 -0
- {transcrypto-1.1.2.dist-info → transcrypto-1.2.0.dist-info}/top_level.txt +0 -0
transcrypto/elgamal.py
CHANGED
|
@@ -23,8 +23,7 @@ import logging
|
|
|
23
23
|
# import pdb
|
|
24
24
|
from typing import Self
|
|
25
25
|
|
|
26
|
-
from . import base
|
|
27
|
-
from . import modmath
|
|
26
|
+
from . import base, modmath, aes
|
|
28
27
|
|
|
29
28
|
__author__ = 'balparda@github.com'
|
|
30
29
|
__version__: str = base.__version__ # version comes from base!
|
|
@@ -33,14 +32,16 @@ __version_tuple__: tuple[int, ...] = base.__version_tuple__
|
|
|
33
32
|
|
|
34
33
|
_MAX_KEY_GENERATION_FAILURES = 15
|
|
35
34
|
|
|
35
|
+
# fixed prefixes: do NOT ever change! will break all encryption and signature schemes
|
|
36
|
+
_ELGAMAL_ENCRYPTION_AAD_PREFIX = b'transcrypto.ElGamal.Encryption.1.0\x00'
|
|
37
|
+
_ELGAMAL_SIGNATURE_HASH_PREFIX = b'transcrypto.ElGamal.Signature.1.0\x00'
|
|
38
|
+
|
|
36
39
|
|
|
37
40
|
@dataclasses.dataclass(kw_only=True, slots=True, frozen=True, repr=False)
|
|
38
41
|
class ElGamalSharedPublicKey(base.CryptoKey):
|
|
39
42
|
"""El-Gamal shared public key. This key can be shared by a group.
|
|
40
43
|
|
|
41
|
-
BEWARE: This is
|
|
42
|
-
These are pedagogical/raw primitives; do not use for new protocols.
|
|
43
|
-
No measures are taken here to prevent timing attacks.
|
|
44
|
+
BEWARE: This is **NOT** DSA! No measures are taken here to prevent timing attacks.
|
|
44
45
|
|
|
45
46
|
Attributes:
|
|
46
47
|
prime_modulus (int): prime modulus, ≥ 7
|
|
@@ -69,9 +70,41 @@ class ElGamalSharedPublicKey(base.CryptoKey):
|
|
|
69
70
|
string representation of ElGamalSharedPublicKey
|
|
70
71
|
"""
|
|
71
72
|
return ('ElGamalSharedPublicKey('
|
|
73
|
+
f'bits={self.prime_modulus.bit_length()}, '
|
|
72
74
|
f'prime_modulus={base.IntToEncoded(self.prime_modulus)}, '
|
|
73
75
|
f'group_base={base.IntToEncoded(self.group_base)})')
|
|
74
76
|
|
|
77
|
+
@property
|
|
78
|
+
def modulus_size(self) -> int:
|
|
79
|
+
"""Modulus size in bytes. The number of bytes used in Encrypt/Decrypt/Sign/Verify."""
|
|
80
|
+
return (self.prime_modulus.bit_length() + 7) // 8
|
|
81
|
+
|
|
82
|
+
def _DomainSeparatedHash(
|
|
83
|
+
self, message: bytes, associated_data: bytes | None, salt: bytes, /) -> int:
|
|
84
|
+
"""Compute the domain-separated hash for signing and verifying.
|
|
85
|
+
|
|
86
|
+
Args:
|
|
87
|
+
message (bytes): message to sign/verify
|
|
88
|
+
associated_data (bytes | None): optional associated data
|
|
89
|
+
salt (bytes): salt to use in the hash
|
|
90
|
+
|
|
91
|
+
Returns:
|
|
92
|
+
int: integer representation of the hash output;
|
|
93
|
+
Hash512("prefix" || len(aad) || aad || message || salt)
|
|
94
|
+
|
|
95
|
+
Raises:
|
|
96
|
+
CryptoError: hash output is out of range
|
|
97
|
+
"""
|
|
98
|
+
aad: bytes = b'' if associated_data is None else associated_data
|
|
99
|
+
la: bytes = base.IntToFixedBytes(len(aad), 8)
|
|
100
|
+
assert len(salt) == 64, 'should never happen: salt should be exactly 64 bytes'
|
|
101
|
+
y: int = base.BytesToInt(
|
|
102
|
+
base.Hash512(_ELGAMAL_SIGNATURE_HASH_PREFIX + la + aad + message + salt))
|
|
103
|
+
if not 1 < y < self.prime_modulus:
|
|
104
|
+
# will only reasonably happen if modulus is small
|
|
105
|
+
raise base.CryptoError(f'hash output {y} is out of range/invalid {self.prime_modulus}')
|
|
106
|
+
return y
|
|
107
|
+
|
|
75
108
|
@classmethod
|
|
76
109
|
def NewShared(cls, bit_length: int, /) -> Self:
|
|
77
110
|
"""Make a new shared public key of `bit_length` bits.
|
|
@@ -89,19 +122,18 @@ class ElGamalSharedPublicKey(base.CryptoKey):
|
|
|
89
122
|
if bit_length < 11:
|
|
90
123
|
raise base.InputError(f'invalid bit length: {bit_length=}')
|
|
91
124
|
# generate random prime and number, create object (should never fail)
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
125
|
+
p: int = modmath.NBitRandomPrime(bit_length)
|
|
126
|
+
g: int = 0
|
|
127
|
+
while not 2 < g < p - 1:
|
|
128
|
+
g = base.RandBits(bit_length)
|
|
129
|
+
return cls(prime_modulus=p, group_base=g)
|
|
96
130
|
|
|
97
131
|
|
|
98
132
|
@dataclasses.dataclass(kw_only=True, slots=True, frozen=True, repr=False)
|
|
99
|
-
class ElGamalPublicKey(ElGamalSharedPublicKey):
|
|
133
|
+
class ElGamalPublicKey(ElGamalSharedPublicKey, base.Encryptor, base.Verifier):
|
|
100
134
|
"""El-Gamal public key. This is an individual public key.
|
|
101
135
|
|
|
102
|
-
BEWARE: This is
|
|
103
|
-
These are pedagogical/raw primitives; do not use for new protocols.
|
|
104
|
-
No measures are taken here to prevent timing attacks.
|
|
136
|
+
BEWARE: This is **NOT** DSA! No measures are taken here to prevent timing attacks.
|
|
105
137
|
|
|
106
138
|
Attributes:
|
|
107
139
|
individual_base (int): individual encryption public base, 3 ≤ i < prime_modulus
|
|
@@ -126,7 +158,8 @@ class ElGamalPublicKey(ElGamalSharedPublicKey):
|
|
|
126
158
|
Returns:
|
|
127
159
|
string representation of ElGamalPublicKey
|
|
128
160
|
"""
|
|
129
|
-
return (
|
|
161
|
+
return ('ElGamalPublicKey('
|
|
162
|
+
f'{super(ElGamalPublicKey, self).__str__()}, ' # pylint: disable=super-with-arguments
|
|
130
163
|
f'individual_base={base.IntToEncoded(self.individual_base)})')
|
|
131
164
|
|
|
132
165
|
def _MakeEphemeralKey(self) -> tuple[int, int]:
|
|
@@ -141,14 +174,16 @@ class ElGamalPublicKey(ElGamalSharedPublicKey):
|
|
|
141
174
|
bit_length: int = self.prime_modulus.bit_length()
|
|
142
175
|
while (not 1 < ephemeral_key < p_1 or
|
|
143
176
|
ephemeral_key in (self.group_base, self.individual_base)):
|
|
144
|
-
ephemeral_key = base.RandBits(bit_length
|
|
177
|
+
ephemeral_key = base.RandBits(bit_length)
|
|
145
178
|
if base.GCD(ephemeral_key, p_1) != 1:
|
|
146
179
|
ephemeral_key = 0 # we have to try again
|
|
147
180
|
return (ephemeral_key, modmath.ModInv(ephemeral_key, p_1))
|
|
148
181
|
|
|
149
|
-
def
|
|
182
|
+
def RawEncrypt(self, message: int, /) -> tuple[int, int]:
|
|
150
183
|
"""Encrypt `message` with this public key.
|
|
151
184
|
|
|
185
|
+
BEWARE: This is raw El-Gamal, no ECIES-style KEM/DEM padding or validation! This is **NOT** DSA!
|
|
186
|
+
These are pedagogical/raw primitives; do not use for new protocols.
|
|
152
187
|
We explicitly disallow `message` to be zero.
|
|
153
188
|
|
|
154
189
|
Args:
|
|
@@ -164,17 +199,66 @@ class ElGamalPublicKey(ElGamalSharedPublicKey):
|
|
|
164
199
|
if not 0 < message < self.prime_modulus:
|
|
165
200
|
raise base.InputError(f'invalid message: {message=}')
|
|
166
201
|
# encrypt
|
|
167
|
-
|
|
168
|
-
|
|
202
|
+
a: int = 0
|
|
203
|
+
b: int = 0
|
|
169
204
|
while a < 2 or b < 2:
|
|
205
|
+
ephemeral_key: int = self._MakeEphemeralKey()[0]
|
|
170
206
|
a = modmath.ModExp(self.group_base, ephemeral_key, self.prime_modulus)
|
|
171
207
|
s: int = modmath.ModExp(self.individual_base, ephemeral_key, self.prime_modulus)
|
|
172
208
|
b = (message * s) % self.prime_modulus
|
|
173
209
|
return (a, b)
|
|
174
210
|
|
|
175
|
-
def
|
|
211
|
+
def Encrypt(self, plaintext: bytes, /, *, associated_data: bytes | None = None) -> bytes:
|
|
212
|
+
"""Encrypt `plaintext` and return `ciphertext`.
|
|
213
|
+
|
|
214
|
+
• Let k = ceil(log2(n))/8 be the modulus size in bytes.
|
|
215
|
+
• Pick random r ∈ [2, n-1]
|
|
216
|
+
• ct1, ct2 = ElGamal(r)
|
|
217
|
+
• return Padded(ct1, k) + Padded(ct2, k) +
|
|
218
|
+
AES-256-GCM(key=SHA512(r)[32:], plaintext,
|
|
219
|
+
associated_data="prefix" + len(aad) + aad +
|
|
220
|
+
Padded(ct1, k) + Padded(ct2, k))
|
|
221
|
+
|
|
222
|
+
We pick fresh random r, send ct = ElGamal(r), and derive the DEM key from r,
|
|
223
|
+
then use AES-GCM for the payload. This is the classic El-Gamal-KEM construction.
|
|
224
|
+
With AEAD as the DEM, we get strong confidentiality and ciphertext integrity
|
|
225
|
+
(CCA resistance in the ROM under standard assumptions). There are no
|
|
226
|
+
Bleichenbacher-style issue because we do not expose any padding semantics.
|
|
227
|
+
|
|
228
|
+
Args:
|
|
229
|
+
plaintext (bytes): Data to encrypt.
|
|
230
|
+
associated_data (bytes, optional): Optional AAD; must be provided again on decrypt
|
|
231
|
+
|
|
232
|
+
Returns:
|
|
233
|
+
bytes: Ciphertext; see above:
|
|
234
|
+
Padded(ct1, k) + Padded(ct2, k) + AES-256-GCM(key=SHA512(r)[32:], plaintext,
|
|
235
|
+
associated_data="prefix" + len(aad) + aad +
|
|
236
|
+
Padded(ct1, k) + Padded(ct2, k))
|
|
237
|
+
|
|
238
|
+
Raises:
|
|
239
|
+
InputError: invalid inputs
|
|
240
|
+
CryptoError: internal crypto failures
|
|
241
|
+
"""
|
|
242
|
+
# generate random r and encrypt it
|
|
243
|
+
r: int = 0
|
|
244
|
+
while not 1 < r < self.prime_modulus - 1:
|
|
245
|
+
r = base.RandBits(self.prime_modulus.bit_length())
|
|
246
|
+
k: int = self.modulus_size
|
|
247
|
+
i_ct: tuple[int, int] = self.RawEncrypt(r)
|
|
248
|
+
ct: bytes = base.IntToFixedBytes(i_ct[0], k) + base.IntToFixedBytes(i_ct[1], k)
|
|
249
|
+
assert len(ct) == 2 * k, 'should never happen: c_kem should be exactly 2k bytes'
|
|
250
|
+
# encrypt plaintext with AES-256-GCM using SHA512(r)[32:] as key; return ct || Encrypt(...)
|
|
251
|
+
ss: bytes = base.Hash512(base.IntToFixedBytes(r, k))
|
|
252
|
+
aad: bytes = b'' if associated_data is None else associated_data
|
|
253
|
+
aad_prime: bytes = (
|
|
254
|
+
_ELGAMAL_ENCRYPTION_AAD_PREFIX + base.IntToFixedBytes(len(aad), 8) + aad + ct)
|
|
255
|
+
return ct + aes.AESKey(key256=ss[32:]).Encrypt(plaintext, associated_data=aad_prime)
|
|
256
|
+
|
|
257
|
+
def RawVerify(self, message: int, signature: tuple[int, int], /) -> bool:
|
|
176
258
|
"""Verify a signature. True if OK; False if failed verification.
|
|
177
259
|
|
|
260
|
+
BEWARE: This is raw El-Gamal, no ECIES-style KEM/DEM padding or validation! This is **NOT** DSA!
|
|
261
|
+
These are pedagogical/raw primitives; do not use for new protocols.
|
|
178
262
|
We explicitly disallow `message` to be zero.
|
|
179
263
|
|
|
180
264
|
Args:
|
|
@@ -199,6 +283,42 @@ class ElGamalPublicKey(ElGamalSharedPublicKey):
|
|
|
199
283
|
c: int = modmath.ModExp(self.individual_base, signature[0], self.prime_modulus)
|
|
200
284
|
return a == (b * c) % self.prime_modulus
|
|
201
285
|
|
|
286
|
+
def Verify(
|
|
287
|
+
self, message: bytes, signature: bytes, /, *, associated_data: bytes | None = None) -> bool:
|
|
288
|
+
"""Verify a `signature` for `message`. True if OK; False if failed verification.
|
|
289
|
+
|
|
290
|
+
• Let k = ceil(log2(n))/8 be the modulus size in bytes.
|
|
291
|
+
• Split signature in 3 parts: the first 64 bytes is salt, the rest is s1 and s2
|
|
292
|
+
• y_check = ElGamal(s1, s2)
|
|
293
|
+
• return y_check == Hash512("prefix" || len(aad) || aad || message || salt)
|
|
294
|
+
• return False for any malformed signature
|
|
295
|
+
|
|
296
|
+
Args:
|
|
297
|
+
message (bytes): Data that was signed
|
|
298
|
+
signature (bytes): Signature data to verify
|
|
299
|
+
associated_data (bytes, optional): Optional AAD (must match what was used during signing)
|
|
300
|
+
|
|
301
|
+
Returns:
|
|
302
|
+
True if signature is valid, False otherwise
|
|
303
|
+
|
|
304
|
+
Raises:
|
|
305
|
+
InputError: invalid inputs
|
|
306
|
+
CryptoError: internal crypto failures, authentication failure, key mismatch, etc
|
|
307
|
+
"""
|
|
308
|
+
k: int = self.modulus_size
|
|
309
|
+
if k <= 64:
|
|
310
|
+
raise base.InputError(f'modulus too small for signing operations: {k} bytes')
|
|
311
|
+
if len(signature) != (64 + k + k):
|
|
312
|
+
logging.info(f'invalid signature length: {len(signature)} ; expected {64 + k + k}')
|
|
313
|
+
return False
|
|
314
|
+
try:
|
|
315
|
+
return self.RawVerify(
|
|
316
|
+
self._DomainSeparatedHash(message, associated_data, signature[:64]),
|
|
317
|
+
(base.BytesToInt(signature[64:64 + k]), base.BytesToInt(signature[64 + k:])))
|
|
318
|
+
except base.InputError as err:
|
|
319
|
+
logging.info(err)
|
|
320
|
+
return False
|
|
321
|
+
|
|
202
322
|
@classmethod
|
|
203
323
|
def Copy(cls, other: ElGamalPublicKey, /) -> Self:
|
|
204
324
|
"""Initialize a public key by taking the public parts of a public/private key."""
|
|
@@ -209,12 +329,10 @@ class ElGamalPublicKey(ElGamalSharedPublicKey):
|
|
|
209
329
|
|
|
210
330
|
|
|
211
331
|
@dataclasses.dataclass(kw_only=True, slots=True, frozen=True, repr=False)
|
|
212
|
-
class ElGamalPrivateKey(ElGamalPublicKey):
|
|
332
|
+
class ElGamalPrivateKey(ElGamalPublicKey, base.Decryptor, base.Signer): # pylint: disable=too-many-ancestors
|
|
213
333
|
"""El-Gamal private key.
|
|
214
334
|
|
|
215
|
-
BEWARE: This is
|
|
216
|
-
These are pedagogical/raw primitives; do not use for new protocols.
|
|
217
|
-
No measures are taken here to prevent timing attacks.
|
|
335
|
+
BEWARE: This is **NOT** DSA! No measures are taken here to prevent timing attacks.
|
|
218
336
|
|
|
219
337
|
Attributes:
|
|
220
338
|
decrypt_exp (int): individual decryption exponent, 3 ≤ i < prime_modulus
|
|
@@ -243,12 +361,16 @@ class ElGamalPrivateKey(ElGamalPublicKey):
|
|
|
243
361
|
Returns:
|
|
244
362
|
string representation of ElGamalPrivateKey without leaking secrets
|
|
245
363
|
"""
|
|
246
|
-
return (
|
|
364
|
+
return ('ElGamalPrivateKey('
|
|
365
|
+
f'{super(ElGamalPrivateKey, self).__str__()}, ' # pylint: disable=super-with-arguments
|
|
247
366
|
f'decrypt_exp={base.ObfuscateSecret(self.decrypt_exp)})')
|
|
248
367
|
|
|
249
|
-
def
|
|
368
|
+
def RawDecrypt(self, ciphertext: tuple[int, int], /) -> int:
|
|
250
369
|
"""Decrypt `ciphertext` tuple with this private key.
|
|
251
370
|
|
|
371
|
+
BEWARE: This is raw El-Gamal, no ECIES-style KEM/DEM padding or validation! This is **NOT** DSA!
|
|
372
|
+
These are pedagogical/raw primitives; do not use for new protocols.
|
|
373
|
+
|
|
252
374
|
Args:
|
|
253
375
|
ciphertext (tuple[int, int]): ciphertext to decrypt, 0 ≤ c1,c2 < modulus
|
|
254
376
|
|
|
@@ -267,9 +389,47 @@ class ElGamalPrivateKey(ElGamalPublicKey):
|
|
|
267
389
|
ciphertext[0], self.prime_modulus - 1 - self.decrypt_exp, self.prime_modulus)
|
|
268
390
|
return (ciphertext[1] * csi) % self.prime_modulus
|
|
269
391
|
|
|
270
|
-
def
|
|
392
|
+
def Decrypt(self, ciphertext: bytes, /, *, associated_data: bytes | None = None) -> bytes:
|
|
393
|
+
"""Decrypt `ciphertext` and return the original `plaintext`.
|
|
394
|
+
|
|
395
|
+
• Let k = ceil(log2(n))/8 be the modulus size in bytes.
|
|
396
|
+
• Split ciphertext in 3 parts: k bytes for ct1, k bytes for ct2, the rest is AES-256-GCM
|
|
397
|
+
• r = ElGamal(ct1, ct2)
|
|
398
|
+
• return AES-256-GCM(key=SHA512(r)[32:], ciphertext,
|
|
399
|
+
associated_data="prefix" + len(aad) + aad +
|
|
400
|
+
Padded(ct1, k) + Padded(ct2, k))
|
|
401
|
+
|
|
402
|
+
Args:
|
|
403
|
+
ciphertext (bytes): Data to decrypt; see Encrypt() above:
|
|
404
|
+
Padded(ct1, k) + Padded(ct2, k) +
|
|
405
|
+
AES-256-GCM(key=SHA512(r)[32:], plaintext,
|
|
406
|
+
associated_data="prefix" + len(aad) + aad + Padded(ct1, k) + Padded(ct2, k))
|
|
407
|
+
associated_data (bytes, optional): Optional AAD (must match what was used during encrypt)
|
|
408
|
+
|
|
409
|
+
Returns:
|
|
410
|
+
bytes: Decrypted plaintext bytes
|
|
411
|
+
|
|
412
|
+
Raises:
|
|
413
|
+
InputError: invalid inputs
|
|
414
|
+
CryptoError: internal crypto failures, authentication failure, key mismatch, etc
|
|
415
|
+
"""
|
|
416
|
+
k: int = self.modulus_size
|
|
417
|
+
if len(ciphertext) < (k + k + 32):
|
|
418
|
+
raise base.InputError(f'invalid ciphertext length: {len(ciphertext)} ; {k=}')
|
|
419
|
+
# split ciphertext in 3 parts: the first 2k bytes is ct, the rest is AES-256-GCM
|
|
420
|
+
ct1, ct2, aes_ct = ciphertext[:k], ciphertext[k:2 * k], ciphertext[2 * k:]
|
|
421
|
+
r: int = self.RawDecrypt((base.BytesToInt(ct1), base.BytesToInt(ct2)))
|
|
422
|
+
ss: bytes = base.Hash512(base.IntToFixedBytes(r, k))
|
|
423
|
+
aad: bytes = b'' if associated_data is None else associated_data
|
|
424
|
+
aad_prime: bytes = (
|
|
425
|
+
_ELGAMAL_ENCRYPTION_AAD_PREFIX + base.IntToFixedBytes(len(aad), 8) + aad + ct1 + ct2)
|
|
426
|
+
return aes.AESKey(key256=ss[32:]).Decrypt(aes_ct, associated_data=aad_prime)
|
|
427
|
+
|
|
428
|
+
def RawSign(self, message: int, /) -> tuple[int, int]:
|
|
271
429
|
"""Sign `message` with this private key.
|
|
272
430
|
|
|
431
|
+
BEWARE: This is raw El-Gamal, no ECIES-style KEM/DEM padding or validation! This is **NOT** DSA!
|
|
432
|
+
These are pedagogical/raw primitives; do not use for new protocols.
|
|
273
433
|
We explicitly disallow `message` to be zero.
|
|
274
434
|
|
|
275
435
|
Args:
|
|
@@ -285,13 +445,49 @@ class ElGamalPrivateKey(ElGamalPublicKey):
|
|
|
285
445
|
if not 0 < message < self.prime_modulus:
|
|
286
446
|
raise base.InputError(f'invalid message: {message=}')
|
|
287
447
|
# sign
|
|
288
|
-
a
|
|
448
|
+
a: int = 0
|
|
449
|
+
b: int = 0
|
|
450
|
+
p_1: int = self.prime_modulus - 1
|
|
289
451
|
while a < 2 or b < 2:
|
|
290
452
|
ephemeral_key, ephemeral_inv = self._MakeEphemeralKey()
|
|
291
453
|
a = modmath.ModExp(self.group_base, ephemeral_key, self.prime_modulus)
|
|
292
454
|
b = (ephemeral_inv * ((message - a * self.decrypt_exp) % p_1)) % p_1
|
|
293
455
|
return (a, b)
|
|
294
456
|
|
|
457
|
+
def Sign(self, message: bytes, /, *, associated_data: bytes | None = None) -> bytes:
|
|
458
|
+
"""Sign `message` and return the `signature`.
|
|
459
|
+
|
|
460
|
+
• Let k = ceil(log2(n))/8 be the modulus size in bytes.
|
|
461
|
+
• Pick random salt of 64 bytes
|
|
462
|
+
• s1, s2 = ElGamal(Hash512("prefix" || len(aad) || aad || message || salt))
|
|
463
|
+
• return salt || Padded(s1, k) || Padded(s2, k)
|
|
464
|
+
|
|
465
|
+
This is basically Full-Domain Hash El-Gamal with a 512-bit hash and per-signature salt,
|
|
466
|
+
which is EUF-CMA secure in the ROM. Our domain-separation prefix and explicit AAD
|
|
467
|
+
length prefix are both correct and remove composition/ambiguity pitfalls.
|
|
468
|
+
There are no Bleichenbacher-style issue because we do not expose any padding semantics.
|
|
469
|
+
|
|
470
|
+
Args:
|
|
471
|
+
message (bytes): Data to sign.
|
|
472
|
+
associated_data (bytes, optional): Optional AAD for AEAD modes; must be
|
|
473
|
+
provided again on decrypt
|
|
474
|
+
|
|
475
|
+
Returns:
|
|
476
|
+
bytes: Signature; salt || Padded(s, k) - see above
|
|
477
|
+
|
|
478
|
+
Raises:
|
|
479
|
+
InputError: invalid inputs
|
|
480
|
+
CryptoError: internal crypto failures
|
|
481
|
+
"""
|
|
482
|
+
k: int = self.modulus_size
|
|
483
|
+
if k <= 64:
|
|
484
|
+
raise base.InputError(f'modulus too small for signing operations: {k} bytes')
|
|
485
|
+
salt: bytes = base.RandBytes(64)
|
|
486
|
+
s_int: tuple[int, int] = self.RawSign(self._DomainSeparatedHash(message, associated_data, salt))
|
|
487
|
+
s_bytes: bytes = base.IntToFixedBytes(s_int[0], k) + base.IntToFixedBytes(s_int[1], k)
|
|
488
|
+
assert len(s_bytes) == 2 * k, 'should never happen: s_bytes should be exactly 2k bytes'
|
|
489
|
+
return salt + s_bytes
|
|
490
|
+
|
|
295
491
|
@classmethod
|
|
296
492
|
def New(cls, shared_key: ElGamalSharedPublicKey, /) -> Self:
|
|
297
493
|
"""Make a new private key based on an existing shared public key.
|
|
@@ -318,7 +514,7 @@ class ElGamalPrivateKey(ElGamalPublicKey):
|
|
|
318
514
|
decrypt_exp: int = 0
|
|
319
515
|
while (not 2 < decrypt_exp < shared_key.prime_modulus - 1 or
|
|
320
516
|
decrypt_exp == shared_key.group_base):
|
|
321
|
-
decrypt_exp = base.RandBits(bit_length
|
|
517
|
+
decrypt_exp = base.RandBits(bit_length)
|
|
322
518
|
# make the object
|
|
323
519
|
return cls(
|
|
324
520
|
prime_modulus=shared_key.prime_modulus,
|