Back to Blog
Python

Python Cryptography: RSA Public and Private Keys

python cryptography rsa public private keys: Learn to generate, serialize, and use RSA public and private keys in Python with the cryptography library, including encry...

cryptographyRSApublic keyprivate keyencryptionsigning
Illustration of RSA public and private key pair with encryption and decryption flow in Python.

python cryptography rsa public private keys requires a clear understanding of the core syntax, runtime behavior, and practical implementation patterns demonstrated in the examples below.

When you need to protect data in transit or at rest, asymmetric encryption with RSA is a common choice. In Python, the cryptography library provides a well-tested API for generating and using RSA public and private keys. This article walks through the core operations: key generation, serialization, encryption, decryption, signing, and verification, along with the security tradeoffs you need to consider.

Generating an RSA Key Pair

The first step is to generate a private key. The cryptography library uses the rsa.generate_private_key() function, which requires you to specify the public exponent and the key size in bits. A key size of 2048 bits is the current minimum for secure RSA; 3072 or 4096 are recommended for long-term security.

from cryptography.hazmat.primitives.asymmetric import rsa private_key = rsa.generate_private_key( public_exponent=65537, key_size=2048, )

The public exponent 65537 is a Fermat prime and is the standard choice for compatibility and performance. The generated private_key object contains both the private and public components. To obtain the public key, call public_key() on the private key object:

public_key = private_key.public_key()

This public key can be shared freely, while the private key must be kept secret. The private_key object also provides methods for signing and decryption, while the public_key object handles verification and encryption.

Serializing Keys to PEM Format

For practical use, you need to store keys in a standard format. PEM is the most widely used encoding for RSA keys. The cryptography library can serialize keys to PEM using the private_bytes() and public_bytes() methods.

For the private key, you typically use PKCS#8 format with a suitable encryption algorithm. Here is how to serialize the private key without encryption (not recommended for production) and with a password:

from cryptography.hazmat.primitives import serialization # Unencrypted PKCS#8 PEM private_pem = private_key.private_bytes( encoding=serialization.Encoding.PEM, format=serialization.PrivateFormat.PKCS8, encryption_algorithm=serialization.NoEncryption(), ) # Encrypted with a password password = b"my-secure-password" private_pem_encrypted = private_key.private_bytes( encoding=serialization.Encoding.PEM, format=serialization.PrivateFormat.PKCS8, encryption_algorithm=serialization.BestAvailableEncryption(password), )

The public key is serialized using the SubjectPublicKeyInfo format:

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

These PEM strings can be written to files or stored in a configuration system. Always protect the private key with a strong password when stored at rest.

Loading Keys from PEM Files

To use a key that was previously serialized, you load it with load_pem_private_key() or load_pem_public_key(). The private key loader requires the password if the key was encrypted:

from cryptography.hazmat.primitives import serialization # Load unencrypted private key with open("private_key.pem", "rb") as f: private_key = serialization.load_pem_private_key(f.read(), password=None) # Load encrypted private key with open("private_key_encrypted.pem", "rb") as f: private_key = serialization.load_pem_private_key(f.read(), password=b"my-secure-password") # Load public key with open("public_key.pem", "rb") as f: public_key = serialization.load_pem_public_key(f.read())

Note that the loaded key objects are of the same type as those generated directly, so you can use them for encryption, decryption, signing, and verification without any additional steps.

Encrypting and Decrypting Data

RSA can only encrypt data that is smaller than the key size minus padding overhead. For a 2048-bit key with OAEP padding, the maximum message length is 190 bytes. For larger data, you should use hybrid encryption: encrypt a symmetric key with RSA and use that key with AES to encrypt the actual data. The cryptography library provides OAEP padding as the recommended scheme.

Encrypt with the public key:

from cryptography.hazmat.primitives.asymmetric import padding message = b"A secret message" ciphertext = public_key.encrypt( message, padding.OAEP( mgf=padding.MGF1(algorithm=hashes.SHA256()), algorithm=hashes.SHA256(), label=None ) )

Decrypt with the private key:

plaintext = private_key.decrypt( ciphertext, padding.OAEP( mgf=padding.MGF1(algorithm=hashes.SHA256()), algorithm=hashes.SHA256(), label=None ) )

OAEP is semantically secure and should be used instead of the older PKCS#1 v1.5 padding. The MGF1 and algorithm parameters must match between encryption and decryption. If you use different hash functions, decryption will fail.

Signing and Verifying Messages

RSA is also used for digital signatures. The private key signs a message, and the public key verifies the signature. This ensures authenticity and integrity. The cryptography library uses the sign() and verify() methods with a padding scheme such as PSS.

from cryptography.hazmat.primitives import hashes from cryptography.hazmat.primitives.asymmetric import padding message = b"Important data" # Sign with private key signature = private_key.sign( message, padding.PSS( mgf=padding.MGF1(hashes.SHA256()), salt_length=padding.PSS.MAX_LENGTH ), hashes.SHA256() ) # Verify with public key try: public_key.verify( signature, message, padding.PSS( mgf=padding.MGF1(hashes.SHA256()), salt_length=padding.PSS.MAX_LENGTH ), hashes.SHA256() ) print("Signature is valid") except InvalidSignature: print("Signature is invalid")

The InvalidSignature exception is raised when the signature does not match the message and key. Always use PSS padding for new systems; it is more secure than PKCS#1 v1.5 signatures.

Key Size and Security Considerations

The security of RSA depends heavily on the key size. As of 2025, 2048-bit keys are considered acceptable for most applications, but 3072-bit or 4096-bit keys provide a larger security margin. Larger keys also increase CPU usage and reduce performance, so choose the smallest size that meets your security requirements.

RSA is computationally expensive compared to symmetric algorithms. If you need to encrypt large volumes of data, use hybrid encryption. For signatures, consider using Ed25519 or ECDSA, which are faster and produce smaller signatures. RSA is still widely used for compatibility with existing systems and protocols.

Another critical concern is key management. Never hardcode private keys in source code. Use environment variables, secure vaults, or key management services. Rotate keys regularly and revoke compromised keys immediately.

Handling Common Errors and Edge Cases

Several errors are common when working with RSA in Python. The ValueError is raised when you try to encrypt data that is too large for the key size. For example, a 2048-bit key with OAEP-SHA256 can encrypt at most 190 bytes. If you need to encrypt more, use hybrid encryption.

Another frequent issue is mismatched padding parameters. If the algorithm or mgf hash differs between encryption and decryption, you will get a ValueError during decryption. Always define the padding as a constant to avoid drift.

When loading keys, a TypeError occurs if you pass a password to an unencrypted key, or if you omit the password for an encrypted key. Ensure that the password is a bytes object, not a string.

Finally, be careful with key serialization formats. The PrivateFormat.PKCS8 is the modern standard, while TraditionalOpenSSL is legacy. For public keys, SubjectPublicKeyInfo is the standard. Using the wrong format can cause interoperability issues with other systems.

When to Choose RSA Over Other Algorithms

RSA is not the only asymmetric algorithm. For new systems, Elliptic Curve Cryptography (ECC) offers smaller keys and better performance. However, RSA is still the right choice when you need to interoperate with legacy systems, comply with standards that mandate RSA, or when you need to use hardware security modules that only support RSA.

If you are starting a new project and have no external constraints, consider using Ed25519 for signatures and X25519 for key exchange. These algorithms are faster, produce smaller keys, and are easier to use correctly. But if you must use RSA, the cryptography library provides a robust and secure implementation that follows current best practices.

python cryptography rsa public private keys: Practical Usage | RYUSLOG DEV