Implement golangci-lint suggestions
This commit is contained in:
parent
5e065c9692
commit
a8b2bec8f5
4 changed files with 462 additions and 314 deletions
|
@ -1,5 +1,6 @@
|
||||||
/*
|
/*
|
||||||
Copyright 2022 CAcert Inc.
|
Copyright 2022 CAcert Inc.
|
||||||
|
SPDX-License-Identifier: Apache-2.0
|
||||||
|
|
||||||
Licensed under the Apache License, Version 2.0 (the "License");
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
you may not use this file except in compliance with the License.
|
you may not use this file except in compliance with the License.
|
||||||
|
@ -31,12 +32,13 @@ import (
|
||||||
"syscall"
|
"syscall"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"git.cacert.org/cacert-goocsp/internal/ocspsource"
|
|
||||||
"github.com/cloudflare/cfssl/ocsp"
|
"github.com/cloudflare/cfssl/ocsp"
|
||||||
"github.com/knadh/koanf"
|
"github.com/knadh/koanf"
|
||||||
"github.com/knadh/koanf/parsers/yaml"
|
"github.com/knadh/koanf/parsers/yaml"
|
||||||
"github.com/knadh/koanf/providers/file"
|
"github.com/knadh/koanf/providers/file"
|
||||||
"github.com/sirupsen/logrus"
|
"github.com/sirupsen/logrus"
|
||||||
|
|
||||||
|
"git.cacert.org/cacert-goocsp/pkg/ocspsource"
|
||||||
)
|
)
|
||||||
|
|
||||||
/* constants for configuration keys */
|
/* constants for configuration keys */
|
||||||
|
@ -50,52 +52,22 @@ const (
|
||||||
)
|
)
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
var serverAddr = flag.String("serverAddr", ":8080", "Server ip addr and port")
|
var (
|
||||||
var config = koanf.New(".")
|
serverAddr = flag.String("serverAddr", ":8080", "Server ip addr and port")
|
||||||
|
config = koanf.New(".")
|
||||||
|
opts []ocspsource.Option
|
||||||
|
)
|
||||||
|
|
||||||
err := config.Load(file.Provider("config.yaml"), yaml.Parser())
|
err := config.Load(file.Provider("config.yaml"), yaml.Parser())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logrus.Panicf("could not load configuration: %v", err)
|
logrus.Panicf("could not load configuration: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
logrus.SetLevel(logrus.DebugLevel)
|
logrus.SetLevel(logrus.DebugLevel)
|
||||||
var opts []ocspsource.Option
|
|
||||||
|
|
||||||
issuerConfigs := config.Slices(coIssuers)
|
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))
|
opts = configureIssuers(issuerConfigs, opts)
|
||||||
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...)
|
cacertSource, err := ocspsource.NewSource(opts...)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -121,17 +93,78 @@ func main() {
|
||||||
if !errors.Is(err, http.ErrServerClosed) {
|
if !errors.Is(err, http.ErrServerClosed) {
|
||||||
logrus.Panicf("could not start the server process: %v", err)
|
logrus.Panicf("could not start the server process: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
logrus.Infof("server shutdown")
|
logrus.Infof("server shutdown")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func configureIssuers(issuerConfigs []*koanf.Koanf, opts []ocspsource.Option) []ocspsource.Option {
|
||||||
|
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))
|
||||||
|
}
|
||||||
|
|
||||||
|
return opts
|
||||||
|
}
|
||||||
|
|
||||||
// The setupCloseHandler takes care of OS signal handling
|
// The setupCloseHandler takes care of OS signal handling
|
||||||
func setupCloseHandler(ctx context.Context, server *http.Server) {
|
func setupCloseHandler(ctx context.Context, server *http.Server) {
|
||||||
c := make(chan os.Signal, 0)
|
c := make(chan os.Signal, 1)
|
||||||
|
|
||||||
signal.Notify(c, os.Interrupt, syscall.SIGTERM)
|
signal.Notify(c, os.Interrupt, syscall.SIGTERM)
|
||||||
|
|
||||||
go func(ctx context.Context) {
|
go func(ctx context.Context) {
|
||||||
<-c
|
<-c
|
||||||
logrus.Infof("program interrupted")
|
logrus.Infof("program interrupted")
|
||||||
|
|
||||||
err := server.Shutdown(ctx)
|
err := server.Shutdown(ctx)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logrus.Errorf("could not close server: %v", err)
|
logrus.Errorf("could not close server: %v", err)
|
||||||
|
@ -143,6 +176,7 @@ func setupCloseHandler(ctx context.Context, server *http.Server) {
|
||||||
func withLogging(next http.HandlerFunc) http.HandlerFunc {
|
func withLogging(next http.HandlerFunc) http.HandlerFunc {
|
||||||
return func(w http.ResponseWriter, r *http.Request) {
|
return func(w http.ResponseWriter, r *http.Request) {
|
||||||
start := time.Now()
|
start := time.Now()
|
||||||
|
|
||||||
next.ServeHTTP(w, r)
|
next.ServeHTTP(w, r)
|
||||||
logrus.Infof("GET %s FROM %s in %dms", r.URL.Path, r.RemoteAddr, time.Since(start).Milliseconds())
|
logrus.Infof("GET %s FROM %s in %dms", r.URL.Path, r.RemoteAddr, time.Since(start).Milliseconds())
|
||||||
}
|
}
|
||||||
|
@ -159,6 +193,7 @@ func parseCertificate(certificateFile string) (*x509.Certificate, error) {
|
||||||
if block == nil {
|
if block == nil {
|
||||||
return nil, fmt.Errorf("could not find PEM data in %s", certificateFile)
|
return nil, fmt.Errorf("could not find PEM data in %s", certificateFile)
|
||||||
}
|
}
|
||||||
|
|
||||||
certificate, err := x509.ParseCertificate(block.Bytes)
|
certificate, err := x509.ParseCertificate(block.Bytes)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("could not parse certificate in %s: %w", certificateFile, err)
|
return nil, fmt.Errorf("could not parse certificate in %s: %w", certificateFile, err)
|
||||||
|
@ -185,12 +220,19 @@ func parsePrivateKey(keyFile string) (crypto.Signer, error) {
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("no usable private key found in %s: %w", keyFile, err)
|
return nil, fmt.Errorf("no usable private key found in %s: %w", keyFile, err)
|
||||||
}
|
}
|
||||||
return key.(crypto.Signer), nil
|
|
||||||
|
signer, ok := key.(crypto.Signer)
|
||||||
|
if !ok {
|
||||||
|
return nil, errors.New("key cannot be used as signer")
|
||||||
|
}
|
||||||
|
|
||||||
|
return signer, nil
|
||||||
case "RSA PRIVATE KEY":
|
case "RSA PRIVATE KEY":
|
||||||
rsaKey, err := x509.ParsePKCS1PrivateKey(block.Bytes)
|
rsaKey, err := x509.ParsePKCS1PrivateKey(block.Bytes)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("no usable private key found in %s: %w", keyFile, err)
|
return nil, fmt.Errorf("no usable private key found in %s: %w", keyFile, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
return rsaKey, nil
|
return rsaKey, nil
|
||||||
default:
|
default:
|
||||||
return nil, fmt.Errorf("unsupported PEM block type %s in %s", block.Type, keyFile)
|
return nil, fmt.Errorf("unsupported PEM block type %s in %s", block.Type, keyFile)
|
||||||
|
|
|
@ -1,247 +0,0 @@
|
||||||
/*
|
|
||||||
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.
|
|
||||||
*/
|
|
||||||
|
|
||||||
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"
|
|
||||||
)
|
|
||||||
|
|
||||||
// OpenSSLCertDB implements the needed glue code to interface with the index.txt format of openssl ca
|
|
||||||
type OpenSSLCertDB struct {
|
|
||||||
fileName string
|
|
||||||
content map[string]*ocsp.Response
|
|
||||||
}
|
|
||||||
|
|
||||||
// NewCertDB creates a new certificate database for the given index.txt file
|
|
||||||
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
|
|
||||||
}
|
|
||||||
|
|
||||||
// The update method reads the content of index.txt and replaces the current content of the OpenSSLCertDB
|
|
||||||
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)
|
|
||||||
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
|
|
||||||
}
|
|
||||||
|
|
||||||
// LookupResponseTemplate retrieves an OCSP response template for the given certificate serial number.
|
|
||||||
func (o *OpenSSLCertDB) LookupResponseTemplate(number *big.Int) *ocsp.Response {
|
|
||||||
serial := number.Text(16)
|
|
||||||
if response, ok := o.content[serial]; ok {
|
|
||||||
return response
|
|
||||||
}
|
|
||||||
// RFC https://datatracker.ietf.org/doc/html/rfc6960#section-2.2 states that certificates we cannot answer for
|
|
||||||
// should be marked as good. This would allow a CRL as sole certificate source.
|
|
||||||
//
|
|
||||||
// We might change this to an unknown response when we have a more accurate certificate source like an API or an
|
|
||||||
// up-to-date openssl index.txt.
|
|
||||||
logrus.Debugf("received request for certificate %s that is not in our data source", serial)
|
|
||||||
response := &ocsp.Response{
|
|
||||||
Status: ocsp.Good,
|
|
||||||
SerialNumber: number,
|
|
||||||
}
|
|
||||||
return response
|
|
||||||
}
|
|
||||||
|
|
||||||
// WatchChanges creates file system monitoring for the index.txt file of the OpenSSLCertDB
|
|
||||||
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)
|
|
||||||
}
|
|
||||||
|
|
||||||
// The parseLine function parses a line of index.txt.
|
|
||||||
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{
|
|
||||||
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
|
|
||||||
}
|
|
||||||
|
|
||||||
// mapRevocationReason takes a OCSP revocation reason string and converts it to the numerical representation required
|
|
||||||
// for OCSP responses.
|
|
||||||
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
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,5 +1,6 @@
|
||||||
/*
|
/*
|
||||||
Copyright 2022 CAcert Inc.
|
Copyright 2022 CAcert Inc.
|
||||||
|
SPDX-License-Identifier: Apache-2.0
|
||||||
|
|
||||||
Licensed under the Apache License, Version 2.0 (the "License");
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
you may not use this file except in compliance with the License.
|
you may not use this file except in compliance with the License.
|
||||||
|
@ -35,7 +36,7 @@ import (
|
||||||
"golang.org/x/crypto/ocsp"
|
"golang.org/x/crypto/ocsp"
|
||||||
)
|
)
|
||||||
|
|
||||||
var unknownIssuer = errors.New("issuer key hash does not match any of the known issuers")
|
var errUnknownIssuer = errors.New("issuer key hash does not match any of the known issuers")
|
||||||
|
|
||||||
// CertificateIssuer is an abstraction for the OCSP responder that knows certificates of a specific CA
|
// CertificateIssuer is an abstraction for the OCSP responder that knows certificates of a specific CA
|
||||||
type CertificateIssuer struct {
|
type CertificateIssuer struct {
|
||||||
|
@ -46,7 +47,11 @@ type CertificateIssuer struct {
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewIssuer is used to construct a new CertificateIssuer instance
|
// NewIssuer is used to construct a new CertificateIssuer instance
|
||||||
func NewIssuer(caCertificate, responderCertificate *x509.Certificate, responderKey crypto.Signer, certificateFile string) (*CertificateIssuer, error) {
|
func NewIssuer(
|
||||||
|
caCertificate, responderCertificate *x509.Certificate,
|
||||||
|
responderKey crypto.Signer,
|
||||||
|
certificateFile string,
|
||||||
|
) (*CertificateIssuer, error) {
|
||||||
certDb, err := NewCertDB(certificateFile)
|
certDb, err := NewCertDB(certificateFile)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("could not initialize certificate database: %w", err)
|
return nil, fmt.Errorf("could not initialize certificate database: %w", err)
|
||||||
|
@ -73,6 +78,7 @@ func (i *CertificateIssuer) publicKeyMatches(requestHash []byte, algorithm crypt
|
||||||
if _, err := h.Write(publicKeyInfo.PublicKey.RightAlign()); err != nil {
|
if _, err := h.Write(publicKeyInfo.PublicKey.RightAlign()); err != nil {
|
||||||
return false, fmt.Errorf("could not update digest instance: %w", err)
|
return false, fmt.Errorf("could not update digest instance: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
issuerHash := h.Sum(nil)
|
issuerHash := h.Sum(nil)
|
||||||
|
|
||||||
return bytes.Equal(issuerHash, requestHash), nil
|
return bytes.Equal(issuerHash, requestHash), nil
|
||||||
|
@ -84,6 +90,7 @@ func (i *CertificateIssuer) buildUnknownResponse(number *big.Int) ([]byte, error
|
||||||
SerialNumber: number,
|
SerialNumber: number,
|
||||||
Status: ocsp.Unknown,
|
Status: ocsp.Unknown,
|
||||||
}
|
}
|
||||||
|
|
||||||
return i.buildResponse(template)
|
return i.buildResponse(template)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -93,7 +100,13 @@ func (i *CertificateIssuer) buildResponse(template *ocsp.Response) ([]byte, erro
|
||||||
template.ThisUpdate = time.Now()
|
template.ThisUpdate = time.Now()
|
||||||
template.NextUpdate = time.Now().Add(time.Hour)
|
template.NextUpdate = time.Now().Add(time.Hour)
|
||||||
template.Certificate = i.responderCertificate
|
template.Certificate = i.responderCertificate
|
||||||
return ocsp.CreateResponse(i.caCertificate, i.responderCertificate, *template, i.responderKey)
|
|
||||||
|
response, err := ocsp.CreateResponse(i.caCertificate, i.responderCertificate, *template, i.responderKey)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("could not create final OCSP response: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return response, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// String returns the name of the CertificateIssuer
|
// String returns the name of the CertificateIssuer
|
||||||
|
@ -107,12 +120,14 @@ func (i *CertificateIssuer) LookupResponse(serialNumber *big.Int) ([]byte, error
|
||||||
if response == nil {
|
if response == nil {
|
||||||
return i.buildUnknownResponse(serialNumber)
|
return i.buildUnknownResponse(serialNumber)
|
||||||
}
|
}
|
||||||
|
|
||||||
return i.buildResponse(response)
|
return i.buildResponse(response)
|
||||||
}
|
}
|
||||||
|
|
||||||
// The backgroundTasks of the issuer are started
|
// The backgroundTasks of the issuer are started
|
||||||
func (i *CertificateIssuer) backgroundTasks(ctx context.Context) {
|
func (i *CertificateIssuer) backgroundTasks(ctx context.Context) {
|
||||||
logrus.Infof("starting background tasks for ceritificate issuer %s", i)
|
logrus.Infof("starting background tasks for ceritificate issuer %s", i)
|
||||||
|
|
||||||
watcherCtx, cancel := context.WithCancel(ctx)
|
watcherCtx, cancel := context.WithCancel(ctx)
|
||||||
go i.certDb.WatchChanges(watcherCtx)
|
go i.certDb.WatchChanges(watcherCtx)
|
||||||
|
|
||||||
|
@ -150,7 +165,9 @@ func NewSource(options ...Option) (*OcspSource, error) {
|
||||||
func WithIssuer(issuer *CertificateIssuer) Option {
|
func WithIssuer(issuer *CertificateIssuer) Option {
|
||||||
return func(o *OcspSource) error {
|
return func(o *OcspSource) error {
|
||||||
o.issuers = append(o.issuers, issuer)
|
o.issuers = append(o.issuers, issuer)
|
||||||
|
|
||||||
logrus.Infof("add issuer %s as known issuer", issuer)
|
logrus.Infof("add issuer %s as known issuer", issuer)
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -159,22 +176,27 @@ func WithIssuer(issuer *CertificateIssuer) Option {
|
||||||
func (o *OcspSource) Response(r *ocsp.Request) ([]byte, http.Header, error) {
|
func (o *OcspSource) Response(r *ocsp.Request) ([]byte, http.Header, error) {
|
||||||
issuer, err := o.getIssuer(r.IssuerKeyHash, r.HashAlgorithm)
|
issuer, err := o.getIssuer(r.IssuerKeyHash, r.HashAlgorithm)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if errors.Is(err, unknownIssuer) {
|
if errors.Is(err, errUnknownIssuer) {
|
||||||
logrus.Infof("received request for unknown issuer key hash %s", hex.EncodeToString(r.IssuerKeyHash))
|
logrus.Infof("received request for unknown issuer key hash %s", hex.EncodeToString(r.IssuerKeyHash))
|
||||||
|
|
||||||
issuer = o.getDefaultIssuer()
|
issuer = o.getDefaultIssuer()
|
||||||
|
|
||||||
response, err := issuer.buildUnknownResponse(r.SerialNumber)
|
response, err := issuer.buildUnknownResponse(r.SerialNumber)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, nil, fmt.Errorf("could not build OCSP response: %v", err)
|
return nil, nil, fmt.Errorf("could not build OCSP response: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
return response, nil, nil
|
return response, nil, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil, nil, fmt.Errorf("cannot answer request: %w", err)
|
return nil, nil, fmt.Errorf("cannot answer request: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
response, err := issuer.LookupResponse(r.SerialNumber)
|
response, err := issuer.LookupResponse(r.SerialNumber)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, nil, fmt.Errorf("could not build OCSP response: %v", err)
|
return nil, nil, fmt.Errorf("could not build OCSP response: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
return response, nil, nil
|
return response, nil, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -185,11 +207,13 @@ func (o *OcspSource) getIssuer(keyHash []byte, algorithm crypto.Hash) (*Certific
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logrus.Errorf("error for issuer: %v", err)
|
logrus.Errorf("error for issuer: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
if matched {
|
if matched {
|
||||||
return issuer, nil
|
return issuer, nil
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return nil, unknownIssuer
|
|
||||||
|
return nil, errUnknownIssuer
|
||||||
}
|
}
|
||||||
|
|
||||||
// BackgroundTasks delegates background task handling to the CertificateIssuer instances of the OcspSource
|
// BackgroundTasks delegates background task handling to the CertificateIssuer instances of the OcspSource
|
||||||
|
@ -197,13 +221,16 @@ func (o *OcspSource) BackgroundTasks(ctx context.Context) {
|
||||||
wg := sync.WaitGroup{}
|
wg := sync.WaitGroup{}
|
||||||
wg.Add(len(o.issuers))
|
wg.Add(len(o.issuers))
|
||||||
logrus.Info("starting OCSP source background tasks")
|
logrus.Info("starting OCSP source background tasks")
|
||||||
|
|
||||||
for _, issuer := range o.issuers {
|
for _, issuer := range o.issuers {
|
||||||
issuer := issuer
|
issuer := issuer
|
||||||
|
|
||||||
go func() {
|
go func() {
|
||||||
issuer.backgroundTasks(ctx)
|
issuer.backgroundTasks(ctx)
|
||||||
wg.Done()
|
wg.Done()
|
||||||
}()
|
}()
|
||||||
}
|
}
|
||||||
|
|
||||||
wg.Wait()
|
wg.Wait()
|
||||||
logrus.Info("stopped OCSP source background tasks")
|
logrus.Info("stopped OCSP source background tasks")
|
||||||
}
|
}
|
326
pkg/ocspsource/opensslcertdb.go
Normal file
326
pkg/ocspsource/opensslcertdb.go
Normal file
|
@ -0,0 +1,326 @@
|
||||||
|
/*
|
||||||
|
Copyright 2022 CAcert Inc.
|
||||||
|
SPDX-License-Identifier: Apache-2.0
|
||||||
|
|
||||||
|
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.
|
||||||
|
*/
|
||||||
|
|
||||||
|
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"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
hexBase = 16
|
||||||
|
opensslUtcFormat = "20060102150405Z"
|
||||||
|
)
|
||||||
|
|
||||||
|
// field positions in index.txt
|
||||||
|
|
||||||
|
const (
|
||||||
|
idxStatus int = iota
|
||||||
|
idxExpiration
|
||||||
|
idxRevocation
|
||||||
|
idxSerial
|
||||||
|
idxFilename
|
||||||
|
idxSubjectDN
|
||||||
|
)
|
||||||
|
|
||||||
|
// OpenSSLCertDB implements the needed glue code to interface with the index.txt format of openssl ca
|
||||||
|
type OpenSSLCertDB struct {
|
||||||
|
fileName string
|
||||||
|
content map[string]*ocsp.Response
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewCertDB creates a new certificate database for the given index.txt file
|
||||||
|
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
|
||||||
|
}
|
||||||
|
|
||||||
|
// The update method reads the content of index.txt and replaces the current content of the OpenSSLCertDB
|
||||||
|
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)
|
||||||
|
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
|
||||||
|
}
|
||||||
|
|
||||||
|
// LookupResponseTemplate retrieves an OCSP response template for the given certificate serial number.
|
||||||
|
func (o *OpenSSLCertDB) LookupResponseTemplate(number *big.Int) *ocsp.Response {
|
||||||
|
serial := number.Text(hexBase)
|
||||||
|
if response, ok := o.content[serial]; ok {
|
||||||
|
return response
|
||||||
|
}
|
||||||
|
|
||||||
|
logrus.Debugf("received request for certificate %s that is not in our data source", serial)
|
||||||
|
|
||||||
|
// RFC https://datatracker.ietf.org/doc/html/rfc6960#section-2.2 states that certificates we cannot answer for
|
||||||
|
// should be marked as good. This would allow a CRL as sole certificate source.
|
||||||
|
//
|
||||||
|
// We might change this to an unknown response when we have a more accurate certificate source like an API or an
|
||||||
|
// up-to-date openssl index.txt.
|
||||||
|
response := &ocsp.Response{
|
||||||
|
Status: ocsp.Good,
|
||||||
|
SerialNumber: number,
|
||||||
|
}
|
||||||
|
|
||||||
|
return response
|
||||||
|
}
|
||||||
|
|
||||||
|
// WatchChanges creates file system monitoring for the index.txt file of the OpenSSLCertDB
|
||||||
|
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 o.watchIndexFile(watcher)
|
||||||
|
|
||||||
|
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 (o *OpenSSLCertDB) watchIndexFile(watcher *fsnotify.Watcher) {
|
||||||
|
for {
|
||||||
|
select {
|
||||||
|
case event, ok := <-watcher.Events:
|
||||||
|
if !ok {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if event.Op&fsnotify.Write == fsnotify.Write {
|
||||||
|
if event.Name == o.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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// The parseLine function parses a line of index.txt.
|
||||||
|
func parseLine(line string) (string, *ocsp.Response) {
|
||||||
|
const (
|
||||||
|
fieldSeparator = "\t"
|
||||||
|
)
|
||||||
|
|
||||||
|
if line == "" {
|
||||||
|
return "", nil
|
||||||
|
}
|
||||||
|
|
||||||
|
parts := strings.Split(line, fieldSeparator)
|
||||||
|
if len(parts) != idxSubjectDN+1 {
|
||||||
|
logrus.Warnf("found invalid line '%s'", line)
|
||||||
|
|
||||||
|
return "", nil
|
||||||
|
}
|
||||||
|
|
||||||
|
serial, serialNumber, err := parseSerialNumber(parts)
|
||||||
|
if err != nil {
|
||||||
|
logrus.Warn(err)
|
||||||
|
|
||||||
|
return "", nil
|
||||||
|
}
|
||||||
|
|
||||||
|
response := ocsp.Response{
|
||||||
|
SerialNumber: serialNumber,
|
||||||
|
}
|
||||||
|
|
||||||
|
mapStatusField(&response, parts)
|
||||||
|
|
||||||
|
if logrus.IsLevelEnabled(logrus.TraceLevel) {
|
||||||
|
traceParsedCertificateLine(parts, serial)
|
||||||
|
}
|
||||||
|
|
||||||
|
if response.Status == ocsp.Revoked {
|
||||||
|
err = handleRevoked(&response, parts, serial)
|
||||||
|
if err != nil {
|
||||||
|
logrus.Warn(err)
|
||||||
|
|
||||||
|
return "", nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return serialNumber.Text(hexBase), &response
|
||||||
|
}
|
||||||
|
|
||||||
|
func parseSerialNumber(parts []string) (string, *big.Int, error) {
|
||||||
|
serial := parts[idxSerial]
|
||||||
|
serialNumber := new(big.Int)
|
||||||
|
|
||||||
|
_, ok := serialNumber.SetString(serial, hexBase)
|
||||||
|
if !ok {
|
||||||
|
return "", nil, fmt.Errorf("could not parse %s as serial number", serial)
|
||||||
|
}
|
||||||
|
|
||||||
|
return serial, serialNumber, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func mapStatusField(response *ocsp.Response, parts []string) {
|
||||||
|
switch parts[idxStatus] {
|
||||||
|
case "V":
|
||||||
|
response.Status = ocsp.Good
|
||||||
|
case "R":
|
||||||
|
response.Status = ocsp.Revoked
|
||||||
|
response.RevocationReason = ocsp.Unspecified
|
||||||
|
default:
|
||||||
|
response.Status = ocsp.Unknown
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func traceParsedCertificateLine(parts []string, serial string) {
|
||||||
|
expirationTimeStamp, err := time.Parse(opensslUtcFormat, parts[idxExpiration])
|
||||||
|
if err != nil {
|
||||||
|
logrus.Warnf("could not parse %s as expiration timestamp for serial %s: %v", parts[idxExpiration], serial, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
logrus.Tracef(
|
||||||
|
"found certificate with serial number %s, expiration timestamp %s and filename '%s' for subject DN '%s'",
|
||||||
|
serial, expirationTimeStamp, parts[idxFilename], parts[idxSubjectDN],
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
func handleRevoked(response *ocsp.Response, parts []string, serial string) error {
|
||||||
|
const lenWithReason = 2
|
||||||
|
|
||||||
|
if parts[idxRevocation] == "" {
|
||||||
|
return fmt.Errorf(
|
||||||
|
"inconsistency detected certificate with serial %s is marked as revoked but has no revocation timestamp",
|
||||||
|
serial,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
revocation := []string{parts[idxRevocation]}
|
||||||
|
if strings.Contains(revocation[0], ",") {
|
||||||
|
revocation = strings.SplitN(revocation[0], ",", lenWithReason)
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(revocation) == lenWithReason {
|
||||||
|
response.RevocationReason = mapRevocationReason(revocation[1])
|
||||||
|
}
|
||||||
|
|
||||||
|
revocationTimeStamp, err := time.Parse(opensslUtcFormat, revocation[0])
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("could not parse %s as revocation timestamp for serial %s: %w", revocation[0], serial, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
response.RevokedAt = revocationTimeStamp.UTC()
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// mapRevocationReason takes a OCSP revocation reason string and converts it to the numerical representation required
|
||||||
|
// for OCSP responses.
|
||||||
|
func mapRevocationReason(reason string) int {
|
||||||
|
var revocationReasons = map[string]int{
|
||||||
|
"keyCompromise": ocsp.KeyCompromise,
|
||||||
|
"CACompromise": ocsp.CACompromise,
|
||||||
|
"affiliationChanged": ocsp.AffiliationChanged,
|
||||||
|
"superseded": ocsp.Superseded,
|
||||||
|
"cessationOfOperation": ocsp.CessationOfOperation,
|
||||||
|
"certificateHold": ocsp.CertificateHold,
|
||||||
|
"removeFromCRL": ocsp.RemoveFromCRL,
|
||||||
|
"privilegeWithdrawn": ocsp.PrivilegeWithdrawn,
|
||||||
|
"AACompromise": ocsp.AACompromise,
|
||||||
|
}
|
||||||
|
|
||||||
|
if reasonCode, exist := revocationReasons[reason]; exist {
|
||||||
|
return reasonCode
|
||||||
|
}
|
||||||
|
|
||||||
|
return ocsp.Unspecified
|
||||||
|
}
|
Loading…
Reference in a new issue