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

232 lines
6.4 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"
"crypto"
"crypto/x509"
"crypto/x509/pkix"
"encoding/asn1"
"encoding/hex"
"errors"
"fmt"
"math/big"
"net/http"
"time"
"github.com/sirupsen/logrus"
"git.cacert.org/cacert-goocsp/pkg/ocsp"
)
type CertificateUpdate struct {
Serial *big.Int
Status int
NotAfter time.Time
RevokedAt time.Time
RevocationReason int
}
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
type CertificateIssuer struct {
responderCertificate *x509.Certificate
responderKey crypto.Signer
caCertificate *x509.Certificate
certDb CertificateDatabase
}
// NewIssuer is used to construct a new CertificateIssuer instance
func NewIssuer(
caCertificate, responderCertificate *x509.Certificate,
responderKey crypto.Signer,
certDb CertificateDatabase,
) *CertificateIssuer {
return &CertificateIssuer{
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
// 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
template.SupportExtendedRevoke = true
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(serial *big.Int) ([]byte, error) {
response := i.certDb.LookupResponseTemplate(serial)
if response == nil {
return i.buildUnknownResponse(serial)
}
return i.buildResponse(response)
}
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
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
}
func (o *OcspSource) getDefaultIssuer() *CertificateIssuer {
return o.issuers[0]
}