From aaa389164f8ca410e7e4e354d80c9f9068e19027 Mon Sep 17 00:00:00 2001 From: Ryan Fowler Date: Sun, 30 Aug 2026 20:15:57 +0000 Subject: [PATCH] fix(tls): report certificates not yet valid --- docs/advanced-features.md | 2 +- internal/tlsinspect/tlsinspect.go | 15 +++++++ internal/tlsinspect/tlsinspect_test.go | 61 +++++++++++++++++++++++++- 3 files changed, 76 insertions(+), 2 deletions(-) diff --git a/docs/advanced-features.md b/docs/advanced-features.md index 2e6346ff..2ca1c87f 100644 --- a/docs/advanced-features.md +++ b/docs/advanced-features.md @@ -368,7 +368,7 @@ fetch --inspect-tls example.com The default output uses the structured diagnostic view. It shows separate connection and certificate sections with the remote address, negotiated TLS, verification status, leaf certificate, issuer, exact validity window, serial number, public-key description, signature algorithm, SHA-256 fingerprint, server and verified chains, all parsed SAN types, OCSP status and timestamps, SCT count, SNI, resolver provenance, and ECH details. The `-v` flag has no effect in TLS inspection mode. Use `-vv` for AIA, CRL, policy, and other niche X.509 details. OCSP staple status checks the matching certificate and response signature, but not responder authorization or freshness. QUIC reports `cipher suite unavailable` only when the transport does not expose it. -Inspection completes the handshake even when certificate verification fails. It returns a nonzero status for a verification failure unless `--insecure` is explicit; `--insecure` reports the failure as ignored and returns success. Expiry is color-coded in the certificate view: red if expired or less than 7 days remaining, yellow if less than 30 days, green otherwise. The inspection result is written to stdout. Warnings and errors are written to stderr, so the result can be redirected or piped without diagnostic output. +Inspection completes the handshake even when certificate verification fails. It returns a nonzero status for a verification failure unless `--insecure` is explicit; `--insecure` reports the failure as ignored and returns success. Certificate validity is color-coded in the certificate view: red if the certificate is not yet valid, expired, or has less than 7 days remaining; yellow if it has less than 30 days remaining; green otherwise. The inspection result is written to stdout. Warnings and errors are written to stderr, so the result can be redirected or piped without diagnostic output. HTTP-only flags (e.g. `--data`, `--timing`, `--grpc`) are ignored with a warning when used with `--inspect-tls`. diff --git a/internal/tlsinspect/tlsinspect.go b/internal/tlsinspect/tlsinspect.go index 4c14f94b..2152bcad 100644 --- a/internal/tlsinspect/tlsinspect.go +++ b/internal/tlsinspect/tlsinspect.go @@ -741,6 +741,9 @@ func renderVerboseVerification(p *core.Printer, result *verificationResult, inse func certificateValidityStatus(cert *x509.Certificate) string { now := tlsInspectNow() + if now.Before(cert.NotBefore) { + return "not yet valid" + } if now.After(cert.NotAfter) { return "expired" } @@ -922,6 +925,18 @@ func certDisplayName(cert *x509.Certificate) string { func certExpiryInfo(cert *x509.Certificate) (string, core.Sequence) { now := tlsInspectNow() + if now.Before(cert.NotBefore) { + untilValid := cert.NotBefore.Sub(now) + days := int(untilValid.Hours() / 24) + switch days { + case 0: + return "valid in <1 day", core.Red + case 1: + return "valid in 1 day", core.Red + default: + return fmt.Sprintf("valid in %d days", days), core.Red + } + } if now.After(cert.NotAfter) { return "expired", core.Red } diff --git a/internal/tlsinspect/tlsinspect_test.go b/internal/tlsinspect/tlsinspect_test.go index ef4bbefd..5dfe94dd 100644 --- a/internal/tlsinspect/tlsinspect_test.go +++ b/internal/tlsinspect/tlsinspect_test.go @@ -397,10 +397,32 @@ func TestCertExpiryInfo(t *testing.T) { tests := []struct { name string + notBefore time.Time notAfter time.Time wantText string wantColor core.Sequence }{ + { + name: "not valid for less than 1 day", + notBefore: fixedNow.Add(12 * time.Hour), + notAfter: fixedNow.Add(30 * 24 * time.Hour), + wantText: "valid in <1 day", + wantColor: core.Red, + }, + { + name: "not valid for 1 day", + notBefore: fixedNow.Add(36 * time.Hour), + notAfter: fixedNow.Add(30 * 24 * time.Hour), + wantText: "valid in 1 day", + wantColor: core.Red, + }, + { + name: "not valid for multiple days", + notBefore: fixedNow.Add(3 * 24 * time.Hour), + notAfter: fixedNow.Add(30 * 24 * time.Hour), + wantText: "valid in 3 days", + wantColor: core.Red, + }, { name: "expired", notAfter: fixedNow.Add(-24 * time.Hour), @@ -459,7 +481,7 @@ func TestCertExpiryInfo(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - cert := &x509.Certificate{NotAfter: tt.notAfter} + cert := &x509.Certificate{NotBefore: tt.notBefore, NotAfter: tt.notAfter} gotText, gotColor := certExpiryInfo(cert) if gotText != tt.wantText { t.Errorf("certExpiryInfo() text = %q, want %q", gotText, tt.wantText) @@ -471,6 +493,43 @@ func TestCertExpiryInfo(t *testing.T) { } } +func TestCertificateValidityStatus(t *testing.T) { + fixedNow := time.Date(2025, 6, 1, 0, 0, 0, 0, time.UTC) + origNow := tlsInspectNow + tlsInspectNow = func() time.Time { return fixedNow } + t.Cleanup(func() { tlsInspectNow = origNow }) + + tests := []struct { + name string + cert *x509.Certificate + want string + }{ + { + name: "not yet valid", + cert: &x509.Certificate{NotBefore: fixedNow.Add(time.Hour), NotAfter: fixedNow.Add(24 * time.Hour)}, + want: "not yet valid", + }, + { + name: "expired", + cert: &x509.Certificate{NotBefore: fixedNow.Add(-48 * time.Hour), NotAfter: fixedNow.Add(-time.Hour)}, + want: "expired", + }, + { + name: "valid", + cert: &x509.Certificate{NotBefore: fixedNow.Add(-time.Hour), NotAfter: fixedNow.Add(36 * time.Hour)}, + want: "1 day remaining", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if got := certificateValidityStatus(tt.cert); got != tt.want { + t.Fatalf("certificateValidityStatus() = %q, want %q", got, tt.want) + } + }) + } +} + func TestRenderCertificateChain(t *testing.T) { fixedNow := time.Date(2025, 6, 1, 0, 0, 0, 0, time.UTC) origNow := tlsInspectNow