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:
parent
55c4a7b3a5
commit
65239769a0
1 changed files with 82 additions and 10 deletions
|
@ -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,
|
||||||
|
|
Loading…
Reference in a new issue