Version 2.0.4.10
Latest Release Bug Fix Release Security PatchRelease Date: June 2026
Version 2.0.4.10 is a bug-fix, performance, and security release. It resolves CVE-2026-34181 — a PKCS#12 PBMAC1 authentication bypass vulnerability in the go-pkcs12 dependency (upgraded from v0.7.1 to v0.7.3) — alongside a significant scan performance improvement on networks with firewall-dropped (blackholed) ports, reducing per-port wait time from ~179 seconds to ~2 seconds, and five additional fixes covering browser extension detection on macOS and Linux, browser extension detection under SYSTEM accounts on Windows, Elasticsearch document delivery when combined with JSON file output, RHEL 9 cost analysis field emission, and missing unique ID fields on six flat NDJSON event datasets.
What's Fixed at a Glance
keyLength < 20 outright./Users/* and /home/* rather than the process owner's home directory.%SYSTEMDRIVE%\Users\* instead of SYSTEM's own home directory.-outputformat json and -posttoelastic are used together, all documents are now correctly sent to Elasticsearch — not just the first ~200.Platform = "redhat" (no space) were silently excluded from the Cost Analysis dashboard. Now correctly emits all os_* cost fields.id field, causing duplicate indexing in Elasticsearch and Splunk pipelines. All six emission points now generate stable deterministic IDs.-ports 1-1000,1001-2000). Port validation now counts total expanded ports across all ranges before accepting the input.Performance Improvements
Blackhole & Filtered Port Handling — Up to 90× Faster
Scale testing against 40 VMs × 1,000 ports revealed three compounding issues: firewall-dropped (blackholed) ports caused each goroutine to wait ~179 seconds (18 sequential TLS probes × 5–8s each), the 1,000-port cap could be bypassed via comma-separated ranges, and at 40 × 1,000 scale, peak memory reached 14 GB — triggering the OOM killer during report serialization on Linux. All three issues are resolved in 2.0.4.10.
- TCP pre-flight check — new
probePortReachability()classifies each port asopen,closed(RST), orfilteredbefore any TLS work begins. Filtered and closed ports return immediately. - Short-circuit on timeout — if the first PQC probe in
goNativeFetchCertsreturns a network timeout, all 16 remaining hybrid probes are skipped. A non-timeout TLS error (e.g. handshake_failure) still continues probing. - Global TLS timeout flag — new
-tls-timeout <seconds>flag (default: 4s) replaces all hardcoded 5s/8s probe timeouts across remote, ARP, connected, and local scan paths. - Streaming JSON serialization — full-detail JSON output now streams directly to the file handle via
json.NewEncoderinstead of allocating the entire marshaled output as a[]bytefirst. Eliminates up to 14 GB of intermediate allocation on large scans. - Port cap enforcement —
validatePorts()now counts total expanded ports across all comma-separated ranges. Inputs like-ports 1-1000,1001-2000(previously accepted) are now rejected.
| Scenario | Before | After |
|---|---|---|
| Closed port (RST) | ~0ms | ~2ms |
| Open TLS port (443) | ~63ms | ~63ms |
| Open non-TLS port | ~179s | ~39s |
| Blackholed port | ~179s | ~2s |
| Full 8-port scan | ~6+ min | ~41s |
| Peak memory (40 × 1k) | ~4.6 GB | ~3.2 GB |
New flag: -tls-timeout <int> — sets the global TLS/SSH probe timeout in seconds. Default is 4. Set to 8 to restore the original per-probe timeout for environments where servers are slow to respond.
New flag: -no-tcp-preflight — disables the TCP pre-flight reachability check. Use with -tls-timeout 8 to fully reproduce the original scan behavior if required for compatibility.
Port cap change: The -ports flag now enforces the 1,000-port limit across all comma-separated ranges combined.
Previously, -ports 1-1000,1001-2000 was accepted. Any automation that passed multiple ranges summing above 1,000 will now receive a startup error — adjust port lists accordingly.
Security Fixes
PKCS#12 PBMAC1 Authentication Bypass — go-pkcs12 v0.7.1 → v0.7.3
go-pkcs12 v0.7.1 accepted PKCS#12 files that used PBMAC1 (RFC 9579) as their MAC algorithm with excessively short HMAC keys — as short as 1 byte. A 1-octet PBKDF2-derived key has only 256 possible values, so an attacker can brute-force the MAC offline and forge a trust-store entry that authenticates under any target password with ~1-in-256 probability. This is an authentication bypass without requiring the original password.
PKCS#12 keystores are a primary format scanned during local endpoint scans, producing
tychon.keystore and
tychon.keystore_certificate events.
A forged .p12 or .pfx file placed on a scanned endpoint could cause the scanner to emit certificate inventory
records that misrepresent the actual cryptographic material present.
- CVE: CVE-2026-34181
- Affected component:
software.sslmate.com/src/go-pkcs12 - Affected versions: < v0.7.2
- Fixed in: v0.7.3 (includes v0.7.2 key-length validation)
- PBMAC1 OID:
1.2.840.113549.1.5.14(RFC 9579) - Attack: Brute-force 1-octet PBKDF2 key → forge HMAC → file authenticates under wrong password
- Scope: Only PKCS#12 files using PBMAC1 MAC (RFC 9579 modern format). Legacy HMAC-SHA1 files are unaffected.
- Short-key rejection: PBMAC1 files with
keyLength < 20are now rejected before MAC verification with: "pkcs12: PBMAC1 key length N is too short to be secure (minimum 20 octets)" - Long-key rejection:
keyLength > 64rejected to prevent large PBKDF2 allocations. - Missing key length: Files with no
keyLengthfield in PBKDF2 params are now rejected (previously silently defaulted to 0). - Scanner integration: All rejections surface in the keystore record's
error_messagefield with"accessible": false.
Evidence & Validation: A standalone validation program at
evidence/TQR-1162/main.go runs four assertions — library rejection,
scanner-level rejection, valid PBMAC1 round-trip, and scanner accessibility of valid modern PKCS#12 — all pass
on build 2.0.4.10. The CVE fixture (evidence/TQR-1162/testdata/pbmac1-short-key-bypass.txt)
is the purpose-built brute-forced salt fixture shipped with go-pkcs12 v0.7.3.
Run with: go run evidence/TQR-1162/main.go
Scope note: The vast majority of .p12/.pfx files in production use legacy HMAC-SHA1 MAC (the default for OpenSSL, Java keytool, and Windows
certificate export). PBMAC1 is only used by tools that explicitly request RFC 9579 modern PKCS#12 encoding (e.g. go-pkcs12's Modern
encoder). Existing deployments are unlikely to encounter PBMAC1 files in practice unless clients have adopted RFC 9579 tooling.
Bug Fixes
Scan Behavior
Browser extension scan generates zero records on macOS and Linux
Running -scan-browser-extensions on macOS Intel, macOS M1, and Linux produced zero browser extension records
even on hosts with Chrome, Edge, and Firefox extensions installed. Windows was unaffected and produced the expected records.
The scan reported "Browser extension scan complete: 0 extension(s) found" on all non-Windows platforms.
Root cause: resolveHomeDirs() on macOS and Linux called
os.UserHomeDir(), which returns the home directory of the process owner.
When the scanner runs as root (e.g. via sudo, a launchd service, or a TYCHON agent deployment),
this resolves to /var/root on macOS or /root on Linux —
directories where no user browser profiles exist.
resolveMacOSHomeDirs() enumerates /Users/* directly, skipping
Shared, Guest, and any entry that is not a directory. The current process home
(os.UserHomeDir()) is still appended as a fallback for non-root single-user runs, with deduplication.
All discovered home directories are walked for Chrome, Edge, and Firefox profiles.
resolveLinuxHomeDirs() enumerates /home/* and applies the same pattern —
skipping non-directory entries, appending the process home as a fallback, and deduplicating.
Matches the enumeration logic already used by the installed-app scanner on Linux.
Also fixed in the same commit: filepath.Join("C:", "Users") Windows path bug (produces C:Users, CWD-relative) corrected to an explicit absolute path.
| Platform | Before | After |
|---|---|---|
| macOS Intel (Ventura 13.7.8) | 0 records | Extensions found |
| macOS M1 (Sequoia 15.6.1) | 0 records | Extensions found |
| Linux (RHEL 9.7) | 0 records | Extensions found |
Validated on macOS (16 extensions found across Chrome, Edge, and Firefox) and Linux. Any elevated user — not just root — now finds extensions across all local user profiles on the machine. Windows behavior is unchanged.
Browser extensions not detected when scanner runs as NT AUTHORITY\SYSTEM (Windows)
When deployed as a Windows service or run under NT AUTHORITY\SYSTEM via a tool like PsExec, the browser extension scanner
found zero extensions even on machines with dozens installed for regular users. Two compounding bugs caused this:
os.UserHomeDir() resolved to
C:\Windows\System32\config\systemprofile (SYSTEM's profile, where no browser data lives),
and filepath.Join("C:", "Users") produces
C:Users (CWD-relative) rather than
C:\Users — so under SYSTEM, whose CWD is
C:\Windows\System32, the directory walk silently searched the wrong path and returned nothing.
resolveHomeDirs() on Windows enumerates
%SYSTEMDRIVE%\Users\* using an absolute UNC path, skipping Public, Default, and All Users.
chromiumProfileRoots() and firefoxProfileRoot() now build paths from enumerated home directories directly instead of relying on
LOCALAPPDATA / APPDATA environment variables (which point to SYSTEM's dirs when the process token is SYSTEM).
ScanBrowserExtensions() iterates all discovered home directories rather than a single one.
HKEY_CURRENT_USER registry reads for the installed app scanner are replaced with
HKEY_USERS enumeration. A new isRealUserSID() helper filters to real user SIDs
(S-1-5-21-* for domain/local, S-1-12-* for Azure AD/Microsoft accounts),
skipping SYSTEM, LOCAL SERVICE, NETWORK SERVICE, and *_Classes redirects.
Both 32-bit and 64-bit Uninstall registry paths are scanned per user.
Side effect: any elevated admin user (not just SYSTEM) now finds extensions and installed apps across all logged-in user profiles on the machine — the correct behavior for a system-wide scanner.
Output & Data Delivery
Only ~200 documents sent to Elasticsearch when -outputformat json and -posttoelastic are combined
Using -outputformat json -posttoelastic together resulted in only a single batch (~200 documents) reaching
Elasticsearch, while the same scan with -outputformat ndjson correctly sent thousands. The root cause was
that the JSON serialization path performed a shallow copy of the report struct and zeroed fields on the copy before marshaling to disk.
Although the original report was not mutated, this path was producing an unexpectedly limited document set for the direct Elasticsearch sender.
The fix ensures the report passed to PostReportToElasticsearch(), Kafka, and Splunk senders
is always the fully-populated original — regardless of output format selected.
Validated across Windows, Linux, macOS Intel, and macOS M1: Elasticsearch received 10,530–51,689 documents per OS; Kafka received 987–2,274 messages; Splunk received 11,377–52,359 events — all matching ndjson-only runs.
Missing top-level id field on six flat NDJSON event datasets
Six emission points in the flat NDJSON output path were not generating a top-level id field.
This caused duplicate document indexing in Elasticsearch pipelines and caused Splunk deduplication to fail for these datasets.
GenerateUniqueID() calls have been added at all six points with stable, deterministic IDs derived from the host UUID plus asset-specific key fields.
| Dataset | ID Key Fields |
|---|---|
tychon.keystore_certificate (primary scan) |
observer.id + keystore path + cert serial + thumbprint |
tychon.keystore_certificate (config scan) |
observer.id + keystore path + cert serial + thumbprint |
tychon.config_cert |
observer.id + config file + property key + resolved path |
tychon.cipher_quick |
observer.id + scanned host + port |
tychon.application (no-port path) |
observer.id + app name + app path |
tychon.app_certificate |
observer.id + app name + cert serial + SHA-256 fingerprint |
Validated across Windows AMD64, Linux AMD64, macOS Intel, and macOS M1 — zero missing or empty id values across all affected datasets. Four new AssetType constants added to unique_id_generator.go.
Dashboard & Field Alignment
RHEL 9 hosts missing from Cost Analysis dashboard — os_* cost fields not emitted
RHEL 9 hosts were invisible in the [TYCHON Quantum Command] Cost Analysis dashboard despite valid scan data being present in Elasticsearch.
The dashboard groups cost data by OS-specific fields (quantum_readiness.cost_analysis.os_category,
os_tier, os_total_cost_usd, etc.),
which were not being emitted for RHEL hosts at all.
Root cause: On RHEL 9.3, gopsutil reads /etc/os-release and returns
Platform = "redhat" (no space, no version suffix). The cost analysis benchmark matcher
builds a combined match string of "linux redhat 9.3". The original keyword table contained
only "rhel 9" and "red hat enterprise linux 9"
— neither matches the no-space form — so matchOsBenchmark() returned nil and all cost fields
were left at zero.
Fix: Added "redhat 9", "redhat 8",
and "redhat 7" keyword variants to osKeywords
in output_cost_analysis.go. All three RHEL major versions now have four matching variants each.
Validated via both direct Elasticsearch insert and the TYCHON Insights insert path — RHEL 9 rows appear correctly in the Cost Analysis dashboard.
Upgrade Notes
-
•
go-pkcs12 upgraded to v0.7.3 (CVE-2026-34181): PKCS#12 files using PBMAC1 (RFC 9579 modern format) with a
keyLength < 20are now rejected. This is a security fix — no configuration change is required. Files using the standard legacy HMAC-SHA1 MAC (the default for OpenSSL, Java keytool, and Windows certificate export) are completely unaffected. -
•
Port range validation is stricter: The
-portsflag now enforces the 1,000-port maximum across all comma-separated ranges combined. Previously,-ports 1-1000,1001-2000was silently accepted. Adjust any automation that passed multiple ranges summing above 1,000. Single ranges up to 1,000 ports are unaffected. -
•
Default TLS probe timeout reduced to 4 seconds: Remote scans against slow or high-latency TLS endpoints may see more
probe_timeoutresults than in prior versions. Use-tls-timeout 8to restore the previous 8-second behavior if needed. -
•
Elasticsearch index templates: The six flat NDJSON datasets now emit a top-level
idfield. Existing Elasticsearch mappings will accept this field without conflict via dynamic mapping. No template update is required. -
•
macOS and Linux browser extension scans now enumerate all user home directories: If the scanner runs as root or a privileged agent, it will now walk all profiles under
/Users/*(macOS) and/home/*(Linux). This may produce more extension records than previous versions on multi-user systems — this is correct behavior. -
•
No other breaking changes: Output field names, scan modes, all existing flags, and all output format structures are unchanged from 2.0.4.x. This release is a drop-in replacement for any 2.0.4.x deployment.