You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
goocsp/pkg/ocspsource/ocspsource.go

241 lines
6.8 KiB
Go

/*
Copyright 2022 CAcert Inc.
SPDX-License-Identifier: Apache-2.0
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package ocspsource
import (
"bytes"
"context"
"crypto"
"crypto/x509"
"crypto/x509/pkix"
"encoding/asn1"
"encoding/hex"
"errors"
"fmt"
"math/big"
"net/http"
"sync"
"time"
"github.com/sirupsen/logrus"
"golang.org/x/crypto/ocsp"
)
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
type CertificateIssuer struct {
responderCertificate *x509.Certificate
responderKey crypto.Signer
caCertificate *x509.Certificate
certDb *OpenSSLCertDB
}
// 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)
}
return &CertificateIssuer{
caCertificate: caCertificate, responderCertificate: responderCertificate, responderKey: responderKey, certDb: certDb,
}, nil
}
// publicKeyMatches determines whether a given public key hash matches the public key of the CA certificate of the
// CertificateIssuer.
func (i *CertificateIssuer) publicKeyMatches(requestHash []byte, algorithm crypto.Hash) (bool, error) {
var publicKeyInfo struct {
Algorithm pkix.AlgorithmIdentifier
PublicKey asn1.BitString
}
if _, err := asn1.Unmarshal(i.caCertificate.RawSubjectPublicKeyInfo, &publicKeyInfo); err != nil {
return false, fmt.Errorf("could not parse CA certificate public key info: %w", err)
}
h := algorithm.New()
if _, err := h.Write(publicKeyInfo.PublicKey.RightAlign()); err != nil {
return false, fmt.Errorf("could not update digest instance: %w", err)
}
issuerHash := h.Sum(nil)
return bytes.Equal(issuerHash, requestHash), nil
}
// buildUnknownResponse builds the OCSP response template for an unknown certificate
func (i *CertificateIssuer) buildUnknownResponse(number *big.Int) ([]byte, error) {
template := &ocsp.Response{
SerialNumber: number,
Status: ocsp.Unknown,
}
return i.buildResponse(template)
}
// buildResponse fills the timestamp fields of the response template and creates a signed OCSP response
func (i *CertificateIssuer) buildResponse(template *ocsp.Response) ([]byte, error) {
template.ProducedAt = time.Now()
template.ThisUpdate = time.Now()
template.NextUpdate = time.Now().Add(time.Hour)
template.Certificate = i.responderCertificate
response, err := ocsp.CreateResponse(i.caCertificate, i.responderCertificate, *template, i.responderKey)
if err != nil {
return nil, fmt.Errorf("could not create final OCSP response: %w", err)
}
return response, nil
}
// String returns the name of the CertificateIssuer
func (i *CertificateIssuer) String() string {
return i.caCertificate.Subject.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)
if response == nil {
return i.buildUnknownResponse(serialNumber)
}
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()
}
// OcspSource is an implementation of the Source interface used by the Cloudflare OCSP response handler
type OcspSource struct {
issuers []*CertificateIssuer
}
// Option to configure OcspSource
type Option func(*OcspSource) error
// NewSource creates a OcspSource instance configured via the passed Options
func NewSource(options ...Option) (*OcspSource, error) {
source := &OcspSource{}
for _, option := range options {
err := option(source)
if err != nil {
return nil, err
}
}
if len(source.issuers) == 0 {
return nil, errors.New("no issuers configured")
}
return source, nil
}
// WithIssuer is an Option to add a CertificateIssuer to an OcspSource
func WithIssuer(issuer *CertificateIssuer) Option {
return func(o *OcspSource) error {
o.issuers = append(o.issuers, issuer)
logrus.Infof("add issuer %s as known issuer", issuer)
return nil
}
}
// Response is dispatching incoming requests to the CertificateIssuer instances of the OcspSource
func (o *OcspSource) Response(r *ocsp.Request) ([]byte, http.Header, error) {
issuer, err := o.getIssuer(r.IssuerKeyHash, r.HashAlgorithm)
if err != nil {
if errors.Is(err, errUnknownIssuer) {
logrus.Infof("received request for unknown issuer key hash %s", hex.EncodeToString(r.IssuerKeyHash))
issuer = o.getDefaultIssuer()
response, err := issuer.buildUnknownResponse(r.SerialNumber)
if err != nil {
return nil, nil, fmt.Errorf("could not build OCSP response: %w", err)
}
return response, nil, nil
}
return nil, nil, fmt.Errorf("cannot answer request: %w", err)
}
response, err := issuer.LookupResponse(r.SerialNumber)
if err != nil {
return nil, nil, fmt.Errorf("could not build OCSP response: %w", err)
}
return response, nil, nil
}
// The getIssuer method looks for a matching issuer for the issuer key hash of an incoming OCSP request
func (o *OcspSource) getIssuer(keyHash []byte, algorithm crypto.Hash) (*CertificateIssuer, error) {
for _, issuer := range o.issuers {
matched, err := issuer.publicKeyMatches(keyHash, algorithm)
if err != nil {
logrus.Errorf("error for issuer: %v", err)
}
if matched {
return issuer, nil
}
}
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]
}