diff --git a/cmd/cacertocsp/main.go b/cmd/cacertocsp/main.go index 63f3005..a4dab74 100644 --- a/cmd/cacertocsp/main.go +++ b/cmd/cacertocsp/main.go @@ -32,6 +32,7 @@ import ( "syscall" "time" + "git.cacert.org/cacert-goocsp/pkg/crlcertdb" "github.com/knadh/koanf" "github.com/knadh/koanf/parsers/yaml" "github.com/knadh/koanf/providers/file" @@ -47,11 +48,12 @@ import ( /* constants for configuration keys */ const ( - coIssuers = "issuers" - issuerCaCert = "caCertificate" - issuerReCert = "responderCertificate" - issuerReKey = "responderKey" - issuerCertList = "certificateList" + coIssuers = "issuers" + issuerCaCert = "caCertificate" + issuerReCert = "responderCertificate" + issuerReKey = "responderKey" + issuerDbType = "dbType" + issuerDbFile = "dbFile" ) func main() { @@ -97,11 +99,18 @@ func main() { } } +var ErrUnknownDBType = errors.New("unknown certificate db type") + +const ( + dbTypeCRL = "crl" + dbTypeOpenSSL = "openssl" +) + func configureIssuers(ctx context.Context, issuerConfigs []*koanf.Koanf, opts []ocspsource.Option) []ocspsource.Option { for number, issuerConfig := range issuerConfigs { hasErrors := false - for _, item := range []string{issuerCaCert, issuerReCert, issuerReKey, issuerCertList} { + for _, item := range []string{issuerCaCert, issuerReCert, issuerReKey, issuerDbType, issuerDbFile} { if v := issuerConfig.String(item); v == "" { logrus.Warnf("%s parameter for issuers entry %d is missing", item, number) @@ -136,7 +145,17 @@ func configureIssuers(ctx context.Context, issuerConfigs []*koanf.Koanf, opts [] continue } - certDb, err := opensslcertdb.NewCertDB(ctx, issuerConfig.String(issuerCertList)) + var certDb ocspsource.CertificateDatabase + + switch issuerConfig.String(issuerDbType) { + case dbTypeOpenSSL: + certDb, err = opensslcertdb.NewCertDB(ctx, issuerConfig.String(issuerDbFile)) + case dbTypeCRL: + certDb, err = crlcertdb.NewCertDB(ctx, issuerConfig.String(issuerDbFile)) + default: + err = ErrUnknownDBType + } + if err != nil { logrus.Errorf("could not create certificate db %d: %v", number, err) diff --git a/pkg/crlcertdb/crlcertdb.go b/pkg/crlcertdb/crlcertdb.go new file mode 100644 index 0000000..abd9760 --- /dev/null +++ b/pkg/crlcertdb/crlcertdb.go @@ -0,0 +1,150 @@ +/* +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 crlcertdb + +import ( + "context" + "crypto/x509" + "fmt" + "io/ioutil" + "math/big" + "path/filepath" + + "git.cacert.org/cacert-goocsp/pkg/filewatcher" + "git.cacert.org/cacert-goocsp/pkg/ocsp" + "git.cacert.org/cacert-goocsp/pkg/ocspsource" + "github.com/fsnotify/fsnotify" + "github.com/sirupsen/logrus" +) + +type CRLCertDB struct { + crlPath string + content map[string]*ocsp.Response +} + +func NewCertDB(ctx context.Context, crlPath string) (*CRLCertDB, error) { + absFile, err := filepath.Abs(crlPath) + if err != nil { + return nil, fmt.Errorf("cold not determine absolute file name of %s: %w", crlPath, err) + } + + certDb := &CRLCertDB{crlPath: 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) + filewatcher.Watch(watcherCtx, certDb.crlPath, certDb.watchCRLFile) + + <-ctx.Done() + cancel() + }(ctx) + + return certDb, nil +} + +func (d *CRLCertDB) update() error { + crlBytes, err := ioutil.ReadFile(d.crlPath) + if err != nil { + return fmt.Errorf("could not read CRL from %s: %w", d.crlPath, err) + } + + crl, err := x509.ParseCRL(crlBytes) + if err != nil { + return fmt.Errorf("could not parse CRL from %s: %w", d.crlPath, err) + } + + var update *ocspsource.CertificateUpdate + + for _, entry := range crl.TBSCertList.RevokedCertificates { + update = &ocspsource.CertificateUpdate{ + Serial: entry.SerialNumber, + Status: ocsp.Revoked, + RevokedAt: entry.RevocationTime, + RevocationReason: ocsp.Unspecified, + } + + d.UpdateCertificate(update) + } + + logrus.Infof( + "parsed CRL '%s', found information for %d certificates", + d.crlPath, + len(crl.TBSCertList.RevokedCertificates), + ) + + return nil +} + +func (d *CRLCertDB) UpdateCertificate(update *ocspsource.CertificateUpdate) { + d.content[update.Serial.Text(16)] = &ocsp.Response{ + Status: update.Status, + SerialNumber: update.Serial, + RevokedAt: update.RevokedAt, + RevocationReason: update.RevocationReason, + } +} + +func (d *CRLCertDB) LookupResponseTemplate(number *big.Int) *ocsp.Response { + serial := number.Text(16) + if response, ok := d.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. + response := &ocsp.Response{ + Status: ocsp.Good, + SerialNumber: number, + } + + return response +} + +func (d *CRLCertDB) watchCRLFile(watcher *fsnotify.Watcher) { + for { + select { + case event, ok := <-watcher.Events: + if !ok { + return + } + + if event.Op&fsnotify.Write == fsnotify.Write { + if event.Name == d.crlPath { + logrus.Infof("modified: %s", event.Name) + + err := d.update() + if err != nil { + logrus.Error(err) + } + } + } + case err, ok := <-watcher.Errors: + if !ok { + return + } + + logrus.Errorf("error from watcher: %w", err) + } + } +} diff --git a/pkg/filewatcher/filewatcher.go b/pkg/filewatcher/filewatcher.go new file mode 100644 index 0000000..3e42ca7 --- /dev/null +++ b/pkg/filewatcher/filewatcher.go @@ -0,0 +1,35 @@ +package filewatcher + +import ( + "context" + "path" + + "github.com/fsnotify/fsnotify" + "github.com/sirupsen/logrus" +) + +// Watch creates file system monitoring for the given file and runs the callback on changes. +func Watch(ctx context.Context, filepath string, callback func(watcher *fsnotify.Watcher)) { + 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", filepath) + }(watcher) + + go callback(watcher) + + err = watcher.Add(path.Dir(filepath)) + if err != nil { + logrus.Fatalf("could not watch %s: %v", filepath, err) + } + + logrus.Infof("watching for changes on %s", filepath) + + <-ctx.Done() + logrus.Infof("ending background tasks for %s", filepath) +} diff --git a/pkg/ocsp/ocsp.go b/pkg/ocsp/ocsp.go index 8afebb9..53e03eb 100644 --- a/pkg/ocsp/ocsp.go +++ b/pkg/ocsp/ocsp.go @@ -442,7 +442,7 @@ type Response struct { // 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} +// SigRequiredErrorResponse = []byte{0x30, 0x03, 0x0A, 0x01, 0x05} // UnauthorizedErrorResponse = []byte{0x30, 0x03, 0x0A, 0x01, 0x06} // ) diff --git a/pkg/opensslcertdb/opensslcertdb.go b/pkg/opensslcertdb/opensslcertdb.go index 697b947..74b5aa9 100644 --- a/pkg/opensslcertdb/opensslcertdb.go +++ b/pkg/opensslcertdb/opensslcertdb.go @@ -25,11 +25,11 @@ import ( "io" "math/big" "os" - "path" "path/filepath" "strings" "time" + "git.cacert.org/cacert-goocsp/pkg/filewatcher" "github.com/fsnotify/fsnotify" "github.com/sirupsen/logrus" @@ -85,7 +85,7 @@ func NewCertDB(ctx context.Context, fileName string) (*OpenSSLCertDB, error) { go func(ctx context.Context) { watcherCtx, cancel := context.WithCancel(ctx) - certDb.WatchChanges(watcherCtx) + filewatcher.Watch(watcherCtx, certDb.fileName, certDb.watchIndexFile) <-ctx.Done() cancel() @@ -158,32 +158,6 @@ func (o *OpenSSLCertDB) LookupResponseTemplate(number *big.Int) *ocsp.Response { 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 {