Signing PDFs (Delphi)
Overview
In this example we will show how to add a visible or invisible signature
to a generated PDF file.
Concepts
FlexCel supports adbe.pkcs7.detached (the original Adobe format), PAdES baseline level B-B, and PAdES baseline level B-T.
Being older, adbe.pkcs7.detached is the most extended and compatible, but PAdES is the standard required by the European Union to sign.
It probably makes sense to sign your PDFs with PAdES.
In order to sign a PDF file you will need a certificate issued by a
valid Certificate Authority, or one issued by yourself. In this
example we will use a self signed certificate. This certificate
will not validate by default when you open it in Acrobat, you need
to add it to your trusted list.
As SHA-1 is deprecated, FlexCel will default to using SHA512
for the signature. You could use a different algorithm by
providing an OID in the EncryptionFactory.GetSigner call.
In order to sign a file, FlexCel will write a requirement for
Acrobat 8 or newer in the generated files. This is because only
Acrobat 8 or newer support SHA512. Older versions of acrobat
will still display the pages but will not validate the signature.
FlexCel currently only has support for signing in Windows,
using CryptoAPI. You can still create your own signature
engine for other platforms by using a third party cryptography
library or by calling the native crypto functions in that platform,
the same way we call CryptoAPI. This is explained in the section
Signing PDF Files in the
PDF exporting guide.
By design, FlexCel never connects to the internet.
So if you want to create a PAdES B-T signature, which includes a timestamp from a TSA server, you need to provide the code to actually connect the server in an anonymous method.
This demo shows how to do it.
Files
USigningPdfs.pas
unit USigningPdfs;
interface
uses
Windows, Messages, SysUtils, Variants, Classes, Graphics,
Controls, Forms, Dialogs,
StdCtrls,
pngimage, ExtCtrls, ExtDlgs,
{$if CompilerVersion >= 23.0} System.UITypes, {$IFEND}
FlexCel.VCLSupport, FlexCel.Core, FlexCel.XlsAdapter, FlexCel.Render, FlexCel.Pdf;
type
{$SCOPEDENUMS ON}
/// <summary>
/// The entries of the "Signature type" combo box, in the same order as they are added in the form.
/// </summary>
TSignatureType = (
/// <summary>
/// "/adbe.pkcs7.detached": the original Adobe format. It is the most compatible, but it is not a CAdES
/// signature, so it doesn't conform to the PAdES standard the European Union requires.
/// </summary>
Pkcs7,
/// <summary>
/// "/ETSI.CAdES.detached" without a timestamp: PAdES baseline level B-B.
/// </summary>
PAdES_B_B,
/// <summary>
/// PAdES baseline level B-T: a B-B signature plus a timestamp from a Time Stamping Authority, which proves
/// the document was signed before a given date instead of trusting the clock of whoever signed.
/// </summary>
PAdES_B_T);
{$SCOPEDENUMS OFF}
TFSigningPdfs = class(TForm)
btnCreateAndSignPdf: TButton;
lblSignatureType: TLabel;
cbSignatureType: TComboBox;
cbCertify: TCheckBox;
cbVisibleSignature: TCheckBox;
SignaturePicture: TImage;
OpenPictureDialog: TOpenPictureDialog;
OpenExcelDialog: TOpenDialog;
SavePdfDialog: TSaveDialog;
procedure cbVisibleSignatureClick(Sender: TObject);
procedure SignaturePictureClick(Sender: TObject);
procedure btnCreateAndSignPdfClick(Sender: TObject);
procedure FormCreate(Sender: TObject);
private
DataPath: string;
ImgData: ByteArray;
procedure LoadImage(const FileName: string);
function CreateSignerFactory(const Signer: TCmsSigner): TPdfSignerFactory;
{ Private declarations }
public
{ Public declarations }
end;
var
FSigningPdfs: TFSigningPdfs;
implementation
uses IOUtils, UFlexCelHDPI, ShellAPI, IdHTTP;
{$R *.dfm}
const
//The Time Stamping Authority we ask for the timestamps. Replace it with the one you use: the public ones are
//rate-limited, and a timestamp is only as trustworthy as the TSA that created it.
TsaUrl = 'http://timestamp.digicert.com';
/// <summary>
/// <b>FlexCel never connects to the internet by itself.</b> It creates the RFC 3161 request and reads the answer,
/// but the connection to the TSA is this function, which you write. That way you know that no part of FlexCel can
/// reach the network unless you let it.
/// </summary>
/// <remarks>
/// We use Indy here because it is available in every Delphi version this demo compiles in. From XE8
/// (CompilerVersion 29) on you can use THTTPClient from the RTL instead: there is a version of this same function
/// written with it right below.
/// <br /><b>About https:</b> the url above is plain http, which is what most TSAs publish, and it is safe because
/// the answer is signed by the TSA and FlexCel verifies it. If the TSA you use is https, THTTPClient talks to it
/// with nothing extra to deploy, as on Windows it goes through the system http stack. TIdHTTP instead needs the
/// OpenSSL dlls beside the exe, and an ssl handler:
/// <code>
/// uses IdSSLOpenSSL;
/// ...
/// SslHandler := TIdSSLIOHandlerSocketOpenSSL.Create(Http);
/// SslHandler.SSLOptions.SSLVersions := [sslvTLSv1_2]; //older Indy defaults to versions no server accepts today
/// Http.IOHandler := SslHandler;
/// </code>
/// </remarks>
function GetTimestamp(timeStampRequest: ByteArray): ByteArray;
var
Http: TIdHTTP;
Request, Response: TMemoryStream;
begin
Http := TIdHTTP.Create(nil);
try
Http.Request.ContentType := 'application/timestamp-query';
Request := TMemoryStream.Create;
try
Response := TMemoryStream.Create;
try
Request.WriteBuffer(timeStampRequest[0], Length(timeStampRequest));
Request.Position := 0;
//This demo signs when you click a button, so we just wait for the answer here. In a server you would
//normally make the whole export async instead of blocking a thread on the TSA.
Http.Post(TsaUrl, Request, Response);
Result := nil;
SetLength(Result, Response.Size);
Response.Position := 0;
if Length(Result) > 0 then Response.ReadBuffer(Result[0], Length(Result));
finally
Response.Free;
end;
finally
Request.Free;
end;
finally
Http.Free;
end;
end;
(*
The same function written with THTTPClient, which is in the RTL from XE8 (CompilerVersion 29) on. Add
System.Net.HttpClient and System.Net.URLClient to the uses clause to use it. Note that, unlike TIdHTTP, it does
not raise an exception when the server answers an error, so we have to look at the status code ourselves.
function GetTimestamp(timeStampRequest: ByteArray): ByteArray;
var
Http: THTTPClient;
Request, Response: TMemoryStream;
HttpResponse: IHTTPResponse;
begin
Http := THTTPClient.Create;
try
Request := TMemoryStream.Create;
try
Response := TMemoryStream.Create;
try
Request.WriteBuffer(timeStampRequest[0], Length(timeStampRequest));
Request.Position := 0;
HttpResponse := Http.Post(TsaUrl, Request, Response,
[TNameValuePair.Create('Content-Type', 'application/timestamp-query')]);
if HttpResponse.StatusCode <> 200 then
raise Exception.Create('The Time Stamping Authority answered ' + IntToStr(HttpResponse.StatusCode)
+ ' ' + HttpResponse.StatusText);
Result := nil;
SetLength(Result, Response.Size);
Response.Position := 0;
if Length(Result) > 0 then Response.ReadBuffer(Result[0], Length(Result));
finally
Response.Free;
end;
finally
Request.Free;
end;
finally
Http.Free;
end;
end;
*)
/// <summary>
/// Creates the factory that will sign the document in the format selected in the combo box.
/// </summary>
function TFSigningPdfs.CreateSignerFactory(const Signer: TCmsSigner): TPdfSignerFactory;
begin
case TSignatureType(cbSignatureType.ItemIndex) of
TSignatureType.PAdES_B_B:
//Besides writing "/ETSI.CAdES.detached" in the pdf, this adds the ESS signing-certificate-v2 signed
//attribute that CAdES needs, so the signature says which certificate created it.
Result := TBuiltInSignerFactory.Create(Signer, TPdfSignatureSubFilter.EtsiCAdESDetached);
TSignatureType.PAdES_B_T:
//FlexCel has to reserve the space for the signature before it knows how big the timestamp will be, so it
//asks the TSA for one sample token the first time. Passing the url as the cache key means the size is
//measured once for the whole application and shared by every factory using this TSA, which matters because
//TSAs tend to rate-limit. If you already know the size, set TokenSizeHint instead and FlexCel will not ask
//for the sample at all.
Result := TBuiltInSignerFactory.Create(Signer, TPdfSignatureSubFilter.EtsiCAdESDetached,
TPdfTimestampSettings_Create(GetTimestamp, TsaUrl));
else
Result := TBuiltInSignerFactory.Create(Signer, TPdfSignatureSubFilter.AdbePkcs7Detached);
end;
end;
procedure TFSigningPdfs.LoadImage(const FileName: string);
begin
ImgData := TFile.ReadAllBytes(FileName);
SignaturePicture.Picture.LoadFromFile(FileName);
end;
procedure TFSigningPdfs.FormCreate(Sender: TObject);
begin
DataPath := TPath.Combine(TPath.GetDirectoryName(ParamStr(0)), '..\..\');
LoadImage(DataPath + 'sign.png');
cbSignatureType.ItemIndex := Ord(TSignatureType.PAdES_B_B);
end;
procedure TFSigningPdfs.btnCreateAndSignPdfClick(Sender: TObject);
var
xls: TExcelFile;
pdf: TFlexCelPdfExport;
Cert: TX509Certificate2;
Signer: TCmsSigner;
SignerFactory: TPdfSignerFactory;
Signature: TPdfSignature;
begin
//Load the Excel file.
if (not OpenExcelDialog.Execute) then exit;
xls := TXlsFile.Create;
try
xls.Open(OpenExcelDialog.FileName);
//Export it to pdf.
pdf := TFlexCelPdfExport.Create(xls, true);
try
pdf.FontEmbed := TFontEmbed.Embed;
//Load the certificate and create a signer.
Cert := EncryptionFactory.GetX509Certificate(TFile.ReadAllBytes(DataPath + 'flexcel.pfx'), 'password');
try
// The current implementation uses only one certificate. The algorithm by
// default if you leave the second parameter empty is SHA512.
Signer := EncryptionFactory.GetSigner(TArray<TX509Certificate2>.Create(Cert), '');
SignerFactory := nil;
try
//The format of the signature is decided by the factory, not by the signature itself.
SignerFactory := CreateSignerFactory(Signer);
Signer := nil; //The factory now owns the Signer so we don't want to free it.
if (cbVisibleSignature.Checked) then
begin
//The -1 as "page" parameter means the last page.
Signature := TPdfVisibleSignature.Create(SignerFactory,
'Signature',
'I have read the document and certify it is valid.',
'Springfield',
'adrian@tmssoftware.com',
-1,
TUIRectangle.Create(50, 50, 140, 70),
ImgData);
end
else
begin
Signature := TPdfSignature.Create(SignerFactory,
'Signature',
'I have read the document and certify it is valid.',
'Springfield',
'adrian@tmssoftware.com');
end;
SignerFactory := nil; //The signature now owns the factory so we don't want to free it.
Except
Signer.Free; //Only if there is an error.
SignerFactory.Free;
raise;
end;
//A certifying signature (the default) says who is responsible for the document and which changes are
//allowed in it afterwards, and only the first signature of a document can certify it. An approval
//signature just says that whoever signed agrees with what the document says at that moment, and many of
//them can be added to the same document.
Signature.Certify := cbCertify.Checked;
//You must sign the document *BEFORE* starting to write it.
pdf.Sign(Signature); //Now the pdf owns the signature. There is no need to free it.
if (not SavePdfDialog.Execute) then exit;
pdf.ExportAllVisibleSheets(SavePdfDialog.FileName, false, 'Signed Pdf');
finally
Cert.Free;
end;
finally
pdf.Free;
end;
finally
xls.Free;
end;
if MessageDlg('Do you want to open the generated file?', mtConfirmation, [mbYes, mbNo], 0) = mrYes then
begin
ShellExecute(0, 'open', PCHAR(SavePdfDialog.FileName), nil, nil, SW_SHOWNORMAL);
end;
end;
procedure TFSigningPdfs.cbVisibleSignatureClick(Sender: TObject);
var
delta: integer;
begin
SignaturePicture.Visible := cbVisibleSignature.Checked;
Delta := SignaturePicture.Height + 30;
if (cbVisibleSignature.Checked) then Height := Height + delta else Height := Height - delta;
end;
procedure TFSigningPdfs.SignaturePictureClick(Sender: TObject);
begin
if (not OpenPictureDialog.Execute) then exit;
LoadImage(OpenPictureDialog.FileName);
end;
end.