2022-03-06 15:51:09 +00:00
|
|
|
/*
|
|
|
|
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.
|
|
|
|
*/
|
|
|
|
|
2022-03-06 13:40:46 +00:00
|
|
|
package ocspsource
|
|
|
|
|
|
|
|
import (
|
|
|
|
"bufio"
|
2022-03-06 15:18:04 +00:00
|
|
|
"context"
|
2022-03-06 13:40:46 +00:00
|
|
|
"errors"
|
|
|
|
"fmt"
|
|
|
|
"io"
|
|
|
|
"math/big"
|
|
|
|
"os"
|
2022-03-06 15:18:04 +00:00
|
|
|
"path"
|
2022-03-06 13:40:46 +00:00
|
|
|
"path/filepath"
|
|
|
|
"strings"
|
|
|
|
"time"
|
|
|
|
|
|
|
|
"github.com/fsnotify/fsnotify"
|
|
|
|
"github.com/sirupsen/logrus"
|
|
|
|
"golang.org/x/crypto/ocsp"
|
|
|
|
)
|
|
|
|
|
2022-03-06 15:51:09 +00:00
|
|
|
// OpenSSLCertDB implements the needed glue code to interface with the index.txt format of openssl ca
|
2022-03-06 13:40:46 +00:00
|
|
|
type OpenSSLCertDB struct {
|
|
|
|
fileName string
|
|
|
|
content map[string]*ocsp.Response
|
|
|
|
}
|
|
|
|
|
2022-03-06 15:51:09 +00:00
|
|
|
// NewCertDB creates a new certificate database for the given index.txt file
|
2022-03-06 13:40:46 +00:00
|
|
|
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)
|
|
|
|
}
|
|
|
|
|
2022-03-06 15:18:04 +00:00
|
|
|
certDb := &OpenSSLCertDB{fileName: absFile}
|
2022-03-06 13:40:46 +00:00
|
|
|
|
|
|
|
err = certDb.update()
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
|
|
|
|
return certDb, nil
|
|
|
|
}
|
|
|
|
|
2022-03-06 15:51:09 +00:00
|
|
|
// The update method reads the content of index.txt and replaces the current content of the OpenSSLCertDB
|
2022-03-06 13:40:46 +00:00
|
|
|
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)
|
|
|
|
|
2022-03-06 15:52:40 +00:00
|
|
|
newContent := make(map[string]*ocsp.Response)
|
2022-03-06 13:40:46 +00:00
|
|
|
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
|
|
|
|
}
|
|
|
|
|
2022-03-06 15:51:09 +00:00
|
|
|
// LookupResponseTemplate retrieves an OCSP response template for the given certificate serial number.
|
2022-03-06 13:40:46 +00:00
|
|
|
func (o *OpenSSLCertDB) LookupResponseTemplate(number *big.Int) *ocsp.Response {
|
|
|
|
serial := number.Text(16)
|
|
|
|
if response, ok := o.content[serial]; ok {
|
|
|
|
return response
|
|
|
|
}
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
2022-03-06 15:51:09 +00:00
|
|
|
// WatchChanges creates file system monitoring for the index.txt file of the OpenSSLCertDB
|
2022-03-06 15:18:04 +00:00
|
|
|
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)
|
|
|
|
}
|
|
|
|
|
2022-03-06 15:51:09 +00:00
|
|
|
// The parseLine function parses a line of index.txt.
|
2022-03-06 13:40:46 +00:00
|
|
|
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
|
|
|
|
}
|
|
|
|
|
2022-03-06 15:51:09 +00:00
|
|
|
// mapRevocationReason takes a OCSP revocation reason string and converts it to the numerical representation required
|
|
|
|
// for OCSP responses.
|
2022-03-06 13:40:46 +00:00
|
|
|
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
|
|
|
|
}
|
|
|
|
}
|