🚧 Add CA and server cert management
This commit is contained in:
@@ -0,0 +1,59 @@
|
||||
"""Key encryption at rest (spec sections 43-44).
|
||||
|
||||
Private keys that the platform must keep (CA and server keys) are stored
|
||||
in PostgreSQL ONLY in encrypted form, using AES-256-GCM authenticated
|
||||
encryption. The master key comes from the SECRET_ENCRYPTION_KEY
|
||||
environment variable (Docker secret in production) and never touches
|
||||
the database.
|
||||
|
||||
Wire format: "v1:<nonce-hex>:<ciphertext-hex>" — the version prefix
|
||||
keeps the format evolvable without breaking existing rows.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import os
|
||||
|
||||
from cryptography.hazmat.primitives.ciphers.aead import AESGCM
|
||||
|
||||
from app.config import get_settings
|
||||
|
||||
_FORMAT = "v1"
|
||||
|
||||
|
||||
def _master_key() -> bytes:
|
||||
"""Derive the 32-byte AES key from the configured secret.
|
||||
|
||||
The secret is hashed to exactly 32 bytes (AES-256) regardless of the
|
||||
length the operator chose for SECRET_ENCRYPTION_KEY.
|
||||
"""
|
||||
import hashlib
|
||||
|
||||
secret = get_settings().secret_encryption_key.encode("utf-8")
|
||||
return hashlib.sha256(secret).digest()
|
||||
|
||||
|
||||
def encrypt_secret(plaintext: str) -> str:
|
||||
"""Encrypt a PEM string into the storage format."""
|
||||
nonce = os.urandom(12) # 96-bit nonce, GCM standard
|
||||
aesgcm = AESGCM(_master_key())
|
||||
ciphertext = aesgcm.encrypt(nonce, plaintext.encode("utf-8"), None)
|
||||
return f"{_FORMAT}:{nonce.hex()}:{ciphertext.hex()}"
|
||||
|
||||
|
||||
def decrypt_secret(blob: str) -> str:
|
||||
"""Decrypt a stored secret back to plaintext PEM."""
|
||||
try:
|
||||
version, nonce_hex, ciphertext_hex = blob.split(":", 2)
|
||||
except ValueError as exc:
|
||||
raise ValueError("corrupt secret blob: bad format") from exc
|
||||
if version != _FORMAT:
|
||||
raise ValueError(f"unsupported secret blob version: {version}")
|
||||
aesgcm = AESGCM(_master_key())
|
||||
try:
|
||||
plaintext = aesgcm.decrypt(
|
||||
bytes.fromhex(nonce_hex), bytes.fromhex(ciphertext_hex), None
|
||||
)
|
||||
except Exception as exc:
|
||||
raise ValueError("secret decryption failed (wrong master key?)") from exc
|
||||
return plaintext.decode("utf-8")
|
||||
Reference in New Issue
Block a user