Codebase list certgraph / run/d960c48e-a500-4dea-b2c1-f3dce5bee045/upstream driver / smtp / smtp.go
run/d960c48e-a500-4dea-b2c1-f3dce5bee045/upstream

Tree @run/d960c48e-a500-4dea-b2c1-f3dce5bee045/upstream (Download .tar.gz)

smtp.go @run/d960c48e-a500-4dea-b2c1-f3dce5bee045/upstreamraw · history · blame

// Package smtp implements a certgraph driver for obtaining SSL certificates over smtp with STARTTLS
package smtp

import (
	"context"
	"crypto/tls"
	"crypto/x509"
	"fmt"
	"net"
	"net/smtp"
	"path"
	"strings"
	"time"

	"github.com/lanrat/certgraph/driver"
	"github.com/lanrat/certgraph/fingerprint"
	"github.com/lanrat/certgraph/status"
)

const driverName = "smtp"

func init() {
	driver.AddDriver(driverName)
}

type smtpDriver struct {
	port      string
	save      bool
	savePath  string
	tlsConfig *tls.Config
	timeout   time.Duration
}

type smtpCertDriver struct {
	host         string
	fingerprints driver.FingerprintMap
	status       status.Map
	mx           []string
	certs        map[fingerprint.Fingerprint]*driver.CertResult
}

func (c *smtpCertDriver) GetFingerprints() (driver.FingerprintMap, error) {
	return c.fingerprints, nil
}

func (c *smtpCertDriver) GetStatus() status.Map {
	return c.status
}

func (c *smtpCertDriver) GetRelated() ([]string, error) {
	return c.mx, nil
}

func (c *smtpCertDriver) QueryCert(fp fingerprint.Fingerprint) (*driver.CertResult, error) {
	cert, found := c.certs[fp]
	if found {
		return cert, nil
	}
	return nil, fmt.Errorf("certificate with Fingerprint %s not found", fp.HexString())
}

// Driver creates a new SSL driver for SMTP Connections
func Driver(timeout time.Duration, savePath string) (driver.Driver, error) {
	d := new(smtpDriver)
	d.port = "25"
	if len(savePath) > 0 {
		d.save = true
		d.savePath = savePath
	}
	d.tlsConfig = &tls.Config{
		InsecureSkipVerify: true,
	}
	d.timeout = timeout

	return d, nil
}

func (d *smtpDriver) GetName() string {
	return driverName
}

func (d *smtpDriver) smtpGetCerts(host string) ([]*x509.Certificate, error) {
	var certs []*x509.Certificate
	addr := net.JoinHostPort(host, d.port)
	dialer := &net.Dialer{Timeout: d.timeout}

	conn, err := dialer.Dial("tcp", addr)
	if err != nil {
		return certs, err
	}
	defer conn.Close()
	smtp, err := smtp.NewClient(conn, host)
	if err != nil {
		return certs, err
	}
	err = smtp.StartTLS(d.tlsConfig)
	if err != nil {
		return certs, err
	}
	connState, ok := smtp.TLSConnectionState()
	if !ok {
		return certs, err
	}
	return connState.PeerCertificates, err
}

// QueryDomain gets the certificates found for a given domain
func (d *smtpDriver) QueryDomain(host string) (driver.Result, error) {
	results := &smtpCertDriver{
		host:         host,
		status:       make(status.Map),
		fingerprints: make(driver.FingerprintMap),
		certs:        make(map[fingerprint.Fingerprint]*driver.CertResult),
	}

	// get related in different query
	results.mx, _ = d.getMX(host)

	certs, err := d.smtpGetCerts(host)
	smtpStatus := status.CheckNetErr(err)
	metaStatus := ""
	if len(results.mx) > 0 {
		metaStatus = fmt.Sprintf("MX(%s)", strings.Join(results.mx, " "))
	}
	results.status.Set(host, status.NewMeta(smtpStatus, metaStatus))

	if smtpStatus != status.GOOD {
		return results, nil
	}

	// only look at leaf certificate which is valid for domain, rest of cert chain is ignored
	certResult := driver.NewCertResult(certs[0])
	results.certs[certResult.Fingerprint] = certResult
	results.fingerprints.Add(host, certResult.Fingerprint)

	// save
	if d.save && len(certs) > 0 {
		err = driver.CertsToPEMFile(certs, path.Join(d.savePath, certResult.Fingerprint.HexString())+".pem")
	}

	return results, err
}

// getMX returns the MX records for the provided domain
func (d *smtpDriver) getMX(domain string) ([]string, error) {
	domains := make([]string, 0, 5)
	ctx, cancel := context.WithTimeout(context.Background(), d.timeout)
	defer cancel()
	mx, err := net.DefaultResolver.LookupMX(ctx, domain)
	if err != nil {
		return domains, err
	}
	for _, v := range mx {
		domains = append(domains, strings.TrimSuffix(v.Host, "."))
	}
	return domains, nil
}