Implement golangci-lint suggestions
This commit is contained in:
parent
5e065c9692
commit
a8b2bec8f5
4 changed files with 462 additions and 314 deletions
|
@ -1,5 +1,6 @@
|
|||
/*
|
||||
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.
|
||||
|
@ -31,12 +32,13 @@ import (
|
|||
"syscall"
|
||||
"time"
|
||||
|
||||
"git.cacert.org/cacert-goocsp/internal/ocspsource"
|
||||
"github.com/cloudflare/cfssl/ocsp"
|
||||
"github.com/knadh/koanf"
|
||||
"github.com/knadh/koanf/parsers/yaml"
|
||||
"github.com/knadh/koanf/providers/file"
|
||||
"github.com/sirupsen/logrus"
|
||||
|
||||
"git.cacert.org/cacert-goocsp/pkg/ocspsource"
|
||||
)
|
||||
|
||||
/* constants for configuration keys */
|
||||
|
@ -50,52 +52,22 @@ const (
|
|||
)
|
||||
|
||||
func main() {
|
||||
var serverAddr = flag.String("serverAddr", ":8080", "Server ip addr and port")
|
||||
var config = koanf.New(".")
|
||||
var (
|
||||
serverAddr = flag.String("serverAddr", ":8080", "Server ip addr and port")
|
||||
config = koanf.New(".")
|
||||
opts []ocspsource.Option
|
||||
)
|
||||
|
||||
err := config.Load(file.Provider("config.yaml"), yaml.Parser())
|
||||
if err != nil {
|
||||
logrus.Panicf("could not load configuration: %v", err)
|
||||
}
|
||||
|
||||
logrus.SetLevel(logrus.DebugLevel)
|
||||
var opts []ocspsource.Option
|
||||
|
||||
issuerConfigs := config.Slices(coIssuers)
|
||||
for number, issuerConfig := range issuerConfigs {
|
||||
hasErrors := false
|
||||
for _, item := range []string{issuerCaCert, issuerReCert, issuerReKey, issuerCertList} {
|
||||
if v := issuerConfig.String(item); v == "" {
|
||||
logrus.Warnf("%s parameter for issuers entry %d is missing", item, number)
|
||||
hasErrors = true
|
||||
}
|
||||
}
|
||||
if hasErrors {
|
||||
logrus.Warnf("configuration for issuers entry %d had errors and has been skipped", number)
|
||||
continue
|
||||
}
|
||||
|
||||
caCertificate, err := parseCertificate(issuerConfig.String(issuerCaCert))
|
||||
if err != nil {
|
||||
logrus.Errorf("could not parse CA certificate for issuer %d: %v", number, err)
|
||||
continue
|
||||
}
|
||||
responderCertificate, err := parseCertificate(issuerConfig.String(issuerReCert))
|
||||
if err != nil {
|
||||
logrus.Errorf("could not parse OCSP responder certificate for issuer %d: %v", number, err)
|
||||
continue
|
||||
}
|
||||
responderKey, err := parsePrivateKey(issuerConfig.String(issuerReKey))
|
||||
if err != nil {
|
||||
logrus.Errorf("could not parse OCSP responder key for issuer %d: %v", number, err)
|
||||
continue
|
||||
}
|
||||
issuer, err := ocspsource.NewIssuer(caCertificate, responderCertificate, responderKey, issuerConfig.String(issuerCertList))
|
||||
if err != nil {
|
||||
logrus.Errorf("could not create issuer %d: %v", number, err)
|
||||
continue
|
||||
}
|
||||
opts = append(opts, ocspsource.WithIssuer(issuer))
|
||||
}
|
||||
opts = configureIssuers(issuerConfigs, opts)
|
||||
|
||||
cacertSource, err := ocspsource.NewSource(opts...)
|
||||
if err != nil {
|
||||
|
@ -121,17 +93,78 @@ func main() {
|
|||
if !errors.Is(err, http.ErrServerClosed) {
|
||||
logrus.Panicf("could not start the server process: %v", err)
|
||||
}
|
||||
|
||||
logrus.Infof("server shutdown")
|
||||
}
|
||||
}
|
||||
|
||||
func configureIssuers(issuerConfigs []*koanf.Koanf, opts []ocspsource.Option) []ocspsource.Option {
|
||||
for number, issuerConfig := range issuerConfigs {
|
||||
hasErrors := false
|
||||
|
||||
for _, item := range []string{issuerCaCert, issuerReCert, issuerReKey, issuerCertList} {
|
||||
if v := issuerConfig.String(item); v == "" {
|
||||
logrus.Warnf("%s parameter for issuers entry %d is missing", item, number)
|
||||
|
||||
hasErrors = true
|
||||
}
|
||||
}
|
||||
|
||||
if hasErrors {
|
||||
logrus.Warnf("configuration for issuers entry %d had errors and has been skipped", number)
|
||||
|
||||
continue
|
||||
}
|
||||
|
||||
caCertificate, err := parseCertificate(issuerConfig.String(issuerCaCert))
|
||||
if err != nil {
|
||||
logrus.Errorf("could not parse CA certificate for issuer %d: %v", number, err)
|
||||
|
||||
continue
|
||||
}
|
||||
|
||||
responderCertificate, err := parseCertificate(issuerConfig.String(issuerReCert))
|
||||
if err != nil {
|
||||
logrus.Errorf("could not parse OCSP responder certificate for issuer %d: %v", number, err)
|
||||
|
||||
continue
|
||||
}
|
||||
|
||||
responderKey, err := parsePrivateKey(issuerConfig.String(issuerReKey))
|
||||
if err != nil {
|
||||
logrus.Errorf("could not parse OCSP responder key for issuer %d: %v", number, err)
|
||||
|
||||
continue
|
||||
}
|
||||
|
||||
issuer, err := ocspsource.NewIssuer(
|
||||
caCertificate,
|
||||
responderCertificate,
|
||||
responderKey,
|
||||
issuerConfig.String(issuerCertList),
|
||||
)
|
||||
if err != nil {
|
||||
logrus.Errorf("could not create issuer %d: %v", number, err)
|
||||
|
||||
continue
|
||||
}
|
||||
|
||||
opts = append(opts, ocspsource.WithIssuer(issuer))
|
||||
}
|
||||
|
||||
return opts
|
||||
}
|
||||
|
||||
// The setupCloseHandler takes care of OS signal handling
|
||||
func setupCloseHandler(ctx context.Context, server *http.Server) {
|
||||
c := make(chan os.Signal, 0)
|
||||
c := make(chan os.Signal, 1)
|
||||
|
||||
signal.Notify(c, os.Interrupt, syscall.SIGTERM)
|
||||
|
||||
go func(ctx context.Context) {
|
||||
<-c
|
||||
logrus.Infof("program interrupted")
|
||||
|
||||
err := server.Shutdown(ctx)
|
||||
if err != nil {
|
||||
logrus.Errorf("could not close server: %v", err)
|
||||
|
@ -143,6 +176,7 @@ func setupCloseHandler(ctx context.Context, server *http.Server) {
|
|||
func withLogging(next http.HandlerFunc) http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
start := time.Now()
|
||||
|
||||
next.ServeHTTP(w, r)
|
||||
logrus.Infof("GET %s FROM %s in %dms", r.URL.Path, r.RemoteAddr, time.Since(start).Milliseconds())
|
||||
}
|
||||
|
@ -159,6 +193,7 @@ func parseCertificate(certificateFile string) (*x509.Certificate, error) {
|
|||
if block == nil {
|
||||
return nil, fmt.Errorf("could not find PEM data in %s", certificateFile)
|
||||
}
|
||||
|
||||
certificate, err := x509.ParseCertificate(block.Bytes)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("could not parse certificate in %s: %w", certificateFile, err)
|
||||
|
@ -185,12 +220,19 @@ func parsePrivateKey(keyFile string) (crypto.Signer, error) {
|
|||
if err != nil {
|
||||
return nil, fmt.Errorf("no usable private key found in %s: %w", keyFile, err)
|
||||
}
|
||||
return key.(crypto.Signer), nil
|
||||
|
||||
signer, ok := key.(crypto.Signer)
|
||||
if !ok {
|
||||
return nil, errors.New("key cannot be used as signer")
|
||||
}
|
||||
|
||||
return signer, nil
|
||||
case "RSA PRIVATE KEY":
|
||||
rsaKey, err := x509.ParsePKCS1PrivateKey(block.Bytes)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("no usable private key found in %s: %w", keyFile, err)
|
||||
}
|
||||
|
||||
return rsaKey, nil
|
||||
default:
|
||||
return nil, fmt.Errorf("unsupported PEM block type %s in %s", block.Type, keyFile)
|
||||
|
|
|
@ -1,247 +0,0 @@
|
|||
/*
|
||||
Copyright 2022 CAcert Inc.
|
||||
|
||||
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 (
|
||||
"bufio"
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"math/big"
|
||||
"os"
|
||||
"path"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/fsnotify/fsnotify"
|
||||
"github.com/sirupsen/logrus"
|
||||
"golang.org/x/crypto/ocsp"
|
||||
)
|
||||
|
||||
// OpenSSLCertDB implements the needed glue code to interface with the index.txt format of openssl ca
|
||||
type OpenSSLCertDB struct {
|
||||
fileName string
|
||||
content map[string]*ocsp.Response
|
||||
}
|
||||
|
||||
// NewCertDB creates a new certificate database for the given index.txt file
|
||||
func NewCertDB(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}
|
||||
|
||||
err = certDb.update()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return certDb, nil
|
||||
}
|
||||
|
||||
// The update method reads the content of index.txt and replaces the current content of the OpenSSLCertDB
|
||||
func (o *OpenSSLCertDB) update() error {
|
||||
f, err := os.Open(o.fileName)
|
||||
if err != nil {
|
||||
return fmt.Errorf("could not open %s: %w", o.fileName, err)
|
||||
}
|
||||
defer func(f *os.File) {
|
||||
_ = f.Close()
|
||||
}(f)
|
||||
|
||||
newContent := make(map[string]*ocsp.Response)
|
||||
b := bufio.NewReader(f)
|
||||
lastLine := false
|
||||
for {
|
||||
line, err := b.ReadString('\n')
|
||||
if err != nil {
|
||||
if !errors.Is(err, io.EOF) {
|
||||
return fmt.Errorf("could not read next line: %w", err)
|
||||
}
|
||||
lastLine = true
|
||||
}
|
||||
|
||||
serial, response := parseLine(strings.TrimSpace(line))
|
||||
if serial != "" {
|
||||
newContent[serial] = response
|
||||
}
|
||||
|
||||
if lastLine {
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
logrus.Infof("parsed certificate database '%s', found information for %d certificates", o.fileName, len(newContent))
|
||||
|
||||
o.content = newContent
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// LookupResponseTemplate retrieves an OCSP response template for the given certificate serial number.
|
||||
func (o *OpenSSLCertDB) LookupResponseTemplate(number *big.Int) *ocsp.Response {
|
||||
serial := number.Text(16)
|
||||
if response, ok := o.content[serial]; ok {
|
||||
return response
|
||||
}
|
||||
// RFC https://datatracker.ietf.org/doc/html/rfc6960#section-2.2 states that certificates we cannot answer for
|
||||
// should be marked as good. This would allow a CRL as sole certificate source.
|
||||
//
|
||||
// We might change this to an unknown response when we have a more accurate certificate source like an API or an
|
||||
// up-to-date openssl index.txt.
|
||||
logrus.Debugf("received request for certificate %s that is not in our data source", serial)
|
||||
response := &ocsp.Response{
|
||||
Status: ocsp.Good,
|
||||
SerialNumber: number,
|
||||
}
|
||||
return response
|
||||
}
|
||||
|
||||
// WatchChanges creates file system monitoring for the index.txt file of the OpenSSLCertDB
|
||||
func (o *OpenSSLCertDB) WatchChanges(ctx context.Context) {
|
||||
watcher, err := fsnotify.NewWatcher()
|
||||
if err != nil {
|
||||
logrus.Fatalf("could not create file watcher: %v", err)
|
||||
}
|
||||
defer func(watcher *fsnotify.Watcher) {
|
||||
_ = watcher.Close()
|
||||
logrus.Infof("stopped watching for %s changes", o.fileName)
|
||||
}(watcher)
|
||||
|
||||
go func(watcher *fsnotify.Watcher, filename string) {
|
||||
for {
|
||||
select {
|
||||
case event, ok := <-watcher.Events:
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
if event.Op&fsnotify.Write == fsnotify.Write {
|
||||
if event.Name == filename {
|
||||
logrus.Infof("modified: %s", event.Name)
|
||||
err := o.update()
|
||||
if err != nil {
|
||||
logrus.Error(err)
|
||||
}
|
||||
}
|
||||
}
|
||||
case err, ok := <-watcher.Errors:
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
logrus.Errorf("error from watcher: %v", err)
|
||||
}
|
||||
}
|
||||
}(watcher, o.fileName)
|
||||
|
||||
err = watcher.Add(path.Dir(o.fileName))
|
||||
if err != nil {
|
||||
logrus.Fatalf("could not watch %s: %v", o.fileName, err)
|
||||
}
|
||||
logrus.Infof("watching for changes on %s", o.fileName)
|
||||
|
||||
<-ctx.Done()
|
||||
logrus.Infof("ending background tasks for %s", o.fileName)
|
||||
}
|
||||
|
||||
// The parseLine function parses a line of index.txt.
|
||||
func parseLine(line string) (string, *ocsp.Response) {
|
||||
if line == "" {
|
||||
return "", nil
|
||||
}
|
||||
|
||||
parts := strings.Split(line, "\t")
|
||||
if len(parts) != 6 {
|
||||
logrus.Warnf("found invalid line '%s'", line)
|
||||
return "", nil
|
||||
}
|
||||
|
||||
serial := parts[3]
|
||||
serialNumber := new(big.Int)
|
||||
_, ok := serialNumber.SetString(serial, 16)
|
||||
if !ok {
|
||||
logrus.Warnf("could not parse %s as serial number", serial)
|
||||
return "", nil
|
||||
}
|
||||
|
||||
response := ocsp.Response{
|
||||
SerialNumber: serialNumber,
|
||||
}
|
||||
|
||||
switch parts[0] {
|
||||
case "V":
|
||||
response.Status = ocsp.Good
|
||||
case "R":
|
||||
response.Status = ocsp.Revoked
|
||||
response.RevocationReason = ocsp.Unspecified
|
||||
default:
|
||||
response.Status = ocsp.Unknown
|
||||
}
|
||||
|
||||
if response.Status == ocsp.Revoked && parts[2] == "" {
|
||||
logrus.Warnf("inconsistency detected certificate with serial %s is marked as revoked but has no revocation timestamp", serial)
|
||||
return "", nil
|
||||
}
|
||||
|
||||
if response.Status == ocsp.Revoked {
|
||||
revocation := []string{parts[2]}
|
||||
if strings.Contains(revocation[0], ",") {
|
||||
revocation = strings.SplitN(revocation[0], ",", 2)
|
||||
}
|
||||
if len(revocation) == 2 {
|
||||
response.RevocationReason = mapRevocationReason(revocation[1])
|
||||
}
|
||||
revocationTimeStamp, err := time.Parse("20060102150405Z", revocation[0])
|
||||
if err != nil {
|
||||
logrus.Warnf("could not parse %s as revocation timestamp for serial %s: %v", revocation[0], serial, err)
|
||||
return "", nil
|
||||
}
|
||||
response.RevokedAt = revocationTimeStamp.UTC()
|
||||
}
|
||||
|
||||
return serialNumber.Text(16), &response
|
||||
}
|
||||
|
||||
// mapRevocationReason takes a OCSP revocation reason string and converts it to the numerical representation required
|
||||
// for OCSP responses.
|
||||
func mapRevocationReason(reason string) int {
|
||||
switch reason {
|
||||
case "keyCompromise":
|
||||
return ocsp.KeyCompromise
|
||||
case "CACompromise":
|
||||
return ocsp.CACompromise
|
||||
case "affiliationChanged":
|
||||
return ocsp.AffiliationChanged
|
||||
case "superseded":
|
||||
return ocsp.Superseded
|
||||
case "cessationOfOperation":
|
||||
return ocsp.CessationOfOperation
|
||||
case "certificateHold":
|
||||
return ocsp.CertificateHold
|
||||
case "removeFromCRL":
|
||||
return ocsp.RemoveFromCRL
|
||||
case "privilegeWithdrawn":
|
||||
return ocsp.PrivilegeWithdrawn
|
||||
case "AACompromise":
|
||||
return ocsp.AACompromise
|
||||
default:
|
||||
return ocsp.Unspecified
|
||||
}
|
||||
}
|
|
@ -1,5 +1,6 @@
|
|||
/*
|
||||
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.
|
||||
|
@ -35,7 +36,7 @@ import (
|
|||
"golang.org/x/crypto/ocsp"
|
||||
)
|
||||
|
||||
var unknownIssuer = errors.New("issuer key hash does not match any of the known issuers")
|
||||
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 {
|
||||
|
@ -46,7 +47,11 @@ type CertificateIssuer struct {
|
|||
}
|
||||
|
||||
// NewIssuer is used to construct a new CertificateIssuer instance
|
||||
func NewIssuer(caCertificate, responderCertificate *x509.Certificate, responderKey crypto.Signer, certificateFile string) (*CertificateIssuer, error) {
|
||||
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)
|
||||
|
@ -73,6 +78,7 @@ func (i *CertificateIssuer) publicKeyMatches(requestHash []byte, algorithm crypt
|
|||
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
|
||||
|
@ -84,6 +90,7 @@ func (i *CertificateIssuer) buildUnknownResponse(number *big.Int) ([]byte, error
|
|||
SerialNumber: number,
|
||||
Status: ocsp.Unknown,
|
||||
}
|
||||
|
||||
return i.buildResponse(template)
|
||||
}
|
||||
|
||||
|
@ -93,7 +100,13 @@ func (i *CertificateIssuer) buildResponse(template *ocsp.Response) ([]byte, erro
|
|||
template.ThisUpdate = time.Now()
|
||||
template.NextUpdate = time.Now().Add(time.Hour)
|
||||
template.Certificate = i.responderCertificate
|
||||
return ocsp.CreateResponse(i.caCertificate, i.responderCertificate, *template, i.responderKey)
|
||||
|
||||
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
|
||||
|
@ -107,12 +120,14 @@ func (i *CertificateIssuer) LookupResponse(serialNumber *big.Int) ([]byte, error
|
|||
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)
|
||||
|
||||
|
@ -150,7 +165,9 @@ func NewSource(options ...Option) (*OcspSource, error) {
|
|||
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
|
||||
}
|
||||
}
|
||||
|
@ -159,22 +176,27 @@ func WithIssuer(issuer *CertificateIssuer) Option {
|
|||
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, unknownIssuer) {
|
||||
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: %v", err)
|
||||
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: %v", err)
|
||||
return nil, nil, fmt.Errorf("could not build OCSP response: %w", err)
|
||||
}
|
||||
|
||||
return response, nil, nil
|
||||
}
|
||||
|
||||
|
@ -185,11 +207,13 @@ func (o *OcspSource) getIssuer(keyHash []byte, algorithm crypto.Hash) (*Certific
|
|||
if err != nil {
|
||||
logrus.Errorf("error for issuer: %v", err)
|
||||
}
|
||||
|
||||
if matched {
|
||||
return issuer, nil
|
||||
}
|
||||
}
|
||||
return nil, unknownIssuer
|
||||
|
||||
return nil, errUnknownIssuer
|
||||
}
|
||||
|
||||
// BackgroundTasks delegates background task handling to the CertificateIssuer instances of the OcspSource
|
||||
|
@ -197,13 +221,16 @@ 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")
|
||||
}
|
326
pkg/ocspsource/opensslcertdb.go
Normal file
326
pkg/ocspsource/opensslcertdb.go
Normal file
|
@ -0,0 +1,326 @@
|
|||
/*
|
||||
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 (
|
||||
"bufio"
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"math/big"
|
||||
"os"
|
||||
"path"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/fsnotify/fsnotify"
|
||||
"github.com/sirupsen/logrus"
|
||||
"golang.org/x/crypto/ocsp"
|
||||
)
|
||||
|
||||
const (
|
||||
hexBase = 16
|
||||
opensslUtcFormat = "20060102150405Z"
|
||||
)
|
||||
|
||||
// field positions in index.txt
|
||||
|
||||
const (
|
||||
idxStatus int = iota
|
||||
idxExpiration
|
||||
idxRevocation
|
||||
idxSerial
|
||||
idxFilename
|
||||
idxSubjectDN
|
||||
)
|
||||
|
||||
// OpenSSLCertDB implements the needed glue code to interface with the index.txt format of openssl ca
|
||||
type OpenSSLCertDB struct {
|
||||
fileName string
|
||||
content map[string]*ocsp.Response
|
||||
}
|
||||
|
||||
// NewCertDB creates a new certificate database for the given index.txt file
|
||||
func NewCertDB(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}
|
||||
|
||||
err = certDb.update()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return certDb, nil
|
||||
}
|
||||
|
||||
// The update method reads the content of index.txt and replaces the current content of the OpenSSLCertDB
|
||||
func (o *OpenSSLCertDB) update() error {
|
||||
f, err := os.Open(o.fileName)
|
||||
if err != nil {
|
||||
return fmt.Errorf("could not open %s: %w", o.fileName, err)
|
||||
}
|
||||
|
||||
defer func(f *os.File) {
|
||||
_ = f.Close()
|
||||
}(f)
|
||||
|
||||
newContent := make(map[string]*ocsp.Response)
|
||||
b := bufio.NewReader(f)
|
||||
lastLine := false
|
||||
|
||||
for {
|
||||
line, err := b.ReadString('\n')
|
||||
if err != nil {
|
||||
if !errors.Is(err, io.EOF) {
|
||||
return fmt.Errorf("could not read next line: %w", err)
|
||||
}
|
||||
|
||||
lastLine = true
|
||||
}
|
||||
|
||||
serial, response := parseLine(strings.TrimSpace(line))
|
||||
if serial != "" {
|
||||
newContent[serial] = response
|
||||
}
|
||||
|
||||
if lastLine {
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
logrus.Infof("parsed certificate database '%s', found information for %d certificates", o.fileName, len(newContent))
|
||||
|
||||
o.content = newContent
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// LookupResponseTemplate retrieves an OCSP response template for the given certificate serial number.
|
||||
func (o *OpenSSLCertDB) LookupResponseTemplate(number *big.Int) *ocsp.Response {
|
||||
serial := number.Text(hexBase)
|
||||
if response, ok := o.content[serial]; ok {
|
||||
return response
|
||||
}
|
||||
|
||||
logrus.Debugf("received request for certificate %s that is not in our data source", serial)
|
||||
|
||||
// RFC https://datatracker.ietf.org/doc/html/rfc6960#section-2.2 states that certificates we cannot answer for
|
||||
// should be marked as good. This would allow a CRL as sole certificate source.
|
||||
//
|
||||
// We might change this to an unknown response when we have a more accurate certificate source like an API or an
|
||||
// up-to-date openssl index.txt.
|
||||
response := &ocsp.Response{
|
||||
Status: ocsp.Good,
|
||||
SerialNumber: number,
|
||||
}
|
||||
|
||||
return response
|
||||
}
|
||||
|
||||
// WatchChanges creates file system monitoring for the index.txt file of the OpenSSLCertDB
|
||||
func (o *OpenSSLCertDB) WatchChanges(ctx context.Context) {
|
||||
watcher, err := fsnotify.NewWatcher()
|
||||
if err != nil {
|
||||
logrus.Fatalf("could not create file watcher: %v", err)
|
||||
}
|
||||
|
||||
defer func(watcher *fsnotify.Watcher) {
|
||||
_ = watcher.Close()
|
||||
|
||||
logrus.Infof("stopped watching for %s changes", o.fileName)
|
||||
}(watcher)
|
||||
|
||||
go o.watchIndexFile(watcher)
|
||||
|
||||
err = watcher.Add(path.Dir(o.fileName))
|
||||
if err != nil {
|
||||
logrus.Fatalf("could not watch %s: %v", o.fileName, err)
|
||||
}
|
||||
|
||||
logrus.Infof("watching for changes on %s", o.fileName)
|
||||
|
||||
<-ctx.Done()
|
||||
logrus.Infof("ending background tasks for %s", o.fileName)
|
||||
}
|
||||
|
||||
func (o *OpenSSLCertDB) watchIndexFile(watcher *fsnotify.Watcher) {
|
||||
for {
|
||||
select {
|
||||
case event, ok := <-watcher.Events:
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
|
||||
if event.Op&fsnotify.Write == fsnotify.Write {
|
||||
if event.Name == o.fileName {
|
||||
logrus.Infof("modified: %s", event.Name)
|
||||
|
||||
err := o.update()
|
||||
if err != nil {
|
||||
logrus.Error(err)
|
||||
}
|
||||
}
|
||||
}
|
||||
case err, ok := <-watcher.Errors:
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
|
||||
logrus.Errorf("error from watcher: %v", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// The parseLine function parses a line of index.txt.
|
||||
func parseLine(line string) (string, *ocsp.Response) {
|
||||
const (
|
||||
fieldSeparator = "\t"
|
||||
)
|
||||
|
||||
if line == "" {
|
||||
return "", nil
|
||||
}
|
||||
|
||||
parts := strings.Split(line, fieldSeparator)
|
||||
if len(parts) != idxSubjectDN+1 {
|
||||
logrus.Warnf("found invalid line '%s'", line)
|
||||
|
||||
return "", nil
|
||||
}
|
||||
|
||||
serial, serialNumber, err := parseSerialNumber(parts)
|
||||
if err != nil {
|
||||
logrus.Warn(err)
|
||||
|
||||
return "", nil
|
||||
}
|
||||
|
||||
response := ocsp.Response{
|
||||
SerialNumber: serialNumber,
|
||||
}
|
||||
|
||||
mapStatusField(&response, parts)
|
||||
|
||||
if logrus.IsLevelEnabled(logrus.TraceLevel) {
|
||||
traceParsedCertificateLine(parts, serial)
|
||||
}
|
||||
|
||||
if response.Status == ocsp.Revoked {
|
||||
err = handleRevoked(&response, parts, serial)
|
||||
if err != nil {
|
||||
logrus.Warn(err)
|
||||
|
||||
return "", nil
|
||||
}
|
||||
}
|
||||
|
||||
return serialNumber.Text(hexBase), &response
|
||||
}
|
||||
|
||||
func parseSerialNumber(parts []string) (string, *big.Int, error) {
|
||||
serial := parts[idxSerial]
|
||||
serialNumber := new(big.Int)
|
||||
|
||||
_, ok := serialNumber.SetString(serial, hexBase)
|
||||
if !ok {
|
||||
return "", nil, fmt.Errorf("could not parse %s as serial number", serial)
|
||||
}
|
||||
|
||||
return serial, serialNumber, nil
|
||||
}
|
||||
|
||||
func mapStatusField(response *ocsp.Response, parts []string) {
|
||||
switch parts[idxStatus] {
|
||||
case "V":
|
||||
response.Status = ocsp.Good
|
||||
case "R":
|
||||
response.Status = ocsp.Revoked
|
||||
response.RevocationReason = ocsp.Unspecified
|
||||
default:
|
||||
response.Status = ocsp.Unknown
|
||||
}
|
||||
}
|
||||
|
||||
func traceParsedCertificateLine(parts []string, serial string) {
|
||||
expirationTimeStamp, err := time.Parse(opensslUtcFormat, parts[idxExpiration])
|
||||
if err != nil {
|
||||
logrus.Warnf("could not parse %s as expiration timestamp for serial %s: %v", parts[idxExpiration], serial, err)
|
||||
}
|
||||
|
||||
logrus.Tracef(
|
||||
"found certificate with serial number %s, expiration timestamp %s and filename '%s' for subject DN '%s'",
|
||||
serial, expirationTimeStamp, parts[idxFilename], parts[idxSubjectDN],
|
||||
)
|
||||
}
|
||||
|
||||
func handleRevoked(response *ocsp.Response, parts []string, serial string) error {
|
||||
const lenWithReason = 2
|
||||
|
||||
if parts[idxRevocation] == "" {
|
||||
return fmt.Errorf(
|
||||
"inconsistency detected certificate with serial %s is marked as revoked but has no revocation timestamp",
|
||||
serial,
|
||||
)
|
||||
}
|
||||
|
||||
revocation := []string{parts[idxRevocation]}
|
||||
if strings.Contains(revocation[0], ",") {
|
||||
revocation = strings.SplitN(revocation[0], ",", lenWithReason)
|
||||
}
|
||||
|
||||
if len(revocation) == lenWithReason {
|
||||
response.RevocationReason = mapRevocationReason(revocation[1])
|
||||
}
|
||||
|
||||
revocationTimeStamp, err := time.Parse(opensslUtcFormat, revocation[0])
|
||||
if err != nil {
|
||||
return fmt.Errorf("could not parse %s as revocation timestamp for serial %s: %w", revocation[0], serial, err)
|
||||
}
|
||||
|
||||
response.RevokedAt = revocationTimeStamp.UTC()
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// mapRevocationReason takes a OCSP revocation reason string and converts it to the numerical representation required
|
||||
// for OCSP responses.
|
||||
func mapRevocationReason(reason string) int {
|
||||
var revocationReasons = map[string]int{
|
||||
"keyCompromise": ocsp.KeyCompromise,
|
||||
"CACompromise": ocsp.CACompromise,
|
||||
"affiliationChanged": ocsp.AffiliationChanged,
|
||||
"superseded": ocsp.Superseded,
|
||||
"cessationOfOperation": ocsp.CessationOfOperation,
|
||||
"certificateHold": ocsp.CertificateHold,
|
||||
"removeFromCRL": ocsp.RemoveFromCRL,
|
||||
"privilegeWithdrawn": ocsp.PrivilegeWithdrawn,
|
||||
"AACompromise": ocsp.AACompromise,
|
||||
}
|
||||
|
||||
if reasonCode, exist := revocationReasons[reason]; exist {
|
||||
return reasonCode
|
||||
}
|
||||
|
||||
return ocsp.Unspecified
|
||||
}
|
Loading…
Reference in a new issue