Initial implementation
- support multiple issuer certificates - support separate responder keys and certificates - support openssl index.txt format certificate databases
This commit is contained in:
parent
446d4b8225
commit
01d8ca46c3
6 changed files with 1902 additions and 0 deletions
3
.gitignore
vendored
Normal file
3
.gitignore
vendored
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
/.idea/
|
||||||
|
/cacertocsp
|
||||||
|
/config.yaml
|
145
cmd/cacertocsp/main.go
Normal file
145
cmd/cacertocsp/main.go
Normal file
|
@ -0,0 +1,145 @@
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"crypto"
|
||||||
|
"crypto/x509"
|
||||||
|
"encoding/pem"
|
||||||
|
"flag"
|
||||||
|
"fmt"
|
||||||
|
"io/ioutil"
|
||||||
|
"net/http"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"git.cacert.org/cacert-goocsp/internal/ocspsource"
|
||||||
|
"github.com/cloudflare/cfssl/ocsp"
|
||||||
|
"github.com/knadh/koanf"
|
||||||
|
"github.com/knadh/koanf/parsers/yaml"
|
||||||
|
"github.com/knadh/koanf/providers/file"
|
||||||
|
"github.com/sirupsen/logrus"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
coIssuers = "issuers"
|
||||||
|
issuerCaCert = "caCertificate"
|
||||||
|
issuerReCert = "responderCertificate"
|
||||||
|
issuerReKey = "responderKey"
|
||||||
|
issuerCertList = "certificateList"
|
||||||
|
)
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
var serverAddr = flag.String("serverAddr", ":8080", "Server ip addr and port")
|
||||||
|
var config = koanf.New(".")
|
||||||
|
|
||||||
|
err := config.Load(file.Provider("config.yaml"), yaml.Parser())
|
||||||
|
if err != nil {
|
||||||
|
logrus.Panicf("could not load configuration: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
var opts []ocspsource.Option
|
||||||
|
|
||||||
|
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))
|
||||||
|
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...)
|
||||||
|
if err != nil {
|
||||||
|
logrus.Panicf("could not create OCSP source: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
http.Handle("/", withLogging(ocsp.NewResponder(cacertSource, nil).ServeHTTP))
|
||||||
|
|
||||||
|
server := &http.Server{
|
||||||
|
Addr: *serverAddr,
|
||||||
|
}
|
||||||
|
if err := server.ListenAndServe(); err != nil {
|
||||||
|
logrus.Panicf("could not start the server process: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func withLogging(next http.HandlerFunc) http.HandlerFunc {
|
||||||
|
return func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
start := time.Now()
|
||||||
|
next.ServeHTTP(w, r)
|
||||||
|
logrus.Infof("GET %s FROM %s in %dms", r.URL.Path, r.RemoteAddr, time.Since(start).Milliseconds())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func parseCertificate(certificateFile string) (*x509.Certificate, error) {
|
||||||
|
pemData, err := ioutil.ReadFile(certificateFile)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("could not read PEM data from %s: %w", certificateFile, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
block, _ := pem.Decode(pemData)
|
||||||
|
if block == nil {
|
||||||
|
return nil, fmt.Errorf("could not find PEM data in %s", certificateFile)
|
||||||
|
}
|
||||||
|
certificate, err := x509.ParseCertificate(block.Bytes)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("could not parse certificate in %s: %w", certificateFile, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return certificate, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func parsePrivateKey(keyFile string) (crypto.Signer, error) {
|
||||||
|
pemData, err := ioutil.ReadFile(keyFile)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("could not read PEM data from %s: %w", keyFile, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
block, _ := pem.Decode(pemData)
|
||||||
|
if block == nil {
|
||||||
|
return nil, fmt.Errorf("could not find PEM data in %s", keyFile)
|
||||||
|
}
|
||||||
|
|
||||||
|
switch block.Type {
|
||||||
|
case "PRIVATE KEY":
|
||||||
|
key, err := x509.ParsePKCS8PrivateKey(block.Bytes)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("no usable private key found in %s: %w", keyFile, err)
|
||||||
|
}
|
||||||
|
return key.(crypto.Signer), nil
|
||||||
|
case "RSA PRIVATE KEY":
|
||||||
|
rsaKey, err := x509.ParsePKCS1PrivateKey(block.Bytes)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("no usable private key found in %s: %w", keyFile, err)
|
||||||
|
}
|
||||||
|
return rsaKey, nil
|
||||||
|
default:
|
||||||
|
return nil, fmt.Errorf("unsupported PEM block type %s in %s", block.Type, keyFile)
|
||||||
|
}
|
||||||
|
}
|
92
go.mod
92
go.mod
|
@ -1,3 +1,95 @@
|
||||||
module git.cacert.org/cacert-goocsp
|
module git.cacert.org/cacert-goocsp
|
||||||
|
|
||||||
go 1.17
|
go 1.17
|
||||||
|
|
||||||
|
require (
|
||||||
|
github.com/cloudflare/cfssl v1.6.1
|
||||||
|
github.com/fsnotify/fsnotify v1.4.9
|
||||||
|
github.com/knadh/koanf v1.4.0
|
||||||
|
github.com/sirupsen/logrus v1.8.1
|
||||||
|
golang.org/x/crypto v0.0.0-20210506145944-38f3c27a63bf
|
||||||
|
)
|
||||||
|
|
||||||
|
require (
|
||||||
|
cloud.google.com/go v0.81.0 // indirect
|
||||||
|
github.com/beorn7/perks v1.0.1 // indirect
|
||||||
|
github.com/bgentry/speakeasy v0.1.0 // indirect
|
||||||
|
github.com/census-instrumentation/opencensus-proto v0.3.0 // indirect
|
||||||
|
github.com/cespare/xxhash/v2 v2.1.1 // indirect
|
||||||
|
github.com/cncf/udpa/go v0.0.0-20210322005330-6414d713912e // indirect
|
||||||
|
github.com/coreos/go-semver v0.3.0 // indirect
|
||||||
|
github.com/coreos/go-systemd/v22 v22.3.2 // indirect
|
||||||
|
github.com/cpuguy83/go-md2man/v2 v2.0.0 // indirect
|
||||||
|
github.com/dustin/go-humanize v1.0.0 // indirect
|
||||||
|
github.com/envoyproxy/go-control-plane v0.9.9-0.20210217033140-668b12f5399d // indirect
|
||||||
|
github.com/envoyproxy/protoc-gen-validate v0.6.1 // indirect
|
||||||
|
github.com/form3tech-oss/jwt-go v3.2.3+incompatible // indirect
|
||||||
|
github.com/fullstorydev/grpcurl v1.8.1 // indirect
|
||||||
|
github.com/gogo/protobuf v1.3.2 // indirect
|
||||||
|
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect
|
||||||
|
github.com/golang/mock v1.5.0 // indirect
|
||||||
|
github.com/golang/protobuf v1.5.2 // indirect
|
||||||
|
github.com/google/btree v1.0.1 // indirect
|
||||||
|
github.com/google/certificate-transparency-go v1.1.2-0.20210511102531-373a877eec92 // indirect
|
||||||
|
github.com/google/go-cmp v0.5.6 // indirect
|
||||||
|
github.com/google/uuid v1.2.0 // indirect
|
||||||
|
github.com/gorilla/websocket v1.4.2 // indirect
|
||||||
|
github.com/grpc-ecosystem/go-grpc-middleware v1.3.0 // indirect
|
||||||
|
github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0 // indirect
|
||||||
|
github.com/grpc-ecosystem/grpc-gateway v1.16.0 // indirect
|
||||||
|
github.com/inconshreveable/mousetrap v1.0.0 // indirect
|
||||||
|
github.com/jhump/protoreflect v1.8.2 // indirect
|
||||||
|
github.com/jmhodges/clock v0.0.0-20160418191101-880ee4c33548 // indirect
|
||||||
|
github.com/jmoiron/sqlx v1.3.3 // indirect
|
||||||
|
github.com/jonboulle/clockwork v0.2.2 // indirect
|
||||||
|
github.com/json-iterator/go v1.1.11 // indirect
|
||||||
|
github.com/kisielk/sqlstruct v0.0.0-20201105191214-5f3e10d3ab46 // indirect
|
||||||
|
github.com/mattn/go-runewidth v0.0.12 // indirect
|
||||||
|
github.com/matttproud/golang_protobuf_extensions v1.0.1 // indirect
|
||||||
|
github.com/mitchellh/copystructure v1.2.0 // indirect
|
||||||
|
github.com/mitchellh/mapstructure v1.4.1 // indirect
|
||||||
|
github.com/mitchellh/reflectwalk v1.0.2 // indirect
|
||||||
|
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
|
||||||
|
github.com/modern-go/reflect2 v1.0.1 // indirect
|
||||||
|
github.com/olekukonko/tablewriter v0.0.5 // indirect
|
||||||
|
github.com/prometheus/client_golang v1.10.0 // indirect
|
||||||
|
github.com/prometheus/client_model v0.2.0 // indirect
|
||||||
|
github.com/prometheus/common v0.24.0 // indirect
|
||||||
|
github.com/prometheus/procfs v0.6.0 // indirect
|
||||||
|
github.com/rivo/uniseg v0.2.0 // indirect
|
||||||
|
github.com/russross/blackfriday/v2 v2.1.0 // indirect
|
||||||
|
github.com/soheilhy/cmux v0.1.5 // indirect
|
||||||
|
github.com/spf13/cobra v1.1.3 // indirect
|
||||||
|
github.com/spf13/pflag v1.0.5 // indirect
|
||||||
|
github.com/tmc/grpc-websocket-proxy v0.0.0-20201229170055-e5319fda7802 // indirect
|
||||||
|
github.com/urfave/cli v1.22.5 // indirect
|
||||||
|
github.com/xiang90/probing v0.0.0-20190116061207-43a291ad63a2 // indirect
|
||||||
|
go.etcd.io/bbolt v1.3.5 // indirect
|
||||||
|
go.etcd.io/etcd/api/v3 v3.5.0-alpha.0 // indirect
|
||||||
|
go.etcd.io/etcd/client/v2 v2.305.0-alpha.0 // indirect
|
||||||
|
go.etcd.io/etcd/client/v3 v3.5.0-alpha.0 // indirect
|
||||||
|
go.etcd.io/etcd/etcdctl/v3 v3.5.0-alpha.0 // indirect
|
||||||
|
go.etcd.io/etcd/pkg/v3 v3.5.0-alpha.0 // indirect
|
||||||
|
go.etcd.io/etcd/raft/v3 v3.5.0-alpha.0 // indirect
|
||||||
|
go.etcd.io/etcd/server/v3 v3.5.0-alpha.0 // indirect
|
||||||
|
go.etcd.io/etcd/tests/v3 v3.5.0-alpha.0 // indirect
|
||||||
|
go.etcd.io/etcd/v3 v3.5.0-alpha.0 // indirect
|
||||||
|
go.uber.org/atomic v1.7.0 // indirect
|
||||||
|
go.uber.org/multierr v1.7.0 // indirect
|
||||||
|
go.uber.org/zap v1.16.0 // indirect
|
||||||
|
golang.org/x/mod v0.4.2 // indirect
|
||||||
|
golang.org/x/net v0.0.0-20210510120150-4163338589ed // indirect
|
||||||
|
golang.org/x/oauth2 v0.0.0-20210427180440-81ed05c6b58c // indirect
|
||||||
|
golang.org/x/sys v0.0.0-20210511113859-b0526f3d8744 // indirect
|
||||||
|
golang.org/x/text v0.3.6 // indirect
|
||||||
|
golang.org/x/time v0.0.0-20210220033141-f8bda1e9f3ba // indirect
|
||||||
|
golang.org/x/tools v0.1.0 // indirect
|
||||||
|
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 // indirect
|
||||||
|
google.golang.org/appengine v1.6.7 // indirect
|
||||||
|
google.golang.org/genproto v0.0.0-20210510173355-fb37daa5cd7a // indirect
|
||||||
|
google.golang.org/grpc v1.37.0 // indirect
|
||||||
|
google.golang.org/protobuf v1.26.0 // indirect
|
||||||
|
gopkg.in/cheggaaa/pb.v1 v1.0.28 // indirect
|
||||||
|
gopkg.in/yaml.v2 v2.4.0 // indirect
|
||||||
|
sigs.k8s.io/yaml v1.2.0 // indirect
|
||||||
|
)
|
||||||
|
|
140
internal/ocspsource/ocspsource.go
Normal file
140
internal/ocspsource/ocspsource.go
Normal file
|
@ -0,0 +1,140 @@
|
||||||
|
package ocspsource
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"crypto"
|
||||||
|
"crypto/x509"
|
||||||
|
"crypto/x509/pkix"
|
||||||
|
"encoding/asn1"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"math/big"
|
||||||
|
"net/http"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/sirupsen/logrus"
|
||||||
|
"golang.org/x/crypto/ocsp"
|
||||||
|
)
|
||||||
|
|
||||||
|
type CertificateIssuer struct {
|
||||||
|
responderCertificate *x509.Certificate
|
||||||
|
responderKey crypto.Signer
|
||||||
|
caCertificate *x509.Certificate
|
||||||
|
certDb *OpenSSLCertDB
|
||||||
|
certificateList []*x509.Certificate
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewIssuer(caCertificate, responderCertificate *x509.Certificate, responderKey crypto.Signer, certificateFile string) (*CertificateIssuer, error) {
|
||||||
|
certDb, err := NewCertDB(certificateFile)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("could not initialize certificate database: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return &CertificateIssuer{
|
||||||
|
caCertificate: caCertificate, responderCertificate: responderCertificate, responderKey: responderKey, certDb: certDb,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (i *CertificateIssuer) publicKeyMatches(requestHash []byte, algorithm crypto.Hash) (bool, error) {
|
||||||
|
var publicKeyInfo struct {
|
||||||
|
Algorithm pkix.AlgorithmIdentifier
|
||||||
|
PublicKey asn1.BitString
|
||||||
|
}
|
||||||
|
|
||||||
|
if _, err := asn1.Unmarshal(i.caCertificate.RawSubjectPublicKeyInfo, &publicKeyInfo); err != nil {
|
||||||
|
return false, fmt.Errorf("could not parse CA certificate public key info: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
h := algorithm.New()
|
||||||
|
if _, err := h.Write(publicKeyInfo.PublicKey.RightAlign()); err != nil {
|
||||||
|
return false, fmt.Errorf("could not update digest instance: %w", err)
|
||||||
|
}
|
||||||
|
issuerHash := h.Sum(nil)
|
||||||
|
|
||||||
|
return bytes.Compare(issuerHash, requestHash) == 0, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (i *CertificateIssuer) buildUnknownResponse(number *big.Int) ([]byte, error) {
|
||||||
|
template := &ocsp.Response{
|
||||||
|
SerialNumber: number,
|
||||||
|
Status: ocsp.Unknown,
|
||||||
|
}
|
||||||
|
return i.buildResponse(template)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (i *CertificateIssuer) buildResponse(template *ocsp.Response) ([]byte, error) {
|
||||||
|
template.ProducedAt = time.Now()
|
||||||
|
template.ThisUpdate = time.Now()
|
||||||
|
template.NextUpdate = time.Now().Add(time.Hour)
|
||||||
|
template.Certificate = i.responderCertificate
|
||||||
|
return ocsp.CreateResponse(i.caCertificate, i.responderCertificate, *template, i.responderKey)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (i *CertificateIssuer) String() string {
|
||||||
|
return i.caCertificate.Subject.String()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (i *CertificateIssuer) LookupResponse(serialNumber *big.Int) ([]byte, error) {
|
||||||
|
response := i.certDb.LookupResponseTemplate(serialNumber)
|
||||||
|
if response == nil {
|
||||||
|
return i.buildUnknownResponse(serialNumber)
|
||||||
|
}
|
||||||
|
return i.buildResponse(response)
|
||||||
|
}
|
||||||
|
|
||||||
|
type OcspSource struct {
|
||||||
|
issuers []*CertificateIssuer
|
||||||
|
}
|
||||||
|
|
||||||
|
type Option func(*OcspSource) error
|
||||||
|
|
||||||
|
func NewSource(options ...Option) (*OcspSource, error) {
|
||||||
|
source := &OcspSource{}
|
||||||
|
|
||||||
|
for _, option := range options {
|
||||||
|
err := option(source)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(source.issuers) == 0 {
|
||||||
|
return nil, errors.New("no issuers configured")
|
||||||
|
}
|
||||||
|
|
||||||
|
return source, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func WithIssuer(issuer *CertificateIssuer) Option {
|
||||||
|
return func(o *OcspSource) error {
|
||||||
|
o.issuers = append(o.issuers, issuer)
|
||||||
|
logrus.Infof("add issuer %s as known issuer", issuer)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (o *OcspSource) Response(r *ocsp.Request) ([]byte, http.Header, error) {
|
||||||
|
issuer, err := o.getIssuer(r.IssuerKeyHash, r.HashAlgorithm)
|
||||||
|
if err != nil {
|
||||||
|
return nil, nil, fmt.Errorf("cannot answer request: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
response, err := issuer.LookupResponse(r.SerialNumber)
|
||||||
|
if err != nil {
|
||||||
|
return nil, nil, fmt.Errorf("could not build OCSP response: %v", err)
|
||||||
|
}
|
||||||
|
return response, nil, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (o *OcspSource) getIssuer(keyHash []byte, algorithm crypto.Hash) (*CertificateIssuer, error) {
|
||||||
|
for _, issuer := range o.issuers {
|
||||||
|
matched, err := issuer.publicKeyMatches(keyHash, algorithm)
|
||||||
|
if err != nil {
|
||||||
|
logrus.Errorf("error for issuer: %v", err)
|
||||||
|
}
|
||||||
|
if matched {
|
||||||
|
return issuer, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil, errors.New("issuer key hash does not match any of the known issuers")
|
||||||
|
}
|
210
internal/ocspsource/opensslcertdb.go
Normal file
210
internal/ocspsource/opensslcertdb.go
Normal file
|
@ -0,0 +1,210 @@
|
||||||
|
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
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in a new issue