// NewSourceFromFile reads the named file into an InMemorySource. // The file read by this function must contain whitespace-separated OCSP // responses. Each OCSP response must be in base64-encoded DER form (i.e., // PEM without headers or whitespace). Invalid responses are ignored. // This function pulls the entire file into an InMemorySource. func NewSourceFromFile(responseFile string) (Source, error) { fileContents, err := ioutil.ReadFile(responseFile) if err != nil { return nil, err } responsesB64 := regexp.MustCompile("\\s").Split(string(fileContents), -1) src := InMemorySource{} for _, b64 := range responsesB64 { der, tmpErr := base64.StdEncoding.DecodeString(b64) if tmpErr != nil { log.Errorf("Base64 decode error on: %s", b64) continue } response, tmpErr := ocsp.ParseResponse(der, nil) if tmpErr != nil { log.Errorf("OCSP decode error on: %s", b64) continue } src[response.SerialNumber.String()] = der } log.Infof("Read %d OCSP responses", len(src)) return src, nil }
func TestDBHandler(t *testing.T) { src, err := makeDBSource("mysql+tcp://ocsp_resp@localhost:3306/boulder_sa_test", "./testdata/test-ca.der.pem", false) if err != nil { t.Fatalf("makeDBSource: %s", err) } defer test.ResetSATestDatabase(t) ocspResp, err := ocsp.ParseResponse(resp, nil) if err != nil { t.Fatalf("ocsp.ParseResponse: %s", err) } status := &core.CertificateStatus{ Serial: core.SerialToString(ocspResp.SerialNumber), OCSPLastUpdated: time.Now(), OCSPResponse: resp, } setupDBMap, err := sa.NewDbMap("mysql+tcp://test_setup@localhost:3306/boulder_sa_test") if err != nil { t.Fatal(err) } err = setupDBMap.Insert(status) if err != nil { t.Fatalf("unable to insert response: %s", err) } h := handler(src, 10*time.Second) w := httptest.NewRecorder() r, err := http.NewRequest("POST", "/", bytes.NewReader(req)) if err != nil { t.Fatal(err) } h.ServeHTTP(w, r) if w.Code != http.StatusOK { t.Errorf("Code: want %d, got %d", http.StatusOK, w.Code) } if !bytes.Equal(w.Body.Bytes(), resp) { t.Errorf("Mismatched body: want %#v, got %#v", resp, w.Body.Bytes()) } }
// A Responder can process both GET and POST requests. The mapping // from an OCSP request to an OCSP response is done by the Source; // the Responder simply decodes the request, and passes back whatever // response is provided by the source. // Note: The caller must use http.StripPrefix to strip any path components // (including '/') on GET requests. // Do not use this responder in conjunction with http.NewServeMux, because the // default handler will try to canonicalize path components by changing any // strings of repeated '/' into a single '/', which will break the base64 // encoding. func (rs Responder) ServeHTTP(response http.ResponseWriter, request *http.Request) { // Read response from request var requestBody []byte var err error switch request.Method { case "GET": base64Request, err := url.QueryUnescape(request.URL.Path) if err != nil { log.Errorf("Error decoding URL: %s", request.URL.Path) response.WriteHeader(http.StatusBadRequest) return } // url.QueryUnescape not only unescapes %2B escaping, but it additionally // turns the resulting '+' into a space, which makes base64 decoding fail. // So we go back afterwards and turn ' ' back into '+'. This means we // accept some malformed input that includes ' ' or %20, but that's fine. base64RequestBytes := []byte(base64Request) for i := range base64RequestBytes { if base64RequestBytes[i] == ' ' { base64RequestBytes[i] = '+' } } requestBody, err = base64.StdEncoding.DecodeString(string(base64RequestBytes)) if err != nil { log.Errorf("Error decoding base64 from URL: %s", base64Request) response.WriteHeader(http.StatusBadRequest) return } case "POST": requestBody, err = ioutil.ReadAll(request.Body) if err != nil { log.Errorf("Problem reading body of POST: %s", err) response.WriteHeader(http.StatusBadRequest) return } default: response.WriteHeader(http.StatusMethodNotAllowed) return } // TODO log request b64Body := base64.StdEncoding.EncodeToString(requestBody) log.Infof("Received OCSP request: %s", b64Body) // All responses after this point will be OCSP. // We could check for the content type of the request, but that // seems unnecessariliy restrictive. response.Header().Add("Content-Type", "application/ocsp-response") // Parse response as an OCSP request // XXX: This fails if the request contains the nonce extension. // We don't intend to support nonces anyway, but maybe we // should return unauthorizedRequest instead of malformed. ocspRequest, err := ocsp.ParseRequest(requestBody) if err != nil { log.Errorf("Error decoding request body: %s", b64Body) response.WriteHeader(http.StatusBadRequest) response.Write(malformedRequestErrorResponse) return } // Look up OCSP response from source ocspResponse, found := rs.Source.Response(ocspRequest) if !found { log.Errorf("No response found for request: %s", b64Body) response.Write(unauthorizedErrorResponse) return } parsedResponse, err := ocsp.ParseResponse(ocspResponse, nil) if err != nil { log.Errorf("Error parsing response: %s", err) response.Write(unauthorizedErrorResponse) return } // Write OCSP response to response response.Header().Add("Last-Modified", parsedResponse.ProducedAt.Format(time.RFC1123)) response.Header().Add("Expires", parsedResponse.NextUpdate.Format(time.RFC1123)) maxAge := int64(parsedResponse.NextUpdate.Sub(rs.clk.Now()) / time.Second) if maxAge > 0 { response.Header().Add("Cache-Control", fmt.Sprintf("max-age=%d", maxAge)) } response.WriteHeader(http.StatusOK) response.Write(ocspResponse) }