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:
parent
b0e490ed3f
commit
6962bfe1a6
87
wfe2/wfe.go
87
wfe2/wfe.go
|
|
@ -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: ®.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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
||||||
100
wfe2/wfe_test.go
100
wfe2/wfe_test.go
|
|
@ -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)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue