From 85ce15a5ad0c0a0e24ed026b1f06a5a573c74283 Mon Sep 17 00:00:00 2001
From: Matthew Holt <mholt@users.noreply.github.com>
Date: Wed, 9 Oct 2019 19:41:45 -0600
Subject: [PATCH] tls: Add custom certificate selection policy

This migrates a feature that was previously reserved for enterprise
users, according to https://github.com/caddyserver/caddy/issues/2786.

Custom certificate selection policies allow advanced control over which
cert is selected when multiple qualify to satisfy a TLS handshake.
---
 modules/caddytls/certselection.go | 71 +++++++++++++++++++++++++++++++
 1 file changed, 71 insertions(+)
 create mode 100644 modules/caddytls/certselection.go

diff --git a/modules/caddytls/certselection.go b/modules/caddytls/certselection.go
new file mode 100644
index 000000000..b56185ac0
--- /dev/null
+++ b/modules/caddytls/certselection.go
@@ -0,0 +1,71 @@
+package caddytls
+
+import (
+	"crypto/tls"
+	"crypto/x509"
+	"fmt"
+	"math/big"
+
+	"github.com/caddyserver/caddy/v2"
+	"github.com/mholt/certmagic"
+)
+
+func init() {
+	caddy.RegisterModule(Policy{})
+}
+
+// Policy represents a policy for selecting the certificate used to
+// complete a handshake when there may be multiple options. All fields
+// specified must match the candidate certificate for it to be chosen.
+// This was needed to solve https://github.com/caddyserver/caddy/issues/2588.
+type Policy struct {
+	SerialNumber        *big.Int           `json:"serial_number,omitempty"`
+	SubjectOrganization string             `json:"subject_organization,omitempty"`
+	PublicKeyAlgorithm  PublicKeyAlgorithm `json:"public_key_algorithm,omitempty"`
+	Tag                 string             `json:"tag,omitempty"`
+}
+
+// CaddyModule returns the Caddy module information.
+func (Policy) CaddyModule() caddy.ModuleInfo {
+	return caddy.ModuleInfo{
+		Name: "tls.certificate_selection.custom",
+		New:  func() caddy.Module { return new(Policy) },
+	}
+}
+
+// SelectCertificate implements certmagic.CertificateSelector.
+func (p Policy) SelectCertificate(_ *tls.ClientHelloInfo, choices []certmagic.Certificate) (certmagic.Certificate, error) {
+	for _, cert := range choices {
+		if p.SerialNumber != nil && cert.SerialNumber.Cmp(p.SerialNumber) != 0 {
+			continue
+		}
+
+		if p.PublicKeyAlgorithm != PublicKeyAlgorithm(x509.UnknownPublicKeyAlgorithm) &&
+			PublicKeyAlgorithm(cert.PublicKeyAlgorithm) != p.PublicKeyAlgorithm {
+			continue
+		}
+
+		if p.SubjectOrganization != "" {
+			var matchOrg bool
+			for _, org := range cert.Subject.Organization {
+				if p.SubjectOrganization == org {
+					matchOrg = true
+					break
+				}
+			}
+			if !matchOrg {
+				continue
+			}
+		}
+
+		if p.Tag != "" && !cert.HasTag(p.Tag) {
+			continue
+		}
+
+		return cert, nil
+	}
+	return certmagic.Certificate{}, fmt.Errorf("no certificates matched custom selection policy")
+}
+
+// Interface guard
+var _ certmagic.CertificateSelector = (*Policy)(nil)