-
Notifications
You must be signed in to change notification settings - Fork 157
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Promote -strict behaviors to the default #203
Changes from all commits
3eeae33
014a484
c23f698
fe5d2f3
6597be9
601e43d
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -73,11 +73,6 @@ const ( | |
// (https://tools.ietf.org/html/draft-ietf-acme-tls-alpn-04#page-4) | ||
var IdPeAcmeIdentifier = asn1.ObjectIdentifier{1, 3, 6, 1, 5, 5, 7, 1, 31} | ||
|
||
// This is the identifier defined in draft-01. This is only supported for backwards | ||
// compatibility. | ||
// (https://tools.ietf.org/html/draft-ietf-acme-tls-alpn-01#page-4) | ||
var IdPeAcmeIdentifierV1Obsolete = asn1.ObjectIdentifier{1, 3, 6, 1, 5, 5, 7, 1, 30, 1} | ||
|
||
func userAgent() string { | ||
return fmt.Sprintf( | ||
"%s (%s; %s)", | ||
|
@@ -396,11 +391,6 @@ func (va VAImpl) validateTLSALPN01(task *vaTask) *core.ValidationRecord { | |
for _, ext := range leafCert.Extensions { | ||
if ext.Critical { | ||
hasAcmeIdentifier := IdPeAcmeIdentifier.Equal(ext.Id) | ||
// For backwards compatibility, check old identifier | ||
// as well if strict mode is not enabled | ||
if !va.strict && IdPeAcmeIdentifierV1Obsolete.Equal(ext.Id) { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. You can also delete |
||
hasAcmeIdentifier = true | ||
} | ||
if hasAcmeIdentifier { | ||
var extValue []byte | ||
if _, err := asn1.Unmarshal(ext.Value, &extValue); err != nil { | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -270,17 +270,10 @@ func (wfe *WebFrontEndImpl) Handler() http.Handler { | |
wfe.HandleFunc(m, acctPath, wfe.UpdateAccount, "POST") | ||
wfe.HandleFunc(m, keyRolloverPath, wfe.KeyRollover, "POST") | ||
wfe.HandleFunc(m, revokeCertPath, wfe.RevokeCert, "POST") | ||
|
||
// GET and POST handlers | ||
wfe.HandleFunc(m, certPath, wfe.Certificate, "GET", "POST") | ||
|
||
// POST handlers with legacy GET support | ||
// NOTE(@cpu): These handlers will eventually not support GET. It is presently | ||
// allowed when `-strict=false` to support ACME <=draft-14 which allowed | ||
// unauthenticated GET access to these resources. | ||
wfe.HandleFunc(m, orderPath, wfe.Order, "GET", "POST") | ||
wfe.HandleFunc(m, authzPath, wfe.Authz, "GET", "POST") | ||
wfe.HandleFunc(m, challengePath, wfe.Challenge, "GET", "POST") | ||
wfe.HandleFunc(m, certPath, wfe.Certificate, "POST") | ||
wfe.HandleFunc(m, orderPath, wfe.Order, "POST") | ||
wfe.HandleFunc(m, authzPath, wfe.Authz, "POST") | ||
wfe.HandleFunc(m, challengePath, wfe.Challenge, "POST") | ||
|
||
return m | ||
} | ||
|
@@ -362,10 +355,8 @@ func (wfe *WebFrontEndImpl) Nonce( | |
request *http.Request) { | ||
statusCode := http.StatusNoContent | ||
// The ACME specification says GET requets should receive http.StatusNoContent | ||
// and HEAD requests should receive http.StatusOK. We only return StatusOK for | ||
// HEAD requests in strict mode because this was the legacy behaviour and it | ||
// may break clients to change. | ||
if wfe.strict && request.Method == "HEAD" { | ||
// and HEAD requests should receive http.StatusOK. | ||
if request.Method == "HEAD" { | ||
statusCode = http.StatusOK | ||
} | ||
response.WriteHeader(statusCode) | ||
|
@@ -486,19 +477,17 @@ func (wfe *WebFrontEndImpl) lookupJWK(request *http.Request, jws *jose.JSONWebSi | |
} | ||
|
||
func (wfe *WebFrontEndImpl) validPOST(request *http.Request) *acme.ProblemDetails { | ||
if wfe.strict { | ||
// Section 6.2 says to reject JWS requests without the expected Content-Type | ||
// using a status code of http.UnsupportedMediaType | ||
if _, present := request.Header["Content-Type"]; !present { | ||
return acme.UnsupportedMediaTypeProblem( | ||
`missing Content-Type header on POST. ` + | ||
`Content-Type must be "application/jose+json"`) | ||
} | ||
if contentType := request.Header.Get("Content-Type"); contentType != expectedJWSContentType { | ||
return acme.UnsupportedMediaTypeProblem( | ||
`Invalid Content-Type header on POST. ` + | ||
`Content-Type must be "application/jose+json"`) | ||
} | ||
// Section 6.2 says to reject JWS requests without the expected Content-Type | ||
// using a status code of http.UnsupportedMediaType | ||
if _, present := request.Header["Content-Type"]; !present { | ||
return acme.UnsupportedMediaTypeProblem( | ||
`missing Content-Type header on POST. ` + | ||
`Content-Type must be "application/jose+json"`) | ||
} | ||
if contentType := request.Header.Get("Content-Type"); contentType != expectedJWSContentType { | ||
return acme.UnsupportedMediaTypeProblem( | ||
`Invalid Content-Type header on POST. ` + | ||
`Content-Type must be "application/jose+json"`) | ||
} | ||
|
||
if _, present := request.Header["Content-Length"]; !present { | ||
|
@@ -758,7 +747,7 @@ func (wfe *WebFrontEndImpl) UpdateAccount( | |
// if this update contains no contacts or deactivated status, | ||
// simply return the existing account and return early. | ||
if updateAcctReq.Contact == nil && updateAcctReq.Status != acme.StatusDeactivated { | ||
if wfe.strict && !postData.postAsGet { | ||
if !postData.postAsGet { | ||
wfe.sendError(acme.MalformedProblem("Use POST-as-GET to retrieve account data instead of doing an empty update"), response) | ||
return | ||
} | ||
|
@@ -1363,24 +1352,15 @@ func (wfe *WebFrontEndImpl) Order( | |
logEvent *requestEvent, | ||
response http.ResponseWriter, | ||
request *http.Request) { | ||
|
||
var account *core.Account | ||
if request.Method == "GET" && wfe.strict { | ||
response.Header().Set("Allow", "POST") | ||
wfe.sendError(acme.MethodNotAllowed(), response) | ||
postData, prob := wfe.verifyPOST(ctx, logEvent, request, wfe.lookupJWK) | ||
if prob != nil { | ||
wfe.sendError(prob, response) | ||
return | ||
} | ||
account, prob := wfe.validPOSTAsGET(postData) | ||
if prob != nil { | ||
wfe.sendError(prob, response) | ||
return | ||
} else if request.Method == "POST" { | ||
postData, prob := wfe.verifyPOST(ctx, logEvent, request, wfe.lookupJWK) | ||
if prob != nil { | ||
wfe.sendError(prob, response) | ||
return | ||
} | ||
acct, prob := wfe.validPOSTAsGET(postData) | ||
if prob != nil { | ||
wfe.sendError(prob, response) | ||
return | ||
} | ||
account = acct | ||
} | ||
|
||
orderID := strings.TrimPrefix(request.URL.Path, orderPath) | ||
|
@@ -1586,11 +1566,12 @@ func (wfe *WebFrontEndImpl) Authz( | |
logEvent *requestEvent, | ||
response http.ResponseWriter, | ||
request *http.Request) { | ||
|
||
// Only allow GET when not being strict | ||
if request.Method == "GET" && wfe.strict { | ||
response.Header().Set("Allow", "POST") | ||
wfe.sendError(acme.MethodNotAllowed(), response) | ||
// There are two types of requests we might get: | ||
// A) a POST to update the authorization | ||
// B) a POST-as-GET to get the authorization | ||
postData, prob := wfe.verifyPOST(ctx, logEvent, request, wfe.lookupJWK) | ||
if prob != nil { | ||
wfe.sendError(prob, response) | ||
return | ||
} | ||
|
||
|
@@ -1601,66 +1582,55 @@ func (wfe *WebFrontEndImpl) Authz( | |
return | ||
} | ||
|
||
// If the request was a POST there are two options: | ||
// A) a POST to update the authorization | ||
// B) a POST-as-GET to get the authorization | ||
if request.Method == "POST" { | ||
postData, prob := wfe.verifyPOST(ctx, logEvent, request, wfe.lookupJWK) | ||
// If the postData is not a POST-as-GET, treat this as case A) and update | ||
// the authorization based on the postData | ||
if !postData.postAsGet { | ||
existingAcct, prob := wfe.getAcctByKey(postData.jwk) | ||
if prob != nil { | ||
wfe.sendError(prob, response) | ||
return | ||
} | ||
|
||
// If the postData is not a POST-as-GET, treat this as case A) and update | ||
// the authorization based on the postData | ||
if !postData.postAsGet { | ||
existingAcct, prob := wfe.getAcctByKey(postData.jwk) | ||
if prob != nil { | ||
wfe.sendError(prob, response) | ||
return | ||
} | ||
|
||
if authz.Order.AccountID != existingAcct.ID { | ||
wfe.sendError(acme.UnauthorizedProblem( | ||
"Account does not own authorization"), response) | ||
return | ||
} | ||
if authz.Order.AccountID != existingAcct.ID { | ||
wfe.sendError(acme.UnauthorizedProblem( | ||
"Account does not own authorization"), response) | ||
return | ||
} | ||
|
||
var deactivateRequest struct { | ||
Status string | ||
} | ||
err := json.Unmarshal(postData.body, &deactivateRequest) | ||
if err != nil { | ||
wfe.sendError(acme.MalformedProblem( | ||
fmt.Sprintf("Malformed authorization update: %s", | ||
err.Error())), response) | ||
return | ||
} | ||
var deactivateRequest struct { | ||
Status string | ||
} | ||
err := json.Unmarshal(postData.body, &deactivateRequest) | ||
if err != nil { | ||
wfe.sendError(acme.MalformedProblem( | ||
fmt.Sprintf("Malformed authorization update: %s", | ||
err.Error())), response) | ||
return | ||
} | ||
|
||
if deactivateRequest.Status != "deactivated" { | ||
wfe.sendError(acme.MalformedProblem( | ||
fmt.Sprintf("Malformed authorization update, status must be \"deactivated\" not %q", | ||
deactivateRequest.Status)), response) | ||
return | ||
} | ||
authz.Status = acme.StatusDeactivated | ||
} else { | ||
// Otherwise this was a POST-as-GET request and we need to verify it | ||
// accordingly and ensure the authorized account owns the authorization | ||
// being fetched. | ||
account, prob := wfe.validPOSTAsGET(postData) | ||
if prob != nil { | ||
wfe.sendError(prob, response) | ||
return | ||
} | ||
if deactivateRequest.Status != "deactivated" { | ||
wfe.sendError(acme.MalformedProblem( | ||
fmt.Sprintf("Malformed authorization update, status must be \"deactivated\" not %q", | ||
deactivateRequest.Status)), response) | ||
return | ||
} | ||
authz.Status = acme.StatusDeactivated | ||
} else { | ||
// Otherwise this was a POST-as-GET request and we need to verify it | ||
// accordingly and ensure the authorized account owns the authorization | ||
// being fetched. | ||
account, prob := wfe.validPOSTAsGET(postData) | ||
if prob != nil { | ||
wfe.sendError(prob, response) | ||
return | ||
} | ||
|
||
if authz.Order.AccountID != account.ID { | ||
response.WriteHeader(http.StatusForbidden) | ||
wfe.sendError(acme.UnauthorizedProblem( | ||
"Account authorizing the request is not the owner of the authorization"), | ||
response) | ||
return | ||
} | ||
if authz.Order.AccountID != account.ID { | ||
response.WriteHeader(http.StatusForbidden) | ||
wfe.sendError(acme.UnauthorizedProblem( | ||
"Account authorizing the request is not the owner of the authorization"), | ||
response) | ||
return | ||
} | ||
} | ||
|
||
|
@@ -1679,12 +1649,12 @@ func (wfe *WebFrontEndImpl) Challenge( | |
logEvent *requestEvent, | ||
response http.ResponseWriter, | ||
request *http.Request) { | ||
|
||
// Unauthenticated GETs to challenges are only allowed when not running in | ||
// strict mode. | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think the diff to this function missed some complexity that could be reduced further down at L1700 (Github won't let me leave a comment on the line):
There shouldn't be any case where |
||
if request.Method == "GET" && wfe.strict { | ||
response.Header().Set("Allow", "POST") | ||
wfe.sendError(acme.MethodNotAllowed(), response) | ||
// There are two possibilities: | ||
// A) request is a POST to begin a challenge | ||
// B) request is a POST-as-GET to poll a challenge | ||
postData, prob := wfe.verifyPOST(ctx, logEvent, request, wfe.lookupJWK) | ||
if prob != nil { | ||
wfe.sendError(prob, response) | ||
return | ||
} | ||
|
||
|
@@ -1695,39 +1665,25 @@ func (wfe *WebFrontEndImpl) Challenge( | |
return | ||
} | ||
|
||
// If the request is a POST there are two possibilities: | ||
// A) it is a POST to begin a challenge | ||
// B) it is a POST-as-GET to poll a challenge | ||
// If the post isn't a POST-as-GET its case A) | ||
var account *core.Account | ||
if request.Method == "POST" { | ||
postData, prob := wfe.verifyPOST(ctx, logEvent, request, wfe.lookupJWK) | ||
if !postData.postAsGet { | ||
wfe.updateChallenge(ctx, postData, response, request) | ||
return | ||
} else { | ||
// Otherwise it is case B) | ||
account, prob = wfe.validPOSTAsGET(postData) | ||
if prob != nil { | ||
wfe.sendError(prob, response) | ||
return | ||
} | ||
|
||
// If the post isn't a POST-as-GET its case A) | ||
if !postData.postAsGet { | ||
wfe.updateChallenge(ctx, postData, response, request) | ||
return | ||
} else { | ||
// Otherwise it is case B) | ||
acct, prob := wfe.validPOSTAsGET(postData) | ||
if prob != nil { | ||
wfe.sendError(prob, response) | ||
return | ||
} | ||
account = acct | ||
} | ||
} | ||
|
||
// Lock the challenge for reading in order to write the response | ||
chal.RLock() | ||
defer chal.RUnlock() | ||
|
||
// If there was an account authenticating this GET request then make sure it | ||
// owns the challenge. | ||
if account != nil && chal.Authz.Order.AccountID != account.ID { | ||
if chal.Authz.Order.AccountID != account.ID { | ||
response.WriteHeader(http.StatusUnauthorized) | ||
wfe.sendError(acme.UnauthorizedProblem( | ||
"Account authenticating request is not the owner of the challenge"), response) | ||
|
@@ -1836,9 +1792,8 @@ func (wfe *WebFrontEndImpl) updateChallenge( | |
// unnecessary, the server can calculate this itself. We could ignore this if | ||
// sent (and that's what Boulder will do) but for Pebble we'd like to offer | ||
// a way to be more aggressive about pushing clients implementations in the | ||
// right direction, so we treat this as a malformed request when running in | ||
// strict mode. | ||
if wfe.strict && chalResp.KeyAuthorization != nil { | ||
// right direction, so we treat this as a malformed request. | ||
if chalResp.KeyAuthorization != nil { | ||
wfe.sendError( | ||
acme.MalformedProblem( | ||
"Challenge response body contained legacy KeyAuthorization field, "+ | ||
|
@@ -1919,21 +1874,15 @@ func (wfe *WebFrontEndImpl) Certificate( | |
logEvent *requestEvent, | ||
response http.ResponseWriter, | ||
request *http.Request) { | ||
if request.Method == "GET" && wfe.strict { | ||
response.Header().Set("Allow", "POST") | ||
wfe.sendError(acme.MethodNotAllowed(), response) | ||
postData, prob := wfe.verifyPOST(ctx, logEvent, request, wfe.lookupJWK) | ||
if prob != nil { | ||
wfe.sendError(prob, response) | ||
return | ||
} | ||
_, prob = wfe.validPOSTAsGET(postData) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think we should be capturing the |
||
if prob != nil { | ||
wfe.sendError(prob, response) | ||
return | ||
} else if request.Method == "POST" { | ||
postData, prob := wfe.verifyPOST(ctx, logEvent, request, wfe.lookupJWK) | ||
if prob != nil { | ||
wfe.sendError(prob, response) | ||
return | ||
} | ||
_, prob = wfe.validPOSTAsGET(postData) | ||
if prob != nil { | ||
wfe.sendError(prob, response) | ||
return | ||
} | ||
} | ||
|
||
serial := strings.TrimPrefix(request.URL.Path, certPath) | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can you file a Pebble issue to link with a TODO here to revert to Certbot master when they've fixed this upstream?