github.com/hellobchain/third_party@v0.0.0-20230331131523-deb0478a2e52/cloudflare/cfssl/ocsp/responder.go (about) 1 // Package ocsp implements an OCSP responder based on a generic storage backend. 2 // It provides a couple of sample implementations. 3 // Because OCSP responders handle high query volumes, we have to be careful 4 // about how much logging we do. Error-level logs are reserved for problems 5 // internal to the server, that can be fixed by an administrator. Any type of 6 // incorrect input from a user should be logged and Info or below. For things 7 // that are logged on every request, Debug is the appropriate level. 8 package ocsp 9 10 import ( 11 "crypto/sha256" 12 "encoding/base64" 13 "encoding/hex" 14 "errors" 15 "fmt" 16 "github.com/hellobchain/newcryptosm/http" 17 "io/ioutil" 18 "net/url" 19 "regexp" 20 "time" 21 22 "github.com/jmhodges/clock" 23 "github.com/hellobchain/third_party/cloudflare/cfssl/certdb" 24 "github.com/hellobchain/third_party/cloudflare/cfssl/log" 25 "golang.org/x/crypto/ocsp" 26 ) 27 28 var ( 29 malformedRequestErrorResponse = []byte{0x30, 0x03, 0x0A, 0x01, 0x01} 30 internalErrorErrorResponse = []byte{0x30, 0x03, 0x0A, 0x01, 0x02} 31 tryLaterErrorResponse = []byte{0x30, 0x03, 0x0A, 0x01, 0x03} 32 sigRequredErrorResponse = []byte{0x30, 0x03, 0x0A, 0x01, 0x05} 33 unauthorizedErrorResponse = []byte{0x30, 0x03, 0x0A, 0x01, 0x06} 34 35 // ErrNotFound indicates the request OCSP response was not found. It is used to 36 // indicate that the responder should reply with unauthorizedErrorResponse. 37 ErrNotFound = errors.New("Request OCSP Response not found") 38 ) 39 40 // Source represents the logical source of OCSP responses, i.e., 41 // the logic that actually chooses a response based on a request. In 42 // order to create an actual responder, wrap one of these in a Responder 43 // object and pass it to http.Handle. By default the Responder will set 44 // the headers Cache-Control to "max-age=(response.NextUpdate-now), public, no-transform, must-revalidate", 45 // Last-Modified to response.ThisUpdate, Expires to response.NextUpdate, 46 // ETag to the SHA256 hash of the response, and Content-Type to 47 // application/ocsp-response. If you want to override these headers, 48 // or set extra headers, your source should return a http.Header 49 // with the headers you wish to set. If you don't want to set any 50 // extra headers you may return nil instead. 51 type Source interface { 52 Response(*ocsp.Request) ([]byte, http.Header, error) 53 } 54 55 // An InMemorySource is a map from serialNumber -> der(response) 56 type InMemorySource map[string][]byte 57 58 // Response looks up an OCSP response to provide for a given request. 59 // InMemorySource looks up a response purely based on serial number, 60 // without regard to what issuer the request is asking for. 61 func (src InMemorySource) Response(request *ocsp.Request) ([]byte, http.Header, error) { 62 response, present := src[request.SerialNumber.String()] 63 if !present { 64 return nil, nil, ErrNotFound 65 } 66 return response, nil, nil 67 } 68 69 // DBSource represnts a source of OCSP responses backed by the certdb package. 70 type DBSource struct { 71 Accessor certdb.Accessor 72 } 73 74 // NewDBSource creates a new DBSource type with an associated dbAccessor. 75 func NewDBSource(dbAccessor certdb.Accessor) Source { 76 return DBSource{ 77 Accessor: dbAccessor, 78 } 79 } 80 81 // Response implements cfssl.ocsp.responder.Source, which returns the 82 // OCSP response in the Database for the given request with the expiration 83 // date furthest in the future. 84 func (src DBSource) Response(req *ocsp.Request) ([]byte, http.Header, error) { 85 if req == nil { 86 return nil, nil, errors.New("called with nil request") 87 } 88 89 aki := hex.EncodeToString(req.IssuerKeyHash) 90 sn := req.SerialNumber 91 92 if sn == nil { 93 return nil, nil, errors.New("request contains no serial") 94 } 95 strSN := sn.String() 96 97 if src.Accessor == nil { 98 log.Errorf("No DB Accessor") 99 return nil, nil, errors.New("called with nil DB accessor") 100 } 101 records, err := src.Accessor.GetOCSP(strSN, aki) 102 103 // Response() logs when there are errors obtaining the OCSP response 104 // and returns nil, false. 105 if err != nil { 106 log.Errorf("Error obtaining OCSP response: %s", err) 107 return nil, nil, fmt.Errorf("failed to obtain OCSP response: %s", err) 108 } 109 110 if len(records) == 0 { 111 return nil, nil, ErrNotFound 112 } 113 114 // Response() finds the OCSPRecord with the expiration date furthest in the future. 115 cur := records[0] 116 for _, rec := range records { 117 if rec.Expiry.After(cur.Expiry) { 118 cur = rec 119 } 120 } 121 return []byte(cur.Body), nil, nil 122 } 123 124 // NewSourceFromFile reads the named file into an InMemorySource. 125 // The file read by this function must contain whitespace-separated OCSP 126 // responses. Each OCSP response must be in base64-encoded DER form (i.e., 127 // PEM without headers or whitespace). Invalid responses are ignored. 128 // This function pulls the entire file into an InMemorySource. 129 func NewSourceFromFile(responseFile string) (Source, error) { 130 fileContents, err := ioutil.ReadFile(responseFile) 131 if err != nil { 132 return nil, err 133 } 134 135 responsesB64 := regexp.MustCompile("\\s").Split(string(fileContents), -1) 136 src := InMemorySource{} 137 for _, b64 := range responsesB64 { 138 // if the line/space is empty just skip 139 if b64 == "" { 140 continue 141 } 142 der, tmpErr := base64.StdEncoding.DecodeString(b64) 143 if tmpErr != nil { 144 log.Errorf("Base64 decode error %s on: %s", tmpErr, b64) 145 continue 146 } 147 148 response, tmpErr := ocsp.ParseResponse(der, nil) 149 if tmpErr != nil { 150 log.Errorf("OCSP decode error %s on: %s", tmpErr, b64) 151 continue 152 } 153 154 src[response.SerialNumber.String()] = der 155 } 156 157 log.Infof("Read %d OCSP responses", len(src)) 158 return src, nil 159 } 160 161 // A Responder object provides the HTTP logic to expose a 162 // Source of OCSP responses. 163 type Responder struct { 164 Source Source 165 clk clock.Clock 166 } 167 168 // NewResponder instantiates a Responder with the give Source. 169 func NewResponder(source Source) *Responder { 170 return &Responder{ 171 Source: source, 172 clk: clock.Default(), 173 } 174 } 175 176 func overrideHeaders(response http.ResponseWriter, headers http.Header) { 177 for k, v := range headers { 178 if len(v) == 1 { 179 response.Header().Set(k, v[0]) 180 } else if len(v) > 1 { 181 response.Header().Del(k) 182 for _, e := range v { 183 response.Header().Add(k, e) 184 } 185 } 186 } 187 } 188 189 // A Responder can process both GET and POST requests. The mapping 190 // from an OCSP request to an OCSP response is done by the Source; 191 // the Responder simply decodes the request, and passes back whatever 192 // response is provided by the source. 193 // Note: The caller must use http.StripPrefix to strip any path components 194 // (including '/') on GET requests. 195 // Do not use this responder in conjunction with http.NewServeMux, because the 196 // default handler will try to canonicalize path components by changing any 197 // strings of repeated '/' into a single '/', which will break the base64 198 // encoding. 199 func (rs Responder) ServeHTTP(response http.ResponseWriter, request *http.Request) { 200 // By default we set a 'max-age=0, no-cache' Cache-Control header, this 201 // is only returned to the client if a valid authorized OCSP response 202 // is not found or an error is returned. If a response if found the header 203 // will be altered to contain the proper max-age and modifiers. 204 response.Header().Add("Cache-Control", "max-age=0, no-cache") 205 // Read response from request 206 var requestBody []byte 207 var err error 208 switch request.Method { 209 case "GET": 210 base64Request, err := url.QueryUnescape(request.URL.Path) 211 if err != nil { 212 log.Infof("Error decoding URL: %s", request.URL.Path) 213 response.WriteHeader(http.StatusBadRequest) 214 return 215 } 216 // url.QueryUnescape not only unescapes %2B escaping, but it additionally 217 // turns the resulting '+' into a space, which makes base64 decoding fail. 218 // So we go back afterwards and turn ' ' back into '+'. This means we 219 // accept some malformed input that includes ' ' or %20, but that's fine. 220 base64RequestBytes := []byte(base64Request) 221 for i := range base64RequestBytes { 222 if base64RequestBytes[i] == ' ' { 223 base64RequestBytes[i] = '+' 224 } 225 } 226 // In certain situations a UA may construct a request that has a double 227 // slash between the host name and the base64 request body due to naively 228 // constructing the request URL. In that case strip the leading slash 229 // so that we can still decode the request. 230 if len(base64RequestBytes) > 0 && base64RequestBytes[0] == '/' { 231 base64RequestBytes = base64RequestBytes[1:] 232 } 233 requestBody, err = base64.StdEncoding.DecodeString(string(base64RequestBytes)) 234 if err != nil { 235 log.Infof("Error decoding base64 from URL: %s", string(base64RequestBytes)) 236 response.WriteHeader(http.StatusBadRequest) 237 return 238 } 239 case "POST": 240 requestBody, err = ioutil.ReadAll(request.Body) 241 if err != nil { 242 log.Errorf("Problem reading body of POST: %s", err) 243 response.WriteHeader(http.StatusBadRequest) 244 return 245 } 246 default: 247 response.WriteHeader(http.StatusMethodNotAllowed) 248 return 249 } 250 b64Body := base64.StdEncoding.EncodeToString(requestBody) 251 log.Debugf("Received OCSP request: %s", b64Body) 252 253 // All responses after this point will be OCSP. 254 // We could check for the content type of the request, but that 255 // seems unnecessariliy restrictive. 256 response.Header().Add("Content-Type", "application/ocsp-response") 257 258 // Parse response as an OCSP request 259 // XXX: This fails if the request contains the nonce extension. 260 // We don't intend to support nonces anyway, but maybe we 261 // should return unauthorizedRequest instead of malformed. 262 ocspRequest, err := ocsp.ParseRequest(requestBody) 263 if err != nil { 264 log.Infof("Error decoding request body: %s", b64Body) 265 response.WriteHeader(http.StatusBadRequest) 266 response.Write(malformedRequestErrorResponse) 267 return 268 } 269 270 // Look up OCSP response from source 271 ocspResponse, headers, err := rs.Source.Response(ocspRequest) 272 if err != nil { 273 if err == ErrNotFound { 274 log.Infof("No response found for request: serial %x, request body %s", 275 ocspRequest.SerialNumber, b64Body) 276 response.Write(unauthorizedErrorResponse) 277 return 278 } 279 log.Infof("Error retrieving response for request: serial %x, request body %s, error: %s", 280 ocspRequest.SerialNumber, b64Body, err) 281 response.WriteHeader(http.StatusInternalServerError) 282 response.Write(internalErrorErrorResponse) 283 return 284 } 285 286 parsedResponse, err := ocsp.ParseResponse(ocspResponse, nil) 287 if err != nil { 288 log.Errorf("Error parsing response for serial %x: %s", 289 ocspRequest.SerialNumber, err) 290 response.Write(unauthorizedErrorResponse) 291 return 292 } 293 294 // Write OCSP response to response 295 response.Header().Add("Last-Modified", parsedResponse.ThisUpdate.Format(time.RFC1123)) 296 response.Header().Add("Expires", parsedResponse.NextUpdate.Format(time.RFC1123)) 297 now := rs.clk.Now() 298 maxAge := 0 299 if now.Before(parsedResponse.NextUpdate) { 300 maxAge = int(parsedResponse.NextUpdate.Sub(now) / time.Second) 301 } else { 302 // TODO(#530): we want max-age=0 but this is technically an authorized OCSP response 303 // (despite being stale) and 5019 forbids attaching no-cache 304 maxAge = 0 305 } 306 response.Header().Set( 307 "Cache-Control", 308 fmt.Sprintf( 309 "max-age=%d, public, no-transform, must-revalidate", 310 maxAge, 311 ), 312 ) 313 responseHash := sha256.Sum256(ocspResponse) 314 response.Header().Add("ETag", fmt.Sprintf("\"%X\"", responseHash)) 315 316 if headers != nil { 317 overrideHeaders(response, headers) 318 } 319 320 // RFC 7232 says that a 304 response must contain the above 321 // headers if they would also be sent for a 200 for the same 322 // request, so we have to wait until here to do this 323 if etag := request.Header.Get("If-None-Match"); etag != "" { 324 if etag == fmt.Sprintf("\"%X\"", responseHash) { 325 response.WriteHeader(http.StatusNotModified) 326 return 327 } 328 } 329 response.WriteHeader(http.StatusOK) 330 response.Write(ocspResponse) 331 }