Artículo técnico

Certificados de prueba autofirmados en Delphi con CryptoAPI

La función PLCreateSelfSignedCertificate de PDF Library for Delphi construye un certificado RSA/SHA-256 autofirmado y lo exporta, clave privada incluida, directamente a un archivo PFX protegido por contraseña, usando nada más que la CryptoAPI Win32 ya instalada en cada máquina Windows. Sin ninguna herramienta externa, sin autoridad de certificación, sin ningún paso manual de makecert u OpenSSL: una llamada de función, un certificado suficientemente bueno para ejercitar una prueba de firma

El escenario que hace que valga la pena tener esta función es casi siempre un pipeline de CI. Una prueba de humo de firma necesita un PFX real con una clave privada real detrás, y añadirlo al repositorio es su propio problema de seguridad, ya que una clave privada confirmada es una clave privada filtrada desde el momento en que ese commit llega. Recurrir a makecert.exe o a una invocación de OpenSSL desde un script de compilación también funciona, pero entonces el pipeline depende de una herramienta que hay que instalar, encontrar en el PATH y mantener con una versión consistente en cada agente de compilación. Generar el certificado dentro del mismo proceso que ejecuta la prueba, con las mismas llamadas de Win32 CryptoAPI que Windows ya distribuye, elimina esa dependencia por completo

¿Qué produce realmente PLCreateSelfSignedCertificate?

PLCreateSelfSignedCertificate produce un archivo PFX protegido por contraseña que contiene un certificado RSA autofirmado y su clave privada, firmado con sha256RSA, gobernado por cinco parámetros: SubjectName, PFXFileName, PFXPassword, ValidDays y KeyBits, y devuelve un simple indicador booleano de éxito. SubjectName acepta una cadena X.500 completa como 'CN=Alice, O=Example', y un nombre desnudo sin ningún signo = en él recibe automáticamente el prefijo CN=. ValidDays por debajo de 1 recae en 365, y KeyBits fuera del rango de 1024 a 16384 recae en 2048. PDF Library for Delphi distribuye esta función desde la v3.224.0, accesible no solo desde la unidad Delphi sino también a través de las superficies DLL y ActiveX, y su propio comentario de documentación es directo sobre dónde deja de ser útil: cada visor convencional marca un certificado autofirmado como no confiable a menos que alguien lo instale explícitamente, así que tratad lo que produce como un certificado para ejercitar una vía de código, no como una firma en la que se le deba pedir a nadie fuera de vuestro equipo que confíe

var
  Success: Boolean;
begin
  Success := PLCreateSelfSignedCertificate(
    'CN=PDF Library for Delphi CI Test, O=Example Corp',
    'ci-test-signer.pfx',
    'a-strong-throwaway-password',
    365,     // ValidDays
    2048);   // KeyBits
  if not Success then
    raise Exception.Create('Self-signed certificate generation failed');
end;

¿Por qué codifica CryptGenKey la longitud de clave en el parámetro de indicadores?

CryptGenKey empaqueta dos ajustes sin relación en un único parámetro dwFlags. La palabra baja lleva indicadores de comportamiento, CRYPT_EXPORTABLE entre ellos, mientras que la palabra alta, para una clave de intercambio de claves RSA, lleva la longitud de clave solicitada en bits. Pasar 2048 como si fuera solo otro indicador lo hace aterrizar en la palabra baja en su lugar, donde no coincide con ningún indicador de comportamiento que defina CryptoAPI, así que la llamada genera una clave con cualquiera que sea la longitud por defecto a la que recaiga el proveedor en lugar de la longitud que quien llama creía haber pedido. Conseguir una clave RSA de 2048 bits de verdad significa desplazar primero el número a la palabra alta

Diagrama de PDF Library for Delphi del parámetro dwFlags de CryptGenKey dividido en una palabra alta que lleva KeyBits shl 16 como longitud de clave RSA y una palabra baja que lleva indicadores de comportamiento como CRYPT_EXPORTABLE
Un 2048 desnudo aterriza en la mitad de indicadores de comportamiento, de modo que el proveedor retrocede silenciosamente a su longitud de clave por defecto
// La longitud de la clave vive en los 16 bits superiores de las flags de CryptGenKey;
// la palabra baja lleva flags de comportamiento como CRYPT_EXPORTABLE.
if not CryptGenKey(hProv, AT_KEYEXCHANGE,
    (Cardinal(KeyBits) shl 16) or CRYPT_EXPORTABLE, hKey) then
  Exit;

¿Qué ocurre si os olvidáis de CRYPT_EXPORTABLE?

Eliminad CRYPT_EXPORTABLE de ese mismo valor de indicadores y CryptGenKey aun así tiene éxito, pero marca la clave privada generada como no exportable a nivel del CSP. Todo lo que viene después también sigue reportando éxito: CertCreateSelfSignCertificate devuelve un contexto de certificado válido, y PFXExportCertStoreEx, incluso llamado con EXPORT_PRIVATE_KEYS, tiene éxito de todos modos y escribe un archivo PFX que se abre, se analiza y parece completamente normal. Lo que no contiene es la clave privada, porque el CSP se negó a dejarla salir del contenedor de claves, y PFXExportCertStoreEx nunca trata esa negativa como motivo para hacer fallar toda la exportación

El fallo solo aparece más tarde, y en un lugar completamente distinto: una llamada de firma abre ese PFX, encuentra un certificado sin ninguna clave privada adjunta, y reporta exactamente el error que obtendríais de un PFX corrupto o equivocado, no de un indicador ausente tres capas más arriba. Cualquiera que depure solo desde el lado de la firma puede quemar una tarde entera con el archivo equivocado antes de darse cuenta de que el fallo real es un único bit ausente en el momento de generar la clave, en una llamada de función completamente distinta, posiblemente en un script de compilación completamente distinto

Diagrama de PDF Library for Delphi de la cascada de fallo silencioso provocada al omitir CRYPT_EXPORTABLE, donde la generación de clave, la creación de certificado y la exportación PFX informan éxito hasta que una llamada de firma posterior no encuentra clave privada dentro del PFX
Omitir CRYPT_EXPORTABLE produce un PFX de apariencia ordinaria cuya clave privada ausente solo sale a la luz durante la firma

¿Por qué tiene que coincidir ProvType entre CryptAcquireContextW y el certificado?

ProvType tiene que coincidir porque CertCreateSelfSignCertificate resuelve la clave privada del certificado nuevo a través de un registro CRYPT_KEY_PROV_INFO, y un campo de ese registro, ProvType, tiene que nombrar exactamente el mismo valor de tipo de CSP que se pasó a CryptAcquireContextW cuando se abrió el contenedor de claves, PROV_RSA_AES, numéricamente 24, en la implementación de PDF Library for Delphi. Poned ProvType a cero, o a cualquier constante de proveedor distinta de aquella a la que realmente pertenece el contenedor, y el certificado aun así puede llegar a crearse, pero su enlace registrado de vuelta a la clave privada ya no se resuelve al contenedor que la contiene, lo que aflora más tarde como un fallo de firma o exportación que no tiene nada que ver con el contenido criptográfico real del certificado

// El tipo de proveedor usado para abrir el contenedor de claves debe coincidir con
// el tipo de proveedor registrado en la información de proveedor de claves del certificado.
CryptAcquireContextW(hProv, PWideChar(Container), nil,
  PROV_RSA_AES, CRYPT_NEWKEYSET);
// ... genera la clave, construye el blob del nombre del sujeto y luego:
KeyProvInfo.ProvType := PROV_RSA_AES;   // misma constante, en ambos puntos de llamada

Juntándolo todo: de contenedor GUID a PFX protegido por contraseña

La cadena de llamadas dentro de PLCreateSelfSignedCertificate sigue una línea recta: abrir un contenedor de claves nuevo con nombre a partir de un GUID recién generado para que las ejecuciones de CI concurrentes nunca choquen por nombres de contenedor, generar el par de claves RSA dentro de él con los dos indicadores cubiertos arriba, codificar SubjectName en un blob de nombre X.500 mediante CertStrToNameW, y llamar a CertCreateSelfSignCertificate con una ventana de validez calculada a partir de ValidDays y entregada como una simple estructura con forma SYSTEMTIME. El contexto de certificado resultante va a un almacén de certificados en memoria abierto con CertOpenStore y CERT_STORE_PROV_MEMORY, puramente para que PFXExportCertStoreEx tenga un almacén desde el que exportar, ya que esa API funciona contra un handle de almacén en lugar de contra un simple contexto de certificado

// Cada llamada abre un contenedor desechable con nombre de un GUID nuevo:
CryptAcquireContextW(hProv, PWideChar(Container), nil,
  PROV_RSA_AES, CRYPT_NEWKEYSET);
// ... genera la clave, autofirma el certificado, exporta el PFX ...
// luego elimina el contenedor una vez que el PFX tiene su propia copia de la clave:
CryptAcquireContextW(hProv, PWideChar(Container), nil,
  PROV_RSA_AES, CRYPT_DELETEKEYSET);

PFXExportCertStoreEx en sí sigue la convención Win32 ordinaria de dos pasadas: llamarla una vez con un búfer de longitud cero para saber cuántos bytes necesita el PFX, reservar esa cantidad, y luego llamarla de nuevo para rellenar el búfer. En cuanto los bytes están en disco, PDF Library for Delphi elimina el contenedor de claves desechable con CRYPT_DELETEKEYSET en lugar de dejarlo atrás, porque el PFX ya lleva su propia copia de cada byte de material de clave que contenía el contenedor. Saltaos esa limpieza y cada llamada a PLCreateSelfSignedCertificate deja un contenedor de claves huérfano, con nombre de GUID, alojado en el perfil del usuario que llama, exactamente el tipo de fuga que un agente de CI que ejecute esta función en cada compilación acumulará durante meses antes de que nadie lo note

Diagrama de PDF Library for Delphi de la cadena de llamadas de PLCreateSelfSignedCertificate desde un contenedor de claves con nombre GUID por CryptGenKey, CertStrToNameW y CertCreateSelfSignCertificate con ProvType coincidente hasta una exportación PFX de dos pasadas, seguida del borrado del contenedor
Un contenedor GUID desechable alimenta una cadena CryptoAPI directa que termina en el PFX y se elimina inmediatamente después

¿Es seguro usar un certificado autofirmado para firma de producción?

No: un certificado autofirmado es seguro para ejercitar una vía de código de firma e inseguro para una firma en la que se espere que confíe alguien fuera del equipo, porque nada lo encadena de vuelta a una raíz en la que ya confíe el software de una parte relying. El siguiente paso natural para un PFX como este es una llamada de firma real, cubierta en la construcción de un banco de firma y conformidad en Delphi con PDF Library for Delphi, donde un PFX construido de este modo gobierna la mitad de firma de un pipeline que también ejecuta preflight PDF/A y auditorías de ByteRange. Firmar, sin embargo, es solo la mitad de lo que rodea a un certificado, y la otra mitad es precisamente donde se supone que debe fallar una hoja autofirmada: la firma y validación PAdES en Delphi con PDF Library for Delphi cubre las comprobaciones de cadena de confianza que ejecuta un validador de conformidad, y un validador que recorre la cadena de vuelta hasta una raíz de confianza no tiene ningún motivo para confiar en un certificado que esta función se inventó hace cinco minutos de la nada

PLCreateSelfSignedCertificate es una función más entre las API de certificados y firma de la biblioteca PDF PDF Library for Delphi para Delphi y C++Builder, y existe exactamente para la brecha descrita aquí: una prueba de firma que necesita un par de claves real detrás y nada externo para generar uno