// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: MPL-2.0

package pki

import (
	"bytes"
	"context"
	"crypto/sha256"
	"crypto/subtle"
	"crypto/tls"
	"crypto/x509"
	"encoding/asn1"
	"encoding/base64"
	"fmt"
	"io"
	"net"
	"net/http"
	"strings"
	"time"
)

const (
	DNSChallengePrefix = "_acme-challenge."
	ALPNProtocol       = "acme-tls/1"
)

// While this should be a constant, there's no way to do a low-level test of
// ValidateTLSALPN01Challenge without spinning up a complicated Docker
// instance to build a custom responder. Because we already have a local
// toolchain, it is far easier to drive this through Go tests with a custom
// (high) port, rather than requiring permission to bind to port 443 (root-run
// tests are even worse).
var ALPNPort = "443"

// OID of the acmeIdentifier X.509 Certificate Extension.
var OIDACMEIdentifier = asn1.ObjectIdentifier{1, 3, 6, 1, 5, 5, 7, 1, 31}

// ValidateKeyAuthorization validates that the given keyAuthz from a challenge
// matches our expectation, returning (true, nil) if so, or (false, err) if
// not.
func ValidateKeyAuthorization(keyAuthz string, token string, thumbprint string) (bool, error) {
	parts := strings.Split(keyAuthz, ".")
	if len(parts) != 2 {
		return false, fmt.Errorf("invalid authorization: got %v parts, expected 2", len(parts))
	}

	tokenPart := parts[0]
	thumbprintPart := parts[1]

	if token != tokenPart || thumbprint != thumbprintPart {
		return false, fmt.Errorf("key authorization was invalid")
	}

	return true, nil
}

// ValidateSHA256KeyAuthorization validates that the given keyAuthz from a
// challenge matches our expectation, returning (true, nil) if so, or
// (false, err) if not.
//
// This is for use with DNS challenges, which require base64 encoding.
func ValidateSHA256KeyAuthorization(keyAuthz string, token string, thumbprint string) (bool, error) {
	authzContents := token + "." + thumbprint
	checksum := sha256.Sum256([]byte(authzContents))
	expectedAuthz := base64.RawURLEncoding.EncodeToString(checksum[:])

	if keyAuthz != expectedAuthz {
		return false, fmt.Errorf("sha256 key authorization was invalid")
	}

	return true, nil
}

// ValidateRawSHA256KeyAuthorization validates that the given keyAuthz from a
// challenge matches our expectation, returning (true, nil) if so, or
// (false, err) if not.
//
// This is for use with TLS challenges, which require the raw hash output.
func ValidateRawSHA256KeyAuthorization(keyAuthz []byte, token string, thumbprint string) (bool, error) {
	authzContents := token + "." + thumbprint
	expectedAuthz := sha256.Sum256([]byte(authzContents))

	if len(keyAuthz) != len(expectedAuthz) || subtle.ConstantTimeCompare(expectedAuthz[:], keyAuthz) != 1 {
		return false, fmt.Errorf("sha256 key authorization was invalid")
	}

	return true, nil
}

func buildResolver(config *acmeConfigEntry) (*net.Resolver, error) {
	if len(config.DNSResolver) == 0 {
		return net.DefaultResolver, nil
	}

	return &net.Resolver{
		PreferGo:     true,
		StrictErrors: false,
		Dial: func(ctx context.Context, network, address string) (net.Conn, error) {
			d := net.Dialer{
				Timeout: 10 * time.Second,
			}
			return d.DialContext(ctx, network, config.DNSResolver)
		},
	}, nil
}

func buildDialerConfig(config *acmeConfigEntry) (*net.Dialer, error) {
	resolver, err := buildResolver(config)
	if err != nil {
		return nil, fmt.Errorf("failed to build resolver: %w", err)
	}

	return &net.Dialer{
		Timeout:   10 * time.Second,
		KeepAlive: -1 * time.Second,
		Resolver:  resolver,
	}, nil
}

// Validates a given ACME http-01 challenge against the specified domain,
// per RFC 8555.
//
// We attempt to be defensive here against timeouts, extra redirects, &c.
func ValidateHTTP01Challenge(domain string, token string, thumbprint string, config *acmeConfigEntry) (bool, error) {
	path := "http://" + domain + "/.well-known/acme-challenge/" + token
	dialer, err := buildDialerConfig(config)
	if err != nil {
		return false, fmt.Errorf("failed to build dialer: %w", err)
	}

	transport := &http.Transport{
		// Only a single request is sent to this server as we do not do any
		// batching of validation attempts. There is no need to do an HTTP
		// KeepAlive as a result.
		DisableKeepAlives:   true,
		MaxIdleConns:        1,
		MaxIdleConnsPerHost: 1,
		MaxConnsPerHost:     1,
		IdleConnTimeout:     1 * time.Second,
		TLSClientConfig:     &tls.Config{InsecureSkipVerify: true},

		// We'd rather timeout and re-attempt validation later than hang
		// too many validators waiting for slow hosts.
		DialContext:           dialer.DialContext,
		ResponseHeaderTimeout: 10 * time.Second,
	}

	maxRedirects := 10
	urlLength := 2000

	client := &http.Client{
		Transport: transport,
		CheckRedirect: func(req *http.Request, via []*http.Request) error {
			if len(via)+1 >= maxRedirects {
				return fmt.Errorf("http-01: too many redirects: %v", len(via)+1)
			}

			reqUrlLen := len(req.URL.String())
			if reqUrlLen > urlLength {
				return fmt.Errorf("http-01: redirect url length too long: %v", reqUrlLen)
			}

			return nil
		},
	}

	resp, err := client.Get(path)
	if err != nil {
		return false, fmt.Errorf("http-01: failed to fetch path %v: %w", path, err)
	}

	// We provision a buffer which allows for a variable size challenge, some
	// whitespace, and a detection gap for too long of a message.
	minExpected := len(token) + 1 + len(thumbprint)
	maxExpected := 512

	defer resp.Body.Close()

	// Attempt to read the body, but don't do so infinitely.
	body, err := io.ReadAll(io.LimitReader(resp.Body, int64(maxExpected+1)))
	if err != nil {
		return false, fmt.Errorf("http-01: unexpected error while reading body: %w", err)
	}

	if len(body) > maxExpected {
		return false, fmt.Errorf("http-01: response too large: received %v > %v bytes", len(body), maxExpected)
	}

	if len(body) < minExpected {
		return false, fmt.Errorf("http-01: response too small: received %v < %v bytes", len(body), minExpected)
	}

	// Per RFC 8555 Section 8.3. HTTP Challenge:
	//
	// > The server SHOULD ignore whitespace characters at the end of the body.
	keyAuthz := string(body)
	keyAuthz = strings.TrimSpace(keyAuthz)

	// If we got here, we got no non-EOF error while reading. Try to validate
	// the token because we're bounded by a reasonable amount of length.
	return ValidateKeyAuthorization(keyAuthz, token, thumbprint)
}

func ValidateDNS01Challenge(domain string, token string, thumbprint string, config *acmeConfigEntry) (bool, error) {
	// Here, domain is the value from the post-wildcard-processed identifier.
	// Per RFC 8555, no difference in validation occurs if a wildcard entry
	// is requested or if a non-wildcard entry is requested.
	//
	// XXX: In this case the DNS server is operator controlled and is assumed
	// to be less malicious so the default resolver is used. In the future,
	// we'll want to use net.Resolver for two reasons:
	//
	// 1. To control the actual resolver via ACME configuration,
	// 2. To use a context to set stricter timeout limits.
	resolver, err := buildResolver(config)
	if err != nil {
		return false, fmt.Errorf("failed to build resolver: %w", err)
	}

	ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
	defer cancel()

	name := DNSChallengePrefix + domain
	results, err := resolver.LookupTXT(ctx, name)
	if err != nil {
		return false, fmt.Errorf("dns-01: failed to lookup TXT records for domain (%v) via resolver %v: %w", name, config.DNSResolver, err)
	}

	for _, keyAuthz := range results {
		ok, _ := ValidateSHA256KeyAuthorization(keyAuthz, token, thumbprint)
		if ok {
			return true, nil
		}
	}

	return false, fmt.Errorf("dns-01: challenge failed against %v records", len(results))
}

func ValidateTLSALPN01Challenge(domain string, token string, thumbprint string, config *acmeConfigEntry) (bool, error) {
	// This RFC is defined in RFC 8737 Automated Certificate Management
	// Environment (ACME) TLS Application‑Layer Protocol Negotiation
	// (ALPN) Challenge Extension.
	//
	// This is conceptually similar to ValidateHTTP01Challenge, but
	// uses a TLS connection on port 443 with the specified ALPN
	// protocol.

	cfg := &tls.Config{
		// Per RFC 8737 Section 3. TLS with Application-Layer Protocol
		// Negotiation (TLS ALPN) Challenge, the name of the negotiated
		// protocol is "acme-tls/1".
		NextProtos: []string{ALPNProtocol},

		// Per RFC 8737 Section 3. TLS with Application-Layer Protocol
		// Negotiation (TLS ALPN) Challenge:
		//
		// > ... and an SNI extension containing only the domain name
		// > being validated during the TLS handshake.
		//
		// According to the Go docs, setting this option (even though
		// InsecureSkipVerify=true is also specified), allows us to
		// set the SNI extension to this value.
		ServerName: domain,

		VerifyConnection: func(connState tls.ConnectionState) error {
			// We initiated a fresh connection with no session tickets;
			// even if we did have a session ticket, we do not wish to
			// use it. Verify that the server has not inadvertently
			// reused connections between validation attempts or something.
			if connState.DidResume {
				return fmt.Errorf("server under test incorrectly reported that handshake was resumed when no session cache was provided; refusing to continue")
			}

			// Per RFC 8737 Section 3. TLS with Application-Layer Protocol
			// Negotiation (TLS ALPN) Challenge:
			//
			// > The ACME server verifies that during the TLS handshake the
			// > application-layer protocol "acme-tls/1" was successfully
			// > negotiated (and that the ALPN extension contained only the
			// > value "acme-tls/1").
			if connState.NegotiatedProtocol != ALPNProtocol {
				return fmt.Errorf("server under test negotiated unexpected ALPN protocol %v", connState.NegotiatedProtocol)
			}

			// Per RFC 8737 Section 3. TLS with Application-Layer Protocol
			// Negotiation (TLS ALPN) Challenge:
			//
			// > and that the certificate returned
			//
			// Because this certificate MUST be self-signed (per earlier
			// statement in RFC 8737 Section 3), there is no point in sending
			// more than one certificate, and so we will err early here if
			// we got more than one.
			if len(connState.PeerCertificates) > 1 {
				return fmt.Errorf("server under test returned multiple (%v) certificates when we expected only one", len(connState.PeerCertificates))
			}
			cert := connState.PeerCertificates[0]

			// Per RFC 8737 Section 3. TLS with Application-Layer Protocol
			// Negotiation (TLS ALPN) Challenge:
			//
			// > The client prepares for validation by constructing a
			// > self-signed certificate that MUST contain an acmeIdentifier
			// > extension and a subjectAlternativeName extension [RFC5280].
			//
			// Verify that this is a self-signed certificate that isn't signed
			// by another certificate (i.e., with the same key material but
			// different issuer).
			// NOTE: Do not use cert.CheckSignatureFrom(cert) as we need to bypass the
			//       checks for the parent certificate having the IsCA basic constraint set.
			err := cert.CheckSignature(cert.SignatureAlgorithm, cert.RawTBSCertificate, cert.Signature)
			if err != nil {
				return fmt.Errorf("server under test returned a non-self-signed certificate: %v", err)
			}

			if !bytes.Equal(cert.RawSubject, cert.RawIssuer) {
				return fmt.Errorf("server under test returned a non-self-signed certificate: invalid subject (%v) <-> issuer (%v) match", cert.Subject.String(), cert.Issuer.String())
			}

			// Per RFC 8737 Section 3. TLS with Application-Layer Protocol
			// Negotiation (TLS ALPN) Challenge:
			//
			// > The subjectAlternativeName extension MUST contain a single
			// > dNSName entry where the value is the domain name being
			// > validated.
			//
			// TODO: this does not validate that there are not other SANs
			// with unknown (to Go) OIDs.
			if len(cert.DNSNames) != 1 || len(cert.EmailAddresses) > 0 || len(cert.IPAddresses) > 0 || len(cert.URIs) > 0 {
				return fmt.Errorf("server under test returned a certificate with incorrect SANs")
			}

			// Per RFC 8737 Section 3. TLS with Application-Layer Protocol
			// Negotiation (TLS ALPN) Challenge:
			//
			// > The comparison of dNSNames MUST be case insensitive
			// > [RFC4343]. Note that as ACME doesn't support Unicode
			// > identifiers, all dNSNames MUST be encoded using the rules
			// > of [RFC3492].
			if !strings.EqualFold(cert.DNSNames[0], domain) {
				return fmt.Errorf("server under test returned a certificate with unexpected identifier: %v", cert.DNSNames[0])
			}

			// Per above, verify that the acmeIdentifier extension is present
			// exactly once and has the correct value.
			var foundACMEId bool
			for _, ext := range cert.Extensions {
				if !ext.Id.Equal(OIDACMEIdentifier) {
					continue
				}

				// There must be only a single ACME extension.
				if foundACMEId {
					return fmt.Errorf("server under test returned a certificate with multiple acmeIdentifier extensions")
				}
				foundACMEId = true

				// Per RFC 8737 Section 3. TLS with Application-Layer Protocol
				// Negotiation (TLS ALPN) Challenge:
				//
				// > a critical acmeIdentifier extension
				if !ext.Critical {
					return fmt.Errorf("server under test returned a certificate with an acmeIdentifier extension marked non-Critical")
				}

				var keyAuthz []byte
				remainder, err := asn1.Unmarshal(ext.Value, &keyAuthz)
				if err != nil {
					return fmt.Errorf("server under test returned a certificate with invalid acmeIdentifier extension value: %w", err)
				}
				if len(remainder) > 0 {
					return fmt.Errorf("server under test returned a certificate with invalid acmeIdentifier extension value with additional trailing data")
				}

				ok, err := ValidateRawSHA256KeyAuthorization(keyAuthz, token, thumbprint)
				if !ok || err != nil {
					return fmt.Errorf("server under test returned a certificate with an invalid key authorization (%w)", err)
				}
			}

			// Per RFC 8737 Section 3. TLS with Application-Layer Protocol
			// Negotiation (TLS ALPN) Challenge:
			//
			// > The ACME server verifies that ... the certificate returned
			// > contains: ... a critical acmeIdentifier extension containing
			// > the expected SHA-256 digest computed in step 1.
			if !foundACMEId {
				return fmt.Errorf("server under test returned a certificate without the required acmeIdentifier extension")
			}

			// Remove the handled critical extension and validate that we
			// have no additional critical extensions left unhandled.
			var index int = -1
			for oidIndex, oid := range cert.UnhandledCriticalExtensions {
				if oid.Equal(OIDACMEIdentifier) {
					index = oidIndex
					break
				}
			}
			if index != -1 {
				// Unlike the foundACMEId case, this is not a failure; if Go
				// updates to "understand" this critical extension, we do not
				// wish to fail.
				cert.UnhandledCriticalExtensions = append(cert.UnhandledCriticalExtensions[0:index], cert.UnhandledCriticalExtensions[index+1:]...)
			}
			if len(cert.UnhandledCriticalExtensions) > 0 {
				return fmt.Errorf("server under test returned a certificate with additional unknown critical extensions (%v)", cert.UnhandledCriticalExtensions)
			}

			// All good!
			return nil
		},

		// We never want to resume a connection; do not provide session
		// cache storage.
		ClientSessionCache: nil,

		// Do not trust any system trusted certificates; we're going to be
		// manually validating the chain, so specifying a non-empty pool
		// here could only cause additional, unnecessary work.
		RootCAs: x509.NewCertPool(),

		// Do not bother validating the client's chain; we know it should be
		// self-signed. This also disables hostname verification, but we do
		// this verification as part of VerifyConnection(...) ourselves.
		//
		// Per Go docs, this option is only safe in conjunction with
		// VerifyConnection which we define above.
		InsecureSkipVerify: true,

		// RFC 8737 Section 4. acme-tls/1 Protocol Definition:
		//
		// > ACME servers that implement "acme-tls/1" MUST only negotiate
		// > TLS 1.2 [RFC5246] or higher when connecting to clients for
		// > validation.
		MinVersion: tls.VersionTLS12,

		// While RFC 8737 does not place restrictions around allowed cipher
		// suites, we wish to restrict ourselves to secure defaults. Specify
		// the Intermediate guideline from Mozilla's TLS config generator to
		// disable obviously weak ciphers.
		//
		// See also: https://ssl-config.mozilla.org/#server=go&version=1.14.4&config=intermediate&guideline=5.7
		CipherSuites: []uint16{
			tls.TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256,
			tls.TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256,
			tls.TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384,
			tls.TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384,
			tls.TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305,
			tls.TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305,
		},
	}

	// Build a dialer using our custom DNS resolver, to ensure domains get
	// resolved according to configuration.
	dialer, err := buildDialerConfig(config)
	if err != nil {
		return false, fmt.Errorf("failed to build dialer: %w", err)
	}

	// Per RFC 8737 Section 3. TLS with Application-Layer Protocol
	// Negotiation (TLS ALPN) Challenge:
	//
	// > 2. The ACME server resolves the domain name being validated and
	// >    chooses one of the IP addresses returned for validation (the
	// >    server MAY validate against multiple addresses if more than
	// >    one is returned).
	// > 3. The ACME server initiates a TLS connection to the chosen IP
	// >    address. This connection MUST use TCP port 443.
	address := fmt.Sprintf("%v:"+ALPNPort, domain)
	conn, err := dialer.Dial("tcp", address)
	if err != nil {
		return false, fmt.Errorf("tls-alpn-01: failed to dial host: %w", err)
	}

	// Initiate the connection to the remote peer.
	client := tls.Client(conn, cfg)

	// We intentionally swallow this error as it isn't useful to the
	// underlying protocol we perform here. Notably, per RFC 8737
	// Section 4. acme-tls/1 Protocol Definition:
	//
	// > Once the handshake is completed, the client MUST NOT exchange
	// > any further data with the server and MUST immediately close the
	// > connection. ... Because of this, an ACME server MAY choose to
	// > withhold authorization if either the certificate signature is
	// > invalid or the handshake doesn't fully complete.
	defer client.Close()

	// We wish to put time bounds on the total time the handshake can
	// stall for, so build a connection context here.
	ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
	defer cancel()

	// See note above about why we can allow Handshake to complete
	// successfully.
	if err := client.HandshakeContext(ctx); err != nil {
		return false, fmt.Errorf("tls-alpn-01: failed to perform handshake: %w", err)
	}
	return true, nil
}
