feat: fetch the chain from a live server#59
Conversation
|
Important Review skippedAuto reviews are disabled on base/target branches other than the default branch. Please check the settings in the CodeRabbit UI or the ⚙️ Run configurationConfiguration used: defaults Review profile: CHILL Plan: Pro Run ID: You can disable this status message by setting the Use the checkbox below for a quick retry:
✨ Finishing Touches🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Code Review
This pull request adds support for fetching X.509 certificate chains from live servers, including support for SMTP, IMAP, and PostgreSQL STARTTLS protocols, and updates the CLI commands to utilize this functionality. The review feedback highlights several critical edge cases in the network and protocol parsing logic, including handling multi-line SMTP responses, untagged IMAP responses, bracketed IPv6 addresses without ports, URLs containing userinfo, and improving the robustness of the file-versus-host detection logic.
Important
The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.
| func expectSMTPCode(reader *bufio.Reader, code string) error { | ||
| line, err := reader.ReadString('\n') | ||
| if err != nil { | ||
| return err | ||
| } | ||
| if !strings.HasPrefix(line, code) { | ||
| return fmt.Errorf("expected %s, got: %s", code, strings.TrimSpace(line)) | ||
| } | ||
| return nil | ||
| } |
There was a problem hiding this comment.
The current implementation of expectSMTPCode only reads a single line from the SMTP server. However, SMTP servers frequently send multi-line greetings or responses (e.g., 220-host\r\n220 ready\r\n). If a multi-line response is received, the remaining lines will be left in the buffer and incorrectly read as the response to the subsequent EHLO command, causing the handshake to fail. Update expectSMTPCode to read all lines of a multi-line response by checking if the 4th character is a hyphen (-).
func expectSMTPCode(reader *bufio.Reader, code string) error {
for {
line, err := reader.ReadString('\n')
if err != nil {
return err
}
if !strings.HasPrefix(line, code) {
return fmt.Errorf("expected %s, got: %s", code, strings.TrimSpace(line))
}
if len(line) < 4 || line[3] != '-' {
break
}
}
return nil
}| for { | ||
| line, err := reader.ReadString('\n') | ||
| if err != nil { | ||
| return err | ||
| } | ||
| if !strings.HasPrefix(line, "250") { | ||
| return fmt.Errorf("unexpected EHLO response: %s", strings.TrimSpace(line)) | ||
| } | ||
| if len(line) > 3 && line[3] == ' ' { | ||
| break | ||
| } | ||
| } |
There was a problem hiding this comment.
The EHLO response loop checks for a space at index 3 to terminate. However, according to RFC 5321, the last line of a multi-line reply can also end with just the code and a newline (without a space, e.g., 250\r\n). In such cases, line[3] would be \r or \n, and the loop would hang waiting for more input. It is safer and more robust to terminate the loop when the 4th character is not a hyphen (-).
| for { | |
| line, err := reader.ReadString('\n') | |
| if err != nil { | |
| return err | |
| } | |
| if !strings.HasPrefix(line, "250") { | |
| return fmt.Errorf("unexpected EHLO response: %s", strings.TrimSpace(line)) | |
| } | |
| if len(line) > 3 && line[3] == ' ' { | |
| break | |
| } | |
| } | |
| for { | |
| line, err := reader.ReadString('\n') | |
| if err != nil { | |
| return err | |
| } | |
| if !strings.HasPrefix(line, "250") { | |
| return fmt.Errorf("unexpected EHLO response: %s", strings.TrimSpace(line)) | |
| } | |
| if len(line) < 4 || line[3] != '-' { | |
| break | |
| } | |
| } |
| line, err = reader.ReadString('\n') | ||
| if err != nil { | ||
| return err | ||
| } | ||
| if !strings.HasPrefix(line, "a001 OK") { | ||
| return fmt.Errorf("server refused STARTTLS: %s", strings.TrimSpace(line)) | ||
| } |
There was a problem hiding this comment.
In IMAP, the server is allowed to send untagged responses (such as * CAPABILITY) before the final tagged completion response (a001 OK) to the STARTTLS command. Reading only a single line and expecting it to start with a001 OK will cause connection failures on compliant IMAP servers that send untagged data first. Update the logic to loop and consume untagged responses until the tagged response is received.
for {
line, err = reader.ReadString('\n')
if err != nil {
return err
}
if strings.HasPrefix(line, "a001 ") {
if !strings.HasPrefix(line, "a001 OK") {
return fmt.Errorf("server refused STARTTLS: %s", strings.TrimSpace(line))
}
break
}
}| host, port, splitErr := net.SplitHostPort(addr) | ||
| if splitErr != nil { | ||
| // No port, or an unbracketed IPv6 literal. Assume the former. | ||
| host, port = addr, DefaultTLSPort | ||
| } |
There was a problem hiding this comment.
If the input address is a bracketed IPv6 address without a port (e.g., [::1]), net.SplitHostPort will fail. The fallback logic sets host = "[::1]", which retains the brackets. This bracketed host is then used as the SNI ServerName, which is invalid and will cause TLS handshake failures. Strip the brackets from the host if net.SplitHostPort fails.
| host, port, splitErr := net.SplitHostPort(addr) | |
| if splitErr != nil { | |
| // No port, or an unbracketed IPv6 literal. Assume the former. | |
| host, port = addr, DefaultTLSPort | |
| } | |
| host, port, splitErr := net.SplitHostPort(addr) | |
| if splitErr != nil { | |
| // No port, or an unbracketed IPv6 literal. Assume the former. | |
| host, port = addr, DefaultTLSPort | |
| if strings.HasPrefix(host, "[") && strings.HasSuffix(host, "]") { | |
| host = host[1 : len(host)-1] | |
| } | |
| } |
| // Tolerate a pasted URL. | ||
| if i := strings.Index(addr, "://"); i >= 0 { | ||
| addr = addr[i+3:] | ||
| } | ||
| addr = strings.TrimSuffix(addr, "/") | ||
| if i := strings.IndexAny(addr, "/?#"); i >= 0 { | ||
| addr = addr[:i] | ||
| } |
There was a problem hiding this comment.
When parsing a pasted URL, if the URL contains userinfo (e.g., https://user:pass@example.com), the scheme and path are stripped but the userinfo is left in the host part. This results in an invalid host (user:pass@example.com) being used for DNS resolution and SNI. Strip any userinfo before parsing the host.
| // Tolerate a pasted URL. | |
| if i := strings.Index(addr, "://"); i >= 0 { | |
| addr = addr[i+3:] | |
| } | |
| addr = strings.TrimSuffix(addr, "/") | |
| if i := strings.IndexAny(addr, "/?#"); i >= 0 { | |
| addr = addr[:i] | |
| } | |
| // Tolerate a pasted URL. | |
| if i := strings.Index(addr, "://"); i >= 0 { | |
| addr = addr[i+3:] | |
| } | |
| addr = strings.TrimSuffix(addr, "/") | |
| if i := strings.IndexAny(addr, "/?#"); i >= 0 { | |
| addr = addr[:i] | |
| } | |
| if i := strings.LastIndex(addr, "@"); i >= 0 { | |
| addr = addr[i+1:] | |
| } |
| if _, err := os.Stat(target); err == nil { | ||
| return false | ||
| } | ||
| if strings.Contains(target, "://") { | ||
| return true | ||
| } | ||
| return strings.ContainsAny(target, ".:") |
There was a problem hiding this comment.
There are two issues in looksLikeHost:
- If
os.Statfails with an error other than "not exist" (e.g., permission denied), the file actually exists but we treat it as a host if it contains.or:. We should explicitly checkos.IsNotExist(err). - If a user mistypes a relative or absolute file path (e.g.,
./certs.pemor/path/to/cert.pem),os.Statwill fail with a "not exist" error, and because the path contains.or/, it will be incorrectly treated as a host. This leads to confusing DNS lookup errors instead of a simple "file not found" error. We should avoid treating paths starting with/,./,../, or containing path separators as hosts.
if _, err := os.Stat(target); err == nil {
return false
} else if !os.IsNotExist(err) {
return false
}
if strings.Contains(target, "://") {
return true
}
if strings.HasPrefix(target, "/") || strings.HasPrefix(target, "./") || strings.HasPrefix(target, "../") || strings.ContainsAny(target, "/\\") {
return false
}
return strings.ContainsAny(target, ".:")|
Thanks — this was a genuinely good review. All six findings were real, and the two SMTP ones were worse than "would fail": the old code hung. SMTP multi-line replies. Reading one line left the rest in the buffer, where it got read back as the answer to the next command. Postfix sends a multi-line greeting by default, so the exchange desynced before it began. And the EHLO loop tested column four for a space — a bare IMAP untagged responses. A compliant server sending Address parsing. Userinfo survived URL stripping, so
All six are pinned by tests that fail against the old code — note the SMTP ones time out rather than assert: Re-verified live afterwards: IMAP is covered by the fake-server tests rather than live — port 143 is blocked from here, so the failure is at TCP, before any protocol runs.
/gemini review |
ec1abb5 to
9073a35
Compare
There was a problem hiding this comment.
Code Review
This pull request introduces the ability to fetch and validate X.509 certificate chains directly from live servers, including support for STARTTLS protocols (SMTP, IMAP, and PostgreSQL). It adds new CLI flags, updates the terminal UI and validation commands to handle network inputs, and implements the connection and protocol negotiation logic. The review feedback suggests improving the host-detection logic to support localhost, using io.ReadFull for robust socket reads, and resolving a temporary goroutine leak in the timeout test.
Important
The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.
| func looksLikeHost(target string) bool { | ||
| if target == "" { | ||
| return false | ||
| } |
There was a problem hiding this comment.
The looksLikeHost function currently treats any bare word without a dot or colon as a file. While this is a good default to avoid confusing DNS errors for mistyped filenames, it means that localhost (a very common hostname for local development) is treated as a file and fails to connect. Adding a special case for localhost improves the developer experience.
| func looksLikeHost(target string) bool { | |
| if target == "" { | |
| return false | |
| } | |
| func looksLikeHost(target string) bool { | |
| if target == "" { | |
| return false | |
| } | |
| if target == "localhost" { | |
| return true | |
| } |
| response := make([]byte, 1) | ||
| if _, err := conn.Read(response); err != nil { | ||
| return err | ||
| } |
There was a problem hiding this comment.
Using conn.Read directly with a 1-byte buffer is generally fine, but using io.ReadFull is more robust and idiomatic in Go to guarantee that the expected number of bytes is fully read before proceeding. Note that you will need to import the io package in this file.
| response := make([]byte, 1) | |
| if _, err := conn.Read(response); err != nil { | |
| return err | |
| } | |
| response := make([]byte, 1) | |
| if _, err := io.ReadFull(conn, response); err != nil { | |
| return err | |
| } |
| go func() { | ||
| conn, err := listener.Accept() | ||
| if err != nil { | ||
| return | ||
| } | ||
| <-time.After(30 * time.Second) | ||
| _ = conn.Close() | ||
| }() |
There was a problem hiding this comment.
The background goroutine in TestFetchChain_Timeout sleeps for 30 seconds using time.After. If the test completes quickly (which it should due to the 200ms timeout), this goroutine remains active in the background for the remainder of the 30 seconds, causing a temporary goroutine leak during test execution. Instead of sleeping, the goroutine can simply block on reading from the connection until it is closed by the client.
| go func() { | |
| conn, err := listener.Accept() | |
| if err != nil { | |
| return | |
| } | |
| <-time.After(30 * time.Second) | |
| _ = conn.Close() | |
| }() | |
| go func() { | |
| conn, err := listener.Accept() | |
| if err != nil { | |
| return | |
| } | |
| defer conn.Close() | |
| buf := make([]byte, 1) | |
| _, _ = conn.Read(buf) | |
| }() |
y509 could only read certificates from a file or stdin. The README
documented the workaround itself -- pipe openssl s_client into it -- which
is a fair sign the feature was missing rather than unwanted. certigo,
step and tlsx all connect; y509 did not.
y509 example.com:443
y509 --connect 10.0.0.1:8443 --servername api.internal
y509 db.example.com:5432 --starttls postgres
y509 validate example.com:443
The handshake sets InsecureSkipVerify. That is deliberate, and it is the
crux of the design: a chain the system does not trust is exactly what the
user came to look at, so rejecting it at the transport would defeat the
purpose. VerifyChain still passes judgement afterwards, and validate exits
non-zero when it should.
Certificates are kept in the order the server sent them rather than being
sorted on arrival. That order is not necessarily a valid chain, and the
ways it can be wrong -- a root the server should not have shipped, a
missing intermediate -- are the bugs worth seeing.
Connecting also means the hostname becomes a real question, so validate
now checks the leaf against the host it dialled unless --host says
otherwise. openssl's own docs concede that -showcerts "is not a verified
chain"; this is.
STARTTLS covers smtp, imap and postgres. postgres in particular is
miserable to do by hand with openssl. An unknown protocol is rejected
before dialling, so a typo does not surface as a connection error.
The connect flags are persistent, so validate and export can use them too.
…esses
The STARTTLS preludes only worked against the simplest servers.
SMTP replies may span several lines: RFC 5321 marks a continuation with a
hyphen in column four and the last line with anything else. Reading only
the first line left the rest in the buffer, where it was read back as the
answer to the next command -- so a multi-line greeting, which Postfix sends
by default, desynced the exchange before it began. The EHLO loop also broke
on a bare "250\r\n" final line, which is legal: it tested column four for a
space and looped forever waiting for one. Both now read a whole reply.
IMAP servers may send untagged responses ("* CAPABILITY ...") before the
tagged completion of STARTTLS. Reading a single line and calling anything
that was not "a001 OK" a refusal failed against compliant servers. Skip to
the tagged line.
Address parsing dropped the scheme and path of a pasted URL but left any
userinfo attached, so "user:pass@example.com" went out as both the DNS name
and the SNI value. A bracketed IPv6 literal with no port kept its brackets,
which JoinHostPort then doubled.
looksLikeHost sent a mistyped path to the network: "./chain.pem" contains a
dot, so a typo in it was answered with a failed DNS lookup rather than
"no such file". Anything shaped like a path is now a path, and a stat error
that is not "not exist" means the file is there and the user meant it.
5bda79b to
8ba0f66
Compare
9073a35 to
0635bf7
Compare
localhost is the obvious target for local development, but it carries neither a dot nor a colon, so looksLikeHost took it for a filename and never connected. Special-case it. Read the Postgres SSLRequest reply with io.ReadFull rather than a bare conn.Read: the single byte is guaranteed, not merely attempted. Stop the timeout test from leaking a goroutine that outlived it by 30s -- block on a read until the client closes instead of sleeping.
|
Thanks, all three applied.
/gemini review |
There was a problem hiding this comment.
Code Review
This pull request adds support for fetching X.509 certificate chains from live servers (including SNI and STARTTLS for SMTP, IMAP, and PostgreSQL) in both the TUI and validation commands. The feedback highlights several improvement opportunities: ensuring STARTTLS negotiation respects context cancellation to prevent blocking, refining the host-vs-file detection logic to treat non-existent files with common certificate extensions as files rather than hosts, updating the corresponding tests, and handling edge cases in address normalization such as trailing colons with empty ports.
Important
The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.
| defer func() { | ||
| if closeErr := conn.Close(); closeErr != nil { | ||
| logger.Warn("failed to close connection", zap.Error(closeErr)) | ||
| } | ||
| }() |
There was a problem hiding this comment.
While tlsConn.HandshakeContext(ctx) respects context cancellation, the synchronous STARTTLS negotiation phase (negotiateStartTLS) does not. If the context is cancelled (e.g., via user interruption or a parent context timeout) during this phase, the connection could block until the TCP timeout or the connection deadline is reached.
To make the STARTTLS negotiation responsive to context cancellation, we can run a background goroutine that closes the connection immediately when the context is done.
defer func() {
if closeErr := conn.Close(); closeErr != nil {
logger.Warn("failed to close connection", zap.Error(closeErr))
}
}()
done := make(chan struct{})
defer close(done)
go func() {
select {
case <-ctx.Done():
_ = conn.Close()
case <-done:
}
}()| if _, err := os.Stat(target); !os.IsNotExist(err) { | ||
| return false | ||
| } |
There was a problem hiding this comment.
If a user specifies a non-existent file with a common certificate extension (e.g., y509 missing-file.pem), looksLikeHost will return true because the file does not exist and contains a dot. This causes the tool to attempt a network connection and fail with a confusing DNS lookup error (e.g., dial tcp: lookup missing-file.pem: no such host) instead of a clear file-not-found error.
We can improve this by explicitly treating targets with common certificate extensions (like .pem, .crt, .der, .cer) as files even if they do not currently exist.
| if _, err := os.Stat(target); !os.IsNotExist(err) { | |
| return false | |
| } | |
| if _, err := os.Stat(target); !os.IsNotExist(err) { | |
| return false | |
| } | |
| lower := strings.ToLower(target) | |
| for _, ext := range []string{".pem", ".crt", ".der", ".cer"} { | |
| if strings.HasSuffix(lower, ext) { | |
| return false | |
| } | |
| } |
| {"certs", false}, // a bare word is likelier a mistyped file | ||
| {"./chain.pem", false}, // path-shaped, even though it has a dot | ||
| {"/etc/ssl/cert.pem", false}, | ||
| {"chain.pem", true}, // a dot with no separator still reads as host-ish; opens as file only if it exists |
There was a problem hiding this comment.
Update the test case to reflect the improved behavior where files ending with .pem are treated as files even if they do not exist.
| {"chain.pem", true}, // a dot with no separator still reads as host-ish; opens as file only if it exists | |
| {"chain.pem", false}, // ends with .pem, so treated as file even if missing |
| host, port, splitErr := net.SplitHostPort(addr) | ||
| if splitErr != nil { | ||
| // No port, or an unbracketed IPv6 literal. Assume the former. | ||
| host, port = addr, DefaultTLSPort | ||
| // A bracketed literal with no port ("[::1]") lands here with its | ||
| // brackets still on. They belong to the address syntax, not the host, | ||
| // and JoinHostPort would add a second pair. | ||
| if strings.HasPrefix(host, "[") && strings.HasSuffix(host, "]") { | ||
| host = host[1 : len(host)-1] | ||
| } | ||
| } |
There was a problem hiding this comment.
If the user inputs an address with a trailing colon but no port (e.g., example.com:), net.SplitHostPort will succeed but return an empty port. This can lead to connection failures or unexpected behavior. We should default the port to DefaultTLSPort if it is empty.
| host, port, splitErr := net.SplitHostPort(addr) | |
| if splitErr != nil { | |
| // No port, or an unbracketed IPv6 literal. Assume the former. | |
| host, port = addr, DefaultTLSPort | |
| // A bracketed literal with no port ("[::1]") lands here with its | |
| // brackets still on. They belong to the address syntax, not the host, | |
| // and JoinHostPort would add a second pair. | |
| if strings.HasPrefix(host, "[") && strings.HasSuffix(host, "]") { | |
| host = host[1 : len(host)-1] | |
| } | |
| } | |
| host, port, splitErr := net.SplitHostPort(addr) | |
| if splitErr != nil { | |
| // No port, or an unbracketed IPv6 literal. Assume the former. | |
| host, port = addr, DefaultTLSPort | |
| // A bracketed literal with no port ("[::1]") lands here with its | |
| // brackets still on. They belong to the address syntax, not the host, | |
| // and JoinHostPort would add a second pair. | |
| if strings.HasPrefix(host, "[") && strings.HasSuffix(host, "]") { | |
| host = host[1 : len(host)-1] | |
| } | |
| } | |
| if port == "" { | |
| port = DefaultTLSPort | |
| } |
…ur ctx during STARTTLS
Three edges in the live-connect path:
A missing file with a certificate extension went to the network:
"y509 chian.pem" (a typo) contains a dot and no separator, so looksLikeHost
took it for a host and answered with a DNS failure instead of "no such
file". Any .pem/.crt/.cer/.der/.p7b/.p7c/.pfx/.p12 suffix now means a file.
A trailing colon ("example.com:") splits cleanly in net.SplitHostPort but
leaves an empty port, which dials an invalid address. Fall back to 443.
The STARTTLS negotiation reads synchronously and did not watch the context.
The deadline bounded it, but an early cancellation waited the deadline out.
Close the connection when the context is done so those reads unblock at
once.
|
Thanks — took three of the four, and they were all worth doing. Cert-extension paths ( Empty port ( Context during STARTTLS ( The
/gemini review |
There was a problem hiding this comment.
Code Review
This pull request adds support for fetching X.509 certificate chains from live servers, including STARTTLS negotiation for SMTP, IMAP, and PostgreSQL, and integrates this functionality into the TUI and validation commands. The review feedback suggests avoiding warning logs when closing connections on context cancellation, and returning an error if both --connect and a positional argument are specified to prevent user confusion.
Important
The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.
| defer func() { | ||
| if closeErr := conn.Close(); closeErr != nil { | ||
| logger.Warn("failed to close connection", zap.Error(closeErr)) | ||
| } | ||
| }() |
There was a problem hiding this comment.
When the connection is closed due to context cancellation or timeout, conn.Close() will return an error (e.g., use of closed network connection). Logging this as a warning can clutter the logs with expected errors. We can avoid logging this warning if the context has already been canceled or timed out by checking ctx.Err() == nil.
| defer func() { | |
| if closeErr := conn.Close(); closeErr != nil { | |
| logger.Warn("failed to close connection", zap.Error(closeErr)) | |
| } | |
| }() | |
| defer func() { | |
| if closeErr := conn.Close(); closeErr != nil && ctx.Err() == nil { | |
| logger.Warn("failed to close connection", zap.Error(closeErr)) | |
| } | |
| }() |
| func loadInput(cmd *cobra.Command, args []string) (*input, error) { | ||
| target, err := cmd.Flags().GetString("connect") | ||
| if err != nil { | ||
| return nil, err | ||
| } | ||
| explicitConnect := target != "" | ||
|
|
||
| if target == "" && len(args) > 0 { | ||
| target = args[0] | ||
| } |
There was a problem hiding this comment.
If both --connect and a positional argument are provided, the positional argument is silently ignored. It would be safer and clearer to return an error when both are specified to prevent user confusion.
func loadInput(cmd *cobra.Command, args []string) (*input, error) {
target, err := cmd.Flags().GetString("connect")
if err != nil {
return nil, err
}
explicitConnect := target != ""
if explicitConnect && len(args) > 0 {
return nil, fmt.Errorf("cannot specify both --connect and a positional argument")
}
if target == "" && len(args) > 0 {
target = args[0]
}
Why
y509 could only read from a file or stdin. The README documented the workaround itself:
openssl s_client -connect example.com:443 -showcerts | y509That is a fair sign the feature was missing rather than unwanted.
certigo connect,step certificate inspect https://…andtlsxall do this; y509 did not. It is the single biggest capability gap.What
New flags (persistent, so
validateandexportget them too):--connect,--servername,--starttls,--timeout.Two design decisions worth reviewing
1. The handshake sets
InsecureSkipVerify. This is deliberate and it is the crux of the thing: a chain the system does not trust is exactly what the user came to look at. Rejecting it at the transport would defeat the entire purpose.VerifyChain(#54) still passes judgement afterwards, andvalidatestill exits non-zero when it should.2. Certificates are kept in the order the server sent them, not sorted on arrival. That order is not necessarily a valid chain — and the ways it can be wrong (a root the server should not have shipped, a missing intermediate) are the bugs worth seeing. openssl's own docs concede that
-showcerts"displays the server certificate list as sent by the server … it is not a verified chain". This is. Preserving the sent order is also the input a future "as-sent vs verified" view needs.3. Connecting makes the hostname a real question, so
validatenow checks the leaf against the host it dialled unless--hostoverrides. That is what a TLS client does.Verified against real servers
badssl.com exists for exactly this, so I used it:
STARTTLS against a real SMTP server:
A pasted URL works (
y509 validate https://github.com/kanywst/y509), and a file that happens to be namedexample.com:443still opens as a file.Tests
Address normalisation (bare host, port, URL with a path, IPv4, bracketed IPv6, empty), a real in-process TLS server asserting the sent order is preserved, an untrusted server whose chain still comes back, timeout, connection refused, and the Postgres
SSLRequestexchange driven against a fake server (accept / refuse / garbage).make lintclean,markdownlintclean, full suite green.