-
Notifications
You must be signed in to change notification settings - Fork 3
Assurance Case
Source of truth: ASSURANCE-CASE.md, kept in the repository and copied here.
This document states what hMailServer claims about its own security, and the argument and evidence for each claim. It is written for someone deciding whether to run this on the public internet, and for anyone auditing that decision.
It is deliberately not a marketing document. Where a claim is limited, the limit is stated. Where a risk is accepted rather than eliminated, it is named in Residual risk.
Contents:
- What the software is
- Security requirements — the claims
- Threat model
- Trust boundaries
- Argument: secure design principles are applied
- Argument: common implementation weaknesses are countered
- Evidence
- Residual risk
- What the software is
hMailServer is a mail server for Windows: an SMTP, IMAP and POP3 implementation, with a management COM API, a Control Panel GUI, and optional REST API, metrics and web-services listeners. It stores mail on the local filesystem and configuration and accounts in a SQL database. It talks to the public internet by design, on ports that are open to anyone.
That last point sets the security posture: the primary interfaces are exposed to unauthenticated strangers, and most of them must accept input before any authentication has happened.
- Security requirements — the claims
These are the properties the project claims and argues for below.
| # | Claim |
|---|---|
| C1 | An unauthenticated network peer cannot execute code, corrupt memory, or crash the service through the mail protocols. |
| C2 | An unauthenticated peer cannot read, modify or delete mail belonging to an account, nor authenticate as one. |
| C3 | Credentials at rest cannot be practically recovered from a stolen database. |
| C4 | Credentials in transit are not exposed to a network attacker, and are never sent to an unverified peer. |
| C5 | Untrusted message content cannot alter the meaning of a database query, a filesystem path, or a protocol exchange with a third party. |
| C6 | An authenticated ordinary user cannot reach another user's mail, nor gain administrative capability. |
| C7 | A user can verify that the software they downloaded is the software the project published. |
What is explicitly not claimed is listed in Security Policy under Out of scope, and repeated in Residual risk.
- Threat model
| Actor | Position | Capability assumed |
|---|---|---|
| A1 — Internet stranger | Can open TCP connections to any listener | Arbitrary bytes, arbitrary volume, arbitrary timing, protocol non-compliance |
| A2 — Sending MTA | Delivers mail inbound | Everything A1 has, plus fully-controlled message content, headers and MIME structure |
| A3 — Network attacker | On-path between this server and a peer | Observe, modify, drop, replay; present a certificate; downgrade attempts |
| A4 — Authenticated user | Holds valid credentials for one account | Everything A2 has, plus authenticated IMAP/POP3/SMTP submission |
| A5 — Malicious upstream | Controls a DNS answer, a TLSA record, or a remote MTA | Poisoned resolution results, hostile TLS peer behaviour |
| A6 — Local attacker | Has a foothold on the host | Read files the service can read, observe process memory |
Administrators are not modelled as adversaries: an administrator can run event scripts and change configuration by design, so administrative access is equivalent to control of the service. This is stated in Security Policy and is a deliberate boundary, not an oversight.
Mail in transit and at rest; account credentials; TLS private keys; the configuration database; the availability of the service itself.
- Protocol parsers (SMTP, IMAP, POP3) — reachable pre-authentication by A1 and A2. The largest and most dangerous surface.
- MIME and header parsing — reachable by anyone who can send mail.
- TLS layer and certificate handling — reachable by A1, A3 and A5.
- Anti-spam and anti-virus paths, including DNS lookups and handoff to external scanners — driven by attacker-supplied content.
- DNS resolution, including DNSSEC, DANE and MTA-STS — influenced by A5.
- The persistence layer, reached indirectly with attacker-influenced data.
- REST API, metrics and web-services listeners, where enabled.
- Trust boundaries
Each boundary below is a place where data changes trust level and must be checked.
flowchart TB
subgraph actors["Actors"]
A1["A1 Internet stranger"]
A2["A2 Sending MTA"]
A3["A3 Network attacker, on-path"]
A4["A4 Authenticated user"]
A5["A5 Malicious upstream:<br/>DNS, TLSA, remote MTA"]
A6["A6 Local attacker"]
ADMIN["Administrator -<br/>NOT modelled as an adversary"]
end
B1["B1 - Network to process<br/>TCP and TLS termination<br/>Server/Common/TCPIP"]
B2["B2 - Bytes to protocol grammar<br/>SMTP, IMAP, POP3 command parsers<br/>MIME and header parsing"]
B3["B3 - Anonymous to authenticated<br/>credential check against a stored,<br/>key-stretched hash"]
BO["Business objects - per account<br/>Server/Common/BO"]
B4["B4 - Program to database<br/>parameterised statements only"]
B5["B5 - Process to external programs<br/>ClamAV, SpamAssassin, event scripts, DNS<br/>untrusted in BOTH directions, every call bounded"]
B6["B6 - Management plane<br/>the COM API, the single seam"]
A1 --> B1
A2 --> B1
A3 --> B1
A4 --> B1
A5 -.->|"poisoned answers,<br/>hostile TLS behaviour"| B5
A6 -.->|"reads what the service can read,<br/>observes process memory"| BO
ADMIN --> B6
B1 --> B2
B2 --> B3
B3 --> BO
B6 --> BO
BO --> B4
BO --> B5
- B1 — Network to process. Everything arriving is hostile until parsed. TLS is terminated here; peer verification happens before application data moves (see C4).
- B2 — Bytes to protocol grammar. Each command is accepted only in the form its grammar defines; anything else is rejected rather than interpreted generously.
- B3 — Anonymous to authenticated. Crossing it requires a credential check against a stored key-stretched hash. Below this line, an identity exists and every mail access is scoped to it.
- B4 — Program to database. Crossed only through parameterised statements; content never becomes query structure.
- B5 — Process to external programs and the network. Content handed to ClamAV, SpamAssassin, event scripts or DNS is untrusted in both directions, and every such call is time-bounded.
- B6 — Management plane. The COM API is the single seam through which configuration and management happen; it sits inside the administrative trust boundary described above.
- Argument: secure design principles are applied
Economy of mechanism / single seam. All configuration and management flows
through one interface — the COM API in Server/COM/ — used by the Control
Panel, the regression suite and external scripts alike. There is no second,
lightly-tested management path to audit. This is the layering documented in
Architecture.
Complete mediation. Mail access is mediated per account below B3; the persistence layer is reached only through the business objects, not directly from protocol code.
Fail-safe defaults. The server refuses to store a new secret under a
scheme that is not a password hash: configuring PreferredHashAlgorithm to
None, Blowfish or MD5 is logged as a misconfiguration and silently upgraded to
PBKDF2 rather than honoured. TLS peer verification is on by default, and a
verification failure fails the handshake rather than downgrading the
connection.
Least privilege, explicitly bounded. The administrative boundary is declared rather than assumed: administrators can run scripts and change configuration by design, and reports predicated on already having administrator access are out of scope. Drawing the line publicly is itself a design decision — it tells operators exactly what administrative access is worth to an attacker.
Defence in depth. No single control is relied on. Transport security is layered (STARTTLS and implicit TLS, DANE, MTA-STS, per-route TLS policy); content is filtered by more than one mechanism; and memory-safety risk is attacked from several directions at once (see §6).
Separation of secrets from code and configuration. TLS private keys are files referenced by path; password hashes live in the database; the optional server-wide pepper is an INI setting. No credential is compiled in.
Cryptographic agility. Password hashing (SHA256 / PBKDF2 / Argon2id), the TLS cipher list, and the TLS key-exchange groups are all configurable, so a broken primitive can be replaced by configuration rather than by waiting for a release.
- Argument: common implementation weaknesses are countered
SQL is exclusively parameterised, as a stated project rule in Contributing: "Use parameterised SQL exclusively — never build SQL strings manually." Message and header content therefore never becomes query structure. The rule is enforced at review, and the persistence layer offers parameterised interfaces as the path of least resistance.
This is C++, so this class cannot be argued away — it is attacked from four directions:
-
Compiler-enforced hygiene. The native build treats warnings as errors
(
TreatWarningAsError, MSVC/WX), so a new warning stops the build. -
Platform hardening.
/GSbuffer security checks, DEP (/NXCOMPAT) and ASLR (/DYNAMICBASE) are active on the shipped x64 binaries. - Static analysis. CodeQL analyses the C# tools on every push and pull request, and the C++ server on every push to master, weekly and on demand (not on pull requests - a change to the protocol parsers is analysed by dispatching the workflow against the branch before merging).
-
Fuzzing. A libFuzzer harness suite lives under
fuzz/, with a seed corpus regenerated from the real test messages (make-corpus.ps1; not committed), a MIME token dictionary and a committed regression directory of fixed findings, aimed at exactly the pre-authentication parsers that A1 and A2 can reach.
SMTP line-terminator handling is deliberately restrictive. Bare-LF spellings of the terminator are accepted only when Allow incorrect line endings is enabled, and even then are recognised only at the very end of the buffer with anything behind one discarded — the CVE-2023-51764 SMTP-smuggling rule. This behaviour is pinned by its own regression test, so it cannot be relaxed by accident.
Passwords are stored as per-user salted, key-stretched hashes — PBKDF2 by
default, Argon2id available — with salts from OpenSSL RAND_bytes and an
optional HMAC pepper mixed into Argon2id hashes. A minimum acceptable scheme
can be configured so legacy weak hashes stop being accepted for
authentication. SCRAM-SHA-256 is supported, and TOTP is available as a second
factor.
All security-relevant randomness comes from OpenSSL's CSPRNG (RAND_bytes),
with every call checked for failure: password-hash salts, SCRAM nonces, TLS
session-ticket keys and IVs, app passwords, TOTP secrets, REST API key
material and DNS/DNSSEC transaction IDs. Non-cryptographic generators appear
only in MIME part-boundary strings and OpenTelemetry trace identifiers,
neither of which is a security mechanism. A previous use of rand() for a
DNS-adjacent value was identified and replaced.
Outbound TLS sets verify_peer with verify_fail_if_no_peer_cert and
installs a verification callback that checks the chain and the expected
hostname, with a DANE verifier where a TLSA record applies. Because this is
installed before the handshake completes, credentials cannot be sent to an
unverified peer — which is claim C4.
Availability is treated as a security property, not a performance one. Several unbounded waits were found and fixed as defects in this fork — database-pool acquisition, event scripts, the custom virus scanner, the delivery-side ClamAV read/write timeout, and spam-test DNS lookups, which previously had no application-level timeout at all. Every crossing of boundary B5 is now time-bounded.
Every release asset is signed with Sigstore cosign (keyless) and verified by the workflow before upload, giving claim C7. Dependencies are enumerated in an SBOM, monitored by Dependabot, and gated by dependency review. Every vendored binary is inventoried with a SHA-256 and its provenance, and an unlisted or changed binary fails the build — closing the "a DLL appeared and nobody noticed" gap that a diff review cannot catch.
New in 6.2.28: the server's own update path applies the same verification.
UpdateChecker reads the release feed and SigstoreVerifier checks the
installer against its .cosign.bundle in-process - the file's SHA-256 is the
one signed and the one Rekor recorded, the certificate chains to the embedded
Sigstore trust root, and its identity must be this repository's
sign-release.yml workflow issued by GitHub's token service - so a substituted
installer is refused before it is run. Where the server reaches the
network through a forward proxy ([Settings] HttpProxy, also new in 6.2.28), an
https target is reached by CONNECT and the TLS handshake runs inside the tunnel
under the same certificate verification as a direct connection, so the proxy sees
the name it was asked for and nothing after it; and the release's own signature,
not the transport, is still what is trusted.
- Evidence
| Claim | Where the evidence is |
|---|---|
| C1 |
fuzz/ harnesses; CodeQL workflow; /WX in hMailServer.vcxproj; the regression suite under hmailserver/test/RegressionTests
|
| C2 | Authentication path and per-account scoping in Server/Common/BO/; regression tests for authentication |
| C3 |
Server/Common/Util/Hashing/HashCreator.cpp; IniFileSettings.cpp hash-scheme handling |
| C4 |
Server/Common/TCPIP/TCPConnection.cpp verify mode and callbacks; SslContextInitializer.cpp
|
| C5 | Parameterised-SQL rule in CONTRIBUTING.md; persistence layer; SMTP terminator tests |
| C6 | Account scoping in the BO layer; administrative boundary in SECURITY.md
|
| C7 |
.github/workflows/sign-release.yml; verify-binary-provenance.yml; sbom.yml
|
Release validation is not a smoke test: the full regression suite runs against
the exact binary being shipped, with live SpamAssassin and ClamAV (real EICAR
detection), DMARC, SPF and DKIM evaluated against a DNS zone the suite serves
itself (Shared/SuiteDns.cs, so a run cannot depend on the public DNS), and TLS
1.2 and 1.3 handshakes end to end. Nothing is mocked except the resolver, and
nothing is skipped.
Read a row as a sentence: this actor reaches this surface, which is defended at this boundary, in support of this claim, and the evidence is here.
| Actor | Surface it reaches | Boundary that defends it | Claim | Principal control | Evidence |
|---|---|---|---|---|---|
| A1 Internet stranger | SMTP, IMAP, POP3 command parsers; TLS handshake; optional listeners | B1, B2 | C1 | Grammar-strict parsing; bounded reads; absolute session ceilings; /guard:cf; /WX
|
fuzz/, CodeQL, the regression suite |
| A2 Sending MTA | Everything A1 reaches, plus MIME structure, headers, attachment names | B2, B5 | C1, C5 | The MIME fuzz harnesses; CrashOracle; bounded scanner calls |
fuzz/, Common/Util/CrashOracle.cpp
|
| A3 Network attacker | The TLS layer, in both directions | B1 | C4 |
verify_peer with verify_fail_if_no_peer_cert; DANE; MTA-STS; TLS 1.2 floor |
TCPConnection.cpp, SslContextInitializer.cpp
|
| A4 Authenticated user | Everything A2 reaches, plus authenticated IMAP, POP3 and submission | B3, B4, B6 | C2, C6 | Per-account scoping in the BO layer; ACLManager as the sole folder-access decider |
Common/BO/, build/check-authz-choke-point.py
|
| A5 Malicious upstream | DNS answers, TLSA records, remote MTA behaviour | B5 | C4, C5 | In-process DNSSEC validation; DANE matching; MTA-STS policy caching; every external call time-bounded |
DnssecResolver.cpp, DaneVerifier.cpp, SMTP/TlsPolicy.cpp
|
| A6 Local attacker | Files the service can read; process memory | outside B1–B5 | C3 | PBKDF2 by default, Argon2id and scrypt available; an optional server-wide pepper kept outside the database |
Hashing/HashCreator.cpp, IniFileSettings.cpp
|
| Anyone downloading | The published artefacts | — | C7 | Signed annotated tags; keyless Sigstore signatures recorded in Rekor; SPDX and CycloneDX SBOMs; a reproducible binary hash |
sign-release.yml, sbom.yml, verify-binary-provenance.yml
|
Section 6 argues eight weakness classes. This is which claim each one serves and where the control lives.
flowchart LR
subgraph cwes["Weakness classes argued in section 6"]
W89["CWE-89<br/>Injection"]
W119["CWE-119, 787, 125<br/>Memory corruption"]
W444["CWE-444<br/>Protocol smuggling"]
W287["CWE-287, 916<br/>Broken auth and<br/>credential storage"]
W338["CWE-338<br/>Weak randomness"]
W295["CWE-295<br/>Improper certificate<br/>validation"]
W400["CWE-400<br/>Uncontrolled resource<br/>consumption"]
W494["CWE-494<br/>Supply-chain and<br/>integrity"]
end
subgraph claims["Claims"]
C1["C1 no code execution,<br/>corruption or crash"]
C2["C2 no unauthenticated<br/>mail access"]
C3["C3 credentials at rest"]
C4["C4 credentials in transit"]
C5["C5 content cannot alter<br/>a query, a path or an<br/>exchange"]
C6["C6 no cross-user or<br/>privilege escalation"]
C7["C7 verifiable download"]
end
W89 --> C5
W119 --> C1
W444 --> C5
W444 --> C1
W287 --> C2
W287 --> C3
W287 --> C6
W338 --> C3
W338 --> C2
W295 --> C4
W400 --> C1
W494 --> C7
An assurance case that lists risks without saying who can act on them is only half useful.
| Residual risk | Who can reduce it | What that would take |
|---|---|---|
| Memory safety is mitigated, not guaranteed | The project | More fuzz targets — Sieve, ManageSieve and the SMTP command parser are named as untrusted and unwired in Fuzzing |
| Native statement coverage is not measured | The project | A coverage build of the C++ server; the .NET Control Panel is already measured in CI |
| Reproducibility shown on one toolchain only | A third party | An independent rebuild on a second machine with independently built OpenSSL, Boost and libpq |
| CFG is a mitigation, not a boundary | Nobody | A defect that overwrites data rather than a pointer is unaffected by /guard:cf, by design |
| Bus factor is 1 | The project, and volunteers | Governance describes how someone becomes a maintainer |
| Administrators are inside the boundary | The operator | Treat administrative credentials as equivalent to host credentials |
| Operator-chosen weakening is possible | The operator | Do not run plaintext listeners or legacy hash schemes. The server warns, but it obeys |
| The installer was not Authenticode-signed before 6.3.1 | The project | Closed 11 September 2026: the installer is signed with Azure Artifact Signing (the service formerly called Trusted Signing) from 6.3.1, with an RFC 3161 countersignature, naming Progressive Robot Ltd. Releases are immutable, so 6.3.0 and earlier stay unsigned. SmartScreen still warns until reputation accumulates, which a new installer every few weeks never does; the Sigstore bundle remains the check that proves provenance for every asset |
- Residual risk
Stated plainly, because an assurance case that claims no residual risk is not credible.
- Memory safety is mitigated, not guaranteed. This is a large C++ codebase with a pre-authentication attack surface. The controls in §6 raise the cost of exploitation; they do not make it impossible.
- Statement coverage is not uniform. The .NET Control Panel is measured for coverage in CI; the native server is not currently measured, so the regression suite's coverage of the C++ code is not quantified.
-
Reproducibility has been shown on one toolchain only. Since 22 August
2026 two clean Release builds of the same tree produce a byte-identical
hMailServer.exe(/Brepro,/d1trimfile,/pdbaltpath,OPENSSL_NO_FILENAMES; RELEASE.md step 8 repeats the check for every release and the notes carry the hash). What has not happened is an independent rebuild on a second machine with an independently built OpenSSL, Boost and libpq, so a third party's ability to confirm a released binary still rests on matching this toolchain exactly. Artifact signatures establish who published a binary; the hash lets anyone with the toolchain check that it corresponds to the source. -
Control Flow Guard is on, and its cost is small but real. Since 6.2.23
Alpha 2 the server is compiled and linked with
/guard:cf(Release and Debug), so an indirect call through a corrupted function pointer or vtable - the natural target of a memory-safety defect in the SMTP, IMAP, POP3 or MIME parsers - terminates the process rather than transferring control. The cost was measured rather than assumed: the full 1,838-test regression gate on the CFG build took 31 minutes on the build machine, against the 55 or so the previous release's gates took on the same machine, so whatever CFG costs on the indirect-call-heavy MIME path is below the run-to-run variance of the suite. The executable grew by about 97 KB of guard tables. CFG is a mitigation, not a boundary: a defect that overwrites data rather than a pointer is unaffected. - Bus factor is 1. A single maintainer performs security triage and releases. See Governance — a slow security response is a realistic failure mode.
- Administrators are inside the boundary. Anyone who can administer the server can run code on it. Operators must treat administrative credentials as equivalent to host credentials.
- Operator-chosen weakening is possible. The server permits plaintext listeners and legacy hash schemes for compatibility. It warns, but it obeys.
This assurance case should be revisited whenever the threat model changes — a new listener, a new external integration, a new authentication mechanism — and at minimum reviewed against the codebase once a year. Corrections are welcome through the normal pull-request process described in Contributing.
hMailServer 6.3.2 · AGPL-3.0-or-later · Repository · Report a documentation error
Hmail Server — full index
Start here
1. Install and run
- Before You Install
- Installing hMailServer
- Installing on Linux
- Running in a Container
- The Control Panel
- Your First Domain and Mailbox
- Connecting a Mail Client
- DNS for Your Domain
2. Secure it
3. Operate it
- Monitoring and Health
- Backup and Restore
- Troubleshooting
- Diagnosing Stalled Mail
- Relocating an Installation
- Upgrading hMailServer
- Upgrading Guide
- Migrating the Database Backend
- High Availability Runbook
- Warm Standby
- Runbooks Digest
4. Extend it
- Rules and Sieve
- Aliases Lists and Public Folders
- Routes and Relays
- The COM API and Scripting
- The REST API
- APIs Reference
5. Contribute to it
- Project Handbook
- Architecture
- Contributing
- Release Process
- Governance
- Assurance Case
- Regression Test Environment
- Fuzzing
- Regulatory Scope
- Third-Party Binaries
Look it up — from any journey