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
Jan Dittberner 2 years ago committed by Jan Dittberner
parent 434b544e78
commit b0a16bb85c

@ -32,7 +32,7 @@ import (
"syscall"
"time"
"github.com/cloudflare/cfssl/ocsp"
"git.cacert.org/cacert-goocsp/pkg/ocsp"
"github.com/knadh/koanf"
"github.com/knadh/koanf/parsers/yaml"
"github.com/knadh/koanf/providers/file"
@ -67,7 +67,8 @@ func main() {
issuerConfigs := config.Slices(coIssuers)
opts = configureIssuers(issuerConfigs, opts)
ctx, cancel := context.WithCancel(context.Background())
opts = configureIssuers(ctx, issuerConfigs, opts)
cacertSource, err := ocspsource.NewSource(opts...)
if err != nil {
@ -76,13 +77,8 @@ func main() {
http.Handle("/", withLogging(ocsp.NewResponder(cacertSource, nil).ServeHTTP))
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
go func() {
cacertSource.BackgroundTasks(ctx)
}()
server := &http.Server{
Addr: *serverAddr,
}
@ -98,7 +94,7 @@ func main() {
}
}
func configureIssuers(issuerConfigs []*koanf.Koanf, opts []ocspsource.Option) []ocspsource.Option {
func configureIssuers(ctx context.Context, issuerConfigs []*koanf.Koanf, opts []ocspsource.Option) []ocspsource.Option {
for number, issuerConfig := range issuerConfigs {
hasErrors := false
@ -137,17 +133,18 @@ func configureIssuers(issuerConfigs []*koanf.Koanf, opts []ocspsource.Option) []
continue
}
issuer, err := ocspsource.NewIssuer(
caCertificate,
responderCertificate,
responderKey,
issuerConfig.String(issuerCertList),
)
certDb, err := ocspsource.NewCertDB(ctx, issuerConfig.String(issuerCertList))
if err != nil {
logrus.Errorf("could not create issuer %d: %v", number, err)
logrus.Errorf("could not create certificate db %d: %v", number, err)
continue
}
issuer := ocspsource.NewIssuer(
caCertificate,
responderCertificate,
responderKey,
certDb,
)
opts = append(opts, ocspsource.WithIssuer(issuer))
}

@ -3,93 +3,23 @@ module git.cacert.org/cacert-goocsp
go 1.17
require (
github.com/cloudflare/cfssl v1.6.1
github.com/fsnotify/fsnotify v1.4.9
github.com/jmhodges/clock v0.0.0-20160418191101-880ee4c33548
github.com/knadh/koanf v1.4.0
github.com/sirupsen/logrus v1.8.1
golang.org/x/crypto v0.0.0-20210506145944-38f3c27a63bf
github.com/stretchr/testify v1.7.0
)
require (
cloud.google.com/go v0.81.0 // indirect
github.com/beorn7/perks v1.0.1 // indirect
github.com/bgentry/speakeasy v0.1.0 // indirect
github.com/census-instrumentation/opencensus-proto v0.3.0 // indirect
github.com/cespare/xxhash/v2 v2.1.1 // indirect
github.com/cncf/udpa/go v0.0.0-20210322005330-6414d713912e // indirect
github.com/coreos/go-semver v0.3.0 // indirect
github.com/coreos/go-systemd/v22 v22.3.2 // indirect
github.com/cpuguy83/go-md2man/v2 v2.0.0 // indirect
github.com/dustin/go-humanize v1.0.0 // indirect
github.com/envoyproxy/go-control-plane v0.9.9-0.20210217033140-668b12f5399d // indirect
github.com/envoyproxy/protoc-gen-validate v0.6.1 // indirect
github.com/form3tech-oss/jwt-go v3.2.3+incompatible // indirect
github.com/fullstorydev/grpcurl v1.8.1 // indirect
github.com/gogo/protobuf v1.3.2 // indirect
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect
github.com/golang/mock v1.5.0 // indirect
github.com/golang/protobuf v1.5.2 // indirect
github.com/google/btree v1.0.1 // indirect
github.com/google/certificate-transparency-go v1.1.2-0.20210511102531-373a877eec92 // indirect
github.com/google/go-cmp v0.5.6 // indirect
github.com/google/uuid v1.2.0 // indirect
github.com/gorilla/websocket v1.4.2 // indirect
github.com/grpc-ecosystem/go-grpc-middleware v1.3.0 // indirect
github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0 // indirect
github.com/grpc-ecosystem/grpc-gateway v1.16.0 // indirect
github.com/inconshreveable/mousetrap v1.0.0 // indirect
github.com/jhump/protoreflect v1.8.2 // indirect
github.com/jmhodges/clock v0.0.0-20160418191101-880ee4c33548 // indirect
github.com/jmoiron/sqlx v1.3.3 // indirect
github.com/jonboulle/clockwork v0.2.2 // indirect
github.com/json-iterator/go v1.1.11 // indirect
github.com/kisielk/sqlstruct v0.0.0-20201105191214-5f3e10d3ab46 // indirect
github.com/mattn/go-runewidth v0.0.12 // indirect
github.com/matttproud/golang_protobuf_extensions v1.0.1 // indirect
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/kr/pretty v0.2.1 // indirect
github.com/kr/text v0.2.0 // indirect
github.com/mitchellh/copystructure v1.2.0 // indirect
github.com/mitchellh/mapstructure v1.4.1 // indirect
github.com/mitchellh/reflectwalk v1.0.2 // indirect
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
github.com/modern-go/reflect2 v1.0.1 // indirect
github.com/olekukonko/tablewriter v0.0.5 // indirect
github.com/prometheus/client_golang v1.10.0 // indirect
github.com/prometheus/client_model v0.2.0 // indirect
github.com/prometheus/common v0.24.0 // indirect
github.com/prometheus/procfs v0.6.0 // indirect
github.com/rivo/uniseg v0.2.0 // indirect
github.com/russross/blackfriday/v2 v2.1.0 // indirect
github.com/soheilhy/cmux v0.1.5 // indirect
github.com/spf13/cobra v1.1.3 // indirect
github.com/spf13/pflag v1.0.5 // indirect
github.com/tmc/grpc-websocket-proxy v0.0.0-20201229170055-e5319fda7802 // indirect
github.com/urfave/cli v1.22.5 // indirect
github.com/xiang90/probing v0.0.0-20190116061207-43a291ad63a2 // indirect
go.etcd.io/bbolt v1.3.5 // indirect
go.etcd.io/etcd/api/v3 v3.5.0-alpha.0 // indirect
go.etcd.io/etcd/client/v2 v2.305.0-alpha.0 // indirect
go.etcd.io/etcd/client/v3 v3.5.0-alpha.0 // indirect
go.etcd.io/etcd/etcdctl/v3 v3.5.0-alpha.0 // indirect
go.etcd.io/etcd/pkg/v3 v3.5.0-alpha.0 // indirect
go.etcd.io/etcd/raft/v3 v3.5.0-alpha.0 // indirect
go.etcd.io/etcd/server/v3 v3.5.0-alpha.0 // indirect
go.etcd.io/etcd/tests/v3 v3.5.0-alpha.0 // indirect
go.etcd.io/etcd/v3 v3.5.0-alpha.0 // indirect
go.uber.org/atomic v1.7.0 // indirect
go.uber.org/multierr v1.7.0 // indirect
go.uber.org/zap v1.16.0 // indirect
golang.org/x/mod v0.4.2 // indirect
golang.org/x/net v0.0.0-20210510120150-4163338589ed // indirect
golang.org/x/oauth2 v0.0.0-20210427180440-81ed05c6b58c // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
golang.org/x/sys v0.0.0-20210511113859-b0526f3d8744 // indirect
golang.org/x/text v0.3.6 // indirect
golang.org/x/time v0.0.0-20210220033141-f8bda1e9f3ba // indirect
golang.org/x/tools v0.1.0 // indirect
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 // indirect
google.golang.org/appengine v1.6.7 // indirect
google.golang.org/genproto v0.0.0-20210510173355-fb37daa5cd7a // indirect
google.golang.org/grpc v1.37.0 // indirect
google.golang.org/protobuf v1.26.0 // indirect
gopkg.in/cheggaaa/pb.v1 v1.0.28 // indirect
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 // indirect
gopkg.in/yaml.v2 v2.4.0 // indirect
sigs.k8s.io/yaml v1.2.0 // indirect
gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b // indirect
)

1149
go.sum

File diff suppressed because it is too large Load Diff

@ -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.

@ -19,7 +19,6 @@ package ocspsource
import (
"bytes"
"context"
"crypto"
"crypto/x509"
"crypto/x509/pkix"
@ -29,13 +28,27 @@ import (
"fmt"
"math/big"
"net/http"
"sync"
"time"
"git.cacert.org/cacert-goocsp/pkg/ocsp"
"github.com/sirupsen/logrus"
"golang.org/x/crypto/ocsp"
)
type CertificateUpdate struct {
Serial *big.Int
Status int
NotAfter time.Time
RevokedAt time.Time
RevocationReason int
}
var idPKIXOCSPExtendedRevoke = asn1.ObjectIdentifier([]int{1, 3, 6, 1, 5, 5, 7, 48, 1, 9})
type CertificateDatabase interface {
LookupResponseTemplate(*big.Int) *ocsp.Response
UpdateCertificate(*CertificateUpdate)
}
var errUnknownIssuer = errors.New("issuer key hash does not match any of the known issuers")
// CertificateIssuer is an abstraction for the OCSP responder that knows certificates of a specific CA
@ -43,23 +56,21 @@ type CertificateIssuer struct {
responderCertificate *x509.Certificate
responderKey crypto.Signer
caCertificate *x509.Certificate
certDb *OpenSSLCertDB
certDb CertificateDatabase
}
// NewIssuer is used to construct a new CertificateIssuer instance
func NewIssuer(
caCertificate, responderCertificate *x509.Certificate,
responderKey crypto.Signer,
certificateFile string,
) (*CertificateIssuer, error) {
certDb, err := NewCertDB(certificateFile)
if err != nil {
return nil, fmt.Errorf("could not initialize certificate database: %w", err)
}
certDb CertificateDatabase,
) *CertificateIssuer {
return &CertificateIssuer{
caCertificate: caCertificate, responderCertificate: responderCertificate, responderKey: responderKey, certDb: certDb,
}, nil
caCertificate: caCertificate,
responderCertificate: responderCertificate,
responderKey: responderKey,
certDb: certDb,
}
}
// publicKeyMatches determines whether a given public key hash matches the public key of the CA certificate of the
@ -101,7 +112,13 @@ func (i *CertificateIssuer) buildResponse(template *ocsp.Response) ([]byte, erro
template.NextUpdate = time.Now().Add(time.Hour)
template.Certificate = i.responderCertificate
response, err := ocsp.CreateResponse(i.caCertificate, i.responderCertificate, *template, i.responderKey)
response, err := ocsp.CreateResponse(
i.caCertificate,
i.responderCertificate,
*template,
i.responderKey,
[]pkix.Extension{{Id: idPKIXOCSPExtendedRevoke, Value: nil, Critical: false}},
)
if err != nil {
return nil, fmt.Errorf("could not create final OCSP response: %w", err)
}
@ -115,24 +132,18 @@ func (i *CertificateIssuer) String() string {
}
// LookupResponse looks for status information of a certificate and returns a signed response
func (i *CertificateIssuer) LookupResponse(serialNumber *big.Int) ([]byte, error) {
response := i.certDb.LookupResponseTemplate(serialNumber)
func (i *CertificateIssuer) LookupResponse(serial *big.Int) ([]byte, error) {
response := i.certDb.LookupResponseTemplate(serial)
if response == nil {
return i.buildUnknownResponse(serialNumber)
return i.buildUnknownResponse(serial)
}
return i.buildResponse(response)
}
// The backgroundTasks of the issuer are started
func (i *CertificateIssuer) backgroundTasks(ctx context.Context) {
logrus.Infof("starting background tasks for ceritificate issuer %s", i)
watcherCtx, cancel := context.WithCancel(ctx)
go i.certDb.WatchChanges(watcherCtx)
<-ctx.Done()
cancel()
func (i *CertificateIssuer) UpdateCert(u *CertificateUpdate) {
logrus.Infof("got certificate update: %+v", u)
i.certDb.UpdateCertificate(u)
}
// OcspSource is an implementation of the Source interface used by the Cloudflare OCSP response handler
@ -216,25 +227,6 @@ func (o *OcspSource) getIssuer(keyHash []byte, algorithm crypto.Hash) (*Certific
return nil, errUnknownIssuer
}
// BackgroundTasks delegates background task handling to the CertificateIssuer instances of the OcspSource
func (o *OcspSource) BackgroundTasks(ctx context.Context) {
wg := sync.WaitGroup{}
wg.Add(len(o.issuers))
logrus.Info("starting OCSP source background tasks")
for _, issuer := range o.issuers {
issuer := issuer
go func() {
issuer.backgroundTasks(ctx)
wg.Done()
}()
}
wg.Wait()
logrus.Info("stopped OCSP source background tasks")
}
func (o *OcspSource) getDefaultIssuer() *CertificateIssuer {
return o.issuers[0]
}

@ -30,9 +30,9 @@ import (
"strings"
"time"
"git.cacert.org/cacert-goocsp/pkg/ocsp"
"github.com/fsnotify/fsnotify"
"github.com/sirupsen/logrus"
"golang.org/x/crypto/ocsp"
)
const (
@ -55,22 +55,40 @@ const (
type OpenSSLCertDB struct {
fileName string
content map[string]*ocsp.Response
issuer *CertificateIssuer
}
func (o *OpenSSLCertDB) UpdateCertificate(update *CertificateUpdate) {
o.content[update.Serial.Text(hexBase)] = &ocsp.Response{
Status: update.Status,
SerialNumber: update.Serial,
RevokedAt: update.RevokedAt,
RevocationReason: update.RevocationReason,
}
}
// NewCertDB creates a new certificate database for the given index.txt file
func NewCertDB(fileName string) (*OpenSSLCertDB, error) {
func NewCertDB(ctx context.Context, fileName string) (*OpenSSLCertDB, error) {
absFile, err := filepath.Abs(fileName)
if err != nil {
return nil, fmt.Errorf("could not determine absolute file name of %s: %w", fileName, err)
}
certDb := &OpenSSLCertDB{fileName: absFile}
certDb := &OpenSSLCertDB{fileName: absFile, content: make(map[string]*ocsp.Response)}
err = certDb.update()
if err != nil {
return nil, err
}
go func(ctx context.Context) {
watcherCtx, cancel := context.WithCancel(ctx)
certDb.WatchChanges(watcherCtx)
<-ctx.Done()
cancel()
}(ctx)
return certDb, nil
}
@ -85,10 +103,10 @@ func (o *OpenSSLCertDB) update() error {
_ = f.Close()
}(f)
newContent := make(map[string]*ocsp.Response)
b := bufio.NewReader(f)
lastLine := false
count := 0
for {
line, err := b.ReadString('\n')
if err != nil {
@ -99,9 +117,10 @@ func (o *OpenSSLCertDB) update() error {
lastLine = true
}
serial, response := parseLine(strings.TrimSpace(line))
if serial != "" {
newContent[serial] = response
update := parseLine(strings.TrimSpace(line))
if update != nil {
o.UpdateCertificate(update)
count += 1
}
if lastLine {
@ -109,9 +128,7 @@ func (o *OpenSSLCertDB) update() error {
}
}
logrus.Infof("parsed certificate database '%s', found information for %d certificates", o.fileName, len(newContent))
o.content = newContent
logrus.Infof("parsed certificate database '%s', found information for %d certificates", o.fileName, count)
return nil
}
@ -193,49 +210,49 @@ func (o *OpenSSLCertDB) watchIndexFile(watcher *fsnotify.Watcher) {
}
// The parseLine function parses a line of index.txt.
func parseLine(line string) (string, *ocsp.Response) {
func parseLine(line string) *CertificateUpdate {
const (
fieldSeparator = "\t"
)
if line == "" {
return "", nil
return nil
}
parts := strings.Split(line, fieldSeparator)
if len(parts) != idxSubjectDN+1 {
logrus.Warnf("found invalid line '%s'", line)
return "", nil
return nil
}
serial, serialNumber, err := parseSerialNumber(parts)
if err != nil {
logrus.Warn(err)
return "", nil
return nil
}
response := ocsp.Response{
SerialNumber: serialNumber,
update := &CertificateUpdate{
Serial: serialNumber,
}
mapStatusField(&response, parts)
mapStatusField(update, parts)
if logrus.IsLevelEnabled(logrus.TraceLevel) {
traceParsedCertificateLine(parts, serial)
}
if response.Status == ocsp.Revoked {
err = handleRevoked(&response, parts, serial)
if update.Status == ocsp.Revoked {
err = handleRevoked(update, parts, serial)
if err != nil {
logrus.Warn(err)
return "", nil
return nil
}
}
return serialNumber.Text(hexBase), &response
return update
}
func parseSerialNumber(parts []string) (string, *big.Int, error) {
@ -250,15 +267,15 @@ func parseSerialNumber(parts []string) (string, *big.Int, error) {
return serial, serialNumber, nil
}
func mapStatusField(response *ocsp.Response, parts []string) {
func mapStatusField(update *CertificateUpdate, parts []string) {
switch parts[idxStatus] {
case "V":
response.Status = ocsp.Good
update.Status = ocsp.Good
case "R":
response.Status = ocsp.Revoked
response.RevocationReason = ocsp.Unspecified
update.Status = ocsp.Revoked
update.RevocationReason = ocsp.Unspecified
default:
response.Status = ocsp.Unknown
update.Status = ocsp.Unknown
}
}
@ -274,7 +291,7 @@ func traceParsedCertificateLine(parts []string, serial string) {
)
}
func handleRevoked(response *ocsp.Response, parts []string, serial string) error {
func handleRevoked(response *CertificateUpdate, parts []string, serial string) error {
const lenWithReason = 2
if parts[idxRevocation] == "" {

@ -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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`,
`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`,
`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`,
`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`,
`MIIJQQIBADANBgkqhkiG9w0BAQEFAASCCSswggknAgEAAoICAQCi/gLhii/iYFL + S1N2/h4MT + 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`,
`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`,
`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`,
`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`,
`MIIJQwIBADANBgkqhkiG9w0BAQEFAASCCS0wggkpAgEAAoICAQCidb3sb8IyHeUJKZrFBPLxyx94CE7JYDf1VDx9KeyNp5Ywz1wD7WZ71tTzYnIcHit92M51qyGA5kxa/XOvhiLCxgx8hV37yyPBlaxfTVtvobsEof966OAlVqfwfqiBgAwo98PfGkePyKSS7SMvwlddDWUANdp4oFAyJPd/ohQY+hnG0jbpsIjS7LP36VQmcOYJDZKySUaJ/i8/bxI4OFUPOijZ/YMO8fqH4SWUWD+ql1X2bpyeU/lcWZ8viJr6zIpMLU6iKymGwpCzw4mLmiQXa8IrYK35IiBdg+kwZFbxhE34gkuRb8KAqK2DFv5rEl6Tgz/UNILvfRKAk/AS5lt0s8di+X/0/mw8o44LqmVSsAJjfUyRcuNBy2EvBoVrEINuLuDNCOiveTSI5+cPgKes4US7wWnjBmE8atsC6g3sJ5ms9A8/H4wF4EwcO6+YY1yoJXUDkASDqmCCH0Aebbi16nI4CZ7xg4zPPaM0vL2sd3uMDRl42VN67vW++P/GMZ+J1YNv8Dsqd9YrpDb3RbRjUq7rcFxYBsaIJvO6oarN2RovxrupGuD5ssp0tjQEq+dwXcqeEUFdUNuMAq8Xq3cp5Gbuj3Ks+2YpWOTaWaHQq98aCDYY0uT78pXf2usQaWD9Rx/XcP9TzZ5KxhpXziMjR9W2P/lBWiO+ln5ZnO+m5QIDAQABAoICABxduJq27M55X8tYw1hRQ+YqFEWOFcudjEe+vI0SK+aT82p0WlmpMvK+kRIqB0iXj/p6R5pOFhx+mahthqyIHDur/vQjCUT0tDqR2rYNOMDmGVR2wLgCP1jlp48bXmq2I+lnUYD8ArC9VNb+pt0o7r4ln5QI0yuqujX3NGvJrf6qdKO9gq8NDubdQ5Mu322S2ueLwih1GObxReiqiSDd3SQ6gHWHiUGryJW/HyOmwP8VS4Iq0BfpC6ovT9cp3/2YhzFD2+3+4VJRWJi6tKJ2eITKYO3YBKhyZ/pMFQRJENE8y6xve6haPjjeuuUft66UV4UkLpnrzUzpprhinFGAfUk8/WzO0nGtYw2RbH3VVDWQcE7Lc7EfLPkQ7uirPwhAPF+iu7+km1P49inX+sEACyhYR8hkYLXORnZnuzEOGDfDiXqlBFqKho8eW4OLj0kDi+LHmuiq8dofPXVLO2M/FEd5jTAGfR4bDyEnzykbE3EP2/iiIQ+sG8Rx2jdvwOVVmQjbh5bgurB4bg40GEAeXgjmf1kD+ydb+3lQ6J9ZD9GyfnKvQlk0fvUMaMpsP94la1qCpI88zCz/pvxdQ7hd41q3AjwHeH+O4+3hV6UuJ+EXZIXbbWNGFnMAH4172txB8KFwVAjOO97R+MXfiYB2pQNR/n0rAaffiTHOh5VIe/alAoIBAQDOzwyLoDH2oSXOnddeWba6PNpM4I9YOBZiQFdLsYp4Z8GDeLRsjlPlPwKObzKEwCz0M9SHFKlJlAL54aSz4hy6929m+1kdaSbKbBVn2mW3BjOYa+XdcPBIflP1LvfVdIDDatk8W9Na8QrfVBS719fO2fCURmigaIRG95cIo1q/aLv/YJ2lXGNeDTFJffq7wdDBocqS4QnKiDEvjulc6KS42k28JU6TY7XwFja6yq8WcqtI0tuC3Mrg2Zc9y/XN8cGWqMyfB+g7+jfHpl1IXAyJ/urDeZ5lRLyznz/TynLlA8Y/cOFTpaJyzLkiO9F00J6q+qPHqCqV+txBC1Iu0l1bAoIBAQDJGjaFg+/eAkgEQNOLEM1GeCtM3i5KljllJYy2vYRN/7ZxkSmJyPkdBx0FIr04e8Vhay47cv+MhHTKC4nlsZ5viWGgKS8Tw06nv0bPHbyZTTVcsnyI4uHQz+QCRlcQXl7EiJOg9oDtDLHkBJtJ+VuwGkq65STpk+cSVVUQO7uMYXqrBAGldpDNQwvj9pI25nDoVx9nVSd2R5KUG/3c0MZZAOFPRJMQ8BB57q5PTI4tjO7T4wYn2pZsbbK42W5irx4FdfAlbSOpk29V1dgaQ79FhcZYmM1OEMwhZd3Dx90xCCYpohg5v1OTbaNDGl6GH09SspkXldFR7ZGlIPk+tAC/AoIBAQDJSiV/0VBQKNFPpbn2D4QXCRvBL+DVkOHYh59wAUIxp2tmt5mE/5MQ0p9+WyTkqNgYaEo3oBiVkQbEiKdynmYwZJ19sq8uPSaQZ1r+VkgiOFsHxB1bx41YQf0d67U1gcvCaMFASnARWAOpE4GKw689mkrbRxyW5KO7H0lWEwlEvIAfNL68MIxXAqgyS/g7v42cqgktpPOOubiF/aeN0rJhabm3ougz9R0krbpfXN/GDZ5Bp1oYqSaea6sVbuu0o9Y0+d8P+vZDSmv4Wyj4QP/DYObHlBxjnN++RQYu8iGaXm4bWZc3f0P6UODxPR/FfziwWnwG3Nt2V1EBzi8Te5V5AoIBAGHBFTmnncTMVReEL5CM9UvJBDtUUSo+hd3N9+2oiBSmNZhU2N06TSW4HHovnc1EFPk68XlCeEhk5qSEIV7/DSXooQUKn/C6YpSWHSR2rwPSDRtnQ7QCnYQ0WgBlEtNJHEXqoeB/ksIoBpzxRE8cIF6lGShqVJwaIhu+USAX+R3PEKLqyqYU0WSFr9T7Oa3oLIA0QXaoZQtQ1UabqLZa4d/H1r4Cdn6oQ0oSJLc1XnNz63AiDg73aWGBnXvPbaIYv74lyztWJxy6J0eRMH/r6KEx2u2cMUg+UAv4CWmu0K2R6YMZYS9I+qZSiCcJPWNy+ssXN+XIFLz/f2nGcoHxW98CggEBALuiiVdhfg+gWuHRvw/4YOcjPy1FlzNwj4hb9CQy+n/2Mj7NuJo6UiQZFN54dZ1TGwUxX1qgKxhBB9zDukrv4WeGuP9AYkoD0ed3ETY7GZOThfLmxhgfW9m55OneKsHM1n82a0yDpABHly3dYBrs0Gtc/ZypzpFEJfFwoF9QqNwvFf9qDkbweEXGok/iAboIMKDgo08mtAaGb1+sUPRg5tleMli8fvPztZZ3p3f0MOx99D3zCbcibwW1CM0loNt7LSpucB2mB+sHCHEAASkmBrMPbGtmEBZBkrcG1aVemkYZwMYsSYz3kFRq0xmN3zRAibzqZP3LylTPBlD+GTNcYsw`,
}
)
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…
Cancel
Save