The Python cryptography
module is pretty straightforward to use. We will highlight here a few methods that are necessary for this assignment to save your time.
You can install the
cryptography
module usingpip
:python3.10 -m pip install cryptography
. Make sure that it is installed to yourPython3.10
and not your previous versions of Python, that is to call thepip
with thepython3.10
version (make sure its in$PATH
, no other alias, etc). If you don’t know how to manage your modules for different versions of Python, please Google harder and try to understand how things are installed in your computer from your knowledge about the OS and the File System. We are way past this right now.
You can skim the sections below or jump straight ahead to read their official documentation about RSA (Asymmetric encryption) here and Fernet (Symmetric encryption) here.
Key Generation
In source/auth/generate_keys.py
, we have provided you with instructions on how to generate 1024-bit RSA key pair. You can run the script as such:
cd source/auth
python3 generate_keys.py server
Two files will be produced in source/auth
:
server_private_key.pem
: private key for server program (note that private key file also contain public key information as well)server_csr.pem
: certificate signing request file, to be sent to our CA bot for signing purposes. The CA will return a.crt
file containing the server’s public key.
Do not change the names of the files generated by generate_keys.py
. Do not change the location of the files either. The two above must be placed in source/auth
.
Reading Certificate
You can extract a public key from a .crt
file. For instance, we give you cacsertificate.crt
file containing our CA’s public key. You can use the CA public key later on to verify the authenticity of the certificate sent by the server.
First, you can open the .crt
file as per normal as bytes
data type,
from cryptography import x509
from cryptography.exceptions import InvalidSignature
from cryptography.hazmat.primitives import hashes
from cryptography.hazmat.primitives.asymmetric import padding
from cryptography.hazmat.backends import default_backend
f = open("auth/cacsertificate.crt", "rb")
ca_cert_raw = f.read()
Then use the x509
method to load it:
ca_cert = x509.load_pem_x509_certificate(
data=ca_cert_raw, backend=default_backend()
)
Afterwards, you can get a public key from the certificate:
ca_public_key = ca_cert.public_key()
Verify signature
You can use a public key to verify any signature data. For instance, suppose you have server_cert_raw
byte data, containing the information about the signed server certificate by the CA. You can verify that this server_cert_raw
is indeed issued by the CA using the verify
method:
server_cert = x509.load_pem_x509_certificate(
data=server_cert_raw, backend=default_backend()
)
ca_public_key.verify(
signature=server_cert.signature, # signature bytes to verify
data=server_cert.tbs_certificate_bytes, # certificate data bytes that was signed by CA
padding=padding.PKCS1v15(), # padding used by CA bot to sign the the server's csr
algorithm=server_cert.signature_hash_algorithm,
)
An InvalidSignature
exception will be raised if the signature fails to verify. Otherwise, the instructions will continue.
Then you can also extract server’s public key from server_cert
after it passes the verification:
server_public_key = server_cert.public_key()
Check Certificate Validity
It is also important to check the validity of a certificate before proceeding with the FTP. You can do this easily by doing:
import datetime
assert server_cert.not_valid_before <= datetime.utcnow() <= server_cert.not_valid_after
Reading a .pem file
You can extract both the private key and public key from the .pem
file generated by generate_keys.py
. There’s no password used by default.
try:
with open("auth/server_private_key.pem", mode="r", encoding="utf8") as key_file:
private_key = serialization.load_pem_private_key(
bytes(key_file.read(), encoding="utf8"), password=None
)
public_key = private_key.public_key()
except Exception as e:
print(e)
# Use private_key or public_key for encryption or decryption from now onwards
Sign Message and Verify
We sign a message by encrypting it with our private_key
. For instance,
message = bytes("hello world", encoding="utf-8")
signed_message = private_key.sign(
message, # message in bytes format
padding.PSS(
mgf=padding.MGF1(hashes.SHA256()),
salt_length=padding.PSS.MAX_LENGTH,
),
hashes.SHA256(), # hashing algorithm used to hash the data before encryption
)
We can then verify the message
using the verify
method that we have seen above with the corresponding public_key
, otherwise will raise InvalidSignature
Exception.
public_key.verify(
signed_message,
message,
padding.PSS(
mgf=padding.MGF1(hashes.SHA256()),
salt_length=padding.PSS.MAX_LENGTH,
),
hashes.SHA256(),
)
# will continue here if the verify above passes
Note that in the above example, SHA256
is used to hash the message
first before encrypting it with private_key
.
The padding used for the example above is PSS
. It is a new signature padding scheme standard that should be used to pad signatures securely. You may read more about it here but it is out of our syllabus scope.
Also note that in the above ca_public_key.verify
example, we used the old padding.PKCS1v15()
because that’s just how our CA bot signed the .csr
, however when our server signs the client’s message, it can use the more advanced PSS
padding. Conversely, the client must also use PSS
padding if the server used it to pad the digital signature.
Standard Encryption
You can encrypt a message with a public_key
.
In the section above, you encrypt a message with a
private_key
, and we call this signing instead.
message = bytes("hello world", encoding="utf-8")
encrypted_message = public_key.encrypt(
message,
padding.OAEP(
mgf=padding.MGF1(hashes.SHA256()),
algorithm=hashes.SHA256(),
label=None,
),
)
Padding and Message Size
Note that in the example above, OAEP
padding scheme is used. This is an RSA encryption padding scheme (as opposed to PSS
, which is a signature padding scheme).
Note that there are two RSA signature schemes specified in PKCS1: RSASSA-PKCS1-v1_5 and RSASSA-PSS, and there are two RSA encryption schemes: RSAES-PKCS-v1_5 and RSAES-OAEP. The details are out of our syllabus.
The minimum length of OAEP
padding is 66 bytes. A 1024
bit RSA keys can at most encrypt 128 bytes of message data chunk at a time. With 66 bytes of padding at minimum this leaves us with 62 bytes of message to encrypt at a time.
If you were to encrypt with PKCS1v15
instead (min 11 bytes of padding):
encrypted_message = public_key.encrypt(message, padding.PKCS1v15())
Then the maximum length of the message to encrypt at a time with 1024 bit RSA key is 117 bytes.
It is up to you to choose which padding implementation to use to encrypt the chunk of messages (file data) sent by client to the server. Just ensure that you set the chunk size accordingly.
Standard Decryption
You can decrypt a message with a private_key
.
Again, you can also decrypt a signed message with a
public_key
, but the keyword used here isverify
and not standard “decryption”.
decrypted_message = private_key.decrypt(
encrypted_message, # in bytes
padding.OAEP( # padding should match whatever used during encryption
mgf=padding.MGF1(hashes.SHA256()),
algorithm=hashes.SHA256(),
label=None,
),
)
You can also decrypt a message with a public_key
. In order to decrypt with a public key, this message has to be encrypted with a private key, and we call this a signature (instead of regular encrypted message). You have seen this in the previous section above.
Generating a Symmetric Key
You can generate a symmetric key as session key for a better file encryption performance. You can use the Fernet
method to generate a secure symmetric key, instead of the usual AES/3DES. It is really simple to use:
from cryptography.fernet import Fernet
session_key_bytes = Fernet.generate_key() # generates 128-bit symmetric key as bytes
session_key = Fernet(session_key_bytes) # instantiate a Fernet instance with key
Using a Symmetric Key
Unlike RSA encryption, you can encrypt
any byte datatype without separating them into chunks:
long_message = b""
with open("source/files/image.ppm", "rb") as f:
long_message = f.read()
encrypted_long_message = session_key.encrypt(long_message)
decrypted_long_message = session_key.decrypt(encrypted_long_message)
assert decrypted_long_message == long_message
You may experiment with other symmetric key algorithms if you wish, but for this assignment, we expect you to use Fernet.
More details
More documentation details can always be found at their official documentation page. Utilise their search bar.