💡 Ejemplo completo disponible en GitHub:
pdf-signing-certificate-checks-python

Introducción

Un servicio firma los PDFs cargados cada noche. Una mañana el certificado que utiliza supera su fecha de vencimiento, y nada parece cambiar: el trabajo se ejecuta, los archivos se escriben, el registro parece normal. Semanas después alguien abre uno de esos documentos en Acrobat y ve una barra de advertencia, porque una firma hecha con un certificado expirado no es una firma más débil, sino una que los validadores reportan como inválida. Los documentos que parecen aprobados valen menos que los que no están firmados, porque la gente los creyó.

Ese rechazo tiene un nombre. La comprobación de validez del certificado es un comportamiento de GroupDocs.Signature para Python que se niega a firmar una vez que el período de validez del certificado ha expirado, o antes de que comience. Llegó en la versión 26.9 junto a dos cambios con la misma forma: SHA-256 se convirtió en el digest predeterminado para firmas PDF, y SignatureSettings.log_level empezó a filtrar en lugar de ser ignorado silenciosamente. Cada uno toma un resultado que antes ocurría en silencio y lo pone delante de ti.

Este artículo compara esos tres controles tal como se comportan desde Python a través de .NET: qué cambia cada uno en la salida, cuándo utilizarlos y qué dos detalles del enlace le costaron a la gente una tarde. Cada resultado citado proviene de ejecutar el ejemplo contra un PDF de una página.

Por Qué Esto Importa Más Que una Nota de Versión

Los tres cambios comparten una propiedad que vale la pena nombrar: todos convierten una falla que descubrirías después en una que descubres ahora.

  • Certificados expirados: la llamada de firma falla donde alguien puede renovar el certificado, en lugar de producir documentos que fallan la validación después de la distribución.
  • Valores predeterminados de digest: las nuevas firmas usan SHA-256 sin que nadie tenga que recordarlo, de modo que la opción débil requiere una decisión en lugar de una falta de atención.
  • Niveles de registro: un servicio que configura solo advertencias ahora recibe solo advertencias, lo que hace que las advertencias sean legibles, lo que significa que se leen.

Ese último punto es menos cosmético de lo que parece. Todo el valor de la advertencia de certificado expirado es que alguien la vea, y una advertencia enterrada entre diez mensajes de traza por ejecución de firma es una advertencia que nadie ve.

Requisitos Previos

Antes de comenzar, asegúrate de tener:

  • 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 a través de .NET 26.10.0, con una licencia temporal gratuita si deseas eliminar los límites de evaluación.
  • Un PDF para firmar y el paquete cryptography si deseas generar certificados de prueba desechables como lo hace el ejemplo.

Instalación

pip install groupdocs-signature-net cryptography

Control 1 – El digest escrito en la firma

hash_algorithm en DigitalSignOptions elige el digest. El valor predeterminado desde la 26.9 es SHA‑256, en el formato adbe.pkcs7.detached que los validadores actuales esperan; antes de eso, las nuevas firmas eran SHA‑1.

with signature.Signature(source_path) as sign:
    options = DigitalSignOptions()
    options.certificate_stream = io.BytesIO(pfx)
    options.password = PASSWORD
    options.hash_algorithm = HashAlgorithm.SHA512
    options.reason = "Approved"

    result = sign.sign(output_path, options)
    return len(result.succeeded)

Dos detalles merecen resaltarse. El certificado llega a través de certificate_stream como un io.BytesIO en lugar de una ruta de archivo, que es la forma en que un PKCS#12 construido en memoria llega a la biblioteca sin nunca escribirse en disco – el ejemplo depende de eso para no incluir ninguna clave privada. Y HashAlgorithm ofrece AUTO, SHA1, SHA256, SHA384 y SHA512, donde una marca de tiempo, si la añades, usa el digest que la firma haya utilizado.

En la práctica, este es el control que menos tocas. El valor predeterminado ya es la respuesta correcta, SHA384 y SHA512 existen para cuando una política de firma los nombra, y SHA1 es una configuración de compatibilidad para validadores que no puedes cambiar.

Control 2 – Si un certificado caducado te detiene

Sin sobrescrituras, firmar con un certificado cuyo período de validez ha terminado – o que aún no ha comenzado – genera GroupDocsSignatureException y no escribe nada.

try:
    sign.sign(output_path, options)
    return True
except signature.GroupDocsSignatureException as error:
    print(f"Rejected: {str(error).splitlines()[0]}")
    return False

El mensaje indica el certificado, la fecha en que expiró, su huella digital y la propiedad que lo permitiría, lo cual es suficiente para que una aplicación indique al operador qué renovar. Tomar solo la primera línea importa específicamente en Python: el texto de la excepción continúa con la traza .NET del enlace, y eso no es algo que se deba mostrar al usuario.

Cuando realmente necesitas firmar de todos modos – una prueba contra un certificado archivado, o un lote que debe ejecutarse esta noche mientras se renueva el certificado – la sobrescritura se hace por llamada:

settings = signature.SignatureSettings(ConsoleLogger())
settings.log_level = LogLevel.WARNING | LogLevel.ERROR

with signature.Signature(source_path, settings=settings) as sign:
    options.allow_expired = True
    result = sign.sign(output_path, options)

allow_not_yet_valid tiene la misma forma para un certificado emitido para una fecha posterior, y las dos banderas son independientes: permitir un certificado expirado no permite uno anticipado. Un certificado anticipado suele indicar que el reloj de la máquina está desajustado más que que el certificado sea inusual, y un reloj incorrecto hace que cada firma que esa máquina produzca sea cuestionable, así que verifica eso antes de sobrescribir cualquier cosa.

Ambas sobrescrituras emiten una advertencia en lugar de pasar silenciosamente, que es la parte que se conecta con el tercer control.

Control 3 – Si alguien lo descubre

SignatureSettings.log_level es un valor de banderas. El ejemplo firma el mismo documento tres veces, bajo LogLevel.NONE, LogLevel.WARNING | LogLevel.ERROR y LogLevel.ALL, contando lo que llega:

logger = CollectingLogger()
settings = signature.SignatureSettings(logger)
settings.log_level = level

with signature.Signature(source_path, settings=settings) as sign:
    options.allow_expired = True
    sign.sign(output_path, options)

Los recuentos resultan en nada, luego una advertencia, y luego esa advertencia más diez trazas. Antes de la 26.9 las tres filas habrían sido idénticas, porque el nivel se aceptaba y se ignoraba – lo cual es útil saber si alguna vez lo configuraste, no viste cambios y concluiste que habías leído mal tu propio código.

Dos detalles del enlace me costaron una tarde, así que vale declararlos claramente. SignatureSettings.logger es de solo lectura, por lo que el logger se pasa como argumento del constructor y asignarle genera AttributeError; log_level se establece normalmente después. Además, un logger personalizado no debe subclasificar groupdocs.signature.logging.ILogger – esa clase base envuelve un objeto nativo cuyo constructor necesita un manejador que posee la biblioteca, de modo que subclasificar genera TypeError. El enlace acepta cualquier objeto plano que proporcione los tres métodos:

class StdlibLogger:
    def error(self, message, exception=None):
        logging.getLogger("groupdocs").error(message, exc_info=exception)

    def warning(self, message, exception=None):
        logging.getLogger("groupdocs").warning(message)

    def trace(self, message):
        logging.getLogger("groupdocs").debug(message)

Da a error y warning un parámetro opcional exception. La biblioteca no siempre pasa uno, y un logger que lo requiera falla con los mensajes que lo omiten.

Comparación de los Tres: Cuándo Usar Cada Uno

Control Mejor para Ventajas clave Limitaciones
hash_algorithm cumplir una política que nombra un digest una asignación; mismo tamaño de salida inútil si el propio certificado no es de confianza
comprobación de validez y sobrescrituras cualquier firma para otras personas el fallo ocurre donde puede corregirse una sobrescritura produce un archivo, no uno confiable
log_level servicios cuyos registros ya están saturados once mensajes se convierten en uno solo filtra el registro, nunca las excepciones

No son alternativas – una única llamada de firma usa los tres. El orden para considerarlos es el orden de consecuencia: la comprobación de validez decide si el archivo existe, el digest decide qué contiene, y el nivel de registro decide quién lo sabe.

¿Cambia el nivel de registro las excepciones que recibo?

No. Decide qué mensajes llegan a tu logger y nada más. Un certificado expirado sigue lanzando GroupDocsSignatureException bajo LogLevel.NONE, y allow_expired sigue firmando bajo LogLevel.ALL; los valores de retorno y las excepciones son idénticos en todos los niveles. Lo que cambia es si la advertencia que explica una firma cuestionable es leída alguna vez por una persona.

Verificación Movida en la Misma Dirección

Vale la pena mencionarlo porque es la otra mitad del mismo lanzamiento. verify con un DigitalVerifyOptions vacío ahora comprueba criptográficamente cada firma digital PDF, de modo que un documento alterado después de la firma vuelve a ser inválido en lugar de simplemente inexplicado:

with signature.Signature(signed_path) as sign:
    result = sign.verify(DigitalVerifyOptions())
    return result.is_valid

Dos líneas, y vale añadirlo a cualquier canal que firme y luego almacene. Observa lo que un True no promete: dice que la firma coincide con el documento, no que el emisor sea de confianza. Los certificados autofirmados del ejemplo se verifican aquí y siguen siendo rechazados por un lector PDF, lo que responde a la cuestión de confianza por separado.

Mejores Prácticas y Consejos

  • Mantén el rechazo como predeterminado en cualquier cosa que firme en nombre de los usuarios, y sobrescribe por llamada en lugar de globalmente. La excepción es barata; un lote de firmas inválidas no lo es.
  • Registra el texto de la advertencia, no solo un contador. Nombra el certificado y la fecha, que es la única parte sobre la que un operador puede actuar.
  • Comprueba el reloj antes de permitir un certificado que aún no es válido. El certificado suele estar correcto y la máquina suele estar equivocada, y eso afecta a más de una llamada de firma.
  • Mantén las trazas fuera de producción. Unas diez por ejecución de firma se acumulan rápido; actívalas mientras diagnosticas y desactívalas después.
  • Verifica después de firmar en cualquier canal, ahora que la comprobación es criptográfica, de modo que una salida corrupta se detecte antes de que el destinatario la encuentre.

Conclusión

Tres controles, una llamada de firma y la misma idea de diseño detrás de todos ellos: el resultado arriesgado ahora necesita una decisión, y el seguro no necesita nada. Mantén la comprobación de validez, trata allow_expired como una excepción por llamada que registras, deja el digest tal cual a menos que una política indique lo contrario, y establece un nivel de registro que haga legibles las advertencias.

Ejecutar el ejemplo contra uno de tus propios PDFs lleva un minuto e imprime exactamente lo que cada control cambió – seis archivos firmados, una negativa deliberada y tres filas de recuentos de mensajes que ya no son iguales.

Recursos Adicionales