Enhancing Security through Certificates, Envelope Encryption and Key Rotation
Published: 2023-07-28 19:39:29

## What is Key Rotation?

Key rotation is the process of regularly updating cryptographic keys used in various encryption schemes. The primary goal of key rotation is to reduce the reuse of the encryption keys, which reduces the window of potential for clever attackers to compromise sensitive data by collecting a sufficient volume of encrypted data such that a pattern can be deduced/noticed as a byproduct of key reuse. 

Frequent rotations ensure that the data encrypted with the compromised key remains insecure only for a limited period, as the compromised key would be retired and replaced with a new one, limiting the amount of data that can be recovered. This significantly enhances the organization's security posture, making it harder for malicious actors to gain unauthorized access to critical information.

## Certificates for Secure Key Management

Certificates play a fundamental role in implementing secure key rotation strategies. A certificate is a digital document issued by a Certificate Authority (CA) that binds a public key to a specific identity, such as a domain or an individual. This public key can be used by anyone to encrypt data such that only that identity is able to decrypt it (using its private part of the key). They are commonly used in public key infrastructure (PKI) to establish trust and enable secure communication over networks.

When it comes to key rotation, certificates enable the secure distribution of new cryptographic keys. By using certificates, organizations can ensure that only authorized parties have access to the new keys, preventing unauthorized access during the rotation process. Additionally, certificates often come with an expiration date, which can be used to provide some form of automatic rotation of keys after a specified period. (This may not be directly enforced by the system as it might be considered too intrusive and in cases of systems such as databases that may have transparent data encryption, it will no longer be **transparent**)

Certificate-based key rotation can be implemented for securing data transmission, securing SSL/TLS connections, and facilitating secure communication between various components of the an organization's infrastructure.

## Envelope Encryption for Enhanced Security

Envelope encryption is a technique that combines both asymmetric and symmetric encryption to protect sensitive data. In this approach, a data encryption key (DEK) is used to encrypt the data, and then the DEK itself is encrypted using a master key called the key encryption key (KEK). The encrypted DEK, along with the necessary metadata, is referred to as the "envelope."

This approach offers several advantages:

1. **Granular Access Control:** By separating the DEK from the KEK, you can control access to the data on a per-user or per-group basis. The KEK can be stored in a more secure location, while the DEK can be accessible only to authorized users.

2. **Efficient Key Rotation:** Envelope encryption facilitates easy key rotation. When the KEK needs to be rotated, only the DEK needs to be re-encrypted with the new KEK, making the process less resource-intensive compared to re-encrypting the entire dataset.

3. **Compliance and Auditing:** Envelope encryption enables clear separation of encryption responsibilities, which is beneficial for compliance and auditing purposes. Different teams can manage the DEKs and KEKs, ensuring strict adherence to security policies.

## Python Code Sample for Encryption using Certificates

The following is an implementation of key rotation using certificate-based encryption. For this example, we'll use the `cryptography` library in Python, which provides powerful tools for encryption and key management.

```python
# Import required libraries
import logging
import datetime
from cryptography import x509
from cryptography.hazmat.primitives import serialization
from cryptography.hazmat.primitives.asymmetric import padding
from cryptography.hazmat.primitives.hashes import SHA256
from cryptography.x509.oid import NameOID
from cryptography.hazmat.primitives.asymmetric import rsa

# Configure logging
logging.basicConfig(level=logging.INFO, format="%(asctime)s - %(levelname)s - %(message)s")
logger = logging.getLogger(__name__)

def generate_key_pair():
    private_key = rsa.generate_private_key(
        public_exponent=65537,
        key_size=2048
    )
    return private_key

def save_private_key(private_key, filename):
    pem = private_key.private_bytes(
        encoding=serialization.Encoding.PEM,
        format=serialization.PrivateFormat.PKCS8,
        encryption_algorithm=serialization.NoEncryption()
    )
    with open(filename, 'wb') as f:
        f.write(pem)

def load_private_key(filename):
    with open(filename, "rb") as f:
        private_key = serialization.load_pem_private_key(
            f.read(),
            password=None
        )
    return private_key

def generate_certificate(private_key, subject_name):
    builder = x509.CertificateBuilder()
    builder = builder.subject_name(x509.Name([x509.NameAttribute(NameOID.COMMON_NAME, subject_name)]))
    builder = builder.issuer_name(x509.Name([x509.NameAttribute(NameOID.COMMON_NAME, subject_name)]))
    builder = builder.not_valid_before(datetime.datetime.utcnow())
    builder = builder.not_valid_after(datetime.datetime.utcnow() + datetime.timedelta(days=365))
    builder = builder.serial_number(x509.random_serial_number())
    builder = builder.public_key(private_key.public_key())
    builder = builder.sign(private_key, SHA256())

    return builder

def encrypt_data(data, public_key):
    encrypted_data = public_key.encrypt(
        data,
        padding.OAEP(
            mgf=padding.MGF1(algorithm=SHA256()),
            algorithm=SHA256(),
            label=None
        )
    )
    return encrypted_data

def decrypt_data(encrypted_data, private_key):
    decrypted_data = private_key.decrypt(
        encrypted_data,
        padding.OAEP(
            mgf=padding.MGF1(algorithm=SHA256()),
            algorithm=SHA256(),
            label=None
        )
    )
    return decrypted_data

def main():
    try:
        # Generate the initial RSA key pair
        initial_private_key = generate_key_pair()
        save_private_key(initial_private_key, "initial_private_key.pem")
        logger.info("Initial RSA key pair generated and saved.")

        # Generate a self-signed certificate with the initial private key
        initial_cert = generate_certificate(initial_private_key, "My Key Rotation Certificate")
        with open("initial_certificate.pem", "wb") as f:
            f.write(initial_cert.public_bytes(serialization.Encoding.PEM))
        logger.info("Initial certificate generated and saved.")

        # Load the initial certificate and private key
        with open("initial_certificate.pem", "rb") as f:
            initial_cert = x509.load_pem_x509_certificate(f.read())
        initial_private_key = load_private_key("initial_private_key.pem")
        logger.info("Initial certificate and private key loaded.")

        # Generate a new RSA key pair for the new certificate
        new_private_key = generate_key_pair()
        save_private_key(new_private_key, "new_private_key.pem")
        logger.info("New RSA key pair generated and saved.")

        # Generate a self-signed certificate with the new private key
        new_cert = generate_certificate(new_private_key, "My New Key Rotation Certificate")
        with open("new_certificate.pem", "wb") as f:
            f.write(new_cert.public_bytes(serialization.Encoding.PEM))
        logger.info("New certificate generated and saved.")

        # Load the new certificate and private key
        with open("new_certificate.pem", "rb") as f:
            new_cert = x509.load_pem_x509_certificate(f.read())
        new_private_key = load_private_key("new_private_key.pem")
        logger.info("New certificate and private key loaded.")

        # Data to be encrypted and rotated
        data_to_encrypt = b"Sensitive data that needs to be protected!"

        # Encrypt the data using the initial public key
        initial_public_key = initial_cert.public_key()
        encrypted_data = encrypt_data(data_to_encrypt, initial_public_key)
        logger.info("Data encrypted with the initial public key.")
        logger.info(f"Encrypted Data: {encrypted_data}")

        # Sanity check: Decrypt the data back to its original form using the initial private key
        decrypted_data = decrypt_data(encrypted_data, initial_private_key)
        logger.info("Data decrypted back to its original form.")
        print(f"Decrypted Data: {decrypted_data}")

        # Perform key rotation - re-encrypt the data using the new public key
        new_public_key = new_cert.public_key()
        re_encrypted_data = encrypt_data(decrypted_data, new_public_key)
        logger.info("Data re-encrypted with the new public key.")

        # Decrypt the data back to its original form using the initial private key
        re_decrypted_data = decrypt_data(re_encrypted_data, new_private_key)
        logger.info("Data decrypted back to its original form.")

        print("Data Encryption Key Rotation Successful!")
        print(f"Original Data: {data_to_encrypt}")
        print(f"Decrypted Data: {re_decrypted_data}")

    except Exception as e:
        logger.error(f"An error occurred during key rotation: {e}")

if __name__ == "__main__":
    main()
```

### Code breakdown

1. **Generate Initial RSA Key Pair**: The process begins by generating the initial RSA key pair, which includes a private key and a corresponding public key. The private key will be used for data decryption, while the public key will be used for encryption.
2. **Save Initial Private Key**: The generated initial private key is saved to a file named `initial_private_key.pem`.
3. **Generate Initial Certificate**: The script generates a self-signed X.509 certificate using the initial private key. This certificate is created with a common name "My Key Rotation Certificate" and is valid for one year.
4. **Save Initial Certificate**: The generated initial certificate is saved to a file named `initial_certificate.pem`.
5. **Load Initial Certificate and Private Key**: The initial certificate and private key are loaded from their respective files.
6. **Generate New RSA Key Pair**: The script generates a new RSA key pair, just like in the first step, to prepare for the key rotation.
7. **Save New Private Key**: The new private key is saved to a file named `new_private_key.pem`.
8. **Generate New Certificate**: Similar to the initial certificate, the script generates a self-signed X.509 certificate using the new private key, with the common name "My New Key Rotation Certificate" and a one-year validity period.
9. **Save New Certificate**: The generated new certificate is saved to a file named `new_certificate.pem`.
10. **Load New Certificate and Private Key**: The new certificate and private key are loaded from their respective files.
11. **Data Encryption**: To simulate the key rotation process, the script encrypts some sample data using the initial public key (from the initial certificate).
12. **Data Decryption Check**: The encrypted data is then decrypted back to its original form using the initial private key. This step serves as a sanity check to ensure successful encryption and decryption using the initial key pair.
13. **Data Re-Encryption**: After the sanity check, the script proceeds to perform key rotation. The decrypted data from the previous step is re-encrypted using the new public key (from the new certificate).
14. **Data Re-Decryption Check**: Finally, the re-encrypted data is decrypted back to its original form using the new private key. This ensures that the data can be successfully decrypted using the new key pair after key rotation.



## Envelope encryption

Envelope encryption is a powerful and widely used technique in the field of data security. It addresses the challenge of securely encrypting large amounts of data by combining the benefits of symmetric and asymmetric encryption. The process begins by generating a random symmetric data encryption key, often referred to as the "data key." 

This data key is used to encrypt the actual data, providing fast and efficient encryption. However, to protect the data key itself, it is encrypted using a strong asymmetric encryption algorithm, typically RSA. 

The encrypted data key, together with the encrypted data, forms the "envelope." This envelope can be securely stored or transmitted, and only authorized parties possessing the private key can decrypt the data key to access the encrypted data. The use of symmetric encryption for data and asymmetric encryption for the data key ensures a balance between performance and security, making envelope encryption an effective solution for safeguarding sensitive information in various scenarios, such as cloud storage, data sharing, and secure communications.

### Python Code Sample for Envelope encryption

```python
import os
import logging
from cryptography.hazmat.primitives import serialization
from cryptography.hazmat.primitives.hashes import SHA256
from cryptography.hazmat.primitives.asymmetric import rsa, padding
from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes
from cryptography.hazmat.backends import default_backend

# Configure logging
logging.basicConfig(level=logging.INFO, format="%(asctime)s - %(levelname)s - %(message)s")
logger = logging.getLogger(__name__)

def generate_rsa_key_pair():
    private_key = rsa.generate_private_key(
        public_exponent=65537,
        key_size=2048
    )
    public_key = private_key.public_key()
    return private_key, public_key

def encrypt_data(data, public_key):
    try:
        encrypted_data_key = os.urandom(32)  # 256-bit symmetric key for data encryption
        cipher_rsa = public_key.encrypt(
            encrypted_data_key,
            padding.OAEP(
                mgf=padding.MGF1(algorithm=SHA256()),
                algorithm=SHA256(),
                label=None
            )
        )

        # Encrypt the actual data using AES-GCM
        iv = os.urandom(12)  # 96-bit Initialization Vector for AES-GCM
        cipher = Cipher(algorithms.AES(encrypted_data_key), modes.GCM(iv), backend=default_backend())
        encryptor = cipher.encryptor()
        encryptor.authenticate_additional_data(b"AuthenticatedData")  # Optional: Authenticates additional data
        encrypted_data = encryptor.update(data) + encryptor.finalize()

        return cipher_rsa, iv, encryptor.tag, encrypted_data

    except Exception as e:
        logger.error(f"Error occurred during data encryption: {e}")
        raise

def decrypt_data(cipher_rsa, iv, tag, encrypted_data, private_key):
    try:
        decrypted_data_key = private_key.decrypt(
            cipher_rsa,
            padding.OAEP(
                mgf=padding.MGF1(algorithm=SHA256()),
                algorithm=SHA256(),
                label=None
            )
        )

        # Decrypt the actual data using AES-GCM
        cipher = Cipher(algorithms.AES(decrypted_data_key), modes.GCM(iv, tag), backend=default_backend())
        decryptor = cipher.decryptor()
        decryptor.authenticate_additional_data(b"AuthenticatedData")  # Optional: Authenticates additional data
        decrypted_data = decryptor.update(encrypted_data) + decryptor.finalize()

        return decrypted_data

    except Exception as e:
        logger.error(f"Error occurred during data decryption: {e}")
        raise

# Driver program
if __name__ == "__main__":
    try:
        data_to_encrypt = b"Sensitive data that needs to be protected!"

        # Generate RSA key pair
        private_key, public_key = generate_rsa_key_pair()

        # Encrypt the data
        cipher_rsa, iv, tag, encrypted_data = encrypt_data(data_to_encrypt, public_key)
        logger.info("Data encrypted successfully.")

        # Decrypt the data
        decrypted_data = decrypt_data(cipher_rsa, iv, tag, encrypted_data, private_key)
        logger.info("Data decrypted successfully.")
        logger.info(f"Original Data: {data_to_encrypt}")
        logger.info(f"Decrypted Data: {decrypted_data}")

    except Exception as e:
        logger.error(f"An error occurred during envelope encryption/decryption: {e}")

```



The part that uses envelope encryption in the code is the `encrypt_data` function. This function performs the envelope encryption process, where the data is encrypted using AES-GCM with a symmetric key, and then this symmetric key is itself encrypted using RSA encryption with the public key.



## References

[How to Manage Encryption at Scale with Envelope Encryption & Key Management Systems (freecodecamp.org)](https://www.freecodecamp.org/news/envelope-encryption/)

[amazon web services - How exactly does encryption key rotation work? - Stack Overflow](https://stackoverflow.com/questions/67207650/how-exactly-does-encryption-key-rotation-work)

[envelope · PyPI](https://pypi.org/project/envelope/)

[Fernet (symmetric encryption) — Cryptography 42.0.0.dev1 documentation](https://cryptography.io/en/latest/fernet/)

[X.509 — Cryptography 42.0.0.dev1 documentation](https://cryptography.io/en/latest/x509/)