COR-170 - Verify TLS certificate hostnames on non-Apple platforms - #108
Conversation
On Windows, Android and Linux mailcore::checkCertificate() only validated that the server certificate chained to a trusted root; the hostname parameter was never used, so a valid certificate for any other domain was accepted for IMAP, SMTP, POP and NNTP (CWE-297). The Apple branch already evaluated SecPolicyCreateSSL(true, hostname). - MCCertificateUtils: attach the expected identity to the X509 verify parameters (X509_VERIFY_PARAM_set1_host / set1_ip_asc, no partial wildcards) so X509_verify_cert() fails with "hostname mismatch"; log X509_verify_cert_error_string() on failure; reject empty hostnames and malformed DER instead of proceeding. - checkCertificate() now fetches the chain from the stream and hands it to checkCertificateChain(cCerts, hostname, cTrustAnchors, verifyTime), whose body is the original verification unchanged. The extra trust anchors and the verify time exist for tests only; production passes NULL and 0 and keeps using the system store. - Send SNI on non-Apple platforms for every TLS path that lacked it: IMAP/SMTP/POP StartTLS and POP/NNTP direct TLS. Without SNI a server may present a default certificate for another name, which the new check would correctly reject. - CCertificateUtils: C entry point so the verifier can be unit-tested from Swift; CertificateUtilsTests cover exact/case-insensitive/ wildcard/IPv4/IPv6 matches and mismatches, wrong root, expiry, empty and malformed input, against fixtures in data/certificates generated with a private CA (leaf valid 2026-01-01..2027-12-31, verification pinned to 2026-06-15). Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
There was a problem hiding this comment.
🟡 Changes recommended
There is a confirmed memory leak on the OpenSSL path when sk_X509_push() fails, and at least one misleading test comment that should be corrected to match actual verification behavior.
Once you've addressed the issues Copilot identified, you can request another Copilot review.
Pull request overview
Updates MailCore2’s non-Apple TLS handling to verify certificate identity (hostname/IP) in addition to chain trust (COR-170), and ensures SNI server name is set for non-Apple TLS/StartTLS connections before post-handshake verification runs.
Changes:
- Add hostname/IP identity checks to non-Apple OpenSSL verification by setting the expected peer name on
X509_VERIFY_PARAM, and refactor verification intocheckCertificateChain(). - Add non-Apple libetpan
*_with_callbackhooks to set SSL server name (SNI) for IMAP/SMTP/POP/NNTP TLS and StartTLS paths. - Add a Swift unit test suite with pinned time and DER fixtures, and wire up new public headers/build scripts.
File summaries
| File | Description |
|---|---|
unittest/CertificateUtilsTests.swift |
Adds Swift tests exercising certificate chain + hostname/IP identity verification using DER fixtures and pinned verification time. |
src/include/MailCore/MCCertificateUtils.h |
Publishes the C++ checkCertificateChain() API in installed headers. |
src/include/MailCore/CCertificateUtils.h |
Publishes the C entry point used by Swift tests to invoke chain+identity verification. |
src/core/security/MCCertificateUtils.h |
Extends internal certificate utils header with checkCertificateChain() and test-only parameters (anchors/time). |
src/core/security/MCCertificateUtils.cpp |
Refactors verification to checkCertificateChain() and adds OpenSSL expected-identity verification (DNS/IP) plus better error reporting. |
src/core/smtp/MCSMTPSession.cpp |
Sets SNI server name via libetpan callback for non-Apple SMTP StartTLS (and uses callback path for TLS connect in file). |
src/core/imap/MCIMAPSession.cpp |
Sets SNI server name via libetpan callback for non-Apple IMAP StartTLS. |
src/core/pop/MCPOPSession.cpp |
Adds SNI callback usage for non-Apple POP TLS connect and StartTLS. |
src/core/nntp/MCNNTPSession.cpp |
Adds SNI callback usage for non-Apple NNTP TLS connect. |
src/c/utils/CCertificateUtils.h |
Adds C wrapper declaration for checkCertificateChain() (used from Swift). |
src/c/utils/CCertificateUtils.cpp |
Implements C wrapper converting Swift/C arrays into libetpan’s carray chain representation. |
src/cmake/public-headers.cmake |
Exposes core/security/MCCertificateUtils.h as a public installed header. |
src/CMakeLists.txt |
Adds libetpan to CMailCore link on Windows to satisfy new usages. |
Package.swift |
Wires the new Swift test file into the SPM test target. |
configure-headers.sh |
Ensures generated installed headers include the new certificate utility headers. |
Review details
Suppressed comments (1)
src/core/security/MCCertificateUtils.cpp:269
- On OpenSSL builds, if
sk_X509_push(certificates, certificate)fails, the newly-createdcertificateis leaked because it was never pushed onto the stack (sosk_X509_pop_free()won’t free it). Free it before jumping tofree_certs.
if (!sk_X509_push(certificates, certificate)) {
MCLog("MCCertificateUtils error: can't sk_X509_push");
goto free_certs;
}
- Files reviewed: 15/18 changed files
- Comments generated: 1
- Review effort level: Lite
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
…or comment COR-170 - IMAPInterruptCurrentCommandTests only needed Darwin for the POSIX socket API; import Android/Glibc instead so the suite runs on Android and Linux too. - CertificateUtilsTests: the private root is trusted in addition to the system store, not as the only anchor (Copilot review remark on PR #108). Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…sts on Android COR-170 MailCore's Android build has no process-wide main queue and Object::getMainQueue() aborts unless the app installed one through MCOOperation.setMainQueue(); in the XCTest process the test has to do it. Seen on the first Android run: the suite crashed with SIGABRT. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…OR-170 The certificate was never pushed onto the stack, so sk_X509_pop_free() in the cleanup path could not release it. Inherited from upstream; surfaced by the Copilot review of PR #108. Only reachable on allocation failure. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
There was a problem hiding this comment.
🔵 Needs a closer look
It changes TLS acceptance semantics across all non-Apple platforms and needs careful human validation of interoperability and failure modes despite the added unit coverage.
Review details
Suppressed comments (1)
Previously missed (1) — in code that hasn't changed since the last review.
src/core/security/MCCertificateUtils.cpp:86
- checkCertificateChain() can be called with an empty (but non-NULL) certificate chain (e.g. the Swift C wrapper builds an empty carray when the input array is empty). In the OpenSSL path this leads to sk_X509_value(certificates, 0) == NULL being passed into X509_STORE_CTX_init(), which relies on OpenSSL’s handling of a NULL leaf and makes failure mode/diagnostics platform-dependent. Add an explicit early return for NULL/empty chains so the function rejects them deterministically.
- Files reviewed: 16/19 changed files
- Comments generated: 0 new
- Review effort level: Lite
Note
High Risk
Changes TLS certificate acceptance for all mail sessions on OpenSSL/non-Apple platforms; misconfiguration could break legitimate servers or, if wrong, reintroduce hostname bypass.
Overview
Fixes COR-170: on non-Apple builds, OpenSSL verification previously checked chain trust only and could accept a valid certificate for a different host than the session hostname (CWE-297). The OpenSSL path now sets expected DNS or IP identity on the verify parameters before
X509_verify_cert(), aligning with Apple’sSecPolicyCreateSSLbehavior.Verification logic is refactored into
checkCertificateChain()(with optional test-only trust anchors and pinned verify time); production still uses the stream wrapper unchanged. ACCertificateUtils_checkCertificateChainC entry point andCertificateUtilsTestsexercise the same path Swift clients rely on after TLS.On non-Apple platforms, IMAP/SMTP/POP/NNTP TLS/StartTLS now use libetpan
*_with_callbackhelpers to set the SSL server name for SNI. Build/header wiring and Android/Linux tweaks forIMAPInterruptCurrentCommandTestsare included so the new tests run in CI.Reviewed by Cursor Bugbot for commit c87ff7d. Bugbot is set up for automated code reviews on this repo. Configure here.