capi/lib/revocation/ocsp/ocsp.go (183 lines of code) (raw):

/* This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ package ocsp import ( "bytes" "crypto/x509" "encoding/json" "fmt" "github.com/pkg/errors" ocsplib "golang.org/x/crypto/ocsp" "io/ioutil" "net/http" "regexp" "strings" "time" ) // RFC 6960 // // Appendix A. OCSP over HTTP // // A.1. Request // // HTTP-based OCSP requests can use either the GET or the POST method to // submit their requests. To enable HTTP caching, small requests (that // after encoding are less than 255 bytes) MAY be submitted using GET. // If HTTP caching is not important or if the request is greater than // 255 bytes, the request SHOULD be submitted using POST. Where privacy // is a requirement, OCSP transactions exchanged using HTTP MAY be // protected using either Transport Layer Security/Secure Socket Layer // (TLS/SSL) or some other lower-layer protocol. // // An OCSP request using the GET method is constructed as follows: // // GET {url}/{url-encoding of base-64 encoding of the DER encoding of // the OCSPRequest} // // where {url} may be derived from the value of the authority // information access extension in the certificate being checked for // revocation, or other local configuration of the OCSP client. // // An OCSP request using the POST method is constructed as follows: The // Content-Type header has the value "application/ocsp-request", while // the body of the message is the binary value of the DER encoding of // the OCSPRequest. // 4.2.1. ASN.1 Specification of the OCSP Response // // // CertStatus ::= CHOICE { // good [0] IMPLICIT NULL, // expired [1] IMPLICIT RevokedInfo, // unknown [2] IMPLICIT UnknownInfo } type OCSPStatus int const ( Good = iota Revoked Unknown Unauthorized CryptoVerifcationError BadResponse InternalError ) type OCSP struct { Error string Responder string Status OCSPStatus } func (o OCSP) MarshalJSON() ([]byte, error) { return json.Marshal(map[string]string{ "Responder": o.Responder, "Status": o.Status.String(), "Error": o.Error, }) } func (o *OCSP) UnmarshalJSON(data []byte) error { r := make(map[string]string) err := json.Unmarshal(data, &r) if err != nil { return err } responder, ok := r["Responder"] if !ok { return errors.New("Invalid OCSP struct, missing Responder key") } o.Responder = responder s, ok := r["Status"] if !ok { return errors.New("Invalid OCSP struct, missing Status key") } status, err := FromString(s) if err != nil { return err } o.Status = status e, ok := r["Error"] if !ok { return errors.New("Invalid OCSP struct, missing Error key") } o.Error = e return nil } func FromString(data string) (OCSPStatus, error) { switch data { case `good`: return Good, nil case `revoked`: return Revoked, nil case `unknown`: return Unknown, nil case `unauthorized`: return Unauthorized, nil case `badResponse`: return BadResponse, nil case `internalError`: return InternalError, nil default: return Unknown, errors.New("unknown OCSPStatus type " + string(data)) } } func (o OCSPStatus) String() string { switch o { case Good: return `good` case Revoked: return `revoked` case Unknown: return `unknown` case Unauthorized: return `unauthorized` case BadResponse: return `badResponse` case CryptoVerifcationError: return `cryptoVerificationError` case InternalError: return `internalError` default: return `error_unknown_ocsp_status` } } const OCSPContentType = "application/ocsp-request" func VerifyChain(chain []*x509.Certificate) [][]OCSP { ocsps := make([][]OCSP, len(chain)) if len(chain) == 1 { return ocsps } for i, cert := range chain[:len(chain)-1] { ocsps[i] = queryOCSP(cert, chain[i+1]) } ocsps[len(ocsps)-1] = make([]OCSP, 0) return ocsps } func queryOCSP(certificate, issuer *x509.Certificate) []OCSP { responses := make([]OCSP, len(certificate.OCSPServer)) for i, responder := range certificate.OCSPServer { responses[i] = newOCSPResponse(certificate, issuer, responder) } return responses } func newOCSPResponse(certificate, issuer *x509.Certificate, responder string) (response OCSP) { response.Responder = responder req, err := ocsplib.CreateRequest(certificate, issuer, nil) if err != nil { response.Status = InternalError response.Error = errors.Wrap(err, "failed to create DER encoded OCSP request").Error() return } r, err := http.NewRequest("POST", responder, bytes.NewReader(req)) if err != nil { response.Status = InternalError response.Error = errors.Wrap(err, "failed to create HTTP POST for OCSP request").Error() return } r.Header.Add("X-Automated-Tool", "https://github.com/mozilla/CCADB-Tools/capi CCADB test website verification tool") r.Header.Set("User-Agent", "Mozilla/5.0 (Macintosh; Intel Mac OS X 10.14; rv:64.0) Gecko/20100101 Firefox/64.0") r.Header.Set("Content-Type", OCSPContentType) client := http.Client{} client.Timeout = time.Duration(20 * time.Second) ret, err := client.Do(r) if err != nil { response.Status = BadResponse response.Error = errors.Wrapf(err, "failed to retrieve HTTP POST response from %v", responder).Error() return } defer ret.Body.Close() httpResp, err := ioutil.ReadAll(ret.Body) if err != nil { response.Status = BadResponse response.Error = err.Error() return } serverResponse, err := ocsplib.ParseResponse(httpResp, issuer) if err != nil { switch true { case strings.Contains(err.Error(), `unauthorized`): response.Status = Unauthorized case strings.Contains(err.Error(), `verification error`): response.Error = err.Error() response.Status = CryptoVerifcationError case itLooksLikeHTML(httpResp): response.Status = BadResponse response.Error = fmt.Sprintf("Response appears to be HTML. Error: %s", err.Error()) default: response.Status = BadResponse response.Error = err.Error() } return } switch serverResponse.Status { case ocsplib.Revoked: response.Status = Revoked case ocsplib.Good: response.Status = Good case ocsplib.Unknown: response.Status = Unknown } return } var HTMLish = regexp.MustCompile(`(<html>|<body>)`) func itLooksLikeHTML(response []byte) bool { return HTMLish.Match(response) }