1package cert
2
3import (
4	"crypto/x509"
5	"fmt"
6	"strings"
7	"time"
8
9	"github.com/hashicorp/vault/helper/policyutil"
10	"github.com/hashicorp/vault/logical"
11	"github.com/hashicorp/vault/logical/framework"
12)
13
14func pathListCerts(b *backend) *framework.Path {
15	return &framework.Path{
16		Pattern: "certs/?",
17
18		Callbacks: map[logical.Operation]framework.OperationFunc{
19			logical.ListOperation: b.pathCertList,
20		},
21
22		HelpSynopsis:    pathCertHelpSyn,
23		HelpDescription: pathCertHelpDesc,
24	}
25}
26
27func pathCerts(b *backend) *framework.Path {
28	return &framework.Path{
29		Pattern: "certs/" + framework.GenericNameRegex("name"),
30		Fields: map[string]*framework.FieldSchema{
31			"name": &framework.FieldSchema{
32				Type:        framework.TypeString,
33				Description: "The name of the certificate",
34			},
35
36			"certificate": &framework.FieldSchema{
37				Type: framework.TypeString,
38				Description: `The public certificate that should be trusted.
39Must be x509 PEM encoded.`,
40			},
41
42			"display_name": &framework.FieldSchema{
43				Type: framework.TypeString,
44				Description: `The display name to use for clients using this
45certificate.`,
46			},
47
48			"policies": &framework.FieldSchema{
49				Type:        framework.TypeString,
50				Description: "Comma-seperated list of policies.",
51			},
52
53			"lease": &framework.FieldSchema{
54				Type: framework.TypeInt,
55				Description: `Deprecated: use "ttl" instead. TTL time in
56seconds. Defaults to system/backend default TTL.`,
57			},
58
59			"ttl": &framework.FieldSchema{
60				Type: framework.TypeDurationSecond,
61				Description: `TTL for tokens issued by this backend.
62Defaults to system/backend default TTL time.`,
63			},
64		},
65
66		Callbacks: map[logical.Operation]framework.OperationFunc{
67			logical.DeleteOperation: b.pathCertDelete,
68			logical.ReadOperation:   b.pathCertRead,
69			logical.UpdateOperation: b.pathCertWrite,
70		},
71
72		HelpSynopsis:    pathCertHelpSyn,
73		HelpDescription: pathCertHelpDesc,
74	}
75}
76
77func (b *backend) Cert(s logical.Storage, n string) (*CertEntry, error) {
78	entry, err := s.Get("cert/" + strings.ToLower(n))
79	if err != nil {
80		return nil, err
81	}
82	if entry == nil {
83		return nil, nil
84	}
85
86	var result CertEntry
87	if err := entry.DecodeJSON(&result); err != nil {
88		return nil, err
89	}
90	return &result, nil
91}
92
93func (b *backend) pathCertDelete(
94	req *logical.Request, d *framework.FieldData) (*logical.Response, error) {
95	err := req.Storage.Delete("cert/" + strings.ToLower(d.Get("name").(string)))
96	if err != nil {
97		return nil, err
98	}
99	return nil, nil
100}
101
102func (b *backend) pathCertList(
103	req *logical.Request, d *framework.FieldData) (*logical.Response, error) {
104	certs, err := req.Storage.List("cert/")
105	if err != nil {
106		return nil, err
107	}
108	return logical.ListResponse(certs), nil
109}
110
111func (b *backend) pathCertRead(
112	req *logical.Request, d *framework.FieldData) (*logical.Response, error) {
113	cert, err := b.Cert(req.Storage, strings.ToLower(d.Get("name").(string)))
114	if err != nil {
115		return nil, err
116	}
117	if cert == nil {
118		return nil, nil
119	}
120
121	duration := cert.TTL
122	if duration == 0 {
123		duration = b.System().DefaultLeaseTTL()
124	}
125
126	return &logical.Response{
127		Data: map[string]interface{}{
128			"certificate":  cert.Certificate,
129			"display_name": cert.DisplayName,
130			"policies":     strings.Join(cert.Policies, ","),
131			"ttl":          duration / time.Second,
132		},
133	}, nil
134}
135
136func (b *backend) pathCertWrite(
137	req *logical.Request, d *framework.FieldData) (*logical.Response, error) {
138	name := strings.ToLower(d.Get("name").(string))
139	certificate := d.Get("certificate").(string)
140	displayName := d.Get("display_name").(string)
141	policies := policyutil.ParsePolicies(d.Get("policies").(string))
142
143	// Default the display name to the certificate name if not given
144	if displayName == "" {
145		displayName = name
146	}
147
148	parsed := parsePEM([]byte(certificate))
149	if len(parsed) == 0 {
150		return logical.ErrorResponse("failed to parse certificate"), nil
151	}
152
153	// If the certificate is not a CA cert, then ensure that x509.ExtKeyUsageClientAuth is set
154	if !parsed[0].IsCA && parsed[0].ExtKeyUsage != nil {
155		var clientAuth bool
156		for _, usage := range parsed[0].ExtKeyUsage {
157			if usage == x509.ExtKeyUsageClientAuth || usage == x509.ExtKeyUsageAny {
158				clientAuth = true
159				break
160			}
161		}
162		if !clientAuth {
163			return logical.ErrorResponse("non-CA certificates should have TLS client authentication set as an extended key usage"), nil
164		}
165	}
166
167	certEntry := &CertEntry{
168		Name:        name,
169		Certificate: certificate,
170		DisplayName: displayName,
171		Policies:    policies,
172	}
173
174	// Parse the lease duration or default to backend/system default
175	maxTTL := b.System().MaxLeaseTTL()
176	ttl := time.Duration(d.Get("ttl").(int)) * time.Second
177	if ttl == time.Duration(0) {
178		ttl = time.Second * time.Duration(d.Get("lease").(int))
179	}
180	if ttl > maxTTL {
181		return logical.ErrorResponse(fmt.Sprintf("Given TTL of %d seconds greater than current mount/system default of %d seconds", ttl/time.Second, maxTTL/time.Second)), nil
182	}
183	if ttl > time.Duration(0) {
184		certEntry.TTL = ttl
185	}
186
187	// Store it
188	entry, err := logical.StorageEntryJSON("cert/"+name, certEntry)
189	if err != nil {
190		return nil, err
191	}
192	if err := req.Storage.Put(entry); err != nil {
193		return nil, err
194	}
195	return nil, nil
196}
197
198type CertEntry struct {
199	Name        string
200	Certificate string
201	DisplayName string
202	Policies    []string
203	TTL         time.Duration
204}
205
206const pathCertHelpSyn = `
207Manage trusted certificates used for authentication.
208`
209
210const pathCertHelpDesc = `
211This endpoint allows you to create, read, update, and delete trusted certificates
212that are allowed to authenticate.
213
214Deleting a certificate will not revoke auth for prior authenticated connections.
215To do this, do a revoke on "login". If you don't need to revoke login immediately,
216then the next renew will cause the lease to expire.
217`
218