"""Pruebas del cifrado de secretos y de la validación del CSD. Nada sale a la red ni toca MinIO: la validación del par ``.cer``/``.key`` es criptografía pura, y es justo la parte que importa comprobar. """ import datetime import pytest from cryptography import x509 from cryptography.fernet import Fernet from cryptography.hazmat.primitives import hashes, serialization from cryptography.hazmat.primitives.asymmetric import rsa from cryptography.x509.oid import NameOID from fastapi import HTTPException from api.v1.modules.fin.issuer import csd_service from core import crypto CERT_NUMBER = "00001000000700000001" PASSWORD = "12345678a" def _par(cert_number: str = CERT_NUMBER, password: str = PASSWORD): """Genera un CSD de juguete con el formato del SAT: (cer_der, key_der_cifrada).""" key = rsa.generate_private_key(public_exponent=65537, key_size=2048) nombre = x509.Name([x509.NameAttribute(NameOID.COMMON_NAME, "EMISOR DE PRUEBA")]) cert = ( x509.CertificateBuilder() .subject_name(nombre) .issuer_name(nombre) .public_key(key.public_key()) .serial_number(int.from_bytes(cert_number.encode("ascii"), "big")) .not_valid_before(datetime.datetime(2026, 1, 1)) .not_valid_after(datetime.datetime(2035, 1, 1)) .sign(key, hashes.SHA256()) ) return ( cert.public_bytes(serialization.Encoding.DER), key.private_bytes( serialization.Encoding.DER, serialization.PrivateFormat.PKCS8, serialization.BestAvailableEncryption(password.encode()), ), ) # --------------------------------------------------------------------------------------- # Cifrado de secretos # --------------------------------------------------------------------------------------- @pytest.fixture def clave_maestra(monkeypatch): clave = Fernet.generate_key().decode() monkeypatch.setattr(crypto.settings, "CSD_ENCRYPTION_KEY", clave, raising=False) return clave def test_cifrar_y_recuperar(clave_maestra): token = crypto.encrypt_secret(PASSWORD) assert token != PASSWORD, "el secreto no puede quedar en claro" assert crypto.decrypt_secret(token) == PASSWORD def test_dos_cifrados_del_mismo_valor_no_son_iguales(clave_maestra): """Fernet incluye IV y timestamp: dos tokens distintos para el mismo dato. Importa porque si fueran iguales, comparar columnas revelaría qué empresas comparten contraseña. """ assert crypto.encrypt_secret(PASSWORD) != crypto.encrypt_secret(PASSWORD) def test_sin_clave_maestra_no_se_cifra(monkeypatch): monkeypatch.setattr(crypto.settings, "CSD_ENCRYPTION_KEY", "", raising=False) assert crypto.secrets_available() is False with pytest.raises(crypto.SecretsNotConfigured): crypto.encrypt_secret(PASSWORD) def test_clave_maestra_invalida_se_detecta(monkeypatch): monkeypatch.setattr(crypto.settings, "CSD_ENCRYPTION_KEY", "no-es-una-clave", raising=False) with pytest.raises(crypto.SecretsNotConfigured): crypto.encrypt_secret(PASSWORD) def test_otra_clave_maestra_no_descifra(clave_maestra, monkeypatch): """Rotar la clave maestra deja ilegibles los secretos: tiene que decirlo, no romperse raro.""" token = crypto.encrypt_secret(PASSWORD) monkeypatch.setattr( crypto.settings, "CSD_ENCRYPTION_KEY", Fernet.generate_key().decode(), raising=False ) with pytest.raises(crypto.SecretDecryptionError): crypto.decrypt_secret(token) def test_no_se_cifra_un_valor_vacio(clave_maestra): with pytest.raises(ValueError): crypto.encrypt_secret("") # --------------------------------------------------------------------------------------- # Validación del par .cer / .key # --------------------------------------------------------------------------------------- def test_par_correcto_devuelve_numero_de_certificado(): cer, key = _par() assert csd_service._verify_pair(cer, key, PASSWORD) == CERT_NUMBER def test_key_de_otro_certificado_se_rechaza(): """El caso que motiva la validación: dos CSD mezclados. Sin esto, el error aparecería hasta que el PAC rechace el comprobante, con un mensaje que no menciona el certificado. """ cer, _ = _par() _, key_ajena = _par(cert_number="00001000000700000002") with pytest.raises(HTTPException) as exc: csd_service._verify_pair(cer, key_ajena, PASSWORD) assert exc.value.status_code == 422 assert "no corresponde" in str(exc.value.detail) def test_contrasena_incorrecta_se_rechaza(): cer, key = _par() with pytest.raises(HTTPException) as exc: csd_service._verify_pair(cer, key, "incorrecta") assert exc.value.status_code == 422 def test_certificado_que_no_es_x509_se_rechaza(): _, key = _par() with pytest.raises(HTTPException) as exc: csd_service._verify_pair(b"esto no es un certificado", key, PASSWORD) assert exc.value.status_code == 422 def test_llave_que_no_es_una_llave_se_rechaza(): cer, _ = _par() with pytest.raises(HTTPException) as exc: csd_service._verify_pair(cer, b"esto no es una llave", PASSWORD) assert exc.value.status_code == 422