-
Notifications
You must be signed in to change notification settings - Fork 4.1k
/
acme_wrappers.go
470 lines (394 loc) · 14.1 KB
/
acme_wrappers.go
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
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: MPL-2.0
package pki
import (
"context"
"encoding/json"
"fmt"
"net/http"
"net/url"
"strings"
"github.com/hashicorp/vault/sdk/framework"
"github.com/hashicorp/vault/sdk/logical"
)
type acmeContext struct {
// baseUrl is the combination of the configured cluster local URL and the acmePath up to /acme/
baseUrl *url.URL
clusterUrl *url.URL
sc *storageContext
role *roleEntry
issuer *issuerEntry
// acmeDirectory is a string that can distinguish the various acme directories we have configured
// if something needs to remain locked into a directory path structure.
acmeDirectory string
eabPolicy EabPolicy
}
func (c acmeContext) getAcmeState() *acmeState {
return c.sc.Backend.acmeState
}
type (
acmeOperation func(acmeCtx *acmeContext, r *logical.Request, _ *framework.FieldData) (*logical.Response, error)
acmeParsedOperation func(acmeCtx *acmeContext, r *logical.Request, fields *framework.FieldData, userCtx *jwsCtx, data map[string]interface{}) (*logical.Response, error)
acmeAccountRequiredOperation func(acmeCtx *acmeContext, r *logical.Request, fields *framework.FieldData, userCtx *jwsCtx, data map[string]interface{}, acct *acmeAccount) (*logical.Response, error)
)
// acmeErrorWrapper the lowest level wrapper that will translate errors into proper ACME error responses
func acmeErrorWrapper(op framework.OperationFunc) framework.OperationFunc {
return func(ctx context.Context, r *logical.Request, data *framework.FieldData) (*logical.Response, error) {
resp, err := op(ctx, r, data)
if err != nil {
return TranslateError(err)
}
return resp, nil
}
}
// acmeWrapper a basic wrapper that all ACME handlers should leverage as the basis.
// This will create a basic ACME context, validate basic ACME configuration is setup
// for operations. This pulls in acmeErrorWrapper to translate error messages for users,
// but does not enforce any sort of ACME authentication.
func (b *backend) acmeWrapper(op acmeOperation) framework.OperationFunc {
return acmeErrorWrapper(func(ctx context.Context, r *logical.Request, data *framework.FieldData) (*logical.Response, error) {
sc := b.makeStorageContext(ctx, r.Storage)
config, err := sc.Backend.acmeState.getConfigWithUpdate(sc)
if err != nil {
return nil, fmt.Errorf("failed to fetch ACME configuration: %w", err)
}
// use string form in case someone messes up our config from raw storage.
eabPolicy, err := getEabPolicyByString(string(config.EabPolicyName))
if err != nil {
return nil, err
}
if isAcmeDisabled(sc, config, eabPolicy) {
return nil, ErrAcmeDisabled
}
if b.useLegacyBundleCaStorage() {
return nil, fmt.Errorf("%w: Can not perform ACME operations until migration has completed", ErrServerInternal)
}
acmeBaseUrl, clusterBase, err := getAcmeBaseUrl(sc, r)
if err != nil {
return nil, err
}
role, issuer, err := getAcmeRoleAndIssuer(sc, data, config)
if err != nil {
return nil, err
}
acmeDirectory, err := getAcmeDirectory(r)
if err != nil {
return nil, err
}
acmeCtx := &acmeContext{
baseUrl: acmeBaseUrl,
clusterUrl: clusterBase,
sc: sc,
role: role,
issuer: issuer,
acmeDirectory: acmeDirectory,
eabPolicy: eabPolicy,
}
return op(acmeCtx, r, data)
})
}
// acmeParsedWrapper is an ACME wrapper that will parse out the ACME request parameters, validate
// that we have a proper signature and pass to the operation a decoded map of arguments received.
// This wrapper builds on top of acmeWrapper. Note that this does perform signature verification
// it does not enforce the account being in a valid state nor existing.
func (b *backend) acmeParsedWrapper(op acmeParsedOperation) framework.OperationFunc {
return b.acmeWrapper(func(acmeCtx *acmeContext, r *logical.Request, fields *framework.FieldData) (*logical.Response, error) {
user, data, err := b.acmeState.ParseRequestParams(acmeCtx, r, fields)
if err != nil {
return nil, err
}
resp, err := op(acmeCtx, r, fields, user, data)
// Our response handlers might not add the necessary headers.
if resp != nil {
if resp.Headers == nil {
resp.Headers = map[string][]string{}
}
if _, ok := resp.Headers["Replay-Nonce"]; !ok {
nonce, _, err := b.acmeState.GetNonce()
if err != nil {
return nil, err
}
resp.Headers["Replay-Nonce"] = []string{nonce}
}
if _, ok := resp.Headers["Link"]; !ok {
resp.Headers["Link"] = genAcmeLinkHeader(acmeCtx)
} else {
directory := genAcmeLinkHeader(acmeCtx)[0]
addDirectory := true
for _, item := range resp.Headers["Link"] {
if item == directory {
addDirectory = false
break
}
}
if addDirectory {
resp.Headers["Link"] = append(resp.Headers["Link"], directory)
}
}
// ACME responses don't understand Vault's default encoding
// format. Rather than expecting everything to handle creating
// ACME-formatted responses, do the marshaling in one place.
if _, ok := resp.Data[logical.HTTPRawBody]; !ok {
ignored_values := map[string]bool{logical.HTTPContentType: true, logical.HTTPStatusCode: true}
fields := map[string]interface{}{}
body := map[string]interface{}{
logical.HTTPContentType: "application/json",
logical.HTTPStatusCode: http.StatusOK,
}
for key, value := range resp.Data {
if _, present := ignored_values[key]; !present {
fields[key] = value
} else {
body[key] = value
}
}
rawBody, err := json.Marshal(fields)
if err != nil {
return nil, fmt.Errorf("Error marshaling JSON body: %w", err)
}
body[logical.HTTPRawBody] = rawBody
resp.Data = body
}
}
return resp, err
})
}
// acmeAccountRequiredWrapper builds on top of acmeParsedWrapper, enforcing the
// request has a proper signature for an existing account, and that account is
// in a valid status. It passes to the operation a decoded form of the request
// parameters as well as the ACME account the request is for.
func (b *backend) acmeAccountRequiredWrapper(op acmeAccountRequiredOperation) framework.OperationFunc {
return b.acmeParsedWrapper(func(acmeCtx *acmeContext, r *logical.Request, fields *framework.FieldData, uc *jwsCtx, data map[string]interface{}) (*logical.Response, error) {
if !uc.Existing {
return nil, fmt.Errorf("cannot process request without a 'kid': %w", ErrMalformed)
}
account, err := requireValidAcmeAccount(acmeCtx, uc)
if err != nil {
return nil, err
}
return op(acmeCtx, r, fields, uc, data, account)
})
}
func requireValidAcmeAccount(acmeCtx *acmeContext, uc *jwsCtx) (*acmeAccount, error) {
account, err := acmeCtx.getAcmeState().LoadAccount(acmeCtx, uc.Kid)
if err != nil {
return nil, fmt.Errorf("error loading account: %w", err)
}
if err = acmeCtx.eabPolicy.EnforceForExistingAccount(account); err != nil {
return nil, err
}
if account.Status != AccountStatusValid {
// Treating "revoked" and "deactivated" as the same here.
return nil, fmt.Errorf("%w: account in status: %s", ErrUnauthorized, account.Status)
}
return account, nil
}
// A helper function that will build up the various path patterns we want for ACME APIs.
func buildAcmeFrameworkPaths(b *backend, patternFunc func(b *backend, pattern string) *framework.Path, acmeApi string) []*framework.Path {
var patterns []*framework.Path
for _, baseUrl := range []string{
"acme",
"roles/" + framework.GenericNameRegex("role") + "/acme",
"issuer/" + framework.GenericNameRegex(issuerRefParam) + "/acme",
"issuer/" + framework.GenericNameRegex(issuerRefParam) + "/roles/" + framework.GenericNameRegex("role") + "/acme",
} {
if !strings.HasPrefix(acmeApi, "/") {
acmeApi = "/" + acmeApi
}
path := patternFunc(b, baseUrl+acmeApi)
patterns = append(patterns, path)
}
return patterns
}
func getAcmeBaseUrl(sc *storageContext, r *logical.Request) (*url.URL, *url.URL, error) {
baseUrl, err := getBasePathFromClusterConfig(sc)
if err != nil {
return nil, nil, err
}
directoryPrefix, err := getAcmeDirectory(r)
if err != nil {
return nil, nil, err
}
return baseUrl.JoinPath(directoryPrefix), baseUrl, nil
}
func getBasePathFromClusterConfig(sc *storageContext) (*url.URL, error) {
cfg, err := sc.getClusterConfig()
if err != nil {
return nil, fmt.Errorf("failed loading cluster config: %w", err)
}
if cfg.Path == "" {
return nil, fmt.Errorf("ACME feature requires local cluster 'path' field configuration to be set")
}
baseUrl, err := url.Parse(cfg.Path)
if err != nil {
return nil, fmt.Errorf("failed parsing URL configured in local cluster 'path' configuration: %s: %s",
cfg.Path, err.Error())
}
return baseUrl, nil
}
func getAcmeIssuer(sc *storageContext, issuerName string) (*issuerEntry, error) {
if issuerName == "" {
issuerName = defaultRef
}
issuerId, err := sc.resolveIssuerReference(issuerName)
if err != nil {
return nil, fmt.Errorf("%w: issuer does not exist", ErrMalformed)
}
issuer, err := sc.fetchIssuerById(issuerId)
if err != nil {
return nil, fmt.Errorf("issuer failed to load: %w", err)
}
if issuer.Usage.HasUsage(IssuanceUsage) && len(issuer.KeyID) > 0 {
return issuer, nil
}
return nil, fmt.Errorf("%w: issuer missing proper issuance usage or key", ErrServerInternal)
}
// getAcmeDirectory return the base acme directory path, without a leading '/' and including
// the trailing /acme/ folder which is the root of all our various directories
func getAcmeDirectory(r *logical.Request) (string, error) {
acmePath := r.Path
if !strings.HasPrefix(acmePath, "/") {
acmePath = "/" + acmePath
}
lastIndex := strings.LastIndex(acmePath, "/acme/")
if lastIndex == -1 {
return "", fmt.Errorf("%w: unable to determine acme base folder path: %s", ErrServerInternal, acmePath)
}
// Skip the leading '/' and return our base path with the /acme/
return strings.TrimLeft(acmePath[0:lastIndex]+"/acme/", "/"), nil
}
func getAcmeRoleAndIssuer(sc *storageContext, data *framework.FieldData, config *acmeConfigEntry) (*roleEntry, *issuerEntry, error) {
requestedIssuer := getRequestedAcmeIssuerFromPath(data)
requestedRole := getRequestedAcmeRoleFromPath(data)
issuerToLoad := requestedIssuer
var role *roleEntry
var err error
if len(requestedRole) == 0 { // Default Directory
policyType, err := getDefaultDirectoryPolicyType(config.DefaultDirectoryPolicy)
if err != nil {
return nil, nil, err
}
switch policyType {
case Forbid:
return nil, nil, fmt.Errorf("%w: default directory not allowed by ACME policy", ErrServerInternal)
case SignVerbatim:
role = buildSignVerbatimRoleWithNoData(&roleEntry{
Issuer: requestedIssuer,
NoStore: false,
Name: requestedRole,
})
case Role:
defaultRole, err := getDefaultDirectoryPolicyRole(config.DefaultDirectoryPolicy)
if err != nil {
return nil, nil, err
}
role, err = getAndValidateAcmeRole(sc, defaultRole)
if err != nil {
return nil, nil, err
}
}
} else { // Requested Role
role, err = getAndValidateAcmeRole(sc, requestedRole)
if err != nil {
return nil, nil, err
}
// Check the Requested Role is Allowed
allowAnyRole := len(config.AllowedRoles) == 1 && config.AllowedRoles[0] == "*"
if !allowAnyRole {
var foundRole bool
for _, name := range config.AllowedRoles {
if name == role.Name {
foundRole = true
break
}
}
if !foundRole {
return nil, nil, fmt.Errorf("%w: specified role not allowed by ACME policy", ErrServerInternal)
}
}
}
// If we haven't loaded an issuer directly from our path and the specified (or default)
// role does specify an issuer prefer the role's issuer rather than the default issuer.
if len(role.Issuer) > 0 && len(requestedIssuer) == 0 {
issuerToLoad = role.Issuer
}
issuer, err := getAcmeIssuer(sc, issuerToLoad)
if err != nil {
return nil, nil, err
}
allowAnyIssuer := len(config.AllowedIssuers) == 1 && config.AllowedIssuers[0] == "*"
if !allowAnyIssuer {
var foundIssuer bool
for index, name := range config.AllowedIssuers {
candidateId, err := sc.resolveIssuerReference(name)
if err != nil {
return nil, nil, fmt.Errorf("failed to resolve reference for allowed_issuer entry %d: %w", index, err)
}
if candidateId == issuer.ID {
foundIssuer = true
break
}
}
if !foundIssuer {
return nil, nil, fmt.Errorf("%w: specified issuer not allowed by ACME policy", ErrServerInternal)
}
}
// If not allowed in configuration, override ExtKeyUsage behavior to force it to only be
// ServerAuth within ACME issued certs
if !config.AllowRoleExtKeyUsage {
role.ExtKeyUsage = []string{"serverauth"}
role.ExtKeyUsageOIDs = []string{}
role.ServerFlag = true
role.ClientFlag = false
role.CodeSigningFlag = false
role.EmailProtectionFlag = false
}
return role, issuer, nil
}
func getAndValidateAcmeRole(sc *storageContext, requestedRole string) (*roleEntry, error) {
var err error
role, err := sc.Backend.getRole(sc.Context, sc.Storage, requestedRole)
if err != nil {
return nil, fmt.Errorf("%w: err loading role", ErrServerInternal)
}
if role == nil {
return nil, fmt.Errorf("%w: role does not exist", ErrMalformed)
}
if role.NoStore {
return nil, fmt.Errorf("%w: role can not be used as NoStore is set to true", ErrServerInternal)
}
return role, nil
}
func getRequestedAcmeRoleFromPath(data *framework.FieldData) string {
requestedRole := ""
roleNameRaw, present := data.GetOk("role")
if present {
requestedRole = roleNameRaw.(string)
}
return requestedRole
}
func getRequestedAcmeIssuerFromPath(data *framework.FieldData) string {
requestedIssuer := ""
requestedIssuerRaw, present := data.GetOk(issuerRefParam)
if present {
requestedIssuer = requestedIssuerRaw.(string)
}
return requestedIssuer
}
func isAcmeDisabled(sc *storageContext, config *acmeConfigEntry, policy EabPolicy) bool {
if !config.Enabled {
return true
}
disableAcme, nonFatalErr := isPublicACMEDisabledByEnv()
if nonFatalErr != nil {
sc.Backend.Logger().Warn(fmt.Sprintf("could not parse env var '%s'", disableAcmeEnvVar), "error", nonFatalErr)
}
// The OS environment if true will override any configuration option.
if disableAcme {
if policy.OverrideEnvDisablingPublicAcme() {
return false
}
return true
}
return false
}