WFE2 new-order implementation (#2981)

Limited tests, since we don't do any integration tests for wfe2 this could still not be perfect.

Fixes #2930.
This commit is contained in:
Roland Bracewell Shoemaker 2017-08-16 12:40:56 -07:00 committed by GitHub
parent b0e490ed3f
commit 6962bfe1a6
2 changed files with 185 additions and 2 deletions

View File

@ -25,6 +25,7 @@ import (
"github.com/letsencrypt/boulder/metrics/measured_http" "github.com/letsencrypt/boulder/metrics/measured_http"
"github.com/letsencrypt/boulder/nonce" "github.com/letsencrypt/boulder/nonce"
"github.com/letsencrypt/boulder/probs" "github.com/letsencrypt/boulder/probs"
rapb "github.com/letsencrypt/boulder/ra/proto"
) )
// Paths are the ACME-spec identified URL path-segments for various methods. // Paths are the ACME-spec identified URL path-segments for various methods.
@ -45,6 +46,8 @@ const (
issuerPath = "/acme/issuer-cert" issuerPath = "/acme/issuer-cert"
buildIDPath = "/build" buildIDPath = "/build"
rolloverPath = "/acme/key-change" rolloverPath = "/acme/key-change"
newOrderPath = "/acme/new-order"
orderPath = "/acme/order/"
) )
// WebFrontEndImpl provides all the logic for Boulder's web-facing interface, // WebFrontEndImpl provides all the logic for Boulder's web-facing interface,
@ -308,6 +311,7 @@ func (wfe *WebFrontEndImpl) Handler() http.Handler {
wfe.HandleFunc(m, issuerPath, wfe.Issuer, "GET") wfe.HandleFunc(m, issuerPath, wfe.Issuer, "GET")
wfe.HandleFunc(m, buildIDPath, wfe.BuildID, "GET") wfe.HandleFunc(m, buildIDPath, wfe.BuildID, "GET")
wfe.HandleFunc(m, rolloverPath, wfe.KeyRollover, "POST") wfe.HandleFunc(m, rolloverPath, wfe.KeyRollover, "POST")
wfe.HandleFunc(m, newOrderPath, wfe.NewOrder, "POST")
// We don't use our special HandleFunc for "/" because it matches everything, // We don't use our special HandleFunc for "/" because it matches everything,
// meaning we can wind up returning 405 when we mean to return 404. See // meaning we can wind up returning 405 when we mean to return 404. See
// https://github.com/letsencrypt/boulder/issues/717 // https://github.com/letsencrypt/boulder/issues/717
@ -1255,3 +1259,86 @@ func (wfe *WebFrontEndImpl) addIssuingCertificateURLs(response http.ResponseWrit
} }
return nil return nil
} }
type orderJSON struct {
Status core.AcmeStatus
Expires time.Time
CSR core.JSONBuffer
Authorizations []string
}
// NewOrder is used by clients to create a new order object from a CSR
func (wfe *WebFrontEndImpl) NewOrder(ctx context.Context, logEvent *requestEvent, response http.ResponseWriter, request *http.Request) {
body, reg, prob := wfe.validPOSTForAccount(request, ctx, logEvent)
addRequesterHeader(response, logEvent.Requester)
if prob != nil {
// validPOSTForAccount handles its own setting of logEvent.Errors
wfe.sendError(response, logEvent, prob, nil)
return
}
var rawCSR core.RawCertificateRequest
// The optional fields NotAfter and NotBefore are ignored if present
// in the request
err := json.Unmarshal(body, &rawCSR)
if err != nil {
logEvent.AddError("unable to JSON unmarshal order request: %s", err)
wfe.sendError(response, logEvent, probs.Malformed("Error unmarshaling order request"), err)
return
}
// Assuming a properly formatted CSR there should be two four byte SEQUENCE
// declarations then a two byte integer declaration which defines the version
// of the CSR. If those two bytes (at offset 8 and 9) and equal to 2 and 0
// then the CSR was generated by a pre-1.0.2 version of OpenSSL with a client
// which didn't explicitly set the version causing the integer to be malformed
// and encoding/asn1 will refuse to parse it. If this is the case exit early
// with a more useful error message.
if len(rawCSR.CSR) >= 10 && rawCSR.CSR[8] == 2 && rawCSR.CSR[9] == 0 {
logEvent.AddError("Pre-1.0.2 OpenSSL malformed CSR")
wfe.sendError(
response,
logEvent,
probs.Malformed("CSR generated using a pre-1.0.2 OpenSSL with a client that doesn't properly specify the CSR version. See https://community.letsencrypt.org/t/openssl-bug-information/19591"),
nil,
)
return
}
// Check for a malformed CSR early to avoid unnecessary RPCs
_, err = x509.ParseCertificateRequest(rawCSR.CSR)
if err != nil {
logEvent.AddError("unable to parse CSR: %s", err)
wfe.sendError(response, logEvent, probs.Malformed("Error parsing certificate request: %s", err), err)
return
}
order, err := wfe.RA.NewOrder(ctx, &rapb.NewOrderRequest{
RegistrationID: &reg.ID,
Csr: rawCSR.CSR,
})
if err != nil {
logEvent.AddError("unable to create order: %s", err)
wfe.sendError(response, logEvent, problemDetailsForError(err, "Error creating new order"), err)
return
}
respObj := orderJSON{
Status: core.AcmeStatus(*order.Status),
Expires: time.Unix(0, *order.Expires).Truncate(time.Second).UTC(),
CSR: core.JSONBuffer(order.Csr),
Authorizations: make([]string, len(order.Authorizations)),
}
for i, authz := range order.Authorizations {
respObj.Authorizations[i] = wfe.relativeEndpoint(request, authzPath+string(*authz.Id))
}
// TODO(#2985): This location header points to a non-existent path, remove
// comment once the order handler is added
response.Header().Set("Location", wfe.relativeEndpoint(request, fmt.Sprintf("%s%d", orderPath, *order.Id)))
err = wfe.writeJsonResponse(response, logEvent, http.StatusCreated, respObj)
if err != nil {
wfe.sendError(response, logEvent, probs.ServerInternal("Error marshaling order"), err)
return
}
}

View File

@ -254,8 +254,20 @@ func (ra *MockRegistrationAuthority) DeactivateRegistration(ctx context.Context,
return nil return nil
} }
func (ra *MockRegistrationAuthority) NewOrder(ctx context.Context, _ *rapb.NewOrderRequest) (*corepb.Order, error) { func (ra *MockRegistrationAuthority) NewOrder(ctx context.Context, req *rapb.NewOrderRequest) (*corepb.Order, error) {
return nil, nil one := int64(1)
status := string(core.StatusPending)
id := "hello"
return &corepb.Order{
Id: &one,
RegistrationID: req.RegistrationID,
Expires: &one,
Csr: req.Csr,
Status: &status,
Authorizations: []*corepb.Authorization{
{Id: &id},
},
}, nil
} }
type mockPA struct{} type mockPA struct{}
@ -808,6 +820,11 @@ func TestHTTPMethods(t *testing.T) {
Path: rolloverPath, Path: rolloverPath,
Allowed: postOnly, Allowed: postOnly,
}, },
{
Name: "New order path should be POST only",
Path: newOrderPath,
Allowed: postOnly,
},
} }
// NOTE: We omit http.MethodOptions because all requests with this method are // NOTE: We omit http.MethodOptions because all requests with this method are
@ -1982,3 +1999,82 @@ func TestDeactivateRegistration(t *testing.T) {
"status": 403 "status": 403
}`) }`)
} }
func TestNewOrder(t *testing.T) {
wfe, _ := setupWFE(t)
responseWriter := httptest.NewRecorder()
targetHost := "localhost"
targetPath := "new-cert"
signedURL := fmt.Sprintf("http://%s/%s", targetHost, targetPath)
// CSR from an < 1.0.2 OpenSSL
oldOpenSSLCSRPayload := `{
"csr": "MIICWjCCAUICADAWMRQwEgYDVQQDEwtleGFtcGxlLmNvbTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAMpwCSKfLhKC3SnvLNpVayAEyAHVixkusgProAPZRBH0VAog_r4JOfoJez7ABiZ2ZIXXA2gg65_05HkGNl9ww-sa0EY8eCty_8WcHxqzafUnyXOJZuLMPJjaJ2oiBv_3BM7PZgpFzyNZ0_0ZuRKdFGtEY-vX9GXZUV0A3sxZMOpce0lhHAiBk_vNARJyM2-O-cZ7WjzZ7R1T9myAyxtsFhWy3QYvIwiKVVF3lDp3KXlPZ_7wBhVIBcVSk0bzhseotyUnKg-aL5qZIeB1ci7IT5qA_6C1_bsCSJSbQ5gnQwIQ0iaUV_SgUBpKNqYbmnSdZmDxvvW8FzhuL6JSDLfBR2kCAwEAAaAAMA0GCSqGSIb3DQEBCwUAA4IBAQBxxkchTXfjv07aSWU9brHnRziNYOLvsSNiOWmWLNlZg9LKdBy6j1xwM8IQRCfTOVSkbuxVV-kU5p-Cg9UF_UGoerl3j8SiupurTovK9-L_PdX0wTKbK9xkh7OUq88jp32Rw0eAT87gODJRD-M1NXlTvm-j896e60hUmL-DIe3iPbFl8auUS-KROAWjci-LJZYVdomm9Iw47E-zr4Hg27EdZhvCZvSyPMK8ioys9mNg5TthHB6ExepKP1YW3HpQa1EdUVYWGEvyVL4upQZOxuEA1WJqHv6iVDzsQqkl5kkahK87NKTPS59k1TFetjw2GLnQ09-g_L7kT8dpq3Bk5Wo="
}`
// openssl req -outform der -new -nodes -key wfe/test/178.key -subj /CN=not-an-example.com | b64url
// a valid CSR
goodCertCSRPayload := `{
"csr": "MIICYjCCAUoCAQAwHTEbMBkGA1UEAwwSbm90LWFuLWV4YW1wbGUuY29tMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAmqs7nue5oFxKBk2WaFZJAma2nm1oFyPIq19gYEAdQN4mWvaJ8RjzHFkDMYUrlIrGxCYuFJDHFUk9dh19Na1MIY-NVLgcSbyNcOML3bLbLEwGmvXPbbEOflBA9mxUS9TLMgXW5ghf_qbt4vmSGKloIim41QXt55QFW6O-84s8Kd2OE6df0wTsEwLhZB3j5pDU-t7j5vTMv4Tc7EptaPkOdfQn-68viUJjlYM_4yIBVRhWCdexFdylCKVLg0obsghQEwULKYCUjdg6F0VJUI115DU49tzscXU_3FS3CyY8rchunuYszBNkdmgpAwViHNWuP7ESdEd_emrj1xuioSe6PwIDAQABoAAwDQYJKoZIhvcNAQELBQADggEBAE_T1nWU38XVYL28hNVSXU0rW5IBUKtbvr0qAkD4kda4HmQRTYkt-LNSuvxoZCC9lxijjgtJi-OJe_DCTdZZpYzewlVvcKToWSYHYQ6Wm1-fxxD_XzphvZOujpmBySchdiz7QSVWJmVZu34XD5RJbIcrmj_cjRt42J1hiTFjNMzQu9U6_HwIMmliDL-soFY2RTvvZf-dAFvOUQ-Wbxt97eM1PbbmxJNWRhbAmgEpe9PWDPTpqV5AK56VAa991cQ1P8ZVmPss5hvwGWhOtpnpTZVHN3toGNYFKqxWPboirqushQlfKiFqT9rpRgM3-mFjOHidGqsKEkTdmfSVlVEk3oo="
}`
testCases := []struct {
Name string
Request *http.Request
ExpectedBody string
ExpectedHeaders map[string]string
}{
{
Name: "POST, but no body",
Request: &http.Request{
Method: "POST",
Header: map[string][]string{
"Content-Length": {"0"},
},
},
ExpectedBody: `{"type":"urn:acme:error:malformed","detail":"No body on POST","status":400}`,
},
{
Name: "POST, with an invalid JWS body",
Request: makePostRequestWithPath("hi", "hi"),
ExpectedBody: `{"type":"urn:acme:error:malformed","detail":"Parse error reading JWS","status":400}`,
},
{
Name: "POST, properly signed JWS, payload isn't valid",
Request: signAndPost(t, targetPath, signedURL, "foo", 1, wfe.nonceService),
ExpectedBody: `{"type":"urn:acme:error:malformed","detail":"Request payload did not parse as JSON","status":400}`,
},
{
Name: "POST, properly signed JWS, trivial JSON payload",
Request: signAndPost(t, targetPath, signedURL, "{}", 1, wfe.nonceService),
ExpectedBody: `{"type":"urn:acme:error:malformed","detail":"Error parsing certificate request: asn1: syntax error: sequence truncated","status":400}`,
},
{
Name: "POST, properly signed JWS, CSR from an old OpenSSL",
Request: signAndPost(t, targetPath, signedURL, oldOpenSSLCSRPayload, 1, wfe.nonceService),
ExpectedBody: `{"type":"urn:acme:error:malformed","detail":"CSR generated using a pre-1.0.2 OpenSSL with a client that doesn't properly specify the CSR version. See https://community.letsencrypt.org/t/openssl-bug-information/19591","status":400}`,
},
{
Name: "POST, properly signed JWS, authorizations for all names in CSR",
Request: signAndPost(t, targetPath, signedURL, goodCertCSRPayload, 1, wfe.nonceService),
ExpectedBody: `{"Status":"pending","Expires":"1970-01-01T00:00:00Z","CSR":"MIICYjCCAUoCAQAwHTEbMBkGA1UEAwwSbm90LWFuLWV4YW1wbGUuY29tMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAmqs7nue5oFxKBk2WaFZJAma2nm1oFyPIq19gYEAdQN4mWvaJ8RjzHFkDMYUrlIrGxCYuFJDHFUk9dh19Na1MIY-NVLgcSbyNcOML3bLbLEwGmvXPbbEOflBA9mxUS9TLMgXW5ghf_qbt4vmSGKloIim41QXt55QFW6O-84s8Kd2OE6df0wTsEwLhZB3j5pDU-t7j5vTMv4Tc7EptaPkOdfQn-68viUJjlYM_4yIBVRhWCdexFdylCKVLg0obsghQEwULKYCUjdg6F0VJUI115DU49tzscXU_3FS3CyY8rchunuYszBNkdmgpAwViHNWuP7ESdEd_emrj1xuioSe6PwIDAQABoAAwDQYJKoZIhvcNAQELBQADggEBAE_T1nWU38XVYL28hNVSXU0rW5IBUKtbvr0qAkD4kda4HmQRTYkt-LNSuvxoZCC9lxijjgtJi-OJe_DCTdZZpYzewlVvcKToWSYHYQ6Wm1-fxxD_XzphvZOujpmBySchdiz7QSVWJmVZu34XD5RJbIcrmj_cjRt42J1hiTFjNMzQu9U6_HwIMmliDL-soFY2RTvvZf-dAFvOUQ-Wbxt97eM1PbbmxJNWRhbAmgEpe9PWDPTpqV5AK56VAa991cQ1P8ZVmPss5hvwGWhOtpnpTZVHN3toGNYFKqxWPboirqushQlfKiFqT9rpRgM3-mFjOHidGqsKEkTdmfSVlVEk3oo","Authorizations":["http://localhost/acme/authz/hello"]}`,
ExpectedHeaders: map[string]string{"Location": "http://localhost/acme/order/1"},
},
}
for _, tc := range testCases {
t.Run(tc.Name, func(t *testing.T) {
responseWriter.Body.Reset()
responseWriter.HeaderMap = http.Header{}
wfe.NewOrder(ctx, newRequestEvent(), responseWriter, tc.Request)
test.AssertUnmarshaledEquals(t, responseWriter.Body.String(), tc.ExpectedBody)
headers := responseWriter.Header()
for k, v := range tc.ExpectedHeaders {
test.AssertEquals(t, headers.Get(k), v)
}
})
}
}