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)
|
||||
}
|
||||
|
||||
// the response file from before, with stuff deleted
|
||||
_, err = NewSourceFromFile(brokenResponseFile)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
// mix of a correct and malformed responses
|
||||
_, err = NewSourceFromFile(mixResponseFile)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
@ -0,0 +1,2 @@
|
||||
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
|
@ -0,0 +1 @@
|
||||
MIICGAoBAKCCAhEwggINBgkrBgEFBQcwAQEEggH+OZ4ZSKS2J85Kr9UaI2LAEFKvOM8/hjk8uyp7KnqJ12h8GOhGZAgIBdaADAQH/GA8wMDAxMDEwMTAwMDAwMFqgERgPMDAwMTAxMDEwMDAwMDBaMA0GCSqGSIb3DQEBCwUAA4IBAQCBGs+8UNwUdkEBladnajZIV+sHtmao/mMTIvpyPqnmV2Ab9KfNWlSDSDuMtZYKS4VsEwtbZ+4kKWI8DugE6egjP3o64R7VP2aqrh41IORwccLGVsexILBpxg4h602JbhXM0sxgXoh5WAt9f1oy6PsHAt/XAuJGSo7yMNv3nHKNFwjExmZt21sNLYlWlljjtX92rlo/mBTWKO0js4YRNyeNQhchARbn9oL18jW0yAVqB9a8rees+EippbTfoktFf0cIhnmkiknPZSZ+dN2qHkxiXIujWlymZzUZcqRTNtrmmhlOdt35QSg7Vw8eyw2rl8ZU94zaI5DPWn1QYn0dk7l9
|
Binary file not shown.
@ -0,0 +1,396 @@
|
||||
package ocspsource
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"crypto"
|
||||
"crypto/rand"
|
||||
"crypto/x509"
|
||||
"crypto/x509/pkix"
|
||||
"encoding/base64"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"math/big"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"git.cacert.org/cacert-goocsp/pkg/ocsp"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/suite"
|
||||
)
|
||||
|
||||
type OcspSourceTestSuite struct {
|
||||
suite.Suite
|
||||
RootKey crypto.Signer
|
||||
IntermediateKey crypto.Signer
|
||||
RootResponderKey crypto.Signer
|
||||
IntermediateResponderKey crypto.Signer
|
||||
RootCertificate *x509.Certificate
|
||||
IntermediateCertificate *x509.Certificate
|
||||
RootResponderCertificate *x509.Certificate
|
||||
IntermediaResponderCertificate *x509.Certificate
|
||||
Server *httptest.Server
|
||||
responder http.Handler
|
||||
RootIssuer *CertificateIssuer
|
||||
IntermediateIssuer *CertificateIssuer
|
||||
}
|
||||
|
||||
type OcspTestHandler struct {
|
||||
suite *OcspSourceTestSuite
|
||||
}
|
||||
|
||||
func (o *OcspTestHandler) ServeHTTP(writer http.ResponseWriter, request *http.Request) {
|
||||
mux := http.NewServeMux()
|
||||
|
||||
mux.HandleFunc("/crl/", o.suite.crlHandler)
|
||||
mux.HandleFunc("/ocsp", o.suite.ocspHandler)
|
||||
mux.ServeHTTP(writer, request)
|
||||
}
|
||||
|
||||
func (suite *OcspSourceTestSuite) crlHandler(_ http.ResponseWriter, _ *http.Request) {
|
||||
panic("implement me")
|
||||
}
|
||||
|
||||
func (suite *OcspSourceTestSuite) ocspHandler(writer http.ResponseWriter, request *http.Request) {
|
||||
suite.responder.ServeHTTP(writer, request)
|
||||
}
|
||||
|
||||
func (suite *OcspSourceTestSuite) SetupSuite() {
|
||||
suite.RootKey = suite.deserializeKey(idxRootKey)
|
||||
|
||||
testHandler := &OcspTestHandler{suite}
|
||||
suite.Server = httptest.NewServer(testHandler)
|
||||
baseUrl := suite.Server.URL
|
||||
|
||||
var (
|
||||
startDate, endDate time.Time
|
||||
template *x509.Certificate
|
||||
serial *big.Int
|
||||
)
|
||||
|
||||
const (
|
||||
organizationalUnit = "CA Ops"
|
||||
organization = "Test CA"
|
||||
)
|
||||
|
||||
serial = suite.newRandomSerial()
|
||||
rootSubject := pkix.Name{
|
||||
CommonName: "Root CA",
|
||||
Organization: []string{organization},
|
||||
OrganizationalUnit: []string{organizationalUnit},
|
||||
}
|
||||
startDate = time.Now().AddDate(-2, 0, 0)
|
||||
endDate = startDate.AddDate(30, 0, 0)
|
||||
template = &x509.Certificate{
|
||||
PublicKey: suite.RootKey.Public(),
|
||||
SerialNumber: serial,
|
||||
Issuer: rootSubject,
|
||||
Subject: rootSubject,
|
||||
NotBefore: startDate,
|
||||
NotAfter: endDate,
|
||||
KeyUsage: x509.KeyUsageCertSign | x509.KeyUsageCRLSign,
|
||||
IsCA: true,
|
||||
MaxPathLen: 3,
|
||||
CRLDistributionPoints: []string{baseUrl + "/crl/root.crl"},
|
||||
}
|
||||
suite.RootCertificate = suite.signCertificate(
|
||||
template,
|
||||
template,
|
||||
suite.RootKey.Public(),
|
||||
suite.RootKey,
|
||||
)
|
||||
|
||||
suite.IntermediateKey = suite.deserializeKey(idxIntermediaryKey)
|
||||
startDate = time.Now().AddDate(-1, 0, 0)
|
||||
endDate = startDate.AddDate(5, 0, 0)
|
||||
serial = suite.newRandomSerial()
|
||||
intermediateSubject := pkix.Name{
|
||||
CommonName: "Intermediate CA",
|
||||
Organization: []string{organization},
|
||||
OrganizationalUnit: []string{organizationalUnit},
|
||||
}
|
||||
template = &x509.Certificate{
|
||||
PublicKey: suite.IntermediateKey.Public(),
|
||||
SerialNumber: serial,
|
||||
Issuer: rootSubject,
|
||||
Subject: intermediateSubject,
|
||||
NotBefore: startDate,
|
||||
NotAfter: endDate,
|
||||
KeyUsage: x509.KeyUsageCertSign | x509.KeyUsageCRLSign,
|
||||
IsCA: true,
|
||||
MaxPathLen: 2,
|
||||
OCSPServer: []string{baseUrl + "/ocsp"},
|
||||
CRLDistributionPoints: []string{baseUrl + "/crl/intermediate.crl"},
|
||||
}
|
||||
suite.IntermediateCertificate = suite.signCertificate(
|
||||
template,
|
||||
suite.RootCertificate,
|
||||
suite.IntermediateKey.Public(),
|
||||
suite.RootKey,
|
||||
)
|
||||
|
||||
suite.RootResponderKey = suite.deserializeKey(idxRootOcspKey)
|
||||
startDate = time.Now().Add(-24 * time.Hour)
|
||||
endDate = startDate.AddDate(0, 6, 0)
|
||||
serial = suite.newRandomSerial()
|
||||
rootResponderSubject := pkix.Name{
|
||||
CommonName: "Root CA OCSP Responder",
|
||||
Organization: []string{organization},
|
||||
OrganizationalUnit: []string{organizationalUnit},
|
||||
}
|
||||
template = &x509.Certificate{
|
||||
PublicKey: suite.RootResponderKey.Public(),
|
||||
SerialNumber: serial,
|
||||
Issuer: rootSubject,
|
||||
Subject: rootResponderSubject,
|
||||
NotBefore: startDate,
|
||||
NotAfter: endDate,
|
||||
KeyUsage: x509.KeyUsageDigitalSignature,
|
||||
IsCA: false,
|
||||
MaxPathLen: 0,
|
||||
ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageOCSPSigning},
|
||||
OCSPServer: []string{baseUrl + "/ocsp"},
|
||||
CRLDistributionPoints: []string{baseUrl + "/crl/root.crl"},
|
||||
}
|
||||
suite.RootResponderCertificate = suite.signCertificate(
|
||||
template,
|
||||
suite.RootCertificate,
|
||||
suite.RootResponderKey.Public(),
|
||||
suite.RootKey,
|
||||
)
|
||||
|
||||
suite.IntermediateResponderKey = suite.deserializeKey(idxIntermediaryOcspKey)
|
||||
startDate = time.Now().Add(-24 * time.Hour)
|
||||
endDate = startDate.AddDate(0, 6, 0)
|
||||
serial = suite.newRandomSerial()
|
||||
intermediateResponderSubject := pkix.Name{
|
||||
CommonName: "Intermediate CA OCSP Responder",
|
||||
Organization: []string{organization},
|
||||
OrganizationalUnit: []string{organizationalUnit},
|
||||
}
|
||||
template = &x509.Certificate{
|
||||
PublicKey: suite.IntermediateResponderKey.Public(),
|
||||
SerialNumber: serial,
|
||||
Issuer: intermediateSubject,
|
||||
Subject: intermediateResponderSubject,
|
||||
NotBefore: startDate,
|
||||
NotAfter: endDate,
|
||||
KeyUsage: x509.KeyUsageDigitalSignature,
|
||||
IsCA: false,
|
||||
MaxPathLen: 0,
|
||||
ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageOCSPSigning},
|
||||
OCSPServer: []string{baseUrl + "/ocsp"},
|
||||
CRLDistributionPoints: []string{baseUrl + "/crl/intermediate.crl"},
|
||||
}
|
||||
suite.IntermediaResponderCertificate = suite.signCertificate(
|
||||
template,
|
||||
suite.IntermediateCertificate,
|
||||
suite.IntermediateResponderKey.Public(),
|
||||
suite.IntermediateKey,
|
||||
)
|
||||
}
|
||||
|
||||
type testCertDB struct {
|
||||
content map[string]*ocsp.Response
|
||||
}
|
||||
|
||||
func newTestCertDB() *testCertDB {
|
||||
return &testCertDB{content: make(map[string]*ocsp.Response)}
|
||||
}
|
||||
|
||||
func (t testCertDB) LookupResponseTemplate(serial *big.Int) *ocsp.Response {
|
||||
serialText := serial.Text(hexBase)
|
||||
if response, ok := t.content[serialText]; ok {
|
||||
return response
|
||||
}
|
||||
|
||||
response := &ocsp.Response{
|
||||
Status: ocsp.Revoked,
|
||||
SerialNumber: serial,
|
||||
}
|
||||
return response
|
||||
}
|
||||
|
||||
func (t testCertDB) UpdateCertificate(update *CertificateUpdate) {
|
||||
t.content[update.Serial.Text(hexBase)] = &ocsp.Response{
|
||||
Status: update.Status,
|
||||
SerialNumber: update.Serial,
|
||||
RevokedAt: update.RevokedAt,
|
||||
RevocationReason: update.RevocationReason,
|
||||
}
|
||||
}
|
||||
|
||||
func (suite *OcspSourceTestSuite) SetupTest() {
|
||||
var err error
|
||||
|
||||
suite.RootIssuer = NewIssuer(
|
||||
suite.RootCertificate,
|
||||
suite.RootResponderCertificate,
|
||||
suite.RootResponderKey,
|
||||
newTestCertDB(),
|
||||
)
|
||||
if err != nil {
|
||||
suite.FailNow("could not create root issuer: %v", err)
|
||||
}
|
||||
|
||||
suite.IntermediateIssuer = NewIssuer(
|
||||
suite.IntermediateCertificate,
|
||||
suite.IntermediaResponderCertificate,
|
||||
suite.IntermediateResponderKey,
|
||||
newTestCertDB(),
|
||||
)
|
||||
if err != nil {
|
||||
suite.FailNow("could not create intermediate issuer: %v", err)
|
||||
}
|
||||
|
||||
source, err := NewSource(WithIssuer(suite.RootIssuer), WithIssuer(suite.IntermediateIssuer))
|
||||
if err != nil {
|
||||
suite.FailNow("could not create ocsp source: %v", err)
|
||||
}
|
||||
|
||||
suite.responder = ocsp.NewResponder(source, nil)
|
||||
}
|
||||
|
||||
func (suite *OcspSourceTestSuite) TearDownSuite() {
|
||||
suite.Server.Close()
|
||||
}
|
||||
|
||||
func (suite *OcspSourceTestSuite) newRandomSerial() *big.Int {
|
||||
serial, err := rand.Int(rand.Reader, big.NewInt(1<<62-1))
|
||||
if err != nil {
|
||||
suite.FailNow("could not generate random serial number: %v", err)
|
||||
}
|
||||
|
||||
return serial
|
||||
}
|
||||
|
||||
func (suite *OcspSourceTestSuite) deserializeKey(index int) crypto.Signer {
|
||||
var (
|
||||
pkcs8Keys = []string{
|
||||
`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`,
|
||||
`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`,
|
||||
`MIIJRAIBADANBgkqhkiG9w0BAQEFAASCCS4wggkqAgEAAoICAQDMOr/Qyfows9H0sb1bXQqlXHHbSuJ5YCalB1B8+Pk+q+bEFn9IK2ELu/CP6mgG2uGKtkqIarAutgxLHnOzFIuY42jOo0GQCp+Pg3b37xDl76Gb8FN5+EsssTdZlJ+YQcQ3pPq22xEsb7LAXRrifsbJomS1KorjOLhXUbyXigwdlbyI07oeBS9Fi59sa1ablGPgL6Dd70JjeZvHboMVLJbFiLYzPL8wYP+CSR9GO8NfQ0lfKLbEmkYwOl0r42oA2mt9qNxvq9K8pOBpQ7ZlTDz/ax0zjgXKG2DLtvew4EaK1cjuuZIvOS6ut6kpOForDDDG/4VyCvs5xKR+CVULugzoTAInFJCGHWr8hMLtuMX/OjjbE/ywaQ7sRH3EiW0eGes0rK+llEtxws4Xcb6Y10pVFgFR46c31B3EHTx436H9bDCTHO4NTS/3kiSXGJgxrJKcFJ1D5kBxYRmZFWIeznaZWghAq0TvJRkXQTfmjre9bDiyPpIVKhE44HyDXWVjSbDU2zQCHfxZcT/g3+efc3e8WFI4VIYKR+3SYb59B5jWbWxuNeOnKiePvrqzErnQ/0t8v2JHzYLdntnZlEFDmVQg369DvP/u7cSfL0zXUI1yz3zn15vmC0BIT+wvzG1Y4lZE7D2v62no2d84lIBy8BdgOi6DHXKjNUjfT5lc2nJAAwIDAQABAoICAQCnALeP7Un4f2MFvD5Csr+c3w5/qyms5RJUyrlnqFRwijNHT7o/crGF3eZmWOE+EchzHIOwL8XoPJeq6bjkzL0mhXdT2i4nsngt6+vh7I0d6al2kbxBNeeLIVNInn8vC0B/3BnkYxkVBDXglYIBinfVtESz3Jj8G4qqZL1aEdm8a5BjNvevyr+QGA+CofeNe3d8gJYERb09CNl46IaqTu7vks6i8V7KGdd2W4MGdKJbiPM7rFhgRmN01pUP6YPqNLHHbMjtTE0kU5LqoSNKjGAo4Rdc7BsR3Pd3PiQncA5fQ1hu1jKTCSiCWM8sVNdu8iYE5DfkPNSYtZNpOQphb39TExPesfQjfmzpyqgRCd20TYY7lvVhqQq/BfxphXl5yYIaeWoYwLCNVGgbUsbDfnAUT/FaLRuirokyzOHcT8d66TavVA2g9bV9U403UeGTvW8bckmOt6yV+YH0BFgAZGvdoC8KklY3dwYbkI6JEdTCrOdDM4SS33jd532lJ5SkhElRQ3cmPBW5Gchj9ZOQuzeaL0J+cqfeaOLRs+jxFzSbfISnkzkXLWe+J4EKtKb8jg97fsevqEY/GzKq0LfB02o6HcJaxdvAECj6j60HcEPGmUxMpSmCiLlNMIAzc4iWwshj7QjCz5t+4ViZa0ELxt6KH7OnOOgAySfso1bEhsHg+QKCAQEA8mz4btEUwPfBj4GWnkGYndGC8w7bIuCtR4N9TZOr3DZbj8d4fgtVafjUqBeFHWwsmKIfsnJLMmwAyFpsbCKECndtmVUpwwDDi76CrnTDCHpiOVC1CiLYTkmZMVt3x5bzRYYePAU80u7MfnlSRicYN6r88gM6VY0bS1czQAclY4NTFO2CLWujLP20dGLzNa6e+YgxN2o52Jy+1VDEKth/IE4SEgCyVMzRQ0abFLeXnbildTCJ9IUFnpG5yJI9EKKJIYPPkzpRI47+Kaa42wm5KY8dapq7dhlOLGM8TvwA6HCHmpMWfDimt8hEydP/IkhAd5p8z9/rqV8D/4zJs6B4XQKCAQEA16pCUw4qEotfMDGN0Zp+R42u62sIg5/Ot61AtEtyPIDQLD4vDR7Kpvx2KziJrUSw8nmUFCUUjmpRWVxQId7zs3fEWkvRYsg9xQxBT/sAjBdhjQxcaN71uK/JSgImKZ/fMVfq0Kfxu8LXGAzcRRkImHQfXLIPEMhwJSwODH88S19zqOU1qsFr3lJIU7F4JTFCXkF0/eKbAFkEf21x7xKAMiSEVXLeaH5brhQP8JZGN204ViUx/6gMZaO4u1HrtLjsO3nHRWKDxToZ/VznDg4R/MdQGdjf3GQqRGmjff8mYpofS+IPZg62zjHwMgPtXNfKGCmg7K1NUQyDpTcEtfyT3wKCAQEA6S6D8CPQcKRfXGfrtqGl6Pejm6OIiQ3P3t/NYRdP0eu/OpdFgQfpypr8AgtNkTWB1/bpwj1Js2BLiGvhqkmw9FhX5YsTNmy/quU+0guhpcACRtsoqG1H3lXQsrcmiabarN8KqfTnwoTL0kNkLfJgyvr/ftI4BB5MSLXBqC63efbRrWmfM407UYVfVCvxgItMgvzLSmA07/QlYw3jAstnzG0L2KEu535zMuDxPoYxohX/oK9lovtoolPNfe2cKyoL+V7OBPGtJiTeuLWTTa0Wm7D7PJVFB53jobJ6Sgb/4JSM+tPMZwQe6NwLvo7Z8+HHkMPaliDujuXD9MU5JjyOSQKCAQAfT7XfbyAGnzuLuLYZZ2/MHbL2tQyAZ7RQkJPjWUzVofIXalPUeNL9WYwijEvY3y89lQ89uYB5iNAUxEVSFZeWR943KejVibMGJ9hXr16N+FsOamwEQdK1JLh5yUQXKQHznQsXjjqXgxtNf5h/Y7oOwFVJVQTA8AjwLKxBfjuXydz1+YlIKFrr5VBg7lQ+Oh+jpadBsSPF8iaoBZDUIACCxzB7Y3kM/MN8y6bgzECXjYDvaOkQcCqUmyeX8rXhADQ0EmMvHLV67guV8GYPcVWFOL7VcdpWmihU1G2LPn8pM601jDWmIJ7D4xY3fMB6iU3rIigeFK80BDgcxZcmgOxLAoIBAQCnBuQdPxexuAkxyoG8jb5U6Z/GVO1r9lzHpuDorSYWHSndRv2XMm5OAEaqQ7lj/jdoyfwO1Be+tbfb++pfC9MppIjgvJjqm94LSL7fQAmqWzf2BICYb+ShFELiWumDdU4zaNeXQcmlrPSaaXv19yuYiyRhWKEfacHLv+4pEwuWKRPAixciOnVOtMwHFbTkWx+PGT9G/FvhSzUgDMmiWbZIWUGI/pz+h3Fl8rOjSS8SGpgu7qn7m6Yz1WT8epNyiNtTC1jznITjiMgYDq4ob0VrTuv9oGKmKMPDL1IaOtsjeEDFm7bliQzIjqHM1nj1ZYWBbgPqaR3DICwXvd/gXJYr`,
|
||||
`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`,
|
||||
`MIIJQgIBADANBgkqhkiG9w0BAQEFAASCCSwwggkoAgEAAoICAQDQ73c5K1xqsfJxiKeK2vYurTsZ12BRUe33vKeTjRw9brp7WFfnIoWkTZhfzaygS35n6bT90GG2gshSH3nYaPuuhqiGgmPJ8dZiowhAVr5OeLmqMzJRx4UwSOEsM8+ARkrvpbr0TfSl8Alpb/tE8nre/iD+N83MdOFDRJvpl4eV1ihiqV4nEKYNezpdWoGpFqBZgJz4gDG4ks25OGjo/Nvl9rGFxMJqac8/UNAB3yUn1PAh6xEMVGNECqUjyTF1aDbhnsAg9bPxHtlPRG0g98Bh4qATPm0eBwqpoiLpg4uhBHBu5kI6R5BuEtJXvwLAFsV9N5sjBRdBfJY9lnmGf5LfCOMT8v0oCA5Vx2pSzAHGyFb+BME+dJq/M/vN+we6tDJ2vWle/mhWLwgeCfcgDly02caIAFjDtqcIXqvqVi5heYEpgoFyLSPL5In3YkTitrxnCf7wigUfr0y8wP2hIqGE+lCx2bkDme0tI4YCjkN/TGcfPVI236r5VEmDcIsjG5FitfIXwjst7eChKBqb2XT+S7X3dnIXF6E12mBh5+OztIwSPufP03qIC+UtjEw/RtT9Eeufhcv9DjOmk31pS6CsUi4AF7ROB4VedFoQb6x0Qvv0gbTkK1+AkKSVAgXi93vQl7qeAu5GimAVbgkgG2RsSN/ByoewM8DlVlxCnuMKXwIDAQABAoICAG5jtzgrYr60mgXGT88bdSqgODOlCaytfu9/a4KX8a27jSeSO49RPA7dOH6VqnleqcC5R/k6WgYV/0T0gGoIvcEuQGghCBwwfDbNMJTdv9qyClaUiiePJR2nw+CY1y9M9Fa915MwhOwPcMmWcsmw255p4oCudztpHYNvAnleqDJQXjISjj5Rm4pP6zxbj1oJLFtdipH5GUwNtEgCN9uxjXVo685jcNzMXsVzKxNyFK2A4gbpra43/+J5RVss98aycxytxZEkgBgLVoF9A7OBctHE7RJ9zyUoT0fk9FEwQVunfsf5PFCScwrVQvE9aHSZE2rfMEZGcE0XSEHt+PNJAA2GS7MAh9kx2X72OstgKxUcf3uazdqQwOiEfFXGxKW+NTtS0w1uDiMxq+kn2/HhjLFdjyh3X6Q2LPDK6CnCOQgSQTOVIqluwmE6YjzZq/7ifK8z4a/Cgpfy785ZQJmwXIiEFzWGbAcI87Wc7vTtkGQ5A/g0lBYXps+D8DuBdU20xRC7SNs7UpmWRPfE8rB3SYbH6uw8t7r9zSt/HkTUQ2W1yzVR4TvTIg3YTY6WgYZR9m/JpzGGs4H2bE6vIWpszFnaW8P+zaXbTky2SQsVemoQLQhpGvRmZfPfArue48d4DVhHE76wJ+2+ic0uKptui0ICTR8E+QoefeHyGabQiGoxAoIBAQDZoSVErLgfTwgo+cE1iC/cUecmjjOMGI3OoPQhn0IKXFFfR98qP1BjTyaqssP4oOvJF0Yge82GvOAVOFc9jVstXeBYokvKBXmSeXVOyvJSnximiTb3oos73PYpwGdkHt2bikIu2zccLtWrU2twzcC7MbsqpjBOBuHpY74BEIGIS8V0a1Wj761AS/RVfgQztMugV0iUNcd2mdRChdFVLVZlIIzuJZeZlaxEh0g0JPBcQTdjOeOLSAH5SAMphoWKM8FxyJ6MiER1BMbvkUCGUoNB9Zj8kyod1+W2dHP9r3KxUebU4Jx1g6dA6tlLdlaysEO7fwM98TUWUukbZlHxBL1dAoIBAQD1xehhUHBtznumy8KsD1pyrUVmMg57asbCdawAsWFS6e8SK1/4Y/t2HWfICVz5z+7Iq4uzLWOQmHgbIgHFWFx2zW+miRZ+8rsXtLBq+h6nmoVvTB48xKfmFCnUmLA4zvnBo+XJDJKfww3uXiNF75KU38wT4d+X6ylzn5zv9B+0SN76ziTgZkoJBFA+nAUgoJVu3Yk5mEztMXvaHTkXfoBBw+RTchuvcrp91mooK5cbsVFPtnyiN541LspwmlQ4fvtbu9IcEX2YcgMdxKWiAoMEX4+CNW0X79Ve1XuLDDyj/kpQei0OJNSPPTm11aADStRcWh/99MCG0ES22HqA5K7rAoIBAQCqZHE5g8HlQYuyCVos/6P5bGb+2PejCNZ+oaaDbJ3YXs+XOj6QZWIbXy13GOA3GkeB+qb9Z/FTPS51btdfaf2pV4b/3pJYalD1TgN63Ys9BTPXsDdJTAqXpDlYFzzcNw5raaTH6vsLJeBFc9r7Rx1Pc3CHw/auINVgVqe9TZ8dh7XhoOg8oruTS+TRywifMz77G1vILxMs2ORH8V4kwoqSQxoUm3pG1dk7DnCJ4yIOGUG0fu6ZbpedBbT0Hk8QFdWvsNPyU/7FYCgiCifrA0+hpRlTKCYhl5hTcnoede/mxallozbdC2c4fKgFXXIkFb3yX0+6f9CA2gmJWmNtwbhBAoIBAD+J5bTa5eYNfrw1a2OuKHUDRLDR+hqAFViKsWgQc5fK8MqiuhHuT0FPAtidSxMuZY4tg1VUQ6xLLqndFdYeLPFt1RKZ2F4DhMjxJrbRaQFtmleWty6ArON3jQTwYIafQ8SDCdH7VSDGfaCFZ9sZv6yLWyg8ueB9pF7HAfyrAVNLj5IJn1EAQN2p5tD45v5zZY6e6W4haKndpCZUbcEcLC5UZYAJPQ0TvRDxUMqH0m4PXnUcOYZ6mIUpTp7j1ygv8+3YVc552x2BRDr67tM+kP6dce14wHZyw5fb2y+sfzTqde3uWB2S+fz6GCRhURfozZP+nMy77NYtm8Ylmy4dcPcCggEALnheGta9YltY8hUch+av3k1YKan284M3ZLKqNK/Tffcx7/HvCrbkZvG+WEaVZ2zp25Zdq94sOia7rjc1/uwtNGnjUBsQTHwW+1sHlB8dhJCgscoR7j+5BY7yIXR6tvUdszQMt/KexaQODnEzFdRQN6agQZywrv+RqXDbwSulZTiAXNGG/egd3QYHWV+vLMHZRdMC4uMEZbb28sRMbYtD36p9pv4DE2i1MVXu9jUDgUY1KS5k2Leyr0LGEu1gJEYs3U5FXzCuUGvB4o3xMeSDnR//p46JE9YUaN/apuVi9SjJItEhV2zjLcKVkd8W0DE77ECatzHByp64AZ6ognSEFw`,
|
||||
`MIIJQQIBADANBgkqhkiG9w0BAQEFAASCCSswggknAgEAAoICAQCi/gLhii/iYFL + S1N2/h4MT + 7cQ3p35b4enrZcL9hEmf9PaeSU7yiwYUxxMPj7JLoeH4dZTkZmEylU2cQ/jT6zDjtU2p/ukBHHFEAS6PyKJKbL0+hDaZGc2nRo3Y4NbpvH2vubt+f3ug/nFEqUR2JdwRyNso1JqkOt3hRoeQkq56w2JKvbO0KT/nLd0Ol7xpeYvlbmaZlBZtbQpSM9faFzJ6QlQsHNFrYp4HZk7Jb4OWe/PHa6KzD0cG+H9yG1eXeIzc7iVShOHNridcmnb7Fvo1Tdgw3apW79U9PMKsp412i2pqM8F7l/FZ+JE3mZX9gML4/Y49fq5qr1ErMEKyjS00uk0P6SWjGNFXEWdjfYPHNTgXCFy4JQGLldf477BqvKbKjDyGAn8PY12o4pxkXJjJ8lSlLBZ8wlO3/q70wHCjht1rCalKXm7U9/wwN8FG3KNmN/jBw+o5NLx+aZHCEJLpqMMEBM2onnSZ3VApdow6eNkZgPlUnnxehPCSGJdH2rbLwgYXXDgpQNCK9X8vFm0blE5y+xFy9sTtv4A99xMoGW0MShrgM0rYT5m9sS3NsdADCYhV0o3Gksu5TcVJbzBcgWDGIPEk9cfKllvUWeSUktmNS6iKoZe2bGtySU4wwYpbiV28fltvrUYeqNWbenTyfUvFsO3/5cS+J0PXqRZQIDAQABAoICAAGr2Ru27wbDNhalbxWpRWWa7DgV+QuBr5Fgzz3iv7T0X0nRLCbazPACD2aZufPZTI9Cg4DG7LHfQG0+TmTYbewoGTOlij5H++aurnmDKhPAjHVKlvcZ/gQ9uTPaEbrRh4dlF29hU1+W6NLVwm4CzV9ou4I4cnpu5ILD5DGuDAoElh94AMsesHAUC9l6aup5/tPyH7H9IzUoeMtq5hGbWCx2G5jGNnaD5RpI0u31/xZMgGPVmuINz1KyVHJwdkgGFtOwSI7JkMA8S4JG8upkms0qP5SF4356c8jlMybEyY6zpr84tjPHin9jGV+uc3C/Ktf1JOR0caJW+ynIJ03Do4eBsYwZJcAWyjUVIKPeMZ1V60+N5UteFKeqWvQixWb281g0D6+xn+XVL6RDLZR6NLflqgU3aXIjDO0087WmyAVrqB01XSMv/nWvM9IPzXm5Lxro/BefXXMczuUuNT5cnskVV5Mfxn3GtZuxydobG3QM8kINMW5XZA8qZo+RZ2b64OfwCfz7MACtKq1YIl6cYHHydMpOmwMi2o3o/0Rl6d+exzzo8wB/ky0XksIoAl5bEFGPQukQt+bYgBaIRtHaczZ6BYQVlCbWpcFuG53MqisdfwiB5H6vdnc5Og5UIuCV6I/3Bb02/AQlnbaWwNd9kQpcCQxOR67WARzlTFHOtuuBAoIBAQDWFp335/WzEKQt65Tjmk0GizKN2uFJgOAlUkzkJo6t2bTf8YnpDoZobD6M3miC73wMLdpj16viJarg5+OvwfJcqhG27lfoDf1FN28N9uj5ofkHd32pIkCqZQevHkPLpcb6sFqiKm17WkPsL/uGOHsmkFZBJWLlEtidUM6N/pwP3hdQbJaOXn9ONfdp+PATFSqL+mjQt+N5k/bdCgB9jb+fiW4/z7g2sk0FhlgSjy8zlT8GGEo09MYAExkvvD2a9bcFPkAsgM+0OvjPFj0gPX8MloqEjbKo9Lcq67cNlwlvVbN0qX5uy0+yZ7m0lZwJLuO7pXMsEtnSLdthb+8WzzkDAoIBAQDC5qFAAM67lkque/vr9JSH8jvrpGZFC3BT7bwj9wAFp71lI4KEC74SpNfAy1CVKtW8qhmXwJbbVyRODeTGhW60lyxSrLeTlb/hbOrBangv0FpKuwOd/yS7dqa5YgVdwjqI/1lr8jvd5ZRbUO/6QMXXo8nPHwrZ95vvjZvsSmZtz0SDMjKFAHYLeI50J6VN7yjhp7Y5A1SGGyry5T6xpOs7Bf3UCbGglTjlAQUNqv0FNN4jDwniUzgKdOvJzqpujph7+EV1H5CWO/qWUauNzk0UQqK5/arwCCuTCnf4VOOIm8bfpdu4tUxA0YCYNyKuzuMql5WhDFqBpLttCDG1oVt3AoIBAHu1igaspQ+Z+CUI0npKuWOkn8vUxQXiUKOAUSotFZlkCd62BgkD+arxkZt5EU9H75hiDqZPA+Wj+oAHv0vaeKAPLjlUtLtFCisrAyNAoBNQA0TLSXOodVGBG9WXWXmeC8LkkRrQBX+YA1PBzkga7x6znQP8BI92OnF9Nj5a9/c7UKGKfd06wbmB+bvku2Vrf7BvPI4PFIU2eRn1I59pf2dC1NBOyZXhphwhK1deIdpezZJKCuda4ec9Ifqxfa/DeMSw2ghdTqhIw2l24k90+ateeiBqeOiksAs/NgHGWw9YqGAraQHAvgbLZUXyhb3QuIu/6TN4mSGYmgJN2gyGJd0CggEAMUiYZdLOc+jhIZYAcf36wRWjyUZdBgCKeu6WsJF40/VCkxyTmEhP2+8LD5lZ2x5lxbtNlW4Bt50ldQdWXkzHXasMbit2kD9BsgMUqav+kDijXFpF4+Hsq+qo5h+/M6I1owbO2dj7AnVXOnAQ/rr6TDkCgvlaiRtvtvecAfgeRo4uyO4ckD05nFTz0LNlvXtIXydFkM0ixnaAOcN2Qv1s7hPpx4BcBjs1k9LaMEaYjIR912eqHCzm8t318aXsuuAb9t0k0yUULqkkCHbGZqlQHFttQpK+csveGOSAya7NinG5rDwIf4B4s7tGRTdL8eAm2VJTZ9Hz4OyC6GM2DrOqqQKCAQAIA07O1D7L7C5rcfs6UXCN/QE2zz4OXje4tp6ctET5yTe/BPT63y6ODa+SMt2JrVTmPQx11GplDGl0vyJ7QJ4mAdthTUjnKaKn7FtNKne+HlTq9UW4XYR8QYGTUn1QmJS63Kc9zLZt4pFI1SJMeilpKAz3k52pJVkJUaeBJq3fiSH9FxzNUpzrHi3/lG5DX0VZfAFGTp680dF5uAOmk6qsqYuf1lBwkqTQCYrfb6umcxzrsgGcd30XSw/hsy0T1fsmV4+KWo8zHyIXHpWorY7UY4eQP07oC0+n/r5Xb1GHUmQ8uAROrtrVFMRPdlUIZSbDYgPrfC2RHoNDlbX4dUbb`,
|
||||
`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`,
|
||||
`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`,
|
||||
`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`,
|
||||
`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`,
|
||||
}
|
||||
)
|
||||
|
||||
assert.Less(suite.T(), index, len(pkcs8Keys))
|
||||
keyBytes, err := base64.RawStdEncoding.DecodeString(pkcs8Keys[index])
|
||||
if err != nil {
|
||||
suite.FailNow("could not decode private key DER bytes: %v", err)
|
||||
}
|
||||
|
||||
pkcs8PrivateKey, err := x509.ParsePKCS8PrivateKey(keyBytes)
|
||||
if err != nil {
|
||||
suite.FailNow("could not parse private key: %v", err)
|
||||
}
|
||||
|
||||
key, ok := pkcs8PrivateKey.(crypto.Signer)
|
||||
if !ok {
|
||||
suite.FailNow("could not use private key as crypto.Signer")
|
||||
}
|
||||
|
||||
return key
|
||||
}
|
||||
|
||||
func (suite *OcspSourceTestSuite) signCertificate(
|
||||
template, parent *x509.Certificate,
|
||||
public crypto.PublicKey,
|
||||
private crypto.Signer,
|
||||
) *x509.Certificate {
|
||||
rootBytes, err := x509.CreateCertificate(rand.Reader, template, parent, public, private)
|
||||
if err != nil {
|
||||
suite.FailNow("could not sign test root certificate %v", err)
|
||||
}
|
||||
certificate, err := x509.ParseCertificate(rootBytes)
|
||||
if err != nil {
|
||||
suite.FailNow("could not parse generated root certificate: %v", err)
|
||||
}
|
||||
return certificate
|
||||
}
|
||||
|
||||
func (suite *OcspSourceTestSuite) TestCertificateInIndexTxt() {
|
||||
const duration90days = 90 * 24 * time.Hour
|
||||
|
||||
serial := suite.newRandomSerial()
|
||||
start := time.Now().Add(-1 * time.Hour)
|
||||
end := start.Add(duration90days)
|
||||
|
||||
key := suite.deserializeKey(idxEndpointKey1)
|
||||
template := &x509.Certificate{
|
||||
PublicKey: key.Public(),
|
||||
SerialNumber: serial,
|
||||
Issuer: suite.IntermediateCertificate.Subject,
|
||||
Subject: pkix.Name{
|
||||
CommonName: "test.example.org",
|
||||
Organization: []string{"Test Company"},
|
||||
Locality: []string{"Acme Town"},
|
||||
},
|
||||
NotBefore: start,
|
||||
NotAfter: end,
|
||||
KeyUsage: x509.KeyUsageDataEncipherment | x509.KeyUsageDigitalSignature,
|
||||
ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth},
|
||||
OCSPServer: suite.IntermediateCertificate.OCSPServer,
|
||||
}
|
||||
cert := suite.signCertificate(
|
||||
template,
|
||||
suite.IntermediateCertificate,
|
||||
key.Public(),
|
||||
suite.IntermediateKey,
|
||||
)
|
||||
suite.IntermediateIssuer.UpdateCert(&CertificateUpdate{
|
||||
Serial: serial,
|
||||
Status: ocsp.Good,
|
||||
NotAfter: end,
|
||||
})
|
||||
|
||||
ocspClient := http.DefaultClient
|
||||
request, err := ocsp.CreateRequest(cert, suite.IntermediateCertificate, nil)
|
||||
if err != nil {
|
||||
suite.FailNow("could not create OCSP request: %v", err)
|
||||
}
|
||||
response, err := ocspClient.Post(suite.IntermediateCertificate.OCSPServer[0], "application/ocsp-request", bytes.NewReader(request))
|
||||
if err != nil {
|
||||
suite.FailNow("could not retrieve response: %v", err)
|
||||
}
|
||||
defer func(Body io.ReadCloser) {
|
||||
_ = Body.Close()
|
||||
}(response.Body)
|
||||
|
||||
assert.Equal(suite.T(), 200, response.StatusCode)
|
||||
|
||||
responseData, err := io.ReadAll(response.Body)
|
||||
if err != nil {
|
||||
suite.FailNow("could not read data from OCSP response: %v", err)
|
||||
}
|
||||
tempFile, _ := ioutil.TempFile("", "ocspresponse-*.der")
|
||||
io.Copy(tempFile, bytes.NewReader(responseData))
|
||||
|
||||
data, err := ocsp.ParseResponse(responseData, suite.IntermediateCertificate)
|
||||
if err != nil {
|
||||
suite.FailNow("could not parse OCSP response: %v", err)
|
||||
}
|
||||
assert.Equal(suite.T(), serial, data.SerialNumber)
|
||||
assert.Equal(suite.T(), ocsp.Good, data.Status)
|
||||
}
|
||||
|
||||
func TestOcspSourceTestSuite(t *testing.T) {
|
||||
suite.Run(t, new(OcspSourceTestSuite))
|
||||
}
|
||||
|
||||
const (
|
||||
idxRootKey int = iota
|
||||
idxIntermediaryKey
|
||||
idxRootOcspKey
|
||||
idxIntermediaryOcspKey
|
||||
idxEndpointKey1
|
||||
idxEndpointKey2
|
||||
idxEndpointKey3
|
||||
)
|
Loading…
Reference in New Issue