💡 Ejemplo completo disponible en GitHub:
sign-docx-with-mldsa-certificates-python
Introducción
Firma un contrato esta tarde con RSA-2048 y habrás hecho una promesa que debe mantenerse mientras el contrato sea relevante. Si eso es durante veinte o treinta años —y para escrituras, formularios de consentimiento y aprobaciones de ingeniería a menudo lo es— la promesa debe superar al algoritmo. El ataque no necesita existir hoy. Necesita existir antes de que el documento deje de importar, y entonces cualquiera que posea la clave pública podrá derivar la privada y firmar en tu nombre.
La firma de documentos post‑cuántica es la característica de GroupDocs.Signature para Python que reemplaza esa promesa por una basada en ML‑DSA, el algoritmo de firma estandarizado por NIST como FIPS 204 en 2024. El soporte para formato Word llegó en GroupDocs.Signature 26.9, y reutiliza la API que ya tienes: una clave ML‑DSA vive en un PFX y se pasa a DigitalSignOptions exactamente como una clave RSA.
Esta guía firma un DOCX en cuatro pasos, compara los tres niveles de seguridad en la salida medida, verifica la firma solo con un certificado público y termina con los dos límites que vale la pena conocer antes de comprometerse.
Por qué esto importa más que la migración habitual
La migración de firmas es diferente de la migración de cifrado en un aspecto que la hace más fácil de posponer y más incómoda de arreglar.
Con el cifrado, el problema de “cosechar‑ahora‑descifrar‑después” es inmediato: cualquier cosa interceptada hoy puede almacenarse y abrirse más tarde. Con las firmas, nada de lo que ya has firmado se vuelve falsificable retroactivamente —pero tampoco nada de lo que firmaste sigue siendo probadamente tuyo, una vez que la clave puede derivarse del certificado del que todos tienen una copia. Volver a firmar una década de documentos archivados con nuevas claves es posible y nadie quiere ser la persona que lo planifique.
Por eso el consejo práctico es estrecho en lugar de amplio: migra los documentos cuya retención sea larga, deja el resto. Algunos perfiles ya han fijado la barra —CNSA 2.0 requiere ML‑DSA‑87 para sistemas de seguridad nacional— y para todos los demás el factor decisivo es cuánto tiempo debe permanecer defendible el archivo.
Requisitos previos
- Python 3.9 o posterior en un intérprete de 64 bits —el paquete incluye un runtime .NET empaquetado y no tiene rueda de 32 bits.
- GroupDocs.Signature para Python vía .NET 26.10.0, con una licencia temporal gratuita para eliminar los límites de evaluación.
- Un certificado ML‑DSA como PFX protegido con contraseña, y un documento Word para firmar.
Instalación
pip install groupdocs-signature-net
Paso 1 - Firmar con un certificado ML‑DSA
El certificado hace el trabajo. La llamada es la que escribirías para RSA:
with signature.Signature(source_path) as sign:
options = DigitalSignOptions(pfx_path)
options.password = CERTIFICATE_PASSWORD
result = sign.sign(output_path, options)
Esa es toda la historia de adopción para código que ya firma: apunta DigitalSignOptions a un PFX diferente. No hay nueva opción, ni parámetro de algoritmo separado, ni rama para post‑cuántico.
Leer el firmante de nuevo requiere un paso más, y contiene la única trampa específica de Python en todo este ejercicio:
for created in result.succeeded:
certificate = getattr(created, "certificate", None)
subject = getattr(certificate, "subject", None)
if subject:
return str(subject)
El certificado en un DigitalSignature es un objeto puente que resuelve atributos dinámicamente. certificate.subject devuelve CN=GroupDocs.Signature MLDSA65 test, mientras que dir() sobre ese mismo objeto no lista nada. Lo inspeccioné con dir() primero, concluí que el sujeto no estaba expuesto y estaba simplemente equivocado —así que si introspectas antes de leer, omitirás un valor que sí está allí.
Paso 2 - Comparar los tres niveles de seguridad
ML‑DSA viene en tres conjuntos de parámetros, y se seleccionan entregando un certificado diferente:
levels = (
("ML-DSA-44", MLDSA44_PFX),
("ML-DSA-65", MLDSA65_PFX),
("ML-DSA-87", MLDSA87_PFX),
)
for level, pfx_path in levels:
with signature.Signature(source_path) as sign:
options = DigitalSignOptions(pfx_path)
options.password = CERTIFICATE_PASSWORD
sign.sign(output_path, options)
sizes[f"{level} -> {file_name}"] = os.path.getsize(output_path)
Este es el paso que vale la pena ejecutar, porque el compromiso suele describirse y rara vez medirse. Desde un contrato fuente de 132 KB:
| Nivel | Categoría de seguridad NIST | Archivo firmado | Sobre el más pequeño |
|---|---|---|---|
| ML-DSA-44 | 2 | 138,202 bytes | - |
| ML-DSA-65 | 3 | 140,650 bytes | +2,448 bytes |
| ML-DSA-87 | 5 | 143,971 bytes | +5,769 bytes |
Menos de 6 KB separan el nivel más débil del más fuerte. En un contrato eso es nada, lo que colapsa la decisión: usa ML‑DSA‑65 como predeterminado, ML‑DSA‑87 donde un perfil exija categoría 5 o donde el tamaño sea irrelevante, y ML‑DSA‑44 solo cuando estés firmando tantos archivos que los kilobytes se acumulen en algo real.
Paso 3 - Verificar con un certificado público
Un destinatario necesita el certificado público del firmante y nada secreto:
with signature.Signature(signed_path) as sign:
options = DigitalVerifyOptions(certificate_path)
if password is not None:
options.password = password
return sign.verify(options).is_valid
El ejemplo llama a esto dos veces sobre el mismo archivo: una con mldsa65.cer, la mitad pública de la clave de firma, y otra con un PFX de otro firmante. La primera devuelve True, la segunda False. Observa que el certificado incorrecto devuelve False en lugar de lanzar una excepción —“firmado por otra persona” es una respuesta que tu código debe manejar, no una excepción. La comprobación cubre el contenido del documento junto con el número de serie y la huella del certificado, de modo que un archivo editado después de la firma también falla.
Paso 4 - Leer las firmas de un documento
Cuando llega un documento firmado y no sabes qué certificado esperar:
with signature.Signature(signed_path) as sign:
found = sign.search(SignatureType.DIGITAL)
for item in found:
print(item.sign_time, item.is_valid)
search con SignatureType.DIGITAL devuelve objetos DigitalSignature que llevan el certificado, la hora de firma y una bandera de validez. Un documento Word puede contener varias firmas, incluyendo una mezcla de RSA y ML‑DSA, y cada una se informa con su propio certificado y su propia validez.
¿Esto cambia cómo verifican los destinatarios?
No, en absoluto. Un destinatario sigue necesitando solo el certificado público del firmante, lo pasa al mismo DigitalVerifyOptions y sigue recibiendo un booleano. Nada del camino de verificación es específico de ML‑DSA. El único lugar donde el algoritmo se muestra es el indicador de firma propio de Microsoft Word, que puede no reconocer ML‑DSA todavía porque el formato no tiene un identificador estándar para él.
Aplicaciones del mundo real
Contratos de retención prolongada
El caso más claro. Un documento que debe permanecer verificable durante décadas se firma una sola vez, ahora, con ML‑DSA‑65 o ML‑DSA‑87, y nunca necesita volver a firmarse porque su algoritmo quedó obsoleto.
Entornos regulados con un perfil nombrado
Donde se aplique CNSA 2.0 o un perfil similar, el nivel no es una decisión —ML‑DSA‑87 es el requisito, y la única pregunta de ingeniería es si el formato está soportado.
Flujos mixtos durante la migración
Firmar nuevos documentos post‑cuánticos mientras se deja el archivo intacto es un estado intermedio perfectamente razonable, y el reporte de search de cada firma por separado es lo que lo hace manejable.
Mejores prácticas y consejos
- Migrar por retención, no por volumen. Los documentos que lo necesitan son los de larga vida; un recibo que solo importa 90 días no lo requiere.
- Predeterminar ML‑DSA‑65 a menos que un perfil nombre otro nivel, y no agonizar por la diferencia de tamaño —es menos de 6 KB por firma.
- Mantener RSA donde el destinatario valida en Word. Firmas correctas que un lector marca como problemáticas son peores que una migración más lenta.
- Reemplazar los certificados de prueba. Los archivos PFX del ejemplo son autofirmados con una contraseña publicada, por lo que cualquier firma hecha con ellos no prueba nada.
- Verificar después de firmar en cualquier flujo, usando el certificado público que tendría el destinatario.
Solución de problemas comunes
Microsoft Word no muestra la firma como válida. Es esperado por ahora: no hay un identificador XML‑DSig estándar para ML‑DSA, así que Word puede no reconocerla aunque la firma sea correcta y GroupDocs.Signature la verifique. Verifica en tu propio flujo y mantén RSA para documentos cuyos destinatarios dependen del indicador de Word.
La llamada de firma rechaza un PDF o una hoja de cálculo. La firma ML‑DSA cubre formatos Word —DOCX, DOC, ODT y los demás. PDF, hojas de cálculo y presentaciones aún no son compatibles, y siguen firmándose con RSA o ECDSA como antes.
El sujeto del certificado vuelve vacío. Casi siempre la trampa dir() del Paso 1: el atributo se resuelve dinámicamente, así que léelo en lugar de probar su existencia primero.
Conclusión
El cambio de código es un cambio de certificado, que es la parte que hace que valga la pena hacerlo antes de que sea urgente. Firma los documentos Word de larga vida con ML‑DSA‑65, usa ML‑DSA‑87 donde un perfil lo requiera, verifica con el certificado público y mantén RSA donde el formato o el lector lo exijan.
Ejecuta el ejemplo contra uno de tus propios contratos y los tres tamaños te dirán, en bytes, exactamente lo que cuesta el nivel más fuerte disponible. En el archivo que probé fueron 5,769 bytes.