Add test, include extensions support
needed to integrate code from cfssl and golang.org/x/crypto/ocsp into new pkg/ocsp to be able to add response extensions.main
parent
434b544e78
commit
b0a16bb85c
@ -0,0 +1,2 @@
|
||||
// Package ocsp contains adapted code from github.com/cloudflare/cfssl/ocsp and golang.org/x/crypto/ocsp
|
||||
package ocsp
|
@ -0,0 +1,791 @@
|
||||
// Copyright 2013 The Go Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
// Package ocsp parses OCSP responses as specified in RFC 2560. OCSP responses
|
||||
// are signed messages attesting to the validity of a certificate for a small
|
||||
// period of time. This is used to manage revocation for X.509 certificates.
|
||||
package ocsp // import "golang.org/x/crypto/ocsp"
|
||||
|
||||
import (
|
||||
"crypto"
|
||||
"crypto/ecdsa"
|
||||
"crypto/elliptic"
|
||||
"crypto/rand"
|
||||
"crypto/rsa"
|
||||
_ "crypto/sha1"
|
||||
_ "crypto/sha256"
|
||||
_ "crypto/sha512"
|
||||
"crypto/x509"
|
||||
"crypto/x509/pkix"
|
||||
"encoding/asn1"
|
||||
"errors"
|
||||
"fmt"
|
||||
"math/big"
|
||||
"strconv"
|
||||
"time"
|
||||
)
|
||||
|
||||
var idPKIXOCSPBasic = asn1.ObjectIdentifier([]int{1, 3, 6, 1, 5, 5, 7, 48, 1, 1})
|
||||
|
||||
// ResponseStatus contains the result of an OCSP request. See
|
||||
// https://tools.ietf.org/html/rfc6960#section-2.3
|
||||
type ResponseStatus int
|
||||
|
||||
const (
|
||||
Success ResponseStatus = 0
|
||||
Malformed ResponseStatus = 1
|
||||
InternalError ResponseStatus = 2
|
||||
TryLater ResponseStatus = 3
|
||||
// Status code four is unused in OCSP. See
|
||||
// https://tools.ietf.org/html/rfc6960#section-4.2.1
|
||||
SignatureRequired ResponseStatus = 5
|
||||
Unauthorized ResponseStatus = 6
|
||||
)
|
||||
|
||||
func (r ResponseStatus) String() string {
|
||||
switch r {
|
||||
case Success:
|
||||
return "success"
|
||||
case Malformed:
|
||||
return "malformed"
|
||||
case InternalError:
|
||||
return "internal error"
|
||||
case TryLater:
|
||||
return "try later"
|
||||
case SignatureRequired:
|
||||
return "signature required"
|
||||
case Unauthorized:
|
||||
return "unauthorized"
|
||||
default:
|
||||
return "unknown OCSP status: " + strconv.Itoa(int(r))
|
||||
}
|
||||
}
|
||||
|
||||
// ResponseError is an error that may be returned by ParseResponse to indicate
|
||||
// that the response itself is an error, not just that it's indicating that a
|
||||
// certificate is revoked, unknown, etc.
|
||||
type ResponseError struct {
|
||||
Status ResponseStatus
|
||||
}
|
||||
|
||||
func (r ResponseError) Error() string {
|
||||
return "ocsp: error from server: " + r.Status.String()
|
||||
}
|
||||
|
||||
// These are internal structures that reflect the ASN.1 structure of an OCSP
|
||||
// response. See RFC 2560, section 4.2.
|
||||
|
||||
type certID struct {
|
||||
HashAlgorithm pkix.AlgorithmIdentifier
|
||||
NameHash []byte
|
||||
IssuerKeyHash []byte
|
||||
SerialNumber *big.Int
|
||||
}
|
||||
|
||||
// https://tools.ietf.org/html/rfc2560#section-4.1.1
|
||||
type ocspRequest struct {
|
||||
TBSRequest tbsRequest
|
||||
}
|
||||
|
||||
type tbsRequest struct {
|
||||
Version int `asn1:"explicit,tag:0,default:0,optional"`
|
||||
RequestorName pkix.RDNSequence `asn1:"explicit,tag:1,optional"`
|
||||
RequestList []request
|
||||
}
|
||||
|
||||
type request struct {
|
||||
Cert certID
|
||||
}
|
||||
|
||||
type responseASN1 struct {
|
||||
Status asn1.Enumerated
|
||||
Response responseBytes `asn1:"explicit,tag:0,optional"`
|
||||
}
|
||||
|
||||
type responseBytes struct {
|
||||
ResponseType asn1.ObjectIdentifier
|
||||
Response []byte
|
||||
}
|
||||
|
||||
type basicResponse struct {
|
||||
TBSResponseData responseData
|
||||
SignatureAlgorithm pkix.AlgorithmIdentifier
|
||||
Signature asn1.BitString
|
||||
Certificates []asn1.RawValue `asn1:"explicit,tag:0,optional"`
|
||||
}
|
||||
|
||||
type responseData struct {
|
||||
Raw asn1.RawContent
|
||||
Version int `asn1:"optional,default:0,explicit,tag:0"`
|
||||
RawResponderID asn1.RawValue
|
||||
ProducedAt time.Time `asn1:"generalized"`
|
||||
Responses []singleResponse
|
||||
ResponseExtensions []pkix.Extension `asn1:"explicit,tag:1,optional"`
|
||||
}
|
||||
|
||||
type singleResponse struct {
|
||||
CertID certID
|
||||
Good asn1.Flag `asn1:"tag:0,optional"`
|
||||
Revoked revokedInfo `asn1:"tag:1,optional"`
|
||||
Unknown asn1.Flag `asn1:"tag:2,optional"`
|
||||
ThisUpdate time.Time `asn1:"generalized"`
|
||||
NextUpdate time.Time `asn1:"generalized,explicit,tag:0,optional"`
|
||||
SingleExtensions []pkix.Extension `asn1:"explicit,tag:1,optional"`
|
||||
}
|
||||
|
||||
type revokedInfo struct {
|
||||
RevocationTime time.Time `asn1:"generalized"`
|
||||
Reason asn1.Enumerated `asn1:"explicit,tag:0,optional"`
|
||||
}
|
||||
|
||||
var (
|
||||
oidSignatureMD2WithRSA = asn1.ObjectIdentifier{1, 2, 840, 113549, 1, 1, 2}
|
||||
oidSignatureMD5WithRSA = asn1.ObjectIdentifier{1, 2, 840, 113549, 1, 1, 4}
|
||||
oidSignatureSHA1WithRSA = asn1.ObjectIdentifier{1, 2, 840, 113549, 1, 1, 5}
|
||||
oidSignatureSHA256WithRSA = asn1.ObjectIdentifier{1, 2, 840, 113549, 1, 1, 11}
|
||||
oidSignatureSHA384WithRSA = asn1.ObjectIdentifier{1, 2, 840, 113549, 1, 1, 12}
|
||||
oidSignatureSHA512WithRSA = asn1.ObjectIdentifier{1, 2, 840, 113549, 1, 1, 13}
|
||||
oidSignatureDSAWithSHA1 = asn1.ObjectIdentifier{1, 2, 840, 10040, 4, 3}
|
||||
oidSignatureDSAWithSHA256 = asn1.ObjectIdentifier{2, 16, 840, 1, 101, 3, 4, 3, 2}
|
||||
oidSignatureECDSAWithSHA1 = asn1.ObjectIdentifier{1, 2, 840, 10045, 4, 1}
|
||||
oidSignatureECDSAWithSHA256 = asn1.ObjectIdentifier{1, 2, 840, 10045, 4, 3, 2}
|
||||
oidSignatureECDSAWithSHA384 = asn1.ObjectIdentifier{1, 2, 840, 10045, 4, 3, 3}
|
||||
oidSignatureECDSAWithSHA512 = asn1.ObjectIdentifier{1, 2, 840, 10045, 4, 3, 4}
|
||||
)
|
||||
|
||||
var hashOIDs = map[crypto.Hash]asn1.ObjectIdentifier{
|
||||
crypto.SHA1: asn1.ObjectIdentifier([]int{1, 3, 14, 3, 2, 26}),
|
||||
crypto.SHA256: asn1.ObjectIdentifier([]int{2, 16, 840, 1, 101, 3, 4, 2, 1}),
|
||||
crypto.SHA384: asn1.ObjectIdentifier([]int{2, 16, 840, 1, 101, 3, 4, 2, 2}),
|
||||
crypto.SHA512: asn1.ObjectIdentifier([]int{2, 16, 840, 1, 101, 3, 4, 2, 3}),
|
||||
}
|
||||
|
||||
// TODO(rlb): This is also from crypto/x509, so same comment as AGL's below
|
||||
var signatureAlgorithmDetails = []struct {
|
||||
algo x509.SignatureAlgorithm
|
||||
oid asn1.ObjectIdentifier
|
||||
pubKeyAlgo x509.PublicKeyAlgorithm
|
||||
hash crypto.Hash
|
||||
}{
|
||||
{x509.MD2WithRSA, oidSignatureMD2WithRSA, x509.RSA, crypto.Hash(0) /* no value for MD2 */},
|
||||
{x509.MD5WithRSA, oidSignatureMD5WithRSA, x509.RSA, crypto.MD5},
|
||||
{x509.SHA1WithRSA, oidSignatureSHA1WithRSA, x509.RSA, crypto.SHA1},
|
||||
{x509.SHA256WithRSA, oidSignatureSHA256WithRSA, x509.RSA, crypto.SHA256},
|
||||
{x509.SHA384WithRSA, oidSignatureSHA384WithRSA, x509.RSA, crypto.SHA384},
|
||||
{x509.SHA512WithRSA, oidSignatureSHA512WithRSA, x509.RSA, crypto.SHA512},
|
||||
{x509.DSAWithSHA1, oidSignatureDSAWithSHA1, x509.DSA, crypto.SHA1},
|
||||
{x509.DSAWithSHA256, oidSignatureDSAWithSHA256, x509.DSA, crypto.SHA256},
|
||||
{x509.ECDSAWithSHA1, oidSignatureECDSAWithSHA1, x509.ECDSA, crypto.SHA1},
|
||||
{x509.ECDSAWithSHA256, oidSignatureECDSAWithSHA256, x509.ECDSA, crypto.SHA256},
|
||||
{x509.ECDSAWithSHA384, oidSignatureECDSAWithSHA384, x509.ECDSA, crypto.SHA384},
|
||||
{x509.ECDSAWithSHA512, oidSignatureECDSAWithSHA512, x509.ECDSA, crypto.SHA512},
|
||||
}
|
||||
|
||||
// TODO(rlb): This is also from crypto/x509, so same comment as AGL's below
|
||||
func signingParamsForPublicKey(pub interface{}, requestedSigAlgo x509.SignatureAlgorithm) (hashFunc crypto.Hash, sigAlgo pkix.AlgorithmIdentifier, err error) {
|
||||
var pubType x509.PublicKeyAlgorithm
|
||||
|
||||
switch pub := pub.(type) {
|
||||
case *rsa.PublicKey:
|
||||
pubType = x509.RSA
|
||||
hashFunc = crypto.SHA256
|
||||
sigAlgo.Algorithm = oidSignatureSHA256WithRSA
|
||||
sigAlgo.Parameters = asn1.RawValue{
|
||||
Tag: 5,
|
||||
}
|
||||
|
||||
case *ecdsa.PublicKey:
|
||||
pubType = x509.ECDSA
|
||||
|
||||
switch pub.Curve {
|
||||
case elliptic.P224(), elliptic.P256():
|
||||
hashFunc = crypto.SHA256
|
||||
sigAlgo.Algorithm = oidSignatureECDSAWithSHA256
|
||||
case elliptic.P384():
|
||||
hashFunc = crypto.SHA384
|
||||
sigAlgo.Algorithm = oidSignatureECDSAWithSHA384
|
||||
case elliptic.P521():
|
||||
hashFunc = crypto.SHA512
|
||||
sigAlgo.Algorithm = oidSignatureECDSAWithSHA512
|
||||
default:
|
||||
err = errors.New("x509: unknown elliptic curve")
|
||||
}
|
||||
|
||||
default:
|
||||
err = errors.New("x509: only RSA and ECDSA keys supported")
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
if requestedSigAlgo == 0 {
|
||||
return
|
||||
}
|
||||
|
||||
found := false
|
||||
for _, details := range signatureAlgorithmDetails {
|
||||
if details.algo == requestedSigAlgo {
|
||||
if details.pubKeyAlgo != pubType {
|
||||
err = errors.New("x509: requested SignatureAlgorithm does not match private key type")
|
||||
return
|
||||
}
|
||||
sigAlgo.Algorithm, hashFunc = details.oid, details.hash
|
||||
if hashFunc == 0 {
|
||||
err = errors.New("x509: cannot sign with hash function requested")
|
||||
return
|
||||
}
|
||||
found = true
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if !found {
|
||||
err = errors.New("x509: unknown SignatureAlgorithm")
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// TODO(agl): this is taken from crypto/x509 and so should probably be exported
|
||||
// from crypto/x509 or crypto/x509/pkix.
|
||||
func getSignatureAlgorithmFromOID(oid asn1.ObjectIdentifier) x509.SignatureAlgorithm {
|
||||
for _, details := range signatureAlgorithmDetails {
|
||||
if oid.Equal(details.oid) {
|
||||
return details.algo
|
||||
}
|
||||
}
|
||||
return x509.UnknownSignatureAlgorithm
|
||||
}
|
||||
|
||||
// TODO(rlb): This is not taken from crypto/x509, but it's of the same general form.
|
||||
func getHashAlgorithmFromOID(target asn1.ObjectIdentifier) crypto.Hash {
|
||||
for hash, oid := range hashOIDs {
|
||||
if oid.Equal(target) {
|
||||
return hash
|
||||
}
|
||||
}
|
||||
return crypto.Hash(0)
|
||||
}
|
||||
|
||||
func getOIDFromHashAlgorithm(target crypto.Hash) asn1.ObjectIdentifier {
|
||||
for hash, oid := range hashOIDs {
|
||||
if hash == target {
|
||||
return oid
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// This is the exposed reflection of the internal OCSP structures.
|
||||
|
||||
// The status values that can be expressed in OCSP. See RFC 6960.
|
||||
const (
|
||||
// Good means that the certificate is valid.
|
||||
Good = iota
|
||||
// Revoked means that the certificate has been deliberately revoked.
|
||||
Revoked
|
||||
// Unknown means that the OCSP responder doesn't know about the certificate.
|
||||
Unknown
|
||||
// ServerFailed is unused and was never used (see
|
||||
// https://go-review.googlesource.com/#/c/18944). ParseResponse will
|
||||
// return a ResponseError when an error response is parsed.
|
||||
ServerFailed
|
||||
)
|
||||
|
||||
// The enumerated reasons for revoking a certificate. See RFC 5280.
|
||||
const (
|
||||
Unspecified = 0
|
||||
KeyCompromise = 1
|
||||
CACompromise = 2
|
||||
AffiliationChanged = 3
|
||||
Superseded = 4
|
||||
CessationOfOperation = 5
|
||||
CertificateHold = 6
|
||||
|
||||
RemoveFromCRL = 8
|
||||
PrivilegeWithdrawn = 9
|
||||
AACompromise = 10
|
||||
)
|
||||
|
||||
// Request represents an OCSP request. See RFC 6960.
|
||||
type Request struct {
|
||||
HashAlgorithm crypto.Hash
|
||||
IssuerNameHash []byte
|
||||
IssuerKeyHash []byte
|
||||
SerialNumber *big.Int
|
||||
}
|
||||
|
||||
// Marshal marshals the OCSP request to ASN.1 DER encoded form.
|
||||
func (req *Request) Marshal() ([]byte, error) {
|
||||
hashAlg := getOIDFromHashAlgorithm(req.HashAlgorithm)
|
||||
if hashAlg == nil {
|
||||
return nil, errors.New("Unknown hash algorithm")
|
||||
}
|
||||
return asn1.Marshal(ocspRequest{
|
||||
tbsRequest{
|
||||
Version: 0,
|
||||
RequestList: []request{
|
||||
{
|
||||
Cert: certID{
|
||||
pkix.AlgorithmIdentifier{
|
||||
Algorithm: hashAlg,
|
||||
Parameters: asn1.RawValue{Tag: 5 /* ASN.1 NULL */},
|
||||
},
|
||||
req.IssuerNameHash,
|
||||
req.IssuerKeyHash,
|
||||
req.SerialNumber,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
// Response represents an OCSP response containing a single SingleResponse. See
|
||||
// RFC 6960.
|
||||
type Response struct {
|
||||
// Status is one of {Good, Revoked, Unknown}
|
||||
Status int
|
||||
SerialNumber *big.Int
|
||||
ProducedAt, ThisUpdate, NextUpdate, RevokedAt time.Time
|
||||
RevocationReason int
|
||||
Certificate *x509.Certificate
|
||||
// TBSResponseData contains the raw bytes of the signed response. If
|
||||
// Certificate is nil then this can be used to verify Signature.
|
||||
TBSResponseData []byte
|
||||
Signature []byte
|
||||
SignatureAlgorithm x509.SignatureAlgorithm
|
||||
|
||||
// IssuerHash is the hash used to compute the IssuerNameHash and IssuerKeyHash.
|
||||
// Valid values are crypto.SHA1, crypto.SHA256, crypto.SHA384, and crypto.SHA512.
|
||||
// If zero, the default is crypto.SHA1.
|
||||
IssuerHash crypto.Hash
|
||||
|
||||
// RawResponderName optionally contains the DER-encoded subject of the
|
||||
// responder certificate. Exactly one of RawResponderName and
|
||||
// ResponderKeyHash is set.
|
||||
RawResponderName []byte
|
||||
// ResponderKeyHash optionally contains the SHA-1 hash of the
|
||||
// responder's public key. Exactly one of RawResponderName and
|
||||
// ResponderKeyHash is set.
|
||||
ResponderKeyHash []byte
|
||||
|
||||
// Extensions contains raw X.509 extensions from the singleExtensions field
|
||||
// of the OCSP response. When parsing certificates, this can be used to
|
||||
// extract non-critical extensions that are not parsed by this package. When
|
||||
// marshaling OCSP responses, the Extensions field is ignored, see
|
||||
// ExtraExtensions.
|
||||
Extensions []pkix.Extension
|
||||
|
||||
// ExtraExtensions contains extensions to be copied, raw, into any marshaled
|
||||
// OCSP response (in the singleExtensions field). Values override any
|
||||
// extensions that would otherwise be produced based on the other fields. The
|
||||
// ExtraExtensions field is not populated when parsing certificates, see
|
||||
// Extensions.
|
||||
ExtraExtensions []pkix.Extension
|
||||
}
|
||||
|
||||
// These are pre-serialized error responses for the various non-success codes
|
||||
// defined by OCSP. The Unauthorized code in particular can be used by an OCSP
|
||||
// responder that supports only pre-signed responses as a response to requests
|
||||
// for certificates with unknown status. See RFC 5019.
|
||||
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}
|
||||
)
|
||||
|
||||
// CheckSignatureFrom checks that the signature in resp is a valid signature
|
||||
// from issuer. This should only be used if resp.Certificate is nil. Otherwise,
|
||||
// the OCSP response contained an intermediate certificate that created the
|
||||
// signature. That signature is checked by ParseResponse and only
|
||||
// resp.Certificate remains to be validated.
|
||||
func (resp *Response) CheckSignatureFrom(issuer *x509.Certificate) error {
|
||||
return issuer.CheckSignature(resp.SignatureAlgorithm, resp.TBSResponseData, resp.Signature)
|
||||
}
|
||||
|
||||
// ParseError results from an invalid OCSP response.
|
||||
type ParseError string
|
||||
|
||||
func (p ParseError) Error() string {
|
||||
return string(p)
|
||||
}
|
||||
|
||||
// ParseRequest parses an OCSP request in DER form. It only supports
|
||||
// requests for a single certificate. Signed requests are not supported.
|
||||
// If a request includes a signature, it will result in a ParseError.
|
||||
func ParseRequest(bytes []byte) (*Request, error) {
|
||||
var req ocspRequest
|
||||
rest, err := asn1.Unmarshal(bytes, &req)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if len(rest) > 0 {
|
||||
return nil, ParseError("trailing data in OCSP request")
|
||||
}
|
||||
|
||||
if len(req.TBSRequest.RequestList) == 0 {
|
||||
return nil, ParseError("OCSP request contains no request body")
|
||||
}
|
||||
innerRequest := req.TBSRequest.RequestList[0]
|
||||
|
||||
hashFunc := getHashAlgorithmFromOID(innerRequest.Cert.HashAlgorithm.Algorithm)
|
||||
if hashFunc == crypto.Hash(0) {
|
||||
return nil, ParseError("OCSP request uses unknown hash function")
|
||||
}
|
||||
|
||||
return &Request{
|
||||
HashAlgorithm: hashFunc,
|
||||
IssuerNameHash: innerRequest.Cert.NameHash,
|
||||
IssuerKeyHash: innerRequest.Cert.IssuerKeyHash,
|
||||
SerialNumber: innerRequest.Cert.SerialNumber,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// ParseResponse parses an OCSP response in DER form. The response must contain
|
||||
// only one certificate status. To parse the status of a specific certificate
|
||||
// from a response which may contain multiple statuses, use ParseResponseForCert
|
||||
// instead.
|
||||
//
|
||||
// If the response contains an embedded certificate, then that certificate will
|
||||
// be used to verify the response signature. If the response contains an
|
||||
// embedded certificate and issuer is not nil, then issuer will be used to verify
|
||||
// the signature on the embedded certificate.
|
||||
//
|
||||
// If the response does not contain an embedded certificate and issuer is not
|
||||
// nil, then issuer will be used to verify the response signature.
|
||||
//
|
||||
// Invalid responses and parse failures will result in a ParseError.
|
||||
// Error responses will result in a ResponseError.
|
||||
func ParseResponse(bytes []byte, issuer *x509.Certificate) (*Response, error) {
|
||||
return ParseResponseForCert(bytes, nil, issuer)
|
||||
}
|
||||
|
||||
// ParseResponseForCert acts identically to ParseResponse, except it supports
|
||||
// parsing responses that contain multiple statuses. If the response contains
|
||||
// multiple statuses and cert is not nil, then ParseResponseForCert will return
|
||||
// the first status which contains a matching serial, otherwise it will return an
|
||||
// error. If cert is nil, then the first status in the response will be returned.
|
||||
func ParseResponseForCert(bytes []byte, cert, issuer *x509.Certificate) (*Response, error) {
|
||||
var resp responseASN1
|
||||
rest, err := asn1.Unmarshal(bytes, &resp)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if len(rest) > 0 {
|
||||
return nil, ParseError("trailing data in OCSP response")
|
||||
}
|
||||
|
||||
if status := ResponseStatus(resp.Status); status != Success {
|
||||
return nil, ResponseError{status}
|
||||
}
|
||||
|
||||
if !resp.Response.ResponseType.Equal(idPKIXOCSPBasic) {
|
||||
return nil, ParseError("bad OCSP response type")
|
||||
}
|
||||
|
||||
var basicResp basicResponse
|
||||
rest, err = asn1.Unmarshal(resp.Response.Response, &basicResp)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if len(rest) > 0 {
|
||||
return nil, ParseError("trailing data in OCSP response")
|
||||
}
|
||||
|
||||
if n := len(basicResp.TBSResponseData.Responses); n == 0 || cert == nil && n > 1 {
|
||||
return nil, ParseError("OCSP response contains bad number of responses")
|
||||
}
|
||||
|
||||
var singleResp singleResponse
|
||||
if cert == nil {
|
||||
singleResp = basicResp.TBSResponseData.Responses[0]
|
||||
} else {
|
||||
match := false
|
||||
for _, resp := range basicResp.TBSResponseData.Responses {
|
||||
if cert.SerialNumber.Cmp(resp.CertID.SerialNumber) == 0 {
|
||||
singleResp = resp
|
||||
match = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !match {
|
||||
return nil, ParseError("no response matching the supplied certificate")
|
||||
}
|
||||
}
|
||||
|
||||
ret := &Response{
|
||||
TBSResponseData: basicResp.TBSResponseData.Raw,
|
||||
Signature: basicResp.Signature.RightAlign(),
|
||||
SignatureAlgorithm: getSignatureAlgorithmFromOID(basicResp.SignatureAlgorithm.Algorithm),
|
||||
Extensions: singleResp.SingleExtensions,
|
||||
SerialNumber: singleResp.CertID.SerialNumber,
|
||||
ProducedAt: basicResp.TBSResponseData.ProducedAt,
|
||||
ThisUpdate: singleResp.ThisUpdate,
|
||||
NextUpdate: singleResp.NextUpdate,
|
||||
}
|
||||
|
||||
// Handle the ResponderID CHOICE tag. ResponderID can be flattened into
|
||||
// TBSResponseData once https://go-review.googlesource.com/34503 has been
|
||||
// released.
|
||||
rawResponderID := basicResp.TBSResponseData.RawResponderID
|
||||
switch rawResponderID.Tag {
|
||||
case 1: // Name
|
||||
var rdn pkix.RDNSequence
|
||||
if rest, err := asn1.Unmarshal(rawResponderID.Bytes, &rdn); err != nil || len(rest) != 0 {
|
||||
return nil, ParseError("invalid responder name")
|
||||
}
|
||||
ret.RawResponderName = rawResponderID.Bytes
|
||||
case 2: // KeyHash
|
||||
if rest, err := asn1.Unmarshal(rawResponderID.Bytes, &ret.ResponderKeyHash); err != nil || len(rest) != 0 {
|
||||
return nil, ParseError("invalid responder key hash")
|
||||
}
|
||||
default:
|
||||
return nil, ParseError("invalid responder id tag")
|
||||
}
|
||||
|
||||
if len(basicResp.Certificates) > 0 {
|
||||
// Responders should only send a single certificate (if they
|
||||
// send any) that connects the responder's certificate to the
|
||||
// original issuer. We accept responses with multiple
|
||||
// certificates due to a number responders sending them[1], but
|
||||
// ignore all but the first.
|
||||
//
|
||||
// [1] https://github.com/golang/go/issues/21527
|
||||
ret.Certificate, err = x509.ParseCertificate(basicResp.Certificates[0].FullBytes)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if err := ret.CheckSignatureFrom(ret.Certificate); err != nil {
|
||||
return nil, ParseError("bad signature on embedded certificate: " + err.Error())
|
||||
}
|
||||
|
||||
if issuer != nil {
|
||||
if err := issuer.CheckSignature(ret.Certificate.SignatureAlgorithm, ret.Certificate.RawTBSCertificate, ret.Certificate.Signature); err != nil {
|
||||
return nil, ParseError("bad OCSP signature: " + err.Error())
|
||||
}
|
||||
}
|
||||
} else if issuer != nil {
|
||||
if err := ret.CheckSignatureFrom(issuer); err != nil {
|
||||
return nil, ParseError("bad OCSP signature: " + err.Error())
|
||||
}
|
||||
}
|
||||
|
||||
for _, ext := range singleResp.SingleExtensions {
|
||||
if ext.Critical {
|
||||
return nil, ParseError("unsupported critical extension")
|
||||
}
|
||||
}
|
||||
|
||||
for h, oid := range hashOIDs {
|
||||
if singleResp.CertID.HashAlgorithm.Algorithm.Equal(oid) {
|
||||
ret.IssuerHash = h
|
||||
break
|
||||
}
|
||||
}
|
||||
if ret.IssuerHash == 0 {
|
||||
return nil, ParseError("unsupported issuer hash algorithm")
|
||||
}
|
||||
|
||||
switch {
|
||||
case bool(singleResp.Good):
|
||||
ret.Status = Good
|
||||
case bool(singleResp.Unknown):
|
||||
ret.Status = Unknown
|
||||
default:
|
||||
ret.Status = Revoked
|
||||
ret.RevokedAt = singleResp.Revoked.RevocationTime
|
||||
ret.RevocationReason = int(singleResp.Revoked.Reason)
|
||||
}
|
||||
|
||||
return ret, nil
|
||||
}
|
||||
|
||||
// RequestOptions contains options for constructing OCSP requests.
|
||||
type RequestOptions struct {
|
||||
// Hash contains the hash function that should be used when
|
||||
// constructing the OCSP request. If zero, SHA-1 will be used.
|
||||
Hash crypto.Hash
|
||||
}
|
||||
|
||||
func (opts *RequestOptions) hash() crypto.Hash {
|
||||
if opts == nil || opts.Hash == 0 {
|
||||
// SHA-1 is nearly universally used in OCSP.
|
||||
return crypto.SHA1
|
||||
}
|
||||
return opts.Hash
|
||||
}
|
||||
|
||||
// CreateRequest returns a DER-encoded, OCSP request for the status of cert. If
|
||||
// opts is nil then sensible defaults are used.
|
||||
func CreateRequest(cert, issuer *x509.Certificate, opts *RequestOptions) ([]byte, error) {
|
||||
hashFunc := opts.hash()
|
||||
|
||||
// OCSP seems to be the only place where these raw hash identifiers are
|
||||
// used. I took the following from
|
||||
// http://msdn.microsoft.com/en-us/library/ff635603.aspx
|
||||
_, ok := hashOIDs[hashFunc]
|
||||
if !ok {
|
||||
return nil, x509.ErrUnsupportedAlgorithm
|
||||
}
|
||||
|
||||
if !hashFunc.Available() {
|
||||
return nil, x509.ErrUnsupportedAlgorithm
|
||||
}
|
||||
h := opts.hash().New()
|
||||
|
||||
var publicKeyInfo struct {
|
||||
Algorithm pkix.AlgorithmIdentifier
|
||||
PublicKey asn1.BitString
|
||||
}
|
||||
if _, err := asn1.Unmarshal(issuer.RawSubjectPublicKeyInfo, &publicKeyInfo); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
h.Write(publicKeyInfo.PublicKey.RightAlign())
|
||||
issuerKeyHash := h.Sum(nil)
|
||||
|
||||
h.Reset()
|
||||
h.Write(issuer.RawSubject)
|
||||
issuerNameHash := h.Sum(nil)
|
||||
|
||||
req := &Request{
|
||||
HashAlgorithm: hashFunc,
|
||||
IssuerNameHash: issuerNameHash,
|
||||
IssuerKeyHash: issuerKeyHash,
|
||||
SerialNumber: cert.SerialNumber,
|
||||
}
|
||||
return req.Marshal()
|
||||
}
|
||||
|
||||
// CreateResponse returns a DER-encoded OCSP response with the specified contents.
|
||||
// The fields in the response are populated as follows:
|
||||
//
|
||||
// The responder cert is used to populate the responder's name field, and the
|
||||
// certificate itself is provided alongside the OCSP response signature.
|
||||
//
|
||||
// The issuer cert is used to puplate the IssuerNameHash and IssuerKeyHash fields.
|
||||
//
|
||||
// The template is used to populate the SerialNumber, Status, RevokedAt,
|
||||
// RevocationReason, ThisUpdate, and NextUpdate fields.
|
||||
//
|
||||
// If template.IssuerHash is not set, SHA1 will be used.
|
||||
//
|
||||
// The ProducedAt date is automatically set to the current date, to the nearest minute.
|
||||
func CreateResponse(issuer, responderCert *x509.Certificate, template Response, priv crypto.Signer, extensions []pkix.Extension) ([]byte, error) {
|
||||
var publicKeyInfo struct {
|
||||
Algorithm pkix.AlgorithmIdentifier
|
||||
PublicKey asn1.BitString
|
||||
}
|
||||
if _, err := asn1.Unmarshal(issuer.RawSubjectPublicKeyInfo, &publicKeyInfo); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if template.IssuerHash == 0 {
|
||||
template.IssuerHash = crypto.SHA1
|
||||
}
|
||||
hashOID := getOIDFromHashAlgorithm(template.IssuerHash)
|
||||
if hashOID == nil {
|
||||
return nil, errors.New("unsupported issuer hash algorithm")
|
||||
}
|
||||
|
||||
if !template.IssuerHash.Available() {
|
||||
return nil, fmt.Errorf("issuer hash algorithm %v not linked into binary", template.IssuerHash)
|
||||
}
|
||||
h := template.IssuerHash.New()
|
||||
h.Write(publicKeyInfo.PublicKey.RightAlign())
|
||||
issuerKeyHash := h.Sum(nil)
|
||||
|
||||
h.Reset()
|
||||
h.Write(issuer.RawSubject)
|
||||
issuerNameHash := h.Sum(nil)
|
||||
|
||||
innerResponse := singleResponse{
|
||||
CertID: certID{
|
||||
HashAlgorithm: pkix.AlgorithmIdentifier{
|
||||
Algorithm: hashOID,
|
||||
Parameters: asn1.RawValue{Tag: 5 /* ASN.1 NULL */},
|
||||
},
|
||||
NameHash: issuerNameHash,
|
||||
IssuerKeyHash: issuerKeyHash,
|
||||
SerialNumber: template.SerialNumber,
|
||||
},
|
||||
ThisUpdate: template.ThisUpdate.UTC(),
|
||||
NextUpdate: template.NextUpdate.UTC(),
|
||||
SingleExtensions: template.ExtraExtensions,
|
||||
}
|
||||
|
||||
switch template.Status {
|
||||
case Good:
|
||||
innerResponse.Good = true
|
||||
case Unknown:
|
||||
innerResponse.Unknown = true
|
||||
case Revoked:
|
||||
innerResponse.Revoked = revokedInfo{
|
||||
RevocationTime: template.RevokedAt.UTC(),
|
||||
Reason: asn1.Enumerated(template.RevocationReason),
|
||||
}
|
||||
}
|
||||
|
||||
rawResponderID := asn1.RawValue{
|
||||
Class: 2, // context-specific
|
||||
Tag: 1, // Name (explicit tag)
|
||||
IsCompound: true,
|
||||
Bytes: responderCert.RawSubject,
|
||||
}
|
||||
tbsResponseData := responseData{
|
||||
Version: 0,
|
||||
RawResponderID: rawResponderID,
|
||||
ProducedAt: time.Now().Truncate(time.Minute).UTC(),
|
||||
Responses: []singleResponse{innerResponse},
|
||||
ResponseExtensions: extensions,
|
||||
}
|
||||
|
||||
tbsResponseDataDER, err := asn1.Marshal(tbsResponseData)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
hashFunc, signatureAlgorithm, err := signingParamsForPublicKey(priv.Public(), template.SignatureAlgorithm)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
responseHash := hashFunc.New()
|
||||
responseHash.Write(tbsResponseDataDER)
|
||||
signature, err := priv.Sign(rand.Reader, responseHash.Sum(nil), hashFunc)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
response := basicResponse{
|
||||
TBSResponseData: tbsResponseData,
|
||||
SignatureAlgorithm: signatureAlgorithm,
|
||||
Signature: asn1.BitString{
|
||||
Bytes: signature,
|
||||
BitLength: 8 * len(signature),
|
||||
},
|
||||
}
|
||||
if template.Certificate != nil {
|
||||
response.Certificates = []asn1.RawValue{
|
||||
{FullBytes: template.Certificate.Raw},
|
||||
}
|
||||
}
|
||||
responseDER, err := asn1.Marshal(response)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return asn1.Marshal(responseASN1{
|
||||
Status: asn1.Enumerated(Success),
|
||||
Response: responseBytes{
|
||||
ResponseType: idPKIXOCSPBasic,
|
||||
Response: responseDER,
|
||||
},
|
||||
})
|
||||
}
|
@ -0,0 +1,349 @@
|
||||
// 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/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"regexp"
|
||||
"time"
|
||||
|
||||
"github.com/jmhodges/clock"
|
||||
"github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
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(*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 *Request) ([]byte, http.Header, error) {
|
||||
response, present := src[request.SerialNumber.String()]
|
||||
if !present {
|
||||
return nil, nil, ErrNotFound
|
||||
}
|
||||
return response, 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 := ioutil.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 {
|
||||
logrus.Errorf("Base64 decode error %s on: %s", tmpErr, b64)
|
||||
continue
|
||||
}
|
||||
|
||||
response, tmpErr := ParseResponse(der, nil)
|
||||
if tmpErr != nil {
|
||||
logrus.Errorf("OCSP decode error %s on: %s", tmpErr, b64)
|
||||
continue
|
||||
}
|
||||
|
||||
src[response.SerialNumber.String()] = der
|
||||
}
|
||||
|
||||
logrus.Infof("Read %d OCSP responses", len(src))
|
||||
return src, nil
|
||||
}
|
||||
|
||||
// Stats is a basic interface that allows users to record information
|
||||
// about returned responses
|
||||
type Stats interface {
|
||||
ResponseStatus(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
|
||||
logrus.Debugf("failed to marshal log event object: %s", err)
|
||||
return
|
||||
}
|
||||
logrus.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 {
|
||||
logrus.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 {
|
||||
logrus.Debugf("Error decoding base64 from URL: %s", string(base64RequestBytes))
|
||||
response.WriteHeader(http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
case "POST":
|
||||
requestBody, err = ioutil.ReadAll(request.Body)
|
||||
if err != nil {
|
||||
logrus.Errorf("Problem reading body of POST: %s", err)
|
||||
response.WriteHeader(http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
default:
|
||||
response.WriteHeader(http.StatusMethodNotAllowed)
|
||||
return
|
||||
}
|
||||
b64Body := base64.StdEncoding.EncodeToString(requestBody)
|
||||
logrus.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 := ParseRequest(requestBody)
|
||||
if err != nil {
|
||||
logrus.Debugf("Error decoding request body: %s", b64Body)
|
||||
response.WriteHeader(http.StatusBadRequest)
|
||||
response.Write(malformedRequestErrorResponse)
|
||||
if rs.stats != nil {
|
||||
rs.stats.ResponseStatus(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 {
|
||||
logrus.Infof("No response found for request: serial %x, request body %s",
|
||||
ocspRequest.SerialNumber, b64Body)
|
||||
response.Write(unauthorizedErrorResponse)
|
||||
if rs.stats != nil {
|
||||
rs.stats.ResponseStatus(Unauthorized)
|
||||
}
|
||||
return
|
||||
}
|
||||
logrus.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(InternalError)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
parsedResponse, err := ParseResponse(ocspResponse, nil)
|
||||
if err != nil {
|
||||
logrus.Errorf("Error parsing response for serial %x: %s",
|
||||
ocspRequest.SerialNumber, err)
|
||||
response.Write(internalErrorErrorResponse)
|
||||
if rs.stats != nil {
|
||||
rs.stats.ResponseStatus(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(Success)
|
||||
}
|
||||
}
|
@ -0,0 +1,199 @@
|
||||
package ocsp
|
||||
|
||||
import (
|
||||
"encoding/hex"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"net/url"
|
||||
"reflect"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/jmhodges/clock"
|
||||
)
|
||||
|
||||
const (
|
||||
responseFile = "testdata/resp64.pem"
|
||||
binResponseFile = "testdata/response.der"
|
||||
brokenResponseFile = "testdata/response_broken.pem"
|
||||
mixResponseFile = "testdata/response_mix.pem"
|
||||
)
|
||||
|
||||
type testSource struct{}
|
||||
|
||||
func (ts testSource) Response(r *Request) ([]byte, http.Header, error) {
|
||||
return []byte("hi"), nil, nil
|
||||
}
|
||||
|
||||
type testCase struct {
|
||||
method, path string
|
||||
expected int
|
||||
}
|
||||
|
||||
func TestOCSP(t *testing.T) {
|
||||
cases := []testCase{
|
||||
{"OPTIONS", "/", http.StatusMethodNotAllowed},
|
||||
{"GET", "/", http.StatusBadRequest},
|
||||
// Bad URL encoding
|
||||
{"GET", "%ZZFQwUjBQME4wTDAJBgUrDgMCGgUABBQ55F6w46hhx%2Fo6OXOHa%2BYfe32YhgQU%2B3hPEvlgFYMsnxd%2FNBmzLjbqQYkCEwD6Wh0MaVKu9gJ3By9DI%2F%2Fxsd4%3D", http.StatusBadRequest},
|
||||
// Bad URL encoding
|
||||
{"GET", "%%FQwUjBQME4wTDAJBgUrDgMCGgUABBQ55F6w46hhx%2Fo6OXOHa%2BYfe32YhgQU%2B3hPEvlgFYMsnxd%2FNBmzLjbqQYkCEwD6Wh0MaVKu9gJ3By9DI%2F%2Fxsd4%3D", http.StatusBadRequest},
|
||||
// Bad base64 encoding
|
||||
{"GET", "==MFQwUjBQME4wTDAJBgUrDgMCGgUABBQ55F6w46hhx%2Fo6OXOHa%2BYfe32YhgQU%2B3hPEvlgFYMsnxd%2FNBmzLjbqQYkCEwD6Wh0MaVKu9gJ3By9DI%2F%2Fxsd4%3D", http.StatusBadRequest},
|
||||
// Bad OCSP DER encoding
|
||||
{"GET", "AAAMFQwUjBQME4wTDAJBgUrDgMCGgUABBQ55F6w46hhx%2Fo6OXOHa%2BYfe32YhgQU%2B3hPEvlgFYMsnxd%2FNBmzLjbqQYkCEwD6Wh0MaVKu9gJ3By9DI%2F%2Fxsd4%3D", http.StatusBadRequest},
|
||||
// Good encoding all around, including a double slash
|
||||
{"GET", "MFQwUjBQME4wTDAJBgUrDgMCGgUABBQ55F6w46hhx%2Fo6OXOHa%2BYfe32YhgQU%2B3hPEvlgFYMsnxd%2FNBmzLjbqQYkCEwD6Wh0MaVKu9gJ3By9DI%2F%2Fxsd4%3D", http.StatusOK},
|
||||
// Good request, leading slash
|
||||
{"GET", "/MFQwUjBQME4wTDAJBgUrDgMCGgUABBQ55F6w46hhx%2Fo6OXOHa%2BYfe32YhgQU%2B3hPEvlgFYMsnxd%2FNBmzLjbqQYkCEwD6Wh0MaVKu9gJ3By9DI%2F%2Fxsd4%3D", http.StatusOK},
|
||||
}
|
||||
|
||||
responder := Responder{
|
||||
Source: testSource{},
|
||||
clk: clock.NewFake(),
|
||||
}
|
||||
|
||||
for _, tc := range cases {
|
||||
rw := httptest.NewRecorder()
|
||||
|
||||
responder.ServeHTTP(rw, &http.Request{
|
||||
Method: tc.method,
|
||||
URL: &url.URL{
|
||||
Path: tc.path,
|
||||
},
|
||||
})
|
||||
if rw.Code != tc.expected {
|
||||
t.Errorf("Incorrect response code: got %d, wanted %d", rw.Code, tc.expected)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var testResp = `308204f90a0100a08204f2308204ee06092b0601050507300101048204df308204db3081a7a003020100a121301f311d301b06035504030c146861707079206861636b65722066616b65204341180f32303135303932333231303630305a306c306a3042300906052b0e03021a0500041439e45eb0e3a861c7fa3a3973876be61f7b7d98860414fb784f12f96015832c9f177f3419b32e36ea41890209009cf1912ea8d509088000180f32303135303932333030303030305aa011180f32303330303832363030303030305a300d06092a864886f70d01010b05000382010100c17ed5f12c408d214092c86cb2d6ba9881637a9d5cafb8ddc05aed85806a554c37abdd83c2e00a4bb25b2d0dda1e1c0be65144377471bca53f14616f379ee0c0b436c697b400b7eba9513c5be6d92fbc817586d568156293cfa0099d64585146def907dee36eb650c424a00207b01813aa7ae90e65045339482eeef12b6fa8656315da8f8bb1375caa29ac3858f891adb85066c35b5176e154726ae746016e42e0d6016668ff10a8aa9637417d29be387a1bdba9268b13558034ab5f3e498a47fb096f2e1b39236b22956545884fbbed1884f1bc9686b834d8def4802bac8f79924a36867af87412f808977abaf6457f3cda9e7eccbd0731bcd04865b899ee41a08203193082031530820311308201f9a0030201020209009cf1912ea8d50908300d06092a864886f70d01010b0500301f311d301b06035504030c146861707079206861636b65722066616b65204341301e170d3135303430373233353033385a170d3235303430343233353033385a301f311d301b06035504030c146861707079206861636b65722066616b6520434130820122300d06092a864886f70d01010105000382010f003082010a0282010100c20a47799a05c512b27717633413d770f936bf99de62f130c8774d476deac0029aa6c9d1bb519605df32d34b336394d48e9adc9bbeb48652767dafdb5241c2fc54ce9650e33cb672298888c403642407270cc2f46667f07696d3dd62cfd1f41a8dc0ed60d7c18366b1d2cd462d34a35e148e8695a9a3ec62b656bd129a211a9a534847992d005b0412bcdffdde23085eeca2c32c2693029b5a79f1090fe0b1cb4a154b5c36bc04c7d5a08fa2a58700d3c88d5059205bc5560dc9480f1732b1ad29b030ed3235f7fb868f904fdc79f98ffb5c4e7d4b831ce195f171729ec3f81294df54e66bd3f83d81843b640aea5d7ec64d0905a9dbb03e6ff0e6ac523d36ab0203010001a350304e301d0603551d0e04160414fb784f12f96015832c9f177f3419b32e36ea4189301f0603551d23041830168014fb784f12f96015832c9f177f3419b32e36ea4189300c0603551d13040530030101ff300d06092a864886f70d01010b050003820101001df436be66ff938ccbfb353026962aa758763a777531119377845109e7c2105476c165565d5bbce1464b41bd1d392b079a7341c978af754ca9b3bd7976d485cbbe1d2070d2d4feec1e0f79e8fec9df741e0ea05a26a658d3866825cc1aa2a96a0a04942b2c203cc39501f917a899161dfc461717fe9301fce6ea1afffd7b7998f8941cf76f62def994c028bd1c4b49b17c4d243a6fb058c484968cf80501234da89347108b56b2640cb408e3c336fd72cd355c7f690a15405a7f4ba1e30a6be4a51d262b586f77f8472b207fdd194efab8d3a2683cc148abda7a11b9de1db9307b8ed5a9cd20226f668bd6ac5a3852fd449e42899b7bc915ee747891a110a971`
|
||||
|
||||
type testHeaderSource struct {
|
||||
headers http.Header
|
||||
}
|
||||
|
||||
func (ts testHeaderSource) Response(r *Request) ([]byte, http.Header, error) {
|
||||
resp, _ := hex.DecodeString(testResp)
|
||||
return resp, ts.headers, nil
|
||||
}
|
||||
|
||||
func TestOverrideHeaders(t *testing.T) {
|
||||
headers := http.Header(map[string][]string{
|
||||
"Content-Type": {"yup"},
|
||||
"Cache-Control": {"nope"},
|
||||
"New": {"header"},
|
||||
"Expires": {"0"},
|
||||
"Last-Modified": {"now"},
|
||||
"Etag": {"mhm"},
|
||||
})
|
||||
responder := Responder{
|
||||
Source: testHeaderSource{headers: headers},
|
||||
clk: clock.NewFake(),
|
||||
}
|
||||
|
||||
rw := httptest.NewRecorder()
|
||||
responder.ServeHTTP(rw, &http.Request{
|
||||
Method: "GET",
|
||||
URL: &url.URL{Path: "MFQwUjBQME4wTDAJBgUrDgMCGgUABBQ55F6w46hhx%2Fo6OXOHa%2BYfe32YhgQU%2B3hPEvlgFYMsnxd%2FNBmzLjbqQYkCEwD6Wh0MaVKu9gJ3By9DI%2F%2Fxsd4%3D"},
|
||||
})
|
||||
|
||||
if !reflect.DeepEqual(rw.Header(), headers) {
|
||||
t.Fatalf("Unexpected Headers returned: wanted %s, got %s", headers, rw.Header())
|
||||
}
|
||||
}
|
||||
|
||||
func TestCacheHeaders(t *testing.T) {
|
||||
source, err := NewSourceFromFile(responseFile)
|
||||
if err != nil {
|
||||
t.Fatalf("Error constructing source: %s", err)
|
||||
}
|
||||
|
||||
fc := clock.NewFake()
|
||||
fc.Set(time.Date(2015, 11, 12, 0, 0, 0, 0, time.UTC))
|
||||
responder := Responder{
|
||||
Source: source,
|
||||
clk: fc,
|
||||
}
|
||||
|
||||
rw := httptest.NewRecorder()
|
||||
responder.ServeHTTP(rw, &http.Request{
|
||||
Method: "GET",
|
||||
URL: &url.URL{
|
||||
Path: "MEMwQTA/MD0wOzAJBgUrDgMCGgUABBSwLsMRhyg1dJUwnXWk++D57lvgagQU6aQ/7p6l5vLV13lgPJOmLiSOl6oCAhJN",
|
||||
},
|
||||
})
|
||||
if rw.Code != http.StatusOK {
|
||||
t.Errorf("Unexpected HTTP status code %d", rw.Code)
|
||||
}
|
||||
testCases := []struct {
|
||||
header string
|
||||
value string
|
||||
}{
|
||||
{"Last-Modified", "Tue, 20 Oct 2015 00:00:00 UTC"},
|
||||
{"Expires", "Sun, 20 Oct 2030 00:00:00 UTC"},
|
||||
{"Cache-Control", "max-age=471398400, public, no-transform, must-revalidate"},
|
||||
{"Etag", "\"8169FB0843B081A76E9F6F13FD70C8411597BEACF8B182136FFDD19FBD26140A\""},
|
||||
}
|
||||
for _, tc := range testCases {
|
||||
headers, ok := rw.HeaderMap[tc.header]
|
||||
if !ok {
|
||||
t.Errorf("Header %s missing from HTTP response", tc.header)
|
||||
continue
|
||||
}
|
||||
if len(headers) != 1 {
|
||||
t.Errorf("Wrong number of headers in HTTP response. Wanted 1, got %d", len(headers))
|
||||
continue
|
||||
}
|
||||
actual := headers[0]
|
||||
if actual != tc.value {
|
||||
t.Errorf("Got header %s: %s. Expected %s", tc.header, actual, tc.value)
|
||||
}
|
||||
}
|
||||
|
||||
rw = httptest.NewRecorder()
|
||||
headers := http.Header{}
|
||||
headers.Add("If-None-Match", "\"8169FB0843B081A76E9F6F13FD70C8411597BEACF8B182136FFDD19FBD26140A\"")
|
||||
responder.ServeHTTP(rw, &http.Request{
|
||||
Method: "GET",
|
||||
URL: &url.URL{
|
||||
Path: "MEMwQTA/MD0wOzAJBgUrDgMCGgUABBSwLsMRhyg1dJUwnXWk++D57lvgagQU6aQ/7p6l5vLV13lgPJOmLiSOl6oCAhJN",
|
||||
},
|
||||
Header: headers,
|
||||
})
|
||||
if rw.Code != http.StatusNotModified {
|
||||
t.Fatalf("Got wrong status code: expected %d, got %d", http.StatusNotModified, rw.Code)
|
||||
}
|
||||
}
|
||||
|
||||
func TestNewSourceFromFile(t *testing.T) {
|
||||
_, err := NewSourceFromFile("")
|
||||
if err == nil {
|
||||
t.Fatal("Didn't fail on non-file input")
|
||||
}
|
||||
|
||||
// expected case
|
||||
_, err = NewSourceFromFile(responseFile)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
// binary-formatted file
|
||||
_, err = NewSourceFromFile(binResponseFile)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||