mirror of
				https://github.com/matter-labs/vault-auth-tee.git
				synced 2025-10-28 21:44:09 +01:00 
			
		
		
		
	
		
			
				
	
	
		
			302 lines
		
	
	
	
		
			11 KiB
		
	
	
	
		
			Go
		
	
	
	
	
	
			
		
		
	
	
			302 lines
		
	
	
	
		
			11 KiB
		
	
	
	
		
			Go
		
	
	
	
	
	
| // SPDX-License-Identifier: MPL-2.0
 | |
| // Copyright (c) HashiCorp, Inc.
 | |
| 
 | |
| // 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.
 | |
| //
 | |
| // From https://github.com/cloudflare/cfssl/blob/master/ocsp/responder.go
 | |
| 
 | |
| package vault_auth_tee
 | |
| 
 | |
| import (
 | |
| 	"crypto"
 | |
| 	"crypto/sha256"
 | |
| 	"encoding/base64"
 | |
| 	"errors"
 | |
| 	"fmt"
 | |
| 	"io/ioutil"
 | |
| 	"net/http"
 | |
| 	"net/url"
 | |
| 	"time"
 | |
| 
 | |
| 	"golang.org/x/crypto/ocsp"
 | |
| )
 | |
| 
 | |
| var (
 | |
| 	malformedRequestErrorResponse = []byte{0x30, 0x03, 0x0A, 0x01, 0x01}
 | |
| 	internalErrorErrorResponse    = []byte{0x30, 0x03, 0x0A, 0x01, 0x02}
 | |
| 	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'log 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
 | |
| }
 | |
| 
 | |
| // Stats is a basic interface that allows users to record information
 | |
| // about returned responses
 | |
| type Stats interface {
 | |
| 	ResponseStatus(ocsp.ResponseStatus)
 | |
| }
 | |
| 
 | |
| type logger interface {
 | |
| 	Log(args ...any)
 | |
| }
 | |
| 
 | |
| // A Responder object provides the HTTP logic to expose a
 | |
| // Source of OCSP responses.
 | |
| type Responder struct {
 | |
| 	log    logger
 | |
| 	Source Source
 | |
| 	stats  Stats
 | |
| }
 | |
| 
 | |
| // NewResponder instantiates a Responder with the give Source.
 | |
| func NewResponder(t logger, source Source, stats Stats) *Responder {
 | |
| 	return &Responder{
 | |
| 		Source: source,
 | |
| 		stats:  stats,
 | |
| 		log:    t,
 | |
| 	}
 | |
| }
 | |
| 
 | |
| 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)
 | |
| 			}
 | |
| 		}
 | |
| 	}
 | |
| }
 | |
| 
 | |
| // 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) {
 | |
| 	// 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 {
 | |
| 			rs.log.Log("Error decoding URL:", 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 {
 | |
| 			rs.log.Log("Error decoding base64 from URL", string(base64RequestBytes))
 | |
| 			response.WriteHeader(http.StatusBadRequest)
 | |
| 			return
 | |
| 		}
 | |
| 	case "POST":
 | |
| 		requestBody, err = ioutil.ReadAll(request.Body)
 | |
| 		if err != nil {
 | |
| 			rs.log.Log("Problem reading body of POST", err)
 | |
| 			response.WriteHeader(http.StatusBadRequest)
 | |
| 			return
 | |
| 		}
 | |
| 	default:
 | |
| 		response.WriteHeader(http.StatusMethodNotAllowed)
 | |
| 		return
 | |
| 	}
 | |
| 	b64Body := base64.StdEncoding.EncodeToString(requestBody)
 | |
| 	rs.log.Log("Received OCSP request", 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'log intend to support nonces anyway, but maybe we
 | |
| 	//      should return unauthorizedRequest instead of malformed.
 | |
| 	ocspRequest, err := ocsp.ParseRequest(requestBody)
 | |
| 	if err != nil {
 | |
| 		rs.log.Log("Error decoding request body", b64Body)
 | |
| 		response.WriteHeader(http.StatusBadRequest)
 | |
| 		response.Write(malformedRequestErrorResponse)
 | |
| 		if rs.stats != nil {
 | |
| 			rs.stats.ResponseStatus(ocsp.Malformed)
 | |
| 		}
 | |
| 		return
 | |
| 	}
 | |
| 
 | |
| 	// Look up OCSP response from source
 | |
| 	ocspResponse, headers, err := rs.Source.Response(ocspRequest)
 | |
| 	if err != nil {
 | |
| 		if err == ErrNotFound {
 | |
| 			rs.log.Log("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
 | |
| 		}
 | |
| 		rs.log.Log("Error retrieving response for request: serial %x, request body %s, error",
 | |
| 			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 {
 | |
| 		rs.log.Log("Error parsing response for serial %x",
 | |
| 			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 := time.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)
 | |
| 	}
 | |
| }
 | |
| 
 | |
| /*
 | |
| Copyright (c) 2014 CloudFlare Inc.
 | |
| 
 | |
| Redistribution and use in source and binary forms, with or without
 | |
| modification, are permitted provided that the following conditions
 | |
| are met:
 | |
| 
 | |
| Redistributions of source code must retain the above copyright notice,
 | |
| this list of conditions and the following disclaimer.
 | |
| 
 | |
| Redistributions in binary form must reproduce the above copyright notice,
 | |
| this list of conditions and the following disclaimer in the documentation
 | |
| and/or other materials provided with the distribution.
 | |
| 
 | |
| THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
 | |
| "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
 | |
| LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
 | |
| A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
 | |
| HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
 | |
| SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED
 | |
| TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
 | |
| PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
 | |
| LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
 | |
| NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
 | |
| SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 | |
| */
 | 
![renovate[bot]](/assets/img/avatar_default.png)