108 lines
3.5 KiB
Go
108 lines
3.5 KiB
Go
// This file is part of the happyDomain (R) project.
|
|
// Copyright (c) 2020-2026 happyDomain
|
|
// Authors: Pierre-Olivier Mercier, et al.
|
|
//
|
|
// This program is offered under a commercial and under the AGPL license.
|
|
// For commercial licensing, contact us at <contact@happydomain.org>.
|
|
//
|
|
// For AGPL licensing:
|
|
// This program is free software: you can redistribute it and/or modify
|
|
// it under the terms of the GNU Affero General Public License as published by
|
|
// the Free Software Foundation, either version 3 of the License, or
|
|
// (at your option) any later version.
|
|
//
|
|
// This program is distributed in the hope that it will be useful,
|
|
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
// GNU Affero General Public License for more details.
|
|
//
|
|
// You should have received a copy of the GNU Affero General Public License
|
|
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|
|
|
package checker
|
|
|
|
import (
|
|
"context"
|
|
"crypto/tls"
|
|
"crypto/x509"
|
|
"fmt"
|
|
"net"
|
|
"strconv"
|
|
"strings"
|
|
"time"
|
|
)
|
|
|
|
// FetchChain dials host:port, optionally upgrades the connection via STARTTLS,
|
|
// completes a TLS handshake (InsecureSkipVerify so callers receive the chain
|
|
// even when PKIX would reject it), and returns the peer certificates leaf
|
|
// first.
|
|
//
|
|
// starttls is the protocol name as registered (smtp, submission, imap, pop3,
|
|
// ldap, xmpp-client, ...); pass "" for direct TLS. AutoSTARTTLS provides
|
|
// well-known port defaults.
|
|
func FetchChain(ctx context.Context, host string, port uint16, starttls string, timeout time.Duration) ([]*x509.Certificate, error) {
|
|
host = strings.TrimSuffix(host, ".")
|
|
addr := net.JoinHostPort(host, strconv.Itoa(int(port)))
|
|
|
|
dialCtx, cancel := context.WithTimeout(ctx, timeout)
|
|
defer cancel()
|
|
|
|
conn, err := (&net.Dialer{}).DialContext(dialCtx, "tcp", addr)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("dial %s: %w", addr, err)
|
|
}
|
|
defer conn.Close()
|
|
if dl, ok := dialCtx.Deadline(); ok {
|
|
_ = conn.SetDeadline(dl)
|
|
}
|
|
|
|
if starttls != "" {
|
|
up, ok := starttlsUpgraders[starttls]
|
|
if !ok {
|
|
return nil, fmt.Errorf("unsupported starttls protocol %q", starttls)
|
|
}
|
|
if err := up(conn, host); err != nil {
|
|
return nil, fmt.Errorf("starttls-%s: %w", starttls, err)
|
|
}
|
|
}
|
|
|
|
tlsConn := tls.Client(conn, &tls.Config{
|
|
ServerName: host,
|
|
InsecureSkipVerify: true, // #nosec G402 -- intentional: caller receives the chain even when PKIX rejects it
|
|
})
|
|
if err := tlsConn.HandshakeContext(dialCtx); err != nil {
|
|
return nil, fmt.Errorf("tls handshake: %w", err)
|
|
}
|
|
state := tlsConn.ConnectionState()
|
|
if len(state.PeerCertificates) == 0 {
|
|
return nil, fmt.Errorf("server presented no certificate")
|
|
}
|
|
return state.PeerCertificates, nil
|
|
}
|
|
|
|
// BuildChain produces a CertInfo per peer certificate (leaf first), with the
|
|
// four (selector, matching_type) DANE hash pairs precomputed. This is the
|
|
// same projection probe() applies internally; exported so HTTP handlers can
|
|
// reuse it without re-deriving the format.
|
|
func BuildChain(certs []*x509.Certificate) []CertInfo {
|
|
return buildChain(certs)
|
|
}
|
|
|
|
// AutoSTARTTLS maps a well-known port to the STARTTLS dialect FetchChain
|
|
// should drive. Returns "" when the port has no auto-mapping (caller should
|
|
// then use direct TLS or pass an explicit dialect).
|
|
func AutoSTARTTLS(port uint16) string {
|
|
switch port {
|
|
case 25, 587:
|
|
return "smtp"
|
|
case 143:
|
|
return "imap"
|
|
case 110:
|
|
return "pop3"
|
|
case 389:
|
|
return "ldap"
|
|
case 5222:
|
|
return "xmpp-client"
|
|
}
|
|
return ""
|
|
}
|