diff --git a/checker/fetch.go b/checker/fetch.go new file mode 100644 index 0000000..1cdd816 --- /dev/null +++ b/checker/fetch.go @@ -0,0 +1,108 @@ +// 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, + }) + 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 "" +}