In class, we also learned how a signed message digest could be used to guarantee the integrity of a message. Signing the digest instead of the message itself gives much better efficiency.
In the final task, we will create and sign a message digest, and verify it with the corresponding public key.
Open 3_sign_digest.py
and let’s begin.
Generate RSA Key-Pair
Task 3-(1,2)
TASK 3-(1,2):
Before we can sign any digest, we need to first generate the asymmetric key pair. It is very simple to do so (you will also encounter this in Programming Assignment 2):
private_key = rsa.generate_private_key(
public_exponent=65537,
key_size=1024,
)
public_key = private_key.public_key()
The first argument dictates the value of e
(the public exponent). Almost everyone uses 65537
(for security purposes) as recommended here. The second argument dictates the key size (1024 bits in for this task).
Using the Keys
Afterwards, we can use the private or public key for encryption or decryption. However, there are some terminologies that you need to know.
Encrypt and Decrypt
Encryption of a message with a public key is normally labeled as encryption
in the API, and decryption with a private key is labeled as decryption
(names that you will expect). Here’s an example to get you started:
data_bytes = b"Lorem ipsum dolor sit amet"
encrypted_data_bytes = public_key.encrypt(
data_bytes,
padding.OAEP(
mgf=padding.MGF1(hashes.SHA256()),
algorithm=hashes.SHA256(),
label=None,
),
)
decrypted_data_bytes = private_key.decrypt( # 128 bytes long
encrypted_data_bytes,
padding.OAEP(
mgf=padding.MGF1(hashes.SHA256()),
algorithm=hashes.SHA256(),
label=None,
),
)
assert decrypted_data_bytes == data_bytes
print("encrypted_data_bytes", encrypted_data_bytes) # ciphertext
print("decrypted_data_bytes", decrypted_data_bytes) # same as data_bytes
Note that for encrypt
with public_key
, the length of data bytes depends on the padding scheme. This example uses OAEP
scheme, but there are other schemes too such as PKCS1v15
, etc. You can read more about alternative paddings and their parameters here.
It is important to know how many bytes the padding will add at minimum.
- With 1024 RSA-key, the maximum length of message that it can encrypt is also 1024 bits (128 bytes)
- OAEP with
SHA-256
takes 66 bytes of padding overhead, leaving you with only 62 bytes of content to encrypt/decrypt at a time - You will encounter this as well in Programming Assignment 2
Sign and Verify
If you want to encrypt a message using the private key, the keyword you should look for in the API is sign
, and the output is commonly called as a signature
- Conversely, if you want to decrypt the output signature, the keyword in the API is
verify
. There will be no output here if the verification succeeds (the decrypted signature matches the initial message), but aVerficationError
will be raised if otherwise. - But don’t be fooled! Verification is a decryption, and signing is an encryption. They just have different names that are tied to their purpose.
Here’s an example to get you started:
data_bytes = b"Lorem ipsum dolor sit amet, consectetuer adipiscing elit, sed diam nonummy nibh euismod tincidunt ut laoreet dolore magna" # 121 bytes
signature = private_key.sign(
data_bytes,
padding.PSS(
mgf=padding.MGF1(hashes.SHA256()),
salt_length=padding.PSS.MAX_LENGTH,
),
hashes.SHA256(), # Algorithm to hash the file_data before signing
)
try:
public_key.verify(
signature,
data_bytes,
padding.PSS(
mgf=padding.MGF1(hashes.SHA256()),
salt_length=padding.PSS.MAX_LENGTH,
),
hashes.SHA256(),
)
print("Verification Succeeds")
except:
print("Verification Fails")
exit()
Note that the length of data_bytes
can be arbitrarily long because it will be hashed automatically (third argument of sign
) before encrypted with the private key. The output length of SHA-256
is 32 bytes only (256 bits) which when added with the padding, will still be less than 128 bytes.
We also use a different padding scheme for the signature (PSS
) and not OAEP
because of the nature of the padding. You may read more about it here or in any other online source materials but it is our of our syllabus.
Creating a Digest
Task 3-(3,4)
TASK 3-(3,4):
In order to create a message digest, you must first create a hash instance, then use it with update(input)
and finalize()
methods.
data_bytes = b"Lorem Ipsum" # 11 bytes
hash_function = hashes.Hash(hashes.SHA256())
hash_function.update(data_bytes)
message_digest_bytes = hash_function.finalize() # 32 bytes
print("message_digest_bytes", message_digest_bytes)
The print output is:
message_digest_bytes b'\x03\r\xc1\xf96\xc3AZ\xff?3W\x165\x15\x19\r4z(\xe7X\xe1\xf7\x17\xd1{\xaeE5A\xc9'
Is the length of
message_digest_bytes
always the same (32 bytes), regardless of the length of the inputdata_bytes
?
Task 3-(5-8)
TASK 3-(5-8):
With all the explanations above, you should be able to complete these tasks. Fill in your answer under the TODO
for this task.
Summary
Finally, if you scroll below you will find these four function calls:
if __name__ == "__main__":
enc_digest("original_files/shorttext.txt")
enc_digest("original_files/longtext.txt")
sign_digest("original_files/shorttext.txt")
sign_digest("original_files/longtext.txt")
Here we test encryption of digest using public key (enc_digest
) and encryption of digest using private key (sign_digest
), and we test each with two files of differing length. Study its output carefully and head to edimension to answer the rest of the questionnaires.