aboutsummaryrefslogtreecommitdiffhomepage
path: root/modules/caddytls/certselection.go
blob: a561e3a1d001c2c7d5817e628eabc839fde88311 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
// Copyright 2015 Matthew Holt and The Caddy Authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//     http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package caddytls

import (
	"crypto/tls"
	"crypto/x509"
	"encoding/json"
	"fmt"
	"math/big"
	"slices"

	"github.com/caddyserver/certmagic"

	"github.com/caddyserver/caddy/v2/caddyconfig/caddyfile"
)

// CustomCertSelectionPolicy 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 CustomCertSelectionPolicy struct {
	// The certificate must have one of these serial numbers.
	SerialNumber []bigInt `json:"serial_number,omitempty"`

	// The certificate must have one of these organization names.
	SubjectOrganization []string `json:"subject_organization,omitempty"`

	// The certificate must use this public key algorithm.
	PublicKeyAlgorithm PublicKeyAlgorithm `json:"public_key_algorithm,omitempty"`

	// The certificate must have at least one of the tags in the list.
	AnyTag []string `json:"any_tag,omitempty"`

	// The certificate must have all of the tags in the list.
	AllTags []string `json:"all_tags,omitempty"`
}

// SelectCertificate implements certmagic.CertificateSelector. It
// only chooses a certificate that at least meets the criteria in
// p. It then chooses the first non-expired certificate that is
// compatible with the client. If none are valid, it chooses the
// first viable candidate anyway.
func (p CustomCertSelectionPolicy) SelectCertificate(hello *tls.ClientHelloInfo, choices []certmagic.Certificate) (certmagic.Certificate, error) {
	viable := make([]certmagic.Certificate, 0, len(choices))

nextChoice:
	for _, cert := range choices {
		if len(p.SerialNumber) > 0 {
			var found bool
			for _, sn := range p.SerialNumber {
				snInt := sn.Int // avoid taking address of iteration variable (gosec warning)
				if cert.Leaf.SerialNumber.Cmp(&snInt) == 0 {
					found = true
					break
				}
			}
			if !found {
				continue
			}
		}

		if len(p.SubjectOrganization) > 0 {
			found := slices.ContainsFunc(p.SubjectOrganization, func(s string) bool {
				return slices.Contains(cert.Leaf.Subject.Organization, s)
			})
			if !found {
				continue
			}
		}

		if p.PublicKeyAlgorithm != PublicKeyAlgorithm(x509.UnknownPublicKeyAlgorithm) &&
			PublicKeyAlgorithm(cert.Leaf.PublicKeyAlgorithm) != p.PublicKeyAlgorithm {
			continue
		}

		if len(p.AnyTag) > 0 {
			var found bool
			for _, tag := range p.AnyTag {
				if cert.HasTag(tag) {
					found = true
					break
				}
			}
			if !found {
				continue
			}
		}

		if len(p.AllTags) > 0 {
			for _, tag := range p.AllTags {
				if !cert.HasTag(tag) {
					continue nextChoice
				}
			}
		}

		// this certificate at least meets the policy's requirements,
		// but we still have to check expiration and compatibility
		viable = append(viable, cert)
	}

	if len(viable) == 0 {
		return certmagic.Certificate{}, fmt.Errorf("no certificates matched custom selection policy")
	}

	return certmagic.DefaultCertificateSelector(hello, viable)
}

// UnmarshalCaddyfile sets up the CustomCertSelectionPolicy from Caddyfile tokens. Syntax:
//
//	cert_selection {
//		all_tags             <values...>
//		any_tag              <values...>
//		public_key_algorithm <dsa|ecdsa|rsa>
//		serial_number        <big_integers...>
//		subject_organization <values...>
//	}
func (p *CustomCertSelectionPolicy) UnmarshalCaddyfile(d *caddyfile.Dispenser) error {
	_, wrapper := d.Next(), d.Val() // consume wrapper name

	// No same-line options are supported
	if d.CountRemainingArgs() > 0 {
		return d.ArgErr()
	}

	var hasPublicKeyAlgorithm bool
	for nesting := d.Nesting(); d.NextBlock(nesting); {
		optionName := d.Val()
		switch optionName {
		case "all_tags":
			if d.CountRemainingArgs() == 0 {
				return d.ArgErr()
			}
			p.AllTags = append(p.AllTags, d.RemainingArgs()...)
		case "any_tag":
			if d.CountRemainingArgs() == 0 {
				return d.ArgErr()
			}
			p.AnyTag = append(p.AnyTag, d.RemainingArgs()...)
		case "public_key_algorithm":
			if hasPublicKeyAlgorithm {
				return d.Errf("duplicate %s option '%s'", wrapper, optionName)
			}
			if d.CountRemainingArgs() != 1 {
				return d.ArgErr()
			}
			d.NextArg()
			if err := p.PublicKeyAlgorithm.UnmarshalJSON([]byte(d.Val())); err != nil {
				return d.Errf("parsing %s option '%s': %v", wrapper, optionName, err)
			}
			hasPublicKeyAlgorithm = true
		case "serial_number":
			if d.CountRemainingArgs() == 0 {
				return d.ArgErr()
			}
			for d.NextArg() {
				val, bi := d.Val(), bigInt{}
				_, ok := bi.SetString(val, 10)
				if !ok {
					return d.Errf("parsing %s option '%s': invalid big.int value %s", wrapper, optionName, val)
				}
				p.SerialNumber = append(p.SerialNumber, bi)
			}
		case "subject_organization":
			if d.CountRemainingArgs() == 0 {
				return d.ArgErr()
			}
			p.SubjectOrganization = append(p.SubjectOrganization, d.RemainingArgs()...)
		default:
			return d.ArgErr()
		}

		// No nested blocks are supported
		if d.NextBlock(nesting + 1) {
			return d.Errf("malformed %s option '%s': blocks are not supported", wrapper, optionName)
		}
	}

	return nil
}

// bigInt is a big.Int type that interops with JSON encodings as a string.
type bigInt struct{ big.Int }

func (bi bigInt) MarshalJSON() ([]byte, error) {
	return json.Marshal(bi.String())
}

func (bi *bigInt) UnmarshalJSON(p []byte) error {
	if string(p) == "null" {
		return nil
	}
	var stringRep string
	err := json.Unmarshal(p, &stringRep)
	if err != nil {
		return err
	}
	_, ok := bi.SetString(stringRep, 10)
	if !ok {
		return fmt.Errorf("not a valid big integer: %s", p)
	}
	return nil
}

// Interface guard
var _ caddyfile.Unmarshaler = (*CustomCertSelectionPolicy)(nil)