Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
48 changes: 27 additions & 21 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -38,10 +38,10 @@

```
$ tlsctl client github.com
github.com (secure, expires in 52 days) ✓
github.com (secure, expires in 84 days) ✓
Subject: CN=github.com
Issuer: CN=Sectigo Public Server Authentication CA DV E36,O=Sectigo Limited,C=GB
Validity: 2026-01-06 → 2026-04-05
Validity: 2026-03-06 → 2026-06-03
SANs: github.com, www.github.com

Chain: github.com → Sectigo Public Server Authentication CA DV E36 → Sectigo Public Server Authentication Root E46 (3 certificates)
Expand Down Expand Up @@ -129,7 +129,7 @@ tlsctl pem cert.pem

```
$ tlsctl client badssl.com
*.badssl.com (secure, expires in 66 days) ✓
*.badssl.com (secure, expires in 40 days) ✓
Subject: CN=*.badssl.com
Issuer: CN=R13,O=Let's Encrypt,C=US
Validity: 2026-01-20 → 2026-04-20
Expand All @@ -142,7 +142,7 @@ $ tlsctl client badssl.com

```
$ tlsctl client expired.badssl.com
*.badssl.com (insecure, certificate expired, expires in -3958 days) ✗
*.badssl.com (insecure, certificate expired, expired 3985 days ago) ✗
Subject: CN=*.badssl.com,OU=Domain Control Validated+OU=PositiveSSL Wildcard
Issuer: CN=COMODO RSA Domain Validation Secure Server CA,O=COMODO CA Limited,L=Salford,ST=Greater Manchester,C=GB
Validity: 2015-04-09 → 2015-04-12
Expand All @@ -155,7 +155,7 @@ $ tlsctl client expired.badssl.com

```
$ tlsctl client wrong.host.badssl.com
*.badssl.com (insecure, hostname mismatch, expires in 66 days) ✗
*.badssl.com (insecure, hostname mismatch, expires in 40 days) ✗
Subject: CN=*.badssl.com
Issuer: CN=R13,O=Let's Encrypt,C=US
Validity: 2026-01-20 → 2026-04-20
Expand All @@ -168,21 +168,21 @@ $ tlsctl client wrong.host.badssl.com

```
$ tlsctl client self-signed.badssl.com
*.badssl.com (insecure, unknown authority, expires in 727 days) ✗
*.badssl.com (insecure, unknown authority, expires in 729 days) ✗
Subject: CN=*.badssl.com,O=BadSSL,L=San Francisco,ST=California,C=US
Issuer: CN=*.badssl.com,O=BadSSL,L=San Francisco,ST=California,C=US
Validity: 2026-02-10 → 2028-02-10
Validity: 2026-03-10 → 2028-03-09
SANs: *.badssl.com, badssl.com
```

### Detecting untrusted root CAs

```
$ tlsctl client untrusted-root.badssl.com
*.badssl.com (insecure, unknown authority, expires in 727 days) ✗
*.badssl.com (insecure, unknown authority, expires in 729 days) ✗
Subject: CN=*.badssl.com,O=BadSSL,L=San Francisco,ST=California,C=US
Issuer: CN=BadSSL Untrusted Root Certificate Authority,O=BadSSL,L=San Francisco,ST=California,C=US
Validity: 2026-02-10 → 2028-02-10
Validity: 2026-03-10 → 2028-03-09
SANs: *.badssl.com, badssl.com

Chain: *.badssl.com → BadSSL Untrusted Root Certificate Authority (2 certificates)
Expand All @@ -192,7 +192,7 @@ $ tlsctl client untrusted-root.badssl.com

```
$ tlsctl client incomplete-chain.badssl.com
*.badssl.com (insecure, unknown authority, expires in 66 days) ✗
*.badssl.com (insecure, incomplete chain, expires in 39 days) ✗
Subject: CN=*.badssl.com
Issuer: CN=R13,O=Let's Encrypt,C=US
Validity: 2026-01-20 → 2026-04-20
Expand All @@ -203,24 +203,24 @@ $ tlsctl client incomplete-chain.badssl.com

```
$ tlsctl client --revocation crl revoked.badssl.com
revoked.badssl.com (secure, expires in 52 days) ✓
revoked.badssl.com (secure, expires in 89 days) ✓
Subject: CN=revoked.badssl.com
Issuer: CN=E7,O=Let's Encrypt,C=US
Validity: 2026-01-06 → 2026-04-06
Issuer: CN=E8,O=Let's Encrypt,C=US
Validity: 2026-03-10 → 2026-06-08
SANs: revoked.badssl.com
Revocation: REVOKED (CRL)

Chain: revoked.badssl.com → E7 (2 certificates)
Chain: revoked.badssl.com → E8 (2 certificates)
```

### Revocation checking with OCSP

```
$ tlsctl client --revocation ocsp google.com
*.google.com (secure, expires in 59 days) ✓
*.google.com (secure, expires in 46 days) ✓
Subject: CN=*.google.com
Issuer: CN=WR2,O=Google Trust Services,C=US
Validity: 2026-01-19 → 2026-04-13
Validity: 2026-02-02 → 2026-04-27
SANs: *.google.com, *.appengine.google.com, *.bdn.dev, *.origin-test.bdn.dev, *.cloud.google.com (+132 more)
Revocation: not revoked (OCSP)

Expand All @@ -233,18 +233,24 @@ Use `--tls-versions` to probe supported TLS versions and enumerate server-side c

```
$ tlsctl client --tls-versions badssl.com
*.badssl.com (secure, expires in 66 days) ✓
*.badssl.com (secure, expires in 40 days) ✓
Subject: CN=*.badssl.com
Issuer: CN=R13,O=Let's Encrypt,C=US
Validity: 2026-01-20 → 2026-04-20
SANs: *.badssl.com, badssl.com
TLS: TLS 1.2, TLS 1.3
TLS: TLS 1.0, TLS 1.1, TLS 1.2
Ciphers (TLS 1.0):
TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA
TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA
...
Ciphers (TLS 1.1):
TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA
TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA
...
Ciphers (TLS 1.2):
TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256
TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384
TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305_SHA256
Ciphers (TLS 1.3):
TLS_AES_256_GCM_SHA384
...

Chain: *.badssl.com → R13 (2 certificates)
```
Expand Down
3 changes: 0 additions & 3 deletions cmd/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,6 @@ func newClientCmd(rt *Runtime) *cobra.Command {
var tlsVersions bool
var serverName string
var startTLS string
var insecure bool
var rf revocationFlags

cmd := &cobra.Command{
Expand Down Expand Up @@ -90,7 +89,6 @@ func newClientCmd(rt *Runtime) *cobra.Command {
TLSVersions: tlsVersions,
ServerName: serverName,
StartTLS: startTLS,
Insecure: insecure,
}

now := rt.NowFunc()
Expand Down Expand Up @@ -141,7 +139,6 @@ func newClientCmd(rt *Runtime) *cobra.Command {
cmd.Flags().BoolVar(&tlsVersions, "tls-versions", false, "Probe and display supported TLS versions")
cmd.Flags().StringVar(&serverName, "servername", "", "Override the SNI server name sent in the TLS handshake")
cmd.Flags().StringVar(&startTLS, "starttls", "", "Use STARTTLS for the given protocol: "+tlsquery.StartTLSProtocolList())
cmd.Flags().BoolVarP(&insecure, "insecure", "k", false, "Skip TLS certificate verification")
addRevocationFlags(cmd, &rf)
addCertFlags(cmd)

Expand Down
4 changes: 0 additions & 4 deletions internal/config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,6 @@ type ClientSettings struct {
TLSVersions *bool `json:"tls-versions,omitempty"`
ServerName *string `json:"servername,omitempty"`
StartTLS *string `json:"starttls,omitempty"`
Insecure *bool `json:"insecure,omitempty"`
Revocation *string `json:"revocation,omitempty"`
RevocationTimeout *Duration `json:"revocation-timeout,omitempty"`
RevocationSoftFail *bool `json:"revocation-soft-fail,omitempty"`
Expand Down Expand Up @@ -213,9 +212,6 @@ func addClientFlags(vals map[string]string, c *ClientSettings) {
if c.StartTLS != nil {
vals["starttls"] = *c.StartTLS
}
if c.Insecure != nil {
vals["insecure"] = boolStr(*c.Insecure)
}
if c.Revocation != nil {
vals["revocation"] = *c.Revocation
}
Expand Down
1 change: 0 additions & 1 deletion internal/config/config_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -128,7 +128,6 @@ func TestLoad_ValidConfig(t *testing.T) {
"revocation": "ocsp",
"revocation-timeout": "10s",
"revocation-soft-fail": false,
"insecure": false,
"servername": "example.com",
"starttls": "smtp",
"cacert": "/etc/ssl/ca.pem",
Expand Down
2 changes: 1 addition & 1 deletion internal/tlsquery/pem.go
Original file line number Diff line number Diff line change
Expand Up @@ -89,7 +89,7 @@ func verifyPEMChain(chain *ChainInfo, certs []*x509.Certificate, opts PEMOptions

if _, err := leaf.Verify(verifyOpts); err != nil {
chain.Verified = false
chain.VerificationError = abbreviateVerifyError(err)
chain.VerificationError = abbreviateVerifyErrorWithChain(err, certs)
} else {
chain.Verified = true
}
Expand Down
45 changes: 37 additions & 8 deletions internal/tlsquery/query.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package tlsquery

import (
"bytes"
"crypto/tls"
"crypto/x509"
"errors"
Expand Down Expand Up @@ -30,27 +31,29 @@ func Query(endpoint string, opts QueryOptions) (*ChainInfo, error) {
return nil, fmt.Errorf("invalid proxy configuration: %w", err)
}

if opts.Insecure {
certs, err := dialAndHandshake(endpoint, proxyURL, config, startTLS)
if err != nil {
if !isVerificationError(err) {
return nil, fmt.Errorf("TLS handshake failed: %w", err)
}
verifyErr := err

insecureConfig := config.Clone()
insecureConfig.InsecureSkipVerify = true
certs, err := dialAndHandshake(endpoint, proxyURL, insecureConfig, startTLS)
certs, err = dialAndHandshake(endpoint, proxyURL, insecureConfig, startTLS)
if err != nil {
return nil, fmt.Errorf("TLS handshake failed: %w", err)
}

chain := buildChain(certs)
chain.Verified = false
chain.VerificationError = "verification skipped (--insecure)"
chain.VerificationError = abbreviateVerifyErrorWithChain(verifyErr, certs)
if probeVersions {
chain.TLSVersions = probeTLSVersions(endpoint, proxyURL, config, true, startTLS)
}
return chain, nil
}

certs, err := dialAndHandshake(endpoint, proxyURL, config, startTLS)
if err != nil {
return nil, fmt.Errorf("TLS handshake failed: %w", err)
}

chain := buildChain(certs)
chain.Verified = true
if probeVersions {
Expand Down Expand Up @@ -94,6 +97,32 @@ func buildChain(certs []*x509.Certificate) *ChainInfo {
return chain
}

func isVerificationError(err error) bool {
var hostErr x509.HostnameError
var unknownAuth x509.UnknownAuthorityError
var certInvalid x509.CertificateInvalidError
var sysRoots x509.SystemRootsError
return errors.As(err, &hostErr) ||
errors.As(err, &unknownAuth) ||
errors.As(err, &certInvalid) ||
errors.As(err, &sysRoots)
}

func abbreviateVerifyErrorWithChain(err error, certs []*x509.Certificate) string {
var unknownAuth x509.UnknownAuthorityError
if errors.As(err, &unknownAuth) && len(certs) > 0 {
top := certs[len(certs)-1]
if !isSelfSigned(top) {
return "incomplete chain"
}
}
return abbreviateVerifyError(err)
}

func isSelfSigned(cert *x509.Certificate) bool {
return bytes.Equal(cert.RawIssuer, cert.RawSubject)
}

func abbreviateVerifyError(err error) string {
var hostErr x509.HostnameError
if errors.As(err, &hostErr) {
Expand Down
12 changes: 6 additions & 6 deletions internal/tlsquery/query_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ func TestQuery_ValidEndpoint(t *testing.T) {
server, addr := startTestTLSServer(t, false)
defer server.Close()

chain, err := Query(addr, QueryOptions{Insecure: true})
chain, err := Query(addr, QueryOptions{})
if err != nil {
t.Fatalf("Query failed: %v", err)
}
Expand All @@ -44,10 +44,10 @@ func TestQuery_ValidEndpoint(t *testing.T) {
t.Errorf("expected 2 SANs, got %d", len(leaf.SubjectAltNames))
}
if chain.Verified {
t.Error("expected Verified to be false with Insecure option")
t.Error("expected Verified to be false for self-signed test certificate")
}
if chain.VerificationError == "" {
t.Error("expected VerificationError to be set with Insecure option")
t.Error("expected VerificationError to be set for self-signed test certificate")
}
}

Expand Down Expand Up @@ -309,7 +309,7 @@ func TestQuery_ViaProxy(t *testing.T) {
proxyAddr, cleanup := startTestHTTPProxy(t, addr, false)
defer cleanup()

chain, err := Query(addr, QueryOptions{Proxy: "http://" + proxyAddr, Insecure: true})
chain, err := Query(addr, QueryOptions{Proxy: "http://" + proxyAddr})
if err != nil {
t.Fatalf("Query via proxy failed: %v", err)
}
Expand All @@ -329,7 +329,7 @@ func TestQuery_ViaProxyWithAuth(t *testing.T) {
proxyAddr, cleanup := startTestHTTPProxy(t, addr, true)
defer cleanup()

chain, err := Query(addr, QueryOptions{Proxy: "http://user:pass@" + proxyAddr, Insecure: true})
chain, err := Query(addr, QueryOptions{Proxy: "http://user:pass@" + proxyAddr})
if err != nil {
t.Fatalf("Query via proxy with auth failed: %v", err)
}
Expand All @@ -346,7 +346,7 @@ func TestQuery_ViaProxyAuthRequired(t *testing.T) {
proxyAddr, cleanup := startTestHTTPProxy(t, addr, true)
defer cleanup()

_, err := Query(addr, QueryOptions{Proxy: "http://" + proxyAddr, Insecure: true})
_, err := Query(addr, QueryOptions{Proxy: "http://" + proxyAddr})
if err == nil {
t.Error("expected error when proxy requires auth but none provided")
}
Expand Down
1 change: 0 additions & 1 deletion internal/tlsquery/types.go
Original file line number Diff line number Diff line change
Expand Up @@ -68,5 +68,4 @@ type QueryOptions struct {
TLSVersions bool // Probe and display supported TLS versions.
ServerName string // SNI override for TLS handshake (useful when connecting by IP).
StartTLS string // STARTTLS protocol: smtp, imap, pop3, ldap.
Insecure bool // Skip TLS certificate verification.
}
Loading