Merge branch 'master' into sig-reuse

This commit is contained in:
Richard Barnes 2015-10-03 14:01:34 -04:00
commit 31ae51129a
6 changed files with 240 additions and 221 deletions

View File

@ -83,6 +83,8 @@ func main() {
wfe.SA = &sac
wfe.SubscriberAgreementURL = c.SubscriberAgreementURL
wfe.AllowOrigins = c.WFE.AllowOrigins
wfe.CertCacheDuration, err = time.ParseDuration(c.WFE.CertCacheDuration)
cmd.FailOnError(err, "Couldn't parse certificate caching duration")
wfe.CertNoCacheExpirationWindow, err = time.ParseDuration(c.WFE.CertNoCacheExpirationWindow)

View File

@ -75,6 +75,8 @@ type Config struct {
BaseURL string
ListenAddress string
AllowOrigins []string
CertCacheDuration string
CertNoCacheExpirationWindow string
IndexCacheDuration string

View File

@ -42,6 +42,7 @@
"wfe": {
"listenAddress": "127.0.0.1:4000",
"allowOrigins": ["*"],
"certCacheDuration": "6h",
"certNoCacheExpirationWindow": "96h",
"indexCacheDuration": "24h",

View File

@ -1,161 +0,0 @@
{
"syslog": {
"network": "",
"server": "",
"tag": "boulder"
},
"amqp": {
"server": "amqp://guest:guest@localhost:5672",
"RA": {
"client": "RA.client",
"server": "RA.server"
},
"VA": {
"client": "VA.client",
"server": "VA.server"
},
"SA": {
"client": "SA.client",
"server": "SA.server"
},
"CA": {
"client": "CA.client",
"server": "CA.server"
}
},
"statsd": {
"server": "localhost:8125",
"prefix": "Boulder"
},
"wfe": {
"listenAddress": "127.0.0.1:4000",
"certCacheDuration": "6h",
"certNoCacheExpirationWindow": "96h",
"indexCacheDuration": "24h",
"issuerCacheDuration": "48h",
"debugAddr": "localhost:8000"
},
"ca": {
"serialPrefix": 255,
"profile": "ee",
"dbConnect": "mysql+tcp://boulder@localhost:3306/boulder_test",
"debugAddr": "localhost:8001",
"testMode": true,
"_comment": "This should only be present in testMode. In prod use an HSM.",
"expiry": "2160h",
"lifespanOCSP": "96h",
"maxNames": 1000,
"Key": {
"PKCS11": {
"_comment": "On OS X, the module will likely be /Library/OpenSC/lib/opensc-pkcs11.so",
"Module": "/usr/lib/x86_64-linux-gnu/opensc-pkcs11.so",
"Token": "Yubico Yubikey NEO OTP+CCID 00 00",
"Label": "SIGN key",
"PIN": "1234"
}
},
"cfssl": {
"signing": {
"profiles": {
"ee": {
"usages": [
"digital signature",
"key encipherment",
"server auth",
"client auth"
],
"backdate": "1h",
"is_ca": false,
"issuer_urls": [
"http://int-x1.letsencrypt.org/cert"
],
"ocsp_url": "http://int-x1.letsencrypt.org/ocsp",
"crl_url": "http://int-x1.letsencrypt.org/crl",
"policies": [
{
"ID": "2.23.140.1.2.1"
},
],
"expiry": "8760h",
"CSRWhitelist": {
"PublicKeyAlgorithm": true,
"PublicKey": true,
"SignatureAlgorithm": true
},
"UseSerialSeq": true
}
},
"default": {
"usages": [
"digital signature"
],
"expiry": "8760h"
}
}
}
},
"ra": {
"debugAddr": "localhost:8002"
},
"sa": {
"dbConnect": "mysql+tcp://boulder@localhost:3306/boulder_test",
"debugAddr": "localhost:8003"
},
"va": {
"userAgent": "boulder",
"debugAddr": "localhost:8004"
},
"sql": {
"SQLDebug": true
},
"revoker": {
"dbConnect": "mysql+tcp://boulder@localhost:3306/boulder_test"
},
"ocspResponder": {
"dbConnect": "mysql+tcp://boulder@localhost:3306/boulder_test",
"debugAddr": "localhost:8004",
"path": "/",
"listenAddress": "localhost:4001"
},
"ocspUpdater": {
"dbConnect": "mysql+tcp://boulder@localhost:3306/boulder_test",
"debugAddr": "localhost:8005",
"minTimeToExpiry": "72h"
},
"activityMonitor": {
"debugAddr": "localhost:8006"
},
"mailer": {
"server": "mail.example.com",
"port": "25",
"username": "cert-master@example.com",
"password": "password",
"dbConnect": "mysql+tcp://boulder@localhost:3306/boulder_test",
"messageLimit": 0,
"nagTimes": ["24h", "72h", "168h", "336h"],
"emailTemplate": "test/example-expiration-template",
"debugAddr": "localhost:8004"
},
"common": {
"baseURL": "http://localhost:4000",
"issuerCert": "test/test-ca.pem",
"dnsResolver": "8.8.8.8:53",
"dnsTimeout": "10s"
},
"subscriberAgreementURL": "http://localhost:4000/terms"
}

View File

@ -73,7 +73,10 @@ type WebFrontEndImpl struct {
IndexCacheDuration time.Duration
IssuerCacheDuration time.Duration
// Gracefull shutdown settings
// CORS settings
AllowOrigins []string
// Graceful shutdown settings
ShutdownStopTimeout time.Duration
ShutdownKillTimeout time.Duration
}
@ -153,17 +156,27 @@ func (mrw BodylessResponseWriter) Write(buf []byte) (int, error) {
//
// * Set a Replay-Nonce header.
//
// * Respond to OPTIONS requests, including CORS preflight requests.
//
// * Respond http.StatusMethodNotAllowed for HTTP methods other than
// those listed.
// those listed.
//
// * Set CORS headers when responding to CORS "actual" requests.
//
// * Never send a body in response to a HEAD request. Anything
// written by the handler will be discarded if the method is HEAD. Also, all
// handlers that accept GET automatically accept HEAD.
// written by the handler will be discarded if the method is HEAD.
// Also, all handlers that accept GET automatically accept HEAD.
func (wfe *WebFrontEndImpl) HandleFunc(mux *http.ServeMux, pattern string, h func(http.ResponseWriter, *http.Request), methods ...string) {
methodsOK := make(map[string]bool)
methodsMap := make(map[string]bool)
for _, m := range methods {
methodsOK[m] = true
methodsMap[m] = true
}
if methodsMap["GET"] && !methodsMap["HEAD"] {
// Allow HEAD for any resource that allows GET
methods = append(methods, "HEAD")
methodsMap["HEAD"] = true
}
methodsStr := strings.Join(methods, ", ")
mux.HandleFunc(pattern, func(response http.ResponseWriter, request *http.Request) {
// We do not propagate errors here, because (1) they should be
// transient, and (2) they fail closed.
@ -171,27 +184,29 @@ func (wfe *WebFrontEndImpl) HandleFunc(mux *http.ServeMux, pattern string, h fun
if err == nil {
response.Header().Set("Replay-Nonce", nonce)
}
response.Header().Set("Access-Control-Allow-Origin", "*")
// Return a bodyless response to HEAD for any resource that allows GET.
if _, ok := methodsOK["GET"]; ok && request.Method == "HEAD" {
// We'll be sending an error anyway, but we
// should still comply with HTTP spec by not
switch request.Method {
case "HEAD":
// Whether or not we're sending a 405 error,
// we should comply with HTTP spec by not
// sending a body.
response = BodylessResponseWriter{response}
h(response, request)
case "OPTIONS":
wfe.Options(response, request, methodsStr, methodsMap)
return
}
if _, ok := methodsOK[request.Method]; !ok {
if !methodsMap[request.Method] {
logEvent := wfe.populateRequestEvent(request)
defer wfe.logRequestDetails(&logEvent)
logEvent.Error = "Method not allowed"
response.Header().Set("Allow", strings.Join(methods, ", "))
response.Header().Set("Allow", methodsStr)
wfe.sendError(response, logEvent.Error, request.Method, http.StatusMethodNotAllowed)
return
}
wfe.setCORSHeaders(response, request, "")
// Call the wrapped handler.
h(response, request)
})
@ -885,7 +900,7 @@ func (wfe *WebFrontEndImpl) prepChallengeForDisplay(authz core.Authorization, ch
// display to the client by clearing its ID and RegistrationID fields, and
// preparing all its challenges.
func (wfe *WebFrontEndImpl) prepAuthorizationForDisplay(authz *core.Authorization) {
for i, _ := range authz.Challenges {
for i := range authz.Challenges {
wfe.prepChallengeForDisplay(*authz, &authz.Challenges[i])
}
authz.ID = ""
@ -1211,6 +1226,61 @@ func (wfe *WebFrontEndImpl) BuildID(response http.ResponseWriter, request *http.
}
}
// Options responds to an HTTP OPTIONS request.
func (wfe *WebFrontEndImpl) Options(response http.ResponseWriter, request *http.Request, methodsStr string, methodsMap map[string]bool) {
// Every OPTIONS request gets an Allow header with a list of supported methods.
response.Header().Set("Allow", methodsStr)
// CORS preflight requests get additional headers. See
// http://www.w3.org/TR/cors/#resource-preflight-requests
reqMethod := request.Header.Get("Access-Control-Request-Method")
if reqMethod == "" {
reqMethod = "GET"
}
if methodsMap[reqMethod] {
wfe.setCORSHeaders(response, request, methodsStr)
}
}
// setCORSHeaders() tells the client that CORS is acceptable for this
// request. If allowMethods == "" the request is assumed to be a CORS
// actual request and no Access-Control-Allow-Methods header will be
// sent.
func (wfe *WebFrontEndImpl) setCORSHeaders(response http.ResponseWriter, request *http.Request, allowMethods string) {
reqOrigin := request.Header.Get("Origin")
if reqOrigin == "" {
// This is not a CORS request.
return
}
// Allow CORS if the current origin (or "*") is listed as an
// allowed origin in config. Otherwise, disallow by returning
// without setting any CORS headers.
allow := false
for _, ao := range wfe.AllowOrigins {
if ao == "*" {
response.Header().Set("Access-Control-Allow-Origin", "*")
allow = true
break
} else if ao == reqOrigin {
response.Header().Set("Vary", "Origin")
response.Header().Set("Access-Control-Allow-Origin", ao)
allow = true
break
}
}
if !allow {
return
}
if allowMethods != "" {
// For an OPTIONS request: allow all methods handled at this URL.
response.Header().Set("Access-Control-Allow-Methods", allowMethods)
}
response.Header().Set("Access-Control-Expose-Headers", "Link, Replay-Nonce")
response.Header().Set("Access-Control-Max-Age", "86400")
}
func (wfe *WebFrontEndImpl) logRequestDetails(logEvent *requestEvent) {
logEvent.ResponseTime = time.Now()
var msg string

View File

@ -252,6 +252,15 @@ func sortHeader(s string) string {
return strings.Join(a, ", ")
}
func addHeadIfGet(s []string) []string {
for _, a := range s {
if a == "GET" {
return append(s, "HEAD")
}
}
return s
}
func TestHandleFunc(t *testing.T) {
wfe := setupWFE(t)
var mux *http.ServeMux
@ -270,32 +279,34 @@ func TestHandleFunc(t *testing.T) {
// Plain requests (no CORS)
type testCase struct {
allowed []string
reqMethod string
shouldSucceed bool
allowed []string
reqMethod string
shouldCallStub bool
shouldSucceed bool
}
var lastNonce string
for _, c := range []testCase{
{[]string{"GET", "POST"}, "GET", true},
{[]string{"GET", "POST"}, "POST", true},
{[]string{"GET"}, "", false},
{[]string{"GET"}, "POST", false},
{[]string{"GET"}, "OPTIONS", false}, // TODO, #469
{[]string{"GET"}, "MAKE-COFFEE", false}, // 405, or 418?
{[]string{"GET", "POST"}, "GET", true, true},
{[]string{"GET", "POST"}, "POST", true, true},
{[]string{"GET"}, "", false, false},
{[]string{"GET"}, "POST", false, false},
{[]string{"GET"}, "OPTIONS", false, true},
{[]string{"GET"}, "MAKE-COFFEE", false, false}, // 405, or 418?
} {
runWrappedHandler(&http.Request{Method: c.reqMethod}, c.allowed...)
test.AssertEquals(t, stubCalled, c.shouldSucceed)
test.AssertEquals(t, stubCalled, c.shouldCallStub)
if c.shouldSucceed {
test.AssertEquals(t, rw.Code, http.StatusOK)
} else {
test.AssertEquals(t, rw.Code, http.StatusMethodNotAllowed)
test.AssertEquals(t, sortHeader(rw.Header().Get("Allow")), strings.Join(c.allowed, ", "))
test.AssertEquals(t, sortHeader(rw.Header().Get("Allow")), sortHeader(strings.Join(addHeadIfGet(c.allowed), ", ")))
test.AssertEquals(t,
rw.Body.String(),
`{"type":"urn:acme:error:malformed","detail":"Method not allowed"}`)
}
nonce := rw.Header().Get("Replay-Nonce")
test.AssertNotEquals(t, nonce, lastNonce)
test.AssertNotEquals(t, nonce, "")
lastNonce = nonce
}
@ -303,7 +314,7 @@ func TestHandleFunc(t *testing.T) {
runWrappedHandler(&http.Request{Method: "PUT"}, "GET", "POST")
test.AssertEquals(t, rw.Header().Get("Content-Type"), "application/problem+json")
test.AssertEquals(t, rw.Body.String(), `{"type":"urn:acme:error:malformed","detail":"Method not allowed"}`)
test.AssertEquals(t, sortHeader(rw.Header().Get("Allow")), "GET, POST")
test.AssertEquals(t, sortHeader(rw.Header().Get("Allow")), "GET, HEAD, POST")
// Disallowed method special case: response to HEAD has got no body
runWrappedHandler(&http.Request{Method: "HEAD"}, "GET", "POST")
@ -313,43 +324,137 @@ func TestHandleFunc(t *testing.T) {
// HEAD doesn't work with POST-only endpoints
runWrappedHandler(&http.Request{Method: "HEAD"}, "POST")
test.AssertEquals(t, stubCalled, false)
test.AssertEquals(t, rw.Code, http.StatusMethodNotAllowed)
test.AssertEquals(t, rw.Header().Get("Content-Type"), "application/problem+json")
test.AssertEquals(t, rw.Body.String(), `{"type":"urn:acme:error:malformed","detail":"Method not allowed"}`)
test.AssertEquals(t, rw.Header().Get("Allow"), "POST")
}
test.AssertEquals(t, rw.Body.String(), "")
func TestStandardHeaders(t *testing.T) {
wfe := setupWFE(t)
mux, err := wfe.Handler()
test.AssertNotError(t, err, "Problem setting up HTTP handlers")
wfe.AllowOrigins = []string{"*"}
testOrigin := "https://example.com"
cases := []struct {
path string
allowed []string
}{
{wfe.NewReg, []string{"POST"}},
{wfe.RegBase, []string{"POST"}},
{wfe.NewAuthz, []string{"POST"}},
{wfe.AuthzBase, []string{"GET"}},
{wfe.ChallengeBase, []string{"GET", "POST"}},
{wfe.NewCert, []string{"POST"}},
{wfe.CertBase, []string{"GET"}},
{wfe.SubscriberAgreementURL, []string{"GET"}},
// CORS "actual" request for disallowed method
runWrappedHandler(&http.Request{
Method: "POST",
Header: map[string][]string{
"Origin": {testOrigin},
},
}, "GET")
test.AssertEquals(t, stubCalled, false)
test.AssertEquals(t, rw.Code, http.StatusMethodNotAllowed)
// CORS "actual" request for allowed method
runWrappedHandler(&http.Request{
Method: "GET",
Header: map[string][]string{
"Origin": {testOrigin},
},
}, "GET", "POST")
test.AssertEquals(t, stubCalled, true)
test.AssertEquals(t, rw.Code, http.StatusOK)
test.AssertEquals(t, rw.Header().Get("Access-Control-Allow-Methods"), "")
test.AssertEquals(t, rw.Header().Get("Access-Control-Allow-Origin"), "*")
test.AssertEquals(t, sortHeader(rw.Header().Get("Access-Control-Expose-Headers")), "Link, Replay-Nonce")
// CORS preflight request for disallowed method
runWrappedHandler(&http.Request{
Method: "OPTIONS",
Header: map[string][]string{
"Origin": {testOrigin},
"Access-Control-Request-Method": {"POST"},
},
}, "GET")
test.AssertEquals(t, stubCalled, false)
test.AssertEquals(t, rw.Code, http.StatusOK)
test.AssertEquals(t, rw.Header().Get("Allow"), "GET, HEAD")
test.AssertEquals(t, rw.Header().Get("Access-Control-Allow-Origin"), "")
// CORS preflight request for allowed method
runWrappedHandler(&http.Request{
Method: "OPTIONS",
Header: map[string][]string{
"Origin": {testOrigin},
"Access-Control-Request-Method": {"POST"},
"Access-Control-Request-Headers": {"X-Accept-Header1, X-Accept-Header2", "X-Accept-Header3"},
},
}, "GET", "POST")
test.AssertEquals(t, rw.Code, http.StatusOK)
test.AssertEquals(t, rw.Header().Get("Access-Control-Allow-Origin"), "*")
test.AssertEquals(t, rw.Header().Get("Access-Control-Max-Age"), "86400")
test.AssertEquals(t, sortHeader(rw.Header().Get("Access-Control-Allow-Methods")), "GET, HEAD, POST")
test.AssertEquals(t, sortHeader(rw.Header().Get("Access-Control-Expose-Headers")), "Link, Replay-Nonce")
// OPTIONS request without an Origin header (i.e., not a CORS
// preflight request)
runWrappedHandler(&http.Request{
Method: "OPTIONS",
Header: map[string][]string{
"Access-Control-Request-Method": {"POST"},
},
}, "GET", "POST")
test.AssertEquals(t, rw.Code, http.StatusOK)
test.AssertEquals(t, rw.Header().Get("Access-Control-Allow-Origin"), "")
test.AssertEquals(t, sortHeader(rw.Header().Get("Allow")), "GET, HEAD, POST")
// CORS preflight request missing optional Request-Method
// header. The "actual" request will be GET.
for _, allowedMethod := range []string{"GET", "POST"} {
runWrappedHandler(&http.Request{
Method: "OPTIONS",
Header: map[string][]string{
"Origin": {testOrigin},
},
}, allowedMethod)
test.AssertEquals(t, rw.Code, http.StatusOK)
if allowedMethod == "GET" {
test.AssertEquals(t, rw.Header().Get("Access-Control-Allow-Origin"), "*")
test.AssertEquals(t, rw.Header().Get("Access-Control-Allow-Methods"), "GET, HEAD")
} else {
test.AssertEquals(t, rw.Header().Get("Access-Control-Allow-Origin"), "")
}
}
for _, c := range cases {
responseWriter := httptest.NewRecorder()
mux.ServeHTTP(responseWriter, &http.Request{
Method: "BOGUS",
URL: mustParseURL(c.path),
})
acao := responseWriter.Header().Get("Access-Control-Allow-Origin")
nonce := responseWriter.Header().Get("Replay-Nonce")
allow := responseWriter.Header().Get("Allow")
test.Assert(t, responseWriter.Code == http.StatusMethodNotAllowed, "Bogus method allowed")
test.Assert(t, acao == "*", "Bad CORS header")
test.Assert(t, len(nonce) > 0, "Bad Replay-Nonce header")
test.Assert(t, len(allow) > 0 && allow == strings.Join(c.allowed, ", "), "Bad Allow header")
// No CORS headers are given when configuration does not list
// "*" or the client-provided origin.
for _, wfe.AllowOrigins = range [][]string{
{},
{"http://example.com", "https://other.example"},
{""}, // Invalid origin is never matched
} {
runWrappedHandler(&http.Request{
Method: "OPTIONS",
Header: map[string][]string{
"Origin": {testOrigin},
"Access-Control-Request-Method": {"POST"},
},
}, "POST")
test.AssertEquals(t, rw.Code, http.StatusOK)
for _, h := range []string{
"Access-Control-Allow-Methods",
"Access-Control-Allow-Origin",
"Access-Control-Expose-Headers",
"Access-Control-Request-Headers",
} {
test.AssertEquals(t, rw.Header().Get(h), "")
}
}
// CORS headers are offered when configuration lists "*" or
// the client-provided origin.
for _, wfe.AllowOrigins = range [][]string{
{testOrigin, "http://example.org", "*"},
{"", "http://example.org", testOrigin}, // Invalid origin is harmless
} {
runWrappedHandler(&http.Request{
Method: "OPTIONS",
Header: map[string][]string{
"Origin": {testOrigin},
"Access-Control-Request-Method": {"POST"},
},
}, "POST")
test.AssertEquals(t, rw.Code, http.StatusOK)
test.AssertEquals(t, rw.Header().Get("Access-Control-Allow-Origin"), testOrigin)
// http://www.w3.org/TR/cors/ section 6.4:
test.AssertEquals(t, rw.Header().Get("Vary"), "Origin")
}
}
@ -1182,12 +1287,12 @@ func assertCsrLogged(t *testing.T, mockLog *mocks.MockSyslogWriter) {
}
func TestLogCsrPem(t *testing.T) {
const certificateRequestJson = `{
const certificateRequestJSON = `{
"csr": "MIICWTCCAUECAQAwFDESMBAGA1UEAwwJbG9jYWxob3N0MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAycX3ca-fViOuRWF38mssORISFxbJvspDfhPGRBZDxJ63NIqQzupB-6dp48xkcX7Z_KDaRJStcpJT2S0u33moNT4FHLklQBETLhExDk66cmlz6Xibp3LGZAwhWuec7wJoEwIgY8oq4rxihIyGq7HVIJoq9DqZGrUgfZMDeEJqbphukQOaXGEop7mD-eeu8-z5EVkB1LiJ6Yej6R8MAhVPHzG5fyOu6YVo6vY6QgwjRLfZHNj5XthxgPIEETZlUbiSoI6J19GYHvLURBTy5Ys54lYAPIGfNwcIBAH4gtH9FrYcDY68R22rp4iuxdvkf03ZWiT0F2W1y7_C9B2jayTzvQIDAQABoAAwDQYJKoZIhvcNAQELBQADggEBAHd6Do9DIZ2hvdt1GwBXYjsqprZidT_DYOMfYcK17KlvdkFT58XrBH88ulLZ72NXEpiFMeTyzfs3XEyGq_Bbe7TBGVYZabUEh-LOskYwhgcOuThVN7tHnH5rhN-gb7cEdysjTb1QL-vOUwYgV75CB6PE5JVYK-cQsMIVvo0Kz4TpNgjJnWzbcH7h0mtvub-fCv92vBPjvYq8gUDLNrok6rbg05tdOJkXsF2G_W-Q6sf2Fvx0bK5JeH4an7P7cXF9VG9nd4sRt5zd-L3IcyvHVKxNhIJXZVH0AOqh_1YrKI9R0QKQiZCEy0xN1okPlcaIVaFhb7IKAHPxTI3r5f72LXY"
}`
wfe := setupWFE(t)
var certificateRequest core.CertificateRequest
err := json.Unmarshal([]byte(certificateRequestJson), &certificateRequest)
err := json.Unmarshal([]byte(certificateRequestJSON), &certificateRequest)
test.AssertNotError(t, err, "Unable to parse certificateRequest")
mockSA := mocks.MockSA{}