💡 Ejemplo completo funcionando disponible en GitHub: vault-transit-pdf-signing-python
Por qué la firma automatizada falla en una revisión de custodia de claves
Los equipos de cumplimiento y operaciones financieras firman documentos por miles: acuerdos de préstamo, estados de cuenta, paquetes de políticas, cartas de auditoría. Casi ninguno de esos se firma mediante una persona que haga clic en un botón. Un servicio lo hace, según un programa, y el certificado adjunto a esos documentos lleva el nombre de la organización. Eso convierte la clave de firma en una de las cosas más sensibles que posee la empresa, y suele estar en un archivo PFX junto a la aplicación que lo utiliza.
La firma externa es una capacidad de GroupDocs.Signature para Python vía .NET que pasa el resumen del documento al código que usted proporcione en lugar de firmar con una clave privada que la aplicación posee. Existe porque la configuración con PFX no sobrevive al escrutinio. Un archivo es una copia, y las copias se propagan: a la imagen del contenedor, la canalización de despliegue, la copia de seguridad, el portátil del desarrollador que depuró un problema de producción la primavera pasada. Cualquiera que posea una de esas copias y la contraseña puede firmar en su nombre hasta que alguien lo note y revoque el certificado.
Lo que estos equipos necesitan no es un formato de archivo diferente. Necesitan que la clave exista en un único lugar, con un registro de auditoría de cada vez que se use, y un servicio de firma que pueda solicitar una firma sin nunca confiar en la clave. Para eso sirve un gestor de claves como HashiCorp Vault, y solo ayuda si la biblioteca de documentos puede ser configurada para delegar la criptografía.
Por qué un archivo de certificado protegido con contraseña no es suficiente
El instinto es asegurar el PFX: restringir los permisos del archivo, almacenar la contraseña en un gestor de secretos, cifrar el volumen. Cada paso ayuda y ninguno cambia el hecho fundamental. En el momento de la firma, la clave privada se carga en el proceso, por lo que un volcado de memoria, una fuga del contenedor o una declaración de registro descuidada aún pueden exponerla. El control que se desea es una clave que no pueda leerse en absoluto, no una clave que sea más difícil de leer.
El motor Transit de Vault proporciona eso. Realiza la criptografía como un servicio: usted envía un valor, él firma o cifra con una clave que posee y devuelve el resultado. Las claves creadas dentro de él pueden marcarse como no exportables, y nunca almacena lo que recibe. GroupDocs.Signature cubre la otra mitad, porque DigitalSignOptions.custom_sign_hash le permite conectar su propio firmante al pipeline de firma de PDF.
Configuración: GroupDocs.Signature para Python con Vault Transit
GroupDocs.Signature hace todo hasta la criptografía. Abre el PDF, decide qué rango de bytes cubre la firma, calcula el hash de ese rango y llama a su firmante con el resumen. Su firmante entrega el resumen a Vault y devuelve los bytes que Vault produce. La biblioteca los incrusta junto con el certificado y escribe un PDF firmado normal.
Necesita Python 3.11 o 3.13, los paquetes groupdocs-signature-net y asn1crypto, y Docker para un contenedor Vault en modo desarrollo. Una licencia de GroupDocs es opcional; sin ella, el modo de evaluación acepta documentos de hasta 2 páginas. El repositorio de ejemplo se ejecuta de extremo a extremo con dos comandos, y el mismo código apunta a un Vault de producción cambiando dos variables de entorno.
Construyéndolo paso a paso
Paso 1 - Leer la clave de firma desde Vault
La aplicación solicita a Vault la clave que está autorizada a usar, y recibe un número de versión y una clave pública. La mitad privada nunca forma parte de la respuesta.
key = vault.read_key(KEY_NAME)
if key is None:
# First run: enable Transit and create an RSA-2048 key.
vault.enable_transit()
vault.create_key(KEY_NAME, "rsa-2048")
key = vault.read_key(KEY_NAME)
print(f"Signing key: '{KEY_NAME}', version {key.version}")
Leer la versión una vez es el objetivo de este paso. Un certificado contiene una clave pública, por lo que el certificado y cada firma deben provenir de la misma versión.
Paso 2 - Construir un certificado para una clave que nadie pueda exportar
Una firma PDF lleva un certificado X.509 para que un verificador pueda encontrar la clave pública. Con una clave no exportable, el certificado se ensambla alrededor de la mitad pública y es firmado por Vault.
tbs_certificate = x509.TbsCertificate({...}) # built around the Vault public key
# Hash locally, then have Vault sign the certificate's own digest.
digest = hashlib.sha256(tbs_certificate.dump()).digest()
signature = vault.sign_hash(key_name, key.version, digest, "sha2-256")
certificate = x509.Certificate({
"tbs_certificate": tbs_certificate,
"signature_algorithm": algorithm,
"signature_value": signature,
})
El certificado deliberadamente comienza un día antes que ahora. Un lector de PDF compara la hora de firma con esa fecha de inicio, y en una máquina dos horas al este de UTC un certificado emitido segundos antes de la firma fue reportado por Adobe Acrobat como expirado o aún no válido.
Paso 3 - Firmar el PDF mediante el firmante externo
certificate_object = DigitalVerifyOptions(
certificate_stream=io.BytesIO(certificate)).certificate
digital_signature = DigitalSignature(certificate=certificate_object)
digital_signature.sign_time = datetime.now().astimezone()
options = DigitalSignOptions()
options.signature = digital_signature
options.custom_sign_hash = VaultSigner(vault, KEY_NAME, key.version)
options.hash_algorithm = HashAlgorithm.SHA256
options.reason = "Document signed with HashiCorp Vault Transit"
with gs.Signature(str(input_pdf)) as signature:
signature.sign(str(output_pdf), options)
Dos líneas son específicas de Python. El enlace no tiene tipo X.509, por lo que el objeto certificado sale de DigitalVerifyOptions; pasar una ruta a DigitalSignOptions en su lugar genera el error “Specified certificate doesn’t contain private key”. Y sign_time es consciente de la zona horaria a propósito, porque un valor con desplazamiento se escribe en el PDF con él.
Dentro del firmante, el resumen se envía a Vault con prehashed = true y pkcs1v15. Esos dos campos deciden si la firma es válida, y el primero falla silenciosamente cuando falta.
Paso 4 - Verificar antes de que cualquier cosa salga del servicio
options = DigitalVerifyOptions(certificate_stream=io.BytesIO(certificate))
with gs.Signature(str(signed_pdf)) as signature:
is_valid = signature.verify(options).is_valid
Firmar y verificar en la misma ejecución convierte el modo de falla silenciosa en uno inmediato. Para una canalización de documentos, esto es la puerta: un archivo que no verifica nunca llega al archivo, a la contraparte o al portal del cliente.
Impacto empresarial: Qué cambia para la auditoría
| Preocupación | PFX en el host de la aplicación | Firma a través de Vault Transit |
|---|---|---|
| Copias de la clave privada | una por host, imagen, copia de seguridad y portátil | una, dentro de Vault |
| Derechos que necesita la aplicación de firma | acceso completo al archivo de clave | leer la clave, llamar a transit/sign |
| Evidencia de que un documento fue firmado | registros de la aplicación, si alguien los añadió | una entrada de auditoría de Vault por firma |
| Respuesta a una fuga sospechada | revocar, volver a emitir, redeplegar en todas partes | rotar la clave en Vault, volver a emitir una vez |
| Quién puede firmar en tu nombre | cualquiera con el archivo y la contraseña | cualquiera que posea un token válido de Vault |
La última fila es la honesta. Este diseño mueve el secreto en lugar de eliminarlo: un token de Vault robado aún puede solicitar firmas. La diferencia es que un token es de corta duración, revocable en un solo lugar, limitado a dos operaciones, y cada uso se registra. Eso es algo sustancialmente más fácil de defender en una revisión que un archivo cuyas copias nadie puede enumerar.
Flujo real: un acuerdo de préstamo firmado por un servicio
Una plataforma de préstamos genera un PDF de acuerdo cuando se aprueba una solicitud, lo firma y lo deposita en la bóveda de documentos del cliente. Antes de este cambio, el servicio de firma tenía el certificado de la empresa y el manual de despliegue incluía un paso para copiar el PFX a los nuevos hosts. Después, el servicio posee un token AppRole con dos permisos, y la clave privada del certificado nunca ha existido en ningún lugar excepto en Vault.
Cuando los auditores preguntan cuántos documentos se firmaron el último trimestre y por quién, la respuesta proviene del dispositivo de auditoría de Vault en lugar de reconstruir los registros de la aplicación. Cuando el equipo rota la clave, la rotan en un solo lugar y emiten un nuevo certificado para la nueva versión.
¿Qué debería acertar primero?
El manejo del resumen. Un nombre de algoritmo incorrecto falla de forma ruidosa, con Vault respondiendo 500 crypto/rsa: input must be hashed message, y cuesta un minuto. Falta la bandera prehashed no produce ningún error: Vault vuelve a hacer hash del resumen, la firma cubre el valor incorrecto y el PDF parece terminado. Verifique cada archivo que firme y ese fallo dejará de ser invisible.
Otras áreas donde este diseño encaja
El gancho no tiene idea de qué hay detrás, por lo que los mismos cuatro pasos funcionan contra un módulo de seguridad de hardware, un KMS en la nube o una tarjeta inteligente. Porté esta construcción desde su hermano .NET esperando que el firmante necesitara rehacerlo, y la única parte que realmente cambió fue la entrega del certificado; el cuerpo de la solicitud a Vault y el anclaje de versión fueron idénticos. Los equipos en salud, gobierno y seguros lo adoptan por la misma razón que lo hace finanzas: la clave debe estar en un lugar que pueda demostrar para qué se utilizó.
Comenzando
- Comienza gratis: obtener una licencia de prueba, acceso completo a la API
- Leer la guía de integración: firmar PDFs con una clave Vault Transit en Python
- Explorar el código: vault-transit-pdf-signing-python
- Hacer preguntas de implementación en el foro de la comunidad