TLS inspection without the archaeology.
openssl s_client hands you a wall of PEM and a feeling. tlsinfo hands you a verdict:
one static binary, one connection, every TLS fact on the wire.
No config files. No daemon. No dependency tree.
TL;DR
go install paepcke.de/tlsinfo/cmd/tlsinfo@latest tlsinfo github.comInstall. Run. Read the verdict. Nothing else to configure.
- Why tlsinfo
- See it in action
- When TLS is broken
- Install
- Usage
- Library
- How it works
- Design promises
- Documentation
- Contributing
- License
- Citation
TLS is invisible until it isn't. Then you are three terminals deep in openssl s_client,
openssl x509, a calendar, a pinning spreadsheet and a browser devtools panel, trying to
answer one question: is this connection what I think it is?
tlsinfo answers it once, in one report, for the CLI and for your Go code, from the same
engine:
- π§ Handshake truth, no interpretation - TLS version, cipher suite, ALPN, resumption,
SCT timestamps and the TLS unique ID, with instant
[OK]/[LEGACY FALLBACK]/[SECURITY ALERT]verdicts. - π Every certificate, parsed - subject, validity window, signature algorithm, public key, SANs, chain layout and the SPKI key pin, without a single OpenSSL invocation.
- π‘οΈ OCSP that means something - the stapled response is verified against the issuer
and bound to the presented leaf. A valid staple for a different certificate is rejected.
Stale staples get
[WARNING], revoked certificates get[SECURITY ALERT]. - π Pins built for real use - SPKI base64 pins for trust-on-first-use, even on untrusted or self-signed chains, with verification helpers ready for production.
- π STARTTLS included - SMTP, IMAP, POP3, NNTP and FTP upgrades, so the mail and news servers nobody remembers to check are one flag away.
- π¨ Output for humans and machines - ANSI color, plain ASCII, HTML (injection-safe),
or fully custom styles via
reportstyle, plus raw PEM/OpenSSL detail on demand. - π§ͺ Zero surprise dependencies - pure Go, CGO off. Direct imports: the standard library
plus
certinfo,reportstyleandx/crypto. Nothing heavier sneaks in.
| Capability | tlsinfo CLI |
Go library |
|---|---|---|
| Full handshake, chain and verdict report | β | β |
| Base64 SPKI key pins | β | β |
| Pin verification (trust-on-first-use) | - | β |
| Leaf-bound OCSP status | β when stapled | β
ReportOcsp |
| STARTTLS: SMTP, IMAP, POP3, NNTP, FTP | β | β |
| Plain text, ANSI, HTML, custom styles | β | β |
| Reports for your own HTTPS clients | - | β
ReportHttpRequest |
Real output from a real connection, colors included:
Handshake, chain, key pin and certificate verdicts in a single pass. Pass several hosts, or pipe a list and let it run:
tlsinfo github.com paepcke.de
echo github.com | tlsinfo
cat hosts.txt | tlsinfo# just the base64 SPKI pin, nothing else
tlsinfo -pin github.com
# the mail server behind port 587
tlsinfo -starttls smtp smtp.gmail.com:587
# the news server nobody has checked since 2009
tlsinfo -starttls nntp news.eternal-september.org
# HTML for a dashboard, injection-safe
tlsinfo -html github.com | your-renderer
# raw PEM and OpenSSL-grade certificate detail
tlsinfo -verbose github.comHosts are normalized for you: the default port is appended, bare IPv6 literals are bracketed, and empty input is rejected instead of quietly probing localhost.
Expired chains, untrusted issuers, legacy fallbacks, stale staples and revoked leaves are not buried in a wall of PEM. They are verdicts, in color, on the line that matters:
Exit codes are just as blunt:
| Code | Meaning |
|---|---|
0 |
every host check succeeded |
1 |
a host check failed (unreachable, failed handshake, top-level [error]) |
2 |
usage error (bad flags, flags after hosts, no hosts at all) |
So tlsinfo slots straight into a deploy gate:
tlsinfo github.com || exit 1# run it right now
go run paepcke.de/tlsinfo/cmd/tlsinfo@latest github.com
# install it
go install paepcke.de/tlsinfo/cmd/tlsinfo@latestOther install options
Prebuilt binaries for Linux, macOS, Windows and the BSDs (amd64/arm64):
github.com/paepckehh/tlsinfo/releases.
.deb and .rpm packages are published with every release.
NixOS / Nix:
nix-shell -p tlsinfoenvironment.systemPackages = [ pkgs.tlsinfo ];Build from source:
git clone https://github.com/paepckehh/tlsinfo
cd tlsinfo
make build
./tlsinfo github.comPOSIX conventions, stdlib flag, tlsinfo -h prints usage with the version.
Every flag has an environment variable fallback for existing scripts
(empty value = off, see no-color.org):
| Flag | Environment variable | Effect |
|---|---|---|
-starttls p |
STARTTLS |
negotiate STARTTLS first: smtp, imap, pop3, nntp, ftp |
-pin |
PINONLY |
print only the base64 SPKI key pin(s) |
-verbose |
VERBOSE |
add raw / PEM / OpenSSL certificate details |
-html |
HTML |
HTML output, escaped and ready for a page |
-no-color |
NO_COLOR |
plain ASCII output, no ANSI colors |
-version |
- | print the build-time version and exit |
Flags go before the hosts (tlsinfo -pin github.com, not tlsinfo github.com -pin).
Piped hosts win when the stream carries any; otherwise the command line is used.
Every run announces its build on stderr (tlsinfo v0.1.69), and -version prints just the
semver on stdout. Release binaries embed the git tag at build time; go install ...@latest
builds fall back to the module version recorded by the Go toolchain, so the startup line
stays a real semver instead of an empty placeholder.
Analyze mail and news servers that only speak TLS after an in-band upgrade:
tlsinfo -starttls smtp smtp.gmail.com:587
tlsinfo -starttls imap mail.example.com
tlsinfo -starttls pop3 mail.example.com
tlsinfo -starttls nntp news.eternal-september.org
tlsinfo -starttls ftp ftp.gnu.orgThe protocol name is case- and whitespace-insensitive (SMTP, smtp both work), and the
default port is appended automatically.
# fetch the base64 SPKI pin of a host
tlsinfo -pin github.com
# works via STARTTLS too
tlsinfo -pin -starttls smtp smtp.gmail.com:587Pin verification lives in the library (PinVerifyHost, PinVerifyStartTLS), so a pin
becomes a one-line check in your deploy pipeline.
The CLI is a thin wrapper around a pure Go engine. Import it and get the same report without shelling out.
package main
import (
"os"
"paepcke.de/tlsinfo"
)
func main() {
os.Stdout.WriteString(tlsinfo.ReportHostText("github.com"))
}Report the clients that hit your own HTTPS endpoint:
// r is the *http.Request inside your handler
report := tlsinfo.ReportHttpRequestHTML(r)Verify a trust-on-first-use pin:
ok := tlsinfo.PinVerifyHost("/wiL5vgOLgwED41WS0DNF8QiTBVR/P41Kd163tmFxK0=", "github.com")Check a mail server through STARTTLS:
report := tlsinfo.ReportStartTLSText("smtp.gmail.com:587", tlsinfo.SMTP)
pin := tlsinfo.PinStartTLS("smtp.gmail.com:587", tlsinfo.SMTP, &tls.Config{})| Function | Input |
|---|---|
ReportHostText / ReportHostHTML / ReportHost |
hostname (+ tls.Config, *reportstyle.Style) |
ReportConnText / ReportConnHTML / ReportConn |
established *tls.Conn |
ReportTlsStateText / ReportTlsStateHTML / ReportTlsState |
*tls.ConnectionState |
ReportHttpRequestText / ReportHttpRequestHTML / ReportHttpRequest |
server-side *http.Request |
ReportOcsp |
*tls.Conn + issuer *x509.Certificate (bound to the leaf, stale responses flagged) |
ReportStartTLS / ReportStartTLSText / ReportStartTLSHTML |
hostname + smtp/imap/pop3/nntp/ftp |
PinHost, PinHostWithTLS |
hostname (+ tls.Config) |
PinStartTLS |
hostname + protocol (+ tls.Config) |
PinVerifyStartTLS |
key pin + hostname + protocol (+ tls.Config) |
PinVerifyHost, PinVerifyHostWithTLS |
key pin + hostname |
PinVerifyConn, PinVerifyState |
key pin + *tls.Conn / *tls.ConnectionState |
Output styles come from paepcke.de/reportstyle:
plain text, ANSI console, HTML, or fully custom. Dynamic values (server name, ALPN, error
text, ...) pass through the style sanitizer, so HTML reports are escaped against injection
and wrapped in the style's Start/End markers.
Library guarantees worth knowing:
- your
*tls.Configis cloned, never mutated, even though inspection forcesInsecureSkipVerify; nilstyles, connections and requests are tolerated (plain-text report, tagged error orfalse, never a panic);- pin verification is false when the lookup failed, so an error message can never be mistaken for a matching pin;
- the library never writes to stdout. Output is always the returned string.
- The host is normalized (
:443appended, bare IPv6 bracketed, empty hosts are rejected instead of probing localhost). - A TLS connection is opened, or a plaintext one upgraded through the protocol table.
tls.ConnectionStateis walked and certificates are rendered bycertinfo.- The result is assembled with the requested style and sanitized on the way out.
There is no daemon, no cache, no config and no background work. Each invocation is one connection and one report.
- Boring dependency tree. Standard library plus three focused direct packages
(
certinfo,reportstyle,x/crypto), and only three transitive modules behind them (certificate-transparency-go,go-logr/logr,k8s.io/klog/v2). No framework, no CLI toolkit, no YAML loader. - Static binary.
CGO_ENABLED=0everywhere, including releases. - Offline tests. The suite spins up loopback TLS and STARTTLS servers;
make testnever touches the network. - Readable failures. Errors are embedded in the returned string as
[tlsinfo] [error] .../[starttls] [error] ..., so CLI exit codes and logs stay simple. - No surprises in HTML. Every injected value is escaped through the style sanitizer.
- pkg.go.dev/paepcke.de/tlsinfo
- Machine-readable repo overview for LLMs: llms.txt
- Agent/contributor notes: AGENTS.md
- Theme-aware README banners (hand-built SVG,
prefers-color-scheme): dark, light - Real terminal captures used above: clean report, expired chain
- Found a security issue? See SECURITY.md
Yes, please! PRs, issues and reproductions are welcome. Start with
CONTRIBUTING.md, run make test, and keep the zero-dependency
philosophy intact.
BSD 3-Clause. See LICENSE.
@misc{tlsinfo,
author = {Michael Paepcke},
title = {Tool to analyze and troubleshoot TLS connections (app/lib/api)},
year = {2022},
publisher = {GitHub},
journal = {GitHub repository},
howpublished = {\url{https://paepcke.de/tlsinfo}}
}