File: engine.go

package info (click to toggle)
golang-github-smallstep-certificates 0.29.0-1
  • links: PTS, VCS
  • area: main
  • in suites: forky, sid
  • size: 6,720 kB
  • sloc: sh: 385; makefile: 129
file content (303 lines) | stat: -rw-r--r-- 10,463 bytes parent folder | download | duplicates (2)
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
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
package policy

import (
	"crypto/x509"
	"fmt"
	"net"
	"net/http"
	"net/url"

	"go.step.sm/crypto/x509util"
	"golang.org/x/crypto/ssh"

	"github.com/smallstep/certificates/errs"
)

type NamePolicyReason int

const (
	// NotAllowed results when an instance of NamePolicyEngine
	// determines that there's a constraint which doesn't permit
	// a DNS or another type of SAN to be signed (or otherwise used).
	NotAllowed NamePolicyReason = iota + 1
	// CannotParseDomain is returned when an error occurs
	// when parsing the domain part of SAN or subject.
	CannotParseDomain
	// CannotParseRFC822Name is returned when an error
	// occurs when parsing an email address.
	CannotParseRFC822Name
	// CannotMatch is the type of error returned when
	// an error happens when matching SAN types.
	CannotMatchNameToConstraint
)

type NameType string

const (
	CNNameType        NameType = "cn"
	DNSNameType       NameType = "dns"
	IPNameType        NameType = "ip"
	EmailNameType     NameType = "email"
	URINameType       NameType = "uri"
	PrincipalNameType NameType = "principal"
)

type NamePolicyError struct {
	Reason   NamePolicyReason
	NameType NameType
	Name     string
	detail   string
}

func (e *NamePolicyError) Error() string {
	switch e.Reason {
	case NotAllowed:
		return fmt.Sprintf("%s name %q not allowed", e.NameType, e.Name)
	case CannotParseDomain:
		return fmt.Sprintf("cannot parse %s domain %q", e.NameType, e.Name)
	case CannotParseRFC822Name:
		return fmt.Sprintf("cannot parse %s rfc822Name %q", e.NameType, e.Name)
	case CannotMatchNameToConstraint:
		return fmt.Sprintf("error matching %s name %q to constraint", e.NameType, e.Name)
	default:
		return fmt.Sprintf("unknown error reason (%d): %s", e.Reason, e.detail)
	}
}

// As implements the As(any) bool interface and allows to use "errors.As()" to
// convert a NotAllowed NamePolicyError to an errs.Error.
func (e *NamePolicyError) As(v any) bool {
	if e.Reason == NotAllowed {
		if err, ok := v.(**errs.Error); ok {
			*err = &errs.Error{
				Status: http.StatusForbidden,
				Msg:    fmt.Sprintf("The request was forbidden by the certificate authority: %s", e.Error()),
				Err:    e,
			}
			return true
		}
	}
	return false
}

func (e *NamePolicyError) Detail() string {
	return e.detail
}

// NamePolicyEngine can be used to check that a CSR or Certificate meets all allowed and
// denied names before a CA creates and/or signs the Certificate.
// TODO(hs): the X509 RFC also defines name checks on directory name; support that?
// TODO(hs): implement Stringer interface: describe the contents of the NamePolicyEngine?
// TODO(hs): implement matching URI schemes, paths, etc; not just the domain part of URI domains

type NamePolicyEngine struct {
	// verifySubjectCommonName is set when Subject Common Name must be verified
	verifySubjectCommonName bool
	// allowLiteralWildcardNames allows literal wildcard DNS domains
	allowLiteralWildcardNames bool

	// permitted and exluded constraints similar to x509 Name Constraints
	permittedCommonNames    []string
	excludedCommonNames     []string
	permittedDNSDomains     []string
	excludedDNSDomains      []string
	permittedIPRanges       []*net.IPNet
	excludedIPRanges        []*net.IPNet
	permittedEmailAddresses []string
	excludedEmailAddresses  []string
	permittedURIDomains     []string
	excludedURIDomains      []string
	permittedPrincipals     []string
	excludedPrincipals      []string

	// some internal counts for housekeeping
	numberOfCommonNameConstraints     int
	numberOfDNSDomainConstraints      int
	numberOfIPRangeConstraints        int
	numberOfEmailAddressConstraints   int
	numberOfURIDomainConstraints      int
	numberOfPrincipalConstraints      int
	totalNumberOfPermittedConstraints int
	totalNumberOfExcludedConstraints  int
	totalNumberOfConstraints          int
}

// NewNamePolicyEngine creates a new NamePolicyEngine with NamePolicyOptions
func New(opts ...NamePolicyOption) (*NamePolicyEngine, error) {
	e := &NamePolicyEngine{}
	for _, option := range opts {
		if err := option(e); err != nil {
			return nil, err
		}
	}

	e.permittedCommonNames = removeDuplicates(e.permittedCommonNames)
	e.permittedDNSDomains = removeDuplicates(e.permittedDNSDomains)
	e.permittedIPRanges = removeDuplicateIPNets(e.permittedIPRanges)
	e.permittedEmailAddresses = removeDuplicates(e.permittedEmailAddresses)
	e.permittedURIDomains = removeDuplicates(e.permittedURIDomains)
	e.permittedPrincipals = removeDuplicates(e.permittedPrincipals)

	e.excludedCommonNames = removeDuplicates(e.excludedCommonNames)
	e.excludedDNSDomains = removeDuplicates(e.excludedDNSDomains)
	e.excludedIPRanges = removeDuplicateIPNets(e.excludedIPRanges)
	e.excludedEmailAddresses = removeDuplicates(e.excludedEmailAddresses)
	e.excludedURIDomains = removeDuplicates(e.excludedURIDomains)
	e.excludedPrincipals = removeDuplicates(e.excludedPrincipals)

	e.numberOfCommonNameConstraints = len(e.permittedCommonNames) + len(e.excludedCommonNames)
	e.numberOfDNSDomainConstraints = len(e.permittedDNSDomains) + len(e.excludedDNSDomains)
	e.numberOfIPRangeConstraints = len(e.permittedIPRanges) + len(e.excludedIPRanges)
	e.numberOfEmailAddressConstraints = len(e.permittedEmailAddresses) + len(e.excludedEmailAddresses)
	e.numberOfURIDomainConstraints = len(e.permittedURIDomains) + len(e.excludedURIDomains)
	e.numberOfPrincipalConstraints = len(e.permittedPrincipals) + len(e.excludedPrincipals)

	e.totalNumberOfPermittedConstraints = len(e.permittedCommonNames) + len(e.permittedDNSDomains) +
		len(e.permittedIPRanges) + len(e.permittedEmailAddresses) + len(e.permittedURIDomains) +
		len(e.permittedPrincipals)

	e.totalNumberOfExcludedConstraints = len(e.excludedCommonNames) + len(e.excludedDNSDomains) +
		len(e.excludedIPRanges) + len(e.excludedEmailAddresses) + len(e.excludedURIDomains) +
		len(e.excludedPrincipals)

	e.totalNumberOfConstraints = e.totalNumberOfPermittedConstraints + e.totalNumberOfExcludedConstraints

	return e, nil
}

// removeDuplicates returns a new slice of strings with
// duplicate values removed. It retains the order of elements
// in the source slice.
func removeDuplicates(items []string) (ret []string) {
	// no need to remove dupes; return original
	if len(items) <= 1 {
		return items
	}

	keys := make(map[string]struct{}, len(items))

	ret = make([]string, 0, len(items))
	for _, item := range items {
		if _, ok := keys[item]; ok {
			continue
		}

		keys[item] = struct{}{}
		ret = append(ret, item)
	}

	return
}

// removeDuplicateIPNets returns a new slice of net.IPNets with
// duplicate values removed. It retains the order of elements in
// the source slice. An IPNet is considered duplicate if its CIDR
// notation exists multiple times in the slice.
func removeDuplicateIPNets(items []*net.IPNet) (ret []*net.IPNet) {
	// no need to remove dupes; return original
	if len(items) <= 1 {
		return items
	}

	keys := make(map[string]struct{}, len(items))

	ret = make([]*net.IPNet, 0, len(items))
	for _, item := range items {
		key := item.String() // use CIDR notation as key
		if _, ok := keys[key]; ok {
			continue
		}

		keys[key] = struct{}{}
		ret = append(ret, item)
	}

	// TODO(hs): implement filter of fully overlapping ranges,
	// so that the smaller ones are automatically removed?

	return
}

// IsX509CertificateAllowed verifies that all SANs in a Certificate are allowed.
func (e *NamePolicyEngine) IsX509CertificateAllowed(cert *x509.Certificate) error {
	if err := e.validateNames(cert.DNSNames, cert.IPAddresses, cert.EmailAddresses, cert.URIs, []string{}); err != nil {
		return err
	}

	if e.verifySubjectCommonName {
		return e.validateCommonName(cert.Subject.CommonName)
	}

	return nil
}

// IsX509CertificateRequestAllowed verifies that all names in the CSR are allowed.
func (e *NamePolicyEngine) IsX509CertificateRequestAllowed(csr *x509.CertificateRequest) error {
	if err := e.validateNames(csr.DNSNames, csr.IPAddresses, csr.EmailAddresses, csr.URIs, []string{}); err != nil {
		return err
	}

	if e.verifySubjectCommonName {
		return e.validateCommonName(csr.Subject.CommonName)
	}

	return nil
}

// AreSANsAllowed verifies that all names in the slice of SANs are allowed.
// The SANs are first split into DNS names, IPs, email addresses and URIs.
func (e *NamePolicyEngine) AreSANsAllowed(sans []string) error {
	dnsNames, ips, emails, uris := x509util.SplitSANs(sans)
	return e.validateNames(dnsNames, ips, emails, uris, []string{})
}

// IsDNSAllowed verifies a single DNS domain is allowed.
func (e *NamePolicyEngine) IsDNSAllowed(dns string) error {
	return e.validateNames([]string{dns}, []net.IP{}, []string{}, []*url.URL{}, []string{})
}

// IsIPAllowed verifies a single IP domain is allowed.
func (e *NamePolicyEngine) IsIPAllowed(ip net.IP) error {
	return e.validateNames([]string{}, []net.IP{ip}, []string{}, []*url.URL{}, []string{})
}

// IsSSHCertificateAllowed verifies that all principals in an SSH certificate are allowed.
func (e *NamePolicyEngine) IsSSHCertificateAllowed(cert *ssh.Certificate) error {
	dnsNames, ips, emails, principals, err := splitSSHPrincipals(cert)
	if err != nil {
		return err
	}
	return e.validateNames(dnsNames, ips, emails, []*url.URL{}, principals)
}

// splitSSHPrincipals splits SSH certificate principals into DNS names, emails and usernames.
func splitSSHPrincipals(cert *ssh.Certificate) (dnsNames []string, ips []net.IP, emails, principals []string, err error) {
	dnsNames = []string{}
	ips = []net.IP{}
	emails = []string{}
	principals = []string{}
	var uris []*url.URL
	switch cert.CertType {
	case ssh.HostCert:
		dnsNames, ips, emails, uris = x509util.SplitSANs(cert.ValidPrincipals)
		if len(uris) > 0 {
			err = fmt.Errorf("URL principals %v not expected in SSH host certificate ", uris)
		}
	case ssh.UserCert:
		// re-using SplitSANs results in anything that can't be parsed as an IP, URI or email
		// to be considered a username principal. This allows usernames like h.slatman to be present
		// in the SSH certificate. We're exluding URIs, because they can be confusing
		// when used in a SSH user certificate.
		principals, ips, emails, uris = x509util.SplitSANs(cert.ValidPrincipals)
		if len(ips) > 0 {
			err = fmt.Errorf("IP principals %v not expected in SSH user certificate ", ips)
		}
		if len(uris) > 0 {
			err = fmt.Errorf("URL principals %v not expected in SSH user certificate ", uris)
		}
	default:
		err = fmt.Errorf("unexpected SSH certificate type %d", cert.CertType)
	}

	return
}