forked from adamlaska/boulder
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathverify.go
More file actions
809 lines (732 loc) · 32.7 KB
/
verify.go
File metadata and controls
809 lines (732 loc) · 32.7 KB
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
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
package wfe2
import (
"context"
"crypto/ecdsa"
"crypto/rsa"
"encoding/json"
"errors"
"fmt"
"io/ioutil"
"net/http"
"net/url"
"strconv"
"strings"
"github.com/honeycombio/beeline-go"
"github.com/prometheus/client_golang/prometheus"
"gopkg.in/square/go-jose.v2"
"github.com/letsencrypt/boulder/core"
berrors "github.com/letsencrypt/boulder/errors"
"github.com/letsencrypt/boulder/grpc"
"github.com/letsencrypt/boulder/nonce"
"github.com/letsencrypt/boulder/probs"
sapb "github.com/letsencrypt/boulder/sa/proto"
"github.com/letsencrypt/boulder/web"
)
const (
// POST requests with a JWS body must have the following Content-Type header
expectedJWSContentType = "application/jose+json"
maxRequestSize = 50000
)
func sigAlgorithmForKey(key *jose.JSONWebKey) (jose.SignatureAlgorithm, error) {
switch k := key.Key.(type) {
case *rsa.PublicKey:
return jose.RS256, nil
case *ecdsa.PublicKey:
switch k.Params().Name {
case "P-256":
return jose.ES256, nil
case "P-384":
return jose.ES384, nil
case "P-521":
return jose.ES512, nil
}
}
return "", errors.New("JWK contains unsupported key type (expected RSA, or ECDSA P-256, P-384, or P-521")
}
var supportedAlgs = map[string]bool{
string(jose.RS256): true,
string(jose.ES256): true,
string(jose.ES384): true,
string(jose.ES512): true,
}
// Check that (1) there is a suitable algorithm for the provided key based on its
// Golang type, (2) the Algorithm field on the JWK is either absent, or matches
// that algorithm, and (3) the Algorithm field on the JWK is present and matches
// that algorithm. Precondition: parsedJws must have exactly one signature on
// it.
func checkAlgorithm(key *jose.JSONWebKey, parsedJWS *jose.JSONWebSignature) error {
sigHeaderAlg := parsedJWS.Signatures[0].Header.Algorithm
if !supportedAlgs[sigHeaderAlg] {
return fmt.Errorf(
"JWS signature header contains unsupported algorithm %q, expected one of RS256, ES256, ES384 or ES512",
parsedJWS.Signatures[0].Header.Algorithm,
)
}
expectedAlg, err := sigAlgorithmForKey(key)
if err != nil {
return err
}
if sigHeaderAlg != string(expectedAlg) {
return fmt.Errorf("JWS signature header algorithm %q does not match expected algorithm %q for JWK", sigHeaderAlg, string(expectedAlg))
}
if key.Algorithm != "" && key.Algorithm != string(expectedAlg) {
return fmt.Errorf("JWK key header algorithm %q does not match expected algorithm %q for JWK", key.Algorithm, string(expectedAlg))
}
return nil
}
// jwsAuthType represents whether a given POST request is authenticated using
// a JWS with an embedded JWK (v1 ACME style, new-account, revoke-cert) or an
// embedded Key ID (v2 AMCE style) or an unsupported/unknown auth type.
type jwsAuthType int
const (
embeddedJWK jwsAuthType = iota
embeddedKeyID
invalidAuthType
)
// checkJWSAuthType examines a JWS' protected headers to determine if
// the request being authenticated by the JWS is identified using an embedded
// JWK or an embedded key ID. If no signatures are present, or mutually
// exclusive authentication types are specified at the same time, a problem is
// returned. checkJWSAuthType is separate from enforceJWSAuthType so that
// endpoints that need to handle both embedded JWK and embedded key ID requests
// can determine which type of request they have and act accordingly (e.g.
// acme v2 cert revocation).
func checkJWSAuthType(jws *jose.JSONWebSignature) (jwsAuthType, *probs.ProblemDetails) {
// checkJWSAuthType is called after parseJWS() which defends against the
// incorrect number of signatures.
header := jws.Signatures[0].Header
// There must not be a Key ID *and* an embedded JWK
if header.KeyID != "" && header.JSONWebKey != nil {
return invalidAuthType, probs.Malformed(
"jwk and kid header fields are mutually exclusive")
} else if header.KeyID != "" {
return embeddedKeyID, nil
} else if header.JSONWebKey != nil {
return embeddedJWK, nil
}
return invalidAuthType, nil
}
// enforceJWSAuthType enforces a provided JWS has the provided auth type. If there
// is an error determining the auth type or if it is not the expected auth type
// then a problem is returned.
func (wfe *WebFrontEndImpl) enforceJWSAuthType(
jws *jose.JSONWebSignature,
expectedAuthType jwsAuthType) *probs.ProblemDetails {
// Check the auth type for the provided JWS
authType, prob := checkJWSAuthType(jws)
if prob != nil {
wfe.stats.joseErrorCount.With(prometheus.Labels{"type": "JWSAuthTypeInvalid"}).Inc()
return prob
}
// If the auth type isn't the one expected return a sensible problem based on
// what was expected
if authType != expectedAuthType {
wfe.stats.joseErrorCount.With(prometheus.Labels{"type": "JWSAuthTypeWrong"}).Inc()
switch expectedAuthType {
case embeddedKeyID:
return probs.Malformed("No Key ID in JWS header")
case embeddedJWK:
return probs.Malformed("No embedded JWK in JWS header")
}
}
return nil
}
// validPOSTRequest checks a *http.Request to ensure it has the headers
// a well-formed ACME POST request has, and to ensure there is a body to
// process.
func (wfe *WebFrontEndImpl) validPOSTRequest(request *http.Request) *probs.ProblemDetails {
// All POSTs should have an accompanying Content-Length header
if _, present := request.Header["Content-Length"]; !present {
wfe.stats.httpErrorCount.With(prometheus.Labels{"type": "ContentLengthRequired"}).Inc()
return probs.ContentLengthRequired()
}
// Per 6.2 ALL POSTs should have the correct JWS Content-Type for flattened
// JSON serialization.
if _, present := request.Header["Content-Type"]; !present {
wfe.stats.httpErrorCount.With(prometheus.Labels{"type": "NoContentType"}).Inc()
return probs.InvalidContentType(fmt.Sprintf("No Content-Type header on POST. Content-Type must be %q",
expectedJWSContentType))
}
if contentType := request.Header.Get("Content-Type"); contentType != expectedJWSContentType {
wfe.stats.httpErrorCount.With(prometheus.Labels{"type": "WrongContentType"}).Inc()
return probs.InvalidContentType(fmt.Sprintf("Invalid Content-Type header on POST. Content-Type must be %q",
expectedJWSContentType))
}
// Per 6.4.1 "Replay-Nonce" clients should not send a Replay-Nonce header in
// the HTTP request, it needs to be part of the signed JWS request body
if _, present := request.Header["Replay-Nonce"]; present {
wfe.stats.httpErrorCount.With(prometheus.Labels{"type": "ReplayNonceOutsideJWS"}).Inc()
return probs.Malformed("HTTP requests should NOT contain Replay-Nonce header. Use JWS nonce field")
}
// All POSTs should have a non-nil body
if request.Body == nil {
wfe.stats.httpErrorCount.With(prometheus.Labels{"type": "NoPOSTBody"}).Inc()
return probs.Malformed("No body on POST")
}
return nil
}
// validNonce checks a JWS' Nonce header to ensure it is one that the
// nonceService knows about, otherwise a bad nonce problem is returned.
// NOTE: this function assumes the JWS has already been verified with the
// correct public key.
func (wfe *WebFrontEndImpl) validNonce(ctx context.Context, jws *jose.JSONWebSignature) *probs.ProblemDetails {
// validNonce is called after validPOSTRequest() and parseJWS() which
// defend against the incorrect number of signatures.
header := jws.Signatures[0].Header
if len(header.Nonce) == 0 {
wfe.stats.joseErrorCount.With(prometheus.Labels{"type": "JWSMissingNonce"}).Inc()
return probs.BadNonce("JWS has no anti-replay nonce")
}
var nonceValid bool
if wfe.remoteNonceService != nil {
valid, err := nonce.RemoteRedeem(ctx, wfe.noncePrefixMap, header.Nonce)
if err != nil {
return probs.ServerInternal(fmt.Sprintf("failed to verify nonce validity: %s", err))
}
nonceValid = valid
} else {
nonceValid = wfe.nonceService.Valid(header.Nonce)
}
if !nonceValid {
wfe.stats.joseErrorCount.With(prometheus.Labels{"type": "JWSInvalidNonce"}).Inc()
return probs.BadNonce(fmt.Sprintf("JWS has an invalid anti-replay nonce: %q", header.Nonce))
}
return nil
}
// validPOSTURL checks the JWS' URL header against the expected URL based on the
// HTTP request. This prevents a JWS intended for one endpoint being replayed
// against a different endpoint. If the URL isn't present, is invalid, or
// doesn't match the HTTP request a problem is returned.
func (wfe *WebFrontEndImpl) validPOSTURL(
request *http.Request,
jws *jose.JSONWebSignature) *probs.ProblemDetails {
// validPOSTURL is called after parseJWS() which defends against the incorrect
// number of signatures.
header := jws.Signatures[0].Header
extraHeaders := header.ExtraHeaders
// Check that there is at least one Extra Header
if len(extraHeaders) == 0 {
wfe.stats.joseErrorCount.With(prometheus.Labels{"type": "JWSNoExtraHeaders"}).Inc()
return probs.Malformed("JWS header parameter 'url' required")
}
// Try to read a 'url' Extra Header as a string
headerURL, ok := extraHeaders[jose.HeaderKey("url")].(string)
if !ok || len(headerURL) == 0 {
wfe.stats.joseErrorCount.With(prometheus.Labels{"type": "JWSMissingURL"}).Inc()
return probs.Malformed("JWS header parameter 'url' required")
}
// Compute the URL we expect to be in the JWS based on the HTTP request
expectedURL := url.URL{
Scheme: requestProto(request),
Host: request.Host,
Path: request.RequestURI,
}
// Check that the URL we expect is the one that was found in the signed JWS
// header
if expectedURL.String() != headerURL {
wfe.stats.joseErrorCount.With(prometheus.Labels{"type": "JWSMismatchedURL"}).Inc()
return probs.Malformed(fmt.Sprintf(
"JWS header parameter 'url' incorrect. Expected %q got %q",
expectedURL.String(), headerURL))
}
return nil
}
// matchJWSURLs checks two JWS' URL headers are equal. This is used during key
// rollover to check that the inner JWS URL matches the outer JWS URL. If the
// JWS URLs do not match a problem is returned.
func (wfe *WebFrontEndImpl) matchJWSURLs(outer, inner *jose.JSONWebSignature) *probs.ProblemDetails {
// Verify that the outer JWS has a non-empty URL header. This is strictly
// defensive since the expectation is that endpoints using `matchJWSURLs`
// have received at least one of their JWS from calling validPOSTForAccount(),
// which checks the outer JWS has the expected URL header before processing
// the inner JWS.
outerURL, ok := outer.Signatures[0].Header.ExtraHeaders[jose.HeaderKey("url")].(string)
if !ok || len(outerURL) == 0 {
wfe.stats.joseErrorCount.With(prometheus.Labels{"type": "KeyRolloverOuterJWSNoURL"}).Inc()
return probs.Malformed("Outer JWS header parameter 'url' required")
}
// Verify the inner JWS has a non-empty URL header.
innerURL, ok := inner.Signatures[0].Header.ExtraHeaders[jose.HeaderKey("url")].(string)
if !ok || len(innerURL) == 0 {
wfe.stats.joseErrorCount.With(prometheus.Labels{"type": "KeyRolloverInnerJWSNoURL"}).Inc()
return probs.Malformed("Inner JWS header parameter 'url' required")
}
// Verify that the outer URL matches the inner URL
if outerURL != innerURL {
wfe.stats.joseErrorCount.With(prometheus.Labels{"type": "KeyRolloverMismatchedURLs"}).Inc()
return probs.Malformed(fmt.Sprintf(
"Outer JWS 'url' value %q does not match inner JWS 'url' value %q",
outerURL, innerURL))
}
return nil
}
// parseJWS extracts a JSONWebSignature from a byte slice. If there is an error
// reading the JWS or it is unacceptable (e.g. too many/too few signatures,
// presence of unprotected headers) a problem is returned, otherwise the parsed
// *JSONWebSignature is returned.
func (wfe *WebFrontEndImpl) parseJWS(body []byte) (*jose.JSONWebSignature, *probs.ProblemDetails) {
// Parse the raw JWS JSON to check that:
// * the unprotected Header field is not being used.
// * the "signatures" member isn't present, just "signature".
//
// This must be done prior to `jose.parseSigned` since it will strip away
// these headers.
var unprotected struct {
Header map[string]string
Signatures []interface{}
}
err := json.Unmarshal(body, &unprotected)
if err != nil {
wfe.stats.joseErrorCount.With(prometheus.Labels{"type": "JWSUnmarshalFailed"}).Inc()
return nil, probs.Malformed("Parse error reading JWS")
}
// ACME v2 never uses values from the unprotected JWS header. Reject JWS that
// include unprotected headers.
if unprotected.Header != nil {
wfe.stats.joseErrorCount.With(prometheus.Labels{"type": "JWSUnprotectedHeaders"}).Inc()
return nil, probs.Malformed(
"JWS \"header\" field not allowed. All headers must be in \"protected\" field")
}
// ACME v2 never uses the "signatures" array of JSON serialized JWS, just the
// mandatory "signature" field. Reject JWS that include the "signatures" array.
if len(unprotected.Signatures) > 0 {
wfe.stats.joseErrorCount.With(prometheus.Labels{"type": "JWSMultiSig"}).Inc()
return nil, probs.Malformed(
"JWS \"signatures\" field not allowed. Only the \"signature\" field should contain a signature")
}
// Parse the JWS using go-jose and enforce that the expected one non-empty
// signature is present in the parsed JWS.
bodyStr := string(body)
parsedJWS, err := jose.ParseSigned(bodyStr)
if err != nil {
wfe.stats.joseErrorCount.With(prometheus.Labels{"type": "JWSParseError"}).Inc()
if strings.HasPrefix(err.Error(), "failed to unmarshal JWK: square/go-jose: invalid EC public key, wrong length") {
return nil, probs.Malformed("Parse error reading JWS: EC public key has incorrect padding")
}
return nil, probs.Malformed("Parse error reading JWS")
}
if len(parsedJWS.Signatures) > 1 {
wfe.stats.joseErrorCount.With(prometheus.Labels{"type": "JWSTooManySignatures"}).Inc()
return nil, probs.Malformed("Too many signatures in POST body")
}
if len(parsedJWS.Signatures) == 0 {
wfe.stats.joseErrorCount.With(prometheus.Labels{"type": "JWSNoSignatures"}).Inc()
return nil, probs.Malformed("POST JWS not signed")
}
if len(parsedJWS.Signatures) == 1 && len(parsedJWS.Signatures[0].Signature) == 0 {
wfe.stats.joseErrorCount.With(prometheus.Labels{"type": "JWSEmptySignature"}).Inc()
return nil, probs.Malformed("POST JWS not signed")
}
return parsedJWS, nil
}
// parseJWSRequest extracts a JSONWebSignature from an HTTP POST request's body using parseJWS.
func (wfe *WebFrontEndImpl) parseJWSRequest(request *http.Request) (*jose.JSONWebSignature, *probs.ProblemDetails) {
// Verify that the POST request has the expected headers
if prob := wfe.validPOSTRequest(request); prob != nil {
return nil, prob
}
// Read the POST request body's bytes. validPOSTRequest has already checked
// that the body is non-nil
bodyBytes, err := ioutil.ReadAll(http.MaxBytesReader(nil, request.Body, maxRequestSize))
if err != nil {
if err.Error() == "http: request body too large" {
return nil, probs.Unauthorized("request body too large")
}
wfe.stats.httpErrorCount.With(prometheus.Labels{"type": "UnableToReadReqBody"}).Inc()
return nil, probs.ServerInternal("unable to read request body")
}
jws, prob := wfe.parseJWS(bodyBytes)
if prob != nil {
return nil, prob
}
return jws, nil
}
// extractJWK extracts a JWK from a provided JWS or returns a problem. It
// expects that the JWS is using the embedded JWK style of authentication and
// does not contain an embedded Key ID. Callers should have acquired the
// provided JWS from parseJWS to ensure it has the correct number of signatures
// present.
func (wfe *WebFrontEndImpl) extractJWK(jws *jose.JSONWebSignature) (*jose.JSONWebKey, *probs.ProblemDetails) {
// extractJWK expects the request to be using an embedded JWK auth type and
// to not contain the mutually exclusive KeyID.
if prob := wfe.enforceJWSAuthType(jws, embeddedJWK); prob != nil {
return nil, prob
}
// extractJWK must be called after parseJWS() which defends against the
// incorrect number of signatures.
header := jws.Signatures[0].Header
// We can be sure that JSONWebKey is != nil because we have already called
// enforceJWSAuthType()
key := header.JSONWebKey
// If the key isn't considered valid by go-jose return a problem immediately
if !key.Valid() {
wfe.stats.joseErrorCount.With(prometheus.Labels{"type": "JWKInvalid"}).Inc()
return nil, probs.Malformed("Invalid JWK in JWS header")
}
return key, nil
}
// acctIDFromURL extracts the numeric int64 account ID from a ACMEv1 or ACMEv2
// account URL. If the acctURL has an invalid URL or the account ID in the
// acctURL is non-numeric a MalformedProblem is returned.
func (wfe *WebFrontEndImpl) acctIDFromURL(acctURL string, request *http.Request) (int64, *probs.ProblemDetails) {
// For normal ACME v2 accounts we expect the account URL has a prefix composed
// of the Host header and the acctPath.
expectedURLPrefix := web.RelativeEndpoint(request, acctPath)
// Process the acctURL to find only the trailing numeric account ID. Both the
// expected URL prefix and a legacy URL prefix are permitted in order to allow
// ACME v1 clients to use legacy accounts with unmodified account URLs for V2
// requests.
var accountIDStr string
if strings.HasPrefix(acctURL, expectedURLPrefix) {
accountIDStr = strings.TrimPrefix(acctURL, expectedURLPrefix)
} else if strings.HasPrefix(acctURL, wfe.LegacyKeyIDPrefix) {
accountIDStr = strings.TrimPrefix(acctURL, wfe.LegacyKeyIDPrefix)
} else {
return 0, probs.Malformed(
fmt.Sprintf("KeyID header contained an invalid account URL: %q", acctURL))
}
// Convert the raw account ID string to an int64 for use with the SA's
// GetRegistration RPC
accountID, err := strconv.ParseInt(accountIDStr, 10, 64)
if err != nil {
return 0, probs.Malformed("Malformed account ID in KeyID header URL: %q", acctURL)
}
return accountID, nil
}
// lookupJWK finds a JWK associated with the Key ID present in a provided JWS,
// returning the JWK and a pointer to the associated account, or a problem. It
// expects that the JWS is using the embedded Key ID style of authentication
// and does not contain an embedded JWK. Callers should have acquired the
// provided JWS from parseJWS to ensure it has the correct number of signatures
// present.
func (wfe *WebFrontEndImpl) lookupJWK(
jws *jose.JSONWebSignature,
ctx context.Context,
request *http.Request,
logEvent *web.RequestEvent) (*jose.JSONWebKey, *core.Registration, *probs.ProblemDetails) {
// We expect the request to be using an embedded Key ID auth type and to not
// contain the mutually exclusive embedded JWK.
if prob := wfe.enforceJWSAuthType(jws, embeddedKeyID); prob != nil {
return nil, nil, prob
}
header := jws.Signatures[0].Header
accountURL := header.KeyID
accountID, prob := wfe.acctIDFromURL(accountURL, request)
if prob != nil {
wfe.stats.joseErrorCount.With(prometheus.Labels{"type": "JWSInvalidKeyID"}).Inc()
return nil, nil, prob
}
// Try to find the account for this account ID
account, err := wfe.accountGetter.GetRegistration(ctx, &sapb.RegistrationID{Id: accountID})
if err != nil {
// If the account isn't found, return a suitable problem
if errors.Is(err, berrors.NotFound) {
wfe.stats.joseErrorCount.With(prometheus.Labels{"type": "JWSKeyIDNotFound"}).Inc()
return nil, nil, probs.AccountDoesNotExist(fmt.Sprintf(
"Account %q not found", accountURL))
}
// If there was an error and it isn't a "Not Found" error, return
// a ServerInternal problem since this is unexpected.
wfe.stats.joseErrorCount.With(prometheus.Labels{"type": "JWSKeyIDLookupFailed"}).Inc()
// Add an error to the log event with the internal error message
logEvent.AddError(fmt.Sprintf("Error calling SA.GetRegistration: %s", err.Error()))
return nil, nil, probs.ServerInternal(fmt.Sprintf(
"Error retrieving account %q", accountURL))
}
// Verify the account is not deactivated
if core.AcmeStatus(account.Status) != core.StatusValid {
wfe.stats.joseErrorCount.With(prometheus.Labels{"type": "JWSKeyIDAccountInvalid"}).Inc()
return nil, nil, probs.Unauthorized(
fmt.Sprintf("Account is not valid, has status %q", account.Status))
}
// Update the logEvent with the account information and return the JWK
logEvent.Requester = account.Id
beeline.AddFieldToTrace(ctx, "acct.id", account.Id)
if account.Contact != nil {
logEvent.Contacts = account.Contact
beeline.AddFieldToTrace(ctx, "contacts", account.Contact)
}
acct, err := grpc.PbToRegistration(account)
if err != nil {
return nil, nil, probs.ServerInternal(fmt.Sprintf(
"Error unmarshalling account %q", accountURL))
}
return acct.Key, &acct, nil
}
// validJWSForKey checks a provided JWS for a given HTTP request validates
// correctly using the provided JWK. If the JWS verifies the protected payload
// is returned. The key/JWS algorithms are verified and
// the JWK is checked against the keyPolicy before any signature validation is
// done. If the JWS signature validates correctly then the JWS nonce value
// and the JWS URL are verified to ensure that they are correct.
func (wfe *WebFrontEndImpl) validJWSForKey(
ctx context.Context,
jws *jose.JSONWebSignature,
jwk *jose.JSONWebKey,
request *http.Request,
logEvent *web.RequestEvent) ([]byte, *probs.ProblemDetails) {
// Check that the public key and JWS algorithms match expected
err := checkAlgorithm(jwk, jws)
if err != nil {
wfe.stats.joseErrorCount.With(prometheus.Labels{"type": "JWSAlgorithmCheckFailed"}).Inc()
return nil, probs.BadSignatureAlgorithm(err.Error())
}
// Verify the JWS signature with the public key.
// NOTE: It might seem insecure for the WFE to be trusted to verify
// client requests, i.e., that the verification should be done at the
// RA. However the WFE is the RA's only view of the outside world
// *anyway*, so it could always lie about what key was used by faking
// the signature itself.
payload, err := jws.Verify(jwk)
if err != nil {
wfe.stats.joseErrorCount.With(prometheus.Labels{"type": "JWSVerifyFailed"}).Inc()
return nil, probs.Malformed("JWS verification error")
}
// Store the verified payload in the logEvent
logEvent.Payload = string(payload)
beeline.AddFieldToTrace(ctx, "payload", string(payload))
// Check that the JWS contains a correct Nonce header
if prob := wfe.validNonce(ctx, jws); prob != nil {
return nil, prob
}
// Check that the HTTP request URL matches the URL in the signed JWS
if prob := wfe.validPOSTURL(request, jws); prob != nil {
return nil, prob
}
// In the WFE1 package the check for the request URL required unmarshalling
// the payload JSON to check the "resource" field of the protected JWS body.
// This caught invalid JSON early and so we preserve this check by explicitly
// trying to unmarshal the payload (when it is non-empty to allow POST-as-GET
// behaviour) as part of the verification and failing early if it isn't valid JSON.
var parsedBody struct{}
err = json.Unmarshal(payload, &parsedBody)
if string(payload) != "" && err != nil {
wfe.stats.joseErrorCount.With(prometheus.Labels{"type": "JWSBodyUnmarshalFailed"}).Inc()
return nil, probs.Malformed("Request payload did not parse as JSON")
}
return payload, nil
}
// validJWSForAccount checks that a given JWS is valid and verifies with the
// public key associated to a known account specified by the JWS Key ID. If the
// JWS is valid (e.g. the JWS is well formed, verifies with the JWK stored for the
// specified key ID, specifies the correct URL, and has a valid nonce) then
// `validJWSForAccount` returns the validated JWS body, the parsed
// JSONWebSignature, and a pointer to the JWK's associated account. If any of
// these conditions are not met or an error occurs only a problem is returned.
func (wfe *WebFrontEndImpl) validJWSForAccount(
jws *jose.JSONWebSignature,
request *http.Request,
ctx context.Context,
logEvent *web.RequestEvent) ([]byte, *jose.JSONWebSignature, *core.Registration, *probs.ProblemDetails) {
// Lookup the account and JWK for the key ID that authenticated the JWS
pubKey, account, prob := wfe.lookupJWK(jws, ctx, request, logEvent)
if prob != nil {
return nil, nil, nil, prob
}
// Verify the JWS with the JWK from the SA
payload, prob := wfe.validJWSForKey(ctx, jws, pubKey, request, logEvent)
if prob != nil {
return nil, nil, nil, prob
}
return payload, jws, account, nil
}
// validPOSTForAccount checks that a given POST request has a valid JWS
// using `validJWSForAccount`. If valid, the authenticated JWS body and the
// registration that authenticated the body are returned. Otherwise a problem is
// returned. The returned JWS body may be empty if the request is a POST-as-GET
// request.
func (wfe *WebFrontEndImpl) validPOSTForAccount(
request *http.Request,
ctx context.Context,
logEvent *web.RequestEvent) ([]byte, *jose.JSONWebSignature, *core.Registration, *probs.ProblemDetails) {
// Parse the JWS from the POST request
jws, prob := wfe.parseJWSRequest(request)
if prob != nil {
return nil, nil, nil, prob
}
return wfe.validJWSForAccount(jws, request, ctx, logEvent)
}
// validPOSTAsGETForAccount checks that a given POST request is valid using
// `validPOSTForAccount`. It additionally validates that the JWS request payload
// is empty, indicating that it is a POST-as-GET request per ACME draft 15+
// section 6.3 "GET and POST-as-GET requests". If a non empty payload is
// provided in the JWS the invalidPOSTAsGETErr problem is returned. This
// function is useful only for endpoints that do not need to handle both POSTs
// with a body and POST-as-GET requests (e.g. Order, Certificate).
func (wfe *WebFrontEndImpl) validPOSTAsGETForAccount(
request *http.Request,
ctx context.Context,
logEvent *web.RequestEvent) (*core.Registration, *probs.ProblemDetails) {
// Call validPOSTForAccount to verify the JWS and extract the body.
body, _, reg, prob := wfe.validPOSTForAccount(request, ctx, logEvent)
if prob != nil {
return nil, prob
}
// Verify the POST-as-GET payload is empty
if string(body) != "" {
return nil, probs.Malformed("POST-as-GET requests must have an empty payload")
}
// To make log analysis easier we choose to elevate the pseudo ACME HTTP
// method "POST-as-GET" to the logEvent's Method, replacing the
// http.MethodPost value.
logEvent.Method = "POST-as-GET"
beeline.AddFieldToTrace(ctx, "method", "POST-as-GET")
return reg, prob
}
// validSelfAuthenticatedJWS checks that a given JWS verifies with the JWK
// embedded in the JWS itself (e.g. self-authenticated). This type of JWS
// is only used for creating new accounts or revoking a certificate by signing
// the request with the private key corresponding to the certificate's public
// key and embedding that public key in the JWS. All other request should be
// validated using `validJWSforAccount`.
// If the JWS validates (e.g. the JWS is well formed, verifies with the JWK
// embedded in it, has the correct URL, and includes a valid nonce) then
// `validSelfAuthenticatedJWS` returns the validated JWS body and the JWK that
// was embedded in the JWS. Otherwise if the valid JWS conditions are not met or
// an error occurs only a problem is returned.
// Note that this function does *not* enforce that the JWK abides by our goodkey
// policies. This is because this method is used by the RevokeCertificate path,
// which must allow JWKs which are signed by blocklisted (i.e. already revoked
// due to compromise) keys, in case multiple clients attempt to revoke the same
// cert.
func (wfe *WebFrontEndImpl) validSelfAuthenticatedJWS(
ctx context.Context,
jws *jose.JSONWebSignature,
request *http.Request,
logEvent *web.RequestEvent) ([]byte, *jose.JSONWebKey, *probs.ProblemDetails) {
// Extract the embedded JWK from the parsed JWS
pubKey, prob := wfe.extractJWK(jws)
if prob != nil {
return nil, nil, prob
}
// Verify the JWS with the embedded JWK
payload, prob := wfe.validJWSForKey(ctx, jws, pubKey, request, logEvent)
if prob != nil {
return nil, nil, prob
}
return payload, pubKey, nil
}
// validSelfAuthenticatedPOST checks that a given POST request has a valid JWS
// using `validSelfAuthenticatedJWS`. It enforces that the JWK abides by our
// goodkey policies (key algorithm, length, blocklist, etc).
func (wfe *WebFrontEndImpl) validSelfAuthenticatedPOST(
ctx context.Context,
request *http.Request,
logEvent *web.RequestEvent) ([]byte, *jose.JSONWebKey, *probs.ProblemDetails) {
// Parse the JWS from the POST request
jws, prob := wfe.parseJWSRequest(request)
if prob != nil {
return nil, nil, prob
}
// Extract and validate the embedded JWK from the parsed JWS
payload, pubKey, prob := wfe.validSelfAuthenticatedJWS(ctx, jws, request, logEvent)
if prob != nil {
return nil, nil, prob
}
// If the key doesn't meet the GoodKey policy return a problem
err := wfe.keyPolicy.GoodKey(ctx, pubKey.Key)
if err != nil {
wfe.stats.joseErrorCount.With(prometheus.Labels{"type": "JWKRejectedByGoodKey"}).Inc()
return nil, nil, probs.BadPublicKey(err.Error())
}
return payload, pubKey, nil
}
// rolloverRequest is a client request to change the key for the account ID
// provided from the specified old key to a new key (the embedded JWK in the
// inner JWS).
type rolloverRequest struct {
OldKey jose.JSONWebKey
Account string
}
// rolloverOperation is a struct representing a requested rollover operation
// from the specified old key to the new key for the given account ID.
type rolloverOperation struct {
rolloverRequest
NewKey jose.JSONWebKey
}
// validKeyRollover checks if the innerJWS is a valid key rollover operation
// given the outer JWS that carried it. It is assumed that the outerJWS has
// already been validated per the normal ACME process using `validPOSTForAccount`.
// It is *critical* this is the case since `validKeyRollover` does not check the
// outerJWS signature. This function checks that:
// 1) the inner JWS is valid and well formed
// 2) the inner JWS has the same "url" header as the outer JWS
// 3) the inner JWS is self-authenticated with an embedded JWK
//
// This function verifies that the inner JWS' body is a rolloverRequest instance
// that specifies the correct oldKey. The returned rolloverOperation's NewKey
// field will be set to the JWK from the inner JWS.
//
// If the request is valid a *rolloverOperation object is returned,
// otherwise a problem is returned. The caller is left to verify
// whether the new key is appropriate (e.g. isn't being used by another existing
// account) and that the account field of the rollover object matches the
// account that verified the outer JWS.
func (wfe *WebFrontEndImpl) validKeyRollover(
ctx context.Context,
outerJWS *jose.JSONWebSignature,
innerJWS *jose.JSONWebSignature,
oldKey *jose.JSONWebKey,
logEvent *web.RequestEvent) (*rolloverOperation, *probs.ProblemDetails) {
// Extract the embedded JWK from the inner JWS
jwk, prob := wfe.extractJWK(innerJWS)
if prob != nil {
return nil, prob
}
// If the key doesn't meet the GoodKey policy return a problem immediately
err := wfe.keyPolicy.GoodKey(ctx, jwk.Key)
if err != nil {
wfe.stats.joseErrorCount.With(prometheus.Labels{"type": "KeyRolloverJWKRejectedByGoodKey"}).Inc()
return nil, probs.BadPublicKey(err.Error())
}
// Check that the public key and JWS algorithms match expected
err = checkAlgorithm(jwk, innerJWS)
if err != nil {
return nil, probs.Malformed(err.Error())
}
// Verify the inner JWS signature with the public key from the embedded JWK.
// NOTE(@cpu): We do not use `wfe.validJWSForKey` here because the inner JWS
// of a key rollover operation is special (e.g. has no nonce, doesn't have an
// HTTP request to match the URL to)
innerPayload, err := innerJWS.Verify(jwk)
if err != nil {
wfe.stats.joseErrorCount.With(prometheus.Labels{"type": "KeyRolloverJWSVerifyFailed"}).Inc()
return nil, probs.Malformed("Inner JWS does not verify with embedded JWK")
}
// NOTE(@cpu): we do not stomp the web.RequestEvent's payload here since that is set
// from the outerJWS in validPOSTForAccount and contains the inner JWS and inner
// payload already.
// Verify that the outer and inner JWS protected URL headers match
if prob := wfe.matchJWSURLs(outerJWS, innerJWS); prob != nil {
return nil, prob
}
var req rolloverRequest
if json.Unmarshal(innerPayload, &req) != nil {
wfe.stats.joseErrorCount.With(prometheus.Labels{"type": "KeyRolloverUnmarshalFailed"}).Inc()
return nil, probs.Malformed(
"Inner JWS payload did not parse as JSON key rollover object")
}
// If there's no oldkey specified fail before trying to use
// core.PublicKeyEqual on a nil argument.
if req.OldKey.Key == nil {
wfe.stats.joseErrorCount.With(prometheus.Labels{"type": "KeyRolloverWrongOldKey"}).Inc()
return nil, probs.Malformed("Inner JWS does not contain old key field matching current account key")
}
// We must validate that the inner JWS' rollover request specifies the correct
// oldKey.
if keysEqual, err := core.PublicKeysEqual(req.OldKey.Key, oldKey.Key); err != nil {
return nil, probs.Malformed("Unable to compare new and old keys: %s", err.Error())
} else if !keysEqual {
wfe.stats.joseErrorCount.With(prometheus.Labels{"type": "KeyRolloverWrongOldKey"}).Inc()
return nil, probs.Malformed("Inner JWS does not contain old key field matching current account key")
}
// Return a rolloverOperation populated with the validated old JWK, the
// requested account, and the new JWK extracted from the inner JWS.
return &rolloverOperation{
rolloverRequest: rolloverRequest{
OldKey: *oldKey,
Account: req.Account,
},
NewKey: *jwk,
}, nil
}