diff --git a/LICENSE b/LICENSE index 48d09b5..58582dc 100644 --- a/LICENSE +++ b/LICENSE @@ -48,7 +48,7 @@ "Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally - submitted to the Licensor for inclusion in the Work by the copyright owner + submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent @@ -60,7 +60,7 @@ designated in writing by the copyright owner as "Not a Contribution." "Contributor" shall mean Licensor and any individual or Legal Entity - on behalf of whom a Contribution has been received by the Licensor and + on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work. 2. Grant of Copyright License. Subject to the terms and conditions of @@ -106,7 +106,7 @@ (d) If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained - within such NOTICE file, excluding any notices that do not + within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or diff --git a/README.md b/README.md index 40ab9bc..55fcd3f 100644 --- a/README.md +++ b/README.md @@ -52,6 +52,8 @@ tlsc ca export # print PEM to stdout tlsc ca export | ssh remote "cat > /etc/ssl/ca.pem" # pipe anywhere ``` +> **Note:** The CA private key (`rootCA-key.pem`) is stored as unencrypted PEM, which is standard for local development CAs. Do not use the generated CA for production environments. + ### Generate certificates Output defaults to `~/.tlsc/certs/`. diff --git a/internal/adapters/http/hsts.go b/internal/adapters/http/hsts.go index c954260..4a4ecf3 100644 --- a/internal/adapters/http/hsts.go +++ b/internal/adapters/http/hsts.go @@ -16,6 +16,11 @@ import ( var maxAgeRe = regexp.MustCompile(`max-age=(\d+)`) +// FetchHSTS retrieves the Strict-Transport-Security header for the given host. +// InsecureSkipVerify is enabled because this is an inspection tool — the caller +// validates the certificate separately. Note that a MITM attacker could strip +// the HSTS header in this configuration; the result should be treated as +// informational rather than a security guarantee. func FetchHSTS(ctx context.Context, host string, port int, timeout time.Duration, serverName string) *domain.HSTS { client := &http.Client{ Timeout: timeout, diff --git a/internal/adapters/tls/connect.go b/internal/adapters/tls/connect.go index 7b6656c..e8aeaa6 100644 --- a/internal/adapters/tls/connect.go +++ b/internal/adapters/tls/connect.go @@ -10,6 +10,7 @@ import ( "html" "io" "net" + "strings" "time" "github.com/dyaa/tlsc/internal/domain" @@ -115,7 +116,7 @@ func smtpHandshake(conn net.Conn, scanner *bufio.Scanner, _ string) error { return err } fmt.Fprintf(conn, "STARTTLS\r\n") - return readLine(scanner) + return readLineExpect(scanner, "220") } func imapHandshake(conn net.Conn, scanner *bufio.Scanner, _ string) error { @@ -123,7 +124,7 @@ func imapHandshake(conn net.Conn, scanner *bufio.Scanner, _ string) error { return err } fmt.Fprintf(conn, "a001 STARTTLS\r\n") - return readLine(scanner) + return readLineExpect(scanner, "a001 OK") } func pop3Handshake(conn net.Conn, scanner *bufio.Scanner, _ string) error { @@ -131,7 +132,7 @@ func pop3Handshake(conn net.Conn, scanner *bufio.Scanner, _ string) error { return err } fmt.Fprintf(conn, "STLS\r\n") - return readLine(scanner) + return readLineExpect(scanner, "+OK") } func ftpHandshake(conn net.Conn, scanner *bufio.Scanner, _ string) error { @@ -139,7 +140,7 @@ func ftpHandshake(conn net.Conn, scanner *bufio.Scanner, _ string) error { return err } fmt.Fprintf(conn, "AUTH TLS\r\n") - return readLine(scanner) + return readLineExpect(scanner, "234") } func ldapHandshake(conn net.Conn, _ *bufio.Scanner, _ string) error { @@ -258,9 +259,13 @@ func xmppHandshake(conn net.Conn, _ *bufio.Scanner, serverName string) error { fmt.Fprintf(conn, "") resp := make([]byte, 4096) - if _, err := io.ReadAtLeast(conn, resp, 1); err != nil { + n, err := io.ReadAtLeast(conn, resp, 1) + if err != nil { return fmt.Errorf("failed to read XMPP STARTTLS response: %w", err) } + if !strings.Contains(string(resp[:n]), "") + server.Read(buf) // starttls request + fmt.Fprint(server, "") + server.Close() + }() + + err := xmppHandshake(client, nil, "example.com") + if err == nil { + t.Error("expected error when XMPP rejects STARTTLS") + } +} + +func TestXmppHandshakeAccept(t *testing.T) { + client, server := net.Pipe() + defer client.Close() + defer server.Close() + + go func() { + buf := make([]byte, 4096) + server.Read(buf) + fmt.Fprint(server, "") + server.Read(buf) + fmt.Fprint(server, "") + server.Close() + }() + + err := xmppHandshake(client, nil, "example.com") + if err != nil { + t.Errorf("unexpected error: %v", err) + } +} diff --git a/internal/crypto/keys.go b/internal/crypto/keys.go index 09f9e32..7ad3c3d 100644 --- a/internal/crypto/keys.go +++ b/internal/crypto/keys.go @@ -52,6 +52,8 @@ func KeyBits(pub crypto.PublicKey) int { return 0 } switch k := pub.(type) { + case ed25519.PublicKey: + return 256 case interface{ Size() int }: return k.Size() * 8 case interface{ Params() *elliptic.CurveParams }: diff --git a/internal/crypto/keys_test.go b/internal/crypto/keys_test.go index 60842cb..2c52dff 100644 --- a/internal/crypto/keys_test.go +++ b/internal/crypto/keys_test.go @@ -133,6 +133,11 @@ func TestKeyBits(t *testing.T) { t.Errorf("expected 2048 bits for RSA-2048, got %d", bits) } + ed25519Key, _ := GenerateKey("ed25519") + if bits := KeyBits(ed25519Key.Public()); bits != 256 { + t.Errorf("expected 256 bits for Ed25519, got %d", bits) + } + if bits := KeyBits(nil); bits != 0 { t.Errorf("expected 0 bits for nil key, got %d", bits) } diff --git a/internal/domain/models.go b/internal/domain/models.go index a3b1a17..f8cec70 100644 --- a/internal/domain/models.go +++ b/internal/domain/models.go @@ -126,6 +126,10 @@ type SANs struct { // Result holds the full inspection outcome for a single TLS certificate. type Result struct { + // Valid indicates whether the certificate passed validation. For remote checks + // (Check/CheckBatch), this reflects full chain and hostname verification. For + // local file inspection (InspectFile), this only checks that the certificate + // has not expired. For CSR inspection, this checks the request signature. Valid bool `json:"valid"` ValidationError string `json:"validationError,omitempty"` ValidFrom string `json:"validFrom"` diff --git a/internal/services/checker/inspect.go b/internal/services/checker/inspect.go index d80ef72..9796108 100644 --- a/internal/services/checker/inspect.go +++ b/internal/services/checker/inspect.go @@ -13,6 +13,8 @@ import ( "github.com/dyaa/tlsc/internal/fileutil" ) +// InspectFile reads a PEM-encoded certificate or CSR from path and returns +// inspection metadata. ctx is accepted for API consistency and future use. func (s *Service) InspectFile(ctx context.Context, path string) (*domain.Result, error) { data, err := fileutil.ReadLimited(path, fileutil.MaxCertFileSize) if err != nil { diff --git a/internal/services/checker/list.go b/internal/services/checker/list.go index fc87696..7430d44 100644 --- a/internal/services/checker/list.go +++ b/internal/services/checker/list.go @@ -15,6 +15,8 @@ import ( "github.com/dyaa/tlsc/internal/fileutil" ) +// ListCerts returns summaries of all PEM-encoded certificates in certsDir. +// ctx is accepted for API consistency and future use. func (s *Service) ListCerts(ctx context.Context, certsDir string) ([]domain.CertSummary, error) { if certsDir == "" { certsDir = domain.DefaultCertsPath() diff --git a/internal/services/checker/verify.go b/internal/services/checker/verify.go index c8fdbea..388a068 100644 --- a/internal/services/checker/verify.go +++ b/internal/services/checker/verify.go @@ -13,6 +13,8 @@ import ( "github.com/dyaa/tlsc/internal/fileutil" ) +// VerifyFile validates a PEM-encoded certificate against the system roots or +// a provided CA. ctx is accepted for API consistency and future use. func (s *Service) VerifyFile(ctx context.Context, certPath string, opts domain.VerifyOptions) (*domain.VerifyResult, error) { certPEM, err := fileutil.ReadLimited(certPath, fileutil.MaxCertFileSize) if err != nil { diff --git a/internal/services/convert/convert.go b/internal/services/convert/convert.go index 76baa6f..b310472 100644 --- a/internal/services/convert/convert.go +++ b/internal/services/convert/convert.go @@ -17,6 +17,8 @@ type Service struct{} func New() *Service { return &Service{} } +// Convert transforms a certificate between PEM and DER formats. +// ctx is accepted for API consistency and future use. func (s *Service) Convert(ctx context.Context, inputPath, outputPath, format string) error { data, err := fileutil.ReadLimited(inputPath, fileutil.MaxCertFileSize) if err != nil {