Chapter 5
Digital signatures

Initial version: 2024-02-14
Last update: 2024-04-20

From the two previous chapters you know two very important puzzles: how to make a fingerprint of data and how to cipher plain text turning it into meaningless, random-like mass of characters. If you join both of them you will get an image of another tool required to implement blockchain.

In this chapter you will learn how to make signature of you data. Signature is more than hash, because it does not only almost uniquely identify the sequence of characters but also inseparably binds this identifier with the sequence of characters preventing it from being changed by unauthorized person.

Table of contents


The power of signature


In our everyday life save characters and keep them in unchanged form means: print document, which is a sequence of characters in most cases organized somehow visually on a page, and sign it by placing on the same page your signature. People believe in the power of handwritten signature. They believe that after signing nothing can be changed on this document. In many cases it is true, because any interference in document will cause visible changes. What is important, the signature is inseparable from the document on which it is placed – it is not possible to sign on a sheet other than the signed content. Sometimes, when you sign a real paper it is called a hard copy and any signature affixed to a hard copy with a pen or other writing device is a wet signature.

Unfortunately in digital word things are not so simple. In digital word you deal with sequence of characters which are separated from medium. The bit string


11000101100000011100001110110011011001001100010110111010
is still the same indistinguishable string of bits (saying "indistinguishable" I mean that you have problem to say what does this string represent; in fact this is a name of my city, Łódź, encoded in UTF-8) no matter if you store it in RAM, SSD or Micro SD card. Moreover, if someone instead writes:


11000101100000011100001110110011011000111100010110111010
looking at the second string and not being able to compare it with the first one, you can't tell if something has changed part of it or not (can you point out the difference between them?).

You will therefore look for a way and tools to digitally sign digital documents. A digital signature must have the same characteristics as (it is believed) a traditional signature has, i.e.:

  • Can be easily done by its "owner".
  • Is hard to falsify – is difficult for a "non-owner" to be done.
  • Is inseparable from signed document.
  • Having signature it may be difficult to identify a person, but having a person you can easily verify if that person made a signature.


Digital signatures


Digital signatures, as their handwritten analogue from real life, proves:

  • authenticity (you know that data signed by XYZ could have only been signed by XYZ, because only XYZ knows XYZ's private key);
  • integrity (data wasn’t forged or tampered);
  • non-repudiation (if data have been signed by XYZ, XYZ can’t deny having signed it – every person with XYZ's public key can verify this).


Making signature


Having basic knowledge from applied cryptography you are ready now to make digital signatures. They utilize cryptographic hashing and the private to public way of using asymmetric cryptography:


[IMG: signature_sign_data]
  1. First, for your unencrypted, plain text data you calculate hash. This way you have a method to detect any attempts of tampering.
  2. Next, using your private key, you encrypt the hash of your data. This way you approve and authorize the hash in the sense that this hash is irrevocable binded with the data, with your data, and the only person who could approve it was you. This way you guarantee data integrity and prevent tampering.
  3. Then you attach (concatenate) the encrypted hash to the data. This way you give all other person who has your public key a possibility to verify truth of document in a sense of its integrity and its author.


Verifying signature


The verification process uses the signer’s public key to check the signature:


[IMG: signature_verify_signature]
  1. At first the recipient of the message separates data from encrypted hash and calculates hash of data.
  2. Then the recipient decrypts the attached cypher text (which is the digital signature) with your shared public key.
  3. Next the recipient compares the hash value calculated in step 1 with plain text obtained in step 2 (which is a deciphered hash calculated by message sender). If both are the same, the recipient can be sure about message integrity and its authorship.


Practical part


Implementing signing


Based on the steps given above and practical skills related to hashing and cryptography you can sign any document you want.

Unfortunately due to PyNaCl library limitation (no asymmetric cryptography in private to public way) I will implement signing with symmetric cryptography – this will not influence the general idea of the algorithm; only decryption / encryption method will be different.

Prepare data to sign

On the following code you see how you can transform data (in JSON form) to sequence of bytes encoded in Base64 coding:


messageText = "Test message"
data = {"message": messageText,
        "timestamp": f"{datetime.datetime.now()}"}
dataString = json.dumps(data, sort_keys=True)
dataBytes = dataString.encode('utf-8')
dataBytesB64 = base64.b64encode(dataBytes).decode("utf-8")

print(dataBytesB64)
When executed you get the readable string you can attach to email or send with any other method you want:


eyJtZXNzYWdlIjogIlRlc3QgbWVzc2FnZSIsICJ0aW1lc3RhbXAiOiAiMjAyNC0wNC0yMCAxMToxNDo1MC43NjU4MTgifQ==
Prepare key

Next you have to prepare key (in symmetric cryptography; in asymmetric you will use a pair of keys):


key = nacl.utils.random(nacl.secret.SecretBox.KEY_SIZE)
keyB64 = base64.b64encode(key).decode("utf-8")
print(keyB64)
Again thanks to Base64 coding you can easily operate with this key which is represented as an ASCII text:


+uJq2y3rPvnLcbIXvUXYmruRObEDqltXxoD/1a1Z3SE=
Signing data

You will sign document with the following function:


'''
data - data as a string encoded in Base64
key - key encoded in Base64
'''
def sign(dataB64, keyB64):
  signature = {}

  # Hash data
  dataBytes = dataB64.encode('utf-8')
  hashObj = hashlib.sha256(dataBytes)
  hashBytes = hashObj.digest()
  hashHex = hashObj.hexdigest()
  hashB64 = base64.b64encode(hashBytes).decode("utf-8")

  # Encrypt hash
  keyBytes = base64.b64decode(keyB64.encode('utf-8'))
  box = nacl.secret.SecretBox(keyBytes)
  hashEncrypted = box.encrypt(hashBytes)
  hashEncryptedB64 = base64.b64encode(hashEncrypted).decode("utf-8")

  signature = {
      "hashHex": hashHex,
      "hashB64": hashB64,
      "signature": hashEncryptedB64
  }

  return signature
As arguments of a sign() function you pass previously prepared data: data you want to sign and key you want to use to protect your signature, both encoded in Base64 coding:


signature = sign(dataBytesB64, keyB64)
This way you obtain your signature (which is an encrypted hash):


{
  'hashHex': 'b4094125e5501af3cfa98673c8aefcccb4e8406df1274f8812ebdb6a8561d729',
  'hashB64': 'tAlBJeVQGvPPqYZzyK78zLToQG3xJ0+IEuvbaoVh1yk=',
  'signature': 'yX6IV0gR7VudAIgDMTIMTzkMSoPblCg8STFi8Ie0B3mPrFYzB98PZR83OSu5r69ziIjKDPYAUg0YQ/m24ScNoZKJUt72Q40G'
}
Your signature, string yX6...40G, is in Base64 coding so you can easily attach it to your data.

At this moment you can send your data and signature to a recipient. Because both are coded in Base64 you can treat them as strings.

Verifying data by using the signature

When you receive data and signature you may verify signature (of course you have to have the key). You will do this with the following function:


def verifySignature(dataB64, signatureB64, keyB64):
  # Hash data
  dataBytes = dataB64.encode('utf-8')
  hashObj = hashlib.sha256(dataBytes)
  hashBytes = hashObj.digest()
  hashHex = hashObj.hexdigest()
  hashB64 = base64.b64encode(hashBytes).decode("utf-8")

  # Decrypt hash
  keyBytes = base64.b64decode(keyB64.encode('utf-8'))
  box = nacl.secret.SecretBox(keyBytes)
  signatureBytes = base64.b64decode(signatureB64.encode('utf-8'))
  hashDecrypted = box.decrypt(signatureBytes)
  hashDecryptedB64 = base64.b64encode(hashDecrypted).decode("utf-8")

  if hashB64 == hashDecryptedB64:
    return True

  return False
As arguments of a verifySignature() function you pass three data: two of them are the same as for sign() function and one is a signature (also encoded in Base64 coding):


result = verifySignature(dataBytesB64, signature["signature"], keyB64)
print(result)
If signature is correct you will get True, otherwise you will get False:


True


Ready to use signing


To simplify this process PyNaCl library offers a set of dedicated tools.

Signing Data with PyNaCl

Import required libraries:


import nacl.encoding
import nacl.signing
Signatory person generates its private and public key:


signerPrivateKey = nacl.signing.SigningKey.generate()
signerPublicKey = signerPrivateKey.verify_key
Since signerPublicKey is a sequence of bytes, signatory person may need to transform it to a readable format before publishing it. You can encode it in hex, Base64 or any other form useful for you:


signerPublicKeyHex = signerPublicKey.encode(encoder=nacl.encoding.HexEncoder)
print(signerPublicKeyHex)

signerPublicKeyBase64 = signerPublicKey.encode(encoder=nacl.encoding.Base64Encoder)
print(signerPublicKeyBase64.decode('utf-8'))
In my case, the public key in hex and Base64 encoding takes the form:


b'b75ee7520ac021d127fedb44108835c3dc753dd9f1784bf237389ac553606126'
t17nUgrAIdEn/ttEEIg1w9x1PdnxeEvyNziaxVNgYSY=
Now signatory person is ready to sign message:


messageText = "Test text"
messageTextBytes = messageText.encode('utf-8')

# Signing a message encoded with Base64Encoder
messageSignedBase64 = signerPrivateKey.sign(messageTextBytes, encoder = nacl.encoding.Base64Encoder)
print(messageSignedBase64.decode('utf-8'))

# Signing a message without encoding the key or message
messageSigned = signerPrivateKey.sign(messageTextBytes)
print(messageSigned)


Depending whether you use encoder (encoder is a function transforming sequence of bytes into another representation form, for example Base64) or not and what kind of it different strings you will see:


SMn7wqpQ2hWS3zl/Uj691UqxFAlUgMxyO1bjtvOXSD+zctjDRx8KemEEylCsjaWUb4tdzI73/zmF8zYXDQePDFRlc3QgdGV4dA==
b'H\xc9\xfb\xc2\xaaP\xda\x15\x92\xdf9\x7fR>\xbd\xd5J\xb1\x14\tT\x80\xccr;V\xe3\xb6\xf3\x97H?\xb3r\xd8\xc3G\x1f\nza\x04\xcaP\xac\x8d\xa5\x94o\x8b]\xcc\x8e\xf7\xff9\x85\xf36\x17\r\x07\x8f\x0cTest text'
As you can see by printing messageSigned , the message is not encrypted after signing but it is padded with bytes containing the signature.

Next, you will see how anyone can use corresponding public key to verify the message.

Verifying data by using the signature PyNaCl

Now you will use the public key signerPublicKeyHex (or signerPublicKeyBase64) to verify that the message was signed by its owner:

Import required libraries:


import nacl.encoding
import nacl.signing
Create a "box" for veryfying signature:


verifyKey = nacl.signing.VerifyKey(signerPublicKeyBase64, encoder=nacl.encoding.Base64Encoder)
Verify the signature of the message:


signedText = verifyKey.verify(messageSignedBase64, encoder=nacl.encoding.Base64Encoder)

print(signedText.decode('utf-8'))
If everything is correct, you will see signed text:


Test text
Of course, for private key encoded in Base64 you can do the same steps:


verifyKey = nacl.signing.VerifyKey(signerPublicKeyHex, encoder=nacl.encoding.HexEncoder)
signedText = verifyKey.verify(messageSignedBase64, encoder=nacl.encoding.Base64Encoder)

print(signedText.decode('utf-8'))
with the same result:


Test text


Summary


You may think that this chapter was quite short. You are right and wrong at the same time. It seems to be short because two previous chapters were long. In this chapter you used what you have learned so far. You put puzzles together to get an impressive view of the way how you can sign digital data in an indisputable way that guarantees the integrity of the data and the possibility of proving that it was you who signed it.