check_ssl_certificate - SMTP STARTTLS support

* Refactored code so that obtaining TLS certificates from server can
  have different implementations
* Added SMTP-specific implementation that will send a HELO followed
  by a STARTTLS.
This commit is contained in:
Emmanuel BENOîT 2021-02-19 16:53:34 +01:00
parent 55c4a7b3a5
commit 65239769a0

View file

@ -4,6 +4,8 @@ import (
"crypto/tls" "crypto/tls"
"crypto/x509" "crypto/x509"
"fmt" "fmt"
"net"
"net/textproto"
"os" "os"
"strings" "strings"
"time" "time"
@ -14,6 +16,72 @@ import (
"github.com/karrick/golf" "github.com/karrick/golf"
) )
//--------------------------------------------------------------------------------------------------------
// Interface that can be implemented to fetch TLS certificates.
type certGetter interface {
getCertificate(tlsConfig *tls.Config, address string) (*x509.Certificate, error)
}
// Full TLS certificate fetcher
type fullTLSGetter struct{}
func (f fullTLSGetter) getCertificate(tlsConfig *tls.Config, address string) (*x509.Certificate, error) {
conn, err := tls.Dial("tcp", address, tlsConfig)
if err != nil {
return nil, err
}
defer conn.Close()
if err := conn.Handshake(); err != nil {
return nil, err
}
return conn.ConnectionState().PeerCertificates[0], nil
}
// SMTP STARTTLS getter
type smtpGetter struct{}
func (f smtpGetter) cmd(tcon *textproto.Conn, expectCode int, text string) (int, string, error) {
id, err := tcon.Cmd("%s", text)
if err != nil {
return 0, "", err
}
tcon.StartResponse(id)
defer tcon.EndResponse(id)
return tcon.ReadResponse(expectCode)
}
func (f smtpGetter) getCertificate(tlsConfig *tls.Config, address string) (*x509.Certificate, error) {
conn, err := net.Dial("tcp", address)
if err != nil {
return nil, err
}
text := textproto.NewConn(conn)
defer text.Close()
if _, _, err := text.ReadResponse(220); err != nil {
return nil, err
}
if _, _, err := f.cmd(text, 250, "HELO localhost"); err != nil {
return nil, err
}
if _, _, err := f.cmd(text, 220, "STARTTLS"); err != nil {
return nil, err
}
t := tls.Client(conn, tlsConfig)
if err := t.Handshake(); err != nil {
return nil, err
}
return t.ConnectionState().PeerCertificates[0], nil
}
// Supported StartTLS protocols
var certGetters map[string]certGetter = map[string]certGetter{
"": fullTLSGetter{},
"smtp": &smtpGetter{},
}
//--------------------------------------------------------------------------------------------------------
// Command line flags that have been parsed. // Command line flags that have been parsed.
type programFlags struct { type programFlags struct {
hostname string // Main host name to connect to hostname string // Main host name to connect to
@ -22,12 +90,14 @@ type programFlags struct {
crit int // Threshold for critical state (days) crit int // Threshold for critical state (days)
ignoreCnOnly bool // Do not warn about SAN-less certificates ignoreCnOnly bool // Do not warn about SAN-less certificates
extraNames []string // Extra names the certificate should include extraNames []string // Extra names the certificate should include
startTLS string // Protocol to use before requesting a switch to TLS.
} }
// Program data including configuration and runtime data. // Program data including configuration and runtime data.
type checkProgram struct { type checkProgram struct {
programFlags // Flags from the command line programFlags // Flags from the command line
plugin *plugin.Plugin // Plugin output state plugin *plugin.Plugin // Plugin output state
getter certGetter // Certificate getter
certificate *x509.Certificate // X.509 certificate from the server certificate *x509.Certificate // X.509 certificate from the server
} }
@ -49,6 +119,8 @@ func (flags *programFlags) parseArguments() {
"Do not issue warnings regarding certificates that do not use SANs at all.") "Do not issue warnings regarding certificates that do not use SANs at all.")
golf.StringVarP(&names, 'a', "additional-names", "", golf.StringVarP(&names, 'a', "additional-names", "",
"A comma-separated list of names that the certificate should also provide.") "A comma-separated list of names that the certificate should also provide.")
golf.StringVarP(&flags.startTLS, 's', "start-tls", "",
"Protocol to use before requesting a switch to TLS. Supported protocols: smtp.")
golf.Parse() golf.Parse()
if help { if help {
golf.Usage() golf.Usage()
@ -90,6 +162,11 @@ func (program *checkProgram) checkFlags() bool {
program.plugin.SetState(plugin.UNKNOWN, "nonsensical thresholds") program.plugin.SetState(plugin.UNKNOWN, "nonsensical thresholds")
return false return false
} }
if _, ok := certGetters[program.startTLS]; !ok {
errstr := fmt.Sprintf("unsupported StartTLS protocol %s", program.startTLS)
program.plugin.SetState(plugin.UNKNOWN, errstr)
return false
}
program.hostname = strings.ToLower(program.hostname) program.hostname = strings.ToLower(program.hostname)
return true return true
} }
@ -102,18 +179,13 @@ func (program *checkProgram) getCertificate() error {
MinVersion: tls.VersionTLS10, MinVersion: tls.VersionTLS10,
} }
connString := fmt.Sprintf("%s:%d", program.hostname, program.port) connString := fmt.Sprintf("%s:%d", program.hostname, program.port)
conn, err := tls.Dial("tcp", connString, tlsConfig) certificate, err := certGetters[program.startTLS].getCertificate(tlsConfig, connString)
if err != nil { program.certificate = certificate
return fmt.Errorf("connection failed: %s", err.Error()) return err
}
defer conn.Close()
if err := conn.Handshake(); err != nil {
return fmt.Errorf("handshake failed: %s", err.Error())
}
program.certificate = conn.ConnectionState().PeerCertificates[0]
return nil
} }
// Check that the CN of a certificate that doesn't contain a SAN actually
// matches the requested host name.
func (program *checkProgram) checkSANlessCertificate() bool { func (program *checkProgram) checkSANlessCertificate() bool {
if !program.ignoreCnOnly || len(program.extraNames) != 0 { if !program.ignoreCnOnly || len(program.extraNames) != 0 {
program.plugin.SetState(plugin.WARNING, program.plugin.SetState(plugin.WARNING,