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" ) type OpenSSLCertDB struct { fileName string content map[string]*ocsp.Response } 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 } 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, 0) 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 } func (o *OpenSSLCertDB) LookupResponseTemplate(number *big.Int) *ocsp.Response { serial := number.Text(16) if response, ok := o.content[serial]; ok { return response } return nil } 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) } 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{ Status: 0, 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 } 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 } }