La función PLCreateSelfSignedCertificate de PDFlibPas 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. PDFlibPas 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=PDFlibPas 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
// Key length lives in the upper 16 bits of the CryptGenKey flags;
// the low word carries behavior flags such as 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
¿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 PDFlibPas. 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
// The provider type used to open the key container must match the
// provider type recorded in the certificate's key-provider info.
CryptAcquireContextW(hProv, PWideChar(Container), nil,
PROV_RSA_AES, CRYPT_NEWKEYSET);
// ... generate the key, build the subject name blob, then:
KeyProvInfo.ProvType := PROV_RSA_AES; // same constant, both call sites
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
// Each call opens a throwaway container named after a fresh GUID:
CryptAcquireContextW(hProv, PWideChar(Container), nil,
PROV_RSA_AES, CRYPT_NEWKEYSET);
// ... generate the key, self-sign the certificate, export the PFX ...
// then delete the container once the PFX holds its own copy of the key:
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, PDFlibPas 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
¿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 PDFlibPas, 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 PDFlibPas 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 PDFlibPas 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