Implement CRL and Health response handling

- add callback support to client and handler
- implement support for updating the CA certificates and profiles from
  health data of the signer
- implement CRL retrieval from the signer including delta CRL support
- implement error response handling
- add configurable start and interval timers for health and CRL data
This commit is contained in:
Jan Dittberner 2022-11-30 18:56:57 +01:00
parent 0e32f7fd16
commit da17fb69d7
7 changed files with 394 additions and 43 deletions

2
.gitignore vendored
View file

@ -1,5 +1,7 @@
*Pty
/.idea/
/config.yaml
/crls/
/dist/
/signerclient
go.work

View file

@ -77,13 +77,14 @@ func main() {
}
commands := make(chan *protocol.Command)
callbacks := make(chan interface{}, client.CallBackBufferSize)
clientHandler, err := handler.New(clientConfig, logger, commands)
clientHandler, err := handler.New(clientConfig, logger, commands, callbacks)
if err != nil {
logger.WithError(err).Fatal("could not setup client handler")
}
signerClient, err := client.New(clientConfig, logger, clientHandler, commands)
signerClient, err := client.New(clientConfig, logger, clientHandler, commands, callbacks)
if err != nil {
logger.WithError(err).Fatal("could not setup client")
}

6
go.mod
View file

@ -1,9 +1,10 @@
module git.cacert.org/cacert-gosignerclient
go 1.17
go 1.19
require (
git.cacert.org/cacert-gosigner v0.0.0-20221129191234-c92b0455db62
git.cacert.org/cacert-gosigner v0.0.0-20221130175146-fffc65a540d5
github.com/balacode/go-delta v0.1.0
github.com/shamaton/msgpackgen v0.3.0
github.com/sirupsen/logrus v1.9.0
github.com/tarm/serial v0.0.0-20180830185346-98f6abe2eb07
@ -11,6 +12,7 @@ require (
)
require (
github.com/balacode/zr v1.0.0 // indirect
github.com/dave/jennifer v1.4.1 // indirect
github.com/google/uuid v1.3.0 // indirect
github.com/justincpresley/go-cobs v1.2.0 // indirect

17
go.sum
View file

@ -1,6 +1,9 @@
git.cacert.org/cacert-gosigner v0.0.0-20221129191234-c92b0455db62 h1:MKpVPTm+rOjVK1/XtEaeGvBp6hQj4FlWnuKl66xK5ls=
git.cacert.org/cacert-gosigner v0.0.0-20221129191234-c92b0455db62/go.mod h1:yFBRxW6BwOyiXGtHQH/Vpro4Dxd0oIl3tCIWg99nYGE=
github.com/ThalesIgnite/crypto11 v1.2.5/go.mod h1:ILDKtnCKiQ7zRoNxcp36Y1ZR8LBPmR2E23+wTQe/MlE=
git.cacert.org/cacert-gosigner v0.0.0-20221130175146-fffc65a540d5 h1:ot7ENgYQj4xcqodTe1V2aIjvGLn+NVVhPu9+6XMuMTk=
git.cacert.org/cacert-gosigner v0.0.0-20221130175146-fffc65a540d5/go.mod h1:mb8oBdxQ26GI3xT4b8B7hXYWGED9vvjPGxehmbicyc4=
github.com/balacode/go-delta v0.1.0 h1:pwz4CMn06P2bIaIfAx3GSabMPwJp/Ww4if+7SgPYa3I=
github.com/balacode/go-delta v0.1.0/go.mod h1:wLNrwTI3lHbPBvnLzqbHmA7HVVlm1u22XLvhbeA6t3o=
github.com/balacode/zr v1.0.0 h1:MCupkEoXvrnCljc4KddiDOhR04ZLUAACgtKuo3o+9vc=
github.com/balacode/zr v1.0.0/go.mod h1:pLeSAL3DhZ9L0JuiRkUtIX3mLOCtzBLnDhfmykbSmkE=
github.com/dave/jennifer v1.4.1 h1:XyqG6cn5RQsTj3qlWQTKlRGAyrTcsk1kUmWdZBzRjDw=
github.com/dave/jennifer v1.4.1/go.mod h1:7jEdnm+qBcxl8PC0zyp7vxcpSRnzXSt9r39tpTVGlwA=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
@ -10,10 +13,6 @@ github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I=
github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/justincpresley/go-cobs v1.2.0 h1:dyWszWzXObEv8sxVMJTAIo9XT7HEM10vkAOZq2eVEsQ=
github.com/justincpresley/go-cobs v1.2.0/go.mod h1:L0d+EbGirv6IzsXNzwULduI2/z3ijkkAmsAuPMpLfqA=
github.com/miekg/pkcs11 v1.0.3-0.20190429190417-a667d056470f/go.mod h1:XsNlhZGX73bx86s2hdc/FuaLm2CPZJemRLMA+WTFxgs=
github.com/miekg/pkcs11 v1.1.1/go.mod h1:XsNlhZGX73bx86s2hdc/FuaLm2CPZJemRLMA+WTFxgs=
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/shamaton/msgpack/v2 v2.1.0 h1:9jJ2eGZw2Wa9KExPX3KaDDckVjgr4zhXGFCfWagUWqg=
@ -24,18 +23,14 @@ github.com/sirupsen/logrus v1.9.0 h1:trlNQbNUG3OdDrDil03MCb1H2o9nJ1x4/5LYw7byDE0
github.com/sirupsen/logrus v1.9.0/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.8.0 h1:pSgiaMZlXftHpm5L7V1+rVB+AZJydKsMxsQBIJw4PKk=
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
github.com/tarm/serial v0.0.0-20180830185346-98f6abe2eb07 h1:UyzmZLoiDWMRywV4DUYb9Fbt8uiOSooupjTq10vpvnU=
github.com/tarm/serial v0.0.0-20180830185346-98f6abe2eb07/go.mod h1:kDXzergiv9cbyO7IOYJZWg1U88JhDg3PB6klq9Hg2pA=
github.com/thales-e-security/pool v0.0.2/go.mod h1:qtpMm2+thHtqhLzTwgDBj/OuNnMpupY8mv0Phz0gjhU=
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8 h1:0A+M6Uqn+Eje4kHMK80dtF3JCXC4ykBgQG4Fe06QRhQ=
golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/term v0.0.0-20220411215600-e5f449aeb171/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=

View file

@ -19,9 +19,15 @@ package client
import (
"context"
"crypto/x509"
"fmt"
"math/big"
"os"
"path"
"sync"
"time"
"github.com/balacode/go-delta"
"github.com/sirupsen/logrus"
"github.com/tarm/serial"
@ -30,6 +36,25 @@ import (
"git.cacert.org/cacert-gosignerclient/internal/config"
)
const CallBackBufferSize = 50
const (
worldReadableDirPerm = 0o755
worldReadableFilePerm = 0o644
)
type Profile struct {
Name string
UseFor string
}
type SignerInfo struct {
SignerHealth bool
SignerVersion string
CACertificates []string
UsableProfiles map[string][]Profile
}
type Client struct {
port *serial.Port
logger *logrus.Logger
@ -39,6 +64,10 @@ type Client struct {
commands chan *protocol.Command
handler protocol.ClientHandler
config *config.ClientConfig
signerInfo *SignerInfo
callback chan interface{}
sync.Mutex
lastKnownCRLS map[string]*big.Int
}
func (c *Client) Run(ctx context.Context) error {
@ -97,6 +126,11 @@ func (c *Client) setupConnection(serialConfig *serial.Config) error {
c.port = s
err = c.port.Flush()
if err != nil {
c.logger.WithError(err).Warn("could not flush buffers of port: %w", err)
}
return nil
}
@ -116,11 +150,40 @@ func (c *Client) Close() error {
func (c *Client) commandLoop(ctx context.Context) {
healthTimer := time.NewTimer(c.config.HealthStart)
fetchCRLTimer := time.NewTimer(c.config.FetchCRLStart)
for {
select {
case <-ctx.Done():
return
case callbackData := <-c.callback:
err := c.handleCallback(callbackData)
if err != nil {
c.logger.WithError(err).Error("callback handling failed")
}
case <-fetchCRLTimer.C:
for _, crlInfo := range c.buildCRLInfo() {
announce, err := messages.BuildCommandAnnounce(messages.CmdFetchCRL)
if err != nil {
c.logger.WithError(err).Error("could not build fetch CRL command announce")
}
var lastKnown []byte
if crlInfo.LastKnown != nil {
lastKnown = crlInfo.LastKnown.Bytes()
}
c.commands <- &protocol.Command{
Announce: announce,
Command: &messages.FetchCRLCommand{
IssuerID: crlInfo.Name,
LastKnownID: lastKnown,
},
}
}
fetchCRLTimer.Reset(c.config.FetchCRLInterval)
case <-healthTimer.C:
announce, err := messages.BuildCommandAnnounce(messages.CmdHealth)
if err != nil {
@ -137,11 +200,189 @@ func (c *Client) commandLoop(ctx context.Context) {
}
}
func (c *Client) handleCallback(data interface{}) error {
switch d := data.(type) {
case SignerInfo:
c.updateSignerInfo(d)
case *messages.FetchCRLResponse:
c.updateCRL(d)
default:
return fmt.Errorf("unknown callback data of type %T", data)
}
return nil
}
func (c *Client) updateSignerInfo(signerInfo SignerInfo) {
c.Lock()
defer c.Unlock()
c.logger.Debug("update signer info")
c.signerInfo = &signerInfo
}
type CRLInfo struct {
Name string
LastKnown *big.Int
}
func (c *Client) buildCRLInfo() []CRLInfo {
c.Lock()
defer c.Unlock()
if c.signerInfo == nil {
c.logger.Warn("no signer info available")
return nil
}
infos := make([]CRLInfo, len(c.signerInfo.CACertificates))
for i, caName := range c.signerInfo.CACertificates {
lastKnown := c.lastKnownCRL(caName)
infos[i] = CRLInfo{Name: caName, LastKnown: lastKnown}
}
return infos
}
func (c *Client) lastKnownCRL(caName string) *big.Int {
crlFileName := c.buildCRLFileName(caName)
_, err := os.Stat(crlFileName)
if err != nil {
c.logger.WithField("crl", crlFileName).Debug("CRL file does not exist")
delete(c.lastKnownCRLS, caName)
return nil
}
lastKnown, ok := c.lastKnownCRLS[caName]
if !ok {
derData, err := os.ReadFile(crlFileName)
if err != nil {
c.logger.WithError(err).WithField("crl", crlFileName).Error("could not read CRL data")
return nil
}
crl, err := x509.ParseRevocationList(derData)
if err != nil {
c.logger.WithError(err).WithField("crl", crlFileName).Error("could not parse CRL data")
return nil
}
lastKnown = crl.Number
c.lastKnownCRLS[caName] = lastKnown
}
return lastKnown
}
func (c *Client) buildCRLFileName(caName string) string {
return path.Join(c.config.CRLDirectory, fmt.Sprintf("%s.crl", caName))
}
func (c *Client) updateCRL(d *messages.FetchCRLResponse) {
var (
der []byte
crlNumber *big.Int
)
if d.UnChanged {
c.logger.WithField("issuer", d.IssuerID).Debug("CRL did not change")
return
}
if !d.IsDelta {
der = d.CRLData
list, err := x509.ParseRevocationList(der)
if err != nil {
c.logger.WithError(err).Error("CRL from signer could not be parsed")
return
}
crlNumber = list.Number
} else {
crlFileName := c.buildCRLFileName(d.IssuerID)
der, err := c.patchCRL(crlFileName, d.CRLData)
if err != nil {
c.logger.WithError(err).Error("CRL patching failed")
delete(c.lastKnownCRLS, d.IssuerID)
return
}
list, err := x509.ParseRevocationList(der)
if err != nil {
c.logger.WithError(err).Error("could not parse patched CRL")
delete(c.lastKnownCRLS, d.IssuerID)
return
}
crlNumber = list.Number
}
if err := c.writeCRL(d.IssuerID, der); err != nil {
c.logger.WithError(err).Error("could not store CRL")
delete(c.lastKnownCRLS, d.IssuerID)
return
}
c.lastKnownCRLS[d.IssuerID] = crlNumber
}
func (c *Client) writeCRL(caName string, crlBytes []byte) error {
if err := os.MkdirAll(c.config.CRLDirectory, worldReadableDirPerm); err != nil {
return fmt.Errorf("could not create CRL directory %s: %w", c.config.CRLDirectory, err)
}
if err := os.WriteFile(c.buildCRLFileName(caName), crlBytes, worldReadableFilePerm); err != nil {
c.logger.WithError(err).Error("could not write CRL file")
}
return nil
}
func (c *Client) patchCRL(crlFileName string, diff []byte) ([]byte, error) {
original, err := os.ReadFile(crlFileName)
if err != nil {
return nil, fmt.Errorf("could not read existing CRL %s: %w", crlFileName, err)
}
patch, err := delta.Load(diff)
if err != nil {
return nil, fmt.Errorf("could not parse CRL delta: %w", err)
}
der, err := patch.Apply(original)
if err != nil {
return nil, fmt.Errorf("could not apply CRL delta: %w", err)
}
return der, nil
}
func New(
cfg *config.ClientConfig,
logger *logrus.Logger,
handler protocol.ClientHandler,
commands chan *protocol.Command,
callback chan interface{},
) (*Client, error) {
client := &Client{
logger: logger,
@ -151,6 +392,8 @@ func New(
commands: commands,
handler: handler,
config: cfg,
callback: callback,
lastKnownCRLS: make(map[string]*big.Int),
}
err := client.setupConnection(&serial.Config{

View file

@ -29,9 +29,12 @@ import (
const (
defaultSerialTimeout = 5 * time.Second
defaultHealthInterval = 30 * time.Second
defaultHealthStart = 5 * time.Second
defaultHealthStart = 2 * time.Second
defaultFetchCRLInterval = 5 * time.Minute
defaultFetchCRLStart = 5 * time.Second
defaultResponseAnnounceTimeout = 30 * time.Second
defaultResponseDataTimeout = 2 * time.Second
defaultCRLDirectory = "crls"
)
type SettingsError struct {
@ -52,8 +55,11 @@ type ClientConfig struct {
Serial Serial `yaml:"serial"`
HealthInterval time.Duration `yaml:"health-interval"`
HealthStart time.Duration `yaml:"health-start"`
FetchCRLStart time.Duration `yaml:"fetch-crl-start"`
FetchCRLInterval time.Duration `yaml:"fetch-crl-interval"`
ResponseAnnounceTimeout time.Duration `yaml:"response-announce-timeout"`
ResponseDataTimeout time.Duration `yaml:"response-data-timeout"`
CRLDirectory string `yaml:"crl-directory"`
}
func (c *ClientConfig) UnmarshalYAML(n *yaml.Node) error {
@ -61,8 +67,11 @@ func (c *ClientConfig) UnmarshalYAML(n *yaml.Node) error {
Serial Serial `yaml:"serial"`
HealthInterval time.Duration `yaml:"health-interval"`
HealthStart time.Duration `yaml:"health-start"`
FetchCRLStart time.Duration `yaml:"fetch-crl-start"`
FetchCRLInterval time.Duration `yaml:"fetch-crl-interval"`
ResponseAnnounceTimeout time.Duration `yaml:"response-announce-timeout"`
ResponseDataTimeout time.Duration `yaml:"response-data-timeout"`
CRLDirectory string `yaml:"crl-directory"`
}{}
err := n.Decode(&data)
@ -96,6 +105,18 @@ func (c *ClientConfig) UnmarshalYAML(n *yaml.Node) error {
c.HealthStart = data.HealthStart
if data.FetchCRLInterval == 0 {
data.FetchCRLInterval = defaultFetchCRLInterval
}
c.FetchCRLInterval = data.FetchCRLInterval
if data.FetchCRLStart == 0 {
data.FetchCRLStart = defaultFetchCRLStart
}
c.FetchCRLStart = data.FetchCRLStart
if data.ResponseAnnounceTimeout == 0 {
data.ResponseAnnounceTimeout = defaultResponseAnnounceTimeout
}
@ -108,6 +129,12 @@ func (c *ClientConfig) UnmarshalYAML(n *yaml.Node) error {
c.ResponseDataTimeout = data.ResponseDataTimeout
if data.CRLDirectory == "" {
data.CRLDirectory = defaultCRLDirectory
}
c.CRLDirectory = data.CRLDirectory
return nil
}
@ -119,7 +146,7 @@ func New(clientConfigFile string) (*ClientConfig, error) {
defer func() { _ = configFile.Close() }()
clientConfig, err := loadConfiguration(configFile)
clientConfig, err := LoadConfiguration(configFile)
if err != nil {
return nil, fmt.Errorf("could not load client configuration from %s: %w", clientConfigFile, err)
}
@ -127,7 +154,7 @@ func New(clientConfigFile string) (*ClientConfig, error) {
return clientConfig, nil
}
func loadConfiguration(r io.Reader) (*ClientConfig, error) {
func LoadConfiguration(r io.Reader) (*ClientConfig, error) {
var config *ClientConfig
decoder := yaml.NewDecoder(r)

View file

@ -24,6 +24,8 @@ import (
"github.com/shamaton/msgpackgen/msgpack"
"github.com/sirupsen/logrus"
"git.cacert.org/cacert-gosignerclient/internal/client"
"git.cacert.org/cacert-gosigner/pkg/messages"
"git.cacert.org/cacert-gosigner/pkg/protocol"
@ -34,6 +36,7 @@ type SignerClientHandler struct {
logger *logrus.Logger
commands chan *protocol.Command
config *config.ClientConfig
clientCallback chan interface{}
}
func (s *SignerClientHandler) Send(command *protocol.Command, out chan []byte) error {
@ -47,7 +50,7 @@ func (s *SignerClientHandler) Send(command *protocol.Command, out chan []byte) e
return fmt.Errorf("could not marshal command annoucement: %w", err)
}
s.logger.WithField("announcement", command.Announce).Info("write command announcement")
s.logger.WithField("announcement", command.Announce).Debug("write command announcement")
s.logger.Trace("writing command announcement")
@ -58,7 +61,7 @@ func (s *SignerClientHandler) Send(command *protocol.Command, out chan []byte) e
return fmt.Errorf("could not marshal command data: %w", err)
}
s.logger.WithField("command", command.Command).Info("write command data")
s.logger.WithField("command", command.Command).Debug("write command data")
out <- frame
@ -103,6 +106,13 @@ func (s *SignerClientHandler) ResponseData(in chan []byte, response *protocol.Re
return fmt.Errorf("could not unmarshal fetch CRL response data: %w", err)
}
response.Response = &resp
case messages.RespError:
var resp messages.ErrorResponse
if err := msgpack.Unmarshal(frame, &resp); err != nil {
return fmt.Errorf("could not unmarshal error response data: %w", err)
}
response.Response = &resp
default:
return fmt.Errorf("unhandled response code %s", response.Announce.Code)
@ -118,19 +128,90 @@ func (s *SignerClientHandler) HandleResponse(response *protocol.Response) error
s.logger.WithField("response", response.Announce).Info("handled response")
s.logger.WithField("response", response).Debug("full response")
// TODO: add real implementations
switch r := response.Response.(type) {
case *messages.ErrorResponse:
s.logger.WithField("message", r.Message).Error("error from signer")
case *messages.HealthResponse:
s.handleHealthResponse(r)
case *messages.FetchCRLResponse:
s.handleFetchCRLResponse(r)
default:
s.logger.WithField("response", response).Warnf("unhandled response of type %T", response.Response)
}
return nil
}
func (s *SignerClientHandler) handleHealthResponse(r *messages.HealthResponse) {
signerInfo := client.SignerInfo{}
signerInfo.SignerHealth = r.Healthy
signerInfo.SignerVersion = r.Version
if !r.Healthy {
// it might be a good idea to notify monitoring if the signer is not OK
s.logger.Error("signer is not healthy")
}
for _, item := range r.Info {
if !item.Healthy {
s.logger.WithField("component", item.Source).Error("signer component is not healthy")
}
switch item.Source {
case "HSM":
signerInfo.CACertificates = make([]string, 0)
signerInfo.UsableProfiles = make(map[string][]client.Profile)
for certName, value := range item.MoreInfo {
certInfo, err := item.ParseCertificateInfo(value)
if err != nil {
s.logger.WithError(err).Error("could not parse certificate information")
break
}
s.logger.WithFields(map[string]interface{}{
"certificate": certName,
"signing": certInfo.Signing,
"profiles": certInfo.Profiles,
"status": certInfo.Status,
"valid-until": certInfo.ValidUntil,
}).Trace("certificate info")
signerInfo.CACertificates = append(signerInfo.CACertificates, certName)
if certInfo.Signing {
for _, profile := range certInfo.Profiles {
signerInfo.UsableProfiles[certName] = append(
signerInfo.UsableProfiles[certName],
client.Profile{Name: profile.Name, UseFor: string(profile.UseFor)},
)
}
}
}
default:
s.logger.WithField("source", item.Source).Warn("unhandled health source")
}
}
s.clientCallback <- signerInfo
}
func (s *SignerClientHandler) handleFetchCRLResponse(r *messages.FetchCRLResponse) {
s.clientCallback <- r
}
func New(
config *config.ClientConfig,
logger *logrus.Logger,
commands chan *protocol.Command,
clientCallback chan interface{},
) (protocol.ClientHandler, error) {
return &SignerClientHandler{
logger: logger,
config: config,
commands: commands,
clientCallback: clientCallback,
}, nil
}