summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorLegrandin <gooksankoo@hoiptorrow.mailexpire.com>2011-09-28 00:09:41 +0200
committerLegrandin <gooksankoo@hoiptorrow.mailexpire.com>2011-09-28 00:09:41 +0200
commit02103e2a5aca97b299b63723fb6752c2cbc00b23 (patch)
tree780af5849ab36fdfcc50c8d5c43451d390476d4e
parent44ed631b690d77659e5ddbcbb4d0851dee7d711e (diff)
downloadpycrypto-02103e2a5aca97b299b63723fb6752c2cbc00b23.tar.gz
Modify decryption function for PKCS#1 v1.5 so that a sentinel is returned in case of padding error, as opposed to an exception being raised. Added also more information on how to avoid timing attacks.
-rw-r--r--lib/Crypto/Cipher/PKCS1_v1_5.py71
-rw-r--r--lib/Crypto/SelfTest/Cipher/test_pkcs1_15.py10
2 files changed, 63 insertions, 18 deletions
diff --git a/lib/Crypto/Cipher/PKCS1_v1_5.py b/lib/Crypto/Cipher/PKCS1_v1_5.py
index b6477c4..6052e89 100644
--- a/lib/Crypto/Cipher/PKCS1_v1_5.py
+++ b/lib/Crypto/Cipher/PKCS1_v1_5.py
@@ -22,27 +22,43 @@
"""RSA encryption protocol according to PKCS#1 v1.5
-See RFC3447 or the original RSA Labs specification at
-http://www.rsa.com/rsalabs/node.asp?id=2125.
+See RFC3447 or the `original RSA Labs specification`__ .
This scheme is more properly called ``RSAES-PKCS1-v1_5``.
+**If you are designing a new protocol, consider using the more robust PKCS#1 OAEP.**
+
As an example, a sender may encrypt a message in this way:
>>> from Crypto.Cipher import PKCS1_v1_5
>>> from Crypto.PublicKey import RSA
+ >>> from Crypto.Hash import SHA
>>> from Crypto import Random
>>>
+ >>> rng = Random.new()
>>> message = 'To be encrypted'
- >>> key = RSA.importKey('pubkey.der')
- >>> rng = Random.new().read
- >>> ciphertext = PKCS1_v1_5.encrypt(message, key, rng)
+ >>> h = SHA.new(message)
+ >>> key = RSA.importKey(open('pubkey.der').read())
+ >>> ciphertext = PKCS1_v1_5.encrypt(message+h.digest(), key, rng.read)
At the receiver side, decryption can be done using the private part of
the RSA key:
- >>> key = RSA.importKey('privkey.der')
- >>> message = PKCS1_v1_5.decrypt(ciphertext):
+ >>> From Crypto.Hash import SHA
+ >>> from Crypto import Random
+ >>>
+ >>> rng = Random.new()
+ >>> dsize = SHA.digest_size
+ >>> key = RSA.importKey(open('privkey.der').read())
+ >>> sentinel = Random.new().read(15+dsize) # Let's assume that average data length is 15
+ >>> message = PKCS1_v1_5.decrypt(ciphertext, key, sentinel)
+ >>> digest = SHA.new(message[:-dsize]).digest()
+ >>> if digest==message[-dsize:]: # Note how we DO NOT look for the sentinel
+ >>> print "Encryption was correct."
+ >>> else:
+ >>> print "Encryption was not correct."
+
+.. __: http://www.rsa.com/rsalabs/node.asp?id=2125.
"""
__revision__ = "$Id$"
@@ -99,7 +115,7 @@ def encrypt(message, key, randFunc):
c = '\x00'*(k-len(m)) + m
return c
-def decrypt(ct, key):
+def decrypt(ct, key, sentinel):
"""Decrypt a PKCS#1 v1.5 ciphertext.
This function is named ``RSAES-PKCS1-V1_5-DECRYPT``, and is specified in
@@ -111,14 +127,44 @@ def decrypt(ct, key):
key : RSA key object
The key to use to verify the message. This is a `Crypto.PublicKey.RSA`
object. It must have its private half.
+ sentinel : string
+ The string to return to indicate that an error was detected during decryption.
- :Return: A string, the original message.
+ :Return: A string, the original message or the ``sentinel``.
:Raise ValueError:
- If the ciphertext length is incorrect, or if the encryption does not
- succeed.
+ If the ciphertext length is incorrect
:Raise TypeError:
If the RSA key has no private half.
+
+ :attention:
+ You should **never** let the party who submitted the ciphertext know that
+ this function returned the ``sentinel`` value, since attacks exist (e.g. `Bleichenbacher's`__)
+ that can compromise your RSA private key by means of such information.
+
+ In general, it should not be possible for the other party to distinguish
+ whether processing at the server side failed because the value returned
+ was a ``sentinel`` as opposed to a random, invalid message.
+
+ In fact, the second option is not that unlikely: encryption done according to PKCS#1 v1.5
+ embeds no good integrity check. There is roughly one chance
+ in 2^16 for a random ciphertext to be returned as a valid message
+ (although random looking).
+
+ It is therefore advisabled to:
+
+ 1. Select as ``sentinel`` a value that resembles a plausable random, invalid message.
+ 2. Not report back an error as soon as you detect a ``sentinel`` value.
+ Put differently, you should not explicitly check if the returned value is the ``sentinel`` or not.
+ 3. Cover all possible errors with a single, generic error indicator.
+ 4. Embed into the definition of ``message`` (at the protocol level) a digest (e.g. ``SHA-1``).
+ 5. Where possible, monitor the number of errors due to ciphertexts originating from the same party,
+ and slow down the rate of the requests from such party (or even blacklist it altogether).
+
+ **If you are designing a new protocol, consider using the more robust PKCS#1 OAEP.**
+
+ .. __: http://www.springerlink.com/index/j5758n240017h867.pdf
"""
+
# TODO: Verify the key is RSA
# See 7.2.1 in RFC3447
@@ -134,9 +180,8 @@ def decrypt(ct, key):
em = '\x00'*(k-len(m)) + m
# Step 3
sep = em.find('\x00',2)
- #print "sep=", sep
if not em.startswith('\x00\x02') or sep<10:
- raise ValueError("Incorrect decryption.")
+ return sentinel
# Step 4
return em[sep+1:]
diff --git a/lib/Crypto/SelfTest/Cipher/test_pkcs1_15.py b/lib/Crypto/SelfTest/Cipher/test_pkcs1_15.py
index 8706b30..f1329b1 100644
--- a/lib/Crypto/SelfTest/Cipher/test_pkcs1_15.py
+++ b/lib/Crypto/SelfTest/Cipher/test_pkcs1_15.py
@@ -127,23 +127,23 @@ HKukWBcq9f/UOmS0oEhai/6g+Uf7VHJdWaeO5LzuvwU=
# Build the key
key = RSA.importKey(test[0])
# The real test
- pt = PKCS.decrypt(t2b(test[2]), key)
+ pt = PKCS.decrypt(t2b(test[2]), key, "---")
self.assertEqual(pt, test[1])
def testVerify2(self):
# Verify that decryption fails if ciphertext is not as long as
# RSA modulus
self.assertRaises(ValueError, PKCS.decrypt, '\x00'*127,
- self.key1024)
+ self.key1024, "---")
self.assertRaises(ValueError, PKCS.decrypt, '\x00'*129,
- self.key1024)
+ self.key1024, "---")
# Verify that decryption fails if there are less then 8 non-zero padding
# bytes
pt = '\x00\x02' + '\xFF'*7 + '\x00' + '\x45'*118
ct = self.key1024.encrypt(pt, 0)[0]
ct = '\x00'*(128-len(ct)) + ct
- self.assertRaises(ValueError, PKCS.decrypt, ct, self.key1024)
+ self.assertEqual("---", PKCS.decrypt(ct, self.key1024, "---"))
def testEncryptVerify1(self):
# Encrypt/Verify messages of length [0..RSAlen-11]
@@ -151,7 +151,7 @@ HKukWBcq9f/UOmS0oEhai/6g+Uf7VHJdWaeO5LzuvwU=
for pt_len in xrange(0,128-11+1):
pt = self.rng(pt_len)
ct = PKCS.encrypt(pt, self.key1024, self.rng)
- pt2 = PKCS.decrypt(ct, self.key1024)
+ pt2 = PKCS.decrypt(ct, self.key1024, "---")
self.assertEqual(pt,pt2)