// 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 . // // 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 . 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 "" }