Tutorial: PAdES PDF Signing in Delphi

A complete walkthrough that takes an unsigned invoice PDF and produces a PAdES-B-T document with an embedded RFC 3161 timestamp — the level expected by eIDAS, AdES verifiers and most public-sector portals. You will configure a key provider (PKCS#11 smart card or Windows certificate store), pick the right PAdES profile and verify the signed file end-to-end.

PAdES-B-B / B-T / B-LT / B-LTA
PKCS#11 / Windows Store / PFX
Delphi 7 – RAD Studio 13

What you will build

A VCL Forms application that loads a PDF, signs it with a smart card or PFX, embeds a trusted timestamp and writes a verifiable PAdES file to disk.

Pick a PAdES level

PAdES-B-B (basic), PAdES-B-T (with timestamp), PAdES-B-LT (long-term with revocation data) or PAdES-B-LTA (long-term with archive timestamp). PAdES-B-T is the sensible default for invoices, contracts and most public-sector workflows.

Load a key

sgcSign supports 10 key providers. This tutorial covers TsgcPKCS11KeyProvider for hardware tokens and TsgcWindowsStoreKeyProvider for certificates already installed in Windows. The same code path works for PFX, PEM, Azure Trusted Signing, AWS KMS and Google KMS.

Sign and verify

TsgcSignPDF drives the signing pipeline, TsgcSignProfile_PAdES configures the profile, and TsgcSignatureVerifier validates the final document. Each step exposes events so you can inspect what is happening at every stage.

Add the units

Each key provider lives in its own unit. Reference only the provider you need so you do not link units you will not use.

Delphi uses clause

For this tutorial we pull in two key providers (PKCS#11 and Windows Store) and the PAdES signing units. sgcSign_TSA is the RFC 3161 timestamp client — the PAdES profile uses it automatically when you set a TSA URL.

If you only have a PFX file, replace sgcSign_KeyProvider_PKCS11 with sgcSign_KeyProvider_PFX and use TsgcPFXKeyProvider as shown in the 5-minute Quick Start.

uPDFSigning.pas
uses
  Classes, SysUtils,
  // sgc
  sgcSign_KeyProvider_PKCS11,
  sgcSign_KeyProvider_WindowsStore,
  sgcSign_PDF,
  sgcSign_Profile_PAdES,
  sgcSign_TSA,
  sgcSign_Verifier;

Configure a key provider

Two common choices — a PKCS#11 hardware token (smart cards, USB HSMs) or a certificate already in the Windows certificate store.

PKCS#11 smart card

Point ModulePath at the vendor PKCS#11 DLL (for example C:\Windows\System32\eTPKCS11.dll for SafeNet). The Slot property selects the token slot — use 0 for the first available token. The PIN is delivered via the OnPinPrompt event so it is never stored in source.

Calling LoadFromToken opens a session, finds the signing certificate by label or thumbprint and prepares the private-key handle. No bytes leave the token — the actual RSA-PSS or ECDSA signing happens on the device.

step1-pkcs11.pas
var
  vKeyProvider: TsgcPKCS11KeyProvider;
begin
  vKeyProvider := TsgcPKCS11KeyProvider.Create(nil);
  try
    vKeyProvider.ModulePath := 'C:\Windows\System32\eTPKCS11.dll';
    vKeyProvider.Slot := 0;
    vKeyProvider.CertificateLabel := 'My Signing Cert';
    vKeyProvider.OnPinPrompt :=
      procedure(Sender: TObject; var aPIN: string)
      begin
        aPIN := InputBox('Smart card', 'PIN:', '');
      end;
    vKeyProvider.LoadFromToken;
    // vKeyProvider is now ready to sign
  finally
    // keep alive until signing finishes, then Free
  end;
end;

Windows certificate store

If your certificate is already installed in the Windows store — for example because you use a qualified eID card with a vendor mini-driver — use TsgcWindowsStoreKeyProvider instead. The provider talks to CNG so SHA-256/SHA-384 work regardless of the original CSP.

Match by subject, by thumbprint, or by serial number. Setting StoreLocation to slCurrentUser uses the personal store; slLocalMachine uses the machine store and typically requires administrator rights.

step1-winstore.pas
var
  vKeyProvider: TsgcWindowsStoreKeyProvider;
begin
  vKeyProvider := TsgcWindowsStoreKeyProvider.Create(nil);
  vKeyProvider.StoreLocation := slCurrentUser;
  vKeyProvider.StoreName := 'MY';
  vKeyProvider.Thumbprint :=
    '1234ABCD5678EF901234567890ABCDEF12345678';
  vKeyProvider.LoadFromStore;
end;

Configure the PAdES-B-T profile

PAdES-B-T adds an RFC 3161 timestamp to the signature so verifiers can prove the document was signed before a known point in time — even if the signing certificate later expires or is revoked.

Profile properties

Level selects the AdES level — palB, palT, palLT or palLTA. For PAdES-B-T set it to palT; the profile then expects a non-empty TSA.URL.

HashAlgorithm defaults to shaSHA256, which is what every modern verifier expects. SHA-384 and SHA-512 are also available; SHA-1 is intentionally not offered because eIDAS no longer accepts it.

The SignatureField sub-object controls where the visible signature widget is placed inside the PDF — page number, rectangle coordinates and optional reason / location / contact text fields. Leave it empty for invisible signatures (the default).

step2-profile.pas
var
  vProfile: TsgcSignProfile_PAdES;
begin
  vProfile := TsgcSignProfile_PAdES.Create(nil);
  vProfile.Level := palT;
  vProfile.HashAlgorithm := shaSHA256;

  // RFC 3161 timestamp authority
  vProfile.TSA.URL := 'http://timestamp.digicert.com';
  vProfile.TSA.HashAlgorithm := shaSHA256;

  // Optional visible signature widget
  vProfile.SignatureField.Visible := True;
  vProfile.SignatureField.Page := 1;
  vProfile.SignatureField.Rect := Rect(50, 50, 250, 120);
  vProfile.SignatureField.Reason := 'Approved';
  vProfile.SignatureField.Location := 'Madrid';
end;

Sign the PDF

Wire the key provider and the profile into TsgcSignPDF, then call SignFile with the input and output file names.

Full signing call

SignFile reads the input PDF, computes the document digest, calls the key provider to produce a signature, fetches a timestamp from the TSA and writes the result. If the input already contains signatures, sgcSign appends an incremental update — original signatures stay valid.

The OnProgress event reports each phase (reading, hashing, signing, timestamping, writing), useful for progress bars on multi-MB files.

step3-sign.pas
var
  vSigner: TsgcSignPDF;
begin
  vSigner := TsgcSignPDF.Create(nil);
  try
    vSigner.KeyProvider := vKeyProvider;
    vSigner.Profile := vProfile;
    vSigner.OnProgress :=
      procedure(Sender: TObject; const aPhase: string; aPct: Integer)
      begin
        Memo1.Lines.Add(Format('%s — %d%%', [aPhase, aPct]));
      end;
    vSigner.SignFile('invoice.pdf', 'invoice-signed.pdf');
  finally
    vSigner.Free;
  end;
end;

Verify the signed PDF

A signed document is only useful if it verifies. TsgcSignatureVerifier validates the cryptographic signature, the certificate chain and the embedded timestamp in one call.

Reading the result

The VerifyFile method returns a TsgcSignatureReport with a per-signature breakdown. For each entry you get Status (svValid, svInvalid, svUnknown), the signer subject, the AdES level actually detected, the timestamp value, and any chain or revocation issues.

Compare Report.Level against your expected level — you wanted palT, so any verification that reports palB means the timestamp request failed silently. The TSA URL is the usual suspect.

step4-verify.pas
var
  vVerifier: TsgcSignatureVerifier;
  vReport: TsgcSignatureReport;
  i: Integer;
begin
  vVerifier := TsgcSignatureVerifier.Create(nil);
  try
    vReport := vVerifier.VerifyFile('invoice-signed.pdf');
    for i := 0 to vReport.SignatureCount - 1 do
    begin
      Memo1.Lines.Add('Signer:    ' + vReport.Signatures[i].Subject);
      Memo1.Lines.Add('Level:     ' + vReport.Signatures[i].LevelAsString);
      Memo1.Lines.Add('Status:    ' + vReport.Signatures[i].StatusAsString);
      Memo1.Lines.Add('TSA time:  ' +
        DateTimeToStr(vReport.Signatures[i].TimestampUTC));
    end;
  finally
    vVerifier.Free;
  end;
end;

What usually goes wrong

A short list of issues we see most often when developers sign their first PAdES document.

TSA returns HTTP 200 but a malformed response

Some TSAs are HTTPS-only and reject plain http:// URLs with a soft error. If Report.Level comes back as palB when you asked for palT, switch the TSA URL to its https:// equivalent. Most public TSAs publish both schemes.

Certificate has no extended key usage

Test certificates often lack the id-kp-documentSigning EKU. Adobe Reader will display a warning even though the cryptographic signature is valid. For production, request a certificate with explicit Document Signing EKU from your CA.

PDF/A documents need PAdES-B-LT

PDF/A-2 and PDF/A-3 archives require all revocation information to be embedded in the document itself. Use palLT or palLTA — sgcSign will harvest OCSP responses and CRLs at signing time and embed them in the DSS dictionary.

Smart card sessions time out

Some PKCS#11 drivers close the session after a few seconds of inactivity. Keep the TsgcPKCS11KeyProvider instance alive for the entire signing operation and call Free only after SignFile returns.

Where to go from here

Long-term archival, country profiles, or move signing off the desktop and into a centralised server.

21 Country Profiles

One-liners for VeriFactu, FatturaPA, KSeF, FACTUR-X and the EU employment-contract profiles — all built on top of the same PAdES / XAdES engine.

Read more →

sgcSign Server

Move signing into a self-hosted daemon — REST API, GitHub Actions integration, Authenticode and ClickOnce on top of PAdES.

Read more →

10 Key Providers

Beyond PFX and Windows store — Azure Trusted Signing, AWS KMS, Google KMS, HashiCorp Vault, Certum and CSC v2 cloud providers.

Read more →

Ready to sign your first PDF?

Download the trial, run this tutorial against your own invoice, license sgcSign when you ship.