goocsp/internal/ocspsource/opensslcertdb.go
Jan Dittberner 01d8ca46c3 Initial implementation
- support multiple issuer certificates
- support separate responder keys and certificates
- support openssl index.txt format certificate databases
2022-03-06 14:41:53 +01:00

210 lines
4.4 KiB
Go

package ocspsource
import (
"bufio"
"errors"
"fmt"
"io"
"math/big"
"os"
"path/filepath"
"strings"
"time"
"github.com/fsnotify/fsnotify"
"github.com/sirupsen/logrus"
"golang.org/x/crypto/ocsp"
)
type OpenSSLCertDB struct {
fileName string
watcher *fsnotify.Watcher
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)
}
watcher, err := fsnotify.NewWatcher()
if err != nil {
logrus.Fatalf("could not create file watcher: %v", err)
}
certDb := &OpenSSLCertDB{fileName: absFile, watcher: watcher}
go func() {
for {
select {
case event, ok := <-watcher.Events:
if !ok {
return
}
if event.Op&fsnotify.Write == fsnotify.Write {
logrus.Infof("modified: %s", event.Name)
err := certDb.update()
if err != nil {
logrus.Error(err)
}
}
case err, ok := <-watcher.Errors:
if !ok {
return
}
logrus.Errorf("error from watcher: %v", err)
}
}
}()
err = watcher.Add(absFile)
if err != nil {
logrus.Fatalf("could not watch %s: %v", absFile, err)
}
err = certDb.update()
if err != nil {
return nil, err
}
return certDb, nil
}
func (o *OpenSSLCertDB) Close() {
err := o.watcher.Close()
if err != nil {
logrus.Errorf("could not close file watcher for %s: %v", o.fileName, err)
return
}
}
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 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
}
}