hMailServer 6.3.1
This is the first release of this project to carry an Authenticode signature. The Windows elevation prompt names Progressive Robot Ltd instead of Unknown publisher, and an enterprise policy that refuses unsigned binaries outright stops being a wall.
Nothing in the server changes. The compiled server differs from 6.3.0 by its version stamp and one comment line; everything else in this release is the path a release travels - the workflows that sign and verify it, found wanting while preparing to use the certificate for the first time - and three documentation corrections.
The installer is signed
The installer is signed with Azure Artifact Signing, against a certificate profile issued to Progressive Robot Ltd after Microsoft's identity validation. The account, the validation and the profile were arranged on 10 September 2026, after 6.3.0 had shipped. A published release here is immutable, so 6.3.0 could not be signed retroactively and never will be.
What it changes. The elevation prompt reads the publisher's name. And for administrators on managed estates - a great many of this server's users - a policy that refuses unsigned binaries no longer refuses this one.
What it does not change, said plainly: SmartScreen still warns. Microsoft's own comparison puts a signed installer in the same row as an unsigned one, flagged as unrecognised until reputation accumulates, and reputation attaches to a file that does not change - which a new 80 MB installer every few weeks never is. An EV certificate would not help; Microsoft removed EV's SmartScreen bypass in 2024. Expect the same Windows protected your PC screen, now with the publisher's real name on it.
Only the Windows installer is Authenticode-signed. There is no Authenticode for a .deb, an .rpm or an AppImage. Sigstore is unchanged and remains the check that means something for every asset on the release: each carries a .cosign.bundle beside it, and cosign verify-blob against that bundle verifies it.
One setting becomes usable. UpdateRequireAuthenticode=1 has existed since 6.2.28 and, until now, made the server's own update path refuse every release of this project, because none carried a signature: the check is WinVerifyTrust on the downloaded installer, and an unsigned file is refused with the file carries no Authenticode signature. From 6.3.1 the installer it downloads carries one. The check is Windows-only: on Linux there is no Authenticode to verify, the server says so rather than reporting a pass, and updating is the package manager's job.
The release path
Releases here are immutable, so each of these could have put out a permanently wrong release.
- The Authenticode gate was checking the wrong four of the six settings it guards. It asked for the three Azure secrets and
ARTIFACT_SIGNING_ACCOUNT- the one input the signing action marks optional - and never asked aboutARTIFACT_SIGNING_ENDPOINTorARTIFACT_SIGNING_PROFILE, the two it marks required. Five half-configured states got through, each a state somebody passes through while filling in three repository variables one at a time; the action throws on an empty endpoint, and the cosign job declares a dependency on this one, so a release that tripped it would have got no Authenticode signature and no Sigstore bundle on any of its twenty assets. There are three outcomes now: none of the six set is the ordinary case and stays a silent no-op, all six signs, and anything in between stops and names what is missing while the release can still be fixed. The six values, and the tag, reach the shell through the environment rather than being pasted into the script text, because an expression pasted into a script is executed as syntax if it holds a quote. - The release is verified as it is attached, not as it was signed. The old check looked only at the job's own copies. A draft stays mutable afterwards, and four things upload over it with
--clobber: a re-run of the Linux publish job, the SBOM workflow, a second Authenticode run, and the hand-recovery commands the Linux job prints into its own log. The workflow now ends by re-downloading the release exactly as it stands and verifying every asset against its attached bundle, in both directions - an asset with no bundle, and a bundle whose asset has gone. - What a release must carry is asserted before a certificate is spent on it. The only completeness check was "did we sign at least one thing", and the SBOMs were named nowhere but in comments - which is how 6.2.22-pre4 came to be published with an installer and no SBOM at all. Both SBOMs are a hard failure now, in the Authenticode job rather than the signing job, so an incomplete release stops before a code-signing certificate has been spent on it. The Linux sums file and the four package names stay warnings, deliberately: a Linux packaging failure must not hold back the Windows installer every server in the field is waiting for.
- An RFC3161 countersignature is required, and is checked. These certificates live about seventy-two hours.
Get-AuthenticodeSignatureanswers Valid for a signature carrying no timestamp for as long as the certificate has not expired, which it never has seconds after signing - so an untimestamped installer would look perfect on the day and stop verifying on every machine in the field three days later, on a release that cannot be corrected. The countersignature is proved before the release asset is replaced, so a timestamping failure costs a re-dispatch and nothing else. - Verification can no longer pass vacuously. A glob that matched nothing used to verify nothing and succeed. Both verification steps now refuse to pass having checked zero assets, and compare what they verified against what the signing step reported.
- Runs are serialised per tag. Two dispatches for one tag could interleave, and one run's bundle could end up describing bytes the other had already replaced. They queue now, and are not cancelled: a run stopped between signing and uploading would leave assets with no signature at all.
- The SBOM workflow refuses to overwrite an SBOM that has already been signed, because cosign signs the bytes attached when it runs and Syft writes a fresh document identifier every time, so a re-upload after signing leaves a signature that verifies against nothing. It also proves the tree it describes is the tag's own commit before Syft runs - a bill of materials describing a tree the release was not built from is the one thing an SBOM must never do.
- The installer smoke test can read a draft release, which is the only kind it is ever pointed at. It asked for
contents: read, and GitHub shows an unpublished release only to a token with push access, so every dispatch against a draft answered "release not found" - which made the documented order, green before publication, impossible rather than merely awkward. It asks forcontents: writenow, and writes nothing. - The Linux package-install check runs under
sudo, like every other command in its step. The package makes/etc/hmailserver0750 root:hmailserveron purpose, because the configuration holds credentials, so a barestatanswered "Permission denied" and underset -etook the job with it. That is why 6.3.0's tagged run attached no Linux packages and they had to be attached by hand.
All of the above was verified by executing the real run bodies against the live 6.3.0 asset list with gh and cosign stubbed - the twenty-asset release, a re-run over already-attached bundles, a Windows-only release, a pre-release tag, the hand-recovery shape, a release with no SBOMs, an orphaned bundle, an asset excluded from signing by design, and an empty download - and the gate across fifteen configurations, including whitespace-only values and five hostile ones, none of which executed anything. The Authenticode path itself was rehearsed end to end on a throwaway copy of the 6.3.0 installer before this tag was cut: a valid, countersigned signature naming Progressive Robot Ltd, attached to nothing.
Documentation
- The README offers the Linux packages on its download line, where a reader looks for a download, rather than 140 lines further on in the Linux section. The headline of 6.3.0 was that this server runs on Linux at all.
- Two files named 6.2.29, a version that does not exist and never will: the
[Directories]header comment in the server source, and the relocating guide's note about when a relative path began to resolve against the program folder. Both features shipped in 6.3.0 and both now say so. Comment and prose only. - The release checklist gives
ARTIFACT_SIGNING_ENDPOINTas a full URI rather than a bare host, which is the form the signing action documents and one of the two values that used to pass the old gate and then fail inside the action, and it says the six settings are required together.
Known, and not fixed here
The orphan sweep in the 6029 to 6030 upgrade step runs children before parents. Before that step adds seventeen foreign keys, it deletes rows whose parent is gone. Three of the parent tables are themselves pruned by those deletes, and each one's children are cleaned earlier, against a parent that has not yet lost its rows - so pruning an orphaned account, fetch account or distribution list re-orphans rows nothing revisits, and the constraint that follows is refused. It affects only an upgrade from a schema below 6030 on a database that already holds orphaned rows; a database without them upgrades correctly, a fresh installation is never on this path, and when it does fire it fails loudly with the engine's own words and rolls back rather than doing anything quietly. It is in all four database backends. No installation has reported hitting it. The fix is a reordering of seventeen delete statements and is held for 6.3.2, because a change to a database upgrade script belongs in a release whose gates can execute an upgrade - and this project's cannot yet, which is exactly how the defect was able to ship. That gate is being built first.
The report that led to this - a Compact Edition upgrade from schema 6011 refused at the same step (#93) - turned out to be #114, already fixed in 6.2.26: the upgrade had succeeded and the verification probe had crashed the database provider afterwards, and the tool then printed a cause it had no way to know. That installation upgrades on 6.3.0 and on this release.
Verification
The full regression suite ran on the stamped binary: 2,175 tests, 2,166 passed, 0 failed, 9 skipped (the seven explicit IMAP stress tests and two ignored), in 40 minutes, with no ERROR log written. hMailServer.exe 6.3.1.40, SHA-256 8b0ab1a2d1f3645f1e12c3bca1315023474bb3a63abf0efb44f60ba0b2d83235.
The installer has two hashes, and both are given deliberately. hMailServer-6.3.1-x64.exe as built, 79,408,192 bytes: SHA-256 3986fbcccda9e46fee29c1e538a79f2423ebc4d5e3f6e59306ffba4cc7bd8cba. As published, 79,423,896 bytes: SHA-256 68c72a60e7a8530bdd32d28b60eaadab5b9c90505e174c03196e5dfa58ac9b70. They are different, and this is the first release for which that is so: the signing job replaces the installer asset with the signed one before cosign runs, so the bytes on the release are not the bytes the build produced. The second is the one to check a download against. Verified after download with Get-AuthenticodeSignature: Valid, signer CN=Progressive Robot Ltd, O=Progressive Robot Ltd, L=Chester, S=Cheshire West and Chester, C=GB, issued by Microsoft ID Verified CS EOC CA 03 for three days from 10 September 2026 19:35 UTC, countersigned by Microsoft Public RSA Time Stamping Authority - which is what keeps the signature valid after the certificate itself has expired. The installer smoke test ran against this signed file on a throwaway runner and passed.
The assertion build, first. The same source compiled with every HM_ASSERT kept - a violated one is reported as HM6364 in the error log rather than compiled out - and the whole suite run on it: 2,175 tests, 0 failed, 9 skipped (the seven explicit IMAP stress tests and two ignored), no assertion violated anywhere in the run.
Reproducible. Two clean Release builds of the stamped source produced a byte-identical hMailServer.exe, SHA-256 8b0ab1a2d1f3645f1e12c3bca1315023474bb3a63abf0efb44f60ba0b2d83235.
Fuzzing. No parser changed between 6.3.0 and this release, so 6.3.0's run stands, as the release checklist allows for a patch release: the three MIME harnesses, thirty minutes each under libFuzzer and ASan, 8,296,354 executions, no crash, hang or assertion.