Skip to content

Commit df83a99

Browse files
danielewoodclaude
andauthored
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 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> * docs: update changelog refs for cipher audit commit Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * ci: add go mod update and npm update pre-commit hooks 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> * build(deps): bump rollup to 4.59.0 and wrangler to 4.20260305.0 Supersedes dependabot PR #81. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * fix: harden QUIC/TLS parsers, address PR review feedback - 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> * docs: update changelog refs from pending to 1adb9b5 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * fix: harden QUIC parser, fix CLI error wrapping, consolidate tests 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> * docs: update changelog refs from pending to 18ed288 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * fix: address PR review — rating bug, error strings, test compaction, 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> * fix: address remaining PR review — test coverage, changelog refs, spinner 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> * fix: use slices.Concat, show empty cipher message, strengthen tests - 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> * docs: update changelog refs from pending to 7a155c3 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * fix: include QUIC ciphers in overall rating and diagnostics, fix spinner data race - Fix OverallRating, FormatCipherRatingLine, and DiagnoseCipherScan ignoring QUIC ciphers — weak QUIC suites were excluded from the overall rating computation and diagnostic count - Fix data race on spinner.started — replace bool with atomic.Bool for safe concurrent access between Start() and Stop() goroutines - Fix bare error returns in deriveTrafficKeys — wrap with context - Replace commit SHA changelog refs with PR refs for squash merge - Add QUIC weak cipher test cases, unknown cipher ID coverage - Remove redundant test cases per T-14 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * fix: address review findings — error wrapping, QUIC bounds checks, test hardening - Wrap ConnectTLS and ScanCipherSuites errors with host context (ERR-1) - Add bounds check before QUIC packet number unmasking (OOB write) - Add panic guard to appendQUICVarint2 for overflow values - Skip QUIC probes on non-443 ports to avoid wasted timeouts - Add context cancellation test for ScanCipherSuites - Add InsecureCipherSuites isolation test (ECDHE+RC4) - Remove redundant per-cipher rating loop (covered by TestRateCipherSuite) - Strengthen TestDiagnoseCipherScan with exact match assertions - Add empty-non-nil edge case to TestFormatCipherRatingLine Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * feat: improve connect diagnostics — hostname mismatch, error-level diagnostics, specific cipher checks Verification failures (verify-failed, hostname-mismatch, ocsp-revoked, crl-revoked) now surface as [ERR] diagnostics in the output instead of a redundant Error: line on stderr. Exit code 2 is preserved. Cipher diagnostics replaced the single vague "N weak cipher suite(s)" message with specific actionable checks: deprecated-tls10, deprecated-tls11, cbc-cipher, static-rsa-kex, 3des-cipher. QUIC probing is no longer restricted to port 443. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * fix: sort diagnostics — errors first, then alphabetically by check name Ensures stable output order regardless of the order diagnostics are appended from different sources (chain analysis, verify errors, cipher scan). Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * feat: add raw TLS 1.0–1.2 legacy prober for DHE/static-RSA-only servers Go's crypto/tls has never implemented DHE key exchange and doesn't offer static RSA by default. Servers that only support these cipher suites (e.g. badssl.com DHE endpoints) fail with "handshake failure". This adds a byte-level TLS 1.0–1.2 ClientHello prober (extending the existing tls13probe.go approach) that can: 1. Probe 13 DHE/DHE-DSS cipher suites in `--ciphers` scans 2. Fall back to raw handshake in ConnectTLS when Go's handshake fails, extracting the server certificate chain for inspection 3. Diagnose the negotiated cipher suite on every connect (CBC, 3DES, static RSA, DHE, deprecated TLS versions) — no --ciphers needed New files: - legacyprobe.go: cipher registry, buildLegacyClientHelloMsg, probeLegacyCipher, readServerCertificates, parseCertificateMessage, legacyFallbackConnect - legacyprobe_test.go: unit tests for packet construction + parsing Modified: - connect.go: populateConnectResult helper (shared normal/legacy paths), legacy fallback in ConnectTLS, DHE probes in ScanCipherSuites, DiagnoseNegotiatedCipher, dhe-kex diagnostic, LegacyProbe field - connect_test.go: TestDiagnoseNegotiatedCipher, DHE cases in TestDiagnoseCipherScan - cmd/certkit/connect.go: LegacyProbe in JSON output Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * fix: address review findings — fallback timeout, dedup diagnostics, remove dead code - Add dedicated 5s timeout for legacy fallback connection to prevent indefinite blocking when a server stalls - Deduplicate diagnostics when --ciphers is used: scan-level diagnostics supersede negotiated-cipher diagnostics by check name - Remove redundant legacyCipherSuiteName — cipherSuiteName already covers legacy IDs - Add cross-record Certificate message test (spanning two TLS records) - Check errAlertReceived sentinel in alert test instead of generic error Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * fix: address PR review comments — error wrapping, lowercase errors, doc fix - Wrap all bare error returns in generateKeyShare with context (ERR-1) - Lowercase "ClientHello", "ServerHello", "Certificate" in error strings (ERR-4) - Fix probeQUICCipher doc comment: says "UDP 443" but actually uses input.addr Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * docs: update PR feedback rules — reply, resolve, and minimize Add minimize step to PR feedback workflow. Expand GraphQL query to fetch both review thread and conversation comment node IDs. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * fix: address PR review comments — error strings, QUIC versions, panic, dup context - Lowercase remaining "ClientHello"/"ServerHello" in legacyprobe.go errors (ERR-4) - Add QUIC cipher versions to SupportedVersions set (versionSet was TCP-only) - Replace appendQUICVarint2 panic with fallback to appendQUICVarint for v>=16384 - Return ConnectTLS error directly in CLI to avoid duplicate context prefix - Use "cipher suite scan for %s" prefix to avoid duplication with ScanCipherSuites internal "scanning cipher suites: ..." wrapping Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * fix: address adversarial review findings — security bounds, UX, test coverage - tighten maxCertificatePayload check in readServerCertificates: enforce limit before allocating record payload buffer (was checked at top of loop, allowing up to one extra 16KB allocation past the cap) - tighten QUIC ACK rangeCount cap: use len(plaintext)/2 since each range item requires at least 2 varint bytes (gap + range) - lowercase CRYPTO in quicprobe.go error strings (ERR-4) - add Note: and Verify: N/A to connect output for LegacyProbe results — replaces misleading Verify: OK for a raw handshake path - add readServerCertificates tests: oversized record, unexpected content type, ServerHelloDone-without-Certificate, alert-after-ServerHello, payload limit enforcement (T-8) - add FormatConnectResult/LegacyProbe test case - remove T-9 violation from TestCipherSuiteNameLegacyIDs (0x1301 tests stdlib routing, not certkit logic) Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * docs: update CHANGELOG refs from pending to commit SHAs Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * fix: address review findings — lowercase errors, naming, QUIC display, 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> * docs: update changelog pending refs to 6492fa5 Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * fix: show real verify result for legacy probe — chain is verified, key 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> * docs: update changelog pending ref to 772742c Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * fix: address review findings — legacy probe OCSP, TLS alert guard, QUIC 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> * fix: address review findings — CS-5, T-11, CL-4 - 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> * fix: address review findings — Stop deadlock, ERR-4/5, CS-5, error context - 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 --------- Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
1 parent 2ba8a32 commit df83a99

18 files changed

Lines changed: 4116 additions & 158 deletions

.claude/rules/commits-and-prs.md

Lines changed: 21 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -55,28 +55,41 @@ When a pre-commit hook modifies files (e.g., goimports reformats struct alignmen
5555

5656
This is especially common with `goimports` reformatting Go files.
5757

58-
## Resolving PR review threads
58+
## Addressing PR feedback
5959

60-
When addressing review feedback, both reply AND resolve:
60+
When working on a PR, address **both** PR review comments (on diffs) and issue-style comments (on the PR conversation). For each piece of feedback:
6161

62-
1. **Reply** via REST: `gh api repos/OWNER/REPO/pulls/N/comments -X POST -F body="..." -F in_reply_to=COMMENT_ID`
63-
2. **Resolve** via GraphQL: `gh api graphql -f query='mutation { resolveReviewThread(input: {threadId: "THREAD_ID"}) { thread { isResolved } } }'`
62+
1. **Fix the code** — make the requested change or explain why not
63+
2. **Reply** explaining what was done: `gh api repos/OWNER/REPO/pulls/N/comments -X POST -F body="..." -F in_reply_to=COMMENT_ID`
64+
3. **Resolve** the thread: `gh api graphql -f query='mutation { resolveReviewThread(input: {threadId: "THREAD_ID"}) { thread { isResolved } } }'`
65+
4. **Minimize** addressed comments to reduce noise: `gh api graphql -f query='mutation { minimizeComment(input: {subjectId: "COMMENT_NODE_ID", classifier: RESOLVED}) { minimizedComment { isMinimized } } }'`
6466

65-
To get thread IDs, query:
67+
To get thread IDs and comment node IDs, query:
6668

6769
```sh
6870
gh api graphql -f query='{
6971
repository(owner: "sensiblebit", name: "certkit") {
7072
pullRequest(number: N) {
71-
reviewThreads(first: 20) {
72-
nodes { id isResolved comments(first: 1) { nodes { body } } }
73+
reviewThreads(first: 50) {
74+
nodes {
75+
id
76+
isResolved
77+
comments(first: 5) {
78+
nodes { id databaseId body }
79+
}
80+
}
81+
}
82+
comments(first: 50) {
83+
nodes { id databaseId body }
7384
}
7485
}
7586
}
7687
}'
7788
```
7889

79-
Just replying does NOT mark the thread as resolved in the GitHub UI.
90+
For issue-style comments (PR conversation), use `minimizeComment` with the comment's node `id`. For review comments, reply + resolve + minimize.
91+
92+
Just replying does NOT mark the thread as resolved or minimized in the GitHub UI — all three steps are required.
8093

8194
## Merging PRs
8295

.pre-commit-config.yaml

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,25 @@ repos:
2020
- id: go-build
2121
- id: go-test
2222

23+
# ── Dependency Updates ──
24+
- repo: local
25+
hooks:
26+
- id: go-mod-update
27+
name: go mod update
28+
entry: bash -c 'go get -u ./... && go mod tidy'
29+
language: system
30+
files: \.go$
31+
pass_filenames: false
32+
stages: [manual]
33+
34+
- id: npm-update
35+
name: npm update
36+
entry: bash -c 'cd web && npm update'
37+
language: system
38+
files: ^web/
39+
pass_filenames: false
40+
stages: [manual]
41+
2342
# ── Docs ──
2443
- repo: local
2544
hooks:

CHANGELOG.md

Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,18 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
99

1010
### Added
1111

12+
- Add raw TLS 1.0–1.2 legacy prober for DHE/DHE-DSS cipher suites that Go's `crypto/tls` doesn't implement — probes individual suites via byte-level ClientHello construction ([`715cb81`])
13+
- Add legacy fallback to `connect` — when Go's TLS handshake fails, attempts a raw handshake to extract server certificates from DHE-only or static-RSA-only servers ([`715cb81`])
14+
- Add DHE cipher suite probing to `connect --ciphers` — detects 13 DHE/DHE-DSS cipher suites using raw ClientHello packets, all rated "weak" ([`715cb81`])
15+
- Add `dhe-kex` diagnostic to `connect --ciphers` — warns when server accepts DHE key exchange cipher suites (deprecated, vulnerable to small DH parameters) ([`715cb81`])
16+
- Add negotiated cipher diagnostics to `connect` — warns about CBC mode, 3DES, static RSA, DHE, and deprecated TLS versions even without `--ciphers` ([`715cb81`])
17+
- Add hostname-mismatch diagnostic to `connect` — detects `x509.HostnameError` and surfaces it as `[ERR] hostname-mismatch` in the diagnostics section ([`715cb81`])
18+
- Add error-level diagnostics (`verify-failed`, `ocsp-revoked`, `crl-revoked`) to `connect` output — validation failures now appear in the Diagnostics section instead of a redundant `Error:` line on stderr ([`715cb81`])
19+
- Add specific cipher diagnostics to `connect --ciphers` — replaces the single "weak cipher" message with actionable checks: `deprecated-tls10`, `deprecated-tls11`, `cbc-cipher`, `static-rsa-kex`, `3des-cipher` ([`715cb81`])
20+
- Add `--ciphers` flag to `connect` command — enumerates all supported cipher suites with good/weak ratings, key exchange subgrouping, and forward secrecy labels ([#82])
21+
- Add raw TLS 1.3 cipher prober — probes all 5 RFC 8446 cipher suites using byte-level ClientHello construction, no shared state or data races ([#82])
22+
- Add key exchange group probing to `--ciphers` — detects all 7 named groups including post-quantum hybrids (X25519MLKEM768, SecP256r1MLKEM768, SecP384r1MLKEM1024) with HelloRetryRequest detection ([#82])
23+
- Add QUIC/UDP cipher probing to `--ciphers` — automatically probes UDP 443 alongside TCP, shows "QUIC: not supported" when server rejects ([#82])
1224
- Auto-generate CLI flag tables in README from Cobra command definitions via `go generate` ([#80])
1325
- Add `gendocs` pre-commit hook and CI check to verify flag tables stay in sync ([#80])
1426
- Add global `--json` persistent flag — all commands now support JSON output; overrides `--format` when both are set ([#80])
@@ -53,6 +65,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
5365

5466
### Changed
5567

68+
- `connect` diagnostics now distinguish `[ERR]` (verification failures) from `[WARN]` (configuration issues) ([`910b977`])
69+
- Harden QUIC response parser — add bounds checks for DCID/SCID lengths, varint decode guards to prevent infinite loops on malformed ACK frames, and increase UDP read buffer to 65535 bytes ([#82])
70+
- Harden TLS ServerHello parser — add explicit bounds check for oversized session ID length before advancing position ([#82])
71+
- Refactor probe functions to use input structs per CS-5 — `probeTLS13Cipher`, `probeKeyExchangeGroup`, `probeQUICCipher`, `probeCipher`, `probeKeyExchangeGroupLegacy` now take `cipherProbeInput` ([#82])
72+
- Convert `populateConnectResult` to a method `(*ConnectResult).populate` per CS-5 — reduces argument count from 3 to 2 (ctx + input) ([#82])
73+
- Convert `appendKeyShareExtension` to accept `appendKeyShareExtensionInput` struct per CS-5 — function had 3 arguments ([#82])
5674
- **Breaking:** Rename `csr --cert` flag to `--from-cert` for clarity — avoids confusion with certificate file arguments in other commands ([#80])
5775
- **Breaking:** `connect` JSON `sha256_fingerprint` format changed from lowercase hex to colon-separated uppercase hex for CLI-4 consistency with `inspect` and `sha1_fingerprint` ([#80])
5876
- **Breaking:** Rename `CRLCheckResult.DistributionPoint` to `CRLCheckResult.URL` (JSON: `url`) and `OCSPResult.ResponderURL` to `OCSPResult.URL` (JSON: `url`) — consistent field name for the checked endpoint across both revocation types (CLI-4) ([#78])
@@ -69,6 +87,41 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
6987

7088
### Fixed
7189

90+
- Fix `connect` legacy probe showing `Verify: N/A` despite performing full x509 chain verification — now shows the real verify result (`OK`/`FAILED`); Note line updated to clarify only server key possession is unverified ([`772742c`])
91+
- Fix `connect --ciphers` showing "none detected" on QUIC-only servers — empty check now covers both TCP and QUIC cipher lists ([`6492fa5`])
92+
- Fix `probeLegacyCipher` hardcoding `"TLS 1.2"` for negotiated version — now returns the actual negotiated version from the ServerHello ([`6492fa5`])
93+
- Fix error strings violating ERR-4 (must be lowercase): `"tls alert received"`, `"tls record too large"`, `"quic packet too short"`, `"tls handshake with ..."` ([`6492fa5`])
94+
- Fix bare `return err` at CLI connect boundary — now wraps with context per ERR-1 ([`6492fa5`])
95+
- Fix missing `slog.Debug` before `continue` in QUIC ACK frame handler per ERR-5 ([`6492fa5`])
96+
- Rename `emptyClientCert``emptyClientCertificate` per naming convention ([`6492fa5`])
97+
- Fix bare `return err` in `connect` CLI dropping host context from error messages — ConnectTLS and ScanCipherSuites errors now wrap with host and operation (ERR-1) ([#82])
98+
- Fix potential out-of-bounds write in QUIC response parser when packet number length exceeds remaining packet bytes ([#82])
99+
- Add panic guard to `appendQUICVarint2` for values >= 16384 that would silently produce corrupt 2-byte encoding ([#82])
100+
- Skip QUIC cipher probes on non-443 ports — avoids wasted 10s of timeout when QUIC is not conventionally served ([#82])
101+
- Use `slices.Concat` instead of `append` for cipher suite slice concatenation — prevents potential mutation of stdlib return value ([#82])
102+
- Show "Cipher suites: none detected" when cipher scan finds no supported suites instead of silent empty output ([#82])
103+
- Fix `OverallRating`, `FormatCipherRatingLine`, and `DiagnoseCipherScan` ignoring QUIC ciphers — weak QUIC ciphers were excluded from the overall rating and diagnostic count ([#82])
104+
- Fix TOCTOU race in `spinner.Stop()` — remove started guard and use `stopOnce` unconditionally so Stop() is safe regardless of concurrency with Start() ([#82])
105+
- Fix `connect` legacy probe running OCSP and CRL checks — revocation checks are now skipped for legacy probes since there is no cryptographic chain to verify revocation against; eliminates misleading `OCSP: skipped (no issuer certificate in chain)` output ([#82])
106+
- Fix `connect` legacy fallback triggering on all TLS handshake failures — now only attempted on `tls.AlertError` (cipher negotiation failure), not network errors or certificate errors that would add a spurious 5-second timeout ([#82])
107+
- Fix `connect` error message swallowing `legacyErr` when legacy fallback also fails — both the original TLS alert and the legacy fallback error are now included ([#82])
108+
- Fix `spinner.Stop()` deadlock when called before `Start()` — Stop() now closes `done` via `startOnce` so `<-s.done` never blocks ([#82])
109+
- Fix uppercase `QUIC` and `TLS` in error strings in `quicprobe.go`, `legacyprobe.go`, and `tls13probe.go` violating ERR-4 ([#82])
110+
- Fix `connect --ciphers` diagnostics filter using in-place slice aliasing — now allocates a new slice to avoid confusing aliasing semantics ([#82])
111+
- Fix bare error returns in `deriveTrafficKeys` — wrap with `%w` context per ERR-1 ([#82])
112+
- Fix `SupportedVersions` missing QUIC-only TLS versions — QUIC cipher versions now added to version set alongside TCP ciphers ([`bed32df`])
113+
- Fix `appendQUICVarint2` panic on values ≥ 16384 — falls back to `appendQUICVarint` instead of panicking on unexpected input ([`bed32df`])
114+
- Fix duplicate error context in `connect` CLI — `ConnectTLS` error returned directly; `ScanCipherSuites` error uses non-repeating prefix ([`bed32df`])
115+
- Fix remaining uppercase protocol names in `legacyprobe.go` error strings (ERR-4) ([`bed32df`])
116+
- Fix `readServerCertificates` totalRead check — enforce `maxCertificatePayload` limit before allocating record payload buffer, preventing over-allocation by a malicious server ([`900d526`])
117+
- Fix QUIC ACK range count cap — use `len(plaintext)/2` instead of `len(plaintext)` since each range item requires at minimum 2 varint bytes ([`900d526`])
118+
- Fix uppercase `CRYPTO` in `quicprobe.go` error strings — lowercase per ERR-4 ([`900d526`])
119+
- Fix `connect` output showing misleading `Verify: OK` when result was obtained via raw legacy probe — now shows `Verify: N/A` and a `Note:` header line ([`900d526`])
120+
- Fix QUIC varint `uint64``int` overflow in `parseQUICInitialResponse` — bounds checks now compare in `uint64` space to prevent truncation on malicious packets ([#82])
121+
- Fix ACK range loop inner `break` not propagating to outer frame parser in QUIC decoder — malformed ACK frames could corrupt subsequent frame parsing ([#82])
122+
- Cap ACK `rangeCount` to plaintext length to prevent CPU exhaustion on malicious QUIC packets ([#82])
123+
- Fix double-wrapped error messages in `connect` CLI — "connecting to: connecting to:" and "scanning cipher suites: scanning cipher suites:" ([#82])
124+
- Fix `CipherScanResult` JSON encoding `supported_versions` and `ciphers` as `null` instead of `[]` when no ciphers detected ([#82])
72125
- Fix backtick-quoted values in flag usage strings being consumed by pflag as type placeholders — all `--format`, `--trust-store`, `--log-level`, `--algorithm`, and `--curve` flags now display correctly in `--help` output ([#80])
73126
- Fix `convert --json` without `-o` missing `format` field in JSON output ([#80])
74127
- Fix data race in `TestCheckLeafCRL` — CRL bytes are now generated before starting the test HTTP server (CC-3) ([#78])
@@ -142,7 +195,31 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
142195

143196
### Tests
144197

198+
- Remove `TestBuildLegacyClientHelloMsg` — behavioral coverage exists through `TestLegacyFallbackConnect` per T-11 ([`6492fa5`])
199+
- Remove `TestParseCertificateMessage` — behavioral coverage exists through `TestReadServerCertificates` per T-11 ([#82])
200+
- Fix `_, _` error discards in `TestLegacyFallbackConnect` mock server goroutine — replaced with `slog.Debug` per ERR-5 ([#82])
201+
- Remove `TestCipherSuiteNameLegacyIDs` — behavioral coverage exists through `TestScanCipherSuites` per T-11 ([#82])
202+
- Strengthen `TestBuildQUICInitialPacket` — verify QUIC v1 version, DCID/SCID in header, and round-trip decrypt CRYPTO frame against original ClientHello ([#82])
203+
- Consolidate `TestRateCipherSuite` from 13 entries to 6 — one per distinct code path (T-12) ([#82])
204+
- Merge `TestScanCipherSuites_KeyExchanges` into `TestScanCipherSuites` — eliminates redundant server setup (T-14) ([#82])
205+
- Fix brittle `tls13Count != 3` assertion — use `>= 1` to tolerate future Go TLS 1.3 cipher additions ([#82])
206+
- Consolidate `FormatCipherScanResult` tests — merge QUIC and key exchange standalone tests into table-driven test ([#82])
207+
- Consolidate `BuildClientHello` tests — merge ALPN/QUIC test into subtests with session ID assertion ([#82])
208+
- Add nil and empty-ciphers test cases to `TestFormatCipherScanResult` — previously the empty case asserted nothing ([#82])
209+
- Consolidate `startTLSServer` to delegate to `startTLSServerWithConfig` — eliminates duplicated accept-loop code ([#82])
210+
- Remove tests that validate upstream behavior rather than certkit logic: `TestDeriveQUICInitialKeys`, `TestGenerateKeyShare`, `TestIsPQKeyExchange` ([#82])
211+
- Add `parseServerHello` edge case tests — oversized session ID length, truncation at compression method ([#82])
212+
- Add `FormatConnectResult` tests for "Verify: FAILED" and "Client Auth: any CA" paths ([#82])
213+
- Add QUIC weak cipher test case to `TestDiagnoseCipherScan` — validates QUIC ciphers are included in diagnostic count ([#82])
214+
- Add QUIC-only test case to `TestFormatCipherRatingLine` — validates QUIC ciphers counted in rating summary ([#82])
215+
- Replace RC4 test case with unknown cipher ID (0xFFFF) in `TestRateCipherSuite` — tests conservative rating for unrecognized ciphers ([#82])
216+
- Remove redundant `TestFormatCipherScanResult/single_cipher` and `TestFormatCipherRatingLine/TCP_good_QUIC_weak` — subsumed by stronger cases (T-14) ([#82])
145217
- Add `TestConnectTLS_CRL_AIAFetchedIssuer` — verifies CRL checking works when issuer is obtained via AIA walking ([#78])
218+
- Add `TestReadServerCertificates` cases for oversized record, unexpected content type, and ServerHelloDone-without-Certificate paths (T-8) ([`900d526`])
219+
- Add `TestReadServerCertificates_AlertAfterServerHello` — verifies ServerHello result is preserved when alert arrives after it ([`900d526`])
220+
- Add `TestReadServerCertificates_PayloadLimit` — verifies `maxCertificatePayload` is enforced before allocation ([`900d526`])
221+
- Add `TestFormatConnectResult/LegacyProbe` case — verifies Note and `Verify: N/A` appear for raw-probe results ([`900d526`])
222+
- Remove T-9 violation from `TestCipherSuiteNameLegacyIDs``0x1301` (TLS_AES_128_GCM_SHA256) test was exercising stdlib routing, not certkit logic ([`900d526`])
146223
- Add `TestFetchCRL_AllowPrivateNetworks` — verifies loopback IPs succeed with `AllowPrivateNetworks` ([#78])
147224
- Add `TestFetchCRL` unit tests for HTTP handling, redirect limits, SSRF blocking, and error paths ([#78])
148225
- Add `TestCheckLeafCRL` table-driven tests covering revoked, good, expired CRL, wrong issuer, no CDPs, and non-HTTP CDPs ([#78])
@@ -773,6 +850,10 @@ Initial release.
773850
[0.1.2]: https://github.com/sensiblebit/certkit/compare/v0.1.1...v0.1.2
774851
[0.1.1]: https://github.com/sensiblebit/certkit/compare/v0.1.0...v0.1.1
775852
[0.1.0]: https://github.com/sensiblebit/certkit/releases/tag/v0.1.0
853+
[`900d526`]: https://github.com/sensiblebit/certkit/commit/900d526
854+
[`bed32df`]: https://github.com/sensiblebit/certkit/commit/bed32df
855+
[`910b977`]: https://github.com/sensiblebit/certkit/commit/910b977
856+
[`715cb81`]: https://github.com/sensiblebit/certkit/commit/715cb81
776857
[`2693116`]: https://github.com/sensiblebit/certkit/commit/2693116
777858
[`84c4edf`]: https://github.com/sensiblebit/certkit/commit/84c4edf
778859
[`2b8cb8c`]: https://github.com/sensiblebit/certkit/commit/2b8cb8c
@@ -837,6 +918,7 @@ Initial release.
837918
[#76]: https://github.com/sensiblebit/certkit/pull/76
838919
[#78]: https://github.com/sensiblebit/certkit/pull/78
839920
[#80]: https://github.com/sensiblebit/certkit/pull/80
921+
[#82]: https://github.com/sensiblebit/certkit/pull/82
840922
[#73]: https://github.com/sensiblebit/certkit/pull/73
841923
[#64]: https://github.com/sensiblebit/certkit/pull/64
842924
[#63]: https://github.com/sensiblebit/certkit/pull/63
@@ -853,3 +935,5 @@ Initial release.
853935
[#25]: https://github.com/sensiblebit/certkit/pull/25
854936
[#26]: https://github.com/sensiblebit/certkit/pull/26
855937
[#27]: https://github.com/sensiblebit/certkit/pull/27
938+
[`6492fa5`]: https://github.com/sensiblebit/certkit/commit/6492fa5
939+
[`772742c`]: https://github.com/sensiblebit/certkit/commit/772742c

EXAMPLES.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -178,6 +178,14 @@ certkit connect example.com --crl
178178

179179
certkit exits with code 2 if the certificate is revoked (via OCSP or CRL).
180180

181+
To enumerate all cipher suites the server supports with security ratings:
182+
183+
```sh
184+
certkit connect example.com --ciphers
185+
```
186+
187+
Each cipher suite is rated `good` (ECDHE + AEAD, all TLS 1.3 suites) or `weak` (CBC, static RSA, RC4, 3DES). Weak ciphers are listed with a warning recommending they be disabled.
188+
181189
For machine-readable output:
182190

183191
```sh

README.md

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -155,12 +155,13 @@ Chain verification is always performed. When the input contains an embedded priv
155155
### Connect Flags
156156

157157
<!-- certkit:flags:connect -->
158-
| Flag | Default | Description |
159-
| -------------- | ------- | -------------------------------------------- |
160-
| `--crl` | `false` | Check CRL distribution points for revocation |
161-
| `--format` | `text` | Output format: text, json |
162-
| `--no-ocsp` | `false` | Disable automatic OCSP revocation check |
163-
| `--servername` | | Override SNI hostname (defaults to host) |
158+
| Flag | Default | Description |
159+
| -------------- | ------- | ----------------------------------------------------------- |
160+
| `--ciphers` | `false` | Enumerate all supported cipher suites with security ratings |
161+
| `--crl` | `false` | Check CRL distribution points for revocation |
162+
| `--format` | `text` | Output format: text, json |
163+
| `--no-ocsp` | `false` | Disable automatic OCSP revocation check |
164+
| `--servername` | | Override SNI hostname (defaults to host) |
164165
<!-- /certkit:flags -->
165166

166167
Port defaults to 443 if not specified. OCSP revocation status is checked automatically (best-effort); use `--no-ocsp` to disable. Use `--verbose` for extended details (serial, key info, signature algorithm, key usage, EKU).

0 commit comments

Comments
 (0)