Initial implementation

- support multiple issuer certificates
- support separate responder keys and certificates
- support openssl index.txt format certificate databases
This commit is contained in:
Jan Dittberner 2022-03-06 14:40:46 +01:00 committed by Jan Dittberner
parent 446d4b8225
commit 01d8ca46c3
6 changed files with 1902 additions and 0 deletions

3
.gitignore vendored Normal file
View file

@ -0,0 +1,3 @@
/.idea/
/cacertocsp
/config.yaml

145
cmd/cacertocsp/main.go Normal file
View 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
View file

@ -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
)

1312
go.sum Normal file

File diff suppressed because it is too large Load diff

View 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")
}

View 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
}
}