[libtacky / TLS] “Ignore for 7 Days” silently accepts a substituted certificate and allows SCRAM-PLUS authentication to begin #15
Loading…
Reference in a new issue
No description provided.
Delete branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
[libtacky / TLS] “Ignore for 7 Days” silently accepts a substituted certificate and allows SCRAM-PLUS authentication to begin
Severity: Major
Issue ID: TLS-05, TLS-05p · QA case: SEC-102 (Under clarification)
Date: November 3, 2026
Summary
After a user selects “Ignore for 7 Days” for an untrusted server certificate, libtacky accepts any certificate presented by that host during the exception period, rather than only the certificate the user originally approved.
An attacker capable of intercepting the connection (e.g. on a public Wi-Fi network or through a compromised network) can therefore substitute the certificate without triggering another certificate warning for up to seven days.
Testing confirmed that:
the substituted certificate is accepted without a new CertificateUntrusted event;
the client proceeds to SCRAM-PLUS authentication;
the authentication request exposes the username to the attacker-controlled server.
Potential additional impact: If the attacker continues the SCRAM exchange instead of immediately rejecting it, the protocol provides material that can be used for offline password guessing. This was derived from the SCRAM protocol and source code, but was not experimentally demonstrated in the test environment.
Authentication cannot be transparently relayed to the legitimate server because SCRAM-PLUS is bound to the attacker's TLS channel. No message history or IP address disclosure was demonstrated.
Affected components:
libtacky: tacky/xmpp/modules/certtrust.tcl, account.tcl, session/sasl.tcl
iOS application: CertificatePromptView / CertificateViews.swift:106, AppEnvironment.swift:1456-1465
Environment
Item Details
Application anotherim-swift
c5fdeae, branch feat/ui-redesign, build 93libtacky 2f42a2f2 / v1.2.0-beta2
Test server checks/tls_server.py, a simulated attacker-controlled STARTTLS server
Client libtacky application profile (checks/tacky/test_tls_agent.tcl)
TLS configuration -allow-plain not set, matching the application
Original certificate selfA, self-signed for example.local
Substituted certificate rogue, signed by an unrelated CA (Charles Proxy CA (rig))
Verification Level 1 cloud-based test environment
Preconditions
Account t05@example.local connects to the simulated server.
The server presents the self-signed certificate selfA.
libtacky rejects it with CertificateUntrusted reason=self_signed.
The user selects “Ignore for 7 Days”, invoking:
account ignore_certificate_errors -host example.local
Steps to Reproduce
Run:
bash setup/run_all.sh tls
This covers TLS-05a, TLS-05, and TLS-05p.
TLS-05a: After the exception is stored, reconnect using the original certificate selfA.
TLS-05: Replace selfA with the different certificate rogue and reconnect.
TLS-05p: Have the attacker-controlled server offer:
SCRAM-SHA-256-PLUS
SCRAM-SHA-1-PLUS
SCRAM-SHA-256
PLAIN
Inspect the client events and the simulated server's JSONL log.
Expected Behavior
The exception should apply only to the certificate the user explicitly approved.
A different certificate presented by the same host should trigger a new certificate warning (changed=1), as in the certificate-pinning behavior covered by TLS-04 PASS.
No SASL authentication mechanism should be initiated until the user explicitly approves the newly presented certificate.
This is a security recommendation, not an explicitly defined product requirement. SEC-102 is currently under clarification.
Actual Behavior
Run e43b9b7a (evidence/events_t05_e43b9b7a.txt and evidence/fake_server_TLS-05_e43b9b7a.jsonl) demonstrated:
TLS-05a — Original certificate
The exception was stored for seven days (done 33 … => 1794337326).
TLS was established with selfA.
SASL was not started because the server offered only non-channel-bound SCRAM mechanisms. The client returned:
AuthError: the server's certificate is accepted only by an exception;
only a channel-bound mechanism is used on it, and the server offers
SCRAM-SHA-256, SCRAM-SHA-1
TLS-05 — Substituted certificate
The server presented rogue, a different certificate signed by an unrelated CA.
TLS 1.3 was established (tls_ok).
No CertificateUntrusted event was emitted.
The certificate substitution was therefore not reported to the user.
TLS-05p — SCRAM-PLUS offered
The attacker-controlled server offered SCRAM-PLUS. The client sent:
cD10bHMtZXhwb3J0ZXIsLG49dDA1LHI9… Decoded client-first message:p=tls-exporter,,n=t05,r=
This confirms that the username t05 was disclosed and SCRAM authentication began.
The simulated server then returned , resulting in AuthError … not-authorized. It did not continue the SCRAM exchange.
Reproduction Rate
2/2 successful runs:
e43b9b7a
b73e3f2c
Both runs confirmed the certificate-substitution and SCRAM-PLUS behavior.
Root Cause
The source code confirms that the exception is host-based rather than certificate-specific:
tacky/xmpp/modules/account.tcl:202-205 documents ignore_certificate_errors as accepting any certificate on that host that fails verification for CERT_IGNORE_SECONDS (seven days).
tacky/xmpp/modules/certtrust.tcl:64-75 (ignored) stores the hostname and expiration time, but not the fingerprint of the approved certificate. Certificate changes are flagged again only after the exception expires.
tacky/xmpp/session/sasl.tcl:95, 151-156 allows only channel-bound authentication mechanisms under this exception. When the server offers SCRAM-PLUS, authentication proceeds.
The iOS application exposes the same behavior:
AppEnvironment.swift:1456-1465
UntrustedCertificate.swift:8-9
The UI describes the action as accepting any failing certificate for that host for one week.
Potential offline password guessing
SCRAM-PLUS binds authentication to the TLS channel using tls-exporter. This prevents the attacker from simply forwarding the authentication exchange to the legitimate server.
However, the attacker terminates TLS themselves and therefore knows the channel binding value. If they continue the SCRAM exchange using their own server-first message (including their chosen salt and iteration count), they can obtain the client's client-final message containing the ClientProof.
This provides material for offline password guessing under the SCRAM protocol (RFC 5802 §9).
Additionally, tacky/xmpp/session/scram.tcl:27-30 accepts iteration counts from 1 to 1,000,000, meaning an attacker-controlled server can request a very low iteration count.
Important: This is a protocol-level assessment, not a demonstrated exploit. The test server immediately returned and did not collect a ClientProof.
Suggested Fix
Consider one of the following approaches:
Bind the exception to a certificate fingerprint. Store the SHA-256 fingerprint of the certificate approved by the user. If a different certificate appears during the exception period, emit CertificateUntrusted with changed=1 and require a new user decision.
Remove “Ignore for 7 Days”. Keep only explicit certificate pinning (“Trust this certificate”) and “Cancel”.
In either case, do not initiate SASL authentication until the currently presented certificate has been explicitly approved.
Verification After Fix
Using the same test setup:
Present selfA and select “Ignore for 7 Days”.
Replace it with rogue.
Reconnect.
Verify that the client reports a certificate change and requires user confirmation.
Verify that no SCRAM-PLUS is sent before approval.
For physical-device testing, use an iPhone 13 (build 93) and a controlled MITM setup. Direct TLS (XEP-0368) on port 5223 may be easier to intercept; ordinary MITM proxies do not necessarily support XMPP STARTTLS on port 5222.
Evidence and Status
Evidence:
evidence/events_t05_.txt — account events.
evidence/fake_server_TLS-05_.jsonl — what the simulated server observed.
evidence/run_tls__TLS-05.txt
evidence/tls_.json
evidence/account.tcl_188-212.txt
evidence/certtrust.tcl_64-75.txt
evidence/sasl.tcl_90-96_148-157.txt
evidence/UntrustedCertificate.swift_1-14.txt
evidence/AppEnvironment.swift_1456-1466.txt
Test report:
results/2026-11-03_tls.md, sections 2–3.
Complete evidence archive:
_claude_tmp/evidence_tls_2026-11-03.tgz
Verified:
Certificate exception behavior in two runs.
Silent acceptance of the substituted certificate.
SCRAM-PLUS initiation and username disclosure.
Relevant source code in libtacky 2f42a2f2 and anotherim-swift
c5fdeae.Not verified:
Actual offline password guessing / ClientProof collection.
Behavior on a physical iPhone.
Behavior against the production another.im server.
HTTPS attachment behavior under the exception. http.tcl:73-79 indicates that credentials are not sent over HTTP under this exception.
Moved to another-im/libtacky#16: the fix belongs to libtacky. The evidence archive went with it.