123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427 |
- // Package ocsp implements an OCSP responder based on a generic storage backend.
- // It provides a couple of sample implementations.
- // Because OCSP responders handle high query volumes, we have to be careful
- // about how much logging we do. Error-level logs are reserved for problems
- // internal to the server, that can be fixed by an administrator. Any type of
- // incorrect input from a user should be logged and Info or below. For things
- // that are logged on every request, Debug is the appropriate level.
- package ocsp
- import (
- "crypto"
- "crypto/sha256"
- "encoding/base64"
- "encoding/hex"
- "encoding/json"
- "errors"
- "fmt"
- "io"
- "net/http"
- "net/url"
- "os"
- "regexp"
- "time"
- "github.com/cloudflare/cfssl/certdb"
- "github.com/cloudflare/cfssl/certdb/dbconf"
- "github.com/cloudflare/cfssl/certdb/sql"
- "github.com/cloudflare/cfssl/log"
- "github.com/jmhodges/clock"
- "golang.org/x/crypto/ocsp"
- )
- var (
- malformedRequestErrorResponse = []byte{0x30, 0x03, 0x0A, 0x01, 0x01}
- internalErrorErrorResponse = []byte{0x30, 0x03, 0x0A, 0x01, 0x02}
- tryLaterErrorResponse = []byte{0x30, 0x03, 0x0A, 0x01, 0x03}
- sigRequredErrorResponse = []byte{0x30, 0x03, 0x0A, 0x01, 0x05}
- unauthorizedErrorResponse = []byte{0x30, 0x03, 0x0A, 0x01, 0x06}
- // ErrNotFound indicates the request OCSP response was not found. It is used to
- // indicate that the responder should reply with unauthorizedErrorResponse.
- ErrNotFound = errors.New("Request OCSP Response not found")
- )
- // Source represents the logical source of OCSP responses, i.e.,
- // the logic that actually chooses a response based on a request. In
- // order to create an actual responder, wrap one of these in a Responder
- // object and pass it to http.Handle. By default the Responder will set
- // the headers Cache-Control to "max-age=(response.NextUpdate-now), public, no-transform, must-revalidate",
- // Last-Modified to response.ThisUpdate, Expires to response.NextUpdate,
- // ETag to the SHA256 hash of the response, and Content-Type to
- // application/ocsp-response. If you want to override these headers,
- // or set extra headers, your source should return a http.Header
- // with the headers you wish to set. If you don't want to set any
- // extra headers you may return nil instead.
- type Source interface {
- Response(*ocsp.Request) ([]byte, http.Header, error)
- }
- // An InMemorySource is a map from serialNumber -> der(response)
- type InMemorySource map[string][]byte
- // Response looks up an OCSP response to provide for a given request.
- // InMemorySource looks up a response purely based on serial number,
- // without regard to what issuer the request is asking for.
- func (src InMemorySource) Response(request *ocsp.Request) ([]byte, http.Header, error) {
- response, present := src[request.SerialNumber.String()]
- if !present {
- return nil, nil, ErrNotFound
- }
- return response, nil, nil
- }
- // DBSource represnts a source of OCSP responses backed by the certdb package.
- type DBSource struct {
- Accessor certdb.Accessor
- }
- // NewDBSource creates a new DBSource type with an associated dbAccessor.
- func NewDBSource(dbAccessor certdb.Accessor) Source {
- return DBSource{
- Accessor: dbAccessor,
- }
- }
- // Response implements cfssl.ocsp.responder.Source, which returns the
- // OCSP response in the Database for the given request with the expiration
- // date furthest in the future.
- func (src DBSource) Response(req *ocsp.Request) ([]byte, http.Header, error) {
- if req == nil {
- return nil, nil, errors.New("called with nil request")
- }
- aki := hex.EncodeToString(req.IssuerKeyHash)
- sn := req.SerialNumber
- if sn == nil {
- return nil, nil, errors.New("request contains no serial")
- }
- strSN := sn.String()
- if src.Accessor == nil {
- log.Errorf("No DB Accessor")
- return nil, nil, errors.New("called with nil DB accessor")
- }
- records, err := src.Accessor.GetOCSP(strSN, aki)
- // Response() logs when there are errors obtaining the OCSP response
- // and returns nil, false.
- if err != nil {
- log.Errorf("Error obtaining OCSP response: %s", err)
- return nil, nil, fmt.Errorf("failed to obtain OCSP response: %s", err)
- }
- if len(records) == 0 {
- return nil, nil, ErrNotFound
- }
- // Response() finds the OCSPRecord with the expiration date furthest in the future.
- cur := records[0]
- for _, rec := range records {
- if rec.Expiry.After(cur.Expiry) {
- cur = rec
- }
- }
- return []byte(cur.Body), nil, nil
- }
- // 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 := os.ReadFile(responseFile)
- if err != nil {
- return nil, err
- }
- responsesB64 := regexp.MustCompile("\\s").Split(string(fileContents), -1)
- src := InMemorySource{}
- for _, b64 := range responsesB64 {
- // if the line/space is empty just skip
- if b64 == "" {
- continue
- }
- der, tmpErr := base64.StdEncoding.DecodeString(b64)
- if tmpErr != nil {
- log.Errorf("Base64 decode error %s on: %s", tmpErr, b64)
- continue
- }
- response, tmpErr := ocsp.ParseResponse(der, nil)
- if tmpErr != nil {
- log.Errorf("OCSP decode error %s on: %s", tmpErr, b64)
- continue
- }
- src[response.SerialNumber.String()] = der
- }
- log.Infof("Read %d OCSP responses", len(src))
- return src, nil
- }
- // NewSourceFromDB reads the given database configuration file
- // and creates a database data source for use with the OCSP responder
- func NewSourceFromDB(DBConfigFile string) (Source, error) {
- // Load DB from cofiguration file
- db, err := dbconf.DBFromConfig(DBConfigFile)
- if err != nil {
- return nil, err
- }
- // Create accesor
- accessor := sql.NewAccessor(db)
- src := NewDBSource(accessor)
- return src, nil
- }
- // Stats is a basic interface that allows users to record information
- // about returned responses
- type Stats interface {
- ResponseStatus(ocsp.ResponseStatus)
- }
- // A Responder object provides the HTTP logic to expose a
- // Source of OCSP responses.
- type Responder struct {
- Source Source
- stats Stats
- clk clock.Clock
- }
- // NewResponder instantiates a Responder with the give Source.
- func NewResponder(source Source, stats Stats) *Responder {
- return &Responder{
- Source: source,
- stats: stats,
- clk: clock.New(),
- }
- }
- func overrideHeaders(response http.ResponseWriter, headers http.Header) {
- for k, v := range headers {
- if len(v) == 1 {
- response.Header().Set(k, v[0])
- } else if len(v) > 1 {
- response.Header().Del(k)
- for _, e := range v {
- response.Header().Add(k, e)
- }
- }
- }
- }
- type logEvent struct {
- IP string `json:"ip,omitempty"`
- UA string `json:"ua,omitempty"`
- Method string `json:"method,omitempty"`
- Path string `json:"path,omitempty"`
- Body string `json:"body,omitempty"`
- Received time.Time `json:"received,omitempty"`
- Took time.Duration `json:"took,omitempty"`
- Headers http.Header `json:"headers,omitempty"`
- Serial string `json:"serial,omitempty"`
- IssuerKeyHash string `json:"issuerKeyHash,omitempty"`
- IssuerNameHash string `json:"issuerNameHash,omitempty"`
- HashAlg string `json:"hashAlg,omitempty"`
- }
- // hashToString contains mappings for the only hash functions
- // x/crypto/ocsp supports
- var hashToString = map[crypto.Hash]string{
- crypto.SHA1: "SHA1",
- crypto.SHA256: "SHA256",
- crypto.SHA384: "SHA384",
- crypto.SHA512: "SHA512",
- }
- // 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) {
- le := logEvent{
- IP: request.RemoteAddr,
- UA: request.UserAgent(),
- Method: request.Method,
- Path: request.URL.Path,
- Received: time.Now(),
- }
- defer func() {
- le.Headers = response.Header()
- le.Took = time.Since(le.Received)
- jb, err := json.Marshal(le)
- if err != nil {
- // we log this error at the debug level as if we aren't at that level anyway
- // we shouldn't really care about marshalling the log event object
- log.Debugf("failed to marshal log event object: %s", err)
- return
- }
- log.Debugf("Received request: %s", string(jb))
- }()
- // By default we set a 'max-age=0, no-cache' Cache-Control header, this
- // is only returned to the client if a valid authorized OCSP response
- // is not found or an error is returned. If a response if found the header
- // will be altered to contain the proper max-age and modifiers.
- response.Header().Add("Cache-Control", "max-age=0, no-cache")
- // 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.Debugf("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] = '+'
- }
- }
- // In certain situations a UA may construct a request that has a double
- // slash between the host name and the base64 request body due to naively
- // constructing the request URL. In that case strip the leading slash
- // so that we can still decode the request.
- if len(base64RequestBytes) > 0 && base64RequestBytes[0] == '/' {
- base64RequestBytes = base64RequestBytes[1:]
- }
- requestBody, err = base64.StdEncoding.DecodeString(string(base64RequestBytes))
- if err != nil {
- log.Debugf("Error decoding base64 from URL: %s", string(base64RequestBytes))
- response.WriteHeader(http.StatusBadRequest)
- return
- }
- case "POST":
- requestBody, err = io.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
- }
- b64Body := base64.StdEncoding.EncodeToString(requestBody)
- log.Debugf("Received OCSP request: %s", b64Body)
- if request.Method == http.MethodPost {
- le.Body = 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.Debugf("Error decoding request body: %s", b64Body)
- response.WriteHeader(http.StatusBadRequest)
- response.Write(malformedRequestErrorResponse)
- if rs.stats != nil {
- rs.stats.ResponseStatus(ocsp.Malformed)
- }
- return
- }
- le.Serial = fmt.Sprintf("%x", ocspRequest.SerialNumber.Bytes())
- le.IssuerKeyHash = fmt.Sprintf("%x", ocspRequest.IssuerKeyHash)
- le.IssuerNameHash = fmt.Sprintf("%x", ocspRequest.IssuerNameHash)
- le.HashAlg = hashToString[ocspRequest.HashAlgorithm]
- // Look up OCSP response from source
- ocspResponse, headers, err := rs.Source.Response(ocspRequest)
- if err != nil {
- if err == ErrNotFound {
- log.Infof("No response found for request: serial %x, request body %s",
- ocspRequest.SerialNumber, b64Body)
- response.Write(unauthorizedErrorResponse)
- if rs.stats != nil {
- rs.stats.ResponseStatus(ocsp.Unauthorized)
- }
- return
- }
- log.Infof("Error retrieving response for request: serial %x, request body %s, error: %s",
- ocspRequest.SerialNumber, b64Body, err)
- response.WriteHeader(http.StatusInternalServerError)
- response.Write(internalErrorErrorResponse)
- if rs.stats != nil {
- rs.stats.ResponseStatus(ocsp.InternalError)
- }
- return
- }
- parsedResponse, err := ocsp.ParseResponse(ocspResponse, nil)
- if err != nil {
- log.Errorf("Error parsing response for serial %x: %s",
- ocspRequest.SerialNumber, err)
- response.Write(internalErrorErrorResponse)
- if rs.stats != nil {
- rs.stats.ResponseStatus(ocsp.InternalError)
- }
- return
- }
- // Write OCSP response to response
- response.Header().Add("Last-Modified", parsedResponse.ThisUpdate.Format(time.RFC1123))
- response.Header().Add("Expires", parsedResponse.NextUpdate.Format(time.RFC1123))
- now := rs.clk.Now()
- maxAge := 0
- if now.Before(parsedResponse.NextUpdate) {
- maxAge = int(parsedResponse.NextUpdate.Sub(now) / time.Second)
- } else {
- // TODO(#530): we want max-age=0 but this is technically an authorized OCSP response
- // (despite being stale) and 5019 forbids attaching no-cache
- maxAge = 0
- }
- response.Header().Set(
- "Cache-Control",
- fmt.Sprintf(
- "max-age=%d, public, no-transform, must-revalidate",
- maxAge,
- ),
- )
- responseHash := sha256.Sum256(ocspResponse)
- response.Header().Add("ETag", fmt.Sprintf("\"%X\"", responseHash))
- if headers != nil {
- overrideHeaders(response, headers)
- }
- // RFC 7232 says that a 304 response must contain the above
- // headers if they would also be sent for a 200 for the same
- // request, so we have to wait until here to do this
- if etag := request.Header.Get("If-None-Match"); etag != "" {
- if etag == fmt.Sprintf("\"%X\"", responseHash) {
- response.WriteHeader(http.StatusNotModified)
- return
- }
- }
- response.WriteHeader(http.StatusOK)
- response.Write(ocspResponse)
- if rs.stats != nil {
- rs.stats.ResponseStatus(ocsp.Success)
- }
- }
|