An introduction to digital signatures & asymmetric encryption in Python

This article will give you an introduction into asymmetric encryption using RSA. Asymmetric encryption uses two keys: a public key and a private key. The public key, can be provided to anyone, the private key should be kept for your own records.

Asymmetric encryption is used heavily online. It’s used to send sensitive information from your device to the server without the risk of the message being intercepted. Consider the example of WhatsApp; they use end to end encryption to ensure that only the person you intended to send the message to can read it.

You have a public and private key. The public key is registered on the Whatsapp application server. The private key stays safe on your device. When you message someone, your device retrieves the receipient’s public key from the application server. Your device then uses this key to encrypt the message you send. The recepient uses their private key to decrypt the message, so that they can read it.

Digital signatures use the private and public keys to help verify the authenticity of a document (was it sent by the person you think? & has the data been tampered with?).

Implementing this in Python is quite easy & an interesting project. To do this, make sure you have used pip to install the cryptography library.

Now, we will create three functions. The first, will generate the keys we will use. It will generate a private key and an associated public key. Note: A public key can be mathematically generated from the private key, but the private key cannot be generated from the public key.

def keygen():
    private_key = rsa.generate_private_key(
    public_exponent=65537,
    key_size=2048, backend=default_backend())
    public_key = private_key.public_key()
    return private_key, public_key

The next function intends to sign the message you sent. This signs the document with your private key, which means, it can only be verified using your public key. Signing a document is essentially creating a sequence of bytes, which we refer to as a signature – it’s just like a hand written signature – signing it with your private key means you approve the content of the message.

Think of it like receiving a letter in the post. The message is & signed by the sender. This gives you confidence that it was sent by the indended sender.

To sign a document, I must use something that only I possess. In this case, that something is my private key. Nobody else has it & so nobody else can sign using it. This produces a hash of the data in the document.

def sign_(message, private_key):
    signature = private_key.sign(message,padding.PSS(mgf=padding.MGF1(hashes.SHA256()),salt_length=padding.PSS.MAX_LENGTH),hashes.SHA256())
    return signature

The final function verifies that the message sent is the message received. The content of the message along with the signature gets passed to this function. It returns a ‘good signature’ or a ‘bad signature’.

When we send our data, we send both the message and the hash of the message encrypted with our private key.

To verify the signature, the receipient calculates the hash of the same data (the message or the files that have been signed). It decrypts the digital signature using the senders public key & it compares the two hash values. If they match, the signature is valid. If it doesn’t match, a different key was used to sign it & it is invalid.

def verify(message, signature, public_key):
    try: 
        public_key.verify(signature, message, padding.PSS(mgf=padding.MGF1(hashes.SHA256()),salt_length=padding.PSS.MAX_LENGTH),hashes.SHA256())
        return True
    except InvalidSignature: 
        return False

In the below, we then call all of these functions.

  1. We generate a private key (called priv) and a public key (called pub).
  2. Define the message we want to send
  3. Call the sign_ function, to sign the document using our private key
  4. Call the verification function (pretending we were the receipient) to confirm it was sent by the right person.
if __name__ == '__main__':
    priv, pub = keygen()
    message = b' I authorise $1,000 dollars to be sent to Ben'
    signature = sign_(message, priv)
    correct = verify(message, signature, pub)
    
    if correct:
        print('message verified - send the money ')
    else:
        print('this does not open with the public key. Bad signature.')
        

But what if we play around with this. In the example below, I genetate a new public & private key. I then sign the message using my NEW private key. But, as the receipient, I try to verify the message using my ORIGINAL public key. If we do this, we will get the message ‘this has been signed by someone else. Do not send money’. This is because, the message has been signed by a different (someone elses) private key, so we cannot confirm the authenticity of the message.

    priv2, pub2 = keygen()
    signature2 = sign_(message, priv2)
    correct = verify(message, signature2, pub)
    if correct:
        print('message verified - send the money ')
    else:
        print('this has been signed by someone else. Do not send money')
       

Similarly, here, change the message. But I pass in the same signature (hashed message using my private key) and try to use the same public key to validate it. So in this case, the signature & the keys should be the same but the message is different. When running this, we would see the error: ‘this is not the original message’. So, great news! it has identified that the message has been tampered with. This is because, the message and the hash are BOTH passed into the verify function.

    tamper = message + b'give me money'
    correct = verify(tamper, signature, pub)
    if correct:
        print('message verified - send the money ')
    else:
        print('this is not the original message')

Full code:

from cryptography.hazmat.primitives.asymmetric import rsa
from cryptography.hazmat.backends import default_backend
from cryptography.hazmat.primitives import hashes
from cryptography.hazmat.primitives.asymmetric import padding
from cryptography.exceptions import *
def keygen():
    private_key = rsa.generate_private_key(
    public_exponent=65537,
    key_size=2048, backend=default_backend())
    public_key = private_key.public_key()
    return private_key, public_key
def sign_(message, private_key):
    signature = private_key.sign(message,padding.PSS(mgf=padding.MGF1(hashes.SHA256()),salt_length=padding.PSS.MAX_LENGTH),hashes.SHA256())
    return signature
def verify(message, signature, public_key):
    try: 
        public_key.verify(signature, message, padding.PSS(mgf=padding.MGF1(hashes.SHA256()),salt_length=padding.PSS.MAX_LENGTH),hashes.SHA256())
        return True
    except InvalidSignature: 
        return False
if __name__ == '__main__':
    priv, pub = keygen()
    message = b' I authorise $1,000 dollars to be sent to Ben'
    signature = sign_(message, priv)
    correct = verify(message, signature, pub)
    
    if correct:
        print('message verified - send the money ')
    else:
        print('this does not open with the public key. Bad signature.')
        
    priv2, pub2 = keygen()
    signature2 = sign_(message, priv2)
    correct = verify(message, signature2, pub)
    if correct:
        print('message verified - send the money ')
    else:
        print('this has been signed by someone else. Do not send money')
        
    tamper = message + b'give me money'
    correct = verify(tamper, signature, pub)
    if correct:
        print('message verified - send the money ')
    else:
        print('this is not the original message')
        
Kodey

One thought on “An introduction to digital signatures & asymmetric encryption in Python

Comments are closed.