Signing a Windows program has always meant a Windows machine. signtool runs only on Windows, so a build pipeline that compiles on Linux still needs a Windows host somewhere, just to put a signature on the file at the end.
From sgcSign 2026.10 that host is optional. The Authenticode format in sgcSign, hashing the PE file and building the signature, is Delphi code rather than a call into the Windows signing APIs. On Windows the key operation still goes through CNG, and everywhere else it runs in sgcSign's own cryptography, so the components that sign an EXE on Windows build and run on Linux 64-bit, and Windows accepts the file they produce like any other signed file. This post shows a short console program that does it, how to build it for Linux, and how to check the result.
The Delphi program signing an EXE on Ubuntu, then the same file checked on Windows. Also on YouTube.
Why sign on Linux at all
- Build agents. Hosted CI runners and build containers are Linux first. Signing on the same agent that built the file removes a hop to a Windows machine and the credentials that hop needs.
- Where the key lives. A code signing key belongs in an HSM or a cloud key vault, and those are reached from Linux just as easily. The PKCS#11 and cloud key providers in sgcSign work on every platform.
- One tool everywhere. The same Delphi program signs on a developer's Windows box and on the release server, with the same options and the same output.
The program
A console program that takes the file to sign, the output file, a PFX with its password and, optionally, a timestamp authority. It is the same code on Windows and on Linux.
program SignExe;
{$APPTYPE CONSOLE}
uses
SysUtils, sgcSign_Authenticode, sgcSign_KeyProvider_PFX, sgcSign_TSA;
var
PFX: TsgcPFXKeyProvider;
TSA: TsgcTSAClient;
Signer: TsgcAuthenticodeSigner;
begin
if ParamCount < 4 then
begin
Writeln('usage: SignExe <input.exe> <output.exe> <file.pfx> <password> [tsa-url]');
Halt(1);
end;
PFX := TsgcPFXKeyProvider.Create(nil);
TSA := TsgcTSAClient.Create(nil);
Signer := TsgcAuthenticodeSigner.Create(nil);
try
try
PFX.LoadFromFile(ParamStr(3), ParamStr(4));
Signer.KeyProvider := PFX;
Signer.Hash := ahSHA256;
Signer.Level := alBES;
Signer.Description := 'sgcSign demo';
if ParamCount >= 5 then
begin
TSA.URL := ParamStr(5);
Signer.TSAClient := TSA;
Signer.Level := alT;
end;
Signer.SignFile(ParamStr(1), ParamStr(2));
Writeln('Signer: ', PFX.Certificate.Subject);
Writeln('Signed: ', ParamStr(2));
except
on E: Exception do
begin
Writeln('Error: ', E.Message);
ExitCode := 1;
end;
end;
finally
Signer.Free;
TSA.Free;
PFX.Free;
end;
end.
TsgcPFXKeyProvider holds the certificate and the private key. TsgcAuthenticodeSigner hashes the PE file the way Windows does, builds the PKCS#7 signature and writes it into the file's certificate table. With a TsgcTSAClient attached it also fetches an RFC 3161 timestamp, so the signature stays valid after the certificate expires.
Build it for Linux 64-bit
In the Projects window, add Linux 64-bit as a target platform, with a Linux SDK set up in the SDK Manager, then build. The sgcSign runtime packages are built for Linux64 from Delphi 10.3 onwards. On Linux the PFX is parsed in pure Pascal, since there is no Windows certificate store to import it into, and the RSA or ECDSA operation runs in sgcSign's own cryptography. The signature format around it is built by the same code that runs on Windows.
Run it and check the result
$ file Hello.exe
PE32+ executable (console) x86-64, for MS Windows
$ ./SignExe Hello.exe Hello-signed-linux.exe ../certs/demo.pfx demo http://timestamp.digicert.com
Signer: O=eSeGeCe Demo, CN=sgcSign Demo Code Signing
Signed: Hello-signed-linux.exe
To check it without Windows, osslsigncode verify recomputes the digest of the file and checks the signature over it: Signature verification: ok.
Then copy the file to Windows. PowerShell and signtool verify /pa /v read the same signature: the file hash Windows computes matches the one that was signed, the signer is the demo certificate, and the DigiCert timestamp verifies. The file still runs.
PS> Get-AuthenticodeSignature .\Hello-signed-linux.exe | Format-List SignatureType,
@{n='Signer';e={$_.SignerCertificate.Subject}},
@{n='TimeStamper';e={$_.TimeStamperCertificate.Subject}}
SignatureType : Authenticode
Signer : CN=sgcSign Demo Code Signing, O=eSeGeCe Demo
TimeStamper : CN=DigiCert SHA256 RSA4096 Timestamp Responder 2026 1, O="DigiCert, Inc.", C=US
The one complaint Windows makes is about the demo certificate, not the signature. The demo chain ends in a self-signed test root that is not installed on the machine, so Windows cannot tie it to a root it trusts. The digest, the signature and the timestamp all verify.
The same bytes on Linux and on Windows
Sign the same Hello.exe with the same PFX and no timestamp, once with the Linux build and once with the Windows build of the program above, and compare the two files:
Linux 82344 bytes SHA-256 6FD63BB01A8E911AB099431BC76F3BFA4EF557082BFB8BC18CCBFA97A9E8079D
Windows 82344 bytes SHA-256 6FD63BB01A8E911AB099431BC76F3BFA4EF557082BFB8BC18CCBFA97A9E8079D
They are identical, byte for byte. An RSA signature of this kind is deterministic, and the signer leaves the signing time out unless you ask for it, so the file depends only on the key and the input, not on the machine that signed it. A timestamp is the one part that differs from run to run, because the timestamp authority stamps the moment it is asked.
Where the key comes from
On Linux the key can come from a PFX or PEM file, from a PKCS#11 token or HSM, or from a cloud signing service: Azure Trusted Signing, AWS KMS, Google Cloud KMS, HashiCorp Vault or a CSC remote signing service. The Windows certificate store provider is the one that stays Windows only, for the obvious reason.
Not just EXE
The same holds for every code signing component in sgcSign: TsgcAuthenticodeSigner for EXE and DLL, TsgcCatalogSigner for .cat catalogs, TsgcMSISigner for Windows Installer .msi and .msp files, TsgcAppxSigner for .msix and .appx packages, TsgcPowerShellSigner for PowerShell scripts, and TsgcAuthenticodeVerifier to check any of them. All of them build and run on Linux, macOS, iOS and Android.
On the server and the command line
The same holds beyond the library. An sgcSign Server built for Linux signs and verifies Authenticode, PowerShell, catalog, MSI and MSIX requests exactly as it does on Windows, and the sgcsign command line tool sends them from any platform. One route stays Windows only in the Delphi command line tool: hashing an EXE or a DLL locally and sending only the hash. The .NET command line tool takes that route on every platform.
Availability
Code signing on Linux, macOS, iOS and Android ships in sgcSign 2026.10 for Delphi. Nothing changes for a Windows program: the same components, the same properties and the same output. Every property is documented in the sgcSign online help.
Questions, or a pipeline you want to move off Windows? Get in touch, and you will get a reply from the people who wrote the code.
