tlscensus
Passive TLS cryptography inventory for Linux, macOS and Windows.
Answers one question that is getting harder to avoid: which of my machines
are still negotiating classical-only key exchange, and with which
destinations?
tlscensus watches TLS handshakes go by, decodes the ClientHello and
ServerHello, and reports what cryptography is actually in use — protocol
versions, cipher suites, key exchange groups, signature algorithms, ALPN,
SNI, and JA4 fingerprints — with a post-quantum readiness verdict for every
handshake. It never terminates, proxies, decrypts or modifies a connection.
There is no CA to install and no traffic interception.
$ tlscensus read capture.pcap
TLS handshakes: 9 (8 with a captured server response)
PQ readiness: 37.5% of observed negotiations used a post-quantum group
POST-QUANTUM STATUS
classical 3 33.3%
post_quantum 3 33.3%
advertised_only 1 11.1%
offered_not_selected 1 11.1%
FINDINGS
CRITICAL 1 broken_cipher TLS_RSA_WITH_RC4_128_SHA uses broken primitive RC4_128
CRITICAL 1 obsolete_protocol TLS 1.0 is deprecated and prohibited by RFC 8996
HIGH 2 no_forward_secrecy TLS_RSA_WITH_AES_128_CBC_SHA has no forward secrecy
Status
v0.1.0. Reads capture files on any platform, captures live on Linux,
macOS and Windows, and decodes QUIC as well as TCP. Every platform has been
run against real hardware, not only compiled for.
There is no roadmap of pending features: what remains is listed in
docs/validation.md as things that have not been
verified rather than things that are coming. The most useful thing anyone can
send is a report of it getting something wrong on a network we do not have —
see CONTRIBUTING.md.
Install
Reading capture files needs no privileges on any platform. Live capture
needs one extra step everywhere — see
docs/permissions.md.
macOS and Linux — Homebrew
brew install tlscensus/tap/tlscensus
Linux — package
sudo dpkg -i tlscensus_*_linux_amd64.deb # or: sudo rpm -i tlscensus-*.rpm
sudo setcap cap_net_raw=eip $(command -v tlscensus)
The package prints that setcap line on install. It does not run it:
granting a binary raw-socket capability is the administrator's decision, and
it has to be reapplied after every upgrade because replacing the binary
clears it.
Windows
# 1. Install Npcap from https://npcap.com (not bundled — its licence
# forbids redistribution. If Wireshark is installed, so is Npcap.)
# 2. Extract tlscensus.exe from the release .zip onto your PATH.
Any platform — verified download
curl -fsSLO https://github.com/connectedinformation/tlscensus/releases/latest/download/checksums.txt
curl -fsSLO https://github.com/connectedinformation/tlscensus/releases/latest/download/tlscensus_Linux_x86_64.tar.gz
sha256sum --ignore-missing -c checksums.txt
tar xzf tlscensus_*.tar.gz && sudo install tlscensus /usr/local/bin/
Every release ships checksums and an SBOM. There is deliberately no
curl | sh installer: this tool runs as root and its readers are security
engineers, so piping a URL into a root shell would route around exactly the
verification the release provides.
The binaries are not code-signed or notarised. Verify the checksum
instead — that is what checksums.txt is for. On macOS this means Gatekeeper
would otherwise refuse a Homebrew-installed binary, so the cask clears the
quarantine attribute on install; if you download the archive by hand and
macOS blocks it, xattr -dr com.apple.quarantine ./tlscensus is the same
step done yourself.
From source
go install github.com/connectedinformation/tlscensus/cmd/tlscensus@latest
Use
# Offline
tlscensus read capture.pcap
tlscensus read -o ndjson capture.pcapng | jq 'select(.pq_status == "classical")'
tlscensus read -o json -records *.pcap > inventory.json
# Live (Linux, macOS)
tlscensus interfaces
sudo tlscensus watch -i en0
sudo tlscensus watch -o ndjson | tee handshakes.ndjson
# Report
tlscensus read -o html capture.pcap > report.html # self-contained page
tlscensus read -o cbom capture.pcap > crypto.cdx.json
tlscensus serve capture.pcap # loopback + token
sudo tlscensus serve -i en0 # live, auto-refreshing
No cgo, anywhere. Linux capture is AF_PACKET; macOS reads /dev/bpf*
directly. There is no libpcap dependency, so go install works with no C
toolchain and one build runner produces every release target.
Reading a capture file needs no privileges at all, which is also why the
entire pipeline is exercised in CI. Live capture needs CAP_NET_RAW on Linux
or BPF device access on macOS — see docs/permissions.md
for how to grant the narrow capability instead of running as root.
What it gets right
Most of these are ways a TLS inventory quietly produces a wrong number
rather than an obvious error.
Key shares are reported separately from supported groups. A client can
advertise X25519MLKEM768 in supported_groups and still send a key share
only for x25519. It will then complete a fully classical handshake against
any server that takes the offer. Collapsing the two is the most common way a
migration dashboard flatters itself, so readiness is reported as a ladder —
post_quantum, offered_not_selected, advertised_only, classical —
rather than as a boolean.
Handshakes are reassembled across TCP segments. A post-quantum key share
is over a kilobyte, so a ClientHello offering one routinely exceeds a single
segment. A parser that reads one packet drops precisely the handshakes a
post-quantum inventory exists to count, and reports a more classical world
than the one on the wire.
Every TCP port is examined, not just 443. Detection is by content. STARTTLS
on 587, a database on 5432, an appliance on 8443 — restricting to the
well-known port is how "we found no weak ciphers" comes to mean "we did not
look".
GREASE is stripped (RFC 8701), so the report does not fill with phantom
0x8a8a cipher suites from every Chrome connection.
ECH presence is reported without being over-read. An
encrypted_client_hello extension does not prove the server name is a
decoy. Chrome sends a GREASE ECH extension on connections where no ECH config
exists, deliberately shaped to be indistinguishable from the real thing so
middleboxes cannot learn to reject it — and in practice most of what carries
the extension is GREASE, with a perfectly genuine server name.
A passive observer cannot tell them apart from the extension alone. What it
can do is watch config_id: a real config published in DNS is stable across
connections, GREASE randomises it, so variation is evidence of GREASE. Names
are reported and counted either way, marked (ech?) only where GREASE has
not been established.
TLS 1.2 groups are read from ServerKeyExchange, the only place a 1.2
handshake names its key exchange group.
QUIC is decoded, not skipped. Initial packets are protected with keys
derived from the connection ID in the clear, so HTTP/3 handshakes are read
the same as TCP ones. This matters more than it sounds: the providers who
deployed hybrid post-quantum key exchange early are the same ones who
deployed HTTP/3 early, so a TCP-only inventory reports a more classical
world than the one on the wire — and a site you just loaded can be missing
from it entirely.
What it cannot see
Stated plainly, because an inventory that overstates its coverage is worse
than no inventory.
-
TLS 1.3 encrypts the certificate. Certificate inventory — key sizes,
issuers, expiry — is only available for TLS 1.2 and below. This is the
protocol, not a gap in the parser.
-
QUIC connection migration, Retry and 0-RTT are not followed. A Retry
re-keys the connection on a new connection ID; it is detected, and the
flow is then abandoned rather than guessed at.
-
No process attribution. Flows are identified by address, port and
client fingerprint, not by the application that opened them. JA4 usually
distinguishes a browser from curl from a Java runtime, but it names the
TLS stack rather than the program.
This is a deliberate omission, not a gap waiting to be filled. Attribution
would be nice to have; it is not worth what it costs in privilege, build
complexity or platform inconsistency, and the report is informative without
it. See docs/roadmap.md for the reasoning.
-
Windows live capture needs Npcap, installed
separately — its licence forbids redistribution. tlscensus loads it at
runtime and does not bundle it, so read works on Windows with nothing
else installed.
-
STARTTLS is not tracked. A session that begins as cleartext SMTP or
IMAP and upgrades in place is not currently detected.
-
Resumed sessions carry no full handshake, so they report what the
client offered and little else.
Output
| Format |
Use |
text |
terminal summary and inventory (default) |
ndjson |
one record per handshake, streamed and not retained — for jq or a log shipper |
json |
the whole report, with -records for per-flow detail |
html |
self-contained page — no network access, no fonts, no CDN |
cbom |
CycloneDX 1.6 with cryptographic-asset components |
Everything except ndjson reports an inventory: handshakes are collapsed
onto the destination and the cryptography negotiated with it, carrying a
count, a time range and how many distinct client fingerprints were involved.
Seven identical connections to one host are one finding seen seven times, not
seven findings. ndjson remains the event stream, because a log pipeline
wants to do its own windowing.
The CBOM is what makes this a feed into other tooling rather than one more
dashboard: post-quantum readiness is a property of your estate, not of this
program. Its serial number is derived from the asset set rather than from
randomness, so two runs over the same inventory produce the same document
and can be diffed. Validate with scripts/validate-cbom.sh.
tlscensus serve puts the report on 127.0.0.1 only, behind a token
minted at startup. There is no flag to bind a routable address. Loopback by
itself is not an authorisation boundary — every local user can reach it —
which is why the token exists too.
Privacy
A list of the hostnames a machine contacted is browsing-history-grade data.
tlscensus has no telemetry, makes no network connections of its own, and
sends nothing anywhere. Output goes to the file or pipe you point it at.
Nothing in this repository phones home; grep -r 'http' over the source is
a short read.
Who makes this
tlscensus is built and maintained by Connected Information, which also
makes commercial TLS tooling. It is open source because a tool that runs as
root and reads every hostname a machine contacts should be one you can audit
before you trust it — and because the reports that make it correct come from
people running it on networks we do not have.
It has no telemetry, makes no network connections of its own, and is
Apache-2.0 licensed. Those are checkable claims, not assurances.
Contributing
A bug report from your own network is worth more to this project than a
pull request. Nearly every real defect it has had was found by someone
running it against real hardware, after a green CI run — see
docs/validation.md. Details in
CONTRIBUTING.md.
License
Apache License 2.0. See LICENSE.