23

I am trying to familiarize myself with the pycrypto module, but the lack of clear documentation makes things difficult.

To start with, I would like to understand signing and verifying data. Could someone please provide an example for how this would be written?

Noah McIlraith
  • 12,932
  • 7
  • 24
  • 35
  • 6
    **WARNING Don't use `pycrypto`!** [It's unmaintained](https://github.com/dlitz/pycrypto/issues/285) since about 2013, and it has at least two **grave vulnerabilities** still unpached as of today. Use `pycryptodome` or [cryptography.io](https://cryptography.io/en/latest/) instead! – ulidtko Jun 08 '19 at 22:05

3 Answers3

22

This is a fleshed-out version of the example in the old PyCrypto documentation:

Ensure you are using pycryptodome and not pycrypto (which is unmaintained!)

pycryptodome can be installed with pip install pycryptodome

import Crypto.Hash.MD5 as MD5
import Crypto.PublicKey.RSA as RSA
import Crypto.PublicKey.DSA as DSA
import Crypto.PublicKey.ElGamal as ElGamal
import Crypto.Util.number as CUN
import os

plaintext = 'The rain in Spain falls mainly on the Plain'

# Here is a hash of the message
hash = MD5.new(plaintext).digest()
print(repr(hash))
# '\xb1./J\xa883\x974\xa4\xac\x1e\x1b!\xc8\x11'

for alg in (RSA, DSA, ElGamal):
    # Generates a fresh public/private key pair
    key = alg.generate(384, os.urandom)

    if alg == DSA:
        K = CUN.getRandomNumber(128, os.urandom)
    elif alg == ElGamal:
        K = CUN.getPrime(128, os.urandom)
        while CUN.GCD(K, key.p - 1) != 1:
            print('K not relatively prime with {n}'.format(n=key.p - 1))
            K = CUN.getPrime(128, os.urandom)
        # print('GCD({K},{n})=1'.format(K=K,n=key.p-1))
    else:
        K = ''

    # You sign the hash
    signature = key.sign(hash, K)
    print(len(signature), alg.__name__)
    # (1, 'Crypto.PublicKey.RSA')
    # (2, 'Crypto.PublicKey.DSA')
    # (2, 'Crypto.PublicKey.ElGamal')

    # You share pubkey with Friend
    pubkey = key.publickey()

    # You send message (plaintext) and signature to Friend.
    # Friend knows how to compute hash.
    # Friend verifies the message came from you this way:
    assert pubkey.verify(hash, signature)

    # A different hash should not pass the test.
    assert not pubkey.verify(hash[:-1], signature)
John Lyon
  • 9,978
  • 2
  • 34
  • 42
unutbu
  • 711,858
  • 148
  • 1,594
  • 1,547
  • Thanks, that's very helpful, just one question, what does the `""` in `signature=RSAkey.sign(hash,"")` mean? – Noah McIlraith Nov 20 '10 at 19:03
  • Also, I see that the signature is a tuple, what is a good way of storing this in a way thats portable? – Noah McIlraith Nov 20 '10 at 19:39
  • 2
    @Noah McIlraith: For RSA, the second argument `K` is not used. For ElGamal and DSA, a long string or random data `K` needs to be supplied. The details can be found in http://www.dlitz.net/software/pycrypto/doc/#crypto-publickey-public-key-algorithms under the section entitled "The ElGamal and DSA algorithms". – unutbu Nov 20 '10 at 20:16
  • 2
    @Noah McIlraith: For portable storage of the signature, I think a plain text string would be easiest. You could use `json.dumps(signature)` to save it as a JSON string, and load it back in (as a tuple) with `json.loads`. – unutbu Nov 20 '10 at 20:23
  • 1
    Would the signature tuple ever contain more than one item? – Noah McIlraith Nov 21 '10 at 10:59
  • 2
    @Noah McIlraith: For RSA the signature tuple has length 1, but for DSA and ElGamal it has length 2. I've edited my answer to show how DSA and ElGamal might be used. – unutbu Nov 21 '10 at 12:53
  • 2
    Downvoted because the help (see `help(key.sign)` for RSA key type) says "attention: this function performs the plain, primitive RSA decryption (*textbook*). In real applications, you always need to use proper cryptographic padding, and you should not directly sign data with this method. Failure to do so may lead to security vulnerabilities. It is recommended to use modules `Crypto.Signature.PKCS1_PSS` or `Crypto.Signature.PKCS1_v1_5` instead." Using encryption is tricky enough to get right, going against a library's suggestion seems like really bad idea... – Azendale Feb 10 '17 at 04:43
  • 1
    **Warning**: this answer demonstrates all the cryptographic pitfalls in one go. MD5 hashing for signatures, too small a key size, ElGamal (which is encryption / decryption), bad padding, low level API use etc. etc. etc. **Nothing** in this answer is usable or secure. – Maarten Bodewes Oct 18 '18 at 02:31
15

Below is the helper class I created to perform all necessary RSA functions (encryption, decryption, signing, verifying signature & generating new keys)

rsa.py

from Crypto.PublicKey import RSA
from Crypto.Cipher import PKCS1_OAEP
from Crypto.Signature import PKCS1_v1_5
from Crypto.Hash import SHA512, SHA384, SHA256, SHA, MD5
from Crypto import Random
from base64 import b64encode, b64decode

hash = "SHA-256"

def newkeys(keysize):
    random_generator = Random.new().read
    key = RSA.generate(keysize, random_generator)
    private, public = key, key.publickey()
    return public, private

def importKey(externKey):
    return RSA.importKey(externKey)

def getpublickey(priv_key):
    return priv_key.publickey()

def encrypt(message, pub_key):
    #RSA encryption protocol according to PKCS#1 OAEP
    cipher = PKCS1_OAEP.new(pub_key)
    return cipher.encrypt(message)

def decrypt(ciphertext, priv_key):
    #RSA encryption protocol according to PKCS#1 OAEP
    cipher = PKCS1_OAEP.new(priv_key)
    return cipher.decrypt(ciphertext)

def sign(message, priv_key, hashAlg="SHA-256"):
    global hash
    hash = hashAlg
    signer = PKCS1_v1_5.new(priv_key)
    if (hash == "SHA-512"):
        digest = SHA512.new()
    elif (hash == "SHA-384"):
        digest = SHA384.new()
    elif (hash == "SHA-256"):
        digest = SHA256.new()
    elif (hash == "SHA-1"):
        digest = SHA.new()
    else:
        digest = MD5.new()
    digest.update(message)
    return signer.sign(digest)

def verify(message, signature, pub_key):
    signer = PKCS1_v1_5.new(pub_key)
    if (hash == "SHA-512"):
        digest = SHA512.new()
    elif (hash == "SHA-384"):
        digest = SHA384.new()
    elif (hash == "SHA-256"):
        digest = SHA256.new()
    elif (hash == "SHA-1"):
        digest = SHA.new()
    else:
        digest = MD5.new()
    digest.update(message)
    return signer.verify(digest, signature)

Sample Usage

import rsa
from base64 import b64encode, b64decode

msg1 = "Hello Tony, I am Jarvis!"
msg2 = "Hello Toni, I am Jarvis!"
keysize = 2048
(public, private) = rsa.newkeys(keysize)
encrypted = b64encode(rsa.encrypt(msg1, public))
decrypted = rsa.decrypt(b64decode(encrypted), private)
signature = b64encode(rsa.sign(msg1, private, "SHA-512"))
verify = rsa.verify(msg1, b64decode(signature), public)

print(private.exportKey('PEM'))
print(public.exportKey('PEM'))
print("Encrypted: " + encrypted)
print("Decrypted: '%s'" % decrypted)
print("Signature: " + signature)
print("Verify: %s" % verify)
rsa.verify(msg2, b64decode(signature), public)
Dennis
  • 2,860
  • 4
  • 23
  • 37
  • I find this confusing. the signature for rsa.encrypt is ```(message, pub_key)``` but the call in the sample usage is ```rsa.encrypt(msg1, private)```, making it appear to want a public key but actually get a private key. Also, rsa.newkeys() returns two values, one of which is derived from the other (in particular, ```(x, x.public_key())```), which seems quite different from the "plain English" interpretation of ```(public, private)``` – mwag Jan 20 '18 at 00:28
  • 1
    thanks for pointing out the mistake in Sample Usage (now updated). In order to do encryption, you will need to call `rsa.encrypt(msg1, public)`. For RSA, you'll need public key for encryption & verification, private key is needed for decryption & signing. Also you can always obtain the `public key` from a `private key` but not possible from the other way round – Dennis Jan 20 '18 at 06:49
13

According to the documentation at:

https://www.dlitz.net/software/pycrypto/api/current/Crypto.PublicKey.RSA._RSAobj-class.html

you should not use Crypto.PublicKey.RSA.sign function from PyCrypto in real code:

Attention: this function performs the plain, primitive RSA decryption (textbook). In real applications, you always need to use proper cryptographic padding, and you should not directly sign data with this method. Failure to do so may lead to security vulnerabilities. It is recommended to use modules Crypto.Signature.PKCS1_PSS or Crypto.Signature.PKCS1_v1_5 instead.

I ended up using the RSA module that implements PKCS1_v1_5. The documentation for signing was pretty straight forward. Others have recommended use M2Crypto.

Community
  • 1
  • 1
jeffmax
  • 459
  • 4
  • 14