1package cert
2
3import (
4	"context"
5	"crypto/x509"
6	"fmt"
7	"strings"
8	"time"
9
10	sockaddr "github.com/hashicorp/go-sockaddr"
11	"github.com/hashicorp/vault/sdk/framework"
12	"github.com/hashicorp/vault/sdk/helper/tokenutil"
13	"github.com/hashicorp/vault/sdk/logical"
14)
15
16func pathListCerts(b *backend) *framework.Path {
17	return &framework.Path{
18		Pattern: "certs/?",
19
20		Callbacks: map[logical.Operation]framework.OperationFunc{
21			logical.ListOperation: b.pathCertList,
22		},
23
24		HelpSynopsis:    pathCertHelpSyn,
25		HelpDescription: pathCertHelpDesc,
26	}
27}
28
29func pathCerts(b *backend) *framework.Path {
30	p := &framework.Path{
31		Pattern: "certs/" + framework.GenericNameRegex("name"),
32		Fields: map[string]*framework.FieldSchema{
33			"name": &framework.FieldSchema{
34				Type:        framework.TypeString,
35				Description: "The name of the certificate",
36			},
37
38			"certificate": &framework.FieldSchema{
39				Type: framework.TypeString,
40				Description: `The public certificate that should be trusted.
41Must be x509 PEM encoded.`,
42			},
43
44			"allowed_names": &framework.FieldSchema{
45				Type: framework.TypeCommaStringSlice,
46				Description: `A comma-separated list of names.
47At least one must exist in either the Common Name or SANs. Supports globbing.
48This parameter is deprecated, please use allowed_common_names, allowed_dns_sans,
49allowed_email_sans, allowed_uri_sans.`,
50			},
51
52			"allowed_common_names": &framework.FieldSchema{
53				Type: framework.TypeCommaStringSlice,
54				Description: `A comma-separated list of names.
55At least one must exist in the Common Name. Supports globbing.`,
56			},
57
58			"allowed_dns_sans": &framework.FieldSchema{
59				Type: framework.TypeCommaStringSlice,
60				Description: `A comma-separated list of DNS names.
61At least one must exist in the SANs. Supports globbing.`,
62			},
63
64			"allowed_email_sans": &framework.FieldSchema{
65				Type: framework.TypeCommaStringSlice,
66				Description: `A comma-separated list of Email Addresses.
67At least one must exist in the SANs. Supports globbing.`,
68			},
69
70			"allowed_uri_sans": &framework.FieldSchema{
71				Type: framework.TypeCommaStringSlice,
72				Description: `A comma-separated list of URIs.
73At least one must exist in the SANs. Supports globbing.`,
74			},
75
76			"allowed_organizational_units": &framework.FieldSchema{
77				Type: framework.TypeCommaStringSlice,
78				Description: `A comma-separated list of Organizational Units names.
79At least one must exist in the OU field.`,
80			},
81
82			"required_extensions": &framework.FieldSchema{
83				Type: framework.TypeCommaStringSlice,
84				Description: `A comma-separated string or array of extensions
85formatted as "oid:value". Expects the extension value to be some type of ASN1 encoded string.
86All values much match. Supports globbing on "value".`,
87			},
88
89			"display_name": &framework.FieldSchema{
90				Type: framework.TypeString,
91				Description: `The display name to use for clients using this
92certificate.`,
93			},
94
95			"policies": &framework.FieldSchema{
96				Type:        framework.TypeCommaStringSlice,
97				Description: tokenutil.DeprecationText("token_policies"),
98				Deprecated:  true,
99			},
100
101			"lease": &framework.FieldSchema{
102				Type:        framework.TypeInt,
103				Description: tokenutil.DeprecationText("token_ttl"),
104				Deprecated:  true,
105			},
106
107			"ttl": &framework.FieldSchema{
108				Type:        framework.TypeDurationSecond,
109				Description: tokenutil.DeprecationText("token_ttl"),
110				Deprecated:  true,
111			},
112
113			"max_ttl": &framework.FieldSchema{
114				Type:        framework.TypeDurationSecond,
115				Description: tokenutil.DeprecationText("token_max_ttl"),
116				Deprecated:  true,
117			},
118
119			"period": &framework.FieldSchema{
120				Type:        framework.TypeDurationSecond,
121				Description: tokenutil.DeprecationText("token_period"),
122				Deprecated:  true,
123			},
124
125			"bound_cidrs": &framework.FieldSchema{
126				Type:        framework.TypeCommaStringSlice,
127				Description: tokenutil.DeprecationText("token_bound_cidrs"),
128				Deprecated:  true,
129			},
130		},
131
132		Callbacks: map[logical.Operation]framework.OperationFunc{
133			logical.DeleteOperation: b.pathCertDelete,
134			logical.ReadOperation:   b.pathCertRead,
135			logical.UpdateOperation: b.pathCertWrite,
136		},
137
138		HelpSynopsis:    pathCertHelpSyn,
139		HelpDescription: pathCertHelpDesc,
140	}
141
142	tokenutil.AddTokenFields(p.Fields)
143	return p
144}
145
146func (b *backend) Cert(ctx context.Context, s logical.Storage, n string) (*CertEntry, error) {
147	entry, err := s.Get(ctx, "cert/"+strings.ToLower(n))
148	if err != nil {
149		return nil, err
150	}
151	if entry == nil {
152		return nil, nil
153	}
154
155	var result CertEntry
156	if err := entry.DecodeJSON(&result); err != nil {
157		return nil, err
158	}
159
160	if result.TokenTTL == 0 && result.TTL > 0 {
161		result.TokenTTL = result.TTL
162	}
163	if result.TokenMaxTTL == 0 && result.MaxTTL > 0 {
164		result.TokenMaxTTL = result.MaxTTL
165	}
166	if result.TokenPeriod == 0 && result.Period > 0 {
167		result.TokenPeriod = result.Period
168	}
169	if len(result.TokenPolicies) == 0 && len(result.Policies) > 0 {
170		result.TokenPolicies = result.Policies
171	}
172	if len(result.TokenBoundCIDRs) == 0 && len(result.BoundCIDRs) > 0 {
173		result.TokenBoundCIDRs = result.BoundCIDRs
174	}
175
176	return &result, nil
177}
178
179func (b *backend) pathCertDelete(ctx context.Context, req *logical.Request, d *framework.FieldData) (*logical.Response, error) {
180	err := req.Storage.Delete(ctx, "cert/"+strings.ToLower(d.Get("name").(string)))
181	if err != nil {
182		return nil, err
183	}
184	return nil, nil
185}
186
187func (b *backend) pathCertList(ctx context.Context, req *logical.Request, d *framework.FieldData) (*logical.Response, error) {
188	certs, err := req.Storage.List(ctx, "cert/")
189	if err != nil {
190		return nil, err
191	}
192	return logical.ListResponse(certs), nil
193}
194
195func (b *backend) pathCertRead(ctx context.Context, req *logical.Request, d *framework.FieldData) (*logical.Response, error) {
196	cert, err := b.Cert(ctx, req.Storage, strings.ToLower(d.Get("name").(string)))
197	if err != nil {
198		return nil, err
199	}
200	if cert == nil {
201		return nil, nil
202	}
203
204	data := map[string]interface{}{
205		"certificate":                  cert.Certificate,
206		"display_name":                 cert.DisplayName,
207		"allowed_names":                cert.AllowedNames,
208		"allowed_common_names":         cert.AllowedCommonNames,
209		"allowed_dns_sans":             cert.AllowedDNSSANs,
210		"allowed_email_sans":           cert.AllowedEmailSANs,
211		"allowed_uri_sans":             cert.AllowedURISANs,
212		"allowed_organizational_units": cert.AllowedOrganizationalUnits,
213		"required_extensions":          cert.RequiredExtensions,
214	}
215	cert.PopulateTokenData(data)
216
217	if cert.TTL > 0 {
218		data["ttl"] = int64(cert.TTL.Seconds())
219	}
220	if cert.MaxTTL > 0 {
221		data["max_ttl"] = int64(cert.MaxTTL.Seconds())
222	}
223	if cert.Period > 0 {
224		data["period"] = int64(cert.Period.Seconds())
225	}
226	if len(cert.Policies) > 0 {
227		data["policies"] = data["token_policies"]
228	}
229	if len(cert.BoundCIDRs) > 0 {
230		data["bound_cidrs"] = data["token_bound_cidrs"]
231	}
232
233	return &logical.Response{
234		Data: data,
235	}, nil
236}
237
238func (b *backend) pathCertWrite(ctx context.Context, req *logical.Request, d *framework.FieldData) (*logical.Response, error) {
239	name := strings.ToLower(d.Get("name").(string))
240
241	cert, err := b.Cert(ctx, req.Storage, name)
242	if err != nil {
243		return nil, err
244	}
245
246	if cert == nil {
247		cert = &CertEntry{
248			Name: name,
249		}
250	}
251
252	// Get non tokenutil fields
253	if certificateRaw, ok := d.GetOk("certificate"); ok {
254		cert.Certificate = certificateRaw.(string)
255	}
256	if displayNameRaw, ok := d.GetOk("display_name"); ok {
257		cert.DisplayName = displayNameRaw.(string)
258	}
259	if allowedNamesRaw, ok := d.GetOk("allowed_names"); ok {
260		cert.AllowedNames = allowedNamesRaw.([]string)
261	}
262	if allowedCommonNamesRaw, ok := d.GetOk("allowed_common_names"); ok {
263		cert.AllowedCommonNames = allowedCommonNamesRaw.([]string)
264	}
265	if allowedDNSSANsRaw, ok := d.GetOk("allowed_dns_sans"); ok {
266		cert.AllowedDNSSANs = allowedDNSSANsRaw.([]string)
267	}
268	if allowedEmailSANsRaw, ok := d.GetOk("allowed_email_sans"); ok {
269		cert.AllowedEmailSANs = allowedEmailSANsRaw.([]string)
270	}
271	if allowedURISANsRaw, ok := d.GetOk("allowed_uri_sans"); ok {
272		cert.AllowedURISANs = allowedURISANsRaw.([]string)
273	}
274	if allowedOrganizationalUnitsRaw, ok := d.GetOk("allowed_organizational_units"); ok {
275		cert.AllowedOrganizationalUnits = allowedOrganizationalUnitsRaw.([]string)
276	}
277	if requiredExtensionsRaw, ok := d.GetOk("required_extensions"); ok {
278		cert.RequiredExtensions = requiredExtensionsRaw.([]string)
279	}
280
281	// Get tokenutil fields
282	if err := cert.ParseTokenFields(req, d); err != nil {
283		return logical.ErrorResponse(err.Error()), logical.ErrInvalidRequest
284	}
285
286	// Handle upgrade cases
287	{
288		if err := tokenutil.UpgradeValue(d, "policies", "token_policies", &cert.Policies, &cert.TokenPolicies); err != nil {
289			return logical.ErrorResponse(err.Error()), nil
290		}
291
292		if err := tokenutil.UpgradeValue(d, "ttl", "token_ttl", &cert.TTL, &cert.TokenTTL); err != nil {
293			return logical.ErrorResponse(err.Error()), nil
294		}
295		// Special case here for old lease value
296		_, ok := d.GetOk("token_ttl")
297		if !ok {
298			_, ok = d.GetOk("ttl")
299			if !ok {
300				ttlRaw, ok := d.GetOk("lease")
301				if ok {
302					cert.TTL = time.Duration(ttlRaw.(int)) * time.Second
303					cert.TokenTTL = cert.TTL
304				}
305			}
306		}
307
308		if err := tokenutil.UpgradeValue(d, "max_ttl", "token_max_ttl", &cert.MaxTTL, &cert.TokenMaxTTL); err != nil {
309			return logical.ErrorResponse(err.Error()), nil
310		}
311
312		if err := tokenutil.UpgradeValue(d, "period", "token_period", &cert.Period, &cert.TokenPeriod); err != nil {
313			return logical.ErrorResponse(err.Error()), nil
314		}
315
316		if err := tokenutil.UpgradeValue(d, "bound_cidrs", "token_bound_cidrs", &cert.BoundCIDRs, &cert.TokenBoundCIDRs); err != nil {
317			return logical.ErrorResponse(err.Error()), nil
318		}
319	}
320
321	var resp logical.Response
322
323	systemDefaultTTL := b.System().DefaultLeaseTTL()
324	if cert.TokenTTL > systemDefaultTTL {
325		resp.AddWarning(fmt.Sprintf("Given ttl of %d seconds is greater than current mount/system default of %d seconds", cert.TokenTTL/time.Second, systemDefaultTTL/time.Second))
326	}
327	systemMaxTTL := b.System().MaxLeaseTTL()
328	if cert.TokenMaxTTL > systemMaxTTL {
329		resp.AddWarning(fmt.Sprintf("Given max_ttl of %d seconds is greater than current mount/system default of %d seconds", cert.TokenMaxTTL/time.Second, systemMaxTTL/time.Second))
330	}
331	if cert.TokenMaxTTL != 0 && cert.TokenTTL > cert.TokenMaxTTL {
332		return logical.ErrorResponse("ttl should be shorter than max_ttl"), nil
333	}
334	if cert.TokenPeriod > systemMaxTTL {
335		resp.AddWarning(fmt.Sprintf("Given period of %d seconds is greater than the backend's maximum TTL of %d seconds", cert.TokenPeriod/time.Second, systemMaxTTL/time.Second))
336	}
337
338	// Default the display name to the certificate name if not given
339	if cert.DisplayName == "" {
340		cert.DisplayName = name
341	}
342
343	parsed := parsePEM([]byte(cert.Certificate))
344	if len(parsed) == 0 {
345		return logical.ErrorResponse("failed to parse certificate"), nil
346	}
347
348	// If the certificate is not a CA cert, then ensure that x509.ExtKeyUsageClientAuth is set
349	if !parsed[0].IsCA && parsed[0].ExtKeyUsage != nil {
350		var clientAuth bool
351		for _, usage := range parsed[0].ExtKeyUsage {
352			if usage == x509.ExtKeyUsageClientAuth || usage == x509.ExtKeyUsageAny {
353				clientAuth = true
354				break
355			}
356		}
357		if !clientAuth {
358			return logical.ErrorResponse("non-CA certificates should have TLS client authentication set as an extended key usage"), nil
359		}
360	}
361
362	// Store it
363	entry, err := logical.StorageEntryJSON("cert/"+name, cert)
364	if err != nil {
365		return nil, err
366	}
367	if err := req.Storage.Put(ctx, entry); err != nil {
368		return nil, err
369	}
370
371	if len(resp.Warnings) == 0 {
372		return nil, nil
373	}
374
375	return &resp, nil
376}
377
378type CertEntry struct {
379	tokenutil.TokenParams
380
381	Name                       string
382	Certificate                string
383	DisplayName                string
384	Policies                   []string
385	TTL                        time.Duration
386	MaxTTL                     time.Duration
387	Period                     time.Duration
388	AllowedNames               []string
389	AllowedCommonNames         []string
390	AllowedDNSSANs             []string
391	AllowedEmailSANs           []string
392	AllowedURISANs             []string
393	AllowedOrganizationalUnits []string
394	RequiredExtensions         []string
395	BoundCIDRs                 []*sockaddr.SockAddrMarshaler
396}
397
398const pathCertHelpSyn = `
399Manage trusted certificates used for authentication.
400`
401
402const pathCertHelpDesc = `
403This endpoint allows you to create, read, update, and delete trusted certificates
404that are allowed to authenticate.
405
406Deleting a certificate will not revoke auth for prior authenticated connections.
407To do this, do a revoke on "login". If you don't need to revoke login immediately,
408then the next renew will cause the lease to expire.
409`
410