An unsigned installer is the first thing a customer sees go wrong. SmartScreen warns about an unknown publisher, the UAC prompt shows no name, and an MSIX package will not install at all, because Windows only installs a package that carries a valid signature.
sgcSign 2026.10 adds two components for it. TsgcMSISigner signs Windows Installer .msi and .msp files, and TsgcAppxSigner signs .msix and .appx packages and their bundles. Both work exactly like the Authenticode signer you may already use for an EXE: a key provider, an optional timestamp authority, one call.
An MSI and an MSIX signed from the Delphi program, and the publisher check stopping a package that would never install. Also on YouTube.
The program
One console program that picks the signer from the file extension, signs with SHA-256 and, when a timestamp authority is given, adds an RFC 3161 timestamp.
program SignInstaller;
{$APPTYPE CONSOLE}
uses
SysUtils, StrUtils, sgcSign_Authenticode, sgcSign_MSI, sgcSign_APPX,
sgcSign_KeyProvider_PFX, sgcSign_TSA;
var
PFX: TsgcPFXKeyProvider;
TSA: TsgcTSAClient;
Signer: TsgcAuthenticodeSigner;
begin
if ParamCount < 4 then
begin
WriteLn('usage: SignInstaller <input.msi|input.msix> <output> ' +
'<file.pfx> <password> [tsa-url]');
Halt(1);
end;
if MatchText(ExtractFileExt(ParamStr(1)), ['.msi', '.msp']) then
Signer := TsgcMSISigner.Create(nil)
else
Signer := TsgcAppxSigner.Create(nil); // .msix .appx and bundles
PFX := TsgcPFXKeyProvider.Create(nil);
TSA := TsgcTSAClient.Create(nil);
try
try
PFX.LoadFromFile(ParamStr(3), ParamStr(4));
Signer.KeyProvider := PFX;
Signer.Hash := ahSHA256;
Signer.Level := alBES;
if ParamCount > 4 then
begin
TSA.URL := ParamStr(5);
Signer.TSAClient := TSA;
Signer.Level := alT;
end;
if Signer is TsgcAppxSigner then
begin
WriteLn('Publisher : ', sgcAppxReadPublisher(ParamStr(1)));
WriteLn('Certificate: ', PFX.Certificate.SubjectRFC2253);
TsgcAppxSigner(Signer).SignFile(ParamStr(1), ParamStr(2));
end
else
TsgcMSISigner(Signer).SignFile(ParamStr(1), ParamStr(2));
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.
Both signers descend from TsgcAuthenticodeSigner, so the key provider, the digest, the timestamp level and the description are set on the common base exactly as for an EXE, and SignFile can be called through the base type as well as on the concrete class.
Signing a Windows Installer package
An .msi is not a PE file. It is a compound file, a small file system of streams and storages, and the digest Windows checks is computed over those streams in a precise order, with the signature streams left out. TsgcMSISigner computes that digest, builds the PKCS#7 signature and writes it into the package's DigitalSignature stream. SHA-1, SHA-256, SHA-384 and SHA-512 are all accepted, and an RFC 3161 timestamp keeps the signature valid after the certificate expires.
Patch packages (.msp) are signed the same way. With AppendSignature set, a package that is already signed takes a second signature next to the first, the way signtool /as appends one to an EXE.
Signing an MSIX or APPX package
An MSIX is a ZIP with a block map, and what Windows signs is not a hash of the file but a small structure of digests over its parts. TsgcAppxSigner builds that structure, signs it and writes the result into the package as AppxSignature.p7x. If the package does not declare the signature part yet, it is added. Windows only accepts SHA-256 for a package, so that is the only digest offered, and signing a package that is already signed replaces the old signature.
The publisher check
This is the mistake that costs an afternoon. The Publisher in the package manifest has to be exactly the subject of the signing certificate. When the two differ, Windows does not report a bad signature. It stops recognising the package and calls it a file format it cannot verify, which looks like a corrupt build.
TsgcAppxSigner checks it before signing and stops with a message that names both values:
> SignInstaller.exe DemoApp-wrongpublisher.msix out.msix ..\certs\demo.pfx demo
Publisher : CN=Someone Else
Certificate: CN=sgcSign Demo Code Signing,O=eSeGeCe Demo
Error: APPX: the signing certificate subject "O=eSeGeCe Demo, CN=sgcSign Demo Code Signing"
does not match the Publisher declared in AppxManifest.xml ("CN=Someone Else"). Windows
requires them to be identical and refuses a package where they differ; signtool rejects
the same combination with 0x8007000B. Sign with a certificate whose subject matches the
manifest, or rebuild the package with the Publisher set to the certificate subject.
The comparison is done by attribute type and value, not as a string, so spacing and ordering differences between the manifest and the certificate do not cause a false alarm. ValidatePublisher (True by default) turns the check off, and sgcAppxReadPublisher and sgcAppxDNMatches let you run the same comparison yourself, for example to fail a build before it reaches the signing step.
Checking the result
> SignInstaller.exe DemoApp.msi DemoApp-signed.msi ..\certs\demo.pfx demo http://timestamp.digicert.com
Signed: DemoApp-signed.msi
> SignInstaller.exe DemoApp.msix DemoApp-signed.msix ..\certs\demo.pfx demo http://timestamp.digicert.com
Publisher : CN=sgcSign Demo Code Signing, O=eSeGeCe Demo
Certificate: CN=sgcSign Demo Code Signing,O=eSeGeCe Demo
Signed: DemoApp-signed.msix
> Get-AuthenticodeSignature DemoApp-signed.msi, DemoApp-signed.msix |
Format-List Path, @{n='Signer';e={$_.SignerCertificate.Subject}},
@{n='TSA';e={$_.TimeStamperCertificate.Subject}}
Path : DemoApp-signed.msi
Signer : CN=sgcSign Demo Code Signing, O=eSeGeCe Demo
TSA : CN=DigiCert SHA256 RSA4096 Timestamp Responder 2026 1, O="DigiCert, Inc.", C=US
Path : DemoApp-signed.msix
Signer : CN=sgcSign Demo Code Signing, O=eSeGeCe Demo
TSA : CN=DigiCert SHA256 RSA4096 Timestamp Responder 2026 1, O="DigiCert, Inc.", C=US
signtool verify /pa /v on Windows finds the same signer and the DigiCert timestamp on both files, and the digest it computes is the one that was signed. Its one complaint is 0x800B010A, because the demo chain ends in a self-signed test root that is not installed on the machine. A broken digest or signature would be 0x80096010 instead. TsgcAuthenticodeVerifier recognises an installer package and an MSIX package on its own, so the same call that verifies an EXE verifies these too.
On the server and the command line
The sgcSign Server signs both formats at /api/v1/sign/msi and /api/v1/sign/appx, and the command line tool takes --format msi and --format appx. Both also have a hash-only route, so a large installer never crosses the network, which is the subject of the next post in this series.
Availability
TsgcMSISigner and TsgcAppxSigner ship in sgcSign 2026.10 for Delphi and C++Builder, together with the server routes and the command line formats. They build and run on Linux, macOS, iOS and Android as well as Windows. Every property is documented in the sgcSign online help.
Questions about a package that will not sign or will not install? Get in touch, and you will get a reply from the people who wrote the code.
