Implement golangci-lint suggestions

This commit is contained in:
Jan Dittberner 2022-03-21 18:46:04 +01:00 committed by Jan Dittberner
parent 5e065c9692
commit a8b2bec8f5
4 changed files with 462 additions and 314 deletions

View file

@ -1,5 +1,6 @@
/* /*
Copyright 2022 CAcert Inc. Copyright 2022 CAcert Inc.
SPDX-License-Identifier: Apache-2.0
Licensed under the Apache License, Version 2.0 (the "License"); Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License. you may not use this file except in compliance with the License.
@ -31,12 +32,13 @@ import (
"syscall" "syscall"
"time" "time"
"git.cacert.org/cacert-goocsp/internal/ocspsource"
"github.com/cloudflare/cfssl/ocsp" "github.com/cloudflare/cfssl/ocsp"
"github.com/knadh/koanf" "github.com/knadh/koanf"
"github.com/knadh/koanf/parsers/yaml" "github.com/knadh/koanf/parsers/yaml"
"github.com/knadh/koanf/providers/file" "github.com/knadh/koanf/providers/file"
"github.com/sirupsen/logrus" "github.com/sirupsen/logrus"
"git.cacert.org/cacert-goocsp/pkg/ocspsource"
) )
/* constants for configuration keys */ /* constants for configuration keys */
@ -50,52 +52,22 @@ const (
) )
func main() { func main() {
var serverAddr = flag.String("serverAddr", ":8080", "Server ip addr and port") var (
var config = koanf.New(".") 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()) err := config.Load(file.Provider("config.yaml"), yaml.Parser())
if err != nil { if err != nil {
logrus.Panicf("could not load configuration: %v", err) logrus.Panicf("could not load configuration: %v", err)
} }
logrus.SetLevel(logrus.DebugLevel) logrus.SetLevel(logrus.DebugLevel)
var opts []ocspsource.Option
issuerConfigs := config.Slices(coIssuers) 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)) opts = configureIssuers(issuerConfigs, opts)
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))
}
cacertSource, err := ocspsource.NewSource(opts...) cacertSource, err := ocspsource.NewSource(opts...)
if err != nil { if err != nil {
@ -121,17 +93,78 @@ func main() {
if !errors.Is(err, http.ErrServerClosed) { if !errors.Is(err, http.ErrServerClosed) {
logrus.Panicf("could not start the server process: %v", err) logrus.Panicf("could not start the server process: %v", err)
} }
logrus.Infof("server shutdown") 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 // The setupCloseHandler takes care of OS signal handling
func setupCloseHandler(ctx context.Context, server *http.Server) { 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) signal.Notify(c, os.Interrupt, syscall.SIGTERM)
go func(ctx context.Context) { go func(ctx context.Context) {
<-c <-c
logrus.Infof("program interrupted") logrus.Infof("program interrupted")
err := server.Shutdown(ctx) err := server.Shutdown(ctx)
if err != nil { if err != nil {
logrus.Errorf("could not close server: %v", err) 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 { func withLogging(next http.HandlerFunc) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) { return func(w http.ResponseWriter, r *http.Request) {
start := time.Now() start := time.Now()
next.ServeHTTP(w, r) next.ServeHTTP(w, r)
logrus.Infof("GET %s FROM %s in %dms", r.URL.Path, r.RemoteAddr, time.Since(start).Milliseconds()) 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 { if block == nil {
return nil, fmt.Errorf("could not find PEM data in %s", certificateFile) return nil, fmt.Errorf("could not find PEM data in %s", certificateFile)
} }
certificate, err := x509.ParseCertificate(block.Bytes) certificate, err := x509.ParseCertificate(block.Bytes)
if err != nil { if err != nil {
return nil, fmt.Errorf("could not parse certificate in %s: %w", certificateFile, err) 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 { if err != nil {
return nil, fmt.Errorf("no usable private key found in %s: %w", keyFile, err) 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": case "RSA PRIVATE KEY":
rsaKey, err := x509.ParsePKCS1PrivateKey(block.Bytes) rsaKey, err := x509.ParsePKCS1PrivateKey(block.Bytes)
if err != nil { if err != nil {
return nil, fmt.Errorf("no usable private key found in %s: %w", keyFile, err) return nil, fmt.Errorf("no usable private key found in %s: %w", keyFile, err)
} }
return rsaKey, nil return rsaKey, nil
default: default:
return nil, fmt.Errorf("unsupported PEM block type %s in %s", block.Type, keyFile) return nil, fmt.Errorf("unsupported PEM block type %s in %s", block.Type, keyFile)

View file

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

View file

@ -1,5 +1,6 @@
/* /*
Copyright 2022 CAcert Inc. Copyright 2022 CAcert Inc.
SPDX-License-Identifier: Apache-2.0
Licensed under the Apache License, Version 2.0 (the "License"); Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License. you may not use this file except in compliance with the License.
@ -35,7 +36,7 @@ import (
"golang.org/x/crypto/ocsp" "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 // CertificateIssuer is an abstraction for the OCSP responder that knows certificates of a specific CA
type CertificateIssuer struct { type CertificateIssuer struct {
@ -46,7 +47,11 @@ type CertificateIssuer struct {
} }
// NewIssuer is used to construct a new CertificateIssuer instance // 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) certDb, err := NewCertDB(certificateFile)
if err != nil { if err != nil {
return nil, fmt.Errorf("could not initialize certificate database: %w", err) 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 { if _, err := h.Write(publicKeyInfo.PublicKey.RightAlign()); err != nil {
return false, fmt.Errorf("could not update digest instance: %w", err) return false, fmt.Errorf("could not update digest instance: %w", err)
} }
issuerHash := h.Sum(nil) issuerHash := h.Sum(nil)
return bytes.Equal(issuerHash, requestHash), nil return bytes.Equal(issuerHash, requestHash), nil
@ -84,6 +90,7 @@ func (i *CertificateIssuer) buildUnknownResponse(number *big.Int) ([]byte, error
SerialNumber: number, SerialNumber: number,
Status: ocsp.Unknown, Status: ocsp.Unknown,
} }
return i.buildResponse(template) return i.buildResponse(template)
} }
@ -93,7 +100,13 @@ func (i *CertificateIssuer) buildResponse(template *ocsp.Response) ([]byte, erro
template.ThisUpdate = time.Now() template.ThisUpdate = time.Now()
template.NextUpdate = time.Now().Add(time.Hour) template.NextUpdate = time.Now().Add(time.Hour)
template.Certificate = i.responderCertificate 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 // String returns the name of the CertificateIssuer
@ -107,12 +120,14 @@ func (i *CertificateIssuer) LookupResponse(serialNumber *big.Int) ([]byte, error
if response == nil { if response == nil {
return i.buildUnknownResponse(serialNumber) return i.buildUnknownResponse(serialNumber)
} }
return i.buildResponse(response) return i.buildResponse(response)
} }
// The backgroundTasks of the issuer are started // The backgroundTasks of the issuer are started
func (i *CertificateIssuer) backgroundTasks(ctx context.Context) { func (i *CertificateIssuer) backgroundTasks(ctx context.Context) {
logrus.Infof("starting background tasks for ceritificate issuer %s", i) logrus.Infof("starting background tasks for ceritificate issuer %s", i)
watcherCtx, cancel := context.WithCancel(ctx) watcherCtx, cancel := context.WithCancel(ctx)
go i.certDb.WatchChanges(watcherCtx) go i.certDb.WatchChanges(watcherCtx)
@ -150,7 +165,9 @@ func NewSource(options ...Option) (*OcspSource, error) {
func WithIssuer(issuer *CertificateIssuer) Option { func WithIssuer(issuer *CertificateIssuer) Option {
return func(o *OcspSource) error { return func(o *OcspSource) error {
o.issuers = append(o.issuers, issuer) o.issuers = append(o.issuers, issuer)
logrus.Infof("add issuer %s as known issuer", issuer) logrus.Infof("add issuer %s as known issuer", issuer)
return nil return nil
} }
} }
@ -159,22 +176,27 @@ func WithIssuer(issuer *CertificateIssuer) Option {
func (o *OcspSource) Response(r *ocsp.Request) ([]byte, http.Header, error) { func (o *OcspSource) Response(r *ocsp.Request) ([]byte, http.Header, error) {
issuer, err := o.getIssuer(r.IssuerKeyHash, r.HashAlgorithm) issuer, err := o.getIssuer(r.IssuerKeyHash, r.HashAlgorithm)
if err != nil { 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)) logrus.Infof("received request for unknown issuer key hash %s", hex.EncodeToString(r.IssuerKeyHash))
issuer = o.getDefaultIssuer() issuer = o.getDefaultIssuer()
response, err := issuer.buildUnknownResponse(r.SerialNumber) response, err := issuer.buildUnknownResponse(r.SerialNumber)
if err != nil { 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 response, nil, nil
} }
return nil, nil, fmt.Errorf("cannot answer request: %w", err) return nil, nil, fmt.Errorf("cannot answer request: %w", err)
} }
response, err := issuer.LookupResponse(r.SerialNumber) response, err := issuer.LookupResponse(r.SerialNumber)
if err != nil { 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 response, nil, nil
} }
@ -185,11 +207,13 @@ func (o *OcspSource) getIssuer(keyHash []byte, algorithm crypto.Hash) (*Certific
if err != nil { if err != nil {
logrus.Errorf("error for issuer: %v", err) logrus.Errorf("error for issuer: %v", err)
} }
if matched { if matched {
return issuer, nil return issuer, nil
} }
} }
return nil, unknownIssuer
return nil, errUnknownIssuer
} }
// BackgroundTasks delegates background task handling to the CertificateIssuer instances of the OcspSource // 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 := sync.WaitGroup{}
wg.Add(len(o.issuers)) wg.Add(len(o.issuers))
logrus.Info("starting OCSP source background tasks") logrus.Info("starting OCSP source background tasks")
for _, issuer := range o.issuers { for _, issuer := range o.issuers {
issuer := issuer issuer := issuer
go func() { go func() {
issuer.backgroundTasks(ctx) issuer.backgroundTasks(ctx)
wg.Done() wg.Done()
}() }()
} }
wg.Wait() wg.Wait()
logrus.Info("stopped OCSP source background tasks") logrus.Info("stopped OCSP source background tasks")
} }

View 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
}