feat: add cipher suite enumeration with raw TLS 1.3, QUIC, and key exchange probing#82
feat: add cipher suite enumeration with raw TLS 1.3, QUIC, and key exchange probing#82danielewood merged 30 commits intomainfrom
Conversation
…change probing
Add --ciphers flag to connect command that enumerates all cipher suites
a server supports across TLS 1.0–1.3, probes key exchange groups
(including post-quantum hybrids), and tests QUIC/UDP alongside TCP.
- Raw TLS 1.3 ClientHello prober (tls13probe.go): byte-level packet
construction offering single cipher/group per probe, fully isolated
with no shared state. Probes all 5 RFC 8446 suites and 7 named groups.
- QUIC v1 Initial packet prober (quicprobe.go): HKDF key derivation,
AES-128-GCM encryption, header protection per RFC 9001.
- HelloRetryRequest detection via RFC 8446 §4.1.3 sentinel random.
- Cipher output subgrouped by TLS version and key exchange type
(ECDHE vs RSA) with forward secrecy labels.
- QUIC section always visible when probed ("not supported" if rejected).
- Good/weak security ratings for all cipher suites.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Auto-update Go and npm dependencies on every commit to stay ahead of security vulnerabilities, eliminating the need for dependabot PRs. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Supersedes dependabot PR #81. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
There was a problem hiding this comment.
Pull request overview
This PR adds cipher suite enumeration functionality to the connect command via a new --ciphers flag. The implementation includes three major components:
Changes:
- Raw TLS 1.3 ClientHello prober with byte-level packet construction, eliminating data races from the previous linkname hack approach
- QUIC v1 Initial packet prober supporting cipher suite detection over UDP/443 with full RFC 9001 compliance
- Key exchange group probing for classical and post-quantum hybrid mechanisms (X25519MLKEM768, SecP256r1MLKEM768, SecP384r1MLKEM1024)
Reviewed changes
Copilot reviewed 12 out of 13 changed files in this pull request and generated 6 comments.
Show a summary per file
| File | Description |
|---|---|
| tls13probe.go | Raw TLS 1.3 ClientHello packet builder and parser with isolated per-probe connections |
| quicprobe.go | QUIC v1 Initial packet builder with HKDF key derivation and AES-128-GCM encryption |
| connect.go | Main cipher scanning logic with concurrent probing, rating system, and result formatting |
| connect_test.go | Comprehensive test coverage for rating, scanning, formatting, and round-trip operations |
| testhelpers_test.go | New test helper for TLS servers with custom configs |
| cmd/certkit/connect.go | CLI integration with spinner and 60-second timeout |
| cmd/certkit/spinner.go | Terminal-aware progress indicator for long-running operations |
| go.mod | Added go-isatty dependency for terminal detection |
| README.md, EXAMPLES.md, CHANGELOG.md | Documentation updates |
Files not reviewed (1)
- web/package-lock.json: Language not supported
This comment has been minimized.
This comment has been minimized.
- Add bounds checks for DCID/SCID lengths in QUIC response parser
- Add varint decode guards to prevent infinite loops on malformed ACK frames
- Increase UDP read buffer from 4096 to 65535 bytes
- Add session ID length bounds check in TLS ServerHello parser
- Refactor probe functions to use cipherProbeInput struct (CS-5)
- Fix EXAMPLES.md cipher rating terminology ("strong" → "good")
- Consolidate tests per T-9/T-11/T-12, add parser edge case coverage
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
Address adversarial review findings from PR #82: - Guard QUIC varint uint64→int casts with uint64-space bounds checks to prevent truncation on malicious packets - Fix ACK range loop inner break not propagating to outer frame parser - Cap ACK rangeCount to plaintext length to prevent CPU exhaustion - Remove double-wrapped error messages in connect CLI - Initialize CipherScanResult nil slices to empty for JSON encoding - Strengthen TestBuildQUICInitialPacket with header + round-trip decrypt - Consolidate TestRateCipherSuite from 13 to 6 entries (T-12) - Merge TestScanCipherSuites_KeyExchanges into TestScanCipherSuites (T-14) - Fix brittle tls13Count != 3 assertion Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This comment has been minimized.
This comment has been minimized.
…spinner safety - Rate TLS_AES_128_CCM_8_SHA256 (0x1305) as weak (IANA Not Recommended, truncated 8-byte auth tag) - Return empty OverallRating when no ciphers detected (omitempty JSON) - Guard FormatCipherScanResult against nil receiver - Tie spinner goroutine to context.Context (CC-2), make Stop idempotent with sync.Once to prevent double-close panic - Lowercase error strings per ERR-4: hello retry request, server hello, long header - Add hkdfExpandLabelInput struct per CS-5 - Remove direct tests of unexported helpers per T-11 (buildClientHelloMsg, parseServerHello, buildQUICInitialPacket, probeTLS13Cipher) — all exercised through ScanCipherSuites - Fix changelog refs: use PR [#82] instead of branch commit SHA - Fix probeTimeout comment to accurately describe context inheritance Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This comment has been minimized.
This comment has been minimized.
…nner safety - Add TLS_AES_128_CCM_8_SHA256 (0x1305) → CipherRatingWeak test case covering the only TLS 1.3 cipher rated weak (T-12) - Replace in-branch commit SHAs (18ed288, 1adb9b5) with PR ref [#82] so changelog links survive squash merge (CL-3/CL-4) - Move go-mod-update and npm-update pre-commit hooks to stages: [manual] to avoid unexpected dependency churn on every commit - Guard spinner.Start() with sync.Once for idempotent calls - Add slog.Debug for QUIC PADDING/PING frame skips (ERR-5) - Document X25519-only limitation in probeTLS13Cipher comment Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This comment has been minimized.
This comment has been minimized.
- Use slices.Concat instead of append for cipher suite slice concatenation to prevent potential mutation of stdlib return value - Show "Cipher suites: none detected" when scan finds no supported suites instead of silent empty output - Add nil and empty-ciphers test cases to TestFormatCipherScanResult with exact-match assertion (previously asserted nothing) - Consolidate startTLSServer to delegate to startTLSServerWithConfig, eliminating duplicated accept-loop code Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…, version return
- ERR-4: lowercase TLS/QUIC error strings across tls13probe.go, legacyprobe.go,
quicprobe.go, and connect.go ("tls alert received", "tls record too large",
"quic packet too short", "tls handshake with …")
- ERR-1: wrap bare return err at CLI connect boundary with fmt.Errorf context
- ERR-5: add slog.Debug before continue in QUIC ACK frame handler
- Naming: rename emptyClientCert → emptyClientCertificate per convention
- Bug: FormatCipherScanResult showed "none detected" for QUIC-only servers;
empty check now covers both r.Ciphers and r.QUICCiphers
- Bug: probeLegacyCipher hardcoded "TLS 1.2" — now returns actual negotiated
version from ServerHello; ScanCipherSuites uses tlsVersionString(negotiatedVer)
- T-11: remove TestBuildLegacyClientHelloMsg — behavioral coverage exists
through TestLegacyFallbackConnect
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
|
Fixed in 6492fa5:
CS-5 on |
|
Fixed in 6492fa5 — all four error strings lowercased:
|
|
Fixed in 6492fa5 — renamed |
|
Fixed in 6492fa5:
|
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…y possession is not The raw legacy probe path calls populateConnectResult which runs full x509.Verify against the system root store, AIA walking, OCSP, and CRL checks. The old "Verify: N/A (raw handshake — certificate not cryptographically verified)" message was misleading — the chain IS cryptographically verified. What is NOT verified is that the server possesses the private key for the certificate (no TLS Finished message was exchanged). Update: remove the LegacyProbe branch that suppressed Verify output; show the real result (OK/FAILED). Update the Note line and the legacy-only diagnostic to accurately describe what is and isn't verified. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
…IC port - Skip OCSP and CRL checks for legacy probes; revocation is not meaningful without an authenticated TLS channel. Eliminates misleading "OCSP: skipped (no issuer in chain)" output on raw handshake results. x509 chain verification is still performed. - Guard legacy fallback behind tls.AlertError: only attempt raw DHE probe when the server sent a TLS alert (cipher negotiation failure), not for network errors or certificate errors that would add a spurious 5-second timeout. - Restrict QUIC probing to port 443: non-443 ports cause spurious timeouts since QUIC is not conventionally served there. - Fix TOCTOU in spinner.Stop(): remove started guard, use stopOnce unconditionally — works correctly whether Stop() races with Start() or not. - Replace in-place diagnostics filter with a new slice allocation to eliminate aliasing confusion. - Remove stale CHANGELOG entry saying QUIC is unrestricted (reversed). Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Convert populateConnectResult to (*ConnectResult).populate — reduces argument count from 3 to 2 (ctx + input) per CS-5 - Remove TestParseCertificateMessage — behavioral coverage exists through TestReadServerCertificates (T-11) - Remove TestCipherSuiteNameLegacyIDs — behavioral coverage exists through TestScanCipherSuites (T-11) - Shorten full-SHA CHANGELOG link definitions to 7-char SHAs (CL-4) Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
|
Fixed in |
|
Fixed in
|
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
…ntext - Fix Stop() deadlock when called before Start(): use startOnce.Do to close done channel, ensuring <-s.done never blocks regardless of call order - Include legacyErr in error message when TLS handshake + fallback both fail - Update RateCipherSuite comment: clarify DHE/DSS have no modern FS guarantees - Lowercase QUIC/TLS error strings in quicprobe.go, legacyprobe.go, tls13probe.go per ERR-4 (error strings must be lowercase, acronyms included) - Extract appendKeyShareExtensionInput struct for appendKeyShareExtension per CS-5 - Fix ERR-5 in TestLegacyFallbackConnect: log Read/Write errors via slog.Debug - Update test assertion for lowercased error string
|
Fixed in
|
|
Fixed in
|
Code reviewERR-5 (MUST): Silent error discard in all probe functionsAffected files: All six probe functions ( ERR-5 (MUST): "Never silently ignore errors. Use The first sentence is a general prohibition — not limited to The probe functions conflate two distinct outcomes into the same silent
Without any logging, unexpected failures are invisible during debugging. Example from // https://github.com/sensiblebit/certkit/blob/0c24518d10590e91e5384bc61ce97dc518a1f86b/tls13probe.go#L391-L395
conn, err := dialer.DialContext(ctx, "tcp", input.addr)
if err != nil {
return false // ERR-5: no slog.Debug
}This pattern appears ~20 times across the four probe files. The fix is to add conn, err := dialer.DialContext(ctx, "tcp", input.addr)
if err != nil {
slog.Debug("tls13 cipher probe dial failed", "addr", input.addr, "cipher", input.cipherID, "error", err)
return false
} |
Summary
--ciphersflag toconnectthat enumerates all cipher suites a server supports across TLS 1.0–1.3, with good/weak security ratingstls13probe.go) — byte-level packet construction, no shared state, no data races. Probes all 5 RFC 8446 cipher suites and 7 key exchange groups (including post-quantum hybrids)quicprobe.go) — HKDF key derivation, AES-128-GCM encryption, header protection per RFC 9001. Automatically probes UDP 443 alongside TCPTest plan
go test -race -count=1 ./...passesgo vet ./...passesgolangci-lint runpassescertkit connect google.com --ciphersshows TCP ciphers, QUIC ciphers, and key exchange groupscertkit connect gmail.com --ciphersshows "QUIC: not supported" (gmail.com rejects QUIC — confirmed with curl --http3-only)certkit connect google.com --ciphers --jsonproduces valid JSON🤖 Generated with Claude Code