Technical Article

Self-Signed Test Certificates in Delphi with CryptoAPI

PDFlibPas's PLCreateSelfSignedCertificate function builds a self-signed RSA/SHA-256 certificate and exports it, private key included, straight into a password-protected PFX file, using nothing but the Win32 CryptoAPI already installed on every Windows machine. No external tool, no certificate authority, no manual makecert or OpenSSL step: one function call, one certificate good enough to drive a signing test

The scenario that makes this function worth having is almost always a CI pipeline. A signing smoke test needs a real PFX with a real private key behind it, and checking one into the repository is its own security problem, since a committed private key is a leaked private key from the moment that commit lands. Shelling out to makecert.exe or an OpenSSL invocation from a build script works too, but then the pipeline depends on a tool that has to be installed, found on PATH, and kept version-consistent across every build agent. Generating the certificate inside the same process that runs the test, with the same Win32 CryptoAPI calls Windows already ships, removes that dependency entirely

What does PLCreateSelfSignedCertificate actually produce?

PLCreateSelfSignedCertificate produces a password-protected PFX file holding a self-signed RSA certificate and its private key, signed with sha256RSA, driven by five parameters: SubjectName, PFXFileName, PFXPassword, ValidDays, and KeyBits, and it returns a plain Boolean success flag. SubjectName accepts a full X.500 string such as 'CN=Alice, O=Example', and a bare name with no = sign in it is automatically prefixed with CN=. ValidDays below 1 falls back to 365, and KeyBits outside the 1024 to 16384 range falls back to 2048. PDFlibPas has shipped this function since v3.224.0, reachable not just from the Delphi unit but also through the DLL and ActiveX surfaces, and its own doc comment is blunt about where it stops being useful: every mainstream viewer marks a self-signed certificate as untrusted unless someone installs it explicitly, so treat what it produces as a certificate for exercising a code path, not a signature anyone outside your team should be asked to rely on

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;

Why does CryptGenKey encode the key length in the flags parameter?

CryptGenKey packs two unrelated settings into a single dwFlags parameter. The low word carries behavior flags, CRYPT_EXPORTABLE among them, while the high word, for an RSA key-exchange key, carries the requested key length in bits. Passing 2048 as if it were just another flag lands it in the low word instead, where it does not match any behavior flag CryptoAPI defines, so the call generates a key at whatever default length the provider falls back to rather than the length the caller thought it asked for. Getting an actual 2048-bit RSA key means shifting the number into the high word first

// 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;

What happens if you forget CRYPT_EXPORTABLE?

Drop CRYPT_EXPORTABLE from that same flags value and CryptGenKey still succeeds, but it marks the generated private key non-exportable at the CSP level. Everything downstream keeps reporting success too: CertCreateSelfSignCertificate returns a valid certificate context, and PFXExportCertStoreEx, even called with EXPORT_PRIVATE_KEYS, succeeds anyway and writes a PFX file that opens, parses, and looks completely ordinary. What it does not contain is the private key, because the CSP refused to let it out of the key container, and PFXExportCertStoreEx never treats that refusal as a reason to fail the whole export

The failure only shows up later, and somewhere else entirely: a signing call opens that PFX, finds a certificate with no private key attached, and reports exactly the error you would get from a corrupted or wrong PFX, not from a missing flag three layers upstream. Anyone debugging from the signing side alone can burn an afternoon on the wrong file before realizing the actual bug is a single missing bit at key-generation time, in a completely different function call, possibly in a completely different build script

Why must ProvType match between CryptAcquireContextW and the certificate?

ProvType must match because CertCreateSelfSignCertificate resolves the new certificate's private key through a CRYPT_KEY_PROV_INFO record, and one field in that record, ProvType, has to name the exact same CSP type value passed to CryptAcquireContextW when the key container was opened, PROV_RSA_AES, numerically 24, in PDFlibPas's implementation. Set ProvType to zero, or to any provider constant other than the one the container actually belongs to, and the certificate can still get created, but its recorded link back to the private key no longer resolves to the container that holds it, which surfaces later as a signing or export failure that has nothing to do with the certificate's actual cryptographic content

// 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

Putting it together: from GUID container to password-protected PFX

The call chain inside PLCreateSelfSignedCertificate follows one straight line, opening a fresh key container named after a newly generated GUID so concurrent CI runs never collide over container names, generating the RSA key pair inside it with the two flags covered above, encoding SubjectName into an X.500 name blob through CertStrToNameW, and calling CertCreateSelfSignCertificate with a validity window computed from ValidDays and handed over as a plain SYSTEMTIME-shaped structure. The resulting certificate context goes into an in-memory certificate store opened with CertOpenStore and CERT_STORE_PROV_MEMORY, purely so PFXExportCertStoreEx has a store to export from, since that API works against a store handle rather than a bare certificate context

// 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 itself follows the ordinary Win32 two-pass convention: call it once with a zero-length buffer to learn how many bytes the PFX needs, allocate that much, then call it again to fill the buffer. Once the bytes are on disk, PDFlibPas deletes the throwaway key container with CRYPT_DELETEKEYSET instead of leaving it behind, because the PFX already carries its own copy of every byte of key material the container held. Skip that cleanup and every call to PLCreateSelfSignedCertificate leaves an orphaned, GUID-named key container sitting in the calling user's profile, which is exactly the kind of leak a CI agent running this function on every build will accumulate for months before anyone notices

Is a self-signed certificate safe to use for production signing?

No: a self-signed certificate is safe for exercising a signing code path and unsafe for a signature anyone outside the team is expected to trust, because nothing chains it back to a root a relying party's software already trusts. The natural next step for a PFX like this is an actual signing call, covered in building a compliance and signing workbench in Delphi with PDFlibPas, where a PFX built this way drives the signing half of a pipeline that also runs PDF/A preflight and ByteRange audits. Signing is only half of what sits around a certificate, though, and the other half is exactly where a self-signed leaf is supposed to fail: PAdES signing and validation in Delphi with PDFlibPas covers the chain-of-trust checks a conformance validator runs, and a validator that walks the chain back to a trusted root has no reason to trust a certificate this function invented five minutes ago from nothing

PLCreateSelfSignedCertificate is one function among the certificate and signing APIs in the PDFlibPas PDF library for Delphi and C++Builder, and it exists for exactly the gap described here: a signing test that needs a real key pair behind it and nothing external to generate one