Generating Encrypted Key Pairs In Python

I recently had a requirement to produce some code that can generate encrypted PEM encoded RSA key pairs that can be used for snowflake key pair authentication for service accounts. I didn't find a lot of clear documentation on what the best practice was to do this in a python environment, so ill share what I came up with that allows you to produce an RSA key pair with the following properties:

  • It is a PEM encoded format for both public and private keys
  • PEM encoded Private key is encrypted with a password for further security and to align with snowflakes recommendations

To achieve this, I used the python cryptography package. It has all the necessary backends we need for PEM encoded keys and for applying our encrypted password. I found that some other common crypt packages did not expose configuration in their python classes that are available in the underlying OpenSSL binary. Our intended outputs from this process will be a password for our private key, an encrypted PEM encoded RSA key, and a PEM encoded public key.

Creating an Encrypted PEM Encoded RSA Key Pair

The first thing we will want to do is generate an RSA key pair with the python cryptography library. You are strongly recommended to use the defaults for this module for the security implications, but you may configure as you need and know the impact of those changes.

from cryptography.hazmat.primitives.asymmetric import rsa

private_key = rsa.generate_private_key(
    public_exponent=65537,
    key_size=2048
)

With your key pair object, you will then be able to encode it in your desired format. We will use the PEM encoding for our key pair and produce the required bytes for our PEM encoded public and private keys. Take note of us passing the bytes for our private key password when calling private_key.private_bytes.

from cryptography.hazmat.primitives import serialization

private_key_pass = b"your-password"

encrypted_pem_private_key = private_key.private_bytes(
    encoding=serialization.Encoding.PEM,
    format=serialization.PrivateFormat.PKCS8,
    encryption_algorithm=serialization.BestAvailableEncryption(private_key_pass)
)

print(encrypted_pem_private_key.splitlines()[0])
# b'-----BEGIN ENCRYPTED PRIVATE KEY-----'

pem_public_key = private_key.public_key().public_bytes(
  encoding=serialization.Encoding.PEM,
  format=serialization.PublicFormat.SubjectPublicKeyInfo
)

print(pem_public_key.splitlines()[0])
# b'-----BEGIN PUBLIC KEY-----'

If you do not require an encrypted private key file, you can always pass no encryption for the key like so.

from cryptography.hazmat.primitives import serialization

unencrypted_pem_private_key = private_key.private_bytes(
    encoding=serialization.Encoding.PEM,
    format=serialization.PrivateFormat.TraditionalOpenSSL,
    encryption_algorithm=serialization.NoEncryption()
)

print(unencrypted_pem_private_key.splitlines()[0])
# b'-----BEGIN RSA PRIVATE KEY-----'

With our PEM encoded public and private key bytes, we can generate them into strings if you wish to write to file or save them in another service.

private_key_file = open("example-rsa.pem", "w")
private_key_file.write(encrypted_pem_private_key.decode())
private_key_file.close()

public_key_file = open("example-rsa.pub", "w")
public_key_file.write(pem_public_key.decode())
public_key_file.close()

When we put all these pieces together, we get the following completed code example.

from cryptography.hazmat.primitives.asymmetric import rsa
from cryptography.hazmat.primitives import serialization

private_key = rsa.generate_private_key(
    public_exponent=65537,
    key_size=2048
)

private_key_pass = b"your-password"

encrypted_pem_private_key = private_key.private_bytes(
    encoding=serialization.Encoding.PEM,
    format=serialization.PrivateFormat.PKCS8,
    encryption_algorithm=serialization.BestAvailableEncryption(private_key_pass)
)

pem_public_key = private_key.public_key().public_bytes(
  encoding=serialization.Encoding.PEM,
  format=serialization.PublicFormat.SubjectPublicKeyInfo
)

private_key_file = open("example-rsa.pem", "w")
private_key_file.write(encrypted_pem_private_key.decode())
private_key_file.close()

public_key_file = open("example-rsa.pub", "w")
public_key_file.write(pem_public_key.decode())
public_key_file.close()

Further Reading

The documentation can answer most of your questions once you work out what objects must be passed at each stage of the key generation process. By intention, the library is very flexible with many modules to change things out for your use cases requirements from crypto backends to serialization formats.

With your generated private and public key bytes, you can create your keys in any encoding format you need for your consuming client. In the projects I have worked on, we have found that it is most helpful for us to store these keys in a standard format in a secret storage service like SSM as base64 encoded strings to preserve the formatting of the keys. This shifts the responsibility of formatting the key to the necessary format to the consumers of the key pair by them either writing the key to file or store it in other formats depending on their use case with a simple decode of the base 64 string from SSM. The flow for this process would look something like in the diagram below.

For the use cases for this process, some services require keys in this format. A notable example and what triggered me to look into this was snowflake's requirement for keys formatted in this way for key-pair authentication for snowflake service accounts in an automated key rotation process. However, snowflake does allow you to use non-encrypted key pairs with most of their SDK's now as well as supporting unencrypted keys too, although snowflake does still recommend using encrypted keys as best practice for their service. I do generally agree with them if you're looking for the most secure setup for your key pairs as it allows for another layer of protection for private keys saved on disk or fetched with the need for a password rather than just relying on file-based access control or your secret provider's security only.

Connect further

  • Read more articles over on my dev.to blog, or on my medium.
  • Feel free to send me a message on LinkedIn if you want to chat.
  • If you are interested in server components and hardware check out my store Server Labs Aus.

55